@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-mem",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Bounded project memory tools and seven deterministic capture hooks, without a resident worker.",
5
5
  "mcpServers": {
6
6
  "agent-mem": {
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.14.0. Requires Bun 1.4.2 or later; no runtime dependencies.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staix/agent-mem",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Curated, shared memory for coding agents: append-only logs, a derived index, tiers and a typed graph.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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 Error(UNAVAILABLE);
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 Error(UNAVAILABLE);
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 Error(UNAVAILABLE);
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") || !positionals.join(" ").trim()) throw new Error(UNAVAILABLE);
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 new Error(UNAVAILABLE);
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() || positionals.length !== 1 || !positionals[0]!.trim()) throw new Error(UNAVAILABLE);
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") || typeof options["--for"] !== "string") throw new Error(UNAVAILABLE);
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 Error(UNAVAILABLE);
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") || positionals.length !== 1) throw new Error(UNAVAILABLE);
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 Error(UNAVAILABLE);
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 new Error(UNAVAILABLE);
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 new Error(UNAVAILABLE);
86
+ if (!only()) throw accepts("capacity", []);
73
87
  return { tool: "amem_capacity", args: {} };
74
88
  }
75
- throw new Error(UNAVAILABLE);
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);
@@ -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
- export function commandHelp(): string {
198
- return ["usage: amem <command> [options]", " [--help] [--version] [--json]", ...completionDefinitions().filter(row => row.path.length).map(row => {
199
- const names = [...row.path, ...row.args.map(arg => `<${arg.name}>`)].join(" ");
200
- return ` ${names}${Object.entries(row.options).map(([name, option]) => ` [${name}${option.value === undefined ? "" : ` <${Array.isArray(option.value) ? option.value.join("|") : option.value}>`}]`).join("")}`;
201
- }), "--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");
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
  }