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.
@@ -1,4 +1,5 @@
1
1
  import type { CodexInteropOptions } from "./codex-interop.js";
2
+ import type { ClaudeImportOptions } from "./claude-import.js";
2
3
  /**
3
4
  * Effective plugin options + configuration diagnostics, owned by a leaf
4
5
  * module so both the plugin entry (writes) and the control tools (read for
@@ -6,8 +7,9 @@ import type { CodexInteropOptions } from "./codex-interop.js";
6
7
  *
7
8
  * Option names and defaults mirror codex's MemoriesToml/MemoriesConfig
8
9
  * (codex-rs/config/src/types.rs). Keep them 1:1 so the drift script and
9
- * manual syncing stay trivial; do not rename for taste. codex_interop is the
10
- * one opencode-specific addition (no codex equivalent).
10
+ * manual syncing stay trivial; do not rename for taste. codex_interop and
11
+ * claude_import are opencode-facing knobs for external-agent exchange
12
+ * (codex's Claude importer is migration-UI gated / default-off).
11
13
  */
12
14
  export interface PluginOptionsState {
13
15
  generate_memories: boolean;
@@ -22,6 +24,7 @@ export interface PluginOptionsState {
22
24
  max_rollouts_per_startup: number;
23
25
  min_rollout_idle_hours: number;
24
26
  codex_interop: CodexInteropOptions;
27
+ claude_import: ClaudeImportOptions;
25
28
  }
26
29
  export declare const pluginOptions: PluginOptionsState;
27
30
  export declare function resetPluginOptions(): void;
@@ -9,16 +9,19 @@ const DEFAULT_PLUGIN_OPTIONS = {
9
9
  max_rollouts_per_startup: 2,
10
10
  min_rollout_idle_hours: 6,
11
11
  codex_interop: { import: false, export: false },
12
+ claude_import: { enabled: false },
12
13
  };
13
14
  export const pluginOptions = {
14
15
  ...DEFAULT_PLUGIN_OPTIONS,
15
16
  codex_interop: { ...DEFAULT_PLUGIN_OPTIONS.codex_interop },
17
+ claude_import: { ...DEFAULT_PLUGIN_OPTIONS.claude_import },
16
18
  };
17
19
  export function resetPluginOptions() {
18
20
  delete pluginOptions.extract_model;
19
21
  delete pluginOptions.consolidation_model;
20
22
  Object.assign(pluginOptions, DEFAULT_PLUGIN_OPTIONS, {
21
23
  codex_interop: { ...DEFAULT_PLUGIN_OPTIONS.codex_interop },
24
+ claude_import: { ...DEFAULT_PLUGIN_OPTIONS.claude_import },
22
25
  });
23
26
  }
24
27
  /**
@@ -1,11 +1,13 @@
1
1
  import { MemoryStore } from "./store.js";
2
2
  import { type CodexInteropOptions } from "./codex-interop.js";
3
+ import { type ClaudeImportOptions } from "./claude-import.js";
3
4
  export interface Phase2Options {
4
5
  maxRaw: number;
5
6
  maxUnusedDays: number;
6
7
  extensionRetentionDays: number;
7
8
  consolidationModel?: string;
8
9
  codexInterop?: CodexInteropOptions;
10
+ claudeImport?: ClaudeImportOptions;
9
11
  /** Override the 90s heartbeat interval (tests / advanced). */
10
12
  heartbeatIntervalMs?: number;
11
13
  }
@@ -5,6 +5,7 @@ import { invalidateCache } from "./source.js";
5
5
  import { memoryRoot } from "./paths.js";
6
6
  import { abortPhase2Consolidation, beginPhase2AbortScope, endPhase2AbortScope, isPluginShuttingDown, } from "./lifecycle.js";
7
7
  import { resolveCodexInterop, syncCodexImport, exportToCodexMemory } from "./codex-interop.js";
8
+ import { syncClaudeImport } from "./claude-import.js";
8
9
  export const DEFAULT_PHASE2_OPTIONS = {
9
10
  maxRaw: 256,
10
11
  maxUnusedDays: 30,
@@ -76,7 +77,8 @@ export async function runPhase2(store, opts = DEFAULT_PHASE2_OPTIONS) {
76
77
  rebuildRawMemories(outputs);
77
78
  writeRolloutSummaries(outputs);
78
79
  pruneExtensionResources(opts.extensionRetentionDays);
79
- // Codex-interop import: inside the claimed job (workspace mutations are
80
+ // External-agent imports (Codex consolidated memory + Claude project
81
+ // memories): inside the claimed job (workspace mutations are
80
82
  // lease-protected — pre-claim writes could race a running consolidator),
81
83
  // after the baseline (copies must show up as diff, not be swallowed by a
82
84
  // first-run baseline init; codex memory_import.rs orders prepare-then-
@@ -92,6 +94,17 @@ export async function runPhase2(store, opts = DEFAULT_PHASE2_OPTIONS) {
92
94
  console.warn("[opencode-codex-memory] codex import sync failed:", err);
93
95
  }
94
96
  }
97
+ if (opts.claudeImport?.enabled) {
98
+ try {
99
+ const result = syncClaudeImport(opts.claudeImport);
100
+ for (const f of result.failures) {
101
+ console.warn(`[opencode-codex-memory] claude import: ${f.message}`);
102
+ }
103
+ }
104
+ catch (err) {
105
+ console.warn("[opencode-codex-memory] claude import sync failed:", err);
106
+ }
107
+ }
95
108
  const diff = await captureWorkspaceDiff();
96
109
  if (releaseIfShuttingDown(store, claim.ownershipToken)) {
97
110
  return { status: "shutting_down" };
@@ -9,8 +9,10 @@ import { assertMemoryRootSafe, readRegularFileNoFollow } from "../src/path-guard
9
9
  import { isPhase2InFlight } from "../src/phase2.js";
10
10
  import { pluginOptions, getConfigWarnings } from "../src/options.js";
11
11
  import { codexInteropMtimes, resolveCodexInterop } from "../src/codex-interop.js";
12
+ import { claudeImportStatus, resolveClaudeHome } from "../src/claude-import.js";
12
13
  import { formatDiagnosticLine, getDiscoveryStatus, getRecentDiagnostics, } from "../src/diagnostics.js";
13
14
  import { isPluginShuttingDown } from "../src/lifecycle.js";
15
+ import { getAgentHealth } from "../src/agent-health.js";
14
16
  function isSymlinkedRoot() {
15
17
  try {
16
18
  assertMemoryRootSafe();
@@ -82,6 +84,21 @@ function renderEffectiveConfig() {
82
84
  }
83
85
  }
84
86
  }
87
+ const cl = o.claude_import;
88
+ if (!cl.enabled) {
89
+ lines.push(" claude_import: off");
90
+ }
91
+ else {
92
+ const home = resolveClaudeHome(cl);
93
+ const reachable = fs.existsSync(home);
94
+ const allow = cl.projects && cl.projects.length > 0 ? ` projects=[${cl.projects.join(", ")}]` : " projects=all";
95
+ lines.push(` claude_import: enabled${allow}`, ` claude home: ${home}${reachable ? "" : " (not found — nothing imported until Claude Code creates it)"}`);
96
+ const st = claudeImportStatus();
97
+ if (st.extensionPresent) {
98
+ const fmt = (ms) => (ms == null ? "none" : new Date(ms).toISOString());
99
+ lines.push(` imported projects (${st.projects.length}): ${st.projects.length > 0 ? st.projects.join(", ") : "(none)"}`, ` instructions mtime: ${fmt(st.instructionsMtimeMs)}`);
100
+ }
101
+ }
85
102
  const warnings = getConfigWarnings();
86
103
  lines.push(warnings.length > 0 ? `config_warnings (${warnings.length}):` : "config_warnings: none", ...warnings.map((w) => ` - ${w}`));
87
104
  return lines;
@@ -121,6 +138,18 @@ function listMemoriesDir() {
121
138
  walk(root, "");
122
139
  return out;
123
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
+ }
124
153
  export const memory_reset = tool({
125
154
  description: "Reset all persistent memory. Wipes the plugin's extracted memories and jobs tables and the entire " +
126
155
  "contents of the memories directory (including git history). Per-session memory modes are preserved, " +
@@ -248,6 +277,8 @@ export const memory_inspect = tool({
248
277
  "",
249
278
  ...renderEffectiveConfig(),
250
279
  "",
280
+ ...renderAgentHealth(),
281
+ "",
251
282
  ...diagnosticLines,
252
283
  "",
253
284
  "Files:",
@@ -271,8 +302,18 @@ export const memory_inspect = tool({
271
302
  summary_chars: summaryChars,
272
303
  summary_tokens_est: summaryTokens,
273
304
  files: listing,
274
- effective_options: { ...pluginOptions, codex_interop: { ...pluginOptions.codex_interop } },
305
+ effective_options: {
306
+ ...pluginOptions,
307
+ codex_interop: { ...pluginOptions.codex_interop },
308
+ claude_import: {
309
+ ...pluginOptions.claude_import,
310
+ ...(pluginOptions.claude_import.projects
311
+ ? { projects: [...pluginOptions.claude_import.projects] }
312
+ : {}),
313
+ },
314
+ },
275
315
  config_warnings: [...getConfigWarnings()],
316
+ agent_health: getAgentHealth(),
276
317
  recent_events: diagnostics,
277
318
  },
278
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.4.11",
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",