From 5e8af0208f3a3f7f1c68e5bd6540383a7cbd252e Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Sat, 8 Aug 2026 21:56:03 +0500 Subject: [PATCH 01/18] chore: enforce zero-tolerance linting and formatting across all file types - ESLint: strict + strictTypeChecked + stylistic for JS/TS, strict YAML and JSON/JSONC rules, all warnings escalated to errors - Type-check the ENTIRE codebase (incl. scripts/) via new tsconfig.check.json and lint:ts, so no type error anywhere can slip past - markdownlint, shellcheck, editorconfig-checker and prettier --check added to the lint pipeline - lint-staged now formats EVERY changed file (code + non-code) with prettier, plus eslint --fix, markdownlint --fix and shellcheck - Husky pre-commit blocks any commit containing any error, warning, info diagnostic, type error, or formatting drift anywhere in the repo - Fix all violations surfaced by the stricter rules (manual fixes only) --- .editorconfig | 38 + .github/dependabot.yml | 8 +- .github/workflows/ci.yml | 3 + .husky/pre-commit | 12 +- .vscode/settings.json | 5 + README.md | 2 +- ...0260514-open-code-provider-architecture.md | 44 +- docs/documentation-standards.md | 6 +- .../07-20260615-model-validation-retry.md | 24 +- ...-20260712-per-profile-go-usage-tracking.md | 10 +- docs/features/11-20260715-vision-proxy.md | 16 +- .../13-20260803-image-normalization.md | 24 +- .../14-20260803-model-picker-enhancements.md | 16 +- .../01-20260515-qwen36-tool-call-loop.md | 6 +- .../02-20260516-context-size-correction.md | 6 +- ...-unavailable-deprecated-model-filtering.md | 6 +- .../04-20260517-pr1-freeonly-review-merge.md | 6 +- ...0517-thinking-mode-picker-configuration.md | 6 +- ...7-thinking-native-submenu-investigation.md | 6 +- .../07-20260517-zen-model-version-labels.md | 6 +- .../08-20260520-vision-image-request-fixes.md | 6 +- .../09-20260524-pr4-review-merge-release.md | 6 +- ...27-context-window-usage-pr6-integration.md | 6 +- ...minimax-think-tags-review-merge-release.md | 6 +- ...-output-channel-cleanup-textdecoder-fix.md | 6 +- ...0609-project-cleanup-immediate-bugfixes.md | 6 +- ...0610-pr15-context-size-reasoning-review.md | 16 +- ...260611-pr18-kimi-thinking-format-review.md | 12 +- ...imax-m3-think-tag-leak-reimplementation.md | 6 +- ...ing-off-missing-for-effort-only-schemas.md | 10 +- ...15-thinking-style-setting-not-respected.md | 12 +- ...kimi-k27-temperature-thinking-rejection.md | 10 +- ...0260617-inline-completions-fim-research.md | 12 +- ...-20260623-pr53-model-picker-crash-1-126.md | 10 +- ...0260624-pr54-security-hardening-cleanup.md | 14 +- ...r42-pr43-duplicate-agent-host-model-fix.md | 14 +- ...-20260708-vscode-128-byok-utility-model.md | 6 +- ...9-thinking-part-byok-surfacing-research.md | 18 +- ...36-20260723-mimo-thinking-infinite-loop.md | 6 +- ...-20260724-estimate-token-count-overflow.md | 10 +- ...0803-pr100-tool-call-flush-review-merge.md | 32 +- ...0260807-pr107-transient-5xx-retry-merge.md | 45 +- eslint.config.mjs | 65 +- package-lock.json | 1523 ++++++++++++++++- package.json | 28 +- scripts/run-unit-tests.mjs | 5 + scripts/test-retry-e2e.mts | 39 +- scripts/validate-models.mts | 110 +- scripts/verify-estimate-token-count.mts | 12 +- src/contextWindowHook.ts | 57 +- src/contextWindowHookBridge.ts | 20 +- src/errors.ts | 18 +- src/extension.ts | 317 ++-- src/goUsageTracker.ts | 57 +- src/imageNormalizer.ts | 6 +- src/metadata.ts | 25 +- src/providerTypes.ts | 2 +- src/responsesRequest.ts | 23 +- src/retry.ts | 10 +- src/routing.ts | 2 +- src/runtimeDiagnostics.ts | 2 +- src/streaming.ts | 74 +- src/test/apiKeyResolution.test.ts | 8 +- src/test/goUsageTracker.test.ts | 134 +- src/test/imageNormalizer.test.ts | 16 +- src/test/metadata.test.ts | 50 +- src/test/modelLimits.test.ts | 12 +- src/test/modelNames.test.ts | 8 +- src/test/responsesRequest.test.ts | 20 +- src/test/retry.test.ts | 62 +- src/test/thinking.test.ts | 101 +- src/test/tokenEstimate.test.ts | 6 +- src/test/toolCallAccumulator.test.ts | 36 +- src/test/usageProfile.test.ts | 24 +- src/test/visionProxy.test.ts | 28 +- src/thinking.ts | 6 +- src/toolCallAccumulator.ts | 2 +- src/usage.ts | 18 +- src/usageProfile.ts | 9 +- src/vscode.proposed.chatProvider.d.ts | 33 +- ...de.proposed.languageModelThinkingPart.d.ts | 4 +- tsconfig.check.json | 9 + 82 files changed, 2579 insertions(+), 921 deletions(-) create mode 100644 .editorconfig create mode 100644 .vscode/settings.json create mode 100644 tsconfig.check.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4ed1a4a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,38 @@ +# https://editorconfig.org +# Universal style enforcement for EVERY file type in the repo. +# editorconfig-checker verifies all of these rules and fails on any violation. + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +# Shell scripts (husky hooks) โ€” POSIX sh, 2-space indent +[.husky/*] +indent_style = space +indent_size = 2 + +# GitHub Actions YAML uses 2-space indentation +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +# Binary media files must be left untouched +[*.{png,gif,jpg,jpeg,ico}] +insert_final_newline = false +trim_trailing_whitespace = false + +# Markdown: no trailing whitespace, final newline required. +# Indentation is NOT enforced here: markdown leading whitespace is content +# (ASCII-art diagrams, nested-list alignment), not code style โ€” markdownlint +# governs markdown formatting instead. +[*.md] +indent_style = unset +indent_size = unset +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a64b8a8..7bd17c9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,9 +1,9 @@ version: 2 updates: - - package-ecosystem: "npm" - directory: "/" + - package-ecosystem: npm + directory: / schedule: - interval: "monthly" + interval: monthly open-pull-requests-limit: 3 labels: - - "dependencies" + - dependencies diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b618597..d6f45e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,9 @@ jobs: - name: ๐Ÿ”Ž Lint source and maintenance docs run: npm run lint + - name: ๐ŸŽจ Check formatting + run: npm run format:check + - name: ๐Ÿงช Run tests run: npm test diff --git a/.husky/pre-commit b/.husky/pre-commit index d0a7784..dc5906c 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,11 @@ -npx lint-staged \ No newline at end of file +#!/usr/bin/env sh +# Format ALL changed files (code + non-code) โ€” prettier on everything it +# supports, eslint --fix on JS/TS, markdownlint --fix on Markdown, and lint +# of husky scripts. Fast path: only staged files. +npx lint-staged + +# Zero-tolerance gate: full-repo lint (eslint, markdownlint, shellcheck, +# editorconfig-checker, tsc type-check incl. scripts/) plus prettier format +# check. A commit is BLOCKED if ANY file in the codebase has any error, +# warning, info-level diagnostic, type error, or formatting drift. +npm run lint diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..398d8fa --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "**/.husky/*": "shellscript" + } +} diff --git a/README.md b/README.md index ec9ef3c..cb28962 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ | ๐Ÿ“Š **Live usage tracking** | Status bar shows Go subscription burn-rate across 5h / weekly / monthly tiers | | ๐Ÿ”Œ **Dual providers** | OpenCode **Go** ($10/mo subscription) + OpenCode **Zen** (free + paid models) โ€” run both at once, switch instantly | | ๐ŸŽฏ **Smart routing** | Each model family auto-routes to its native transport (`/responses`, `/messages`, `streamGenerateContent`, `/chat/completions`) | -| ๐Ÿ–ผ๏ธ **Vision + PDF + Audio** | Multimodal models pass through image, PDF, audio, and video inputs. Oversized images auto-resize to 2000ร—2000 / 5MB to match the gateway contract. | +| ๐Ÿ–ผ๏ธ **Vision + PDF + Audio** | Multimodal models pass through image, PDF, audio, and video inputs. Oversized images auto-resize to 2000ร—2000 / 5MB to match the gateway contract. | | ๐Ÿ“ **Context-size picker** | Kimi K3 and other tiered-context models expose `256K` vs full-window selection in the per-model configuration, with the cheaper tier selected by default. | | ๐Ÿ”’ **Your key, your control** | API key stored in VS Code SecretStorage โ€” never leaves your machine | diff --git a/docs/architecture/01-20260514-open-code-provider-architecture.md b/docs/architecture/01-20260514-open-code-provider-architecture.md index 3806739..8ff7e2f 100644 --- a/docs/architecture/01-20260514-open-code-provider-architecture.md +++ b/docs/architecture/01-20260514-open-code-provider-architecture.md @@ -2,11 +2,11 @@ # OpenCode Provider Architecture -**Topic:** provider / models / routing / usage / security -**Updated:** 2026-06-24 -**Tags:** #provider #models #routing #byok #vscode #tool-calling #thinking #usage #security +**Topic:** provider / models / routing / usage / security +**Updated:** 2026-06-24 +**Tags:** #provider #models #routing #byok #vscode #tool-calling #thinking #usage #security **Supersedes:** - -**Original Session:** 2026-05-14 +**Original Session:** 2026-05-14 **Documented:** 2026-06-12 **Last verified:** 2026-06-24 @@ -33,24 +33,24 @@ This document is intentionally backdated to the original provider-architecture s ## Timeline -| Date | Version | Change | Status | -| ---------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| 2026-05-14 | 0.1.0 | Initial OpenCode Go provider, model list, fallback limits, endpoint routing, tool support, and diagnostics | โœ… Solved | -| 2026-05-14 | 0.1.1 | Native VS Code Language Models BYOK configuration schema and secret `apiKey` flow | โœ… Solved | -| 2026-05-14 | 0.1.2 | Separate OpenCode Zen provider, free-model filtering, key caching, tool-call streaming, and DeepSeek reasoning replay | โœ… Solved | -| 2026-05-16 | 0.1.3 | Context-size metadata corrected and model limits split per provider | โœ… Solved | -| 2026-05-17 | 0.1.4 | Zen `freeOnly`, per-model thinking configuration, model-label fixes, schema sanitization, and unavailable-model filtering | โœ… Solved | -| 2026-05-21 | 0.1.6 | Request timeout, sticky gateway headers, models.dev cache, Zen GPT `/responses`, and Zen Gemini routing | โœ… Solved | -| 2026-05-27 | 0.1.7 | Transport diagnostics, usage status bar, usage DataPart, context-window hook, and OpenCode auth/body fixes | โœ… Solved | -| 2026-06-04 | 0.1.8 | Pricing metadata, modality detection, provider capability shape, and redundant experimental context setting removal | โœ… Solved | -| 2026-06-05 | 0.2.0 | Go Usage Tracker for subscription limits and cost tracking | โœ… Solved | -| 2026-06-09 | 0.2.4 | Context Size selector, dynamic reasoning options, Mimo/MiniMax/DeepSeek/Kimi thinking controls, and strip-think-tags setting | โœ… Solved | -| 2026-06-12 | 0.2.7 | Temperature support guard and Kimi thinking documentation correction | โœ… Solved | -| 2026-06-23 | 0.3.4 | VS Code โ‰ฅ1.126 model picker crash fix: `category` type from object to string, secrets fallback via `options.configuration` discriminator, agent variant independent resolution | โœ… Solved | -| 2026-06-24 | 0.3.4 | Security hardening: removed API key debug log leak, Clear API Key BYOK warning, `reasoningContentByToolCallId` memory cap at 500, removed dead `agentProvidersByBaseVendor` map and `categoryOrder` field | โœ… Solved | -| 2026-08-03 | 0.5.0 | Issue [#86](https://github.com/ltmoerdani/opencode-copilot-chat/issues/86) (PR [#101](https://github.com/ltmoerdani/opencode-copilot-chat/pull/101)): dropped the `isAgentVariant \|\| options.configuration` guard so non-agent `opencodezen` / `opencodego` providers fall back to `SecretStorage` whenever `options.configuration` is absent. Mirrors Copilot's own `AbstractLanguageModelChatProvider`. The previous in-code comment claiming `configuration=undefined` was a transient "still resolving" state was incorrect. | โœ… Solved | -| 2026-08-05 | 0.5.0 | Issue [#106](https://github.com/ltmoerdani/opencode-copilot-chat/issues/106) (PR [#108](https://github.com/ltmoerdani/opencode-copilot-chat/pull/108)): regression from the #86 fix where a native BYOK group caused every Zen model to be listed twice. The provider now records per vendor when a BYOK group exists and keeps the groupless call silent in that case. | โœ… Solved | -| 2026-08-07 | Unreleased | PR [#113](https://github.com/ltmoerdani/opencode-copilot-chat/pull/113) bridge hardening (#103 + #109): `truncation: "auto"` + bounded output on Responses, tool/MCP schemas in prompt estimates, proportional tokenizer headroom, upstream-count HTTP 400 recovery across 4 transports, `editTools` dropped for Marketplace, cold-start `SecretStorage` credentials, runtime diagnostics, blocking CI | โœ… Solved | +| Date | Version | Change | Status | +| ---------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| 2026-05-14 | 0.1.0 | Initial OpenCode Go provider, model list, fallback limits, endpoint routing, tool support, and diagnostics | โœ… Solved | +| 2026-05-14 | 0.1.1 | Native VS Code Language Models BYOK configuration schema and secret `apiKey` flow | โœ… Solved | +| 2026-05-14 | 0.1.2 | Separate OpenCode Zen provider, free-model filtering, key caching, tool-call streaming, and DeepSeek reasoning replay | โœ… Solved | +| 2026-05-16 | 0.1.3 | Context-size metadata corrected and model limits split per provider | โœ… Solved | +| 2026-05-17 | 0.1.4 | Zen `freeOnly`, per-model thinking configuration, model-label fixes, schema sanitization, and unavailable-model filtering | โœ… Solved | +| 2026-05-21 | 0.1.6 | Request timeout, sticky gateway headers, models.dev cache, Zen GPT `/responses`, and Zen Gemini routing | โœ… Solved | +| 2026-05-27 | 0.1.7 | Transport diagnostics, usage status bar, usage DataPart, context-window hook, and OpenCode auth/body fixes | โœ… Solved | +| 2026-06-04 | 0.1.8 | Pricing metadata, modality detection, provider capability shape, and redundant experimental context setting removal | โœ… Solved | +| 2026-06-05 | 0.2.0 | Go Usage Tracker for subscription limits and cost tracking | โœ… Solved | +| 2026-06-09 | 0.2.4 | Context Size selector, dynamic reasoning options, Mimo/MiniMax/DeepSeek/Kimi thinking controls, and strip-think-tags setting | โœ… Solved | +| 2026-06-12 | 0.2.7 | Temperature support guard and Kimi thinking documentation correction | โœ… Solved | +| 2026-06-23 | 0.3.4 | VS Code โ‰ฅ1.126 model picker crash fix: `category` type from object to string, secrets fallback via `options.configuration` discriminator, agent variant independent resolution | โœ… Solved | +| 2026-06-24 | 0.3.4 | Security hardening: removed API key debug log leak, Clear API Key BYOK warning, `reasoningContentByToolCallId` memory cap at 500, removed dead `agentProvidersByBaseVendor` map and `categoryOrder` field | โœ… Solved | +| 2026-08-03 | 0.5.0 | Issue [#86](https://github.com/ltmoerdani/opencode-copilot-chat/issues/86) (PR [#101](https://github.com/ltmoerdani/opencode-copilot-chat/pull/101)): dropped the `isAgentVariant \|\| options.configuration` guard so non-agent `opencodezen` / `opencodego` providers fall back to `SecretStorage` whenever `options.configuration` is absent. Mirrors Copilot's own `AbstractLanguageModelChatProvider`. The previous in-code comment claiming `configuration=undefined` was a transient "still resolving" state was incorrect. | โœ… Solved | +| 2026-08-05 | 0.5.0 | Issue [#106](https://github.com/ltmoerdani/opencode-copilot-chat/issues/106) (PR [#108](https://github.com/ltmoerdani/opencode-copilot-chat/pull/108)): regression from the #86 fix where a native BYOK group caused every Zen model to be listed twice. The provider now records per vendor when a BYOK group exists and keeps the groupless call silent in that case. | โœ… Solved | +| 2026-08-07 | Unreleased | PR [#113](https://github.com/ltmoerdani/opencode-copilot-chat/pull/113) bridge hardening (#103 + #109): `truncation: "auto"` + bounded output on Responses, tool/MCP schemas in prompt estimates, proportional tokenizer headroom, upstream-count HTTP 400 recovery across 4 transports, `editTools` dropped for Marketplace, cold-start `SecretStorage` credentials, runtime diagnostics, blocking CI | โœ… Solved | --- diff --git a/docs/documentation-standards.md b/docs/documentation-standards.md index 793efa0..2b86975 100644 --- a/docs/documentation-standards.md +++ b/docs/documentation-standards.md @@ -44,9 +44,9 @@ grep -r "ClassName" src/ # Document Title -**Topic:** streaming / routing / models / provider / usage -**Updated:** YYYY-MM-DD -**Tags:** #tag1 #tag2 +**Topic:** streaming / routing / models / provider / usage +**Updated:** YYYY-MM-DD +**Tags:** #tag1 #tag2 **Supersedes:** [Link if deprecated] --- diff --git a/docs/features/07-20260615-model-validation-retry.md b/docs/features/07-20260615-model-validation-retry.md index b83675b..36416b5 100644 --- a/docs/features/07-20260615-model-validation-retry.md +++ b/docs/features/07-20260615-model-validation-retry.md @@ -39,7 +39,7 @@ The upstream OpenCode API serves 30+ models from different providers (Moonshot, The retry module (`src/retry.ts`) hosts two distinct retry families that chain in a single request lifecycle: -``` +```text fetch โ†’ 400? โ†’ analyzeHttp400ForRetry โ†’ patch body โ†’ fetch โ†’ 5xx? โ†’ isTransientServerError โ†’ retry up to 2ร— (backoff + jitter) โ†’ surface error ``` @@ -75,20 +75,20 @@ When the gateway momentarily has no healthy backend for a model, it returns `502 **Classifier (`isTransientServerError`):** -| Status | Body | Retry? | -| --- | --- | --- | -| `502` / `503` / `504` | any | โœ… transient by definition | -| other `5xx` | names `Router.Unavailable` (case-insensitive, non-letters stripped) | โœ… momentary condition | -| other `5xx` | unrelated body (e.g. raw `Internal Server Error`) | โŒ permanent, surfaces real bugs | -| non-5xx (`429`, `404`, etc.) | any | โŒ handled by their own paths | +| Status | Body | Retry? | +| ---------------------------- | ------------------------------------------------------------------- | -------------------------------- | +| `502` / `503` / `504` | any | โœ… transient by definition | +| other `5xx` | names `Router.Unavailable` (case-insensitive, non-letters stripped) | โœ… momentary condition | +| other `5xx` | unrelated body (e.g. raw `Internal Server Error`) | โŒ permanent, surfaces real bugs | +| non-5xx (`429`, `404`, etc.) | any | โŒ handled by their own paths | **Constants:** -| Constant | Value | Purpose | -| --- | --- | --- | -| `TRANSIENT_5XX_MAX_RETRIES` | `2` | Hard cap before surfacing the error | -| `TRANSIENT_5XX_RETRY_BASE_MS` | `1000` | Base backoff, doubles per attempt (1s, 2s) | -| `TRANSIENT_5XX_RETRY_JITTER_MS` | `250` | Max random jitter to spread concurrent retries | +| Constant | Value | Purpose | +| ------------------------------- | ------ | ---------------------------------------------- | +| `TRANSIENT_5XX_MAX_RETRIES` | `2` | Hard cap before surfacing the error | +| `TRANSIENT_5XX_RETRY_BASE_MS` | `1000` | Base backoff, doubles per attempt (1s, 2s) | +| `TRANSIENT_5XX_RETRY_JITTER_MS` | `250` | Max random jitter to spread concurrent retries | **Backoff formula:** `Math.round(BASE * 2 ** (attempt - 1) + Math.random() * JITTER)` โ€” exponential with jitter to avoid thundering-herd under concurrent agent / tool-call bursts. diff --git a/docs/features/10-20260712-per-profile-go-usage-tracking.md b/docs/features/10-20260712-per-profile-go-usage-tracking.md index 2ebb20f..9ab7dd7 100644 --- a/docs/features/10-20260712-per-profile-go-usage-tracking.md +++ b/docs/features/10-20260712-per-profile-go-usage-tracking.md @@ -1,10 +1,10 @@ # 10 โ€” Per-Profile Go Usage Tracking for Multi-Account Setups -**Status:** ๐ŸŸข Active -**Author:** Wallacy (Wallacy Freitas) -**PR:** [#75](https://github.com/ltmoerdani/opencode-copilot-chat/pull/75) -**Issue:** [#63](https://github.com/ltmoerdani/opencode-copilot-chat/issues/63) -**Merged:** 2026-07-12 +**Status:** ๐ŸŸข Active +**Author:** Wallacy (Wallacy Freitas) +**PR:** [#75](https://github.com/ltmoerdani/opencode-copilot-chat/pull/75) +**Issue:** [#63](https://github.com/ltmoerdani/opencode-copilot-chat/issues/63) +**Merged:** 2026-07-12 **Commits:** 4 (`4353c1e`, `7d8a008`, `1734242`, `7b9fae5`) --- diff --git a/docs/features/11-20260715-vision-proxy.md b/docs/features/11-20260715-vision-proxy.md index 39f9877..f12d160 100644 --- a/docs/features/11-20260715-vision-proxy.md +++ b/docs/features/11-20260715-vision-proxy.md @@ -1,13 +1,13 @@ # 11 โ€” Vision Proxy for Text-Only Models -**Status:** ๐ŸŸข Active -**Author:** Wallacy (Wallacy Freitas) -**Reviewer:** ltmoerdani -**PR:** [#76](https://github.com/ltmoerdani/opencode-copilot-chat/pull/76) -**Issues:** [#74](https://github.com/ltmoerdani/opencode-copilot-chat/issues/74) (vision proxy), [#67](https://github.com/ltmoerdani/opencode-copilot-chat/issues/67) (output pane focus steal), [#68](https://github.com/ltmoerdani/opencode-copilot-chat/issues/68) (context overflow safety) -**Merged:** 2026-07-15 -**Merge commit:** `d2fcbe4` (merge commit, NOT squash) -**Commits preserved:** 4 (`69902bb`, `4a36009`, `a17f91e`, `8a0d813`) +**Status:** ๐ŸŸข Active +**Author:** Wallacy (Wallacy Freitas) +**Reviewer:** ltmoerdani +**PR:** [#76](https://github.com/ltmoerdani/opencode-copilot-chat/pull/76) +**Issues:** [#74](https://github.com/ltmoerdani/opencode-copilot-chat/issues/74) (vision proxy), [#67](https://github.com/ltmoerdani/opencode-copilot-chat/issues/67) (output pane focus steal), [#68](https://github.com/ltmoerdani/opencode-copilot-chat/issues/68) (context overflow safety) +**Merged:** 2026-07-15 +**Merge commit:** `d2fcbe4` (merge commit, NOT squash) +**Commits preserved:** 4 (`69902bb`, `4a36009`, `a17f91e`, `8a0d813`) **Released:** `v0.4.1` --- diff --git a/docs/features/13-20260803-image-normalization.md b/docs/features/13-20260803-image-normalization.md index 4891a3c..0c87730 100644 --- a/docs/features/13-20260803-image-normalization.md +++ b/docs/features/13-20260803-image-normalization.md @@ -23,12 +23,12 @@ Prior to this feature, the extension forwarded raw `Uint8Array` image bytes as b ### Observed behavior -| Image | Before fix | After fix | -|-------|------------|-----------| -| Small PNG, any dimensions (<1MB raw) | โœ… Sent as-is | โœ… Passed through unchanged (already in spec) | -| Sub-2MB raw, dimensions >2000px | โœ… Sent as-is, but `400 Upstream request failed` on some models (e.g. `gpt-5.6-luna`, see issue #94 `payloadBytes=880950`) | โœ… Resized to โ‰ค2000px, re-encoded, sent successfully | -| >2MB raw, any dimensions | โŒ Replaced with placeholder text part (`MAX_TOP_LEVEL_IMAGE_BYTES`) | โœ… Resized + re-encoded; only dropped if normalized base64 still exceeds 5MB | -| Tool-result image (MCP screenshot) | โœ… Subject to separate `MAX_TOOL_RESULT_IMAGE_BYTES = 1MB` raw guard | โœ… Normalized first, then same 1MB raw guard still applies for cumulative history bounding | +| Image | Before fix | After fix | +| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| Small PNG, any dimensions (<1MB raw) | โœ… Sent as-is | โœ… Passed through unchanged (already in spec) | +| Sub-2MB raw, dimensions >2000px | โœ… Sent as-is, but `400 Upstream request failed` on some models (e.g. `gpt-5.6-luna`, see issue #94 `payloadBytes=880950`) | โœ… Resized to โ‰ค2000px, re-encoded, sent successfully | +| >2MB raw, any dimensions | โŒ Replaced with placeholder text part (`MAX_TOP_LEVEL_IMAGE_BYTES`) | โœ… Resized + re-encoded; only dropped if normalized base64 still exceeds 5MB | +| Tool-result image (MCP screenshot) | โœ… Subject to separate `MAX_TOOL_RESULT_IMAGE_BYTES = 1MB` raw guard | โœ… Normalized first, then same 1MB raw guard still applies for cumulative history bounding | ### Why the raw-byte guard was not enough @@ -89,13 +89,13 @@ No user-facing settings. Normalization is always on for image attachments. If th ## Files -| File | Change | -|------|--------| -| `src/imageNormalizer.ts` | New module: `normalizeImageDataUrl`, `getImageDataUrlBase64Bytes`, `MAX_IMAGE_BASE64_BYTES` export | -| `src/extension.ts` | `convertMessage()` โ†’ async, inline `normalizeImagePart`, delete `MAX_TOP_LEVEL_IMAGE_BYTES` + old `normalizeImagePartsInPlace` | +| File | Change | +| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `src/imageNormalizer.ts` | New module: `normalizeImageDataUrl`, `getImageDataUrlBase64Bytes`, `MAX_IMAGE_BASE64_BYTES` export | +| `src/extension.ts` | `convertMessage()` โ†’ async, inline `normalizeImagePart`, delete `MAX_TOP_LEVEL_IMAGE_BYTES` + old `normalizeImagePartsInPlace` | | `src/test/imageNormalizer.test.ts` | New: small image pass-through, dimension-limit resize, non-data URL passthrough, malformed image passthrough, 2MB-raw-but-5MB-base64 regression | -| `.vscodeignore` | Exception for `node_modules/@silvia-odwyer/photon-node/**` so WASM artifact ships in VSIX | -| `package.json` | New runtime dependency `@silvia-odwyer/photon-node` ^0.3.4 | +| `.vscodeignore` | Exception for `node_modules/@silvia-odwyer/photon-node/**` so WASM artifact ships in VSIX | +| `package.json` | New runtime dependency `@silvia-odwyer/photon-node` ^0.3.4 | --- diff --git a/docs/features/14-20260803-model-picker-enhancements.md b/docs/features/14-20260803-model-picker-enhancements.md index f9fd2b1..5841457 100644 --- a/docs/features/14-20260803-model-picker-enhancements.md +++ b/docs/features/14-20260803-model-picker-enhancements.md @@ -75,14 +75,14 @@ New function `getContextSizeOptionsForModel(modelId, cost, fullContextWindow)` i ## Files -| File | Change | -|------|--------| -| `src/modelNames.ts` | New: `formatModelName` (extracted), `providerModelDisplayName` | -| `src/metadata.ts` | New: `getContextSizeOptionsForModel` (Kimi-aware tier synthesis) | -| `src/extension.ts` | Import `getContextSizeOptionsForModel` + `providerModelDisplayName`; `modelInfoProviders` array for setting-change refresh; config listener for `showProviderPrefix` | -| `package.json` | New setting `opencodego.showProviderPrefix` | -| `src/test/modelNames.test.ts` | New: numeric version formatting, prefix on/off | -| `src/test/metadata.test.ts` | New: Kimi tier synthesis, `k3` short id, 256K-boundary skip, models.dev precedence | +| File | Change | +| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `src/modelNames.ts` | New: `formatModelName` (extracted), `providerModelDisplayName` | +| `src/metadata.ts` | New: `getContextSizeOptionsForModel` (Kimi-aware tier synthesis) | +| `src/extension.ts` | Import `getContextSizeOptionsForModel` + `providerModelDisplayName`; `modelInfoProviders` array for setting-change refresh; config listener for `showProviderPrefix` | +| `package.json` | New setting `opencodego.showProviderPrefix` | +| `src/test/modelNames.test.ts` | New: numeric version formatting, prefix on/off | +| `src/test/metadata.test.ts` | New: Kimi tier synthesis, `k3` short id, 256K-boundary skip, models.dev precedence | --- diff --git a/docs/issues/01-20260515-qwen36-tool-call-loop.md b/docs/issues/01-20260515-qwen36-tool-call-loop.md index fa2bf30..0d3bdbc 100644 --- a/docs/issues/01-20260515-qwen36-tool-call-loop.md +++ b/docs/issues/01-20260515-qwen36-tool-call-loop.md @@ -2,9 +2,9 @@ # Qwen 3.6 Plus Free โ€” Tool-Call Loop Investigation -**Topic:** streaming / tool-calling / routing / models / provider -**Updated:** 2026-06-13 -**Tags:** #streaming #tool-calling #routing #models #qwen #anthropic-bridge #zen +**Topic:** streaming / tool-calling / routing / models / provider +**Updated:** 2026-06-13 +**Tags:** #streaming #tool-calling #routing #models #qwen #anthropic-bridge #zen **Supersedes:** โ€” --- diff --git a/docs/issues/02-20260516-context-size-correction.md b/docs/issues/02-20260516-context-size-correction.md index e570cac..773eab9 100644 --- a/docs/issues/02-20260516-context-size-correction.md +++ b/docs/issues/02-20260516-context-size-correction.md @@ -2,9 +2,9 @@ # Context Size Display Correction โ€” Per-Provider Model Limits -**Topic:** models / provider / metadata -**Updated:** 2026-05-16 -**Tags:** #models #provider #metadata #vscode #context-window #models-dev +**Topic:** models / provider / metadata +**Updated:** 2026-05-16 +**Tags:** #models #provider #metadata #vscode #context-window #models-dev **Supersedes:** โ€” --- diff --git a/docs/issues/03-20260516-unavailable-deprecated-model-filtering.md b/docs/issues/03-20260516-unavailable-deprecated-model-filtering.md index 88c39cd..048ccd2 100644 --- a/docs/issues/03-20260516-unavailable-deprecated-model-filtering.md +++ b/docs/issues/03-20260516-unavailable-deprecated-model-filtering.md @@ -2,9 +2,9 @@ # Unavailable and Deprecated Model Filtering -**Topic:** models / provider / registry / availability -**Updated:** 2026-05-16 -**Tags:** #models #provider #routing #vscode #byok #zen #go +**Topic:** models / provider / registry / availability +**Updated:** 2026-05-16 +**Tags:** #models #provider #routing #vscode #byok #zen #go **Supersedes:** โ€” --- diff --git a/docs/issues/04-20260517-pr1-freeonly-review-merge.md b/docs/issues/04-20260517-pr1-freeonly-review-merge.md index f237767..7ee17df 100644 --- a/docs/issues/04-20260517-pr1-freeonly-review-merge.md +++ b/docs/issues/04-20260517-pr1-freeonly-review-merge.md @@ -2,9 +2,9 @@ # PR #1 Review, Test, and Merge โ€” opencodego.freeOnly Setting -**Topic:** provider / models / open-source / community-contribution -**Updated:** 2026-05-17 -**Tags:** #provider #models #open-source #community #zen #freeOnly #byok +**Topic:** provider / models / open-source / community-contribution +**Updated:** 2026-05-17 +**Tags:** #provider #models #open-source #community #zen #freeOnly #byok **Supersedes:** โ€” --- diff --git a/docs/issues/05-20260517-thinking-mode-picker-configuration.md b/docs/issues/05-20260517-thinking-mode-picker-configuration.md index b08faee..d1f56c0 100644 --- a/docs/issues/05-20260517-thinking-mode-picker-configuration.md +++ b/docs/issues/05-20260517-thinking-mode-picker-configuration.md @@ -2,9 +2,9 @@ # Per-Model Thinking Mode โ€” Feature Implementation -**Topic:** provider / models / thinking / vscode / copilot-chat -**Updated:** 2026-05-17 -**Tags:** #provider #models #thinking #vscode #copilot-chat #reasoning #byok #feat +**Topic:** provider / models / thinking / vscode / copilot-chat +**Updated:** 2026-05-17 +**Tags:** #provider #models #thinking #vscode #copilot-chat #reasoning #byok #feat **Supersedes:** โ€” --- diff --git a/docs/issues/06-20260517-thinking-native-submenu-investigation.md b/docs/issues/06-20260517-thinking-native-submenu-investigation.md index 46a0e29..b73b96e 100644 --- a/docs/issues/06-20260517-thinking-native-submenu-investigation.md +++ b/docs/issues/06-20260517-thinking-native-submenu-investigation.md @@ -2,9 +2,9 @@ # Native Thinking Submenu โ€” Issue Investigation & v0.1.4 Release -**Topic:** vscode / thinking / copilot-chat / native-ui / configurationSchema / release -**Updated:** 2026-05-17 -**Tags:** #vscode #thinking #copilot-chat #native-ui #configurationSchema #byok #reasoning #tool-calling #streaming #release +**Topic:** vscode / thinking / copilot-chat / native-ui / configurationSchema / release +**Updated:** 2026-05-17 +**Tags:** #vscode #thinking #copilot-chat #native-ui #configurationSchema #byok #reasoning #tool-calling #streaming #release **Extends:** `05-20260517-thinking-mode-picker-configuration.md` --- diff --git a/docs/issues/07-20260517-zen-model-version-labels.md b/docs/issues/07-20260517-zen-model-version-labels.md index 4e5796b..c008c93 100644 --- a/docs/issues/07-20260517-zen-model-version-labels.md +++ b/docs/issues/07-20260517-zen-model-version-labels.md @@ -2,9 +2,9 @@ # Zen Model Version Labels โ€” Naming, Packaging, and Changelog Classification -**Topic:** models / vscode / picker / packaging / changelog -**Updated:** 2026-05-17 -**Tags:** #models #vscode #provider #zen #packaging #changelog +**Topic:** models / vscode / picker / packaging / changelog +**Updated:** 2026-05-17 +**Tags:** #models #vscode #provider #zen #packaging #changelog **Supersedes:** - --- diff --git a/docs/issues/08-20260520-vision-image-request-fixes.md b/docs/issues/08-20260520-vision-image-request-fixes.md index 3c76da7..a24226d 100644 --- a/docs/issues/08-20260520-vision-image-request-fixes.md +++ b/docs/issues/08-20260520-vision-image-request-fixes.md @@ -2,9 +2,9 @@ # Vision Image Requests โ€” Attachment Capability, Encoding, Qwen Budget, and v0.1.5 Release -**Topic:** vision / image-input / provider / qwen / models / packaging / release -**Updated:** 2026-05-20 -**Tags:** #vision #image-input #provider #qwen #models #vscode #packaging #thinking #release +**Topic:** vision / image-input / provider / qwen / models / packaging / release +**Updated:** 2026-05-20 +**Tags:** #vision #image-input #provider #qwen #models #vscode #packaging #thinking #release **Supersedes:** โ€” --- diff --git a/docs/issues/09-20260524-pr4-review-merge-release.md b/docs/issues/09-20260524-pr4-review-merge-release.md index a43a4ed..6fe1371 100644 --- a/docs/issues/09-20260524-pr4-review-merge-release.md +++ b/docs/issues/09-20260524-pr4-review-merge-release.md @@ -2,9 +2,9 @@ # PR #4 Review, Merge, and v0.1.6 Marketplace Release -**Topic:** routing / models / provider / release -**Updated:** 2026-05-24 -**Tags:** #routing #models #provider #release #pr-review #vision +**Topic:** routing / models / provider / release +**Updated:** 2026-05-24 +**Tags:** #routing #models #provider #release #pr-review #vision **Supersedes:** โ€” --- diff --git a/docs/issues/10-20260527-context-window-usage-pr6-integration.md b/docs/issues/10-20260527-context-window-usage-pr6-integration.md index 6be51c8..e5c8b85 100644 --- a/docs/issues/10-20260527-context-window-usage-pr6-integration.md +++ b/docs/issues/10-20260527-context-window-usage-pr6-integration.md @@ -2,9 +2,9 @@ # Context Window Usage Indicator and PR #6 Integration -**Topic:** usage / context-window / streaming / provider / release -**Updated:** 2026-05-27 -**Tags:** #usage #context-window #streaming #provider #vscode #byok #release #pr-review +**Topic:** usage / context-window / streaming / provider / release +**Updated:** 2026-05-27 +**Tags:** #usage #context-window #streaming #provider #vscode #byok #release #pr-review **Supersedes:** โ€” --- diff --git a/docs/issues/14-20260608-pr13-minimax-think-tags-review-merge-release.md b/docs/issues/14-20260608-pr13-minimax-think-tags-review-merge-release.md index da2eaff..1f675b4 100644 --- a/docs/issues/14-20260608-pr13-minimax-think-tags-review-merge-release.md +++ b/docs/issues/14-20260608-pr13-minimax-think-tags-review-merge-release.md @@ -2,9 +2,9 @@ # PR #13 Review, Merge, and v0.2.2 Release -**Topic:** streaming / models / provider -**Updated:** 2026-06-08 -**Tags:** #streaming #models #minimax #thinking #community-pr +**Topic:** streaming / models / provider +**Updated:** 2026-06-08 +**Tags:** #streaming #models #minimax #thinking #community-pr **Supersedes:** โ€” --- diff --git a/docs/issues/15-20260609-output-channel-cleanup-textdecoder-fix.md b/docs/issues/15-20260609-output-channel-cleanup-textdecoder-fix.md index 026eeaf..ae5fa6b 100644 --- a/docs/issues/15-20260609-output-channel-cleanup-textdecoder-fix.md +++ b/docs/issues/15-20260609-output-channel-cleanup-textdecoder-fix.md @@ -2,9 +2,9 @@ # Output Channel Cleanup & Buffer TypeScript Fix -**Topic:** extension / output channel / debug logging / TypeScript -**Updated:** 2026-06-09 -**Tags:** #extension #fix #output-channel #debug #typescript +**Topic:** extension / output channel / debug logging / TypeScript +**Updated:** 2026-06-09 +**Tags:** #extension #fix #output-channel #debug #typescript **Supersedes:** โ€” --- diff --git a/docs/issues/17-20260609-project-cleanup-immediate-bugfixes.md b/docs/issues/17-20260609-project-cleanup-immediate-bugfixes.md index 4489bb4..c7c4b82 100644 --- a/docs/issues/17-20260609-project-cleanup-immediate-bugfixes.md +++ b/docs/issues/17-20260609-project-cleanup-immediate-bugfixes.md @@ -2,9 +2,9 @@ # Project Cleanup โ€” Immediate Bug Fixes & Improvement Analysis -**Topic:** cleanup / bugs / config -**Updated:** 2026-06-09 -**Tags:** #cleanup #bugs #config #activation #changelog +**Topic:** cleanup / bugs / config +**Updated:** 2026-06-09 +**Tags:** #cleanup #bugs #config #activation #changelog **Supersedes:** โ€” --- diff --git a/docs/issues/18-20260610-pr15-context-size-reasoning-review.md b/docs/issues/18-20260610-pr15-context-size-reasoning-review.md index d3d248d..e5c4963 100644 --- a/docs/issues/18-20260610-pr15-context-size-reasoning-review.md +++ b/docs/issues/18-20260610-pr15-context-size-reasoning-review.md @@ -2,9 +2,9 @@ # PR #15 Review โ€” Context-Size Tiers, Models.dev Reasoning Options, and Richer Thinking Efforts -**Topic:** models / thinking / provider / metadata -**Updated:** 2026-06-13 -**Tags:** #models #thinking #reasoning #modelsdev #community-pr #context-size #pricing +**Topic:** models / thinking / provider / metadata +**Updated:** 2026-06-13 +**Tags:** #models #thinking #reasoning #modelsdev #community-pr #context-size #pricing **Supersedes:** โ€” --- @@ -13,9 +13,9 @@ Full review of community contributor PR #15 by [Wallacy](https://github.com/Wallacy), which adds three tightly related features: **Context Size selector** for tiered-pricing models, **dynamic reasoning options** from models.dev, and **richer thinking effort levels** for DeepSeek/Mimo/MiniMax families. Includes code analysis, risk assessment, and review feedback posted to GitHub. -**PR:** [ltmoerdani/opencode-copilot-chat#15](https://github.com/ltmoerdani/opencode-copilot-chat/pull/15) -**Branch:** `feature/mimo-think` โ†’ `main` -**Author:** Wallacy Freitas +**PR:** [ltmoerdani/opencode-copilot-chat#15](https://github.com/ltmoerdani/opencode-copilot-chat/pull/15) +**Branch:** `feature/mimo-think` โ†’ `main` +**Author:** Wallacy Freitas **Files changed:** 5 (+487 / โˆ’57) --- @@ -184,8 +184,8 @@ export interface ContextSizeOption { Review performed by reading the full diff (38KB) via `gh pr diff 15`. No local build or install was performed โ€” PR is still OPEN awaiting merge. -**CI Status:** โœ… GitGuardian Security Checks โ€” No secrets detected. -**Mergeable:** โœ… Yes +**CI Status:** โœ… GitGuardian Security Checks โ€” No secrets detected. +**Mergeable:** โœ… Yes **Reviews:** None yet (review feedback to be posted by maintainer) --- diff --git a/docs/issues/19-20260611-pr18-kimi-thinking-format-review.md b/docs/issues/19-20260611-pr18-kimi-thinking-format-review.md index 6a49587..638051e 100644 --- a/docs/issues/19-20260611-pr18-kimi-thinking-format-review.md +++ b/docs/issues/19-20260611-pr18-kimi-thinking-format-review.md @@ -2,9 +2,9 @@ # PR #18 Review โ€” Fix Kimi Thinking Format and Update Documentation -**Topic:** models / thinking / provider -**Updated:** 2026-06-11 -**Tags:** #models #thinking #kimi #community-pr #bugfix +**Topic:** models / thinking / provider +**Updated:** 2026-06-11 +**Tags:** #models #thinking #kimi #community-pr #bugfix **Supersedes:** โ€” --- @@ -13,9 +13,9 @@ Full review and community feedback for contributor PR #18 by [Wallacy](https://github.com/Wallacy), which fixes the Kimi (MoonshotAI) thinking payload format. The extension was sending `enable_thinking: true | false` but the OpenCode Go gateway rejects this field with HTTP 400: "Extra inputs are not permitted". The correct format is `thinking: { type: "enabled" | "disabled" }` โ€” matching the GLM family format. -**PR:** [ltmoerdani/opencode-copilot-chat#18](https://github.com/ltmoerdani/opencode-copilot-chat/pull/18) -**Branch:** `fix/kimi-thinking-format` โ†’ `main` -**Author:** Wallacy Freitas +**PR:** [ltmoerdani/opencode-copilot-chat#18](https://github.com/ltmoerdani/opencode-copilot-chat/pull/18) +**Branch:** `fix/kimi-thinking-format` โ†’ `main` +**Author:** Wallacy Freitas **Files changed:** 3 (+14 / โˆ’5) --- diff --git a/docs/issues/21-20260613-minimax-m3-think-tag-leak-reimplementation.md b/docs/issues/21-20260613-minimax-m3-think-tag-leak-reimplementation.md index 7ae6235..996093f 100644 --- a/docs/issues/21-20260613-minimax-m3-think-tag-leak-reimplementation.md +++ b/docs/issues/21-20260613-minimax-m3-think-tag-leak-reimplementation.md @@ -2,9 +2,9 @@ # MiniMax M3 `` Tag Leak โ€” Reimplementation -**Topic:** streaming / models / thinking / provider -**Updated:** 2026-06-13 -**Tags:** #streaming #models #minimax #thinking #bugfix +**Topic:** streaming / models / thinking / provider +**Updated:** 2026-06-13 +**Tags:** #streaming #models #minimax #thinking #bugfix **Supersedes:** โ€” --- diff --git a/docs/issues/22-20260614-thinking-off-missing-for-effort-only-schemas.md b/docs/issues/22-20260614-thinking-off-missing-for-effort-only-schemas.md index 7105c41..465b1a0 100644 --- a/docs/issues/22-20260614-thinking-off-missing-for-effort-only-schemas.md +++ b/docs/issues/22-20260614-thinking-off-missing-for-effort-only-schemas.md @@ -2,11 +2,11 @@ # Thinking Effort "Off" Missing for models.dev Effort-Only Schemas -**Topic:** thinking / reasoning / models.dev / ui -**Updated:** 2026-06-14 -**Tags:** #thinking #reasoning #models-dev #ui #bug -**GitHub Issue:** [#35](https://github.com/ltmoerdani/opencode-copilot-chat/issues/35) -**GitHub PR:** [#38](https://github.com/ltmoerdani/opencode-copilot-chat/pull/38) +**Topic:** thinking / reasoning / models.dev / ui +**Updated:** 2026-06-14 +**Tags:** #thinking #reasoning #models-dev #ui #bug +**GitHub Issue:** [#35](https://github.com/ltmoerdani/opencode-copilot-chat/issues/35) +**GitHub PR:** [#38](https://github.com/ltmoerdani/opencode-copilot-chat/pull/38) **Reporter/Fixer:** [@sublimode](https://github.com/sublimode) --- diff --git a/docs/issues/23-20260615-thinking-style-setting-not-respected.md b/docs/issues/23-20260615-thinking-style-setting-not-respected.md index b7ace9e..5321ba2 100644 --- a/docs/issues/23-20260615-thinking-style-setting-not-respected.md +++ b/docs/issues/23-20260615-thinking-style-setting-not-respected.md @@ -6,12 +6,12 @@ # `chat.agent.thinkingStyle` Not Respected โ€” Reasoning Always Expanded -**Topic:** thinking / reasoning / vscode / byok / copilot-chat -**Updated:** 2026-06-15 -**Tags:** #thinking #reasoning #vscode #byok #copilot-chat -**GitHub Issue:** [#22](https://github.com/ltmoerdani/opencode-copilot-chat/issues/22) -**Upstream Blocker:** [microsoft/vscode#318211](https://github.com/microsoft/vscode/issues/318211) -**Reporter:** [@hu3bi](https://github.com/hu3bi) +**Topic:** thinking / reasoning / vscode / byok / copilot-chat +**Updated:** 2026-06-15 +**Tags:** #thinking #reasoning #vscode #byok #copilot-chat +**GitHub Issue:** [#22](https://github.com/ltmoerdani/opencode-copilot-chat/issues/22) +**Upstream Blocker:** [microsoft/vscode#318211](https://github.com/microsoft/vscode/issues/318211) +**Reporter:** [@hu3bi](https://github.com/hu3bi) **Participants:** [@hu3bi](https://github.com/hu3bi), [@Wallacy](https://github.com/Wallacy), [@sublimode](https://github.com/sublimode) --- diff --git a/docs/issues/24-20260615-kimi-k27-temperature-thinking-rejection.md b/docs/issues/24-20260615-kimi-k27-temperature-thinking-rejection.md index c0ad5ab..bce8468 100644 --- a/docs/issues/24-20260615-kimi-k27-temperature-thinking-rejection.md +++ b/docs/issues/24-20260615-kimi-k27-temperature-thinking-rejection.md @@ -2,11 +2,11 @@ # Kimi K2.7-Code Rejects `temperature` and `thinking.type: "disabled"` โ€” Dual 400 Errors -**Topic:** models / thinking / temperature / provider / kimi -**Updated:** 2026-06-15 -**Tags:** #models #thinking #kimi #temperature #breaking-change #bugfix -**GitHub Issue:** [#25](https://github.com/ltmoerdani/opencode-copilot-chat/issues/25) -**Related:** [#20](./20-20260611-pr18-kimi-thinking-format-review.md) (Kimi thinking format fix for K2.6/K2.5) +**Topic:** models / thinking / temperature / provider / kimi +**Updated:** 2026-06-15 +**Tags:** #models #thinking #kimi #temperature #breaking-change #bugfix +**GitHub Issue:** [#25](https://github.com/ltmoerdani/opencode-copilot-chat/issues/25) +**Related:** [#20](./20-20260611-pr18-kimi-thinking-format-review.md) (Kimi thinking format fix for K2.6/K2.5) **Reporters:** [@JacksApps](https://github.com/JacksApps), [@Tynamix](https://github.com/Tynamix) --- diff --git a/docs/issues/25-20260617-inline-completions-fim-research.md b/docs/issues/25-20260617-inline-completions-fim-research.md index 0a73d72..f19dcad 100644 --- a/docs/issues/25-20260617-inline-completions-fim-research.md +++ b/docs/issues/25-20260617-inline-completions-fim-research.md @@ -2,12 +2,12 @@ # Inline Code Suggestions (Ghost Text) โ€” FIM Endpoint Research & Execution Options -**Topic:** inline-completions / fim / autocomplete / ghost-text / deepseek / qwen-coder / ollama / byok -**Updated:** 2026-06-17 -**Tags:** #inline-completions #fim #autocomplete #ghost-text #deepseek #qwen-coder #ollama #byok #research #feature-request -**GitHub Issue:** [#49](https://github.com/ltmoerdani/opencode-copilot-chat/issues/49) โ€” _[FEATURE] Add inline code suggestions with selectable AI model (Copilot-like experience)_ -**Related Branch (external):** [`Wallacy/opencode-copilot-chat@feat/persistent-autocomplete`](https://github.com/Wallacy/opencode-copilot-chat/tree/feat/persistent-autocomplete) -**Reporter (issue):** [@lorelore789](https://github.com/lorelore789) +**Topic:** inline-completions / fim / autocomplete / ghost-text / deepseek / qwen-coder / ollama / byok +**Updated:** 2026-06-17 +**Tags:** #inline-completions #fim #autocomplete #ghost-text #deepseek #qwen-coder #ollama #byok #research #feature-request +**GitHub Issue:** [#49](https://github.com/ltmoerdani/opencode-copilot-chat/issues/49) โ€” _[FEATURE] Add inline code suggestions with selectable AI model (Copilot-like experience)_ +**Related Branch (external):** [`Wallacy/opencode-copilot-chat@feat/persistent-autocomplete`](https://github.com/Wallacy/opencode-copilot-chat/tree/feat/persistent-autocomplete) +**Reporter (issue):** [@lorelore789](https://github.com/lorelore789) **Prior Art (comment):** [@Wallacy](https://github.com/Wallacy) โ€” explored implementation, blocked on latency --- diff --git a/docs/issues/26-20260623-pr53-model-picker-crash-1-126.md b/docs/issues/26-20260623-pr53-model-picker-crash-1-126.md index 15c5890..43e92a6 100644 --- a/docs/issues/26-20260623-pr53-model-picker-crash-1-126.md +++ b/docs/issues/26-20260623-pr53-model-picker-crash-1-126.md @@ -2,9 +2,9 @@ # PR #53 โ€” Model Picker Crash & Duplication on VS Code โ‰ฅ1.126 -**Topic:** models / provider / byok / vscode -**Updated:** 2026-06-23 -**Tags:** #models #provider #byok #vscode #bugfix #community +**Topic:** models / provider / byok / vscode +**Updated:** 2026-06-23 +**Tags:** #models #provider #byok #vscode #bugfix #community **Supersedes:** โ€” --- @@ -20,8 +20,8 @@ The crash blocked all model selection on 1.126. The duplication made the picker This document covers the root cause of both regressions, the two iterations the contributor (@Wallacy) went through, and the final approach that shipped. -**Documented:** 2026-06-23 -**Fixed in:** v0.3.4 (PR [#53](https://github.com/ltmoerdani/opencode-copilot-chat/pull/53)) +**Documented:** 2026-06-23 +**Fixed in:** v0.3.4 (PR [#53](https://github.com/ltmoerdani/opencode-copilot-chat/pull/53)) **Issue report:** [#51](https://github.com/ltmoerdani/opencode-copilot-chat/issues/51) --- diff --git a/docs/issues/27-20260624-pr54-security-hardening-cleanup.md b/docs/issues/27-20260624-pr54-security-hardening-cleanup.md index 44b4a38..7cf81db 100644 --- a/docs/issues/27-20260624-pr54-security-hardening-cleanup.md +++ b/docs/issues/27-20260624-pr54-security-hardening-cleanup.md @@ -2,10 +2,10 @@ # PR #54 โ€” Security Hardening and Optimization Cleanup -**Topic:** security / memory / dead code / cleanup -**Updated:** 2026-06-24 -**Tags:** #security #memory-leak #dead-code #cleanup #community -**Supersedes:** โ€” +**Topic:** security / memory / dead code / cleanup +**Updated:** 2026-06-24 +**Tags:** #security #memory-leak #dead-code #cleanup #community +**Supersedes:** โ€” **Depends on:** PR [#53](https://github.com/ltmoerdani/opencode-copilot-chat/pull/53) --- @@ -20,9 +20,9 @@ Follow-up to PR #53 that addresses three categories of issues identified during This PR contains **zero behavioral changes**. All fixes are internal: security, memory bounds, and dead code removal. -**Documented:** 2026-06-24 -**Fixed in:** v0.3.4 (PR [#54](https://github.com/ltmoerdani/opencode-copilot-chat/pull/54)) -**Contributor:** [@Wallacy](https://github.com/Wallacy) +**Documented:** 2026-06-24 +**Fixed in:** v0.3.4 (PR [#54](https://github.com/ltmoerdani/opencode-copilot-chat/pull/54)) +**Contributor:** [@Wallacy](https://github.com/Wallacy) **Commits:** 2 (`afd26e3`, `b7111a9`) --- diff --git a/docs/issues/28-20260615-pr42-pr43-duplicate-agent-host-model-fix.md b/docs/issues/28-20260615-pr42-pr43-duplicate-agent-host-model-fix.md index b0bc8bc..8e951d8 100644 --- a/docs/issues/28-20260615-pr42-pr43-duplicate-agent-host-model-fix.md +++ b/docs/issues/28-20260615-pr42-pr43-duplicate-agent-host-model-fix.md @@ -2,13 +2,13 @@ # PR #42 / PR #43 โ€” Duplicate Agent-Host Model Fix (Issue #41) -**Topic:** models / vscode / agents-window / byok / routing -**Updated:** 2026-06-15 -**Tags:** #models #agents-window #byok #duplicate #routing #vendor #community-pr -**GitHub Issue:** [ltmoerdani/opencode-copilot-chat#41](https://github.com/ltmoerdani/opencode-copilot-chat/issues/41) -**GitHub PR:** [#42](https://github.com/ltmoerdani/opencode-copilot-chat/pull/42) (by [@Marinski](https://github.com/Marinski)) โ€” opt-in gate hotfix -**GitHub PR:** [#43](https://github.com/ltmoerdani/opencode-copilot-chat/pull/43) (by [@Wallacy](https://github.com/Wallacy)) โ€” separate vendor IDs (final solution) -**Related Feature Doc:** [`docs/features/06-20260614-agents-window-model-visibility.md`](../features/06-20260614-agents-window-model-visibility.md) +**Topic:** models / vscode / agents-window / byok / routing +**Updated:** 2026-06-15 +**Tags:** #models #agents-window #byok #duplicate #routing #vendor #community-pr +**GitHub Issue:** [ltmoerdani/opencode-copilot-chat#41](https://github.com/ltmoerdani/opencode-copilot-chat/issues/41) +**GitHub PR:** [#42](https://github.com/ltmoerdani/opencode-copilot-chat/pull/42) (by [@Marinski](https://github.com/Marinski)) โ€” opt-in gate hotfix +**GitHub PR:** [#43](https://github.com/ltmoerdani/opencode-copilot-chat/pull/43) (by [@Wallacy](https://github.com/Wallacy)) โ€” separate vendor IDs (final solution) +**Related Feature Doc:** [`docs/features/06-20260614-agents-window-model-visibility.md`](../features/06-20260614-agents-window-model-visibility.md) **Supersedes:** PR #42 (`showInAgentsWindow` setting replaced by `agentsWindow` + `showAgentModelsInManagePanel`) --- diff --git a/docs/issues/32-20260708-vscode-128-byok-utility-model.md b/docs/issues/32-20260708-vscode-128-byok-utility-model.md index b21a991..ea2fdb0 100644 --- a/docs/issues/32-20260708-vscode-128-byok-utility-model.md +++ b/docs/issues/32-20260708-vscode-128-byok-utility-model.md @@ -4,9 +4,9 @@ > > **Current behavior:** The extension no longer changes global utility-model settings during activation. Run `OpenCode: Configure Utility Models` to choose `chat.byokUtilityModelDefault`, `chat.utilityModel`, or `chat.utilitySmallModel` explicitly. The implementation below is retained as release history for version 0.3.6. > -> **Date:** July 8, 2026 -> **Extension version:** 0.3.6 -> **Severity:** High โ€” every background utility task (chat title generation, commit messages, intent detection) broken for all BYOK users after updating VS Code +> **Date:** July 8, 2026 +> **Extension version:** 0.3.6 +> **Severity:** High โ€” every background utility task (chat title generation, commit messages, intent detection) broken for all BYOK users after updating VS Code > **Root Cause:** VS Code 1.128 introduced `chat.byokUtilityModelDefault` with default value `"none"`, disabling utility models for BYOK extensions that do not explicitly configure one. --- diff --git a/docs/issues/33-20260709-thinking-part-byok-surfacing-research.md b/docs/issues/33-20260709-thinking-part-byok-surfacing-research.md index 544b264..d1d8d64 100644 --- a/docs/issues/33-20260709-thinking-part-byok-surfacing-research.md +++ b/docs/issues/33-20260709-thinking-part-byok-surfacing-research.md @@ -2,16 +2,16 @@ # Reasoning Not Surfaced as Thinking Part โ€” Issues #22 + #71 (Duplicate) -**Topic:** thinking / reasoning / vscode / byok / copilot-chat / streaming / languageModelThinkingPart -**Updated:** 2026-07-09 -**Tags:** #thinking #reasoning #vscode #byok #copilot-chat #streaming #languageModelThinkingPart #upstream -**GitHub Issues:** [#22](https://github.com/ltmoerdani/opencode-copilot-chat/issues/22), [#71](https://github.com/ltmoerdani/opencode-copilot-chat/issues/71) -**Fixed in:** v0.3.7 (branch `fix/thinking-part-byok-surfacing-22-71`) +**Topic:** thinking / reasoning / vscode / byok / copilot-chat / streaming / languageModelThinkingPart +**Updated:** 2026-07-09 +**Tags:** #thinking #reasoning #vscode #byok #copilot-chat #streaming #languageModelThinkingPart #upstream +**GitHub Issues:** [#22](https://github.com/ltmoerdani/opencode-copilot-chat/issues/22), [#71](https://github.com/ltmoerdani/opencode-copilot-chat/issues/71) +**Fixed in:** v0.3.7 (branch `fix/thinking-part-byok-surfacing-22-71`) **Manual test:** โœ… Verified with DeepSeek + Kimi in Copilot Chat (2026-07-09) -**Supersedes:** [`23-20260615-thinking-style-setting-not-respected.md`](./23-20260615-thinking-style-setting-not-respected.md) (marked deprecated โ€” conclusion overturned) -**Upstream (still open, NOT a blocker):** [microsoft/vscode#318211](https://github.com/microsoft/vscode/issues/318211) -**Proof-of-concept:** [`Vizards/deepseek-v4-for-copilot`](https://github.com/Vizards/deepseek-v4-for-copilot) v0.6.2 (Marketplace, working) -**Reporters:** [@hu3bi](https://github.com/hu3bi) (#22), [@alexaroth](https://github.com/alexaroth) (#71) +**Supersedes:** [`23-20260615-thinking-style-setting-not-respected.md`](./23-20260615-thinking-style-setting-not-respected.md) (marked deprecated โ€” conclusion overturned) +**Upstream (still open, NOT a blocker):** [microsoft/vscode#318211](https://github.com/microsoft/vscode/issues/318211) +**Proof-of-concept:** [`Vizards/deepseek-v4-for-copilot`](https://github.com/Vizards/deepseek-v4-for-copilot) v0.6.2 (Marketplace, working) +**Reporters:** [@hu3bi](https://github.com/hu3bi) (#22), [@alexaroth](https://github.com/alexaroth) (#71) **Participants:** [@hu3bi](https://github.com/hu3bi), [@alexaroth](https://github.com/alexaroth), [@yinhx3](https://github.com/yinhx3), [@Wallacy](https://github.com/Wallacy), [@sublimode](https://github.com/sublimode) --- diff --git a/docs/issues/36-20260723-mimo-thinking-infinite-loop.md b/docs/issues/36-20260723-mimo-thinking-infinite-loop.md index 43487ac..d84c7a8 100644 --- a/docs/issues/36-20260723-mimo-thinking-infinite-loop.md +++ b/docs/issues/36-20260723-mimo-thinking-infinite-loop.md @@ -2,8 +2,8 @@ # MiMo 2.5 โ€” Thinking Loops + Go Gateway Reasoning Leak (#36) -**Topic:** thinking / mimo / streaming / gateway / workaround -**Reported:** 2026-07-23 +**Topic:** thinking / mimo / streaming / gateway / workaround +**Reported:** 2026-07-23 **Tags:** #thinking #mimo #streaming #gateway #workaround #bug --- @@ -48,7 +48,7 @@ POST https://opencode.ai/zen/go/v1/chat/completions โ†’ Non-streaming endpoint returns `content` correctly (only streaming affected) ``` -**Affected:** ALL opencode-go models (deepseek, kimi, glm, mimo, minimax, qwen, grok). +**Affected:** ALL opencode-go models (deepseek, kimi, glm, mimo, minimax, qwen, grok). **Not affected:** Zen gateway (`/zen/v1/`). Related upstream issues: diff --git a/docs/issues/37-20260724-estimate-token-count-overflow.md b/docs/issues/37-20260724-estimate-token-count-overflow.md index c6c56a9..483cf9d 100644 --- a/docs/issues/37-20260724-estimate-token-count-overflow.md +++ b/docs/issues/37-20260724-estimate-token-count-overflow.md @@ -2,9 +2,9 @@ # estimateTokenCount Overestimation Causes `max_tokens: 1` on Large Conversations -**Topic:** streaming / models / context-window -**Updated:** 2026-07-24 -**Tags:** #streaming #models #bug #estimateTokenCount #context-window #max-tokens +**Topic:** streaming / models / context-window +**Updated:** 2026-07-24 +**Tags:** #streaming #models #bug #estimateTokenCount #context-window #max-tokens **Fixes:** [#83](https://github.com/ltmoerdani/opencode-copilot-chat/issues/83) --- @@ -13,8 +13,8 @@ When a user has a large conversation that approaches the model's context window limit (~754K tokens on a 1M window), the extension sends `max_tokens: 1` to the API. The model generates exactly 1 token, hits `finishReason: length`, and the user sees an empty or 1-word response. The chat becomes unusable until the conversation is cleared. -**Reported:** [#83](https://github.com/ltmoerdani/opencode-copilot-chat/issues/83) by @gwynnbleiidd (2026-07-24) -**Environment:** Extension v0.4.2, VS Code 1.130.0, OpenCode Go provider +**Reported:** [#83](https://github.com/ltmoerdani/opencode-copilot-chat/issues/83) by @gwynnbleiidd (2026-07-24) +**Environment:** Extension v0.4.2, VS Code 1.130.0, OpenCode Go provider **Affected models:** `deepseek-v4-flash`, `mimo-v2.5`, `hy3` (all Go models with large context windows) ### Symptoms diff --git a/docs/issues/50-20260803-pr100-tool-call-flush-review-merge.md b/docs/issues/50-20260803-pr100-tool-call-flush-review-merge.md index 691b90d..c7e5243 100644 --- a/docs/issues/50-20260803-pr100-tool-call-flush-review-merge.md +++ b/docs/issues/50-20260803-pr100-tool-call-flush-review-merge.md @@ -2,9 +2,9 @@ # PR #100 Review, Merge, and v0.4.5 Release -**Topic:** streaming / tool-calling / provider -**Updated:** 2026-08-08 -**Tags:** #streaming #tool-calling #community-pr #deepseek #regression +**Topic:** streaming / tool-calling / provider +**Updated:** 2026-08-08 +**Tags:** #streaming #tool-calling #community-pr #deepseek #regression **Supersedes:** โ€” --- @@ -58,9 +58,9 @@ Full root-cause analysis: [`docs/issues/42-20260803-premature-tool-call-flush.md ## PR #100 Changes -**Author:** [@xianhongtao](https://github.com/xianhongtao) (external contributor) -**Branch:** `xianhongtao/issue98` โ†’ `main` -**Size:** +532 / โˆ’78, 8 files +**Author:** [@xianhongtao](https://github.com/xianhongtao) (external contributor) +**Branch:** `xianhongtao/issue98` โ†’ `main` +**Size:** +532 / โˆ’78, 8 files **Commits:** 3 (initial fix + 2 follow-ups from review feedback) ### Commit 1 โ€” `b36ecf2e` "fix(streaming): flush tool calls only at stream end (fixes #98)" @@ -138,16 +138,16 @@ Copilot AI review: reviewed 6/7 files, generated no comments. ## Files Touched -| File | Status | Purpose | -| --- | --- | --- | -| `src/toolCallAccumulator.ts` | new (141 lines) | Pure tool-call accumulator module | -| `src/test/toolCallAccumulator.test.ts` | new (197 lines) | 15 unit test cases | -| `src/streaming.ts` | modified | `OpenAiResponseExtractor` integration + transport flush calls | -| `package.json` | modified | Version bump 0.4.4 โ†’ 0.4.5 | -| `package-lock.json` | modified | Version bump 0.4.4 โ†’ 0.4.5 (2 locations) | -| `CHANGELOG.md` | modified | 0.4.5 entry | -| `docs/issues/42-20260803-premature-tool-call-flush.md` | new | Root-cause analysis + fix details | -| `docs/devlog.md` | modified | Session handoff entry | +| File | Status | Purpose | +| ------------------------------------------------------ | --------------- | ------------------------------------------------------------- | +| `src/toolCallAccumulator.ts` | new (141 lines) | Pure tool-call accumulator module | +| `src/test/toolCallAccumulator.test.ts` | new (197 lines) | 15 unit test cases | +| `src/streaming.ts` | modified | `OpenAiResponseExtractor` integration + transport flush calls | +| `package.json` | modified | Version bump 0.4.4 โ†’ 0.4.5 | +| `package-lock.json` | modified | Version bump 0.4.4 โ†’ 0.4.5 (2 locations) | +| `CHANGELOG.md` | modified | 0.4.5 entry | +| `docs/issues/42-20260803-premature-tool-call-flush.md` | new | Root-cause analysis + fix details | +| `docs/devlog.md` | modified | Session handoff entry | --- diff --git a/docs/issues/51-20260807-pr107-transient-5xx-retry-merge.md b/docs/issues/51-20260807-pr107-transient-5xx-retry-merge.md index 4773635..6e902d2 100644 --- a/docs/issues/51-20260807-pr107-transient-5xx-retry-merge.md +++ b/docs/issues/51-20260807-pr107-transient-5xx-retry-merge.md @@ -44,11 +44,11 @@ export function isTransientServerError(status: number, errorDetail: string): boo Constants exported for callers and tests: -| Constant | Value | Purpose | -| --- | --- | --- | -| `TRANSIENT_5XX_MAX_RETRIES` | `2` | Hard cap on retries before surfacing the error | -| `TRANSIENT_5XX_RETRY_BASE_MS` | `1000` | Base backoff, doubles per attempt (1s, 2s) | -| `TRANSIENT_5XX_RETRY_JITTER_MS` | `250` | Max random jitter added per backoff | +| Constant | Value | Purpose | +| ------------------------------- | ------ | ---------------------------------------------- | +| `TRANSIENT_5XX_MAX_RETRIES` | `2` | Hard cap on retries before surfacing the error | +| `TRANSIENT_5XX_RETRY_BASE_MS` | `1000` | Base backoff, doubles per attempt (1s, 2s) | +| `TRANSIENT_5XX_RETRY_JITTER_MS` | `250` | Max random jitter added per backoff | ### 2. Streaming retry loop + cancellation-aware sleep (`src/streaming.ts`) @@ -56,10 +56,7 @@ Constants exported for callers and tests: - After the HTTP 400 retry block, a `while` loop retries up to `TRANSIENT_5XX_MAX_RETRIES` times whenever `isTransientServerError(response.status, consumedErrorBody ?? "")` returns true. Backoff is exponential with jitter: ```ts - const backoffMs = Math.round( - TRANSIENT_5XX_RETRY_BASE_MS * 2 ** (attempt - 1) + - Math.random() * TRANSIENT_5XX_RETRY_JITTER_MS, - ); + const backoffMs = Math.round(TRANSIENT_5XX_RETRY_BASE_MS * 2 ** (attempt - 1) + Math.random() * TRANSIENT_5XX_RETRY_JITTER_MS); ``` - `sleepWithCancellation(ms, token)` waits for the backoff but aborts early if the user cancels. The cancellation listener is registered **before** `setTimeout` and resolves the promise on fire, so there is no window where a cancellation arrives between `await` resuming and the listener being removed. @@ -82,7 +79,7 @@ Five `node:test` cases for `isTransientServerError`: The two retry families chain in a single request lifecycle: -``` +```text fetch โ†’ 400? โ†’ analyzeHttp400ForRetry โ†’ patch body โ†’ fetch โ†’ 5xx? โ†’ retry up to 2ร— (backoff + jitter) โ†’ surface error ``` @@ -97,12 +94,12 @@ Worst case for one user request is 4 fetches: initial + 1 HTTP 400 patched retry **Merged:** 2026-08-07 (merge commit `6d519f7`, merge not squash) **Final size:** +167 / -34, 4 files -| File | Change | -| --- | --- | -| `src/retry.ts` | Added `isTransientServerError`, `TRANSIENT_5XX_*` constants, rewrote header comment to document both retry families | -| `src/streaming.ts` | Added `fetchWithBody`, `sleepWithCancellation`, transient 5xx retry loop with jitter, stale body resets; fixed 3 pre-existing lint issues | -| `src/errors.ts` | Added `describeRouterUnavailable` for actionable user-facing hint | -| `src/test/retry.test.ts` | 5 unit tests for `isTransientServerError` | +| File | Change | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `src/retry.ts` | Added `isTransientServerError`, `TRANSIENT_5XX_*` constants, rewrote header comment to document both retry families | +| `src/streaming.ts` | Added `fetchWithBody`, `sleepWithCancellation`, transient 5xx retry loop with jitter, stale body resets; fixed 3 pre-existing lint issues | +| `src/errors.ts` | Added `describeRouterUnavailable` for actionable user-facing hint | +| `src/test/retry.test.ts` | 5 unit tests for `isTransientServerError` | The original submission also bundled a husky + eslint + markdownlint + AGENTS.md + tsconfig stack; per review feedback that stack was split off to PR [#110](https://github.com/ltmoerdani/opencode-copilot-chat/pull/110) and the standalone `AGENTS.md` was dropped in favor of folding relevant guidance into `CONTRIBUTING.md`. @@ -110,14 +107,14 @@ The original submission also bundled a husky + eslint + markdownlint + AGENTS.md ## Review Findings -| # | Concern | Severity | Resolution | -| --- | --- | --- | --- | -| 1 | Scope creep: PR title said `fix(streaming)` but 6 of 11 files were tooling/docs/build | High | Split into two PRs; this PR narrowed to 4 source files | -| 2 | No jitter on backoff; concurrent retries could thundering-herd the gateway | Medium | Added `+ Math.random() * TRANSIENT_5XX_RETRY_JITTER_MS` (โ‰ค250ms) | -| 3 | 400โ†’5xx handoff could trigger up to 4 fetches per request | Low | Confirmed intentional; `consumedErrorBody` reset documented | -| 4 | `compactErrorCode` regex could false-positive on `*routerunavailable*` substrings | Low | Accepted; real-world error bodies don't contain that substring outside the genuine condition | -| 5 | `AGENTS.md` conflicted with project workflow (e.g. "never run build, user's job") | Medium | Dropped; relevant parts folded into `CONTRIBUTING.md` in PR #110 | -| 6 | `tsconfig.json` `"include": ["src"]` change | Low | Moved to PR #110 with the rest of the build tooling | +| # | Concern | Severity | Resolution | +| --- | ------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------- | +| 1 | Scope creep: PR title said `fix(streaming)` but 6 of 11 files were tooling/docs/build | High | Split into two PRs; this PR narrowed to 4 source files | +| 2 | No jitter on backoff; concurrent retries could thundering-herd the gateway | Medium | Added `+ Math.random() * TRANSIENT_5XX_RETRY_JITTER_MS` (โ‰ค250ms) | +| 3 | 400โ†’5xx handoff could trigger up to 4 fetches per request | Low | Confirmed intentional; `consumedErrorBody` reset documented | +| 4 | `compactErrorCode` regex could false-positive on `*routerunavailable*` substrings | Low | Accepted; real-world error bodies don't contain that substring outside the genuine condition | +| 5 | `AGENTS.md` conflicted with project workflow (e.g. "never run build, user's job") | Medium | Dropped; relevant parts folded into `CONTRIBUTING.md` in PR #110 | +| 6 | `tsconfig.json` `"include": ["src"]` change | Low | Moved to PR #110 with the rest of the build tooling | --- diff --git a/eslint.config.mjs b/eslint.config.mjs index 7601852..9ae9fb2 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,32 +1,85 @@ +// ESLint flat config โ€” MAXIMUM strictness. +// +// Stack (strictest available for each layer): +// - typescript-eslint `strict` + `strictTypeChecked` + `stylistic`: +// type-aware rules (no-unsafe-*, no-unnecessary-condition, ...), +// strict correctness rules, and stylistic consistency. +// - eslint-plugin-yml `flat/standard`: strict YAML linting. +// - eslint-plugin-jsonc `flat/recommended-with-jsonc`: strict JSON/JSONC. +// +// Zero tolerance: every violation is an error; warnings are turned into errors +// via `--max-warnings 0` in package.json. Nothing is disabled here except what +// the strict stacks themselves permit. + import { readFileSync } from "node:fs"; +import { defineConfig } from "eslint/config"; import tseslint from "typescript-eslint"; +import yml from "eslint-plugin-yml"; +import jsonc from "eslint-plugin-jsonc"; const gitignore = readFileSync(new URL(".gitignore", import.meta.url), "utf8") .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line && !line.startsWith("#") && !line.startsWith("!")); -export default tseslint.config( +// Files not covered by tsconfig (which only includes src/), type-checked via +// the default project so strictTypeChecked rules still apply to them. +const nonProjectFiles = ["eslint.config.mjs", "scripts/*.mjs", "scripts/*.mts"]; + +// The typescript-eslint `config()` helper is deprecated; ESLint core now +// provides `defineConfig()`. We replicate the helper's `extends` expansion +// explicitly by applying the TS `files` glob to each config object. +const tsFiles = ["**/*.{ts,mts,cts,js,mjs,cjs}"]; + +export default defineConfig([ { ignores: gitignore, }, - ...tseslint.configs.recommended, + // --- TypeScript / JavaScript: strictest type-aware rules ----------------- + ...tseslint.configs.strict.map((conf) => ({ ...conf, files: tsFiles })), + ...tseslint.configs.strictTypeChecked.map((conf) => ({ ...conf, files: tsFiles })), + ...tseslint.configs.stylistic.map((conf) => ({ ...conf, files: tsFiles })), { + files: tsFiles, + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: nonProjectFiles, + }, + tsconfigRootDir: import.meta.dirname, + }, + }, rules: { + // Parameters required by an interface/callback signature but unused in + // an implementation are conventionally prefixed with `_` to signal + // intentional non-use (TypeScript convention, used by typescript-eslint + // recommended config). We cannot remove them without breaking the + // interface conformance, so honor the `_` prefix instead of disabling + // the rule. "@typescript-eslint/no-unused-vars": [ "error", { argsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_", - varsIgnorePattern: "^_", }, ], }, }, + // --- YAML: strict rules from eslint-plugin-yml --------------------------- + ...yml.configs["flat/standard"], + // --- JSON / JSONC: strict rules from eslint-plugin-jsonc ----------------- + ...jsonc.configs["flat/recommended-with-jsonc"], + // --- Repo-wide hard rules ------------------------------------------------ { - files: ["**/*.d.ts"], rules: { - "@typescript-eslint/no-explicit-any": "off", + // Zero tolerance: no unfinished-work marker comments may ever be committed. + "no-warning-comments": [ + "error", + { + terms: ["todo", "fixme", "xxx", "hack", "@ts-ignore", "@ts-expect-error"], + location: "anywhere", + }, + ], }, }, -); +]); diff --git a/package-lock.json b/package-lock.json index aab7569..f3f0cec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "opencode-copilot-chat", - "version": "0.5.0", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opencode-copilot-chat", - "version": "0.5.0", + "version": "0.5.1", "license": "MIT", "dependencies": { "@silvia-odwyer/photon-node": "^0.3.4" @@ -15,11 +15,15 @@ "@types/node": "^26.1.0", "@types/vscode": "^1.125.0", "@vscode/vsce": "^3.9.2", + "editorconfig-checker": "^6.1.1", "eslint": "^10.8.0", + "eslint-plugin-jsonc": "^3.4.1", + "eslint-plugin-yml": "^3.8.1", "husky": "^9.1.7", "lint-staged": "^17.3.0", "markdownlint-cli2": "^0.23.2", "prettier": "^3.9.6", + "shellcheck": "^4.1.0", "typescript": "^6.0.3", "typescript-eslint": "^8.66.0" }, @@ -237,6 +241,17 @@ "node": ">=6.9.0" } }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", @@ -344,6 +359,22 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@felipecrs/decompress-tarxz": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@felipecrs/decompress-tarxz/-/decompress-tarxz-5.0.4.tgz", + "integrity": "sha512-a+nAnDsiUA84Sy/a+FKYJtjOjFvNtW8Jcbi3NwE8kJKPpYAxINFLYsC9mev9/wngiNEBA3jfHn0qNFwICeZNJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xhmikosr/decompress-tar": "^8.1.0", + "file-type": "^20.5.0", + "is-stream": "^2.0.1", + "xz-decompress": "^0.2.3" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -448,6 +479,32 @@ "node": ">= 8" } }, + "node_modules/@ota-meshi/ast-token-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@ota-meshi/ast-token-store/-/ast-token-store-0.3.0.tgz", + "integrity": "sha512-XRO0zi2NIUKq2lUk3T1ecFSld1fMWRKE6naRFGkgkdeosx7IslyUKNv5Dcb5PJTja9tHJoFu0v/7yEpAkrkrTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, "node_modules/@secretlint/config-creator": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", @@ -725,6 +782,32 @@ "@textlint/ast-node-types": "15.6.1" } }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -1235,6 +1318,49 @@ "win32" ] }, + "node_modules/@xhmikosr/decompress-tar": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-tar/-/decompress-tar-8.1.0.tgz", + "integrity": "sha512-m0q8x6lwxenh1CrsTby0Jrjq4vzW/QU1OLhTHMQLEdHpmjR1lgahGz++seZI0bXF3XcZw3U3xHfqZSz+JPP2Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^20.5.0", + "is-stream": "^2.0.1", + "tar-stream": "^3.1.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-tar/node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/@xhmikosr/decompress-unzip": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-unzip/-/decompress-unzip-7.1.0.tgz", + "integrity": "sha512-oqTYAcObqTlg8owulxFTqiaJkfv2SHsxxxz9Wg4krJAHVzGWlZsU8tAB30R6ow+aHrfv4Kub6WQ8u04NWVPUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^20.5.0", + "get-stream": "^6.0.1", + "yauzl": "^3.1.2" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", @@ -1354,6 +1480,22 @@ "dev": true, "license": "MIT" }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/azure-devops-node-api": { "version": "12.5.0", "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", @@ -1365,6 +1507,21 @@ "typed-rest-client": "^1.8.4" } }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1375,6 +1532,91 @@ "node": "18 || 20 || >=22" } }, + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.0.tgz", + "integrity": "sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.1.tgz", + "integrity": "sha512-cD5ciQuKlx+eumTCfqbfiL+fhQm+dHbVNB/cX4+d+I/nx4JsVop9VoFEPAs5RJ6I84QR6bZIBjpd2kjDOYeWcg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1394,8 +1636,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/binaryextensions": { "version": "6.11.0", @@ -1433,6 +1674,14 @@ "dev": true, "license": "ISC" }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT" + }, "node_modules/boundary": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", @@ -1486,12 +1735,29 @@ } ], "license": "MIT", - "optional": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true, + "license": "MIT" + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -1509,6 +1775,13 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -1525,6 +1798,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1711,6 +2003,13 @@ "node": ">=18" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1788,6 +2087,26 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/decompress": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -1805,88 +2124,352 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "node_modules/decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", "dev": true, "license": "MIT", - "optional": true, + "dependencies": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, "engines": { - "node": ">=4.0.0" + "node": ">=4" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/default-browser": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "node_modules/decompress-tar/node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", "dev": true, "license": "MIT", "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" } }, - "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "node_modules/decompress-tar/node_modules/file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "node_modules/decompress-tar/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/delayed-stream": { + "node_modules/decompress-tar/node_modules/isarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/decompress-tar/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.4.0" + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "node_modules/decompress-tar/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/decompress-tar/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "safe-buffer": "~5.1.0" } }, - "node_modules/detect-libc": { + "node_modules/decompress-tar/node_modules/tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-targz/node_modules/file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-targz/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-unzip/node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip/node_modules/get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip/node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", @@ -1897,6 +2480,13 @@ "node": ">=8" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT" + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -1911,6 +2501,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -2012,6 +2612,24 @@ "url": "https://bevry.me/fund" } }, + "node_modules/editorconfig-checker": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/editorconfig-checker/-/editorconfig-checker-6.1.1.tgz", + "integrity": "sha512-kiOb6qaWpMNt7Z/43ba0Pa1Inhr2/t9nKbvEKtCeXJ5AesztoM9AgLOOQVB4QUv/nGjgz3xkbx4pcogVRD2NWw==", + "dev": true, + "license": "MIT", + "bin": { + "ec": "dist/index.js", + "editorconfig-checker": "dist/index.js" + }, + "engines": { + "node": ">=20.11.0" + }, + "funding": { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/mstruebing" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2039,7 +2657,6 @@ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "once": "^1.4.0" } @@ -2057,6 +2674,19 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/envalid": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/envalid/-/envalid-8.1.0.tgz", + "integrity": "sha512-OT6+qVhKVyCidaGoXflb2iK1tC8pd0OV2Q+v9n33wNhUJ+lus+rJobUj4vJaQBPxPZ0vYrPGuxdrenyCAIJcow==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -2119,6 +2749,13 @@ "node": ">= 0.4" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2191,6 +2828,93 @@ } } }, + "node_modules/eslint-json-compat-utils": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/eslint-json-compat-utils/-/eslint-json-compat-utils-0.2.3.tgz", + "integrity": "sha512-RbBmDFyu7FqnjE8F0ZxPNzx5UaptdeS9Uu50r7A+D7s/+FCX+ybiyViYEgFUaFIFqSWJgZRTpL5d8Kanxxl2lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esquery": "^1.6.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": "*", + "jsonc-eslint-parser": "^2.4.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@eslint/json": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jsonc": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsonc/-/eslint-plugin-jsonc-3.4.1.tgz", + "integrity": "sha512-HHWkjAmVJ3QAffCkfo0XKl3CstLtgbIu5EGfFfVDLQT6vqPrxl4pFbUwFwY03nOlvyuDiiBixhHFrlbzn9u09Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.5.1", + "@eslint/core": "^1.0.1", + "@eslint/plugin-kit": "^0.7.0", + "@ota-meshi/ast-token-store": "^0.3.0", + "diff-sequences": "^29.6.3", + "eslint-json-compat-utils": "^0.2.3", + "jsonc-eslint-parser": "^3.1.0", + "natural-compare": "^1.4.0", + "synckit": "^0.11.12" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": ">=9.38.0" + } + }, + "node_modules/eslint-plugin-yml": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-yml/-/eslint-plugin-yml-3.8.1.tgz", + "integrity": "sha512-E/70psRwxz5EJ8dBtzrFfqSiiInuSytbdtFCjueb/GcKjsWzPBfxfhtQePImMAPjHwMA/VDurO7DJwStDJ4wWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint/core": "^1.0.1", + "@eslint/plugin-kit": "^0.7.0", + "@ota-meshi/ast-token-store": "^0.3.0", + "diff-sequences": "^29.0.0", + "escape-string-regexp": "5.0.0", + "natural-compare": "^1.4.0", + "yaml-eslint-parser": "^2.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": ">=9.38.0" + } + }, + "node_modules/eslint-plugin-yml/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-scope": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", @@ -2334,6 +3058,16 @@ "node": ">=0.10.0" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -2352,6 +3086,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -2410,6 +3151,23 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true, + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2423,6 +3181,25 @@ "node": ">=16.0.0" } }, + "node_modules/file-type": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.5.0.tgz", + "integrity": "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2474,6 +3251,22 @@ "dev": true, "license": "ISC" }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", @@ -2496,8 +3289,7 @@ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/fs-extra": { "version": "11.3.5", @@ -2576,6 +3368,19 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -2615,6 +3420,41 @@ "node": ">= 6" } }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/globby": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", @@ -2666,6 +3506,19 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2830,8 +3683,7 @@ "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/ignore": { "version": "7.0.5", @@ -2871,8 +3723,7 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", @@ -2908,6 +3759,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-decimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", @@ -2998,6 +3862,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3021,6 +3892,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-wsl": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", @@ -3037,6 +3937,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3113,6 +4020,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3126,6 +4040,24 @@ "node": ">=6" } }, + "node_modules/jsonc-eslint-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/jsonc-eslint-parser/-/jsonc-eslint-parser-3.3.0.tgz", + "integrity": "sha512-hYTGkHGNRZnXOFZ1urhINADoqDrGfpy53cjw+dxk84QE0pUDujQzeUeamNs6Mz44/TKD49z2x6/GVSu4ZrtA+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.5.0", + "eslint-visitor-keys": "^5.0.0", + "verkit": "^0.3.2" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } + }, "node_modules/jsonc-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", @@ -3425,6 +4357,29 @@ "node": ">=10" } }, + "node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/markdown-it": { "version": "14.3.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", @@ -3604,6 +4559,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4389,6 +5357,16 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -4402,13 +5380,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", - "optional": true, "dependencies": { "wrappy": "1" } @@ -4693,6 +5680,39 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -4703,6 +5723,16 @@ "node": ">=4" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -4758,6 +5788,13 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -4941,6 +5978,24 @@ "node": ">=0.10.0" } }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -5038,6 +6093,27 @@ "node": ">=20.0.0" } }, + "node_modules/seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^2.8.1" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/seek-bzip/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/semver": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", @@ -5051,6 +6127,60 @@ "node": ">=10" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT" + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5074,6 +6204,26 @@ "node": ">=8" } }, + "node_modules/shellcheck": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/shellcheck/-/shellcheck-4.1.0.tgz", + "integrity": "sha512-8143z6YGO4+Puwp9Ghn/g7+QxllSKlXaZSm3HXfvQXUfRXhM5P8TPORRHBBlyobl9BnniVne+d1Ff6RgNiccsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@felipecrs/decompress-tarxz": "5.0.4", + "@xhmikosr/decompress-unzip": "7.1.0", + "decompress": "4.2.1", + "envalid": "8.1.0", + "global-agent": "3.0.0" + }, + "bin": { + "shellcheck": "bin/shellcheck.js" + }, + "engines": { + "node": ">=20.9.0" + } + }, "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", @@ -5279,6 +6429,25 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -5354,6 +6523,16 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-natural-number": "^4.0.1" + } + }, "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -5365,6 +6544,23 @@ "node": ">=0.10.0" } }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/structured-source": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", @@ -5405,6 +6601,22 @@ "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" } }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/table": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", @@ -5477,6 +6689,16 @@ "node": ">=6" } }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/terminal-link": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", @@ -5494,6 +6716,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5517,6 +6749,13 @@ "url": "https://bevry.me/fund" } }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", @@ -5585,6 +6824,21 @@ "node": ">=14.14" } }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5598,6 +6852,25 @@ "node": ">=8.0" } }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -5668,6 +6941,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/typed-rest-client": { "version": "1.8.11", "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", @@ -5725,6 +7013,30 @@ "dev": true, "license": "MIT" }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, "node_modules/underscore": { "version": "1.13.8", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", @@ -5794,8 +7106,7 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/validate-npm-package-license": { "version": "3.0.4", @@ -5808,6 +7119,19 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/verkit": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.3.2.tgz", + "integrity": "sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/version-range": { "version": "4.15.0", "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", @@ -5861,6 +7185,28 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -5876,8 +7222,7 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/wsl-utils": { "version": "0.1.0", @@ -5919,6 +7264,26 @@ "node": ">=4.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/xz-decompress": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/xz-decompress/-/xz-decompress-0.2.3.tgz", + "integrity": "sha512-O8v6HG8T0PrKBcpyWA13GkSYWFvncwzuzcLx5A7++l3HsE3atmoetXjIxrZ/JV/nbvSZ7WS4+3XvREZuVn+rEA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -5932,7 +7297,6 @@ "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", - "optional": true, "bin": { "yaml": "bin.mjs" }, @@ -5943,6 +7307,23 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yaml-eslint-parser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/yaml-eslint-parser/-/yaml-eslint-parser-2.1.0.tgz", + "integrity": "sha512-1zo9KRfp6vIAXBEhWAz09ex3hUwh3T+/6dGbGteHvNseA0Uk4FgQnPES55klZ6cDzSy9FpgKS0D/CoLUvCCj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^5.0.0", + "yaml": "^2.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } + }, "node_modules/yauzl": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.0.tgz", diff --git a/package.json b/package.json index 0d90d0b..9374acc 100644 --- a/package.json +++ b/package.json @@ -330,12 +330,16 @@ ] }, "scripts": { - "lint": "npm run lint:js && npm run lint:md", - "lint:js": "eslint .", - "lint:fix": "eslint . --fix", - "lint:md": "markdownlint-cli2 --config .markdownlint.json \"**/*.md\" \"#node_modules\" \"#docs/**\"", - "lint:md:all": "markdownlint-cli2 --config .markdownlint.json \"**/*.md\" \"#node_modules\"", - "format": "npx prettier --write .", + "lint": "npm run lint:js && npm run lint:md && npm run lint:sh && npm run lint:hygiene && npm run lint:ts && npm run format:check", + "lint:js": "eslint . --max-warnings 0", + "lint:fix": "eslint . --fix --max-warnings 0", + "lint:md": "markdownlint-cli2 --config .markdownlint.json \"**/*.md\" \"#node_modules\"", + "lint:sh": "shellcheck .husky/pre-commit", + "lint:hygiene": "editorconfig-checker", + "lint:ts": "tsc -p tsconfig.check.json", + "format": "npm run format:prettier && npm run format:js", + "format:js": "eslint . --fix --max-warnings 0", + "format:prettier": "npx prettier --write .", "format:check": "npx prettier --check .", "clean": "node -e \"require('node:fs').rmSync('out', { recursive: true, force: true })\"", "compile": "npm run clean && tsc -p ./", @@ -349,20 +353,24 @@ "prepare": "husky" }, "lint-staged": { - "*.{js,mjs,cjs,ts,mts,cts}": [ - "eslint --fix --max-warnings 0" - ], - "*.{json,jsonc,yml,yaml,css,scss,less,html,svg}": "prettier --write" + "*": "prettier --write --ignore-unknown", + "*.{js,mjs,cjs,ts,mts,cts}": "eslint --fix --max-warnings 0", + "*.md": "markdownlint-cli2 --config .markdownlint.json --fix", + ".husky/*": "shellcheck" }, "devDependencies": { "@types/node": "^26.1.0", "@types/vscode": "^1.125.0", "@vscode/vsce": "^3.9.2", + "editorconfig-checker": "^6.1.1", "eslint": "^10.8.0", + "eslint-plugin-jsonc": "^3.4.1", + "eslint-plugin-yml": "^3.8.1", "husky": "^9.1.7", "lint-staged": "^17.3.0", "markdownlint-cli2": "^0.23.2", "prettier": "^3.9.6", + "shellcheck": "^4.1.0", "typescript": "^6.0.3", "typescript-eslint": "^8.66.0" }, diff --git a/scripts/run-unit-tests.mjs b/scripts/run-unit-tests.mjs index 17fdbc0..889c369 100644 --- a/scripts/run-unit-tests.mjs +++ b/scripts/run-unit-tests.mjs @@ -17,6 +17,11 @@ const result = spawnSync(process.execPath, ["--test", ...testFiles], { if (result.error) throw result.error; process.exit(result.status ?? 1); +/** + * Recursively collect compiled `*.test.js` files under a directory. + * @param {string} directory + * @returns {string[]} + */ function collectTests(directory) { return readdirSync(directory, { withFileTypes: true }) .sort((left, right) => left.name.localeCompare(right.name)) diff --git a/scripts/test-retry-e2e.mts b/scripts/test-retry-e2e.mts index 90f87b1..4d785e1 100644 --- a/scripts/test-retry-e2e.mts +++ b/scripts/test-retry-e2e.mts @@ -17,15 +17,31 @@ import { analyzeHttp400ForRetry } from "../src/retry.js"; // Mock OpenCode API server // --------------------------------------------------------------------------- +interface MockRequestBody { + model?: unknown; + thinking?: { type?: unknown }; + temperature?: unknown; + reasoning_effort?: unknown; +} + +/** JSON.parse returns `any`; shape it into a minimal, typed view of the body. */ +function parseMockBody(body: string): MockRequestBody { + const parsed: unknown = JSON.parse(body); + if (typeof parsed !== "object" || parsed === null) { + return {}; + } + return parsed; +} + function createMockServer() { return createServer((req: IncomingMessage, res: ServerResponse) => { let body = ""; - req.on("data", (chunk) => { - body += chunk; + req.on("data", (chunk: Buffer) => { + body += chunk.toString(); }); req.on("end", () => { try { - const parsed = JSON.parse(body); + const parsed = parseMockBody(body); const model = parsed.model as string; // Kimi K2.5: reject thinking.type "disabled" @@ -177,7 +193,7 @@ async function runTest(tc: TestCase, baseUrl: string): Promise { } if (res.status !== 400) { - console.log(`โŒ Expected 400 or 200, got ${res.status}`); + console.log(`โŒ Expected 400 or 200, got ${String(res.status)}`); return false; } console.log(`HTTP 400 โœ“`); @@ -204,9 +220,14 @@ async function runTest(tc: TestCase, baseUrl: string): Promise { // Step 4: Retry with patched body process.stdout.write(` Step 4: Retry with patched bodyโ€ฆ `); - const retryRes = await sendRequest(`${baseUrl}/chat/completions`, patch.body!); + const retryBody = patch.body; + if (!retryBody) { + console.log(`โŒ Patch produced no body`); + return false; + } + const retryRes = await sendRequest(`${baseUrl}/chat/completions`, retryBody); if (retryRes.status !== 200) { - console.log(`โŒ Got ${retryRes.status}: ${retryRes.body.slice(0, 100)}`); + console.log(`โŒ Got ${String(retryRes.status)}: ${retryRes.body.slice(0, 100)}`); return false; } console.log(`HTTP 200 โœ“`); @@ -230,7 +251,7 @@ async function main() { console.error("Failed to start mock server"); process.exit(1); } - const baseUrl = `http://127.0.0.1:${addr.port}`; + const baseUrl = `http://127.0.0.1:${String(addr.port)}`; console.log(`Mock server: ${baseUrl}\n`); let passed = 0; @@ -250,7 +271,7 @@ async function main() { server.close(); console.log("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•"); - console.log(` Results: ${passed} passed, ${failed} failed`); + console.log(` Results: ${String(passed)} passed, ${String(failed)} failed`); console.log("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n"); if (failed > 0) { @@ -260,7 +281,7 @@ async function main() { console.log("โœ… All tests passed. Retry mechanism works end-to-end."); } -main().catch((err) => { +main().catch((err: unknown) => { console.error("Fatal:", err); process.exit(1); }); diff --git a/scripts/validate-models.mts b/scripts/validate-models.mts index 64c1e80..7ef44f6 100644 --- a/scripts/validate-models.mts +++ b/scripts/validate-models.mts @@ -38,8 +38,7 @@ const { values: args } = parseArgs({ timeout: { type: "string", default: "30000" }, help: { type: "boolean", short: "h" }, }, - strict: false, -}); +} as const); if (args.help) { console.log(` @@ -59,14 +58,14 @@ const ZEN_BASE = process.env.OPENCODE_ZEN_URL ?? "https://opencode.ai/zen/v1"; const MODELS_DEV_URL = "https://models.dev/api.json"; const TIMEOUT_MS = Number(args.timeout) || 30000; -const INCLUDE_GO = args.go !== false; -const INCLUDE_ZEN_FREE = args["zen-free"] !== false; -const INCLUDE_ZEN_PAID = args["zen-paid"] === true; +const INCLUDE_GO = args.go; +const INCLUDE_ZEN_FREE = args["zen-free"]; +const INCLUDE_ZEN_PAID = args["zen-paid"]; const FAMILIES_FILTER = args.families?.split(",").map((f) => f.trim().toLowerCase()); const MODELS_FILTER = args.models?.split(",").map((m) => m.trim()); const SKIP_MODELS = new Set(args["skip-models"]?.split(",").map((m) => m.trim()) ?? []); -const DRY_RUN = args["dry-run"] === true; -const OUTPUT_JSON = args.json === true; +const DRY_RUN = args["dry-run"]; +const OUTPUT_JSON = args.json; // --------------------------------------------------------------------------- // Types @@ -77,7 +76,7 @@ interface ModelInfo { vendor: "go" | "zen"; family: string; reasoning: boolean; - reasoningOptions?: Array<{ type?: string; values?: string[] }>; + reasoningOptions?: { type?: string; values?: string[] }[]; temperature: boolean; } @@ -127,6 +126,7 @@ const DEFAULT_SETTINGS: ThinkingSettings = { glm: "off", kimi: "off", minimax: "off", + openai: "off", qwen: "off", qwenBudget: "auto", mimo: "off", @@ -137,7 +137,7 @@ function buildThinkingTests(model: ModelInfo): ParamTest[] { const tests: ParamTest[] = []; // Temperature tests - if (model.temperature !== false) { + if (model.temperature) { tests.push({ name: "temp=0.2", settings: {} }); } tests.push({ name: "no-temp", settings: {} }); @@ -200,7 +200,7 @@ function buildThinkingTests(model: ModelInfo): ParamTest[] { // API call using extension's routing + auth // --------------------------------------------------------------------------- -async function testParameter(model: ModelInfo, test: ParamTest): Promise { +async function testParameter(model: ModelInfo, test: ParamTest, apiKey: string): Promise { // Build the provider definition matching what the extension uses const provider = model.vendor === "go" @@ -223,7 +223,7 @@ async function testParameter(model: ModelInfo, test: ParamTest): Promise controller.abort(), TIMEOUT_MS); + const timer = setTimeout(() => { + controller.abort(); + }, TIMEOUT_MS); const response = await fetch(routing.endpointUrl, { method: "POST", @@ -297,23 +299,25 @@ async function testParameter(model: ModelInfo, test: ParamTest): Promise; - temperature?: boolean; - } - >; - }; -} +type ModelsDevResponse = Record< + string, + | { + models: Record< + string, + { + status?: string; + reasoning?: boolean; + reasoning_options?: { type?: string; values?: string[] }[]; + temperature?: boolean; + } + >; + } + | undefined +>; async function fetchModels(): Promise { const response = await fetch(MODELS_DEV_URL); - if (!response.ok) throw new Error(`models.dev: ${response.status}`); + if (!response.ok) throw new Error(`models.dev: ${String(response.status)}`); const data = (await response.json()) as ModelsDevResponse; const models: ModelInfo[] = []; @@ -369,13 +373,17 @@ function formatReport(results: TestResult[], models: ModelInfo[]): string { const lines: string[] = []; lines.push("# Model Parameter Validation Report"); lines.push(`Generated: ${new Date().toISOString()}`); - lines.push(`Models: ${models.length} | Tests: ${results.length}`); + lines.push(`Models: ${String(models.length)} | Tests: ${String(results.length)}`); lines.push(""); const byFamily = new Map(); for (const r of results) { - if (!byFamily.has(r.family)) byFamily.set(r.family, []); - byFamily.get(r.family)!.push(r); + const familyResults = byFamily.get(r.family); + if (familyResults) { + familyResults.push(r); + } else { + byFamily.set(r.family, [r]); + } } lines.push("## Summary by Family"); @@ -389,7 +397,7 @@ function formatReport(results: TestResult[], models: ModelInfo[]): string { const fail = familyResults.filter((r) => r.status === "โŒ").length; const failModels = [...new Set(familyResults.filter((r) => r.status === "โŒ").map((r) => r.model))]; lines.push( - `| ${family} | ${modelCount} | ${familyResults.length} | ${pass} | ${fail} | ${failModels.length > 0 ? failModels.join(", ") : "โ€”"} |`, + `| ${family} | ${String(modelCount)} | ${String(familyResults.length)} | ${String(pass)} | ${String(fail)} | ${failModels.length > 0 ? failModels.join(", ") : "โ€”"} |`, ); } lines.push(""); @@ -401,7 +409,7 @@ function formatReport(results: TestResult[], models: ModelInfo[]): string { lines.push("| Model | Vendor | Parameter | HTTP | Error |"); lines.push("|-------|--------|-----------|------|-------|"); for (const r of failures) { - lines.push(`| ${r.model} | ${r.vendor} | ${r.param} | ${r.httpStatus} | ${(r.error ?? "").slice(0, 80)} |`); + lines.push(`| ${r.model} | ${r.vendor} | ${r.param} | ${String(r.httpStatus)} | ${(r.error ?? "").slice(0, 80)} |`); } lines.push(""); } @@ -411,19 +419,25 @@ function formatReport(results: TestResult[], models: ModelInfo[]): string { const byModel = new Map(); for (const r of results) { - if (!byModel.has(r.model)) byModel.set(r.model, []); - byModel.get(r.model)!.push(r); + const modelResults = byModel.get(r.model); + if (modelResults) { + modelResults.push(r); + } else { + byModel.set(r.model, [r]); + } } for (const [modelId, modelResults] of [...byModel.entries()].sort()) { const model = models.find((m) => m.id === modelId); const pass = modelResults.filter((r) => r.status === "โœ…").length; const fail = modelResults.filter((r) => r.status === "โŒ").length; - lines.push(`### ${modelId} (${model?.vendor}/${model?.family}) โ€” ${pass}โœ… ${fail}โŒ`); + const vendorLabel = model?.vendor ?? "unknown"; + const familyLabel = model?.family ?? "unknown"; + lines.push(`### ${modelId} (${vendorLabel}/${familyLabel}) โ€” ${String(pass)}โœ… ${String(fail)}โŒ`); lines.push(""); for (const r of modelResults) { const err = r.error ? ` โ€” ${r.error.slice(0, 60)}` : ""; - lines.push(`- ${r.status} \`${r.param}\` (HTTP ${r.httpStatus})${err}`); + lines.push(`- ${r.status} \`${r.param}\` (HTTP ${String(r.httpStatus)})${err}`); } lines.push(""); } @@ -446,7 +460,7 @@ async function main() { console.error("๐Ÿ“ก Fetching models from models.devโ€ฆ"); const models = await fetchModels(); - console.error(` Found ${models.length} models.\n`); + console.error(` Found ${String(models.length)} models.\n`); if (models.length === 0) { console.error("No models to test."); @@ -456,11 +470,16 @@ async function main() { const results: TestResult[] = []; let testCount = 0; + if (!DRY_RUN && !API_KEY) { + console.error("โŒ API key required for live testing. Use --api-key or set OPENCODE_API_KEY."); + process.exit(1); + } + for (let i = 0; i < models.length; i++) { const model = models[i]; const tests = buildThinkingTests(model); - process.stderr.write(`[${i + 1}/${models.length}] ${model.vendor}/${model.id} (${tests.length} params)โ€ฆ `); + process.stderr.write(`[${String(i + 1)}/${String(models.length)}] ${model.vendor}/${model.id} (${String(tests.length)} params)โ€ฆ `); if (DRY_RUN) { const summaries = tests.map((t) => { @@ -473,10 +492,17 @@ async function main() { continue; } + // DRY_RUN returned above, so API_KEY is guaranteed here (required by the + // guards at the top of main()). TS cannot correlate the `!DRY_RUN && !API_KEY` + // exit with the DRY_RUN `continue` above, so re-assert the invariant. + if (!API_KEY) { + throw new Error("API key required for live testing"); + } + let pass = 0; let fail = 0; for (const test of tests) { - const result = await testParameter(model, test); + const result = await testParameter(model, test, API_KEY); results.push(result); testCount++; if (result.status === "โœ…") pass++; @@ -484,7 +510,7 @@ async function main() { await new Promise((r) => setTimeout(r, 300)); } - console.error(fail === 0 ? `โœ… ${pass}/${pass}` : `โŒ ${fail} failed`); + console.error(fail === 0 ? `โœ… ${String(pass)}/${String(pass)}` : `โŒ ${String(fail)} failed`); } if (DRY_RUN) process.exit(0); @@ -496,12 +522,12 @@ async function main() { } const failures = results.filter((r) => r.status === "โŒ"); - console.error(`\n๐Ÿ“Š Total: ${testCount} tests, ${testCount - failures.length} passed, ${failures.length} failed`); + console.error(`\n๐Ÿ“Š Total: ${String(testCount)} tests, ${String(testCount - failures.length)} passed, ${String(failures.length)} failed`); if (failures.length > 0) process.exit(1); } -main().catch((err) => { +main().catch((err: unknown) => { console.error("Fatal:", err); process.exit(1); }); diff --git a/scripts/verify-estimate-token-count.mts b/scripts/verify-estimate-token-count.mts index c913228..3e86164 100644 --- a/scripts/verify-estimate-token-count.mts +++ b/scripts/verify-estimate-token-count.mts @@ -116,7 +116,7 @@ function generateChatPayload(targetTokens: number, hasToolCalls: boolean): strin if (hasToolCalls && i % 3 === 0) { userMsg.tool_calls = [ { - id: `call_${i}_0`, + id: `call_${String(i)}_0`, type: "function", function: { name: "readFile", @@ -124,7 +124,7 @@ function generateChatPayload(targetTokens: number, hasToolCalls: boolean): strin }, }, { - id: `call_${i}_1`, + id: `call_${String(i)}_1`, type: "function", function: { name: "searchFiles", @@ -276,12 +276,12 @@ describe("ServiceManager", () => { if (hasToolCalls && (i - 1) % 4 === 0) { assistantMsg.tool_calls = [ { - id: `call_res_${i}`, + id: `call_res_${String(i)}`, type: "function", function: { name: "createFile", arguments: JSON.stringify({ - path: `src/services/example-${i}.ts`, + path: `src/services/example-${String(i)}.ts`, }), }, }, @@ -374,7 +374,7 @@ for (const tc of testCases) { console.log(` NEW: max_tokens = ${newMaxTokens.toLocaleString()}`); if (oldMaxTokens < 1000) { - console.log(`\n โŒ OLD: max_tokens collapsed to ${oldMaxTokens} โ€” BUG REPRODUCED`); + console.log(`\n โŒ OLD: max_tokens collapsed to ${String(oldMaxTokens)} โ€” BUG REPRODUCED`); } else { console.log(`\n โœ… OLD: OK (${oldMaxTokens.toLocaleString()} tokens)`); } @@ -382,7 +382,7 @@ for (const tc of testCases) { if (newMaxTokens >= 4096) { console.log(` โœ… NEW: OK (${newMaxTokens.toLocaleString()} tokens) โ€” FIX VERIFIED`); } else { - console.log(` โŒ NEW: max_tokens still only ${newMaxTokens} โ€” FIX FAILED`); + console.log(` โŒ NEW: max_tokens still only ${String(newMaxTokens)} โ€” FIX FAILED`); allPassed = false; } diff --git a/src/contextWindowHook.ts b/src/contextWindowHook.ts index c10e4f5..9439965 100644 --- a/src/contextWindowHook.ts +++ b/src/contextWindowHook.ts @@ -4,16 +4,16 @@ import type { UsageSnapshot } from "./usage"; type HandleProgressChunkFn = (requestId: string, chunks: unknown[]) => Promise; -type CapturedProxy = { +interface CapturedProxy { proxyTarget: Record; originalHandleProgressChunk: HandleProgressChunkFn; -}; +} -type ContextWindowUsage = { +interface ContextWindowUsage { promptTokens: number; completionTokens: number; outputBuffer?: number; -}; +} type SetAddFn = typeof Set.prototype.add; type SetDeleteFn = typeof Set.prototype.delete; @@ -143,14 +143,24 @@ function cleanupVsCodeRequest(requestId: string): void { } async function captureProxy(logDiagnostic?: (message: string) => void): Promise { - const originalMapSet = Map.prototype.set; - const probeId = `_opencode_probe_${Date.now()}`; - let found = false; - let capturedProxyTarget: Record | null = null; - let capturedHandleProgressChunk: HandleProgressChunkFn | null = null; + // Wrapper keeps `this` bound to the real Map instance at call time (extracting + // the raw method would trip unbound-method, while `.bind()` would freeze the + // receiver to Map.prototype and break the call). + const originalMapSet = function (this: Map, key: unknown, value: unknown): Map { + return Map.prototype.set.call(this, key, value); + }; + const probeId = `_opencode_probe_${String(Date.now())}`; + // Capture state lives in an object so the type checker does not narrow the + // fields to their initial literals โ€” the monkey-patched `Map.prototype.set` + // below mutates them at runtime. + const captureState: { + found: boolean; + proxyTarget: Record | null; + handleProgressChunk: HandleProgressChunkFn | null; + } = { found: false, proxyTarget: null, handleProgressChunk: null }; Map.prototype.set = function (this: Map, key: unknown, value: unknown) { - if (!found && isRecord(value)) { + if (!captureState.found && isRecord(value)) { const candidate = isRecord(value._proxy) ? value._proxy : undefined; const handleProgressChunk = candidate?.$handleProgressChunk; if ( @@ -158,9 +168,9 @@ async function captureProxy(logDiagnostic?: (message: string) => void): Promise< typeof handleProgressChunk === "function" && (value.id === probeId || value.label === probeId || value.name === probeId) ) { - capturedProxyTarget = candidate; - capturedHandleProgressChunk = handleProgressChunk as HandleProgressChunkFn; - found = true; + captureState.proxyTarget = candidate; + captureState.handleProgressChunk = handleProgressChunk as HandleProgressChunkFn; + captureState.found = true; } } @@ -179,13 +189,13 @@ async function captureProxy(logDiagnostic?: (message: string) => void): Promise< Map.prototype.set = originalMapSet; } - if (!found || !capturedProxyTarget || !capturedHandleProgressChunk) { + if (!captureState.found || !captureState.proxyTarget || !captureState.handleProgressChunk) { return null; } return { - proxyTarget: capturedProxyTarget, - originalHandleProgressChunk: capturedHandleProgressChunk, + proxyTarget: captureState.proxyTarget, + originalHandleProgressChunk: captureState.handleProgressChunk, }; } @@ -207,8 +217,7 @@ function patchProxy(captured: CapturedProxy): void { const stored = pendingUsage.get(requestId); if (stored) { - for (let index = 0; index < chunks.length; index += 1) { - const raw = chunks[index]; + for (const raw of chunks) { const chunk = (Array.isArray(raw) ? raw[0] : raw) as Record | undefined; if (chunk?.kind === "usage") { chunk.promptTokens = stored.promptTokens; @@ -251,15 +260,21 @@ function installRequestTracking(): void { return; } - const capturedOriginalAdd = Set.prototype.add; - const capturedOriginalDelete = Set.prototype.delete; + // Wrappers preserve the dynamic `this` receiver while avoiding raw prototype + // method extraction (unbound-method). + const capturedOriginalAdd = function (this: Set, value: T): Set { + return Set.prototype.add.call(this, value) as Set; + }; + const capturedOriginalDelete = function (this: Set, value: T): boolean { + return Set.prototype.delete.call(this, value); + }; const nextPatchedAdd: SetAddFn = function (this: Set, value: T): Set { if (isRecord(value) && typeof value.requestId === "string" && "extRequest" in value) { inFlightRequestIds.set(value.requestId, true); } - return capturedOriginalAdd.call(this, value); + return capturedOriginalAdd.call(this, value) as Set; }; const nextPatchedDelete: SetDeleteFn = function (this: Set, value: T): boolean { diff --git a/src/contextWindowHookBridge.ts b/src/contextWindowHookBridge.ts index 63d1b41..25882b1 100644 --- a/src/contextWindowHookBridge.ts +++ b/src/contextWindowHookBridge.ts @@ -14,8 +14,12 @@ let reportProgressImpl = ( ): void => { progress.report(part); }; -let clearRequestImpl = (_localRequestId: string): void => {}; -let setOutputBufferImpl = (_localRequestId: string, _outputBuffer: number): void => {}; +let clearRequestImpl = (_localRequestId: string): void => { + // No-op: request tracking is cleared by the hook module when it is active. +}; +let setOutputBufferImpl = (_localRequestId: string, _outputBuffer: number): void => { + // No-op: output-buffer tracking is handled by the hook module when it is active. +}; function installNoopImplementations(): void { reportUsageImpl = (_localRequestId: string, _usage: UsageSnapshot): boolean => false; @@ -26,8 +30,12 @@ function installNoopImplementations(): void { ): void => { progress.report(part); }; - clearRequestImpl = (_localRequestId: string): void => {}; - setOutputBufferImpl = (_localRequestId: string, _outputBuffer: number): void => {}; + clearRequestImpl = (_localRequestId: string): void => { + // No-op: request tracking is cleared by the hook module when it is active. + }; + setOutputBufferImpl = (_localRequestId: string, _outputBuffer: number): void => { + // No-op: output-buffer tracking is handled by the hook module when it is active. + }; } function installHookImplementations(hookModule: ContextWindowHookModule): void { @@ -48,7 +56,7 @@ async function loadContextWindowHookModule(logDiagnostic?: (message: string) => loadedContextWindowHookModule = hookModule; return hookModule; }) - .catch((error) => { + .catch((error: unknown) => { const message = error instanceof Error ? error.message : String(error); logDiagnostic?.(`contextWindowHook: failed to import hook module โ€” ${message}`); loadingContextWindowHookModule = undefined; @@ -101,7 +109,7 @@ export async function initializeContextWindowHookBridge(logDiagnostic?: (message return success; } -export async function disposeContextWindowHookBridge(): Promise { +export function disposeContextWindowHookBridge(): boolean { installNoopImplementations(); if (!loadedContextWindowHookModule) { diff --git a/src/errors.ts b/src/errors.ts index 0e94efb..1ca4234 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -35,7 +35,7 @@ export function buildOpenCodeRequestError( const apiError = parseApiError(rawDetail); const rateLimitInfo = readRateLimitInfo(response.headers); const modelHint = modelId ? ` model=${modelId}` : ""; - const sizeHint = ` payloadBytes=${payloadBytes}`; + const sizeHint = ` payloadBytes=${String(payloadBytes)}`; const apiMessage = (apiError.message ?? rawDetail.trim()) || response.statusText; const isLimit = isRateLimitResponse(response.status, apiError); @@ -48,15 +48,15 @@ export function buildOpenCodeRequestError( waitText ? `Retry after ${waitText}.` : undefined, quotaText ? `Quota: ${quotaText}.` : undefined, ].filter((part): part is string => Boolean(part)); - const userMessage = `${providerDisplayName}: ${reason}${modelHint ? ` (${modelId})` : ""}. ${details.join(" ")}`.trim(); + const userMessage = `${providerDisplayName}: ${reason}${modelId ? ` (${modelId})` : ""}. ${details.join(" ")}`.trim(); return new OpenCodeRequestError( - `${providerDisplayName} API rate/quota limit (${response.status})${modelHint}${sizeHint}: ${apiMessage}; ${quotaText || "no quota headers"}`, + `${providerDisplayName} API rate/quota limit (${String(response.status)})${modelHint}${sizeHint}: ${apiMessage}; ${quotaText || "no quota headers"}`, userMessage, ); } - const userMessage = `${providerDisplayName} API request failed (HTTP ${response.status})${modelHint ? ` for ${modelId}` : ""}: ${describeRouterUnavailable(apiError, apiMessage)}${capacityHint}`; - const requestMessage = `${providerDisplayName} API request failed (${response.status})${modelHint}${sizeHint}${capacityHint}: ${apiMessage}`; + const userMessage = `${providerDisplayName} API request failed (HTTP ${String(response.status)})${modelId ? ` for ${modelId}` : ""}: ${describeRouterUnavailable(apiError, apiMessage)}${capacityHint}`; + const requestMessage = `${providerDisplayName} API request failed (${String(response.status)})${modelHint}${sizeHint}${capacityHint}: ${apiMessage}`; return new OpenCodeRequestError(requestMessage, userMessage); } @@ -75,7 +75,7 @@ function describeRouterUnavailable(apiError: ParsedApiError, fallback: string): export function truncateForLog(value: string, max = 1200): string { const collapsed = value.replace(/\s+/g, " ").trim(); - return collapsed.length > max ? `${collapsed.slice(0, max)}โ€ฆ (+${collapsed.length - max} chars)` : collapsed; + return collapsed.length > max ? `${collapsed.slice(0, max)}โ€ฆ (+${String(collapsed.length - max)} chars)` : collapsed; } function parseApiError(rawDetail: string): ParsedApiError { @@ -281,12 +281,12 @@ export function formatDuration(ms: number): string { const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; if (hours) { - return `${hours}h ${minutes}m`; + return `${String(hours)}h ${String(minutes)}m`; } if (minutes) { - return `${minutes}m ${seconds}s`; + return `${String(minutes)}m ${String(seconds)}s`; } - return `${seconds}s`; + return `${String(seconds)}s`; } function isRecord(value: unknown): value is Record { diff --git a/src/extension.ts b/src/extension.ts index a731e26..005f142 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -88,7 +88,7 @@ let goUsageStatusBarItem: vscode.StatusBarItem | undefined; /** Singleton tracker โ€” the first/legacy account. Used for backward compat until first migration. */ let goUsageTracker: GoUsageTracker | undefined; /** Per-profile trackers indexed by key fingerprint. */ -const goUsageTrackers: Map = new Map(); +const goUsageTrackers = new Map(); let usageWebviewPanel: vscode.WebviewPanel | undefined; let profilesCache: UsageProfile[] = []; @@ -101,8 +101,10 @@ function getOrCreateTracker(fingerprint: string): GoUsageTracker { let tracker = goUsageTrackers.get(fingerprint); if (tracker) return tracker; tracker = new GoUsageTracker( - _extensionContext!, - (msg) => _usageLogChannel!.appendLine(`[${new Date().toISOString()}] [${fingerprint}] ${msg}`), + extensionContext(), + (msg) => { + usageLogChannel().appendLine(`[${new Date().toISOString()}] [${fingerprint}] ${msg}`); + }, (modelId) => modelMetadataSnapshot?.providers[GO_VENDOR]?.[modelId]?.cost, fingerprint, ); @@ -119,7 +121,7 @@ function activeGoUsageTracker(): GoUsageTracker | undefined { /** Switch the active profile and refresh the UI. */ async function setActiveProfile(fingerprint: string): Promise { activeProfileFingerprint = fingerprint; - await writeActiveProfile(_extensionContext!, fingerprint); + await writeActiveProfile(extensionContext(), fingerprint); refreshGoUsageStatusBar(); updateWebviewContent(); } @@ -139,24 +141,24 @@ function ensureProfileSync(apiKey: string): void { const nextNumber = nonLegacyCount(profilesCache) + 1; profilesCache.push({ fingerprint: fp, - label: `Profile ${nextNumber}`, + label: `Profile ${String(nextNumber)}`, lastSeenAt: Date.now(), }); - writeProfiles(_extensionContext!, profilesCache); + void writeProfiles(extensionContext(), profilesCache); } // One-time migration from singleton - if (!readMigratedTo(_extensionContext!)) { + if (!readMigratedTo(extensionContext())) { if (goUsageTracker && fp !== LEGACY_FINGERPRINT) { tracker.migrateFromSingleton(); } - writeMigratedTo(_extensionContext!, fp); - profilesCache = readProfiles(_extensionContext!); + void writeMigratedTo(extensionContext(), fp); + profilesCache = readProfiles(extensionContext()); } // Update active profile to this one activeProfileFingerprint = fp; - writeActiveProfile(_extensionContext!, fp); + void writeActiveProfile(extensionContext(), fp); } /** @@ -168,8 +170,30 @@ function ensureProfileForApiKey(apiKey: string): GoUsageTracker { return getOrCreateTracker(keyFingerprint(apiKey)); } -let _extensionContext: vscode.ExtensionContext; -let _usageLogChannel: vscode.OutputChannel; +let _extensionContext: vscode.ExtensionContext | undefined; +let _usageLogChannel: vscode.OutputChannel | undefined; + +/** + * Returns the extension context, or throws if the extension has not been + * activated yet. Callers must be reached after `activate()` has run. + */ +function extensionContext(): vscode.ExtensionContext { + if (!_extensionContext) { + throw new Error("extension context not initialized"); + } + return _extensionContext; +} + +/** + * Returns the usage log output channel, or throws if the extension has not + * been activated yet. Callers must be reached after `activate()` has run. + */ +function usageLogChannel(): vscode.OutputChannel { + if (!_usageLogChannel) { + throw new Error("usage log channel not initialized"); + } + return _usageLogChannel; +} interface ProviderDefinition { vendor: AllProviderVendor; @@ -228,8 +252,9 @@ let cachedUserAgent: string | undefined; */ function getUserAgent(): string { if (cachedUserAgent) return cachedUserAgent; - const version = vscode.extensions.getExtension("ltmoerdani.opencode-copilot-chat")?.packageJSON?.version; - cachedUserAgent = typeof version === "string" && version ? `opencode-copilot-chat/${version} VSCode` : FALLBACK_USER_AGENT; + const packageJSON = vscode.extensions.getExtension("ltmoerdani.opencode-copilot-chat")?.packageJSON as { version?: unknown } | undefined; + const version = typeof packageJSON?.version === "string" ? packageJSON.version : undefined; + cachedUserAgent = version ? `opencode-copilot-chat/${version} VSCode` : FALLBACK_USER_AGENT; return cachedUserAgent; } @@ -299,13 +324,16 @@ function sleep(ms: number, token?: vscode.CancellationToken): Promise { resolve(); } }; - const timer = setTimeout(() => finish(false), ms); + const timer = setTimeout(() => { + finish(false); + }, ms); if (token) { - state.subscription = token.onCancellationRequested(() => finish(true)); - if (settled) { - state.subscription.dispose(); - } else if (token.isCancellationRequested) { + if (token.isCancellationRequested) { finish(true); + } else { + state.subscription = token.onCancellationRequested(() => { + finish(true); + }); } } }); @@ -415,7 +443,7 @@ const PROVIDERS: Record = (() "big-pickle", ], filterModel: (modelId) => - vscode.workspace.getConfiguration("opencodego").get("freeOnly", true) + vscode.workspace.getConfiguration("opencodego").get("freeOnly", true) ? modelId.endsWith("-free") || FREE_ZEN_MODEL_IDS.has(modelId) : true, }; @@ -712,7 +740,9 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand("opencodezen.refreshModels", () => zenProvider.refreshModels()), vscode.commands.registerCommand("opencodego.modelPickerDiagnostics", () => showModelPickerDiagnostics()), vscode.commands.registerCommand("opencodego.setThinkingEffort", () => showThinkingEffortPicker()), - vscode.commands.registerCommand("opencodego.showUsageDetails", () => showUsageWebview(context)), + vscode.commands.registerCommand("opencodego.showUsageDetails", () => { + showUsageWebview(context); + }), vscode.commands.registerCommand("opencodego.setUsageTargets", async () => { const tracker = activeGoUsageTracker(); if (!tracker) return; @@ -735,7 +765,7 @@ export function activate(context: vscode.ExtensionContext) { const sessionItem: vscode.QuickPickItem = { label: `$(comment) Latest Session (est)`, description: `$${sessionCost.cost.toFixed(4)}`, - detail: `${tokens(totalTokens)} tokens ยท ${sessionCost.requests} requests`, + detail: `${tokens(totalTokens)} tokens ยท ${String(sessionCost.requests)} requests`, alwaysShow: true, }; const dailyIdx = items.findIndex((i) => i.kind === vscode.QuickPickItemKind.Separator && i.label === "Daily Summary"); @@ -790,7 +820,7 @@ export function activate(context: vscode.ExtensionContext) { } else if (action === "showUsageDetails") { vscode.commands.executeCommand("opencodego.showUsageDetails"); } else if (action === "switchProfile" && "_fp" in picked) { - setActiveProfile((picked as { _fp: string })._fp); + void setActiveProfile((picked as { _fp: string })._fp); } }), vscode.commands.registerCommand("opencodego.renameActiveProfile", async () => { @@ -806,14 +836,14 @@ export function activate(context: vscode.ExtensionContext) { placeHolder: "e.g. OpenCode Go (Works)", }); if (!newLabel || !newLabel.trim()) return; - await renameProfile(_extensionContext, activeProfileFingerprint, newLabel); - profilesCache = readProfiles(_extensionContext); + await renameProfile(extensionContext(), activeProfileFingerprint, newLabel); + profilesCache = readProfiles(extensionContext()); refreshGoUsageStatusBar(); updateWebviewContent(); vscode.window.showInformationMessage(`Profile renamed to "${newLabel}".`); }), vscode.commands.registerCommand("opencodego.deleteProfile", async () => { - const profiles = readActiveProfiles(_extensionContext); + const profiles = readActiveProfiles(extensionContext()); if (profiles.length === 0) { vscode.window.showInformationMessage("No profiles to delete."); return; @@ -838,7 +868,7 @@ export function activate(context: vscode.ExtensionContext) { if (confirm !== "Delete") return; goUsageTrackers.delete(fp); - const ctx = _extensionContext; + const ctx = extensionContext(); ctx.globalState.update(`opencodego.usageLog.v1.${fp}`, []); ctx.globalState.update(`opencodego.usageBaseline.v1.${fp}`, {}); ctx.globalState.update(`opencodego.sessionCosts.v1.${fp}`, []); @@ -919,7 +949,7 @@ async function showModelPickerDiagnostics(): Promise { for (const vendor of vendors) { const models = await vscode.lm.selectChatModels({ vendor }); - sections.push(`## vendor: ${vendor}`, "", `models: ${models.length}`, ""); + sections.push(`## vendor: ${vendor}`, "", `models: ${String(models.length)}`, ""); for (const model of models) { const internalModel = model as unknown as { configurationSchema?: unknown; detail?: unknown }; const schema = internalModel.configurationSchema; @@ -1178,13 +1208,14 @@ async function showUsageTargetEditor(tracker: GoUsageTracker): Promise { const n = parseCurrencyInput(value); if (isNaN(n) || n < 0) return "Enter a valid number using digits and . or , as decimal separator (e.g. 3.50)."; - if (n > GO_LIMITS.session) return `Session limit is $${GO_LIMITS.session}. Enter a value between 0 and ${GO_LIMITS.session}.`; + if (n > GO_LIMITS.session) + return `Session limit is $${String(GO_LIMITS.session)}. Enter a value between 0 and ${String(GO_LIMITS.session)}.`; return undefined; }, }); @@ -1193,13 +1224,14 @@ async function showUsageTargetEditor(tracker: GoUsageTracker): Promise { const n = parseCurrencyInput(value); if (isNaN(n) || n < 0) return "Enter a valid number using digits and . or , as decimal separator (e.g. 12.00)."; - if (n > GO_LIMITS.weekly) return `Weekly limit is $${GO_LIMITS.weekly}. Enter a value between 0 and ${GO_LIMITS.weekly}.`; + if (n > GO_LIMITS.weekly) + return `Weekly limit is $${String(GO_LIMITS.weekly)}. Enter a value between 0 and ${String(GO_LIMITS.weekly)}.`; return undefined; }, }); @@ -1208,13 +1240,14 @@ async function showUsageTargetEditor(tracker: GoUsageTracker): Promise { const n = parseCurrencyInput(value); if (isNaN(n) || n < 0) return "Enter a valid number using digits and . or , as decimal separator (e.g. 25.00)."; - if (n > GO_LIMITS.monthly) return `Monthly limit is $${GO_LIMITS.monthly}. Enter a value between 0 and ${GO_LIMITS.monthly}.`; + if (n > GO_LIMITS.monthly) + return `Monthly limit is $${String(GO_LIMITS.monthly)}. Enter a value between 0 and ${String(GO_LIMITS.monthly)}.`; return undefined; }, }); @@ -1296,14 +1329,14 @@ function buildUsageTooltipSvg( const noDataMsg = s.hasData ? null : nonLegacyCount(profilesCache) > 0 ? "No data yet for this profile." : "No usage data yet."; const text = (value: string, x: number, y: number, size: number, weight = 400, color = fg, anchor: "start" | "end" = "start"): string => - `${escapeSvg(value)}`; + `${escapeSvg(value)}`; const bar = (pct: number, x: number, y: number, barWidth: number): string => { const clamped = Math.min(Math.max(pct, 0), 100); const fillWidth = Math.max(0, Math.round((clamped / 100) * barWidth)); return [ - ``, - fillWidth > 0 ? `` : "", + ``, + fillWidth > 0 ? `` : "", ].join(""); }; @@ -1317,14 +1350,14 @@ function buildUsageTooltipSvg( ].join(""); if (!s.hasData) { - return ` + return ` ${text(svgTitle, 14, 26, 16, 700)} ${text(noDataMsg ?? "No usage data yet. Send a chat message to start tracking.", 14, 50, 12, 400, muted)} `; } - return ` + return ` ${text(svgTitle, 14, 26, 16, 700)} ${period("Session (5h rolling)", s.session, 54)} @@ -1335,11 +1368,11 @@ ${ hasSession ? [ text("Session (est):", 14, 250, 13, 400, muted), - text(`$${sc!.cost.toFixed(4)}`, cx, 250, 13, 700), + text(`$${sc.cost.toFixed(4)}`, cx, 250, 13, 700), text("Requests:", 200, 250, 13, 400, muted), - text(String(sc!.requests), 280, 250, 13, 700), + text(String(sc.requests), 280, 250, 13, 700), text("Tokens:", 320, 250, 13, 400, muted), - text(tokens(sc!.promptTokens + sc!.completionTokens), 400, 250, 13, 700), + text(tokens(sc.promptTokens + sc.completionTokens), 400, 250, 13, 700), ].join("") : "" } @@ -1378,13 +1411,13 @@ function tokens(v: number): string { } function rel(date: Date): string { const min = Math.max(0, Math.floor((date.getTime() - Date.now()) / 60_000)); - if (min < 60) return `${min}m`; + if (min < 60) return `${String(min)}m`; const h = Math.floor(min / 60), m = min % 60; - if (h < 24) return m ? `${h}h ${m}m` : `${h}h`; + if (h < 24) return m ? `${String(h)}h ${String(m)}m` : `${String(h)}h`; const d = Math.floor(h / 24), rh = h % 24; - return rh ? `${d}d ${rh}h` : `${d}d`; + return rh ? `${String(d)}d ${String(rh)}h` : `${String(d)}d`; } class OpenCodeProvider implements vscode.LanguageModelChatProvider { @@ -1407,7 +1440,7 @@ class OpenCodeProvider implements vscode.LanguageModelChatProvider { + private hasByokGroupConfigured(): boolean { return this.context.globalState.get(this.byokGroupStateKey, false); } @@ -1522,12 +1555,10 @@ class OpenCodeProvider implements vscode.LanguageModelChatProvider 262K corrections). - version: `1.2.0-${MODEL_METADATA_REVISION}-${limits.contextWindow}-${limits.maxOutputTokens}`, + version: `1.2.0-${MODEL_METADATA_REVISION}-${String(limits.contextWindow)}-${String(limits.maxOutputTokens)}`, detail: capacityNote ? `${baseDetail} โ€ข Limited capacity` : modalityBadges ? `${baseDetail} โ€ข ${modalityBadges}` : baseDetail, tooltip: capacityNote ? `${baseTooltip}\n\n${capacityNote}` : modalityBadges ? `${baseTooltip}\n\n${modalityBadges}` : baseTooltip, isUserSelectable: true, @@ -1985,7 +2016,7 @@ class OpenCodeProvider implements vscode.LanguageModelChatProvider 0) { this.log( - `Models registered: count=${registeredCount} provider=${this.definition.vendor}` + + `Models registered: count=${String(registeredCount)} provider=${this.definition.vendor}` + ` first=${firstModelId} last=${lastModelId}` + (this.definition.isAgentVariant ? " (agents)" : ""), ); @@ -2023,7 +2054,7 @@ class OpenCodeProvider implements vscode.LanguageModelChatProvider result.messages)); const normalizedImageCount = convertedMessages.map((result) => result.normalizedImageCount).reduce((total, count) => total + count, 0); if (normalizedImageCount > 0) { - this.log(`[vision] Normalized ${normalizedImageCount} image attachment(s) to provider-safe dimensions/encoding.`); + this.log(`[vision] Normalized ${String(normalizedImageCount)} image attachment(s) to provider-safe dimensions/encoding.`); } const baseSettings = getSettings(); // Apply per-request Thinking selection (from Copilot Chat submenu) on top @@ -2054,8 +2085,7 @@ class OpenCodeProvider implements vscode.LanguageModelChatProvider p.type === "image_url")) { const textParts = msg.content @@ -2078,8 +2108,7 @@ class OpenCodeProvider implements vscode.LanguageModelChatProvider p.type === "image_url")) { const textParts = msg.content @@ -2110,7 +2139,7 @@ class OpenCodeProvider implements vscode.LanguageModelChatProvider 0) { this.log( - `[history-trim] Replaced ${trimmedCount} old image(s) with placeholder text to bound payload (kept most recent ${MAX_HISTORY_IMAGES_KEPT}).`, + `[history-trim] Replaced ${String(trimmedCount)} old image(s) with placeholder text to bound payload (kept most recent ${String(MAX_HISTORY_IMAGES_KEPT)}).`, ); } @@ -2139,19 +2168,17 @@ class OpenCodeProvider implements vscode.LanguageModelChatProvider { - return typeof text === "string" ? estimateTokenCount(text) : estimateChatMessageTokenCount(text); + return Promise.resolve(typeof text === "string" ? estimateTokenCount(text) : estimateChatMessageTokenCount(text)); } /** @@ -2323,11 +2350,11 @@ class OpenCodeProvider implements vscode.LanguageModelChatProvider controller.abort()); - if (token.isCancellationRequested) controller.abort(); + subscription = token.onCancellationRequested(() => { + controller.abort(); + }); } return { signal: controller.signal, @@ -2512,7 +2540,7 @@ async function refreshOpenCodeModelMetadata( }); if (!response.ok) { - throw new Error(`models.dev request failed (${response.status}): ${response.statusText}`); + throw new Error(`models.dev request failed (${String(response.status)}): ${response.statusText}`); } const data = (await response.json()) as ModelsDevResponse; @@ -2520,11 +2548,11 @@ async function refreshOpenCodeModelMetadata( modelMetadataSnapshot = snapshot; await context.globalState.update(MODEL_METADATA_CACHE_KEY, snapshot); output?.appendLine( - `[metadata] refreshed models.dev cache go=${Object.keys(snapshot.providers[GO_VENDOR]).length} zen=${Object.keys(snapshot.providers[ZEN_VENDOR]).length}`, + `[metadata] refreshed models.dev cache go=${String(Object.keys(snapshot.providers[GO_VENDOR] ?? {}).length)} zen=${String(Object.keys(snapshot.providers[ZEN_VENDOR] ?? {}).length)}`, ); return snapshot; })() - .catch((error) => { + .catch((error: unknown) => { const cached = modelMetadataSnapshot ?? context.globalState.get(MODEL_METADATA_CACHE_KEY); if (cached) { const message = error instanceof Error ? error.message : String(error); @@ -2628,7 +2656,8 @@ function buildAnthropicMessages(messages: ApiMessage[]): AnthropicRequestMessage continue; } - if (message.role === "tool" && message.tool_call_id) { + // After the user/assistant continues above, role is narrowed to "tool". + if (message.tool_call_id) { anthropicMessages.push({ role: "user", content: [ @@ -2767,7 +2796,7 @@ function anthropicImageSource(part: OpenAiContentPart): AnthropicImageSource | u return { type: "url", url }; } -function mapResponsesTools(tools: readonly vscode.LanguageModelChatTool[] | undefined): Array> { +function mapResponsesTools(tools: readonly vscode.LanguageModelChatTool[] | undefined): Record[] { return (tools ?? []).map((tool) => ({ type: "function", name: tool.name, @@ -2818,7 +2847,7 @@ function buildGoogleGenerateContentBody( }; } -function mapGoogleTools(tools: readonly vscode.LanguageModelChatTool[] | undefined): Array> { +function mapGoogleTools(tools: readonly vscode.LanguageModelChatTool[] | undefined): Record[] { return (tools ?? []).map((tool) => ({ name: tool.name, description: tool.description, @@ -2834,9 +2863,9 @@ function googleToolConfig(mode: vscode.LanguageModelChatToolMode): Record> { +function googleContentsFromMessages(messages: ApiMessage[]): Record[] { const toolNamesById = new Map(); - const contents: Array> = []; + const contents: Record[] = []; for (const message of messages) { if (message.role === "user") { @@ -2848,7 +2877,7 @@ function googleContentsFromMessages(messages: ApiMessage[]): Array> = []; + const parts: Record[] = []; if (typeof message.reasoning_content === "string" && message.reasoning_content.trim()) { parts.push({ text: message.reasoning_content, thought: true }); } @@ -2867,7 +2896,8 @@ function googleContentsFromMessages(messages: ApiMessage[]): Array> { +function googleUserParts(content: ApiMessage["content"]): Record[] { if (typeof content === "string") { return content ? [{ text: content }] : []; } @@ -2893,7 +2923,7 @@ function googleUserParts(content: ApiMessage["content"]): Array> => { + return content.flatMap((part): Record[] => { if (part.type === "text" && typeof part.text === "string") { return [{ text: part.text }]; } @@ -2927,13 +2957,15 @@ function dataUrlToInlineData(url: string): { mimeType: string; data: string } | function googleFunctionResponseContent( content: ApiMessage["content"], name: string, -): { name: string; content: string; parts?: Array> } { +): { name: string; content: string; parts?: Record[] } { if (typeof content === "string") { return { name, content }; } if (!Array.isArray(content)) { - return { name, content: JSON.stringify(content ?? "") }; + // ApiMessage content is `string | null | OpenAiContentPart[]`; after the + // string and array checks above, this branch only sees null. + return { name, content: JSON.stringify("") }; } const text = joinedTextContent(content, "\n"); @@ -2942,7 +2974,7 @@ function googleFunctionResponseContent( return { name, content: text }; } - const parts: Array> = []; + const parts: Record[] = []; if (text) { parts.push({ text }); } @@ -2964,7 +2996,7 @@ function parseToolInput(value: string): object { } try { - const parsed = JSON.parse(value); + const parsed: unknown = JSON.parse(value); return isRecord(parsed) ? parsed : {}; } catch { return {}; @@ -3000,7 +3032,7 @@ function buildOpenCodeRequestHeaders( ); const requestId = cleanHeaderValue( findStringOption(options, ["requestId", "requestID", "messageId", "messageID"]) ?? - `req-${stableHash(`${Date.now()}-${Math.random()}-${sessionId}-${modelId}`)}`, + `req-${stableHash(`${String(Date.now())}-${String(Math.random())}-${sessionId}-${modelId}`)}`, ); return { @@ -3011,6 +3043,31 @@ function buildOpenCodeRequestHeaders( }; } +/** + * Stringify an arbitrary transport-layer initiator value for diagnostics. + * Objects and functions are JSON-serialized, nullish values are dropped, and + * primitives are converted directly so logs never show "[object Object]". + */ +function stringifyInitiator(initiator: unknown): string | undefined { + if (initiator === undefined || initiator === null) { + return undefined; + } + if (typeof initiator === "string") { + return initiator; + } + if (typeof initiator === "object" || typeof initiator === "function") { + return JSON.stringify(initiator); + } + if (typeof initiator === "symbol" || typeof initiator === "bigint") { + return initiator.toString(); + } + if (typeof initiator === "number" || typeof initiator === "boolean") { + return String(initiator); + } + // No known primitive type left; nothing useful to stringify. + return undefined; +} + function findStringOption(options: unknown, paths: string[]): string | undefined { for (const path of paths) { const value = readPath(options, path.split(".")); @@ -3033,7 +3090,7 @@ function readPath(value: unknown, path: string[]): unknown { } function conversationAnchor(messages: readonly vscode.LanguageModelChatRequestMessage[], modelId: string): string { - const anchorMessages = messages.slice(0, 3).map((message) => `${message.role}:${messageText(message).slice(0, 2048)}`); + const anchorMessages = messages.slice(0, 3).map((message) => `${String(message.role)}:${messageText(message).slice(0, 2048)}`); return anchorMessages.length ? anchorMessages.join("\n") : modelId; } @@ -3194,7 +3251,7 @@ async function convertMessage( type: "function", function: { name: part.name, - arguments: JSON.stringify(part.input ?? {}), + arguments: JSON.stringify(part.input), }, }); continue; @@ -3225,7 +3282,7 @@ async function convertMessage( ) { if (resultPart.data.byteLength > MAX_TOOL_RESULT_IMAGE_BYTES) { toolTextParts.push( - `[Image attachment omitted: ${resultPart.data.byteLength} bytes exceeds the ${MAX_TOOL_RESULT_IMAGE_BYTES}-byte limit for tool results. Ask the tool to produce a smaller screenshot or save it to a file.]`, + `[Image attachment omitted: ${String(resultPart.data.byteLength)} bytes exceeds the ${String(MAX_TOOL_RESULT_IMAGE_BYTES)}-byte limit for tool results. Ask the tool to produce a smaller screenshot or save it to a file.]`, ); continue; } @@ -3263,7 +3320,7 @@ async function convertMessage( const flattened: string[] = [...toolTextParts]; for (let i = 0; i < toolImageParts.length; i++) { flattened.push( - `[Tool returned an image attachment, but the MiMo upstream provider does not accept images in tool messages. Image ${i + 1} of ${toolImageParts.length} was dropped to keep the request valid.]`, + `[Tool returned an image attachment, but the MiMo upstream provider does not accept images in tool messages. Image ${String(i + 1)} of ${String(toolImageParts.length)} was dropped to keep the request valid.]`, ); } toolContent = flattened.join("\n"); @@ -3297,7 +3354,7 @@ async function convertMessage( if (base64Bytes === undefined || base64Bytes > MAX_IMAGE_BASE64_BYTES) { textParts.push( `[Image attachment omitted: normalized payload exceeds the ` + - `${Math.floor(MAX_IMAGE_BASE64_BYTES / (1024 * 1024))} MB base64 limit. ` + + `${String(Math.floor(MAX_IMAGE_BASE64_BYTES / (1024 * 1024)))} MB base64 limit. ` + `Resize or compress the image and re-attach it.]`, ); continue; @@ -3406,7 +3463,7 @@ function estimateChatMessageTokenCount(message: vscode.LanguageModelChatRequestM ); } -function partToTokenCount(part: vscode.LanguageModelInputPart | unknown): number { +function partToTokenCount(part: unknown): number { if (part instanceof vscode.LanguageModelTextPart) { return estimateTokenCount(part.value); } @@ -3457,7 +3514,7 @@ function estimateDataPartTokenCount(part: vscode.LanguageModelDataPart): number return Math.max(1, Math.ceil(part.data.byteLength / 4)); } -function partToText(part: vscode.LanguageModelInputPart | unknown): string { +function partToText(part: unknown): string { if (part instanceof vscode.LanguageModelTextPart) { return part.value; } @@ -3505,7 +3562,7 @@ function normalizeMessages(messages: ApiMessage[]): ApiMessage[] { !prevHasToolCalls && !msgHasToolCalls ) { - previous.content = `${prevContent ?? ""}\n\n${msgContent ?? ""}`.trim(); + previous.content = `${prevContent}\n\n${msgContent}`.trim(); } else { normalized.push({ ...message }); } @@ -3818,7 +3875,7 @@ async function proxyVision( const nonAgent = (models: readonly vscode.LanguageModelChat[]) => models.filter((m) => !m.id.includes("-agent:")); let visionModels = nonAgent(await vscode.lm.selectChatModels({ id: visionModelId })); - if (!visionModels || visionModels.length === 0) { + if (visionModels.length === 0) { // Try matching by name substring across all providers const allVisible = nonAgent(await vscode.lm.selectChatModels({})); visionModels = allVisible.filter( @@ -3828,7 +3885,7 @@ async function proxyVision( m.family.toLowerCase().includes(visionModelId.toLowerCase()), ); } - if (!visionModels || visionModels.length === 0) { + if (visionModels.length === 0) { throw new Error(`Vision model "${visionModelId}" not found. ` + `Run "OpenCode Go: Configure Vision Proxy" to see available models.`); } @@ -3841,14 +3898,14 @@ async function proxyVision( // Build a request preserving images and text from the original messages const requestMessages: vscode.LanguageModelChatMessage[] = []; for (const msg of messages) { - const parts: Array = []; + const parts: (vscode.LanguageModelTextPart | vscode.LanguageModelDataPart)[] = []; for (const part of msg.content) { if (part instanceof vscode.LanguageModelDataPart && part.mimeType.startsWith("image/")) { parts.push(part); } else if (part instanceof vscode.LanguageModelTextPart) { parts.push(part); } else if (typeof part === "object" && part !== null && "value" in part) { - const valuePart = part as { value: unknown }; + const valuePart = part; parts.push(new vscode.LanguageModelTextPart(String(valuePart.value))); } } @@ -3892,7 +3949,7 @@ const DEFAULT_VISION_PROXY_PROMPT = * stored in globalState via the "OpenCode Go: Configure Vision Proxy" command). */ function isVisionProxyEnabled(): boolean { - return (_extensionContext?.globalState.get(VISION_PROXY_MODEL_ID_KEY, "") ?? "").length > 0; + return extensionContext().globalState.get(VISION_PROXY_MODEL_ID_KEY, "").length > 0; } /** @@ -3957,7 +4014,7 @@ async function showVisionProxyPicker(context: vscode.ExtensionContext): Promise< return a.label.localeCompare(b.label); }); - const items: Array<{ + const items: { label: string; description?: string; detail?: string; @@ -3966,7 +4023,7 @@ async function showVisionProxyPicker(context: vscode.ExtensionContext): Promise< _kind: "none" | "prompt" | "model" | "separator"; _supportsVision?: boolean; kind?: vscode.QuickPickItemKind; - }> = [ + }[] = [ { label: "$(circle-slash) None (disable)", detail: currentModelId ? "" : "currently selected", picked: !currentModelId, _kind: "none" }, { label: "", kind: vscode.QuickPickItemKind.Separator, _kind: "separator" }, { diff --git a/src/goUsageTracker.ts b/src/goUsageTracker.ts index 4d4c666..b776b28 100644 --- a/src/goUsageTracker.ts +++ b/src/goUsageTracker.ts @@ -31,7 +31,7 @@ const WEEK_MS = 7 * 24 * 60 * 60 * 1000; // This table is a static snapshot kept as a last resort. The primary source // is the live models.dev metadata cache injected via CostResolver. -const GO_MODEL_PRICING: Record = { +const GO_MODEL_PRICING: Record = { "glm-5.1": { input: 1.4, output: 4.4, cache_read: 0.26 }, "glm-5": { input: 1.0, output: 3.2, cache_read: 0.2 }, "kimi-k2.6": { input: 0.95, output: 4.0, cache_read: 0.16 }, @@ -182,9 +182,10 @@ function buildMonthlyWindow( earliestMs?: number | null, ): { monthStartMs: number; monthEndMs: number } { // Priority 1: user-configured anchor (set via "Set spent targets") - const monthlyAnchor = baseline.monthly?.anchorDay; - if (monthlyAnchor && monthlyAnchor >= 1 && monthlyAnchor <= 31) { - const hour = baseline.monthly!.anchorHour ?? 0; + const monthly = baseline.monthly; + const monthlyAnchor = monthly?.anchorDay; + if (monthly && monthlyAnchor && monthlyAnchor >= 1 && monthlyAnchor <= 31) { + const hour = monthly.anchorHour ?? 0; const start = anchoredMonthStart(nowMs, monthlyAnchor, hour); const end = anchoredMonthEnd(start, monthlyAnchor, hour); return { monthStartMs: start, monthEndMs: end }; @@ -272,7 +273,7 @@ function readOpenCodeHistory(): HistoryRow[] | null { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], }); - const rows = JSON.parse(result); + const rows: unknown = JSON.parse(result); if (!Array.isArray(rows)) return null; return rows.filter((row): row is HistoryRow => { if (!row || typeof row !== "object") return false; @@ -307,7 +308,7 @@ export class GoUsageTracker { * so multiple Go accounts can coexist. Empty string = legacy mode * (single account, shared key). */ - private readonly storageKeySuffix: string = "", + private readonly storageKeySuffix = "", ) { this.log = log; this.costResolver = costResolver; @@ -341,7 +342,7 @@ export class GoUsageTracker { // Migrate baseline const legacyBaseline = this.context.globalState.get(BASELINE_STORAGE_KEY, {}); - if (legacyBaseline && Object.keys(legacyBaseline).length > 0) { + if (Object.keys(legacyBaseline).length > 0) { const targetBase = this.storageKey(BASELINE_STORAGE_KEY); this.context.globalState.update(targetBase, legacyBaseline); this.context.globalState.update(BASELINE_STORAGE_KEY, {}); @@ -355,7 +356,7 @@ export class GoUsageTracker { this.context.globalState.update(targetSess, legacySessions); this.context.globalState.update(SESSION_COSTS_KEY, []); for (const s of legacySessions) { - if (s && typeof s.sessionId === "string" && typeof s.cost === "number") { + if (typeof s.sessionId === "string" && typeof s.cost === "number") { this.sessionCosts.set(s.sessionId, s); } } @@ -378,7 +379,7 @@ export class GoUsageTracker { const cached = summary.cachedTokens ?? 0; if (prompt + completion === 0) { - this.log?.(`[go-tracker] SKIP: zero tokens (prompt=${prompt} completion=${completion}) for model=${summary.modelId}`); + this.log?.(`[go-tracker] SKIP: zero tokens (prompt=${String(prompt)} completion=${String(completion)}) for model=${summary.modelId}`); return; } @@ -388,7 +389,7 @@ export class GoUsageTracker { const copilotCredits = cost * 100; this.log?.( - `[go-tracker] RECORD: model=${summary.modelId} prompt=${prompt} completion=${completion} cached=${cached} cost=$${cost.toFixed(6)} credits=${copilotCredits.toFixed(4)}`, + `[go-tracker] RECORD: model=${summary.modelId} prompt=${String(prompt)} completion=${String(completion)} cached=${String(cached)} cost=$${cost.toFixed(6)} credits=${copilotCredits.toFixed(4)}`, ); this.entries.push({ @@ -828,7 +829,7 @@ export class GoUsageTracker { const entry = this.baseline[period]; if (!entry) return 0; if (entry.expiresAt <= nowMs) { - delete this.baseline[period]; + this.baseline[period] = undefined; this.persistBaseline(); return 0; } @@ -842,7 +843,7 @@ export class GoUsageTracker { } const baseline = this.context.globalState.get(this.storageKey(BASELINE_STORAGE_KEY), {}); - if (baseline && typeof baseline === "object") { + if (typeof baseline === "object") { this.baseline = baseline; } @@ -850,7 +851,7 @@ export class GoUsageTracker { const storedSessions = this.context.globalState.get(this.storageKey(SESSION_COSTS_KEY), []); if (Array.isArray(storedSessions)) { for (const s of storedSessions) { - if (s && typeof s.sessionId === "string" && typeof s.cost === "number") { + if (typeof s.sessionId === "string" && typeof s.cost === "number") { this.sessionCosts.set(s.sessionId, s); } } @@ -867,7 +868,7 @@ function fmtUsd(v: number): string { function fmtTokens(n: number): string { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; - if (n >= 1_000) return `${Math.round(n / 1_000)}k`; + if (n >= 1_000) return `${String(Math.round(n / 1_000))}k`; return String(n); } @@ -882,9 +883,9 @@ function fmtRelativeTime(target: Date, from: Date = new Date()): string { const totalMinutes = Math.ceil(diffMs / 60_000); const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; - if (hours === 0) return `${minutes}m`; - if (minutes === 0) return `${hours}h`; - return `${hours}h ${minutes}m`; + if (hours === 0) return `${String(minutes)}m`; + if (minutes === 0) return `${String(hours)}h`; + return `${String(hours)}h ${String(minutes)}m`; } function fmtDate(d: Date): string { @@ -912,7 +913,7 @@ export function formatGoUsageStatusBarText(summary: UsageSummary): string { const w = summary.weekly.percent; const m = summary.monthly.percent; const warn = s >= 80 || w >= 80 || m >= 80 ? " $(warning)" : ""; - return `Go: ${s}%ยท${w}%ยท${m}%${warn}`; + return `Go: ${String(s)}%ยท${String(w)}%ยท${String(m)}%${warn}`; } /** Multiline tooltip (VS Code renders newlines in tooltips as-is) */ @@ -933,18 +934,18 @@ export function formatGoUsageTooltip(summary: UsageSummary): vscode.MarkdownStri const resets = fmtRelativeTime(period.resetsAt); md.appendMarkdown( `${icon} **${label}**\n\n` + - `\`${bar}\` ${period.percent}% ยท ${fmtUsd(period.spent)} / ${fmtUsd(period.limit)} ยท resets in ${resets}\n\n`, + `\`${bar}\` ${String(period.percent)}% ยท ${fmtUsd(period.spent)} / ${fmtUsd(period.limit)} ยท resets in ${resets}\n\n`, ); } md.appendMarkdown("---\n\n"); md.appendMarkdown( - `$(history) **Today:** ${fmtUsd(summary.today.cost)} ยท ${fmtTokens(summary.today.tokens)} tokens ยท ${summary.today.requests} req\n\n`, + `$(history) **Today:** ${fmtUsd(summary.today.cost)} ยท ${fmtTokens(summary.today.tokens)} tokens ยท ${String(summary.today.requests)} req\n\n`, ); if (summary.yesterday.requests > 0) { md.appendMarkdown( - `$(history) **Yesterday:** ${fmtUsd(summary.yesterday.cost)} ยท ${fmtTokens(summary.yesterday.tokens)} tokens ยท ${summary.yesterday.requests} req\n\n`, + `$(history) **Yesterday:** ${fmtUsd(summary.yesterday.cost)} ยท ${fmtTokens(summary.yesterday.tokens)} tokens ยท ${String(summary.yesterday.requests)} req\n\n`, ); } @@ -957,19 +958,19 @@ export function formatGoUsageLanguageStatusDetail(summary: UsageSummary): string const now = new Date(); const sessionLine = [ - `Session ${summary.session.percent}%`, + `Session ${String(summary.session.percent)}%`, `${fmtUsd(summary.session.spent)} / ${fmtUsd(summary.session.limit)}`, `resets in ${fmtRelativeTime(summary.session.resetsAt, now)}`, ].join(" ยท "); const weeklyLine = [ - `Weekly ${summary.weekly.percent}%`, + `Weekly ${String(summary.weekly.percent)}%`, `${fmtUsd(summary.weekly.spent)} / ${fmtUsd(summary.weekly.limit)}`, `resets in ${fmtRelativeTime(summary.weekly.resetsAt, now)}`, ].join(" ยท "); const monthlyLine = [ - `Monthly ${summary.monthly.percent}%`, + `Monthly ${String(summary.monthly.percent)}%`, `${fmtUsd(summary.monthly.spent)} / ${fmtUsd(summary.monthly.limit)}`, `resets in ${fmtRelativeTime(summary.monthly.resetsAt, now)}`, ].join(" ยท "); @@ -977,7 +978,7 @@ export function formatGoUsageLanguageStatusDetail(summary: UsageSummary): string const todayLine = [ `Today ${fmtUsd(summary.today.cost)}`, `${fmtTokens(summary.today.tokens)} tokens`, - `${summary.today.requests} req`, + `${String(summary.today.requests)} req`, ].join(" ยท "); return [sessionLine, weeklyLine, monthlyLine, todayLine].join("\n"); @@ -995,7 +996,7 @@ export function buildUsageQuickPickItems(summary: UsageSummary): vscode.QuickPic const resets = fmtRelativeTime(period.resetsAt, now); return { label: `${icon} ${label}`, - description: `${bar} ${period.percent}%`, + description: `${bar} ${String(period.percent)}%`, detail: `${spent} / ${limit} used ยท resets in ${resets} (${resetLabel})`, alwaysShow: true, }; @@ -1035,7 +1036,7 @@ export function buildUsageQuickPickItems(summary: UsageSummary): vscode.QuickPic items.push({ label: `$(history) Today`, description: fmtUsd(summary.today.cost), - detail: `${fmtTokens(summary.today.tokens)} tokens ยท ${summary.today.requests} requests`, + detail: `${fmtTokens(summary.today.tokens)} tokens ยท ${String(summary.today.requests)} requests`, alwaysShow: true, }); @@ -1043,7 +1044,7 @@ export function buildUsageQuickPickItems(summary: UsageSummary): vscode.QuickPic items.push({ label: `$(history) Yesterday`, description: fmtUsd(summary.yesterday.cost), - detail: `${fmtTokens(summary.yesterday.tokens)} tokens ยท ${summary.yesterday.requests} requests`, + detail: `${fmtTokens(summary.yesterday.tokens)} tokens ยท ${String(summary.yesterday.requests)} requests`, alwaysShow: true, }); } diff --git a/src/imageNormalizer.ts b/src/imageNormalizer.ts index 54f7ceb..2108331 100644 --- a/src/imageNormalizer.ts +++ b/src/imageNormalizer.ts @@ -28,11 +28,11 @@ export function getImageDataUrlBase64Bytes(url: string): number | undefined { return parsed ? Buffer.byteLength(parsed.base64, "utf8") : undefined; } -function candidateSizes(width: number, height: number): Array<{ width: number; height: number }> { +function candidateSizes(width: number, height: number): { width: number; height: number }[] { const scale = Math.min(1, MAX_IMAGE_WIDTH / width, MAX_IMAGE_HEIGHT / height); let nextWidth = Math.max(1, Math.round(width * scale)); let nextHeight = Math.max(1, Math.round(height * scale)); - const sizes: Array<{ width: number; height: number }> = []; + const sizes: { width: number; height: number }[] = []; while (sizes.length < 32) { if (sizes.some((size) => size.width === nextWidth && size.height === nextHeight)) { @@ -91,7 +91,7 @@ export async function normalizeImageDataUrl(url: string): Promise { for (const size of candidateSizes(width, height)) { const resized = photon.resize(decoded, size.width, size.height, photon.SamplingFilter.Lanczos3); try { - const candidates: Array<{ mime: string; bytes: Uint8Array }> = [ + const candidates: { mime: string; bytes: Uint8Array }[] = [ { mime: "image/png", bytes: resized.get_bytes() }, ...JPEG_QUALITIES.map((quality) => ({ mime: "image/jpeg", diff --git a/src/metadata.ts b/src/metadata.ts index 83b21af..f105528 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -45,7 +45,7 @@ export interface ModelMetadataFields { supportsPdf?: boolean; reasoning?: boolean; /** Raw reasoning_options from models.dev, e.g. [{ type: "toggle" }, { type: "effort", values: ["low","medium","high"] }]. */ - reasoningOptions?: Array<{ type?: string; values?: string[] }>; + reasoningOptions?: { type?: string; values?: string[] }[]; /** Whether the model supports the temperature parameter. False means temperature is deprecated/unsupported. */ temperature?: boolean; status?: string; @@ -54,7 +54,8 @@ export interface ModelMetadataFields { export interface CachedModelMetadataSnapshot { fetchedAt: number; - providers: Record>; + /** Vendors may be absent in cached data loaded from disk. */ + providers: Record | undefined>; } export interface ResolvedModelMetadata extends BaseModelLimits { @@ -64,7 +65,7 @@ export interface ResolvedModelMetadata extends BaseModelLimits { supportsPdf: boolean; reasoning: boolean; /** Parsed reasoning_options from models.dev, if available. */ - reasoningOptions?: Array<{ type?: string; values?: string[] }>; + reasoningOptions?: { type?: string; values?: string[] }[]; /** Whether the model supports the temperature parameter. Undefined means unknown (assume supported). */ temperature?: boolean; status?: string; @@ -102,7 +103,7 @@ export interface ModelsDevModelRecord { }; attachment?: boolean; reasoning?: boolean; - reasoning_options?: Array<{ type?: string; values?: string[] }>; + reasoning_options?: { type?: string; values?: string[] }[]; temperature?: boolean; modalities?: { input?: string[]; @@ -113,13 +114,13 @@ export interface ModelsDevModelRecord { output?: number; cache_read?: number; cache_write?: number; - tiers?: Array<{ + tiers?: { input: number; output: number; cache_read?: number; cache_write?: number; tier: { type: string; size: number }; - }>; + }[]; context_over_200k?: { input?: number; output?: number; @@ -153,7 +154,7 @@ const MODELS_DEV_PROVIDER_BY_VENDOR: Record> = { +const MODEL_LIMITS_BY_PROVIDER: Record> = { [GO_VENDOR]: { "deepseek-v4-flash": { contextWindow: 1000000, maxOutputTokens: 384000 }, "deepseek-v4-pro": { contextWindow: 1000000, maxOutputTokens: 384000 }, @@ -356,7 +357,7 @@ export function resolveModelMetadata( snapshot: CachedModelMetadataSnapshot, liveModelMetadataById: Map, ): ResolvedModelMetadata { - const cachedMetadata = snapshot.providers[vendor][modelId]; + const cachedMetadata = snapshot.providers[vendor]?.[modelId]; const liveMetadata = liveModelMetadataById.get(modelId); const fallbackMetadata = fallbackModelMetadata(modelId, vendor); @@ -401,7 +402,7 @@ function normalizeModelsDevProvider(models: Record const modalities = detectModalityFlags(model.modalities, model.attachment); const rawCost = model.cost; const cost: ModelCost | undefined = - typeof rawCost?.input === "number" && typeof rawCost?.output === "number" + typeof rawCost?.input === "number" && typeof rawCost.output === "number" ? { input: rawCost.input, output: rawCost.output, @@ -535,7 +536,7 @@ export function getContextSizeOptions(cost: ModelCost | undefined, fullContextWi // Collect all distinct context thresholds from explicit tiers const thresholds = (tiers ?? []) - .filter((t) => t.tier?.type === "context" && typeof t.tier.size === "number" && t.tier.size > 0) + .filter((t) => t.tier.type === "context" && typeof t.tier.size === "number" && t.tier.size > 0) .map((t) => t.tier.size) .sort((a, b) => a - b); @@ -626,11 +627,11 @@ export function getContextSizeOptionsForModel( function formatContextSize(size: number): string { if (size >= 1_000_000) { const m = size / 1_000_000; - return m === Math.floor(m) ? `${m}M` : `${m.toFixed(1)}M`; + return m === Math.floor(m) ? `${String(m)}M` : `${m.toFixed(1)}M`; } if (size >= 1_000) { const k = size / 1_000; - return k === Math.floor(k) ? `${k}K` : `${k.toFixed(1)}K`; + return k === Math.floor(k) ? `${String(k)}K` : `${k.toFixed(1)}K`; } return String(size); } diff --git a/src/providerTypes.ts b/src/providerTypes.ts index 1a9b497..f305b26 100644 --- a/src/providerTypes.ts +++ b/src/providerTypes.ts @@ -11,7 +11,7 @@ export type AllProviderVendor = typeof GO_VENDOR | typeof ZEN_VENDOR | typeof AG /** Resolve agent-host vendor variants back to their base vendor for metadata/routing lookups. */ export function resolveBaseVendor(vendor: AllProviderVendor): ProviderVendor { - return vendor === AGENT_GO_VENDOR ? GO_VENDOR : vendor === AGENT_ZEN_VENDOR ? ZEN_VENDOR : (vendor as ProviderVendor); + return vendor === AGENT_GO_VENDOR ? GO_VENDOR : vendor === AGENT_ZEN_VENDOR ? ZEN_VENDOR : vendor; } export interface ProviderRoutingDefinition { diff --git a/src/responsesRequest.ts b/src/responsesRequest.ts index a314268..ccea7b9 100644 --- a/src/responsesRequest.ts +++ b/src/responsesRequest.ts @@ -59,14 +59,14 @@ export function buildResponsesRequestEnvelope(options: ResponsesRequestEnvelopeO * Convert one internal `ApiMessage` into the Responses API `input` items. * Returns an empty array for unsupported roles / empty user content. */ -export function responsesInputItemsFromMessage(message: ResponsesApiMessage): Array> { +export function responsesInputItemsFromMessage(message: ResponsesApiMessage): Record[] { if (message.role === "user") { const content = responsesUserContent(message.content); return content.length ? [{ role: "user", content }] : []; } if (message.role === "assistant") { - const items: Array> = []; + const items: Record[] = []; const text = responsesAssistantText(message.content); if (text) { items.push({ role: "assistant", content: [{ type: "output_text", text }] }); @@ -95,7 +95,7 @@ export function responsesInputItemsFromMessage(message: ResponsesApiMessage): Ar return [ { type: "function_call_output", - call_id: message.tool_call_id ?? `tool-${Date.now()}`, + call_id: message.tool_call_id ?? `tool-${String(Date.now())}`, output, }, ]; @@ -104,16 +104,21 @@ export function responsesInputItemsFromMessage(message: ResponsesApiMessage): Ar return []; } -function responsesUserContent(content: ResponsesApiMessage["content"]): Array> { +/** Narrow a union value to a content-part array without falling back to `any[]`. */ +function isContentPartArray(value: unknown): value is readonly ResponsesApiContentPart[] { + return Array.isArray(value); +} + +function responsesUserContent(content: ResponsesApiMessage["content"]): Record[] { if (typeof content === "string") { return content ? [{ type: "input_text", text: content }] : []; } - if (!Array.isArray(content)) { + if (!isContentPartArray(content)) { return []; } - return content.flatMap((part): Array> => { + return content.flatMap((part): Record[] => { if (part.type === "text" && typeof part.text === "string") { return [{ type: "input_text", text: part.text }]; } @@ -142,7 +147,7 @@ function responsesAssistantText(content: ResponsesApiMessage["content"]): string // image was present. The note is intentionally brief (not a data URI) so it // doesn't bloat the payload; the model is told the image was omitted. function responsesToolOutput(content: ResponsesApiMessage["content"]): string { - if (!Array.isArray(content)) { + if (!isContentPartArray(content)) { return JSON.stringify(content ?? ""); } @@ -164,12 +169,12 @@ export function joinedTextContent(content: string | null | readonly { type: stri return content; } - if (!Array.isArray(content)) { + if (!isContentPartArray(content)) { return ""; } return content - .filter((part): part is { type: string; text: string } => part.type === "text" && typeof part.text === "string") + .filter((part): part is { type: "text"; text: string } => part.type === "text" && typeof part.text === "string") .map((part) => part.text) .join(separator); } diff --git a/src/retry.ts b/src/retry.ts index ea2a5a5..6156282 100644 --- a/src/retry.ts +++ b/src/retry.ts @@ -44,11 +44,11 @@ const CONTEXT_RETRY_SAFETY_RATIO = 0.001; * * Order matters: more specific patterns should come first. */ -const RECOVERABLE_ERROR_PATTERNS: Array<{ +const RECOVERABLE_ERROR_PATTERNS: { pattern: RegExp; patch: (body: Record, match?: RegExpMatchArray) => Record; describe: (match: RegExpMatchArray) => string; -}> = [ +}[] = [ // --- Thinking errors --- // "invalid thinking: only type=enabled is allowed for this model" { @@ -169,10 +169,10 @@ const RECOVERABLE_ERROR_PATTERNS: Array<{ const fieldName = match?.[1]; if (!fieldName) return body; const next = { ...body }; - delete next[fieldName]; + next[fieldName] = undefined; return next; }, - describe: (match) => `removed field '${match?.[1]}' (not accepted by this model)`, + describe: (match) => `removed field '${match[1]}' (not accepted by this model)`, }, ]; @@ -232,7 +232,7 @@ function patchContextOverflow(errorMessage: string, body: Record> = []; + const toolCalls: Record[] = []; for (const item of output) { if (!isRecord(item)) { diff --git a/src/runtimeDiagnostics.ts b/src/runtimeDiagnostics.ts index 6c95a22..0cea633 100644 --- a/src/runtimeDiagnostics.ts +++ b/src/runtimeDiagnostics.ts @@ -11,7 +11,7 @@ export function runtimeDiagnosticsLines(context: vscode.ExtensionContext): strin `- remoteName: ${vscode.env.remoteName ?? "local"}`, `- uiKind: ${vscode.env.uiKind === vscode.UIKind.Web ? "web" : "desktop"}`, `- extensionMode: ${extensionModeLabel(context.extensionMode)}`, - `- workspaceTrusted: ${vscode.workspace.isTrusted}`, + `- workspaceTrusted: ${String(vscode.workspace.isTrusted)}`, `- platform: ${process.platform}`, `- architecture: ${process.arch}`, `- nodeVersion: ${process.version}`, diff --git a/src/streaming.ts b/src/streaming.ts index 33cb96f..0af392e 100644 --- a/src/streaming.ts +++ b/src/streaming.ts @@ -99,7 +99,7 @@ export async function streamChatCompletions(options: StreamRequestOptions): Prom const treatReasoningAsContent = isGoGateway && !hasReasoningEffort; if (isGoGateway) { options.output?.appendLine( - `[go-gw] model=${options.modelId} hasReasoningEffort=${hasReasoningEffort} treatReasoningAsContent=${treatReasoningAsContent}`, + `[go-gw] model=${options.modelId} hasReasoningEffort=${String(hasReasoningEffort)} treatReasoningAsContent=${String(treatReasoningAsContent)}`, ); } const extractor = new OpenAiResponseExtractor( @@ -121,11 +121,11 @@ export async function streamChatCompletions(options: StreamRequestOptions): Prom extractor.flushRemainingToolCalls(options.progress, options.requestHeaders["x-opencode-request"]); extractor.flushReasoningFallback(options.progress, options.requestHeaders["x-opencode-request"]); options.output?.appendLine( - `[stream-summary model=${options.modelId}] textChars=${extractor.emittedText} toolCalls=${extractor.emittedTools} reasoningChars=${extractor.reasoningChars}`, + `[stream-summary model=${options.modelId}] textChars=${String(extractor.emittedText)} toolCalls=${String(extractor.emittedTools)} reasoningChars=${String(extractor.reasoningChars)}`, ); if (extractor.reasoningLoopSuppressed) { options.output?.appendLine( - `[warn] model=${options.modelId} output suppressed after ~${extractor.emittedText} visible chars (probable model degradation at large context). Try a shorter conversation or use a different model.`, + `[warn] model=${options.modelId} output suppressed after ~${String(extractor.emittedText)} visible chars (probable model degradation at large context). Try a shorter conversation or use a different model.`, ); } if (extractor.emittedText === 0 && extractor.emittedTools === 0) { @@ -155,7 +155,7 @@ export async function streamAnthropicMessages(options: StreamRequestOptions): Pr extractor.flushReasoningFallback(options.progress, options.requestHeaders["x-opencode-request"]); options.output?.appendLine( - `[stream-summary model=${options.modelId}] textChars=${extractor.emittedText} toolCalls=${extractor.emittedTools} reasoningChars=${extractor.reasoningChars}`, + `[stream-summary model=${options.modelId}] textChars=${String(extractor.emittedText)} toolCalls=${String(extractor.emittedTools)} reasoningChars=${String(extractor.reasoningChars)}`, ); } @@ -178,7 +178,7 @@ export async function streamResponsesApi(options: StreamRequestOptions): Promise extractor.flushRemainingToolCalls(options.progress, options.requestHeaders["x-opencode-request"]); extractor.flushReasoningFallback(options.progress, options.requestHeaders["x-opencode-request"]); options.output?.appendLine( - `[stream-summary model=${options.modelId}] textChars=${extractor.emittedText} toolCalls=${extractor.emittedTools} reasoningChars=${extractor.reasoningChars}`, + `[stream-summary model=${options.modelId}] textChars=${String(extractor.emittedText)} toolCalls=${String(extractor.emittedTools)} reasoningChars=${String(extractor.reasoningChars)}`, ); } @@ -202,7 +202,7 @@ export async function streamGoogleGenerateContent(options: StreamRequestOptions) extractor.flushRemainingToolCalls(options.progress, options.requestHeaders["x-opencode-request"]); extractor.flushReasoningFallback(options.progress, options.requestHeaders["x-opencode-request"]); options.output?.appendLine( - `[stream-summary model=${options.modelId}] textChars=${extractor.emittedText} toolCalls=${extractor.emittedTools} reasoningChars=${extractor.reasoningChars}`, + `[stream-summary model=${options.modelId}] textChars=${String(extractor.emittedText)} toolCalls=${String(extractor.emittedTools)} reasoningChars=${String(extractor.reasoningChars)}`, ); } @@ -299,11 +299,10 @@ function sleepWithCancellation(ms: number, token: vscode.CancellationToken): Pro if (token.isCancellationRequested) return Promise.resolve(); return new Promise((resolve) => { - let settled = false; - const state: { cancellation?: vscode.Disposable } = {}; + const state: { cancellation?: vscode.Disposable; settled: boolean } = { settled: false }; const finish = () => { - if (settled) return; - settled = true; + if (state.settled) return; + state.settled = true; clearTimeout(timer); state.cancellation?.dispose(); resolve(); @@ -312,7 +311,7 @@ function sleepWithCancellation(ms: number, token: vscode.CancellationToken): Pro state.cancellation = token.onCancellationRequested(finish); // Close the race between the initial check and listener registration. - if (settled) { + if (state.settled) { state.cancellation.dispose(); } else if (token.isCancellationRequested) { finish(); @@ -334,14 +333,20 @@ async function streamOpenCodeResponse(options: StreamOpenCodeResponseOptions): P abortReason ??= reason; controller.abort(); }; - const cancellation = options.token.onCancellationRequested(() => abort("cancelled")); - const requestTimeout = setTimeout(() => abort("request-timeout"), options.requestTimeoutMs); + const cancellation = options.token.onCancellationRequested(() => { + abort("cancelled"); + }); + const requestTimeout = setTimeout(() => { + abort("request-timeout"); + }, options.requestTimeoutMs); let streamIdleTimeout: ReturnType | undefined; const resetStreamIdleTimeout = () => { if (streamIdleTimeout) { clearTimeout(streamIdleTimeout); } - streamIdleTimeout = setTimeout(() => abort("stream-idle-timeout"), options.streamIdleTimeoutMs); + streamIdleTimeout = setTimeout(() => { + abort("stream-idle-timeout"); + }, options.streamIdleTimeoutMs); }; const emitSummary = (totalBytes: number, totalEvents: number, extra?: Partial) => { if (emittedSummary) { @@ -375,7 +380,7 @@ async function streamOpenCodeResponse(options: StreamOpenCodeResponseOptions): P options.onTransportSummary?.(summary); options.output?.appendLine( - `[response-summary] status=${summary.status ?? "n/a"} durationMs=${summary.durationMs} ttfbMs=${summary.ttfbMs ?? "n/a"} promptTokens=${summary.promptTokens ?? "n/a"} completionTokens=${summary.completionTokens ?? "n/a"} totalTokens=${summary.totalTokens ?? "n/a"} cachedTokens=${summary.cachedTokens ?? "n/a"} finishReason=${summary.finishReason ?? ""} totalBytes=${summary.totalBytes} totalEvents=${summary.totalEvents}`, + `[response-summary] status=${String(summary.status ?? "n/a")} durationMs=${String(summary.durationMs)} ttfbMs=${String(summary.ttfbMs ?? "n/a")} promptTokens=${String(summary.promptTokens ?? "n/a")} completionTokens=${String(summary.completionTokens ?? "n/a")} totalTokens=${String(summary.totalTokens ?? "n/a")} cachedTokens=${String(summary.cachedTokens ?? "n/a")} finishReason=${summary.finishReason ?? ""} totalBytes=${String(summary.totalBytes)} totalEvents=${String(summary.totalEvents)}`, ); const usageLog = formatUsageLogLine({ promptTokens: summary.promptTokens, @@ -423,7 +428,7 @@ async function streamOpenCodeResponse(options: StreamOpenCodeResponseOptions): P // Log request for debugging latency. options.output?.appendLine( - `[request] url=${options.url} payloadBytes=${rawPayload.length} requestTimeoutMs=${options.requestTimeoutMs} streamIdleTimeoutMs=${options.streamIdleTimeoutMs}`, + `[request] url=${options.url} payloadBytes=${String(rawPayload.length)} requestTimeoutMs=${String(options.requestTimeoutMs)} streamIdleTimeoutMs=${String(options.streamIdleTimeoutMs)}`, ); // ------------------------------------------------------------------ @@ -461,7 +466,7 @@ async function streamOpenCodeResponse(options: StreamOpenCodeResponseOptions): P options.output?.appendLine(`[retry] HTTP 400 recoverable: ${patch.reason}. Retrying with patched bodyโ€ฆ`); payload = JSON.stringify(patch.body); response = await fetchWithBody(payload); - options.output?.appendLine(`[retry] Response after patch: ${response.status} ${response.statusText}`); + options.output?.appendLine(`[retry] Response after patch: ${String(response.status)} ${response.statusText}`); // If retry also returned 400, consume its body so the normal error // handler below doesn't try to re-read (the stream is already consumed). if (!response.ok && response.status === 400) { @@ -485,7 +490,7 @@ async function streamOpenCodeResponse(options: StreamOpenCodeResponseOptions): P // at the same timestamp. const backoffMs = Math.round(TRANSIENT_5XX_RETRY_BASE_MS * 2 ** (attempt - 1) + Math.random() * TRANSIENT_5XX_RETRY_JITTER_MS); options.output?.appendLine( - `[retry] transient ${response.status} (attempt ${attempt}/${TRANSIENT_5XX_MAX_RETRIES}); retrying in ${backoffMs}msโ€ฆ`, + `[retry] transient ${String(response.status)} (attempt ${String(attempt)}/${String(TRANSIENT_5XX_MAX_RETRIES)}); retrying in ${String(backoffMs)}msโ€ฆ`, ); await sleepWithCancellation(backoffMs, options.token); if (options.token.isCancellationRequested) { @@ -498,7 +503,7 @@ async function streamOpenCodeResponse(options: StreamOpenCodeResponseOptions): P responseStatus = response.status; responseContentType = response.headers.get("content-type") ?? ""; - options.output?.appendLine(`[http] ${response.status} ${response.statusText} content-type=${responseContentType || ""}`); + options.output?.appendLine(`[http] ${String(response.status)} ${response.statusText} content-type=${responseContentType || ""}`); const rateLimitSummary = formatRateLimitSummary(readRateLimitInfo(response.headers)); if (rateLimitSummary) { options.output?.appendLine(`[rate-limit] ${rateLimitSummary}`); @@ -568,13 +573,13 @@ async function streamOpenCodeResponse(options: StreamOpenCodeResponseOptions): P } resetStreamIdleTimeout(); - totalBytes += value?.byteLength ?? 0; - if (firstByteAt === undefined && (value?.byteLength ?? 0) > 0) { + totalBytes += value.byteLength; + if (firstByteAt === undefined && value.byteLength > 0) { firstByteAt = Date.now(); } const chunk = decoder.decode(value, { stream: true }); if (options.debugReasoning && options.output && chunk) { - options.output.appendLine(`[sse-raw bytes=${value?.byteLength ?? 0}] ${truncateForLog(chunk)}`); + options.output.appendLine(`[sse-raw bytes=${String(value.byteLength)}] ${truncateForLog(chunk)}`); } buffer += chunk; const events = buffer.split("\n\n"); @@ -609,7 +614,9 @@ async function streamOpenCodeResponse(options: StreamOpenCodeResponseOptions): P } if (options.debugReasoning && options.output) { - options.output.appendLine(`[sse-stats] totalBytes=${totalBytes} totalEvents=${totalEvents} bufferTailLen=${buffer.length}`); + options.output.appendLine( + `[sse-stats] totalBytes=${String(totalBytes)} totalEvents=${String(totalEvents)} bufferTailLen=${String(buffer.length)}`, + ); } // Diagnostic: when the gateway reported completion tokens but our @@ -618,10 +625,10 @@ async function streamOpenCodeResponse(options: StreamOpenCodeResponseOptions): P // but the response content is in an unrecognized format. if (usageSummary.completionTokens && usageSummary.completionTokens > 0 && extractedPartCount === 0 && rawSseData.length > 0) { options.output?.appendLine( - `[diag-empty-response] model=${options.modelId} completionTokens=${usageSummary.completionTokens} totalEvents=${totalEvents} rawSseDataCount=${rawSseData.length}`, + `[diag-empty-response] model=${options.modelId} completionTokens=${String(usageSummary.completionTokens)} totalEvents=${String(totalEvents)} rawSseDataCount=${String(rawSseData.length)}`, ); for (let i = 0; i < rawSseData.length; i++) { - options.output?.appendLine(`[diag-sse-event-${i}] ${truncateForLog(JSON.stringify(rawSseData[i]))}`); + options.output?.appendLine(`[diag-sse-event-${String(i)}] ${truncateForLog(JSON.stringify(rawSseData[i]))}`); } } @@ -916,7 +923,7 @@ class OpenAiResponseExtractor { * genuinely uses reasoning_content for CoT โ†’ goes to thinking panel. * - Zen gateway and all non-Go models are never affected. */ - private readonly treatReasoningAsContent: boolean = false, + private readonly treatReasoningAsContent = false, ) {} get emittedText(): number { @@ -1014,7 +1021,7 @@ class OpenAiResponseExtractor { return []; } - const first = data.choices[0]; + const first: unknown = data.choices[0]; if (!isRecord(first)) { return []; } @@ -1148,7 +1155,8 @@ class OpenAiResponseExtractor { private flushToolCalls(): vscode.LanguageModelToolCallPart[] { const calls = this.toolCallAccumulator.flush(); const parts = calls.map( - (call, index) => new vscode.LanguageModelToolCallPart(call.id || `opencodego-tool-${Date.now()}-${index}`, call.name, call.input), + (call, index) => + new vscode.LanguageModelToolCallPart(call.id || `opencodego-tool-${String(Date.now())}-${String(index)}`, call.name, call.input), ); if (this.reasoningContent.trim()) { @@ -1448,7 +1456,7 @@ class AnthropicResponseExtractor { const parts = toolCalls.map( (toolCall, index) => new vscode.LanguageModelToolCallPart( - toolCall.id || `opencodego-tool-${Date.now()}-${index}`, + toolCall.id || `opencodego-tool-${String(Date.now())}-${String(index)}`, toolCall.name, parseToolInput(toolCall.arguments), ), @@ -1473,7 +1481,7 @@ function extractChatCompletionParts(data: unknown): vscode.LanguageModelResponse return []; } - const first = data.choices[0]; + const first: unknown = data.choices[0]; if (!isRecord(first)) { return []; } @@ -1538,7 +1546,7 @@ function extractReasoningFromDelta(delta: Record): string { delta.reasoning_content, delta.reasoning, delta.thinking, - isRecord(delta.message) ? (delta.message as Record).reasoning_content : undefined, + isRecord(delta.message) ? delta.message.reasoning_content : undefined, ]; let collected = ""; for (const candidate of candidates) { @@ -1589,7 +1597,7 @@ function extractAnthropicParts(data: unknown): vscode.LanguageModelResponsePart[ } if (block.type === "tool_use" && typeof block.name === "string") { - const id = typeof block.id === "string" ? block.id : `opencodego-tool-${Date.now()}`; + const id = typeof block.id === "string" ? block.id : `opencodego-tool-${String(Date.now())}`; const input = isRecord(block.input) ? block.input : parseToolInput(typeof block.input === "string" ? block.input : "{}"); parts.push(new vscode.LanguageModelToolCallPart(id, block.name, input)); } @@ -1621,7 +1629,7 @@ function toolCallPartsFromOpenAiMessage(toolCalls: unknown): vscode.LanguageMode .filter(isRecord) .map((toolCall, index) => { const fn = toolCall.function; - const id = typeof toolCall.id === "string" ? toolCall.id : `opencodego-tool-${Date.now()}-${index}`; + const id = typeof toolCall.id === "string" ? toolCall.id : `opencodego-tool-${String(Date.now())}-${String(index)}`; const name = isRecord(fn) && typeof fn.name === "string" ? fn.name : ""; const args = isRecord(fn) && typeof fn.arguments === "string" ? fn.arguments : "{}"; return name ? new vscode.LanguageModelToolCallPart(id, name, parseToolInput(args)) : undefined; diff --git a/src/test/apiKeyResolution.test.ts b/src/test/apiKeyResolution.test.ts index fbee1fc..edaa984 100644 --- a/src/test/apiKeyResolution.test.ts +++ b/src/test/apiKeyResolution.test.ts @@ -2,16 +2,16 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { resolveResponseApiKey } from "../apiKeyResolution.js"; -describe("resolveResponseApiKey", () => { - it("prefers the request's native BYOK configuration", () => { +void describe("resolveResponseApiKey", () => { + void it("prefers the request's native BYOK configuration", () => { assert.equal(resolveResponseApiKey("configured", "registered", "stored"), "configured"); }); - it("uses the key captured while registering the selected model", () => { + void it("uses the key captured while registering the selected model", () => { assert.equal(resolveResponseApiKey(undefined, "registered", "stored"), "registered"); }); - it("falls back to SecretStorage after an extension-host cold start", () => { + void it("falls back to SecretStorage after an extension-host cold start", () => { assert.equal(resolveResponseApiKey(undefined, undefined, "stored"), "stored"); }); }); diff --git a/src/test/goUsageTracker.test.ts b/src/test/goUsageTracker.test.ts index bd6ea1c..d646bbc 100644 --- a/src/test/goUsageTracker.test.ts +++ b/src/test/goUsageTracker.test.ts @@ -34,9 +34,11 @@ interface GoUsageTrackerInstance { clear(): void; } -interface GoUsageTrackerConstructor { - new (context: unknown, log?: (msg: string) => void, costResolver?: (modelId: string) => ModelCost | undefined): GoUsageTrackerInstance; -} +type GoUsageTrackerConstructor = new ( + context: unknown, + log?: (msg: string) => void, + costResolver?: (modelId: string) => ModelCost | undefined, +) => GoUsageTrackerInstance; let GoUsageTracker: GoUsageTrackerConstructor; @@ -119,7 +121,7 @@ moduleResolver._resolveFilename = function (request: string, parent: unknown, .. // properly awaited by the test runner before any child tests execute. // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• -describe("goUsageTracker", () => { +void describe("goUsageTracker", () => { // โ”€โ”€ Bootstrap: dynamically import module under test โ”€โ”€ // (vscode mock is already installed via Module._resolveFilename above) @@ -133,8 +135,8 @@ describe("goUsageTracker", () => { // estimateCost() // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - describe("estimateCost()", () => { - it("uses bundled snapshot pricing for a known model (qwen3.6-plus)", () => { + void describe("estimateCost()", () => { + void it("uses bundled snapshot pricing for a known model (qwen3.6-plus)", () => { const cost = estimateCost("qwen3.6-plus", 100, 50, 10); // billablePrompt = max(0, 100-10) = 90 // pricing: { input: 0.50, output: 3.00, cache_read: 0.05 } @@ -144,7 +146,7 @@ describe("goUsageTracker", () => { assert.equal(cost, 0.0001955); }); - it("uses bundled snapshot pricing for deepseek-v4-flash", () => { + void it("uses bundled snapshot pricing for deepseek-v4-flash", () => { const cost = estimateCost("deepseek-v4-flash", 1000, 500, 200); // billablePrompt = 800 // pricing: { input: 0.14, output: 0.28, cache_read: 0.003 } @@ -154,12 +156,12 @@ describe("goUsageTracker", () => { assert.equal(cost, 0.0002526); }); - it("returns 0 for an unknown model with no resolver", () => { + void it("returns 0 for an unknown model with no resolver", () => { const cost = estimateCost("nonexistent-model-v99", 100, 50, 0); assert.equal(cost, 0); }); - it("prefers externalCost over the bundled table", () => { + void it("prefers externalCost over the bundled table", () => { const external: ModelCost = { input: 1.0, output: 2.0, cache_read: 0.1 }; const cost = estimateCost("qwen3.6-plus", 100, 50, 10, external); // billablePrompt = 90 @@ -169,7 +171,7 @@ describe("goUsageTracker", () => { assert.equal(cost, 0.000191); }); - it("prefers liveCostResolver over the bundled table when externalCost absent", () => { + void it("prefers liveCostResolver over the bundled table when externalCost absent", () => { const resolver = (id: string): ModelCost | undefined => (id === "custom-model" ? { input: 2.0, output: 4.0 } : undefined); const cost = estimateCost("custom-model", 100, 50, 0, undefined, resolver); // billablePrompt = 100 @@ -178,41 +180,41 @@ describe("goUsageTracker", () => { assert.equal(cost, 0.0004); }); - it("falls back to bundled table when resolver returns undefined", () => { + void it("falls back to bundled table when resolver returns undefined", () => { const resolver = (): ModelCost | undefined => undefined; const cost = estimateCost("qwen3.6-plus", 100, 50, 0, undefined, resolver); // 100 * 0.5/1M = 0.00005 // 50 * 3.0/1M = 0.00015 // IEEE 754: 0.05 + 0.00015 = 0.00019999999999999998 - assert.ok(Math.abs(cost - 0.0002) < 1e-12, `expected ~0.0002, got ${cost}`); + assert.ok(Math.abs(cost - 0.0002) < 1e-12, `expected ~0.0002, got ${String(cost)}`); }); - it("subtracts cached tokens from prompt tokens for billing", () => { + void it("subtracts cached tokens from prompt tokens for billing", () => { const cost = estimateCost("qwen3.6-plus", 100, 50, 40); // billablePrompt = 60 // 60 * 0.5/1M = 0.00003 // 50 * 3.0/1M = 0.00015 // 40 * 0.05/1M = 0.000002 // IEEE 754: 0.00003 + 0.00015 + 0.000002 = 0.00018199999999999998 - assert.ok(Math.abs(cost - 0.000182) < 1e-12, `expected ~0.000182, got ${cost}`); + assert.ok(Math.abs(cost - 0.000182) < 1e-12, `expected ~0.000182, got ${String(cost)}`); }); - it("handles all-cached requests (billable prompt = 0)", () => { + void it("handles all-cached requests (billable prompt = 0)", () => { const cost = estimateCost("qwen3.6-plus", 100, 50, 200); // billablePrompt = max(0, 100-200) = 0 // 0 * 0.5/1M = 0 // 50 * 3.0/1M = 0.00015 // 200 * 0.05/1M = 0.00001 // IEEE 754: 0 + 0.00015 + 0.00001 = 0.00015999999999999999 - assert.ok(Math.abs(cost - 0.00016) < 1e-12, `expected ~0.00016, got ${cost}`); + assert.ok(Math.abs(cost - 0.00016) < 1e-12, `expected ~0.00016, got ${String(cost)}`); }); - it("handles zero tokens gracefully", () => { + void it("handles zero tokens gracefully", () => { const cost = estimateCost("qwen3.6-plus", 0, 0, 0); assert.equal(cost, 0); }); - it("uses explicit cache_read when provided in pricing", () => { + void it("uses explicit cache_read when provided in pricing", () => { const external: ModelCost = { input: 1.0, output: 2.0, cache_read: 0.5 }; const cost = estimateCost("any-model", 200, 100, 50, external); // billablePrompt = 150 @@ -222,7 +224,7 @@ describe("goUsageTracker", () => { assert.equal(cost, 0.000375); }); - it("falls back to input * 0.1 when cache_read is missing", () => { + void it("falls back to input * 0.1 when cache_read is missing", () => { const external: ModelCost = { input: 2.0, output: 4.0 }; // no cache_read const cost = estimateCost("any-model", 100, 50, 10, external); // billablePrompt = 90 @@ -237,11 +239,11 @@ describe("goUsageTracker", () => { // GoUsageTracker // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - describe("GoUsageTracker", () => { + void describe("GoUsageTracker", () => { // โ”€โ”€ record() โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - describe("record()", () => { - it("accumulates cost for the same sessionId", () => { + void describe("record()", () => { + void it("accumulates cost for the same sessionId", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record(makeSummary({ sessionId: "s1", promptTokens: 100, completionTokens: 50, cachedTokens: 0 })); @@ -253,27 +255,27 @@ describe("goUsageTracker", () => { const session = tracker.getCurrentSessionCost(); assert.equal(session?.sessionId, "s1"); - assert.equal(session?.cost, 0.0006); - assert.equal(session?.requests, 2); - assert.equal(session?.promptTokens, 300); - assert.equal(session?.completionTokens, 150); + assert.equal(session.cost, 0.0006); + assert.equal(session.requests, 2); + assert.equal(session.promptTokens, 300); + assert.equal(session.completionTokens, 150); }); - it("skips records when providerDisplayName does not contain 'go'", () => { + void it("skips records when providerDisplayName does not contain 'go'", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record(makeSummary({ providerDisplayName: "OpenCode Zen", sessionId: "s1" })); assert.equal(tracker.getCurrentSessionCost(), undefined); }); - it("skips records when prompt+completion tokens are zero", () => { + void it("skips records when prompt+completion tokens are zero", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record(makeSummary({ sessionId: "s1", promptTokens: 0, completionTokens: 0, cachedTokens: 0 })); assert.equal(tracker.getCurrentSessionCost(), undefined); }); - it("creates separate entries for different sessionIds", () => { + void it("creates separate entries for different sessionIds", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record(makeSummary({ sessionId: "s1", promptTokens: 100, completionTokens: 50 })); tracker.record(makeSummary({ sessionId: "s2", promptTokens: 10, completionTokens: 5 })); @@ -281,7 +283,7 @@ describe("goUsageTracker", () => { assert.equal(tracker.getRecentSessionCosts(5).length, 2); }); - it("accepts an externalCost override", () => { + void it("accepts an externalCost override", () => { const tracker = new GoUsageTracker(createMockContext()); const externalCost: ModelCost = { input: 10, output: 20 }; tracker.record(makeSummary({ sessionId: "s1", promptTokens: 100, completionTokens: 50, cachedTokens: 0 }), externalCost); @@ -291,7 +293,7 @@ describe("goUsageTracker", () => { assert.equal(session?.cost, 0.002); }); - it("delegates to costResolver when no externalCost is passed", () => { + void it("delegates to costResolver when no externalCost is passed", () => { const resolver = (id: string): ModelCost | undefined => (id === "custom-resolved" ? { input: 5, output: 10 } : undefined); const tracker = new GoUsageTracker(createMockContext(), undefined, resolver); @@ -304,7 +306,7 @@ describe("goUsageTracker", () => { assert.equal(session?.cost, 0.001); }); - it("persists data to globalState after record()", () => { + void it("persists data to globalState after record()", () => { const context = createMockContext(); const tracker = new GoUsageTracker(context); @@ -321,8 +323,8 @@ describe("goUsageTracker", () => { // โ”€โ”€ getCurrentSessionCost() โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - describe("getCurrentSessionCost()", () => { - it("returns the most recently active session", () => { + void describe("getCurrentSessionCost()", () => { + void it("returns the most recently active session", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record(makeSummary({ sessionId: "old", promptTokens: 1, completionTokens: 0 })); // Ensure distinct timestamps for deterministic ordering @@ -333,7 +335,7 @@ describe("goUsageTracker", () => { assert.equal(tracker.getCurrentSessionCost()?.sessionId, "new"); }); - it("returns the aggregated cost for the most recent session", () => { + void it("returns the aggregated cost for the most recent session", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record(makeSummary({ sessionId: "s1", promptTokens: 100, completionTokens: 50, cachedTokens: 0 })); tracker.record(makeSummary({ sessionId: "s2", promptTokens: 200, completionTokens: 100, cachedTokens: 0 })); @@ -343,11 +345,11 @@ describe("goUsageTracker", () => { // s2: 0.0004 โ€” most recent const session = tracker.getCurrentSessionCost(); assert.equal(session?.sessionId, "s1"); // s1 was last to be active - assert.equal(session?.cost, 0.0003); - assert.equal(session?.requests, 2); + assert.equal(session.cost, 0.0003); + assert.equal(session.requests, 2); }); - it("returns undefined when no sessions have been recorded", () => { + void it("returns undefined when no sessions have been recorded", () => { const tracker = new GoUsageTracker(createMockContext()); assert.equal(tracker.getCurrentSessionCost(), undefined); }); @@ -355,7 +357,7 @@ describe("goUsageTracker", () => { // โ”€โ”€ getRecentSessionCosts() โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - describe("getRecentSessionCosts()", () => { + void describe("getRecentSessionCosts()", () => { /** Ensure each record gets a distinct timestamp for deterministic ordering. */ function recordWithDistinctTimestamp(tracker: GoUsageTrackerInstance, sessionId: string): void { tracker.record(makeSummary({ sessionId, promptTokens: 1, completionTokens: 0 })); @@ -363,7 +365,7 @@ describe("goUsageTracker", () => { while (Date.now() === t) {} // wait for next millisecond } - it("returns sessions ordered by lastActivity descending", () => { + void it("returns sessions ordered by lastActivity descending", () => { const tracker = new GoUsageTracker(createMockContext()); recordWithDistinctTimestamp(tracker, "a"); recordWithDistinctTimestamp(tracker, "b"); @@ -376,10 +378,10 @@ describe("goUsageTracker", () => { assert.equal(sessions[2].sessionId, "a"); }); - it("respects the limit parameter", () => { + void it("respects the limit parameter", () => { const tracker = new GoUsageTracker(createMockContext()); for (let i = 0; i < 10; i++) { - tracker.record(makeSummary({ sessionId: `s${i}`, promptTokens: 1, completionTokens: 0 })); + tracker.record(makeSummary({ sessionId: `s${String(i)}`, promptTokens: 1, completionTokens: 0 })); } assert.equal(tracker.getRecentSessionCosts(3).length, 3); @@ -388,7 +390,7 @@ describe("goUsageTracker", () => { assert.equal(tracker.getRecentSessionCosts(100).length, 10); }); - it("returns empty array when no sessions exist", () => { + void it("returns empty array when no sessions exist", () => { const tracker = new GoUsageTracker(createMockContext()); assert.deepEqual(tracker.getRecentSessionCosts(), []); }); @@ -396,8 +398,8 @@ describe("goUsageTracker", () => { // โ”€โ”€ State restoration from globalState โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - describe("state restoration from globalState", () => { - it("restores entries and session costs from stored state", () => { + void describe("state restoration from globalState", () => { + void it("restores entries and session costs from stored state", () => { const now = Date.now(); const initial: Record = { "opencodego.usageLog.v1": [ @@ -428,13 +430,13 @@ describe("goUsageTracker", () => { const session = tracker.getCurrentSessionCost(); assert.equal(session?.sessionId, "restored-session"); - assert.equal(session?.cost, 0.5); - assert.equal(session?.requests, 3); - assert.equal(session?.promptTokens, 150); - assert.equal(session?.completionTokens, 75); + assert.equal(session.cost, 0.5); + assert.equal(session.requests, 3); + assert.equal(session.promptTokens, 150); + assert.equal(session.completionTokens, 75); }); - it("filters invalid entries during restore", () => { + void it("filters invalid entries during restore", () => { const initial: Record = { "opencodego.usageLog.v1": [ { timestamp: Date.now(), modelId: "valid", cost: 0.1, promptTokens: 10, completionTokens: 5, cachedTokens: 0, sessionId: "s1" }, @@ -457,7 +459,7 @@ describe("goUsageTracker", () => { assert.equal(sessions.length, 1); }); - it("starts clean when no state is stored", () => { + void it("starts clean when no state is stored", () => { const tracker = new GoUsageTracker(createMockContext()); assert.equal(tracker.getCurrentSessionCost(), undefined); assert.deepEqual(tracker.getRecentSessionCosts(), []); @@ -466,12 +468,12 @@ describe("goUsageTracker", () => { // โ”€โ”€ Pruning behavior โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - describe("pruning behavior", () => { + void describe("pruning behavior", () => { afterEach(() => { mock.timers.reset(); }); - it("removes idle sessions (older than 2h) on record()", () => { + void it("removes idle sessions (older than 2h) on record()", () => { mock.timers.enable({ apis: ["Date"] }); const baseTime = 1_000_000_000_000; mock.timers.setTime(baseTime); @@ -491,7 +493,7 @@ describe("goUsageTracker", () => { assert.equal(sessions.length, 1); }); - it("removes multiple idle sessions at once", () => { + void it("removes multiple idle sessions at once", () => { mock.timers.enable({ apis: ["Date"] }); mock.timers.setTime(1_000_000_000_000); @@ -508,11 +510,11 @@ describe("goUsageTracker", () => { assert.equal(tracker.getCurrentSessionCost()?.sessionId, "s3"); }); - it("caps at MAX_SESSIONS (50) and removes oldest", () => { + void it("caps at MAX_SESSIONS (50) and removes oldest", () => { const tracker = new GoUsageTracker(createMockContext()); // Create 51 sessions for (let i = 0; i < 51; i++) { - tracker.record(makeSummary({ sessionId: `s${i}`, promptTokens: 1, completionTokens: 0 })); + tracker.record(makeSummary({ sessionId: `s${String(i)}`, promptTokens: 1, completionTokens: 0 })); } const sessions = tracker.getRecentSessionCosts(100); @@ -533,8 +535,8 @@ describe("goUsageTracker", () => { // โ”€โ”€ Edge cases โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - describe("edge cases", () => { - it("handles missing sessionId (no session cost tracked)", () => { + void describe("edge cases", () => { + void it("handles missing sessionId (no session cost tracked)", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record(makeSummary({ sessionId: undefined, promptTokens: 100, completionTokens: 50 })); @@ -542,7 +544,7 @@ describe("goUsageTracker", () => { assert.equal(tracker.getRecentSessionCosts().length, 0); }); - it("handles unknown modelId (cost = 0)", () => { + void it("handles unknown modelId (cost = 0)", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record( makeSummary({ @@ -555,11 +557,11 @@ describe("goUsageTracker", () => { const session = tracker.getCurrentSessionCost(); assert.equal(session?.sessionId, "s1"); - assert.equal(session?.cost, 0); - assert.equal(session?.requests, 1); + assert.equal(session.cost, 0); + assert.equal(session.requests, 1); }); - it("handles record with only cached tokens (no prompt or completion)", () => { + void it("handles record with only cached tokens (no prompt or completion)", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record(makeSummary({ sessionId: "s1", promptTokens: 0, completionTokens: 0, cachedTokens: 100 })); @@ -567,7 +569,7 @@ describe("goUsageTracker", () => { assert.equal(tracker.getCurrentSessionCost(), undefined); }); - it("handles record with only cached tokens but non-zero prompt", () => { + void it("handles record with only cached tokens but non-zero prompt", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record(makeSummary({ sessionId: "s1", promptTokens: 50, completionTokens: 0, cachedTokens: 50 })); @@ -576,10 +578,10 @@ describe("goUsageTracker", () => { // cost = 0 * 0.5/1M + 0 * 3.0/1M + 50 * 0.05/1M = 0.0000025 const session = tracker.getCurrentSessionCost(); assert.equal(session?.sessionId, "s1"); - assert.equal(session?.cost, 0.0000025); + assert.equal(session.cost, 0.0000025); }); - it("handles multiple records in the same session with reset in between", () => { + void it("handles multiple records in the same session with reset in between", () => { const context = createMockContext(); const tracker1 = new GoUsageTracker(context); tracker1.record(makeSummary({ sessionId: "shared", promptTokens: 100, completionTokens: 50, cachedTokens: 0 })); @@ -593,7 +595,7 @@ describe("goUsageTracker", () => { // Second tracker should have restored state and accumulated further const session = tracker2.getCurrentSessionCost(); assert.equal(session?.cost, 0.0003); // 0.0002 + 0.0001 - assert.equal(session?.requests, 2); + assert.equal(session.requests, 2); }); }); }); diff --git a/src/test/imageNormalizer.test.ts b/src/test/imageNormalizer.test.ts index 11eb7cf..8177a24 100644 --- a/src/test/imageNormalizer.test.ts +++ b/src/test/imageNormalizer.test.ts @@ -5,13 +5,13 @@ import { getImageDataUrlBase64Bytes, MAX_IMAGE_BASE64_BYTES, normalizeImageDataU const ONE_PIXEL_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="; -describe("normalizeImageDataUrl", () => { - it("keeps a small image unchanged", async () => { +void describe("normalizeImageDataUrl", () => { + void it("keeps a small image unchanged", async () => { const url = `data:image/png;base64,${ONE_PIXEL_PNG}`; assert.equal(await normalizeImageDataUrl(url), url); }); - it("resizes an image that exceeds the CLI dimension limit", async () => { + void it("resizes an image that exceeds the CLI dimension limit", async () => { const image = new PhotonImage(new Uint8Array(2_001 * 4).fill(255), 2_001, 1); try { const url = `data:image/png;base64,${Buffer.from(image.get_bytes()).toString("base64")}`; @@ -24,7 +24,7 @@ describe("normalizeImageDataUrl", () => { } }); - it("does not reject a large raw image when its normalized base64 payload fits", async () => { + void it("does not reject a large raw image when its normalized base64 payload fits", async () => { const width = 750; const height = 1_000; const pixels = new Uint8Array(width * height * 4); @@ -42,18 +42,20 @@ describe("normalizeImageDataUrl", () => { const normalized = await normalizeImageDataUrl(url); assert.equal(normalized, url); - assert.ok(getImageDataUrlBase64Bytes(normalized)! <= MAX_IMAGE_BASE64_BYTES); + const dataBytes = getImageDataUrlBase64Bytes(normalized); + assert.ok(dataBytes !== undefined); + assert.ok(dataBytes <= MAX_IMAGE_BASE64_BYTES); } finally { image.free(); } }); - it("passes non-data URLs through unchanged", async () => { + void it("passes non-data URLs through unchanged", async () => { const url = "https://example.com/image.png"; assert.equal(await normalizeImageDataUrl(url), url); }); - it("passes malformed image data through unchanged", async () => { + void it("passes malformed image data through unchanged", async () => { const url = "data:image/png;base64,not-an-image"; assert.equal(await normalizeImageDataUrl(url), url); }); diff --git a/src/test/metadata.test.ts b/src/test/metadata.test.ts index 59038ac..257e577 100644 --- a/src/test/metadata.test.ts +++ b/src/test/metadata.test.ts @@ -14,67 +14,67 @@ import { GO_VENDOR, ZEN_VENDOR } from "../providerTypes.js"; * These tests verify the bundled fallback metadata is correct even when the * live models.dev fetch is unavailable. */ -describe("fallbackModelMetadata โ€” kimi-k2.7-code (issue #25)", () => { - it("returns metadata for kimi-k2.7-code on GO_VENDOR", () => { +void describe("fallbackModelMetadata โ€” kimi-k2.7-code (issue #25)", () => { + void it("returns metadata for kimi-k2.7-code on GO_VENDOR", () => { const meta = fallbackModelMetadata("kimi-k2.7-code", GO_VENDOR); assert.ok(meta, "expected fallback metadata to be defined"); }); - it("reports temperature: false (Moonshot rejects non-default temperature)", () => { + void it("reports temperature: false (Moonshot rejects non-default temperature)", () => { const meta = fallbackModelMetadata("kimi-k2.7-code", GO_VENDOR); assert.equal(meta?.temperature, false); }); - it("reports correct context/output limits (models.dev: 256000 / 262144)", () => { + void it("reports correct context/output limits (models.dev: 256000 / 262144)", () => { const meta = fallbackModelMetadata("kimi-k2.7-code", GO_VENDOR); assert.equal(meta?.contextWindow, 256000); - assert.equal(meta?.maxOutputTokens, 262144); + assert.equal(meta.maxOutputTokens, 262144); }); - it("reports vision capability (models.dev attachment: true)", () => { + void it("reports vision capability (models.dev attachment: true)", () => { const meta = fallbackModelMetadata("kimi-k2.7-code", GO_VENDOR); assert.equal(meta?.supportsVision, true); }); - it("reports reasoning capability (supportsReasoning matches /^kimi-/i)", () => { + void it("reports reasoning capability (supportsReasoning matches /^kimi-/i)", () => { const meta = fallbackModelMetadata("kimi-k2.7-code", GO_VENDOR); assert.equal(meta?.reasoning, true); }); }); -describe("fallbackModelMetadata โ€” regression safety for other kimi models", () => { - it("kimi-k2.6 does NOT report temperature: false (still accepts temperature)", () => { +void describe("fallbackModelMetadata โ€” regression safety for other kimi models", () => { + void it("kimi-k2.6 does NOT report temperature: false (still accepts temperature)", () => { const meta = fallbackModelMetadata("kimi-k2.6", GO_VENDOR); // temperature should be undefined (not false) so the request body still // includes the configured temperature for k2.6. assert.notEqual(meta?.temperature, false); }); - it("kimi-k2.5 does NOT report temperature: false", () => { + void it("kimi-k2.5 does NOT report temperature: false", () => { const meta = fallbackModelMetadata("kimi-k2.5", GO_VENDOR); assert.notEqual(meta?.temperature, false); }); }); -describe("fallbackModelMetadata โ€” non-kimi models unaffected", () => { - it("glm-5 does not report temperature: false", () => { +void describe("fallbackModelMetadata โ€” non-kimi models unaffected", () => { + void it("glm-5 does not report temperature: false", () => { const meta = fallbackModelMetadata("glm-5", GO_VENDOR); assert.notEqual(meta?.temperature, false); }); - it("deepseek-v4-pro does not report temperature: false", () => { + void it("deepseek-v4-pro does not report temperature: false", () => { const meta = fallbackModelMetadata("deepseek-v4-pro", GO_VENDOR); assert.notEqual(meta?.temperature, false); }); - it("claude-opus-4-7 on ZEN does not report temperature: false", () => { + void it("claude-opus-4-7 on ZEN does not report temperature: false", () => { const meta = fallbackModelMetadata("claude-opus-4-7", ZEN_VENDOR); assert.notEqual(meta?.temperature, false); }); }); -describe("VISION_CAPABLE_MODELS", () => { - it("includes known vision models (minimax-m2.7, kimi-k2.6, mimo-v2.5)", () => { +void describe("VISION_CAPABLE_MODELS", () => { + void it("includes known vision models (minimax-m2.7, kimi-k2.6, mimo-v2.5)", () => { assert.ok(VISION_CAPABLE_MODELS.has("minimax-m2.7")); assert.ok(VISION_CAPABLE_MODELS.has("kimi-k2.6")); assert.ok(VISION_CAPABLE_MODELS.has("mimo-v2.5")); @@ -82,32 +82,32 @@ describe("VISION_CAPABLE_MODELS", () => { assert.ok(VISION_CAPABLE_MODELS.has("mimo-v2.5-pro")); }); - it("does NOT include text-only models (deepseek-v4-flash, hy3-preview, big-pickle)", () => { + void it("does NOT include text-only models (deepseek-v4-flash, hy3-preview, big-pickle)", () => { assert.ok(!VISION_CAPABLE_MODELS.has("deepseek-v4-flash")); assert.ok(!VISION_CAPABLE_MODELS.has("deepseek-v4-pro")); assert.ok(!VISION_CAPABLE_MODELS.has("hy3-preview")); assert.ok(!VISION_CAPABLE_MODELS.has("big-pickle")); }); - it("is an exported Set", () => { + void it("is an exported Set", () => { assert.ok(VISION_CAPABLE_MODELS instanceof Set); assert.ok(VISION_CAPABLE_MODELS.size > 10); }); }); -describe("getContextSizeOptionsForModel โ€” Kimi context tiers (issue #87)", () => { - it("offers 256K and the full window when Kimi has a larger context", () => { +void describe("getContextSizeOptionsForModel โ€” Kimi context tiers (issue #87)", () => { + void it("offers 256K and the full window when Kimi has a larger context", () => { const options = getContextSizeOptionsForModel("kimi-k3", { input: 3, output: 15 }, 1_048_576); assert.deepEqual( options?.map((option) => option.value), [256_000, 1_048_576], ); - assert.equal(options?.[0].isDefault, true); - assert.equal(options?.[1].description, "Higher pricing"); + assert.equal(options[0].isDefault, true); + assert.equal(options[1].description, "Higher pricing"); }); - it("recognizes the official short K3 model id", () => { + void it("recognizes the official short K3 model id", () => { const options = getContextSizeOptionsForModel("k3", undefined, 1_000_000); assert.deepEqual( options?.map((option) => option.value), @@ -115,11 +115,11 @@ describe("getContextSizeOptionsForModel โ€” Kimi context tiers (issue #87)", () ); }); - it("does not add a redundant tier to a 256K Kimi model", () => { + void it("does not add a redundant tier to a 256K Kimi model", () => { assert.equal(getContextSizeOptionsForModel("kimi-k2.6", { input: 0.95, output: 4 }, 262_144), undefined); }); - it("prefers explicit models.dev pricing tiers", () => { + void it("prefers explicit models.dev pricing tiers", () => { const options = getContextSizeOptionsForModel( "kimi-k3", { diff --git a/src/test/modelLimits.test.ts b/src/test/modelLimits.test.ts index 65bd2cb..8cdb753 100644 --- a/src/test/modelLimits.test.ts +++ b/src/test/modelLimits.test.ts @@ -7,8 +7,8 @@ const metadata = { maxOutputTokens: 32_000, }; -describe("calculateModelLimits", () => { - it("uses a conservative registration budget when prompt size is unknown", () => { +void describe("calculateModelLimits", () => { + void it("uses a conservative registration budget when prompt size is unknown", () => { const limits = calculateModelLimits(metadata); assert.equal(limits.maxOutputTokens, 19_936); @@ -17,19 +17,19 @@ describe("calculateModelLimits", () => { assert.equal(limits.advertisedMaxInputTokens, 91_808); }); - it("caps output to the context remaining after the prompt and safety margin", () => { + void it("caps output to the context remaining after the prompt and safety margin", () => { const limits = calculateModelLimits(metadata, { promptTokens: 70_000 }); assert.equal(limits.maxOutputTokens, 21_600); }); - it("never restores a 4K minimum that would overflow a nearly full context", () => { + void it("never restores a 4K minimum that would overflow a nearly full context", () => { const limits = calculateModelLimits(metadata, { promptTokens: 99_990 }); assert.equal(limits.maxOutputTokens, 1); }); - it("honors context and output overrides without exceeding either", () => { + void it("honors context and output overrides without exceeding either", () => { const limits = calculateModelLimits(metadata, { contextSize: 50_000, maxOutputTokens: 12_000, @@ -40,7 +40,7 @@ describe("calculateModelLimits", () => { assert.equal(limits.maxOutputTokens, 10_800); }); - it("keeps the issue #109 DeepSeek request below the real context limit", () => { + void it("keeps the issue #109 DeepSeek request below the real context limit", () => { const limits = calculateModelLimits({ contextWindow: 1_048_576, maxOutputTokens: 384_000 }, { promptTokens: 604_839 }); assert.equal(limits.maxOutputTokens, 371_156); diff --git a/src/test/modelNames.test.ts b/src/test/modelNames.test.ts index 4deb8e4..923ed44 100644 --- a/src/test/modelNames.test.ts +++ b/src/test/modelNames.test.ts @@ -2,16 +2,16 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { formatModelName, providerModelDisplayName } from "../modelNames.js"; -describe("provider model display names", () => { - it("formats numeric model versions like the existing picker", () => { +void describe("provider model display names", () => { + void it("formats numeric model versions like the existing picker", () => { assert.equal(formatModelName("gpt-5-6-luna"), "Gpt 5.6 Luna"); }); - it("includes the provider prefix by default", () => { + void it("includes the provider prefix by default", () => { assert.equal(providerModelDisplayName("OpenCode Go", "kimi-k3"), "OpenCode Go / Kimi K3"); }); - it("can hide the provider prefix without changing the model name", () => { + void it("can hide the provider prefix without changing the model name", () => { assert.equal(providerModelDisplayName("OpenCode Zen", "kimi-k3", false), "Kimi K3"); }); }); diff --git a/src/test/responsesRequest.test.ts b/src/test/responsesRequest.test.ts index 163fc19..a3b3dcb 100644 --- a/src/test/responsesRequest.test.ts +++ b/src/test/responsesRequest.test.ts @@ -2,8 +2,8 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { buildResponsesRequestEnvelope, responsesInputItemsFromMessage } from "../responsesRequest.js"; -describe("buildResponsesRequestEnvelope", () => { - it("enables server-side input truncation for long Responses sessions", () => { +void describe("buildResponsesRequestEnvelope", () => { + void it("enables server-side input truncation for long Responses sessions", () => { const body = buildResponsesRequestEnvelope({ model: "gpt-5.6-luna", input: [{ role: "user", content: "hello" }], @@ -14,7 +14,7 @@ describe("buildResponsesRequestEnvelope", () => { assert.equal(body.max_output_tokens, 4096); }); - it("does not force an unsupported text verbosity option", () => { + void it("does not force an unsupported text verbosity option", () => { const body = buildResponsesRequestEnvelope({ model: "gpt-5.6-luna", input: [], @@ -24,7 +24,7 @@ describe("buildResponsesRequestEnvelope", () => { assert.ok(!("text" in body)); }); - it("only includes optional temperature and tool fields when provided", () => { + void it("only includes optional temperature and tool fields when provided", () => { const body = buildResponsesRequestEnvelope({ model: "gpt-5.6-luna", input: [], @@ -42,8 +42,8 @@ describe("buildResponsesRequestEnvelope", () => { }); }); -describe("responsesInputItemsFromMessage", () => { - it("emits user image as input_image with image_url as a plain STRING", () => { +void describe("responsesInputItemsFromMessage", () => { + void it("emits user image as input_image with image_url as a plain STRING", () => { // Regression: the Responses API expects `input_image.image_url` to be a // string (URL or base64 data URL), NOT the `{ url }` object shape used by // Chat Completions. The nested object made the gateway reject the request @@ -64,12 +64,12 @@ describe("responsesInputItemsFromMessage", () => { ]); }); - it("drops an empty string user message", () => { + void it("drops an empty string user message", () => { const items = responsesInputItemsFromMessage({ role: "user", content: "" }); assert.deepEqual(items, []); }); - it("emits assistant text as output_text and tool calls as function_call", () => { + void it("emits assistant text as output_text and tool calls as function_call", () => { const items = responsesInputItemsFromMessage({ role: "assistant", content: [{ type: "text", text: "let me check" }], @@ -94,7 +94,7 @@ describe("responsesInputItemsFromMessage", () => { ]); }); - it("degrades tool results with images to a text note", () => { + void it("degrades tool results with images to a text note", () => { const items = responsesInputItemsFromMessage({ role: "tool", tool_call_id: "call_1", @@ -110,7 +110,7 @@ describe("responsesInputItemsFromMessage", () => { assert.match(output, /Responses API does not support images in tool output/); }); - it("returns no items for unsupported roles", () => { + void it("returns no items for unsupported roles", () => { const items = responsesInputItemsFromMessage({ role: "system", content: "be helpful", diff --git a/src/test/retry.test.ts b/src/test/retry.test.ts index c90b19b..d6b46d9 100644 --- a/src/test/retry.test.ts +++ b/src/test/retry.test.ts @@ -2,73 +2,73 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { analyzeHttp400ForRetry, isTransientServerError } from "../retry.js"; -describe("analyzeHttp400ForRetry โ€” thinking errors", () => { - it("patches 'only type=enabled is allowed' to force thinking.type='enabled'", () => { +void describe("analyzeHttp400ForRetry โ€” thinking errors", () => { + void it("patches 'only type=enabled is allowed' to force thinking.type='enabled'", () => { const body = { model: "kimi-k2.5", thinking: { type: "disabled" } }; const result = analyzeHttp400ForRetry("invalid thinking: only type=enabled is allowed for this model", body); assert.ok(result, "should be recoverable"); - assert.deepEqual(result!.body, { model: "kimi-k2.5", thinking: { type: "enabled" } }); - assert.match(result!.reason, /thinking/i); + assert.deepEqual(result.body, { model: "kimi-k2.5", thinking: { type: "enabled" } }); + assert.match(result.reason, /thinking/i); }); - it("patches 'only type=disabled is allowed' by removing thinking", () => { + void it("patches 'only type=disabled is allowed' by removing thinking", () => { const body = { model: "some-model", thinking: { type: "enabled" } }; const result = analyzeHttp400ForRetry("invalid thinking: only type=disabled is allowed", body); assert.ok(result, "should be recoverable"); - assert.deepEqual(result!.body, { model: "some-model" }); + assert.deepEqual(result.body, { model: "some-model" }); }); - it("patches generic 'invalid thinking' by removing thinking field", () => { + void it("patches generic 'invalid thinking' by removing thinking field", () => { const body = { model: "test", thinking: { type: "disabled" }, temperature: 0.2 }; const result = analyzeHttp400ForRetry("invalid thinking parameter", body); assert.ok(result, "should be recoverable"); - assert.deepEqual(result!.body, { model: "test", temperature: 0.2 }); + assert.deepEqual(result.body, { model: "test", temperature: 0.2 }); }); }); -describe("analyzeHttp400ForRetry โ€” temperature errors", () => { - it("patches 'invalid temperature: only 1 is allowed' by removing temperature", () => { +void describe("analyzeHttp400ForRetry โ€” temperature errors", () => { + void it("patches 'invalid temperature: only 1 is allowed' by removing temperature", () => { const body = { model: "kimi-k2.7-code", temperature: 0.2 }; const result = analyzeHttp400ForRetry("invalid temperature: only 1 is allowed for this model", body); assert.ok(result, "should be recoverable"); - assert.deepEqual(result!.body, { model: "kimi-k2.7-code" }); + assert.deepEqual(result.body, { model: "kimi-k2.7-code" }); }); }); -describe("analyzeHttp400ForRetry โ€” enable_thinking errors", () => { - it("patches 'Extra inputs are not permitted, field: enable_thinking'", () => { +void describe("analyzeHttp400ForRetry โ€” enable_thinking errors", () => { + void it("patches 'Extra inputs are not permitted, field: enable_thinking'", () => { const body = { model: "kimi-k2.5", enable_thinking: false }; const result = analyzeHttp400ForRetry("Extra inputs are not permitted, field: 'enable_thinking', value: False", body); assert.ok(result, "should be recoverable"); - assert.deepEqual(result!.body, { model: "kimi-k2.5" }); + assert.deepEqual(result.body, { model: "kimi-k2.5" }); }); }); -describe("analyzeHttp400ForRetry โ€” reasoning_effort errors", () => { - it("patches reasoning_effort rejection", () => { +void describe("analyzeHttp400ForRetry โ€” reasoning_effort errors", () => { + void it("patches reasoning_effort rejection", () => { const body = { model: "minimax-m2.7", reasoning_effort: "high" }; const result = analyzeHttp400ForRetry("MiniMax M2 only accepts string reasoning_effort values ('low', 'medium', 'high')", body); assert.ok(result, "should be recoverable"); - assert.deepEqual(result!.body, { model: "minimax-m2.7" }); + assert.deepEqual(result.body, { model: "minimax-m2.7" }); }); }); -describe("analyzeHttp400ForRetry โ€” non-recoverable errors", () => { - it("returns undefined for auth errors", () => { +void describe("analyzeHttp400ForRetry โ€” non-recoverable errors", () => { + void it("returns undefined for auth errors", () => { const body = { model: "test" }; const result = analyzeHttp400ForRetry("unauthorized", body); assert.equal(result, undefined); }); - it("returns undefined for unrelated errors", () => { + void it("returns undefined for unrelated errors", () => { const body = { model: "test" }; const result = analyzeHttp400ForRetry("model not found", body); assert.equal(result, undefined); }); }); -describe("analyzeHttp400ForRetry โ€” context overflow", () => { - it("reduces max_tokens using the authoritative counts from issue #109", () => { +void describe("analyzeHttp400ForRetry โ€” context overflow", () => { + void it("reduces max_tokens using the authoritative counts from issue #109", () => { const body = { model: "deepseek-v4-flash", max_tokens: 384_000 }; const result = analyzeHttp400ForRetry( "This model's maximum context length is 1048576 tokens. However, you requested 1050237 tokens (666237 in the messages, 384000 in the completion).", @@ -80,7 +80,7 @@ describe("analyzeHttp400ForRetry โ€” context overflow", () => { assert.match(result.reason, /upstream context counts/i); }); - it("supports Responses-style max_output_tokens and formatted counts", () => { + void it("supports Responses-style max_output_tokens and formatted counts", () => { const body = { model: "gpt-test", max_output_tokens: 32_000 }; const result = analyzeHttp400ForRetry( "Maximum context length is 128,000 tokens; you requested 130,000 tokens (98,000 in the input, 32,000 in the output).", @@ -91,7 +91,7 @@ describe("analyzeHttp400ForRetry โ€” context overflow", () => { assert.equal(result.body?.max_output_tokens, 29_744); }); - it("patches the nested Google output budget", () => { + void it("patches the nested Google output budget", () => { const body = { model: "gemini-test", generationConfig: { maxOutputTokens: 32_000, temperature: 0.2 } }; const result = analyzeHttp400ForRetry( "Maximum context length is 128,000 tokens; you requested 130,000 tokens (98,000 in the input, 32,000 in the output).", @@ -102,7 +102,7 @@ describe("analyzeHttp400ForRetry โ€” context overflow", () => { assert.deepEqual(result.body?.generationConfig, { maxOutputTokens: 29_744, temperature: 0.2 }); }); - it("does not retry when reducing completion cannot fit the prompt", () => { + void it("does not retry when reducing completion cannot fit the prompt", () => { const result = analyzeHttp400ForRetry( "Maximum context length is 1,000 tokens. You requested 1,500 tokens (1,400 in the messages, 100 in the completion).", { model: "test", max_tokens: 100 }, @@ -112,26 +112,26 @@ describe("analyzeHttp400ForRetry โ€” context overflow", () => { }); }); -describe("isTransientServerError", () => { - it("flags 502/503/504 as transient", () => { +void describe("isTransientServerError", () => { + void it("flags 502/503/504 as transient", () => { assert.equal(isTransientServerError(502, "Bad Gateway"), true); assert.equal(isTransientServerError(503, "Service Unavailable"), true); assert.equal(isTransientServerError(504, "Gateway Timeout"), true); }); - it("flags a 500 whose body names Router.Unavailable as transient", () => { + void it("flags a 500 whose body names Router.Unavailable as transient", () => { assert.equal(isTransientServerError(500, '{"error":{"type":"Router.Unavailable"}}'), true); }); - it("treats 500 with unrelated body as permanent", () => { + void it("treats 500 with unrelated body as permanent", () => { assert.equal(isTransientServerError(500, "Internal Server Error"), false); }); - it("treats non-5xx statuses as permanent", () => { + void it("treats non-5xx statuses as permanent", () => { assert.equal(isTransientServerError(429, "Too Many Requests"), false); }); - it("matches Router.Unavailable case-insensitively", () => { + void it("matches Router.Unavailable case-insensitively", () => { assert.equal(isTransientServerError(500, "type: router.unavailable"), true); }); }); diff --git a/src/test/thinking.test.ts b/src/test/thinking.test.ts index 3838fd9..d547594 100644 --- a/src/test/thinking.test.ts +++ b/src/test/thinking.test.ts @@ -31,57 +31,57 @@ const defaultSettings: ThinkingSettings = { * FIX: buildThinkingPayload special-cases /^kimi-k2\.7/i to always emit * { type: "enabled", keep: "all" } regardless of the user's thinking setting. */ -describe("buildThinkingPayload โ€” kimi-k2.7-code (issue #25)", () => { - it("always emits { type: 'enabled', keep: 'all' } even when thinking.kimi is 'off'", () => { +void describe("buildThinkingPayload โ€” kimi-k2.7-code (issue #25)", () => { + void it("always emits { type: 'enabled', keep: 'all' } even when thinking.kimi is 'off'", () => { const payload = buildThinkingPayload("kimi-k2.7-code", { ...defaultSettings, kimi: "off" }); assert.deepEqual(payload, { thinking: { type: "enabled", keep: "all" } }); }); - it("emits { type: 'enabled', keep: 'all' } when thinking.kimi is 'on'", () => { + void it("emits { type: 'enabled', keep: 'all' } when thinking.kimi is 'on'", () => { const payload = buildThinkingPayload("kimi-k2.7-code", { ...defaultSettings, kimi: "on" }); assert.deepEqual(payload, { thinking: { type: "enabled", keep: "all" } }); }); - it("matches kimi-k2.7-code-highspeed variant too (same model, faster output)", () => { + void it("matches kimi-k2.7-code-highspeed variant too (same model, faster output)", () => { const payload = buildThinkingPayload("kimi-k2.7-code-highspeed", defaultSettings); assert.deepEqual(payload, { thinking: { type: "enabled", keep: "all" } }); }); }); -describe("buildThinkingPayload โ€” regression safety for other kimi models", () => { - it("kimi-k2.6 with kimi='off' emits { type: 'disabled' } (still accepts disabled)", () => { +void describe("buildThinkingPayload โ€” regression safety for other kimi models", () => { + void it("kimi-k2.6 with kimi='off' emits { type: 'disabled' } (still accepts disabled)", () => { const payload = buildThinkingPayload("kimi-k2.6", { ...defaultSettings, kimi: "off" }); assert.deepEqual(payload, { thinking: { type: "disabled" } }); }); - it("kimi-k2.6 with kimi='on' emits { type: 'enabled' }", () => { + void it("kimi-k2.6 with kimi='on' emits { type: 'enabled' }", () => { const payload = buildThinkingPayload("kimi-k2.6", { ...defaultSettings, kimi: "on" }); assert.deepEqual(payload, { thinking: { type: "enabled" } }); }); - it("kimi-k2.5 with kimi='off' emits { type: 'disabled' }", () => { + void it("kimi-k2.5 with kimi='off' emits { type: 'disabled' }", () => { const payload = buildThinkingPayload("kimi-k2.5", { ...defaultSettings, kimi: "off" }); assert.deepEqual(payload, { thinking: { type: "disabled" } }); }); }); -describe("buildThinkingPayload โ€” other families unchanged", () => { - it("deepseek with 'off' emits empty object (no reasoning_effort)", () => { +void describe("buildThinkingPayload โ€” other families unchanged", () => { + void it("deepseek with 'off' emits empty object (no reasoning_effort)", () => { const payload = buildThinkingPayload("deepseek-v4-pro", { ...defaultSettings, deepseek: "off" }); assert.deepEqual(payload, {}); }); - it("deepseek with 'high' emits reasoning_effort", () => { + void it("deepseek with 'high' emits reasoning_effort", () => { const payload = buildThinkingPayload("deepseek-v4-pro", { ...defaultSettings, deepseek: "high" }); assert.deepEqual(payload, { reasoning_effort: "high" }); }); - it("glm with 'off' emits { type: 'disabled' }", () => { + void it("glm with 'off' emits { type: 'disabled' }", () => { const payload = buildThinkingPayload("glm-5", { ...defaultSettings, glm: "off" }); assert.deepEqual(payload, { thinking: { type: "disabled" } }); }); - it("qwen with 'off' emits enable_thinking: false", () => { + void it("qwen with 'off' emits enable_thinking: false", () => { const payload = buildThinkingPayload("qwen3.6-plus", { ...defaultSettings, qwen: "off" }); assert.deepEqual(payload, { enable_thinking: false }); }); @@ -92,19 +92,20 @@ describe("buildThinkingPayload โ€” other families unchanged", () => { * users understand thinking cannot be disabled, rather than hiding the picker * or silently forcing "on". */ -describe("buildFamilyThinkingSchema โ€” kimi-k2.7-code picker", () => { - it("exposes a single 'on' option with 'Always On (K2.7)' label", () => { +void describe("buildFamilyThinkingSchema โ€” kimi-k2.7-code picker", () => { + void it("exposes a single 'on' option with 'Always On (K2.7)' label", () => { const schema = buildFamilyThinkingSchema("kimi-k2.7-code"); assert.ok(schema, "expected schema to be defined"); - const reasoningEffort = schema!.properties.reasoningEffort as Record; + const reasoningEffort = schema.properties.reasoningEffort as Record; assert.deepEqual(reasoningEffort.enum, ["on"]); assert.deepEqual(reasoningEffort.enumItemLabels, ["Always On (K2.7)"]); assert.equal(reasoningEffort.default, "on"); }); - it("mentions the Moonshot API constraint in the description", () => { + void it("mentions the Moonshot API constraint in the description", () => { const schema = buildFamilyThinkingSchema("kimi-k2.7-code"); - const reasoningEffort = schema!.properties.reasoningEffort as Record; + assert.ok(schema, "expected schema to be defined"); + const reasoningEffort = schema.properties.reasoningEffort as Record; const descriptions = reasoningEffort.enumDescriptions as string[]; assert.ok( descriptions.some((d) => d.includes("Moonshot API constraint")), @@ -113,18 +114,18 @@ describe("buildFamilyThinkingSchema โ€” kimi-k2.7-code picker", () => { }); }); -describe("buildFamilyThinkingSchema โ€” other kimi models keep off/on", () => { - it("kimi-k2.6 exposes both 'off' and 'on'", () => { +void describe("buildFamilyThinkingSchema โ€” other kimi models keep off/on", () => { + void it("kimi-k2.6 exposes both 'off' and 'on'", () => { const schema = buildFamilyThinkingSchema("kimi-k2.6"); assert.ok(schema); - const reasoningEffort = schema!.properties.reasoningEffort as Record; + const reasoningEffort = schema.properties.reasoningEffort as Record; assert.deepEqual(reasoningEffort.enum, ["off", "on"]); }); - it("kimi-k2.5 exposes both 'off' and 'on'", () => { + void it("kimi-k2.5 exposes both 'off' and 'on'", () => { const schema = buildFamilyThinkingSchema("kimi-k2.5"); assert.ok(schema); - const reasoningEffort = schema!.properties.reasoningEffort as Record; + const reasoningEffort = schema.properties.reasoningEffort as Record; assert.deepEqual(reasoningEffort.enum, ["off", "on"]); }); }); @@ -133,36 +134,36 @@ describe("buildFamilyThinkingSchema โ€” other kimi models keep off/on", () => { * Override tests: even if VS Code caches a stale picker value (e.g. "off"), * applyRequestThinkingOverride must force kimi="on" for K2.7-code. */ -describe("applyRequestThinkingOverride โ€” kimi-k2.7-code defensive force-on", () => { - it("forces kimi='on' even when override requests 'off'", () => { +void describe("applyRequestThinkingOverride โ€” kimi-k2.7-code defensive force-on", () => { + void it("forces kimi='on' even when override requests 'off'", () => { const result = applyRequestThinkingOverride("kimi-k2.7-code", defaultSettings, { reasoningEffort: "off", }); assert.equal(result.kimi, "on"); }); - it("forces kimi='on' even when override requests 'on' (no-op but explicit)", () => { + void it("forces kimi='on' even when override requests 'on' (no-op but explicit)", () => { const result = applyRequestThinkingOverride("kimi-k2.7-code", defaultSettings, { reasoningEffort: "on", }); assert.equal(result.kimi, "on"); }); - it("forces kimi='on' when override is empty (defensive against stale cache)", () => { + void it("forces kimi='on' when override is empty (defensive against stale cache)", () => { const result = applyRequestThinkingOverride("kimi-k2.7-code", defaultSettings, {}); assert.equal(result.kimi, "on"); }); }); -describe("applyRequestThinkingOverride โ€” other kimi models respect override", () => { - it("kimi-k2.6 respects 'off' override", () => { +void describe("applyRequestThinkingOverride โ€” other kimi models respect override", () => { + void it("kimi-k2.6 respects 'off' override", () => { const result = applyRequestThinkingOverride("kimi-k2.6", defaultSettings, { reasoningEffort: "off", }); assert.equal(result.kimi, "off"); }); - it("kimi-k2.6 respects 'on' override", () => { + void it("kimi-k2.6 respects 'on' override", () => { const result = applyRequestThinkingOverride("kimi-k2.6", defaultSettings, { reasoningEffort: "on", }); @@ -170,16 +171,16 @@ describe("applyRequestThinkingOverride โ€” other kimi models respect override", }); }); -describe("thinkingFamily โ€” detection", () => { - it("classifies kimi-k2.7-code as 'kimi'", () => { +void describe("thinkingFamily โ€” detection", () => { + void it("classifies kimi-k2.7-code as 'kimi'", () => { assert.equal(thinkingFamily("kimi-k2.7-code"), "kimi"); }); - it("classifies kimi-k2.6 as 'kimi'", () => { + void it("classifies kimi-k2.6 as 'kimi'", () => { assert.equal(thinkingFamily("kimi-k2.6"), "kimi"); }); - it("returns null for unknown prefixes", () => { + void it("returns null for unknown prefixes", () => { assert.equal(thinkingFamily("unknown-model"), null); }); }); @@ -195,35 +196,35 @@ describe("thinkingFamily โ€” detection", () => { * The new "high"/"max" values must map to thinking enabled in the payload, * and the per-model picker should expose only the relevant options. */ -describe("buildThinkingPayload โ€” GLM with effort values (issue #61)", () => { - it("glm-5.2 with glm='high' emits reasoning_effort: 'high'", () => { +void describe("buildThinkingPayload โ€” GLM with effort values (issue #61)", () => { + void it("glm-5.2 with glm='high' emits reasoning_effort: 'high'", () => { const payload = buildThinkingPayload("glm-5.2", { ...defaultSettings, glm: "high" }); assert.deepEqual(payload, { reasoning_effort: "high" }); }); - it("glm-5.2 with glm='max' emits reasoning_effort: 'max'", () => { + void it("glm-5.2 with glm='max' emits reasoning_effort: 'max'", () => { const payload = buildThinkingPayload("glm-5.2", { ...defaultSettings, glm: "max" }); assert.deepEqual(payload, { reasoning_effort: "max" }); }); - it("glm-5.2 with glm='off' emits thinking disabled", () => { + void it("glm-5.2 with glm='off' emits thinking disabled", () => { const payload = buildThinkingPayload("glm-5.2", { ...defaultSettings, glm: "off" }); assert.deepEqual(payload, { thinking: { type: "disabled" } }); }); - it("glm-5 (toggle-only) with glm='high' sends reasoning_effort (gateway resolves)", () => { + void it("glm-5 (toggle-only) with glm='high' sends reasoning_effort (gateway resolves)", () => { const payload = buildThinkingPayload("glm-5", { ...defaultSettings, glm: "high" }); assert.deepEqual(payload, { reasoning_effort: "high" }); }); - it("glm-5 (toggle-only) with glm='off' emits thinking disabled", () => { + void it("glm-5 (toggle-only) with glm='off' emits thinking disabled", () => { const payload = buildThinkingPayload("glm-5", { ...defaultSettings, glm: "off" }); assert.deepEqual(payload, { thinking: { type: "disabled" } }); }); }); -describe("buildFamilyThinkingSchema โ€” GLM 5.2 with reasoning_options metadata", () => { - it("exposes off, high, max when reasoning_options has effort values", () => { +void describe("buildFamilyThinkingSchema โ€” GLM 5.2 with reasoning_options metadata", () => { + void it("exposes off, high, max when reasoning_options has effort values", () => { const metadata = { reasoning: true, reasoningOptions: [{ type: "effort" as const, values: ["high", "max"] }], @@ -237,44 +238,44 @@ describe("buildFamilyThinkingSchema โ€” GLM 5.2 with reasoning_options metadata" }; const schema = buildFamilyThinkingSchema("glm-5.2", metadata); assert.ok(schema, "expected schema to be defined"); - const reasoningEffort = schema!.properties.reasoningEffort as Record; + const reasoningEffort = schema.properties.reasoningEffort as Record; assert.deepEqual(reasoningEffort.enum, ["off", "high", "max"]); assert.deepEqual(reasoningEffort.enumItemLabels, ["Off", "High", "Max"]); assert.equal(reasoningEffort.default, "off"); }); - it("falls back to off/high/max for GLM models without reasoning_options (no invalid 'on')", () => { + void it("falls back to off/high/max for GLM models without reasoning_options (no invalid 'on')", () => { const schema = buildFamilyThinkingSchema("glm-5"); assert.ok(schema, "expected schema to be defined"); - const reasoningEffort = schema!.properties.reasoningEffort as Record; + const reasoningEffort = schema.properties.reasoningEffort as Record; assert.deepEqual(reasoningEffort.enum, ["off", "high", "max"]); assert.deepEqual(reasoningEffort.enumItemLabels, ["Off", "High", "Max"]); }); }); -describe("applyRequestThinkingOverride โ€” GLM with effort values (issue #61)", () => { - it("accepts 'high' override for glm-5.2", () => { +void describe("applyRequestThinkingOverride โ€” GLM with effort values (issue #61)", () => { + void it("accepts 'high' override for glm-5.2", () => { const result = applyRequestThinkingOverride("glm-5.2", defaultSettings, { reasoningEffort: "high", }); assert.equal(result.glm, "high"); }); - it("accepts 'max' override for glm-5.2", () => { + void it("accepts 'max' override for glm-5.2", () => { const result = applyRequestThinkingOverride("glm-5.2", defaultSettings, { reasoningEffort: "max", }); assert.equal(result.glm, "max"); }); - it("accepts 'off' override for glm-5.2", () => { + void it("accepts 'off' override for glm-5.2", () => { const result = applyRequestThinkingOverride("glm-5.2", defaultSettings, { reasoningEffort: "off", }); assert.equal(result.glm, "off"); }); - it("rejects invalid values like 'on' and 'medium' for glm", () => { + void it("rejects invalid values like 'on' and 'medium' for glm", () => { const resultOn = applyRequestThinkingOverride("glm-5.2", defaultSettings, { reasoningEffort: "on", }); diff --git a/src/test/tokenEstimate.test.ts b/src/test/tokenEstimate.test.ts index 0f01668..f83394c 100644 --- a/src/test/tokenEstimate.test.ts +++ b/src/test/tokenEstimate.test.ts @@ -2,13 +2,13 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { estimatePromptTokenCount, estimateTokenCount } from "../tokenEstimate.js"; -describe("token estimates", () => { - it("returns zero for empty content", () => { +void describe("token estimates", () => { + void it("returns zero for empty content", () => { assert.equal(estimateTokenCount(""), 0); assert.equal(estimateTokenCount(" \n\t"), 0); }); - it("includes tool schemas in the prompt estimate", () => { + void it("includes tool schemas in the prompt estimate", () => { const messages = [{ role: "user", content: "inspect the workspace" }]; const withoutTools = estimatePromptTokenCount(messages); const withTools = estimatePromptTokenCount(messages, [ diff --git a/src/test/toolCallAccumulator.test.ts b/src/test/toolCallAccumulator.test.ts index 0fc99d9..167f8ce 100644 --- a/src/test/toolCallAccumulator.test.ts +++ b/src/test/toolCallAccumulator.test.ts @@ -39,8 +39,8 @@ const argsChunk1 = deltaChunk([{ index: 0, function: { arguments: '{"query":' } const argsChunk2 = deltaChunk([{ index: 0, function: { arguments: '"search"}' } }]); -describe("ToolCallAccumulator โ€” no premature flush on intermediate chunks (#98)", () => { - it("does not flush while finish_reason is null, even with pending tool calls", () => { +void describe("ToolCallAccumulator โ€” no premature flush on intermediate chunks (#98)", () => { + void it("does not flush while finish_reason is null, even with pending tool calls", () => { const acc = new ToolCallAccumulator(); acc.collect(nameChunk); acc.collect(argsChunk1); @@ -54,7 +54,7 @@ describe("ToolCallAccumulator โ€” no premature flush on intermediate chunks (#98 assert.equal(ToolCallAccumulator.shouldFlushOnFinishReason(undefined), false); }); - it("flushes exactly ONE complete tool call when finish_reason is 'tool_calls'", () => { + void it("flushes exactly ONE complete tool call when finish_reason is 'tool_calls'", () => { const acc = new ToolCallAccumulator(); acc.collect(nameChunk); acc.collect(argsChunk1); @@ -74,7 +74,7 @@ describe("ToolCallAccumulator โ€” no premature flush on intermediate chunks (#98 assert.deepEqual(acc.flush(), []); }); - it("is a no-op when nothing was collected", () => { + void it("is a no-op when nothing was collected", () => { const acc = new ToolCallAccumulator(); assert.deepEqual(acc.flush(), []); assert.deepEqual(acc.flushRemainingToolCalls(), []); @@ -82,8 +82,8 @@ describe("ToolCallAccumulator โ€” no premature flush on intermediate chunks (#98 }); }); -describe("ToolCallAccumulator โ€” end-of-stream flush for gateways omitting finish_reason (#93)", () => { - it("flushRemainingToolCalls emits the complete call when the gateway never sends 'tool_calls'", () => { +void describe("ToolCallAccumulator โ€” end-of-stream flush for gateways omitting finish_reason (#93)", () => { + void it("flushRemainingToolCalls emits the complete call when the gateway never sends 'tool_calls'", () => { const acc = new ToolCallAccumulator(); acc.collect(nameChunk); acc.collect(argsChunk1); @@ -99,7 +99,7 @@ describe("ToolCallAccumulator โ€” end-of-stream flush for gateways omitting fini assert.equal(acc.size, 0); }); - it("flushRemainingToolCalls is a no-op after a normal finish_reason flush", () => { + void it("flushRemainingToolCalls is a no-op after a normal finish_reason flush", () => { const acc = new ToolCallAccumulator(); acc.collect(nameChunk); acc.collect(argsChunk1); @@ -109,8 +109,8 @@ describe("ToolCallAccumulator โ€” end-of-stream flush for gateways omitting fini }); }); -describe("ToolCallAccumulator โ€” delta handling edge cases", () => { - it("ignores non-array and non-record deltas", () => { +void describe("ToolCallAccumulator โ€” delta handling edge cases", () => { + void it("ignores non-array and non-record deltas", () => { const acc = new ToolCallAccumulator(); acc.collect(undefined); acc.collect("not an array"); @@ -118,7 +118,7 @@ describe("ToolCallAccumulator โ€” delta handling edge cases", () => { assert.equal(acc.size, 0); }); - it("filters out arguments-only deltas that never supplied a name", () => { + void it("filters out arguments-only deltas that never supplied a name", () => { const acc = new ToolCallAccumulator(); acc.collect([{ index: 0, function: { arguments: '{"a":1}' } }]); assert.equal(acc.size, 1); @@ -127,7 +127,7 @@ describe("ToolCallAccumulator โ€” delta handling edge cases", () => { assert.deepEqual(flushed, []); }); - it("accumulates multiple tool calls independently by index", () => { + void it("accumulates multiple tool calls independently by index", () => { const acc = new ToolCallAccumulator(); acc.collect([ { index: 0, id: "a", function: { name: "read_file", arguments: '{"path":' } }, @@ -146,7 +146,7 @@ describe("ToolCallAccumulator โ€” delta handling edge cases", () => { assert.deepEqual(flushed[1].input, { query: "foo" }); }); - it("appends name fragments (name split across chunks)", () => { + void it("appends name fragments (name split across chunks)", () => { const acc = new ToolCallAccumulator(); acc.collect([{ index: 0, function: { name: "read_" } }]); acc.collect([{ index: 0, function: { name: "file" } }]); @@ -155,28 +155,28 @@ describe("ToolCallAccumulator โ€” delta handling edge cases", () => { }); }); -describe("parseToolInput", () => { - it("returns {} for empty or whitespace-only input", () => { +void describe("parseToolInput", () => { + void it("returns {} for empty or whitespace-only input", () => { assert.deepEqual(parseToolInput(""), {}); assert.deepEqual(parseToolInput(" "), {}); }); - it("returns {} for partial / invalid JSON", () => { + void it("returns {} for partial / invalid JSON", () => { assert.deepEqual(parseToolInput('{"a":'), {}); assert.deepEqual(parseToolInput("not json"), {}); }); - it("parses valid JSON objects", () => { + void it("parses valid JSON objects", () => { assert.deepEqual(parseToolInput('{"a":1}'), { a: 1 }); assert.deepEqual(parseToolInput("{}"), {}); }); - it("returns {} for non-object JSON scalars (strings, numbers)", () => { + void it("returns {} for non-object JSON scalars (strings, numbers)", () => { assert.deepEqual(parseToolInput('"str"'), {}); assert.deepEqual(parseToolInput("42"), {}); }); - it("passes through JSON arrays (isRecord treats arrays as objects โ€” original semantics)", () => { + void it("passes through JSON arrays (isRecord treats arrays as objects โ€” original semantics)", () => { assert.deepEqual(parseToolInput("[1,2]"), [1, 2]); }); }); diff --git a/src/test/usageProfile.test.ts b/src/test/usageProfile.test.ts index d65a9ac..4fdd6e0 100644 --- a/src/test/usageProfile.test.ts +++ b/src/test/usageProfile.test.ts @@ -45,31 +45,31 @@ before(async () => { mod = await import("../usageProfile.js"); }); -describe("keyFingerprint", () => { - it("returns 'legacy' for empty input", () => { +void describe("keyFingerprint", () => { + void it("returns 'legacy' for empty input", () => { assert.equal(mod.keyFingerprint(""), mod.LEGACY_FINGERPRINT); }); - it("takes 8 leading + 8 trailing chars", () => { + void it("takes 8 leading + 8 trailing chars", () => { assert.equal(mod.keyFingerprint("sk-90UzXXab-XXXXXXXX-cdWToa"), "sk-90UzX-X-cdWToa"); }); - it("is stable (same key, same fingerprint)", () => { + void it("is stable (same key, same fingerprint)", () => { const k = "sk-aaaabbbb-cccccccc-dddd-eeee-ffff-12345678"; assert.equal(mod.keyFingerprint(k), mod.keyFingerprint(k)); }); }); -describe("profile registry", () => { - it("returns empty when no profiles stored", () => { +void describe("profile registry", () => { + void it("returns empty when no profiles stored", () => { assert.deepEqual(mod.readProfiles(createMockContext()), []); }); - it("round-trips profiles through writeProfiles/readProfiles", async () => { + void it("round-trips profiles through writeProfiles/readProfiles", async () => { const ctx = createMockContext(); const p = { fingerprint: "fp1", label: "Profile 1", lastSeenAt: Date.now(), isLegacy: false }; await mod.writeProfiles(ctx, [p]); assert.equal(mod.readProfiles(ctx).length, 1); assert.equal(mod.readProfiles(ctx)[0].fingerprint, "fp1"); }); - it("findProfile returns matching profile or undefined", () => { + void it("findProfile returns matching profile or undefined", () => { const profiles = [ { fingerprint: "a", label: "A", lastSeenAt: 0, isLegacy: false }, { fingerprint: "b", label: "B", lastSeenAt: 0, isLegacy: false }, @@ -77,7 +77,7 @@ describe("profile registry", () => { assert.equal(mod.findProfile(profiles, "a")?.label, "A"); assert.equal(mod.findProfile(profiles, "missing"), undefined); }); - it("renameProfile updates label", async () => { + void it("renameProfile updates label", async () => { const ctx = createMockContext(); const p = { fingerprint: "fp1", label: "Profile 1", lastSeenAt: Date.now(), isLegacy: false }; await mod.writeProfiles(ctx, [p]); @@ -86,11 +86,11 @@ describe("profile registry", () => { }); }); -describe("active profile", () => { - it("defaults to legacy when not stored", () => { +void describe("active profile", () => { + void it("defaults to legacy when not stored", () => { assert.equal(mod.readActiveProfile(createMockContext()), mod.LEGACY_FINGERPRINT); }); - it("round-trips through writeActiveProfile", async () => { + void it("round-trips through writeActiveProfile", async () => { const ctx = createMockContext(); await mod.writeActiveProfile(ctx, "my-fp"); assert.equal(mod.readActiveProfile(ctx), "my-fp"); diff --git a/src/test/visionProxy.test.ts b/src/test/visionProxy.test.ts index 339b711..d96e7c1 100644 --- a/src/test/visionProxy.test.ts +++ b/src/test/visionProxy.test.ts @@ -15,38 +15,38 @@ import { buildStableModelCapabilities } from "../modelCapabilities"; * model natively supports images), NOT the enhanced capabilities. */ -type ProxyConditionInput = { +interface ProxyConditionInput { hasImageInput: boolean; actuallySupportsVision: boolean; visionProxyModelId: string; -}; +} function shouldProxy({ hasImageInput, actuallySupportsVision, visionProxyModelId }: ProxyConditionInput): boolean { return Boolean(hasImageInput && !actuallySupportsVision && visionProxyModelId); } -describe("vision proxy condition (shouldProxy)", () => { - it("enters proxy when text-only model receives images with proxy configured", () => { +void describe("vision proxy condition (shouldProxy)", () => { + void it("enters proxy when text-only model receives images with proxy configured", () => { assert.ok(shouldProxy({ hasImageInput: true, actuallySupportsVision: false, visionProxyModelId: "gpt-5.5" })); }); - it("skips proxy when no images present", () => { + void it("skips proxy when no images present", () => { assert.ok(!shouldProxy({ hasImageInput: false, actuallySupportsVision: false, visionProxyModelId: "gpt-5.5" })); }); - it("skips proxy when model natively supports vision", () => { + void it("skips proxy when model natively supports vision", () => { assert.ok(!shouldProxy({ hasImageInput: true, actuallySupportsVision: true, visionProxyModelId: "gpt-5.5" })); }); - it("skips proxy when no vision model is configured (empty string)", () => { + void it("skips proxy when no vision model is configured (empty string)", () => { assert.ok(!shouldProxy({ hasImageInput: true, actuallySupportsVision: false, visionProxyModelId: "" })); }); - it("skips proxy when all conditions are false", () => { + void it("skips proxy when all conditions are false", () => { assert.ok(!shouldProxy({ hasImageInput: false, actuallySupportsVision: true, visionProxyModelId: "" })); }); - it("cached supportsVision (actuallySupportsVision) prevents circular regression", () => { + void it("cached supportsVision (actuallySupportsVision) prevents circular regression", () => { // This is the fix for #74: even if modelCapabilities overrides // metadata.supportsVision to true (because proxy is enabled), // the CACHED value (actuallySupportsVision) stays false for @@ -61,27 +61,27 @@ describe("vision proxy condition (shouldProxy)", () => { }); }); -describe("modelCapabilities vision proxy flag", () => { +void describe("modelCapabilities vision proxy flag", () => { // modelCapabilities() returns imageInput: true when: // metadata.supportsVision (native) OR isVisionProxyEnabled() // This tells VS Code NOT to strip images from requests. - it("returns imageInput: true when proxy is enabled on text-only models", () => { + void it("returns imageInput: true when proxy is enabled on text-only models", () => { const capabilities = buildStableModelCapabilities(true); assert.equal(capabilities.imageInput, true); }); - it("returns imageInput: true when model natively supports vision", () => { + void it("returns imageInput: true when model natively supports vision", () => { const capabilities = buildStableModelCapabilities(true); assert.equal(capabilities.imageInput, true); }); - it("returns imageInput: false only when no vision support and no proxy", () => { + void it("returns imageInput: false only when no vision support and no proxy", () => { const capabilities = buildStableModelCapabilities(false); assert.equal(capabilities.imageInput, false); }); - it("keeps tool calling enabled without proposal-gated edit tool hints", () => { + void it("keeps tool calling enabled without proposal-gated edit tool hints", () => { const capabilities = buildStableModelCapabilities(true); assert.equal(capabilities.toolCalling, true); diff --git a/src/thinking.ts b/src/thinking.ts index 07a0aa1..367dfe0 100644 --- a/src/thinking.ts +++ b/src/thinking.ts @@ -65,8 +65,8 @@ export function buildFamilyThinkingSchema( if (opts && opts.length > 0) { // Collect unique effort values across all effort-type options const effortValues = opts - .filter((o) => o.type === "effort" && Array.isArray(o.values) && o.values.length > 0) - .flatMap((o) => o.values!) + .filter((o): o is { type: "effort"; values: string[] } => o.type === "effort" && Array.isArray(o.values) && o.values.length > 0) + .flatMap((o) => o.values) .filter((v, i, a) => a.indexOf(v) === i); // Check if a toggle-type option exists @@ -471,7 +471,7 @@ export function buildThinkingPayload(modelId: string, thinking: ThinkingSettings if (thinking.mimo === "off") { return {}; } - const mimoBudgetMap: Record = { + const mimoBudgetMap: Record = { low: 8192, medium: 16384, high: 32768, diff --git a/src/toolCallAccumulator.ts b/src/toolCallAccumulator.ts index 8a904d0..941053c 100644 --- a/src/toolCallAccumulator.ts +++ b/src/toolCallAccumulator.ts @@ -31,7 +31,7 @@ export function parseToolInput(value: string): object { } try { - const parsed = JSON.parse(value); + const parsed: unknown = JSON.parse(value); return isRecord(parsed) ? parsed : {}; } catch { return {}; diff --git a/src/usage.ts b/src/usage.ts index 89917d9..1c181a7 100644 --- a/src/usage.ts +++ b/src/usage.ts @@ -43,7 +43,7 @@ export function formatCompactTokenCount(value: number | undefined): string { } if (value >= 10000) { - return `${Math.round(value / 1000)}k`; + return `${String(Math.round(value / 1000))}k`; } if (value >= 1000) { @@ -84,13 +84,13 @@ export function formatUsageStatusBarTooltip(providerDisplayName: string, modelId const lines = [ `Provider: ${providerDisplayName}`, `Model: ${modelId}`, - `Prompt: ${normalized.promptTokens ?? "n/a"} tokens`, - `Output: ${normalized.completionTokens ?? "n/a"} tokens`, - `Total: ${normalized.totalTokens ?? "n/a"} tokens`, + `Prompt: ${String(normalized.promptTokens ?? "n/a")} tokens`, + `Output: ${String(normalized.completionTokens ?? "n/a")} tokens`, + `Total: ${String(normalized.totalTokens ?? "n/a")} tokens`, ]; if (normalized.cachedTokens !== undefined) { - lines.push(`Cached input: ${normalized.cachedTokens} tokens`); + lines.push(`Cached input: ${String(normalized.cachedTokens)} tokens`); } const ratio = formatCacheHitRatio(normalized); @@ -112,13 +112,13 @@ export function formatUsageLogLine(usage: UsageSnapshot): string | undefined { } const parts = [ - `prompt=${normalized.promptTokens ?? "n/a"}`, - `completion=${normalized.completionTokens ?? "n/a"}`, - `total=${normalized.totalTokens ?? "n/a"}`, + `prompt=${String(normalized.promptTokens ?? "n/a")}`, + `completion=${String(normalized.completionTokens ?? "n/a")}`, + `total=${String(normalized.totalTokens ?? "n/a")}`, ]; if (normalized.cachedTokens !== undefined) { - parts.push(`cached=${normalized.cachedTokens}`); + parts.push(`cached=${String(normalized.cachedTokens)}`); } const ratio = formatCacheHitRatio(normalized); diff --git a/src/usageProfile.ts b/src/usageProfile.ts index 8e790fb..8d5c431 100644 --- a/src/usageProfile.ts +++ b/src/usageProfile.ts @@ -44,9 +44,12 @@ export async function writeMigratedTo(context: vscode.ExtensionContext, fingerpr } export function readProfiles(context: vscode.ExtensionContext): UsageProfile[] { - const stored = context.globalState.get(PROFILES_REGISTRY_KEY, []); + const stored = context.globalState.get<(UsageProfile | null)[]>(PROFILES_REGISTRY_KEY, []); if (!Array.isArray(stored)) return []; - return stored.filter((p) => p && typeof p.fingerprint === "string" && typeof p.label === "string" && typeof p.lastSeenAt === "number"); + return stored.filter( + (p): p is UsageProfile => + p !== null && typeof p.fingerprint === "string" && typeof p.label === "string" && typeof p.lastSeenAt === "number", + ); } export async function writeProfiles(context: vscode.ExtensionContext, profiles: UsageProfile[]): Promise { @@ -78,7 +81,7 @@ export async function getOrCreateProfile(context: vscode.ExtensionContext, finge const nextNumber = profiles.length + 1; const profile: UsageProfile = { fingerprint, - label: `Profile ${nextNumber}`, + label: `Profile ${String(nextNumber)}`, lastSeenAt: Date.now(), }; profiles.push(profile); diff --git a/src/vscode.proposed.chatProvider.d.ts b/src/vscode.proposed.chatProvider.d.ts index a438945..e7781bb 100644 --- a/src/vscode.proposed.chatProvider.d.ts +++ b/src/vscode.proposed.chatProvider.d.ts @@ -21,9 +21,7 @@ declare module "vscode" { * in the user's language models configuration file, validated against the model's * {@linkcode LanguageModelChatInformation.configurationSchema configurationSchema}. */ - readonly modelConfiguration?: { - readonly [key: string]: any; - }; + readonly modelConfiguration?: Readonly>; } /** @@ -84,7 +82,7 @@ declare module "vscode" { * Whether or not this will be selected by default in the model picker * NOT BEING FINALIZED */ - readonly isDefault?: boolean | { [K in ChatLocation]?: boolean }; + readonly isDefault?: boolean | Partial>; /** * Whether or not the model will show up in the model picker immediately upon being made known via {@linkcode LanguageModelChatProvider.provideLanguageModelChatInformation}. @@ -139,16 +137,19 @@ declare module "vscode" { /** * A [JSON Schema](https://json-schema.org) describing configuration options for a language model. */ - export type LanguageModelConfigurationSchema = { + export interface LanguageModelConfigurationSchema { readonly type?: string; - readonly properties?: { - readonly [key: string]: Record & { - readonly enumItemLabels?: string[]; - readonly enumDescriptions?: string[]; - readonly group?: string; - }; - }; - }; + readonly properties?: Readonly< + Record< + string, + Record & { + readonly enumItemLabels?: string[]; + readonly enumDescriptions?: string[]; + readonly group?: string; + } + > + >; + } export interface LanguageModelChatProvider { provideLanguageModelChatInformation(options: PrepareLanguageModelChatModelOptions, token: CancellationToken): ProviderResult; @@ -162,12 +163,10 @@ declare module "vscode" { } export interface PrepareLanguageModelChatModelOptions { - readonly configuration?: { - readonly [key: string]: any; - }; + readonly configuration?: Readonly>; } export interface ChatRequest { - readonly modelConfiguration?: { readonly [key: string]: any }; + readonly modelConfiguration?: Readonly>; } } diff --git a/src/vscode.proposed.languageModelThinkingPart.d.ts b/src/vscode.proposed.languageModelThinkingPart.d.ts index 9ace275..ad31c9e 100644 --- a/src/vscode.proposed.languageModelThinkingPart.d.ts +++ b/src/vscode.proposed.languageModelThinkingPart.d.ts @@ -46,7 +46,7 @@ declare module "vscode" { /** * Optional metadata associated with this thinking sequence. */ - metadata?: { readonly [key: string]: any }; + metadata?: Readonly>; /** * Construct a thinking part with the given content. @@ -54,6 +54,6 @@ declare module "vscode" { * @param id Optional unique identifier for this thinking sequence. * @param metadata Optional metadata associated with this thinking sequence. */ - constructor(value: string | string[], id?: string, metadata?: { readonly [key: string]: any }); + constructor(value: string | string[], id?: string, metadata?: Readonly>); } } diff --git a/tsconfig.check.json b/tsconfig.check.json new file mode 100644 index 0000000..5e5f717 --- /dev/null +++ b/tsconfig.check.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "." + }, + "include": ["src", "scripts"], + "exclude": ["node_modules", ".vscode-test", "out"] +} From fb5ca6ae74e492a9abd43e180d295c4291b12a2e Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Sat, 8 Aug 2026 22:05:53 +0500 Subject: [PATCH 02/18] fix(hooks): resolve node/npm/npx from nvm when PATH is stripped Some commit UIs (IDE/GUI) run hooks with a PATH that excludes version-manager bin dirs, causing 'npx: not found' and blocking every commit. The pre-commit hook now auto-resolves nvm's node bin dir and prepends it to PATH. --- .husky/pre-commit | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.husky/pre-commit b/.husky/pre-commit index dc5906c..6b6cf57 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,18 @@ #!/usr/bin/env sh +# Some commit UIs (IDE/GUI) run hooks with a stripped-down PATH that excludes +# version-manager bin dirs (e.g. nvm), so node/npm/npx can be missing here. +# Resolve them before running anything so the gate never silently skips. +if ! command -v npx >/dev/null 2>&1; then + # Prefer the version nvm has selected, then the highest installed one. + node_bin="${NVM_BIN:-}" + if [ -z "$node_bin" ] || [ ! -x "$node_bin/npx" ]; then + node_bin="$(find "$HOME/.nvm/versions/node" -maxdepth 3 -type d -name bin 2>/dev/null | sort -V | tail -n 1)" + fi + if [ -n "$node_bin" ] && [ -x "$node_bin/npx" ]; then + export PATH="$node_bin:$PATH" + fi +fi + # Format ALL changed files (code + non-code) โ€” prettier on everything it # supports, eslint --fix on JS/TS, markdownlint --fix on Markdown, and lint # of husky scripts. Fast path: only staged files. From b4c3a338d06b5c0416ce01d382cf550e18aa79e8 Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Sun, 9 Aug 2026 08:09:33 +0500 Subject: [PATCH 03/18] fix(hooks): lint before format so the formatter can't mask errors Reorders the pre-commit hook to run the full zero-tolerance lint gate before lint-staged formatting, and adds set -e so a lint failure blocks the commit instead of continuing into the formatter (which could auto-fix and hide it). --- .husky/pre-commit | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 6b6cf57..60f5bf9 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,9 +1,10 @@ #!/usr/bin/env sh -# Some commit UIs (IDE/GUI) run hooks with a stripped-down PATH that excludes -# version-manager bin dirs (e.g. nvm), so node/npm/npx can be missing here. -# Resolve them before running anything so the gate never silently skips. +# Fail fast: a failing step aborts the commit so the formatter can't mask a lint error. +set -e + +# Some commit UIs run hooks with a stripped PATH that omits nvm's bin dir, so +# npx/npm can be missing here. Resolve them so the gate never silently skips. if ! command -v npx >/dev/null 2>&1; then - # Prefer the version nvm has selected, then the highest installed one. node_bin="${NVM_BIN:-}" if [ -z "$node_bin" ] || [ ! -x "$node_bin/npx" ]; then node_bin="$(find "$HOME/.nvm/versions/node" -maxdepth 3 -type d -name bin 2>/dev/null | sort -V | tail -n 1)" @@ -13,13 +14,10 @@ if ! command -v npx >/dev/null 2>&1; then fi fi -# Format ALL changed files (code + non-code) โ€” prettier on everything it -# supports, eslint --fix on JS/TS, markdownlint --fix on Markdown, and lint -# of husky scripts. Fast path: only staged files. -npx lint-staged - -# Zero-tolerance gate: full-repo lint (eslint, markdownlint, shellcheck, -# editorconfig-checker, tsc type-check incl. scripts/) plus prettier format -# check. A commit is BLOCKED if ANY file in the codebase has any error, -# warning, info-level diagnostic, type error, or formatting drift. +# Zero-tolerance gate FIRST, on raw content: block the commit if any file has +# any error, warning, info diagnostic, type error, or formatting drift. +# Linting before formatting ensures the linter sees the file as written. npm run lint + +# Format changed files (code + non-code) after linting passes. +npx lint-staged From a4ce3af545d66e01cdd5d6f854281bbea077e5fe Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Sun, 9 Aug 2026 09:37:03 +0500 Subject: [PATCH 04/18] chore(scripts): use .js lint/format with styled alphabetical output --- eslint.config.mjs | 2 +- package-lock.json | 1 + package.json | 5 ++-- scripts/format.js | 46 ++++++++++++++++++++++++++++++ scripts/lint.js | 66 ++++++++++++++++++++++++++++++++++++++++++++ scripts/package.json | 3 ++ 6 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 scripts/format.js create mode 100644 scripts/lint.js create mode 100644 scripts/package.json diff --git a/eslint.config.mjs b/eslint.config.mjs index 9ae9fb2..15f9560 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -24,7 +24,7 @@ const gitignore = readFileSync(new URL(".gitignore", import.meta.url), "utf8") // Files not covered by tsconfig (which only includes src/), type-checked via // the default project so strictTypeChecked rules still apply to them. -const nonProjectFiles = ["eslint.config.mjs", "scripts/*.mjs", "scripts/*.mts"]; +const nonProjectFiles = ["eslint.config.mjs", "scripts/*.js", "scripts/*.mjs", "scripts/*.mts"]; // The typescript-eslint `config()` helper is deprecated; ESLint core now // provides `defineConfig()`. We replicate the helper's `extends` expansion diff --git a/package-lock.json b/package-lock.json index f3f0cec..8c02b83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "husky": "^9.1.7", "lint-staged": "^17.3.0", "markdownlint-cli2": "^0.23.2", + "picocolors": "^1.1.1", "prettier": "^3.9.6", "shellcheck": "^4.1.0", "typescript": "^6.0.3", diff --git a/package.json b/package.json index 9374acc..0d9e07b 100644 --- a/package.json +++ b/package.json @@ -330,14 +330,14 @@ ] }, "scripts": { - "lint": "npm run lint:js && npm run lint:md && npm run lint:sh && npm run lint:hygiene && npm run lint:ts && npm run format:check", + "lint": "node scripts/lint.js", "lint:js": "eslint . --max-warnings 0", "lint:fix": "eslint . --fix --max-warnings 0", "lint:md": "markdownlint-cli2 --config .markdownlint.json \"**/*.md\" \"#node_modules\"", "lint:sh": "shellcheck .husky/pre-commit", "lint:hygiene": "editorconfig-checker", "lint:ts": "tsc -p tsconfig.check.json", - "format": "npm run format:prettier && npm run format:js", + "format": "node scripts/format.js", "format:js": "eslint . --fix --max-warnings 0", "format:prettier": "npx prettier --write .", "format:check": "npx prettier --check .", @@ -369,6 +369,7 @@ "husky": "^9.1.7", "lint-staged": "^17.3.0", "markdownlint-cli2": "^0.23.2", + "picocolors": "^1.1.1", "prettier": "^3.9.6", "shellcheck": "^4.1.0", "typescript": "^6.0.3", diff --git a/scripts/format.js b/scripts/format.js new file mode 100644 index 0000000..76063c2 --- /dev/null +++ b/scripts/format.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node +// Runs the formatters and prints a compact per-tool result. On success only a +// green check is shown; on failure the relevant output is printed. + +import { spawnSync } from "node:child_process"; +import path from "node:path"; +import pc from "picocolors"; + +const root = path.resolve(import.meta.dirname, ".."); + +/** @param {string} name @returns {string} */ +const bin = (name) => path.join(root, "node_modules", ".bin", name); + +/** @type {Array<{label: string, cmd: string, args: string[]}>} */ +const steps = [ + { label: "ESLint", cmd: bin("eslint"), args: [".", "--fix", "--max-warnings", "0"] }, + { label: "Prettier", cmd: bin("prettier"), args: ["--write", "--log-level", "warn", "."] }, +]; + +console.log(pc.bold("Format")); +let failed = false; +for (const step of steps) { + const res = /** @type {import("node:child_process").SpawnSyncReturns} */ ( + spawnSync(step.cmd, step.args, { cwd: root, encoding: "utf8" }) + ); + const output = `${res.stdout}${res.stderr}`.trim(); + if (res.status === 0) { + console.log(` ${pc.green("โœ”")} ${step.label}`); + } else { + failed = true; + console.log(` ${pc.red("โœ–")} ${step.label}`); + if (output) { + console.log(indent(output)); + } + } +} +console.log(failed ? pc.red("Failed") : pc.green("Passed")); +process.exit(failed ? 1 : 0); + +/** @param {string} text @returns {string} */ +function indent(text) { + return text + .split("\n") + .map((line) => ` ${line}`) + .join("\n"); +} diff --git a/scripts/lint.js b/scripts/lint.js new file mode 100644 index 0000000..0123373 --- /dev/null +++ b/scripts/lint.js @@ -0,0 +1,66 @@ +#!/usr/bin/env node +// Runs every linter and prints a compact per-tool result. On success only a +// green check is shown; on failure the relevant error output is printed. + +import { spawnSync } from "node:child_process"; +import path from "node:path"; +import pc from "picocolors"; + +const root = path.resolve(import.meta.dirname, ".."); + +/** @param {string} name @returns {string} */ +const bin = (name) => path.join(root, "node_modules", ".bin", name); + +// Strip markdownlint-cli2 banner/summary noise and prettier's status header. +const NOISE = /^(markdownlint-cli2 v|Finding:|Linting:|Summary:|Checking formatting\.\.\.)/; + +/** @param {string} text @returns {string} */ +function clean(text) { + return text + .split("\n") + .map((line) => line.trim()) + .filter((line) => line && !NOISE.test(line)) + .join("\n"); +} + +/** @type {Array<{label: string, cmd: string, args: string[]}>} */ +const steps = [ + { label: "Editorconfig", cmd: bin("editorconfig-checker"), args: [] }, + { label: "ESLint", cmd: bin("eslint"), args: [".", "--max-warnings", "0"] }, + { + label: "Markdown", + cmd: bin("markdownlint-cli2"), + args: ["--config", ".markdownlint.json", "**/*.md", "#node_modules"], + }, + { label: "Prettier", cmd: bin("prettier"), args: ["--check", "."] }, + { label: "Shell", cmd: bin("shellcheck"), args: [".husky/pre-commit"] }, + { label: "TypeScript", cmd: bin("tsc"), args: ["-p", "tsconfig.check.json"] }, +]; + +console.log(pc.bold("Lint")); +let failed = false; +for (const step of steps) { + const res = /** @type {import("node:child_process").SpawnSyncReturns} */ ( + spawnSync(step.cmd, step.args, { cwd: root, encoding: "utf8" }) + ); + const output = clean(`${res.stdout}${res.stderr}`); + if (res.status === 0) { + console.log(` ${pc.green("โœ”")} ${step.label}`); + } else { + failed = true; + console.log(` ${pc.red("โœ–")} ${step.label}`); + if (output) { + console.log(indent(output)); + } + } +} +console.log(failed ? pc.red("Failed") : pc.green("Passed")); +process.exit(failed ? 1 : 0); + +/** @param {string} text @returns {string} */ +function indent(text) { + return text + .split("\n") + .map((line) => ` ${line}`) + .join("\n"); +} diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} From 97544e9b28c8cf935d7b3b704e74a3272d70537a Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Sun, 9 Aug 2026 09:37:37 +0500 Subject: [PATCH 05/18] chore(package): drop deprecated managementCommand from model providers --- package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package.json b/package.json index 0d9e07b..5c3353e 100644 --- a/package.json +++ b/package.json @@ -280,7 +280,6 @@ { "vendor": "opencodego", "displayName": "OpenCode Go", - "managementCommand": "opencodego.manage", "configuration": { "type": "object", "required": [ @@ -299,7 +298,6 @@ { "vendor": "opencodezen", "displayName": "OpenCode Zen", - "managementCommand": "opencodezen.manage", "configuration": { "type": "object", "required": [ @@ -318,13 +316,11 @@ { "vendor": "opencodego-agent", "displayName": "OpenCode Go (Agents)", - "managementCommand": "opencodego.manage", "when": "config.opencodego.showAgentModelsInManagePanel" }, { "vendor": "opencodezen-agent", "displayName": "OpenCode Zen (Agents)", - "managementCommand": "opencodezen.manage", "when": "config.opencodego.showAgentModelsInManagePanel" } ] From 3ee512e04ebfcf72d7b0657a5bf02cc96fda68b6 Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Sun, 9 Aug 2026 09:38:28 +0500 Subject: [PATCH 06/18] chore(deps): bump @types/node to 26.2 and eslint to 10.8.1 --- package-lock.json | 16 ++++++++-------- package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8c02b83..689dc86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,11 +12,11 @@ "@silvia-odwyer/photon-node": "^0.3.4" }, "devDependencies": { - "@types/node": "^26.1.0", + "@types/node": "^26.2.0", "@types/vscode": "^1.125.0", "@vscode/vsce": "^3.9.2", "editorconfig-checker": "^6.1.1", - "eslint": "^10.8.0", + "eslint": "^10.8.1", "eslint-plugin-jsonc": "^3.4.1", "eslint-plugin-yml": "^3.8.1", "husky": "^9.1.7", @@ -855,9 +855,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", - "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", "dev": true, "license": "MIT", "dependencies": { @@ -2771,9 +2771,9 @@ } }, "node_modules/eslint": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", - "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.1.tgz", + "integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==", "dev": true, "license": "MIT", "workspaces": [ diff --git a/package.json b/package.json index 5c3353e..81e65e1 100644 --- a/package.json +++ b/package.json @@ -355,11 +355,11 @@ ".husky/*": "shellcheck" }, "devDependencies": { - "@types/node": "^26.1.0", + "@types/node": "^26.2.0", "@types/vscode": "^1.125.0", "@vscode/vsce": "^3.9.2", "editorconfig-checker": "^6.1.1", - "eslint": "^10.8.0", + "eslint": "^10.8.1", "eslint-plugin-jsonc": "^3.4.1", "eslint-plugin-yml": "^3.8.1", "husky": "^9.1.7", From f18548b4c1a33274857c1d434b051483a8c34e52 Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Sun, 9 Aug 2026 09:48:28 +0500 Subject: [PATCH 07/18] chore(scripts): rename remaining mjs/mts scripts to js/ts --- .gitignore | 4 ++-- eslint.config.mjs | 2 +- package.json | 6 +++--- scripts/{run-unit-tests.mjs => run-unit-tests.js} | 0 scripts/{test-retry-e2e.mts => test-retry-e2e.ts} | 4 ++-- .../{validate-models.mts => validate-models.ts} | 14 +++++++------- ...en-count.mts => verify-estimate-token-count.ts} | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) rename scripts/{run-unit-tests.mjs => run-unit-tests.js} (100%) rename scripts/{test-retry-e2e.mts => test-retry-e2e.ts} (98%) rename scripts/{validate-models.mts => validate-models.ts} (97%) rename scripts/{verify-estimate-token-count.mts => verify-estimate-token-count.ts} (99%) diff --git a/.gitignore b/.gitignore index c5942d9..a978388 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,5 @@ node_modules/ out/ *.vsix .DS_Store -scripts/validate-models.mjs -scripts/validate-models.mjs.map +scripts/validate-models.js +scripts/validate-models.js.map diff --git a/eslint.config.mjs b/eslint.config.mjs index 15f9560..6932aca 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -24,7 +24,7 @@ const gitignore = readFileSync(new URL(".gitignore", import.meta.url), "utf8") // Files not covered by tsconfig (which only includes src/), type-checked via // the default project so strictTypeChecked rules still apply to them. -const nonProjectFiles = ["eslint.config.mjs", "scripts/*.js", "scripts/*.mjs", "scripts/*.mts"]; +const nonProjectFiles = ["eslint.config.mjs", "scripts/*.js", "scripts/*.mjs", "scripts/*.ts"]; // The typescript-eslint `config()` helper is deprecated; ESLint core now // provides `defineConfig()`. We replicate the helper's `extends` expansion diff --git a/package.json b/package.json index 81e65e1..56325f2 100644 --- a/package.json +++ b/package.json @@ -339,10 +339,10 @@ "format:check": "npx prettier --check .", "clean": "node -e \"require('node:fs').rmSync('out', { recursive: true, force: true })\"", "compile": "npm run clean && tsc -p ./", - "test": "npm run compile && node scripts/run-unit-tests.mjs", + "test": "npm run compile && node scripts/run-unit-tests.js", "watch": "npm run clean && tsc -watch -p ./", - "validate-models": "npx --yes tsx scripts/validate-models.mts", - "test-retry": "npx --yes tsx scripts/test-retry-e2e.mts", + "validate-models": "npx --yes tsx scripts/validate-models.ts", + "test-retry": "npx --yes tsx scripts/test-retry-e2e.ts", "prepackage": "npm test", "package": "vsce package", "vscode:prepublish": "npm run compile", diff --git a/scripts/run-unit-tests.mjs b/scripts/run-unit-tests.js similarity index 100% rename from scripts/run-unit-tests.mjs rename to scripts/run-unit-tests.js diff --git a/scripts/test-retry-e2e.mts b/scripts/test-retry-e2e.ts similarity index 98% rename from scripts/test-retry-e2e.mts rename to scripts/test-retry-e2e.ts index 4d785e1..762f36d 100644 --- a/scripts/test-retry-e2e.mts +++ b/scripts/test-retry-e2e.ts @@ -1,13 +1,13 @@ #!/usr/bin/env node /** - * test-retry-e2e.mts โ€” End-to-end retry integration test with mock server. + * test-retry-e2e.ts โ€” End-to-end retry integration test with mock server. * * Proves the full retry flow works WITHOUT a real API key: * 1. Starts a local HTTP server simulating OpenCode API * 2. Server returns 400 for invalid params, 200 for valid params * 3. Sends request โ†’ gets 400 โ†’ analyzeHttp400ForRetry() โ†’ retries โ†’ gets 200 * - * Usage: npx tsx scripts/test-retry-e2e.mts + * Usage: npx tsx scripts/test-retry-e2e.ts */ import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; diff --git a/scripts/validate-models.mts b/scripts/validate-models.ts similarity index 97% rename from scripts/validate-models.mts rename to scripts/validate-models.ts index 7ef44f6..9c7bec9 100644 --- a/scripts/validate-models.mts +++ b/scripts/validate-models.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * validate-models.mts โ€” Comprehensive model parameter validation suite. + * validate-models.ts โ€” Comprehensive model parameter validation suite. * * Reuses the EXACT same logic as the extension: * - buildThinkingPayload() from thinking.ts @@ -11,8 +11,8 @@ * the live OpenCode API to verify what actually works. * * Usage: - * npx tsx scripts/validate-models.mts --api-key YOUR_KEY - * OPENCODE_API_KEY=... npx tsx scripts/validate-models.mts + * npx tsx scripts/validate-models.ts --api-key YOUR_KEY + * OPENCODE_API_KEY=... npx tsx scripts/validate-models.ts */ import { parseArgs } from "node:util"; @@ -42,12 +42,12 @@ const { values: args } = parseArgs({ if (args.help) { console.log(` -Usage: npx tsx scripts/validate-models.mts [options] +Usage: npx tsx scripts/validate-models.ts [options] Examples: - npx tsx scripts/validate-models.mts --api-key YOUR_KEY - npx tsx scripts/validate-models.mts --api-key YOUR_KEY --families deepseek,kimi - npx tsx scripts/validate-models.mts --dry-run + npx tsx scripts/validate-models.ts --api-key YOUR_KEY + npx tsx scripts/validate-models.ts --api-key YOUR_KEY --families deepseek,kimi + npx tsx scripts/validate-models.ts --dry-run `); process.exit(0); } diff --git a/scripts/verify-estimate-token-count.mts b/scripts/verify-estimate-token-count.ts similarity index 99% rename from scripts/verify-estimate-token-count.mts rename to scripts/verify-estimate-token-count.ts index 3e86164..573339a 100644 --- a/scripts/verify-estimate-token-count.mts +++ b/scripts/verify-estimate-token-count.ts @@ -4,7 +4,7 @@ * Simulates JSON-serialized chat messages at various sizes and checks * that the safeOutputBudget does not collapse to 1. * - * Run: npx tsx scripts/verify-estimate-token-count.mts + * Run: npx tsx scripts/verify-estimate-token-count.ts */ // โ”€โ”€ Heuristic under test (same logic as extension.ts after fix) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ From 130470293de82d4afea7902683db63409a9b97ac Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Sun, 9 Aug 2026 10:11:36 +0500 Subject: [PATCH 08/18] chore(deps): refresh lockfile for transitive updates --- package-lock.json | 678 +++++++++++++++++++++++----------------------- 1 file changed, 341 insertions(+), 337 deletions(-) diff --git a/package-lock.json b/package-lock.json index 689dc86..86ef085 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,22 +50,22 @@ } }, "node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==", "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" } }, "node_modules/@azure/core-auth": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", - "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.11.0.tgz", + "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==", "dev": true, "license": "MIT", "dependencies": { @@ -74,13 +74,13 @@ "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" } }, "node_modules/@azure/core-client": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", - "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.11.0.tgz", + "integrity": "sha512-JjQWO6akOck45PH/XBrxzsQGAiKrfFl4m5iggJ0ItMIz5omRufOXWpqCPpdjKN3vKDzlSUvFjaMb7Zwf0gvAdA==", "dev": true, "license": "MIT", "dependencies": { @@ -93,13 +93,13 @@ "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" } }, "node_modules/@azure/core-rest-pipeline": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", - "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz", + "integrity": "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==", "dev": true, "license": "MIT", "dependencies": { @@ -112,26 +112,26 @@ "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" } }, "node_modules/@azure/core-tracing": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", - "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.4.0.tgz", + "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==", "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" } }, "node_modules/@azure/core-util": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", - "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.14.0.tgz", + "integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==", "dev": true, "license": "MIT", "dependencies": { @@ -140,7 +140,7 @@ "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" } }, "node_modules/@azure/identity": { @@ -167,9 +167,9 @@ } }, "node_modules/@azure/logger": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", - "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.4.0.tgz", + "integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==", "dev": true, "license": "MIT", "dependencies": { @@ -177,26 +177,26 @@ "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" } }, "node_modules/@azure/msal-browser": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.10.1.tgz", - "integrity": "sha512-hTbvOi9Ko2Jvn+G/fSmjzHf9WbNcf/o3epMtbeGx/pMwMrVAbi6OgCJVeCfsAb8IybSRpaCSc4EDRlYAhgngUQ==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.18.0.tgz", + "integrity": "sha512-SPTeHYZghdEdRddJzNjhH+CI5MSQtquNYwGJnYXfOHIBRXCmrWimBS85OhwXpXFIlrCtNTbBPm5mPAWRNEoktA==", "dev": true, "license": "MIT", "dependencies": { - "@azure/msal-common": "16.6.1" + "@azure/msal-common": "16.12.0" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.1.tgz", - "integrity": "sha512-VxKdEtUwDuLD0F1hOQP7kye0YadZxFJfv37Em440geEf/w9uggKnHpRrqwZJOdxmPUOdhZ9kyRtKuAJW8wUcRg==", + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.12.0.tgz", + "integrity": "sha512-hgLgfRdbG2AmhXPygebf1KYJEvse86+ZZLWufdiTKaGRYEUqOzHdlf6AS1IiuUCHWbynkgbHc451jSNkbfhWlg==", "dev": true, "license": "MIT", "engines": { @@ -204,13 +204,13 @@ } }, "node_modules/@azure/msal-node": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.1.tgz", - "integrity": "sha512-tmQiQ2HvtzaeLqYGy3BemiPOSGPY4wCy1IW5zDWITKSs/s35WEd7Zij/hCxvUdAOzj6U3qnyaGbYXY91ortFEQ==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.5.0.tgz", + "integrity": "sha512-A/2WIsuH0vsC6JVkkafjS4kHpi2LDR4AzDT0kJ+oIRtXYeYtvGQ2pwN2X88thQPhSek+82ela3MprsKXWQRrhQ==", "dev": true, "license": "MIT", "dependencies": { - "@azure/msal-common": "16.6.1", + "@azure/msal-common": "16.12.0", "jsonwebtoken": "^9.0.0" }, "engines": { @@ -218,13 +218,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -233,9 +233,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -687,9 +687,9 @@ "license": "Apache-2.0" }, "node_modules/@sindresorhus/merge-streams": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", - "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", "dev": true, "license": "MIT", "engines": { @@ -700,33 +700,35 @@ } }, "node_modules/@textlint/ast-node-types": { - "version": "15.6.1", - "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.6.1.tgz", - "integrity": "sha512-KXUhbpBctWkSumyNwFQBufwWCcjdxARGVnlH6AfERaFAnpi300L8og+l2Hs/bIqkXu26T5tIs7jNnRgUs20hmQ==", + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.8.0.tgz", + "integrity": "sha512-5CiH9COYmovWmExQgs7763DzX6Gy9zjkjJ7JxCC95wyTcjwQn/8poNF6fv3qzRlmx8CRRde8DHr9FcgAAiPzgw==", "dev": true, "license": "MIT" }, "node_modules/@textlint/linter-formatter": { - "version": "15.6.1", - "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.6.1.tgz", - "integrity": "sha512-mrLdBQkySnUsznPCfOuzyZk3381bJoaK2hPzwmRvOqUeRm04SdPEqN3rkqnbeB4Vw61d1z6gM+kJSS0y8+Zmog==", + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.8.0.tgz", + "integrity": "sha512-+oU3A235NATv6Lzi4xa4kJ65PuNJlIxesaO4AvDhDWA9FWm7y4XKWaoQCW1esgaQQ6dwnUiFKArQ8TcJ86mC4w==", "dev": true, "license": "MIT", "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", - "@textlint/module-interop": "15.6.1", - "@textlint/resolver": "15.6.1", - "@textlint/types": "15.6.1", - "chalk": "^4.1.2", + "@textlint/module-interop": "15.8.0", + "@textlint/resolver": "15.8.0", + "@textlint/types": "15.8.0", "debug": "^4.4.3", - "js-yaml": "^4.1.1", + "js-yaml": "^4.3.0", "lodash": "^4.18.1", "pluralize": "^2.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "table": "^6.9.0", "text-table": "^0.2.0" + }, + "engines": { + "node": ">=20.18.0" } }, "node_modules/@textlint/linter-formatter/node_modules/ansi-regex": { @@ -760,27 +762,27 @@ } }, "node_modules/@textlint/module-interop": { - "version": "15.6.1", - "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.6.1.tgz", - "integrity": "sha512-hrsG9S7dlTPoFQ9ImKCHt1I7uPGt25lBXoc/ENP0U47tETAsg8mwjwHBQ4SEEH/X+AXYIEg6+saPyKAj08Aa+Q==", + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.8.0.tgz", + "integrity": "sha512-rt+OR1WYGoLOY8HkA/aBPrqufF6yUUEsKEAh7XohTsT3lp9IyZFT6zOIbjul9P4FAzsmSPkcrYjVx3Bz/IUfkg==", "dev": true, "license": "MIT" }, "node_modules/@textlint/resolver": { - "version": "15.6.1", - "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.6.1.tgz", - "integrity": "sha512-kC8MFJH9mIoTtw1IPupsl9k2KF/xj48ZqJoEfVmAqJ2yqd7gvvNRAgndOIBTZOLHDyFK1ouHpFPhW3ID/kNcmw==", + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.8.0.tgz", + "integrity": "sha512-E88tzfX3K8Jykk+38aJ9cy8RquD8ABVOPTO2rFEESq0wcg8x6/ypdAS8ZgR7OKiGqlRF0hkO/m5PbQwVfKM3VA==", "dev": true, "license": "MIT" }, "node_modules/@textlint/types": { - "version": "15.6.1", - "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.6.1.tgz", - "integrity": "sha512-dRdyTAMRaqcU5eHpJWgTq9kcKGErs+cVVFwNTP3gUAFDJxEVxdOlJU2zjMgzJAzf/4WfOE6UJ56Mmn09acxmzw==", + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.8.0.tgz", + "integrity": "sha512-Anhc6y5736YIsvqae0U6k0YmB2M/QVHkEeOv2aydAn/WIkdI69dCOiDbe3/+RagS3qstFTSFWJzNRA2lUjv19w==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/ast-node-types": "15.6.1" + "@textlint/ast-node-types": "15.8.0" } }, "node_modules/@tokenizer/inflate": { @@ -921,6 +923,16 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@typescript-eslint/parser": { "version": "8.66.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", @@ -1113,9 +1125,9 @@ } }, "node_modules/@typespec/ts-http-runtime": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", - "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.8.tgz", + "integrity": "sha512-bLMpVcWZNzq6lYOybwFwOAR1IXKcHnhUNqYeHjl1bET/qE3jFPFH+p8Wrh3rU4xwdnifPxmKNESBYnvnmc75aA==", "dev": true, "license": "MIT", "dependencies": { @@ -1124,7 +1136,7 @@ "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" } }, "node_modules/@vscode/vsce": { @@ -1334,19 +1346,6 @@ "node": ">=18" } }, - "node_modules/@xhmikosr/decompress-tar/node_modules/tar-stream": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", - "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, "node_modules/@xhmikosr/decompress-unzip": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-unzip/-/decompress-unzip-7.1.0.tgz", @@ -1656,16 +1655,14 @@ } }, "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" } }, "node_modules/boolbase": { @@ -2140,17 +2137,6 @@ "node": ">=4" } }, - "node_modules/decompress-tar/node_modules/bl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", - "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - } - }, "node_modules/decompress-tar/node_modules/file-type": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", @@ -2171,46 +2157,6 @@ "node": ">=0.10.0" } }, - "node_modules/decompress-tar/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/decompress-tar/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/decompress-tar/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/decompress-tar/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/decompress-tar/node_modules/tar-stream": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", @@ -2722,9 +2668,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -2965,29 +2911,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3111,6 +3034,19 @@ "node": ">=8.6.0" } }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3162,6 +3098,24 @@ "pend": "~1.2.0" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fflate": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", @@ -3293,9 +3247,9 @@ "license": "MIT" }, "node_modules/fs-extra": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", - "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", "dev": true, "license": "MIT", "dependencies": { @@ -3409,16 +3363,16 @@ } }, "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" } }, "node_modules/global-agent": { @@ -3457,26 +3411,36 @@ } }, "node_modules/globby": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", - "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.2.tgz", + "integrity": "sha512-NLvV9ubZ6NDsJaOpKPy3cQeJpKi9DcWiyCiFUpJPA0YihRqiE6RWaLUmgNNPr8MgPpLZjnBjSmou7uZBRJv9wA==", "dev": true, "license": "MIT", "dependencies": { - "@sindresorhus/merge-streams": "^2.1.0", + "@sindresorhus/merge-streams": "^4.0.0", "fast-glob": "^3.3.3", - "ignore": "^7.0.3", - "path-type": "^6.0.0", + "ignore": "^7.0.5", + "is-path-inside": "^4.0.0", "slash": "^5.1.0", - "unicorn-magic": "^0.3.0" + "unicorn-magic": "^0.4.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3687,9 +3651,9 @@ "license": "BSD-3-Clause" }, "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -3939,9 +3903,9 @@ } }, "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true, "license": "MIT" }, @@ -4253,19 +4217,6 @@ "yaml": "^2.9.0" } }, - "node_modules/lint-staged/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4473,40 +4424,6 @@ "markdownlint-cli2": ">=0.0.4" } }, - "node_modules/markdownlint-cli2/node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/markdownlint-cli2/node_modules/globby": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.2.tgz", - "integrity": "sha512-NLvV9ubZ6NDsJaOpKPy3cQeJpKi9DcWiyCiFUpJPA0YihRqiE6RWaLUmgNNPr8MgPpLZjnBjSmou7uZBRJv9wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^4.0.0", - "fast-glob": "^3.3.3", - "ignore": "^7.0.5", - "is-path-inside": "^4.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.4.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/markdownlint-cli2/node_modules/js-yaml": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.2.tgz", @@ -4530,19 +4447,6 @@ "js-yaml": "bin/js-yaml.mjs" } }, - "node_modules/markdownlint-cli2/node_modules/unicorn-magic": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", - "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/markdownlint/node_modules/string-width": { "version": "8.2.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", @@ -4584,9 +4488,9 @@ } }, "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", "dev": true, "license": "MIT" }, @@ -5150,6 +5054,19 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -5201,13 +5118,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -5275,9 +5192,9 @@ "license": "MIT" }, "node_modules/node-abi": { - "version": "3.92.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", - "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", "dev": true, "license": "MIT", "optional": true, @@ -5471,9 +5388,9 @@ } }, "node_modules/p-map": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.6.tgz", + "integrity": "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg==", "dev": true, "license": "MIT", "engines": { @@ -5632,9 +5549,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -5669,13 +5586,13 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -5943,21 +5860,28 @@ } }, "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -6063,9 +5987,9 @@ "license": "MIT" }, "node_modules/sax": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -6094,6 +6018,63 @@ "node": ">=20.0.0" } }, + "node_modules/secretlint/node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/secretlint/node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/secretlint/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/secretlint/node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/seek-bzip": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", @@ -6116,9 +6097,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -6450,16 +6431,22 @@ } }, "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "safe-buffer": "~5.2.0" + "safe-buffer": "~5.1.0" } }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -6659,9 +6646,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", "dev": true, "license": "MIT", "optional": true, @@ -6672,7 +6659,36 @@ "tar-stream": "^2.1.4" } }, - "node_modules/tar-stream": { + "node_modules/tar-fs/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/tar-fs/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tar-fs/node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", @@ -6690,6 +6706,19 @@ "node": ">=6" } }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/teex": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", @@ -6784,37 +6813,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tmp": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", @@ -6840,6 +6838,13 @@ "node": ">= 0.4" } }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7063,13 +7068,13 @@ "license": "MIT" }, "node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", + "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7326,13 +7331,12 @@ } }, "node_modules/yauzl": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.0.tgz", - "integrity": "sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", "dev": true, "license": "MIT", "dependencies": { - "buffer-crc32": "~0.2.3", "pend": "~1.2.0" }, "engines": { From ffce99df1e33f93f74b2cdd2510e501ed7017cdf Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Tue, 11 Aug 2026 16:06:47 +0500 Subject: [PATCH 09/18] chore(eslint): keep type-aware strictness, drop style-only rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pure-stylistic typescript-eslint layer (array-type, consistent-type-definitions, prefer-for-of, ...) produced churn without catching bugs โ€” prettier already owns formatting. Keep strict + strictTypeChecked (the type-aware correctness rules: no-unsafe-*, no-unnecessary-condition, ...), the yml/jsonc config linters and the zero-tolerance no-warning-comments gate. Also quiet two rules whose noise outweighed their value: - restrict-template-expressions: allow numbers/booleans (String() around them was ceremony, not safety). - no-floating-promises: off for *.test.* files โ€” node:test's describe/it are scheduled by the runner, so the void/await ceremony added nothing. --- eslint.config.mjs | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 6932aca..9c89e40 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,15 +1,19 @@ -// ESLint flat config โ€” MAXIMUM strictness. +// ESLint flat config โ€” strict where it catches real bugs, quiet where it +// would only add ceremony. // -// Stack (strictest available for each layer): -// - typescript-eslint `strict` + `strictTypeChecked` + `stylistic`: -// type-aware rules (no-unsafe-*, no-unnecessary-condition, ...), -// strict correctness rules, and stylistic consistency. +// Stack: +// - typescript-eslint `strict` + `strictTypeChecked`: +// type-aware correctness rules (no-unsafe-*, no-unnecessary-condition, +// ...). The pure-stylistic layer (`stylistic`) is deliberately NOT +// enabled โ€” it fights the formatter (prettier owns formatting) and its +// rules (array-type, consistent-type-definitions, prefer-for-of, ...) +// produced churn without catching bugs. // - eslint-plugin-yml `flat/standard`: strict YAML linting. // - eslint-plugin-jsonc `flat/recommended-with-jsonc`: strict JSON/JSONC. // // Zero tolerance: every violation is an error; warnings are turned into errors -// via `--max-warnings 0` in package.json. Nothing is disabled here except what -// the strict stacks themselves permit. +// via `--max-warnings 0`. The only rules disabled here are ones whose noise +// outweighs their value (see the scoped overrides below). import { readFileSync } from "node:fs"; import { defineConfig } from "eslint/config"; @@ -30,15 +34,15 @@ const nonProjectFiles = ["eslint.config.mjs", "scripts/*.js", "scripts/*.mjs", " // provides `defineConfig()`. We replicate the helper's `extends` expansion // explicitly by applying the TS `files` glob to each config object. const tsFiles = ["**/*.{ts,mts,cts,js,mjs,cjs}"]; +const testFiles = ["**/*.test.{ts,tsx,js,mjs,cjs}"]; export default defineConfig([ { ignores: gitignore, }, - // --- TypeScript / JavaScript: strictest type-aware rules ----------------- + // --- TypeScript / JavaScript: strict type-aware rules -------------------- ...tseslint.configs.strict.map((conf) => ({ ...conf, files: tsFiles })), ...tseslint.configs.strictTypeChecked.map((conf) => ({ ...conf, files: tsFiles })), - ...tseslint.configs.stylistic.map((conf) => ({ ...conf, files: tsFiles })), { files: tsFiles, languageOptions: { @@ -63,6 +67,23 @@ export default defineConfig([ caughtErrorsIgnorePattern: "^_", }, ], + // Numbers and booleans interpolate unambiguously; requiring `String()` + // around them is noise, not safety. + "@typescript-eslint/restrict-template-expressions": [ + "error", + { + allowNumber: true, + allowBoolean: true, + }, + ], + }, + }, + { + // node:test's describe/it are scheduled by the test runner; the returned + // promise is handled there, so the `void`/`await` ceremony adds nothing. + files: testFiles, + rules: { + "@typescript-eslint/no-floating-promises": "off", }, }, // --- YAML: strict rules from eslint-plugin-yml --------------------------- From a84402a6a6977dc63cd28e4fc4cfcf497ffb1efb Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Tue, 11 Aug 2026 16:08:43 +0500 Subject: [PATCH 10/18] chore(lint): run unit tests as part of the lint command `bun lint` / `npm run lint` now ends with a Tests step (compile + unit tests), so a single lint invocation covers both static checks and the test suite. --- scripts/lint.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/lint.js b/scripts/lint.js index f8b8a13..2bfc2c8 100644 --- a/scripts/lint.js +++ b/scripts/lint.js @@ -35,6 +35,7 @@ const steps = [ { label: "Prettier", cmd: bin("prettier"), args: ["--check", ".", "--ignore-path", ".gitignore"] }, { label: "Shell", cmd: bin("shellcheck"), args: [".husky/pre-commit"] }, { label: "TypeScript", cmd: bin("tsc"), args: ["-p", "tsconfig.check.json"] }, + { label: "Tests", cmd: "npm", args: ["test"] }, ]; console.log(pc.bold("Lint")); From a4ef44964051b05c7e4df8b3c94b76ca1a650e9f Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Tue, 11 Aug 2026 16:24:16 +0500 Subject: [PATCH 11/18] =?UTF-8?q?feat(hooks):=20intelligent=20staged=20lin?= =?UTF-8?q?t=20=E2=80=94=20changed=20files=20plus=20their=20dependents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-commit previously linted the whole tree (slow) or only the staged files (fast but blind). scripts/staged-lint.js runs the linters on exactly what a change can affect: - ESLint: staged JS/TS files PLUS their direct dependents (files that import a staged file, resolved from the actual import graph), so changing a module can never leave type-aware errors in its consumers. - markdownlint: staged Markdown files. - editorconfig-checker: staged files. - shellcheck: staged .husky scripts. - tsc -p tsconfig.check.json + unit tests: only when src/ or scripts/ changed (they are whole-project by nature). Formatting stays with lint-staged, which runs after this gate; the full tree lint (including tests) remains available via `npm run lint` and is enforced in CI. --- scripts/staged-lint.js | 186 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 scripts/staged-lint.js diff --git a/scripts/staged-lint.js b/scripts/staged-lint.js new file mode 100644 index 0000000..604a718 --- /dev/null +++ b/scripts/staged-lint.js @@ -0,0 +1,186 @@ +#!/usr/bin/env node +// Intelligent pre-commit lint gate. +// +// Instead of linting the whole tree on every commit (slow), lint only what a +// change can actually affect: +// - ESLint runs on the staged JS/TS files PLUS their direct dependents +// (files that import a staged file), so changing a module can never leave +// type-aware errors behind in its consumers. +// - markdownlint runs on staged Markdown files. +// - editorconfig-checker runs on staged files. +// - shellcheck runs on staged .husky scripts. +// - Type-checking (tsc -p tsconfig.check.json) and unit tests are +// whole-project by nature and only run when src/ or scripts/ changed. +// +// Formatting (prettier --write, eslint --fix, markdownlint --fix) is left to +// lint-staged, which runs after this gate. The full-tree lint (including +// tests) stays available as `npm run lint` and is enforced in CI. + +import { spawnSync } from "node:child_process"; +import { readdirSync, readFileSync, statSync } from "node:fs"; +import path from "node:path"; +import pc from "picocolors"; + +const root = path.resolve(import.meta.dirname, ".."); + +/** @param {string} name @returns {string} */ +const bin = (name) => path.join(root, "node_modules", ".bin", name); + +const SRC_DIRS = ["src", "scripts"]; +const TS_EXT = new Set([".ts", ".tsx", ".js", ".mjs", ".cjs", ".mts", ".cts"]); +const IMPORT_RE = /(?:from\s*|import\s*\(\s*|require\s*\(\s*)["'](\.[^"']+)["']/g; + +/** @param {string} text @returns {string} */ +function indent(text) { + return text + .split("\n") + .map((line) => ` ${line}`) + .join("\n"); +} + +/** @param {string} cmd @param {string[]} args @returns {{status: number|null, output: string}} */ +function run(cmd, args) { + const res = /** @type {import("node:child_process").SpawnSyncReturns} */ (spawnSync(cmd, args, { cwd: root, encoding: "utf8" })); + return { status: res.status, output: `${res.stdout}${res.stderr}`.trim() }; +} + +/** Staged (added/copied/modified) file paths relative to the repo root. @returns {string[]} */ +function stagedFiles() { + const res = run("git", ["diff", "--cached", "--name-only", "-z", "--diff-filter=ACM"]); + if (res.status !== 0) { + return []; + } + return res.output.split("\0").filter(Boolean); +} + +/** Every TS/JS source file under src/ and scripts/. @returns {string[]} */ +function collectSourceFiles() { + /** @type {string[]} */ + const out = []; + for (const dir of SRC_DIRS) { + /** @param {string} dirPath */ + const walk = (dirPath) => { + for (const entry of readdirSync(dirPath, { withFileTypes: true })) { + if (entry.isDirectory()) { + walk(path.join(dirPath, entry.name)); + } else if (TS_EXT.has(path.extname(entry.name))) { + out.push(path.join(dirPath, entry.name)); + } + } + }; + walk(dir); + } + return out; +} + +/** Resolve a relative import specifier to an existing file, if any. + * @param {string} fromFile + * @param {string} spec + * @returns {string | undefined} + */ +function resolveImport(fromFile, spec) { + const base = path.resolve(path.dirname(fromFile), spec); + const candidates = [ + base, + `${base}.ts`, + `${base}.tsx`, + `${base}.js`, + `${base}.mjs`, + `${base}.mts`, + path.join(base, "index.ts"), + path.join(base, "index.js"), + ]; + for (const candidate of candidates) { + try { + statSync(candidate); + return candidate; + } catch { + // keep trying + } + } + return undefined; +} + +/** resolved file path โ†’ set of source files importing it (one level deep). @returns {Map>} */ +function buildImporters() { + /** @type {Map>} */ + const importers = new Map(); + for (const file of collectSourceFiles()) { + const text = readFileSync(file, "utf8"); + for (const match of text.matchAll(IMPORT_RE)) { + const resolved = resolveImport(file, match[1]); + if (!resolved) { + continue; + } + let set = importers.get(resolved); + if (!set) { + set = new Set(); + importers.set(resolved, set); + } + set.add(file); + } + } + return importers; +} + +/** + * Files related to the staged ones: their direct dependents within src/ and + * scripts/, so changing a module's contract also lints its consumers. + * @param {string[]} changedFiles + * @returns {string[]} + */ +function relatedFiles(changedFiles) { + const importers = buildImporters(); + /** @type {Set} */ + const related = new Set(); + for (const file of changedFiles) { + for (const importer of importers.get(file) ?? []) { + related.add(importer); + } + } + return [...related]; +} + +const staged = stagedFiles(); +if (staged.length === 0) { + console.log(pc.green("No staged files โ€” nothing to lint.")); + process.exit(0); +} + +const jsTsFiles = staged.filter((file) => TS_EXT.has(path.extname(file))); +const mdFiles = staged.filter((file) => file.endsWith(".md")); +const huskyFiles = staged.filter((file) => file.startsWith(".husky/")); +const sourceChanged = staged.some((file) => SRC_DIRS.some((dir) => file.startsWith(`${dir}/`))); + +const eslintTargets = [...new Set([...jsTsFiles, ...relatedFiles(jsTsFiles)])]; + +/** @type {Array<{label: string, cmd: string, args: string[], run: boolean}>} */ +const steps = [ + { label: "ESLint", cmd: bin("eslint"), args: ["--max-warnings", "0", ...eslintTargets], run: eslintTargets.length > 0 }, + { label: "Markdown", cmd: bin("markdownlint-cli2"), args: ["--config", ".markdownlint-cli2.jsonc", ...mdFiles], run: mdFiles.length > 0 }, + { label: "Editorconfig", cmd: bin("editorconfig-checker"), args: [...staged], run: true }, + { label: "Shell", cmd: bin("shellcheck"), args: [...huskyFiles], run: huskyFiles.length > 0 }, + { label: "TypeScript", cmd: bin("tsc"), args: ["-p", "tsconfig.check.json"], run: sourceChanged }, + { label: "Tests", cmd: "npm", args: ["test"], run: sourceChanged }, +]; + +console.log(pc.bold("Lint (staged)")); +let failed = false; +for (const step of steps) { + if (!step.run) { + console.log(` ${pc.dim("-")} ${step.label} (nothing affected)`); + continue; + } + const { status, output } = run(step.cmd, step.args); + if (status === 0) { + console.log(` ${pc.green("โœ”")} ${step.label}`); + } else { + failed = true; + console.log(` ${pc.red("โœ–")} ${step.label}`); + if (output) { + console.log(indent(output)); + } + } +} +console.log(failed ? pc.red("Failed") : pc.green("Passed")); +process.exit(failed ? 1 : 0); From 6cf7714217d1b68a0971dcd5f5eca5f0cb43990a Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Tue, 11 Aug 2026 16:26:46 +0500 Subject: [PATCH 12/18] feat(hooks): run intelligent staged lint in pre-commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the full-tree `npm run lint` in the pre-commit hook with `node scripts/staged-lint.js` (exposed as `npm run lint:staged`): the linters now run on the staged files plus their direct dependents, and type-checking + tests only when source files changed โ€” commits are fast without losing the guarantee that a change (and its consumers) are clean. The full-tree gate (lint incl. tests, format check, packaging) remains in CI and available on demand via `npm run lint` / `npm run format:check`. --- .husky/pre-commit | 9 +++++---- package.json | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 60f5bf9..4938fcb 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -14,10 +14,11 @@ if ! command -v npx >/dev/null 2>&1; then fi fi -# Zero-tolerance gate FIRST, on raw content: block the commit if any file has -# any error, warning, info diagnostic, type error, or formatting drift. -# Linting before formatting ensures the linter sees the file as written. -npm run lint +# Zero-tolerance gate on exactly what this change can affect: the staged +# files plus the files that import them (see scripts/staged-lint.js). Runs +# BEFORE formatting so the linter sees the file as written. The full-tree +# lint (including tests) runs in CI and on demand via `npm run lint`. +node scripts/staged-lint.js # Format changed files (code + non-code) after linting passes. npx lint-staged diff --git a/package.json b/package.json index 1404862..f6de84a 100644 --- a/package.json +++ b/package.json @@ -357,6 +357,7 @@ }, "scripts": { "lint": "node scripts/lint.js", + "lint:staged": "node scripts/staged-lint.js", "lint:js": "eslint . --max-warnings 0", "lint:fix": "eslint . --fix --max-warnings 0", "lint:md": "markdownlint-cli2 --config .markdownlint-cli2.jsonc \"**/*.md\" \"#node_modules\"", From 37d2bb6cb64a43a239221f65e3f4dea9d2a63ac9 Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Tue, 11 Aug 2026 16:34:19 +0500 Subject: [PATCH 13/18] style(docs): prettier-format release-cut docs merged from main The 0.5.2 release documentation merged from main was not prettier-clean (main's CI only checks formatting for src, not docs). --- ...11-pr120-vision-proxy-description-cache.md | 36 +++++++------- ...60811-pr124-managementcommand-byok-flow.md | 17 +++---- ...0260811-pr125-agents-window-byok-bridge.md | 48 +++++++++---------- ...811-pr126-reasoning-history-guard-tests.md | 44 +++++++++-------- docs/issues/60-20260811-release-0-5-2-plan.md | 38 +++++++-------- 5 files changed, 93 insertions(+), 90 deletions(-) diff --git a/docs/issues/56-20260811-pr120-vision-proxy-description-cache.md b/docs/issues/56-20260811-pr120-vision-proxy-description-cache.md index af8f625..2e1485f 100644 --- a/docs/issues/56-20260811-pr120-vision-proxy-description-cache.md +++ b/docs/issues/56-20260811-pr120-vision-proxy-description-cache.md @@ -25,7 +25,7 @@ This PR adds a per-image description cache keyed by the SHA-256 of the image's b ### 2. `src/extension.ts` โ€” `proxyVision()` refactor - **Lazy vision-model resolution:** the model is only resolved (`selectChatModels()`) when a message actually needs a new description. When every image is cached, neither `selectChatModels()` nor `sendRequest()` runs. -- **Per-message describing (default):** only the message that contains a *new* image is sent to the vision model (message parts + prompt), instead of re-sending the whole conversation. +- **Per-message describing (default):** only the message that contains a _new_ image is sent to the vision model (message parts + prompt), instead of re-sending the whole conversation. - **Whole-conversation mode (opt-in):** `opencodego.visionProxyWholeConversation` (default `false`). When on, `proxyVision()` sends ONE request over all messages so descriptions carry full conversation context; the combined description is stored under every image hash (same no-partial-reuse rule). - **Flattened-message mapping:** converted messages are flattened with a `flatSourceIndex` array, tracking which original message produced each `apiMessage` (one input message can expand into several, e.g. tool results). Descriptions are keyed by original message index so the correct description lands on the right `apiMessage`. - Refactored request builders: `collectRequestParts()`, `buildVisionRequestMessage()` (single message), `buildWholeConversationRequest()` (whole conversation). @@ -60,7 +60,7 @@ This PR adds a per-image description cache keyed by the SHA-256 of the image's b ## Design Notes (non-blocking) -- **Whole-conversation mode and the cache:** in whole-conversation mode every turn re-describes the whole conversation (the context changes each turn), so the cache is effectively not hit *within* that mode โ€” it only pays off after switching back to the default mode. This is the expected "full context, more tokens" trade-off; the setting is opt-in. +- **Whole-conversation mode and the cache:** in whole-conversation mode every turn re-describes the whole conversation (the context changes each turn), so the cache is effectively not hit _within_ that mode โ€” it only pays off after switching back to the default mode. This is the expected "full context, more tokens" trade-off; the setting is opt-in. - **Tool-result images:** nested images inside `LanguageModelToolResultPart` are not sent to the vision model (consistent with prior behavior). They fall back to the first available description in the pass. ## Merge Strategy @@ -71,25 +71,25 @@ Merged via **regular merge commit** (`8f6cb9f`, parents `190b9ee` + `e370512`) t 7 files, +439 / โˆ’73 (cumulative over the PR's 2 commits): -| File | Change | -| --- | --- | -| `src/visionProxyCache.ts` | **New** โ€” image description cache (SHA-256 key, 200-entry FIFO) | -| `src/extension.ts` | `proxyVision()` refactor (lazy model resolve, per-message + whole-conversation modes, flattened-message mapping, request builders), setting read | -| `src/test/visionProxy.test.ts` | 7 new cache tests | -| `package.json` | `opencodego.visionProxyWholeConversation` setting | -| `README.md` | Settings table row | -| `docs/features/11-20260715-vision-proxy.md` | "Description Cache & Whole-Conversation Mode" section | -| `package-lock.json` | Version sync `0.5.0 โ†’ 0.5.1` + `peer: true` noise on a few dev deps | +| File | Change | +| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| `src/visionProxyCache.ts` | **New** โ€” image description cache (SHA-256 key, 200-entry FIFO) | +| `src/extension.ts` | `proxyVision()` refactor (lazy model resolve, per-message + whole-conversation modes, flattened-message mapping, request builders), setting read | +| `src/test/visionProxy.test.ts` | 7 new cache tests | +| `package.json` | `opencodego.visionProxyWholeConversation` setting | +| `README.md` | Settings table row | +| `docs/features/11-20260715-vision-proxy.md` | "Description Cache & Whole-Conversation Mode" section | +| `package-lock.json` | Version sync `0.5.0 โ†’ 0.5.1` + `peer: true` noise on a few dev deps | ## Code Locations (on `main`) -| Concern | Location | -| --- | --- | -| Cache module | `src/visionProxyCache.ts` โ€” `imageDescriptionCache` L22, `IMAGE_DESCRIPTION_CACHE_LIMIT` L25, `imageDescriptionKey` L32, `lookupImageDescriptions` L42, `storeImageDescriptions` L63, `clearImageDescriptionCache` L77 | -| Cache import | `src/extension.ts` L51 | -| Whole-conversation setting read | `src/extension.ts` ~L2265 | -| Request builders | `src/extension.ts` โ€” `collectRequestParts()` L4088, `buildVisionRequestMessage()` L4111, `buildWholeConversationRequest()` L4137 | -| `proxyVision()` | `src/extension.ts` ~L4178 | +| Concern | Location | +| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Cache module | `src/visionProxyCache.ts` โ€” `imageDescriptionCache` L22, `IMAGE_DESCRIPTION_CACHE_LIMIT` L25, `imageDescriptionKey` L32, `lookupImageDescriptions` L42, `storeImageDescriptions` L63, `clearImageDescriptionCache` L77 | +| Cache import | `src/extension.ts` L51 | +| Whole-conversation setting read | `src/extension.ts` ~L2265 | +| Request builders | `src/extension.ts` โ€” `collectRequestParts()` L4088, `buildVisionRequestMessage()` L4111, `buildWholeConversationRequest()` L4137 | +| `proxyVision()` | `src/extension.ts` ~L4178 | ## References diff --git a/docs/issues/57-20260811-pr124-managementcommand-byok-flow.md b/docs/issues/57-20260811-pr124-managementcommand-byok-flow.md index 1e94f1c..7a87bc6 100644 --- a/docs/issues/57-20260811-pr124-managementcommand-byok-flow.md +++ b/docs/issues/57-20260811-pr124-managementcommand-byok-flow.md @@ -21,8 +21,8 @@ The `languageModelChatProviders` contributions in `package.json` declared **both ### Reported symptoms -1. **Context menu unresponsive** โ€” *Rename Group*, *Update API Key*, *Delete*, and *Open in Language Models (JSON)* did nothing when clicked. -2. **"+ Add Models" dead** โ€” clicking *OpenCode Go* or *OpenCode Zen* under **+ Add Models** did not prompt for a group name or API key and never created a group. +1. **Context menu unresponsive** โ€” _Rename Group_, _Update API Key_, _Delete_, and _Open in Language Models (JSON)_ did nothing when clicked. +2. **"+ Add Models" dead** โ€” clicking _OpenCode Go_ or _OpenCode Zen_ under **+ Add Models** did not prompt for a group name or API key and never created a group. 3. **Leftover default group cannot be removed** โ€” the `OpenCode Go` group persisted in `chatLanguageModels.json` (e.g. created by per-model `reasoningEffort` configuration) could never be deleted. ### Root cause @@ -31,8 +31,8 @@ Verified against the VS Code source (`src/vs/workbench/contrib/chat/common/langu ```ts if (vendor.managementCommand) { - await this._resolveAllLanguageModels(vendor.vendor, false); - return; // โ† short-circuit: re-resolve models only, never prompt/create a group + await this._resolveAllLanguageModels(vendor.vendor, false); + return; // โ† short-circuit: re-resolve models only, never prompt/create a group } // ...only below this point does VS Code prompt for a group name + configuration // and create the BYOK group in chatLanguageModels.json @@ -69,10 +69,10 @@ The extension's own management commands (`OpenCode Go: Manage Provider`, `OpenCo ## Files Changed -| File | Change | -| ---- | ------ | +| File | Change | +| -------------- | ----------------------------------------------------------------------------------------------------- | | `package.json` | Removed `managementCommand` from `opencodego`, `opencodezen`, `opencodego-agent`, `opencodezen-agent` | -| `CHANGELOG.md` | New `[Unreleased] โ†’ Fixed` entry for #121 | +| `CHANGELOG.md` | New `[Unreleased] โ†’ Fixed` entry for #121 | No `src/` changes โ€” this is a manifest-only fix. @@ -86,6 +86,7 @@ No `src/` changes โ€” this is a manifest-only fix. > `managementCommand` โ€” Deprecated. Use `configuration` instead. Command ID that opens a UI for managing this provider. And `configuration` is documented as "the recommended way to let users configure a provider." + 3. **No auth regression** โ€” `provideLanguageModelChatInformation` / `provideLanguageModelChatResponse` already fall back to the extension's `SecretStorage` via `getConfiguredApiKey` when `options.configuration.apiKey` is absent, so users who set the key through the legacy commands keep working. ### Minor UX trade-off (agent vendors) @@ -95,7 +96,7 @@ The "+ Add Models" dropdown in VS Code lists vendors with `managementCommand || - The agent vendors (`*-agent`) no longer appear in "+ Add Models" โ€” acceptable, since clicking them was previously a dead short-circuit too. - The gear "Manage (Agents)โ€ฆ" entry in the Manage Language Models panel is no longer rendered for agent vendors (that branch is `else if (vendorEntry.vendor.managementCommand)`). Only relevant when `opencodego.showAgentModelsInManagePanel` is `true` (default `false`); the commands remain reachable from the Command Palette. -> **Note โ€” not affected by PR #125 (#122):** the *base* providers `opencodego` / `opencodezen` DO appear in the Agents window's "+ Add Models" list since PR #125 auto-enables the BYOK agent-host bridge (`chat.agentHost.byokModels.enabled` + `extensions.supportAgentsWindow`). That is the base vendor flow, separate from the `*-agent` variant vendors discussed above. PR #125 also added `when` clauses (`config.opencodego.enabled` / `config.opencodezen.enabled`) to the contributions but did **not** reintroduce `managementCommand`, so the native BYOK group flow from this PR stays intact. +> **Note โ€” not affected by PR #125 (#122):** the _base_ providers `opencodego` / `opencodezen` DO appear in the Agents window's "+ Add Models" list since PR #125 auto-enables the BYOK agent-host bridge (`chat.agentHost.byokModels.enabled` + `extensions.supportAgentsWindow`). That is the base vendor flow, separate from the `*-agent` variant vendors discussed above. PR #125 also added `when` clauses (`config.opencodego.enabled` / `config.opencodezen.enabled`) to the contributions but did **not** reintroduce `managementCommand`, so the native BYOK group flow from this PR stays intact. --- diff --git a/docs/issues/58-20260811-pr125-agents-window-byok-bridge.md b/docs/issues/58-20260811-pr125-agents-window-byok-bridge.md index 48493f5..04e4263 100644 --- a/docs/issues/58-20260811-pr125-agents-window-byok-bridge.md +++ b/docs/issues/58-20260811-pr125-agents-window-byok-bridge.md @@ -35,10 +35,10 @@ This PR fixes both: it auto-enables the two VS Code core settings the Agents win VS Code โ‰ฅ1.129 runs the **Agents window in a separate agent host process** and keeps the two mechanisms this feature depends on **off by default**: -| Setting | Role | Default | -| ------- | ---- | ------- | -| `chat.agentHost.byokModels.enabled` | Experimental BYOK language-model bridge that mirrors extension BYOK models into agent-host sessions (VS Code 1.129+) | `false` | -| `extensions.supportAgentsWindow` | The ONLY way a code extension is allowed to run in the Agents window (sessions window) process | unset per extension | +| Setting | Role | Default | +| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------- | +| `chat.agentHost.byokModels.enabled` | Experimental BYOK language-model bridge that mirrors extension BYOK models into agent-host sessions (VS Code 1.129+) | `false` | +| `extensions.supportAgentsWindow` | The ONLY way a code extension is allowed to run in the Agents window (sessions window) process | unset per extension | Without `extensions.supportAgentsWindow.`, VS Code disables any extension with a `main` entry in the Agents window process (`canExecuteOnSessionsWindow` returns `false`), so the extension's `languageModelChatProviders` vendors are never registered there โ€” neither the model picker nor the "+ Add Models" list can show OpenCode Go/Zen. @@ -106,31 +106,31 @@ Placeholders for temporary working copies live under `tmp/`, are never committed ## Files Changed -| File | Change | -| ---- | ------ | -| `src/providerEnablement.ts` | **NEW** โ€” pure helper `providerEnabledSetting()` mapping vendor โ†’ full root config key (base vendor for agent variants); documents the section-scoped read pitfall | -| `src/test/providerEnablement.test.ts` | **NEW** โ€” regression tests: base vendors map to their own setting, agent variants follow base, keys are full root-configuration keys (10 tests) | -| `src/extension.ts` | Auto-enable/revert `chat.agentHost.byokModels.enabled` + `extensions.supportAgentsWindow`; gated provider registration; `toggleProviderEnabled()`; `ensureAgentsWindowSupport()` / `revertAgentsWindowSupport()`; `isModernAgentHostVscode()`; `manage()` uses `providerEnabledSetting` and drops the missing-key early-return; `warmModelPickerMetadata()` reads enabled from root config | -| `package.json` | New `opencodego.enabled` / `opencodezen.enabled` / `opencodego.autoEnableAgentsWindow` settings; `when` clauses on vendor contributions; `Remove/Re-add Provider in Language Models` commands; lint/format script updates (markdownlint-cli2 rename, `--ignore-path .gitignore`) | -| `eslint.config.mjs` | Uses `readGitignorePatterns()` from the shared module | -| `scripts/gitignore-patterns.mjs` | **NEW** โ€” shared gitignore-pattern reader for ESLint | -| `.markdownlint-cli2.jsonc` | **NEW** โ€” replaces `.markdownlint.json`, adds `"gitignore": true` | -| `.markdownlint.json` | Deleted (renamed to `.markdownlint-cli2.jsonc`) | -| `.gitignore` | Added `tmp/` | -| `.vscodeignore` | `tmp/**`; `.markdownlint.json` โ†’ `.markdownlint-cli2.jsonc` | -| `CHANGELOG.md` | `[Unreleased] โ†’ Added` entries (remove providers + Agents window support) | -| `README.md` | Agents Window section rewritten for the VS Code โ‰ฅ1.129 BYOK bridge + auto-enable flow | +| File | Change | +| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `src/providerEnablement.ts` | **NEW** โ€” pure helper `providerEnabledSetting()` mapping vendor โ†’ full root config key (base vendor for agent variants); documents the section-scoped read pitfall | +| `src/test/providerEnablement.test.ts` | **NEW** โ€” regression tests: base vendors map to their own setting, agent variants follow base, keys are full root-configuration keys (10 tests) | +| `src/extension.ts` | Auto-enable/revert `chat.agentHost.byokModels.enabled` + `extensions.supportAgentsWindow`; gated provider registration; `toggleProviderEnabled()`; `ensureAgentsWindowSupport()` / `revertAgentsWindowSupport()`; `isModernAgentHostVscode()`; `manage()` uses `providerEnabledSetting` and drops the missing-key early-return; `warmModelPickerMetadata()` reads enabled from root config | +| `package.json` | New `opencodego.enabled` / `opencodezen.enabled` / `opencodego.autoEnableAgentsWindow` settings; `when` clauses on vendor contributions; `Remove/Re-add Provider in Language Models` commands; lint/format script updates (markdownlint-cli2 rename, `--ignore-path .gitignore`) | +| `eslint.config.mjs` | Uses `readGitignorePatterns()` from the shared module | +| `scripts/gitignore-patterns.mjs` | **NEW** โ€” shared gitignore-pattern reader for ESLint | +| `.markdownlint-cli2.jsonc` | **NEW** โ€” replaces `.markdownlint.json`, adds `"gitignore": true` | +| `.markdownlint.json` | Deleted (renamed to `.markdownlint-cli2.jsonc`) | +| `.gitignore` | Added `tmp/` | +| `.vscodeignore` | `tmp/**`; `.markdownlint.json` โ†’ `.markdownlint-cli2.jsonc` | +| `CHANGELOG.md` | `[Unreleased] โ†’ Added` entries (remove providers + Agents window support) | +| `README.md` | Agents Window section rewritten for the VS Code โ‰ฅ1.129 BYOK bridge + auto-enable flow | --- ## Configuration Summary (new/changed) -| Setting | Default | Purpose | -| ------- | ------- | ------- | -| `opencodego.enabled` | `true` | Register the OpenCode Go provider; `false` removes it from Language Models and all pickers (until reload) | -| `opencodezen.enabled` | `true` | Register the OpenCode Zen provider; `false` removes it from Language Models and all pickers (until reload) | -| `opencodego.autoEnableAgentsWindow` | `true` | Auto-manage `chat.agentHost.byokModels.enabled` + `extensions.supportAgentsWindow` when `agentsWindow` is on; `false` leaves them to the user | -| `opencodego.agentsWindow` | `true` | Master switch for Agents window support (existing) | +| Setting | Default | Purpose | +| ----------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `opencodego.enabled` | `true` | Register the OpenCode Go provider; `false` removes it from Language Models and all pickers (until reload) | +| `opencodezen.enabled` | `true` | Register the OpenCode Zen provider; `false` removes it from Language Models and all pickers (until reload) | +| `opencodego.autoEnableAgentsWindow` | `true` | Auto-manage `chat.agentHost.byokModels.enabled` + `extensions.supportAgentsWindow` when `agentsWindow` is on; `false` leaves them to the user | +| `opencodego.agentsWindow` | `true` | Master switch for Agents window support (existing) | New commands: diff --git a/docs/issues/59-20260811-pr126-reasoning-history-guard-tests.md b/docs/issues/59-20260811-pr126-reasoning-history-guard-tests.md index 3164263..f522c41 100644 --- a/docs/issues/59-20260811-pr126-reasoning-history-guard-tests.md +++ b/docs/issues/59-20260811-pr126-reasoning-history-guard-tests.md @@ -27,7 +27,9 @@ The branch also carries the same `tmp/` ignore + `.gitignore`-aware tooling comm `thinkingPartText()` in `src/extension.ts` (added by PR #123) used: ```ts -if (!(part instanceof vscode.LanguageModelThinkingPart)) { return ""; } +if (!(part instanceof vscode.LanguageModelThinkingPart)) { + return ""; +} ``` `LanguageModelThinkingPart` is a proposed VS Code API. The extension's `engines.vscode: ^1.125.0` guarantees it is present at runtime (API shipped August 2025, VS Code PR #259939), and the `src/vscode.proposed.languageModelThinkingPart.d.ts` augmentation already documents the `typeof ... === 'function'` guard as the recommended pattern. `src/streaming.ts` follows that pattern via `thinkingPartConstructor` (lines 261โ€“265), but the `extension.ts` site added by #123 did not. @@ -68,9 +70,9 @@ Two exported functions extracted from `extension.ts`: Covers: -| Function | Cases | -| -------- | ----- | -| `thinkingTextFromValue()` | plain strings, string chunk arrays, non-string chunks dropped, empty string, empty array | +| Function | Cases | +| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `thinkingTextFromValue()` | plain strings, string chunk arrays, non-string chunks dropped, empty string, empty array | | `shouldEchoThinkingHistory()` | `undefined` (no echo), DeepSeek/Kimi/GLM/Qwen/MiniMax (echo), Gemini (echo), MiMo/GPT/Claude (no echo, preserves issue #38 carve-out), unknown model id (no echo) | ### 4. Tooling commits (shared with PR #125) @@ -88,28 +90,28 @@ After #125 merged, these auto-reconcile to no-ops on merge. ### Author-reported (PR description) -| Check | Result | -| ----- | ------ | -| `npm run compile` | โœ… PASS | -| `npm test` | โœ… 177/177 pass (was 161/161; +16 new) | -| `npm run lint` | โœ… PASS (markdownlint now ignores `tmp/` via `gitignore: true`) | -| GitHub PR checks | โœ… passing | +| Check | Result | +| ----------------- | --------------------------------------------------------------- | +| `npm run compile` | โœ… PASS | +| `npm test` | โœ… 177/177 pass (was 161/161; +16 new) | +| `npm run lint` | โœ… PASS (markdownlint now ignores `tmp/` via `gitignore: true`) | +| GitHub PR checks | โœ… passing | ### Maintainer-side verification (this review, 2026-08-11) Performed locally against `main` at `3001d68` (post-#125): -| Check | Result | -| ----- | ------ | -| `gh pr view 126 --json mergeable,mergeStateStatus` | `MERGEABLE` + `CLEAN` โœ… | -| `git fetch origin pull/126/head:pr-126 && git log --oneline main..pr-126` | 3 commits, 2 of which are tooling duplicates of #125 โœ… | -| `git merge-tree $(git merge-base main pr-126) main pr-126` (conflict count) | **0 conflicts** โœ… | -| Simulated `git merge --no-ff --no-commit pr-126` on clean `main` | Auto-merge of `package.json` + `src/extension.ts`, clean working tree โœ… | -| Post-merge `reasoningHistory.ts` presence | single definition, no duplication โœ… | -| Post-merge `shouldEchoThinkingHistory` definition count | 1 (in `reasoningHistory.ts` only), removed from `extension.ts` โœ… | -| Post-merge `typeof` guard in `extension.ts` | present at the `thinkingPartText()` site โœ… | -| Tooling from #125 preserved post-merge | `.markdownlint-cli2.jsonc`, `scripts/gitignore-patterns.mjs`, `tmp/` in `.gitignore` all intact โœ… | -| Actual merge diff | `extension.ts` โˆ’30/+36, `reasoningHistory.ts` +43, `reasoningHistory.test.ts` +48 (only reasoning changes, no tooling noise) โœ… | +| Check | Result | +| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `gh pr view 126 --json mergeable,mergeStateStatus` | `MERGEABLE` + `CLEAN` โœ… | +| `git fetch origin pull/126/head:pr-126 && git log --oneline main..pr-126` | 3 commits, 2 of which are tooling duplicates of #125 โœ… | +| `git merge-tree $(git merge-base main pr-126) main pr-126` (conflict count) | **0 conflicts** โœ… | +| Simulated `git merge --no-ff --no-commit pr-126` on clean `main` | Auto-merge of `package.json` + `src/extension.ts`, clean working tree โœ… | +| Post-merge `reasoningHistory.ts` presence | single definition, no duplication โœ… | +| Post-merge `shouldEchoThinkingHistory` definition count | 1 (in `reasoningHistory.ts` only), removed from `extension.ts` โœ… | +| Post-merge `typeof` guard in `extension.ts` | present at the `thinkingPartText()` site โœ… | +| Tooling from #125 preserved post-merge | `.markdownlint-cli2.jsonc`, `scripts/gitignore-patterns.mjs`, `tmp/` in `.gitignore` all intact โœ… | +| Actual merge diff | `extension.ts` โˆ’30/+36, `reasoningHistory.ts` +43, `reasoningHistory.test.ts` +48 (only reasoning changes, no tooling noise) โœ… | ### Residual note (out of scope for this PR) diff --git a/docs/issues/60-20260811-release-0-5-2-plan.md b/docs/issues/60-20260811-release-0-5-2-plan.md index 8773d20..1ad23b7 100644 --- a/docs/issues/60-20260811-release-0-5-2-plan.md +++ b/docs/issues/60-20260811-release-0-5-2-plan.md @@ -28,24 +28,24 @@ The cut also bundles the Agents window + provider management work (#122 / #125, ### Shipped fixes (already on `main`) -| Area | PR | Change | Issue doc | -| ---- | -- | ------ | --------- | -| Thinking | [#123](https://github.com/ltmoerdani/opencode-copilot-chat/pull/123) | Multi-turn `reasoning_content` echo for DeepSeek V4 + OpenAI-compatible reasoning models | `55-โ€ฆ` | -| VS Code | [#124](https://github.com/ltmoerdani/opencode-copilot-chat/pull/124) | Dropped `managementCommand` so "+ Add Models" + context-menu actions work (#121) | `57-โ€ฆ` | -| Agents | [#125](https://github.com/ltmoerdani/opencode-copilot-chat/pull/125) | OpenCode Go/Zen in the Agents window + provider remove/re-add (#122) | `58-โ€ฆ` | -| Vision | [#120](https://github.com/ltmoerdani/opencode-copilot-chat/pull/120) | Vision proxy description cache + whole-conversation mode (#119) | `56-โ€ฆ` | +| Area | PR | Change | Issue doc | +| -------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | --------- | +| Thinking | [#123](https://github.com/ltmoerdani/opencode-copilot-chat/pull/123) | Multi-turn `reasoning_content` echo for DeepSeek V4 + OpenAI-compatible reasoning models | `55-โ€ฆ` | +| VS Code | [#124](https://github.com/ltmoerdani/opencode-copilot-chat/pull/124) | Dropped `managementCommand` so "+ Add Models" + context-menu actions work (#121) | `57-โ€ฆ` | +| Agents | [#125](https://github.com/ltmoerdani/opencode-copilot-chat/pull/125) | OpenCode Go/Zen in the Agents window + provider remove/re-add (#122) | `58-โ€ฆ` | +| Vision | [#120](https://github.com/ltmoerdani/opencode-copilot-chat/pull/120) | Vision proxy description cache + whole-conversation mode (#119) | `56-โ€ฆ` | ### Pending inputs (merge before cut) -| Area | PR | Status | Notes | -| ---- | -- | ------ | ----- | -| Testing | [#126](https://github.com/ltmoerdani/opencode-copilot-chat/pull/126) | open, `MERGEABLE` + `CLEAN` | `typeof` guard + reasoning-history unit tests. Follow-up on #123. See `59-โ€ฆ` | -| Deps (patch) | [#91](https://github.com/ltmoerdani/opencode-copilot-chat/pull/91) | open (dependabot) | `@types/node` 26.1.0 โ†’ 26.1.2. Patch bump, low risk. | +| Area | PR | Status | Notes | +| ------------ | -------------------------------------------------------------------- | --------------------------- | ---------------------------------------------------------------------------- | +| Testing | [#126](https://github.com/ltmoerdani/opencode-copilot-chat/pull/126) | open, `MERGEABLE` + `CLEAN` | `typeof` guard + reasoning-history unit tests. Follow-up on #123. See `59-โ€ฆ` | +| Deps (patch) | [#91](https://github.com/ltmoerdani/opencode-copilot-chat/pull/91) | open (dependabot) | `@types/node` 26.1.0 โ†’ 26.1.2. Patch bump, low risk. | ### Deferred (not in 0.5.2) -| Area | PR | Reason | -| ---- | -- | ------ | +| Area | PR | Reason | +| ------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Deps (major) | [#90](https://github.com/ltmoerdani/opencode-copilot-chat/pull/90) | TypeScript 6 โ†’ 7 is a major toolchain bump. Needs a separate local build verification pass and a clean compile check before landing. Cut in a later minor release. | --- @@ -145,13 +145,13 @@ git push origin main --follow-tags ## Risk Assessment -| Risk | Likelihood | Mitigation | -| ---- | ---------- | ---------- | -| #126 merge introduces unexpected conflict | Low | Local simulation already confirmed 0 conflicts, tooling commits auto-reconcile with #125 | -| `@types/node` patch (#91) breaks build | Very low | Patch bump, CI runs on the PR itself | -| `npm run compile` fails after merges | Low | All PRs report clean compile; gate is blocking before publish | -| DeepSeek fix does not actually resolve the 400 | Low | Fix verified locally in #123 review; family gating preserves the MiMo carve-out | -| Publish pipeline fails | Low | `vsce publish` is the standard path; PAT/login must be current | +| Risk | Likelihood | Mitigation | +| ---------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------- | +| #126 merge introduces unexpected conflict | Low | Local simulation already confirmed 0 conflicts, tooling commits auto-reconcile with #125 | +| `@types/node` patch (#91) breaks build | Very low | Patch bump, CI runs on the PR itself | +| `npm run compile` fails after merges | Low | All PRs report clean compile; gate is blocking before publish | +| DeepSeek fix does not actually resolve the 400 | Low | Fix verified locally in #123 review; family gating preserves the MiMo carve-out | +| Publish pipeline fails | Low | `vsce publish` is the standard path; PAT/login must be current | --- From 5246434710e1042ca1a067017f630c2c28d060d7 Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Wed, 12 Aug 2026 07:47:13 +0500 Subject: [PATCH 14/18] chore(eslint): allow @ts-expect-error for proposed-API workarounds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no-warning-comments banned @ts-expect-error repo-wide alongside todo/fixme markers. This extension leans on VS Code proposed APIs (chatProvider.d.ts, languageModelThinkingPart.d.ts) whose type gaps are real โ€” a future contributor needs a clean escape hatch. @ts-expect-error has an expiry: the error resurfaces the moment the API lands, so it cannot silently rot. The never-expiring alternative stays banned. --- eslint.config.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 9c89e40..bba90d0 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -94,10 +94,14 @@ export default defineConfig([ { rules: { // Zero tolerance: no unfinished-work marker comments may ever be committed. + // `@ts-expect-error` is intentionally allowed: this extension leans on + // VS Code proposed APIs whose type gaps are real, and unlike its + // never-expiring alternative it surfaces the error again the moment the + // API lands. "no-warning-comments": [ "error", { - terms: ["todo", "fixme", "xxx", "hack", "@ts-ignore", "@ts-expect-error"], + terms: ["todo", "fixme", "xxx", "hack", "@ts-ignore"], location: "anywhere", }, ], From 22e04b7d6374849c2586162f3e4f6d82cf23e06c Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Wed, 12 Aug 2026 07:47:19 +0500 Subject: [PATCH 15/18] style(tests): drop the void prefix from describe/it/test calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no-floating-promises is off for *.test.* (node:test's runner handles the returned promises), so the void ceremony is no longer required โ€” remove it from all 217 call sites so the test files match the narrative and read naturally. --- src/test/apiKeyResolution.test.ts | 8 +-- src/test/goUsageTracker.test.ts | 88 ++++++++++++++-------------- src/test/imageNormalizer.test.ts | 12 ++-- src/test/metadata.test.ts | 44 +++++++------- src/test/modelLimits.test.ts | 12 ++-- src/test/modelNames.test.ts | 8 +-- src/test/providerEnablement.test.ts | 6 +- src/test/reasoningHistory.test.ts | 18 +++--- src/test/responsesRequest.test.ts | 20 +++---- src/test/retry.test.ts | 48 +++++++-------- src/test/thinking.test.ts | 88 ++++++++++++++-------------- src/test/tokenEstimate.test.ts | 6 +- src/test/toolCallAccumulator.test.ts | 36 ++++++------ src/test/usageProfile.test.ts | 24 ++++---- src/test/visionProxy.test.ts | 40 ++++++------- 15 files changed, 229 insertions(+), 229 deletions(-) diff --git a/src/test/apiKeyResolution.test.ts b/src/test/apiKeyResolution.test.ts index edaa984..fbee1fc 100644 --- a/src/test/apiKeyResolution.test.ts +++ b/src/test/apiKeyResolution.test.ts @@ -2,16 +2,16 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { resolveResponseApiKey } from "../apiKeyResolution.js"; -void describe("resolveResponseApiKey", () => { - void it("prefers the request's native BYOK configuration", () => { +describe("resolveResponseApiKey", () => { + it("prefers the request's native BYOK configuration", () => { assert.equal(resolveResponseApiKey("configured", "registered", "stored"), "configured"); }); - void it("uses the key captured while registering the selected model", () => { + it("uses the key captured while registering the selected model", () => { assert.equal(resolveResponseApiKey(undefined, "registered", "stored"), "registered"); }); - void it("falls back to SecretStorage after an extension-host cold start", () => { + it("falls back to SecretStorage after an extension-host cold start", () => { assert.equal(resolveResponseApiKey(undefined, undefined, "stored"), "stored"); }); }); diff --git a/src/test/goUsageTracker.test.ts b/src/test/goUsageTracker.test.ts index d646bbc..4b5f50a 100644 --- a/src/test/goUsageTracker.test.ts +++ b/src/test/goUsageTracker.test.ts @@ -121,7 +121,7 @@ moduleResolver._resolveFilename = function (request: string, parent: unknown, .. // properly awaited by the test runner before any child tests execute. // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• -void describe("goUsageTracker", () => { +describe("goUsageTracker", () => { // โ”€โ”€ Bootstrap: dynamically import module under test โ”€โ”€ // (vscode mock is already installed via Module._resolveFilename above) @@ -135,8 +135,8 @@ void describe("goUsageTracker", () => { // estimateCost() // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - void describe("estimateCost()", () => { - void it("uses bundled snapshot pricing for a known model (qwen3.6-plus)", () => { + describe("estimateCost()", () => { + it("uses bundled snapshot pricing for a known model (qwen3.6-plus)", () => { const cost = estimateCost("qwen3.6-plus", 100, 50, 10); // billablePrompt = max(0, 100-10) = 90 // pricing: { input: 0.50, output: 3.00, cache_read: 0.05 } @@ -146,7 +146,7 @@ void describe("goUsageTracker", () => { assert.equal(cost, 0.0001955); }); - void it("uses bundled snapshot pricing for deepseek-v4-flash", () => { + it("uses bundled snapshot pricing for deepseek-v4-flash", () => { const cost = estimateCost("deepseek-v4-flash", 1000, 500, 200); // billablePrompt = 800 // pricing: { input: 0.14, output: 0.28, cache_read: 0.003 } @@ -156,12 +156,12 @@ void describe("goUsageTracker", () => { assert.equal(cost, 0.0002526); }); - void it("returns 0 for an unknown model with no resolver", () => { + it("returns 0 for an unknown model with no resolver", () => { const cost = estimateCost("nonexistent-model-v99", 100, 50, 0); assert.equal(cost, 0); }); - void it("prefers externalCost over the bundled table", () => { + it("prefers externalCost over the bundled table", () => { const external: ModelCost = { input: 1.0, output: 2.0, cache_read: 0.1 }; const cost = estimateCost("qwen3.6-plus", 100, 50, 10, external); // billablePrompt = 90 @@ -171,7 +171,7 @@ void describe("goUsageTracker", () => { assert.equal(cost, 0.000191); }); - void it("prefers liveCostResolver over the bundled table when externalCost absent", () => { + it("prefers liveCostResolver over the bundled table when externalCost absent", () => { const resolver = (id: string): ModelCost | undefined => (id === "custom-model" ? { input: 2.0, output: 4.0 } : undefined); const cost = estimateCost("custom-model", 100, 50, 0, undefined, resolver); // billablePrompt = 100 @@ -180,7 +180,7 @@ void describe("goUsageTracker", () => { assert.equal(cost, 0.0004); }); - void it("falls back to bundled table when resolver returns undefined", () => { + it("falls back to bundled table when resolver returns undefined", () => { const resolver = (): ModelCost | undefined => undefined; const cost = estimateCost("qwen3.6-plus", 100, 50, 0, undefined, resolver); // 100 * 0.5/1M = 0.00005 @@ -189,7 +189,7 @@ void describe("goUsageTracker", () => { assert.ok(Math.abs(cost - 0.0002) < 1e-12, `expected ~0.0002, got ${String(cost)}`); }); - void it("subtracts cached tokens from prompt tokens for billing", () => { + it("subtracts cached tokens from prompt tokens for billing", () => { const cost = estimateCost("qwen3.6-plus", 100, 50, 40); // billablePrompt = 60 // 60 * 0.5/1M = 0.00003 @@ -199,7 +199,7 @@ void describe("goUsageTracker", () => { assert.ok(Math.abs(cost - 0.000182) < 1e-12, `expected ~0.000182, got ${String(cost)}`); }); - void it("handles all-cached requests (billable prompt = 0)", () => { + it("handles all-cached requests (billable prompt = 0)", () => { const cost = estimateCost("qwen3.6-plus", 100, 50, 200); // billablePrompt = max(0, 100-200) = 0 // 0 * 0.5/1M = 0 @@ -209,12 +209,12 @@ void describe("goUsageTracker", () => { assert.ok(Math.abs(cost - 0.00016) < 1e-12, `expected ~0.00016, got ${String(cost)}`); }); - void it("handles zero tokens gracefully", () => { + it("handles zero tokens gracefully", () => { const cost = estimateCost("qwen3.6-plus", 0, 0, 0); assert.equal(cost, 0); }); - void it("uses explicit cache_read when provided in pricing", () => { + it("uses explicit cache_read when provided in pricing", () => { const external: ModelCost = { input: 1.0, output: 2.0, cache_read: 0.5 }; const cost = estimateCost("any-model", 200, 100, 50, external); // billablePrompt = 150 @@ -224,7 +224,7 @@ void describe("goUsageTracker", () => { assert.equal(cost, 0.000375); }); - void it("falls back to input * 0.1 when cache_read is missing", () => { + it("falls back to input * 0.1 when cache_read is missing", () => { const external: ModelCost = { input: 2.0, output: 4.0 }; // no cache_read const cost = estimateCost("any-model", 100, 50, 10, external); // billablePrompt = 90 @@ -239,11 +239,11 @@ void describe("goUsageTracker", () => { // GoUsageTracker // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - void describe("GoUsageTracker", () => { + describe("GoUsageTracker", () => { // โ”€โ”€ record() โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - void describe("record()", () => { - void it("accumulates cost for the same sessionId", () => { + describe("record()", () => { + it("accumulates cost for the same sessionId", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record(makeSummary({ sessionId: "s1", promptTokens: 100, completionTokens: 50, cachedTokens: 0 })); @@ -261,21 +261,21 @@ void describe("goUsageTracker", () => { assert.equal(session.completionTokens, 150); }); - void it("skips records when providerDisplayName does not contain 'go'", () => { + it("skips records when providerDisplayName does not contain 'go'", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record(makeSummary({ providerDisplayName: "OpenCode Zen", sessionId: "s1" })); assert.equal(tracker.getCurrentSessionCost(), undefined); }); - void it("skips records when prompt+completion tokens are zero", () => { + it("skips records when prompt+completion tokens are zero", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record(makeSummary({ sessionId: "s1", promptTokens: 0, completionTokens: 0, cachedTokens: 0 })); assert.equal(tracker.getCurrentSessionCost(), undefined); }); - void it("creates separate entries for different sessionIds", () => { + it("creates separate entries for different sessionIds", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record(makeSummary({ sessionId: "s1", promptTokens: 100, completionTokens: 50 })); tracker.record(makeSummary({ sessionId: "s2", promptTokens: 10, completionTokens: 5 })); @@ -283,7 +283,7 @@ void describe("goUsageTracker", () => { assert.equal(tracker.getRecentSessionCosts(5).length, 2); }); - void it("accepts an externalCost override", () => { + it("accepts an externalCost override", () => { const tracker = new GoUsageTracker(createMockContext()); const externalCost: ModelCost = { input: 10, output: 20 }; tracker.record(makeSummary({ sessionId: "s1", promptTokens: 100, completionTokens: 50, cachedTokens: 0 }), externalCost); @@ -293,7 +293,7 @@ void describe("goUsageTracker", () => { assert.equal(session?.cost, 0.002); }); - void it("delegates to costResolver when no externalCost is passed", () => { + it("delegates to costResolver when no externalCost is passed", () => { const resolver = (id: string): ModelCost | undefined => (id === "custom-resolved" ? { input: 5, output: 10 } : undefined); const tracker = new GoUsageTracker(createMockContext(), undefined, resolver); @@ -306,7 +306,7 @@ void describe("goUsageTracker", () => { assert.equal(session?.cost, 0.001); }); - void it("persists data to globalState after record()", () => { + it("persists data to globalState after record()", () => { const context = createMockContext(); const tracker = new GoUsageTracker(context); @@ -323,8 +323,8 @@ void describe("goUsageTracker", () => { // โ”€โ”€ getCurrentSessionCost() โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - void describe("getCurrentSessionCost()", () => { - void it("returns the most recently active session", () => { + describe("getCurrentSessionCost()", () => { + it("returns the most recently active session", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record(makeSummary({ sessionId: "old", promptTokens: 1, completionTokens: 0 })); // Ensure distinct timestamps for deterministic ordering @@ -335,7 +335,7 @@ void describe("goUsageTracker", () => { assert.equal(tracker.getCurrentSessionCost()?.sessionId, "new"); }); - void it("returns the aggregated cost for the most recent session", () => { + it("returns the aggregated cost for the most recent session", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record(makeSummary({ sessionId: "s1", promptTokens: 100, completionTokens: 50, cachedTokens: 0 })); tracker.record(makeSummary({ sessionId: "s2", promptTokens: 200, completionTokens: 100, cachedTokens: 0 })); @@ -349,7 +349,7 @@ void describe("goUsageTracker", () => { assert.equal(session.requests, 2); }); - void it("returns undefined when no sessions have been recorded", () => { + it("returns undefined when no sessions have been recorded", () => { const tracker = new GoUsageTracker(createMockContext()); assert.equal(tracker.getCurrentSessionCost(), undefined); }); @@ -357,7 +357,7 @@ void describe("goUsageTracker", () => { // โ”€โ”€ getRecentSessionCosts() โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - void describe("getRecentSessionCosts()", () => { + describe("getRecentSessionCosts()", () => { /** Ensure each record gets a distinct timestamp for deterministic ordering. */ function recordWithDistinctTimestamp(tracker: GoUsageTrackerInstance, sessionId: string): void { tracker.record(makeSummary({ sessionId, promptTokens: 1, completionTokens: 0 })); @@ -365,7 +365,7 @@ void describe("goUsageTracker", () => { while (Date.now() === t) {} // wait for next millisecond } - void it("returns sessions ordered by lastActivity descending", () => { + it("returns sessions ordered by lastActivity descending", () => { const tracker = new GoUsageTracker(createMockContext()); recordWithDistinctTimestamp(tracker, "a"); recordWithDistinctTimestamp(tracker, "b"); @@ -378,7 +378,7 @@ void describe("goUsageTracker", () => { assert.equal(sessions[2].sessionId, "a"); }); - void it("respects the limit parameter", () => { + it("respects the limit parameter", () => { const tracker = new GoUsageTracker(createMockContext()); for (let i = 0; i < 10; i++) { tracker.record(makeSummary({ sessionId: `s${String(i)}`, promptTokens: 1, completionTokens: 0 })); @@ -390,7 +390,7 @@ void describe("goUsageTracker", () => { assert.equal(tracker.getRecentSessionCosts(100).length, 10); }); - void it("returns empty array when no sessions exist", () => { + it("returns empty array when no sessions exist", () => { const tracker = new GoUsageTracker(createMockContext()); assert.deepEqual(tracker.getRecentSessionCosts(), []); }); @@ -398,8 +398,8 @@ void describe("goUsageTracker", () => { // โ”€โ”€ State restoration from globalState โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - void describe("state restoration from globalState", () => { - void it("restores entries and session costs from stored state", () => { + describe("state restoration from globalState", () => { + it("restores entries and session costs from stored state", () => { const now = Date.now(); const initial: Record = { "opencodego.usageLog.v1": [ @@ -436,7 +436,7 @@ void describe("goUsageTracker", () => { assert.equal(session.completionTokens, 75); }); - void it("filters invalid entries during restore", () => { + it("filters invalid entries during restore", () => { const initial: Record = { "opencodego.usageLog.v1": [ { timestamp: Date.now(), modelId: "valid", cost: 0.1, promptTokens: 10, completionTokens: 5, cachedTokens: 0, sessionId: "s1" }, @@ -459,7 +459,7 @@ void describe("goUsageTracker", () => { assert.equal(sessions.length, 1); }); - void it("starts clean when no state is stored", () => { + it("starts clean when no state is stored", () => { const tracker = new GoUsageTracker(createMockContext()); assert.equal(tracker.getCurrentSessionCost(), undefined); assert.deepEqual(tracker.getRecentSessionCosts(), []); @@ -468,12 +468,12 @@ void describe("goUsageTracker", () => { // โ”€โ”€ Pruning behavior โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - void describe("pruning behavior", () => { + describe("pruning behavior", () => { afterEach(() => { mock.timers.reset(); }); - void it("removes idle sessions (older than 2h) on record()", () => { + it("removes idle sessions (older than 2h) on record()", () => { mock.timers.enable({ apis: ["Date"] }); const baseTime = 1_000_000_000_000; mock.timers.setTime(baseTime); @@ -493,7 +493,7 @@ void describe("goUsageTracker", () => { assert.equal(sessions.length, 1); }); - void it("removes multiple idle sessions at once", () => { + it("removes multiple idle sessions at once", () => { mock.timers.enable({ apis: ["Date"] }); mock.timers.setTime(1_000_000_000_000); @@ -510,7 +510,7 @@ void describe("goUsageTracker", () => { assert.equal(tracker.getCurrentSessionCost()?.sessionId, "s3"); }); - void it("caps at MAX_SESSIONS (50) and removes oldest", () => { + it("caps at MAX_SESSIONS (50) and removes oldest", () => { const tracker = new GoUsageTracker(createMockContext()); // Create 51 sessions for (let i = 0; i < 51; i++) { @@ -535,8 +535,8 @@ void describe("goUsageTracker", () => { // โ”€โ”€ Edge cases โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - void describe("edge cases", () => { - void it("handles missing sessionId (no session cost tracked)", () => { + describe("edge cases", () => { + it("handles missing sessionId (no session cost tracked)", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record(makeSummary({ sessionId: undefined, promptTokens: 100, completionTokens: 50 })); @@ -544,7 +544,7 @@ void describe("goUsageTracker", () => { assert.equal(tracker.getRecentSessionCosts().length, 0); }); - void it("handles unknown modelId (cost = 0)", () => { + it("handles unknown modelId (cost = 0)", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record( makeSummary({ @@ -561,7 +561,7 @@ void describe("goUsageTracker", () => { assert.equal(session.requests, 1); }); - void it("handles record with only cached tokens (no prompt or completion)", () => { + it("handles record with only cached tokens (no prompt or completion)", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record(makeSummary({ sessionId: "s1", promptTokens: 0, completionTokens: 0, cachedTokens: 100 })); @@ -569,7 +569,7 @@ void describe("goUsageTracker", () => { assert.equal(tracker.getCurrentSessionCost(), undefined); }); - void it("handles record with only cached tokens but non-zero prompt", () => { + it("handles record with only cached tokens but non-zero prompt", () => { const tracker = new GoUsageTracker(createMockContext()); tracker.record(makeSummary({ sessionId: "s1", promptTokens: 50, completionTokens: 0, cachedTokens: 50 })); @@ -581,7 +581,7 @@ void describe("goUsageTracker", () => { assert.equal(session.cost, 0.0000025); }); - void it("handles multiple records in the same session with reset in between", () => { + it("handles multiple records in the same session with reset in between", () => { const context = createMockContext(); const tracker1 = new GoUsageTracker(context); tracker1.record(makeSummary({ sessionId: "shared", promptTokens: 100, completionTokens: 50, cachedTokens: 0 })); diff --git a/src/test/imageNormalizer.test.ts b/src/test/imageNormalizer.test.ts index 8177a24..6e32e52 100644 --- a/src/test/imageNormalizer.test.ts +++ b/src/test/imageNormalizer.test.ts @@ -5,13 +5,13 @@ import { getImageDataUrlBase64Bytes, MAX_IMAGE_BASE64_BYTES, normalizeImageDataU const ONE_PIXEL_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="; -void describe("normalizeImageDataUrl", () => { - void it("keeps a small image unchanged", async () => { +describe("normalizeImageDataUrl", () => { + it("keeps a small image unchanged", async () => { const url = `data:image/png;base64,${ONE_PIXEL_PNG}`; assert.equal(await normalizeImageDataUrl(url), url); }); - void it("resizes an image that exceeds the CLI dimension limit", async () => { + it("resizes an image that exceeds the CLI dimension limit", async () => { const image = new PhotonImage(new Uint8Array(2_001 * 4).fill(255), 2_001, 1); try { const url = `data:image/png;base64,${Buffer.from(image.get_bytes()).toString("base64")}`; @@ -24,7 +24,7 @@ void describe("normalizeImageDataUrl", () => { } }); - void it("does not reject a large raw image when its normalized base64 payload fits", async () => { + it("does not reject a large raw image when its normalized base64 payload fits", async () => { const width = 750; const height = 1_000; const pixels = new Uint8Array(width * height * 4); @@ -50,12 +50,12 @@ void describe("normalizeImageDataUrl", () => { } }); - void it("passes non-data URLs through unchanged", async () => { + it("passes non-data URLs through unchanged", async () => { const url = "https://example.com/image.png"; assert.equal(await normalizeImageDataUrl(url), url); }); - void it("passes malformed image data through unchanged", async () => { + it("passes malformed image data through unchanged", async () => { const url = "data:image/png;base64,not-an-image"; assert.equal(await normalizeImageDataUrl(url), url); }); diff --git a/src/test/metadata.test.ts b/src/test/metadata.test.ts index 257e577..056b3e5 100644 --- a/src/test/metadata.test.ts +++ b/src/test/metadata.test.ts @@ -14,67 +14,67 @@ import { GO_VENDOR, ZEN_VENDOR } from "../providerTypes.js"; * These tests verify the bundled fallback metadata is correct even when the * live models.dev fetch is unavailable. */ -void describe("fallbackModelMetadata โ€” kimi-k2.7-code (issue #25)", () => { - void it("returns metadata for kimi-k2.7-code on GO_VENDOR", () => { +describe("fallbackModelMetadata โ€” kimi-k2.7-code (issue #25)", () => { + it("returns metadata for kimi-k2.7-code on GO_VENDOR", () => { const meta = fallbackModelMetadata("kimi-k2.7-code", GO_VENDOR); assert.ok(meta, "expected fallback metadata to be defined"); }); - void it("reports temperature: false (Moonshot rejects non-default temperature)", () => { + it("reports temperature: false (Moonshot rejects non-default temperature)", () => { const meta = fallbackModelMetadata("kimi-k2.7-code", GO_VENDOR); assert.equal(meta?.temperature, false); }); - void it("reports correct context/output limits (models.dev: 256000 / 262144)", () => { + it("reports correct context/output limits (models.dev: 256000 / 262144)", () => { const meta = fallbackModelMetadata("kimi-k2.7-code", GO_VENDOR); assert.equal(meta?.contextWindow, 256000); assert.equal(meta.maxOutputTokens, 262144); }); - void it("reports vision capability (models.dev attachment: true)", () => { + it("reports vision capability (models.dev attachment: true)", () => { const meta = fallbackModelMetadata("kimi-k2.7-code", GO_VENDOR); assert.equal(meta?.supportsVision, true); }); - void it("reports reasoning capability (supportsReasoning matches /^kimi-/i)", () => { + it("reports reasoning capability (supportsReasoning matches /^kimi-/i)", () => { const meta = fallbackModelMetadata("kimi-k2.7-code", GO_VENDOR); assert.equal(meta?.reasoning, true); }); }); -void describe("fallbackModelMetadata โ€” regression safety for other kimi models", () => { - void it("kimi-k2.6 does NOT report temperature: false (still accepts temperature)", () => { +describe("fallbackModelMetadata โ€” regression safety for other kimi models", () => { + it("kimi-k2.6 does NOT report temperature: false (still accepts temperature)", () => { const meta = fallbackModelMetadata("kimi-k2.6", GO_VENDOR); // temperature should be undefined (not false) so the request body still // includes the configured temperature for k2.6. assert.notEqual(meta?.temperature, false); }); - void it("kimi-k2.5 does NOT report temperature: false", () => { + it("kimi-k2.5 does NOT report temperature: false", () => { const meta = fallbackModelMetadata("kimi-k2.5", GO_VENDOR); assert.notEqual(meta?.temperature, false); }); }); -void describe("fallbackModelMetadata โ€” non-kimi models unaffected", () => { - void it("glm-5 does not report temperature: false", () => { +describe("fallbackModelMetadata โ€” non-kimi models unaffected", () => { + it("glm-5 does not report temperature: false", () => { const meta = fallbackModelMetadata("glm-5", GO_VENDOR); assert.notEqual(meta?.temperature, false); }); - void it("deepseek-v4-pro does not report temperature: false", () => { + it("deepseek-v4-pro does not report temperature: false", () => { const meta = fallbackModelMetadata("deepseek-v4-pro", GO_VENDOR); assert.notEqual(meta?.temperature, false); }); - void it("claude-opus-4-7 on ZEN does not report temperature: false", () => { + it("claude-opus-4-7 on ZEN does not report temperature: false", () => { const meta = fallbackModelMetadata("claude-opus-4-7", ZEN_VENDOR); assert.notEqual(meta?.temperature, false); }); }); -void describe("VISION_CAPABLE_MODELS", () => { - void it("includes known vision models (minimax-m2.7, kimi-k2.6, mimo-v2.5)", () => { +describe("VISION_CAPABLE_MODELS", () => { + it("includes known vision models (minimax-m2.7, kimi-k2.6, mimo-v2.5)", () => { assert.ok(VISION_CAPABLE_MODELS.has("minimax-m2.7")); assert.ok(VISION_CAPABLE_MODELS.has("kimi-k2.6")); assert.ok(VISION_CAPABLE_MODELS.has("mimo-v2.5")); @@ -82,21 +82,21 @@ void describe("VISION_CAPABLE_MODELS", () => { assert.ok(VISION_CAPABLE_MODELS.has("mimo-v2.5-pro")); }); - void it("does NOT include text-only models (deepseek-v4-flash, hy3-preview, big-pickle)", () => { + it("does NOT include text-only models (deepseek-v4-flash, hy3-preview, big-pickle)", () => { assert.ok(!VISION_CAPABLE_MODELS.has("deepseek-v4-flash")); assert.ok(!VISION_CAPABLE_MODELS.has("deepseek-v4-pro")); assert.ok(!VISION_CAPABLE_MODELS.has("hy3-preview")); assert.ok(!VISION_CAPABLE_MODELS.has("big-pickle")); }); - void it("is an exported Set", () => { + it("is an exported Set", () => { assert.ok(VISION_CAPABLE_MODELS instanceof Set); assert.ok(VISION_CAPABLE_MODELS.size > 10); }); }); -void describe("getContextSizeOptionsForModel โ€” Kimi context tiers (issue #87)", () => { - void it("offers 256K and the full window when Kimi has a larger context", () => { +describe("getContextSizeOptionsForModel โ€” Kimi context tiers (issue #87)", () => { + it("offers 256K and the full window when Kimi has a larger context", () => { const options = getContextSizeOptionsForModel("kimi-k3", { input: 3, output: 15 }, 1_048_576); assert.deepEqual( @@ -107,7 +107,7 @@ void describe("getContextSizeOptionsForModel โ€” Kimi context tiers (issue #87)" assert.equal(options[1].description, "Higher pricing"); }); - void it("recognizes the official short K3 model id", () => { + it("recognizes the official short K3 model id", () => { const options = getContextSizeOptionsForModel("k3", undefined, 1_000_000); assert.deepEqual( options?.map((option) => option.value), @@ -115,11 +115,11 @@ void describe("getContextSizeOptionsForModel โ€” Kimi context tiers (issue #87)" ); }); - void it("does not add a redundant tier to a 256K Kimi model", () => { + it("does not add a redundant tier to a 256K Kimi model", () => { assert.equal(getContextSizeOptionsForModel("kimi-k2.6", { input: 0.95, output: 4 }, 262_144), undefined); }); - void it("prefers explicit models.dev pricing tiers", () => { + it("prefers explicit models.dev pricing tiers", () => { const options = getContextSizeOptionsForModel( "kimi-k3", { diff --git a/src/test/modelLimits.test.ts b/src/test/modelLimits.test.ts index 8cdb753..65bd2cb 100644 --- a/src/test/modelLimits.test.ts +++ b/src/test/modelLimits.test.ts @@ -7,8 +7,8 @@ const metadata = { maxOutputTokens: 32_000, }; -void describe("calculateModelLimits", () => { - void it("uses a conservative registration budget when prompt size is unknown", () => { +describe("calculateModelLimits", () => { + it("uses a conservative registration budget when prompt size is unknown", () => { const limits = calculateModelLimits(metadata); assert.equal(limits.maxOutputTokens, 19_936); @@ -17,19 +17,19 @@ void describe("calculateModelLimits", () => { assert.equal(limits.advertisedMaxInputTokens, 91_808); }); - void it("caps output to the context remaining after the prompt and safety margin", () => { + it("caps output to the context remaining after the prompt and safety margin", () => { const limits = calculateModelLimits(metadata, { promptTokens: 70_000 }); assert.equal(limits.maxOutputTokens, 21_600); }); - void it("never restores a 4K minimum that would overflow a nearly full context", () => { + it("never restores a 4K minimum that would overflow a nearly full context", () => { const limits = calculateModelLimits(metadata, { promptTokens: 99_990 }); assert.equal(limits.maxOutputTokens, 1); }); - void it("honors context and output overrides without exceeding either", () => { + it("honors context and output overrides without exceeding either", () => { const limits = calculateModelLimits(metadata, { contextSize: 50_000, maxOutputTokens: 12_000, @@ -40,7 +40,7 @@ void describe("calculateModelLimits", () => { assert.equal(limits.maxOutputTokens, 10_800); }); - void it("keeps the issue #109 DeepSeek request below the real context limit", () => { + it("keeps the issue #109 DeepSeek request below the real context limit", () => { const limits = calculateModelLimits({ contextWindow: 1_048_576, maxOutputTokens: 384_000 }, { promptTokens: 604_839 }); assert.equal(limits.maxOutputTokens, 371_156); diff --git a/src/test/modelNames.test.ts b/src/test/modelNames.test.ts index 923ed44..4deb8e4 100644 --- a/src/test/modelNames.test.ts +++ b/src/test/modelNames.test.ts @@ -2,16 +2,16 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { formatModelName, providerModelDisplayName } from "../modelNames.js"; -void describe("provider model display names", () => { - void it("formats numeric model versions like the existing picker", () => { +describe("provider model display names", () => { + it("formats numeric model versions like the existing picker", () => { assert.equal(formatModelName("gpt-5-6-luna"), "Gpt 5.6 Luna"); }); - void it("includes the provider prefix by default", () => { + it("includes the provider prefix by default", () => { assert.equal(providerModelDisplayName("OpenCode Go", "kimi-k3"), "OpenCode Go / Kimi K3"); }); - void it("can hide the provider prefix without changing the model name", () => { + it("can hide the provider prefix without changing the model name", () => { assert.equal(providerModelDisplayName("OpenCode Zen", "kimi-k3", false), "Kimi K3"); }); }); diff --git a/src/test/providerEnablement.test.ts b/src/test/providerEnablement.test.ts index 1f2f04a..bf22bcb 100644 --- a/src/test/providerEnablement.test.ts +++ b/src/test/providerEnablement.test.ts @@ -3,17 +3,17 @@ import { test } from "node:test"; import { providerEnabledSetting } from "../providerEnablement"; import { AGENT_GO_VENDOR, AGENT_ZEN_VENDOR, GO_VENDOR, ZEN_VENDOR } from "../providerTypes"; -void test("providerEnabledSetting โ€” base vendors map to their own setting", () => { +test("providerEnabledSetting โ€” base vendors map to their own setting", () => { assert.equal(providerEnabledSetting(GO_VENDOR), "opencodego.enabled"); assert.equal(providerEnabledSetting(ZEN_VENDOR), "opencodezen.enabled"); }); -void test("providerEnabledSetting โ€” agent-host variants follow their base vendor", () => { +test("providerEnabledSetting โ€” agent-host variants follow their base vendor", () => { assert.equal(providerEnabledSetting(AGENT_GO_VENDOR), "opencodego.enabled"); assert.equal(providerEnabledSetting(AGENT_ZEN_VENDOR), "opencodezen.enabled"); }); -void test("providerEnabledSetting โ€” keys are full root-configuration keys (regression: #125 review)", () => { +test("providerEnabledSetting โ€” keys are full root-configuration keys (regression: #125 review)", () => { // Section-scoped reads (getConfiguration("opencodego")) resolve keys relative // to the section. The Zen flag must be read from the root configuration with // the full "opencodezen.enabled" key, otherwise the read silently hits diff --git a/src/test/reasoningHistory.test.ts b/src/test/reasoningHistory.test.ts index 311ada3..b39cdd0 100644 --- a/src/test/reasoningHistory.test.ts +++ b/src/test/reasoningHistory.test.ts @@ -2,29 +2,29 @@ import assert from "node:assert/strict"; import { test } from "node:test"; import { shouldEchoThinkingHistory, thinkingTextFromValue } from "../reasoningHistory"; -void test("thinkingTextFromValue โ€” passes plain strings through", () => { +test("thinkingTextFromValue โ€” passes plain strings through", () => { assert.equal(thinkingTextFromValue("hello"), "hello"); }); -void test("thinkingTextFromValue โ€” joins string chunk arrays", () => { +test("thinkingTextFromValue โ€” joins string chunk arrays", () => { assert.equal(thinkingTextFromValue(["think ", "step", " 1"]), "think \nstep\n 1"); }); -void test("thinkingTextFromValue โ€” drops non-string chunks", () => { +test("thinkingTextFromValue โ€” drops non-string chunks", () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any assert.equal(thinkingTextFromValue(["a", 42 as any, "b"]), "a\nb"); }); -void test("thinkingTextFromValue โ€” empty inputs", () => { +test("thinkingTextFromValue โ€” empty inputs", () => { assert.equal(thinkingTextFromValue(""), ""); assert.equal(thinkingTextFromValue([]), ""); }); -void test("shouldEchoThinkingHistory โ€” undefined model id is never echoed", () => { +test("shouldEchoThinkingHistory โ€” undefined model id is never echoed", () => { assert.equal(shouldEchoThinkingHistory(undefined), false); }); -void test("shouldEchoThinkingHistory โ€” OpenAI-compatible reasoning families require the echo", () => { +test("shouldEchoThinkingHistory โ€” OpenAI-compatible reasoning families require the echo", () => { assert.equal(shouldEchoThinkingHistory("deepseek-v4-flash"), true); assert.equal(shouldEchoThinkingHistory("deepseek-v4-pro"), true); assert.equal(shouldEchoThinkingHistory("kimi-k2.6"), true); @@ -33,16 +33,16 @@ void test("shouldEchoThinkingHistory โ€” OpenAI-compatible reasoning families re assert.equal(shouldEchoThinkingHistory("minimax-m2.7"), true); }); -void test("shouldEchoThinkingHistory โ€” Gemini needs the echo for thought parts", () => { +test("shouldEchoThinkingHistory โ€” Gemini needs the echo for thought parts", () => { assert.equal(shouldEchoThinkingHistory("gemini-3.5-pro"), true); }); -void test("shouldEchoThinkingHistory โ€” families that reject or ignore the field", () => { +test("shouldEchoThinkingHistory โ€” families that reject or ignore the field", () => { assert.equal(shouldEchoThinkingHistory("mimo-v2.5"), false); assert.equal(shouldEchoThinkingHistory("gpt-5.6"), false); assert.equal(shouldEchoThinkingHistory("claude-sonnet-4.6"), false); }); -void test("shouldEchoThinkingHistory โ€” unknown families are left untouched", () => { +test("shouldEchoThinkingHistory โ€” unknown families are left untouched", () => { assert.equal(shouldEchoThinkingHistory("some-future-model"), false); }); diff --git a/src/test/responsesRequest.test.ts b/src/test/responsesRequest.test.ts index a3b3dcb..163fc19 100644 --- a/src/test/responsesRequest.test.ts +++ b/src/test/responsesRequest.test.ts @@ -2,8 +2,8 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { buildResponsesRequestEnvelope, responsesInputItemsFromMessage } from "../responsesRequest.js"; -void describe("buildResponsesRequestEnvelope", () => { - void it("enables server-side input truncation for long Responses sessions", () => { +describe("buildResponsesRequestEnvelope", () => { + it("enables server-side input truncation for long Responses sessions", () => { const body = buildResponsesRequestEnvelope({ model: "gpt-5.6-luna", input: [{ role: "user", content: "hello" }], @@ -14,7 +14,7 @@ void describe("buildResponsesRequestEnvelope", () => { assert.equal(body.max_output_tokens, 4096); }); - void it("does not force an unsupported text verbosity option", () => { + it("does not force an unsupported text verbosity option", () => { const body = buildResponsesRequestEnvelope({ model: "gpt-5.6-luna", input: [], @@ -24,7 +24,7 @@ void describe("buildResponsesRequestEnvelope", () => { assert.ok(!("text" in body)); }); - void it("only includes optional temperature and tool fields when provided", () => { + it("only includes optional temperature and tool fields when provided", () => { const body = buildResponsesRequestEnvelope({ model: "gpt-5.6-luna", input: [], @@ -42,8 +42,8 @@ void describe("buildResponsesRequestEnvelope", () => { }); }); -void describe("responsesInputItemsFromMessage", () => { - void it("emits user image as input_image with image_url as a plain STRING", () => { +describe("responsesInputItemsFromMessage", () => { + it("emits user image as input_image with image_url as a plain STRING", () => { // Regression: the Responses API expects `input_image.image_url` to be a // string (URL or base64 data URL), NOT the `{ url }` object shape used by // Chat Completions. The nested object made the gateway reject the request @@ -64,12 +64,12 @@ void describe("responsesInputItemsFromMessage", () => { ]); }); - void it("drops an empty string user message", () => { + it("drops an empty string user message", () => { const items = responsesInputItemsFromMessage({ role: "user", content: "" }); assert.deepEqual(items, []); }); - void it("emits assistant text as output_text and tool calls as function_call", () => { + it("emits assistant text as output_text and tool calls as function_call", () => { const items = responsesInputItemsFromMessage({ role: "assistant", content: [{ type: "text", text: "let me check" }], @@ -94,7 +94,7 @@ void describe("responsesInputItemsFromMessage", () => { ]); }); - void it("degrades tool results with images to a text note", () => { + it("degrades tool results with images to a text note", () => { const items = responsesInputItemsFromMessage({ role: "tool", tool_call_id: "call_1", @@ -110,7 +110,7 @@ void describe("responsesInputItemsFromMessage", () => { assert.match(output, /Responses API does not support images in tool output/); }); - void it("returns no items for unsupported roles", () => { + it("returns no items for unsupported roles", () => { const items = responsesInputItemsFromMessage({ role: "system", content: "be helpful", diff --git a/src/test/retry.test.ts b/src/test/retry.test.ts index d6b46d9..08ffaad 100644 --- a/src/test/retry.test.ts +++ b/src/test/retry.test.ts @@ -2,8 +2,8 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { analyzeHttp400ForRetry, isTransientServerError } from "../retry.js"; -void describe("analyzeHttp400ForRetry โ€” thinking errors", () => { - void it("patches 'only type=enabled is allowed' to force thinking.type='enabled'", () => { +describe("analyzeHttp400ForRetry โ€” thinking errors", () => { + it("patches 'only type=enabled is allowed' to force thinking.type='enabled'", () => { const body = { model: "kimi-k2.5", thinking: { type: "disabled" } }; const result = analyzeHttp400ForRetry("invalid thinking: only type=enabled is allowed for this model", body); assert.ok(result, "should be recoverable"); @@ -11,14 +11,14 @@ void describe("analyzeHttp400ForRetry โ€” thinking errors", () => { assert.match(result.reason, /thinking/i); }); - void it("patches 'only type=disabled is allowed' by removing thinking", () => { + it("patches 'only type=disabled is allowed' by removing thinking", () => { const body = { model: "some-model", thinking: { type: "enabled" } }; const result = analyzeHttp400ForRetry("invalid thinking: only type=disabled is allowed", body); assert.ok(result, "should be recoverable"); assert.deepEqual(result.body, { model: "some-model" }); }); - void it("patches generic 'invalid thinking' by removing thinking field", () => { + it("patches generic 'invalid thinking' by removing thinking field", () => { const body = { model: "test", thinking: { type: "disabled" }, temperature: 0.2 }; const result = analyzeHttp400ForRetry("invalid thinking parameter", body); assert.ok(result, "should be recoverable"); @@ -26,8 +26,8 @@ void describe("analyzeHttp400ForRetry โ€” thinking errors", () => { }); }); -void describe("analyzeHttp400ForRetry โ€” temperature errors", () => { - void it("patches 'invalid temperature: only 1 is allowed' by removing temperature", () => { +describe("analyzeHttp400ForRetry โ€” temperature errors", () => { + it("patches 'invalid temperature: only 1 is allowed' by removing temperature", () => { const body = { model: "kimi-k2.7-code", temperature: 0.2 }; const result = analyzeHttp400ForRetry("invalid temperature: only 1 is allowed for this model", body); assert.ok(result, "should be recoverable"); @@ -35,8 +35,8 @@ void describe("analyzeHttp400ForRetry โ€” temperature errors", () => { }); }); -void describe("analyzeHttp400ForRetry โ€” enable_thinking errors", () => { - void it("patches 'Extra inputs are not permitted, field: enable_thinking'", () => { +describe("analyzeHttp400ForRetry โ€” enable_thinking errors", () => { + it("patches 'Extra inputs are not permitted, field: enable_thinking'", () => { const body = { model: "kimi-k2.5", enable_thinking: false }; const result = analyzeHttp400ForRetry("Extra inputs are not permitted, field: 'enable_thinking', value: False", body); assert.ok(result, "should be recoverable"); @@ -44,8 +44,8 @@ void describe("analyzeHttp400ForRetry โ€” enable_thinking errors", () => { }); }); -void describe("analyzeHttp400ForRetry โ€” reasoning_effort errors", () => { - void it("patches reasoning_effort rejection", () => { +describe("analyzeHttp400ForRetry โ€” reasoning_effort errors", () => { + it("patches reasoning_effort rejection", () => { const body = { model: "minimax-m2.7", reasoning_effort: "high" }; const result = analyzeHttp400ForRetry("MiniMax M2 only accepts string reasoning_effort values ('low', 'medium', 'high')", body); assert.ok(result, "should be recoverable"); @@ -53,22 +53,22 @@ void describe("analyzeHttp400ForRetry โ€” reasoning_effort errors", () => { }); }); -void describe("analyzeHttp400ForRetry โ€” non-recoverable errors", () => { - void it("returns undefined for auth errors", () => { +describe("analyzeHttp400ForRetry โ€” non-recoverable errors", () => { + it("returns undefined for auth errors", () => { const body = { model: "test" }; const result = analyzeHttp400ForRetry("unauthorized", body); assert.equal(result, undefined); }); - void it("returns undefined for unrelated errors", () => { + it("returns undefined for unrelated errors", () => { const body = { model: "test" }; const result = analyzeHttp400ForRetry("model not found", body); assert.equal(result, undefined); }); }); -void describe("analyzeHttp400ForRetry โ€” context overflow", () => { - void it("reduces max_tokens using the authoritative counts from issue #109", () => { +describe("analyzeHttp400ForRetry โ€” context overflow", () => { + it("reduces max_tokens using the authoritative counts from issue #109", () => { const body = { model: "deepseek-v4-flash", max_tokens: 384_000 }; const result = analyzeHttp400ForRetry( "This model's maximum context length is 1048576 tokens. However, you requested 1050237 tokens (666237 in the messages, 384000 in the completion).", @@ -80,7 +80,7 @@ void describe("analyzeHttp400ForRetry โ€” context overflow", () => { assert.match(result.reason, /upstream context counts/i); }); - void it("supports Responses-style max_output_tokens and formatted counts", () => { + it("supports Responses-style max_output_tokens and formatted counts", () => { const body = { model: "gpt-test", max_output_tokens: 32_000 }; const result = analyzeHttp400ForRetry( "Maximum context length is 128,000 tokens; you requested 130,000 tokens (98,000 in the input, 32,000 in the output).", @@ -91,7 +91,7 @@ void describe("analyzeHttp400ForRetry โ€” context overflow", () => { assert.equal(result.body?.max_output_tokens, 29_744); }); - void it("patches the nested Google output budget", () => { + it("patches the nested Google output budget", () => { const body = { model: "gemini-test", generationConfig: { maxOutputTokens: 32_000, temperature: 0.2 } }; const result = analyzeHttp400ForRetry( "Maximum context length is 128,000 tokens; you requested 130,000 tokens (98,000 in the input, 32,000 in the output).", @@ -102,7 +102,7 @@ void describe("analyzeHttp400ForRetry โ€” context overflow", () => { assert.deepEqual(result.body?.generationConfig, { maxOutputTokens: 29_744, temperature: 0.2 }); }); - void it("does not retry when reducing completion cannot fit the prompt", () => { + it("does not retry when reducing completion cannot fit the prompt", () => { const result = analyzeHttp400ForRetry( "Maximum context length is 1,000 tokens. You requested 1,500 tokens (1,400 in the messages, 100 in the completion).", { model: "test", max_tokens: 100 }, @@ -112,26 +112,26 @@ void describe("analyzeHttp400ForRetry โ€” context overflow", () => { }); }); -void describe("isTransientServerError", () => { - void it("flags 502/503/504 as transient", () => { +describe("isTransientServerError", () => { + it("flags 502/503/504 as transient", () => { assert.equal(isTransientServerError(502, "Bad Gateway"), true); assert.equal(isTransientServerError(503, "Service Unavailable"), true); assert.equal(isTransientServerError(504, "Gateway Timeout"), true); }); - void it("flags a 500 whose body names Router.Unavailable as transient", () => { + it("flags a 500 whose body names Router.Unavailable as transient", () => { assert.equal(isTransientServerError(500, '{"error":{"type":"Router.Unavailable"}}'), true); }); - void it("treats 500 with unrelated body as permanent", () => { + it("treats 500 with unrelated body as permanent", () => { assert.equal(isTransientServerError(500, "Internal Server Error"), false); }); - void it("treats non-5xx statuses as permanent", () => { + it("treats non-5xx statuses as permanent", () => { assert.equal(isTransientServerError(429, "Too Many Requests"), false); }); - void it("matches Router.Unavailable case-insensitively", () => { + it("matches Router.Unavailable case-insensitively", () => { assert.equal(isTransientServerError(500, "type: router.unavailable"), true); }); }); diff --git a/src/test/thinking.test.ts b/src/test/thinking.test.ts index d547594..24da99f 100644 --- a/src/test/thinking.test.ts +++ b/src/test/thinking.test.ts @@ -31,57 +31,57 @@ const defaultSettings: ThinkingSettings = { * FIX: buildThinkingPayload special-cases /^kimi-k2\.7/i to always emit * { type: "enabled", keep: "all" } regardless of the user's thinking setting. */ -void describe("buildThinkingPayload โ€” kimi-k2.7-code (issue #25)", () => { - void it("always emits { type: 'enabled', keep: 'all' } even when thinking.kimi is 'off'", () => { +describe("buildThinkingPayload โ€” kimi-k2.7-code (issue #25)", () => { + it("always emits { type: 'enabled', keep: 'all' } even when thinking.kimi is 'off'", () => { const payload = buildThinkingPayload("kimi-k2.7-code", { ...defaultSettings, kimi: "off" }); assert.deepEqual(payload, { thinking: { type: "enabled", keep: "all" } }); }); - void it("emits { type: 'enabled', keep: 'all' } when thinking.kimi is 'on'", () => { + it("emits { type: 'enabled', keep: 'all' } when thinking.kimi is 'on'", () => { const payload = buildThinkingPayload("kimi-k2.7-code", { ...defaultSettings, kimi: "on" }); assert.deepEqual(payload, { thinking: { type: "enabled", keep: "all" } }); }); - void it("matches kimi-k2.7-code-highspeed variant too (same model, faster output)", () => { + it("matches kimi-k2.7-code-highspeed variant too (same model, faster output)", () => { const payload = buildThinkingPayload("kimi-k2.7-code-highspeed", defaultSettings); assert.deepEqual(payload, { thinking: { type: "enabled", keep: "all" } }); }); }); -void describe("buildThinkingPayload โ€” regression safety for other kimi models", () => { - void it("kimi-k2.6 with kimi='off' emits { type: 'disabled' } (still accepts disabled)", () => { +describe("buildThinkingPayload โ€” regression safety for other kimi models", () => { + it("kimi-k2.6 with kimi='off' emits { type: 'disabled' } (still accepts disabled)", () => { const payload = buildThinkingPayload("kimi-k2.6", { ...defaultSettings, kimi: "off" }); assert.deepEqual(payload, { thinking: { type: "disabled" } }); }); - void it("kimi-k2.6 with kimi='on' emits { type: 'enabled' }", () => { + it("kimi-k2.6 with kimi='on' emits { type: 'enabled' }", () => { const payload = buildThinkingPayload("kimi-k2.6", { ...defaultSettings, kimi: "on" }); assert.deepEqual(payload, { thinking: { type: "enabled" } }); }); - void it("kimi-k2.5 with kimi='off' emits { type: 'disabled' }", () => { + it("kimi-k2.5 with kimi='off' emits { type: 'disabled' }", () => { const payload = buildThinkingPayload("kimi-k2.5", { ...defaultSettings, kimi: "off" }); assert.deepEqual(payload, { thinking: { type: "disabled" } }); }); }); -void describe("buildThinkingPayload โ€” other families unchanged", () => { - void it("deepseek with 'off' emits empty object (no reasoning_effort)", () => { +describe("buildThinkingPayload โ€” other families unchanged", () => { + it("deepseek with 'off' emits empty object (no reasoning_effort)", () => { const payload = buildThinkingPayload("deepseek-v4-pro", { ...defaultSettings, deepseek: "off" }); assert.deepEqual(payload, {}); }); - void it("deepseek with 'high' emits reasoning_effort", () => { + it("deepseek with 'high' emits reasoning_effort", () => { const payload = buildThinkingPayload("deepseek-v4-pro", { ...defaultSettings, deepseek: "high" }); assert.deepEqual(payload, { reasoning_effort: "high" }); }); - void it("glm with 'off' emits { type: 'disabled' }", () => { + it("glm with 'off' emits { type: 'disabled' }", () => { const payload = buildThinkingPayload("glm-5", { ...defaultSettings, glm: "off" }); assert.deepEqual(payload, { thinking: { type: "disabled" } }); }); - void it("qwen with 'off' emits enable_thinking: false", () => { + it("qwen with 'off' emits enable_thinking: false", () => { const payload = buildThinkingPayload("qwen3.6-plus", { ...defaultSettings, qwen: "off" }); assert.deepEqual(payload, { enable_thinking: false }); }); @@ -92,8 +92,8 @@ void describe("buildThinkingPayload โ€” other families unchanged", () => { * users understand thinking cannot be disabled, rather than hiding the picker * or silently forcing "on". */ -void describe("buildFamilyThinkingSchema โ€” kimi-k2.7-code picker", () => { - void it("exposes a single 'on' option with 'Always On (K2.7)' label", () => { +describe("buildFamilyThinkingSchema โ€” kimi-k2.7-code picker", () => { + it("exposes a single 'on' option with 'Always On (K2.7)' label", () => { const schema = buildFamilyThinkingSchema("kimi-k2.7-code"); assert.ok(schema, "expected schema to be defined"); const reasoningEffort = schema.properties.reasoningEffort as Record; @@ -102,7 +102,7 @@ void describe("buildFamilyThinkingSchema โ€” kimi-k2.7-code picker", () => { assert.equal(reasoningEffort.default, "on"); }); - void it("mentions the Moonshot API constraint in the description", () => { + it("mentions the Moonshot API constraint in the description", () => { const schema = buildFamilyThinkingSchema("kimi-k2.7-code"); assert.ok(schema, "expected schema to be defined"); const reasoningEffort = schema.properties.reasoningEffort as Record; @@ -114,15 +114,15 @@ void describe("buildFamilyThinkingSchema โ€” kimi-k2.7-code picker", () => { }); }); -void describe("buildFamilyThinkingSchema โ€” other kimi models keep off/on", () => { - void it("kimi-k2.6 exposes both 'off' and 'on'", () => { +describe("buildFamilyThinkingSchema โ€” other kimi models keep off/on", () => { + it("kimi-k2.6 exposes both 'off' and 'on'", () => { const schema = buildFamilyThinkingSchema("kimi-k2.6"); assert.ok(schema); const reasoningEffort = schema.properties.reasoningEffort as Record; assert.deepEqual(reasoningEffort.enum, ["off", "on"]); }); - void it("kimi-k2.5 exposes both 'off' and 'on'", () => { + it("kimi-k2.5 exposes both 'off' and 'on'", () => { const schema = buildFamilyThinkingSchema("kimi-k2.5"); assert.ok(schema); const reasoningEffort = schema.properties.reasoningEffort as Record; @@ -134,36 +134,36 @@ void describe("buildFamilyThinkingSchema โ€” other kimi models keep off/on", () * Override tests: even if VS Code caches a stale picker value (e.g. "off"), * applyRequestThinkingOverride must force kimi="on" for K2.7-code. */ -void describe("applyRequestThinkingOverride โ€” kimi-k2.7-code defensive force-on", () => { - void it("forces kimi='on' even when override requests 'off'", () => { +describe("applyRequestThinkingOverride โ€” kimi-k2.7-code defensive force-on", () => { + it("forces kimi='on' even when override requests 'off'", () => { const result = applyRequestThinkingOverride("kimi-k2.7-code", defaultSettings, { reasoningEffort: "off", }); assert.equal(result.kimi, "on"); }); - void it("forces kimi='on' even when override requests 'on' (no-op but explicit)", () => { + it("forces kimi='on' even when override requests 'on' (no-op but explicit)", () => { const result = applyRequestThinkingOverride("kimi-k2.7-code", defaultSettings, { reasoningEffort: "on", }); assert.equal(result.kimi, "on"); }); - void it("forces kimi='on' when override is empty (defensive against stale cache)", () => { + it("forces kimi='on' when override is empty (defensive against stale cache)", () => { const result = applyRequestThinkingOverride("kimi-k2.7-code", defaultSettings, {}); assert.equal(result.kimi, "on"); }); }); -void describe("applyRequestThinkingOverride โ€” other kimi models respect override", () => { - void it("kimi-k2.6 respects 'off' override", () => { +describe("applyRequestThinkingOverride โ€” other kimi models respect override", () => { + it("kimi-k2.6 respects 'off' override", () => { const result = applyRequestThinkingOverride("kimi-k2.6", defaultSettings, { reasoningEffort: "off", }); assert.equal(result.kimi, "off"); }); - void it("kimi-k2.6 respects 'on' override", () => { + it("kimi-k2.6 respects 'on' override", () => { const result = applyRequestThinkingOverride("kimi-k2.6", defaultSettings, { reasoningEffort: "on", }); @@ -171,16 +171,16 @@ void describe("applyRequestThinkingOverride โ€” other kimi models respect overri }); }); -void describe("thinkingFamily โ€” detection", () => { - void it("classifies kimi-k2.7-code as 'kimi'", () => { +describe("thinkingFamily โ€” detection", () => { + it("classifies kimi-k2.7-code as 'kimi'", () => { assert.equal(thinkingFamily("kimi-k2.7-code"), "kimi"); }); - void it("classifies kimi-k2.6 as 'kimi'", () => { + it("classifies kimi-k2.6 as 'kimi'", () => { assert.equal(thinkingFamily("kimi-k2.6"), "kimi"); }); - void it("returns null for unknown prefixes", () => { + it("returns null for unknown prefixes", () => { assert.equal(thinkingFamily("unknown-model"), null); }); }); @@ -196,35 +196,35 @@ void describe("thinkingFamily โ€” detection", () => { * The new "high"/"max" values must map to thinking enabled in the payload, * and the per-model picker should expose only the relevant options. */ -void describe("buildThinkingPayload โ€” GLM with effort values (issue #61)", () => { - void it("glm-5.2 with glm='high' emits reasoning_effort: 'high'", () => { +describe("buildThinkingPayload โ€” GLM with effort values (issue #61)", () => { + it("glm-5.2 with glm='high' emits reasoning_effort: 'high'", () => { const payload = buildThinkingPayload("glm-5.2", { ...defaultSettings, glm: "high" }); assert.deepEqual(payload, { reasoning_effort: "high" }); }); - void it("glm-5.2 with glm='max' emits reasoning_effort: 'max'", () => { + it("glm-5.2 with glm='max' emits reasoning_effort: 'max'", () => { const payload = buildThinkingPayload("glm-5.2", { ...defaultSettings, glm: "max" }); assert.deepEqual(payload, { reasoning_effort: "max" }); }); - void it("glm-5.2 with glm='off' emits thinking disabled", () => { + it("glm-5.2 with glm='off' emits thinking disabled", () => { const payload = buildThinkingPayload("glm-5.2", { ...defaultSettings, glm: "off" }); assert.deepEqual(payload, { thinking: { type: "disabled" } }); }); - void it("glm-5 (toggle-only) with glm='high' sends reasoning_effort (gateway resolves)", () => { + it("glm-5 (toggle-only) with glm='high' sends reasoning_effort (gateway resolves)", () => { const payload = buildThinkingPayload("glm-5", { ...defaultSettings, glm: "high" }); assert.deepEqual(payload, { reasoning_effort: "high" }); }); - void it("glm-5 (toggle-only) with glm='off' emits thinking disabled", () => { + it("glm-5 (toggle-only) with glm='off' emits thinking disabled", () => { const payload = buildThinkingPayload("glm-5", { ...defaultSettings, glm: "off" }); assert.deepEqual(payload, { thinking: { type: "disabled" } }); }); }); -void describe("buildFamilyThinkingSchema โ€” GLM 5.2 with reasoning_options metadata", () => { - void it("exposes off, high, max when reasoning_options has effort values", () => { +describe("buildFamilyThinkingSchema โ€” GLM 5.2 with reasoning_options metadata", () => { + it("exposes off, high, max when reasoning_options has effort values", () => { const metadata = { reasoning: true, reasoningOptions: [{ type: "effort" as const, values: ["high", "max"] }], @@ -244,7 +244,7 @@ void describe("buildFamilyThinkingSchema โ€” GLM 5.2 with reasoning_options meta assert.equal(reasoningEffort.default, "off"); }); - void it("falls back to off/high/max for GLM models without reasoning_options (no invalid 'on')", () => { + it("falls back to off/high/max for GLM models without reasoning_options (no invalid 'on')", () => { const schema = buildFamilyThinkingSchema("glm-5"); assert.ok(schema, "expected schema to be defined"); const reasoningEffort = schema.properties.reasoningEffort as Record; @@ -253,29 +253,29 @@ void describe("buildFamilyThinkingSchema โ€” GLM 5.2 with reasoning_options meta }); }); -void describe("applyRequestThinkingOverride โ€” GLM with effort values (issue #61)", () => { - void it("accepts 'high' override for glm-5.2", () => { +describe("applyRequestThinkingOverride โ€” GLM with effort values (issue #61)", () => { + it("accepts 'high' override for glm-5.2", () => { const result = applyRequestThinkingOverride("glm-5.2", defaultSettings, { reasoningEffort: "high", }); assert.equal(result.glm, "high"); }); - void it("accepts 'max' override for glm-5.2", () => { + it("accepts 'max' override for glm-5.2", () => { const result = applyRequestThinkingOverride("glm-5.2", defaultSettings, { reasoningEffort: "max", }); assert.equal(result.glm, "max"); }); - void it("accepts 'off' override for glm-5.2", () => { + it("accepts 'off' override for glm-5.2", () => { const result = applyRequestThinkingOverride("glm-5.2", defaultSettings, { reasoningEffort: "off", }); assert.equal(result.glm, "off"); }); - void it("rejects invalid values like 'on' and 'medium' for glm", () => { + it("rejects invalid values like 'on' and 'medium' for glm", () => { const resultOn = applyRequestThinkingOverride("glm-5.2", defaultSettings, { reasoningEffort: "on", }); diff --git a/src/test/tokenEstimate.test.ts b/src/test/tokenEstimate.test.ts index f83394c..0f01668 100644 --- a/src/test/tokenEstimate.test.ts +++ b/src/test/tokenEstimate.test.ts @@ -2,13 +2,13 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { estimatePromptTokenCount, estimateTokenCount } from "../tokenEstimate.js"; -void describe("token estimates", () => { - void it("returns zero for empty content", () => { +describe("token estimates", () => { + it("returns zero for empty content", () => { assert.equal(estimateTokenCount(""), 0); assert.equal(estimateTokenCount(" \n\t"), 0); }); - void it("includes tool schemas in the prompt estimate", () => { + it("includes tool schemas in the prompt estimate", () => { const messages = [{ role: "user", content: "inspect the workspace" }]; const withoutTools = estimatePromptTokenCount(messages); const withTools = estimatePromptTokenCount(messages, [ diff --git a/src/test/toolCallAccumulator.test.ts b/src/test/toolCallAccumulator.test.ts index 167f8ce..0fc99d9 100644 --- a/src/test/toolCallAccumulator.test.ts +++ b/src/test/toolCallAccumulator.test.ts @@ -39,8 +39,8 @@ const argsChunk1 = deltaChunk([{ index: 0, function: { arguments: '{"query":' } const argsChunk2 = deltaChunk([{ index: 0, function: { arguments: '"search"}' } }]); -void describe("ToolCallAccumulator โ€” no premature flush on intermediate chunks (#98)", () => { - void it("does not flush while finish_reason is null, even with pending tool calls", () => { +describe("ToolCallAccumulator โ€” no premature flush on intermediate chunks (#98)", () => { + it("does not flush while finish_reason is null, even with pending tool calls", () => { const acc = new ToolCallAccumulator(); acc.collect(nameChunk); acc.collect(argsChunk1); @@ -54,7 +54,7 @@ void describe("ToolCallAccumulator โ€” no premature flush on intermediate chunks assert.equal(ToolCallAccumulator.shouldFlushOnFinishReason(undefined), false); }); - void it("flushes exactly ONE complete tool call when finish_reason is 'tool_calls'", () => { + it("flushes exactly ONE complete tool call when finish_reason is 'tool_calls'", () => { const acc = new ToolCallAccumulator(); acc.collect(nameChunk); acc.collect(argsChunk1); @@ -74,7 +74,7 @@ void describe("ToolCallAccumulator โ€” no premature flush on intermediate chunks assert.deepEqual(acc.flush(), []); }); - void it("is a no-op when nothing was collected", () => { + it("is a no-op when nothing was collected", () => { const acc = new ToolCallAccumulator(); assert.deepEqual(acc.flush(), []); assert.deepEqual(acc.flushRemainingToolCalls(), []); @@ -82,8 +82,8 @@ void describe("ToolCallAccumulator โ€” no premature flush on intermediate chunks }); }); -void describe("ToolCallAccumulator โ€” end-of-stream flush for gateways omitting finish_reason (#93)", () => { - void it("flushRemainingToolCalls emits the complete call when the gateway never sends 'tool_calls'", () => { +describe("ToolCallAccumulator โ€” end-of-stream flush for gateways omitting finish_reason (#93)", () => { + it("flushRemainingToolCalls emits the complete call when the gateway never sends 'tool_calls'", () => { const acc = new ToolCallAccumulator(); acc.collect(nameChunk); acc.collect(argsChunk1); @@ -99,7 +99,7 @@ void describe("ToolCallAccumulator โ€” end-of-stream flush for gateways omitting assert.equal(acc.size, 0); }); - void it("flushRemainingToolCalls is a no-op after a normal finish_reason flush", () => { + it("flushRemainingToolCalls is a no-op after a normal finish_reason flush", () => { const acc = new ToolCallAccumulator(); acc.collect(nameChunk); acc.collect(argsChunk1); @@ -109,8 +109,8 @@ void describe("ToolCallAccumulator โ€” end-of-stream flush for gateways omitting }); }); -void describe("ToolCallAccumulator โ€” delta handling edge cases", () => { - void it("ignores non-array and non-record deltas", () => { +describe("ToolCallAccumulator โ€” delta handling edge cases", () => { + it("ignores non-array and non-record deltas", () => { const acc = new ToolCallAccumulator(); acc.collect(undefined); acc.collect("not an array"); @@ -118,7 +118,7 @@ void describe("ToolCallAccumulator โ€” delta handling edge cases", () => { assert.equal(acc.size, 0); }); - void it("filters out arguments-only deltas that never supplied a name", () => { + it("filters out arguments-only deltas that never supplied a name", () => { const acc = new ToolCallAccumulator(); acc.collect([{ index: 0, function: { arguments: '{"a":1}' } }]); assert.equal(acc.size, 1); @@ -127,7 +127,7 @@ void describe("ToolCallAccumulator โ€” delta handling edge cases", () => { assert.deepEqual(flushed, []); }); - void it("accumulates multiple tool calls independently by index", () => { + it("accumulates multiple tool calls independently by index", () => { const acc = new ToolCallAccumulator(); acc.collect([ { index: 0, id: "a", function: { name: "read_file", arguments: '{"path":' } }, @@ -146,7 +146,7 @@ void describe("ToolCallAccumulator โ€” delta handling edge cases", () => { assert.deepEqual(flushed[1].input, { query: "foo" }); }); - void it("appends name fragments (name split across chunks)", () => { + it("appends name fragments (name split across chunks)", () => { const acc = new ToolCallAccumulator(); acc.collect([{ index: 0, function: { name: "read_" } }]); acc.collect([{ index: 0, function: { name: "file" } }]); @@ -155,28 +155,28 @@ void describe("ToolCallAccumulator โ€” delta handling edge cases", () => { }); }); -void describe("parseToolInput", () => { - void it("returns {} for empty or whitespace-only input", () => { +describe("parseToolInput", () => { + it("returns {} for empty or whitespace-only input", () => { assert.deepEqual(parseToolInput(""), {}); assert.deepEqual(parseToolInput(" "), {}); }); - void it("returns {} for partial / invalid JSON", () => { + it("returns {} for partial / invalid JSON", () => { assert.deepEqual(parseToolInput('{"a":'), {}); assert.deepEqual(parseToolInput("not json"), {}); }); - void it("parses valid JSON objects", () => { + it("parses valid JSON objects", () => { assert.deepEqual(parseToolInput('{"a":1}'), { a: 1 }); assert.deepEqual(parseToolInput("{}"), {}); }); - void it("returns {} for non-object JSON scalars (strings, numbers)", () => { + it("returns {} for non-object JSON scalars (strings, numbers)", () => { assert.deepEqual(parseToolInput('"str"'), {}); assert.deepEqual(parseToolInput("42"), {}); }); - void it("passes through JSON arrays (isRecord treats arrays as objects โ€” original semantics)", () => { + it("passes through JSON arrays (isRecord treats arrays as objects โ€” original semantics)", () => { assert.deepEqual(parseToolInput("[1,2]"), [1, 2]); }); }); diff --git a/src/test/usageProfile.test.ts b/src/test/usageProfile.test.ts index 4fdd6e0..d65a9ac 100644 --- a/src/test/usageProfile.test.ts +++ b/src/test/usageProfile.test.ts @@ -45,31 +45,31 @@ before(async () => { mod = await import("../usageProfile.js"); }); -void describe("keyFingerprint", () => { - void it("returns 'legacy' for empty input", () => { +describe("keyFingerprint", () => { + it("returns 'legacy' for empty input", () => { assert.equal(mod.keyFingerprint(""), mod.LEGACY_FINGERPRINT); }); - void it("takes 8 leading + 8 trailing chars", () => { + it("takes 8 leading + 8 trailing chars", () => { assert.equal(mod.keyFingerprint("sk-90UzXXab-XXXXXXXX-cdWToa"), "sk-90UzX-X-cdWToa"); }); - void it("is stable (same key, same fingerprint)", () => { + it("is stable (same key, same fingerprint)", () => { const k = "sk-aaaabbbb-cccccccc-dddd-eeee-ffff-12345678"; assert.equal(mod.keyFingerprint(k), mod.keyFingerprint(k)); }); }); -void describe("profile registry", () => { - void it("returns empty when no profiles stored", () => { +describe("profile registry", () => { + it("returns empty when no profiles stored", () => { assert.deepEqual(mod.readProfiles(createMockContext()), []); }); - void it("round-trips profiles through writeProfiles/readProfiles", async () => { + it("round-trips profiles through writeProfiles/readProfiles", async () => { const ctx = createMockContext(); const p = { fingerprint: "fp1", label: "Profile 1", lastSeenAt: Date.now(), isLegacy: false }; await mod.writeProfiles(ctx, [p]); assert.equal(mod.readProfiles(ctx).length, 1); assert.equal(mod.readProfiles(ctx)[0].fingerprint, "fp1"); }); - void it("findProfile returns matching profile or undefined", () => { + it("findProfile returns matching profile or undefined", () => { const profiles = [ { fingerprint: "a", label: "A", lastSeenAt: 0, isLegacy: false }, { fingerprint: "b", label: "B", lastSeenAt: 0, isLegacy: false }, @@ -77,7 +77,7 @@ void describe("profile registry", () => { assert.equal(mod.findProfile(profiles, "a")?.label, "A"); assert.equal(mod.findProfile(profiles, "missing"), undefined); }); - void it("renameProfile updates label", async () => { + it("renameProfile updates label", async () => { const ctx = createMockContext(); const p = { fingerprint: "fp1", label: "Profile 1", lastSeenAt: Date.now(), isLegacy: false }; await mod.writeProfiles(ctx, [p]); @@ -86,11 +86,11 @@ void describe("profile registry", () => { }); }); -void describe("active profile", () => { - void it("defaults to legacy when not stored", () => { +describe("active profile", () => { + it("defaults to legacy when not stored", () => { assert.equal(mod.readActiveProfile(createMockContext()), mod.LEGACY_FINGERPRINT); }); - void it("round-trips through writeActiveProfile", async () => { + it("round-trips through writeActiveProfile", async () => { const ctx = createMockContext(); await mod.writeActiveProfile(ctx, "my-fp"); assert.equal(mod.readActiveProfile(ctx), "my-fp"); diff --git a/src/test/visionProxy.test.ts b/src/test/visionProxy.test.ts index e216a2f..ca18ea9 100644 --- a/src/test/visionProxy.test.ts +++ b/src/test/visionProxy.test.ts @@ -33,28 +33,28 @@ function shouldProxy({ hasImageInput, actuallySupportsVision, visionProxyModelId return Boolean(hasImageInput && !actuallySupportsVision && visionProxyModelId); } -void describe("vision proxy condition (shouldProxy)", () => { - void it("enters proxy when text-only model receives images with proxy configured", () => { +describe("vision proxy condition (shouldProxy)", () => { + it("enters proxy when text-only model receives images with proxy configured", () => { assert.ok(shouldProxy({ hasImageInput: true, actuallySupportsVision: false, visionProxyModelId: "gpt-5.5" })); }); - void it("skips proxy when no images present", () => { + it("skips proxy when no images present", () => { assert.ok(!shouldProxy({ hasImageInput: false, actuallySupportsVision: false, visionProxyModelId: "gpt-5.5" })); }); - void it("skips proxy when model natively supports vision", () => { + it("skips proxy when model natively supports vision", () => { assert.ok(!shouldProxy({ hasImageInput: true, actuallySupportsVision: true, visionProxyModelId: "gpt-5.5" })); }); - void it("skips proxy when no vision model is configured (empty string)", () => { + it("skips proxy when no vision model is configured (empty string)", () => { assert.ok(!shouldProxy({ hasImageInput: true, actuallySupportsVision: false, visionProxyModelId: "" })); }); - void it("skips proxy when all conditions are false", () => { + it("skips proxy when all conditions are false", () => { assert.ok(!shouldProxy({ hasImageInput: false, actuallySupportsVision: true, visionProxyModelId: "" })); }); - void it("cached supportsVision (actuallySupportsVision) prevents circular regression", () => { + it("cached supportsVision (actuallySupportsVision) prevents circular regression", () => { // This is the fix for #74: even if modelCapabilities overrides // metadata.supportsVision to true (because proxy is enabled), // the CACHED value (actuallySupportsVision) stays false for @@ -69,27 +69,27 @@ void describe("vision proxy condition (shouldProxy)", () => { }); }); -void describe("modelCapabilities vision proxy flag", () => { +describe("modelCapabilities vision proxy flag", () => { // modelCapabilities() returns imageInput: true when: // metadata.supportsVision (native) OR isVisionProxyEnabled() // This tells VS Code NOT to strip images from requests. - void it("returns imageInput: true when proxy is enabled on text-only models", () => { + it("returns imageInput: true when proxy is enabled on text-only models", () => { const capabilities = buildStableModelCapabilities(true); assert.equal(capabilities.imageInput, true); }); - void it("returns imageInput: true when model natively supports vision", () => { + it("returns imageInput: true when model natively supports vision", () => { const capabilities = buildStableModelCapabilities(true); assert.equal(capabilities.imageInput, true); }); - void it("returns imageInput: false only when no vision support and no proxy", () => { + it("returns imageInput: false only when no vision support and no proxy", () => { const capabilities = buildStableModelCapabilities(false); assert.equal(capabilities.imageInput, false); }); - void it("keeps tool calling enabled without proposal-gated edit tool hints", () => { + it("keeps tool calling enabled without proposal-gated edit tool hints", () => { const capabilities = buildStableModelCapabilities(true); assert.equal(capabilities.toolCalling, true); @@ -98,8 +98,8 @@ void describe("modelCapabilities vision proxy flag", () => { }); }); -void describe("vision proxy image description cache", () => { - void it("imageDescriptionKey is a stable sha-256 hash of the base64 bytes", () => { +describe("vision proxy image description cache", () => { + it("imageDescriptionKey is a stable sha-256 hash of the base64 bytes", () => { const key = imageDescriptionKey("aGVsbG8="); assert.equal(imageDescriptionKey("aGVsbG8="), key, "same bytes produce the same key"); @@ -107,12 +107,12 @@ void describe("vision proxy image description cache", () => { assert.notEqual(imageDescriptionKey("aGVsbG8="), imageDescriptionKey("d29ybGQ="), "different bytes produce different keys"); }); - void it("lookupImageDescriptions returns undefined when nothing is cached", () => { + it("lookupImageDescriptions returns undefined when nothing is cached", () => { clearImageDescriptionCache(); assert.equal(lookupImageDescriptions([imageDescriptionKey("aGVsbG8=")]), undefined); }); - void it("stores and looks up a description under every image hash", () => { + it("stores and looks up a description under every image hash", () => { clearImageDescriptionCache(); const h1 = imageDescriptionKey("aGVsbG8="); const h2 = imageDescriptionKey("d29ybGQ="); @@ -125,7 +125,7 @@ void describe("vision proxy image description cache", () => { assert.equal(lookupImageDescriptions([h1, h2]), description); }); - void it("lookupImageDescriptions returns undefined when only some hashes are cached", () => { + it("lookupImageDescriptions returns undefined when only some hashes are cached", () => { clearImageDescriptionCache(); const h1 = imageDescriptionKey("aGVsbG8="); const h2 = imageDescriptionKey("d29ybGQ="); @@ -135,7 +135,7 @@ void describe("vision proxy image description cache", () => { assert.equal(lookupImageDescriptions([h1, h2]), undefined); }); - void it("reuses the cached description instead of re-describing (same image twice)", () => { + it("reuses the cached description instead of re-describing (same image twice)", () => { clearImageDescriptionCache(); const hash = imageDescriptionKey("cmV1c2UtbWU="); const description = "Description cached on the first turn."; @@ -149,7 +149,7 @@ void describe("vision proxy image description cache", () => { assert.equal(imageDescriptionCache.size, 1); }); - void it("evicts the oldest entries once the cache exceeds its limit", () => { + it("evicts the oldest entries once the cache exceeds its limit", () => { clearImageDescriptionCache(); const firstKey = imageDescriptionKey("Zmlyc3Q="); @@ -166,7 +166,7 @@ void describe("vision proxy image description cache", () => { ); }); - void it("clearImageDescriptionCache empties the cache", () => { + it("clearImageDescriptionCache empties the cache", () => { clearImageDescriptionCache(); storeImageDescriptions([imageDescriptionKey("aGVsbG8=")], "hello description"); assert.equal(imageDescriptionCache.size, 1); From 514a63f79c31027602f9f753a4353e813252c3d8 Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Wed, 12 Aug 2026 09:27:18 +0500 Subject: [PATCH 16/18] chore(config): use standard file extensions everywhere eslint.config.mjs was the last non-standard extension in the repo. Rename it to eslint.config.js and convert to CommonJS (the repo root has no "type": "module", so require() is the correct module system there), with a self-exemption for the config file itself. Drop every remaining .mjs/.mts reference from the lint stack (tsFiles/testFiles globs, nonProjectFiles, lint-staged pattern, .vscodeignore, staged-lint.js extension sets and import resolution). --- .vscodeignore | 2 +- eslint.config.mjs => eslint.config.js | 31 +++++++++++++++++---------- package.json | 2 +- scripts/staged-lint.js | 13 ++--------- 4 files changed, 24 insertions(+), 24 deletions(-) rename eslint.config.mjs => eslint.config.js (82%) diff --git a/.vscodeignore b/.vscodeignore index 2372dff..7b03897 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -6,7 +6,7 @@ .vscode/** .vscode-test/** docs/**/*.md -eslint.config.mjs +eslint.config.js node_modules/** !node_modules/@silvia-odwyer/ !node_modules/@silvia-odwyer/photon-node/ diff --git a/eslint.config.mjs b/eslint.config.js similarity index 82% rename from eslint.config.mjs rename to eslint.config.js index bba90d0..8ec89de 100644 --- a/eslint.config.mjs +++ b/eslint.config.js @@ -15,28 +15,29 @@ // via `--max-warnings 0`. The only rules disabled here are ones whose noise // outweighs their value (see the scoped overrides below). -import { readFileSync } from "node:fs"; -import { defineConfig } from "eslint/config"; -import tseslint from "typescript-eslint"; -import yml from "eslint-plugin-yml"; -import jsonc from "eslint-plugin-jsonc"; +const { readFileSync } = require("node:fs"); +const path = require("node:path"); +const { defineConfig } = require("eslint/config"); +const tseslint = require("typescript-eslint"); +const yml = require("eslint-plugin-yml"); +const jsonc = require("eslint-plugin-jsonc"); -const gitignore = readFileSync(new URL(".gitignore", import.meta.url), "utf8") +const gitignore = readFileSync(path.join(__dirname, ".gitignore"), "utf8") .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line && !line.startsWith("#") && !line.startsWith("!")); // Files not covered by tsconfig (which only includes src/), type-checked via // the default project so strictTypeChecked rules still apply to them. -const nonProjectFiles = ["eslint.config.mjs", "scripts/*.js", "scripts/*.mjs", "scripts/*.ts"]; +const nonProjectFiles = ["eslint.config.js", "scripts/*.js", "scripts/*.ts"]; // The typescript-eslint `config()` helper is deprecated; ESLint core now // provides `defineConfig()`. We replicate the helper's `extends` expansion // explicitly by applying the TS `files` glob to each config object. -const tsFiles = ["**/*.{ts,mts,cts,js,mjs,cjs}"]; -const testFiles = ["**/*.test.{ts,tsx,js,mjs,cjs}"]; +const tsFiles = ["**/*.{ts,js,cjs}"]; +const testFiles = ["**/*.test.{ts,tsx,js,cjs}"]; -export default defineConfig([ +module.exports = defineConfig([ { ignores: gitignore, }, @@ -50,7 +51,7 @@ export default defineConfig([ projectService: { allowDefaultProject: nonProjectFiles, }, - tsconfigRootDir: import.meta.dirname, + tsconfigRootDir: __dirname, }, }, rules: { @@ -86,6 +87,14 @@ export default defineConfig([ "@typescript-eslint/no-floating-promises": "off", }, }, + { + // The ESLint config file itself must be CommonJS (the repo root has no + // "type": "module"), so `require()` is the only option there. + files: ["eslint.config.js"], + rules: { + "@typescript-eslint/no-require-imports": "off", + }, + }, // --- YAML: strict rules from eslint-plugin-yml --------------------------- ...yml.configs["flat/standard"], // --- JSON / JSONC: strict rules from eslint-plugin-jsonc ----------------- diff --git a/package.json b/package.json index ac688fe..48c3d24 100644 --- a/package.json +++ b/package.json @@ -382,7 +382,7 @@ }, "lint-staged": { "*": "prettier --write --ignore-unknown", - "*.{js,mjs,cjs,ts,mts,cts}": "eslint --fix --max-warnings 0", + "*.{js,cjs,ts}": "eslint --fix --max-warnings 0", "*.md": "markdownlint-cli2 --config .markdownlint-cli2.jsonc --fix", ".husky/*": "shellcheck" }, diff --git a/scripts/staged-lint.js b/scripts/staged-lint.js index 604a718..4a1b8ed 100644 --- a/scripts/staged-lint.js +++ b/scripts/staged-lint.js @@ -27,7 +27,7 @@ const root = path.resolve(import.meta.dirname, ".."); const bin = (name) => path.join(root, "node_modules", ".bin", name); const SRC_DIRS = ["src", "scripts"]; -const TS_EXT = new Set([".ts", ".tsx", ".js", ".mjs", ".cjs", ".mts", ".cts"]); +const TS_EXT = new Set([".ts", ".tsx", ".js", ".cjs", ".cts"]); const IMPORT_RE = /(?:from\s*|import\s*\(\s*|require\s*\(\s*)["'](\.[^"']+)["']/g; /** @param {string} text @returns {string} */ @@ -80,16 +80,7 @@ function collectSourceFiles() { */ function resolveImport(fromFile, spec) { const base = path.resolve(path.dirname(fromFile), spec); - const candidates = [ - base, - `${base}.ts`, - `${base}.tsx`, - `${base}.js`, - `${base}.mjs`, - `${base}.mts`, - path.join(base, "index.ts"), - path.join(base, "index.js"), - ]; + const candidates = [base, `${base}.ts`, `${base}.tsx`, `${base}.js`, path.join(base, "index.ts"), path.join(base, "index.js")]; for (const candidate of candidates) { try { statSync(candidate); From 76570ccd940ffbd4f369b2f258cfd2e7d953e06e Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Wed, 12 Aug 2026 09:34:57 +0500 Subject: [PATCH 17/18] chore(config): prefer TypeScript over JavaScript everywhere MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extension policy: latest standards, standard file names only (.ts/.js, never .mjs/.mts), TypeScript preferred over JavaScript. - eslint.config.js -> eslint.config.ts โ€” ESLint 10 loads TS configs natively (requires jiti, added as a devDependency); the config no longer needs the CommonJS conversion or the require() self-exemption. - scripts: lint.js, format.js, staged-lint.js, run-unit-tests.js -> lint.ts, format.ts, staged-lint.ts, run-unit-tests.ts โ€” full TypeScript instead of JSDoc-typed JavaScript. - tsx added as a devDependency so the .ts scripts run on any Node (CI runs Node 20, which has no native type stripping); validate-models and test-retry now use the local tsx instead of npx --yes tsx (offline, faster). - package.json scripts, .husky/pre-commit and .vscodeignore updated to the new names. --- .husky/pre-commit | 4 +- .vscodeignore | 2 +- eslint.config.js => eslint.config.ts | 31 +- package-lock.json | 534 +++++++++++++++++- package.json | 14 +- scripts/{format.js => format.ts} | 19 +- scripts/{lint.js => lint.ts} | 27 +- .../{run-unit-tests.js => run-unit-tests.ts} | 4 +- scripts/{staged-lint.js => staged-lint.ts} | 82 +-- 9 files changed, 624 insertions(+), 93 deletions(-) rename eslint.config.js => eslint.config.ts (83%) rename scripts/{format.js => format.ts} (71%) rename scripts/{lint.js => lint.ts} (71%) rename scripts/{run-unit-tests.js => run-unit-tests.ts} (91%) rename scripts/{staged-lint.js => staged-lint.ts} (77%) diff --git a/.husky/pre-commit b/.husky/pre-commit index 4938fcb..1a4e69c 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -15,10 +15,10 @@ if ! command -v npx >/dev/null 2>&1; then fi # Zero-tolerance gate on exactly what this change can affect: the staged -# files plus the files that import them (see scripts/staged-lint.js). Runs +# files plus the files that import them (see scripts/staged-lint.ts). Runs # BEFORE formatting so the linter sees the file as written. The full-tree # lint (including tests) runs in CI and on demand via `npm run lint`. -node scripts/staged-lint.js +npx tsx scripts/staged-lint.ts # Format changed files (code + non-code) after linting passes. npx lint-staged diff --git a/.vscodeignore b/.vscodeignore index 7b03897..c2a592a 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -6,7 +6,7 @@ .vscode/** .vscode-test/** docs/**/*.md -eslint.config.js +eslint.config.ts node_modules/** !node_modules/@silvia-odwyer/ !node_modules/@silvia-odwyer/photon-node/ diff --git a/eslint.config.js b/eslint.config.ts similarity index 83% rename from eslint.config.js rename to eslint.config.ts index 8ec89de..708812b 100644 --- a/eslint.config.js +++ b/eslint.config.ts @@ -1,5 +1,7 @@ // ESLint flat config โ€” strict where it catches real bugs, quiet where it -// would only add ceremony. +// would only add ceremony. TypeScript config file (ESLint 10 loads +// eslint.config.ts natively); kept as `.ts` per the repo's extension policy +// (standard extensions only, TypeScript preferred over JavaScript). // // Stack: // - typescript-eslint `strict` + `strictTypeChecked`: @@ -15,21 +17,20 @@ // via `--max-warnings 0`. The only rules disabled here are ones whose noise // outweighs their value (see the scoped overrides below). -const { readFileSync } = require("node:fs"); -const path = require("node:path"); -const { defineConfig } = require("eslint/config"); -const tseslint = require("typescript-eslint"); -const yml = require("eslint-plugin-yml"); -const jsonc = require("eslint-plugin-jsonc"); +import { readFileSync } from "node:fs"; +import { defineConfig } from "eslint/config"; +import tseslint from "typescript-eslint"; +import yml from "eslint-plugin-yml"; +import jsonc from "eslint-plugin-jsonc"; -const gitignore = readFileSync(path.join(__dirname, ".gitignore"), "utf8") +const gitignore = readFileSync(new URL(".gitignore", import.meta.url), "utf8") .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line && !line.startsWith("#") && !line.startsWith("!")); // Files not covered by tsconfig (which only includes src/), type-checked via // the default project so strictTypeChecked rules still apply to them. -const nonProjectFiles = ["eslint.config.js", "scripts/*.js", "scripts/*.ts"]; +const nonProjectFiles = ["eslint.config.ts", "scripts/*.ts"]; // The typescript-eslint `config()` helper is deprecated; ESLint core now // provides `defineConfig()`. We replicate the helper's `extends` expansion @@ -37,7 +38,7 @@ const nonProjectFiles = ["eslint.config.js", "scripts/*.js", "scripts/*.ts"]; const tsFiles = ["**/*.{ts,js,cjs}"]; const testFiles = ["**/*.test.{ts,tsx,js,cjs}"]; -module.exports = defineConfig([ +export default defineConfig([ { ignores: gitignore, }, @@ -51,7 +52,7 @@ module.exports = defineConfig([ projectService: { allowDefaultProject: nonProjectFiles, }, - tsconfigRootDir: __dirname, + tsconfigRootDir: import.meta.dirname, }, }, rules: { @@ -87,14 +88,6 @@ module.exports = defineConfig([ "@typescript-eslint/no-floating-promises": "off", }, }, - { - // The ESLint config file itself must be CommonJS (the repo root has no - // "type": "module"), so `require()` is the only option there. - files: ["eslint.config.js"], - rules: { - "@typescript-eslint/no-require-imports": "off", - }, - }, // --- YAML: strict rules from eslint-plugin-yml --------------------------- ...yml.configs["flat/standard"], // --- JSON / JSONC: strict rules from eslint-plugin-jsonc ----------------- diff --git a/package-lock.json b/package-lock.json index 0cc97ac..0fa2df3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "opencode-copilot-chat", - "version": "0.5.1", + "version": "0.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opencode-copilot-chat", - "version": "0.5.1", + "version": "0.5.2", "license": "MIT", "dependencies": { "@silvia-odwyer/photon-node": "^0.3.4" @@ -20,11 +20,13 @@ "eslint-plugin-jsonc": "^3.4.1", "eslint-plugin-yml": "^3.8.1", "husky": "^9.1.7", + "jiti": "^2.7.0", "lint-staged": "^17.3.0", "markdownlint-cli2": "^0.23.2", "picocolors": "^1.1.1", "prettier": "^3.9.6", "shellcheck": "^4.1.0", + "tsx": "^4.23.12", "typescript": "^6.0.3", "typescript-eslint": "^8.66.0" }, @@ -253,6 +255,448 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", @@ -2757,6 +3201,48 @@ "dev": true, "license": "MIT" }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3307,6 +3793,21 @@ "node": ">=14.14" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3970,6 +4471,16 @@ "url": "https://bevry.me/fund" } }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6892,6 +7403,25 @@ "dev": true, "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", diff --git a/package.json b/package.json index 48c3d24..bca870c 100644 --- a/package.json +++ b/package.json @@ -356,8 +356,8 @@ ] }, "scripts": { - "lint": "node scripts/lint.js", - "lint:staged": "node scripts/staged-lint.js", + "lint": "tsx scripts/lint.ts", + "lint:staged": "tsx scripts/staged-lint.ts", "lint:js": "eslint . --max-warnings 0", "lint:fix": "eslint . --fix --max-warnings 0", "lint:md": "markdownlint-cli2 --config .markdownlint-cli2.jsonc \"**/*.md\" \"#node_modules\"", @@ -365,16 +365,16 @@ "lint:sh": "shellcheck .husky/pre-commit", "lint:hygiene": "editorconfig-checker", "lint:ts": "tsc -p tsconfig.check.json", - "format": "node scripts/format.js", + "format": "tsx scripts/format.ts", "format:js": "eslint . --fix --max-warnings 0", "format:prettier": "npx prettier --write . --ignore-path .gitignore", "format:check": "npx prettier --check . --ignore-path .gitignore", "clean": "node -e \"require('node:fs').rmSync('out', { recursive: true, force: true })\"", "compile": "npm run clean && tsc -p ./", - "test": "npm run compile && node scripts/run-unit-tests.js", + "test": "npm run compile && tsx scripts/run-unit-tests.ts", "watch": "npm run clean && tsc -watch -p ./", - "validate-models": "npx --yes tsx scripts/validate-models.ts", - "test-retry": "npx --yes tsx scripts/test-retry-e2e.ts", + "validate-models": "tsx scripts/validate-models.ts", + "test-retry": "tsx scripts/test-retry-e2e.ts", "prepackage": "npm test", "package": "vsce package", "vscode:prepublish": "npm run compile", @@ -395,11 +395,13 @@ "eslint-plugin-jsonc": "^3.4.1", "eslint-plugin-yml": "^3.8.1", "husky": "^9.1.7", + "jiti": "^2.7.0", "lint-staged": "^17.3.0", "markdownlint-cli2": "^0.23.2", "picocolors": "^1.1.1", "prettier": "^3.9.6", "shellcheck": "^4.1.0", + "tsx": "^4.23.12", "typescript": "^6.0.3", "typescript-eslint": "^8.66.0" }, diff --git a/scripts/format.js b/scripts/format.ts similarity index 71% rename from scripts/format.js rename to scripts/format.ts index 163a86b..9fb46f5 100644 --- a/scripts/format.js +++ b/scripts/format.ts @@ -8,11 +8,15 @@ import pc from "picocolors"; const root = path.resolve(import.meta.dirname, ".."); -/** @param {string} name @returns {string} */ -const bin = (name) => path.join(root, "node_modules", ".bin", name); +const bin = (name: string): string => path.join(root, "node_modules", ".bin", name); -/** @type {Array<{label: string, cmd: string, args: string[]}>} */ -const steps = [ +interface FormatStep { + label: string; + cmd: string; + args: string[]; +} + +const steps: FormatStep[] = [ { label: "ESLint", cmd: bin("eslint"), args: [".", "--fix", "--max-warnings", "0"] }, { label: "Prettier", cmd: bin("prettier"), args: ["--write", "--log-level", "warn", ".", "--ignore-path", ".gitignore"] }, ]; @@ -20,9 +24,7 @@ const steps = [ console.log(pc.bold("Format")); let failed = false; for (const step of steps) { - const res = /** @type {import("node:child_process").SpawnSyncReturns} */ ( - spawnSync(step.cmd, step.args, { cwd: root, encoding: "utf8" }) - ); + const res = spawnSync(step.cmd, step.args, { cwd: root, encoding: "utf8" }); const output = `${res.stdout}${res.stderr}`.trim(); if (res.status === 0) { console.log(` ${pc.green("โœ”")} ${step.label}`); @@ -37,8 +39,7 @@ for (const step of steps) { console.log(failed ? pc.red("Failed") : pc.green("Passed")); process.exit(failed ? 1 : 0); -/** @param {string} text @returns {string} */ -function indent(text) { +function indent(text: string): string { return text .split("\n") .map((line) => ` ${line}`) diff --git a/scripts/lint.js b/scripts/lint.ts similarity index 71% rename from scripts/lint.js rename to scripts/lint.ts index 2bfc2c8..e3d1d42 100644 --- a/scripts/lint.js +++ b/scripts/lint.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node -// Runs every linter and prints a compact per-tool result. On success only a -// green check is shown; on failure the relevant error output is printed. +// Runs every linter (and the test suite) and prints a compact per-tool +// result. On success only a green check is shown; on failure the relevant +// error output is printed. import { spawnSync } from "node:child_process"; import path from "node:path"; @@ -8,14 +9,12 @@ import pc from "picocolors"; const root = path.resolve(import.meta.dirname, ".."); -/** @param {string} name @returns {string} */ -const bin = (name) => path.join(root, "node_modules", ".bin", name); +const bin = (name: string): string => path.join(root, "node_modules", ".bin", name); // Strip markdownlint-cli2 banner/summary noise and prettier's status header. const NOISE = /^(markdownlint-cli2 v|Finding:|Linting:|Summary:|Checking formatting\.\.\.)/; -/** @param {string} text @returns {string} */ -function clean(text) { +function clean(text: string): string { return text .split("\n") .map((line) => line.trim()) @@ -23,8 +22,13 @@ function clean(text) { .join("\n"); } -/** @type {Array<{label: string, cmd: string, args: string[]}>} */ -const steps = [ +interface LintStep { + label: string; + cmd: string; + args: string[]; +} + +const steps: LintStep[] = [ { label: "Editorconfig", cmd: bin("editorconfig-checker"), args: [] }, { label: "ESLint", cmd: bin("eslint"), args: [".", "--max-warnings", "0"] }, { @@ -41,9 +45,7 @@ const steps = [ console.log(pc.bold("Lint")); let failed = false; for (const step of steps) { - const res = /** @type {import("node:child_process").SpawnSyncReturns} */ ( - spawnSync(step.cmd, step.args, { cwd: root, encoding: "utf8" }) - ); + const res = spawnSync(step.cmd, step.args, { cwd: root, encoding: "utf8" }); const output = clean(`${res.stdout}${res.stderr}`); if (res.status === 0) { console.log(` ${pc.green("โœ”")} ${step.label}`); @@ -58,8 +60,7 @@ for (const step of steps) { console.log(failed ? pc.red("Failed") : pc.green("Passed")); process.exit(failed ? 1 : 0); -/** @param {string} text @returns {string} */ -function indent(text) { +function indent(text: string): string { return text .split("\n") .map((line) => ` ${line}`) diff --git a/scripts/run-unit-tests.js b/scripts/run-unit-tests.ts similarity index 91% rename from scripts/run-unit-tests.js rename to scripts/run-unit-tests.ts index 889c369..8e3351f 100644 --- a/scripts/run-unit-tests.js +++ b/scripts/run-unit-tests.ts @@ -19,10 +19,8 @@ process.exit(result.status ?? 1); /** * Recursively collect compiled `*.test.js` files under a directory. - * @param {string} directory - * @returns {string[]} */ -function collectTests(directory) { +function collectTests(directory: string): string[] { return readdirSync(directory, { withFileTypes: true }) .sort((left, right) => left.name.localeCompare(right.name)) .flatMap((entry) => { diff --git a/scripts/staged-lint.js b/scripts/staged-lint.ts similarity index 77% rename from scripts/staged-lint.js rename to scripts/staged-lint.ts index 4a1b8ed..c008781 100644 --- a/scripts/staged-lint.js +++ b/scripts/staged-lint.ts @@ -16,36 +16,31 @@ // lint-staged, which runs after this gate. The full-tree lint (including // tests) stays available as `npm run lint` and is enforced in CI. -import { spawnSync } from "node:child_process"; +import { spawnSync, type SpawnSyncReturns } from "node:child_process"; import { readdirSync, readFileSync, statSync } from "node:fs"; import path from "node:path"; import pc from "picocolors"; const root = path.resolve(import.meta.dirname, ".."); -/** @param {string} name @returns {string} */ -const bin = (name) => path.join(root, "node_modules", ".bin", name); +const bin = (name: string): string => path.join(root, "node_modules", ".bin", name); const SRC_DIRS = ["src", "scripts"]; const TS_EXT = new Set([".ts", ".tsx", ".js", ".cjs", ".cts"]); const IMPORT_RE = /(?:from\s*|import\s*\(\s*|require\s*\(\s*)["'](\.[^"']+)["']/g; -/** @param {string} text @returns {string} */ -function indent(text) { - return text - .split("\n") - .map((line) => ` ${line}`) - .join("\n"); +interface CommandResult { + status: number | null; + output: string; } -/** @param {string} cmd @param {string[]} args @returns {{status: number|null, output: string}} */ -function run(cmd, args) { - const res = /** @type {import("node:child_process").SpawnSyncReturns} */ (spawnSync(cmd, args, { cwd: root, encoding: "utf8" })); +function run(cmd: string, args: string[]): CommandResult { + const res: SpawnSyncReturns = spawnSync(cmd, args, { cwd: root, encoding: "utf8" }); return { status: res.status, output: `${res.stdout}${res.stderr}`.trim() }; } -/** Staged (added/copied/modified) file paths relative to the repo root. @returns {string[]} */ -function stagedFiles() { +/** Staged (added/copied/modified) file paths relative to the repo root. */ +function stagedFiles(): string[] { const res = run("git", ["diff", "--cached", "--name-only", "-z", "--diff-filter=ACM"]); if (res.status !== 0) { return []; @@ -53,13 +48,11 @@ function stagedFiles() { return res.output.split("\0").filter(Boolean); } -/** Every TS/JS source file under src/ and scripts/. @returns {string[]} */ -function collectSourceFiles() { - /** @type {string[]} */ - const out = []; +/** Every TS/JS source file under src/ and scripts/. */ +function collectSourceFiles(): string[] { + const out: string[] = []; for (const dir of SRC_DIRS) { - /** @param {string} dirPath */ - const walk = (dirPath) => { + const walk = (dirPath: string): void => { for (const entry of readdirSync(dirPath, { withFileTypes: true })) { if (entry.isDirectory()) { walk(path.join(dirPath, entry.name)); @@ -73,14 +66,18 @@ function collectSourceFiles() { return out; } -/** Resolve a relative import specifier to an existing file, if any. - * @param {string} fromFile - * @param {string} spec - * @returns {string | undefined} - */ -function resolveImport(fromFile, spec) { +/** Resolve a relative import specifier to an existing file, if any. */ +function resolveImport(fromFile: string, spec: string): string | undefined { const base = path.resolve(path.dirname(fromFile), spec); - const candidates = [base, `${base}.ts`, `${base}.tsx`, `${base}.js`, path.join(base, "index.ts"), path.join(base, "index.js")]; + const candidates = [ + base, + `${base}.ts`, + `${base}.tsx`, + `${base}.js`, + `${base}.cjs`, + path.join(base, "index.ts"), + path.join(base, "index.js"), + ]; for (const candidate of candidates) { try { statSync(candidate); @@ -92,10 +89,9 @@ function resolveImport(fromFile, spec) { return undefined; } -/** resolved file path โ†’ set of source files importing it (one level deep). @returns {Map>} */ -function buildImporters() { - /** @type {Map>} */ - const importers = new Map(); +/** resolved file path โ†’ set of source files importing it (one level deep). */ +function buildImporters(): Map> { + const importers = new Map>(); for (const file of collectSourceFiles()) { const text = readFileSync(file, "utf8"); for (const match of text.matchAll(IMPORT_RE)) { @@ -117,13 +113,10 @@ function buildImporters() { /** * Files related to the staged ones: their direct dependents within src/ and * scripts/, so changing a module's contract also lints its consumers. - * @param {string[]} changedFiles - * @returns {string[]} */ -function relatedFiles(changedFiles) { +function relatedFiles(changedFiles: string[]): string[] { const importers = buildImporters(); - /** @type {Set} */ - const related = new Set(); + const related = new Set(); for (const file of changedFiles) { for (const importer of importers.get(file) ?? []) { related.add(importer); @@ -145,8 +138,14 @@ const sourceChanged = staged.some((file) => SRC_DIRS.some((dir) => file.startsWi const eslintTargets = [...new Set([...jsTsFiles, ...relatedFiles(jsTsFiles)])]; -/** @type {Array<{label: string, cmd: string, args: string[], run: boolean}>} */ -const steps = [ +interface StagedStep { + label: string; + cmd: string; + args: string[]; + run: boolean; +} + +const steps: StagedStep[] = [ { label: "ESLint", cmd: bin("eslint"), args: ["--max-warnings", "0", ...eslintTargets], run: eslintTargets.length > 0 }, { label: "Markdown", cmd: bin("markdownlint-cli2"), args: ["--config", ".markdownlint-cli2.jsonc", ...mdFiles], run: mdFiles.length > 0 }, { label: "Editorconfig", cmd: bin("editorconfig-checker"), args: [...staged], run: true }, @@ -175,3 +174,10 @@ for (const step of steps) { } console.log(failed ? pc.red("Failed") : pc.green("Passed")); process.exit(failed ? 1 : 0); + +function indent(text: string): string { + return text + .split("\n") + .map((line) => ` ${line}`) + .join("\n"); +} From c8178716a281361a30ab779be149281e473a53bc Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Wed, 12 Aug 2026 09:37:11 +0500 Subject: [PATCH 18/18] chore(config): rename markdownlint config to standard .json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .markdownlint-cli2.jsonc contained no comments, so .json is the honest standard extension (markdownlint-cli2 accepts it via --config). Updated package.json scripts, lint-staged, lint.ts, staged-lint.ts and .vscodeignore. Also removed the empty .markdownlint/rules directory. Repo-wide extension inventory after this: TypeScript (.ts) for all source and scripts, JSON for configs โ€” no .js, .mjs or .mts anywhere. --- .markdownlint-cli2.jsonc => .markdownlint-cli2.json | 6 +++--- .vscodeignore | 2 +- package.json | 6 +++--- scripts/lint.ts | 2 +- scripts/staged-lint.ts | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) rename .markdownlint-cli2.jsonc => .markdownlint-cli2.json (60%) diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.json similarity index 60% rename from .markdownlint-cli2.jsonc rename to .markdownlint-cli2.json index eddce7a..f5d8878 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.json @@ -2,11 +2,11 @@ "config": { "MD013": false, "MD024": { - "siblings_only": true, + "siblings_only": true }, "MD033": false, "MD041": false, - "MD060": false, + "MD060": false }, - "gitignore": true, + "gitignore": true } diff --git a/.vscodeignore b/.vscodeignore index c2a592a..43a7fc6 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,7 +1,7 @@ .gitignore .github/** .husky/** -.markdownlint-cli2.jsonc +.markdownlint-cli2.json .prettierrc.json .vscode/** .vscode-test/** diff --git a/package.json b/package.json index bca870c..2c0b517 100644 --- a/package.json +++ b/package.json @@ -360,8 +360,8 @@ "lint:staged": "tsx scripts/staged-lint.ts", "lint:js": "eslint . --max-warnings 0", "lint:fix": "eslint . --fix --max-warnings 0", - "lint:md": "markdownlint-cli2 --config .markdownlint-cli2.jsonc \"**/*.md\" \"#node_modules\"", - "lint:md:all": "markdownlint-cli2 --config .markdownlint-cli2.jsonc \"**/*.md\"", + "lint:md": "markdownlint-cli2 --config .markdownlint-cli2.json \"**/*.md\" \"#node_modules\"", + "lint:md:all": "markdownlint-cli2 --config .markdownlint-cli2.json \"**/*.md\"", "lint:sh": "shellcheck .husky/pre-commit", "lint:hygiene": "editorconfig-checker", "lint:ts": "tsc -p tsconfig.check.json", @@ -383,7 +383,7 @@ "lint-staged": { "*": "prettier --write --ignore-unknown", "*.{js,cjs,ts}": "eslint --fix --max-warnings 0", - "*.md": "markdownlint-cli2 --config .markdownlint-cli2.jsonc --fix", + "*.md": "markdownlint-cli2 --config .markdownlint-cli2.json --fix", ".husky/*": "shellcheck" }, "devDependencies": { diff --git a/scripts/lint.ts b/scripts/lint.ts index e3d1d42..10ffe80 100644 --- a/scripts/lint.ts +++ b/scripts/lint.ts @@ -34,7 +34,7 @@ const steps: LintStep[] = [ { label: "Markdown", cmd: bin("markdownlint-cli2"), - args: ["--config", ".markdownlint-cli2.jsonc", "**/*.md", "#node_modules"], + args: ["--config", ".markdownlint-cli2.json", "**/*.md", "#node_modules"], }, { label: "Prettier", cmd: bin("prettier"), args: ["--check", ".", "--ignore-path", ".gitignore"] }, { label: "Shell", cmd: bin("shellcheck"), args: [".husky/pre-commit"] }, diff --git a/scripts/staged-lint.ts b/scripts/staged-lint.ts index c008781..757c3d1 100644 --- a/scripts/staged-lint.ts +++ b/scripts/staged-lint.ts @@ -147,7 +147,7 @@ interface StagedStep { const steps: StagedStep[] = [ { label: "ESLint", cmd: bin("eslint"), args: ["--max-warnings", "0", ...eslintTargets], run: eslintTargets.length > 0 }, - { label: "Markdown", cmd: bin("markdownlint-cli2"), args: ["--config", ".markdownlint-cli2.jsonc", ...mdFiles], run: mdFiles.length > 0 }, + { label: "Markdown", cmd: bin("markdownlint-cli2"), args: ["--config", ".markdownlint-cli2.json", ...mdFiles], run: mdFiles.length > 0 }, { label: "Editorconfig", cmd: bin("editorconfig-checker"), args: [...staged], run: true }, { label: "Shell", cmd: bin("shellcheck"), args: [...huskyFiles], run: huskyFiles.length > 0 }, { label: "TypeScript", cmd: bin("tsc"), args: ["-p", "tsconfig.check.json"], run: sourceChanged },