zidane 6.0.1 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/dist/{acp-Irby04Us.js → acp-cfR8T19u.js} +8 -8
- package/dist/{acp-Irby04Us.js.map → acp-cfR8T19u.js.map} +1 -1
- package/dist/acp-cli.js +7 -7
- package/dist/acp-cli.js.map +1 -1
- package/dist/acp.d.ts +1 -1
- package/dist/acp.js +1 -1
- package/dist/{agent-DkSmGkDM.js → agent-DZRA7wip.js} +377 -61
- package/dist/agent-DZRA7wip.js.map +1 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/{anthropic-CTHsdFG9.js → anthropic-DNJbRMUK.js} +7 -7
- package/dist/anthropic-DNJbRMUK.js.map +1 -0
- package/dist/atomic-write-B7zr1ddF.js.map +1 -1
- package/dist/{auth-BDSu_0t3.js → auth-B3cB5K6d.js} +7 -7
- package/dist/auth-B3cB5K6d.js.map +1 -0
- package/dist/cache-telemetry-BKAwsNxm.js.map +1 -1
- package/dist/chat/pure.d.ts +4 -4
- package/dist/chat/pure.js +3 -3
- package/dist/chat.d.ts +44 -21
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +7 -7
- package/dist/chat.js.map +1 -1
- package/dist/completion-core-CXYSVhZo.js.map +1 -1
- package/dist/context-breakdown-Dzo25N45.js.map +1 -1
- package/dist/contexts/daytona.d.ts +3 -1
- package/dist/contexts/daytona.d.ts.map +1 -1
- package/dist/contexts/daytona.js +2 -2
- package/dist/contexts/daytona.js.map +1 -1
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js +2 -2
- package/dist/contexts/docker.js.map +1 -1
- package/dist/contexts/e2b.d.ts +3 -1
- package/dist/contexts/e2b.d.ts.map +1 -1
- package/dist/contexts/e2b.js +1 -1
- package/dist/contexts/e2b.js.map +1 -1
- package/dist/{contexts-Vone3qQQ.js → contexts-BIbcfBFH.js} +2 -2
- package/dist/contexts-BIbcfBFH.js.map +1 -0
- package/dist/contexts.js +1 -1
- package/dist/edit-utils-EGosADZq.js.map +1 -1
- package/dist/eval.d.ts +1 -1
- package/dist/eval.js +4 -3
- package/dist/eval.js.map +1 -1
- package/dist/exec-file-size-PXkr-MGA.js.map +1 -1
- package/dist/extensions.d.ts +1 -1
- package/dist/extensions.js +1 -1
- package/dist/{fetch-url-CWE8X5OD.js → fetch-url-_5WKWpbQ.js} +2 -2
- package/dist/{fetch-url-CWE8X5OD.js.map → fetch-url-_5WKWpbQ.js.map} +1 -1
- package/dist/{format-BNOXpl-1.js → format-BF745pLL.js} +9 -2
- package/dist/format-BF745pLL.js.map +1 -0
- package/dist/generate-data-WL_yGVL6.js.map +1 -1
- package/dist/hash-DJRzmcPI.js.map +1 -1
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +4 -4
- package/dist/headless.js.map +1 -1
- package/dist/{index-B8UtiZy7.d.ts → index-BGpM2YaU.d.ts} +378 -5
- package/dist/index-BGpM2YaU.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +11 -11
- package/dist/index.js.map +1 -1
- package/dist/logger-DBX9uYOw.js.map +1 -1
- package/dist/{login-DOF8pMVM.js → login-DZLG__2f.js} +4 -3
- package/dist/{login-DOF8pMVM.js.map → login-DZLG__2f.js.map} +1 -1
- package/dist/mcp-CcvuVXB9.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/media-sniff-Bn76JxAu.js.map +1 -1
- package/dist/{messages-CwujiS74.js → messages-C5BDOjUG.js} +2 -2
- package/dist/{messages-CwujiS74.js.map → messages-C5BDOjUG.js.map} +1 -1
- package/dist/oauth-8LqbeI_Q.js.map +1 -1
- package/dist/{openai-compat-BvgYGSR1.js → openai-compat-B0kqvZqN.js} +2 -2
- package/dist/openai-compat-B0kqvZqN.js.map +1 -0
- package/dist/output/stream-json.d.ts +1 -1
- package/dist/output/terminal.d.ts +1 -1
- package/dist/output/terminal.js +1 -1
- package/dist/output/terminal.js.map +1 -1
- package/dist/output-cap-kBUHBlnq.js.map +1 -1
- package/dist/presets.d.ts +1 -1
- package/dist/presets.js +1 -1
- package/dist/providers/anthropic.d.ts +1 -1
- package/dist/providers/anthropic.js +1 -1
- package/dist/providers/arcee.d.ts +1 -1
- package/dist/providers/arcee.js +1 -1
- package/dist/providers/baseten.d.ts +1 -1
- package/dist/providers/baseten.js +2 -2
- package/dist/providers/baseten.js.map +1 -1
- package/dist/providers/cerebras.d.ts +1 -1
- package/dist/providers/cerebras.js +1 -1
- package/dist/providers/local.d.ts +1 -1
- package/dist/providers/local.js +1 -1
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai-compat.js +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +8 -8
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +1 -1
- package/dist/{providers-z6LDMAr3.js → providers-BMlrTr6a.js} +3 -3
- package/dist/{providers-z6LDMAr3.js.map → providers-BMlrTr6a.js.map} +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +4 -4
- package/dist/read-state-Dq_TXUX1.js.map +1 -1
- package/dist/{resolve-Cr8JSL_O.js → resolve-CC8Lmwcj.js} +3 -3
- package/dist/{resolve-Cr8JSL_O.js.map → resolve-CC8Lmwcj.js.map} +1 -1
- package/dist/restate.d.ts +1 -1
- package/dist/restate.d.ts.map +1 -1
- package/dist/restate.js.map +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +75 -20
- package/dist/session/sqlite.js.map +1 -1
- package/dist/{session-CsdDIPY8.js → session-CsKzK8-P.js} +3 -3
- package/dist/session-CsKzK8-P.js.map +1 -0
- package/dist/session.d.ts +2 -2
- package/dist/session.js +2 -2
- package/dist/{skills-Bsb0rvWI.js → skills-BjeXSRlQ.js} +2 -2
- package/dist/{skills-Bsb0rvWI.js.map → skills-BjeXSRlQ.js.map} +1 -1
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +2 -2
- package/dist/stdio-loader-Ce68wUmM.js.map +1 -1
- package/dist/{tool-formatters-DY0TDb1r.d.ts → tool-formatters-CMwjDvkp.d.ts} +2 -2
- package/dist/tool-formatters-CMwjDvkp.d.ts.map +1 -0
- package/dist/tools/fetch-url.d.ts +1 -1
- package/dist/tools/fetch-url.d.ts.map +1 -1
- package/dist/tools/fetch-url.js +1 -1
- package/dist/tools/web-search.d.ts +1 -1
- package/dist/tools/web-search.js +1 -1
- package/dist/tools/web-search.js.map +1 -1
- package/dist/tools.d.ts +2 -2
- package/dist/tools.js +2 -2
- package/dist/{transcript-anchors-DSRPWjiH.js → transcript-anchors-BTNe1OJi.js} +346 -89
- package/dist/transcript-anchors-BTNe1OJi.js.map +1 -0
- package/dist/{transcript-anchors-Cb-bDbNH.d.ts → transcript-anchors-D5wvdKQQ.d.ts} +37 -3
- package/dist/transcript-anchors-D5wvdKQQ.d.ts.map +1 -0
- package/dist/tui.d.ts +21 -6
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +497 -83
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-Dq9Kx7m2.js → turn-operations-CEt_l3fy.js} +23 -8
- package/dist/turn-operations-CEt_l3fy.js.map +1 -0
- package/dist/{turn-operations-CgMROLsr.d.ts → turn-operations-JO5MYM9_.d.ts} +27 -9
- package/dist/turn-operations-JO5MYM9_.d.ts.map +1 -0
- package/dist/types-BIarq1qE.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/{xai-B_e6TOA8.js → xai-C_aq671K.js} +3 -3
- package/dist/{xai-B_e6TOA8.js.map → xai-C_aq671K.js.map} +1 -1
- package/docs/ARCHITECTURE.md +4 -0
- package/docs/CHAT.md +12 -10
- package/docs/SKILL.md +1 -1
- package/docs/TUI.md +3 -3
- package/package.json +2 -2
- package/dist/agent-DkSmGkDM.js.map +0 -1
- package/dist/anthropic-CTHsdFG9.js.map +0 -1
- package/dist/auth-BDSu_0t3.js.map +0 -1
- package/dist/contexts-Vone3qQQ.js.map +0 -1
- package/dist/format-BNOXpl-1.js.map +0 -1
- package/dist/index-B8UtiZy7.d.ts.map +0 -1
- package/dist/openai-compat-BvgYGSR1.js.map +0 -1
- package/dist/session-CsdDIPY8.js.map +0 -1
- package/dist/tool-formatters-DY0TDb1r.d.ts.map +0 -1
- package/dist/transcript-anchors-Cb-bDbNH.d.ts.map +0 -1
- package/dist/transcript-anchors-DSRPWjiH.js.map +0 -1
- package/dist/turn-operations-CgMROLsr.d.ts.map +0 -1
- package/dist/turn-operations-Dq9Kx7m2.js.map +0 -1
package/dist/extensions.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { $a as
|
|
1
|
+
import { $a as ExtensionBehaviorTransform, $i as ExtensionInstallError, $o as ExtensionHostRuntimeBridge, Aa as ExtensionConfigBooleanItem, Ao as ExtensionToolPolicy, Ba as ExtensionConfigValue, Bo as GuiContributions, Ca as scanExtensionDirectories, Co as ExtensionRuntimeEnv, Da as ExtensionPromptDelivery, Do as ExtensionStorageScope, Ea as ExtensionPromptBridge, Eo as ExtensionSource, Fa as ExtensionConfigItemBase, Fo as ExtensionToolResultMetadataRegistration, Ga as coerceExtensionConfigInput, Gi as buildExtensionRegistry, Go as ExtensionHudState, Ha as WireExtensionConfigItem, Hi as BuildExtensionRegistryOptions, Ho as TuiContributions, Ia as ExtensionConfigNumberItem, Io as ExtensionToolResultPatch, Ja as resolveExtensionConfig, Ji as filterExtensionRegistry, Jo as ExtensionNotification, Ka as isValidExtensionConfigItem, Ki as composeExtensionPreset, Ko as ExtensionHudStatus, La as ExtensionConfigScope, Lo as ExtensionToolResultTransform, Ma as ExtensionConfigChangeBehavior, Mo as ExtensionToolRenderer, Na as ExtensionConfigEnumItem, No as ExtensionToolRendererEntry, Oa as createExtensionPromptBridge, Oo as ExtensionStorageSurface, Pa as ExtensionConfigItem, Po as ExtensionToolRendererRegistration, Qa as ExtensionApiVersion, Qi as ExtensionInstallCandidate, Qo as createExtensionHud, Ra as ExtensionConfigStringItem, Ro as ExtensionToolVerdict, Sa as parseExtensionScanPathEnv, So as ExtensionRunTransform, Ta as discoverAndBuildExtensionRegistry, To as ExtensionSkillPolicy, Ua as WireExtensionConfigSchema, Ui as ExtensionCatalogPartition, Uo as ExtensionBannerOptions, Va as StoredExtensionConfigItem, Vo as LoadedExtension, Wa as clampExtensionConfigNumber, Wi as buildExtensionPreset, Wo as ExtensionHud, Xa as toWireConfigSchema, Xi as partitionExtensionCatalog, Xo as ExtensionNotifyTone, Ya as resolveExtensionConfigValue, Yi as mergeContributions, Yo as ExtensionNotifyOptions, Za as EMPTY_EXTENSION_REGISTRY, Zi as ExtensionFetchResult, Zo as ExtensionStatusOptions, _a as ExtensionScanEntry, _o as ExtensionPaths, aa as ExtensionInstallSource, ao as ExtensionCustomRendererEntry, as as ExtensionWorkspaceSurface, ba as inlineLoadedExtension, bo as ExtensionProviderMiddleware, ca as fetchExtensionSource, co as ExtensionGuiPanel, da as parseExtensionInstallSource, do as ExtensionKeybindingDispatchContext, ea as ExtensionInstallErrorCode, eo as ExtensionCapability, es as ExtensionHostRuntimeImpl, fa as readExtensionInstallProvenance, fo as ExtensionManifest, ga as ENTRY_CANDIDATES, go as ExtensionOrderedContribution, ha as registerExtensionHostModules, ho as ExtensionModalNode, ia as ExtensionInstallScope, io as ExtensionCustomRenderer, is as ExtensionWorkspacePair, ja as ExtensionConfigCategory, jo as ExtensionToolRenderProps, ka as defineExtension, ko as ExtensionToolCatalogTransform, la as installExtensionFromSpec, lo as ExtensionGuiSurface, ma as updateInstalledExtension, mo as ExtensionModal, na as ExtensionInstallProvenance, no as ExtensionContextTransform, ns as ExtensionSessionSummary, oa as InstallExtensionFromSpecOptions, oo as ExtensionDefinition, os as createExtensionHostRuntimeBridge, pa as removeInstalledExtension, po as ExtensionMcpConfigTransform, qa as isValidExtensionConfigValue, qi as disposeExtensionRegistry, qo as ExtensionHudWriter, ra as ExtensionInstallResult, ro as ExtensionContributions, rs as ExtensionSessionsSurface, sa as InstalledExtensionTargetOptions, so as ExtensionFooterHint, ss as createWorkspaceSurface, ta as ExtensionInstallIo, to as ExtensionContext, ts as ExtensionRuntimeSurface, ua as listExtensionCandidates, uo as ExtensionKeybinding, va as defaultExtensionScanPaths, vo as ExtensionPermission, wa as DiscoverAndBuildExtensionRegistryOptions, wo as ExtensionSessionStateSurface, xa as isExtensionDefinition, xo as ExtensionRegistry, ya as discoverExtensions, yo as ExtensionPromptTransform, za as ExtensionConfigSurface, zo as ExtensionTuiSurface } from "./index-BGpM2YaU.js";
|
|
2
2
|
export { type BuildExtensionRegistryOptions, type DiscoverAndBuildExtensionRegistryOptions, EMPTY_EXTENSION_REGISTRY, ENTRY_CANDIDATES, type ExtensionApiVersion, type ExtensionBannerOptions, type ExtensionBehaviorTransform, type ExtensionCapability, type ExtensionCatalogPartition, type ExtensionConfigBooleanItem, type ExtensionConfigCategory, type ExtensionConfigChangeBehavior, type ExtensionConfigEnumItem, type ExtensionConfigItem, type ExtensionConfigItemBase, type ExtensionConfigNumberItem, type ExtensionConfigScope, type ExtensionConfigStringItem, type ExtensionConfigSurface, type ExtensionConfigValue, type ExtensionContext, type ExtensionContextTransform, type ExtensionContributions, type ExtensionCustomRenderer, type ExtensionCustomRendererEntry, type ExtensionDefinition, type ExtensionFetchResult, type ExtensionFooterHint, type ExtensionGuiPanel, type ExtensionGuiSurface, type ExtensionHostRuntimeBridge, type ExtensionHostRuntimeImpl, type ExtensionHud, type ExtensionHudState, type ExtensionHudStatus, type ExtensionHudWriter, type ExtensionInstallCandidate, ExtensionInstallError, type ExtensionInstallErrorCode, type ExtensionInstallIo, type ExtensionInstallProvenance, type ExtensionInstallResult, type ExtensionInstallScope, type ExtensionInstallSource, type ExtensionKeybinding, type ExtensionKeybindingDispatchContext, type ExtensionManifest, type ExtensionMcpConfigTransform, type ExtensionModal, type ExtensionModalNode, type ExtensionNotification, type ExtensionNotifyOptions, type ExtensionNotifyTone, type ExtensionOrderedContribution, type ExtensionPaths, type ExtensionPermission, type ExtensionPromptBridge, type ExtensionPromptDelivery, type ExtensionPromptTransform, type ExtensionProviderMiddleware, type ExtensionRegistry, type ExtensionRunTransform, type ExtensionRuntimeEnv, type ExtensionRuntimeSurface, type ExtensionScanEntry, type ExtensionSessionStateSurface, type ExtensionSessionSummary, type ExtensionSessionsSurface, type ExtensionSkillPolicy, type ExtensionSource, type ExtensionStatusOptions, type ExtensionStorageScope, type ExtensionStorageSurface, type ExtensionToolCatalogTransform, type ExtensionToolPolicy, type ExtensionToolRenderProps, type ExtensionToolRenderer, type ExtensionToolRendererEntry, type ExtensionToolRendererRegistration, type ExtensionToolResultMetadataRegistration, type ExtensionToolResultPatch, type ExtensionToolResultTransform, type ExtensionToolVerdict, type ExtensionTuiSurface, type ExtensionWorkspacePair, type ExtensionWorkspaceSurface, type GuiContributions, type InstallExtensionFromSpecOptions, type InstalledExtensionTargetOptions, type LoadedExtension, type StoredExtensionConfigItem, type TuiContributions, type WireExtensionConfigItem, type WireExtensionConfigSchema, buildExtensionPreset, buildExtensionRegistry, clampExtensionConfigNumber, coerceExtensionConfigInput, composeExtensionPreset, createExtensionHostRuntimeBridge, createExtensionHud, createExtensionPromptBridge, createWorkspaceSurface, defaultExtensionScanPaths, defineExtension, discoverAndBuildExtensionRegistry, discoverExtensions, disposeExtensionRegistry, fetchExtensionSource, filterExtensionRegistry, inlineLoadedExtension, installExtensionFromSpec, isExtensionDefinition, isValidExtensionConfigItem, isValidExtensionConfigValue, listExtensionCandidates, mergeContributions, parseExtensionInstallSource, parseExtensionScanPathEnv, partitionExtensionCatalog, readExtensionInstallProvenance, registerExtensionHostModules, removeInstalledExtension, resolveExtensionConfig, resolveExtensionConfigValue, scanExtensionDirectories, toWireConfigSchema, updateInstalledExtension };
|
package/dist/extensions.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as composeExtensionPreset, $t as inlineLoadedExtension, B as ExtensionInstallError, G as readExtensionInstallProvenance, H as installExtensionFromSpec, J as createExtensionHud, K as removeInstalledExtension, Q as buildExtensionRegistry, Qt as discoverExtensions, R as EMPTY_EXTENSION_REGISTRY, U as listExtensionCandidates, V as fetchExtensionSource, W as parseExtensionInstallSource, X as discoverAndBuildExtensionRegistry, Xt as ENTRY_CANDIDATES, Y as registerExtensionHostModules, Z as buildExtensionPreset, Zt as defaultExtensionScanPaths, an as coerceExtensionConfigInput, at as createWorkspaceSurface, cn as resolveExtensionConfig, en as isExtensionDefinition, et as disposeExtensionRegistry, in as clampExtensionConfigNumber, it as createExtensionHostRuntimeBridge, ln as resolveExtensionConfigValue, nn as scanExtensionDirectories, nt as mergeContributions, on as isValidExtensionConfigItem, q as updateInstalledExtension, rn as defineExtension, rt as partitionExtensionCatalog, sn as isValidExtensionConfigValue, tn as parseExtensionScanPathEnv, tt as filterExtensionRegistry, un as toWireConfigSchema, z as createExtensionPromptBridge } from "./agent-DZRA7wip.js";
|
|
2
2
|
export { EMPTY_EXTENSION_REGISTRY, ENTRY_CANDIDATES, ExtensionInstallError, buildExtensionPreset, buildExtensionRegistry, clampExtensionConfigNumber, coerceExtensionConfigInput, composeExtensionPreset, createExtensionHostRuntimeBridge, createExtensionHud, createExtensionPromptBridge, createWorkspaceSurface, defaultExtensionScanPaths, defineExtension, discoverAndBuildExtensionRegistry, discoverExtensions, disposeExtensionRegistry, fetchExtensionSource, filterExtensionRegistry, inlineLoadedExtension, installExtensionFromSpec, isExtensionDefinition, isValidExtensionConfigItem, isValidExtensionConfigValue, listExtensionCandidates, mergeContributions, parseExtensionInstallSource, parseExtensionScanPathEnv, partitionExtensionCatalog, readExtensionInstallProvenance, registerExtensionHostModules, removeInstalledExtension, resolveExtensionConfig, resolveExtensionConfigValue, scanExtensionDirectories, toWireConfigSchema, updateInstalledExtension };
|
|
@@ -472,7 +472,7 @@ const fetchUrl = {
|
|
|
472
472
|
isConcurrencySafe: true,
|
|
473
473
|
spec: {
|
|
474
474
|
name: "fetch_url",
|
|
475
|
-
description: "Fetch a public URL and return its body as plain text. HTML is reduced to text (scripts, styles, tags stripped; entities decoded). If a `web_search` tool is also available, use it first
|
|
475
|
+
description: "Fetch a public URL and return its body as plain text. HTML is reduced to text (scripts, styles, tags stripped; entities decoded). If a `web_search` tool is also available, use it first when you do not already know the exact URL. Only http(s) URLs are accepted; loopback, link-local, private, and cloud-metadata addresses are refused. Output is capped (default 200 KiB).",
|
|
476
476
|
inputSchema: {
|
|
477
477
|
type: "object",
|
|
478
478
|
properties: {
|
|
@@ -531,4 +531,4 @@ const fetchUrl = {
|
|
|
531
531
|
//#endregion
|
|
532
532
|
export { isBlockedAddress as a, stripHtmlInline as c, fetchWithSsrfGuard as i, clearFetchUrlCache as n, isHostAllowed as o, fetchUrl as r, pinnedRequest as s, __fetchCacheTestApi as t };
|
|
533
533
|
|
|
534
|
-
//# sourceMappingURL=fetch-url-
|
|
534
|
+
//# sourceMappingURL=fetch-url-_5WKWpbQ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-url-CWE8X5OD.js","names":["dns","request","httpsRequest","httpRequest"],"sources":["../src/tools/_html.ts","../src/tools/fetch-url.ts"],"sourcesContent":["/**\n * Shared HTML → plain-text helpers used by the web-egress tools (`web_search`\n * snippet extraction, `fetch_url` body decoding).\n *\n * Best-effort by contract: a real HTML parser would be more correct but the\n * payload is always model context, never user-facing rendered output, so a\n * tag-stripper plus a curated entity table covers the practical cases\n * (Wikipedia, GitHub READMEs, MDN docs, Stack Overflow answers). When a tool\n * needs richer parsing it should reach for an actual parser rather than\n * extending these helpers ad-hoc.\n */\n\n/**\n * Curated named-entity table. Kept narrow on purpose — the long tail\n * (`Å`, `×`, mathematical refs, …) is out of scope because the\n * model handles unknown entities fine when they pass through verbatim. Add\n * entries when an entity is observed mangling real output, not speculatively.\n *\n * Keys are stored without the leading `&` / trailing `;` and matched\n * case-insensitively by the decoder.\n */\nconst NAMED_ENTITIES: Record<string, string> = {\n amp: '&',\n apos: '\\'',\n bull: '•',\n copy: '\\u00A9',\n ensp: ' ',\n emsp: ' ',\n gt: '>',\n hellip: '\\u2026',\n laquo: '\\u00AB',\n ldquo: '\\u201C',\n lsquo: '\\u2018',\n lt: '<',\n mdash: '\\u2014',\n middot: '\\u00B7',\n nbsp: ' ',\n ndash: '\\u2013',\n quot: '\"',\n raquo: '\\u00BB',\n rdquo: '\\u201D',\n reg: '\\u00AE',\n rsquo: '\\u2019',\n thinsp: ' ',\n trade: '\\u2122',\n}\n\n/**\n * `String.fromCodePoint` raises on out-of-range / non-finite inputs; the\n * agent's untrusted-payload assumption means we can't let a single broken\n * numeric reference crash the whole decode. Empty string on rejection is\n * the safest outcome (matches what most browsers do for invalid refs).\n */\nfunction safeFromCodePoint(cp: number): string {\n if (!Number.isFinite(cp) || cp < 0 || cp > 0x10FFFF)\n return ''\n try {\n return String.fromCodePoint(cp)\n }\n catch {\n return ''\n }\n}\n\n/**\n * Decode numeric character references (`&#x...;` / `&#NNN;`) and the curated\n * subset of named entities in {@link NAMED_ENTITIES}. Unknown named entities\n * are left as-is so the model can detect them rather than seeing a silently\n * corrupted token.\n *\n * `&` is decoded LAST so a double-escaped string like `&lt;` resolves\n * to `<` (not `<`) — that's the spec-correct single-pass decode shape.\n */\nexport function decodeHtmlEntities(text: string): string {\n let s = text\n .replace(/&#x([0-9a-f]+);/gi, (_, h) => safeFromCodePoint(Number.parseInt(h, 16)))\n .replace(/&#(\\d+);/g, (_, n) => safeFromCodePoint(Number.parseInt(n, 10)))\n s = s.replace(/&([a-z][a-z0-9]*);/gi, (full, name) => {\n const key = name.toLowerCase()\n // Defer `amp` so `&lt;` doesn't double-decode in a single pass.\n if (key === 'amp')\n return full\n const replacement = NAMED_ENTITIES[key]\n return replacement ?? full\n })\n return s.replace(/&/gi, '&')\n}\n\n/**\n * Strip `<script>`, `<style>`, `<noscript>`, comments, and every other tag.\n * Block-level openers (`<br>`, `<p>`, `<div>`, `<li>`, `<tr>`, `<h1-6>`) are\n * replaced with newlines so the resulting plaintext preserves readable\n * structure. Other tags collapse to spaces.\n */\nexport function stripHtml(html: string): string {\n return html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, ' ')\n .replace(/<!--[\\s\\S]*?-->/g, ' ')\n .replace(/<(?:br|p|div|li|tr|h[1-6])[^>]*>/gi, '\\n')\n .replace(/<[^>]+>/g, ' ')\n}\n\n/**\n * Inline variant — strip tags + decode entities + collapse all whitespace\n * (including line breaks) into single spaces. For short snippets where\n * structural breaks would be visual noise (search-result titles, link text).\n */\nexport function stripHtmlInline(html: string): string {\n return decodeHtmlEntities(stripHtml(html))\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\n/**\n * Block variant — strip tags + decode entities, preserve newlines emitted\n * by block-level openers, then collapse inline whitespace per-line and drop\n * empty lines. For full-page reductions where structure aids reading.\n */\nexport function stripHtmlBlock(html: string): string {\n return decodeHtmlEntities(stripHtml(html))\n .split('\\n')\n .map(line => line.replace(/[ \\t]+/g, ' ').trim())\n .filter(line => line.length > 0)\n .join('\\n')\n}\n","import type { RequestOptions as HttpRequestOptions } from 'node:http'\nimport type { RequestOptions as HttpsRequestOptions } from 'node:https'\nimport type { ToolContext, ToolDef } from './types'\nimport { Buffer } from 'node:buffer'\nimport { promises as dns } from 'node:dns'\nimport { request as httpRequest } from 'node:http'\nimport { request as httpsRequest } from 'node:https'\nimport { isIP } from 'node:net'\nimport { errorMessage } from '../errors'\nimport { stripHtmlBlock } from './_html'\n\n/**\n * Fetch a URL and return its text content.\n *\n * Companion to `web_search` (when registered) — after the model finds a\n * promising result it fetches the page to read the body in full. HTML is\n * reduced to plain text because the model rarely benefits from the markup\n * and we want to keep the payload inside the per-turn output budget.\n *\n * Restrictions:\n * - Only http(s) URLs. Anything else (file://, data:, ftp:) is rejected so\n * the tool can't be coerced into reading local files or arbitrary\n * resources.\n * - SSRF guard with TOCTOU defense: the host is DNS-resolved against the\n * loopback / link-local / private / reserved blocklist BEFORE the\n * request, and the resolved IP is then PINNED at the connection layer\n * via `node:http(s).request({ host: <resolved IP>, headers: { Host: ... },\n * servername: ... })`. This closes the classic DNS-rebinding gap where a\n * `globalThis.fetch` re-resolves the hostname and connects to a freshly\n * minted private IP. TLS verification still runs against the original\n * hostname via the explicit `servername` option.\n *\n * Note: the more idiomatic Node fix (an `undici.Agent` with a custom\n * `connect.lookup`) is silently a no-op on Bun ≤1.3 — both\n * `undici.Agent` and `node:https.Agent.lookup` are stubbed out. The\n * explicit `host: IP` + `Host:` header dance is the only mechanism that\n * actually pins on the current Bun runtime.\n * - Redirects are followed manually (up to `MAX_REDIRECTS`) and each hop\n * is re-validated, so a public hostname that 302s into a metadata\n * endpoint is rejected at the redirect step.\n * - 15s hard timeout per request.\n * - Output capped at `max_bytes` (default 200 KiB) — long pages are\n * truncated with an explicit marker. The pinned reader also stops\n * buffering past 2× the cap so a multi-MB body can't OOM us.\n */\n\nconst DEFAULT_MAX_BYTES = 200 * 1024\nconst HARD_MAX_BYTES = 1024 * 1024\nconst HTTP_TIMEOUT_MS = 15_000\nconst MAX_REDIRECTS = 5\n\n/**\n * IPv4 ranges that the SSRF guard refuses to fetch from. Covers the cloud\n * metadata services (AWS/Azure 169.254.169.254, GCP routes through the same\n * link-local block) plus every RFC1918 / loopback / reserved block a model\n * could be prompt-injected into hitting.\n */\nfunction isBlockedIPv4(ip: string): boolean {\n const parts = ip.split('.').map(Number)\n if (parts.length !== 4 || parts.some(p => !Number.isInteger(p) || p < 0 || p > 255))\n return true\n const [a, b] = parts as [number, number, number, number]\n if (a === 0)\n return true // 0.0.0.0/8 unspecified / \"this network\"\n if (a === 10)\n return true // 10/8 private\n if (a === 127)\n return true // loopback\n if (a === 169 && b === 254)\n return true // link-local + cloud metadata\n if (a === 172 && b >= 16 && b <= 31)\n return true // 172.16/12 private\n if (a === 192 && b === 168)\n return true // 192.168/16 private\n if (a === 192 && b === 0 && parts[2] === 0)\n return true // 192.0.0/24 IETF\n if (a === 198 && (b === 18 || b === 19))\n return true // 198.18/15 benchmark\n if (a >= 224)\n return true // multicast + reserved (224/4, 240/4)\n return false\n}\n\nfunction isBlockedIPv6(ip: string): boolean {\n const lower = ip.toLowerCase()\n // Normalise IPv4-mapped (::ffff:1.2.3.4) — recurse on the v4 tail. URL\n // parsing tends to canonicalise the dotted form into the hex pair\n // `::ffff:a9fe:a9fe`, so accept both shapes.\n const v4MappedDotted = lower.match(/^::ffff:([0-9.]+)$/)\n if (v4MappedDotted)\n return isBlockedIPv4(v4MappedDotted[1]!)\n const v4MappedHex = lower.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/)\n if (v4MappedHex) {\n const hi = Number.parseInt(v4MappedHex[1]!, 16)\n const lo = Number.parseInt(v4MappedHex[2]!, 16)\n const ipv4 = `${(hi >> 8) & 0xFF}.${hi & 0xFF}.${(lo >> 8) & 0xFF}.${lo & 0xFF}`\n return isBlockedIPv4(ipv4)\n }\n if (lower === '::' || lower === '::1')\n return true\n // fc00::/7 unique-local (covers fc.. and fd..)\n if (/^f[cd][0-9a-f]{0,2}:/.test(lower))\n return true\n // fe80::/10 link-local\n if (/^fe[89ab][0-9a-f]?:/.test(lower))\n return true\n // ff00::/8 multicast\n if (lower.startsWith('ff'))\n return true\n return false\n}\n\nexport function isBlockedAddress(ip: string): boolean {\n const family = isIP(ip)\n if (family === 4)\n return isBlockedIPv4(ip)\n if (family === 6)\n return isBlockedIPv6(ip)\n return true // not a recognised IP literal — refuse\n}\n\n/**\n * Normalize a hostname for allowlist comparison: lowercase, strip a trailing\n * dot (FQDN root) and IPv6 brackets, drop a leading dot on allowlist entries.\n */\nfunction normalizeHost(host: string): string {\n let h = host.trim().toLowerCase()\n if (h.startsWith('[') && h.endsWith(']'))\n h = h.slice(1, -1)\n if (h.endsWith('.'))\n h = h.slice(0, -1)\n if (h.startsWith('.'))\n h = h.slice(1)\n return h\n}\n\n/**\n * Site identity for cross-host redirect detection: the lowercased hostname\n * with a leading `www.` stripped, so `example.com` ⇄ `www.example.com` and a\n * path-only redirect aren't flagged as cross-host. Returns `null` for an\n * unparseable URL (caller skips the note rather than guessing).\n */\nfunction sameSiteHost(url: string): string | null {\n try {\n return new URL(url).hostname.toLowerCase().replace(/^www\\./, '')\n }\n catch {\n return null\n }\n}\n\n/**\n * Host-suffix allowlist check. An empty / undefined list means \"no allowlist\"\n * → every host passes (the SSRF blocklist is the only gate). Otherwise the\n * host must equal, or be a subdomain of, one of the entries. Subdomain match\n * is on a dot boundary so `example.com` matches `docs.example.com` but not\n * `notexample.com`.\n */\nexport function isHostAllowed(hostname: string, allowHosts: readonly string[] | undefined): boolean {\n if (!allowHosts || allowHosts.length === 0)\n return true\n const host = normalizeHost(hostname)\n for (const entry of allowHosts) {\n const allowed = normalizeHost(entry)\n if (allowed.length === 0)\n continue\n if (host === allowed || host.endsWith(`.${allowed}`))\n return true\n }\n return false\n}\n\n/**\n * Resolve `hostname` and refuse if any answer falls in a blocked range.\n *\n * Returns the first allowed `{ address, family }` so the caller can pin\n * the connection to it. Throws on any blocked answer so a multi-record\n * response with one private record fails closed.\n */\nasync function resolveAndCheck(hostname: string, allowHosts?: readonly string[]): Promise<{ address: string, family: 4 | 6 }> {\n // Egress allowlist (when configured) gates BEFORE DNS so a non-approved\n // host never even triggers a lookup. Enforced here so every redirect hop\n // is re-checked, not just the initial URL.\n if (!isHostAllowed(hostname, allowHosts))\n throw new Error(`refused: ${hostname} is not in the configured egress allowlist`)\n // `URL.hostname` keeps brackets around IPv6 literals (`[::1]`) — strip them\n // before the IP check so `isIP` recognises the literal and `dns.lookup`\n // doesn't try to resolve `[::1]` as a name.\n const bare = hostname.startsWith('[') && hostname.endsWith(']')\n ? hostname.slice(1, -1)\n : hostname\n const family = isIP(bare)\n if (family === 4 || family === 6) {\n if (isBlockedAddress(bare))\n throw new Error(`refused: ${bare} is in a blocked range`)\n return { address: bare, family }\n }\n const answers = await dns.lookup(bare, { all: true, verbatim: true })\n if (answers.length === 0)\n throw new Error(`DNS lookup returned no records for ${bare}`)\n for (const { address } of answers) {\n if (isBlockedAddress(address))\n throw new Error(`refused: ${bare} resolves to blocked address ${address}`)\n }\n const first = answers[0]!\n const fam = first.family === 6 ? 6 : 4\n return { address: first.address, family: fam }\n}\n\n/**\n * Pinned HTTP/HTTPS request. Connects to `pinnedIp` directly (bypassing\n * Node/Bun's own DNS resolution) while preserving the `Host:` header and\n * (for HTTPS) TLS SNI on the URL's original hostname. This is the\n * concrete defense against DNS rebinding — see the file-level docstring.\n *\n * Reads up to `maxBufferBytes` of body and then forcibly closes the\n * connection. The body is decoded as UTF-8; binary responses degrade to\n * mojibake but the tool's contract is text payloads anyway.\n */\ninterface PinnedResponse {\n status: number\n url: string\n contentType: string\n body: string\n location: string | null\n /**\n * True when the body was cut at `maxBufferBytes` before the server\n * finished sending. Optional so test seams that stub `requestImpl`\n * don't have to populate it; absent means \"not truncated\".\n */\n truncated?: boolean\n}\n\nfunction buildHostHeader(url: URL): string {\n if (!url.port)\n return url.hostname\n const defaultPort = url.protocol === 'https:' ? '443' : '80'\n return url.port === defaultPort ? url.hostname : `${url.hostname}:${url.port}`\n}\n\nexport function pinnedRequest(\n url: URL,\n pinnedIp: string,\n family: 4 | 6,\n headers: Record<string, string>,\n signal: AbortSignal,\n maxBufferBytes: number,\n): Promise<PinnedResponse> {\n return new Promise((resolve, reject) => {\n const isHttps = url.protocol === 'https:'\n const port = url.port\n ? Number.parseInt(url.port, 10)\n : (isHttps ? 443 : 80)\n\n // `host` is the literal IP, `servername` is the original hostname so\n // TLS cert verification still matches the cert's SAN/CN against the\n // user-visible name. Node uses `servername` for `checkServerIdentity`\n // automatically when set.\n const options: HttpRequestOptions & HttpsRequestOptions = {\n host: pinnedIp,\n port,\n path: `${url.pathname}${url.search}`,\n method: 'GET',\n family,\n headers: { ...headers, Host: buildHostHeader(url) },\n ...(isHttps ? { servername: url.hostname } : {}),\n }\n\n const request = isHttps ? httpsRequest : httpRequest\n const chunks: Buffer[] = []\n let received = 0\n // Single-shot settle guard. Bun's `node:http` is quirky around\n // `req.destroy(err)` — passing an error doesn't reliably surface as an\n // `error` event the way it does on Node. We track the settle state\n // ourselves and call `resolve` / `reject` directly from the abort /\n // timeout paths so the promise terminates even when the underlying\n // socket teardown is silent.\n let settled = false\n // Forward-declared so the `finish` / abort / timeout handlers can\n // close over `req` before it's assigned below. The request object is\n // wired into them via mutable refs because the handlers must be set\n // up *before* `request(options, callback)` fires its first event.\n let req: ReturnType<typeof httpRequest> | undefined\n let timer: ReturnType<typeof setTimeout> | undefined\n\n const teardown = (): void => {\n try { req?.destroy() }\n catch { /* tear-down best effort */ }\n }\n\n const finish = (settler: () => void): void => {\n if (settled)\n return\n settled = true\n if (timer)\n clearTimeout(timer)\n signal.removeEventListener('abort', onSignalAbort)\n settler()\n }\n\n function onSignalAbort(): void {\n finish(() => reject(new Error('request aborted')))\n teardown()\n }\n\n // Pre-aborted signal — short-circuit so we don't even open a socket.\n // (This is the realistic shape when a user hits Ctrl+C while the agent\n // is already shutting down: the signal is `.aborted` by the time the\n // tool function runs.)\n if (signal.aborted) {\n reject(new Error('request aborted'))\n return\n }\n\n timer = setTimeout(() => {\n finish(() => reject(new Error(`request timed out after ${HTTP_TIMEOUT_MS}ms`)))\n teardown()\n }, HTTP_TIMEOUT_MS)\n\n signal.addEventListener('abort', onSignalAbort, { once: true })\n\n req = request(options, (res) => {\n const status = res.statusCode ?? 0\n const headersOut = res.headers\n const contentType = (headersOut['content-type'] ?? '').toString().toLowerCase()\n const location = typeof headersOut.location === 'string' ? headersOut.location : null\n\n // Short-circuit on redirect responses — body is irrelevant.\n if (status >= 300 && status < 400 && location) {\n res.resume() // drain so the socket can be released\n finish(() => resolve({ status, url: url.toString(), contentType, body: '', location }))\n return\n }\n\n let bodyTruncated = false\n res.on('data', (chunk: Buffer) => {\n if (received >= maxBufferBytes) {\n // Stop accumulating; tear the socket down so the server stops\n // sending. We still resolve cleanly from `end` (or `close` if\n // the server doesn't finish flushing before the kill lands).\n bodyTruncated = true\n teardown()\n return\n }\n // Keep only what fits in the budget — a chunk straddling the cap\n // would otherwise overshoot it by up to one chunk. A partial slice\n // means the body was cut, so flag it and stop the transfer.\n const room = maxBufferBytes - received\n if (chunk.length > room) {\n chunks.push(chunk.subarray(0, room))\n received += room\n bodyTruncated = true\n teardown()\n return\n }\n received += chunk.length\n chunks.push(chunk)\n })\n res.on('end', () => {\n const body = Buffer.concat(chunks).toString('utf-8')\n finish(() => resolve({ status, url: url.toString(), contentType, body, location: null, truncated: bodyTruncated }))\n })\n res.on('close', () => {\n // Socket torn down before `end` (e.g. when our maxBufferBytes\n // trigger destroyed the request). Resolve with whatever we\n // buffered so far — `truncated` tells the caller the body is cut.\n if (!settled) {\n const body = Buffer.concat(chunks).toString('utf-8')\n finish(() => resolve({ status, url: url.toString(), contentType, body, location: null, truncated: bodyTruncated }))\n }\n })\n res.on('error', err => finish(() => reject(err)))\n })\n\n req.on('error', (err: Error) => finish(() => reject(err)))\n req.end()\n })\n}\n\n/**\n * Walk up to {@link MAX_REDIRECTS} hops, validating the target host of each\n * 3xx response against the SSRF blocklist before following.\n *\n * `deps` is an internal seam for tests — production callers use the\n * defaults. Both knobs are intentionally opaque: hosts that want to\n * customize the blocklist should use a higher-level mechanism (a future\n * `behavior.fetchUrlAllowHosts` setting) rather than reaching into this.\n */\nexport interface FetchSsrfDeps {\n /** Resolve + validate a hostname. Defaults to {@link resolveAndCheck}. */\n resolver?: (hostname: string) => Promise<{ address: string, family: 4 | 6 }>\n /** Execute a single pinned HTTP request. Defaults to {@link pinnedRequest}. */\n requestImpl?: typeof pinnedRequest\n}\n\nexport async function fetchWithSsrfGuard(\n startUrl: URL,\n headers: Record<string, string>,\n signal: AbortSignal,\n maxBufferBytes: number,\n deps: FetchSsrfDeps = {},\n allowHosts?: readonly string[],\n): Promise<PinnedResponse> {\n // Default resolver binds the egress allowlist; a test-injected resolver\n // owns its own policy.\n const resolver = deps.resolver ?? ((hostname: string) => resolveAndCheck(hostname, allowHosts))\n const requestImpl = deps.requestImpl ?? pinnedRequest\n let current = startUrl\n for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {\n const { address, family } = await resolver(current.hostname)\n const res = await requestImpl(current, address, family, headers, signal, maxBufferBytes)\n if (!res.location)\n return res\n if (hop === MAX_REDIRECTS)\n throw new Error(`too many redirects (>${MAX_REDIRECTS})`)\n let next: URL\n try {\n next = new URL(res.location, current)\n }\n catch {\n throw new Error(`invalid redirect target: ${res.location}`)\n }\n if (next.protocol !== 'http:' && next.protocol !== 'https:')\n throw new Error(`refused redirect to non-http(s) target: ${next.protocol}`)\n current = next\n }\n throw new Error('redirect loop exited unexpectedly')\n}\n\n/**\n * Opt-in, process-local response cache for `fetch_url`. Disabled unless\n * `behavior.fetchUrlCacheTtlMs` is set (> 0). Bounded by entry count and total\n * cached bytes; eviction is oldest-first (insertion order via `Map`, which is\n * a good-enough LRU because we delete-then-set on hit to refresh recency).\n *\n * Keyed on `<url>\\u0000<cap>\\u0000<egress-policy>` so two calls with different\n * `max_bytes` (hence different truncation) don't alias, and — because the\n * cache is module-global — an agent with a permissive `fetchUrlAllowHosts`\n * can't populate entries that a co-resident agent with a stricter allowlist\n * would then be served. Only successful text responses are stored — the\n * caller gates on status before writing.\n */\nconst FETCH_CACHE_MAX_ENTRIES = 64\nconst FETCH_CACHE_MAX_BYTES = 16 * 1024 * 1024 // 16 MiB total\n\ninterface FetchCacheEntry {\n /** Fully-formatted tool output (header + body) ready to return verbatim. */\n output: string\n expiresAt: number\n bytes: number\n}\n\nconst fetchCache = new Map<string, FetchCacheEntry>()\nlet fetchCacheBytes = 0\n\nfunction cacheKey(url: string, cap: number, allowHosts?: readonly string[]): string {\n // Normalize + sort so equivalent policies (`['A.com', 'b.com']` vs\n // `['b.com', 'a.com.']`) share entries; an absent/empty allowlist keys\n // as the empty policy.\n const policy = (allowHosts ?? []).map(normalizeHost).sort().join(',')\n return `${url}\\u0000${cap}\\u0000${policy}`\n}\n\nfunction cacheGet(key: string): string | null {\n const entry = fetchCache.get(key)\n if (!entry)\n return null\n if (Date.now() >= entry.expiresAt) {\n fetchCache.delete(key)\n fetchCacheBytes -= entry.bytes\n return null\n }\n // Refresh recency: re-insert at the tail of the Map's iteration order.\n fetchCache.delete(key)\n fetchCache.set(key, entry)\n return entry.output\n}\n\nfunction cacheSet(key: string, output: string, ttlMs: number): void {\n const bytes = Buffer.byteLength(output, 'utf-8')\n // A single oversize payload that can't fit under the byte cap is simply not\n // cached (still returned to the caller) rather than evicting everything.\n if (bytes > FETCH_CACHE_MAX_BYTES)\n return\n // Drop any stale entry for this key first so the byte accounting stays exact.\n const prev = fetchCache.get(key)\n if (prev) {\n fetchCache.delete(key)\n fetchCacheBytes -= prev.bytes\n }\n fetchCache.set(key, { output, expiresAt: Date.now() + ttlMs, bytes })\n fetchCacheBytes += bytes\n // Evict oldest-first until both caps are satisfied.\n while (fetchCache.size > FETCH_CACHE_MAX_ENTRIES || fetchCacheBytes > FETCH_CACHE_MAX_BYTES) {\n const oldest = fetchCache.keys().next().value\n if (oldest === undefined)\n break\n const victim = fetchCache.get(oldest)!\n fetchCache.delete(oldest)\n fetchCacheBytes -= victim.bytes\n }\n}\n\n/** Test seam — drop all cached entries. */\nexport function clearFetchUrlCache(): void {\n fetchCache.clear()\n fetchCacheBytes = 0\n}\n\n/**\n * Test seam — exercise the cache get/set/eviction in isolation without\n * standing up the full SSRF + HTTP path. Not part of the public tool surface;\n * `execute` uses the module-private functions directly.\n */\nexport const __fetchCacheTestApi = {\n key: cacheKey,\n get: cacheGet,\n set: cacheSet,\n size: () => fetchCache.size,\n bytes: () => fetchCacheBytes,\n}\n\nexport const fetchUrl: ToolDef = {\n isConcurrencySafe: true,\n spec: {\n name: 'fetch_url',\n description: 'Fetch a public URL and return its body as plain text. HTML is reduced to text (scripts, styles, tags stripped; entities decoded). If a `web_search` tool is also available, use it first to find the URL. Only http(s) URLs are accepted; loopback, link-local, private, and cloud-metadata addresses are refused. Output is capped (default 200 KiB).',\n inputSchema: {\n type: 'object',\n properties: {\n url: {\n type: 'string',\n description: 'HTTP or HTTPS URL to fetch.',\n },\n max_bytes: {\n type: 'number',\n description: `Truncate the response body beyond this many bytes. Default: ${DEFAULT_MAX_BYTES}. Hard cap: ${HARD_MAX_BYTES}.`,\n },\n },\n required: ['url'],\n },\n },\n async execute({ url, max_bytes }, ctx: ToolContext) {\n const raw = typeof url === 'string' ? url.trim() : ''\n if (!raw)\n return 'fetch_url error: `url` is required'\n let parsed: URL\n try {\n parsed = new URL(raw)\n }\n catch {\n return `fetch_url error: invalid URL: ${raw}`\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')\n return `fetch_url error: only http(s) URLs are allowed (got ${parsed.protocol})`\n\n const cap = Math.min(\n typeof max_bytes === 'number' && max_bytes > 0 ? Math.floor(max_bytes) : DEFAULT_MAX_BYTES,\n HARD_MAX_BYTES,\n )\n // Buffer up to 2× the cap so HTML overhead doesn't starve the visible\n // text budget — a 200 KiB cap on rendered text can legitimately need to\n // read ~400 KiB of raw HTML before stripping. Hard ceiling is still\n // governed by `HARD_MAX_BYTES * 2` which is comfortably below OOM.\n const bufferCap = Math.min(cap * 2, HARD_MAX_BYTES * 2)\n\n // Opt-in response cache (off unless `behavior.fetchUrlCacheTtlMs > 0`).\n // The egress allowlist is part of the key AND re-checked before serving\n // a hit — the cache is module-global, so neither a policy change within\n // an agent nor a co-resident agent's entries may bypass this agent's\n // allowlist.\n const allowHosts = ctx.behavior?.fetchUrlAllowHosts\n const ttlMs = ctx.behavior?.fetchUrlCacheTtlMs\n const cacheEnabled = typeof ttlMs === 'number' && ttlMs > 0\n const key = cacheKey(parsed.toString(), cap, allowHosts)\n if (cacheEnabled && isHostAllowed(parsed.hostname, allowHosts)) {\n const hit = cacheGet(key)\n if (hit !== null)\n return hit\n }\n\n try {\n const res = await fetchWithSsrfGuard(\n parsed,\n {\n 'user-agent': 'Mozilla/5.0 (compatible; zidane/1.0)',\n 'accept': 'text/html,text/plain,application/json;q=0.9,*/*;q=0.5',\n 'accept-encoding': 'identity', // no gzip — we don't decompress\n },\n ctx.signal,\n bufferCap,\n {},\n allowHosts,\n )\n const body = res.body\n const isHtml = res.contentType.includes('html') || /^\\s*<!doctype html|^\\s*<html/i.test(body)\n const text = isHtml ? stripHtmlBlock(body) : body\n const truncated = text.length > cap\n // Two distinct truncation reasons: the rendered text overflowing `cap`,\n // and the raw transfer being cut at the buffer ceiling (`res.truncated`)\n // — the latter can leave the rendered text under `cap` (HTML overhead\n // stripped away), so it needs its own marker or the model would treat\n // a partial body as complete.\n const out = truncated\n ? `${text.slice(0, cap)}\\n\\n[truncated at ${cap} bytes; full length ${text.length}]`\n : res.truncated\n ? `${text}\\n\\n[response truncated: body exceeded the ${bufferCap}-byte transfer buffer]`\n : text\n // `res.url` reflects the FINAL hop after redirects; flag a cross-host\n // landing so the model knows the content came from a different origin\n // than it asked for (open-redirect awareness). Same-host path changes\n // and www add/strip are treated as the same site and not flagged.\n const finalHost = sameSiteHost(res.url)\n const requestedHost = sameSiteHost(parsed.toString())\n const redirectNote = finalHost && requestedHost && finalHost !== requestedHost\n ? `Note: ${parsed.hostname} redirected to a different host (${new URL(res.url).hostname}).\\n`\n : ''\n const header = `URL: ${res.url}\\nStatus: ${res.status}\\nContent-Type: ${res.contentType || '(none)'}\\n${redirectNote}\\n`\n const formatted = header + out\n // Cache only successful, complete text responses — never errors /\n // redirects / transfer-truncated bodies (a retry may get the rest).\n if (cacheEnabled && res.status >= 200 && res.status < 300 && res.truncated !== true)\n cacheSet(key, formatted, ttlMs)\n return formatted\n }\n catch (err) {\n return `fetch_url error: ${errorMessage(err)}`\n }\n },\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fetch-url-_5WKWpbQ.js","names":["dns","request","httpsRequest","httpRequest"],"sources":["../src/tools/_html.ts","../src/tools/fetch-url.ts"],"sourcesContent":["/**\n * Shared HTML → plain-text helpers used by the web-egress tools (`web_search`\n * snippet extraction, `fetch_url` body decoding).\n *\n * Best-effort by contract: a real HTML parser would be more correct but the\n * payload is always model context, never user-facing rendered output, so a\n * tag-stripper plus a curated entity table covers the practical cases\n * (Wikipedia, GitHub READMEs, MDN docs, Stack Overflow answers). When a tool\n * needs richer parsing it should reach for an actual parser rather than\n * extending these helpers ad-hoc.\n */\n\n/**\n * Curated named-entity table. Kept narrow on purpose — the long tail\n * (`Å`, `×`, mathematical refs, …) is out of scope because the\n * model handles unknown entities fine when they pass through verbatim. Add\n * entries when an entity is observed mangling real output, not speculatively.\n *\n * Keys are stored without the leading `&` / trailing `;` and matched\n * case-insensitively by the decoder.\n */\nconst NAMED_ENTITIES: Record<string, string> = {\n amp: '&',\n apos: '\\'',\n bull: '•',\n copy: '\\u00A9',\n ensp: ' ',\n emsp: ' ',\n gt: '>',\n hellip: '\\u2026',\n laquo: '\\u00AB',\n ldquo: '\\u201C',\n lsquo: '\\u2018',\n lt: '<',\n mdash: '\\u2014',\n middot: '\\u00B7',\n nbsp: ' ',\n ndash: '\\u2013',\n quot: '\"',\n raquo: '\\u00BB',\n rdquo: '\\u201D',\n reg: '\\u00AE',\n rsquo: '\\u2019',\n thinsp: ' ',\n trade: '\\u2122',\n}\n\n/**\n * `String.fromCodePoint` raises on out-of-range / non-finite inputs; the\n * agent's untrusted-payload assumption means we can't let a single broken\n * numeric reference crash the whole decode. Empty string on rejection is\n * the safest outcome (matches what most browsers do for invalid refs).\n */\nfunction safeFromCodePoint(cp: number): string {\n if (!Number.isFinite(cp) || cp < 0 || cp > 0x10FFFF)\n return ''\n try {\n return String.fromCodePoint(cp)\n }\n catch {\n return ''\n }\n}\n\n/**\n * Decode numeric character references (`&#x...;` / `&#NNN;`) and the curated\n * subset of named entities in {@link NAMED_ENTITIES}. Unknown named entities\n * are left as-is so the model can detect them rather than seeing a silently\n * corrupted token.\n *\n * `&` is decoded LAST so a double-escaped string like `&lt;` resolves\n * to `<` (not `<`) — that's the spec-correct single-pass decode shape.\n */\nexport function decodeHtmlEntities(text: string): string {\n let s = text\n .replace(/&#x([0-9a-f]+);/gi, (_, h) => safeFromCodePoint(Number.parseInt(h, 16)))\n .replace(/&#(\\d+);/g, (_, n) => safeFromCodePoint(Number.parseInt(n, 10)))\n s = s.replace(/&([a-z][a-z0-9]*);/gi, (full, name) => {\n const key = name.toLowerCase()\n // Defer `amp` so `&lt;` doesn't double-decode in a single pass.\n if (key === 'amp')\n return full\n const replacement = NAMED_ENTITIES[key]\n return replacement ?? full\n })\n return s.replace(/&/gi, '&')\n}\n\n/**\n * Strip `<script>`, `<style>`, `<noscript>`, comments, and every other tag.\n * Block-level openers (`<br>`, `<p>`, `<div>`, `<li>`, `<tr>`, `<h1-6>`) are\n * replaced with newlines so the resulting plaintext preserves readable\n * structure. Other tags collapse to spaces.\n */\nexport function stripHtml(html: string): string {\n return html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, ' ')\n .replace(/<!--[\\s\\S]*?-->/g, ' ')\n .replace(/<(?:br|p|div|li|tr|h[1-6])[^>]*>/gi, '\\n')\n .replace(/<[^>]+>/g, ' ')\n}\n\n/**\n * Inline variant — strip tags + decode entities + collapse all whitespace\n * (including line breaks) into single spaces. For short snippets where\n * structural breaks would be visual noise (search-result titles, link text).\n */\nexport function stripHtmlInline(html: string): string {\n return decodeHtmlEntities(stripHtml(html))\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\n/**\n * Block variant — strip tags + decode entities, preserve newlines emitted\n * by block-level openers, then collapse inline whitespace per-line and drop\n * empty lines. For full-page reductions where structure aids reading.\n */\nexport function stripHtmlBlock(html: string): string {\n return decodeHtmlEntities(stripHtml(html))\n .split('\\n')\n .map(line => line.replace(/[ \\t]+/g, ' ').trim())\n .filter(line => line.length > 0)\n .join('\\n')\n}\n","import type { RequestOptions as HttpRequestOptions } from 'node:http'\nimport type { RequestOptions as HttpsRequestOptions } from 'node:https'\nimport type { ToolContext, ToolDef } from './types'\nimport { Buffer } from 'node:buffer'\nimport { promises as dns } from 'node:dns'\nimport { request as httpRequest } from 'node:http'\nimport { request as httpsRequest } from 'node:https'\nimport { isIP } from 'node:net'\nimport { errorMessage } from '../errors'\nimport { stripHtmlBlock } from './_html'\n\n/**\n * Fetch a URL and return its text content.\n *\n * Companion to `web_search` (when registered) — after the model finds a\n * promising result it fetches the page to read the body in full. HTML is\n * reduced to plain text because the model rarely benefits from the markup\n * and we want to keep the payload inside the per-turn output budget.\n *\n * Restrictions:\n * - Only http(s) URLs. Anything else (file://, data:, ftp:) is rejected so\n * the tool can't be coerced into reading local files or arbitrary\n * resources.\n * - SSRF guard with TOCTOU defense: the host is DNS-resolved against the\n * loopback / link-local / private / reserved blocklist BEFORE the\n * request, and the resolved IP is then PINNED at the connection layer\n * via `node:http(s).request({ host: <resolved IP>, headers: { Host: ... },\n * servername: ... })`. This closes the classic DNS-rebinding gap where a\n * `globalThis.fetch` re-resolves the hostname and connects to a freshly\n * minted private IP. TLS verification still runs against the original\n * hostname via the explicit `servername` option.\n *\n * Note: the more idiomatic Node fix (an `undici.Agent` with a custom\n * `connect.lookup`) is silently a no-op on Bun ≤1.3 — both\n * `undici.Agent` and `node:https.Agent.lookup` are stubbed out. The\n * explicit `host: IP` + `Host:` header dance is the only mechanism that\n * actually pins on the current Bun runtime.\n * - Redirects are followed manually (up to `MAX_REDIRECTS`) and each hop\n * is re-validated, so a public hostname that 302s into a metadata\n * endpoint is rejected at the redirect step.\n * - 15s hard timeout per request.\n * - Output capped at `max_bytes` (default 200 KiB) — long pages are\n * truncated with an explicit marker. The pinned reader also stops\n * buffering past 2× the cap so a multi-MB body can't OOM us.\n */\n\nconst DEFAULT_MAX_BYTES = 200 * 1024\nconst HARD_MAX_BYTES = 1024 * 1024\nconst HTTP_TIMEOUT_MS = 15_000\nconst MAX_REDIRECTS = 5\n\n/**\n * IPv4 ranges that the SSRF guard refuses to fetch from. Covers the cloud\n * metadata services (AWS/Azure 169.254.169.254, GCP routes through the same\n * link-local block) plus every RFC1918 / loopback / reserved block a model\n * could be prompt-injected into hitting.\n */\nfunction isBlockedIPv4(ip: string): boolean {\n const parts = ip.split('.').map(Number)\n if (parts.length !== 4 || parts.some(p => !Number.isInteger(p) || p < 0 || p > 255))\n return true\n const [a, b] = parts as [number, number, number, number]\n if (a === 0)\n return true // 0.0.0.0/8 unspecified / \"this network\"\n if (a === 10)\n return true // 10/8 private\n if (a === 127)\n return true // loopback\n if (a === 169 && b === 254)\n return true // link-local + cloud metadata\n if (a === 172 && b >= 16 && b <= 31)\n return true // 172.16/12 private\n if (a === 192 && b === 168)\n return true // 192.168/16 private\n if (a === 192 && b === 0 && parts[2] === 0)\n return true // 192.0.0/24 IETF\n if (a === 198 && (b === 18 || b === 19))\n return true // 198.18/15 benchmark\n if (a >= 224)\n return true // multicast + reserved (224/4, 240/4)\n return false\n}\n\nfunction isBlockedIPv6(ip: string): boolean {\n const lower = ip.toLowerCase()\n // Normalise IPv4-mapped (::ffff:1.2.3.4) — recurse on the v4 tail. URL\n // parsing tends to canonicalise the dotted form into the hex pair\n // `::ffff:a9fe:a9fe`, so accept both shapes.\n const v4MappedDotted = lower.match(/^::ffff:([0-9.]+)$/)\n if (v4MappedDotted)\n return isBlockedIPv4(v4MappedDotted[1]!)\n const v4MappedHex = lower.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/)\n if (v4MappedHex) {\n const hi = Number.parseInt(v4MappedHex[1]!, 16)\n const lo = Number.parseInt(v4MappedHex[2]!, 16)\n const ipv4 = `${(hi >> 8) & 0xFF}.${hi & 0xFF}.${(lo >> 8) & 0xFF}.${lo & 0xFF}`\n return isBlockedIPv4(ipv4)\n }\n if (lower === '::' || lower === '::1')\n return true\n // fc00::/7 unique-local (covers fc.. and fd..)\n if (/^f[cd][0-9a-f]{0,2}:/.test(lower))\n return true\n // fe80::/10 link-local\n if (/^fe[89ab][0-9a-f]?:/.test(lower))\n return true\n // ff00::/8 multicast\n if (lower.startsWith('ff'))\n return true\n return false\n}\n\nexport function isBlockedAddress(ip: string): boolean {\n const family = isIP(ip)\n if (family === 4)\n return isBlockedIPv4(ip)\n if (family === 6)\n return isBlockedIPv6(ip)\n return true // not a recognised IP literal — refuse\n}\n\n/**\n * Normalize a hostname for allowlist comparison: lowercase, strip a trailing\n * dot (FQDN root) and IPv6 brackets, drop a leading dot on allowlist entries.\n */\nfunction normalizeHost(host: string): string {\n let h = host.trim().toLowerCase()\n if (h.startsWith('[') && h.endsWith(']'))\n h = h.slice(1, -1)\n if (h.endsWith('.'))\n h = h.slice(0, -1)\n if (h.startsWith('.'))\n h = h.slice(1)\n return h\n}\n\n/**\n * Site identity for cross-host redirect detection: the lowercased hostname\n * with a leading `www.` stripped, so `example.com` ⇄ `www.example.com` and a\n * path-only redirect aren't flagged as cross-host. Returns `null` for an\n * unparseable URL (caller skips the note rather than guessing).\n */\nfunction sameSiteHost(url: string): string | null {\n try {\n return new URL(url).hostname.toLowerCase().replace(/^www\\./, '')\n }\n catch {\n return null\n }\n}\n\n/**\n * Host-suffix allowlist check. An empty / undefined list means \"no allowlist\"\n * → every host passes (the SSRF blocklist is the only gate). Otherwise the\n * host must equal, or be a subdomain of, one of the entries. Subdomain match\n * is on a dot boundary so `example.com` matches `docs.example.com` but not\n * `notexample.com`.\n */\nexport function isHostAllowed(hostname: string, allowHosts: readonly string[] | undefined): boolean {\n if (!allowHosts || allowHosts.length === 0)\n return true\n const host = normalizeHost(hostname)\n for (const entry of allowHosts) {\n const allowed = normalizeHost(entry)\n if (allowed.length === 0)\n continue\n if (host === allowed || host.endsWith(`.${allowed}`))\n return true\n }\n return false\n}\n\n/**\n * Resolve `hostname` and refuse if any answer falls in a blocked range.\n *\n * Returns the first allowed `{ address, family }` so the caller can pin\n * the connection to it. Throws on any blocked answer so a multi-record\n * response with one private record fails closed.\n */\nasync function resolveAndCheck(hostname: string, allowHosts?: readonly string[]): Promise<{ address: string, family: 4 | 6 }> {\n // Egress allowlist (when configured) gates BEFORE DNS so a non-approved\n // host never even triggers a lookup. Enforced here so every redirect hop\n // is re-checked, not just the initial URL.\n if (!isHostAllowed(hostname, allowHosts))\n throw new Error(`refused: ${hostname} is not in the configured egress allowlist`)\n // `URL.hostname` keeps brackets around IPv6 literals (`[::1]`) — strip them\n // before the IP check so `isIP` recognises the literal and `dns.lookup`\n // doesn't try to resolve `[::1]` as a name.\n const bare = hostname.startsWith('[') && hostname.endsWith(']')\n ? hostname.slice(1, -1)\n : hostname\n const family = isIP(bare)\n if (family === 4 || family === 6) {\n if (isBlockedAddress(bare))\n throw new Error(`refused: ${bare} is in a blocked range`)\n return { address: bare, family }\n }\n const answers = await dns.lookup(bare, { all: true, verbatim: true })\n if (answers.length === 0)\n throw new Error(`DNS lookup returned no records for ${bare}`)\n for (const { address } of answers) {\n if (isBlockedAddress(address))\n throw new Error(`refused: ${bare} resolves to blocked address ${address}`)\n }\n const first = answers[0]!\n const fam = first.family === 6 ? 6 : 4\n return { address: first.address, family: fam }\n}\n\n/**\n * Pinned HTTP/HTTPS request. Connects to `pinnedIp` directly (bypassing\n * Node/Bun's own DNS resolution) while preserving the `Host:` header and\n * (for HTTPS) TLS SNI on the URL's original hostname. This is the\n * concrete defense against DNS rebinding — see the file-level docstring.\n *\n * Reads up to `maxBufferBytes` of body and then forcibly closes the\n * connection. The body is decoded as UTF-8; binary responses degrade to\n * mojibake but the tool's contract is text payloads anyway.\n */\ninterface PinnedResponse {\n status: number\n url: string\n contentType: string\n body: string\n location: string | null\n /**\n * True when the body was cut at `maxBufferBytes` before the server\n * finished sending. Optional so test seams that stub `requestImpl`\n * don't have to populate it; absent means \"not truncated\".\n */\n truncated?: boolean\n}\n\nfunction buildHostHeader(url: URL): string {\n if (!url.port)\n return url.hostname\n const defaultPort = url.protocol === 'https:' ? '443' : '80'\n return url.port === defaultPort ? url.hostname : `${url.hostname}:${url.port}`\n}\n\nexport function pinnedRequest(\n url: URL,\n pinnedIp: string,\n family: 4 | 6,\n headers: Record<string, string>,\n signal: AbortSignal,\n maxBufferBytes: number,\n): Promise<PinnedResponse> {\n return new Promise((resolve, reject) => {\n const isHttps = url.protocol === 'https:'\n const port = url.port\n ? Number.parseInt(url.port, 10)\n : (isHttps ? 443 : 80)\n\n // `host` is the literal IP, `servername` is the original hostname so\n // TLS cert verification still matches the cert's SAN/CN against the\n // user-visible name. Node uses `servername` for `checkServerIdentity`\n // automatically when set.\n const options: HttpRequestOptions & HttpsRequestOptions = {\n host: pinnedIp,\n port,\n path: `${url.pathname}${url.search}`,\n method: 'GET',\n family,\n headers: { ...headers, Host: buildHostHeader(url) },\n ...(isHttps ? { servername: url.hostname } : {}),\n }\n\n const request = isHttps ? httpsRequest : httpRequest\n const chunks: Buffer[] = []\n let received = 0\n // Single-shot settle guard. Bun's `node:http` is quirky around\n // `req.destroy(err)` — passing an error doesn't reliably surface as an\n // `error` event the way it does on Node. We track the settle state\n // ourselves and call `resolve` / `reject` directly from the abort /\n // timeout paths so the promise terminates even when the underlying\n // socket teardown is silent.\n let settled = false\n // Forward-declared so the `finish` / abort / timeout handlers can\n // close over `req` before it's assigned below. The request object is\n // wired into them via mutable refs because the handlers must be set\n // up *before* `request(options, callback)` fires its first event.\n let req: ReturnType<typeof httpRequest> | undefined\n let timer: ReturnType<typeof setTimeout> | undefined\n\n const teardown = (): void => {\n try { req?.destroy() }\n catch { /* tear-down best effort */ }\n }\n\n const finish = (settler: () => void): void => {\n if (settled)\n return\n settled = true\n if (timer)\n clearTimeout(timer)\n signal.removeEventListener('abort', onSignalAbort)\n settler()\n }\n\n function onSignalAbort(): void {\n finish(() => reject(new Error('request aborted')))\n teardown()\n }\n\n // Pre-aborted signal — short-circuit so we don't even open a socket.\n // (This is the realistic shape when a user hits Ctrl+C while the agent\n // is already shutting down: the signal is `.aborted` by the time the\n // tool function runs.)\n if (signal.aborted) {\n reject(new Error('request aborted'))\n return\n }\n\n timer = setTimeout(() => {\n finish(() => reject(new Error(`request timed out after ${HTTP_TIMEOUT_MS}ms`)))\n teardown()\n }, HTTP_TIMEOUT_MS)\n\n signal.addEventListener('abort', onSignalAbort, { once: true })\n\n req = request(options, (res) => {\n const status = res.statusCode ?? 0\n const headersOut = res.headers\n const contentType = (headersOut['content-type'] ?? '').toString().toLowerCase()\n const location = typeof headersOut.location === 'string' ? headersOut.location : null\n\n // Short-circuit on redirect responses — body is irrelevant.\n if (status >= 300 && status < 400 && location) {\n res.resume() // drain so the socket can be released\n finish(() => resolve({ status, url: url.toString(), contentType, body: '', location }))\n return\n }\n\n let bodyTruncated = false\n res.on('data', (chunk: Buffer) => {\n if (received >= maxBufferBytes) {\n // Stop accumulating; tear the socket down so the server stops\n // sending. We still resolve cleanly from `end` (or `close` if\n // the server doesn't finish flushing before the kill lands).\n bodyTruncated = true\n teardown()\n return\n }\n // Keep only what fits in the budget — a chunk straddling the cap\n // would otherwise overshoot it by up to one chunk. A partial slice\n // means the body was cut, so flag it and stop the transfer.\n const room = maxBufferBytes - received\n if (chunk.length > room) {\n chunks.push(chunk.subarray(0, room))\n received += room\n bodyTruncated = true\n teardown()\n return\n }\n received += chunk.length\n chunks.push(chunk)\n })\n res.on('end', () => {\n const body = Buffer.concat(chunks).toString('utf-8')\n finish(() => resolve({ status, url: url.toString(), contentType, body, location: null, truncated: bodyTruncated }))\n })\n res.on('close', () => {\n // Socket torn down before `end` (e.g. when our maxBufferBytes\n // trigger destroyed the request). Resolve with whatever we\n // buffered so far — `truncated` tells the caller the body is cut.\n if (!settled) {\n const body = Buffer.concat(chunks).toString('utf-8')\n finish(() => resolve({ status, url: url.toString(), contentType, body, location: null, truncated: bodyTruncated }))\n }\n })\n res.on('error', err => finish(() => reject(err)))\n })\n\n req.on('error', (err: Error) => finish(() => reject(err)))\n req.end()\n })\n}\n\n/**\n * Walk up to {@link MAX_REDIRECTS} hops, validating the target host of each\n * 3xx response against the SSRF blocklist before following.\n *\n * `deps` is an internal seam for tests — production callers use the\n * defaults. Both knobs are intentionally opaque: hosts that want to\n * customize the blocklist should use a higher-level mechanism (a future\n * `behavior.fetchUrlAllowHosts` setting) rather than reaching into this.\n */\nexport interface FetchSsrfDeps {\n /** Resolve + validate a hostname. Defaults to {@link resolveAndCheck}. */\n resolver?: (hostname: string) => Promise<{ address: string, family: 4 | 6 }>\n /** Execute a single pinned HTTP request. Defaults to {@link pinnedRequest}. */\n requestImpl?: typeof pinnedRequest\n}\n\nexport async function fetchWithSsrfGuard(\n startUrl: URL,\n headers: Record<string, string>,\n signal: AbortSignal,\n maxBufferBytes: number,\n deps: FetchSsrfDeps = {},\n allowHosts?: readonly string[],\n): Promise<PinnedResponse> {\n // Default resolver binds the egress allowlist; a test-injected resolver\n // owns its own policy.\n const resolver = deps.resolver ?? ((hostname: string) => resolveAndCheck(hostname, allowHosts))\n const requestImpl = deps.requestImpl ?? pinnedRequest\n let current = startUrl\n for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {\n const { address, family } = await resolver(current.hostname)\n const res = await requestImpl(current, address, family, headers, signal, maxBufferBytes)\n if (!res.location)\n return res\n if (hop === MAX_REDIRECTS)\n throw new Error(`too many redirects (>${MAX_REDIRECTS})`)\n let next: URL\n try {\n next = new URL(res.location, current)\n }\n catch {\n throw new Error(`invalid redirect target: ${res.location}`)\n }\n if (next.protocol !== 'http:' && next.protocol !== 'https:')\n throw new Error(`refused redirect to non-http(s) target: ${next.protocol}`)\n current = next\n }\n throw new Error('redirect loop exited unexpectedly')\n}\n\n/**\n * Opt-in, process-local response cache for `fetch_url`. Disabled unless\n * `behavior.fetchUrlCacheTtlMs` is set (> 0). Bounded by entry count and total\n * cached bytes; eviction is oldest-first (insertion order via `Map`, which is\n * a good-enough LRU because we delete-then-set on hit to refresh recency).\n *\n * Keyed on `<url>\\u0000<cap>\\u0000<egress-policy>` so two calls with different\n * `max_bytes` (hence different truncation) don't alias, and — because the\n * cache is module-global — an agent with a permissive `fetchUrlAllowHosts`\n * can't populate entries that a co-resident agent with a stricter allowlist\n * would then be served. Only successful text responses are stored — the\n * caller gates on status before writing.\n */\nconst FETCH_CACHE_MAX_ENTRIES = 64\nconst FETCH_CACHE_MAX_BYTES = 16 * 1024 * 1024 // 16 MiB total\n\ninterface FetchCacheEntry {\n /** Fully-formatted tool output (header + body) ready to return verbatim. */\n output: string\n expiresAt: number\n bytes: number\n}\n\nconst fetchCache = new Map<string, FetchCacheEntry>()\nlet fetchCacheBytes = 0\n\nfunction cacheKey(url: string, cap: number, allowHosts?: readonly string[]): string {\n // Normalize + sort so equivalent policies (`['A.com', 'b.com']` vs\n // `['b.com', 'a.com.']`) share entries; an absent/empty allowlist keys\n // as the empty policy.\n const policy = (allowHosts ?? []).map(normalizeHost).sort().join(',')\n return `${url}\\u0000${cap}\\u0000${policy}`\n}\n\nfunction cacheGet(key: string): string | null {\n const entry = fetchCache.get(key)\n if (!entry)\n return null\n if (Date.now() >= entry.expiresAt) {\n fetchCache.delete(key)\n fetchCacheBytes -= entry.bytes\n return null\n }\n // Refresh recency: re-insert at the tail of the Map's iteration order.\n fetchCache.delete(key)\n fetchCache.set(key, entry)\n return entry.output\n}\n\nfunction cacheSet(key: string, output: string, ttlMs: number): void {\n const bytes = Buffer.byteLength(output, 'utf-8')\n // A single oversize payload that can't fit under the byte cap is simply not\n // cached (still returned to the caller) rather than evicting everything.\n if (bytes > FETCH_CACHE_MAX_BYTES)\n return\n // Drop any stale entry for this key first so the byte accounting stays exact.\n const prev = fetchCache.get(key)\n if (prev) {\n fetchCache.delete(key)\n fetchCacheBytes -= prev.bytes\n }\n fetchCache.set(key, { output, expiresAt: Date.now() + ttlMs, bytes })\n fetchCacheBytes += bytes\n // Evict oldest-first until both caps are satisfied.\n while (fetchCache.size > FETCH_CACHE_MAX_ENTRIES || fetchCacheBytes > FETCH_CACHE_MAX_BYTES) {\n const oldest = fetchCache.keys().next().value\n if (oldest === undefined)\n break\n const victim = fetchCache.get(oldest)!\n fetchCache.delete(oldest)\n fetchCacheBytes -= victim.bytes\n }\n}\n\n/** Test seam — drop all cached entries. */\nexport function clearFetchUrlCache(): void {\n fetchCache.clear()\n fetchCacheBytes = 0\n}\n\n/**\n * Test seam — exercise the cache get/set/eviction in isolation without\n * standing up the full SSRF + HTTP path. Not part of the public tool surface;\n * `execute` uses the module-private functions directly.\n */\nexport const __fetchCacheTestApi = {\n key: cacheKey,\n get: cacheGet,\n set: cacheSet,\n size: () => fetchCache.size,\n bytes: () => fetchCacheBytes,\n}\n\nexport const fetchUrl: ToolDef = {\n isConcurrencySafe: true,\n spec: {\n name: 'fetch_url',\n description: 'Fetch a public URL and return its body as plain text. HTML is reduced to text (scripts, styles, tags stripped; entities decoded). If a `web_search` tool is also available, use it first when you do not already know the exact URL. Only http(s) URLs are accepted; loopback, link-local, private, and cloud-metadata addresses are refused. Output is capped (default 200 KiB).',\n inputSchema: {\n type: 'object',\n properties: {\n url: {\n type: 'string',\n description: 'HTTP or HTTPS URL to fetch.',\n },\n max_bytes: {\n type: 'number',\n description: `Truncate the response body beyond this many bytes. Default: ${DEFAULT_MAX_BYTES}. Hard cap: ${HARD_MAX_BYTES}.`,\n },\n },\n required: ['url'],\n },\n },\n async execute({ url, max_bytes }, ctx: ToolContext) {\n const raw = typeof url === 'string' ? url.trim() : ''\n if (!raw)\n return 'fetch_url error: `url` is required'\n let parsed: URL\n try {\n parsed = new URL(raw)\n }\n catch {\n return `fetch_url error: invalid URL: ${raw}`\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')\n return `fetch_url error: only http(s) URLs are allowed (got ${parsed.protocol})`\n\n const cap = Math.min(\n typeof max_bytes === 'number' && max_bytes > 0 ? Math.floor(max_bytes) : DEFAULT_MAX_BYTES,\n HARD_MAX_BYTES,\n )\n // Buffer up to 2× the cap so HTML overhead doesn't starve the visible\n // text budget — a 200 KiB cap on rendered text can legitimately need to\n // read ~400 KiB of raw HTML before stripping. Hard ceiling is still\n // governed by `HARD_MAX_BYTES * 2` which is comfortably below OOM.\n const bufferCap = Math.min(cap * 2, HARD_MAX_BYTES * 2)\n\n // Opt-in response cache (off unless `behavior.fetchUrlCacheTtlMs > 0`).\n // The egress allowlist is part of the key AND re-checked before serving\n // a hit — the cache is module-global, so neither a policy change within\n // an agent nor a co-resident agent's entries may bypass this agent's\n // allowlist.\n const allowHosts = ctx.behavior?.fetchUrlAllowHosts\n const ttlMs = ctx.behavior?.fetchUrlCacheTtlMs\n const cacheEnabled = typeof ttlMs === 'number' && ttlMs > 0\n const key = cacheKey(parsed.toString(), cap, allowHosts)\n if (cacheEnabled && isHostAllowed(parsed.hostname, allowHosts)) {\n const hit = cacheGet(key)\n if (hit !== null)\n return hit\n }\n\n try {\n const res = await fetchWithSsrfGuard(\n parsed,\n {\n 'user-agent': 'Mozilla/5.0 (compatible; zidane/1.0)',\n 'accept': 'text/html,text/plain,application/json;q=0.9,*/*;q=0.5',\n 'accept-encoding': 'identity', // no gzip — we don't decompress\n },\n ctx.signal,\n bufferCap,\n {},\n allowHosts,\n )\n const body = res.body\n const isHtml = res.contentType.includes('html') || /^\\s*<!doctype html|^\\s*<html/i.test(body)\n const text = isHtml ? stripHtmlBlock(body) : body\n const truncated = text.length > cap\n // Two distinct truncation reasons: the rendered text overflowing `cap`,\n // and the raw transfer being cut at the buffer ceiling (`res.truncated`)\n // — the latter can leave the rendered text under `cap` (HTML overhead\n // stripped away), so it needs its own marker or the model would treat\n // a partial body as complete.\n const out = truncated\n ? `${text.slice(0, cap)}\\n\\n[truncated at ${cap} bytes; full length ${text.length}]`\n : res.truncated\n ? `${text}\\n\\n[response truncated: body exceeded the ${bufferCap}-byte transfer buffer]`\n : text\n // `res.url` reflects the FINAL hop after redirects; flag a cross-host\n // landing so the model knows the content came from a different origin\n // than it asked for (open-redirect awareness). Same-host path changes\n // and www add/strip are treated as the same site and not flagged.\n const finalHost = sameSiteHost(res.url)\n const requestedHost = sameSiteHost(parsed.toString())\n const redirectNote = finalHost && requestedHost && finalHost !== requestedHost\n ? `Note: ${parsed.hostname} redirected to a different host (${new URL(res.url).hostname}).\\n`\n : ''\n const header = `URL: ${res.url}\\nStatus: ${res.status}\\nContent-Type: ${res.contentType || '(none)'}\\n${redirectNote}\\n`\n const formatted = header + out\n // Cache only successful, complete text responses — never errors /\n // redirects / transfer-truncated bodies (a retry may get the rest).\n if (cacheEnabled && res.status >= 200 && res.status < 300 && res.truncated !== true)\n cacheSet(key, formatted, ttlMs)\n return formatted\n }\n catch (err) {\n return `fetch_url error: ${errorMessage(err)}`\n }\n },\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAqBA,MAAM,iBAAyC;CAC7C,KAAK;CACL,MAAM;CACN,MAAM;CACN,MAAM;CACN,MAAM;CACN,MAAM;CACN,IAAI;CACJ,QAAQ;CACR,OAAO;CACP,OAAO;CACP,OAAO;CACP,IAAI;CACJ,OAAO;CACP,QAAQ;CACR,MAAM;CACN,OAAO;CACP,MAAM;CACN,OAAO;CACP,OAAO;CACP,KAAK;CACL,OAAO;CACP,QAAQ;CACR,OAAO;AACT;;;;;;;AAQA,SAAS,kBAAkB,IAAoB;CAC7C,IAAI,CAAC,OAAO,SAAS,EAAE,KAAK,KAAK,KAAK,KAAK,SACzC,OAAO;CACT,IAAI;EACF,OAAO,OAAO,cAAc,EAAE;CAChC,QACM;EACJ,OAAO;CACT;AACF;;;;;;;;;;AAWA,SAAgB,mBAAmB,MAAsB;CACvD,IAAI,IAAI,KACL,QAAQ,sBAAsB,GAAG,MAAM,kBAAkB,OAAO,SAAS,GAAG,EAAE,CAAC,CAAC,CAAC,CACjF,QAAQ,cAAc,GAAG,MAAM,kBAAkB,OAAO,SAAS,GAAG,EAAE,CAAC,CAAC;CAC3E,IAAI,EAAE,QAAQ,yBAAyB,MAAM,SAAS;EACpD,MAAM,MAAM,KAAK,YAAY;EAE7B,IAAI,QAAQ,OACV,OAAO;EAET,OADoB,eAAe,QACb;CACxB,CAAC;CACD,OAAO,EAAE,QAAQ,WAAW,GAAG;AACjC;;;;;;;AAQA,SAAgB,UAAU,MAAsB;CAC9C,OAAO,KACJ,QAAQ,+BAA+B,GAAG,CAAC,CAC3C,QAAQ,6BAA6B,GAAG,CAAC,CACzC,QAAQ,mCAAmC,GAAG,CAAC,CAC/C,QAAQ,oBAAoB,GAAG,CAAC,CAChC,QAAQ,sCAAsC,IAAI,CAAC,CACnD,QAAQ,YAAY,GAAG;AAC5B;;;;;;AAOA,SAAgB,gBAAgB,MAAsB;CACpD,OAAO,mBAAmB,UAAU,IAAI,CAAC,CAAC,CACvC,QAAQ,QAAQ,GAAG,CAAC,CACpB,KAAK;AACV;;;;;;AAOA,SAAgB,eAAe,MAAsB;CACnD,OAAO,mBAAmB,UAAU,IAAI,CAAC,CAAC,CACvC,MAAM,IAAI,CAAC,CACX,KAAI,SAAQ,KAAK,QAAQ,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAChD,QAAO,SAAQ,KAAK,SAAS,CAAC,CAAC,CAC/B,KAAK,IAAI;AACd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AChFA,MAAM,oBAAoB,MAAM;AAChC,MAAM,iBAAiB,OAAO;AAC9B,MAAM,kBAAkB;AACxB,MAAM,gBAAgB;;;;;;;AAQtB,SAAS,cAAc,IAAqB;CAC1C,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,CAAC,IAAI,MAAM;CACtC,IAAI,MAAM,WAAW,KAAK,MAAM,MAAK,MAAK,CAAC,OAAO,UAAU,CAAC,KAAK,IAAI,KAAK,IAAI,GAAG,GAChF,OAAO;CACT,MAAM,CAAC,GAAG,KAAK;CACf,IAAI,MAAM,GACR,OAAO;CACT,IAAI,MAAM,IACR,OAAO;CACT,IAAI,MAAM,KACR,OAAO;CACT,IAAI,MAAM,OAAO,MAAM,KACrB,OAAO;CACT,IAAI,MAAM,OAAO,KAAK,MAAM,KAAK,IAC/B,OAAO;CACT,IAAI,MAAM,OAAO,MAAM,KACrB,OAAO;CACT,IAAI,MAAM,OAAO,MAAM,KAAK,MAAM,OAAO,GACvC,OAAO;CACT,IAAI,MAAM,QAAQ,MAAM,MAAM,MAAM,KAClC,OAAO;CACT,IAAI,KAAK,KACP,OAAO;CACT,OAAO;AACT;AAEA,SAAS,cAAc,IAAqB;CAC1C,MAAM,QAAQ,GAAG,YAAY;CAI7B,MAAM,iBAAiB,MAAM,MAAM,oBAAoB;CACvD,IAAI,gBACF,OAAO,cAAc,eAAe,EAAG;CACzC,MAAM,cAAc,MAAM,MAAM,0CAA0C;CAC1E,IAAI,aAAa;EACf,MAAM,KAAK,OAAO,SAAS,YAAY,IAAK,EAAE;EAC9C,MAAM,KAAK,OAAO,SAAS,YAAY,IAAK,EAAE;EAE9C,OAAO,cAAc,GADJ,MAAM,IAAK,IAAK,GAAG,KAAK,IAAK,GAAI,MAAM,IAAK,IAAK,GAAG,KAAK,KACjD;CAC3B;CACA,IAAI,UAAU,QAAQ,UAAU,OAC9B,OAAO;CAET,IAAI,uBAAuB,KAAK,KAAK,GACnC,OAAO;CAET,IAAI,sBAAsB,KAAK,KAAK,GAClC,OAAO;CAET,IAAI,MAAM,WAAW,IAAI,GACvB,OAAO;CACT,OAAO;AACT;AAEA,SAAgB,iBAAiB,IAAqB;CACpD,MAAM,SAAS,KAAK,EAAE;CACtB,IAAI,WAAW,GACb,OAAO,cAAc,EAAE;CACzB,IAAI,WAAW,GACb,OAAO,cAAc,EAAE;CACzB,OAAO;AACT;;;;;AAMA,SAAS,cAAc,MAAsB;CAC3C,IAAI,IAAI,KAAK,KAAK,CAAC,CAAC,YAAY;CAChC,IAAI,EAAE,WAAW,GAAG,KAAK,EAAE,SAAS,GAAG,GACrC,IAAI,EAAE,MAAM,GAAG,EAAE;CACnB,IAAI,EAAE,SAAS,GAAG,GAChB,IAAI,EAAE,MAAM,GAAG,EAAE;CACnB,IAAI,EAAE,WAAW,GAAG,GAClB,IAAI,EAAE,MAAM,CAAC;CACf,OAAO;AACT;;;;;;;AAQA,SAAS,aAAa,KAA4B;CAChD,IAAI;EACF,OAAO,IAAI,IAAI,GAAG,CAAC,CAAC,SAAS,YAAY,CAAC,CAAC,QAAQ,UAAU,EAAE;CACjE,QACM;EACJ,OAAO;CACT;AACF;;;;;;;;AASA,SAAgB,cAAc,UAAkB,YAAoD;CAClG,IAAI,CAAC,cAAc,WAAW,WAAW,GACvC,OAAO;CACT,MAAM,OAAO,cAAc,QAAQ;CACnC,KAAK,MAAM,SAAS,YAAY;EAC9B,MAAM,UAAU,cAAc,KAAK;EACnC,IAAI,QAAQ,WAAW,GACrB;EACF,IAAI,SAAS,WAAW,KAAK,SAAS,IAAI,SAAS,GACjD,OAAO;CACX;CACA,OAAO;AACT;;;;;;;;AASA,eAAe,gBAAgB,UAAkB,YAA6E;CAI5H,IAAI,CAAC,cAAc,UAAU,UAAU,GACrC,MAAM,IAAI,MAAM,YAAY,SAAS,2CAA2C;CAIlF,MAAM,OAAO,SAAS,WAAW,GAAG,KAAK,SAAS,SAAS,GAAG,IAC1D,SAAS,MAAM,GAAG,EAAE,IACpB;CACJ,MAAM,SAAS,KAAK,IAAI;CACxB,IAAI,WAAW,KAAK,WAAW,GAAG;EAChC,IAAI,iBAAiB,IAAI,GACvB,MAAM,IAAI,MAAM,YAAY,KAAK,uBAAuB;EAC1D,OAAO;GAAE,SAAS;GAAM;EAAO;CACjC;CACA,MAAM,UAAU,MAAMA,SAAI,OAAO,MAAM;EAAE,KAAK;EAAM,UAAU;CAAK,CAAC;CACpE,IAAI,QAAQ,WAAW,GACrB,MAAM,IAAI,MAAM,sCAAsC,MAAM;CAC9D,KAAK,MAAM,EAAE,aAAa,SACxB,IAAI,iBAAiB,OAAO,GAC1B,MAAM,IAAI,MAAM,YAAY,KAAK,+BAA+B,SAAS;CAE7E,MAAM,QAAQ,QAAQ;CACtB,MAAM,MAAM,MAAM,WAAW,IAAI,IAAI;CACrC,OAAO;EAAE,SAAS,MAAM;EAAS,QAAQ;CAAI;AAC/C;AA0BA,SAAS,gBAAgB,KAAkB;CACzC,IAAI,CAAC,IAAI,MACP,OAAO,IAAI;CACb,MAAM,cAAc,IAAI,aAAa,WAAW,QAAQ;CACxD,OAAO,IAAI,SAAS,cAAc,IAAI,WAAW,GAAG,IAAI,SAAS,GAAG,IAAI;AAC1E;AAEA,SAAgB,cACd,KACA,UACA,QACA,SACA,QACA,gBACyB;CACzB,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,UAAU,IAAI,aAAa;EASjC,MAAM,UAAoD;GACxD,MAAM;GACN,MAVW,IAAI,OACb,OAAO,SAAS,IAAI,MAAM,EAAE,IAC3B,UAAU,MAAM;GASnB,MAAM,GAAG,IAAI,WAAW,IAAI;GAC5B,QAAQ;GACR;GACA,SAAS;IAAE,GAAG;IAAS,MAAM,gBAAgB,GAAG;GAAE;GAClD,GAAI,UAAU,EAAE,YAAY,IAAI,SAAS,IAAI,CAAC;EAChD;EAEA,MAAMC,YAAU,UAAUC,YAAeC;EACzC,MAAM,SAAmB,CAAC;EAC1B,IAAI,WAAW;EAOf,IAAI,UAAU;EAKd,IAAI;EACJ,IAAI;EAEJ,MAAM,iBAAuB;GAC3B,IAAI;IAAE,KAAK,QAAQ;GAAE,QACf,CAA8B;EACtC;EAEA,MAAM,UAAU,YAA8B;GAC5C,IAAI,SACF;GACF,UAAU;GACV,IAAI,OACF,aAAa,KAAK;GACpB,OAAO,oBAAoB,SAAS,aAAa;GACjD,QAAQ;EACV;EAEA,SAAS,gBAAsB;GAC7B,aAAa,uBAAO,IAAI,MAAM,iBAAiB,CAAC,CAAC;GACjD,SAAS;EACX;EAMA,IAAI,OAAO,SAAS;GAClB,uBAAO,IAAI,MAAM,iBAAiB,CAAC;GACnC;EACF;EAEA,QAAQ,iBAAiB;GACvB,aAAa,uBAAO,IAAI,MAAM,2BAA2B,gBAAgB,GAAG,CAAC,CAAC;GAC9E,SAAS;EACX,GAAG,eAAe;EAElB,OAAO,iBAAiB,SAAS,eAAe,EAAE,MAAM,KAAK,CAAC;EAE9D,MAAMF,UAAQ,UAAU,QAAQ;GAC9B,MAAM,SAAS,IAAI,cAAc;GACjC,MAAM,aAAa,IAAI;GACvB,MAAM,eAAe,WAAW,mBAAmB,GAAA,CAAI,SAAS,CAAC,CAAC,YAAY;GAC9E,MAAM,WAAW,OAAO,WAAW,aAAa,WAAW,WAAW,WAAW;GAGjF,IAAI,UAAU,OAAO,SAAS,OAAO,UAAU;IAC7C,IAAI,OAAO;IACX,aAAa,QAAQ;KAAE;KAAQ,KAAK,IAAI,SAAS;KAAG;KAAa,MAAM;KAAI;IAAS,CAAC,CAAC;IACtF;GACF;GAEA,IAAI,gBAAgB;GACpB,IAAI,GAAG,SAAS,UAAkB;IAChC,IAAI,YAAY,gBAAgB;KAI9B,gBAAgB;KAChB,SAAS;KACT;IACF;IAIA,MAAM,OAAO,iBAAiB;IAC9B,IAAI,MAAM,SAAS,MAAM;KACvB,OAAO,KAAK,MAAM,SAAS,GAAG,IAAI,CAAC;KACnC,YAAY;KACZ,gBAAgB;KAChB,SAAS;KACT;IACF;IACA,YAAY,MAAM;IAClB,OAAO,KAAK,KAAK;GACnB,CAAC;GACD,IAAI,GAAG,aAAa;IAClB,MAAM,OAAO,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,OAAO;IACnD,aAAa,QAAQ;KAAE;KAAQ,KAAK,IAAI,SAAS;KAAG;KAAa;KAAM,UAAU;KAAM,WAAW;IAAc,CAAC,CAAC;GACpH,CAAC;GACD,IAAI,GAAG,eAAe;IAIpB,IAAI,CAAC,SAAS;KACZ,MAAM,OAAO,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,OAAO;KACnD,aAAa,QAAQ;MAAE;MAAQ,KAAK,IAAI,SAAS;MAAG;MAAa;MAAM,UAAU;MAAM,WAAW;KAAc,CAAC,CAAC;IACpH;GACF,CAAC;GACD,IAAI,GAAG,UAAS,QAAO,aAAa,OAAO,GAAG,CAAC,CAAC;EAClD,CAAC;EAED,IAAI,GAAG,UAAU,QAAe,aAAa,OAAO,GAAG,CAAC,CAAC;EACzD,IAAI,IAAI;CACV,CAAC;AACH;AAkBA,eAAsB,mBACpB,UACA,SACA,QACA,gBACA,OAAsB,CAAC,GACvB,YACyB;CAGzB,MAAM,WAAW,KAAK,cAAc,aAAqB,gBAAgB,UAAU,UAAU;CAC7F,MAAM,cAAc,KAAK,eAAe;CACxC,IAAI,UAAU;CACd,KAAK,IAAI,MAAM,GAAG,OAAO,eAAe,OAAO;EAC7C,MAAM,EAAE,SAAS,WAAW,MAAM,SAAS,QAAQ,QAAQ;EAC3D,MAAM,MAAM,MAAM,YAAY,SAAS,SAAS,QAAQ,SAAS,QAAQ,cAAc;EACvF,IAAI,CAAC,IAAI,UACP,OAAO;EACT,IAAI,QAAQ,eACV,MAAM,IAAI,MAAM,wBAAwB,cAAc,EAAE;EAC1D,IAAI;EACJ,IAAI;GACF,OAAO,IAAI,IAAI,IAAI,UAAU,OAAO;EACtC,QACM;GACJ,MAAM,IAAI,MAAM,4BAA4B,IAAI,UAAU;EAC5D;EACA,IAAI,KAAK,aAAa,WAAW,KAAK,aAAa,UACjD,MAAM,IAAI,MAAM,2CAA2C,KAAK,UAAU;EAC5E,UAAU;CACZ;CACA,MAAM,IAAI,MAAM,mCAAmC;AACrD;;;;;;;;;;;;;;AAeA,MAAM,0BAA0B;AAChC,MAAM,wBAAwB,KAAK,OAAO;AAS1C,MAAM,6BAAa,IAAI,IAA6B;AACpD,IAAI,kBAAkB;AAEtB,SAAS,SAAS,KAAa,KAAa,YAAwC;CAKlF,OAAO,GAAG,IAAI,QAAQ,IAAI,SADV,cAAc,CAAC,EAAA,CAAG,IAAI,aAAa,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,GAC1B;AACzC;AAEA,SAAS,SAAS,KAA4B;CAC5C,MAAM,QAAQ,WAAW,IAAI,GAAG;CAChC,IAAI,CAAC,OACH,OAAO;CACT,IAAI,KAAK,IAAI,KAAK,MAAM,WAAW;EACjC,WAAW,OAAO,GAAG;EACrB,mBAAmB,MAAM;EACzB,OAAO;CACT;CAEA,WAAW,OAAO,GAAG;CACrB,WAAW,IAAI,KAAK,KAAK;CACzB,OAAO,MAAM;AACf;AAEA,SAAS,SAAS,KAAa,QAAgB,OAAqB;CAClE,MAAM,QAAQ,OAAO,WAAW,QAAQ,OAAO;CAG/C,IAAI,QAAQ,uBACV;CAEF,MAAM,OAAO,WAAW,IAAI,GAAG;CAC/B,IAAI,MAAM;EACR,WAAW,OAAO,GAAG;EACrB,mBAAmB,KAAK;CAC1B;CACA,WAAW,IAAI,KAAK;EAAE;EAAQ,WAAW,KAAK,IAAI,IAAI;EAAO;CAAM,CAAC;CACpE,mBAAmB;CAEnB,OAAO,WAAW,OAAO,2BAA2B,kBAAkB,uBAAuB;EAC3F,MAAM,SAAS,WAAW,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;EACxC,IAAI,WAAW,KAAA,GACb;EACF,MAAM,SAAS,WAAW,IAAI,MAAM;EACpC,WAAW,OAAO,MAAM;EACxB,mBAAmB,OAAO;CAC5B;AACF;;AAGA,SAAgB,qBAA2B;CACzC,WAAW,MAAM;CACjB,kBAAkB;AACpB;;;;;;AAOA,MAAa,sBAAsB;CACjC,KAAK;CACL,KAAK;CACL,KAAK;CACL,YAAY,WAAW;CACvB,aAAa;AACf;AAEA,MAAa,WAAoB;CAC/B,mBAAmB;CACnB,MAAM;EACJ,MAAM;EACN,aAAa;EACb,aAAa;GACX,MAAM;GACN,YAAY;IACV,KAAK;KACH,MAAM;KACN,aAAa;IACf;IACA,WAAW;KACT,MAAM;KACN,aAAa,+DAA+D,kBAAkB,cAAc,eAAe;IAC7H;GACF;GACA,UAAU,CAAC,KAAK;EAClB;CACF;CACA,MAAM,QAAQ,EAAE,KAAK,aAAa,KAAkB;EAClD,MAAM,MAAM,OAAO,QAAQ,WAAW,IAAI,KAAK,IAAI;EACnD,IAAI,CAAC,KACH,OAAO;EACT,IAAI;EACJ,IAAI;GACF,SAAS,IAAI,IAAI,GAAG;EACtB,QACM;GACJ,OAAO,iCAAiC;EAC1C;EACA,IAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UACrD,OAAO,uDAAuD,OAAO,SAAS;EAEhF,MAAM,MAAM,KAAK,IACf,OAAO,cAAc,YAAY,YAAY,IAAI,KAAK,MAAM,SAAS,IAAI,mBACzE,cACF;EAKA,MAAM,YAAY,KAAK,IAAI,MAAM,GAAG,iBAAiB,CAAC;EAOtD,MAAM,aAAa,IAAI,UAAU;EACjC,MAAM,QAAQ,IAAI,UAAU;EAC5B,MAAM,eAAe,OAAO,UAAU,YAAY,QAAQ;EAC1D,MAAM,MAAM,SAAS,OAAO,SAAS,GAAG,KAAK,UAAU;EACvD,IAAI,gBAAgB,cAAc,OAAO,UAAU,UAAU,GAAG;GAC9D,MAAM,MAAM,SAAS,GAAG;GACxB,IAAI,QAAQ,MACV,OAAO;EACX;EAEA,IAAI;GACF,MAAM,MAAM,MAAM,mBAChB,QACA;IACE,cAAc;IACd,UAAU;IACV,mBAAmB;GACrB,GACA,IAAI,QACJ,WACA,CAAC,GACD,UACF;GACA,MAAM,OAAO,IAAI;GAEjB,MAAM,OADS,IAAI,YAAY,SAAS,MAAM,KAAK,gCAAgC,KAAK,IAAI,IACtE,eAAe,IAAI,IAAI;GAO7C,MAAM,MANY,KAAK,SAAS,MAO5B,GAAG,KAAK,MAAM,GAAG,GAAG,EAAE,oBAAoB,IAAI,sBAAsB,KAAK,OAAO,KAChF,IAAI,YACF,GAAG,KAAK,6CAA6C,UAAU,0BAC/D;GAKN,MAAM,YAAY,aAAa,IAAI,GAAG;GACtC,MAAM,gBAAgB,aAAa,OAAO,SAAS,CAAC;GACpD,MAAM,eAAe,aAAa,iBAAiB,cAAc,gBAC7D,SAAS,OAAO,SAAS,mCAAmC,IAAI,IAAI,IAAI,GAAG,CAAC,CAAC,SAAS,QACtF;GAEJ,MAAM,YAAY,QADK,IAAI,IAAI,YAAY,IAAI,OAAO,kBAAkB,IAAI,eAAe,SAAS,IAAI,aAAa,MAC1F;GAG3B,IAAI,gBAAgB,IAAI,UAAU,OAAO,IAAI,SAAS,OAAO,IAAI,cAAc,MAC7E,SAAS,KAAK,WAAW,KAAK;GAChC,OAAO;EACT,SACO,KAAK;GACV,OAAO,oBAAoB,aAAa,GAAG;EAC7C;CACF;AACF"}
|
|
@@ -16,6 +16,13 @@ function fmtTokens(n) {
|
|
|
16
16
|
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 2 : 1)}k`;
|
|
17
17
|
return `${(n / 1e6).toFixed(2)}M`;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* USD cost, dollar-prefixed — 2 decimals, 4 below a cent so tiny spends
|
|
21
|
+
* stay visible (a haiku worker's $0.0007 must not render as $0.00).
|
|
22
|
+
*/
|
|
23
|
+
function fmtCost(cost) {
|
|
24
|
+
return `$${cost.toFixed(cost < .01 ? 4 : 2)}`;
|
|
25
|
+
}
|
|
19
26
|
/** Compact relative-time formatter — "just now / 5m / 3h / 2d". */
|
|
20
27
|
function ageString(ts, now = Date.now()) {
|
|
21
28
|
const m = Math.floor((now - ts) / 6e4);
|
|
@@ -131,6 +138,6 @@ function compactPath(path, maxWidth, home) {
|
|
|
131
138
|
return display;
|
|
132
139
|
}
|
|
133
140
|
//#endregion
|
|
134
|
-
export {
|
|
141
|
+
export { formatDuration as a, previewLine as c, fmtTokens as i, shortId as l, compactPath as n, formatTaskStatus as o, fmtCost as r, formatTaskSummary as s, ageString as t };
|
|
135
142
|
|
|
136
|
-
//# sourceMappingURL=format-
|
|
143
|
+
//# sourceMappingURL=format-BF745pLL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-BF745pLL.js","names":[],"sources":["../src/chat/format.ts"],"sourcesContent":["/**\n * Resolve the user's home directory from env (no static os-module import),\n * so this module stays node-free and importable from a browser-context\n * renderer (Electron's renderer, Vite) via `zidane/chat/pure`. Reads the\n * platform env vars Node/Electron populate; returns '' when unavailable\n * (renderer), in which case `compactPath` falls back to the verbatim path.\n */\nfunction resolveHome(): string {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env\n return env?.HOME ?? env?.USERPROFILE ?? ''\n}\n\n/** Compact token formatter — 12_415 → \"12.4k\", 1_234_567 → \"1.23M\". */\nexport function fmtTokens(n: number): string {\n if (n < 1000)\n return String(n)\n if (n < 1_000_000)\n return `${(n / 1000).toFixed(n < 10_000 ? 2 : 1)}k`\n return `${(n / 1_000_000).toFixed(2)}M`\n}\n\n/**\n * USD cost, dollar-prefixed — 2 decimals, 4 below a cent so tiny spends\n * stay visible (a haiku worker's $0.0007 must not render as $0.00).\n */\nexport function fmtCost(cost: number): string {\n const digits = cost.toFixed(cost < 0.01 ? 4 : 2)\n return `$${digits}`\n}\n\n/** Compact relative-time formatter — \"just now / 5m / 3h / 2d\". */\nexport function ageString(ts: number, now: number = Date.now()): string {\n const m = Math.floor((now - ts) / 60_000)\n if (m < 1)\n return 'just now'\n if (m < 60)\n return `${m}m ago`\n const h = Math.floor(m / 60)\n if (h < 24)\n return `${h}h ago`\n return `${Math.floor(h / 24)}d ago`\n}\n\n/** Six-char short form of a session id for headers and lists. */\nexport function shortId(id: string): string {\n return id.replace(/-/g, '').slice(0, 6)\n}\n\n/**\n * Single-line preview of a multi-line string, capped at `max` chars and\n * ellipsis-terminated when truncated.\n *\n * Whitespace runs (newlines, tabs, multiple spaces) collapse into one\n * space so the rendered output stays on a single visual row no matter\n * how the input was shaped. Used by every transcript \"preview\" surface\n * (spawn-start task, `tool: shell (background): <command>`,\n * `<task-notification>` summary line, etc.) — without the whitespace\n * collapse, a 60-char `slice` on a string with an inline `\\n\\n` paints\n * the second paragraph below the first, producing the visible\n * \"preview text spills onto multiple lines\" bug (and, downstream,\n * misaligned spawn markers when the wrapped lines collide with\n * other events).\n *\n * Reserves one slot for the `…` so the displayed width is exactly\n * `max` when truncation kicks in.\n */\nexport function previewLine(s: string, max: number): string {\n const single = s.replace(/\\s+/g, ' ').trim()\n if (single.length <= max)\n return single\n return `${single.slice(0, max - 1)}…`\n}\n\n/**\n * Compact human-readable duration formatter shared by background-task\n * surfaces (the `<task-notification>` summary, the TUI banner, the\n * `shell_kill` tool result, etc.).\n *\n * Format ladder:\n * - `< 1s` → `\"Nms\"`\n * - `< 10s` → `\"N.Ns\"` (one decimal)\n * - `< 1m` → `\"Ns\"` (whole seconds)\n * - `< 1h` → `\"NmNs\"` / `\"Nm\"` when seconds round to 0\n * - `≥ 1h` → `\"NhNm\"` / `\"Nh\"` when minutes round to 0\n *\n * Single source of truth so a 60s task renders the same across the\n * model-facing XML summary and the user-facing banner. Earlier\n * separate formatters disagreed (XML said `\"60.0s\"`, banner said `\"1m\"`)\n * which was confusing to the user reading both side by side.\n */\nexport function formatDuration(ms: number): string {\n if (ms < 0)\n ms = 0\n if (ms < 1000)\n return `${ms}ms`\n if (ms < 60_000)\n return `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)}s`\n const minutes = Math.floor(ms / 60_000)\n const seconds = Math.floor((ms % 60_000) / 1000)\n if (minutes < 60)\n return seconds > 0 ? `${minutes}m${seconds}s` : `${minutes}m`\n const hours = Math.floor(minutes / 60)\n const remMinutes = minutes % 60\n return remMinutes > 0 ? `${hours}h${remMinutes}m` : `${hours}h`\n}\n\n/**\n * Status label for a terminated background task — `\"exited <code>\"`\n * for natural exits, `\"killed\"` (with the signal name when known)\n * for our-issued SIGTERMs.\n *\n * Pulled out as its own function so the `<task-notification>` XML\n * summary, the TUI banner header, the `shell_kill` tool result, and\n * future surfaces all read the same string.\n */\nexport function formatTaskStatus(info: {\n status: 'exited' | 'killed'\n exitCode: number\n signal?: NodeJS.Signals\n}): string {\n return info.status === 'killed'\n ? `killed${info.signal ? ` (${info.signal})` : ''}`\n : `exited ${info.exitCode}`\n}\n\n/**\n * One-line summary of a terminated background task — the shape used by\n * the `<task-notification>` XML's `<summary>` tag AND the TUI banner's\n * `event.text` fallback string. Three dot-separated segments:\n *\n * `<command preview · status · duration>`\n *\n * Centralizes the format so live + replay + wire all agree, and so a\n * future cosmetic tweak (separator glyph, segment ordering) lands in\n * exactly one place.\n */\nexport function formatTaskSummary(info: {\n command: string\n status: 'exited' | 'killed'\n exitCode: number\n signal?: NodeJS.Signals\n durationMs: number\n}, maxCommandChars = 80): string {\n return `${previewLine(info.command, maxCommandChars)} · ${formatTaskStatus(info)} · ${formatDuration(info.durationMs)}`\n}\n\n/**\n * Compact an absolute path for display: replace the user's `$HOME`\n * prefix with `~` (so `/Users/yael/Code/zidane` → `~/Code/zidane`),\n * and optionally left-truncate with an ellipsis when the result\n * still exceeds `maxWidth` (so the path's *tail* — the part the user\n * recognizes — stays visible: `…/zidane` rather than `/Users/yaeluil…`).\n *\n * `maxWidth` is the maximum *display width* in cells. Omit to skip\n * truncation. Paths outside `$HOME` are returned verbatim modulo\n * truncation. The ellipsis (`…`) counts as one cell.\n *\n * `home` overrides `os.homedir()` for tests; production callers leave\n * it undefined and pay the cheap one-syscall lookup per call.\n */\nexport function compactPath(path: string, maxWidth?: number, home?: string): string {\n const h = home ?? resolveHome()\n let display = path\n if (h) {\n if (path === h)\n display = '~'\n else if (path.startsWith(`${h}/`))\n display = `~${path.slice(h.length)}`\n }\n if (maxWidth !== undefined && maxWidth > 1 && display.length > maxWidth) {\n // Left-truncate: keep the rightmost `maxWidth - 1` chars and\n // prepend `…`. The right side of a path is the distinctive\n // bit (repo name, leaf folder); the left side is the boilerplate\n // (home, common parents) we're happy to drop.\n return `…${display.slice(display.length - maxWidth + 1)}`\n }\n return display\n}\n"],"mappings":";;;;;;;;AAOA,SAAS,cAAsB;CAC7B,MAAM,MAAO,WAA0E,SAAS;CAChG,OAAO,KAAK,QAAQ,KAAK,eAAe;AAC1C;;AAGA,SAAgB,UAAU,GAAmB;CAC3C,IAAI,IAAI,KACN,OAAO,OAAO,CAAC;CACjB,IAAI,IAAI,KACN,OAAO,IAAI,IAAI,IAAA,CAAM,QAAQ,IAAI,MAAS,IAAI,CAAC,EAAE;CACnD,OAAO,IAAI,IAAI,IAAA,CAAW,QAAQ,CAAC,EAAE;AACvC;;;;;AAMA,SAAgB,QAAQ,MAAsB;CAE5C,OAAO,IADQ,KAAK,QAAQ,OAAO,MAAO,IAAI,CAC9B;AAClB;;AAGA,SAAgB,UAAU,IAAY,MAAc,KAAK,IAAI,GAAW;CACtE,MAAM,IAAI,KAAK,OAAO,MAAM,MAAM,GAAM;CACxC,IAAI,IAAI,GACN,OAAO;CACT,IAAI,IAAI,IACN,OAAO,GAAG,EAAE;CACd,MAAM,IAAI,KAAK,MAAM,IAAI,EAAE;CAC3B,IAAI,IAAI,IACN,OAAO,GAAG,EAAE;CACd,OAAO,GAAG,KAAK,MAAM,IAAI,EAAE,EAAE;AAC/B;;AAGA,SAAgB,QAAQ,IAAoB;CAC1C,OAAO,GAAG,QAAQ,MAAM,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;AACxC;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,YAAY,GAAW,KAAqB;CAC1D,MAAM,SAAS,EAAE,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK;CAC3C,IAAI,OAAO,UAAU,KACnB,OAAO;CACT,OAAO,GAAG,OAAO,MAAM,GAAG,MAAM,CAAC,EAAE;AACrC;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,eAAe,IAAoB;CACjD,IAAI,KAAK,GACP,KAAK;CACP,IAAI,KAAK,KACP,OAAO,GAAG,GAAG;CACf,IAAI,KAAK,KACP,OAAO,IAAI,KAAK,IAAA,CAAM,QAAQ,KAAK,MAAS,IAAI,CAAC,EAAE;CACrD,MAAM,UAAU,KAAK,MAAM,KAAK,GAAM;CACtC,MAAM,UAAU,KAAK,MAAO,KAAK,MAAU,GAAI;CAC/C,IAAI,UAAU,IACZ,OAAO,UAAU,IAAI,GAAG,QAAQ,GAAG,QAAQ,KAAK,GAAG,QAAQ;CAC7D,MAAM,QAAQ,KAAK,MAAM,UAAU,EAAE;CACrC,MAAM,aAAa,UAAU;CAC7B,OAAO,aAAa,IAAI,GAAG,MAAM,GAAG,WAAW,KAAK,GAAG,MAAM;AAC/D;;;;;;;;;;AAWA,SAAgB,iBAAiB,MAItB;CACT,OAAO,KAAK,WAAW,WACnB,SAAS,KAAK,SAAS,KAAK,KAAK,OAAO,KAAK,OAC7C,UAAU,KAAK;AACrB;;;;;;;;;;;;AAaA,SAAgB,kBAAkB,MAM/B,kBAAkB,IAAY;CAC/B,OAAO,GAAG,YAAY,KAAK,SAAS,eAAe,EAAE,KAAK,iBAAiB,IAAI,EAAE,KAAK,eAAe,KAAK,UAAU;AACtH;;;;;;;;;;;;;;;AAgBA,SAAgB,YAAY,MAAc,UAAmB,MAAuB;CAClF,MAAM,IAAI,QAAQ,YAAY;CAC9B,IAAI,UAAU;CACd,IAAI;MACE,SAAS,GACX,UAAU;OACP,IAAI,KAAK,WAAW,GAAG,EAAE,EAAE,GAC9B,UAAU,IAAI,KAAK,MAAM,EAAE,MAAM;CAAA;CAErC,IAAI,aAAa,KAAA,KAAa,WAAW,KAAK,QAAQ,SAAS,UAK7D,OAAO,IAAI,QAAQ,MAAM,QAAQ,SAAS,WAAW,CAAC;CAExD,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-data-WL_yGVL6.js","names":[],"sources":["../src/chat/commands.ts","../src/zod.ts","../src/chat/generate-title.ts","../src/chat/generate-data.ts"],"sourcesContent":["/**\n * `/` command model — an extension-contributed workflow invoked by typing\n * `/<name> [args]` as the whole prompt. A command is an ordered list of\n * handler **steps**; a bare `run` is sugar for a single step. Steps share a\n * mutable `data` bag, emit local output blocks (rendered in the transcript,\n * never sent to the model), and can drive the agent via `ctx.runAgent` so a\n * workflow can interact with the LLM mid-flight.\n *\n * This module is renderer-free and node-free: the types are the authoring\n * surface extensions compile against, and `executeCommand` / the parse\n * helper are pure so they're unit-testable without a TUI. The host supplies\n * the side-effecting capabilities on the step context: `emit` (render +\n * persist local output), `runAgent` (interactive sub-run), `generateData`\n * (silent schema-pinned JSON), and the optional `session` (read-only view)\n * and `interaction` (confirm / forms / plan approval) surfaces.\n */\n\nimport type { Logger } from '../logger'\nimport type { SessionRun } from '../session'\nimport type { JsonValue, SessionTurn } from '../types'\nimport type { InferSchemaOutput, SchemaInput, StandardSchemaLike } from '../zod'\nimport type { AnswerValue, PlanPayload, PlanResponse, QuestionPayload } from './interactions'\nimport type { StreamEvent } from './types'\n\n/**\n * A block of local output a step renders into the chat.\n *\n * Plain blocks (`text` only) render as full markdown, both live and on\n * reload — use them for the output the user should read in full (e.g. a\n * final commit message). Blocks with `detail` render as a collapsed\n * `command-detail` row instead (dim `▸ label` + line-capped body with\n * \"… N more lines\") — use them for bulky supporting output (diffs, logs,\n * listings) that shouldn't drown the transcript.\n */\nexport interface CommandOutputBlock {\n /**\n * The block body — full markdown, the detail body, or (for `custom`\n * blocks) the MANDATORY fallback markdown rendered wherever the\n * renderer isn't available: extension uninstalled or disabled,\n * headless host, GUI, renderer threw. The fallback is what persists\n * as the block's output, so pre-custom hosts render it natively.\n */\n text: string\n /**\n * Render as a collapsed detail row (dim `▸ label` + line-capped body)\n * instead of full markdown. `syntax: 'diff'` additionally colors\n * `+N` / `−N` count tokens (added green / deleted red) in both label\n * and body — for diff-stat-style output.\n */\n detail?: { label?: string, syntax?: 'diff' }\n /**\n * Render through an extension-registered custom renderer\n * (`ctx.tui.registerRenderer`). `renderer` must be the NAMESPACED id\n * (`<extension>/<id>`) — `ctx.appendNote` auto-prefixes bare ids for\n * its own extension; command `emit` requires the full form. `data`\n * must be JSON-safe (it persists in the session). Takes precedence\n * over `detail` when both are set.\n */\n custom?: { renderer: string, data?: JsonValue }\n}\n\n/**\n * User-interaction surface for commands — the same prompt-slot primitives\n * the `ask_user` / `present_plan` TOOLS render, but WITHOUT the tools: a\n * command enqueues directly into the host's interaction queue. No tool\n * calls, no turns, no model involvement — the answer routes straight back\n * to the awaiting step. Every method rejects on cancel/esc, which the\n * executor classifies as a quiet abort.\n */\nexport interface CommandInteraction {\n /**\n * One-tap yes/no — sugar over a single `confirm`-type question:\n *\n * if (await ctx.interaction.confirm('Push to origin/main?')) …\n */\n confirm: (prompt: string, opts?: {\n /** Optional context shown above the question (Markdown). */\n intro?: string\n /** Label for the affirmative option. Default `'yes'`. */\n affirmLabel?: string\n /** Label for the negative option. Default `'no'`. */\n denyLabel?: string\n }) => Promise<boolean>\n /**\n * Show a form (text / textarea / select / confirm questions — identical\n * UI to the ask_user tool) and resolve with the answers keyed by\n * `Question.id` (`string` for text/select, `boolean` for confirm).\n */\n askUser: (form: QuestionPayload) => Promise<Readonly<Record<string, AnswerValue>>>\n /**\n * Present a plan for the full approval ceremony (approve / reject /\n * revise + optional comment — identical UI to present_plan). For\n * lightweight gating prefer {@link confirm}.\n */\n presentPlan: (plan: PlanPayload) => Promise<PlanResponse>\n}\n\n/**\n * Read-only view over the session a command runs inside — live getters, so\n * turns appended mid-workflow (e.g. by a `runAgent` sub-run) are visible on\n * the next read. Deliberately projection-only: no mutators, so commands\n * can't bypass the display-only `command`-turn contract or the wire guard\n * (`isWireTurn`). Persisting output goes through `emit`; driving the\n * conversation goes through `runAgent`.\n */\nexport interface CommandSessionView {\n readonly id: string\n /** Full persisted history, including display-only `command` turns. */\n readonly turns: readonly SessionTurn[]\n /** Run records (status, usage, cost, `origin`, …), newest last. */\n readonly runs: readonly SessionRun[]\n readonly metadata: Readonly<Record<string, unknown>>\n}\n\n/** Options for `ctx.generateData` — a silent, schema-pinned model call. */\nexport interface CommandGenerateDataOptions {\n /** Instruction describing the value to produce. */\n prompt: string\n /**\n * Shape of the result — a zod v4 schema (return type inferred, no casts)\n * or a plain JSON Schema object (name the type via the generic).\n */\n schema: SchemaInput\n /** Optional system prompt override. */\n system?: string\n /**\n * Fold a compacted transcript of the recent conversation into the call as\n * context. Default false — most workflow values derive from data the\n * command already gathered itself.\n */\n includeConversation?: boolean\n}\n\n/** Result of driving the agent from within a step via `ctx.runAgent`. */\nexport interface CommandAgentResult {\n /** The assistant's final text for this sub-run. */\n text: string\n}\n\n/** Capabilities + inputs handed to every step. */\nexport interface CommandStepContext {\n /** Raw text after `/<name>` (trimmed of the leading space). */\n readonly args: string\n /** `args` split on whitespace, empties removed. */\n readonly argv: readonly string[]\n /** Mutable bag threaded across steps; a step's returned `data` is merged in. */\n readonly data: Record<string, unknown>\n /** Render + persist a local output block. */\n readonly emit: (block: CommandOutputBlock) => void\n /**\n * Drive one agent run to completion and return its result. The prompt is\n * injected as a real user turn (model-visible, unchanged on the wire) and\n * the response streams into the transcript. Serialized with the host's run\n * slot, so a workflow can interleave deterministic steps with LLM\n * interaction. Any output `emit`ted before this call is flushed first,\n * preserving transcript order.\n *\n * `opts.display` replaces the full-prompt echo in the transcript with a\n * short line (the full prompt still renders beneath it as a collapsed\n * detail row, and still reaches the model verbatim) — pass it when the\n * prompt embeds bulky material like a diff.\n */\n readonly runAgent: (prompt: string, opts?: { readonly display?: string }) => Promise<CommandAgentResult>\n /**\n * Generate schema-conforming JSON with ONE silent model call — no session\n * turns, no transcript output, nothing added to the conversation (the\n * workflow presents results itself via `emit`). Reach for this when a\n * step needs a VALUE (a commit message, a classification, extracted\n * fields); reach for `runAgent` when it needs an interactive sub-run\n * with tools and streaming.\n */\n readonly generateData: {\n /** zod v4 schema → the result type is inferred from the schema. */\n <S extends StandardSchemaLike>(opts: Omit<CommandGenerateDataOptions, 'schema'> & { schema: S }): Promise<InferSchemaOutput<S>>\n /** Plain JSON Schema → name the result type explicitly. */\n <T = unknown>(opts: CommandGenerateDataOptions): Promise<T>\n }\n /** Logger with a `command=<name>` binding, routed to the host's log funnel. */\n readonly logger: Logger\n /** Aborts when the user presses esc during execution. */\n readonly signal: AbortSignal\n /** Working directory the command runs against. */\n readonly cwd: string\n /**\n * The surrounding session, when the host runs commands inside one (the\n * TUI always does). ABSENT in session-less contexts — a command that\n * reads it must guard (`if (!ctx.session) …`) or degrade gracefully.\n */\n readonly session?: CommandSessionView\n /**\n * User interaction (confirm / forms / plan approval), when the host has\n * an interactive surface (the TUI always does). ABSENT on UI-less hosts\n * — guard (`if (!ctx.interaction) …`) and degrade gracefully.\n */\n readonly interaction?: CommandInteraction\n}\n\n/** Optional per-step return — `data` is merged into the shared bag for later steps. */\nexport interface CommandStepResult {\n data?: Record<string, unknown>\n}\n\nexport type CommandStepFn = (\n ctx: CommandStepContext,\n) => void | CommandStepResult | Promise<void | CommandStepResult>\n\nexport interface CommandStep {\n run: CommandStepFn\n}\n\n/** One argument suggestion returned by {@link Command.completeArgs}. */\nexport interface CommandArgSuggestion {\n /** Text inserted after `/<name> ` on accept. */\n value: string\n /** Display label. Defaults to `value`. */\n label?: string\n description?: string\n}\n\n/** An extension-contributed `/` command. Provide EITHER `run` (one step) or `steps`. */\nexport interface Command {\n /** Invoked as `/<name>`. Lowercase, no whitespace. */\n name: string\n /** One-line summary shown in the completion popover. */\n description: string\n /** Single-step sugar. */\n run?: CommandStepFn\n /** Explicit ordered pipeline. Takes precedence over `run` when both are set. */\n steps?: readonly CommandStep[]\n /**\n * Argument completion — called while the user types `/<name> <prefix>▌`\n * with everything after the command name (may itself contain spaces).\n * Return suggestions to show in the `/` popover; return `[]`/`undefined`\n * for none. Sync or async.\n */\n completeArgs?: (argsPrefix: string) => readonly CommandArgSuggestion[] | undefined | Promise<readonly CommandArgSuggestion[] | undefined>\n}\n\n/**\n * Persisted form of output blocks inside a display-only `command` turn:\n * plain blocks → `text` content; `detail` blocks → `tool_result` content\n * with the label/syntax carried in namespaced metadata (rebuilt into the\n * collapsed rendering on reload). Shared by the command emit flush and\n * `ctx.appendNote` so both persist identically.\n */\nexport function commandBlocksToContent(blocks: readonly CommandOutputBlock[]): SessionTurn['content'] {\n return blocks.map((block) => {\n if (!block.detail && !block.custom)\n return { type: 'text' as const, text: block.text }\n // Custom blocks persist with the FALLBACK as the output — an older\n // zidane (or any host without the renderer) reads this turn as a\n // plain detail block and renders the fallback natively. The renderer\n // binding rides namespaced metadata, exactly like detail label/syntax.\n const commandMeta: Record<string, JsonValue> = block.custom\n ? { custom: { renderer: block.custom.renderer, ...(block.custom.data !== undefined ? { data: block.custom.data } : {}) } }\n : {\n ...(block.detail?.label ? { label: block.detail.label } : {}),\n ...(block.detail?.syntax ? { syntax: block.detail.syntax } : {}),\n }\n return {\n type: 'tool_result' as const,\n callId: `command_detail_${crypto.randomUUID()}`,\n output: block.text,\n ...(Object.keys(commandMeta).length > 0 ? { metadata: { command: commandMeta } } : {}),\n }\n })\n}\n\n/**\n * Map ONE command-output block to its LIVE transcript event — the\n * in-memory mirror of {@link commandBlocksToContent}'s persisted shape\n * (whose replay side lives in `eventsFromTurns`). One definition shared\n * by the command emit flush and the `ctx.appendNote` host impl so live\n * rendering can never drift from reloaded rendering — the exact drift\n * that shipped `custom` blocks rendering only after a reload.\n *\n * Precedence matches the persisted side: `custom` > `detail` > markdown.\n */\nexport function commandBlockToEvent(block: CommandOutputBlock): StreamEvent {\n if (block.custom) {\n return {\n kind: 'custom',\n text: block.text,\n custom: {\n renderer: block.custom.renderer,\n ...(block.custom.data !== undefined ? { data: block.custom.data } : {}),\n },\n }\n }\n if (block.detail) {\n return {\n kind: 'command-detail',\n text: block.text,\n ...(block.detail.label ? { label: block.detail.label } : {}),\n ...(block.detail.syntax ? { syntax: block.detail.syntax } : {}),\n }\n }\n return { kind: 'markdown', text: block.text, streaming: false }\n}\n\n/** Normalize a command to its ordered step list (`steps` wins over `run`). */\nexport function commandSteps(command: Command): readonly CommandStep[] {\n if (command.steps && command.steps.length > 0)\n return command.steps\n if (command.run)\n return [{ run: command.run }]\n return []\n}\n\n/** Parsed `/<name> [args]` invocation. */\nexport interface CommandInvocation {\n name: string\n /** Text after the name, trimmed. */\n args: string\n /** `args` split on whitespace. */\n argv: string[]\n}\n\n/**\n * Parse a prompt as a command invocation, but only when the command token is\n * the LEADING token of the whole prompt and names a known command. This is\n * what keeps mid-prompt `/skill` mentions routed to the skills provider:\n * `\"summarize /research\"` is not a command, `\"/research foo\"` is.\n *\n * `knownNames` is matched case-insensitively; the returned `name` is the\n * canonical (registered) casing.\n */\nexport function parseCommandInvocation(\n prompt: string,\n knownNames: readonly string[],\n): CommandInvocation | null {\n const body = prompt.trimStart()\n if (body[0] !== '/')\n return null\n // First token = the command name; the remainder (after the first run of\n // whitespace) is the args. Linear scan — no backtracking-prone regex.\n const afterSlash = body.slice(1)\n const wsIdx = afterSlash.search(/\\s/)\n const typed = (wsIdx === -1 ? afterSlash : afterSlash.slice(0, wsIdx)).toLowerCase()\n if (typed.length === 0)\n return null\n const canonical = knownNames.find(n => n.toLowerCase() === typed)\n if (canonical === undefined)\n return null\n const args = (wsIdx === -1 ? '' : afterSlash.slice(wsIdx + 1)).trim()\n return { name: canonical, args, argv: args.length > 0 ? args.split(/\\s+/) : [] }\n}\n\n/** Outcome of a command run. `error` is set when a step threw (pipeline stops). */\nexport interface CommandExecution {\n ok: boolean\n /** Number of steps that ran (including the one that threw). */\n ranSteps: number\n error?: unknown\n /** True when the run stopped because `ctx.signal` aborted. */\n aborted?: boolean\n}\n\n/**\n * Run a command's steps in order, threading `ctx.data`. Stops at the first\n * step that throws (error isolated + returned, not rethrown) or when the\n * signal aborts. Pure orchestration — all I/O lives in the host-supplied\n * `ctx` capabilities.\n */\nexport async function executeCommand(\n command: Command,\n ctx: CommandStepContext,\n): Promise<CommandExecution> {\n const steps = commandSteps(command)\n let ranSteps = 0\n for (const step of steps) {\n if (ctx.signal.aborted)\n return { ok: false, ranSteps, aborted: true }\n ranSteps++\n try {\n const result = await step.run(ctx)\n if (result && result.data)\n Object.assign(ctx.data, result.data)\n }\n catch (error) {\n // An esc mid-step usually surfaces as a thrown AbortError from whatever\n // I/O the handler had in flight. Classify by the signal so hosts can\n // stay quiet on user-initiated aborts instead of painting an error.\n if (ctx.signal.aborted)\n return { ok: false, ranSteps, aborted: true }\n return { ok: false, ranSteps, error }\n }\n }\n return { ok: true, ranSteps }\n}\n","/**\n * Zod v4 integration helpers.\n *\n * Zod is an OPTIONAL peer dependency, so this module never imports it at the\n * top level. Schema-accepting APIs (`ctx.generateData`, `generateStructuredData`)\n * take either a plain JSON Schema object or any Standard-Schema-carrying\n * value (https://standardschema.dev — zod v4 implements it). Zod schemas are\n * detected structurally by vendor and converted with a lazy\n * `import('zod')` — safe because whoever constructed the schema necessarily\n * has zod installed. Result types are inferred from the standard-schema\n * `types.output` slot, so callers get full typing without a single cast:\n *\n * const CommitMessage = z.object({ subject: z.string(), body: z.string().optional() })\n * const msg = await ctx.generateData({ prompt, schema: CommitMessage })\n * // ^? { subject: string, body?: string }\n */\n\n/**\n * Structural stand-in for a Standard Schema (zod v4 et al.) — enough surface\n * to detect the vendor and infer the output type WITHOUT importing zod's\n * types into our public API.\n */\nexport interface StandardSchemaLike<Output = unknown> {\n readonly '~standard': {\n readonly version: number\n readonly vendor: string\n /** Type-level only (undefined at runtime) — carries the inferred output type. */\n readonly types?: { readonly output: Output } | undefined\n }\n}\n\n/** Output type carried by a {@link StandardSchemaLike}; `unknown` for plain JSON Schema. */\nexport type InferSchemaOutput<S> = S extends StandardSchemaLike<infer O> ? O : unknown\n\n/** A schema-accepting input: plain JSON Schema, or a Standard Schema (e.g. zod v4). */\nexport type SchemaInput = Record<string, unknown> | StandardSchemaLike\n\nexport function isStandardSchema(value: unknown): value is StandardSchemaLike {\n if (typeof value !== 'object' || value === null)\n return false\n const marker = (value as Record<string, unknown>)['~standard']\n return typeof marker === 'object' && marker !== null\n && typeof (marker as Record<string, unknown>).vendor === 'string'\n}\n\n/**\n * Resolve a {@link SchemaInput} to the plain JSON Schema object providers\n * expect. Zod schemas convert via the lazily-imported `z.toJSONSchema`\n * (v4 native); other Standard Schema vendors are rejected with a clear\n * error — conversion is vendor-specific and only zod is wired today.\n */\nexport async function resolveSchemaInput(schema: SchemaInput): Promise<Record<string, unknown>> {\n if (!isStandardSchema(schema))\n return schema\n const vendor = schema['~standard'].vendor\n if (vendor !== 'zod') {\n throw new Error(\n `unsupported schema vendor \"${vendor}\" — pass a zod v4 schema or a plain JSON Schema object`,\n )\n }\n // Lazy + guarded: zod is an optional peer, but a zod schema in hand means\n // the caller's process has zod — this import only fails on truly broken\n // installs, and then with an actionable message.\n let toJSONSchema: (schema: unknown) => Record<string, unknown>\n try {\n const zod = await import('zod')\n toJSONSchema = zod.z.toJSONSchema as typeof toJSONSchema\n }\n catch {\n throw new Error('a zod schema was passed but the \"zod\" package could not be imported — install zod >= 4')\n }\n return zodToJsonSchema(toJSONSchema(schema))\n}\n\n/**\n * Normalize a JSON Schema (e.g. from zod v4's z.toJSONSchema()) for use\n * as a ToolSpec.inputSchema.\n *\n * Strips the $schema key that some providers reject.\n */\nexport function zodToJsonSchema(jsonSchema: Record<string, unknown>): Record<string, unknown> {\n const { $schema, ...rest } = jsonSchema\n return rest\n}\n","/**\n * One-shot title generation for an existing session — feeds the last N\n * turns to the provider with a \"write me a title\" system prompt and\n * returns the cleaned-up result.\n *\n * Provider-agnostic: works with anything implementing the renderer-\n * shared `Provider` interface. No tools, no session mutation, no\n * caching — this is a cheap, isolated completion that should be safe to\n * fire even mid-session without touching the live agent loop.\n */\n\nimport type { Provider } from '../providers'\nimport type { SessionTurn } from '../types'\n\n/** Hard cap on the result length. Anything longer is truncated client-side. */\nconst TITLE_MAX_CHARS = 60\n\n/** Default turn count fed to the model. 10 covers most exchanges without bloat. */\nconst DEFAULT_MAX_TURNS = 10\n\n/** Tokens budgeted for the model's reply. 64 fits any sane title with headroom. */\nconst TITLE_RESPONSE_MAX_TOKENS = 64\n\n/** Per-turn body cap when assembling the prompt — keeps the request payload tight. */\nconst PROMPT_TURN_CHAR_MAX = 2000\n\nexport interface GenerateSessionTitleOptions {\n provider: Provider\n /** Model id used for the call. */\n model: string\n /** Conversation history to summarize. Empty → throws synchronously. */\n turns: readonly SessionTurn[]\n /** Cap on how many trailing turns to feed the model. Defaults to 10. */\n maxTurns?: number\n /** Optional cancellation signal — forwarded to the provider's stream. */\n signal?: AbortSignal\n}\n\n/**\n * Drive the provider's `stream()` once and return a concise title for\n * the conversation represented by `turns`. Throws when:\n * - `turns` contains no text-bearing content (nothing to summarize),\n * - the provider stream completes with no output text,\n * - `signal` is aborted (rethrown verbatim).\n *\n * Output is trimmed, single-line, with surrounding quotes / trailing\n * punctuation stripped, and truncated to `TITLE_MAX_CHARS`.\n */\nexport async function generateSessionTitle({\n provider,\n model,\n turns,\n maxTurns = DEFAULT_MAX_TURNS,\n signal,\n}: GenerateSessionTitleOptions): Promise<string> {\n const slice = turns.slice(-Math.max(1, maxTurns))\n const transcript = renderTurnsForPrompt(slice)\n if (!transcript)\n throw new Error('No text content in the recent turns to summarize.')\n\n const system = [\n 'You generate concise, descriptive titles for chat conversations.',\n 'Reply with ONLY the title — no quotes, no markdown, no trailing punctuation,',\n 'no preamble (do not say \"Title:\" or similar).',\n `Aim for 3 to 7 words and stay under ${TITLE_MAX_CHARS} characters.`,\n ].join(' ')\n\n const userPrompt = `Generate a title for this conversation:\\n\\n${transcript}`\n\n let text = ''\n const result = await provider.stream(\n {\n model,\n system,\n tools: [],\n messages: [provider.userMessage(userPrompt)],\n maxTokens: TITLE_RESPONSE_MAX_TOKENS,\n ...(signal ? { signal } : {}),\n },\n {\n onText: (delta) => { text += delta },\n },\n )\n // Prefer the streamed-text accumulator (driven by deltas) but fall\n // back to the provider's already-concatenated `result.text` for\n // adapters that batch on the final frame instead of emitting deltas.\n const raw = text.trim().length > 0 ? text : result.text\n return cleanTitle(raw)\n}\n\n/**\n * Decide whether a session is ready for one-shot automatic titling.\n *\n * Returns `true` iff the conversation has had its first exchange — at\n * least two turns, i.e. the opening user message plus the assistant's\n * reply (this is exactly the \"N turns\" count the sessions list shows) —\n * AND no explicit title has been set yet. An existing `metadata.title`\n * (a previous auto-generation, or a manual title from the session-details\n * \"generate title\" action) is never clobbered.\n *\n * Pure + total. The runtime layers its own \"is one already in flight?\"\n * and \"is a provider picked?\" guards on top of this; the predicate only\n * encodes the content-level rule so it can be pinned in tests without a\n * mock provider — same rationale as the exported {@link cleanTitle}.\n */\nexport function shouldAutoGenerateTitle(\n turns: readonly SessionTurn[],\n metadata?: Record<string, unknown>,\n): boolean {\n const existing = typeof metadata?.title === 'string' ? metadata.title.trim() : ''\n if (existing.length > 0)\n return false\n return turns.length >= 2\n}\n\n/**\n * Compact a turn list into a transcript-style prompt:\n *\n * user: …\n * assistant: …\n * user: …\n *\n * Tool calls and tool results are summarized to keep the request\n * payload small — the model needs the rough flow of the conversation,\n * not full JSON blobs. Per-turn text is clipped at `PROMPT_TURN_CHAR_MAX`\n * so a single huge code dump doesn't blow the budget.\n *\n * Returns an empty string when the slice has no text-bearing content\n * (e.g. a turn list that's only tool plumbing).\n *\n * Exported for reuse by other silent one-shot helpers (`generate-data`'s\n * optional conversation context) so every side-channel inference compacts\n * history the same way.\n */\nexport function renderTurnsForPrompt(turns: readonly SessionTurn[]): string {\n const lines: string[] = []\n for (const turn of turns) {\n const body = textBodyOf(turn)\n if (!body)\n continue\n const role = turn.role === 'assistant' ? 'assistant' : turn.role === 'user' ? 'user' : 'system'\n lines.push(`${role}: ${clip(body, PROMPT_TURN_CHAR_MAX)}`)\n }\n return lines.join('\\n\\n')\n}\n\nfunction textBodyOf(turn: SessionTurn): string {\n const parts: string[] = []\n for (const block of turn.content) {\n if (block.type === 'text' && block.text.trim())\n parts.push(block.text.trim())\n else if (block.type === 'tool_call')\n parts.push(`[tool: ${block.name}]`)\n else if (block.type === 'tool_result')\n parts.push(`[tool result]`)\n }\n return parts.join(' ')\n}\n\n/**\n * Normalize a model-generated title into the shape we want to persist:\n *\n * - Collapse internal whitespace + take the first line only (some\n * models emit \"Title: foo\\n\\nReason: …\" despite instructions).\n * - Strip surrounding quote characters (`\"foo\"`, `'foo'`, `` `foo` ``).\n * - Strip a leading `Title:` / `title -` prefix if the model added one.\n * - Strip trailing punctuation (`.`, `!`, `?`) — titles read cleaner\n * without it.\n * - Truncate to `TITLE_MAX_CHARS` with a trailing `…` when over.\n *\n * Exported as `cleanTitle` so tests can pin the normalization rules\n * without going through a mock provider.\n */\nexport function cleanTitle(raw: string): string {\n let s = raw.split('\\n')[0]?.trim() ?? ''\n // Strip a \"Title:\" / \"Title -\" preamble the model sometimes adds.\n s = s.replace(/^\\s*title\\s*[:\\-–—]\\s*/i, '')\n // Strip surrounding matched quotes.\n if (s.length >= 2) {\n const first = s.charAt(0)\n const last = s.charAt(s.length - 1)\n const matched = (first === '\"' && last === '\"')\n || (first === '\\'' && last === '\\'')\n || (first === '`' && last === '`')\n || (first === '“' && last === '”')\n if (matched)\n s = s.slice(1, -1).trim()\n }\n // Trim trailing terminal punctuation — titles don't take periods.\n s = s.replace(/[.!?]+$/, '').trim()\n if (s.length === 0)\n throw new Error('Model returned an empty title.')\n return s.length > TITLE_MAX_CHARS ? `${s.slice(0, TITLE_MAX_CHARS - 1)}…` : s\n}\n\nfunction clip(text: string, max: number): string {\n return text.length > max ? `${text.slice(0, max)}…` : text\n}\n","/**\n * Silent structured-output generation — one provider call that returns\n * schema-conforming JSON without touching the session or the transcript.\n *\n * Modeled on the two proven side-channel patterns in the codebase:\n * - `generate-title.ts`: a single `provider.stream()` with no session\n * writes and no transcript events (optionally seeded with a compacted\n * conversation transcript), and\n * - the loop's `__output__` schema enforcement: a synthetic tool whose\n * `inputSchema` IS the requested JSON Schema, with `toolChoice` pinned\n * so the model must answer with a conforming payload.\n *\n * Used by `/` commands via `ctx.generateData` to derive values from the\n * model (or from the conversation) mid-workflow without bloating the chat\n * history — e.g. `/commit` generating `{ subject, body }` from a diff.\n * For genuinely interactive sub-runs (real turns, streaming, tools), use\n * `ctx.runAgent` instead.\n */\n\nimport type { Provider, ToolSpec } from '../providers'\nimport type { SessionTurn } from '../types'\nimport type { InferSchemaOutput, SchemaInput, StandardSchemaLike } from '../zod'\nimport { resolveSchemaInput } from '../zod'\nimport { renderTurnsForPrompt } from './generate-title'\n\n/** Synthetic forced-choice tool name (never persisted — the call is side-channel only). */\nconst DATA_TOOL_NAME = '__data__'\n\n/** Default cap on conversation turns folded into the context block. */\nconst DEFAULT_CONTEXT_TURNS = 12\n\nexport interface GenerateStructuredDataOptions {\n provider: Provider\n model: string\n /** Instruction describing the value to produce. */\n prompt: string\n /**\n * Shape of the result — a plain JSON Schema object, or a zod v4 schema\n * (converted automatically; the return type is then inferred from it).\n * Becomes the forced tool's inputSchema.\n */\n schema: SchemaInput\n /** Optional system prompt. Defaults to a terse data-extraction persona. */\n system?: string\n /**\n * Conversation turns to fold in as compacted context (same clipping as\n * session auto-titling). Omit or pass empty for a context-free call.\n */\n turns?: readonly SessionTurn[]\n /** Cap on turns rendered into the context block. Default 12 (most recent). */\n maxContextTurns?: number\n signal?: AbortSignal\n}\n\n/**\n * Drive one schema-pinned provider call and return the conforming payload.\n * Throws when the provider returns no structured payload (and rethrows\n * aborts verbatim). The caller owns presentation — nothing is rendered or\n * persisted here.\n *\n * Pass a zod v4 schema and the return type is inferred from it; pass a\n * plain JSON Schema object and name the type via the generic.\n */\nexport async function generateStructuredData<S extends StandardSchemaLike>(\n options: GenerateStructuredDataOptions & { schema: S },\n): Promise<InferSchemaOutput<S>>\nexport async function generateStructuredData<T = unknown>(\n options: GenerateStructuredDataOptions,\n): Promise<T>\nexport async function generateStructuredData(\n options: GenerateStructuredDataOptions,\n): Promise<unknown> {\n const { provider, model, prompt, signal } = options\n const spec: ToolSpec = {\n name: DATA_TOOL_NAME,\n description: 'Return the result. The input of this call IS the answer — it must conform to the schema.',\n inputSchema: await resolveSchemaInput(options.schema),\n }\n\n const contextTurns = options.turns && options.turns.length > 0\n ? options.turns.slice(-(options.maxContextTurns ?? DEFAULT_CONTEXT_TURNS))\n : []\n const transcript = contextTurns.length > 0 ? renderTurnsForPrompt(contextTurns) : ''\n const userPrompt = transcript\n ? `<conversation>\\n${transcript}\\n</conversation>\\n\\n${prompt}`\n : prompt\n\n const result = await provider.stream(\n {\n model,\n system: options.system\n ?? `You produce structured data. Call the ${DATA_TOOL_NAME} tool exactly once with the requested value. No prose.`,\n tools: provider.formatTools([spec]),\n messages: [provider.userMessage(userPrompt)],\n toolChoice: { type: 'tool', name: DATA_TOOL_NAME },\n ...(signal ? { signal } : {}),\n },\n {\n onText: () => {},\n },\n )\n\n const payload = result.toolCalls.find(tc => tc.name === DATA_TOOL_NAME)?.input\n if (payload === undefined)\n throw new Error('generateStructuredData: the model returned no structured payload')\n return payload\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"generate-data-WL_yGVL6.js","names":[],"sources":["../src/chat/commands.ts","../src/zod.ts","../src/chat/generate-title.ts","../src/chat/generate-data.ts"],"sourcesContent":["/**\n * `/` command model — an extension-contributed workflow invoked by typing\n * `/<name> [args]` as the whole prompt. A command is an ordered list of\n * handler **steps**; a bare `run` is sugar for a single step. Steps share a\n * mutable `data` bag, emit local output blocks (rendered in the transcript,\n * never sent to the model), and can drive the agent via `ctx.runAgent` so a\n * workflow can interact with the LLM mid-flight.\n *\n * This module is renderer-free and node-free: the types are the authoring\n * surface extensions compile against, and `executeCommand` / the parse\n * helper are pure so they're unit-testable without a TUI. The host supplies\n * the side-effecting capabilities on the step context: `emit` (render +\n * persist local output), `runAgent` (interactive sub-run), `generateData`\n * (silent schema-pinned JSON), and the optional `session` (read-only view)\n * and `interaction` (confirm / forms / plan approval) surfaces.\n */\n\nimport type { Logger } from '../logger'\nimport type { SessionRun } from '../session'\nimport type { JsonValue, SessionTurn } from '../types'\nimport type { InferSchemaOutput, SchemaInput, StandardSchemaLike } from '../zod'\nimport type { AnswerValue, PlanPayload, PlanResponse, QuestionPayload } from './interactions'\nimport type { StreamEvent } from './types'\n\n/**\n * A block of local output a step renders into the chat.\n *\n * Plain blocks (`text` only) render as full markdown, both live and on\n * reload — use them for the output the user should read in full (e.g. a\n * final commit message). Blocks with `detail` render as a collapsed\n * `command-detail` row instead (dim `▸ label` + line-capped body with\n * \"… N more lines\") — use them for bulky supporting output (diffs, logs,\n * listings) that shouldn't drown the transcript.\n */\nexport interface CommandOutputBlock {\n /**\n * The block body — full markdown, the detail body, or (for `custom`\n * blocks) the MANDATORY fallback markdown rendered wherever the\n * renderer isn't available: extension uninstalled or disabled,\n * headless host, GUI, renderer threw. The fallback is what persists\n * as the block's output, so pre-custom hosts render it natively.\n */\n text: string\n /**\n * Render as a collapsed detail row (dim `▸ label` + line-capped body)\n * instead of full markdown. `syntax: 'diff'` additionally colors\n * `+N` / `−N` count tokens (added green / deleted red) in both label\n * and body — for diff-stat-style output.\n */\n detail?: { label?: string, syntax?: 'diff' }\n /**\n * Render through an extension-registered custom renderer\n * (`ctx.tui.registerRenderer`). `renderer` must be the NAMESPACED id\n * (`<extension>/<id>`) — `ctx.appendNote` auto-prefixes bare ids for\n * its own extension; command `emit` requires the full form. `data`\n * must be JSON-safe (it persists in the session). Takes precedence\n * over `detail` when both are set.\n */\n custom?: { renderer: string, data?: JsonValue }\n}\n\n/**\n * User-interaction surface for commands — the same prompt-slot primitives\n * the `ask_user` / `present_plan` TOOLS render, but WITHOUT the tools: a\n * command enqueues directly into the host's interaction queue. No tool\n * calls, no turns, no model involvement — the answer routes straight back\n * to the awaiting step. Every method rejects on cancel/esc, which the\n * executor classifies as a quiet abort.\n */\nexport interface CommandInteraction {\n /**\n * One-tap yes/no — sugar over a single `confirm`-type question:\n *\n * if (await ctx.interaction.confirm('Push to origin/main?')) …\n */\n confirm: (prompt: string, opts?: {\n /** Optional context shown above the question (Markdown). */\n intro?: string\n /** Label for the affirmative option. Default `'yes'`. */\n affirmLabel?: string\n /** Label for the negative option. Default `'no'`. */\n denyLabel?: string\n }) => Promise<boolean>\n /**\n * Show a form (text / textarea / select / confirm questions — identical\n * UI to the ask_user tool) and resolve with the answers keyed by\n * `Question.id` (`string` for text/select, `boolean` for confirm).\n */\n askUser: (form: QuestionPayload) => Promise<Readonly<Record<string, AnswerValue>>>\n /**\n * Present a plan for the full approval ceremony (approve / reject /\n * revise + optional comment — identical UI to present_plan). For\n * lightweight gating prefer {@link confirm}.\n */\n presentPlan: (plan: PlanPayload) => Promise<PlanResponse>\n}\n\n/**\n * Read-only view over the session a command runs inside — live getters, so\n * turns appended mid-workflow (e.g. by a `runAgent` sub-run) are visible on\n * the next read. Deliberately projection-only: no mutators, so commands\n * can't bypass the display-only `command`-turn contract or the wire guard\n * (`isWireTurn`). Persisting output goes through `emit`; driving the\n * conversation goes through `runAgent`.\n */\nexport interface CommandSessionView {\n readonly id: string\n /** Full persisted history, including display-only `command` turns. */\n readonly turns: readonly SessionTurn[]\n /** Run records (status, usage, cost, `origin`, …), newest last. */\n readonly runs: readonly SessionRun[]\n readonly metadata: Readonly<Record<string, unknown>>\n}\n\n/** Options for `ctx.generateData` — a silent, schema-pinned model call. */\nexport interface CommandGenerateDataOptions {\n /** Instruction describing the value to produce. */\n prompt: string\n /**\n * Shape of the result — a zod v4 schema (return type inferred, no casts)\n * or a plain JSON Schema object (name the type via the generic).\n */\n schema: SchemaInput\n /** Optional system prompt override. */\n system?: string\n /**\n * Fold a compacted transcript of the recent conversation into the call as\n * context. Default false — most workflow values derive from data the\n * command already gathered itself.\n */\n includeConversation?: boolean\n}\n\n/** Result of driving the agent from within a step via `ctx.runAgent`. */\nexport interface CommandAgentResult {\n /** The assistant's final text for this sub-run. */\n text: string\n}\n\n/** Capabilities + inputs handed to every step. */\nexport interface CommandStepContext {\n /** Raw text after `/<name>` (trimmed of the leading space). */\n readonly args: string\n /** `args` split on whitespace, empties removed. */\n readonly argv: readonly string[]\n /** Mutable bag threaded across steps; a step's returned `data` is merged in. */\n readonly data: Record<string, unknown>\n /** Render + persist a local output block. */\n readonly emit: (block: CommandOutputBlock) => void\n /**\n * Drive one agent run to completion and return its result. The prompt is\n * injected as a real user turn (model-visible, unchanged on the wire) and\n * the response streams into the transcript. Serialized with the host's run\n * slot, so a workflow can interleave deterministic steps with LLM\n * interaction. Any output `emit`ted before this call is flushed first,\n * preserving transcript order.\n *\n * `opts.display` replaces the full-prompt echo in the transcript with a\n * short line (the full prompt still renders beneath it as a collapsed\n * detail row, and still reaches the model verbatim) — pass it when the\n * prompt embeds bulky material like a diff.\n */\n readonly runAgent: (prompt: string, opts?: { readonly display?: string }) => Promise<CommandAgentResult>\n /**\n * Generate schema-conforming JSON with ONE silent model call — no session\n * turns, no transcript output, nothing added to the conversation (the\n * workflow presents results itself via `emit`). Reach for this when a\n * step needs a VALUE (a commit message, a classification, extracted\n * fields); reach for `runAgent` when it needs an interactive sub-run\n * with tools and streaming.\n */\n readonly generateData: {\n /** zod v4 schema → the result type is inferred from the schema. */\n <S extends StandardSchemaLike>(opts: Omit<CommandGenerateDataOptions, 'schema'> & { schema: S }): Promise<InferSchemaOutput<S>>\n /** Plain JSON Schema → name the result type explicitly. */\n <T = unknown>(opts: CommandGenerateDataOptions): Promise<T>\n }\n /** Logger with a `command=<name>` binding, routed to the host's log funnel. */\n readonly logger: Logger\n /** Aborts when the user presses esc during execution. */\n readonly signal: AbortSignal\n /** Working directory the command runs against. */\n readonly cwd: string\n /**\n * The surrounding session, when the host runs commands inside one (the\n * TUI always does). ABSENT in session-less contexts — a command that\n * reads it must guard (`if (!ctx.session) …`) or degrade gracefully.\n */\n readonly session?: CommandSessionView\n /**\n * User interaction (confirm / forms / plan approval), when the host has\n * an interactive surface (the TUI always does). ABSENT on UI-less hosts\n * — guard (`if (!ctx.interaction) …`) and degrade gracefully.\n */\n readonly interaction?: CommandInteraction\n}\n\n/** Optional per-step return — `data` is merged into the shared bag for later steps. */\nexport interface CommandStepResult {\n data?: Record<string, unknown>\n}\n\nexport type CommandStepFn = (\n ctx: CommandStepContext,\n) => void | CommandStepResult | Promise<void | CommandStepResult>\n\nexport interface CommandStep {\n run: CommandStepFn\n}\n\n/** One argument suggestion returned by {@link Command.completeArgs}. */\nexport interface CommandArgSuggestion {\n /** Text inserted after `/<name> ` on accept. */\n value: string\n /** Display label. Defaults to `value`. */\n label?: string\n description?: string\n}\n\n/** An extension-contributed `/` command. Provide EITHER `run` (one step) or `steps`. */\nexport interface Command {\n /** Invoked as `/<name>`. Lowercase, no whitespace. */\n name: string\n /** One-line summary shown in the completion popover. */\n description: string\n /** Single-step sugar. */\n run?: CommandStepFn\n /** Explicit ordered pipeline. Takes precedence over `run` when both are set. */\n steps?: readonly CommandStep[]\n /**\n * Argument completion — called while the user types `/<name> <prefix>▌`\n * with everything after the command name (may itself contain spaces).\n * Return suggestions to show in the `/` popover; return `[]`/`undefined`\n * for none. Sync or async.\n */\n completeArgs?: (argsPrefix: string) => readonly CommandArgSuggestion[] | undefined | Promise<readonly CommandArgSuggestion[] | undefined>\n}\n\n/**\n * Persisted form of output blocks inside a display-only `command` turn:\n * plain blocks → `text` content; `detail` blocks → `tool_result` content\n * with the label/syntax carried in namespaced metadata (rebuilt into the\n * collapsed rendering on reload). Shared by the command emit flush and\n * `ctx.appendNote` so both persist identically.\n */\nexport function commandBlocksToContent(blocks: readonly CommandOutputBlock[]): SessionTurn['content'] {\n return blocks.map((block) => {\n if (!block.detail && !block.custom)\n return { type: 'text' as const, text: block.text }\n // Custom blocks persist with the FALLBACK as the output — an older\n // zidane (or any host without the renderer) reads this turn as a\n // plain detail block and renders the fallback natively. The renderer\n // binding rides namespaced metadata, exactly like detail label/syntax.\n const commandMeta: Record<string, JsonValue> = block.custom\n ? { custom: { renderer: block.custom.renderer, ...(block.custom.data !== undefined ? { data: block.custom.data } : {}) } }\n : {\n ...(block.detail?.label ? { label: block.detail.label } : {}),\n ...(block.detail?.syntax ? { syntax: block.detail.syntax } : {}),\n }\n return {\n type: 'tool_result' as const,\n callId: `command_detail_${crypto.randomUUID()}`,\n output: block.text,\n ...(Object.keys(commandMeta).length > 0 ? { metadata: { command: commandMeta } } : {}),\n }\n })\n}\n\n/**\n * Map ONE command-output block to its LIVE transcript event — the\n * in-memory mirror of {@link commandBlocksToContent}'s persisted shape\n * (whose replay side lives in `eventsFromTurns`). One definition shared\n * by the command emit flush and the `ctx.appendNote` host impl so live\n * rendering can never drift from reloaded rendering — the exact drift\n * that shipped `custom` blocks rendering only after a reload.\n *\n * Precedence matches the persisted side: `custom` > `detail` > markdown.\n */\nexport function commandBlockToEvent(block: CommandOutputBlock): StreamEvent {\n if (block.custom) {\n return {\n kind: 'custom',\n text: block.text,\n custom: {\n renderer: block.custom.renderer,\n ...(block.custom.data !== undefined ? { data: block.custom.data } : {}),\n },\n }\n }\n if (block.detail) {\n return {\n kind: 'command-detail',\n text: block.text,\n ...(block.detail.label ? { label: block.detail.label } : {}),\n ...(block.detail.syntax ? { syntax: block.detail.syntax } : {}),\n }\n }\n return { kind: 'markdown', text: block.text, streaming: false }\n}\n\n/** Normalize a command to its ordered step list (`steps` wins over `run`). */\nexport function commandSteps(command: Command): readonly CommandStep[] {\n if (command.steps && command.steps.length > 0)\n return command.steps\n if (command.run)\n return [{ run: command.run }]\n return []\n}\n\n/** Parsed `/<name> [args]` invocation. */\nexport interface CommandInvocation {\n name: string\n /** Text after the name, trimmed. */\n args: string\n /** `args` split on whitespace. */\n argv: string[]\n}\n\n/**\n * Parse a prompt as a command invocation, but only when the command token is\n * the LEADING token of the whole prompt and names a known command. This is\n * what keeps mid-prompt `/skill` mentions routed to the skills provider:\n * `\"summarize /research\"` is not a command, `\"/research foo\"` is.\n *\n * `knownNames` is matched case-insensitively; the returned `name` is the\n * canonical (registered) casing.\n */\nexport function parseCommandInvocation(\n prompt: string,\n knownNames: readonly string[],\n): CommandInvocation | null {\n const body = prompt.trimStart()\n if (body[0] !== '/')\n return null\n // First token = the command name; the remainder (after the first run of\n // whitespace) is the args. Linear scan — no backtracking-prone regex.\n const afterSlash = body.slice(1)\n const wsIdx = afterSlash.search(/\\s/)\n const typed = (wsIdx === -1 ? afterSlash : afterSlash.slice(0, wsIdx)).toLowerCase()\n if (typed.length === 0)\n return null\n const canonical = knownNames.find(n => n.toLowerCase() === typed)\n if (canonical === undefined)\n return null\n const args = (wsIdx === -1 ? '' : afterSlash.slice(wsIdx + 1)).trim()\n return { name: canonical, args, argv: args.length > 0 ? args.split(/\\s+/) : [] }\n}\n\n/** Outcome of a command run. `error` is set when a step threw (pipeline stops). */\nexport interface CommandExecution {\n ok: boolean\n /** Number of steps that ran (including the one that threw). */\n ranSteps: number\n error?: unknown\n /** True when the run stopped because `ctx.signal` aborted. */\n aborted?: boolean\n}\n\n/**\n * Run a command's steps in order, threading `ctx.data`. Stops at the first\n * step that throws (error isolated + returned, not rethrown) or when the\n * signal aborts. Pure orchestration — all I/O lives in the host-supplied\n * `ctx` capabilities.\n */\nexport async function executeCommand(\n command: Command,\n ctx: CommandStepContext,\n): Promise<CommandExecution> {\n const steps = commandSteps(command)\n let ranSteps = 0\n for (const step of steps) {\n if (ctx.signal.aborted)\n return { ok: false, ranSteps, aborted: true }\n ranSteps++\n try {\n const result = await step.run(ctx)\n if (result && result.data)\n Object.assign(ctx.data, result.data)\n }\n catch (error) {\n // An esc mid-step usually surfaces as a thrown AbortError from whatever\n // I/O the handler had in flight. Classify by the signal so hosts can\n // stay quiet on user-initiated aborts instead of painting an error.\n if (ctx.signal.aborted)\n return { ok: false, ranSteps, aborted: true }\n return { ok: false, ranSteps, error }\n }\n }\n return { ok: true, ranSteps }\n}\n","/**\n * Zod v4 integration helpers.\n *\n * Zod is an OPTIONAL peer dependency, so this module never imports it at the\n * top level. Schema-accepting APIs (`ctx.generateData`, `generateStructuredData`)\n * take either a plain JSON Schema object or any Standard-Schema-carrying\n * value (https://standardschema.dev — zod v4 implements it). Zod schemas are\n * detected structurally by vendor and converted with a lazy\n * `import('zod')` — safe because whoever constructed the schema necessarily\n * has zod installed. Result types are inferred from the standard-schema\n * `types.output` slot, so callers get full typing without a single cast:\n *\n * const CommitMessage = z.object({ subject: z.string(), body: z.string().optional() })\n * const msg = await ctx.generateData({ prompt, schema: CommitMessage })\n * // ^? { subject: string, body?: string }\n */\n\n/**\n * Structural stand-in for a Standard Schema (zod v4 et al.) — enough surface\n * to detect the vendor and infer the output type WITHOUT importing zod's\n * types into our public API.\n */\nexport interface StandardSchemaLike<Output = unknown> {\n readonly '~standard': {\n readonly version: number\n readonly vendor: string\n /** Type-level only (undefined at runtime) — carries the inferred output type. */\n readonly types?: { readonly output: Output } | undefined\n }\n}\n\n/** Output type carried by a {@link StandardSchemaLike}; `unknown` for plain JSON Schema. */\nexport type InferSchemaOutput<S> = S extends StandardSchemaLike<infer O> ? O : unknown\n\n/** A schema-accepting input: plain JSON Schema, or a Standard Schema (e.g. zod v4). */\nexport type SchemaInput = Record<string, unknown> | StandardSchemaLike\n\nexport function isStandardSchema(value: unknown): value is StandardSchemaLike {\n if (typeof value !== 'object' || value === null)\n return false\n const marker = (value as Record<string, unknown>)['~standard']\n return typeof marker === 'object' && marker !== null\n && typeof (marker as Record<string, unknown>).vendor === 'string'\n}\n\n/**\n * Resolve a {@link SchemaInput} to the plain JSON Schema object providers\n * expect. Zod schemas convert via the lazily-imported `z.toJSONSchema`\n * (v4 native); other Standard Schema vendors are rejected with a clear\n * error — conversion is vendor-specific and only zod is wired today.\n */\nexport async function resolveSchemaInput(schema: SchemaInput): Promise<Record<string, unknown>> {\n if (!isStandardSchema(schema))\n return schema\n const vendor = schema['~standard'].vendor\n if (vendor !== 'zod') {\n throw new Error(\n `unsupported schema vendor \"${vendor}\" — pass a zod v4 schema or a plain JSON Schema object`,\n )\n }\n // Lazy + guarded: zod is an optional peer, but a zod schema in hand means\n // the caller's process has zod — this import only fails on truly broken\n // installs, and then with an actionable message.\n let toJSONSchema: (schema: unknown) => Record<string, unknown>\n try {\n const zod = await import('zod')\n toJSONSchema = zod.z.toJSONSchema as typeof toJSONSchema\n }\n catch {\n throw new Error('a zod schema was passed but the \"zod\" package could not be imported — install zod >= 4')\n }\n return zodToJsonSchema(toJSONSchema(schema))\n}\n\n/**\n * Normalize a JSON Schema (e.g. from zod v4's z.toJSONSchema()) for use\n * as a ToolSpec.inputSchema.\n *\n * Strips the $schema key that some providers reject.\n */\nexport function zodToJsonSchema(jsonSchema: Record<string, unknown>): Record<string, unknown> {\n const { $schema, ...rest } = jsonSchema\n return rest\n}\n","/**\n * One-shot title generation for an existing session — feeds the last N\n * turns to the provider with a \"write me a title\" system prompt and\n * returns the cleaned-up result.\n *\n * Provider-agnostic: works with anything implementing the renderer-\n * shared `Provider` interface. No tools, no session mutation, no\n * caching — this is a cheap, isolated completion that should be safe to\n * fire even mid-session without touching the live agent loop.\n */\n\nimport type { Provider } from '../providers'\nimport type { SessionTurn } from '../types'\n\n/** Hard cap on the result length. Anything longer is truncated client-side. */\nconst TITLE_MAX_CHARS = 60\n\n/** Default turn count fed to the model. 10 covers most exchanges without bloat. */\nconst DEFAULT_MAX_TURNS = 10\n\n/** Tokens budgeted for the model's reply. 64 fits any sane title with headroom. */\nconst TITLE_RESPONSE_MAX_TOKENS = 64\n\n/** Per-turn body cap when assembling the prompt — keeps the request payload tight. */\nconst PROMPT_TURN_CHAR_MAX = 2000\n\nexport interface GenerateSessionTitleOptions {\n provider: Provider\n /** Model id used for the call. */\n model: string\n /** Conversation history to summarize. Empty → throws synchronously. */\n turns: readonly SessionTurn[]\n /** Cap on how many trailing turns to feed the model. Defaults to 10. */\n maxTurns?: number\n /** Optional cancellation signal — forwarded to the provider's stream. */\n signal?: AbortSignal\n}\n\n/**\n * Drive the provider's `stream()` once and return a concise title for\n * the conversation represented by `turns`. Throws when:\n * - `turns` contains no text-bearing content (nothing to summarize),\n * - the provider stream completes with no output text,\n * - `signal` is aborted (rethrown verbatim).\n *\n * Output is trimmed, single-line, with surrounding quotes / trailing\n * punctuation stripped, and truncated to `TITLE_MAX_CHARS`.\n */\nexport async function generateSessionTitle({\n provider,\n model,\n turns,\n maxTurns = DEFAULT_MAX_TURNS,\n signal,\n}: GenerateSessionTitleOptions): Promise<string> {\n const slice = turns.slice(-Math.max(1, maxTurns))\n const transcript = renderTurnsForPrompt(slice)\n if (!transcript)\n throw new Error('No text content in the recent turns to summarize.')\n\n const system = [\n 'You generate concise, descriptive titles for chat conversations.',\n 'Reply with ONLY the title — no quotes, no markdown, no trailing punctuation,',\n 'no preamble (do not say \"Title:\" or similar).',\n `Aim for 3 to 7 words and stay under ${TITLE_MAX_CHARS} characters.`,\n ].join(' ')\n\n const userPrompt = `Generate a title for this conversation:\\n\\n${transcript}`\n\n let text = ''\n const result = await provider.stream(\n {\n model,\n system,\n tools: [],\n messages: [provider.userMessage(userPrompt)],\n maxTokens: TITLE_RESPONSE_MAX_TOKENS,\n ...(signal ? { signal } : {}),\n },\n {\n onText: (delta) => { text += delta },\n },\n )\n // Prefer the streamed-text accumulator (driven by deltas) but fall\n // back to the provider's already-concatenated `result.text` for\n // adapters that batch on the final frame instead of emitting deltas.\n const raw = text.trim().length > 0 ? text : result.text\n return cleanTitle(raw)\n}\n\n/**\n * Decide whether a session is ready for one-shot automatic titling.\n *\n * Returns `true` iff the conversation has had its first exchange — at\n * least two turns, i.e. the opening user message plus the assistant's\n * reply (this is exactly the \"N turns\" count the sessions list shows) —\n * AND no explicit title has been set yet. An existing `metadata.title`\n * (a previous auto-generation, or a manual title from the session-details\n * \"generate title\" action) is never clobbered.\n *\n * Pure + total. The runtime layers its own \"is one already in flight?\"\n * and \"is a provider picked?\" guards on top of this; the predicate only\n * encodes the content-level rule so it can be pinned in tests without a\n * mock provider — same rationale as the exported {@link cleanTitle}.\n */\nexport function shouldAutoGenerateTitle(\n turns: readonly SessionTurn[],\n metadata?: Record<string, unknown>,\n): boolean {\n const existing = typeof metadata?.title === 'string' ? metadata.title.trim() : ''\n if (existing.length > 0)\n return false\n return turns.length >= 2\n}\n\n/**\n * Compact a turn list into a transcript-style prompt:\n *\n * user: …\n * assistant: …\n * user: …\n *\n * Tool calls and tool results are summarized to keep the request\n * payload small — the model needs the rough flow of the conversation,\n * not full JSON blobs. Per-turn text is clipped at `PROMPT_TURN_CHAR_MAX`\n * so a single huge code dump doesn't blow the budget.\n *\n * Returns an empty string when the slice has no text-bearing content\n * (e.g. a turn list that's only tool plumbing).\n *\n * Exported for reuse by other silent one-shot helpers (`generate-data`'s\n * optional conversation context) so every side-channel inference compacts\n * history the same way.\n */\nexport function renderTurnsForPrompt(turns: readonly SessionTurn[]): string {\n const lines: string[] = []\n for (const turn of turns) {\n const body = textBodyOf(turn)\n if (!body)\n continue\n const role = turn.role === 'assistant' ? 'assistant' : turn.role === 'user' ? 'user' : 'system'\n lines.push(`${role}: ${clip(body, PROMPT_TURN_CHAR_MAX)}`)\n }\n return lines.join('\\n\\n')\n}\n\nfunction textBodyOf(turn: SessionTurn): string {\n const parts: string[] = []\n for (const block of turn.content) {\n if (block.type === 'text' && block.text.trim())\n parts.push(block.text.trim())\n else if (block.type === 'tool_call')\n parts.push(`[tool: ${block.name}]`)\n else if (block.type === 'tool_result')\n parts.push(`[tool result]`)\n }\n return parts.join(' ')\n}\n\n/**\n * Normalize a model-generated title into the shape we want to persist:\n *\n * - Collapse internal whitespace + take the first line only (some\n * models emit \"Title: foo\\n\\nReason: …\" despite instructions).\n * - Strip surrounding quote characters (`\"foo\"`, `'foo'`, `` `foo` ``).\n * - Strip a leading `Title:` / `title -` prefix if the model added one.\n * - Strip trailing punctuation (`.`, `!`, `?`) — titles read cleaner\n * without it.\n * - Truncate to `TITLE_MAX_CHARS` with a trailing `…` when over.\n *\n * Exported as `cleanTitle` so tests can pin the normalization rules\n * without going through a mock provider.\n */\nexport function cleanTitle(raw: string): string {\n let s = raw.split('\\n')[0]?.trim() ?? ''\n // Strip a \"Title:\" / \"Title -\" preamble the model sometimes adds.\n s = s.replace(/^\\s*title\\s*[:\\-–—]\\s*/i, '')\n // Strip surrounding matched quotes.\n if (s.length >= 2) {\n const first = s.charAt(0)\n const last = s.charAt(s.length - 1)\n const matched = (first === '\"' && last === '\"')\n || (first === '\\'' && last === '\\'')\n || (first === '`' && last === '`')\n || (first === '“' && last === '”')\n if (matched)\n s = s.slice(1, -1).trim()\n }\n // Trim trailing terminal punctuation — titles don't take periods.\n s = s.replace(/[.!?]+$/, '').trim()\n if (s.length === 0)\n throw new Error('Model returned an empty title.')\n return s.length > TITLE_MAX_CHARS ? `${s.slice(0, TITLE_MAX_CHARS - 1)}…` : s\n}\n\nfunction clip(text: string, max: number): string {\n return text.length > max ? `${text.slice(0, max)}…` : text\n}\n","/**\n * Silent structured-output generation — one provider call that returns\n * schema-conforming JSON without touching the session or the transcript.\n *\n * Modeled on the two proven side-channel patterns in the codebase:\n * - `generate-title.ts`: a single `provider.stream()` with no session\n * writes and no transcript events (optionally seeded with a compacted\n * conversation transcript), and\n * - the loop's `__output__` schema enforcement: a synthetic tool whose\n * `inputSchema` IS the requested JSON Schema, with `toolChoice` pinned\n * so the model must answer with a conforming payload.\n *\n * Used by `/` commands via `ctx.generateData` to derive values from the\n * model (or from the conversation) mid-workflow without bloating the chat\n * history — e.g. `/commit` generating `{ subject, body }` from a diff.\n * For genuinely interactive sub-runs (real turns, streaming, tools), use\n * `ctx.runAgent` instead.\n */\n\nimport type { Provider, ToolSpec } from '../providers'\nimport type { SessionTurn } from '../types'\nimport type { InferSchemaOutput, SchemaInput, StandardSchemaLike } from '../zod'\nimport { resolveSchemaInput } from '../zod'\nimport { renderTurnsForPrompt } from './generate-title'\n\n/** Synthetic forced-choice tool name (never persisted — the call is side-channel only). */\nconst DATA_TOOL_NAME = '__data__'\n\n/** Default cap on conversation turns folded into the context block. */\nconst DEFAULT_CONTEXT_TURNS = 12\n\nexport interface GenerateStructuredDataOptions {\n provider: Provider\n model: string\n /** Instruction describing the value to produce. */\n prompt: string\n /**\n * Shape of the result — a plain JSON Schema object, or a zod v4 schema\n * (converted automatically; the return type is then inferred from it).\n * Becomes the forced tool's inputSchema.\n */\n schema: SchemaInput\n /** Optional system prompt. Defaults to a terse data-extraction persona. */\n system?: string\n /**\n * Conversation turns to fold in as compacted context (same clipping as\n * session auto-titling). Omit or pass empty for a context-free call.\n */\n turns?: readonly SessionTurn[]\n /** Cap on turns rendered into the context block. Default 12 (most recent). */\n maxContextTurns?: number\n signal?: AbortSignal\n}\n\n/**\n * Drive one schema-pinned provider call and return the conforming payload.\n * Throws when the provider returns no structured payload (and rethrows\n * aborts verbatim). The caller owns presentation — nothing is rendered or\n * persisted here.\n *\n * Pass a zod v4 schema and the return type is inferred from it; pass a\n * plain JSON Schema object and name the type via the generic.\n */\nexport async function generateStructuredData<S extends StandardSchemaLike>(\n options: GenerateStructuredDataOptions & { schema: S },\n): Promise<InferSchemaOutput<S>>\nexport async function generateStructuredData<T = unknown>(\n options: GenerateStructuredDataOptions,\n): Promise<T>\nexport async function generateStructuredData(\n options: GenerateStructuredDataOptions,\n): Promise<unknown> {\n const { provider, model, prompt, signal } = options\n const spec: ToolSpec = {\n name: DATA_TOOL_NAME,\n description: 'Return the result. The input of this call IS the answer — it must conform to the schema.',\n inputSchema: await resolveSchemaInput(options.schema),\n }\n\n const contextTurns = options.turns && options.turns.length > 0\n ? options.turns.slice(-(options.maxContextTurns ?? DEFAULT_CONTEXT_TURNS))\n : []\n const transcript = contextTurns.length > 0 ? renderTurnsForPrompt(contextTurns) : ''\n const userPrompt = transcript\n ? `<conversation>\\n${transcript}\\n</conversation>\\n\\n${prompt}`\n : prompt\n\n const result = await provider.stream(\n {\n model,\n system: options.system\n ?? `You produce structured data. Call the ${DATA_TOOL_NAME} tool exactly once with the requested value. No prose.`,\n tools: provider.formatTools([spec]),\n messages: [provider.userMessage(userPrompt)],\n toolChoice: { type: 'tool', name: DATA_TOOL_NAME },\n ...(signal ? { signal } : {}),\n },\n {\n onText: () => {},\n },\n )\n\n const payload = result.toolCalls.find(tc => tc.name === DATA_TOOL_NAME)?.input\n if (payload === undefined)\n throw new Error('generateStructuredData: the model returned no structured payload')\n return payload\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hash-DJRzmcPI.js","names":[],"sources":["../src/hash.ts"],"sourcesContent":["/**\n * FNV-1a (32-bit) rendered in base36 — a tiny, dependency-free, deterministic\n * hash.\n *\n * Used to disambiguate sanitized identifiers: distinct inputs that would\n * otherwise collapse to the same sanitized string (e.g. `a.b` and `a/b` both\n * → `a_b`, or two over-long names truncated to the same prefix) keep a stable,\n * unique suffix. Determinism matters because the suffix must be identical\n * across runs and across a `tool_use` / `tool_result` pair derived from the\n * same original value.\n */\nexport function fnv1aBase36(value: string): string {\n let hash = 0x811C9DC5\n for (let i = 0; i < value.length; i++) {\n hash ^= value.charCodeAt(i)\n hash = Math.imul(hash, 0x01000193)\n }\n return (hash >>> 0).toString(36)\n}\n"],"mappings":";;;;;;;;;;;;AAWA,SAAgB,YAAY,OAAuB;CACjD,IAAI,OAAO;CACX,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACrC,QAAQ,MAAM,WAAW,CAAC;EAC1B,OAAO,KAAK,KAAK,MAAM,QAAU;CACnC;CACA,QAAQ,SAAS,
|
|
1
|
+
{"version":3,"file":"hash-DJRzmcPI.js","names":[],"sources":["../src/hash.ts"],"sourcesContent":["/**\n * FNV-1a (32-bit) rendered in base36 — a tiny, dependency-free, deterministic\n * hash.\n *\n * Used to disambiguate sanitized identifiers: distinct inputs that would\n * otherwise collapse to the same sanitized string (e.g. `a.b` and `a/b` both\n * → `a_b`, or two over-long names truncated to the same prefix) keep a stable,\n * unique suffix. Determinism matters because the suffix must be identical\n * across runs and across a `tool_use` / `tool_result` pair derived from the\n * same original value.\n */\nexport function fnv1aBase36(value: string): string {\n let hash = 0x811C9DC5\n for (let i = 0; i < value.length; i++) {\n hash ^= value.charCodeAt(i)\n hash = Math.imul(hash, 0x01000193)\n }\n return (hash >>> 0).toString(36)\n}\n"],"mappings":";;;;;;;;;;;;AAWA,SAAgB,YAAY,OAAuB;CACjD,IAAI,OAAO;CACX,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACrC,QAAQ,MAAM,WAAW,CAAC;EAC1B,OAAO,KAAK,KAAK,MAAM,QAAU;CACnC;CACA,QAAQ,SAAS,EAAA,CAAG,SAAS,EAAE;AACjC"}
|
package/dist/headless.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { An as
|
|
1
|
+
import { An as FormattedHeadlessTurnEvent, Bn as ProviderTranscriptFormat, Fn as HeadlessResult, Gn as headlessEventToJsonl, Hn as formatHeadlessResult, In as HeadlessStatus, Jn as runHeadless, Kn as installHeadlessEventAdapter, Ln as HeadlessUsage, Mn as HeadlessEvent, Nn as HeadlessOptions, Pn as HeadlessOutputFormat, Rn as OpenAIChatContentPart, Un as formatHeadlessTurnEvent, Vn as exitCodeForHeadlessResult, Wn as formattedHeadlessTurnEventToJsonl, Xn as transcriptToProviderMessages, Yn as transcriptToOpenAIMessages, jn as HeadlessErrorInfo, kn as FormattedHeadlessResult, qn as providerTranscriptFormatForProvider, zn as OpenAIChatMessage } from "./index-BGpM2YaU.js";
|
|
2
2
|
export { FormattedHeadlessResult, FormattedHeadlessTurnEvent, HeadlessErrorInfo, HeadlessEvent, HeadlessOptions, HeadlessOutputFormat, HeadlessResult, HeadlessStatus, HeadlessUsage, OpenAIChatContentPart, OpenAIChatMessage, ProviderTranscriptFormat, exitCodeForHeadlessResult, formatHeadlessResult, formatHeadlessTurnEvent, formattedHeadlessTurnEventToJsonl, headlessEventToJsonl, installHeadlessEventAdapter, providerTranscriptFormatForProvider, runHeadless, transcriptToOpenAIMessages, transcriptToProviderMessages };
|
package/dist/headless.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { a as isWireTurn, i as documentBlockMarker, l as toolResultToText, n as assertResolvedMediaBlock } from "./types-BIarq1qE.js";
|
|
2
|
-
import {
|
|
3
|
-
import { n as createProcessContext } from "./contexts-
|
|
2
|
+
import { lt as basic_default, t as createAgent } from "./agent-DZRA7wip.js";
|
|
3
|
+
import { n as createProcessContext } from "./contexts-BIbcfBFH.js";
|
|
4
4
|
import { t as audioFormatFromMediaType } from "./media-sniff-Bn76JxAu.js";
|
|
5
|
-
import { p as toAnthropic, s as ensureToolResultPairing } from "./messages-
|
|
6
|
-
import { t as createSession, u as createMemoryStore } from "./session-
|
|
5
|
+
import { p as toAnthropic, s as ensureToolResultPairing } from "./messages-C5BDOjUG.js";
|
|
6
|
+
import { t as createSession, u as createMemoryStore } from "./session-CsKzK8-P.js";
|
|
7
7
|
import { t as createRunSummaryCollector } from "./run-summary-nD3IQ4ZQ.js";
|
|
8
8
|
//#region src/headless.ts
|
|
9
9
|
function exitCodeForHeadlessResult(result) {
|