opencode-gateway 0.2.9 → 0.3.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 +28 -11
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/opencode-server.d.ts +19 -0
- package/dist/cli/serve.d.ts +6 -0
- package/dist/cli/warm.d.ts +6 -0
- package/dist/cli.js +207 -49
- package/dist/config/memory.d.ts +2 -1
- package/dist/gateway.d.ts +3 -1
- package/dist/index.js +399 -85
- package/dist/memory/files.d.ts +13 -0
- package/dist/memory/runtime.d.ts +28 -0
- package/dist/tools/memory-get.d.ts +3 -0
- package/dist/tools/memory-search.d.ts +3 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -28,10 +28,22 @@ Check what it resolved:
|
|
|
28
28
|
npx opencode-gateway doctor
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Recommended:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
opencode-gateway serve
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This wraps `opencode serve` and warms the gateway plugin worker immediately, so
|
|
38
|
+
Telegram polling and scheduled jobs do not stay idle until the first
|
|
39
|
+
project-scoped request.
|
|
40
|
+
|
|
41
|
+
If you still prefer the raw OpenCode command, warm the gateway explicitly after
|
|
42
|
+
startup:
|
|
32
43
|
|
|
33
44
|
```bash
|
|
34
45
|
opencode serve
|
|
46
|
+
opencode-gateway warm
|
|
35
47
|
```
|
|
36
48
|
|
|
37
49
|
If you want a separate managed config tree instead of editing your existing
|
|
@@ -39,9 +51,7 @@ OpenCode config:
|
|
|
39
51
|
|
|
40
52
|
```bash
|
|
41
53
|
npx opencode-gateway init --managed
|
|
42
|
-
|
|
43
|
-
export OPENCODE_CONFIG_DIR="$HOME/.config/opencode-gateway/opencode"
|
|
44
|
-
opencode serve
|
|
54
|
+
opencode-gateway serve --managed
|
|
45
55
|
```
|
|
46
56
|
|
|
47
57
|
## Example gateway config
|
|
@@ -88,8 +98,12 @@ inject_content = true
|
|
|
88
98
|
[[memory.entries]]
|
|
89
99
|
path = "memory/notes"
|
|
90
100
|
description = "Domain notes and operating docs"
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
search_only = true
|
|
102
|
+
|
|
103
|
+
[[memory.entries]]
|
|
104
|
+
path = "memory/snippets"
|
|
105
|
+
description = "Selected files are auto-injected; the rest stay searchable on demand"
|
|
106
|
+
globs = ["**/*.md", "notes/**/*.txt"]
|
|
93
107
|
```
|
|
94
108
|
|
|
95
109
|
When Telegram is enabled, export the bot token through the configured
|
|
@@ -112,13 +126,16 @@ Mailbox rules:
|
|
|
112
126
|
Memory rules:
|
|
113
127
|
|
|
114
128
|
- all entries inject their configured path and description
|
|
115
|
-
- file contents are injected only when `inject_content = true`
|
|
116
|
-
-
|
|
117
|
-
-
|
|
118
|
-
-
|
|
119
|
-
|
|
129
|
+
- file contents are auto-injected only when `inject_content = true`
|
|
130
|
+
- `search_only = true` keeps an entry available to `memory_search` and `memory_get`
|
|
131
|
+
without auto-injecting its content
|
|
132
|
+
- directory entries default to description-only plus on-demand search
|
|
133
|
+
- directory `globs` are relative to the configured directory and define which
|
|
134
|
+
files are auto-injected; other UTF-8 text files remain searchable on demand
|
|
120
135
|
- relative paths are resolved from `opencode-gateway-workspace`
|
|
121
136
|
- absolute paths are still allowed
|
|
122
137
|
- missing files and directories are created automatically on load
|
|
123
138
|
- the default template includes `USER.md` as persistent user-profile memory
|
|
124
139
|
- memory is injected only into gateway-managed sessions
|
|
140
|
+
- `memory_search` returns matching snippets and paths; `memory_get` reads a
|
|
141
|
+
specific configured memory file by path and optional line window
|
package/dist/cli/args.d.ts
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type CliOptions = {
|
|
2
|
+
managed: boolean;
|
|
3
|
+
configDir: string | null;
|
|
4
|
+
};
|
|
5
|
+
export type ResolvedServeTarget = {
|
|
6
|
+
configDir: string;
|
|
7
|
+
gatewayConfigPath: string;
|
|
8
|
+
workspaceDirPath: string;
|
|
9
|
+
opencodeConfigPath: string;
|
|
10
|
+
serverOrigin: string;
|
|
11
|
+
env: Record<string, string>;
|
|
12
|
+
};
|
|
13
|
+
export declare function resolveServeTarget(options: CliOptions, env: Record<string, string | undefined>): Promise<ResolvedServeTarget>;
|
|
14
|
+
export declare function warmGatewayProject(target: ResolvedServeTarget, options?: {
|
|
15
|
+
deadlineMs?: number;
|
|
16
|
+
intervalMs?: number;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
export declare function resolveServerOriginFromDocument(document: Record<string, unknown>): string;
|
|
19
|
+
export {};
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ function parseCliCommand(argv) {
|
|
|
7
7
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
8
8
|
return { kind: "help" };
|
|
9
9
|
}
|
|
10
|
-
if (command !== "init" && command !== "doctor") {
|
|
10
|
+
if (command !== "init" && command !== "doctor" && command !== "serve" && command !== "warm") {
|
|
11
11
|
throw new Error(`unknown command: ${command}`);
|
|
12
12
|
}
|
|
13
13
|
let managed = false;
|
|
@@ -48,6 +48,8 @@ function formatCliHelp() {
|
|
|
48
48
|
"Commands:",
|
|
49
49
|
" opencode-gateway init [--managed] [--config-dir <path>]",
|
|
50
50
|
" opencode-gateway doctor [--managed] [--config-dir <path>]",
|
|
51
|
+
" opencode-gateway warm [--managed] [--config-dir <path>]",
|
|
52
|
+
" opencode-gateway serve [--managed] [--config-dir <path>]",
|
|
51
53
|
"",
|
|
52
54
|
"Defaults:",
|
|
53
55
|
" init/doctor use OPENCODE_CONFIG_DIR when set, otherwise ~/.config/opencode",
|
|
@@ -57,42 +59,7 @@ function formatCliHelp() {
|
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
// src/cli/doctor.ts
|
|
60
|
-
import { readFile } from "node:fs/promises";
|
|
61
|
-
import { join as join3 } from "node:path";
|
|
62
|
-
|
|
63
|
-
// src/config/paths.ts
|
|
64
|
-
import { homedir } from "node:os";
|
|
65
|
-
import { dirname, join, resolve as resolve2 } from "node:path";
|
|
66
|
-
var GATEWAY_CONFIG_FILE = "opencode-gateway.toml";
|
|
67
|
-
var OPENCODE_CONFIG_FILE = "opencode.json";
|
|
68
|
-
var OPENCODE_CONFIG_FILE_JSONC = "opencode.jsonc";
|
|
69
|
-
var OPENCODE_CONFIG_FILE_CANDIDATES = [OPENCODE_CONFIG_FILE_JSONC, OPENCODE_CONFIG_FILE];
|
|
70
|
-
var GATEWAY_WORKSPACE_DIR = "opencode-gateway-workspace";
|
|
71
|
-
function resolveOpencodeConfigDir(env) {
|
|
72
|
-
const explicit = env.OPENCODE_CONFIG_DIR;
|
|
73
|
-
if (explicit && explicit.trim().length > 0) {
|
|
74
|
-
return resolve2(explicit);
|
|
75
|
-
}
|
|
76
|
-
return defaultOpencodeConfigDir(env);
|
|
77
|
-
}
|
|
78
|
-
function resolveManagedOpencodeConfigDir(env) {
|
|
79
|
-
return join(resolveConfigHome(env), "opencode-gateway", "opencode");
|
|
80
|
-
}
|
|
81
|
-
function resolveGatewayWorkspacePath(configPath) {
|
|
82
|
-
return join(dirname(configPath), GATEWAY_WORKSPACE_DIR);
|
|
83
|
-
}
|
|
84
|
-
function defaultGatewayStateDbPath(env) {
|
|
85
|
-
return join(resolveDataHome(env), "opencode-gateway", "state.db");
|
|
86
|
-
}
|
|
87
|
-
function resolveConfigHome(env) {
|
|
88
|
-
return env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
89
|
-
}
|
|
90
|
-
function resolveDataHome(env) {
|
|
91
|
-
return env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
|
|
92
|
-
}
|
|
93
|
-
function defaultOpencodeConfigDir(env) {
|
|
94
|
-
return join(resolveConfigHome(env), "opencode");
|
|
95
|
-
}
|
|
62
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
96
63
|
|
|
97
64
|
// src/cli/opencode-config.ts
|
|
98
65
|
var OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json";
|
|
@@ -290,6 +257,47 @@ function isGatewayPluginReference(entry) {
|
|
|
290
257
|
// src/cli/opencode-config-file.ts
|
|
291
258
|
import { join as join2 } from "node:path";
|
|
292
259
|
|
|
260
|
+
// src/config/paths.ts
|
|
261
|
+
import { homedir } from "node:os";
|
|
262
|
+
import { dirname, join, resolve as resolve2 } from "node:path";
|
|
263
|
+
var GATEWAY_CONFIG_FILE = "opencode-gateway.toml";
|
|
264
|
+
var OPENCODE_CONFIG_FILE = "opencode.json";
|
|
265
|
+
var OPENCODE_CONFIG_FILE_JSONC = "opencode.jsonc";
|
|
266
|
+
var OPENCODE_CONFIG_FILE_CANDIDATES = [OPENCODE_CONFIG_FILE_JSONC, OPENCODE_CONFIG_FILE];
|
|
267
|
+
var GATEWAY_WORKSPACE_DIR = "opencode-gateway-workspace";
|
|
268
|
+
function resolveGatewayConfigPath(env) {
|
|
269
|
+
const explicit = env.OPENCODE_GATEWAY_CONFIG;
|
|
270
|
+
if (explicit && explicit.trim().length > 0) {
|
|
271
|
+
return resolve2(explicit);
|
|
272
|
+
}
|
|
273
|
+
return join(resolveOpencodeConfigDir(env), GATEWAY_CONFIG_FILE);
|
|
274
|
+
}
|
|
275
|
+
function resolveOpencodeConfigDir(env) {
|
|
276
|
+
const explicit = env.OPENCODE_CONFIG_DIR;
|
|
277
|
+
if (explicit && explicit.trim().length > 0) {
|
|
278
|
+
return resolve2(explicit);
|
|
279
|
+
}
|
|
280
|
+
return defaultOpencodeConfigDir(env);
|
|
281
|
+
}
|
|
282
|
+
function resolveManagedOpencodeConfigDir(env) {
|
|
283
|
+
return join(resolveConfigHome(env), "opencode-gateway", "opencode");
|
|
284
|
+
}
|
|
285
|
+
function resolveGatewayWorkspacePath(configPath) {
|
|
286
|
+
return join(dirname(configPath), GATEWAY_WORKSPACE_DIR);
|
|
287
|
+
}
|
|
288
|
+
function defaultGatewayStateDbPath(env) {
|
|
289
|
+
return join(resolveDataHome(env), "opencode-gateway", "state.db");
|
|
290
|
+
}
|
|
291
|
+
function resolveConfigHome(env) {
|
|
292
|
+
return env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
293
|
+
}
|
|
294
|
+
function resolveDataHome(env) {
|
|
295
|
+
return env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
|
|
296
|
+
}
|
|
297
|
+
function defaultOpencodeConfigDir(env) {
|
|
298
|
+
return join(resolveConfigHome(env), "opencode");
|
|
299
|
+
}
|
|
300
|
+
|
|
293
301
|
// src/cli/paths.ts
|
|
294
302
|
import { constants } from "node:fs";
|
|
295
303
|
import { access } from "node:fs/promises";
|
|
@@ -329,19 +337,106 @@ async function resolveOpencodeConfigFile(configDir) {
|
|
|
329
337
|
};
|
|
330
338
|
}
|
|
331
339
|
|
|
340
|
+
// src/cli/opencode-server.ts
|
|
341
|
+
import { readFile } from "node:fs/promises";
|
|
342
|
+
var DEFAULT_SERVER_ORIGIN = "http://127.0.0.1:4096";
|
|
343
|
+
var WARM_REQUEST_TIMEOUT_MS = 2000;
|
|
344
|
+
async function resolveServeTarget(options, env) {
|
|
345
|
+
const configDir = resolveCliConfigDir(options, env);
|
|
346
|
+
const gatewayConfigPath = resolveGatewayConfigPath({
|
|
347
|
+
...env,
|
|
348
|
+
OPENCODE_CONFIG_DIR: configDir
|
|
349
|
+
});
|
|
350
|
+
const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
|
|
351
|
+
const opencodeConfig = await resolveOpencodeConfigFile(configDir);
|
|
352
|
+
const serverOrigin = opencodeConfig.exists ? await resolveServerOrigin(opencodeConfig.path) : DEFAULT_SERVER_ORIGIN;
|
|
353
|
+
return {
|
|
354
|
+
configDir,
|
|
355
|
+
gatewayConfigPath,
|
|
356
|
+
workspaceDirPath,
|
|
357
|
+
opencodeConfigPath: opencodeConfig.path,
|
|
358
|
+
serverOrigin,
|
|
359
|
+
env: {
|
|
360
|
+
OPENCODE_CONFIG_DIR: configDir,
|
|
361
|
+
OPENCODE_CONFIG: opencodeConfig.path
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
async function warmGatewayProject(target, options) {
|
|
366
|
+
const deadlineMs = options?.deadlineMs ?? 30000;
|
|
367
|
+
const intervalMs = options?.intervalMs ?? 250;
|
|
368
|
+
const warmUrl = new URL("/experimental/tool/ids", target.serverOrigin);
|
|
369
|
+
warmUrl.searchParams.set("directory", target.workspaceDirPath);
|
|
370
|
+
const deadline = Date.now() + deadlineMs;
|
|
371
|
+
while (Date.now() < deadline) {
|
|
372
|
+
try {
|
|
373
|
+
const response = await fetch(warmUrl, {
|
|
374
|
+
signal: AbortSignal.timeout(WARM_REQUEST_TIMEOUT_MS)
|
|
375
|
+
});
|
|
376
|
+
if (response.ok) {
|
|
377
|
+
const payload = await response.json();
|
|
378
|
+
if (isGatewayToolList(payload)) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
} catch {}
|
|
383
|
+
await delay(intervalMs);
|
|
384
|
+
}
|
|
385
|
+
throw new Error(`failed to warm the gateway plugin at ${target.serverOrigin} for ${target.workspaceDirPath}`);
|
|
386
|
+
}
|
|
387
|
+
function resolveServerOriginFromDocument(document) {
|
|
388
|
+
const server = document.server;
|
|
389
|
+
if (server === null || typeof server !== "object" || Array.isArray(server)) {
|
|
390
|
+
return DEFAULT_SERVER_ORIGIN;
|
|
391
|
+
}
|
|
392
|
+
const hostname = readNonEmptyString(server.hostname);
|
|
393
|
+
const port = readPort(server.port);
|
|
394
|
+
if (hostname === null || port === null) {
|
|
395
|
+
return DEFAULT_SERVER_ORIGIN;
|
|
396
|
+
}
|
|
397
|
+
return `http://${hostname}:${port}`;
|
|
398
|
+
}
|
|
399
|
+
async function resolveServerOrigin(configPath) {
|
|
400
|
+
const source = await readFile(configPath, "utf8");
|
|
401
|
+
const document = parseOpencodeConfig(source, configPath);
|
|
402
|
+
return resolveServerOriginFromDocument(document);
|
|
403
|
+
}
|
|
404
|
+
function readNonEmptyString(value) {
|
|
405
|
+
if (typeof value !== "string") {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
const normalized = value.trim();
|
|
409
|
+
return normalized.length > 0 ? normalized : null;
|
|
410
|
+
}
|
|
411
|
+
function readPort(value) {
|
|
412
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
return value >= 1 && value <= 65535 ? value : null;
|
|
416
|
+
}
|
|
417
|
+
function isGatewayToolList(value) {
|
|
418
|
+
return Array.isArray(value) && value.includes("gateway_status");
|
|
419
|
+
}
|
|
420
|
+
function delay(durationMs) {
|
|
421
|
+
return new Promise((resolve4) => {
|
|
422
|
+
setTimeout(resolve4, durationMs);
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
332
426
|
// src/cli/doctor.ts
|
|
333
427
|
async function runDoctor(options, env) {
|
|
334
428
|
const configDir = resolveCliConfigDir(options, env);
|
|
335
|
-
const gatewayConfigPath = join3(configDir, GATEWAY_CONFIG_FILE);
|
|
336
|
-
const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
|
|
337
429
|
const opencodeConfig = await resolveOpencodeConfigFile(configDir);
|
|
338
430
|
const opencodeStatus = await inspectOpencodeConfig(opencodeConfig.path);
|
|
339
431
|
const gatewayOverride = env.OPENCODE_GATEWAY_CONFIG?.trim() || null;
|
|
432
|
+
const serveTarget = await resolveServeTarget(options, env);
|
|
340
433
|
console.log("doctor report");
|
|
341
434
|
console.log(` config dir: ${configDir}`);
|
|
342
435
|
console.log(` opencode config: ${await describePath(opencodeConfig.path)}`);
|
|
343
|
-
console.log(` gateway config: ${await describePath(gatewayConfigPath)}`);
|
|
344
|
-
console.log(` gateway workspace: ${await describePath(workspaceDirPath)}`);
|
|
436
|
+
console.log(` gateway config: ${await describePath(serveTarget.gatewayConfigPath)}`);
|
|
437
|
+
console.log(` gateway workspace: ${await describePath(serveTarget.workspaceDirPath)}`);
|
|
438
|
+
console.log(` warm server: ${serveTarget.serverOrigin}`);
|
|
439
|
+
console.log(` warm directory: ${serveTarget.workspaceDirPath}`);
|
|
345
440
|
console.log(` gateway config override: ${gatewayOverride ?? "not set"}`);
|
|
346
441
|
console.log(` plugin configured: ${opencodeStatus.pluginConfigured}`);
|
|
347
442
|
console.log(` TELEGRAM_BOT_TOKEN: ${env.TELEGRAM_BOT_TOKEN?.trim() ? "set" : "missing"}`);
|
|
@@ -360,7 +455,7 @@ async function inspectOpencodeConfig(path) {
|
|
|
360
455
|
};
|
|
361
456
|
}
|
|
362
457
|
try {
|
|
363
|
-
const parsed = parseOpencodeConfig(await
|
|
458
|
+
const parsed = parseOpencodeConfig(await readFile2(path, "utf8"), path);
|
|
364
459
|
return {
|
|
365
460
|
pluginConfigured: inspectGatewayPlugin(parsed),
|
|
366
461
|
error: null
|
|
@@ -374,8 +469,8 @@ async function inspectOpencodeConfig(path) {
|
|
|
374
469
|
}
|
|
375
470
|
|
|
376
471
|
// src/cli/init.ts
|
|
377
|
-
import { mkdir, readFile as
|
|
378
|
-
import { dirname as dirname2, join as
|
|
472
|
+
import { mkdir, readFile as readFile3, writeFile } from "node:fs/promises";
|
|
473
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
379
474
|
|
|
380
475
|
// src/cli/templates.ts
|
|
381
476
|
function buildGatewayConfigTemplate(stateDbPath) {
|
|
@@ -423,10 +518,19 @@ function buildGatewayConfigTemplate(stateDbPath) {
|
|
|
423
518
|
"inject_content = true",
|
|
424
519
|
"#",
|
|
425
520
|
"# [[memory.entries]]",
|
|
521
|
+
'# path = "memory/project.md"',
|
|
522
|
+
'# description = "Project conventions and long-lived context"',
|
|
523
|
+
"# inject_content = true",
|
|
524
|
+
"#",
|
|
525
|
+
"# [[memory.entries]]",
|
|
426
526
|
'# path = "memory/notes"',
|
|
427
|
-
'# description = "Domain notes and operating docs"',
|
|
428
|
-
"#
|
|
429
|
-
|
|
527
|
+
'# description = "Domain notes and operating docs. Use memory_search and memory_get to inspect this directory on demand."',
|
|
528
|
+
"# search_only = true",
|
|
529
|
+
"#",
|
|
530
|
+
"# [[memory.entries]]",
|
|
531
|
+
'# path = "memory/snippets"',
|
|
532
|
+
'# description = "Selected files are auto-injected; the rest stay searchable on demand."',
|
|
533
|
+
'# globs = ["**/*.md", "notes/**/*.txt"]',
|
|
430
534
|
""
|
|
431
535
|
].join(`
|
|
432
536
|
`);
|
|
@@ -438,7 +542,7 @@ function escapeTomlString(value) {
|
|
|
438
542
|
// src/cli/init.ts
|
|
439
543
|
async function runInit(options, env) {
|
|
440
544
|
const configDir = resolveCliConfigDir(options, env);
|
|
441
|
-
const gatewayConfigPath =
|
|
545
|
+
const gatewayConfigPath = join3(configDir, GATEWAY_CONFIG_FILE);
|
|
442
546
|
const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
|
|
443
547
|
const opencodeConfig = await resolveOpencodeConfigFile(configDir);
|
|
444
548
|
const opencodeConfigPath = opencodeConfig.path;
|
|
@@ -449,7 +553,7 @@ async function runInit(options, env) {
|
|
|
449
553
|
await writeFile(opencodeConfigPath, stringifyOpencodeConfig(createDefaultOpencodeConfig(options.managed)));
|
|
450
554
|
opencodeStatus = "created";
|
|
451
555
|
} else {
|
|
452
|
-
const source = await
|
|
556
|
+
const source = await readFile3(opencodeConfigPath, "utf8");
|
|
453
557
|
const parsed = parseOpencodeConfig(source, opencodeConfigPath);
|
|
454
558
|
const next = ensureGatewayPlugin(parsed);
|
|
455
559
|
if (next.changed) {
|
|
@@ -467,6 +571,54 @@ async function runInit(options, env) {
|
|
|
467
571
|
console.log(`opencode config: ${opencodeConfigPath} (${opencodeStatus})`);
|
|
468
572
|
console.log(`gateway config: ${gatewayConfigPath} (${gatewayStatus})`);
|
|
469
573
|
console.log(`gateway workspace: ${workspaceDirPath} (ready)`);
|
|
574
|
+
console.log("next step: start OpenCode with `opencode-gateway serve`");
|
|
575
|
+
console.log("fallback: if you still run `opencode serve`, run `opencode-gateway warm` after startup");
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/cli/serve.ts
|
|
579
|
+
import { spawn } from "node:child_process";
|
|
580
|
+
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
581
|
+
async function runServe(options, env) {
|
|
582
|
+
const target = await resolveServeTarget(options, env);
|
|
583
|
+
await mkdir2(target.workspaceDirPath, { recursive: true });
|
|
584
|
+
const child = spawn("opencode", ["serve"], {
|
|
585
|
+
stdio: "inherit",
|
|
586
|
+
env: {
|
|
587
|
+
...process.env,
|
|
588
|
+
...target.env
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
warmGatewayProject(target).catch((error) => {
|
|
592
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
593
|
+
console.warn(`warning: ${message}`);
|
|
594
|
+
console.warn("warning: the gateway plugin may stay idle until the first project-scoped request");
|
|
595
|
+
});
|
|
596
|
+
const exitCode = await waitForChild(child);
|
|
597
|
+
if (exitCode !== 0) {
|
|
598
|
+
process.exitCode = exitCode;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function waitForChild(child) {
|
|
602
|
+
return new Promise((resolve4, reject) => {
|
|
603
|
+
child.once("error", reject);
|
|
604
|
+
child.once("exit", (code, signal) => {
|
|
605
|
+
if (signal !== null) {
|
|
606
|
+
resolve4(1);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
resolve4(code ?? 0);
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// src/cli/warm.ts
|
|
615
|
+
import { mkdir as mkdir3 } from "node:fs/promises";
|
|
616
|
+
async function runWarm(options, env) {
|
|
617
|
+
const target = await resolveServeTarget(options, env);
|
|
618
|
+
await mkdir3(target.workspaceDirPath, { recursive: true });
|
|
619
|
+
await warmGatewayProject(target);
|
|
620
|
+
console.log(`gateway plugin warmed: ${target.serverOrigin}`);
|
|
621
|
+
console.log(`warm directory: ${target.workspaceDirPath}`);
|
|
470
622
|
}
|
|
471
623
|
|
|
472
624
|
// src/cli.ts
|
|
@@ -482,6 +634,12 @@ async function main() {
|
|
|
482
634
|
case "init":
|
|
483
635
|
await runInit(command, process.env);
|
|
484
636
|
return;
|
|
637
|
+
case "serve":
|
|
638
|
+
await runServe(command, process.env);
|
|
639
|
+
return;
|
|
640
|
+
case "warm":
|
|
641
|
+
await runWarm(command, process.env);
|
|
642
|
+
return;
|
|
485
643
|
}
|
|
486
644
|
}
|
|
487
645
|
main().catch((error) => {
|
package/dist/config/memory.d.ts
CHANGED
|
@@ -7,12 +7,13 @@ export type GatewayMemoryEntryConfig = {
|
|
|
7
7
|
displayPath: string;
|
|
8
8
|
description: string;
|
|
9
9
|
injectContent: boolean;
|
|
10
|
+
searchOnly: boolean;
|
|
10
11
|
} | {
|
|
11
12
|
kind: "directory";
|
|
12
13
|
path: string;
|
|
13
14
|
displayPath: string;
|
|
14
15
|
description: string;
|
|
15
|
-
injectMarkdownContents: boolean;
|
|
16
16
|
globs: string[];
|
|
17
|
+
searchOnly: boolean;
|
|
17
18
|
};
|
|
18
19
|
export declare function parseMemoryConfig(value: unknown, workspaceDirPath: string): Promise<GatewayMemoryConfig>;
|
package/dist/gateway.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { PluginInput } from "@opencode-ai/plugin";
|
|
|
2
2
|
import type { GatewayBindingModule, GatewayContract } from "./binding";
|
|
3
3
|
import { GatewayCronRuntime } from "./cron/runtime";
|
|
4
4
|
import { ChannelFileSender } from "./host/file-sender";
|
|
5
|
+
import { GatewayMemoryRuntime } from "./memory/runtime";
|
|
5
6
|
import { GatewayExecutor } from "./runtime/executor";
|
|
6
7
|
import { GatewaySessionContext } from "./session/context";
|
|
7
8
|
import { ChannelSessionSwitcher } from "./session/switcher";
|
|
@@ -29,7 +30,8 @@ export declare class GatewayPluginRuntime {
|
|
|
29
30
|
readonly channelSessions: ChannelSessionSwitcher;
|
|
30
31
|
readonly sessionContext: GatewaySessionContext;
|
|
31
32
|
readonly systemPrompts: GatewaySystemPromptBuilder;
|
|
32
|
-
|
|
33
|
+
readonly memory: GatewayMemoryRuntime;
|
|
34
|
+
constructor(contract: GatewayContract, executor: GatewayExecutor, cron: GatewayCronRuntime, telegram: GatewayTelegramRuntime, files: ChannelFileSender, channelSessions: ChannelSessionSwitcher, sessionContext: GatewaySessionContext, systemPrompts: GatewaySystemPromptBuilder, memory: GatewayMemoryRuntime);
|
|
33
35
|
status(): GatewayPluginStatus;
|
|
34
36
|
}
|
|
35
37
|
export declare function createGatewayRuntime(module: GatewayBindingModule, input: PluginInput): Promise<GatewayPluginRuntime>;
|
package/dist/index.js
CHANGED
|
@@ -16151,30 +16151,42 @@ async function readMemoryEntry(value, index, workspaceDirPath) {
|
|
|
16151
16151
|
throw new Error(`${field} must be a table`);
|
|
16152
16152
|
}
|
|
16153
16153
|
const entry = value;
|
|
16154
|
+
if (entry.inject_markdown_contents !== undefined) {
|
|
16155
|
+
throw new Error(`${field}.inject_markdown_contents has been removed; use globs for directory injection or search_only for on-demand access`);
|
|
16156
|
+
}
|
|
16154
16157
|
const displayPath = readRequiredString(entry.path, `${field}.path`);
|
|
16155
16158
|
const description = readRequiredString(entry.description, `${field}.description`);
|
|
16156
16159
|
const resolvedPath = resolve(workspaceDirPath, displayPath);
|
|
16157
16160
|
const metadata = await ensurePathMetadata(resolvedPath, displayPath, entry, `${field}.path`);
|
|
16161
|
+
const searchOnly = readBoolean2(entry.search_only, `${field}.search_only`, false);
|
|
16158
16162
|
if (metadata.isFile()) {
|
|
16159
|
-
ensureDirectoryOnlyFieldIsAbsent(entry.inject_markdown_contents, `${field}.inject_markdown_contents`);
|
|
16160
16163
|
ensureDirectoryOnlyFieldIsAbsent(entry.globs, `${field}.globs`);
|
|
16164
|
+
const injectContent = readBoolean2(entry.inject_content, `${field}.inject_content`, false);
|
|
16165
|
+
if (searchOnly && injectContent) {
|
|
16166
|
+
throw new Error(`${field} cannot enable both inject_content and search_only`);
|
|
16167
|
+
}
|
|
16161
16168
|
return {
|
|
16162
16169
|
kind: "file",
|
|
16163
16170
|
path: resolvedPath,
|
|
16164
16171
|
displayPath,
|
|
16165
16172
|
description,
|
|
16166
|
-
injectContent
|
|
16173
|
+
injectContent,
|
|
16174
|
+
searchOnly
|
|
16167
16175
|
};
|
|
16168
16176
|
}
|
|
16169
16177
|
if (metadata.isDirectory()) {
|
|
16170
16178
|
ensureFileOnlyFieldIsAbsent(entry.inject_content, `${field}.inject_content`);
|
|
16179
|
+
const globs = readGlobList(entry.globs, `${field}.globs`);
|
|
16180
|
+
if (searchOnly && globs.length > 0) {
|
|
16181
|
+
throw new Error(`${field} cannot enable both globs and search_only`);
|
|
16182
|
+
}
|
|
16171
16183
|
return {
|
|
16172
16184
|
kind: "directory",
|
|
16173
16185
|
path: resolvedPath,
|
|
16174
16186
|
displayPath,
|
|
16175
16187
|
description,
|
|
16176
|
-
|
|
16177
|
-
|
|
16188
|
+
globs,
|
|
16189
|
+
searchOnly
|
|
16178
16190
|
};
|
|
16179
16191
|
}
|
|
16180
16192
|
throw new Error(`${field}.path must point to a regular file or directory`);
|
|
@@ -16198,7 +16210,7 @@ function inferMissingEntryKind(displayPath, entry) {
|
|
|
16198
16210
|
if (entry.inject_content !== undefined) {
|
|
16199
16211
|
return "file";
|
|
16200
16212
|
}
|
|
16201
|
-
if (entry.
|
|
16213
|
+
if (entry.globs !== undefined) {
|
|
16202
16214
|
return "directory";
|
|
16203
16215
|
}
|
|
16204
16216
|
const trimmedPath = displayPath.trim();
|
|
@@ -17614,87 +17626,58 @@ class GatewayMailboxRouter {
|
|
|
17614
17626
|
}
|
|
17615
17627
|
}
|
|
17616
17628
|
|
|
17617
|
-
// src/memory/
|
|
17629
|
+
// src/memory/files.ts
|
|
17618
17630
|
var import_fast_glob = __toESM(require_out4(), 1);
|
|
17619
17631
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
17620
17632
|
import { extname as extname3, relative } from "node:path";
|
|
17621
|
-
var
|
|
17633
|
+
var ALL_FILES_GLOB = "**/*";
|
|
17622
17634
|
var UTF8_TEXT_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
17623
|
-
|
|
17624
|
-
class GatewayMemoryPromptProvider {
|
|
17625
|
-
config;
|
|
17626
|
-
logger;
|
|
17627
|
-
constructor(config, logger) {
|
|
17628
|
-
this.config = config;
|
|
17629
|
-
this.logger = logger;
|
|
17630
|
-
}
|
|
17631
|
-
async buildPrompt() {
|
|
17632
|
-
if (this.config.entries.length === 0) {
|
|
17633
|
-
return null;
|
|
17634
|
-
}
|
|
17635
|
-
const sections = await Promise.all(this.config.entries.map((entry) => this.buildEntrySection(entry)));
|
|
17636
|
-
return ["Gateway memory:", ...sections].join(`
|
|
17637
|
-
|
|
17638
|
-
`);
|
|
17639
|
-
}
|
|
17640
|
-
async buildEntrySection(entry) {
|
|
17641
|
-
const lines = [`Configured path: ${entry.displayPath}`, `Description: ${entry.description}`];
|
|
17642
|
-
const injectedFiles = await collectInjectedFiles(entry, this.logger);
|
|
17643
|
-
for (const file of injectedFiles) {
|
|
17644
|
-
lines.push("");
|
|
17645
|
-
lines.push(`File: ${file.displayPath}`);
|
|
17646
|
-
lines.push(codeFence(file.infoString, file.text));
|
|
17647
|
-
}
|
|
17648
|
-
return lines.join(`
|
|
17649
|
-
`);
|
|
17650
|
-
}
|
|
17651
|
-
}
|
|
17652
|
-
async function collectInjectedFiles(entry, logger) {
|
|
17635
|
+
async function collectInjectedMemoryFiles(entry, logger) {
|
|
17653
17636
|
if (entry.kind === "file") {
|
|
17654
|
-
if (!entry.injectContent) {
|
|
17637
|
+
if (entry.searchOnly || !entry.injectContent) {
|
|
17655
17638
|
return [];
|
|
17656
17639
|
}
|
|
17657
|
-
const text = await
|
|
17640
|
+
const text = await readMemoryTextFile(entry.path, logger);
|
|
17658
17641
|
if (text === null) {
|
|
17659
17642
|
return [];
|
|
17660
17643
|
}
|
|
17661
17644
|
return [
|
|
17662
17645
|
{
|
|
17646
|
+
path: entry.path,
|
|
17663
17647
|
displayPath: entry.displayPath,
|
|
17648
|
+
description: entry.description,
|
|
17664
17649
|
infoString: inferFenceInfoString(entry.path),
|
|
17665
17650
|
text
|
|
17666
17651
|
}
|
|
17667
17652
|
];
|
|
17668
17653
|
}
|
|
17669
|
-
|
|
17670
|
-
|
|
17671
|
-
for (const pattern of MARKDOWN_GLOBS) {
|
|
17672
|
-
addMatchingFiles(filePaths, entry.path, pattern);
|
|
17673
|
-
}
|
|
17674
|
-
}
|
|
17675
|
-
for (const pattern of entry.globs) {
|
|
17676
|
-
addMatchingFiles(filePaths, entry.path, pattern);
|
|
17654
|
+
if (entry.searchOnly || entry.globs.length === 0) {
|
|
17655
|
+
return [];
|
|
17677
17656
|
}
|
|
17678
|
-
|
|
17679
|
-
|
|
17680
|
-
|
|
17681
|
-
|
|
17657
|
+
return await readDirectoryFiles(entry, entry.globs, logger);
|
|
17658
|
+
}
|
|
17659
|
+
async function collectSearchableMemoryFiles(config, logger) {
|
|
17660
|
+
const files = [];
|
|
17661
|
+
for (const entry of config.entries) {
|
|
17662
|
+
if (entry.kind === "file") {
|
|
17663
|
+
const text = await readMemoryTextFile(entry.path, logger);
|
|
17664
|
+
if (text === null) {
|
|
17665
|
+
continue;
|
|
17666
|
+
}
|
|
17667
|
+
files.push({
|
|
17668
|
+
path: entry.path,
|
|
17669
|
+
displayPath: entry.displayPath,
|
|
17670
|
+
description: entry.description,
|
|
17671
|
+
infoString: inferFenceInfoString(entry.path),
|
|
17672
|
+
text
|
|
17673
|
+
});
|
|
17682
17674
|
continue;
|
|
17683
17675
|
}
|
|
17684
|
-
|
|
17685
|
-
displayPath: relativeDisplayPath(entry.path, entry.displayPath, filePath),
|
|
17686
|
-
infoString: inferFenceInfoString(filePath),
|
|
17687
|
-
text
|
|
17688
|
-
});
|
|
17676
|
+
files.push(...await readDirectoryFiles(entry, [ALL_FILES_GLOB], logger));
|
|
17689
17677
|
}
|
|
17690
|
-
return
|
|
17678
|
+
return files;
|
|
17691
17679
|
}
|
|
17692
|
-
function
|
|
17693
|
-
for (const match of import_fast_glob.globSync(pattern, { cwd, absolute: true, onlyFiles: true })) {
|
|
17694
|
-
result.add(match);
|
|
17695
|
-
}
|
|
17696
|
-
}
|
|
17697
|
-
async function readTextFile(path, logger) {
|
|
17680
|
+
async function readMemoryTextFile(path, logger) {
|
|
17698
17681
|
let bytes;
|
|
17699
17682
|
try {
|
|
17700
17683
|
bytes = await readFile3(path);
|
|
@@ -17715,6 +17698,37 @@ async function readTextFile(path, logger) {
|
|
|
17715
17698
|
}
|
|
17716
17699
|
return text;
|
|
17717
17700
|
}
|
|
17701
|
+
function codeFence(infoString, text) {
|
|
17702
|
+
const language = infoString.length === 0 ? "" : infoString;
|
|
17703
|
+
return [`\`\`\`${language}`, text, "```"].join(`
|
|
17704
|
+
`);
|
|
17705
|
+
}
|
|
17706
|
+
function addMatchingFiles(result, cwd, pattern) {
|
|
17707
|
+
for (const match of import_fast_glob.globSync(pattern, { cwd, absolute: true, onlyFiles: true, followSymbolicLinks: false })) {
|
|
17708
|
+
result.add(match);
|
|
17709
|
+
}
|
|
17710
|
+
}
|
|
17711
|
+
async function readDirectoryFiles(entry, patterns, logger) {
|
|
17712
|
+
const filePaths = new Set;
|
|
17713
|
+
for (const pattern of patterns) {
|
|
17714
|
+
addMatchingFiles(filePaths, entry.path, pattern);
|
|
17715
|
+
}
|
|
17716
|
+
const files = [];
|
|
17717
|
+
for (const filePath of [...filePaths].sort((left, right) => left.localeCompare(right))) {
|
|
17718
|
+
const text = await readMemoryTextFile(filePath, logger);
|
|
17719
|
+
if (text === null) {
|
|
17720
|
+
continue;
|
|
17721
|
+
}
|
|
17722
|
+
files.push({
|
|
17723
|
+
path: filePath,
|
|
17724
|
+
displayPath: relativeDisplayPath(entry.path, entry.displayPath, filePath),
|
|
17725
|
+
description: entry.description,
|
|
17726
|
+
infoString: inferFenceInfoString(filePath),
|
|
17727
|
+
text
|
|
17728
|
+
});
|
|
17729
|
+
}
|
|
17730
|
+
return files;
|
|
17731
|
+
}
|
|
17718
17732
|
function relativeDisplayPath(rootPath, rootDisplayPath, filePath) {
|
|
17719
17733
|
const suffix = relative(rootPath, filePath);
|
|
17720
17734
|
if (suffix.length === 0) {
|
|
@@ -17729,11 +17743,6 @@ function inferFenceInfoString(path) {
|
|
|
17729
17743
|
}
|
|
17730
17744
|
return extension2;
|
|
17731
17745
|
}
|
|
17732
|
-
function codeFence(infoString, text) {
|
|
17733
|
-
const language = infoString.length === 0 ? "" : infoString;
|
|
17734
|
-
return [`\`\`\`${language}`, text, "```"].join(`
|
|
17735
|
-
`);
|
|
17736
|
-
}
|
|
17737
17746
|
function formatError2(error) {
|
|
17738
17747
|
if (error instanceof Error && error.message.trim().length > 0) {
|
|
17739
17748
|
return error.message;
|
|
@@ -17741,6 +17750,218 @@ function formatError2(error) {
|
|
|
17741
17750
|
return String(error);
|
|
17742
17751
|
}
|
|
17743
17752
|
|
|
17753
|
+
// src/memory/prompt.ts
|
|
17754
|
+
class GatewayMemoryPromptProvider {
|
|
17755
|
+
config;
|
|
17756
|
+
logger;
|
|
17757
|
+
constructor(config, logger) {
|
|
17758
|
+
this.config = config;
|
|
17759
|
+
this.logger = logger;
|
|
17760
|
+
}
|
|
17761
|
+
async buildPrompt() {
|
|
17762
|
+
if (this.config.entries.length === 0) {
|
|
17763
|
+
return null;
|
|
17764
|
+
}
|
|
17765
|
+
const sections = await Promise.all(this.config.entries.map((entry) => this.buildEntrySection(entry)));
|
|
17766
|
+
return ["Gateway memory:", ...sections].join(`
|
|
17767
|
+
|
|
17768
|
+
`);
|
|
17769
|
+
}
|
|
17770
|
+
async buildEntrySection(entry) {
|
|
17771
|
+
const lines = [
|
|
17772
|
+
`Configured path: ${entry.displayPath}`,
|
|
17773
|
+
`Description: ${entry.description}`,
|
|
17774
|
+
`Access: ${describeMemoryAccess(entry)}`
|
|
17775
|
+
];
|
|
17776
|
+
const injectedFiles = await collectInjectedMemoryFiles(entry, this.logger);
|
|
17777
|
+
if (entry.kind === "directory" && entry.globs.length > 0 && !entry.searchOnly) {
|
|
17778
|
+
lines.push(`Auto-injected globs: ${entry.globs.join(", ")}`);
|
|
17779
|
+
}
|
|
17780
|
+
for (const file of injectedFiles) {
|
|
17781
|
+
lines.push("");
|
|
17782
|
+
lines.push(`File: ${file.displayPath}`);
|
|
17783
|
+
lines.push(codeFence(file.infoString, file.text));
|
|
17784
|
+
}
|
|
17785
|
+
return lines.join(`
|
|
17786
|
+
`);
|
|
17787
|
+
}
|
|
17788
|
+
}
|
|
17789
|
+
function describeMemoryAccess(entry) {
|
|
17790
|
+
if (entry.kind === "file") {
|
|
17791
|
+
if (entry.injectContent && !entry.searchOnly) {
|
|
17792
|
+
return "auto-injected; use memory_search or memory_get for targeted follow-up";
|
|
17793
|
+
}
|
|
17794
|
+
return "search-only; use memory_search or memory_get when this file is relevant";
|
|
17795
|
+
}
|
|
17796
|
+
if (entry.searchOnly) {
|
|
17797
|
+
return "search-only; all UTF-8 text files under this directory are available via memory_search or memory_get";
|
|
17798
|
+
}
|
|
17799
|
+
if (entry.globs.length > 0) {
|
|
17800
|
+
return "globs are auto-injected; all UTF-8 text files under this directory remain available via memory_search or memory_get";
|
|
17801
|
+
}
|
|
17802
|
+
return "search-only by default; use memory_search or memory_get when this directory is relevant";
|
|
17803
|
+
}
|
|
17804
|
+
|
|
17805
|
+
// src/memory/runtime.ts
|
|
17806
|
+
import { dirname as dirname3 } from "node:path";
|
|
17807
|
+
import { FileFinder } from "@ff-labs/fff-node";
|
|
17808
|
+
var DEFAULT_SEARCH_LIMIT = 5;
|
|
17809
|
+
var MAX_SEARCH_LIMIT = 20;
|
|
17810
|
+
var DEFAULT_GET_MAX_LINES = 200;
|
|
17811
|
+
var MAX_GET_MAX_LINES = 500;
|
|
17812
|
+
var SEARCH_CONTEXT_RADIUS = 1;
|
|
17813
|
+
var SEARCH_SCAN_TIMEOUT_MS = 5000;
|
|
17814
|
+
|
|
17815
|
+
class GatewayMemoryRuntime {
|
|
17816
|
+
config;
|
|
17817
|
+
logger;
|
|
17818
|
+
finderCache = new Map;
|
|
17819
|
+
constructor(config, logger) {
|
|
17820
|
+
this.config = config;
|
|
17821
|
+
this.logger = logger;
|
|
17822
|
+
}
|
|
17823
|
+
hasEntries() {
|
|
17824
|
+
return this.config.entries.length > 0;
|
|
17825
|
+
}
|
|
17826
|
+
async search(query, limit = DEFAULT_SEARCH_LIMIT) {
|
|
17827
|
+
const normalizedQuery = normalizeRequiredString(query, "query");
|
|
17828
|
+
const normalizedLimit = normalizePositiveInteger(limit, "limit", MAX_SEARCH_LIMIT);
|
|
17829
|
+
const searchableFiles = await collectSearchableMemoryFiles(this.config, this.logger);
|
|
17830
|
+
const searchableFilesByPath = new Map(searchableFiles.map((file) => [file.path, file]));
|
|
17831
|
+
const results = [];
|
|
17832
|
+
for (const entry of this.config.entries) {
|
|
17833
|
+
const finder = await this.getFinder(searchRootForEntry(entry));
|
|
17834
|
+
const grep = finder.grep(normalizedQuery, {
|
|
17835
|
+
mode: "plain",
|
|
17836
|
+
beforeContext: SEARCH_CONTEXT_RADIUS,
|
|
17837
|
+
afterContext: SEARCH_CONTEXT_RADIUS
|
|
17838
|
+
});
|
|
17839
|
+
if (!grep.ok) {
|
|
17840
|
+
throw new Error(`memory search failed for ${entry.displayPath}: ${grep.error}`);
|
|
17841
|
+
}
|
|
17842
|
+
for (const match of grep.value.items) {
|
|
17843
|
+
if (entry.kind === "file" && match.path !== entry.path) {
|
|
17844
|
+
continue;
|
|
17845
|
+
}
|
|
17846
|
+
const searchableFile = searchableFilesByPath.get(match.path);
|
|
17847
|
+
if (searchableFile === undefined) {
|
|
17848
|
+
continue;
|
|
17849
|
+
}
|
|
17850
|
+
const window2 = readSnippetWindow(searchableFile.text, match.lineNumber, SEARCH_CONTEXT_RADIUS);
|
|
17851
|
+
results.push({
|
|
17852
|
+
path: displayPathForMatch(entry, match.relativePath),
|
|
17853
|
+
description: entry.description,
|
|
17854
|
+
lineStart: window2.lineStart,
|
|
17855
|
+
lineEnd: window2.lineEnd,
|
|
17856
|
+
snippet: window2.text,
|
|
17857
|
+
infoString: searchableFile.infoString
|
|
17858
|
+
});
|
|
17859
|
+
if (results.length >= normalizedLimit) {
|
|
17860
|
+
return results;
|
|
17861
|
+
}
|
|
17862
|
+
}
|
|
17863
|
+
}
|
|
17864
|
+
return results;
|
|
17865
|
+
}
|
|
17866
|
+
async get(path, startLine = 1, maxLines = DEFAULT_GET_MAX_LINES) {
|
|
17867
|
+
const normalizedPath = normalizeRequiredString(path, "path");
|
|
17868
|
+
const normalizedStartLine = normalizePositiveInteger(startLine, "start_line");
|
|
17869
|
+
const normalizedMaxLines = normalizePositiveInteger(maxLines, "max_lines", MAX_GET_MAX_LINES);
|
|
17870
|
+
const files = await collectSearchableMemoryFiles(this.config, this.logger);
|
|
17871
|
+
const matches = files.filter((file2) => file2.displayPath === normalizedPath);
|
|
17872
|
+
if (matches.length === 0) {
|
|
17873
|
+
throw new Error(`memory path was not found: ${normalizedPath}`);
|
|
17874
|
+
}
|
|
17875
|
+
if (matches.length > 1) {
|
|
17876
|
+
throw new Error(`memory path is ambiguous: ${normalizedPath}`);
|
|
17877
|
+
}
|
|
17878
|
+
const file = matches[0];
|
|
17879
|
+
const lines = splitLines(file.text);
|
|
17880
|
+
if (normalizedStartLine > lines.length) {
|
|
17881
|
+
throw new Error(`start_line ${normalizedStartLine} exceeds the file length of ${lines.length} line(s) for ${normalizedPath}`);
|
|
17882
|
+
}
|
|
17883
|
+
const startIndex = normalizedStartLine - 1;
|
|
17884
|
+
const window2 = lines.slice(startIndex, startIndex + normalizedMaxLines);
|
|
17885
|
+
return {
|
|
17886
|
+
path: file.displayPath,
|
|
17887
|
+
description: file.description,
|
|
17888
|
+
lineStart: normalizedStartLine,
|
|
17889
|
+
lineEnd: startIndex + window2.length,
|
|
17890
|
+
text: window2.join(`
|
|
17891
|
+
`),
|
|
17892
|
+
infoString: file.infoString
|
|
17893
|
+
};
|
|
17894
|
+
}
|
|
17895
|
+
async getFinder(rootPath) {
|
|
17896
|
+
const cached = this.finderCache.get(rootPath);
|
|
17897
|
+
if (cached !== undefined && !cached.isDestroyed) {
|
|
17898
|
+
return cached;
|
|
17899
|
+
}
|
|
17900
|
+
const created = FileFinder.create({
|
|
17901
|
+
basePath: rootPath,
|
|
17902
|
+
aiMode: true
|
|
17903
|
+
});
|
|
17904
|
+
if (!created.ok) {
|
|
17905
|
+
throw new Error(`could not initialize memory search index for ${rootPath}: ${created.error}`);
|
|
17906
|
+
}
|
|
17907
|
+
const finder = created.value;
|
|
17908
|
+
const ready = await finder.waitForScan(SEARCH_SCAN_TIMEOUT_MS);
|
|
17909
|
+
if (!ready.ok) {
|
|
17910
|
+
finder.destroy();
|
|
17911
|
+
throw new Error(`memory search index failed while waiting for scan: ${ready.error}`);
|
|
17912
|
+
}
|
|
17913
|
+
if (!ready.value) {
|
|
17914
|
+
this.logger.log("warn", `memory search scan is still warming after ${SEARCH_SCAN_TIMEOUT_MS}ms: ${rootPath}`);
|
|
17915
|
+
}
|
|
17916
|
+
this.finderCache.set(rootPath, finder);
|
|
17917
|
+
return finder;
|
|
17918
|
+
}
|
|
17919
|
+
}
|
|
17920
|
+
function searchRootForEntry(entry) {
|
|
17921
|
+
return entry.kind === "file" ? dirname3(entry.path) : entry.path;
|
|
17922
|
+
}
|
|
17923
|
+
function displayPathForMatch(entry, relativePath) {
|
|
17924
|
+
if (entry.kind === "file") {
|
|
17925
|
+
return entry.displayPath;
|
|
17926
|
+
}
|
|
17927
|
+
const normalizedRelativePath = relativePath.replaceAll("\\", "/");
|
|
17928
|
+
return normalizedRelativePath.length === 0 ? entry.displayPath : `${entry.displayPath}/${normalizedRelativePath}`;
|
|
17929
|
+
}
|
|
17930
|
+
function splitLines(text) {
|
|
17931
|
+
return text.split(/\r?\n/);
|
|
17932
|
+
}
|
|
17933
|
+
function readSnippetWindow(text, lineNumber, contextRadius) {
|
|
17934
|
+
const lines = splitLines(text);
|
|
17935
|
+
const matchIndex = Math.min(Math.max(lineNumber - 1, 0), Math.max(lines.length - 1, 0));
|
|
17936
|
+
const startIndex = Math.max(0, matchIndex - contextRadius);
|
|
17937
|
+
const endIndex = Math.min(lines.length - 1, matchIndex + contextRadius);
|
|
17938
|
+
return {
|
|
17939
|
+
lineStart: startIndex + 1,
|
|
17940
|
+
lineEnd: endIndex + 1,
|
|
17941
|
+
text: lines.slice(startIndex, endIndex + 1).join(`
|
|
17942
|
+
`)
|
|
17943
|
+
};
|
|
17944
|
+
}
|
|
17945
|
+
function normalizePositiveInteger(value, field, maxValue) {
|
|
17946
|
+
if (!Number.isSafeInteger(value)) {
|
|
17947
|
+
throw new Error(`${field} must be an integer`);
|
|
17948
|
+
}
|
|
17949
|
+
if (value <= 0) {
|
|
17950
|
+
throw new Error(`${field} must be greater than 0`);
|
|
17951
|
+
}
|
|
17952
|
+
if (maxValue !== undefined && value > maxValue) {
|
|
17953
|
+
throw new Error(`${field} must be less than or equal to ${maxValue}`);
|
|
17954
|
+
}
|
|
17955
|
+
return value;
|
|
17956
|
+
}
|
|
17957
|
+
function normalizeRequiredString(value, field) {
|
|
17958
|
+
const trimmed = value.trim();
|
|
17959
|
+
if (trimmed.length === 0) {
|
|
17960
|
+
throw new Error(`${field} must not be empty`);
|
|
17961
|
+
}
|
|
17962
|
+
return trimmed;
|
|
17963
|
+
}
|
|
17964
|
+
|
|
17744
17965
|
// src/opencode/adapter.ts
|
|
17745
17966
|
import { basename as basename2 } from "node:path";
|
|
17746
17967
|
import { pathToFileURL } from "node:url";
|
|
@@ -17754,7 +17975,9 @@ function delay(durationMs) {
|
|
|
17754
17975
|
|
|
17755
17976
|
// src/opencode/adapter.ts
|
|
17756
17977
|
var SESSION_IDLE_POLL_MS = 250;
|
|
17757
|
-
var
|
|
17978
|
+
var PROMPT_RESPONSE_PROGRESS_TIMEOUT_MS = 90000;
|
|
17979
|
+
var PROMPT_RESPONSE_MAX_TIMEOUT_MS = 10 * 60000;
|
|
17980
|
+
var PROMPT_RESPONSE_SETTLE_MS = 1000;
|
|
17758
17981
|
|
|
17759
17982
|
class OpencodeSdkAdapter {
|
|
17760
17983
|
client;
|
|
@@ -17900,8 +18123,12 @@ class OpencodeSdkAdapter {
|
|
|
17900
18123
|
};
|
|
17901
18124
|
}
|
|
17902
18125
|
async awaitPromptResponse(command) {
|
|
17903
|
-
const
|
|
18126
|
+
const startedAtMs = Date.now();
|
|
18127
|
+
const maxDeadline = startedAtMs + PROMPT_RESPONSE_MAX_TIMEOUT_MS;
|
|
18128
|
+
let progressDeadline = startedAtMs + PROMPT_RESPONSE_PROGRESS_TIMEOUT_MS;
|
|
17904
18129
|
let stableCandidateKey = null;
|
|
18130
|
+
let stableCandidateSinceMs = null;
|
|
18131
|
+
let progressKey = null;
|
|
17905
18132
|
for (;; ) {
|
|
17906
18133
|
const messages = await this.client.session.messages({
|
|
17907
18134
|
path: { id: command.sessionId },
|
|
@@ -17912,22 +18139,32 @@ class OpencodeSdkAdapter {
|
|
|
17912
18139
|
responseStyle: "data",
|
|
17913
18140
|
throwOnError: true
|
|
17914
18141
|
});
|
|
17915
|
-
const
|
|
18142
|
+
const assistantChildren = listAssistantResponses(unwrapData(messages), command.messageId);
|
|
18143
|
+
const nextProgressKey = createAssistantProgressKey(assistantChildren);
|
|
18144
|
+
const now = Date.now();
|
|
18145
|
+
if (progressKey !== nextProgressKey) {
|
|
18146
|
+
progressKey = nextProgressKey;
|
|
18147
|
+
progressDeadline = now + PROMPT_RESPONSE_PROGRESS_TIMEOUT_MS;
|
|
18148
|
+
}
|
|
18149
|
+
const response = selectAssistantResponse(assistantChildren);
|
|
17916
18150
|
if (response !== null) {
|
|
17917
18151
|
const candidateKey = createAssistantCandidateKey(response);
|
|
17918
18152
|
if (stableCandidateKey === candidateKey) {
|
|
17919
|
-
|
|
17920
|
-
|
|
17921
|
-
|
|
17922
|
-
|
|
17923
|
-
|
|
17924
|
-
|
|
18153
|
+
if (stableCandidateSinceMs !== null && now - stableCandidateSinceMs >= PROMPT_RESPONSE_SETTLE_MS) {
|
|
18154
|
+
return toAwaitPromptResponseResult(command.sessionId, response);
|
|
18155
|
+
}
|
|
18156
|
+
} else {
|
|
18157
|
+
stableCandidateKey = candidateKey;
|
|
18158
|
+
stableCandidateSinceMs = now;
|
|
17925
18159
|
}
|
|
17926
|
-
stableCandidateKey = candidateKey;
|
|
17927
18160
|
} else {
|
|
17928
18161
|
stableCandidateKey = null;
|
|
18162
|
+
stableCandidateSinceMs = null;
|
|
17929
18163
|
}
|
|
17930
|
-
if (
|
|
18164
|
+
if (now >= progressDeadline || now >= maxDeadline) {
|
|
18165
|
+
if (response !== null) {
|
|
18166
|
+
return toAwaitPromptResponseResult(command.sessionId, response);
|
|
18167
|
+
}
|
|
17931
18168
|
throw new Error(`assistant message for prompt ${command.messageId} is unavailable after prompt completion`);
|
|
17932
18169
|
}
|
|
17933
18170
|
await delay(SESSION_IDLE_POLL_MS);
|
|
@@ -17988,8 +18225,10 @@ function toSessionPromptPart(part) {
|
|
|
17988
18225
|
};
|
|
17989
18226
|
}
|
|
17990
18227
|
}
|
|
17991
|
-
function
|
|
17992
|
-
|
|
18228
|
+
function listAssistantResponses(messages, userMessageId) {
|
|
18229
|
+
return messages.filter(isAssistantChildMessage(userMessageId));
|
|
18230
|
+
}
|
|
18231
|
+
function selectAssistantResponse(assistantChildren) {
|
|
17993
18232
|
for (let index = assistantChildren.length - 1;index >= 0; index -= 1) {
|
|
17994
18233
|
const candidate = assistantChildren[index];
|
|
17995
18234
|
if (hasVisibleText(candidate)) {
|
|
@@ -18004,6 +18243,9 @@ function selectAssistantResponse(messages, userMessageId) {
|
|
|
18004
18243
|
}
|
|
18005
18244
|
return null;
|
|
18006
18245
|
}
|
|
18246
|
+
function createAssistantProgressKey(messages) {
|
|
18247
|
+
return JSON.stringify(messages.map(createAssistantCandidateKey));
|
|
18248
|
+
}
|
|
18007
18249
|
function createAssistantCandidateKey(message) {
|
|
18008
18250
|
return JSON.stringify({
|
|
18009
18251
|
messageId: message.info.id,
|
|
@@ -18020,6 +18262,14 @@ function createAssistantCandidateKey(message) {
|
|
|
18020
18262
|
function isAssistantChildMessage(userMessageId) {
|
|
18021
18263
|
return (message) => message.info?.role === "assistant" && message.info.parentID === userMessageId;
|
|
18022
18264
|
}
|
|
18265
|
+
function toAwaitPromptResponseResult(sessionId, message) {
|
|
18266
|
+
return {
|
|
18267
|
+
kind: "awaitPromptResponse",
|
|
18268
|
+
sessionId,
|
|
18269
|
+
messageId: message.info.id,
|
|
18270
|
+
parts: message.parts.flatMap(toBindingMessagePart)
|
|
18271
|
+
};
|
|
18272
|
+
}
|
|
18023
18273
|
function toBindingMessagePart(part) {
|
|
18024
18274
|
if (typeof part.id !== "string" || typeof part.messageID !== "string" || part.type.length === 0) {
|
|
18025
18275
|
return [];
|
|
@@ -22463,7 +22713,7 @@ function buildGatewayContextPrompt(targets) {
|
|
|
22463
22713
|
|
|
22464
22714
|
// src/store/sqlite.ts
|
|
22465
22715
|
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
22466
|
-
import { dirname as
|
|
22716
|
+
import { dirname as dirname4 } from "node:path";
|
|
22467
22717
|
|
|
22468
22718
|
// src/store/migrations.ts
|
|
22469
22719
|
var LATEST_SCHEMA_VERSION = 7;
|
|
@@ -23377,7 +23627,7 @@ function readBooleanField(value, field, fallback) {
|
|
|
23377
23627
|
return raw;
|
|
23378
23628
|
}
|
|
23379
23629
|
async function openSqliteStore(path) {
|
|
23380
|
-
await mkdir2(
|
|
23630
|
+
await mkdir2(dirname4(path), { recursive: true });
|
|
23381
23631
|
const { openRuntimeSqliteDatabase: openRuntimeSqliteDatabase2 } = await Promise.resolve().then(() => (init_database(), exports_database));
|
|
23382
23632
|
const db2 = await openRuntimeSqliteDatabase2(path);
|
|
23383
23633
|
migrateGatewayDatabase(db2);
|
|
@@ -24107,7 +24357,8 @@ class GatewayPluginRuntime {
|
|
|
24107
24357
|
channelSessions;
|
|
24108
24358
|
sessionContext;
|
|
24109
24359
|
systemPrompts;
|
|
24110
|
-
|
|
24360
|
+
memory;
|
|
24361
|
+
constructor(contract, executor, cron, telegram, files, channelSessions, sessionContext, systemPrompts, memory) {
|
|
24111
24362
|
this.contract = contract;
|
|
24112
24363
|
this.executor = executor;
|
|
24113
24364
|
this.cron = cron;
|
|
@@ -24116,6 +24367,7 @@ class GatewayPluginRuntime {
|
|
|
24116
24367
|
this.channelSessions = channelSessions;
|
|
24117
24368
|
this.sessionContext = sessionContext;
|
|
24118
24369
|
this.systemPrompts = systemPrompts;
|
|
24370
|
+
this.memory = memory;
|
|
24119
24371
|
}
|
|
24120
24372
|
status() {
|
|
24121
24373
|
const rustStatus = this.contract.gatewayStatus();
|
|
@@ -24146,6 +24398,7 @@ async function createGatewayRuntime(module, input) {
|
|
|
24146
24398
|
const effectiveCronTimeZone = resolveEffectiveCronTimeZone(module, config);
|
|
24147
24399
|
const store = await openSqliteStore(config.stateDbPath);
|
|
24148
24400
|
const sessionContext = new GatewaySessionContext(store);
|
|
24401
|
+
const memory = new GatewayMemoryRuntime(config.memory, logger);
|
|
24149
24402
|
const memoryPrompts = new GatewayMemoryPromptProvider(config.memory, logger);
|
|
24150
24403
|
const systemPrompts = new GatewaySystemPromptBuilder(sessionContext, memoryPrompts);
|
|
24151
24404
|
const telegramClient = config.telegram.enabled ? new TelegramBotClient(config.telegram.botToken) : null;
|
|
@@ -24170,7 +24423,7 @@ async function createGatewayRuntime(module, input) {
|
|
|
24170
24423
|
cron.start();
|
|
24171
24424
|
mailbox.start();
|
|
24172
24425
|
telegram.start();
|
|
24173
|
-
return new GatewayPluginRuntime(module, executor, cron, telegram, files, channelSessions, sessionContext, systemPrompts);
|
|
24426
|
+
return new GatewayPluginRuntime(module, executor, cron, telegram, files, channelSessions, sessionContext, systemPrompts, memory);
|
|
24174
24427
|
});
|
|
24175
24428
|
}
|
|
24176
24429
|
function resolveEffectiveCronTimeZone(module, config) {
|
|
@@ -36732,6 +36985,63 @@ function formatGatewayStatus(status) {
|
|
|
36732
36985
|
`);
|
|
36733
36986
|
}
|
|
36734
36987
|
|
|
36988
|
+
// src/tools/memory-get.ts
|
|
36989
|
+
function createMemoryGetTool(runtime) {
|
|
36990
|
+
return tool({
|
|
36991
|
+
description: "Read a configured gateway memory file by path. Use the path returned by memory_search or a configured file path.",
|
|
36992
|
+
args: {
|
|
36993
|
+
path: tool.schema.string().min(1),
|
|
36994
|
+
start_line: tool.schema.number().optional(),
|
|
36995
|
+
max_lines: tool.schema.number().optional()
|
|
36996
|
+
},
|
|
36997
|
+
async execute(args) {
|
|
36998
|
+
return formatMemoryGetResult(await runtime.get(args.path, args.start_line, args.max_lines));
|
|
36999
|
+
}
|
|
37000
|
+
});
|
|
37001
|
+
}
|
|
37002
|
+
function formatMemoryGetResult(result) {
|
|
37003
|
+
return [
|
|
37004
|
+
`path=${result.path}`,
|
|
37005
|
+
`description=${result.description}`,
|
|
37006
|
+
`line_start=${result.lineStart}`,
|
|
37007
|
+
`line_end=${result.lineEnd}`,
|
|
37008
|
+
"content:",
|
|
37009
|
+
codeFence(result.infoString, result.text)
|
|
37010
|
+
].join(`
|
|
37011
|
+
`);
|
|
37012
|
+
}
|
|
37013
|
+
|
|
37014
|
+
// src/tools/memory-search.ts
|
|
37015
|
+
function createMemorySearchTool(runtime) {
|
|
37016
|
+
return tool({
|
|
37017
|
+
description: "Search configured gateway memory files and directories. Returns matching snippets and paths that can be read in more detail with memory_get.",
|
|
37018
|
+
args: {
|
|
37019
|
+
query: tool.schema.string().min(1),
|
|
37020
|
+
limit: tool.schema.number().optional()
|
|
37021
|
+
},
|
|
37022
|
+
async execute(args) {
|
|
37023
|
+
const results = await runtime.search(args.query, args.limit);
|
|
37024
|
+
if (results.length === 0) {
|
|
37025
|
+
return "no memory matches";
|
|
37026
|
+
}
|
|
37027
|
+
return results.map((result, index) => formatMemorySearchResult(result, index + 1)).join(`
|
|
37028
|
+
|
|
37029
|
+
`);
|
|
37030
|
+
}
|
|
37031
|
+
});
|
|
37032
|
+
}
|
|
37033
|
+
function formatMemorySearchResult(result, ordinal) {
|
|
37034
|
+
return [
|
|
37035
|
+
`result[${ordinal}].path=${result.path}`,
|
|
37036
|
+
`result[${ordinal}].description=${result.description}`,
|
|
37037
|
+
`result[${ordinal}].line_start=${result.lineStart}`,
|
|
37038
|
+
`result[${ordinal}].line_end=${result.lineEnd}`,
|
|
37039
|
+
`result[${ordinal}].snippet:`,
|
|
37040
|
+
codeFence(result.infoString, result.snippet)
|
|
37041
|
+
].join(`
|
|
37042
|
+
`);
|
|
37043
|
+
}
|
|
37044
|
+
|
|
36735
37045
|
// src/tools/schedule-cancel.ts
|
|
36736
37046
|
function createScheduleCancelTool(runtime) {
|
|
36737
37047
|
return tool({
|
|
@@ -36972,6 +37282,10 @@ var OpencodeGatewayPlugin = async (input) => {
|
|
|
36972
37282
|
schedule_once: createScheduleOnceTool(runtime.cron, runtime.sessionContext),
|
|
36973
37283
|
schedule_status: createScheduleStatusTool(runtime.cron)
|
|
36974
37284
|
};
|
|
37285
|
+
if (runtime.memory.hasEntries()) {
|
|
37286
|
+
tools.memory_search = createMemorySearchTool(runtime.memory);
|
|
37287
|
+
tools.memory_get = createMemoryGetTool(runtime.memory);
|
|
37288
|
+
}
|
|
36975
37289
|
if (runtime.files.hasEnabledChannel()) {
|
|
36976
37290
|
tools.channel_send_file = createChannelSendFileTool(runtime.files, runtime.sessionContext);
|
|
36977
37291
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BindingLoggerHost } from "../binding";
|
|
2
|
+
import type { GatewayMemoryConfig, GatewayMemoryEntryConfig } from "../config/memory";
|
|
3
|
+
export type SearchableMemoryFile = {
|
|
4
|
+
path: string;
|
|
5
|
+
displayPath: string;
|
|
6
|
+
description: string;
|
|
7
|
+
infoString: string;
|
|
8
|
+
text: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function collectInjectedMemoryFiles(entry: GatewayMemoryEntryConfig, logger: Pick<BindingLoggerHost, "log">): Promise<SearchableMemoryFile[]>;
|
|
11
|
+
export declare function collectSearchableMemoryFiles(config: GatewayMemoryConfig, logger: Pick<BindingLoggerHost, "log">): Promise<SearchableMemoryFile[]>;
|
|
12
|
+
export declare function readMemoryTextFile(path: string, logger: Pick<BindingLoggerHost, "log">): Promise<string | null>;
|
|
13
|
+
export declare function codeFence(infoString: string, text: string): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { BindingLoggerHost } from "../binding";
|
|
2
|
+
import type { GatewayMemoryConfig } from "../config/memory";
|
|
3
|
+
export type MemorySearchResult = {
|
|
4
|
+
path: string;
|
|
5
|
+
description: string;
|
|
6
|
+
lineStart: number;
|
|
7
|
+
lineEnd: number;
|
|
8
|
+
snippet: string;
|
|
9
|
+
infoString: string;
|
|
10
|
+
};
|
|
11
|
+
export type MemoryGetResult = {
|
|
12
|
+
path: string;
|
|
13
|
+
description: string;
|
|
14
|
+
lineStart: number;
|
|
15
|
+
lineEnd: number;
|
|
16
|
+
text: string;
|
|
17
|
+
infoString: string;
|
|
18
|
+
};
|
|
19
|
+
export declare class GatewayMemoryRuntime {
|
|
20
|
+
private readonly config;
|
|
21
|
+
private readonly logger;
|
|
22
|
+
private readonly finderCache;
|
|
23
|
+
constructor(config: GatewayMemoryConfig, logger: Pick<BindingLoggerHost, "log">);
|
|
24
|
+
hasEntries(): boolean;
|
|
25
|
+
search(query: string, limit?: number): Promise<MemorySearchResult[]>;
|
|
26
|
+
get(path: string, startLine?: number, maxLines?: number): Promise<MemoryGetResult>;
|
|
27
|
+
private getFinder;
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-gateway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Gateway plugin for OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"access": "public"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
+
"@ff-labs/fff-node": "^0.4.2",
|
|
55
56
|
"@opencode-ai/plugin": "~1.3.0",
|
|
56
57
|
"@opencode-ai/sdk": "~1.3.0",
|
|
57
58
|
"better-sqlite3": "^12.8.0",
|