opencode-gateway 0.1.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 (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/dist/binding/execution.d.ts +24 -0
  4. package/dist/binding/execution.js +1 -0
  5. package/dist/binding/gateway.d.ts +71 -0
  6. package/dist/binding/gateway.js +1 -0
  7. package/dist/binding/index.d.ts +15 -0
  8. package/dist/binding/index.js +4 -0
  9. package/dist/binding/opencode.d.ts +123 -0
  10. package/dist/binding/opencode.js +1 -0
  11. package/dist/cli/args.d.ts +9 -0
  12. package/dist/cli/args.js +53 -0
  13. package/dist/cli/doctor.d.ts +6 -0
  14. package/dist/cli/doctor.js +59 -0
  15. package/dist/cli/init.d.ts +6 -0
  16. package/dist/cli/init.js +35 -0
  17. package/dist/cli/opencode-config.d.ts +10 -0
  18. package/dist/cli/opencode-config.js +62 -0
  19. package/dist/cli/paths.d.ts +7 -0
  20. package/dist/cli/paths.js +22 -0
  21. package/dist/cli/templates.d.ts +1 -0
  22. package/dist/cli/templates.js +26 -0
  23. package/dist/cli.d.ts +2 -0
  24. package/dist/cli.js +314 -0
  25. package/dist/config/cron.d.ts +7 -0
  26. package/dist/config/cron.js +52 -0
  27. package/dist/config/gateway.d.ts +26 -0
  28. package/dist/config/gateway.js +142 -0
  29. package/dist/config/paths.d.ts +10 -0
  30. package/dist/config/paths.js +33 -0
  31. package/dist/config/telegram.d.ts +13 -0
  32. package/dist/config/telegram.js +91 -0
  33. package/dist/cron/runtime.d.ts +40 -0
  34. package/dist/cron/runtime.js +237 -0
  35. package/dist/delivery/telegram.d.ts +16 -0
  36. package/dist/delivery/telegram.js +75 -0
  37. package/dist/delivery/text.d.ts +21 -0
  38. package/dist/delivery/text.js +175 -0
  39. package/dist/gateway.d.ts +33 -0
  40. package/dist/gateway.js +105 -0
  41. package/dist/host/file-sender.d.ts +16 -0
  42. package/dist/host/file-sender.js +59 -0
  43. package/dist/host/noop.d.ts +4 -0
  44. package/dist/host/noop.js +14 -0
  45. package/dist/host/transport.d.ts +9 -0
  46. package/dist/host/transport.js +35 -0
  47. package/dist/index.d.ts +7 -0
  48. package/dist/index.js +52 -0
  49. package/dist/mailbox/router.d.ts +7 -0
  50. package/dist/mailbox/router.js +16 -0
  51. package/dist/media/mime.d.ts +2 -0
  52. package/dist/media/mime.js +45 -0
  53. package/dist/opencode/adapter.d.ts +19 -0
  54. package/dist/opencode/adapter.js +291 -0
  55. package/dist/opencode/driver-hub.d.ts +15 -0
  56. package/dist/opencode/driver-hub.js +82 -0
  57. package/dist/opencode/event-normalize.d.ts +48 -0
  58. package/dist/opencode/event-normalize.js +48 -0
  59. package/dist/opencode/event-stream.d.ts +23 -0
  60. package/dist/opencode/event-stream.js +65 -0
  61. package/dist/opencode/events.d.ts +2 -0
  62. package/dist/opencode/events.js +1 -0
  63. package/dist/questions/client.d.ts +5 -0
  64. package/dist/questions/client.js +36 -0
  65. package/dist/questions/format.d.ts +3 -0
  66. package/dist/questions/format.js +36 -0
  67. package/dist/questions/normalize.d.ts +10 -0
  68. package/dist/questions/normalize.js +45 -0
  69. package/dist/questions/parser.d.ts +11 -0
  70. package/dist/questions/parser.js +96 -0
  71. package/dist/questions/runtime.d.ts +53 -0
  72. package/dist/questions/runtime.js +195 -0
  73. package/dist/questions/types.d.ts +22 -0
  74. package/dist/questions/types.js +1 -0
  75. package/dist/runtime/attachments.d.ts +3 -0
  76. package/dist/runtime/attachments.js +12 -0
  77. package/dist/runtime/executor.d.ts +24 -0
  78. package/dist/runtime/executor.js +188 -0
  79. package/dist/runtime/mailbox.d.ts +25 -0
  80. package/dist/runtime/mailbox.js +112 -0
  81. package/dist/runtime/opencode-runner.d.ts +26 -0
  82. package/dist/runtime/opencode-runner.js +79 -0
  83. package/dist/session/context.d.ts +10 -0
  84. package/dist/session/context.js +44 -0
  85. package/dist/session/conversation-key.d.ts +3 -0
  86. package/dist/session/conversation-key.js +3 -0
  87. package/dist/session/switcher.d.ts +25 -0
  88. package/dist/session/switcher.js +59 -0
  89. package/dist/store/migrations.d.ts +2 -0
  90. package/dist/store/migrations.js +183 -0
  91. package/dist/store/sqlite.d.ts +127 -0
  92. package/dist/store/sqlite.js +678 -0
  93. package/dist/telegram/client.d.ts +35 -0
  94. package/dist/telegram/client.js +179 -0
  95. package/dist/telegram/media.d.ts +13 -0
  96. package/dist/telegram/media.js +65 -0
  97. package/dist/telegram/normalize.d.ts +47 -0
  98. package/dist/telegram/normalize.js +119 -0
  99. package/dist/telegram/poller.d.ts +29 -0
  100. package/dist/telegram/poller.js +97 -0
  101. package/dist/telegram/runtime.d.ts +51 -0
  102. package/dist/telegram/runtime.js +133 -0
  103. package/dist/telegram/state.d.ts +36 -0
  104. package/dist/telegram/state.js +128 -0
  105. package/dist/telegram/types.d.ts +80 -0
  106. package/dist/telegram/types.js +1 -0
  107. package/dist/tools/channel-new-session.d.ts +4 -0
  108. package/dist/tools/channel-new-session.js +27 -0
  109. package/dist/tools/channel-send-file.d.ts +9 -0
  110. package/dist/tools/channel-send-file.js +27 -0
  111. package/dist/tools/channel-target.d.ts +7 -0
  112. package/dist/tools/channel-target.js +28 -0
  113. package/dist/tools/cron-list.d.ts +3 -0
  114. package/dist/tools/cron-list.js +34 -0
  115. package/dist/tools/cron-remove.d.ts +3 -0
  116. package/dist/tools/cron-remove.js +12 -0
  117. package/dist/tools/cron-run.d.ts +3 -0
  118. package/dist/tools/cron-run.js +20 -0
  119. package/dist/tools/cron-upsert.d.ts +3 -0
  120. package/dist/tools/cron-upsert.js +37 -0
  121. package/dist/tools/gateway-dispatch-cron.d.ts +3 -0
  122. package/dist/tools/gateway-dispatch-cron.js +33 -0
  123. package/dist/tools/gateway-status.d.ts +3 -0
  124. package/dist/tools/gateway-status.js +25 -0
  125. package/dist/tools/telegram-send-test.d.ts +3 -0
  126. package/dist/tools/telegram-send-test.js +26 -0
  127. package/dist/tools/telegram-status.d.ts +3 -0
  128. package/dist/tools/telegram-status.js +49 -0
  129. package/dist/tools/time.d.ts +3 -0
  130. package/dist/tools/time.js +25 -0
  131. package/dist/utils/error.d.ts +1 -0
  132. package/dist/utils/error.js +57 -0
  133. package/generated/wasm/pkg/opencode_gateway_ffi.d.ts +23 -0
  134. package/generated/wasm/pkg/opencode_gateway_ffi.js +574 -0
  135. package/generated/wasm/pkg/opencode_gateway_ffi_bg.wasm +0 -0
  136. package/generated/wasm/pkg/opencode_gateway_ffi_bg.wasm.d.ts +22 -0
  137. package/package.json +61 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 M4n5ter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # opencode-gateway
2
+
3
+ Gateway plugin for OpenCode.
4
+
5
+ ## Quick Start
6
+
7
+ Initialize your OpenCode config:
8
+
9
+ ```bash
10
+ npx opencode-gateway init
11
+ ```
12
+
13
+ This ensures:
14
+
15
+ - `plugin: ["opencode-gateway"]` exists in `opencode.json`
16
+ - `opencode-gateway.toml` exists next to `opencode.json`
17
+
18
+ By default the CLI uses `OPENCODE_CONFIG_DIR` when it is set, otherwise it
19
+ writes to:
20
+
21
+ - `~/.config/opencode/opencode.json`
22
+ - `~/.config/opencode/opencode-gateway.toml`
23
+
24
+ Check what it resolved:
25
+
26
+ ```bash
27
+ npx opencode-gateway doctor
28
+ ```
29
+
30
+ Then start OpenCode normally:
31
+
32
+ ```bash
33
+ opencode serve
34
+ ```
35
+
36
+ If you want a separate managed config tree instead of editing your existing
37
+ OpenCode config:
38
+
39
+ ```bash
40
+ npx opencode-gateway init --managed
41
+ export OPENCODE_CONFIG="$HOME/.config/opencode-gateway/opencode/opencode.json"
42
+ export OPENCODE_CONFIG_DIR="$HOME/.config/opencode-gateway/opencode"
43
+ opencode serve
44
+ ```
45
+
46
+ ## Example gateway config
47
+
48
+ ```toml
49
+ [gateway]
50
+ state_db = "/home/you/.local/share/opencode-gateway/state.db"
51
+
52
+ [cron]
53
+ enabled = true
54
+ tick_seconds = 5
55
+ max_concurrent_runs = 1
56
+
57
+ [channels.telegram]
58
+ enabled = false
59
+ bot_token_env = "TELEGRAM_BOT_TOKEN"
60
+ poll_timeout_seconds = 25
61
+ allowed_chats = []
62
+ allowed_users = []
63
+ ```
64
+
65
+ When Telegram is enabled, export the bot token through the configured
66
+ environment variable, for example:
67
+
68
+ ```bash
69
+ export TELEGRAM_BOT_TOKEN="..."
70
+ ```
@@ -0,0 +1,24 @@
1
+ export type BindingExecutionObservation = {
2
+ kind: "messageUpdated";
3
+ sessionId: string;
4
+ messageId: string;
5
+ role: string;
6
+ parentId: string | null;
7
+ } | {
8
+ kind: "textPartUpdated";
9
+ sessionId: string;
10
+ messageId: string;
11
+ partId: string;
12
+ text: string | null;
13
+ delta: string | null;
14
+ ignored: boolean;
15
+ } | {
16
+ kind: "textPartDelta";
17
+ messageId: string;
18
+ partId: string;
19
+ delta: string;
20
+ };
21
+ export type BindingProgressiveDirective = {
22
+ kind: string;
23
+ text: string | null;
24
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ export type GatewayStatusSnapshot = {
2
+ runtimeMode: string;
3
+ supportsTelegram: boolean;
4
+ supportsCron: boolean;
5
+ hasWebUi: boolean;
6
+ };
7
+ export type BindingCronJobSpec = {
8
+ id: string;
9
+ schedule: string;
10
+ prompt: string;
11
+ deliveryChannel: string | null;
12
+ deliveryTarget: string | null;
13
+ deliveryTopic: string | null;
14
+ };
15
+ export type BindingRuntimeReport = {
16
+ conversationKey: string;
17
+ responseText: string;
18
+ delivered: boolean;
19
+ recordedAtMs: bigint;
20
+ };
21
+ export type BindingHostAck = {
22
+ errorMessage: string | null;
23
+ };
24
+ export type BindingDeliveryTarget = {
25
+ channel: string;
26
+ target: string;
27
+ topic: string | null;
28
+ };
29
+ export type BindingInboundAttachment = {
30
+ kind: "image";
31
+ mimeType: string;
32
+ fileName: string | null;
33
+ localPath: string;
34
+ };
35
+ export type BindingInboundMessage = {
36
+ deliveryTarget: BindingDeliveryTarget;
37
+ sender: string;
38
+ text: string | null;
39
+ attachments: BindingInboundAttachment[];
40
+ mailboxKey?: string | null;
41
+ };
42
+ export type BindingPromptPart = {
43
+ kind: "text";
44
+ text: string;
45
+ } | {
46
+ kind: "file";
47
+ mimeType: string;
48
+ fileName: string | null;
49
+ localPath: string;
50
+ };
51
+ export type BindingPreparedExecution = {
52
+ conversationKey: string;
53
+ promptParts: BindingPromptPart[];
54
+ replyTarget: BindingDeliveryTarget | null;
55
+ };
56
+ export type BindingOutboundMessage = {
57
+ deliveryTarget: BindingDeliveryTarget;
58
+ body: string;
59
+ };
60
+ export type BindingTransportHost = {
61
+ sendMessage(message: BindingOutboundMessage): Promise<BindingHostAck>;
62
+ };
63
+ export type BindingLoggerHost = {
64
+ log(level: string, message: string): void;
65
+ };
66
+ export type GatewayContract = {
67
+ gatewayStatus(): GatewayStatusSnapshot;
68
+ conversationKeyForDeliveryTarget(target: BindingDeliveryTarget): string;
69
+ nextCronRunAt(job: BindingCronJobSpec, afterMs: number, timeZone: string): number;
70
+ normalizeCronTimeZone(timeZone: string): string;
71
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import type { BindingCronJobSpec, BindingInboundMessage, BindingPreparedExecution, GatewayContract } from "./gateway";
2
+ import type { BindingOpencodeExecutionInput, OpencodeExecutionDriver } from "./opencode";
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";
5
+ export type { BindingOpencodeCommand, BindingOpencodeCommandPart, BindingOpencodeCommandResult, BindingOpencodeDriverStep, BindingOpencodeExecutionInput, BindingOpencodeMessage, BindingOpencodeMessagePart, BindingOpencodePrompt, OpencodeExecutionDriver, } from "./opencode";
6
+ export type GatewayBindingModule = GatewayContract & {
7
+ prepareInboundExecution: (message: BindingInboundMessage) => BindingPreparedExecution;
8
+ prepareCronExecution: (job: BindingCronJobSpec) => BindingPreparedExecution;
9
+ OpencodeExecutionDriver: {
10
+ new (input: BindingOpencodeExecutionInput): OpencodeExecutionDriver;
11
+ };
12
+ initSync?: (module?: BufferSource | WebAssembly.Module) => unknown;
13
+ default?: (module?: BufferSource | WebAssembly.Module) => Promise<unknown>;
14
+ };
15
+ export declare function loadGatewayBindingModule(): Promise<GatewayBindingModule>;
@@ -0,0 +1,4 @@
1
+ const GENERATED_NODE_ENTRYPOINT = new URL("../../generated/wasm/pkg/opencode_gateway_ffi.js", import.meta.url);
2
+ export async function loadGatewayBindingModule() {
3
+ return (await import(GENERATED_NODE_ENTRYPOINT.href));
4
+ }
@@ -0,0 +1,123 @@
1
+ import type { BindingExecutionObservation, BindingProgressiveDirective } from "./execution";
2
+ import type { BindingPromptPart } from "./gateway";
3
+ export type BindingOpencodePrompt = {
4
+ promptKey: string;
5
+ parts: BindingPromptPart[];
6
+ };
7
+ export type BindingOpencodeExecutionInput = {
8
+ conversationKey: string;
9
+ persistedSessionId: string | null;
10
+ mode: "progressive" | "oneshot";
11
+ flushIntervalMs: number;
12
+ prompts: BindingOpencodePrompt[];
13
+ };
14
+ export type BindingOpencodeMessagePart = {
15
+ messageId: string;
16
+ partId: string;
17
+ type: string;
18
+ text: string | null;
19
+ ignored: boolean;
20
+ };
21
+ export type BindingOpencodeMessage = {
22
+ messageId: string;
23
+ role: string;
24
+ parentId: string | null;
25
+ parts: BindingOpencodeMessagePart[];
26
+ };
27
+ export type BindingOpencodeCommand = {
28
+ kind: "lookupSession";
29
+ sessionId: string;
30
+ } | {
31
+ kind: "createSession";
32
+ title: string;
33
+ } | {
34
+ kind: "waitUntilIdle";
35
+ sessionId: string;
36
+ } | {
37
+ kind: "appendPrompt";
38
+ sessionId: string;
39
+ messageId: string;
40
+ parts: BindingOpencodeCommandPart[];
41
+ } | {
42
+ kind: "sendPromptAsync";
43
+ sessionId: string;
44
+ messageId: string;
45
+ parts: BindingOpencodeCommandPart[];
46
+ } | {
47
+ kind: "awaitPromptResponse";
48
+ sessionId: string;
49
+ messageId: string;
50
+ } | {
51
+ kind: "readMessage";
52
+ sessionId: string;
53
+ messageId: string;
54
+ } | {
55
+ kind: "listMessages";
56
+ sessionId: string;
57
+ };
58
+ export type BindingOpencodeCommandResult = {
59
+ kind: "lookupSession";
60
+ sessionId: string;
61
+ found: boolean;
62
+ } | {
63
+ kind: "createSession";
64
+ sessionId: string;
65
+ } | {
66
+ kind: "waitUntilIdle";
67
+ sessionId: string;
68
+ } | {
69
+ kind: "appendPrompt";
70
+ sessionId: string;
71
+ } | {
72
+ kind: "sendPromptAsync";
73
+ sessionId: string;
74
+ } | {
75
+ kind: "awaitPromptResponse";
76
+ sessionId: string;
77
+ messageId: string;
78
+ parts: BindingOpencodeMessagePart[];
79
+ } | {
80
+ kind: "readMessage";
81
+ sessionId: string;
82
+ messageId: string;
83
+ parts: BindingOpencodeMessagePart[];
84
+ } | {
85
+ kind: "listMessages";
86
+ sessionId: string;
87
+ messages: BindingOpencodeMessage[];
88
+ } | {
89
+ kind: "error";
90
+ commandKind: string;
91
+ sessionId: string | null;
92
+ code: "missingSession" | "unknown";
93
+ message: string;
94
+ };
95
+ export type BindingOpencodeCommandPart = {
96
+ kind: "text";
97
+ partId: string;
98
+ text: string;
99
+ } | {
100
+ kind: "file";
101
+ partId: string;
102
+ mimeType: string;
103
+ fileName: string | null;
104
+ localPath: string;
105
+ };
106
+ export type BindingOpencodeDriverStep = {
107
+ kind: "command";
108
+ command: BindingOpencodeCommand;
109
+ } | {
110
+ kind: "complete";
111
+ sessionId: string;
112
+ responseText: string;
113
+ finalText: string | null;
114
+ } | {
115
+ kind: "failed";
116
+ message: string;
117
+ };
118
+ export type OpencodeExecutionDriver = {
119
+ start(): BindingOpencodeDriverStep;
120
+ resume(result: BindingOpencodeCommandResult): BindingOpencodeDriverStep;
121
+ observeEvent(observation: BindingExecutionObservation, nowMs: number): BindingProgressiveDirective;
122
+ free?(): void;
123
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ export type CliCommand = {
2
+ kind: "help";
3
+ } | {
4
+ kind: "init" | "doctor";
5
+ managed: boolean;
6
+ configDir: string | null;
7
+ };
8
+ export declare function parseCliCommand(argv: string[]): CliCommand;
9
+ export declare function formatCliHelp(): string;
@@ -0,0 +1,53 @@
1
+ import { resolve } from "node:path";
2
+ export function parseCliCommand(argv) {
3
+ const [command, ...rest] = argv;
4
+ if (!command || command === "help" || command === "--help" || command === "-h") {
5
+ return { kind: "help" };
6
+ }
7
+ if (command !== "init" && command !== "doctor") {
8
+ throw new Error(`unknown command: ${command}`);
9
+ }
10
+ let managed = false;
11
+ let configDir = null;
12
+ for (let index = 0; index < rest.length; index += 1) {
13
+ const argument = rest[index];
14
+ if (argument === "--managed") {
15
+ managed = true;
16
+ continue;
17
+ }
18
+ if (argument === "--config-dir") {
19
+ const value = rest[index + 1];
20
+ if (!value) {
21
+ throw new Error("--config-dir requires a value");
22
+ }
23
+ configDir = resolve(value);
24
+ index += 1;
25
+ continue;
26
+ }
27
+ if (argument === "--help" || argument === "-h") {
28
+ return { kind: "help" };
29
+ }
30
+ throw new Error(`unknown argument: ${argument}`);
31
+ }
32
+ if (managed && configDir !== null) {
33
+ throw new Error("--managed cannot be combined with --config-dir");
34
+ }
35
+ return {
36
+ kind: command,
37
+ managed,
38
+ configDir,
39
+ };
40
+ }
41
+ export function formatCliHelp() {
42
+ return [
43
+ "opencode-gateway",
44
+ "",
45
+ "Commands:",
46
+ " opencode-gateway init [--managed] [--config-dir <path>]",
47
+ " opencode-gateway doctor [--managed] [--config-dir <path>]",
48
+ "",
49
+ "Defaults:",
50
+ " init/doctor use OPENCODE_CONFIG_DIR when set, otherwise ~/.config/opencode",
51
+ " --managed uses ~/.config/opencode-gateway/opencode",
52
+ ].join("\n");
53
+ }
@@ -0,0 +1,6 @@
1
+ type DoctorOptions = {
2
+ managed: boolean;
3
+ configDir: string | null;
4
+ };
5
+ export declare function runDoctor(options: DoctorOptions, env: Record<string, string | undefined>): Promise<void>;
6
+ export {};
@@ -0,0 +1,59 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { GATEWAY_CONFIG_FILE, OPENCODE_CONFIG_FILE } from "../config/paths";
4
+ import { parseOpencodeConfig } from "./opencode-config";
5
+ import { pathExists, resolveCliConfigDir } from "./paths";
6
+ export async function runDoctor(options, env) {
7
+ const configDir = resolveCliConfigDir(options, env);
8
+ const opencodeConfigPath = join(configDir, OPENCODE_CONFIG_FILE);
9
+ const gatewayConfigPath = join(configDir, GATEWAY_CONFIG_FILE);
10
+ const opencodeStatus = await inspectOpencodeConfig(opencodeConfigPath);
11
+ const gatewayOverride = env.OPENCODE_GATEWAY_CONFIG?.trim() || null;
12
+ console.log("doctor report");
13
+ console.log(` config dir: ${configDir}`);
14
+ console.log(` opencode config: ${await describePath(opencodeConfigPath)}`);
15
+ console.log(` gateway config: ${await describePath(gatewayConfigPath)}`);
16
+ console.log(` gateway config override: ${gatewayOverride ?? "not set"}`);
17
+ console.log(` plugin configured: ${opencodeStatus.pluginConfigured}`);
18
+ console.log(` TELEGRAM_BOT_TOKEN: ${env.TELEGRAM_BOT_TOKEN?.trim() ? "set" : "missing"}`);
19
+ if (opencodeStatus.error !== null) {
20
+ console.log(` opencode config error: ${opencodeStatus.error}`);
21
+ }
22
+ }
23
+ async function describePath(path) {
24
+ return (await pathExists(path)) ? `present at ${path}` : `missing at ${path}`;
25
+ }
26
+ async function inspectOpencodeConfig(path) {
27
+ if (!(await pathExists(path))) {
28
+ return {
29
+ pluginConfigured: "no",
30
+ error: null,
31
+ };
32
+ }
33
+ try {
34
+ const parsed = parseOpencodeConfig(await readFile(path, "utf8"), path);
35
+ const plugins = parsed.plugin;
36
+ if (plugins === undefined) {
37
+ return {
38
+ pluginConfigured: "no",
39
+ error: null,
40
+ };
41
+ }
42
+ if (!Array.isArray(plugins)) {
43
+ return {
44
+ pluginConfigured: "invalid",
45
+ error: "`plugin` is not an array",
46
+ };
47
+ }
48
+ return {
49
+ pluginConfigured: plugins.some((entry) => entry === "opencode-gateway") ? "yes" : "no",
50
+ error: null,
51
+ };
52
+ }
53
+ catch (error) {
54
+ return {
55
+ pluginConfigured: "unknown",
56
+ error: error instanceof Error ? error.message : String(error),
57
+ };
58
+ }
59
+ }
@@ -0,0 +1,6 @@
1
+ type InitOptions = {
2
+ managed: boolean;
3
+ configDir: string | null;
4
+ };
5
+ export declare function runInit(options: InitOptions, env: Record<string, string | undefined>): Promise<void>;
6
+ export {};
@@ -0,0 +1,35 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { defaultGatewayStateDbPath, GATEWAY_CONFIG_FILE, OPENCODE_CONFIG_FILE, } from "../config/paths";
4
+ import { createDefaultOpencodeConfig, ensureGatewayPlugin, parseOpencodeConfig, stringifyOpencodeConfig, } from "./opencode-config";
5
+ import { pathExists, resolveCliConfigDir } from "./paths";
6
+ import { buildGatewayConfigTemplate } from "./templates";
7
+ export async function runInit(options, env) {
8
+ const configDir = resolveCliConfigDir(options, env);
9
+ const opencodeConfigPath = join(configDir, OPENCODE_CONFIG_FILE);
10
+ const gatewayConfigPath = join(configDir, GATEWAY_CONFIG_FILE);
11
+ await mkdir(configDir, { recursive: true });
12
+ let opencodeStatus = "already present";
13
+ if (!(await pathExists(opencodeConfigPath))) {
14
+ await writeFile(opencodeConfigPath, stringifyOpencodeConfig(createDefaultOpencodeConfig(options.managed)));
15
+ opencodeStatus = "created";
16
+ }
17
+ else {
18
+ const source = await readFile(opencodeConfigPath, "utf8");
19
+ const parsed = parseOpencodeConfig(source, opencodeConfigPath);
20
+ const next = ensureGatewayPlugin(parsed);
21
+ if (next.changed) {
22
+ await writeFile(opencodeConfigPath, stringifyOpencodeConfig(next.document));
23
+ opencodeStatus = "updated";
24
+ }
25
+ }
26
+ let gatewayStatus = "already present";
27
+ if (!(await pathExists(gatewayConfigPath))) {
28
+ await mkdir(dirname(gatewayConfigPath), { recursive: true });
29
+ await writeFile(gatewayConfigPath, buildGatewayConfigTemplate(defaultGatewayStateDbPath(env)));
30
+ gatewayStatus = "created";
31
+ }
32
+ console.log(`config dir: ${configDir}`);
33
+ console.log(`opencode config: ${opencodeConfigPath} (${opencodeStatus})`);
34
+ console.log(`gateway config: ${gatewayConfigPath} (${gatewayStatus})`);
35
+ }
@@ -0,0 +1,10 @@
1
+ type OpencodeConfigDocument = Record<string, unknown>;
2
+ export type EnsurePluginResult = {
3
+ changed: boolean;
4
+ document: OpencodeConfigDocument;
5
+ };
6
+ export declare function createDefaultOpencodeConfig(managed: boolean): OpencodeConfigDocument;
7
+ export declare function ensureGatewayPlugin(document: OpencodeConfigDocument): EnsurePluginResult;
8
+ export declare function parseOpencodeConfig(source: string, path: string): OpencodeConfigDocument;
9
+ export declare function stringifyOpencodeConfig(document: OpencodeConfigDocument): string;
10
+ export {};
@@ -0,0 +1,62 @@
1
+ const OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json";
2
+ const PACKAGE_NAME = "opencode-gateway";
3
+ export function createDefaultOpencodeConfig(managed) {
4
+ const document = {
5
+ $schema: OPENCODE_SCHEMA_URL,
6
+ plugin: [PACKAGE_NAME],
7
+ };
8
+ if (managed) {
9
+ document.server = {
10
+ hostname: "127.0.0.1",
11
+ port: 4096,
12
+ };
13
+ }
14
+ return document;
15
+ }
16
+ export function ensureGatewayPlugin(document) {
17
+ const plugins = document.plugin;
18
+ if (plugins === undefined) {
19
+ return {
20
+ changed: true,
21
+ document: {
22
+ ...document,
23
+ plugin: [PACKAGE_NAME],
24
+ },
25
+ };
26
+ }
27
+ if (!Array.isArray(plugins)) {
28
+ throw new Error("opencode.json field `plugin` must be an array when present");
29
+ }
30
+ if (plugins.some((entry) => entry === PACKAGE_NAME)) {
31
+ return {
32
+ changed: false,
33
+ document,
34
+ };
35
+ }
36
+ return {
37
+ changed: true,
38
+ document: {
39
+ ...document,
40
+ plugin: [...plugins, PACKAGE_NAME],
41
+ },
42
+ };
43
+ }
44
+ export function parseOpencodeConfig(source, path) {
45
+ let parsed;
46
+ try {
47
+ parsed = JSON.parse(source);
48
+ }
49
+ catch (error) {
50
+ throw new Error(`failed to parse opencode config ${path}: ${formatError(error)}`);
51
+ }
52
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
53
+ throw new Error(`opencode config ${path} must decode to a JSON object`);
54
+ }
55
+ return parsed;
56
+ }
57
+ export function stringifyOpencodeConfig(document) {
58
+ return `${JSON.stringify(document, null, 2)}\n`;
59
+ }
60
+ function formatError(error) {
61
+ return error instanceof Error ? error.message : String(error);
62
+ }
@@ -0,0 +1,7 @@
1
+ type CliPathOptions = {
2
+ managed: boolean;
3
+ configDir: string | null;
4
+ };
5
+ export declare function resolveCliConfigDir(options: CliPathOptions, env: Record<string, string | undefined>): string;
6
+ export declare function pathExists(path: string): Promise<boolean>;
7
+ export {};
@@ -0,0 +1,22 @@
1
+ import { access } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { resolveManagedOpencodeConfigDir, resolveOpencodeConfigDir } from "../config/paths";
5
+ export function resolveCliConfigDir(options, env) {
6
+ if (options.configDir !== null) {
7
+ return resolve(options.configDir);
8
+ }
9
+ if (options.managed) {
10
+ return resolveManagedOpencodeConfigDir(env);
11
+ }
12
+ return resolveOpencodeConfigDir(env);
13
+ }
14
+ export async function pathExists(path) {
15
+ try {
16
+ await access(path, constants.F_OK);
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
@@ -0,0 +1 @@
1
+ export declare function buildGatewayConfigTemplate(stateDbPath: string): string;
@@ -0,0 +1,26 @@
1
+ export function buildGatewayConfigTemplate(stateDbPath) {
2
+ return [
3
+ "# Opencode Gateway configuration",
4
+ "# Fill in secrets and provider details before enabling real integrations.",
5
+ "",
6
+ "[gateway]",
7
+ `state_db = "${escapeTomlString(stateDbPath)}"`,
8
+ "",
9
+ "[cron]",
10
+ "enabled = true",
11
+ "tick_seconds = 5",
12
+ "max_concurrent_runs = 1",
13
+ '# timezone = "Asia/Shanghai"',
14
+ "",
15
+ "[channels.telegram]",
16
+ "enabled = false",
17
+ 'bot_token_env = "TELEGRAM_BOT_TOKEN"',
18
+ "poll_timeout_seconds = 25",
19
+ "allowed_chats = []",
20
+ "allowed_users = []",
21
+ "",
22
+ ].join("\n");
23
+ }
24
+ function escapeTomlString(value) {
25
+ return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
26
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};