opencode-gateway 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +26 -0
  2. package/dist/binding/gateway.d.ts +2 -1
  3. package/dist/binding/index.d.ts +1 -1
  4. package/dist/cli/doctor.js +3 -1
  5. package/dist/cli/init.js +4 -1
  6. package/dist/cli/paths.js +1 -1
  7. package/dist/cli/templates.js +15 -0
  8. package/dist/cli.js +28 -4
  9. package/dist/config/gateway.d.ts +5 -0
  10. package/dist/config/gateway.js +6 -1
  11. package/dist/config/memory.d.ts +18 -0
  12. package/dist/config/memory.js +105 -0
  13. package/dist/config/paths.d.ts +2 -0
  14. package/dist/config/paths.js +5 -1
  15. package/dist/cron/runtime.d.ts +24 -5
  16. package/dist/cron/runtime.js +178 -13
  17. package/dist/delivery/text.js +1 -1
  18. package/dist/gateway.d.ts +3 -1
  19. package/dist/gateway.js +49 -37
  20. package/dist/host/logger.d.ts +8 -0
  21. package/dist/host/logger.js +53 -0
  22. package/dist/index.js +11 -7
  23. package/dist/memory/prompt.d.ts +9 -0
  24. package/dist/memory/prompt.js +122 -0
  25. package/dist/opencode/adapter.d.ts +2 -0
  26. package/dist/opencode/adapter.js +56 -7
  27. package/dist/runtime/conversation-coordinator.d.ts +4 -0
  28. package/dist/runtime/conversation-coordinator.js +22 -0
  29. package/dist/runtime/executor.d.ts +34 -5
  30. package/dist/runtime/executor.js +241 -22
  31. package/dist/runtime/runtime-singleton.d.ts +2 -0
  32. package/dist/runtime/runtime-singleton.js +28 -0
  33. package/dist/session/context.d.ts +1 -1
  34. package/dist/session/context.js +2 -23
  35. package/dist/session/system-prompt.d.ts +8 -0
  36. package/dist/session/system-prompt.js +52 -0
  37. package/dist/store/migrations.js +15 -1
  38. package/dist/store/sqlite.d.ts +20 -2
  39. package/dist/store/sqlite.js +103 -4
  40. package/dist/tools/channel-target.d.ts +5 -0
  41. package/dist/tools/channel-target.js +6 -0
  42. package/dist/tools/cron-run.js +1 -1
  43. package/dist/tools/cron-upsert.d.ts +2 -1
  44. package/dist/tools/cron-upsert.js +20 -6
  45. package/dist/tools/{cron-list.d.ts → schedule-cancel.d.ts} +1 -1
  46. package/dist/tools/schedule-cancel.js +12 -0
  47. package/dist/tools/schedule-format.d.ts +4 -0
  48. package/dist/tools/schedule-format.js +48 -0
  49. package/dist/tools/{cron-remove.d.ts → schedule-list.d.ts} +1 -1
  50. package/dist/tools/schedule-list.js +17 -0
  51. package/dist/tools/schedule-once.d.ts +4 -0
  52. package/dist/tools/schedule-once.js +43 -0
  53. package/dist/tools/schedule-status.d.ts +3 -0
  54. package/dist/tools/schedule-status.js +23 -0
  55. package/generated/wasm/pkg/opencode_gateway_ffi_bg.wasm +0 -0
  56. package/package.json +4 -4
  57. package/dist/host/noop.d.ts +0 -4
  58. package/dist/host/noop.js +0 -14
  59. package/dist/tools/cron-list.js +0 -34
  60. package/dist/tools/cron-remove.js +0 -12
package/README.md CHANGED
@@ -48,6 +48,7 @@ opencode serve
48
48
  ```toml
49
49
  [gateway]
50
50
  state_db = "/home/you/.local/share/opencode-gateway/state.db"
51
+ # log_level = "warn"
51
52
 
52
53
  [cron]
53
54
  enabled = true
@@ -60,6 +61,17 @@ bot_token_env = "TELEGRAM_BOT_TOKEN"
60
61
  poll_timeout_seconds = 25
61
62
  allowed_chats = []
62
63
  allowed_users = []
64
+
65
+ [[memory.entries]]
66
+ path = "memory/project.md"
67
+ description = "Project conventions and long-lived context"
68
+ inject_content = true
69
+
70
+ [[memory.entries]]
71
+ path = "memory/notes"
72
+ description = "Domain notes and operating docs"
73
+ inject_markdown_contents = true
74
+ globs = ["**/*.rs", "notes/**/*.txt"]
63
75
  ```
64
76
 
65
77
  When Telegram is enabled, export the bot token through the configured
@@ -68,3 +80,17 @@ environment variable, for example:
68
80
  ```bash
69
81
  export TELEGRAM_BOT_TOKEN="..."
70
82
  ```
83
+
84
+ Gateway plugin logs are off by default. Set `gateway.log_level` to `error`,
85
+ `warn`, `info`, or `debug` to emit that level and anything above it.
86
+
87
+ Memory rules:
88
+
89
+ - all entries inject their configured path and description
90
+ - file contents are injected only when `inject_content = true`
91
+ - directory entries default to description-only
92
+ - `inject_markdown_contents = true` recursively injects `*.md` and `*.markdown`
93
+ - `globs` are relative to the configured directory and may match other UTF-8
94
+ text files
95
+ - relative paths are resolved from `opencode-gateway.toml`
96
+ - memory is injected only into gateway-managed sessions
@@ -60,8 +60,9 @@ export type BindingOutboundMessage = {
60
60
  export type BindingTransportHost = {
61
61
  sendMessage(message: BindingOutboundMessage): Promise<BindingHostAck>;
62
62
  };
63
+ export type BindingLogLevel = "debug" | "info" | "warn" | "error";
63
64
  export type BindingLoggerHost = {
64
- log(level: string, message: string): void;
65
+ log(level: BindingLogLevel, message: string): void;
65
66
  };
66
67
  export type GatewayContract = {
67
68
  gatewayStatus(): GatewayStatusSnapshot;
@@ -1,7 +1,7 @@
1
1
  import type { BindingCronJobSpec, BindingInboundMessage, BindingPreparedExecution, GatewayContract } from "./gateway";
2
2
  import type { BindingOpencodeExecutionInput, OpencodeExecutionDriver } from "./opencode";
3
3
  export type { BindingExecutionObservation, BindingProgressiveDirective } from "./execution";
4
- export type { BindingCronJobSpec, BindingDeliveryTarget, BindingHostAck, BindingInboundAttachment, BindingInboundMessage, BindingLoggerHost, BindingOutboundMessage, BindingPreparedExecution, BindingPromptPart, BindingRuntimeReport, BindingTransportHost, GatewayContract, GatewayStatusSnapshot, } from "./gateway";
4
+ export type { BindingCronJobSpec, BindingDeliveryTarget, BindingHostAck, BindingInboundAttachment, BindingInboundMessage, BindingLoggerHost, BindingLogLevel, BindingOutboundMessage, BindingPreparedExecution, BindingPromptPart, BindingRuntimeReport, BindingTransportHost, GatewayContract, GatewayStatusSnapshot, } from "./gateway";
5
5
  export type { BindingOpencodeCommand, BindingOpencodeCommandPart, BindingOpencodeCommandResult, BindingOpencodeDriverStep, BindingOpencodeExecutionInput, BindingOpencodeMessage, BindingOpencodeMessagePart, BindingOpencodePrompt, OpencodeExecutionDriver, } from "./opencode";
6
6
  export type GatewayBindingModule = GatewayContract & {
7
7
  prepareInboundExecution: (message: BindingInboundMessage) => BindingPreparedExecution;
@@ -1,18 +1,20 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { GATEWAY_CONFIG_FILE, OPENCODE_CONFIG_FILE } from "../config/paths";
3
+ import { GATEWAY_CONFIG_FILE, OPENCODE_CONFIG_FILE, resolveGatewayWorkspacePath } from "../config/paths";
4
4
  import { parseOpencodeConfig } from "./opencode-config";
5
5
  import { pathExists, resolveCliConfigDir } from "./paths";
6
6
  export async function runDoctor(options, env) {
7
7
  const configDir = resolveCliConfigDir(options, env);
8
8
  const opencodeConfigPath = join(configDir, OPENCODE_CONFIG_FILE);
9
9
  const gatewayConfigPath = join(configDir, GATEWAY_CONFIG_FILE);
10
+ const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
10
11
  const opencodeStatus = await inspectOpencodeConfig(opencodeConfigPath);
11
12
  const gatewayOverride = env.OPENCODE_GATEWAY_CONFIG?.trim() || null;
12
13
  console.log("doctor report");
13
14
  console.log(` config dir: ${configDir}`);
14
15
  console.log(` opencode config: ${await describePath(opencodeConfigPath)}`);
15
16
  console.log(` gateway config: ${await describePath(gatewayConfigPath)}`);
17
+ console.log(` gateway workspace: ${await describePath(workspaceDirPath)}`);
16
18
  console.log(` gateway config override: ${gatewayOverride ?? "not set"}`);
17
19
  console.log(` plugin configured: ${opencodeStatus.pluginConfigured}`);
18
20
  console.log(` TELEGRAM_BOT_TOKEN: ${env.TELEGRAM_BOT_TOKEN?.trim() ? "set" : "missing"}`);
package/dist/cli/init.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
- import { defaultGatewayStateDbPath, GATEWAY_CONFIG_FILE, OPENCODE_CONFIG_FILE, } from "../config/paths";
3
+ import { defaultGatewayStateDbPath, GATEWAY_CONFIG_FILE, OPENCODE_CONFIG_FILE, resolveGatewayWorkspacePath, } from "../config/paths";
4
4
  import { createDefaultOpencodeConfig, ensureGatewayPlugin, parseOpencodeConfig, stringifyOpencodeConfig, } from "./opencode-config";
5
5
  import { pathExists, resolveCliConfigDir } from "./paths";
6
6
  import { buildGatewayConfigTemplate } from "./templates";
@@ -8,7 +8,9 @@ export async function runInit(options, env) {
8
8
  const configDir = resolveCliConfigDir(options, env);
9
9
  const opencodeConfigPath = join(configDir, OPENCODE_CONFIG_FILE);
10
10
  const gatewayConfigPath = join(configDir, GATEWAY_CONFIG_FILE);
11
+ const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
11
12
  await mkdir(configDir, { recursive: true });
13
+ await mkdir(workspaceDirPath, { recursive: true });
12
14
  let opencodeStatus = "already present";
13
15
  if (!(await pathExists(opencodeConfigPath))) {
14
16
  await writeFile(opencodeConfigPath, stringifyOpencodeConfig(createDefaultOpencodeConfig(options.managed)));
@@ -32,4 +34,5 @@ export async function runInit(options, env) {
32
34
  console.log(`config dir: ${configDir}`);
33
35
  console.log(`opencode config: ${opencodeConfigPath} (${opencodeStatus})`);
34
36
  console.log(`gateway config: ${gatewayConfigPath} (${gatewayStatus})`);
37
+ console.log(`gateway workspace: ${workspaceDirPath} (ready)`);
35
38
  }
package/dist/cli/paths.js CHANGED
@@ -1,5 +1,5 @@
1
- import { access } from "node:fs/promises";
2
1
  import { constants } from "node:fs";
2
+ import { access } from "node:fs/promises";
3
3
  import { resolve } from "node:path";
4
4
  import { resolveManagedOpencodeConfigDir, resolveOpencodeConfigDir } from "../config/paths";
5
5
  export function resolveCliConfigDir(options, env) {
@@ -5,6 +5,7 @@ export function buildGatewayConfigTemplate(stateDbPath) {
5
5
  "",
6
6
  "[gateway]",
7
7
  `state_db = "${escapeTomlString(stateDbPath)}"`,
8
+ '# log_level = "warn"',
8
9
  "",
9
10
  "[cron]",
10
11
  "enabled = true",
@@ -19,6 +20,20 @@ export function buildGatewayConfigTemplate(stateDbPath) {
19
20
  "allowed_chats = []",
20
21
  "allowed_users = []",
21
22
  "",
23
+ "# Optional long-lived memory sources injected into gateway-managed sessions.",
24
+ "# Relative paths are resolved from this config file.",
25
+ "#",
26
+ "# [[memory.entries]]",
27
+ '# path = "memory/project.md"',
28
+ '# description = "Project conventions and long-lived context"',
29
+ "# inject_content = true",
30
+ "#",
31
+ "# [[memory.entries]]",
32
+ '# path = "memory/notes"',
33
+ '# description = "Domain notes and operating docs"',
34
+ "# inject_markdown_contents = true",
35
+ '# globs = ["**/*.rs", "notes/**/*.txt"]',
36
+ "",
22
37
  ].join("\n");
23
38
  }
24
39
  function escapeTomlString(value) {
package/dist/cli.js CHANGED
@@ -62,9 +62,10 @@ import { join as join2 } from "node:path";
62
62
 
63
63
  // src/config/paths.ts
64
64
  import { homedir } from "node:os";
65
- import { join, resolve as resolve2 } from "node:path";
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 GATEWAY_WORKSPACE_DIR = "opencode-gateway-workspace";
68
69
  function resolveOpencodeConfigDir(env) {
69
70
  const explicit = env.OPENCODE_CONFIG_DIR;
70
71
  if (explicit && explicit.trim().length > 0) {
@@ -75,6 +76,9 @@ function resolveOpencodeConfigDir(env) {
75
76
  function resolveManagedOpencodeConfigDir(env) {
76
77
  return join(resolveConfigHome(env), "opencode-gateway", "opencode");
77
78
  }
79
+ function resolveGatewayWorkspacePath(configPath) {
80
+ return join(dirname(configPath), GATEWAY_WORKSPACE_DIR);
81
+ }
78
82
  function defaultGatewayStateDbPath(env) {
79
83
  return join(resolveDataHome(env), "opencode-gateway", "state.db");
80
84
  }
@@ -153,8 +157,8 @@ function formatError(error) {
153
157
  }
154
158
 
155
159
  // src/cli/paths.ts
156
- import { access } from "node:fs/promises";
157
160
  import { constants } from "node:fs";
161
+ import { access } from "node:fs/promises";
158
162
  import { resolve as resolve3 } from "node:path";
159
163
  function resolveCliConfigDir(options, env) {
160
164
  if (options.configDir !== null) {
@@ -179,12 +183,14 @@ async function runDoctor(options, env) {
179
183
  const configDir = resolveCliConfigDir(options, env);
180
184
  const opencodeConfigPath = join2(configDir, OPENCODE_CONFIG_FILE);
181
185
  const gatewayConfigPath = join2(configDir, GATEWAY_CONFIG_FILE);
186
+ const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
182
187
  const opencodeStatus = await inspectOpencodeConfig(opencodeConfigPath);
183
188
  const gatewayOverride = env.OPENCODE_GATEWAY_CONFIG?.trim() || null;
184
189
  console.log("doctor report");
185
190
  console.log(` config dir: ${configDir}`);
186
191
  console.log(` opencode config: ${await describePath(opencodeConfigPath)}`);
187
192
  console.log(` gateway config: ${await describePath(gatewayConfigPath)}`);
193
+ console.log(` gateway workspace: ${await describePath(workspaceDirPath)}`);
188
194
  console.log(` gateway config override: ${gatewayOverride ?? "not set"}`);
189
195
  console.log(` plugin configured: ${opencodeStatus.pluginConfigured}`);
190
196
  console.log(` TELEGRAM_BOT_TOKEN: ${env.TELEGRAM_BOT_TOKEN?.trim() ? "set" : "missing"}`);
@@ -231,7 +237,7 @@ async function inspectOpencodeConfig(path) {
231
237
 
232
238
  // src/cli/init.ts
233
239
  import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
234
- import { dirname, join as join3 } from "node:path";
240
+ import { dirname as dirname2, join as join3 } from "node:path";
235
241
 
236
242
  // src/cli/templates.ts
237
243
  function buildGatewayConfigTemplate(stateDbPath) {
@@ -241,6 +247,7 @@ function buildGatewayConfigTemplate(stateDbPath) {
241
247
  "",
242
248
  "[gateway]",
243
249
  `state_db = "${escapeTomlString(stateDbPath)}"`,
250
+ '# log_level = "warn"',
244
251
  "",
245
252
  "[cron]",
246
253
  "enabled = true",
@@ -254,6 +261,20 @@ function buildGatewayConfigTemplate(stateDbPath) {
254
261
  "poll_timeout_seconds = 25",
255
262
  "allowed_chats = []",
256
263
  "allowed_users = []",
264
+ "",
265
+ "# Optional long-lived memory sources injected into gateway-managed sessions.",
266
+ "# Relative paths are resolved from this config file.",
267
+ "#",
268
+ "# [[memory.entries]]",
269
+ '# path = "memory/project.md"',
270
+ '# description = "Project conventions and long-lived context"',
271
+ "# inject_content = true",
272
+ "#",
273
+ "# [[memory.entries]]",
274
+ '# path = "memory/notes"',
275
+ '# description = "Domain notes and operating docs"',
276
+ "# inject_markdown_contents = true",
277
+ '# globs = ["**/*.rs", "notes/**/*.txt"]',
257
278
  ""
258
279
  ].join(`
259
280
  `);
@@ -267,7 +288,9 @@ async function runInit(options, env) {
267
288
  const configDir = resolveCliConfigDir(options, env);
268
289
  const opencodeConfigPath = join3(configDir, OPENCODE_CONFIG_FILE);
269
290
  const gatewayConfigPath = join3(configDir, GATEWAY_CONFIG_FILE);
291
+ const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
270
292
  await mkdir(configDir, { recursive: true });
293
+ await mkdir(workspaceDirPath, { recursive: true });
271
294
  let opencodeStatus = "already present";
272
295
  if (!await pathExists(opencodeConfigPath)) {
273
296
  await writeFile(opencodeConfigPath, stringifyOpencodeConfig(createDefaultOpencodeConfig(options.managed)));
@@ -283,13 +306,14 @@ async function runInit(options, env) {
283
306
  }
284
307
  let gatewayStatus = "already present";
285
308
  if (!await pathExists(gatewayConfigPath)) {
286
- await mkdir(dirname(gatewayConfigPath), { recursive: true });
309
+ await mkdir(dirname2(gatewayConfigPath), { recursive: true });
287
310
  await writeFile(gatewayConfigPath, buildGatewayConfigTemplate(defaultGatewayStateDbPath(env)));
288
311
  gatewayStatus = "created";
289
312
  }
290
313
  console.log(`config dir: ${configDir}`);
291
314
  console.log(`opencode config: ${opencodeConfigPath} (${opencodeStatus})`);
292
315
  console.log(`gateway config: ${gatewayConfigPath} (${gatewayStatus})`);
316
+ console.log(`gateway workspace: ${workspaceDirPath} (ready)`);
293
317
  }
294
318
 
295
319
  // src/cli.ts
@@ -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;
@@ -15,9 +17,12 @@ export type GatewayConfig = {
15
17
  configPath: string;
16
18
  stateDbPath: string;
17
19
  mediaRootPath: string;
20
+ workspaceDirPath: string;
21
+ logLevel: GatewayLogLevel;
18
22
  hasLegacyGatewayTimezone: boolean;
19
23
  legacyGatewayTimezone: string | null;
20
24
  mailbox: GatewayMailboxConfig;
25
+ memory: GatewayMemoryConfig;
21
26
  cron: CronConfig;
22
27
  telegram: TelegramConfig;
23
28
  };
@@ -1,7 +1,9 @@
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";
4
- import { defaultGatewayStateDbPath, resolveGatewayConfigPath } from "./paths";
5
+ import { parseMemoryConfig } from "./memory";
6
+ import { defaultGatewayStateDbPath, resolveGatewayConfigPath, resolveGatewayWorkspacePath } from "./paths";
5
7
  import { parseTelegramConfig } from "./telegram";
6
8
  export async function loadGatewayConfig(env = process.env) {
7
9
  const configPath = resolveGatewayConfigPath(env);
@@ -15,9 +17,12 @@ export async function loadGatewayConfig(env = process.env) {
15
17
  configPath,
16
18
  stateDbPath,
17
19
  mediaRootPath: resolveMediaRootPath(stateDbPath),
20
+ workspaceDirPath: resolveGatewayWorkspacePath(configPath),
21
+ logLevel: parseGatewayLogLevel(rawConfig?.gateway?.log_level, "gateway.log_level"),
18
22
  hasLegacyGatewayTimezone: rawConfig?.gateway?.timezone !== undefined,
19
23
  legacyGatewayTimezone: readLegacyGatewayTimezone(rawConfig?.gateway?.timezone),
20
24
  mailbox: parseMailboxConfig(rawConfig?.gateway?.mailbox),
25
+ memory: await parseMemoryConfig(rawConfig?.memory, configPath),
21
26
  cron: parseCronConfig(rawConfig?.cron),
22
27
  telegram: parseTelegramConfig(rawConfig?.channels?.telegram, env),
23
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,9 +1,11 @@
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 GATEWAY_WORKSPACE_DIR = "opencode-gateway-workspace";
3
4
  type EnvSource = Record<string, string | undefined>;
4
5
  export declare function resolveGatewayConfigPath(env: EnvSource): string;
5
6
  export declare function resolveOpencodeConfigDir(env: EnvSource): string;
6
7
  export declare function resolveManagedOpencodeConfigDir(env: EnvSource): string;
8
+ export declare function resolveGatewayWorkspacePath(configPath: string): string;
7
9
  export declare function defaultGatewayStateDbPath(env: EnvSource): string;
8
10
  export declare function resolveConfigHome(env: EnvSource): string;
9
11
  export declare function resolveDataHome(env: EnvSource): string;
@@ -1,7 +1,8 @@
1
1
  import { homedir } from "node:os";
2
- import { join, resolve } from "node:path";
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 GATEWAY_WORKSPACE_DIR = "opencode-gateway-workspace";
5
6
  export function resolveGatewayConfigPath(env) {
6
7
  const explicit = env.OPENCODE_GATEWAY_CONFIG;
7
8
  if (explicit && explicit.trim().length > 0) {
@@ -19,6 +20,9 @@ export function resolveOpencodeConfigDir(env) {
19
20
  export function resolveManagedOpencodeConfigDir(env) {
20
21
  return join(resolveConfigHome(env), "opencode-gateway", "opencode");
21
22
  }
23
+ export function resolveGatewayWorkspacePath(configPath) {
24
+ return join(dirname(configPath), GATEWAY_WORKSPACE_DIR);
25
+ }
22
26
  export function defaultGatewayStateDbPath(env) {
23
27
  return join(resolveDataHome(env), "opencode-gateway", "state.db");
24
28
  }
@@ -1,7 +1,7 @@
1
- import type { BindingLoggerHost, BindingRuntimeReport, GatewayContract } from "../binding";
1
+ import type { BindingDeliveryTarget, BindingLoggerHost, BindingRuntimeReport, GatewayContract } from "../binding";
2
2
  import type { CronConfig } from "../config/cron";
3
3
  import type { GatewayExecutorLike } from "../runtime/executor";
4
- import type { CronJobRecord, SqliteStore } from "../store/sqlite";
4
+ import type { CronJobRecord, CronRunRecord, SqliteStore } from "../store/sqlite";
5
5
  export type UpsertCronJobInput = {
6
6
  id: string;
7
7
  schedule: string;
@@ -11,6 +11,21 @@ export type UpsertCronJobInput = {
11
11
  deliveryTarget: string | null;
12
12
  deliveryTopic: string | null;
13
13
  };
14
+ export type ScheduleOnceInput = {
15
+ id: string;
16
+ prompt: string;
17
+ delaySeconds: number | null;
18
+ runAtMs: number | null;
19
+ deliveryChannel: string | null;
20
+ deliveryTarget: string | null;
21
+ deliveryTopic: string | null;
22
+ };
23
+ export type ScheduleJobState = "scheduled" | "running" | "succeeded" | "failed" | "abandoned" | "canceled";
24
+ export type ScheduleJobStatus = {
25
+ job: CronJobRecord;
26
+ state: ScheduleJobState;
27
+ runs: CronRunRecord[];
28
+ };
14
29
  export declare class GatewayCronRuntime {
15
30
  private readonly executor;
16
31
  private readonly contract;
@@ -18,22 +33,26 @@ export declare class GatewayCronRuntime {
18
33
  private readonly logger;
19
34
  private readonly config;
20
35
  private readonly effectiveTimeZone;
36
+ private readonly resolveConversationKeyForTarget;
21
37
  private readonly runningJobIds;
22
38
  private running;
23
- constructor(executor: GatewayExecutorLike, contract: GatewayContract, store: SqliteStore, logger: BindingLoggerHost, config: CronConfig, effectiveTimeZone: string);
39
+ constructor(executor: GatewayExecutorLike, contract: GatewayContract, store: SqliteStore, logger: BindingLoggerHost, config: CronConfig, effectiveTimeZone: string, resolveConversationKeyForTarget: (target: BindingDeliveryTarget) => string);
24
40
  isEnabled(): boolean;
25
41
  isRunning(): boolean;
26
42
  runningJobs(): number;
27
43
  timeZone(): string;
28
44
  start(): void;
29
- listJobs(): CronJobRecord[];
45
+ listJobs(includeTerminal?: boolean): CronJobRecord[];
46
+ getJobStatus(id: string, limit?: number): ScheduleJobStatus;
30
47
  upsertJob(input: UpsertCronJobInput): CronJobRecord;
31
- removeJob(id: string): boolean;
48
+ scheduleOnce(input: ScheduleOnceInput): CronJobRecord;
49
+ cancelJob(id: string): boolean;
32
50
  runNow(id: string): Promise<BindingRuntimeReport>;
33
51
  private runLoop;
34
52
  reconcileOnce(nowMs?: number): Promise<void>;
35
53
  tickOnce(nowMs?: number): Promise<void>;
36
54
  private executeJob;
55
+ private appendScheduleResultToTarget;
37
56
  private requireJob;
38
57
  private rebaseJobs;
39
58
  private readStoredEffectiveTimeZone;