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
@@ -0,0 +1,133 @@
1
+ import { formatUnixMsAsUtc } from "../tools/time";
2
+ import { formatError } from "../utils/error";
3
+ import { readTelegramHealthSnapshot, recordTelegramProbeFailure, recordTelegramProbeSuccess, recordTelegramSendFailure, } from "./state";
4
+ export class GatewayTelegramRuntime {
5
+ client;
6
+ delivery;
7
+ store;
8
+ logger;
9
+ config;
10
+ polling;
11
+ opencodeEvents;
12
+ constructor(client, delivery, store, logger, config, polling, opencodeEvents) {
13
+ this.client = client;
14
+ this.delivery = delivery;
15
+ this.store = store;
16
+ this.logger = logger;
17
+ this.config = config;
18
+ this.polling = polling;
19
+ this.opencodeEvents = opencodeEvents;
20
+ }
21
+ isEnabled() {
22
+ return this.config.enabled;
23
+ }
24
+ isPolling() {
25
+ return this.polling?.isRunning() ?? false;
26
+ }
27
+ allowlistMode() {
28
+ return this.config.enabled ? "explicit" : "disabled";
29
+ }
30
+ start() {
31
+ this.polling?.start();
32
+ }
33
+ async status() {
34
+ const snapshot = readTelegramHealthSnapshot(this.store);
35
+ if (!this.config.enabled || this.client === null) {
36
+ return {
37
+ ...snapshot,
38
+ enabled: false,
39
+ polling: false,
40
+ allowlistMode: "disabled",
41
+ allowedChatsCount: 0,
42
+ allowedUsersCount: 0,
43
+ liveProbe: "disabled",
44
+ liveProbeError: null,
45
+ liveBotId: null,
46
+ liveBotUsername: null,
47
+ streamingEnabled: false,
48
+ opencodeEventStreamConnected: this.opencodeEvents.isConnected(),
49
+ lastEventStreamError: this.opencodeEvents.lastStreamError(),
50
+ };
51
+ }
52
+ try {
53
+ const bot = await this.client.getMe();
54
+ const recordedAtMs = Date.now();
55
+ recordTelegramProbeSuccess(this.store, bot, recordedAtMs);
56
+ return buildEnabledStatus(this.config, readTelegramHealthSnapshot(this.store), this.isPolling(), "ok", null, bot, this.opencodeEvents);
57
+ }
58
+ catch (error) {
59
+ const message = formatError(error);
60
+ const recordedAtMs = Date.now();
61
+ recordTelegramProbeFailure(this.store, message, recordedAtMs);
62
+ this.logger.log("warn", `telegram live probe failed: ${message}`);
63
+ return buildEnabledStatus(this.config, readTelegramHealthSnapshot(this.store), this.isPolling(), "failed", message, null, this.opencodeEvents);
64
+ }
65
+ }
66
+ async sendTest(chatId, topic, text, mode) {
67
+ const normalizedChatId = normalizeRequiredField(chatId, "chat_id");
68
+ const normalizedTopic = normalizeOptionalField(topic);
69
+ const body = normalizeOptionalField(text) ?? defaultTestMessage();
70
+ if (!this.config.enabled || this.client === null) {
71
+ throw new Error("telegram is not enabled");
72
+ }
73
+ try {
74
+ const sentAtMs = Date.now();
75
+ const result = await this.delivery.sendTest({
76
+ channel: "telegram",
77
+ target: normalizedChatId,
78
+ topic: normalizedTopic,
79
+ }, body, mode);
80
+ if (!result.delivered) {
81
+ throw new Error("telegram test delivery produced no final message");
82
+ }
83
+ return {
84
+ chatId: normalizedChatId,
85
+ topic: normalizedTopic,
86
+ text: body,
87
+ sentAtMs,
88
+ mode: result.mode,
89
+ };
90
+ }
91
+ catch (error) {
92
+ const message = formatError(error);
93
+ recordTelegramSendFailure(this.store, message, Date.now());
94
+ this.logger.log("warn", `telegram_send_test failed: ${message}`);
95
+ throw error;
96
+ }
97
+ }
98
+ }
99
+ function buildEnabledStatus(config, snapshot, polling, liveProbe, liveProbeError, bot, opencodeEvents) {
100
+ return {
101
+ ...snapshot,
102
+ enabled: true,
103
+ polling,
104
+ allowlistMode: "explicit",
105
+ allowedChatsCount: config.allowedChats.length,
106
+ allowedUsersCount: config.allowedUsers.length,
107
+ liveProbe,
108
+ liveProbeError,
109
+ liveBotId: bot ? String(bot.id) : null,
110
+ liveBotUsername: bot?.username ?? null,
111
+ streamingEnabled: true,
112
+ opencodeEventStreamConnected: opencodeEvents.isConnected(),
113
+ lastEventStreamError: opencodeEvents.lastStreamError(),
114
+ };
115
+ }
116
+ function normalizeRequiredField(value, field) {
117
+ const trimmed = value.trim();
118
+ if (trimmed.length === 0) {
119
+ throw new Error(`${field} must not be empty`);
120
+ }
121
+ return trimmed;
122
+ }
123
+ function normalizeOptionalField(value) {
124
+ if (value === null) {
125
+ return null;
126
+ }
127
+ const trimmed = value.trim();
128
+ return trimmed.length === 0 ? null : trimmed;
129
+ }
130
+ function defaultTestMessage() {
131
+ const recordedAtMs = Date.now();
132
+ return `opencode-gateway telegram_send_test at ${formatUnixMsAsUtc(recordedAtMs)}`;
133
+ }
@@ -0,0 +1,36 @@
1
+ import type { SqliteStore } from "../store/sqlite";
2
+ import type { TelegramBotProfile, TelegramChatType } from "./types";
3
+ export type TelegramStreamingFallbackReason = "non_private_chat" | "progressive_handle_unavailable" | "draft_send_failed" | "preview_not_established";
4
+ export type TelegramHealthSnapshot = {
5
+ updateOffset: number | null;
6
+ lastPollSuccessMs: number | null;
7
+ lastPollErrorAtMs: number | null;
8
+ lastPollErrorMessage: string | null;
9
+ lastSendSuccessMs: number | null;
10
+ lastSendErrorAtMs: number | null;
11
+ lastSendErrorMessage: string | null;
12
+ lastProbeSuccessMs: number | null;
13
+ lastProbeErrorAtMs: number | null;
14
+ lastProbeErrorMessage: string | null;
15
+ lastBotId: string | null;
16
+ lastBotUsername: string | null;
17
+ lastDraftSuccessMs: number | null;
18
+ lastDraftErrorAtMs: number | null;
19
+ lastDraftErrorMessage: string | null;
20
+ lastPreviewEmitMs: number | null;
21
+ lastStreamFallbackAtMs: number | null;
22
+ lastStreamFallbackReason: TelegramStreamingFallbackReason | null;
23
+ };
24
+ export declare function readTelegramHealthSnapshot(store: SqliteStore): TelegramHealthSnapshot;
25
+ export declare function recordTelegramPollSuccess(store: SqliteStore, recordedAtMs: number): void;
26
+ export declare function recordTelegramPollFailure(store: SqliteStore, message: string, recordedAtMs: number): void;
27
+ export declare function recordTelegramSendSuccess(store: SqliteStore, recordedAtMs: number): void;
28
+ export declare function recordTelegramSendFailure(store: SqliteStore, message: string, recordedAtMs: number): void;
29
+ export declare function recordTelegramProbeSuccess(store: SqliteStore, bot: TelegramBotProfile, recordedAtMs: number): void;
30
+ export declare function recordTelegramProbeFailure(store: SqliteStore, message: string, recordedAtMs: number): void;
31
+ export declare function recordTelegramDraftSuccess(store: SqliteStore, recordedAtMs: number): void;
32
+ export declare function recordTelegramDraftFailure(store: SqliteStore, message: string, recordedAtMs: number): void;
33
+ export declare function recordTelegramPreviewEmit(store: SqliteStore, recordedAtMs: number): void;
34
+ export declare function recordTelegramStreamFallback(store: SqliteStore, reason: TelegramStreamingFallbackReason, recordedAtMs: number): void;
35
+ export declare function readTelegramChatType(store: SqliteStore, chatId: string): TelegramChatType | null;
36
+ export declare function recordTelegramChatType(store: SqliteStore, chatId: string, chatType: TelegramChatType, recordedAtMs: number): void;
@@ -0,0 +1,128 @@
1
+ const TELEGRAM_UPDATE_OFFSET_KEY = "telegram.update_offset";
2
+ const TELEGRAM_LAST_POLL_SUCCESS_MS_KEY = "telegram.last_poll_success_ms";
3
+ const TELEGRAM_LAST_POLL_ERROR_AT_MS_KEY = "telegram.last_poll_error_at_ms";
4
+ const TELEGRAM_LAST_POLL_ERROR_MESSAGE_KEY = "telegram.last_poll_error_message";
5
+ const TELEGRAM_LAST_SEND_SUCCESS_MS_KEY = "telegram.last_send_success_ms";
6
+ const TELEGRAM_LAST_SEND_ERROR_AT_MS_KEY = "telegram.last_send_error_at_ms";
7
+ const TELEGRAM_LAST_SEND_ERROR_MESSAGE_KEY = "telegram.last_send_error_message";
8
+ const TELEGRAM_LAST_PROBE_SUCCESS_MS_KEY = "telegram.last_probe_success_ms";
9
+ const TELEGRAM_LAST_PROBE_ERROR_AT_MS_KEY = "telegram.last_probe_error_at_ms";
10
+ const TELEGRAM_LAST_PROBE_ERROR_MESSAGE_KEY = "telegram.last_probe_error_message";
11
+ const TELEGRAM_LAST_BOT_ID_KEY = "telegram.last_bot_id";
12
+ const TELEGRAM_LAST_BOT_USERNAME_KEY = "telegram.last_bot_username";
13
+ const TELEGRAM_LAST_DRAFT_SUCCESS_MS_KEY = "telegram.last_draft_success_ms";
14
+ const TELEGRAM_LAST_DRAFT_ERROR_AT_MS_KEY = "telegram.last_draft_error_at_ms";
15
+ const TELEGRAM_LAST_DRAFT_ERROR_MESSAGE_KEY = "telegram.last_draft_error_message";
16
+ const TELEGRAM_LAST_PREVIEW_EMIT_MS_KEY = "telegram.last_preview_emit_ms";
17
+ const TELEGRAM_LAST_STREAM_FALLBACK_AT_MS_KEY = "telegram.last_stream_fallback_at_ms";
18
+ const TELEGRAM_LAST_STREAM_FALLBACK_REASON_KEY = "telegram.last_stream_fallback_reason";
19
+ export function readTelegramHealthSnapshot(store) {
20
+ return {
21
+ updateOffset: readStoredInteger(store, TELEGRAM_UPDATE_OFFSET_KEY),
22
+ lastPollSuccessMs: readStoredInteger(store, TELEGRAM_LAST_POLL_SUCCESS_MS_KEY),
23
+ lastPollErrorAtMs: readStoredInteger(store, TELEGRAM_LAST_POLL_ERROR_AT_MS_KEY),
24
+ lastPollErrorMessage: readStoredText(store, TELEGRAM_LAST_POLL_ERROR_MESSAGE_KEY),
25
+ lastSendSuccessMs: readStoredInteger(store, TELEGRAM_LAST_SEND_SUCCESS_MS_KEY),
26
+ lastSendErrorAtMs: readStoredInteger(store, TELEGRAM_LAST_SEND_ERROR_AT_MS_KEY),
27
+ lastSendErrorMessage: readStoredText(store, TELEGRAM_LAST_SEND_ERROR_MESSAGE_KEY),
28
+ lastProbeSuccessMs: readStoredInteger(store, TELEGRAM_LAST_PROBE_SUCCESS_MS_KEY),
29
+ lastProbeErrorAtMs: readStoredInteger(store, TELEGRAM_LAST_PROBE_ERROR_AT_MS_KEY),
30
+ lastProbeErrorMessage: readStoredText(store, TELEGRAM_LAST_PROBE_ERROR_MESSAGE_KEY),
31
+ lastBotId: readStoredText(store, TELEGRAM_LAST_BOT_ID_KEY),
32
+ lastBotUsername: readStoredText(store, TELEGRAM_LAST_BOT_USERNAME_KEY),
33
+ lastDraftSuccessMs: readStoredInteger(store, TELEGRAM_LAST_DRAFT_SUCCESS_MS_KEY),
34
+ lastDraftErrorAtMs: readStoredInteger(store, TELEGRAM_LAST_DRAFT_ERROR_AT_MS_KEY),
35
+ lastDraftErrorMessage: readStoredText(store, TELEGRAM_LAST_DRAFT_ERROR_MESSAGE_KEY),
36
+ lastPreviewEmitMs: readStoredInteger(store, TELEGRAM_LAST_PREVIEW_EMIT_MS_KEY),
37
+ lastStreamFallbackAtMs: readStoredInteger(store, TELEGRAM_LAST_STREAM_FALLBACK_AT_MS_KEY),
38
+ lastStreamFallbackReason: readStoredFallbackReason(store),
39
+ };
40
+ }
41
+ export function recordTelegramPollSuccess(store, recordedAtMs) {
42
+ store.putStateValue(TELEGRAM_LAST_POLL_SUCCESS_MS_KEY, String(recordedAtMs), recordedAtMs);
43
+ store.putStateValue(TELEGRAM_LAST_POLL_ERROR_AT_MS_KEY, "", recordedAtMs);
44
+ store.putStateValue(TELEGRAM_LAST_POLL_ERROR_MESSAGE_KEY, "", recordedAtMs);
45
+ }
46
+ export function recordTelegramPollFailure(store, message, recordedAtMs) {
47
+ store.putStateValue(TELEGRAM_LAST_POLL_ERROR_AT_MS_KEY, String(recordedAtMs), recordedAtMs);
48
+ store.putStateValue(TELEGRAM_LAST_POLL_ERROR_MESSAGE_KEY, message, recordedAtMs);
49
+ }
50
+ export function recordTelegramSendSuccess(store, recordedAtMs) {
51
+ store.putStateValue(TELEGRAM_LAST_SEND_SUCCESS_MS_KEY, String(recordedAtMs), recordedAtMs);
52
+ store.putStateValue(TELEGRAM_LAST_SEND_ERROR_AT_MS_KEY, "", recordedAtMs);
53
+ store.putStateValue(TELEGRAM_LAST_SEND_ERROR_MESSAGE_KEY, "", recordedAtMs);
54
+ }
55
+ export function recordTelegramSendFailure(store, message, recordedAtMs) {
56
+ store.putStateValue(TELEGRAM_LAST_SEND_ERROR_AT_MS_KEY, String(recordedAtMs), recordedAtMs);
57
+ store.putStateValue(TELEGRAM_LAST_SEND_ERROR_MESSAGE_KEY, message, recordedAtMs);
58
+ }
59
+ export function recordTelegramProbeSuccess(store, bot, recordedAtMs) {
60
+ store.putStateValue(TELEGRAM_LAST_PROBE_SUCCESS_MS_KEY, String(recordedAtMs), recordedAtMs);
61
+ store.putStateValue(TELEGRAM_LAST_PROBE_ERROR_AT_MS_KEY, "", recordedAtMs);
62
+ store.putStateValue(TELEGRAM_LAST_PROBE_ERROR_MESSAGE_KEY, "", recordedAtMs);
63
+ store.putStateValue(TELEGRAM_LAST_BOT_ID_KEY, String(bot.id), recordedAtMs);
64
+ store.putStateValue(TELEGRAM_LAST_BOT_USERNAME_KEY, bot.username ?? "", recordedAtMs);
65
+ }
66
+ export function recordTelegramProbeFailure(store, message, recordedAtMs) {
67
+ store.putStateValue(TELEGRAM_LAST_PROBE_ERROR_AT_MS_KEY, String(recordedAtMs), recordedAtMs);
68
+ store.putStateValue(TELEGRAM_LAST_PROBE_ERROR_MESSAGE_KEY, message, recordedAtMs);
69
+ }
70
+ export function recordTelegramDraftSuccess(store, recordedAtMs) {
71
+ store.putStateValue(TELEGRAM_LAST_DRAFT_SUCCESS_MS_KEY, String(recordedAtMs), recordedAtMs);
72
+ store.putStateValue(TELEGRAM_LAST_DRAFT_ERROR_AT_MS_KEY, "", recordedAtMs);
73
+ store.putStateValue(TELEGRAM_LAST_DRAFT_ERROR_MESSAGE_KEY, "", recordedAtMs);
74
+ }
75
+ export function recordTelegramDraftFailure(store, message, recordedAtMs) {
76
+ store.putStateValue(TELEGRAM_LAST_DRAFT_ERROR_AT_MS_KEY, String(recordedAtMs), recordedAtMs);
77
+ store.putStateValue(TELEGRAM_LAST_DRAFT_ERROR_MESSAGE_KEY, message, recordedAtMs);
78
+ }
79
+ export function recordTelegramPreviewEmit(store, recordedAtMs) {
80
+ store.putStateValue(TELEGRAM_LAST_PREVIEW_EMIT_MS_KEY, String(recordedAtMs), recordedAtMs);
81
+ }
82
+ export function recordTelegramStreamFallback(store, reason, recordedAtMs) {
83
+ store.putStateValue(TELEGRAM_LAST_STREAM_FALLBACK_AT_MS_KEY, String(recordedAtMs), recordedAtMs);
84
+ store.putStateValue(TELEGRAM_LAST_STREAM_FALLBACK_REASON_KEY, reason, recordedAtMs);
85
+ }
86
+ export function readTelegramChatType(store, chatId) {
87
+ const value = readStoredText(store, telegramChatTypeKey(chatId));
88
+ return value === null ? null : value;
89
+ }
90
+ export function recordTelegramChatType(store, chatId, chatType, recordedAtMs) {
91
+ store.putStateValue(telegramChatTypeKey(chatId), chatType, recordedAtMs);
92
+ }
93
+ function telegramChatTypeKey(chatId) {
94
+ return `telegram.chat_type:${chatId}`;
95
+ }
96
+ function readStoredInteger(store, key) {
97
+ const value = store.getStateValue(key);
98
+ if (value === null || value.length === 0) {
99
+ return null;
100
+ }
101
+ const parsed = Number.parseInt(value, 10);
102
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
103
+ throw new Error(`stored ${key} is invalid: ${value}`);
104
+ }
105
+ return parsed;
106
+ }
107
+ function readStoredText(store, key) {
108
+ const value = store.getStateValue(key);
109
+ if (value === null || value.length === 0) {
110
+ return null;
111
+ }
112
+ return value;
113
+ }
114
+ function readStoredFallbackReason(store) {
115
+ const value = readStoredText(store, TELEGRAM_LAST_STREAM_FALLBACK_REASON_KEY);
116
+ if (value === null) {
117
+ return null;
118
+ }
119
+ switch (value) {
120
+ case "non_private_chat":
121
+ case "progressive_handle_unavailable":
122
+ case "draft_send_failed":
123
+ case "preview_not_established":
124
+ return value;
125
+ default:
126
+ throw new Error(`stored ${TELEGRAM_LAST_STREAM_FALLBACK_REASON_KEY} is invalid: ${value}`);
127
+ }
128
+ }
@@ -0,0 +1,80 @@
1
+ export type TelegramApiResponse<Result> = {
2
+ ok: true;
3
+ result: Result;
4
+ } | {
5
+ ok: false;
6
+ description?: string;
7
+ error_code?: number;
8
+ };
9
+ export type TelegramUpdate = {
10
+ update_id: number;
11
+ message?: TelegramMessage;
12
+ callback_query?: TelegramCallbackQuery;
13
+ };
14
+ export type TelegramMessage = {
15
+ message_id: number;
16
+ message_thread_id?: number;
17
+ text?: string;
18
+ caption?: string;
19
+ photo?: TelegramPhotoSize[];
20
+ document?: TelegramDocument;
21
+ from?: TelegramUser;
22
+ chat: TelegramChat;
23
+ };
24
+ export type TelegramCallbackQuery = {
25
+ id: string;
26
+ from: TelegramUser;
27
+ data?: string;
28
+ message?: TelegramCallbackMessage;
29
+ };
30
+ export type TelegramCallbackMessage = {
31
+ message_id: number;
32
+ message_thread_id?: number;
33
+ chat: TelegramChat;
34
+ };
35
+ export type TelegramPhotoSize = {
36
+ file_id: string;
37
+ file_unique_id?: string;
38
+ width: number;
39
+ height: number;
40
+ file_size?: number;
41
+ };
42
+ export type TelegramDocument = {
43
+ file_id: string;
44
+ file_unique_id?: string;
45
+ file_name?: string;
46
+ mime_type?: string;
47
+ file_size?: number;
48
+ };
49
+ export type TelegramUser = {
50
+ id: number;
51
+ is_bot?: boolean;
52
+ username?: string;
53
+ can_join_groups?: boolean;
54
+ can_read_all_group_messages?: boolean;
55
+ supports_inline_queries?: boolean;
56
+ };
57
+ export type TelegramChat = {
58
+ id: number;
59
+ type: string;
60
+ };
61
+ export type TelegramChatType = TelegramChat["type"];
62
+ export type TelegramBotProfile = TelegramUser & {
63
+ is_bot: true;
64
+ };
65
+ export type TelegramFileRecord = {
66
+ file_id: string;
67
+ file_unique_id?: string;
68
+ file_size?: number;
69
+ file_path?: string;
70
+ };
71
+ export type TelegramInlineKeyboardButton = {
72
+ text: string;
73
+ callback_data: string;
74
+ };
75
+ export type TelegramInlineKeyboardMarkup = {
76
+ inline_keyboard: TelegramInlineKeyboardButton[][];
77
+ };
78
+ export type TelegramSentMessage = {
79
+ message_id: number;
80
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { GatewaySessionContext } from "../session/context";
3
+ import type { ChannelSessionSwitcher } from "../session/switcher";
4
+ export declare function createChannelNewSessionTool(switcher: Pick<ChannelSessionSwitcher, "createAndSwitchSession">, sessions: GatewaySessionContext): ToolDefinition;
@@ -0,0 +1,27 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { resolveToolDeliveryTarget } from "./channel-target";
3
+ export function createChannelNewSessionTool(switcher, sessions) {
4
+ return tool({
5
+ description: "Create a fresh OpenCode session for a channel route and switch future inbound messages to it. When called from a channel-backed session, channel, target, and topic default to the current reply target.",
6
+ args: {
7
+ channel: tool.schema.string().min(1).optional(),
8
+ target: tool.schema.string().min(1).optional(),
9
+ topic: tool.schema.string().optional(),
10
+ title: tool.schema.string().optional(),
11
+ },
12
+ async execute(args, context) {
13
+ return formatChannelSessionSwitchResult(await switcher.createAndSwitchSession(resolveToolDeliveryTarget(args, context.sessionID, sessions), args.title ?? null));
14
+ },
15
+ });
16
+ }
17
+ function formatChannelSessionSwitchResult(result) {
18
+ return [
19
+ `channel=${result.channel}`,
20
+ `target=${result.target}`,
21
+ `topic=${result.topic ?? "none"}`,
22
+ `conversation_key=${result.conversationKey}`,
23
+ `previous_session_id=${result.previousSessionId ?? "none"}`,
24
+ `new_session_id=${result.newSessionId}`,
25
+ `effective_on=${result.effectiveOn}`,
26
+ ].join("\n");
27
+ }
@@ -0,0 +1,9 @@
1
+ import type { ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { ChannelFileSendResult } from "../host/file-sender";
3
+ import type { GatewaySessionContext } from "../session/context";
4
+ import { resolveToolDeliveryTarget } from "./channel-target";
5
+ export declare function createChannelSendFileTool(sender: ChannelFileSenderLike, sessions: GatewaySessionContext): ToolDefinition;
6
+ type ChannelFileSenderLike = {
7
+ sendFile(target: ReturnType<typeof resolveToolDeliveryTarget>, filePath: string, caption: string | null): Promise<ChannelFileSendResult>;
8
+ };
9
+ export {};
@@ -0,0 +1,27 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { resolveToolDeliveryTarget } from "./channel-target";
3
+ export function createChannelSendFileTool(sender, sessions) {
4
+ return tool({
5
+ description: "Send a local absolute-path file to a channel target. When called from a channel-backed session, channel, target, and topic default to the current reply target.",
6
+ args: {
7
+ channel: tool.schema.string().min(1).optional(),
8
+ target: tool.schema.string().min(1).optional(),
9
+ topic: tool.schema.string().optional(),
10
+ file_path: tool.schema.string().min(1),
11
+ caption: tool.schema.string().optional(),
12
+ },
13
+ async execute(args, context) {
14
+ return formatChannelFileSendResult(await sender.sendFile(resolveToolDeliveryTarget(args, context.sessionID, sessions), args.file_path, args.caption ?? null));
15
+ },
16
+ });
17
+ }
18
+ function formatChannelFileSendResult(result) {
19
+ return [
20
+ `channel=${result.channel}`,
21
+ `target=${result.target}`,
22
+ `topic=${result.topic ?? "none"}`,
23
+ `file_path=${result.filePath}`,
24
+ `mime_type=${result.mimeType}`,
25
+ `delivery_kind=${result.deliveryKind}`,
26
+ ].join("\n");
27
+ }
@@ -0,0 +1,7 @@
1
+ import type { BindingDeliveryTarget } from "../binding";
2
+ import type { GatewaySessionContext } from "../session/context";
3
+ export declare function resolveToolDeliveryTarget(args: {
4
+ channel?: string;
5
+ target?: string;
6
+ topic?: string;
7
+ }, sessionId: string | null | undefined, sessions: GatewaySessionContext): BindingDeliveryTarget;
@@ -0,0 +1,28 @@
1
+ export function resolveToolDeliveryTarget(args, sessionId, sessions) {
2
+ const fallback = sessionId ? sessions.getDefaultReplyTarget(sessionId) : null;
3
+ const channel = normalizeRequired(args.channel ?? fallback?.channel ?? null, "channel");
4
+ const target = normalizeRequired(args.target ?? fallback?.target ?? null, "target");
5
+ const topic = normalizeOptional(args.topic ?? fallback?.topic ?? null);
6
+ return {
7
+ channel,
8
+ target,
9
+ topic,
10
+ };
11
+ }
12
+ function normalizeRequired(value, field) {
13
+ if (value === null) {
14
+ throw new Error(`${field} is required when the current session has no default reply target`);
15
+ }
16
+ const trimmed = value.trim();
17
+ if (trimmed.length === 0) {
18
+ throw new Error(`${field} must not be empty`);
19
+ }
20
+ return trimmed;
21
+ }
22
+ function normalizeOptional(value) {
23
+ if (value === null) {
24
+ return null;
25
+ }
26
+ const trimmed = value.trim();
27
+ return trimmed.length === 0 ? null : trimmed;
28
+ }
@@ -0,0 +1,3 @@
1
+ import type { ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { GatewayCronRuntime } from "../cron/runtime";
3
+ export declare function createCronListTool(runtime: GatewayCronRuntime): ToolDefinition;
@@ -0,0 +1,34 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { formatUnixMsAsUtc, formatUnixMsInTimeZone } from "./time";
3
+ export function createCronListTool(runtime) {
4
+ return tool({
5
+ description: "List persisted gateway cron jobs. Schedules use cron.timezone or the runtime local time zone.",
6
+ args: {},
7
+ async execute() {
8
+ const jobs = runtime.listJobs();
9
+ if (jobs.length === 0) {
10
+ return "no cron jobs";
11
+ }
12
+ return jobs.map((job) => formatCronJob(job, runtime.timeZone())).join("\n\n");
13
+ },
14
+ });
15
+ }
16
+ function formatCronJob(job, timeZone) {
17
+ return [
18
+ `id=${job.id}`,
19
+ `enabled=${job.enabled}`,
20
+ `schedule=${job.schedule}`,
21
+ `timezone=${timeZone}`,
22
+ `next_run_at_ms=${job.nextRunAtMs}`,
23
+ `next_run_at_local=${formatUnixMsInTimeZone(job.nextRunAtMs, timeZone)}`,
24
+ `next_run_at_utc=${formatUnixMsAsUtc(job.nextRunAtMs)}`,
25
+ `delivery=${formatDelivery(job.deliveryChannel, job.deliveryTarget, job.deliveryTopic)}`,
26
+ `prompt=${job.prompt}`,
27
+ ].join("\n");
28
+ }
29
+ function formatDelivery(channel, target, topic) {
30
+ if (channel === null || target === null) {
31
+ return "none";
32
+ }
33
+ return topic === null ? `${channel}:${target}` : `${channel}:${target}:topic:${topic}`;
34
+ }
@@ -0,0 +1,3 @@
1
+ import type { ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { GatewayCronRuntime } from "../cron/runtime";
3
+ export declare function createCronRemoveTool(runtime: GatewayCronRuntime): ToolDefinition;
@@ -0,0 +1,12 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ export function createCronRemoveTool(runtime) {
3
+ return tool({
4
+ description: "Remove a persisted gateway cron job",
5
+ args: {
6
+ id: tool.schema.string().min(1),
7
+ },
8
+ async execute(args) {
9
+ return runtime.removeJob(args.id) ? `removed=${args.id}` : `missing=${args.id}`;
10
+ },
11
+ });
12
+ }
@@ -0,0 +1,3 @@
1
+ import type { ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { GatewayCronRuntime } from "../cron/runtime";
3
+ export declare function createCronRunTool(runtime: GatewayCronRuntime): ToolDefinition;
@@ -0,0 +1,20 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ export function createCronRunTool(runtime) {
3
+ return tool({
4
+ description: "Run one persisted gateway cron job immediately without changing its schedule",
5
+ args: {
6
+ id: tool.schema.string().min(1),
7
+ },
8
+ async execute(args) {
9
+ return formatRuntimeReport(await runtime.runNow(args.id));
10
+ },
11
+ });
12
+ }
13
+ function formatRuntimeReport(report) {
14
+ return [
15
+ `conversation_key=${report.conversationKey}`,
16
+ `response_text=${report.responseText}`,
17
+ `delivered=${report.delivered}`,
18
+ `recorded_at_ms=${report.recordedAtMs}`,
19
+ ].join("\n");
20
+ }
@@ -0,0 +1,3 @@
1
+ import type { ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { GatewayCronRuntime } from "../cron/runtime";
3
+ export declare function createCronUpsertTool(runtime: GatewayCronRuntime): ToolDefinition;
@@ -0,0 +1,37 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { formatUnixMsAsUtc, formatUnixMsInTimeZone } from "./time";
3
+ export function createCronUpsertTool(runtime) {
4
+ return tool({
5
+ description: "Create or replace a persisted gateway cron job. The schedule uses cron.timezone or the runtime local time zone.",
6
+ args: {
7
+ id: tool.schema.string().min(1),
8
+ schedule: tool.schema.string().min(1),
9
+ prompt: tool.schema.string().min(1),
10
+ enabled: tool.schema.boolean().optional(),
11
+ delivery_channel: tool.schema.string().optional(),
12
+ delivery_target: tool.schema.string().optional(),
13
+ delivery_topic: tool.schema.string().optional(),
14
+ },
15
+ async execute(args) {
16
+ const timeZone = runtime.timeZone();
17
+ const job = runtime.upsertJob({
18
+ id: args.id,
19
+ schedule: args.schedule,
20
+ prompt: args.prompt,
21
+ enabled: args.enabled ?? true,
22
+ deliveryChannel: args.delivery_channel ?? null,
23
+ deliveryTarget: args.delivery_target ?? null,
24
+ deliveryTopic: args.delivery_topic ?? null,
25
+ });
26
+ return [
27
+ `id=${job.id}`,
28
+ `enabled=${job.enabled}`,
29
+ `schedule=${job.schedule}`,
30
+ `timezone=${timeZone}`,
31
+ `next_run_at_ms=${job.nextRunAtMs}`,
32
+ `next_run_at_local=${formatUnixMsInTimeZone(job.nextRunAtMs, timeZone)}`,
33
+ `next_run_at_utc=${formatUnixMsAsUtc(job.nextRunAtMs)}`,
34
+ ].join("\n");
35
+ },
36
+ });
37
+ }
@@ -0,0 +1,3 @@
1
+ import type { ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { GatewayExecutorLike } from "../runtime/executor";
3
+ export declare function createGatewayDispatchCronTool(executor: GatewayExecutorLike): ToolDefinition;
@@ -0,0 +1,33 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ export function createGatewayDispatchCronTool(executor) {
3
+ return tool({
4
+ description: "Dispatch one gateway cron-style prompt through the gateway runtime",
5
+ args: {
6
+ id: tool.schema.string().min(1),
7
+ schedule: tool.schema.string().min(1),
8
+ prompt: tool.schema.string().min(1),
9
+ },
10
+ async execute(args) {
11
+ const report = await executor.dispatchCronJob(toCronJobSpec(args));
12
+ return formatRuntimeReport(report);
13
+ },
14
+ });
15
+ }
16
+ function toCronJobSpec(args) {
17
+ return {
18
+ id: args.id,
19
+ schedule: args.schedule,
20
+ prompt: args.prompt,
21
+ deliveryChannel: null,
22
+ deliveryTarget: null,
23
+ deliveryTopic: null,
24
+ };
25
+ }
26
+ function formatRuntimeReport(report) {
27
+ return [
28
+ `conversation_key=${report.conversationKey}`,
29
+ `response_text=${report.responseText}`,
30
+ `delivered=${report.delivered}`,
31
+ `recorded_at_ms=${report.recordedAtMs}`,
32
+ ].join("\n");
33
+ }
@@ -0,0 +1,3 @@
1
+ import type { ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { GatewayPluginRuntime } from "../gateway";
3
+ export declare function createGatewayStatusTool(runtime: GatewayPluginRuntime): ToolDefinition;