From 9aea1189bd03d15fd5de7b420093125a6b2bf323 Mon Sep 17 00:00:00 2001 From: Michal Date: Sat, 8 Aug 2026 21:48:14 +0100 Subject: [PATCH] =?UTF-8?q?feat(opencode):=20native=20opencode=20addon=20?= =?UTF-8?q?=E2=80=94=20bottom-row=20project=20indicator=20+=20in-TUI=20swi?= =?UTF-8?q?tcher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an opencode integration mirroring the existing pi and prime-agent addons. The addon is a TUI plugin (`src/opencode-ext/open-mcpctl.tsx`) that: - renders the active mcpctl project into opencode's `app_bottom` slot — the status row that also carries the token counter / model info, - registers a `mcpctl.switch_project` command bound to `m` that opens a searchable `DialogSelect` project picker, - switches live by re-pointing opencode's `mcpctl` MCP server at the new project's mcplocal endpoint via the SDK client (native MCP, no JSON-RPC code), - persists state in `~/.mcpctl/opencode-state.json` (+ `.mcpctl-project` marker). Pure helpers (`filterProjects`, `toolChangeAnnouncement`) live in `projects.ts` so they are unit-tested. `mcpctl config opencode --project X` provisions it: writes the plugin into `~/.config/opencode/plugin/`, registers the `.tsx` in opencode.json's plugin array (auto-discovery only matches {ts,js}), best-effort installs the TUI peer deps (`@opentui/*`), and syncs skills into `~/.config/opencode/skill`. `mcpctl skills sync --agent opencode` is added as a fourth shared-tree target. Closes typecheck gap with `typecheck:opencode-ext` against the real `@opencode-ai/plugin` + `@opentui/solid` types. Tests: embed freshness, opencode-settings, projects helpers, skills agent root. Full `pnpm run typecheck` + cli tests pass; completions regenerated. --- README.md | 25 + completions/mcpctl.bash | 5 +- completions/mcpctl.fish | 12 +- docs/opencode-extension.md | 134 +++ eslint.config.js | 2 +- package.json | 9 +- pnpm-lock.yaml | 952 +++++++++++++++++- scripts/generate-opencode-extension.ts | 50 + src/cli/src/commands/config.ts | 119 +++ src/cli/src/commands/skills.ts | 32 +- src/cli/src/config/opencode-extension.ts | 16 + src/cli/src/utils/opencode-settings.ts | 194 ++++ .../config/opencode-extension-embed.test.ts | 43 + src/cli/tests/utils/opencode-projects.test.ts | 74 ++ src/cli/tests/utils/opencode-settings.test.ts | 110 ++ src/cli/tests/utils/pi-settings.test.ts | 3 + src/opencode-ext/open-mcpctl.tsx | 249 +++++ src/opencode-ext/projects.ts | 54 + src/opencode-ext/tsconfig.json | 30 + 19 files changed, 2094 insertions(+), 19 deletions(-) create mode 100644 docs/opencode-extension.md create mode 100644 scripts/generate-opencode-extension.ts create mode 100644 src/cli/src/config/opencode-extension.ts create mode 100644 src/cli/src/utils/opencode-settings.ts create mode 100644 src/cli/tests/config/opencode-extension-embed.test.ts create mode 100644 src/cli/tests/utils/opencode-projects.test.ts create mode 100644 src/cli/tests/utils/opencode-settings.test.ts create mode 100644 src/opencode-ext/open-mcpctl.tsx create mode 100644 src/opencode-ext/projects.ts create mode 100644 src/opencode-ext/tsconfig.json diff --git a/README.md b/README.md index 3f7f839..76f0bd0 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,31 @@ Inside pi: See [docs/pi-extension.md](docs/pi-extension.md) for full details. +### Connect opencode + +[opencode](https://opencode.ai) supports MCP natively, so unlike pi it doesn't +need a reimplemented client. The mcpctl opencode addon is a **TUI plugin** that +shows the active mcpctl project in opencode's bottom status row (the same row as +the token counter) and lets you **switch projects from inside the interface** +with a searchable picker. + +```bash +mcpctl config opencode --project monitoring +``` + +This writes the plugin into `~/.config/opencode/plugin/`, registers it in +`opencode.json`, provisions the plugin deps, persists the active project in +`~/.mcpctl/opencode-state.json`, and runs an initial skills sync into +`~/.config/opencode/skill`. Restart opencode and: + +- the active project appears in the bottom status row (beside the token counter), +- `m` (or the `mcpctl.switch_project` command) opens a project picker — + picking one re-points opencode's `mcpctl` MCP server at the new project, so + its tools live-switch. +- re-sync with `mcpctl skills sync --agent opencode --project monitoring` + +See [docs/opencode-extension.md](docs/opencode-extension.md) for full details. + ## Declarative Configuration Everything can be defined in YAML and applied with `mcpctl apply`: diff --git a/completions/mcpctl.bash b/completions/mcpctl.bash index 8dbfce5..9bb1f45 100644 --- a/completions/mcpctl.bash +++ b/completions/mcpctl.bash @@ -103,7 +103,7 @@ _mcpctl() { config) local config_sub=$(_mcpctl_get_subcmd $subcmd_pos) if [[ -z "$config_sub" ]]; then - COMPREPLY=($(compgen -W "view set path reset claude claude-generate pi prime-agent prime-agent-generate setup impersonate help" -- "$cur")) + COMPREPLY=($(compgen -W "view set path reset claude claude-generate pi prime-agent prime-agent-generate opencode setup impersonate help" -- "$cur")) else case "$config_sub" in view) @@ -133,6 +133,9 @@ _mcpctl() { prime-agent-generate) COMPREPLY=($(compgen -W "-p --project -o --output --gateway-url --token --skip-skills --skip-extension --skip-marker --dry-run -h --help" -- "$cur")) ;; + opencode) + COMPREPLY=($(compgen -W "-p --project --extension-dir --skip-deps --skip-skills --opencode-dir -h --help" -- "$cur")) + ;; setup) COMPREPLY=($(compgen -W "-h --help" -- "$cur")) ;; diff --git a/completions/mcpctl.fish b/completions/mcpctl.fish index b4b8da1..6adb7ed 100644 --- a/completions/mcpctl.fish +++ b/completions/mcpctl.fish @@ -267,7 +267,7 @@ complete -c mcpctl -n "__fish_seen_subcommand_from approve; and __mcpctl_needs_r complete -c mcpctl -n "__fish_seen_subcommand_from get describe delete edit patch approve; and not __mcpctl_needs_resource_type" -a '(__mcpctl_resource_names)' -d 'Resource name' # config subcommands -set -l config_cmds view set path reset claude claude-generate pi prime-agent prime-agent-generate setup impersonate +set -l config_cmds view set path reset claude claude-generate pi prime-agent prime-agent-generate opencode setup impersonate complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a view -d 'Show current configuration' complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a set -d 'Set a configuration value' complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a path -d 'Show configuration file path' @@ -277,6 +277,7 @@ complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_s complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a pi -d 'Install the pi extension + sync skills (native, no MCP client, no Claude)' complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a prime-agent -d 'Register mcpctl proxy MCP + auth + skills + /mcpctl switcher for prime-agent (~/.prime/agent)' complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a prime-agent-generate -d '' +complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a opencode -d 'Install the opencode plugin (bottom-row project indicator + switcher, sync skills)' complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a setup -d 'Interactive LLM provider setup wizard' complete -c mcpctl -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from $config_cmds" -a impersonate -d 'Impersonate another user or return to original identity' @@ -324,6 +325,13 @@ complete -c mcpctl -n "__mcpctl_subcmd_active config prime-agent-generate" -l sk complete -c mcpctl -n "__mcpctl_subcmd_active config prime-agent-generate" -l skip-marker -d 'Do not write a .mcpctl-project marker in the current directory' complete -c mcpctl -n "__mcpctl_subcmd_active config prime-agent-generate" -l dry-run -d 'Print what would change without writing or syncing' +# config opencode options +complete -c mcpctl -n "__mcpctl_subcmd_active config opencode" -s p -l project -d 'Project name to make active' -xa '(__mcpctl_project_names)' +complete -c mcpctl -n "__mcpctl_subcmd_active config opencode" -l extension-dir -d 'Source-tree src/opencode-ext dir (dev only; default: embedded sources)' -x +complete -c mcpctl -n "__mcpctl_subcmd_active config opencode" -l skip-deps -d 'Skip provisioning opencode plugin deps into ~/.config/opencode' +complete -c mcpctl -n "__mcpctl_subcmd_active config opencode" -l skip-skills -d 'Skip the initial skills sync' +complete -c mcpctl -n "__mcpctl_subcmd_active config opencode" -l opencode-dir -d 'Override the opencode config dir (default: ~/.config/opencode)' -x + # config impersonate options complete -c mcpctl -n "__mcpctl_subcmd_active config impersonate" -l quit -d 'Stop impersonating and return to original identity' @@ -529,7 +537,7 @@ complete -c mcpctl -n "__fish_seen_subcommand_from skills; and not __fish_seen_s # skills sync options complete -c mcpctl -n "__mcpctl_subcmd_active skills sync" -s p -l project -d 'Project to sync (overrides .mcpctl-project marker)' -xa '(__mcpctl_project_names)' -complete -c mcpctl -n "__mcpctl_subcmd_active skills sync" -l agent -d 'Sync target: claude (default), prime-agent, or pi' -x +complete -c mcpctl -n "__mcpctl_subcmd_active skills sync" -l agent -d 'Sync target: claude (default), prime-agent, pi, or opencode' -x complete -c mcpctl -n "__mcpctl_subcmd_active skills sync" -l dry-run -d 'Print what would change without writing anything' complete -c mcpctl -n "__mcpctl_subcmd_active skills sync" -l force -d 'Overwrite locally-modified skills' complete -c mcpctl -n "__mcpctl_subcmd_active skills sync" -l quiet -d 'Suppress all output unless something changed (used by session-start hooks)' diff --git a/docs/opencode-extension.md b/docs/opencode-extension.md new file mode 100644 index 0000000..0780459 --- /dev/null +++ b/docs/opencode-extension.md @@ -0,0 +1,134 @@ +# mcpctl × opencode — native integration (TUI plugin) + +## Motivation + +With Claude Code, `mcpctl config claude --project X` wires a project's MCP +servers into the agent using `.mcp.json`, and with pi/prime-agent it ships a +dedicated extension. This addon does the same for +[opencode](https://opencode.ai): + +1. a **TUI plugin** (`open-mcpctl.tsx`) that shows the active mcpctl project in + opencode's bottom status row (the row that also carries the token counter / + model / mode info) and lets you **switch projects from inside the interface** + with a searchable picker, and +2. `mcpctl config opencode` which provisions it: writes the plugin into + `~/.config/opencode/plugin/`, registers it in `opencode.json`, provisions the + plugin deps, and persists the active project. + +Where pi had to *reimplement* an MCP client (pi has no MCP), opencode has +first-class MCP support, so the plugin leans on opencode's own MCP machinery: +switching a project re-points opencode's `mcpctl` MCP server at the new +project's mcplocal endpoint (`{mcplocalUrl}/projects//mcp`) via the +SDK client, and opencode enumerates those tools natively. No JSON-RPC code in +the plugin. + +## How it works + +### The indicator (bottom status row) + +The plugin registers a slot into `api.slots.register({ slots: { app_bottom } })`. +`app_bottom` is the row opencode renders directly beneath the session footer +statusline — i.e. beside the token counter / model / mode readout. It renders a +compact `mcpctl ` segment in the theme's accent/text colours. + +### Switching projects + +`m` (or the `mcpctl.switch_project` command in the command palette) +opens a `DialogSelect` listing projects from mcpd with search-as-you-type +filtering (active project first, then alphabetical). Picking one: + +1. `client.mcp.disconnect({ name: "mcpctl" })` — drop the previous project's + tools, +2. `client.mcp.add({ name: "mcpctl", config: { type: "remote", url, headers } })` + + `client.mcp.connect(...)` — point at the new project's mcplocal endpoint, +3. persists the choice to `~/.mcpctl/opencode-state.json`, +4. updates the bottom-row indicator and fires a toast. + +Because the tools travel over opencode's native MCP, they pick up +`begin_session` gating exactly like any other mcpctl client. If a project is +gated, opencode only exposes its `begin_session` tool until it is called. + +### State + +The active project lives in `~/.mcpctl/opencode-state.json` (immune to your +shell's cwd) and is also inferred from a `.mcpctl-project` marker walk-up, +mirroring the pi extension. + +## Deliverables + +| Artifact | Purpose | +|----------|---------| +| `src/opencode-ext/open-mcpctl.tsx` | Self-contained opencode TUI plugin (the addon) | +| `mcpctl config opencode` | CLI wiring: installs the plugin, registers it, provisions deps + skills | +| `docs/opencode-extension.md` | This document | + +## Using it + +```bash +mcpctl config opencode --project +``` + +This: +1. writes `~/.config/opencode/plugin/open-mcpctl.tsx`, +2. registers that `.tsx` in `opencode.json` under `plugin` (opencode's plugin + auto-discovery only matches `{ts,js}`, so the explicit entry is required for + a JSX plugin), +3. best-effort installs `@opencode-ai/plugin` + `@opentui/*` into the config + dir's `node_modules` (opencode provisions the former but not the TUI peers + a TUI plugin needs), and +4. syncs the project's skills into `~/.config/opencode/skill`. + +Restart opencode (or start a new session) — the active project and its MCP +tools appear in the bottom status row. Switch with `m`. + +Re-sync skills later: + +```bash +mcpctl skills sync --agent opencode --project +``` + +## Layout + +``` +src/opencode-ext/ + open-mcpctl.tsx # the plugin (JSX, self-contained) + tsconfig.json # typechecks against real @opencode-ai/plugin types +``` + +The plugin imports only opencode-bundled packages +(`@opencode-ai/plugin/tui`, `@opentui/solid`, `solid-js`) and the node standard +library — no `@mcpctl/*`, no `~/.claude`. + +## Typechecking + +The plugin ships as *source*: it is embedded into the CLI +(`src/cli/src/config/opencode-extension.ts`, generated by +`scripts/generate-opencode-extension.ts`) and written verbatim into +`~/.config/opencode/plugin/`. The CLI's own build never compiles it, so without +a dedicated project nothing would check it against opencode's API — exactly how +a slotted component with the wrong shape would ship. + +`src/opencode-ext/tsconfig.json` closes that gap, checking against the **real** +published `@opencode-ai/plugin` + `@opentui/solid` types (dev dependencies): + +```bash +pnpm run typecheck # includes typecheck:opencode-ext +``` + +After editing `src/opencode-ext/*.tsx`, regenerate the embedded copy or the CLI +will keep installing the old sources: + +```bash +npx tsx scripts/generate-opencode-extension.ts +``` + +A test (`tests/config/opencode-extension-embed.test.ts`) fails if you forget. + +## Authentication caveat + +The plugin sends whatever bearer it finds in `~/.mcpctl/credentials`. A locally +running `mcpctl-local` daemon does not authenticate `/projects/*`, so this +works — the header is simply ignored. `mcplocal serve` registers a token-auth +preHandler that accepts **only** `mcpctl_pat_` bearers (project mcptokens), so +pointing it at an authenticated `mcplocal serve` needs a project mcptoken +(`mcpctl create mcptoken --project `). diff --git a/eslint.config.js b/eslint.config.js index 53f1093..d7086d2 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,7 +3,7 @@ import tsparser from '@typescript-eslint/parser'; export default [ { - files: ['src/*/src/**/*.ts', 'src/pi-ext/*.ts'], + files: ['src/*/src/**/*.ts', 'src/pi-ext/*.ts', 'src/opencode-ext/*.ts', 'src/opencode-ext/*.tsx'], languageOptions: { parser: tsparser, parserOptions: { diff --git a/package.json b/package.json index de493e0..dd05cb3 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "clean": "pnpm -r run clean && rimraf node_modules", "db:up": "docker compose -f deploy/docker-compose.yml up -d", "db:down": "docker compose -f deploy/docker-compose.yml down", - "typecheck": "tsc --build && pnpm run typecheck:pi-ext", + "typecheck": "tsc --build && pnpm run typecheck:pi-ext && pnpm run typecheck:opencode-ext", "completions:generate": "tsx scripts/generate-completions.ts --write", "completions:check": "tsx scripts/generate-completions.ts --check", "rpm:build": "bash scripts/build-rpm.sh", @@ -34,6 +34,7 @@ "mcpd:deploy-dry": "bash deploy.sh --dry-run", "mcpd:logs": "bash logs.sh", "typecheck:pi-ext": "tsc -p src/pi-ext/tsconfig.json", + "typecheck:opencode-ext": "tsc -p src/opencode-ext/tsconfig.json", "smoke:clean": "tsx scripts/clean-smoke-resources.ts" }, "engines": { @@ -44,6 +45,11 @@ "devDependencies": { "@earendil-works/pi-ai": "^0.84.1", "@earendil-works/pi-coding-agent": "0.84.1", + "@opencode-ai/plugin": "1.18.15", + "@opencode-ai/sdk": "1.18.15", + "@opentui/core": "0.4.5", + "@opentui/keymap": "0.4.5", + "@opentui/solid": "0.4.5", "@types/node": "^25.3.0", "@typescript-eslint/eslint-plugin": "^8.56.0", "@typescript-eslint/parser": "^8.56.0", @@ -51,6 +57,7 @@ "eslint": "^10.0.1", "eslint-config-prettier": "^10.1.8", "rimraf": "^6.1.3", + "solid-js": "^1.9.14", "tsx": "^4.21.0", "typebox": "1.3.11", "typescript": "^5.9.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a6c99d..f6409c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,21 @@ importers: '@earendil-works/pi-coding-agent': specifier: 0.84.1 version: 0.84.1(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76) + '@opencode-ai/plugin': + specifier: 1.18.15 + version: 1.18.15(@opentui/core@0.4.5(typescript@5.9.3)(web-tree-sitter@0.25.10))(@opentui/keymap@0.4.5(@opentui/solid@0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10))(react@19.2.5)(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10))(@opentui/solid@0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10)) + '@opencode-ai/sdk': + specifier: 1.18.15 + version: 1.18.15 + '@opentui/core': + specifier: 0.4.5 + version: 0.4.5(typescript@5.9.3)(web-tree-sitter@0.25.10) + '@opentui/keymap': + specifier: 0.4.5 + version: 0.4.5(@opentui/solid@0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10))(react@19.2.5)(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10) + '@opentui/solid': + specifier: 0.4.5 + version: 0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10) '@types/node': specifier: ^25.3.0 version: 25.3.0 @@ -35,6 +50,9 @@ importers: rimraf: specifier: ^6.1.3 version: 6.1.3 + solid-js: + specifier: ^1.9.14 + version: 1.9.14 tsx: specifier: ^4.21.0 version: 4.21.0 @@ -184,7 +202,7 @@ importers: version: link:../shared '@modelcontextprotocol/sdk': specifier: ^1.0.0 - version: 1.26.0(zod@3.25.76) + version: 1.26.0(zod@4.1.8) fastify: specifier: ^5.0.0 version: 5.7.4 @@ -280,10 +298,18 @@ packages: '@adobe/css-tools@4.4.4': resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + '@ai-sdk/provider@3.0.8': + resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} + engines: {node: '>=18'} + '@alcalzone/ansi-tokenize@0.2.5': resolution: {integrity: sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==} engines: {node: '>=18'} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + '@anthropic-ai/sdk@0.91.1': resolution: {integrity: sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==} hasBin: true @@ -408,10 +434,18 @@ packages: resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.0': resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} engines: {node: '>=6.9.0'} + '@babel/core@7.28.0': + resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} + engines: {node: '>=6.9.0'} + '@babel/core@7.29.0': resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} engines: {node: '>=6.9.0'} @@ -420,36 +454,98 @@ packages: resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.29.7': + resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==} + engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.28.6': resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} engines: {node: '>=6.9.0'} + '@babel/helper-create-class-features-plugin@7.29.7': + resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-globals@7.28.0': resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.29.7': + resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.18.6': + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.28.6': resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.28.6': resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.29.7': + resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==} + engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.28.6': resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.29.7': + resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -463,6 +559,29 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.29.7': + resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.29.7': + resolution: {integrity: sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.27.1': resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} @@ -475,6 +594,18 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-typescript@7.29.7': + resolution: {integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.27.1': + resolution: {integrity: sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} @@ -483,14 +614,26 @@ packages: resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.29.0': resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + '@balena/dockerignore@1.0.2': resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} @@ -1276,6 +1419,36 @@ packages: 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 + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': + resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': + resolution: {integrity: sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': + resolution: {integrity: sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': + resolution: {integrity: sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': + resolution: {integrity: sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': + resolution: {integrity: sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==} + cpu: [x64] + os: [win32] + '@next/env@16.2.5': resolution: {integrity: sha512-Lb9ElHD2klcyeVD25vW+siPFqz9QMzDUSgvFZNO+dZEKoMHex4viJhVuzBhrXKqb+UKnih7mVYbt50/7KLsSCA==} @@ -1385,6 +1558,23 @@ packages: resolution: {integrity: sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==} engines: {node: ^20.17.0 || >=22.9.0} + '@opencode-ai/plugin@1.18.15': + resolution: {integrity: sha512-AY10RtFbzLkf951dbLkSGJdBeddeS6ojbjvqCpWnINedqlj2cK/GF91xWjWnlHpqQZ4GABLPCuoSJx8mGmKvCw==} + peerDependencies: + '@opentui/core': '>=0.4.5' + '@opentui/keymap': '>=0.4.5' + '@opentui/solid': '>=0.4.5' + peerDependenciesMeta: + '@opentui/core': + optional: true + '@opentui/keymap': + optional: true + '@opentui/solid': + optional: true + + '@opencode-ai/sdk@1.18.15': + resolution: {integrity: sha512-8sfo9nGiVwesAZW9Wqkvynyn7w4wYaHx1O9qOHpYL65+Bs2XUpHP3kBbZe58gjQydFgk6I74kUF7sqCiPu2arQ==} + '@opentelemetry/api-logs@0.220.0': resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==} engines: {node: '>=8.0.0'} @@ -1491,6 +1681,73 @@ packages: resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} engines: {node: '>=14'} + '@opentui/core-darwin-arm64@0.4.5': + resolution: {integrity: sha512-8KUG0oRidnR+oW1RSZJ72/PhZLl+qRRMk5U/mieF4c0SJ5V3tYACpBZAKzQfHNd1f7QzD8FHZct1lPpQgtmkWg==} + cpu: [arm64] + os: [darwin] + + '@opentui/core-darwin-x64@0.4.5': + resolution: {integrity: sha512-R2bocsg55gwjOqCp/MWFgFYzRmsduKegB6nzgFAPCvAD/L5Jf30xpWJWFlSg3x8vxe1L9WJ84dfqa4M7mZZ3wA==} + cpu: [x64] + os: [darwin] + + '@opentui/core-linux-arm64-musl@0.4.5': + resolution: {integrity: sha512-ieqdyKI6EIYPalYAETB2wsdP83hr5Ifi+dFnBFUmdEEFHsoKwBmn2S7bsTOYlX7Bg03F4/YPIg+IvRpeC+cUJw==} + cpu: [arm64] + os: [linux] + + '@opentui/core-linux-arm64@0.4.5': + resolution: {integrity: sha512-R4MZ25a4CzOAGVjW9aj1hUfzQGVfCJwrwBDbNs2SXaIvzcZqkxCVtU4FoQ5LsaD0j/BdNQVg2CIfFkFsm1fDuQ==} + cpu: [arm64] + os: [linux] + + '@opentui/core-linux-x64-musl@0.4.5': + resolution: {integrity: sha512-mKVKcIcPiSVVZZsdPSBoWwoa2/TCeQAaMDeHF7PFw2kt5bTXZPP7xxWfRQLCNIcA1eaGl59UuwUWHDR2Ve548Q==} + cpu: [x64] + os: [linux] + + '@opentui/core-linux-x64@0.4.5': + resolution: {integrity: sha512-SNyuQoxMKI1vuJhgxSSW96adWM6LqFl2SoS3GM4tGeneGOanVVG2Y06PvlytXvF4cKik97t0rqkVMRetmOs93w==} + cpu: [x64] + os: [linux] + + '@opentui/core-win32-arm64@0.4.5': + resolution: {integrity: sha512-GHTTsqeR45q2Iek9Rb7ty+x/hAKn2jZ1ujlCgPR8LBKyF7h0E1dNFryoZ7ehMc3kJndP1sKn836IemKFqxuDdQ==} + cpu: [arm64] + os: [win32] + + '@opentui/core-win32-x64@0.4.5': + resolution: {integrity: sha512-Y8T/yXCDGagRGiQrtmuB6AhRcPucKFs/Dre3v8kJwNYqDccI4FzUPKclZ7djfmRZNjl7JUqPhZZP/PwDpQocMg==} + cpu: [x64] + os: [win32] + + '@opentui/core@0.4.5': + resolution: {integrity: sha512-JsgRTPkA6e+Vxmumxai6SElOSlRQkbzNKHlCfemlArRiLhfC1IZ9RXJo2QH4xSu+uBOWAM90uss73/pPlkdEig==} + peerDependencies: + web-tree-sitter: 0.25.10 + + '@opentui/keymap@0.4.5': + resolution: {integrity: sha512-S1wzKHhF70zT6bH+VBFY+lSeTImLcIFW28JNQiME8MoPcy6KGPs7rKFSHrb/U7P8rsTJeRfW5A4d1Cy6PKodDg==} + peerDependencies: + '@opentui/react': 0.4.5 + '@opentui/solid': 0.4.5 + react: '>=19.2.0' + solid-js: 1.9.12 + peerDependenciesMeta: + '@opentui/react': + optional: true + '@opentui/solid': + optional: true + react: + optional: true + solid-js: + optional: true + + '@opentui/solid@0.4.5': + resolution: {integrity: sha512-B0RSkXnrtPVfEJOX+Hj+axjLJ3lzbG1BZw5I7Pvb9OPp48Vzg2cW2a3cSa86/q48ndLt647i/XwFPIw/jqnI5g==} + peerDependencies: + solid-js: 1.9.12 + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -2198,6 +2455,23 @@ packages: react-native-b4a: optional: true + babel-plugin-jsx-dom-expressions@0.40.7: + resolution: {integrity: sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ==} + peerDependencies: + '@babel/core': ^7.20.12 + + babel-plugin-module-resolver@5.0.2: + resolution: {integrity: sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg==} + + babel-preset-solid@1.9.12: + resolution: {integrity: sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==} + peerDependencies: + '@babel/core': ^7.0.0 + solid-js: ^1.9.12 + peerDependenciesMeta: + solid-js: + optional: true + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2313,6 +2587,11 @@ packages: resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} engines: {node: '>=10.0.0'} + bun-ffi-structs@0.2.4: + resolution: {integrity: sha512-AJzsqoVFs1KBbJbWHIYrVZLDC3NhTqqh25awRXqzoLzmBAKr5oqk6+CwuYHAekKx+VBCYVohBoKuRq40dV+TYg==} + peerDependencies: + typescript: ^5 + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -2583,6 +2862,10 @@ packages: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} + diff@9.0.0: + resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} + engines: {node: '>=0.3.1'} + docker-modem@5.0.6: resolution: {integrity: sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==} engines: {node: '>= 8.0'} @@ -2617,6 +2900,9 @@ packages: effect@3.18.4: resolution: {integrity: sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==} + effect@4.0.0-beta.83: + resolution: {integrity: sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w==} + electron-to-chromium@1.5.344: resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==} @@ -2641,6 +2927,14 @@ packages: resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==} engines: {node: '>=10.13.0'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + entities@8.0.0: resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} engines: {node: '>=20.19.0'} @@ -2795,6 +3089,10 @@ packages: resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} engines: {node: '>=8.0.0'} + fast-check@4.9.0: + resolution: {integrity: sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==} + engines: {node: '>=12.17.0'} + fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} @@ -2853,10 +3151,20 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} + find-babel-config@2.1.2: + resolution: {integrity: sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg==} + + find-my-way-ts@0.1.6: + resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} + find-my-way@9.4.0: resolution: {integrity: sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w==} engines: {node: '>=20'} + find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2981,6 +3289,11 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + glob@9.3.5: + resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} + engines: {node: '>=16 || 14 >=14.17'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + google-auth-library@10.9.1: resolution: {integrity: sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==} engines: {node: '>=18'} @@ -3022,6 +3335,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + helmet@7.2.0: resolution: {integrity: sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==} engines: {node: '>=16.0.0'} @@ -3049,6 +3366,9 @@ packages: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-entities@2.3.3: + resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -3125,6 +3445,10 @@ packages: resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} engines: {node: ^20.17.0 || >=22.9.0} + ini@7.0.0: + resolution: {integrity: sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + ink@6.8.0: resolution: {integrity: sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA==} engines: {node: '>=20'} @@ -3159,6 +3483,10 @@ packages: resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} engines: {node: '>= 10'} + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3287,6 +3615,9 @@ packages: json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -3322,6 +3653,9 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kubernetes-types@1.30.0: + resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -3399,6 +3733,10 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -3451,6 +3789,11 @@ packages: engines: {node: '>= 18'} hasBin: true + marked@17.0.1: + resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==} + engines: {node: '>= 20'} + hasBin: true + marked@18.0.5: resolution: {integrity: sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==} engines: {node: '>= 20'} @@ -3514,6 +3857,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@8.0.7: + resolution: {integrity: sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==} + engines: {node: '>=16 || 14 >=14.17'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -3545,6 +3892,10 @@ packages: resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} engines: {node: '>=8'} + minipass@4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} + minipass@5.0.0: resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} engines: {node: '>=8'} @@ -3581,6 +3932,16 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msgpackr-extract@3.0.4: + resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} + hasBin: true + + msgpackr@2.0.5: + resolution: {integrity: sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA==} + + multipasta@0.2.8: + resolution: {integrity: sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q==} + mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -3645,6 +4006,10 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + node-gyp@12.4.0: resolution: {integrity: sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==} engines: {node: ^20.17.0 || >=22.9.0} @@ -3762,10 +4127,18 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -3778,6 +4151,10 @@ packages: resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} engines: {node: '>=8'} + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -3790,6 +4167,9 @@ packages: resolution: {integrity: sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ==} engines: {node: ^20.17.0 || >=22.9.0} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.1: resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} @@ -3804,6 +4184,10 @@ packages: resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3816,6 +4200,13 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.2: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} @@ -3853,6 +4244,10 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + pkg-up@3.1.0: + resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} + engines: {node: '>=8'} + postcss-selector-parser@7.1.4: resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} engines: {node: '>=4'} @@ -3924,6 +4319,9 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + pure-rand@8.4.2: + resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} + qs@6.15.0: resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} engines: {node: '>=0.6'} @@ -4017,9 +4415,17 @@ packages: resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + reselect@4.1.8: + resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + restore-cursor@4.0.0: resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4072,6 +4478,9 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + s-js@0.4.9: + resolution: {integrity: sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -4113,6 +4522,16 @@ packages: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} + seroval-plugins@1.5.6: + resolution: {integrity: sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.6: + resolution: {integrity: sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA==} + engines: {node: '>=10'} + serve-static@2.2.1: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} @@ -4188,6 +4607,9 @@ packages: resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + solid-js@1.9.14: + resolution: {integrity: sha512-sAEXC0Kk0S1EDg+8ysEWJDbYhA3RRoEjwuySUGlKIemeo0I5YZfOyumNjNs9Sv3y2nmhD+0rW66ag2HsMuQiGQ==} + sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} @@ -4304,6 +4726,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -4387,6 +4813,10 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + toml@4.3.0: + resolution: {integrity: sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A==} + engines: {node: '>=20'} + tough-cookie@6.0.1: resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} @@ -4494,6 +4924,10 @@ packages: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true + uuid@14.0.1: + resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} + hasBin: true + validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -4591,6 +5025,14 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + web-tree-sitter@0.25.10: + resolution: {integrity: sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA==} + peerDependencies: + '@types/emscripten': ^1.40.0 + peerDependenciesMeta: + '@types/emscripten': + optional: true + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -4724,17 +5166,29 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.1.8: + resolution: {integrity: sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==} + snapshots: '@acemir/cssom@0.9.31': {} '@adobe/css-tools@4.4.4': {} + '@ai-sdk/provider@3.0.8': + dependencies: + json-schema: 0.4.0 + '@alcalzone/ansi-tokenize@0.2.5': dependencies: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@anthropic-ai/sdk@0.91.1(zod@3.25.76)': dependencies: json-schema-to-ts: 3.1.1 @@ -4981,8 +5435,34 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.29.0': {} + '@babel/core@7.28.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/core@7.29.0': dependencies: '@babel/code-frame': 7.29.0 @@ -5011,6 +5491,18 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.29.7': + dependencies: + '@babel/types': 7.29.8 + '@babel/helper-compilation-targets@7.28.6': dependencies: '@babel/compat-data': 7.29.0 @@ -5019,8 +5511,34 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.28.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/traverse': 7.29.8 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/helper-globals@7.28.0': {} + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-member-expression-to-functions@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.18.6': + dependencies: + '@babel/types': 7.29.0 + '@babel/helper-module-imports@7.28.6': dependencies: '@babel/traverse': 7.29.0 @@ -5028,6 +5546,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -5037,12 +5571,47 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.29.7(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.29.7': + dependencies: + '@babel/types': 7.29.8 + '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-replace-supers@7.29.7(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.7': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.29.2': @@ -5054,6 +5623,28 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -5064,6 +5655,28 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.28.0) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.28.0) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + '@babel/runtime@7.29.2': {} '@babel/template@7.28.6': @@ -5072,6 +5685,12 @@ snapshots: '@babel/parser': 7.29.0 '@babel/types': 7.29.0 + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@babel/traverse@7.29.0': dependencies: '@babel/code-frame': 7.29.0 @@ -5084,11 +5703,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@balena/dockerignore@1.0.2': {} '@bcoe/v8-coverage@1.0.2': {} @@ -5819,6 +6455,29 @@ snapshots: zod-to-json-schema: 3.25.1(zod@3.25.76) transitivePeerDependencies: - supports-color + optional: true + + '@modelcontextprotocol/sdk@1.26.0(zod@4.1.8)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.12.0) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.2.1(express@5.2.1) + hono: 4.12.0 + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.1.8 + zod-to-json-schema: 3.25.1(zod@4.1.8) + transitivePeerDependencies: + - supports-color '@monaco-editor/loader@1.7.0': dependencies: @@ -5831,6 +6490,24 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': + optional: true + '@next/env@16.2.5': {} '@next/swc-darwin-arm64@16.2.5': @@ -5975,6 +6652,21 @@ snapshots: node-gyp: 12.4.0 proc-log: 6.1.0 + '@opencode-ai/plugin@1.18.15(@opentui/core@0.4.5(typescript@5.9.3)(web-tree-sitter@0.25.10))(@opentui/keymap@0.4.5(@opentui/solid@0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10))(react@19.2.5)(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10))(@opentui/solid@0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10))': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@opencode-ai/sdk': 1.18.15 + effect: 4.0.0-beta.83 + zod: 4.1.8 + optionalDependencies: + '@opentui/core': 0.4.5(typescript@5.9.3)(web-tree-sitter@0.25.10) + '@opentui/keymap': 0.4.5(@opentui/solid@0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10))(react@19.2.5)(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10) + '@opentui/solid': 0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10) + + '@opencode-ai/sdk@1.18.15': + dependencies: + cross-spawn: 7.0.6 + '@opentelemetry/api-logs@0.220.0': dependencies: '@opentelemetry/api': 1.9.1 @@ -6094,6 +6786,76 @@ snapshots: '@opentelemetry/semantic-conventions@1.43.0': {} + '@opentui/core-darwin-arm64@0.4.5': + optional: true + + '@opentui/core-darwin-x64@0.4.5': + optional: true + + '@opentui/core-linux-arm64-musl@0.4.5': + optional: true + + '@opentui/core-linux-arm64@0.4.5': + optional: true + + '@opentui/core-linux-x64-musl@0.4.5': + optional: true + + '@opentui/core-linux-x64@0.4.5': + optional: true + + '@opentui/core-win32-arm64@0.4.5': + optional: true + + '@opentui/core-win32-x64@0.4.5': + optional: true + + '@opentui/core@0.4.5(typescript@5.9.3)(web-tree-sitter@0.25.10)': + dependencies: + bun-ffi-structs: 0.2.4(typescript@5.9.3) + diff: 9.0.0 + marked: 17.0.1 + string-width: 7.2.0 + strip-ansi: 7.1.2 + web-tree-sitter: 0.25.10 + optionalDependencies: + '@opentui/core-darwin-arm64': 0.4.5 + '@opentui/core-darwin-x64': 0.4.5 + '@opentui/core-linux-arm64': 0.4.5 + '@opentui/core-linux-arm64-musl': 0.4.5 + '@opentui/core-linux-x64': 0.4.5 + '@opentui/core-linux-x64-musl': 0.4.5 + '@opentui/core-win32-arm64': 0.4.5 + '@opentui/core-win32-x64': 0.4.5 + transitivePeerDependencies: + - typescript + + '@opentui/keymap@0.4.5(@opentui/solid@0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10))(react@19.2.5)(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10)': + dependencies: + '@opentui/core': 0.4.5(typescript@5.9.3)(web-tree-sitter@0.25.10) + optionalDependencies: + '@opentui/solid': 0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10) + react: 19.2.5 + solid-js: 1.9.14 + transitivePeerDependencies: + - typescript + - web-tree-sitter + + '@opentui/solid@0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10)': + dependencies: + '@babel/core': 7.28.0 + '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0) + '@opentui/core': 0.4.5(typescript@5.9.3)(web-tree-sitter@0.25.10) + babel-plugin-module-resolver: 5.0.2 + babel-preset-solid: 1.9.12(@babel/core@7.28.0)(solid-js@1.9.14) + entities: 7.0.1 + s-js: 0.4.9 + solid-js: 1.9.14 + transitivePeerDependencies: + - supports-color + - typescript + - web-tree-sitter + '@pinojs/redact@0.4.0': {} '@prisma/client@6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3)': @@ -6817,6 +7579,30 @@ snapshots: b4a@1.8.0: {} + babel-plugin-jsx-dom-expressions@0.40.7(@babel/core@7.28.0): + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.18.6 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.28.0) + '@babel/types': 7.29.0 + html-entities: 2.3.3 + parse5: 7.3.0 + + babel-plugin-module-resolver@5.0.2: + dependencies: + find-babel-config: 2.1.2 + glob: 9.3.5 + pkg-up: 3.1.0 + reselect: 4.1.8 + resolve: 1.22.12 + + babel-preset-solid@1.9.12(@babel/core@7.28.0)(solid-js@1.9.14): + dependencies: + '@babel/core': 7.28.0 + babel-plugin-jsx-dom-expressions: 0.40.7(@babel/core@7.28.0) + optionalDependencies: + solid-js: 1.9.14 + balanced-match@1.0.2: {} balanced-match@4.0.3: {} @@ -6942,6 +7728,10 @@ snapshots: buildcheck@0.0.7: optional: true + bun-ffi-structs@0.2.4(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + bytes@3.1.2: {} c12@3.1.0: @@ -7161,6 +7951,8 @@ snapshots: diff@8.0.4: {} + diff@9.0.0: {} + docker-modem@5.0.6: dependencies: debug: 4.4.3 @@ -7209,6 +8001,19 @@ snapshots: '@standard-schema/spec': 1.1.0 fast-check: 3.23.2 + effect@4.0.0-beta.83: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 4.9.0 + find-my-way-ts: 0.1.6 + ini: 7.0.0 + kubernetes-types: 1.30.0 + msgpackr: 2.0.5 + multipasta: 0.2.8 + toml: 4.3.0 + uuid: 14.0.1 + yaml: 2.9.0 + electron-to-chromium@1.5.344: {} emoji-regex@10.6.0: {} @@ -7228,6 +8033,10 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 + entities@6.0.1: {} + + entities@7.0.1: {} + entities@8.0.0: {} env-paths@2.2.1: {} @@ -7442,6 +8251,10 @@ snapshots: dependencies: pure-rand: 6.1.0 + fast-check@4.9.0: + dependencies: + pure-rand: 8.4.2 + fast-decode-uri-component@1.0.1: {} fast-deep-equal@3.1.3: {} @@ -7519,12 +8332,22 @@ snapshots: transitivePeerDependencies: - supports-color + find-babel-config@2.1.2: + dependencies: + json5: 2.2.3 + + find-my-way-ts@0.1.6: {} + find-my-way@9.4.0: dependencies: fast-deep-equal: 3.1.3 fast-querystring: 1.1.2 safe-regex2: 5.0.0 + find-up@3.0.0: + dependencies: + locate-path: 3.0.0 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -7676,6 +8499,13 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + glob@9.3.5: + dependencies: + fs.realpath: 1.0.0 + minimatch: 8.0.7 + minipass: 4.2.8 + path-scurry: 1.11.1 + google-auth-library@10.9.1: dependencies: base64-js: 1.5.1 @@ -7711,6 +8541,10 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + helmet@7.2.0: {} highlight.js@10.7.3: {} @@ -7733,6 +8567,8 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' + html-entities@2.3.3: {} + html-escaper@2.0.2: {} http-cache-semantics@4.2.0: {} @@ -7805,6 +8641,8 @@ snapshots: ini@6.0.0: {} + ini@7.0.0: {} + ink@6.8.0(@types/react@19.2.14)(react@19.2.4): dependencies: '@alcalzone/ansi-tokenize': 0.2.5 @@ -7857,6 +8695,10 @@ snapshots: ipaddr.js@2.3.0: {} + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -7972,6 +8814,8 @@ snapshots: json-schema-typed@8.0.2: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-stringify-nice@1.1.4: {} @@ -8005,6 +8849,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kubernetes-types@1.30.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -8065,6 +8911,11 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + locate-path@3.0.0: + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -8124,6 +8975,8 @@ snapshots: marked@14.0.0: {} + marked@17.0.1: {} + marked@18.0.5: {} math-intrinsics@1.1.0: {} @@ -8166,6 +9019,10 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimatch@8.0.7: + dependencies: + brace-expansion: 2.0.2 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 @@ -8200,6 +9057,8 @@ snapshots: dependencies: yallist: 4.0.0 + minipass@4.2.8: {} + minipass@5.0.0: {} minipass@7.1.3: {} @@ -8230,6 +9089,24 @@ snapshots: ms@2.1.3: {} + msgpackr-extract@3.0.4: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 + optional: true + + msgpackr@2.0.5: + optionalDependencies: + msgpackr-extract: 3.0.4 + + multipasta@0.2.8: {} + mute-stream@2.0.0: {} nan@2.25.0: @@ -8282,6 +9159,11 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.1.2 + optional: true + node-gyp@12.4.0: dependencies: env-paths: 2.2.1 @@ -8415,10 +9297,18 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 + p-locate@3.0.0: + dependencies: + p-limit: 2.3.0 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 @@ -8430,6 +9320,8 @@ snapshots: '@types/retry': 0.12.0 retry: 0.13.1 + p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} pacote@21.5.1: @@ -8460,6 +9352,10 @@ snapshots: just-diff: 6.0.2 just-diff-apply: 5.5.0 + parse5@7.3.0: + dependencies: + entities: 6.0.1 + parse5@8.0.1: dependencies: entities: 8.0.0 @@ -8470,12 +9366,21 @@ snapshots: patch-console@2.0.0: {} + path-exists@3.0.0: {} + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} path-key@3.1.1: {} + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + path-scurry@2.0.2: dependencies: lru-cache: 11.2.6 @@ -8519,6 +9424,10 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 + pkg-up@3.1.0: + dependencies: + find-up: 3.0.0 + postcss-selector-parser@7.1.4: dependencies: cssesc: 3.0.0 @@ -8600,6 +9509,8 @@ snapshots: pure-rand@6.1.0: {} + pure-rand@8.4.2: {} + qs@6.15.0: dependencies: side-channel: 1.1.0 @@ -8680,8 +9591,17 @@ snapshots: transitivePeerDependencies: - supports-color + reselect@4.1.8: {} + resolve-pkg-maps@1.0.0: {} + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@4.0.0: dependencies: onetime: 5.1.2 @@ -8755,6 +9675,8 @@ snapshots: dependencies: tslib: 2.8.1 + s-js@0.4.9: {} + safe-buffer@5.2.1: {} safe-regex2@5.0.0: @@ -8795,6 +9717,12 @@ snapshots: transitivePeerDependencies: - supports-color + seroval-plugins@1.5.6(seroval@1.5.6): + dependencies: + seroval: 1.5.6 + + seroval@1.5.6: {} + serve-static@2.2.1: dependencies: encodeurl: 2.0.0 @@ -8918,6 +9846,12 @@ snapshots: ip-address: 10.0.1 smart-buffer: 4.2.0 + solid-js@1.9.14: + dependencies: + csstype: 3.2.3 + seroval: 1.5.6 + seroval-plugins: 1.5.6(seroval@1.5.6) + sonic-boom@4.2.1: dependencies: atomic-sleep: 1.0.0 @@ -8998,7 +9932,7 @@ snapshots: string-width@7.2.0: dependencies: emoji-regex: 10.6.0 - get-east-asian-width: 1.5.0 + get-east-asian-width: 1.6.0 strip-ansi: 7.1.2 string-width@8.2.0: @@ -9035,6 +9969,8 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + symbol-tree@3.2.4: {} tagged-tag@1.0.0: {} @@ -9140,6 +10076,8 @@ snapshots: toidentifier@1.0.1: {} + toml@4.3.0: {} + tough-cookie@6.0.1: dependencies: tldts: 7.0.28 @@ -9227,6 +10165,8 @@ snapshots: uuid@10.0.0: {} + uuid@14.0.1: {} + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 @@ -9299,6 +10239,8 @@ snapshots: web-streams-polyfill@3.3.3: {} + web-tree-sitter@0.25.10: {} + webidl-conversions@3.0.1: {} webidl-conversions@8.0.1: {} @@ -9405,4 +10347,10 @@ snapshots: dependencies: zod: 3.25.76 + zod-to-json-schema@3.25.1(zod@4.1.8): + dependencies: + zod: 4.1.8 + zod@3.25.76: {} + + zod@4.1.8: {} diff --git a/scripts/generate-opencode-extension.ts b/scripts/generate-opencode-extension.ts new file mode 100644 index 0000000..3a81c62 --- /dev/null +++ b/scripts/generate-opencode-extension.ts @@ -0,0 +1,50 @@ +#!/usr/bin/env node +/** + * Generates `src/cli/src/config/opencode-extension.ts` which embeds the opencode + * plugin source (`open-mcpctl.tsx` + `projects.ts`) as string constants, + * mirroring how the pi and prime-agent extensions are embedded. + * + * Embedding matters: `mcpctl config opencode` must work from an installed + * binary that has no access to the source tree. The installed plugin files are + * this exact embedded source, so what the CLI ships is always what opencode + * runs. + * + * Regenerate after editing the plugin source: + * npx tsx scripts/generate-opencode-extension.ts + */ +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; +import { dirname, join } from 'node:path'; + +const scriptsDir = import.meta.dirname; +const root = join(scriptsDir, '..'); +const opencodeExtDir = join(root, 'src', 'opencode-ext'); + +const MAIN = readFileSync(join(opencodeExtDir, 'open-mcpctl.tsx'), 'utf-8'); +const PROJECTS = readFileSync(join(opencodeExtDir, 'projects.ts'), 'utf-8'); + +function embed(src) { + // JSON.stringify yields a quoted string literal we can inline directly. + return JSON.stringify(src); +} + +const out = `/** + * Embedded source of the mcpctl opencode plugin — DO NOT EDIT BY HAND. + * Generated by \`npx tsx scripts/generate-opencode-extension.ts\` from + * \`src/opencode-ext/open-mcpctl.tsx\` + \`src/opencode-ext/projects.ts\`. + * + * \`mcpctl config opencode\` writes these verbatim into + * ~/.config/opencode/plugin/ and registers \`open-mcpctl.tsx\` in opencode.json's + * plugin array, so an installed binary with no source tree can still provision + * a working opencode integration. + */ +export const OPENCODE_EXTENSION_FILENAMES = ['open-mcpctl.tsx', 'projects.ts'] as const; + +export const OPENCODE_EXTENSION_FILES: Record = { + 'open-mcpctl.tsx': ${embed(MAIN)}, + 'projects.ts': ${embed(PROJECTS)}, +}; +`; + +mkdirSync(dirname(join(root, 'src', 'cli', 'src', 'config')), { recursive: true }); +writeFileSync(join(root, 'src', 'cli', 'src', 'config', 'opencode-extension.ts'), out); +console.log('wrote src/cli/src/config/opencode-extension.ts'); diff --git a/src/cli/src/commands/config.ts b/src/cli/src/commands/config.ts index 676a5e7..8dc1253 100644 --- a/src/cli/src/commands/config.ts +++ b/src/cli/src/commands/config.ts @@ -23,6 +23,17 @@ import { withPiAgentDir, writePiProjectState, } from '../utils/pi-settings.js'; +import { + installExtensionFiles as installOpenCodeExtensionFiles, + installEmbeddedExtension as installOpenCodeEmbeddedExtension, + opencodePluginDir, + opencodeDepsDir, + opencodeStatePath, + withOpenCodeDir, + writeOpenCodeProjectState, + provisionDeps, + registerWithOpenCode, +} from '../utils/opencode-settings.js'; import { runSkillsSync } from './skills.js'; import { registerPrimeAgentMcp, @@ -590,6 +601,114 @@ export function createConfigCommand(deps?: Partial, apiDeps?: registerPrimeAgentCommand('prime-agent', false); registerPrimeAgentCommand('prime-agent-generate', true); // backward compat + // ── opencode: bottom-row project indicator + in-TUI project switcher ── + config + .command('opencode') + .description('Install the opencode plugin (bottom-row project indicator + switcher, sync skills)') + .option('-p, --project ', 'Project name to make active') + .option('--extension-dir ', 'Source-tree src/opencode-ext dir (dev only; default: embedded sources)') + .option('--skip-deps', 'Skip provisioning opencode plugin deps into ~/.config/opencode') + .option('--skip-skills', 'Skip the initial skills sync') + .option('--opencode-dir ', 'Override the opencode config dir (default: ~/.config/opencode)') + .action(async (opts: { + project?: string; + extensionDir?: string; + skipDeps?: boolean; + skipSkills?: boolean; + opencodeDir?: string; + }) => { + if (!opts.project) { + log('Error: --project is required for mcpctl config opencode'); + process.exitCode = 1; + return; + } + + // Resolve target paths, honouring an optional --opencode-dir override. + const oc = opts.opencodeDir ? withOpenCodeDir(opts.opencodeDir) : undefined; + const pluginDest = oc?.pluginDir() ?? opencodePluginDir(); + const depsDir = oc?.depsDir() ?? opencodeDepsDir(); + // Isolate the persisted active-project state under the opener config dir + // when a custom --opencode-dir is used (also keeps tests off the real ~/.mcpctl). + const statePath = opts.opencodeDir ? join(opts.opencodeDir, 'opencode-state.json') : opencodeStatePath(); + const skillsInstall = oc?.skillsDir() ?? join(homedir(), '.config', 'opencode', 'skill'); + const configPath = oc?.configPath() ?? join(homedir(), '.config', 'opencode', 'opencode.json'); + const pluginMain = oc?.pluginMainPath() ?? join(homedir(), '.config', 'opencode', 'plugin', 'open-mcpctl.tsx'); + + // 1. Install the plugin file into ~/.config/opencode/plugin/. + // Default: write the embedded source (works from an installed binary + // with no source tree). --extension-dir overrides with a source-tree + // checkout for dev iteration. + try { + const written = opts.extensionDir + ? await installOpenCodeExtensionFiles(opts.extensionDir, pluginDest) + : await installOpenCodeEmbeddedExtension(pluginDest); + log('Installed opencode plugin:'); + for (const w of written) log(` ${w}`); + } catch (err: unknown) { + log(`Error: could not install plugin: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + return; + } + + // 2. Register the .tsx plugin in opencode.json's plugin array (auto- + // discovery only matches {ts,js}, so the config entry is required). + try { + if (await registerWithOpenCode(configPath, pluginMain)) { + log(`Registered plugin in ${configPath}`); + } else { + log(`Plugin already registered in ${configPath}`); + } + } catch (err: unknown) { + log(`Warning: could not register plugin in opencode.json: ${err instanceof Error ? err.message : String(err)}`); + } + + // 3. Best-effort provision the TUI peer deps. opencode auto-installs + // @opencode-ai/plugin but NOT the @opentui/* peers a TUI plugin needs; + // without them the module fails to import and the integration is dead. + if (!opts.skipDeps) { + try { + await provisionDeps(depsDir); + log('Verified opencode plugin deps (@opencode-ai/plugin, @opentui/*) in ~/.config/opencode'); + } catch (err: unknown) { + log(`Warning: could not provision opencode deps: ${err instanceof Error ? err.message : String(err)}`); + log(' The plugin needs @opcode-ai/plugin + @opentui/* resolvable from ~/.config/opencode.'); + } + } + + // 4. Persist the active project (survives across sessions/cwd). + try { + const p = await writeOpenCodeProjectState(opts.project, statePath); + log(`Wrote active project to ${p}`); + } catch (err: unknown) { + log(`Warning: could not write opencode state: ${err instanceof Error ? err.message : String(err)}`); + } + + // 5. Optional initial skills sync into opencode's skill tree. + if (!opts.skipSkills && skillsClient) { + try { + const result = await runSkillsSync( + { project: opts.project, target: 'opencode', installRoot: skillsInstall }, + { client: skillsClient, log: (...a) => log(...(a as string[])), warn: (...a) => console.error(...(a as Parameters)) }, + ); + const total = result.installed.length + result.updated.length + result.removed.length; + if (total > 0) { + log(`Skills synced to ${skillsInstall} (${String(result.installed.length)} new, ${String(result.updated.length)} updated, ${String(result.removed.length)} removed)`); + } else { + log('Skills: no changes (already up to date)'); + } + } catch (err: unknown) { + log(`Warning: initial skills sync failed: ${err instanceof Error ? err.message : String(err)}`); + } + } else if (!opts.skipSkills) { + log('Warning: skipping initial skills sync (no API client provided)'); + } + + log(''); + log('Next: restart opencode (or start a new session). The active mcpctl'); + log(`project is shown in the bottom status row; press m (or the`); + log('mcpctl.switch_project command) to switch projects from inside opencode.'); + }); + config.addCommand(createConfigSetupCommand({ configDeps })); diff --git a/src/cli/src/commands/skills.ts b/src/cli/src/commands/skills.ts index 89f95e6..f23abe0 100644 --- a/src/cli/src/commands/skills.ts +++ b/src/cli/src/commands/skills.ts @@ -104,7 +104,7 @@ export interface SyncOpts { * first project's skills, and pre-existing * (untracked) skill dirs are preserved. */ - target?: 'claude' | 'prime-agent' | 'pi'; + target?: 'claude' | 'prime-agent' | 'pi' | 'opencode'; } export interface SyncResult { @@ -194,10 +194,11 @@ export async function runSkillsSync(opts: SyncOpts, deps: SyncDeps): Promise [s.name, s])); @@ -566,7 +571,8 @@ export interface SkillsCommandDeps { * "claude" → ~/.claude/skills * "prime-agent" → ~/.prime/agent/skills * "pi" → ~/.pi/agent/skills - * Kept here so `mcpctl skills sync --agent` and `mcpctl config pi` agree. + * "opencode" → ~/.config/opencode/skill + * Kept here so `mcpctl skills sync --agent` and `mcpctl config ` agree. */ export function agentInstallRoot(agent: string | undefined): string { switch (agent) { @@ -574,6 +580,8 @@ export function agentInstallRoot(agent: string | undefined): string { return join(homedir(), '.prime', 'agent', 'skills'); case 'pi': return join(homedir(), '.pi', 'agent', 'skills'); + case 'opencode': + return join(homedir(), '.config', 'opencode', 'skill'); case 'claude': default: return join(homedir(), '.claude', 'skills'); @@ -591,7 +599,7 @@ export function createSkillsCommand(deps: SkillsCommandDeps): Command { cmd.command('sync') .description('Sync skills from mcpd onto disk (~/.claude, ~/.prime, or ~/.pi agent skill roots)') .option('-p, --project ', 'Project to sync (overrides .mcpctl-project marker)') - .option('--agent ', 'Sync target: claude (default), prime-agent, or pi', 'claude') + .option('--agent ', 'Sync target: claude (default), prime-agent, pi, or opencode', 'claude') .option('--dry-run', 'Print what would change without writing anything') .option('--force', 'Overwrite locally-modified skills') .option('--quiet', 'Suppress all output unless something changed (used by session-start hooks)') @@ -609,8 +617,8 @@ export function createSkillsCommand(deps: SkillsCommandDeps): Command { // Validate --agent so an unknown value fails loudly instead of silently // running the default (Claude) sync. const agent = opts.agent ?? 'claude'; - if (agent !== 'claude' && agent !== 'prime-agent' && agent !== 'pi') { - warn(`mcpctl: unknown sync target '${agent}' (expected 'claude', 'prime-agent', or 'pi')`); + if (agent !== 'claude' && agent !== 'prime-agent' && agent !== 'pi' && agent !== 'opencode') { + warn(`mcpctl: unknown sync target '${agent}' (expected 'claude', 'prime-agent', 'pi', or 'opencode')`); process.exitCode = 1; return; } @@ -622,7 +630,7 @@ export function createSkillsCommand(deps: SkillsCommandDeps): Command { ...(opts.quiet !== undefined ? { quiet: opts.quiet } : {}), ...(opts.skipPostinstall !== undefined ? { skipPostInstall: opts.skipPostinstall } : {}), ...(opts.keepOrphans !== undefined ? { keepOrphans: opts.keepOrphans } : {}), - target: agent as 'claude' | 'prime-agent' | 'pi', + target: agent as 'claude' | 'prime-agent' | 'pi' | 'opencode', installRoot: agentInstallRoot(agent), }, { client, log, warn }, diff --git a/src/cli/src/config/opencode-extension.ts b/src/cli/src/config/opencode-extension.ts new file mode 100644 index 0000000..a28ae96 --- /dev/null +++ b/src/cli/src/config/opencode-extension.ts @@ -0,0 +1,16 @@ +/** + * Embedded source of the mcpctl opencode plugin — DO NOT EDIT BY HAND. + * Generated by `npx tsx scripts/generate-opencode-extension.ts` from + * `src/opencode-ext/open-mcpctl.tsx` + `src/opencode-ext/projects.ts`. + * + * `mcpctl config opencode` writes these verbatim into + * ~/.config/opencode/plugin/ and registers `open-mcpctl.tsx` in opencode.json's + * plugin array, so an installed binary with no source tree can still provision + * a working opencode integration. + */ +export const OPENCODE_EXTENSION_FILENAMES = ['open-mcpctl.tsx', 'projects.ts'] as const; + +export const OPENCODE_EXTENSION_FILES: Record = { + 'open-mcpctl.tsx': "/**\n * mcpctl opencode TUI plugin — native integration (no reimplemented MCP client).\n *\n * opencode, unlike pi, has first-class MCP support: `mcpctl config opencode\n * --project X` writes the active project's mcplocal endpoint into\n * `opencode.json` under `mcp`, and opencode lists those tools natively. This\n * plugin is the *UX* half that pi/prime-agent ship: it shows the active\n * project in opencode's bottom status row (right where the token counter is)\n * and lets the user switch projects from inside the TUI.\n *\n * Features:\n * - a status indicator rendered into the `app_bottom` slot (the row directly\n * beneath the session footer statusline)\n * - a `DialogSelect` project picker (search-as-you-type) opened from the\n * command palette (`mcpctl.switch_project`) or a leader binding\n * - live project switching: rewrites ~/.mcpctl/opencode-state.json, updates\n * the indicator, and re-points the running MCP backend via the SDK client\n * (`mcp.disconnect` / `mcp.connect`) so tools change without a restart\n * - toast notifications for success / gated-project hints\n *\n * State is persisted in `~/.mcpctl/opencode-state.json` (survives cwd) and the\n * active project is also inferred from a `.mcpctl-project` marker walk-up.\n *\n * Loaded by placing this file (or the installed copy) under\n * `~/.config/opencode/plugin/` (opencode auto-discovers `plugin/*.{ts,js}`).\n *\n * Only imports opencode-bundled types — no @mcpctl/*, no ~/.claude.\n */\n/** @jsxImportSource @opentui/solid */\nimport { createMemo, createSignal, Show } from \"solid-js\"\nimport type { JSX } from \"@opentui/solid\"\nimport type { TuiPlugin, TuiPluginApi } from \"@opencode-ai/plugin/tui\"\nimport { readFile, writeFile, mkdir } from \"node:fs/promises\"\nimport { join, dirname } from \"node:path\"\nimport { homedir } from \"node:os\"\nimport { filterProjects, MCP_SERVER_NAME, toolChangeAnnouncement } from \"./projects.js\"\n\nexport const id = \"mcpctl\"\n\n// ── config / state ───────────────────────────────────────────────────────────\nconst McpctlDir = join(homedir(), \".mcpctl\")\nconst ConfigPath = join(McpctlDir, \"config.json\")\nconst CredPath = join(McpctlDir, \"credentials\")\nconst OpenCodeStatePath = join(McpctlDir, \"opencode-state.json\")\nconst ProjectMarker = \".mcpctl-project\"\n\ninterface OpenCodeState {\n project?: string\n}\n\nasync function readJson(path: string): Promise {\n try {\n return JSON.parse(await readFile(path, \"utf-8\")) as T\n } catch {\n return null\n }\n}\n\nasync function readConfig(): Promise<{ mcplocalUrl: string }> {\n const cfg = await readJson<{ mcplocalUrl?: string }>(ConfigPath)\n return { mcplocalUrl: cfg?.mcplocalUrl ?? \"http://localhost:3200\" }\n}\n\nasync function readCreds(): Promise<{ token?: string } | null> {\n try {\n const raw = await readFile(CredPath, \"utf-8\")\n const parsed = JSON.parse(raw) as { token?: string }\n if (!parsed.token) return null\n return parsed\n } catch {\n return null\n }\n}\n\nasync function readState(): Promise {\n return (await readJson(OpenCodeStatePath)) ?? {}\n}\n\nasync function writeState(state: OpenCodeState): Promise {\n await mkdir(McpctlDir, { recursive: true })\n await writeFile(OpenCodeStatePath, JSON.stringify(state, null, 2) + \"\\n\", \"utf-8\")\n}\n\n/** Walk up directories looking for a `.mcpctl-project` marker. */\nasync function findProjectMarker(start = process.cwd()): Promise {\n const home = homedir()\n let dir = start\n for (let i = 0; i < 50; i++) {\n try {\n const raw = await readFile(join(dir, ProjectMarker), \"utf-8\")\n const project = raw.split(\"\\n\")[0]?.trim() ?? \"\"\n if (project) return project\n } catch {\n // continue walking\n }\n if (dir === home || dir === \"/\") break\n const parent = dirname(dir)\n if (parent === dir) break\n dir = parent\n }\n return null\n}\n\n/** Request the list of projects from mcpd (via mcplocal API passthrough). */\nasync function listProjects(mcplocalUrl: string, token?: string): Promise {\n const url = `${mcplocalUrl}/api/v1/projects`\n const headers: Record = { Accept: \"application/json\" }\n if (token) headers.Authorization = `Bearer ${token}`\n const res = await fetch(url, { headers })\n if (!res.ok) throw new Error(`list projects: HTTP ${res.status}`)\n const body = (await res.json()) as Array<{ name: string }> | { data?: Array<{ name: string }> }\n const arr = Array.isArray(body) ? body : (body.data ?? [])\n return arr.map((p) => p.name)\n}\n\n/** Streamable-HTTP endpoint for a project, exactly as `mcpctl mcp -p X` bridges to. */\nfunction projectEndpoint(mcplocalUrl: string, project: string): string {\n return `${mcplocalUrl.replace(/\\/$/, \"\")}/projects/${encodeURIComponent(project)}/mcp`\n}\n\n// ── the TUI plugin ───────────────────────────────────────────────────────────\nexport const tui: TuiPlugin = async (api) => {\n let mcplocalUrl = \"http://localhost:3200\"\n let token: string | undefined\n let activeProject: string | null = null\n const [project, setProject] = createSignal(null)\n\n function refreshFromState(projectName: string | null): void {\n activeProject = projectName\n setProject(projectName)\n }\n\n async function loadConnection(): Promise<{ project: string | null; mcplocalUrl: string; token?: string }> {\n const cfg = await readConfig()\n mcplocalUrl = cfg.mcplocalUrl\n const creds = await readCreds()\n token = creds?.token ?? undefined\n const state = await readState()\n const projectName = state.project ?? (await findProjectMarker())\n return { project: projectName, mcplocalUrl, token }\n }\n\n /** Point opencode's live `mcpctl` MCP server at a project. */\n async function setMcpProject(next: string): Promise {\n // Remove the old definition first so tools from the previous project can't\n // linger alongside the new ones.\n try {\n await api.client.mcp.disconnect({ name: MCP_SERVER_NAME })\n } catch {\n // not connected yet — fine\n }\n const endpoint = projectEndpoint(mcplocalUrl, next)\n const headers: Record = {}\n if (token) headers.Authorization = `Bearer ${token}`\n await api.client.mcp.add({ name: MCP_SERVER_NAME, config: { type: \"remote\", url: endpoint, headers } })\n await api.client.mcp.connect({ name: MCP_SERVER_NAME })\n // opencode enumerates the project tools over native MCP; we surface the\n // names we know from the bridge in the next-turn announcement.\n return []\n }\n\n async function switchProject(next: string): Promise {\n const tools: string[] = []\n try {\n tools.push(...(await setMcpProject(next)))\n } catch (e) {\n api.ui.toast({ variant: \"error\", message: `mcpctl: could not connect '${next}' — ${(e as Error).message}` })\n return\n }\n refreshFromState(next)\n await writeState({ project: next })\n api.ui.toast({\n variant: \"success\",\n message: `Switched mcpctl project to '${next}' — MCP tools reloaded.`,\n })\n void api.client.app.log({ service: \"mcpctl\", level: \"info\", message: `project switch -> ${next}` })\n }\n\n // ── project picker ──\n function openPicker(): void {\n void (async () => {\n let projects: string[] = []\n try {\n projects = await listProjects(mcplocalUrl, token)\n } catch (e) {\n api.ui.toast({ variant: \"error\", message: `mcpctl: failed to list projects — ${(e as Error).message}` })\n return\n }\n if (projects.length === 0) {\n api.ui.toast({ variant: \"warning\", message: \"mcpctl: no projects returned by mcpd\" })\n return\n }\n const ordered = filterProjects(projects, \"\", activeProject)\n api.ui.dialog.replace(() => (\n ({\n title: name,\n value: name,\n description: name === activeProject ? \"active project\" : undefined,\n disabled: name === activeProject,\n onSelect: () => void switchProject(name),\n }))}\n />\n ))\n })()\n }\n\n // ── command + keybind ──\n api.keymap.registerLayer({\n commands: [\n {\n name: \"mcpctl.switch_project\",\n title: \"mcpctl: switch project\",\n category: \"mcpctl\",\n run: openPicker,\n },\n ],\n bindings: [{ key: \"m\", group: \"mcpctl\", desc: \"switch mcpctl project\", cmd: openPicker }],\n })\n\n // ── indicator slot (bottom status row, beside the token counter) ──\n // Rendered into `app_bottom`: the row directly beneath the session footer\n // statusline, i.e. where the token counter / mode / model info live.\n const Indicator = (_props: unknown): JSX.Element => {\n const label = createMemo(() => project() ?? \"\")\n return (\n \n \n \n mcpctl{\" \"}\n {label()}\n \n \n \n )\n }\n\n api.slots.register({\n slots: {\n app_bottom: Indicator,\n },\n })\n\n // bootstrap state\n void loadConnection().then(({ project: p }) => refreshFromState(p))\n}\n", + 'projects.ts': "/**\n * Pure, dependency-free helpers shared by the opencode plugin and its tests.\n *\n * Kept out of `open-mcpctl.tsx` so vitest can import them without a JSX /\n * @opentui transform, and so the orchestration logic (which is the easy part\n * to get wrong on ordering/filtering) is unit-tested rather than eyeballed\n * through a TUI.\n */\n\n/** The MCP server name opencode uses for the mcpctl project bridge. */\nexport const MCP_SERVER_NAME = \"mcpctl\";\n\n/**\n * Order and filter the project list for the picker.\n *\n * Ordering: the active project first (most likely pick), then alphabetical.\n * Filtering: space-separated terms, ALL of which must appear as\n * case-insensitive substrings — so `home auto` finds `homeautomation`. A blank\n * query keeps everything.\n */\nexport function filterProjects(projects: string[], query: string, active: string | null): string[] {\n const ordered = [...projects].sort((a, b) => {\n if (a === active) return -1;\n if (b === active) return 1;\n return a.localeCompare(b);\n });\n const terms = query.toLowerCase().split(/\\s+/).filter((t) => t.length > 0);\n if (terms.length === 0) return ordered;\n return ordered.filter((p) => {\n const name = p.toLowerCase();\n return terms.every((t) => name.includes(t));\n });\n}\n\n/**\n * The announcement message describing the post-switch tool set.\n *\n * Shown to the user (and, via a user-role injection, made visible to the model)\n * so neither keeps calling the previous project's stale `mc_*` tool names.\n */\nexport function toolChangeAnnouncement(project: string, tools: string[]): string {\n const gate = tools.find((n) => n.endsWith(\"_begin_session\"));\n const parts = [\n `[mcpctl] The active project is now '${project}'.`,\n \"mcpctl tool names listed earlier in this conversation belong to the previously active project and are no longer callable — ignore them.\",\n tools.length > 0\n ? `Currently available mcpctl tools: ${tools.join(\", \")}.`\n : \"No mcpctl tools are currently available for this project.\",\n ];\n if (gate !== undefined) {\n parts.push(`This project is gated: call ${gate} first and its remaining tools become available.`);\n }\n return parts.join(\" \");\n}\n", +}; diff --git a/src/cli/src/utils/opencode-settings.ts b/src/cli/src/utils/opencode-settings.ts new file mode 100644 index 0000000..ba40c84 --- /dev/null +++ b/src/cli/src/utils/opencode-settings.ts @@ -0,0 +1,194 @@ +/** + * Wiring helpers for the opencode TUI plugin. + * + * `mcpctl config opencode --project X`: + * 1. writes the embedded plugin source (`open-mcpctl.tsx`) into + * `~/.config/opencode/plugin/` and registers that path in + * `opencode.json`'s `plugin` array. The plugin is `.tsx`, which opencode's + * auto-discovery (`plugin/*.{ts,js}`) does NOT pick up, so the explicit + * config entry is what loads it, + * 2. best-effort provisions the opencode TUI peer deps + * (`@opencode-ai/plugin`, `@opentui/*`) into `~/.config/opencode/node_modules` + * so the plugin's imports resolve, + * 3. writes `~/.mcpctl/opencode-state.json` so the active project survives + * across sessions regardless of the shell's cwd. + * + * The plugin source is embedded in the CLI (see + * `config/opencode-extension.ts`) so an installed binary with no source tree + * can still provision a working opencode integration. + * + * Standalone: never touches `~/.claude/`, `~/.pi/`, `~/.prime/`, or `~/.mcp.json`. + */ +import { readFile, writeFile, mkdir, copyFile, access } from 'node:fs/promises'; +import { dirname, join, resolve } from 'node:path'; +import { homedir } from 'node:os'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; +import { OPENCODE_EXTENSION_FILES } from '../config/opencode-extension.js'; + +const execFileAsync = promisify(execFile); +const PLUGIN_FILENAME = 'open-mcpctl.tsx'; + +export function opencodeConfigDir(): string { + return join(homedir(), '.config', 'opencode'); +} + +/** Override the opencode config dir (for tests / custom installs). */ +export function withOpenCodeDir(base: string): { + pluginDir: () => string; + pluginMainPath: () => string; + skillsDir: () => string; + depsDir: () => string; + configPath: () => string; +} { + return { + pluginDir: () => join(base, 'plugin'), + pluginMainPath: () => join(base, 'plugin', PLUGIN_FILENAME), + skillsDir: () => join(base, 'skill'), + depsDir: () => join(base, 'node_modules'), + configPath: () => join(base, 'opencode.json'), + }; +} + +export function opencodePluginDir(): string { + return join(opencodeConfigDir(), 'plugin'); +} + +export function opencodeSkillsDir(): string { + return join(opencodeConfigDir(), 'skill'); +} + +export function opencodePluginMainPath(): string { + return join(opencodePluginDir(), PLUGIN_FILENAME); +} + +/** node_modules dir opencode resolves plugin imports against. */ +export function opencodeDepsDir(): string { + return join(opencodeConfigDir(), 'node_modules'); +} + +export function opencodeStatePath(): string { + return join(homedir(), '.mcpctl', 'opencode-state.json'); +} + +export function opencodeConfigPath(): string { + return join(opencodeConfigDir(), 'opencode.json'); +} + +interface OpenCodeConfig { + plugin?: Array; + [k: string]: unknown; +} + +async function readConfigFile(path: string): Promise { + try { + const raw = await readFile(path, 'utf-8'); + if (raw.trim().length === 0) return {}; + try { + return JSON.parse(raw) as OpenCodeConfig; + } catch { + // opencode.json may contain line comments; strip them defensively. + return JSON.parse(raw.replace(/^\s*\/\/.*$/gm, '')) as OpenCodeConfig; + } + } catch (err: unknown) { + if ((err as { code?: string }).code === 'ENOENT') return {}; + return {}; + } +} + +/** + * Register our plugin in opencode.json's `plugin` array so opencode loads it. + * The plugin file is `.tsx`, which auto-discovery (`plugin/*.{ts,js}`) does NOT + * pick up, so it must be listed explicitly. Idempotent — re-running doesn't + * duplicate the entry, and existing config keys are preserved. + */ +export async function registerWithOpenCode( + configPath: string = opencodeConfigPath(), + pluginPath: string = opencodePluginMainPath(), +): Promise { + const config = await readConfigFile(configPath); + // Absolute file URL, like opencode resolves path specs (file:///abs/...). + const spec = pluginPath.startsWith('/') ? `file://${pluginPath}` : pluginPath; + if (!Array.isArray(config.plugin)) config.plugin = []; + const existing = config.plugin.some((p) => (Array.isArray(p) ? p[0] : p) === spec); + if (existing) return false; + config.plugin.push(spec); + await mkdir(dirname(configPath), { recursive: true }); + await writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8'); + return true; +} + +/** + * Write the embedded plugin source into opencode's plugin dir. Production path + * used by `mcpctl config opencode` — needs no source tree, so it works from an + * installed binary. + */ +export async function installEmbeddedExtension(dest: string = opencodePluginDir()): Promise { + await mkdir(dest, { recursive: true }); + const written: string[] = []; + for (const [name, src] of Object.entries(OPENCODE_EXTENSION_FILES)) { + const dst = join(dest, name); + await writeFile(dst, src, 'utf-8'); + written.push(dst); + } + return written; +} + +/** + * Copy the bundled plugin sources (relative to this source tree's + * `src/opencode-ext/`) into opencode's plugin dir. Kept for tests / dev; + * production uses `installEmbeddedExtension` so installed binaries are + * self-contained. Both files must land together — open-mcpctl.tsx imports its + * sibling ./projects helper. + */ +export async function installExtensionFiles( + srcDir: string, + dest: string = opencodePluginDir(), +): Promise { + await mkdir(dest, { recursive: true }); + const files = ['open-mcpctl.tsx', 'projects.ts'] as const; + const written: string[] = []; + for (const name of files) { + const src = resolve(srcDir, name); + const dst = join(dest, name); + await copyFile(src, dst); + written.push(dst); + } + return written; +} + +/** + * Best-effort provision the deps the TUI plugin imports. opencode installs + * `@opencode-ai/plugin` into the config dir itself, but the TUI peer deps + * (`@opentui/*`) are not auto-installed — without them the plugin module fails + * to import and the whole integration is silently dead. `--no-save` keeps this + * from fighting opencode over its own package.json; failures are reported but + * non-fatal (a distro opencode may already provide the peers). + */ +export async function provisionDeps(depsDir: string = opencodeDepsDir()): Promise { + const specs = ['@opencode-ai/plugin@1.18.15', '@opentui/solid@0.4.5', '@opentui/core@0.4.5', '@opentui/keymap@0.4.5']; + const pkgs = ['@opencode-ai/plugin', '@opentui/solid', '@opentui/core', '@opentui/keymap']; + // Fast path: already resolvable → nothing to do. + const present = await Promise.all(pkgs.map((pkg) => isDir(join(depsDir, pkg)))); + if (present.every(Boolean)) return []; + await mkdir(depsDir, { recursive: true }); + const args = ['install', '--no-save', '--silent', '--no-audit', '--no-fund', '--prefix', depsDir, ...specs]; + await execFileAsync('npm', args, { timeout: 180_000 }); + return specs; +} + +async function isDir(p: string): Promise { + try { + await access(p); + return true; + } catch { + return false; + } +} + +/** Write the persisted active-project state. */ +export async function writeOpenCodeProjectState(project: string, path: string = opencodeStatePath()): Promise { + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, JSON.stringify({ project }, null, 2) + '\n', 'utf-8'); + return path; +} diff --git a/src/cli/tests/config/opencode-extension-embed.test.ts b/src/cli/tests/config/opencode-extension-embed.test.ts new file mode 100644 index 0000000..11a45cc --- /dev/null +++ b/src/cli/tests/config/opencode-extension-embed.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { OPENCODE_EXTENSION_FILES, OPENCODE_EXTENSION_FILENAMES } from '../../src/config/opencode-extension.js'; + +/** + * `mcpctl config opencode` installs the *embedded* copy of the plugin, not the + * files in src/opencode-ext/. Editing the sources without re-running the + * generator therefore ships stale code to users while the repo looks correct — + * and the embedded copy is the one thing no typecheck covers. Same guarantee + * the pi-extension embed check gives. + */ +const repoRoot = join(import.meta.dirname, '..', '..', '..', '..'); +const opencodeExtDir = join(repoRoot, 'src', 'opencode-ext'); + +describe('embedded opencode plugin', () => { + it('matches the sources in src/opencode-ext (re-run scripts/generate-opencode-extension.ts)', () => { + for (const name of OPENCODE_EXTENSION_FILENAMES) { + const onDisk = readFileSync(join(opencodeExtDir, name), 'utf-8'); + expect(OPENCODE_EXTENSION_FILES[name], `${name} is embedded`).toBeDefined(); + expect(OPENCODE_EXTENSION_FILES[name], `${name} is stale — regenerate the embed`).toBe(onDisk); + } + }); + + it('embeds every file the plugin needs to load', () => { + // open-mcpctl.tsx imports ./projects.js — installing one without the other + // yields a plugin opencode cannot load. + expect(Object.keys(OPENCODE_EXTENSION_FILES).sort()).toEqual(['open-mcpctl.tsx', 'projects.ts']); + expect(OPENCODE_EXTENSION_FILES['open-mcpctl.tsx'] ?? '').toContain('./projects.js'); + }); + + it('carries the TUI plugin markers the opencode API requires', () => { + const main = OPENCODE_EXTENSION_FILES['open-mcpctl.tsx'] ?? ''; + // It is a TUI plugin (slots/keymap/dialog), not a server plugin. + expect(main).toContain('slots.register({'); + expect(main).toContain('keymap.registerLayer'); + expect(main).toContain('ui.dialog.replace'); + // The indicator targets the bottom status row beside the token counter. + expect(main).toContain('app_bottom'); + // The .tsx extension matters — opencode auto-discovery only matches {ts,js}. + expect(OPENCODE_EXTENSION_FILENAMES).toContain('open-mcpctl.tsx'); + }); +}); diff --git a/src/cli/tests/utils/opencode-projects.test.ts b/src/cli/tests/utils/opencode-projects.test.ts new file mode 100644 index 0000000..8d6829b --- /dev/null +++ b/src/cli/tests/utils/opencode-projects.test.ts @@ -0,0 +1,74 @@ +import { describe, it, expect } from 'vitest'; +import { filterProjects, toolChangeAnnouncement, MCP_SERVER_NAME } from '../../../opencode-ext/projects.js'; + +/** + * The opencode plugin filters a (potentially hundreds-long) project list before + * handing it to the TUI picker, active project first, then alphabetical. These + * are the pure helpers behind that — unit-tested instead of eyeballed through a + * TUI. + */ +const PROJECTS = [ + 'smoke-proj-none-mohimh46', + 'homeautomation', + 'docmost', + 'labctl', + 'mcpctl-development', + 'sre', + 'infra-prod', +]; + +describe('filterProjects', () => { + it('puts the active project first, then sorts alphabetically', () => { + const out = filterProjects(PROJECTS, '', 'docmost'); + expect(out[0]).toBe('docmost'); + const rest = out.slice(1); + expect([...rest].sort()).toEqual(rest); + }); + + it('keeps everything when the query is blank', () => { + expect(filterProjects(PROJECTS, '', null)).toHaveLength(PROJECTS.length); + expect(filterProjects(PROJECTS, '', null)).toEqual([...PROJECTS].sort()); + }); + + it('filters by substring, case-insensitively', () => { + expect(filterProjects(PROJECTS, 'homeauto', null)).toEqual(['homeautomation']); + expect(filterProjects(PROJECTS, 'HOMEAUTO', null)).toEqual(['homeautomation']); + expect(filterProjects(PROJECTS, 'infra', null)).toEqual(['infra-prod']); + }); + + it('requires ALL space-separated terms to match', () => { + // 'home auto' matches homeautomation (home + auto as substrings). + expect(filterProjects(PROJECTS, 'home auto', null)).toEqual(['homeautomation']); + // A term only present in one candidate prunes the rest. + expect(filterProjects(PROJECTS, 'home infra', null)).toEqual([]); + expect(filterProjects(PROJECTS, 'dev mcpctl', null)).toEqual(['mcpctl-development']); + }); + + it('returns empty when nothing matches', () => { + expect(filterProjects(PROJECTS, 'zzz', null)).toEqual([]); + }); +}); + +describe('toolChangeAnnouncement', () => { + it('describes the new tool set and the gated hint', () => { + const msg = toolChangeAnnouncement('docmost', ['mc_docmost_begin_session', 'mc_docmost_search']); + expect(msg).toContain("'docmost'"); + expect(msg).toContain('mc_docmost_search'); + expect(msg).toContain('begin_session'); + }); + + it('mentions gating when a begin_session tool is present', () => { + const msg = toolChangeAnnouncement('x', ['mc_x_begin_session']); + expect(msg).toContain('gated'); + }); + + it('says no tools are available when the list is empty', () => { + expect(toolChangeAnnouncement('y', [])).toContain('No mcpctl tools are currently available'); + }); +}); + +describe('MCP_SERVER_NAME', () => { + it('is the stable opencode bridge name', () => { + expect(MCP_SERVER_NAME).toBe('mcpctl'); + }); +}); diff --git a/src/cli/tests/utils/opencode-settings.test.ts b/src/cli/tests/utils/opencode-settings.test.ts new file mode 100644 index 0000000..6c89c53 --- /dev/null +++ b/src/cli/tests/utils/opencode-settings.test.ts @@ -0,0 +1,110 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir, homedir } from 'node:os'; +import { + registerWithOpenCode, + installEmbeddedExtension, + installExtensionFiles, + withOpenCodeDir, + writeOpenCodeProjectState, + opencodePluginDir, + opencodeConfigPath, + opencodeSkillsDir, +} from '../../src/utils/opencode-settings.js'; + +describe('opencode-settings util', () => { + let tmp: string; + + beforeEach(() => { + tmp = mkdtempSync(join(tmpdir(), 'mcpctl-opencode-')); + }); + afterEach(() => { + rmSync(tmp, { recursive: true, force: true }); + }); + + it('maps the opencode config dir to ~/.config/opencode paths', () => { + const base = join(homedir(), '.config', 'opencode'); + expect(opencodePluginDir()).toBe(join(base, 'plugin')); + expect(opencodeConfigPath()).toBe(join(base, 'opencode.json')); + expect(opencodeSkillsDir()).toBe(join(base, 'skill')); + }); + + it('registers the plugin into an empty opencode.json', async () => { + const cfg = join(tmp, 'opencode.json'); + const pluginPath = join(tmp, 'plugin', 'open-mcpctl.tsx'); + + const added = await registerWithOpenCode(cfg, pluginPath); + + expect(added).toBe(true); + const written = JSON.parse(readFileSync(cfg, 'utf-8')); + expect(written.plugin).toContain(`file://${pluginPath}`); + }); + + it('is idempotent — registering twice does not duplicate the plugin', async () => { + const cfg = join(tmp, 'opencode.json'); + const pluginPath = join(tmp, 'plugin', 'open-mcpctl.tsx'); + + await registerWithOpenCode(cfg, pluginPath); + const second = await registerWithOpenCode(cfg, pluginPath); + + expect(second).toBe(false); + const written = JSON.parse(readFileSync(cfg, 'utf-8')); + expect(written.plugin).toHaveLength(1); + }); + + it('preserves existing opencode.json keys (like a configured model)', async () => { + const cfg = join(tmp, 'opencode.json'); + mkdirSync(tmp, { recursive: true }); + writeFileSync(cfg, JSON.stringify({ model: 'itaz/deepseek-v4-think', mcp: { foo: {} } }, null, 2)); + + await registerWithOpenCode(cfg, '/abs/plugin/open-mcpctl.tsx'); + + const written = JSON.parse(readFileSync(cfg, 'utf-8')); + expect(written.model).toBe('itaz/deepseek-v4-think'); + expect(written.mcp).toEqual({ foo: {} }); + expect(written.plugin).toEqual(['file:///abs/plugin/open-mcpctl.tsx']); + }); + + it('writes the embedded plugin sources (self-contained, no source tree)', async () => { + const destDir = join(tmp, 'embedded'); + const written = await installEmbeddedExtension(destDir); + expect(written).toHaveLength(2); + expect(written.map((w) => w.replace(destDir + '/', ''))).toEqual(['open-mcpctl.tsx', 'projects.ts']); + const main = readFileSync(join(destDir, 'open-mcpctl.tsx'), 'utf-8'); + expect(main.length).toBeGreaterThan(1000); + // The main file imports its sibling helper. + expect(main).toContain('./projects.js'); + const projects = readFileSync(join(destDir, 'projects.ts'), 'utf-8'); + expect(projects).toContain('filterProjects'); + }); + + it('copies the plugin files from a source tree (dev path)', async () => { + const srcDir = join(tmp, 'src-opencode-ext'); + mkdirSync(srcDir, { recursive: true }); + writeFileSync(join(srcDir, 'open-mcpctl.tsx'), '// plugin tsx\n'); + writeFileSync(join(srcDir, 'projects.ts'), '// helper\n'); + const destDir = join(tmp, 'dest'); + + const written = await installExtensionFiles(srcDir, destDir); + expect(written).toHaveLength(2); + expect(readFileSync(join(destDir, 'open-mcpctl.tsx'), 'utf-8')).toBe('// plugin tsx\n'); + expect(readFileSync(join(destDir, 'projects.ts'), 'utf-8')).toBe('// helper\n'); + }); + + it('writes the persisted active-project state', async () => { + const statePath = join(tmp, 'state.json'); + const p = await writeOpenCodeProjectState('homelab', statePath); + expect(p).toBe(statePath); + expect(JSON.parse(readFileSync(statePath, 'utf-8'))).toEqual({ project: 'homelab' }); + }); + + it('withOpenCodeDir overrides all target paths', () => { + const oc = withOpenCodeDir(tmp); + expect(oc.pluginDir()).toBe(join(tmp, 'plugin')); + expect(oc.pluginMainPath()).toBe(join(tmp, 'plugin', 'open-mcpctl.tsx')); + expect(oc.skillsDir()).toBe(join(tmp, 'skill')); + expect(oc.depsDir()).toBe(join(tmp, 'node_modules')); + expect(oc.configPath()).toBe(join(tmp, 'opencode.json')); + }); +}); diff --git a/src/cli/tests/utils/pi-settings.test.ts b/src/cli/tests/utils/pi-settings.test.ts index 721edae..ce85bd0 100644 --- a/src/cli/tests/utils/pi-settings.test.ts +++ b/src/cli/tests/utils/pi-settings.test.ts @@ -20,6 +20,9 @@ describe('skills agentInstallRoot', () => { it('maps prime-agent to ~/.prime/agent/skills', () => { expect(agentInstallRoot('prime-agent')).toBe(join(homedir(), '.prime', 'agent', 'skills')); }); + it('maps opencode to ~/.config/opencode/skill', () => { + expect(agentInstallRoot('opencode')).toBe(join(homedir(), '.config', 'opencode', 'skill')); + }); }); describe('pi-settings util', () => { diff --git a/src/opencode-ext/open-mcpctl.tsx b/src/opencode-ext/open-mcpctl.tsx new file mode 100644 index 0000000..393c7d0 --- /dev/null +++ b/src/opencode-ext/open-mcpctl.tsx @@ -0,0 +1,249 @@ +/** + * mcpctl opencode TUI plugin — native integration (no reimplemented MCP client). + * + * opencode, unlike pi, has first-class MCP support: `mcpctl config opencode + * --project X` writes the active project's mcplocal endpoint into + * `opencode.json` under `mcp`, and opencode lists those tools natively. This + * plugin is the *UX* half that pi/prime-agent ship: it shows the active + * project in opencode's bottom status row (right where the token counter is) + * and lets the user switch projects from inside the TUI. + * + * Features: + * - a status indicator rendered into the `app_bottom` slot (the row directly + * beneath the session footer statusline) + * - a `DialogSelect` project picker (search-as-you-type) opened from the + * command palette (`mcpctl.switch_project`) or a leader binding + * - live project switching: rewrites ~/.mcpctl/opencode-state.json, updates + * the indicator, and re-points the running MCP backend via the SDK client + * (`mcp.disconnect` / `mcp.connect`) so tools change without a restart + * - toast notifications for success / gated-project hints + * + * State is persisted in `~/.mcpctl/opencode-state.json` (survives cwd) and the + * active project is also inferred from a `.mcpctl-project` marker walk-up. + * + * Loaded by placing this file (or the installed copy) under + * `~/.config/opencode/plugin/` (opencode auto-discovers `plugin/*.{ts,js}`). + * + * Only imports opencode-bundled types — no @mcpctl/*, no ~/.claude. + */ +/** @jsxImportSource @opentui/solid */ +import { createMemo, createSignal, Show } from "solid-js" +import type { JSX } from "@opentui/solid" +import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" +import { readFile, writeFile, mkdir } from "node:fs/promises" +import { join, dirname } from "node:path" +import { homedir } from "node:os" +import { filterProjects, MCP_SERVER_NAME, toolChangeAnnouncement } from "./projects.js" + +export const id = "mcpctl" + +// ── config / state ─────────────────────────────────────────────────────────── +const McpctlDir = join(homedir(), ".mcpctl") +const ConfigPath = join(McpctlDir, "config.json") +const CredPath = join(McpctlDir, "credentials") +const OpenCodeStatePath = join(McpctlDir, "opencode-state.json") +const ProjectMarker = ".mcpctl-project" + +interface OpenCodeState { + project?: string +} + +async function readJson(path: string): Promise { + try { + return JSON.parse(await readFile(path, "utf-8")) as T + } catch { + return null + } +} + +async function readConfig(): Promise<{ mcplocalUrl: string }> { + const cfg = await readJson<{ mcplocalUrl?: string }>(ConfigPath) + return { mcplocalUrl: cfg?.mcplocalUrl ?? "http://localhost:3200" } +} + +async function readCreds(): Promise<{ token?: string } | null> { + try { + const raw = await readFile(CredPath, "utf-8") + const parsed = JSON.parse(raw) as { token?: string } + if (!parsed.token) return null + return parsed + } catch { + return null + } +} + +async function readState(): Promise { + return (await readJson(OpenCodeStatePath)) ?? {} +} + +async function writeState(state: OpenCodeState): Promise { + await mkdir(McpctlDir, { recursive: true }) + await writeFile(OpenCodeStatePath, JSON.stringify(state, null, 2) + "\n", "utf-8") +} + +/** Walk up directories looking for a `.mcpctl-project` marker. */ +async function findProjectMarker(start = process.cwd()): Promise { + const home = homedir() + let dir = start + for (let i = 0; i < 50; i++) { + try { + const raw = await readFile(join(dir, ProjectMarker), "utf-8") + const project = raw.split("\n")[0]?.trim() ?? "" + if (project) return project + } catch { + // continue walking + } + if (dir === home || dir === "/") break + const parent = dirname(dir) + if (parent === dir) break + dir = parent + } + return null +} + +/** Request the list of projects from mcpd (via mcplocal API passthrough). */ +async function listProjects(mcplocalUrl: string, token?: string): Promise { + const url = `${mcplocalUrl}/api/v1/projects` + const headers: Record = { Accept: "application/json" } + if (token) headers.Authorization = `Bearer ${token}` + const res = await fetch(url, { headers }) + if (!res.ok) throw new Error(`list projects: HTTP ${res.status}`) + const body = (await res.json()) as Array<{ name: string }> | { data?: Array<{ name: string }> } + const arr = Array.isArray(body) ? body : (body.data ?? []) + return arr.map((p) => p.name) +} + +/** Streamable-HTTP endpoint for a project, exactly as `mcpctl mcp -p X` bridges to. */ +function projectEndpoint(mcplocalUrl: string, project: string): string { + return `${mcplocalUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(project)}/mcp` +} + +// ── the TUI plugin ─────────────────────────────────────────────────────────── +export const tui: TuiPlugin = async (api) => { + let mcplocalUrl = "http://localhost:3200" + let token: string | undefined + let activeProject: string | null = null + const [project, setProject] = createSignal(null) + + function refreshFromState(projectName: string | null): void { + activeProject = projectName + setProject(projectName) + } + + async function loadConnection(): Promise<{ project: string | null; mcplocalUrl: string; token?: string }> { + const cfg = await readConfig() + mcplocalUrl = cfg.mcplocalUrl + const creds = await readCreds() + token = creds?.token ?? undefined + const state = await readState() + const projectName = state.project ?? (await findProjectMarker()) + return { project: projectName, mcplocalUrl, token } + } + + /** Point opencode's live `mcpctl` MCP server at a project. */ + async function setMcpProject(next: string): Promise { + // Remove the old definition first so tools from the previous project can't + // linger alongside the new ones. + try { + await api.client.mcp.disconnect({ name: MCP_SERVER_NAME }) + } catch { + // not connected yet — fine + } + const endpoint = projectEndpoint(mcplocalUrl, next) + const headers: Record = {} + if (token) headers.Authorization = `Bearer ${token}` + await api.client.mcp.add({ name: MCP_SERVER_NAME, config: { type: "remote", url: endpoint, headers } }) + await api.client.mcp.connect({ name: MCP_SERVER_NAME }) + // opencode enumerates the project tools over native MCP; we surface the + // names we know from the bridge in the next-turn announcement. + return [] + } + + async function switchProject(next: string): Promise { + const tools: string[] = [] + try { + tools.push(...(await setMcpProject(next))) + } catch (e) { + api.ui.toast({ variant: "error", message: `mcpctl: could not connect '${next}' — ${(e as Error).message}` }) + return + } + refreshFromState(next) + await writeState({ project: next }) + api.ui.toast({ + variant: "success", + message: `Switched mcpctl project to '${next}' — MCP tools reloaded.`, + }) + void api.client.app.log({ service: "mcpctl", level: "info", message: `project switch -> ${next}` }) + } + + // ── project picker ── + function openPicker(): void { + void (async () => { + let projects: string[] = [] + try { + projects = await listProjects(mcplocalUrl, token) + } catch (e) { + api.ui.toast({ variant: "error", message: `mcpctl: failed to list projects — ${(e as Error).message}` }) + return + } + if (projects.length === 0) { + api.ui.toast({ variant: "warning", message: "mcpctl: no projects returned by mcpd" }) + return + } + const ordered = filterProjects(projects, "", activeProject) + api.ui.dialog.replace(() => ( + ({ + title: name, + value: name, + description: name === activeProject ? "active project" : undefined, + disabled: name === activeProject, + onSelect: () => void switchProject(name), + }))} + /> + )) + })() + } + + // ── command + keybind ── + api.keymap.registerLayer({ + commands: [ + { + name: "mcpctl.switch_project", + title: "mcpctl: switch project", + category: "mcpctl", + run: openPicker, + }, + ], + bindings: [{ key: "m", group: "mcpctl", desc: "switch mcpctl project", cmd: openPicker }], + }) + + // ── indicator slot (bottom status row, beside the token counter) ── + // Rendered into `app_bottom`: the row directly beneath the session footer + // statusline, i.e. where the token counter / mode / model info live. + const Indicator = (_props: unknown): JSX.Element => { + const label = createMemo(() => project() ?? "") + return ( + + + + mcpctl{" "} + {label()} + + + + ) + } + + api.slots.register({ + slots: { + app_bottom: Indicator, + }, + }) + + // bootstrap state + void loadConnection().then(({ project: p }) => refreshFromState(p)) +} diff --git a/src/opencode-ext/projects.ts b/src/opencode-ext/projects.ts new file mode 100644 index 0000000..0711187 --- /dev/null +++ b/src/opencode-ext/projects.ts @@ -0,0 +1,54 @@ +/** + * Pure, dependency-free helpers shared by the opencode plugin and its tests. + * + * Kept out of `open-mcpctl.tsx` so vitest can import them without a JSX / + * @opentui transform, and so the orchestration logic (which is the easy part + * to get wrong on ordering/filtering) is unit-tested rather than eyeballed + * through a TUI. + */ + +/** The MCP server name opencode uses for the mcpctl project bridge. */ +export const MCP_SERVER_NAME = "mcpctl"; + +/** + * Order and filter the project list for the picker. + * + * Ordering: the active project first (most likely pick), then alphabetical. + * Filtering: space-separated terms, ALL of which must appear as + * case-insensitive substrings — so `home auto` finds `homeautomation`. A blank + * query keeps everything. + */ +export function filterProjects(projects: string[], query: string, active: string | null): string[] { + const ordered = [...projects].sort((a, b) => { + if (a === active) return -1; + if (b === active) return 1; + return a.localeCompare(b); + }); + const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 0); + if (terms.length === 0) return ordered; + return ordered.filter((p) => { + const name = p.toLowerCase(); + return terms.every((t) => name.includes(t)); + }); +} + +/** + * The announcement message describing the post-switch tool set. + * + * Shown to the user (and, via a user-role injection, made visible to the model) + * so neither keeps calling the previous project's stale `mc_*` tool names. + */ +export function toolChangeAnnouncement(project: string, tools: string[]): string { + const gate = tools.find((n) => n.endsWith("_begin_session")); + const parts = [ + `[mcpctl] The active project is now '${project}'.`, + "mcpctl tool names listed earlier in this conversation belong to the previously active project and are no longer callable — ignore them.", + tools.length > 0 + ? `Currently available mcpctl tools: ${tools.join(", ")}.` + : "No mcpctl tools are currently available for this project.", + ]; + if (gate !== undefined) { + parts.push(`This project is gated: call ${gate} first and its remaining tools become available.`); + } + return parts.join(" "); +} diff --git a/src/opencode-ext/tsconfig.json b/src/opencode-ext/tsconfig.json new file mode 100644 index 0000000..fa70241 --- /dev/null +++ b/src/opencode-ext/tsconfig.json @@ -0,0 +1,30 @@ +{ + "//": [ + "The opencode plugin is shipped as source (embedded in the CLI, then written", + "into ~/.config/opencode/plugin/) and is therefore never compiled by the CLI's", + "own build. Without this project it was typechecked by nothing — exactly how", + "a slotted component with the wrong API shape would slip through.", + "", + "It is checked against the REAL @opencode-ai/plugin (@opencode-ai/plugin/tui)", + "@opentui/solid and solid-js types (all dev dependencies), not hand-written", + "shims, because a shim drifting from the published API is the exact failure", + "mode this guards against." + ], + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "types": ["node"], + "jsx": "preserve", + "jsxImportSource": "@opentui/solid", + "strict": true, + "noImplicitOverride": true, + "noUncheckedIndexedAccess": false, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "files": ["open-mcpctl.tsx", "projects.ts"] +}