ai-memory-cli
Advanced tools
+5
-2
@@ -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":"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","names":["readdir","readFile","stat","join","homedir","UUID_RE"]} |
+1
-1
| { | ||
| "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) |
+127
-145
@@ -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
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 10 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 10 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
271011
0.1%2505
0.24%344
-1.43%36
2.86%