@staix/agent-mem 0.3.1 → 0.5.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/README.md +76 -6
- package/package.json +4 -3
- package/src/cli.ts +126 -15
- package/src/commands-table.ts +16 -2
- package/src/index.ts +13 -0
- package/src/mcp-memory.ts +194 -0
- package/src/mcp.ts +122 -0
- package/src/memory/candidates.ts +22 -14
- package/src/memory/capacity.ts +179 -0
- package/src/memory/capture.ts +557 -0
- package/src/memory/catalog.ts +135 -0
- package/src/memory/commands.ts +7 -4
- package/src/memory/generation-index.ts +54 -11
- package/src/memory/generation.ts +41 -2
- package/src/memory/home.ts +5 -0
- package/src/memory/import.ts +59 -0
- package/src/memory/migrate.ts +8 -0
- package/src/memory/mutation.ts +13 -1
- package/src/memory/ops-contract.ts +19 -2
- package/src/memory/policy.ts +120 -0
- package/src/memory/project.ts +22 -1
- package/src/memory/reader-lease.ts +136 -0
- package/src/memory/screen.ts +8 -0
- package/src/memory/search-fts.ts +244 -0
- package/src/memory/shard-bridge.ts +19 -0
- package/src/memory/shard-migration.ts +562 -0
- package/src/memory/shard.ts +213 -0
- package/src/memory/store.ts +376 -67
- package/src/memory/vault.ts +40 -0
- package/src/memory/work-lists.ts +331 -0
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.5.0. Requires Bun 1.4.2 or later; no runtime dependencies.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -32,11 +32,11 @@ amem goal new "Ship a release" --milestone "Review changes"
|
|
|
32
32
|
|
|
33
33
|
`amem goal link` requires the host task boundary; use `ahub goal link` for board links.
|
|
34
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.
|
|
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. `amem alias add|list` manages person-reviewed project key aliases; `amem capacity` reports and proposes capture bounds (see below).
|
|
36
36
|
|
|
37
|
-
`amem`
|
|
37
|
+
Ordinary `amem` commands are for the person and refuse agent-session markers, including `AGENTHUB_PEER_ID`, `CLAUDECODE`, and `CODEX_THREAD_ID`. `amem mcp` is the sole intentional session agent entrypoint; it supplies scoped reads and bounded candidate/work-list writes, never direct curated-note writes. Hosted agents can also use the hub's `hub_memory_*` read tools. Environment-marker detection is an honest-session guard, not authentication. Ordinary CLI mutations remain person-only and require a TTY on stdin.
|
|
38
38
|
|
|
39
|
-
This package does not ship the host's privacy policy.
|
|
39
|
+
This package does not ship the host's privacy policy. Ordinary person reads show stored data without screening; the vault-candidate listing uses its current policy. Import and promotion retain the built-in secret screen. Commands that cross the privacy boundary require the person's explicit `--policy-free` choice:
|
|
40
40
|
|
|
41
41
|
```sh
|
|
42
42
|
amem promote <id> --policy-free
|
|
@@ -131,6 +131,18 @@ These interactive checks remain unrun until a person checks them in each install
|
|
|
131
131
|
|
|
132
132
|
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.
|
|
133
133
|
|
|
134
|
+
## Capture tier and full-text search
|
|
135
|
+
|
|
136
|
+
From issue #13 (phase P1), the package carries a second, machine-local tier beside the curated store, born sharded (#15 T1): one capture shard per project key at `projects/<key-hash>/machine/<machine-id>/` under the memory home, with its own locks so two projects never contend. The capture tier records deterministic tool-event rows (project, agent, event, tool, paths, exit status and a bounded, secret-screened excerpt) in the shard's `capture.db`. It is never synced, never read as curated evidence, and never writes a curated log, so its maintenance can never evict notes, goals or work lists. Capture is bounded by a soft size cap and a retention window (defaults 512 MB per shard, 90 days), resolved at every invocation from the library options, then the shard's `config.json`, then the home default `capture-config.json`; eviction removes the oldest capture rows first in bounded batches. The 2026-10-17 owner decision keeps the P1 cap per shard; the machine-wide total is #15 T5. On lock contention a writer appends to an append-only spool and exits fast; the next uncontended writer replays the spool idempotently. The capture path makes no network or model call. The pre-T1 machine-wide `capture/` (an unreleased layout of this branch) is migrated per project on open and removed once empty.
|
|
137
|
+
|
|
138
|
+
Project identity (#15 D2) keeps `memoryProjectKey`: a worktree, a submodule and the main checkout of one repository share its key; two unrelated same-named repositories without a remote get distinct path-hash keys, flagged `local` in the shard's `project.json` and the catalog. A no-remote key hashes the main working root (using the actual top-level for a separate git dir), so keys written from the repository root keep their pre-#15 value. Known limit: a linked worktree of a no-remote separate-git-dir repository gets its own key, because git does not record that repository's main working root (probe: `alpha` and `alpha/sub` share `alpha@<hash>`, `alpha-wt` gets `alpha-wt@<hash>`); the key stays as is for root-key compatibility, and a reviewed alias reconciles its capture. Keys written by older builds from a subdirectory or worktree still used that invocation's cwd hash; a person-reviewed alias maps such a key to its canonical one for capture, work lists and the CLI and MCP project binding. Curated rows written under the old key keep their key in the log (nothing is rewritten), and the CLI and MCP read them under the canonical project: the project filter of notes, goals, candidates, vault candidates and work lists covers the canonical key and every key aliased to it, and presents those rows under the canonical key (#27). The library `MemoryStore` still reads by the raw key unless a host passes `reconcile`, and shared rows stay visible as before. Writes still go to the key's own log and shard, and a settle or forget of an aliased row lands in the shard that holds it. `amem alias add <from-key> <to-key>` appends an op to `aliases.jsonl` (latest wins, never chained, never automatic), `amem alias remove <from-key>` appends a removal (the old key reads under itself again, the history stays), `amem alias list` shows the ops including removals. Both are person-only and need a terminal. An amem or hub older than this change ignores a removal and keeps the old binding until it is upgraded, so upgrade every host that shares the home first. A correction accepted on an aliased row lives under the canonical key, so after a removal the old key shows the superseded note again (see `docs/contracts.md`). `catalog.db` in the memory home (#15 D3's storage part) is derived only from the shards and the alias log and rebuilds to the same rows; the D3 management commands are #15 T5 and not built.
|
|
139
|
+
|
|
140
|
+
`store.search(query, filter)` searches the curated tier and the current project's capture shard with SQLite FTS5 and BM25 plus tier/recency weighting: natural multi-word queries match partial term sets (the substring filter required every word), and a curated row outranks the same text in the capture tier. Prefix terms match inflections and Korean particles in either direction by retaining the original query term and adding a common-particle-stripped alternative. The FTS copy of the curated rows lives in its own derived `search.db` (`index.db`'s table set is a frozen contract and gains nothing); it is written from the same apply path as the index, and a prune, crash or rebuild is detected and resynced, so the incremental FTS copy equals a full resync. Where FTS5 is unavailable the engine falls back to the substring filter and reports `engine: "substring"`; capture recall needs FTS5 and is skipped in the fallback. The library entry exports `CaptureStore`, `captureSearch`, `readCaptureConfig`, `writeCaptureConfig`, `SearchIndex`, `fts5Available`, the shard helpers (`resolveAlias`, `appendAlias`, `listAliases`, `shardKeyHash`), the catalog (`rebuildCatalog`, `readCatalog`, `scanCatalog`) and the capacity advisor (`capacityReport`).
|
|
141
|
+
|
|
142
|
+
The R7 capacity advisor reports this machine's load and proposes capture bounds: `amem capacity [--json]` shows capture size and growth, disk, memory, the rolling `metrics.jsonl` readings and the proposal with its binding constraint; `amem capacity apply [--cap-mb N] [--retention-days N]` saves a preview, and only a matching later `--confirm` writes the home default config. Applying is person-only (agent markers refused, a terminal required); an agent may read the proposal through the library, never apply it. The 2026-10-17 owner decision keeps P1 shrink-only; growth proposals are a follow-up after the agent-hub release, while explicit person-directed growth remains available through `apply --cap-mb N`. Benchmark samples stay in the measuring script's synthetic home; only real hook and search invocations feed the advisor's rolling metrics.
|
|
143
|
+
|
|
144
|
+
Resource budgets are acceptance criteria, measured by `bun scripts/measure-budget.ts` in CI on Ubuntu and macOS: a hook-like append (including the spool-and-exit path under contention) must stay under 150 ms p95 wall time and 100 MB peak RSS (R2), the search path under 100 MB idle and 200 MB peak RSS (R3), and no amem process may remain after exit (R1). The script prints a machine-readable verdict per budget and its CI log is also the FTS5 availability evidence for Linux (R5). The AC4 retrieval re-check of the frozen P0 query set is in [docs/p1-ac4-recheck.md](https://github.com/STAIxBWLB/agent-mem/blob/main/docs/p1-ac4-recheck.md).
|
|
145
|
+
|
|
134
146
|
## Library
|
|
135
147
|
|
|
136
148
|
```ts
|
|
@@ -151,7 +163,7 @@ The entry point exports the store, types and identity helpers, sync and claude-m
|
|
|
151
163
|
|
|
152
164
|
## Contracts
|
|
153
165
|
|
|
154
|
-
Shared segments retain the `agent-hub.memory/v1` profile. `OP_CONTRACTS` and `V1_OPS` keep the shared-v1 operation contract; both shared append paths refuse unknown or truth-changing operations. The separate `GENERATION_OP_CONTRACTS` enables
|
|
166
|
+
Shared segments retain the `agent-hub.memory/v1` profile. `OP_CONTRACTS` and `V1_OPS` keep the shared-v1 operation contract; both shared append paths refuse unknown or truth-changing operations. The separate `GENERATION_OP_CONTRACTS` enables `prune` and local exact-entry `vault_link` after an explicit generation selection. `correct`, `lift`, `consolidate` and `group` remain blocked. Reader announcements cannot authorize a new operation or relax generation isolation.
|
|
155
167
|
|
|
156
168
|
The shared-v1 contract suites run the published 0.1.1 reader and compare all six v1 derived tables after replay and rebuild; generation fixtures cover their expanded derived state separately. Privacy and evidence matrices test the core's actual callbacks and sidecar boundaries; model-facing reads, PII turns, staleness checks and capture remain the host's responsibility. Direct person reads remain unscreened. Expiry after a wall-clock rollback retains the existing C05 ceiling: a rebuild can restore a previously expired row until its deadline passes again. A generation can persist exact-entry prune; shared-v1 and unpruned entries retain their existing expiry limitations.
|
|
157
169
|
|
|
@@ -192,10 +204,68 @@ Only explicit serialized expiry is eligible: a note's `expires`, or an exact dem
|
|
|
192
204
|
|
|
193
205
|
Standalone CLI prune is local-only, even over a synced target. No `--policy-free` escape enables public prune. A later policy-aware host integration must authenticate intent and attest the exact public payload and current policy revision; only verified public target/basis provenance may sync. Local masks, plans, journals, caches and sidecars never sync. Generation sync refuses public or unknown remote visibility. An interrupted apply resumes the same durable row IDs and exact partial tail; it never rewrites complete lines.
|
|
194
206
|
|
|
195
|
-
Candidate/counter sidecars are not migrated. `amem review`
|
|
207
|
+
Candidate/counter sidecars are not migrated. `amem review list --kind memory` lists the new machine-local memory candidates under a selected generation; the default legacy review list retains its generation refusal. General memory candidates can be accepted by the person with `review accept --kind memory` into the current project store after current standalone screening; older correction/fail candidates still require profile-aware host integration for generation acceptance.
|
|
196
208
|
|
|
197
209
|
Real migration remains blocked until the separately reviewed generation-capable amem release and exact-pinned agent-hub integration are installed and their running stores prove the same home/G/selector revision. Synthetic cross-writes and exact prune readbacks precede separately confirmed real migration; known local handles must be quiesced/closed and reopened through the supported session-preserving lifecycle. Release, deployment, real migration, host evidence/capture receipts and real multi-machine sync are not established by these source changes. Deactivation omits generation-only notes/prunes from the v1 view and is not a lossless rollback.
|
|
198
210
|
|
|
211
|
+
## Curated shard migration (#15 T2)
|
|
212
|
+
|
|
213
|
+
`amem migrate --shards` moves the curated single home into `projects/<key-hash>/` and `user/` through a resumable generation journal. The original single-home log bytes are never rewritten. A populated selected generation is supported and keeps its generation ID. Each shard keeps private legacy inputs in read-only `legacy/`, public-main inputs in read-only `legacy-main/`, and generation-born rows in its own `log/` and `local/`. It has its own `index.db`, `search.db` and mutation lock; original IDs, hashes, placement and own/local/public provenance are preserved. The source generation bytes stay unchanged. Immutable prune proofs are validated before effects; a multi-shard prune is translated into one deterministic row per shard, with a receipt naming the original row. Existing capture and `work/` directories are not migration inputs and are left untouched. `catalog.db` remains derived and rebuildable, now including curated entry counts and last use. Per-shard sync refs are T3 and are not built.
|
|
214
|
+
|
|
215
|
+
Before splitting a live home, every installed host must open it through `openShardStore({ project })` or the explicit user shard. Plain `new MemoryStore()` refuses any split marker, including an interrupted split, so it cannot silently keep writing the old layout. No host capability registry currently exists; migration preflight output states that limitation. Do not release T2 for agent-hub use until its memory factory is integrated with `openShardStore` and that host version is installed. Existing unannounced older builds remain the documented unchanged-source ceiling.
|
|
216
|
+
|
|
217
|
+
Use the same synthetic-home and person-authority boundaries as generation migration:
|
|
218
|
+
|
|
219
|
+
```sh
|
|
220
|
+
AMEM_HOME=/path/to/synthetic-memory amem migrate --shards --dry-run
|
|
221
|
+
AMEM_HOME=/path/to/synthetic-memory amem migrate --shards
|
|
222
|
+
AMEM_HOME=/path/to/synthetic-memory amem status
|
|
223
|
+
# Later old-layout appends are reported by status and carried in idempotently:
|
|
224
|
+
AMEM_HOME=/path/to/synthetic-memory amem migrate --resume
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Every ordinary build that reads the single-home layout holds a cooperating lease in `readers/<pid>-<start-signature>.json`, renewed within 30 seconds and removed on close. Apply and resume refuse a live or unverifiable lease and remove only leases whose pid is confirmed gone. Removed lease filenames and times are recorded in the migration journal and status; registration also refuses while the profile lock is held. Published builds without leases cannot be detected: no process scan proves their absence, and their safety rests only on unchanged legacy bytes. Old-format writers may still append after the split; `amem status` reports those rows and resume carries them into the proper shards. A resume with pending work marks the split incomplete before copying; reads and writes through new or existing shard handles refuse until complete activation. `amem status` then reports the captured phase and pending rows, and `migrate --resume` finishes it idempotently.
|
|
228
|
+
|
|
229
|
+
Library callers open an already sealed shard directly with `openShardStore({ home, project })` or `openShardStore({ home, user: true })`; these opens do not scan the legacy home or walk the catalog. A project handle reads its own shard plus the user overlay, merging lifecycle metadata before applying visibility; it never reads another project's private text. Shared notes and goals written through a project handle go to `user/`, including their lifecycle operations. Project entries accept only their own project key, while the user shard accepts shared entries. `store.dir` names the selected shard; `store.profile.home` remains the base memory directory. Library callers with a direct base path can use `memoryDir` instead of the historical parent `home` option. Two project shards have independent writer locks, and each incremental index equals its rebuild. `bun scripts/measure-shard-ac8.ts` builds 20 projects and 200,000 entries, poisons the other 19 indexes, and measures open plus ten searches on one shard against the R3 idle/peak RSS limits.
|
|
230
|
+
|
|
231
|
+
Dry run reports v1, generation-owned/local, prune and translated-prune counts, plus refused relations. A private-to-private replacement across projects refuses before effects. A shared entry superseded by a private entry also refuses: the dry run reports `shared superseded by private: N` and entry IDs only. Un-supersede it, or promote the replacement to shared, then rerun. `ponytail:` supporting that direction requires a body-free lifecycle witness; revisit when a read-only dry run on a copy of the person's home reports a nonzero count, never by probing or migrating the live home here. Private-to-shared replacements remain supported. See [the T2 contract](docs/curated-shard-migration.md).
|
|
232
|
+
|
|
233
|
+
After a split, ordinary generation `amem migrate` modes (including `--deactivate`) refuse without changing the selector. Use `amem migrate --shards --resume` to finish the split. General memory candidates can be accepted with `review accept --kind memory` into the selected project shard after standalone screening; correction/fail candidates still refuse pending shard-aware host integration. Default `prune` selects the current project's shard; use the person-only `amem prune --user --cutoff <UTC> --dry-run` and `amem prune --user --apply --plan <file>` for shared/user entries. The actor and current-project authorization checks are unchanged.
|
|
234
|
+
|
|
235
|
+
A native shard crash tail is left to its original writer's recovery. Migration checks planned copy tails and translation-receipt tails before changing activation; matching copies finish append-only and mismatches give an inspection hint. Dangling lifecycle/suggestion metadata is carried as ids-only user data and counted, so a target arriving later retains its intent. Cross-private replacements have their own ids-only count and the un-supersede/promote-to-shared remedy. During interruption the catalog keeps capture information available while `curatedEntries` is `null` and `curatedAvailable` is `false`; its cached capture counts may still lag.
|
|
236
|
+
|
|
237
|
+
## Session MCP and work lists (#13 P2)
|
|
238
|
+
|
|
239
|
+
Run `amem mcp` as a stdio child of the agent CLI. It binds the working directory, canonical project key, exact memory home and selected profile once, accepts no project/home overrides in tool arguments, and closes all stores on stdin EOF. It has no listener, daemon, watcher or model call. Ordinary person commands retain their agent-marker refusal and mutation TTY checks; `mcp` is the sole agent entry point.
|
|
240
|
+
|
|
241
|
+
MCP search, timeline and show remain available above 5,000 rows. Reads page exact-project candidates and screen them before result selection. Search ranks the newest 5,000 permitted matching candidates per tier; `since` narrows that window. Timeline can anchor an older row directly. Capture MCP indexing is lazy on queried reads, never hook construction or append. Index contention uses a call-pinned substring fallback; later searches retry. See [MCP candidate reads](docs/contracts.md#mcp-candidate-reads-13-task-209) for the bounded-window and fallback contract.
|
|
242
|
+
|
|
243
|
+
The frozen nine-tool contract for dotfiles#255 and maru#458:
|
|
244
|
+
|
|
245
|
+
| Tool | Purpose |
|
|
246
|
+
| --- | --- |
|
|
247
|
+
| `amem_search` | Screened project search, FTS5/BM25 with tier/recency ranking |
|
|
248
|
+
| `amem_show` | Exact or unique-prefix curated note; explicit `capture:<id>` recall |
|
|
249
|
+
| `amem_timeline` | Screened notes and labelled capture near an id or time |
|
|
250
|
+
| `amem_graph` | Screened curated note neighbourhood, excluding capture and candidate data |
|
|
251
|
+
| `amem_work_state_read` | Project work lists, optional closed lists/items |
|
|
252
|
+
| `amem_work_state_write` | Append scalar fields, null clears, task selects an item |
|
|
253
|
+
| `amem_remember` | General memory candidate only, awaiting person acceptance |
|
|
254
|
+
| `amem_vault_candidates` | Read-only screened durable curated notes |
|
|
255
|
+
| `amem_capacity` | Numeric machine capacity proposal; no apply capability |
|
|
256
|
+
|
|
257
|
+
Search/show/timeline label capture as recall with `evidence: false`; capture never enters the curated graph. Policy screens complete rows before selection, limits or graph construction. The standalone secret screen, private flags, PII patterns and deny paths compose with a host's stricter policy through `createMemoryMcp({cwd, base, policy})`. Screening errors fail closed. `base` is the exact memory root; historical library `home` options still name its parent. Scope and profile changes refuse further access instead of silently reopening another store. Graph reads retain the 5,000-row curated ceiling; work-list reads stream the complete ordered history without an operation-count ceiling, applying policy before latest-value reduction. The working state scales with the current result, not historical payloads. These are operational ceilings, not exposed counts of unscreened data. Graphs cover curated-note relationships, not goal/suggestion tables.
|
|
258
|
+
|
|
259
|
+
`WorkListStore` writes `projects/<key-hash>/work/` with its own append log, derived index and lock. Scalar fields reduce by latest value, null clears, list status `done` closes, and item status is `todo|doing|done|dropped`. Work lists are outside generation selection and capture eviction. `amem import claude-mem` additionally imports `work_state_entries` only through persisted person-reviewed D2 aliases, preserving source DB bytes and stable replay receipts; JSON keeps observation fields and adds `work` counts/cursor.
|
|
260
|
+
|
|
261
|
+
`amem vault-candidates --json` and the MCP read tool select durable long-term notes under current policy, excluding task/review/PR/CI progress and already-linked entries. The deterministic status filter covers English status phrases and can miss paraphrased or other-language status text; candidates require person review. No vault is opened, indexed or written. Library `recordVaultLink` records an exact-entry hash and relative Markdown path in a generation-only local append op; it survives index rebuild and shard migration, stays machine-local, and does not change shared-v1 schemas or sync semantics. There is no MCP link writer.
|
|
262
|
+
|
|
263
|
+
Do not record vault links on a home shared with a host using published agent-mem 0.3.1, including agent-hub while its exact pin stays at 0.3.1. That reader can keep listing from a shared derived index already advanced by the P2 writer, but its next full rebuild refuses the unknown operation. An independent unadvanced index refuses on refresh too. The compatibility fixture omits derived indexes and therefore forces this rebuild refusal on open. Upgrade the sharing host to a pin that includes P2 vault-link support before recording the first link; an ordinary successful list is not rollout readiness. See [the vault-link compatibility contract](docs/contracts.md#generation-only-vault-links).
|
|
264
|
+
|
|
265
|
+
General memory candidates are separate from curated evidence. `amem review accept <id> --kind memory --policy-free` remains person-only and rechecks the current standalone privacy policy before the accepted note append. Machine capacity reports numeric aggregate use across shards, as R7 requires; it does not return foreign project names or text.
|
|
266
|
+
|
|
267
|
+
`bun scripts/measure-mcp-budget.ts` exercises an actual stdio session with 5,001 curated notes, 5,001 capture rows and 10,001 work-list operations reduced into 100 items. It checks the R3 idle RSS limit of 100 MB, peak RSS limit of 200 MB across searches, latest/anchored timelines and work-list reads, and child reaping after EOF. CI runs the harness on macOS and Linux. All harness data is synthetic; live P6 measurements remain separate.
|
|
268
|
+
|
|
199
269
|
## Development
|
|
200
270
|
|
|
201
271
|
```sh
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@staix/agent-mem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -15,14 +15,15 @@
|
|
|
15
15
|
"@types/bun": "^1.3.0",
|
|
16
16
|
"typescript": "^5.8.0",
|
|
17
17
|
"@staix/agent-mem-v1": "npm:@staix/agent-mem@0.1.1",
|
|
18
|
-
"@staix/agent-mem-v02": "npm:@staix/agent-mem@0.2.0"
|
|
18
|
+
"@staix/agent-mem-v02": "npm:@staix/agent-mem@0.2.0",
|
|
19
|
+
"@staix/agent-mem-pre-vault": "npm:@staix/agent-mem@0.3.1"
|
|
19
20
|
},
|
|
20
21
|
"exports": {
|
|
21
22
|
".": "./src/index.ts",
|
|
22
23
|
"./platform": "./src/memory/platform.ts"
|
|
23
24
|
},
|
|
24
25
|
"bin": {
|
|
25
|
-
"amem": "
|
|
26
|
+
"amem": "bin/amem.js"
|
|
26
27
|
},
|
|
27
28
|
"files": [
|
|
28
29
|
"src/",
|
package/src/cli.ts
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createMemoryMcp } from "./mcp-memory.ts";
|
|
2
|
+
import { serveMcp } from "./mcp.ts";
|
|
3
|
+
import { createStandalonePolicy } from "./memory/policy.ts";
|
|
4
|
+
import { vaultCandidates } from "./memory/vault.ts";
|
|
5
|
+
import { importClaudeMemWorkState } from "./memory/import.ts";
|
|
6
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { MemoryStore, looksSecret, machineId, openShardStore, type StoreOptions } from "./memory/store.ts";
|
|
3
9
|
import { rawMemoryCommand, rawGoalCommand } from "./memory/commands.ts";
|
|
4
10
|
import { memoryProjectKey } from "./memory/project.ts";
|
|
5
11
|
import { CandidateStore, type CandidateFilter } from "./memory/candidates.ts";
|
|
12
|
+
import { aliasedKeys, appendAlias, appendUnalias, listAliases, readShardedSelection, resolveAlias } from "./memory/shard.ts";
|
|
13
|
+
import { capacityReport, capacityText } from "./memory/capacity.ts";
|
|
14
|
+
import { readCaptureConfigFile, writeCaptureConfigFile, type CaptureConfig } from "./memory/capture.ts";
|
|
6
15
|
|
|
7
16
|
import { commandHelp, parseCommand } from "./commands-table.ts";
|
|
8
17
|
import { completionScript } from "./completion.ts";
|
|
9
18
|
import { memoryHome } from "./memory/home.ts";
|
|
10
19
|
import { resolveProfile } from "./memory/generation.ts";
|
|
11
20
|
import { migrate } from "./memory/migrate.ts";
|
|
21
|
+
import { migrateShards, shardMigrationStatus } from "./memory/shard-migration.ts";
|
|
12
22
|
import { applyPrune, planPrune, PRUNE_PLAN_MAX_BYTES } from "./memory/prune.ts";
|
|
13
23
|
|
|
14
24
|
export const PERSON_ONLY = "amem is for the person; agents read memory through the hub's hub_memory_* tools";
|
|
@@ -55,7 +65,28 @@ export function agentMarked(env: Readonly<Record<string, string | undefined>>):
|
|
|
55
65
|
return ["AGENTHUB_PEER_ID", "CLAUDECODE", "CLAUDE_CODE_SESSION_ID", "CODEX_THREAD_ID"].some(key => env[key] !== undefined);
|
|
56
66
|
}
|
|
57
67
|
|
|
68
|
+
function shardStatePresent(): boolean {
|
|
69
|
+
try { lstatSync(join(memoryHome(), "shards.json")); return true; }
|
|
70
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return false; throw error; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** `reconciled` (#27): the project filter also covers every key aliased to `project`; prune and other planners keep the exact key. */
|
|
74
|
+
function curatedStore(project: string, opts: StoreOptions, reconciled = true): MemoryStore {
|
|
75
|
+
const aliases = reconciled ? aliasedKeys(memoryHome(), project) : [];
|
|
76
|
+
const withAliases: StoreOptions = aliases.length ? { ...opts, reconcile: { canonical: project, aliases } } : opts;
|
|
77
|
+
if (shardStatePresent()) {
|
|
78
|
+
readShardedSelection(memoryHome());
|
|
79
|
+
return openShardStore({ ...withAliases, project });
|
|
80
|
+
}
|
|
81
|
+
return new MemoryStore(withAliases);
|
|
82
|
+
}
|
|
83
|
+
|
|
58
84
|
export async function runCli(argv: readonly string[], context: CliContext = {}): Promise<string> {
|
|
85
|
+
if (argv.length === 1 && argv[0] === "mcp") {
|
|
86
|
+
const memory = createMemoryMcp({ cwd: context.cwd ?? process.cwd(), base: memoryHome(context.env ?? process.env) });
|
|
87
|
+
await serveMcp({ tools: memory.tools, close: memory.close, version: VERSION });
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
59
90
|
if (agentMarked(context.env ?? process.env)) throw new Error(PERSON_ONLY);
|
|
60
91
|
if (argv[0] === "--version" || argv[0] === "version") return `amem ${VERSION}`;
|
|
61
92
|
if (argv[0] === "--help" || argv[0] === "help" || argv.includes("--help")) return commandHelp();
|
|
@@ -79,30 +110,48 @@ export async function runCli(argv: readonly string[], context: CliContext = {}):
|
|
|
79
110
|
if (definition.policyFree && !policyFree) {
|
|
80
111
|
throw new Error(review ? "review accept requires --policy-free; use ahub memory to apply the host's privacy policy" : `${sub} requires --policy-free; use ahub memory ${sub} to apply the host's privacy policy`);
|
|
81
112
|
}
|
|
113
|
+
let canonicalProject: string | undefined;
|
|
114
|
+
const boundProject = () => canonicalProject ??= resolveAlias(memoryHome(), memoryProjectKey(context.cwd ?? process.cwd()));
|
|
82
115
|
if (sub === "migrate") {
|
|
83
116
|
const modes = ["--dry-run", "--resume", "--deactivate"].filter(key => options[key] === true);
|
|
84
117
|
if (modes.length > 1) throw new Error("migration modes are mutually exclusive");
|
|
85
118
|
const mode = options["--dry-run"] ? "dry-run" : options["--resume"] ? "resume" : options["--deactivate"] ? "deactivate" : "apply";
|
|
86
|
-
|
|
119
|
+
const common = { memoryDir: memoryHome(), ...(options["--generation"] ? { generation: String(options["--generation"]) } : {}), authorizedPerson: true, tty: true };
|
|
120
|
+
const shardRequested = options["--shards"] === true || (mode === "resume" && shardStatePresent());
|
|
121
|
+
if (shardRequested) {
|
|
122
|
+
if (mode === "deactivate") throw new Error("--deactivate is not a shard migration mode");
|
|
123
|
+
return JSON.stringify(await migrateShards({ ...common, mode }), null, 2);
|
|
124
|
+
}
|
|
125
|
+
return JSON.stringify(await migrate({ ...common, mode }), null, 2);
|
|
87
126
|
}
|
|
127
|
+
if (sub === "status") return JSON.stringify(shardMigrationStatus(memoryHome()), null, 2);
|
|
88
128
|
if (sub === "prune") {
|
|
89
129
|
const applying = options["--apply"] === true;
|
|
90
|
-
if (applying ? options["--dry-run"] || options["--cutoff"] || !options["--plan"] : !options["--dry-run"] || !options["--cutoff"] || options["--plan"]) throw new Error("usage: amem prune --cutoff <UTC> --dry-run | --apply --plan <file>");
|
|
130
|
+
if (applying ? options["--dry-run"] || options["--cutoff"] || !options["--plan"] : !options["--dry-run"] || !options["--cutoff"] || options["--plan"]) throw new Error("usage: amem prune [--user] --cutoff <UTC> --dry-run | [--user] --apply --plan <file>");
|
|
91
131
|
let input: string | undefined;
|
|
92
132
|
if (applying) {
|
|
93
133
|
input = readPrunePlanFile(String(options["--plan"]));
|
|
94
134
|
}
|
|
95
|
-
const
|
|
135
|
+
const project = boundProject();
|
|
136
|
+
if (options["--user"] === true) {
|
|
137
|
+
if (!shardStatePresent()) throw new Error("prune --user requires an active curated shard migration; the default prune command selects the current project");
|
|
138
|
+
readShardedSelection(memoryHome());
|
|
139
|
+
}
|
|
140
|
+
const store = options["--user"] === true
|
|
141
|
+
? openShardStore({ memoryDir: memoryHome(), user: true, deferPromotions: true })
|
|
142
|
+
: curatedStore(project, { deferPromotions: true }, false);
|
|
96
143
|
try {
|
|
97
|
-
const auth = { project
|
|
144
|
+
const auth = { project, actor: "person" as const, authorize: () => true };
|
|
98
145
|
return JSON.stringify(applying ? applyPrune(store.prunePort(), input, auth) : planPrune(store.prunePort(), { cutoff: String(options["--cutoff"]) }, auth), null, 2);
|
|
99
146
|
} finally { store.close(); }
|
|
100
147
|
}
|
|
101
148
|
if (review) {
|
|
102
|
-
if (
|
|
149
|
+
if (sub === "accept" && options["--kind"] !== "memory" && shardStatePresent()) throw new Error("candidate acceptance requires shard-aware review integration; use --kind memory only for a general memory candidate");
|
|
150
|
+
if (sub === "accept" && options["--kind"] !== "memory" && resolveProfile(memoryHome()).kind === "generation") throw new Error("candidate review is not yet integrated with this generation; use --kind memory only for a general memory candidate");
|
|
151
|
+
if (sub === "list" && options["--kind"] !== "memory" && resolveProfile(memoryHome()).kind === "generation") throw new Error("candidate review is not yet integrated with this generation; select --kind memory for the standalone memory candidate surface");
|
|
103
152
|
const id = parsed.positionals[0];
|
|
104
153
|
if (sub !== "list" && (!id || !/^[a-f0-9]{64}$/.test(id))) throw new Error("review accept/reject requires a full 64-character candidate id");
|
|
105
|
-
const project =
|
|
154
|
+
const project = boundProject(), aliases = aliasedKeys(memoryHome(), project), candidates = new CandidateStore(aliases.length ? { reconcile: { canonical: project, aliases } } : {});
|
|
106
155
|
try {
|
|
107
156
|
if (sub === "list") {
|
|
108
157
|
const rows = candidates.list({ project, ...(options["--status"] ? { status: options["--status"] as CandidateFilter["status"] } : {}),
|
|
@@ -115,34 +164,96 @@ export async function runCli(argv: readonly string[], context: CliContext = {}):
|
|
|
115
164
|
}
|
|
116
165
|
const candidate = candidates.get(id!, project);
|
|
117
166
|
if (!candidate) throw new Error("candidate unavailable");
|
|
167
|
+
if (options["--kind"] === "memory" && candidate.input.kind !== "memory") throw new Error("--kind memory accepts only a general memory candidate");
|
|
118
168
|
if (candidate.input.kind === "decay") throw new Error("decay candidates are listing-only; use ahub memory demote or forget after review");
|
|
169
|
+
if (candidate.input.kind !== "memory" && (shardStatePresent() || resolveProfile(memoryHome()).kind === "generation")) throw new Error("candidate acceptance requires shard-aware and profile-aware host integration for this candidate kind; the candidate remains pending");
|
|
170
|
+
const standalone = createStandalonePolicy();
|
|
171
|
+
if (!await standalone.screen(candidate.input)) throw new Error("candidate refused by current host policy; current standalone privacy policy declined it");
|
|
119
172
|
// Candidate acceptance uses the actual secret screen; the core defers unrelated automatic promotions.
|
|
120
|
-
const store =
|
|
173
|
+
const store = curatedStore(project, { deferPromotions: true, screen: note => looksSecret(`${note.title ?? ""}\n${note.text}`) ? "keep" : "promote" });
|
|
121
174
|
try {
|
|
122
175
|
const result = candidates.accept(id!, { project, actor: "person", verdict: "public", store,
|
|
123
176
|
policy: (proposal, _target, note) => !looksSecret(JSON.stringify(proposal.input)) && !looksSecret(JSON.stringify(note)),
|
|
124
177
|
});
|
|
125
|
-
return options["--json"] ? JSON.stringify(result, null, 2) : `accepted candidate ${id} as ordinary
|
|
178
|
+
return options["--json"] ? JSON.stringify(result, null, 2) : `accepted candidate ${id} as ordinary note ${result.noteId}`;
|
|
126
179
|
} finally { store.close(); }
|
|
127
180
|
} finally { candidates.close(); }
|
|
128
181
|
}
|
|
182
|
+
if (sub === "vault-candidates") {
|
|
183
|
+
const project = boundProject(), store = curatedStore(project, { deferPromotions: true });
|
|
184
|
+
try {
|
|
185
|
+
const result = await vaultCandidates(store, { project, policy: createStandalonePolicy(), ...(options["--limit"] ? { limit: Number(options["--limit"]) } : {}) });
|
|
186
|
+
return JSON.stringify(result, null, 2);
|
|
187
|
+
} finally { store.close(); }
|
|
188
|
+
}
|
|
189
|
+
if (sub === "capacity") {
|
|
190
|
+
const share = options["--disk-share"];
|
|
191
|
+
if (share !== undefined && (!Number.isFinite(Number(share)) || Number(share) <= 0 || Number(share) > 1)) throw new Error("--disk-share must be a number in (0, 1]");
|
|
192
|
+
const report = capacityReport({ ...(share !== undefined ? { diskShare: Number(share) } : {}) });
|
|
193
|
+
if (parsed.path[1] !== "apply") return options["--json"] ? JSON.stringify(report, null, 2) : capacityText(report);
|
|
194
|
+
// Person-only (agent marker and TTY are refused above): preview first, --confirm writes the home default config.
|
|
195
|
+
const file = join(memoryHome(), "capture-config.json");
|
|
196
|
+
const target: CaptureConfig = {
|
|
197
|
+
capMb: options["--cap-mb"] ? Number(options["--cap-mb"]) : report.proposal.capMb,
|
|
198
|
+
retentionDays: options["--retention-days"] ? Number(options["--retention-days"]) : report.proposal.retentionDays,
|
|
199
|
+
};
|
|
200
|
+
if (!Number.isFinite(target.capMb) || target.capMb <= 0 || !Number.isFinite(target.retentionDays) || target.retentionDays <= 0) throw new Error("capacity apply needs positive --cap-mb and --retention-days");
|
|
201
|
+
const preview = { file, current: readCaptureConfigFile(file), target, reason: report.proposal.reason };
|
|
202
|
+
const previewFile = join(memoryHome(), "capture-config.preview.json");
|
|
203
|
+
if (options["--confirm"] !== true) {
|
|
204
|
+
writeFileSync(previewFile, JSON.stringify(preview) + "\n", { mode: 0o600 });
|
|
205
|
+
return JSON.stringify({ preview, confirm: "re-run with --confirm to write" }, null, 2);
|
|
206
|
+
}
|
|
207
|
+
let approved: { file?: string; current?: unknown; target?: CaptureConfig };
|
|
208
|
+
try { approved = JSON.parse(readFileSync(previewFile, "utf8")) as typeof approved; }
|
|
209
|
+
catch { throw new Error("capacity apply needs a preview first; re-run without --confirm"); }
|
|
210
|
+
if (approved.file !== file || JSON.stringify(approved.current) !== JSON.stringify(preview.current)
|
|
211
|
+
|| approved.target?.capMb !== target.capMb || approved.target?.retentionDays !== target.retentionDays) {
|
|
212
|
+
throw new Error("capacity preview changed; re-run without --confirm");
|
|
213
|
+
}
|
|
214
|
+
const written = writeCaptureConfigFile(file, target);
|
|
215
|
+
rmSync(previewFile, { force: true });
|
|
216
|
+
return JSON.stringify({ preview, written: { cap_mb: written.capMb, retention_days: written.retentionDays } }, null, 2);
|
|
217
|
+
}
|
|
218
|
+
if (sub === "alias") {
|
|
219
|
+
if (parsed.path[1] === "add") {
|
|
220
|
+
const [from, to] = parsed.positionals;
|
|
221
|
+
if (!from || !to) throw new Error("usage: amem alias add <from-key> <to-key>");
|
|
222
|
+
const op = appendAlias(memoryHome(), { alias: from, key: to, actor: "person", machine: machineId(memoryHome()).id });
|
|
223
|
+
return options["--json"] ? JSON.stringify(op, null, 2) : `alias ${op.alias} -> ${op.key}`;
|
|
224
|
+
}
|
|
225
|
+
if (parsed.path[1] === "remove") {
|
|
226
|
+
// #27 option 2: person only (ordinary CLI refusal of agent markers above, a TTY through `mutates`), append only, the latest op wins.
|
|
227
|
+
const [from] = parsed.positionals;
|
|
228
|
+
if (!from || parsed.positionals.length !== 1) throw new Error("usage: amem alias remove <alias-key>");
|
|
229
|
+
const op = appendUnalias(memoryHome(), { alias: from, actor: "person", machine: machineId(memoryHome()).id });
|
|
230
|
+
return options["--json"] ? JSON.stringify(op, null, 2) : `alias ${op.alias} removed`;
|
|
231
|
+
}
|
|
232
|
+
const ops = listAliases(memoryHome());
|
|
233
|
+
return options["--json"] ? JSON.stringify(ops, null, 2) : ops.length ? ops.map((op) => op.op === "alias" ? `${op.at} ${op.alias} -> ${op.key} (${op.actor})` : `${op.at} ${op.alias} removed (${op.actor})`).join("\n") : "no aliases recorded";
|
|
234
|
+
}
|
|
129
235
|
// ponytail: board task links need the host's authenticated task boundary; add a host bridge before supporting them here.
|
|
130
236
|
if (goal && sub === "link") throw new Error("goal link needs host integration; use ahub goal link");
|
|
131
237
|
const crosses = definition.policyFree === true && policyFree;
|
|
132
|
-
// Reads can trigger held-note reconciliation. Defer it unless this terminal explicitly authorized crossing policy.
|
|
133
|
-
const open = () => new MemoryStore({ screen: note => !crosses ? "skip" : looksSecret(`${note.title ?? ""}\n${note.text}`) ? "keep" : "promote" });
|
|
134
238
|
const cwd = context.cwd ?? process.cwd();
|
|
239
|
+
// Reads can trigger held-note reconciliation. Defer it unless this terminal explicitly authorized crossing policy.
|
|
240
|
+
const open = () => curatedStore(boundProject(), { screen: note => !crosses ? "skip" : looksSecret(`${note.title ?? ""}\n${note.text}`) ? "keep" : "promote" });
|
|
135
241
|
// A person's reads are unscreened. Writes crossing the boundary remain secret-screened without host policy.
|
|
136
|
-
if (goal) return rawGoalCommand(args, cwd, { open, clean: (title, milestones) => !mutating || !looksSecret(`${title}\n${milestones.join("\n")}`) });
|
|
242
|
+
if (goal) return rawGoalCommand(args, cwd, { project: boundProject(), open, clean: (title, milestones) => !mutating || !looksSecret(`${title}\n${milestones.join("\n")}`) });
|
|
137
243
|
const receipt: { bridgeStatus?: { skippedRows: number } } = {};
|
|
138
|
-
const output = await rawMemoryCommand(args, cwd, { open, readable: notes => notes, importPolicy: () => () => false,
|
|
244
|
+
const output = await rawMemoryCommand(args, cwd, { project: boundProject(), open, readable: notes => notes, importPolicy: () => () => false,
|
|
139
245
|
onBridgeStatus: status => { receipt.bridgeStatus = status; } });
|
|
246
|
+
if (sub === "import" && parsed.path[1] === "claude-mem") {
|
|
247
|
+
const policy = createStandalonePolicy();
|
|
248
|
+
const work = await importClaudeMemWorkState({ base: memoryHome(), project: boundProject(), policy: row => policy.screen(row), ...(options["--db"] ? { db: String(options["--db"]) } : {}), ...(options["--limit"] ? { limit: Number(options["--limit"]) } : {}), restart: options["--restart"] === true });
|
|
249
|
+
return options["--json"] ? JSON.stringify({ ...JSON.parse(output), work }, null, 2) : `${output}\nwork lists: ${JSON.stringify(work)}`;
|
|
250
|
+
}
|
|
140
251
|
return !options["--json"] && receipt.bridgeStatus
|
|
141
252
|
? `${output}\n(legacy malformed lines skipped: ${receipt.bridgeStatus.skippedRows})` : output;
|
|
142
253
|
}
|
|
143
254
|
|
|
144
255
|
export async function main(argv = process.argv.slice(2)): Promise<void> {
|
|
145
|
-
try {
|
|
256
|
+
try { const output = await runCli(argv); if (output) console.log(output); }
|
|
146
257
|
catch (error) { console.error(`amem: ${(error as Error).message}`); process.exitCode = (error as { exitCode?: number }).exitCode ?? 1; }
|
|
147
258
|
}
|
|
148
259
|
|
package/src/commands-table.ts
CHANGED
|
@@ -21,6 +21,8 @@ const filters = { ...json, "--tier": { description: "Memory tier", value: ["shor
|
|
|
21
21
|
export const GLOBAL_OPTIONS = { "--help": flag("Show help") };
|
|
22
22
|
export const ROOT_OPTIONS = { "--version": flag("Show version"), ...json };
|
|
23
23
|
export const COMMANDS: Record<string, CommandDefinition> = {
|
|
24
|
+
mcp: { description: "Run the project-bound MCP stdio server for this session" },
|
|
25
|
+
"vault-candidates": { description: "List screened durable curated vault candidates", options: { ...json, "--limit": text("Maximum candidates (1..100)") } },
|
|
24
26
|
list: { description: "List memory", options: filters },
|
|
25
27
|
search: { description: "Search memory", options: filters, args: [{ name: "words", variadic: true }] },
|
|
26
28
|
show: { description: "Show a note", options: json, args: [{ name: "id" }] },
|
|
@@ -50,19 +52,31 @@ export const COMMANDS: Record<string, CommandDefinition> = {
|
|
|
50
52
|
review: { description: "Review machine-local non-evidentiary candidates", options: json, defaultCommand: "list", commands: {
|
|
51
53
|
list: { description: "List review candidates", options: {
|
|
52
54
|
"--status": { description: "Candidate status", value: ["pending", "accepting", "accepted", "rejected", "all"] },
|
|
53
|
-
"--kind": { description: "Candidate kind", value: ["correction", "fail", "decay"] }, "--limit": text("Maximum candidates (1..500)"),
|
|
55
|
+
"--kind": { description: "Candidate kind", value: ["correction", "fail", "decay", "memory"] }, "--limit": text("Maximum candidates (1..500)"),
|
|
54
56
|
} },
|
|
55
|
-
accept: { description: "Accept one candidate through the current memory policy", options: policy, args: [{ name: "full-candidate-id" }], mutates: true, policyFree: true },
|
|
57
|
+
accept: { description: "Accept one candidate through the current memory policy", options: { ...policy, "--kind": { description: "Explicit standalone candidate kind for selected profiles", value: ["memory"] } }, args: [{ name: "full-candidate-id" }], mutates: true, policyFree: true },
|
|
56
58
|
reject: { description: "Reject one candidate", args: [{ name: "full-candidate-id" }], mutates: true },
|
|
57
59
|
} },
|
|
58
60
|
migrate: { description: "Select a person-approved generation or deactivate it", mutates: true, options: {
|
|
59
61
|
...json, "--dry-run": flag("Read-only migration receipt"), "--generation": text("Explicit 32-hex generation ID"),
|
|
60
62
|
"--resume": flag("Resume the recorded migration"), "--deactivate": flag("Select shared v1 without deleting a generation"),
|
|
63
|
+
"--shards": flag("Migrate the curated home into project and user shards"),
|
|
61
64
|
} },
|
|
65
|
+
status: { description: "Report shard migration and pending old-layout appends", options: json },
|
|
62
66
|
prune: { description: "Plan or apply exact-entry durable expiry in a generation", mutates: true, options: {
|
|
63
67
|
...json, "--cutoff": text("Canonical UTC expiry cutoff"), "--dry-run": flag("Print a local sealed plan without writes"),
|
|
68
|
+
"--user": flag("Select the user shard explicitly after a curated split (default: current project)"),
|
|
64
69
|
"--apply": flag("Apply the exact local plan"), "--plan": { description: "Sealed local plan JSON file", value: "path" },
|
|
65
70
|
} },
|
|
71
|
+
capacity: { description: "Report capture use and propose R6 bounds (R7)", options: { ...json, "--disk-share": text("Disk-share bound for the proposal (default 0.02)") }, commands: {
|
|
72
|
+
apply: { description: "Write the proposed (or given) bounds to the home capture config", mutates: true, options: {
|
|
73
|
+
...json, "--cap-mb": text("Capture cap in MB"), "--retention-days": text("Capture retention in days"), "--confirm": flag("Write after the preview") } },
|
|
74
|
+
} },
|
|
75
|
+
alias: { description: "Person-reviewed project key aliases (#15 D2)", options: json, defaultCommand: "list", commands: {
|
|
76
|
+
list: { description: "List alias ops" },
|
|
77
|
+
add: { description: "Map one key onto its canonical key (append-only op)", args: [{ name: "from-key" }, { name: "to-key" }], mutates: true },
|
|
78
|
+
remove: { description: "Remove an alias so the key resolves to itself again (append-only op, the latest wins)", args: [{ name: "alias-key" }], mutates: true },
|
|
79
|
+
} },
|
|
66
80
|
completion: { description: "Print static shell completion", args: [{ name: "shell", values: ["zsh", "bash", "fish"] }] },
|
|
67
81
|
help: { description: "Show help" }, version: { description: "Show version" },
|
|
68
82
|
};
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export * from "./memory/store.ts";
|
|
2
|
+
export * from "./memory/capture.ts";
|
|
3
|
+
export { fts5Available, ftsTerms, SearchIndex, captureSearch } from "./memory/search-fts.ts";
|
|
4
|
+
export type { CuratedDoc, RankedRef } from "./memory/search-fts.ts";
|
|
5
|
+
export * from "./memory/shard.ts";
|
|
6
|
+
export * from "./memory/catalog.ts";
|
|
7
|
+
export * from "./memory/capacity.ts";
|
|
2
8
|
export * from "./memory/sync.ts";
|
|
3
9
|
export * from "./memory/import.ts";
|
|
4
10
|
export * from "./memory/project.ts";
|
|
@@ -10,3 +16,10 @@ export * from "./memory/ops-contract.ts";
|
|
|
10
16
|
export * from "./memory/generation.ts";
|
|
11
17
|
export * from "./memory/migrate.ts";
|
|
12
18
|
export * from "./memory/prune.ts";
|
|
19
|
+
export * from "./memory/shard-migration.ts";
|
|
20
|
+
|
|
21
|
+
export * from "./mcp.ts";
|
|
22
|
+
export * from "./mcp-memory.ts";
|
|
23
|
+
export * from "./memory/policy.ts";
|
|
24
|
+
export * from "./memory/work-lists.ts";
|
|
25
|
+
export * from "./memory/vault.ts";
|