feat(mcp): auto-import Claude memories, upgrade server instructions#107
feat(mcp): auto-import Claude memories, upgrade server instructions#107
Conversation
…CP tool MCP and REST API create projects with name-as-ID while the orchestrator uses sha256(absPath)[:12], causing duplicate project rows and fragmented memories. EnsureProject now auto-detects and merges same-name duplicates when called with a real filesystem path. - Extract project.HashID() for reusable hash ID computation - Add Store.MergeProject() to reassign all child records in a transaction - Auto-merge in EnsureProject when abs-path project meets name-as-ID dup - Add ghost_list_projects MCP tool for project discovery - Expand MCP Instructions field with workflow, categories, importance guide
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughAdded Claude Code auto-import on first project contact, a new MCP tool Changes
Sequence DiagramsequenceDiagram
participant Client
participant MCPServer
participant EnsureProject
participant Store
participant ClaudeImport
participant Database
Client->>MCPServer: request project context / first contact
MCPServer->>EnsureProject: resolve project_id (HashID / name resolution)
EnsureProject->>Store: CountMemories(projectID)
alt count == 0
EnsureProject->>ClaudeImport: Import(ctx, store, projectID, projectPath)
ClaudeImport-->>Store: Upsert imported memories (tags: claude-code, auto-import)
ClaudeImport-->>EnsureProject: return imported count
alt imported == 0 and LLM client present
EnsureProject->>MCPServer: spawn onboardProject(...)
end
end
alt duplicate detected (abs path vs name)
EnsureProject->>Store: MergeProject(oldID, newID)
Store->>Database: BEGIN
Database->>Database: UPDATE project_id in memories, conversations, tasks, decisions, token_usage, audit_log
Database->>Database: DELETE ghost_state[oldID], DELETE projects[oldID]
Database->>Store: COMMIT
Store-->>EnsureProject: merge success
end
EnsureProject-->>Client: project ensured / context returned
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
internal/memory/store.go (1)
117-121: Platform-specific path detection in auto-merge query.The condition
path NOT LIKE '/%'only detects non-absolute paths on Unix-like systems. On Windows, absolute paths start with a drive letter (e.g.,C:\), so this heuristic would incorrectly treat Windows absolute paths as MCP-created paths.If Windows support is needed, consider using a more robust check or storing a flag indicating the path type.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/memory/store.go` around lines 117 - 121, The SQL heuristic `path NOT LIKE '/%'` in the QueryRowContext call incorrectly treats Windows absolute paths as non-absolute; update the logic to be platform-agnostic by recording path metadata instead of relying on string patterns: add a boolean column (e.g., projects.is_mcp_created or projects.is_absolute) and set it when inserting/updating projects, then change the query in the function that calls s.db.QueryRowContext (the query that scans into dupID) to filter on that boolean flag instead of `path NOT LIKE '/%'`; ensure all insert/update code paths that create project rows (where name, id, path are set) also set the new flag.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/mcpserver/mcpserver.go`:
- Around line 569-572: In the loop that builds the project list (the for _, p :=
range projects block) replace the sb.WriteString(fmt.Sprintf(...)) pattern with
a direct fmt.Fprintf call to write to sb (fmt.Fprintf(sb, "- **%s** (id: `%s`,
path: `%s`) — %d memories\n", p.Name, p.ID, p.Path, count)). Use sb as the
io.Writer and handle/ignore the returned (int, error) per project (e.g., assign
to _, _ or check and log) to satisfy staticcheck QF1012 and avoid the
unnecessary intermediate allocation; keep the call site referencing sb and
s.store.CountMemories unchanged.
---
Nitpick comments:
In `@internal/memory/store.go`:
- Around line 117-121: The SQL heuristic `path NOT LIKE '/%'` in the
QueryRowContext call incorrectly treats Windows absolute paths as non-absolute;
update the logic to be platform-agnostic by recording path metadata instead of
relying on string patterns: add a boolean column (e.g., projects.is_mcp_created
or projects.is_absolute) and set it when inserting/updating projects, then
change the query in the function that calls s.db.QueryRowContext (the query that
scans into dupID) to filter on that boolean flag instead of `path NOT LIKE
'/%'`; ensure all insert/update code paths that create project rows (where name,
id, path are set) also set the new flag.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7688f3ff-2bc8-43d5-a35a-f9087f5779c9
📒 Files selected for processing (7)
internal/mcpserver/mcpserver.gointernal/memory/store.gointernal/memory/store_test.gointernal/project/context.gointernal/project/context_test.gointernal/provider/provider.gointernal/server/server_test.go
Fixes staticcheck QF1012 in ghost_list_projects tool.
Add internal/claudeimport package that reads Claude Code's auto-memory files (~/.claude/projects/*/memory/*.md) on first project contact. Read-only — never modifies Claude's files. Parses YAML frontmatter, maps types to Ghost categories, uses upsert for dedup. Hook into orchestrator cold-start (CLI path) and ghost_project_context handler (MCP path). If Claude memories exist, import them before falling back to Haiku-based onboarding. Upgrade MCP ServerOptions.Instructions from 15 lines to comprehensive guidance covering proactive save triggers, category examples, importance guidelines, and cross-project workflows. Update 3 tool descriptions to encourage proactive memory saving. Update README: add ghost_list_projects to MCP tools table (12→13), document claudeimport package in architecture, note auto-import in feature list.
| } | ||
| encoded := encodeProjectPath(projectPath) | ||
| dir := filepath.Join(home, ".claude", "projects", encoded, "memory") | ||
| if info, err := os.Stat(dir); err != nil || !info.IsDir() { |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Copilot Autofix
AI about 2 months ago
Copilot could not generate an autofix suggestion
Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.
| // Ghost category, and importance. Returns skip=true for files that should | ||
| // not be imported. | ||
| func ParseMemoryFile(path string) (content, category string, importance float32, skip bool, err error) { | ||
| data, err := os.ReadFile(path) |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Copilot Autofix
AI about 2 months ago
Copilot could not generate an autofix suggestion
Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.
|
|
||
| // importFromDir imports all Claude Code memories from a specific directory. | ||
| func importFromDir(ctx context.Context, store provider.MemoryStore, projectID, dir string, logger *slog.Logger) (int, error) { | ||
| entries, err := os.ReadDir(dir) |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, to fix uncontrolled path usage you ensure that any path derived from user input is constrained to a safe root and validated as such before using it with filesystem APIs. This usually means joining under a fixed base directory, resolving to an absolute path, and confirming that the result still lies under that base (for example, via filepath.Abs + prefix check), or normalizing components and rejecting unwanted patterns.
In this codebase, ClaudeMemoryDir already constructs dir as filepath.Join(home, ".claude", "projects", encoded, "memory"). To make the path use explicitly safe and clear to both CodeQL and future maintainers, we can add a normalization / containment check there (or just before os.ReadDir). The safest minimal change—without altering observable behavior—is to normalize the constructed dir to an absolute path, build a canonical base ($HOME/.claude/projects) the same way, and ensure that dir has base as a prefix. If it fails the check, we return "" so no import or directory read occurs. This keeps behavior the same for valid inputs but prevents any crafted value from escaping the intended Claude projects directory and should address the CodeQL concern.
Concretely:
- Edit
ClaudeMemoryDirininternal/claudeimport/import.go. - After constructing
dir, compute:base := filepath.Join(home, ".claude", "projects")baseAbs := filepath.Clean(base)dirAbs := filepath.Clean(dir)
- If
dirAbsis not equal tobaseAbsand does not havebaseAbs+string(os.PathSeparator)as a prefix, return"". - Keep the existing
os.Stat/IsDircheck afterwards.
This uses only existing imports (os,filepath) and doesn’t require changes elsewhere.
| @@ -57,11 +57,21 @@ | ||
| return "" | ||
| } | ||
| encoded := encodeProjectPath(projectPath) | ||
| dir := filepath.Join(home, ".claude", "projects", encoded, "memory") | ||
| if info, err := os.Stat(dir); err != nil || !info.IsDir() { | ||
| // Construct the expected memory directory under the fixed Claude projects root. | ||
| base := filepath.Join(home, ".claude", "projects") | ||
| dir := filepath.Join(base, encoded, "memory") | ||
|
|
||
| // Normalize and ensure dir is within the Claude projects base directory. | ||
| baseAbs := filepath.Clean(base) | ||
| dirAbs := filepath.Clean(dir) | ||
| if dirAbs != baseAbs && !strings.HasPrefix(dirAbs, baseAbs+string(os.PathSeparator)) { | ||
| return "" | ||
| } | ||
| return dir | ||
|
|
||
| if info, err := os.Stat(dirAbs); err != nil || !info.IsDir() { | ||
| return "" | ||
| } | ||
| return dirAbs | ||
| } | ||
|
|
||
| // skipFile returns true if the filename should not be imported. |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/claudeimport/import_test.go`:
- Around line 289-293: Replace the unchecked defer db.Close() with a checked
cleanup: call t.Cleanup(func() { if err := db.Close(); err != nil {
t.Fatalf("closing memory DB: %v", err) } }) so any error returned by
memory.OpenDB's Close is reported; update the block where
memory.OpenDB(":memory:") is called and remove the plain defer db.Close().
In `@internal/claudeimport/import.go`:
- Around line 46-60: encodeProjectPath and ClaudeMemoryDir fail for native
Windows paths because backslashes and drive letters make the joined path rooted;
normalize the projectPath in ClaudeMemoryDir (or inside encodeProjectPath) by
calling filepath.Clean and filepath.ToSlash, strip any Windows volume name (use
filepath.VolumeName to detect and strings.TrimPrefix to remove it), then replace
remaining "/" with "-" (and TrimLeft any leading "-" so a leading slash or
removed drive doesn't produce a leading dash). Update the encodeProjectPath
and/or ClaudeMemoryDir flow to operate on the normalized path so C:\work\ghost
becomes "C-work-ghost" or "work-ghost" consistently and the final
filepath.Join(home, ".claude", "projects", encoded, "memory") is not treated as
rooted on Windows.
- Around line 138-149: parseFrontmatter currently only recognizes LF delimiters
("---\n") so CRLF frontmatter ("---\r\n") is treated as body; update
parseFrontmatter to accept both LF and CRLF boundaries by checking for both
starting delimiters (e.g., "---\n" and "---\r\n") and searching for matching
closing delimiters ("\n---" and "\r\n---") when computing end and slicing body;
ensure fm extraction and body = raw[...] calculations account for the chosen
delimiter length (2 vs 4 chars) so indices remain correct and the returned
name/description/fileType parsing still uses the cleaned fm.
In `@internal/mcpserver/mcpserver.go`:
- Around line 271-277: The code swallows the error returned by
claudeimport.Import which hides import failures; update the clause in
mcpserver.go (around CountMemories/ListProjects logic) to capture the returned
error from claudeimport.Import(ctx, s.store, args.ProjectID, p.Path, s.logger)
and handle it instead of discarding it — at minimum log it via s.logger (or
return it to the caller) so the errcheck violation is resolved and first-contact
import failures are visible; ensure you modify the block that calls
CountMemories, ListProjects and claudeimport.Import to check the import error
and act accordingly.
- Around line 86-87: ghost_memory_save is resolving name-based args.ProjectID to
the hash ID then calling EnsureProject(path=args.ProjectID), which overwrites
the existing projects.path with the hash; update ghost_memory_save so that if
the project already exists (lookup via the resolved ID or EnsureProject result
indicates existing), you do not replace the stored Path/Name—either skip the
EnsureProject call or call EnsureProject with the original filesystem path/name
preserved and avoid updating projects.path when a row already exists; ensure
this check uses the same identifiers used elsewhere (args.ProjectID, the
resolved project hash, and the EnsureProject return) to detect existing projects
and preserve projects.path so ghost_list_projects and import/merge gates keep
working.
In `@internal/orchestrator/orchestrator.go`:
- Around line 84-96: The goroutine uses the initial CountMemories result to
decide onboarding, which can race with other imports; after running
claudeimport.Import (or if importErr != nil), re-query the store by calling
o.store.CountMemories(ctx, projCtx.ID) inside the goroutine and only call
onboardProject(bgCtx, o.client, o.store, projCtx, o.logger) if that fresh count
is still 0 and o.client != nil; also handle and log any count error (treat
non-nil error as conservatively non-zero to avoid onboarding duplication).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7b59b24d-e107-42ba-b5a7-d32aa8922087
📒 Files selected for processing (5)
README.mdinternal/claudeimport/import.gointernal/claudeimport/import_test.gointernal/mcpserver/mcpserver.gointernal/orchestrator/orchestrator.go
| db, err := memory.OpenDB(":memory:") | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| defer db.Close() |
There was a problem hiding this comment.
Check db.Close() in test cleanup.
CI is failing on Line 293 because the db.Close() error is ignored. Wrap it in t.Cleanup or a checked defer so cleanup failures are still reported.
🔧 Suggested fix
db, err := memory.OpenDB(":memory:")
if err != nil {
t.Fatal(err)
}
- defer db.Close()
+ t.Cleanup(func() {
+ if err := db.Close(); err != nil {
+ t.Fatalf("close db: %v", err)
+ }
+ })📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| db, err := memory.OpenDB(":memory:") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer db.Close() | |
| db, err := memory.OpenDB(":memory:") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| t.Cleanup(func() { | |
| if err := db.Close(); err != nil { | |
| t.Fatalf("close db: %v", err) | |
| } | |
| }) |
🧰 Tools
🪛 GitHub Actions: ci
[error] 293-293: golangci-lint (errcheck): Error return value of db.Close is not checked.
🪛 GitHub Check: build-and-test
[failure] 293-293:
Error return value of db.Close is not checked (errcheck)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/claudeimport/import_test.go` around lines 289 - 293, Replace the
unchecked defer db.Close() with a checked cleanup: call t.Cleanup(func() { if
err := db.Close(); err != nil { t.Fatalf("closing memory DB: %v", err) } }) so
any error returned by memory.OpenDB's Close is reported; update the block where
memory.OpenDB(":memory:") is called and remove the plain defer db.Close().
| // encodeProjectPath converts an absolute path to Claude's directory name format. | ||
| // E.g., "/home/wayne/git/ghost" → "-home-wayne-git-ghost". | ||
| func encodeProjectPath(projectPath string) string { | ||
| return strings.ReplaceAll(projectPath, "/", "-") | ||
| } | ||
|
|
||
| // ClaudeMemoryDir returns the path to Claude Code's memory directory for the | ||
| // given absolute project path. Returns "" if the directory does not exist. | ||
| func ClaudeMemoryDir(projectPath string) string { | ||
| home, err := os.UserHomeDir() | ||
| if err != nil { | ||
| return "" | ||
| } | ||
| encoded := encodeProjectPath(projectPath) | ||
| dir := filepath.Join(home, ".claude", "projects", encoded, "memory") |
There was a problem hiding this comment.
Normalize Windows paths before encoding Claude's project directory.
encodeProjectPath only rewrites /. For a native Windows path like C:\work\ghost, the encoded value still contains a drive prefix and backslashes, so filepath.Join treats it as rooted and ClaudeMemoryDir no longer points under ~/.claude/projects/.../memory. That makes the new auto-import path effectively POSIX-only.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/claudeimport/import.go` around lines 46 - 60, encodeProjectPath and
ClaudeMemoryDir fail for native Windows paths because backslashes and drive
letters make the joined path rooted; normalize the projectPath in
ClaudeMemoryDir (or inside encodeProjectPath) by calling filepath.Clean and
filepath.ToSlash, strip any Windows volume name (use filepath.VolumeName to
detect and strings.TrimPrefix to remove it), then replace remaining "/" with "-"
(and TrimLeft any leading "-" so a leading slash or removed drive doesn't
produce a leading dash). Update the encodeProjectPath and/or ClaudeMemoryDir
flow to operate on the normalized path so C:\work\ghost becomes "C-work-ghost"
or "work-ghost" consistently and the final filepath.Join(home, ".claude",
"projects", encoded, "memory") is not treated as rooted on Windows.
| func parseFrontmatter(raw string) (name, description, fileType, body string) { | ||
| if !strings.HasPrefix(raw, "---\n") { | ||
| return "", "", "", raw | ||
| } | ||
|
|
||
| end := strings.Index(raw[4:], "\n---") | ||
| if end < 0 { | ||
| return "", "", "", raw | ||
| } | ||
|
|
||
| fm := raw[4 : 4+end] | ||
| body = raw[4+end+4:] // skip closing "---\n" |
There was a problem hiding this comment.
CRLF frontmatter is parsed as plain markdown.
parseFrontmatter only recognizes ---\n delimiters. Files written with CRLF (---\r\n) fall through the “no frontmatter” path, so the YAML header ends up in the stored content and the Claude type never maps to the intended Ghost category/importance.
🔧 Suggested fix
func parseFrontmatter(raw string) (name, description, fileType, body string) {
+ raw = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(raw)
if !strings.HasPrefix(raw, "---\n") {
return "", "", "", raw
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func parseFrontmatter(raw string) (name, description, fileType, body string) { | |
| if !strings.HasPrefix(raw, "---\n") { | |
| return "", "", "", raw | |
| } | |
| end := strings.Index(raw[4:], "\n---") | |
| if end < 0 { | |
| return "", "", "", raw | |
| } | |
| fm := raw[4 : 4+end] | |
| body = raw[4+end+4:] // skip closing "---\n" | |
| func parseFrontmatter(raw string) (name, description, fileType, body string) { | |
| raw = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(raw) | |
| if !strings.HasPrefix(raw, "---\n") { | |
| return "", "", "", raw | |
| } | |
| end := strings.Index(raw[4:], "\n---") | |
| if end < 0 { | |
| return "", "", "", raw | |
| } | |
| fm := raw[4 : 4+end] | |
| body = raw[4+end+4:] // skip closing "---\n" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/claudeimport/import.go` around lines 138 - 149, parseFrontmatter
currently only recognizes LF delimiters ("---\n") so CRLF frontmatter
("---\r\n") is treated as body; update parseFrontmatter to accept both LF and
CRLF boundaries by checking for both starting delimiters (e.g., "---\n" and
"---\r\n") and searching for matching closing delimiters ("\n---" and "\r\n---")
when computing end and slicing body; ensure fm extraction and body = raw[...]
calculations account for the chosen delimiter length (2 vs 4 chars) so indices
remain correct and the returned name/description/fileType parsing still uses the
cleaned fm.
| ## Project IDs | ||
| Pass the project name (e.g. "ghost", "roller") as project_id. Ghost resolves names to internal IDs automatically.` |
There was a problem hiding this comment.
Name-based project_id saves currently overwrite the real project path.
The new instructions tell MCP clients to pass project names, but ghost_memory_save resolves that name to the hash ID and then calls EnsureProject with path=args.ProjectID. On Line 225 that updates an existing row's absolute projects.path to the hash, so ghost_list_projects loses the filesystem path and the absolute-path gates for Claude import / auto-merge stop working until another TUI session repairs it. Please preserve the stored Path/Name when the project already exists, or skip EnsureProject in that case.
Also applies to: 199-199
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/mcpserver/mcpserver.go` around lines 86 - 87, ghost_memory_save is
resolving name-based args.ProjectID to the hash ID then calling
EnsureProject(path=args.ProjectID), which overwrites the existing projects.path
with the hash; update ghost_memory_save so that if the project already exists
(lookup via the resolved ID or EnsureProject result indicates existing), you do
not replace the stored Path/Name—either skip the EnsureProject call or call
EnsureProject with the original filesystem path/name preserved and avoid
updating projects.path when a row already exists; ensure this check uses the
same identifiers used elsewhere (args.ProjectID, the resolved project hash, and
the EnsureProject return) to detect existing projects and preserve projects.path
so ghost_list_projects and import/merge gates keep working.
| // First-contact import: if project has zero memories, try importing | ||
| // from Claude Code's auto-memory files (read-only, one-time). | ||
| if cnt, cntErr := s.store.CountMemories(ctx, args.ProjectID); cntErr == nil && cnt == 0 { | ||
| if projects, lErr := s.store.ListProjects(ctx); lErr == nil { | ||
| for _, p := range projects { | ||
| if p.ID == args.ProjectID && filepath.IsAbs(p.Path) { | ||
| claudeimport.Import(ctx, s.store, args.ProjectID, p.Path, s.logger) |
There was a problem hiding this comment.
Do not drop the Claude import error here.
CI is already failing on this errcheck violation, and swallowing the error makes a broken first-contact import look like a legitimate “no memories found” case to the client.
🔧 Suggested fix
for _, p := range projects {
if p.ID == args.ProjectID && filepath.IsAbs(p.Path) {
- claudeimport.Import(ctx, s.store, args.ProjectID, p.Path, s.logger)
+ if _, err := claudeimport.Import(ctx, s.store, args.ProjectID, p.Path, s.logger); err != nil {
+ s.logger.Warn("claude memory import failed", "project", p.Name, "path", p.Path, "error", err)
+ }
break
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // First-contact import: if project has zero memories, try importing | |
| // from Claude Code's auto-memory files (read-only, one-time). | |
| if cnt, cntErr := s.store.CountMemories(ctx, args.ProjectID); cntErr == nil && cnt == 0 { | |
| if projects, lErr := s.store.ListProjects(ctx); lErr == nil { | |
| for _, p := range projects { | |
| if p.ID == args.ProjectID && filepath.IsAbs(p.Path) { | |
| claudeimport.Import(ctx, s.store, args.ProjectID, p.Path, s.logger) | |
| // First-contact import: if project has zero memories, try importing | |
| // from Claude Code's auto-memory files (read-only, one-time). | |
| if cnt, cntErr := s.store.CountMemories(ctx, args.ProjectID); cntErr == nil && cnt == 0 { | |
| if projects, lErr := s.store.ListProjects(ctx); lErr == nil { | |
| for _, p := range projects { | |
| if p.ID == args.ProjectID && filepath.IsAbs(p.Path) { | |
| if _, err := claudeimport.Import(ctx, s.store, args.ProjectID, p.Path, s.logger); err != nil { | |
| s.logger.Warn("claude memory import failed", "project", p.Name, "path", p.Path, "error", err) | |
| } | |
| break | |
| } | |
| } |
🧰 Tools
🪛 GitHub Check: build-and-test
[failure] 277-277:
Error return value of claudeimport.Import is not checked (errcheck)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/mcpserver/mcpserver.go` around lines 271 - 277, The code swallows
the error returned by claudeimport.Import which hides import failures; update
the clause in mcpserver.go (around CountMemories/ListProjects logic) to capture
the returned error from claudeimport.Import(ctx, s.store, args.ProjectID,
p.Path, s.logger) and handle it instead of discarding it — at minimum log it via
s.logger (or return it to the caller) so the errcheck violation is resolved and
first-contact import failures are visible; ensure you modify the block that
calls CountMemories, ListProjects and claudeimport.Import to check the import
error and act accordingly.
| // Cold-start: import Claude Code memories first, fall back to LLM onboarding. | ||
| count, err := o.store.CountMemories(ctx, projCtx.ID) | ||
| if err == nil && count == 0 { | ||
| go onboardProject(context.Background(), o.client, o.store, projCtx, o.logger) | ||
| go func() { | ||
| bgCtx := context.Background() | ||
| imported, importErr := claudeimport.Import(bgCtx, o.store, projCtx.ID, projCtx.Path, o.logger) | ||
| if importErr != nil { | ||
| o.logger.Warn("claude memory import failed", "project", projCtx.Name, "error", importErr) | ||
| } | ||
| if imported == 0 && o.client != nil { | ||
| onboardProject(bgCtx, o.client, o.store, projCtx, o.logger) | ||
| } | ||
| }() |
There was a problem hiding this comment.
Re-check the memory count before calling the onboarding fallback.
The zero-count decision is made on Line 85, but the fallback runs later in a goroutine. Another first-contact path can import/save memories between Line 85 and Line 93, and this still calls onboardProject, which burns an extra LLM call and writes a second batch of onboarding memories.
🔧 Suggested fix
if importErr != nil {
o.logger.Warn("claude memory import failed", "project", projCtx.Name, "error", importErr)
}
if imported == 0 && o.client != nil {
- onboardProject(bgCtx, o.client, o.store, projCtx, o.logger)
+ if count, err := o.store.CountMemories(bgCtx, projCtx.ID); err == nil && count == 0 {
+ onboardProject(bgCtx, o.client, o.store, projCtx, o.logger)
+ }
}
}()
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Cold-start: import Claude Code memories first, fall back to LLM onboarding. | |
| count, err := o.store.CountMemories(ctx, projCtx.ID) | |
| if err == nil && count == 0 { | |
| go onboardProject(context.Background(), o.client, o.store, projCtx, o.logger) | |
| go func() { | |
| bgCtx := context.Background() | |
| imported, importErr := claudeimport.Import(bgCtx, o.store, projCtx.ID, projCtx.Path, o.logger) | |
| if importErr != nil { | |
| o.logger.Warn("claude memory import failed", "project", projCtx.Name, "error", importErr) | |
| } | |
| if imported == 0 && o.client != nil { | |
| onboardProject(bgCtx, o.client, o.store, projCtx, o.logger) | |
| } | |
| }() | |
| // Cold-start: import Claude Code memories first, fall back to LLM onboarding. | |
| count, err := o.store.CountMemories(ctx, projCtx.ID) | |
| if err == nil && count == 0 { | |
| go func() { | |
| bgCtx := context.Background() | |
| imported, importErr := claudeimport.Import(bgCtx, o.store, projCtx.ID, projCtx.Path, o.logger) | |
| if importErr != nil { | |
| o.logger.Warn("claude memory import failed", "project", projCtx.Name, "error", importErr) | |
| } | |
| if imported == 0 && o.client != nil { | |
| if count, err := o.store.CountMemories(bgCtx, projCtx.ID); err == nil && count == 0 { | |
| onboardProject(bgCtx, o.client, o.store, projCtx, o.logger) | |
| } | |
| } | |
| }() | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/orchestrator/orchestrator.go` around lines 84 - 96, The goroutine
uses the initial CountMemories result to decide onboarding, which can race with
other imports; after running claudeimport.Import (or if importErr != nil),
re-query the store by calling o.store.CountMemories(ctx, projCtx.ID) inside the
goroutine and only call onboardProject(bgCtx, o.client, o.store, projCtx,
o.logger) if that fresh count is still 0 and o.client != nil; also handle and
log any count error (treat non-nil error as conservatively non-zero to avoid
onboarding duplication).
Summary
internal/claudeimport/package reads Claude Code's~/.claude/projects/*/memory/*.mdfiles on first project contact. Read-only — never modifies Claude's files. Hooks into both CLI cold-start (orchestrator) and MCP first-contact (ghost_project_context).ServerOptions.Instructionsfrom 15 lines to full guidance covering proactive save triggers, category examples, importance scale, cross-project workflows. Any Ghost user gets the full experience without manual CLAUDE.md edits.Test plan
go vet ./...cleango test ./...all pass (19 new tests in claudeimport)CGO_ENABLED=0)Summary by CodeRabbit
New Features
Bug Fixes
Docs
Tests