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 +158 -28
- package/dist/src/agent-health.d.ts +21 -0
- package/dist/src/agent-health.js +133 -0
- package/dist/src/claude-import.d.ts +65 -0
- package/dist/src/claude-import.js +499 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +69 -8
- package/dist/src/options.d.ts +5 -2
- package/dist/src/options.js +3 -0
- package/dist/src/phase2.d.ts +2 -0
- package/dist/src/phase2.js +14 -1
- package/dist/tools/control.js +42 -1
- package/dist/tools/memory.d.ts +2 -0
- package/dist/tools/memory.js +50 -12
- package/package.json +1 -1
package/dist/src/options.d.ts
CHANGED
|
@@ -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
|
|
10
|
-
*
|
|
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;
|
package/dist/src/options.js
CHANGED
|
@@ -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
|
/**
|
package/dist/src/phase2.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/src/phase2.js
CHANGED
|
@@ -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
|
-
//
|
|
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" };
|
package/dist/tools/control.js
CHANGED
|
@@ -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: {
|
|
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
|
};
|
package/dist/tools/memory.d.ts
CHANGED
|
@@ -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: {
|
package/dist/tools/memory.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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 -
|
|
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.
|
|
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",
|