@sayknow-cli/coding-agent 0.3.1 → 0.3.4
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/bin/skc.js +4 -0
- package/dist/types/cli/mcp-cli.d.ts +25 -0
- package/dist/types/cli/plugin-cli.d.ts +2 -0
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/commands/mcp.d.ts +70 -0
- package/dist/types/commands/plugin.d.ts +6 -0
- package/dist/types/commands/session.d.ts +6 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +8 -1
- package/dist/types/config/model-profiles.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config-schema.d.ts +0 -5
- package/dist/types/config/settings-schema.d.ts +79 -68
- package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/skc-plugins/compiler.d.ts +19 -0
- package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +29 -0
- package/dist/types/extensibility/skc-plugins/index.d.ts +9 -0
- package/dist/types/extensibility/skc-plugins/injection.d.ts +9 -0
- package/dist/types/extensibility/skc-plugins/installer.d.ts +13 -0
- package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +26 -0
- package/dist/types/extensibility/skc-plugins/observability.d.ts +27 -0
- package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +16 -0
- package/dist/types/extensibility/skc-plugins/registry.d.ts +32 -0
- package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +64 -0
- package/dist/types/extensibility/skc-plugins/session-validation.d.ts +42 -0
- package/dist/types/extensibility/skc-plugins/types.d.ts +158 -2
- package/dist/types/extensibility/skc-plugins/validation.d.ts +8 -1
- package/dist/types/i18n/messages/en.d.ts +0 -1
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +8 -0
- package/dist/types/modes/components/settings-defs.d.ts +0 -1
- package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
- package/dist/types/modes/theme/defaults/index.d.ts +99 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/notifications/html-format.d.ts +11 -0
- package/dist/types/notifications/index.d.ts +149 -1
- package/dist/types/notifications/lifecycle-commands.d.ts +60 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
- package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
- package/dist/types/notifications/operator-runtime.d.ts +52 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +35 -0
- package/dist/types/notifications/telegram-daemon.d.ts +114 -16
- package/dist/types/notifications/telegram-reference.d.ts +3 -1
- package/dist/types/notifications/topic-registry.d.ts +12 -9
- package/dist/types/runtime-mcp/types.d.ts +7 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +14 -4
- package/dist/types/session/blob-store.d.ts +25 -0
- package/dist/types/session/session-manager.d.ts +57 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/skc-runtime/psmux-detect.d.ts +78 -0
- package/dist/types/skc-runtime/state-renderer.d.ts +5 -0
- package/dist/types/skc-runtime/team-runtime.d.ts +2 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +30 -2
- package/dist/types/skc-runtime/tmux-sessions.d.ts +18 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +37 -1
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +80 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +9 -1
- package/dist/types/tools/browser/tab-supervisor.d.ts +31 -0
- package/dist/types/tools/composer-bash-policy.d.ts +14 -0
- package/dist/types/tools/computer-gc.d.ts +23 -0
- package/dist/types/tools/cron.d.ts +36 -61
- package/dist/types/tools/index.d.ts +3 -2
- package/dist/types/tools/resource-gc.d.ts +54 -0
- package/dist/types/utils/changelog.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +6 -3
- package/dist/types/web/scrapers/types.d.ts +5 -0
- package/dist/types/web/scrapers/utils.d.ts +7 -1
- package/dist/types/web/search/index.d.ts +1 -0
- package/dist/types/web/search/providers/utils.d.ts +11 -4
- package/package.json +11 -9
- package/scripts/g004-tmux-smoke.ts +100 -0
- package/scripts/g005-daemon-smoke.ts +180 -0
- package/scripts/g011-daemon-path-smoke.ts +153 -0
- package/src/cli/args.ts +0 -1
- package/src/cli/fast-help.ts +29 -2
- package/src/cli/mcp-cli.ts +272 -0
- package/src/cli/plugin-cli.ts +66 -3
- package/src/cli/web-search-cli.ts +5 -0
- package/src/cli.ts +30 -12
- package/src/commands/mcp.ts +117 -0
- package/src/commands/plugin.ts +4 -0
- package/src/commands/session.ts +18 -0
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-profile-activation.ts +62 -8
- package/src/config/model-profiles.ts +3 -4
- package/src/config/model-registry.ts +3 -6
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +88 -103
- package/src/deep-interview/plaintext-gate-guard.ts +94 -0
- package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +1 -1
- package/src/defaults/skc/skills/deep-interview/SKILL.md +7 -6
- package/src/defaults/skc/skills/team/SKILL.md +5 -3
- package/src/defaults/skc/skills/ultragoal/SKILL.md +41 -13
- package/src/export/html/index.ts +2 -2
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -12
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/skc-plugins/compiler.ts +351 -0
- package/src/extensibility/skc-plugins/constrained-hooks.ts +170 -0
- package/src/extensibility/skc-plugins/index.ts +9 -0
- package/src/extensibility/skc-plugins/injection.ts +109 -0
- package/src/extensibility/skc-plugins/installer.ts +434 -0
- package/src/extensibility/skc-plugins/loader.ts +3 -1
- package/src/extensibility/skc-plugins/mcp-policy.ts +239 -0
- package/src/extensibility/skc-plugins/observability.ts +84 -0
- package/src/extensibility/skc-plugins/paths.ts +1 -1
- package/src/extensibility/skc-plugins/prompt-appendix.ts +109 -0
- package/src/extensibility/skc-plugins/registry.ts +180 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +234 -0
- package/src/extensibility/skc-plugins/schema.ts +250 -20
- package/src/extensibility/skc-plugins/session-validation.ts +147 -0
- package/src/extensibility/skc-plugins/types.ts +199 -3
- package/src/extensibility/skc-plugins/validation.ts +80 -0
- package/src/extensibility/skills.ts +15 -0
- package/src/goals/tools/goal-tool.ts +14 -1
- package/src/hooks/skill-state.ts +57 -0
- package/src/i18n/messages/de.ts +0 -1
- package/src/i18n/messages/en.ts +0 -1
- package/src/i18n/messages/es.ts +0 -1
- package/src/i18n/messages/fr.ts +0 -1
- package/src/i18n/messages/ja.ts +0 -1
- package/src/i18n/messages/ko.ts +0 -1
- package/src/i18n/messages/zh.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +14 -11
- package/src/main.ts +14 -3
- package/src/modes/bridge/bridge-mode.ts +11 -0
- package/src/modes/components/assistant-message.ts +49 -1
- package/src/modes/components/custom-editor.ts +2 -0
- package/src/modes/components/footer.ts +2 -3
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/hook-selector.ts +67 -43
- package/src/modes/components/model-selector.ts +65 -12
- package/src/modes/components/settings-defs.ts +1 -2
- package/src/modes/components/settings-selector.ts +2 -7
- package/src/modes/components/status-line/git-utils.ts +25 -0
- package/src/modes/components/status-line.ts +10 -11
- package/src/modes/components/welcome.ts +2 -3
- package/src/modes/controllers/extension-ui-controller.ts +0 -27
- package/src/modes/controllers/selector-controller.ts +59 -11
- package/src/modes/interactive-mode.ts +4 -1
- package/src/modes/shared/agent-wire/scopes.ts +1 -1
- package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
- package/src/modes/theme/defaults/index.ts +2 -0
- package/src/modes/theme/theme.ts +0 -4
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/notifications/html-format.ts +38 -0
- package/src/notifications/index.ts +242 -12
- package/src/notifications/lifecycle-commands.ts +238 -0
- package/src/notifications/lifecycle-control-runtime.ts +405 -0
- package/src/notifications/lifecycle-orchestrator.ts +358 -0
- package/src/notifications/operator-runtime.ts +171 -0
- package/src/notifications/rate-limit-pool.ts +19 -0
- package/src/notifications/recent-activity.ts +132 -0
- package/src/notifications/telegram-daemon.ts +778 -257
- package/src/notifications/telegram-reference.ts +25 -7
- package/src/notifications/topic-registry.ts +23 -9
- package/src/prompts/agents/executor.md +2 -2
- package/src/prompts/system/system-prompt.md +2 -2
- package/src/prompts/tools/cron.md +5 -3
- package/src/prompts/tools/read.md +1 -1
- package/src/runtime-mcp/transports/stdio.ts +38 -4
- package/src/runtime-mcp/types.ts +7 -0
- package/src/sdk.ts +162 -10
- package/src/session/agent-session.ts +210 -74
- package/src/session/blob-store.ts +196 -8
- package/src/session/session-manager.ts +762 -12
- package/src/skc-runtime/launch-tmux.ts +68 -19
- package/src/skc-runtime/psmux-detect.ts +239 -0
- package/src/skc-runtime/state-renderer.ts +13 -0
- package/src/skc-runtime/team-runtime.ts +56 -23
- package/src/skc-runtime/tmux-common.ts +88 -4
- package/src/skc-runtime/tmux-sessions.ts +111 -9
- package/src/skc-runtime/ultragoal-guard.ts +192 -8
- package/src/skc-runtime/ultragoal-runtime.ts +296 -16
- package/src/slash-commands/builtin-registry.ts +23 -3
- package/src/slash-commands/helpers/fast-status-report.ts +13 -3
- package/src/slash-commands/helpers/parse.ts +2 -1
- package/src/system-prompt.ts +9 -0
- package/src/task/executor.ts +31 -7
- package/src/task/index.ts +2 -0
- package/src/tools/ask.ts +5 -1
- package/src/tools/bash.ts +9 -0
- package/src/tools/browser/tab-supervisor.ts +86 -2
- package/src/tools/composer-bash-policy.ts +96 -0
- package/src/tools/computer-gc.ts +66 -0
- package/src/tools/computer.ts +2 -0
- package/src/tools/cron.ts +75 -112
- package/src/tools/fetch.ts +18 -2
- package/src/tools/index.ts +5 -9
- package/src/tools/read.ts +25 -55
- package/src/tools/renderers.ts +0 -2
- package/src/tools/resource-gc.ts +291 -0
- package/src/tools/ultragoal-ask-guard.ts +7 -1
- package/src/utils/changelog.ts +8 -0
- package/src/web/insane/url-guard.ts +18 -14
- package/src/web/scrapers/types.ts +143 -45
- package/src/web/scrapers/utils.ts +70 -19
- package/src/web/search/index.ts +1 -0
- package/src/web/search/providers/utils.ts +22 -5
- package/vendor/insane-search/MANIFEST.json +3 -1
- package/vendor/insane-search/engine/__init__.py +14 -0
- package/vendor/insane-search/engine/content_safety.py +151 -0
- package/vendor/insane-search/engine/fetch_chain.py +32 -0
- package/vendor/insane-search/engine/tests/test_u8.py +216 -0
- package/dist/types/tools/inspect-image-renderer.d.ts +0 -26
- package/dist/types/tools/inspect-image.d.ts +0 -31
- package/src/prompts/tools/inspect-image-system.md +0 -20
- package/src/prompts/tools/inspect-image.md +0 -32
- package/src/tools/inspect-image-renderer.ts +0 -103
- package/src/tools/inspect-image.ts +0 -172
package/src/tools/fetch.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { CachedOutputBlock } from "../tui/output-block";
|
|
|
17
17
|
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
18
18
|
import { ensureTool } from "../utils/tools-manager";
|
|
19
19
|
import { INSANE_NOTES, tryInsaneFetch } from "../web/insane/bridge";
|
|
20
|
-
import { validatePublicHttpUrlForInsane } from "../web/insane/url-guard";
|
|
20
|
+
import { validatePublicHttpUrl, validatePublicHttpUrlForInsane } from "../web/insane/url-guard";
|
|
21
21
|
import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../web/parallel";
|
|
22
22
|
import { specialHandlers } from "../web/scrapers";
|
|
23
23
|
import type { RenderResult } from "../web/scrapers/types";
|
|
@@ -789,6 +789,21 @@ async function renderUrl(
|
|
|
789
789
|
|
|
790
790
|
// Step 0: Normalize URL (ensure scheme for special handlers)
|
|
791
791
|
url = normalizeUrl(url);
|
|
792
|
+
const publicUrl = await validatePublicHttpUrl(url);
|
|
793
|
+
if (!publicUrl.ok) {
|
|
794
|
+
notes.push(`Blocked URL fetch: target URL is not public HTTP(S): ${publicUrl.reason}`);
|
|
795
|
+
return {
|
|
796
|
+
url,
|
|
797
|
+
finalUrl: url,
|
|
798
|
+
contentType: "unknown",
|
|
799
|
+
method: "failed",
|
|
800
|
+
content: "",
|
|
801
|
+
fetchedAt,
|
|
802
|
+
truncated: false,
|
|
803
|
+
notes,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
url = publicUrl.url.toString();
|
|
792
807
|
|
|
793
808
|
// Step 1: Try special handlers for known sites (unless raw mode)
|
|
794
809
|
if (!raw) {
|
|
@@ -802,7 +817,8 @@ async function renderUrl(
|
|
|
802
817
|
throw new ToolAbortError();
|
|
803
818
|
}
|
|
804
819
|
if (!response.ok) {
|
|
805
|
-
const failureNote =
|
|
820
|
+
const failureNote =
|
|
821
|
+
response.error ?? (response.status ? `Failed to fetch URL (HTTP ${response.status})` : "Failed to fetch URL");
|
|
806
822
|
notes.push(failureNote);
|
|
807
823
|
const insane = await tryInsaneFallback({
|
|
808
824
|
url,
|
package/src/tools/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentTelemetryConfig, AgentTool } from "@sayknow-cli/agent-core";
|
|
2
|
-
import type { Model, ToolChoice } from "@sayknow-cli/ai";
|
|
2
|
+
import type { Model, ServiceTier, ToolChoice } from "@sayknow-cli/ai";
|
|
3
3
|
import { $env, $flag, logger } from "@sayknow-cli/utils";
|
|
4
4
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
5
5
|
import type { Settings } from "../config/settings";
|
|
@@ -38,12 +38,11 @@ import { BrowserTool } from "./browser";
|
|
|
38
38
|
import { CalculatorTool } from "./calculator";
|
|
39
39
|
import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
|
|
40
40
|
import { ComputerTool, isComputerCallable, isComputerLoadablePlatform } from "./computer";
|
|
41
|
-
import {
|
|
41
|
+
import { CronTool } from "./cron";
|
|
42
42
|
import { DebugTool } from "./debug";
|
|
43
43
|
import { EvalTool } from "./eval";
|
|
44
44
|
import { FindTool } from "./find";
|
|
45
45
|
import { GithubTool } from "./gh";
|
|
46
|
-
import { InspectImageTool } from "./inspect-image";
|
|
47
46
|
import { IrcTool } from "./irc";
|
|
48
47
|
import { JobTool } from "./job";
|
|
49
48
|
import { MonitorTool } from "./monitor";
|
|
@@ -83,7 +82,6 @@ export * from "./eval";
|
|
|
83
82
|
export * from "./find";
|
|
84
83
|
export * from "./gh";
|
|
85
84
|
export * from "./image-gen";
|
|
86
|
-
export * from "./inspect-image";
|
|
87
85
|
export * from "./irc";
|
|
88
86
|
export * from "./job";
|
|
89
87
|
export * from "./monitor";
|
|
@@ -222,6 +220,8 @@ export interface ToolSession {
|
|
|
222
220
|
agentOutputManager?: AgentOutputManager;
|
|
223
221
|
/** Settings instance for passing to subagents */
|
|
224
222
|
settings: Settings;
|
|
223
|
+
/** Live service-tier intent of the parent session, inherited by `inherit` subagents. */
|
|
224
|
+
serviceTier?: ServiceTier;
|
|
225
225
|
/** Plan mode state (if active) */
|
|
226
226
|
getPlanModeState?: () => PlanModeState | undefined;
|
|
227
227
|
/** Goal mode state (if active or paused) */
|
|
@@ -387,7 +387,6 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
387
387
|
find: s => new FindTool(s),
|
|
388
388
|
search: s => new SearchTool(s),
|
|
389
389
|
lsp: LspTool.createIf,
|
|
390
|
-
inspect_image: s => new InspectImageTool(s),
|
|
391
390
|
browser: s => new BrowserTool(s),
|
|
392
391
|
...(isComputerLoadablePlatform() ? { computer: ComputerTool.createIf } : {}),
|
|
393
392
|
checkpoint: CheckpointTool.createIf,
|
|
@@ -396,9 +395,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
396
395
|
subagent: s => new SubagentTool(s),
|
|
397
396
|
job: JobTool.createIf,
|
|
398
397
|
monitor: MonitorTool.createIf,
|
|
399
|
-
|
|
400
|
-
CronList: CronListTool.createIf,
|
|
401
|
-
CronDelete: CronDeleteTool.createIf,
|
|
398
|
+
cron: CronTool.createIf,
|
|
402
399
|
recipe: RecipeTool.createIf,
|
|
403
400
|
irc: IrcTool.createIf,
|
|
404
401
|
todo_write: s => new TodoWriteTool(s),
|
|
@@ -558,7 +555,6 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
558
555
|
if (name === "ast_grep") return session.settings.get("astGrep.enabled");
|
|
559
556
|
if (name === "ast_edit") return session.settings.get("astEdit.enabled");
|
|
560
557
|
if (name === "render_mermaid") return session.settings.get("renderMermaid.enabled");
|
|
561
|
-
if (name === "inspect_image") return session.settings.get("inspect_image.enabled");
|
|
562
558
|
if (name === "web_search") return session.settings.get("web_search.enabled");
|
|
563
559
|
// search_tool_bm25 is allowed when either legacy mcp.discoveryMode or new tools.discoveryMode is active.
|
|
564
560
|
if (name === "search_tool_bm25") return discoveryActive;
|
package/src/tools/read.ts
CHANGED
|
@@ -670,7 +670,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
670
670
|
|
|
671
671
|
readonly #autoResizeImages: boolean;
|
|
672
672
|
readonly #defaultLimit: number;
|
|
673
|
-
readonly #inspectImageEnabled: boolean;
|
|
674
673
|
|
|
675
674
|
constructor(private readonly session: ToolSession) {
|
|
676
675
|
const displayMode = resolveFileDisplayMode(session);
|
|
@@ -679,7 +678,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
679
678
|
1,
|
|
680
679
|
Math.min(session.settings.get("read.defaultLimit") ?? DEFAULT_MAX_LINES, DEFAULT_MAX_LINES),
|
|
681
680
|
);
|
|
682
|
-
this.#inspectImageEnabled = session.settings.get("inspect_image.enabled");
|
|
683
681
|
this.description = prompt.render(readDescription, {
|
|
684
682
|
DEFAULT_LIMIT: String(this.#defaultLimit),
|
|
685
683
|
DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
|
|
@@ -1572,62 +1570,34 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1572
1570
|
| undefined;
|
|
1573
1571
|
|
|
1574
1572
|
if (mimeType) {
|
|
1575
|
-
if (
|
|
1576
|
-
const
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
this.session.cwd,
|
|
1596
|
-
)}" and a question describing what to inspect and the desired output format.`,
|
|
1573
|
+
if (fileSize > MAX_IMAGE_SIZE) {
|
|
1574
|
+
const sizeStr = formatBytes(fileSize);
|
|
1575
|
+
const maxStr = formatBytes(MAX_IMAGE_SIZE);
|
|
1576
|
+
throw new ToolError(`Image file too large: ${sizeStr} exceeds ${maxStr} limit.`);
|
|
1577
|
+
}
|
|
1578
|
+
try {
|
|
1579
|
+
const imageInput = await loadImageInput({
|
|
1580
|
+
path: readPath,
|
|
1581
|
+
cwd: this.session.cwd,
|
|
1582
|
+
autoResize: this.#autoResizeImages,
|
|
1583
|
+
maxBytes: MAX_IMAGE_SIZE,
|
|
1584
|
+
resolvedPath: absolutePath,
|
|
1585
|
+
detectedMimeType: mimeType,
|
|
1586
|
+
});
|
|
1587
|
+
if (!imageInput) {
|
|
1588
|
+
throw new ToolError(`Read image file [${mimeType}] failed: unsupported image format.`);
|
|
1589
|
+
}
|
|
1590
|
+
content = [
|
|
1591
|
+
{ type: "text", text: imageInput.textNote },
|
|
1592
|
+
{ type: "image", data: imageInput.data, mimeType: imageInput.mimeType },
|
|
1597
1593
|
];
|
|
1598
|
-
content = [{ type: "text", text: metadataLines.join("\n") }];
|
|
1599
1594
|
details = {};
|
|
1600
|
-
sourcePath =
|
|
1601
|
-
}
|
|
1602
|
-
if (
|
|
1603
|
-
|
|
1604
|
-
const maxStr = formatBytes(MAX_IMAGE_SIZE);
|
|
1605
|
-
throw new ToolError(`Image file too large: ${sizeStr} exceeds ${maxStr} limit.`);
|
|
1606
|
-
}
|
|
1607
|
-
try {
|
|
1608
|
-
const imageInput = await loadImageInput({
|
|
1609
|
-
path: readPath,
|
|
1610
|
-
cwd: this.session.cwd,
|
|
1611
|
-
autoResize: this.#autoResizeImages,
|
|
1612
|
-
maxBytes: MAX_IMAGE_SIZE,
|
|
1613
|
-
resolvedPath: absolutePath,
|
|
1614
|
-
detectedMimeType: mimeType,
|
|
1615
|
-
});
|
|
1616
|
-
if (!imageInput) {
|
|
1617
|
-
throw new ToolError(`Read image file [${mimeType}] failed: unsupported image format.`);
|
|
1618
|
-
}
|
|
1619
|
-
content = [
|
|
1620
|
-
{ type: "text", text: imageInput.textNote },
|
|
1621
|
-
{ type: "image", data: imageInput.data, mimeType: imageInput.mimeType },
|
|
1622
|
-
];
|
|
1623
|
-
details = {};
|
|
1624
|
-
sourcePath = imageInput.resolvedPath;
|
|
1625
|
-
} catch (error) {
|
|
1626
|
-
if (error instanceof ImageInputTooLargeError) {
|
|
1627
|
-
throw new ToolError(error.message);
|
|
1628
|
-
}
|
|
1629
|
-
throw error;
|
|
1595
|
+
sourcePath = imageInput.resolvedPath;
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
if (error instanceof ImageInputTooLargeError) {
|
|
1598
|
+
throw new ToolError(error.message);
|
|
1630
1599
|
}
|
|
1600
|
+
throw error;
|
|
1631
1601
|
}
|
|
1632
1602
|
} else if (isNotebookPath(absolutePath) && !isRawSelector(parsed)) {
|
|
1633
1603
|
const notebookText = await readEditableNotebookText(absolutePath, localReadPath);
|
package/src/tools/renderers.ts
CHANGED
|
@@ -22,7 +22,6 @@ import { debugToolRenderer } from "./debug";
|
|
|
22
22
|
import { evalToolRenderer } from "./eval";
|
|
23
23
|
import { findToolRenderer } from "./find";
|
|
24
24
|
import { githubToolRenderer } from "./gh-renderer";
|
|
25
|
-
import { inspectImageToolRenderer } from "./inspect-image-renderer";
|
|
26
25
|
import { jobToolRenderer } from "./job";
|
|
27
26
|
import { readToolRenderer } from "./read";
|
|
28
27
|
import { recipeToolRenderer } from "./recipe/render";
|
|
@@ -63,7 +62,6 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
63
62
|
find: findToolRenderer as ToolRenderer,
|
|
64
63
|
search: searchToolRenderer as ToolRenderer,
|
|
65
64
|
lsp: lspToolRenderer as ToolRenderer,
|
|
66
|
-
inspect_image: inspectImageToolRenderer as ToolRenderer,
|
|
67
65
|
read: readToolRenderer as ToolRenderer,
|
|
68
66
|
job: jobToolRenderer as ToolRenderer,
|
|
69
67
|
resolve: resolveToolRenderer as ToolRenderer,
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { logger } from "@sayknow-cli/utils";
|
|
2
|
+
import type { Settings } from "../config/settings";
|
|
3
|
+
import { listTabsForGc, releaseTabIfGcEligible, type TabGcSnapshot } from "./browser/tab-supervisor";
|
|
4
|
+
import { cleanupStaleScreenshotFallbackDirs, hasCreatedScreenshotFallbackDir } from "./computer-gc";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mandatory, session-aware resource garbage collector.
|
|
8
|
+
*
|
|
9
|
+
* A single process-wide, reference-counted, unref'd, non-overlapping interval sweeps:
|
|
10
|
+
* - browser tabs (the heavyweight resource: one worker thread per tab + Chrome child
|
|
11
|
+
* processes) via an idle sweep and an opportunistic RSS-pressure sweep, and
|
|
12
|
+
* - stale computer-use screenshot fallback directories on disk (lazy-armed + throttled).
|
|
13
|
+
*
|
|
14
|
+
* Eviction targets ONLY alive, non-in-flight, SKC-managed headless/spawned tabs owned by a
|
|
15
|
+
* registered session; connected/real-Chrome/held/in-flight tabs and ownerless tabs are never
|
|
16
|
+
* touched. RSS is the SKC parent-process RSS only (`process.memoryUsage().rss`); pressure
|
|
17
|
+
* eviction is best-effort and never force-evicts.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const DEFAULT_SWEEP_INTERVAL_MS = 30_000;
|
|
21
|
+
const BYTES_PER_MB = 1024 * 1024;
|
|
22
|
+
|
|
23
|
+
export interface BrowserGcPolicy {
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
idleMs: number;
|
|
26
|
+
rssLimitBytes: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ComputerGcPolicy {
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
staleMs: number;
|
|
32
|
+
scanIntervalMs: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveBrowserGcPolicy(settings: Settings): BrowserGcPolicy {
|
|
36
|
+
return {
|
|
37
|
+
enabled: settings.get("browser.gc.enabled"),
|
|
38
|
+
idleMs: settings.get("browser.gc.idleMs"),
|
|
39
|
+
rssLimitBytes: settings.get("browser.gc.rssLimitMb") * BYTES_PER_MB,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resolveComputerGcPolicy(settings: Settings): ComputerGcPolicy {
|
|
44
|
+
return {
|
|
45
|
+
enabled: settings.get("computer.screenshotGc.enabled"),
|
|
46
|
+
staleMs: settings.get("computer.screenshotGc.staleMs"),
|
|
47
|
+
scanIntervalMs: settings.get("computer.screenshotGc.scanIntervalMs"),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveSweepIntervalMs(settings: Settings): number {
|
|
52
|
+
return settings.get("resourceGc.sweepIntervalMs");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Injectable seams so the controller is fully testable without real browsers/filesystem/RSS. */
|
|
56
|
+
export interface ResourceGcDeps {
|
|
57
|
+
now: () => number;
|
|
58
|
+
rssBytes: () => number;
|
|
59
|
+
logWarn: (msg: string, meta?: Record<string, unknown>) => void;
|
|
60
|
+
listTabs: () => TabGcSnapshot[];
|
|
61
|
+
releaseTab: (name: string, policy: { now: () => number; idleMs: number }) => Promise<boolean>;
|
|
62
|
+
cleanupScreenshots: (opts: { now: () => number; staleMs: number }) => Promise<{ scanned: number; removed: number }>;
|
|
63
|
+
screenshotArmed: () => boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const defaultDeps: ResourceGcDeps = {
|
|
67
|
+
now: () => Date.now(),
|
|
68
|
+
rssBytes: () => process.memoryUsage().rss,
|
|
69
|
+
logWarn: (msg, meta) => logger.warn(msg, meta),
|
|
70
|
+
listTabs: () => listTabsForGc(),
|
|
71
|
+
releaseTab: (name, policy) => releaseTabIfGcEligible(name, policy),
|
|
72
|
+
cleanupScreenshots: opts => cleanupStaleScreenshotFallbackDirs(opts),
|
|
73
|
+
screenshotArmed: () => hasCreatedScreenshotFallbackDir(),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// ── Controller state (process-global; tabs/browsers are module-global too) ──────────────────
|
|
77
|
+
const activeSessions = new Map<string, Settings>();
|
|
78
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
79
|
+
let stopped = false;
|
|
80
|
+
// Bumped on every stop so an in-flight tick from a previous schedule cannot reschedule after a
|
|
81
|
+
// stop+re-register and leak a duplicate timer.
|
|
82
|
+
let timerGeneration = 0;
|
|
83
|
+
let inProgress = false;
|
|
84
|
+
let rssWarningActive = false;
|
|
85
|
+
let lastScreenshotScanAt = 0;
|
|
86
|
+
let deps: ResourceGcDeps = defaultDeps;
|
|
87
|
+
|
|
88
|
+
export interface ResourceGcRegistration {
|
|
89
|
+
sessionId: string;
|
|
90
|
+
settings: Settings;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Register a session with the resource GC. Starts the single shared timer on the first
|
|
95
|
+
* registration. Returns an idempotent unregister function; the timer stops only when the last
|
|
96
|
+
* session unregisters.
|
|
97
|
+
*/
|
|
98
|
+
export function registerResourceGcSession(reg: ResourceGcRegistration): () => void {
|
|
99
|
+
activeSessions.set(reg.sessionId, reg.settings);
|
|
100
|
+
ensureTimerStarted();
|
|
101
|
+
let unregistered = false;
|
|
102
|
+
return () => {
|
|
103
|
+
if (unregistered) return;
|
|
104
|
+
unregistered = true;
|
|
105
|
+
activeSessions.delete(reg.sessionId);
|
|
106
|
+
if (activeSessions.size === 0) stopTimer();
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function currentSweepIntervalMs(): number {
|
|
111
|
+
let min = Number.POSITIVE_INFINITY;
|
|
112
|
+
for (const settings of activeSessions.values()) min = Math.min(min, resolveSweepIntervalMs(settings));
|
|
113
|
+
return Number.isFinite(min) ? min : DEFAULT_SWEEP_INTERVAL_MS;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function ensureTimerStarted(): void {
|
|
117
|
+
if (timer) return;
|
|
118
|
+
stopped = false;
|
|
119
|
+
scheduleNextSweep();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Recursive setTimeout (not setInterval) so the cadence is recomputed every cycle and later
|
|
123
|
+
// session register/unregister changes to resourceGc.sweepIntervalMs are honored live.
|
|
124
|
+
function scheduleNextSweep(): void {
|
|
125
|
+
if (stopped || activeSessions.size === 0) {
|
|
126
|
+
timer = null;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const generation = timerGeneration;
|
|
130
|
+
timer = setTimeout(() => {
|
|
131
|
+
void tickAndReschedule(generation);
|
|
132
|
+
}, currentSweepIntervalMs());
|
|
133
|
+
timer.unref?.();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function tickAndReschedule(generation: number): Promise<void> {
|
|
137
|
+
await runTick();
|
|
138
|
+
// A stop (and possible re-register) happened during the tick: a newer cycle owns the timer now.
|
|
139
|
+
if (generation !== timerGeneration) return;
|
|
140
|
+
scheduleNextSweep();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function stopTimer(): void {
|
|
144
|
+
stopped = true;
|
|
145
|
+
timerGeneration++;
|
|
146
|
+
if (timer) {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
timer = null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function runTick(): Promise<void> {
|
|
153
|
+
if (inProgress) return;
|
|
154
|
+
inProgress = true;
|
|
155
|
+
try {
|
|
156
|
+
await sweepOnce(deps);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
logger.debug("resource GC sweep failed", { error: (err as Error).message });
|
|
159
|
+
} finally {
|
|
160
|
+
inProgress = false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function sweepOnce(d: ResourceGcDeps = deps): Promise<void> {
|
|
165
|
+
if (activeSessions.size === 0) return;
|
|
166
|
+
await sweepBrowserTabs(d);
|
|
167
|
+
await sweepScreenshots(d);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function ownerBrowserPolicy(snapshot: TabGcSnapshot): BrowserGcPolicy | null {
|
|
171
|
+
if (!snapshot.ownerId) return null;
|
|
172
|
+
const settings = activeSessions.get(snapshot.ownerId);
|
|
173
|
+
if (!settings) return null;
|
|
174
|
+
return resolveBrowserGcPolicy(settings);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Coarse, ordering-only eligibility; the live recheck in releaseTabIfGcEligible is authoritative. */
|
|
178
|
+
function isCoarselyEligible(snapshot: TabGcSnapshot): boolean {
|
|
179
|
+
return (
|
|
180
|
+
snapshot.state === "alive" &&
|
|
181
|
+
snapshot.pendingCount === 0 &&
|
|
182
|
+
(snapshot.kindTag === "headless" || snapshot.kindTag === "spawned")
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Collect idle, non-in-flight, SKC-managed, owned-and-enabled tabs, sorted LRU (oldest first). */
|
|
187
|
+
function collectIdleCandidates(d: ResourceGcDeps): Array<{ snapshot: TabGcSnapshot; policy: BrowserGcPolicy }> {
|
|
188
|
+
const candidates: Array<{ snapshot: TabGcSnapshot; policy: BrowserGcPolicy }> = [];
|
|
189
|
+
for (const snapshot of d.listTabs()) {
|
|
190
|
+
if (!isCoarselyEligible(snapshot)) continue;
|
|
191
|
+
const policy = ownerBrowserPolicy(snapshot);
|
|
192
|
+
if (!policy?.enabled) continue;
|
|
193
|
+
if (d.now() - snapshot.lastUsedAt <= policy.idleMs) continue;
|
|
194
|
+
candidates.push({ snapshot, policy });
|
|
195
|
+
}
|
|
196
|
+
candidates.sort((a, b) => a.snapshot.lastUsedAt - b.snapshot.lastUsedAt);
|
|
197
|
+
return candidates;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function sweepBrowserTabs(d: ResourceGcDeps): Promise<void> {
|
|
201
|
+
// Reclamation honors IR-1 strictly: ONLY idle, non-in-flight, SKC-managed, owned tabs are ever
|
|
202
|
+
// evicted. RSS pressure never relaxes that boundary — it only drives the warning below.
|
|
203
|
+
for (const { snapshot, policy } of collectIdleCandidates(d)) {
|
|
204
|
+
await d.releaseTab(snapshot.name, { now: d.now, idleMs: policy.idleMs });
|
|
205
|
+
}
|
|
206
|
+
evaluateRssPressureWarning(d);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Owners whose own RSS limit is exceeded by the single shared parent-process RSS sample. */
|
|
210
|
+
function pressuredOwnerIds(d: ResourceGcDeps): Set<string> {
|
|
211
|
+
const rss = d.rssBytes();
|
|
212
|
+
const owners = new Set<string>();
|
|
213
|
+
for (const [sessionId, settings] of activeSessions) {
|
|
214
|
+
const policy = resolveBrowserGcPolicy(settings);
|
|
215
|
+
if (policy.enabled && rss > policy.rssLimitBytes) owners.add(sessionId);
|
|
216
|
+
}
|
|
217
|
+
return owners;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* RSS pressure is a best-effort warning signal only. Because eviction is always idle-gated
|
|
222
|
+
* (IR-1), when parent-process RSS stays over an enabled owner's limit and no idle, unheld tab
|
|
223
|
+
* remains to reclaim for a pressured owner, we warn exactly once per continuous episode and
|
|
224
|
+
* never force-evict. The warning episode resets when RSS recovers or a reclaimable tab appears.
|
|
225
|
+
*/
|
|
226
|
+
function evaluateRssPressureWarning(d: ResourceGcDeps): void {
|
|
227
|
+
const pressured = pressuredOwnerIds(d);
|
|
228
|
+
if (pressured.size === 0) {
|
|
229
|
+
rssWarningActive = false;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const reclaimableRemains = collectIdleCandidates(d).some(
|
|
233
|
+
c => c.snapshot.ownerId !== undefined && pressured.has(c.snapshot.ownerId),
|
|
234
|
+
);
|
|
235
|
+
if (reclaimableRemains) {
|
|
236
|
+
rssWarningActive = false;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (!rssWarningActive) {
|
|
240
|
+
rssWarningActive = true;
|
|
241
|
+
d.logWarn("Browser GC: RSS over limit but no safe (idle, unheld) browser tabs are evictable", {
|
|
242
|
+
rssBytes: d.rssBytes(),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function sweepScreenshots(d: ResourceGcDeps): Promise<void> {
|
|
248
|
+
if (!d.screenshotArmed()) return;
|
|
249
|
+
|
|
250
|
+
let staleMs: number | null = null;
|
|
251
|
+
let scanIntervalMs = Number.POSITIVE_INFINITY;
|
|
252
|
+
for (const settings of activeSessions.values()) {
|
|
253
|
+
const policy = resolveComputerGcPolicy(settings);
|
|
254
|
+
if (!policy.enabled) continue;
|
|
255
|
+
staleMs = staleMs === null ? policy.staleMs : Math.min(staleMs, policy.staleMs);
|
|
256
|
+
scanIntervalMs = Math.min(scanIntervalMs, policy.scanIntervalMs);
|
|
257
|
+
}
|
|
258
|
+
if (staleMs === null) return; // no session has screenshot GC enabled
|
|
259
|
+
|
|
260
|
+
const now = d.now();
|
|
261
|
+
if (now - lastScreenshotScanAt < scanIntervalMs) return;
|
|
262
|
+
lastScreenshotScanAt = now;
|
|
263
|
+
await d.cleanupScreenshots({ now: d.now, staleMs });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Test-only seams ─────────────────────────────────────────────────────────────────────────
|
|
267
|
+
export function __setResourceGcDepsForTest(overrides: Partial<ResourceGcDeps>): void {
|
|
268
|
+
deps = { ...defaultDeps, ...overrides };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function __runResourceGcTickForTest(): Promise<void> {
|
|
272
|
+
await runTick();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function __getResourceGcStateForTest(): {
|
|
276
|
+
timerActive: boolean;
|
|
277
|
+
sessionCount: number;
|
|
278
|
+
rssWarningActive: boolean;
|
|
279
|
+
inProgress: boolean;
|
|
280
|
+
} {
|
|
281
|
+
return { timerActive: timer !== null, sessionCount: activeSessions.size, rssWarningActive, inProgress };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function __resetResourceGcForTest(): void {
|
|
285
|
+
stopTimer();
|
|
286
|
+
activeSessions.clear();
|
|
287
|
+
inProgress = false;
|
|
288
|
+
rssWarningActive = false;
|
|
289
|
+
lastScreenshotScanAt = 0;
|
|
290
|
+
deps = defaultDeps;
|
|
291
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { AgentTool } from "@sayknow-cli/agent-core";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
consumeUltragoalAskNudge,
|
|
4
|
+
isUltragoalAskBlocked,
|
|
5
|
+
type UltragoalAskBlockDiagnostic,
|
|
6
|
+
} from "../skc-runtime/ultragoal-guard";
|
|
3
7
|
import { ToolError } from "./tool-errors";
|
|
4
8
|
|
|
5
9
|
const ULTRAGOAL_ASK_GUARD = Symbol.for("sayknow-cli.ultragoalAskGuard");
|
|
@@ -17,6 +21,8 @@ export function formatUltragoalAskBlockMessage(diagnostic: UltragoalAskBlockDiag
|
|
|
17
21
|
export async function assertUltragoalAskAllowed(cwd: string): Promise<void> {
|
|
18
22
|
const diagnostic = await isUltragoalAskBlocked(cwd);
|
|
19
23
|
if (!diagnostic.active) return;
|
|
24
|
+
const nudge = await consumeUltragoalAskNudge(cwd);
|
|
25
|
+
if (nudge.nudged) throw new ToolError(nudge.message);
|
|
20
26
|
throw new ToolError(formatUltragoalAskBlockMessage(diagnostic));
|
|
21
27
|
}
|
|
22
28
|
|
package/src/utils/changelog.ts
CHANGED
|
@@ -92,6 +92,14 @@ export function getDisplayChangelogEntries(): ChangelogEntry[] {
|
|
|
92
92
|
return parseChangelogContent(CHANGELOG_TEXT);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
export function getInstalledVersionChangelogEntry(
|
|
96
|
+
entries: readonly ChangelogEntry[],
|
|
97
|
+
installedVersion: string,
|
|
98
|
+
): ChangelogEntry | undefined {
|
|
99
|
+
const [major = 0, minor = 0, patch = 0] = installedVersion.split(".").map(Number);
|
|
100
|
+
return entries.find(entry => entry.major === major && entry.minor === minor && entry.patch === patch) ?? entries[0];
|
|
101
|
+
}
|
|
102
|
+
|
|
95
103
|
/**
|
|
96
104
|
* Compare versions. Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2
|
|
97
105
|
*/
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Public HTTP(S) URL guard for
|
|
2
|
+
* Public HTTP(S) URL guard for user-supplied web fetch targets.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* dependency probe or engine subprocess is spawned. It is fail-closed: anything
|
|
8
|
-
* it cannot prove is a public, non-credentialed http/https target is rejected.
|
|
4
|
+
* Network-capable URL readers MUST run this guard before the first request and
|
|
5
|
+
* before following any redirect target. It is fail-closed: anything it cannot
|
|
6
|
+
* prove is a public, non-credentialed http/https target is rejected.
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* opt-in (default off).
|
|
8
|
+
* The vendored insane-search engine performs its own redirects outside the
|
|
9
|
+
* TypeScript fetch path, so its fallback remains opt-in and is guarded before
|
|
10
|
+
* any dependency probe or engine subprocess is spawned.
|
|
14
11
|
*/
|
|
15
12
|
import * as dns from "node:dns/promises";
|
|
16
13
|
import * as net from "node:net";
|
|
@@ -105,11 +102,11 @@ export function isPrivateOrSpecialAddress(address: string): boolean {
|
|
|
105
102
|
}
|
|
106
103
|
|
|
107
104
|
/**
|
|
108
|
-
* Validate that `rawUrl` is a public http/https target
|
|
109
|
-
*
|
|
110
|
-
*
|
|
105
|
+
* Validate that `rawUrl` is a public http/https target. Resolves DNS names and
|
|
106
|
+
* rejects any that map to a private/special address. Never throws; returns a
|
|
107
|
+
* discriminated result.
|
|
111
108
|
*/
|
|
112
|
-
export async function
|
|
109
|
+
export async function validatePublicHttpUrl(
|
|
113
110
|
rawUrl: string,
|
|
114
111
|
options: { resolver?: AddressResolver } = {},
|
|
115
112
|
): Promise<PublicUrlResult> {
|
|
@@ -153,3 +150,10 @@ export async function validatePublicHttpUrlForInsane(
|
|
|
153
150
|
}
|
|
154
151
|
return { ok: true, url, addresses };
|
|
155
152
|
}
|
|
153
|
+
|
|
154
|
+
export async function validatePublicHttpUrlForInsane(
|
|
155
|
+
rawUrl: string,
|
|
156
|
+
options: { resolver?: AddressResolver } = {},
|
|
157
|
+
): Promise<PublicUrlResult> {
|
|
158
|
+
return validatePublicHttpUrl(rawUrl, options);
|
|
159
|
+
}
|