opencode-gateway 0.2.8 → 0.2.10
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 +40 -4
- 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 +213 -50
- package/dist/index.js +43 -14
- package/package.json +1 -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
|
|
@@ -51,6 +61,17 @@ opencode serve
|
|
|
51
61
|
state_db = "/home/you/.local/share/opencode-gateway/state.db"
|
|
52
62
|
# log_level = "warn"
|
|
53
63
|
|
|
64
|
+
# Optional mailbox batching and route overrides.
|
|
65
|
+
# [gateway.mailbox]
|
|
66
|
+
# batch_replies = false
|
|
67
|
+
# batch_window_ms = 1500
|
|
68
|
+
#
|
|
69
|
+
# [[gateway.mailbox.routes]]
|
|
70
|
+
# channel = "telegram"
|
|
71
|
+
# target = "6212645712"
|
|
72
|
+
# topic = "12345"
|
|
73
|
+
# mailbox_key = "shared:telegram:dev"
|
|
74
|
+
|
|
54
75
|
[cron]
|
|
55
76
|
enabled = true
|
|
56
77
|
tick_seconds = 5
|
|
@@ -60,9 +81,15 @@ max_concurrent_runs = 1
|
|
|
60
81
|
enabled = false
|
|
61
82
|
bot_token_env = "TELEGRAM_BOT_TOKEN"
|
|
62
83
|
poll_timeout_seconds = 25
|
|
84
|
+
# Configure at least one allowlist when Telegram is enabled.
|
|
63
85
|
allowed_chats = []
|
|
64
86
|
allowed_users = []
|
|
65
87
|
|
|
88
|
+
[[memory.entries]]
|
|
89
|
+
path = "USER.md"
|
|
90
|
+
description = "Persistent user profile and preference memory. Keep this file accurate and concise. Record stable preferences, communication style, workflow habits, project conventions, tool constraints, review expectations, and other recurring facts that should shape future assistance. Update it proactively when you learn something durable about the user. Do not store one-off task details or transient context here."
|
|
91
|
+
inject_content = true
|
|
92
|
+
|
|
66
93
|
[[memory.entries]]
|
|
67
94
|
path = "memory/project.md"
|
|
68
95
|
description = "Project conventions and long-lived context"
|
|
@@ -85,6 +112,13 @@ export TELEGRAM_BOT_TOKEN="..."
|
|
|
85
112
|
Gateway plugin logs are off by default. Set `gateway.log_level` to `error`,
|
|
86
113
|
`warn`, `info`, or `debug` to emit that level and anything above it.
|
|
87
114
|
|
|
115
|
+
Mailbox rules:
|
|
116
|
+
|
|
117
|
+
- `gateway.mailbox.batch_replies` defaults to `false`
|
|
118
|
+
- `gateway.mailbox.batch_window_ms` defaults to `1500`
|
|
119
|
+
- `gateway.mailbox.routes` lets multiple ingress targets share one logical mailbox/session
|
|
120
|
+
- each route needs `channel`, `target`, optional `topic`, and a `mailbox_key`
|
|
121
|
+
|
|
88
122
|
Memory rules:
|
|
89
123
|
|
|
90
124
|
- all entries inject their configured path and description
|
|
@@ -95,4 +129,6 @@ Memory rules:
|
|
|
95
129
|
text files
|
|
96
130
|
- relative paths are resolved from `opencode-gateway-workspace`
|
|
97
131
|
- absolute paths are still allowed
|
|
132
|
+
- missing files and directories are created automatically on load
|
|
133
|
+
- the default template includes `USER.md` as persistent user-profile memory
|
|
98
134
|
- memory is injected only into gateway-managed sessions
|
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) {
|
|
@@ -387,6 +482,18 @@ function buildGatewayConfigTemplate(stateDbPath) {
|
|
|
387
482
|
`state_db = "${escapeTomlString(stateDbPath)}"`,
|
|
388
483
|
'# log_level = "warn"',
|
|
389
484
|
"",
|
|
485
|
+
"# Optional mailbox settings.",
|
|
486
|
+
"# [gateway.mailbox]",
|
|
487
|
+
"# batch_replies = false",
|
|
488
|
+
"# batch_window_ms = 1500",
|
|
489
|
+
"#",
|
|
490
|
+
"# Optional route overrides. Matching ingress targets can share one mailbox/session.",
|
|
491
|
+
"# [[gateway.mailbox.routes]]",
|
|
492
|
+
'# channel = "telegram"',
|
|
493
|
+
'# target = "6212645712"',
|
|
494
|
+
'# topic = "12345"',
|
|
495
|
+
'# mailbox_key = "shared:telegram:dev"',
|
|
496
|
+
"",
|
|
390
497
|
"[cron]",
|
|
391
498
|
"enabled = true",
|
|
392
499
|
"tick_seconds = 5",
|
|
@@ -397,16 +504,18 @@ function buildGatewayConfigTemplate(stateDbPath) {
|
|
|
397
504
|
"enabled = false",
|
|
398
505
|
'bot_token_env = "TELEGRAM_BOT_TOKEN"',
|
|
399
506
|
"poll_timeout_seconds = 25",
|
|
507
|
+
"# Configure at least one allowlist when Telegram is enabled.",
|
|
400
508
|
"allowed_chats = []",
|
|
401
509
|
"allowed_users = []",
|
|
402
510
|
"",
|
|
403
511
|
"# Optional long-lived memory sources injected into gateway-managed sessions.",
|
|
404
512
|
"# Relative paths are resolved from opencode-gateway-workspace.",
|
|
513
|
+
"# Missing files and directories are created automatically.",
|
|
405
514
|
"#",
|
|
406
|
-
"
|
|
407
|
-
'
|
|
408
|
-
'
|
|
409
|
-
"
|
|
515
|
+
"[[memory.entries]]",
|
|
516
|
+
'path = "USER.md"',
|
|
517
|
+
'description = "Persistent user profile and preference memory. Keep this file accurate and concise. Record stable preferences, communication style, workflow habits, project conventions, tool constraints, review expectations, and other recurring facts that should shape future assistance. Update it proactively when you learn something durable about the user. Do not store one-off task details or transient context here."',
|
|
518
|
+
"inject_content = true",
|
|
410
519
|
"#",
|
|
411
520
|
"# [[memory.entries]]",
|
|
412
521
|
'# path = "memory/notes"',
|
|
@@ -424,7 +533,7 @@ function escapeTomlString(value) {
|
|
|
424
533
|
// src/cli/init.ts
|
|
425
534
|
async function runInit(options, env) {
|
|
426
535
|
const configDir = resolveCliConfigDir(options, env);
|
|
427
|
-
const gatewayConfigPath =
|
|
536
|
+
const gatewayConfigPath = join3(configDir, GATEWAY_CONFIG_FILE);
|
|
428
537
|
const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
|
|
429
538
|
const opencodeConfig = await resolveOpencodeConfigFile(configDir);
|
|
430
539
|
const opencodeConfigPath = opencodeConfig.path;
|
|
@@ -435,7 +544,7 @@ async function runInit(options, env) {
|
|
|
435
544
|
await writeFile(opencodeConfigPath, stringifyOpencodeConfig(createDefaultOpencodeConfig(options.managed)));
|
|
436
545
|
opencodeStatus = "created";
|
|
437
546
|
} else {
|
|
438
|
-
const source = await
|
|
547
|
+
const source = await readFile3(opencodeConfigPath, "utf8");
|
|
439
548
|
const parsed = parseOpencodeConfig(source, opencodeConfigPath);
|
|
440
549
|
const next = ensureGatewayPlugin(parsed);
|
|
441
550
|
if (next.changed) {
|
|
@@ -453,6 +562,54 @@ async function runInit(options, env) {
|
|
|
453
562
|
console.log(`opencode config: ${opencodeConfigPath} (${opencodeStatus})`);
|
|
454
563
|
console.log(`gateway config: ${gatewayConfigPath} (${gatewayStatus})`);
|
|
455
564
|
console.log(`gateway workspace: ${workspaceDirPath} (ready)`);
|
|
565
|
+
console.log("next step: start OpenCode with `opencode-gateway serve`");
|
|
566
|
+
console.log("fallback: if you still run `opencode serve`, run `opencode-gateway warm` after startup");
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/cli/serve.ts
|
|
570
|
+
import { spawn } from "node:child_process";
|
|
571
|
+
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
572
|
+
async function runServe(options, env) {
|
|
573
|
+
const target = await resolveServeTarget(options, env);
|
|
574
|
+
await mkdir2(target.workspaceDirPath, { recursive: true });
|
|
575
|
+
const child = spawn("opencode", ["serve"], {
|
|
576
|
+
stdio: "inherit",
|
|
577
|
+
env: {
|
|
578
|
+
...process.env,
|
|
579
|
+
...target.env
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
warmGatewayProject(target).catch((error) => {
|
|
583
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
584
|
+
console.warn(`warning: ${message}`);
|
|
585
|
+
console.warn("warning: the gateway plugin may stay idle until the first project-scoped request");
|
|
586
|
+
});
|
|
587
|
+
const exitCode = await waitForChild(child);
|
|
588
|
+
if (exitCode !== 0) {
|
|
589
|
+
process.exitCode = exitCode;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function waitForChild(child) {
|
|
593
|
+
return new Promise((resolve4, reject) => {
|
|
594
|
+
child.once("error", reject);
|
|
595
|
+
child.once("exit", (code, signal) => {
|
|
596
|
+
if (signal !== null) {
|
|
597
|
+
resolve4(1);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
resolve4(code ?? 0);
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/cli/warm.ts
|
|
606
|
+
import { mkdir as mkdir3 } from "node:fs/promises";
|
|
607
|
+
async function runWarm(options, env) {
|
|
608
|
+
const target = await resolveServeTarget(options, env);
|
|
609
|
+
await mkdir3(target.workspaceDirPath, { recursive: true });
|
|
610
|
+
await warmGatewayProject(target);
|
|
611
|
+
console.log(`gateway plugin warmed: ${target.serverOrigin}`);
|
|
612
|
+
console.log(`warm directory: ${target.workspaceDirPath}`);
|
|
456
613
|
}
|
|
457
614
|
|
|
458
615
|
// src/cli.ts
|
|
@@ -468,6 +625,12 @@ async function main() {
|
|
|
468
625
|
case "init":
|
|
469
626
|
await runInit(command, process.env);
|
|
470
627
|
return;
|
|
628
|
+
case "serve":
|
|
629
|
+
await runServe(command, process.env);
|
|
630
|
+
return;
|
|
631
|
+
case "warm":
|
|
632
|
+
await runWarm(command, process.env);
|
|
633
|
+
return;
|
|
471
634
|
}
|
|
472
635
|
}
|
|
473
636
|
main().catch((error) => {
|
package/dist/index.js
CHANGED
|
@@ -16179,7 +16179,7 @@ async function readMemoryEntry(value, index, workspaceDirPath) {
|
|
|
16179
16179
|
}
|
|
16180
16180
|
throw new Error(`${field}.path must point to a regular file or directory`);
|
|
16181
16181
|
}
|
|
16182
|
-
async function ensurePathMetadata(path, displayPath, entry,
|
|
16182
|
+
async function ensurePathMetadata(path, displayPath, entry, _field) {
|
|
16183
16183
|
try {
|
|
16184
16184
|
return await stat(path);
|
|
16185
16185
|
} catch (error) {
|
|
@@ -17754,7 +17754,9 @@ function delay(durationMs) {
|
|
|
17754
17754
|
|
|
17755
17755
|
// src/opencode/adapter.ts
|
|
17756
17756
|
var SESSION_IDLE_POLL_MS = 250;
|
|
17757
|
-
var
|
|
17757
|
+
var PROMPT_RESPONSE_PROGRESS_TIMEOUT_MS = 90000;
|
|
17758
|
+
var PROMPT_RESPONSE_MAX_TIMEOUT_MS = 10 * 60000;
|
|
17759
|
+
var PROMPT_RESPONSE_SETTLE_MS = 1000;
|
|
17758
17760
|
|
|
17759
17761
|
class OpencodeSdkAdapter {
|
|
17760
17762
|
client;
|
|
@@ -17900,8 +17902,12 @@ class OpencodeSdkAdapter {
|
|
|
17900
17902
|
};
|
|
17901
17903
|
}
|
|
17902
17904
|
async awaitPromptResponse(command) {
|
|
17903
|
-
const
|
|
17905
|
+
const startedAtMs = Date.now();
|
|
17906
|
+
const maxDeadline = startedAtMs + PROMPT_RESPONSE_MAX_TIMEOUT_MS;
|
|
17907
|
+
let progressDeadline = startedAtMs + PROMPT_RESPONSE_PROGRESS_TIMEOUT_MS;
|
|
17904
17908
|
let stableCandidateKey = null;
|
|
17909
|
+
let stableCandidateSinceMs = null;
|
|
17910
|
+
let progressKey = null;
|
|
17905
17911
|
for (;; ) {
|
|
17906
17912
|
const messages = await this.client.session.messages({
|
|
17907
17913
|
path: { id: command.sessionId },
|
|
@@ -17912,22 +17918,32 @@ class OpencodeSdkAdapter {
|
|
|
17912
17918
|
responseStyle: "data",
|
|
17913
17919
|
throwOnError: true
|
|
17914
17920
|
});
|
|
17915
|
-
const
|
|
17921
|
+
const assistantChildren = listAssistantResponses(unwrapData(messages), command.messageId);
|
|
17922
|
+
const nextProgressKey = createAssistantProgressKey(assistantChildren);
|
|
17923
|
+
const now = Date.now();
|
|
17924
|
+
if (progressKey !== nextProgressKey) {
|
|
17925
|
+
progressKey = nextProgressKey;
|
|
17926
|
+
progressDeadline = now + PROMPT_RESPONSE_PROGRESS_TIMEOUT_MS;
|
|
17927
|
+
}
|
|
17928
|
+
const response = selectAssistantResponse(assistantChildren);
|
|
17916
17929
|
if (response !== null) {
|
|
17917
17930
|
const candidateKey = createAssistantCandidateKey(response);
|
|
17918
17931
|
if (stableCandidateKey === candidateKey) {
|
|
17919
|
-
|
|
17920
|
-
|
|
17921
|
-
|
|
17922
|
-
|
|
17923
|
-
|
|
17924
|
-
|
|
17932
|
+
if (stableCandidateSinceMs !== null && now - stableCandidateSinceMs >= PROMPT_RESPONSE_SETTLE_MS) {
|
|
17933
|
+
return toAwaitPromptResponseResult(command.sessionId, response);
|
|
17934
|
+
}
|
|
17935
|
+
} else {
|
|
17936
|
+
stableCandidateKey = candidateKey;
|
|
17937
|
+
stableCandidateSinceMs = now;
|
|
17925
17938
|
}
|
|
17926
|
-
stableCandidateKey = candidateKey;
|
|
17927
17939
|
} else {
|
|
17928
17940
|
stableCandidateKey = null;
|
|
17941
|
+
stableCandidateSinceMs = null;
|
|
17929
17942
|
}
|
|
17930
|
-
if (
|
|
17943
|
+
if (now >= progressDeadline || now >= maxDeadline) {
|
|
17944
|
+
if (response !== null) {
|
|
17945
|
+
return toAwaitPromptResponseResult(command.sessionId, response);
|
|
17946
|
+
}
|
|
17931
17947
|
throw new Error(`assistant message for prompt ${command.messageId} is unavailable after prompt completion`);
|
|
17932
17948
|
}
|
|
17933
17949
|
await delay(SESSION_IDLE_POLL_MS);
|
|
@@ -17988,8 +18004,10 @@ function toSessionPromptPart(part) {
|
|
|
17988
18004
|
};
|
|
17989
18005
|
}
|
|
17990
18006
|
}
|
|
17991
|
-
function
|
|
17992
|
-
|
|
18007
|
+
function listAssistantResponses(messages, userMessageId) {
|
|
18008
|
+
return messages.filter(isAssistantChildMessage(userMessageId));
|
|
18009
|
+
}
|
|
18010
|
+
function selectAssistantResponse(assistantChildren) {
|
|
17993
18011
|
for (let index = assistantChildren.length - 1;index >= 0; index -= 1) {
|
|
17994
18012
|
const candidate = assistantChildren[index];
|
|
17995
18013
|
if (hasVisibleText(candidate)) {
|
|
@@ -18004,6 +18022,9 @@ function selectAssistantResponse(messages, userMessageId) {
|
|
|
18004
18022
|
}
|
|
18005
18023
|
return null;
|
|
18006
18024
|
}
|
|
18025
|
+
function createAssistantProgressKey(messages) {
|
|
18026
|
+
return JSON.stringify(messages.map(createAssistantCandidateKey));
|
|
18027
|
+
}
|
|
18007
18028
|
function createAssistantCandidateKey(message) {
|
|
18008
18029
|
return JSON.stringify({
|
|
18009
18030
|
messageId: message.info.id,
|
|
@@ -18020,6 +18041,14 @@ function createAssistantCandidateKey(message) {
|
|
|
18020
18041
|
function isAssistantChildMessage(userMessageId) {
|
|
18021
18042
|
return (message) => message.info?.role === "assistant" && message.info.parentID === userMessageId;
|
|
18022
18043
|
}
|
|
18044
|
+
function toAwaitPromptResponseResult(sessionId, message) {
|
|
18045
|
+
return {
|
|
18046
|
+
kind: "awaitPromptResponse",
|
|
18047
|
+
sessionId,
|
|
18048
|
+
messageId: message.info.id,
|
|
18049
|
+
parts: message.parts.flatMap(toBindingMessagePart)
|
|
18050
|
+
};
|
|
18051
|
+
}
|
|
18023
18052
|
function toBindingMessagePart(part) {
|
|
18024
18053
|
if (typeof part.id !== "string" || typeof part.messageID !== "string" || part.type.length === 0) {
|
|
18025
18054
|
return [];
|