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/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 join2 } from "node:path";
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: [PACKAGE_NAME]
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.plugin;
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: [PACKAGE_NAME]
121
+ plugin: [PACKAGE_SPEC]
119
122
  }
120
123
  };
121
124
  }
122
- if (!Array.isArray(plugins)) {
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, PACKAGE_NAME]
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 opencodeConfigPath = join2(configDir, OPENCODE_CONFIG_FILE);
185
- const gatewayConfigPath = join2(configDir, GATEWAY_CONFIG_FILE);
335
+ const gatewayConfigPath = join3(configDir, GATEWAY_CONFIG_FILE);
186
336
  const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
187
- const opencodeStatus = await inspectOpencodeConfig(opencodeConfigPath);
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(opencodeConfigPath)}`);
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: plugins.some((entry) => entry === "opencode-gateway") ? "yes" : "no",
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 join3 } from "node:path";
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 opencodeConfigPath = join3(configDir, OPENCODE_CONFIG_FILE);
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 (!await pathExists(opencodeConfigPath)) {
434
+ if (!opencodeConfig.exists) {
281
435
  await writeFile(opencodeConfigPath, stringifyOpencodeConfig(createDefaultOpencodeConfig(options.managed)));
282
436
  opencodeStatus = "created";
283
437
  } else {
@@ -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
  };
@@ -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
+ }
@@ -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;
@@ -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
- constructor(contract: GatewayContract, executor: GatewayExecutor, cron: GatewayCronRuntime, telegram: GatewayTelegramRuntime, files: ChannelFileSender, channelSessions: ChannelSessionSwitcher, sessionContext: GatewaySessionContext);
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/noop";
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
- constructor(contract, executor, cron, telegram, files, channelSessions, sessionContext) {
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 systemPrompt = runtime.sessionContext.buildSystemPrompt(sessionId);
50
- if (systemPrompt !== null) {
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
+ }