@staix/agent-mem 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 STAIxBWLB
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,145 @@
1
- # Temporary Holding Version
1
+ # agent-mem
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+
5
+ Version: 0.1.0. Requires Bun 1.4.2 or later; no runtime dependencies.
6
+
7
+ ## Install
8
+
9
+ Install the public package:
10
+
11
+ ```sh
12
+ bun add -g @staix/agent-mem
13
+ amem --version
14
+ amem --help
15
+ ```
16
+
17
+ From a checkout, install development dependencies with `bun install` and run `bun src/cli.ts --help`.
18
+
19
+ ## CLI
20
+
21
+ The flat `amem` commands mirror `ahub memory` arguments; goals use `amem goal`:
22
+
23
+ ```sh
24
+ amem list --json
25
+ amem search "release decision" --limit 10
26
+ amem show <id> --json
27
+ amem graph <id-or-node> --hops 2 --json
28
+ amem suggestions
29
+ amem goal list --all --json
30
+ amem goal new "Ship a release" --milestone "Review changes"
31
+ ```
32
+
33
+ `amem goal link` requires the host task boundary; use `ahub goal link` for board links.
34
+
35
+ Memory commands: `list`, `search`, `show`, `graph`, `suggestions`, `confirm`, `reject`, `promote`, `demote`, `forget`, `supersede`, `rebuild`, `dedupe`, `sync`, `machine`, and `import`. Use `--json` where supported by the corresponding `ahub memory` command. `dedupe` previews candidates; `dedupe --apply` appends reconciliation operations.
36
+
37
+ `amem` is for the person. Every command is refused when an agent-session marker is present, including `AGENTHUB_PEER_ID`, `CLAUDECODE`, and `CODEX_THREAD_ID`. Agents read memory through the hub's `hub_memory_*` tools. Environment-marker detection is an honest-session guard, not authentication. Mutating commands also require a TTY on stdin.
38
+
39
+ This package does not ship the host's privacy policy. Person reads show stored data without screening. Import and promotion retain the built-in secret screen. Commands that cross the privacy boundary require the person's explicit `--policy-free` choice:
40
+
41
+ ```sh
42
+ amem promote <id> --policy-free
43
+ amem import claude-mem --db <path> --policy-free
44
+ amem sync --remote <private-git-url> --policy-free
45
+ ```
46
+
47
+ Use `ahub memory` for the policy-applying path. `--policy-free` does not bypass the secret screen or the sync remote's privacy checks. Forgetting a shared note appends an operation; its previous content remains in the remote's git history.
48
+
49
+ ## Shell completion
50
+
51
+ Generate completion for `zsh`, `bash`, or `fish` with `amem completion <shell>`. Generation prints a script to stdout; it does not install files or edit shell configuration. The following commands are for a person's terminal.
52
+
53
+ ### zsh
54
+
55
+ ```sh
56
+ mkdir -p ~/.zfunc
57
+ amem completion zsh > ~/.zfunc/_amem
58
+ ```
59
+
60
+ Enable it by adding these lines to `~/.zshrc`, placing `fpath` before your existing `compinit` call (use only one `compinit` call):
61
+
62
+ ```sh
63
+ fpath=(~/.zfunc $fpath)
64
+ autoload -Uz compinit
65
+ compinit
66
+ ```
67
+
68
+ Start a new shell. After upgrading amem, regenerate `~/.zfunc/_amem` with the same command and start a new shell. To remove it, delete `~/.zfunc/_amem` and start a new shell; remove the added `fpath` entry only if no other completions use that directory.
69
+
70
+ ### bash
71
+
72
+ ```sh
73
+ mkdir -p ~/.local/share/bash-completion/completions
74
+ amem completion bash > ~/.local/share/bash-completion/completions/amem
75
+ ```
76
+
77
+ Enable it by adding this line to `~/.bashrc`:
78
+
79
+ ```sh
80
+ source ~/.local/share/bash-completion/completions/amem
81
+ ```
82
+
83
+ Source that file or start a new interactive Bash. After upgrading amem, regenerate it and source it again. To remove it, delete that file, remove its source line from `~/.bashrc`, and run `complete -r amem` in the current shell (or start a new shell).
84
+
85
+ ### fish
86
+
87
+ ```sh
88
+ mkdir -p ~/.config/fish/completions
89
+ amem completion fish > ~/.config/fish/completions/amem.fish
90
+ ```
91
+
92
+ Fish loads this directory automatically. Start a new fish shell. After upgrading amem, regenerate the file and start a new shell. To remove it, delete `~/.config/fish/completions/amem.fish` and start a new shell, or run `complete -c amem -e` to clear the current shell's definitions.
93
+
94
+ ### Manual Tab checks
95
+
96
+ These interactive checks remain unrun until a person checks them in each installed shell; automated syntax/candidate tests do not establish terminal behavior.
97
+
98
+ - Type `amem ` and press Tab: top-level commands appear.
99
+ - Type `amem goal ` and press Tab: goal subcommands appear.
100
+ - Type `amem list --tier ` and press Tab: `short` and `long` appear.
101
+ - Type `amem goal state <id> ` and press Tab: goal states appear.
102
+ - Type `amem import claude-mem --db ` and press Tab in a directory containing a filename with spaces: the path remains one argument.
103
+ - Type `amem completion ` and press Tab: `zsh`, `bash`, and `fish` appear.
104
+
105
+ ## Data directory
106
+
107
+ `memoryHome()` selects `AMEM_HOME` first, otherwise `$AGENTHUB_HOME/memory`, otherwise `~/.agenthub/memory`. `AMEM_HOME` names the memory directory itself; `AGENTHUB_HOME` names its parent. Existing agent-hub memory directories need no migration. An explicit `StoreOptions.home` retains the parent-directory convention and appends `/memory`.
108
+
109
+ The JSONL segments are authoritative. `index.db` is derived and can be rebuilt. Entries are immutable; changes append operations. The compatibility schema remains `agent-hub.memory/v1` because it participates in entry IDs.
110
+
111
+ ## Library
112
+
113
+ ```ts
114
+ import { MemoryStore, memoryProjectKey } from "@staix/agent-mem";
115
+
116
+ const store = new MemoryStore();
117
+ try {
118
+ const notes = store.list({ project: memoryProjectKey(process.cwd()) });
119
+ console.log(notes);
120
+ } finally {
121
+ store.close();
122
+ }
123
+ ```
124
+
125
+ The entry point exports the store, types and identity helpers, sync and claude-mem import, project/home helpers, raw memory/goal command handlers, and platform helpers. Hosts inject their own privacy policy through `StoreOptions.screen`; CLI wrappers can supply screened reads and import policy through the raw command options.
126
+
127
+ ## Development
128
+
129
+ ```sh
130
+ bun install --frozen-lockfile
131
+ bun x tsc --noEmit
132
+ bun scripts/check-boundary.ts
133
+ bun test
134
+ npm pack --dry-run
135
+ ```
136
+
137
+ CI runs the typecheck, boundary check and tests on Ubuntu and macOS with Bun 1.4.2. Tests use synthetic fixtures and isolated homes, not personal memory stores.
138
+
139
+ ## Publishing
140
+
141
+ The first publication is the person's responsibility. npm trusted publishing requires an existing package: first publish `@staix/agent-mem` manually, then configure its GitHub Actions trusted publisher for repository `STAIxBWLB/agent-mem` and workflow `release.yml`. No publication is performed by this extraction task.
142
+
143
+ After setup, a `v*` tag triggers both operating-system gates. The tag must equal `v` plus `package.json`'s version. Only after both gates pass does the release job run `npm publish --provenance --access public`, authenticated through GitHub OIDC with `id-token: write`. The workflow uses no `NPM_TOKEN`.
144
+
145
+ License: MIT.
package/bin/amem.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bun
2
+ // Plain JavaScript: refuse Node before it can load TypeScript or bun:sqlite.
3
+ const version = /^(\d+)\.(\d+)\.(\d+)/.exec(process.versions.bun ?? "");
4
+ if (!version || Number(version[1]) < 1 || (Number(version[1]) === 1 && (Number(version[2]) < 4 || (Number(version[2]) === 4 && Number(version[3]) < 2)))) {
5
+ console.error("amem: Bun >=1.4.2 is required. Install Bun: https://bun.sh");
6
+ process.exit(1);
7
+ }
8
+ const { main } = await import("../src/cli.ts");
9
+ await main();
package/package.json CHANGED
@@ -1,6 +1,33 @@
1
1
  {
2
2
  "name": "@staix/agent-mem",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Curated, shared memory for coding agents: append-only logs, a derived index, tiers and a typed graph.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/STAIxBWLB/agent-mem.git"
10
+ },
11
+ "engines": {
12
+ "bun": ">=1.4.2"
13
+ },
14
+ "devDependencies": {
15
+ "@types/bun": "^1.3.0",
16
+ "typescript": "^5.8.0"
17
+ },
18
+ "exports": {
19
+ ".": "./src/index.ts"
20
+ },
21
+ "bin": {
22
+ "amem": "./bin/amem.js"
23
+ },
24
+ "files": [
25
+ "src/",
26
+ "bin/",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "scripts": {
31
+ "check": "bun x tsc --noEmit && bun scripts/check-boundary.ts && bun test --timeout 20000"
32
+ }
33
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { MemoryStore, looksSecret } from "./memory/store.ts";
3
+ import { rawMemoryCommand, rawGoalCommand } from "./memory/commands.ts";
4
+
5
+ import { commandHelp, parseCommand } from "./commands-table.ts";
6
+ import { completionScript } from "./completion.ts";
7
+
8
+ export const PERSON_ONLY = "amem is for the person; agents read memory through the hub's hub_memory_* tools";
9
+ const VERSION = (JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { version: string }).version;
10
+ export interface CliContext { cwd?: string; env?: Readonly<Record<string, string | undefined>>; stdinTTY?: boolean }
11
+
12
+ /** Honest native-shell defaults, matching every marker detectCliIdentity treats as non-console (invalid included). */
13
+ export function agentMarked(env: Readonly<Record<string, string | undefined>>): boolean {
14
+ return ["AGENTHUB_PEER_ID", "CLAUDECODE", "CLAUDE_CODE_SESSION_ID", "CODEX_THREAD_ID"].some(key => env[key] !== undefined);
15
+ }
16
+
17
+ export async function runCli(argv: readonly string[], context: CliContext = {}): Promise<string> {
18
+ if (agentMarked(context.env ?? process.env)) throw new Error(PERSON_ONLY);
19
+ if (argv[0] === "--version" || argv[0] === "version") return `amem ${VERSION}`;
20
+ if (argv[0] === "--help" || argv[0] === "help" || argv.includes("--help")) return commandHelp();
21
+ let parsed;
22
+ try {
23
+ parsed = parseCommand(argv);
24
+ if (parsed.path[0] === "version") return `amem ${VERSION}`;
25
+ if (parsed.path[0] === "completion") {
26
+ if (parsed.positionals.length !== 1) throw new Error("usage: amem completion zsh|bash|fish");
27
+ return completionScript(parsed.positionals[0]!);
28
+ }
29
+ } catch (error) {
30
+ if (argv.find(arg => !arg.startsWith("--")) === "completion") Object.assign(error as Error, { exitCode: 2 });
31
+ throw error;
32
+ }
33
+ const { definition, options, rawArgs: args } = parsed;
34
+ const goal = parsed.path[0] === "goal", sub = goal ? parsed.path[1]! : parsed.path[0]!;
35
+ const policyFree = options["--policy-free"] === true;
36
+ const mutating = definition.mutates === true || definition.mutates === "apply" && options["--apply"] === true || definition.mutates === "label" && options["--label"] !== undefined;
37
+ if (mutating && !(context.stdinTTY ?? process.stdin.isTTY)) throw new Error("mutating commands require a terminal on stdin");
38
+ if (definition.policyFree && !policyFree) {
39
+ throw new Error(`${sub} requires --policy-free; use ahub memory ${sub} to apply the host's privacy policy`);
40
+ }
41
+ // ponytail: board task links need the host's authenticated task boundary; add a host bridge before supporting them here.
42
+ if (goal && sub === "link") throw new Error("goal link needs host integration; use ahub goal link");
43
+ const crosses = definition.policyFree === true && policyFree;
44
+ // Reads can trigger held-note reconciliation. Defer it unless this terminal explicitly authorized crossing policy.
45
+ const open = () => new MemoryStore({ screen: note => !crosses ? "skip" : looksSecret(`${note.title ?? ""}\n${note.text}`) ? "keep" : "promote" });
46
+ const cwd = context.cwd ?? process.cwd();
47
+ // A person's reads are unscreened. Writes crossing the boundary remain secret-screened without host policy.
48
+ return goal
49
+ ? rawGoalCommand(args, cwd, { open, clean: (title, milestones) => !mutating || !looksSecret(`${title}\n${milestones.join("\n")}`) })
50
+ : rawMemoryCommand(args, cwd, { open, readable: notes => notes, importPolicy: () => () => false });
51
+ }
52
+
53
+ export async function main(argv = process.argv.slice(2)): Promise<void> {
54
+ try { console.log(await runCli(argv)); }
55
+ catch (error) { console.error(`amem: ${(error as Error).message}`); process.exitCode = (error as { exitCode?: number }).exitCode ?? 1; }
56
+ }
57
+
58
+ if (import.meta.main) await main();
@@ -0,0 +1,123 @@
1
+ import { GOAL_STATES, KINDS } from "./memory/store.ts";
2
+
3
+ export interface OptionDefinition { description: string; value?: "text" | "path" | readonly string[] }
4
+ export interface ArgumentDefinition { name: string; values?: readonly string[]; path?: boolean; variadic?: boolean }
5
+ export interface CommandDefinition {
6
+ description: string;
7
+ options?: Record<string, OptionDefinition>;
8
+ args?: readonly ArgumentDefinition[];
9
+ commands?: Record<string, CommandDefinition>;
10
+ mutates?: boolean | "apply" | "label";
11
+ policyFree?: boolean;
12
+ defaultCommand?: string;
13
+ }
14
+ const flag = (description: string): OptionDefinition => ({ description });
15
+ const text = (description: string): OptionDefinition => ({ description, value: "text" });
16
+ const json = { "--json": flag("Print JSON where supported") };
17
+ const policy = { "--policy-free": flag("Explicitly bypass host privacy policy") };
18
+ const filters = { ...json, "--tier": { description: "Memory tier", value: ["short", "long"] } as OptionDefinition,
19
+ "--kind": { description: "Note kind", value: KINDS }, "--peer": text("Author peer"), "--machine": text("Machine ID"),
20
+ "--since": text("ISO date or time"), "--limit": text("Maximum results") };
21
+ export const GLOBAL_OPTIONS = { "--help": flag("Show help") };
22
+ export const ROOT_OPTIONS = { "--version": flag("Show version"), ...json };
23
+ export const COMMANDS: Record<string, CommandDefinition> = {
24
+ list: { description: "List memory", options: filters },
25
+ search: { description: "Search memory", options: filters, args: [{ name: "words", variadic: true }] },
26
+ show: { description: "Show a note", options: json, args: [{ name: "id" }] },
27
+ graph: { description: "Show a graph", options: { ...json, "--hops": { description: "Graph depth", value: ["1", "2"] } }, args: [{ name: "id-or-node" }] },
28
+ suggestions: { description: "List pending suggestions", options: json },
29
+ confirm: { description: "Confirm a suggestion", options: json, args: [{ name: "id" }], mutates: true },
30
+ reject: { description: "Reject a suggestion", options: json, args: [{ name: "id" }], mutates: true },
31
+ promote: { description: "Promote a note", options: { ...json, ...policy }, args: [{ name: "id" }], mutates: true, policyFree: true },
32
+ demote: { description: "Demote a note", options: json, args: [{ name: "id" }], mutates: true },
33
+ forget: { description: "Forget a note", options: json, args: [{ name: "id" }], mutates: true },
34
+ supersede: { description: "Replace a note", options: json, args: [{ name: "old-id" }, { name: "new-id" }], mutates: true },
35
+ rebuild: { description: "Rebuild the derived index", options: json, mutates: true },
36
+ dedupe: { description: "Preview duplicate reconciliation", options: { ...json, "--apply": flag("Append reconciliation operations") }, mutates: "apply" },
37
+ sync: { description: "Sync a private remote", options: { ...json, ...policy, "--remote": text("Private git URL") }, mutates: true, policyFree: true },
38
+ machine: { description: "Show or label this machine", options: { ...json, "--label": text("Machine label") }, mutates: "label" },
39
+ import: { description: "Import external memory", options: { ...json, ...policy, "--db": { description: "SQLite database file", value: "path" }, "--limit": text("Maximum observations"), "--restart": flag("Restart import cursor") }, mutates: true, policyFree: true,
40
+ commands: { "claude-mem": { description: "Import claude-mem observations" } } },
41
+ goal: { description: "Long-term goals", options: json, defaultCommand: "list", commands: {
42
+ list: { description: "List goals", options: { "--all": flag("Include completed goals") } },
43
+ show: { description: "Show a goal", args: [{ name: "id" }] },
44
+ new: { description: "Create a goal", options: { "--milestone": text("Milestone"), "--depends": text("Dependency goal ID"), "--shared": flag("Share across projects") }, args: [{ name: "title", variadic: true }], mutates: true },
45
+ link: { description: "Link through ahub goal link", args: [{ name: "id" }, { name: "task-id" }], mutates: true },
46
+ state: { description: "Set goal state", args: [{ name: "id" }, { name: "state", values: GOAL_STATES }], mutates: true },
47
+ done: { description: "Complete a goal", args: [{ name: "id" }], mutates: true },
48
+ take: { description: "Take a goal lease", options: { "--force": flag("Override the prior lease") }, args: [{ name: "id" }], mutates: true },
49
+ } },
50
+ completion: { description: "Print static shell completion", args: [{ name: "shell", values: ["zsh", "bash", "fish"] }] },
51
+ help: { description: "Show help" }, version: { description: "Show version" },
52
+ };
53
+
54
+ export interface CompletionDefinition { path: string[]; options: Record<string, OptionDefinition>; args: ArgumentDefinition[]; children: string[] }
55
+ export function completionDefinitions(): CompletionDefinition[] {
56
+ const rows: CompletionDefinition[] = [{ path: [], options: { ...GLOBAL_OPTIONS, ...ROOT_OPTIONS, ...COMMANDS.list!.options }, args: [], children: Object.keys(COMMANDS) }];
57
+ const walk = (commands: Record<string, CommandDefinition>, path: string[], inherited: Record<string, OptionDefinition>) => {
58
+ for (const [name, command] of Object.entries(commands)) {
59
+ const next = [...path, name], ownOptions = { ...inherited, ...command.options };
60
+ const options = { ...ownOptions, ...(command.defaultCommand ? command.commands?.[command.defaultCommand]?.options : {}) };
61
+ rows.push({ path: next, options, args: [...command.args ?? []], children: Object.keys(command.commands ?? {}) });
62
+ if (command.commands) walk(command.commands, next, ownOptions);
63
+ }
64
+ };
65
+ walk(COMMANDS, [], GLOBAL_OPTIONS); return rows;
66
+ }
67
+ export interface ParsedCommand { path: string[]; definition: CommandDefinition; options: Record<string, string | boolean>; positionals: string[]; rawArgs: string[] }
68
+
69
+ /** Pure validation, shared definitions with help/completion; no store, files or network. */
70
+ export function parseCommand(argv: readonly string[]): ParsedCommand {
71
+ const tokens = argv.flatMap(token => {
72
+ const equals = token.startsWith("--") ? token.indexOf("=") : -1;
73
+ return equals >= 0 ? [token.slice(0, equals), token.slice(equals + 1)] : [token];
74
+ });
75
+ const path: string[] = [], options: Record<string, string | boolean> = {}, positionals: string[] = [];
76
+ let definition: CommandDefinition = { description: "amem", commands: COMMANDS };
77
+ let available: Record<string, OptionDefinition> = { ...GLOBAL_OPTIONS, ...ROOT_OPTIONS, ...COMMANDS.list!.options };
78
+ const commandIndexes = new Set<number>();
79
+ for (let i = 0; i < tokens.length; i++) {
80
+ const token = tokens[i]!;
81
+ if (token.startsWith("--")) {
82
+ const option = available[token];
83
+ if (!option) throw new Error(`unknown option ${token}`);
84
+ if (option.value !== undefined) {
85
+ const value = tokens[++i];
86
+ if (value === undefined || value.startsWith("--")) throw new Error(`${token} needs a value`);
87
+ if (Array.isArray(option.value) && !option.value.includes(value)) throw new Error(`${token} takes ${option.value.join(" or ")}`);
88
+ options[token] = value;
89
+ } else options[token] = true;
90
+ } else if (definition.commands) {
91
+ const child: CommandDefinition | undefined = definition.commands[token];
92
+ if (!child) throw new Error(`unknown command ${[...path, token].join(" ")}`);
93
+ commandIndexes.add(i);
94
+ definition = { ...definition, ...child }; path.push(token);
95
+ // Child metadata inherits policy, but its command children belong only to that child.
96
+ definition.commands = child.commands;
97
+ available = completionDefinitions().find(row => row.path.join(" ") === path.join(" "))!.options;
98
+ } else positionals.push(token);
99
+ }
100
+ if (!path.length) {
101
+ const name = options["--version"] === true ? "version" : "list";
102
+ path.push(name); definition = COMMANDS[name]!;
103
+ }
104
+ if (definition.defaultCommand && definition.commands) {
105
+ const name = definition.defaultCommand; definition = { ...definition, ...definition.commands[name]!, commands: undefined }; path.push(name);
106
+ }
107
+ if (positionals.length > (definition.args?.length ?? 0) && !definition.args?.at(-1)?.variadic) throw new Error(`too many arguments for ${path.join(" ")}`);
108
+ for (let i = 0; i < positionals.length; i++) {
109
+ const argument = definition.args?.[i];
110
+ if (argument?.values && !argument.values.includes(positionals[i]!)) throw new Error(`${argument.name} takes ${argument.values.join(" or ")}`);
111
+ }
112
+ const allowed = path[0] === "version" && options["--version"] === true ? completionDefinitions()[0]!.options : completionDefinitions().find(row => row.path.join(" ") === path.join(" "))!.options;
113
+ for (const name of Object.keys(options)) if (!allowed[name]) throw new Error(`unknown option ${name} for ${path.join(" ")}`);
114
+ const rawArgs = [...(path[0] === "goal" ? path.slice(1) : path), ...tokens.filter((arg, index) => arg !== "--policy-free" && !commandIndexes.has(index))];
115
+ return { path, definition, options, positionals, rawArgs };
116
+ }
117
+
118
+ export function commandHelp(): string {
119
+ return ["usage: amem <command> [options]", " [--help] [--version] [--json]", ...completionDefinitions().filter(row => row.path.length).map(row => {
120
+ const names = [...row.path, ...row.args.map(arg => `<${arg.name}>`)].join(" ");
121
+ return ` ${names}${Object.entries(row.options).map(([name, option]) => ` [${name}${option.value === undefined ? "" : ` <${Array.isArray(option.value) ? option.value.join("|") : option.value}>`}]`).join("")}`;
122
+ }), "--policy-free: use ahub memory for the host's privacy policy.", "Mutating commands require a terminal on stdin. Agents use the hub's hub_memory_* tools."].join("\n");
123
+ }
@@ -0,0 +1,240 @@
1
+ import { completionDefinitions } from "./commands-table.ts";
2
+
3
+ const quote = (text: string) => `'${text.replace(/'/g, `'"'"'`)}'`;
4
+ const print = (values: readonly string[]) => values.length ? `printf '%s\\n' ${values.map(quote).join(" ")}` : "true";
5
+
6
+ /** Static lookup data generated from the exact definitions the CLI uses. No installed executable is queried. */
7
+ function metadata(fish: boolean): string {
8
+ const cases: [string, string[]][] = [];
9
+ for (const definition of completionDefinitions()) {
10
+ const state = definition.path.join(" ");
11
+ cases.push([`children:${state}:`, definition.children]);
12
+ cases.push([`options:${state}:`, Object.keys(definition.options)]);
13
+ for (const [name, option] of Object.entries(definition.options)) {
14
+ if (option.value === undefined) continue;
15
+ cases.push([`type:${state}:${name}`, [typeof option.value === "string" ? option.value : "enum"]]);
16
+ if (typeof option.value !== "string") cases.push([`values:${state}:${name}`, [...option.value]]);
17
+ }
18
+ for (const [index, argument] of definition.args.entries()) {
19
+ cases.push([`argument:${state}:${index}`, [argument.path ? "path" : argument.values ? "enum" : "text"]]);
20
+ if (argument.values) cases.push([`arguments:${state}:${index}`, [...argument.values]]);
21
+ }
22
+ }
23
+ if (fish) return `function _amem_meta\n switch "$argv[1]:$argv[2]:$argv[3]"\n${cases.map(([key, values]) => ` case ${quote(key)}\n ${print(values)}`).join("\n")}\n end\nend\n`;
24
+ return `_amem_meta() {\n case "$1:$2:$3" in\n${cases.map(([key, values]) => ` ${quote(key)}) ${print(values)} ;;`).join("\n")}\n esac\n}\n`;
25
+ }
26
+
27
+ /** Bash and zsh share a POSIX-style word walker; the last supplied token is the current prefix. */
28
+ const context = `_amem_context() {
29
+ _amem_state='' _amem_pos=0 _amem_pending='' _amem_cur='' _amem_literal=0
30
+ local word child option i=0
31
+ for word do
32
+ i=$((i + 1))
33
+ if [ "$i" -eq "$#" ]; then _amem_cur=$word; break; fi
34
+ if [ -n "$_amem_pending" ]; then _amem_pending=''; continue; fi
35
+ if [ "$_amem_literal" -eq 0 ]; then
36
+ if [ "$word" = '--' ]; then _amem_literal=1; continue; fi
37
+ case "$word" in
38
+ --*=*) continue ;;
39
+ -*)
40
+ if [ -n "$(_amem_meta type "$_amem_state" "$word")" ]; then _amem_pending=$word; fi
41
+ continue ;;
42
+ esac
43
+ if [ "$_amem_pos" -eq 0 ]; then
44
+ child=''
45
+ while IFS= read -r option; do
46
+ if [ "$option" = "$word" ]; then child=$word; break; fi
47
+ done <<AMEM_CHILDREN
48
+ $(_amem_meta children "$_amem_state" '')
49
+ AMEM_CHILDREN
50
+ if [ -n "$child" ]; then _amem_state="\${_amem_state:+$_amem_state }$child"; _amem_pos=0; continue; fi
51
+ fi
52
+ fi
53
+ _amem_pos=$((_amem_pos + 1))
54
+ done
55
+ }
56
+
57
+ _amem_emit() {
58
+ local candidate
59
+ while IFS= read -r candidate; do
60
+ [ -n "$candidate" ] || continue
61
+ case "$candidate" in "$_amem_cur"*) printf '%s\\n' "$_amem_lead$candidate" ;; esac
62
+ done
63
+ }
64
+ `;
65
+
66
+ function shCandidates(zsh: boolean): string {
67
+ const files = zsh ? `local pattern="\${(b)_amem_cur}*" candidate
68
+ local -a paths
69
+ paths=( \${~pattern}(N) )
70
+ for candidate in "\${paths[@]}"; do
71
+ [ -d "$candidate" ] && candidate="$candidate/"
72
+ printf '%s\\n' "$_amem_lead$candidate"
73
+ done` : `local candidate
74
+ while IFS= read -r candidate; do
75
+ [ -d "$candidate" ] && candidate="$candidate/"
76
+ printf '%s\\n' "$_amem_lead$candidate"
77
+ done < <(compgen -f -- "$_amem_cur")`;
78
+ return `${context}
79
+ _amem_candidates() {
80
+ ${zsh ? "emulate -L zsh" : ":"}
81
+ _amem_context "$@"
82
+ local kind='' _amem_lead=''
83
+ if [ -n "$_amem_pending" ]; then
84
+ kind=$(_amem_meta type "$_amem_state" "$_amem_pending")
85
+ elif [ "$_amem_literal" -eq 0 ]; then
86
+ case "$_amem_cur" in
87
+ --*=*)
88
+ _amem_pending=\${_amem_cur%%=*}
89
+ _amem_lead="$_amem_pending="
90
+ _amem_cur=\${_amem_cur#*=}
91
+ kind=$(_amem_meta type "$_amem_state" "$_amem_pending") ;;
92
+ esac
93
+ fi
94
+ if [ -z "$kind" ] && [ -z "$_amem_pending" ]; then kind=$(_amem_meta argument "$_amem_state" "$_amem_pos"); fi
95
+ if [ "$kind" = path ]; then
96
+ ${files}
97
+ elif [ "$kind" = enum ]; then
98
+ if [ -n "$_amem_pending" ]; then _amem_meta values "$_amem_state" "$_amem_pending"
99
+ else _amem_meta arguments "$_amem_state" "$_amem_pos"; fi | _amem_emit
100
+ elif [ -n "$_amem_pending" ]; then
101
+ return 0
102
+ else
103
+ if [ "$_amem_literal" -eq 0 ]; then
104
+ {
105
+ [ "$_amem_pos" -ne 0 ] || _amem_meta children "$_amem_state" ''
106
+ _amem_meta options "$_amem_state" ''
107
+ } | _amem_emit
108
+ fi
109
+ fi
110
+ }
111
+ `;
112
+ }
113
+
114
+ function bash(): string {
115
+ return `# amem bash completion; generated from command definitions.\n${metadata(false)}${shCandidates(false)}
116
+ _amem_complete() {
117
+ local candidate
118
+ COMPREPLY=()
119
+ while IFS= read -r candidate; do COMPREPLY+=("$candidate"); done < <(_amem_candidates "\${COMP_WORDS[@]:1:COMP_CWORD}")
120
+ compopt -o filenames 2>/dev/null || :
121
+ }
122
+ complete -F _amem_complete amem
123
+ `;
124
+ }
125
+
126
+ function zsh(): string {
127
+ return `#compdef amem\n# amem zsh completion; generated from command definitions.\n${metadata(false)}${shCandidates(true)}
128
+ _amem_complete() {
129
+ emulate -L zsh
130
+ local -a candidates
131
+ _amem_context "\${words[@]:1:$((CURRENT - 1))}"
132
+ local kind=''
133
+ if [ -n "$_amem_pending" ]; then kind=$(_amem_meta type "$_amem_state" "$_amem_pending")
134
+ else kind=$(_amem_meta argument "$_amem_state" "$_amem_pos"); fi
135
+ if [ "$kind" = path ]; then _files; return; fi
136
+ candidates=( "\${(@f)$(_amem_candidates "\${words[@]:1:$((CURRENT - 1))}")}" )
137
+ (( \${#candidates} )) && compadd -- "\${candidates[@]}"
138
+ }
139
+ # An autoloaded _amem file is itself the first completion invocation.
140
+ if [[ "\${funcstack[1]-}" == _amem ]]; then
141
+ _amem_complete
142
+ elif (( $+functions[compdef] )); then
143
+ compdef _amem_complete amem
144
+ fi
145
+ `;
146
+ }
147
+
148
+ function fish(): string {
149
+ return `# amem fish completion; generated from command definitions.\n${metadata(true)}
150
+ function _amem_candidates
151
+ set -l state ''
152
+ set -l positional 0
153
+ set -l pending ''
154
+ set -l literal 0
155
+ set -l current ''
156
+ set -l index 0
157
+ for word in $argv
158
+ set index (math $index + 1)
159
+ if test $index -eq (count $argv)
160
+ set current "$word"
161
+ break
162
+ end
163
+ if test -n "$pending"
164
+ set pending ''
165
+ continue
166
+ end
167
+ if test $literal -eq 0
168
+ if test "$word" = --
169
+ set literal 1
170
+ continue
171
+ end
172
+ if string match -q -- '-*' "$word"
173
+ set -l option_kind (_amem_meta type "$state" "$word")
174
+ if not string match -q -- '*=*' "$word"; and test (count $option_kind) -gt 0
175
+ set pending "$word"
176
+ end
177
+ continue
178
+ end
179
+ if test $positional -eq 0; and contains -- "$word" (_amem_meta children "$state" '')
180
+ set state (string trim -- "$state $word")
181
+ set positional 0
182
+ continue
183
+ end
184
+ end
185
+ set positional (math $positional + 1)
186
+ end
187
+ set -l kind ''
188
+ set -l lead ''
189
+ if test -n "$pending"
190
+ set kind (_amem_meta type "$state" "$pending")
191
+ else if test $literal -eq 0; and string match -q -- '--*=*' "$current"
192
+ set pending (string split -m 1 = -- "$current")[1]
193
+ set lead "$pending="
194
+ set current (string split -m 1 = -- "$current")[2]
195
+ set kind (_amem_meta type "$state" "$pending")
196
+ end
197
+ if test -z "$kind"; and test -z "$pending"
198
+ set kind (_amem_meta argument "$state" "$positional")
199
+ end
200
+ if test "$kind" = path
201
+ for candidate in "$current"*
202
+ if test -d "$candidate"
203
+ set candidate "$candidate/"
204
+ end
205
+ printf '%s\\n' "$lead$candidate"
206
+ end
207
+ else
208
+ set -l candidates
209
+ if test "$kind" = enum
210
+ if test -n "$pending"
211
+ set candidates (_amem_meta values "$state" "$pending")
212
+ else
213
+ set candidates (_amem_meta arguments "$state" "$positional")
214
+ end
215
+ else if test -z "$pending"; and test $literal -eq 0
216
+ if test $positional -eq 0
217
+ set candidates (_amem_meta children "$state" '')
218
+ end
219
+ set -a candidates (_amem_meta options "$state" '')
220
+ end
221
+ set -l prefix_regex (string escape --style=regex -- "$current")
222
+ for candidate in $candidates
223
+ if string match -rq -- "^$prefix_regex" "$candidate"
224
+ printf '%s\\n' "$lead$candidate"
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ complete -c amem -f -a '(_amem_candidates (commandline -opc)[2..-1] (commandline -ct))'
231
+ `;
232
+ }
233
+
234
+ /** Generate a self-contained script; neither generation nor completion consults memory or network. */
235
+ export function completionScript(shell: string): string {
236
+ if (shell === "bash") return bash();
237
+ if (shell === "zsh") return zsh();
238
+ if (shell === "fish") return fish();
239
+ throw new Error(`unsupported completion shell ${shell}; choose zsh, bash or fish`);
240
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from "./memory/store.ts";
2
+ export * from "./memory/sync.ts";
3
+ export * from "./memory/import.ts";
4
+ export * from "./memory/project.ts";
5
+ export * from "./memory/home.ts";
6
+ export * from "./memory/platform.ts";
7
+ export * from "./memory/commands.ts";