@mono-agent/agent-app 0.3.0 → 0.4.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.
Files changed (186) hide show
  1. package/README.md +24 -10
  2. package/dist/adapter-send-tools-main.d.ts +3 -0
  3. package/dist/adapter-send-tools-main.d.ts.map +1 -0
  4. package/dist/adapter-send-tools-main.js +20 -0
  5. package/dist/adapter-send-tools-main.js.map +1 -0
  6. package/dist/adapter-send-tools.d.ts +125 -0
  7. package/dist/adapter-send-tools.d.ts.map +1 -0
  8. package/dist/adapter-send-tools.js +629 -0
  9. package/dist/adapter-send-tools.js.map +1 -0
  10. package/dist/app-config.d.ts +86 -26
  11. package/dist/app-config.d.ts.map +1 -1
  12. package/dist/app-config.js +284 -90
  13. package/dist/app-config.js.map +1 -1
  14. package/dist/app.d.ts +51 -20
  15. package/dist/app.d.ts.map +1 -1
  16. package/dist/app.js +889 -119
  17. package/dist/app.js.map +1 -1
  18. package/dist/artifact-retention.d.ts +32 -0
  19. package/dist/artifact-retention.d.ts.map +1 -0
  20. package/dist/artifact-retention.js +111 -0
  21. package/dist/artifact-retention.js.map +1 -0
  22. package/dist/audit-runs.d.ts +12 -0
  23. package/dist/audit-runs.d.ts.map +1 -0
  24. package/dist/audit-runs.js +68 -0
  25. package/dist/audit-runs.js.map +1 -0
  26. package/dist/backfill.d.ts +75 -0
  27. package/dist/backfill.d.ts.map +1 -0
  28. package/dist/backfill.js +350 -0
  29. package/dist/backfill.js.map +1 -0
  30. package/dist/background.d.ts +8 -5
  31. package/dist/background.d.ts.map +1 -1
  32. package/dist/background.js +279 -53
  33. package/dist/background.js.map +1 -1
  34. package/dist/broadcast-recorder.d.ts +27 -0
  35. package/dist/broadcast-recorder.d.ts.map +1 -0
  36. package/dist/broadcast-recorder.js +82 -0
  37. package/dist/broadcast-recorder.js.map +1 -0
  38. package/dist/channel-config-view.d.ts +28 -0
  39. package/dist/channel-config-view.d.ts.map +1 -0
  40. package/dist/channel-config-view.js +79 -0
  41. package/dist/channel-config-view.js.map +1 -0
  42. package/dist/channel-gate.d.ts +22 -0
  43. package/dist/channel-gate.d.ts.map +1 -0
  44. package/dist/channel-gate.js +35 -0
  45. package/dist/channel-gate.js.map +1 -0
  46. package/dist/channel-plugins.d.ts +39 -0
  47. package/dist/channel-plugins.d.ts.map +1 -0
  48. package/dist/channel-plugins.js +346 -0
  49. package/dist/channel-plugins.js.map +1 -0
  50. package/dist/channels.d.ts +62 -66
  51. package/dist/channels.d.ts.map +1 -1
  52. package/dist/channels.js +850 -120
  53. package/dist/channels.js.map +1 -1
  54. package/dist/cli.d.ts +103 -4
  55. package/dist/cli.d.ts.map +1 -1
  56. package/dist/cli.js +984 -112
  57. package/dist/cli.js.map +1 -1
  58. package/dist/config-reference.d.ts +24 -0
  59. package/dist/config-reference.d.ts.map +1 -0
  60. package/dist/config-reference.js +580 -0
  61. package/dist/config-reference.js.map +1 -0
  62. package/dist/configured-agent.d.ts +104 -0
  63. package/dist/configured-agent.d.ts.map +1 -0
  64. package/dist/configured-agent.js +578 -0
  65. package/dist/configured-agent.js.map +1 -0
  66. package/dist/consumer-contract.d.ts +36 -0
  67. package/dist/consumer-contract.d.ts.map +1 -0
  68. package/dist/consumer-contract.js +290 -0
  69. package/dist/consumer-contract.js.map +1 -0
  70. package/dist/doctor.d.ts +17 -0
  71. package/dist/doctor.d.ts.map +1 -1
  72. package/dist/doctor.js +392 -95
  73. package/dist/doctor.js.map +1 -1
  74. package/dist/index.d.ts +14 -6
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +7 -3
  77. package/dist/index.js.map +1 -1
  78. package/dist/init.d.ts +23 -7
  79. package/dist/init.d.ts.map +1 -1
  80. package/dist/init.js +91 -26
  81. package/dist/init.js.map +1 -1
  82. package/dist/install-skill.d.ts +1 -1
  83. package/dist/install-skill.js +2 -2
  84. package/dist/install-skill.js.map +1 -1
  85. package/dist/interaction-bridge.d.ts +72 -0
  86. package/dist/interaction-bridge.d.ts.map +1 -0
  87. package/dist/interaction-bridge.js +354 -0
  88. package/dist/interaction-bridge.js.map +1 -0
  89. package/dist/launchd.d.ts +0 -2
  90. package/dist/launchd.d.ts.map +1 -1
  91. package/dist/launchd.js +0 -2
  92. package/dist/launchd.js.map +1 -1
  93. package/dist/memory-command.d.ts +10 -0
  94. package/dist/memory-command.d.ts.map +1 -0
  95. package/dist/memory-command.js +649 -0
  96. package/dist/memory-command.js.map +1 -0
  97. package/dist/memory-recall-main.d.ts +3 -0
  98. package/dist/memory-recall-main.d.ts.map +1 -0
  99. package/dist/memory-recall-main.js +30 -0
  100. package/dist/memory-recall-main.js.map +1 -0
  101. package/dist/memory-recall.d.ts +147 -0
  102. package/dist/memory-recall.d.ts.map +1 -0
  103. package/dist/memory-recall.js +345 -0
  104. package/dist/memory-recall.js.map +1 -0
  105. package/dist/memory-rituals.d.ts +8 -11
  106. package/dist/memory-rituals.d.ts.map +1 -1
  107. package/dist/memory-rituals.js +26 -25
  108. package/dist/memory-rituals.js.map +1 -1
  109. package/dist/metrics.d.ts +13 -0
  110. package/dist/metrics.d.ts.map +1 -0
  111. package/dist/metrics.js +105 -0
  112. package/dist/metrics.js.map +1 -0
  113. package/dist/notify-destinations.d.ts +23 -0
  114. package/dist/notify-destinations.d.ts.map +1 -0
  115. package/dist/notify-destinations.js +79 -0
  116. package/dist/notify-destinations.js.map +1 -0
  117. package/dist/posted-message-index.d.ts +61 -0
  118. package/dist/posted-message-index.d.ts.map +1 -0
  119. package/dist/posted-message-index.js +178 -0
  120. package/dist/posted-message-index.js.map +1 -0
  121. package/dist/proactive-notify.d.ts +32 -0
  122. package/dist/proactive-notify.d.ts.map +1 -0
  123. package/dist/proactive-notify.js +62 -0
  124. package/dist/proactive-notify.js.map +1 -0
  125. package/dist/recipes/base.d.ts +15 -0
  126. package/dist/recipes/base.d.ts.map +1 -0
  127. package/dist/recipes/base.js +51 -0
  128. package/dist/recipes/base.js.map +1 -0
  129. package/dist/recipes/catalog.d.ts +4 -0
  130. package/dist/recipes/catalog.d.ts.map +1 -0
  131. package/dist/recipes/catalog.js +509 -0
  132. package/dist/recipes/catalog.js.map +1 -0
  133. package/dist/recipes/index.d.ts +11 -0
  134. package/dist/recipes/index.d.ts.map +1 -0
  135. package/dist/recipes/index.js +14 -0
  136. package/dist/recipes/index.js.map +1 -0
  137. package/dist/recipes/types.d.ts +70 -0
  138. package/dist/recipes/types.d.ts.map +1 -0
  139. package/dist/recipes/types.js +15 -0
  140. package/dist/recipes/types.js.map +1 -0
  141. package/dist/request-model-override.d.ts +67 -0
  142. package/dist/request-model-override.d.ts.map +1 -0
  143. package/dist/request-model-override.js +96 -0
  144. package/dist/request-model-override.js.map +1 -0
  145. package/dist/runs-health.d.ts +22 -0
  146. package/dist/runs-health.d.ts.map +1 -0
  147. package/dist/runs-health.js +144 -0
  148. package/dist/runs-health.js.map +1 -0
  149. package/dist/seen-conversations.d.ts +22 -0
  150. package/dist/seen-conversations.d.ts.map +1 -0
  151. package/dist/seen-conversations.js +120 -0
  152. package/dist/seen-conversations.js.map +1 -0
  153. package/dist/sessions.d.ts +21 -0
  154. package/dist/sessions.d.ts.map +1 -0
  155. package/dist/sessions.js +45 -0
  156. package/dist/sessions.js.map +1 -0
  157. package/dist/setup.d.ts +29 -0
  158. package/dist/setup.d.ts.map +1 -0
  159. package/dist/setup.js +97 -0
  160. package/dist/setup.js.map +1 -0
  161. package/dist/trigger-overrides.d.ts +17 -0
  162. package/dist/trigger-overrides.d.ts.map +1 -0
  163. package/dist/trigger-overrides.js +30 -0
  164. package/dist/trigger-overrides.js.map +1 -0
  165. package/dist/tui-command.d.ts +56 -0
  166. package/dist/tui-command.d.ts.map +1 -0
  167. package/dist/tui-command.js +160 -0
  168. package/dist/tui-command.js.map +1 -0
  169. package/dist/ui.d.ts +54 -0
  170. package/dist/ui.d.ts.map +1 -0
  171. package/dist/ui.js +157 -0
  172. package/dist/ui.js.map +1 -0
  173. package/dist/web-command.d.ts +39 -0
  174. package/dist/web-command.d.ts.map +1 -0
  175. package/dist/web-command.js +151 -0
  176. package/dist/web-command.js.map +1 -0
  177. package/package.json +22 -17
  178. package/schema/mono-agent.config.schema.json +1686 -0
  179. package/skills/mono-agent-composer/SKILL.md +38 -12
  180. package/skills/mono-agent-composer/agents/openai.yaml +4 -0
  181. package/skills/mono-agent-composer/references/config-blueprint.md +84 -31
  182. package/skills/mono-agent-composer/references/discovery-questions.md +49 -36
  183. package/skills/mono-agent-composer/references/feature-coverage.md +26 -19
  184. package/skills/mono-agent-composer/references/package-map.md +20 -22
  185. package/skills/mono-agent-composer/references/playbooks.md +209 -0
  186. package/skills/mono-agent-composer/references/validation.md +14 -5
@@ -0,0 +1,629 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { basename, resolve as resolvePath } from "node:path";
3
+ import process from "node:process";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import * as z from "zod/v4";
7
+ import { appendPostedMessage } from "./posted-message-index.js";
8
+ let slackModule;
9
+ let telegramModule;
10
+ const loadSlackModule = async () => (slackModule ??= await import("@mono-agent/slack-adapter"));
11
+ const loadTelegramModule = async () => (telegramModule ??= await import("@mono-agent/telegram-adapter"));
12
+ /**
13
+ * Model-visible send tools for explicitly allowed, already-enabled communication adapters.
14
+ *
15
+ * This mirrors `memory_recall`: agent-app injects a stdio MCP server through
16
+ * per-request runtime options. The adapter config remains the source of truth:
17
+ * if Slack or Telegram is disabled, invalid, blocked by tool policy, or lacks an
18
+ * allowed destination policy, the corresponding send tool is not exposed. When
19
+ * exposed, each tool enforces the same adapter allowlist before calling the
20
+ * adapter-owned sender.
21
+ */
22
+ export const ADAPTER_SEND_TOOLS_MCP_SERVER_NAME = "mono-agent-adapter-send";
23
+ export async function resolveAdapterSendToolsSettings(input, options = {}) {
24
+ const telegramSendAllowed = isAdapterToolAllowed("telegram_send_message", options);
25
+ const telegramAskAllowed = isAdapterToolAllowed("telegram_ask", options);
26
+ const telegramDocumentAllowed = isAdapterToolAllowed("telegram_send_document", options);
27
+ const telegramPhotoAllowed = isAdapterToolAllowed("telegram_send_photo", options);
28
+ const telegramAnyAllowed = telegramSendAllowed || telegramAskAllowed || telegramDocumentAllowed || telegramPhotoAllowed;
29
+ const [slack, telegram] = await Promise.all([
30
+ isAdapterToolAllowed("slack_send_message", options)
31
+ ? resolveSlackSendToolSettings(input, options)
32
+ : undefined,
33
+ telegramAnyAllowed
34
+ ? resolveTelegramSendToolSettings(input, options, {
35
+ send: telegramSendAllowed,
36
+ ask: telegramAskAllowed,
37
+ document: telegramDocumentAllowed,
38
+ photo: telegramPhotoAllowed,
39
+ })
40
+ : undefined,
41
+ ]);
42
+ const askUser = isAdapterToolAllowed("ask_user", options)
43
+ ? resolveAskUserToolSettings(input.env)
44
+ : undefined;
45
+ if (slack === undefined && telegram === undefined && askUser === undefined) {
46
+ return undefined;
47
+ }
48
+ return {
49
+ ...(slack === undefined ? {} : { slack }),
50
+ ...(telegram === undefined ? {} : { telegram }),
51
+ ...(askUser === undefined ? {} : { askUser }),
52
+ };
53
+ }
54
+ /**
55
+ * ask_user is available only when the app exported a live interaction bridge
56
+ * into the environment (URL + bearer token). The producing conversation id is
57
+ * per-request env, present in the spawned child.
58
+ */
59
+ function resolveAskUserToolSettings(env) {
60
+ const bridgeUrl = optionalString(env.MONO_AGENT_INTERACTION_BRIDGE_URL);
61
+ const bridgeToken = optionalString(env.MONO_AGENT_INTERACTION_BRIDGE_TOKEN);
62
+ if (bridgeUrl === undefined || bridgeToken === undefined) {
63
+ return undefined;
64
+ }
65
+ const timeoutRaw = Number(optionalString(env.MONO_AGENT_ASK_USER_TIMEOUT_MS));
66
+ const timeoutMs = Number.isFinite(timeoutRaw) && timeoutRaw >= 1000 ? timeoutRaw : 600_000;
67
+ const conversationId = optionalString(env.MONO_AGENT_ADAPTER_TOOLS_PRODUCING_CONVERSATION_ID);
68
+ return {
69
+ bridgeUrl,
70
+ bridgeToken,
71
+ timeoutMs,
72
+ ...(conversationId === undefined ? {} : { conversationId }),
73
+ };
74
+ }
75
+ export function adapterSendToolNames(settings) {
76
+ const names = [];
77
+ if (settings.slack !== undefined) {
78
+ names.push("slack_send_message");
79
+ }
80
+ if (settings.telegram?.tools.send === true) {
81
+ names.push("telegram_send_message");
82
+ }
83
+ if (settings.telegram?.tools.ask === true) {
84
+ names.push("telegram_ask");
85
+ }
86
+ if (settings.telegram?.tools.document === true) {
87
+ names.push("telegram_send_document");
88
+ }
89
+ if (settings.telegram?.tools.photo === true) {
90
+ names.push("telegram_send_photo");
91
+ }
92
+ if (settings.askUser !== undefined) {
93
+ names.push("ask_user");
94
+ }
95
+ return names;
96
+ }
97
+ /**
98
+ * Public re-export of the per-tool allow check so callers (e.g. the Telegram
99
+ * channel driver) can gate behavior on whether a specific adapter send tool is
100
+ * permitted by `tools.allowedTools`/`disallowedTools`, matching this module's policy.
101
+ */
102
+ export function isAdapterSendToolAllowed(name, policy) {
103
+ return isAdapterToolAllowed(name, policy);
104
+ }
105
+ export function adapterSendToolsMcpEnv(configPath, allowedTools, context) {
106
+ return {
107
+ MONO_AGENT_ADAPTER_TOOLS_CONFIG_PATH: configPath,
108
+ MONO_AGENT_ADAPTER_TOOLS_ALLOWED_TOOLS: JSON.stringify(allowedTools),
109
+ ...(context?.conversationId === undefined
110
+ ? {}
111
+ : { MONO_AGENT_ADAPTER_TOOLS_PRODUCING_CONVERSATION_ID: context.conversationId }),
112
+ ...(context?.indexPath === undefined
113
+ ? {}
114
+ : { MONO_AGENT_ADAPTER_TOOLS_POST_INDEX_PATH: context.indexPath }),
115
+ };
116
+ }
117
+ export function adapterSendToolsChildConfigFromEnv(env, cwd) {
118
+ const configPath = optionalString(env.MONO_AGENT_ADAPTER_TOOLS_CONFIG_PATH);
119
+ if (configPath === undefined) {
120
+ throw new Error("adapter-send-tools: missing required environment (MONO_AGENT_ADAPTER_TOOLS_CONFIG_PATH).");
121
+ }
122
+ const conversationId = optionalString(env.MONO_AGENT_ADAPTER_TOOLS_PRODUCING_CONVERSATION_ID);
123
+ const indexPath = optionalString(env.MONO_AGENT_ADAPTER_TOOLS_POST_INDEX_PATH);
124
+ // Both must be present to index; either alone is a misconfiguration we simply skip.
125
+ const indexing = conversationId !== undefined && indexPath !== undefined ? { conversationId, indexPath } : undefined;
126
+ return {
127
+ input: { env, cwd, configPath },
128
+ allowedTools: parseAllowedToolNames(env.MONO_AGENT_ADAPTER_TOOLS_ALLOWED_TOOLS),
129
+ ...(indexing === undefined ? {} : { indexing }),
130
+ };
131
+ }
132
+ export function adapterSendToolsMcpServerSpec(configPath, cwd, allowedTools, context) {
133
+ return {
134
+ type: "stdio",
135
+ command: process.execPath,
136
+ args: [fileURLToPath(new URL("./adapter-send-tools-main.js", import.meta.url))],
137
+ cwd,
138
+ env: adapterSendToolsMcpEnv(configPath, allowedTools, context),
139
+ };
140
+ }
141
+ /**
142
+ * Per-request runtime extension that injects the adapter-send stdio MCP server. It
143
+ * reads the request's producing conversationId and, when an `indexPath` is
144
+ * configured, forwards both to the child so a `slack_send_message` post is linked
145
+ * back to this conversation (so a later in-thread reply resumes it).
146
+ */
147
+ export function createAdapterSendToolsRuntimeExtension(configPath, cwd, allowedTools, indexPath) {
148
+ return async (input) => {
149
+ const conversationId = input?.request?.conversationId;
150
+ const hasConversation = typeof conversationId === "string" && conversationId.trim().length > 0;
151
+ // The conversation id is forwarded whenever known — ask_user targets it even
152
+ // without indexing; the index path additionally enables posted-message links.
153
+ const context = hasConversation
154
+ ? { conversationId, ...(indexPath === undefined ? {} : { indexPath }) }
155
+ : undefined;
156
+ return {
157
+ runtimeOptions: {
158
+ mcpServers: {
159
+ [ADAPTER_SEND_TOOLS_MCP_SERVER_NAME]: adapterSendToolsMcpServerSpec(configPath, cwd, allowedTools, context),
160
+ },
161
+ },
162
+ cleanup: async () => { },
163
+ };
164
+ };
165
+ }
166
+ export async function createAdapterSendToolsClients(settings) {
167
+ const slack = settings.slack === undefined
168
+ ? undefined
169
+ : new (await loadSlackModule()).SlackWebApiClient({ botToken: settings.slack.botToken });
170
+ const telegram = settings.telegram === undefined
171
+ ? undefined
172
+ : (await loadTelegramModule()).createTelegramMessageSender(settings.telegram.botToken, {
173
+ ...(settings.telegram.apiRoot === undefined ? {} : { apiRoot: settings.telegram.apiRoot }),
174
+ });
175
+ return {
176
+ ...(slack === undefined ? {} : { slack }),
177
+ ...(telegram === undefined ? {} : { telegram }),
178
+ };
179
+ }
180
+ export async function createAdapterSendToolsServer(settings, clients, indexing) {
181
+ const server = new McpServer({ name: "agent-adapter-send-tools", version: "0.3.0" });
182
+ if (settings.slack !== undefined && clients.slack !== undefined) {
183
+ registerSlackSendTool(server, settings.slack, clients.slack, indexing);
184
+ }
185
+ if (settings.telegram !== undefined && clients.telegram !== undefined) {
186
+ // Loaded once here (not per-register-call) because registerTelegramAskTool
187
+ // needs TELEGRAM_ASK_MAX_OPTIONS synchronously while building its zod
188
+ // schema, at registration time — not deferred into the request handler.
189
+ const adapter = await loadTelegramModule();
190
+ if (settings.telegram.tools.send) {
191
+ registerTelegramSendTool(server, settings.telegram, clients.telegram);
192
+ }
193
+ if (settings.telegram.tools.ask) {
194
+ registerTelegramAskTool(server, settings.telegram, clients.telegram, adapter);
195
+ }
196
+ if (settings.telegram.tools.document) {
197
+ registerTelegramSendFileTool(server, settings.telegram, clients.telegram, "document", adapter);
198
+ }
199
+ if (settings.telegram.tools.photo) {
200
+ registerTelegramSendFileTool(server, settings.telegram, clients.telegram, "photo", adapter);
201
+ }
202
+ }
203
+ // ask_user needs a target conversation; the parent app process resolves the
204
+ // settings without one (for tool-name gating) and must not register the tool.
205
+ if (settings.askUser?.conversationId !== undefined) {
206
+ registerAskUserTool(server, { ...settings.askUser, conversationId: settings.askUser.conversationId });
207
+ }
208
+ return server;
209
+ }
210
+ /** Long-poll wait per bridge request; the overall wait is bounded server-side by the ask's timeout. */
211
+ const ASK_USER_POLL_WAIT_MS = 20_000;
212
+ function registerAskUserTool(server, settings) {
213
+ server.registerTool("ask_user", {
214
+ title: "Ask the user and wait",
215
+ description: "Ask the user ONE consolidated free-text question on the current conversation and WAIT for their reply (blocking, up to the configured timeout — default 10 minutes). Returns the user's answer text. Consolidate everything you need into a single question — a second concurrent ask fails. If the wait times out, the tool returns without an answer and the user's late reply will arrive as their next message: finish the turn gracefully (proceed with sensible defaults and say what you assumed).",
216
+ inputSchema: {
217
+ question: z.string().min(1).describe("The full question to show the user (plain text, no markdown)."),
218
+ },
219
+ }, async (args, extra) => {
220
+ const created = await askBridgeRequest(settings, "POST", "/v1/asks", {
221
+ conversationId: settings.conversationId,
222
+ question: args.question,
223
+ timeoutMs: settings.timeoutMs,
224
+ });
225
+ if (created.status === 409) {
226
+ return askUserResult("A question is already pending for the user. Wait for its answer instead of asking again.", { answered: false, reason: "already_pending" });
227
+ }
228
+ if (created.status === 501) {
229
+ return askUserResult("This conversation's channel does not support interactive asks. Ask your question in your final reply instead.", { answered: false, reason: "unsupported_channel" });
230
+ }
231
+ if (created.status !== 201 || typeof created.body.askId !== "string") {
232
+ throw new Error(`ask_user: the interaction bridge rejected the ask (HTTP ${String(created.status)}).`);
233
+ }
234
+ const askId = created.body.askId;
235
+ const startedMs = Date.now();
236
+ for (;;) {
237
+ const poll = await askBridgeRequest(settings, "GET", `/v1/asks/${encodeURIComponent(askId)}?waitMs=${String(ASK_USER_POLL_WAIT_MS)}`);
238
+ if (poll.status !== 200) {
239
+ throw new Error(`ask_user: lost the pending ask (HTTP ${String(poll.status)}).`);
240
+ }
241
+ // Keep-alive: progress notifications reset the runtime's MCP inactivity
242
+ // timeout so a long human wait cannot kill the tool call.
243
+ await sendAskProgress(extra, Math.round((Date.now() - startedMs) / 1000));
244
+ const status = poll.body.status;
245
+ if (status === "answered" && typeof poll.body.answer === "string") {
246
+ return askUserResult(`The user answered:\n${poll.body.answer}`, {
247
+ answered: true,
248
+ answer: poll.body.answer,
249
+ });
250
+ }
251
+ if (status === "expired") {
252
+ return askUserResult("The user did not answer within the wait window. Their reply will arrive as their next message — wrap up this turn gracefully (proceed with sensible defaults and say what you assumed).", { answered: false, reason: "timeout" });
253
+ }
254
+ if (status === "cancelled") {
255
+ return askUserResult("The user cancelled the current run. Stop this task.", {
256
+ answered: false,
257
+ reason: "cancelled",
258
+ });
259
+ }
260
+ }
261
+ });
262
+ }
263
+ function askUserResult(text, structured) {
264
+ return {
265
+ content: [{ type: "text", text }],
266
+ structuredContent: { ok: true, ...structured },
267
+ };
268
+ }
269
+ async function askBridgeRequest(settings, method, path, body) {
270
+ const response = await fetch(new URL(path, settings.bridgeUrl), {
271
+ method,
272
+ headers: {
273
+ authorization: `Bearer ${settings.bridgeToken}`,
274
+ ...(body === undefined ? {} : { "content-type": "application/json" }),
275
+ },
276
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
277
+ });
278
+ let parsed;
279
+ try {
280
+ parsed = await response.json();
281
+ }
282
+ catch {
283
+ parsed = {};
284
+ }
285
+ return {
286
+ status: response.status,
287
+ body: typeof parsed === "object" && parsed !== null ? parsed : {},
288
+ };
289
+ }
290
+ async function sendAskProgress(extra, elapsedSeconds) {
291
+ const handler = extra;
292
+ const progressToken = handler._meta?.progressToken;
293
+ if (progressToken === undefined || handler.sendNotification === undefined) {
294
+ return;
295
+ }
296
+ try {
297
+ await handler.sendNotification({
298
+ method: "notifications/progress",
299
+ params: {
300
+ progressToken,
301
+ progress: elapsedSeconds,
302
+ message: `waiting for the user's reply (${String(elapsedSeconds)}s)`,
303
+ },
304
+ });
305
+ }
306
+ catch {
307
+ // Keep-alive only; a lost notification must never fail the ask.
308
+ }
309
+ }
310
+ function registerSlackSendTool(server, settings, client, indexing) {
311
+ server.registerTool("slack_send_message", {
312
+ title: "Send Slack message",
313
+ description: "Send a message to an allowed Slack channel or DM ID using the configured Slack adapter bot token.",
314
+ inputSchema: {
315
+ channel: z.string().min(1).describe("Slack channel or DM ID, e.g. C123456 or D123456."),
316
+ text: z.string().min(1).describe("Message text to send."),
317
+ thread_ts: z.string().min(1).optional().describe("Optional Slack thread timestamp to reply in."),
318
+ mrkdwn: z.boolean().optional().describe("Whether Slack mrkdwn formatting is enabled."),
319
+ unfurl_links: z.boolean().optional().describe("Whether Slack should unfurl links."),
320
+ unfurl_media: z.boolean().optional().describe("Whether Slack should unfurl media."),
321
+ },
322
+ }, async (args) => {
323
+ assertSlackChannelAllowed(settings, args.channel);
324
+ const result = await client.chatPostMessage({
325
+ channel: args.channel.trim(),
326
+ text: args.text,
327
+ ...(args.thread_ts === undefined ? {} : { thread_ts: args.thread_ts }),
328
+ ...(args.mrkdwn === undefined ? {} : { mrkdwn: args.mrkdwn }),
329
+ ...(args.unfurl_links === undefined ? {} : { unfurl_links: args.unfurl_links }),
330
+ ...(args.unfurl_media === undefined ? {} : { unfurl_media: args.unfurl_media }),
331
+ });
332
+ // Link the posted message back to this conversation so an in-thread reply can
333
+ // resume it. Best-effort: appendPostedMessage never throws, so a failed index
334
+ // write can never fail the send.
335
+ if (indexing !== undefined) {
336
+ await appendPostedMessage(indexing.indexPath, {
337
+ channelId: result.channel,
338
+ ts: result.ts,
339
+ conversationId: indexing.conversationId,
340
+ });
341
+ }
342
+ return {
343
+ content: [{ type: "text", text: `Sent Slack message to ${result.channel} at ${result.ts}.` }],
344
+ structuredContent: { ok: true, channel: result.channel, ts: result.ts },
345
+ };
346
+ });
347
+ }
348
+ function registerTelegramSendTool(server, settings, client) {
349
+ server.registerTool("telegram_send_message", {
350
+ title: "Send Telegram message",
351
+ description: "Send a message to an allowed Telegram chat using the configured Telegram adapter bot token.",
352
+ inputSchema: {
353
+ chat_id: z.union([z.string().min(1), z.number().int()]).describe("Telegram chat id from the adapter allowlist."),
354
+ text: z.string().min(1).describe("Message text to send."),
355
+ parse_mode: z.string().min(1).optional().describe("Optional Telegram parse mode, e.g. MarkdownV2 or HTML."),
356
+ reply_to_message_id: z.number().int().optional().describe("Optional message id to reply to."),
357
+ disable_web_page_preview: z.boolean().optional().describe("Disable Telegram link previews."),
358
+ },
359
+ }, async (args) => {
360
+ assertTelegramChatAllowed(settings, args.chat_id);
361
+ const result = await client.sendMessage({
362
+ chat_id: args.chat_id,
363
+ text: args.text,
364
+ ...(args.parse_mode === undefined ? {} : { parse_mode: args.parse_mode }),
365
+ ...(args.reply_to_message_id === undefined ? {} : { reply_to_message_id: args.reply_to_message_id }),
366
+ ...(args.disable_web_page_preview === undefined ? {} : { disable_web_page_preview: args.disable_web_page_preview }),
367
+ });
368
+ return {
369
+ content: [{ type: "text", text: `Sent Telegram message ${result.message_id} to ${String(result.chat.id)}.` }],
370
+ structuredContent: { ok: true, chat_id: result.chat.id, message_id: result.message_id },
371
+ };
372
+ });
373
+ }
374
+ function registerTelegramAskTool(server, settings, client, adapter) {
375
+ server.registerTool("telegram_ask", {
376
+ title: "Ask via Telegram buttons",
377
+ description: "Ask the owner a question in an allowed Telegram chat with tappable inline-keyboard options (e.g. a confirmation or a multiple-choice). This tool returns immediately after posting the question; it does NOT wait for the answer. When the user taps a button, their choice arrives as a new message on the same conversation, so continue on that next turn.",
378
+ inputSchema: {
379
+ chat_id: z.union([z.string().min(1), z.number().int()]).describe("Telegram chat id from the adapter allowlist."),
380
+ question: z.string().min(1).describe("The question to show above the buttons."),
381
+ options: z
382
+ .array(z.string().min(1).max(100))
383
+ .min(2)
384
+ .max(adapter.TELEGRAM_ASK_MAX_OPTIONS)
385
+ .describe("Button labels (2–8). The label the user taps is echoed back as a new message."),
386
+ note: z.string().min(1).optional().describe("Optional extra context shown beneath the question."),
387
+ },
388
+ }, async (args) => {
389
+ assertTelegramChatAllowed(settings, args.chat_id);
390
+ const inlineKeyboard = args.options.map((label, index) => [
391
+ { text: label, callback_data: adapter.telegramAskCallbackData(index) },
392
+ ]);
393
+ const text = args.note === undefined ? args.question : `${args.question}\n\n${args.note}`;
394
+ const result = await client.sendMessage({
395
+ chat_id: args.chat_id,
396
+ text,
397
+ reply_markup: { inline_keyboard: inlineKeyboard },
398
+ });
399
+ return {
400
+ content: [
401
+ {
402
+ type: "text",
403
+ text: `Posted question ${result.message_id} to ${String(result.chat.id)} with ${String(args.options.length)} options. The user's choice will arrive as a new message.`,
404
+ },
405
+ ],
406
+ structuredContent: {
407
+ ok: true,
408
+ chat_id: result.chat.id,
409
+ message_id: result.message_id,
410
+ options: args.options,
411
+ },
412
+ };
413
+ });
414
+ }
415
+ /**
416
+ * Register `telegram_send_document` / `telegram_send_photo`. Both accept the file
417
+ * as base64 `data` (with a `filename`) OR a workspace `path` (filename derived
418
+ * from the basename), enforce the adapter allowlist, and bound the size to the
419
+ * adapter's inbound cap before uploading via the adapter-owned sender.
420
+ */
421
+ function registerTelegramSendFileTool(server, settings, client, kind, adapter) {
422
+ const toolName = kind === "document" ? "telegram_send_document" : "telegram_send_photo";
423
+ server.registerTool(toolName, {
424
+ title: kind === "document" ? "Send Telegram document" : "Send Telegram photo",
425
+ description: kind === "document"
426
+ ? "Upload and send a file (document) to an allowed Telegram chat. Provide the bytes as base64 `data` with a `filename`, or a workspace `path` (preferred — with a self-hosted Bot API server a `path` upload streams from disk with no size buffering, up to the configured cap)."
427
+ : "Upload and send an image (photo, shown inline) to an allowed Telegram chat. Provide the bytes as base64 `data`, or a workspace `path`.",
428
+ inputSchema: {
429
+ chat_id: z.union([z.string().min(1), z.number().int()]).describe("Telegram chat id from the adapter allowlist."),
430
+ data: z.string().min(1).optional().describe("Base64-encoded file bytes. Provide this or `path`."),
431
+ path: z.string().min(1).optional().describe("Path to a file to upload (resolved against the agent working dir). Provide this or `data`."),
432
+ filename: z.string().min(1).optional().describe("Filename to present. Required with `data` for a document; derived from `path` otherwise."),
433
+ caption: z.string().min(1).optional().describe("Optional caption shown with the file."),
434
+ },
435
+ }, async (args) => {
436
+ assertTelegramChatAllowed(settings, args.chat_id);
437
+ const maxUploadBytes = settings.maxUploadBytes ?? adapter.DEFAULT_ATTACHMENT_MAX_BYTES;
438
+ // file:// fast path: a --local self-hosted server reads the file straight
439
+ // from disk, so a path upload needs no buffering at any size — only a
440
+ // stat-level cap check. Falls back once to the buffered path when the
441
+ // server rejects the URI (e.g. a non---local self-hosted root).
442
+ if (kind === "document" && settings.apiRoot !== undefined && args.path !== undefined && args.data === undefined) {
443
+ const resolved = resolvePath(process.cwd(), args.path);
444
+ const info = await stat(resolved);
445
+ if (!info.isFile() || info.size === 0) {
446
+ throw new Error("file is empty or not a regular file.");
447
+ }
448
+ if (info.size > maxUploadBytes) {
449
+ throw new Error(`file exceeds the ${String(maxUploadBytes)}-byte upload cap.`);
450
+ }
451
+ try {
452
+ const sent = await client.sendDocument({
453
+ chat_id: args.chat_id,
454
+ document: pathToFileURL(resolved).href,
455
+ ...(args.caption === undefined ? {} : { caption: args.caption }),
456
+ });
457
+ const name = basename(resolved);
458
+ return {
459
+ content: [{ type: "text", text: `Sent ${kind} ${sent.message_id} (${name}) to ${String(sent.chat.id)}.` }],
460
+ structuredContent: { ok: true, chat_id: sent.chat.id, message_id: sent.message_id, filename: name },
461
+ };
462
+ }
463
+ catch (error) {
464
+ // Retry buffered exactly once; rethrow anything that isn't a server-side rejection.
465
+ if (error.kind !== "telegram") {
466
+ throw error;
467
+ }
468
+ }
469
+ }
470
+ const { bytes, filename } = await resolveTelegramFileBytes({
471
+ data: args.data,
472
+ path: args.path,
473
+ filename: args.filename,
474
+ requireFilename: kind === "document",
475
+ maxBytes: maxUploadBytes,
476
+ });
477
+ const result = kind === "document"
478
+ ? await client.sendDocument({
479
+ chat_id: args.chat_id,
480
+ document: bytes,
481
+ filename,
482
+ ...(args.caption === undefined ? {} : { caption: args.caption }),
483
+ })
484
+ : await client.sendPhoto({
485
+ chat_id: args.chat_id,
486
+ photo: bytes,
487
+ filename,
488
+ ...(args.caption === undefined ? {} : { caption: args.caption }),
489
+ });
490
+ return {
491
+ content: [{ type: "text", text: `Sent ${kind} ${result.message_id} (${filename}) to ${String(result.chat.id)}.` }],
492
+ structuredContent: { ok: true, chat_id: result.chat.id, message_id: result.message_id, filename },
493
+ };
494
+ });
495
+ }
496
+ /** Resolve the upload bytes + filename from exactly one of base64 `data` or a `path`. */
497
+ async function resolveTelegramFileBytes(input) {
498
+ const hasData = input.data !== undefined;
499
+ const hasPath = input.path !== undefined;
500
+ if (hasData === hasPath) {
501
+ throw new Error("provide exactly one of `data` (base64) or `path`.");
502
+ }
503
+ let bytes;
504
+ let filename;
505
+ if (hasData) {
506
+ bytes = new Uint8Array(Buffer.from(input.data, "base64"));
507
+ if (input.filename === undefined) {
508
+ if (input.requireFilename) {
509
+ throw new Error("`filename` is required when sending a document by `data`.");
510
+ }
511
+ filename = "image";
512
+ }
513
+ else {
514
+ filename = input.filename;
515
+ }
516
+ }
517
+ else {
518
+ const resolved = resolvePath(process.cwd(), input.path);
519
+ bytes = new Uint8Array(await readFile(resolved));
520
+ filename = input.filename ?? basename(resolved);
521
+ }
522
+ if (bytes.byteLength === 0) {
523
+ throw new Error("file is empty.");
524
+ }
525
+ if (bytes.byteLength > input.maxBytes) {
526
+ throw new Error(`file exceeds the ${String(input.maxBytes)}-byte upload cap.`);
527
+ }
528
+ return { bytes, filename };
529
+ }
530
+ async function resolveSlackSendToolSettings(input, options) {
531
+ try {
532
+ const adapter = await loadSlackModule();
533
+ const config = await adapter.loadSlackAdapterConfig({ env: input.env, jsonPath: input.configPath });
534
+ if (!config.enabled) {
535
+ return undefined;
536
+ }
537
+ return {
538
+ botToken: config.botToken,
539
+ allowedChannelIds: config.allowedChannelIds.map(normalizeSlackChannelId),
540
+ allowAllChannels: config.allowAllChannels,
541
+ };
542
+ }
543
+ catch (error) {
544
+ options.logger?.warn?.("Slack send tool skipped because Slack adapter config is unavailable.", {
545
+ reason: reasonOf(error),
546
+ });
547
+ return undefined;
548
+ }
549
+ }
550
+ async function resolveTelegramSendToolSettings(input, options, tools) {
551
+ try {
552
+ const adapter = await loadTelegramModule();
553
+ const config = await adapter.loadTelegramAdapterConfig({ env: input.env, jsonPath: input.configPath });
554
+ if (!config.enabled) {
555
+ return undefined;
556
+ }
557
+ return {
558
+ botToken: config.botToken,
559
+ allowedChatIds: config.allowedChatIds,
560
+ allowAllChats: config.allowAllChats,
561
+ ...(config.apiRoot === undefined ? {} : { apiRoot: config.apiRoot }),
562
+ maxUploadBytes: config.attachments?.maxUploadBytes ?? adapter.DEFAULT_ATTACHMENT_MAX_BYTES,
563
+ tools,
564
+ };
565
+ }
566
+ catch (error) {
567
+ options.logger?.warn?.("Telegram send tool skipped because Telegram adapter config is unavailable.", {
568
+ reason: reasonOf(error),
569
+ });
570
+ return undefined;
571
+ }
572
+ }
573
+ function assertSlackChannelAllowed(settings, channel) {
574
+ if (settings.allowAllChannels || settings.allowedChannelIds.includes(normalizeSlackChannelId(channel))) {
575
+ return;
576
+ }
577
+ throw new Error("slack_send_message: channel is not allowed by Slack adapter config.");
578
+ }
579
+ function assertTelegramChatAllowed(settings, chatId) {
580
+ const normalized = String(chatId);
581
+ if (settings.allowAllChats || settings.allowedChatIds.includes(normalized)) {
582
+ return;
583
+ }
584
+ throw new Error("telegram_send_message: chat_id is not allowed by Telegram adapter config.");
585
+ }
586
+ function optionalString(value) {
587
+ if (value === undefined) {
588
+ return undefined;
589
+ }
590
+ const trimmed = value.trim();
591
+ return trimmed.length === 0 ? undefined : trimmed;
592
+ }
593
+ function normalizeSlackChannelId(value) {
594
+ return value.trim().toLowerCase();
595
+ }
596
+ function isAdapterToolAllowed(name, options) {
597
+ const wildcard = `mcp__${ADAPTER_SEND_TOOLS_MCP_SERVER_NAME}__*`;
598
+ const aliases = [name, `mcp__${ADAPTER_SEND_TOOLS_MCP_SERVER_NAME}__${name}`];
599
+ const allowed = options.allowedTools ?? [];
600
+ const disallowed = options.disallowedTools ?? [];
601
+ if (disallowed.includes(wildcard) || aliases.some((alias) => disallowed.includes(alias))) {
602
+ return false;
603
+ }
604
+ if (aliases.some((alias) => allowed.includes(alias))) {
605
+ return true;
606
+ }
607
+ return allowed.includes(wildcard);
608
+ }
609
+ function parseAllowedToolNames(raw) {
610
+ const value = optionalString(raw);
611
+ if (value === undefined) {
612
+ throw new Error("adapter-send-tools: missing required environment (MONO_AGENT_ADAPTER_TOOLS_ALLOWED_TOOLS).");
613
+ }
614
+ let parsed;
615
+ try {
616
+ parsed = JSON.parse(value);
617
+ }
618
+ catch {
619
+ throw new Error("adapter-send-tools: invalid MONO_AGENT_ADAPTER_TOOLS_ALLOWED_TOOLS (expected a JSON string array).");
620
+ }
621
+ if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
622
+ throw new Error("adapter-send-tools: invalid MONO_AGENT_ADAPTER_TOOLS_ALLOWED_TOOLS (expected a JSON string array).");
623
+ }
624
+ return parsed;
625
+ }
626
+ function reasonOf(error) {
627
+ return error instanceof Error ? error.message : String(error);
628
+ }
629
+ //# sourceMappingURL=adapter-send-tools.js.map