opencode-codex-memory 0.5.0 → 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
@@ -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.5.0"]
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
@@ -240,7 +254,7 @@ To set options, turn the plugin entry into a `[name, options]` pair:
240
254
  ```json
241
255
  {
242
256
  "plugin": [
243
- ["opencode-codex-memory@0.5.0", { "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 }]
244
258
  ]
245
259
  }
246
260
  ```
@@ -302,7 +316,7 @@ Off by default; no changes to Codex's own config are required.
302
316
  {
303
317
  "plugin": [
304
318
  [
305
- "opencode-codex-memory@0.5.0",
319
+ "opencode-codex-memory@0.6.0",
306
320
  { "codex_interop": { "import": true, "export": true } }
307
321
  ]
308
322
  ]
@@ -359,7 +373,7 @@ from the project memories Claude already keeps on your machine. **One-way only**
359
373
  ```json
360
374
  {
361
375
  "plugin": [
362
- ["opencode-codex-memory@0.5.0", { "claude_import": { "enabled": true } }]
376
+ ["opencode-codex-memory@0.6.0", { "claude_import": { "enabled": true } }]
363
377
  ]
364
378
  }
365
379
  ```
@@ -386,7 +400,7 @@ Claude names each project with an opaque id (a folder under
386
400
  {
387
401
  "plugin": [
388
402
  [
389
- "opencode-codex-memory@0.5.0",
403
+ "opencode-codex-memory@0.6.0",
390
404
  {
391
405
  "claude_import": {
392
406
  "enabled": true,
@@ -475,6 +489,7 @@ It reports:
475
489
  - phase-2 status / last error / cooldown
476
490
  - last session-discovery outcome
477
491
  - effective options (after clamping) and config warnings
492
+ - effective memory-agent health, including user overrides and required permissions
478
493
  - a short eligibility reminder (`min_rollout_idle_hours`, default **6h**)
479
494
 
480
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
+ }
@@ -1,5 +1,7 @@
1
1
  import { MemoryStore } from "./store.js";
2
2
  import type { PluginInput, PluginOptions } from "@opencode-ai/plugin";
3
+ /** Test seam: wait for all hook-launched work, including follow-up phase 2. */
4
+ export declare function waitForBackgroundTasks(): Promise<void>;
3
5
  export declare function takeNewCitations(partKey: string, ids: string[]): string[];
4
6
  export declare function markTurnSeen(sessionId: string): boolean;
5
7
  export declare function shouldHandleIdle(sessionId: string, now?: number): boolean;
@@ -58,11 +60,13 @@ declare const _default: {
58
60
  description: string;
59
61
  args: {
60
62
  path: import("zod").ZodDefault<import("zod").ZodString>;
63
+ cursor: import("zod").ZodOptional<import("zod").ZodString>;
61
64
  max_results: import("zod").ZodDefault<import("zod").ZodNumber>;
62
65
  };
63
66
  execute(args: {
64
67
  path: string;
65
68
  max_results: number;
69
+ cursor?: string | undefined;
66
70
  }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
67
71
  };
68
72
  memory_add_note: {
package/dist/src/index.js CHANGED
@@ -11,10 +11,26 @@ import { pluginOptions, recordConfigWarning, clearConfigWarnings, resetPluginOpt
11
11
  import { beginPluginShutdown, isPluginShuttingDown, resetPluginLifecycle } from "./lifecycle.js";
12
12
  import { hostMcpStatus } from "./host-client.js";
13
13
  import { recordDiagnostic } from "./diagnostics.js";
14
- import fs from "fs";
14
+ import { loadBundledAgentDefinitions, recordAgentConfig, resetAgentHealth } from "./agent-health.js";
15
15
  import path from "path";
16
16
  let phase1InFlight = false;
17
17
  let pluginClient = null;
18
+ const backgroundTasks = new Set();
19
+ function trackBackgroundTask(task) {
20
+ // Hooks must remain non-blocking, but test teardown needs a way to wait until
21
+ // work started by a hook has released its DB handle.
22
+ const tracked = task.catch((err) => {
23
+ console.error("[opencode-codex-memory] background task error:", err);
24
+ });
25
+ backgroundTasks.add(tracked);
26
+ void tracked.then(() => backgroundTasks.delete(tracked));
27
+ }
28
+ /** Test seam: wait for all hook-launched work, including follow-up phase 2. */
29
+ export async function waitForBackgroundTasks() {
30
+ while (backgroundTasks.size > 0) {
31
+ await Promise.all([...backgroundTasks]);
32
+ }
33
+ }
18
34
  // Single-flight guard for mcp.status(); see mcpToolPrefixes below.
19
35
  let mcpStatusInFlight = null;
20
36
  const MCP_STATUS_TIMEOUT_MS = 1_000;
@@ -83,7 +99,7 @@ export function handleSessionDeleted(sessionId, store = getStore(),
83
99
  // enqueued job runs when generation is re-enabled (codex: delete only
84
100
  // enqueues; the pipeline itself is gated elsewhere).
85
101
  schedulePhase2 = () => { if (pluginOptions.generate_memories)
86
- void triggerPhase2(); }) {
102
+ trackBackgroundTask(triggerPhase2()); }) {
87
103
  if (store.deleteSessionMemory(sessionId))
88
104
  schedulePhase2();
89
105
  }
@@ -94,6 +110,7 @@ export default {
94
110
  resetPluginLifecycle();
95
111
  setPluginInput(input);
96
112
  pluginClient = input.client;
113
+ resetAgentHealth();
97
114
  mcpStatusInFlight = null;
98
115
  // Unconditional, like the caches above: a boot WITHOUT options must not
99
116
  // inherit the previous boot's warnings (opencode can host several
@@ -320,8 +337,7 @@ async function classifyExternalContextTool(toolName) {
320
337
  export function injectAgentDefinitions(config) {
321
338
  let defs;
322
339
  try {
323
- const raw = fs.readFileSync(path.join(import.meta.dirname, "..", "opencode.json"), "utf8");
324
- defs = JSON.parse(raw).agent ?? {};
340
+ defs = loadBundledAgentDefinitions();
325
341
  }
326
342
  catch (err) {
327
343
  console.warn("[opencode-codex-memory] could not load bundled agent definitions:", err);
@@ -339,6 +355,7 @@ export function injectAgentDefinitions(config) {
339
355
  if (!config.agent[name])
340
356
  config.agent[name] = def;
341
357
  }
358
+ recordAgentConfig(config, true, defs);
342
359
  }
343
360
  function buildHooks() {
344
361
  const base = {
@@ -346,8 +363,15 @@ function buildHooks() {
346
363
  try {
347
364
  // The write pipeline is the only consumer of the sub-agents; with
348
365
  // generation off they would just pollute the user's agent list.
349
- if (!pluginOptions.generate_memories)
366
+ if (!pluginOptions.generate_memories) {
367
+ try {
368
+ recordAgentConfig(input, false, loadBundledAgentDefinitions());
369
+ }
370
+ catch (err) {
371
+ console.warn("[opencode-codex-memory] could not inspect bundled agent definitions:", err);
372
+ }
350
373
  return;
374
+ }
351
375
  injectAgentDefinitions(input);
352
376
  }
353
377
  catch (err) {
@@ -450,7 +474,7 @@ function buildHooks() {
450
474
  catch (e) {
451
475
  console.error("[opencode-codex-memory] stampMemoryModeIfAbsent failed:", e);
452
476
  }
453
- void triggerPhase1(sid);
477
+ trackBackgroundTask(triggerPhase1(sid));
454
478
  }
455
479
  catch (err) {
456
480
  console.error("[opencode-codex-memory] chat.message error:", err);
@@ -578,7 +602,7 @@ function buildHooks() {
578
602
  catch (e) {
579
603
  console.error("[opencode-codex-memory] stampMemoryModeIfAbsent failed:", e);
580
604
  }
581
- void triggerPhase1(sid);
605
+ trackBackgroundTask(triggerPhase1(sid));
582
606
  }
583
607
  // Control tools (reset/inspect/mode) are always available. The memory
584
608
  // read/search/list/add-note tools require BOTH use_memories and
@@ -622,7 +646,7 @@ async function triggerPhase1(currentSessionId) {
622
646
  finally {
623
647
  phase1InFlight = false;
624
648
  }
625
- void triggerPhase2();
649
+ trackBackgroundTask(triggerPhase2());
626
650
  }
627
651
  async function triggerPhase2() {
628
652
  if (isPluginShuttingDown())
@@ -12,6 +12,7 @@ import { codexInteropMtimes, resolveCodexInterop } from "../src/codex-interop.js
12
12
  import { claudeImportStatus, resolveClaudeHome } from "../src/claude-import.js";
13
13
  import { formatDiagnosticLine, getDiscoveryStatus, getRecentDiagnostics, } from "../src/diagnostics.js";
14
14
  import { isPluginShuttingDown } from "../src/lifecycle.js";
15
+ import { getAgentHealth } from "../src/agent-health.js";
15
16
  function isSymlinkedRoot() {
16
17
  try {
17
18
  assertMemoryRootSafe();
@@ -137,6 +138,18 @@ function listMemoriesDir() {
137
138
  walk(root, "");
138
139
  return out;
139
140
  }
141
+ function renderAgentHealth() {
142
+ const health = getAgentHealth();
143
+ const lines = [
144
+ `agent_config: ${health.observed ? "observed" : "not observed (config hook has not run)"}`,
145
+ `agent_generation_enabled: ${health.generationEnabled ?? "unknown"}`,
146
+ ];
147
+ for (const name of ["memorize", "memorize-extract"]) {
148
+ const entry = health.agents[name];
149
+ lines.push(` agent_${name}: source=${entry.source} status=${entry.healthy ? "healthy" : "degraded"}`, ...entry.issues.map((issue) => ` issue: ${issue}`));
150
+ }
151
+ return lines;
152
+ }
140
153
  export const memory_reset = tool({
141
154
  description: "Reset all persistent memory. Wipes the plugin's extracted memories and jobs tables and the entire " +
142
155
  "contents of the memories directory (including git history). Per-session memory modes are preserved, " +
@@ -264,6 +277,8 @@ export const memory_inspect = tool({
264
277
  "",
265
278
  ...renderEffectiveConfig(),
266
279
  "",
280
+ ...renderAgentHealth(),
281
+ "",
267
282
  ...diagnosticLines,
268
283
  "",
269
284
  "Files:",
@@ -298,6 +313,7 @@ export const memory_inspect = tool({
298
313
  },
299
314
  },
300
315
  config_warnings: [...getConfigWarnings()],
316
+ agent_health: getAgentHealth(),
301
317
  recent_events: diagnostics,
302
318
  },
303
319
  };
@@ -15,11 +15,13 @@ export declare const memory_list: {
15
15
  description: string;
16
16
  args: {
17
17
  path: import("zod").ZodDefault<import("zod").ZodString>;
18
+ cursor: import("zod").ZodOptional<import("zod").ZodString>;
18
19
  max_results: import("zod").ZodDefault<import("zod").ZodNumber>;
19
20
  };
20
21
  execute(args: {
21
22
  path: string;
22
23
  max_results: number;
24
+ cursor?: string | undefined;
23
25
  }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
24
26
  };
25
27
  export declare const memory_search: {
@@ -92,15 +92,23 @@ function visibleEntries(dir) {
92
92
  return out;
93
93
  }
94
94
  const LIST_MAX_RESULTS = 2000;
95
+ // Codex sorts paths lexically (`Path` ordering), not with locale collation.
96
+ // Keep ordering stable across hosts and match ASCII path ordering.
97
+ function comparePathNames(a, b) {
98
+ return a < b ? -1 : a > b ? 1 : 0;
99
+ }
95
100
  export const memory_list = tool({
96
101
  description: "List the immediate entries of a directory in the persistent memory workspace, sorted by name, " +
97
- "with entry types. Hidden files and symlinks are skipped. Use path '' (empty) for the memory root.",
102
+ "with entry types. Hidden files and symlinks are skipped. Supports cursor pagination and listing " +
103
+ "a single file. Use path '' (empty) for the memory root.",
98
104
  args: {
99
105
  path: tool.schema.string().default("").describe("Relative directory path inside the memory workspace ('' for the root)."),
106
+ cursor: tool.schema.string().optional().describe("Pagination cursor from a previous response's next_cursor."),
100
107
  max_results: tool.schema.number().int().min(1).max(LIST_MAX_RESULTS).default(LIST_MAX_RESULTS).describe("Maximum entries to return."),
101
108
  },
102
109
  async execute(args) {
103
110
  try {
111
+ const root = assertMemoryRootSafe();
104
112
  const fullPath = safeResolveMemoryPath(args.path || ".");
105
113
  if (!fs.existsSync(fullPath))
106
114
  return { output: `Not found: ${args.path}` };
@@ -111,19 +119,49 @@ export const memory_list = tool({
111
119
  if (st.isSymbolicLink()) {
112
120
  return { output: `memory_list error: symlinks are not allowed in the memory workspace: ${args.path}` };
113
121
  }
114
- if (!st.isDirectory())
115
- return { output: `memory_list error: not a directory: ${args.path}` };
116
- const entries = visibleEntries(fullPath).sort((a, b) => a.name.localeCompare(b.name));
117
- const truncated = entries.length > args.max_results;
118
- const shown = entries.slice(0, args.max_results);
119
- const prefix = args.path ? `${args.path.replace(/\/+$/, "")}/` : "";
120
- const listing = shown.map((e) => ({ path: `${prefix}${e.name}`, entry_type: e.isDir ? "directory" : "file" }));
121
- if (listing.length === 0)
122
- return { output: `Directory ${args.path || "."} is empty.` };
122
+ const entries = st.isFile()
123
+ ? [{ path: path.relative(root, fullPath).split(path.sep).join("/"), entry_type: "file" }]
124
+ : st.isDirectory()
125
+ ? visibleEntries(fullPath)
126
+ .sort((a, b) => comparePathNames(a.name, b.name))
127
+ .map((e) => ({
128
+ path: path.relative(root, path.join(fullPath, e.name)).split(path.sep).join("/"),
129
+ entry_type: e.isDir ? "directory" : "file",
130
+ }))
131
+ : [];
132
+ let startIndex = 0;
133
+ if (args.cursor !== undefined) {
134
+ if (!/^\d+$/.test(args.cursor)) {
135
+ return { output: `memory_list error: invalid cursor "${args.cursor}" (must be a non-negative integer).` };
136
+ }
137
+ startIndex = Number(args.cursor);
138
+ if (!Number.isSafeInteger(startIndex)) {
139
+ return { output: `memory_list error: invalid cursor "${args.cursor}" (must be a non-negative integer).` };
140
+ }
141
+ }
142
+ if (startIndex > entries.length) {
143
+ return { output: `memory_list error: cursor ${args.cursor} exceeds result count ${entries.length}.` };
144
+ }
145
+ const maxResults = args.max_results ?? LIST_MAX_RESULTS;
146
+ const endIndex = Math.min(startIndex + maxResults, entries.length);
147
+ const nextCursor = endIndex < entries.length ? String(endIndex) : null;
148
+ const truncated = nextCursor !== null;
149
+ const listing = entries.slice(startIndex, endIndex);
150
+ if (listing.length === 0) {
151
+ const output = st.isDirectory()
152
+ ? entries.length === 0
153
+ ? `Directory ${args.path || "."} is empty.`
154
+ : `No entries at cursor ${startIndex} for directory ${args.path || "."}.`
155
+ : "";
156
+ return {
157
+ output,
158
+ metadata: { path: args.path, entries: [], next_cursor: nextCursor, truncated },
159
+ };
160
+ }
123
161
  return {
124
162
  output: listing.map((e) => `${e.entry_type === "directory" ? "d" : "f"} ${e.path}`).join("\n") +
125
- (truncated ? `\n[truncated: ${entries.length - args.max_results} more entries]` : ""),
126
- metadata: { path: args.path, entries: listing, truncated },
163
+ (truncated ? `\n[truncated: ${entries.length - endIndex} more entries; pass cursor=${nextCursor}]` : ""),
164
+ metadata: { path: args.path, entries: listing, next_cursor: nextCursor, truncated },
127
165
  };
128
166
  }
129
167
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codex-memory",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Persistent memory plugin for opencode — ports codex's two-phase memory system (extraction → consolidation → injection → citation feedback)",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",