open-memex 0.1.0 → 0.2.0-alpha
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/AGENTS.md +31 -5
- package/CONTRIBUTING.md +31 -0
- package/README.md +17 -7
- package/docs/SCOPES.md +81 -0
- package/docs/V2-DESIGN.md +484 -0
- package/package.json +1 -1
- package/scripts/smoke-pure.ts +209 -9
- package/src/cli.ts +138 -22
- package/src/config.ts +3 -10
- package/src/index.ts +18 -6
- package/src/redact.ts +151 -4
- package/src/retrieve/cjk.ts +63 -0
- package/src/retrieve/inject.ts +2 -2
- package/src/retrieve/search.ts +115 -28
- package/src/scope.ts +7 -2
- package/src/store/db.ts +62 -14
- package/src/store/lifecycle.ts +280 -0
- package/src/store/markdown.ts +163 -11
- package/src/store/sync.ts +53 -9
- package/src/store/v2migrate.ts +190 -0
- package/src/tools/memory.ts +88 -27
package/AGENTS.md
CHANGED
|
@@ -46,25 +46,51 @@ Layout: `memories/<scope_key>/<id>.md` (YAML frontmatter + body) + `index.db` (S
|
|
|
46
46
|
|
|
47
47
|
## Scope keys
|
|
48
48
|
|
|
49
|
-
- `
|
|
49
|
+
- `personal` scope key is literal `"personal"` (v1 called this `user`; renamed in v2, design §19).
|
|
50
50
|
- `project` scope key: `project__<sanitized-name>__<12-hex-sha256>`, seeded from normalized git origin URL, else lowercased cwd. See `src/scope.ts`. Same repo across machines → same key (intentional; enables future git-commit of memories).
|
|
51
|
+
- `org`, `team`, `public` are reserved — the schema rejects writes. See `docs/SCOPES.md`.
|
|
51
52
|
- The scope key **changes** when a repo gains/loses a git origin. `src/index.ts` logs a one-shot warning on load if it finds files under the legacy cwd-only key (`resolveCwdScope`). Use `cli scopes` to enumerate all scope dirs and `cli migrate --from <old>` to reconcile — see `src/store/migrate.ts`. No auto-migration; two unrelated repos at the same cwd would silently merge.
|
|
52
53
|
- Read-only callers must use `memoriesDirPath` (in `src/paths.ts`), never `memoriesDirFor`. The latter `mkdir -p`s the directory as a side effect and will pollute storage with empty scope dirs.
|
|
53
54
|
|
|
54
55
|
## Capture / write path invariants
|
|
55
56
|
|
|
56
57
|
Every write path (tool, keyword hook, CLI `add`) must:
|
|
57
|
-
1. Call `redact(content, cfg.redactPatterns)`.
|
|
58
|
+
1. Call `redact(content, cfg.redactPatterns)`. Built-in provider patterns live in `src/redact.ts` (always on); config `redactPatterns` is for user extras only.
|
|
58
59
|
2. If `hadSecret` → refuse the write (do not save `[REDACTED]` unless the user wrapped it in `<private>…</private>`).
|
|
59
|
-
3. `
|
|
60
|
+
3. Check `findDuplicates` (design §3.4): identical content is idempotent (return existing id); near-duplicates (similarity ≥ 0.8) warn but save — suggest `supersede` when the new content replaces the old.
|
|
61
|
+
4. `writeMemoryFile` first, then `readMemoryFile` + `upsertFromFile` to keep FTS in sync.
|
|
62
|
+
|
|
63
|
+
## Lifecycle invariants (design §3.3)
|
|
64
|
+
|
|
65
|
+
- Statuses: `active → superseded | deprecated | retracted | archived`. Retrieval excludes `retracted`/`archived`, ranks `active` above `deprecated`.
|
|
66
|
+
- Never hand-write `status: superseded` or half a chain. Use the `supersede` code path (`src/store/lifecycle.ts`): the old record keeps its file, flips to `superseded`, and both sides get `supersedes`/`superseded_by`. Only `active` memories can be superseded.
|
|
67
|
+
- Chain integrity is self-healing: on read, a missing counterpart is auto-completed with a warning; a dangling pointer warns but is never fabricated. Don't "fix" chains by editing frontmatter directly — let the read path do it.
|
|
68
|
+
- Frontmatter is `schema_version: 2`. The SQLite index schema is versioned separately and rebuilds automatically on version change — never hand-edit `index.db`.
|
|
60
69
|
|
|
61
70
|
Keyword capture fires from `chat.message` on the assistant's `output.parts` text. Patterns live in `src/capture/keywords.ts` / config `keywordPatterns`; regex group 1 is the memory body.
|
|
62
71
|
|
|
63
72
|
Context injection happens exactly once per session in `experimental.chat.system.transform`, guarded by an in-memory `Set<sessionID>` in `src/index.ts`. It is not persisted — restarting opencode re-injects on the next first turn.
|
|
64
73
|
|
|
65
|
-
## Design constraints
|
|
74
|
+
## Design constraints — read the frozen design first
|
|
75
|
+
|
|
76
|
+
`docs/V2-DESIGN.md` is the frozen protocol v0.2 (zero open questions). Per its §12:
|
|
77
|
+
AGENTS.md answers "how should AI work here"; the design doc answers "why is it
|
|
78
|
+
built this way" (principles, iron rules, D1–D13 decision log). Before changing
|
|
79
|
+
architecture, scope semantics, lifecycle, or the protocol surface (frontmatter
|
|
80
|
+
schema, MCP tools, CLI contract), read the relevant design section — the decision
|
|
81
|
+
log records what was already considered and rejected.
|
|
82
|
+
|
|
83
|
+
Still hard: no cloud, no silent sync (explicit pull only), Markdown is the source
|
|
84
|
+
of truth, `personal` scope never leaves the machine. Embeddings are an *optional
|
|
85
|
+
capability* per the design — do not add them (or LLM-driven extraction, or a
|
|
86
|
+
knowledge graph) without updating the design doc first. `PLAN.md` tracks the
|
|
87
|
+
build roadmap; the design doc tracks the *why*.
|
|
88
|
+
|
|
89
|
+
## Branch workflow
|
|
66
90
|
|
|
67
|
-
|
|
91
|
+
`main` (stable, mirrors npm) ← `V2` (v2 integration) ← `V2-dev-p<n>`
|
|
92
|
+
(phase work; draft PRs into `V2`). Never create `V2/<anything>` — git can't
|
|
93
|
+
hold `V2` and `V2/…` simultaneously. Full rules: `CONTRIBUTING.md`.
|
|
68
94
|
|
|
69
95
|
## Style notes
|
|
70
96
|
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Contributing to open-memex
|
|
2
|
+
|
|
3
|
+
## Branch workflow
|
|
4
|
+
|
|
5
|
+
- `main` — stable. Mirrors the npm release line. Never commit directly;
|
|
6
|
+
only merge from `V2` when a milestone is tested and ready to release.
|
|
7
|
+
- `V2` — integration branch for the v2 line. Phase work lands here via
|
|
8
|
+
pull request. Merges to `main` only after the milestone is dogfooded
|
|
9
|
+
(plugin tested in opencode, `migrate --to-v2 --dry-run` clean on real data).
|
|
10
|
+
- `V2-dev-p<n>` — phase dev branches (e.g. `V2-dev-p1`). Open as **draft**
|
|
11
|
+
PRs against `V2`; mark ready and merge after local testing passes.
|
|
12
|
+
|
|
13
|
+
**Naming rule:** never create `V2/<anything>` — git cannot hold a branch
|
|
14
|
+
named `V2` and a branch named `V2/…` at the same time (ref file vs.
|
|
15
|
+
directory conflict). Use the flat `V2-dev-*` form instead.
|
|
16
|
+
|
|
17
|
+
## Before opening a PR
|
|
18
|
+
|
|
19
|
+
- `npm run typecheck` is clean
|
|
20
|
+
- `node --experimental-strip-types scripts/smoke-pure.ts` passes
|
|
21
|
+
- If you touched SQLite/FTS: `open-memex reindex` works against a scratch
|
|
22
|
+
`MY_O_MEMORY_HOME`
|
|
23
|
+
- No secrets in fixtures (redaction tests are the exception)
|
|
24
|
+
|
|
25
|
+
## Design authority
|
|
26
|
+
|
|
27
|
+
Protocol decisions live in `docs/V2-DESIGN.md` (frozen v0.2, decision log
|
|
28
|
+
D1–D13). Changing architecture, scope semantics, lifecycle, or the protocol
|
|
29
|
+
surface (frontmatter schema, MCP tools, CLI contract) requires updating the
|
|
30
|
+
design doc first. `AGENTS.md` has the working notes for AI agents; this file
|
|
31
|
+
has the contributor workflow.
|
package/README.md
CHANGED
|
@@ -30,8 +30,9 @@ Restart opencode.
|
|
|
30
30
|
| Tool | What it does |
|
|
31
31
|
|---|---|
|
|
32
32
|
| `memory_add` | Save a fact, preference, decision, note |
|
|
33
|
-
| `memory_search` | Keyword search (BM25) across project +
|
|
33
|
+
| `memory_search` | Keyword search (BM25) across project + personal memories |
|
|
34
34
|
| `memory_list` | List memories in a scope, newest first |
|
|
35
|
+
| `memory_supersede` | Replace a memory with a newer version (keeps a supersede chain) |
|
|
35
36
|
| `memory_forget` | Delete a memory by id |
|
|
36
37
|
|
|
37
38
|
## Capture
|
|
@@ -43,11 +44,13 @@ Restart opencode.
|
|
|
43
44
|
## Scopes
|
|
44
45
|
|
|
45
46
|
- **project** — scoped to the current repo (keyed off the git origin URL hash, or the cwd if no remote). Default for new memories.
|
|
46
|
-
- **
|
|
47
|
+
- **personal** — global across all your projects, this machine only, never synced. Use for personal preferences. (v1 called this `user`; `migrate --to-v2` renames it.)
|
|
48
|
+
|
|
49
|
+
See [docs/SCOPES.md](./docs/SCOPES.md) for the full scope model: key derivation, migration, visibility, reserved names.
|
|
47
50
|
|
|
48
51
|
## Retrieval
|
|
49
52
|
|
|
50
|
-
On the first turn of every session, `open-memex` injects a `[OPEN-MEMEX]` block into the system prompt containing top-N recent project memories + top-N
|
|
53
|
+
On the first turn of every session, `open-memex` injects a `[OPEN-MEMEX]` block into the system prompt containing top-N recent project memories + top-N personal preferences. The agent can also call `memory_search` on demand.
|
|
51
54
|
|
|
52
55
|
## Storage layout
|
|
53
56
|
|
|
@@ -56,13 +59,17 @@ On the first turn of every session, `open-memex` injects a `[OPEN-MEMEX]` block
|
|
|
56
59
|
$XDG_DATA_HOME/open-memex/ (Linux/macOS)
|
|
57
60
|
├── index.db # SQLite FTS5 index (rebuildable)
|
|
58
61
|
└── memories/
|
|
59
|
-
├──
|
|
62
|
+
├── personal/
|
|
60
63
|
│ └── <id>.md
|
|
61
64
|
└── project__<name>__<hash12>/
|
|
62
65
|
└── <id>.md
|
|
63
66
|
```
|
|
64
67
|
|
|
65
|
-
Each `.md` file has YAML frontmatter (`id, scope_key,
|
|
68
|
+
Each `.md` file has v2 YAML frontmatter (`id, scope, scope_key, visibility, role, type,
|
|
69
|
+
importance, status, tags, created_at, updated_at, schema_version`, ...) followed by the
|
|
70
|
+
memory content. You can edit them by hand — the plugin re-syncs on startup by comparing
|
|
71
|
+
file mtimes. Markdown is the source of truth; the SQLite index is derived and rebuildable
|
|
72
|
+
(`open-memex reindex`).
|
|
66
73
|
|
|
67
74
|
## Config
|
|
68
75
|
|
|
@@ -77,10 +84,13 @@ node --experimental-strip-types src/cli.ts where
|
|
|
77
84
|
node --experimental-strip-types src/cli.ts list --scope project
|
|
78
85
|
node --experimental-strip-types src/cli.ts search "auth flow"
|
|
79
86
|
node --experimental-strip-types src/cli.ts add "This repo uses better-sqlite3" --type project-config
|
|
87
|
+
node --experimental-strip-types src/cli.ts supersede <id> "Updated content"
|
|
88
|
+
node --experimental-strip-types src/cli.ts status <id> deprecated
|
|
80
89
|
node --experimental-strip-types src/cli.ts forget <id>
|
|
81
90
|
node --experimental-strip-types src/cli.ts reindex
|
|
82
91
|
node --experimental-strip-types src/cli.ts scopes
|
|
83
92
|
node --experimental-strip-types src/cli.ts migrate --from <old-scope-key> [--dry-run]
|
|
93
|
+
node --experimental-strip-types src/cli.ts migrate --to-v2 [--dry-run]
|
|
84
94
|
```
|
|
85
95
|
|
|
86
96
|
Or via the npm script: `npm run cli -- list --scope project`.
|
|
@@ -88,8 +98,8 @@ Or via the npm script: `npm run cli -- list --scope project`.
|
|
|
88
98
|
|
|
89
99
|
## Status
|
|
90
100
|
|
|
91
|
-
|
|
101
|
+
v2 alpha (`0.2.0-alpha`): v2 data model + migration, dedup + lifecycle (supersede/status), redaction hardening, CJK bigram retrieval. See `PLAN.md` for the roadmap (local embeddings, auto-capture, compaction hook, etc).
|
|
92
102
|
|
|
93
103
|
## License
|
|
94
104
|
|
|
95
|
-
[
|
|
105
|
+
[Apache-2.0](./LICENSE)
|
package/docs/SCOPES.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Scopes
|
|
2
|
+
|
|
3
|
+
**Scope = ownership** (who owns the memory). It answers "whose memory is
|
|
4
|
+
this and where does it live", not "who may read it" — that's `visibility`,
|
|
5
|
+
a separate v2 field (see below).
|
|
6
|
+
|
|
7
|
+
## The three scopes
|
|
8
|
+
|
|
9
|
+
| Scope | Owner | Lives where | Synced? |
|
|
10
|
+
|------------|------------------|--------------------------------------|----------------|
|
|
11
|
+
| `personal` | you | this machine only (`memories/personal/`) | **never** |
|
|
12
|
+
| `project` | repo collaborators | this machine, keyed by repo (`memories/project__<name>__<hash>/`) | via git, only if you opt in |
|
|
13
|
+
| `org` | org members | dedicated org memory repo (planned) | via git (planned) |
|
|
14
|
+
|
|
15
|
+
**`personal` never leaves the machine.** No sync, no upload, no exceptions.
|
|
16
|
+
Put anything here that should never be shared: credentials-adjacent notes,
|
|
17
|
+
private preferences, personal instructions.
|
|
18
|
+
|
|
19
|
+
**`project`** is the default for new memories. It is keyed off the repo, so
|
|
20
|
+
the same project resolves to the same scope on every machine.
|
|
21
|
+
|
|
22
|
+
**`org`** is reserved for a future dedicated org memory repo. The schema
|
|
23
|
+
accepts it; the CLI does not create org scopes yet.
|
|
24
|
+
|
|
25
|
+
## How the project key is derived
|
|
26
|
+
|
|
27
|
+
`resolveProjectScope(cwd)` (`src/scope.ts`):
|
|
28
|
+
|
|
29
|
+
1. Read `git config --get remote.origin.url` in the cwd.
|
|
30
|
+
2. If a remote exists: normalize it (strip `.git`, `git@host:` → `https://host/`,
|
|
31
|
+
lowercase) and take `sha256(normalized).slice(0, 12)` as the key suffix.
|
|
32
|
+
The project name comes from the last URL path segment.
|
|
33
|
+
3. If no remote: fall back to the absolute cwd path (lowercased), hashed the
|
|
34
|
+
same way. This is also how v1 "legacy" scopes are detected during
|
|
35
|
+
migration when a repo gains a remote later.
|
|
36
|
+
|
|
37
|
+
Key format: `project__<sanitized-name>__<12-hex-chars>`, e.g.
|
|
38
|
+
`project__open-memex__9e2a8a546c21`. The 12-char hash keeps collisions
|
|
39
|
+
astronomically unlikely while staying readable in `ls`.
|
|
40
|
+
|
|
41
|
+
## CLI
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
open-memex add "content" --scope personal # default is project
|
|
45
|
+
open-memex list --scope personal
|
|
46
|
+
open-memex search "query" --scope both # project + personal
|
|
47
|
+
open-memex scopes # list all known scope dirs
|
|
48
|
+
open-memex migrate --from <old-key> --to <new-key> [--dry-run]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`--scope user` is accepted as a deprecated alias of `--scope personal`.
|
|
52
|
+
|
|
53
|
+
When a repo gains a git remote after memories were already stored under the
|
|
54
|
+
cwd-based key, `migrate --from <cwd-key> --to <remote-key>` moves them
|
|
55
|
+
(`scopes` shows you the exact keys). Nothing is automatic — you run it
|
|
56
|
+
explicitly, previewing with `--dry-run` first.
|
|
57
|
+
|
|
58
|
+
## v1 → v2 rename
|
|
59
|
+
|
|
60
|
+
v1's `user` scope is renamed to `personal` in v2 (design §19 — "user" was
|
|
61
|
+
ambiguous next to "org members are users too"). `open-memex migrate --to-v2`
|
|
62
|
+
moves `memories/user/` → `memories/personal/` and rewrites the frontmatter
|
|
63
|
+
(`scope: user` → `scope: personal`). A dated backup of the pre-migration
|
|
64
|
+
tree is kept. Reads remain backward compatible: a v1 file with `scope: user`
|
|
65
|
+
is interpreted as `personal`.
|
|
66
|
+
|
|
67
|
+
## Visibility (planned, not yet enforced)
|
|
68
|
+
|
|
69
|
+
v2 frontmatter carries a separate `visibility` field (`private` | `internal` |
|
|
70
|
+
`shared`). The intended rule: `visibility: private` inside a shared scope is
|
|
71
|
+
**physically isolated** — written to a local-only cache directory, never
|
|
72
|
+
under `.open-memex/` — rather than relying on `.gitignore`. This is not
|
|
73
|
+
implemented yet; today, treat `personal` as the only confidentiality
|
|
74
|
+
boundary and review anything you place under `.open-memex/` before pushing.
|
|
75
|
+
|
|
76
|
+
## Reserved names
|
|
77
|
+
|
|
78
|
+
`team` and `public` are reserved scope names: the schema rejects writes to
|
|
79
|
+
them. Rationale: `project` already expresses team sharing; a distinct `team`
|
|
80
|
+
scope needs a clear semantic difference (e.g. cross-repo) before it earns
|
|
81
|
+
existence.
|