diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..31b5170b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,103 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + core: + name: Core / Node 24 + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + - name: Verify exact source checkout + shell: bash + env: + EXPECTED_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + run: | + set -euo pipefail + actual="$(git rev-parse HEAD)" + if [[ "$actual" != "$EXPECTED_SHA" ]]; then + echo "wrong source checkout: expected=$EXPECTED_SHA actual=$actual" >&2 + exit 1 + fi + echo "exact source checkout: $actual" + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + - name: Install frozen dependency graph + run: npm ci + - name: Verify exact DeepSeek Harness release closure + run: node scripts/verify-dsh-closure.mjs + - name: Unit and contract tests + run: npm test + - name: Verify DSH ownership boundary + run: node scripts/verify-ownership-boundary.mjs + - name: Boot and dispose official DeepSeek Harness composition + run: node scripts/runtime-smoke.mjs + - name: Syntax check shipped JavaScript + shell: bash + run: | + set -euo pipefail + while IFS= read -r file; do node --check "$file"; done < <(find src bin scripts -type f -name '*.mjs' -print | sort) + - name: Syntax check PTY smoke + run: python3 -m py_compile test/tui_smoke.py + + linux-tui: + name: Linux TUI / pinned Codex / local IPC + needs: core + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + - name: Verify exact source checkout + shell: bash + env: + EXPECTED_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + run: | + set -euo pipefail + actual="$(git rev-parse HEAD)" + if [[ "$actual" != "$EXPECTED_SHA" ]]; then + echo "wrong source checkout: expected=$EXPECTED_SHA actual=$actual" >&2 + exit 1 + fi + echo "exact source checkout: $actual" + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + - name: Install frozen dependency graph + run: npm ci + - name: Verify exact DeepSeek Harness release closure + run: node scripts/verify-dsh-closure.mjs + - name: Build pinned Codex TUI + local IPC bridge + shell: bash + env: + DSHX_VERSION: 1.0.0-ci + run: bash scripts/build-codex-tui.sh + - name: Verify thin-fork and transport invariants + run: node scripts/verify-tui-invariants.mjs + - name: Verify local IPC data plane + shell: bash + run: dist/bin/dshx-ipc-bridge --check + - name: Exercise real TUI through Linux PTY + CJK + resize + shell: bash + run: | + set -euo pipefail + VENV="$RUNNER_TEMP/dshx-pty-venv" + python3 -m venv "$VENV" + "$VENV/bin/python" -m pip install --disable-pip-version-check pexpect + "$VENV/bin/python" test/tui_smoke.py diff --git a/.github/workflows/rc-freeze-dependencies.yml b/.github/workflows/rc-freeze-dependencies.yml deleted file mode 100644 index 723e11bb..00000000 --- a/.github/workflows/rc-freeze-dependencies.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Freeze 1.0 RC dependencies - -on: - pull_request: - branches: [main] - types: [opened, synchronize, reopened] - -permissions: - contents: write - pull-requests: read - -concurrency: - group: freeze-1.0-rc-dependencies - cancel-in-progress: true - -jobs: - freeze: - if: >- - github.event.pull_request.head.repo.full_name == github.repository && - github.event.pull_request.head.ref == 'release/1.0-rc' - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Check out exact RC head - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 0 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Resolve lock without executing package scripts - shell: bash - run: npm install --package-lock-only --ignore-scripts --no-audit --no-fund - - name: Fail if the supported DSH direct line drifted - shell: bash - run: | - node - <<'NODE' - const fs = require('fs'); - const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); - const expected = '0.1.0-rc.8'; - const required = [ - '@deepseek-ai/dsh', - '@deepseek-ai/dsh-agent', - '@deepseek-ai/dsh-app-boot', - '@deepseek-ai/dsh-host-apiproxy', - '@deepseek-ai/dsh-launch-environment', - '@deepseek-ai/dsh-llm', - '@deepseek-ai/dsh-session' - ]; - for (const name of required) { - if (pkg.dependencies?.[name] !== expected) { - throw new Error(`${name} must stay exactly ${expected}; got ${pkg.dependencies?.[name]}`); - } - } - const lock = JSON.parse(fs.readFileSync('package-lock.json', 'utf8')); - if (lock.packages?.['']?.dependencies?.['@deepseek-ai/dsh'] !== expected) { - throw new Error('generated lock does not preserve the exact DSH root version'); - } - NODE - - name: Commit frozen graph back to the RC branch - shell: bash - env: - RC_BRANCH: ${{ github.event.pull_request.head.ref }} - run: | - set -euo pipefail - if git diff --quiet -- package-lock.json && git ls-files --error-unmatch package-lock.json >/dev/null 2>&1; then - echo 'package-lock.json already matches the RC manifest' - exit 0 - fi - git config user.name 'dshx-release-bot' - git config user.email 'actions@users.noreply.github.com' - git add package-lock.json - git commit -m 'chore: freeze RC npm dependency graph' - git push origin "HEAD:${RC_BRANCH}" diff --git a/.github/workflows/rc-platform.yml b/.github/workflows/rc-platform.yml new file mode 100644 index 00000000..553f5bad --- /dev/null +++ b/.github/workflows/rc-platform.yml @@ -0,0 +1,106 @@ +name: RC Platform Gate + +on: + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: rc-platform-${{ github.event.pull_request.head.sha || github.sha }} + cancel-in-progress: true + +jobs: + windows: + if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release/') + name: Windows ConPTY / CJK / local IPC + runs-on: windows-latest + timeout-minutes: 150 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + - name: Verify exact source checkout + shell: pwsh + env: + EXPECTED_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + run: | + $actual = (git rev-parse HEAD).Trim() + if ($actual -ne $env:EXPECTED_SHA) { + throw "wrong source checkout: expected=$env:EXPECTED_SHA actual=$actual" + } + Write-Host "exact source checkout: $actual" + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + - name: Install frozen dependency graph + run: npm ci --include=optional --ignore-scripts=false + - name: Verify exact DeepSeek Harness release closure + run: node scripts/verify-dsh-closure.mjs + - name: Boot and dispose official DeepSeek Harness composition + run: node scripts/runtime-smoke.mjs + - name: Build pinned Codex TUI + local IPC bridge + shell: pwsh + env: + DSHX_VERSION: 1.0.0-ci + run: ./scripts/build-codex-tui.ps1 + - name: Verify thin-fork and transport invariants + run: node scripts/verify-tui-invariants.mjs + - name: Verify Windows local IPC data plane + shell: pwsh + run: | + & .\dist\bin\dshx-ipc-bridge.exe --check + if ($LASTEXITCODE -ne 0) { throw 'local IPC data-plane self-check failed' } + - name: Exercise real TUI through Windows ConPTY + CJK text + run: node test/tui_smoke_windows.mjs + + macos: + if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release/') + name: macOS PTY / CJK / resize / local IPC + runs-on: macos-26 + timeout-minutes: 120 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + - name: Verify exact source checkout + shell: bash + env: + EXPECTED_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + run: | + set -euo pipefail + actual="$(git rev-parse HEAD)" + if [[ "$actual" != "$EXPECTED_SHA" ]]; then + echo "wrong source checkout: expected=$EXPECTED_SHA actual=$actual" >&2 + exit 1 + fi + echo "exact source checkout: $actual" + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + - name: Install frozen dependency graph + run: npm ci --include=optional --ignore-scripts=false + - name: Verify exact DeepSeek Harness release closure + run: node scripts/verify-dsh-closure.mjs + - name: Boot and dispose official DeepSeek Harness composition + run: node scripts/runtime-smoke.mjs + - name: Build pinned Codex TUI + local IPC bridge + shell: bash + env: + DSHX_VERSION: 1.0.0-ci + run: bash scripts/build-codex-tui.sh + - name: Verify thin-fork and transport invariants + run: node scripts/verify-tui-invariants.mjs + - name: Verify macOS local IPC data plane + shell: bash + run: dist/bin/dshx-ipc-bridge --check + - name: Exercise real TUI through macOS PTY + CJK + resize + shell: bash + run: | + set -euo pipefail + VENV="$RUNNER_TEMP/dshx-pty-venv" + python3 -m venv "$VENV" + "$VENV/bin/python" -m pip install --disable-pip-version-check pexpect + "$VENV/bin/python" test/tui_smoke.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..5732589e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,211 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + issues: read + pull-requests: read + +jobs: + build: + name: Build / ${{ matrix.label }} + runs-on: ${{ matrix.os }} + timeout-minutes: 150 + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + label: linux-x64 + - os: windows-latest + label: windows-x64 + - os: macos-26 + label: macos-arm64 + - os: macos-26-intel + label: macos-x64 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + - name: Enforce release tag lineage + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + if [[ "$GITHUB_REF_NAME" == v1.0.0-rc.* ]]; then + expected="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/release/1.0-rc" --jq .object.sha)" + source_ref='release/1.0-rc' + else + expected="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq .object.sha)" + source_ref='main' + fi + if [[ "$GITHUB_SHA" != "$expected" ]]; then + echo "release tag $GITHUB_REF_NAME must point at the current $source_ref head: expected=$expected actual=$GITHUB_SHA" >&2 + exit 1 + fi + - name: Enforce stable 1.0 promotion ledger + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + if [[ "$GITHUB_REF_NAME" != "v1.0.0" ]]; then + exit 0 + fi + for issue in 12 13 14 15; do + state="$(gh issue view "$issue" --repo "$GITHUB_REPOSITORY" --json state --jq .state)" + if [[ "$state" != "CLOSED" ]]; then + echo "stable 1.0 is blocked: release-gate issue #$issue is $state" >&2 + exit 1 + fi + done + merged="$(gh pr view 11 --repo "$GITHUB_REPOSITORY" --json mergedAt --jq '.mergedAt != null')" + if [[ "$merged" != "true" ]]; then + echo 'stable 1.0 is blocked: PR #11 has not been merged' >&2 + exit 1 + fi + - name: Resolve release version (Unix) + if: runner.os != 'Windows' + shell: bash + run: echo "DSHX_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV" + - name: Resolve release version (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: Add-Content -Path $env:GITHUB_ENV -Value "DSHX_VERSION=$($env:GITHUB_REF_NAME.TrimStart('v'))" + - name: Test presentation adapter from frozen graph + run: | + npm ci --include=optional --ignore-scripts=false + node scripts/verify-dsh-closure.mjs + npm test + - name: Boot official DSH runtime + run: node scripts/runtime-smoke.mjs + - name: Build pinned TUI and local IPC bridge (Unix) + if: runner.os != 'Windows' + shell: bash + run: bash scripts/build-codex-tui.sh + - name: Build pinned TUI and local IPC bridge (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: ./scripts/build-codex-tui.ps1 + - name: Verify local IPC data plane (Unix) + if: runner.os != 'Windows' + shell: bash + run: dist/bin/dshx-ipc-bridge --check + - name: Verify local IPC data plane (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + & .\dist\bin\dshx-ipc-bridge.exe --check + if ($LASTEXITCODE -ne 0) { throw 'local IPC data-plane self-check failed' } + - name: Real pinned TUI local-IPC PTY + CJK + resize smoke + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + VENV="$RUNNER_TEMP/dshx-pty-venv" + python3 -m venv "$VENV" + "$VENV/bin/python" -m pip install --disable-pip-version-check pexpect + "$VENV/bin/python" test/tui_smoke.py + - name: Real pinned TUI Windows ConPTY + CJK local-IPC smoke + if: runner.os == 'Windows' + run: node test/tui_smoke_windows.mjs + - name: Build installable platform tarball (Unix) + if: runner.os != 'Windows' + shell: bash + run: node scripts/package-platform.mjs + - name: Build installable platform tarball (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: node scripts/package-platform.mjs + - name: Clean install smoke (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + TGZ="$(find dist/release -maxdepth 1 -name 'dshx-*.tgz' -print -quit)" + PREFIX="$RUNNER_TEMP/dshx-release-smoke" + npm install --global --prefix "$PREFIX" --include=optional --ignore-scripts=false "$TGZ" + "$PREFIX/bin/dshx" --version | grep -F "$DSHX_VERSION" + "$PREFIX/bin/dshx" doctor + - name: Clean install smoke (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $tgz = Get-ChildItem dist\release\dshx-*.tgz | Select-Object -First 1 + if (-not $tgz) { throw 'platform tarball missing' } + $prefix = Join-Path $env:RUNNER_TEMP 'dshx-release-smoke' + npm install --global --prefix $prefix --include=optional --ignore-scripts=false $tgz.FullName + $version = & (Join-Path $prefix 'dshx.cmd') --version + if (-not ($version -match [regex]::Escape($env:DSHX_VERSION))) { throw "wrong dshx version: $version" } + & (Join-Path $prefix 'dshx.cmd') doctor + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: dshx-${{ matrix.label }} + path: | + dist/release/*.tgz + dist/release/*.json + if-no-files-found: error + + publish: + name: Publish GitHub release + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: artifacts + merge-multiple: true + - name: Verify cross-platform artifact provenance + shell: bash + run: | + set -euo pipefail + python3 - <<'PY' + import glob + import json + import os + + files = sorted(glob.glob('artifacts/dshx-*.tgz.json')) + if len(files) != 4: + raise SystemExit(f'expected 4 platform provenance sidecars, found {len(files)}: {files}') + records = [] + for filename in files: + with open(filename, encoding='utf-8') as handle: + records.append((filename, json.load(handle))) + expected_version = os.environ['GITHUB_REF_NAME'].removeprefix('v') + keys = ('version', 'dshVersion', 'codexCommit', 'dshCommit', 'transport', 'dependencyGraph', 'sourceLockSha256') + baseline = {key: records[0][1].get(key) for key in keys} + if baseline['version'] != expected_version: + raise SystemExit(f"artifact version mismatch: expected {expected_version}, got {baseline['version']}") + if baseline['dependencyGraph'] != 'source-package-lock': + raise SystemExit(f"unexpected dependency graph provenance: {baseline['dependencyGraph']}") + lock_hash = baseline['sourceLockSha256'] + if not isinstance(lock_hash, str) or len(lock_hash) != 64 or any(ch not in '0123456789abcdef' for ch in lock_hash): + raise SystemExit(f'invalid source lock SHA-256: {lock_hash!r}') + for filename, record in records[1:]: + current = {key: record.get(key) for key in keys} + if current != baseline: + raise SystemExit(f'cross-platform provenance mismatch in {filename}: {current} != {baseline}') + print(json.dumps(baseline, sort_keys=True)) + PY + - name: Create checksums + shell: bash + run: | + set -euo pipefail + cd artifacts + sha256sum dshx-*.tgz dshx-*.tgz.json > SHA256SUMS + cat SHA256SUMS + - name: Create immutable GitHub release + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + flags=(--verify-tag --generate-notes) + if [[ "$GITHUB_REF_NAME" == *-* ]]; then flags+=(--prerelease); fi + gh release create "$GITHUB_REF_NAME" artifacts/* --repo "$GITHUB_REPOSITORY" "${flags[@]}" diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..33df6516 --- /dev/null +++ b/NOTICE @@ -0,0 +1,27 @@ +DSHX / dsh-codex +Copyright 2026 dsh-codex contributors + +This distribution includes a modified build of OpenAI Codex TUI sourced from: +https://github.com/openai/codex +Pinned source commit: see upstream/CODEX_COMMIT + +OpenAI Codex +Copyright 2025 OpenAI +Licensed under the Apache License, Version 2.0. + +The upstream OpenAI Codex NOTICE states: + +OpenAI Codex +Copyright 2025 OpenAI + +This project includes code derived from Ratatui (https://github.com/ratatui/ratatui), licensed under the MIT license. +Copyright (c) 2016-2022 Florian Dehau +Copyright (c) 2023-2025 The Ratatui Developers + +DSHX modifies only a small presentation/launcher seam in the pinned Codex TUI. +The ordered source patches are distributed under upstream/patches/codex/. + +DeepSeek Harness is not forked or vendored by DSHX. It is installed as an +upstream npm dependency and remains the authoritative runtime for Agent, +Session, model routing, tools, sandboxing, approvals, skills and related +capabilities. Its own package license and notices remain with that dependency. diff --git a/README.md b/README.md index 0c8342b0..2112224b 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,149 @@ -# dsh-codex +# DSHX (`dsh-codex`) -A production-oriented DeepSeek Harness CLI that reuses the Codex terminal UX as closely as practical while keeping **DeepSeek Harness as the agent runtime and source of truth**. +DSHX is a terminal frontend for **official DeepSeek Harness** that reuses a pinned Codex TUI thin fork instead of reimplementing a look-alike terminal UI. -## Goal +## Product contract -`dsh-codex` is not another Codex-like TUI. The target is a thin Codex TUI fork plus a DSH backend compatibility layer, so the visible interaction model stays aligned with Codex while sessions, model routing, tools, sandboxing, approvals, skills and subagents remain owned by DSH. +From a code project, the normal user entry point is: + +```bash +dshx +``` + +No separate Codex installation, remote-mode command, bridge command, DSH WebUI, or manual profile plumbing is part of the production UX. + +The interaction target is the pinned Codex CLI/TUI: welcome screen, composer, slash commands, model and permission pickers, approvals, ask-user flows, tool/shell cells, diffs, plan/reasoning presentation, steering, Ctrl+C interrupt, resume, status/footer, scrolling, resize and mouse behavior. Windows Terminal and CJK/IME are first-class release gates. + +## Ownership boundary + +We maintain only: + +- the Codex TUI thin fork and upstream sync patches; +- TUI behavior and UX parity; +- the `dshx` launcher, local presentation transport and packaging; +- the thinnest practical projection from DSH **public APIs/events** into the TUI protocol. + +We do **not** maintain, fork or recreate DeepSeek Harness capabilities. DSH owns the agent loop, sessions, model routing, tools, sandbox, approval policy, skills, subagents, plugins, jobs/workflows and persistence. + +If DSH does not expose a capability, DSHX waits for upstream or explicitly hides/degrades the corresponding UI. It does not implement a replacement runtime. ```text -Codex TUI (thin fork) +pinned Codex TUI thin fork ← DSHX presentation │ - │ app-server compatible protocol + │ WebSocket framing over private local UDS ▼ -dsh-codex adapter +dshx-ipc-bridge ← tiny transport bridge; no runtime state │ + │ JSONL over child stdio ▼ -DeepSeek Harness / Cordis +DSHX presentation adapter ← translation only │ - ├─ Session - ├─ Agent loop - ├─ LLM routing - ├─ Tools - ├─ Sandbox / Approval - ├─ Skills - └─ Subagents +════════════════ ownership boundary ════════════════ + │ official DSH public APIs/events + ▼ +Official DeepSeek Harness ← Agent / Session / tools / policy / persistence ``` -## Product principles +There is **no TCP listener in the production launch path**. The bridge reuses Codex's own cross-platform `codex_uds` implementation so the Rust TUI and Node DSH composition can stay in separate processes without unsafe FFI. -1. **DSH is authoritative.** No second session database and no second policy engine. -2. **Reuse Codex UI instead of imitating it.** Keep the upstream TUI diff small and auditable. -3. **Protocol boundary first.** Translate DSH Session/Agent/Event concepts into Codex Thread/Turn/Item concepts. -4. **Production transports only.** Experimental remote transports are useful for development, not the final runtime dependency. -5. **Windows is a first-class target.** Windows Terminal/PowerShell behavior is part of the release gate. +## Current integration status -## Repository status +`release/1.0-rc` is the sole production release-candidate integration branch, reviewed directly against `main` in Draft PR #11. Older M0/M1/thin-launcher/transport PRs are historical workstreams rather than release dependencies. -The repository is being reset around the thin-fork architecture. The earlier custom `pi-tui` prototype is preserved separately for reference and is **not** the target implementation. +The selected production transport is the small local-IPC bridge above. The alternative experiment that added a larger direct-stdio transport implementation inside the Codex client is intentionally excluded from the 1.0 line to keep the maintained upstream diff smaller. -See: +The candidate implementation includes: -- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) -- [`docs/ROADMAP.md`](docs/ROADMAP.md) -- [`docs/UX-PARITY.md`](docs/UX-PARITY.md) -- [`CONTRIBUTING.md`](CONTRIBUTING.md) -- [`SECURITY.md`](SECURITY.md) +- zero-argument `dshx` launch and `dshx resume` flows; +- official DSH runtime composition and exact supported DSH rc.8 release-closure checks; +- DSH-backed session/history, model, permissions, approvals, ask-user, tools/shell/diffs, plan/reasoning, steering/interrupt, rename, fork/compaction, skills and subagent presentation boundaries; +- pinned Codex TUI build plus packaged cross-platform local IPC bridge; +- an executable visible-slash-command contract so every runtime-owned command left visible is either DSH-backed or the build fails; +- release artifacts for Linux x64, Windows x64, macOS arm64 and macOS x64; +- clean-install `dshx doctor`, including a real local UDS + WebSocket-framing ping/pong data-plane self-check; +- Linux/macOS real-TUI PTY smoke and Windows real-TUI ConPTY + Chinese-text smoke through the production local-IPC topology; +- fail-closed ownership/security tests that keep DSH authoritative. -## Intended CLI +A commit is not called 1.0-ready merely because these paths exist. The exact release candidate must pass the repository CI/release gates and the side-by-side Windows Terminal/CJK/IME acceptance pass described in `docs/RELEASE-READINESS.md` and `docs/UX-PARITY.md`. + +## Install a release artifact + +Production users should install the **platform-specific tarball generated by a published DSHX release**, not build the pinned Codex workspace themselves. + +1. Download the tarball for your platform from the chosen GitHub release and verify its SHA-256 against that release's `SHA256SUMS`. +2. Install the downloaded artifact globally with npm: ```bash -dsh-codex -# or eventually -dsh --profile codex +npm install --global ./dshx---.tgz ``` -The user-facing experience should intentionally track Codex for the welcome screen, composer, slash commands, model/permission pickers, approvals, tool cells, diffs, plans, steering, resume picker, status/footer, scrolling and keyboard behavior. +3. Verify the installed product, then enter any code project and start it: + +```bash +dshx --version +dshx doctor +cd /path/to/project +dshx +``` + +Windows PowerShell uses the same npm install command. For a 1.0 release-candidate acceptance run, use `scripts/windows-rc-acceptance.ps1` so the tarball checksum, isolated install, `dshx --version` and `dshx doctor` are verified before the real Windows Terminal/IME comparison. + +The release tarball already contains the matching native `dshx-tui` and `dshx-ipc-bridge` binaries. A separate Codex install, DSH WebUI process, bridge command or remote-mode setup is not part of installation. + +## Build from source + +Source builds are for contributors and DSHX development. The pinned DSH runtime supports Node.js `^22.19.0 || >=24.0.0`; DSHX 1.0 standardizes dependency freezing, CI and release builds on **Node.js 24 LTS / npm 11**. Rust/Cargo, Git and the platform toolchain required by the pinned Codex workspace are also required for native TUI builds. + +The release dependency graph is frozen outside CI so GitHub Actions never mutates the candidate branch. On a trusted Node 24 LTS machine with npm-registry access, generate and verify the lock with: + +```bash +npm run freeze:deps +``` + +Review and commit the generated `package-lock.json`. Do not hand-edit or fabricate it. After the graph has been frozen, all contributor/CI installs use: + +```bash +npm ci +``` + +Linux/macOS: + +```bash +./scripts/build-codex-tui.sh +npm link +dshx doctor +dshx +``` + +Windows PowerShell: + +```powershell +.\scripts\build-codex-tui.ps1 +npm link +dshx doctor +dshx +``` + +`dshx doctor` checks the DSH-compatible Node runtime, the packaged TUI, the local IPC data plane, the official DSH composition and the isolated DSHX presentation home/IPC path. + +## Release model + +GitHub Actions is an automation surface rather than the authority for repository correctness. Ordinary PR/main CI is deliberately lean: one Node 24 core job followed by one Linux TUI job. `release/**` adds real Windows ConPTY and macOS PTY/CJK/resize gates. Only `v*` tags run the full platform packaging, clean-install, provenance, checksum and publication matrix. + +The release workflow builds platform-specific installable tarballs from the frozen npm graph, builds the pinned TUI and IPC bridge, clean-installs each artifact, runs `dshx doctor`, exercises Linux/macOS PTY and Windows ConPTY real-TUI gates, verifies cross-platform provenance, and publishes SHA-256 checksums for both tarballs and provenance sidecars. Release candidates containing `-` in the tag are published as prereleases. + +The publishable `npm-shrinkwrap.json` inside each artifact is derived from the reviewed source `package-lock.json`; packaging does not resolve a second dependency graph. Release sidecars record the source-lock SHA-256 plus the pinned Codex and DSH commits. + +## Design references + +- `docs/ARCHITECTURE.md` +- `docs/RUNTIME-INTEGRATION.md` +- `docs/DSH-CAPABILITY-MATRIX.md` +- `docs/ROADMAP.md` +- `docs/UX-PARITY.md` +- `docs/RELEASE-READINESS.md` +- `SECURITY.md` ## License -Apache-2.0. Any vendored upstream code keeps its original notices and license requirements. +Apache-2.0 for project-owned code. Synchronized upstream code keeps its applicable upstream notices and license requirements. diff --git a/SECURITY.md b/SECURITY.md index 73124044..7024e2ad 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ ## Scope -Security-sensitive areas include command execution, filesystem mutation, sandbox policy, approval translation, credential/model configuration, session persistence and remote/local transports. +Security-sensitive areas include command execution, filesystem mutation, sandbox policy, approval translation, credential/model configuration, session persistence and local presentation transports. ## Core invariant @@ -10,6 +10,32 @@ The TUI is presentation. DeepSeek Harness owns policy enforcement. The adapter m When a DSH permission concept cannot be represented faithfully by the current Codex UI/protocol, fail closed and surface the incompatibility to the user. +## Production transport + +DSHX does not expose its production presentation protocol on a TCP listener. The product topology is: + +```text +pinned Codex TUI + ↕ WebSocket framing over private local Unix-domain socket +packaged dshx-ipc-bridge + ↕ JSONL over child stdio +DSHX presentation adapter + ↕ official public APIs/events +DeepSeek Harness +``` + +The pinned TUI explicitly rejects TCP WebSocket endpoints in DSHX mode. + +On Unix, the rendezvous directory is a fresh random directory below the system temporary directory and both DSHX and pinned `codex_uds` enforce owner-only `0700` directory permissions. + +On Windows, pinned `codex_uds` cannot apply Unix permission bits, so DSHX deliberately does **not** trust an arbitrary/shared `%TEMP%` as the rendezvous root. The random socket directory is created below the current user's DSHX presentation home (`~/.dshx/codex-tui/i/` by default), inheriting the user's profile ACL. A custom `DSHX_TUI_HOME` is rejected if it resolves outside the current Windows user profile, including a different drive or a shared temporary directory, so the development override cannot weaken the production ACL boundary. The Windows suffix is deliberately short, and DSHX rejects an AF_UNIX pathname at or above the fixed 108-byte `sun_path` capacity before launching the bridge; the error instructs the user to select a shorter `DSHX_TUI_HOME` under the user profile. Path length is measured in UTF-8 bytes so non-ASCII profile paths fail predictably rather than at bind time. + +Startup is transactional. Socket-directory creation/path validation, official DSH boot, bridge readiness, and presentation-Adapter construction are one startup transaction. If any stage fails, DSHX closes readline state, terminates the bridge with bounded escalation, disposes the official DSH runtime when one exists, and removes the private rendezvous directory. Cleanup failures are diagnostic-only during failed startup: they must never replace the original startup error reported to the user. + +Normal shutdown is also one idempotent transaction. Concurrent `close()` callers await the same cleanup promise; adapter shutdown, bridge termination, DSH disposal and rendezvous removal each still run after an earlier cleanup failure. The first failure remains the reported root cause and later cleanup failures are diagnostic-only. Rendezvous cleanup therefore does not depend on successful adapter or DSH disposal. + +The isolated DSHX presentation home is never the user's ordinary `CODEX_HOME`. + ## Reporting Please report security issues privately to the repository owner rather than opening a public issue with exploit details. @@ -23,5 +49,12 @@ A production release must include regression coverage for: - interrupted turns; - session resume without privilege drift; - model/provider configuration preservation; +- user profile/home DSH patch files remaining unmodified by DSHX; - malformed/unknown protocol events; +- private local-IPC path creation and Windows AF_UNIX UTF-8 path limits; +- custom Windows presentation homes being rejected outside the current user profile ACL boundary; +- transactional startup rollback for socket creation, DSH boot, bridge readiness and Adapter-construction failures; +- startup cleanup failures preserving the original failure as the reported root cause; +- concurrent/idempotent shutdown and first-error preservation across adapter/bridge/DSH/rendezvous cleanup failures; +- no production TCP listener or Codex-core fallback; - transport disconnect/reconnect behavior where applicable. diff --git a/bin/dshx-stub-local.mjs b/bin/dshx-stub-local.mjs new file mode 100644 index 00000000..5750a9c4 --- /dev/null +++ b/bin/dshx-stub-local.mjs @@ -0,0 +1,87 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import { startDshxLocalServer } from '../src/dsh/local-server.mjs'; +import { ProtocolStub, normalizeDispatchResult } from '../src/protocol.mjs'; + +const delayMs = Number(process.env.DSHX_STUB_EVENT_DELAY_MS || 18); +const traceFile = process.env.DSHX_STUB_TRACE_FILE; +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +function trace(record) { + if (!traceFile) return; + fs.appendFileSync(traceFile, `${JSON.stringify({ at: Date.now(), ...record })}\n`, 'utf8'); +} + +class LocalStubAdapter { + constructor({ send, cwd }) { + this.send = send; + this.stub = new ProtocolStub({ cwd }); + } + + async handle(message) { + if (message?.id === undefined) { + trace({ direction: 'in', kind: 'notification', method: message?.method ?? null }); + return null; + } + trace({ direction: 'in', kind: 'request', id: message.id, method: message?.method ?? null }); + const { response, events } = normalizeDispatchResult(this.stub, message); + return { + response, + afterResponse: async () => { + trace({ + direction: 'out', + kind: response?.error ? 'error' : 'response', + id: response?.id ?? null, + method: message?.method ?? null, + error: response?.error?.message ?? null + }); + for (const event of events) { + const threadId = event?.params?.threadId ?? event?.params?.thread?.id; + const turnId = event?.params?.turnId ?? event?.params?.turn?.id; + const belongsToActiveTurn = + typeof threadId === 'string' && + typeof turnId === 'string' && + event.method !== 'turn/completed'; + if (belongsToActiveTurn && !this.stub.isTurnActive(threadId, turnId)) { + trace({ direction: 'drop', method: event.method, threadId, turnId, reason: 'inactive-before-delay' }); + break; + } + if (delayMs > 0) await sleep(delayMs); + if (belongsToActiveTurn && !this.stub.isTurnActive(threadId, turnId)) { + trace({ direction: 'drop', method: event.method, threadId, turnId, reason: 'inactive-after-delay' }); + break; + } + this.send(event); + trace({ direction: 'out', kind: 'notification', method: event.method, threadId: threadId ?? null, turnId: turnId ?? null }); + if ( + event.method === 'turn/completed' && + typeof threadId === 'string' && + typeof turnId === 'string' + ) { + this.stub.completeTurn(threadId, turnId); + } + } + } + }; + } + + async close() {} +} + +const server = await startDshxLocalServer({ + runtime: { async dispose() {} }, + Adapter: LocalStubAdapter, + log: (message) => process.stderr.write(`[dshx-local-stub] ${message}\n`) +}); +process.stdout.write(`${server.url}\n`); + +async function shutdown() { + try { + await server.close(); + } finally { + process.exit(0); + } +} + +process.on('SIGINT', shutdown); +process.on('SIGTERM', shutdown); diff --git a/bin/dshx-stub-server.mjs b/bin/dshx-stub-server.mjs new file mode 100644 index 00000000..0ae2602a --- /dev/null +++ b/bin/dshx-stub-server.mjs @@ -0,0 +1,21 @@ +#!/usr/bin/env node +import { startProtocolStubServer } from '../src/server.mjs'; + +const token = process.env.DSHX_STUB_TOKEN || undefined; +const server = await startProtocolStubServer({ + token, + log: (message) => process.stderr.write(`[dshx-stub] ${message}\n`) +}); + +process.stdout.write(`${server.url}\n`); + +async function shutdown() { + try { + await server.close(); + } finally { + process.exit(0); + } +} + +process.on('SIGINT', shutdown); +process.on('SIGTERM', shutdown); diff --git a/bin/dshx.mjs b/bin/dshx.mjs new file mode 100755 index 00000000..518bc714 --- /dev/null +++ b/bin/dshx.mjs @@ -0,0 +1,361 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import process from 'node:process'; +import { spawn, spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { parseCliInvocation } from '../src/cli/arguments.mjs'; +import { + DSHX_DOCTOR_BOOT_TIMEOUT_MS, + DSHX_DOCTOR_DISPOSE_TIMEOUT_MS, + isSupportedNodeVersion +} from '../src/cli/runtime.mjs'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const PACKAGE = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')); +const VERSION = PACKAGE.version; +const REQUIRED_RUNTIME_SERVICES = [ + 'agents', + 'agentDefaultModel', + 'llm', + 'sessions', + 'sessionPersistence', + 'sessionQuery', + 'sessionProjections', + 'sessionTitle', + 'attachments', + 'tools', + 'commands', + 'compaction', + 'subagents', + 'permissionPresets', + 'approval', + 'userQuestions', + 'skills', + 'planMode' +]; + +function usage() { + return `DSHX ${VERSION}\n\nUsage:\n dshx Start DSHX in the current project\n dshx Start with an initial prompt\n dshx resume Open the Codex-style DSH session picker\n dshx resume --last Resume the most recent DSH session\n dshx resume Resume a specific DSH session\n dshx doctor Check packaged TUI, local IPC and official DSH composition\n dshx --version Print version\n dshx --help Show this help\n\nEnvironment:\n DSHX_TUI_BIN Override the packaged DSHX TUI binary (development only)\n DSHX_IPC_BRIDGE_BIN Override the packaged local IPC bridge (development only)\n DSHX_TUI_HOME Override DSHX presentation-only Codex home (development only)\n DSHX_DEBUG=1 Print DSHX adapter diagnostics\n\nProduct boundary:\n DSHX owns only the launcher, Codex TUI thin fork and presentation adapter.\n DeepSeek Harness remains the authoritative Agent/Session/Tool runtime.\n DSHX never reads or writes the user's ordinary CODEX_HOME.\n`; +} + +function packagedTuiBinary() { + const name = process.platform === 'win32' ? 'dshx-tui.exe' : 'dshx-tui'; + const candidates = [ + process.env.DSHX_TUI_BIN, + path.join(ROOT, 'dist', 'bin', name), + path.join(ROOT, '.build', 'codex', 'release', process.platform === 'win32' ? 'codex-tui.exe' : 'codex-tui') + ].filter(Boolean); + return candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0]; +} + +function packagedIpcBridgeBinary() { + const name = process.platform === 'win32' ? 'dshx-ipc-bridge.exe' : 'dshx-ipc-bridge'; + const candidates = [ + process.env.DSHX_IPC_BRIDGE_BIN, + path.join(ROOT, 'dist', 'bin', name), + path.join(ROOT, '.build', 'codex', 'release', name) + ].filter(Boolean); + return candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0]; +} + +function dshxTuiHome() { + return path.resolve(process.env.DSHX_TUI_HOME || path.join(os.homedir(), '.dshx', 'codex-tui')); +} + +function commandDetail(command, result) { + if (result.status === 0) return (result.stdout || result.stderr).trim(); + if (result.error) return `${command} failed: ${result.error.message}`; + return `${command} is not runnable${result.stderr ? `: ${result.stderr.trim()}` : ''}`; +} + +function withTimeout(promise, timeoutMs, label) { + let timer; + return Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`${label} exceeded ${timeoutMs}ms`)), timeoutMs); + }) + ]).finally(() => clearTimeout(timer)); +} + +function presentationHomeCheck(localServerInternals, loadError) { + const home = dshxTuiHome(); + try { + fs.mkdirSync(home, { recursive: true, mode: 0o700 }); + const stat = fs.statSync(home); + if (!stat.isDirectory()) throw new Error('path exists but is not a directory'); + fs.accessSync(home, fs.constants.R_OK | fs.constants.W_OK); + if (!localServerInternals) { + throw new Error(`IPC policy unavailable because DSH modules failed to load: ${loadError}`); + } + + // On Windows this also enforces that a custom DSHX_TUI_HOME stays below + // the current user's profile ACL boundary. Probe the fixed suffix used by + // the real random rendezvous directory so an obviously-too-long home fails + // in doctor before a runtime boot is attempted. + const socketRoot = localServerInternals.defaultSocketRoot({ home }); + const probeSocket = path.join(socketRoot, 'd-XXXXXX', 's'); + localServerInternals.assertSocketPathSupported(probeSocket); + return { ok: true, detail: home }; + } catch (error) { + return { ok: false, detail: `${home}: ${error instanceof Error ? error.message : String(error)}` }; + } +} + +async function officialRuntimeCheck(runtimeBoot, loadError) { + if (typeof runtimeBoot?.bootDshxRuntime !== 'function') { + return { ok: false, detail: `official DSH modules failed to load: ${loadError ?? 'unknown error'}` }; + } + + let ctx; + let primaryError; + let cleanupError; + try { + // Doctor deliberately exercises the same official composition and profile + // watcher path as production startup. This catches missing native optional + // dependencies (for example sharp/koffi) that a static bundle-entry check + // cannot detect. The bounded timeout is intentionally generous enough for + // first-run Windows module/native initialization on supported hardware. + ctx = await withTimeout( + runtimeBoot.bootDshxRuntime({ cwd: process.cwd(), watch: true }), + DSHX_DOCTOR_BOOT_TIMEOUT_MS, + 'official DSH composition boot' + ); + const missing = REQUIRED_RUNTIME_SERVICES.filter((name) => ctx.get(name) == null); + if (missing.length > 0) { + throw new Error(`official DSH composition is missing required presentation services: ${missing.join(', ')}`); + } + } catch (error) { + primaryError = error; + } finally { + if (ctx) { + try { + await withTimeout( + Promise.resolve(ctx.dispose?.()), + DSHX_DOCTOR_DISPOSE_TIMEOUT_MS, + 'official DSH composition disposal' + ); + } catch (error) { + cleanupError = error; + } + } + } + + if (primaryError) { + const detail = primaryError instanceof Error ? primaryError.message : String(primaryError); + const cleanup = cleanupError + ? `; secondary cleanup failure: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}` + : ''; + return { ok: false, detail: `${detail}${cleanup}` }; + } + if (cleanupError) { + return { + ok: false, + detail: `official DSH composition disposal failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}` + }; + } + return { + ok: true, + detail: `official DSH composition boot/dispose (${REQUIRED_RUNTIME_SERVICES.length} required services)` + }; +} + +async function doctor() { + const executable = packagedTuiBinary(); + const tuiCheck = executable && fs.existsSync(executable) + ? spawnSync(executable, ['--version'], { encoding: 'utf8' }) + : { status: 127, stdout: '', stderr: '', error: null }; + const bridge = packagedIpcBridgeBinary(); + const bridgeCheck = bridge && fs.existsSync(bridge) + ? spawnSync(bridge, ['--check'], { encoding: 'utf8' }) + : { status: 127, stdout: '', stderr: '', error: null }; + + let localServerInternals; + let runtimeBoot; + let runtimeLoadError; + try { + const [localServer, loadedRuntimeBoot] = await Promise.all([ + import('../src/dsh/local-server.mjs'), + import('../src/dsh/runtime-boot.mjs') + ]); + localServerInternals = localServer.localServerInternals; + runtimeBoot = loadedRuntimeBoot; + } catch (error) { + runtimeLoadError = error instanceof Error ? error.message : String(error); + } + const homeCheck = presentationHomeCheck(localServerInternals, runtimeLoadError); + const runtimeCheck = await officialRuntimeCheck(runtimeBoot, runtimeLoadError); + + const rows = [ + ['Node', `${process.version} (required: ${PACKAGE.engines.node})`, isSupportedNodeVersion(process.versions.node)], + [ + 'Pinned Codex TUI', + tuiCheck.status === 0 ? commandDetail(executable, tuiCheck) : (executable ? commandDetail(executable, tuiCheck) : 'not found'), + tuiCheck.status === 0 + ], + [ + 'Local IPC bridge', + bridgeCheck.status === 0 ? `${bridge} (cross-platform Codex UDS)` : (bridge ? commandDetail(bridge, bridgeCheck) : 'not found'), + bridgeCheck.status === 0 + ], + ['DeepSeek Harness', runtimeCheck.detail, runtimeCheck.ok], + ['Presentation home / IPC path', homeCheck.detail, homeCheck.ok] + ]; + for (const [name, detail, ok] of rows) { + process.stdout.write(`${ok ? '✓' : '✗'} ${name}: ${detail}\n`); + } + if (rows.some((row) => !row[2])) { + process.stderr.write( + process.platform === 'win32' + ? 'For a source checkout, build the pinned TUI + IPC bridge with `.\\scripts\\build-codex-tui.ps1`; packaged releases include both.\n' + : 'For a source checkout, build the pinned TUI + IPC bridge with `./scripts/build-codex-tui.sh`; packaged releases include both.\n' + ); + process.exitCode = 1; + } +} + +async function run() { + const args = process.argv.slice(2); + let invocation; + try { + invocation = parseCliInvocation(args); + } catch (error) { + process.stderr.write(`dshx: ${error.message}\n`); + process.exitCode = 2; + return; + } + + if (invocation.kind === 'help') { + process.stdout.write(usage()); + return; + } + if (invocation.kind === 'version') { + process.stdout.write(`${VERSION}\n`); + return; + } + if (invocation.kind === 'doctor') { + await doctor(); + return; + } + + if (!isSupportedNodeVersion(process.versions.node)) { + process.stderr.write(`dshx: unsupported Node ${process.version}; pinned DeepSeek Harness requires ${PACKAGE.engines.node}\n`); + process.stderr.write('Use Node 24 LTS for the DSHX 1.0 release baseline.\n'); + process.exitCode = 1; + return; + } + + let startDshxLocalServer; + try { + ({ startDshxLocalServer } = await import('../src/dsh/local-server.mjs')); + } catch (error) { + process.stderr.write(`dshx: failed to load official DeepSeek Harness runtime: ${error instanceof Error ? error.message : error}\n`); + process.exitCode = 1; + return; + } + + const executable = packagedTuiBinary(); + if (!executable || !fs.existsSync(executable)) { + process.stderr.write(`dshx: packaged TUI binary not found (${executable ?? 'unknown path'})\n`); + process.stderr.write('Run `dshx doctor` for build/install guidance.\n'); + process.exitCode = 127; + return; + } + const bridge = packagedIpcBridgeBinary(); + if (!bridge || !fs.existsSync(bridge)) { + process.stderr.write(`dshx: packaged local IPC bridge not found (${bridge ?? 'unknown path'})\n`); + process.stderr.write('Run `dshx doctor` for build/install guidance.\n'); + process.exitCode = 127; + return; + } + + const tuiHome = dshxTuiHome(); + try { + fs.mkdirSync(tuiHome, { recursive: true, mode: 0o700 }); + } catch (error) { + process.stderr.write(`dshx: cannot create presentation home ${tuiHome}: ${error instanceof Error ? error.message : error}\n`); + process.exitCode = 1; + return; + } + + const debug = process.env.DSHX_DEBUG === '1'; + const log = debug ? (message) => process.stderr.write(`[dshx] ${message}\n`) : () => {}; + let local; + try { + local = await startDshxLocalServer({ + cwd: process.cwd(), + home: tuiHome, + version: VERSION, + bridgeCommand: bridge, + log + }); + } catch (error) { + process.stderr.write(`dshx: failed to boot DeepSeek Harness/local IPC: ${error instanceof Error ? error.message : error}\n`); + process.exitCode = 1; + return; + } + + let child; + try { + child = spawn(executable, invocation.tuiArgs, { + cwd: process.cwd(), + env: { + ...process.env, + // Do not inherit CODEX_HOME: DSHX uses Codex code as a presentation + // component only and must not read/write the user's ordinary Codex state. + CODEX_HOME: tuiHome, + ...invocation.resumeEnv, + DSHX_APP_SERVER_ENDPOINT: local.url + }, + stdio: 'inherit', + windowsHide: false + }); + } catch (error) { + process.stderr.write(`dshx: failed to launch pinned Codex TUI: ${error instanceof Error ? error.message : error}\n`); + try { + await local.close(); + } catch (cleanupError) { + if (debug) process.stderr.write(`[dshx] startup cleanup: ${cleanupError instanceof Error ? cleanupError.message : cleanupError}\n`); + } + process.exitCode = 127; + return; + } + + let closing = false; + const close = async (exitCode) => { + if (closing) return; + closing = true; + try { + await local.close(); + } catch (error) { + process.stderr.write(`dshx: shutdown cleanup failed: ${error instanceof Error ? error.message : error}\n`); + if (exitCode === 0) exitCode = 1; + } + process.exitCode = exitCode; + }; + + child.on('error', async (error) => { + process.stderr.write(`dshx: failed to launch pinned Codex TUI: ${error.message}\n`); + await close(127); + }); + child.on('exit', async (code, signal) => { + if (signal && debug) process.stderr.write(`[dshx] TUI exited via ${signal}\n`); + await close(code ?? (signal ? 1 : 0)); + }); + + // Process-lifecycle signals are forwarded to the TUI. Do not intercept + // SIGINT: Ctrl+C is an in-TUI interaction used by Codex to interrupt the + // active DSH turn. + process.once('SIGTERM', () => { + if (!child.killed) child.kill('SIGTERM'); + }); + if (process.platform !== 'win32') { + process.once('SIGHUP', () => { + if (!child.killed) child.kill('SIGHUP'); + }); + } +} + +await run(); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c004c68b..2139b10f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2,94 +2,117 @@ ## Objective -`dsh-codex` should preserve the Codex terminal interaction model while replacing the Codex agent backend with DeepSeek Harness. +DSHX is a terminal frontend for official DeepSeek Harness. Its purpose is to preserve the pinned Codex terminal interaction model while projecting DSH public runtime state into that UI. -## Ownership boundary +It is **not** a fork of DeepSeek Harness and must not grow into an agent framework. + +## Normative ownership boundary | Concern | Owner | |---|---| -| Terminal layout / composer / keyboard / overlays | Codex TUI thin fork | -| Session persistence | DSH | -| Agent loop | DSH | -| Model routing | DSH | -| Tool registry and execution | DSH | -| Sandbox and approval policy | DSH | -| Skills / subagents / jobs | DSH | -| Protocol translation | dsh-codex adapter | +| Terminal layout / composer / keyboard / overlays | DSHX / Codex TUI thin fork | +| UX parity and branding | DSHX | +| `dshx` launcher / packaging | DSHX | +| Public API/event → TUI protocol projection | DSHX thin adapter | +| Session persistence | DeepSeek Harness upstream | +| Agent loop | DeepSeek Harness upstream | +| Model routing | DeepSeek Harness upstream | +| Tool registry and execution | DeepSeek Harness upstream | +| Sandbox and approval policy | DeepSeek Harness upstream | +| Skills / subagents / plugins / jobs | DeepSeek Harness upstream | + +The adapter may translate and cache disposable presentation state. It must never become a second source of truth for session history, policy, model configuration, tools or agent state. -The TUI must never become a second source of truth for session history, tool policy, model configuration or agent state. +If a DSH capability is unavailable or cannot be represented safely, DSHX must disable, hide or explicitly mark the corresponding UI unsupported. It must **not** implement the missing runtime capability. ## Target topology ```text ┌─────────────────────────────────────┐ -│ Codex TUI thin fork │ +│ Codex TUI thin fork │ project-owned presentation │ │ │ welcome / composer / cells / diff │ │ slash commands / pickers / footer │ └──────────────────┬──────────────────┘ │ - app-server-compatible API + app-server-compatible view │ ┌──────────────────▼──────────────────┐ -│ dsh-codex protocol adapter │ +│ dsh-codex thin adapter │ project-owned translation │ │ -│ Thread ↔ Session │ -│ Turn ↔ Agent turn │ -│ Item ↔ DSH event/tool projection │ +│ Thread ↔ Session id/projection │ +│ Turn ↔ DSH turn lifecycle │ +│ Item ↔ exposed DSH events │ └──────────────────┬──────────────────┘ - │ Cordis +═══════════════════╪════════════════════ ownership boundary + │ official public APIs/events ┌──────────────────▼──────────────────┐ -│ DeepSeek Harness │ +│ DeepSeek Harness │ upstream-owned runtime │ │ │ agents / sessions / llm / tools │ -│ sandbox / approvals / skills │ +│ sandbox / approvals / skills / ... │ └─────────────────────────────────────┘ ``` ## Protocol mapping -The adapter starts with a deliberately small vertical slice. +Mappings are UI projections, not replacement implementations. -| Codex concept | DSH concept | +| Codex concept | DSH source | |---|---| -| Thread | Session | -| `thread/start` | create DSH Session + Agent | -| `thread/resume` | inspect and resume DSH Session | -| `thread/list` | DSH session catalog | -| Turn | one user interaction with a DSH Agent | -| agent-message delta | model/agent streaming event | -| command execution item | DSH shell/tool event | -| file-change item | DSH filesystem/editor event | -| approval request | DSH approval request | -| user-input request | DSH ask-user request | -| plan update | DSH plan/todo projection | -| turn interrupt | DSH turn cancellation | +| Thread | DSH Session identity and exposed metadata | +| `thread/start` | official DSH Session/Agent creation API | +| `thread/resume` | official DSH Session inspection/resume API | +| `thread/list` | official DSH session catalog | +| Turn | official DSH user-interaction lifecycle | +| agent-message delta | exposed DSH agent/model streaming event | +| command execution item | exposed DSH tool/shell event | +| file-change item | exposed DSH filesystem/editor event | +| approval request | official DSH approval request | +| user-input request | official DSH ask-user request | +| plan update | exposed DSH plan/todo state | +| turn interrupt | official DSH cancellation/control API | + +Where no official mapping exists, the adapter reports unsupported instead of synthesizing a shadow capability. ## Session invariants -1. DSH session IDs are authoritative. -2. Resume reads model/provider/reasoning configuration from persisted DSH session events where available. -3. The TUI may cache presentation state, but it must be disposable. -4. No Codex session database is used for DSH conversations. +1. DSH session IDs and persisted events are authoritative. +2. DSHX does not keep a parallel conversation database. +3. Resume obtains model/provider/reasoning state from DSH through supported interfaces; the TUI does not invent replacements. +4. TUI caches are disposable and reconstructable. +5. DSHX never changes permission semantics to make a Codex widget easier to reuse. -## Upstream strategy +## Codex upstream strategy -The Codex TUI fork should be pinned to a known upstream commit. Upstream changes are integrated as explicit sync commits. Product-specific modifications should live behind narrow traits/interfaces whenever practical. +The TUI is pinned to a known `openai/codex` commit. Upstream changes are integrated as explicit sync commits. Project-specific changes should live behind narrow extension points so rendering/input code remains close to upstream. -The desired long-term diff is: +Desired long-term diff: ```text Codex TUI upstream 95%+ unchanged -DSH adapter project-owned +DSH UI adapter project-owned and thin Branding minimal patch -DSH-specific commands isolated extension points +DSH-specific UI isolated extension points only ``` ## Runtime transport -Development may use Codex remote/app-server modes to validate mapping quickly. A production release should use a local production-grade transport (same process, stdio, or local socket) and must not depend on an upstream transport explicitly documented as experimental. +M0 may use Codex remote/app-server WebSocket mode strictly as a development protocol harness. It is not an accepted production dependency. + +Production `dshx` must hide transport/runtime orchestration from the user and use a local supported integration path (for example in-process, stdio or a production-grade local socket) consistent with the pinned TUI architecture. + +## Launcher contract + +Production 1.0 must support: + +```bash +cd +dshx +``` + +The user must not need to invoke `codex --remote`, a bridge script, package-manager commands or DSH profile plumbing for normal startup. ## Security model -The UI renders approval decisions; DSH enforces them. The adapter must not silently upgrade permissions. Any translation that cannot faithfully represent a DSH permission request must fail closed and surface an explicit unsupported-policy message. +The TUI renders approval decisions; DSH enforces them. Translation must never silently upgrade privileges. If the current Codex UI/protocol cannot faithfully represent a DSH permission request, DSHX fails closed and surfaces an explicit incompatibility. diff --git a/docs/DSH-CAPABILITY-MATRIX.md b/docs/DSH-CAPABILITY-MATRIX.md new file mode 100644 index 00000000..280a15d5 --- /dev/null +++ b/docs/DSH-CAPABILITY-MATRIX.md @@ -0,0 +1,81 @@ +# DSH public capability matrix + +This document is a guardrail for the DSHX adapter. Every Codex-facing behavior must map to an **official DeepSeek Harness public API/event**. If no suitable public capability exists at the supported DSH release, DSHX marks the surface unsupported or degrades the presentation; it does not implement a replacement runtime. + +Supported DSH package line: `0.1.0-rc.8`. +Pinned DSH source reference: `141eb6fef83422698aef7a981029e843e8161534`. + +## Core interaction mapping + +| Codex/TUI need | Official DSH source | DSHX responsibility | Status | +|---|---|---|---| +| Start a conversation | `ctx.agents.create(...)` | Project result into `thread/start` / Thread UI | Implemented + tested | +| Resume a conversation | `ctx.agents.resume({ resumeSessionId })` | Project resumed Session into Thread/resume UI | Implemented + tested | +| Live agent lookup | `ctx.agents.get(id)`, `list()`, `roots()` | Presentation/navigation only | Implemented + tested | +| User follow-up | `Agent.followup(message)` | Convert composer input to official DSH user content | Implemented + tested | +| Steering while running | `Agent.steer(message)` | Forward active-turn steering from composer | Implemented + tested | +| Cancel / Ctrl+C | `Agent.cancel(cause, options?)` | Map TUI interrupt intent; never invent policy | Implemented + tested | +| Wait for quiescence | `Agent.whenIdle()` | Drive UI busy/idle convergence | Implemented + tested | +| Agent/subagent status | `agent/status`, `agent/created`, `agent/disposed` | Render thread active/idle/not-loaded state | Implemented + tested | +| Durable transcript source | `Session` append-only event log | Project only; never create a second transcript DB | Implemented + guarded | +| Session event feed | `session/event` | Translate exposed event vocabulary to TUI items | Implemented + tested | +| User message | `user/message` | Render transcript/input history | Implemented + tested | +| Token/chunk streaming | `assistant/chunk` | Convert visible/reasoning chunks to Codex deltas | Implemented + tested | +| Final assistant message | `assistant/message` | Finalize message/reasoning items | Implemented + tested | +| Tool invocation/result | `tool/call`, `tool/result` | Render matching Codex tool/shell/file/diff cells; never execute locally | Implemented + tested | +| File contextual diff | producing tool result/meta | Present diff/card only | Implemented where DSH emits faithful metadata | +| Plan / todo | `todo/write`, `planMode` | Project plan list/mode into Codex presentation | Implemented + tested | +| Provider/model catalog | `ctx.llm.listProviders()`, `listModels()`, `resolveModelInfo()` | Populate `/model` using public registry | Implemented + tested | +| Model/reasoning validation | `ctx.llm.resolveCallConfig()` + official model-selection service | Validate/apply picker changes through DSH | Implemented + tested | +| Persisted route metadata | `request/header` | Restore model/reasoning presentation on resume | Implemented + tested | +| Context/token usage | `request/context` + persisted session usage | Feed footer/usage projection when exposed | Implemented + tested | +| Session listing/inspection | `ctx.sessionPersistence.list()`, `inspect()` | Resume picker/history hydration | Implemented + tested | +| Session persistence/crash repair | `ctx.sessionPersistence` implementations | **None** — DSH owns durability and repair | Upstream-owned | +| Paginated history | persisted Session events | Convert immutable events into Codex turn/item pages | Implemented + tested | +| Permission preset list/current | `ctx.permissionPresets` | Populate Codex-style permissions selector | Implemented + tested | +| Permission preset switch | `ctx.permissionPresets.set(...)` | Delegate selected value; do not write policy events ourselves | Implemented + tested | +| Effective sandbox/approval state | DSH sandbox/approval services | Present DSH enforcement without inventing Codex semantics | Implemented + tested | +| Interactive approval request | scoped `approval/request` waterfall | Present only faithful one-shot decisions; fail closed otherwise | Implemented + tested | +| Ask user | `ctx.userQuestions.registerProvider(...)` | Present Codex request-user-input overlay and return DSH answer | Implemented for faithful single-select/free-text cases | +| Skills discovery | official DSH skills registry/provider surface | Adapt current registry snapshot to `skills/list` | Implemented + tested | +| Session fork | Host API `sessions.fork` | Select DSH event boundary and delegate durable fork | Implemented + ownership guarded | +| Manual compaction | official DSH `/compact` command | Invoke through DSH command service; render compaction events | Implemented + ownership guarded | +| Shell execution | official DSH `tools.execute` with owning Agent | Map Codex shell request/result to DSH tool execution | Implemented + ownership guarded | +| Workspace diff command | official DSH `tools.execute` with owning Agent | Use DSH execution, then present result | Implemented + ownership guarded | +| Subagent lifecycle | official `subagents` service + agent lifecycle events | Present child threads/status; delegate interrupt authority | Implemented + fail-closed authority tests | + +## Permission semantic notes + +DSH sandbox mode governs the semantics exposed by DSH; DSHX must not invent network/process guarantees that are absent from that public contract. Where Codex's legacy sandbox shape is broader than DSH's vocabulary, DSHX presents an external sandbox plus the actual DSH permission preset rather than falsely claiming Codex-native enforcement. + +DSH approval outcomes are closed and fail-closed. Only an explicit DSH grant is projected as approval. Codex's session-wide approval actions are hidden when no faithful DSH equivalent exists. Unknown plugin-tool approvals are not guessed into shell/file semantics; unclaimed requests remain unavailable/denied according to DSH policy. + +## Session durability and restart semantics + +DSHX keeps no durable transcript. Resume calls `ctx.agents.resume(...)`, history is read from DSH persistence, and the latest persisted `request/header` wins over the current machine default when restoring model/reasoning presentation. DSHX does not repair, replay or rewrite Session state after a crash. + +## Known deliberate capability gaps + +These are not shadow-implemented: + +- **DSH multi-select user questions:** the pinned Codex request-user-input presentation does not provide a faithful multi-select contract in the current adapter, so DSHX rejects that request instead of silently changing its semantics. +- **Codex-specific review/apps/plugins/hooks/memory/personality surfaces:** hidden where the supported DSH release has no faithful public equivalent wired into DSHX. +- **Jobs/workflows UI:** DSH may own these capabilities, but a stable terminal presentation contract has not yet been adopted; this is a presentation-only future surface. +- **Ephemeral Codex fork / goal-continuation semantics:** not mapped onto durable DSH Session forks. +- **Codex service-tier and reasoning-summary/personality thread settings:** rejected because there is no equivalent public DSH setting to project. + +A visible Codex command is kept only when its semantics are backed by DSH or are purely local TUI presentation. Unsupported runtime-owned commands are hidden rather than routed to Codex core. + +## Capability fallback rule + +For every UI feature: + +1. Detect whether the supported DSH composition exposes a faithful public capability. +2. If yes, adapt it narrowly into the Codex TUI surface. +3. If semantics are only partially representable, show an explicit reduced/unsupported state. +4. If representing it would weaken permissions, authority or durability semantics, fail closed. +5. Never solve a missing upstream capability by adding a shadow runtime to `dsh-codex`. + +## Release evidence + +This matrix describes implementation/ownership, not release-candidate pass status. The exact candidate must still satisfy `docs/RELEASE-READINESS.md`, including CI, packaged local-IPC checks and manual Windows Terminal/CJK/IME parity acceptance. diff --git a/docs/RELEASE-READINESS.md b/docs/RELEASE-READINESS.md new file mode 100644 index 00000000..7a38a4bc --- /dev/null +++ b/docs/RELEASE-READINESS.md @@ -0,0 +1,88 @@ +# 1.0 release readiness + +This file defines the evidence required before a DSHX release candidate may be promoted to 1.0. It intentionally distinguishes **implemented checks** from **checks that have actually passed on a release candidate**. + +## Rule + +No item becomes green because code for the check exists. A release candidate is green only when the corresponding repository gate or CI job has passed for the exact candidate commit/tag and the result is recorded. GitHub Actions is an automation surface, not the authority for release correctness: runner-independent repository scripts are preferred wherever a check does not intrinsically require a hosted platform. + +The pinned DSH `0.1.0-rc.8` line requires Node `^22.19.0 || >=24.0.0`. DSHX 1.0 standardizes dependency freezing, CI and release artifact generation on **Node 24 LTS / npm 11** so all release evidence uses one supported toolchain. + +## Automation tiers + +- **PR / main CI:** two Ubuntu jobs only. `Core / Node 24` runs first; `Linux TUI / pinned Codex / local IPC` runs only after Core passes. +- **RC platform gate:** `release/**` PR heads targeting `main` (or manual dispatch) run real Windows ConPTY and macOS PTY/CJK/resize validation with explicit exact-head checkout verification. +- **Release tag:** `v*` runs the full platform packaging, clean-install, provenance, checksum and publication matrix. +- **Dependency freeze:** never writes a branch from CI. Run `npm run freeze:deps` in a trusted Node 24 LTS / npm 11 environment with npm-registry access, review the generated `package-lock.json`, then commit it. CI consumes it with `npm ci`. + +## Automated gates + +| Gate | Evidence implemented in repository | Required result for 1.0 | +|---|---|---| +| Zero-argument launch | `bin/dshx.mjs`, clean-install smoke | pass on supported release artifacts | +| Supported Node runtime | package engine matches pinned DSH (`^22.19.0 || >=24.0.0`); release/freeze baseline Node 24 LTS | pass | +| Official DSH composition | `scripts/runtime-smoke.mjs` | pass on exact candidate | +| Frozen source dependency graph | checked-in `package-lock.json`; `npm run freeze:deps` resolves it with install scripts disabled under Node 24/npm 11 | lock present, reviewed and no manifest drift | +| Exact DSH dependency closure | `scripts/verify-dsh-closure.mjs`, source lock and release shrinkwrap | pass | +| DSH ownership boundary | adapter/unit tests + `scripts/verify-ownership-boundary.mjs` | pass | +| Approval/permission fail-closed | approval/permission/subagent authority tests | pass | +| Session resume/durability projection | DSH `agents.resume`, persistence projection and resume tests; persisted model/header wins over machine default | pass | +| Local production transport | no production TCP listener; pinned TUI rejects TCP WebSocket endpoint | pass | +| Cross-platform local IPC data plane | `dshx-ipc-bridge --check` performs real private UDS + WebSocket-framing ping/pong | pass on Linux/Windows/macOS | +| Visible slash-command contract | `scripts/verify-slash-contract.mjs` classifies every pinned Codex command and asserts DSH-backed methods for every runtime-owned command left visible in DSHX | pass | +| Real pinned TUI / Linux + macOS | PTY smoke drives pinned TUI through local IPC, resizes the terminal, and roundtrips a CJK prompt | pass on both Unix release platforms | +| Real pinned TUI / Windows | ConPTY smoke drives pinned TUI through the same local-IPC topology, resizes the terminal, and roundtrips a Chinese prompt | pass | +| Automated CJK/resize sanity | Linux/macOS PTY + Windows ConPTY verify UTF-8 input/echo survives a live resize without process/session loss | pass | +| CI action supply chain | every external `uses:` ref is a current GitHub Action release pinned to a full immutable commit SHA; `test/ci-actions-pins.test.mjs` rejects mutable refs | pass | +| Packaging | platform tarball + static local-import closure check + publishable shrinkwrap derived from the frozen source graph | pass | +| Clean installation | install generated tarball then `dshx --version` + `dshx doctor` | pass on every release platform | +| Release provenance | platform sidecar records Codex pin, DSH pin and SHA-256 of the frozen source `package-lock.json` | present and consistent across artifacts | +| Release artifacts | Linux x64, Windows x64, macOS arm64, macOS x64 | all built | +| Integrity | release `SHA256SUMS` covers tarballs and provenance sidecars | published | +| Codex thin-fork invariants | patch application/build + `scripts/verify-tui-invariants.mjs` | pass | + +## Manual UX acceptance gate + +Automation can exercise PTY/ConPTY resize and Chinese text, but it cannot establish real **IME composition-state behavior**, mouse behavior, or visual parity. For the exact 1.0 release candidate, perform a side-by-side run against the pinned Codex TUI using the same Windows Terminal dimensions/theme and record pass/fail for: + +- Windows Terminal startup, visual reflow after resize, scroll and mouse behavior; +- Chinese IME composition/commit/cancel in the multiline composer; +- wide CJK glyph alignment in transcript/tool/diff cells; +- slash completion and help keyboard behavior; +- model and permission pickers; +- approval and ask-user overlays; +- shell/tool streaming and collapse behavior; +- unified diff review; +- plan/reasoning cells when DSH exposes them; +- steering and Ctrl+C interrupt during an active turn; +- resume picker and restored session/model state; +- status/footer rendering. + +Before starting the manual comparison, verify and install the **exact generated Windows artifact** with: + +```powershell +./scripts/windows-rc-acceptance.ps1 ` + -Tarball .\dshx--win32-x64.tgz ` + -ExpectedSha256 +``` + +The harness verifies the artifact hash, performs an isolated global-prefix install, runs `dshx --version` and `dshx doctor`, and prints the exact installed `dshx.cmd` path to use for the Windows Terminal/IME session. Do not substitute a source checkout after this setup. + +Any difference is either fixed, documented as a deliberate product difference in `docs/UX-PARITY.md`, or blocks 1.0. + +## Promotion procedure + +1. In a trusted Node 24 LTS / npm 11 environment with npm-registry access, run `npm run freeze:deps`; review and commit the generated `package-lock.json`, then freeze the candidate commit on `release/1.0-rc`. Codex/DSH pins must not change during validation. +2. Require the exact candidate to pass the lean `CI` workflow (`Core / Node 24` then `Linux TUI`) and the `RC Platform Gate` (Windows ConPTY + macOS PTY). Equivalent runner-independent repository gates may be used for diagnosis, but platform acceptance must execute on the named platform. +3. Create a prerelease tag (for example `v1.0.0-rc.N`) and require every release-matrix build/clean-install/TUI gate to pass. The RC tag must point at the current `release/1.0-rc` head. +4. Confirm each release sidecar identifies the same frozen source-lock SHA-256 and expected Codex/DSH pins. +5. Run `scripts/windows-rc-acceptance.ps1` against the generated Windows release artifact and its published SHA-256, then use the printed installed launcher for the manual Windows Terminal/CJK/IME side-by-side acceptance run. +6. Resolve every failed gate without weakening a DSH ownership or security boundary. +7. Re-run affected automated and manual gates after fixes. +8. Merge PR #11 only after all RC gates are green; stable `v1.0.0` must point at the current `main` head and the release ledger issues #12–#15 must be closed. + +## Current integration note + +`release/1.0-rc` is the sole release-candidate integration branch. It is based on the production local-IPC implementation from `agent/production-ipc`; the alternative direct-stdio Codex-client experiment is intentionally excluded because it requires a much larger upstream TUI/client patch and would increase DSHX maintenance ownership. + +Older stacked draft PRs remain historical engineering workstreams only. They are not release evidence. The exact `release/1.0-rc` head and its eventual `v1.0.0-rc.N` tag are the only commits whose automated/manual evidence may be used to promote 1.0. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1d2a4f02..99c929fd 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,59 +1,88 @@ # Roadmap -## Milestone 0 — Protocol proof +All milestones obey one ownership rule: **DSHX maintains the TUI, launcher, transport and thin public-API adapter only. DeepSeek Harness capabilities remain upstream-owned.** -Goal: prove that an unmodified or minimally modified Codex TUI can render a DSH-backed conversation. +Milestone status describes implementation progress. A 1.0 release requires the independent evidence in `RELEASE-READINESS.md`. -- Pin a Codex upstream commit. -- Implement initialize / thread start / turn start / agent-message streaming. -- Map one DSH read/search tool event into a Codex item. -- Support interrupt. -- Run on Windows Terminal and Linux. +## Milestone 0 — Codex TUI protocol proof — complete -Exit gate: `input → DSH → tool → streaming answer` works end to end in the Codex TUI. +Goal: prove that the pinned Codex TUI can be driven without Codex's agent runtime. -## Milestone 1 — Daily coding loop +Delivered: -- Session resume/list. -- Shell command cells. -- File change + unified diff. -- Approval requests. -- Ask-user prompts. -- Steering during an active turn. -- Model picker backed by DSH model registry. -- Context/token footer. -- Reliable Ctrl+C behavior. +- pinned Codex and DSH upstream references; +- deterministic app-server-compatible development stub; +- bootstrap/thread/turn/streaming/interrupt protocol proof; +- zero-argument development launcher; +- thin-fork patches proving the Codex TUI can run with DSHX as its backend. -Exit gate: the CLI can be used for a full coding task without falling back to DSH WebUI. +The historical TCP/WebSocket path is retained only as a development fixture and is excluded from the production package. -## Milestone 2 — UX parity +## Milestone 1 — Official DSH public-API adapter / daily coding loop — implementation complete, RC evidence pending -- Welcome screen parity. -- Slash command palette parity. -- `/model`, `/permissions`, `/status`, `/review` behavior parity where concepts map cleanly. -- Resume picker parity. -- Plan/reasoning cells. -- Scroll/search/mouse/resize behavior. -- Windows Terminal and CJK/IME regression tests. +Delivered through official DSH public APIs/events: -Exit gate: side-by-side comparison with the pinned Codex release passes the UX parity matrix. +- session start/resume/list/history projection; +- response + reasoning streaming; +- specialized shell/tool/file/diff presentation where DSH exposes faithful semantics; +- approval and ask-user presentation with DSH policy authoritative; +- steering and Ctrl+C interrupt; +- model/provider/reasoning picker backed by official DSH registry/resolution; +- context/token/footer projection where DSH exposes the data; +- skills discovery; +- fork, compaction and subagent boundaries delegated to DSH-owned services; +- fail-closed ownership tests preventing local execution/runtime duplication. -## Milestone 3 — DSH-native capabilities +Exit gate: the exact release candidate must pass the automated daily-loop/PTY gates and real dogfooding; implementation alone does not declare the milestone release-green. -- Skills. -- Subagents. -- Jobs/workflows. -- DSH-specific command namespace without disturbing Codex-compatible commands. -- Plugin diagnostics and configuration surface. +## Milestone 2 — Codex UX parity — thin-fork parity implemented; manual terminal acceptance pending -## Milestone 4 — Production 1.0 +Implemented/inherited from the pinned Codex TUI: -- Versioned protocol compatibility contract. -- Crash recovery and session durability tests. -- Security/approval fail-closed tests. -- Windows/Linux/macOS release artifacts. -- Reproducible CI builds and checksums. -- Upstream sync playbook. -- Release candidate dogfooding gate. +- welcome screen, composer and slash-command interaction; +- model and permission pickers; +- resume picker using DSH session data; +- approval and ask-user overlays; +- tool/shell/diff/plan/reasoning cells; +- steering/interrupt behavior; +- Codex-native scroll, mouse and resize behavior; +- status/footer rendering where DSH exposes data. -1.0 is released only when the daily coding loop and UX parity matrix are both green on supported platforms. +Unsupported Codex runtime semantics are explicitly hidden/rejected rather than silently emulated. See `DSH-CAPABILITY-MATRIX.md`. + +Exit gate still pending: side-by-side Windows Terminal + Chinese IME/CJK acceptance against the pinned Codex TUI using the exact generated release-candidate artifact. + +## Milestone 3 — Additional DSH capability surfaces (presentation only) — partial + +Implemented where official seams are stable: + +- Skills discovery/UI feed. +- Subagent thread/status presentation and DSH-owned interruption authority. + +Future presentation-only candidates: + +- Jobs/workflows UI. +- Plugin diagnostics/configuration UI where DSH exposes a stable product-facing seam. +- Additional DSH-specific commands that do not disturb Codex-compatible commands. + +This milestone is not a blocker for the core 1.0 daily coding loop unless a listed capability is required by the final product acceptance matrix. + +## Milestone 4 — Production packaging and 1.0 gate — implementation complete, external validation pending + +Implemented: + +- `dshx` zero-argument startup with no manual bridge/profile/remote command; +- pinned Codex TUI included in platform release artifacts; +- packaged cross-platform `dshx-ipc-bridge` using Codex `codex_uds`; +- production Unix-socket transport with no TCP listener / bearer-token loopback dependency; +- real local-IPC data-plane self-check in `dshx doctor`; +- exact supported DSH release closure and compatibility gates; +- DSH-owned session durability/resume projection tests; +- approval/permission/subagent fail-closed tests; +- Linux x64, Windows x64, macOS arm64 and macOS x64 release jobs; +- clean-install artifact smoke and SHA-256 checksums; +- Linux real pinned-TUI PTY smoke in CI **and tagged release workflow**; +- Codex/DSH pin + patch-queue sync model; +- explicit RC promotion checklist in `RELEASE-READINESS.md`. + +1.0 may ship only when the exact candidate has green CI/release evidence and the manual Windows Terminal/CJK/IME UX acceptance is recorded as passing. diff --git a/docs/RUNTIME-INTEGRATION.md b/docs/RUNTIME-INTEGRATION.md new file mode 100644 index 00000000..f095a383 --- /dev/null +++ b/docs/RUNTIME-INTEGRATION.md @@ -0,0 +1,119 @@ +# Production runtime integration + +## Decision + +DSHX reuses the same architectural seam that official DeepSeek Harness uses for its headless product: a direct Cordis/core entry point over the official DSH composition, without requiring the WebUI/browser runtime for normal terminal use. + +The TUI remains project-owned. DeepSeek Harness remains upstream-owned. + +Supported DSH package line for the current release branch: `0.1.0-rc.8`. +Pinned DSH source reference: `141eb6fef83422698aef7a981029e843e8161534`. + +The official runtime owns Agent creation/resume, Session persistence, model selection/routing, tools, approvals, permission policy, skills, subagents and every other capability surface. DSHX generalizes only the **presentation lifetime** from a headless/one-shot consumer into an interactive terminal frontend. + +## Production topology + +```text +┌──────────────────────────────────────────┐ +│ dshx Node launcher │ +│ - boots official DSH composition │ +│ - owns child-process lifetime only │ +└────────────────────┬─────────────────────┘ + │ + │ DSH public APIs/events + ▼ +┌──────────────────────────────────────────┐ +│ DSHX presentation adapter │ +│ - Codex protocol projection only │ +└────────────────────┬─────────────────────┘ + │ JSONL / child stdio + ▼ +┌──────────────────────────────────────────┐ +│ dshx-ipc-bridge │ +│ - pinned Codex workspace binary │ +│ - no agent/session/runtime state │ +└────────────────────┬─────────────────────┘ + │ WebSocket framing over + │ private local UDS only + ▼ +┌──────────────────────────────────────────┐ +│ pinned Codex TUI thin fork │ +│ presentation/input only │ +└──────────────────────────────────────────┘ +``` + +The bridge exists because the TUI is Rust and the official DSH composition is Node/TypeScript. It reuses Codex's own cross-platform `codex_uds` implementation rather than introducing unsafe FFI or a second runtime. + +## Why WebSocket framing is still present + +The pinned Codex app-server client speaks WebSocket frames for its remote transport abstraction, including its Unix-socket endpoint. DSHX keeps that framing but changes the production carrier: + +- **forbidden in production:** `ws://127.0.0.1:*`, TCP listeners, bearer-token loopback transport, user-facing Codex remote mode; +- **production:** a private `unix://.../app.sock` endpoint implemented by `codex_uds`, with the bridge relaying JSON messages to Node over child stdio. + +Therefore production does not depend on Codex's experimental TCP WebSocket remote mode, while the TUI can continue using the upstream protocol machinery with a minimal thin-fork patch. + +## Launcher lifecycle + +`dshx` performs the following sequence: + +1. Resolve and validate the packaged `dshx-tui` and `dshx-ipc-bridge` binaries. +2. Create an isolated presentation-only `CODEX_HOME` under `~/.dshx/codex-tui` (or the development override). +3. Boot the supported official DSH composition in Node. +4. Create a private temporary socket directory. +5. Spawn `dshx-ipc-bridge`, wait for its readiness control message, and expose the resulting `unix://` endpoint to the TUI. +6. Spawn the pinned Codex TUI with `DSHX_APP_SERVER_ENDPOINT` set to that local endpoint. +7. Relay app-server JSON between the TUI and the DSH presentation adapter while DSH remains authoritative for all runtime state. +8. On TUI exit or process shutdown, close presentation state, dispose DSH, terminate the bridge and remove the temporary socket directory. + +Ctrl+C is intentionally not intercepted by the Node launcher because it is an in-TUI interaction used to interrupt/steer the active DSH turn. Process `SIGTERM` is propagated for lifecycle shutdown. + +## What the adapter may do + +- Create/resume Agents through official DSH services. +- Forward follow-up, steering and cancellation to official Agent/subagent control APIs. +- Subscribe to public Agent/Session events. +- Read official session persistence/list/inspection surfaces. +- Project DSH events into Codex Thread/Turn/Item UI events. +- Keep disposable UI correlation state such as `DSH turn number → current Codex turn id`. +- Translate a user decision from an upstream Codex picker into the corresponding official DSH approval/question API call without weakening it. + +## What the adapter must not do + +- Implement an agent loop. +- Call model providers directly instead of DSH. +- Execute tools or shell commands outside DSH. +- Store a second durable transcript or session database. +- Repair/replay sessions with DSHX-owned persistence logic. +- Decide sandbox/approval policy. +- Reimplement compaction, fork, skills, subagents, jobs or plugins. +- Persist an independent model choice when DSH already owns selection. + +The test suite contains ownership guards for these boundaries. Missing official services are compatibility failures, not invitations to add shadow implementations. + +## Development-only transport + +The historical deterministic protocol PoC and TCP/WebSocket stub remain useful test fixtures for protocol work, but they are not copied into the installable production package. The `ws` dependency is development-only. + +The real TUI smoke test uses `bin/dshx-stub-local.mjs`, which drives the deterministic fixture through the same packaged local-IPC bridge as production. + +## Diagnostics + +`dshx doctor` validates: + +- supported Node runtime; +- packaged pinned Codex TUI; +- packaged `dshx-ipc-bridge`; +- a real UDS + WebSocket `ping → pong` self-test using Codex's cross-platform UDS implementation; +- official DSH bundle composition; +- isolated DSHX presentation home. + +A successful bridge self-test proves the local transport data plane on the current OS; it is stronger than merely checking that the binary exists. + +## Compatibility and upstream sync + +DSH and Codex are both moving upstreams. The repository pins source/package versions and keeps the Codex divergence as an ordered patch queue. Compatibility is defined by public capabilities actually exposed by the supported DSH line. + +If a DSH update removes a required public seam, that version is unsupported until DSHX can adapt through another official seam or DSH restores one. DSHX must not preserve compatibility by copying DSH internals. + +If a Codex update breaks a patch or app-server shape, the pin is advanced only after the patch queue, protocol tests, PTY smoke, packaging checks and UX parity gate are revalidated. diff --git a/docs/TEAM.md b/docs/TEAM.md new file mode 100644 index 00000000..69515bb6 --- /dev/null +++ b/docs/TEAM.md @@ -0,0 +1,54 @@ +# DSHX delivery team + +This repository is organized around six engineering roles. These are workstreams, not ownership of DeepSeek Harness internals. + +## 1. Codex TUI upstream + +Owns the pinned Codex TUI source, upstream syncs, patch minimization, branding deltas, terminal rendering and keyboard behavior. + +**May change:** TUI source and narrow extension points. + +**Must not change:** DSH runtime behavior. + +## 2. DSH public-API integration + +Tracks the public DeepSeek Harness interfaces needed by the TUI and converts them into a stable adapter boundary. + +**May change:** adapter calls, compatibility detection and graceful unsupported states. + +**Must not implement:** agent loop, session engine, tools, sandbox, approval policy, skills, subagents, jobs or workflows. + +## 3. Protocol mapping + +Owns Codex Thread/Turn/Item ↔ DSH public event/API projections and compatibility tests. Unknown or unsafe permission semantics fail closed. + +## 4. Terminal and Windows UX + +Owns side-by-side Codex parity on Windows Terminal first, plus Linux/macOS, resize, scrolling, mouse, CJK/IME, Ctrl+C and shell/terminal edge cases. + +## 5. QA and security + +Owns protocol fixtures, regression tests, crash/reconnect tests, approval fail-closed checks and release blockers. Visual completeness alone can never pass a release gate. + +## 6. Release and upstream operations + +Owns `dshx` packaging, reproducible builds, checksums, `dshx doctor`, platform artifacts and upstream pin/sync policy. + +## Non-negotiable ownership rule + +`dsh-codex` maintains **TUI + launcher + the thinnest practical public-API adapter**. + +DeepSeek Harness remains an upstream dependency. If DSH does not expose a capability that Codex UI can display, DSHX must hide/disable/degrade that UI or wait for upstream. It must not recreate the missing capability locally. + +A PR is out of scope if its primary purpose is to reimplement a DeepSeek Harness capability rather than present or adapt one. + +## Product command contract + +Production 1.0 requires this to be the normal path: + +```bash +cd +dshx +``` + +No user-facing `codex --remote`, bridge process, manual profile command or package-manager orchestration is acceptable in the production launch path. diff --git a/docs/UX-PARITY.md b/docs/UX-PARITY.md index 3ac144e7..d8d39a00 100644 --- a/docs/UX-PARITY.md +++ b/docs/UX-PARITY.md @@ -1,49 +1,76 @@ -# Codex UX Parity Matrix +# Codex UX parity matrix -The target is behavioral parity with the pinned Codex TUI, not a screenshot-inspired imitation. +The target is **behavioral parity with the pinned Codex TUI**, not a screenshot-inspired imitation. DSHX therefore keeps upstream Codex widgets/input handling wherever possible and changes only backend ownership, product data/branding, and unsupported-capability visibility. + +Status below means implementation/evidence coverage, not that the final release candidate has passed manual acceptance. ## Acceptance matrix -| Surface | Target | -|---|---| -| Welcome screen | same structure, DSH branding/data only | -| Composer | same multiline/editing behavior | -| Slash completion | same interaction pattern | -| Help/shortcuts | same keyboard model where applicable | -| Model picker | same presentation; data from DSH | -| Permission picker | same presentation; policy from DSH | -| Tool cells | same renderer classes where mappings exist | -| Shell cells | same streaming/collapse behavior | -| File diffs | same review rendering | -| Approval | same interaction; decision returned to DSH | -| Ask user | same picker/freeform experience | -| Plan | same presentation mapped from DSH plan state | -| Reasoning | same cell behavior where DSH exposes it | -| Steering | same active-turn input semantics | -| Interrupt | same Ctrl+C expectation | -| Resume | same picker behavior; sessions from DSH | -| Status/footer | same structure; DSH metrics/data | -| Scroll/mouse | same behavior | -| Resize | no corruption or state loss | -| Windows Terminal | first-class release gate | -| CJK/IME | first-class release gate | +| Surface | Target | Current implementation evidence | +|---|---|---| +| Welcome screen | same structure, DSH branding/data only | Codex TUI thin fork + branding patch; PTY smoke asserts DSH surface | +| Composer | same multiline/editing behavior | inherited unchanged from pinned Codex TUI | +| Slash completion | same interaction pattern | inherited Codex UI; backend-incompatible commands hidden by narrow capability patch | +| Help/shortcuts | same keyboard model where applicable | inherited from pinned Codex TUI | +| Model picker | same presentation; data from DSH | official DSH model/provider directory + settings tests | +| Permission picker | same presentation; policy from DSH | DSH permission preset projection + backend-aware Codex picker patch | +| Tool cells | same renderer classes where mappings exist | DSH tool presentation specialization + tests | +| Shell cells | same streaming/collapse behavior | Codex shell renderer driven by DSH-owned tool execution/events | +| File diffs | same review rendering | Codex diff renderer driven by DSH tool metadata/workspace command path | +| Approval | same one-shot interaction where semantics match | DSH approval provider + fail-closed backend-aware overlay patch | +| Ask user | same picker/freeform experience where semantics match | `ctx.userQuestions` provider → Codex request-user-input overlay; multi-select remains explicit unsupported gap | +| Plan | same presentation mapped from DSH plan state | `todo/write` + DSH plan mode projection/tests | +| Reasoning | same cell behavior where DSH exposes it | DSH reasoning chunks/messages → Codex reasoning item/delta | +| Steering | same active-turn input semantics | Codex active composer → `Agent.steer(...)` through DSH controller | +| Interrupt | same Ctrl+C expectation | launcher preserves Ctrl+C for TUI; TUI interrupt delegates to DSH Agent/subagent authority | +| Resume | same picker behavior; sessions from DSH | Codex resume UI + DSH persistence list/inspect/resume/history hydration | +| Status/footer | same structure; DSH metrics/data | persisted/current model + token/context projections where exposed | +| Scroll/mouse | same behavior | inherited from pinned Codex TUI | +| Resize | no corruption or state loss | inherited from pinned Codex TUI; manual RC verification required | +| Windows Terminal | first-class release gate | native Windows build/package + real local-IPC doctor; visual/input acceptance still manual | +| CJK/IME | first-class release gate | Codex input code is inherited; exact RC artifact still requires manual Chinese IME acceptance | + +## Backend command visibility + +A Codex slash command is visible only when either: + +1. it is a presentation/local-TUI operation, or +2. DSHX has a faithful mapping to an official DSH public capability. + +Codex runtime-owned surfaces such as unsupported app/plugin/hook/memory/personality/review modes are hidden rather than falling through to Codex core. `/diff` remains visible because its execution path is DSH-owned. Skills remain visible because `skills/list` is backed by the official DSH registry surface. ## Allowed deliberate differences - Product branding. -- Model/provider names. +- Model/provider names and DSH-owned configuration vocabulary. - Backend-specific capabilities that have no Codex equivalent. -- DSH-specific commands placed under an isolated namespace. - Explicit unsupported messages when semantic translation would be unsafe or misleading. +- Hidden Codex runtime commands when the supported DSH line has no faithful public equivalent. +- DSH multi-select user questions until the pinned Codex UI can represent them without semantic loss. ## Not acceptable - Reimplementing a Codex widget merely because it looks simple. -- Maintaining a separate session history for UI convenience. -- Downgrading an approval requirement during protocol translation. +- Maintaining a separate durable session history for UI convenience. +- Downgrading an approval or subagent authority requirement during protocol translation. - Silent model changes on resume. -- Calling a generic readline prompt a TUI. +- Routing a hidden/unsupported command back into Codex's agent runtime. +- Replacing the inherited Codex composer with a generic readline prompt. +- Calling automated transport tests proof of Windows Terminal/CJK/IME parity. + +## Automated parity protection + +The repository protects the parts that can be automated: + +- pinned Codex patch application/build; +- source invariants keeping DSHX on the external backend path; +- DSH adapter protocol/unit tests; +- ownership/fail-closed tests; +- real pinned-TUI PTY lifecycle smoke over production local IPC; +- native platform package + `dshx doctor`, including a real local-IPC data-plane self-test. + +## Manual release comparison -## Release comparison +For every 1.0 release candidate, install the **generated release artifact** and compare it side by side with the pinned upstream Codex version using the same Windows Terminal dimensions/theme. At minimum verify composer editing, Chinese IME composition/commit/cancel, wide-glyph alignment, slash completion, pickers, overlays, tool/shell/diff cells, reasoning/plan, steering/Ctrl+C, resume, scroll/mouse, resize and footer rendering. -Every release candidate should be tested side by side against the pinned upstream Codex version on the same terminal dimensions and theme. Differences are either fixed, documented as deliberate, or block release. +Differences are either fixed, documented above as deliberate, or block release. The promotion procedure lives in `RELEASE-READINESS.md`. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..003dc24c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7796 @@ +{ + "name": "dsh-codex", + "version": "0.0.0-m1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dsh-codex", + "version": "0.0.0-m1", + "dependencies": { + "@deepseek-ai/dsh": "0.1.0-rc.8", + "@deepseek-ai/dsh-agent": "0.1.0-rc.8", + "@deepseek-ai/dsh-app-boot": "0.1.0-rc.8", + "@deepseek-ai/dsh-host-apiproxy": "0.1.0-rc.8", + "@deepseek-ai/dsh-launch-environment": "0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "0.1.0-rc.8", + "@deepseek-ai/dsh-session": "0.1.0-rc.8" + }, + "bin": { + "dshx": "bin/dshx.mjs" + }, + "devDependencies": { + "ws": "^8.18.0" + }, + "engines": { + "node": "^22.19.0 || >=24.0.0" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmmirror.com/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1048.0", + "resolved": "https://registry.npmmirror.com/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", + "@aws-sdk/eventstream-handler-node": "^3.972.16", + "@aws-sdk/middleware-eventstream": "^3.972.12", + "@aws-sdk/middleware-websocket": "^3.972.19", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.977.8", + "resolved": "https://registry.npmmirror.com/@aws-sdk/core/-/core-3.977.8.tgz", + "integrity": "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.4", + "@aws-sdk/xml-builder": "^3.972.39", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.31.1", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.69", + "resolved": "https://registry.npmmirror.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.69.tgz", + "integrity": "sha512-AreCFzcB4kH2HF9031Ot0jSJr3KXvRg6e8uDeub20JEVdZU3Bv0sTq1plc7VsT3KiqutlzH7l0j50UcCWHUioA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.71", + "resolved": "https://registry.npmmirror.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.71.tgz", + "integrity": "sha512-A8ObcqVmDMnk4F9NozZ7JwmUu9Q4xyBJkmyq1C5U+wNM9ht9J7+EuuyabsLWXZnOoTqFaJuYBYTKf5CTipkEjA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-http-handler": { + "version": "4.11.2", + "resolved": "https://registry.npmmirror.com/@smithy/node-http-handler/-/node-http-handler-4.11.2.tgz", + "integrity": "sha512-avwAh9HM3h2lcfjvP3zYIZGf+XVgLQ91wOJ2qoFbNpW1UZeZb33aGlhTZvtkANHfcGhJroRY64525OjfgOg30g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.14", + "resolved": "https://registry.npmmirror.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.14.tgz", + "integrity": "sha512-7c+Wti2LsERNWMfm7ySz3/6RPopFW3Nmn7s63Xpcq6R/tRuY5hpvkHA2xVgi5ukJbvok9l0IDtVEvqTtg+X7dw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/credential-provider-env": "^3.972.69", + "@aws-sdk/credential-provider-http": "^3.972.71", + "@aws-sdk/credential-provider-login": "^3.972.76", + "@aws-sdk/credential-provider-process": "^3.972.69", + "@aws-sdk/credential-provider-sso": "^3.973.13", + "@aws-sdk/credential-provider-web-identity": "^3.972.75", + "@aws-sdk/nested-clients": "^3.997.43", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.76", + "resolved": "https://registry.npmmirror.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.76.tgz", + "integrity": "sha512-LVixwOnEJfrrfKHeZjBA8pIMTZjNDq8ak8VpcoWUuCJDrSnBNU8POJksULMgvN089P0MXtQYH2Zs627/MK1K0g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/nested-clients": "^3.997.43", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.80", + "resolved": "https://registry.npmmirror.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.80.tgz", + "integrity": "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.69", + "@aws-sdk/credential-provider-http": "^3.972.71", + "@aws-sdk/credential-provider-ini": "^3.973.14", + "@aws-sdk/credential-provider-process": "^3.972.69", + "@aws-sdk/credential-provider-sso": "^3.973.13", + "@aws-sdk/credential-provider-web-identity": "^3.972.75", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.69", + "resolved": "https://registry.npmmirror.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.69.tgz", + "integrity": "sha512-9kpTNdZTrcqXTfhxM7fgl9Z68ek3Fu5oe3Yf+A/pJGibEqpgZxz2tSY7SinmyCIU2PJ+ygY4FPoBBnLpocMtrQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.13", + "resolved": "https://registry.npmmirror.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.13.tgz", + "integrity": "sha512-Oc81qauMPzUoTnAS2YKpNwY6sY/LUyQTEeaf6yP197WMxkEBQfcKLR1MFpD7+pNTubXnfkH6gwpji+Gc7iyD2Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/nested-clients": "^3.997.43", + "@aws-sdk/token-providers": "3.1111.0", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1111.0", + "resolved": "https://registry.npmmirror.com/@aws-sdk/token-providers/-/token-providers-3.1111.0.tgz", + "integrity": "sha512-JfljgoVtl+s3Qy21n9a7Z48uCQaOXcN74KJ3TEQfPoB293GrXFSt6HSQJF1sTZ8c/5QedEvd3NjJQMO4u9qa5A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/nested-clients": "^3.997.43", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.75", + "resolved": "https://registry.npmmirror.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.75.tgz", + "integrity": "sha512-YPN6uoGDgjjjeVFZrcOeCJqmB6zpXoeeNgIjqe+DexJaWqdjVfCCe+VAZwli9Z2h8KhFW8oxkO39emQ1tyz/Mw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/nested-clients": "^3.997.43", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.33", + "resolved": "https://registry.npmmirror.com/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.33.tgz", + "integrity": "sha512-1Dd5WyEE2Kb3HvY44u7Ob16ST2W6iutOqsQ8Y2hUmsL2mAH/STlGS1dS9h3IOE6L7Ld3AR2HzKJ6XeCMOw8Peg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.28", + "resolved": "https://registry.npmmirror.com/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.28.tgz", + "integrity": "sha512-Z1EDXnS01P7H5jVrUx+/dBqV0m7dta7bSxLclkOuDuS93pNNQm0IcT4YLUbuvWKPYNxbI8aTG0p5Br30GSKDgA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.51", + "resolved": "https://registry.npmmirror.com/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.51.tgz", + "integrity": "sha512-jdgP3jR5Q96j1jjZ98GGwpGg1CBNFIO2YE+vXg8cg8PvNY4NvgQNYJsqDaRX2PYv5gSUX/+C0D58Fhspj9ELMQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.43", + "resolved": "https://registry.npmmirror.com/@aws-sdk/nested-clients/-/nested-clients-3.997.43.tgz", + "integrity": "sha512-bit+VpqWNyi3wHxFoTsTliNXimCSL2r2OeDTm7ZrG+YsTZ2D7ofDJ6r/t9PVBn80i6/v0X2h9Tgw6QP2MAKfPw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/signature-v4-multi-region": "^3.996.45", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler": { + "version": "4.11.2", + "resolved": "https://registry.npmmirror.com/@smithy/node-http-handler/-/node-http-handler-4.11.2.tgz", + "integrity": "sha512-avwAh9HM3h2lcfjvP3zYIZGf+XVgLQ91wOJ2qoFbNpW1UZeZb33aGlhTZvtkANHfcGhJroRY64525OjfgOg30g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.45", + "resolved": "https://registry.npmmirror.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.45.tgz", + "integrity": "sha512-bBuyztukzXq6plzFGHAWiQt0QXo+HL8b8lX5cFTzkez/74PtS1c0qPFCIVuHkyoT+miH2qOjAcm1/yoro2ESPA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.4", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmmirror.com/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.974.4", + "resolved": "https://registry.npmmirror.com/@aws-sdk/types/-/types-3.974.4.tgz", + "integrity": "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.10", + "resolved": "https://registry.npmmirror.com/@aws-sdk/util-locate-window/-/util-locate-window-3.965.10.tgz", + "integrity": "sha512-ycwH6Zd2GhuSqdXX9ihbCjeGTB6xOJs+O3+Jb8/zDG9978XU80qs75dfkPJRMNKe5MvBZPuNeFpd4JZKPoUF4g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.39", + "resolved": "https://registry.npmmirror.com/@aws-sdk/xml-builder/-/xml-builder-3.972.39.tgz", + "integrity": "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@deepseek-ai/cordis": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/cordis/-/cordis-4.0.1.tgz", + "integrity": "sha512-YBdskTU2Po1kru3GgcUWUbkTsPMA9LkSQDAY8rBkFJeajdgcQad3QPJZE26JyK99Xb6HaASvoXg2DSUTeN/0Nw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/cosmokit": "^1.8.2", + "@standard-schema/spec": "^1.1.0" + }, + "bin": { + "cordis": "bin.js" + }, + "peerDependencies": { + "@deepseek-ai/cordis-plugin-include": "^1.0.6", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2" + }, + "peerDependenciesMeta": { + "@deepseek-ai/cordis-plugin-include": { + "optional": true + }, + "@deepseek-ai/cordis-plugin-loader": { + "optional": true + } + } + }, + "node_modules/@deepseek-ai/cordis-plugin-group": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/cordis-plugin-group/-/cordis-plugin-group-1.0.1.tgz", + "integrity": "sha512-E1NThkFB3jn3TCqa6Oc++1zQHqLejF3W2wDwv2BlL3UEmgtBXL1K1j1koc+j676RuiOXtJkUJlPcOntRGKLWBQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2" + } + }, + "node_modules/@deepseek-ai/cordis-plugin-hmr": { + "version": "1.0.16", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/cordis-plugin-hmr/-/cordis-plugin-hmr-1.0.16.tgz", + "integrity": "sha512-S7VHfylDg1+Y5O6fdYYZu0KFRAj/snHywcFPnX3KmQYa/qv2Q8IXi4zAt9ABq0BJYqhNoqRlbpGMfIjabgXjKA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@deepseek-ai/cosmokit": "^1.8.2", + "@deepseek-ai/schemastery": "^3.18.1", + "chokidar": "^4.0.3", + "picomatch": "^4.0.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/cordis-plugin-timer": "^1.1.3" + } + }, + "node_modules/@deepseek-ai/cordis-plugin-include": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/cordis-plugin-include/-/cordis-plugin-include-1.0.6.tgz", + "integrity": "sha512-i1VXrZCbv6tk/iUgedCNjrxxArbWT3IvRZGB5sdqJ3ectnihivXXQbRZ8JJ73DSmAPvlMGmrbtjFAfm10yvXRg==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/cosmokit": "^1.8.2", + "js-yaml": "^4.1.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2" + } + }, + "node_modules/@deepseek-ai/cordis-plugin-loader": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/cordis-plugin-loader/-/cordis-plugin-loader-1.0.2.tgz", + "integrity": "sha512-RIW9hoVyhYDWdCI9BsvtZccPde1ECLC4OAxupwowGTak78vwVTVdb3HezTSOK1Y1/Ax3Ru0LA1pYOB04CnTxIQ==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/cosmokit": "^1.8.2" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "node-addon-require-builtin": "^0.1.4" + }, + "peerDependenciesMeta": { + "node-addon-require-builtin": { + "optional": true + } + } + }, + "node_modules/@deepseek-ai/cordis-plugin-timer": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/cordis-plugin-timer/-/cordis-plugin-timer-1.1.3.tgz", + "integrity": "sha512-IOkey1VNmJwYa5U8bksyMOnM7mtirqjjbWLr3xA8QybLMK0sMooG3a6iJwtvd8P3MFwo3FQibEg+JPixp37MEw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/cosmokit": "^1.8.2" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1" + } + }, + "node_modules/@deepseek-ai/cosmokit": { + "version": "1.8.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/cosmokit/-/cosmokit-1.8.2.tgz", + "integrity": "sha512-muBOKtSrUKU5m/xpq8ZXWL6hQ/jgd4PhU2PqH97bcxIiLEJfNwZOGQEx4t/aS/GgxRAR+ra9pMHPMtTHU4sqqA==", + "license": "MIT" + }, + "node_modules/@deepseek-ai/dsh": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh/-/dsh-0.1.0-rc.8.tgz", + "integrity": "sha512-VQU5NlomrKLRgcXuOf+sxWFvqxPA8q9vMhrKPlPPXiOJEhGlGlAdiyxZvZxkCVI+v0zbhe21cY3/luLyxpSzzA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/cordis-plugin-hmr": "^1.0.16", + "@deepseek-ai/cordis-plugin-include": "^1.0.6", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2", + "@deepseek-ai/cordis-plugin-timer": "^1.1.3", + "@deepseek-ai/dsh-agent-instructions": "^0.1.0-rc.8", + "@deepseek-ai/dsh-agent-tool-presentation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-app-boot": "^0.1.0-rc.8", + "@deepseek-ai/dsh-base": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-agent-preset": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-cordis": "^0.1.0-rc.8", + "@deepseek-ai/dsh-cmdline": "^0.1.0-rc.8", + "@deepseek-ai/dsh-command-compact": "^0.1.0-rc.8", + "@deepseek-ai/dsh-command-goal": "^0.1.0-rc.8", + "@deepseek-ai/dsh-compaction-basic": "^0.1.0-rc.8", + "@deepseek-ai/dsh-compaction-tool-result-pruner": "^0.1.0-rc.8", + "@deepseek-ai/dsh-cordis-client-runner": "^0.1.0-rc.8", + "@deepseek-ai/dsh-fs-local": "^0.1.0-rc.8", + "@deepseek-ai/dsh-goal": "^0.1.0-rc.8", + "@deepseek-ai/dsh-goal-round-driver": "^0.1.0-rc.8", + "@deepseek-ai/dsh-headless": "^0.1.0-rc.8", + "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.8", + "@deepseek-ai/dsh-jobs-local": "^0.1.0-rc.8", + "@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-mcp-client": "^0.1.0-rc.8", + "@deepseek-ai/dsh-persona": "^0.1.0-rc.8", + "@deepseek-ai/dsh-plan-mode": "^0.1.0-rc.8", + "@deepseek-ai/dsh-pwsh-local": "^0.1.0-rc.8", + "@deepseek-ai/dsh-pwsh-sandbox": "^0.1.0-rc.8", + "@deepseek-ai/dsh-schedule": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-reference": "^0.1.0-rc.8", + "@deepseek-ai/dsh-skill": "^0.1.0-rc.8", + "@deepseek-ai/dsh-skill-filesystem": "^0.1.0-rc.8", + "@deepseek-ai/dsh-terminal": "^0.1.0-rc.8", + "@deepseek-ai/dsh-terminal-bash": "^0.1.0-rc.8", + "@deepseek-ai/dsh-time-context": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tmux-context": "^0.1.0-rc.8", + "@deepseek-ai/dsh-token-meter": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-ask-user": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-bash": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-bash-persistent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-cordis": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-fs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-fs-search": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-goal": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-jobs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-pwsh": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-pwsh-persistent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-ralph": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-skill": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-str-replace-editor": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-subagent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-subagent-control": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-todo": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-web": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-workflow": "^0.1.0-rc.8", + "@deepseek-ai/dsh-web-app": "^0.1.0-rc.8", + "@deepseek-ai/dsh-workflow-worker-thread": "^0.1.0-rc.8", + "commander": "^15.0.0", + "js-yaml": "^4.2.0", + "node-addon-require-builtin": "^0.1.4" + }, + "bin": { + "dsh": "lib/bin.js" + } + }, + "node_modules/@deepseek-ai/dsh-agent": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-agent/-/dsh-agent-0.1.0-rc.8.tgz", + "integrity": "sha512-fOl46ZvzoYHalrlyfOJAHNhzyKeVDTUnZZKNSJEr9GY/98WJTjcV21agVEofTXldkJ7a/ws2HpJkVNJVFMtslg==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-scope": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-agent-default-model": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-agent-default-model/-/dsh-agent-default-model-0.1.0-rc.8.tgz", + "integrity": "sha512-FSmgVy8yeVYx3krUwfWZR3JokLD202FSuEd/ZfGdxA+BhOKAOfhNv539kujatVqXyzCG3XiRNda7zWxCh2h7/A==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-agent-instructions": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-agent-instructions/-/dsh-agent-instructions-0.1.0-rc.8.tgz", + "integrity": "sha512-T+JER2gUZbopOxNeHKUE7EfNof30yihsi7GXKB/lOLoy1XMvd5mlzjSHA/OzqjLxiZyyN4V7XAGURApVD3Z7sw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-fs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-agent-loop": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-agent-loop/-/dsh-agent-loop-0.1.0-rc.8.tgz", + "integrity": "sha512-6Vg5oLy3UZ6lJKMPxldqqBylKSMOpbV/FRrqzDqn3hGdksYBzt9K2hUJm/rE2RyguYKUKFPm2hExs0nn4HKmWA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-scope": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-agent-presets": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-agent-presets/-/dsh-agent-presets-0.1.0-rc.8.tgz", + "integrity": "sha512-7h+kE0KDrJCk2Y8GAqF4fu4YqZmFi5U8/AarFkGUqRI3HDugD6oYf05c7ZWNF7lQXh1OmwmrSbuPhT/YXJapOg==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "js-yaml": "^4.1.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/cordis-plugin-include": "^1.0.6", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-atomic-write": "^0.1.0-rc.8", + "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-scope": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-agent-tool-presentation": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-agent-tool-presentation/-/dsh-agent-tool-presentation-0.1.0-rc.8.tgz", + "integrity": "sha512-DQouZBh0yjKy8bi0EHQZrZ1B0E+Sc321o5oIyrrmL3hh1sbBsWVrnTvwKPP+SU7J6gnIzyITJ7qEV7pcCGOB2w==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-anonymous-user-id": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-anonymous-user-id/-/dsh-anonymous-user-id-0.1.0-rc.8.tgz", + "integrity": "sha512-vUESbXtDdqIyelCNqX3ijOfqNfw65iSaJaqzfiqQhmSd9ZrNJ1fPUHl8FoW1BdfK8FjrDyh32R6BtuVpI7Zdhw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-api-gateway": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-api-gateway/-/dsh-api-gateway-0.1.0-rc.8.tgz", + "integrity": "sha512-rNA9+Sq3lXFDmyoFLpXNbcuWg7iFeepCwDi+jt1oGjwZTdZJ2nEWpU6GKlOiqadQjHYzfwEbCqmcuJy32qke/A==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-registry": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-api-remotes": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-api-remotes/-/dsh-api-remotes-0.1.0-rc.8.tgz", + "integrity": "sha512-s/zmFmU6R+dDxUWbHucIyfVgJG4v8kXa3YRjwjMO+Bu871ZG5Zsht6cvRqeSyu0A/Gs06ZfKYeKE0sdbCZ1NiA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.8", + "@deepseek-ai/dsh-api-gateway": "^0.1.0-rc.8", + "@deepseek-ai/dsh-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-cordis-host-runner": "^0.1.0-rc.8", + "@deepseek-ai/dsh-credentials": "^0.1.0-rc.8", + "@deepseek-ai/dsh-file-reference": "^0.1.0-rc.8", + "@deepseek-ai/dsh-goal": "^0.1.0-rc.8", + "@deepseek-ai/dsh-host-plugin-inventory": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-message-feedback": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-reference": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-registry": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-app-boot": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-app-boot/-/dsh-app-boot-0.1.0-rc.8.tgz", + "integrity": "sha512-DS63o1PhX2Gvqdgl6Wd8vZnLl0qo8gJ9LpSHXeJBJL9HITpLPhdVYE3X6S0icIYdbGO2plNyhxVJOSN6ibO7VA==", + "license": "MIT", + "dependencies": { + "js-yaml": "^4.2.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/cordis-plugin-group": "^1.0.1", + "@deepseek-ai/cordis-plugin-hmr": "^1.0.16", + "@deepseek-ai/cordis-plugin-include": "^1.0.6", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2", + "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8" + }, + "peerDependenciesMeta": { + "@deepseek-ai/cordis-plugin-hmr": { + "optional": true + } + } + }, + "node_modules/@deepseek-ai/dsh-atomic-write": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-atomic-write/-/dsh-atomic-write-0.1.0-rc.8.tgz", + "integrity": "sha512-SWP+NAat3oteaPi47zLPz4vxeYCHNVmFpN1aeKDuPHpe/ouNTl+WcyIONAJ+1MbZ+8APWhSaJIVu6ORJEUhVGw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-attachment": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-attachment/-/dsh-attachment-0.1.0-rc.8.tgz", + "integrity": "sha512-cCrg4WWiav7pGtbdU8dJTpAG28cPnkWVB2YPOrlBCgBbhQziGcG9Dv67Ti6L3PI4OQKC55gYBFinQghFO/7FGg==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-attachment-local": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-attachment-local/-/dsh-attachment-local-0.1.0-rc.8.tgz", + "integrity": "sha512-TPsDbDPA8yoE3hw1MtrW61AMh72oMiCY+rkO3bDnvc4MtGwKvXcHWtbNPeVpLgMeznaXsnTU3GUp9Fa6PXdp8A==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "sharp": "^0.35.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-attachment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-base": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-base/-/dsh-base-0.1.0-rc.8.tgz", + "integrity": "sha512-aMjXT6d5t8SGQg24geOSX6O0ky+hJLijhzZklDcICrBvGSKXvuS8jxLOcL5u4s4pFCODXhuOpvTfFuUhN2RK3Q==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/cordis-plugin-hmr": "^1.0.16", + "@deepseek-ai/cordis-plugin-timer": "^1.1.3", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-agent-default-model": "^0.1.0-rc.8", + "@deepseek-ai/dsh-agent-instructions": "^0.1.0-rc.8", + "@deepseek-ai/dsh-agent-loop": "^0.1.0-rc.8", + "@deepseek-ai/dsh-api-gateway": "^0.1.0-rc.8", + "@deepseek-ai/dsh-attachment-local": "^0.1.0-rc.8", + "@deepseek-ai/dsh-bash-sandbox": "^0.1.0-rc.8", + "@deepseek-ai/dsh-command-compact": "^0.1.0-rc.8", + "@deepseek-ai/dsh-command-feedback": "^0.1.0-rc.8", + "@deepseek-ai/dsh-command-goal": "^0.1.0-rc.8", + "@deepseek-ai/dsh-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-compaction-basic": "^0.1.0-rc.8", + "@deepseek-ai/dsh-compaction-tool-result-pruner": "^0.1.0-rc.8", + "@deepseek-ai/dsh-credentials-local": "^0.1.0-rc.8", + "@deepseek-ai/dsh-fs-local": "^0.1.0-rc.8", + "@deepseek-ai/dsh-fs-observation-policy": "^0.1.0-rc.8", + "@deepseek-ai/dsh-fs-sandbox": "^0.1.0-rc.8", + "@deepseek-ai/dsh-goal": "^0.1.0-rc.8", + "@deepseek-ai/dsh-goal-round-driver": "^0.1.0-rc.8", + "@deepseek-ai/dsh-jobs-local": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm-deepseek": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm-pi-ai": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm-retry": "^0.1.0-rc.8", + "@deepseek-ai/dsh-permission-presets": "^0.1.0-rc.8", + "@deepseek-ai/dsh-plan-mode": "^0.1.0-rc.8", + "@deepseek-ai/dsh-pwsh-sandbox": "^0.1.0-rc.8", + "@deepseek-ai/dsh-repeat-tool-reminder": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox-local": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-checkpoint-policy": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-query-sqlite": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-telemetry-otel": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-title": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-title-first-prompt-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings-file": "^0.1.0-rc.8", + "@deepseek-ai/dsh-shell-env": "^0.1.0-rc.8", + "@deepseek-ai/dsh-skill": "^0.1.0-rc.8", + "@deepseek-ai/dsh-skill-badge": "^0.1.0-rc.8", + "@deepseek-ai/dsh-skill-filesystem": "^0.1.0-rc.8", + "@deepseek-ai/dsh-spill-local": "^0.1.0-rc.8", + "@deepseek-ai/dsh-spill-policy": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subagent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subagent-fork-in-process": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subagent-spawn-in-process": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subprocess-local": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-token-meter": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-bash": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-call-timeout-policy": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-fs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-fs-search": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-goal": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-jobs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-pwsh": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-ralph": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-skill": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-str-replace-editor": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-subagent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-subagent-control": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-subagent-report": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-todo": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-web": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-workflow": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-loader": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-registry": "^0.1.0-rc.8", + "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.8", + "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.8", + "@deepseek-ai/dsh-web": "^0.1.0-rc.8", + "@deepseek-ai/dsh-web-search-deepseek": "^0.1.0-rc.8", + "@deepseek-ai/dsh-workflow-worker-thread": "^0.1.0-rc.8" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-bash-local": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-bash-local/-/dsh-bash-local-0.1.0-rc.8.tgz", + "integrity": "sha512-vMf3dMRrvVvwWCLJxobYONSebPtWqy2ZZKpRZ6o5fBrZbM88irbvL6F/6r/mu4ADTIDigUtpoxlRpOEQKbSuTg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-shell": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subprocess": "^0.1.0-rc.8", + "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-bash-sandbox": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-bash-sandbox/-/dsh-bash-sandbox-0.1.0-rc.8.tgz", + "integrity": "sha512-gCFrmFwYyxjXJsSRrzs5RbvCli0I0Tn9JAJEHCWSskE8x9iCVU7ljtmrKvgRYr0J4YeXwzteNSfNhlJqasu/QQ==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-bash-local": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.8", + "@deepseek-ai/dsh-shell": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-brand": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-brand/-/dsh-brand-0.1.0-rc.8.tgz", + "integrity": "sha512-402aUAfHxIZJrArBV4gDf0I/A/69A/By26FX6HTIOyFmCnB4wUBKh8jnglz1CorKWdZy8AKldAhh8HQhQEbGOQ==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-connection": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-connection/-/dsh-client-connection-0.1.0-rc.8.tgz", + "integrity": "sha512-O1Y1NmkvJjpR/rCYkzulRrKbFwUbpMlINR4G6fbpGXaLy0VzTqN+ptk0b3wJ4RrnKPi4WWLKNNdV647Fg/1sCw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "ws": "^8.21.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-attachment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-host-apiproxy": "^0.1.0-rc.8", + "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-hmr": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-hmr/-/dsh-client-hmr-0.1.0-rc.8.tgz", + "integrity": "sha512-Olzlpiw+2VIhNOZVEgi22b2U0jSv6HciAQpKj41TggI7oV+jhRDC2GuYFJoRQ8XBs3mcJCLG0DZtAcGrRP8n9Q==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2", + "@deepseek-ai/dsh-client-modules": "^0.1.0-rc.8", + "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-locale": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-locale/-/dsh-client-locale-0.1.0-rc.8.tgz", + "integrity": "sha512-S8ztym/oFU1aWPDtSZLTGWfprUBIuhfOrRNlw/qMAwhavJiQU/Ag0plg19JffGiOMAQuz2BU2n2mW/xhbpA5Vw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-modules": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-modules/-/dsh-client-modules-0.1.0-rc.8.tgz", + "integrity": "sha512-K+uDZSmSbpmHFpvrKkdctHHDIjimv1NWDZoc7CYy7QIk0Vuis4jShDau6H5L0B9TScWBDyXAz/R7rXttFlwseA==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2", + "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-runtime": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-runtime/-/dsh-client-runtime-0.1.0-rc.8.tgz", + "integrity": "sha512-wW/nktO/GVbGO/k163NlyJab/rK4D6xS5EL0bhYw4ojF1mxmWfrb3RUEYR5LcEUpFx/yXUHsUSgeO1ezmgIrvw==", + "license": "MIT", + "dependencies": { + "immer": "^10.1.1", + "zustand": "~4.4.7" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-attachment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-host-apiproxy": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm-retry": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-title": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-registry": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-agent-preset": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-agent-preset/-/dsh-client-ui-agent-preset-0.1.0-rc.8.tgz", + "integrity": "sha512-z+okJSOT2/uAA4oomx1WNNl/2k9HsoNDmtewOeQVfSy9C8Ey9YBoVoiSQDsYyDHhesxxlBI8RMLU44Pj/w1uug==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-attachment": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-attachment/-/dsh-client-ui-attachment-0.1.0-rc.8.tgz", + "integrity": "sha512-TiKZEUGd22AFxkWpgQCLW9VmuSgWQ11RlIQd0941wPAc4RDZxtkjQWDMOlvRS/m8Oqod/5oKr3U6WRCZKS81vA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-attachment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-brand-official": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-brand-official/-/dsh-client-ui-brand-official-0.1.0-rc.8.tgz", + "integrity": "sha512-eHzJkJGOKlQpap6uYaAZSh62MyJFEoPX8EvIp/kV3YHFDr4bZNc7Yg+ZLMaEXfIjPXKp26FhPiCIia9SP56n8A==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-commands": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-commands/-/dsh-client-ui-commands-0.1.0-rc.8.tgz", + "integrity": "sha512-nMDF0MbRM03AEmQJzXCbXQWZxSfVPHBO0z+9DqR6LgGXshYLkzVMp1zkfvqLs5QwFAwDSci4vi/GONJOBSL5WA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.0-rc.8", + "@deepseek-ai/dsh-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-conversation": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-conversation/-/dsh-client-ui-conversation-0.1.0-rc.8.tgz", + "integrity": "sha512-GnQSE47/goSm1UzxQpYQrQnMJLyTmYFH6NENdrOxrVJxQnB55ruactgM1kVwfFnc/WUQVYy0599AK7pxjbxjMg==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-attachment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-layout": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-compaction": "^0.1.0-rc.8", + "@deepseek-ai/dsh-goal": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm-retry": "^0.1.0-rc.8", + "@deepseek-ai/dsh-permission-presets": "^0.1.0-rc.8", + "@deepseek-ai/dsh-plan-mode": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-stats": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-token-meter": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-todo": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-cordis": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-cordis/-/dsh-client-ui-cordis-0.1.0-rc.8.tgz", + "integrity": "sha512-NvaSJxqC+HNPLPYPINHzJGRHI427mHSB2aaTGoyc9EFrYVJjvKpNu1QusWwObae5Y/Tx43I2NpFKTAcNVPXkOw==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-tool": "^0.1.0-rc.8", + "@deepseek-ai/dsh-cordis-client-runner": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-deliverables": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-deliverables/-/dsh-client-ui-deliverables-0.1.0-rc.8.tgz", + "integrity": "sha512-b0XZ/V7FPpTTtmHoKNEVOgl9bJGXGqktzMNNJgifHGl/Us1nNnQVDhM9XDbuUV5iXFPnRrKsR29OTnuTB+txZA==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-directory-picker-browse": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-directory-picker-browse/-/dsh-client-ui-directory-picker-browse-0.1.0-rc.8.tgz", + "integrity": "sha512-sjQcMuut8CRjqnFZHzX1Zzs1zISDlp/gYQVM9Wkyt9KF+XJfV5pHP0KTHZHwOslAqt2OJMnE38uW2yVncLg3sA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-workspace": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-directory-picker-native": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-directory-picker-native/-/dsh-client-ui-directory-picker-native-0.1.0-rc.8.tgz", + "integrity": "sha512-UPR5kSjfMGjLpO41fXXVxcen9Bq9eCzonStnhqMMk/6zC2EQUDkRs+KhYmaInkHI/CMjdTBBqBPRwjFG2iEsYQ==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-workspace": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-goal": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-goal/-/dsh-client-ui-goal-0.1.0-rc.8.tgz", + "integrity": "sha512-kAs6DktlELshs27S3nnjzciK9bGhL2b6RXmZ/mUNYwgiYxeYY4/QYynSNbD0ZQINe0VeGy6yQTv9dbqDBArndA==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-goal": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-input-trigger": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-input-trigger/-/dsh-client-ui-input-trigger-0.1.0-rc.8.tgz", + "integrity": "sha512-uVJEZ1ftUdMemLvNf17gzB6ThYw0DL4k+0kcQrUPc6LHNQ2ZeuuvOz/931OlqzzUKMtlAGZgy0LvWyJ62LlSog==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-file-reference": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-jobs": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-jobs/-/dsh-client-ui-jobs-0.1.0-rc.8.tgz", + "integrity": "sha512-gCJR9ymswAZa8OiyZQUKa9GpxRT2sP76tfqBGcF5pRN0xOKudlREM6xD3as2knY5EZELW5qQMVKlVVJZ2Uu7fA==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-layout": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-layout/-/dsh-client-ui-layout-0.1.0-rc.8.tgz", + "integrity": "sha512-tbHawbf8FsJqdwnXrU46V8u13bfAFVQQf0hA0OXH4k0PiLiThdZgQFn/T94+56JMUzD1QLqe7DLmNz29cwmjvQ==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-theme": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-message-feedback": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-message-feedback/-/dsh-client-ui-message-feedback-0.1.0-rc.8.tgz", + "integrity": "sha512-tzm/vvhysNa3+1BlTnuj3WJit5+u8U1NDecLqMPTm6U1P2s2bjKtvUpmMNqk+AdeYGYcJjXPadLEuWSNsIWFlQ==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-message-feedback": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-model-selection": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-model-selection/-/dsh-client-ui-model-selection-0.1.0-rc.8.tgz", + "integrity": "sha512-ODRo7IX4VKkgy6C0ibElP5p1Ek6NBm6Qz6S95cZQH2J+PXWTb57/6UvNfxqyst8nFh1wll4oRb1m4fm6MU/fqA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-permission-presets": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-permission-presets/-/dsh-client-ui-permission-presets-0.1.0-rc.8.tgz", + "integrity": "sha512-mYF/TX4GVJgRxDYPIk/iQKkGXz8qHkfFJE3GqIw5E27g+Msfg8AEGqfWEPp4CZeIK/3zcgpMLzgjMLOliWZ2dA==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-permission-presets": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-plan": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-plan/-/dsh-client-ui-plan-0.1.0-rc.8.tgz", + "integrity": "sha512-OmCuuYCqSlbYqd4s781WipQNKWdHGUn8TPIxrKrcbe94vJacuBtIUIJkDi48dy0IimI+ln/eHxD9WC/9+bJEmA==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-plan-mode": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-reference": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-reference/-/dsh-client-ui-reference-0.1.0-rc.8.tgz", + "integrity": "sha512-9qQYmOqG58qlsYNipOMsrL+Ay5Imx8lHJk9GV1rW++LGl6HMb6V1qZBHGIAwO/KOOInnJSEZE65DYB54r+z1Fw==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.0-rc.8", + "@deepseek-ai/dsh-file-reference": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-reference": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-renderer": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-renderer/-/dsh-client-ui-renderer-0.1.0-rc.8.tgz", + "integrity": "sha512-/MpR9i++feUFNzJgCChLfct1RphTPJ1WKfZrIsczjyOAZNBzAM5LRjpvM0ZO5Vg7rs7id+PLJYuUKy2e2V5aAA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-settings": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-settings/-/dsh-client-ui-settings-0.1.0-rc.8.tgz", + "integrity": "sha512-qSOjpmglOPsLzx6Q5yysrU4c4W6QWFOkyVyqePA5lNPOdny2MH19qbuNleQHbiBo4Zadh/dtBn/SuNRfSp+pjw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-settings-general": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-settings-general/-/dsh-client-ui-settings-general-0.1.0-rc.8.tgz", + "integrity": "sha512-lGGLCV17yUhKTHFAL+LWNY6LICLHmL614TbgaTFC80HWqeduURKzHU7a2PoySyMC2AOvYGpKjIRNUfifhv3aZw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-settings-models": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-settings-models/-/dsh-client-ui-settings-models-0.1.0-rc.8.tgz", + "integrity": "sha512-6iTLiQ4+4ErqV2e0Lxeunr1Q0JLFJ3EcJ1KcxvT0mEy5xzZuilX7KJxsBWq11q3Wy/I0MYhbHZzZC+d42mFM4g==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-settings-plugin-inventory": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-settings-plugin-inventory/-/dsh-client-ui-settings-plugin-inventory-0.1.0-rc.8.tgz", + "integrity": "sha512-IiixKrJbNqn4D8kvpSIRbxsfetEEbYN5Z3Uz209UWLOiY8Ja9VdQaVbh6BZlecO9BXX1Lgo8l8q+tXlC0IpifA==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-settings-plugins": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-settings-plugins/-/dsh-client-ui-settings-plugins-0.1.0-rc.8.tgz", + "integrity": "sha512-BLLFFxBz4TmnXXsR5bnQaLMboXBOvCfhoqSbdPFqkqrmJSD+3KGbg4SQJZPcfLL19Kq7qeDraPCXwxo3wpIULg==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-sidebar": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-sidebar/-/dsh-client-ui-sidebar-0.1.0-rc.8.tgz", + "integrity": "sha512-7wcBnS27j8iIVDuai8BVifxcDZd5DIjlHgNHQKx8xQM+x92VKhiFOoMccjhXNaE6sExCUsER1OWRVp65MJvjqg==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-layout": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-skill": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-skill/-/dsh-client-ui-skill-0.1.0-rc.8.tgz", + "integrity": "sha512-s9tW3tDQ/82TzZcUHC/dZy5zmPppFKbSXnBotQDvjAU+rkS3/4k4Y6u9LffyZ813OjCoCfs7GOfWHAyZ0VmBag==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-tool": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-subagent": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-subagent/-/dsh-client-ui-subagent-0.1.0-rc.8.tgz", + "integrity": "sha512-uK23jWwzFXbAELsnqS+7A4jL3Q/Ft8AI1hPrJE1ssldq535znexgdGAguZFC17fG+L0OWUtqwAKdXHmjyzqLiQ==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subagent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-token-meter": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-theme": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-theme/-/dsh-client-ui-theme-0.1.0-rc.8.tgz", + "integrity": "sha512-orLl5eoWOnvjj76lI6xucE+lT+o+ouniRMSFsuYwF1DNH2pbyfVPQYJWL3iM55gE28yyZJGgmtmABnQBiEF5iA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-tool": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-tool/-/dsh-client-ui-tool-0.1.0-rc.8.tgz", + "integrity": "sha512-+Mtgx56PptrtoN4Q36yISe3u0lnVazdqV9s+1cLU2C2sAw7bB7o2Y/J9PBPiuBSsK8rmCLbUPYK6QefenwFE7Q==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-trajectory": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-trajectory/-/dsh-client-ui-trajectory-0.1.0-rc.8.tgz", + "integrity": "sha512-nEKtvkbNhk1Qau8jpyvGfcGLCL2Tfmw6Oi72i2oiZ/DHIcsBaeD0JVWOraEmQp3jYVjuuLSdjdCiPkuNU9nwgw==", + "license": "MIT", + "dependencies": { + "@tanstack/react-virtual": "^3.14.9", + "diff": "^9.0.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-compaction": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-trajectory/node_modules/@tanstack/react-virtual": { + "version": "3.14.10", + "resolved": "https://registry.npmmirror.com/@tanstack/react-virtual/-/react-virtual-3.14.10.tgz", + "integrity": "sha512-SRyoUbdFMRHuYXMijV5H4ZarQWpXkj3iANq8OFre+pybeVap8ZJjZ3Nz9bVjx4d8PfobVUQUdKyyyHYk3E+djw==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.17.8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-trajectory/node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmmirror.com/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-trajectory/node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-user-questions": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-user-questions/-/dsh-client-ui-user-questions-0.1.0-rc.8.tgz", + "integrity": "sha512-zv2U7Ur1ovGSU7j5mwsiCHUK/L20IXRcSFBis3kDZ9nMMl9m0N+56bXrZtJD1U9kTU7gtJ4Xat+rDpr3uTn0JA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-workflow-run": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-workflow-run/-/dsh-client-ui-workflow-run-0.1.0-rc.8.tgz", + "integrity": "sha512-lVvzaR66JwR2lgdURooLEyinUFu5cSaJLWBgdLetw0Z1B6N4RzBNwiIZxmIEZKwaVS7vhO9w6mphGV+4ujiypA==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tool-workflow": "^0.1.0-rc.8", + "@deepseek-ai/dsh-workflow": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-workspace": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-workspace/-/dsh-client-ui-workspace-0.1.0-rc.8.tgz", + "integrity": "sha512-Il4hRRV9cDXyZptVC/OKHHOPNd84SwtflHCJ6RP97+OYxnENftdKgHENX7Kqf1RsSXF8RwmOtEvZoHljvYD79A==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-cmdline": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-cmdline/-/dsh-cmdline-0.1.0-rc.8.tgz", + "integrity": "sha512-xsnNFm2XpMD9wpBBLq80x8yXTi664DHywkLwxgAhDsWyp9tB2aSWaWVKxugPWSWwrB3F4nfloWqaeUfgXRFtBQ==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-code-runtime": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-code-runtime/-/dsh-code-runtime-0.1.0-rc.8.tgz", + "integrity": "sha512-6ZKyEE7dHH1UkKe5b0lg18Byww/7ocZyrNMVjAoUWEWp5i96Brf9Eavu9q3zepBMDMZJErCfV0pyt7KDhBJSig==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-code-runtime-worker-thread": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-code-runtime-worker-thread/-/dsh-code-runtime-worker-thread-0.1.0-rc.8.tgz", + "integrity": "sha512-l9fsKYZN2qthnR8fbH8LrTbhWman8rjFCumIttahOKvQEjFMWImY1p8ypM3QtreAtUF8jyHdDnlV8xWQLfanmA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-code-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-command-compact": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-command-compact/-/dsh-command-compact-0.1.0-rc.8.tgz", + "integrity": "sha512-3Zlgw9ghDvj2/22/O7V4kvkxKRfknQw4t9ks5GtBEEVRuU9ullpFElAwq/EZ049zeCnmHnyskyJmwYZRIGaiRA==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-compaction": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-command-feedback": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-command-feedback/-/dsh-command-feedback-0.1.0-rc.8.tgz", + "integrity": "sha512-RqEcutd+nHVGND/JobUybb8HsgRigr5iEowDk0D7aZeXFJ3tN/0pW67w7DOxdhAVnODIpWg5tSnFpCT8fS78kQ==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-anonymous-user-id": "^0.1.0-rc.8", + "@deepseek-ai/dsh-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-telemetry": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-command-goal": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-command-goal/-/dsh-command-goal-0.1.0-rc.8.tgz", + "integrity": "sha512-kMjRuGJA79czVNzx//JEMrjAqDvYpiaOSH95NUu+Mome76S34jmnhCnIyzcg02GcXelDbk9epELAz5HR1Yti/Q==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-goal": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-commands": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-commands/-/dsh-commands-0.1.0-rc.8.tgz", + "integrity": "sha512-/i99EDLrUyd4bUVn4d7XS46vg09ZE0ervbE/aQaVUWLjDYKrarb7hPx9slADAg5/cUlJsN6L6CP544h9qlUo4g==", + "license": "MIT", + "dependencies": { + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-attachment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-scope": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-compaction": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-compaction/-/dsh-compaction-0.1.0-rc.8.tgz", + "integrity": "sha512-sWNqZqejzpKsnEMPsNVA/WYK4S59XlYJtnWw8wCPushzQdUt5zhf+g7LVx0cStSozEfwurxRvaZdOmNaa4WhNQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-compaction-basic": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-compaction-basic/-/dsh-compaction-basic-0.1.0-rc.8.tgz", + "integrity": "sha512-UWAUbJbxFQTI3m4LnwZDlcYOfzWNFN1WLjy0qNONNcz3BBa5Nu9kcz4C9jioSp6pg3KMfUNUOtbBRySigTIt7g==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-compaction": "^0.1.0-rc.8", + "@deepseek-ai/dsh-compaction-tool-result-pruner": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-token-meter": "^0.1.0-rc.8" + }, + "peerDependenciesMeta": { + "@deepseek-ai/dsh-compaction-tool-result-pruner": { + "optional": true + } + } + }, + "node_modules/@deepseek-ai/dsh-compaction-tool-result-pruner": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-compaction-tool-result-pruner/-/dsh-compaction-tool-result-pruner-0.1.0-rc.8.tgz", + "integrity": "sha512-Mk1QE57HxCMScgQ9ury0o7AM14h6g41cDg1G2kiNqD4Z2LBwFTlZJJAgipyhts4mcr1hz2WZTc8Xgbl9067ONA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-compaction": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-token-meter": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-cordis-client-runner": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-cordis-client-runner/-/dsh-cordis-client-runner-0.1.0-rc.8.tgz", + "integrity": "sha512-UbV2zFdOCubLAjQkmYi+6aPxxDfFTSshiyL4EHRbZyqQTdhrnDMfkY48SiVz8s026+E8xxGRPU4kNf3lg5DGeA==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-modules": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-theme": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-cordis-host-runner": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-cordis-host-runner/-/dsh-cordis-host-runner-0.1.0-rc.8.tgz", + "integrity": "sha512-DCJr6INHCI0Dypr1JAJj/BnvN31tpIt3Y8+KQ3GDfHW6pUBsc9GnXHpKsuQ4sK/fTj80GjaDYPGdWeB/uKMMdw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-scope": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-credentials": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-credentials/-/dsh-credentials-0.1.0-rc.8.tgz", + "integrity": "sha512-VopOhtUEa/fSmJr3fyKbsEG043kX1oGuBrmELAbkhmuDigl1cN88g0VPb9rMG2/dJ1h08wg+h0RJP+Ex4LjRWw==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-credentials-local": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-credentials-local/-/dsh-credentials-local-0.1.0-rc.8.tgz", + "integrity": "sha512-Msbz/+xNIHFXLjcDy2hZFpfV1qduuyjLl35BJOUqNz9eCEYv8b0lfKDI7nirv5E/TdxzLkO75WQ4f+8nifeUpQ==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "chokidar": "^4.0.3", + "yaml": "^2.9.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-atomic-write": "^0.1.0-rc.8", + "@deepseek-ai/dsh-credentials": "^0.1.0-rc.8", + "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-file-reference": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-file-reference/-/dsh-file-reference-0.1.0-rc.8.tgz", + "integrity": "sha512-SoU4Zm7L8E9ud5nTs1tVAINFrgKvIKypCdOOMLBjJkjLUvXMYmKFxfCM7xaJjOmK/C5suEVUtpukZhZZvZALNA==", + "license": "MIT", + "dependencies": { + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-file-reference-local": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-file-reference-local/-/dsh-file-reference-local-0.1.0-rc.8.tgz", + "integrity": "sha512-dw3p1NmuDgJkse1J2RuLwgfiySEFiyRL40o3mo1ireGxXt85JfLns1FR2mdN6v7z6pBpIogGPKV//0U31BWTfg==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-file-reference": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-fs": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-fs/-/dsh-fs-0.1.0-rc.8.tgz", + "integrity": "sha512-ge5GW56+z20vYACQ2jqtevusdgMXa64laeU/Chql+VIA3+6xyJ8gLX+oj/1O5TlzhvmZ7ZPfZGBFBEVsEZzTQw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-fs-local": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-fs-local/-/dsh-fs-local-0.1.0-rc.8.tgz", + "integrity": "sha512-yWk6WrAHs8b6395XnZZOCf3wY90xYKjh/tyU3/rwFy4oU8zq9gzn/x8rdaZD0zUp+wE0XqfBf2AS4Jo03SKHyQ==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "koffi": "^3.1.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-fs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-fs-observation-policy": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-fs-observation-policy/-/dsh-fs-observation-policy-0.1.0-rc.8.tgz", + "integrity": "sha512-iarNlz2QoEROJUOHPjy7OKOwXuGeEhzk6nWj95F+UHNvKKaquagBE8YE1gzFqbg2U1wFVmPGoHBYl8q7hb5Z8Q==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-fs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-fs-sandbox": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-fs-sandbox/-/dsh-fs-sandbox-0.1.0-rc.8.tgz", + "integrity": "sha512-JfNgZHFyUyqO5XV/KQ2msO9OQ+98WrGCyfQp34yqCbIXSGS2r471C35Sub+OyTlpCkavGFUjsxo3iT7PLGyVVw==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-fs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-fs-local": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-goal": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-goal/-/dsh-goal-0.1.0-rc.8.tgz", + "integrity": "sha512-PR/9JjRn2H/33eyAVbtYBxzRWQhFO5N0S/188eAV9gjUTtHe5oSOnPxYy8iNkkMXxr+VE8Nno25Sun8WYmS8lQ==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-scope": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-goal-round-driver": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-goal-round-driver/-/dsh-goal-round-driver-0.1.0-rc.8.tgz", + "integrity": "sha512-hCOOofSuqYxWo+Zc/vcF1C+qk38JEgzJ2ya4bBisWYiRwVFygHODquQllK5ffDjFM+SnX+CdmA+l2piiwCkzcA==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-goal": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-headless": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-headless/-/dsh-headless-0.1.0-rc.8.tgz", + "integrity": "sha512-SfjPjyLeno7iScndgFI3s9s8focqXADLNgIxkFbsL0BdAY7v3A6TOe+iSxnYX0fMhDqFC1jT3FVYlEzbJBg0Kg==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/dsh-cmdline": "^0.1.0-rc.8", + "@deepseek-ai/dsh-code-runtime-worker-thread": "^0.1.0-rc.8", + "@deepseek-ai/schemastery": "^3.18.1", + "commander": "^15.0.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-agent-default-model": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-home-paths": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-home-paths/-/dsh-home-paths-0.1.0-rc.8.tgz", + "integrity": "sha512-9cQhH9CZsw7Zi73VnZk7CcuEVA6yR/Ox+7lp3VHq53fFYSPi0RL+kMWmh5XCrZB4U1OM3+PVsWsA5II9tB+t0Q==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-host-apiproxy": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-host-apiproxy/-/dsh-host-apiproxy-0.1.0-rc.8.tgz", + "integrity": "sha512-viq2CoS6bRavSe1t9sLjgunykO+Vn/m83uSgONMH8O7QN0Pk3+FnZGHlPZ1lQ4auNJTwQjxFG6w372cGq9usWw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-agent-default-model": "^0.1.0-rc.8", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-attachment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-credentials": "^0.1.0-rc.8", + "@deepseek-ai/dsh-goal": "^0.1.0-rc.8", + "@deepseek-ai/dsh-host-directory-picker": "^0.1.0-rc.8", + "@deepseek-ai/dsh-jobs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-native-command": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-projection-cache": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-query": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-title": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-skill": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subagent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8", + "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.8", + "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.8", + "@deepseek-ai/dsh-workspace": "^0.1.0-rc.8", + "@deepseek-ai/schemastery": "^3.18.1", + "fflate": "^0.8.2", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.8", + "@deepseek-ai/dsh-cordis-host-runner": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-host-directory-picker": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-host-directory-picker/-/dsh-host-directory-picker-0.1.0-rc.8.tgz", + "integrity": "sha512-aOUaUX0MTl8oLIuL27OkATMm+x2oTANuhhHpeGizn7+kfOX5bRKB3tyi7h6lGZ9O7WDiDLl2JajRqzBXlIPAAg==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-host-directory-picker-auto": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-host-directory-picker-auto/-/dsh-host-directory-picker-auto-0.1.0-rc.8.tgz", + "integrity": "sha512-t6sn3AlcAARVCOgYMNZS/Yneiw+/2mDM6QOGxR6bu5E7DYTT/tllHe0PlHF1Wgh/LtfOTeDtcSPPfXjmrd+Lqw==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2", + "@deepseek-ai/dsh-client-ui-directory-picker-browse": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-directory-picker-native": "^0.1.0-rc.8", + "@deepseek-ai/dsh-host-directory-picker-browse": "^0.1.0-rc.8", + "@deepseek-ai/dsh-host-directory-picker-native": "^0.1.0-rc.8", + "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-host-directory-picker-browse": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-host-directory-picker-browse/-/dsh-host-directory-picker-browse-0.1.0-rc.8.tgz", + "integrity": "sha512-zEQ7Wnz953GWiR3Y9GaqPGmhntEdjmcD+x6cbYKXWyeOtMHvv7Lu9EWGNneFHXFt50nMinDBT5kyGzElZ+cajA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/dsh-host-directory-picker": "^0.1.0-rc.8", + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-host-directory-picker-native": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-host-directory-picker-native/-/dsh-host-directory-picker-native-0.1.0-rc.8.tgz", + "integrity": "sha512-WUB4lW+ZmR743uH9LpFK0JrJU88601E7zvevffa93n7Bl4X7C5Y2yhbDK337H8W/brVlLNVgD2Fw2H/nIPZkDg==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/dsh-host-directory-picker": "^0.1.0-rc.8", + "@deepseek-ai/dsh-native-command": "^0.1.0-rc.8", + "koffi": "^3.1.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-host-frontend-static": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-host-frontend-static/-/dsh-host-frontend-static-0.1.0-rc.8.tgz", + "integrity": "sha512-/ef1xAygNL9hyGtSDzJ04nM5WSNzSxsloSUD2MQfM0FU3Y6b/CbHkUNFYaS0MnhS1YVSulDAGRQvXSgES8vZkg==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-host-plugin-inventory": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-host-plugin-inventory/-/dsh-host-plugin-inventory-0.1.0-rc.8.tgz", + "integrity": "sha512-knPtxCDZn1jbwRfOUnskjaj8hSXUCfBcjD9LPFXnKffsM6TFOeQrFGhkNKlZteXAMSChtY6pV9bpJj17y3SPNg==", + "license": "MIT", + "dependencies": { + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-host-webserver": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-host-webserver/-/dsh-host-webserver-0.1.0-rc.8.tgz", + "integrity": "sha512-MEKFrhvsYfj970QHLaKPpF/sIBpCe8NuSA0HGiWYJctX6TM9N3rLRhXx5p3zATDmXZclu1rbYhIbsqWw/twPig==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-invariants": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-invariants/-/dsh-invariants-0.1.0-rc.8.tgz", + "integrity": "sha512-u0lYqyxOYwfsVnbsfGXZos5vFvA4cqFnBEW3/ezgljNwkYwzeUP/Y5wjPnQjP+ZzBn3CnVeIF6s2N2Vk3iA5mQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1" + } + }, + "node_modules/@deepseek-ai/dsh-jobs": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-jobs/-/dsh-jobs-0.1.0-rc.8.tgz", + "integrity": "sha512-HqQudOA4VkjCMpxWiqTCMmuiTa/pehW6yG7dDsctO1vW5K48hWQ/h7czdJKgrh4SMuK4kchv3IipS1KaJmdaog==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-jobs-local": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-jobs-local/-/dsh-jobs-local-0.1.0-rc.8.tgz", + "integrity": "sha512-8/cr4rYVS2sLB/Z4lL/ERUn14iBqT9T85rJFxxhEKuT3gMcAkiJuKP7t+ZFWPYSSmcNqDPmMI1ExG+GzjxsLVQ==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-jobs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-scope": "^0.1.0-rc.8", + "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-launch-environment": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-launch-environment/-/dsh-launch-environment-0.1.0-rc.8.tgz", + "integrity": "sha512-VvYo5R7tu4QhX/dsNpuBy3AaauizU6yPCeWQUNzI3ZjGA0VHkkZhInilcgxVJkPA1lwEXYB+OoCAo07PUy6D/w==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-llm": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-llm/-/dsh-llm-0.1.0-rc.8.tgz", + "integrity": "sha512-hf6RmX2Stn1UtzpktSHQLkIktQkBm9i/3yFfJlbGSkwyw5Cv9sTYJbyoHzsSnnh6i8nRRcMcBhvMgm23I00oIw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-attachment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-llm-deepseek": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-llm-deepseek/-/dsh-llm-deepseek-0.1.0-rc.8.tgz", + "integrity": "sha512-s4LatnGbHwPJhcWGulO/7RsJBPHHBDB+2+1iRTJEn6vYPB0lOSRWtr91qdmAkUxBZmK5mwrf21IbuW+kWwtAZQ==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "eventsource-parser": "^3.1.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-anonymous-user-id": "^0.1.0-rc.8", + "@deepseek-ai/dsh-attachment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-credentials": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-llm-pi-ai": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-llm-pi-ai/-/dsh-llm-pi-ai-0.1.0-rc.8.tgz", + "integrity": "sha512-r9dmHYy759gKtAG0S96wTCS6O8WFDqGLQu8t+zGY6LnfdT4qUJdmWtJ+GwCn+BGwq1AglBzjabvyRBp6BxXcMQ==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "@earendil-works/pi-ai": "^0.82.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-attachment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-credentials": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-llm-retry": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-llm-retry/-/dsh-llm-retry-0.1.0-rc.8.tgz", + "integrity": "sha512-DmcOajgiku5l7aa5hU9DtwnIZu+i/XCGdb+Wego5W9bAswYeBAwbceloRNE3O6+jdRzy+K5U7KQ9V+olBsNqRQ==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-mcp-client": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-mcp-client/-/dsh-mcp-client-0.1.0-rc.8.tgz", + "integrity": "sha512-pcqXawjq9jRJQNHZTLN7pswJFMWjxCsEOdHUI9BdJqb9lRDsMi2cul1Hizfi51oKkcNo1u/K9Toqgh8o/ScAXg==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "@modelcontextprotocol/sdk": "^1.12.0", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-attachment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subprocess": "^0.1.0-rc.8", + "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-message-feedback": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-message-feedback/-/dsh-message-feedback-0.1.0-rc.8.tgz", + "integrity": "sha512-5DYXVLcnvTRs74Att52LGNR+x+RCf1X5s5+gOSUddsjnQ0wn4f3pzfc0o4F+91aE2zX3xzQ3/LhlW5UOJdkB0Q==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8", + "@deepseek-ai/dsh-storage-domain": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-native-command": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-native-command/-/dsh-native-command-0.1.0-rc.8.tgz", + "integrity": "sha512-bRaT8LOb2xmWQ88oSzM4+Y3N4Yy4J0tf15/kQTP53PY1FnOA5hGYWoRglQgfxIcJCrw4fEdUPie8Fff0dCOwdQ==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-output-retention": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-output-retention/-/dsh-output-retention-0.1.0-rc.8.tgz", + "integrity": "sha512-DeVjqZ9TtH2AEG57GBsHYT68274VygzuP9EqQLL01Ht1TrdK75xlVet1l3jdEbA7Eik4aSGDjAZD9/MsnlUIXg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-permission-presets": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-permission-presets/-/dsh-permission-presets-0.1.0-rc.8.tgz", + "integrity": "sha512-EeTn8Av5kmb1Qj53D7myRnKcNQrrupGrM2m9LVUO8ieWvlIaYX7n2g0y8kg14SUpcD2YzaT40oGJ6VeDuhTZAA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-shell": "^0.1.0-rc.8", + "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-persona": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-persona/-/dsh-persona-0.1.0-rc.8.tgz", + "integrity": "sha512-3COzIzZ5kBd+txuH418h9HG4feuVFyhdh02HWvzsWgaPsxjatuFS0OfS6qsejUTZrFDODQSr1WIOeSt80bDBtA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-plan-mode": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-plan-mode/-/dsh-plan-mode-0.1.0-rc.8.tgz", + "integrity": "sha512-/SY/iil7KLDvveqKRUZxO4HiADwDlOX9u3xyG/93h785mWSfRXooiv4Zp2tiBjU+pJB0tJXMsQNAXDdg7DWtwA==", + "license": "MIT", + "dependencies": { + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8", + "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.8" + }, + "peerDependenciesMeta": { + "@deepseek-ai/dsh-commands": { + "optional": true + } + } + }, + "node_modules/@deepseek-ai/dsh-pwsh-local": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-pwsh-local/-/dsh-pwsh-local-0.1.0-rc.8.tgz", + "integrity": "sha512-lHAEwC4pL5bVSKgwaeO2dMM9s/jnpDBRuTeVD2HkunpFfT1Kq4lFi+/YXj00L7mW9DBP6FMfwRRV/0Wgo1FtdA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-shell": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subprocess": "^0.1.0-rc.8", + "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-pwsh-sandbox": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-pwsh-sandbox/-/dsh-pwsh-sandbox-0.1.0-rc.8.tgz", + "integrity": "sha512-GVvj4zqpC/lwCGaRmIXIhFqOqewgoGOuokGFuv0QcjQUY3v9FnPor2dqDZr5pav8KFGb5GeGUzLoUbedmsWyZg==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-pwsh-local": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.8", + "@deepseek-ai/dsh-shell": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-repeat-tool-reminder": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-repeat-tool-reminder/-/dsh-repeat-tool-reminder-0.1.0-rc.8.tgz", + "integrity": "sha512-p7/g+NH6JJ0BnLx2V5o0hCm4wZr+dXQ9/Ev8LU6YyFRlcnma8TJuATL0wsA01xWfvvGLRxbe/tH41TRS1NVrJg==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-sandbox": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-sandbox/-/dsh-sandbox-0.1.0-rc.8.tgz", + "integrity": "sha512-dJpy5p/KjKkHG9AnMKU4+/ilj/4DCglsq3kof/jhIwtsZJCix07TEXP9vV8LCv4fU8af7cK3antXw4eVMqkwuQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-sandbox-local": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-sandbox-local/-/dsh-sandbox-local-0.1.0-rc.8.tgz", + "integrity": "sha512-sTrKDXD74yG6B24uhCyN9DuGSpX0SCelN4o5DZylKxt56WRRyywAGdLN0Xt8enqiKCxrq+PJBi+hDh2GtXBj6w==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/dsh-sandbox-windows-acl": "^0.1.0-rc.8", + "@deepseek-ai/node-addon-landlock-run": "^0.1.1", + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-sandbox-policy": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-sandbox-policy/-/dsh-sandbox-policy-0.1.0-rc.8.tgz", + "integrity": "sha512-xQOlYWbUr1Oj6cnRd5aHZIz52EOEAdSO1HJIfeDkQrVaOgohFsiiCIp6PNFumuGNWdCEnf4Q18ebafLNtazYlA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-sandbox-windows-acl": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-sandbox-windows-acl/-/dsh-sandbox-windows-acl-0.1.0-rc.8.tgz", + "integrity": "sha512-3DeFPD+laUbXrzVpEJ0FrF5pMQCqeu7YCJAWgORGj1aju7sIjpineEnKpE6EXhbc8C9Jqu2y5S7B9z6ydOL/Zg==", + "license": "MIT", + "dependencies": { + "koffi": "^3.1.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-schedule": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-schedule/-/dsh-schedule-0.1.0-rc.8.tgz", + "integrity": "sha512-L4GO1Bv3+SHIXafjGQcf0sJI9eTf9whxVmYlBxYHw7h7StOZJPMhG9HpsKz4mO9FZ7J4zhBtpYlFbhOQaJheqA==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-scope": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-scope/-/dsh-scope-0.1.0-rc.8.tgz", + "integrity": "sha512-/o+PxkVV2zUy9lYlMFr4TWSb+ESW4DE+fI891yIbsLzOJdFgfbmmLpGrlWgnz8/unIGJZxjeXdwyppV04Z277Q==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-session": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session/-/dsh-session-0.1.0-rc.8.tgz", + "integrity": "sha512-Qs5Mgn32WPPVB4rKHjsZBgLOkh0TQurUAcc9GaDVudK4dUkpno2HDh++qu/PrwRA3CZ/iwPAoFHCpB3AMMaoLg==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-scope": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-session-checkpoint-policy": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-checkpoint-policy/-/dsh-session-checkpoint-policy-0.1.0-rc.8.tgz", + "integrity": "sha512-9DpZhQN4MExSjbjBSmaH08DmFS/GHRrsVhO5e3WgTVuH13ugdl5yxc4MEb8qvOhEJUUtOx3fXHuMpSTa0Dxa7w==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-session-log-export": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-log-export/-/dsh-session-log-export-0.1.0-rc.8.tgz", + "integrity": "sha512-6vDC7uH0WZkwmlj/iql6AmSL9Uifiqi28OIZSDIi8/KRHDHnt5hvjobJecuIxhV9hQSDmNsstErHGx4ng2GJFQ==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-session-persistence": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-persistence/-/dsh-session-persistence-0.1.0-rc.8.tgz", + "integrity": "sha512-JujorG/UCern0+zxJzNswyWl2tFG9sLzfVNvA5YkTLaRLZt7ZsEhxG4DxWZ6uj+3RJ8sf4bqdKqvpIn9t1pwUw==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-session-persistence-jsonl": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-persistence-jsonl/-/dsh-session-persistence-jsonl-0.1.0-rc.8.tgz", + "integrity": "sha512-T/Lvh76O2tpVw04FAhgLF3Qovq3SrUnzaywIbB+Uw67a2DD63Qg+cJY9WO6Pu/UowPxbpuUOd12c6iPY5x8d7Q==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "koffi": "^3.1.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-session-projection": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-projection/-/dsh-session-projection-0.1.0-rc.8.tgz", + "integrity": "sha512-q00B+rl7WAuK0Bgh93SY2+ZpqaH2kJhfJ9n6Jg6R/m4plnH0Ip4oCAHl6u8rBzDgb1mniQGEQDz/7BN5Q3It8Q==", + "license": "MIT", + "dependencies": { + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-session-projection-cache": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-projection-cache/-/dsh-session-projection-cache-0.1.0-rc.8.tgz", + "integrity": "sha512-IPvO7k9FlekOJXn4XLWUwU5+JDQn7khu8Oa04GFDystU3c1CnOwoa7SGMHOfJau3r177M4ncXApwN5Ra6uhyXw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-storage-domain": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-session-query": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-query/-/dsh-session-query-0.1.0-rc.8.tgz", + "integrity": "sha512-q8eyfpgPuJx24l1fEevDkc3Dux3lNp/yDzVkDL4nSTGNaDx/h9Y6iG7SZdqWM5nxOET3iwxi4AkPM7lYyLg3cQ==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-title": "^0.1.0-rc.8" + }, + "peerDependenciesMeta": { + "@deepseek-ai/dsh-session-persistence": { + "optional": true + } + } + }, + "node_modules/@deepseek-ai/dsh-session-query-sqlite": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-query-sqlite/-/dsh-session-query-sqlite-0.1.0-rc.8.tgz", + "integrity": "sha512-jrNq2vgWIH7OzNMHXrKEjEMwy86I36I94a8X5u937xo2B2JD8ZIWfuWHC9YyLqabRwNf7VcQ6VZ6ruSgESRE7A==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-query": "^0.1.0-rc.8" + }, + "peerDependenciesMeta": { + "@deepseek-ai/dsh-session-persistence": { + "optional": true + } + } + }, + "node_modules/@deepseek-ai/dsh-session-reference": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-reference/-/dsh-session-reference-0.1.0-rc.8.tgz", + "integrity": "sha512-meNS02BOkLc2SnF2qWvl5X1Psk59t0Q1vvwjfCkczD29g4nm7Y02EZ089HJKii0VAsSB39RYA2rf6MCSl6vhow==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-compaction": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-output-retention": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-query": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-session-stats": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-stats/-/dsh-session-stats-0.1.0-rc.8.tgz", + "integrity": "sha512-T+SEzBkMxZpQ31MaeJCjaLN3/xu8yyZ3Va5u6osfq0nsJz1tvuZ53MHMbApdwERHVV0t/1eILTUo1J6XdksUEA==", + "license": "MIT", + "dependencies": { + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-session-telemetry": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-telemetry/-/dsh-session-telemetry-0.1.0-rc.8.tgz", + "integrity": "sha512-Rs7w+PP/uuOaa34RFY4q6Xjes/C32H0gPvZe/gV9M87y1EcJs+mFBzhnc/zyKeaAdT25RWIwO3KATmKlT/D0Dg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-session-telemetry-otel": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-telemetry-otel/-/dsh-session-telemetry-otel-0.1.0-rc.8.tgz", + "integrity": "sha512-rpPwidBwKV53WM2bYW/eYOZ6j5U9VHOIVrITQkQtGDMAzD9UaqfAXfXfUlsw9vsfjED2gKmSf7vn+bVfy8Tjaw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/api-logs": "^0.220.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.220.0", + "@opentelemetry/otlp-exporter-base": "^0.220.0", + "@opentelemetry/resources": "^2.9.0", + "@opentelemetry/sdk-logs": "^0.220.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-anonymous-user-id": "^0.1.0-rc.8", + "@deepseek-ai/dsh-command-feedback": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-telemetry": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-session-title": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-title/-/dsh-session-title-0.1.0-rc.8.tgz", + "integrity": "sha512-OFG9kHFGnM5ND4LCkqg8oZr57qgF6xMkBxR/4MRaJJ42QrPI9QKzoZVRpNpXEl3v3uw8oR7Jj0jbc7WClNl3ow==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-session-title-first-prompt-llm": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-title-first-prompt-llm/-/dsh-session-title-first-prompt-llm-0.1.0-rc.8.tgz", + "integrity": "sha512-pFx2hPVYyRCRcbd4WJZVapVekLLEGMAG1KAcrixf3XOGDNaqVEPF8LWatx8iKQRfVSQlvIOwDO8uEsC0NZiPNA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-title": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-title-llm": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-session-title-llm": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-title-llm/-/dsh-session-title-llm-0.1.0-rc.8.tgz", + "integrity": "sha512-6xz3umGBtGmM+t5O5wwlCwbH9gmBOlQBfg7qGnPJftWc4VWHG7l2VUYqd3DNTQVTHIA/hSN4qg/LXTm79rADGQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-title": "^0.1.0-rc.8", + "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-settings": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-settings/-/dsh-settings-0.1.0-rc.8.tgz", + "integrity": "sha512-pqbMoiSP+Ieww9/4gcHYXcBCDlno1zYDu2OPq+5xdZ+McnK+5YBtHsiRpnR9cHQxGTH7lkfIv+adkOueQW0utg==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/schemastery": "^3.18.1" + } + }, + "node_modules/@deepseek-ai/dsh-settings-file": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-settings-file/-/dsh-settings-file-0.1.0-rc.8.tgz", + "integrity": "sha512-2mCyAPkxA5r7Ezgt41XtJxRBsj5Dkl21e5v7w+6YE3C8/n9zRRlN4X5oInHcUKEmgoTrNLCBCgOs5DNrl+pErw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "chokidar": "^4.0.3", + "yaml": "^2.9.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-atomic-write": "^0.1.0-rc.8", + "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-shell": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-shell/-/dsh-shell-0.1.0-rc.8.tgz", + "integrity": "sha512-RNhp7Y25DXUH7o36X4GcN0lnOLg6NXTlWZpYNURT7aDj3UHcLLajRsyddiAh1EbwQnl0GRKRZhLaZHrrG6gMIw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subprocess": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-shell-env": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-shell-env/-/dsh-shell-env-0.1.0-rc.8.tgz", + "integrity": "sha512-E/Rlka8BnpB7GM67ZJPjvZyp0O9L5x28giyJhHWZcPHN4rsEQFVVqk0cH6iDT5STy+dMc9BAUW+im18kATIxsA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8", + "@deepseek-ai/dsh-shell": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-skill": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-skill/-/dsh-skill-0.1.0-rc.8.tgz", + "integrity": "sha512-MqcOzl4Y5pQEvFBNVAi5w0IZWH5kVbXmp1RYScrl+8HpVATZbgumDhztVnEc2kUrZu+o6LKb1hnbunK6JHUE5g==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-scope": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-skill-badge": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-skill-badge/-/dsh-skill-badge-0.1.0-rc.8.tgz", + "integrity": "sha512-VjISwf5qDpkD+bxLuVXYL7IakhPoZudIOwWCFjTD++Tj8VQTN9ORCq1uCOatFLi5v/an8MzdZ91O2pXfQtb7dQ==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-skill": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-skill-filesystem": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-skill-filesystem/-/dsh-skill-filesystem-0.1.0-rc.8.tgz", + "integrity": "sha512-A87t/1+wxPiMkUXWVdf4DfgxA8rp1eLmStMvzfdthR/WxtGPzuEv2L5oyNDzKiCBwIcxBN6Fy2THNa2Xba2kQQ==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "chokidar": "^5.0.0", + "yaml": "^2.4.2" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-fs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-skill": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-skill-filesystem/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@deepseek-ai/dsh-skill-filesystem/node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@deepseek-ai/dsh-spill": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-spill/-/dsh-spill-0.1.0-rc.8.tgz", + "integrity": "sha512-LDfyBuRS/yfc+JYvSicEXYJDj5PFBfWN6vJ5G3wy14eMrq3OKoYip8lyIjEdR4qwCVh0s+oSi28AlcvkPuyOdw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-spill-local": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-spill-local/-/dsh-spill-local-0.1.0-rc.8.tgz", + "integrity": "sha512-4Ew3cBOQxW6wA4U8svXCjUyhX00+h0W/bK9CzTy/HoYrNJhP85euJVV7SfNlq6OfTeQBWn3U0wKwNlcc3gAyAg==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-spill": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-spill-policy": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-spill-policy/-/dsh-spill-policy-0.1.0-rc.8.tgz", + "integrity": "sha512-9y2IHaKowtqGxhYd59R3OfWv1sonIDv2h9WX/jhxbagMvORfRkhjWJP4YAxp+l4Ba1LhVLcuG/nb2p8hqGdO4g==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-output-retention": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-spill": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-storage": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-storage/-/dsh-storage-0.1.0-rc.8.tgz", + "integrity": "sha512-KcxiNFAC1X0GhbS9yWGo7FLARBNsxoyU9JatxQtA23MuqOZaN4Nzy3bsMViK4/snzpPGwm//I+wfUywSjM6uBA==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-storage-domain": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-storage-domain/-/dsh-storage-domain-0.1.0-rc.8.tgz", + "integrity": "sha512-rHphQ0jveKoCurwh9TX+VAVFsMQivBgghQBOAsWdVDZ3W6boeXNhMZWuGWZ7WjMPD3iJKgiFCx+650hhsao+LQ==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-storage": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-storage-json": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-storage-json/-/dsh-storage-json-0.1.0-rc.8.tgz", + "integrity": "sha512-cTlqy+5bQH7/UOFqRoWSaY9DaaDpcx4jmvWcwY8ciMUBse8BXcEtchVr5zqqss+xA/+Z/l9VraH+diwwRuAWbg==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-storage": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-subagent": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-subagent/-/dsh-subagent-0.1.0-rc.8.tgz", + "integrity": "sha512-EI94+hYMfAe8BQkkH/FTcciGxajFfAdQe66Jhy2/eyR3dssRAZ+oJsV6SzU20x3m2yd0gmMaXzO1iV3gwYhY+g==", + "license": "MIT", + "dependencies": { + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.8", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-jobs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.8", + "@deepseek-ai/dsh-scope": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-projection-cache": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8", + "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.8" + }, + "peerDependenciesMeta": { + "@deepseek-ai/dsh-agent-presets": { + "optional": true + }, + "@deepseek-ai/dsh-jobs": { + "optional": true + }, + "@deepseek-ai/dsh-sandbox": { + "optional": true + }, + "@deepseek-ai/dsh-sandbox-policy": { + "optional": true + }, + "@deepseek-ai/dsh-session-persistence": { + "optional": true + }, + "@deepseek-ai/dsh-session-projection": { + "optional": true + }, + "@deepseek-ai/dsh-session-projection-cache": { + "optional": true + }, + "@deepseek-ai/dsh-user-approval": { + "optional": true + } + } + }, + "node_modules/@deepseek-ai/dsh-subagent-fork-in-process": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-subagent-fork-in-process/-/dsh-subagent-fork-in-process-0.1.0-rc.8.tgz", + "integrity": "sha512-CVen9Fu0TwiwuyTHmZeankN/v0EwdMwOAcq8fSTIyFGPG4ngtqQNSRMxwYTrz39kKyHv0VBW+r8tRd1LBdTAWA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subagent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subagent-in-process-driver": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-subagent-in-process-driver": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-subagent-in-process-driver/-/dsh-subagent-in-process-driver-0.1.0-rc.8.tgz", + "integrity": "sha512-UJXwh484V/rEmRrvf5l0IWg2ceVOaSOU7Z6DEBq++bKOuFQ2BVkMm+Uj3dk4qFngeb10+/GXlRkbe3mqPwQDSA==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subagent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-subagent-spawn-in-process": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-subagent-spawn-in-process/-/dsh-subagent-spawn-in-process-0.1.0-rc.8.tgz", + "integrity": "sha512-+ZratMZJiNg0vDmaEauJpiNI9IzVwCWisbidow6AV8O5CLC9/XlcMTjFGxmxcbnPdNlVM9i8OLK8gY7/kZ7tTA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subagent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subagent-in-process-driver": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-subprocess": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-subprocess/-/dsh-subprocess-0.1.0-rc.8.tgz", + "integrity": "sha512-rTWwWCq/WrBTjaXhApu7gaho6UHwMZq0+l7nZ06YMUreNZoCHfg67pAXopMqTgr/iotphmXQdChrLgBabzL1gA==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-subprocess-local": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-subprocess-local/-/dsh-subprocess-local-0.1.0-rc.8.tgz", + "integrity": "sha512-2QMd6Nwjd+ARor4g+GSZ3dNyTvEf2IPOje47nQwa3UCmltLxn8K8gjDAN/5sGr/IF56P14FeF4xFag7tMK+Zdg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "koffi": "^3.1.0", + "node-pty": "1.2.0-beta.15" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subprocess": "^0.1.0-rc.8", + "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-system-prompt": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-system-prompt/-/dsh-system-prompt-0.1.0-rc.8.tgz", + "integrity": "sha512-yk8Z85rzX2EwpKNWA5i+K3I3mqsIIUc9niwbDpXO8vlHvpC2qR3RNHjJIJN2EZYDqfSzTVtkbxHFsuMzDXIbew==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-scope": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-terminal": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-terminal/-/dsh-terminal-0.1.0-rc.8.tgz", + "integrity": "sha512-Vu73FrQGx8h7Mqq0QdKtzAiZ3xgVM4fB9wmf3fEl6OiJTcqfc8t4lgug3gE7uBeDVRaLn+aq7kHbEUOY0fsm1Q==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-terminal-bash": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-terminal-bash/-/dsh-terminal-bash-0.1.0-rc.8.tgz", + "integrity": "sha512-ehBMVr/0kZ15Vud7uE6PLobukwJi/BqGDbI1oonPPTOoTjDvuCqlKotNEQtS/G4NsvGshEABv+m5X5ELcRCWUA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/dsh-pwsh-local": "^0.1.0-rc.8", + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subprocess": "^0.1.0-rc.8", + "@deepseek-ai/dsh-terminal": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-time-context": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-time-context/-/dsh-time-context-0.1.0-rc.8.tgz", + "integrity": "sha512-9Rdse95S6Uk8OAHf78AQN/pxV6+29fCkIbF4ONP/F/6YBciKDORypRyMbHNSY81fBMHQPhW1xvZecGISdIHHiQ==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-timeout": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-timeout/-/dsh-timeout-0.1.0-rc.8.tgz", + "integrity": "sha512-RwOC/qHribE6b+LStAO7aAgweefLE5sqDO7Uz6/mnTDyJxCt7HNQnE/Op8sAvRa33d752aUkPfO9S0mgzVAoFQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tmux-context": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tmux-context/-/dsh-tmux-context-0.1.0-rc.8.tgz", + "integrity": "sha512-ezhL3Wjpn4TqqvgSuKrJgNVG7jrWGekEWRYimqD1SvmAHQAS+Xr9q7DBl1uSFfJDWUOAPlAB8VAuo3k/CLUwKA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-shell": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-token-meter": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-token-meter/-/dsh-token-meter-0.1.0-rc.8.tgz", + "integrity": "sha512-ZcS6vuuJhoqjUrbyROuiiYk4nrvChkkYEfIoj7CjGbNArWOhbStmBaiPyahtcB1kThcnUKdu7vv7DaZJdLw8Lg==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-compaction": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-ask-user": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-ask-user/-/dsh-tool-ask-user-0.1.0-rc.8.tgz", + "integrity": "sha512-PQVcuvNvTI18B2jSvgS20wX0GRnpom+6X6iNBL/WPLiicJeoxvDYj+M7SjdSsqBNv5t0i9VlYhO4NYicmluy+g==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8", + "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-bash": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-bash/-/dsh-tool-bash-0.1.0-rc.8.tgz", + "integrity": "sha512-VvP8Ky8ifqS7Iy7XUxc6cWB7sse3X6gqr92m0hBuodwQblzrZq2b6IyzfTzXKZKLqt5mMUaLRt7IPIDOOG+WEQ==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-jobs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.8", + "@deepseek-ai/dsh-shell": "^0.1.0-rc.8", + "@deepseek-ai/dsh-shell-env": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8", + "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-bash-persistent": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-bash-persistent/-/dsh-tool-bash-persistent-0.1.0-rc.8.tgz", + "integrity": "sha512-GySj2Nr7y4ewYTH0miD5DzfLEZNxAmWfSyJ5oOYVtzIbqkcU5WOjpsjQvDg0kuERXAq4MYcdc2QfrPet1/MkIQ==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-terminal": "^0.1.0-rc.8", + "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-call-timeout-policy": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-call-timeout-policy/-/dsh-tool-call-timeout-policy-0.1.0-rc.8.tgz", + "integrity": "sha512-Z6M/nkcz40DisaOIHqUOjjjW4wCi+JqQNoUhOu+VAoBDWHl7SDmEvdWSQUwkyOETjcfdRSG4RTJ5z/rBUVCPHQ==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-cordis": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-cordis/-/dsh-tool-cordis-0.1.0-rc.8.tgz", + "integrity": "sha512-bIbkazp5xasqtJBFljOGVIdxyAAWIcKoxMuhDoUpfPFuEqZaXJumYJJ9+ugqr+KKkddjuf2SvlpE5XB4zWcNvg==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-cordis-host-runner": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-scope": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-fs": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-fs/-/dsh-tool-fs-0.1.0-rc.8.tgz", + "integrity": "sha512-pxo2YfVlqcw0sCA2+4DtqaaYsuxkx1QfvjT3VO+ckZMWn/UnGOUuzjJtKhVynwjm8prttGqK2LBXUTBZPlKTWg==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "diff": "^9.0.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-attachment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-fs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8", + "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-fs-search": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-fs-search/-/dsh-tool-fs-search-0.1.0-rc.8.tgz", + "integrity": "sha512-AZvESByrPbBIYIGDoTgVOHH48antHy2ZSaLfsVfz1Rw/jQIeiBKwt2LYIPhRGGvev6TXw95qWKt5cyv6WDXHlg==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "@vscode/ripgrep": "^1.18.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-output-retention": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-spill": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subprocess": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-goal": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-goal/-/dsh-tool-goal-0.1.0-rc.8.tgz", + "integrity": "sha512-EO8iqRV3R8YgdMGUVAp/20XCKbegdwZ7fqr7+oSg11lGHuTnXDz4im4HBXri/wmL9XHeBjmcAQ0BXESbhX2VOQ==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-goal": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-jobs": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-jobs/-/dsh-tool-jobs-0.1.0-rc.8.tgz", + "integrity": "sha512-p3gWi38ryJFRes1lTAdVkdZrUBB8RYPpy4XqZ9DgC2qcrLOJZnnsprIUxyDDmhbyargOgTGc3hEeRU5g8OQAIA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-jobs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-output-retention": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-pwsh": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-pwsh/-/dsh-tool-pwsh-0.1.0-rc.8.tgz", + "integrity": "sha512-B5F08tsSD7DHqXDXjn/Q11FavHw1U8/WAEqp8AC47vDRymPJIgTM8zSlJ0Lx7aZaKPTW4zQYtSmwUhy93q6zfw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-jobs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.8", + "@deepseek-ai/dsh-shell": "^0.1.0-rc.8", + "@deepseek-ai/dsh-shell-env": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8", + "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-pwsh-persistent": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-pwsh-persistent/-/dsh-tool-pwsh-persistent-0.1.0-rc.8.tgz", + "integrity": "sha512-OxQhywkvUTuGj26yaE1LyoADsCKttaIdiq2dJd+Xcbjc+tnI1gfoVrBj+Sta3/uT7x66kKjXZ1+gnw6gK5qISQ==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-terminal": "^0.1.0-rc.8", + "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-ralph": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-ralph/-/dsh-tool-ralph-0.1.0-rc.8.tgz", + "integrity": "sha512-+kVCbLEBDxOCCHPdR5+hgQFbkTvTaJ3D7zTNxNEtS5Tr/7eGAU7/VmwNMMSZ4Z+eyHQQEm4vgT+O1pMTvLmb7g==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subagent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8", + "@deepseek-ai/dsh-workflow": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-skill": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-skill/-/dsh-tool-skill-0.1.0-rc.8.tgz", + "integrity": "sha512-oQAPyXiQDDZ73rZwST7KqwUAr0Al3U1dxWdtzETsMzvNSQE9fmMBz5jUObFIEjxxJcA07Pp1DFzuJdpkCIDeCw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-skill": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-str-replace-editor": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-str-replace-editor/-/dsh-tool-str-replace-editor-0.1.0-rc.8.tgz", + "integrity": "sha512-2gyO5a5xNvBo9I5vmVSvgXRfxUfKGDu3WjjdK0aDWx/Y+C1r1mGVC7R5mN/oJstcUCIyKm483ux1pT9/O1QD8w==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-fs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.8", + "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-subagent": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-subagent/-/dsh-tool-subagent-0.1.0-rc.8.tgz", + "integrity": "sha512-mwNV8M7hsy9+OsSFCluv87FiDHaXO91m83xF/o34Ip9xrgkGs7LOCjvUdCpwjxFTJenWEcvEU90aa7vv3aT/Iw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-jobs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subagent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-subagent-control": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-subagent-control/-/dsh-tool-subagent-control-0.1.0-rc.8.tgz", + "integrity": "sha512-zXF9iIzr96w/S3jNxkRcyV1ouwA1eUCSmvqhlSDkkcA8RH5jt5vWkTia8fwRs1pFS9m7OpbLtvbDxjC8D3nedw==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subagent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-subagent-report": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-subagent-report/-/dsh-tool-subagent-report-0.1.0-rc.8.tgz", + "integrity": "sha512-u0CzoSu7O1YyOpGyHfmT/P2hCfnepSzVKrs6MHFrzjbuiRFEcVme6/iLrc7QdLtg484Q1kcnqG2z0OZS24AnWQ==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subagent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-todo": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-todo/-/dsh-tool-todo-0.1.0-rc.8.tgz", + "integrity": "sha512-eWxwTeX5WsQPSsz1qyJq+UMiASYB5UHLdObVd4vFLYKbFOoglUK0X+Ie5OjlAkFTP+OzCQMAzUl2gJMwJPWKig==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-web": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-web/-/dsh-tool-web-0.1.0-rc.8.tgz", + "integrity": "sha512-MrTkQ2L52A13ysTM94bSW+5sXq3AyVNoo5q3bK47bsl+hg9Kga5gDgGAZ+SGVSQZAUEHXfuYGHaErdUp50bncw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "@joplin/turndown-plugin-gfm": "^1.0.67", + "turndown": "^7.2.4" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8", + "@deepseek-ai/dsh-web": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tool-workflow": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tool-workflow/-/dsh-tool-workflow-0.1.0-rc.8.tgz", + "integrity": "sha512-Z3MCCLM28J/nqiqwHG9roGVUzwMKEADRsSNLPYNemXq5vVEYjm5sBwC+QcWtiQ7NxKXf5ViUu3eKXTe1Fh5TNg==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8", + "@deepseek-ai/dsh-workflow": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-tools": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tools/-/dsh-tools-0.1.0-rc.8.tgz", + "integrity": "sha512-tvuGAVUS4yoPW9zH7CGWJgMlAzGjfu2UlRiricldlcwSKAaJZPZx/K/YfTivUyDTER6aK1DJUnIpVG7RD/DoHQ==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-code-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-scope": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8", + "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-typert-loader": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-typert-loader/-/dsh-typert-loader-0.1.0-rc.8.tgz", + "integrity": "sha512-IB2Bkv7UglS0y/Dt7buFq87bbUkD/njvuCp4/bRxlbb/8U02b7v18UXnYFoZjCusfzeYxaCBZihImv7aDMBhCA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-typert-registry": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-typert-protocol": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-typert-protocol/-/dsh-typert-protocol-0.1.0-rc.8.tgz", + "integrity": "sha512-scwEAefRBL7gRpaXiEdiHRMvlr8dLmUbiEvy0LOAX/8X3FZBd/5tHQ/8lCppAQNTDZMI/LSiSMpu5XrFId+7cQ==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-typert-registry": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-typert-registry/-/dsh-typert-registry-0.1.0-rc.8.tgz", + "integrity": "sha512-pxfRE5f1y81h2lNqj0uooYrErTVgpzKfYm4W+ppRafN5s/+1jlLIYWiG3wWD3bnxyoD2NLcTEhi/GXoaqiwdaw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-user-approval": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-user-approval/-/dsh-user-approval-0.1.0-rc.8.tgz", + "integrity": "sha512-6SFCxp7dgNFEyzQIcs5i9C+5djgXsAeV0iKmZUCLlcIeVM1chcCMHyGcoXlw8yyB14qFMrMs+TPvhvpu2WWxAw==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-scope": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-user-questions": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-user-questions/-/dsh-user-questions-0.1.0-rc.8.tgz", + "integrity": "sha512-kiOAlpKKfB89iObWiiVpueD4XCaday203LGUs7+eGb2isolY6VppoGb7ZmcJQqsW63bjbAA7wZeJuN3CoxcvSQ==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-web": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-web/-/dsh-web-0.1.0-rc.8.tgz", + "integrity": "sha512-YLraDwZTx0Y3Jqw2cu4eClv9ZgEJYoeSFzTe7c9JZ4Z3ThaNCCwEaXWEPMBiPKU6QlR8ahkPPj6xoU9+nPO66g==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-web-app": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-web-app/-/dsh-web-app-0.1.0-rc.8.tgz", + "integrity": "sha512-Te/N+AxFmQF2267yMqama5Lp2evsBJeFsqhnEZPfvFhquLBBWgtO5DNZ2cxto5NhGGNNCk/JV25vHRxd7bZl8g==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.8", + "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8", + "@deepseek-ai/dsh-app-boot": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-hmr": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-modules": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-agent-preset": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-attachment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-brand-official": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-commands": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-cordis": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-deliverables": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-directory-picker-browse": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-directory-picker-native": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-goal": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-jobs": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-layout": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-message-feedback": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-model-selection": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-permission-presets": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-plan": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-reference": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-renderer": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-settings-general": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-settings-models": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-settings-plugin-inventory": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-skill": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-subagent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-theme": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-tool": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-trajectory": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-user-questions": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-workflow-run": "^0.1.0-rc.8", + "@deepseek-ai/dsh-client-ui-workspace": "^0.1.0-rc.8", + "@deepseek-ai/dsh-cmdline": "^0.1.0-rc.8", + "@deepseek-ai/dsh-code-runtime-worker-thread": "^0.1.0-rc.8", + "@deepseek-ai/dsh-cordis-client-runner": "^0.1.0-rc.8", + "@deepseek-ai/dsh-cordis-host-runner": "^0.1.0-rc.8", + "@deepseek-ai/dsh-file-reference": "^0.1.0-rc.8", + "@deepseek-ai/dsh-file-reference-local": "^0.1.0-rc.8", + "@deepseek-ai/dsh-host-apiproxy": "^0.1.0-rc.8", + "@deepseek-ai/dsh-host-directory-picker-auto": "^0.1.0-rc.8", + "@deepseek-ai/dsh-host-directory-picker-browse": "^0.1.0-rc.8", + "@deepseek-ai/dsh-host-directory-picker-native": "^0.1.0-rc.8", + "@deepseek-ai/dsh-host-frontend-static": "^0.1.0-rc.8", + "@deepseek-ai/dsh-host-plugin-inventory": "^0.1.0-rc.8", + "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.8", + "@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-message-feedback": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-log-export": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-projection-cache": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-reference": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-stats": "^0.1.0-rc.8", + "@deepseek-ai/dsh-storage": "^0.1.0-rc.8", + "@deepseek-ai/dsh-storage-domain": "^0.1.0-rc.8", + "@deepseek-ai/dsh-storage-json": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subprocess": "^0.1.0-rc.8", + "@deepseek-ai/dsh-web-frontend": "^0.1.0-rc.8", + "@deepseek-ai/dsh-workspace": "^0.1.0-rc.8", + "@deepseek-ai/schemastery": "^3.18.1", + "commander": "^15.0.0", + "open": "^11.0.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-shell-env": "^0.1.0-rc.8", + "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-web-frontend": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-web-frontend/-/dsh-web-frontend-0.1.0-rc.8.tgz", + "integrity": "sha512-GxWJtkNEniYtSH19XWIJvMO2RSP2bMAZvs3Z6m9cEr9OFbngCY5XCXwqsJMYqOJIao0eSePp/eRCPeXNvqeGXA==", + "license": "MIT" + }, + "node_modules/@deepseek-ai/dsh-web-search-deepseek": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-web-search-deepseek/-/dsh-web-search-deepseek-0.1.0-rc.8.tgz", + "integrity": "sha512-ifaFw+Cil3yte/9r//3wwMWWPIMuYhvYAB0Hk56IVf+8RDbnRkTJY2foFNNgD91dq1ZL2gBijkdxv1c7wDt+EA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-credentials": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "^0.1.0-rc.8", + "@deepseek-ai/dsh-web": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-workflow": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-workflow/-/dsh-workflow-0.1.0-rc.8.tgz", + "integrity": "sha512-a4UJKJAFOPQgVaWegk8MIh8BQjqerkGhHu2+uGF4wyGanzI7FHHY/OUmH4hHTVEx2qFcWh732Od/T8LgL6gcbg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-workflow-worker-thread": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-workflow-worker-thread/-/dsh-workflow-worker-thread-0.1.0-rc.8.tgz", + "integrity": "sha512-QBkHJ8l174U3VqHRi2KyJIFey+Uxzo/OOFbTFx7TP5f761lPvqYiXlILuQJ9QvT7WJTyZO3QRLIIU6ZEBwxTIA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-subagent": "^0.1.0-rc.8", + "@deepseek-ai/dsh-tools": "^0.1.0-rc.8", + "@deepseek-ai/dsh-workflow": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/dsh-workspace": { + "version": "0.1.0-rc.8", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-workspace/-/dsh-workspace-0.1.0-rc.8.tgz", + "integrity": "sha512-Uyw44WcYvXRQjb0kUstTEJmL83hkGQbWnJzglaGWcAvsZBjQHeBgKuLzOZin1yc/dRX2pKuk1InFwxKXpaHjBA==", + "license": "MIT", + "dependencies": { + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.0-rc.8", + "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session": "^0.1.0-rc.8", + "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8", + "@deepseek-ai/dsh-storage": "^0.1.0-rc.8", + "@deepseek-ai/dsh-storage-domain": "^0.1.0-rc.8" + } + }, + "node_modules/@deepseek-ai/node-addon-landlock-run": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/node-addon-landlock-run/-/node-addon-landlock-run-0.1.1.tgz", + "integrity": "sha512-aHGhlQJEutfobKM/4K59SERbT7RmQdD2oMKzD8Bne/Ps7TeT8AweCN+dpdfuxQhMNbFcJMymrgPnID0WYQ30Tw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "@deepseek-ai/node-addon-landlock-run-linux-arm64": "0.1.1", + "@deepseek-ai/node-addon-landlock-run-linux-x64": "0.1.1" + } + }, + "node_modules/@deepseek-ai/node-addon-landlock-run-linux-arm64": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/node-addon-landlock-run-linux-arm64/-/node-addon-landlock-run-linux-arm64-0.1.1.tgz", + "integrity": "sha512-lYY2RbcPW4rGRM5hVJbrXlvLqyBxeJBjBqvt+QTHTU+GtfUVVjTODKa4e3CRwMQoCEpOjoARdQBHbN7HvE72WQ==", + "cpu": [ + "arm64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20" + } + }, + "node_modules/@deepseek-ai/node-addon-landlock-run-linux-x64": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/node-addon-landlock-run-linux-x64/-/node-addon-landlock-run-linux-x64-0.1.1.tgz", + "integrity": "sha512-OHAzPW2Coe/iYobAJAAA8CeVrBoKV4BnNHsgwvXwOfishxkUVSWSvdyxrZPiwYRXutpIGVrSo9zV3WOQy2euBA==", + "cpu": [ + "x64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20" + } + }, + "node_modules/@deepseek-ai/schemastery": { + "version": "3.18.1", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/schemastery/-/schemastery-3.18.1.tgz", + "integrity": "sha512-Qn0FCSwCQnpnj6SB31I6i2sIKgKWnkbJM8O0EU91Gv2UsYVvtZTl6IA0sCwk2e2MZf5S8w5hpq9QkeVvK9qwxg==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/cosmokit": "^1.8.2", + "@standard-schema/spec": "^1.1.0" + } + }, + "node_modules/@earendil-works/pi-ai": { + "version": "0.82.1", + "resolved": "https://registry.npmmirror.com/@earendil-works/pi-ai/-/pi-ai-0.82.1.tgz", + "integrity": "sha512-3WFYRhEp3lQB3444EhPMBcM7zSaEUE3eJgHOR7s4081NLqbw/FsWilIKWXSua0Gv3sRr7m9xMidR3pPDE7jI/A==", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.26.0", + "partial-json": "0.1.7", + "typebox": "1.1.38" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-ai/node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@hono/node-server": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/@hono/node-server/-/node-server-2.1.1.tgz", + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@joplin/turndown-plugin-gfm": { + "version": "1.0.67", + "resolved": "https://registry.npmmirror.com/@joplin/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.67.tgz", + "integrity": "sha512-FZfW5EZfidhzd1IaY1uxHnIZPTVOxAdleMZ4/1U6Nt5b7+Qj5JThDnaIomuJtetnUBzuRNbe9FWMuqD4B3dlWA==", + "license": "MIT" + }, + "node_modules/@koromix/koffi-darwin-arm64": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@koromix/koffi-darwin-arm64/-/koffi-darwin-arm64-3.1.5.tgz", + "integrity": "sha512-IpqITl2fJi3QN9bTtNnygWPdK7ScSjw3xtGu8e6feYGvimCysu+spgI5KyeslY2jTnqxGS9xr8pLAbLhGJ8edA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-darwin-x64": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@koromix/koffi-darwin-x64/-/koffi-darwin-x64-3.1.5.tgz", + "integrity": "sha512-4Tia4BS5EV/+vN9eIrdToanVe+U/2VqTZCBgOzoUbPKjgky51eqM+3J4qdRUvmYJohcJNPob5/hsxeItUZrl1g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-freebsd-arm64": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@koromix/koffi-freebsd-arm64/-/koffi-freebsd-arm64-3.1.5.tgz", + "integrity": "sha512-bP94uzseFO79NG3flpU3WxfyvltD+jzC/kN8FDZLi8J0VUZNW1Wmu8yq87KEzjX1qT9KyX4Y+elVbGqHXHTv2Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-freebsd-ia32": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@koromix/koffi-freebsd-ia32/-/koffi-freebsd-ia32-3.1.5.tgz", + "integrity": "sha512-raFXXAPHzvCQWhaoMUF+Cc2ZWgg2UBU0RVoowHZhaw9nQYPC1pERcPRH+JA+SNIN6g4d2GFW6uPFc+QbUhsagA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-freebsd-x64": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@koromix/koffi-freebsd-x64/-/koffi-freebsd-x64-3.1.5.tgz", + "integrity": "sha512-h6RyBZmPMBIDWTABkJIhzDdYwSnYAJvTacHpEjbT55Arkmw1H15Rl7CFtXuEuBrqh+uivoCrRgA6vszl9CsJ9g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-linux-arm64": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@koromix/koffi-linux-arm64/-/koffi-linux-arm64-3.1.5.tgz", + "integrity": "sha512-u0vCmKPu4yQDhl/ri1J6U3vDnvYtYjoZaIWb+oMbRXhVZeiqdE53MGPb+q2A7Dj2n9IbYloAfEICQbL6l0pmiQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-linux-ia32": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@koromix/koffi-linux-ia32/-/koffi-linux-ia32-3.1.5.tgz", + "integrity": "sha512-Xa5JbumWglwPVZgrJcLhqyC1wCWlfm7+C00p3FuOTNGp0qoYuf2/tOoAh0/q7+taVm30cMopu/6lRHwdmF6I/w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-linux-loong64": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@koromix/koffi-linux-loong64/-/koffi-linux-loong64-3.1.5.tgz", + "integrity": "sha512-F3i2CeTcqVBUQiSRUBSEzX1VgtXmLLiZb/ouZtXHkWpTLhJNd9TH7s3CizTofci0VRqlKdexGUYhK5vzPDAAHA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-linux-riscv64": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@koromix/koffi-linux-riscv64/-/koffi-linux-riscv64-3.1.5.tgz", + "integrity": "sha512-2TgQuzy+4PfDg+rw3kOmN6lywEWdzKT3eaLPbOp0b/9DaN7CLBJ/QIR5GhGsMNpeI30zU0YzFeYFxoVoJ/LqFw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-linux-x64": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@koromix/koffi-linux-x64/-/koffi-linux-x64-3.1.5.tgz", + "integrity": "sha512-2yaIg/1V0m4CiAUMzG4CIlWmq1WJ+QBMlFfaGr9su+OH5fuIqC7V3BbMiB03IwIl1VofIZO5JA4Db4lID8tpbw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-openbsd-ia32": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@koromix/koffi-openbsd-ia32/-/koffi-openbsd-ia32-3.1.5.tgz", + "integrity": "sha512-8/OXd+u9omMooykhvdJPEP7u6FFzzrrFo9gOmSHc9/DPt3XkVYOtSsE97PDk6zYaAzwIYHSKjHvIXsfFwFc7sg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-openbsd-x64": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@koromix/koffi-openbsd-x64/-/koffi-openbsd-x64-3.1.5.tgz", + "integrity": "sha512-SpeqldKkuDk2aTj5PVWumy7eq6Tr2GtBPAOI1NiDHhg8xe433KraxrA9V9UjEd+1+kSGJQGR04nQByN3MPA9PQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-win32-arm64": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@koromix/koffi-win32-arm64/-/koffi-win32-arm64-3.1.5.tgz", + "integrity": "sha512-uej3YAEKAhlfVPoIo5sOwtxhTLRVJ01LgtWrKGpnnAQU3C+Ilmaxdh+Oc2xc1G3NK30N5eCVJpyO9r3pjKC6Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-win32-ia32": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@koromix/koffi-win32-ia32/-/koffi-win32-ia32-3.1.5.tgz", + "integrity": "sha512-d42jv2f4PwtJGNJS19Xfn/BRtGsBNNVkw0O0K5tkIGI+yNq4MnPTSUsaGbDIkCLNsCgk/LFqAaE8BExyCdrujA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@koromix/koffi-win32-x64": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@koromix/koffi-win32-x64/-/koffi-win32-x64-3.1.5.tgz", + "integrity": "sha512-Pyo1WEHEP6Ek2NEn2pquwJzSPLOdY4vymoPSz0an1DgvFWSkOyBYYkGVoxL1ajfj5I1pPdXysYqixtVTzAtOfQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/@mistralai/mistralai": { + "version": "2.2.6", + "resolved": "https://registry.npmmirror.com/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@mixmark-io/domino": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/@mixmark-io/domino/-/domino-2.2.0.tgz", + "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==", + "license": "BSD-2-Clause" + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmmirror.com/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.220.0", + "resolved": "https://registry.npmmirror.com/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", + "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.220.0", + "resolved": "https://registry.npmmirror.com/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.220.0.tgz", + "integrity": "sha512-8186thl+pTw64iz/qEEen5oJZoZ/gO73XruChdaGlYdWOdBIQ42r+vHLf6a7vIDqTD4b8ZOoMlyxptanECaI9A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/sdk-logs": "0.220.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.220.0", + "resolved": "https://registry.npmmirror.com/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.220.0.tgz", + "integrity": "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-transformer": "0.220.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.220.0", + "resolved": "https://registry.npmmirror.com/@opentelemetry/otlp-transformer/-/otlp-transformer-0.220.0.tgz", + "integrity": "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-logs": "0.220.0", + "@opentelemetry/sdk-metrics": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://registry.npmmirror.com/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://registry.npmmirror.com/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.220.0", + "resolved": "https://registry.npmmirror.com/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz", + "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", + "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/@opentelemetry/sdk-trace/-/sdk-trace-2.9.0.tgz", + "integrity": "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmmirror.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@smithy/core": { + "version": "3.33.2", + "resolved": "https://registry.npmmirror.com/@smithy/core/-/core-3.33.2.tgz", + "integrity": "sha512-CUGXpnPkVdjUCbix+83sWLW9VFgQOm44MDOx/ihITJMAnOZKvL8YYIc7DR9pP/tZ8CIRvMiON/TucvygqbHO3w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.5.2", + "resolved": "https://registry.npmmirror.com/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", + "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.7.2", + "resolved": "https://registry.npmmirror.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.7.2.tgz", + "integrity": "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmmirror.com/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.7.2", + "resolved": "https://registry.npmmirror.com/@smithy/signature-v4/-/signature-v4-5.7.2.tgz", + "integrity": "sha512-P7Ki6px6OOrxVtx8K7nLmyx4SlXUW/uTKDdMG44UHefmPGSRMBKe2v+TM59WdLcpUIrBrnuCsIqiM2MbsZjmhw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.17.2", + "resolved": "https://registry.npmmirror.com/@smithy/types/-/types-4.17.2.tgz", + "integrity": "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.17.8", + "resolved": "https://registry.npmmirror.com/@tanstack/virtual-core/-/virtual-core-3.17.8.tgz", + "integrity": "sha512-BfEvehNpOT75r5Ksc5xW6NZuXujTfb7nlSEyVu4XHG3gdxNg1KqXruWbDewXOUaUYIo4oRbSfkjIajz4MAT8tA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@vscode/ripgrep": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/@vscode/ripgrep/-/ripgrep-1.18.0.tgz", + "integrity": "sha512-ns5lWe44tSfbTMbVUsyB+I1819PVSw4AdpgK0RNkzfWfwy6+3IUNSxwSrfTno1/oWaS/hERNz+XLWVyga2aJBQ==", + "license": "MIT", + "optionalDependencies": { + "@vscode/ripgrep-darwin-arm64": "1.18.0", + "@vscode/ripgrep-darwin-x64": "1.18.0", + "@vscode/ripgrep-linux-arm": "1.18.0", + "@vscode/ripgrep-linux-arm64": "1.18.0", + "@vscode/ripgrep-linux-ia32": "1.18.0", + "@vscode/ripgrep-linux-ppc64": "1.18.0", + "@vscode/ripgrep-linux-riscv64": "1.18.0", + "@vscode/ripgrep-linux-s390x": "1.18.0", + "@vscode/ripgrep-linux-x64": "1.18.0", + "@vscode/ripgrep-win32-arm64": "1.18.0", + "@vscode/ripgrep-win32-ia32": "1.18.0", + "@vscode/ripgrep-win32-x64": "1.18.0" + } + }, + "node_modules/@vscode/ripgrep-darwin-arm64": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/@vscode/ripgrep-darwin-arm64/-/ripgrep-darwin-arm64-1.18.0.tgz", + "integrity": "sha512-r3ktHSvbFycQNF6sl7sNDPocpsI7J+mEzh1IaZFkY0spm3k2Z9t8hPAeOK7+p0l6p6/swkQC14XWX01low+94Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/ripgrep-darwin-x64": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/@vscode/ripgrep-darwin-x64/-/ripgrep-darwin-x64-1.18.0.tgz", + "integrity": "sha512-25b4gWbL138dGuQU244ebCKKc0q05ULBMoFSz9oAEUHNeqK/lOJViDS7DRvbDazzAzSEdan391Znks/R5mkaTQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/ripgrep-linux-arm": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/@vscode/ripgrep-linux-arm/-/ripgrep-linux-arm-1.18.0.tgz", + "integrity": "sha512-GDAvufNDHu8zqLEmXstalQF0Wh6wQvdsBi/Vg3Yi3CK4a8XoFXqqXVEHEZ9xQz3t0NfoSEc9JbvK9DDS6FxyxQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/ripgrep-linux-arm64": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/@vscode/ripgrep-linux-arm64/-/ripgrep-linux-arm64-1.18.0.tgz", + "integrity": "sha512-lQ/5zTG++U0E3IhVgS4EPTTn/U4okncaRMM5GOFfOYZywS4nuD31GhkHbNYlDk5CuDC68+hYJ0/eQeyCKJDA+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/ripgrep-linux-ia32": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/@vscode/ripgrep-linux-ia32/-/ripgrep-linux-ia32-1.18.0.tgz", + "integrity": "sha512-YWLkSUtFd4Jh5EepIhA9RJSfv3uMAVMo+2rBIGHPBnvgLrZciIs2cDKei1/p6Wc/aCzUoHyMAg2R6tw4ZCBKGg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/ripgrep-linux-ppc64": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/@vscode/ripgrep-linux-ppc64/-/ripgrep-linux-ppc64-1.18.0.tgz", + "integrity": "sha512-quXVY8fwQ8O/lvU1yrSqSl3jlUzysRSb+AfUfCL/tRtphxsKlFvPAejryZ6vg4Bgvn8XL74xb4qMCDmWgYrT5w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/ripgrep-linux-riscv64": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/@vscode/ripgrep-linux-riscv64/-/ripgrep-linux-riscv64-1.18.0.tgz", + "integrity": "sha512-f5kBQBrWfQt8Q7OhSORuNDei5dkYagBj3y4jImSUXGMy8B/Ke7SltSRcUtjPv166FAFfHCAmWuZp3+cWnX2/Vw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/ripgrep-linux-s390x": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/@vscode/ripgrep-linux-s390x/-/ripgrep-linux-s390x-1.18.0.tgz", + "integrity": "sha512-rTOcJFGGcl2c07RUOWUo4U1ndnemKhY6A9hnMB18uk7jSgJc0d/QLBGWMWpumdtoJtpizn/wIv5mXIisJukusQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/ripgrep-linux-x64": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/@vscode/ripgrep-linux-x64/-/ripgrep-linux-x64-1.18.0.tgz", + "integrity": "sha512-mQ3bVrUpnD2vs7QT0vX90Lt0cnUq467uFtEktIdsJJmW296RoSULRGqWgzG1AKxyBpNDD6l4ZO4qKf6SgyC23Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/ripgrep-win32-arm64": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/@vscode/ripgrep-win32-arm64/-/ripgrep-win32-arm64-1.18.0.tgz", + "integrity": "sha512-vfTIjq1OHnzUjxZcHVQAMbnggp8dpGf+0QKFOZHwWPqFwXxQC8eCWM+5NUdoJ6yrElCeMzoUTXoK/LdZaniB+Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/ripgrep-win32-ia32": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/@vscode/ripgrep-win32-ia32/-/ripgrep-win32-ia32-1.18.0.tgz", + "integrity": "sha512-//rfAE+BOw5AC2EMmepmiE36jUuevtQYNQqqlw1s3m9FlRxjxEut97RkRPHAu9BG4mSojatZx+kXZXNdyI9caQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/ripgrep-win32-x64": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/@vscode/ripgrep-win32-x64/-/ripgrep-win32-x64-1.18.0.tgz", + "integrity": "sha512-KNPvtElldqILHdnAetujPaowkNbpqJy3ssIGGN6F6Kve9Qi+nNLI2DN01O83JjCEVQbCzl8Ov3QZ9Eov3BR8Dg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmmirror.com/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "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.npmmirror.com/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmmirror.com/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.6.2", + "resolved": "https://registry.npmmirror.com/express-rate-limit/-/express-rate-limit-8.6.2.tgz", + "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmmirror.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "7.3.1", + "resolved": "https://registry.npmmirror.com/gaxios/-/gaxios-7.3.1.tgz", + "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmmirror.com/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/google-auth-library": { + "version": "10.9.1", + "resolved": "https://registry.npmmirror.com/google-auth-library/-/google-auth-library-10.9.1.tgz", + "integrity": "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.13.3", + "resolved": "https://registry.npmmirror.com/hono/-/hono-4.13.3.tgz", + "integrity": "sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.5.0", + "resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.9", + "resolved": "https://registry.npmmirror.com/jose/-/jose-6.2.9.tgz", + "integrity": "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/koffi": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/koffi/-/koffi-3.1.5.tgz", + "integrity": "sha512-XVwwrxg0Ca6IEUQF4YtGIU4XN0LSselFYpYvgfhh8wafCunhEEx5hPr7LZhp5QyeFA/LcRsKHTqncCdjWjWAlg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "url": "https://liberapay.com/Koromix" + }, + "optionalDependencies": { + "@koromix/koffi-darwin-arm64": "3.1.5", + "@koromix/koffi-darwin-x64": "3.1.5", + "@koromix/koffi-freebsd-arm64": "3.1.5", + "@koromix/koffi-freebsd-ia32": "3.1.5", + "@koromix/koffi-freebsd-x64": "3.1.5", + "@koromix/koffi-linux-arm64": "3.1.5", + "@koromix/koffi-linux-ia32": "3.1.5", + "@koromix/koffi-linux-loong64": "3.1.5", + "@koromix/koffi-linux-riscv64": "3.1.5", + "@koromix/koffi-linux-x64": "3.1.5", + "@koromix/koffi-openbsd-ia32": "3.1.5", + "@koromix/koffi-openbsd-x64": "3.1.5", + "@koromix/koffi-win32-arm64": "3.1.5", + "@koromix/koffi-win32-ia32": "3.1.5", + "@koromix/koffi-win32-x64": "3.1.5" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-addon-native-custom-loader": { + "version": "0.1.5", + "resolved": "https://registry.npmmirror.com/node-addon-native-custom-loader/-/node-addon-native-custom-loader-0.1.5.tgz", + "integrity": "sha512-rL0XXRytayIChYf1xaG9/1NzGBRTyFkQTIpn+MDz/hpr0PZw1a9PZIoH9HlYfagNawhXU+jfN650EvoSIvR7Vw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-addon-require-builtin": { + "version": "0.1.5", + "resolved": "https://registry.npmmirror.com/node-addon-require-builtin/-/node-addon-require-builtin-0.1.5.tgz", + "integrity": "sha512-isaquiStlp7qdFpaffy+2ssJPUR/kLPRE5gqfeWbb0Ahph9H7O3BOFwThqkVaTrxfpnjrQauxlOEgvzD+snhUg==", + "license": "MIT", + "dependencies": { + "node-addon-native-custom-loader": "0.1.5" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "node-addon-require-builtin-darwin-arm64": "0.1.5", + "node-addon-require-builtin-darwin-x64": "0.1.5", + "node-addon-require-builtin-linux-arm64-gnu": "0.1.5", + "node-addon-require-builtin-linux-x64-gnu": "0.1.5", + "node-addon-require-builtin-win32-arm64-msvc": "0.1.5", + "node-addon-require-builtin-win32-ia32-msvc": "0.1.5", + "node-addon-require-builtin-win32-x64-msvc": "0.1.5" + } + }, + "node_modules/node-addon-require-builtin-darwin-arm64": { + "version": "0.1.5", + "resolved": "https://registry.npmmirror.com/node-addon-require-builtin-darwin-arm64/-/node-addon-require-builtin-darwin-arm64-0.1.5.tgz", + "integrity": "sha512-MZ5RyGhuU7DTbwuNpOQW81/ep8n5Yd5YlZa6dWiEfJEh+01Dl4cCKGmMCWNgb0VtqAImRtAOtmZIPtwngiemKQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "node-addon-native-custom-loader": "0.1.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/node-addon-require-builtin-darwin-x64": { + "version": "0.1.5", + "resolved": "https://registry.npmmirror.com/node-addon-require-builtin-darwin-x64/-/node-addon-require-builtin-darwin-x64-0.1.5.tgz", + "integrity": "sha512-wdCSPn49AdNZ2EXHQqswiXk1cROFe9zSRaAX8PwQLxmjdLt+xZF7vZP6VlqgXm+VAOHv98q8kp9m7mO9jY3OgQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "node-addon-native-custom-loader": "0.1.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/node-addon-require-builtin-linux-arm64-gnu": { + "version": "0.1.5", + "resolved": "https://registry.npmmirror.com/node-addon-require-builtin-linux-arm64-gnu/-/node-addon-require-builtin-linux-arm64-gnu-0.1.5.tgz", + "integrity": "sha512-hCqzKpQiknDDv6rUdZqrxCBXROg/uUruO0geUuZsdM9M/Lds45gSb4bDE958HlPaCvrDyR0wTHl3II8A7yQeew==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "dependencies": { + "node-addon-native-custom-loader": "0.1.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/node-addon-require-builtin-linux-x64-gnu": { + "version": "0.1.5", + "resolved": "https://registry.npmmirror.com/node-addon-require-builtin-linux-x64-gnu/-/node-addon-require-builtin-linux-x64-gnu-0.1.5.tgz", + "integrity": "sha512-lgP+ruwWXbcXDslhp5uIc+dFgzITEGPZ4E50WdHGt/OzcoVhN6y5z72pRRvxy8DE661//QxqmCLe72rS3SVg+w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "dependencies": { + "node-addon-native-custom-loader": "0.1.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/node-addon-require-builtin-win32-arm64-msvc": { + "version": "0.1.5", + "resolved": "https://registry.npmmirror.com/node-addon-require-builtin-win32-arm64-msvc/-/node-addon-require-builtin-win32-arm64-msvc-0.1.5.tgz", + "integrity": "sha512-KEO2eWS4lvZg6JmPPz04OdI4Ojrc7YzOs++4r6VnvwsqYN2r6mjGSGpouQkk8J2eLXBzC7X/D9wYTbUuQS8u1Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "dependencies": { + "node-addon-native-custom-loader": "0.1.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/node-addon-require-builtin-win32-ia32-msvc": { + "version": "0.1.5", + "resolved": "https://registry.npmmirror.com/node-addon-require-builtin-win32-ia32-msvc/-/node-addon-require-builtin-win32-ia32-msvc-0.1.5.tgz", + "integrity": "sha512-iSC988EqZm5XbKWp/giW6K6EBRTrHt/YQbAkKsZLglt7PplmvvrZ7C3tUsyh1wVxUTYAaxFdE2xbqSdpw0rb6g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "dependencies": { + "node-addon-native-custom-loader": "0.1.5" + }, + "engines": { + "node": ">=20 <23" + } + }, + "node_modules/node-addon-require-builtin-win32-x64-msvc": { + "version": "0.1.5", + "resolved": "https://registry.npmmirror.com/node-addon-require-builtin-win32-x64-msvc/-/node-addon-require-builtin-win32-x64-msvc-0.1.5.tgz", + "integrity": "sha512-pjuco/ESU3ChIp7WwacWfY2ENu2K5rGAgqjcXedoiFcTrjTf5Ln/miKSuNfE6IiuJ7tfMTJH6VH5hq+C1Y8JMQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "dependencies": { + "node-addon-native-custom-loader": "0.1.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-pty": { + "version": "1.2.0-beta.15", + "resolved": "https://registry.npmmirror.com/node-pty/-/node-pty-1.2.0-beta.15.tgz", + "integrity": "sha512-vORSzHXi4Ofl7HemVWpuudLqCPdaQb4LfpRCUpE5HPxhp4JYscl8zZwxh11p26v2wvW24WMwnMfLjhRLixrfxA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.1.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "11.0.1", + "resolved": "https://registry.npmmirror.com/open/-/open-11.0.1.tgz", + "integrity": "sha512-NzwMUB6C1D0+Kd+9iMS/H4k+Ck3cTX6Ckyfr/gAGlmvSE1LUQZnEZvWBi4PYmMwH/S5SMeTXnE+9uAz8uF+pWw==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.4.0", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.2.0", + "wsl-utils": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openai": { + "version": "6.26.0", + "resolved": "https://registry.npmmirror.com/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmmirror.com/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "license": "MIT" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/powershell-utils": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/powershell-utils/-/powershell-utils-0.2.0.tgz", + "integrity": "sha512-ZlsFlG7MtSFCoc5xreOvBAozCJ6Pf06opgJjh9ONEv418xpZSAzNjstD36C6+JwOnfSqOW/9uDkqKjezTdxZhw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmmirror.com/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmmirror.com/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT", + "peer": true + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.35.3", + "resolved": "https://registry.npmmirror.com/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/turndown": { + "version": "7.2.4", + "resolved": "https://registry.npmmirror.com/turndown/-/turndown-7.2.4.tgz", + "integrity": "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==", + "license": "MIT", + "dependencies": { + "@mixmark-io/domino": "^2.2.0" + }, + "engines": { + "node": ">=18", + "npm": ">=9" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmmirror.com/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-1.0.0.tgz", + "integrity": "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wsl-utils/node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "node_modules/zustand": { + "version": "4.4.7", + "resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.4.7.tgz", + "integrity": "sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..12a63b79 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "dsh-codex", + "version": "0.0.0-m1", + "private": true, + "type": "module", + "engines": { + "node": "^22.19.0 || >=24.0.0" + }, + "bin": { + "dshx": "./bin/dshx.mjs" + }, + "scripts": { + "test": "node scripts/run-unit-tests.mjs", + "test:protocol": "node --test --test-force-exit test/protocol.test.mjs test/wire.test.mjs", + "test:dsh": "node --test --test-force-exit \"test/dsh-*.test.mjs\"", + "freeze:deps": "node scripts/freeze-deps.mjs", + "verify:release-deps": "node scripts/verify-dsh-closure.mjs . package-lock.json", + "stub": "node ./bin/dshx-stub-server.mjs", + "dev": "node ./bin/dshx.mjs" + }, + "dependencies": { + "@deepseek-ai/dsh": "0.1.0-rc.8", + "@deepseek-ai/dsh-agent": "0.1.0-rc.8", + "@deepseek-ai/dsh-app-boot": "0.1.0-rc.8", + "@deepseek-ai/dsh-host-apiproxy": "0.1.0-rc.8", + "@deepseek-ai/dsh-launch-environment": "0.1.0-rc.8", + "@deepseek-ai/dsh-llm": "0.1.0-rc.8", + "@deepseek-ai/dsh-session": "0.1.0-rc.8" + }, + "devDependencies": { + "ws": "^8.18.0" + } +} diff --git a/scripts/build-codex-tui.ps1 b/scripts/build-codex-tui.ps1 new file mode 100644 index 00000000..2ea797e0 --- /dev/null +++ b/scripts/build-codex-tui.ps1 @@ -0,0 +1,22 @@ +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +$Root = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +& (Join-Path $Root 'scripts\materialize-codex.ps1') +node (Join-Path $Root 'scripts\verify-slash-contract.mjs') +$CodeXDir = if ($env:DSHX_CODEX_DIR) { $env:DSHX_CODEX_DIR } else { Join-Path $Root '.upstream\codex' } +$TargetDir = if ($env:CARGO_TARGET_DIR) { $env:CARGO_TARGET_DIR } else { Join-Path $Root '.build\codex' } +$OutDir = if ($env:DSHX_TUI_OUT_DIR) { $env:DSHX_TUI_OUT_DIR } else { Join-Path $Root 'dist\bin' } + +New-Item -ItemType Directory -Force $OutDir | Out-Null +$env:CARGO_TARGET_DIR = $TargetDir +cargo build --manifest-path (Join-Path $CodeXDir 'codex-rs\Cargo.toml') --locked --release -p codex-tui --bin codex-tui +cargo build --manifest-path (Join-Path $CodeXDir 'codex-rs\Cargo.toml') --locked --release -p codex-stdio-to-uds --bin dshx-ipc-bridge +$TuiSource = Join-Path $TargetDir 'release\codex-tui.exe' +$TuiDestination = Join-Path $OutDir 'dshx-tui.exe' +$BridgeSource = Join-Path $TargetDir 'release\dshx-ipc-bridge.exe' +$BridgeDestination = Join-Path $OutDir 'dshx-ipc-bridge.exe' +Copy-Item -Force $TuiSource $TuiDestination +Copy-Item -Force $BridgeSource $BridgeDestination +Write-Host $TuiDestination +Write-Host $BridgeDestination diff --git a/scripts/build-codex-tui.sh b/scripts/build-codex-tui.sh new file mode 100755 index 00000000..c391076e --- /dev/null +++ b/scripts/build-codex-tui.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +bash "$ROOT/scripts/materialize-codex.sh" +node "$ROOT/scripts/verify-slash-contract.mjs" +CODEX_DIR="${DSHX_CODEX_DIR:-$ROOT/.upstream/codex}" +TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT/.build/codex}" +OUT_DIR="${DSHX_TUI_OUT_DIR:-$ROOT/dist/bin}" + +mkdir -p "$OUT_DIR" +CARGO_TARGET_DIR="$TARGET_DIR" cargo build \ + --manifest-path "$CODEX_DIR/codex-rs/Cargo.toml" \ + --locked --release -p codex-tui --bin codex-tui +CARGO_TARGET_DIR="$TARGET_DIR" cargo build \ + --manifest-path "$CODEX_DIR/codex-rs/Cargo.toml" \ + --locked --release -p codex-stdio-to-uds --bin dshx-ipc-bridge + +cp "$TARGET_DIR/release/codex-tui" "$OUT_DIR/dshx-tui" +cp "$TARGET_DIR/release/dshx-ipc-bridge" "$OUT_DIR/dshx-ipc-bridge" +chmod +x "$OUT_DIR/dshx-tui" "$OUT_DIR/dshx-ipc-bridge" +printf '%s\n' "$OUT_DIR/dshx-tui" "$OUT_DIR/dshx-ipc-bridge" diff --git a/scripts/freeze-deps.mjs b/scripts/freeze-deps.mjs new file mode 100644 index 00000000..b386ccc6 --- /dev/null +++ b/scripts/freeze-deps.mjs @@ -0,0 +1,62 @@ +import { spawnSync } from 'node:child_process'; +import { assertReleaseNodeVersion } from '../src/cli/runtime.mjs'; + +function run(command, args) { + const result = spawnSync(command, args, { + stdio: 'inherit', + shell: false, + }); + if (result.error) throw result.error; + if (result.status !== 0) process.exit(result.status ?? 1); +} + +function capture(command, args) { + const result = spawnSync(command, args, { + encoding: 'utf8', + shell: false, + }); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error(`${command} ${args.join(' ')} failed with exit ${result.status}: ${result.stderr?.trim() ?? ''}`); + } + return result.stdout.trim(); +} + +function npmInvocation(args) { + // `npm run` provides the exact npm CLI entrypoint in npm_execpath. Execute + // that JavaScript file through the current Node process instead of spawning + // npm.cmd directly: Node 24 on Windows rejects direct .cmd execution with + // shell:false (EINVAL), while this path is shell-free and identical across + // Windows, Linux, and macOS. + const npmCli = process.env.npm_execpath; + if (!npmCli) { + throw new Error('DSHX dependency freeze must be started with `npm run freeze:deps` so the exact npm CLI can be reused'); + } + return [process.execPath, [npmCli, ...args]]; +} + +assertReleaseNodeVersion(); +const [npmCommand, npmVersionArgs] = npmInvocation(['--version']); +const npmVersion = capture(npmCommand, npmVersionArgs); +const npmMajor = Number(npmVersion.split('.')[0]); +if (npmMajor !== 11) { + throw new Error(`DSHX dependency freeze baseline requires npm 11.x with Node 24; got npm ${npmVersion}`); +} + +console.log(`Freezing DSHX dependencies with Node ${process.version} / npm ${npmVersion}`); +const [installCommand, installArgs] = npmInvocation([ + 'install', + '--package-lock-only', + '--ignore-scripts', + '--no-audit', + '--no-fund', +]); +run(installCommand, installArgs); + +run(process.execPath, [ + 'scripts/verify-dsh-closure.mjs', + '.', + 'package-lock.json', +]); + +console.log('Frozen npm dependency graph is ready in package-lock.json'); diff --git a/scripts/materialize-codex.ps1 b/scripts/materialize-codex.ps1 new file mode 100644 index 00000000..2add41fb --- /dev/null +++ b/scripts/materialize-codex.ps1 @@ -0,0 +1,30 @@ +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +$Root = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +$Pin = (Get-Content (Join-Path $Root 'upstream/CODEX_COMMIT') -Raw).Trim() +$Dest = if ($env:DSHX_CODEX_DIR) { $env:DSHX_CODEX_DIR } else { Join-Path $Root '.upstream\codex' } +$Repo = if ($env:DSHX_CODEX_REPO) { $env:DSHX_CODEX_REPO } else { 'https://github.com/openai/codex.git' } + +if (Test-Path (Join-Path $Dest '.git')) { + git -C $Dest fetch --quiet origin $Pin +} else { + if (Test-Path $Dest) { Remove-Item -Recurse -Force $Dest } + New-Item -ItemType Directory -Force (Split-Path -Parent $Dest) | Out-Null + git clone --filter=blob:none --no-checkout $Repo $Dest + git -C $Dest fetch --quiet origin $Pin +} + +git -C $Dest checkout --detach --force $Pin +git -C $Dest reset --hard $Pin | Out-Null +git -C $Dest clean -ffd | Out-Null + +$Patches = Get-ChildItem (Join-Path $Root 'upstream\patches\codex\*.patch') -ErrorAction SilentlyContinue | Sort-Object Name +foreach ($Patch in $Patches) { + # Recount stale hand-maintained hunk line counts, but still require every + # context line to match the pinned Codex commit before applying the patch. + git -C $Dest apply --recount --check $Patch.FullName + git -C $Dest apply --recount $Patch.FullName +} + +Write-Host "Materialized Codex $Pin at $Dest" diff --git a/scripts/materialize-codex.sh b/scripts/materialize-codex.sh new file mode 100644 index 00000000..3a1cde68 --- /dev/null +++ b/scripts/materialize-codex.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PIN="$(tr -d '\r\n' < "$ROOT/upstream/CODEX_COMMIT")" +DEST="${DSHX_CODEX_DIR:-$ROOT/.upstream/codex}" +REPO="${DSHX_CODEX_REPO:-https://github.com/openai/codex.git}" + +if [[ -d "$DEST/.git" ]]; then + git -C "$DEST" fetch --quiet origin "$PIN" +else + rm -rf "$DEST" + mkdir -p "$(dirname "$DEST")" + git clone --filter=blob:none --no-checkout "$REPO" "$DEST" + git -C "$DEST" fetch --quiet origin "$PIN" +fi + +git -C "$DEST" checkout --detach --force "$PIN" +git -C "$DEST" reset --hard "$PIN" >/dev/null +git -C "$DEST" clean -ffd >/dev/null + +for patch in "$ROOT"/upstream/patches/codex/*.patch; do + [[ -e "$patch" ]] || continue + # The maintained thin-fork patches are intentionally hand-readable and may + # have stale hunk line counts after upstream rebases. Recount from the hunk + # bodies while still requiring every context line to match the pinned Codex. + git -C "$DEST" apply --recount --check "$patch" + git -C "$DEST" apply --recount "$patch" +done + +printf '%s\n' "Materialized Codex $PIN at $DEST" diff --git a/scripts/package-platform.mjs b/scripts/package-platform.mjs new file mode 100644 index 00000000..d7e042b3 --- /dev/null +++ b/scripts/package-platform.mjs @@ -0,0 +1,216 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { createHash } from 'node:crypto'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const rootPackage = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); +const sourceLockPath = path.join(root, 'package-lock.json'); +if (!fs.existsSync(sourceLockPath)) { + throw new Error('Missing frozen package-lock.json; freeze the RC dependency graph before packaging'); +} +const sourceLockBytes = fs.readFileSync(sourceLockPath); +const sourceLockSha256 = createHash('sha256').update(sourceLockBytes).digest('hex'); +const sourceLock = JSON.parse(sourceLockBytes.toString('utf8')); +const sourceLockRoot = sourceLock.packages?.['']; +if (!sourceLockRoot) throw new Error('Frozen package-lock.json has no root package record'); + +const version = process.env.DSHX_VERSION || rootPackage.version; +const platform = process.platform; +const arch = process.arch; +const tuiExe = platform === 'win32' ? 'dshx-tui.exe' : 'dshx-tui'; +const bridgeExe = platform === 'win32' ? 'dshx-ipc-bridge.exe' : 'dshx-ipc-bridge'; +const tuiSource = path.join(root, 'dist', 'bin', tuiExe); +const bridgeSource = path.join(root, 'dist', 'bin', bridgeExe); +if (!fs.existsSync(tuiSource)) { + throw new Error(`Missing built TUI at ${tuiSource}; build the pinned Codex TUI first`); +} +if (!fs.existsSync(bridgeSource)) { + throw new Error(`Missing built IPC bridge at ${bridgeSource}; build the pinned Codex transport bridge first`); +} + +assertDependencyMap('dependencies', sourceLockRoot.dependencies, rootPackage.dependencies); +assertDependencyMap('devDependencies', sourceLockRoot.devDependencies, rootPackage.devDependencies); + +const releaseRoot = path.join(root, '.release'); +const stage = path.join(releaseRoot, `dshx-${platform}-${arch}`); +const out = path.join(root, 'dist', 'release'); +fs.rmSync(stage, { recursive: true, force: true }); +fs.mkdirSync(stage, { recursive: true }); +fs.mkdirSync(out, { recursive: true }); + +function copy(relative) { + const from = path.join(root, relative); + const to = path.join(stage, relative); + if (!fs.existsSync(from)) throw new Error(`Release input is missing: ${relative}`); + fs.mkdirSync(path.dirname(to), { recursive: true }); + fs.cpSync(from, to, { recursive: true }); +} + +// Ship only the production presentation closure. Early TCP/WebSocket protocol +// stubs remain source-tree development fixtures and are deliberately excluded +// from installable artifacts. +copy('bin/dshx.mjs'); +copy('src/cli'); +copy('src/dsh'); +copy('src/protocol'); +copy('README.md'); +copy('NOTICE'); +copy('upstream/CODEX_COMMIT'); +copy('upstream/DSH_COMMIT'); +copy('upstream/patches/codex'); + +fs.mkdirSync(path.join(stage, 'dist', 'bin'), { recursive: true }); +fs.copyFileSync(tuiSource, path.join(stage, 'dist', 'bin', tuiExe)); +fs.copyFileSync(bridgeSource, path.join(stage, 'dist', 'bin', bridgeExe)); +if (platform !== 'win32') { + fs.chmodSync(path.join(stage, 'dist', 'bin', tuiExe), 0o755); + fs.chmodSync(path.join(stage, 'dist', 'bin', bridgeExe), 0o755); +} + +const codexLicense = path.join(root, '.upstream', 'codex', 'LICENSE'); +if (!fs.existsSync(codexLicense)) { + throw new Error('Pinned Codex LICENSE is missing; materialize Codex before packaging'); +} +fs.copyFileSync(codexLicense, path.join(stage, 'LICENSE')); + +const packageJson = { + name: 'dsh-codex', + version, + description: 'Codex-grade TUI presentation for official DeepSeek Harness', + type: 'module', + license: 'Apache-2.0', + os: [platform], + cpu: [arch], + engines: rootPackage.engines, + bin: { dshx: './bin/dshx.mjs' }, + dependencies: rootPackage.dependencies, + repository: { type: 'git', url: 'https://github.com/code-new-bie/dsh-codex.git' }, + files: [ + 'bin/dshx.mjs', + 'src/cli', + 'src/dsh', + 'src/protocol', + 'dist/bin', + 'upstream', + 'README.md', + 'LICENSE', + 'NOTICE', + 'npm-shrinkwrap.json' + ] +}; +fs.writeFileSync(path.join(stage, 'package.json'), `${JSON.stringify(packageJson, null, 2)}\n`); + +// Every relative import in the release closure must resolve inside the staged +// package. Cover static `from`, side-effect `import`, and dynamic `import()` so +// a source-tree-only helper cannot hide behind a lazy code path and leak into +// 1.0 packaging. +const localImportPattern = /(?:from\s+|import\s*(?:\(\s*)?)["'](\.{1,2}\/[^"']+)["']/g; +for (const file of walkJs(stage)) { + const source = fs.readFileSync(file, 'utf8'); + for (const match of source.matchAll(localImportPattern)) { + const target = resolveLocalImport(path.dirname(file), match[1]); + if (!target) throw new Error(`Release package has unresolved local import ${match[1]} from ${path.relative(stage, file)}`); + } +} + +// npm-shrinkwrap.json is the publishable form of package-lock.json. Derive the +// artifact lock from the trusted source lock so packaging cannot silently +// resolve a different transitive graph. The source lock may contain dev-only +// nodes for repository tests; the publishable root deliberately exposes only +// production dependencies, so those nodes are not reachable from the shipped +// package even though their resolved records remain frozen in the lockfile. +const shrinkwrap = structuredClone(sourceLock); +shrinkwrap.name = packageJson.name; +shrinkwrap.version = packageJson.version; +const shrinkwrapRoot = shrinkwrap.packages['']; +shrinkwrapRoot.name = packageJson.name; +shrinkwrapRoot.version = packageJson.version; +shrinkwrapRoot.license = packageJson.license; +shrinkwrapRoot.os = packageJson.os; +shrinkwrapRoot.cpu = packageJson.cpu; +shrinkwrapRoot.engines = packageJson.engines; +shrinkwrapRoot.bin = packageJson.bin; +shrinkwrapRoot.dependencies = packageJson.dependencies; +delete shrinkwrapRoot.devDependencies; +const shrinkwrapPath = path.join(stage, 'npm-shrinkwrap.json'); +fs.writeFileSync(shrinkwrapPath, `${JSON.stringify(shrinkwrap, null, 2)}\n`); + +execFileSync(process.execPath, [ + path.join(root, 'scripts', 'verify-dsh-closure.mjs'), + stage, + shrinkwrapPath +], { cwd: root, stdio: 'inherit' }); + +const packArgs = ['pack', stage, '--pack-destination', out, '--json']; +let npmExecutable = 'npm'; +let npmArgs = packArgs; +if (platform === 'win32') { + // Node 24 no longer reliably spawns .cmd launchers through execFileSync. + // setup-node ships npm's JavaScript CLI beside node.exe, so execute that + // directly with the exact Node runtime used by the release job. + const npmCli = path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js'); + if (!fs.existsSync(npmCli)) { + throw new Error(`Bundled npm CLI is missing: ${npmCli}`); + } + npmExecutable = process.execPath; + npmArgs = [npmCli, ...packArgs]; +} +const packOutput = execFileSync( + npmExecutable, + npmArgs, + { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'] } +); +const packed = JSON.parse(packOutput); +const filename = packed[0]?.filename; +if (!filename) throw new Error(`npm pack did not return a filename: ${packOutput}`); +const sourceTarball = path.join(out, filename); +const targetTarball = path.join(out, `dshx-${version}-${platform}-${arch}.tgz`); +if (path.resolve(sourceTarball) !== path.resolve(targetTarball)) { + fs.rmSync(targetTarball, { force: true }); + fs.renameSync(sourceTarball, targetTarball); +} + +// Keep metadata deterministic for a given source/version/platform. Wall clock +// and builder hostname belong in CI provenance, not the distributable sidecar. +const metadata = { + version, + platform, + arch, + node: process.version, + dshVersion: rootPackage.dependencies['@deepseek-ai/dsh'], + codexCommit: fs.readFileSync(path.join(root, 'upstream', 'CODEX_COMMIT'), 'utf8').trim(), + dshCommit: fs.readFileSync(path.join(root, 'upstream', 'DSH_COMMIT'), 'utf8').trim(), + transport: 'local-uds-via-stdio-bridge', + dependencyGraph: 'source-package-lock', + sourceLockSha256, + tarball: path.basename(targetTarball) +}; +fs.writeFileSync(`${targetTarball}.json`, `${JSON.stringify(metadata, null, 2)}\n`); +process.stdout.write(`${targetTarball}\n`); + +function assertDependencyMap(label, actual = {}, expected = {}) { + const actualEntries = Object.entries(actual).sort(([a], [b]) => a.localeCompare(b)); + const expectedEntries = Object.entries(expected).sort(([a], [b]) => a.localeCompare(b)); + if (JSON.stringify(actualEntries) !== JSON.stringify(expectedEntries)) { + throw new Error(`Frozen package-lock.json ${label} does not match package.json`); + } +} + +function* walkJs(directory) { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === 'upstream') continue; + const absolute = path.join(directory, entry.name); + if (entry.isDirectory()) yield* walkJs(absolute); + else if (entry.isFile() && /\.(?:mjs|js)$/.test(entry.name)) yield absolute; + } +} + +function resolveLocalImport(directory, specifier) { + const base = path.resolve(directory, specifier); + const candidates = [base, `${base}.mjs`, `${base}.js`, path.join(base, 'index.mjs'), path.join(base, 'index.js')]; + return candidates.find((candidate) => fs.existsSync(candidate)); +} diff --git a/scripts/run-unit-tests.mjs b/scripts/run-unit-tests.mjs new file mode 100644 index 00000000..eddede03 --- /dev/null +++ b/scripts/run-unit-tests.mjs @@ -0,0 +1,38 @@ +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const TEST_DIR = path.join(ROOT, 'test'); +const TIMEOUT_MS = 30_000; + +const files = fs.readdirSync(TEST_DIR, { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name.endsWith('.test.mjs')) + .map((entry) => path.join('test', entry.name)) + .sort(); + +if (files.length === 0) throw new Error('No unit test files found'); + +for (const file of files) { + process.stdout.write(`\n==> ${file}\n`); + const result = spawnSync( + process.execPath, + ['--test', '--test-force-exit', file], + { + cwd: ROOT, + stdio: 'inherit', + timeout: TIMEOUT_MS, + killSignal: 'SIGTERM' + } + ); + + if (result.error?.code === 'ETIMEDOUT') { + console.error(`Unit test file exceeded ${TIMEOUT_MS / 1000}s: ${file}`); + process.exit(124); + } + if (result.error) throw result.error; + if (result.status !== 0) process.exit(result.status ?? 1); +} + +console.log(`\nPassed ${files.length} unit/contract test files.`); diff --git a/scripts/runtime-smoke.mjs b/scripts/runtime-smoke.mjs new file mode 100644 index 00000000..0c1e3f1c --- /dev/null +++ b/scripts/runtime-smoke.mjs @@ -0,0 +1,71 @@ +#!/usr/bin/env node +import process from 'node:process'; +import { bootDshxRuntime } from '../src/dsh/runtime-boot.mjs'; + +const REQUIRED_SERVICES = [ + 'agents', + 'agentDefaultModel', + 'llm', + 'sessions', + 'sessionPersistence', + 'sessionQuery', + 'sessionProjections', + 'sessionTitle', + 'attachments', + 'tools', + 'commands', + 'compaction', + 'subagents', + 'permissionPresets', + 'approval', + 'userQuestions', + 'skills', + 'planMode' +]; + +function withTimeout(promise, timeoutMs, label) { + let timer; + return Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`${label} exceeded ${timeoutMs}ms`)), timeoutMs); + }) + ]).finally(() => clearTimeout(timer)); +} + +let ctx; +let primaryError; +try { + process.stdout.write('booting official DSH composition with production patch watchers enabled\n'); + ctx = await withTimeout( + bootDshxRuntime({ cwd: process.cwd(), watch: true }), + 20_000, + 'official DSH composition boot' + ); + process.stdout.write('official DSH composition boot completed\n'); + + const missing = REQUIRED_SERVICES.filter((name) => ctx.get(name) == null); + if (missing.length > 0) { + throw new Error(`official DSH composition is missing required presentation services: ${missing.join(', ')}`); + } + // Workspace command probes are Agent-scoped and use the official `tools` + // service. `workspaceRegistry` was an older composition detail and is not a + // DSHX product dependency. Command discovery is likewise Agent-scoped in + // DSH rc.8, so do not invent a fake Agent merely to inspect /compact here. + process.stdout.write(`official DSH runtime booted: ${REQUIRED_SERVICES.join(', ')}\n`); +} catch (error) { + primaryError = error; +} finally { + if (ctx) { + process.stdout.write('disposing official DSH composition\n'); + try { + await withTimeout(Promise.resolve(ctx.dispose?.()), 10_000, 'official DSH composition disposal'); + process.stdout.write('official DSH composition disposed\n'); + } catch (disposeError) { + if (!primaryError) primaryError = disposeError; + else process.stderr.write(`secondary DSH disposal failure: ${disposeError instanceof Error ? disposeError.stack : disposeError}\n`); + } + } +} + +if (primaryError) throw primaryError; diff --git a/scripts/verify-dsh-closure.mjs b/scripts/verify-dsh-closure.mjs new file mode 100644 index 00000000..02458937 --- /dev/null +++ b/scripts/verify-dsh-closure.mjs @@ -0,0 +1,44 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; + +const root = path.resolve(process.argv[2] ?? process.cwd()); +const packagePath = path.join(root, 'package.json'); +const lockPath = process.argv[3] + ? path.resolve(process.argv[3]) + : path.join(root, fs.existsSync(path.join(root, 'npm-shrinkwrap.json')) ? 'npm-shrinkwrap.json' : 'package-lock.json'); + +if (!fs.existsSync(packagePath)) throw new Error(`Missing package.json at ${packagePath}`); +if (!fs.existsSync(lockPath)) throw new Error(`Missing npm lockfile at ${lockPath}`); + +const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); +const expected = pkg.dependencies?.['@deepseek-ai/dsh']; +if (typeof expected !== 'string' || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(expected)) { + throw new Error('DSHX requires an exact @deepseek-ai/dsh dependency version'); +} + +const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); +const packages = lock.packages ?? {}; +const mismatches = []; +let count = 0; +for (const [location, record] of Object.entries(packages)) { + // package-lock `packages` keys identify the package by the final + // node_modules segment. Match only the actual @deepseek-ai/dsh* record, + // never an arbitrary transitive dependency nested below a DSH package. + // Example that must NOT match: + // node_modules/@deepseek-ai/dsh-client-ui-trajectory/node_modules/react + if (!/(?:^|\/)node_modules\/@deepseek-ai\/dsh(?:-[^/]+)?$/.test(location)) continue; + if (!record || typeof record.version !== 'string') continue; + count += 1; + if (record.version !== expected) { + mismatches.push(`${location}: ${record.version}`); + } +} +if (count === 0) throw new Error('npm lockfile contains no @deepseek-ai/dsh* packages'); +if (mismatches.length > 0) { + throw new Error( + `DSHX refuses a mixed DeepSeek Harness release closure; expected every @deepseek-ai/dsh* package at ${expected}:\n${mismatches.join('\n')}` + ); +} +process.stdout.write(`Verified ${count} DeepSeek Harness packages at ${expected}\n`); diff --git a/scripts/verify-ownership-boundary.mjs b/scripts/verify-ownership-boundary.mjs new file mode 100644 index 00000000..b6b0c11a --- /dev/null +++ b/scripts/verify-ownership-boundary.mjs @@ -0,0 +1,65 @@ +import { readFileSync } from 'node:fs'; + +function read(path) { + return readFileSync(path, 'utf8'); +} + +function requireText(path, needle) { + const text = read(path); + if (!text.includes(needle)) { + throw new Error(`${path} is missing required invariant: ${needle}`); + } +} + +function forbidText(path, pattern, label = String(pattern)) { + const text = read(path); + const hit = pattern instanceof RegExp ? pattern.test(text) : text.includes(pattern); + if (hit) { + throw new Error(`${path} violates ownership invariant: ${label}`); + } +} + +requireText('docs/ARCHITECTURE.md', 'must not grow into an agent framework'); +requireText('docs/TEAM.md', 'DeepSeek Harness remains an upstream dependency'); + +// Runtime composition must stay on the official DSH app-boot/profile plane. +// Do not key this guard to historical bundle row ids such as `dsh-base`: +// those are upstream implementation details, not the ownership boundary. +requireText('src/dsh/runtime-boot.mjs', "from '@deepseek-ai/dsh-app-boot'"); +requireText('src/dsh/runtime-boot.mjs', "const DEFAULT_PROFILE = 'headless'"); +requireText('src/dsh/runtime-boot.mjs', 'loadProfile('); +requireText('src/dsh/runtime-boot.mjs', 'composeEntries('); +requireText('src/dsh/runtime-boot.mjs', "{ id: 'headless-startup', disabled: true }"); +requireText('src/dsh/runtime-boot.mjs', "{ id: 'headless-runner', disabled: true }"); +requireText('src/dsh/runtime-boot.mjs', 'ctx = await boot('); +requireText('src/dsh/runtime-boot.mjs', 'profile.layers.flatMap((layer) => layer.patches)'); +requireText('src/dsh/runtime-boot.mjs', 'profile.patches'); +requireText('src/dsh/runtime-boot.mjs', 'homePatches'); +// Presentation lifetime may adapt the Context shape, but teardown must remain +// delegated to Cordis/DSH root-fiber ownership rather than a DSHX runtime. +requireText('src/dsh/runtime-boot.mjs', 'ctx.fiber.dispose()'); +requireText('src/dsh/runtime-boot.mjs', 'attachPresentationLifetime(ctx)'); + +for (const path of ['src/dsh/user-shell.mjs', 'src/dsh/workspace-command.mjs']) { + forbidText(path, /execFile|execSync|spawn\(/, 'local process execution'); + requireText(path, 'tools.execute'); +} +requireText('src/dsh/user-shell.mjs', 'agent: controller.agent'); +requireText('src/dsh/workspace-command.mjs', 'agent'); +requireText('src/dsh/release-adapter.mjs', "case 'command/exec'"); + +requireText('src/dsh/host-api.mjs', '@deepseek-ai/dsh-host-apiproxy'); +requireText('src/dsh/host-api.mjs', 'api.sessions.fork'); +requireText('src/dsh/product-adapter.mjs', 'executeDshCommand'); +requireText('src/dsh/commands.mjs', 'commands.execute'); +forbidText('src/dsh/product-adapter.mjs', 'compactNow'); +for (const path of ['src/dsh/agent-driver.mjs', 'src/dsh/product-adapter.mjs']) { + forbidText(path, /seedLength|parentSession|dshForkSeed/, 'shadow fork/session seed state'); +} +forbidText('src/dsh/agent-driver.mjs', 'fork(sourceAgent'); + +forbidText('src/dsh/local-server.mjs', 'WebSocketServer', 'production TCP WebSocket server'); +requireText('src/dsh/local-server.mjs', 'unix://'); +requireText('scripts/build-codex-tui.sh', 'dshx-ipc-bridge'); + +console.log('DSHX ownership boundary verified'); diff --git a/scripts/verify-slash-contract.mjs b/scripts/verify-slash-contract.mjs new file mode 100644 index 00000000..e299ce81 --- /dev/null +++ b/scripts/verify-slash-contract.mjs @@ -0,0 +1,91 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const SLASH = path.join(ROOT, '.upstream', 'codex', 'codex-rs', 'tui', 'src', 'slash_command.rs'); + +if (!fs.existsSync(SLASH)) { + throw new Error(`materialized pinned Codex slash_command.rs is missing: ${SLASH}`); +} + +const source = fs.readFileSync(SLASH, 'utf8'); +const enumMatch = source.match(/pub enum SlashCommand\s*\{([\s\S]*?)\n\}/); +if (!enumMatch) throw new Error('cannot parse pinned Codex SlashCommand enum'); +const variants = new Set( + [...enumMatch[1].matchAll(/^\s*([A-Z][A-Za-z0-9_]*)\s*,\s*$/gm)].map((match) => match[1]) +); + +const visibilityMatch = source.match( + /if crate::dshx_backend\(\)[\s\S]*?matches!\([\s\S]*?self,([\s\S]*?)\)\s*\{\s*return false;/ +); +if (!visibilityMatch) throw new Error('cannot find DSHX slash visibility guard in patched Codex'); +const dshxHidden = new Set( + [...visibilityMatch[1].matchAll(/SlashCommand::([A-Z][A-Za-z0-9_]*)/g)].map((match) => match[1]) +); + +// Commands whose behavior is entirely presentation-local. Their state lives in +// the isolated DSHX Codex home or the terminal process, never in a shadow Agent runtime. +const presentationOnly = new Set([ + 'Keymap', 'Vim', 'Copy', 'Export', 'Raw', 'Mention', 'Status', 'Pwd', + 'Title', 'Statusline', 'Theme', 'Pets', 'Quit', 'Exit' +]); + +// Release-only upstream visibility guards keep these out of normal builds. +const upstreamDebugOnly = new Set(['Rollout', 'TestApproval']); + +// Visible runtime-owned commands must have a faithful DSH-backed app-server surface. +// The method strings are also asserted to exist in the adapter sources below. +const runtimeBacked = new Map([ + ['Model', ['thread/settings/update', 'model/list']], + ['Permissions', ['thread/settings/update']], + ['Skills', ['skills/list']], + ['Rename', ['thread/name/set']], + ['New', ['thread/start']], + ['Clear', ['thread/start']], + ['Resume', ['thread/list', 'thread/resume']], + ['Fork', ['thread/fork']], + ['Init', ['turn/start']], + ['Compact', ['thread/compact/start']], + ['Plan', ['thread/settings/update']], + ['Agents', ['thread/loaded/list']], + ['Diff', ['command/exec']] +]); + +const classified = new Set([ + ...dshxHidden, + ...presentationOnly, + ...upstreamDebugOnly, + ...runtimeBacked.keys() +]); +const unclassified = [...variants].filter((variant) => !classified.has(variant)).sort(); +if (unclassified.length > 0) { + throw new Error( + `Pinned Codex introduced visible/unclassified slash commands: ${unclassified.join(', ')}. ` + + 'Classify each as DSH-backed, presentation-only, upstream-debug-only, or hide it explicitly for DSHX.' + ); +} + +for (const hidden of dshxHidden) { + if (!variants.has(hidden)) throw new Error(`DSHX hides unknown SlashCommand variant ${hidden}`); +} + +const adapterSources = [ + 'src/dsh/app-server-adapter.mjs', + 'src/dsh/product-adapter.mjs', + 'src/dsh/release-adapter.mjs' +].map((relative) => fs.readFileSync(path.join(ROOT, relative), 'utf8')).join('\n'); +for (const [command, methods] of runtimeBacked) { + for (const method of methods) { + if (!adapterSources.includes(`'${method}'`) && !adapterSources.includes(`\"${method}\"`)) { + throw new Error(`/${command} is visible in DSHX but adapter source does not claim ${method}`); + } + } +} + +process.stdout.write( + `DSHX slash contract OK: ${variants.size} pinned commands; ` + + `${dshxHidden.size} hidden, ${runtimeBacked.size} DSH-backed, ` + + `${presentationOnly.size} presentation-only, ${upstreamDebugOnly.size} debug-only.\n` +); diff --git a/scripts/verify-tui-invariants.mjs b/scripts/verify-tui-invariants.mjs new file mode 100644 index 00000000..2a578f29 --- /dev/null +++ b/scripts/verify-tui-invariants.mjs @@ -0,0 +1,69 @@ +import { readFileSync } from 'node:fs'; + +function read(path) { + return readFileSync(path, 'utf8'); +} + +function requireText(path, needle) { + const text = read(path); + if (!text.includes(needle)) { + throw new Error(`${path} is missing required invariant: ${needle}`); + } +} + +function forbidText(path, needle, label = needle) { + const text = read(path); + if (text.includes(needle)) { + throw new Error(`${path} violates TUI invariant: ${label}`); + } +} + +const main = '.upstream/codex/codex-rs/tui/src/main.rs'; +const lib = '.upstream/codex/codex-rs/tui/src/lib.rs'; +const app = '.upstream/codex/codex-rs/tui/src/app.rs'; +const session = '.upstream/codex/codex-rs/tui/src/app_server_session.rs'; +const slash = '.upstream/codex/codex-rs/tui/src/slash_command.rs'; +const startup = '.upstream/codex/codex-rs/tui/src/app/startup.rs'; +const event = '.upstream/codex/codex-rs/tui/src/app/event_dispatch.rs'; +const bridge = '.upstream/codex/codex-rs/stdio-to-uds/src/dshx_ipc_bridge.rs'; + +for (const needle of [ + 'DSHX_APP_SERVER_ENDPOINT is required', + 'DSHX production launcher forbids TCP WebSocket transport', + 'RemoteAppServerEndpoint::UnixSocket', + 'Some(dshx_endpoint)', + 'DSHX_RESUME_MODE', +]) requireText(main, needle); +forbidText(main, 'DSHX_APP_SERVER_TOKEN', 'legacy TCP bearer-token transport'); + +for (const needle of ['UnixListener', 'UnixStream', 'dshxBridge']) requireText(bridge, needle); +requireText('.upstream/codex/codex-rs/tui/src/history_cell/session.rs', 'DeepSeek Harness'); +requireText('.upstream/codex/codex-rs/tui/src/bottom_pane/approval_overlay.rs', 'fn dshx_backend'); +requireText('.upstream/codex/codex-rs/tui/src/chatwidget/permission_popups.rs', 'fn dshx_backend'); +requireText(lib, 'fn dshx_backend'); +requireText(lib, '!dshx_backend()'); +requireText(startup, '!crate::dshx_backend()'); +requireText(event, '!crate::dshx_backend()'); +requireText(session, 'ThreadParamsMode::Remote'); +requireText(app, 'dshx'); +requireText(slash, 'if crate::dshx_backend()'); +requireText(slash, 'DeepSeek Harness'); + +forbidText('src/dsh/local-server.mjs', 'WebSocketServer', 'production TCP WebSocket server'); +requireText('src/dsh/local-server.mjs', 'unix://'); +requireText('scripts/package-platform.mjs', 'local-uds-via-stdio-bridge'); + +const pkg = JSON.parse(read('package.json')); +if (pkg.dependencies?.ws) throw new Error('ws must not be a production dependency'); +if (!pkg.devDependencies?.ws) throw new Error('development protocol stub still requires ws'); + +const slashText = read(slash); +const hiddenStart = slashText.indexOf('if crate::dshx_backend()'); +const hiddenEnd = slashText.indexOf('return false;', hiddenStart); +if (hiddenStart < 0 || hiddenEnd < 0) throw new Error('DSHX slash hidden block not found'); +const hidden = slashText.slice(hiddenStart, hiddenEnd); +if (hidden.includes('SlashCommand::Diff')) { + throw new Error('/diff must remain visible in DSHX now that command/exec is DSH-backed'); +} + +console.log('DSHX pinned TUI invariants verified'); diff --git a/scripts/windows-rc-acceptance.ps1 b/scripts/windows-rc-acceptance.ps1 new file mode 100644 index 00000000..c76ceaad --- /dev/null +++ b/scripts/windows-rc-acceptance.ps1 @@ -0,0 +1,60 @@ +param( + [Parameter(Mandatory = $true)] + [string]$Tarball, + + [Parameter(Mandatory = $true)] + [string]$ExpectedSha256, + + [string]$Prefix = (Join-Path $env:TEMP 'dshx-rc-acceptance') +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +$artifact = (Resolve-Path -LiteralPath $Tarball).Path +$expected = $ExpectedSha256.Trim().ToLowerInvariant() +if ($expected -notmatch '^[0-9a-f]{64}$') { + throw 'ExpectedSha256 must be exactly 64 hexadecimal characters.' +} + +$actual = (Get-FileHash -LiteralPath $artifact -Algorithm SHA256).Hash.ToLowerInvariant() +if ($actual -ne $expected) { + throw "RC artifact SHA-256 mismatch. expected=$expected actual=$actual" +} + +if (Test-Path -LiteralPath $Prefix) { + Remove-Item -LiteralPath $Prefix -Recurse -Force +} +New-Item -ItemType Directory -Path $Prefix -Force | Out-Null + +npm install --global --prefix $Prefix $artifact +if ($LASTEXITCODE -ne 0) { + throw "npm failed to install RC artifact $artifact" +} + +$dshx = Join-Path $Prefix 'dshx.cmd' +if (-not (Test-Path -LiteralPath $dshx)) { + throw "installed dshx launcher not found: $dshx" +} + +$version = (& $dshx --version | Out-String).Trim() +if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { + throw 'installed dshx --version failed' +} + +& $dshx doctor +if ($LASTEXITCODE -ne 0) { + throw 'installed dshx doctor failed' +} + +Write-Host '' +Write-Host 'DSHX RC Windows acceptance setup passed.' +Write-Host "Artifact: $artifact" +Write-Host "SHA-256: $actual" +Write-Host "Version: $version" +Write-Host "Launcher: $dshx" +Write-Host '' +Write-Host 'Use this exact launcher for issue #12. In the project directory, run:' +Write-Host " & '$dshx'" +Write-Host '' +Write-Host 'Do not replace the generated artifact with a source checkout during IME/visual acceptance.' diff --git a/src/cli/arguments.mjs b/src/cli/arguments.mjs new file mode 100644 index 00000000..811ae5d0 --- /dev/null +++ b/src/cli/arguments.mjs @@ -0,0 +1,29 @@ +export function parseLaunchArgs(args) { + if (args[0] !== 'resume') return { tuiArgs: args, resumeEnv: {} }; + const rest = args.slice(1); + if (rest.length === 0) return { tuiArgs: [], resumeEnv: { DSHX_RESUME_MODE: 'picker' } }; + if (rest.length === 1 && rest[0] === '--last') { + return { tuiArgs: [], resumeEnv: { DSHX_RESUME_MODE: 'last' } }; + } + if (rest.length === 1 && !rest[0].startsWith('-')) { + return { + tuiArgs: [], + resumeEnv: { DSHX_RESUME_MODE: 'id', DSHX_RESUME_SESSION_ID: rest[0] } + }; + } + throw new Error('Usage: dshx resume [--last|]'); +} + +export function parseCliInvocation(args) { + if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) { + return { kind: 'help' }; + } + if (args.length === 1 && (args[0] === '--version' || args[0] === '-V')) { + return { kind: 'version' }; + } + if (args[0] === 'doctor') { + if (args.length !== 1) throw new Error('Usage: dshx doctor'); + return { kind: 'doctor' }; + } + return { kind: 'launch', ...parseLaunchArgs(args) }; +} diff --git a/src/cli/runtime.mjs b/src/cli/runtime.mjs new file mode 100644 index 00000000..e590597f --- /dev/null +++ b/src/cli/runtime.mjs @@ -0,0 +1,27 @@ +export const DSHX_NODE_ENGINE = '^22.19.0 || >=24.0.0'; +export const DSHX_RELEASE_NODE_MAJOR = 24; +// `dshx doctor` deliberately boots the full official DSH composition, including +// profile watchers. A fresh Windows install can incur one-time module/native +// initialization that exceeds 20s even though subsequent and normal product +// startup are healthy. Keep the diagnostic bounded, but allow realistic cold +// starts on supported desktop hardware. +export const DSHX_DOCTOR_BOOT_TIMEOUT_MS = 60_000; +export const DSHX_DOCTOR_DISPOSE_TIMEOUT_MS = 10_000; + +export function isSupportedNodeVersion(version) { + const match = /^(\d+)\.(\d+)\.(\d+)/.exec(String(version ?? '')); + if (!match) return false; + const major = Number(match[1]); + const minor = Number(match[2]); + return (major === 22 && minor >= 19) || major >= 24; +} + +export function assertReleaseNodeVersion(version = process.versions.node) { + const major = Number(String(version).split('.')[0]); + if (major !== DSHX_RELEASE_NODE_MAJOR) { + throw new Error( + `DSHX dependency freeze/release baseline requires Node ${DSHX_RELEASE_NODE_MAJOR}.x; got ${version}. ` + + `Runtime compatibility follows pinned DSH (${DSHX_NODE_ENGINE}).` + ); + } +} diff --git a/src/dsh/agent-driver.mjs b/src/dsh/agent-driver.mjs new file mode 100644 index 00000000..3641bc70 --- /dev/null +++ b/src/dsh/agent-driver.mjs @@ -0,0 +1,213 @@ +import { randomUUID } from 'node:crypto'; +import { installModelSelection } from '@deepseek-ai/dsh-agent'; +import { createUserMessage } from '@deepseek-ai/dsh-llm'; +import { SessionId } from '@deepseek-ai/dsh-session'; + +function requireService(ctx, name) { + const service = ctx.get(name); + if (service === undefined) throw new Error(`DSHX requires DSH service: ${name}`); + return service; +} + +function userMessage(content) { + const blocks = Array.isArray(content) ? content : [{ type: 'text', text: String(content ?? '') }]; + return createUserMessage({ + source: { kind: 'user' }, + content: blocks + }); +} + +/** Thin delegation layer over official DeepSeek Harness public services. */ +export class DshAgentDriver { + constructor(ctx) { + if (!ctx || typeof ctx.get !== 'function') throw new Error('DshAgentDriver requires a Cordis Context'); + this.ctx = ctx; + this.selections = new WeakMap(); + this.externalSelections = new WeakMap(); + } + + async settleComposition() { + await this.ctx.get('loader')?.await?.(); + } + + persistedSelection(agent) { + const logged = agent.session.requestHeader?.()?.config; + if (logged === undefined) return undefined; + return { + provider: logged.provider, + model: logged.model, + ...(logged.reasoningEffort === undefined ? {} : { reasoningEffort: logged.reasoningEffort }) + }; + } + + restoredSelection(agent) { + return this.persistedSelection(agent) + ?? requireService(this.ctx, 'agentDefaultModel').currentSelection(); + } + + selectionFor(agent) { + const existing = this.selections.get(agent); + if (existing) return existing; + if (this.externalSelections.has(agent)) { + throw new Error('DSHX must not install a second model-selection listener on a Host-owned Agent'); + } + const driver = this; + let picked = this.persistedSelection(agent); + const selection = { + get current() { + return picked + ?? requireService(driver.ctx, 'agentDefaultModel').currentSelection(); + }, + set current(next) { picked = next; }, + assembled: undefined + }; + installModelSelection(agent.ctx, selection); + this.selections.set(agent, selection); + return selection; + } + + /** + * Attach presentation state to an Agent created by another official DSH + * entrypoint (currently Host sessions.fork). No routing listener is installed; + * writes must go back through the owning DSH API delegate. + */ + adoptExternalSelection(agent, { current, select } = {}) { + if (this.selections.has(agent)) { + throw new Error('Cannot adopt Host-owned selection after DSHX installed its own selection listener'); + } + const state = { + current: current ?? this.restoredSelection(agent), + select + }; + this.externalSelections.set(agent, state); + return state.current; + } + + installSelection(agentCtx) { + const agent = agentCtx.agent; + if (!agent) throw new Error('DSHX agent setup has no scoped DSH Agent'); + this.selectionFor(agent); + } + + async create({ cwd = process.cwd(), sessionId = `session-${randomUUID()}` } = {}) { + await this.settleComposition(); + const agents = requireService(this.ctx, 'agents'); + const selection = requireService(this.ctx, 'agentDefaultModel').currentSelection(); + return agents.create({ + sessionId: SessionId(sessionId), + meta: { cwd }, + agentOptions: { provider: selection.provider, model: selection.model }, + setup: (agentCtx) => this.installSelection(agentCtx) + }); + } + + async resume(sessionId) { + await this.settleComposition(); + const agents = requireService(this.ctx, 'agents'); + return agents.resume({ + resumeSessionId: SessionId(sessionId), + setup: (agentCtx) => this.installSelection(agentCtx) + }); + } + + currentModel(agent) { + return this.externalSelections.get(agent)?.current ?? this.selectionFor(agent).current; + } + + currentTitle(agent) { + return this.ctx.get('sessionTitle')?.get?.(agent.session); + } + + renameTitle(agent, title) { + const service = requireService(this.ctx, 'sessionTitle'); + return service.rename(agent.session, title); + } + + async readTitle(sessionId) { + const query = this.ctx.get('sessionQuery'); + if (query?.readTitle) return query.readTitle(SessionId(sessionId)); + const live = this.getLive(sessionId); + return live ? this.currentTitle(live) : undefined; + } + + async selectModel(agent, requested) { + const llm = requireService(this.ctx, 'llm'); + const resolved = await llm.resolveCallConfig({ + provider: requested.provider, + model: requested.model, + ...(requested.reasoningEffort === undefined ? {} : { reasoningEffort: requested.reasoningEffort }) + }); + const selected = { + provider: resolved.provider, + model: resolved.model, + ...(resolved.reasoningEffort === undefined ? {} : { reasoningEffort: resolved.reasoningEffort }) + }; + const external = this.externalSelections.get(agent); + if (external) { + if (typeof external.select !== 'function') { + throw new Error('Host-owned DSH Agent has no model-selection delegate'); + } + const committed = await external.select(selected); + external.current = committed ?? selected; + return external.current; + } + this.selectionFor(agent).current = selected; + return selected; + } + + async modelDirectory() { + const llm = requireService(this.ctx, 'llm'); + const providers = llm.listProviders(); + const settled = await Promise.all(providers.map(async (provider) => { + try { + const models = await llm.listModels(provider.id); + const entries = await Promise.all(models.map(async (model) => { + const exact = await llm.resolveModelInfo(provider.id, model.id); + return { + id: model.id, + name: model.name, + ...(model.description === undefined ? {} : { description: model.description }), + ...(exact.inputModalities === undefined ? {} : { inputModalities: [...exact.inputModalities] }), + ...(exact.context === undefined ? {} : { contextWindow: exact.context.contextWindow }), + ...(exact.reasoning === undefined ? {} : { + reasoning: { + efforts: exact.reasoning.efforts.map((effort) => ({ + id: effort.id, + name: effort.name, + ...(effort.description === undefined ? {} : { description: effort.description }) + })), + ...(exact.reasoning.defaultEffort === undefined ? {} : { defaultEffort: exact.reasoning.defaultEffort }) + } + }) + }; + })); + return { ok: true, group: { provider: provider.id, name: provider.name ?? provider.id, models: entries } }; + } catch (error) { + return { + ok: false, + failure: { provider: provider.id, message: error instanceof Error ? error.message : String(error) } + }; + } + })); + return { + groups: settled.filter((entry) => entry.ok && entry.group.models.length > 0).map((entry) => entry.group), + failures: settled.filter((entry) => !entry.ok).map((entry) => entry.failure) + }; + } + + followup(agent, content) { agent.followup(userMessage(content)); } + steer(agent, content) { agent.steer(userMessage(content)); } + interrupt(agent, { keepInbox = false } = {}) { agent.cancel({ kind: 'user' }, { keepInbox }); } + whenIdle(agent) { return agent.whenIdle(); } + getLive(sessionId) { return requireService(this.ctx, 'agents').get(SessionId(sessionId)); } + listLive() { return requireService(this.ctx, 'agents').list(); } + listRootAgents() { return requireService(this.ctx, 'agents').roots(); } + + async listSessions(signal) { + return requireService(this.ctx, 'sessionPersistence').list(signal); + } + + async inspectSession(sessionId, signal) { + return requireService(this.ctx, 'sessionPersistence').inspect(SessionId(sessionId), signal); + } +} diff --git a/src/dsh/app-server-adapter.mjs b/src/dsh/app-server-adapter.mjs new file mode 100644 index 00000000..ffa13754 --- /dev/null +++ b/src/dsh/app-server-adapter.mjs @@ -0,0 +1,502 @@ +import crypto from 'node:crypto'; +import os from 'node:os'; +import path from 'node:path'; +import { DshAgentDriver } from './agent-driver.mjs'; +import { DshApprovalBridge } from './approval-bridge.mjs'; +import { + codexModelsFromDsh, + decodeDshModel, + dshThreadFromSnapshot, + dshTurnsFromSession, + encodeDshModel, + normalizeCodexEffort, + normalizeSessionHeaders +} from './codex-shapes.mjs'; +import { DshPermissionView } from './permissions.mjs'; +import { DshThreadController } from './thread-controller.mjs'; +import { DshUserQuestionBridge } from './user-question-bridge.mjs'; +import { UiRequestBroker } from '../protocol/request-broker.mjs'; + +const INVALID_REQUEST = -32600; +const METHOD_NOT_FOUND = -32601; +const INVALID_PARAMS = -32602; +const INTERNAL_ERROR = -32603; + +function platformFamily() { + return process.platform === 'win32' ? 'windows' : 'unix'; +} + +function platformOs() { + if (process.platform === 'darwin') return 'macos'; + if (process.platform === 'win32') return 'windows'; + if (process.platform === 'linux') return 'linux'; + return process.platform; +} + +function codexApprovalToDsh(value) { + if (value == null) return undefined; + if (value === 'on-request') return 'ask'; + if (value === 'never') return 'never'; + throw new Error(`DSHX cannot map Codex approval policy ${JSON.stringify(value)} to DSH`); +} + +function textInput(params) { + const inputs = Array.isArray(params?.input) ? params.input : []; + const unsupported = inputs.filter((item) => item?.type !== 'text'); + if (unsupported.length > 0) { + throw new Error(`DSHX turn input currently supports Codex text items only; got ${unsupported.map((item) => item?.type).join(', ')}`); + } + return inputs.map((item) => item.text ?? '').join('\n').trim(); +} + +function pagination(value, total, defaultLimit = 50) { + const cursor = value?.cursor == null ? 0 : Number(value.cursor); + if (!Number.isSafeInteger(cursor) || cursor < 0) throw new Error('invalid pagination cursor'); + const requested = value?.limit == null ? defaultLimit : Number(value.limit); + if (!Number.isSafeInteger(requested) || requested <= 0) throw new Error('limit must be a positive integer'); + const end = Math.min(total, cursor + requested); + return { start: cursor, end, nextCursor: end < total ? String(end) : null }; +} + +function sandboxMatchesCurrent(sandbox, permission) { + if (sandbox == null) return true; + if (sandbox.type === 'dangerFullAccess') return permission.dsh.sandboxMode === 'danger-full-access'; + if (sandbox.type === 'externalSandbox') { + return permission.dsh.sandboxMode === 'read-only' || permission.dsh.sandboxMode === 'workspace-write'; + } + return false; +} + +function fullThreadSnapshot(agent, { includeTurns = false, cliVersion = 'dshx' } = {}) { + const turns = includeTurns ? dshTurnsFromSession({ ctx: agent.ctx, agent }) : []; + return dshThreadFromSnapshot({ + meta: agent.session.header, + events: agent.session.events, + model: agent.session.requestHeader?.()?.config, + turns, + loaded: true, + cliVersion + }); +} + +/** + * Presentation adapter from pinned Codex app-server JSON-RPC to official DSH + * public services. It owns no Agent loop, provider routing, tool dispatch, + * persistence, sandbox enforcement, approval policy or user-question semantics. + */ +export class DshAppServerAdapter { + constructor({ + ctx, + send, + cwd = process.cwd(), + home = path.join(os.homedir(), '.dshx'), + version = '0.1.0-dev', + diagnostics = () => {} + }) { + if (!ctx || typeof ctx.get !== 'function') throw new Error('DshAppServerAdapter requires a Cordis Context'); + if (typeof send !== 'function') throw new Error('DshAppServerAdapter requires send(message)'); + this.ctx = ctx; + this.send = send; + this.cwd = path.resolve(cwd); + this.home = path.resolve(home); + this.version = version; + this.diagnostics = diagnostics; + this.driver = new DshAgentDriver(ctx); + this.permissions = new DshPermissionView(ctx); + this.broker = new UiRequestBroker({ send }); + this.controllers = new Map(); + this.approvals = new Map(); + this.syntheticInteractions = new Map(); + this.readyPromise = null; + this.userQuestions = null; + this.closed = false; + } + + async ensureReady() { + if (this.readyPromise) return this.readyPromise; + this.readyPromise = (async () => { + const loader = this.ctx.get('loader'); + if (loader?.await) await loader.await(); + if (!this.userQuestions && this.ctx.get('userQuestions')) { + this.userQuestions = new DshUserQuestionBridge({ + ctx: this.ctx, + broker: this.broker, + locate: (request) => this.openSyntheticInteraction(request), + complete: (location, outcome) => this.completeSyntheticInteraction(location, outcome), + diagnostics: this.diagnostics + }); + } + })(); + return this.readyPromise; + } + + async handle(message) { + if (this.broker.handleResponse(message)) return null; + if (!message || message.id === undefined || typeof message.method !== 'string') { + if (message?.id === undefined && typeof message?.method === 'string') return null; + return { response: this.error(message?.id ?? null, INVALID_REQUEST, 'Invalid Request') }; + } + try { + await this.ensureReady(); + const dispatched = await this.dispatch(message.method, message.params ?? {}); + return { + response: { id: message.id, result: dispatched?.result }, + afterResponse: dispatched?.afterResponse + }; + } catch (error) { + const code = error?.code === METHOD_NOT_FOUND ? METHOD_NOT_FOUND + : error?.code === INVALID_PARAMS ? INVALID_PARAMS + : INTERNAL_ERROR; + return { + response: this.error(message.id, code, error instanceof Error ? error.message : String(error)) + }; + } + } + + async dispatch(method, params) { + switch (method) { + case 'initialize': + return { + result: { + userAgent: `dshx/${this.version}`, + codexHome: this.home, + platformFamily: platformFamily(), + platformOs: platformOs() + } + }; + case 'account/read': + return { result: { account: null, requiresOpenaiAuth: false } }; + case 'configRequirements/read': + return { result: { requirements: null } }; + case 'skills/list': + // DSH skills remain upstream-owned. Until their public catalog is + // mapped, claiming none is safer than synthesizing Codex skill paths. + return { result: { data: [] } }; + case 'model/list': + return this.modelList(params); + case 'thread/list': + return this.threadList(params); + case 'thread/loaded/list': + return this.loadedThreadList(); + case 'thread/start': + return this.threadStart(params); + case 'thread/resume': + return this.threadResume(params); + case 'thread/read': + return this.threadRead(params); + case 'thread/unsubscribe': + return this.threadUnsubscribe(params); + case 'turn/start': + return this.turnStart(params); + case 'turn/interrupt': + return this.turnInterrupt(params); + default: { + const error = new Error(`Method not found: ${method}`); + error.code = METHOD_NOT_FOUND; + throw error; + } + } + } + + async modelList(params) { + const directory = await this.driver.modelDirectory(); + for (const failure of directory.failures ?? []) { + this.diagnostics(`DSH model catalog ${failure.provider}: ${failure.message}`); + } + const active = this.driver.listRootAgents()[0]; + const current = active + ? this.driver.currentModel(active) + : this.ctx.get('agentDefaultModel')?.currentSelection?.(); + const all = codexModelsFromDsh(directory, current); + const visible = params.includeHidden ? all : all.filter((model) => !model.hidden); + const page = pagination(params, visible.length); + return { result: { data: visible.slice(page.start, page.end), nextCursor: page.nextCursor } }; + } + + async threadList(params) { + const headers = normalizeSessionHeaders(await this.driver.listSessions()); + const snapshots = await Promise.all(headers.map(async (header) => { + try { + const inspected = await this.driver.inspectSession(String(header.id)); + const meta = inspected?.meta ?? header; + const events = inspected?.events ?? []; + return dshThreadFromSnapshot({ + meta, + events, + loaded: Boolean(this.controllers.has(String(meta.id)) || this.driver.getLive(String(meta.id))), + cliVersion: this.version + }); + } catch (error) { + this.diagnostics(`inspect ${String(header.id)}: ${error instanceof Error ? error.message : error}`); + return undefined; + } + })); + let threads = snapshots.filter(Boolean); + if (params.cwd != null) { + const wanted = new Set((Array.isArray(params.cwd) ? params.cwd : [params.cwd]).map((entry) => path.resolve(entry))); + threads = threads.filter((thread) => wanted.has(path.resolve(thread.cwd))); + } + if (params.searchTerm) { + const needle = String(params.searchTerm).toLowerCase(); + threads = threads.filter((thread) => `${thread.name ?? ''}\n${thread.preview}`.toLowerCase().includes(needle)); + } + if (Array.isArray(params.modelProviders) && params.modelProviders.length > 0) { + const providers = new Set(params.modelProviders); + threads = threads.filter((thread) => providers.has(thread.modelProvider)); + } + threads.sort((a, b) => (params.sortDirection === 'asc' ? 1 : -1) * (a.recencyAt - b.recencyAt)); + const page = pagination(params, threads.length); + return { + result: { + data: threads.slice(page.start, page.end).map((thread) => ({ ...thread, turns: [] })), + nextCursor: page.nextCursor, + backwardsCursor: null + } + }; + } + + loadedThreadList() { + const data = [...this.controllers.values()].map((controller) => fullThreadSnapshot(controller.agent, { + includeTurns: false, + cliVersion: this.version + })); + return { result: { data } }; + } + + async applyStartPermissions(agent, params) { + const desiredSandbox = params.sandbox; + const desiredApproval = codexApprovalToDsh(params.approvalPolicy); + if (desiredSandbox == null && desiredApproval == null) return this.permissions.current(agent); + + const current = this.permissions.current(agent); + const sandboxMode = desiredSandbox ?? current.dsh.sandboxMode; + const approvalPolicy = desiredApproval ?? current.dsh.approvalPolicy; + const presets = this.ctx.get('permissionPresets'); + const match = presets?.names?.find((name) => { + const preset = presets.resolve(name); + return preset.sandbox === sandboxMode && preset.approval === approvalPolicy; + }); + if (!match) { + throw new Error(`DSHX refuses to synthesize a DSH custom permission bundle (${sandboxMode}, ${approvalPolicy})`); + } + this.permissions.set(agent, match); + return this.permissions.current(agent); + } + + async applyModelOverride(agent, { model, modelProvider, effort } = {}) { + if (model == null && effort == null) return this.driver.currentModel(agent); + let selection; + if (model != null) { + selection = decodeDshModel(model); + if (!selection && modelProvider) selection = { provider: modelProvider, model }; + if (!selection) throw new Error('DSHX accepts model switches only from its DSH-backed model catalog'); + } else { + selection = this.driver.currentModel(agent); + } + const normalizedEffort = normalizeCodexEffort(effort); + if (normalizedEffort !== undefined) selection = { ...selection, reasoningEffort: normalizedEffort }; + return this.driver.selectModel(agent, selection); + } + + installController(handle) { + const threadId = String(handle.agent.id); + const existing = this.controllers.get(threadId); + if (existing) return existing; + const controller = new DshThreadController({ + handle, + driver: this.driver, + emit: this.send, + diagnostics: this.diagnostics + }); + this.controllers.set(threadId, controller); + const approval = new DshApprovalBridge({ + agent: handle.agent, + broker: this.broker, + classify: (req) => req.callId == null ? null : controller.toolCorrelation(req.callId), + diagnostics: this.diagnostics + }); + this.approvals.set(threadId, approval); + return controller; + } + + threadResponse(agent, { includeTurns = false } = {}) { + const selection = this.driver.currentModel(agent); + const permission = this.permissions.current(agent); + const thread = fullThreadSnapshot(agent, { includeTurns, cliVersion: this.version }); + return { + thread, + model: encodeDshModel(selection), + modelProvider: selection.provider, + serviceTier: null, + cwd: thread.cwd, + instructionSources: [], + approvalPolicy: permission.codex.approvalPolicy, + approvalsReviewer: permission.codex.approvalsReviewer, + sandbox: permission.codex.sandbox, + reasoningEffort: selection.reasoningEffort ?? null + }; + } + + async threadStart(params) { + const sessionId = crypto.randomUUID(); + const cwd = path.resolve(params.cwd ?? this.cwd); + const handle = await this.driver.create({ cwd, sessionId }); + try { + await this.applyModelOverride(handle.agent, params); + await this.applyStartPermissions(handle.agent, params); + this.installController(handle); + const result = this.threadResponse(handle.agent); + return { + result, + afterResponse: () => this.send({ method: 'thread/started', params: { thread: result.thread } }) + }; + } catch (error) { + await handle.dispose?.(); + throw error; + } + } + + async threadResume(params) { + if (!params.threadId) throw new Error('thread/resume requires threadId'); + let controller = this.controllers.get(String(params.threadId)); + if (!controller) { + const live = this.driver.getLive(String(params.threadId)); + const handle = live + ? { agent: live, dispose: async () => {} } + : await this.driver.resume(String(params.threadId)); + controller = this.installController(handle); + } + const agent = controller.agent; + if (params.cwd != null && path.resolve(params.cwd) !== path.resolve(agent.session.header?.cwd ?? this.cwd)) { + throw new Error('DSHX does not override a persisted DSH session cwd during resume'); + } + await this.applyModelOverride(agent, params); + await this.applyStartPermissions(agent, params); + return { result: this.threadResponse(agent, { includeTurns: true }) }; + } + + async threadRead(params) { + if (!params.threadId) throw new Error('thread/read requires threadId'); + const live = this.driver.getLive(String(params.threadId)); + if (live) { + return { result: { thread: fullThreadSnapshot(live, { includeTurns: Boolean(params.includeTurns), cliVersion: this.version }) } }; + } + const inspected = await this.driver.inspectSession(String(params.threadId)); + if (!inspected) throw new Error(`Unknown DSH session: ${String(params.threadId)}`); + return { + result: { + thread: dshThreadFromSnapshot({ + meta: inspected.meta, + events: inspected.events, + loaded: false, + cliVersion: this.version, + // A cold persisted session cannot faithfully use tool presenters + // without a composed Agent. The TUI resumes before requesting rich + // history; avoid fabricating tool semantics on a cold read. + turns: [] + }) + } + }; + } + + async turnStart(params) { + const controller = this.controllers.get(String(params.threadId)); + if (!controller) throw new Error(`Thread is not resumed in DSHX: ${String(params.threadId)}`); + if (params.cwd != null && path.resolve(params.cwd) !== path.resolve(controller.agent.session.header?.cwd ?? this.cwd)) { + throw new Error('DSHX does not own per-turn cwd mutation; resume/start the DSH session in the desired cwd'); + } + if (params.outputSchema != null) throw new Error('DSHX has no faithful public DSH mapping for Codex outputSchema yet'); + + await this.applyModelOverride(controller.agent, { model: params.model, effort: params.effort }); + const permission = this.permissions.current(controller.agent); + const desiredApproval = codexApprovalToDsh(params.approvalPolicy); + if (desiredApproval != null && desiredApproval !== permission.dsh.approvalPolicy) { + throw new Error('Turn-level approval changes must use a DSH permission preset'); + } + if (!sandboxMatchesCurrent(params.sandboxPolicy, permission)) { + throw new Error('Turn-level sandbox changes must use a DSH permission preset'); + } + + const started = await controller.startTurn(textInput(params)); + return { result: { turn: started.turn }, afterResponse: started.release }; + } + + turnInterrupt(params) { + const controller = this.controllers.get(String(params.threadId)); + if (!controller) return { result: {} }; + controller.interrupt({ keepInbox: true }); + return { result: {} }; + } + + async threadUnsubscribe(params) { + const threadId = String(params.threadId ?? ''); + const controller = this.controllers.get(threadId); + if (controller) { + this.approvals.get(threadId)?.dispose(); + this.approvals.delete(threadId); + this.controllers.delete(threadId); + await controller.close(); + } + return { result: {} }; + } + + openSyntheticInteraction(request) { + const agentId = request?.agent?.id; + if (!agentId) return undefined; + const controller = this.controllers.get(String(agentId)); + const location = controller?.currentLocation(); + if (!location) return undefined; + const itemId = `dshx-interaction-${crypto.randomUUID()}`; + const startedAtMs = Date.now(); + const item = { + type: 'dynamicToolCall', + id: itemId, + namespace: 'dshx', + tool: 'userInteraction', + arguments: { questionCount: request.questions?.length ?? 0 }, + status: 'inProgress', + contentItems: null, + success: null, + durationMs: null + }; + this.syntheticInteractions.set(itemId, { ...location, item, startedAtMs }); + this.send({ + method: 'item/started', + params: { ...location, item, startedAtMs } + }); + return { ...location, itemId }; + } + + completeSyntheticInteraction(location, outcome) { + const state = this.syntheticInteractions.get(location.itemId); + if (!state) return; + this.syntheticInteractions.delete(location.itemId); + const completedAtMs = Date.now(); + const item = { + ...state.item, + status: outcome.status === 'completed' ? 'completed' : 'failed', + success: outcome.status === 'completed', + durationMs: Math.max(0, completedAtMs - state.startedAtMs) + }; + this.send({ + method: 'item/completed', + params: { threadId: state.threadId, turnId: state.turnId, item, completedAtMs } + }); + } + + error(id, code, message, data = undefined) { + const error = { code, message }; + if (data !== undefined) error.data = data; + return { id, error }; + } + + async close() { + if (this.closed) return; + this.closed = true; + this.userQuestions?.dispose(); + this.userQuestions = null; + this.broker.close(); + const ids = [...this.controllers.keys()]; + for (const id of ids) await this.threadUnsubscribe({ threadId: id }); + } +} diff --git a/src/dsh/approval-bridge.mjs b/src/dsh/approval-bridge.mjs new file mode 100644 index 00000000..20bd0319 --- /dev/null +++ b/src/dsh/approval-bridge.mjs @@ -0,0 +1,88 @@ +import { mapCodexDecisionToDshApprovalOutcome } from './permissions.mjs'; + +function unavailable() { + return 'unavailable'; +} + +/** + * Presentation-only answerer for DSH `approval/request`. + * + * `classify(req)` must return a faithful presentation descriptor for a DSH + * tool that already has a matching Codex UI surface, or null when DSHX cannot + * represent the request. Null deliberately delegates to the next DSH answerer; + * if nobody owns it, DSH itself returns `unavailable` and callers fail closed. + */ +export class DshApprovalBridge { + constructor({ agent, broker, classify, diagnostics = () => {} }) { + if (!agent?.ctx) throw new Error('DshApprovalBridge requires a live DSH Agent'); + if (!broker) throw new Error('DshApprovalBridge requires a UiRequestBroker'); + if (typeof classify !== 'function') throw new Error('DshApprovalBridge requires classify(req)'); + this.agent = agent; + this.broker = broker; + this.classify = classify; + this.diagnostics = diagnostics; + this.disposeListener = agent.ctx.on('approval/request', (req, next) => this.answer(req, next)); + } + + async answer(req, next) { + const descriptor = this.classify(req); + if (!descriptor) return next(); + + const { kind, threadId, turnId, itemId } = descriptor; + if (!threadId || !turnId || !itemId) { + this.diagnostics('approval classifier returned an incomplete descriptor'); + return unavailable(); + } + + let method; + let params; + if (kind === 'command') { + method = 'item/commandExecution/requestApproval'; + params = { + threadId, + turnId, + itemId, + startedAtMs: Date.now(), + approvalId: null, + environmentId: null, + reason: req.reason ?? null, + networkApprovalContext: null, + command: descriptor.command ?? null, + cwd: descriptor.cwd ?? null, + commandActions: null, + proposedExecpolicyAmendment: null, + proposedNetworkPolicyAmendments: null + }; + } else if (kind === 'fileChange') { + method = 'item/fileChange/requestApproval'; + params = { + threadId, + turnId, + itemId, + startedAtMs: Date.now(), + reason: req.reason ?? null, + grantRoot: null + }; + } else { + this.diagnostics(`unsupported approval descriptor kind: ${String(kind)}`); + return unavailable(); + } + + try { + const result = await this.broker.request(method, params, { signal: req.signal }); + return mapCodexDecisionToDshApprovalOutcome(result?.decision); + } catch (error) { + // Any transport failure, abort, timeout, unsupported persistent grant or + // malformed decision fails closed. DSH remains the policy owner and logs + // the resulting `unavailable`/cancelled decision through its own service. + if (req.signal?.aborted || error?.name === 'AbortError') return 'cancelled'; + this.diagnostics(error instanceof Error ? error.message : String(error)); + return unavailable(); + } + } + + dispose() { + this.disposeListener?.(); + this.disposeListener = undefined; + } +} diff --git a/src/dsh/codex-shapes.mjs b/src/dsh/codex-shapes.mjs new file mode 100644 index 00000000..acfa7e05 --- /dev/null +++ b/src/dsh/codex-shapes.mjs @@ -0,0 +1,419 @@ +import { DshToolPresentationResolver } from './tool-presentation.mjs'; + +const MODEL_PREFIX = 'dshx:'; +const DEFAULT_REASONING_SENTINEL = 'dsh-default'; +const DSH_SUBAGENT_DESCRIPTOR_VERSION = 2; + +function seconds(timeMs, fallback = Math.floor(Date.now() / 1000)) { + return Number.isFinite(timeMs) ? Math.floor(timeMs / 1000) : fallback; +} + +function visibleText(content) { + return (content ?? []) + .filter((block) => block?.type === 'text' && typeof block.text === 'string') + .map((block) => block.text) + .join(''); +} + +function visibleReasoning(content) { + return (content ?? []) + .filter((block) => block?.type === 'reasoning' && typeof block.text === 'string') + .map((block) => block.text); +} + +function latestRequestConfig(events) { + for (let index = (events?.length ?? 0) - 1; index >= 0; index -= 1) { + const event = events[index]; + if (event?.type === 'request/header') return event.data?.header?.config ?? null; + } + return null; +} + +function firstHumanPreview(events) { + for (const event of events ?? []) { + if (event?.type !== 'user/message' || event.data?.source?.kind !== 'user') continue; + const text = visibleText(event.data.content).trim(); + if (text) return text.slice(0, 240); + } + return ''; +} + +function turnStatus(reason) { + switch (reason?.kind) { + case 'completed': return 'completed'; + case 'aborted': + case 'interrupted': return 'interrupted'; + default: return 'failed'; + } +} + +function turnError(reason) { + if (reason?.kind !== 'error') return null; + return { + message: reason.error?.message ?? 'DSH turn failed', + codexErrorInfo: null, + additionalDetails: null + }; +} + +function compactionError(message) { + return message == null + ? null + : { message: String(message), codexErrorInfo: null, additionalDetails: null }; +} + +function normalizeList(value) { + if (Array.isArray(value)) return value; + if (Array.isArray(value?.entries)) return value.entries; + return []; +} + +function subagentDescriptor(events = []) { + const event = events.find((candidate) => candidate?.type === 'subagent/descriptor'); + const value = event?.data; + if (!value || value.version !== DSH_SUBAGENT_DESCRIPTOR_VERSION) return null; + if ((value.mode !== 'one-shot' && value.mode !== 'continuable') || typeof value.provider !== 'string') return null; + return { + mode: value.mode, + provider: value.provider, + label: typeof value.label === 'string' && value.label.length > 0 ? value.label : null + }; +} + +function sourceForHeader(header, events = []) { + if (header?.origin === 'subagent' && header.parentSession != null) { + const descriptor = subagentDescriptor(events); + return { + subagent: { + thread_spawn: { + parent_thread_id: String(header.parentSession), + depth: Number.isInteger(header.delegationDepth) ? header.delegationDepth : 1, + agent_path: null, + agent_nickname: descriptor?.label ?? null, + agent_role: null + } + } + }; + } + return { custom: 'dshx' }; +} + +function inputFromDshMessage(message) { + const content = []; + for (const block of message?.content ?? []) { + if (block?.type === 'text' && typeof block.text === 'string') { + content.push({ type: 'text', text: block.text, text_elements: [] }); + } + } + return content; +} + +function historyTurn(id, startedAt) { + return { + id, + items: [], + itemsView: 'full', + status: 'inProgress', + error: null, + startedAt, + completedAt: null, + durationMs: null + }; +} + +export function encodeDshModel(selection) { + if (!selection?.provider || !selection?.model) throw new Error('DSHX model selection requires provider and model'); + const payload = Buffer.from(JSON.stringify([selection.provider, selection.model]), 'utf8').toString('base64url'); + return `${MODEL_PREFIX}${payload}`; +} + +export function decodeDshModel(value) { + if (typeof value !== 'string' || !value.startsWith(MODEL_PREFIX)) return undefined; + try { + const decoded = JSON.parse(Buffer.from(value.slice(MODEL_PREFIX.length), 'base64url').toString('utf8')); + if (!Array.isArray(decoded) || decoded.length !== 2 || decoded.some((part) => typeof part !== 'string' || !part)) { + return undefined; + } + return { provider: decoded[0], model: decoded[1] }; + } catch { + return undefined; + } +} + +export function normalizeCodexEffort(value) { + return value === DEFAULT_REASONING_SENTINEL ? undefined : value; +} + +/** + * Translate the public DSH LLM registry into Codex's provider-less model picker. + * Provider identity is encoded only in the opaque `model`/`id` value and decoded + * again by DSHX before calling `ctx.llm.resolveCallConfig()`. + */ +export function codexModelsFromDsh(directory, currentSelection) { + const groups = directory?.groups ?? []; + const multipleProviders = groups.length > 1; + const models = []; + for (const group of groups) { + for (const model of group.models ?? []) { + const encoded = encodeDshModel({ provider: group.provider, model: model.id }); + const efforts = model.reasoning?.efforts ?? []; + const supportedReasoningEfforts = efforts.length > 0 + ? efforts.map((effort) => ({ + reasoningEffort: effort.id, + description: effort.description ?? effort.name ?? effort.id + })) + : [{ reasoningEffort: DEFAULT_REASONING_SENTINEL, description: 'Use the DSH provider default' }]; + const defaultReasoningEffort = model.reasoning?.defaultEffort ?? DEFAULT_REASONING_SENTINEL; + const displayName = multipleProviders + ? `${model.name ?? model.id} · ${group.name ?? group.provider}` + : (model.name ?? model.id); + models.push({ + id: encoded, + model: encoded, + upgrade: null, + upgradeInfo: null, + availabilityNux: null, + displayName, + description: model.description ?? `${group.name ?? group.provider} / ${model.id}`, + modelSpecialty: null, + hidden: false, + supportedReasoningEfforts, + defaultReasoningEffort, + inputModalities: (model.inputModalities ?? []).filter((entry) => entry === 'text' || entry === 'image'), + supportsPersonality: false, + multiAgentVersion: null, + additionalSpeedTiers: [], + serviceTiers: [], + defaultServiceTier: null, + isDefault: currentSelection?.provider === group.provider && currentSelection?.model === model.id + }); + } + } + return models; +} + +export function dshThreadFromSnapshot({ meta, events = [], model, turns = [], loaded = false, cliVersion = 'dshx' }) { + if (!meta?.id) throw new Error('DSHX thread projection requires SessionHeader.id'); + const config = model ?? latestRequestConfig(events) ?? {}; + const createdAt = seconds(meta.createdAt); + const lastTime = events.length > 0 ? events[events.length - 1]?.time : meta.createdAt; + const isSubagent = meta.origin === 'subagent'; + const descriptor = isSubagent ? subagentDescriptor(events) : null; + let openTurn = false; + for (const event of events) { + if (event?.type === 'turn/start') openTurn = true; + else if (event?.type === 'turn/end') openTurn = false; + } + return { + id: String(meta.id), + extra: null, + sessionId: String(meta.id), + forkedFromId: !isSubagent && meta.parentSession ? String(meta.parentSession) : null, + parentThreadId: isSubagent && meta.parentSession ? String(meta.parentSession) : null, + preview: firstHumanPreview(events), + ephemeral: false, + section: null, + sectionEnteredAt: null, + projectId: null, + historyMode: 'paginated', + modelProvider: config.provider ?? 'dsh', + createdAt, + updatedAt: seconds(lastTime, createdAt), + recencyAt: seconds(lastTime, createdAt), + status: loaded ? (openTurn ? { type: 'active', activeFlags: [] } : { type: 'idle' }) : { type: 'notLoaded' }, + path: null, + cwd: meta.cwd ?? process.cwd(), + cliVersion, + source: sourceForHeader(meta, events), + // DSH continuation authority belongs to ctx.subagents. Showing a child in + // Codex navigation must never create a presentation-side bypass that sends + // a user turn directly to the child Agent. + canAcceptDirectInput: isSubagent ? false : loaded ? true : null, + threadSource: null, + agentNickname: descriptor?.label ?? null, + agentRole: null, + gitInfo: null, + name: null, + turns + }; +} + +/** + * Replay the authoritative DSH raw log as a human transcript. This intentionally + * does not fold the model surface: DSH's own terminal/web transcript contract + * keeps messages shadowed by compaction visible to the human and inserts a + * compaction marker at the landed checkpoint. The plugin-sourced replacement + * user/message is therefore ignored as model-only framing. + */ +export function dshTurnsFromSession({ ctx, agent, diagnostics = () => {} }) { + const session = agent?.session; + if (!session) return []; + const turns = new Map(); + const compactions = new Map(); + let currentTurn; + const toolPresenter = new DshToolPresentationResolver({ + ctx: agent.ctx ?? ctx, + agent, + threadId: String(agent.id), + workspaceCwd: session.header?.cwd ?? process.cwd(), + diagnostics + }); + + for (const event of session.events ?? []) { + switch (event.type) { + case 'turn/start': { + const number = event.data?.turn; + if (!Number.isInteger(number)) break; + const startedAt = seconds(event.time); + turns.set(number, historyTurn(`dsh-turn-${number}`, startedAt)); + currentTurn = number; + break; + } + case 'user/message': { + const turn = turns.get(currentTurn); + const message = event.data; + if (!turn || message?.source?.kind !== 'user') break; + const content = inputFromDshMessage(message); + if (content.length === 0) break; + turn.items.push({ type: 'userMessage', id: String(message.id), clientId: null, content }); + break; + } + case 'assistant/message': { + const turn = turns.get(event.data?.turn); + const message = event.data?.message; + if (!turn || !message) break; + const reasoning = visibleReasoning(message.content); + if (reasoning.length > 0) { + turn.items.push({ + type: 'reasoning', + id: `dsh-reasoning-${event.data.turn}-${event.data.step}`, + summary: [], + content: reasoning + }); + } + const text = visibleText(message.content); + if (text.length > 0) { + turn.items.push({ + type: 'agentMessage', + id: String(message.id ?? `dsh-assistant-${event.data.turn}-${event.data.step}`), + text, + phase: null, + memoryCitation: null, + delivery: null + }); + } + break; + } + case 'tool/call': { + const turn = turns.get(event.data?.turn); + if (!turn) break; + const projected = toolPresenter.start({ + turnId: turn.id, + callId: event.data.callId, + name: event.data.name, + rawArguments: event.data.arguments, + startedAtMs: event.time + }); + turn.items.push(projected.item); + break; + } + case 'tool/result': { + const callId = event.data?.message?.source?.callId + ?? event.data?.message?.content?.find?.((block) => block?.type === 'tool-result')?.toolCallId; + if (!callId) break; + const completed = toolPresenter.complete({ callId, resultData: event.data, completedAtMs: event.time }); + if (!completed) break; + for (const turn of turns.values()) { + const index = turn.items.findIndex((item) => item.id === completed.item.id); + if (index >= 0) { + turn.items[index] = completed.item; + break; + } + } + break; + } + case 'compaction/start': { + const { compactionId, turn } = event.data ?? {}; + if (typeof compactionId !== 'string' || compactionId.length === 0) break; + let turnKey; + let target; + let manual = false; + if (Number.isInteger(turn)) { + turnKey = turn; + target = turns.get(turnKey); + if (!target) break; + } else if (turn === null) { + manual = true; + turnKey = `compact:${compactionId}`; + target = historyTurn(`dsh-maintenance-compaction-${compactionId}`, seconds(event.time)); + turns.set(turnKey, target); + } else { + break; + } + compactions.set(compactionId, { + turnKey, + turn: target, + manual, + summarized: false, + itemId: `dsh-compaction-${compactionId}` + }); + break; + } + case 'compaction/summary': { + const state = compactions.get(event.data?.compactionId); + if (!state) break; + state.summarized = true; + state.turn.items.push({ type: 'contextCompaction', id: state.itemId }); + break; + } + case 'compaction/end': { + const state = compactions.get(event.data?.compactionId); + if (!state) break; + compactions.delete(event.data.compactionId); + if (state.manual) { + if (!state.summarized) { + turns.delete(state.turnKey); + break; + } + const completedAt = seconds(event.time); + state.turn.status = event.data?.error == null ? 'completed' : 'failed'; + state.turn.error = compactionError(event.data?.error); + state.turn.completedAt = completedAt; + state.turn.durationMs = state.turn.startedAt == null + ? null + : Math.max(0, (completedAt - state.turn.startedAt) * 1000); + } + break; + } + case 'turn/end': { + const number = event.data?.turn; + const turn = turns.get(number); + if (!turn) break; + const completedAt = seconds(event.time); + turn.status = turnStatus(event.data.reason); + turn.error = turnError(event.data.reason); + turn.completedAt = completedAt; + turn.durationMs = turn.startedAt == null ? null : Math.max(0, (completedAt - turn.startedAt) * 1000); + if (currentTurn === number) currentTurn = undefined; + break; + } + default: + break; + } + } + return [...turns.values()]; +} + +export function normalizeSessionHeaders(value) { + return normalizeList(value); +} + +export const internals = { + DEFAULT_REASONING_SENTINEL, + latestRequestConfig, + firstHumanPreview, + inputFromDshMessage, + visibleReasoning, + subagentDescriptor, + sourceForHeader +}; diff --git a/src/dsh/commands.mjs b/src/dsh/commands.mjs new file mode 100644 index 00000000..d63bec35 --- /dev/null +++ b/src/dsh/commands.mjs @@ -0,0 +1,27 @@ +function requireService(ctx, name) { + const service = ctx?.get?.(name); + if (service == null) throw new Error(`DSHX requires DSH service: ${name}`); + return service; +} + +/** + * Execute one human-facing DSH command through the official command plane. + * DSHX does not parse, implement, persist, or run the domain operation itself. + */ +export async function executeDshCommand({ ctx, agent, line, images = [], signal }) { + if (!agent) throw new Error('DSHX command execution requires a live DSH Agent'); + const commands = requireService(ctx, 'commands'); + const execution = await commands.execute(agent, line, images, signal); + if (execution == null) throw new Error(`DSH command is not registered: ${line}`); + if (execution.result?.kind === 'error') { + throw new Error(execution.result.text || `DSH command failed: ${line}`); + } + return execution; +} + +/** Read the current agent-scoped command catalog from DSH. */ +export function listDshCommands({ ctx, agent }) { + if (!agent) throw new Error('DSHX command discovery requires a live DSH Agent'); + const commands = requireService(ctx, 'commands'); + return commands.list(agent); +} diff --git a/src/dsh/history-pages.mjs b/src/dsh/history-pages.mjs new file mode 100644 index 00000000..f27881b3 --- /dev/null +++ b/src/dsh/history-pages.mjs @@ -0,0 +1,77 @@ +import { dshTurnsFromSession } from './codex-shapes.mjs'; + +function cursorOffset(cursor) { + if (cursor == null) return 0; + const match = /^dshx:(\d+)$/.exec(String(cursor)); + if (!match) throw new Error(`invalid DSHX history cursor: ${JSON.stringify(cursor)}`); + const value = Number(match[1]); + if (!Number.isSafeInteger(value) || value < 0) throw new Error('invalid DSHX history cursor'); + return value; +} + +function pageSize(limit, fallback) { + if (limit == null) return fallback; + const value = Number(limit); + if (!Number.isSafeInteger(value) || value <= 0) throw new Error('history limit must be a positive integer'); + return Math.min(value, 1000); +} + +function page(values, { cursor, limit, sortDirection, fallbackLimit }) { + const ordered = sortDirection === 'asc' ? values : [...values].reverse(); + const start = cursorOffset(cursor); + const size = pageSize(limit, fallbackLimit); + const end = Math.min(ordered.length, start + size); + return { + data: ordered.slice(start, end), + nextCursor: end < ordered.length ? `dshx:${end}` : null, + // DSHX cursors are forward-only presentation cursors. The TUI only needs + // nextCursor for bounded backwards hydration; no durable cursor state is owned here. + backwardsCursor: null + }; +} + +function projectedTurns(controller, diagnostics) { + return dshTurnsFromSession({ + ctx: controller.agent.ctx, + agent: controller.agent, + diagnostics + }); +} + +function viewTurn(turn, requestedView) { + if (requestedView === 'notLoaded') { + return { ...turn, items: [], itemsView: 'notLoaded' }; + } + // DSHX has a faithful full presentation fold, not a second lossy summary + // algorithm. When Codex asks for summary, return full and label it honestly. + return { ...turn, itemsView: 'full' }; +} + +export function dshThreadTurnsPage({ controller, params = {}, diagnostics = () => {} }) { + const turns = projectedTurns(controller, diagnostics).map((turn) => viewTurn(turn, params.itemsView)); + return page(turns, { + cursor: params.cursor, + limit: params.limit, + sortDirection: params.sortDirection, + fallbackLimit: 5 + }); +} + +export function dshThreadItemsPage({ controller, params = {}, diagnostics = () => {} }) { + let turns = projectedTurns(controller, diagnostics); + if (params.turnId != null) { + turns = turns.filter((turn) => turn.id === String(params.turnId)); + } + const entries = []; + for (const turn of turns) { + for (const item of turn.items) entries.push({ turnId: turn.id, item }); + } + return page(entries, { + cursor: params.cursor, + limit: params.limit, + sortDirection: params.sortDirection, + fallbackLimit: 100 + }); +} + +export const historyInternals = { cursorOffset, pageSize, page, viewTurn }; diff --git a/src/dsh/host-api.mjs b/src/dsh/host-api.mjs new file mode 100644 index 00000000..287bd9fc --- /dev/null +++ b/src/dsh/host-api.mjs @@ -0,0 +1,61 @@ +import { randomUUID } from 'node:crypto'; +import { createApiProxy, RpcId } from '@deepseek-ai/dsh-host-apiproxy'; + +function requireService(ctx, name) { + const service = ctx?.get?.(name); + if (service == null) throw new Error(`DSHX requires DSH service: ${name}`); + return service; +} + +function rpcError(error, fallback) { + if (error?.message) return new Error(error.message); + return new Error(fallback); +} + +function value(response, fallback) { + if (!response?.result?.ok) throw rpcError(response?.result?.error, fallback); + return response.result.value; +} + +/** + * Very small façade over DSH's own transport-agnostic Host API. + * Domain semantics stay in @deepseek-ai/dsh-host-apiproxy; DSHX only adapts + * Codex presentation requests into that public API. + */ +export class DshHostApi { + constructor(ctx, { cwd = process.cwd() } = {}) { + if (!ctx || typeof ctx.get !== 'function') throw new Error('DshHostApi requires a Cordis Context'); + this.ctx = ctx; + this.api = createApiProxy(ctx, { + cwd, + defaultModelSelection: () => requireService(ctx, 'agentDefaultModel').currentSelection(), + saveDefaultModelSelection: (selection) => requireService(ctx, 'agentDefaultModel').saveSelection(selection) + }); + } + + request(payload) { + return { + rpcId: RpcId(`dshx-${randomUUID()}`), + payload + }; + } + + async forkSession({ sessionId, atSeq } = {}) { + const response = await this.api.sessions.fork(this.request({ + sessionId, + ...(atSeq == null ? {} : { atSeq }) + })); + return value(response, `DSH session fork failed for ${String(sessionId)}`); + } + + async selectModel({ sessionId, provider, model, reasoningEffort } = {}) { + const response = await this.api.sessions.selectModel(this.request({ + sessionId, + provider, + model, + ...(reasoningEffort == null ? {} : { reasoningEffort }) + })); + const result = value(response, `DSH model selection failed for ${String(sessionId)}`); + return result?.selected ?? result; + } +} diff --git a/src/dsh/local-server.mjs b/src/dsh/local-server.mjs new file mode 100644 index 00000000..22d4ccca --- /dev/null +++ b/src/dsh/local-server.mjs @@ -0,0 +1,357 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawn } from 'node:child_process'; +import { createInterface } from 'node:readline'; +import { fileURLToPath } from 'node:url'; +import { DshxReleaseAdapter } from './release-adapter.mjs'; +import { bootDshxRuntime } from './runtime-boot.mjs'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); +const READY = 'ready'; +const WINDOWS_UNIX_PATH_MAX = 108; + +function packagedBridgeBinary() { + const name = process.platform === 'win32' ? 'dshx-ipc-bridge.exe' : 'dshx-ipc-bridge'; + return process.env.DSHX_IPC_BRIDGE_BIN || path.join(ROOT, 'dist', 'bin', name); +} + +function parseError(error) { + return { + id: null, + error: { + code: -32700, + message: `Parse error: ${error instanceof Error ? error.message : String(error)}` + } + }; +} + +function isPathWithin(root, candidate, pathApi = path) { + const relative = pathApi.relative(pathApi.resolve(root), pathApi.resolve(candidate)); + return relative === '' || ( + relative !== '..' && + !relative.startsWith(`..${pathApi.sep}`) && + !pathApi.isAbsolute(relative) + ); +} + +function defaultSocketRoot({ + home, + platform = process.platform, + temporaryDirectory = os.tmpdir(), + userHome = os.homedir() +} = {}) { + if (platform !== 'win32') return temporaryDirectory; + // codex_uds can enforce 0700 on Unix. Its Windows implementation inherits + // directory ACLs instead, so do not trust an arbitrary/shared %TEMP% root. + // Anchor the rendezvous below the current user's DSHX presentation home, + // whose parent in turn lives below the user's profile by default. Keep the + // Windows suffix deliberately short because sockaddr_un.sun_path is bounded. + const pathApi = path.win32; + const userProfile = pathApi.resolve(userHome); + const presentationHome = pathApi.resolve(home || pathApi.join(userProfile, '.dshx', 'codex-tui')); + if (!isPathWithin(userProfile, presentationHome, pathApi)) { + throw new Error( + `DSHX_TUI_HOME must remain under the Windows user profile (${userProfile}) so local IPC inherits private user ACLs; got ${presentationHome}` + ); + } + return pathApi.join(presentationHome, 'i'); +} + +function createSocketDirectory(root) { + fs.mkdirSync(root, { recursive: true, mode: 0o700 }); + const directory = fs.mkdtempSync(path.join(root, 'd-')); + if (process.platform !== 'win32') fs.chmodSync(directory, 0o700); + return directory; +} + +function assertSocketPathSupported(socketPath, { + platform = process.platform, + windowsUnixPathMax = WINDOWS_UNIX_PATH_MAX +} = {}) { + if (platform !== 'win32') return; + const bytes = Buffer.byteLength(socketPath, 'utf8'); + // sun_path needs one byte for the terminating NUL, so the pathname itself + // must remain strictly shorter than the fixed buffer. + if (bytes >= windowsUnixPathMax) { + throw new Error( + `DSHX local IPC path is ${bytes} UTF-8 bytes; Windows AF_UNIX requires fewer than ${windowsUnixPathMax}. ` + + 'Set DSHX_TUI_HOME to a shorter path under your user profile and retry.' + ); + } +} + +function waitForBridgeReady(lines, child, stderrText) { + return new Promise((resolve, reject) => { + const fail = (error) => { + cleanup(); + reject(error); + }; + const onLine = (line) => { + let message; + try { + message = JSON.parse(line); + } catch (error) { + fail(new Error(`DSHX IPC bridge emitted invalid readiness JSON: ${line}`, { cause: error })); + return; + } + if (message?.dshxBridge !== READY) { + fail(new Error(`DSHX IPC bridge emitted unexpected readiness message: ${line}`)); + return; + } + cleanup(); + resolve(message); + }; + const onError = (error) => fail(new Error(`failed to start DSHX IPC bridge: ${error.message}`, { cause: error })); + const onExit = (code, signal) => { + const detail = stderrText().trim(); + fail(new Error(`DSHX IPC bridge exited before ready (${signal ?? code ?? 'unknown'})${detail ? `: ${detail}` : ''}`)); + }; + const cleanup = () => { + lines.off('line', onLine); + child.off('error', onError); + child.off('exit', onExit); + }; + lines.once('line', onLine); + child.once('error', onError); + child.once('exit', onExit); + }); +} + +function waitForExit(child, timeoutMs = 750) { + if (!child || child.exitCode !== null || child.signalCode !== null) return Promise.resolve(); + return new Promise((resolve) => { + let done = false; + const finish = () => { + if (done) return; + done = true; + clearTimeout(timer); + child.off('exit', finish); + resolve(); + }; + const timer = setTimeout(() => { + if (child.exitCode === null && child.signalCode === null) child.kill('SIGTERM'); + finish(); + }, timeoutMs); + child.once('exit', finish); + }); +} + +function cleanupDiagnostic(log, scope, error) { + log(`${scope}: ${error instanceof Error ? error.message : String(error)}`); +} + +async function cleanupFailedStartup({ lines, bridge, ctx, socketDirectory, log }) { + try { + lines?.close(); + } catch (error) { + cleanupDiagnostic(log, 'startup rollback: readline cleanup failed', error); + } + if (bridge) { + try { + if (bridge.stdin?.writable) bridge.stdin.end(); + if (bridge.exitCode === null && bridge.signalCode === null) bridge.kill?.('SIGTERM'); + await waitForExit(bridge); + if (bridge.exitCode === null && bridge.signalCode === null) bridge.kill?.('SIGKILL'); + } catch (error) { + cleanupDiagnostic(log, 'startup rollback: bridge cleanup failed', error); + } + } + try { + await ctx?.dispose?.(); + } catch (error) { + cleanupDiagnostic(log, 'startup rollback: DSH dispose failed', error); + } + if (socketDirectory) { + try { + fs.rmSync(socketDirectory, { recursive: true, force: true }); + } catch (error) { + cleanupDiagnostic(log, 'startup rollback: rendezvous cleanup failed', error); + } + } +} + +/** + * Starts the production-local DSHX presentation transport. + * + * The official DeepSeek Harness runtime remains in-process in Node. The pinned + * Codex TUI connects only to a private cross-platform Unix-domain socket owned + * by the tiny packaged Rust bridge. JSON-RPC is relayed between that bridge and + * this adapter over child stdio; no TCP listener or Codex remote WebSocket mode + * exists in the production launch path. + */ +export async function startDshxLocalServer({ + cwd = process.cwd(), + home, + version = '0.1.0-dev', + runtime, + bootRuntime = bootDshxRuntime, + Adapter = DshxReleaseAdapter, + log = () => {}, + bridgeCommand = packagedBridgeBinary(), + bridgeArgs, + spawnBridge = spawn, + socketRoot +} = {}) { + const rendezvousRoot = path.resolve(socketRoot ?? defaultSocketRoot({ home })); + let socketDirectory; + let socketPath; + let ctx = runtime; + let bridge; + let lines; + let adapter; + let closing = false; + let closePromise; + let stderr = ''; + + const send = (message) => { + if (!bridge?.stdin?.writable) throw new Error('DSHX IPC bridge stdin is not writable'); + bridge.stdin.write(`${JSON.stringify(message)}\n`); + }; + const safeSend = (message) => { + try { + send(message); + return true; + } catch (error) { + log(`IPC response send failed: ${error instanceof Error ? error.message : String(error)}`); + return false; + } + }; + + try { + socketDirectory = createSocketDirectory(rendezvousRoot); + socketPath = path.join(socketDirectory, 's'); + assertSocketPathSupported(socketPath); + ctx ??= await bootRuntime({ cwd }); + const args = bridgeArgs ?? [socketPath]; + bridge = spawnBridge(bridgeCommand, args, { + cwd, + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true + }); + bridge.stderr?.on('data', (chunk) => { + const text = chunk.toString('utf8'); + stderr = `${stderr}${text}`.slice(-8192); + if (process.env.DSHX_DEBUG === '1') log(`ipc bridge: ${text.trimEnd()}`); + }); + lines = createInterface({ input: bridge.stdout, crlfDelay: Infinity }); + await waitForBridgeReady(lines, bridge, () => stderr); + adapter = new Adapter({ + ctx, + cwd, + home, + version, + send, + diagnostics: log + }); + } catch (error) { + await cleanupFailedStartup({ lines, bridge, ctx, socketDirectory, log }); + throw error; + } + + lines.on('line', (line) => { + void (async () => { + let message; + try { + message = JSON.parse(line); + } catch (error) { + safeSend(parseError(error)); + return; + } + if (message?.dshxBridge) { + log(`IPC bridge control message: ${message.dshxBridge}`); + return; + } + try { + const handled = await adapter.handle(message); + if (!handled) return; + if (!safeSend(handled.response)) return; + await handled.afterResponse?.(); + } catch (error) { + log(`request handling failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}`); + if (message?.id !== undefined) { + safeSend({ + id: message.id, + error: { + code: -32603, + message: error instanceof Error ? error.message : String(error) + } + }); + } + } + })(); + }); + + bridge.on('error', (error) => log(`IPC bridge error: ${error.message}`)); + bridge.on('exit', (code, signal) => { + if (!closing) log(`IPC bridge exited unexpectedly (${signal ?? code ?? 'unknown'})`); + }); + + const close = () => { + if (closePromise) return closePromise; + closing = true; + closePromise = (async () => { + let primaryError; + const capture = (scope, error) => { + if (!primaryError) { + primaryError = error; + return; + } + cleanupDiagnostic(log, `shutdown cleanup: ${scope}`, error); + }; + + try { + lines?.close(); + } catch (error) { + capture('readline cleanup failed', error); + } + + try { + await adapter?.close?.(); + } catch (error) { + capture('adapter close failed', error); + } + + try { + if (bridge?.stdin?.writable) bridge.stdin.end(); + await waitForExit(bridge); + if (bridge && bridge.exitCode === null && bridge.signalCode === null) bridge.kill?.('SIGKILL'); + } catch (error) { + capture('bridge cleanup failed', error); + } + + try { + await ctx?.dispose?.(); + } catch (error) { + capture('DSH dispose failed', error); + } + + if (socketDirectory) { + try { + fs.rmSync(socketDirectory, { recursive: true, force: true }); + } catch (error) { + capture('rendezvous cleanup failed', error); + } + } + + if (primaryError) throw primaryError; + })(); + return closePromise; + }; + + return { + path: socketPath, + url: `unix://${socketPath}`, + context: ctx, + close + }; +} + +export const localServerInternals = { + WINDOWS_UNIX_PATH_MAX, + isPathWithin, + defaultSocketRoot, + createSocketDirectory, + assertSocketPathSupported, + cleanupFailedStartup +}; diff --git a/src/dsh/permissions.mjs b/src/dsh/permissions.mjs new file mode 100644 index 00000000..f1f1b4df --- /dev/null +++ b/src/dsh/permissions.mjs @@ -0,0 +1,106 @@ +function requireService(ctx, name) { + const service = ctx.get(name); + if (service === undefined) throw new Error(`DSHX requires DSH service: ${name}`); + return service; +} + +function codexApprovalPolicy(policy) { + if (policy === 'ask') return 'on-request'; + if (policy === 'never') return 'never'; + throw new Error(`Unsupported DSH approval policy: ${String(policy)}`); +} + +/** + * Project official DSH permission state into Codex-facing presentation data. + * + * DSH sandbox modes govern filesystem effects only; Codex legacy sandbox + * shapes also encode network policy. To avoid claiming Codex-native sandbox + * semantics that DSH does not own, confined DSH modes are advertised as an + * external sandbox. `legacyNetworkAccess` describes only the legacy Codex + * field and never changes DSH enforcement. + */ +export class DshPermissionView { + constructor(ctx, { legacyNetworkAccess = 'enabled' } = {}) { + if (!ctx || typeof ctx.get !== 'function') throw new Error('DshPermissionView requires a Cordis Context'); + if (!['enabled', 'restricted'].includes(legacyNetworkAccess)) { + throw new Error('legacyNetworkAccess must be "enabled" or "restricted"'); + } + this.ctx = ctx; + this.legacyNetworkAccess = legacyNetworkAccess; + } + + current(agent) { + if (!agent?.session) throw new Error('DshPermissionView.current requires a live DSH Agent'); + const presets = requireService(this.ctx, 'permissionPresets'); + const sandboxPolicy = requireService(this.ctx, 'sandboxPolicy'); + const approval = requireService(this.ctx, 'approval'); + const session = agent.session; + + const preset = presets.current(session.events); + const options = [ + ...presets.names.map((name) => presets.optionOf(name)), + ...(preset === 'custom' ? [presets.optionOf('custom')] : []) + ]; + const resolvedSandbox = sandboxPolicy.resolve({ session }); + const approvalPolicy = approval.overrideOf(session) ?? approval.config?.policy ?? 'ask'; + + let sandbox; + if (resolvedSandbox.mode === 'danger-full-access') { + sandbox = { type: 'dangerFullAccess' }; + } else if (resolvedSandbox.mode === 'read-only' || resolvedSandbox.mode === 'workspace-write') { + sandbox = { type: 'externalSandbox', networkAccess: this.legacyNetworkAccess }; + } else { + throw new Error(`Unsupported DSH sandbox mode: ${String(resolvedSandbox.mode)}`); + } + + return { + preset, + options, + dsh: { + sandboxMode: resolvedSandbox.mode, + workspaceRoot: resolvedSandbox.workspaceRoot, + approvalPolicy + }, + codex: { + approvalPolicy: codexApprovalPolicy(approvalPolicy), + approvalsReviewer: 'user', + sandbox + } + }; + } + + set(agent, presetName) { + if (!agent?.session) throw new Error('DshPermissionView.set requires a live DSH Agent'); + const presets = requireService(this.ctx, 'permissionPresets'); + presets.set(agent.session, presetName); + } +} + +export function mapDshApprovalOutcomeToCodexDecision(outcome) { + switch (outcome) { + case 'allowed-once': + return 'accept'; + case 'rejected': + case 'unavailable': + return 'decline'; + case 'cancelled': + return 'cancel'; + default: + throw new Error(`Unknown DSH approval outcome: ${String(outcome)}`); + } +} + +export function mapCodexDecisionToDshApprovalOutcome(decision) { + switch (decision) { + case 'accept': + return 'allowed-once'; + case 'decline': + return 'rejected'; + case 'cancel': + return 'cancelled'; + case 'acceptForSession': + throw new Error('DSH has no session-wide approval grant; DSHX must not offer acceptForSession'); + default: + throw new Error('Codex approval decision has no faithful DSH one-shot mapping'); + } +} diff --git a/src/dsh/plan-presentation.mjs b/src/dsh/plan-presentation.mjs new file mode 100644 index 00000000..e1d40893 --- /dev/null +++ b/src/dsh/plan-presentation.mjs @@ -0,0 +1,55 @@ +export function codexPlanTarget(collaborationMode) { + if (!collaborationMode || typeof collaborationMode !== 'object' || Array.isArray(collaborationMode)) { + throw new Error('DSHX collaborationMode must be a Codex collaboration mode object'); + } + if (collaborationMode.mode === 'plan') return true; + if (collaborationMode.mode === 'default') return false; + throw new Error(`DSHX cannot map Codex collaboration mode ${JSON.stringify(collaborationMode.mode)} to DSH plan mode`); +} + +export function effectiveDshPlanState(state) { + if (typeof state?.pending === 'boolean') return state.pending; + return Boolean(state?.active); +} + +export function codexCollaborationMode({ model, reasoningEffort, active }) { + return { + mode: active ? 'plan' : 'default', + settings: { + model, + reasoning_effort: reasoningEffort ?? null, + // DSH owns plan guidance. Never copy Codex developer instructions into + // the Harness request just to satisfy a presentation protocol shape. + developer_instructions: null + } + }; +} + +export function threadSettingsUpdatedNotification({ threadId, response, planState }) { + const active = effectiveDshPlanState(planState); + return { + method: 'thread/settings/updated', + params: { + threadId, + threadSettings: { + cwd: response.cwd, + approvalPolicy: response.approvalPolicy, + approvalsReviewer: response.approvalsReviewer, + sandboxPolicy: response.sandbox, + activePermissionProfile: response.activePermissionProfile ?? null, + model: response.model, + modelProvider: response.modelProvider, + serviceTier: response.serviceTier ?? null, + effort: response.reasoningEffort ?? null, + summary: null, + collaborationMode: codexCollaborationMode({ + model: response.model, + reasoningEffort: response.reasoningEffort, + active + }), + multiAgentMode: response.multiAgentMode ?? 'explicitRequestOnly', + personality: null + } + } + }; +} diff --git a/src/dsh/product-adapter.mjs b/src/dsh/product-adapter.mjs new file mode 100644 index 00000000..2f8a089f --- /dev/null +++ b/src/dsh/product-adapter.mjs @@ -0,0 +1,415 @@ +import path from 'node:path'; +import { DshAppServerAdapter } from './app-server-adapter.mjs'; +import { executeDshCommand } from './commands.mjs'; +import { dshThreadItemsPage, dshThreadTurnsPage } from './history-pages.mjs'; +import { DshHostApi } from './host-api.mjs'; +import { codexForkAtSeq } from './session-fork.mjs'; +import { dshSkillsListEntry } from './skills.mjs'; +import { persistedTokenUsageNotification } from './token-usage.mjs'; + +function textInput(params) { + const inputs = Array.isArray(params?.input) ? params.input : []; + const unsupported = inputs.filter((item) => item?.type !== 'text'); + if (unsupported.length > 0) { + throw new Error(`DSHX steer currently supports Codex text items only; got ${unsupported.map((item) => item?.type).join(', ')}`); + } + return inputs.map((item) => item.text ?? '').join('\n').trim(); +} + +function dshPermissionPreset(ctx, codexId) { + const presets = ctx.get('permissionPresets'); + if (!presets) throw new Error('DSHX requires DSH service: permissionPresets'); + const mapped = codexId === ':workspace' + ? 'workspace-write' + : codexId === ':danger-full-access' + ? 'danger-full-access' + : codexId === ':read-only' + ? 'read-only' + : codexId; + if (!presets.names.includes(mapped)) { + throw new Error(`DSH has no permission preset corresponding to Codex profile ${JSON.stringify(codexId)}`); + } + return mapped; +} + +function codexPermissionProfileId(preset) { + if (preset === 'workspace-write') return ':workspace'; + if (preset === 'danger-full-access') return ':danger-full-access'; + if (preset === 'read-only') return ':read-only'; + if (preset === 'custom' || preset == null) return null; + return preset; +} + +function hasSupportedThreadSetting(params) { + return params.model != null + || params.effort != null + || params.permissions != null + || params.approvalPolicy != null; +} + +/** + * Product-level protocol tail on top of the stable DSH public API adapter. + * Keep feature-specific compatibility decisions here instead of widening the + * runtime-owning base: every method still delegates to official DSH services. + */ +export class DshxProductAdapter extends DshAppServerAdapter { + async dispatch(method, params) { + switch (method) { + case 'account/usage/read': + return this.accountUsageRead(params); + case 'skills/list': + return this.skillsList(params); + case 'thread/compact/start': + return this.threadCompactStart(params); + case 'thread/fork': + return this.threadFork(params); + case 'thread/turns/list': + return this.threadTurnsList(params); + case 'thread/items/list': + return this.threadItemsList(params); + case 'thread/settings/update': + return this.threadSettingsUpdate(params); + case 'thread/unsubscribe': + return this.threadUnsubscribeWithStatus(params); + case 'turn/interrupt': + return this.turnInterruptProduct(params); + case 'turn/steer': + return this.turnSteer(params); + default: + return super.dispatch(method, params); + } + } + + accountUsageRead(_params = {}) { + return { + result: { + summary: { + lifetimeTokens: null, + peakDailyTokens: null, + longestRunningTurnSec: null, + currentStreakDays: null, + longestStreakDays: null + }, + dailyUsageBuckets: null, + threadUsage: null + } + }; + } + + manualCompactions() { + return this._manualCompactions ??= new Map(); + } + + hostApi() { + return this._hostApi ??= new DshHostApi(this.ctx, { cwd: this.cwd }); + } + + warnThread(threadId, message) { + this.send({ method: 'warning', params: { threadId, message } }); + } + + threadResponse(agent, options = {}) { + const response = super.threadResponse(agent, options); + const permission = this.permissions.current(agent); + const id = codexPermissionProfileId(permission.preset); + return { + ...response, + runtimeWorkspaceRoots: [], + activePermissionProfile: id == null ? null : { id, extends: null }, + multiAgentMode: 'explicitRequestOnly', + initialTurnsPage: null, + turnsBackwardsCursor: null, + itemsBackwardsCursor: null + }; + } + + async applyStartPermissions(agent, params = {}) { + if (params.permissions == null) return super.applyStartPermissions(agent, params); + if (params.sandbox != null) { + throw new Error('Codex thread/start permissions and sandbox cannot both be selected for DSHX'); + } + const preset = dshPermissionPreset(this.ctx, String(params.permissions)); + this.permissions.set(agent, preset); + const current = this.permissions.current(agent); + if (params.approvalPolicy != null && current.codex.approvalPolicy !== params.approvalPolicy) { + throw new Error( + `DSH preset ${preset} does not match requested Codex approval policy ${JSON.stringify(params.approvalPolicy)}` + ); + } + return current; + } + + async skillsList(params = {}) { + if (params.forceReload === true) { + this.diagnostics('Codex requested skills forceReload; DSHX is reading the current DSH registry snapshot without overriding provider-owned cache policy'); + } + const cwds = Array.isArray(params.cwds) && params.cwds.length > 0 + ? params.cwds.map((cwd) => path.resolve(cwd)) + : [this.cwd]; + const data = await Promise.all(cwds.map((cwd) => dshSkillsListEntry({ + ctx: this.ctx, + cwd, + diagnostics: this.diagnostics + }))); + return { result: { data } }; + } + + threadCompactStart(params = {}) { + const threadId = String(params.threadId ?? ''); + const controller = this.controllers.get(threadId); + if (!controller) throw new Error(`Thread is not resumed in DSHX: ${threadId}`); + const active = this.manualCompactions(); + if (active.has(threadId)) { + throw new Error('Compaction is unavailable because this thread already has an active manual compaction'); + } + const abortController = new AbortController(); + active.set(threadId, abortController); + return { + result: {}, + afterResponse: () => { + void this.runManualCompaction({ threadId, controller, abortController }); + } + }; + } + + async runManualCompaction({ threadId, controller, abortController }) { + try { + const execution = await executeDshCommand({ + ctx: controller.agent.ctx ?? this.ctx, + agent: controller.agent, + line: '/compact', + signal: abortController.signal + }); + if (execution.result?.sourceEventSeq == null && execution.result?.text) { + this.warnThread(threadId, execution.result.text); + } + } catch (error) { + if (abortController.signal.aborted) { + this.warnThread(threadId, 'Compaction cancelled.'); + } else { + const message = error instanceof Error ? error.message : String(error); + this.warnThread(threadId, message); + this.diagnostics(`manual compaction failed for ${threadId}: ${message}`); + } + } finally { + if (this.manualCompactions().get(threadId) === abortController) { + this.manualCompactions().delete(threadId); + } + } + } + + turnInterruptProduct(params = {}) { + const threadId = String(params.threadId ?? ''); + if (String(params.turnId ?? '').startsWith('dsh-maintenance-compaction-')) { + const active = this.manualCompactions().get(threadId); + if (active) { + active.abort(new Error('user cancelled DSH compaction')); + return { result: {} }; + } + } + return super.turnInterrupt(params); + } + + async threadResume(params = {}) { + if (!params.threadId) throw new Error('thread/resume requires threadId'); + const threadId = String(params.threadId); + let controller = this.controllers.get(threadId); + if (!controller) { + const live = this.driver.getLive(threadId); + const handle = live + ? { agent: live, dispose: async () => {} } + : await this.driver.resume(threadId); + controller = this.installController(handle); + } + const agent = controller.agent; + if (params.cwd != null && path.resolve(params.cwd) !== path.resolve(agent.session.header?.cwd ?? this.cwd)) { + throw new Error('DSHX does not override a persisted DSH session cwd during resume'); + } + await this.applyModelOverride(agent, params); + await this.applyStartPermissions(agent, params); + const result = this.threadResponse(agent, { + includeTurns: params.excludeTurns !== true + }); + const replay = persistedTokenUsageNotification({ + ctx: this.ctx, + session: agent.session, + threadId + }); + return { + result, + afterResponse: replay ? () => this.send(replay) : undefined + }; + } + + async forkSourceEvents(sourceId) { + const live = this.controllers.get(sourceId)?.agent ?? this.driver.getLive(sourceId); + if (live?.session) return live.session.events ?? []; + const inspected = await this.driver.inspectSession(sourceId); + if (!inspected) throw new Error(`Unknown DSH session: ${sourceId}`); + return inspected.events ?? []; + } + + async threadFork(params = {}) { + const sourceId = String(params.threadId ?? ''); + if (!sourceId) throw new Error('thread/fork requires threadId'); + if (params.path != null) throw new Error('DSHX forks DSH sessions by threadId, not Codex rollout paths'); + if (params.deferGoalContinuation === true) { + throw new Error('DSH has no Codex thread-goal continuation state for DSHX to defer'); + } + if (params.ephemeral === true) { + throw new Error('DSHX does not expose ephemeral Codex forks over durable DSH sessions'); + } + + const events = await this.forkSourceEvents(sourceId); + const atSeq = codexForkAtSeq(events, { + lastTurnId: params.lastTurnId, + beforeTurnId: params.beforeTurnId + }); + const forked = await this.hostApi().forkSession({ sessionId: sourceId, atSeq }); + const childId = String(forked?.sessionId ?? ''); + if (!childId) throw new Error('DSH Host fork returned no child session id'); + + const childAgent = this.driver.getLive(childId); + if (!childAgent) throw new Error(`DSH Host fork did not publish child Agent ${childId}`); + this.driver.adoptExternalSelection(childAgent, { + select: (selection) => this.hostApi().selectModel({ sessionId: childId, ...selection }) + }); + const controller = this.installController({ agent: childAgent, dispose: async () => {} }); + const result = this.threadResponse(controller.agent, { + includeTurns: params.excludeTurns !== true + }); + return { + result, + afterResponse: () => this.send({ method: 'thread/started', params: { thread: result.thread } }) + }; + } + + historyController(threadId) { + const controller = this.controllers.get(String(threadId ?? '')); + if (!controller) { + throw new Error(`Thread must be resumed before paginated DSH history is rendered: ${String(threadId ?? '')}`); + } + return controller; + } + + threadTurnsList(params = {}) { + const controller = this.historyController(params.threadId); + return { + result: dshThreadTurnsPage({ + controller, + params, + diagnostics: this.diagnostics + }) + }; + } + + threadItemsList(params = {}) { + const controller = this.historyController(params.threadId); + return { + result: dshThreadItemsPage({ + controller, + params, + diagnostics: this.diagnostics + }) + }; + } + + async threadUnsubscribeWithStatus(params = {}) { + const threadId = String(params.threadId ?? ''); + this.manualCompactions().get(threadId)?.abort(new Error('thread unsubscribed')); + const subscribed = this.controllers.has(threadId); + const loaded = Boolean(this.driver.getLive(threadId)); + await super.threadUnsubscribe(params); + return { + result: { + status: subscribed ? 'unsubscribed' : loaded ? 'notSubscribed' : 'notLoaded' + } + }; + } + + async saveDefaultModelSelection(selection) { + const defaults = this.ctx.get('agentDefaultModel'); + if (!defaults?.saveSelection) { + this.diagnostics('DSH agentDefaultModel service has no saveSelection(); current session selection remains valid but deployment default was not persisted'); + return; + } + await defaults.saveSelection({ + provider: selection.provider, + model: selection.model, + ...(selection.reasoningEffort == null ? {} : { reasoningEffort: selection.reasoningEffort }) + }); + } + + async threadSettingsUpdate(params = {}) { + const threadId = String(params.threadId ?? ''); + const controller = this.controllers.get(threadId); + if (!controller) throw new Error(`Thread is not resumed in DSHX: ${threadId}`); + const agent = controller.agent; + + if (params.cwd != null) { + const currentCwd = path.resolve(agent.session.header?.cwd ?? this.cwd); + if (path.resolve(params.cwd) !== currentCwd) { + throw new Error('DSHX does not retarget a live DSH session cwd from the TUI'); + } + } + if (params.sandboxPolicy != null) { + throw new Error('DSHX changes permissions through DSH named permission presets, not Codex legacy sandboxPolicy'); + } + if (params.serviceTier !== undefined) { + throw new Error('DSH has no public thread service-tier setter for DSHX to project'); + } + if (params.summary != null) { + throw new Error('DSH has no equivalent public reasoning-summary thread setting'); + } + if (params.personality != null) { + throw new Error('DSH has no equivalent public personality thread setting'); + } + if (params.approvalsReviewer != null && params.approvalsReviewer !== 'user') { + throw new Error('DSH approval review is user-owned; DSHX cannot select a Codex-specific reviewer'); + } + + const supported = hasSupportedThreadSetting(params); + if (params.collaborationMode != null && !supported) { + throw new Error('DSH has no equivalent public collaboration-mode thread setting'); + } + + if (params.permissions != null || params.approvalPolicy != null) { + await this.applyStartPermissions(agent, { + permissions: params.permissions, + approvalPolicy: params.approvalPolicy + }); + } + if (params.model != null || params.effort != null) { + const selection = await this.applyModelOverride(agent, { + model: params.model, + effort: params.effort + }); + await this.saveDefaultModelSelection(selection); + } + + return { result: {} }; + } + + turnSteer(params = {}) { + const threadId = String(params.threadId ?? ''); + const controller = this.controllers.get(threadId); + if (!controller) throw new Error(`Thread is not resumed in DSHX: ${threadId}`); + const location = controller.currentLocation(); + if (!location) throw new Error(`Thread has no active DSH turn to steer: ${threadId}`); + if (typeof params.expectedTurnId !== 'string' || params.expectedTurnId !== location.turnId) { + throw new Error( + `DSHX steer expectedTurnId ${JSON.stringify(params.expectedTurnId)} does not match active DSH turn ${location.turnId}` + ); + } + controller.steer(textInput(params)); + return { result: { turnId: location.turnId } }; + } + + async close() { + for (const controller of this.manualCompactions().values()) { + controller.abort(new Error('DSHX adapter closing')); + } + this.manualCompactions().clear(); + await super.close(); + } +} diff --git a/src/dsh/release-adapter.mjs b/src/dsh/release-adapter.mjs new file mode 100644 index 00000000..3694a17e --- /dev/null +++ b/src/dsh/release-adapter.mjs @@ -0,0 +1,437 @@ +import path from 'node:path'; +import { decodeDshModel, dshThreadFromSnapshot, normalizeCodexEffort } from './codex-shapes.mjs'; +import { DshxProductAdapter } from './product-adapter.mjs'; +import { codexPlanTarget, threadSettingsUpdatedNotification } from './plan-presentation.mjs'; +import { foldDshSessionTitle, threadNameUpdatedNotification } from './thread-title.mjs'; +import { codexInputToDshContent, dshContentText } from './user-input.mjs'; +import { DshUserShellBridge } from './user-shell.mjs'; +import { DshWorkspaceCommandBridge } from './workspace-command.mjs'; + +function snapshotTitle(snapshot) { + return typeof snapshot?.title === 'string' && snapshot.title.length > 0 ? snapshot.title : null; +} + +function isDshSubagent(agent) { + return agent?.session?.header?.origin === 'subagent'; +} + +/** + * Final product-facing protocol tail. Keep small metadata/UI compatibility + * extensions here so the core DSH public adapter remains stable and auditable. + */ +export class DshxReleaseAdapter extends DshxProductAdapter { + async ensureReady() { + await super.ensureReady(); + if (this._agentPresentationDisposers) return; + this._agentPresentationDisposers = [ + this.ctx.on('agent/created', ({ agent }) => { + if (!isDshSubagent(agent)) return; + try { + this.send({ method: 'thread/started', params: { thread: this.liveAgentThread(agent) } }); + } catch (error) { + this.diagnostics(`subagent thread/started projection failed for ${String(agent?.id ?? '')}: ${error instanceof Error ? error.message : error}`); + } + }), + this.ctx.on('agent/status', ({ agent, status }) => { + try { + this.send({ + method: 'thread/status/changed', + params: { + threadId: String(agent.id), + status: status === 'running' ? { type: 'active', activeFlags: [] } : { type: 'idle' } + } + }); + } catch (error) { + this.diagnostics(`agent status projection failed for ${String(agent?.id ?? '')}: ${error instanceof Error ? error.message : error}`); + } + }), + this.ctx.on('agent/disposed', ({ agent }) => { + try { + this.send({ + method: 'thread/status/changed', + params: { threadId: String(agent.id), status: { type: 'notLoaded' } } + }); + } catch (error) { + this.diagnostics(`agent disposal projection failed for ${String(agent?.id ?? '')}: ${error instanceof Error ? error.message : error}`); + } + }) + ]; + } + + userShell() { + return this._userShell ??= new DshUserShellBridge({ + send: this.send, + diagnostics: this.diagnostics + }); + } + + workspaceCommands() { + return this._workspaceCommands ??= new DshWorkspaceCommandBridge({ driver: this.driver }); + } + + planListeners() { + return this._planListeners ??= new Map(); + } + + planMutationSuppressed() { + return this._planMutationSuppressed ??= new Set(); + } + + planMode(agent) { + const service = agent?.ctx?.get?.('planMode') ?? this.ctx.get('planMode'); + if (!service?.set || !service?.get) throw new Error('DSHX requires DSH service: planMode'); + return service; + } + + planSettingsNotification(agent) { + const response = this.threadResponse(agent); + return threadSettingsUpdatedNotification({ + threadId: String(agent.id), + response, + planState: this.planMode(agent).get(agent) + }); + } + + liveAgentThread(agent) { + const thread = dshThreadFromSnapshot({ + meta: agent.session.header, + events: agent.session.events ?? [], + model: agent.session.requestHeader?.()?.config, + loaded: true, + cliVersion: this.version + }); + const title = snapshotTitle(this.driver.currentTitle(agent)) ?? foldDshSessionTitle(agent.session.events ?? []); + return { + ...thread, + status: agent.status === 'running' ? { type: 'active', activeFlags: [] } : { type: 'idle' }, + name: title, + turns: [] + }; + } + + loadedThreadList() { + // Pinned Codex ThreadLoadedListResponse.data is Array. Keep rich + // thread metadata on thread/list/read/started and expose only live IDs here. + return { + result: { + data: this.driver.listLive().map((agent) => String(agent.id)) + } + }; + } + + directController(threadId, operation) { + const controller = this.controllers.get(String(threadId ?? '')); + if (!controller) throw new Error(`Thread is not resumed in DSHX: ${String(threadId ?? '')}`); + if (isDshSubagent(controller.agent)) { + throw new Error(`DSHX refuses ${operation} on a DSH subagent; child runtime control remains owned by ctx.subagents`); + } + return controller; + } + + async interruptSubagent(controller) { + const parentSessionId = controller.agent.session.header?.parentSession; + if (parentSessionId == null) throw new Error('DSH subagent is missing durable parentSession authority'); + const subagents = this.ctx.get('subagents'); + if (!subagents?.interrupt) throw new Error('DSHX requires DSH service: subagents.interrupt'); + await subagents.interrupt(String(controller.agent.id), { + kind: 'user', + parentSessionId: String(parentSessionId) + }); + return { result: {} }; + } + + installController(handle) { + const controller = super.installController(handle); + const threadId = String(controller.agent.id); + if (!this.planListeners().has(threadId)) { + const dispose = controller.agent.ctx.on('session/event', (session, event) => { + if (session !== controller.agent.session || event?.type !== 'plan/mode') return; + if (this.planMutationSuppressed().has(threadId)) return; + try { + this.send(this.planSettingsNotification(controller.agent)); + } catch (error) { + this.diagnostics(`plan settings projection failed for ${threadId}: ${error instanceof Error ? error.message : error}`); + } + }); + this.planListeners().set(threadId, dispose); + } + return controller; + } + + async dispatch(method, params) { + switch (method) { + case 'command/exec': + return this.commandExec(params); + case 'config/batchWrite': + return this.configBatchWrite(params); + case 'thread/loaded/list': + return this.loadedThreadList(); + case 'thread/fork': + return this.threadForkPresentation(params); + case 'thread/name/set': + return this.threadNameSet(params); + case 'thread/settings/update': + return this.threadSettingsUpdatePresentation(params); + case 'thread/shellCommand': + return this.threadShellCommand(params); + case 'thread/compact/start': + this.directController(params?.threadId, 'direct compaction'); + return super.dispatch(method, params); + case 'turn/start': + case 'turn/steer': + return this.richUserTurn(method, params); + case 'turn/interrupt': { + if (this.userShell().interrupt(params?.threadId, params?.turnId)) return { result: {} }; + const controller = this.controllers.get(String(params?.threadId ?? '')); + if (controller && isDshSubagent(controller.agent)) return this.interruptSubagent(controller); + return super.dispatch(method, params); + } + case 'thread/unsubscribe': { + const threadId = String(params?.threadId ?? ''); + this.userShell().abortThread(threadId, 'thread unsubscribed'); + this.planListeners().get(threadId)?.(); + this.planListeners().delete(threadId); + this.planMutationSuppressed().delete(threadId); + return super.dispatch(method, params); + } + default: + return super.dispatch(method, params); + } + } + + async commandExec(params = {}) { + return { result: await this.workspaceCommands().execute(params) }; + } + + async configBatchWrite(params = {}) { + const edits = Array.isArray(params.edits) ? params.edits : []; + if (edits.length === 0) throw new Error('DSHX config/batchWrite requires at least one model-selection edit'); + + const supported = new Set(['model', 'model_reasoning_effort']); + for (const edit of edits) { + if (!supported.has(edit?.keyPath)) { + throw new Error(`DSHX config/batchWrite refuses Codex-owned setting: ${String(edit?.keyPath ?? '')}`); + } + if (edit?.mergeStrategy != null && edit.mergeStrategy !== 'replace') { + throw new Error(`DSHX config/batchWrite supports replace semantics only: ${String(edit.keyPath)}`); + } + } + + const defaults = this.ctx.get('agentDefaultModel'); + const llm = this.ctx.get('llm'); + if (!defaults?.currentSelection || !defaults?.saveSelection) { + throw new Error('DSHX requires DSH service: agentDefaultModel.saveSelection'); + } + if (!llm?.resolveCallConfig) throw new Error('DSHX requires DSH service: llm.resolveCallConfig'); + + const current = defaults.currentSelection(); + let requested = { provider: current.provider, model: current.model }; + if (current.reasoningEffort !== undefined) requested.reasoningEffort = current.reasoningEffort; + + const modelEdit = edits.find((edit) => edit.keyPath === 'model'); + if (modelEdit) { + const decoded = decodeDshModel(modelEdit.value); + if (!decoded) throw new Error('DSHX persists default models only from its DSH-backed opaque model catalog'); + requested = { ...decoded }; + } + + const effortEdit = edits.find((edit) => edit.keyPath === 'model_reasoning_effort'); + if (effortEdit) { + const effort = effortEdit.value == null ? undefined : normalizeCodexEffort(effortEdit.value); + requested = { provider: requested.provider, model: requested.model }; + if (effort !== undefined) requested.reasoningEffort = effort; + } + + const resolved = await llm.resolveCallConfig(requested); + const persisted = { provider: resolved.provider, model: resolved.model }; + // Preserve provider-default semantics when the TUI clears its reasoning + // setting; otherwise persist the DSH-resolved explicit effort. + if (requested.reasoningEffort !== undefined) { + persisted.reasoningEffort = resolved.reasoningEffort ?? requested.reasoningEffort; + } + await defaults.saveSelection(persisted); + + this._configRevision = (this._configRevision ?? 0) + 1; + return { + result: { + status: 'ok', + version: `dshx-${this._configRevision}`, + // Codex requires an absolute path in ConfigWriteResponse. Persistence is + // actually owned by DSH settings; this marker stays inside presentation home. + filePath: path.join(this.home, 'dsh-settings'), + overriddenMetadata: null + } + }; + } + + async richUserTurn(method, params = {}) { + const threadId = String(params.threadId ?? ''); + const controller = this.controllers.get(threadId); + if (!controller) return super.dispatch(method, params); + if (isDshSubagent(controller.agent)) { + throw new Error(`DSHX refuses ${method} on a DSH subagent; send/steer authority remains owned by ctx.subagents`); + } + const content = await codexInputToDshContent(this.ctx, params.input ?? []); + const clear = controller.prepareUserContent(content); + try { + // Pinned Codex serializes an unset reasoning effort as JSON null. DSH + // interprets absence as provider default, so never forward null as an + // explicit provider effort. + const normalizedParams = params.effort == null ? { ...params, effort: undefined } : params; + // Preserve the original base/product validation path. The fallback text is + // consumed only if no prepared rich content exists; it also keeps the + // base adapter's expected text-only parameter shape intact. + return await super.dispatch(method, { + ...normalizedParams, + input: [{ type: 'text', text: dshContentText(content) }] + }); + } finally { + clear(); + } + } + + threadResponse(agent, options = {}) { + const response = super.threadResponse(agent, options); + const folded = this.driver.currentTitle(agent); + const name = snapshotTitle(folded) ?? foldDshSessionTitle(agent.session?.events ?? []); + return { + ...response, + thread: { ...response.thread, name } + }; + } + + async threadResume(params = {}) { + const response = await super.threadResume(params); + const threadId = String(params.threadId ?? response.result.thread?.id ?? ''); + const agent = this.controllers.get(threadId)?.agent; + const prior = response.afterResponse; + return { + ...response, + afterResponse: () => { + prior?.(); + if (agent) this.send(this.planSettingsNotification(agent)); + } + }; + } + + async threadForkPresentation(params = {}) { + const response = await super.threadFork(params); + const startedThread = { ...response.result.thread, turns: [] }; + const agent = this.controllers.get(String(response.result.thread.id))?.agent; + return { + ...response, + // Pinned Codex emits copied history only in the fork response. The + // thread/started notification introduces metadata/live state and must not + // replay those copied turns a second time before paginated hydration. + afterResponse: () => { + this.send({ + method: 'thread/started', + params: { thread: startedThread } + }); + if (agent) this.send(this.planSettingsNotification(agent)); + } + }; + } + + async threadSettingsUpdatePresentation(params = {}) { + const threadId = String(params.threadId ?? ''); + const controller = this.controllers.get(threadId); + if (controller && isDshSubagent(controller.agent)) { + throw new Error('DSHX refuses direct model/permission/plan changes on a DSH subagent; composition remains DSH-owned'); + } + if (params.collaborationMode == null) return super.threadSettingsUpdate(params); + if (!controller) throw new Error(`Thread is not resumed in DSHX: ${threadId}`); + const active = codexPlanTarget(params.collaborationMode); + const { collaborationMode: _ignored, ...ordinary } = params; + + // Let the existing DSH-backed settings adapter validate/apply every other + // supported field. The collaboration payload's model/reasoning/instructions + // are intentionally not treated as DSH model or prompt configuration. + await super.threadSettingsUpdate(ordinary); + + this.planMutationSuppressed().add(threadId); + try { + await this.planMode(controller.agent).set(controller.agent, active); + } finally { + this.planMutationSuppressed().delete(threadId); + } + + return { + result: {}, + afterResponse: () => this.send(this.planSettingsNotification(controller.agent)) + }; + } + + async threadList(params = {}) { + const response = await super.threadList(params); + const data = await Promise.all(response.result.data.map(async (thread) => { + try { + const snapshot = await this.driver.readTitle(thread.id); + return { ...thread, name: snapshotTitle(snapshot) ?? thread.name ?? null }; + } catch (error) { + this.diagnostics(`title read ${thread.id}: ${error instanceof Error ? error.message : error}`); + return thread; + } + })); + return { result: { ...response.result, data } }; + } + + async threadRead(params = {}) { + const response = await super.threadRead(params); + const thread = response.result.thread; + try { + const snapshot = await this.driver.readTitle(thread.id); + return { + result: { + ...response.result, + thread: { ...thread, name: snapshotTitle(snapshot) ?? thread.name ?? null } + } + }; + } catch (error) { + this.diagnostics(`title read ${thread.id}: ${error instanceof Error ? error.message : error}`); + return response; + } + } + + threadShellCommand(params = {}) { + const controller = this.directController(params.threadId, 'direct shell execution'); + return this.userShell().start(controller, params.command); + } + + async threadNameSet(params = {}) { + const threadId = String(params.threadId ?? ''); + if (!threadId) throw new Error('thread/name/set requires threadId'); + if (typeof params.name !== 'string') throw new Error('thread/name/set requires name'); + + let temporaryHandle; + let agent = this.controllers.get(threadId)?.agent ?? this.driver.getLive(threadId); + if (!agent) { + temporaryHandle = await this.driver.resume(threadId); + agent = temporaryHandle.agent; + } + + try { + const snapshot = this.driver.renameTitle(agent, params.name); + const threadName = snapshotTitle(snapshot); + if (threadName == null) throw new Error('DSH sessionTitle.rename returned no accepted title'); + return { + result: {}, + afterResponse: () => this.send(threadNameUpdatedNotification(threadId, threadName)) + }; + } finally { + await temporaryHandle?.dispose?.(); + } + } + + async close() { + this._userShell?.close(); + for (const dispose of this._agentPresentationDisposers ?? []) dispose?.(); + this._agentPresentationDisposers = []; + for (const dispose of this.planListeners().values()) dispose?.(); + this.planListeners().clear(); + this.planMutationSuppressed().clear(); + await super.close(); + } +} + +export const releaseAdapterInternals = { snapshotTitle, isDshSubagent }; \ No newline at end of file diff --git a/src/dsh/runtime-boot.mjs b/src/dsh/runtime-boot.mjs new file mode 100644 index 00000000..44ad800b --- /dev/null +++ b/src/dsh/runtime-boot.mjs @@ -0,0 +1,234 @@ +import { writeFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { dirname, join } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { + boot, + composeEntries, + healProfilesModuleFallback, + loadLayeredEnv, + loadOptionalPatches, + loadProfile, + PROFILE_PATCH_FILENAME, + watchUserPatches +} from '@deepseek-ai/dsh-app-boot'; +import { DSH_LAUNCH_ENVIRONMENT_KEY } from '@deepseek-ai/dsh-launch-environment'; + +const NAME = 'dshx'; +const DSH_PROFILE_BIN_NAME = 'dsh'; +const DEFAULT_PROFILE = 'headless'; +const PROFILE_ROOT_FILENAME = 'cordis.yml'; +// Match the official `dsh` launcher's profile-root contract: this file is only +// the Loader/include base anchor. The real user-owned configuration is carried +// by the profile's `cordis.patch.yml` plus `$DSH_HOME/cordis.patch.yml`. +// Rewriting this empty root on every boot is intentional DSH behavior and +// prevents Loader tree write-back from becoming a second persisted config. +const PROFILE_ROOT_CONFIG = `# DSHX uses the same empty profile root contract as the official dsh launcher.\n# The effective tree is composed from DSH bundle and patch layers.\n# Edit cordis.patch.yml, not this file.\n[]\n`; +const DSHX_SURFACE_PATCHES = [ + // The official headless profile is the default because it is the DSH runtime + // without a browser surface. Its one-shot CLI startup/runner are themselves + // presentation rows, so DSHX replaces only those two rows with Codex TUI. + { id: 'headless-startup', disabled: true }, + { id: 'headless-runner', disabled: true } +]; + +function installationAnchor() { + return createRequire(import.meta.url).resolve('@deepseek-ai/dsh/package.json'); +} + +function selectedProfile(explicit) { + const value = explicit ?? process.env.DSHX_PROFILE ?? DEFAULT_PROFILE; + if (typeof value !== 'string' || value.trim().length === 0) { + throw new Error('dshx: DSH profile name must be non-empty'); + } + return value.trim(); +} + +function profileHome(profile) { + return dirname(dirname(profile.dir)); +} + +function homePatchPath(profile) { + return join(profileHome(profile), PROFILE_PATCH_FILENAME); +} + +function shippedAgentPresetPatch(entries, installAnchor) { + const agentPresets = entries.find((entry) => entry.id === 'agent-presets'); + if (!agentPresets) return []; + return [{ + id: 'agent-presets', + config: { + ...(agentPresets.config ?? {}), + roots: [{ path: join(dirname(installAnchor), 'config', 'agent-presets'), trust: 'system' }] + } + }]; +} + +function telemetryPatch(entries) { + if (!process.env.DSH_TELEMETRY_DISABLED) return []; + if (!entries.some((entry) => entry.id === 'session-telemetry-otel')) return []; + return [{ id: 'session-telemetry-otel', disabled: true }]; +} + +/** + * Load the exact official DSH profile layers that DSHX presents. + * + * Order remains DSH-owned: profile bundle layers -> profile user patch -> + * $DSH_HOME/cordis.patch.yml. DSHX then adds only installation-equivalent + * launcher patches and its presentation locks. Out-of-tree profile plugins + * resolve from the profile directory through DSH's own healed module fallback. + */ +export function dshxRuntimeProfile({ + profile: explicitProfile, + installAnchor = installationAnchor(), + home, + overlays = [] +} = {}) { + const profileName = selectedProfile(explicitProfile); + healProfilesModuleFallback(installAnchor, home); + const profile = loadProfile(DSH_PROFILE_BIN_NAME, profileName, installAnchor, home); + const rootConfig = join(profile.dir, PROFILE_ROOT_FILENAME); + // Official DSH deliberately rewrites this empty include root before boot. + // Never write `profile.patchPath` or the home patch here: those are the + // authoritative user configuration layers and are read-only to DSHX. + writeFileSync(rootConfig, PROFILE_ROOT_CONFIG); + + const bundlePatches = profile.layers.flatMap((layer) => layer.patches); + const homePatches = loadOptionalPatches(DSH_PROFILE_BIN_NAME, homePatchPath(profile)) ?? []; + const beforeLauncher = composeEntries([[...bundlePatches, ...profile.patches, ...homePatches]]); + const launcherPatches = [ + ...shippedAgentPresetPatch(beforeLauncher, installAnchor), + ...telemetryPatch(beforeLauncher), + ...structuredClone(overlays), + // Last on purpose: DSH user/profile configuration owns capabilities, but + // cannot re-enable a competing presentation runner in the DSHX process. + ...structuredClone(DSHX_SURFACE_PATCHES) + ]; + + return { + name: profileName, + profile, + rootConfig, + installAnchor, + homePatchPath: homePatchPath(profile), + patches: [ + ...structuredClone(bundlePatches), + ...structuredClone(profile.patches), + ...structuredClone(homePatches), + ...launcherPatches + ] + }; +} + +export function dshxRuntimePatches(options = {}) { + return dshxRuntimeProfile(options).patches; +} + +export function dshxRuntimeEntries(options = {}) { + return composeEntries([dshxRuntimePatches(options)]); +} + +async function installProfileWatchers(ctx, options) { + if (ctx.get('loader') == null) { + throw new Error('dshx: official DSH profile boot has no Loader for profile hot reload'); + } + if (ctx.get('hmr') == null) { + if (ctx.get('timer') == null) { + await ctx.loader.create({ name: '@deepseek-ai/cordis-plugin-timer' }); + } + await ctx.loader.create({ name: '@deepseek-ai/cordis-plugin-hmr', config: { root: [] } }); + } + + const current = () => dshxRuntimeProfile(options).patches; + const composition = dshxRuntimeProfile(options); + await watchUserPatches(ctx, { + binName: NAME, + filename: composition.profile.patchPath, + compose: () => current() + }); + await watchUserPatches(ctx, { + binName: NAME, + filename: composition.homePatchPath, + compose: () => current() + }); +} + +async function disposeOfficialContext(ctx) { + if (!ctx) return; + if (typeof ctx.fiber?.dispose !== 'function') { + throw new Error('dshx: official DSH Context has no root fiber disposal contract'); + } + await ctx.fiber.dispose(); +} + +function attachPresentationLifetime(ctx) { + // Cordis Context intentionally owns teardown through its root Fiber rather + // than a Context.dispose() method. DSHX keeps its launcher/local-server + // lifetime API small by exposing a non-enumerable alias that delegates to + // that official root-fiber contract; no runtime service is replaced. + Object.defineProperty(ctx, 'dispose', { + configurable: true, + enumerable: false, + value: () => disposeOfficialContext(ctx) + }); + return ctx; +} + +/** + * Boot the selected official Harness profile in-process and return its root Context. + * DSHX owns only presentation lifetime. Project and DSH-home environment and + * live patch behavior intentionally follow the official DSH profile launcher. + */ +export async function bootDshxRuntime({ + cwd = process.cwd(), + profile, + environment = loadLayeredEnv(DSH_PROFILE_BIN_NAME, cwd), + overlays = [], + home, + watch = true +} = {}) { + const options = { profile, overlays, home }; + const composition = dshxRuntimeProfile(options); + let ctx; + try { + ctx = await boot( + NAME, + composition.rootConfig, + structuredClone(composition.patches), + (hostCtx) => { + hostCtx.provide(DSH_LAUNCH_ENVIRONMENT_KEY, environment); + }, + pathToFileURL(composition.installAnchor).href + ); + attachPresentationLifetime(ctx); + if (watch) await installProfileWatchers(ctx, options); + return ctx; + } catch (error) { + if (ctx) { + try { + await disposeOfficialContext(ctx); + } catch (disposeError) { + if (error && typeof error === 'object') { + try { Object.defineProperty(error, 'dshxCleanupError', { value: disposeError }); } catch {} + } + } + } + throw error; + } +} + +export const runtimeInternals = { + NAME, + DSH_PROFILE_BIN_NAME, + DEFAULT_PROFILE, + PROFILE_ROOT_FILENAME, + PROFILE_ROOT_CONFIG, + DSHX_SURFACE_PATCHES, + installationAnchor, + selectedProfile, + profileHome, + homePatchPath, + installProfileWatchers, + disposeOfficialContext, + attachPresentationLifetime +}; diff --git a/src/dsh/session-fork.mjs b/src/dsh/session-fork.mjs new file mode 100644 index 00000000..0482b66e --- /dev/null +++ b/src/dsh/session-fork.mjs @@ -0,0 +1,55 @@ +function turnNumber(turnId) { + const match = /^dsh-turn-(\d+)$/.exec(String(turnId ?? '')); + if (!match) throw new Error(`DSHX cannot map Codex turn id to DSH turn: ${JSON.stringify(turnId)}`); + const value = Number(match[1]); + if (!Number.isSafeInteger(value) || value < 0) throw new Error(`Invalid DSH turn id: ${turnId}`); + return value; +} + +function completedTurnEnd(events, turn) { + return (events ?? []).find((event) => event?.type === 'turn/end' && event.data?.turn === turn); +} + +/** + * Translate Codex's fork presentation anchors into the one `atSeq` hint owned + * by DSH Host `sessions.fork`. This function deliberately does NOT select or + * copy a seed, decide lineage, inherit model state, or create a Session. + */ +export function codexForkAtSeq(events = [], { lastTurnId, beforeTurnId } = {}) { + if (lastTurnId != null && beforeTurnId != null) { + throw new Error('thread/fork lastTurnId and beforeTurnId are mutually exclusive'); + } + + if (lastTurnId != null) { + const turn = turnNumber(lastTurnId); + const end = completedTurnEnd(events, turn); + if (!end || !Number.isInteger(end.seq)) { + throw new Error(`DSH session has no completed ${lastTurnId}`); + } + return end.seq; + } + + if (beforeTurnId != null) { + const turn = turnNumber(beforeTurnId); + const start = (events ?? []).find((event) => event?.type === 'turn/start' && event.data?.turn === turn); + if (!start || !Number.isInteger(start.seq)) { + throw new Error(`DSH session has no ${beforeTurnId}`); + } + const priorEnds = (events ?? []).filter((event) => + event?.type === 'turn/end' + && Number.isInteger(event.seq) + && event.seq < start.seq + ); + const prior = priorEnds.at(-1); + if (!prior) { + throw new Error('DSH Host fork cannot represent a Codex beforeTurnId before the first completed turn'); + } + return prior.seq; + } + + // Omission is meaningful: the official DSH Host chooses the latest completed + // turn, including its own handling for live/cold sessions and open tails. + return undefined; +} + +export const forkInternals = { turnNumber, completedTurnEnd }; diff --git a/src/dsh/session-projector.mjs b/src/dsh/session-projector.mjs new file mode 100644 index 00000000..8fe644ab --- /dev/null +++ b/src/dsh/session-projector.mjs @@ -0,0 +1,427 @@ +const nowSeconds = () => Math.floor(Date.now() / 1000); +const nowMillis = () => Date.now(); + +function parseJsonOrString(value) { + if (typeof value !== 'string') return value ?? null; + try { return JSON.parse(value); } catch { return value; } +} + +function visibleText(message) { + return (message?.content ?? []) + .filter((block) => block?.type === 'text' && typeof block.text === 'string') + .map((block) => block.text) + .join(''); +} + +function reasoningContent(message) { + return (message?.content ?? []) + .filter((block) => block?.type === 'reasoning' && typeof block.text === 'string') + .map((block) => block.text); +} + +function toolResultCallId(data) { + const sourceId = data?.message?.source?.kind === 'tool' ? data.message.source.callId : undefined; + if (sourceId !== undefined) return sourceId; + const block = data?.message?.content?.find?.((entry) => entry?.type === 'tool-result'); + return block?.toolCallId; +} + +function toolResultFailed(data) { + const block = data?.message?.content?.find?.((entry) => entry?.type === 'tool-result'); + return Boolean(block?.isError ?? data?.error); +} + +function mapTurnStatus(reason) { + switch (reason?.kind) { + case 'completed': return 'completed'; + case 'aborted': + case 'interrupted': return 'interrupted'; + default: return 'failed'; + } +} + +function mapPlanStatus(status) { + if (status === 'in_progress') return 'inProgress'; + if (status === 'completed') return 'completed'; + return 'pending'; +} + +function turnRecord(id, status, startedAt, completedAt = null, error = null) { + return { + id, + items: [], + itemsView: 'full', + status, + error, + startedAt, + completedAt, + durationMs: startedAt != null && completedAt != null + ? Math.max(0, (completedAt - startedAt) * 1000) + : null + }; +} + +function compactionError(message) { + return message == null + ? null + : { message: String(message), codexErrorInfo: null, additionalDetails: null }; +} + +/** + * Stateful presentation projection of official DSH Session events. + * Durable truth stays in DSH; all maps here are disposable UI correlation. + */ +export class DshSessionProjector { + constructor({ threadId, sessionId = threadId, toolPresentation = null } = {}) { + if (!threadId) throw new Error('DshSessionProjector requires threadId'); + this.threadId = threadId; + this.sessionId = sessionId; + this.toolPresentation = toolPresentation; + this.turns = new Map(); + this.assistantItems = new Map(); + this.reasoningItems = new Map(); + this.tools = new Map(); + this.compactions = new Map(); + this.currentTurn = null; + this.latestHeader = null; + this.latestContext = null; + } + + project(event) { + if (!event || typeof event.type !== 'string') return []; + switch (event.type) { + case 'turn/start': return this.turnStarted(event); + case 'turn/end': return this.turnEnded(event); + case 'assistant/chunk': return this.assistantChunk(event); + case 'assistant/message': return this.assistantMessage(event); + case 'tool/call': return this.toolCall(event); + case 'tool/result': return this.toolResult(event); + case 'todo/write': return this.todoWrite(event); + case 'compaction/start': return this.compactionStarted(event); + case 'compaction/summary': return this.compactionSummarized(event); + case 'compaction/end': return this.compactionEnded(event); + case 'request/header': + this.latestHeader = event.data?.header ?? null; + return []; + case 'request/context': + this.latestContext = event.data ?? null; + return []; + default: + return []; + } + } + + turnStarted(event) { + const dshTurn = event.data?.turn; + if (!Number.isInteger(dshTurn)) return []; + const turnId = `dsh-turn-${dshTurn}`; + const startedAt = Number.isFinite(event.time) ? Math.floor(event.time / 1000) : nowSeconds(); + this.turns.set(dshTurn, { id: turnId, startedAt }); + this.currentTurn = dshTurn; + return [{ method: 'turn/started', params: { threadId: this.threadId, turn: turnRecord(turnId, 'inProgress', startedAt) } }]; + } + + turnEnded(event) { + const dshTurn = event.data?.turn; + const state = this.turns.get(dshTurn); + if (!state) return []; + const reason = event.data?.reason; + const status = mapTurnStatus(reason); + const completedAt = Number.isFinite(event.time) ? Math.floor(event.time / 1000) : nowSeconds(); + const error = status === 'failed' && reason?.kind === 'error' + ? { message: reason.error?.message ?? 'DSH turn failed', codexErrorInfo: null, additionalDetails: null } + : null; + if (this.currentTurn === dshTurn) this.currentTurn = null; + return [{ method: 'turn/completed', params: { threadId: this.threadId, turn: turnRecord(state.id, status, state.startedAt, completedAt, error) } }]; + } + + compactionStarted(event) { + const { compactionId, turn } = event.data ?? {}; + if (typeof compactionId !== 'string' || compactionId.length === 0) return []; + const startedAt = Number.isFinite(event.time) ? Math.floor(event.time / 1000) : nowSeconds(); + const startedAtMs = Number.isFinite(event.time) ? event.time : nowMillis(); + const item = { type: 'contextCompaction', id: `dsh-compaction-${compactionId}` }; + let turnId; + let manual = false; + const events = []; + if (Number.isInteger(turn)) { + const state = this.turns.get(turn); + if (!state) return []; + turnId = state.id; + } else if (turn === null) { + manual = true; + turnId = `dsh-maintenance-compaction-${compactionId}`; + events.push({ + method: 'turn/started', + params: { threadId: this.threadId, turn: turnRecord(turnId, 'inProgress', startedAt) } + }); + } else { + return []; + } + this.compactions.set(compactionId, { + compactionId, + turnId, + manual, + startedAt, + item, + summarized: false + }); + events.push({ + method: 'item/started', + params: { threadId: this.threadId, turnId, startedAtMs, item } + }); + return events; + } + + compactionSummarized(event) { + const compactionId = event.data?.compactionId; + const state = this.compactions.get(compactionId); + if (state) state.summarized = true; + return []; + } + + compactionEnded(event) { + const { compactionId, error } = event.data ?? {}; + const state = this.compactions.get(compactionId); + if (!state) return []; + this.compactions.delete(compactionId); + const completedAtMs = Number.isFinite(event.time) ? event.time : nowMillis(); + const completedAt = Math.floor(completedAtMs / 1000); + const events = [{ + method: 'item/completed', + params: { + threadId: this.threadId, + turnId: state.turnId, + completedAtMs, + item: state.item + } + }]; + if (state.manual) { + events.push({ + method: 'turn/completed', + params: { + threadId: this.threadId, + turn: turnRecord( + state.turnId, + error == null ? 'completed' : 'failed', + state.startedAt, + completedAt, + compactionError(error) + ) + } + }); + } + return events; + } + + assistantChunk(event) { + const { turn, step, chunk } = event.data ?? {}; + if (!Number.isInteger(turn) || !Number.isInteger(step) || !chunk) return []; + const turnState = this.turns.get(turn); + if (!turnState) return []; + + if (chunk.type === 'reasoning-delta' && typeof chunk.text === 'string') { + const key = `${turn}:${step}`; + let item = this.reasoningItems.get(key); + const events = []; + if (!item) { + item = { id: `dsh-reasoning-${turn}-${step}`, content: [] }; + this.reasoningItems.set(key, item); + events.push({ + method: 'item/started', + params: { + threadId: this.threadId, + turnId: turnState.id, + startedAtMs: Number.isFinite(event.time) ? event.time : nowMillis(), + item: { type: 'reasoning', id: item.id, summary: [], content: [] } + } + }); + } + const index = Number.isInteger(chunk.index) && chunk.index >= 0 ? chunk.index : 0; + item.content[index] = `${item.content[index] ?? ''}${chunk.text}`; + events.push({ + method: 'item/reasoning/textDelta', + params: { + threadId: this.threadId, + turnId: turnState.id, + itemId: item.id, + delta: chunk.text, + contentIndex: index + } + }); + return events; + } + + if (chunk.type !== 'text-delta' || typeof chunk.text !== 'string') return []; + const key = `${turn}:${step}`; + let item = this.assistantItems.get(key); + const events = []; + if (!item) { + item = { id: `dsh-assistant-${turn}-${step}`, text: '' }; + this.assistantItems.set(key, item); + events.push({ + method: 'item/started', + params: { + threadId: this.threadId, + turnId: turnState.id, + startedAtMs: Number.isFinite(event.time) ? event.time : nowMillis(), + item: { type: 'agentMessage', id: item.id, text: '', phase: null, memoryCitation: null, delivery: null } + } + }); + } + item.text += chunk.text; + events.push({ + method: 'item/agentMessage/delta', + params: { threadId: this.threadId, turnId: turnState.id, itemId: item.id, delta: chunk.text } + }); + return events; + } + + assistantMessage(event) { + const { turn, step, message } = event.data ?? {}; + if (!Number.isInteger(turn) || !Number.isInteger(step) || !message) return []; + const turnState = this.turns.get(turn); + if (!turnState) return []; + const key = `${turn}:${step}`; + const completedAtMs = Number.isFinite(event.time) ? event.time : nowMillis(); + const events = []; + + const finalReasoning = reasoningContent(message); + let reasoning = this.reasoningItems.get(key); + if (reasoning || finalReasoning.length > 0) { + if (!reasoning) { + reasoning = { id: `dsh-reasoning-${turn}-${step}`, content: [] }; + this.reasoningItems.set(key, reasoning); + events.push({ + method: 'item/started', + params: { + threadId: this.threadId, + turnId: turnState.id, + startedAtMs: completedAtMs, + item: { type: 'reasoning', id: reasoning.id, summary: [], content: [] } + } + }); + } + reasoning.content = finalReasoning.length > 0 ? finalReasoning : reasoning.content; + events.push({ + method: 'item/completed', + params: { + threadId: this.threadId, + turnId: turnState.id, + completedAtMs, + item: { type: 'reasoning', id: reasoning.id, summary: [], content: reasoning.content } + } + }); + } + + const finalText = visibleText(message); + let item = this.assistantItems.get(key); + if (!item && finalText.length > 0) { + item = { id: `dsh-assistant-${turn}-${step}`, text: '' }; + this.assistantItems.set(key, item); + events.push({ + method: 'item/started', + params: { + threadId: this.threadId, + turnId: turnState.id, + startedAtMs: completedAtMs, + item: { type: 'agentMessage', id: item.id, text: '', phase: null, memoryCitation: null, delivery: null } + } + }); + } + if (item) { + item.text = finalText; + events.push({ + method: 'item/completed', + params: { + threadId: this.threadId, + turnId: turnState.id, + completedAtMs, + item: { type: 'agentMessage', id: item.id, text: finalText, phase: null, memoryCitation: null, delivery: null } + } + }); + } + return events; + } + + toolCall(event) { + const { turn, callId, name, arguments: rawArguments } = event.data ?? {}; + const turnState = this.turns.get(turn); + if (!turnState || !callId || typeof name !== 'string') return []; + const startedAtMs = Number.isFinite(event.time) ? event.time : nowMillis(); + + let item; + if (this.toolPresentation) { + ({ item } = this.toolPresentation.start({ + turnId: turnState.id, + callId, + name, + rawArguments, + startedAtMs + })); + } else { + item = { + type: 'dynamicToolCall', + id: `dsh-tool-${String(callId)}`, + namespace: null, + tool: name, + arguments: parseJsonOrString(rawArguments), + status: 'inProgress', + contentItems: null, + success: null, + durationMs: null + }; + this.tools.set(String(callId), { item, turnId: turnState.id, startedAt: startedAtMs }); + } + + return [{ method: 'item/started', params: { threadId: this.threadId, turnId: turnState.id, startedAtMs, item } }]; + } + + toolResult(event) { + const callId = toolResultCallId(event.data); + if (callId === undefined) return []; + const completedAtMs = Number.isFinite(event.time) ? event.time : nowMillis(); + + if (this.toolPresentation) { + const completed = this.toolPresentation.complete({ callId, resultData: event.data, completedAtMs }); + if (!completed) return []; + return [{ + method: 'item/completed', + params: { + threadId: this.threadId, + turnId: completed.state.turnId, + completedAtMs, + item: completed.item + } + }]; + } + + const state = this.tools.get(String(callId)); + if (!state) return []; + const failed = toolResultFailed(event.data); + const completed = { + ...state.item, + status: failed ? 'failed' : 'completed', + success: !failed, + durationMs: Math.max(0, completedAtMs - state.startedAt) + }; + this.tools.delete(String(callId)); + return [{ method: 'item/completed', params: { threadId: this.threadId, turnId: state.turnId, completedAtMs, item: completed } }]; + } + + todoWrite(event) { + if (this.currentTurn == null) return []; + const turnState = this.turns.get(this.currentTurn); + if (!turnState) return []; + const todos = Array.isArray(event.data?.todos) ? event.data.todos : []; + return [{ + method: 'turn/plan/updated', + params: { + threadId: this.threadId, + turnId: turnState.id, + explanation: null, + plan: todos.map((todo) => ({ step: String(todo.content ?? ''), status: mapPlanStatus(todo.status) })) + } + }]; + } +} diff --git a/src/dsh/skills.mjs b/src/dsh/skills.mjs new file mode 100644 index 00000000..a5ec249b --- /dev/null +++ b/src/dsh/skills.mjs @@ -0,0 +1,67 @@ +import path from 'node:path'; + +function requireSkills(ctx) { + const skills = ctx.get('skills'); + if (skills === undefined) throw new Error('DSHX requires DSH service: skills'); + return skills; +} + +function codexScope(source) { + if (source === 'project-dsh' || source === 'project-agents') return 'repo'; + if (source === 'user-dsh' || source === 'user-agents') return 'user'; + return 'system'; +} + +/** + * Project the subset of DSH skills that Codex's filesystem-oriented skill UI + * can represent faithfully. DSH remains the complete skill registry; a remote + * or runtime skill with no absolute file path is not fabricated into a path. + */ +export async function dshSkillsListEntry({ ctx, cwd, scope, diagnostics = () => {} }) { + const absoluteCwd = path.resolve(cwd); + const skills = requireSkills(ctx); + const options = { cwd: absoluteCwd, ...(scope === undefined ? {} : { scope }) }; + const snapshot = await skills.snapshot(options); + const data = []; + const errors = []; + + if (snapshot.complete !== true) { + errors.push({ + path: absoluteCwd, + message: 'DSH skill discovery was incomplete; available candidates are shown without claiming the catalog is authoritative' + }); + } + + for (const summary of snapshot.skills ?? []) { + if (summary?.invocation?.userInvocable !== true) continue; + try { + const definition = await skills.get(summary.name, options); + if (!definition) { + errors.push({ path: summary.name, message: 'DSH skill disappeared before it could be loaded' }); + continue; + } + if (typeof definition.path !== 'string' || !path.isAbsolute(definition.path)) { + // Codex SkillMetadata requires an absolute filesystem path. The DSH + // registry explicitly permits provider-managed skills without one. + diagnostics(`skill ${summary.name} is user-invocable but has no absolute filesystem path; omitted from Codex skill UI`); + continue; + } + data.push({ + name: definition.name, + description: definition.description, + path: definition.path, + scope: codexScope(definition.source), + enabled: true + }); + } catch (error) { + errors.push({ + path: summary.name, + message: error instanceof Error ? error.message : String(error) + }); + } + } + + return { cwd: absoluteCwd, skills: data, errors }; +} + +export const skillInternals = { codexScope }; diff --git a/src/dsh/thread-controller.mjs b/src/dsh/thread-controller.mjs new file mode 100644 index 00000000..23a550c0 --- /dev/null +++ b/src/dsh/thread-controller.mjs @@ -0,0 +1,205 @@ +import { DshSessionProjector } from './session-projector.mjs'; +import { tokenUsageNotification } from './token-usage.mjs'; +import { DshToolPresentationResolver } from './tool-presentation.mjs'; + +function deferred(timeoutMs, label) { + let resolve; + let reject; + let timer; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + timer = setTimeout(() => rej(new Error(`Timed out waiting for ${label}`)), timeoutMs); + }); + return { + promise, + resolve(value) { clearTimeout(timer); resolve(value); }, + reject(error) { clearTimeout(timer); reject(error); } + }; +} + +function usageRelevant(event) { + return event?.type === 'assistant/message' + || event?.type === 'request/context' + || (event?.type === 'assistant/chunk' && event.data?.chunk?.type === 'usage'); +} + +/** + * Owns only the presentation lifetime for one already-created DSH Agent. + * The Agent and Session remain authoritative and are never reconstructed here. + */ +export class DshThreadController { + constructor({ handle, driver, emit = () => {}, turnStartTimeoutMs = 15_000, diagnostics = () => {} }) { + if (!handle?.agent) throw new Error('DshThreadController requires a DSH AgentHandle'); + this.handle = handle; + this.agent = handle.agent; + this.driver = driver; + this.emit = emit; + this.turnStartTimeoutMs = turnStartTimeoutMs; + this.diagnostics = diagnostics; + + this.toolPresentation = typeof this.agent.ctx?.get === 'function' + ? new DshToolPresentationResolver({ + ctx: this.agent.ctx, + agent: this.agent, + threadId: String(this.agent.id), + workspaceCwd: this.agent.session?.header?.cwd ?? process.cwd(), + diagnostics + }) + : null; + + this.projector = new DshSessionProjector({ + threadId: String(this.agent.id), + sessionId: String(this.agent.id), + toolPresentation: this.toolPresentation + }); + this.pendingTurnStart = null; + this.responseGate = null; + this.preparedUserContent = null; + this.lastTokenUsageSignature = null; + this.closed = false; + + const onSessionEvent = (session, event) => this.onSessionEvent(session, event); + this.disposeSessionListener = this.agent.ctx.on('session/event', onSessionEvent); + } + + get threadId() { + return String(this.agent.id); + } + + toolCorrelation(callId) { + return this.toolPresentation?.correlation(callId); + } + + currentLocation() { + const number = this.projector.currentTurn; + if (!Number.isInteger(number)) return undefined; + const turn = this.projector.turns.get(number); + if (!turn) return undefined; + return { threadId: this.threadId, turnId: turn.id }; + } + + /** + * Stage rich Codex input for exactly one existing base-adapter turn/steer call. + * The base adapter still owns all model/cwd/permission/sandbox validation. + */ + prepareUserContent(content) { + if (this.preparedUserContent != null) throw new Error('DSHX rich input slot is already occupied'); + const slot = { content }; + this.preparedUserContent = slot; + return () => { + if (this.preparedUserContent === slot) this.preparedUserContent = null; + }; + } + + consumeUserContent(fallback) { + const slot = this.preparedUserContent; + this.preparedUserContent = null; + return slot?.content ?? fallback; + } + + deliver(notification) { + if (this.responseGate && !this.responseGate.released) { + this.responseGate.buffered.push(notification); + return; + } + this.emit(notification); + } + + maybeProjectTokenUsage(event) { + if (!usageRelevant(event)) return; + const location = this.currentLocation(); + if (!location) return; + try { + const notification = tokenUsageNotification({ + ctx: this.agent.ctx, + session: this.agent.session, + threadId: location.threadId, + turnId: location.turnId + }); + if (!notification) return; + const signature = JSON.stringify(notification.params.tokenUsage); + if (signature === this.lastTokenUsageSignature) return; + this.lastTokenUsageSignature = signature; + this.deliver(notification); + } catch (error) { + this.diagnostics(`token usage projection failed: ${error instanceof Error ? error.message : error}`); + } + } + + onSessionEvent(session, event) { + if (this.closed || session !== this.agent.session) return; + const projected = this.projector.project(event); + for (const notification of projected) { + if (notification.method === 'turn/started' && this.pendingTurnStart) { + const pending = this.pendingTurnStart; + this.pendingTurnStart = null; + const gate = { + firstNotification: notification, + buffered: [], + released: false + }; + this.responseGate = gate; + pending.resolve({ turn: notification.params.turn, gate }); + continue; + } + this.deliver(notification); + } + this.maybeProjectTokenUsage(event); + } + + /** Submit a normal follow-up and wait until DSH itself commits turn/start. */ + async startTurn(fallbackContent) { + if (this.pendingTurnStart || (this.responseGate && !this.responseGate.released)) { + throw new Error('A turn/start request is already awaiting DSHX protocol release'); + } + const content = this.consumeUserContent(fallbackContent); + const pending = deferred(this.turnStartTimeoutMs, 'DSH turn/start'); + this.pendingTurnStart = pending; + try { + this.driver.followup(this.agent, content); + const { turn, gate } = await pending.promise; + return { + turn, + release: () => { + if (gate.released) return; + gate.released = true; + if (this.responseGate === gate) this.responseGate = null; + this.emit(gate.firstNotification); + for (const notification of gate.buffered) this.emit(notification); + gate.buffered.length = 0; + } + }; + } catch (error) { + if (this.pendingTurnStart === pending) this.pendingTurnStart = null; + throw error; + } + } + + steer(fallbackContent) { + this.driver.steer(this.agent, this.consumeUserContent(fallbackContent)); + } + + interrupt(options) { + this.driver.interrupt(this.agent, options); + } + + whenIdle() { + return this.driver.whenIdle(this.agent); + } + + async close() { + if (this.closed) return; + this.closed = true; + this.pendingTurnStart?.reject(new Error('DSHX thread controller closed')); + this.pendingTurnStart = null; + this.preparedUserContent = null; + if (this.responseGate && !this.responseGate.released) { + this.responseGate.released = true; + this.responseGate.buffered.length = 0; + } + this.responseGate = null; + this.disposeSessionListener?.(); + await this.handle.dispose?.(); + } +} diff --git a/src/dsh/thread-title.mjs b/src/dsh/thread-title.mjs new file mode 100644 index 00000000..afda2f90 --- /dev/null +++ b/src/dsh/thread-title.mjs @@ -0,0 +1,19 @@ +export function foldDshSessionTitle(events = []) { + for (let index = events.length - 1; index >= 0; index -= 1) { + const event = events[index]; + if (event?.type !== 'session/title') continue; + const title = event.data?.title; + if (typeof title === 'string' && title.length > 0) return title; + } + return null; +} + +export function threadNameUpdatedNotification(threadId, title) { + return { + method: 'thread/name/updated', + params: { + threadId: String(threadId), + threadName: title == null ? null : String(title) + } + }; +} diff --git a/src/dsh/token-usage.mjs b/src/dsh/token-usage.mjs new file mode 100644 index 00000000..cafce57c --- /dev/null +++ b/src/dsh/token-usage.mjs @@ -0,0 +1,116 @@ +function finiteNonNegative(value) { + return Number.isFinite(value) && value >= 0 ? Number(value) : 0; +} + +function breakdown(usage = {}) { + const inputTokens = finiteNonNegative(usage.inputTokens ?? usage.uncachedInputTokens); + const cachedInputTokens = finiteNonNegative(usage.cacheReadTokens); + const cacheWriteInputTokens = finiteNonNegative(usage.cacheWriteTokens); + const outputTokens = finiteNonNegative(usage.outputTokens); + const reasoningOutputTokens = finiteNonNegative(usage.reasoningTokens); + return { + totalTokens: inputTokens + cachedInputTokens + cacheWriteInputTokens + outputTokens, + inputTokens, + cachedInputTokens, + cacheWriteInputTokens, + outputTokens, + reasoningOutputTokens + }; +} + +function usageSamples(events = []) { + const byStep = new Map(); + let last = null; + let lastTurn = null; + for (const event of events) { + const data = event?.data; + if (!Number.isInteger(data?.turn) || !Number.isInteger(data?.step)) continue; + let usage; + if (event.type === 'assistant/chunk' && data.chunk?.type === 'usage') { + usage = data.chunk.usage; + } else if (event.type === 'assistant/message' && data.usage) { + usage = data.usage; + } + if (!usage) continue; + const sample = { ...usage }; + byStep.set(`${data.turn}:${data.step}`, sample); + last = sample; + lastTurn = data.turn; + } + return { byStep, last, lastTurn }; +} + +function reasoningTotal(events = []) { + const { byStep } = usageSamples(events); + let total = 0; + for (const usage of byStep.values()) total += finiteNonNegative(usage.reasoningTokens); + return total; +} + +function latestContextWindow(events = []) { + for (let index = events.length - 1; index >= 0; index -= 1) { + const event = events[index]; + if (event?.type === 'request/context' && Number.isFinite(event.data?.contextWindow)) { + return Number(event.data.contextWindow); + } + } + return null; +} + +/** + * Read-only presentation snapshot over official DSH session projections. + * DSH token-meter owns accounting; this module only reshapes its values into + * the pinned Codex TUI contract. + */ +export function codexThreadTokenUsage({ ctx, session }) { + const projections = ctx?.get?.('sessionProjections'); + if (!session || !projections?.snapshot) return null; + const snapshot = projections.snapshot(session); + const cumulative = snapshot?.values?.tokenUsage; + if (!cumulative) return null; + + const events = session.events ?? []; + const { last } = usageSamples(events); + const total = breakdown({ + inputTokens: cumulative.uncachedInputTokens, + cacheReadTokens: cumulative.cacheReadTokens, + cacheWriteTokens: cumulative.cacheWriteTokens, + outputTokens: cumulative.outputTokens, + reasoningTokens: reasoningTotal(events) + }); + const contextWindow = Number.isFinite(snapshot?.values?.contextPressure?.contextWindow) + ? Number(snapshot.values.contextPressure.contextWindow) + : latestContextWindow(events); + + return { + total, + last: breakdown(last ?? {}), + modelContextWindow: contextWindow + }; +} + +export function latestTokenUsageTurnId(session) { + const { lastTurn } = usageSamples(session?.events ?? []); + return Number.isInteger(lastTurn) ? `dsh-turn-${lastTurn}` : null; +} + +export function tokenUsageNotification({ ctx, session, threadId, turnId }) { + const tokenUsage = codexThreadTokenUsage({ ctx, session }); + if (!tokenUsage || !threadId || !turnId) return null; + return { + method: 'thread/tokenUsage/updated', + params: { threadId, turnId, tokenUsage } + }; +} + +export function persistedTokenUsageNotification({ ctx, session, threadId }) { + const turnId = latestTokenUsageTurnId(session); + return tokenUsageNotification({ ctx, session, threadId, turnId }); +} + +export const tokenUsageInternals = { + breakdown, + usageSamples, + reasoningTotal, + latestContextWindow +}; diff --git a/src/dsh/tool-presentation.mjs b/src/dsh/tool-presentation.mjs new file mode 100644 index 00000000..4396c471 --- /dev/null +++ b/src/dsh/tool-presentation.mjs @@ -0,0 +1,219 @@ +import path from 'node:path'; + +function parseArguments(raw) { + if (typeof raw !== 'string') return raw ?? {}; + try { return JSON.parse(raw); } catch { return { raw }; } +} + +function toolResultFromEvent(data) { + const block = data?.message?.content?.find?.((entry) => entry?.type === 'tool-result'); + return { + content: Array.isArray(block?.content) ? block.content : [], + isError: Boolean(block?.isError ?? data?.error), + ...(data?.meta === undefined ? {} : { meta: data.meta }) + }; +} + +function textContentItems(content) { + const items = []; + for (const block of content ?? []) { + if (block?.type === 'text' && typeof block.text === 'string') { + items.push({ type: 'inputText', text: block.text }); + } + } + return items.length === 0 ? null : items; +} + +function resolveCwd(workspaceCwd, candidate) { + if (!candidate) return workspaceCwd; + return path.isAbsolute(candidate) ? candidate : path.resolve(workspaceCwd, candidate); +} + +function lineCount(text) { + if (text === '') return 0; + return text.split(/\r?\n/).length; +} + +function displayDiff(file) { + // DSH FileDiff may represent a contextual hunk rather than whole-file text. + // Codex only needs display diff text here; avoid fabricating source line numbers. + const oldLines = String(file.oldText ?? '').split(/\r?\n/); + const newLines = String(file.newText ?? '').split(/\r?\n/); + const oldCount = lineCount(String(file.oldText ?? '')); + const newCount = lineCount(String(file.newText ?? '')); + return [ + `--- a/${file.path}`, + `+++ b/${file.path}`, + `@@ -1,${oldCount} +1,${newCount} @@`, + ...oldLines.map((line) => `-${line}`), + ...newLines.map((line) => `+${line}`) + ].join('\n'); +} + +function canUseCodexFileChange(view) { + return view?.card === 'diff' && + Array.isArray(view.diffs) && + view.diffs.length > 0 && + view.diffs.every((diff) => typeof diff?.path === 'string' && diff.oldText !== null); +} + +function fileChangeItem(id, view, status) { + return { + type: 'fileChange', + id, + changes: view.diffs.map((diff) => ({ + path: diff.path, + kind: { type: 'update', move_path: null }, + diff: displayDiff(diff) + })), + status + }; +} + +function dynamicItem(id, toolName, args, status, result = null, durationMs = null) { + return { + type: 'dynamicToolCall', + id, + namespace: null, + tool: toolName, + arguments: args, + status, + contentItems: result ? textContentItems(result.content) : null, + success: result ? !result.isError : null, + durationMs + }; +} + +/** + * Resolves UI semantics exclusively through DSH ToolDefinition presenters. + * + * No tool names or argument schemas are embedded here. `ctx.tools.get(name, + * agent)` is authoritative for the definition visible to this Agent; its pure + * `presentCall/presentResult` methods decide terminal/diff/generic intent. + */ +export class DshToolPresentationResolver { + constructor({ ctx, agent, threadId, workspaceCwd = process.cwd(), diagnostics = () => {} }) { + if (!ctx || typeof ctx.get !== 'function') throw new Error('DshToolPresentationResolver requires a Cordis Context'); + if (!agent) throw new Error('DshToolPresentationResolver requires a DSH Agent'); + this.ctx = ctx; + this.agent = agent; + this.threadId = String(threadId ?? agent.id); + this.workspaceCwd = path.resolve(workspaceCwd); + this.diagnostics = diagnostics; + this.calls = new Map(); + } + + definition(name) { + return this.ctx.get('tools')?.get?.(name, this.agent); + } + + start({ turnId, callId, name, rawArguments, startedAtMs = Date.now() }) { + const args = parseArguments(rawArguments); + const definition = this.definition(name); + let view; + try { view = definition?.presentCall?.(args); } + catch (error) { this.diagnostics(`presentCall(${name}): ${error instanceof Error ? error.message : error}`); } + + const id = `dsh-tool-${String(callId)}`; + let item; + let semantic = 'generic'; + if (view?.card === 'terminal') { + semantic = 'command'; + item = { + type: 'commandExecution', + id, + pluginId: null, + scriptPath: null, + command: view.title, + cwd: resolveCwd(this.workspaceCwd, view.cwd), + processId: null, + source: 'agent', + status: 'inProgress', + commandActions: [], + aggregatedOutput: null, + exitCode: null, + durationMs: null + }; + } else if (canUseCodexFileChange(view)) { + semantic = 'fileChange'; + item = fileChangeItem(id, view, 'inProgress'); + } else { + // DSH `oldText:null` is intentionally ambiguous between create and + // overwrite, while Codex requires add/delete/update. Stay generic rather + // than fabricate a PatchChangeKind. + item = dynamicItem(id, name, args, 'inProgress'); + } + + const state = { + callId: String(callId), + name, + args, + definition, + callView: view, + semantic, + item, + turnId, + startedAtMs + }; + this.calls.set(String(callId), state); + return { item, state }; + } + + complete({ callId, resultData, completedAtMs = Date.now() }) { + const state = this.calls.get(String(callId)); + if (!state) return undefined; + const result = toolResultFromEvent(resultData); + let view; + try { view = state.definition?.presentResult?.(state.args, result); } + catch (error) { this.diagnostics(`presentResult(${state.name}): ${error instanceof Error ? error.message : error}`); } + const durationMs = Math.max(0, completedAtMs - state.startedAtMs); + + let item; + if (state.semantic === 'command') { + const terminal = view?.card === 'terminal' ? view : undefined; + const failed = result.isError || Boolean(terminal?.signal) || + (terminal?.exitCode !== undefined && terminal.exitCode !== 0); + item = { + ...state.item, + status: failed ? 'failed' : 'completed', + aggregatedOutput: terminal?.output ?? textContentItems(result.content)?.map((entry) => entry.text).join('\n') ?? null, + exitCode: terminal?.exitCode ?? null, + durationMs + }; + } else if (state.semantic === 'fileChange') { + item = view?.card === 'diff' && canUseCodexFileChange(view) + ? fileChangeItem(state.item.id, view, result.isError ? 'failed' : 'completed') + : { ...state.item, status: result.isError ? 'failed' : 'completed' }; + } else { + item = dynamicItem( + state.item.id, + state.name, + state.args, + result.isError ? 'failed' : 'completed', + result, + durationMs + ); + } + + state.item = item; + state.resultView = view; + state.result = result; + return { item, state }; + } + + correlation(callId) { + const state = this.calls.get(String(callId)); + if (!state) return undefined; + return { + threadId: this.threadId, + turnId: state.turnId, + itemId: state.item.id, + semantic: state.semantic, + ...(state.semantic === 'command' + ? { kind: 'command', command: state.item.command, cwd: state.item.cwd } + : state.semantic === 'fileChange' + ? { kind: 'fileChange' } + : {}) + }; + } +} diff --git a/src/dsh/user-input.mjs b/src/dsh/user-input.mjs new file mode 100644 index 00000000..b1084826 --- /dev/null +++ b/src/dsh/user-input.mjs @@ -0,0 +1,106 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; + +const MEDIA_TYPES = new Map([ + ['.png', 'image/png'], + ['.jpg', 'image/jpeg'], + ['.jpeg', 'image/jpeg'], + ['.webp', 'image/webp'], + ['.gif', 'image/gif'] +]); + +function requireAttachments(ctx) { + const attachments = ctx?.get?.('attachments'); + if (!attachments?.saveImages) throw new Error('DSHX image input requires DSH service: attachments.saveImages'); + return attachments; +} + +function localMediaType(filePath) { + const mediaType = MEDIA_TYPES.get(path.extname(filePath).toLowerCase()); + if (!mediaType) throw new Error(`Unsupported local image type: ${path.extname(filePath) || ''}`); + return mediaType; +} + +function parseDataImage(url) { + const match = /^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/=\r\n]+)$/i.exec(String(url ?? '')); + if (!match) return undefined; + return { + mediaType: match[1].toLowerCase(), + data: Buffer.from(match[2].replace(/[\r\n]/g, ''), 'base64') + }; +} + +function skillGesture(name) { + const value = String(name ?? ''); + if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) { + throw new Error(`Invalid DSH skill name from Codex input: ${JSON.stringify(name)}`); + } + return `/${value}`; +} + +/** + * Convert pinned Codex UserInput into the public DSH UserContent vocabulary. + * Images are committed through DSH's attachment service before their refs are + * placed in the durable user message. User-explicit skills deliberately remain + * plain `/name` gestures because that is DSH's authoritative pre-step seam. + */ +export async function codexInputToDshContent(ctx, inputs = []) { + if (!Array.isArray(inputs)) throw new Error('Codex input must be an array'); + const plan = []; + const pendingImages = []; + + for (const input of inputs) { + switch (input?.type) { + case 'text': + if (typeof input.text !== 'string') throw new Error('Codex text input requires text'); + plan.push({ kind: 'content', block: { type: 'text', text: input.text } }); + break; + case 'skill': + plan.push({ kind: 'content', block: { type: 'text', text: skillGesture(input.name) } }); + break; + case 'localImage': { + if (typeof input.path !== 'string' || input.path.length === 0) { + throw new Error('Codex localImage input requires path'); + } + const filePath = path.resolve(input.path); + const mediaType = localMediaType(filePath); + const data = await fs.readFile(filePath); + const imageIndex = pendingImages.length; + pendingImages.push({ data, mediaType, name: path.basename(filePath) }); + plan.push({ kind: 'image', imageIndex }); + break; + } + case 'image': { + const inline = parseDataImage(input.url); + if (!inline) { + throw new Error('DSHX accepts Codex image input only as localImage or data:image/*; remote image URLs remain provider-owned'); + } + const imageIndex = pendingImages.length; + pendingImages.push({ ...inline, name: `pasted-image-${imageIndex + 1}` }); + plan.push({ kind: 'image', imageIndex }); + break; + } + case 'mention': + throw new Error('DSHX does not yet project Codex structured mentions into a DSH user message'); + default: + throw new Error(`Unsupported Codex user input type: ${JSON.stringify(input?.type)}`); + } + } + + const refs = pendingImages.length > 0 + ? await requireAttachments(ctx).saveImages(pendingImages) + : []; + return plan.map((entry) => entry.kind === 'image' + ? { type: 'image', ref: refs[entry.imageIndex] } + : entry.block); +} + +export function dshContentText(content = []) { + return content + .filter((block) => block?.type === 'text' && typeof block.text === 'string') + .map((block) => block.text) + .join('\n') + .trim(); +} + +export const inputInternals = { localMediaType, parseDataImage, skillGesture }; diff --git a/src/dsh/user-question-bridge.mjs b/src/dsh/user-question-bridge.mjs new file mode 100644 index 00000000..8e1a4b49 --- /dev/null +++ b/src/dsh/user-question-bridge.mjs @@ -0,0 +1,101 @@ +function requireService(ctx, name) { + const service = ctx.get(name); + if (service === undefined) throw new Error(`DSHX requires DSH service: ${name}`); + return service; +} + +function codexQuestion(question) { + const prompt = question.detail === undefined + ? question.question + : `${question.question}\n\n${question.detail}`; + return { + id: question.id, + header: question.header ?? 'Question', + question: prompt, + isOther: true, + isSecret: false, + options: question.options === undefined + ? null + : question.options.map((option) => ({ + label: option.label, + description: option.description ?? '' + })) + }; +} + +function dshAnswer(question, response) { + const values = response?.answers?.[question.id]?.answers ?? []; + const labels = new Set((question.options ?? []).map((option) => option.label)); + const selected = values.filter((value) => labels.has(value)); + const custom = values.filter((value) => !labels.has(value)); + return { + id: question.id, + selected, + ...(custom.length === 0 ? {} : { custom: custom.join('\n') }) + }; +} + +/** + * UI provider for the official DSH `ctx.userQuestions` seam. + * + * DSH's public AskUserQuestionRequest deliberately has no tool call id. The + * bridge therefore asks its owner for a PRESENTATION interaction location; a + * Codex client may attach the dialog to a synthetic UI-only item for the live + * DSH turn instead of guessing which parallel tool call caused the question. + * No question/answer semantics or durable state are owned here. + */ +export class DshUserQuestionBridge { + constructor({ ctx, broker, locate, complete = () => {}, diagnostics = () => {} }) { + if (!ctx || typeof ctx.get !== 'function') throw new Error('DshUserQuestionBridge requires a Cordis Context'); + if (!broker) throw new Error('DshUserQuestionBridge requires a UiRequestBroker'); + if (typeof locate !== 'function') throw new Error('DshUserQuestionBridge requires locate(request)'); + if (typeof complete !== 'function') throw new Error('DshUserQuestionBridge complete must be a function'); + this.ctx = ctx; + this.broker = broker; + this.locate = locate; + this.complete = complete; + this.diagnostics = diagnostics; + this.disposeProvider = requireService(ctx, 'userQuestions').registerProvider({ + ask: (request) => this.ask(request) + }); + } + + async ask(request) { + if (request.questions.some((question) => question.multiSelect === true)) { + throw new Error('DSHX pinned Codex TUI does not yet represent DSH multi-select questions faithfully'); + } + const location = await this.locate(request); + if (!location?.threadId || !location?.turnId || !location?.itemId) { + throw new Error('DSHX could not open a Codex presentation item for the DSH user question'); + } + + try { + const response = await this.broker.request( + 'item/tool/requestUserInput', + { + threadId: location.threadId, + turnId: location.turnId, + itemId: location.itemId, + questions: request.questions.map(codexQuestion), + isBlocking: true, + autoResolutionMs: null + }, + { signal: request.signal } + ); + const answer = { answers: request.questions.map((question) => dshAnswer(question, response)) }; + await this.complete(location, { status: 'completed', answer }); + return answer; + } catch (error) { + this.diagnostics(error instanceof Error ? error.message : String(error)); + try { await this.complete(location, { status: 'failed', error }); } catch (completeError) { + this.diagnostics(completeError instanceof Error ? completeError.message : String(completeError)); + } + throw error; + } + } + + dispose() { + this.disposeProvider?.(); + this.disposeProvider = undefined; + } +} diff --git a/src/dsh/user-shell.mjs b/src/dsh/user-shell.mjs new file mode 100644 index 00000000..313dcd06 --- /dev/null +++ b/src/dsh/user-shell.mjs @@ -0,0 +1,209 @@ +import { randomUUID } from 'node:crypto'; + +function seconds(ms = Date.now()) { + return Math.floor(ms / 1000); +} + +function turnRecord(id, status, startedAt, completedAt = null, error = null) { + return { + id, + items: [], + itemsView: 'full', + status, + error, + startedAt, + completedAt, + durationMs: completedAt == null ? null : Math.max(0, (completedAt - startedAt) * 1000) + }; +} + +function errorRecord(message) { + return { message: String(message), codexErrorInfo: null, additionalDetails: null }; +} + +function resultData(callId, result) { + return { + message: { + source: { kind: 'tool', callId }, + content: [{ + type: 'tool-result', + toolCallId: callId, + content: Array.isArray(result?.content) ? result.content : [], + isError: Boolean(result?.isError) + }] + }, + ...(result?.meta === undefined ? {} : { meta: result.meta }) + }; +} + +function pickTool(agent) { + const tools = agent.ctx?.get?.('tools'); + if (!tools) throw new Error('DSHX user shell requires DSH service: tools'); + const preferred = process.platform === 'win32' ? ['pwsh', 'bash'] : ['bash', 'pwsh']; + for (const name of preferred) { + if (tools.get?.(name, agent)) return { tools, name }; + } + throw new Error('No DSH shell tool is visible in the current Agent scope'); +} + +/** + * Presentation-only bridge for Codex `!` commands. Execution still goes through + * the official DSH ToolRuntime, so pre-execute policy, guards, approval, + * sandboxing, timeouts, post-policy and result normalization all remain DSH-owned. + * Like Codex's own user-shell action, these cells are intentionally not durable + * conversation history. + */ +export class DshUserShellBridge { + constructor({ send, diagnostics = () => {} } = {}) { + if (typeof send !== 'function') throw new Error('DshUserShellBridge requires send(message)'); + this.send = send; + this.diagnostics = diagnostics; + this.active = new Map(); + } + + start(controller, command) { + const threadId = String(controller?.threadId ?? controller?.agent?.id ?? ''); + if (!threadId) throw new Error('thread/shellCommand requires a resumed DSH thread'); + if (typeof command !== 'string' || command.trim().length === 0) { + throw new Error('thread/shellCommand requires a non-empty command'); + } + if (this.active.has(threadId)) throw new Error('A DSHX user-shell command is already active for this thread'); + + const existing = controller.currentLocation(); + const callId = `dshx-user-shell-${randomUUID()}`; + const turnId = existing?.turnId ?? `dsh-user-shell-turn-${randomUUID()}`; + const maintenance = existing == null; + const startedAtMs = Date.now(); + const abortController = new AbortController(); + const state = { + threadId, + turnId, + callId, + maintenance, + startedAtMs, + abortController, + controller, + command + }; + this.active.set(threadId, state); + + return { + result: {}, + afterResponse: () => { + void this.run(state); + } + }; + } + + async run(state) { + const { threadId, turnId, callId, maintenance, startedAtMs, abortController, controller, command } = state; + try { + if (maintenance) { + this.send({ + method: 'turn/started', + params: { threadId, turn: turnRecord(turnId, 'inProgress', seconds(startedAtMs)) } + }); + } + + const { tools, name } = pickTool(controller.agent); + const args = { + command, + description: 'Run command from interactive user shell', + workdir: controller.agent.session?.header?.cwd, + run_in_background: false + }; + const projected = controller.toolPresentation?.start({ + turnId, + callId, + name, + rawArguments: args, + startedAtMs + }); + if (!projected || projected.state.semantic !== 'command') { + throw new Error(`DSH shell tool ${name} does not expose terminal presentation semantics`); + } + projected.item.source = 'userShell'; + projected.state.item = projected.item; + this.send({ + method: 'item/started', + params: { threadId, turnId, startedAtMs, item: projected.item } + }); + + const result = await tools.execute({ + callId, + name, + arguments: args, + signal: abortController.signal, + agent: controller.agent + }); + const completedAtMs = Date.now(); + const completed = controller.toolPresentation.complete({ + callId, + resultData: resultData(callId, result), + completedAtMs + }); + if (!completed) throw new Error(`DSHX lost user-shell presentation correlation for ${callId}`); + completed.item.source = 'userShell'; + this.send({ + method: 'item/completed', + params: { threadId, turnId, completedAtMs, item: completed.item } + }); + if (maintenance) { + const completedAt = seconds(completedAtMs); + this.send({ + method: 'turn/completed', + params: { + threadId, + turn: turnRecord( + turnId, + result?.isError ? 'failed' : 'completed', + seconds(startedAtMs), + completedAt, + result?.isError ? errorRecord(result.error?.message ?? 'DSH shell command failed') : null + ) + } + }); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.diagnostics(`user shell ${threadId}: ${message}`); + if (maintenance) { + const completedAt = seconds(); + this.send({ + method: 'turn/completed', + params: { + threadId, + turn: turnRecord(turnId, 'failed', seconds(startedAtMs), completedAt, errorRecord(message)) + } + }); + } else { + this.send({ method: 'warning', params: { threadId, message } }); + } + } finally { + if (this.active.get(threadId) === state) this.active.delete(threadId); + } + } + + interrupt(threadId, turnId) { + const state = this.active.get(String(threadId)); + if (!state || state.turnId !== String(turnId)) return false; + state.abortController.abort(new Error('user cancelled DSHX shell command')); + return true; + } + + abortThread(threadId, reason = 'thread closed') { + const state = this.active.get(String(threadId)); + if (!state) return false; + state.abortController.abort(new Error(reason)); + return true; + } + + close() { + for (const state of this.active.values()) { + state.abortController.abort(new Error('DSHX adapter closing')); + } + this.active.clear(); + } +} + +export const userShellInternals = { pickTool, resultData, turnRecord }; diff --git a/src/dsh/workspace-command.mjs b/src/dsh/workspace-command.mjs new file mode 100644 index 00000000..89867393 --- /dev/null +++ b/src/dsh/workspace-command.mjs @@ -0,0 +1,166 @@ +import path from 'node:path'; +import { randomUUID } from 'node:crypto'; + +function shellTool(agent) { + const tools = agent?.ctx?.get?.('tools'); + if (!tools) throw new Error('DSHX workspace command requires DSH service: tools'); + const preferred = process.platform === 'win32' ? ['pwsh', 'bash'] : ['bash', 'pwsh']; + for (const name of preferred) { + if (tools.get?.(name, agent)) return { tools, name }; + } + throw new Error('No DSH shell tool is visible in the active Agent scope'); +} + +function bashQuote(value) { + const text = String(value); + if (text.includes('\0')) throw new Error('command/exec argv cannot contain NUL'); + return `'${text.replaceAll("'", `'"'"'`)}'`; +} + +function pwshQuote(value) { + const text = String(value); + if (text.includes('\0')) throw new Error('command/exec argv cannot contain NUL'); + return `'${text.replaceAll("'", "''")}'`; +} + +function envName(name) { + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) { + throw new Error(`command/exec environment key is not portable: ${JSON.stringify(name)}`); + } + return name; +} + +function commandText(argv, env, dialect) { + const quote = dialect === 'pwsh' ? pwshQuote : bashQuote; + const invocation = dialect === 'pwsh' + ? `& ${argv.map(quote).join(' ')}` + : argv.map(quote).join(' '); + const entries = Object.entries(env ?? {}); + if (entries.length === 0) return invocation; + + if (dialect === 'pwsh') { + const prefix = entries.map(([rawName, value]) => { + const name = envName(rawName); + return value == null + ? `Remove-Item Env:${name} -ErrorAction SilentlyContinue` + : `$env:${name} = ${quote(value)}`; + }); + return `${prefix.join('; ')}; ${invocation}`; + } + + const prefix = entries.map(([rawName, value]) => { + const name = envName(rawName); + return value == null ? `unset ${name}` : `export ${name}=${quote(value)}`; + }); + return `${prefix.join('; ')}; ${invocation}`; +} + +function textError(result) { + return (result?.content ?? []) + .filter((block) => block?.type === 'text' && typeof block.text === 'string') + .map((block) => block.text) + .join('\n') + .trim(); +} + +function capUtf8(text, limit) { + if (!Number.isSafeInteger(limit) || limit < 0) return text; + const bytes = Buffer.from(String(text), 'utf8'); + if (bytes.length <= limit) return String(text); + let end = Math.min(limit, bytes.length); + while (end > 0 && (bytes[end] & 0b1100_0000) === 0b1000_0000) end -= 1; + return bytes.subarray(0, end).toString('utf8'); +} + +function bufferedResult(result, params) { + if (result?.isError) { + throw new Error(textError(result) || result.error?.message || 'DSH workspace command failed'); + } + const value = result?.value; + if (!value || value.kind !== 'foreground') { + throw new Error(`DSH shell returned non-foreground command/exec result: ${JSON.stringify(value?.kind)}`); + } + const exitCode = Number.isInteger(value.exitCode) ? value.exitCode : 1; + const cap = params.disableOutputCap === true ? undefined : params.outputBytesCap; + return { + exitCode, + stdout: capUtf8(value.stdout?.text ?? '', cap), + stderr: capUtf8(value.stderr?.text ?? '', cap) + }; +} + +function validateParams(params) { + if (!Array.isArray(params.command) || params.command.length === 0 || params.command.some((entry) => typeof entry !== 'string' || entry.length === 0)) { + throw new Error('command/exec requires a non-empty argv string array'); + } + if (params.processId != null || params.tty === true || params.streamStdin === true || params.streamStdoutStderr === true || params.size != null) { + throw new Error('DSHX command/exec supports buffered non-TTY workspace commands only'); + } + if (params.disableTimeout === true) { + throw new Error('DSHX command/exec does not override the DSH shell executor timeout policy'); + } + if (params.outputBytesCap != null && params.disableOutputCap === true) { + throw new Error('command/exec outputBytesCap and disableOutputCap are mutually exclusive'); + } + if (params.timeoutMs != null && (!Number.isSafeInteger(params.timeoutMs) || params.timeoutMs <= 0)) { + throw new Error('command/exec timeoutMs must be a positive integer'); + } + if (params.sandboxPolicy != null || params.permissionProfile != null) { + throw new Error('DSHX command/exec uses the active DSH session permission policy and rejects Codex policy overrides'); + } +} + +/** + * TUI-owned one-off workspace probes (`git`, `/diff`, status metadata) mapped + * onto the official DSH shell tool runtime. No process is spawned by DSHX. + */ +export class DshWorkspaceCommandBridge { + constructor({ driver } = {}) { + if (!driver?.listRootAgents) throw new Error('DshWorkspaceCommandBridge requires DshAgentDriver'); + this.driver = driver; + } + + agentFor(cwd) { + const roots = this.driver.listRootAgents(); + if (roots.length === 0) throw new Error('command/exec requires an active DSH root Agent'); + if (cwd != null) { + const wanted = path.resolve(cwd); + const matches = roots.filter((agent) => path.resolve(agent.session?.header?.cwd ?? process.cwd()) === wanted); + if (matches.length === 1) return matches[0]; + if (matches.length > 1) throw new Error(`command/exec cwd matches multiple active DSH Agents: ${wanted}`); + } + if (roots.length === 1) return roots[0]; + throw new Error('command/exec cannot choose among multiple active DSH root Agents without an exact cwd match'); + } + + async execute(params = {}) { + validateParams(params); + const agent = this.agentFor(params.cwd); + const { tools, name } = shellTool(agent); + const args = { + command: commandText(params.command, params.env, name), + description: 'Run DSHX TUI workspace command', + workdir: params.cwd ?? agent.session?.header?.cwd, + run_in_background: false, + ...(params.timeoutMs == null ? {} : { timeoutMs: params.timeoutMs }) + }; + const result = await tools.execute({ + callId: `dshx-workspace-${randomUUID()}`, + name, + arguments: args, + signal: new AbortController().signal, + agent + }); + return bufferedResult(result, params); + } +} + +export const workspaceCommandInternals = { + bashQuote, + pwshQuote, + commandText, + bufferedResult, + validateParams, + capUtf8, + shellTool +}; diff --git a/src/protocol.mjs b/src/protocol.mjs new file mode 100644 index 00000000..e1728f7f --- /dev/null +++ b/src/protocol.mjs @@ -0,0 +1,288 @@ +import crypto from 'node:crypto'; +import os from 'node:os'; +import path from 'node:path'; + +const nowSeconds = () => Math.floor(Date.now() / 1000); +const nowMillis = () => Date.now(); +const id = (prefix) => `${prefix}-${crypto.randomUUID()}`; + +function platformFamily() { + return process.platform === 'win32' ? 'windows' : 'unix'; +} + +function platformOs() { + if (process.platform === 'darwin') return 'macos'; + if (process.platform === 'win32') return 'windows'; + if (process.platform === 'linux') return 'linux'; + return process.platform; +} + +function absoluteCwd(value) { + return path.resolve(value || process.cwd()); +} + +function modelRecord() { + return { + id: 'dshx-stub', + model: 'dshx-stub', + upgrade: null, + upgradeInfo: null, + availabilityNux: null, + displayName: 'DSHX Protocol Stub', + description: 'Deterministic M0 model used only to validate Codex TUI protocol compatibility.', + modelSpecialty: null, + hidden: false, + supportedReasoningEfforts: [ + { reasoningEffort: 'medium', description: 'M0 deterministic protocol mode' } + ], + defaultReasoningEffort: 'medium', + inputModalities: ['text'], + supportsPersonality: false, + multiAgentVersion: null, + additionalSpeedTiers: [], + serviceTiers: [], + defaultServiceTier: null, + isDefault: true + }; +} + +function emptyThread(cwd) { + const timestamp = nowSeconds(); + // Codex treats thread IDs as UUIDs and validates them before opening a remote thread. + // Keep the deterministic protocol stub aligned with that public app-server contract. + const threadId = crypto.randomUUID(); + return { + id: threadId, + sessionId: threadId, + forkedFromId: null, + parentThreadId: null, + preview: '', + ephemeral: false, + section: null, + sectionEnteredAt: null, + projectId: null, + modelProvider: 'dsh', + createdAt: timestamp, + updatedAt: timestamp, + recencyAt: timestamp, + status: { type: 'idle' }, + path: null, + cwd, + cliVersion: '0.0.0-m0', + source: { custom: 'dshx' }, + threadSource: null, + agentNickname: null, + agentRole: null, + gitInfo: null, + name: null, + turns: [] + }; +} + +function turnRecord(turnId, status = 'inProgress', completed = false) { + const startedAt = nowSeconds(); + return { + id: turnId, + items: [], + itemsView: 'full', + status, + error: null, + startedAt, + completedAt: completed ? nowSeconds() : null, + durationMs: completed ? 0 : null + }; +} + +function textFromTurnStart(params = {}) { + const input = Array.isArray(params.input) ? params.input : []; + return input + .filter((item) => item && item.type === 'text' && typeof item.text === 'string') + .map((item) => item.text) + .join('\n') + .trim(); +} + +export class ProtocolStub { + constructor({ cwd = process.cwd(), home = null } = {}) { + this.cwd = absoluteCwd(cwd); + this.home = home ? path.resolve(home) : path.join(os.homedir(), '.dshx'); + this.threads = new Map(); + this.activeTurns = new Map(); + } + + dispatch(method, params) { + switch (method) { + case 'initialize': + return { + result: { + userAgent: 'dsh-codex-app-server/0.0.0-m0', + codexHome: this.home, + platformFamily: platformFamily(), + platformOs: platformOs() + } + }; + case 'account/read': + return { result: { account: null, requiresOpenaiAuth: false } }; + case 'model/list': + return { result: { data: [modelRecord()], nextCursor: null } }; + case 'configRequirements/read': + return { result: { requirements: null } }; + case 'skills/list': + return { result: { data: [] } }; + case 'thread/list': + return { result: { data: [...this.threads.values()], nextCursor: null, backwardsCursor: null } }; + case 'thread/start': + return this.startThread(params); + case 'turn/start': + return this.startTurn(params); + case 'turn/interrupt': + return this.interruptTurn(params); + default: + return { + result: undefined, + events: [], + error: { code: -32601, message: `Method not found: ${method}` } + }; + } + } + + startThread(params) { + const cwd = absoluteCwd(params.cwd || this.cwd); + const thread = emptyThread(cwd); + this.threads.set(thread.id, thread); + return { + result: { + thread, + model: 'dshx-stub', + modelProvider: 'dsh', + serviceTier: null, + cwd, + instructionSources: [], + approvalPolicy: 'on-request', + approvalsReviewer: 'user', + sandbox: { + type: 'workspaceWrite', + writableRoots: [cwd], + networkAccess: false, + excludeTmpdirEnvVar: false, + excludeSlashTmp: false + }, + reasoningEffort: 'medium' + }, + events: [this.notification('thread/started', { thread })] + }; + } + + startTurn(params) { + const threadId = params.threadId; + if (typeof threadId !== 'string' || !this.threads.has(threadId)) { + throw new Error(`Unknown thread: ${threadId ?? ''}`); + } + + const turnId = id('turn'); + const itemId = id('item'); + const turn = turnRecord(turnId); + const prompt = textFromTurnStart(params); + const answer = prompt + ? `DSHX protocol stub received: ${prompt}` + : 'DSHX protocol stub is connected.'; + const chunks = answer.match(/.{1,24}(?:\s|$)/g) ?? [answer]; + const startedItem = { + type: 'agentMessage', + id: itemId, + text: '', + phase: null, + memoryCitation: null, + delivery: null + }; + const completedItem = { ...startedItem, text: answer }; + const completedTurn = turnRecord(turnId, 'completed', true); + completedTurn.items = [completedItem]; + + this.activeTurns.set(threadId, { turnId, itemId, startedAt: nowSeconds() }); + const events = [ + this.notification('turn/started', { threadId, turn }), + this.notification('item/started', { + item: startedItem, + threadId, + turnId, + startedAtMs: nowMillis() + }), + ...chunks.map((delta) => + this.notification('item/agentMessage/delta', { threadId, turnId, itemId, delta }) + ), + this.notification('item/completed', { + item: completedItem, + threadId, + turnId, + completedAtMs: nowMillis() + }), + this.notification('turn/completed', { threadId, turn: completedTurn }) + ]; + + return { result: { turn }, events }; + } + + interruptTurn(params) { + const threadId = params.threadId; + const active = this.activeTurns.get(threadId); + if (!active) return { result: {} }; + this.activeTurns.delete(threadId); + const turn = { + ...turnRecord(active.turnId, 'interrupted', true), + startedAt: active.startedAt + }; + return { + result: {}, + events: [this.notification('turn/completed', { threadId, turn })] + }; + } + + isTurnActive(threadId, turnId) { + return this.activeTurns.get(threadId)?.turnId === turnId; + } + + completeTurn(threadId, turnId) { + if (this.isTurnActive(threadId, turnId)) this.activeTurns.delete(threadId); + } + + notification(method, params) { + return { method, params }; + } + + error(requestId, code, message, data = undefined) { + const error = { code, message }; + if (data !== undefined) error.data = data; + return { id: requestId, error }; + } +} + +export function normalizeDispatchResult(stub, message) { + const { id: requestId, method } = message ?? {}; + if (requestId === undefined || typeof method !== 'string') { + return { response: stub.error(requestId ?? null, -32600, 'Invalid Request'), events: [] }; + } + + try { + const dispatched = stub.dispatch(method, message?.params ?? {}); + if (dispatched.error) { + return { + response: stub.error(requestId, dispatched.error.code, dispatched.error.message), + events: [] + }; + } + return { + response: { id: requestId, result: dispatched.result }, + events: dispatched.events ?? [] + }; + } catch (error) { + return { + response: stub.error( + requestId, + -32603, + error instanceof Error ? error.message : String(error) + ), + events: [] + }; + } +} diff --git a/src/protocol/request-broker.mjs b/src/protocol/request-broker.mjs new file mode 100644 index 00000000..dd4787fa --- /dev/null +++ b/src/protocol/request-broker.mjs @@ -0,0 +1,79 @@ +function makeAbortError(message = 'The request was aborted') { + const error = new Error(message); + error.name = 'AbortError'; + return error; +} + +/** + * Correlates app-server requests initiated by DSHX with responses from the TUI. + * The broker owns no approval policy: transport loss, timeout and cancellation + * reject the presentation request so the DSH answerer can fail closed. + */ +export class UiRequestBroker { + constructor({ send, timeoutMs = 120_000, idPrefix = 'dshx-ui' } = {}) { + if (typeof send !== 'function') throw new Error('UiRequestBroker requires send(message)'); + this.send = send; + this.timeoutMs = timeoutMs; + this.idPrefix = idPrefix; + this.nextId = 1; + this.pending = new Map(); + this.closed = false; + } + + request(method, params, { signal } = {}) { + if (this.closed) return Promise.reject(new Error('UI request broker is closed')); + if (signal?.aborted) return Promise.reject(makeAbortError()); + + const id = `${this.idPrefix}-${this.nextId++}`; + return new Promise((resolve, reject) => { + const cleanup = () => { + clearTimeout(entry.timer); + signal?.removeEventListener('abort', entry.onAbort); + this.pending.delete(id); + }; + const entry = { + resolve: (value) => { cleanup(); resolve(value); }, + reject: (error) => { cleanup(); reject(error); }, + onAbort: () => entry.reject(makeAbortError()), + timer: undefined + }; + entry.timer = setTimeout( + () => entry.reject(new Error(`UI request timed out: ${method}`)), + this.timeoutMs + ); + signal?.addEventListener('abort', entry.onAbort, { once: true }); + this.pending.set(id, entry); + + try { + this.send({ id, method, params }); + } catch (error) { + entry.reject(error instanceof Error ? error : new Error(String(error))); + } + }); + } + + /** + * Consume a client response (`{id,result}` or `{id,error}`). Returns false + * when the id does not belong to an outstanding server-initiated request. + */ + handleResponse(message) { + if (!message || message.id === undefined || message.method !== undefined) return false; + const entry = this.pending.get(String(message.id)); + if (!entry) return false; + if (message.error !== undefined) { + const error = new Error(message.error?.message ?? 'TUI request failed'); + error.code = message.error?.code; + entry.reject(error); + } else { + entry.resolve(message.result); + } + return true; + } + + close(reason = 'UI transport closed') { + if (this.closed) return; + this.closed = true; + const entries = [...this.pending.values()]; + for (const entry of entries) entry.reject(new Error(reason)); + } +} diff --git a/src/server.mjs b/src/server.mjs new file mode 100644 index 00000000..ad4b3fdf --- /dev/null +++ b/src/server.mjs @@ -0,0 +1,127 @@ +import crypto from 'node:crypto'; +import { WebSocketServer } from 'ws'; +import { ProtocolStub, normalizeDispatchResult } from './protocol.mjs'; + +const DEFAULT_HOST = '127.0.0.1'; +const DEFAULT_EVENT_DELAY_MS = 18; + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function sendJson(socket, message) { + if (socket.readyState === 1) socket.send(JSON.stringify(message)); +} + +function tokenMatches(header, token) { + if (!token) return true; + const left = Buffer.from(typeof header === 'string' ? header : '', 'utf8'); + const right = Buffer.from(`Bearer ${token}`, 'utf8'); + return left.length === right.length && crypto.timingSafeEqual(left, right); +} + +async function sendEvents(socket, events, delayMs, stub) { + for (const event of events) { + const threadId = event?.params?.threadId; + const turnId = event?.params?.turnId ?? event?.params?.turn?.id; + const belongsToActiveTurn = + typeof threadId === 'string' && + typeof turnId === 'string' && + event.method !== 'turn/completed'; + + if (belongsToActiveTurn && !stub.isTurnActive(threadId, turnId)) break; + if (delayMs > 0) await sleep(delayMs); + if (belongsToActiveTurn && !stub.isTurnActive(threadId, turnId)) break; + + sendJson(socket, event); + if ( + event.method === 'turn/completed' && + typeof threadId === 'string' && + typeof turnId === 'string' + ) { + stub.completeTurn(threadId, turnId); + } + } +} + +export async function startProtocolStubServer({ + host = DEFAULT_HOST, + port = 0, + cwd = process.cwd(), + eventDelayMs = DEFAULT_EVENT_DELAY_MS, + token, + log = () => {} +} = {}) { + const stub = new ProtocolStub({ cwd }); + const server = new WebSocketServer({ + host, + port, + verifyClient: token + ? (info, done) => done(tokenMatches(info.req.headers.authorization, token), 401, 'Unauthorized') + : undefined + }); + + server.on('connection', (socket, request) => { + log(`client connected from ${request.socket.remoteAddress ?? 'unknown'}`); + + socket.on('message', (raw) => { + let message; + try { + message = JSON.parse(raw.toString('utf8')); + } catch (error) { + sendJson(socket, { + id: null, + error: { + code: -32700, + message: `Parse error: ${error instanceof Error ? error.message : error}` + } + }); + return; + } + + // Notifications intentionally have no id and receive no response. + if (message?.id === undefined) { + log(`notification ${message?.method ?? ''}`); + return; + } + + log(`request ${message.method} id=${String(message.id)}`); + const { response, events } = normalizeDispatchResult(stub, message); + sendJson(socket, response); + if (events.length > 0) void sendEvents(socket, events, eventDelayMs, stub); + }); + + socket.on('error', (error) => log(`socket error: ${error.message}`)); + }); + + await new Promise((resolve, reject) => { + if (server.address()) return resolve(); + server.once('listening', resolve); + server.once('error', reject); + }); + + const address = server.address(); + if (!address || typeof address === 'string') { + throw new Error('Protocol stub server did not bind a TCP address'); + } + + let closePromise; + const close = () => { + if (closePromise) return closePromise; + closePromise = new Promise((resolve, reject) => { + // WebSocketServer.close() waits for connected clients. Stub shutdown owns + // those test/dev clients so callers cannot deadlock by closing the server first. + for (const client of server.clients) client.terminate(); + server.close((error) => (error ? reject(error) : resolve())); + }); + return closePromise; + }; + + return { + host, + port: address.port, + url: `ws://${host}:${address.port}`, + token, + close + }; +} diff --git a/test/ci-actions-pins.test.mjs b/test/ci-actions-pins.test.mjs new file mode 100644 index 00000000..d2203dbe --- /dev/null +++ b/test/ci-actions-pins.test.mjs @@ -0,0 +1,32 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const workflowRoot = path.join(root, '.github', 'workflows'); +const immutableAction = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\/[^@\s#]+)?@[0-9a-f]{40}$/; + +test('every external GitHub Action is pinned to an immutable full commit SHA', () => { + const failures = []; + for (const entry of fs.readdirSync(workflowRoot, { withFileTypes: true })) { + if (!entry.isFile() || !/\.ya?ml$/.test(entry.name)) continue; + const file = path.join(workflowRoot, entry.name); + const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/); + for (let index = 0; index < lines.length; index += 1) { + const match = lines[index].match(/^\s*(?:-\s*)?uses:\s*([^\s#]+)/); + if (!match) continue; + const ref = match[1]; + if (ref.startsWith('./') || ref.startsWith('docker://')) continue; + if (!immutableAction.test(ref)) { + failures.push(`${entry.name}:${index + 1}: ${ref}`); + } + } + } + assert.deepEqual( + failures, + [], + `External Actions must use owner/repo[/path]@<40-hex-sha>; mutable refs found:\n${failures.join('\n')}` + ); +}); diff --git a/test/cli-arguments.test.mjs b/test/cli-arguments.test.mjs new file mode 100644 index 00000000..facbb6fe --- /dev/null +++ b/test/cli-arguments.test.mjs @@ -0,0 +1,52 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { parseCliInvocation, parseLaunchArgs } from '../src/cli/arguments.mjs'; + +test('top-level help and version are recognized only as the sole argument', () => { + assert.deepEqual(parseCliInvocation(['--help']), { kind: 'help' }); + assert.deepEqual(parseCliInvocation(['-h']), { kind: 'help' }); + assert.deepEqual(parseCliInvocation(['--version']), { kind: 'version' }); + assert.deepEqual(parseCliInvocation(['-V']), { kind: 'version' }); + + assert.deepEqual(parseCliInvocation(['explain', '--version', 'behavior']), { + kind: 'launch', + tuiArgs: ['explain', '--version', 'behavior'], + resumeEnv: {} + }); + assert.deepEqual(parseCliInvocation(['explain', '--help', 'output']), { + kind: 'launch', + tuiArgs: ['explain', '--help', 'output'], + resumeEnv: {} + }); +}); + +test('doctor is an exact top-level command and rejects trailing arguments', () => { + assert.deepEqual(parseCliInvocation(['doctor']), { kind: 'doctor' }); + assert.throws(() => parseCliInvocation(['doctor', 'extra']), /Usage: dshx doctor/); +}); + +test('zero-argument launch and ordinary prompts pass through unchanged', () => { + assert.deepEqual(parseCliInvocation([]), { kind: 'launch', tuiArgs: [], resumeEnv: {} }); + assert.deepEqual(parseCliInvocation(['hello', 'world']), { + kind: 'launch', + tuiArgs: ['hello', 'world'], + resumeEnv: {} + }); +}); + +test('resume modes remain explicit and reject ambiguous option combinations', () => { + assert.deepEqual(parseLaunchArgs(['resume']), { + tuiArgs: [], + resumeEnv: { DSHX_RESUME_MODE: 'picker' } + }); + assert.deepEqual(parseLaunchArgs(['resume', '--last']), { + tuiArgs: [], + resumeEnv: { DSHX_RESUME_MODE: 'last' } + }); + assert.deepEqual(parseLaunchArgs(['resume', 'session-123']), { + tuiArgs: [], + resumeEnv: { DSHX_RESUME_MODE: 'id', DSHX_RESUME_SESSION_ID: 'session-123' } + }); + assert.throws(() => parseLaunchArgs(['resume', '--unknown']), /Usage: dshx resume/); + assert.throws(() => parseLaunchArgs(['resume', 'a', 'b']), /Usage: dshx resume/); +}); diff --git a/test/cli-runtime.test.mjs b/test/cli-runtime.test.mjs new file mode 100644 index 00000000..b522767d --- /dev/null +++ b/test/cli-runtime.test.mjs @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + DSHX_DOCTOR_BOOT_TIMEOUT_MS, + DSHX_DOCTOR_DISPOSE_TIMEOUT_MS, + DSHX_NODE_ENGINE, + DSHX_RELEASE_NODE_MAJOR, + assertReleaseNodeVersion, + isSupportedNodeVersion +} from '../src/cli/runtime.mjs'; + +test('runtime support matches the pinned DSH Node engine boundary', () => { + assert.equal(DSHX_NODE_ENGINE, '^22.19.0 || >=24.0.0'); + assert.equal(isSupportedNodeVersion('20.20.2'), false); + assert.equal(isSupportedNodeVersion('22.18.0'), false); + assert.equal(isSupportedNodeVersion('22.19.0'), true); + assert.equal(isSupportedNodeVersion('22.23.0'), true); + assert.equal(isSupportedNodeVersion('23.11.0'), false); + assert.equal(isSupportedNodeVersion('24.0.0'), true); + assert.equal(isSupportedNodeVersion('26.3.0'), true); + assert.equal(isSupportedNodeVersion('not-a-version'), false); +}); + +test('release/freeze work is intentionally standardized on Node 24 LTS', () => { + assert.equal(DSHX_RELEASE_NODE_MAJOR, 24); + assert.doesNotThrow(() => assertReleaseNodeVersion('24.19.0')); + assert.throws(() => assertReleaseNodeVersion('22.23.0'), /requires Node 24\.x/); + assert.throws(() => assertReleaseNodeVersion('26.3.0'), /requires Node 24\.x/); +}); + +test('doctor permits realistic first-run DSH cold starts while remaining bounded', () => { + assert.equal(DSHX_DOCTOR_BOOT_TIMEOUT_MS, 60_000); + assert.equal(DSHX_DOCTOR_DISPOSE_TIMEOUT_MS, 10_000); + assert.ok(DSHX_DOCTOR_BOOT_TIMEOUT_MS > 20_000); +}); diff --git a/test/dsh-agent-driver.test.mjs b/test/dsh-agent-driver.test.mjs new file mode 100644 index 00000000..6da7b312 --- /dev/null +++ b/test/dsh-agent-driver.test.mjs @@ -0,0 +1,227 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { DshAgentDriver } from '../src/dsh/agent-driver.mjs'; + +function fixture({ loggedConfig = undefined } = {}) { + const calls = []; + const listeners = []; + const agent = { + id: 'session-live', + session: { + requestHeader() { + return loggedConfig === undefined ? undefined : { config: loggedConfig }; + } + }, + ctx: { + on(name) { + listeners.push(name); + return () => {}; + } + }, + followup(message) { calls.push(['followup', message]); }, + steer(message) { calls.push(['steer', message]); }, + cancel(cause, options) { calls.push(['cancel', cause, options]); }, + whenIdle() { calls.push(['whenIdle']); return Promise.resolve(); } + }; + const handle = { agent, dispose: async () => {} }; + const agentCtx = { ...agent.ctx, agent }; + const services = { + loader: { async await() { calls.push(['loader.await']); } }, + agentDefaultModel: { + currentSelection() { + calls.push(['defaultModel.currentSelection']); + return { provider: 'deepseek', model: 'deepseek-test', reasoningEffort: 'high' }; + } + }, + agents: { + async create(options) { + calls.push(['agents.create', options]); + await options.setup?.(agentCtx); + return handle; + }, + async resume(options) { + calls.push(['agents.resume', options]); + await options.setup?.(agentCtx); + return handle; + }, + get(id) { calls.push(['agents.get', id]); return agent; }, + list() { calls.push(['agents.list']); return [agent]; }, + roots() { calls.push(['agents.roots']); return [agent]; } + }, + llm: { + listProviders() { + calls.push(['llm.listProviders']); + return [ + { id: 'deepseek', name: 'DeepSeek' }, + { id: 'broken', name: 'Broken provider' } + ]; + }, + async listModels(provider) { + calls.push(['llm.listModels', provider]); + if (provider === 'broken') throw new Error('catalog unavailable'); + return [ + { provider, id: 'model-a', name: 'Model A', description: 'First model' }, + { provider, id: 'model-b', name: 'Model B' } + ]; + }, + async resolveModelInfo(provider, model) { + calls.push(['llm.resolveModelInfo', provider, model]); + return { + provider, + id: model, + name: model === 'model-a' ? 'Model A' : 'Model B', + inputModalities: ['text'], + context: { contextWindow: 128000 }, + ...(model === 'model-a' + ? { + reasoning: { + efforts: [ + { id: 'low', name: 'Low' }, + { id: 'high', name: 'High', description: 'More reasoning' } + ], + defaultEffort: 'low' + } + } + : {}) + }; + }, + async resolveCallConfig(config) { + calls.push(['llm.resolveCallConfig', config]); + return { + ...config, + ...(config.reasoningEffort === undefined ? { reasoningEffort: 'low' } : {}) + }; + } + }, + sessionPersistence: { + async list(options) { calls.push(['sessionPersistence.list', options]); return { entries: [] }; }, + async inspect(id) { calls.push(['sessionPersistence.inspect', id]); return { id, events: [] }; } + } + }; + return { + calls, + listeners, + agent, + handle, + ctx: { get(name) { return services[name]; } } + }; +} + +test('create uses DSH default model and installs the official model-selection hooks', async () => { + const fx = fixture(); + const driver = new DshAgentDriver(fx.ctx); + const handle = await driver.create({ cwd: '/workspace', sessionId: 'session-new' }); + assert.equal(handle, fx.handle); + + const create = fx.calls.find(([name]) => name === 'agents.create'); + assert.ok(create); + const options = create[1]; + assert.equal(options.sessionId, 'session-new'); + assert.equal(options.meta.cwd, '/workspace'); + assert.deepEqual(options.agentOptions, { provider: 'deepseek', model: 'deepseek-test' }); + assert.equal(typeof options.setup, 'function'); + assert.deepEqual(fx.listeners.sort(), ['agent/request', 'system-prompt/assemble'].sort()); +}); + +test('resume restores latest request/header before consulting machine current default', async () => { + const fx = fixture({ + loggedConfig: { provider: 'persisted', model: 'old-model', reasoningEffort: 'max' } + }); + const driver = new DshAgentDriver(fx.ctx); + await driver.resume('session-old'); + + const resume = fx.calls.find(([name]) => name === 'agents.resume'); + assert.equal(resume[1].resumeSessionId, 'session-old'); + assert.equal(typeof resume[1].setup, 'function'); + assert.deepEqual(driver.currentModel(fx.agent), { + provider: 'persisted', + model: 'old-model', + reasoningEffort: 'max' + }); + assert.equal( + fx.calls.some(([name]) => name === 'defaultModel.currentSelection'), + false, + 'persisted request/header must win over the current machine default' + ); +}); + +test('blank resumed session falls back to live DSH default only when read', async () => { + const fx = fixture(); + const driver = new DshAgentDriver(fx.ctx); + await driver.resume('blank-session'); + assert.equal(fx.calls.some(([name]) => name === 'defaultModel.currentSelection'), false); + assert.deepEqual(driver.currentModel(fx.agent), { + provider: 'deepseek', + model: 'deepseek-test', + reasoningEffort: 'high' + }); +}); + +test('model directory uses public LLM registry and isolates provider failures', async () => { + const fx = fixture(); + const directory = await new DshAgentDriver(fx.ctx).modelDirectory(); + assert.equal(directory.groups.length, 1); + assert.equal(directory.groups[0].provider, 'deepseek'); + assert.equal(directory.groups[0].models[0].reasoning.defaultEffort, 'low'); + assert.deepEqual(directory.groups[0].models[0].inputModalities, ['text']); + assert.equal(directory.groups[0].models[0].contextWindow, 128000); + assert.deepEqual(directory.failures, [{ provider: 'broken', message: 'catalog unavailable' }]); +}); + +test('model switch is validated/defaulted by DSH resolveCallConfig before selection changes', async () => { + const fx = fixture(); + const driver = new DshAgentDriver(fx.ctx); + await driver.resume('blank-session'); + const selected = await driver.selectModel(fx.agent, { provider: 'deepseek', model: 'model-a' }); + assert.deepEqual(selected, { provider: 'deepseek', model: 'model-a', reasoningEffort: 'low' }); + assert.deepEqual(driver.currentModel(fx.agent), selected); + assert.deepEqual(fx.calls.find(([name]) => name === 'llm.resolveCallConfig'), [ + 'llm.resolveCallConfig', + { provider: 'deepseek', model: 'model-a' } + ]); +}); + +test('followup and steering create official DSH user messages and delegate to Agent', () => { + const fx = fixture(); + const driver = new DshAgentDriver(fx.ctx); + driver.followup(fx.agent, 'hello'); + driver.steer(fx.agent, 'change direction'); + + const followup = fx.calls.find(([name]) => name === 'followup')[1]; + assert.equal(followup.role, 'user'); + assert.deepEqual(followup.source, { kind: 'user' }); + assert.deepEqual(followup.content, [{ type: 'text', text: 'hello' }]); + assert.equal(typeof followup.id, 'string'); + + const steering = fx.calls.find(([name]) => name === 'steer')[1]; + assert.deepEqual(steering.content, [{ type: 'text', text: 'change direction' }]); +}); + +test('interrupt delegates user cancellation to DSH and does not invent policy', () => { + const fx = fixture(); + const driver = new DshAgentDriver(fx.ctx); + driver.interrupt(fx.agent, { keepInbox: true }); + assert.deepEqual(fx.calls.find(([name]) => name === 'cancel'), [ + 'cancel', + { kind: 'user' }, + { keepInbox: true } + ]); +}); + +test('live/session lookup methods delegate to official registries', async () => { + const fx = fixture(); + const driver = new DshAgentDriver(fx.ctx); + assert.equal(driver.getLive('session-live'), fx.agent); + assert.deepEqual(driver.listLive(), [fx.agent]); + assert.deepEqual(driver.listRootAgents(), [fx.agent]); + assert.deepEqual(await driver.listSessions({ limit: 20 }), { entries: [] }); + assert.deepEqual(await driver.inspectSession('session-old'), { id: 'session-old', events: [] }); + + assert.ok(fx.calls.some((call) => call[0] === 'sessionPersistence.list')); + assert.ok(fx.calls.some((call) => call[0] === 'sessionPersistence.inspect')); +}); + +test('missing DSH service is a hard compatibility error, not a shadow implementation', async () => { + const driver = new DshAgentDriver({ get() { return undefined; } }); + await assert.rejects(() => driver.create({ sessionId: 'session-x' }), /DSHX requires DSH service: agents/); +}); diff --git a/test/dsh-app-server-adapter.test.mjs b/test/dsh-app-server-adapter.test.mjs new file mode 100644 index 00000000..0717e4de --- /dev/null +++ b/test/dsh-app-server-adapter.test.mjs @@ -0,0 +1,379 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { DshAppServerAdapter } from '../src/dsh/app-server-adapter.mjs'; +import { decodeDshModel } from '../src/dsh/codex-shapes.mjs'; + +function fixture() { + const sent = []; + const calls = []; + const sessions = new Map(); + const live = new Map(); + let questionProvider; + let clock = 1_700_000_000_000; + + const presetTable = { + 'workspace-write': { sandbox: 'workspace-write', approval: 'ask', name: 'Workspace' }, + 'danger-full-access': { sandbox: 'danger-full-access', approval: 'never', name: 'Danger' } + }; + + const services = {}; + const rootCtx = { get(name) { return services[name]; } }; + + function requestHeader(session) { + for (let index = session.events.length - 1; index >= 0; index -= 1) { + const event = session.events[index]; + if (event.type === 'request/header') return event.data.header; + } + return undefined; + } + + function makeSession(id, cwd = '/workspace', seed = []) { + const session = { + id, + header: { id, createdAt: clock++, cwd }, + events: [...seed], + permissionPreset: 'workspace-write', + requestHeader() { return requestHeader(session); } + }; + sessions.set(String(id), session); + return session; + } + + function makeAgent(session) { + const listeners = new Map(); + const agent = { + id: session.id, + session, + ctx: { + agent: undefined, + get(name) { return services[name]; }, + on(name, listener) { + const bucket = listeners.get(name) ?? []; + bucket.push(listener); + listeners.set(name, bucket); + return () => { + const current = listeners.get(name) ?? []; + const index = current.indexOf(listener); + if (index >= 0) current.splice(index, 1); + }; + } + }, + followup(message) { + calls.push(['followup', String(agent.id), message]); + const turn = 1 + session.events.filter((event) => event.type === 'turn/start').length; + append(agent, 'turn/start', { turn }); + append(agent, 'user/message', message); + }, + steer(message) { calls.push(['steer', String(agent.id), message]); }, + cancel(cause, options) { calls.push(['cancel', String(agent.id), cause, options]); }, + whenIdle() { return Promise.resolve(); }, + _listeners: listeners + }; + agent.ctx.agent = agent; + return agent; + } + + function append(agent, type, data) { + const event = { type, seq: agent.session.events.length, time: clock++, data }; + agent.session.events.push(event); + for (const listener of agent._listeners.get('session/event') ?? []) listener(agent.session, event); + return event; + } + + async function approval(agent, req) { + const listeners = agent._listeners.get('approval/request') ?? []; + let index = 0; + const next = async () => { + const listener = listeners[index++]; + return listener ? listener(req, next) : 'unavailable'; + }; + return next(); + } + + services.loader = { async await() { calls.push(['loader.await']); } }; + services.agentDefaultModel = { + currentSelection() { return { provider: 'deepseek', model: 'model-a', reasoningEffort: 'low' }; } + }; + services.llm = { + listProviders() { return [{ id: 'deepseek', name: 'DeepSeek' }]; }, + async listModels(provider) { return [{ provider, id: 'model-a', name: 'Model A', description: 'Primary' }]; }, + async resolveModelInfo(provider, model) { + return { + provider, + id: model, + name: 'Model A', + inputModalities: ['text'], + reasoning: { + efforts: [{ id: 'low', name: 'Low' }, { id: 'high', name: 'High' }], + defaultEffort: 'low' + } + }; + }, + async resolveCallConfig(config) { + calls.push(['resolveCallConfig', config]); + if (config.provider !== 'deepseek' || config.model !== 'model-a') throw new Error('unknown route'); + return { ...config, reasoningEffort: config.reasoningEffort ?? 'low' }; + } + }; + services.tools = { + get(name, agent) { + assert.ok(agent); + if (name === 'shell') { + return { + presentCall: () => ({ card: 'terminal', title: 'echo ok', cwd: agent.session.header.cwd }), + presentResult: () => ({ card: 'terminal', output: 'ok\n', exitCode: 0 }) + }; + } + return undefined; + } + }; + services.permissionPresets = { + names: Object.keys(presetTable), + current(events) { + const session = [...sessions.values()].find((candidate) => candidate.events === events); + return session?.permissionPreset ?? 'workspace-write'; + }, + optionOf(name) { + if (name === 'custom') return { value: 'custom', name: 'Custom' }; + const preset = presetTable[name]; + return { value: name, name: preset.name, description: preset.description }; + }, + resolve(name) { + const preset = presetTable[name]; + if (!preset) throw new Error('unknown preset'); + return preset; + }, + set(session, name) { + if (!presetTable[name]) throw new Error('unknown preset'); + calls.push(['permissionPresets.set', String(session.id), name]); + session.permissionPreset = name; + } + }; + services.sandboxPolicy = { + resolve({ session }) { + const preset = presetTable[session.permissionPreset]; + return { mode: preset.sandbox, workspaceRoot: session.header.cwd }; + } + }; + services.approval = { + config: { policy: 'ask' }, + overrideOf(session) { return presetTable[session.permissionPreset].approval; } + }; + services.userQuestions = { + registerProvider(provider) { + assert.equal(questionProvider, undefined); + questionProvider = provider; + return () => { questionProvider = undefined; }; + } + }; + services.sessionPersistence = { + async list(signal) { + calls.push(['sessionPersistence.list', signal]); + return [...sessions.values()].map((session) => session.header); + }, + async inspect(id) { + const session = sessions.get(String(id)); + if (!session) return undefined; + return { meta: session.header, events: session.events }; + } + }; + services.agents = { + async create(options) { + calls.push(['agents.create', options]); + const session = makeSession(String(options.sessionId), options.meta?.cwd); + const agent = makeAgent(session); + live.set(String(agent.id), agent); + await options.setup?.(agent.ctx); + return { + agent, + async dispose() { calls.push(['handle.dispose', String(agent.id)]); live.delete(String(agent.id)); } + }; + }, + async resume(options) { + calls.push(['agents.resume', options]); + const session = sessions.get(String(options.resumeSessionId)); + if (!session) throw new Error('session not found'); + const agent = makeAgent(session); + live.set(String(agent.id), agent); + await options.setup?.(agent.ctx); + return { + agent, + async dispose() { calls.push(['handle.dispose', String(agent.id)]); live.delete(String(agent.id)); } + }; + }, + get(id) { return live.get(String(id)); }, + list() { return [...live.values()]; }, + roots() { return [...live.values()]; } + }; + + const adapter = new DshAppServerAdapter({ + ctx: rootCtx, + send: (message) => sent.push(message), + cwd: '/workspace', + home: '/home/test/.dshx', + version: 'test' + }); + + return { + adapter, + sent, + calls, + sessions, + live, + append, + approval, + makeSession, + async ask(request) { + assert.ok(questionProvider, 'adapter should register the DSH userQuestions provider'); + return questionProvider.ask(request); + } + }; +} + +async function request(adapter, id, method, params = {}) { + const handled = await adapter.handle({ id, method, params }); + assert.ok(handled?.response); + if (handled.response.error) throw new Error(handled.response.error.message); + return handled; +} + +test('bootstrap reads public DSH model catalog and exposes opaque provider-aware ids', async () => { + const fx = fixture(); + const init = await request(fx.adapter, 1, 'initialize'); + assert.equal(init.response.result.platformOs.length > 0, true); + + const models = await request(fx.adapter, 2, 'model/list'); + assert.equal(models.response.result.data.length, 1); + assert.deepEqual(decodeDshModel(models.response.result.data[0].model), { + provider: 'deepseek', model: 'model-a' + }); +}); + +test('thread/start delegates creation and emits thread/started only after RPC response point', async () => { + const fx = fixture(); + const started = await request(fx.adapter, 1, 'thread/start', { cwd: '/workspace' }); + const result = started.response.result; + assert.equal(result.modelProvider, 'deepseek'); + assert.equal(result.sandbox.type, 'externalSandbox'); + assert.ok(fx.calls.some(([name]) => name === 'agents.create')); + assert.deepEqual(fx.sent, []); + + started.afterResponse(); + assert.equal(fx.sent[0].method, 'thread/started'); + assert.equal(fx.sent[0].params.thread.id, result.thread.id); +}); + +test('turn/start uses Agent.followup and DSH-committed turn identity', async () => { + const fx = fixture(); + const created = await request(fx.adapter, 1, 'thread/start', {}); + created.afterResponse(); + const threadId = created.response.result.thread.id; + fx.sent.length = 0; + + const turn = await request(fx.adapter, 2, 'turn/start', { + threadId, + input: [{ type: 'text', text: 'hello', text_elements: [] }] + }); + assert.equal(turn.response.result.turn.id, 'dsh-turn-1'); + const followup = fx.calls.find(([name]) => name === 'followup'); + assert.equal(followup[2].source.kind, 'user'); + assert.deepEqual(fx.sent, [], 'turn notification stays buffered until transport sends response'); + turn.afterResponse(); + assert.equal(fx.sent[0].method, 'turn/started'); +}); + +test('DSH approval/request round-trips through the matching presenter cell and stays one-shot', async () => { + const fx = fixture(); + const created = await request(fx.adapter, 1, 'thread/start', {}); + const threadId = created.response.result.thread.id; + const agent = fx.live.get(threadId); + const turn = await request(fx.adapter, 2, 'turn/start', { + threadId, + input: [{ type: 'text', text: 'run', text_elements: [] }] + }); + turn.afterResponse(); + fx.sent.length = 0; + + fx.append(agent, 'tool/call', { turn: 1, step: 1, callId: 'call-1', name: 'shell', arguments: '{}' }); + assert.equal(fx.sent[0].params.item.type, 'commandExecution'); + const decisionPromise = fx.approval(agent, { + agent, + toolName: 'shell', + callId: 'call-1', + reason: 'needs permission' + }); + await Promise.resolve(); + const uiRequest = fx.sent.find((message) => message.method === 'item/commandExecution/requestApproval'); + assert.ok(uiRequest); + await fx.adapter.handle({ id: uiRequest.id, result: { decision: 'accept' } }); + assert.equal(await decisionPromise, 'allowed-once'); +}); + +test('DSH userQuestions uses a synthetic presentation item because the public request has no call id', async () => { + const fx = fixture(); + const created = await request(fx.adapter, 1, 'thread/start', {}); + const threadId = created.response.result.thread.id; + const agent = fx.live.get(threadId); + const turn = await request(fx.adapter, 2, 'turn/start', { + threadId, + input: [{ type: 'text', text: 'ask me', text_elements: [] }] + }); + turn.afterResponse(); + fx.sent.length = 0; + + const answerPromise = fx.ask({ + agent, + questions: [{ id: 'q1', question: 'Choose?', options: [{ label: 'A' }, { label: 'B' }] }] + }); + await Promise.resolve(); + const started = fx.sent.find((message) => message.method === 'item/started'); + const uiRequest = fx.sent.find((message) => message.method === 'item/tool/requestUserInput'); + assert.equal(started.params.item.namespace, 'dshx'); + assert.ok(uiRequest); + await fx.adapter.handle({ id: uiRequest.id, result: { answers: { q1: { answers: ['A'] } } } }); + assert.deepEqual(await answerPromise, { answers: [{ id: 'q1', selected: ['A'] }] }); + assert.ok(fx.sent.some((message) => message.method === 'item/completed')); +}); + +test('thread/resume restores DSH request-header model and authoritative persisted transcript', async () => { + const fx = fixture(); + const session = fx.makeSession('persisted', '/project', [ + { type: 'turn/start', seq: 0, time: 1_700_000_000_000, data: { turn: 1 } }, + { + type: 'user/message', seq: 1, time: 1_700_000_000_010, + data: { id: 'u1', role: 'user', source: { kind: 'user' }, content: [{ type: 'text', text: 'fix it' }] } + }, + { + type: 'request/header', seq: 2, time: 1_700_000_000_020, + data: { header: { config: { provider: 'deepseek', model: 'model-a', reasoningEffort: 'high' } }, reason: 'initial' } + }, + { + type: 'assistant/message', seq: 3, time: 1_700_000_000_030, + data: { turn: 1, step: 1, message: { id: 'a1', content: [{ type: 'text', text: 'fixed' }] } } + }, + { type: 'turn/end', seq: 4, time: 1_700_000_000_040, data: { turn: 1, reason: { kind: 'completed' } } } + ]); + session.permissionPreset = 'workspace-write'; + + const resumed = await request(fx.adapter, 1, 'thread/resume', { threadId: 'persisted' }); + assert.deepEqual(decodeDshModel(resumed.response.result.model), { provider: 'deepseek', model: 'model-a' }); + assert.equal(resumed.response.result.reasoningEffort, 'high'); + assert.equal(resumed.response.result.thread.turns.length, 1); + assert.deepEqual(resumed.response.result.thread.turns[0].items.map((item) => item.type), ['userMessage', 'agentMessage']); +}); + +test('thread/list reads DSH persistence instead of maintaining a DSHX session database', async () => { + const fx = fixture(); + fx.makeSession('s1', '/a', [{ + type: 'user/message', seq: 0, time: 1_700_000_000_000, + data: { id: 'u1', source: { kind: 'user' }, content: [{ type: 'text', text: 'Alpha task' }] } + }]); + fx.makeSession('s2', '/b', [{ + type: 'user/message', seq: 0, time: 1_700_000_000_100, + data: { id: 'u2', source: { kind: 'user' }, content: [{ type: 'text', text: 'Beta task' }] } + }]); + + const listed = await request(fx.adapter, 1, 'thread/list', { cwd: '/b' }); + assert.deepEqual(listed.response.result.data.map((thread) => thread.id), ['s2']); + assert.ok(fx.calls.some(([name]) => name === 'sessionPersistence.list')); +}); diff --git a/test/dsh-approval-bridge.test.mjs b/test/dsh-approval-bridge.test.mjs new file mode 100644 index 00000000..8830c0aa --- /dev/null +++ b/test/dsh-approval-bridge.test.mjs @@ -0,0 +1,110 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { DshApprovalBridge } from '../src/dsh/approval-bridge.mjs'; + +function fixture({ classify, brokerResult = { decision: 'accept' }, brokerError = null } = {}) { + let listener; + let disposed = false; + const requests = []; + const diagnostics = []; + const agent = { + id: 'session-1', + ctx: { + on(name, fn) { + assert.equal(name, 'approval/request'); + listener = fn; + return () => { disposed = true; listener = undefined; }; + } + } + }; + const broker = { + async request(method, params, options) { + requests.push({ method, params, options }); + if (brokerError) throw brokerError; + return brokerResult; + } + }; + const bridge = new DshApprovalBridge({ + agent, + broker, + classify: classify ?? (() => ({ + kind: 'command', + threadId: 'session-1', + turnId: 'dsh-turn-1', + itemId: 'dsh-tool-call-1', + command: 'npm test', + cwd: '/workspace' + })), + diagnostics: (message) => diagnostics.push(message) + }); + return { + bridge, + requests, + diagnostics, + invoke(req, next = async () => 'unavailable') { return listener(req, next); }, + get disposed() { return disposed; } + }; +} + +test('command approval is rendered through Codex command approval and maps accept to allowed-once', async () => { + const fx = fixture(); + const outcome = await fx.invoke({ toolName: 'bash', callId: 'call-1', reason: 'needs wider write access' }); + assert.equal(outcome, 'allowed-once'); + assert.equal(fx.requests.length, 1); + assert.equal(fx.requests[0].method, 'item/commandExecution/requestApproval'); + assert.equal(fx.requests[0].params.command, 'npm test'); + assert.equal(fx.requests[0].params.cwd, '/workspace'); + assert.equal(fx.requests[0].params.reason, 'needs wider write access'); +}); + +test('file approval uses Codex file-change approval only when classifier proves the semantics', async () => { + const fx = fixture({ + classify: () => ({ kind: 'fileChange', threadId: 'session-1', turnId: 'dsh-turn-2', itemId: 'file-1' }), + brokerResult: { decision: 'decline' } + }); + assert.equal(await fx.invoke({ toolName: 'edit', callId: 'call-2' }), 'rejected'); + assert.equal(fx.requests[0].method, 'item/fileChange/requestApproval'); + assert.equal(fx.requests[0].params.grantRoot, null); +}); + +test('unknown DSH tool approval delegates instead of being mislabeled as shell/file', async () => { + const fx = fixture({ classify: () => null }); + let delegated = 0; + const outcome = await fx.invoke( + { toolName: 'plugin-owned-tool', callId: 'call-x' }, + async () => { delegated += 1; return 'rejected'; } + ); + assert.equal(outcome, 'rejected'); + assert.equal(delegated, 1); + assert.equal(fx.requests.length, 0); +}); + +test('Codex acceptForSession fails closed because DSH exposes no equivalent grant', async () => { + const fx = fixture({ brokerResult: { decision: 'acceptForSession' } }); + const outcome = await fx.invoke({ toolName: 'bash', callId: 'call-1' }); + assert.equal(outcome, 'unavailable'); + assert.match(fx.diagnostics[0], /no session-wide approval grant/); +}); + +test('transport errors fail closed as unavailable', async () => { + const fx = fixture({ brokerError: new Error('UI disconnected') }); + const outcome = await fx.invoke({ toolName: 'bash', callId: 'call-1' }); + assert.equal(outcome, 'unavailable'); + assert.equal(fx.diagnostics[0], 'UI disconnected'); +}); + +test('aborted DSH request settles cancelled, never granted', async () => { + const controller = new AbortController(); + const abort = new Error('aborted'); + abort.name = 'AbortError'; + const fx = fixture({ brokerError: abort }); + controller.abort(); + const outcome = await fx.invoke({ toolName: 'bash', callId: 'call-1', signal: controller.signal }); + assert.equal(outcome, 'cancelled'); +}); + +test('disposing bridge removes only its presentation answerer', () => { + const fx = fixture(); + fx.bridge.dispose(); + assert.equal(fx.disposed, true); +}); diff --git a/test/dsh-codex-shapes.test.mjs b/test/dsh-codex-shapes.test.mjs new file mode 100644 index 00000000..ce9cf5ee --- /dev/null +++ b/test/dsh-codex-shapes.test.mjs @@ -0,0 +1,153 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + codexModelsFromDsh, + decodeDshModel, + dshThreadFromSnapshot, + dshTurnsFromSession, + encodeDshModel, + normalizeCodexEffort +} from '../src/dsh/codex-shapes.mjs'; + +test('model picker encodes provider identity without hard-coding provider semantics', () => { + const directory = { + groups: [ + { + provider: 'p1', + name: 'Provider One', + models: [{ + id: 'shared', + name: 'Shared Model', + inputModalities: ['text', 'image'], + reasoning: { + efforts: [{ id: 'low', name: 'Low' }, { id: 'high', name: 'High' }], + defaultEffort: 'low' + } + }] + }, + { + provider: 'p2', + name: 'Provider Two', + models: [{ id: 'shared', name: 'Shared Model', inputModalities: ['text'] }] + } + ] + }; + const models = codexModelsFromDsh(directory, { provider: 'p2', model: 'shared' }); + assert.equal(models.length, 2); + assert.notEqual(models[0].model, models[1].model); + assert.deepEqual(decodeDshModel(models[0].model), { provider: 'p1', model: 'shared' }); + assert.deepEqual(decodeDshModel(models[1].model), { provider: 'p2', model: 'shared' }); + assert.match(models[0].displayName, /Provider One/); + assert.equal(models[1].isDefault, true); + assert.equal(models[0].defaultReasoningEffort, 'low'); + assert.equal(models[1].defaultReasoningEffort, 'dsh-default'); + assert.equal(normalizeCodexEffort('dsh-default'), undefined); +}); + +test('opaque DSH model ids round-trip and reject unowned values', () => { + const encoded = encodeDshModel({ provider: 'deepseek', model: 'deepseek-chat' }); + assert.deepEqual(decodeDshModel(encoded), { provider: 'deepseek', model: 'deepseek-chat' }); + assert.equal(decodeDshModel('gpt-5'), undefined); +}); + +test('persisted SessionHeader and request header project to a not-loaded Codex thread', () => { + const thread = dshThreadFromSnapshot({ + meta: { id: 'session-1', createdAt: 1_700_000_000_000, cwd: '/work' }, + events: [ + { + type: 'user/message', + time: 1_700_000_000_100, + data: { id: 'u1', source: { kind: 'user' }, content: [{ type: 'text', text: 'Fix the parser' }] } + }, + { + type: 'request/header', + time: 1_700_000_000_200, + data: { header: { config: { provider: 'deepseek', model: 'model-a' } }, reason: 'initial' } + } + ] + }); + assert.equal(thread.id, 'session-1'); + assert.equal(thread.preview, 'Fix the parser'); + assert.equal(thread.modelProvider, 'deepseek'); + assert.deepEqual(thread.status, { type: 'notLoaded' }); + assert.equal(thread.cwd, '/work'); +}); + +test('history projection includes only direct human input and stable assistant/tool/reasoning items', () => { + const definitions = { + shell: { + presentCall: () => ({ card: 'terminal', title: 'echo ok', cwd: '/work' }), + presentResult: () => ({ card: 'terminal', output: 'ok\n', exitCode: 0 }) + } + }; + const agent = { + id: 'session-1', + ctx: { + get(name) { + if (name === 'tools') return { get(toolName, scope) { assert.equal(scope, agent); return definitions[toolName]; } }; + } + }, + session: { + header: { id: 'session-1', createdAt: 1_700_000_000_000, cwd: '/work' }, + events: [ + { type: 'turn/start', time: 1_700_000_000_000, data: { turn: 1 } }, + { + type: 'user/message', + time: 1_700_000_000_010, + data: { id: 'u1', role: 'user', source: { kind: 'user' }, content: [{ type: 'text', text: 'run it' }] } + }, + { + type: 'user/message', + time: 1_700_000_000_011, + data: { id: 'ctx1', role: 'user', source: { kind: 'plugin', plugin: 'context' }, content: [{ type: 'text', text: 'internal context' }] } + }, + { + type: 'tool/call', + time: 1_700_000_000_020, + data: { turn: 1, step: 1, callId: 'c1', name: 'shell', arguments: '{}' } + }, + { + type: 'tool/result', + time: 1_700_000_000_030, + data: { + turn: 1, + step: 1, + message: { + source: { kind: 'tool', callId: 'c1' }, + content: [{ type: 'tool-result', toolCallId: 'c1', content: [{ type: 'text', text: 'ok' }], isError: false }] + } + } + }, + { + type: 'assistant/message', + time: 1_700_000_000_040, + data: { + turn: 1, + step: 1, + message: { + id: 'a1', + content: [ + { type: 'reasoning', text: 'checked the command result' }, + { type: 'text', text: 'done' } + ] + } + } + }, + { type: 'turn/end', time: 1_700_000_000_050, data: { turn: 1, reason: { kind: 'completed' } } } + ] + } + }; + const turns = dshTurnsFromSession({ ctx: agent.ctx, agent }); + assert.equal(turns.length, 1); + assert.equal(turns[0].status, 'completed'); + assert.deepEqual(turns[0].items.map((item) => item.type), [ + 'userMessage', + 'commandExecution', + 'reasoning', + 'agentMessage' + ]); + assert.equal(turns[0].items[0].content[0].text, 'run it'); + assert.equal(turns[0].items[1].aggregatedOutput, 'ok\n'); + assert.deepEqual(turns[0].items[2].content, ['checked the command result']); + assert.equal(turns[0].items[3].text, 'done'); +}); diff --git a/test/dsh-compaction-projection.test.mjs b/test/dsh-compaction-projection.test.mjs new file mode 100644 index 00000000..459945e8 --- /dev/null +++ b/test/dsh-compaction-projection.test.mjs @@ -0,0 +1,126 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { dshTurnsFromSession } from '../src/dsh/codex-shapes.mjs'; +import { DshxProductAdapter } from '../src/dsh/product-adapter.mjs'; +import { DshSessionProjector } from '../src/dsh/session-projector.mjs'; + +const event = (type, data, time, seq = 0) => ({ type, data, time, seq }); + +test('automatic DSH compaction renders as a native contextCompaction item inside its real turn', () => { + const projector = new DshSessionProjector({ threadId: 'session-1' }); + projector.project(event('turn/start', { turn: 1 }, 1000, 1)); + const started = projector.project(event('compaction/start', { + compactionId: 'compact-1', + turn: 1 + }, 1100, 2)); + assert.deepEqual(started.map((entry) => entry.method), ['item/started']); + assert.deepEqual(started[0].params.item, { type: 'contextCompaction', id: 'dsh-compaction-compact-1' }); + assert.equal(started[0].params.turnId, 'dsh-turn-1'); + projector.project(event('compaction/summary', { compactionId: 'compact-1' }, 1200, 3)); + const ended = projector.project(event('compaction/end', { + compactionId: 'compact-1', + turn: 1 + }, 1300, 5)); + assert.deepEqual(ended.map((entry) => entry.method), ['item/completed']); +}); + +test('standalone manual DSH compaction uses a presentation-only maintenance turn', () => { + const projector = new DshSessionProjector({ threadId: 'session-1' }); + const started = projector.project(event('compaction/start', { + compactionId: 'manual-1', + turn: null + }, 2000, 10)); + assert.deepEqual(started.map((entry) => entry.method), ['turn/started', 'item/started']); + assert.equal(started[0].params.turn.id, 'dsh-maintenance-compaction-manual-1'); + assert.equal(started[1].params.turnId, 'dsh-maintenance-compaction-manual-1'); + projector.project(event('compaction/summary', { compactionId: 'manual-1' }, 2200, 11)); + const ended = projector.project(event('compaction/end', { + compactionId: 'manual-1', + turn: null + }, 2500, 13)); + assert.deepEqual(ended.map((entry) => entry.method), ['item/completed', 'turn/completed']); + assert.equal(ended[1].params.turn.status, 'completed'); +}); + +test('resumed human transcript keeps shadowed history and adds one compaction marker', () => { + const agent = { + id: 'session-1', + ctx: { get() { return { get() { return undefined; } }; } }, + session: { + header: { id: 'session-1', cwd: process.cwd() }, + events: [ + event('turn/start', { turn: 1 }, 1000, 1), + event('user/message', { + id: 'u1', source: { kind: 'user' }, content: [{ type: 'text', text: 'old prompt' }] + }, 1010, 2), + event('assistant/message', { + turn: 1, step: 1, message: { id: 'a1', content: [{ type: 'text', text: 'old answer' }] } + }, 1020, 3), + event('turn/end', { turn: 1, reason: { kind: 'completed' } }, 1030, 4), + event('compaction/start', { compactionId: 'manual-1', turn: null }, 2000, 5), + event('compaction/summary', { compactionId: 'manual-1', summary: [] }, 2100, 6), + event('user/message', { + id: 'checkpoint', + source: { kind: 'plugin', plugin: 'compact', compactionId: 'manual-1' }, + content: [{ type: 'text', text: 'MODEL ONLY CHECKPOINT' }], + surfaceOp: { op: 'replace', start: 2, end: 3 } + }, 2110, 7), + event('compaction/end', { compactionId: 'manual-1', turn: null }, 2200, 8) + ] + } + }; + const turns = dshTurnsFromSession({ ctx: agent.ctx, agent }); + assert.equal(turns.length, 2); + assert.deepEqual(turns[0].items.map((item) => item.type), ['userMessage', 'agentMessage']); + assert.equal(turns[0].items[0].content[0].text, 'old prompt'); + assert.equal(turns[0].items[1].text, 'old answer'); + assert.deepEqual(turns[1].items, [{ type: 'contextCompaction', id: 'dsh-compaction-manual-1' }]); + assert.equal(JSON.stringify(turns).includes('MODEL ONLY CHECKPOINT'), false); +}); + +test('thread/compact/start delegates to the official DSH command plane and reports a no-op result', async () => { + const adapter = Object.create(DshxProductAdapter.prototype); + const warnings = []; + adapter.send = (message) => warnings.push(message); + adapter.diagnostics = () => {}; + const agent = { id: 'session-1' }; + adapter.controllers = new Map([['session-1', { agent }]]); + adapter._manualCompactions = new Map(); + const commandCalls = []; + adapter.ctx = { + get(name) { + if (name === 'commands') { + return { + async execute(receivedAgent, line, images, signal) { + commandCalls.push([receivedAgent, line, images]); + assert.equal(signal.aborted, false); + return { result: { text: 'No compactable history yet.' } }; + } + }; + } + return undefined; + } + }; + + const response = adapter.threadCompactStart({ threadId: 'session-1' }); + assert.deepEqual(response.result, {}); + response.afterResponse(); + await new Promise((resolve) => setImmediate(resolve)); + assert.deepEqual(commandCalls, [[agent, '/compact', []]]); + assert.deepEqual(warnings, [{ + method: 'warning', + params: { threadId: 'session-1', message: 'No compactable history yet.' } + }]); +}); + +test('synthetic compaction turn interrupt aborts only the tracked official DSH operation', () => { + const adapter = Object.create(DshxProductAdapter.prototype); + const abort = new AbortController(); + adapter._manualCompactions = new Map([['session-1', abort]]); + const result = adapter.turnInterruptProduct({ + threadId: 'session-1', + turnId: 'dsh-maintenance-compaction-c1' + }); + assert.deepEqual(result, { result: {} }); + assert.equal(abort.signal.aborted, true); +}); diff --git a/test/dsh-domain-delegation.test.mjs b/test/dsh-domain-delegation.test.mjs new file mode 100644 index 00000000..5e1b8902 --- /dev/null +++ b/test/dsh-domain-delegation.test.mjs @@ -0,0 +1,110 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { DshxProductAdapter } from '../src/dsh/product-adapter.mjs'; + +function bareAdapter() { + const adapter = Object.create(DshxProductAdapter.prototype); + adapter.cwd = '/workspace'; + adapter.controllers = new Map(); + adapter.diagnostics = () => {}; + adapter.send = () => {}; + return adapter; +} + +test('manual compaction delegates to DSH commands.execute and does not own compaction semantics', async () => { + const adapter = bareAdapter(); + const calls = []; + const warnings = []; + adapter.warnThread = (threadId, message) => warnings.push([threadId, message]); + const commands = { + async execute(agent, line, images, signal) { + calls.push({ agent, line, images, signal }); + return { result: { kind: 'success', text: 'No compactable history yet.' } }; + } + }; + const agent = { + ctx: { get(name) { return name === 'commands' ? commands : undefined; } } + }; + const abortController = new AbortController(); + adapter._manualCompactions = new Map([['session-1', abortController]]); + + await adapter.runManualCompaction({ + threadId: 'session-1', + controller: { agent }, + abortController + }); + + assert.equal(calls.length, 1); + assert.equal(calls[0].agent, agent); + assert.equal(calls[0].line, '/compact'); + assert.deepEqual(calls[0].images, []); + assert.equal(calls[0].signal, abortController.signal); + assert.deepEqual(warnings, [['session-1', 'No compactable history yet.']]); + assert.equal(adapter._manualCompactions.size, 0); +}); + +test('thread/fork passes only a presentation anchor to DSH Host and adopts Host-owned model selection', async () => { + const adapter = bareAdapter(); + const hostCalls = []; + const modelCalls = []; + const adoptions = []; + const sourceEvents = [ + { seq: 1, type: 'turn/start', data: { turn: 1 } }, + { seq: 2, type: 'user/message', data: { id: 'u1' } }, + { seq: 3, type: 'turn/end', data: { turn: 1, reason: { kind: 'completed' } } } + ]; + const childAgent = { id: 'child-session', session: { events: [], header: { id: 'child-session' } } }; + + adapter.forkSourceEvents = async (sourceId) => { + assert.equal(sourceId, 'source-session'); + return sourceEvents; + }; + adapter.hostApi = () => ({ + async forkSession(request) { + hostCalls.push(request); + return { sessionId: 'child-session' }; + }, + async selectModel(request) { + modelCalls.push(request); + return request; + } + }); + adapter.driver = { + getLive(sessionId) { + assert.equal(sessionId, 'child-session'); + return childAgent; + }, + adoptExternalSelection(agent, options) { + adoptions.push({ agent, options }); + } + }; + adapter.installController = (handle) => { + assert.equal(handle.agent, childAgent); + const controller = { agent: childAgent }; + adapter.controllers.set('child-session', controller); + return controller; + }; + adapter.threadResponse = (agent, options) => { + assert.equal(agent, childAgent); + assert.equal(options.includeTurns, false); + return { thread: { id: 'child-session', turns: [] } }; + }; + + const response = await adapter.threadFork({ + threadId: 'source-session', + lastTurnId: 'dsh-turn-1', + excludeTurns: true + }); + + assert.deepEqual(hostCalls, [{ sessionId: 'source-session', atSeq: 3 }]); + assert.equal(adoptions.length, 1); + assert.equal(adoptions[0].agent, childAgent); + await adoptions[0].options.select({ provider: 'deepseek', model: 'reasoner', reasoningEffort: 'high' }); + assert.deepEqual(modelCalls, [{ + sessionId: 'child-session', + provider: 'deepseek', + model: 'reasoner', + reasoningEffort: 'high' + }]); + assert.equal(response.result.thread.id, 'child-session'); +}); diff --git a/test/dsh-history-pages.test.mjs b/test/dsh-history-pages.test.mjs new file mode 100644 index 00000000..94538a29 --- /dev/null +++ b/test/dsh-history-pages.test.mjs @@ -0,0 +1,67 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { dshThreadItemsPage, dshThreadTurnsPage } from '../src/dsh/history-pages.mjs'; + +function controller() { + const agent = { + id: 'session-1', + ctx: { + get(name) { + if (name === 'tools') return { get() { return undefined; } }; + return undefined; + } + }, + session: { + header: { id: 'session-1', cwd: '/work' }, + events: [ + { type: 'turn/start', time: 1000, data: { turn: 1 } }, + { type: 'user/message', time: 1010, data: { id: 'u1', source: { kind: 'user' }, content: [{ type: 'text', text: 'one' }] } }, + { type: 'assistant/message', time: 1020, data: { turn: 1, step: 1, message: { id: 'a1', content: [{ type: 'text', text: 'first' }] } } }, + { type: 'turn/end', time: 1030, data: { turn: 1, reason: { kind: 'completed' } } }, + { type: 'turn/start', time: 2000, data: { turn: 2 } }, + { type: 'user/message', time: 2010, data: { id: 'u2', source: { kind: 'user' }, content: [{ type: 'text', text: 'two' }] } }, + { type: 'assistant/message', time: 2020, data: { turn: 2, step: 1, message: { id: 'a2', content: [{ type: 'text', text: 'second' }] } } }, + { type: 'turn/end', time: 2030, data: { turn: 2, reason: { kind: 'completed' } } } + ] + } + }; + return { agent }; +} + +test('turn pages satisfy Codex descending hydration without owning history state', () => { + const first = dshThreadTurnsPage({ + controller: controller(), + params: { limit: 1, sortDirection: 'desc', itemsView: 'notLoaded' } + }); + assert.equal(first.data.length, 1); + assert.equal(first.data[0].id, 'dsh-turn-2'); + assert.equal(first.data[0].itemsView, 'notLoaded'); + assert.deepEqual(first.data[0].items, []); + assert.equal(first.nextCursor, 'dshx:1'); + assert.equal(first.backwardsCursor, null); + + const second = dshThreadTurnsPage({ + controller: controller(), + params: { cursor: first.nextCursor, limit: 1, sortDirection: 'desc', itemsView: 'notLoaded' } + }); + assert.equal(second.data[0].id, 'dsh-turn-1'); + assert.equal(second.nextCursor, null); +}); + +test('item pages preserve turn correlation and can filter one turn', () => { + const page = dshThreadItemsPage({ + controller: controller(), + params: { turnId: 'dsh-turn-2', sortDirection: 'desc', limit: 10 } + }); + assert.deepEqual(page.data.map((entry) => entry.turnId), ['dsh-turn-2', 'dsh-turn-2']); + assert.deepEqual(page.data.map((entry) => entry.item.type), ['agentMessage', 'userMessage']); + assert.equal(page.nextCursor, null); + assert.equal(page.backwardsCursor, null); +}); + +test('history cursors are opaque and invalid cursors fail closed', () => { + assert.throws( + () => dshThreadTurnsPage({ controller: controller(), params: { cursor: 'not-owned-by-dshx' } }), + /invalid DSHX history cursor/ + ); +}); diff --git a/test/dsh-local-server-close.test.mjs b/test/dsh-local-server-close.test.mjs new file mode 100644 index 00000000..c8ce6136 --- /dev/null +++ b/test/dsh-local-server-close.test.mjs @@ -0,0 +1,119 @@ +import assert from 'node:assert/strict'; +import { EventEmitter } from 'node:events'; +import { existsSync, mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { PassThrough } from 'node:stream'; +import test from 'node:test'; +import { startDshxLocalServer } from '../src/dsh/local-server.mjs'; + +class FakeChild extends EventEmitter { + constructor() { + super(); + this.stdin = new PassThrough(); + this.stdout = new PassThrough(); + this.stderr = new PassThrough(); + this.exitCode = null; + this.signalCode = null; + this.stdin.once('finish', () => this.exit(0, null)); + } + exit(code, signal) { + if (this.exitCode !== null || this.signalCode !== null) return; + this.exitCode = code; + this.signalCode = signal; + queueMicrotask(() => this.emit('exit', code, signal)); + } + kill(signal = 'SIGTERM') { + this.exit(null, signal); + return true; + } +} + +function bridgeSpawner() { + return () => { + const child = new FakeChild(); + queueMicrotask(() => child.stdout.write('{"dshxBridge":"ready"}\n')); + return child; + }; +} + +function deferred() { + let resolve; + const promise = new Promise((done) => { resolve = done; }); + return { promise, resolve }; +} + +test('concurrent close callers await the same shutdown transaction', async () => { + const root = mkdtempSync(join(tmpdir(), 'dshx-ipc-concurrent-close-test-')); + const gate = deferred(); + let adapterCloses = 0; + let disposed = 0; + + class SlowAdapter { + async handle() {} + async close() { + adapterCloses += 1; + await gate.promise; + } + } + + const server = await startDshxLocalServer({ + runtime: { async dispose() { disposed += 1; } }, + Adapter: SlowAdapter, + bridgeCommand: 'fake-dshx-ipc-bridge', + spawnBridge: bridgeSpawner(), + socketRoot: root + }); + const rendezvousDirectory = dirname(server.path); + + try { + const first = server.close(); + const second = server.close(); + assert.equal(first, second); + assert.equal(adapterCloses, 1); + assert.equal(existsSync(rendezvousDirectory), true); + + gate.resolve(); + await Promise.all([first, second]); + assert.equal(adapterCloses, 1); + assert.equal(disposed, 1); + assert.equal(existsSync(rendezvousDirectory), false); + } finally { + gate.resolve(); + rmSync(root, { recursive: true, force: true }); + } +}); + +test('shutdown preserves the first failure while logging later cleanup failures and removing rendezvous state', async () => { + const root = mkdtempSync(join(tmpdir(), 'dshx-ipc-close-root-cause-test-')); + const logs = []; + + class ThrowingAdapter { + async handle() {} + async close() { + throw new Error('primary adapter close failure'); + } + } + + const server = await startDshxLocalServer({ + runtime: { + async dispose() { + throw new Error('secondary DSH dispose failure'); + } + }, + Adapter: ThrowingAdapter, + bridgeCommand: 'fake-dshx-ipc-bridge', + spawnBridge: bridgeSpawner(), + socketRoot: root, + log: (message) => logs.push(message) + }); + const rendezvousDirectory = dirname(server.path); + + try { + await assert.rejects(server.close(), /primary adapter close failure/); + assert.ok(logs.some((message) => message.includes('shutdown cleanup: DSH dispose failed: secondary DSH dispose failure'))); + assert.equal(existsSync(rendezvousDirectory), false); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/test/dsh-local-server.test.mjs b/test/dsh-local-server.test.mjs new file mode 100644 index 00000000..37ac1532 --- /dev/null +++ b/test/dsh-local-server.test.mjs @@ -0,0 +1,311 @@ +import assert from 'node:assert/strict'; +import { EventEmitter } from 'node:events'; +import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, win32 } from 'node:path'; +import { PassThrough } from 'node:stream'; +import test from 'node:test'; +import { localServerInternals, startDshxLocalServer } from '../src/dsh/local-server.mjs'; + +class FakeChild extends EventEmitter { + constructor() { + super(); + this.stdin = new PassThrough(); + this.stdout = new PassThrough(); + this.stderr = new PassThrough(); + this.exitCode = null; + this.signalCode = null; + this.killed = false; + this.stdin.once('finish', () => this.exit(0, null)); + } + exit(code, signal) { + if (this.exitCode !== null || this.signalCode !== null) return; + this.exitCode = code; + this.signalCode = signal; + queueMicrotask(() => this.emit('exit', code, signal)); + } + kill(signal = 'SIGTERM') { + this.killed = true; + this.exit(null, signal); + return true; + } +} + +function fakeBridgeSpawner() { + let child; + return { + spawn() { + child = new FakeChild(); + queueMicrotask(() => child.stdout.write('{"dshxBridge":"ready"}\n')); + return child; + }, + child() { + return child; + } + }; +} + +function readJsonLine(stream) { + return new Promise((resolve, reject) => { + let buffer = ''; + const onData = (chunk) => { + buffer += chunk.toString('utf8'); + const newline = buffer.indexOf('\n'); + if (newline < 0) return; + stream.off('data', onData); + try { + resolve(JSON.parse(buffer.slice(0, newline))); + } catch (error) { + reject(error); + } + }; + stream.on('data', onData); + }); +} + +class FakeAdapter { + static closes = 0; + constructor({ send }) { + this.send = send; + } + async handle(message) { + return { response: { id: message.id, result: { echoed: message.method } } }; + } + async close() { + FakeAdapter.closes += 1; + } +} + +test('Windows default local IPC root is anchored below DSHX user presentation home, not TEMP', () => { + const root = localServerInternals.defaultSocketRoot({ + platform: 'win32', + home: 'C:\\Users\\Alice\\.dshx\\codex-tui', + temporaryDirectory: 'C:\\shared-temp', + userHome: 'C:\\Users\\Alice' + }); + assert.equal(root, win32.resolve('C:\\Users\\Alice\\.dshx\\codex-tui', 'i')); + assert.doesNotMatch(root.toLowerCase(), /shared-temp/); +}); + +test('Windows fallback IPC root remains below the current user home when no presentation home is supplied', () => { + const root = localServerInternals.defaultSocketRoot({ + platform: 'win32', + temporaryDirectory: 'C:\\shared-temp', + userHome: 'C:\\Users\\Alice' + }); + assert.equal(root, win32.resolve('C:\\Users\\Alice', '.dshx', 'codex-tui', 'i')); +}); + +test('Windows custom presentation home cannot escape the current user profile ACL boundary', () => { + assert.throws(() => localServerInternals.defaultSocketRoot({ + platform: 'win32', + home: 'C:\\shared-temp\\dshx', + userHome: 'C:\\Users\\Alice' + }), /must remain under the Windows user profile/); + assert.throws(() => localServerInternals.defaultSocketRoot({ + platform: 'win32', + home: 'D:\\dshx', + userHome: 'C:\\Users\\Alice' + }), /must remain under the Windows user profile/); + assert.equal( + localServerInternals.isPathWithin('C:\\Users\\Alice', 'c:\\users\\alice\\.dshx', win32), + true + ); +}); + +test('Windows local IPC path guard measures UTF-8 bytes and reserves the terminating NUL', () => { + const max = localServerInternals.WINDOWS_UNIX_PATH_MAX; + assert.equal(max, 108); + assert.doesNotThrow(() => localServerInternals.assertSocketPathSupported( + `C:\\${'a'.repeat(104)}`, + { platform: 'win32' } + )); + assert.throws(() => localServerInternals.assertSocketPathSupported( + `C:\\${'a'.repeat(105)}`, + { platform: 'win32' } + ), /requires fewer than 108/); + assert.throws(() => localServerInternals.assertSocketPathSupported( + `C:\\${'你'.repeat(35)}`, + { platform: 'win32' } + ), /108 UTF-8 bytes/); +}); + +test('Unix default local IPC root stays in tmp; createSocketDirectory enforces an ephemeral subdirectory', () => { + const root = mkdtempSync(join(tmpdir(), 'dshx-ipc-root-test-')); + try { + assert.equal(localServerInternals.defaultSocketRoot({ platform: 'linux', temporaryDirectory: root }), root); + const child = localServerInternals.createSocketDirectory(root); + assert.match(child, /^.*d-/); + assert.notEqual(child, root); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test('local production transport exposes only a unix endpoint and relays RPC over bridge stdio', async () => { + let disposed = 0; + FakeAdapter.closes = 0; + const fake = fakeBridgeSpawner(); + const runtime = { async dispose() { disposed += 1; } }; + const server = await startDshxLocalServer({ + runtime, + Adapter: FakeAdapter, + bridgeCommand: 'fake-dshx-ipc-bridge', + spawnBridge: (...args) => fake.spawn(...args) + }); + try { + assert.match(server.url, /^unix:\/\//); + assert.equal('token' in server, false); + + const response = readJsonLine(fake.child().stdin); + fake.child().stdout.write(`${JSON.stringify({ id: 7, method: 'initialize', params: {} })}\n`); + assert.deepEqual(await response, { id: 7, result: { echoed: 'initialize' } }); + } finally { + await server.close(); + } + assert.equal(FakeAdapter.closes, 1); + assert.equal(disposed, 1); +}); + +test('startup rollback disposes DSH, terminates the bridge, and removes rendezvous state when Adapter construction fails', async () => { + const root = mkdtempSync(join(tmpdir(), 'dshx-ipc-adapter-startup-test-')); + const fake = fakeBridgeSpawner(); + let disposed = 0; + class ThrowingAdapter { + constructor() { + throw new Error('synthetic Adapter constructor failure'); + } + } + const runtime = { async dispose() { disposed += 1; } }; + try { + await assert.rejects(startDshxLocalServer({ + runtime, + Adapter: ThrowingAdapter, + bridgeCommand: 'fake-dshx-ipc-bridge', + spawnBridge: (...args) => fake.spawn(...args), + socketRoot: root + }), /synthetic Adapter constructor failure/); + assert.equal(disposed, 1); + assert.ok(fake.child().exitCode !== null || fake.child().signalCode !== null); + assert.deepEqual(readdirSync(root), []); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test('startup rollback removes the rendezvous directory when official DSH boot fails before bridge spawn', async () => { + const root = mkdtempSync(join(tmpdir(), 'dshx-ipc-boot-startup-test-')); + let spawnCount = 0; + try { + await assert.rejects(startDshxLocalServer({ + bootRuntime: async () => { + throw new Error('synthetic DSH boot failure'); + }, + spawnBridge: () => { + spawnCount += 1; + return new FakeChild(); + }, + socketRoot: root + }), /synthetic DSH boot failure/); + assert.equal(spawnCount, 0); + assert.deepEqual(readdirSync(root), []); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test('startup rollback disposes an injected runtime when socket-root creation fails and preserves the filesystem error', async () => { + const parent = mkdtempSync(join(tmpdir(), 'dshx-ipc-root-failure-test-')); + const blockedRoot = join(parent, 'not-a-directory'); + writeFileSync(blockedRoot, 'block mkdir'); + let disposed = 0; + const runtime = { async dispose() { disposed += 1; } }; + try { + await assert.rejects(startDshxLocalServer({ + runtime, + socketRoot: blockedRoot + }), (error) => { + assert.ok(['EEXIST', 'ENOTDIR'].includes(error?.code), `unexpected error code: ${error?.code}`); + return true; + }); + assert.equal(disposed, 1); + assert.equal(existsSync(blockedRoot), true); + } finally { + rmSync(parent, { recursive: true, force: true }); + } +}); + +test('startup rollback keeps the original failure when DSH disposal also fails', async () => { + const root = mkdtempSync(join(tmpdir(), 'dshx-ipc-double-failure-test-')); + const fake = fakeBridgeSpawner(); + const logs = []; + class ThrowingAdapter { + constructor() { + throw new Error('primary startup failure'); + } + } + const runtime = { + async dispose() { + throw new Error('secondary dispose failure'); + } + }; + try { + await assert.rejects(startDshxLocalServer({ + runtime, + Adapter: ThrowingAdapter, + bridgeCommand: 'fake-dshx-ipc-bridge', + spawnBridge: (...args) => fake.spawn(...args), + socketRoot: root, + log: (message) => logs.push(message) + }), /primary startup failure/); + assert.ok(logs.some((message) => message.includes('startup rollback: DSH dispose failed: secondary dispose failure'))); + assert.deepEqual(readdirSync(root), []); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test('close removes the private rendezvous directory even when DSH disposal fails', async () => { + const root = mkdtempSync(join(tmpdir(), 'dshx-ipc-close-test-')); + const fake = fakeBridgeSpawner(); + const runtime = { + async dispose() { + throw new Error('synthetic DSH dispose failure'); + } + }; + const server = await startDshxLocalServer({ + runtime, + Adapter: FakeAdapter, + bridgeCommand: 'fake-dshx-ipc-bridge', + spawnBridge: (...args) => fake.spawn(...args), + socketRoot: root + }); + const rendezvousDirectory = dirname(server.path); + assert.equal(existsSync(rendezvousDirectory), true); + try { + await assert.rejects(server.close(), /synthetic DSH dispose failure/); + assert.equal(existsSync(rendezvousDirectory), false); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test('local production transport returns JSON-RPC parse errors through bridge stdio', async () => { + const fake = fakeBridgeSpawner(); + const runtime = { async dispose() {} }; + const server = await startDshxLocalServer({ + runtime, + Adapter: FakeAdapter, + bridgeCommand: 'fake-dshx-ipc-bridge', + spawnBridge: (...args) => fake.spawn(...args) + }); + try { + const response = readJsonLine(fake.child().stdin); + fake.child().stdout.write('{not-json}\n'); + const parsed = await response; + assert.equal(parsed.id, null); + assert.equal(parsed.error.code, -32700); + } finally { + await server.close(); + } +}); diff --git a/test/dsh-permissions.test.mjs b/test/dsh-permissions.test.mjs new file mode 100644 index 00000000..52f03883 --- /dev/null +++ b/test/dsh-permissions.test.mjs @@ -0,0 +1,124 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + DshPermissionView, + mapCodexDecisionToDshApprovalOutcome, + mapDshApprovalOutcomeToCodexDecision +} from '../src/dsh/permissions.mjs'; + +function fixture({ sandboxMode = 'workspace-write', approvalPolicy = 'ask', currentPreset = 'workspace-write' } = {}) { + const calls = []; + const session = { events: [], id: 'session-1' }; + const agent = { session }; + const specs = { + 'workspace-write': { + sandbox: 'workspace-write', + approval: 'ask', + name: 'Workspace write', + description: 'Write in workspace with approval prompts.' + }, + 'danger-full-access': { + sandbox: 'danger-full-access', + approval: 'never', + name: 'Full access', + description: 'Unconfined file access.' + } + }; + const services = { + permissionPresets: { + names: Object.keys(specs), + current(events) { calls.push(['permissionPresets.current', events]); return currentPreset; }, + optionOf(name) { + if (name === 'custom') return { value: 'custom', name: 'Custom' }; + return { value: name, name: specs[name].name, description: specs[name].description }; + }, + resolve(name) { return specs[name]; }, + set(target, name) { calls.push(['permissionPresets.set', target, name]); } + }, + sandboxPolicy: { + resolve({ session: target }) { + calls.push(['sandboxPolicy.resolve', target]); + return { mode: sandboxMode, workspaceRoot: '/workspace', sessionId: target.id }; + } + }, + approval: { + config: { policy: approvalPolicy }, + overrideOf(target) { calls.push(['approval.overrideOf', target]); return undefined; } + } + }; + return { + agent, + calls, + ctx: { get(name) { return services[name]; } } + }; +} + +test('workspace-write is presented as external sandbox, not fake Codex-native workspaceWrite', () => { + const fx = fixture(); + const view = new DshPermissionView(fx.ctx); + const current = view.current(fx.agent); + + assert.equal(current.preset, 'workspace-write'); + assert.equal(current.dsh.sandboxMode, 'workspace-write'); + assert.equal(current.dsh.approvalPolicy, 'ask'); + assert.deepEqual(current.codex, { + approvalPolicy: 'on-request', + approvalsReviewer: 'user', + sandbox: { type: 'externalSandbox', networkAccess: 'enabled' } + }); +}); + +test('danger-full-access has an exact Codex legacy sandbox representation', () => { + const fx = fixture({ + sandboxMode: 'danger-full-access', + approvalPolicy: 'never', + currentPreset: 'danger-full-access' + }); + const current = new DshPermissionView(fx.ctx).current(fx.agent); + assert.deepEqual(current.codex, { + approvalPolicy: 'never', + approvalsReviewer: 'user', + sandbox: { type: 'dangerFullAccess' } + }); +}); + +test('custom preset is presentation-only and included only when actually current', () => { + const fx = fixture({ sandboxMode: 'read-only', currentPreset: 'custom' }); + const current = new DshPermissionView(fx.ctx, { legacyNetworkAccess: 'restricted' }).current(fx.agent); + assert.equal(current.preset, 'custom'); + assert.equal(current.options.at(-1).value, 'custom'); + assert.deepEqual(current.codex.sandbox, { type: 'externalSandbox', networkAccess: 'restricted' }); +}); + +test('permission changes delegate to the official permissionPresets setter', () => { + const fx = fixture(); + const view = new DshPermissionView(fx.ctx); + view.set(fx.agent, 'danger-full-access'); + assert.deepEqual(fx.calls.find(([name]) => name === 'permissionPresets.set'), [ + 'permissionPresets.set', + fx.agent.session, + 'danger-full-access' + ]); +}); + +test('DSH approval outcomes map only to Codex decisions with matching semantics', () => { + assert.equal(mapDshApprovalOutcomeToCodexDecision('allowed-once'), 'accept'); + assert.equal(mapDshApprovalOutcomeToCodexDecision('rejected'), 'decline'); + assert.equal(mapDshApprovalOutcomeToCodexDecision('unavailable'), 'decline'); + assert.equal(mapDshApprovalOutcomeToCodexDecision('cancelled'), 'cancel'); +}); + +test('Codex persistent approval is rejected because DSH exposes only allowed-once', () => { + assert.equal(mapCodexDecisionToDshApprovalOutcome('accept'), 'allowed-once'); + assert.equal(mapCodexDecisionToDshApprovalOutcome('decline'), 'rejected'); + assert.equal(mapCodexDecisionToDshApprovalOutcome('cancel'), 'cancelled'); + assert.throws( + () => mapCodexDecisionToDshApprovalOutcome('acceptForSession'), + /no session-wide approval grant/ + ); +}); + +test('missing permission service is a hard compatibility error', () => { + const view = new DshPermissionView({ get() { return undefined; } }); + assert.throws(() => view.current({ session: { events: [] } }), /permissionPresets/); +}); diff --git a/test/dsh-plan-presentation.test.mjs b/test/dsh-plan-presentation.test.mjs new file mode 100644 index 00000000..0c2170f2 --- /dev/null +++ b/test/dsh-plan-presentation.test.mjs @@ -0,0 +1,68 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + codexCollaborationMode, + codexPlanTarget, + effectiveDshPlanState, + threadSettingsUpdatedNotification +} from '../src/dsh/plan-presentation.mjs'; + +test('Codex plan/default modes map only to the DSH boolean plan target', () => { + assert.equal(codexPlanTarget({ mode: 'plan', settings: {} }), true); + assert.equal(codexPlanTarget({ mode: 'default', settings: {} }), false); + assert.throws(() => codexPlanTarget({ mode: 'review', settings: {} }), /cannot map Codex collaboration mode/); + assert.throws(() => codexPlanTarget(null), /must be a Codex collaboration mode object/); +}); + +test('pending DSH plan selection is the effective UI state until committed', () => { + assert.equal(effectiveDshPlanState({ active: false, pending: true }), true); + assert.equal(effectiveDshPlanState({ active: true, pending: false }), false); + assert.equal(effectiveDshPlanState({ active: true }), true); + assert.equal(effectiveDshPlanState({ active: false }), false); +}); + +test('Codex collaboration presentation never copies developer instructions into DSH state', () => { + assert.deepEqual(codexCollaborationMode({ + model: 'dshx:model', + reasoningEffort: 'high', + active: true + }), { + mode: 'plan', + settings: { + model: 'dshx:model', + reasoning_effort: 'high', + developer_instructions: null + } + }); +}); + +test('thread settings notification projects DSH plan state into pinned Codex wire shape', () => { + const notification = threadSettingsUpdatedNotification({ + threadId: 'session-1', + response: { + cwd: '/workspace', + approvalPolicy: 'on-request', + approvalsReviewer: 'user', + sandbox: { type: 'workspaceWrite', writableRoots: ['/workspace'], networkAccess: false, excludeTmpdirEnvVar: false, excludeSlashTmp: false }, + activePermissionProfile: { id: ':workspace', extends: null }, + model: 'dshx:model', + modelProvider: 'deepseek', + serviceTier: null, + reasoningEffort: null, + multiAgentMode: 'explicitRequestOnly' + }, + planState: { active: false, pending: true } + }); + + assert.equal(notification.method, 'thread/settings/updated'); + assert.equal(notification.params.threadId, 'session-1'); + assert.equal(notification.params.threadSettings.collaborationMode.mode, 'plan'); + assert.deepEqual(notification.params.threadSettings.collaborationMode.settings, { + model: 'dshx:model', + reasoning_effort: null, + developer_instructions: null + }); + assert.equal(notification.params.threadSettings.activePermissionProfile.id, ':workspace'); + assert.equal(notification.params.threadSettings.summary, null); + assert.equal(notification.params.threadSettings.personality, null); +}); diff --git a/test/dsh-product-adapter.test.mjs b/test/dsh-product-adapter.test.mjs new file mode 100644 index 00000000..d748a81d --- /dev/null +++ b/test/dsh-product-adapter.test.mjs @@ -0,0 +1,252 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import test from 'node:test'; +import { DshxProductAdapter } from '../src/dsh/product-adapter.mjs'; + +function bareAdapter() { + const adapter = Object.create(DshxProductAdapter.prototype); + adapter.cwd = path.resolve('/workspace'); + adapter.diagnostics = () => {}; + adapter.controllers = new Map(); + return adapter; +} + +function skillRegistry(absoluteSkill, calls = []) { + return { + async snapshot(options) { + calls.push(['snapshot', options]); + return { + complete: true, + skills: [ + { + name: 'review', + description: 'Review code', + invocation: { modelInvocable: true, userInvocable: true }, + source: 'project-agents', + provider: 'filesystem' + }, + { + name: 'remote-skill', + description: 'Remote skill', + invocation: { modelInvocable: true, userInvocable: true }, + source: 'custom', + provider: 'remote' + }, + { + name: 'model-only', + description: 'Model only', + invocation: { modelInvocable: true, userInvocable: false }, + source: 'runtime', + provider: 'runtime' + } + ] + }; + }, + async get(name) { + calls.push(['get', name]); + if (name === 'review') { + return { + name, + description: 'Review code', + invocation: { modelInvocable: true, userInvocable: true }, + source: 'project-agents', + provider: 'filesystem', + content: 'review', + path: absoluteSkill + }; + } + if (name === 'remote-skill') { + return { + name, + description: 'Remote skill', + invocation: { modelInvocable: true, userInvocable: true }, + source: 'custom', + provider: 'remote', + content: 'remote' + }; + } + return undefined; + } + }; +} + +test('skills/list reads DSH public registry and only returns faithfully path-backed user skills', async () => { + const adapter = bareAdapter(); + const absoluteSkill = path.resolve('/workspace/.agents/skills/review/SKILL.md'); + const calls = []; + adapter.ctx = { + get(name) { + return name === 'skills' ? skillRegistry(absoluteSkill, calls) : undefined; + } + }; + + const result = await adapter.skillsList({ cwds: [] }); + assert.equal(calls[0][0], 'snapshot'); + assert.equal(calls[0][1].cwd, path.resolve('/workspace')); + assert.equal(result.result.data.length, 1); + assert.deepEqual(result.result.data[0].skills, [{ + name: 'review', + description: 'Review code', + path: absoluteSkill, + scope: 'repo', + enabled: true + }]); +}); + +test('skills/list accepts pinned Codex startup forceReload as a fresh DSH snapshot read', async () => { + const adapter = bareAdapter(); + const absoluteSkill = path.resolve('/workspace/.agents/skills/review/SKILL.md'); + const calls = []; + const diagnostics = []; + adapter.diagnostics = (message) => diagnostics.push(message); + adapter.ctx = { + get(name) { + return name === 'skills' ? skillRegistry(absoluteSkill, calls) : undefined; + } + }; + + const result = await adapter.skillsList({ cwds: [path.resolve('/workspace')], forceReload: true }); + assert.equal(calls.filter(([kind]) => kind === 'snapshot').length, 1); + assert.equal(result.result.data[0].skills[0].name, 'review'); + assert.match(diagnostics.join('\n'), /provider-owned cache policy/); +}); + +test('thread/settings/update maps Codex workspace profile to the DSH canonical preset', async () => { + const adapter = bareAdapter(); + const applied = []; + const agent = { session: { header: { cwd: path.resolve('/workspace') } } }; + adapter.controllers.set('session-1', { agent }); + adapter.ctx = { + get(name) { + if (name === 'permissionPresets') return { names: ['workspace-write', 'danger-full-access'] }; + return undefined; + } + }; + adapter.permissions = { + set(receivedAgent, preset) { + assert.equal(receivedAgent, agent); + applied.push(preset); + }, + current() { + return { + preset: 'workspace-write', + codex: { approvalPolicy: 'on-request' }, + dsh: { sandboxMode: 'workspace-write', approvalPolicy: 'ask' } + }; + } + }; + adapter.applyModelOverride = async () => { throw new Error('model path should not run'); }; + + const result = await adapter.threadSettingsUpdate({ + threadId: 'session-1', + permissions: ':workspace', + approvalPolicy: 'on-request' + }); + assert.deepEqual(applied, ['workspace-write']); + assert.deepEqual(result, { result: {} }); +}); + +test('thread/settings/update delegates model/effort and persists the DSH deployment default', async () => { + const adapter = bareAdapter(); + const agent = { session: { header: { cwd: path.resolve('/workspace') } } }; + adapter.controllers.set('session-1', { agent }); + const modelCalls = []; + const saved = []; + adapter.ctx = { + get(name) { + if (name === 'agentDefaultModel') { + return { async saveSelection(selection) { saved.push(selection); } }; + } + return undefined; + } + }; + adapter.applyModelOverride = async (receivedAgent, update) => { + assert.equal(receivedAgent, agent); + modelCalls.push(update); + return { provider: 'deepseek', model: 'deepseek-chat', reasoningEffort: 'high' }; + }; + adapter.applyStartPermissions = async () => { throw new Error('permission path should not run'); }; + + const result = await adapter.threadSettingsUpdate({ + threadId: 'session-1', + model: 'dshx:opaque-model', + effort: 'high', + collaborationMode: { mode: 'default', settings: {} } + }); + assert.deepEqual(modelCalls, [{ model: 'dshx:opaque-model', effort: 'high' }]); + assert.deepEqual(saved, [{ provider: 'deepseek', model: 'deepseek-chat', reasoningEffort: 'high' }]); + assert.deepEqual(result, { result: {} }); +}); + +test('missing DSH settings provider does not invalidate the current session model switch', async () => { + const adapter = bareAdapter(); + const diagnostics = []; + adapter.diagnostics = (message) => diagnostics.push(message); + const agent = { session: { header: { cwd: path.resolve('/workspace') } } }; + adapter.controllers.set('session-1', { agent }); + adapter.ctx = { get() { return undefined; } }; + adapter.applyModelOverride = async () => ({ provider: 'deepseek', model: 'chat' }); + + const result = await adapter.threadSettingsUpdate({ threadId: 'session-1', effort: 'medium' }); + assert.deepEqual(result, { result: {} }); + assert.match(diagnostics.join('\n'), /deployment default was not persisted/); +}); + +test('thread/settings/update refuses unsupported Codex-only settings when they are the actual requested change', async () => { + const adapter = bareAdapter(); + adapter.controllers.set('session-1', { + agent: { session: { header: { cwd: path.resolve('/workspace') } } } + }); + await assert.rejects( + () => adapter.threadSettingsUpdate({ + threadId: 'session-1', + collaborationMode: { mode: 'plan', settings: {} } + }), + /no equivalent public collaboration-mode/ + ); + await assert.rejects( + () => adapter.threadSettingsUpdate({ + threadId: 'session-1', + personality: 'friendly' + }), + /no equivalent public personality/ + ); +}); + +test('turn/steer delegates only to the exact active DSH turn', () => { + const adapter = bareAdapter(); + const steered = []; + adapter.controllers.set('session-1', { + currentLocation() { return { threadId: 'session-1', turnId: 'dsh-turn-4' }; }, + steer(text) { steered.push(text); } + }); + + const result = adapter.turnSteer({ + threadId: 'session-1', + expectedTurnId: 'dsh-turn-4', + input: [ + { type: 'text', text: 'change direction', text_elements: [] }, + { type: 'text', text: 'keep tests', text_elements: [] } + ] + }); + assert.deepEqual(result, { result: { turnId: 'dsh-turn-4' } }); + assert.deepEqual(steered, ['change direction\nkeep tests']); +}); + +test('turn/steer rejects stale expectedTurnId instead of steering a different DSH turn', () => { + const adapter = bareAdapter(); + let called = false; + adapter.controllers.set('session-1', { + currentLocation() { return { threadId: 'session-1', turnId: 'dsh-turn-5' }; }, + steer() { called = true; } + }); + assert.throws( + () => adapter.turnSteer({ + threadId: 'session-1', + expectedTurnId: 'dsh-turn-4', + input: [{ type: 'text', text: 'late', text_elements: [] }] + }), + /does not match active DSH turn/ + ); + assert.equal(called, false); +}); diff --git a/test/dsh-product-fork.test.mjs b/test/dsh-product-fork.test.mjs new file mode 100644 index 00000000..e6271e92 --- /dev/null +++ b/test/dsh-product-fork.test.mjs @@ -0,0 +1,131 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { DshxProductAdapter } from '../src/dsh/product-adapter.mjs'; + +function sourceAgent() { + return { + id: 'source-session', + session: { + header: { id: 'source-session', cwd: process.cwd() }, + events: [ + { seq: 1, type: 'turn/start', data: { turn: 1 } }, + { seq: 2, type: 'turn/end', data: { turn: 1, reason: { kind: 'completed' } } } + ] + } + }; +} + +function adapterFixture({ cold = false } = {}) { + const adapter = Object.create(DshxProductAdapter.prototype); + adapter.controllers = new Map(); + adapter.send = () => {}; + const source = sourceAgent(); + const target = { + id: 'fork-session', + session: { + header: { id: 'fork-session', parentSession: source.id, cwd: process.cwd() }, + events: source.session.events + } + }; + const driverCalls = []; + const hostCalls = []; + const adoptions = []; + adapter.driver = { + getLive(id) { + driverCalls.push(['getLive', id]); + if (!cold && id === source.id) return source; + if (id === target.id) return target; + return undefined; + }, + async inspectSession(id) { + driverCalls.push(['inspectSession', id]); + assert.equal(id, source.id); + return { id, events: source.session.events }; + }, + adoptExternalSelection(agent, options) { + driverCalls.push(['adoptExternalSelection', agent.id]); + adoptions.push({ agent, options }); + } + }; + adapter.hostApi = () => ({ + async forkSession(options) { + hostCalls.push(['forkSession', options]); + return { sessionId: target.id }; + }, + async selectModel(options) { + hostCalls.push(['selectModel', options]); + return options; + } + }); + adapter.installController = (handle) => { + const controller = { agent: handle.agent }; + adapter.controllers.set(String(handle.agent.id), controller); + return controller; + }; + adapter.threadResponse = (agent, options) => ({ + thread: { + id: String(agent.id), + forkedFromId: source.id, + turns: options.includeTurns ? ['history'] : [] + } + }); + return { adapter, source, target, driverCalls, hostCalls, adoptions }; +} + +test('thread/fork translates the Codex turn anchor and delegates creation to DSH Host', async () => { + const fx = adapterFixture(); + const response = await fx.adapter.threadFork({ + threadId: fx.source.id, + lastTurnId: 'dsh-turn-1', + excludeTurns: true + }); + + assert.deepEqual(fx.hostCalls[0], [ + 'forkSession', + { sessionId: fx.source.id, atSeq: 2 } + ]); + assert.equal(fx.adoptions.length, 1); + assert.equal(fx.adoptions[0].agent, fx.target); + assert.equal(response.result.thread.id, fx.target.id); + assert.deepEqual(response.result.thread.turns, []); + assert.equal(typeof response.afterResponse, 'function'); + + await fx.adoptions[0].options.select({ + provider: 'deepseek', + model: 'reasoner', + reasoningEffort: 'high' + }); + assert.deepEqual(fx.hostCalls[1], [ + 'selectModel', + { + sessionId: fx.target.id, + provider: 'deepseek', + model: 'reasoner', + reasoningEffort: 'high' + } + ]); +}); + +test('cold thread/fork reads DSH persistence without temporarily resuming or cloning source state', async () => { + const fx = adapterFixture({ cold: true }); + await fx.adapter.threadFork({ threadId: fx.source.id }); + + assert.ok(fx.driverCalls.some((call) => call[0] === 'inspectSession' && call[1] === fx.source.id)); + assert.equal(fx.driverCalls.some((call) => call[0] === 'resume'), false); + assert.deepEqual(fx.hostCalls[0], [ + 'forkSession', + { sessionId: fx.source.id, atSeq: undefined } + ]); +}); + +test('thread/fork rejects Codex-only fork state DSH cannot faithfully own', async () => { + const fx = adapterFixture(); + await assert.rejects( + () => fx.adapter.threadFork({ threadId: fx.source.id, deferGoalContinuation: true }), + /no Codex thread-goal continuation state/ + ); + await assert.rejects( + () => fx.adapter.threadFork({ threadId: fx.source.id, ephemeral: true }), + /does not expose ephemeral Codex forks/ + ); +}); diff --git a/test/dsh-product-resume.test.mjs b/test/dsh-product-resume.test.mjs new file mode 100644 index 00000000..4e2e115f --- /dev/null +++ b/test/dsh-product-resume.test.mjs @@ -0,0 +1,39 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { DshxProductAdapter } from '../src/dsh/product-adapter.mjs'; + +function adapterFixture() { + const adapter = Object.create(DshxProductAdapter.prototype); + adapter.cwd = process.cwd(); + adapter.controllers = new Map(); + adapter.driver = { getLive() { return undefined; } }; + adapter.applyModelOverride = async () => {}; + adapter.applyStartPermissions = async () => {}; + return adapter; +} + +test('thread/resume excludeTurns keeps the response bounded for Codex paginated hydration', async () => { + const adapter = adapterFixture(); + const agent = { session: { header: { cwd: process.cwd() } } }; + adapter.controllers.set('session-1', { agent }); + const calls = []; + adapter.threadResponse = (receivedAgent, options) => { + assert.equal(receivedAgent, agent); + calls.push(options); + return { thread: { id: 'session-1', turns: options.includeTurns ? ['large-history'] : [] } }; + }; + + const response = await adapter.threadResume({ threadId: 'session-1', excludeTurns: true }); + assert.deepEqual(calls, [{ includeTurns: false }]); + assert.deepEqual(response.result.thread.turns, []); +}); + +test('legacy-compatible resume can still request inline turns when excludeTurns is absent', async () => { + const adapter = adapterFixture(); + const agent = { session: { header: { cwd: process.cwd() } } }; + adapter.controllers.set('session-1', { agent }); + adapter.threadResponse = (_agent, options) => ({ thread: { turns: options.includeTurns ? ['history'] : [] } }); + + const response = await adapter.threadResume({ threadId: 'session-1' }); + assert.deepEqual(response.result.thread.turns, ['history']); +}); diff --git a/test/dsh-rc5-acceptance-regressions.test.mjs b/test/dsh-rc5-acceptance-regressions.test.mjs new file mode 100644 index 00000000..1a4321f3 --- /dev/null +++ b/test/dsh-rc5-acceptance-regressions.test.mjs @@ -0,0 +1,103 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { encodeDshModel } from '../src/dsh/codex-shapes.mjs'; +import { DshxReleaseAdapter } from '../src/dsh/release-adapter.mjs'; + +test('thread/loaded/list returns thread ids rather than thread maps', () => { + const adapter = Object.create(DshxReleaseAdapter.prototype); + adapter.driver = { + listLive() { return [{ id: 'root-1' }, { id: 'child-2' }]; } + }; + assert.deepEqual(adapter.loadedThreadList(), { + result: { data: ['root-1', 'child-2'] } + }); +}); + +test('config/batchWrite delegates model defaults to official DSH persistence', async () => { + const saved = []; + const resolved = []; + const adapter = Object.create(DshxReleaseAdapter.prototype); + adapter.home = '/tmp/dshx-presentation'; + adapter.ctx = { + get(name) { + if (name === 'agentDefaultModel') { + return { + currentSelection() { + return { provider: 'deepseek', model: 'old-model', reasoningEffort: 'high' }; + }, + async saveSelection(selection) { saved.push(selection); } + }; + } + if (name === 'llm') { + return { + async resolveCallConfig(selection) { + resolved.push(selection); + return { + provider: selection.provider, + model: selection.model, + ...(selection.reasoningEffort === undefined ? {} : { reasoningEffort: selection.reasoningEffort }) + }; + } + }; + } + return undefined; + } + }; + + const model = encodeDshModel({ provider: 'opencode-free', model: 'x-preview-f-free' }); + const response = await adapter.configBatchWrite({ + edits: [ + { keyPath: 'model', value: model, mergeStrategy: 'replace' }, + { keyPath: 'model_reasoning_effort', value: null, mergeStrategy: 'replace' } + ], + reloadUserConfig: true + }); + + assert.deepEqual(resolved, [{ provider: 'opencode-free', model: 'x-preview-f-free' }]); + assert.deepEqual(saved, [{ provider: 'opencode-free', model: 'x-preview-f-free' }]); + assert.equal(response.result.status, 'ok'); + assert.match(response.result.version, /^dshx-/); + assert.equal(response.result.overriddenMetadata, null); +}); + +test('rich user turn strips Codex null effort before the DSH-owned base path', async () => { + const adapter = Object.create(DshxReleaseAdapter.prototype); + const agent = { session: { header: { origin: 'user' } } }; + adapter.controllers = new Map([['thread-1', { + agent, + prepareUserContent() { return () => {}; } + }]]); + adapter.ctx = { get() { return undefined; } }; + + const product = Object.getPrototypeOf(DshxReleaseAdapter.prototype); + const originalDispatch = product.dispatch; + let forwarded; + product.dispatch = async (method, params) => { + forwarded = { method, params }; + return { result: {} }; + }; + try { + await adapter.richUserTurn('turn/start', { + threadId: 'thread-1', + effort: null, + input: [{ type: 'text', text: 'hello', text_elements: [] }] + }); + } finally { + product.dispatch = originalDispatch; + } + + assert.equal(forwarded.method, 'turn/start'); + assert.equal(forwarded.params.effort, undefined); +}); + +test('config/batchWrite fails closed on Codex-owned settings', async () => { + const adapter = Object.create(DshxReleaseAdapter.prototype); + adapter.home = '/tmp/dshx-presentation'; + adapter.ctx = { get() { return undefined; } }; + await assert.rejects( + () => adapter.configBatchWrite({ + edits: [{ keyPath: 'features.web_search', value: true, mergeStrategy: 'replace' }] + }), + /refuses Codex-owned setting/ + ); +}); \ No newline at end of file diff --git a/test/dsh-release-title.test.mjs b/test/dsh-release-title.test.mjs new file mode 100644 index 00000000..b0dd5a3d --- /dev/null +++ b/test/dsh-release-title.test.mjs @@ -0,0 +1,113 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { DshxReleaseAdapter } from '../src/dsh/release-adapter.mjs'; +import { foldDshSessionTitle } from '../src/dsh/thread-title.mjs'; + +test('durable title fold is latest-wins over DSH session/title events', () => { + assert.equal(foldDshSessionTitle([ + { type: 'session/title', data: { title: 'First' } }, + { type: 'user/message', data: {} }, + { type: 'session/title', data: { title: 'Pinned rename' } } + ]), 'Pinned rename'); + assert.equal(foldDshSessionTitle([]), null); +}); + +test('thread/name/set delegates loaded rename to DSH and notifies only after RPC response', async () => { + const adapter = Object.create(DshxReleaseAdapter.prototype); + const sent = []; + const agent = { id: 'session-1', session: { events: [] } }; + adapter.controllers = new Map([['session-1', { agent }]]); + adapter.send = (message) => sent.push(message); + adapter.driver = { + getLive() { return undefined; }, + renameTitle(received, title) { + assert.equal(received, agent); + assert.equal(title, 'New title'); + return { title: 'New title', source: { kind: 'user' } }; + } + }; + + const response = await adapter.threadNameSet({ threadId: 'session-1', name: 'New title' }); + assert.deepEqual(response.result, {}); + assert.deepEqual(sent, []); + response.afterResponse(); + assert.deepEqual(sent, [{ + method: 'thread/name/updated', + params: { threadId: 'session-1', threadName: 'New title' } + }]); +}); + +test('cold thread/name/set uses official DSH resume only for mutation lifetime', async () => { + const adapter = Object.create(DshxReleaseAdapter.prototype); + const agent = { id: 'session-2', session: { events: [] } }; + let disposed = 0; + adapter.controllers = new Map(); + adapter.send = () => {}; + adapter.driver = { + getLive() { return undefined; }, + async resume(id) { + assert.equal(id, 'session-2'); + return { agent, async dispose() { disposed += 1; } }; + }, + renameTitle(received, title) { + assert.equal(received, agent); + return { title }; + } + }; + + const response = await adapter.threadNameSet({ threadId: 'session-2', name: 'Stored rename' }); + assert.equal(disposed, 1); + assert.equal(typeof response.afterResponse, 'function'); +}); + +test('live thread response always carries latest DSH title', () => { + const adapter = Object.create(DshxReleaseAdapter.prototype); + const agent = { + id: 'session-1', + session: { events: [{ type: 'session/title', data: { title: 'Log title' } }] } + }; + adapter.driver = { + currentTitle(received) { + assert.equal(received, agent); + return { title: 'Folded service title' }; + } + }; + adapter.permissions = { current() { return { preset: 'workspace-write' }; } }; + Object.getPrototypeOf(DshxReleaseAdapter.prototype).threadResponse = () => { + throw new Error('test should stub through product prototype explicitly'); + }; + + const product = Object.getPrototypeOf(DshxReleaseAdapter.prototype); + const original = product.threadResponse; + product.threadResponse = () => ({ thread: { id: 'session-1', name: null } }); + try { + const response = adapter.threadResponse(agent); + assert.equal(response.thread.name, 'Folded service title'); + } finally { + product.threadResponse = original; + } +}); + +test('thread list and read enrich cold metadata with sessionQuery title snapshots', async () => { + const adapter = Object.create(DshxReleaseAdapter.prototype); + adapter.diagnostics = () => {}; + adapter.driver = { + async readTitle(id) { return { title: `Title ${id}` }; } + }; + const product = Object.getPrototypeOf(DshxReleaseAdapter.prototype); + const originalList = product.threadList; + const originalRead = product.threadRead; + product.threadList = async () => ({ + result: { data: [{ id: 'a', name: null }, { id: 'b', name: null }], nextCursor: null, backwardsCursor: null } + }); + product.threadRead = async () => ({ result: { thread: { id: 'a', name: null } } }); + try { + const listed = await adapter.threadList({}); + assert.deepEqual(listed.result.data.map((thread) => thread.name), ['Title a', 'Title b']); + const read = await adapter.threadRead({ threadId: 'a' }); + assert.equal(read.result.thread.name, 'Title a'); + } finally { + product.threadList = originalList; + product.threadRead = originalRead; + } +}); diff --git a/test/dsh-runtime-boot.test.mjs b/test/dsh-runtime-boot.test.mjs new file mode 100644 index 00000000..e76f713e --- /dev/null +++ b/test/dsh-runtime-boot.test.mjs @@ -0,0 +1,120 @@ +import assert from 'node:assert/strict'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; +import { dshxRuntimeEntries, dshxRuntimeProfile, runtimeInternals } from '../src/dsh/runtime-boot.mjs'; + +function temporaryDshHome(run) { + const home = mkdtempSync(join(tmpdir(), 'dshx-profile-test-')); + try { + return run(home); + } finally { + rmSync(home, { recursive: true, force: true }); + } +} + +test('runtime auto-initializes and composes the official DSH headless profile', () => { + temporaryDshHome((home) => { + assert.equal(runtimeInternals.DEFAULT_PROFILE, 'headless'); + const composition = dshxRuntimeProfile({ home }); + assert.equal(composition.name, 'headless'); + + const manifest = JSON.parse(readFileSync(join(home, 'profiles', 'headless', 'package.json'), 'utf8')); + assert.deepEqual(manifest.dsh.profile.bundles, [ + '@deepseek-ai/dsh-base', + '@deepseek-ai/dsh-headless' + ]); + + assert.equal( + readFileSync(composition.rootConfig, 'utf8'), + runtimeInternals.PROFILE_ROOT_CONFIG, + 'DSHX may only rewrite the same empty Loader root role used by official dsh' + ); + assert.match(runtimeInternals.PROFILE_ROOT_CONFIG, /Edit cordis\.patch\.yml, not this file/); + + const entries = dshxRuntimeEntries({ home }); + const byId = new Map(entries.map((entry) => [entry.id, entry])); + + // These are public DSH services DSHX consumes. Their plugin identities remain + // the official profile rows — DSHX never substitutes implementations. + assert.equal(byId.get('llm')?.name, '@deepseek-ai/dsh-llm'); + assert.equal(byId.get('session')?.name, '@deepseek-ai/dsh-session'); + assert.equal(byId.get('agent')?.name, '@deepseek-ai/dsh-agent'); + assert.equal(byId.get('permission')?.name, '@deepseek-ai/dsh-permission-presets'); + assert.equal(byId.get('approval')?.name, '@deepseek-ai/dsh-user-approval'); + assert.equal(byId.get('user-questions')?.name, '@deepseek-ai/dsh-user-questions'); + assert.equal(byId.get('session-persistence-jsonl')?.name, '@deepseek-ai/dsh-session-persistence-jsonl'); + assert.equal(byId.get('tools')?.name, '@deepseek-ai/dsh-tools'); + assert.equal(byId.get('code-runtime')?.name, '@deepseek-ai/dsh-code-runtime-worker-thread'); + + // Only the competing headless presentation surface is locked off. + assert.equal(byId.get('headless-startup')?.disabled, true); + assert.equal(byId.get('headless-runner')?.disabled, true); + }); +}); + +test('profile-local and DSH-home user patch layers remain authoritative and byte-for-byte untouched', () => { + temporaryDshHome((home) => { + // First read creates the official profile using DSH's own template. + const initial = dshxRuntimeProfile({ home }); + const profilePatchPath = initial.profile.patchPath; + const homePatchPath = initial.homePatchPath; + const profilePatch = `# user profile layer - DSHX must never rewrite this byte stream\n- id: tools\n config:\n dshxProfileProbe: profile\n`; + const homePatch = `# machine DSH layer - DSHX must never rewrite this byte stream\n- id: llm\n config:\n dshxHomeProbe: home\n`; + writeFileSync(profilePatchPath, profilePatch); + writeFileSync(homePatchPath, homePatch); + + const entries = dshxRuntimeEntries({ home }); + const byId = new Map(entries.map((entry) => [entry.id, entry])); + assert.equal(byId.get('tools')?.config?.dshxProfileProbe, 'profile'); + assert.equal(byId.get('llm')?.config?.dshxHomeProbe, 'home'); + + assert.equal(readFileSync(profilePatchPath, 'utf8'), profilePatch); + assert.equal(readFileSync(homePatchPath, 'utf8'), homePatch); + assert.equal(readFileSync(initial.rootConfig, 'utf8'), runtimeInternals.PROFILE_ROOT_CONFIG); + }); +}); + +test('recomposition rewrites only the generated empty root, never either user patch layer', () => { + temporaryDshHome((home) => { + const initial = dshxRuntimeProfile({ home }); + const profilePatch = '- id: tools\n config: { probe: keep-profile }\n'; + const homePatch = '- id: llm\n config: { probe: keep-home }\n'; + writeFileSync(initial.profile.patchPath, profilePatch); + writeFileSync(initial.homePatchPath, homePatch); + writeFileSync(initial.rootConfig, 'this is generated state and must be reset\n'); + + const recomposed = dshxRuntimeProfile({ home }); + assert.equal(readFileSync(recomposed.rootConfig, 'utf8'), runtimeInternals.PROFILE_ROOT_CONFIG); + assert.equal(readFileSync(recomposed.profile.patchPath, 'utf8'), profilePatch); + assert.equal(readFileSync(recomposed.homePatchPath, 'utf8'), homePatch); + }); +}); + +test('DSHX surface patches cannot insert a second Agent, Session, tool or provider implementation', () => { + for (const patch of runtimeInternals.DSHX_SURFACE_PATCHES) { + assert.ok('id' in patch); + assert.equal('insert' in patch, false); + assert.equal(patch.disabled, true); + } +}); + +test('presentation lifetime delegates disposal to the official DSH root Fiber', async () => { + let disposals = 0; + const ctx = { + fiber: { + async dispose() { + disposals += 1; + } + } + }; + + const decorated = runtimeInternals.attachPresentationLifetime(ctx); + assert.equal(decorated, ctx); + assert.equal(typeof ctx.dispose, 'function'); + assert.equal(Object.prototype.propertyIsEnumerable.call(ctx, 'dispose'), false); + + await ctx.dispose(); + assert.equal(disposals, 1); +}); diff --git a/test/dsh-session-fork.test.mjs b/test/dsh-session-fork.test.mjs new file mode 100644 index 00000000..098070a4 --- /dev/null +++ b/test/dsh-session-fork.test.mjs @@ -0,0 +1,41 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { codexForkAtSeq } from '../src/dsh/session-fork.mjs'; + +const events = [ + { seq: 1, type: 'turn/start', data: { turn: 1 } }, + { seq: 2, type: 'user/message', data: { id: 'u1' } }, + { seq: 3, type: 'assistant/message', data: { turn: 1, step: 1 } }, + { seq: 4, type: 'turn/end', data: { turn: 1, reason: { kind: 'completed' } } }, + { seq: 5, type: 'session/title', data: { title: 'after turn one' } }, + { seq: 6, type: 'turn/start', data: { turn: 2 } }, + { seq: 7, type: 'user/message', data: { id: 'u2' } }, + { seq: 8, type: 'turn/end', data: { turn: 2, reason: { kind: 'completed' } } } +]; + +test('default fork delegates boundary selection entirely to DSH Host', () => { + assert.equal(codexForkAtSeq(events), undefined); +}); + +test('lastTurnId translates only to the matching durable turn/end anchor', () => { + assert.equal(codexForkAtSeq(events, { lastTurnId: 'dsh-turn-1' }), 4); + assert.equal(codexForkAtSeq(events, { lastTurnId: 'dsh-turn-2' }), 8); +}); + +test('beforeTurnId translates to the prior completed-turn anchor without slicing a seed', () => { + assert.equal(codexForkAtSeq(events, { beforeTurnId: 'dsh-turn-2' }), 4); + assert.throws( + () => codexForkAtSeq(events, { beforeTurnId: 'dsh-turn-1' }), + /cannot represent.*before the first completed turn/i + ); +}); + +test('fork anchor translation rejects ambiguous or nonexistent Codex turn ids', () => { + assert.throws( + () => codexForkAtSeq(events, { lastTurnId: 'dsh-turn-1', beforeTurnId: 'dsh-turn-2' }), + /mutually exclusive/ + ); + assert.throws(() => codexForkAtSeq(events, { lastTurnId: 'turn-1' }), /cannot map Codex turn id/); + assert.throws(() => codexForkAtSeq(events, { lastTurnId: 'dsh-turn-99' }), /no completed/); + assert.throws(() => codexForkAtSeq(events, { beforeTurnId: 'dsh-turn-99' }), /has no dsh-turn-99/); +}); diff --git a/test/dsh-session-projector.test.mjs b/test/dsh-session-projector.test.mjs new file mode 100644 index 00000000..0c55aa67 --- /dev/null +++ b/test/dsh-session-projector.test.mjs @@ -0,0 +1,187 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { DshSessionProjector } from '../src/dsh/session-projector.mjs'; + +const event = (type, data, time = 1_700_000_000_000) => ({ type, data, time }); + +function start(projector, turn = 1) { + return projector.project(event('turn/start', { turn })); +} + +test('projects DSH turn lifecycle without owning runtime state', () => { + const projector = new DshSessionProjector({ threadId: 'session-1' }); + const started = start(projector); + assert.equal(started[0].method, 'turn/started'); + assert.equal(started[0].params.turn.id, 'dsh-turn-1'); + assert.equal(started[0].params.turn.status, 'inProgress'); + + const ended = projector.project(event('turn/end', { turn: 1, reason: { kind: 'completed' } }, 1_700_000_001_000)); + assert.equal(ended[0].method, 'turn/completed'); + assert.equal(ended[0].params.turn.status, 'completed'); +}); + +test('projects visible DSH text chunks into one Codex agent message item', () => { + const projector = new DshSessionProjector({ threadId: 'session-1' }); + start(projector); + + const first = projector.project( + event('assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'hel' } }) + ); + assert.deepEqual(first.map((entry) => entry.method), ['item/started', 'item/agentMessage/delta']); + assert.equal(first[1].params.delta, 'hel'); + + const second = projector.project( + event('assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'lo' } }) + ); + assert.deepEqual(second.map((entry) => entry.method), ['item/agentMessage/delta']); + + const completed = projector.project( + event('assistant/message', { + turn: 1, + step: 1, + message: { content: [{ type: 'text', text: 'hello' }] } + }) + ); + assert.equal(completed[0].method, 'item/completed'); + assert.equal(completed[0].params.item.text, 'hello'); +}); + +test('projects DSH reasoning chunks into a distinct native Codex reasoning item', () => { + const projector = new DshSessionProjector({ threadId: 'session-1' }); + start(projector); + + const first = projector.project( + event('assistant/chunk', { turn: 1, step: 1, chunk: { type: 'reasoning-delta', index: 0, text: 'think' } }) + ); + assert.deepEqual(first.map((entry) => entry.method), ['item/started', 'item/reasoning/textDelta']); + assert.deepEqual(first[0].params.item, { + type: 'reasoning', + id: 'dsh-reasoning-1-1', + summary: [], + content: [] + }); + assert.equal(first[1].params.contentIndex, 0); + assert.equal(first[1].params.delta, 'think'); + + const second = projector.project( + event('assistant/chunk', { turn: 1, step: 1, chunk: { type: 'reasoning-delta', index: 0, text: 'ing' } }) + ); + assert.deepEqual(second.map((entry) => entry.method), ['item/reasoning/textDelta']); + + const completed = projector.project(event('assistant/message', { + turn: 1, + step: 1, + message: { + content: [ + { type: 'reasoning', text: 'thinking' }, + { type: 'text', text: 'answer' } + ] + } + })); + assert.deepEqual(completed.map((entry) => entry.method), [ + 'item/completed', + 'item/started', + 'item/completed' + ]); + assert.deepEqual(completed[0].params.item, { + type: 'reasoning', + id: 'dsh-reasoning-1-1', + summary: [], + content: ['thinking'] + }); + assert.equal(completed[2].params.item.text, 'answer'); +}); + +test('projects unknown DSH tools conservatively and correlates official tool/result message source', () => { + const projector = new DshSessionProjector({ threadId: 'session-1' }); + start(projector); + const started = projector.project( + event('tool/call', { turn: 1, step: 1, callId: 'call-1', name: 'custom_tool', arguments: '{"x":1}' }) + ); + assert.equal(started[0].method, 'item/started'); + assert.equal(started[0].params.item.type, 'dynamicToolCall'); + assert.deepEqual(started[0].params.item.arguments, { x: 1 }); + assert.equal(started[0].params.item.status, 'inProgress'); + + const completed = projector.project( + event('tool/result', { + turn: 1, + step: 1, + message: { + source: { kind: 'tool', callId: 'call-1' }, + content: [{ + type: 'tool-result', + toolCallId: 'call-1', + content: [{ type: 'text', text: 'ok' }], + isError: false + }] + } + }, 1_700_000_000_050) + ); + assert.equal(completed.length, 1); + assert.equal(completed[0].params.item.status, 'completed'); + assert.equal(completed[0].params.item.success, true); +}); + +test('falls back to tool-result block call id when message source is unavailable', () => { + const projector = new DshSessionProjector({ threadId: 'session-1' }); + start(projector); + projector.project(event('tool/call', { + turn: 1, + step: 1, + callId: 'call-2', + name: 'custom_tool', + arguments: '{}' + })); + const completed = projector.project(event('tool/result', { + turn: 1, + step: 1, + message: { + content: [{ + type: 'tool-result', + toolCallId: 'call-2', + content: [{ type: 'text', text: 'failed' }], + isError: true + }] + } + })); + assert.equal(completed[0].params.item.status, 'failed'); + assert.equal(completed[0].params.item.success, false); +}); + +test('projects DSH todo snapshots into Codex plan updates', () => { + const projector = new DshSessionProjector({ threadId: 'session-1' }); + start(projector, 7); + const plan = projector.project( + event('todo/write', { + todos: [ + { content: 'Inspect project', status: 'completed' }, + { content: 'Apply fix', status: 'in_progress' }, + { content: 'Run tests', status: 'pending' } + ] + }) + ); + assert.equal(plan[0].method, 'turn/plan/updated'); + assert.deepEqual(plan[0].params.plan, [ + { step: 'Inspect project', status: 'completed' }, + { step: 'Apply fix', status: 'inProgress' }, + { step: 'Run tests', status: 'pending' } + ]); +}); + +test('caches request route metadata only as disposable presentation state', () => { + const projector = new DshSessionProjector({ threadId: 'session-1' }); + assert.deepEqual( + projector.project( + event('request/header', { + header: { config: { provider: 'deepseek', model: 'example', reasoningEffort: 'high' } }, + reason: 'initial' + }) + ), + [] + ); + assert.equal(projector.latestHeader.config.provider, 'deepseek'); + + projector.project(event('request/context', { provider: 'deepseek', model: 'example', contextWindow: 128000 })); + assert.equal(projector.latestContext.contextWindow, 128000); +}); diff --git a/test/dsh-subagent-authority.test.mjs b/test/dsh-subagent-authority.test.mjs new file mode 100644 index 00000000..a48b1538 --- /dev/null +++ b/test/dsh-subagent-authority.test.mjs @@ -0,0 +1,95 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { DshxReleaseAdapter } from '../src/dsh/release-adapter.mjs'; + +function childController() { + let cancelled = 0; + const agent = { + id: 'child-1', + session: { + header: { + id: 'child-1', + origin: 'subagent', + parentSession: 'root-1', + cwd: '/work' + } + }, + cancel() { cancelled += 1; } + }; + return { controller: { agent }, cancelled: () => cancelled }; +} + +function adapterWithChild() { + const { controller, cancelled } = childController(); + const adapter = Object.create(DshxReleaseAdapter.prototype); + adapter.controllers = new Map([['child-1', controller]]); + adapter.diagnostics = () => {}; + return { adapter, controller, cancelled }; +} + +test('Codex direct turns cannot bypass DSH continuation authority for child agents', async () => { + const { adapter } = adapterWithChild(); + await assert.rejects( + () => adapter.richUserTurn('turn/start', { + threadId: 'child-1', + input: [{ type: 'text', text: 'bypass child authority', text_elements: [] }] + }), + /send\/steer authority remains owned by ctx\.subagents/ + ); + await assert.rejects( + () => adapter.richUserTurn('turn/steer', { + threadId: 'child-1', + expectedTurnId: 'dsh-turn-1', + input: [{ type: 'text', text: 'bypass steer', text_elements: [] }] + }), + /ctx\.subagents/ + ); +}); + +test('Codex shell, compaction, and settings are presentation-blocked on child agents', async () => { + const { adapter } = adapterWithChild(); + assert.throws( + () => adapter.threadShellCommand({ threadId: 'child-1', command: 'pwd' }), + /child runtime control remains owned by ctx\.subagents/ + ); + assert.throws( + () => adapter.directController('child-1', 'direct compaction'), + /child runtime control remains owned by ctx\.subagents/ + ); + await assert.rejects( + () => adapter.threadSettingsUpdatePresentation({ threadId: 'child-1', model: 'anything' }), + /composition remains DSH-owned/ + ); +}); + +test('child interrupt delegates only to official ctx.subagents interrupt authority', async () => { + const { adapter, controller, cancelled } = adapterWithChild(); + const calls = []; + adapter.ctx = { + get(name) { + if (name !== 'subagents') return undefined; + return { + async interrupt(childId, authority) { + calls.push({ childId, authority }); + } + }; + } + }; + + assert.deepEqual(await adapter.interruptSubagent(controller), { result: {} }); + assert.deepEqual(calls, [{ + childId: 'child-1', + authority: { kind: 'user', parentSessionId: 'root-1' } + }]); + assert.equal(cancelled(), 0, 'DSHX must not call child Agent.cancel() directly'); +}); + +test('child interrupt fails closed without durable parent authority', async () => { + const { adapter, controller } = adapterWithChild(); + controller.agent.session.header.parentSession = undefined; + adapter.ctx = { get() { throw new Error('subagent service must not be consulted without parent authority'); } }; + await assert.rejects( + () => adapter.interruptSubagent(controller), + /missing durable parentSession authority/ + ); +}); diff --git a/test/dsh-subagent-presentation.test.mjs b/test/dsh-subagent-presentation.test.mjs new file mode 100644 index 00000000..3555ffa6 --- /dev/null +++ b/test/dsh-subagent-presentation.test.mjs @@ -0,0 +1,84 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { dshThreadFromSnapshot } from '../src/dsh/codex-shapes.mjs'; +import { DshxReleaseAdapter } from '../src/dsh/release-adapter.mjs'; + +function childAgent() { + return { + id: 'child-1', + status: 'running', + session: { + header: { + id: 'child-1', + origin: 'subagent', + parentSession: 'root-1', + delegationDepth: 2, + createdAt: 1_700_000_000_000, + cwd: '/work' + }, + events: [ + { + type: 'subagent/descriptor', + time: 1_700_000_000_010, + data: { version: 2, mode: 'continuable', provider: 'in-process', label: 'Scout' } + } + ], + requestHeader() { return { config: { provider: 'deepseek', model: 'model-a' } }; } + } + }; +} + +test('DSH subagent lineage becomes native Codex thread_spawn metadata without fake fork ancestry', () => { + const agent = childAgent(); + const thread = dshThreadFromSnapshot({ + meta: agent.session.header, + events: agent.session.events, + model: agent.session.requestHeader().config, + loaded: true + }); + + assert.equal(thread.forkedFromId, null); + assert.equal(thread.parentThreadId, 'root-1'); + assert.equal(thread.agentNickname, 'Scout'); + assert.equal(thread.agentRole, null); + assert.equal(thread.canAcceptDirectInput, false); + assert.deepEqual(thread.source, { + subagent: { + thread_spawn: { + parent_thread_id: 'root-1', + depth: 2, + agent_path: null, + agent_nickname: 'Scout', + agent_role: null + } + } + }); +}); + +test('release loaded-thread wire response lists official DSH live ids while rich child metadata stays on thread projections', () => { + const root = { + id: 'root-1', + status: 'idle', + session: { + header: { id: 'root-1', createdAt: 1_700_000_000_000, cwd: '/work' }, + events: [], + requestHeader() { return { config: { provider: 'deepseek', model: 'model-a' } }; } + } + }; + const child = childAgent(); + const adapter = Object.create(DshxReleaseAdapter.prototype); + adapter.version = 'test'; + adapter.driver = { + listLive() { return [root, child]; }, + currentTitle(agent) { return agent.id === 'child-1' ? { title: 'Scout' } : undefined; } + }; + + const response = adapter.loadedThreadList(); + assert.deepEqual(response.result.data, ['root-1', 'child-1']); + + const projectedChild = adapter.liveAgentThread(child); + assert.equal(projectedChild.name, 'Scout'); + assert.equal(projectedChild.status.type, 'active'); + assert.equal(projectedChild.canAcceptDirectInput, false); + assert.equal(projectedChild.source.subagent.thread_spawn.parent_thread_id, 'root-1'); +}); \ No newline at end of file diff --git a/test/dsh-thread-controller.test.mjs b/test/dsh-thread-controller.test.mjs new file mode 100644 index 00000000..59921110 --- /dev/null +++ b/test/dsh-thread-controller.test.mjs @@ -0,0 +1,169 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { DshThreadController } from '../src/dsh/thread-controller.mjs'; + +function fixture() { + const emitted = []; + const calls = []; + let sessionListener; + let disposed = false; + const session = { id: 'session-1', events: [], header: { cwd: '/workspace' } }; + const agent = { + id: 'session-1', + session, + ctx: { + get() { return undefined; }, + on(name, listener) { + assert.equal(name, 'session/event'); + sessionListener = listener; + return () => { sessionListener = undefined; }; + } + } + }; + const handle = { + agent, + async dispose() { disposed = true; } + }; + const driver = { + followup(_agent, text) { + calls.push(['followup', text]); + queueMicrotask(() => { + const event = { + type: 'turn/start', + seq: 1, + time: 1_700_000_000_000, + data: { turn: 4 } + }; + session.events.push(event); + sessionListener?.(session, event); + }); + }, + steer(_agent, text) { calls.push(['steer', text]); }, + interrupt(_agent, options) { calls.push(['interrupt', options]); }, + whenIdle() { calls.push(['whenIdle']); return Promise.resolve(); } + }; + const controller = new DshThreadController({ handle, driver, emit: (event) => emitted.push(event) }); + return { + controller, + session, + emitted, + calls, + emitSession(event, target = session) { + if (target === session) session.events.push(event); + sessionListener?.(target, event); + }, + get disposed() { return disposed; }, + get hasListener() { return Boolean(sessionListener); } + }; +} + +test('startTurn waits for DSH turn/start and releases notification after RPC response point', async () => { + const fx = fixture(); + const started = await fx.controller.startTurn('hello'); + assert.deepEqual(fx.calls[0], ['followup', 'hello']); + assert.equal(started.turn.id, 'dsh-turn-4'); + assert.equal(started.turn.status, 'inProgress'); + assert.deepEqual(fx.emitted, [], 'turn/started must remain buffered until transport sends response'); + + started.release(); + assert.equal(fx.emitted.length, 1); + assert.equal(fx.emitted[0].method, 'turn/started'); + assert.equal(fx.emitted[0].params.turn.id, started.turn.id); + + started.release(); + assert.equal(fx.emitted.length, 1, 'release must be idempotent'); +}); + +test('all notifications produced before the turn/start RPC response remain ordered behind turn/started', async () => { + const fx = fixture(); + const started = await fx.controller.startTurn('hello'); + fx.emitSession({ + type: 'assistant/chunk', + seq: 2, + time: 1_700_000_000_010, + data: { turn: 4, step: 1, chunk: { type: 'text-delta', index: 0, text: 'fast' } } + }); + assert.deepEqual(fx.emitted, []); + + started.release(); + assert.deepEqual(fx.emitted.map((event) => event.method), [ + 'turn/started', + 'item/started', + 'item/agentMessage/delta' + ]); +}); + +test('controller ignores global session/event traffic from other DSH sessions', async () => { + const fx = fixture(); + const started = await fx.controller.startTurn('hello'); + started.release(); + const otherSession = { id: 'session-2', events: [] }; + fx.emitSession({ + type: 'assistant/chunk', + seq: 1, + time: 1_700_000_000_010, + data: { turn: 99, step: 1, chunk: { type: 'text-delta', index: 0, text: 'wrong thread' } } + }, otherSession); + assert.deepEqual(fx.emitted.map((event) => event.method), ['turn/started']); +}); + +test('later DSH session events stream directly through the projector', async () => { + const fx = fixture(); + const started = await fx.controller.startTurn('hello'); + started.release(); + + fx.emitSession({ + type: 'assistant/chunk', + seq: 2, + time: 1_700_000_000_010, + data: { turn: 4, step: 1, chunk: { type: 'text-delta', index: 0, text: 'hi' } } + }); + fx.emitSession({ + type: 'assistant/message', + seq: 3, + time: 1_700_000_000_020, + data: { + turn: 4, + step: 1, + message: { + role: 'assistant', + source: { kind: 'model', provider: 'deepseek', model: 'test' }, + content: [{ type: 'text', text: 'hi' }] + } + } + }); + fx.emitSession({ + type: 'turn/end', + seq: 4, + time: 1_700_000_000_030, + data: { turn: 4, reason: { kind: 'completed' } } + }); + + assert.deepEqual(fx.emitted.map((event) => event.method), [ + 'turn/started', + 'item/started', + 'item/agentMessage/delta', + 'item/completed', + 'turn/completed' + ]); +}); + +test('steering, interrupt and idle are pure delegation', async () => { + const fx = fixture(); + fx.controller.steer('new constraint'); + fx.controller.interrupt({ keepInbox: true }); + await fx.controller.whenIdle(); + assert.deepEqual(fx.calls, [ + ['steer', 'new constraint'], + ['interrupt', { keepInbox: true }], + ['whenIdle'] + ]); +}); + +test('close detaches the scoped session listener and disposes the DSH handle', async () => { + const fx = fixture(); + assert.equal(fx.hasListener, true); + await fx.controller.close(); + assert.equal(fx.hasListener, false); + assert.equal(fx.disposed, true); +}); diff --git a/test/dsh-token-usage.test.mjs b/test/dsh-token-usage.test.mjs new file mode 100644 index 00000000..04c205e4 --- /dev/null +++ b/test/dsh-token-usage.test.mjs @@ -0,0 +1,139 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { codexThreadTokenUsage, tokenUsageNotification } from '../src/dsh/token-usage.mjs'; + +function fixture() { + const session = { + events: [ + { + type: 'request/context', + seq: 1, + data: { provider: 'deepseek', model: 'model-a', contextWindow: 128000 } + }, + { + type: 'assistant/chunk', + seq: 2, + data: { + turn: 1, + step: 1, + chunk: { + type: 'usage', + usage: { + inputTokens: 10, + cacheReadTokens: 3, + outputTokens: 5, + reasoningTokens: 4 + } + } + } + }, + { + type: 'assistant/message', + seq: 3, + data: { + turn: 1, + step: 1, + message: { content: [] }, + usage: { + inputTokens: 12, + cacheReadTokens: 4, + cacheWriteTokens: 2, + outputTokens: 6, + reasoningTokens: 5 + } + } + }, + { + type: 'assistant/message', + seq: 4, + data: { + turn: 1, + step: 2, + message: { content: [] }, + usage: { + inputTokens: 20, + outputTokens: 7, + reasoningTokens: 3 + } + } + } + ] + }; + const ctx = { + get(name) { + if (name !== 'sessionProjections') return undefined; + return { + snapshot(received) { + assert.equal(received, session); + return { + asOfSeq: 4, + values: { + tokenUsage: { + uncachedInputTokens: 32, + outputTokens: 13, + cacheReadTokens: 4, + cacheWriteTokens: 2 + }, + contextPressure: { + pressureTokens: 20, + projectedTokens: 24, + contextWindow: 128000 + } + } + }; + } + }; + } + }; + return { ctx, session }; +} + +test('Codex total uses DSH token-meter buckets without double-counting reasoning', () => { + const { ctx, session } = fixture(); + const usage = codexThreadTokenUsage({ ctx, session }); + assert.deepEqual(usage.total, { + totalTokens: 51, + inputTokens: 32, + cachedInputTokens: 4, + cacheWriteInputTokens: 2, + outputTokens: 13, + reasoningOutputTokens: 8 + }); + assert.equal(usage.total.totalTokens, 32 + 4 + 2 + 13); + assert.equal(usage.modelContextWindow, 128000); +}); + +test('Codex last usage is the latest DSH call sample and final usage replaces same-step chunk', () => { + const { ctx, session } = fixture(); + const usage = codexThreadTokenUsage({ ctx, session }); + assert.deepEqual(usage.last, { + totalTokens: 27, + inputTokens: 20, + cachedInputTokens: 0, + cacheWriteInputTokens: 0, + outputTokens: 7, + reasoningOutputTokens: 3 + }); + assert.equal(usage.total.reasoningOutputTokens, 5 + 3, 'chunk reasoning must not be double counted'); +}); + +test('notification matches pinned Codex thread/tokenUsage/updated envelope', () => { + const { ctx, session } = fixture(); + const notification = tokenUsageNotification({ + ctx, + session, + threadId: 'session-1', + turnId: 'dsh-turn-1' + }); + assert.equal(notification.method, 'thread/tokenUsage/updated'); + assert.equal(notification.params.threadId, 'session-1'); + assert.equal(notification.params.turnId, 'dsh-turn-1'); + assert.equal(notification.params.tokenUsage.modelContextWindow, 128000); +}); + +test('missing optional DSH projection omits footer update instead of inventing usage', () => { + const session = { events: [] }; + const ctx = { get() { return undefined; } }; + assert.equal(codexThreadTokenUsage({ ctx, session }), null); + assert.equal(tokenUsageNotification({ ctx, session, threadId: 's', turnId: 't' }), null); +}); diff --git a/test/dsh-tool-presentation.test.mjs b/test/dsh-tool-presentation.test.mjs new file mode 100644 index 00000000..c9cb817e --- /dev/null +++ b/test/dsh-tool-presentation.test.mjs @@ -0,0 +1,187 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import test from 'node:test'; +import { DshToolPresentationResolver } from '../src/dsh/tool-presentation.mjs'; + +function fixture(definitions, { cwd = process.cwd() } = {}) { + const agent = { id: 'session-1' }; + const ctx = { + get(name) { + if (name !== 'tools') return undefined; + return { + get(toolName, scope) { + assert.equal(scope, agent, 'resolver must ask for the Agent-scoped visible tool'); + return definitions[toolName]; + } + }; + } + }; + return { + agent, + resolver: new DshToolPresentationResolver({ + ctx, + agent, + threadId: 'session-1', + workspaceCwd: cwd + }) + }; +} + +function toolResult(callId, { content = [{ type: 'text', text: 'ok' }], isError = false, meta } = {}) { + return { + message: { + source: { kind: 'tool', callId }, + content: [{ type: 'tool-result', toolCallId: callId, content, isError }] + }, + ...(meta === undefined ? {} : { meta }) + }; +} + +test('terminal presenter maps to Codex commandExecution and result status/output', () => { + const cwd = path.resolve('workspace'); + const { resolver } = fixture({ + shell: { + presentCall(args) { + assert.deepEqual(args, { cmd: 'echo hi' }); + return { card: 'terminal', title: 'echo hi', cwd: '.' }; + }, + presentResult(_args, result) { + assert.equal(result.isError, false); + return { card: 'terminal', output: 'hi\n', exitCode: 0 }; + } + } + }, { cwd }); + + const started = resolver.start({ + turnId: 'dsh-turn-1', + callId: 'call-shell', + name: 'shell', + rawArguments: '{"cmd":"echo hi"}', + startedAtMs: 100 + }); + assert.equal(started.item.type, 'commandExecution'); + assert.equal(started.item.command, 'echo hi'); + assert.equal(started.item.cwd, cwd); + assert.equal(started.item.status, 'inProgress'); + + const completed = resolver.complete({ + callId: 'call-shell', + resultData: toolResult('call-shell'), + completedAtMs: 125 + }); + assert.equal(completed.item.type, 'commandExecution'); + assert.equal(completed.item.status, 'completed'); + assert.equal(completed.item.aggregatedOutput, 'hi\n'); + assert.equal(completed.item.exitCode, 0); + assert.equal(completed.item.durationMs, 25); + assert.deepEqual(resolver.correlation('call-shell'), { + threadId: 'session-1', + turnId: 'dsh-turn-1', + itemId: 'dsh-tool-call-shell', + semantic: 'command', + kind: 'command', + command: 'echo hi', + cwd + }); +}); + +test('unambiguous DSH diff update maps to Codex fileChange', () => { + const { resolver } = fixture({ + editor: { + presentCall() { + return { + card: 'diff', + title: 'Edit a.txt', + diffs: [{ path: 'a.txt', oldText: 'old', newText: 'new' }] + }; + }, + presentResult() { + return { + card: 'diff', + title: 'Edit a.txt', + diffs: [{ path: 'a.txt', oldText: 'old', newText: 'new' }] + }; + } + } + }); + + const started = resolver.start({ + turnId: 'dsh-turn-2', + callId: 'call-edit', + name: 'editor', + rawArguments: '{}', + startedAtMs: 1 + }); + assert.equal(started.item.type, 'fileChange'); + assert.equal(started.item.status, 'inProgress'); + assert.deepEqual(started.item.changes[0].kind, { type: 'update', move_path: null }); + assert.match(started.item.changes[0].diff, /--- a\/a\.txt/); + + const completed = resolver.complete({ + callId: 'call-edit', + resultData: toolResult('call-edit'), + completedAtMs: 2 + }); + assert.equal(completed.item.type, 'fileChange'); + assert.equal(completed.item.status, 'completed'); + assert.equal(resolver.correlation('call-edit').kind, 'fileChange'); +}); + +test('ambiguous DSH oldText:null diff stays generic instead of fabricating add/update', () => { + const { resolver } = fixture({ + writer: { + presentCall() { + return { + card: 'diff', + title: 'Write a.txt', + diffs: [{ path: 'a.txt', oldText: null, newText: 'new' }] + }; + } + } + }); + + const started = resolver.start({ + turnId: 'dsh-turn-3', + callId: 'call-write', + name: 'writer', + rawArguments: '{"file":"a.txt"}' + }); + assert.equal(started.item.type, 'dynamicToolCall'); + assert.equal(started.state.semantic, 'generic'); + assert.equal(resolver.correlation('call-write').kind, undefined); +}); + +test('unknown or presenter-less tools remain generic and preserve text result content', () => { + const { resolver } = fixture({}); + const started = resolver.start({ + turnId: 'dsh-turn-4', + callId: 'call-custom', + name: 'custom', + rawArguments: '{"x":1}', + startedAtMs: 10 + }); + assert.equal(started.item.type, 'dynamicToolCall'); + assert.deepEqual(started.item.arguments, { x: 1 }); + + const completed = resolver.complete({ + callId: 'call-custom', + resultData: toolResult('call-custom', { content: [{ type: 'text', text: 'done' }] }), + completedAtMs: 15 + }); + assert.equal(completed.item.status, 'completed'); + assert.equal(completed.item.success, true); + assert.deepEqual(completed.item.contentItems, [{ type: 'inputText', text: 'done' }]); +}); + +test('terminal signal or non-zero exit code fails the Codex command cell', () => { + const { resolver } = fixture({ + shell: { + presentCall: () => ({ card: 'terminal', title: 'bad' }), + presentResult: () => ({ card: 'terminal', output: 'oops', exitCode: 2 }) + } + }); + resolver.start({ turnId: 'dsh-turn-5', callId: 'call-bad', name: 'shell', rawArguments: '{}' }); + const completed = resolver.complete({ callId: 'call-bad', resultData: toolResult('call-bad') }); + assert.equal(completed.item.status, 'failed'); + assert.equal(completed.item.exitCode, 2); +}); diff --git a/test/dsh-user-question-bridge.test.mjs b/test/dsh-user-question-bridge.test.mjs new file mode 100644 index 00000000..88e25a41 --- /dev/null +++ b/test/dsh-user-question-bridge.test.mjs @@ -0,0 +1,132 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { DshUserQuestionBridge } from '../src/dsh/user-question-bridge.mjs'; + +function fixture({ response, locate } = {}) { + let provider; + let disposed = false; + const requests = []; + const completions = []; + const diagnostics = []; + const userQuestions = { + registerProvider(next) { + provider = next; + return () => { disposed = true; provider = undefined; }; + } + }; + const broker = { + async request(method, params, options) { + requests.push({ method, params, options }); + if (response instanceof Error) throw response; + return response ?? { answers: {} }; + } + }; + const bridge = new DshUserQuestionBridge({ + ctx: { get(name) { return name === 'userQuestions' ? userQuestions : undefined; } }, + broker, + locate: locate ?? (() => ({ threadId: 'session-1', turnId: 'dsh-turn-1', itemId: 'ask-ui-1' })), + complete: (location, outcome) => completions.push({ location, outcome }), + diagnostics: (message) => diagnostics.push(message) + }); + return { + bridge, + requests, + completions, + diagnostics, + ask(request) { return provider.ask(request); }, + get disposed() { return disposed; } + }; +} + +test('single-select question maps to Codex request and structured DSH answer', async () => { + const fx = fixture({ + response: { answers: { q1: { answers: ['PostgreSQL'] } } } + }); + const result = await fx.ask({ + questions: [{ + id: 'q1', + header: 'Database', + question: 'Which database?', + options: [ + { label: 'PostgreSQL', description: 'Production database' }, + { label: 'SQLite' } + ] + }] + }); + + assert.equal(fx.requests[0].method, 'item/tool/requestUserInput'); + assert.deepEqual(fx.requests[0].params.questions[0], { + id: 'q1', + header: 'Database', + question: 'Which database?', + isOther: true, + isSecret: false, + options: [ + { label: 'PostgreSQL', description: 'Production database' }, + { label: 'SQLite', description: '' } + ] + }); + assert.deepEqual(result, { answers: [{ id: 'q1', selected: ['PostgreSQL'] }] }); + assert.equal(fx.completions[0].outcome.status, 'completed'); +}); + +test('free-text answer is returned as DSH custom text', async () => { + const fx = fixture({ response: { answers: { q1: { answers: ['Use the internal cluster'] } } } }); + const result = await fx.ask({ + questions: [{ id: 'q1', question: 'Where should this deploy?' }] + }); + assert.equal(fx.requests[0].params.questions[0].options, null); + assert.deepEqual(result, { + answers: [{ id: 'q1', selected: [], custom: 'Use the internal cluster' }] + }); +}); + +test('detail is preserved in display text without changing answer encoding', async () => { + const fx = fixture({ response: { answers: { review: { answers: ['Approve'] } } } }); + await fx.ask({ + questions: [{ + id: 'review', + question: 'Approve this plan?', + detail: '1. Inspect\n2. Edit\n3. Test', + options: [{ label: 'Approve' }, { label: 'Reject' }], + intent: { kind: 'plan-review', approve: 'Approve' } + }] + }); + assert.match(fx.requests[0].params.questions[0].question, /1\. Inspect/); +}); + +test('multi-select is explicit unsupported instead of silently degrading to single-select', async () => { + const fx = fixture(); + await assert.rejects( + () => fx.ask({ + questions: [{ + id: 'many', + question: 'Choose several', + multiSelect: true, + options: [{ label: 'A' }, { label: 'B' }] + }] + }), + /does not yet represent DSH multi-select questions faithfully/ + ); + assert.equal(fx.requests.length, 0); +}); + +test('missing presentation location is an explicit compatibility failure', async () => { + const fx = fixture({ locate: () => null }); + await assert.rejects( + () => fx.ask({ questions: [{ id: 'q', question: 'Question?' }] }), + /could not open a Codex presentation item/ + ); +}); + +test('request failure closes the synthetic interaction as failed', async () => { + const fx = fixture({ response: new Error('transport lost') }); + await assert.rejects(() => fx.ask({ questions: [{ id: 'q', question: 'Question?' }] }), /transport lost/); + assert.equal(fx.completions[0].outcome.status, 'failed'); +}); + +test('bridge disposal unregisters only the UI provider', () => { + const fx = fixture(); + fx.bridge.dispose(); + assert.equal(fx.disposed, true); +}); diff --git a/test/dsh-user-shell.test.mjs b/test/dsh-user-shell.test.mjs new file mode 100644 index 00000000..b47e2280 --- /dev/null +++ b/test/dsh-user-shell.test.mjs @@ -0,0 +1,139 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { DshToolPresentationResolver } from '../src/dsh/tool-presentation.mjs'; +import { DshUserShellBridge } from '../src/dsh/user-shell.mjs'; + +function fixture({ activeTurn = null, execute } = {}) { + const sent = []; + const calls = []; + const session = { header: { cwd: process.cwd() }, events: [] }; + const definition = { + presentCall(args) { + return { card: 'terminal', title: args.command, cwd: args.workdir }; + }, + presentResult(_args, result) { + const text = result.content?.find?.((block) => block?.type === 'text')?.text ?? ''; + return { card: 'terminal', output: text, exitCode: result.isError ? 1 : 0 }; + } + }; + const tools = { + get(name) { + if (name === 'bash' || name === 'pwsh') return definition; + return undefined; + }, + async execute(input) { + calls.push(input); + if (execute) return execute(input); + return { isError: false, value: {}, content: [{ type: 'text', text: 'safe output\n' }] }; + } + }; + const agent = { + id: 'session-1', + session, + ctx: { get(name) { return name === 'tools' ? tools : undefined; } } + }; + const controller = { + threadId: 'session-1', + agent, + currentLocation() { + return activeTurn ? { threadId: 'session-1', turnId: activeTurn } : undefined; + } + }; + controller.toolPresentation = new DshToolPresentationResolver({ + ctx: agent.ctx, + agent, + threadId: controller.threadId, + workspaceCwd: session.header.cwd + }); + const bridge = new DshUserShellBridge({ send: (message) => sent.push(message) }); + return { bridge, controller, sent, calls, session }; +} + +async function settle() { + await new Promise((resolve) => setImmediate(resolve)); + await new Promise((resolve) => setImmediate(resolve)); +} + +test('idle bang shell uses a non-durable presentation turn and DSH ToolRuntime', async () => { + const fx = fixture(); + const response = fx.bridge.start(fx.controller, 'echo hello'); + assert.deepEqual(response.result, {}); + assert.deepEqual(fx.sent, [], 'events must wait until shellCommand RPC response'); + response.afterResponse(); + await settle(); + + assert.equal(fx.calls.length, 1); + const input = fx.calls[0]; + assert.equal(input.agent, fx.controller.agent); + assert.equal(input.arguments.command, 'echo hello'); + assert.equal(input.arguments.workdir, process.cwd()); + assert.equal(input.arguments.run_in_background, false); + assert.equal('sandbox_permissions' in input.arguments, false); + assert.equal(input.signal instanceof AbortSignal, true); + assert.ok(input.name === 'bash' || input.name === 'pwsh'); + + assert.deepEqual(fx.sent.map((message) => message.method), [ + 'turn/started', + 'item/started', + 'item/completed', + 'turn/completed' + ]); + assert.equal(fx.sent[1].params.item.type, 'commandExecution'); + assert.equal(fx.sent[1].params.item.source, 'userShell'); + assert.equal(fx.sent[2].params.item.source, 'userShell'); + assert.equal(fx.sent[2].params.item.aggregatedOutput, 'safe output\n'); + assert.deepEqual(fx.session.events, [], 'user shell stays out of durable DSH conversation history'); +}); + +test('bang shell during an active DSH turn attaches only its command item to that live turn', async () => { + const fx = fixture({ activeTurn: 'dsh-turn-4' }); + const response = fx.bridge.start(fx.controller, 'pwd'); + response.afterResponse(); + await settle(); + assert.deepEqual(fx.sent.map((message) => message.method), ['item/started', 'item/completed']); + assert.equal(fx.sent[0].params.turnId, 'dsh-turn-4'); + assert.equal(fx.sent[1].params.turnId, 'dsh-turn-4'); +}); + +test('Ctrl+C aborts the exact official DSH tool signal instead of killing a host process directly', async () => { + let observedAbort = false; + const fx = fixture({ + execute: ({ signal }) => new Promise((resolve) => { + signal.addEventListener('abort', () => { + observedAbort = true; + resolve({ + isError: true, + error: { message: 'aborted' }, + content: [{ type: 'text', text: 'aborted' }] + }); + }, { once: true }); + }) + }); + const response = fx.bridge.start(fx.controller, 'sleep 30'); + const state = fx.bridge.active.get('session-1'); + assert.ok(state); + response.afterResponse(); + await new Promise((resolve) => setImmediate(resolve)); + assert.equal(fx.bridge.interrupt('session-1', state.turnId), true); + await settle(); + assert.equal(observedAbort, true); + assert.equal(fx.bridge.active.has('session-1'), false); +}); + +test('thread close aborts active DSH shell execution', async () => { + let observedAbort = false; + const fx = fixture({ + execute: ({ signal }) => new Promise((resolve) => { + signal.addEventListener('abort', () => { + observedAbort = true; + resolve({ isError: true, error: { message: 'closed' }, content: [] }); + }, { once: true }); + }) + }); + const response = fx.bridge.start(fx.controller, 'long command'); + response.afterResponse(); + await new Promise((resolve) => setImmediate(resolve)); + assert.equal(fx.bridge.abortThread('session-1'), true); + await settle(); + assert.equal(observedAbort, true); +}); diff --git a/test/dsh-workspace-command.test.mjs b/test/dsh-workspace-command.test.mjs new file mode 100644 index 00000000..a3872ac8 --- /dev/null +++ b/test/dsh-workspace-command.test.mjs @@ -0,0 +1,116 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { DshWorkspaceCommandBridge, workspaceCommandInternals } from '../src/dsh/workspace-command.mjs'; + +function agent(id, cwd, calls) { + const tools = { + get(name) { + return name === 'bash' ? { name: 'bash' } : undefined; + }, + async execute(input) { + calls.push(input); + return { + isError: false, + value: { + kind: 'foreground', + exitCode: 7, + signal: null, + timedOut: false, + aborted: false, + timeoutMs: 1234, + stdout: { text: 'héllo\n', truncated: false }, + stderr: { text: 'warn\n', truncated: false } + }, + content: [{ type: 'text', text: 'rendered output' }] + }; + } + }; + return { + id, + session: { header: { id, cwd } }, + ctx: { get(name) { return name === 'tools' ? tools : undefined; } } + }; +} + +test('workspace command maps buffered Codex argv execution through official DSH tools', async () => { + const calls = []; + const a = agent('a', '/workspace/a', calls); + const b = agent('b', '/workspace/b', calls); + const bridge = new DshWorkspaceCommandBridge({ + driver: { listRootAgents() { return [a, b]; } } + }); + + const result = await bridge.execute({ + command: ['git', 'show', "a'b"], + cwd: '/workspace/b', + env: { NO_COLOR: '1', REMOVE_ME: null }, + timeoutMs: 1234, + outputBytesCap: 5 + }); + + assert.deepEqual(result, { exitCode: 7, stdout: 'héll', stderr: 'warn\n' }); + assert.equal(calls.length, 1); + assert.equal(calls[0].agent, b); + assert.equal(calls[0].name, 'bash'); + assert.equal(calls[0].arguments.workdir, '/workspace/b'); + assert.equal(calls[0].arguments.timeoutMs, 1234); + assert.match(calls[0].arguments.command, /export NO_COLOR='1'/); + assert.match(calls[0].arguments.command, /unset REMOVE_ME/); + assert.match(calls[0].arguments.command, /'a'"'"'b'/); +}); + +test('workspace command quoting is dialect-safe for bash and PowerShell', () => { + assert.equal(workspaceCommandInternals.bashQuote("a'b"), `'a'"'"'b'`); + assert.equal(workspaceCommandInternals.pwshQuote("a'b"), `'a''b'`); + assert.match( + workspaceCommandInternals.commandText(['git', 'status'], { FLAG: "a'b" }, 'pwsh'), + /\$env:FLAG = 'a''b'; & 'git' 'status'/ + ); +}); + +test('workspace command preserves complete output when disableOutputCap is true', () => { + const result = workspaceCommandInternals.bufferedResult({ + isError: false, + value: { + kind: 'foreground', + exitCode: 0, + stdout: { text: '0123456789' }, + stderr: { text: 'abcdefghij' } + } + }, { disableOutputCap: true, outputBytesCap: null }); + assert.deepEqual(result, { exitCode: 0, stdout: '0123456789', stderr: 'abcdefghij' }); +}); + +test('workspace command rejects Codex capabilities DSHX cannot faithfully own', async () => { + const calls = []; + const bridge = new DshWorkspaceCommandBridge({ + driver: { listRootAgents() { return [agent('a', process.cwd(), calls)]; } } + }); + await assert.rejects( + () => bridge.execute({ command: ['git'], tty: true }), + /buffered non-TTY/ + ); + await assert.rejects( + () => bridge.execute({ command: ['git'], sandboxPolicy: { type: 'dangerFullAccess' } }), + /active DSH session permission policy/ + ); + await assert.rejects( + () => bridge.execute({ command: ['git'], disableTimeout: true }), + /does not override the DSH shell executor timeout policy/ + ); +}); + +test('workspace command fails closed when multiple active roots are ambiguous', async () => { + const calls = []; + const bridge = new DshWorkspaceCommandBridge({ + driver: { + listRootAgents() { + return [agent('a', '/one', calls), agent('b', '/two', calls)]; + } + } + }); + await assert.rejects( + () => bridge.execute({ command: ['git', 'status'] }), + /cannot choose among multiple active DSH root Agents/ + ); +}); diff --git a/test/dshx-rc5-presentation-patch.test.mjs b/test/dshx-rc5-presentation-patch.test.mjs new file mode 100644 index 00000000..39f40378 --- /dev/null +++ b/test/dshx-rc5-presentation-patch.test.mjs @@ -0,0 +1,22 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import test from 'node:test'; + +const patch = fs.readFileSync('upstream/patches/codex/0012-dshx-rc5-presentation-fixes.patch', 'utf8'); + +test('RC presentation patch resolves opaque model ids through model catalog display names', () => { + for (const needle of [ + 'display_name_for', + 'self.model_display_name().to_string()', + 'self.session_header.set_model(&model_display_name)', + 'if !crate::dshx_backend() && requested_model != session.model.as_str()' + ]) { + assert.match(patch, new RegExp(needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + } +}); + +test('RC presentation patch uses the tag-derived DSHX_VERSION in DSHX surfaces', () => { + const occurrences = patch.match(/option_env!\("DSHX_VERSION"\)/g) ?? []; + assert.ok(occurrences.length >= 2, 'welcome header and status/footer must both use DSHX_VERSION'); + assert.match(patch, /unwrap_or\(CODEX_CLI_VERSION\)/); +}); \ No newline at end of file diff --git a/test/dshx-upstream-patch-policy.test.mjs b/test/dshx-upstream-patch-policy.test.mjs new file mode 100644 index 00000000..2abe9bb8 --- /dev/null +++ b/test/dshx-upstream-patch-policy.test.mjs @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import test from 'node:test'; + +const slashPatch = fs.readFileSync('upstream/patches/codex/0009-dshx-slash-capabilities.patch', 'utf8'); + +function hiddenCapabilityBlock() { + const start = slashPatch.indexOf('+ if crate::dshx_backend()'); + const end = slashPatch.indexOf('+ return false;', start); + assert.notEqual(start, -1, 'DSHX slash hidden-capability block must exist'); + assert.notEqual(end, -1, 'DSHX slash hidden-capability block must terminate'); + return slashPatch.slice(start, end); +} + +test('DSH-backed daily commands remain visible in the Codex thin fork', () => { + const hidden = hiddenCapabilityBlock(); + for (const command of ['Diff', 'Fork', 'Compact', 'Agents']) { + assert.equal( + hidden.includes(`SlashCommand::${command}`), + false, + `/${command.toLowerCase()} is DSH-backed and must not be hidden` + ); + } +}); + +test('Codex-only runtime owners stay hidden from DSHX', () => { + const hidden = hiddenCapabilityBlock(); + for (const command of ['AutoReview', 'Mcp', 'Plugins', 'Logout', 'MultiAgents']) { + assert.equal( + hidden.includes(`SlashCommand::${command}`), + true, + `/${command.toLowerCase()} must stay hidden until an equivalent DSH public seam is faithfully projected` + ); + } +}); diff --git a/test/protocol.test.mjs b/test/protocol.test.mjs new file mode 100644 index 00000000..af88e639 --- /dev/null +++ b/test/protocol.test.mjs @@ -0,0 +1,88 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import test from 'node:test'; +import { ProtocolStub, normalizeDispatchResult } from '../src/protocol.mjs'; + +function request(stub, id, method, params = {}) { + return normalizeDispatchResult(stub, { id, method, params }); +} + +const workspace = path.resolve('tmp', 'workspace'); +const dshxHome = path.resolve('tmp', 'dshx-home'); + +test('initialize matches Codex remote handshake fields', () => { + const stub = new ProtocolStub({ cwd: workspace, home: dshxHome }); + const { response } = request(stub, 'initialize', 'initialize', { + clientInfo: { name: 'codex-tui', version: 'test' } + }); + assert.equal(response.id, 'initialize'); + assert.equal(response.result.codexHome, dshxHome); + assert.match(response.result.userAgent, /^dsh-codex-app-server\//); + assert.equal(typeof response.result.platformFamily, 'string'); + assert.equal(typeof response.result.platformOs, 'string'); +}); + +test('bootstrap methods return auth-free model catalog', () => { + const stub = new ProtocolStub({ cwd: workspace }); + assert.deepEqual(request(stub, 1, 'account/read').response.result, { + account: null, + requiresOpenaiAuth: false + }); + const models = request(stub, 2, 'model/list').response.result; + assert.equal(models.data.length, 1); + assert.equal(models.data[0].model, 'dshx-stub'); + assert.equal(models.data[0].isDefault, true); + assert.deepEqual(request(stub, 3, 'configRequirements/read').response.result, { + requirements: null + }); +}); + +test('thread start emits thread/started and uses DSH-like provider identity', () => { + const stub = new ProtocolStub({ cwd: workspace }); + const { response, events } = request(stub, 4, 'thread/start', { cwd: workspace }); + assert.equal(response.result.modelProvider, 'dsh'); + assert.equal(response.result.thread.modelProvider, 'dsh'); + assert.equal(response.result.thread.status.type, 'idle'); + assert.equal(response.result.cwd, workspace); + assert.equal(response.result.thread.cwd, workspace); + assert.equal(events[0].method, 'thread/started'); + assert.equal(events[0].params.thread.id, response.result.thread.id); +}); + +test('turn start emits the minimal Codex streaming lifecycle', () => { + const stub = new ProtocolStub({ cwd: workspace }); + const thread = request(stub, 4, 'thread/start', { cwd: workspace }).response.result.thread; + const { response, events } = request(stub, 5, 'turn/start', { + threadId: thread.id, + input: [{ type: 'text', text: 'hello dshx', text_elements: [] }] + }); + assert.equal(response.result.turn.status, 'inProgress'); + assert.equal(events[0].method, 'turn/started'); + assert.equal(events[1].method, 'item/started'); + assert.ok(events.some((event) => event.method === 'item/agentMessage/delta')); + assert.equal(events.at(-2).method, 'item/completed'); + assert.equal(events.at(-1).method, 'turn/completed'); + assert.match(events.at(-2).params.item.text, /hello dshx/); +}); + +test('interrupt emits an interrupted turn completion', () => { + const stub = new ProtocolStub({ cwd: workspace }); + const thread = request(stub, 10, 'thread/start', { cwd: workspace }).response.result.thread; + const started = request(stub, 11, 'turn/start', { + threadId: thread.id, + input: [{ type: 'text', text: 'long task', text_elements: [] }] + }); + const turnId = started.response.result.turn.id; + assert.equal(stub.isTurnActive(thread.id, turnId), true); + const interrupted = request(stub, 12, 'turn/interrupt', { threadId: thread.id, turnId }); + assert.deepEqual(interrupted.response.result, {}); + assert.equal(stub.isTurnActive(thread.id, turnId), false); + assert.equal(interrupted.events[0].method, 'turn/completed'); + assert.equal(interrupted.events[0].params.turn.status, 'interrupted'); +}); + +test('unknown method is rejected with JSON-RPC method-not-found', () => { + const stub = new ProtocolStub(); + const { response } = normalizeDispatchResult(stub, { id: 9, method: 'nope/unknown', params: {} }); + assert.equal(response.error.code, -32601); +}); diff --git a/test/request-broker.test.mjs b/test/request-broker.test.mjs new file mode 100644 index 00000000..5ee7c21a --- /dev/null +++ b/test/request-broker.test.mjs @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { UiRequestBroker } from '../src/protocol/request-broker.mjs'; + +test('correlates a server-initiated request with a TUI response', async () => { + const sent = []; + const broker = new UiRequestBroker({ send: (message) => sent.push(message), timeoutMs: 1000 }); + const pending = broker.request('item/commandExecution/requestApproval', { itemId: 'item-1' }); + assert.equal(sent.length, 1); + assert.equal(sent[0].method, 'item/commandExecution/requestApproval'); + assert.match(sent[0].id, /^dshx-ui-/); + + assert.equal(broker.handleResponse({ id: sent[0].id, result: { decision: 'accept' } }), true); + assert.deepEqual(await pending, { decision: 'accept' }); + assert.equal(broker.pending.size, 0); +}); + +test('client errors reject only the matching request', async () => { + const sent = []; + const broker = new UiRequestBroker({ send: (message) => sent.push(message), timeoutMs: 1000 }); + const pending = broker.request('x', {}); + broker.handleResponse({ id: sent[0].id, error: { code: -32000, message: 'nope' } }); + await assert.rejects(pending, /nope/); +}); + +test('unknown client response is not consumed', () => { + const broker = new UiRequestBroker({ send() {} }); + assert.equal(broker.handleResponse({ id: 'other', result: {} }), false); + assert.equal(broker.handleResponse({ id: 'other', method: 'turn/start', params: {} }), false); +}); + +test('abort rejects the UI wait immediately', async () => { + const controller = new AbortController(); + const broker = new UiRequestBroker({ send() {}, timeoutMs: 1000 }); + const pending = broker.request('x', {}, { signal: controller.signal }); + controller.abort(); + await assert.rejects(pending, (error) => error?.name === 'AbortError'); + assert.equal(broker.pending.size, 0); +}); + +test('closing transport rejects all outstanding presentation waits', async () => { + const broker = new UiRequestBroker({ send() {}, timeoutMs: 1000 }); + const first = broker.request('a', {}); + const second = broker.request('b', {}); + broker.close('connection lost'); + await assert.rejects(first, /connection lost/); + await assert.rejects(second, /connection lost/); + assert.equal(broker.pending.size, 0); +}); diff --git a/test/tui-smoke-pty-contract.test.mjs b/test/tui-smoke-pty-contract.test.mjs new file mode 100644 index 00000000..e42cc43a --- /dev/null +++ b/test/tui-smoke-pty-contract.test.mjs @@ -0,0 +1,17 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import test from 'node:test'; + +const source = fs.readFileSync(new URL('./tui_smoke.py', import.meta.url), 'utf8'); + +test('PTY smoke validates rendered prompt tokens instead of contiguous raw terminal bytes', () => { + assert.match(source, /PROMPT_TOKENS\s*=\s*\(/); + assert.match(source, /def expect_rendered_tokens\(/); + assert.equal(source.includes('child.expect(PROMPT_TAIL)'), false); + assert.equal(source.includes('child.expect(PROMPT)'), false); + assert.equal(source.includes('child.expect_exact(PROMPT)'), false); + assert.ok( + source.split('expect_rendered_tokens(child, PROMPT_TOKENS)').length - 1 >= 2, + 'input and echoed-response prompt must both use cursor-addressing-safe token assertions' + ); +}); diff --git a/test/tui_smoke.py b/test/tui_smoke.py new file mode 100644 index 00000000..c49f9ea0 --- /dev/null +++ b/test/tui_smoke.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +import json +import os +import pathlib +import subprocess +import tempfile +import time + +import pexpect + +ROOT = pathlib.Path(__file__).resolve().parents[1] +PROMPT = "你好,DSHX PTY resize" +# Ratatui redraws a line with cursor-addressing writes. On some terminals/runners, +# the visible prompt can therefore arrive as multiple printable spans separated by +# ANSI cursor moves rather than one contiguous raw PTY substring. Validate the +# visible content in order without weakening the CJK/ASCII prompt contract. +PROMPT_TOKENS = ("你", "好", ",", "DSHX", "PTY", "resize") +EXPECTED_TUI_VERSION = os.environ.get("DSHX_VERSION", "1.0.0-ci") +EXPECTED_MODEL_DISPLAY_NAME = "DSHX Protocol Stub" +trace_fd, trace_name = tempfile.mkstemp(prefix="dshx-protocol-trace-", suffix=".jsonl") +os.close(trace_fd) +trace_path = pathlib.Path(trace_name) +server_env = os.environ.copy() +server_env["DSHX_STUB_TRACE_FILE"] = str(trace_path) +server = subprocess.Popen( + ["node", "bin/dshx-stub-local.mjs"], + cwd=ROOT, + env=server_env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, +) + + +def wait_for_protocol_notification(method, timeout=10): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if trace_path.exists(): + for line in trace_path.read_text(encoding="utf-8").splitlines(): + try: + record = json.loads(line) + except json.JSONDecodeError: + continue + if ( + record.get("direction") == "out" + and record.get("kind") == "notification" + and record.get("method") == method + ): + if method == "thread/started" and not isinstance(record.get("threadId"), str): + continue + return + time.sleep(0.05) + trace = trace_path.read_text(encoding="utf-8") if trace_path.exists() else "" + raise AssertionError(f"Timed out waiting for protocol notification {method!r}.\nProtocol trace:\n{trace}") + + +def expect_rendered_tokens(child, tokens): + """Consume printable tokens in visible order while tolerating ANSI redraws between them.""" + rendered = [] + for token in tokens: + child.expect_exact(token) + rendered.append(child.before + child.after) + return "".join(rendered) + + +try: + endpoint = server.stdout.readline().strip() + if not endpoint.startswith("unix://"): + stderr = server.stderr.read() + raise RuntimeError(f"local stub did not produce a unix endpoint: {endpoint!r}\n{stderr}") + + binary = ROOT / "dist" / "bin" / "dshx-tui" + if not binary.exists(): + raise RuntimeError(f"built TUI missing: {binary}") + + with tempfile.TemporaryDirectory(prefix="dshx-codex-home-") as codex_home: + env = os.environ.copy() + env.update({ + "TERM": "xterm-256color", + "CODEX_HOME": codex_home, + "DSHX_APP_SERVER_ENDPOINT": endpoint, + # pexpect provides a PTY transport, not a terminal emulator that can + # negotiate kitty keyboard enhancement modes. Use Codex's supported + # fallback so this gate measures TUI/local-IPC/CJK/resize behavior + # consistently on Linux and macOS rather than terminal emulation. + "CODEX_TUI_DISABLE_KEYBOARD_ENHANCEMENT": "1", + }) + child = pexpect.spawn( + str(binary), + cwd=str(ROOT), + env=env, + encoding="utf-8", + timeout=30, + dimensions=(32, 120), + ) + transcript = [] + try: + # Ratatui can place ANSI style transitions between the product name + # and the version suffix. Assert them independently so the smoke + # validates visible content rather than terminal styling bytes. + child.expect("DeepSeek Harness") + transcript.append(child.before + child.after) + child.expect(f"v{EXPECTED_TUI_VERSION}") + transcript.append(child.before + child.after) + wait_for_protocol_notification("thread/started") + # Startup is asynchronous: thread/started can arrive while the model + # banner still says "loading". Wait for the human-readable model label, + # not the provider-aware internal wire id. + child.expect(EXPECTED_MODEL_DISPLAY_NAME) + transcript.append(child.before + child.after) + rendered_startup = "".join(transcript) + if "v0.0.0" in rendered_startup: + raise AssertionError( + "Codex crate version leaked into DSHX startup presentation:\n" + rendered_startup + ) + if "dshx:Wy" in rendered_startup: + raise AssertionError( + "opaque DSHX model wire id leaked into TUI presentation:\n" + rendered_startup + ) + child.setwinsize(40, 100) + child.send(PROMPT) + # A PTY write completing does not mean the TUI has consumed the + # bytes. On slower macOS runners, a fixed delay measured from write + # completion can expire while the prompt is still queued, making + # Enter arrive immediately after the paste-like burst is processed. + # Wait until every visible CJK/ASCII prompt token has been rendered + # in order. Cursor-addressing escape sequences may occur between + # tokens, so raw contiguous-string matching is intentionally wrong. + transcript.append(expect_rendered_tokens(child, PROMPT_TOKENS)) + time.sleep(0.25) + child.send("\r") + # "received:" is unique to the stub response, so it establishes that + # the submitted turn reached the TUI response path. Then verify the + # echoed prompt using the same rendered-token semantics as the input. + child.expect_exact("received:") + transcript.append(child.before + child.after) + transcript.append(expect_rendered_tokens(child, PROMPT_TOKENS)) + except Exception: + transcript.append(child.before or "") + trace = trace_path.read_text(encoding="utf-8") if trace_path.exists() else "" + raise AssertionError( + "Pinned DSHX TUI local-IPC/CJK/resize smoke failed.\n" + f"Protocol trace:\n{trace}\n" + "Transcript:\n" + "".join(transcript) + ) + finally: + child.close(force=True) +finally: + server.terminate() + try: + server.wait(timeout=5) + except subprocess.TimeoutExpired: + server.kill() + server.wait(timeout=5) + trace_path.unlink(missing_ok=True) diff --git a/test/tui_smoke_windows.mjs b/test/tui_smoke_windows.mjs new file mode 100644 index 00000000..55699f02 --- /dev/null +++ b/test/tui_smoke_windows.mjs @@ -0,0 +1,207 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import process from 'node:process'; +import { spawn } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { createInterface } from 'node:readline'; +import * as pty from 'node-pty'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const EXPECTED_NODE_PTY_VERSION = '1.2.0-beta.15'; +const EXPECTED_TUI_VERSION = process.env.DSHX_VERSION ?? '1.0.0-ci'; +const EXPECTED_MODEL_DISPLAY_NAME = 'DSHX Protocol Stub'; + +function stripTerminalControl(value) { + return value + .replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '') + .replace(/\x1B\[[0-?]*[ -\/]*[@-~]/g, '') + .replace(/\r/g, ''); +} + +function stringEnv(extra = {}) { + return Object.fromEntries( + Object.entries({ ...process.env, ...extra }) + .filter(([, value]) => value != null) + .map(([key, value]) => [key, String(value)]) + ); +} + +function waitForOutput(state, needle, timeoutMs = 30_000) { + const started = Date.now(); + return new Promise((resolve, reject) => { + const poll = setInterval(() => { + const transcript = stripTerminalControl(state.output); + if (transcript.includes(needle)) { + clearInterval(poll); + resolve(); + return; + } + if (Date.now() - started < timeoutMs) return; + clearInterval(poll); + reject(new Error(`Timed out waiting for ${JSON.stringify(needle)}. Transcript:\n${transcript}`)); + }, 50); + }); +} + +function waitForProtocolNotification(traceFile, method, timeoutMs = 10_000) { + const started = Date.now(); + return new Promise((resolve, reject) => { + const poll = setInterval(() => { + if (fs.existsSync(traceFile)) { + for (const line of fs.readFileSync(traceFile, 'utf8').split(/\r?\n/)) { + if (!line) continue; + let record; + try { record = JSON.parse(line); } catch { continue; } + if ( + record.direction === 'out' && + record.kind === 'notification' && + record.method === method && + (method !== 'thread/started' || typeof record.threadId === 'string') + ) { + clearInterval(poll); + resolve(); + return; + } + } + } + if (Date.now() - started < timeoutMs) return; + clearInterval(poll); + const trace = fs.existsSync(traceFile) ? fs.readFileSync(traceFile, 'utf8') : ''; + reject(new Error(`Timed out waiting for protocol notification ${JSON.stringify(method)}.\nProtocol trace:\n${trace}`)); + }, 50); + }); +} + +function firstLine(stream, child, stderrText, timeoutMs = 15_000) { + const lines = createInterface({ input: stream, crlfDelay: Infinity }); + return new Promise((resolve, reject) => { + const timer = setTimeout(() => fail(new Error(`Timed out waiting for local IPC stub: ${stderrText()}`)), timeoutMs); + const cleanup = () => { + clearTimeout(timer); + child.off('error', onError); + child.off('exit', onExit); + }; + const fail = (error) => { + cleanup(); + lines.close(); + reject(error); + }; + const onError = (error) => fail(error); + const onExit = (code, signal) => fail(new Error(`local IPC stub exited before ready (${signal ?? code ?? 'unknown'}): ${stderrText()}`)); + child.once('error', onError); + child.once('exit', onExit); + lines.once('line', (line) => { + cleanup(); + lines.close(); + resolve(line); + }); + }); +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function stopChild(child) { + if (!child || child.exitCode !== null || child.signalCode !== null) return; + const exited = new Promise((resolve) => child.once('exit', resolve)); + child.kill('SIGTERM'); + const timer = new Promise((resolve) => setTimeout(resolve, 5_000, 'timeout')); + if (await Promise.race([exited.then(() => 'exit'), timer]) === 'timeout') { + child.kill('SIGKILL'); + await exited; + } +} + +if (process.platform !== 'win32') { + throw new Error('tui_smoke_windows.mjs must run on Windows/ConPTY'); +} + +const nodePtyPackage = JSON.parse(fs.readFileSync(path.join(ROOT, 'node_modules', 'node-pty', 'package.json'), 'utf8')); +if (nodePtyPackage.version !== EXPECTED_NODE_PTY_VERSION) { + throw new Error( + `Windows ConPTY smoke must use frozen node-pty ${EXPECTED_NODE_PTY_VERSION}; got ${nodePtyPackage.version}` + ); +} + +const binary = path.join(ROOT, 'dist', 'bin', 'dshx-tui.exe'); +const bridge = path.join(ROOT, 'dist', 'bin', 'dshx-ipc-bridge.exe'); +if (!fs.existsSync(binary)) throw new Error(`built DSHX TUI missing: ${binary}`); +if (!fs.existsSync(bridge)) throw new Error(`built DSHX IPC bridge missing: ${bridge}`); + +const codexHome = fs.mkdtempSync(path.join(os.tmpdir(), 'dshx-conpty-home-')); +const traceFile = path.join(os.tmpdir(), `dshx-conpty-trace-${process.pid}-${Date.now()}.jsonl`); +let server; +let serverStderr = ''; +let term; +let dataDisposable; +try { + server = spawn(process.execPath, [path.join(ROOT, 'bin', 'dshx-stub-local.mjs')], { + cwd: ROOT, + env: stringEnv({ DSHX_IPC_BRIDGE_BIN: bridge, DSHX_STUB_TRACE_FILE: traceFile }), + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true + }); + server.stderr.on('data', (chunk) => { + serverStderr = `${serverStderr}${chunk.toString('utf8')}`.slice(-8192); + }); + const endpoint = await firstLine(server.stdout, server, () => serverStderr); + if (!endpoint.startsWith('unix://')) { + throw new Error(`local IPC stub returned non-local endpoint ${JSON.stringify(endpoint)}: ${serverStderr}`); + } + + term = pty.spawn(binary, [], { + name: 'xterm-256color', + cols: 120, + rows: 32, + cwd: ROOT, + env: stringEnv({ + TERM: 'xterm-256color', + CODEX_HOME: codexHome, + DSHX_APP_SERVER_ENDPOINT: endpoint, + // node-pty provides a ConPTY transport, not a terminal emulator capable + // of negotiating Codex's keyboard enhancement protocol. Use Codex's + // supported fallback and exercise canonical terminal key bytes instead. + CODEX_TUI_DISABLE_KEYBOARD_ENHANCEMENT: '1' + }) + }); + const state = { output: '' }; + dataDisposable = term.onData((chunk) => { state.output += chunk; }); + + await waitForOutput(state, `DeepSeek Harness (v${EXPECTED_TUI_VERSION})`); + await waitForProtocolNotification(traceFile, 'thread/started'); + // thread/started precedes the asynchronous model/banner refresh. Wait for + // the human-readable model label, not the provider-aware internal wire id. + await waitForOutput(state, EXPECTED_MODEL_DISPLAY_NAME); + const startupTranscript = stripTerminalControl(state.output); + if (startupTranscript.includes('v0.0.0')) { + throw new Error(`Codex crate version leaked into DSHX startup presentation:\n${startupTranscript}`); + } + if (startupTranscript.includes('dshx:Wy')) { + throw new Error(`opaque DSHX model wire id leaked into TUI presentation:\n${startupTranscript}`); + } + term.resize(100, 40); + const prompt = '你好,DSHX ConPTY resize'; + term.write(prompt); + // ConPTY writes this UTF-8 prompt as a burst. Codex deliberately suppresses + // Enter for 120 ms after paste-like input; wait safely beyond that product + // behavior before submitting. Real keyboard/IME fidelity is gate #12. + await sleep(250); + term.write('\r'); + await waitForOutput(state, 'DSHX protocol stub received:'); + // Validate the echoed assistant response rather than requiring the input + // composer's ANSI redraws to contain one contiguous copy of the prompt. + await waitForOutput(state, prompt); + process.stdout.write('Windows ConPTY DSHX local-IPC + CJK + resize smoke passed\n'); +} finally { + dataDisposable?.dispose?.(); + try { term?.kill(); } catch {} + // Give ConPTY/TUI a bounded moment to close the local socket cleanly before + // terminating the deterministic adapter process. + await sleep(100); + await stopChild(server); + fs.rmSync(codexHome, { recursive: true, force: true }); + fs.rmSync(traceFile, { force: true }); +} diff --git a/test/verify-dsh-closure.test.mjs b/test/verify-dsh-closure.test.mjs new file mode 100644 index 00000000..2464292a --- /dev/null +++ b/test/verify-dsh-closure.test.mjs @@ -0,0 +1,54 @@ +import assert from 'node:assert/strict'; +import { execFileSync, spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const VERIFY = path.join(ROOT, 'scripts', 'verify-dsh-closure.mjs'); + +function fixture(lockPackages) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dshx-closure-test-')); + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ + dependencies: { '@deepseek-ai/dsh': '0.1.0-rc.8' } + })); + fs.writeFileSync(path.join(dir, 'package-lock.json'), JSON.stringify({ + lockfileVersion: 3, + packages: lockPackages + })); + return dir; +} + +test('DSH closure verifier ignores ordinary dependencies nested below DSH packages', () => { + const dir = fixture({ + '': { dependencies: { '@deepseek-ai/dsh': '0.1.0-rc.8' } }, + 'node_modules/@deepseek-ai/dsh': { version: '0.1.0-rc.8' }, + 'node_modules/@deepseek-ai/dsh-client-ui-trajectory': { version: '0.1.0-rc.8' }, + 'node_modules/@deepseek-ai/dsh-client-ui-trajectory/node_modules/react': { version: '19.2.8' }, + 'node_modules/@deepseek-ai/dsh-skill-filesystem': { version: '0.1.0-rc.8' }, + 'node_modules/@deepseek-ai/dsh-skill-filesystem/node_modules/chokidar': { version: '5.0.0' } + }); + try { + const output = execFileSync(process.execPath, [VERIFY, dir, path.join(dir, 'package-lock.json')], { encoding: 'utf8' }); + assert.match(output, /Verified 3 DeepSeek Harness packages at 0\.1\.0-rc\.8/); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +test('DSH closure verifier still rejects an actual mismatched DSH package record', () => { + const dir = fixture({ + '': { dependencies: { '@deepseek-ai/dsh': '0.1.0-rc.8' } }, + 'node_modules/@deepseek-ai/dsh': { version: '0.1.0-rc.8' }, + 'node_modules/@deepseek-ai/dsh-agent': { version: '0.1.0-rc.7' } + }); + try { + const result = spawnSync(process.execPath, [VERIFY, dir, path.join(dir, 'package-lock.json')], { encoding: 'utf8' }); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /dsh-agent: 0\.1\.0-rc\.7/); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/test/wire.test.mjs b/test/wire.test.mjs new file mode 100644 index 00000000..9ced428a --- /dev/null +++ b/test/wire.test.mjs @@ -0,0 +1,152 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { WebSocket } from 'ws'; +import { startProtocolStubServer } from '../src/server.mjs'; + +function openSocket(url) { + return new Promise((resolve, reject) => { + const socket = new WebSocket(url); + socket.once('open', () => resolve(socket)); + socket.once('error', reject); + }); +} + +function createInbox(socket) { + const queued = []; + const waiters = []; + + socket.on('message', (raw) => { + const message = JSON.parse(raw.toString('utf8')); + const waiter = waiters.shift(); + if (waiter) waiter.resolve(message); + else queued.push(message); + }); + + socket.on('error', (error) => { + for (const waiter of waiters.splice(0)) waiter.reject(error); + }); + + return { + async next(timeoutMs = 2000) { + if (queued.length > 0) return queued.shift(); + let timer; + try { + return await new Promise((resolve, reject) => { + const waiter = { resolve, reject }; + waiters.push(waiter); + timer = setTimeout(() => { + const index = waiters.indexOf(waiter); + if (index !== -1) waiters.splice(index, 1); + reject(new Error(`Timed out waiting for WebSocket message after ${timeoutMs}ms`)); + }, timeoutMs); + }); + } finally { + clearTimeout(timer); + } + }, + + async until(predicate, timeoutMs = 3000) { + const deadline = Date.now() + timeoutMs; + const seen = []; + while (Date.now() < deadline) { + const message = await this.next(Math.max(1, deadline - Date.now())); + seen.push(message); + if (predicate(message)) return { message, seen }; + } + throw new Error('Timed out waiting for matching WebSocket message'); + } + }; +} + +function send(socket, message) { + socket.send(JSON.stringify(message)); +} + +function closeSocket(socket) { + return new Promise((resolve) => { + if (socket.readyState === WebSocket.CLOSED) return resolve(); + socket.once('close', resolve); + socket.close(); + }); +} + +test('wire: Codex-style initialize → thread → streamed turn lifecycle', async (t) => { + const server = await startProtocolStubServer({ eventDelayMs: 2 }); + const socket = await openSocket(server.url); + t.after(async () => { + await closeSocket(socket); + await server.close(); + }); + const inbox = createInbox(socket); + + send(socket, { + id: 'initialize', + method: 'initialize', + params: { clientInfo: { name: 'codex-tui', version: 'wire-test' } } + }); + const initialize = await inbox.next(); + assert.equal(initialize.id, 'initialize'); + assert.equal(initialize.result.platformFamily.length > 0, true); + assert.equal(Object.hasOwn(initialize, 'jsonrpc'), false); + + // Codex sends this after a successful initialize response. It is a notification, + // so the server must accept it without replying. + send(socket, { method: 'initialized' }); + + for (const [id, method, expected] of [ + [1, 'account/read', (result) => result.requiresOpenaiAuth === false], + [2, 'model/list', (result) => result.data?.[0]?.model === 'dshx-stub'], + [3, 'configRequirements/read', (result) => result.requirements === null] + ]) { + send(socket, { id, method, params: {} }); + const response = await inbox.until((message) => message.id === id); + assert.equal(expected(response.message.result), true, `${method} returned unexpected result`); + } + + send(socket, { id: 4, method: 'thread/start', params: {} }); + const threadStart = await inbox.until((message) => message.id === 4); + const threadId = threadStart.message.result.thread.id; + assert.equal(typeof threadId, 'string'); + + const threadStarted = await inbox.until( + (message) => message.method === 'thread/started' && message.params?.thread?.id === threadId + ); + assert.equal(threadStarted.message.params.thread.modelProvider, 'dsh'); + + send(socket, { + id: 5, + method: 'turn/start', + params: { + threadId, + input: [{ type: 'text', text: 'wire hello', text_elements: [] }] + } + }); + + const turnResponse = await inbox.until((message) => message.id === 5); + const turnId = turnResponse.message.result.turn.id; + assert.equal(turnResponse.message.result.turn.status, 'inProgress'); + + const events = []; + while (true) { + const message = await inbox.next(); + if (message.method) events.push(message); + if ( + message.method === 'turn/completed' && + message.params?.threadId === threadId && + message.params?.turn?.id === turnId + ) break; + } + + const methods = events.map((event) => event.method); + assert.equal(methods[0], 'turn/started'); + assert.equal(methods[1], 'item/started'); + assert.ok(methods.includes('item/agentMessage/delta')); + assert.equal(methods.at(-2), 'item/completed'); + assert.equal(methods.at(-1), 'turn/completed'); + + const deltas = events + .filter((event) => event.method === 'item/agentMessage/delta') + .map((event) => event.params.delta) + .join(''); + assert.match(deltas, /wire hello/); +}); diff --git a/upstream/DSH_COMMIT b/upstream/DSH_COMMIT index e9035fd1..cbba87c8 100644 --- a/upstream/DSH_COMMIT +++ b/upstream/DSH_COMMIT @@ -1 +1 @@ -99f6f02fecdb7dff40c3fbc9470f5907c29f74ca +141eb6fef83422698aef7a981029e843e8161534 diff --git a/upstream/patches/codex/0001-dshx-force-ui-backend.patch b/upstream/patches/codex/0001-dshx-force-ui-backend.patch new file mode 100644 index 00000000..f2a8bb2c --- /dev/null +++ b/upstream/patches/codex/0001-dshx-force-ui-backend.patch @@ -0,0 +1,65 @@ +diff --git a/codex-rs/tui/src/main.rs b/codex-rs/tui/src/main.rs +--- a/codex-rs/tui/src/main.rs ++++ b/codex-rs/tui/src/main.rs +@@ -1,10 +1,12 @@ + use clap::Parser; ++use codex_app_server_client::RemoteAppServerEndpoint; + use codex_arg0::Arg0DispatchPaths; + use codex_arg0::arg0_dispatch_or_else; + use codex_config::LoaderOverrides; + use codex_tui::AppExitInfo; + use codex_tui::Cli; + use codex_tui::ExitReason; ++use codex_tui::resolve_remote_addr; + use codex_tui::run_main; + use codex_utils_cli::CliConfigOverrides; + use std::io::Write; +@@ -47,6 +49,42 @@ fn main() -> anyhow::Result<()> { + inner + .config_overrides + .raw_overrides + .splice(0..0, top_cli.config_overrides.raw_overrides); ++ if let Ok(mode) = std::env::var("DSHX_RESUME_MODE") { ++ match mode.as_str() { ++ "picker" => inner.resume_picker = true, ++ "last" => inner.resume_last = true, ++ "id" => { ++ inner.resume_session_id = Some( ++ std::env::var("DSHX_RESUME_SESSION_ID").map_err(|_| { ++ anyhow::anyhow!( ++ "DSHX_RESUME_SESSION_ID is required when DSHX_RESUME_MODE=id" ++ ) ++ })?, ++ ); ++ } ++ other => anyhow::bail!("invalid DSHX_RESUME_MODE: {other}"), ++ } ++ } ++ let dshx_endpoint = std::env::var("DSHX_APP_SERVER_ENDPOINT").map_err(|_| { ++ anyhow::anyhow!( ++ "DSHX_APP_SERVER_ENDPOINT is required; DSHX never falls back to the Codex agent runtime" ++ ) ++ })?; ++ let dshx_token = std::env::var("DSHX_APP_SERVER_TOKEN").map_err(|_| { ++ anyhow::anyhow!( ++ "DSHX_APP_SERVER_TOKEN is required; DSHX local transport is always authenticated" ++ ) ++ })?; ++ let mut dshx_endpoint = resolve_remote_addr(&dshx_endpoint)?; ++ match &mut dshx_endpoint { ++ RemoteAppServerEndpoint::WebSocket { auth_token, .. } => { ++ *auth_token = Some(dshx_token); ++ } ++ RemoteAppServerEndpoint::UnixSocket { .. } => { ++ anyhow::bail!("DSHX production launcher requires authenticated loopback WebSocket transport") ++ } ++ } + let exit_info = run_main( + inner, + arg0_paths, + LoaderOverrides::default(), +- /*explicit_remote_endpoint*/ None, ++ /*explicit_remote_endpoint*/ Some(dshx_endpoint), + ) + .await?; + let is_fatal = match &exit_info.exit_reason { diff --git a/upstream/patches/codex/0002-dshx-branding.patch b/upstream/patches/codex/0002-dshx-branding.patch new file mode 100644 index 00000000..ed950523 --- /dev/null +++ b/upstream/patches/codex/0002-dshx-branding.patch @@ -0,0 +1,38 @@ +diff --git a/codex-rs/tui/src/history_cell/session.rs b/codex-rs/tui/src/history_cell/session.rs +--- a/codex-rs/tui/src/history_cell/session.rs ++++ b/codex-rs/tui/src/history_cell/session.rs +@@ -142,7 +142,7 @@ pub(crate) fn new_session_info( + Line::from(vec![ + " ".into(), + "/init".into(), +- " - create an AGENTS.md file with instructions for Codex".dim(), ++ " - create an AGENTS.md file with instructions for DeepSeek Harness".dim(), + ]), +@@ -152,7 +152,7 @@ pub(crate) fn new_session_info( + Line::from(vec![ + " ".into(), + "/permissions".into(), +- " - choose what Codex is allowed to do".dim(), ++ " - choose what DeepSeek Harness is allowed to do".dim(), + ]), +@@ -269,10 +269,10 @@ impl HistoryCell for SessionHeaderHistoryCell { + let make_row = |spans: Vec>| Line::from(spans); + +- // Title line rendered inside the box: ">_ OpenAI Codex (vX)" ++ // DSHX changes branding only; layout and interaction stay upstream Codex. + let title_spans: Vec> = vec![ + Span::from(">_ ").dim(), +- Span::from("OpenAI Codex").bold(), ++ Span::from("DeepSeek Harness").bold(), + Span::from(" ").dim(), + Span::from(format!("(v{})", self.version)).dim(), + ]; +@@ -335,7 +335,7 @@ impl HistoryCell for SessionHeaderHistoryCell { + + fn raw_lines(&self) -> Vec> { + let mut lines = vec![ +- Line::from(format!("OpenAI Codex (v{})", self.version)), ++ Line::from(format!("DeepSeek Harness (v{})", self.version)), + Line::from(format!( + "model: {}{}", + self.model, diff --git a/upstream/patches/codex/0003-dshx-approval-semantics.patch b/upstream/patches/codex/0003-dshx-approval-semantics.patch new file mode 100644 index 00000000..a54067df --- /dev/null +++ b/upstream/patches/codex/0003-dshx-approval-semantics.patch @@ -0,0 +1,91 @@ +diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs +--- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs ++++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs +@@ -796,6 +796,10 @@ struct ApprovalOption { + shortcuts: Vec, + } + ++fn dshx_backend() -> bool { ++ std::env::var_os("DSHX_APP_SERVER_ENDPOINT").is_some() ++} ++ + fn command_decision_to_review_decision( + decision: &CommandExecutionApprovalDecision, + ) -> ReviewDecision { +@@ -855,6 +859,7 @@ fn exec_options( + }) + } ++ CommandExecutionApprovalDecision::AcceptForSession if dshx_backend() => None, + CommandExecutionApprovalDecision::AcceptForSession => Some(ApprovalOption { + label: if network_approval_context.is_some() { + "Yes, and allow this host for this conversation".to_string() +@@ -951,24 +956,27 @@ fn patch_options(keymap: &ApprovalKeymap) -> Vec { +- vec![ ++ let mut options = vec![ + ApprovalOption { + label: "Yes, proceed".to_string(), + decision: ApprovalDecision::FileChange(FileChangeApprovalDecision::Accept), + shortcuts: keymap.approve.clone(), + }, +- ApprovalOption { +- label: "Yes, and don't ask again for these files".to_string(), +- decision: ApprovalDecision::FileChange(FileChangeApprovalDecision::AcceptForSession), +- shortcuts: keymap.approve_for_session.clone(), +- }, + ApprovalOption { +- label: "No, and tell Codex what to do differently".to_string(), ++ label: if dshx_backend() { ++ "No, and tell DeepSeek Harness what to do differently".to_string() ++ } else { ++ "No, and tell Codex what to do differently".to_string() ++ }, + decision: ApprovalDecision::FileChange(FileChangeApprovalDecision::Cancel), + shortcuts: keymap.decline.clone(), + }, +- ] ++ ]; ++ if !dshx_backend() { ++ options.insert(1, ApprovalOption { ++ label: "Yes, and don't ask again for these files".to_string(), ++ decision: ApprovalDecision::FileChange(FileChangeApprovalDecision::AcceptForSession), ++ shortcuts: keymap.approve_for_session.clone(), ++ }); ++ } ++ options + } + + fn permissions_options(keymap: &ApprovalKeymap) -> Vec { +@@ -978,7 +986,7 @@ fn permissions_options(keymap: &ApprovalKeymap) -> Vec { + .filter(|shortcut| shortcut.parts() != (KeyCode::Esc, KeyModifiers::NONE)) + .collect(); + +- vec![ ++ let mut options = vec![ + ApprovalOption { + label: "Yes, grant these permissions for this turn".to_string(), + decision: ApprovalDecision::Permissions(PermissionsDecision::GrantForTurn), +@@ -991,16 +999,19 @@ fn permissions_options(keymap: &ApprovalKeymap) -> Vec { + ), + shortcuts: vec![key_hint::plain(KeyCode::Char('r'))], + }, +- ApprovalOption { +- label: "Yes, grant these permissions for this session".to_string(), +- decision: ApprovalDecision::Permissions(PermissionsDecision::GrantForSession), +- shortcuts: keymap.approve_for_session.clone(), +- }, + ApprovalOption { + label: "No, continue without permissions".to_string(), + decision: ApprovalDecision::Permissions(PermissionsDecision::Deny), + shortcuts: deny_shortcuts, + }, +- ] ++ ]; ++ if !dshx_backend() { ++ options.insert(2, ApprovalOption { ++ label: "Yes, grant these permissions for this session".to_string(), ++ decision: ApprovalDecision::Permissions(PermissionsDecision::GrantForSession), ++ shortcuts: keymap.approve_for_session.clone(), ++ }); ++ } ++ options + } diff --git a/upstream/patches/codex/0004-dshx-permission-picker.patch b/upstream/patches/codex/0004-dshx-permission-picker.patch new file mode 100644 index 00000000..14366be2 --- /dev/null +++ b/upstream/patches/codex/0004-dshx-permission-picker.patch @@ -0,0 +1,60 @@ +diff --git a/codex-rs/tui/src/chatwidget/permission_popups.rs b/codex-rs/tui/src/chatwidget/permission_popups.rs +--- a/codex-rs/tui/src/chatwidget/permission_popups.rs ++++ b/codex-rs/tui/src/chatwidget/permission_popups.rs +@@ -7,33 +7,39 @@ + use super::*; + use codex_protocol::openai_models::MODEL_SPECIALTY_CYBER; + ++fn dshx_backend() -> bool { ++ std::env::var_os("DSHX_APP_SERVER_ENDPOINT").is_some() ++} ++ + impl ChatWidget { + /// Open the permissions popup. + pub(crate) fn open_approvals_popup(&mut self) { + self.open_permissions_popup(); + } + + /// Open a popup to choose the permissions mode. + pub(crate) fn open_permissions_popup(&mut self) { + if self.config.explicit_permission_profile_mode { + self.open_permission_profiles_popup(); + return; + } + + let include_read_only = cfg!(target_os = "windows"); + let current_approval = + AskForApproval::from(self.config.permissions.approval_policy.value()); + let current_permission_profile = self.config.permissions.permission_profile().clone(); +- let guardian_approval_enabled = self.config.features.enabled(Feature::GuardianApproval); ++ let guardian_approval_enabled = !dshx_backend() ++ && self.config.features.enabled(Feature::GuardianApproval); + let current_review_policy = self.config.approvals_reviewer; + let mut items: Vec = Vec::new(); + let presets: Vec = builtin_approval_presets(); + + #[cfg(target_os = "windows")] + let windows_sandbox_level = crate::windows_sandbox::level_from_config(&self.config); + #[cfg(target_os = "windows")] + let windows_degraded_sandbox_enabled = +- matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken); ++ !dshx_backend() ++ && matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken); + #[cfg(not(target_os = "windows"))] + let windows_degraded_sandbox_enabled = false; + + let show_elevate_sandbox_hint = + windows_degraded_sandbox_enabled && presets.iter().any(|preset| preset.id == "auto"); +@@ -313,7 +319,11 @@ impl ChatWidget { + }); + })]; + } +- if approvals_reviewer == ApprovalsReviewer::User && preset.id == "auto" { ++ // DSHX never enables or configures Codex's local Windows sandbox. The ++ // selected workspace preset is sent to the DSH app-server adapter and ++ // DSH remains the enforcement owner. ++ if !dshx_backend() ++ && approvals_reviewer == ApprovalsReviewer::User && preset.id == "auto" { + #[cfg(target_os = "windows")] + { + if crate::windows_sandbox::level_from_config(&self.config) diff --git a/upstream/patches/codex/0005-dshx-skip-codex-onboarding.patch b/upstream/patches/codex/0005-dshx-skip-codex-onboarding.patch new file mode 100644 index 00000000..d55e8ef2 --- /dev/null +++ b/upstream/patches/codex/0005-dshx-skip-codex-onboarding.patch @@ -0,0 +1,112 @@ +diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs +--- a/codex-rs/tui/src/lib.rs ++++ b/codex-rs/tui/src/lib.rs +@@ -232,8 +232,12 @@ + const TUI_LOG_FILE_NAME: &str = "codex-tui.log"; + const INTERACTIVE_OTEL_SHUTDOWN_TIMEOUT: Duration = Duration::from_millis(/*millis*/ 500); + ++fn dshx_backend() -> bool { ++ std::env::var_os("DSHX_APP_SERVER_ENDPOINT").is_some() ++} ++ + #[cfg(unix)] + const AUTO_CONNECT_DAEMON_CONNECT_TIMEOUT: std::time::Duration = + std::time::Duration::from_millis(50); + +@@ -1068,7 +1072,9 @@ + } + } + let remote_project_trust = +- if uses_remote_workspace && let Some(remote_cwd) = remote_cwd_override.as_deref() { ++ if !dshx_backend() ++ && uses_remote_workspace ++ && let Some(remote_cwd) = remote_cwd_override.as_deref() { + match startup_draft + .run_until( + &mut tui, +@@ -1094,10 +1100,11 @@ + } else { + None + }; + let mut app_server = Some(app_server_session); +- let should_show_trust_screen_flag = remote_project_trust.is_some() +- || (!uses_remote_workspace && should_show_trust_screen(&initial_config)); ++ let should_show_trust_screen_flag = !dshx_backend() ++ && (remote_project_trust.is_some() ++ || (!uses_remote_workspace && should_show_trust_screen(&initial_config))); + #[cfg(target_os = "windows")] + let mut trust_decision_was_made = false; + let startup_model_provider = initial_config.model_provider_id.clone(); + let (login_status, mut startup_account) = if workload_identity_selected { + (LoginStatus::AuthMode(AuthMode::Chatgpt), None) +- } else if initial_config.model_provider.requires_openai_auth { ++ } else if !dshx_backend() && initial_config.model_provider.requires_openai_auth { + let Some(active_app_server) = app_server.as_mut() else { + unreachable!("app server should exist when auth is required"); + }; +@@ -1128,8 +1135,9 @@ + } else { + (LoginStatus::NotAuthenticated, None) + }; + let should_show_onboarding = +- should_show_onboarding(login_status, &initial_config, should_show_trust_screen_flag); ++ !dshx_backend() ++ && should_show_onboarding(login_status, &initial_config, should_show_trust_screen_flag); + + let config = if should_show_onboarding { + if let Err(err) = startup_draft.flush_pending_events(&mut tui).await { + shutdown_startup_session(app_server.take(), &mut terminal_restore_guard).await; +diff --git a/codex-rs/tui/src/app/startup.rs b/codex-rs/tui/src/app/startup.rs +--- a/codex-rs/tui/src/app/startup.rs ++++ b/codex-rs/tui/src/app/startup.rs +@@ -4,9 +4,12 @@ + //! remains isolated from protected interactive requests until the initialized composer owns it. + + use super::*; + + async fn resolve_runtime_model_provider_base_url(provider: &ModelProviderInfo) -> Option { ++ if crate::dshx_backend() { ++ return None; ++ } + let provider = create_model_provider(provider.clone(), /*auth_manager*/ None); + match provider.runtime_base_url().await { + Ok(base_url) => base_url, + Err(err) => { +@@ -384,8 +387,9 @@ + chat_widget.note_rendered_width(tui.terminal.last_known_screen_size.width); + chat_widget.remote_connection = remote_connection; + let thread_and_widget_ms = thread_and_widget_started_at.elapsed().as_millis(); +- chat_widget +- .maybe_prompt_windows_sandbox_enable(should_prompt_windows_sandbox_nux_at_startup); ++ chat_widget.maybe_prompt_windows_sandbox_enable( ++ should_prompt_windows_sandbox_nux_at_startup && !crate::dshx_backend(), ++ ); + + let file_search = FileSearchManager::new(config.cwd.to_path_buf(), app_event_tx.clone()); + let runtime_keymap = RuntimeKeymap::from_config(&config.tui_keymap).map_err(|err| { +@@ -399,7 +403,11 @@ + See the Codex keymap documentation for supported actions and examples." + ) + })?; + #[cfg(not(debug_assertions))] +- let upgrade_version = crate::updates::get_upgrade_version(&config); ++ let upgrade_version = if crate::dshx_backend() { ++ None ++ } else { ++ crate::updates::get_upgrade_version(&config) ++ }; + + let mut app = Self { + model_catalog, +@@ -488,8 +496,9 @@ + // world-writable dirs on Windows. + #[cfg(target_os = "windows")] + { + let startup_permission_profile = app.config.permissions.effective_permission_profile(); +- let should_check = crate::windows_sandbox::level_from_config(&app.config) ++ let should_check = !crate::dshx_backend() ++ && crate::windows_sandbox::level_from_config(&app.config) + != WindowsSandboxLevel::Disabled + && managed_filesystem_sandbox_is_restricted(&startup_permission_profile) + && !app + .config diff --git a/upstream/patches/codex/0006-dshx-config-ownership.patch b/upstream/patches/codex/0006-dshx-config-ownership.patch new file mode 100644 index 00000000..456486b7 --- /dev/null +++ b/upstream/patches/codex/0006-dshx-config-ownership.patch @@ -0,0 +1,13 @@ +diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs +--- a/codex-rs/tui/src/app/event_dispatch.rs ++++ b/codex-rs/tui/src/app/event_dispatch.rs +@@ -1518,7 +1518,8 @@ impl App { + self.sync_active_thread_service_tier_to_cached_session() + .await; + +- if let Some(default_effort) = default_effort.as_ref() ++ if !crate::dshx_backend() ++ && let Some(default_effort) = default_effort.as_ref() + && let Err(err) = crate::config_update::write_config_batch( + app_server.request_handle(), + crate::config_update::build_model_selection_edits( diff --git a/upstream/patches/codex/0007-dshx-thread-ownership.patch b/upstream/patches/codex/0007-dshx-thread-ownership.patch new file mode 100644 index 00000000..41161174 --- /dev/null +++ b/upstream/patches/codex/0007-dshx-thread-ownership.patch @@ -0,0 +1,46 @@ +diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs +--- a/codex-rs/tui/src/app_server_session.rs ++++ b/codex-rs/tui/src/app_server_session.rs +@@ -1660,6 +1660,18 @@ fn thread_start_params_from_config( + remote_cwd_override: Option<&std::path::Path>, + session_start_source: Option, + ) -> ThreadStartParams { ++ if crate::dshx_backend() && matches!(thread_params_mode, ThreadParamsMode::Remote) { ++ return ThreadStartParams { ++ cwd: thread_cwd_from_config(config, thread_params_mode, remote_cwd_override), ++ ephemeral: Some(config.ephemeral), ++ history_mode: (!config.ephemeral).then_some(ThreadHistoryMode::Paginated), ++ session_start_source, ++ thread_source: Some(ThreadSource::User), ++ ..ThreadStartParams::default() ++ }; ++ } + let permissions = permissions_selection_from_config(config, thread_params_mode); + let sandbox = permissions + .is_none() +@@ -1703,6 +1715,12 @@ fn thread_resume_params_from_config( + remote_cwd_override: Option<&std::path::Path>, + model_settings: ResumeModelSettings, + ) -> ThreadResumeParams { ++ if crate::dshx_backend() && matches!(thread_params_mode, ThreadParamsMode::Remote) { ++ return ThreadResumeParams { ++ thread_id: thread_id.to_string(), ++ ..ThreadResumeParams::default() ++ }; ++ } + if model_settings == ResumeModelSettings::PreserveExistingThread { + return ThreadResumeParams { + thread_id: thread_id.to_string(), +@@ -1755,6 +1773,12 @@ fn thread_fork_params_from_config( + thread_params_mode: ThreadParamsMode, + remote_cwd_override: Option<&std::path::Path>, + ) -> ThreadForkParams { ++ if crate::dshx_backend() && matches!(thread_params_mode, ThreadParamsMode::Remote) { ++ return ThreadForkParams { ++ thread_id: thread_id.to_string(), ++ ..ThreadForkParams::default() ++ }; ++ } + let permissions = permissions_selection_from_config(&config, thread_params_mode); + let sandbox = permissions + .is_none() diff --git a/upstream/patches/codex/0008-dshx-resume-hint.patch b/upstream/patches/codex/0008-dshx-resume-hint.patch new file mode 100644 index 00000000..117c9ffc --- /dev/null +++ b/upstream/patches/codex/0008-dshx-resume-hint.patch @@ -0,0 +1,17 @@ +diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs +--- a/codex-rs/tui/src/app.rs ++++ b/codex-rs/tui/src/app.rs +@@ -471,7 +471,12 @@ fn resume_hint_for_resumable_thread( + rollout_path: Option<&Path>, + ) -> Option { + let thread = resumable_thread(thread_id, thread_name, rollout_path)?; +- codex_utils_cli::resume_hint(thread.thread_name.as_deref(), Some(thread.thread_id)) ++ let hint = codex_utils_cli::resume_hint(thread.thread_name.as_deref(), Some(thread.thread_id)); ++ if crate::dshx_backend() { ++ hint.map(|hint| hint.replacen("codex", "dshx", 1)) ++ } else { ++ hint ++ } + } + + fn rollout_path_is_resumable(rollout_path: &Path) -> bool { diff --git a/upstream/patches/codex/0009-dshx-slash-capabilities.patch b/upstream/patches/codex/0009-dshx-slash-capabilities.patch new file mode 100644 index 00000000..df2eb2a3 --- /dev/null +++ b/upstream/patches/codex/0009-dshx-slash-capabilities.patch @@ -0,0 +1,106 @@ +diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs +--- a/codex-rs/tui/src/slash_command.rs ++++ b/codex-rs/tui/src/slash_command.rs +@@ -86,48 +86,48 @@ impl SlashCommand { + pub fn description(self) -> &'static str { + match self { + SlashCommand::Feedback => "send logs to maintainers", + SlashCommand::New => "start a new chat during a conversation", +- SlashCommand::Init => "create an AGENTS.md file with instructions for Codex", ++ SlashCommand::Init => "create an AGENTS.md file with instructions for DeepSeek Harness", + SlashCommand::Compact => "summarize conversation to prevent hitting the context limit", + SlashCommand::Review => "review my current changes and find issues", + SlashCommand::Rename => "rename the current thread", + SlashCommand::Resume => "resume a saved chat", + SlashCommand::Archive => "archive this session and exit", + SlashCommand::Delete => "permanently delete this session and exit", + SlashCommand::Clear => "clear the terminal and start a new chat", + SlashCommand::Fork => "fork the current chat", + SlashCommand::App => "continue this session in the Desktop app", +- SlashCommand::Quit | SlashCommand::Exit => "exit Codex", ++ SlashCommand::Quit | SlashCommand::Exit => "exit DSHX", + SlashCommand::Copy => "copy last response as markdown", + SlashCommand::Export => "export the conversation as markdown", + SlashCommand::Raw => "toggle raw scrollback mode for copy-friendly terminal selection", + SlashCommand::Diff => "show git diff (including untracked files)", + SlashCommand::Mention => "mention a file", +- SlashCommand::Skills => "use skills to improve how Codex performs specific tasks", ++ SlashCommand::Skills => "use skills to improve how DeepSeek Harness performs specific tasks", + SlashCommand::Import => "import setup, this project, and recent chats from Claude Code", + SlashCommand::Hooks => "view and manage lifecycle hooks", + SlashCommand::Status => "show current session configuration and token usage", + SlashCommand::Cd => "change the current working directory", + SlashCommand::Pwd => "show the current working directory", + SlashCommand::Usage => "view account usage or use a usage limit reset", + SlashCommand::DebugConfig => "show config layers and requirement sources for debugging", + SlashCommand::Title => "configure which items appear in the terminal title", + SlashCommand::Statusline => "configure which items appear in the status line", + SlashCommand::Theme => "choose a syntax highlighting theme", + SlashCommand::Pets => "choose or hide the terminal pet", + SlashCommand::Ps => "list background terminals", + SlashCommand::Stop => "stop all background terminals", + SlashCommand::MemoryDrop => "DO NOT USE", + SlashCommand::MemoryUpdate => "DO NOT USE", + SlashCommand::Model => "choose what model and reasoning effort to use", + SlashCommand::Ide => { + "include current selection, open files, and other context from your IDE" + } + SlashCommand::Personality => "choose a communication style for Codex", + SlashCommand::Plan => "switch to Plan mode", + SlashCommand::Goal => "set or view the goal for a long-running task", + SlashCommand::Agents => "view and switch between all active agent sessions", + SlashCommand::MultiAgents => "switch between this session's subagents", + SlashCommand::Side | SlashCommand::Btw => { + "start a side conversation in an ephemeral fork" + } +- SlashCommand::Permissions => "choose what Codex is allowed to do", ++ SlashCommand::Permissions => "choose what DeepSeek Harness is allowed to do", + SlashCommand::Keymap => "remap TUI shortcuts", + SlashCommand::Vim => "toggle Vim mode for the composer", + SlashCommand::ElevateSandbox => "set up elevated agent sandbox", + SlashCommand::SandboxReadRoot => { +@@ -260,7 +260,40 @@ impl SlashCommand { + } + + fn is_visible(self) -> bool { ++ if crate::dshx_backend() ++ && matches!( ++ self, ++ SlashCommand::Ide ++ | SlashCommand::ElevateSandbox ++ | SlashCommand::SandboxReadRoot ++ | SlashCommand::Experimental ++ | SlashCommand::AutoReview ++ | SlashCommand::Memories ++ | SlashCommand::Import ++ | SlashCommand::Hooks ++ | SlashCommand::Review ++ | SlashCommand::Archive ++ | SlashCommand::Delete ++ | SlashCommand::App ++ | SlashCommand::Goal ++ | SlashCommand::MultiAgents ++ | SlashCommand::Side ++ | SlashCommand::Btw ++ | SlashCommand::Cd ++ | SlashCommand::Usage ++ | SlashCommand::DebugConfig ++ | SlashCommand::Ps ++ | SlashCommand::Stop ++ | SlashCommand::Mcp ++ | SlashCommand::Apps ++ | SlashCommand::Plugins ++ | SlashCommand::Logout ++ | SlashCommand::Feedback ++ | SlashCommand::Personality ++ | SlashCommand::MemoryDrop ++ | SlashCommand::MemoryUpdate ++ ) ++ { ++ return false; ++ } + match self { + SlashCommand::SandboxReadRoot => cfg!(target_os = "windows"), + SlashCommand::Copy => !cfg!(target_os = "android"), + SlashCommand::App => cfg!(any(target_os = "macos", target_os = "windows")), + SlashCommand::Rollout | SlashCommand::TestApproval => cfg!(debug_assertions), diff --git a/upstream/patches/codex/0010-dshx-local-ipc-bridge.patch b/upstream/patches/codex/0010-dshx-local-ipc-bridge.patch new file mode 100644 index 00000000..153d959a --- /dev/null +++ b/upstream/patches/codex/0010-dshx-local-ipc-bridge.patch @@ -0,0 +1,187 @@ +diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock +--- a/codex-rs/Cargo.lock ++++ b/codex-rs/Cargo.lock +@@ -4250,16 +4250,18 @@ + [[package]] + name = "codex-stdio-to-uds" + version = "0.0.0" + dependencies = [ + "anyhow", + "codex-uds", + "codex-utils-cargo-bin", ++ "futures", + "pretty_assertions", + "tempfile", + "tokio", ++ "tokio-tungstenite", + ] + + [[package]] + name = "codex-terminal-detection" + version = "0.0.0" + dependencies = [ +diff --git a/codex-rs/stdio-to-uds/Cargo.toml b/codex-rs/stdio-to-uds/Cargo.toml +--- a/codex-rs/stdio-to-uds/Cargo.toml ++++ b/codex-rs/stdio-to-uds/Cargo.toml +@@ -7,6 +7,10 @@ + [[bin]] + name = "codex-stdio-to-uds" + path = "src/main.rs" ++ ++[[bin]] ++name = "dshx-ipc-bridge" ++path = "src/dshx_ipc_bridge.rs" + + [lib] + name = "codex_stdio_to_uds" +@@ -20,12 +24,14 @@ + [dependencies] + anyhow = { workspace = true } + codex-uds = { workspace = true } ++futures = { workspace = true } + tokio = { workspace = true, features = [ + "io-std", + "io-util", + "macros", + "rt-multi-thread", + ] } ++tokio-tungstenite = { workspace = true } + + [dev-dependencies] + codex-utils-cargo-bin = { workspace = true } +diff --git a/codex-rs/stdio-to-uds/src/dshx_ipc_bridge.rs b/codex-rs/stdio-to-uds/src/dshx_ipc_bridge.rs +new file mode 100644 +--- /dev/null ++++ b/codex-rs/stdio-to-uds/src/dshx_ipc_bridge.rs +@@ -0,0 +1,101 @@ ++#![deny(clippy::print_stdout)] ++ ++use std::io::ErrorKind; ++use std::path::PathBuf; ++ ++use anyhow::Context; ++use codex_uds::UnixListener; ++use futures::SinkExt; ++use futures::StreamExt; ++use tokio::io::AsyncBufReadExt; ++use tokio::io::AsyncWriteExt; ++use tokio::io::BufReader; ++use tokio_tungstenite::accept_async; ++use tokio_tungstenite::tungstenite::Message; ++ ++struct SocketGuard(PathBuf); ++ ++impl Drop for SocketGuard { ++ fn drop(&mut self) { ++ if let Err(err) = std::fs::remove_file(&self.0) ++ && err.kind() != ErrorKind::NotFound ++ { ++ // Best effort only. The launcher removes the unique parent directory. ++ } ++ } ++} ++ ++async fn write_line(stdout: &mut tokio::io::Stdout, line: &str) -> anyhow::Result<()> { ++ stdout.write_all(line.as_bytes()).await?; ++ stdout.write_all(b"\n").await?; ++ stdout.flush().await?; ++ Ok(()) ++} ++ ++#[tokio::main] ++async fn main() -> anyhow::Result<()> { ++ let mut args = std::env::args_os().skip(1); ++ let Some(first) = args.next() else { ++ anyhow::bail!("usage: dshx-ipc-bridge "); ++ }; ++ if first == std::ffi::OsStr::new("--check") { ++ return Ok(()); ++ } ++ if args.next().is_some() { ++ anyhow::bail!("usage: dshx-ipc-bridge "); ++ } ++ ++ let socket_path = PathBuf::from(first); ++ if let Some(parent) = socket_path.parent() { ++ codex_uds::prepare_private_socket_directory(parent) ++ .await ++ .with_context(|| format!("failed to prepare {}", parent.display()))?; ++ } ++ let mut listener = UnixListener::bind(&socket_path) ++ .await ++ .with_context(|| format!("failed to bind {}", socket_path.display()))?; ++ let _guard = SocketGuard(socket_path.clone()); ++ ++ // Stdout is a private JSONL control channel to the Node presentation adapter. ++ // Never log to stdout from this process. ++ let mut stdout = tokio::io::stdout(); ++ write_line(&mut stdout, r#"{"dshxBridge":"ready"}"#).await?; ++ ++ let stream = listener ++ .accept() ++ .await ++ .with_context(|| format!("failed to accept {}", socket_path.display()))?; ++ let mut websocket = accept_async(stream) ++ .await ++ .context("failed to upgrade local DSHX IPC connection")?; ++ let mut stdin_lines = BufReader::new(tokio::io::stdin()).lines(); ++ ++ loop { ++ tokio::select! { ++ incoming = websocket.next() => { ++ match incoming { ++ Some(Ok(Message::Text(text))) => write_line(&mut stdout, &text.to_string()).await?, ++ Some(Ok(Message::Binary(bytes))) => { ++ let text = String::from_utf8(bytes.to_vec()).context("binary IPC frame was not UTF-8")?; ++ write_line(&mut stdout, &text).await?; ++ } ++ Some(Ok(Message::Ping(payload))) => websocket.send(Message::Pong(payload)).await?, ++ Some(Ok(Message::Close(_))) | None => break, ++ Some(Ok(_)) => {} ++ Some(Err(err)) => return Err(err).context("local DSHX IPC websocket failed"), ++ } ++ } ++ outbound = stdin_lines.next_line() => { ++ match outbound? { ++ Some(line) => websocket.send(Message::Text(line.into())).await?, ++ None => { ++ websocket.close(None).await?; ++ break; ++ } ++ } ++ } ++ } ++ } ++ ++ Ok(()) ++} +diff --git a/codex-rs/tui/src/main.rs b/codex-rs/tui/src/main.rs +--- a/codex-rs/tui/src/main.rs ++++ b/codex-rs/tui/src/main.rs +@@ -72,19 +72,13 @@ + "DSHX_APP_SERVER_ENDPOINT is required; DSHX never falls back to the Codex agent runtime" + ) + })?; +- let dshx_token = std::env::var("DSHX_APP_SERVER_TOKEN").map_err(|_| { +- anyhow::anyhow!( +- "DSHX_APP_SERVER_TOKEN is required; DSHX local transport is always authenticated" +- ) +- })?; +- let mut dshx_endpoint = resolve_remote_addr(&dshx_endpoint)?; +- match &mut dshx_endpoint { +- RemoteAppServerEndpoint::WebSocket { auth_token, .. } => { +- *auth_token = Some(dshx_token); +- } +- RemoteAppServerEndpoint::UnixSocket { .. } => { +- anyhow::bail!("DSHX production launcher requires authenticated loopback WebSocket transport") +- } ++ let dshx_endpoint = resolve_remote_addr(&dshx_endpoint) ++ .map_err(|err| anyhow::anyhow!("{err:#}"))?; ++ match &dshx_endpoint { ++ RemoteAppServerEndpoint::WebSocket { .. } => anyhow::bail!( ++ "DSHX production launcher forbids TCP WebSocket transport; use the packaged local IPC bridge" ++ ), ++ RemoteAppServerEndpoint::UnixSocket { .. } => {} + } + let exit_info = run_main( + inner, diff --git a/upstream/patches/codex/0011-dshx-ipc-self-check.patch b/upstream/patches/codex/0011-dshx-ipc-self-check.patch new file mode 100644 index 00000000..8fb1079f --- /dev/null +++ b/upstream/patches/codex/0011-dshx-ipc-self-check.patch @@ -0,0 +1,100 @@ +diff --git a/codex-rs/stdio-to-uds/src/dshx_ipc_bridge.rs b/codex-rs/stdio-to-uds/src/dshx_ipc_bridge.rs +--- a/codex-rs/stdio-to-uds/src/dshx_ipc_bridge.rs ++++ b/codex-rs/stdio-to-uds/src/dshx_ipc_bridge.rs +@@ -2,16 +2,21 @@ + + use std::io::ErrorKind; + use std::path::PathBuf; ++use std::time::SystemTime; ++use std::time::UNIX_EPOCH; + + use anyhow::Context; + use codex_uds::UnixListener; ++use codex_uds::UnixStream; + use futures::SinkExt; + use futures::StreamExt; + use tokio::io::AsyncBufReadExt; + use tokio::io::AsyncWriteExt; + use tokio::io::BufReader; + use tokio_tungstenite::accept_async; ++use tokio_tungstenite::client_async; + use tokio_tungstenite::tungstenite::Message; ++use tokio_tungstenite::tungstenite::client::IntoClientRequest; + + struct SocketGuard(PathBuf); + +@@ -32,6 +37,65 @@ + Ok(()) + } + ++struct DirectoryGuard(PathBuf); ++ ++impl Drop for DirectoryGuard { ++ fn drop(&mut self) { ++ let _ = std::fs::remove_dir_all(&self.0); ++ } ++} ++ ++async fn self_check() -> anyhow::Result<()> { ++ let nonce = SystemTime::now() ++ .duration_since(UNIX_EPOCH) ++ .context("system clock is before Unix epoch")? ++ .as_nanos(); ++ let directory = std::env::temp_dir().join(format!( ++ "dshx-ipc-check-{}-{nonce}", ++ std::process::id() ++ )); ++ codex_uds::prepare_private_socket_directory(&directory) ++ .await ++ .with_context(|| format!("failed to prepare {}", directory.display()))?; ++ let _directory_guard = DirectoryGuard(directory.clone()); ++ let socket_path = directory.join("app.sock"); ++ let mut listener = UnixListener::bind(&socket_path) ++ .await ++ .with_context(|| format!("failed to bind {}", socket_path.display()))?; ++ ++ let server = tokio::spawn(async move { ++ let stream = listener.accept().await.context("self-check accept failed")?; ++ let mut websocket = accept_async(stream) ++ .await ++ .context("self-check server websocket handshake failed")?; ++ match websocket.next().await { ++ Some(Ok(Message::Text(text))) if text.as_str() == "ping" => { ++ websocket.send(Message::Text("pong".into())).await?; ++ } ++ other => anyhow::bail!("self-check server received unexpected frame: {other:?}"), ++ } ++ websocket.close(None).await?; ++ anyhow::Ok(()) ++ }); ++ ++ let stream = UnixStream::connect(&socket_path) ++ .await ++ .with_context(|| format!("failed to connect {}", socket_path.display()))?; ++ let request = "ws://localhost/rpc" ++ .into_client_request() ++ .context("failed to create self-check websocket request")?; ++ let (mut websocket, _) = client_async(request, stream) ++ .await ++ .context("self-check client websocket handshake failed")?; ++ websocket.send(Message::Text("ping".into())).await?; ++ match websocket.next().await { ++ Some(Ok(Message::Text(text))) if text.as_str() == "pong" => {} ++ other => anyhow::bail!("self-check client received unexpected frame: {other:?}"), ++ } ++ server.await.context("self-check server task failed")??; ++ Ok(()) ++} ++ + #[tokio::main] + async fn main() -> anyhow::Result<()> { + let mut args = std::env::args_os().skip(1); +@@ -39,7 +103,7 @@ + anyhow::bail!("usage: dshx-ipc-bridge "); + }; + if first == std::ffi::OsStr::new("--check") { +- return Ok(()); ++ return self_check().await; + } + if args.next().is_some() { + anyhow::bail!("usage: dshx-ipc-bridge "); diff --git a/upstream/patches/codex/0012-dshx-rc5-presentation-fixes.patch b/upstream/patches/codex/0012-dshx-rc5-presentation-fixes.patch new file mode 100644 index 00000000..e03017f7 --- /dev/null +++ b/upstream/patches/codex/0012-dshx-rc5-presentation-fixes.patch @@ -0,0 +1,144 @@ +diff --git a/codex-rs/tui/src/model_catalog.rs b/codex-rs/tui/src/model_catalog.rs +--- a/codex-rs/tui/src/model_catalog.rs ++++ b/codex-rs/tui/src/model_catalog.rs +@@ -12,4 +12,10 @@ impl ModelCatalog { + pub(crate) fn try_list_models(&self) -> Result, Infallible> { + Ok(self.models.clone()) + } ++ ++ pub(crate) fn display_name_for(&self, model: &str) -> Option<&str> { ++ self.models ++ .iter() ++ .find(|preset| preset.model == model) ++ .map(|preset| preset.display_name.as_str()) ++ } + } +diff --git a/codex-rs/tui/src/chatwidget/settings.rs b/codex-rs/tui/src/chatwidget/settings.rs +--- a/codex-rs/tui/src/chatwidget/settings.rs ++++ b/codex-rs/tui/src/chatwidget/settings.rs +@@ -456,6 +456,10 @@ impl ChatWidget { + pub(super) fn refresh_model_display(&mut self) { + let effective = self.effective_collaboration_mode(); +- self.session_header.set_model(effective.model()); ++ let model_display_name = self ++ .model_catalog ++ .display_name_for(effective.model()) ++ .unwrap_or(effective.model()).to_string(); ++ self.session_header.set_model(&model_display_name); + // Keep composer paste affordances aligned with the currently effective model. + self.sync_image_paste_enabled(); + self.sync_service_tier_commands(); +@@ -569,6 +573,8 @@ impl ChatWidget { + if model.is_empty() { + DEFAULT_MODEL_DISPLAY_NAME + } else { +- model ++ self.model_catalog ++ .display_name_for(model) ++ .unwrap_or(model) + } + } +diff --git a/codex-rs/tui/src/chatwidget/session_flow.rs b/codex-rs/tui/src/chatwidget/session_flow.rs +--- a/codex-rs/tui/src/chatwidget/session_flow.rs ++++ b/codex-rs/tui/src/chatwidget/session_flow.rs +@@ -119,8 +119,12 @@ impl ChatWidget { + self.sync_personality_command_enabled(); + self.sync_plugins_command_enabled(); + self.sync_goal_command_enabled(); + self.refresh_plugin_mentions(); +- let model_for_header = self.current_model().to_string(); ++ let model_for_header = if crate::dshx_backend() { ++ self.model_display_name().to_string() ++ } else { ++ self.current_model().to_string() ++ }; + if matches!( + display, + SessionConfiguredDisplay::Normal | SessionConfiguredDisplay::PromptEdit +diff --git a/codex-rs/tui/src/history_cell/session.rs b/codex-rs/tui/src/history_cell/session.rs +--- a/codex-rs/tui/src/history_cell/session.rs ++++ b/codex-rs/tui/src/history_cell/session.rs +@@ -126,13 +126,23 @@ pub(crate) fn new_session_info( + auth_plan: Option, + show_fast_status: bool, + ) -> SessionInfoCell { ++ let header_model = if crate::dshx_backend() { ++ requested_model.to_string() ++ } else { ++ session.model.clone() ++ }; ++ let header_version = if crate::dshx_backend() { ++ option_env!("DSHX_VERSION").unwrap_or(CODEX_CLI_VERSION) ++ } else { ++ CODEX_CLI_VERSION ++ }; + // Header box rendered as history (so it appears at the very top) + let header = SessionHeaderHistoryCell::new( +- session.model.clone(), ++ header_model, + session.reasoning_effort.clone(), + show_fast_status, + config.cwd.to_path_buf(), +- CODEX_CLI_VERSION, ++ header_version, + ) + .with_yolo_mode(has_yolo_permissions( + session.approval_policy, +@@ -175,8 +185,8 @@ pub(crate) fn new_session_info( + .map(|tip| TooltipHistoryCell::new(tip, &config.cwd)) + { + parts.push(Box::new(tooltips)); + } +- if requested_model != session.model.as_str() { ++ if !crate::dshx_backend() && requested_model != session.model.as_str() { + let lines = vec![ + "model changed:".magenta().bold().into(), + format!("requested: {requested_model}").into(), +diff --git a/codex-rs/tui/src/chatwidget/status_surfaces.rs b/codex-rs/tui/src/chatwidget/status_surfaces.rs +--- a/codex-rs/tui/src/chatwidget/status_surfaces.rs ++++ b/codex-rs/tui/src/chatwidget/status_surfaces.rs +@@ -681,11 +681,18 @@ impl ChatWidget { + StatusLineItem::WeeklyLimit => { + let (window, is_secondary) = self + .rate_limit_snapshots_by_limit_id + .get("codex") + .and_then(weekly_status_window)?; + let label = limit_label_for_window(window.window_minutes, is_secondary); + self.status_line_limit_display(Some(window), &label) + } +- StatusLineItem::CodexVersion => Some(CODEX_CLI_VERSION.to_string()), ++ StatusLineItem::CodexVersion => Some( ++ if crate::dshx_backend() { ++ option_env!("DSHX_VERSION").unwrap_or(CODEX_CLI_VERSION) ++ } else { ++ CODEX_CLI_VERSION ++ } ++ .to_string(), ++ ), + StatusLineItem::ContextWindowSize => self + .status_line_context_window_size() + .map(|cws| format!("{} window", format_tokens_compact(cws))), +diff --git a/codex-rs/tui/src/startup_draft.rs b/codex-rs/tui/src/startup_draft.rs +--- a/codex-rs/tui/src/startup_draft.rs ++++ b/codex-rs/tui/src/startup_draft.rs +@@ -447,6 +447,12 @@ fn startup_session_header(config: Option<&Config>) -> Box { + || PathBuf::from("loading"), + |config| config.cwd.to_path_buf(), + ); ++ let header_version = if crate::dshx_backend() { ++ option_env!("DSHX_VERSION").unwrap_or(CODEX_CLI_VERSION) ++ } else { ++ CODEX_CLI_VERSION ++ }; + Box::new( + history_cell::SessionHeaderHistoryCell::new_with_style( + "loading".to_string(), +@@ -456,7 +462,7 @@ fn startup_session_header(config: Option<&Config>) -> Box { + /*reasoning_effort*/ None, + /*show_fast_status*/ false, + directory, +- CODEX_CLI_VERSION, ++ header_version, + ) + .with_yolo_mode(config.is_some_and(history_cell::is_yolo_mode)), + )