Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

ai-memory-cli

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ai-memory-cli - npm Package Compare versions

Comparing version
1.1.0
to
1.1.2
+5
-2
dist/public.js

@@ -264,3 +264,4 @@ // src/sources/cursor.ts

}
const { DatabaseSync } = await import("sqlite");
const sqliteMod = "node:sqlite";
const { DatabaseSync } = await import(sqliteMod);
const db = new DatabaseSync(tmpDb, { readonly: true });

@@ -278,3 +279,5 @@ const row = db.prepare("SELECT value FROM ItemTable WHERE key = ?").get("composer.composerHeaders");

return map;
} catch {
} catch (err) {
process.stderr.write(`[ai-memory] title map load failed: ${err}
`);
return /* @__PURE__ */ new Map();

@@ -281,0 +284,0 @@ }

@@ -1,1 +0,1 @@

{"version":3,"sources":["../src/sources/cursor.ts","../src/sources/claude-code.ts","../src/sources/detector.ts"],"sourcesContent":["import { readdir, readFile, stat, copyFile, mkdir } from \"node:fs/promises\";\nimport { join, basename } from \"node:path\";\nimport { homedir, tmpdir } from \"node:os\";\nimport type {\n Source,\n ConversationMeta,\n Conversation,\n ConversationTurn,\n} from \"../types.js\";\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nexport class CursorSource implements Source {\n readonly type = \"cursor\" as const;\n private basePath: string;\n private projectName: string | undefined;\n\n constructor(projectName?: string) {\n this.projectName = projectName;\n this.basePath = join(homedir(), \".cursor\", \"projects\");\n }\n\n async detect(): Promise<boolean> {\n try {\n const projectDir = await this.resolveProjectDir();\n if (!projectDir) return false;\n const transcriptsDir = join(projectDir, \"agent-transcripts\");\n const s = await stat(transcriptsDir);\n return s.isDirectory();\n } catch {\n return false;\n }\n }\n\n async listConversations(): Promise<ConversationMeta[]> {\n const transcriptsDir = await this.getTranscriptsDir();\n if (!transcriptsDir) return [];\n\n // Load real titles from Cursor's workspace DB (best-effort)\n const titleMap = await this.loadTitleMap();\n\n const entries = await readdir(transcriptsDir, { withFileTypes: true });\n const conversations: ConversationMeta[] = [];\n\n for (const entry of entries) {\n if (entry.isDirectory() && UUID_RE.test(entry.name)) {\n const meta = await this.readConversationMeta(\n transcriptsDir,\n entry.name,\n titleMap\n );\n if (meta) conversations.push(meta);\n } else if (entry.isFile() && entry.name.endsWith(\".txt\")) {\n const meta = await this.readLegacyMeta(transcriptsDir, entry.name);\n if (meta) conversations.push(meta);\n }\n }\n\n return conversations.sort((a, b) => b.modifiedAt - a.modifiedAt);\n }\n\n async loadConversation(meta: ConversationMeta): Promise<Conversation> {\n if (meta.filePath.endsWith(\".txt\")) {\n return this.loadLegacyConversation(meta);\n }\n return this.loadJsonlConversation(meta);\n }\n\n // --- JSONL format (current) ---\n\n private async loadJsonlConversation(\n meta: ConversationMeta\n ): Promise<Conversation> {\n const raw = await readFile(meta.filePath, \"utf-8\");\n const turns = this.parseJsonlContent(raw);\n return { meta: { ...meta, turnCount: turns.length }, turns };\n }\n\n parseJsonlContent(raw: string): ConversationTurn[] {\n const turns: ConversationTurn[] = [];\n\n for (const line of raw.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n try {\n const obj = JSON.parse(trimmed);\n const role = obj.role as \"user\" | \"assistant\";\n if (role !== \"user\" && role !== \"assistant\") continue;\n\n const textParts = this.extractTextParts(obj.message?.content);\n if (!textParts) continue;\n\n turns.push({ role, text: textParts });\n } catch {\n // Skip malformed lines\n }\n }\n\n return turns;\n }\n\n private extractTextParts(content: unknown): string | null {\n if (!Array.isArray(content)) return null;\n\n const texts: string[] = [];\n for (const part of content) {\n if (\n part &&\n typeof part === \"object\" &&\n \"type\" in part &&\n part.type === \"text\" &&\n \"text\" in part &&\n typeof part.text === \"string\"\n ) {\n texts.push(this.cleanUserQuery(part.text));\n }\n }\n\n const joined = texts.join(\"\\n\").trim();\n return joined || null;\n }\n\n private cleanUserQuery(text: string): string {\n const match = text.match(/<user_query>\\s*([\\s\\S]*?)\\s*<\\/user_query>/);\n if (match) return match[1].trim();\n return text;\n }\n\n // --- Legacy .txt format ---\n\n private async loadLegacyConversation(\n meta: ConversationMeta\n ): Promise<Conversation> {\n const raw = await readFile(meta.filePath, \"utf-8\");\n const turns = this.parseLegacyContent(raw);\n return { meta: { ...meta, turnCount: turns.length }, turns };\n }\n\n parseLegacyContent(raw: string): ConversationTurn[] {\n const turns: ConversationTurn[] = [];\n const blocks = raw.split(/^(user|assistant):\\s*$/m);\n\n let currentRole: \"user\" | \"assistant\" | null = null;\n for (const block of blocks) {\n const trimmed = block.trim();\n if (trimmed === \"user\") {\n currentRole = \"user\";\n } else if (trimmed === \"assistant\") {\n currentRole = \"assistant\";\n } else if (currentRole && trimmed) {\n turns.push({\n role: currentRole,\n text: this.cleanUserQuery(trimmed),\n });\n currentRole = null;\n }\n }\n\n return turns;\n }\n\n // --- Metadata helpers ---\n\n private async readConversationMeta(\n transcriptsDir: string,\n uuid: string,\n titleMap: Map<string, string> = new Map()\n ): Promise<ConversationMeta | null> {\n const jsonlPath = join(transcriptsDir, uuid, `${uuid}.jsonl`);\n try {\n const fileStat = await stat(jsonlPath);\n const raw = await readFile(jsonlPath, \"utf-8\");\n // Prefer real title from DB; fall back to first user message\n const title = titleMap.get(uuid) ?? this.extractTitle(raw, uuid);\n const turnCount = this.countTurns(raw);\n\n return {\n id: uuid,\n source: \"cursor\",\n filePath: jsonlPath,\n title,\n modifiedAt: fileStat.mtimeMs,\n turnCount,\n };\n } catch {\n return null;\n }\n }\n\n private async readLegacyMeta(\n transcriptsDir: string,\n filename: string\n ): Promise<ConversationMeta | null> {\n const filePath = join(transcriptsDir, filename);\n try {\n const fileStat = await stat(filePath);\n const id = basename(filename, \".txt\");\n const raw = await readFile(filePath, \"utf-8\");\n const title = this.extractLegacyTitle(raw, id);\n\n return {\n id,\n source: \"cursor\",\n filePath,\n title,\n modifiedAt: fileStat.mtimeMs,\n turnCount: (raw.match(/^(user|assistant):\\s*$/m) || []).length,\n };\n } catch {\n return null;\n }\n }\n\n private extractTitle(jsonlRaw: string, fallbackId: string): string {\n for (const line of jsonlRaw.split(\"\\n\").slice(0, 5)) {\n try {\n const obj = JSON.parse(line.trim());\n if (obj.role === \"user\") {\n const text = this.extractTextParts(obj.message?.content);\n if (text) {\n const cleaned = text.replace(/\\s+/g, \" \").trim();\n return cleaned.length > 60\n ? cleaned.slice(0, 57) + \"...\"\n : cleaned;\n }\n }\n } catch {\n // skip\n }\n }\n return fallbackId.slice(0, 8);\n }\n\n private extractLegacyTitle(raw: string, fallbackId: string): string {\n const match = raw.match(\n /<user_query>\\s*([\\s\\S]*?)\\s*<\\/user_query>/\n );\n if (match) {\n const text = match[1].replace(/\\s+/g, \" \").trim();\n return text.length > 60 ? text.slice(0, 57) + \"...\" : text;\n }\n return fallbackId.slice(0, 8);\n }\n\n private countTurns(jsonlRaw: string): number {\n let count = 0;\n for (const line of jsonlRaw.split(\"\\n\")) {\n if (line.includes('\"role\"')) count++;\n }\n return count;\n }\n\n // --- Project directory resolution ---\n\n private async resolveProjectDir(): Promise<string | null> {\n if (this.projectName) {\n const dir = join(this.basePath, this.projectName);\n try {\n const s = await stat(dir);\n if (s.isDirectory()) return dir;\n } catch {\n // fall through\n }\n }\n\n try {\n const projects = await readdir(this.basePath, { withFileTypes: true });\n\n // Collect candidates with transcript dirs\n type Candidate = { dir: string; count: number; mtime: number };\n const candidates: Candidate[] = [];\n\n for (const p of projects) {\n if (!p.isDirectory()) continue;\n // Skip temp/AppData/numeric-only project directories\n if (this.isTempProject(p.name)) continue;\n\n const transcriptsDir = join(this.basePath, p.name, \"agent-transcripts\");\n try {\n const s = await stat(transcriptsDir);\n if (!s.isDirectory()) continue;\n\n const entries = await readdir(transcriptsDir);\n const count = entries.length;\n if (count === 0) continue;\n\n candidates.push({\n dir: join(this.basePath, p.name),\n count,\n mtime: s.mtimeMs,\n });\n } catch {\n // no transcripts\n }\n }\n\n if (candidates.length === 0) return null;\n\n // Prefer the project matching the current working directory\n const cwd = process.cwd().replace(/\\\\/g, \"-\").replace(/:/g, \"\").toLowerCase();\n const cwdMatch = candidates.find((c) =>\n cwd.endsWith(basename(c.dir).toLowerCase())\n );\n if (cwdMatch) return cwdMatch.dir;\n\n // Otherwise pick the project with the most conversations\n candidates.sort((a, b) => b.count - a.count || b.mtime - a.mtime);\n return candidates[0].dir;\n } catch {\n // .cursor/projects doesn't exist\n }\n\n return null;\n }\n\n private isTempProject(name: string): boolean {\n // Skip numeric-only names (e.g. \"1775614169037\")\n if (/^\\d+$/.test(name)) return true;\n // Skip AppData/Temp paths encoded as project names\n if (name.toLowerCase().includes(\"appdata\")) return true;\n if (name.toLowerCase().includes(\"temp\")) return true;\n if (name.toLowerCase().includes(\"local-temp\")) return true;\n return false;\n }\n\n private async getTranscriptsDir(): Promise<string | null> {\n const projectDir = await this.resolveProjectDir();\n if (!projectDir) return null;\n return join(projectDir, \"agent-transcripts\");\n }\n\n // --- Title map from Cursor's workspace SQLite DB ---\n\n /**\n * Returns a map of { [composerId/UUID]: title } by reading\n * Cursor's GLOBAL state.vscdb (key \"composer.composerHeaders\").\n * This file contains titles for ALL workspaces.\n * Uses node:sqlite (Node.js 22+). Silently returns empty map on any error.\n */\n private async loadTitleMap(): Promise<Map<string, string>> {\n try {\n const dbPath = this.getGlobalDbPath();\n if (!dbPath) return new Map();\n\n // Copy db + WAL + SHM to temp to avoid lock conflicts when Cursor is running\n const ts = Date.now();\n const tmpDb = join(tmpdir(), `ai-memory-global-${ts}.vscdb`);\n await copyFile(dbPath, tmpDb);\n for (const ext of [\"-wal\", \"-shm\"]) {\n try { await copyFile(dbPath + ext, tmpDb + ext); } catch { /* optional */ }\n }\n\n const { DatabaseSync } = await import(\"node:sqlite\" as string);\n const db = new (DatabaseSync as new (p: string, opts?: Record<string, unknown>) => {\n prepare(sql: string): { get(...args: unknown[]): unknown };\n close(): void;\n })(tmpDb, { readonly: true });\n\n const row = db\n .prepare(\"SELECT value FROM ItemTable WHERE key = ?\")\n .get(\"composer.composerHeaders\") as { value?: string } | undefined;\n db.close();\n\n if (!row?.value) return new Map();\n\n const data = JSON.parse(row.value) as {\n allComposers?: Array<{ composerId?: string; name?: string }>;\n };\n\n const map = new Map<string, string>();\n for (const c of data.allComposers ?? []) {\n if (c.composerId && c.name && c.name.trim()) {\n map.set(c.composerId, c.name.trim());\n }\n }\n return map;\n } catch {\n return new Map();\n }\n }\n\n private getGlobalDbPath(): string | null {\n if (process.platform === \"win32\") {\n const appData = process.env.APPDATA;\n return appData\n ? join(appData, \"Cursor\", \"User\", \"globalStorage\", \"state.vscdb\")\n : null;\n }\n if (process.platform === \"darwin\") {\n return join(\n homedir(),\n \"Library\",\n \"Application Support\",\n \"Cursor\",\n \"User\",\n \"globalStorage\",\n \"state.vscdb\"\n );\n }\n return join(\n homedir(),\n \".config\",\n \"Cursor\",\n \"User\",\n \"globalStorage\",\n \"state.vscdb\"\n );\n }\n}\n","import { readdir, readFile, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport type {\n Source,\n ConversationMeta,\n Conversation,\n ConversationTurn,\n} from \"../types.js\";\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Claude Code stores sessions at:\n * ~/.claude/projects/{path-with-dashes}/{uuid}.jsonl\n *\n * Each line is a JSONL event with role \"user\" | \"assistant\".\n * Assistant messages may contain tool_use / tool_result blocks — we extract\n * only text blocks for knowledge extraction.\n */\nexport class ClaudeCodeSource implements Source {\n readonly type = \"claude-code\" as const;\n private basePath: string;\n\n constructor() {\n this.basePath = join(homedir(), \".claude\", \"projects\");\n }\n\n async detect(): Promise<boolean> {\n try {\n const s = await stat(this.basePath);\n return s.isDirectory();\n } catch {\n return false;\n }\n }\n\n async listConversations(): Promise<ConversationMeta[]> {\n const conversations: ConversationMeta[] = [];\n\n let projectDirs: string[];\n try {\n const entries = await readdir(this.basePath, { withFileTypes: true });\n // Claude Code encodes the project path as a dir name with path separators\n // replaced by \"-\" (e.g. /home/user/myproject → -home-user-myproject).\n // Filter to only dirs that match the current working directory.\n const cwdEncoded = process.cwd().replace(/[/\\\\:]/g, \"-\").toLowerCase();\n const cwdBasename = process.cwd().split(/[/\\\\]/).pop()?.toLowerCase() ?? \"\";\n\n projectDirs = entries\n .filter((e) => {\n if (!e.isDirectory()) return false;\n const name = e.name.toLowerCase();\n // Match if dir name ends with or contains the cwd path encoding\n return name.endsWith(cwdEncoded) || name.includes(`-${cwdBasename}`);\n })\n .map((e) => join(this.basePath, e.name));\n\n // If no cwd-specific dirs found, fall back to all dirs (avoids empty results)\n if (projectDirs.length === 0) {\n projectDirs = entries\n .filter((e) => e.isDirectory())\n .map((e) => join(this.basePath, e.name));\n }\n } catch {\n return [];\n }\n\n for (const projectDir of projectDirs) {\n const files = await readdir(projectDir).catch(() => []);\n for (const file of files) {\n if (!file.endsWith(\".jsonl\")) continue;\n const id = file.replace(\".jsonl\", \"\");\n if (!UUID_RE.test(id)) continue;\n\n const filePath = join(projectDir, file);\n const meta = await this.readMeta(filePath, id);\n if (meta) conversations.push(meta);\n }\n }\n\n return conversations.sort((a, b) => b.modifiedAt - a.modifiedAt);\n }\n\n async loadConversation(meta: ConversationMeta): Promise<Conversation> {\n const raw = await readFile(meta.filePath, \"utf-8\");\n const turns = this.parseJsonlContent(raw);\n return { meta: { ...meta, turnCount: turns.length }, turns };\n }\n\n parseJsonlContent(raw: string): ConversationTurn[] {\n const turns: ConversationTurn[] = [];\n\n for (const line of raw.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n try {\n const obj = JSON.parse(trimmed);\n const role = obj.role as \"user\" | \"assistant\";\n if (role !== \"user\" && role !== \"assistant\") continue;\n\n const text = this.extractText(obj);\n if (!text) continue;\n\n turns.push({ role, text });\n } catch {\n // Skip malformed lines\n }\n }\n\n return turns;\n }\n\n private extractText(obj: Record<string, unknown>): string | null {\n // Claude Code format: message may be in obj.message or obj directly\n const content =\n (obj.message as Record<string, unknown>)?.content ?? obj.content;\n\n if (typeof content === \"string\") {\n return content.trim() || null;\n }\n\n if (Array.isArray(content)) {\n const texts: string[] = [];\n for (const part of content) {\n if (\n part &&\n typeof part === \"object\" &&\n \"type\" in part &&\n part.type === \"text\" &&\n \"text\" in part &&\n typeof part.text === \"string\" &&\n part.text.trim()\n ) {\n texts.push((part.text as string).trim());\n }\n }\n const joined = texts.join(\"\\n\").trim();\n return joined || null;\n }\n\n return null;\n }\n\n private async readMeta(\n filePath: string,\n id: string\n ): Promise<ConversationMeta | null> {\n try {\n const fileStat = await stat(filePath);\n const raw = await readFile(filePath, \"utf-8\");\n const turns = this.parseJsonlContent(raw);\n const title = this.extractTitle(raw, id);\n\n return {\n id,\n source: \"claude-code\",\n filePath,\n title,\n modifiedAt: fileStat.mtimeMs,\n turnCount: turns.length,\n };\n } catch {\n return null;\n }\n }\n\n private extractTitle(raw: string, fallbackId: string): string {\n for (const line of raw.split(\"\\n\").slice(0, 5)) {\n try {\n const obj = JSON.parse(line.trim());\n if (obj.role === \"user\") {\n const text = this.extractText(obj);\n if (text) {\n const cleaned = text.replace(/\\s+/g, \" \").trim();\n return cleaned.length > 60\n ? cleaned.slice(0, 57) + \"...\"\n : cleaned;\n }\n }\n } catch {\n // skip\n }\n }\n return fallbackId.slice(0, 8);\n }\n}\n","import type { Source, SourceType } from \"../types.js\";\nimport { CursorSource } from \"./cursor.js\";\nimport { ClaudeCodeSource } from \"./claude-code.js\";\n\ninterface DetectionResult {\n available: Source[];\n unavailable: SourceType[];\n}\n\nexport async function detectSources(\n projectName?: string\n): Promise<DetectionResult> {\n const candidates: Source[] = [\n new CursorSource(projectName),\n new ClaudeCodeSource(),\n ];\n\n const available: Source[] = [];\n const unavailable: SourceType[] = [];\n\n for (const source of candidates) {\n const ok = await source.detect();\n if (ok) {\n available.push(source);\n } else {\n unavailable.push(source.type);\n }\n }\n\n return { available, unavailable };\n}\n\nexport function createSource(\n type: SourceType,\n projectName?: string\n): Source {\n switch (type) {\n case \"cursor\":\n return new CursorSource(projectName);\n case \"claude-code\":\n return new ClaudeCodeSource();\n default:\n throw new Error(`Source type \"${type}\" is not yet supported`);\n }\n}\n"],"mappings":"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","names":["readdir","readFile","stat","join","homedir","UUID_RE"]}
{"version":3,"sources":["../src/sources/cursor.ts","../src/sources/claude-code.ts","../src/sources/detector.ts"],"sourcesContent":["import { readdir, readFile, stat, copyFile, mkdir } from \"node:fs/promises\";\nimport { join, basename } from \"node:path\";\nimport { homedir, tmpdir } from \"node:os\";\nimport type {\n Source,\n ConversationMeta,\n Conversation,\n ConversationTurn,\n} from \"../types.js\";\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nexport class CursorSource implements Source {\n readonly type = \"cursor\" as const;\n private basePath: string;\n private projectName: string | undefined;\n\n constructor(projectName?: string) {\n this.projectName = projectName;\n this.basePath = join(homedir(), \".cursor\", \"projects\");\n }\n\n async detect(): Promise<boolean> {\n try {\n const projectDir = await this.resolveProjectDir();\n if (!projectDir) return false;\n const transcriptsDir = join(projectDir, \"agent-transcripts\");\n const s = await stat(transcriptsDir);\n return s.isDirectory();\n } catch {\n return false;\n }\n }\n\n async listConversations(): Promise<ConversationMeta[]> {\n const transcriptsDir = await this.getTranscriptsDir();\n if (!transcriptsDir) return [];\n\n // Load real titles from Cursor's workspace DB (best-effort)\n const titleMap = await this.loadTitleMap();\n\n const entries = await readdir(transcriptsDir, { withFileTypes: true });\n const conversations: ConversationMeta[] = [];\n\n for (const entry of entries) {\n if (entry.isDirectory() && UUID_RE.test(entry.name)) {\n const meta = await this.readConversationMeta(\n transcriptsDir,\n entry.name,\n titleMap\n );\n if (meta) conversations.push(meta);\n } else if (entry.isFile() && entry.name.endsWith(\".txt\")) {\n const meta = await this.readLegacyMeta(transcriptsDir, entry.name);\n if (meta) conversations.push(meta);\n }\n }\n\n return conversations.sort((a, b) => b.modifiedAt - a.modifiedAt);\n }\n\n async loadConversation(meta: ConversationMeta): Promise<Conversation> {\n if (meta.filePath.endsWith(\".txt\")) {\n return this.loadLegacyConversation(meta);\n }\n return this.loadJsonlConversation(meta);\n }\n\n // --- JSONL format (current) ---\n\n private async loadJsonlConversation(\n meta: ConversationMeta\n ): Promise<Conversation> {\n const raw = await readFile(meta.filePath, \"utf-8\");\n const turns = this.parseJsonlContent(raw);\n return { meta: { ...meta, turnCount: turns.length }, turns };\n }\n\n parseJsonlContent(raw: string): ConversationTurn[] {\n const turns: ConversationTurn[] = [];\n\n for (const line of raw.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n try {\n const obj = JSON.parse(trimmed);\n const role = obj.role as \"user\" | \"assistant\";\n if (role !== \"user\" && role !== \"assistant\") continue;\n\n const textParts = this.extractTextParts(obj.message?.content);\n if (!textParts) continue;\n\n turns.push({ role, text: textParts });\n } catch {\n // Skip malformed lines\n }\n }\n\n return turns;\n }\n\n private extractTextParts(content: unknown): string | null {\n if (!Array.isArray(content)) return null;\n\n const texts: string[] = [];\n for (const part of content) {\n if (\n part &&\n typeof part === \"object\" &&\n \"type\" in part &&\n part.type === \"text\" &&\n \"text\" in part &&\n typeof part.text === \"string\"\n ) {\n texts.push(this.cleanUserQuery(part.text));\n }\n }\n\n const joined = texts.join(\"\\n\").trim();\n return joined || null;\n }\n\n private cleanUserQuery(text: string): string {\n const match = text.match(/<user_query>\\s*([\\s\\S]*?)\\s*<\\/user_query>/);\n if (match) return match[1].trim();\n return text;\n }\n\n // --- Legacy .txt format ---\n\n private async loadLegacyConversation(\n meta: ConversationMeta\n ): Promise<Conversation> {\n const raw = await readFile(meta.filePath, \"utf-8\");\n const turns = this.parseLegacyContent(raw);\n return { meta: { ...meta, turnCount: turns.length }, turns };\n }\n\n parseLegacyContent(raw: string): ConversationTurn[] {\n const turns: ConversationTurn[] = [];\n const blocks = raw.split(/^(user|assistant):\\s*$/m);\n\n let currentRole: \"user\" | \"assistant\" | null = null;\n for (const block of blocks) {\n const trimmed = block.trim();\n if (trimmed === \"user\") {\n currentRole = \"user\";\n } else if (trimmed === \"assistant\") {\n currentRole = \"assistant\";\n } else if (currentRole && trimmed) {\n turns.push({\n role: currentRole,\n text: this.cleanUserQuery(trimmed),\n });\n currentRole = null;\n }\n }\n\n return turns;\n }\n\n // --- Metadata helpers ---\n\n private async readConversationMeta(\n transcriptsDir: string,\n uuid: string,\n titleMap: Map<string, string> = new Map()\n ): Promise<ConversationMeta | null> {\n const jsonlPath = join(transcriptsDir, uuid, `${uuid}.jsonl`);\n try {\n const fileStat = await stat(jsonlPath);\n const raw = await readFile(jsonlPath, \"utf-8\");\n // Prefer real title from DB; fall back to first user message\n const title = titleMap.get(uuid) ?? this.extractTitle(raw, uuid);\n const turnCount = this.countTurns(raw);\n\n return {\n id: uuid,\n source: \"cursor\",\n filePath: jsonlPath,\n title,\n modifiedAt: fileStat.mtimeMs,\n turnCount,\n };\n } catch {\n return null;\n }\n }\n\n private async readLegacyMeta(\n transcriptsDir: string,\n filename: string\n ): Promise<ConversationMeta | null> {\n const filePath = join(transcriptsDir, filename);\n try {\n const fileStat = await stat(filePath);\n const id = basename(filename, \".txt\");\n const raw = await readFile(filePath, \"utf-8\");\n const title = this.extractLegacyTitle(raw, id);\n\n return {\n id,\n source: \"cursor\",\n filePath,\n title,\n modifiedAt: fileStat.mtimeMs,\n turnCount: (raw.match(/^(user|assistant):\\s*$/m) || []).length,\n };\n } catch {\n return null;\n }\n }\n\n private extractTitle(jsonlRaw: string, fallbackId: string): string {\n for (const line of jsonlRaw.split(\"\\n\").slice(0, 5)) {\n try {\n const obj = JSON.parse(line.trim());\n if (obj.role === \"user\") {\n const text = this.extractTextParts(obj.message?.content);\n if (text) {\n const cleaned = text.replace(/\\s+/g, \" \").trim();\n return cleaned.length > 60\n ? cleaned.slice(0, 57) + \"...\"\n : cleaned;\n }\n }\n } catch {\n // skip\n }\n }\n return fallbackId.slice(0, 8);\n }\n\n private extractLegacyTitle(raw: string, fallbackId: string): string {\n const match = raw.match(\n /<user_query>\\s*([\\s\\S]*?)\\s*<\\/user_query>/\n );\n if (match) {\n const text = match[1].replace(/\\s+/g, \" \").trim();\n return text.length > 60 ? text.slice(0, 57) + \"...\" : text;\n }\n return fallbackId.slice(0, 8);\n }\n\n private countTurns(jsonlRaw: string): number {\n let count = 0;\n for (const line of jsonlRaw.split(\"\\n\")) {\n if (line.includes('\"role\"')) count++;\n }\n return count;\n }\n\n // --- Project directory resolution ---\n\n private async resolveProjectDir(): Promise<string | null> {\n if (this.projectName) {\n const dir = join(this.basePath, this.projectName);\n try {\n const s = await stat(dir);\n if (s.isDirectory()) return dir;\n } catch {\n // fall through\n }\n }\n\n try {\n const projects = await readdir(this.basePath, { withFileTypes: true });\n\n // Collect candidates with transcript dirs\n type Candidate = { dir: string; count: number; mtime: number };\n const candidates: Candidate[] = [];\n\n for (const p of projects) {\n if (!p.isDirectory()) continue;\n // Skip temp/AppData/numeric-only project directories\n if (this.isTempProject(p.name)) continue;\n\n const transcriptsDir = join(this.basePath, p.name, \"agent-transcripts\");\n try {\n const s = await stat(transcriptsDir);\n if (!s.isDirectory()) continue;\n\n const entries = await readdir(transcriptsDir);\n const count = entries.length;\n if (count === 0) continue;\n\n candidates.push({\n dir: join(this.basePath, p.name),\n count,\n mtime: s.mtimeMs,\n });\n } catch {\n // no transcripts\n }\n }\n\n if (candidates.length === 0) return null;\n\n // Prefer the project matching the current working directory\n const cwd = process.cwd().replace(/\\\\/g, \"-\").replace(/:/g, \"\").toLowerCase();\n const cwdMatch = candidates.find((c) =>\n cwd.endsWith(basename(c.dir).toLowerCase())\n );\n if (cwdMatch) return cwdMatch.dir;\n\n // Otherwise pick the project with the most conversations\n candidates.sort((a, b) => b.count - a.count || b.mtime - a.mtime);\n return candidates[0].dir;\n } catch {\n // .cursor/projects doesn't exist\n }\n\n return null;\n }\n\n private isTempProject(name: string): boolean {\n // Skip numeric-only names (e.g. \"1775614169037\")\n if (/^\\d+$/.test(name)) return true;\n // Skip AppData/Temp paths encoded as project names\n if (name.toLowerCase().includes(\"appdata\")) return true;\n if (name.toLowerCase().includes(\"temp\")) return true;\n if (name.toLowerCase().includes(\"local-temp\")) return true;\n return false;\n }\n\n private async getTranscriptsDir(): Promise<string | null> {\n const projectDir = await this.resolveProjectDir();\n if (!projectDir) return null;\n return join(projectDir, \"agent-transcripts\");\n }\n\n // --- Title map from Cursor's workspace SQLite DB ---\n\n /**\n * Returns a map of { [composerId/UUID]: title } by reading\n * Cursor's GLOBAL state.vscdb (key \"composer.composerHeaders\").\n * This file contains titles for ALL workspaces.\n * Uses node:sqlite (Node.js 22+). Silently returns empty map on any error.\n */\n private async loadTitleMap(): Promise<Map<string, string>> {\n try {\n const dbPath = this.getGlobalDbPath();\n if (!dbPath) return new Map();\n\n // Copy db + WAL + SHM to temp to avoid lock conflicts when Cursor is running\n const ts = Date.now();\n const tmpDb = join(tmpdir(), `ai-memory-global-${ts}.vscdb`);\n await copyFile(dbPath, tmpDb);\n for (const ext of [\"-wal\", \"-shm\"]) {\n try { await copyFile(dbPath + ext, tmpDb + ext); } catch { /* optional */ }\n }\n\n // Dynamic specifier prevents esbuild from stripping the \"node:\" prefix\n const sqliteMod = \"node\" + \":sqlite\";\n const { DatabaseSync } = await import(sqliteMod);\n const db = new (DatabaseSync as new (p: string, opts?: Record<string, unknown>) => {\n prepare(sql: string): { get(...args: unknown[]): unknown };\n close(): void;\n })(tmpDb, { readonly: true });\n\n const row = db\n .prepare(\"SELECT value FROM ItemTable WHERE key = ?\")\n .get(\"composer.composerHeaders\") as { value?: string } | undefined;\n db.close();\n\n if (!row?.value) return new Map();\n\n const data = JSON.parse(row.value) as {\n allComposers?: Array<{ composerId?: string; name?: string }>;\n };\n\n const map = new Map<string, string>();\n for (const c of data.allComposers ?? []) {\n if (c.composerId && c.name && c.name.trim()) {\n map.set(c.composerId, c.name.trim());\n }\n }\n return map;\n } catch (err) {\n // Non-fatal — fall back to first-message title extraction\n process.stderr.write(`[ai-memory] title map load failed: ${err}\\n`);\n return new Map();\n }\n }\n\n private getGlobalDbPath(): string | null {\n if (process.platform === \"win32\") {\n const appData = process.env.APPDATA;\n return appData\n ? join(appData, \"Cursor\", \"User\", \"globalStorage\", \"state.vscdb\")\n : null;\n }\n if (process.platform === \"darwin\") {\n return join(\n homedir(),\n \"Library\",\n \"Application Support\",\n \"Cursor\",\n \"User\",\n \"globalStorage\",\n \"state.vscdb\"\n );\n }\n return join(\n homedir(),\n \".config\",\n \"Cursor\",\n \"User\",\n \"globalStorage\",\n \"state.vscdb\"\n );\n }\n}\n","import { readdir, readFile, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport type {\n Source,\n ConversationMeta,\n Conversation,\n ConversationTurn,\n} from \"../types.js\";\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Claude Code stores sessions at:\n * ~/.claude/projects/{path-with-dashes}/{uuid}.jsonl\n *\n * Each line is a JSONL event with role \"user\" | \"assistant\".\n * Assistant messages may contain tool_use / tool_result blocks — we extract\n * only text blocks for knowledge extraction.\n */\nexport class ClaudeCodeSource implements Source {\n readonly type = \"claude-code\" as const;\n private basePath: string;\n\n constructor() {\n this.basePath = join(homedir(), \".claude\", \"projects\");\n }\n\n async detect(): Promise<boolean> {\n try {\n const s = await stat(this.basePath);\n return s.isDirectory();\n } catch {\n return false;\n }\n }\n\n async listConversations(): Promise<ConversationMeta[]> {\n const conversations: ConversationMeta[] = [];\n\n let projectDirs: string[];\n try {\n const entries = await readdir(this.basePath, { withFileTypes: true });\n // Claude Code encodes the project path as a dir name with path separators\n // replaced by \"-\" (e.g. /home/user/myproject → -home-user-myproject).\n // Filter to only dirs that match the current working directory.\n const cwdEncoded = process.cwd().replace(/[/\\\\:]/g, \"-\").toLowerCase();\n const cwdBasename = process.cwd().split(/[/\\\\]/).pop()?.toLowerCase() ?? \"\";\n\n projectDirs = entries\n .filter((e) => {\n if (!e.isDirectory()) return false;\n const name = e.name.toLowerCase();\n // Match if dir name ends with or contains the cwd path encoding\n return name.endsWith(cwdEncoded) || name.includes(`-${cwdBasename}`);\n })\n .map((e) => join(this.basePath, e.name));\n\n // If no cwd-specific dirs found, fall back to all dirs (avoids empty results)\n if (projectDirs.length === 0) {\n projectDirs = entries\n .filter((e) => e.isDirectory())\n .map((e) => join(this.basePath, e.name));\n }\n } catch {\n return [];\n }\n\n for (const projectDir of projectDirs) {\n const files = await readdir(projectDir).catch(() => []);\n for (const file of files) {\n if (!file.endsWith(\".jsonl\")) continue;\n const id = file.replace(\".jsonl\", \"\");\n if (!UUID_RE.test(id)) continue;\n\n const filePath = join(projectDir, file);\n const meta = await this.readMeta(filePath, id);\n if (meta) conversations.push(meta);\n }\n }\n\n return conversations.sort((a, b) => b.modifiedAt - a.modifiedAt);\n }\n\n async loadConversation(meta: ConversationMeta): Promise<Conversation> {\n const raw = await readFile(meta.filePath, \"utf-8\");\n const turns = this.parseJsonlContent(raw);\n return { meta: { ...meta, turnCount: turns.length }, turns };\n }\n\n parseJsonlContent(raw: string): ConversationTurn[] {\n const turns: ConversationTurn[] = [];\n\n for (const line of raw.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n try {\n const obj = JSON.parse(trimmed);\n const role = obj.role as \"user\" | \"assistant\";\n if (role !== \"user\" && role !== \"assistant\") continue;\n\n const text = this.extractText(obj);\n if (!text) continue;\n\n turns.push({ role, text });\n } catch {\n // Skip malformed lines\n }\n }\n\n return turns;\n }\n\n private extractText(obj: Record<string, unknown>): string | null {\n // Claude Code format: message may be in obj.message or obj directly\n const content =\n (obj.message as Record<string, unknown>)?.content ?? obj.content;\n\n if (typeof content === \"string\") {\n return content.trim() || null;\n }\n\n if (Array.isArray(content)) {\n const texts: string[] = [];\n for (const part of content) {\n if (\n part &&\n typeof part === \"object\" &&\n \"type\" in part &&\n part.type === \"text\" &&\n \"text\" in part &&\n typeof part.text === \"string\" &&\n part.text.trim()\n ) {\n texts.push((part.text as string).trim());\n }\n }\n const joined = texts.join(\"\\n\").trim();\n return joined || null;\n }\n\n return null;\n }\n\n private async readMeta(\n filePath: string,\n id: string\n ): Promise<ConversationMeta | null> {\n try {\n const fileStat = await stat(filePath);\n const raw = await readFile(filePath, \"utf-8\");\n const turns = this.parseJsonlContent(raw);\n const title = this.extractTitle(raw, id);\n\n return {\n id,\n source: \"claude-code\",\n filePath,\n title,\n modifiedAt: fileStat.mtimeMs,\n turnCount: turns.length,\n };\n } catch {\n return null;\n }\n }\n\n private extractTitle(raw: string, fallbackId: string): string {\n for (const line of raw.split(\"\\n\").slice(0, 5)) {\n try {\n const obj = JSON.parse(line.trim());\n if (obj.role === \"user\") {\n const text = this.extractText(obj);\n if (text) {\n const cleaned = text.replace(/\\s+/g, \" \").trim();\n return cleaned.length > 60\n ? cleaned.slice(0, 57) + \"...\"\n : cleaned;\n }\n }\n } catch {\n // skip\n }\n }\n return fallbackId.slice(0, 8);\n }\n}\n","import type { Source, SourceType } from \"../types.js\";\nimport { CursorSource } from \"./cursor.js\";\nimport { ClaudeCodeSource } from \"./claude-code.js\";\n\ninterface DetectionResult {\n available: Source[];\n unavailable: SourceType[];\n}\n\nexport async function detectSources(\n projectName?: string\n): Promise<DetectionResult> {\n const candidates: Source[] = [\n new CursorSource(projectName),\n new ClaudeCodeSource(),\n ];\n\n const available: Source[] = [];\n const unavailable: SourceType[] = [];\n\n for (const source of candidates) {\n const ok = await source.detect();\n if (ok) {\n available.push(source);\n } else {\n unavailable.push(source.type);\n }\n }\n\n return { available, unavailable };\n}\n\nexport function createSource(\n type: SourceType,\n projectName?: string\n): Source {\n switch (type) {\n case \"cursor\":\n return new CursorSource(projectName);\n case \"claude-code\":\n return new ClaudeCodeSource();\n default:\n throw new Error(`Source type \"${type}\" is not yet supported`);\n }\n}\n"],"mappings":";AAAA,SAAS,SAAS,UAAU,MAAM,gBAAuB;AACzD,SAAS,MAAM,gBAAgB;AAC/B,SAAS,SAAS,cAAc;AAQhC,IAAM,UAAU;AAET,IAAM,eAAN,MAAqC;AAAA,EACjC,OAAO;AAAA,EACR;AAAA,EACA;AAAA,EAER,YAAY,aAAsB;AAChC,SAAK,cAAc;AACnB,SAAK,WAAW,KAAK,QAAQ,GAAG,WAAW,UAAU;AAAA,EACvD;AAAA,EAEA,MAAM,SAA2B;AAC/B,QAAI;AACF,YAAM,aAAa,MAAM,KAAK,kBAAkB;AAChD,UAAI,CAAC,WAAY,QAAO;AACxB,YAAM,iBAAiB,KAAK,YAAY,mBAAmB;AAC3D,YAAM,IAAI,MAAM,KAAK,cAAc;AACnC,aAAO,EAAE,YAAY;AAAA,IACvB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,oBAAiD;AACrD,UAAM,iBAAiB,MAAM,KAAK,kBAAkB;AACpD,QAAI,CAAC,eAAgB,QAAO,CAAC;AAG7B,UAAM,WAAW,MAAM,KAAK,aAAa;AAEzC,UAAM,UAAU,MAAM,QAAQ,gBAAgB,EAAE,eAAe,KAAK,CAAC;AACrE,UAAM,gBAAoC,CAAC;AAE3C,eAAW,SAAS,SAAS;AAC3B,UAAI,MAAM,YAAY,KAAK,QAAQ,KAAK,MAAM,IAAI,GAAG;AACnD,cAAM,OAAO,MAAM,KAAK;AAAA,UACtB;AAAA,UACA,MAAM;AAAA,UACN;AAAA,QACF;AACA,YAAI,KAAM,eAAc,KAAK,IAAI;AAAA,MACnC,WAAW,MAAM,OAAO,KAAK,MAAM,KAAK,SAAS,MAAM,GAAG;AACxD,cAAM,OAAO,MAAM,KAAK,eAAe,gBAAgB,MAAM,IAAI;AACjE,YAAI,KAAM,eAAc,KAAK,IAAI;AAAA,MACnC;AAAA,IACF;AAEA,WAAO,cAAc,KAAK,CAAC,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU;AAAA,EACjE;AAAA,EAEA,MAAM,iBAAiB,MAA+C;AACpE,QAAI,KAAK,SAAS,SAAS,MAAM,GAAG;AAClC,aAAO,KAAK,uBAAuB,IAAI;AAAA,IACzC;AACA,WAAO,KAAK,sBAAsB,IAAI;AAAA,EACxC;AAAA;AAAA,EAIA,MAAc,sBACZ,MACuB;AACvB,UAAM,MAAM,MAAM,SAAS,KAAK,UAAU,OAAO;AACjD,UAAM,QAAQ,KAAK,kBAAkB,GAAG;AACxC,WAAO,EAAE,MAAM,EAAE,GAAG,MAAM,WAAW,MAAM,OAAO,GAAG,MAAM;AAAA,EAC7D;AAAA,EAEA,kBAAkB,KAAiC;AACjD,UAAM,QAA4B,CAAC;AAEnC,eAAW,QAAQ,IAAI,MAAM,IAAI,GAAG;AAClC,YAAM,UAAU,KAAK,KAAK;AAC1B,UAAI,CAAC,QAAS;AAEd,UAAI;AACF,cAAM,MAAM,KAAK,MAAM,OAAO;AAC9B,cAAM,OAAO,IAAI;AACjB,YAAI,SAAS,UAAU,SAAS,YAAa;AAE7C,cAAM,YAAY,KAAK,iBAAiB,IAAI,SAAS,OAAO;AAC5D,YAAI,CAAC,UAAW;AAEhB,cAAM,KAAK,EAAE,MAAM,MAAM,UAAU,CAAC;AAAA,MACtC,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,iBAAiB,SAAiC;AACxD,QAAI,CAAC,MAAM,QAAQ,OAAO,EAAG,QAAO;AAEpC,UAAM,QAAkB,CAAC;AACzB,eAAW,QAAQ,SAAS;AAC1B,UACE,QACA,OAAO,SAAS,YAChB,UAAU,QACV,KAAK,SAAS,UACd,UAAU,QACV,OAAO,KAAK,SAAS,UACrB;AACA,cAAM,KAAK,KAAK,eAAe,KAAK,IAAI,CAAC;AAAA,MAC3C;AAAA,IACF;AAEA,UAAM,SAAS,MAAM,KAAK,IAAI,EAAE,KAAK;AACrC,WAAO,UAAU;AAAA,EACnB;AAAA,EAEQ,eAAe,MAAsB;AAC3C,UAAM,QAAQ,KAAK,MAAM,4CAA4C;AACrE,QAAI,MAAO,QAAO,MAAM,CAAC,EAAE,KAAK;AAChC,WAAO;AAAA,EACT;AAAA;AAAA,EAIA,MAAc,uBACZ,MACuB;AACvB,UAAM,MAAM,MAAM,SAAS,KAAK,UAAU,OAAO;AACjD,UAAM,QAAQ,KAAK,mBAAmB,GAAG;AACzC,WAAO,EAAE,MAAM,EAAE,GAAG,MAAM,WAAW,MAAM,OAAO,GAAG,MAAM;AAAA,EAC7D;AAAA,EAEA,mBAAmB,KAAiC;AAClD,UAAM,QAA4B,CAAC;AACnC,UAAM,SAAS,IAAI,MAAM,yBAAyB;AAElD,QAAI,cAA2C;AAC/C,eAAW,SAAS,QAAQ;AAC1B,YAAM,UAAU,MAAM,KAAK;AAC3B,UAAI,YAAY,QAAQ;AACtB,sBAAc;AAAA,MAChB,WAAW,YAAY,aAAa;AAClC,sBAAc;AAAA,MAChB,WAAW,eAAe,SAAS;AACjC,cAAM,KAAK;AAAA,UACT,MAAM;AAAA,UACN,MAAM,KAAK,eAAe,OAAO;AAAA,QACnC,CAAC;AACD,sBAAc;AAAA,MAChB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA,EAIA,MAAc,qBACZ,gBACA,MACA,WAAgC,oBAAI,IAAI,GACN;AAClC,UAAM,YAAY,KAAK,gBAAgB,MAAM,GAAG,IAAI,QAAQ;AAC5D,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,SAAS;AACrC,YAAM,MAAM,MAAM,SAAS,WAAW,OAAO;AAE7C,YAAM,QAAQ,SAAS,IAAI,IAAI,KAAK,KAAK,aAAa,KAAK,IAAI;AAC/D,YAAM,YAAY,KAAK,WAAW,GAAG;AAErC,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ;AAAA,QACR,UAAU;AAAA,QACV;AAAA,QACA,YAAY,SAAS;AAAA,QACrB;AAAA,MACF;AAAA,IACF,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAc,eACZ,gBACA,UACkC;AAClC,UAAM,WAAW,KAAK,gBAAgB,QAAQ;AAC9C,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,QAAQ;AACpC,YAAM,KAAK,SAAS,UAAU,MAAM;AACpC,YAAM,MAAM,MAAM,SAAS,UAAU,OAAO;AAC5C,YAAM,QAAQ,KAAK,mBAAmB,KAAK,EAAE;AAE7C,aAAO;AAAA,QACL;AAAA,QACA,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA,YAAY,SAAS;AAAA,QACrB,YAAY,IAAI,MAAM,yBAAyB,KAAK,CAAC,GAAG;AAAA,MAC1D;AAAA,IACF,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEQ,aAAa,UAAkB,YAA4B;AACjE,eAAW,QAAQ,SAAS,MAAM,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG;AACnD,UAAI;AACF,cAAM,MAAM,KAAK,MAAM,KAAK,KAAK,CAAC;AAClC,YAAI,IAAI,SAAS,QAAQ;AACvB,gBAAM,OAAO,KAAK,iBAAiB,IAAI,SAAS,OAAO;AACvD,cAAI,MAAM;AACR,kBAAM,UAAU,KAAK,QAAQ,QAAQ,GAAG,EAAE,KAAK;AAC/C,mBAAO,QAAQ,SAAS,KACpB,QAAQ,MAAM,GAAG,EAAE,IAAI,QACvB;AAAA,UACN;AAAA,QACF;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO,WAAW,MAAM,GAAG,CAAC;AAAA,EAC9B;AAAA,EAEQ,mBAAmB,KAAa,YAA4B;AAClE,UAAM,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AACA,QAAI,OAAO;AACT,YAAM,OAAO,MAAM,CAAC,EAAE,QAAQ,QAAQ,GAAG,EAAE,KAAK;AAChD,aAAO,KAAK,SAAS,KAAK,KAAK,MAAM,GAAG,EAAE,IAAI,QAAQ;AAAA,IACxD;AACA,WAAO,WAAW,MAAM,GAAG,CAAC;AAAA,EAC9B;AAAA,EAEQ,WAAW,UAA0B;AAC3C,QAAI,QAAQ;AACZ,eAAW,QAAQ,SAAS,MAAM,IAAI,GAAG;AACvC,UAAI,KAAK,SAAS,QAAQ,EAAG;AAAA,IAC/B;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAIA,MAAc,oBAA4C;AACxD,QAAI,KAAK,aAAa;AACpB,YAAM,MAAM,KAAK,KAAK,UAAU,KAAK,WAAW;AAChD,UAAI;AACF,cAAM,IAAI,MAAM,KAAK,GAAG;AACxB,YAAI,EAAE,YAAY,EAAG,QAAO;AAAA,MAC9B,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,QAAI;AACF,YAAM,WAAW,MAAM,QAAQ,KAAK,UAAU,EAAE,eAAe,KAAK,CAAC;AAIrE,YAAM,aAA0B,CAAC;AAEjC,iBAAW,KAAK,UAAU;AACxB,YAAI,CAAC,EAAE,YAAY,EAAG;AAEtB,YAAI,KAAK,cAAc,EAAE,IAAI,EAAG;AAEhC,cAAM,iBAAiB,KAAK,KAAK,UAAU,EAAE,MAAM,mBAAmB;AACtE,YAAI;AACF,gBAAM,IAAI,MAAM,KAAK,cAAc;AACnC,cAAI,CAAC,EAAE,YAAY,EAAG;AAEtB,gBAAM,UAAU,MAAM,QAAQ,cAAc;AAC5C,gBAAM,QAAQ,QAAQ;AACtB,cAAI,UAAU,EAAG;AAEjB,qBAAW,KAAK;AAAA,YACd,KAAK,KAAK,KAAK,UAAU,EAAE,IAAI;AAAA,YAC/B;AAAA,YACA,OAAO,EAAE;AAAA,UACX,CAAC;AAAA,QACH,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,UAAI,WAAW,WAAW,EAAG,QAAO;AAGpC,YAAM,MAAM,QAAQ,IAAI,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,MAAM,EAAE,EAAE,YAAY;AAC5E,YAAM,WAAW,WAAW;AAAA,QAAK,CAAC,MAChC,IAAI,SAAS,SAAS,EAAE,GAAG,EAAE,YAAY,CAAC;AAAA,MAC5C;AACA,UAAI,SAAU,QAAO,SAAS;AAG9B,iBAAW,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK;AAChE,aAAO,WAAW,CAAC,EAAE;AAAA,IACvB,QAAQ;AAAA,IAER;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,cAAc,MAAuB;AAE3C,QAAI,QAAQ,KAAK,IAAI,EAAG,QAAO;AAE/B,QAAI,KAAK,YAAY,EAAE,SAAS,SAAS,EAAG,QAAO;AACnD,QAAI,KAAK,YAAY,EAAE,SAAS,MAAM,EAAG,QAAO;AAChD,QAAI,KAAK,YAAY,EAAE,SAAS,YAAY,EAAG,QAAO;AACtD,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,oBAA4C;AACxD,UAAM,aAAa,MAAM,KAAK,kBAAkB;AAChD,QAAI,CAAC,WAAY,QAAO;AACxB,WAAO,KAAK,YAAY,mBAAmB;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAc,eAA6C;AACzD,QAAI;AACF,YAAM,SAAS,KAAK,gBAAgB;AACpC,UAAI,CAAC,OAAQ,QAAO,oBAAI,IAAI;AAG5B,YAAM,KAAK,KAAK,IAAI;AACpB,YAAM,QAAQ,KAAK,OAAO,GAAG,oBAAoB,EAAE,QAAQ;AAC3D,YAAM,SAAS,QAAQ,KAAK;AAC5B,iBAAW,OAAO,CAAC,QAAQ,MAAM,GAAG;AAClC,YAAI;AAAE,gBAAM,SAAS,SAAS,KAAK,QAAQ,GAAG;AAAA,QAAG,QAAQ;AAAA,QAAiB;AAAA,MAC5E;AAGA,YAAM,YAAY;AAClB,YAAM,EAAE,aAAa,IAAI,MAAM,OAAO;AACtC,YAAM,KAAK,IAAK,aAGb,OAAO,EAAE,UAAU,KAAK,CAAC;AAE5B,YAAM,MAAM,GACT,QAAQ,2CAA2C,EACnD,IAAI,0BAA0B;AACjC,SAAG,MAAM;AAET,UAAI,CAAC,KAAK,MAAO,QAAO,oBAAI,IAAI;AAEhC,YAAM,OAAO,KAAK,MAAM,IAAI,KAAK;AAIjC,YAAM,MAAM,oBAAI,IAAoB;AACpC,iBAAW,KAAK,KAAK,gBAAgB,CAAC,GAAG;AACvC,YAAI,EAAE,cAAc,EAAE,QAAQ,EAAE,KAAK,KAAK,GAAG;AAC3C,cAAI,IAAI,EAAE,YAAY,EAAE,KAAK,KAAK,CAAC;AAAA,QACrC;AAAA,MACF;AACA,aAAO;AAAA,IACT,SAAS,KAAK;AAEZ,cAAQ,OAAO,MAAM,sCAAsC,GAAG;AAAA,CAAI;AAClE,aAAO,oBAAI,IAAI;AAAA,IACjB;AAAA,EACF;AAAA,EAEQ,kBAAiC;AACvC,QAAI,QAAQ,aAAa,SAAS;AAChC,YAAM,UAAU,QAAQ,IAAI;AAC5B,aAAO,UACH,KAAK,SAAS,UAAU,QAAQ,iBAAiB,aAAa,IAC9D;AAAA,IACN;AACA,QAAI,QAAQ,aAAa,UAAU;AACjC,aAAO;AAAA,QACL,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;;;AC7ZA,SAAS,WAAAA,UAAS,YAAAC,WAAU,QAAAC,aAAY;AACxC,SAAS,QAAAC,aAAY;AACrB,SAAS,WAAAC,gBAAe;AAQxB,IAAMC,WAAU;AAUT,IAAM,mBAAN,MAAyC;AAAA,EACrC,OAAO;AAAA,EACR;AAAA,EAER,cAAc;AACZ,SAAK,WAAWF,MAAKC,SAAQ,GAAG,WAAW,UAAU;AAAA,EACvD;AAAA,EAEA,MAAM,SAA2B;AAC/B,QAAI;AACF,YAAM,IAAI,MAAMF,MAAK,KAAK,QAAQ;AAClC,aAAO,EAAE,YAAY;AAAA,IACvB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,oBAAiD;AACrD,UAAM,gBAAoC,CAAC;AAE3C,QAAI;AACJ,QAAI;AACF,YAAM,UAAU,MAAMF,SAAQ,KAAK,UAAU,EAAE,eAAe,KAAK,CAAC;AAIpE,YAAM,aAAa,QAAQ,IAAI,EAAE,QAAQ,WAAW,GAAG,EAAE,YAAY;AACrE,YAAM,cAAc,QAAQ,IAAI,EAAE,MAAM,OAAO,EAAE,IAAI,GAAG,YAAY,KAAK;AAEzE,oBAAc,QACX,OAAO,CAAC,MAAM;AACb,YAAI,CAAC,EAAE,YAAY,EAAG,QAAO;AAC7B,cAAM,OAAO,EAAE,KAAK,YAAY;AAEhC,eAAO,KAAK,SAAS,UAAU,KAAK,KAAK,SAAS,IAAI,WAAW,EAAE;AAAA,MACrE,CAAC,EACA,IAAI,CAAC,MAAMG,MAAK,KAAK,UAAU,EAAE,IAAI,CAAC;AAGzC,UAAI,YAAY,WAAW,GAAG;AAC5B,sBAAc,QACX,OAAO,CAAC,MAAM,EAAE,YAAY,CAAC,EAC7B,IAAI,CAAC,MAAMA,MAAK,KAAK,UAAU,EAAE,IAAI,CAAC;AAAA,MAC3C;AAAA,IACF,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAEA,eAAW,cAAc,aAAa;AACpC,YAAM,QAAQ,MAAMH,SAAQ,UAAU,EAAE,MAAM,MAAM,CAAC,CAAC;AACtD,iBAAW,QAAQ,OAAO;AACxB,YAAI,CAAC,KAAK,SAAS,QAAQ,EAAG;AAC9B,cAAM,KAAK,KAAK,QAAQ,UAAU,EAAE;AACpC,YAAI,CAACK,SAAQ,KAAK,EAAE,EAAG;AAEvB,cAAM,WAAWF,MAAK,YAAY,IAAI;AACtC,cAAM,OAAO,MAAM,KAAK,SAAS,UAAU,EAAE;AAC7C,YAAI,KAAM,eAAc,KAAK,IAAI;AAAA,MACnC;AAAA,IACF;AAEA,WAAO,cAAc,KAAK,CAAC,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU;AAAA,EACjE;AAAA,EAEA,MAAM,iBAAiB,MAA+C;AACpE,UAAM,MAAM,MAAMF,UAAS,KAAK,UAAU,OAAO;AACjD,UAAM,QAAQ,KAAK,kBAAkB,GAAG;AACxC,WAAO,EAAE,MAAM,EAAE,GAAG,MAAM,WAAW,MAAM,OAAO,GAAG,MAAM;AAAA,EAC7D;AAAA,EAEA,kBAAkB,KAAiC;AACjD,UAAM,QAA4B,CAAC;AAEnC,eAAW,QAAQ,IAAI,MAAM,IAAI,GAAG;AAClC,YAAM,UAAU,KAAK,KAAK;AAC1B,UAAI,CAAC,QAAS;AAEd,UAAI;AACF,cAAM,MAAM,KAAK,MAAM,OAAO;AAC9B,cAAM,OAAO,IAAI;AACjB,YAAI,SAAS,UAAU,SAAS,YAAa;AAE7C,cAAM,OAAO,KAAK,YAAY,GAAG;AACjC,YAAI,CAAC,KAAM;AAEX,cAAM,KAAK,EAAE,MAAM,KAAK,CAAC;AAAA,MAC3B,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,YAAY,KAA6C;AAE/D,UAAM,UACH,IAAI,SAAqC,WAAW,IAAI;AAE3D,QAAI,OAAO,YAAY,UAAU;AAC/B,aAAO,QAAQ,KAAK,KAAK;AAAA,IAC3B;AAEA,QAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,YAAM,QAAkB,CAAC;AACzB,iBAAW,QAAQ,SAAS;AAC1B,YACE,QACA,OAAO,SAAS,YAChB,UAAU,QACV,KAAK,SAAS,UACd,UAAU,QACV,OAAO,KAAK,SAAS,YACrB,KAAK,KAAK,KAAK,GACf;AACA,gBAAM,KAAM,KAAK,KAAgB,KAAK,CAAC;AAAA,QACzC;AAAA,MACF;AACA,YAAM,SAAS,MAAM,KAAK,IAAI,EAAE,KAAK;AACrC,aAAO,UAAU;AAAA,IACnB;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,SACZ,UACA,IACkC;AAClC,QAAI;AACF,YAAM,WAAW,MAAMC,MAAK,QAAQ;AACpC,YAAM,MAAM,MAAMD,UAAS,UAAU,OAAO;AAC5C,YAAM,QAAQ,KAAK,kBAAkB,GAAG;AACxC,YAAM,QAAQ,KAAK,aAAa,KAAK,EAAE;AAEvC,aAAO;AAAA,QACL;AAAA,QACA,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA,YAAY,SAAS;AAAA,QACrB,WAAW,MAAM;AAAA,MACnB;AAAA,IACF,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEQ,aAAa,KAAa,YAA4B;AAC5D,eAAW,QAAQ,IAAI,MAAM,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG;AAC9C,UAAI;AACF,cAAM,MAAM,KAAK,MAAM,KAAK,KAAK,CAAC;AAClC,YAAI,IAAI,SAAS,QAAQ;AACvB,gBAAM,OAAO,KAAK,YAAY,GAAG;AACjC,cAAI,MAAM;AACR,kBAAM,UAAU,KAAK,QAAQ,QAAQ,GAAG,EAAE,KAAK;AAC/C,mBAAO,QAAQ,SAAS,KACpB,QAAQ,MAAM,GAAG,EAAE,IAAI,QACvB;AAAA,UACN;AAAA,QACF;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO,WAAW,MAAM,GAAG,CAAC;AAAA,EAC9B;AACF;;;AClLA,eAAsB,cACpB,aAC0B;AAC1B,QAAM,aAAuB;AAAA,IAC3B,IAAI,aAAa,WAAW;AAAA,IAC5B,IAAI,iBAAiB;AAAA,EACvB;AAEA,QAAM,YAAsB,CAAC;AAC7B,QAAM,cAA4B,CAAC;AAEnC,aAAW,UAAU,YAAY;AAC/B,UAAM,KAAK,MAAM,OAAO,OAAO;AAC/B,QAAI,IAAI;AACN,gBAAU,KAAK,MAAM;AAAA,IACvB,OAAO;AACL,kBAAY,KAAK,OAAO,IAAI;AAAA,IAC9B;AAAA,EACF;AAEA,SAAO,EAAE,WAAW,YAAY;AAClC;AAEO,SAAS,aACd,MACA,aACQ;AACR,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,IAAI,aAAa,WAAW;AAAA,IACrC,KAAK;AACH,aAAO,IAAI,iBAAiB;AAAA,IAC9B;AACE,YAAM,IAAI,MAAM,gBAAgB,IAAI,wBAAwB;AAAA,EAChE;AACF;","names":["readdir","readFile","stat","join","homedir","UUID_RE"]}
{
"name": "ai-memory-cli",
"version": "1.1.0",
"version": "1.1.2",
"description": "Extract structured knowledge from AI editor conversations (Cursor, Claude Code) into git-trackable files",

@@ -5,0 +5,0 @@ "type": "module",

+62
-67

@@ -8,3 +8,3 @@ # ai-memory

```bash
npx ai-memory extract --incremental
npx ai-memory-cli extract --incremental
```

@@ -30,3 +30,3 @@

Cursor transcripts → AI extracts → .ai-memory/
Cursor transcripts �? AI extracts �? .ai-memory/
Claude Code sessions decisions/todos/ ├── decisions/

@@ -52,3 +52,3 @@ architecture/ ├── architecture/

```bash
npx ai-memory extract
npx ai-memory-cli extract
```

@@ -67,8 +67,8 @@

### `list` — Browse available conversations
### `list` �?Browse available conversations
```bash
npx ai-memory list # list all conversations with status
npx ai-memory list --source cursor # filter by source
npx ai-memory list --json # JSON output
npx ai-memory-cli list # list all conversations with status
npx ai-memory-cli list --source cursor # filter by source
npx ai-memory-cli list --json # JSON output
```

@@ -78,29 +78,29 @@

### `extract` — Extract memories from conversation history
### `extract` �?Extract memories from conversation history
```bash
npx ai-memory extract # auto-detect all sources
npx ai-memory extract --incremental # only new/modified conversations
npx ai-memory extract --pick 4 # process specific conversation by list index
npx ai-memory extract --pick 1,4,7 # process multiple by index
npx ai-memory extract --id b5677be8 # process by conversation ID prefix
npx ai-memory extract --since "3 days ago" # conversations modified in last 3 days
npx ai-memory extract --since "2 weeks ago" # also supports weeks
npx ai-memory extract --source cursor # specify source
npx ai-memory extract --type decision,todo # only extract specific types
npx ai-memory extract --dry-run # preview conversations to process (no LLM, no writes)
npx ai-memory extract --verbose # show LLM request details
npx ai-memory extract --json # JSON output for CI
npx ai-memory-cli extract # auto-detect all sources
npx ai-memory-cli extract --incremental # only new/modified conversations
npx ai-memory-cli extract --pick 4 # process specific conversation by list index
npx ai-memory-cli extract --pick 1,4,7 # process multiple by index
npx ai-memory-cli extract --id b5677be8 # process by conversation ID prefix
npx ai-memory-cli extract --since "3 days ago" # conversations modified in last 3 days
npx ai-memory-cli extract --since "2 weeks ago" # also supports weeks
npx ai-memory-cli extract --source cursor # specify source
npx ai-memory-cli extract --type decision,todo # only extract specific types
npx ai-memory-cli extract --dry-run # preview conversations to process (no LLM, no writes)
npx ai-memory-cli extract --verbose # show LLM request details
npx ai-memory-cli extract --json # JSON output for CI
```
### `summary` — Generate a project-level summary
### `summary` �?Generate a project-level summary
```bash
npx ai-memory summary # write/update SUMMARY.md
npx ai-memory summary --output MEMORY.md # custom output path
npx ai-memory summary --focus "payment module" # focus on a topic
npx ai-memory summary --verbose # show LLM debug info
npx ai-memory-cli summary # write/update SUMMARY.md
npx ai-memory-cli summary --output MEMORY.md # custom output path
npx ai-memory-cli summary --focus "payment module" # focus on a topic
npx ai-memory-cli summary --verbose # show LLM debug info
```
### `context` — Generate a continuation prompt
### `context` �?Generate a continuation prompt

@@ -110,16 +110,16 @@ For seamlessly resuming work in a new conversation or on another machine:

```bash
npx ai-memory context # generate context block (instant, no LLM)
npx ai-memory context --copy # generate and copy to clipboard
npx ai-memory context --topic "coupon system" # focus on a specific topic
npx ai-memory context --recent 7 # only last 7 days of memories
npx ai-memory context --output CONTEXT.md # write to file
npx ai-memory context --summarize # use LLM to write a condensed prose summary
npx ai-memory-cli context # generate context block (instant, no LLM)
npx ai-memory-cli context --copy # generate and copy to clipboard
npx ai-memory-cli context --topic "coupon system" # focus on a specific topic
npx ai-memory-cli context --recent 7 # only last 7 days of memories
npx ai-memory-cli context --output CONTEXT.md # write to file
npx ai-memory-cli context --summarize # use LLM to write a condensed prose summary
```
The default (no `--summarize`) assembles a structured block directly from your memories — instant, free, and lossless. Paste the output at the start of your next Cursor/Claude Code conversation.
The default (no `--summarize`) assembles a structured block directly from your memories �?instant, free, and lossless. Paste the output at the start of your next Cursor/Claude Code conversation.
### `init` — Initialize config
### `init` �?Initialize config
```bash
npx ai-memory init
npx ai-memory-cli init
```

@@ -158,3 +158,3 @@

**Content**: Use OAuth Bridge pattern — static/oauth-bridge.html receives the redirect callback and forwards it to the App via postMessage
**Content**: Use OAuth Bridge pattern �?static/oauth-bridge.html receives the redirect callback and forwards it to the App via postMessage

@@ -186,6 +186,6 @@ **Reasoning**: Embedded WebViews cannot receive OAuth redirects directly; bridge page acts as intermediary

# 1. See what conversations are available
npx ai-memory list
npx ai-memory-cli list
# 2. Extract everything (takes a few minutes on first run)
npx ai-memory extract
npx ai-memory-cli extract

@@ -201,3 +201,3 @@ # 3. Commit the knowledge base

# After a productive coding session
npx ai-memory extract --incremental
npx ai-memory-cli extract --incremental

@@ -212,9 +212,9 @@ # Commit new memories

# Generate a context block and copy to clipboard
npx ai-memory context --copy
npx ai-memory-cli context --copy
# Focus on what you're about to work on
npx ai-memory context --topic "payment module" --copy
npx ai-memory-cli context --topic "payment module" --copy
# Or write to a file and attach it
npx ai-memory context --output CONTEXT.md
npx ai-memory-cli context --output CONTEXT.md
```

@@ -237,3 +237,3 @@

The AI will immediately understand your project's decisions, conventions, and current state — no need to re-explain.
The AI will immediately understand your project's decisions, conventions, and current state �?no need to re-explain.

@@ -244,9 +244,9 @@ ### Processing a specific conversation

# First, find the index of the conversation you want
npx ai-memory list
npx ai-memory-cli list
# Then extract just that one
npx ai-memory extract --pick 3
npx ai-memory-cli extract --pick 3
# Or match by ID prefix (shown in list output)
npx ai-memory extract --id b5677be8
npx ai-memory-cli extract --id b5677be8
```

@@ -262,14 +262,9 @@

Cursor / Claude Code dev work
npx ai-memory extract --incremental
git add .ai-memory/
�?npx ai-memory-cli extract --incremental
�?git add .ai-memory/
git commit && git push
git pull
npx ai-memory context --topic "today's work"
Paste context → new conversation
Seamlessly resume
�? npx ai-memory-cli context --topic "today's work"
�? Paste context �?new conversation
�? Seamlessly resume
```

@@ -281,3 +276,3 @@

`ai-memory` works with zero config. To customize, run `npx ai-memory init` or create `.ai-memory/.config.json` manually:
`ai-memory` works with zero config. To customize, run `npx ai-memory-cli init` or create `.ai-memory/.config.json` manually:

@@ -298,3 +293,3 @@ ```jsonc

"summaryFile": "SUMMARY.md",
"language": "zh" // "zh" or "en" — output language for summaries
"language": "zh" // "zh" or "en" �?output language for summaries
},

@@ -325,12 +320,12 @@ "model": "" // leave empty for auto-selection

├── decisions/
│ ├── 2026-04-12-oauth-bridge-pattern.md
│ └── 2026-04-13-async-job-queue-design.md
�? ├── 2026-04-12-oauth-bridge-pattern.md
�? └── 2026-04-13-async-job-queue-design.md
├── architecture/
│ └── 2026-04-10-payment-module-design.md
�? └── 2026-04-10-payment-module-design.md
├── conventions/
│ └── 2026-04-08-coding-conventions.md
�? └── 2026-04-08-coding-conventions.md
├── todos/
│ └── 2026-04-12-add-retry-logic.md
�? └── 2026-04-12-add-retry-logic.md
├── issues/
│ └── 2026-04-11-sqlite-locking-fix.md
�? └── 2026-04-11-sqlite-locking-fix.md
├── .index/ # Extraction index (auto-managed)

@@ -341,3 +336,3 @@ ├── .config.json # Configuration (commit this)

Add `.ai-memory/.state.json` to `.gitignore` — it tracks which conversations have been processed and is machine-specific.
Add `.ai-memory/.state.json` to `.gitignore` �?it tracks which conversations have been processed and is machine-specific.

@@ -351,3 +346,3 @@ ---

- name: Extract AI memories
run: npx ai-memory extract --incremental --json
run: npx ai-memory-cli extract --incremental --json
env:

@@ -366,2 +361,2 @@ AI_REVIEW_API_KEY: ${{ secrets.AI_REVIEW_API_KEY }}

MIT — [Conor Liu](https://github.com/conorliu)
MIT �?[Conor Liu](https://github.com/conorliu)

@@ -1,2 +0,2 @@

# ai-memory
# ai-memory

@@ -8,3 +8,3 @@ 从 AI 编辑器对话历史(Cursor、Claude Code)中提取结构化知识,保存为可 git 跟踪的 Markdown 文件。

```bash
npx ai-memory extract --incremental
npx ai-memory-cli extract --incremental
```

@@ -18,3 +18,3 @@

每天用 Cursor 或 Claude Code 开发时,你会做出无数决策:"用 OAuth Bridge 模式"、"异步任务走 SSE bridge"、"这个项目里不用 `getServerSideProps`"。这些决策活在聊天记录里,在这些情况下会彻底丢失:
每天在 Cursor 或 Claude Code 开发时,你会做出无数决策:"用 OAuth Bridge 模式"、"异步任务走 SSE bridge"、"这个项目里不用 `getServerSideProps`"。这些决策活在聊天记录里,在这些情况下会彻底丢失:

@@ -25,9 +25,9 @@ - 换了台电脑

`ai-memory` 读取本地对话历史,用 AI 提取有价值的内容,保存为结构化 Markdown 文件提交到 git。
`ai-memory-cli` 读取本地对话历史,用 AI 提取有价值的内容,保存为结构化 Markdown 文件提交到 git。
```
你的聊天记录 ai-memory 你的 git 仓库
(本地,非结构化) (提取 + 分类) (结构化,可搜索)
你的聊天记录 ai-memory-cli 你的 git 仓库
(本地,非结构化) (提取 + 分类) (结构化,可搜索)
Cursor 对话 → AI 提取 → .ai-memory/
Cursor 对话 --> AI 提取 --> .ai-memory/
Claude Code 对话 决策/待办/架构/问题 ├── decisions/

@@ -39,3 +39,2 @@ ├── architecture/

---

@@ -55,3 +54,3 @@

```bash
npx ai-memory extract
npx ai-memory-cli extract
```

@@ -63,3 +62,3 @@

git add .ai-memory/
git commit -m "chore: 添加 AI 对话知识库"
git commit --trailer "Made-with: Cursor" -m "chore: 添加 AI 对话知识库"
```

@@ -74,5 +73,5 @@

```bash
npx ai-memory list # 显示所有对话及提取状态
npx ai-memory list --source cursor # 指定来源
npx ai-memory list --json # JSON 输出
npx ai-memory-cli list # 显示所有对话及提取状态
npx ai-memory-cli list --source cursor # 指定来源
npx ai-memory-cli list --json # JSON 输出
```

@@ -85,14 +84,14 @@

```bash
npx ai-memory extract # 自动检测所有来源
npx ai-memory extract --incremental # 只处理上次之后新增或修改的对话
npx ai-memory extract --pick 4 # 按列表序号处理特定对话
npx ai-memory extract --pick 1,4,7 # 按多个序号处理
npx ai-memory extract --id b5677be8 # 按对话 ID 前缀处理
npx ai-memory extract --since "3 days ago" # 只处理最近 3 天的对话
npx ai-memory extract --since "2 weeks ago" # 也支持 weeks 单位
npx ai-memory extract --source cursor # 指定来源
npx ai-memory extract --type decision,todo # 只提取指定类型
npx ai-memory extract --dry-run # 预览要处理的对话(不调用 LLM,不写入文件)
npx ai-memory extract --verbose # 显示 LLM 请求详情
npx ai-memory extract --json # JSON 输出(CI 友好)
npx ai-memory-cli extract # 自动检测所有来源
npx ai-memory-cli extract --incremental # 只处理上次之后新增或修改的对话
npx ai-memory-cli extract --pick 4 # 按列表序号处理特定对话
npx ai-memory-cli extract --pick 1,4,7 # 按多个序号处理
npx ai-memory-cli extract --id b5677be8 # 按对话 ID 前缀处理
npx ai-memory-cli extract --since "3 days ago" # 只处理最近 3 天的对话
npx ai-memory-cli extract --since "2 weeks ago" # 也支持 weeks 单位
npx ai-memory-cli extract --source cursor # 指定来源
npx ai-memory-cli extract --type decision,todo # 只提取指定类型
npx ai-memory-cli extract --dry-run # 预览要处理的对话(不调用 LLM,不写入文件)
npx ai-memory-cli extract --verbose # 显示 LLM 请求详情
npx ai-memory-cli extract --json # JSON 输出(CI 友好)
```

@@ -103,6 +102,6 @@

```bash
npx ai-memory summary # 生成/更新 SUMMARY.md
npx ai-memory summary --output MEMORY.md # 自定义输出路径
npx ai-memory summary --focus "支付模块" # 聚焦特定主题
npx ai-memory summary --verbose # 显示 LLM 调试信息
npx ai-memory-cli summary # 生成/更新 SUMMARY.md
npx ai-memory-cli summary --output MEMORY.md # 自定义输出路径
npx ai-memory-cli summary --focus "支付模块" # 聚焦特定主题
npx ai-memory-cli summary --verbose # 显示 LLM 调试信息
```

@@ -112,14 +111,14 @@

在新对话或换设备时无缝续接上下文:
在新对话或换设备时无缝续接上下文。
```bash
npx ai-memory context # 生成上下文块(即时,无需调用 LLM)
npx ai-memory context --copy # 生成并复制到剪贴板
npx ai-memory context --topic "优惠券系统" # 聚焦特定主题
npx ai-memory context --recent 7 # 只包含最近 7 天的记忆
npx ai-memory context --output CONTEXT.md # 写入文件
npx ai-memory context --summarize # 用 LLM 生成精简的散文摘要(较慢,消耗 token)
npx ai-memory-cli context # 生成上下文块(即时,无需调用 LLM)
npx ai-memory-cli context --copy # 生成并复制到剪贴板
npx ai-memory-cli context --topic "优惠券系统" # 聚焦特定主题
npx ai-memory-cli context --recent 7 # 只包含最近 7 天的记忆
npx ai-memory-cli context --output CONTEXT.md # 写入文件
npx ai-memory-cli context --summarize # 用 LLM 生成精简的散文摘要(较慢,消耗 token)
```
默认模式(不带 `--summarize`)直接从记忆组装结构化块 — 即时、免费、无信息损失。将输出粘贴到下一次 Cursor/Claude Code 对话的开头。
默认模式(不加 `--summarize`)直接从记忆组装结构化块——即时、免费、无信息损失。将输出粘贴到下一个 Cursor/Claude Code 对话的开头。

@@ -129,6 +128,6 @@ ### `init` — 初始化配置

```bash
npx ai-memory init
npx ai-memory-cli init
```
自动检测你使用的编辑器,创建 `.ai-memory/.config.json`,并把 `.ai-memory/.state.json` 加入 `.gitignore`。
自动检测你使用的编辑器,创建 `.ai-memory/.config.json`,并将 `.ai-memory/.state.json` 加入 `.gitignore`。

@@ -139,4 +138,4 @@ ---

| 类型 | 捕获内容 |
| ---------------------- | -------------------------------------- |
| 类型 | 捕获内容 |
|------------------|----------------------------------------|
| **decision** | 技术决策:选了什么、为什么、排除了什么 |

@@ -152,12 +151,22 @@ | **architecture** | 系统设计、模块划分、数据流 |

每条记忆保存为独立的 Markdown 文件(如 `.ai-memory/decisions/2026-03-25-oauth-bridge-webview.md`):
```markdown
## [Decision] WebView OAuth Bridge 模式
# OAuth Bridge 模式(WebView)
- **日期**: 2026-03-25
- **来源**: cursor:fa49d306 (HF OAuth 集成)
- **上下文**: hf-app 需要在 App 内嵌 WebView 中完成 Google/Facebook OAuth
- **决策**: 采用 OAuth Bridge 模式,通过 static/oauth-bridge.html 中转回调,再用 postMessage 传回 App
- **理由**: App 内嵌 WebView 无法直接接收 redirect,Bridge 页面接收后中转
- **排除方案**: Deep Link(Android/iOS 行为不一致)、Custom URL Scheme(部分浏览器不支持)
- **影响**: hf-app login 页面、oauth-web、后端 OAuth 回调路由
> **日期**: 2026-03-25
> **来源**: cursor:fa49d306
> **对话**: HF OAuth 集成
---
**上下文**: hf-app 需要在 App 内嵌 WebView 中完成 Google/Facebook OAuth
**决策**: 采用 OAuth Bridge 模式,通过 static/oauth-bridge.html 中转回调,再用 postMessage 传回 App
**理由**: App 内嵌 WebView 无法直接接收 redirect,Bridge 页面接收后中转
**排除方案**: Deep Link(Android/iOS 行为不一致)、Custom URL Scheme(部分浏览器不支持)
**影响**: hf-app login 页面、oauth-web、后端 OAuth 回调路由
```

@@ -169,7 +178,7 @@

| 来源 | 数据位置 | 状态 |
| --------------------- | ------------------------------------------------ | -------------- |
| **Cursor** | `~/.cursor/projects/{name}/agent-transcripts/` | 已支持 |
| **Claude Code** | `~/.claude/projects/{path}/*.jsonl` | Beta(自动检测)|
| Windsurf | 本地存储路径未公开 | 计划中 |
| 来源 | 数据位置 | 状态 |
|-----------------|----------------------------------------------------|-----------------|
| **Cursor** | `~/.cursor/projects/{name}/agent-transcripts/` | 已支持 |
| **Claude Code** | `~/.claude/projects/{path}/*.jsonl` | Beta(自动检测)|
| Windsurf | 本地存储路径未公开 | 计划中 |

@@ -184,10 +193,10 @@ ---

# 1. 先看有哪些对话
npx ai-memory list
npx ai-memory-cli list
# 2. 全量提取(首次运行需几分钟)
npx ai-memory extract
npx ai-memory-cli extract
# 3. 提交知识库
git add .ai-memory/
git commit -m "chore: 初始化 AI 对话知识库"
git commit --trailer "Made-with: Cursor" -m "chore: 初始化 AI 对话知识库"
```

@@ -199,22 +208,22 @@

# 每次编码结束后
npx ai-memory extract --incremental
npx ai-memory-cli extract --incremental
# 提交新增记忆
git add .ai-memory/ && git commit -m "chore: 更新对话记忆"
# 提交新记忆
git add .ai-memory/ && git commit --trailer "Made-with: Cursor" -m "chore: 更新记忆"
```
### 开始新对话前(关键用法)
### 开始新对话
```bash
# 生成上下文块并复制到剪贴板
npx ai-memory context --copy
npx ai-memory-cli context --copy
# 聚焦到即将处理的模块
npx ai-memory context --topic "支付模块" --copy
# 聚焦即将开始的工作
npx ai-memory-cli context --topic "支付模块" --copy
# 或写入文件作为附件
npx ai-memory context --output CONTEXT.md
npx ai-memory-cli context --output CONTEXT.md
```
将复制的内容粘贴到新 Cursor/Claude Code 对话的**开头**,输出格式如下:
将复制的 prompt 粘贴到新 Cursor/Claude Code 会话的开头。输出示例:

@@ -224,25 +233,25 @@ ```markdown

### 技术决策(勿随意更改)
- **OAuth Bridge 模式**: WebView 无法直接接收 redirect,改用 Bridge 页中转...
### 关键决策(直接遵循,无需重新讨论)
- **使用 OAuth Bridge 模式**: WebView 无法直接接收 redirect...
### 代码约定(必须遵守)
- **不在该项目中调用 getServerSideProps**: ...
### 约定(始终遵守)
- **此项目中不使用 getServerSideProps**: ...
### 待办事项
- [ ] 给支付 webhook handler 加重试逻辑
### 当前待办
- [ ] 为支付 webhook handler 添加重试逻辑
```
AI 会立即了解你的项目决策、规范和当前状态,无需重新解释,实现无缝续接。
AI 会立即理解你项目的决策、约定和当前状态——无需重新解释。
### 处理特定对话
### 只处理指定对话
```bash
# 先查看对话列表,找到序号
npx ai-memory list
# 先找到目标对话的序号
npx ai-memory-cli list
# 只处理这一个
npx ai-memory extract --pick 3
# 只处理这一条
npx ai-memory-cli extract --pick 3
# 或按 ID 前缀匹配(list 输出中有显示)
npx ai-memory extract --id b5677be8
npx ai-memory-cli extract --id b5677be8
```

@@ -255,17 +264,12 @@

```
工作电脑 家里电脑
──────── ────────
Cursor / Claude Code 对话开发
npx ai-memory extract --incremental
git add .ai-memory/
git commit && git push
git pull
npx ai-memory context --topic "今天的工作"
复制 context → 粘贴到新对话开头
无缝续接上下文
工作机 家用机
────── ──────
Cursor / Claude Code 开发
--> npx ai-memory-cli extract --incremental
--> git add .ai-memory/
git commit --trailer "Made-with: Cursor" && git push
git pull
--> npx ai-memory-cli context --topic "今天的工作"
--> 粘贴上下文到新对话
--> 无缝续接
```

@@ -277,3 +281,3 @@

`ai-memory` 开箱即用,无需配置。如需自定义,运行 `npx ai-memory init` 或手动创建 `.ai-memory/.config.json`:
`ai-memory-cli` 开箱即用,无需配置。如需自定义,运行 `npx ai-memory-cli init` 或手动创建 `.ai-memory/.config.json`:

@@ -288,4 +292,4 @@ ```jsonc

"types": ["decision", "architecture", "convention", "todo", "issue"],
"ignoreConversations": [], // 需要跳过的对话 UUID
"minConversationLength": 5 // 少于 N 轮的对话跳过
"ignoreConversations": [], // 要跳过的对话 UUID
"minConversationLength": 5 // 跳过过短的对话
},

@@ -295,5 +299,5 @@ "output": {

"summaryFile": "SUMMARY.md",
"language": "zh" // "zh" 或 "en",影响 summary/context 输出语言
"language": "zh" // "zh" 或 "en",摘要输出语言
},
"model": "" // 留空自动选择
"model": "" // 留空则自动选择
}

@@ -304,61 +308,39 @@ ```

| 变量 | 说明 |
| ---------------------- | ------------------------------------------- |
| `AI_REVIEW_API_KEY` | API key(推荐,与 ai-review-pipeline 共用) |
| `OPENAI_API_KEY` | OpenAI API key |
| `ANTHROPIC_API_KEY` | Anthropic API key |
| `AI_REVIEW_BASE_URL` | 自定义 API 接口地址 |
| `AI_REVIEW_MODEL` | 指定模型(默认:`gpt-4o-mini`) |
| 变量 | 说明 |
|------------------------|--------------------------------------------|
| `AI_REVIEW_API_KEY` | API key(推荐,与 ai-review-pipeline 共用)|
| `OPENAI_API_KEY` | OpenAI API key |
| `ANTHROPIC_API_KEY` | Anthropic API key |
| `AI_REVIEW_BASE_URL` | 自定义 API 地址 |
| `AI_REVIEW_MODEL` | 使用的模型(默认:`gpt-4o-mini`) |
---
## 输出目录结构
## 输出结构
每条记忆是一个独立 Markdown 文件,按类型分目录存放:
每条记忆是独立的文件,按类型分目录存放:
```
.ai-memory/
├── SUMMARY.md # 项目级总结(由 summary 命令生成)
├── decisions/ # 技术决策
├── SUMMARY.md # 项目总结(summary 命令生成)
├── decisions/
│ ├── 2026-04-12-oauth-bridge-pattern.md
│ └── 2026-04-13-async-job-queue-design.md
├── architecture/ # 架构设计
├── architecture/
│ └── 2026-04-10-payment-module-design.md
├── conventions/ # 编码规范
│ └── 2026-04-08-typescript-strict-mode.md
├── todos/ # 待办事项
├── conventions/
│ └── 2026-04-08-coding-conventions.md
├── todos/
│ └── 2026-04-12-add-retry-logic.md
├── issues/ # Bug/问题记录
├── issues/
│ └── 2026-04-11-sqlite-locking-fix.md
├── .index/ # 提取索引(自动维护,无需手动修改)
├── .config.json # 配置文件(提交到 git)
├── .index/ # 提取索引(自动管理)
├── .config.json # 配置文件(建议提交到 git)
└── .state.json # 提取状态(加入 .gitignore)
```
每个 Markdown 文件的格式:
将 `.ai-memory/.state.json` 加入 `.gitignore`——它记录哪些对话已处理,是机器相关的文件。
```markdown
# OAuth Bridge 模式用于 WebView
> **日期**: 2026-03-25
> **来源**: cursor:fa49d306
> **对话**: HF OAuth 集成
---
**上下文**: hf-app 需要在 App 内嵌 WebView 中完成 OAuth
**内容**: 采用 OAuth Bridge 模式,static/oauth-bridge.html 接收 redirect 后通过 postMessage 传回 App
**理由**: App 内嵌 WebView 无法直接接收 redirect 回调
**排除方案**: Deep Link(Android/iOS 行为不一致)、Custom URL Scheme(兼容性差)
**影响**: hf-app login 页面、oauth-web、后端 OAuth 回调路由
```
`.ai-memory/.state.json` 记录已处理的对话,是机器级别的状态,加入 `.gitignore` 不用提交。
---
## CI 集成

@@ -368,4 +350,4 @@

# .github/workflows/memory.yml
- name: 提取 AI 对话记忆
run: npx ai-memory extract --incremental --json
- name: 提取 AI 记忆
run: npx ai-memory-cli extract --incremental --json
env:

@@ -379,7 +361,7 @@ AI_REVIEW_API_KEY: ${{ secrets.AI_REVIEW_API_KEY }}

- Node.js >= 22(依赖内置 `node:sqlite` 模块)
- 任意 OpenAI 兼容 provider 的 API key
- Node.js >= 22(内置 `node:sqlite` 支持所需)
- 任意 OpenAI 兼容提供商的 API key
## License
MIT — [Conor Liu](https://github.com/conorliu)
MIT — [Conor Liu](https://github.com/conorliu)

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display