@letrquan/book 0.2.0 → 0.3.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/CHANGELOG.md +2747 -40
- package/README.md +132 -1311
- package/dist/additional-roots-H7UNAEHS.js +31 -0
- package/dist/{app-MOXXWEOY.js → app-GE4OHJOC.js} +5740 -4045
- package/dist/app-GE4OHJOC.js.map +1 -0
- package/dist/{chunk-5GDZ22YP.js → chunk-224FJZLH.js} +99 -108
- package/dist/chunk-224FJZLH.js.map +1 -0
- package/dist/chunk-2AAOF5B5.js +28 -0
- package/dist/chunk-2AAOF5B5.js.map +1 -0
- package/dist/{chunk-CAGOB2N4.js → chunk-4AS7LKKQ.js} +6 -4
- package/dist/chunk-4AS7LKKQ.js.map +1 -0
- package/dist/chunk-525CAZPY.js +120 -0
- package/dist/chunk-525CAZPY.js.map +1 -0
- package/dist/{chunk-URLQAXTR.js → chunk-56SIEMDX.js} +2 -2
- package/dist/chunk-56SIEMDX.js.map +1 -0
- package/dist/{chunk-XIFFTATU.js → chunk-5YHTKWJN.js} +5420 -4031
- package/dist/chunk-5YHTKWJN.js.map +1 -0
- package/dist/chunk-62QL5QUP.js +283 -0
- package/dist/chunk-62QL5QUP.js.map +1 -0
- package/dist/{chunk-KWHQ2XXL.js → chunk-67UZEWWF.js} +3 -3
- package/dist/chunk-67UZEWWF.js.map +1 -0
- package/dist/chunk-6OQQOMZR.js +785 -0
- package/dist/chunk-6OQQOMZR.js.map +1 -0
- package/dist/{chunk-GTVB4HEM.js → chunk-AQEQX4FF.js} +344 -315
- package/dist/chunk-AQEQX4FF.js.map +1 -0
- package/dist/chunk-C7ADUNPR.js +273 -0
- package/dist/chunk-C7ADUNPR.js.map +1 -0
- package/dist/{chunk-E75IPST5.js → chunk-EFJ2NH6E.js} +12 -58
- package/dist/chunk-EFJ2NH6E.js.map +1 -0
- package/dist/{chunk-5KLVH3PY.js → chunk-HQI7NYGX.js} +5 -3
- package/dist/chunk-HQI7NYGX.js.map +1 -0
- package/dist/chunk-IME7R2UI.js +6541 -0
- package/dist/chunk-IME7R2UI.js.map +1 -0
- package/dist/chunk-J4F2QLM2.js +273 -0
- package/dist/chunk-J4F2QLM2.js.map +1 -0
- package/dist/{chunk-MAQXCUR4.js → chunk-J62VZYOB.js} +444 -112
- package/dist/chunk-J62VZYOB.js.map +1 -0
- package/dist/chunk-JTPOI3KU.js +826 -0
- package/dist/chunk-JTPOI3KU.js.map +1 -0
- package/dist/{chunk-WKBO5R4O.js → chunk-KBQS35OV.js} +2 -2
- package/dist/chunk-KBQS35OV.js.map +1 -0
- package/dist/chunk-KXAYUIKP.js +90 -0
- package/dist/chunk-KXAYUIKP.js.map +1 -0
- package/dist/{chunk-VZQXX3WW.js → chunk-QYBD22H5.js} +1 -1
- package/dist/chunk-QYBD22H5.js.map +1 -0
- package/dist/{chunk-V4BOYR52.js → chunk-RFJ6Q6LE.js} +763 -198
- package/dist/chunk-RFJ6Q6LE.js.map +1 -0
- package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
- package/dist/chunk-RT5SATF7.js.map +1 -0
- package/dist/{chunk-C7D4NJ2D.js → chunk-S4JMID4G.js} +68 -26
- package/dist/chunk-S4JMID4G.js.map +1 -0
- package/dist/{chunk-ANSJPYGI.js → chunk-SOW4WRQP.js} +3 -3
- package/dist/chunk-SOW4WRQP.js.map +1 -0
- package/dist/{chunk-J7MIZ5CA.js → chunk-TLX3H2CL.js} +518 -79
- package/dist/chunk-TLX3H2CL.js.map +1 -0
- package/dist/{chunk-DGMQEI6Y.js → chunk-VPNLCMVR.js} +22 -9
- package/dist/chunk-VPNLCMVR.js.map +1 -0
- package/dist/{chunk-BTZZUCLI.js → chunk-W3XFHEF4.js} +173 -29
- package/dist/chunk-W3XFHEF4.js.map +1 -0
- package/dist/{chunk-574C6MQR.js → chunk-X4NJT436.js} +130 -60
- package/dist/chunk-X4NJT436.js.map +1 -0
- package/dist/chunk-X66I2NQO.js +137 -0
- package/dist/chunk-X66I2NQO.js.map +1 -0
- package/dist/chunk-XHAO3K7I.js +18 -0
- package/dist/chunk-XHAO3K7I.js.map +1 -0
- package/dist/{chunk-3U5IBM24.js → chunk-Y2VHN7TC.js} +2 -2
- package/dist/chunk-Y2VHN7TC.js.map +1 -0
- package/dist/chunk-YC6W4EJH.js +947 -0
- package/dist/chunk-YC6W4EJH.js.map +1 -0
- package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
- package/dist/chunk-ZPSZZUWF.js.map +1 -0
- package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-VBIU6BLM.js} +6 -5
- package/dist/context-CBOBDY5Q.js +38 -0
- package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-ZVT4V7IM.js} +5 -5
- package/dist/index.js +996 -155
- package/dist/index.js.map +1 -1
- package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-T4I4SEHU.js} +13 -9
- package/dist/interactive-assets-T4I4SEHU.js.map +1 -0
- package/dist/job-runner.js +159 -111
- package/dist/job-runner.js.map +1 -1
- package/dist/job-supervisor.d.ts +2 -0
- package/dist/job-supervisor.js +112 -0
- package/dist/job-supervisor.js.map +1 -0
- package/dist/loader-S77OVTHH.js +18 -0
- package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-HJ2VDYMC.js} +5 -5
- package/dist/{mcp-config-YIJIES2C.js → mcp-config-RNB5LEVE.js} +3 -3
- package/dist/memory-store-DZUT5HUH.js +75 -0
- package/dist/{model-window-store-YFVNXCIP.js → model-window-store-MTSQ5JDJ.js} +8 -4
- package/dist/openai-compatible-2OB54YST.js +14 -0
- package/dist/{permission-approvals-76PVREFD.js → permission-approvals-YLV6UC5W.js} +5 -5
- package/dist/permissions-DHOKF3W2.js +45 -0
- package/dist/sandbox-WRYTZH3D.js +41 -0
- package/dist/{scrollback-LUKLIBVI.js → scrollback-35ME2GFX.js} +32 -22
- package/dist/scrollback-35ME2GFX.js.map +1 -0
- package/dist/sdk.d.ts +2219 -2361
- package/dist/sdk.js +53 -38
- package/dist/sdk.js.map +1 -1
- package/dist/settings-loader-JUNPGAAM.js +35 -0
- package/dist/settings-loader-JUNPGAAM.js.map +1 -0
- package/dist/{shell-selection-V6QAYXPX.js → shell-selection-CDHGJ4GC.js} +2 -2
- package/dist/shell-selection-CDHGJ4GC.js.map +1 -0
- package/package.json +23 -22
- package/dist/app-MOXXWEOY.js.map +0 -1
- package/dist/chunk-24AXE6SP.js.map +0 -1
- package/dist/chunk-3U5IBM24.js.map +0 -1
- package/dist/chunk-574C6MQR.js.map +0 -1
- package/dist/chunk-5GDZ22YP.js.map +0 -1
- package/dist/chunk-5KLVH3PY.js.map +0 -1
- package/dist/chunk-7N4J557C.js.map +0 -1
- package/dist/chunk-A7JLBK2Y.js +0 -3506
- package/dist/chunk-A7JLBK2Y.js.map +0 -1
- package/dist/chunk-ANSJPYGI.js.map +0 -1
- package/dist/chunk-BTZZUCLI.js.map +0 -1
- package/dist/chunk-C7D4NJ2D.js.map +0 -1
- package/dist/chunk-CAGOB2N4.js.map +0 -1
- package/dist/chunk-DGMQEI6Y.js.map +0 -1
- package/dist/chunk-E75IPST5.js.map +0 -1
- package/dist/chunk-EYGD6X2T.js +0 -180
- package/dist/chunk-EYGD6X2T.js.map +0 -1
- package/dist/chunk-GTVB4HEM.js.map +0 -1
- package/dist/chunk-J7MIZ5CA.js.map +0 -1
- package/dist/chunk-KWHQ2XXL.js.map +0 -1
- package/dist/chunk-L6LK2QIQ.js +0 -657
- package/dist/chunk-L6LK2QIQ.js.map +0 -1
- package/dist/chunk-MAQXCUR4.js.map +0 -1
- package/dist/chunk-S4XL7HOM.js +0 -402
- package/dist/chunk-S4XL7HOM.js.map +0 -1
- package/dist/chunk-URLQAXTR.js.map +0 -1
- package/dist/chunk-V4BOYR52.js.map +0 -1
- package/dist/chunk-VZQXX3WW.js.map +0 -1
- package/dist/chunk-WKBO5R4O.js.map +0 -1
- package/dist/chunk-XIFFTATU.js.map +0 -1
- package/dist/context-ZC5IFEAX.js +0 -25
- package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
- package/dist/loader-4IDS2RIE.js +0 -15
- package/dist/openai-compatible-RHZUBHV6.js +0 -10
- package/dist/permissions-CHCKTFQQ.js +0 -28
- package/dist/sandbox-CNVAQDA7.js +0 -23
- package/dist/scrollback-LUKLIBVI.js.map +0 -1
- package/dist/settings-loader-M2QP5TX2.js +0 -21
- package/patches/ink+6.8.0.patch +0 -13
- package/scripts/apply-ink-patch.mjs +0 -29
- package/scripts/ink-patch.mjs +0 -59
- package/scripts/verify-ink-patch.mjs +0 -11
- /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-H7UNAEHS.js.map} +0 -0
- /package/dist/{context-ZC5IFEAX.js.map → command-approvals-VBIU6BLM.js.map} +0 -0
- /package/dist/{hook-approvals-CTUFKEJT.js.map → context-CBOBDY5Q.js.map} +0 -0
- /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-ZVT4V7IM.js.map} +0 -0
- /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-S77OVTHH.js.map} +0 -0
- /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-HJ2VDYMC.js.map} +0 -0
- /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-RNB5LEVE.js.map} +0 -0
- /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-DZUT5HUH.js.map} +0 -0
- /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-MTSQ5JDJ.js.map} +0 -0
- /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-2OB54YST.js.map} +0 -0
- /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-YLV6UC5W.js.map} +0 -0
- /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-DHOKF3W2.js.map} +0 -0
- /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-WRYTZH3D.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/provider/reliability.ts","../src/provider/openai-compatible.ts"],"sourcesContent":["import type { RetryConfig } from '../types/runtime.js';\n\ninterface RetryLogger {\n warn(message: string, data?: Record<string, unknown>): void;\n}\n\nimport { systemClock, type Clock, type MonotonicMs } from '../clock.js';\n\nexport type ProviderErrorCode =\n | 'network'\n | 'timeout'\n | 'context_overflow'\n | 'rate_limited'\n | 'overloaded'\n | 'server_error'\n | 'auth'\n | 'bad_request'\n | 'not_found'\n | 'unprocessable'\n | 'quota'\n | 'unknown';\n\nexport function classifyHttpStatus(status: number): {\n code: ProviderErrorCode;\n retryable: boolean;\n} {\n if (status === 413) return { code: 'context_overflow', retryable: false };\n if (status === 429) return { code: 'rate_limited', retryable: true };\n if (status === 529) return { code: 'overloaded', retryable: true };\n if (status >= 500 && status < 600) return { code: 'server_error', retryable: true };\n if (status === 408) return { code: 'timeout', retryable: true };\n if (status === 401 || status === 403) return { code: 'auth', retryable: false };\n if (status === 402) return { code: 'quota', retryable: false };\n if (status === 400) return { code: 'bad_request', retryable: false };\n if (status === 404) return { code: 'not_found', retryable: false };\n if (status === 422) return { code: 'unprocessable', retryable: false };\n return { code: 'unknown', retryable: false };\n}\n\n/**\n * The two 404 wordings a router hands back when its route has nothing left to\n * serve a model with (#383). Recognized only when the body also states a\n * `(reset after …)` cooldown, so a permanent \"No endpoints found for\n * unknown-model\" — a missing model, not a capacity moment — stays a terminal\n * 404.\n */\nconst CAPACITY_OUTAGE_404 = /no\\s+(?:endpoints|accounts)\\s+found/i;\n\n/** The cooldown statement the capacity outage arrives with. */\nconst RESET_AFTER = /\\(reset after\\s*[^)]+\\)/i;\n\n/**\n * The upstream HTTP status a router quoted inside its own error body, if any.\n *\n * 9router wraps an upstream 4xx as a 503 plus a cooldown, so the wrapper's status\n * says \"transient\" while the body says the request itself is invalid. Only a 4xx\n * is ever taken from the body: a quoted 5xx says nothing the wrapper did not.\n *\n * Only two shapes count as a quote: 9router's own `[<route>] [400]:` prefix, and\n * a JSON `\"error\"` object whose `code` is a 4xx. A status mentioned in prose\n * (`upstream sent HTTP 403`, `chunk [404]`) or a longer number that merely starts\n * like one (`\"code\": 4001`) is not: misreading an outage body ends the run after\n * a single request, or parks it as a rejected credential.\n *\n * A quoted 404 whose body says no endpoints or no accounts were found — and\n * states a cooldown — is a capacity outage (#383): the router turns an upstream\n * `[CommandCode error: No endpoints found for <model>.]` into a 503 with a\n * `(reset after …)` cooldown, so it is transient and the quote must not end the\n * retries. The read ends there: such a body can also carry a JSON error object\n * with some other code, and reading that quote would park the run as if the\n * credential had been refused. A cooldown-less \"no endpoints found\" is kept as\n * the 404 it is.\n */\nexport function quotedUpstreamStatus(body: string): number | undefined {\n if (!body) return undefined;\n const text = body.length > 65536 ? body.slice(0, 65536) : body;\n const patterns = [\n /\\[[^\\]\\s]+\\]\\s*\\[(4\\d\\d)\\]:/,\n /\"error\"\\s*:\\s*\\{[^{}]*?\"code\"\\s*:\\s*\"?(4\\d\\d)(?![\\d.])/,\n ];\n const capacityOutage = CAPACITY_OUTAGE_404.test(text) && RESET_AFTER.test(text);\n if (capacityOutage) return undefined;\n for (const pattern of patterns) {\n const match = text.match(pattern);\n if (match) return Number(match[1]);\n }\n return undefined;\n}\n\n/**\n * The upstream 4xx quoted inside a retryable response, when that quote is what\n * classifies it: 9router answers 503 and names the real status in the body. A\n * non-retryable status is its own answer, and its body is never re-read.\n */\nexport function wrappedUpstreamStatus(status: number, body: string): number | undefined {\n return classifyHttpStatus(status).retryable ? quotedUpstreamStatus(body) : undefined;\n}\n\n/** `error.code` or `error.type` values that name a context overflow outright. */\nconst CONTEXT_OVERFLOW_ERROR_NAMES: ReadonlySet<string> = new Set([\n 'context_length_exceeded',\n 'request_too_large',\n // llama.cpp's server.\n 'exceed_context_size_error',\n]);\n\n/**\n * The message and the `code` / `type` names of a provider's error body. The\n * message is `error.message` (or `error` itself when it is a string), else a\n * top-level `message` or `detail`; a body that is not JSON is its own message.\n * A body that reached the read cap and starts like JSON but does not parse is\n * read for its first `\"message\"`, `\"error\"` or `\"detail\"` string and its first\n * `\"code\"` / `\"type\"` names alone, never for the rest, which may echo the\n * request. `raw` is the upstream's own error body that OpenRouter forwards in\n * `error.metadata.raw`, as a string or as an object. `parsed` says the body\n * read as a JSON object, so a reader can tell a body with no message of its own\n * from a body that is not JSON at all.\n */\nfunction errorBodyParts(body: string): {\n message: string;\n names: string[];\n raw?: string;\n parsed: boolean;\n} {\n let parsedJson: unknown;\n try {\n parsedJson = JSON.parse(body);\n } catch {\n // Only a body that reached the read cap can be JSON the cap cut; a short malformed one\n // (a Python-repr `{'error': …}`) is plain text, read whole. A router's plain text can open\n // with a bracket of its own (9router's `[antigravity/x] [400]: …`), so only a `{` or an\n // array of objects counts as JSON.\n // The `- 3` allows for a multi-byte character the cap cut and the decoder replaced.\n const reachedCap = new TextEncoder().encode(body).byteLength >= ERROR_BODY_MAX_BYTES - 3;\n if (!reachedCap || !/^\\s*(?:\\{|\\[\\s*\\{)/.test(body)) {\n return { message: body, names: [], parsed: false };\n }\n // Cut JSON: the first string of `\"message\"`, else of `\"error\"`, else of `\"detail\"`, and\n // the first `\"code\"` / `\"type\"` names — never the rest, which may echo the request.\n const readString = (key: string): string => {\n const match = body.match(new RegExp(`\"${key}\"\\\\s*:\\\\s*\"((?:[^\"\\\\\\\\]|\\\\\\\\.)*)\"`));\n if (!match) return '';\n try {\n return JSON.parse(`\"${match[1]}\"`) as string;\n } catch {\n return '';\n }\n };\n const message = readString('message') || readString('error') || readString('detail');\n const names = [body.match(/\"code\"\\s*:\\s*\"([^\"]*)\"/), body.match(/\"type\"\\s*:\\s*\"([^\"]*)\"/)]\n .filter((name): name is RegExpMatchArray => name !== null)\n .map((name) => name[1]);\n return { message, names, parsed: true };\n }\n const root: unknown = Array.isArray(parsedJson) ? parsedJson[0] : parsedJson;\n if (typeof root !== 'object' || root === null) {\n return { message: body, names: [], parsed: false };\n }\n const error: unknown = (root as { error?: unknown }).error;\n if (typeof error === 'string') return { message: error, names: [], parsed: true };\n const source = (typeof error === 'object' && error !== null ? error : root) as Record<\n string,\n unknown\n >;\n const fallback = (root as { detail?: unknown }).detail;\n const message =\n typeof source.message === 'string'\n ? source.message\n : typeof fallback === 'string'\n ? fallback\n : '';\n const metadata = source.metadata as { raw?: unknown } | null | undefined;\n const raw = metadata?.raw;\n return {\n message,\n names: [source.code, source.type].filter((name): name is string => typeof name === 'string'),\n raw:\n typeof raw === 'string'\n ? raw\n : typeof raw === 'object' && raw !== null\n ? JSON.stringify(raw)\n : undefined,\n parsed: true,\n };\n}\n\n/**\n * True when an error body states that the input is too large: its `error.code`\n * or `error.type` names an overflow, or its message says so\n * (`isContextOverflowError`). Only the message is read for wording, never the\n * rest of the body: a field path such as `contents[413]` or an id such as\n * `req-413-x` elsewhere in a 400 says nothing about the context window, and a\n * stated overflow permanently lowers the model's learned window. OpenRouter's\n * own message is generic (`Provider returned error`), so the upstream body it\n * forwards is read the same way; each level is shorter than the last.\n */\nfunction statesContextOverflow(body: string): boolean {\n const { message, names, raw } = errorBodyParts(body);\n return (\n names.some((name) => CONTEXT_OVERFLOW_ERROR_NAMES.has(name.toLowerCase())) ||\n isContextOverflowError(message) ||\n (raw !== undefined && statesContextOverflow(raw))\n );\n}\n\nexport function classifyApiError(status: number, body: string): ProviderErrorCode {\n const code = classifyHttpStatus(wrappedUpstreamStatus(status, body) ?? status).code;\n return code === 'bad_request' && statesContextOverflow(body) ? 'context_overflow' : code;\n}\n\nexport function classifyProviderError(error: unknown): ProviderErrorCode {\n if (error instanceof DOMException && error.name === 'TimeoutError') return 'timeout';\n const message = (error instanceof Error ? error.message : String(error)).toLowerCase();\n if (message.includes('timed out') || message.includes('timeout')) return 'timeout';\n if (isContextOverflowError(message)) return 'context_overflow';\n return 'network';\n}\n\nexport function formatApiError(status: number, body: string): string {\n const code = classifyApiError(status, body);\n const detail = safeErrorDetail(body);\n const base = `API Error: ${status}`;\n switch (code) {\n case 'context_overflow':\n return `${base} ${detail || 'Input exceeds the model context window.'} Reduce the conversation or tool output and try again.`;\n case 'rate_limited':\n return isOversizedForRateLimit(detail)\n ? `${base} ${detail}. The request is larger than the rate limit allows at once, so it has to shrink; waiting will not help.`\n : `${base} ${detail}. This may be a temporary capacity issue. Try again in a moment.`;\n case 'overloaded':\n return `${base} Repeated 529 Overloaded errors. The API is at capacity — this is usually temporary. Try again in a moment.`;\n case 'server_error':\n return `${base} ${detail}. This is a server-side issue, usually temporary — try again in a moment.`;\n case 'timeout':\n return 'Request timed out. Check your network connection and try again.';\n case 'auth':\n return `${base} ${detail}. Check BOOK_API_KEY, or provider.<id>.apiKey in settings.`;\n case 'quota':\n return `${base} ${detail}. Check your usage/credits.`;\n default:\n return `${base} ${detail}`;\n }\n}\n\n/**\n * Detect provider/router responses that mean the input, rather than transport, is\n * too large. A 413 counts only where it is named as a status (`API Error: 413`,\n * `HTTP 413`, `status 413`, `Error code: 413`), never as a bare number: a field\n * path such as `contents[413]` or an id such as `req-413-x` is not one.\n */\nexport function isContextOverflowError(error: unknown): boolean {\n const normalized = (error instanceof Error ? error.message : String(error)).toLowerCase();\n return (\n /\\b(?:api\\s+error|http|(?:error|status)(?:\\s+code)?)\\s*:?\\s*413\\b/.test(normalized) ||\n normalized.includes('request entity too large') ||\n normalized.includes('payload too large') ||\n normalized.includes('request too large') ||\n normalized.includes('context_length_exceeded') ||\n normalized.includes('maximum context length') ||\n normalized.includes('context window') ||\n normalized.includes('prompt too long') ||\n /prompt\\s+is\\s+too\\s+long/.test(normalized) ||\n /input\\s+(?:is\\s+)?too\\s+long/.test(normalized) ||\n normalized.includes('too many tokens') ||\n // Gemini names the count: `The input token count (1196266) exceeds the maximum …`.\n /input\\s+(?:token\\s+count\\s+)?(?:\\(\\d+\\)\\s+)?exceeds?.*(?:context|limit|maximum)/.test(\n normalized,\n ) ||\n /input\\s+exceeds?.*(?:context\\s+window|context\\s+length|token\\s+limit)/.test(normalized) ||\n /maximum\\s+context|context\\s+(?:length|window|size).*(?:exceed|overflow|too\\s+(?:long|large)|maximum)/.test(\n normalized,\n ) ||\n /too\\s+many\\s+(?:input\\s+)?tokens|request\\s+too\\s+large.*token/.test(normalized)\n );\n}\n\n/**\n * True when a rate-limit error says the request itself is larger than the limit allows at\n * once, which no amount of waiting fixes: OpenAI's `Request too large for <model> … on tokens\n * per min (TPM): Limit 30000, Requested 45000.` A transient per-minute limit reads `Rate limit\n * reached …` and is not this, and neither is a bare \"too many tokens\". When the text gives\n * both numbers, the request must exceed the limit.\n */\nexport function isOversizedForRateLimit(text: string): boolean {\n if (!/\\brequest too large\\b/i.test(text)) return false;\n const limit = text.match(/\\blimit:?\\s*(\\d+)/i);\n const requested = text.match(/\\brequested:?\\s*(\\d+)/i);\n return !limit || !requested || Number(requested[1]) > Number(limit[1]);\n}\n\n/** The per-request limit an oversized rate-limit error states (`Limit 30000`), if any. */\nexport function statedRateLimit(text: string): number | undefined {\n if (!isOversizedForRateLimit(text)) return undefined;\n const limit = text.match(/\\blimit:?\\s*(\\d+)/i);\n return limit ? Number(limit[1]) : undefined;\n}\n\n/**\n * The longest text still read as an error envelope rather than an answer when a\n * model may have written it. OpenAI's server-error sentence is about 260\n * characters, and a JSON-rendered upstream error rarely passes 1,000.\n */\nconst ERROR_ENVELOPE_MAX_CHARS = 2_000;\n\ntype EnvelopeUsage = {\n promptTokens: number;\n completionTokens: number;\n cacheReadInputTokens?: number;\n cacheCreationInputTokens?: number;\n} | null;\n\n/**\n * A usage block that reports zero tokens both ways: what a router reports for\n * text it wrote itself. A provider that sends no usage block at all gives\n * `undefined`, which is not this.\n */\nfunction isZeroUsage(usage?: EnvelopeUsage): boolean {\n // A full cache hit leaves `promptTokens` at 0 on its own; the cache counts are usage too.\n return (\n usage != null &&\n usage.promptTokens === 0 &&\n usage.completionTokens === 0 &&\n (usage.cacheReadInputTokens ?? 0) === 0 &&\n (usage.cacheCreationInputTokens ?? 0) === 0\n );\n}\n\n/**\n * True when `text` has the shape a router gives an upstream error it renders as\n * the answer. 9router's Responses translator writes an upstream `error` or\n * `response.failed` event as `[Error] <message>` (or the error object as JSON\n * when it has no message) and ends the stream there. When a model may have\n * written the text, the envelope must be the whole answer: one `[Error] …` line.\n * An answer that quotes such a line and goes on to explain it is an answer. With\n * a usage block that reports zero tokens both ways any answer that opens with\n * `[Error]` is read as the router's, however many lines it runs to.\n */\nexport function isErrorEnvelopeShape(text: string, usage?: EnvelopeUsage): boolean {\n if (isZeroUsage(usage)) return /^\\s*\\[Error\\]/i.test(text);\n const trimmed = text.trim();\n return (\n trimmed.length <= ERROR_ENVELOPE_MAX_CHARS &&\n /^\\[Error\\]/i.test(trimmed) &&\n !/[\\r\\n]/.test(trimmed)\n );\n}\n\n/**\n * A router that answers 200 with the upstream's error as the assistant text has\n * not answered. The envelope shape (`isErrorEnvelopeShape`) is required; then\n * either a usage block reporting zero tokens both ways or the upstream's own\n * server-error wording confirms it. The supported shapes are the table in\n * `reliability.test.ts`:\n * - OpenAI's `[Error] An error occurred while processing your request. …\n * help.openai.com … Please include the request ID <id> in your message.`,\n * whatever the usage says;\n * - any other one-line server error that names its request ID;\n * - with a usage block that reports zero tokens both ways, any answer that\n * opens with `[Error]`, one line or many.\n */\nexport function isUpstreamErrorEnvelope(text: string, usage?: EnvelopeUsage): boolean {\n if (!isErrorEnvelopeShape(text, usage)) return false;\n return (\n isZeroUsage(usage) ||\n /\\brequest id\\b/i.test(text) ||\n /help\\.openai\\.com/i.test(text) ||\n /error occurred while processing/i.test(text)\n );\n}\n\n/**\n * How long a retryable response's error body may take to arrive. The body only\n * informs the retry decision (a quoted upstream 4xx stops the retries), so a\n * router that sends its headers and then stalls must not hold every attempt for\n * the full `requestTimeoutMs`: ten attempts at 600 s each by default. After this\n * long the decision is made on what arrived.\n */\nexport const ERROR_BODY_READ_TIMEOUT_MS = 5_000;\n\n/** The most of an error body that is ever read; the rest is cancelled unread. */\nconst ERROR_BODY_MAX_BYTES = 65_536;\n\n/**\n * Read at most ERROR_BODY_MAX_BYTES of an error body, for at most\n * ERROR_BODY_READ_TIMEOUT_MS. What arrived before the cap, the deadline, an\n * abort or a stream error is kept, and the rest of the body is cancelled.\n *\n * Every error body goes through here, retryable or not: a non-retryable one is\n * what the classification and the text the user is shown are both read from, so\n * an unbounded read there is a socket held for as long as the provider sends.\n */\nexport async function readErrorBody(response: Response, signal?: AbortSignal): Promise<string> {\n if (signal?.aborted || !response.body) return '';\n let reader: ReadableStreamDefaultReader<Uint8Array>;\n try {\n reader = response.body.getReader();\n } catch {\n // A body something else already locked has nothing left to read here.\n return '';\n }\n const decoder = new TextDecoder();\n let text = '';\n let received = 0;\n let finished = false;\n // Cancelling resolves the pending read as done, which ends the loop below.\n const stop = (): void => {\n reader.cancel().catch(() => {});\n };\n const timer = setTimeout(stop, ERROR_BODY_READ_TIMEOUT_MS);\n signal?.addEventListener('abort', stop, { once: true });\n try {\n while (received < ERROR_BODY_MAX_BYTES) {\n const chunk = await reader.read();\n if (chunk.done) {\n finished = true;\n break;\n }\n const bytes = chunk.value.subarray(0, ERROR_BODY_MAX_BYTES - received);\n received += bytes.byteLength;\n text += decoder.decode(bytes, { stream: true });\n }\n } catch {\n // A body that fails part-way still leaves what arrived before it.\n } finally {\n clearTimeout(timer);\n signal?.removeEventListener('abort', stop);\n if (!finished) stop();\n try {\n reader.releaseLock();\n } catch {\n // Nothing is pending once the loop has ended.\n }\n }\n return text + decoder.decode();\n}\n\nexport async function fetchWithRetry(\n url: string,\n init: RequestInit,\n retry: RetryConfig,\n signal?: AbortSignal,\n onRetry?: (attempt: number, max: number, delayMs: number) => void,\n logger?: RetryLogger,\n // The retry budget is a duration, so it reads the monotonic clock. On the\n // wall clock an NTP step or a resumed VM could hand a week-long run either\n // failure mode: a backwards correction makes the budget never expire and the\n // call retries forever, a forwards one exhausts it on the first attempt.\n clock: Clock = systemClock,\n): Promise<Response> {\n const startMs = clock.monotonicNowMs();\n const maxAttempts = retry.watchdog ? Number.MAX_SAFE_INTEGER : retry.maxAttempts;\n let lastError: string | null = null;\n\n for (let attempt = 0; attempt <= maxAttempts; attempt++) {\n if (attempt > 0 && signal?.aborted) throw abortError(signal);\n const fetchSignal = requestSignal(signal, retry.requestTimeoutMs);\n let response: Response;\n try {\n response = await fetch(url, { ...init, signal: fetchSignal });\n } catch (error) {\n if (signal?.aborted) throw error;\n lastError = error instanceof Error ? error.message : String(error);\n if (attempt >= maxAttempts || budgetExhausted(clock, startMs, retry.totalBudgetMs)) break;\n const delay = boundedDelay(clock, backoffMs(attempt, retry), startMs, retry.totalBudgetMs);\n logger?.warn('retry network error', {\n attempt: attempt + 1,\n delayMs: delay,\n error: lastError,\n });\n onRetry?.(attempt + 1, maxAttempts === Number.MAX_SAFE_INTEGER ? -1 : maxAttempts, delay);\n await sleep(delay, signal);\n continue;\n }\n\n const classification = classifyHttpStatus(response.status);\n if (!classification.retryable) return response;\n lastError = `API error ${response.status}`;\n\n const bodyText = await readErrorBody(response, signal);\n const quoted = quotedUpstreamStatus(bodyText);\n if (quoted !== undefined && !classifyHttpStatus(quoted).retryable) {\n logger?.warn('upstream error quoted in retryable status; not retrying', {\n status: response.status,\n upstreamStatus: quoted,\n });\n return new Response(bodyText, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n });\n }\n\n // A 429 that states the request itself is too large (OpenAI's per-minute cap on one request\n // that can never fit under it) is refused the same way however long the retries wait: the\n // loop's overflow recovery is what lets it through, so it gets the response now. Tested on\n // the same formatted text the loop reads, so a statement buried in `metadata.raw` — which\n // that text does not carry — leaves the retries running. A router may wrap that 429 in\n // another status (`503 [route] [429]: …`), so the quoted one counts.\n if (\n (quoted ?? response.status) === 429 &&\n isOversizedForRateLimit(formatApiError(response.status, bodyText))\n ) {\n logger?.warn('rate limit on a request too large to ever fit; not retrying', {\n status: response.status,\n });\n return new Response(bodyText, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n });\n }\n\n const watchdogRetry = retry.watchdog && (response.status === 429 || response.status === 529);\n const effectiveMax = watchdogRetry ? Number.MAX_SAFE_INTEGER : maxAttempts;\n if (attempt >= effectiveMax || budgetExhausted(clock, startMs, retry.totalBudgetMs)) {\n return new Response(bodyText, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n });\n }\n\n const delay = boundedDelay(\n clock,\n backoffMs(attempt, retry, response.headers.get('retry-after'), bodyText),\n startMs,\n retry.totalBudgetMs,\n );\n logger?.warn('retry http status', {\n attempt: attempt + 1,\n status: response.status,\n delayMs: delay,\n });\n onRetry?.(attempt + 1, effectiveMax === Number.MAX_SAFE_INTEGER ? -1 : effectiveMax, delay);\n try {\n await sleep(delay, signal);\n } catch {\n return new Response(bodyText, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n });\n }\n try {\n await response.body?.cancel();\n } catch {\n // A custom fetch implementation may expose an already-locked body.\n }\n }\n\n throw new Error(lastError ?? 'request failed after retries');\n}\n\nexport async function readStreamChunk(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n signal: AbortSignal | undefined,\n stallTimeoutMs: number,\n): Promise<\n | { tag: 'read'; done: boolean; value: Uint8Array | undefined }\n | { tag: 'stall' }\n | { tag: 'abort' }\n> {\n if (signal?.aborted) return { tag: 'abort' };\n let timeout: ReturnType<typeof setTimeout> | undefined;\n let onAbort: (() => void) | undefined;\n try {\n return await Promise.race([\n reader.read().then((result) => ({\n tag: 'read' as const,\n done: result.done,\n value: result.value,\n })),\n ...(stallTimeoutMs > 0\n ? [\n new Promise<{ tag: 'stall' }>((resolve) => {\n timeout = setTimeout(() => resolve({ tag: 'stall' }), stallTimeoutMs);\n }),\n ]\n : []),\n ...(signal\n ? [\n new Promise<{ tag: 'abort' }>((resolve) => {\n onAbort = () => resolve({ tag: 'abort' });\n signal.addEventListener('abort', onAbort, { once: true });\n }),\n ]\n : []),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n if (signal && onAbort) signal.removeEventListener('abort', onAbort);\n }\n}\n\nfunction requestSignal(\n signal: AbortSignal | undefined,\n timeoutMs: number,\n): AbortSignal | undefined {\n if (timeoutMs <= 0) return signal;\n const timeout = AbortSignal.timeout(timeoutMs);\n return signal ? AbortSignal.any([signal, timeout]) : timeout;\n}\n\nfunction abortError(signal: AbortSignal): Error {\n return signal.reason instanceof Error ? signal.reason : new Error('Aborted');\n}\n\n/**\n * The message `errorBodyParts` reads, so the text shown and the text classified are the\n * same. A body with no message of its own (a nested `error.error.message`) or that is not\n * JSON shows its first characters instead.\n */\nfunction safeErrorDetail(body: string): string {\n const { message, parsed } = errorBodyParts(body);\n if (parsed && message) return message.slice(0, 2000);\n return body.slice(0, 200);\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortError(signal));\n return;\n }\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal ? abortError(signal) : new Error('Aborted'));\n };\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n\nfunction backoffMs(\n attempt: number,\n retry: RetryConfig,\n retryAfter?: string | null,\n body?: string,\n): number {\n const retryAfterMs = parseRetryAfter(retryAfter) ?? parseCooldownReset(body);\n if (retryAfterMs !== undefined) return Math.min(retryAfterMs, retry.maxDelayMs);\n const exponential = Math.min(retry.baseDelayMs * 2 ** attempt, retry.maxDelayMs);\n return Math.round(exponential * (0.5 + Math.random()));\n}\n\nfunction parseRetryAfter(value?: string | null): number | undefined {\n if (!value) return undefined;\n const seconds = Number(value);\n if (Number.isFinite(seconds) && seconds > 0) return seconds * 1000;\n const timestamp = Date.parse(value);\n if (!Number.isFinite(timestamp)) return undefined;\n // Wall clock on purpose, and the one place in this file that should be: the\n // server sent an HTTP date, which is a point on the wall clock by definition.\n // Subtracting a monotonic reading from it would be meaningless.\n return Math.max(0, timestamp - Date.now());\n}\n\n/**\n * How many milliseconds each unit word in a stated cooldown is worth. Keyed by\n * every spelling the matcher accepts.\n */\nconst COOLDOWN_UNIT_MS: Record<string, number> = {\n ms: 1,\n s: 1_000,\n sec: 1_000,\n secs: 1_000,\n second: 1_000,\n seconds: 1_000,\n m: 60_000,\n min: 60_000,\n mins: 60_000,\n minute: 60_000,\n minutes: 60_000,\n h: 3_600_000,\n hr: 3_600_000,\n hrs: 3_600_000,\n hour: 3_600_000,\n hours: 3_600_000,\n};\n\n// Longest unit spellings first, and no unit may be followed by another letter:\n// the boundary is what keeps `500ms` from being read as 500 minutes — the `m`\n// would otherwise swallow the `ms` — while still reading compound forms such as\n// `1m26s`, where a digit follows the unit.\nconst COOLDOWN_UNIT = /(\\d+)\\s*(ms|sec(?:ond)?s?|min(?:ute)?s?|h(?:ours?|rs?)?|s|m)(?![a-z])/gi;\n\n/**\n * The cooldown a router states in its error body (`(reset after 5s)`), read when\n * there is no Retry-After header to wait by (#383). Forms seen in the wild:\n * `5s`, `42s`, `1m26s`, `1 min 26 s`, and millisecond durations such as `500ms`\n * and `1s 200ms`. Units are ms, s/sec/seconds, m/min/minutes and h/hours, so `m`\n * is never read when it belongs to `ms`. Returns milliseconds, or undefined\n * when the body states no duration.\n */\nfunction parseCooldownReset(body?: string | null): number | undefined {\n if (!body) return undefined;\n const stated = body.match(/\\(reset after\\s*([^)]+)\\)/i);\n if (!stated) return undefined;\n let totalMs = 0;\n let statedUnits = 0;\n for (const unit of stated[1].matchAll(COOLDOWN_UNIT)) {\n totalMs += Number(unit[1]) * COOLDOWN_UNIT_MS[unit[2].toLowerCase()];\n statedUnits++;\n }\n return statedUnits > 0 && totalMs > 0 ? totalMs : undefined;\n}\n\nfunction budgetExhausted(clock: Clock, startMs: MonotonicMs, budgetMs: number): boolean {\n return budgetMs > 0 && clock.monotonicNowMs() - startMs >= budgetMs;\n}\n\nfunction boundedDelay(\n clock: Clock,\n delayMs: number,\n startMs: MonotonicMs,\n budgetMs: number,\n): number {\n if (budgetMs <= 0) return delayMs;\n return Math.min(delayMs, Math.max(0, budgetMs - (clock.monotonicNowMs() - startMs)));\n}\n","import type { AgentConfig } from '../types/runtime.js';\nimport type {\n ProviderMessage,\n ProviderStreamEvent,\n SystemPromptZones,\n} from '../types/providers.js';\nimport type { ToolDefinition } from '../types/tools.js';\nimport type { Usage } from '../types/messages.js';\nimport { createDebugLogger, isDebugEnabled } from '../debug-log.js';\nimport { escapeInvisibleCharacters } from '../control-characters.js';\nimport {\n classifyApiError,\n classifyProviderError,\n fetchWithRetry,\n formatApiError,\n readErrorBody,\n readStreamChunk,\n wrappedUpstreamStatus,\n} from './reliability.js';\n\nconst log = createDebugLogger('provider');\n\nfunction parseToolArguments(raw: string): {\n arguments: Record<string, unknown>;\n unparsedArguments?: { raw: string; error: string };\n} {\n if (!raw.trim()) return { arguments: {} };\n try {\n const parsed = JSON.parse(raw);\n return {\n arguments: parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {},\n };\n } catch (error) {\n return {\n arguments: {},\n unparsedArguments: { raw, error: error instanceof Error ? error.message : String(error) },\n };\n }\n}\n\nfunction parseReasoningDelta(delta: Record<string, unknown>): string | undefined {\n for (const key of ['reasoning_content', 'reasoning', 'thinking', 'analysis']) {\n const value = delta[key];\n if (typeof value === 'string' && value) return value;\n }\n const details = delta.reasoning_details;\n if (!Array.isArray(details)) return undefined;\n const text = details\n .map((detail) => {\n if (typeof detail === 'string') return detail;\n if (!detail || typeof detail !== 'object') return '';\n const item = detail as Record<string, unknown>;\n for (const key of ['text', 'reasoning', 'content']) {\n if (typeof item[key] === 'string') return item[key] as string;\n }\n return '';\n })\n .filter(Boolean)\n .join('');\n return text || undefined;\n}\n\nfunction isSystemPromptZones(content: ProviderMessage['content']): content is SystemPromptZones {\n return (\n !!content &&\n typeof content === 'object' &&\n 'cachedPrefix' in content &&\n 'dynamicSuffix' in content\n );\n}\n\nfunction flattenMessages(messages: ProviderMessage[]): Array<{\n role: string;\n content:\n | string\n | null\n | Array<{ type: 'text'; text: string } | { type: 'image_url'; image_url: { url: string } }>;\n tool_calls?: ProviderMessage['tool_calls'];\n tool_call_id?: string;\n}> {\n return messages.map(({ providerMetadata: _providerMetadata, reasoningContent, ...msg }) => ({\n ...msg,\n content: isSystemPromptZones(msg.content)\n ? [msg.content.cachedPrefix, msg.content.dynamicSuffix].filter(Boolean).join('\\n\\n')\n : Array.isArray(msg.content)\n ? msg.content.map((part) =>\n part.type === 'text'\n ? part\n : {\n type: 'image_url' as const,\n image_url: { url: `data:${part.mediaType};base64,${part.data}` },\n },\n )\n : reasoningContent\n ? ['<reasoning_context>', reasoningContent, '</reasoning_context>', msg.content ?? '']\n .filter(Boolean)\n .join('\\n')\n : msg.content,\n }));\n}\n\n/**\n * Map an OpenAI-compatible `usage` object onto Book's `Usage`, prompt-cache tokens included.\n *\n * Book's `promptTokens` is the uncached input, as on the Anthropic path, with cache reads and\n * writes counted separately. Where the cache counts sit depends on who reported them:\n *\n * - `prompt_tokens_details.cached_tokens` / `cache_creation_tokens` / `cache_write_tokens`\n * (OpenAI, xAI, 9router, OpenRouter) and DeepSeek's `prompt_cache_hit_tokens`, plus Moonshot's\n * top-level `cached_tokens` and DashScope's `prompt_tokens_details.cache_creation_input_tokens`,\n * are part of `prompt_tokens`, so they are subtracted from it. `total_tokens` already covers them,\n * so `contextTokens` stays unset and compaction pressure is `total_tokens`, as before.\n * - Anthropic's own top-level names (`cache_read_input_tokens`, `cache_creation_input_tokens`)\n * from a proxy that sends no `prompt_tokens_details` are Anthropic's numbers passed through,\n * where the input count excludes the cache: they are added on top, and `contextTokens` carries\n * the whole prompt. A proxy that reports an OpenAI-style cache field beside\n * them, even at zero (LiteLLM's `prompt_tokens_details.cached_tokens`), has normalised\n * `prompt_tokens` to include them.\n *\n * Counts that cannot fit inside `prompt_tokens` are treated as on top of it whatever their\n * source. A usage with no cache tokens maps exactly as before, with no cache fields.\n */\nexport function parseCompatibleUsage(raw: unknown): Usage | null {\n if (!raw || typeof raw !== 'object') return null;\n const usage = raw as Record<string, unknown>;\n const details =\n usage.prompt_tokens_details && typeof usage.prompt_tokens_details === 'object'\n ? (usage.prompt_tokens_details as Record<string, unknown>)\n : {};\n const tokens = (value: unknown): number =>\n typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;\n const firstCount = (...values: unknown[]): number => {\n for (const value of values) {\n const count = tokens(value);\n if (count > 0) return count;\n }\n return 0;\n };\n const prompt = tokens(usage.prompt_tokens);\n const base: Usage = {\n promptTokens: prompt,\n completionTokens: tokens(usage.completion_tokens),\n // A provider that omits `total_tokens` would otherwise read as zero context pressure.\n totalTokens: tokens(usage.total_tokens) || prompt + tokens(usage.completion_tokens),\n };\n const topRead = tokens(usage.cache_read_input_tokens);\n const topWrite = tokens(usage.cache_creation_input_tokens);\n // Inside `prompt_tokens` by the OpenAI convention: OpenAI, xAI, 9router, OpenRouter, DeepSeek,\n // Moonshot (top-level `cached_tokens`) and DashScope (`cache_creation_input_tokens` in details).\n const normalisedRead = firstCount(\n details.cached_tokens,\n usage.prompt_cache_hit_tokens,\n usage.cached_tokens,\n );\n const normalisedWrite = firstCount(\n details.cache_creation_tokens,\n details.cache_write_tokens,\n details.cache_creation_input_tokens,\n );\n const read = normalisedRead || topRead;\n const write = normalisedWrite || topWrite;\n if (read === 0 && write === 0) return base;\n // Anthropic's top-level names with no OpenAI-style cache field beside them (not even a zero)\n // are Anthropic's numbers passed through, where the input count excludes the cache. A field\n // beside them, even `cached_tokens: 0` on LiteLLM's cold turn, says `prompt_tokens` was\n // normalised to include them.\n const reportsOpenAiCacheField = [\n details.cached_tokens,\n details.cache_creation_tokens,\n details.cache_write_tokens,\n details.cache_creation_input_tokens,\n usage.prompt_cache_hit_tokens,\n usage.cached_tokens,\n ].some((value) => typeof value === 'number');\n const anthropicPassThrough = !reportsOpenAiCacheField && topRead + topWrite > 0;\n if (!anthropicPassThrough && read + write <= prompt) {\n return {\n ...base,\n promptTokens: prompt - read - write,\n cacheReadInputTokens: read,\n cacheCreationInputTokens: write,\n };\n }\n return {\n ...base,\n cacheReadInputTokens: read,\n cacheCreationInputTokens: write,\n contextTokens: prompt + read + write,\n };\n}\n\nexport function convertTools(tools: ToolDefinition[]): Array<{\n type: 'function';\n function: { name: string; description: string; parameters: Record<string, unknown> };\n}> {\n return tools.map((tool) => ({\n type: 'function',\n function: {\n name: tool.name,\n description: tool.description,\n parameters: tool.inputSchema ?? tool.parameters,\n },\n }));\n}\n\nexport async function* chatCompletionStream(\n config: AgentConfig,\n messages: ProviderMessage[],\n tools: ToolDefinition[],\n options?: {\n signal?: AbortSignal;\n onRetry?: (attempt: number, max: number, delayMs: number) => void;\n onStreamStall?: (countdownMs: number) => void;\n onStreamResume?: () => void;\n maxOutputTokens?: number;\n },\n): AsyncGenerator<ProviderStreamEvent> {\n const retry = config.retry;\n const signal = options?.signal;\n const url = `${config.baseUrl}/chat/completions`;\n\n const body: Record<string, unknown> = {\n model: config.model,\n messages: flattenMessages(messages),\n stream: true,\n // Request token usage in the final SSE chunk so we can track cost.\n stream_options: { include_usage: true },\n };\n if (options?.maxOutputTokens || config.maxTokensExplicit || config.modelInfo?.maxOutputTokens) {\n body.max_tokens = options?.maxOutputTokens ?? config.maxTokens;\n }\n if (config.effortExplicit && config.effort) body.reasoning_effort = config.effort;\n\n // Whether this request expects the model to reason before answering: either we\n // asked it to, or the model's catalog entry declares an effort range. An entry\n // of `false` means the model does not reason at all; an absent entry is unknown,\n // and stays on the chat regime rather than guessing.\n const reasoningEnabled =\n body.reasoning_effort !== undefined || typeof config.modelInfo?.effort === 'object';\n\n if (tools.length > 0) {\n body.tools = convertTools(tools);\n }\n\n log.debug('chatCompletionStream request', {\n model: config.model,\n messageCount: messages.length,\n toolCount: tools.length,\n maxTokens: config.maxTokens,\n });\n\n let response: Response;\n try {\n response = await fetchWithRetry(\n url,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`,\n },\n body: JSON.stringify(body),\n },\n retry,\n signal,\n options?.onRetry,\n log,\n );\n } catch (e) {\n if (signal?.aborted) return;\n log.warn('fetchWithRetry failed', e instanceof Error ? e.message : String(e));\n yield {\n type: 'error',\n error: e instanceof Error ? e.message : String(e),\n errorCode: classifyProviderError(e),\n };\n return;\n }\n\n if (signal?.aborted) {\n await response.body?.cancel();\n return;\n }\n\n log.debug('response received', { status: response.status, ok: response.ok });\n\n if (!response.ok) {\n const errorText = await readErrorBody(response, signal);\n yield {\n type: 'error',\n error: formatApiError(response.status, errorText),\n errorCode: classifyApiError(response.status, errorText),\n upstreamStatus: wrappedUpstreamStatus(response.status, errorText),\n };\n return;\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n yield { type: 'error', error: 'No response body', errorCode: 'protocol_error' };\n return;\n }\n\n const decoder = new TextDecoder();\n let buffer = '';\n const toolCallParts: Array<{\n index: number;\n id: string;\n name: string;\n arguments: string;\n /** Deltas that carried an arguments fragment: a wire format that sends one, vs many. */\n fragments: number;\n }> = [];\n let currentUsage: Usage | null = null;\n let responseModel: string | undefined;\n let responseId: string | undefined;\n const finishReasons = new Set<string>();\n\n const emitToolCalls = function* (): Generator<ProviderStreamEvent> {\n for (const part of [...toolCallParts].sort((a, b) => a.index - b.index)) {\n if (!part.id && !part.name) continue;\n const { arguments: arguments_, unparsedArguments } = parseToolArguments(part.arguments);\n if (unparsedArguments) {\n // The head, not the whole text: enough to see whether the call arrived with its\n // first fragment missing (#260), which is a route's wire format, not the model's JSON.\n log.warn('tool call arguments are not valid JSON', {\n index: part.index,\n id: part.id,\n name: escapeInvisibleCharacters(part.name),\n fragments: part.fragments,\n length: part.arguments.length,\n head: escapeInvisibleCharacters(part.arguments.slice(0, 120)),\n });\n }\n yield {\n type: 'tool_call',\n toolCall: {\n id: part.id || `tool-${part.index}`,\n name: part.name,\n arguments: arguments_,\n ...(unparsedArguments ? { unparsedArguments } : {}),\n },\n };\n }\n };\n\n const emitDone = function* (\n terminal: '[DONE]' | 'finish_reason',\n ): Generator<ProviderStreamEvent> {\n yield* emitToolCalls();\n log.info('stream done', {\n terminal,\n promptTokens: currentUsage?.promptTokens ?? 0,\n completionTokens: currentUsage?.completionTokens ?? 0,\n totalTokens: currentUsage?.totalTokens ?? 0,\n cacheReadInputTokens: currentUsage?.cacheReadInputTokens ?? 0,\n cacheCreationInputTokens: currentUsage?.cacheCreationInputTokens ?? 0,\n });\n yield {\n type: 'done',\n usage: currentUsage ?? undefined,\n ...(responseModel ? { responseModel } : {}),\n ...(responseId ? { responseId } : {}),\n ...(finishReasons.size > 0 ? { finishReasons: [...finishReasons] } : {}),\n };\n };\n\n const processSseLine = function* (line: string): Generator<ProviderStreamEvent, boolean> {\n const trimmed = line.trim();\n if (!trimmed || !trimmed.startsWith('data:')) return false;\n\n const data = trimmed.slice(5).replace(/^ /, '');\n if (data === '[DONE]') {\n yield* emitDone('[DONE]');\n return true;\n }\n\n try {\n const parsed = JSON.parse(data);\n if (typeof parsed.model === 'string') responseModel = parsed.model;\n if (typeof parsed.id === 'string') responseId = parsed.id;\n // OpenAI sends usage on the final chunk when stream_options.include_usage is set.\n const usage = parseCompatibleUsage(parsed.usage);\n if (usage) currentUsage = usage;\n const choice = parsed.choices?.[0];\n if (!choice) return false;\n const finishReason: unknown = choice.finish_reason;\n if (typeof finishReason === 'string' && finishReason) finishReasons.add(finishReason);\n\n const delta = choice.delta as\n | (Record<string, unknown> & {\n content?: string;\n tool_calls?: Array<{\n index?: number;\n id?: string;\n function?: { name?: string; arguments?: string };\n }>;\n })\n | undefined;\n if (!delta) return false;\n\n const reasoning = parseReasoningDelta(delta);\n if (reasoning) {\n // The model is reasoning, whatever the request could not say (#379), so\n // this phase waits out the thinking ceiling. Only this phase does.\n if (retry.thinkingStallTimeoutMs) {\n stallTimeoutMs = Math.max(stallTimeoutMs, retry.thinkingStallTimeoutMs);\n }\n log.debug('stream reasoning', { len: reasoning.length });\n yield { type: 'reasoning', reasoning };\n }\n\n if (delta.content) {\n // The model has started answering, so the chat ceiling bounds this\n // phase again — reasoning can resume and promote the ceiling back.\n stallTimeoutMs = startingStallTimeoutMs;\n log.debug('stream text', { len: delta.content.length });\n yield { type: 'text', content: delta.content };\n }\n\n if (delta.tool_calls) {\n // A tool call is the answer phase too, and the one where a stall costs\n // most: the turn is waiting on a function call that is not coming.\n stallTimeoutMs = startingStallTimeoutMs;\n for (const tc of delta.tool_calls) {\n const explicitIndex = Number.isInteger(tc.index) ? tc.index : undefined;\n let index = explicitIndex;\n\n if (index === undefined && tc.id) {\n index = toolCallParts.find((part) => part.id === tc.id)?.index;\n }\n if (index === undefined) {\n index = toolCallParts.length;\n }\n\n let part = toolCallParts.find((p) => p.index === index);\n if (!part) {\n part = { index, id: '', name: '', arguments: '', fragments: 0 };\n toolCallParts.push(part);\n }\n\n if (tc.id) part.id = tc.id;\n if (tc.function?.name) part.name = tc.function.name;\n if (tc.function?.arguments) {\n // The head of each call's first fragment, not of every one: a call that arrives\n // missing its opening `{\"filePath\": ` is visible here (#260) and nowhere else.\n // Escaped, because the text is the model's and a debug log on stderr may be a\n // terminal; behind the flag, so a large streamed Write does no string work and\n // writes no lines when debugging is off.\n if (part.fragments === 0 && isDebugEnabled()) {\n log.debug('stream tool_call delta', {\n index,\n id: tc.id,\n name: escapeInvisibleCharacters(tc.function.name ?? ''),\n argumentsLength: tc.function.arguments.length,\n argumentsHead: escapeInvisibleCharacters(tc.function.arguments.slice(0, 120)),\n });\n }\n part.arguments += tc.function.arguments;\n part.fragments++;\n }\n }\n }\n } catch {\n // Skip unparseable lines.\n }\n\n return false;\n };\n\n // Stream stall detection: if no data arrives for the ceiling in force, call\n // the onStreamStall callback and yield a visible error instead of leaving the\n // TUI stuck in a pending read forever.\n //\n // The ceiling is chosen per stream and follows its phase, not just per\n // request. It starts at what the request can already prove: the thinking\n // ceiling when we asked for reasoning or the model's catalog entry declares an\n // effort range, the chat-tuned `streamStallTimeoutMs` otherwise. What a\n // request cannot prove is whether the model reasons, because plenty of routes\n // serve a reasoning model Book has no catalog entry for, and send it no\n // `reasoning_effort` — there is no evidence before the stream opens, so the\n // first thinking pause lands on the chat ceiling and a healthy request is\n // cancelled mid-thought and reported as a stalled stream (#379), the single\n // most common way a run \"just stops\". A reasoning delta is that evidence\n // arriving late, so it promotes that phase to the thinking ceiling.\n //\n // A content or tool-call delta ends the thinking phase, and the ceiling goes\n // back to where it started. Keeping the promotion for the rest of the stream\n // would apply fifteen minutes to the answer phase, where a dead socket is the\n // commonest fault by far and is worth noticing in twenty seconds; reasoning\n // that resumes mid-answer promotes the ceiling again. A stream that never\n // reasons keeps the chat ceiling throughout, and a request with an explicit\n // effort or a catalogued one is unchanged.\n const startingStallTimeoutMs =\n reasoningEnabled && retry.thinkingStallTimeoutMs\n ? Math.max(retry.streamStallTimeoutMs, retry.thinkingStallTimeoutMs)\n : retry.streamStallTimeoutMs;\n let stallTimeoutMs = startingStallTimeoutMs;\n\n try {\n while (true) {\n const result = await readStreamChunk(reader, signal, stallTimeoutMs);\n\n if (result.tag === 'abort') {\n try {\n await reader.cancel();\n } catch {\n // ignore cancellation failures\n }\n return;\n }\n\n if (result.tag === 'stall') {\n log.warn('stream stalled', { timeoutMs: stallTimeoutMs });\n options?.onStreamStall?.(stallTimeoutMs);\n try {\n await reader.cancel();\n } catch {\n // ignore cancellation failures\n }\n yield {\n type: 'error',\n error: 'Stream stalled: no data received for ' + stallTimeoutMs + 'ms',\n errorCode: 'stream_stall',\n };\n return;\n }\n\n if (result.done) break;\n\n const value = result.value;\n if (!value) continue;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (yield* processSseLine(line)) return;\n }\n }\n\n buffer += decoder.decode();\n for (const line of buffer.split('\\n')) {\n if (yield* processSseLine(line)) return;\n }\n\n // Some OpenAI-compatible gateways omit [DONE] and close after a finish_reason chunk.\n if (finishReasons.size > 0) {\n yield* emitDone('finish_reason');\n return;\n }\n } catch (e) {\n if (signal?.aborted) return;\n // Unexpected stream error — surface it instead of silently completing,\n // otherwise the TUI can show an empty completed assistant message.\n yield {\n type: 'error',\n error: e instanceof Error ? e.message : String(e),\n errorCode: classifyProviderError(e),\n };\n return;\n } finally {\n try {\n reader.releaseLock();\n } catch {\n // ignore: the reader may already have been cancelled/released\n }\n }\n\n yield {\n type: 'error',\n error: 'Provider stream ended before its terminal event.',\n errorCode: 'transport_interrupted',\n };\n}\n"],"mappings":"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","names":["message","delay","part"]}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import {
|
|
2
|
+
systemClock
|
|
3
|
+
} from "./chunk-RT5SATF7.js";
|
|
4
|
+
import {
|
|
5
|
+
buildChildEnv
|
|
6
|
+
} from "./chunk-XHAO3K7I.js";
|
|
7
|
+
import {
|
|
8
|
+
toolFailure,
|
|
9
|
+
toolSuccess
|
|
10
|
+
} from "./chunk-224FJZLH.js";
|
|
11
|
+
|
|
12
|
+
// src/jobs/process-tree.ts
|
|
13
|
+
import { execFile } from "child_process";
|
|
14
|
+
|
|
15
|
+
// src/system32.ts
|
|
16
|
+
import { existsSync } from "fs";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
function system32Executable(name, platform = process.platform, env = process.env) {
|
|
19
|
+
if (platform !== "win32") return name;
|
|
20
|
+
const fileName = name.toLowerCase().endsWith(".exe") ? name : `${name}.exe`;
|
|
21
|
+
const systemRoot = env.SystemRoot || env.windir || "C:\\Windows";
|
|
22
|
+
const targetPath = join(systemRoot, "System32", fileName);
|
|
23
|
+
return existsSync(targetPath) ? targetPath : name;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/jobs/process-tree.ts
|
|
27
|
+
var GROUP_POLL_INTERVAL_MS = 25;
|
|
28
|
+
var TERMINATE_GRACE_MS = 1500;
|
|
29
|
+
function signalTargetExists(target) {
|
|
30
|
+
try {
|
|
31
|
+
process.kill(target, 0);
|
|
32
|
+
return true;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return error.code === "EPERM";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function isProcessAlive(pid) {
|
|
38
|
+
return pid === void 0 ? false : signalTargetExists(pid);
|
|
39
|
+
}
|
|
40
|
+
function isProcessGroupAlive(pgid) {
|
|
41
|
+
if (pgid === void 0 || process.platform === "win32") return false;
|
|
42
|
+
return signalTargetExists(-pgid);
|
|
43
|
+
}
|
|
44
|
+
function signalProcessGroup(proc, pgid, signal) {
|
|
45
|
+
try {
|
|
46
|
+
process.kill(-pgid, signal);
|
|
47
|
+
} catch {
|
|
48
|
+
try {
|
|
49
|
+
proc?.kill(signal);
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function waitForProcessGroupExit(pgid, timeoutMs, clock = systemClock) {
|
|
55
|
+
const deadline = clock.monotonicNowMs() + timeoutMs;
|
|
56
|
+
while (isProcessGroupAlive(pgid)) {
|
|
57
|
+
if (clock.monotonicNowMs() >= deadline) return false;
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, GROUP_POLL_INTERVAL_MS));
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
function waitForProcessClose(proc, timeoutMs) {
|
|
63
|
+
if (proc.exitCode !== null || proc.signalCode !== null) return Promise.resolve(true);
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
const timer = setTimeout(() => {
|
|
66
|
+
cleanup();
|
|
67
|
+
resolve(proc.exitCode !== null || proc.signalCode !== null);
|
|
68
|
+
}, timeoutMs);
|
|
69
|
+
const onClose = () => {
|
|
70
|
+
cleanup();
|
|
71
|
+
resolve(true);
|
|
72
|
+
};
|
|
73
|
+
const cleanup = () => {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
proc.off("close", onClose);
|
|
76
|
+
};
|
|
77
|
+
proc.once("close", onClose);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async function runTaskkill(pid, timeoutMs = TERMINATE_GRACE_MS) {
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
execFile(
|
|
83
|
+
system32Executable("taskkill"),
|
|
84
|
+
["/PID", String(pid), "/T", "/F"],
|
|
85
|
+
// Book's own helper, but it is a command like any other and is handed no more of Book's
|
|
86
|
+
// environment than the next one would be.
|
|
87
|
+
{ windowsHide: true, timeout: timeoutMs, env: buildChildEnv() },
|
|
88
|
+
(error) => resolve(!error)
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async function terminateWindowsProcessTree(proc, pid, signal, treeKill = runTaskkill) {
|
|
93
|
+
const alreadyExited = proc.exitCode !== null || proc.signalCode !== null;
|
|
94
|
+
if (alreadyExited) return true;
|
|
95
|
+
if (await treeKill(pid)) return true;
|
|
96
|
+
try {
|
|
97
|
+
proc.kill(signal);
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
async function terminateProcessTree(proc, pid, hasClosed) {
|
|
103
|
+
if (!proc || pid === void 0) return true;
|
|
104
|
+
if (process.platform !== "win32") {
|
|
105
|
+
signalProcessGroup(proc, pid, "SIGTERM");
|
|
106
|
+
if (await waitForProcessGroupExit(pid, TERMINATE_GRACE_MS)) return true;
|
|
107
|
+
signalProcessGroup(proc, pid, "SIGKILL");
|
|
108
|
+
return waitForProcessGroupExit(pid, TERMINATE_GRACE_MS);
|
|
109
|
+
}
|
|
110
|
+
const confirmed = await terminateWindowsProcessTree(proc, pid, "SIGTERM");
|
|
111
|
+
if (await hasClosed(TERMINATE_GRACE_MS)) return confirmed;
|
|
112
|
+
try {
|
|
113
|
+
proc.kill("SIGKILL");
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return await hasClosed(TERMINATE_GRACE_MS) && confirmed;
|
|
118
|
+
}
|
|
119
|
+
async function terminateForegroundProcess(proc) {
|
|
120
|
+
await terminateProcessTree(proc, proc.pid, (timeoutMs) => waitForProcessClose(proc, timeoutMs));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/tools/git.ts
|
|
124
|
+
import { execFile as execFile2 } from "child_process";
|
|
125
|
+
var GIT_HARDENING_ARGS = [
|
|
126
|
+
"-c",
|
|
127
|
+
"core.fsmonitor=false",
|
|
128
|
+
"-c",
|
|
129
|
+
"core.pager=cat",
|
|
130
|
+
"-c",
|
|
131
|
+
"core.hooksPath=",
|
|
132
|
+
"-c",
|
|
133
|
+
"core.untrackedCache=false",
|
|
134
|
+
"-c",
|
|
135
|
+
"gc.auto=0",
|
|
136
|
+
"-c",
|
|
137
|
+
"maintenance.auto=false",
|
|
138
|
+
"-c",
|
|
139
|
+
"log.showSignature=false",
|
|
140
|
+
"--no-optional-locks"
|
|
141
|
+
];
|
|
142
|
+
var HARDENED_DIFF_ARGS = ["--no-ext-diff", "--no-textconv"];
|
|
143
|
+
function hardenedGitEnv() {
|
|
144
|
+
return { GIT_PAGER: "cat", GIT_TERMINAL_PROMPT: "0" };
|
|
145
|
+
}
|
|
146
|
+
function hardenedGitArgs(args) {
|
|
147
|
+
return [...GIT_HARDENING_ARGS, ...args];
|
|
148
|
+
}
|
|
149
|
+
async function runGit(args, ctx, execute = execFile2) {
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
const options = {
|
|
152
|
+
cwd: ctx.workspaceRoot,
|
|
153
|
+
encoding: "utf-8",
|
|
154
|
+
timeout: 3e4,
|
|
155
|
+
signal: ctx.signal,
|
|
156
|
+
// A hook git runs is a project command like any other, so it must not inherit a NODE_ENV
|
|
157
|
+
// Book defaulted for its own renderer.
|
|
158
|
+
env: buildChildEnv(process.env, ctx.env)
|
|
159
|
+
};
|
|
160
|
+
execute("git", args, options, (error, stdout, stderr) => {
|
|
161
|
+
if (!error) {
|
|
162
|
+
resolve({ success: true, output: stdout || "(no output)" });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (ctx.signal?.aborted) {
|
|
166
|
+
resolve({ success: false, output: "", error: "CANCELLED: Git command was cancelled" });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
resolve({
|
|
170
|
+
success: false,
|
|
171
|
+
output: "",
|
|
172
|
+
error: stderr || error.message || "Git command failed"
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function readOnlyGit(args, ctx) {
|
|
178
|
+
return runGit(hardenedGitArgs(args), {
|
|
179
|
+
...ctx,
|
|
180
|
+
env: { ...ctx.env, ...hardenedGitEnv() }
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function gitResult(result) {
|
|
184
|
+
return result.success ? toolSuccess(result.output) : toolFailure(result.error ?? "Git command failed", { content: result.output });
|
|
185
|
+
}
|
|
186
|
+
async function gitStatus(_args, ctx) {
|
|
187
|
+
const result = await readOnlyGit(READ_ONLY_GIT_ARGS.GitStatus, ctx);
|
|
188
|
+
return gitResult(result);
|
|
189
|
+
}
|
|
190
|
+
async function gitDiff(_args, ctx) {
|
|
191
|
+
const result = await readOnlyGit(READ_ONLY_GIT_ARGS.GitDiff, ctx);
|
|
192
|
+
return gitResult(result);
|
|
193
|
+
}
|
|
194
|
+
async function gitLog(_args, ctx) {
|
|
195
|
+
const result = await readOnlyGit(READ_ONLY_GIT_ARGS.GitLog, ctx);
|
|
196
|
+
return gitResult(result);
|
|
197
|
+
}
|
|
198
|
+
async function gitCommit(args, ctx) {
|
|
199
|
+
const message = args.message;
|
|
200
|
+
const result = await runGit(["commit", "-m", message], ctx);
|
|
201
|
+
return gitResult(result);
|
|
202
|
+
}
|
|
203
|
+
async function gitBranch(_args, ctx) {
|
|
204
|
+
const result = await readOnlyGit(READ_ONLY_GIT_ARGS.GitBranch, ctx);
|
|
205
|
+
return gitResult(result);
|
|
206
|
+
}
|
|
207
|
+
var READ_ONLY_GIT_ARGS = {
|
|
208
|
+
GitStatus: ["status", "--short"],
|
|
209
|
+
GitDiff: ["diff", ...HARDENED_DIFF_ARGS],
|
|
210
|
+
GitLog: ["log", "--oneline", "-20", "--no-show-signature"],
|
|
211
|
+
GitBranch: ["branch", "-a"]
|
|
212
|
+
};
|
|
213
|
+
var gitTools = [
|
|
214
|
+
{
|
|
215
|
+
name: "GitStatus",
|
|
216
|
+
idempotent: true,
|
|
217
|
+
policy: { concurrency: "parallel" },
|
|
218
|
+
description: "Show the working tree status (git status --short)",
|
|
219
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
220
|
+
execute: gitStatus
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: "GitDiff",
|
|
224
|
+
idempotent: true,
|
|
225
|
+
policy: { concurrency: "parallel" },
|
|
226
|
+
description: "Show changes between commits, commit and working tree, etc.",
|
|
227
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
228
|
+
execute: gitDiff
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: "GitLog",
|
|
232
|
+
idempotent: true,
|
|
233
|
+
policy: { concurrency: "parallel" },
|
|
234
|
+
description: "Show recent commit logs (last 20, oneline)",
|
|
235
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
236
|
+
execute: gitLog
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: "GitCommit",
|
|
240
|
+
description: "Create a new commit with a message",
|
|
241
|
+
parameters: {
|
|
242
|
+
type: "object",
|
|
243
|
+
properties: { message: { type: "string", description: "Commit message" } },
|
|
244
|
+
required: ["message"]
|
|
245
|
+
},
|
|
246
|
+
execute: gitCommit
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "GitBranch",
|
|
250
|
+
idempotent: true,
|
|
251
|
+
policy: { concurrency: "parallel" },
|
|
252
|
+
description: "List branches",
|
|
253
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
254
|
+
execute: gitBranch
|
|
255
|
+
}
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
export {
|
|
259
|
+
TERMINATE_GRACE_MS,
|
|
260
|
+
isProcessAlive,
|
|
261
|
+
signalProcessGroup,
|
|
262
|
+
waitForProcessGroupExit,
|
|
263
|
+
waitForProcessClose,
|
|
264
|
+
runTaskkill,
|
|
265
|
+
terminateProcessTree,
|
|
266
|
+
terminateForegroundProcess,
|
|
267
|
+
HARDENED_DIFF_ARGS,
|
|
268
|
+
hardenedGitEnv,
|
|
269
|
+
hardenedGitArgs,
|
|
270
|
+
runGit,
|
|
271
|
+
gitTools
|
|
272
|
+
};
|
|
273
|
+
//# sourceMappingURL=chunk-C7ADUNPR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/jobs/process-tree.ts","../src/system32.ts","../src/tools/git.ts"],"sourcesContent":["/**\n * Process-liveness helpers shared by the shell manager and the detached job runner.\n *\n * A POSIX process group outlives its leader. Background commands run through `sh -c`, and the\n * shell forks the real worker instead of exec'ing it whenever it cannot hand its own process\n * over, so the direct child can exit — for instance by taking the SIGTERM a worker ignores —\n * while that worker keeps running in the same group. Termination therefore has to be judged on\n * group membership; the direct child's own exit proves nothing about the tree behind it.\n */\n\nimport { execFile, type ChildProcess } from 'node:child_process';\nimport { systemClock, type Clock } from '../clock.js';\nimport { buildChildEnv } from '../child-env.js';\nimport { system32Executable } from '../system32.js';\n\nconst GROUP_POLL_INTERVAL_MS = 25;\n/** How long a process group is given between SIGTERM and SIGKILL, and how long taskkill may take. */\nexport const TERMINATE_GRACE_MS = 1_500;\n\n/** A signal target exists when it accepts signal 0, or rejects it as another user's. */\nfunction signalTargetExists(target: number): boolean {\n try {\n process.kill(target, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\nexport function isProcessAlive(pid: number | undefined): boolean {\n return pid === undefined ? false : signalTargetExists(pid);\n}\n\n/** True while any process remains in the group, whether or not the leader is one of them. */\nexport function isProcessGroupAlive(pgid: number | undefined): boolean {\n if (pgid === undefined || process.platform === 'win32') return false;\n return signalTargetExists(-pgid);\n}\n\n/** Signal every process in the group, falling back to the direct child when it has no group. */\nexport function signalProcessGroup(\n proc: { kill(signal: NodeJS.Signals): boolean } | undefined,\n pgid: number,\n signal: NodeJS.Signals,\n): void {\n try {\n process.kill(-pgid, signal);\n } catch {\n try {\n proc?.kill(signal);\n } catch {\n // The tree may have exited between the group signal and the direct-child fallback.\n }\n }\n}\n\n/**\n * Resolve `true` once the group holds no processes, or `false` when the bound elapses first.\n *\n * `kill(-pgid, 0)` also succeeds for a member that has exited but has not been reaped yet, so a\n * group reads as alive until its reaper runs. That bound is deliberate: a zombie holds no ports,\n * file handles, or CPU, and reporting \"not stopped\" for the moments before it is reaped is\n * honest, where trusting the direct child's exit is not.\n */\nexport async function waitForProcessGroupExit(\n pgid: number,\n timeoutMs: number,\n // A kill bound is a duration: on the wall clock, a backwards step here waits\n // past the bound and a forwards one abandons a group that was about to exit.\n clock: Clock = systemClock,\n): Promise<boolean> {\n const deadline = clock.monotonicNowMs() + timeoutMs;\n while (isProcessGroupAlive(pgid)) {\n if (clock.monotonicNowMs() >= deadline) return false;\n await new Promise((resolve) => setTimeout(resolve, GROUP_POLL_INTERVAL_MS));\n }\n return true;\n}\n\nexport function waitForProcessClose(proc: ChildProcess, timeoutMs: number): Promise<boolean> {\n if (proc.exitCode !== null || proc.signalCode !== null) return Promise.resolve(true);\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n cleanup();\n resolve(proc.exitCode !== null || proc.signalCode !== null);\n }, timeoutMs);\n const onClose = () => {\n cleanup();\n resolve(true);\n };\n const cleanup = () => {\n clearTimeout(timer);\n proc.off('close', onClose);\n };\n proc.once('close', onClose);\n });\n}\n\ntype WindowsTreeKill = (pid: number) => Promise<boolean>;\n\n/**\n * `taskkill /T /F` on the tree rooted at `pid`, resolved through `system32Executable` so a\n * workspace on the PATH cannot stand in for it. Reports whether the kill actually ran. The bound is\n * a parameter because a caller that exits straight afterwards has nobody to escalate for it and can\n * afford to wait far longer than one that does.\n */\nexport async function runTaskkill(\n pid: number,\n timeoutMs: number = TERMINATE_GRACE_MS,\n): Promise<boolean> {\n return new Promise((resolve) => {\n execFile(\n system32Executable('taskkill'),\n ['/PID', String(pid), '/T', '/F'],\n // Book's own helper, but it is a command like any other and is handed no more of Book's\n // environment than the next one would be.\n { windowsHide: true, timeout: timeoutMs, env: buildChildEnv() },\n (error) => resolve(!error),\n );\n });\n}\n\nexport async function terminateWindowsProcessTree(\n proc: ChildProcess,\n pid: number,\n signal: NodeJS.Signals,\n treeKill: WindowsTreeKill = runTaskkill,\n): Promise<boolean> {\n const alreadyExited = proc.exitCode !== null || proc.signalCode !== null;\n if (alreadyExited) return true;\n if (await treeKill(pid)) return true;\n try {\n proc.kill(signal);\n } catch {\n // The process may have exited between taskkill and the direct-child fallback.\n }\n return false;\n}\n\n/**\n * Escalate SIGTERM → SIGKILL across the whole tree and report whether it is really gone.\n *\n * On POSIX the direct child is the `sh -c` wrapper, which dies from SIGTERM even when the worker\n * it forked ignores it, so its exit says nothing about the tree — success is judged on whether\n * the process group still holds anything. On Windows `taskkill /T /F` covers the tree, so the\n * direct child's close speaks for the tree only when taskkill actually ran.\n */\nexport async function terminateProcessTree(\n proc: ChildProcess | undefined,\n pid: number | undefined,\n hasClosed: (timeoutMs: number) => Promise<boolean>,\n): Promise<boolean> {\n if (!proc || pid === undefined) return true;\n if (process.platform !== 'win32') {\n signalProcessGroup(proc, pid, 'SIGTERM');\n if (await waitForProcessGroupExit(pid, TERMINATE_GRACE_MS)) return true;\n signalProcessGroup(proc, pid, 'SIGKILL');\n return waitForProcessGroupExit(pid, TERMINATE_GRACE_MS);\n }\n const confirmed = await terminateWindowsProcessTree(proc, pid, 'SIGTERM');\n if (await hasClosed(TERMINATE_GRACE_MS)) return confirmed;\n try {\n proc.kill('SIGKILL');\n } catch {\n return false;\n }\n return (await hasClosed(TERMINATE_GRACE_MS)) && confirmed;\n}\n\nexport async function terminateForegroundProcess(proc: ChildProcess): Promise<void> {\n await terminateProcessTree(proc, proc.pid, (timeoutMs) => waitForProcessClose(proc, timeoutMs));\n}\n","import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/**\n * Resolve an executable from %SystemRoot%\\System32 on Windows.\n *\n * When System32 is missing from PATH (e.g. Git Bash, MSYS, or sanitized subprocess environments),\n * bare executable names fail with ENOENT under execFile / spawn with shell: false.\n * Off Windows, or if the executable is not found in System32, falls back to the bare name.\n */\nexport function system32Executable(\n name: string,\n platform: NodeJS.Platform = process.platform,\n env: NodeJS.ProcessEnv = process.env,\n): string {\n if (platform !== 'win32') return name;\n const fileName = name.toLowerCase().endsWith('.exe') ? name : `${name}.exe`;\n const systemRoot = env.SystemRoot || env.windir || 'C:\\\\Windows';\n const targetPath = join(systemRoot, 'System32', fileName);\n return existsSync(targetPath) ? targetPath : name;\n}\n","import { execFile } from 'child_process';\nimport type { ToolDefinition, ToolContext, ToolResult } from '../types/tools.js';\nimport { buildChildEnv } from '../child-env.js';\nimport { toolFailure, toolSuccess } from './result.js';\n\ntype ExecFile = typeof execFile;\n\n/**\n * `-c` overrides the Git tools and the managed-agent subsystem carry, so a checkout's own\n * configuration cannot turn a git call into something that runs a program.\n *\n * The configuration belongs to the checkout, and a checkout may have come from an archive, a\n * shared directory, or someone else's machine, so any of these can be set without the operator\n * having made that choice themselves:\n *\n * - `core.fsmonitor` — a command `git status` executes to check whether the tree changed. This\n * is the sharpest one: a status call is supposed to be inert and it will happily run whatever\n * the repository named.\n * - `log.showSignature` — `git log` then verifies every signature it prints, and verification\n * runs `gpg.program`, another program the repository names. `GitDiff` and `GitStatus` do not\n * reach it, but `GitLog` does on every commit that carries a `gpgsig` header.\n * - `core.pager` — a command Git runs to page its output. `execFile` never allocates a TTY, so\n * git normally skips it, but the setting is read before that decision.\n * - `core.hooksPath` — where hooks live, and where most of the effect of this list lands: a\n * `post-checkout` on `worktree add`, a `pre-commit` on an agent's commit, a\n * `reference-transaction` on every ref Book moves, a `prepare-commit-msg` on the\n * `cherry-pick` that applies a candidate. The four read-only tools here run no hook at all, so\n * for them this is belt and braces against a future subcommand; for\n * `src/agents/git-isolation.ts`, whose calls are writes nothing asks about (#348), switching\n * them off is the decision. It is **not** applied to `GitCommit` or to `runGit` generally: on\n * the operator's own commit it would silently disable a hook they installed, and their code\n * silently not running is a worse failure than the one this list prevents.\n * - `core.untrackedCache`, `gc.auto`, `maintenance.auto` — background writers. `gc.auto=0` is not\n * the whole of it: `maintenance.auto` is separately on by default, so every commit spawns\n * `git maintenance run --auto`, which repacks and can run whatever strategies the configuration\n * registers while Book is moving refs. `--no-optional-locks` below covers the index lock these\n * need.\n *\n * `--no-optional-locks` is a top-level flag, not a config key, so it is added separately: it stops\n * Git taking `.git/index.lock` for a call that only reads, which a read-only worktree and a\n * concurrent `book` session would otherwise fight over.\n *\n * Each of these was checked against a real `git`, not read off this list — see\n * `git.test.ts`. Anything not reached through a non-TTY `execFile` (`column.ui`, `pager.log`,\n * `diff.external`) is not here either, because the flags that close those are per-command and\n * belong in {@link HARDENED_DIFF_ARGS}, which every hardened caller that runs `git diff` uses.\n */\nconst GIT_HARDENING_ARGS: readonly string[] = [\n '-c',\n 'core.fsmonitor=false',\n '-c',\n 'core.pager=cat',\n '-c',\n 'core.hooksPath=',\n '-c',\n 'core.untrackedCache=false',\n '-c',\n 'gc.auto=0',\n '-c',\n 'maintenance.auto=false',\n '-c',\n 'log.showSignature=false',\n '--no-optional-locks',\n];\n\n/**\n * The per-command flags that close the two remaining routes a checkout owns for producing a diff,\n * for every hardened caller that runs `git diff`: `diff.external` names a program to produce it,\n * and a `.gitattributes` `textconv` line names one per file. They are command flags rather than\n * configuration, which is why they are not in {@link GIT_HARDENING_ARGS}, and they are on the\n * command as well as in the config so the guarantee does not rest on the `-c` overrides being\n * consulted for the subcommand.\n */\nexport const HARDENED_DIFF_ARGS = ['--no-ext-diff', '--no-textconv'] as const;\n\n/**\n * The environment every hardened call carries: a pager, so a checkout cannot name a program git\n * pages its output with, and a credential prompt it cannot sit waiting on. Callers that have an\n * environment of their own to merge (a tool's `ToolContext.env`, a temporary `GIT_INDEX_FILE`,\n * git isolation's commit identity) spread this into it; the pager and the prompt are the floor,\n * not a replacement.\n *\n * The return type is `Record<string, string>` rather than `NodeJS.ProcessEnv`: a caller's\n * environment is `Record<string, string>` too, and spreading a wider one into it would make the\n * whole object `string | undefined` where nothing may be undefined.\n */\nexport function hardenedGitEnv(): Record<string, string> {\n return { GIT_PAGER: 'cat', GIT_TERMINAL_PROMPT: '0' };\n}\n\n/**\n * The hardening flags in front of a caller's own arguments; see {@link GIT_HARDENING_ARGS}.\n *\n * Four callers, and they read this differently. The read-only Git tools ({@link readOnlyGit}) take\n * it because nothing asks before they run, so a checkout must not be able to start a program\n * through one. `/review`'s target (`src/review/target.ts`) and the TUI status poll\n * (`src/tui/hooks/useGitStatus.ts`) are the same: reports and background polls that fire with no\n * prompt. Git isolation (`src/agents/git-isolation.ts`) is the one that takes it on writes as\n * well — the agent's commit, the cherry-pick, `worktree add`, `update-ref` — where hooks being\n * off is a decision (#348) rather than a consequence: that work is Book's, in Book's worktree,\n * and a hook the operator installed is theirs to decide when it runs. The operator's own commits\n * keep their hooks, through {@link runGit} below.\n */\nexport function hardenedGitArgs(args: readonly string[]): string[] {\n return [...GIT_HARDENING_ARGS, ...args];\n}\n\n/**\n * `runGit` runs a caller's arguments as given, and the caller chooses.\n *\n * The hardening is a property of the calls that are *not* the operator's own work, not of every\n * git call this module makes. `GitCommit` is the operator's own work, and it is the only\n * mutating tool here: it comes through this function with its own argv and the environment the\n * user configured, exactly as it did before the hardening existed, so a `pre-commit` or\n * `commit-msg` hook still runs. Silently not running the user's hook would be a worse failure\n * than the one the hardening exists to prevent. The read-only tools go through\n * {@link readOnlyGit}, and a caller whose work is Book's own builds its argv with\n * {@link hardenedGitArgs} directly.\n */\nexport async function runGit(\n args: string[],\n ctx: ToolContext,\n execute: ExecFile = execFile,\n): Promise<{ success: boolean; output: string; error?: string }> {\n return new Promise((resolve) => {\n const options = {\n cwd: ctx.workspaceRoot,\n encoding: 'utf-8' as const,\n timeout: 30_000,\n signal: ctx.signal,\n // A hook git runs is a project command like any other, so it must not inherit a NODE_ENV\n // Book defaulted for its own renderer.\n env: buildChildEnv(process.env, ctx.env),\n };\n\n execute('git', args, options, (error, stdout, stderr) => {\n if (!error) {\n resolve({ success: true, output: stdout || '(no output)' });\n return;\n }\n\n if (ctx.signal?.aborted) {\n resolve({ success: false, output: '', error: 'CANCELLED: Git command was cancelled' });\n return;\n }\n\n resolve({\n success: false,\n output: '',\n error: stderr || error.message || 'Git command failed',\n });\n });\n });\n}\n\n/** A read-only report: hardened, and with the pager and credential prompt it cannot wait on. */\nfunction readOnlyGit(args: readonly string[], ctx: ToolContext) {\n return runGit(hardenedGitArgs(args), {\n ...ctx,\n env: { ...ctx.env, ...hardenedGitEnv() },\n });\n}\n\nfunction gitResult(result: Awaited<ReturnType<typeof runGit>>): ToolResult {\n return result.success\n ? toolSuccess(result.output)\n : toolFailure(result.error ?? 'Git command failed', { content: result.output });\n}\n\nasync function gitStatus(_args: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {\n const result = await readOnlyGit(READ_ONLY_GIT_ARGS.GitStatus, ctx);\n return gitResult(result);\n}\n\nasync function gitDiff(_args: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {\n const result = await readOnlyGit(READ_ONLY_GIT_ARGS.GitDiff, ctx);\n return gitResult(result);\n}\n\nasync function gitLog(_args: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {\n const result = await readOnlyGit(READ_ONLY_GIT_ARGS.GitLog, ctx);\n return gitResult(result);\n}\n\nasync function gitCommit(args: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {\n const message = args.message as string;\n const result = await runGit(['commit', '-m', message], ctx);\n return gitResult(result);\n}\n\nasync function gitBranch(_args: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {\n const result = await readOnlyGit(READ_ONLY_GIT_ARGS.GitBranch, ctx);\n return gitResult(result);\n}\n\n/**\n * The arguments the four read-only tools run.\n *\n * These are the tools `permissions.ts` lets through without a prompt, so their whole security\n * argument rests on two things: they read the workspace and change nothing, and the repository's\n * own configuration cannot make them execute a program (see {@link GIT_HARDENING_ARGS}). Exported\n * so that argument can be asserted per tool rather than taken on trust.\n *\n * `GitDiff` carries {@link HARDENED_DIFF_ARGS}, which close the two remaining routes a checkout\n * owns: `diff.external` names a program to produce the diff, and a `.gitattributes` `textconv`\n * line names one per file. `GitLog` carries `--no-show-signature` for the same reason\n * `log.showSignature=false` is in the hardening: verifying a signature runs `gpg.program`. The flag\n * is on the command as well as in the config so the guarantee does not rest on the `-c` override\n * being consulted for this subcommand. `GitCommit` is deliberately not here — it is a mutation,\n * still asks, and runs with the user's own hooks (see {@link runGit}).\n */\nexport const READ_ONLY_GIT_ARGS = {\n GitStatus: ['status', '--short'],\n GitDiff: ['diff', ...HARDENED_DIFF_ARGS],\n GitLog: ['log', '--oneline', '-20', '--no-show-signature'],\n GitBranch: ['branch', '-a'],\n} as const satisfies Readonly<Record<string, string[]>>;\n\nexport const gitTools: ToolDefinition[] = [\n {\n name: 'GitStatus',\n idempotent: true,\n policy: { concurrency: 'parallel' },\n description: 'Show the working tree status (git status --short)',\n parameters: { type: 'object', properties: {}, required: [] },\n execute: gitStatus,\n },\n {\n name: 'GitDiff',\n idempotent: true,\n policy: { concurrency: 'parallel' },\n description: 'Show changes between commits, commit and working tree, etc.',\n parameters: { type: 'object', properties: {}, required: [] },\n execute: gitDiff,\n },\n {\n name: 'GitLog',\n idempotent: true,\n policy: { concurrency: 'parallel' },\n description: 'Show recent commit logs (last 20, oneline)',\n parameters: { type: 'object', properties: {}, required: [] },\n execute: gitLog,\n },\n {\n name: 'GitCommit',\n description: 'Create a new commit with a message',\n parameters: {\n type: 'object',\n properties: { message: { type: 'string', description: 'Commit message' } },\n required: ['message'],\n },\n execute: gitCommit,\n },\n {\n name: 'GitBranch',\n idempotent: true,\n policy: { concurrency: 'parallel' },\n description: 'List branches',\n parameters: { type: 'object', properties: {}, required: [] },\n execute: gitBranch,\n },\n];\n"],"mappings":"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","names":["execFile","execFile"]}
|
|
@@ -1,70 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseFrontmatter
|
|
3
|
+
} from "./chunk-KXAYUIKP.js";
|
|
1
4
|
import {
|
|
2
5
|
applyShellInjections,
|
|
3
6
|
assertProjectCommandApproved,
|
|
4
7
|
extractShellInjections
|
|
5
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-HQI7NYGX.js";
|
|
9
|
+
import {
|
|
10
|
+
buildChildEnv
|
|
11
|
+
} from "./chunk-XHAO3K7I.js";
|
|
6
12
|
import {
|
|
7
13
|
resolveBookHome
|
|
8
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-ZPSZZUWF.js";
|
|
9
15
|
|
|
10
16
|
// src/commands/loader.ts
|
|
11
17
|
import { readFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
12
18
|
import { join, extname } from "path";
|
|
13
19
|
|
|
14
|
-
// src/frontmatter.ts
|
|
15
|
-
function parseFrontmatter(raw) {
|
|
16
|
-
const normalized = raw.replace(/\r\n?/g, "\n");
|
|
17
|
-
const lines = normalized.split("\n");
|
|
18
|
-
if (lines[0]?.trim() !== "---") {
|
|
19
|
-
return { body: raw, frontmatter: {} };
|
|
20
|
-
}
|
|
21
|
-
const endIdx = lines.indexOf("---", 1);
|
|
22
|
-
if (endIdx === -1) {
|
|
23
|
-
return { body: raw, frontmatter: {} };
|
|
24
|
-
}
|
|
25
|
-
const fmLines = lines.slice(1, endIdx);
|
|
26
|
-
const body = lines.slice(endIdx + 1).join("\n").trim();
|
|
27
|
-
const frontmatter = {};
|
|
28
|
-
let currentKey = null;
|
|
29
|
-
let currentArray = [];
|
|
30
|
-
for (const line of fmLines) {
|
|
31
|
-
const trimmed = line.trim();
|
|
32
|
-
if (!trimmed) continue;
|
|
33
|
-
const arrayMatch = trimmed.match(/^-\s+(.+)$/);
|
|
34
|
-
if (arrayMatch && currentKey) {
|
|
35
|
-
currentArray.push(arrayMatch[1]);
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
if (currentKey) {
|
|
39
|
-
if (currentArray.length > 0) {
|
|
40
|
-
frontmatter[currentKey] = currentArray;
|
|
41
|
-
}
|
|
42
|
-
currentKey = null;
|
|
43
|
-
currentArray = [];
|
|
44
|
-
}
|
|
45
|
-
const kvMatch = trimmed.match(/^([a-z][a-zA-Z0-9_-]*)\s*:\s*(.*)$/);
|
|
46
|
-
if (kvMatch) {
|
|
47
|
-
const key = kvMatch[1];
|
|
48
|
-
const value = kvMatch[2].trim();
|
|
49
|
-
if (value === "") {
|
|
50
|
-
currentKey = key;
|
|
51
|
-
currentArray = [];
|
|
52
|
-
} else {
|
|
53
|
-
const unquoted = value.replace(/^["'](.*)["']$/, "$1");
|
|
54
|
-
if ((key === "tools" || key === "allowed-tools") && unquoted.startsWith("[") && unquoted.endsWith("]")) {
|
|
55
|
-
frontmatter[key] = unquoted.slice(1, -1).split(",").map((item) => item.trim().replace(/^["'](.*)["']$/, "$1")).filter(Boolean);
|
|
56
|
-
} else {
|
|
57
|
-
frontmatter[key] = unquoted;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (currentKey && currentArray.length > 0) {
|
|
63
|
-
frontmatter[currentKey] = currentArray;
|
|
64
|
-
}
|
|
65
|
-
return { body, frontmatter };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
20
|
// src/commands/resolve.ts
|
|
69
21
|
import { exec } from "child_process";
|
|
70
22
|
function parseSlashInput(value) {
|
|
@@ -177,6 +129,9 @@ function executeInjection(injection, workspace, errors, signal) {
|
|
|
177
129
|
{
|
|
178
130
|
cwd: workspace,
|
|
179
131
|
encoding: "utf8",
|
|
132
|
+
// A `!command` in a slash command is a command the user wrote, not one Book chose, so it
|
|
133
|
+
// must not inherit the NODE_ENV Book defaulted for its own renderer.
|
|
134
|
+
env: buildChildEnv(),
|
|
180
135
|
timeout: 5e3,
|
|
181
136
|
maxBuffer: 1024 * 1024,
|
|
182
137
|
signal,
|
|
@@ -279,11 +234,10 @@ function generateCommandListing(commands, budgetChars = 1024) {
|
|
|
279
234
|
}
|
|
280
235
|
|
|
281
236
|
export {
|
|
282
|
-
parseFrontmatter,
|
|
283
237
|
parseSlashInput,
|
|
284
238
|
resolveCommandBody,
|
|
285
239
|
commandEnforcementContext,
|
|
286
240
|
discoverCommands,
|
|
287
241
|
generateCommandListing
|
|
288
242
|
};
|
|
289
|
-
//# sourceMappingURL=chunk-
|
|
243
|
+
//# sourceMappingURL=chunk-EFJ2NH6E.js.map
|