245 lines
8.0 KiB
JavaScript
245 lines
8.0 KiB
JavaScript
import { readFileSync } from "node:fs";
|
|
import { apiFetch } from "../api.mjs";
|
|
import { emit } from "../output.mjs";
|
|
import { t } from "../i18n.mjs";
|
|
|
|
const VALID_TYPES = ["factual", "episodic", "procedural", "semantic"];
|
|
|
|
const LEGACY_TYPE_MAP = {
|
|
user: "factual",
|
|
feedback: "factual",
|
|
project: "factual",
|
|
reference: "factual",
|
|
};
|
|
|
|
/**
|
|
* Plan 21 Bug#4/D17 fix: remap legacy types in ALL CLI subcommands
|
|
* (search/list/clear in addition to add), with a stderr warning on remap.
|
|
* Returns the canonical type, or the original value (which the backend will
|
|
* 400 on if invalid) — never throws.
|
|
*/
|
|
function applyLegacyTypeMap(type) {
|
|
if (!type) return type;
|
|
if (Object.prototype.hasOwnProperty.call(LEGACY_TYPE_MAP, type)) {
|
|
const mapped = LEGACY_TYPE_MAP[type];
|
|
process.stderr.write(
|
|
`Warning: legacy type '${type}' is deprecated; using '${mapped}'. Use --type factual|episodic|procedural|semantic.\n`
|
|
);
|
|
return mapped;
|
|
}
|
|
if (!VALID_TYPES.includes(type)) {
|
|
process.stderr.write(
|
|
`Warning: unknown type '${type}'. Valid types: factual, episodic, procedural, semantic.\n`
|
|
);
|
|
}
|
|
return type;
|
|
}
|
|
|
|
function truncate(v, len = 60) {
|
|
if (v == null) return "-";
|
|
const s = String(v);
|
|
return s.length > len ? s.slice(0, len - 1) + "…" : s;
|
|
}
|
|
|
|
function fmtTs(v) {
|
|
if (!v) return "-";
|
|
try {
|
|
return new Date(v).toLocaleString();
|
|
} catch {
|
|
return String(v);
|
|
}
|
|
}
|
|
|
|
const memorySchema = [
|
|
{ key: "id", header: "ID", width: 14 },
|
|
{ key: "type", header: "Type", width: 12 },
|
|
{ key: "content", header: "Content", width: 60, formatter: truncate },
|
|
{ key: "score", header: "Score", formatter: (v) => (v != null ? v.toFixed(3) : "-") },
|
|
{ key: "createdAt", header: "Created", formatter: fmtTs },
|
|
];
|
|
|
|
function parseDuration(s) {
|
|
const m = String(s).match(/^(\d+)(d|m|y)$/i);
|
|
if (!m) return null;
|
|
const n = parseInt(m[1], 10);
|
|
const unit = m[2].toLowerCase();
|
|
const now = Date.now();
|
|
if (unit === "d") return new Date(now - n * 86400000).toISOString();
|
|
if (unit === "m") return new Date(now - n * 30 * 86400000).toISOString();
|
|
if (unit === "y") return new Date(now - n * 365 * 86400000).toISOString();
|
|
return null;
|
|
}
|
|
|
|
async function confirm(question) {
|
|
return new Promise((resolve) => {
|
|
process.stdout.write(`${question} (yes/no) `);
|
|
process.stdin.setEncoding("utf8");
|
|
process.stdin.once("data", (chunk) => {
|
|
resolve(chunk.toString().trim().toLowerCase().startsWith("y"));
|
|
});
|
|
});
|
|
}
|
|
|
|
export async function runMemorySearch(query, opts, cmd) {
|
|
const globalOpts = cmd.optsWithGlobals();
|
|
const params = new URLSearchParams({ q: query, limit: String(opts.limit ?? 20) });
|
|
const mappedSearchType = applyLegacyTypeMap(opts.type);
|
|
if (mappedSearchType) params.set("type", mappedSearchType);
|
|
if (opts.apiKey) params.set("apiKey", opts.apiKey);
|
|
if (opts.tokenBudget) params.set("tokenBudget", String(opts.tokenBudget));
|
|
const res = await apiFetch(`/api/memory?${params}`);
|
|
if (!res.ok) {
|
|
process.stderr.write(`Error: ${res.status}\n`);
|
|
process.exit(1);
|
|
}
|
|
const data = await res.json();
|
|
emit(data.items ?? data, globalOpts, memorySchema);
|
|
}
|
|
|
|
export async function runMemoryAdd(opts, cmd) {
|
|
const globalOpts = cmd.optsWithGlobals();
|
|
const content = opts.content ?? (opts.file ? readFileSync(opts.file, "utf8") : null);
|
|
if (!content) {
|
|
process.stderr.write("--content or --file required\n");
|
|
process.exit(2);
|
|
}
|
|
const resolvedType = opts.type ? applyLegacyTypeMap(opts.type) : "factual";
|
|
const body = {
|
|
content,
|
|
type: resolvedType,
|
|
...(opts.metadata ? { metadata: JSON.parse(opts.metadata) } : {}),
|
|
...(opts.apiKey ? { apiKey: opts.apiKey } : {}),
|
|
};
|
|
const res = await apiFetch("/api/memory", { method: "POST", body });
|
|
if (!res.ok) {
|
|
process.stderr.write(`Error: ${res.status}\n`);
|
|
process.exit(1);
|
|
}
|
|
const created = await res.json();
|
|
emit(created, globalOpts, memorySchema);
|
|
}
|
|
|
|
export async function runMemoryClear(opts, cmd) {
|
|
const globalOpts = cmd.optsWithGlobals();
|
|
if (!opts.yes) {
|
|
const ok = await confirm("This will delete memories. Continue?");
|
|
if (!ok) process.exit(0);
|
|
}
|
|
const params = new URLSearchParams();
|
|
const mappedClearType = applyLegacyTypeMap(opts.type);
|
|
if (mappedClearType) params.set("type", mappedClearType);
|
|
if (opts.olderThan) {
|
|
const iso = parseDuration(opts.olderThan);
|
|
if (!iso) {
|
|
process.stderr.write(`Invalid --older-than value: ${opts.olderThan}\n`);
|
|
process.exit(2);
|
|
}
|
|
params.set("olderThan", iso);
|
|
}
|
|
if (opts.apiKey) params.set("apiKey", opts.apiKey);
|
|
const res = await apiFetch(`/api/memory?${params}`, { method: "DELETE" });
|
|
const data = await res.json();
|
|
emit(data, globalOpts);
|
|
}
|
|
|
|
export async function runMemoryList(opts, cmd) {
|
|
const globalOpts = cmd.optsWithGlobals();
|
|
const params = new URLSearchParams({ limit: String(opts.limit ?? 100) });
|
|
const mappedListType = applyLegacyTypeMap(opts.type);
|
|
if (mappedListType) params.set("type", mappedListType);
|
|
if (opts.apiKey) params.set("apiKey", opts.apiKey);
|
|
const res = await apiFetch(`/api/memory?${params}`);
|
|
if (!res.ok) {
|
|
process.stderr.write(`Error: ${res.status}\n`);
|
|
process.exit(1);
|
|
}
|
|
const data = await res.json();
|
|
emit(data.items ?? data, globalOpts, memorySchema);
|
|
}
|
|
|
|
export async function runMemoryGet(id, opts, cmd) {
|
|
const globalOpts = cmd.optsWithGlobals();
|
|
const res = await apiFetch(`/api/memory/${id}`);
|
|
if (!res.ok) {
|
|
process.stderr.write(`Not found: ${id}\n`);
|
|
process.exit(1);
|
|
}
|
|
const data = await res.json();
|
|
emit(data, globalOpts, memorySchema);
|
|
}
|
|
|
|
export async function runMemoryDelete(id, opts, cmd) {
|
|
const globalOpts = cmd.optsWithGlobals();
|
|
if (!opts.yes) {
|
|
const ok = await confirm(`Delete memory ${id}?`);
|
|
if (!ok) process.exit(0);
|
|
}
|
|
const res = await apiFetch(`/api/memory/${id}`, { method: "DELETE" });
|
|
if (!res.ok) {
|
|
process.stderr.write(`Error: ${res.status}\n`);
|
|
process.exit(1);
|
|
}
|
|
process.stdout.write(`Deleted: ${id}\n`);
|
|
}
|
|
|
|
export async function runMemoryHealth(opts, cmd) {
|
|
const globalOpts = cmd.optsWithGlobals();
|
|
const res = await apiFetch("/api/memory/health");
|
|
if (!res.ok) {
|
|
process.stderr.write(`Error: ${res.status}\n`);
|
|
process.exit(1);
|
|
}
|
|
const data = await res.json();
|
|
emit(data, globalOpts);
|
|
}
|
|
|
|
export function registerMemory(program) {
|
|
const memory = program.command("memory").description(t("memory.description"));
|
|
|
|
memory
|
|
.command("search <query>")
|
|
.description(t("memory.search.description"))
|
|
.option("--type <type>", t("memory.search.type"))
|
|
.option("--limit <n>", t("memory.search.limit"), parseInt, 20)
|
|
.option("--api-key <key>", t("memory.search.api_key"))
|
|
.option("--token-budget <n>", t("memory.search.token_budget"), parseInt)
|
|
.action(runMemorySearch);
|
|
|
|
memory
|
|
.command("add")
|
|
.description(t("memory.add.description"))
|
|
.option("--content <text>", t("memory.add.content"))
|
|
.option("--file <path>", t("memory.add.file"))
|
|
.option("--type <type>", t("memory.add.type"))
|
|
.option("--metadata <json>", t("memory.add.metadata"))
|
|
.option("--api-key <key>", t("memory.add.api_key"))
|
|
.action(runMemoryAdd);
|
|
|
|
memory
|
|
.command("clear")
|
|
.description(t("memory.clear.description"))
|
|
.option("--type <type>", t("memory.clear.type"))
|
|
.option("--older-than <duration>", t("memory.clear.older"))
|
|
.option("--api-key <key>", t("memory.clear.api_key"))
|
|
.option("--yes", t("memory.clear.yes"))
|
|
.action(runMemoryClear);
|
|
|
|
memory
|
|
.command("list")
|
|
.description(t("memory.list.description"))
|
|
.option("--type <type>", t("memory.list.type"))
|
|
.option("--limit <n>", t("memory.list.limit"), parseInt, 100)
|
|
.option("--api-key <key>", t("memory.list.api_key"))
|
|
.action(runMemoryList);
|
|
|
|
memory.command("get <id>").description(t("memory.get.description")).action(runMemoryGet);
|
|
|
|
memory
|
|
.command("delete <id>")
|
|
.description(t("memory.delete.description"))
|
|
.option("--yes", t("memory.delete.yes"))
|
|
.action(runMemoryDelete);
|
|
|
|
memory.command("health").description(t("memory.health.description")).action(runMemoryHealth);
|
|
}
|