@sayknow-cli/coding-agent 0.2.7 → 0.3.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.
- package/CHANGELOG.md +20 -0
- package/dist/types/async/job-manager.d.ts +3 -1
- package/dist/types/cli/daemon-cli.d.ts +25 -0
- package/dist/types/cli/notify-cli.d.ts +25 -0
- package/dist/types/cli/setup-cli.d.ts +20 -1
- package/dist/types/commands/daemon.d.ts +41 -0
- package/dist/types/commands/notify.d.ts +41 -0
- package/dist/types/config/model-profile-activation.d.ts +12 -0
- package/dist/types/config/model-profiles.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +136 -3
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/daemon/builtin.d.ts +20 -0
- package/dist/types/daemon/control-types.d.ts +57 -0
- package/dist/types/daemon/runtime.d.ts +25 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/i18n/messages/en.d.ts +1 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +2 -0
- package/dist/types/modes/components/settings-defs.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/notifications/attachment-registry.d.ts +17 -0
- package/dist/types/notifications/chat-adapters.d.ts +9 -0
- package/dist/types/notifications/config-commands.d.ts +26 -0
- package/dist/types/notifications/config.d.ts +69 -0
- package/dist/types/notifications/engine.d.ts +59 -0
- package/dist/types/notifications/helpers.d.ts +55 -0
- package/dist/types/notifications/html-format.d.ts +62 -0
- package/dist/types/notifications/index.d.ts +28 -0
- package/dist/types/notifications/managed-daemon.d.ts +48 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
- package/dist/types/notifications/telegram-cli.d.ts +19 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
- package/dist/types/notifications/telegram-daemon.d.ts +295 -0
- package/dist/types/notifications/telegram-reference.d.ts +111 -0
- package/dist/types/notifications/threaded-inbound.d.ts +77 -0
- package/dist/types/notifications/threaded-render.d.ts +71 -0
- package/dist/types/notifications/topic-registry.d.ts +67 -0
- package/dist/types/rlm/index.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +41 -2
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/setup/credential-auto-import.d.ts +63 -0
- package/dist/types/setup/credential-import.d.ts +3 -0
- package/dist/types/setup/host-plugin-setup.d.ts +39 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/state-writer.d.ts +2 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +15 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +14 -0
- package/dist/types/tools/ask-answer-registry.d.ts +13 -0
- package/dist/types/tools/fetch.d.ts +23 -0
- package/dist/types/tools/index.d.ts +19 -0
- package/dist/types/tools/subagent.d.ts +3 -0
- package/dist/types/tools/telegram-send.d.ts +32 -0
- package/dist/types/web/insane/bridge.d.ts +103 -0
- package/dist/types/web/insane/url-guard.d.ts +22 -0
- package/dist/types/web/search/provider.d.ts +18 -1
- package/dist/types/web/search/providers/insane.d.ts +53 -0
- package/dist/types/web/search/providers/text-citations.d.ts +23 -0
- package/dist/types/web/search/types.d.ts +12 -4
- package/package.json +10 -8
- package/scripts/build-binary.ts +3 -0
- package/scripts/verify-insane-vendor.ts +132 -0
- package/src/async/job-manager.ts +5 -1
- package/src/cli/args.ts +1 -1
- package/src/cli/daemon-cli.ts +122 -0
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/notify-cli.ts +421 -0
- package/src/cli/setup-cli.ts +173 -84
- package/src/cli.ts +3 -3
- package/src/commands/daemon.ts +47 -0
- package/src/commands/notify.ts +61 -0
- package/src/commands/setup.ts +11 -1
- package/src/commands/team.ts +1 -1
- package/src/config/model-profile-activation.ts +74 -5
- package/src/config/model-profiles.ts +7 -4
- package/src/config/model-registry.ts +6 -3
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +143 -0
- package/src/coordinator/contract.ts +3 -0
- package/src/coordinator-mcp/server.ts +270 -1
- package/src/daemon/builtin.ts +46 -0
- package/src/daemon/control-types.ts +65 -0
- package/src/daemon/runtime.ts +51 -0
- package/src/defaults/skc/rules/ponytail.md +68 -0
- package/src/defaults/skc/skills/ralplan/SKILL.md +11 -4
- package/src/defaults/skc/skills/ultragoal/SKILL.md +16 -0
- package/src/edit/modes/replace.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/extensibility/shared-events.ts +1 -0
- package/src/goals/tools/goal-tool.ts +11 -2
- package/src/hashline/hash.ts +1 -1
- package/src/i18n/messages/de.ts +1 -0
- package/src/i18n/messages/en.ts +1 -0
- package/src/i18n/messages/es.ts +1 -0
- package/src/i18n/messages/fr.ts +1 -0
- package/src/i18n/messages/ja.ts +1 -0
- package/src/i18n/messages/ko.ts +1 -0
- package/src/i18n/messages/zh.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +12 -10
- package/src/lsp/config.ts +16 -3
- package/src/lsp/defaults.json +7 -0
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +30 -0
- package/src/modes/acp/acp-event-mapper.ts +1 -0
- package/src/modes/components/hook-editor.ts +7 -2
- package/src/modes/components/oauth-selector.ts +19 -0
- package/src/modes/components/settings-defs.ts +2 -1
- package/src/modes/components/settings-selector.ts +7 -2
- package/src/modes/controllers/event-controller.ts +35 -0
- package/src/modes/controllers/selector-controller.ts +80 -17
- package/src/modes/interactive-mode.ts +52 -4
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/event-contract.ts +1 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
- package/src/modes/shared/agent-wire/event-observation.ts +16 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
- package/src/modes/theme/theme.ts +4 -0
- package/src/modes/types.ts +7 -1
- package/src/modes/utils/context-usage.ts +2 -2
- package/src/modes/utils/ui-helpers.ts +23 -0
- package/src/notifications/attachment-registry.ts +23 -0
- package/src/notifications/chat-adapters.ts +147 -0
- package/src/notifications/config-commands.ts +50 -0
- package/src/notifications/config.ts +128 -0
- package/src/notifications/engine.ts +100 -0
- package/src/notifications/helpers.ts +135 -0
- package/src/notifications/html-format.ts +389 -0
- package/src/notifications/index.ts +842 -0
- package/src/notifications/managed-daemon.ts +163 -0
- package/src/notifications/rate-limit-pool.ts +179 -0
- package/src/notifications/telegram-cli.ts +194 -0
- package/src/notifications/telegram-daemon-cli.ts +74 -0
- package/src/notifications/telegram-daemon-control.ts +370 -0
- package/src/notifications/telegram-daemon.ts +1591 -0
- package/src/notifications/telegram-reference.ts +335 -0
- package/src/notifications/threaded-inbound.ts +136 -0
- package/src/notifications/threaded-render.ts +173 -0
- package/src/notifications/topic-registry.ts +133 -0
- package/src/rlm/index.ts +19 -0
- package/src/sdk.ts +16 -0
- package/src/session/agent-session.ts +195 -54
- package/src/session/auth-storage.ts +3 -0
- package/src/session/session-dump-format.ts +43 -2
- package/src/session/session-manager.ts +39 -5
- package/src/setup/credential-auto-import.ts +258 -0
- package/src/setup/credential-import.ts +17 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
- package/src/setup/host-plugin-setup.ts +142 -0
- package/src/skc-runtime/deep-interview-recorder.ts +2 -2
- package/src/skc-runtime/launch-tmux.ts +27 -5
- package/src/skc-runtime/ledger-event-renderer.ts +1 -0
- package/src/skc-runtime/ralplan-runtime.ts +2 -2
- package/src/skc-runtime/state-runtime.ts +18 -10
- package/src/skc-runtime/state-writer.ts +8 -8
- package/src/skc-runtime/tmux-common.ts +8 -0
- package/src/skc-runtime/tmux-sessions.ts +8 -1
- package/src/skc-runtime/ultragoal-guard.ts +57 -2
- package/src/skc-runtime/ultragoal-runtime.ts +105 -19
- package/src/skc-runtime/workflow-manifest.generated.json +56 -2
- package/src/skc-runtime/workflow-manifest.ts +18 -3
- package/src/slash-commands/builtin-registry.ts +4 -1
- package/src/task/executor.ts +5 -1
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/ask.ts +77 -6
- package/src/tools/fetch.ts +78 -1
- package/src/tools/image-gen.ts +5 -8
- package/src/tools/index.ts +22 -0
- package/src/tools/inspect-image.ts +16 -11
- package/src/tools/subagent-render.ts +7 -0
- package/src/tools/subagent.ts +38 -7
- package/src/tools/telegram-send.ts +137 -0
- package/src/web/insane/bridge.ts +350 -0
- package/src/web/insane/url-guard.ts +155 -0
- package/src/web/search/provider.ts +77 -18
- package/src/web/search/providers/anthropic.ts +70 -3
- package/src/web/search/providers/codex.ts +1 -119
- package/src/web/search/providers/gemini.ts +99 -0
- package/src/web/search/providers/insane.ts +551 -0
- package/src/web/search/providers/openai-compatible.ts +66 -32
- package/src/web/search/providers/text-citations.ts +111 -0
- package/src/web/search/types.ts +13 -2
- package/vendor/insane-search/LICENSE +21 -0
- package/vendor/insane-search/MANIFEST.json +24 -0
- package/vendor/insane-search/engine/__init__.py +23 -0
- package/vendor/insane-search/engine/__main__.py +128 -0
- package/vendor/insane-search/engine/bias_check.py +183 -0
- package/vendor/insane-search/engine/executor.py +254 -0
- package/vendor/insane-search/engine/fetch_chain.py +725 -0
- package/vendor/insane-search/engine/learning.py +175 -0
- package/vendor/insane-search/engine/phase0.py +214 -0
- package/vendor/insane-search/engine/safety.py +91 -0
- package/vendor/insane-search/engine/templates/package.json +11 -0
- package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
- package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
- package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
- package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
- package/vendor/insane-search/engine/tests/test_u1.py +200 -0
- package/vendor/insane-search/engine/tests/test_u4.py +131 -0
- package/vendor/insane-search/engine/tests/test_u5.py +163 -0
- package/vendor/insane-search/engine/tests/test_u7.py +124 -0
- package/vendor/insane-search/engine/transport.py +211 -0
- package/vendor/insane-search/engine/url_transforms.py +98 -0
- package/vendor/insane-search/engine/validators.py +331 -0
- package/vendor/insane-search/engine/waf_detector.py +214 -0
- package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
package/src/tools/subagent.ts
CHANGED
|
@@ -17,6 +17,8 @@ const MAX_LIST_LIMIT = 50;
|
|
|
17
17
|
const RECEIPT_PREVIEW_WIDTH = 280;
|
|
18
18
|
const PREVIEW_WIDTH = 2_000;
|
|
19
19
|
const FULL_PREVIEW_WIDTH = 12_000;
|
|
20
|
+
const STEER_QUEUED_GUIDANCE =
|
|
21
|
+
"The steer message is queued for the subagent's next steering boundary and has not necessarily taken effect yet.";
|
|
20
22
|
|
|
21
23
|
const subagentSchema = z.object({
|
|
22
24
|
action: z
|
|
@@ -63,6 +65,9 @@ export interface SubagentSnapshot {
|
|
|
63
65
|
outputRef?: string;
|
|
64
66
|
truncated?: boolean;
|
|
65
67
|
guidance?: string;
|
|
68
|
+
steerMessage?: string;
|
|
69
|
+
steerState?: "queued" | "resume_queued" | "resume_started";
|
|
70
|
+
steerPauseRequested?: boolean;
|
|
66
71
|
/** Live streaming progress for the awaited subagent (await panel only; UI detail). */
|
|
67
72
|
progress?: AgentProgress;
|
|
68
73
|
/** True when a live in-session progress producer exists for this subagent. */
|
|
@@ -240,6 +245,7 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
240
245
|
}
|
|
241
246
|
const records: SubagentRecord[] = [];
|
|
242
247
|
const missing: SubagentSnapshot[] = [];
|
|
248
|
+
const steerStates = new Map<string, NonNullable<SubagentSnapshot["steerState"]>>();
|
|
243
249
|
const record = this.#findVisibleRecord(manager, id, ownerFilter);
|
|
244
250
|
const verifiedOutputIds = await this.#verifiedOutputIds(record ? [record] : []);
|
|
245
251
|
if (!record) {
|
|
@@ -249,23 +255,41 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
249
255
|
if (record.status === "running") {
|
|
250
256
|
const handle = manager.getLiveHandle(record.subagentId);
|
|
251
257
|
if (!handle) throw new ToolError(`Subagent ${record.subagentId} has no live handle.`);
|
|
252
|
-
|
|
258
|
+
const fromAgentId = this.session.getAgentId?.() ?? undefined;
|
|
259
|
+
await handle.injectMessage(message, "steer", { fromAgentId });
|
|
253
260
|
if (params.pause === true) manager.pauseSubagent(record.subagentId, ownerFilter);
|
|
261
|
+
records.push(manager.getSubagentRecord(record.subagentId, ownerFilter) ?? record);
|
|
262
|
+
steerStates.set(record.subagentId, "queued");
|
|
254
263
|
} else {
|
|
255
264
|
const result = manager.resumeSubagent(record.subagentId, ownerFilter, message);
|
|
256
|
-
if (!result.ok && result.reason === "context_unavailable") throw new ToolError("context unavailable");
|
|
257
265
|
if (!result.ok && result.reason === "not_found") {
|
|
258
266
|
missing.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
|
|
267
|
+
} else if (!result.ok) {
|
|
268
|
+
throw new ToolError(`Failed to resume subagent ${record.subagentId}: ${result.reason ?? "unknown"}.`);
|
|
259
269
|
} else {
|
|
260
|
-
|
|
270
|
+
const snapshotRecord = manager.getSubagentRecord(record.subagentId, ownerFilter) ?? record;
|
|
271
|
+
records.push(snapshotRecord);
|
|
272
|
+
steerStates.set(
|
|
273
|
+
snapshotRecord.subagentId,
|
|
274
|
+
result.queued === true || result.status === "queued" ? "resume_queued" : "resume_started",
|
|
275
|
+
);
|
|
261
276
|
}
|
|
262
277
|
}
|
|
263
|
-
if (record.status === "running")
|
|
264
|
-
records.push(manager.getSubagentRecord(record.subagentId, ownerFilter) ?? record);
|
|
265
278
|
}
|
|
266
279
|
return this.#buildSnapshotResult(
|
|
267
280
|
[
|
|
268
|
-
...records.map(record =>
|
|
281
|
+
...records.map(record => {
|
|
282
|
+
const snapshot = this.#recordSnapshot(manager, record, false, verbosity, verifiedOutputIds);
|
|
283
|
+
return {
|
|
284
|
+
...snapshot,
|
|
285
|
+
steerMessage: message,
|
|
286
|
+
steerState: steerStates.get(record.subagentId) ?? "queued",
|
|
287
|
+
steerPauseRequested: params.pause === true,
|
|
288
|
+
guidance: snapshot.guidance
|
|
289
|
+
? `${snapshot.guidance} ${STEER_QUEUED_GUIDANCE}`
|
|
290
|
+
: STEER_QUEUED_GUIDANCE,
|
|
291
|
+
};
|
|
292
|
+
}),
|
|
269
293
|
...missing,
|
|
270
294
|
],
|
|
271
295
|
"Subagent steer",
|
|
@@ -288,7 +312,7 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
288
312
|
if (ids.length === 1) return ids[0]!;
|
|
289
313
|
if (ids.length > 1) {
|
|
290
314
|
throw new ToolError(
|
|
291
|
-
`\`${action}\` accepts exactly one target because \`message\`
|
|
315
|
+
`\`${action}\` accepts exactly one target because \`message\` can be queued for only one subagent.`,
|
|
292
316
|
);
|
|
293
317
|
}
|
|
294
318
|
throw new ToolError(`\`${action}\` requires a single subagent id via \`id\`.`);
|
|
@@ -533,6 +557,10 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
533
557
|
if (snapshot.description) lines.push(`Description: ${snapshot.description}`);
|
|
534
558
|
if (snapshot.outputRef) lines.push(`Output: ${snapshot.outputRef}`);
|
|
535
559
|
if (snapshot.assignment) lines.push("Assignment:", "```", snapshot.assignment, "```");
|
|
560
|
+
if (snapshot.steerMessage) {
|
|
561
|
+
lines.push(`Steer (${snapshot.steerState ?? "queued"}):`, "```", snapshot.steerMessage, "```");
|
|
562
|
+
lines.push(STEER_QUEUED_GUIDANCE);
|
|
563
|
+
}
|
|
536
564
|
if (snapshot.resultPreview) {
|
|
537
565
|
lines.push(snapshot.errorText ? "Error preview:" : "Result preview:", "```", snapshot.resultPreview, "```");
|
|
538
566
|
if (snapshot.truncated)
|
|
@@ -770,6 +798,9 @@ function canonicalizeSnapshotForSignature(snapshot: SubagentSnapshot): unknown {
|
|
|
770
798
|
outputRef: snapshot.outputRef ?? null,
|
|
771
799
|
truncated: snapshot.truncated ?? false,
|
|
772
800
|
guidance: snapshot.guidance ?? null,
|
|
801
|
+
steerMessage: snapshot.steerMessage ?? null,
|
|
802
|
+
steerState: snapshot.steerState ?? null,
|
|
803
|
+
steerPauseRequested: snapshot.steerPauseRequested ?? false,
|
|
773
804
|
liveProgressAvailable: snapshot.liveProgressAvailable ?? null,
|
|
774
805
|
effectiveModel: snapshot.effectiveModel ?? null,
|
|
775
806
|
requestedModel: snapshot.requestedModel ?? null,
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@sayknow-cli/agent-core";
|
|
4
|
+
import { z } from "zod/v4";
|
|
5
|
+
import { getTelegramFileSink } from "../notifications/attachment-registry";
|
|
6
|
+
import { getNotificationConfig, isGloballyConfigured } from "../notifications/config";
|
|
7
|
+
import type { ToolSession } from "./index";
|
|
8
|
+
|
|
9
|
+
const telegramSendSchema = z.object({
|
|
10
|
+
path: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe("file path (absolute or relative to cwd) to send to Telegram; must resolve inside the workspace"),
|
|
13
|
+
caption: z.string().optional().describe("optional caption"),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
type TelegramSendParams = z.infer<typeof telegramSendSchema>;
|
|
17
|
+
|
|
18
|
+
interface TelegramSendDetails {
|
|
19
|
+
path: string;
|
|
20
|
+
caption?: string;
|
|
21
|
+
ok: boolean;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class TelegramSendTool implements AgentTool<typeof telegramSendSchema, TelegramSendDetails> {
|
|
26
|
+
readonly name = "telegram_send";
|
|
27
|
+
readonly label = "TelegramSend";
|
|
28
|
+
readonly summary = "Send a workspace file to Telegram";
|
|
29
|
+
readonly loadMode = "discoverable";
|
|
30
|
+
readonly description =
|
|
31
|
+
"Send a file from the current workspace to the connected Telegram chat as a document. The path must resolve " +
|
|
32
|
+
"(after following symlinks) to a regular file inside the project root; paths outside the workspace are rejected.";
|
|
33
|
+
readonly parameters = telegramSendSchema;
|
|
34
|
+
readonly strict = true;
|
|
35
|
+
|
|
36
|
+
constructor(private readonly session: ToolSession) {}
|
|
37
|
+
|
|
38
|
+
static createIf(session: ToolSession): TelegramSendTool | null {
|
|
39
|
+
return isGloballyConfigured(getNotificationConfig(session.settings)) ? new TelegramSendTool(session) : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve `requested` against the workspace root and confine it via realpath:
|
|
44
|
+
* blocks absolute paths outside the project, `..` traversal, and symlinks that
|
|
45
|
+
* escape the root. Returns the resolved real path of a regular file, or an
|
|
46
|
+
* error message. This is the egress safety boundary — the model can only send
|
|
47
|
+
* files that genuinely live inside the session workspace.
|
|
48
|
+
*/
|
|
49
|
+
private async resolveContainedFile(
|
|
50
|
+
requested: string,
|
|
51
|
+
): Promise<{ ok: true; path: string } | { ok: false; error: string }> {
|
|
52
|
+
let root: string;
|
|
53
|
+
try {
|
|
54
|
+
root = await fs.promises.realpath(this.session.cwd);
|
|
55
|
+
} catch {
|
|
56
|
+
return { ok: false, error: "workspace root is unavailable" };
|
|
57
|
+
}
|
|
58
|
+
const absolute = path.isAbsolute(requested) ? requested : path.resolve(root, requested);
|
|
59
|
+
let real: string;
|
|
60
|
+
try {
|
|
61
|
+
real = await fs.promises.realpath(absolute);
|
|
62
|
+
} catch {
|
|
63
|
+
return { ok: false, error: `file not found: ${requested}` };
|
|
64
|
+
}
|
|
65
|
+
const rel = path.relative(root, real);
|
|
66
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
|
|
67
|
+
return { ok: false, error: "path escapes the workspace root; only files inside the project can be sent" };
|
|
68
|
+
}
|
|
69
|
+
let stat: fs.Stats;
|
|
70
|
+
try {
|
|
71
|
+
stat = await fs.promises.stat(real);
|
|
72
|
+
} catch {
|
|
73
|
+
return { ok: false, error: `file not found: ${requested}` };
|
|
74
|
+
}
|
|
75
|
+
if (!stat.isFile()) {
|
|
76
|
+
return { ok: false, error: "not a regular file" };
|
|
77
|
+
}
|
|
78
|
+
return { ok: true, path: real };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async execute(
|
|
82
|
+
_toolCallId: string,
|
|
83
|
+
params: TelegramSendParams,
|
|
84
|
+
_signal?: AbortSignal,
|
|
85
|
+
_onUpdate?: AgentToolUpdateCallback<TelegramSendDetails>,
|
|
86
|
+
_context?: AgentToolContext,
|
|
87
|
+
): Promise<AgentToolResult<TelegramSendDetails>> {
|
|
88
|
+
const sessionId = this.session.getSessionId?.();
|
|
89
|
+
if (!sessionId) {
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text: "telegram_send: no active session id" }],
|
|
92
|
+
details: { path: params.path, caption: params.caption, ok: false, error: "no active session id" },
|
|
93
|
+
isError: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const contained = await this.resolveContainedFile(params.path);
|
|
98
|
+
if (!contained.ok) {
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: "text", text: `telegram_send: ${contained.error}` }],
|
|
101
|
+
details: { path: params.path, caption: params.caption, ok: false, error: contained.error },
|
|
102
|
+
isError: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const abs = contained.path;
|
|
106
|
+
|
|
107
|
+
const sink = getTelegramFileSink(sessionId);
|
|
108
|
+
if (!sink) {
|
|
109
|
+
return {
|
|
110
|
+
content: [
|
|
111
|
+
{ type: "text", text: "telegram_send: Telegram notifications are not connected for this session" },
|
|
112
|
+
],
|
|
113
|
+
details: {
|
|
114
|
+
path: abs,
|
|
115
|
+
caption: params.caption,
|
|
116
|
+
ok: false,
|
|
117
|
+
error: "Telegram notifications are not connected",
|
|
118
|
+
},
|
|
119
|
+
isError: true,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const result = await sink({ path: abs, caption: params.caption });
|
|
124
|
+
if (result.ok) {
|
|
125
|
+
return {
|
|
126
|
+
content: [{ type: "text", text: `Sent ${path.basename(abs)} to Telegram.` }],
|
|
127
|
+
details: { path: abs, caption: params.caption, ok: true },
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: "text", text: `telegram_send failed: ${result.error}` }],
|
|
133
|
+
details: { path: abs, caption: params.caption, ok: false, error: result.error },
|
|
134
|
+
isError: true,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge from TypeScript to the vendored insane-search Python engine.
|
|
3
|
+
*
|
|
4
|
+
* Invokes `python3 -m engine "<url>" --json` per fallback attempt (cwd + PYTHONPATH
|
|
5
|
+
* pointed at the vendored engine), validates the JSON envelope, and maps it onto a
|
|
6
|
+
* discriminated result. Hardened: clamped timeout, AbortSignal propagation that
|
|
7
|
+
* kills+reaps the child, bounded stdout/stderr capture, and a per-process
|
|
8
|
+
* concurrency cap so blocked reads cannot fork-storm.
|
|
9
|
+
*
|
|
10
|
+
* Fail-closed: missing dependencies / bad output / auth-required never throw past
|
|
11
|
+
* the caller and never auto-install anything; they return ok:false with a stable,
|
|
12
|
+
* bounded note so `read` can continue with its normal degraded result.
|
|
13
|
+
*/
|
|
14
|
+
import { type ChildProcess, spawn as nodeSpawn } from "node:child_process";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { $which } from "@sayknow-cli/utils";
|
|
18
|
+
|
|
19
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
/** packages/coding-agent/vendor/insane-search */
|
|
21
|
+
export const INSANE_VENDOR_DIR = path.resolve(HERE, "../../../vendor/insane-search");
|
|
22
|
+
const TEMPLATES_DIR = path.join(INSANE_VENDOR_DIR, "engine", "templates");
|
|
23
|
+
|
|
24
|
+
const MAX_STDOUT_BYTES = 8 * 1024 * 1024;
|
|
25
|
+
const MAX_STDERR_BYTES = 64 * 1024;
|
|
26
|
+
const DEFAULT_TIMEOUT_MS = 25_000;
|
|
27
|
+
const MIN_TIMEOUT_MS = 1_000;
|
|
28
|
+
const MAX_TIMEOUT_MS = 120_000;
|
|
29
|
+
const DEFAULT_CONCURRENCY = 2;
|
|
30
|
+
const KILL_GRACE_MS = 2_000;
|
|
31
|
+
|
|
32
|
+
/** Stable note prefixes — tests assert on these without depending on full stderr. */
|
|
33
|
+
export const INSANE_NOTES = {
|
|
34
|
+
guardBlocked: (reason: string) => `insane fallback blocked: target URL is not public HTTP(S): ${reason}`,
|
|
35
|
+
vendorMissing: `insane fallback unavailable: vendor engine missing at packages/coding-agent/vendor/insane-search`,
|
|
36
|
+
noPython: `insane fallback unavailable: python3 not found; install python3 and curl_cffi, then retry with web.insaneFallback=true`,
|
|
37
|
+
noCurlCffi: `insane fallback unavailable: python3 cannot import curl_cffi; install curl_cffi for Phase 0-2`,
|
|
38
|
+
noBrowser: `insane fallback unavailable: node/playwright/stealth dependencies missing for Phase 3; install dependencies under packages/coding-agent/vendor/insane-search/engine/templates`,
|
|
39
|
+
timeout: (seconds: number) => `insane fallback timed out after ${seconds}s; normal read fallback preserved`,
|
|
40
|
+
invalidJson: `insane fallback failed: engine returned invalid JSON`,
|
|
41
|
+
authRequired: `insane fallback stopped: authentication required`,
|
|
42
|
+
verdict: (verdict: string) => `insane fallback failed: engine returned verdict=${verdict}`,
|
|
43
|
+
untried: (routes: string) => `insane fallback routes not tried: ${routes}`,
|
|
44
|
+
mustBrowserMcp: `insane fallback requires browser MCP/manual phase: must_invoke_playwright_mcp=true`,
|
|
45
|
+
concurrency: `insane fallback skipped: max concurrent engine attempts reached`,
|
|
46
|
+
emptyContent: `insane fallback failed: engine reported ok but returned no content`,
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
/** Raw JSON envelope produced by `python3 -m engine --json`. */
|
|
50
|
+
export interface InsaneFetchResultRaw {
|
|
51
|
+
ok?: boolean;
|
|
52
|
+
verdict?: string;
|
|
53
|
+
content?: string;
|
|
54
|
+
profile_used?: string;
|
|
55
|
+
trace?: unknown;
|
|
56
|
+
untried_routes?: string[];
|
|
57
|
+
must_invoke_playwright_mcp?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface InsaneSuccess {
|
|
61
|
+
ok: true;
|
|
62
|
+
content: string;
|
|
63
|
+
profileUsed?: string;
|
|
64
|
+
notes: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface InsaneFailure {
|
|
68
|
+
ok: false;
|
|
69
|
+
reason: string;
|
|
70
|
+
verdict?: string;
|
|
71
|
+
notes: string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type InsaneBridgeResult = InsaneSuccess | InsaneFailure;
|
|
75
|
+
|
|
76
|
+
export interface EngineInvocation {
|
|
77
|
+
url: string;
|
|
78
|
+
timeoutMs: number;
|
|
79
|
+
signal?: AbortSignal;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface EngineRawOutput {
|
|
83
|
+
code: number | null;
|
|
84
|
+
stdout: string;
|
|
85
|
+
stderr: string;
|
|
86
|
+
timedOut: boolean;
|
|
87
|
+
aborted: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Seam: run the engine subprocess. Default spawns python3. */
|
|
91
|
+
export type EngineRunner = (inv: EngineInvocation) => Promise<EngineRawOutput>;
|
|
92
|
+
|
|
93
|
+
export interface InsaneDependencyStatus {
|
|
94
|
+
vendorPresent: boolean;
|
|
95
|
+
python: boolean;
|
|
96
|
+
curlCffi: boolean;
|
|
97
|
+
browser: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Seam: probe dependencies. Default probes the real environment (cached). */
|
|
101
|
+
export type DependencyProber = () => Promise<InsaneDependencyStatus>;
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Subprocess runner
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
type SpawnImpl = typeof nodeSpawn;
|
|
108
|
+
|
|
109
|
+
function clampTimeoutMs(timeoutMs: number | undefined): number {
|
|
110
|
+
const value = timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
111
|
+
if (!Number.isFinite(value)) return DEFAULT_TIMEOUT_MS;
|
|
112
|
+
return Math.max(MIN_TIMEOUT_MS, Math.min(MAX_TIMEOUT_MS, Math.floor(value)));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function appendCapped(buffer: string, chunk: string, cap: number): string {
|
|
116
|
+
if (buffer.length >= cap) return buffer;
|
|
117
|
+
const remaining = cap - buffer.length;
|
|
118
|
+
return buffer + (chunk.length > remaining ? chunk.slice(0, remaining) : chunk);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Kill a child and its group, escalating to SIGKILL after a grace period. */
|
|
122
|
+
function killChild(child: ChildProcess): void {
|
|
123
|
+
try {
|
|
124
|
+
child.kill("SIGTERM");
|
|
125
|
+
} catch {
|
|
126
|
+
// already gone
|
|
127
|
+
}
|
|
128
|
+
const timer = setTimeout(() => {
|
|
129
|
+
try {
|
|
130
|
+
child.kill("SIGKILL");
|
|
131
|
+
} catch {
|
|
132
|
+
// already gone
|
|
133
|
+
}
|
|
134
|
+
}, KILL_GRACE_MS);
|
|
135
|
+
timer.unref?.();
|
|
136
|
+
child.once("exit", () => clearTimeout(timer));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Real engine runner: `python3 -m engine "<url>" --json`. */
|
|
140
|
+
export function runEngineSubprocess(
|
|
141
|
+
inv: EngineInvocation,
|
|
142
|
+
options: { spawnImpl?: SpawnImpl } = {},
|
|
143
|
+
): Promise<EngineRawOutput> {
|
|
144
|
+
const spawnImpl = options.spawnImpl ?? nodeSpawn;
|
|
145
|
+
return new Promise<EngineRawOutput>(resolve => {
|
|
146
|
+
let stdout = "";
|
|
147
|
+
let stderr = "";
|
|
148
|
+
let settled = false;
|
|
149
|
+
let timedOut = false;
|
|
150
|
+
let aborted = false;
|
|
151
|
+
|
|
152
|
+
const child = spawnImpl("python3", ["-m", "engine", inv.url, "--json"], {
|
|
153
|
+
cwd: INSANE_VENDOR_DIR,
|
|
154
|
+
env: { ...process.env, PYTHONPATH: INSANE_VENDOR_DIR },
|
|
155
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const finish = (code: number | null): void => {
|
|
159
|
+
if (settled) return;
|
|
160
|
+
settled = true;
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
inv.signal?.removeEventListener("abort", onAbort);
|
|
163
|
+
resolve({ code, stdout, stderr, timedOut, aborted });
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const timer = setTimeout(() => {
|
|
167
|
+
timedOut = true;
|
|
168
|
+
killChild(child);
|
|
169
|
+
}, inv.timeoutMs);
|
|
170
|
+
timer.unref?.();
|
|
171
|
+
|
|
172
|
+
const onAbort = (): void => {
|
|
173
|
+
aborted = true;
|
|
174
|
+
killChild(child);
|
|
175
|
+
};
|
|
176
|
+
if (inv.signal) {
|
|
177
|
+
if (inv.signal.aborted) onAbort();
|
|
178
|
+
else inv.signal.addEventListener("abort", onAbort, { once: true });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
182
|
+
stdout = appendCapped(stdout, chunk.toString("utf8"), MAX_STDOUT_BYTES);
|
|
183
|
+
});
|
|
184
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
185
|
+
stderr = appendCapped(stderr, chunk.toString("utf8"), MAX_STDERR_BYTES);
|
|
186
|
+
});
|
|
187
|
+
child.on("error", () => finish(null));
|
|
188
|
+
child.on("close", code => finish(code));
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Dependency probes (cached)
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
let probeCache: Promise<InsaneDependencyStatus> | null = null;
|
|
197
|
+
|
|
198
|
+
/** Reset the probe cache between tests so probe state never leaks. */
|
|
199
|
+
export function resetInsaneProbeCacheForTest(): void {
|
|
200
|
+
probeCache = null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function runProbeCommand(cmd: string, args: string[], cwd?: string): Promise<boolean> {
|
|
204
|
+
return new Promise<boolean>(resolve => {
|
|
205
|
+
let settled = false;
|
|
206
|
+
const done = (ok: boolean): void => {
|
|
207
|
+
if (settled) return;
|
|
208
|
+
settled = true;
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
resolve(ok);
|
|
211
|
+
};
|
|
212
|
+
const child = nodeSpawn(cmd, args, { cwd, stdio: "ignore" });
|
|
213
|
+
const timer = setTimeout(() => {
|
|
214
|
+
try {
|
|
215
|
+
child.kill("SIGKILL");
|
|
216
|
+
} catch {
|
|
217
|
+
// gone
|
|
218
|
+
}
|
|
219
|
+
done(false);
|
|
220
|
+
}, 10_000);
|
|
221
|
+
timer.unref?.();
|
|
222
|
+
child.on("error", () => done(false));
|
|
223
|
+
child.on("close", code => done(code === 0));
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function probeRealDependencies(): Promise<InsaneDependencyStatus> {
|
|
228
|
+
const { existsSync } = await import("node:fs");
|
|
229
|
+
const vendorPresent = existsSync(path.join(INSANE_VENDOR_DIR, "engine", "__main__.py"));
|
|
230
|
+
if (!vendorPresent) {
|
|
231
|
+
return { vendorPresent: false, python: false, curlCffi: false, browser: false };
|
|
232
|
+
}
|
|
233
|
+
const python = Boolean($which("python3"));
|
|
234
|
+
const curlCffi = python ? await runProbeCommand("python3", ["-c", "import curl_cffi"]) : false;
|
|
235
|
+
const node = Boolean($which("node"));
|
|
236
|
+
const browser = node
|
|
237
|
+
? await runProbeCommand(
|
|
238
|
+
"node",
|
|
239
|
+
[
|
|
240
|
+
"-e",
|
|
241
|
+
"require.resolve('playwright');require.resolve('playwright-extra');require.resolve('puppeteer-extra-plugin-stealth')",
|
|
242
|
+
],
|
|
243
|
+
TEMPLATES_DIR,
|
|
244
|
+
)
|
|
245
|
+
: false;
|
|
246
|
+
return { vendorPresent, python, curlCffi, browser };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Probe (and cache) the insane-search runtime dependencies. */
|
|
250
|
+
export function probeInsaneDependencies(): Promise<InsaneDependencyStatus> {
|
|
251
|
+
if (!probeCache) probeCache = probeRealDependencies();
|
|
252
|
+
return probeCache;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// Concurrency gate
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
let inFlight = 0;
|
|
260
|
+
|
|
261
|
+
export function resetInsaneConcurrencyForTest(): void {
|
|
262
|
+
inFlight = 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// High-level bridge
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
export interface TryInsaneFetchOptions {
|
|
270
|
+
timeoutMs?: number;
|
|
271
|
+
signal?: AbortSignal;
|
|
272
|
+
concurrencyLimit?: number;
|
|
273
|
+
/** Seam: dependency prober (default real, cached). */
|
|
274
|
+
prober?: DependencyProber;
|
|
275
|
+
/** Seam: engine runner (default real subprocess). */
|
|
276
|
+
runner?: EngineRunner;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function mapEngineOutput(raw: EngineRawOutput, timeoutMs: number): InsaneBridgeResult {
|
|
280
|
+
const notes: string[] = [];
|
|
281
|
+
if (raw.aborted) {
|
|
282
|
+
return { ok: false, reason: "aborted", notes };
|
|
283
|
+
}
|
|
284
|
+
if (raw.timedOut) {
|
|
285
|
+
notes.push(INSANE_NOTES.timeout(Math.round(timeoutMs / 1000)));
|
|
286
|
+
return { ok: false, reason: "timeout", notes };
|
|
287
|
+
}
|
|
288
|
+
let parsed: InsaneFetchResultRaw;
|
|
289
|
+
try {
|
|
290
|
+
parsed = JSON.parse(raw.stdout) as InsaneFetchResultRaw;
|
|
291
|
+
} catch {
|
|
292
|
+
notes.push(INSANE_NOTES.invalidJson);
|
|
293
|
+
return { ok: false, reason: "invalid-json", notes };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const verdict = parsed.verdict?.trim();
|
|
297
|
+
// The engine emits the Verdict enum value `auth_required` (401/407); also tolerate
|
|
298
|
+
// the human-readable phrase defensively. Either is a terminal public-content boundary.
|
|
299
|
+
if (verdict && /^(?:auth_required|authentication required)$/i.test(verdict)) {
|
|
300
|
+
notes.push(INSANE_NOTES.authRequired);
|
|
301
|
+
return { ok: false, reason: "auth-required", verdict, notes };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (parsed.untried_routes && parsed.untried_routes.length > 0) {
|
|
305
|
+
notes.push(INSANE_NOTES.untried(parsed.untried_routes.slice(0, 8).join(", ")));
|
|
306
|
+
}
|
|
307
|
+
if (parsed.must_invoke_playwright_mcp) {
|
|
308
|
+
notes.push(INSANE_NOTES.mustBrowserMcp);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (parsed.ok && typeof parsed.content === "string" && parsed.content.trim().length > 0) {
|
|
312
|
+
return { ok: true, content: parsed.content, profileUsed: parsed.profile_used, notes };
|
|
313
|
+
}
|
|
314
|
+
if (parsed.ok) {
|
|
315
|
+
notes.push(INSANE_NOTES.emptyContent);
|
|
316
|
+
return { ok: false, reason: "empty-content", notes };
|
|
317
|
+
}
|
|
318
|
+
notes.push(INSANE_NOTES.verdict(verdict || "unknown"));
|
|
319
|
+
return { ok: false, reason: "engine-failed", verdict, notes };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Attempt to read `url` through the insane-search engine. The caller is
|
|
324
|
+
* responsible for the opt-in gate, raw-mode skip, and the public-URL guard
|
|
325
|
+
* (which MUST run before this is called). Never throws; always returns a result.
|
|
326
|
+
*/
|
|
327
|
+
export async function tryInsaneFetch(url: string, options: TryInsaneFetchOptions = {}): Promise<InsaneBridgeResult> {
|
|
328
|
+
const prober = options.prober ?? probeInsaneDependencies;
|
|
329
|
+
const runner = options.runner ?? (inv => runEngineSubprocess(inv));
|
|
330
|
+
const limit = options.concurrencyLimit ?? DEFAULT_CONCURRENCY;
|
|
331
|
+
|
|
332
|
+
const deps = await prober();
|
|
333
|
+
if (!deps.vendorPresent) return { ok: false, reason: "vendor-missing", notes: [INSANE_NOTES.vendorMissing] };
|
|
334
|
+
if (!deps.python) return { ok: false, reason: "no-python", notes: [INSANE_NOTES.noPython] };
|
|
335
|
+
if (!deps.curlCffi) return { ok: false, reason: "no-curl-cffi", notes: [INSANE_NOTES.noCurlCffi] };
|
|
336
|
+
if (!deps.browser) return { ok: false, reason: "no-browser", notes: [INSANE_NOTES.noBrowser] };
|
|
337
|
+
|
|
338
|
+
if (inFlight >= limit) {
|
|
339
|
+
return { ok: false, reason: "concurrency", notes: [INSANE_NOTES.concurrency] };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
inFlight++;
|
|
343
|
+
try {
|
|
344
|
+
const timeoutMs = clampTimeoutMs(options.timeoutMs);
|
|
345
|
+
const raw = await runner({ url, timeoutMs, signal: options.signal });
|
|
346
|
+
return mapEngineOutput(raw, timeoutMs);
|
|
347
|
+
} finally {
|
|
348
|
+
inFlight--;
|
|
349
|
+
}
|
|
350
|
+
}
|