@mono-agent/agent-app 0.6.2 → 0.8.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.
- package/README.md +210 -19
- package/dist/adapter-send-proxy.d.ts +18 -0
- package/dist/adapter-send-proxy.d.ts.map +1 -0
- package/dist/adapter-send-proxy.js +229 -0
- package/dist/adapter-send-proxy.js.map +1 -0
- package/dist/adapter-send-tools-main.js +62 -4
- package/dist/adapter-send-tools-main.js.map +1 -1
- package/dist/adapter-send-tools.d.ts +33 -6
- package/dist/adapter-send-tools.d.ts.map +1 -1
- package/dist/adapter-send-tools.js +485 -87
- package/dist/adapter-send-tools.js.map +1 -1
- package/dist/app-config.d.ts.map +1 -1
- package/dist/app-config.js +8 -0
- package/dist/app-config.js.map +1 -1
- package/dist/app.d.ts +2 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +528 -135
- package/dist/app.js.map +1 -1
- package/dist/broadcast-recorder.d.ts.map +1 -1
- package/dist/broadcast-recorder.js +24 -2
- package/dist/broadcast-recorder.js.map +1 -1
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +16 -6
- package/dist/channels.js.map +1 -1
- package/dist/cli.d.ts +76 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2102 -102
- package/dist/cli.js.map +1 -1
- package/dist/config-reference.d.ts.map +1 -1
- package/dist/config-reference.js +195 -6
- package/dist/config-reference.js.map +1 -1
- package/dist/configuration-proposal-main.d.ts +3 -0
- package/dist/configuration-proposal-main.d.ts.map +1 -0
- package/dist/configuration-proposal-main.js +13 -0
- package/dist/configuration-proposal-main.js.map +1 -0
- package/dist/configuration-proposal-tool.d.ts +26 -0
- package/dist/configuration-proposal-tool.d.ts.map +1 -0
- package/dist/configuration-proposal-tool.js +139 -0
- package/dist/configuration-proposal-tool.js.map +1 -0
- package/dist/configured-agent.d.ts +12 -0
- package/dist/configured-agent.d.ts.map +1 -1
- package/dist/configured-agent.js +150 -18
- package/dist/configured-agent.js.map +1 -1
- package/dist/consumer-contract.d.ts +3 -0
- package/dist/consumer-contract.d.ts.map +1 -1
- package/dist/consumer-contract.js +44 -1
- package/dist/consumer-contract.js.map +1 -1
- package/dist/doctor.d.ts +27 -5
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +1197 -101
- package/dist/doctor.js.map +1 -1
- package/dist/first-run-managed-memory.d.ts +39 -0
- package/dist/first-run-managed-memory.d.ts.map +1 -0
- package/dist/first-run-managed-memory.js +521 -0
- package/dist/first-run-managed-memory.js.map +1 -0
- package/dist/first-run-readiness.d.ts +111 -0
- package/dist/first-run-readiness.d.ts.map +1 -0
- package/dist/first-run-readiness.js +757 -0
- package/dist/first-run-readiness.js.map +1 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +88 -3
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +1212 -23
- package/dist/init.js.map +1 -1
- package/dist/interaction-bridge.d.ts +25 -1
- package/dist/interaction-bridge.d.ts.map +1 -1
- package/dist/interaction-bridge.js +281 -19
- package/dist/interaction-bridge.js.map +1 -1
- package/dist/local-configuration.d.ts +56 -0
- package/dist/local-configuration.d.ts.map +1 -0
- package/dist/local-configuration.js +914 -0
- package/dist/local-configuration.js.map +1 -0
- package/dist/memory-command.d.ts +4 -0
- package/dist/memory-command.d.ts.map +1 -1
- package/dist/memory-command.js +751 -27
- package/dist/memory-command.js.map +1 -1
- package/dist/memory-recall.d.ts +38 -10
- package/dist/memory-recall.d.ts.map +1 -1
- package/dist/memory-recall.js +82 -25
- package/dist/memory-recall.js.map +1 -1
- package/dist/memory-retrieval.d.ts +66 -0
- package/dist/memory-retrieval.d.ts.map +1 -0
- package/dist/memory-retrieval.js +332 -0
- package/dist/memory-retrieval.js.map +1 -0
- package/dist/modules/base.d.ts +6 -5
- package/dist/modules/base.d.ts.map +1 -1
- package/dist/modules/base.js +11 -7
- package/dist/modules/base.js.map +1 -1
- package/dist/modules/catalog.d.ts.map +1 -1
- package/dist/modules/catalog.js +54 -20
- package/dist/modules/catalog.js.map +1 -1
- package/dist/modules/index.d.ts +2 -2
- package/dist/modules/index.d.ts.map +1 -1
- package/dist/modules/index.js +1 -1
- package/dist/modules/index.js.map +1 -1
- package/dist/modules/known-tools.d.ts +3 -0
- package/dist/modules/known-tools.d.ts.map +1 -1
- package/dist/modules/known-tools.js +11 -1
- package/dist/modules/known-tools.js.map +1 -1
- package/dist/modules/types.d.ts +20 -6
- package/dist/modules/types.d.ts.map +1 -1
- package/dist/modules/types.js.map +1 -1
- package/dist/package-version.d.ts +3 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +14 -0
- package/dist/package-version.js.map +1 -0
- package/dist/pi-auth-store-inspection.d.ts +21 -0
- package/dist/pi-auth-store-inspection.d.ts.map +1 -0
- package/dist/pi-auth-store-inspection.js +120 -0
- package/dist/pi-auth-store-inspection.js.map +1 -0
- package/dist/pi-oauth-login-main.d.ts +3 -0
- package/dist/pi-oauth-login-main.d.ts.map +1 -0
- package/dist/pi-oauth-login-main.js +14 -0
- package/dist/pi-oauth-login-main.js.map +1 -0
- package/dist/pi-oauth-login.d.ts +22 -0
- package/dist/pi-oauth-login.d.ts.map +1 -0
- package/dist/pi-oauth-login.js +79 -0
- package/dist/pi-oauth-login.js.map +1 -0
- package/dist/project-skills.d.ts +40 -0
- package/dist/project-skills.d.ts.map +1 -0
- package/dist/project-skills.js +573 -0
- package/dist/project-skills.js.map +1 -0
- package/dist/provider-setup.d.ts +117 -2
- package/dist/provider-setup.d.ts.map +1 -1
- package/dist/provider-setup.js +1505 -49
- package/dist/provider-setup.js.map +1 -1
- package/dist/readiness-probe-worker.d.ts +12 -0
- package/dist/readiness-probe-worker.d.ts.map +1 -0
- package/dist/readiness-probe-worker.js +276 -0
- package/dist/readiness-probe-worker.js.map +1 -0
- package/dist/readiness-probe.d.ts +100 -0
- package/dist/readiness-probe.d.ts.map +1 -0
- package/dist/readiness-probe.js +780 -0
- package/dist/readiness-probe.js.map +1 -0
- package/dist/request-model-override.d.ts +43 -1
- package/dist/request-model-override.d.ts.map +1 -1
- package/dist/request-model-override.js +178 -19
- package/dist/request-model-override.js.map +1 -1
- package/dist/run-history.d.ts +32 -0
- package/dist/run-history.d.ts.map +1 -0
- package/dist/run-history.js +859 -0
- package/dist/run-history.js.map +1 -0
- package/dist/runs-health.d.ts.map +1 -1
- package/dist/runs-health.js +12 -4
- package/dist/runs-health.js.map +1 -1
- package/dist/runtime-option-extensions.d.ts +14 -0
- package/dist/runtime-option-extensions.d.ts.map +1 -0
- package/dist/runtime-option-extensions.js +98 -0
- package/dist/runtime-option-extensions.js.map +1 -0
- package/dist/runtime-routes.d.ts +9 -0
- package/dist/runtime-routes.d.ts.map +1 -0
- package/dist/runtime-routes.js +13 -0
- package/dist/runtime-routes.js.map +1 -0
- package/dist/sandbox-manager.d.ts +69 -0
- package/dist/sandbox-manager.d.ts.map +1 -0
- package/dist/sandbox-manager.js +856 -0
- package/dist/sandbox-manager.js.map +1 -0
- package/dist/supermemory-plugin.d.ts +54 -0
- package/dist/supermemory-plugin.d.ts.map +1 -0
- package/dist/supermemory-plugin.js +136 -0
- package/dist/supermemory-plugin.js.map +1 -0
- package/dist/tui-command.d.ts +15 -0
- package/dist/tui-command.d.ts.map +1 -1
- package/dist/tui-command.js +46 -6
- package/dist/tui-command.js.map +1 -1
- package/dist/web-command.d.ts +8 -0
- package/dist/web-command.d.ts.map +1 -1
- package/dist/web-command.js +174 -17
- package/dist/web-command.js.map +1 -1
- package/dist/wizard/answers.d.ts +31 -4
- package/dist/wizard/answers.d.ts.map +1 -1
- package/dist/wizard/answers.js +145 -16
- package/dist/wizard/answers.js.map +1 -1
- package/dist/wizard/from-flags.d.ts +7 -0
- package/dist/wizard/from-flags.d.ts.map +1 -1
- package/dist/wizard/from-flags.js +36 -2
- package/dist/wizard/from-flags.js.map +1 -1
- package/dist/wizard/index.d.ts +4 -4
- package/dist/wizard/index.d.ts.map +1 -1
- package/dist/wizard/index.js +2 -2
- package/dist/wizard/index.js.map +1 -1
- package/dist/wizard/model-discovery.d.ts +33 -2
- package/dist/wizard/model-discovery.d.ts.map +1 -1
- package/dist/wizard/model-discovery.js +826 -108
- package/dist/wizard/model-discovery.js.map +1 -1
- package/dist/wizard/presets.d.ts +1 -1
- package/dist/wizard/presets.d.ts.map +1 -1
- package/dist/wizard/presets.js +7 -12
- package/dist/wizard/presets.js.map +1 -1
- package/dist/wizard/prompts.d.ts +56 -9
- package/dist/wizard/prompts.d.ts.map +1 -1
- package/dist/wizard/prompts.js +172 -20
- package/dist/wizard/prompts.js.map +1 -1
- package/dist/wizard/run.d.ts +59 -3
- package/dist/wizard/run.d.ts.map +1 -1
- package/dist/wizard/run.js +1133 -191
- package/dist/wizard/run.js.map +1 -1
- package/package.json +21 -16
- package/resources/srt/package-lock.json +66 -0
- package/resources/srt/package.json +9 -0
- package/schema/mono-agent.config.schema.json +381 -17
- package/skills/mono-agent-composer/SKILL.md +4 -4
- package/skills/mono-agent-composer/references/config-blueprint.md +5 -4
- package/skills/mono-agent-composer/references/discovery-questions.md +8 -8
- package/skills/mono-agent-composer/references/feature-coverage.md +8 -6
- package/skills/mono-agent-composer/references/package-map.md +1 -1
- package/skills/mono-agent-composer/references/playbooks.md +8 -7
- package/skills/mono-agent-composer/references/validation.md +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import { lstat, mkdir, open, readFile, realpath, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, isAbsolute, join, relative, resolve as resolvePath, sep } from "node:path";
|
|
3
4
|
import process from "node:process";
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -13,6 +14,8 @@ let telegramModule;
|
|
|
13
14
|
const loadSlackModule = async () => (slackModule ??= await import("@mono-agent/slack-adapter"));
|
|
14
15
|
const loadTelegramModule = async () => (telegramModule ??= await import("@mono-agent/telegram-adapter"));
|
|
15
16
|
const SLACK_SEND_MESSAGE_MAX_CHARS = 40_000;
|
|
17
|
+
/** Keep cancellation responsive even when the loopback bridge is wedged. */
|
|
18
|
+
const ASK_BRIDGE_CLEANUP_TIMEOUT_MS = 1_000;
|
|
16
19
|
/**
|
|
17
20
|
* Model-visible send tools for explicitly allowed, already-enabled communication adapters.
|
|
18
21
|
*
|
|
@@ -26,10 +29,13 @@ const SLACK_SEND_MESSAGE_MAX_CHARS = 40_000;
|
|
|
26
29
|
export const ADAPTER_SEND_TOOLS_MCP_SERVER_NAME = "mono-agent-adapter-send";
|
|
27
30
|
export async function resolveAdapterSendToolsSettings(input, options = {}) {
|
|
28
31
|
const telegramSendAllowed = isAdapterToolAllowed("TelegramSendMessage", options);
|
|
29
|
-
const telegramAskAllowed =
|
|
32
|
+
const telegramAskAllowed = options.suppressInteractionTools !== true
|
|
33
|
+
&& isAdapterToolAllowed("TelegramAskButtons", options);
|
|
30
34
|
const telegramFileAllowed = isAdapterToolAllowed("TelegramSendFile", options);
|
|
31
35
|
const telegramAnyAllowed = telegramSendAllowed || telegramAskAllowed || telegramFileAllowed;
|
|
32
|
-
const telegramAskBridge = telegramAskAllowed
|
|
36
|
+
const telegramAskBridge = telegramAskAllowed
|
|
37
|
+
? resolveAskUserToolSettings(input.env, options.interaction)
|
|
38
|
+
: undefined;
|
|
33
39
|
const [slack, telegram] = await Promise.all([
|
|
34
40
|
isAdapterToolAllowed("SlackSendMessage", options)
|
|
35
41
|
? resolveSlackSendToolSettings(input, options)
|
|
@@ -42,8 +48,8 @@ export async function resolveAdapterSendToolsSettings(input, options = {}) {
|
|
|
42
48
|
}, telegramAskBridge)
|
|
43
49
|
: undefined,
|
|
44
50
|
]);
|
|
45
|
-
const askUser = isAdapterToolAllowed("AskUser", options)
|
|
46
|
-
? resolveAskUserToolSettings(input.env)
|
|
51
|
+
const askUser = options.suppressInteractionTools !== true && isAdapterToolAllowed("AskUser", options)
|
|
52
|
+
? resolveAskUserToolSettings(input.env, options.interaction)
|
|
47
53
|
: undefined;
|
|
48
54
|
if (slack === undefined && telegram === undefined && askUser === undefined) {
|
|
49
55
|
return undefined;
|
|
@@ -59,20 +65,23 @@ export async function resolveAdapterSendToolsSettings(input, options = {}) {
|
|
|
59
65
|
* into the environment (URL + bearer token). The producing conversation id is
|
|
60
66
|
* per-request env, present in the spawned child.
|
|
61
67
|
*/
|
|
62
|
-
function resolveAskUserToolSettings(env) {
|
|
63
|
-
const bridgeUrl = optionalString(env.MONO_AGENT_INTERACTION_BRIDGE_URL);
|
|
64
|
-
const bridgeToken = optionalString(env.MONO_AGENT_INTERACTION_BRIDGE_TOKEN);
|
|
68
|
+
function resolveAskUserToolSettings(env, interaction) {
|
|
69
|
+
const bridgeUrl = interaction?.bridgeUrl ?? optionalString(env.MONO_AGENT_INTERACTION_BRIDGE_URL);
|
|
70
|
+
const bridgeToken = interaction?.bridgeToken ?? optionalString(env.MONO_AGENT_INTERACTION_BRIDGE_TOKEN);
|
|
65
71
|
if (bridgeUrl === undefined || bridgeToken === undefined) {
|
|
66
72
|
return undefined;
|
|
67
73
|
}
|
|
68
74
|
const timeoutRaw = Number(optionalString(env.MONO_AGENT_ASK_USER_TIMEOUT_MS));
|
|
69
|
-
const timeoutMs =
|
|
75
|
+
const timeoutMs = interaction?.timeoutMs
|
|
76
|
+
?? (Number.isFinite(timeoutRaw) && timeoutRaw >= 1000 ? timeoutRaw : 600_000);
|
|
70
77
|
const conversationId = optionalString(env.MONO_AGENT_ADAPTER_TOOLS_PRODUCING_CONVERSATION_ID);
|
|
78
|
+
const runId = optionalString(env.MONO_AGENT_ADAPTER_TOOLS_PRODUCING_RUN_ID);
|
|
71
79
|
return {
|
|
72
80
|
bridgeUrl,
|
|
73
81
|
bridgeToken,
|
|
74
82
|
timeoutMs,
|
|
75
83
|
...(conversationId === undefined ? {} : { conversationId }),
|
|
84
|
+
...(runId === undefined ? {} : { runId }),
|
|
76
85
|
};
|
|
77
86
|
}
|
|
78
87
|
export function adapterSendToolNames(settings) {
|
|
@@ -109,9 +118,21 @@ export function adapterSendToolsMcpEnv(configPath, allowedTools, context, intera
|
|
|
109
118
|
...(context?.conversationId === undefined
|
|
110
119
|
? {}
|
|
111
120
|
: { MONO_AGENT_ADAPTER_TOOLS_PRODUCING_CONVERSATION_ID: context.conversationId }),
|
|
121
|
+
...(context?.runId === undefined
|
|
122
|
+
? {}
|
|
123
|
+
: { MONO_AGENT_ADAPTER_TOOLS_PRODUCING_RUN_ID: context.runId }),
|
|
112
124
|
...(context?.indexPath === undefined
|
|
113
125
|
? {}
|
|
114
126
|
: { MONO_AGENT_ADAPTER_TOOLS_POST_INDEX_PATH: context.indexPath }),
|
|
127
|
+
...(context?.runOutputDir === undefined
|
|
128
|
+
? {}
|
|
129
|
+
: { MONO_AGENT_ADAPTER_TOOLS_RUN_OUTPUT_DIR: context.runOutputDir }),
|
|
130
|
+
...(context?.runOutputIdentity === undefined
|
|
131
|
+
? {}
|
|
132
|
+
: {
|
|
133
|
+
MONO_AGENT_ADAPTER_TOOLS_RUN_OUTPUT_DEV: String(context.runOutputIdentity.dev),
|
|
134
|
+
MONO_AGENT_ADAPTER_TOOLS_RUN_OUTPUT_INO: String(context.runOutputIdentity.ino),
|
|
135
|
+
}),
|
|
115
136
|
...(interaction === undefined
|
|
116
137
|
? {}
|
|
117
138
|
: {
|
|
@@ -137,13 +158,26 @@ export function adapterSendToolsChildConfigFromEnv(env, cwd) {
|
|
|
137
158
|
};
|
|
138
159
|
}
|
|
139
160
|
export function adapterSendToolsMcpServerSpec(configPath, cwd, allowedTools, context, interaction) {
|
|
140
|
-
|
|
161
|
+
const spec = {
|
|
141
162
|
type: "stdio",
|
|
142
163
|
command: process.execPath,
|
|
143
|
-
|
|
164
|
+
// Node 24 on macOS defaults to the system trust store, whose trustd access
|
|
165
|
+
// is intentionally unavailable inside strict SRT. Force Node's bundled CA
|
|
166
|
+
// set for this child while still honoring NODE_EXTRA_CA_CERTS when SRT TLS
|
|
167
|
+
// termination or an operator supplies an additional root.
|
|
168
|
+
args: ["--use-bundled-ca", fileURLToPath(new URL("./adapter-send-tools-main.js", import.meta.url))],
|
|
144
169
|
cwd,
|
|
145
170
|
env: adapterSendToolsMcpEnv(configPath, allowedTools, context, interaction),
|
|
146
171
|
};
|
|
172
|
+
// Only the trusted app-owned adapter child receives SRT's coarse loopback
|
|
173
|
+
// capability. A symbol cannot be supplied by JSON MCP config and disappears
|
|
174
|
+
// from serialized provider/tool metadata, so ordinary Bash/project MCP
|
|
175
|
+
// processes keep the stricter no-bind allowlist policy.
|
|
176
|
+
Object.defineProperty(spec, Symbol.for("@mono-agent/app-owned-local-binding"), {
|
|
177
|
+
value: true,
|
|
178
|
+
enumerable: false,
|
|
179
|
+
});
|
|
180
|
+
return spec;
|
|
147
181
|
}
|
|
148
182
|
/**
|
|
149
183
|
* Per-request runtime extension that injects the adapter-send stdio MCP server. It
|
|
@@ -151,14 +185,26 @@ export function adapterSendToolsMcpServerSpec(configPath, cwd, allowedTools, con
|
|
|
151
185
|
* configured, forwards both to the child so a `SlackSendMessage` post is linked
|
|
152
186
|
* back to this conversation (so a later in-thread reply resumes it).
|
|
153
187
|
*/
|
|
154
|
-
export function createAdapterSendToolsRuntimeExtension(configPath, cwd, allowedTools, indexPath, interaction) {
|
|
188
|
+
export function createAdapterSendToolsRuntimeExtension(configPath, cwd, allowedTools, indexPath, interaction, runOutputRoot) {
|
|
155
189
|
return async (input) => {
|
|
156
190
|
const conversationId = input?.request?.conversationId;
|
|
191
|
+
const runId = input?.runId;
|
|
157
192
|
const hasConversation = typeof conversationId === "string" && conversationId.trim().length > 0;
|
|
193
|
+
const hasRunId = typeof runId === "string" && runId.trim().length > 0;
|
|
194
|
+
const runOutput = runOutputRoot === undefined || !hasRunId
|
|
195
|
+
? undefined
|
|
196
|
+
: await ensureAdapterRunOutputDir(runOutputRoot, runId);
|
|
158
197
|
// The conversation id is forwarded whenever known — AskUser targets it even
|
|
159
198
|
// without indexing; the index path additionally enables posted-message links.
|
|
160
|
-
const context = hasConversation
|
|
161
|
-
? {
|
|
199
|
+
const context = hasConversation || hasRunId
|
|
200
|
+
? {
|
|
201
|
+
...(hasConversation ? { conversationId } : {}),
|
|
202
|
+
...(hasRunId ? { runId } : {}),
|
|
203
|
+
...(indexPath === undefined ? {} : { indexPath }),
|
|
204
|
+
...(runOutput === undefined
|
|
205
|
+
? {}
|
|
206
|
+
: { runOutputDir: runOutput.path, runOutputIdentity: runOutput.identity }),
|
|
207
|
+
}
|
|
162
208
|
: undefined;
|
|
163
209
|
return {
|
|
164
210
|
runtimeOptions: {
|
|
@@ -167,27 +213,37 @@ export function createAdapterSendToolsRuntimeExtension(configPath, cwd, allowedT
|
|
|
167
213
|
},
|
|
168
214
|
},
|
|
169
215
|
cleanup: async () => { },
|
|
216
|
+
settleCleanup: async () => {
|
|
217
|
+
if (runOutput !== undefined) {
|
|
218
|
+
await removeOwnedDirectory(runOutput.path, runOutput.identity);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
170
221
|
};
|
|
171
222
|
};
|
|
172
223
|
}
|
|
173
|
-
export async function createAdapterSendToolsClients(settings) {
|
|
224
|
+
export async function createAdapterSendToolsClients(settings, options = {}) {
|
|
174
225
|
const slack = settings.slack === undefined
|
|
175
226
|
? undefined
|
|
176
|
-
: new (await loadSlackModule()).SlackWebApiClient({
|
|
227
|
+
: new (await loadSlackModule()).SlackWebApiClient({
|
|
228
|
+
botToken: settings.slack.botToken,
|
|
229
|
+
...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
|
|
230
|
+
});
|
|
177
231
|
const telegram = settings.telegram === undefined
|
|
178
232
|
? undefined
|
|
179
233
|
: (await loadTelegramModule()).createTelegramMessageSender(settings.telegram.botToken, {
|
|
180
234
|
...(settings.telegram.apiRoot === undefined ? {} : { apiRoot: settings.telegram.apiRoot }),
|
|
235
|
+
...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
|
|
181
236
|
});
|
|
182
237
|
return {
|
|
183
238
|
...(slack === undefined ? {} : { slack }),
|
|
184
239
|
...(telegram === undefined ? {} : { telegram }),
|
|
185
240
|
};
|
|
186
241
|
}
|
|
187
|
-
export async function createAdapterSendToolsServer(settings, clients, indexing) {
|
|
242
|
+
export async function createAdapterSendToolsServer(settings, clients, indexing, options = {}) {
|
|
188
243
|
const server = new McpServer({ name: "agent-adapter-send-tools", version: "0.3.0" });
|
|
189
244
|
if (settings.slack !== undefined && clients.slack !== undefined) {
|
|
190
|
-
|
|
245
|
+
const adapter = await loadSlackModule();
|
|
246
|
+
registerSlackSendTool(server, settings.slack, clients.slack, adapter.formatMarkdownForSlack, indexing);
|
|
191
247
|
}
|
|
192
248
|
if (settings.telegram !== undefined && clients.telegram !== undefined) {
|
|
193
249
|
// Loaded once here (not per-register-call) because registerTelegramAskTool
|
|
@@ -198,7 +254,7 @@ export async function createAdapterSendToolsServer(settings, clients, indexing)
|
|
|
198
254
|
registerTelegramSendTool(server, settings.telegram, clients.telegram);
|
|
199
255
|
}
|
|
200
256
|
if (settings.telegram.tools.ask) {
|
|
201
|
-
registerTelegramAskTool(server, settings.telegram, clients.telegram, adapter);
|
|
257
|
+
registerTelegramAskTool(server, settings.telegram, clients.telegram, adapter, options.fetchImpl ?? globalThis.fetch);
|
|
202
258
|
}
|
|
203
259
|
if (settings.telegram.tools.file) {
|
|
204
260
|
registerTelegramSendFileTool(server, settings.telegram, clients.telegram, adapter);
|
|
@@ -207,13 +263,13 @@ export async function createAdapterSendToolsServer(settings, clients, indexing)
|
|
|
207
263
|
// AskUser needs a target conversation; the parent app process resolves the
|
|
208
264
|
// settings without one (for tool-name gating) and must not register the tool.
|
|
209
265
|
if (settings.askUser?.conversationId !== undefined) {
|
|
210
|
-
registerAskUserTool(server, { ...settings.askUser, conversationId: settings.askUser.conversationId });
|
|
266
|
+
registerAskUserTool(server, { ...settings.askUser, conversationId: settings.askUser.conversationId }, options.fetchImpl ?? globalThis.fetch);
|
|
211
267
|
}
|
|
212
268
|
return server;
|
|
213
269
|
}
|
|
214
270
|
/** Long-poll wait per bridge request; the overall wait is bounded server-side by the ask's timeout. */
|
|
215
271
|
const ASK_USER_POLL_WAIT_MS = 20_000;
|
|
216
|
-
function registerAskUserTool(server, settings) {
|
|
272
|
+
function registerAskUserTool(server, settings, fetchImpl) {
|
|
217
273
|
server.registerTool("AskUser", {
|
|
218
274
|
title: "Ask the user and wait",
|
|
219
275
|
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).",
|
|
@@ -221,11 +277,14 @@ function registerAskUserTool(server, settings) {
|
|
|
221
277
|
question: z.string().min(1).describe("The full question to show the user (plain text, no markdown)."),
|
|
222
278
|
},
|
|
223
279
|
}, async (args, extra) => {
|
|
224
|
-
const created = await askBridgeRequest(settings, "POST", "/v1/asks", {
|
|
280
|
+
const created = await askBridgeRequest(settings, fetchImpl, "POST", "/v1/asks", {
|
|
225
281
|
conversationId: settings.conversationId,
|
|
282
|
+
producerConversationId: settings.conversationId,
|
|
283
|
+
runId: settings.runId,
|
|
284
|
+
toolName: "AskUser",
|
|
226
285
|
question: args.question,
|
|
227
286
|
timeoutMs: settings.timeoutMs,
|
|
228
|
-
});
|
|
287
|
+
}, extra.signal);
|
|
229
288
|
if (created.status === 409) {
|
|
230
289
|
return askToolResult("A question is already pending for the user. Wait for its answer instead of asking again.", { answered: false, reason: "already_pending" });
|
|
231
290
|
}
|
|
@@ -235,7 +294,7 @@ function registerAskUserTool(server, settings) {
|
|
|
235
294
|
if (created.status !== 201 || typeof created.body.askId !== "string") {
|
|
236
295
|
throw new Error(`AskUser: the interaction bridge rejected the ask (HTTP ${String(created.status)}).`);
|
|
237
296
|
}
|
|
238
|
-
return await awaitBridgeAsk(settings, created.body.askId, "AskUser", extra);
|
|
297
|
+
return await awaitBridgeAsk(settings, created.body.askId, "AskUser", extra, fetchImpl, extra.signal);
|
|
239
298
|
});
|
|
240
299
|
}
|
|
241
300
|
function askToolResult(text, structured, extraStructured = {}) {
|
|
@@ -244,42 +303,74 @@ function askToolResult(text, structured, extraStructured = {}) {
|
|
|
244
303
|
structuredContent: { ok: true, ...extraStructured, ...structured },
|
|
245
304
|
};
|
|
246
305
|
}
|
|
247
|
-
async function awaitBridgeAsk(settings, askId, toolName, extra, extraStructured = {}) {
|
|
306
|
+
async function awaitBridgeAsk(settings, askId, toolName, extra, fetchImpl, signal, extraStructured = {}) {
|
|
248
307
|
const startedMs = Date.now();
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
308
|
+
try {
|
|
309
|
+
for (;;) {
|
|
310
|
+
const poll = await askBridgeRequest(settings, fetchImpl, "GET", `/v1/asks/${encodeURIComponent(askId)}?waitMs=${String(ASK_USER_POLL_WAIT_MS)}`, undefined, signal);
|
|
311
|
+
if (poll.status !== 200) {
|
|
312
|
+
throw new Error(`${toolName}: lost the pending ask (HTTP ${String(poll.status)}).`);
|
|
313
|
+
}
|
|
314
|
+
// Keep-alive: progress notifications reset the runtime's MCP inactivity
|
|
315
|
+
// timeout so a long human wait cannot kill the tool call.
|
|
316
|
+
await sendAskProgress(extra, Math.round((Date.now() - startedMs) / 1000));
|
|
317
|
+
const status = poll.body.status;
|
|
318
|
+
if (status === "answered" && typeof poll.body.answer === "string") {
|
|
319
|
+
const answeredText = toolName === "TelegramAskButtons"
|
|
320
|
+
? `The user tapped:\n${poll.body.answer}`
|
|
321
|
+
: `The user answered:\n${poll.body.answer}`;
|
|
322
|
+
return askToolResult(answeredText, { answered: true, answer: poll.body.answer }, extraStructured);
|
|
323
|
+
}
|
|
324
|
+
if (status === "expired") {
|
|
325
|
+
return askToolResult("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" }, extraStructured);
|
|
326
|
+
}
|
|
327
|
+
if (status === "cancelled") {
|
|
328
|
+
return askToolResult("The user cancelled the current run. Stop this task.", {
|
|
329
|
+
answered: false,
|
|
330
|
+
reason: "cancelled",
|
|
331
|
+
}, extraStructured);
|
|
332
|
+
}
|
|
272
333
|
}
|
|
273
334
|
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
// A cancelled MCP call must not leave a bridge ask pending until its full
|
|
337
|
+
// human timeout. Cleanup gets its own bounded signal (not the aborted call
|
|
338
|
+
// signal), and its failure must never replace the primary tool failure.
|
|
339
|
+
await cleanupBridgeAskBestEffort(settings, fetchImpl, askId);
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
274
342
|
}
|
|
275
|
-
async function
|
|
276
|
-
const
|
|
343
|
+
async function cleanupBridgeAskBestEffort(settings, fetchImpl, askId) {
|
|
344
|
+
const controller = new AbortController();
|
|
345
|
+
let deadlineTimer;
|
|
346
|
+
const deadline = new Promise((resolve) => {
|
|
347
|
+
deadlineTimer = setTimeout(() => {
|
|
348
|
+
controller.abort(new Error("interaction bridge ask cleanup timed out"));
|
|
349
|
+
resolve();
|
|
350
|
+
}, ASK_BRIDGE_CLEANUP_TIMEOUT_MS);
|
|
351
|
+
deadlineTimer.unref?.();
|
|
352
|
+
});
|
|
353
|
+
// Attach the rejection handler before racing. If a test seam or nonstandard
|
|
354
|
+
// fetch ignores abort and rejects later, it cannot become an unhandled error.
|
|
355
|
+
const cleanup = askBridgeRequest(settings, fetchImpl, "DELETE", `/v1/asks/${encodeURIComponent(askId)}`, undefined, controller.signal).then(() => undefined, () => undefined);
|
|
356
|
+
try {
|
|
357
|
+
await Promise.race([cleanup, deadline]);
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
if (deadlineTimer !== undefined) {
|
|
361
|
+
clearTimeout(deadlineTimer);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function askBridgeRequest(settings, fetchImpl, method, path, body, signal) {
|
|
366
|
+
const response = await fetchImpl(new URL(path, settings.bridgeUrl), {
|
|
277
367
|
method,
|
|
278
368
|
headers: {
|
|
279
369
|
authorization: `Bearer ${settings.bridgeToken}`,
|
|
280
370
|
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
281
371
|
},
|
|
282
372
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
373
|
+
...(signal === undefined ? {} : { signal }),
|
|
283
374
|
});
|
|
284
375
|
let parsed;
|
|
285
376
|
try {
|
|
@@ -313,31 +404,33 @@ async function sendAskProgress(extra, elapsedSeconds) {
|
|
|
313
404
|
// Keep-alive only; a lost notification must never fail the ask.
|
|
314
405
|
}
|
|
315
406
|
}
|
|
316
|
-
function registerSlackSendTool(server, settings, client, indexing) {
|
|
407
|
+
function registerSlackSendTool(server, settings, client, formatMarkdownForSlack, indexing) {
|
|
317
408
|
server.registerTool("SlackSendMessage", {
|
|
318
409
|
title: "Send Slack message",
|
|
319
410
|
description: "Send a message to an allowed Slack channel or DM ID using the configured Slack adapter bot token.",
|
|
320
411
|
inputSchema: {
|
|
321
412
|
channel: z.string().min(1).describe("Slack channel or DM ID, e.g. C123456 or D123456."),
|
|
322
|
-
text: z.string().min(1).describe("Message text to send."),
|
|
413
|
+
text: z.string().min(1).describe("Message text to send. Defaults to standard Markdown converted to Slack mrkdwn."),
|
|
323
414
|
thread_ts: z.string().min(1).optional().describe("Optional Slack thread timestamp to reply in."),
|
|
324
|
-
mrkdwn: z.boolean().optional().describe("Whether Slack mrkdwn formatting is enabled."),
|
|
415
|
+
mrkdwn: z.boolean().optional().describe("Whether Slack mrkdwn formatting is enabled. Defaults to true; set false to send plain text unchanged."),
|
|
325
416
|
unfurl_links: z.boolean().optional().describe("Whether Slack should unfurl links."),
|
|
326
417
|
unfurl_media: z.boolean().optional().describe("Whether Slack should unfurl media."),
|
|
327
418
|
},
|
|
328
|
-
}, async (args) => {
|
|
419
|
+
}, async (args, extra) => {
|
|
329
420
|
assertSlackChannelAllowed(settings, args.channel);
|
|
330
|
-
const
|
|
421
|
+
const mrkdwn = args.mrkdwn ?? true;
|
|
422
|
+
const text = mrkdwn ? formatMarkdownForSlack(args.text) : args.text;
|
|
423
|
+
const chunks = splitTextByCodePoints(text, SLACK_SEND_MESSAGE_MAX_CHARS);
|
|
331
424
|
const results = [];
|
|
332
425
|
for (const chunk of chunks) {
|
|
333
426
|
const result = await client.chatPostMessage({
|
|
334
427
|
channel: args.channel.trim(),
|
|
335
428
|
text: chunk,
|
|
336
429
|
...(args.thread_ts === undefined ? {} : { thread_ts: args.thread_ts }),
|
|
337
|
-
|
|
430
|
+
mrkdwn,
|
|
338
431
|
...(args.unfurl_links === undefined ? {} : { unfurl_links: args.unfurl_links }),
|
|
339
432
|
...(args.unfurl_media === undefined ? {} : { unfurl_media: args.unfurl_media }),
|
|
340
|
-
});
|
|
433
|
+
}, { signal: extra.signal });
|
|
341
434
|
results.push(result);
|
|
342
435
|
// Link every posted chunk back to this conversation so an in-thread reply can
|
|
343
436
|
// resume it. Best-effort: appendPostedMessage never throws, so a failed index
|
|
@@ -383,7 +476,7 @@ function registerTelegramSendTool(server, settings, client) {
|
|
|
383
476
|
reply_to_message_id: z.number().int().optional().describe("Optional message id to reply to."),
|
|
384
477
|
disable_web_page_preview: z.boolean().optional().describe("Disable Telegram link previews."),
|
|
385
478
|
},
|
|
386
|
-
}, async (args) => {
|
|
479
|
+
}, async (args, extra) => {
|
|
387
480
|
assertTelegramChatAllowed(settings, args.chat_id);
|
|
388
481
|
const result = await client.sendMessage({
|
|
389
482
|
chat_id: args.chat_id,
|
|
@@ -391,17 +484,89 @@ function registerTelegramSendTool(server, settings, client) {
|
|
|
391
484
|
...(args.parse_mode === undefined ? {} : { parse_mode: args.parse_mode }),
|
|
392
485
|
...(args.reply_to_message_id === undefined ? {} : { reply_to_message_id: args.reply_to_message_id }),
|
|
393
486
|
...(args.disable_web_page_preview === undefined ? {} : { disable_web_page_preview: args.disable_web_page_preview }),
|
|
394
|
-
});
|
|
487
|
+
}, { signal: extra.signal });
|
|
395
488
|
return {
|
|
396
489
|
content: [{ type: "text", text: `Sent Telegram message ${result.message_id} to ${String(result.chat.id)}.` }],
|
|
397
490
|
structuredContent: { ok: true, chat_id: result.chat.id, message_id: result.message_id },
|
|
398
491
|
};
|
|
399
492
|
});
|
|
400
493
|
}
|
|
401
|
-
|
|
494
|
+
/** Marker appended (buttons kept) when a Telegram ask times out. */
|
|
495
|
+
const TELEGRAM_ASK_EXPIRED_MARKER = "⌛ Expired — tapping a button still reaches me and starts a new turn.";
|
|
496
|
+
/** Marker appended (buttons kept) when a Telegram ask is cancelled. */
|
|
497
|
+
const TELEGRAM_ASK_CANCELLED_MARKER = "🚫 Cancelled — tapping a button still reaches me and starts a new turn.";
|
|
498
|
+
/** Bounded own-signal budget for the best-effort expiry edit — never the aborted call signal. */
|
|
499
|
+
const TELEGRAM_ASK_EXPIRY_EDIT_TIMEOUT_MS = 2_000;
|
|
500
|
+
/** A Telegram 400 is the parse-entity rejection we fall back from (raw text, no parse_mode). */
|
|
501
|
+
function isTelegramParseError(error, adapter) {
|
|
502
|
+
return error instanceof adapter.TelegramApiError && error.errorCode === 400;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Send the ask keyboard rendered as MarkdownV2, retrying ONCE with the raw source and
|
|
506
|
+
* no parse_mode when Telegram rejects the entities with a 400 parse error. Any other
|
|
507
|
+
* failure propagates so the caller can clean up a pending bridge ask.
|
|
508
|
+
*/
|
|
509
|
+
async function sendTelegramAskKeyboard(client, adapter, params) {
|
|
510
|
+
try {
|
|
511
|
+
return await client.sendMessage({
|
|
512
|
+
chat_id: params.chat_id,
|
|
513
|
+
text: params.rendered,
|
|
514
|
+
parse_mode: "MarkdownV2",
|
|
515
|
+
reply_markup: params.replyMarkup,
|
|
516
|
+
}, { signal: params.signal });
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
if (!isTelegramParseError(error, adapter)) {
|
|
520
|
+
throw error;
|
|
521
|
+
}
|
|
522
|
+
return await client.sendMessage({
|
|
523
|
+
chat_id: params.chat_id,
|
|
524
|
+
text: params.rawSource,
|
|
525
|
+
reply_markup: params.replyMarkup,
|
|
526
|
+
}, { signal: params.signal });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Best-effort: append an expiry/cancel note to the buttons message while KEEPING the
|
|
531
|
+
* inline keyboard (Telegram strips the keyboard on `editMessageText` unless reply_markup
|
|
532
|
+
* is re-sent). Renders through the same MarkdownV2-with-plain-fallback path. Uses its own
|
|
533
|
+
* bounded signal — never the aborted call signal — and swallows every failure so a failed
|
|
534
|
+
* edit can never replace the primary ask result.
|
|
535
|
+
*/
|
|
536
|
+
async function editExpiredTelegramAskMessage(client, adapter, params) {
|
|
537
|
+
try {
|
|
538
|
+
const source = `${params.rawSource}\n\n${params.marker}`;
|
|
539
|
+
const rendered = adapter.renderTelegramMarkdown(source);
|
|
540
|
+
const signal = AbortSignal.timeout(TELEGRAM_ASK_EXPIRY_EDIT_TIMEOUT_MS);
|
|
541
|
+
try {
|
|
542
|
+
await client.editMessageText({
|
|
543
|
+
chat_id: params.chat_id,
|
|
544
|
+
message_id: params.message_id,
|
|
545
|
+
text: rendered,
|
|
546
|
+
parse_mode: "MarkdownV2",
|
|
547
|
+
reply_markup: params.replyMarkup,
|
|
548
|
+
}, { signal });
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
if (!isTelegramParseError(error, adapter)) {
|
|
552
|
+
throw error;
|
|
553
|
+
}
|
|
554
|
+
await client.editMessageText({
|
|
555
|
+
chat_id: params.chat_id,
|
|
556
|
+
message_id: params.message_id,
|
|
557
|
+
text: source,
|
|
558
|
+
reply_markup: params.replyMarkup,
|
|
559
|
+
}, { signal });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
// Best-effort: a failed expiry edit must never replace the ask result.
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function registerTelegramAskTool(server, settings, client, adapter, fetchImpl) {
|
|
402
567
|
server.registerTool("TelegramAskButtons", {
|
|
403
568
|
title: "Ask via Telegram buttons",
|
|
404
|
-
description: "Ask the owner a question in an allowed Telegram chat with tappable inline-keyboard buttons (e.g. a confirmation or a multiple-choice)
|
|
569
|
+
description: "Ask the owner a question in an allowed Telegram chat with tappable inline-keyboard buttons (e.g. a confirmation or a multiple-choice). By default WAITS for their tap (up to the ask timeout) and returns the tapped label; a second concurrent ask in the same chat fails. On a proactive or scheduled turn (cron/heartbeat) set `wait:false`: the buttons are sent and the tool returns immediately, and the tap arrives later as a new message quoting the question.",
|
|
405
570
|
inputSchema: {
|
|
406
571
|
chat_id: z.union([z.string().min(1), z.number().int()]).describe("Telegram chat id from the adapter allowlist."),
|
|
407
572
|
question: z.string().min(1).describe("The question to show above the buttons."),
|
|
@@ -411,9 +576,34 @@ function registerTelegramAskTool(server, settings, client, adapter) {
|
|
|
411
576
|
.max(adapter.TELEGRAM_ASK_MAX_OPTIONS)
|
|
412
577
|
.describe("Button labels (2–8). The label the user taps is echoed back as a new message."),
|
|
413
578
|
note: z.string().min(1).optional().describe("Optional extra context shown beneath the question."),
|
|
579
|
+
wait: z
|
|
580
|
+
.boolean()
|
|
581
|
+
.optional()
|
|
582
|
+
.describe("Default true: block until the user taps (up to the ask timeout). Set false on proactive/scheduled (cron/heartbeat) turns — send the buttons and return immediately; the tapped answer arrives later as a new message quoting the question."),
|
|
414
583
|
},
|
|
415
584
|
}, async (args, extra) => {
|
|
416
585
|
assertTelegramChatAllowed(settings, args.chat_id);
|
|
586
|
+
const inlineKeyboard = args.options.map((label, index) => [
|
|
587
|
+
{ text: label, callback_data: adapter.telegramAskCallbackData(index) },
|
|
588
|
+
]);
|
|
589
|
+
const replyMarkup = { inline_keyboard: inlineKeyboard };
|
|
590
|
+
// Raw source is what the bridge records and what the plain-text fallback (and the
|
|
591
|
+
// expiry edit) re-send; the rendered form is only for the MarkdownV2 send.
|
|
592
|
+
const source = args.note === undefined ? args.question : `${args.question}\n\n${args.note}`;
|
|
593
|
+
const rendered = adapter.renderTelegramMarkdown(source);
|
|
594
|
+
// Non-blocking mode (proactive/scheduled turns): send the buttons and return.
|
|
595
|
+
// No bridge is consulted — this must work even with no bridge configured.
|
|
596
|
+
if (args.wait === false) {
|
|
597
|
+
const sent = await sendTelegramAskKeyboard(client, adapter, {
|
|
598
|
+
chat_id: args.chat_id,
|
|
599
|
+
rendered,
|
|
600
|
+
rawSource: source,
|
|
601
|
+
replyMarkup,
|
|
602
|
+
signal: extra.signal,
|
|
603
|
+
});
|
|
604
|
+
return askToolResult(`Sent the question with ${String(args.options.length)} buttons to ${String(sent.chat.id)} (message ${String(sent.message_id)}). Not waiting — the tapped answer will arrive as a new message.`, { answered: false, reason: "not_waiting" }, { chat_id: sent.chat.id, message_id: sent.message_id, options: args.options });
|
|
605
|
+
}
|
|
606
|
+
// Blocking mode requires the interaction bridge to correlate the tap.
|
|
417
607
|
const bridge = settings.askBridge;
|
|
418
608
|
if (bridge === undefined) {
|
|
419
609
|
return askToolResult("TelegramAskButtons requires a live interaction bridge, but none is configured.", {
|
|
@@ -421,18 +611,18 @@ function registerTelegramAskTool(server, settings, client, adapter) {
|
|
|
421
611
|
reason: "unsupported_channel",
|
|
422
612
|
});
|
|
423
613
|
}
|
|
424
|
-
const inlineKeyboard = args.options.map((label, index) => [
|
|
425
|
-
{ text: label, callback_data: adapter.telegramAskCallbackData(index) },
|
|
426
|
-
]);
|
|
427
|
-
const text = args.note === undefined ? args.question : `${args.question}\n\n${args.note}`;
|
|
428
614
|
const conversationId = `telegram:${String(args.chat_id)}`;
|
|
429
|
-
const created = await askBridgeRequest(bridge, "POST", "/v1/asks", {
|
|
615
|
+
const created = await askBridgeRequest(bridge, fetchImpl, "POST", "/v1/asks", {
|
|
430
616
|
conversationId,
|
|
431
|
-
|
|
617
|
+
producerConversationId: bridge.conversationId,
|
|
618
|
+
runId: bridge.runId,
|
|
619
|
+
toolName: "TelegramAskButtons",
|
|
620
|
+
question: source,
|
|
621
|
+
options: args.options,
|
|
432
622
|
timeoutMs: bridge.timeoutMs,
|
|
433
623
|
postQuestion: false,
|
|
434
624
|
answerKind: "callback",
|
|
435
|
-
});
|
|
625
|
+
}, extra.signal);
|
|
436
626
|
if (created.status === 409) {
|
|
437
627
|
return askToolResult("A question is already pending for this Telegram chat. Wait for its answer instead of asking again.", { answered: false, reason: "already_pending" }, { chat_id: args.chat_id });
|
|
438
628
|
}
|
|
@@ -445,21 +635,36 @@ function registerTelegramAskTool(server, settings, client, adapter) {
|
|
|
445
635
|
const askId = created.body.askId;
|
|
446
636
|
let result;
|
|
447
637
|
try {
|
|
448
|
-
result = await client
|
|
638
|
+
result = await sendTelegramAskKeyboard(client, adapter, {
|
|
449
639
|
chat_id: args.chat_id,
|
|
450
|
-
|
|
451
|
-
|
|
640
|
+
rendered,
|
|
641
|
+
rawSource: source,
|
|
642
|
+
replyMarkup,
|
|
643
|
+
signal: extra.signal,
|
|
452
644
|
});
|
|
453
645
|
}
|
|
454
646
|
catch (error) {
|
|
455
|
-
await
|
|
647
|
+
await cleanupBridgeAskBestEffort(bridge, fetchImpl, askId);
|
|
456
648
|
throw error;
|
|
457
649
|
}
|
|
458
|
-
|
|
650
|
+
const askResult = await awaitBridgeAsk(bridge, askId, "TelegramAskButtons", extra, fetchImpl, extra.signal, {
|
|
459
651
|
chat_id: result.chat.id,
|
|
460
652
|
message_id: result.message_id,
|
|
461
653
|
options: args.options,
|
|
462
654
|
});
|
|
655
|
+
// An expired/cancelled ask leaves a live-looking keyboard forever; append a note
|
|
656
|
+
// while keeping the buttons (a tap still starts a fresh turn). Best-effort only.
|
|
657
|
+
const reason = askResult.structuredContent.reason;
|
|
658
|
+
if (reason === "timeout" || reason === "cancelled") {
|
|
659
|
+
await editExpiredTelegramAskMessage(client, adapter, {
|
|
660
|
+
chat_id: result.chat.id,
|
|
661
|
+
message_id: result.message_id,
|
|
662
|
+
rawSource: source,
|
|
663
|
+
marker: reason === "timeout" ? TELEGRAM_ASK_EXPIRED_MARKER : TELEGRAM_ASK_CANCELLED_MARKER,
|
|
664
|
+
replyMarkup,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
return askResult;
|
|
463
668
|
});
|
|
464
669
|
}
|
|
465
670
|
/**
|
|
@@ -473,7 +678,7 @@ function registerTelegramAskTool(server, settings, client, adapter) {
|
|
|
473
678
|
function registerTelegramSendFileTool(server, settings, client, adapter) {
|
|
474
679
|
server.registerTool("TelegramSendFile", {
|
|
475
680
|
title: "Send Telegram file",
|
|
476
|
-
description: "Upload and send a file to an allowed Telegram chat. Set `kind:\"document\"` to send any file (shown as a downloadable document) or `kind:\"photo\"` to send an image inline. Provide the bytes as base64 `data` (with a `filename` — required for a document), or a workspace `path
|
|
681
|
+
description: "Upload and send a file to an allowed Telegram chat. Set `kind:\"document\"` to send any file (shown as a downloadable document) or `kind:\"photo\"` to send an image inline. Provide the bytes as base64 `data` (with a `filename` — required for a document), or a workspace `path`. A self-hosted Bot API can stream legacy path uploads; strict run-output paths are always read through a pinned descriptor.",
|
|
477
682
|
inputSchema: {
|
|
478
683
|
kind: z.enum(["document", "photo"]).describe("`document` for any file (downloadable), `photo` for an image shown inline."),
|
|
479
684
|
chat_id: z.union([z.string().min(1), z.number().int()]).describe("Telegram chat id from the adapter allowlist."),
|
|
@@ -482,17 +687,27 @@ function registerTelegramSendFileTool(server, settings, client, adapter) {
|
|
|
482
687
|
filename: z.string().min(1).optional().describe("Filename to present. Required with `data` for a document; derived from `path` otherwise."),
|
|
483
688
|
caption: z.string().min(1).optional().describe("Optional caption shown with the file."),
|
|
484
689
|
},
|
|
485
|
-
}, async (args) => {
|
|
690
|
+
}, async (args, extra) => {
|
|
691
|
+
extra.signal.throwIfAborted();
|
|
486
692
|
const kind = args.kind;
|
|
487
693
|
assertTelegramChatAllowed(settings, args.chat_id);
|
|
694
|
+
if ((args.data !== undefined) === (args.path !== undefined)) {
|
|
695
|
+
throw new Error("provide exactly one of `data` (base64) or `path`.");
|
|
696
|
+
}
|
|
488
697
|
const maxUploadBytes = settings.maxUploadBytes ?? adapter.DEFAULT_ATTACHMENT_MAX_BYTES;
|
|
698
|
+
const strictPathUpload = args.path !== undefined && settings.sendTools?.pathScope === "run-output";
|
|
699
|
+
const strictFile = strictPathUpload
|
|
700
|
+
? await readStrictTelegramFile(settings, args.path, maxUploadBytes, extra.signal, args.filename)
|
|
701
|
+
: undefined;
|
|
702
|
+
const uploadPath = args.path === undefined || strictPathUpload
|
|
703
|
+
? undefined
|
|
704
|
+
: await resolveTelegramUploadPath(args.path);
|
|
489
705
|
// file:// fast path: a --local self-hosted server reads the file straight
|
|
490
706
|
// from disk, so a path upload needs no buffering at any size — only a
|
|
491
707
|
// stat-level cap check. Falls back once to the buffered path when the
|
|
492
708
|
// server rejects the URI (e.g. a non---local self-hosted root).
|
|
493
|
-
if (kind === "document" && settings.apiRoot !== undefined &&
|
|
494
|
-
const
|
|
495
|
-
const info = await stat(resolved);
|
|
709
|
+
if (kind === "document" && settings.apiRoot !== undefined && uploadPath !== undefined && args.data === undefined) {
|
|
710
|
+
const info = await stat(uploadPath);
|
|
496
711
|
if (!info.isFile() || info.size === 0) {
|
|
497
712
|
throw new Error("file is empty or not a regular file.");
|
|
498
713
|
}
|
|
@@ -502,10 +717,10 @@ function registerTelegramSendFileTool(server, settings, client, adapter) {
|
|
|
502
717
|
try {
|
|
503
718
|
const sent = await client.sendDocument({
|
|
504
719
|
chat_id: args.chat_id,
|
|
505
|
-
document: pathToFileURL(
|
|
720
|
+
document: pathToFileURL(uploadPath).href,
|
|
506
721
|
...(args.caption === undefined ? {} : { caption: args.caption }),
|
|
507
|
-
});
|
|
508
|
-
const name = basename(
|
|
722
|
+
}, { signal: extra.signal });
|
|
723
|
+
const name = basename(uploadPath);
|
|
509
724
|
return {
|
|
510
725
|
content: [{ type: "text", text: `Sent ${kind} ${sent.message_id} (${name}) to ${String(sent.chat.id)}.` }],
|
|
511
726
|
structuredContent: { ok: true, chat_id: sent.chat.id, message_id: sent.message_id, filename: name },
|
|
@@ -518,12 +733,14 @@ function registerTelegramSendFileTool(server, settings, client, adapter) {
|
|
|
518
733
|
}
|
|
519
734
|
}
|
|
520
735
|
}
|
|
521
|
-
const { bytes, filename } = await resolveTelegramFileBytes({
|
|
736
|
+
const { bytes, filename } = strictFile ?? await resolveTelegramFileBytes({
|
|
522
737
|
data: args.data,
|
|
523
738
|
path: args.path,
|
|
739
|
+
...(uploadPath === undefined ? {} : { resolvedPath: uploadPath }),
|
|
524
740
|
filename: args.filename,
|
|
525
741
|
requireFilename: kind === "document",
|
|
526
742
|
maxBytes: maxUploadBytes,
|
|
743
|
+
signal: extra.signal,
|
|
527
744
|
});
|
|
528
745
|
const result = kind === "document"
|
|
529
746
|
? await client.sendDocument({
|
|
@@ -531,13 +748,13 @@ function registerTelegramSendFileTool(server, settings, client, adapter) {
|
|
|
531
748
|
document: bytes,
|
|
532
749
|
filename,
|
|
533
750
|
...(args.caption === undefined ? {} : { caption: args.caption }),
|
|
534
|
-
})
|
|
751
|
+
}, { signal: extra.signal })
|
|
535
752
|
: await client.sendPhoto({
|
|
536
753
|
chat_id: args.chat_id,
|
|
537
754
|
photo: bytes,
|
|
538
755
|
filename,
|
|
539
756
|
...(args.caption === undefined ? {} : { caption: args.caption }),
|
|
540
|
-
});
|
|
757
|
+
}, { signal: extra.signal });
|
|
541
758
|
return {
|
|
542
759
|
content: [{ type: "text", text: `Sent ${kind} ${result.message_id} (${filename}) to ${String(result.chat.id)}.` }],
|
|
543
760
|
structuredContent: { ok: true, chat_id: result.chat.id, message_id: result.message_id, filename },
|
|
@@ -566,8 +783,8 @@ async function resolveTelegramFileBytes(input) {
|
|
|
566
783
|
}
|
|
567
784
|
}
|
|
568
785
|
else {
|
|
569
|
-
const resolved = resolvePath(process.cwd(), input.path);
|
|
570
|
-
bytes = new Uint8Array(await readFile(resolved));
|
|
786
|
+
const resolved = input.resolvedPath ?? resolvePath(process.cwd(), input.path);
|
|
787
|
+
bytes = new Uint8Array(await readFile(resolved, { signal: input.signal }));
|
|
571
788
|
filename = input.filename ?? basename(resolved);
|
|
572
789
|
}
|
|
573
790
|
if (bytes.byteLength === 0) {
|
|
@@ -578,6 +795,110 @@ async function resolveTelegramFileBytes(input) {
|
|
|
578
795
|
}
|
|
579
796
|
return { bytes, filename };
|
|
580
797
|
}
|
|
798
|
+
async function resolveTelegramUploadPath(inputPath) {
|
|
799
|
+
return resolvePath(process.cwd(), inputPath);
|
|
800
|
+
}
|
|
801
|
+
const STRICT_TELEGRAM_PATH_ERROR = "TelegramSendFile: path must be a regular file inside the current run output directory.";
|
|
802
|
+
/**
|
|
803
|
+
* Read a strict upload through one no-follow file descriptor. Every path check
|
|
804
|
+
* deliberately collapses to the same error so callers cannot use the tool as
|
|
805
|
+
* an existence oracle for files outside the run directory.
|
|
806
|
+
*/
|
|
807
|
+
async function readStrictTelegramFile(settings, inputPath, maxBytes, signal, requestedFilename) {
|
|
808
|
+
try {
|
|
809
|
+
signal.throwIfAborted();
|
|
810
|
+
if (settings.runOutputDir === undefined || settings.runOutputIdentity === undefined) {
|
|
811
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
812
|
+
}
|
|
813
|
+
const rootStats = await lstat(settings.runOutputDir);
|
|
814
|
+
if (!rootStats.isDirectory()
|
|
815
|
+
|| rootStats.isSymbolicLink()
|
|
816
|
+
|| !sameFileIdentity(rootStats, settings.runOutputIdentity)) {
|
|
817
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
818
|
+
}
|
|
819
|
+
const rootReal = await realpath(settings.runOutputDir);
|
|
820
|
+
const candidate = resolvePath(process.cwd(), inputPath);
|
|
821
|
+
const candidateLinkStats = await lstat(candidate);
|
|
822
|
+
if (!candidateLinkStats.isFile()
|
|
823
|
+
|| candidateLinkStats.isSymbolicLink()
|
|
824
|
+
|| candidateLinkStats.nlink !== 1) {
|
|
825
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
826
|
+
}
|
|
827
|
+
const candidateReal = await realpath(candidate);
|
|
828
|
+
if (!pathIsInside(rootReal, candidateReal))
|
|
829
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
830
|
+
const handle = await open(candidateReal, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
831
|
+
try {
|
|
832
|
+
const openedStats = await handle.stat();
|
|
833
|
+
if (!openedStats.isFile()
|
|
834
|
+
|| openedStats.nlink !== 1
|
|
835
|
+
|| openedStats.size === 0
|
|
836
|
+
|| openedStats.size > maxBytes) {
|
|
837
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
838
|
+
}
|
|
839
|
+
if (!sameFileIdentity(openedStats, fileIdentity(candidateLinkStats))) {
|
|
840
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
841
|
+
}
|
|
842
|
+
await assertPinnedCandidate(candidateReal, openedStats);
|
|
843
|
+
await assertUnchangedRunRoot(settings.runOutputDir, rootReal, settings.runOutputIdentity);
|
|
844
|
+
const bytes = await readPinnedBytes(handle, openedStats.size, signal);
|
|
845
|
+
await assertPinnedCandidate(candidateReal, openedStats);
|
|
846
|
+
await assertUnchangedRunRoot(settings.runOutputDir, rootReal, settings.runOutputIdentity);
|
|
847
|
+
const finalStats = await handle.stat();
|
|
848
|
+
if (!sameFileIdentity(finalStats, fileIdentity(openedStats))
|
|
849
|
+
|| finalStats.nlink !== 1
|
|
850
|
+
|| finalStats.size !== openedStats.size
|
|
851
|
+
|| bytes.byteLength === 0
|
|
852
|
+
|| bytes.byteLength > maxBytes) {
|
|
853
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
854
|
+
}
|
|
855
|
+
return { bytes, filename: requestedFilename ?? basename(candidateReal) };
|
|
856
|
+
}
|
|
857
|
+
finally {
|
|
858
|
+
await handle.close();
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
if (signal.aborted)
|
|
863
|
+
throw signal.reason ?? error;
|
|
864
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
async function readPinnedBytes(handle, size, signal) {
|
|
868
|
+
const bytes = Buffer.allocUnsafe(size);
|
|
869
|
+
let offset = 0;
|
|
870
|
+
while (offset < size) {
|
|
871
|
+
signal.throwIfAborted();
|
|
872
|
+
const read = await handle.read(bytes, offset, size - offset, offset);
|
|
873
|
+
if (read.bytesRead === 0)
|
|
874
|
+
break;
|
|
875
|
+
offset += read.bytesRead;
|
|
876
|
+
}
|
|
877
|
+
if (offset !== size)
|
|
878
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
879
|
+
return new Uint8Array(bytes);
|
|
880
|
+
}
|
|
881
|
+
function pathIsInside(root, candidate) {
|
|
882
|
+
const fromRoot = relative(root, candidate);
|
|
883
|
+
return fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot);
|
|
884
|
+
}
|
|
885
|
+
async function assertPinnedCandidate(path, opened) {
|
|
886
|
+
const current = await lstat(path);
|
|
887
|
+
if (!current.isFile()
|
|
888
|
+
|| current.isSymbolicLink()
|
|
889
|
+
|| current.nlink !== 1
|
|
890
|
+
|| !sameFileIdentity(current, fileIdentity(opened))) {
|
|
891
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
async function assertUnchangedRunRoot(path, expectedRealPath, expectedIdentity) {
|
|
895
|
+
const current = await lstat(path);
|
|
896
|
+
if (!current.isDirectory() || current.isSymbolicLink() || !sameFileIdentity(current, expectedIdentity)) {
|
|
897
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
898
|
+
}
|
|
899
|
+
if (await realpath(path) !== expectedRealPath)
|
|
900
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
901
|
+
}
|
|
581
902
|
async function resolveSlackSendToolSettings(input, options) {
|
|
582
903
|
try {
|
|
583
904
|
const adapter = await loadSlackModule();
|
|
@@ -605,6 +926,9 @@ async function resolveTelegramSendToolSettings(input, options, tools, askBridge)
|
|
|
605
926
|
if (!config.enabled) {
|
|
606
927
|
return undefined;
|
|
607
928
|
}
|
|
929
|
+
const producingConversationId = optionalString(input.env.MONO_AGENT_ADAPTER_TOOLS_PRODUCING_CONVERSATION_ID);
|
|
930
|
+
const runOutputDir = optionalString(input.env.MONO_AGENT_ADAPTER_TOOLS_RUN_OUTPUT_DIR);
|
|
931
|
+
const runOutputIdentity = parseFileIdentity(input.env.MONO_AGENT_ADAPTER_TOOLS_RUN_OUTPUT_DEV, input.env.MONO_AGENT_ADAPTER_TOOLS_RUN_OUTPUT_INO);
|
|
608
932
|
return {
|
|
609
933
|
botToken: config.botToken,
|
|
610
934
|
allowedChatIds: config.allowedChatIds,
|
|
@@ -613,6 +937,10 @@ async function resolveTelegramSendToolSettings(input, options, tools, askBridge)
|
|
|
613
937
|
maxUploadBytes: config.attachments?.maxUploadBytes ?? adapter.DEFAULT_ATTACHMENT_MAX_BYTES,
|
|
614
938
|
tools,
|
|
615
939
|
...(askBridge === undefined ? {} : { askBridge }),
|
|
940
|
+
...(config.sendTools === undefined ? {} : { sendTools: config.sendTools }),
|
|
941
|
+
...(producingConversationId === undefined ? {} : { producingConversationId }),
|
|
942
|
+
...(runOutputDir === undefined ? {} : { runOutputDir }),
|
|
943
|
+
...(runOutputIdentity === undefined ? {} : { runOutputIdentity }),
|
|
616
944
|
};
|
|
617
945
|
}
|
|
618
946
|
catch (error) {
|
|
@@ -630,11 +958,81 @@ function assertSlackChannelAllowed(settings, channel) {
|
|
|
630
958
|
}
|
|
631
959
|
function assertTelegramChatAllowed(settings, chatId) {
|
|
632
960
|
const normalized = String(chatId);
|
|
961
|
+
if (settings.sendTools?.scope === "producing-conversation") {
|
|
962
|
+
const producingChatId = telegramChatIdFromConversation(settings.producingConversationId);
|
|
963
|
+
if (producingChatId === undefined) {
|
|
964
|
+
throw new Error("TelegramSendMessage: producing Telegram conversation context is unavailable.");
|
|
965
|
+
}
|
|
966
|
+
if (normalized !== producingChatId) {
|
|
967
|
+
throw new Error("TelegramSendMessage: chat_id must match the producing Telegram conversation.");
|
|
968
|
+
}
|
|
969
|
+
}
|
|
633
970
|
if (settings.allowAllChats || settings.allowedChatIds.includes(normalized)) {
|
|
634
971
|
return;
|
|
635
972
|
}
|
|
636
973
|
throw new Error("TelegramSendMessage: chat_id is not allowed by Telegram adapter config.");
|
|
637
974
|
}
|
|
975
|
+
function telegramChatIdFromConversation(conversationId) {
|
|
976
|
+
if (conversationId === undefined)
|
|
977
|
+
return undefined;
|
|
978
|
+
const base = conversationId.split("#", 1)[0] ?? conversationId;
|
|
979
|
+
if (!base.startsWith("telegram:"))
|
|
980
|
+
return undefined;
|
|
981
|
+
const chatId = base.slice("telegram:".length);
|
|
982
|
+
return chatId.length === 0 ? undefined : chatId;
|
|
983
|
+
}
|
|
984
|
+
const SAFE_ADAPTER_RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/u;
|
|
985
|
+
async function ensureAdapterRunOutputDir(root, runId) {
|
|
986
|
+
if (!SAFE_ADAPTER_RUN_ID.test(runId)) {
|
|
987
|
+
throw new Error("adapter-send-tools: run id is unsafe for the run output directory.");
|
|
988
|
+
}
|
|
989
|
+
const outputRoot = resolvePath(root);
|
|
990
|
+
const runOutputDir = join(outputRoot, runId);
|
|
991
|
+
await mkdir(outputRoot, { recursive: true, mode: 0o700 });
|
|
992
|
+
try {
|
|
993
|
+
await mkdir(runOutputDir, { mode: 0o700 });
|
|
994
|
+
}
|
|
995
|
+
catch (error) {
|
|
996
|
+
if (error.code !== "EEXIST")
|
|
997
|
+
throw error;
|
|
998
|
+
const existing = await lstat(runOutputDir);
|
|
999
|
+
if (!existing.isDirectory() || existing.isSymbolicLink()) {
|
|
1000
|
+
throw new Error("adapter-send-tools: run output path is not a real directory.");
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
const current = await lstat(runOutputDir);
|
|
1004
|
+
if (!current.isDirectory() || current.isSymbolicLink()) {
|
|
1005
|
+
throw new Error("adapter-send-tools: run output path is not a real directory.");
|
|
1006
|
+
}
|
|
1007
|
+
return { path: runOutputDir, identity: fileIdentity(current) };
|
|
1008
|
+
}
|
|
1009
|
+
function fileIdentity(stats) {
|
|
1010
|
+
return { dev: stats.dev, ino: stats.ino };
|
|
1011
|
+
}
|
|
1012
|
+
function sameFileIdentity(stats, expected) {
|
|
1013
|
+
return stats.dev === expected.dev && stats.ino === expected.ino;
|
|
1014
|
+
}
|
|
1015
|
+
function parseFileIdentity(devValue, inoValue) {
|
|
1016
|
+
const dev = Number(devValue);
|
|
1017
|
+
const ino = Number(inoValue);
|
|
1018
|
+
return Number.isSafeInteger(dev) && dev >= 0 && Number.isSafeInteger(ino) && ino > 0
|
|
1019
|
+
? { dev, ino }
|
|
1020
|
+
: undefined;
|
|
1021
|
+
}
|
|
1022
|
+
async function removeOwnedDirectory(path, expected) {
|
|
1023
|
+
let current;
|
|
1024
|
+
try {
|
|
1025
|
+
current = await lstat(path);
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
if (error.code === "ENOENT")
|
|
1029
|
+
return;
|
|
1030
|
+
throw error;
|
|
1031
|
+
}
|
|
1032
|
+
if (!current.isDirectory() || current.isSymbolicLink() || !sameFileIdentity(current, expected))
|
|
1033
|
+
return;
|
|
1034
|
+
await rm(path, { recursive: true, force: true });
|
|
1035
|
+
}
|
|
638
1036
|
function optionalString(value) {
|
|
639
1037
|
if (value === undefined) {
|
|
640
1038
|
return undefined;
|