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 +20 -5
- package/dist/src/agent-health.d.ts +21 -0
- package/dist/src/agent-health.js +133 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +32 -8
- package/dist/tools/control.js +16 -0
- package/dist/tools/memory.d.ts +2 -0
- package/dist/tools/memory.js +50 -12
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
649
|
+
trackBackgroundTask(triggerPhase2());
|
|
626
650
|
}
|
|
627
651
|
async function triggerPhase2() {
|
|
628
652
|
if (isPluginShuttingDown())
|
package/dist/tools/control.js
CHANGED
|
@@ -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
|
};
|
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",
|