/** * Memory CRUD commands: add, search, get, list, update, delete. */ import fs from "node:fs"; import { printError, printInfo, printScope, printSuccess, timedStatus } from "../branding.js"; import type { Backend } from "../backend/base.js"; import { formatAddResult, formatJson, formatMemoriesTable, formatMemoriesText, formatSingleMemory, printResultSummary, } from "../output.js"; export async function cmdAdd( backend: Backend, text: string | undefined, opts: { userId?: string; agentId?: string; appId?: string; runId?: string; messages?: string; file?: string; metadata?: string; immutable: boolean; noInfer: boolean; expires?: string; categories?: string; enableGraph: boolean; output: string; }, ): Promise { let msgs: Record[] | undefined; let content = text; // Read from file if (opts.file) { try { const raw = fs.readFileSync(opts.file, "utf-8"); msgs = JSON.parse(raw); } catch (e) { printError(`Failed to read file: ${e instanceof Error ? e.message : e}`); process.exit(1); } } // Parse messages JSON else if (opts.messages) { try { msgs = JSON.parse(opts.messages); } catch (e) { printError(`Invalid JSON in --messages: ${e instanceof Error ? e.message : e}`); process.exit(1); } } // Read from stdin if piped else if (!content && !process.stdin.isTTY) { content = fs.readFileSync(0, "utf-8").trim(); } if (!content && !msgs) { printError("No content provided. Pass text, --messages, --file, or pipe via stdin."); process.exit(1); } let meta: Record | undefined; if (opts.metadata) { try { meta = JSON.parse(opts.metadata); } catch { printError("Invalid JSON in --metadata."); process.exit(1); } } let cats: string[] | undefined; if (opts.categories) { try { cats = JSON.parse(opts.categories); } catch { cats = opts.categories.split(",").map((c) => c.trim()); } } let result: Record; try { result = await timedStatus("Adding memory...", async () => { return backend.add(content ?? undefined, msgs, { userId: opts.userId, agentId: opts.agentId, appId: opts.appId, runId: opts.runId, metadata: meta, immutable: opts.immutable, infer: !opts.noInfer, expires: opts.expires, categories: cats, enableGraph: opts.enableGraph, }); }); } catch (e) { printError(e instanceof Error ? e.message : String(e)); process.exit(1); } if (opts.output === "quiet") return; if (opts.output === "json") { formatAddResult(result, opts.output); return; } console.log(); printScope({ user_id: opts.userId, agent_id: opts.agentId, app_id: opts.appId, run_id: opts.runId }); const results = Array.isArray(result) ? result : ((result.results as unknown[]) ?? [result]); const count = results.length; printSuccess(`Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`); formatAddResult(result, opts.output); } export async function cmdSearch( backend: Backend, query: string | undefined, opts: { userId?: string; agentId?: string; appId?: string; runId?: string; topK: number; threshold: number; rerank: boolean; keyword: boolean; filterJson?: string; fields?: string; enableGraph: boolean; output: string; }, ): Promise { if (!query) { printError("No query provided. Pass a query argument or pipe via stdin."); process.exit(1); } let filters: Record | undefined; if (opts.filterJson) { try { filters = JSON.parse(opts.filterJson); } catch { printError("Invalid JSON in --filter."); process.exit(1); } } const fieldList = opts.fields ? opts.fields.split(",").map((f) => f.trim()) : undefined; const start = performance.now(); let results: Record[]; try { results = await timedStatus("Searching memories...", async () => { return backend.search(query!, { userId: opts.userId, agentId: opts.agentId, appId: opts.appId, runId: opts.runId, topK: opts.topK, threshold: opts.threshold, rerank: opts.rerank, keyword: opts.keyword, filters, fields: fieldList, enableGraph: opts.enableGraph, }); }); } catch (e) { printError(e instanceof Error ? e.message : String(e)); process.exit(1); } const elapsed = (performance.now() - start) / 1000; if (opts.output === "json") { formatJson(results); } else if (opts.output === "table") { if (results.length > 0) { formatMemoriesTable(results); printResultSummary({ count: results.length, durationSecs: elapsed, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } }); } else { console.log(); printInfo("No memories found matching your query."); console.log(); } } else { if (results.length > 0) { formatMemoriesText(results); printResultSummary({ count: results.length, durationSecs: elapsed, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } }); } else { console.log(); printInfo("No memories found matching your query."); console.log(); } } } export async function cmdGet( backend: Backend, memoryId: string, opts: { output: string }, ): Promise { let result: Record; try { result = await timedStatus("Fetching memory...", async () => { return backend.get(memoryId); }); } catch (e) { printError(e instanceof Error ? e.message : String(e)); process.exit(1); } formatSingleMemory(result, opts.output); } export async function cmdList( backend: Backend, opts: { userId?: string; agentId?: string; appId?: string; runId?: string; page: number; pageSize: number; category?: string; after?: string; before?: string; enableGraph: boolean; output: string; }, ): Promise { const start = performance.now(); let results: Record[]; try { results = await timedStatus("Listing memories...", async () => { return backend.listMemories({ userId: opts.userId, agentId: opts.agentId, appId: opts.appId, runId: opts.runId, page: opts.page, pageSize: opts.pageSize, category: opts.category, after: opts.after, before: opts.before, enableGraph: opts.enableGraph, }); }); } catch (e) { printError(e instanceof Error ? e.message : String(e)); process.exit(1); } const elapsed = (performance.now() - start) / 1000; if (opts.output === "json") { formatJson(results); } else if (opts.output === "table") { if (results.length > 0) { formatMemoriesTable(results); printResultSummary({ count: results.length, durationSecs: elapsed, page: opts.page, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } }); } else { console.log(); printInfo("No memories found."); console.log(); } } else { if (results.length > 0) { formatMemoriesText(results, "memories"); printResultSummary({ count: results.length, durationSecs: elapsed, page: opts.page, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } }); } else { console.log(); printInfo("No memories found."); console.log(); } } } export async function cmdUpdate( backend: Backend, memoryId: string, text: string | undefined, opts: { metadata?: string; output: string }, ): Promise { let meta: Record | undefined; if (opts.metadata) { try { meta = JSON.parse(opts.metadata); } catch { printError("Invalid JSON in --metadata."); process.exit(1); } } const start = performance.now(); let result: Record; try { result = await timedStatus("Updating memory...", async () => { return backend.update(memoryId, text, meta); }); } catch (e) { printError(e instanceof Error ? e.message : String(e)); process.exit(1); } const elapsed = (performance.now() - start) / 1000; if (opts.output === "json") { formatJson(result); } else if (opts.output !== "quiet") { printSuccess(`Memory ${memoryId.slice(0, 8)} updated (${elapsed.toFixed(2)}s)`); } } export async function cmdDelete( backend: Backend, memoryId: string, opts: { output: string; dryRun?: boolean; force?: boolean }, ): Promise { if (opts.dryRun) { let mem: Record; try { mem = await backend.get(memoryId); } catch (e) { printError(e instanceof Error ? e.message : String(e)); process.exit(1); } const text = (mem.memory ?? mem.text ?? "") as string; printInfo(`Would delete memory ${memoryId.slice(0, 8)}: ${text}`); printInfo("No changes made."); return; } const start = performance.now(); let result: Record; try { result = await timedStatus("Deleting...", async () => { return backend.delete(memoryId); }); } catch (e) { printError(e instanceof Error ? e.message : String(e)); process.exit(1); } const elapsed = (performance.now() - start) / 1000; if (opts.output === "json") { formatJson(result); } else if (opts.output !== "quiet") { printSuccess(`Memory ${memoryId.slice(0, 8)} deleted (${elapsed.toFixed(2)}s)`); } } export async function cmdDeleteAll( backend: Backend, opts: { force: boolean; dryRun?: boolean; all?: boolean; userId?: string; agentId?: string; appId?: string; runId?: string; output: string; }, ): Promise { if (opts.all) { // Project-wide wipe using wildcard entity IDs if (opts.dryRun) { printInfo("Would delete ALL memories project-wide."); printInfo("No changes made."); return; } if (!opts.force) { const readline = await import("node:readline"); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = await new Promise((resolve) => { rl.question(`\n \u26a0 Delete ALL memories across the ENTIRE project? This cannot be undone. [y/N] `, resolve); }); rl.close(); if (answer.toLowerCase() !== "y") { printInfo("Cancelled."); process.exit(0); } } const start = performance.now(); let result: Record; try { result = await timedStatus("Deleting all memories project-wide...", async () => { return backend.delete(undefined, { all: true, userId: "*", agentId: "*", appId: "*", runId: "*", }); }); } catch (e) { printError(e instanceof Error ? e.message : String(e)); process.exit(1); } const elapsed = (performance.now() - start) / 1000; if (opts.output === "json") { formatJson(result); } else if (opts.output !== "quiet") { if (result.message) { printInfo("Deletion started. Memories will be removed in the background."); } else { printSuccess(`All project memories deleted (${elapsed.toFixed(2)}s)`); } } return; } if (opts.dryRun) { let memories: Record[]; try { memories = await backend.listMemories({ userId: opts.userId, agentId: opts.agentId, appId: opts.appId, runId: opts.runId, }); } catch (e) { printError(e instanceof Error ? e.message : String(e)); process.exit(1); } printInfo(`Would delete ${memories.length} memories.`); printInfo("No changes made."); return; } if (!opts.force) { const scopeParts: string[] = []; if (opts.userId) scopeParts.push(`user=${opts.userId}`); if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`); if (opts.appId) scopeParts.push(`app=${opts.appId}`); if (opts.runId) scopeParts.push(`run=${opts.runId}`); const scope = scopeParts.length > 0 ? scopeParts.join(", ") : "ALL entities"; const readline = await import("node:readline"); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = await new Promise((resolve) => { rl.question(`\n \u26a0 Delete ALL memories for ${scope}? This cannot be undone. [y/N] `, resolve); }); rl.close(); if (answer.toLowerCase() !== "y") { printInfo("Cancelled."); process.exit(0); } } const start = performance.now(); let result: Record; try { result = await timedStatus("Deleting all memories...", async () => { return backend.delete(undefined, { all: true, userId: opts.userId, agentId: opts.agentId, appId: opts.appId, runId: opts.runId, }); }); } catch (e) { printError(e instanceof Error ? e.message : String(e)); process.exit(1); } const elapsed = (performance.now() - start) / 1000; if (opts.output === "json") { formatJson(result); } else if (opts.output !== "quiet") { if (result.message) { printInfo("Deletion started. Memories will be removed in the background."); } else { printSuccess(`All matching memories deleted (${elapsed.toFixed(2)}s)`); } } }