opencode-codex-memory 0.1.9 → 0.2.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 +17 -7
- package/dist/opencode.json +4 -4
- package/dist/src/capture.d.ts +29 -4
- package/dist/src/capture.js +106 -57
- package/dist/src/index.d.ts +51 -4
- package/dist/src/index.js +142 -16
- package/dist/src/llm.d.ts +1 -0
- package/dist/src/llm.js +1 -1
- package/dist/src/path-guard.d.ts +9 -0
- package/dist/src/path-guard.js +23 -1
- package/dist/src/paths.d.ts +0 -1
- package/dist/src/paths.js +0 -3
- package/dist/src/phase1.d.ts +1 -0
- package/dist/src/phase1.js +9 -4
- package/dist/src/phase2.d.ts +2 -0
- package/dist/src/phase2.js +4 -0
- package/dist/src/redact.js +5 -2
- package/dist/src/store.d.ts +5 -0
- package/dist/src/store.js +29 -13
- package/dist/src/workspace.js +8 -3
- package/dist/tools/control.js +40 -14
- package/dist/tools/memory.d.ts +20 -4
- package/dist/tools/memory.js +182 -60
- package/opencode.json +4 -4
- package/package.json +1 -1
package/dist/src/path-guard.js
CHANGED
|
@@ -10,8 +10,30 @@ import { memoryRoot } from "./paths.js";
|
|
|
10
10
|
* - every existing component is lstat-checked: symlinks are rejected, so a
|
|
11
11
|
* link placed inside the workspace cannot lead reads outside it
|
|
12
12
|
*/
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The memory root itself must not be a symlink: every scoped resolution and
|
|
15
|
+
* every workspace walk starts there, so a symlinked root would redirect ALL
|
|
16
|
+
* memory reads/writes elsewhere on disk. codex rejects a symlinked root when
|
|
17
|
+
* clearing (control.rs clear_memory_root_contents); the model-facing tools
|
|
18
|
+
* here extend that check to every memory operation. Returns the root path.
|
|
19
|
+
* A missing root is fine — callers create it as a real directory.
|
|
20
|
+
*/
|
|
21
|
+
export function assertMemoryRootSafe() {
|
|
14
22
|
const root = memoryRoot();
|
|
23
|
+
let st = null;
|
|
24
|
+
try {
|
|
25
|
+
st = fs.lstatSync(root);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return root;
|
|
29
|
+
}
|
|
30
|
+
if (st.isSymbolicLink()) {
|
|
31
|
+
throw new Error(`memory root is a symlink; refusing memory operations: ${root}`);
|
|
32
|
+
}
|
|
33
|
+
return root;
|
|
34
|
+
}
|
|
35
|
+
export function safeResolveMemoryPath(rel) {
|
|
36
|
+
const root = assertMemoryRootSafe();
|
|
15
37
|
if (path.isAbsolute(rel)) {
|
|
16
38
|
throw new Error(`path escapes memory root: ${rel}`);
|
|
17
39
|
}
|
package/dist/src/paths.d.ts
CHANGED
package/dist/src/paths.js
CHANGED
|
@@ -15,9 +15,6 @@ export function memoryRoot() {
|
|
|
15
15
|
export function memoryDbPath() {
|
|
16
16
|
return path.join(dataRoot(), MEMORY_DB_NAME);
|
|
17
17
|
}
|
|
18
|
-
export function opencodeDbPath() {
|
|
19
|
-
return path.join(dataRoot(), "opencode.db");
|
|
20
|
-
}
|
|
21
18
|
export function memorySummaryPath() {
|
|
22
19
|
return path.join(memoryRoot(), "memory_summary.md");
|
|
23
20
|
}
|
package/dist/src/phase1.d.ts
CHANGED
|
@@ -9,3 +9,4 @@ export interface Phase1Options {
|
|
|
9
9
|
}
|
|
10
10
|
export declare const DEFAULT_PHASE1_OPTIONS: Phase1Options;
|
|
11
11
|
export declare function runPhase1(store: MemoryStore, opts?: Phase1Options): Promise<void>;
|
|
12
|
+
export declare function buildTranscript(sessionId: string): Promise<string>;
|
package/dist/src/phase1.js
CHANGED
|
@@ -26,7 +26,7 @@ export async function runPhase1(store, opts = DEFAULT_PHASE1_OPTIONS) {
|
|
|
26
26
|
console.warn("[opencode-codex-memory] skipping phase1 due to rate limit:", rl.reason);
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
|
-
const eligible = selectEligibleSessions(store, opts);
|
|
29
|
+
const eligible = await selectEligibleSessions(store, opts);
|
|
30
30
|
if (eligible.length === 0)
|
|
31
31
|
return;
|
|
32
32
|
const claimed = store.claimStage1Jobs(eligible, opts.excludeSession, opts.maxClaimed);
|
|
@@ -38,7 +38,7 @@ export async function runPhase1(store, opts = DEFAULT_PHASE1_OPTIONS) {
|
|
|
38
38
|
try {
|
|
39
39
|
const session = sessionById.get(sid);
|
|
40
40
|
const sourceUpdatedAt = session?.updated_at ?? Date.now();
|
|
41
|
-
const transcript = buildTranscript(sid);
|
|
41
|
+
const transcript = await buildTranscript(sid);
|
|
42
42
|
if (!transcript.trim()) {
|
|
43
43
|
store.markStage1SucceededNoOutput(sid, claim.ownershipToken, sourceUpdatedAt);
|
|
44
44
|
return;
|
|
@@ -68,14 +68,19 @@ export async function runPhase1(store, opts = DEFAULT_PHASE1_OPTIONS) {
|
|
|
68
68
|
}
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
|
-
function buildTranscript(sessionId) {
|
|
72
|
-
const msgs = loadTranscript(sessionId);
|
|
71
|
+
export async function buildTranscript(sessionId) {
|
|
72
|
+
const msgs = await loadTranscript(sessionId);
|
|
73
73
|
if (msgs.length === 0)
|
|
74
74
|
return "";
|
|
75
75
|
const lines = [];
|
|
76
76
|
for (const m of msgs) {
|
|
77
77
|
if (m.type === "system")
|
|
78
78
|
continue;
|
|
79
|
+
// codex sanitize_response_item_for_memories drops developer-role messages
|
|
80
|
+
// entirely (injected instructions, not conversation). opencode 1.17 only
|
|
81
|
+
// stores user/assistant roles; this guards future role additions.
|
|
82
|
+
if (m.role === "developer")
|
|
83
|
+
continue;
|
|
79
84
|
const role = m.role ?? m.type;
|
|
80
85
|
const text = m.text ?? "";
|
|
81
86
|
if (!text.trim())
|
package/dist/src/phase2.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export interface Phase2Options {
|
|
|
6
6
|
consolidationModel?: string;
|
|
7
7
|
}
|
|
8
8
|
export declare const DEFAULT_PHASE2_OPTIONS: Phase2Options;
|
|
9
|
+
/** True while THIS process runs a consolidation (memory_reset refuses then). */
|
|
10
|
+
export declare function isPhase2InFlight(): boolean;
|
|
9
11
|
export declare function runPhase2(store: MemoryStore, opts?: Phase2Options): Promise<{
|
|
10
12
|
status: string;
|
|
11
13
|
}>;
|
package/dist/src/phase2.js
CHANGED
|
@@ -10,6 +10,10 @@ export const DEFAULT_PHASE2_OPTIONS = {
|
|
|
10
10
|
extensionRetentionDays: 7,
|
|
11
11
|
};
|
|
12
12
|
let phase2InFlight = false;
|
|
13
|
+
/** True while THIS process runs a consolidation (memory_reset refuses then). */
|
|
14
|
+
export function isPhase2InFlight() {
|
|
15
|
+
return phase2InFlight;
|
|
16
|
+
}
|
|
13
17
|
export async function runPhase2(store, opts = DEFAULT_PHASE2_OPTIONS) {
|
|
14
18
|
if (phase2InFlight)
|
|
15
19
|
return { status: "already_running" };
|
package/dist/src/redact.js
CHANGED
|
@@ -10,8 +10,11 @@ const REDACTIONS = [
|
|
|
10
10
|
re: /-----BEGIN [A-Z]+ PRIVATE KEY-----[\s\S]*?-----END [A-Z]+ PRIVATE KEY-----/g,
|
|
11
11
|
replacement: "[REDACTED:private-key]",
|
|
12
12
|
},
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// Optional quotes around the KEY cover JSON/YAML forms like
|
|
14
|
+
// "password": "value" — codex's SECRET_ASSIGNMENT_REGEX misses those (it
|
|
15
|
+
// allows a quote only before the value); this is a deliberate superset.
|
|
16
|
+
{ re: /["']?(password|passwd|pwd|secret|api[_-]?key|token|access[_-]?token)["']?\s*[:=]\s*["']?[^\s"']{4,}["']?/gi, replacement: "$1=[REDACTED]" },
|
|
17
|
+
{ re: /["']?(aws_secret_access_key|aws_access_key_id)["']?\s*[:=]\s*["']?[^\s"']{4,}["']?/gi, replacement: "$1=[REDACTED]" },
|
|
15
18
|
];
|
|
16
19
|
export function redact(text) {
|
|
17
20
|
let out = text;
|
package/dist/src/store.d.ts
CHANGED
|
@@ -69,6 +69,11 @@ export declare class MemoryStore {
|
|
|
69
69
|
* still back the consolidated artifacts.
|
|
70
70
|
*/
|
|
71
71
|
markPhase2Succeeded(ownershipToken: string, selected?: Pick<Stage1Output, "session_id" | "source_updated_at">[]): void;
|
|
72
|
+
/** Last recorded phase-2 success info (memory_inspect). Null when phase 2 never succeeded. */
|
|
73
|
+
phase2LastSuccess(): {
|
|
74
|
+
finished_at: number | null;
|
|
75
|
+
last_success_watermark: number | null;
|
|
76
|
+
} | null;
|
|
72
77
|
markPhase2Failed(ownershipToken: string, error: string): void;
|
|
73
78
|
/**
|
|
74
79
|
* Phase 2 input set, mirroring codex get_phase2_input_selection:
|
package/dist/src/store.js
CHANGED
|
@@ -269,19 +269,35 @@ export class MemoryStore {
|
|
|
269
269
|
// codex stores the completion watermark = max source_updated_at consumed;
|
|
270
270
|
// the 6h cooldown is keyed on finished_at, not on this value.
|
|
271
271
|
const watermark = selected.reduce((max, s) => Math.max(max, s.source_updated_at), 0);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
272
|
+
// One transaction for the job row + the selected-input flags (codex
|
|
273
|
+
// mark_global_phase2_job_succeeded does the same): a crash between them
|
|
274
|
+
// must not leave a done job whose retention flags still describe the
|
|
275
|
+
// previous run — pruning could then delete inputs backing the artifacts.
|
|
276
|
+
this.db.transaction(() => {
|
|
277
|
+
const res = this.db
|
|
278
|
+
.prepare(`UPDATE memory_jobs SET status='done', finished_at=?, lease_until=NULL, last_error=NULL, retry_remaining=?,
|
|
279
|
+
last_success_watermark=MAX(COALESCE(last_success_watermark, 0), ?), retry_at=NULL
|
|
280
|
+
WHERE kind='memory_consolidate_global' AND job_key='global' AND ownership_token=? AND status='running'`)
|
|
281
|
+
.run(nowSec(), DEFAULT_RETRY_REMAINING, watermark, ownershipToken);
|
|
282
|
+
if (res.changes === 0)
|
|
283
|
+
return;
|
|
284
|
+
this.db.exec("UPDATE memory_stage1_outputs SET selected_for_phase2 = 0, selected_for_phase2_source_updated_at = NULL");
|
|
285
|
+
const mark = this.db.prepare(`UPDATE memory_stage1_outputs
|
|
286
|
+
SET selected_for_phase2 = 1, selected_for_phase2_source_updated_at = ?
|
|
287
|
+
WHERE session_id = ? AND source_updated_at = ?`);
|
|
288
|
+
for (const s of selected)
|
|
289
|
+
mark.run(s.source_updated_at, s.session_id, s.source_updated_at);
|
|
290
|
+
}).immediate();
|
|
291
|
+
}
|
|
292
|
+
/** Last recorded phase-2 success info (memory_inspect). Null when phase 2 never succeeded. */
|
|
293
|
+
phase2LastSuccess() {
|
|
294
|
+
const row = this.db
|
|
295
|
+
.prepare(`SELECT finished_at, last_success_watermark FROM memory_jobs
|
|
296
|
+
WHERE kind='memory_consolidate_global' AND job_key='global'`)
|
|
297
|
+
.get();
|
|
298
|
+
if (!row || !row.last_success_watermark)
|
|
299
|
+
return null;
|
|
300
|
+
return row;
|
|
285
301
|
}
|
|
286
302
|
markPhase2Failed(ownershipToken, error) {
|
|
287
303
|
const res = this.db
|
package/dist/src/workspace.js
CHANGED
|
@@ -166,9 +166,10 @@ export function pruneExtensionResources(retentionDays) {
|
|
|
166
166
|
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
167
167
|
for (const extName of fs.readdirSync(extensionsDir)) {
|
|
168
168
|
const extDir = path.join(extensionsDir, extName);
|
|
169
|
+
// lstat: never prune through a symlinked extension dir.
|
|
169
170
|
let extStat;
|
|
170
171
|
try {
|
|
171
|
-
extStat = fs.
|
|
172
|
+
extStat = fs.lstatSync(extDir);
|
|
172
173
|
}
|
|
173
174
|
catch {
|
|
174
175
|
continue;
|
|
@@ -213,8 +214,12 @@ export function writeWorkspaceDiff(diff) {
|
|
|
213
214
|
rendered += `- ${change.status} ${change.path}\n`;
|
|
214
215
|
}
|
|
215
216
|
let body = diff.unifiedDiff;
|
|
216
|
-
|
|
217
|
-
|
|
217
|
+
// The cap is in BYTES: .length counts UTF-16 code units and undercounts
|
|
218
|
+
// multibyte content. Cut on the byte buffer and drop a split trailing char.
|
|
219
|
+
if (Buffer.byteLength(body, "utf8") > WORKSPACE_DIFF_MAX_BYTES) {
|
|
220
|
+
body =
|
|
221
|
+
Buffer.from(body, "utf8").subarray(0, WORKSPACE_DIFF_MAX_BYTES).toString("utf8").replace(/\uFFFD+$/, "") +
|
|
222
|
+
`\n[workspace diff truncated at ${WORKSPACE_DIFF_MAX_BYTES} bytes]\n`;
|
|
218
223
|
}
|
|
219
224
|
rendered += "\n## Diff\n\n```diff\n" + body + (body.endsWith("\n") ? "" : "\n") + "```\n";
|
|
220
225
|
}
|
package/dist/tools/control.js
CHANGED
|
@@ -5,32 +5,34 @@ import { memoryRoot, memorySummaryPath } from "../src/paths.js";
|
|
|
5
5
|
import { MemoryStore } from "../src/store.js";
|
|
6
6
|
import { invalidateCache } from "../src/source.js";
|
|
7
7
|
import { estimateTokens } from "../src/token.js";
|
|
8
|
+
import { assertMemoryRootSafe } from "../src/path-guard.js";
|
|
9
|
+
import { isPhase2InFlight } from "../src/phase2.js";
|
|
8
10
|
function isSymlinkedRoot() {
|
|
9
|
-
const root = memoryRoot();
|
|
10
11
|
try {
|
|
11
|
-
|
|
12
|
+
assertMemoryRootSafe();
|
|
13
|
+
return false;
|
|
12
14
|
}
|
|
13
15
|
catch {
|
|
14
|
-
return
|
|
16
|
+
return true;
|
|
15
17
|
}
|
|
16
18
|
}
|
|
17
19
|
// Mirrors codex clear_memory_root_contents: deletes EVERY entry including
|
|
18
20
|
// .git, so previously deleted/redacted memory content is not recoverable
|
|
19
|
-
// from git history after a reset.
|
|
21
|
+
// from git history after a reset. Deletion errors PROPAGATE — codex bubbles
|
|
22
|
+
// every remove failure up, and a swallowed error here would report a
|
|
23
|
+
// successful reset while secrets/memories survive on disk. lstat semantics:
|
|
24
|
+
// a symlinked entry is unlinked itself, never followed.
|
|
20
25
|
function wipeMemoriesDir() {
|
|
21
26
|
const root = memoryRoot();
|
|
22
27
|
if (!fs.existsSync(root))
|
|
23
28
|
return;
|
|
24
29
|
for (const entry of fs.readdirSync(root)) {
|
|
25
30
|
const abs = path.join(root, entry);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
fs.unlinkSync(abs);
|
|
32
|
-
}
|
|
33
|
-
catch { }
|
|
31
|
+
const st = fs.lstatSync(abs);
|
|
32
|
+
if (st.isDirectory())
|
|
33
|
+
fs.rmSync(abs, { recursive: true, force: true });
|
|
34
|
+
else
|
|
35
|
+
fs.unlinkSync(abs);
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
function listMemoriesDir() {
|
|
@@ -44,14 +46,19 @@ function listMemoriesDir() {
|
|
|
44
46
|
continue;
|
|
45
47
|
const abs = path.join(dir, name);
|
|
46
48
|
const rel = prefix ? `${prefix}/${name}` : name;
|
|
49
|
+
// lstat: report symlinks as entries but never walk THROUGH them —
|
|
50
|
+
// a link to a big/looping directory must not be followed.
|
|
47
51
|
let stat;
|
|
48
52
|
try {
|
|
49
|
-
stat = fs.
|
|
53
|
+
stat = fs.lstatSync(abs);
|
|
50
54
|
}
|
|
51
55
|
catch {
|
|
52
56
|
continue;
|
|
53
57
|
}
|
|
54
|
-
if (stat.
|
|
58
|
+
if (stat.isSymbolicLink()) {
|
|
59
|
+
out.push(`${rel}@`);
|
|
60
|
+
}
|
|
61
|
+
else if (stat.isDirectory()) {
|
|
55
62
|
out.push(`${rel}/`);
|
|
56
63
|
walk(abs, rel);
|
|
57
64
|
}
|
|
@@ -76,6 +83,15 @@ export const memory_reset = tool({
|
|
|
76
83
|
if (isSymlinkedRoot()) {
|
|
77
84
|
return { output: "Reset refused: memory root is a symlink. Remove it manually to be safe." };
|
|
78
85
|
}
|
|
86
|
+
// A consolidation running in THIS process would recreate files right
|
|
87
|
+
// after the wipe (the sub-agent edits live artifacts and resets the git
|
|
88
|
+
// baseline). Refuse instead of racing it. Cross-process consolidators
|
|
89
|
+
// are still ownership-guarded DB-side (the wiped job rows make their
|
|
90
|
+
// final confirmation a no-op) but may leave stray files; same window
|
|
91
|
+
// codex has between CLI clear and a running daemon.
|
|
92
|
+
if (isPhase2InFlight()) {
|
|
93
|
+
return { output: "Reset refused: memory consolidation is currently running. Try again in a few minutes." };
|
|
94
|
+
}
|
|
79
95
|
try {
|
|
80
96
|
const store = new MemoryStore();
|
|
81
97
|
store.clearMemoryData();
|
|
@@ -96,6 +112,8 @@ export const memory_inspect = tool({
|
|
|
96
112
|
args: {},
|
|
97
113
|
async execute() {
|
|
98
114
|
try {
|
|
115
|
+
// Refuse to walk/report through a symlinked root (same rule as reset).
|
|
116
|
+
assertMemoryRootSafe();
|
|
99
117
|
const store = new MemoryStore();
|
|
100
118
|
const outputs = store.stage1Outputs();
|
|
101
119
|
const summaryPath = memorySummaryPath();
|
|
@@ -107,8 +125,14 @@ export const memory_inspect = tool({
|
|
|
107
125
|
summaryTokens = estimateTokens(text);
|
|
108
126
|
}
|
|
109
127
|
const listing = listMemoriesDir();
|
|
128
|
+
// The tool description promises the last Phase 2 success watermark.
|
|
129
|
+
const phase2 = store.phase2LastSuccess();
|
|
130
|
+
const watermark = phase2?.last_success_watermark ? new Date(phase2.last_success_watermark).toISOString() : "none";
|
|
131
|
+
const finishedAt = phase2?.finished_at ? new Date(phase2.finished_at * 1000).toISOString() : "none";
|
|
110
132
|
const out = [
|
|
111
133
|
`stage1_outputs: ${outputs.length}`,
|
|
134
|
+
`phase2_last_success_watermark: ${watermark}`,
|
|
135
|
+
`phase2_last_finished_at: ${finishedAt}`,
|
|
112
136
|
`memory_summary_chars: ${summaryChars}`,
|
|
113
137
|
`memory_summary_tokens_est: ${summaryTokens}`,
|
|
114
138
|
`memories_dir_entries: ${listing.length}`,
|
|
@@ -120,6 +144,8 @@ export const memory_inspect = tool({
|
|
|
120
144
|
output: out,
|
|
121
145
|
metadata: {
|
|
122
146
|
stage1_count: outputs.length,
|
|
147
|
+
phase2_last_success_watermark: phase2?.last_success_watermark ?? null,
|
|
148
|
+
phase2_last_finished_at: phase2?.finished_at ?? null,
|
|
123
149
|
summary_chars: summaryChars,
|
|
124
150
|
summary_tokens_est: summaryTokens,
|
|
125
151
|
files: listing,
|
package/dist/tools/memory.d.ts
CHANGED
|
@@ -25,16 +25,32 @@ export declare const memory_list: {
|
|
|
25
25
|
export declare const memory_search: {
|
|
26
26
|
description: string;
|
|
27
27
|
args: {
|
|
28
|
-
|
|
28
|
+
queries: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
|
|
29
|
+
match_mode: import("zod").ZodDefault<import("zod").ZodEnum<{
|
|
30
|
+
any: "any";
|
|
31
|
+
all_on_same_line: "all_on_same_line";
|
|
32
|
+
all_within_lines: "all_within_lines";
|
|
33
|
+
}>>;
|
|
34
|
+
line_count: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
35
|
+
path: import("zod").ZodOptional<import("zod").ZodString>;
|
|
36
|
+
cursor: import("zod").ZodOptional<import("zod").ZodString>;
|
|
37
|
+
context_lines: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
29
38
|
case_sensitive: import("zod").ZodDefault<import("zod").ZodBoolean>;
|
|
39
|
+
normalized: import("zod").ZodDefault<import("zod").ZodBoolean>;
|
|
30
40
|
since: import("zod").ZodOptional<import("zod").ZodString>;
|
|
31
41
|
until: import("zod").ZodOptional<import("zod").ZodString>;
|
|
32
|
-
|
|
42
|
+
max_results: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
33
43
|
};
|
|
34
44
|
execute(args: {
|
|
45
|
+
match_mode: "any" | "all_on_same_line" | "all_within_lines";
|
|
46
|
+
context_lines: number;
|
|
35
47
|
case_sensitive: boolean;
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
normalized: boolean;
|
|
49
|
+
max_results: number;
|
|
50
|
+
queries?: string[] | undefined;
|
|
51
|
+
line_count?: number | undefined;
|
|
52
|
+
path?: string | undefined;
|
|
53
|
+
cursor?: string | undefined;
|
|
38
54
|
since?: string | undefined;
|
|
39
55
|
until?: string | undefined;
|
|
40
56
|
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|