@staix/agent-mem 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cli-agent-reads.ts +27 -13
- package/src/cli.ts +12 -4
- package/src/commands-table.ts +33 -6
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Curated memory with append-only JSONL logs, a derived SQLite index, short- and long-term tiers, a typed graph, goals with leases, and git sync between machines. Extracted from [agent-hub](https://github.com/STAIxBWLB/agent-hub), with compatible storage and entry IDs.
|
|
4
4
|
|
|
5
|
-
Version: 0.
|
|
5
|
+
Version: 0.15.0. Requires Bun 1.4.2 or later; no runtime dependencies.
|
|
6
6
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
package/package.json
CHANGED
package/src/cli-agent-reads.ts
CHANGED
|
@@ -6,6 +6,16 @@ import type { ParsedCommand } from "./commands-table.ts";
|
|
|
6
6
|
|
|
7
7
|
const UNAVAILABLE = "Memory operation unavailable or refused; no partial result is shown.";
|
|
8
8
|
const RESULT_BYTES = 262_144;
|
|
9
|
+
const AGENT_READS = "Agent reads: search, list, show, brief, graph, vault-candidates, capacity.";
|
|
10
|
+
|
|
11
|
+
/** A refusal decided from the parsed command alone, before any store or file is opened: safe to show as is. */
|
|
12
|
+
export class AgentUsageError extends Error {
|
|
13
|
+
constructor(message: string) { super(message); this.name = "AgentUsageError"; }
|
|
14
|
+
}
|
|
15
|
+
const accepts = (command: string, allowed: readonly string[]) => new AgentUsageError(allowed.length
|
|
16
|
+
? `agent-marked ${command} accepts only ${allowed.length === 1 ? allowed[0] : `${allowed.slice(0, -1).join(", ")} and ${allowed.at(-1)}`}.`
|
|
17
|
+
: `agent-marked ${command} accepts no options.`);
|
|
18
|
+
|
|
9
19
|
type ReadCall = { tool: keyof ReturnType<typeof createMemoryMcp>["tools"]; args: Record<string, unknown> };
|
|
10
20
|
|
|
11
21
|
function values(argv: readonly string[], name: string): string[] {
|
|
@@ -29,50 +39,54 @@ function positiveLimit(value: string | boolean | undefined): number | undefined
|
|
|
29
39
|
function route(parsed: ParsedCommand, argv: readonly string[]): ReadCall | undefined {
|
|
30
40
|
const { path, options, positionals, definition } = parsed;
|
|
31
41
|
|
|
32
|
-
if (path[0] === "machine" && options["--label"] === undefined) throw new
|
|
42
|
+
if (path[0] === "machine" && options["--label"] === undefined) throw new AgentUsageError(`amem machine without --label is not available to an agent-marked session. ${AGENT_READS}`);
|
|
33
43
|
// Write/preview/confirmation paths continue through the guarded administrative handlers.
|
|
34
44
|
if (definition.mutates !== undefined) return undefined;
|
|
35
|
-
if (path[0] === "project" && path[1] === "export") throw new
|
|
45
|
+
if (path[0] === "project" && path[1] === "export") throw new AgentUsageError(`amem project export is not available to an agent-marked session. ${AGENT_READS}`);
|
|
36
46
|
if (path[0] === "setup") return undefined;
|
|
37
47
|
|
|
38
48
|
const flags = Object.keys(options).filter(key => key !== "--json" && key !== "--policy-free");
|
|
39
49
|
const only = (...allowed: string[]) => flags.every(flag => allowed.includes(flag));
|
|
40
50
|
const limit = positiveLimit(options["--limit"]);
|
|
41
|
-
if (options["--limit"] !== undefined && limit === undefined) throw new
|
|
51
|
+
if (options["--limit"] !== undefined && limit === undefined) throw new AgentUsageError("--limit takes a whole number from 1 to 100.");
|
|
42
52
|
|
|
43
53
|
if (path[0] === "search") {
|
|
44
|
-
if (!only("--limit", "--since", "--explain")
|
|
54
|
+
if (!only("--limit", "--since", "--explain")) throw accepts("search", ["--limit", "--since", "--explain"]);
|
|
55
|
+
if (!positionals.join(" ").trim()) throw new AgentUsageError("agent-marked search needs a query.");
|
|
45
56
|
return { tool: "amem_search", args: { query: positionals.join(" "), ...(limit === undefined ? {} : { limit }), ...(options["--since"] !== undefined ? { since: options["--since"] } : {}), ...(options["--explain"] === true ? { explain: true } : {}) } };
|
|
46
57
|
}
|
|
47
58
|
if (path[0] === "list") {
|
|
48
|
-
if (!only("--limit", "--since")) throw
|
|
59
|
+
if (!only("--limit", "--since")) throw accepts("list", ["--limit", "--since"]);
|
|
49
60
|
return { tool: "amem_timeline", args: { ...(limit === undefined ? {} : { limit }), ...(options["--since"] !== undefined ? { since: options["--since"] } : {}) } };
|
|
50
61
|
}
|
|
51
62
|
if (path[0] === "show") {
|
|
52
|
-
if (!only()
|
|
63
|
+
if (!only()) throw accepts("show", []);
|
|
64
|
+
if (positionals.length !== 1 || !positionals[0]!.trim()) throw new AgentUsageError("agent-marked show takes exactly one id.");
|
|
53
65
|
return { tool: "amem_show", args: { id: positionals[0] } };
|
|
54
66
|
}
|
|
55
67
|
if (path[0] === "brief") {
|
|
56
|
-
if (!only("--for", "--paths", "--symbols", "--bytes")
|
|
68
|
+
if (!only("--for", "--paths", "--symbols", "--bytes")) throw accepts("brief", ["--for", "--paths", "--symbols", "--bytes"]);
|
|
69
|
+
if (typeof options["--for"] !== "string") throw new AgentUsageError("agent-marked brief needs --for.");
|
|
57
70
|
const rawBytes = options["--bytes"];
|
|
58
|
-
if (rawBytes !== undefined && !/^[1-9][0-9]*$/.test(String(rawBytes))) throw new
|
|
71
|
+
if (rawBytes !== undefined && !/^[1-9][0-9]*$/.test(String(rawBytes))) throw new AgentUsageError("--bytes takes a positive whole number.");
|
|
59
72
|
return { tool: "amem_brief", args: { intent: options["--for"], paths: values(argv, "--paths"), symbols: values(argv, "--symbols"), ...(rawBytes === undefined ? {} : { maxBytes: Number(rawBytes) }) } };
|
|
60
73
|
}
|
|
61
74
|
if (path[0] === "graph") {
|
|
62
|
-
if (!only("--hops")
|
|
75
|
+
if (!only("--hops")) throw accepts("graph", ["--hops"]);
|
|
76
|
+
if (positionals.length !== 1) throw new AgentUsageError("agent-marked graph takes exactly one start id.");
|
|
63
77
|
const hops = options["--hops"] === undefined ? 1 : Number(options["--hops"]);
|
|
64
|
-
if (hops !== 1 && hops !== 2) throw new
|
|
78
|
+
if (hops !== 1 && hops !== 2) throw new AgentUsageError("--hops takes 1 or 2.");
|
|
65
79
|
return { tool: "amem_graph", args: { start: positionals[0], ...(options["--hops"] === undefined ? {} : { hops }) } };
|
|
66
80
|
}
|
|
67
81
|
if (path[0] === "vault-candidates") {
|
|
68
|
-
if (!only("--limit") || options["--all-projects"] !== undefined || options["--project"] !== undefined) throw
|
|
82
|
+
if (!only("--limit") || options["--all-projects"] !== undefined || options["--project"] !== undefined) throw accepts("vault-candidates", ["--limit"]);
|
|
69
83
|
return { tool: "amem_vault_candidates", args: limit === undefined ? {} : { limit } };
|
|
70
84
|
}
|
|
71
85
|
if (path[0] === "capacity" && path.length === 1) {
|
|
72
|
-
if (!only()) throw
|
|
86
|
+
if (!only()) throw accepts("capacity", []);
|
|
73
87
|
return { tool: "amem_capacity", args: {} };
|
|
74
88
|
}
|
|
75
|
-
throw new
|
|
89
|
+
throw new AgentUsageError(`amem ${path.join(" ")} is not available to an agent-marked session. ${AGENT_READS}`);
|
|
76
90
|
}
|
|
77
91
|
|
|
78
92
|
/** Route marked-agent read commands through the same startup-bound MCP policy and handlers. */
|
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { activatePublicPruneView } from "./memory/public-prune-activation.ts";
|
|
2
|
-
import { agentCliRead } from "./cli-agent-reads.ts";
|
|
2
|
+
import { AgentUsageError, agentCliRead } from "./cli-agent-reads.ts";
|
|
3
3
|
import { cliAttribution, withCliAttribution } from "./memory/cli-attribution.ts";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import type { Writable } from "node:stream";
|
|
@@ -212,6 +212,14 @@ export async function runCli(argv: readonly string[], context: CliContext = {}):
|
|
|
212
212
|
if (!readable) throw new CliPartialApplyError(error.applied.length, error.jsonOutput);
|
|
213
213
|
const safe = new DecayApplyError(error.report, undefined); safe.jsonOutput = error.jsonOutput; throw safe;
|
|
214
214
|
}
|
|
215
|
+
// Data-free errors (decided before any store or file is opened) keep their own message.
|
|
216
|
+
if (marked && error instanceof AgentUsageError) throw error;
|
|
217
|
+
if (marked) {
|
|
218
|
+
// parseCommand is pure: when it rejects argv, the error it raised is a usage error, not memory data.
|
|
219
|
+
let usage: Error | undefined;
|
|
220
|
+
try { parseCommand(argv); } catch (parseError) { usage = parseError as Error; }
|
|
221
|
+
if (usage) throw Object.assign(new Error(usage.message), (error as { exitCode?: number }).exitCode === undefined ? {} : { exitCode: (error as { exitCode?: number }).exitCode });
|
|
222
|
+
}
|
|
215
223
|
// Marked administrative failures must not reveal a target's private state or native errors.
|
|
216
224
|
if (marked) throw Error("Memory operation unavailable or refused; no partial result is shown.");
|
|
217
225
|
throw error;
|
|
@@ -223,14 +231,14 @@ async function runCliCommand(argv: readonly string[], context: CliContext): Prom
|
|
|
223
231
|
await serveMcp({ tools: memory.tools, close: memory.close, onEmitted: memory.onEmitted, version: VERSION });
|
|
224
232
|
return "";
|
|
225
233
|
}
|
|
226
|
-
if (argv[0] === "hook") {
|
|
234
|
+
if (argv[0] === "hook" && !argv.includes("--help")) {
|
|
227
235
|
const { hookMain } = await import("./hooks.ts");
|
|
228
236
|
return hookMain(argv.slice(1));
|
|
229
237
|
}
|
|
230
238
|
if (argv[0] === "ingest") {
|
|
231
239
|
// Capture-only native intake: no project/home override, policy bypass or curated write capability.
|
|
232
240
|
const parsed = parseCommand(argv);
|
|
233
|
-
if (parsed.options["--help"] === true) return commandHelp();
|
|
241
|
+
if (parsed.options["--help"] === true) return commandHelp(argv);
|
|
234
242
|
const kind = parsed.positionals[0];
|
|
235
243
|
if (parsed.positionals.length !== 1 || !["codex", "kimi", "pi", "qwen"].includes(kind ?? "")) throw new Error("usage: amem ingest codex|kimi|pi|qwen [--since ISO] [--json]");
|
|
236
244
|
const { ingestTranscript } = await import("./ingest.ts");
|
|
@@ -244,7 +252,7 @@ async function runCliCommand(argv: readonly string[], context: CliContext): Prom
|
|
|
244
252
|
if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "version")) return `amem ${VERSION}`;
|
|
245
253
|
if (argv.length === 1 && (argv[0] === "--help" || argv[0] === "help")) return commandHelp();
|
|
246
254
|
if (argv[0] === "--version" || argv[0] === "version") return `amem ${VERSION}`;
|
|
247
|
-
if (argv[0] === "--help" || argv[0] === "help" || argv.includes("--help")) return commandHelp();
|
|
255
|
+
if (argv[0] === "--help" || argv[0] === "help" || argv.includes("--help")) return commandHelp(argv);
|
|
248
256
|
let parsed;
|
|
249
257
|
try {
|
|
250
258
|
parsed = parseCommand(argv);
|
package/src/commands-table.ts
CHANGED
|
@@ -167,7 +167,7 @@ export function parseCommand(argv: readonly string[]): ParsedCommand {
|
|
|
167
167
|
options[token] = value;
|
|
168
168
|
} else options[token] = true;
|
|
169
169
|
} else if (definition.commands) {
|
|
170
|
-
const child: CommandDefinition | undefined = definition.commands[token];
|
|
170
|
+
const child: CommandDefinition | undefined = Object.hasOwn(definition.commands, token) ? definition.commands[token] : undefined;
|
|
171
171
|
if (!child) throw new Error(`unknown command ${[...path, token].join(" ")}`);
|
|
172
172
|
commandIndexes.add(i);
|
|
173
173
|
definition = { ...definition, ...child }; path.push(token);
|
|
@@ -194,9 +194,36 @@ export function parseCommand(argv: readonly string[]): ParsedCommand {
|
|
|
194
194
|
return { path, definition, options, positionals, rawArgs };
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
197
|
+
const optionLabel = (name: string, option: OptionDefinition) =>
|
|
198
|
+
`${name}${option.value === undefined ? "" : ` <${Array.isArray(option.value) ? option.value.join("|") : option.value}>`}`;
|
|
199
|
+
const usageRow = (row: CompletionDefinition) =>
|
|
200
|
+
`${[...row.path, ...row.args.map(arg => `<${arg.name}>`)].join(" ")}${Object.entries(row.options).map(([name, option]) => ` [${optionLabel(name, option)}]`).join("")}`;
|
|
201
|
+
const described = (rows: [string, string][]) => {
|
|
202
|
+
// Long enum labels (review --kind) would push every description far right; they overflow instead.
|
|
203
|
+
const width = Math.min(24, Math.max(...rows.map(([label]) => label.length)));
|
|
204
|
+
return rows.map(([label, description]) => ` ${label.padEnd(width)} ${description}`);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/** Full command list, or one command's help when argv names a command path (defaultCommand is not applied). */
|
|
208
|
+
export function commandHelp(argv: readonly string[] = []): string {
|
|
209
|
+
const path: string[] = [], rows = completionDefinitions();
|
|
210
|
+
let commands: Record<string, CommandDefinition> | undefined = COMMANDS, command: CommandDefinition | undefined, row = rows[0]!;
|
|
211
|
+
for (let i = 0; i < argv.length; i++) {
|
|
212
|
+
const token = argv[i]!;
|
|
213
|
+
if (token.startsWith("--")) {
|
|
214
|
+
// An option's value is never a command name.
|
|
215
|
+
if (!token.includes("=") && row.options[token]?.value !== undefined) i++;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!commands || !Object.hasOwn(commands, token)) break;
|
|
219
|
+
command = commands[token]!; path.push(token); commands = command.commands;
|
|
220
|
+
row = rows.find(candidate => candidate.path.join(" ") === path.join(" "))!;
|
|
221
|
+
}
|
|
222
|
+
if (command && path[0] !== "help") {
|
|
223
|
+
const lines = [`usage: amem ${usageRow(row)}`, "", command.description, "", "options:", ...described(Object.entries(row.options).map(([name, option]) => [optionLabel(name, option), option.description]))];
|
|
224
|
+
if (command.commands) lines.push("", "commands:", ...described(Object.entries(command.commands).map(([name, child]) => [name, child.description])));
|
|
225
|
+
return lines.join("\n");
|
|
226
|
+
}
|
|
227
|
+
return ["usage: amem <command> [options]", " [--help] [--version] [--json]", ...completionDefinitions().filter(row => row.path.length).map(row => ` ${usageRow(row)}`),
|
|
228
|
+
"--policy-free: accepted and ignored; the configured privacy policy (amem.config.json in the memory home, narrowed by .amem/config) always applies.", "Agent-marked reads use the MCP privacy ceiling. Explicit apply/confirm flags do not require a terminal; review walk and the note editor remain interactive."].join("\n");
|
|
202
229
|
}
|