diff --git a/.nvmrc b/.nvmrc index 11c309c5..cd44bfcf 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v24.13.1 +v26.3.1 diff --git a/apps/claude-sdk-cli/CHANGELOG.md b/apps/claude-sdk-cli/CHANGELOG.md index 499ea299..b7606a4b 100644 --- a/apps/claude-sdk-cli/CHANGELOG.md +++ b/apps/claude-sdk-cli/CHANGELOG.md @@ -53,6 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Customize which commands ExecV3 will run or refuse, without a mistake in that customization ever disabling safety or breaking the rest of your settings - Decode escape sequences in --prompt values: \n, \r, \t, \\ - Display server tool use as its own block in the conversation +- Each connection logs the HTTP protocol it negotiated - ESC while a tool is running cancels the tool instead of the query, so Claude receives the cancellation and can continue - F3 opens a conversation view listing every conversation held in the current directory, with its model, cost, query and turn counts, context use, span, opening ask and last reply; space peeks at the tail of a conversation and enter switches to it in place, without restarting the CLI - Flash tool approval prompt with inverted colours when awaiting Y/N @@ -61,6 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Inject a skill-catalogue delta: re-scan the skill roots each query and prepend a system-reminder naming the skills whose SKILL.md content changed, silent on the first scan of a session and after a resume - Inject the available-skills catalogue as a cached system-reminder on the first user message, scanned from skillDirs at startup and re-injected after compaction, so the model can discover skills to load - Mark model with * suffix in status bar when overridden via --model +- New http.allowH2 setting, off by default, so API requests negotiate HTTP/1.1 - Publish conversation activity as opt-in NATS tap events - Publish the agent concern: ready/pulse/attached/detached telemetry and service/drain/chdir requests - Ref and PreviewEdit state is now persisted to disk @@ -139,6 +141,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A tool call refused without a prompt now says what refused it: the permission setting that decided, the operation it judged, and the paths that selected that setting. It previously reported only that the tool 'is configured to be denied automatically', which was untrue of every case and left both Claude and the operator guessing at a decision the CLI had already made - Add `typescript` as a production dependency so consumers do not need it installed separately +- An error written to the log keeps its name, message, stack and cause instead of rendering as an empty object - Apply biome formatting fixes - Attachments added while a query is streaming are no longer cleared once that query finishes - Count tool approval wait time as tool time in the status-line clock diff --git a/apps/claude-sdk-cli/changes.jsonl b/apps/claude-sdk-cli/changes.jsonl index 1f5b4ed1..474c35d3 100644 --- a/apps/claude-sdk-cli/changes.jsonl +++ b/apps/claude-sdk-cli/changes.jsonl @@ -175,3 +175,6 @@ {"description":"Deleting a symlink inside the scratchpad is now approved. Removing a link never touches what it points at, so judging the delete by its destination made any link Claude created in its own scratchpad permanently undeletable. Writes still follow a link to where they land, and a delete whose parent directory resolves outside the scratchpad is still refused","category":"fixed"} {"description":"Render markdown tables in a response, honouring column alignment","category":"added"} {"description":"Fix a fenced code block drawing its border in the wrong place when it holds a link or a wide character","category":"fixed"} +{"description":"New http.allowH2 setting, off by default, so API requests negotiate HTTP/1.1","category":"added"} +{"description":"An error written to the log keeps its name, message, stack and cause instead of rendering as an empty object","category":"fixed"} +{"description":"Each connection logs the HTTP protocol it negotiated","category":"added"} diff --git a/apps/claude-sdk-cli/package.json b/apps/claude-sdk-cli/package.json index 5ec9e4a7..2ddecb10 100644 --- a/apps/claude-sdk-cli/package.json +++ b/apps/claude-sdk-cli/package.json @@ -44,7 +44,7 @@ "@shellicar/build-version": "^2.0.0", "@shellicar/typescript-config": "workspace:^", "@tsconfig/node24": "^24.0.4", - "@types/node": "^25.9.5", + "@types/node": "^26.2.0", "ajv": "^8.20.0", "esbuild": "^0.28.0", "tsx": "^4.22.5", @@ -68,6 +68,7 @@ "marked": "^18.0.7", "string-width": "^8.2.2", "typescript": "^5.9.3", + "undici": "^8.10.0", "winston": "^3.19.0", "yaml": "^2.9.0", "zod": "^4.4.3" diff --git a/apps/claude-sdk-cli/src/cli-config/schema.ts b/apps/claude-sdk-cli/src/cli-config/schema.ts index 491c69ef..2be3420a 100644 --- a/apps/claude-sdk-cli/src/cli-config/schema.ts +++ b/apps/claude-sdk-cli/src/cli-config/schema.ts @@ -278,6 +278,14 @@ const preventSleepSchema = z .default({ enabled: true, platforms: { macos: 'caffeinate', windows: null, linux: null } }) .catch({ enabled: true, platforms: { macos: 'caffeinate', windows: null, linux: null } }); +const httpSchema = z + .object({ + allowH2: z.boolean().optional().default(false).catch(false).describe('Allow HTTP/2 for API requests. Off by default, so requests negotiate HTTP/1.1.'), + }) + .optional() + .default({ allowH2: false }) + .catch({ allowH2: false }); + const secretsSchema = z .object({ stripGhCredentials: z @@ -392,6 +400,7 @@ export const sdkConfigSchema = z permissions: permissionsSchema.describe('Tool approval permission matrix'), workspace: workspaceSchema.describe('Scratchpad directory configuration'), preventSleep: preventSleepSchema.describe('Sleep prevention during in-flight network requests'), + http: httpSchema.describe('HTTP transport configuration'), persistence: persistenceSchema.describe('Persistence (SQLite) configuration'), markdown: markdownSchema.describe('Markdown rendering configuration'), memory: memorySchema.describe('Persistent memory configuration'), diff --git a/apps/claude-sdk-cli/src/logger.ts b/apps/claude-sdk-cli/src/logger.ts index c4dc97f5..e36ea53e 100644 --- a/apps/claude-sdk-cli/src/logger.ts +++ b/apps/claude-sdk-cli/src/logger.ts @@ -1,5 +1,5 @@ import winston from 'winston'; -import { redact } from './redact'; +import { isPlainObject, redact } from './redact'; const levels = { error: 0, warn: 1, info: 2, debug: 3, trace: 4 }; const colors = { error: 'red', warn: 'yellow', info: 'green', debug: 'blue', trace: 'gray' }; @@ -55,9 +55,30 @@ const winstonLogger = winston.createLogger({ transports, }) as winston.Logger & { trace: winston.LeveledLogMethod }; +// An Error's name, message, stack and cause are non-enumerable, so JSON.stringify +// renders one as `{}`. Flatten it to a plain object before it reaches the format. +const serialiseErrors = (value: unknown): unknown => { + if (value instanceof Error) { + return { + ...(serialiseErrors({ ...value }) as object), + name: value.name, + message: value.message, + stack: value.stack, + cause: serialiseErrors(value.cause), + }; + } + if (Array.isArray(value)) { + return value.map(serialiseErrors); + } + if (isPlainObject(value)) { + return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, serialiseErrors(v)])); + } + return value; +}; + const wrapMeta = (meta: unknown[]): object => { const wrapped = meta.length === 0 ? {} : meta.length === 1 ? { data: meta[0] } : { data: meta }; - return redact(wrapped) as object; + return redact(serialiseErrors(wrapped)) as object; }; export const logger = { diff --git a/apps/claude-sdk-cli/src/redact.ts b/apps/claude-sdk-cli/src/redact.ts index cb2f5c74..4fc897a6 100644 --- a/apps/claude-sdk-cli/src/redact.ts +++ b/apps/claude-sdk-cli/src/redact.ts @@ -1,6 +1,6 @@ const SENSITIVE_KEYS = new Set(['authorization', 'x-api-key', 'api-key', 'api_key', 'apikey', 'password', 'secret', 'token']); -const isPlainObject = (value: unknown): value is Record => { +export const isPlainObject = (value: unknown): value is Record => { if (value === null || typeof value !== 'object') { return false; } diff --git a/apps/claude-sdk-cli/src/setup/container.ts b/apps/claude-sdk-cli/src/setup/container.ts index 5672e9f8..3fb0a9ce 100644 --- a/apps/claude-sdk-cli/src/setup/container.ts +++ b/apps/claude-sdk-cli/src/setup/container.ts @@ -172,6 +172,7 @@ import { ConversationSwitcher, IConversationSwitcher } from './ConversationSwitc import { CwdTracker } from './CwdTracker.js'; import { DurableConfigFactory } from './DurableConfigFactory.js'; import { GitMemoryEnvironmentProvider } from './GitMemoryEnvironmentProvider.js'; +import { createHttpDispatcher } from './httpDispatcher.js'; import { IRuntimeOptions } from './IRuntimeOptions.js'; import { ModelOverrides } from './ModelOverrides.js'; import { SdkChannel } from './SdkChannel.js'; @@ -399,7 +400,7 @@ export function buildContainer(options: ContainerOptions): IServiceCollection { services.register(LoginFlow).as(ILoginFlow); services .register(AnthropicClient) - .using([ICredentialProvider, ILogger], (credentials, log) => new AnthropicClient(credentials, log)) + .using([ICredentialProvider, ILogger, ConfigLoader], (credentials, log, loader) => new AnthropicClient(credentials, log, createHttpDispatcher(loader.config.http.allowH2, log))) .as(IMessageStreamer); services .register(ModelCatalog) diff --git a/apps/claude-sdk-cli/src/setup/httpDispatcher.ts b/apps/claude-sdk-cli/src/setup/httpDispatcher.ts new file mode 100644 index 00000000..f86412e6 --- /dev/null +++ b/apps/claude-sdk-cli/src/setup/httpDispatcher.ts @@ -0,0 +1,14 @@ +import diagnostics_channel from 'node:diagnostics_channel'; +import type { ILogger } from '@shellicar/claude-core/logging/ILogger'; +import { Agent } from 'undici'; + +type ConnectedEvent = { socket?: { alpnProtocol?: string | false; servername?: string } }; + +/** The negotiated protocol is not otherwise observable, so each connection logs it. */ +export const createHttpDispatcher = (allowH2: boolean, logger: ILogger): Agent => { + diagnostics_channel.subscribe('undici:client:connected', (event) => { + const socket = (event as ConnectedEvent)?.socket; + logger.info('connection established', { alpn: socket?.alpnProtocol ?? null, host: socket?.servername ?? null, allowH2 }); + }); + return new Agent({ allowH2 }); +}; diff --git a/apps/claude-sdk-cli/test/cli-config.spec.ts b/apps/claude-sdk-cli/test/cli-config.spec.ts index 2de747a9..b81d521b 100644 --- a/apps/claude-sdk-cli/test/cli-config.spec.ts +++ b/apps/claude-sdk-cli/test/cli-config.spec.ts @@ -37,6 +37,7 @@ describe('sdkConfigSchema', () => { }, workspace: { enabled: true }, preventSleep: { enabled: true, platforms: { macos: 'caffeinate', windows: null, linux: null } }, + http: { allowH2: false }, persistence: { database: 'persistence.db' }, markdown: { enabled: true, streaming: true }, memory: { tenantId: null, environment: {}, git: { enabled: true } }, diff --git a/packages/claude-core/package.json b/packages/claude-core/package.json index 5c4bc1f8..9e3b8c30 100644 --- a/packages/claude-core/package.json +++ b/packages/claude-core/package.json @@ -51,7 +51,7 @@ "@shellicar/build-version": "^2.0.0", "@shellicar/typescript-config": "workspace:^", "@tsconfig/node24": "^24.0.4", - "@types/node": "^25.9.5", + "@types/node": "^26.2.0", "tsup": "^8.5.1", "tsx": "^4.22.5", "typescript": "^5.9.3" diff --git a/packages/claude-sdk-tools/package.json b/packages/claude-sdk-tools/package.json index c5fa693f..2410a997 100644 --- a/packages/claude-sdk-tools/package.json +++ b/packages/claude-sdk-tools/package.json @@ -373,7 +373,7 @@ "@shellicar/build-version": "^2.0.0", "@shellicar/typescript-config": "workspace:^", "@tsconfig/node24": "^24.0.4", - "@types/node": "^25.9.5", + "@types/node": "^26.2.0", "esbuild": "^0.28.0", "tsup": "^8.5.1", "tsx": "^4.22.5", diff --git a/packages/claude-sdk/CHANGELOG.md b/packages/claude-sdk/CHANGELOG.md index a073f331..dff8143c 100644 --- a/packages/claude-sdk/CHANGELOG.md +++ b/packages/claude-sdk/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add support for Claude Opus 4.8 - Add the 'escalate' tool operation: a tool that crosses a privilege boundary always prompts for approval, independent of the read/write/delete cwd-zone matrix or any auto-approve config - Add updateIdentityBody to the durable config provider, folding a live system-identity body in as the first system prompt on the next config read +- AnthropicClient accepts an undici Dispatcher, so a consumer chooses the HTTP protocol its requests negotiate - Carry the request delta and its message, turn, and query ids through the final_message event, so the CLI can record each turn as a user/assistant pair - Classify a mid-stream connection drop and retry it on a bounded fixed schedule instead of surfacing it as a fatal error, with injection seams to hold a wake lock and signal a reconnect - Deliver tool attachments as native content blocks inside tool results @@ -29,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Emit tool_exec_start and tool_exec_end around tool execution, bracketing the run phase (approval waits included) so a consumer can frame and time it separately from tool-call generation - ESC while a tool is running cancels the tool and delivers a cancellation result to Claude; ESC otherwise ends the query - Export `IMessageStreamer` from the public barrel +- Failed API requests are logged with their cause, how they were classified, and each retry attempt and delay - Inject a live per-turn date/time stamp into every request - isSystemReminderBlock is now exported, so a consumer can tell a block apart from a message's own words without reimplementing the test - Mark a tool-schema field as a filesystem path and normalise all marked paths once from that marker, so the display, the permission check, and handler execution read one produced path diff --git a/packages/claude-sdk/changes.jsonl b/packages/claude-sdk/changes.jsonl index edbf95e9..2c47a5b1 100644 --- a/packages/claude-sdk/changes.jsonl +++ b/packages/claude-sdk/changes.jsonl @@ -69,3 +69,5 @@ {"description":"Stored credentials and the browser login are now separate services a consumer resolves and can substitute (ICredentialProvider and ILoginFlow), replacing AnthropicAuth. A per-request caller holds the credential provider, which cannot open a browser","category":"changed"} {"description":"The OAuth callback's state is checked against the authorisation request it was built for, so a callback arriving from anywhere else is refused instead of exchanged","category":"security"} {"description":"DurableConfig gains conversationReminders, for standing facts about the current conversation. They are injected and re-injected exactly as cachedReminders are, but sit after them, so the prefix cache marker still falls on the last cached block and a per-conversation value cannot cost the shared prefix its reuse","category":"added"} +{"description":"Failed API requests are logged with their cause, how they were classified, and each retry attempt and delay","category":"added"} +{"description":"AnthropicClient accepts an undici Dispatcher, so a consumer chooses the HTTP protocol its requests negotiate","category":"added"} diff --git a/packages/claude-sdk/package.json b/packages/claude-sdk/package.json index fb83dc42..73d02356 100644 --- a/packages/claude-sdk/package.json +++ b/packages/claude-sdk/package.json @@ -99,11 +99,12 @@ "@shellicar/build-version": "^2.0.0", "@shellicar/typescript-config": "workspace:^", "@tsconfig/node24": "^24.0.4", - "@types/node": "^25.9.5", + "@types/node": "^26.2.0", "esbuild": "^0.28.0", "tsup": "^8.5.1", "tsx": "^4.22.5", "typescript": "^5.9.3", + "undici": "^8.10.0", "vitest": "^4.1.10" } } diff --git a/packages/claude-sdk/src/private/AnthropicClient.ts b/packages/claude-sdk/src/private/AnthropicClient.ts index 8a0ea497..493baae2 100644 --- a/packages/claude-sdk/src/private/AnthropicClient.ts +++ b/packages/claude-sdk/src/private/AnthropicClient.ts @@ -2,6 +2,7 @@ import type { Anthropic } from '@anthropic-ai/sdk'; import type { BetaMessageStreamParams } from '@anthropic-ai/sdk/resources/beta/messages.js'; import versionJson from '@shellicar/build-version/version'; import type { ILogger } from '@shellicar/claude-core/logging/ILogger'; +import type { Dispatcher } from 'undici'; import type { ICredentialProvider } from './Client/Auth/interfaces'; import { customFetch } from './http/customFetch'; import { streamMessages } from './http/transport'; @@ -22,6 +23,8 @@ import { type IMessageStream, IMessageStreamer } from './MessageStreamer'; export class AnthropicClient extends IMessageStreamer { readonly #credentials: ICredentialProvider; readonly #fetch: typeof fetch; + readonly #logger: ILogger; + readonly #dispatcher: Dispatcher | undefined; readonly #defaultHeaders: Record = { 'user-agent': `@shellicar/claude-sdk/${versionJson.version}`, }; @@ -29,9 +32,11 @@ export class AnthropicClient extends IMessageStreamer { // The fetch wrapper is built once, eagerly, so a setup failure surfaces at // composition (buildProvider) rather than on the first request. The app's // composition root supplies the auth and logger through the factory. - public constructor(credentials: ICredentialProvider, logger: ILogger) { + public constructor(credentials: ICredentialProvider, logger: ILogger, dispatcher?: Dispatcher) { super(); this.#credentials = credentials; + this.#logger = logger; + this.#dispatcher = dispatcher; this.#fetch = customFetch(logger) as typeof fetch; } @@ -45,6 +50,8 @@ export class AnthropicClient extends IMessageStreamer { authToken: this.#authToken, fetch: this.#fetch, defaultHeaders: this.#defaultHeaders, + logger: this.#logger, + dispatcher: this.#dispatcher, }); } } diff --git a/packages/claude-sdk/src/private/TurnRunner.ts b/packages/claude-sdk/src/private/TurnRunner.ts index 560ef880..57c794b9 100644 --- a/packages/claude-sdk/src/private/TurnRunner.ts +++ b/packages/claude-sdk/src/private/TurnRunner.ts @@ -186,18 +186,24 @@ export class TurnRunner extends ITurnRunner { this.requestClock.requestSettled(false); // ESC during the request: a normal in-flight cancel, never retried. if (turnInput.abortSignal.aborted) { + this.logger.debug('request cancelled in flight'); throw err; } + this.logger.warn('request failed', { error: err, retryable: isRetryable(err), transientAttempt, streamInterruptAttempt }); + // Account-limit 429 (retry-after exceeds the 60s cap): non-transient. // The give-up decision is made immediately after each 429, before any wait. if (isAccountLimit(err, RETRY_AFTER_CAP_MS)) { const now = this.clock.instant(); firstAccountLimitAt ??= now; - if (Duration.between(firstAccountLimitAt, now).toMillis() >= ACCOUNT_LIMIT_BUDGET_MS) { + const waitedMs = Duration.between(firstAccountLimitAt, now).toMillis(); + if (waitedMs >= ACCOUNT_LIMIT_BUDGET_MS) { + this.logger.error('account limit; budget exhausted, giving up', { waitedMs, budgetMs: ACCOUNT_LIMIT_BUDGET_MS }); this.accountLimit.stopped(); throw new AccountLimitStoppedError(); } + this.logger.warn('account limit; waiting', { waitedMs, budgetMs: ACCOUNT_LIMIT_BUDGET_MS, delayMs: RETRY_AFTER_CAP_MS }); this.accountLimit.retrying(); await this.sleeper.sleep(RETRY_AFTER_CAP_MS, turnInput.abortSignal); if (turnInput.abortSignal.aborted) { @@ -214,9 +220,10 @@ export class TurnRunner extends ITurnRunner { if (err instanceof StreamInterruptedError) { streamInterruptAttempt++; if (streamInterruptAttempt > STREAM_INTERRUPT_MAX_RETRIES) { + this.logger.error('stream interrupted; retries exhausted', { attempt: streamInterruptAttempt, maxRetries: STREAM_INTERRUPT_MAX_RETRIES }); throw err; } - this.logger.warn('stream interrupted; reconnecting', { attempt: streamInterruptAttempt }); + this.logger.warn('stream interrupted; reconnecting', { attempt: streamInterruptAttempt, maxRetries: STREAM_INTERRUPT_MAX_RETRIES, delayMs: STREAM_INTERRUPT_DELAY_MS }); this.interruption.reconnecting(); await this.sleeper.sleep(STREAM_INTERRUPT_DELAY_MS, turnInput.abortSignal); if (turnInput.abortSignal.aborted) { @@ -228,12 +235,12 @@ export class TurnRunner extends ITurnRunner { // Other transient errors: existing exponential backoff + jitter, bounded. transientAttempt++; if (!isRetryable(err) || transientAttempt > MAX_RETRIES) { + this.logger.error('giving up', { reason: isRetryable(err) ? 'retries exhausted' : 'not retryable', attempt: transientAttempt, maxRetries: MAX_RETRIES }); throw err; } - await this.sleeper.sleep( - calculateBackoffDelay(transientAttempt, () => this.random.next()), - turnInput.abortSignal, - ); + const delayMs = calculateBackoffDelay(transientAttempt, () => this.random.next()); + this.logger.warn('retrying after backoff', { attempt: transientAttempt, maxRetries: MAX_RETRIES, delayMs }); + await this.sleeper.sleep(delayMs, turnInput.abortSignal); if (turnInput.abortSignal.aborted) { // On abort, surface a standard cancel: throwIfAborted() throws signal.reason // (a DOMException when abort() has no reason). Deliberately not the SDK's diff --git a/packages/claude-sdk/src/private/http/customFetch.ts b/packages/claude-sdk/src/private/http/customFetch.ts index 64bcc956..f424b70e 100644 --- a/packages/claude-sdk/src/private/http/customFetch.ts +++ b/packages/claude-sdk/src/private/http/customFetch.ts @@ -12,7 +12,15 @@ export const customFetch = (logger: ILogger | undefined) => { method: init?.method, body, }); - const response = await fetch(input, init); + const startMs = Date.now(); + let response: Response; + // try { + response = await fetch(input, init); + // } catch (error) { + // logger?.error('HTTP Request failed', { method: init?.method, elapsedMs: Date.now() - startMs, error }); + // throw error; + // } + const elapsedMs = Date.now() - startMs; const isStream = response.headers.get('content-type')?.includes('text/event-stream') ?? false; if (!isStream) { const text = await response.clone().text(); @@ -26,6 +34,7 @@ export const customFetch = (logger: ILogger | undefined) => { headers: getHeaders(response.headers), status: response.status, statusText: response.statusText, + elapsedMs, body: responseBody, }); } else { @@ -33,6 +42,7 @@ export const customFetch = (logger: ILogger | undefined) => { headers: getHeaders(response.headers), status: response.status, statusText: response.statusText, + elapsedMs, }); } return response; diff --git a/packages/claude-sdk/src/private/http/transport.ts b/packages/claude-sdk/src/private/http/transport.ts index 053add80..bcc0f049 100644 --- a/packages/claude-sdk/src/private/http/transport.ts +++ b/packages/claude-sdk/src/private/http/transport.ts @@ -1,4 +1,6 @@ import type { BetaRawMessageStreamEvent } from '@anthropic-ai/sdk/resources/beta.mjs'; +import type { ILogger } from '@shellicar/claude-core/logging/ILogger'; +import type { Dispatcher } from 'undici'; import { ConnectionError, HttpError, parseRetryAfter, StreamInterruptedError, safeReadBody, TimeoutError, TransportError } from './errors'; import { parseSse } from './sse'; @@ -13,6 +15,8 @@ export type TransportParams = { authToken: () => Promise; fetch: typeof fetch; defaultHeaders: Record; + logger?: ILogger; + dispatcher?: Dispatcher; }; /** @@ -35,27 +39,35 @@ export async function* streamMessages(params: TransportParams): AsyncGenerator=14'} - undici-types@7.24.6: - resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + undici@8.10.0: + resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==} + engines: {node: '>=22.19.0'} universal-user-agent@7.0.3: resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} @@ -3764,122 +3774,122 @@ snapshots: '@inquirer/ansi@2.0.7': {} - '@inquirer/checkbox@5.2.1(@types/node@25.9.5)': + '@inquirer/checkbox@5.2.1(@types/node@26.2.0)': dependencies: '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@25.9.5) + '@inquirer/core': 11.2.1(@types/node@26.2.0) '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@25.9.5) + '@inquirer/type': 4.0.7(@types/node@26.2.0) optionalDependencies: - '@types/node': 25.9.5 + '@types/node': 26.2.0 - '@inquirer/confirm@6.1.1(@types/node@25.9.5)': + '@inquirer/confirm@6.1.1(@types/node@26.2.0)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.5) - '@inquirer/type': 4.0.7(@types/node@25.9.5) + '@inquirer/core': 11.2.1(@types/node@26.2.0) + '@inquirer/type': 4.0.7(@types/node@26.2.0) optionalDependencies: - '@types/node': 25.9.5 + '@types/node': 26.2.0 - '@inquirer/core@11.2.1(@types/node@25.9.5)': + '@inquirer/core@11.2.1(@types/node@26.2.0)': dependencies: '@inquirer/ansi': 2.0.7 '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@25.9.5) + '@inquirer/type': 4.0.7(@types/node@26.2.0) cli-width: 4.1.0 fast-wrap-ansi: 0.2.2 mute-stream: 3.0.0 signal-exit: 4.1.0 optionalDependencies: - '@types/node': 25.9.5 + '@types/node': 26.2.0 - '@inquirer/editor@5.2.2(@types/node@25.9.5)': + '@inquirer/editor@5.2.2(@types/node@26.2.0)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.5) - '@inquirer/external-editor': 3.0.3(@types/node@25.9.5) - '@inquirer/type': 4.0.7(@types/node@25.9.5) + '@inquirer/core': 11.2.1(@types/node@26.2.0) + '@inquirer/external-editor': 3.0.3(@types/node@26.2.0) + '@inquirer/type': 4.0.7(@types/node@26.2.0) optionalDependencies: - '@types/node': 25.9.5 + '@types/node': 26.2.0 - '@inquirer/expand@5.1.1(@types/node@25.9.5)': + '@inquirer/expand@5.1.1(@types/node@26.2.0)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.5) - '@inquirer/type': 4.0.7(@types/node@25.9.5) + '@inquirer/core': 11.2.1(@types/node@26.2.0) + '@inquirer/type': 4.0.7(@types/node@26.2.0) optionalDependencies: - '@types/node': 25.9.5 + '@types/node': 26.2.0 - '@inquirer/external-editor@3.0.3(@types/node@25.9.5)': + '@inquirer/external-editor@3.0.3(@types/node@26.2.0)': dependencies: chardet: 2.2.0 iconv-lite: 0.7.3 optionalDependencies: - '@types/node': 25.9.5 + '@types/node': 26.2.0 '@inquirer/figures@2.0.7': {} - '@inquirer/input@5.1.2(@types/node@25.9.5)': + '@inquirer/input@5.1.2(@types/node@26.2.0)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.5) - '@inquirer/type': 4.0.7(@types/node@25.9.5) + '@inquirer/core': 11.2.1(@types/node@26.2.0) + '@inquirer/type': 4.0.7(@types/node@26.2.0) optionalDependencies: - '@types/node': 25.9.5 + '@types/node': 26.2.0 - '@inquirer/number@4.1.1(@types/node@25.9.5)': + '@inquirer/number@4.1.1(@types/node@26.2.0)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.5) - '@inquirer/type': 4.0.7(@types/node@25.9.5) + '@inquirer/core': 11.2.1(@types/node@26.2.0) + '@inquirer/type': 4.0.7(@types/node@26.2.0) optionalDependencies: - '@types/node': 25.9.5 + '@types/node': 26.2.0 - '@inquirer/password@5.1.1(@types/node@25.9.5)': + '@inquirer/password@5.1.1(@types/node@26.2.0)': dependencies: '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@25.9.5) - '@inquirer/type': 4.0.7(@types/node@25.9.5) + '@inquirer/core': 11.2.1(@types/node@26.2.0) + '@inquirer/type': 4.0.7(@types/node@26.2.0) optionalDependencies: - '@types/node': 25.9.5 - - '@inquirer/prompts@8.5.2(@types/node@25.9.5)': - dependencies: - '@inquirer/checkbox': 5.2.1(@types/node@25.9.5) - '@inquirer/confirm': 6.1.1(@types/node@25.9.5) - '@inquirer/editor': 5.2.2(@types/node@25.9.5) - '@inquirer/expand': 5.1.1(@types/node@25.9.5) - '@inquirer/input': 5.1.2(@types/node@25.9.5) - '@inquirer/number': 4.1.1(@types/node@25.9.5) - '@inquirer/password': 5.1.1(@types/node@25.9.5) - '@inquirer/rawlist': 5.3.1(@types/node@25.9.5) - '@inquirer/search': 4.2.1(@types/node@25.9.5) - '@inquirer/select': 5.2.1(@types/node@25.9.5) + '@types/node': 26.2.0 + + '@inquirer/prompts@8.5.2(@types/node@26.2.0)': + dependencies: + '@inquirer/checkbox': 5.2.1(@types/node@26.2.0) + '@inquirer/confirm': 6.1.1(@types/node@26.2.0) + '@inquirer/editor': 5.2.2(@types/node@26.2.0) + '@inquirer/expand': 5.1.1(@types/node@26.2.0) + '@inquirer/input': 5.1.2(@types/node@26.2.0) + '@inquirer/number': 4.1.1(@types/node@26.2.0) + '@inquirer/password': 5.1.1(@types/node@26.2.0) + '@inquirer/rawlist': 5.3.1(@types/node@26.2.0) + '@inquirer/search': 4.2.1(@types/node@26.2.0) + '@inquirer/select': 5.2.1(@types/node@26.2.0) optionalDependencies: - '@types/node': 25.9.5 + '@types/node': 26.2.0 - '@inquirer/rawlist@5.3.1(@types/node@25.9.5)': + '@inquirer/rawlist@5.3.1(@types/node@26.2.0)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.5) - '@inquirer/type': 4.0.7(@types/node@25.9.5) + '@inquirer/core': 11.2.1(@types/node@26.2.0) + '@inquirer/type': 4.0.7(@types/node@26.2.0) optionalDependencies: - '@types/node': 25.9.5 + '@types/node': 26.2.0 - '@inquirer/search@4.2.1(@types/node@25.9.5)': + '@inquirer/search@4.2.1(@types/node@26.2.0)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.5) + '@inquirer/core': 11.2.1(@types/node@26.2.0) '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@25.9.5) + '@inquirer/type': 4.0.7(@types/node@26.2.0) optionalDependencies: - '@types/node': 25.9.5 + '@types/node': 26.2.0 - '@inquirer/select@5.2.1(@types/node@25.9.5)': + '@inquirer/select@5.2.1(@types/node@26.2.0)': dependencies: '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@25.9.5) + '@inquirer/core': 11.2.1(@types/node@26.2.0) '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@25.9.5) + '@inquirer/type': 4.0.7(@types/node@26.2.0) optionalDependencies: - '@types/node': 25.9.5 + '@types/node': 26.2.0 - '@inquirer/type@4.0.7(@types/node@25.9.5)': + '@inquirer/type@4.0.7(@types/node@26.2.0)': optionalDependencies: - '@types/node': 25.9.5 + '@types/node': 26.2.0 '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -3933,9 +3943,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@napi-rs/cli@3.7.4(@emnapi/runtime@1.9.2)(@types/node@25.9.5)(supports-color@7.2.0)': + '@napi-rs/cli@3.7.4(@emnapi/runtime@1.9.2)(@types/node@26.2.0)(supports-color@7.2.0)': dependencies: - '@inquirer/prompts': 8.5.2(@types/node@25.9.5) + '@inquirer/prompts': 8.5.2(@types/node@26.2.0) '@napi-rs/cross-toolchain': 1.0.3(supports-color@7.2.0) '@napi-rs/wasm-tools': 1.1.0 '@octokit/rest': 22.0.1 @@ -4618,20 +4628,20 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true - '@shellicar/build-clean@1.3.6(esbuild@0.28.1)(rolldown@1.0.3)(vite@7.3.6(@types/node@25.9.5)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0))': + '@shellicar/build-clean@1.3.6(esbuild@0.28.1)(rolldown@1.0.3)(vite@7.3.6(@types/node@26.2.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0))': dependencies: unplugin: 2.3.11 optionalDependencies: esbuild: 0.28.1 rolldown: 1.0.3 - vite: 7.3.6(@types/node@25.9.5)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0) + vite: 7.3.6(@types/node@26.2.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0) - '@shellicar/build-version@2.0.0(esbuild@0.28.1)(vite@7.3.6(@types/node@25.9.5)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0))': + '@shellicar/build-version@2.0.0(esbuild@0.28.1)(vite@7.3.6(@types/node@26.2.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0))': dependencies: unplugin: 2.3.11 optionalDependencies: esbuild: 0.28.1 - vite: 7.3.6(@types/node@25.9.5)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0) + vite: 7.3.6(@types/node@26.2.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0) '@shellicar/core-di-engine@5.0.0-alpha.2': {} @@ -4694,9 +4704,9 @@ snapshots: '@types/estree@1.0.9': {} - '@types/node@25.9.5': + '@types/node@26.2.0': dependencies: - undici-types: 7.24.6 + undici-types: 8.3.0 '@types/semver@7.7.1': {} @@ -4711,10 +4721,10 @@ snapshots: istanbul-lib-report: 3.0.1 istanbul-reports: 3.2.0 magicast: 0.5.3 - obug: 2.1.3 + obug: 2.1.4 std-env: 4.2.0 tinyrainbow: 3.1.0 - vitest: 4.1.10(@types/node@25.9.5)(@vitest/coverage-v8@4.1.10)(vite@7.3.6(@types/node@25.9.5)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0)) + vitest: 4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(vite@7.3.6(@types/node@26.2.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0)) '@vitest/expect@4.1.10': dependencies: @@ -4725,13 +4735,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(vite@7.3.6(@types/node@25.9.5)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(vite@7.3.6(@types/node@26.2.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.6(@types/node@25.9.5)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0) + vite: 7.3.6(@types/node@26.2.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0) '@vitest/pretty-format@4.1.10': dependencies: @@ -5828,8 +5838,8 @@ snapshots: tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 tinyrainbow@3.1.0: {} @@ -5913,7 +5923,9 @@ snapshots: unbash@3.0.0: {} - undici-types@7.24.6: {} + undici-types@8.3.0: {} + + undici@8.10.0: {} universal-user-agent@7.0.3: {} @@ -5930,7 +5942,7 @@ snapshots: vary@1.1.2: {} - vite@7.3.6(@types/node@25.9.5)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0): + vite@7.3.6(@types/node@26.2.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0): dependencies: esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.5) @@ -5939,17 +5951,17 @@ snapshots: rollup: 4.62.2 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 25.9.5 + '@types/node': 26.2.0 fsevents: 2.3.3 jiti: 2.7.0 lightningcss: 1.32.0 tsx: 4.22.5 yaml: 2.9.0 - vitest@4.1.10(@types/node@25.9.5)(@vitest/coverage-v8@4.1.10)(vite@7.3.6(@types/node@25.9.5)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0)): + vitest@4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(vite@7.3.6(@types/node@26.2.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@7.3.6(@types/node@25.9.5)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@7.3.6(@types/node@26.2.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -5966,10 +5978,10 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 7.3.6(@types/node@25.9.5)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0) + vite: 7.3.6(@types/node@26.2.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.5)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.9.5 + '@types/node': 26.2.0 '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) transitivePeerDependencies: - msw diff --git a/schema/sdk-config.schema.json b/schema/sdk-config.schema.json index 187cdb32..c3295fb4 100644 --- a/schema/sdk-config.schema.json +++ b/schema/sdk-config.schema.json @@ -756,6 +756,20 @@ } } }, + "http": { + "default": { + "allowH2": false + }, + "description": "HTTP transport configuration", + "type": "object", + "properties": { + "allowH2": { + "default": false, + "description": "Allow HTTP/2 for API requests. Off by default, so requests negotiate HTTP/1.1.", + "type": "boolean" + } + } + }, "persistence": { "default": { "database": "persistence.db" diff --git a/scripts/package.json b/scripts/package.json index 0f8443f4..e0a4fefa 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -17,7 +17,7 @@ "@shellicar/claude-sdk-cli": "workspace:^", "@shellicar/claude-sdk-tools": "workspace:^", "@shellicar/typescript-config": "workspace:^", - "@types/node": "^25.9.5", + "@types/node": "^26.2.0", "@types/semver": "^7.5.8", "ajv": "^8.20.0", "semver": "^7.8.5",