opencode-gateway 0.1.1 → 0.2.1
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 +30 -3
- package/dist/binding/gateway.d.ts +2 -1
- package/dist/binding/index.d.ts +1 -1
- package/dist/cli/doctor.js +7 -19
- package/dist/cli/init.js +5 -3
- package/dist/cli/opencode-config-file.d.ts +5 -0
- package/dist/cli/opencode-config-file.js +18 -0
- package/dist/cli/opencode-config.d.ts +2 -0
- package/dist/cli/opencode-config.js +141 -9
- package/dist/cli/templates.js +15 -0
- package/dist/cli.js +186 -32
- package/dist/config/gateway.d.ts +4 -0
- package/dist/config/gateway.js +4 -0
- package/dist/config/memory.d.ts +18 -0
- package/dist/config/memory.js +105 -0
- package/dist/config/paths.d.ts +2 -0
- package/dist/config/paths.js +2 -0
- package/dist/gateway.d.ts +3 -1
- package/dist/gateway.js +10 -4
- package/dist/host/logger.d.ts +8 -0
- package/dist/host/logger.js +53 -0
- package/dist/index.js +2 -2
- package/dist/memory/prompt.d.ts +9 -0
- package/dist/memory/prompt.js +122 -0
- package/dist/runtime/executor.d.ts +1 -0
- package/dist/runtime/executor.js +14 -2
- package/dist/session/context.d.ts +1 -1
- package/dist/session/context.js +2 -29
- package/dist/session/system-prompt.d.ts +8 -0
- package/dist/session/system-prompt.js +52 -0
- package/dist/store/sqlite.d.ts +1 -0
- package/dist/store/sqlite.js +22 -0
- package/package.json +1 -1
- package/dist/host/noop.d.ts +0 -4
- package/dist/host/noop.js +0 -14
package/dist/cli.js
CHANGED
|
@@ -58,13 +58,15 @@ function formatCliHelp() {
|
|
|
58
58
|
|
|
59
59
|
// src/cli/doctor.ts
|
|
60
60
|
import { readFile } from "node:fs/promises";
|
|
61
|
-
import { join as
|
|
61
|
+
import { join as join3 } from "node:path";
|
|
62
62
|
|
|
63
63
|
// src/config/paths.ts
|
|
64
64
|
import { homedir } from "node:os";
|
|
65
65
|
import { dirname, join, resolve as resolve2 } from "node:path";
|
|
66
66
|
var GATEWAY_CONFIG_FILE = "opencode-gateway.toml";
|
|
67
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];
|
|
68
70
|
var GATEWAY_WORKSPACE_DIR = "opencode-gateway-workspace";
|
|
69
71
|
function resolveOpencodeConfigDir(env) {
|
|
70
72
|
const explicit = env.OPENCODE_CONFIG_DIR;
|
|
@@ -95,10 +97,11 @@ function defaultOpencodeConfigDir(env) {
|
|
|
95
97
|
// src/cli/opencode-config.ts
|
|
96
98
|
var OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json";
|
|
97
99
|
var PACKAGE_NAME = "opencode-gateway";
|
|
100
|
+
var PACKAGE_SPEC = "opencode-gateway@latest";
|
|
98
101
|
function createDefaultOpencodeConfig(managed) {
|
|
99
102
|
const document = {
|
|
100
103
|
$schema: OPENCODE_SCHEMA_URL,
|
|
101
|
-
plugin: [
|
|
104
|
+
plugin: [PACKAGE_SPEC]
|
|
102
105
|
};
|
|
103
106
|
if (managed) {
|
|
104
107
|
document.server = {
|
|
@@ -109,20 +112,17 @@ function createDefaultOpencodeConfig(managed) {
|
|
|
109
112
|
return document;
|
|
110
113
|
}
|
|
111
114
|
function ensureGatewayPlugin(document) {
|
|
112
|
-
const plugins = document
|
|
115
|
+
const plugins = readPluginArray(document);
|
|
113
116
|
if (plugins === undefined) {
|
|
114
117
|
return {
|
|
115
118
|
changed: true,
|
|
116
119
|
document: {
|
|
117
120
|
...document,
|
|
118
|
-
plugin: [
|
|
121
|
+
plugin: [PACKAGE_SPEC]
|
|
119
122
|
}
|
|
120
123
|
};
|
|
121
124
|
}
|
|
122
|
-
if (
|
|
123
|
-
throw new Error("opencode.json field `plugin` must be an array when present");
|
|
124
|
-
}
|
|
125
|
-
if (plugins.some((entry) => entry === PACKAGE_NAME)) {
|
|
125
|
+
if (plugins.includes(PACKAGE_SPEC)) {
|
|
126
126
|
return {
|
|
127
127
|
changed: false,
|
|
128
128
|
document
|
|
@@ -132,14 +132,14 @@ function ensureGatewayPlugin(document) {
|
|
|
132
132
|
changed: true,
|
|
133
133
|
document: {
|
|
134
134
|
...document,
|
|
135
|
-
plugin: [...plugins,
|
|
135
|
+
plugin: [...plugins, PACKAGE_SPEC]
|
|
136
136
|
}
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
139
|
function parseOpencodeConfig(source, path) {
|
|
140
140
|
let parsed;
|
|
141
141
|
try {
|
|
142
|
-
parsed = JSON.parse(source);
|
|
142
|
+
parsed = JSON.parse(toStrictJson(source));
|
|
143
143
|
} catch (error) {
|
|
144
144
|
throw new Error(`failed to parse opencode config ${path}: ${formatError(error)}`);
|
|
145
145
|
}
|
|
@@ -152,9 +152,143 @@ function stringifyOpencodeConfig(document) {
|
|
|
152
152
|
return `${JSON.stringify(document, null, 2)}
|
|
153
153
|
`;
|
|
154
154
|
}
|
|
155
|
+
function inspectGatewayPlugin(document) {
|
|
156
|
+
const plugins = readPluginArray(document);
|
|
157
|
+
if (plugins === undefined) {
|
|
158
|
+
return "no";
|
|
159
|
+
}
|
|
160
|
+
if (plugins.includes(PACKAGE_SPEC)) {
|
|
161
|
+
return "yes";
|
|
162
|
+
}
|
|
163
|
+
return plugins.some((entry) => isGatewayPluginReference(entry)) ? "needs_update" : "no";
|
|
164
|
+
}
|
|
155
165
|
function formatError(error) {
|
|
156
166
|
return error instanceof Error ? error.message : String(error);
|
|
157
167
|
}
|
|
168
|
+
function readPluginArray(document) {
|
|
169
|
+
const plugins = document.plugin;
|
|
170
|
+
if (plugins === undefined) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!Array.isArray(plugins)) {
|
|
174
|
+
throw new Error("opencode config field `plugin` must be an array when present");
|
|
175
|
+
}
|
|
176
|
+
const normalized = [];
|
|
177
|
+
for (const [index, entry] of plugins.entries()) {
|
|
178
|
+
if (typeof entry !== "string") {
|
|
179
|
+
throw new Error(`opencode config field \`plugin[${index}]\` must be a string`);
|
|
180
|
+
}
|
|
181
|
+
normalized.push(entry);
|
|
182
|
+
}
|
|
183
|
+
return normalized;
|
|
184
|
+
}
|
|
185
|
+
function toStrictJson(source) {
|
|
186
|
+
return stripTrailingCommas(stripJsonComments(source));
|
|
187
|
+
}
|
|
188
|
+
function stripJsonComments(source) {
|
|
189
|
+
let result = "";
|
|
190
|
+
let inString = false;
|
|
191
|
+
let escaped = false;
|
|
192
|
+
let inLineComment = false;
|
|
193
|
+
let inBlockComment = false;
|
|
194
|
+
for (let index = 0;index < source.length; index += 1) {
|
|
195
|
+
const current = source[index];
|
|
196
|
+
const next = source[index + 1];
|
|
197
|
+
if (inLineComment) {
|
|
198
|
+
if (current === `
|
|
199
|
+
`) {
|
|
200
|
+
inLineComment = false;
|
|
201
|
+
result += current;
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (inBlockComment) {
|
|
206
|
+
if (current === "*" && next === "/") {
|
|
207
|
+
inBlockComment = false;
|
|
208
|
+
index += 1;
|
|
209
|
+
} else if (current === `
|
|
210
|
+
`) {
|
|
211
|
+
result += current;
|
|
212
|
+
}
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (inString) {
|
|
216
|
+
result += current;
|
|
217
|
+
if (escaped) {
|
|
218
|
+
escaped = false;
|
|
219
|
+
} else if (current === "\\") {
|
|
220
|
+
escaped = true;
|
|
221
|
+
} else if (current === '"') {
|
|
222
|
+
inString = false;
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (current === '"') {
|
|
227
|
+
inString = true;
|
|
228
|
+
result += current;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (current === "/" && next === "/") {
|
|
232
|
+
inLineComment = true;
|
|
233
|
+
index += 1;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (current === "/" && next === "*") {
|
|
237
|
+
inBlockComment = true;
|
|
238
|
+
index += 1;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
result += current;
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
function stripTrailingCommas(source) {
|
|
246
|
+
let result = "";
|
|
247
|
+
let inString = false;
|
|
248
|
+
let escaped = false;
|
|
249
|
+
for (let index = 0;index < source.length; index += 1) {
|
|
250
|
+
const current = source[index];
|
|
251
|
+
if (inString) {
|
|
252
|
+
result += current;
|
|
253
|
+
if (escaped) {
|
|
254
|
+
escaped = false;
|
|
255
|
+
} else if (current === "\\") {
|
|
256
|
+
escaped = true;
|
|
257
|
+
} else if (current === '"') {
|
|
258
|
+
inString = false;
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (current === '"') {
|
|
263
|
+
inString = true;
|
|
264
|
+
result += current;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (current === ",") {
|
|
268
|
+
const nextSignificant = findNextSignificantCharacter(source, index + 1);
|
|
269
|
+
if (nextSignificant === "]" || nextSignificant === "}") {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
result += current;
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
function findNextSignificantCharacter(source, startIndex) {
|
|
278
|
+
for (let index = startIndex;index < source.length; index += 1) {
|
|
279
|
+
const current = source[index];
|
|
280
|
+
if (!/\s/.test(current)) {
|
|
281
|
+
return current;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
function isGatewayPluginReference(entry) {
|
|
287
|
+
return entry === PACKAGE_NAME || entry.startsWith(`${PACKAGE_NAME}@`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/cli/opencode-config-file.ts
|
|
291
|
+
import { join as join2 } from "node:path";
|
|
158
292
|
|
|
159
293
|
// src/cli/paths.ts
|
|
160
294
|
import { constants } from "node:fs";
|
|
@@ -178,17 +312,34 @@ async function pathExists(path) {
|
|
|
178
312
|
}
|
|
179
313
|
}
|
|
180
314
|
|
|
315
|
+
// src/cli/opencode-config-file.ts
|
|
316
|
+
async function resolveOpencodeConfigFile(configDir) {
|
|
317
|
+
for (const fileName of OPENCODE_CONFIG_FILE_CANDIDATES) {
|
|
318
|
+
const path = join2(configDir, fileName);
|
|
319
|
+
if (await pathExists(path)) {
|
|
320
|
+
return {
|
|
321
|
+
path,
|
|
322
|
+
exists: true
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
path: join2(configDir, OPENCODE_CONFIG_FILE_JSONC),
|
|
328
|
+
exists: false
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
181
332
|
// src/cli/doctor.ts
|
|
182
333
|
async function runDoctor(options, env) {
|
|
183
334
|
const configDir = resolveCliConfigDir(options, env);
|
|
184
|
-
const
|
|
185
|
-
const gatewayConfigPath = join2(configDir, GATEWAY_CONFIG_FILE);
|
|
335
|
+
const gatewayConfigPath = join3(configDir, GATEWAY_CONFIG_FILE);
|
|
186
336
|
const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
|
|
187
|
-
const
|
|
337
|
+
const opencodeConfig = await resolveOpencodeConfigFile(configDir);
|
|
338
|
+
const opencodeStatus = await inspectOpencodeConfig(opencodeConfig.path);
|
|
188
339
|
const gatewayOverride = env.OPENCODE_GATEWAY_CONFIG?.trim() || null;
|
|
189
340
|
console.log("doctor report");
|
|
190
341
|
console.log(` config dir: ${configDir}`);
|
|
191
|
-
console.log(` opencode config: ${await describePath(
|
|
342
|
+
console.log(` opencode config: ${await describePath(opencodeConfig.path)}`);
|
|
192
343
|
console.log(` gateway config: ${await describePath(gatewayConfigPath)}`);
|
|
193
344
|
console.log(` gateway workspace: ${await describePath(workspaceDirPath)}`);
|
|
194
345
|
console.log(` gateway config override: ${gatewayOverride ?? "not set"}`);
|
|
@@ -210,21 +361,8 @@ async function inspectOpencodeConfig(path) {
|
|
|
210
361
|
}
|
|
211
362
|
try {
|
|
212
363
|
const parsed = parseOpencodeConfig(await readFile(path, "utf8"), path);
|
|
213
|
-
const plugins = parsed.plugin;
|
|
214
|
-
if (plugins === undefined) {
|
|
215
|
-
return {
|
|
216
|
-
pluginConfigured: "no",
|
|
217
|
-
error: null
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
if (!Array.isArray(plugins)) {
|
|
221
|
-
return {
|
|
222
|
-
pluginConfigured: "invalid",
|
|
223
|
-
error: "`plugin` is not an array"
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
364
|
return {
|
|
227
|
-
pluginConfigured:
|
|
365
|
+
pluginConfigured: inspectGatewayPlugin(parsed),
|
|
228
366
|
error: null
|
|
229
367
|
};
|
|
230
368
|
} catch (error) {
|
|
@@ -237,7 +375,7 @@ async function inspectOpencodeConfig(path) {
|
|
|
237
375
|
|
|
238
376
|
// src/cli/init.ts
|
|
239
377
|
import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
|
|
240
|
-
import { dirname as dirname2, join as
|
|
378
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
241
379
|
|
|
242
380
|
// src/cli/templates.ts
|
|
243
381
|
function buildGatewayConfigTemplate(stateDbPath) {
|
|
@@ -247,6 +385,7 @@ function buildGatewayConfigTemplate(stateDbPath) {
|
|
|
247
385
|
"",
|
|
248
386
|
"[gateway]",
|
|
249
387
|
`state_db = "${escapeTomlString(stateDbPath)}"`,
|
|
388
|
+
'# log_level = "warn"',
|
|
250
389
|
"",
|
|
251
390
|
"[cron]",
|
|
252
391
|
"enabled = true",
|
|
@@ -260,6 +399,20 @@ function buildGatewayConfigTemplate(stateDbPath) {
|
|
|
260
399
|
"poll_timeout_seconds = 25",
|
|
261
400
|
"allowed_chats = []",
|
|
262
401
|
"allowed_users = []",
|
|
402
|
+
"",
|
|
403
|
+
"# Optional long-lived memory sources injected into gateway-managed sessions.",
|
|
404
|
+
"# Relative paths are resolved from this config file.",
|
|
405
|
+
"#",
|
|
406
|
+
"# [[memory.entries]]",
|
|
407
|
+
'# path = "memory/project.md"',
|
|
408
|
+
'# description = "Project conventions and long-lived context"',
|
|
409
|
+
"# inject_content = true",
|
|
410
|
+
"#",
|
|
411
|
+
"# [[memory.entries]]",
|
|
412
|
+
'# path = "memory/notes"',
|
|
413
|
+
'# description = "Domain notes and operating docs"',
|
|
414
|
+
"# inject_markdown_contents = true",
|
|
415
|
+
'# globs = ["**/*.rs", "notes/**/*.txt"]',
|
|
263
416
|
""
|
|
264
417
|
].join(`
|
|
265
418
|
`);
|
|
@@ -271,13 +424,14 @@ function escapeTomlString(value) {
|
|
|
271
424
|
// src/cli/init.ts
|
|
272
425
|
async function runInit(options, env) {
|
|
273
426
|
const configDir = resolveCliConfigDir(options, env);
|
|
274
|
-
const
|
|
275
|
-
const gatewayConfigPath = join3(configDir, GATEWAY_CONFIG_FILE);
|
|
427
|
+
const gatewayConfigPath = join4(configDir, GATEWAY_CONFIG_FILE);
|
|
276
428
|
const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
|
|
429
|
+
const opencodeConfig = await resolveOpencodeConfigFile(configDir);
|
|
430
|
+
const opencodeConfigPath = opencodeConfig.path;
|
|
277
431
|
await mkdir(configDir, { recursive: true });
|
|
278
432
|
await mkdir(workspaceDirPath, { recursive: true });
|
|
279
433
|
let opencodeStatus = "already present";
|
|
280
|
-
if (!
|
|
434
|
+
if (!opencodeConfig.exists) {
|
|
281
435
|
await writeFile(opencodeConfigPath, stringifyOpencodeConfig(createDefaultOpencodeConfig(options.managed)));
|
|
282
436
|
opencodeStatus = "created";
|
|
283
437
|
} else {
|
package/dist/config/gateway.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { type GatewayLogLevel } from "../host/logger";
|
|
1
2
|
import { type CronConfig } from "./cron";
|
|
3
|
+
import { type GatewayMemoryConfig } from "./memory";
|
|
2
4
|
import { type TelegramConfig } from "./telegram";
|
|
3
5
|
export type GatewayMailboxRouteConfig = {
|
|
4
6
|
channel: string;
|
|
@@ -16,9 +18,11 @@ export type GatewayConfig = {
|
|
|
16
18
|
stateDbPath: string;
|
|
17
19
|
mediaRootPath: string;
|
|
18
20
|
workspaceDirPath: string;
|
|
21
|
+
logLevel: GatewayLogLevel;
|
|
19
22
|
hasLegacyGatewayTimezone: boolean;
|
|
20
23
|
legacyGatewayTimezone: string | null;
|
|
21
24
|
mailbox: GatewayMailboxConfig;
|
|
25
|
+
memory: GatewayMemoryConfig;
|
|
22
26
|
cron: CronConfig;
|
|
23
27
|
telegram: TelegramConfig;
|
|
24
28
|
};
|
package/dist/config/gateway.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { parseGatewayLogLevel } from "../host/logger";
|
|
3
4
|
import { parseCronConfig } from "./cron";
|
|
5
|
+
import { parseMemoryConfig } from "./memory";
|
|
4
6
|
import { defaultGatewayStateDbPath, resolveGatewayConfigPath, resolveGatewayWorkspacePath } from "./paths";
|
|
5
7
|
import { parseTelegramConfig } from "./telegram";
|
|
6
8
|
export async function loadGatewayConfig(env = process.env) {
|
|
@@ -16,9 +18,11 @@ export async function loadGatewayConfig(env = process.env) {
|
|
|
16
18
|
stateDbPath,
|
|
17
19
|
mediaRootPath: resolveMediaRootPath(stateDbPath),
|
|
18
20
|
workspaceDirPath: resolveGatewayWorkspacePath(configPath),
|
|
21
|
+
logLevel: parseGatewayLogLevel(rawConfig?.gateway?.log_level, "gateway.log_level"),
|
|
19
22
|
hasLegacyGatewayTimezone: rawConfig?.gateway?.timezone !== undefined,
|
|
20
23
|
legacyGatewayTimezone: readLegacyGatewayTimezone(rawConfig?.gateway?.timezone),
|
|
21
24
|
mailbox: parseMailboxConfig(rawConfig?.gateway?.mailbox),
|
|
25
|
+
memory: await parseMemoryConfig(rawConfig?.memory, configPath),
|
|
22
26
|
cron: parseCronConfig(rawConfig?.cron),
|
|
23
27
|
telegram: parseTelegramConfig(rawConfig?.channels?.telegram, env),
|
|
24
28
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type GatewayMemoryConfig = {
|
|
2
|
+
entries: GatewayMemoryEntryConfig[];
|
|
3
|
+
};
|
|
4
|
+
export type GatewayMemoryEntryConfig = {
|
|
5
|
+
kind: "file";
|
|
6
|
+
path: string;
|
|
7
|
+
displayPath: string;
|
|
8
|
+
description: string;
|
|
9
|
+
injectContent: boolean;
|
|
10
|
+
} | {
|
|
11
|
+
kind: "directory";
|
|
12
|
+
path: string;
|
|
13
|
+
displayPath: string;
|
|
14
|
+
description: string;
|
|
15
|
+
injectMarkdownContents: boolean;
|
|
16
|
+
globs: string[];
|
|
17
|
+
};
|
|
18
|
+
export declare function parseMemoryConfig(value: unknown, configPath: string): Promise<GatewayMemoryConfig>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
export async function parseMemoryConfig(value, configPath) {
|
|
4
|
+
const table = readMemoryTable(value);
|
|
5
|
+
const entries = await readMemoryEntries(table.entries, configPath);
|
|
6
|
+
return { entries };
|
|
7
|
+
}
|
|
8
|
+
function readMemoryTable(value) {
|
|
9
|
+
if (value === undefined) {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
13
|
+
throw new Error("memory must be a table when present");
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
async function readMemoryEntries(value, configPath) {
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
if (!Array.isArray(value)) {
|
|
22
|
+
throw new Error("memory.entries must be an array when present");
|
|
23
|
+
}
|
|
24
|
+
return await Promise.all(value.map((entry, index) => readMemoryEntry(entry, index, configPath)));
|
|
25
|
+
}
|
|
26
|
+
async function readMemoryEntry(value, index, configPath) {
|
|
27
|
+
const field = `memory.entries[${index}]`;
|
|
28
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
29
|
+
throw new Error(`${field} must be a table`);
|
|
30
|
+
}
|
|
31
|
+
const entry = value;
|
|
32
|
+
const displayPath = readRequiredString(entry.path, `${field}.path`);
|
|
33
|
+
const description = readRequiredString(entry.description, `${field}.description`);
|
|
34
|
+
const resolvedPath = resolve(dirname(configPath), displayPath);
|
|
35
|
+
const metadata = await statPath(resolvedPath, `${field}.path`);
|
|
36
|
+
if (metadata.isFile()) {
|
|
37
|
+
ensureDirectoryOnlyFieldIsAbsent(entry.inject_markdown_contents, `${field}.inject_markdown_contents`);
|
|
38
|
+
ensureDirectoryOnlyFieldIsAbsent(entry.globs, `${field}.globs`);
|
|
39
|
+
return {
|
|
40
|
+
kind: "file",
|
|
41
|
+
path: resolvedPath,
|
|
42
|
+
displayPath,
|
|
43
|
+
description,
|
|
44
|
+
injectContent: readBoolean(entry.inject_content, `${field}.inject_content`, false),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (metadata.isDirectory()) {
|
|
48
|
+
ensureFileOnlyFieldIsAbsent(entry.inject_content, `${field}.inject_content`);
|
|
49
|
+
return {
|
|
50
|
+
kind: "directory",
|
|
51
|
+
path: resolvedPath,
|
|
52
|
+
displayPath,
|
|
53
|
+
description,
|
|
54
|
+
injectMarkdownContents: readBoolean(entry.inject_markdown_contents, `${field}.inject_markdown_contents`, false),
|
|
55
|
+
globs: readGlobList(entry.globs, `${field}.globs`),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
throw new Error(`${field}.path must point to a regular file or directory`);
|
|
59
|
+
}
|
|
60
|
+
async function statPath(path, field) {
|
|
61
|
+
try {
|
|
62
|
+
return await stat(path);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw new Error(`${field} does not exist: ${path}`, { cause: error });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function ensureDirectoryOnlyFieldIsAbsent(value, field) {
|
|
69
|
+
if (value !== undefined) {
|
|
70
|
+
throw new Error(`${field} is only valid for directory entries`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function ensureFileOnlyFieldIsAbsent(value, field) {
|
|
74
|
+
if (value !== undefined) {
|
|
75
|
+
throw new Error(`${field} is only valid for file entries`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function readBoolean(value, field, fallback) {
|
|
79
|
+
if (value === undefined) {
|
|
80
|
+
return fallback;
|
|
81
|
+
}
|
|
82
|
+
if (typeof value !== "boolean") {
|
|
83
|
+
throw new Error(`${field} must be a boolean when present`);
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
function readGlobList(value, field) {
|
|
88
|
+
if (value === undefined) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
if (!Array.isArray(value)) {
|
|
92
|
+
throw new Error(`${field} must be an array when present`);
|
|
93
|
+
}
|
|
94
|
+
return value.map((entry, index) => readRequiredString(entry, `${field}[${index}]`));
|
|
95
|
+
}
|
|
96
|
+
function readRequiredString(value, field) {
|
|
97
|
+
if (typeof value !== "string") {
|
|
98
|
+
throw new Error(`${field} must be a string`);
|
|
99
|
+
}
|
|
100
|
+
const trimmed = value.trim();
|
|
101
|
+
if (trimmed.length === 0) {
|
|
102
|
+
throw new Error(`${field} must not be empty`);
|
|
103
|
+
}
|
|
104
|
+
return trimmed;
|
|
105
|
+
}
|
package/dist/config/paths.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export declare const GATEWAY_CONFIG_FILE = "opencode-gateway.toml";
|
|
2
2
|
export declare const OPENCODE_CONFIG_FILE = "opencode.json";
|
|
3
|
+
export declare const OPENCODE_CONFIG_FILE_JSONC = "opencode.jsonc";
|
|
4
|
+
export declare const OPENCODE_CONFIG_FILE_CANDIDATES: readonly ["opencode.jsonc", "opencode.json"];
|
|
3
5
|
export declare const GATEWAY_WORKSPACE_DIR = "opencode-gateway-workspace";
|
|
4
6
|
type EnvSource = Record<string, string | undefined>;
|
|
5
7
|
export declare function resolveGatewayConfigPath(env: EnvSource): string;
|
package/dist/config/paths.js
CHANGED
|
@@ -2,6 +2,8 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
export const GATEWAY_CONFIG_FILE = "opencode-gateway.toml";
|
|
4
4
|
export const OPENCODE_CONFIG_FILE = "opencode.json";
|
|
5
|
+
export const OPENCODE_CONFIG_FILE_JSONC = "opencode.jsonc";
|
|
6
|
+
export const OPENCODE_CONFIG_FILE_CANDIDATES = [OPENCODE_CONFIG_FILE_JSONC, OPENCODE_CONFIG_FILE];
|
|
5
7
|
export const GATEWAY_WORKSPACE_DIR = "opencode-gateway-workspace";
|
|
6
8
|
export function resolveGatewayConfigPath(env) {
|
|
7
9
|
const explicit = env.OPENCODE_GATEWAY_CONFIG;
|
package/dist/gateway.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { ChannelFileSender } from "./host/file-sender";
|
|
|
5
5
|
import { GatewayExecutor } from "./runtime/executor";
|
|
6
6
|
import { GatewaySessionContext } from "./session/context";
|
|
7
7
|
import { ChannelSessionSwitcher } from "./session/switcher";
|
|
8
|
+
import { GatewaySystemPromptBuilder } from "./session/system-prompt";
|
|
8
9
|
import { GatewayTelegramRuntime } from "./telegram/runtime";
|
|
9
10
|
export type GatewayPluginStatus = {
|
|
10
11
|
runtimeMode: string;
|
|
@@ -27,7 +28,8 @@ export declare class GatewayPluginRuntime {
|
|
|
27
28
|
readonly files: ChannelFileSender;
|
|
28
29
|
readonly channelSessions: ChannelSessionSwitcher;
|
|
29
30
|
readonly sessionContext: GatewaySessionContext;
|
|
30
|
-
|
|
31
|
+
readonly systemPrompts: GatewaySystemPromptBuilder;
|
|
32
|
+
constructor(contract: GatewayContract, executor: GatewayExecutor, cron: GatewayCronRuntime, telegram: GatewayTelegramRuntime, files: ChannelFileSender, channelSessions: ChannelSessionSwitcher, sessionContext: GatewaySessionContext, systemPrompts: GatewaySystemPromptBuilder);
|
|
31
33
|
status(): GatewayPluginStatus;
|
|
32
34
|
}
|
|
33
35
|
export declare function createGatewayRuntime(module: GatewayBindingModule, input: PluginInput): Promise<GatewayPluginRuntime>;
|
package/dist/gateway.js
CHANGED
|
@@ -4,9 +4,10 @@ import { GatewayCronRuntime } from "./cron/runtime";
|
|
|
4
4
|
import { TelegramProgressiveSupport } from "./delivery/telegram";
|
|
5
5
|
import { GatewayTextDelivery } from "./delivery/text";
|
|
6
6
|
import { ChannelFileSender } from "./host/file-sender";
|
|
7
|
-
import { ConsoleLoggerHost } from "./host/
|
|
7
|
+
import { ConsoleLoggerHost } from "./host/logger";
|
|
8
8
|
import { GatewayTransportHost } from "./host/transport";
|
|
9
9
|
import { GatewayMailboxRouter } from "./mailbox/router";
|
|
10
|
+
import { GatewayMemoryPromptProvider } from "./memory/prompt";
|
|
10
11
|
import { OpencodeSdkAdapter } from "./opencode/adapter";
|
|
11
12
|
import { OpencodeEventStream } from "./opencode/event-stream";
|
|
12
13
|
import { OpencodeEventHub } from "./opencode/events";
|
|
@@ -18,6 +19,7 @@ import { getOrCreateRuntimeSingleton } from "./runtime/runtime-singleton";
|
|
|
18
19
|
import { GatewaySessionContext } from "./session/context";
|
|
19
20
|
import { resolveConversationKeyForTarget } from "./session/conversation-key";
|
|
20
21
|
import { ChannelSessionSwitcher } from "./session/switcher";
|
|
22
|
+
import { GatewaySystemPromptBuilder } from "./session/system-prompt";
|
|
21
23
|
import { openSqliteStore } from "./store/sqlite";
|
|
22
24
|
import { TelegramBotClient } from "./telegram/client";
|
|
23
25
|
import { TelegramInboundMediaStore } from "./telegram/media";
|
|
@@ -31,7 +33,8 @@ export class GatewayPluginRuntime {
|
|
|
31
33
|
files;
|
|
32
34
|
channelSessions;
|
|
33
35
|
sessionContext;
|
|
34
|
-
|
|
36
|
+
systemPrompts;
|
|
37
|
+
constructor(contract, executor, cron, telegram, files, channelSessions, sessionContext, systemPrompts) {
|
|
35
38
|
this.contract = contract;
|
|
36
39
|
this.executor = executor;
|
|
37
40
|
this.cron = cron;
|
|
@@ -39,6 +42,7 @@ export class GatewayPluginRuntime {
|
|
|
39
42
|
this.files = files;
|
|
40
43
|
this.channelSessions = channelSessions;
|
|
41
44
|
this.sessionContext = sessionContext;
|
|
45
|
+
this.systemPrompts = systemPrompts;
|
|
42
46
|
}
|
|
43
47
|
status() {
|
|
44
48
|
const rustStatus = this.contract.gatewayStatus();
|
|
@@ -61,7 +65,7 @@ export async function createGatewayRuntime(module, input) {
|
|
|
61
65
|
const config = await loadGatewayConfig();
|
|
62
66
|
return await getOrCreateRuntimeSingleton(config.configPath, async () => {
|
|
63
67
|
await mkdir(config.workspaceDirPath, { recursive: true });
|
|
64
|
-
const logger = new ConsoleLoggerHost();
|
|
68
|
+
const logger = new ConsoleLoggerHost(config.logLevel);
|
|
65
69
|
if (config.hasLegacyGatewayTimezone) {
|
|
66
70
|
const suffix = config.legacyGatewayTimezone === null ? "" : ` (${config.legacyGatewayTimezone})`;
|
|
67
71
|
logger.log("warn", `gateway.timezone${suffix} is ignored; use cron.timezone instead`);
|
|
@@ -69,6 +73,8 @@ export async function createGatewayRuntime(module, input) {
|
|
|
69
73
|
const effectiveCronTimeZone = resolveEffectiveCronTimeZone(module, config);
|
|
70
74
|
const store = await openSqliteStore(config.stateDbPath);
|
|
71
75
|
const sessionContext = new GatewaySessionContext(store);
|
|
76
|
+
const memoryPrompts = new GatewayMemoryPromptProvider(config.memory, logger);
|
|
77
|
+
const systemPrompts = new GatewaySystemPromptBuilder(sessionContext, memoryPrompts);
|
|
72
78
|
const telegramClient = config.telegram.enabled ? new TelegramBotClient(config.telegram.botToken) : null;
|
|
73
79
|
const telegramMediaStore = config.telegram.enabled && telegramClient !== null
|
|
74
80
|
? new TelegramInboundMediaStore(telegramClient, config.mediaRootPath)
|
|
@@ -95,7 +101,7 @@ export async function createGatewayRuntime(module, input) {
|
|
|
95
101
|
cron.start();
|
|
96
102
|
mailbox.start();
|
|
97
103
|
telegram.start();
|
|
98
|
-
return new GatewayPluginRuntime(module, executor, cron, telegram, files, channelSessions, sessionContext);
|
|
104
|
+
return new GatewayPluginRuntime(module, executor, cron, telegram, files, channelSessions, sessionContext, systemPrompts);
|
|
99
105
|
});
|
|
100
106
|
}
|
|
101
107
|
function resolveEffectiveCronTimeZone(module, config) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BindingLoggerHost, BindingLogLevel } from "../binding";
|
|
2
|
+
export type GatewayLogLevel = BindingLogLevel | "off";
|
|
3
|
+
export declare class ConsoleLoggerHost implements BindingLoggerHost {
|
|
4
|
+
private readonly threshold;
|
|
5
|
+
constructor(threshold: GatewayLogLevel);
|
|
6
|
+
log(level: BindingLogLevel, message: string): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function parseGatewayLogLevel(value: unknown, field: string): GatewayLogLevel;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const LOG_LEVEL_PRIORITY = {
|
|
2
|
+
debug: 10,
|
|
3
|
+
info: 20,
|
|
4
|
+
warn: 30,
|
|
5
|
+
error: 40,
|
|
6
|
+
};
|
|
7
|
+
export class ConsoleLoggerHost {
|
|
8
|
+
threshold;
|
|
9
|
+
constructor(threshold) {
|
|
10
|
+
this.threshold = threshold;
|
|
11
|
+
}
|
|
12
|
+
log(level, message) {
|
|
13
|
+
if (!shouldLog(level, this.threshold)) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const line = `[gateway:${level}] ${message}`;
|
|
17
|
+
switch (level) {
|
|
18
|
+
case "error":
|
|
19
|
+
console.error(line);
|
|
20
|
+
return;
|
|
21
|
+
case "warn":
|
|
22
|
+
console.warn(line);
|
|
23
|
+
return;
|
|
24
|
+
default:
|
|
25
|
+
console.info(line);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function parseGatewayLogLevel(value, field) {
|
|
30
|
+
if (value === undefined) {
|
|
31
|
+
return "off";
|
|
32
|
+
}
|
|
33
|
+
if (typeof value !== "string") {
|
|
34
|
+
throw new Error(`${field} must be a string when present`);
|
|
35
|
+
}
|
|
36
|
+
const normalized = value.trim().toLowerCase();
|
|
37
|
+
if (normalized === "off") {
|
|
38
|
+
return "off";
|
|
39
|
+
}
|
|
40
|
+
if (isBindingLogLevel(normalized)) {
|
|
41
|
+
return normalized;
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`${field} must be one of: off, error, warn, info, debug`);
|
|
44
|
+
}
|
|
45
|
+
function shouldLog(level, threshold) {
|
|
46
|
+
if (threshold === "off") {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[threshold];
|
|
50
|
+
}
|
|
51
|
+
function isBindingLogLevel(value) {
|
|
52
|
+
return value === "debug" || value === "info" || value === "warn" || value === "error";
|
|
53
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -46,8 +46,8 @@ export const OpencodeGatewayPlugin = async (input) => {
|
|
|
46
46
|
if (!sessionId) {
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
|
-
const
|
|
50
|
-
|
|
49
|
+
const systemPrompts = await runtime.systemPrompts.buildPrompts(sessionId);
|
|
50
|
+
for (const systemPrompt of systemPrompts) {
|
|
51
51
|
output.system.push(systemPrompt);
|
|
52
52
|
}
|
|
53
53
|
},
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BindingLoggerHost } from "../binding";
|
|
2
|
+
import type { GatewayMemoryConfig } from "../config/memory";
|
|
3
|
+
export declare class GatewayMemoryPromptProvider {
|
|
4
|
+
private readonly config;
|
|
5
|
+
private readonly logger;
|
|
6
|
+
constructor(config: GatewayMemoryConfig, logger: Pick<BindingLoggerHost, "log">);
|
|
7
|
+
buildPrompt(): Promise<string | null>;
|
|
8
|
+
private buildEntrySection;
|
|
9
|
+
}
|