opencode-codex-memory 0.4.11 → 0.6.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 CHANGED
@@ -27,10 +27,10 @@ from there everything lives under `~/.local/share/opencode/`, so you can read it
27
27
  grep it, edit it, or delete it like anything else you own. Nothing leaves your
28
28
  machine beyond the model calls OpenCode already makes.
29
29
 
30
- If you *do* also use the Codex CLI: the plugin can share memory with Codex in
31
- both directions what one assistant learns on your machine, the other picks
32
- up. Off by default, one config flag per direction; see
33
- [Sharing memory with the Codex CLI](#sharing-memory-with-the-codex-cli).
30
+ If you *do* also use the Codex CLI or Claude Code on the same machine: the
31
+ plugin can bring their memories in (and, for Codex, push ours back). Off by
32
+ default see [Sharing memory with the Codex CLI](#sharing-memory-with-the-codex-cli)
33
+ and [Importing memory from Claude Code](#importing-memory-from-claude-code).
34
34
 
35
35
  ## Why
36
36
 
@@ -52,7 +52,7 @@ If you want the mental model before the details, jump to
52
52
 
53
53
  ```json
54
54
  {
55
- "plugin": ["opencode-codex-memory@0.4.11"]
55
+ "plugin": ["opencode-codex-memory@0.6.0"]
56
56
  }
57
57
  ```
58
58
 
@@ -191,6 +191,20 @@ them — it's yours. (The `memories/` folder also holds a few working files and
191
191
  an internal `.git/` the plugin uses for change tracking; `memory_reset` wipes
192
192
  those too.)
193
193
 
194
+ ### Backup and restore
195
+
196
+ Back up the whole OpenCode data directory while OpenCode is stopped. The
197
+ SQLite database and `memories/` workspace are a pair: restoring only one can
198
+ leave job state, Git baseline, and memory files out of sync. Include hidden
199
+ files, especially `memories/.git/`, and SQLite sidecars such as `memory.db-wal`
200
+ or `memory.db-shm` when present.
201
+
202
+ The directory is `$XDG_DATA_HOME/opencode` when `XDG_DATA_HOME` is set,
203
+ otherwise `~/.local/share/opencode`. Copy that whole directory to a dated
204
+ backup location. To restore, stop OpenCode, replace the current `opencode/`
205
+ data directory with the backup copy, then start OpenCode again. Do not restore
206
+ while OpenCode is running or copy only `memory.db` or only `memories/`.
207
+
194
208
  ## Privacy & safety
195
209
 
196
210
  - **Local only.** There is no remote storage option to enable, by accident or
@@ -233,13 +247,14 @@ Codex's `[memories]` config so the two stay easy to compare:
233
247
  | `max_rollouts_per_startup` | `2` | Max sessions extracted per pass |
234
248
  | `max_unused_days` | `30` | Prune memories unused for this long |
235
249
  | `codex_interop` | `{ "import": false, "export": false }` | Two-way memory exchange with a local Codex CLI (see below) |
250
+ | `claude_import` | `{ "enabled": false }` | One-way import of Claude Code project memories (see below) |
236
251
 
237
252
  To set options, turn the plugin entry into a `[name, options]` pair:
238
253
 
239
254
  ```json
240
255
  {
241
256
  "plugin": [
242
- ["opencode-codex-memory@0.4.11", { "disable_on_external_context": true, "min_rollout_idle_hours": 2 }]
257
+ ["opencode-codex-memory@0.6.0", { "disable_on_external_context": true, "min_rollout_idle_hours": 2 }]
243
258
  ]
244
259
  }
245
260
  ```
@@ -291,36 +306,150 @@ explicitly, so they win over an agent-level `model`.
291
306
 
292
307
  ### Sharing memory with the Codex CLI
293
308
 
294
- If you switch between OpenCode and OpenAI's Codex CLI on the same machine, the
295
- plugin can exchange consolidated memories with Codex in either or both
296
- directions:
309
+ If you use OpenCode and the Codex CLI on the same machine, turn this on so each
310
+ side can pick up what the other already learned. **Either or both directions.**
311
+ Off by default; no changes to Codex's own config are required.
312
+
313
+ **Enable both directions:**
314
+
315
+ ```json
316
+ {
317
+ "plugin": [
318
+ [
319
+ "opencode-codex-memory@0.6.0",
320
+ { "codex_interop": { "import": true, "export": true } }
321
+ ]
322
+ ]
323
+ }
324
+ ```
325
+
326
+ - **Import:** on each consolidation pass here, durable memory from Codex is
327
+ merged into this plugin's store (tagged so you can tell it came from Codex).
328
+ - **Export:** after a successful consolidation here, this plugin's memory is
329
+ offered to Codex; Codex merges it on *its* next consolidation pass.
330
+
331
+ **Options** (all under `codex_interop`):
332
+
333
+ | Option | Default | Meaning |
334
+ | --- | --- | --- |
335
+ | `import` | `false` | Bring Codex's consolidated memory into OpenCode. |
336
+ | `export` | `false` | Offer this plugin's consolidated memory to Codex. |
337
+ | `codex_home` | `$CODEX_HOME`, else `~/.codex` | Where the Codex CLI keeps its data. Set this if you use a non-default location. |
338
+
339
+ **Notes**
340
+
341
+ - Export only runs once Codex has created its own memory workspace
342
+ (`$CODEX_HOME/memories`). Until then, import/export quietly do nothing for
343
+ the missing side.
344
+ - Codex's own files are never rewritten in place — export only adds a side
345
+ channel Codex already knows how to read.
346
+ - Both sides tag origin (`[from codex]` / `[from opencode]`) and skip the other
347
+ tag on re-import, so the same facts don't bounce back and forth.
348
+ - Safe to combine with `claude_import`. Check status with `memory_inspect`
349
+ (`codex_interop:` section).
350
+
351
+ **Turning it off**
352
+
353
+ Setting `import` / `export` back to `false` (or removing `codex_interop`) only
354
+ stops further sync. Already merged memory stays in this plugin's store, and any
355
+ staging copies stay on disk (harmless while import is off). The last export left
356
+ in Codex stays until Codex consolidates it away or you remove it there.
357
+
358
+ To drop the local staging copies too: turn import off first, then delete
359
+ `extensions/codex_import/` under the plugin memory directory (see
360
+ [Where your data lives](#where-your-data-lives)). Leave import off — if you turn
361
+ it back on, the next pass will recreate them from Codex. After a deletion, the
362
+ next consolidation can treat the missing files as a signal to drop entries that
363
+ only came from that import.
364
+
365
+ ### Importing memory from Claude Code
366
+
367
+ If you use Claude Code as well as OpenCode, turn this on so OpenCode can learn
368
+ from the project memories Claude already keeps on your machine. **One-way only**
369
+ (Claude → OpenCode); nothing is written back into Claude. Off by default.
370
+
371
+ **Enable everything Claude has:**
297
372
 
298
373
  ```json
299
374
  {
300
375
  "plugin": [
301
- ["opencode-codex-memory@0.4.11", { "codex_interop": { "import": true, "export": true } }]
376
+ ["opencode-codex-memory@0.6.0", { "claude_import": { "enabled": true } }]
302
377
  ]
303
378
  }
304
379
  ```
305
380
 
306
- - `import` copies Codex's consolidated `MEMORY.md` / `memory_summary.md` into a
307
- memory extension (`extensions/codex_import/`) before each consolidation pass.
308
- The consolidator merges what's new, tagging it `[from codex]`.
309
- - `export` copies this plugin's consolidated memory into Codex's memory
310
- workspace as an extension (`extensions/opencode_import/`) after each
311
- successful consolidation, together with instructions for Codex's own
312
- consolidator. Codex picks it up on its next consolidation — no Codex
313
- configuration needed. Nothing is exported until Codex's memory feature has
314
- created `$CODEX_HOME/memories`, and Codex's own files are never modified.
315
- (After a `memory_reset` here, the last export stays in Codex until your
316
- next successful consolidation replaces it.)
317
- - `codex_home` overrides where Codex lives (default: `$CODEX_HOME`, else
318
- `~/.codex`).
319
-
320
- Both sides mark imported content with a provenance tag (`[from codex]` /
321
- `[from opencode]`) and skip content carrying the other side's tag, so memories
322
- don't ping-pong between the two systems. This follows the same extension
323
- mechanism Codex itself uses to import Claude memories.
381
+ After the next consolidation pass, durable facts from Claude's memories show up
382
+ in this plugin's memory the same way OpenCode's own extractions do. Project-specific
383
+ detail stays labeled by project; broadly useful preferences can land in the
384
+ global summary.
385
+
386
+ **Options** (all under `claude_import`):
387
+
388
+ | Option | Default | Meaning |
389
+ | --- | --- | --- |
390
+ | `enabled` | `false` | Turn the importer on. |
391
+ | `claude_home` | `~/.claude` | Where Claude Code stores its data. Set this if you use a non-default location. |
392
+ | `projects` | all projects | Optional allowlist of Claude project ids to import (see below). |
393
+
394
+ **Limit to specific projects:**
395
+
396
+ Claude names each project with an opaque id (a folder under
397
+ `~/.claude/projects/`), not the path you work in. List those folder names:
398
+
399
+ ```json
400
+ {
401
+ "plugin": [
402
+ [
403
+ "opencode-codex-memory@0.6.0",
404
+ {
405
+ "claude_import": {
406
+ "enabled": true,
407
+ "projects": ["-Users-you-Desktop-git-my-app"]
408
+ }
409
+ }
410
+ ]
411
+ ]
412
+ }
413
+ ```
414
+
415
+ Find the ids on your machine:
416
+
417
+ ```bash
418
+ ls ~/.claude/projects
419
+ ```
420
+
421
+ - Omit `projects` (or leave it empty) to import every Claude project the plugin
422
+ can place on disk.
423
+ - Add or remove ids anytime while import is **on**. Dropping an id (or deleting
424
+ that project's memory in Claude) stops syncing it and removes the staging
425
+ copy on the next pass so consolidation can drop derived entries. Memory
426
+ already merged into the main store is not instantly deleted; the consolidator
427
+ cleans it up when it next runs.
428
+ - Turning `enabled` off (see below) does **not** prune anything.
429
+
430
+ **Turning it off**
431
+
432
+ Setting `enabled` to `false` (or removing `claude_import`) only stops further
433
+ import. Already merged memory stays in this plugin's store, and staging copies
434
+ stay on disk (harmless while import is off).
435
+
436
+ To drop the local staging copies too: turn import off first, then delete
437
+ `extensions/external_agent_import/` under the plugin memory directory (see
438
+ [Where your data lives](#where-your-data-lives)). Leave import off — if you turn
439
+ it back on, the next pass will recreate them from Claude. After a deletion, the
440
+ next consolidation can treat the missing files as a signal to drop entries that
441
+ only came from that import.
442
+
443
+ To stop importing *some* projects without turning the feature off: shrink
444
+ `projects` (or delete that memory in Claude) and let the next pass run. That
445
+ only affects the Claude import channel.
446
+
447
+ **Notes**
448
+
449
+ - Safe to combine with `codex_interop` (Codex sharing uses a separate channel).
450
+ - If you also run another OpenCode plugin that *writes* Claude-style memory,
451
+ pick one writer — two systems updating the same long-term store will fight.
452
+ - Check status anytime with `memory_inspect` (`claude_import:` section).
324
453
 
325
454
  ## Why one global memory?
326
455
 
@@ -360,6 +489,7 @@ It reports:
360
489
  - phase-2 status / last error / cooldown
361
490
  - last session-discovery outcome
362
491
  - effective options (after clamping) and config warnings
492
+ - effective memory-agent health, including user overrides and required permissions
363
493
  - a short eligibility reminder (`min_rollout_idle_hours`, default **6h**)
364
494
 
365
495
  Common causes:
@@ -0,0 +1,21 @@
1
+ declare const AGENT_NAMES: readonly ["memorize", "memorize-extract"];
2
+ type AgentName = (typeof AGENT_NAMES)[number];
3
+ export interface AgentHealthEntry {
4
+ source: "shipped" | "user_override" | "missing";
5
+ healthy: boolean;
6
+ issues: string[];
7
+ }
8
+ export interface AgentHealthSnapshot {
9
+ observed: boolean;
10
+ generationEnabled: boolean | null;
11
+ agents: Record<AgentName, AgentHealthEntry>;
12
+ }
13
+ export declare function loadBundledAgentDefinitions(): Record<string, unknown>;
14
+ /** Record the effective agent config after the plugin config hook runs. */
15
+ export declare function recordAgentConfig(config: {
16
+ agent?: Record<string, unknown>;
17
+ }, generationEnabled: boolean, shipped: Record<string, unknown>): void;
18
+ export declare function getAgentHealth(): AgentHealthSnapshot;
19
+ /** Test seam and boot boundary. */
20
+ export declare function resetAgentHealth(): void;
21
+ export {};
@@ -0,0 +1,133 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { memoryRoot } from "./paths.js";
4
+ const AGENT_NAMES = ["memorize", "memorize-extract"];
5
+ const REQUIRED_ALLOWS = {
6
+ memorize: ["read", "edit", "write", "glob", "grep"],
7
+ "memorize-extract": ["StructuredOutput"],
8
+ };
9
+ const SAFE_ALLOWS = {
10
+ memorize: new Set(["read", "edit", "write", "glob", "grep", "external_directory"]),
11
+ "memorize-extract": new Set(["StructuredOutput"]),
12
+ };
13
+ const initialEntry = () => ({ source: "missing", healthy: false, issues: ["config hook has not run"] });
14
+ let snapshot = {
15
+ observed: false,
16
+ generationEnabled: null,
17
+ agents: {
18
+ memorize: initialEntry(),
19
+ "memorize-extract": initialEntry(),
20
+ },
21
+ };
22
+ export function loadBundledAgentDefinitions() {
23
+ const raw = fs.readFileSync(path.join(import.meta.dirname, "..", "opencode.json"), "utf8");
24
+ return JSON.parse(raw).agent ?? {};
25
+ }
26
+ function asRecord(value) {
27
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
28
+ }
29
+ function hasNonDenyAction(value) {
30
+ const rules = asRecord(value);
31
+ return rules ? Object.values(rules).some((action) => action !== "deny") : value !== "deny";
32
+ }
33
+ /** Structural compare — config reload re-parses shipped defs into new objects. */
34
+ function definitionsEqual(a, b) {
35
+ try {
36
+ return JSON.stringify(a) === JSON.stringify(b);
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ function permissionIssues(name, definition) {
43
+ const issues = [];
44
+ const record = asRecord(definition);
45
+ if (record?.mode !== "subagent")
46
+ issues.push("agent mode must be 'subagent'");
47
+ const permission = asRecord(record?.permission);
48
+ if (!permission) {
49
+ issues.push("missing permission map");
50
+ return issues;
51
+ }
52
+ const keys = Object.keys(permission);
53
+ if (keys[0] !== "*")
54
+ issues.push("permission wildcard '*' must be the first rule");
55
+ if (permission["*"] !== "deny")
56
+ issues.push("permission wildcard '*' must be 'deny'");
57
+ for (const toolName of REQUIRED_ALLOWS[name]) {
58
+ if (permission[toolName] !== "allow") {
59
+ issues.push(`required permission '${toolName}: allow' is missing`);
60
+ }
61
+ }
62
+ for (const [toolName, value] of Object.entries(permission)) {
63
+ if (toolName === "*")
64
+ continue;
65
+ if (!SAFE_ALLOWS[name].has(toolName)) {
66
+ if (hasNonDenyAction(value))
67
+ issues.push(`unexpected permission '${toolName}' must be denied`);
68
+ }
69
+ }
70
+ if (name === "memorize") {
71
+ const external = asRecord(permission.external_directory);
72
+ const expectedPath = path.join(memoryRoot(), "*");
73
+ if (external?.[expectedPath] !== "allow") {
74
+ issues.push(`consolidator must allow external_directory '${expectedPath}'`);
75
+ }
76
+ if (external) {
77
+ for (const [grantedPath, action] of Object.entries(external)) {
78
+ if (grantedPath === expectedPath)
79
+ continue;
80
+ if (hasNonDenyAction(action)) {
81
+ issues.push(`consolidator must deny extra external_directory '${grantedPath}'`);
82
+ }
83
+ }
84
+ }
85
+ }
86
+ else if (permission.external_directory !== undefined) {
87
+ issues.push("extractor must not have external_directory access");
88
+ }
89
+ return issues;
90
+ }
91
+ function inspectAgent(name, definition, source) {
92
+ const issues = permissionIssues(name, definition);
93
+ return { source, healthy: issues.length === 0, issues };
94
+ }
95
+ /** Record the effective agent config after the plugin config hook runs. */
96
+ export function recordAgentConfig(config, generationEnabled, shipped) {
97
+ const configured = asRecord(config.agent);
98
+ const agents = {};
99
+ for (const name of AGENT_NAMES) {
100
+ const definition = configured?.[name];
101
+ const source = definition === undefined
102
+ ? "missing"
103
+ : definitionsEqual(shipped[name], definition)
104
+ ? "shipped"
105
+ : "user_override";
106
+ agents[name] = inspectAgent(name, definition, source);
107
+ if (!generationEnabled && definition === undefined) {
108
+ agents[name] = { source: "missing", healthy: true, issues: ["generation disabled; agent not injected"] };
109
+ }
110
+ }
111
+ snapshot = { observed: true, generationEnabled, agents };
112
+ }
113
+ export function getAgentHealth() {
114
+ return {
115
+ observed: snapshot.observed,
116
+ generationEnabled: snapshot.generationEnabled,
117
+ agents: {
118
+ memorize: { ...snapshot.agents.memorize, issues: [...snapshot.agents.memorize.issues] },
119
+ "memorize-extract": { ...snapshot.agents["memorize-extract"], issues: [...snapshot.agents["memorize-extract"].issues] },
120
+ },
121
+ };
122
+ }
123
+ /** Test seam and boot boundary. */
124
+ export function resetAgentHealth() {
125
+ snapshot = {
126
+ observed: false,
127
+ generationEnabled: null,
128
+ agents: {
129
+ memorize: initialEntry(),
130
+ "memorize-extract": initialEntry(),
131
+ },
132
+ };
133
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Claude Code memory import — port of codex's external-agent memory sync
3
+ * (codex-rs/external-agent-migration/src/memory.rs + memory_import.rs).
4
+ *
5
+ * One-way: reads Claude project memory markdown under
6
+ * `~/.claude/projects/<key>/memory/`, copies into
7
+ * `extensions/external_agent_import/resources/<key>/` with `scope.json`, and
8
+ * seeds `instructions.md` so phase-2 consolidation merges them. Never writes
9
+ * back to Claude. Never touches Claude session transcripts except to resolve
10
+ * a project cwd (newest *.jsonl with an absolute, existing cwd).
11
+ *
12
+ * Codex selects projects via a migration UI; this plugin has no such surface.
13
+ * When enabled, continuous phase-2 sync imports every project that has a
14
+ * reliable cwd (optional `projects` allowlist). Default-off.
15
+ */
16
+ export declare const EXTENSION_NAME = "external_agent_import";
17
+ export interface ClaudeImportOptions {
18
+ /** When true, sync Claude project memories into the extension each phase 2. */
19
+ enabled: boolean;
20
+ /** Override Claude home (default: `~/.claude`). */
21
+ claude_home?: string;
22
+ /**
23
+ * Optional project-key allowlist (Claude's `projects/<key>` directory names).
24
+ * Omitted/empty = every project with a reliable cwd.
25
+ */
26
+ projects?: string[];
27
+ }
28
+ export interface ExternalMemoryFile {
29
+ projectKey: string;
30
+ projectCwd: string | null;
31
+ sourcePath: string;
32
+ relativePath: string;
33
+ }
34
+ export interface ClaudeImportSyncResult {
35
+ changed: boolean;
36
+ synchronizedProjects: string[];
37
+ skippedNoCwd: string[];
38
+ failures: Array<{
39
+ projectKey: string;
40
+ message: string;
41
+ }>;
42
+ }
43
+ export declare function resolveClaudeHome(opts: ClaudeImportOptions): string;
44
+ /**
45
+ * Resolve project cwd from Claude session jsonl under the project root.
46
+ * Mirrors codex project_cwd_from_sessions: newest *.jsonl first, first absolute
47
+ * cwd that canonicalizes to an existing directory wins.
48
+ */
49
+ export declare function projectCwdFromSessions(projectRoot: string): string | null;
50
+ /** Discover every Markdown file under each Claude project memory directory. */
51
+ export declare function discoverExternalMemoryFiles(claudeHome: string): ExternalMemoryFile[];
52
+ /** Owned = resource dirs that carry a regular scope.json (codex owned_project_keys). */
53
+ export declare function ownedProjectKeys(): string[];
54
+ /**
55
+ * Sync Claude project memories into `extensions/external_agent_import/`.
56
+ * Call inside a claimed phase-2 job after baseline, before diff capture.
57
+ * Unreachable Claude home → no-op (never a deletion signal).
58
+ */
59
+ export declare function syncClaudeImport(opts: ClaudeImportOptions): ClaudeImportSyncResult;
60
+ /** Inspect helpers: imported project keys + instruction mtime. */
61
+ export declare function claudeImportStatus(): {
62
+ extensionPresent: boolean;
63
+ projects: string[];
64
+ instructionsMtimeMs: number | null;
65
+ };