diff --git a/README.md b/README.md index 69a9d743..b1e4f0a3 100644 --- a/README.md +++ b/README.md @@ -66,26 +66,28 @@ Install the DevSpace CLI: npm install -g @waishnav/devspace ``` -Then initialize and start the server: +Then initialize DevSpace: ```bash devspace init -devspace serve ``` Or run it without a global install: ```bash npx @waishnav/devspace init -npx @waishnav/devspace serve ``` During setup, DevSpace asks for: -- the local project folders ChatGPT is allowed to open through DevSpace -- the local port, usually `7676` -- your public HTTPS base URL from Cloudflare Tunnel, ngrok, Pinggy, Tailscale Funnel, or - another reverse proxy +- where you will use it: ChatGPT, Coding Agents, or both +- which Coding Agents DevSpace may use + +If you select ChatGPT, setup also asks which local project folders it may open +and for your public HTTPS base URL from Cloudflare Tunnel, ngrok, Pinggy, +Tailscale Funnel, or another reverse proxy. A Coding Agents-only setup asks +neither question: local commands use the current Git project, or the current +directory outside a repository. Use the public origin without `/mcp` during setup: @@ -94,6 +96,8 @@ https://your-tunnel-host.example.com ``` You will configure your MCP client with the public `/mcp` URL after setup. +Run `devspace serve` when using ChatGPT. For Coding Agents, setup prints a +`skills` command and lets the Skills CLI handle installation. When the client connects, DevSpace opens an Owner password approval page. Enter the Owner password printed by `devspace init`. It is also stored in: diff --git a/docs/chatgpt-coding-workflow.md b/docs/chatgpt-coding-workflow.md index 1ec7fe57..d7a5d13c 100644 --- a/docs/chatgpt-coding-workflow.md +++ b/docs/chatgpt-coding-workflow.md @@ -116,7 +116,7 @@ DevSpace discovers standard Agent Skills from: It also keeps compatibility with: -- the bundled `subagent-delegation` skill when `DEVSPACE_SUBAGENTS=1`, unless `~/.devspace/skills/subagent-delegation/SKILL.md` exists +- the bundled `subagents` skill when Subagents are enabled, unless `~/.devspace/skills/subagents/SKILL.md` exists - `DEVSPACE_AGENT_DIR/skills`, defaulting to `~/.codex/skills` - additional paths from `DEVSPACE_SKILL_PATHS` @@ -140,13 +140,13 @@ Skill paths may be outside the workspace. DevSpace only permits reading: - advertised `SKILL.md` files - files under a skill directory after that skill's `SKILL.md` has been read -Set `DEVSPACE_SKILLS=0` to hide skills from workspace output. Set -`DEVSPACE_SUBAGENTS=1` to expose the experimental subagent catalog and -`subagent-delegation` skill. That skill teaches the minimal -`devspace agents ls`, `devspace agents run`, `devspace agents continue`, and -`devspace agents show` -workflow. The catalog comes from `open_workspace`; `devspace agents ls` lists -existing subagent sessions for that workspace. +Set `DEVSPACE_SKILLS=0` to hide skills from workspace output. Enable Subagents +and choose providers through `devspace init` or the persisted provider +configuration. The bundled `subagents` skill teaches the minimal +`devspace agents targets`, `devspace agents ls`, `devspace agents run`, +`devspace agents continue`, and `devspace agents show` workflow. The catalog +comes from `open_workspace`; `devspace agents ls` lists existing subagent +sessions for that workspace. ## Tool Names diff --git a/docs/configuration.md b/docs/configuration.md index 997e5b5d..56e1b204 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -136,7 +136,7 @@ DevSpace discovers standard Agent Skills from: It also keeps compatibility with: -- the bundled `subagent-delegation` skill when `DEVSPACE_SUBAGENTS=1`, unless `~/.devspace/skills/subagent-delegation/SKILL.md` exists +- the bundled `subagents` skill when Subagents are enabled, unless `~/.devspace/skills/subagents/SKILL.md` exists - `DEVSPACE_AGENT_DIR/skills`, defaulting to `~/.codex/skills` - additional paths from `DEVSPACE_SKILL_PATHS` @@ -185,10 +185,18 @@ descriptions, providers, and optional models/effort levels so the host model can agent without reading provider-specific launch details. Disabled or unavailable providers and their profiles are omitted from this model-facing catalog. `devspace agents ls` lists existing subagent sessions for the current workspace, scoped by the -workspace environment injected into shell commands. The `subagent-delegation` +workspace environment injected into shell commands. The `subagents` skill teaches the model to use only the minimal `devspace agents ls`, -`devspace agents run`, `devspace agents continue`, and `devspace agents show` -workflow. +`devspace agents targets`, `devspace agents run`, `devspace agents continue`, +and `devspace agents show` workflow. + +For Codex, Claude Code, OpenCode, Pi, or another supported Coding Agent, use +the Skills CLI to install the same skill. DevSpace setup prints this command but +does not run it or write into agent skill directories: + +```bash +npx skills add Waishnav/devspace --skill subagents --global +``` Starter profile templates are available under `examples/agents/`. Copy or adapt them into one of the active profile directories before use. diff --git a/docs/gotchas.md b/docs/gotchas.md index 5769d3e0..495243bb 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -154,9 +154,11 @@ DevSpace does not currently prune workspace sessions, conversation bindings, or review refs. A future product retention policy will define safe cleanup for these records; no automatic deletion is performed today. -## Workspace Path Rejected +## MCP Workspace Path Rejected -The path must be inside one of the allowed roots configured during setup. +The path passed to `open_workspace` must be inside one of the allowed roots +configured during ChatGPT setup. Direct `devspace agents` commands instead use +the current local project and are not gated by MCP allowed roots. Run: @@ -216,21 +218,31 @@ DevSpace looks in standard Agent Skills locations: It also checks compatibility and custom paths: -- the bundled `subagent-delegation` skill when `DEVSPACE_SUBAGENTS=1`, unless `~/.devspace/skills/subagent-delegation/SKILL.md` exists +- the bundled `subagents` skill when Subagents are enabled, unless `~/.devspace/skills/subagents/SKILL.md` exists - `DEVSPACE_AGENT_DIR/skills`, defaulting to `~/.codex/skills` - additional paths from `DEVSPACE_SKILL_PATHS` -When `DEVSPACE_SUBAGENTS=1`, DevSpace loads agent profiles from +When Subagents are enabled, DevSpace loads agent profiles from `~/.devspace/agents/*.md` and project `.devspace/agents/*.md`, then exposes a compact profile catalog through `open_workspace`. The bundled -`subagent-delegation` skill keeps the model-facing workflow to -`devspace agents ls`, `devspace agents run`, `devspace agents continue`, and -`devspace agents show`. +`subagents` skill keeps the model-facing workflow to +`devspace agents targets`, `devspace agents ls`, `devspace agents run`, +`devspace agents continue`, and `devspace agents show`. Those commands automatically manage the internal local agent daemon; `devspace serve` is not a prerequisite. `devspace agents ls` lists existing subagent sessions, not profile definitions. +For a Coding Agent, run the installation command printed by +`devspace init`: + +```bash +npx skills add Waishnav/devspace --skill subagents --global +``` + +The Skills CLI handles agent discovery and installation. DevSpace setup does +not copy files into agent skill directories. + Packaged agent profile examples under `examples/agents/` are starter templates. Copy or adapt them into one of the active profile directories before use. diff --git a/docs/setup.md b/docs/setup.md index e332f216..934b0b8c 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -1,7 +1,6 @@ # Setup Guide -This guide is for users who want ChatGPT or another MCP host to work in local -projects through DevSpace. +This guide covers ChatGPT and Coding Agents using DevSpace with local projects. ## Requirements @@ -9,10 +8,12 @@ projects through DevSpace. - npm - Git - Bash, including Git Bash or WSL on Windows -- a public HTTPS URL that forwards to the local DevSpace server +- a public HTTPS URL that forwards to the local DevSpace server, only when + ChatGPT will connect -DevSpace does not create the public tunnel for you. Use Cloudflare Tunnel, -ngrok, Pinggy, Tailscale Funnel, or your own HTTPS reverse proxy. +DevSpace does not create the public tunnel for you. ChatGPT users can use +Cloudflare Tunnel, ngrok, Pinggy, Tailscale Funnel, or their own HTTPS reverse +proxy. ## Install And Configure @@ -24,10 +25,13 @@ npx @waishnav/devspace init The setup flow asks one question at a time. -### Project Roots +First choose where you will use DevSpace: ChatGPT, Coding Agents, or both. +DevSpace uses that answer to skip setup that does not apply to you. -Choose the folders ChatGPT is allowed to open through DevSpace. Keep this -narrow. +### Project roots + +If you selected ChatGPT, choose the project folders it may open through +DevSpace. Keep this narrow. Examples: @@ -43,20 +47,31 @@ Examples: C:\Users\alice\dev,C:\Users\alice\work ``` -### Local Port +A Coding Agents-only setup skips this question. Direct `devspace agents` +commands use the current Git project, or the current directory outside a +repository, with the authority of your local shell. MCP workspace operations +remain limited to the roots configured for ChatGPT. -The default is `7676`. +### Coding Agents -The local MCP URL is: +Setup detects supported Coding Agents and asks which ones DevSpace may use. +These choices are stored as provider objects under `subagents` in +`~/.devspace/config.json`. -```text -http://127.0.0.1:7676/mcp +If you selected Coding Agents, setup prints: + +```bash +npx skills add Waishnav/devspace --skill subagents --global ``` -### Public Base URL +The Skills CLI asks which installed Coding Agents should receive the skill. +The skill uses `devspace agents targets`, `run`, `continue`, `show`, and `ls`. +These commands do not require `devspace serve`. + +### Connect ChatGPT -Start your tunnel or reverse proxy before entering this value. Point the tunnel -at: +Setup only asks for a public URL if you selected ChatGPT. Start your tunnel or +reverse proxy first and point it at: ```text http://127.0.0.1:7676 @@ -74,6 +89,8 @@ Configure the MCP client with the full MCP endpoint: https://your-tunnel-host.example.com/mcp ``` +A Coding Agents-only setup skips this section. + ## Start The Server Run: diff --git a/package.json b/package.json index a87531b4..b6e62669 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "dev": "node scripts/dev-server.mjs", "postinstall": "node scripts/fix-node-pty-permissions.mjs", "start": "node dist/cli.js serve", - "test": "tsx src/config.test.ts && tsx src/cli-workspace.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-config.test.ts && tsx src/local-agent-catalog.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-daemon-lifecycle.test.ts && tsx src/local-agent-daemon-protocol.test.ts && tsx src/local-agent-daemon.test.ts && tsx src/local-agent-codex.test.ts && tsx src/local-agent-opencode.test.ts && tsx src/local-agent-acp.test.ts && tsx src/local-agent-pi-sandbox.test.ts && tsx src/local-agent-pi.test.ts && tsx src/local-agent-claude.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/local-agent-manager.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts", + "test": "tsx src/config.test.ts && tsx src/onboarding.test.ts && tsx src/cli-workspace.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-config.test.ts && tsx src/local-agent-catalog.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-daemon-lifecycle.test.ts && tsx src/local-agent-daemon-protocol.test.ts && tsx src/local-agent-daemon.test.ts && tsx src/local-agent-codex.test.ts && tsx src/local-agent-opencode.test.ts && tsx src/local-agent-acp.test.ts && tsx src/local-agent-pi-sandbox.test.ts && tsx src/local-agent-pi.test.ts && tsx src/local-agent-claude.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/local-agent-manager.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts", "typecheck": "tsc -p tsconfig.json --noEmit" }, "keywords": [], diff --git a/skills/subagent-delegation/SKILL.md b/skills/subagent-delegation/SKILL.md deleted file mode 100644 index 24fc7266..00000000 --- a/skills/subagent-delegation/SKILL.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -name: subagent-delegation -description: Delegate coding tasks to user-configured DevSpace subagents. ---- - -# Subagent Delegation - -Use this skill when the user explicitly asks to delegate work to another coding -agent, use a named subagent, get a second opinion, compare approaches, or run -a subagent-like workflow. - -Do not use subagents silently. Tell the user when another subagent is -being used. - -## Core commands - -Use only these commands for normal delegation: - -```bash -devspace agents ls -devspace agents run "" -devspace agents continue "" -devspace agents show -``` - -`ls` shows existing subagent sessions for the current workspace. DevSpace scopes -it automatically from the shell environment injected by the workspace tool. -Use the returned logical `agt_...` ID with `continue`; provider session IDs and -prefixes are not interchangeable with logical agent IDs. - -`run ""` starts a new configured profile and prints a -DevSpace agent id. - -`run ""` starts a raw built-in provider when no configured -profile is needed. Built-in providers are listed by `open_workspace`. - -`continue ""` sends a follow-up to an existing agent. Do not use -`run ` for continuation. - -Continuation supports the same per-turn model and effort overrides: - -```bash -devspace agents continue --model "" -devspace agents continue --effort "" -``` - -`show ` prints status and the latest response. If the agent is still -running, `show` waits briefly. If there is still no final response, call `show` -again later. - -The commands automatically start the internal `devspace-agentd` process when -needed. `devspace serve` is not required for local-agent execution. The daemon -owns shared agent sessions and provider runtimes for the configured DevSpace -state directory. - -Do not run provider CLIs such as `codex`, `claude`, `opencode`, `pi`, -`cursor-agent`, or `copilot` directly unless you are explicitly debugging -DevSpace agent integration. - -## Choosing a profile - -Choose profiles from the compact subagent profile catalog returned by -`open_workspace`. Use the profile name with `devspace agents run`. If no -profile fits and delegation is still appropriate, use a built-in provider name -from `open_workspace`. - -Profiles may declare a model and optional effort level. To override the -configured/default provider model or effort level for a run, pass `--model` -or `--effort`: - -```bash -devspace agents run --model "" -devspace agents run --effort "" -``` - -Use `--effort` only when the user asks for a specific reasoning depth or when -the task clearly needs a different effort than the configured profile default. -Effort values are provider-specific passthrough values. Use names supported by -the selected local agent harness; DevSpace does not translate values between -providers. - -Good delegation targets: - -- `reviewer`: second opinion, bug risk, security risk, test gaps. -- `explorer`: read-only codebase investigation. -- `implementer`: focused implementation when the user asked for delegation. - -Do not delegate ordinary coding work just because a profile exists. Use normal -DevSpace tools unless the user asked for delegation, another agent's opinion, -parallel work, or a named subagent. - -## Worker prompts - -Agents start with only the prompt you send plus their configured profile -instructions. Make prompts self-contained. - -Implementation prompt shape: - -```text -Goal: - - -Context: - - -Relevant files: - - -Acceptance criteria: -- - -Rules: -- Keep changes focused. -- Do not perform unrelated refactors. -- Report blockers clearly. -``` - -Read-only investigation prompt shape: - -```text -Question: - - -Scope: - - -Rules: -- Do not modify files. -- Cite relevant file paths and symbols. -- Separate facts from guesses. -``` - -## After the worker responds - -Always review the result before presenting it as verified. - -For write-capable tasks, inspect changed files and run or explain relevant -tests. For read-only tasks, verify that important claims are supported by repo -evidence. - -Be transparent in the final response: - -```text -I used . It reported . I verified . Remaining risk: -. -``` - -Never hide that a subagent was used. diff --git a/skills/subagents/SKILL.md b/skills/subagents/SKILL.md new file mode 100644 index 00000000..b799ade2 --- /dev/null +++ b/skills/subagents/SKILL.md @@ -0,0 +1,54 @@ +--- +name: subagents +description: Delegate focused coding, research, review, or verification work to a bounded DevSpace subagent. Use when a task benefits from separate context, a specialist perspective, or a follow-up with the same worker. +--- + +# DevSpace subagents + +Use the DevSpace CLI through the shell or process tool. Run commands from the project the subagent should work on. + +## Choose a target + +Discover usable targets instead of guessing names: + +```bash +devspace agents targets --json +``` + +Configured profiles include a description and may define provider, model, effort, and task instructions. Choose a matching profile when one fits. Use a provider target when no profile fits or a specific provider is needed. + +Usually rely on the target's configured model and effort. Pass `--model` or `--effort` only with a value supported by that provider. DevSpace passes these values through without translating them between providers. + +## Start work + +Give the subagent a self-contained brief. Include the objective, relevant paths, constraints, decisions it needs from the current conversation, and the expected result. The subagent receives the brief and its profile instructions, not the parent conversation. + +```bash +devspace agents run "" --json +devspace agents run --model --effort "" --json +``` + +The result contains a DevSpace agent `id` and its current status. Execution continues independently, so retain the ID for later inspection or follow-up. + +## Inspect and continue + +```bash +devspace agents show --json +devspace agents continue "" --json +devspace agents ls --json +``` + +- `show` waits briefly for active work, then returns the current status and any + available response or error. +- `continue` gives the same subagent another turn with its existing provider + session and context. +- `ls` returns sessions belonging to the current project. + +Call `show --json` again later while the status is `starting` or `running`. `idle` means the response is ready. `error` and `stopped` are terminal without a successful response. Continue an agent when its existing context is useful; start another agent for unrelated work. + +## Good uses + +- Review a change for correctness, security, or missing tests. +- Investigate a bounded part of a codebase and report findings. +- Implement one isolated change with clear acceptance criteria. +- Run a focused verification pass after other work. \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index 148571b4..4c15a0a5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -8,6 +8,7 @@ import { getShellConfig } from "@earendil-works/pi-coding-agent"; import { satisfies } from "semver"; import { loadConfig } from "./config.js"; import { resolveCliWorkspaceContext } from "./cli-workspace.js"; +import { resolveSubagentsConfig } from "./local-agent-config.js"; import { getLocalAgentProviderAvailabilitySnapshot, } from "./local-agent-availability.js"; @@ -18,6 +19,7 @@ import { formatLocalAgentProviderStatusSummary, } from "./local-agent-catalog.js"; import { loadLocalAgentProfiles } from "./local-agent-profiles.js"; +import type { LocalAgentProvider } from "./local-agent-profiles.js"; import { parseLocalAgentContinueArgs, parseLocalAgentRunArgs, @@ -26,10 +28,16 @@ import { createLocalAgentClient } from "./local-agent-client.js"; import { toAgentErrorPayload, type LocalAgentError } from "./local-agent-errors.js"; import type { LocalAgentRecord } from "./local-agent-store.js"; import { - ensureDevspaceDefaultSkills, + type OnboardingDestination, + SUBAGENT_SKILL_INSTALL_COMMAND, + resolveOnboardingUsage, + updateOnboardingSubagentsConfig, + usesChatGpt, + usesCodingAgents, +} from "./onboarding.js"; +import { generateOwnerToken, loadDevspaceFiles, - resolveSubagentsFlag, writeDevspaceAuth, writeDevspaceConfig, type DevspaceUserConfig, @@ -94,7 +102,7 @@ async function ensureConfigured(): Promise { "Run:", " devspace init", "", - "Or provide DEVSPACE_OAUTH_OWNER_TOKEN and DEVSPACE_ALLOWED_ROOTS.", + "Or provide DEVSPACE_OAUTH_OWNER_TOKEN.", ].join("\n"), ); } @@ -113,78 +121,140 @@ async function runInit({ force }: { force: boolean }): Promise { try { prompts.intro("DevSpace setup"); - const defaultRoots = files.config.allowedRoots?.join(", ") || process.cwd(); - const rootsAnswer = await textPrompt({ - message: `Where are your projects located? Press Enter to use ${defaultRoots}`, - placeholder: defaultRoots, - defaultValue: defaultRoots, - validate: (value) => value?.trim() ? undefined : "Enter at least one project root.", - }); - const allowedRoots = rootsAnswer - .split(",") - .map((root) => resolve(expandHomePath(root.trim()))) - .filter(Boolean); - - const defaultPort = String(files.config.port ?? 7676); - const portAnswer = await textPrompt({ - message: `Which local port should DevSpace use? Press Enter to use ${defaultPort}`, - placeholder: defaultPort, - defaultValue: defaultPort, - validate: validatePort, + const destinationAnswer = await prompts.multiselect({ + message: "Where will you use DevSpace?", + options: [ + { + value: "chatgpt", + label: "ChatGPT", + hint: "Connect ChatGPT to projects on this computer.", + }, + { + value: "coding-agents", + label: "Coding Agents", + hint: "Use DevSpace from Codex, Claude Code, OpenCode, Pi, and similar tools.", + }, + ], + initialValues: files.config.publicBaseUrl ? ["chatgpt"] : ["coding-agents"], + required: true, }); - const port = Number(portAnswer); + if (prompts.isCancel(destinationAnswer)) throw new SetupCancelledError(); + const usage = resolveOnboardingUsage(destinationAnswer as OnboardingDestination[]); + const useChatGpt = usesChatGpt(usage); + const useCodingAgents = usesCodingAgents(usage); + + let allowedRoots: string[] | undefined; + if (useChatGpt) { + const defaultRoots = files.config.allowedRoots?.join(", ") || process.cwd(); + const rootsAnswer = await textPrompt({ + message: `Which project folders can DevSpace access? Press Enter to use ${defaultRoots}`, + placeholder: defaultRoots, + defaultValue: defaultRoots, + validate: (value) => value?.trim() ? undefined : "Enter at least one project root.", + }); + allowedRoots = rootsAnswer + .split(",") + .map((root) => resolve(expandHomePath(root.trim()))) + .filter(Boolean); + } - prompts.note( - [ - "DevSpace needs a public base URL so ChatGPT or Claude can reach this MCP server.", - "Create a tunnel or reverse proxy with Cloudflare Tunnel, ngrok, Pinggy, Tailscale Funnel, or your own HTTPS proxy.", - "Paste the public origin here, without /mcp.", - "", - "Example: https://your-tunnel-host.example.com", - ].join("\n"), - "Public URL required", + const port = isValidPort(files.config.port) ? files.config.port : 7676; + + let publicBaseUrl: string | null = null; + if (useChatGpt) { + prompts.note( + [ + `Point your HTTPS tunnel or reverse proxy to http://127.0.0.1:${port}.`, + "Paste its public URL below.", + "", + "Example: https://your-tunnel-host.example.com", + ].join("\n"), + "Connect ChatGPT", + ); + publicBaseUrl = normalizePublicBaseUrl(await textPrompt({ + message: files.config.publicBaseUrl + ? `What public URL will ChatGPT connect to? Press Enter to keep ${files.config.publicBaseUrl}` + : "What public URL will ChatGPT connect to?", + placeholder: files.config.publicBaseUrl ?? "https://your-tunnel-host.example.com", + defaultValue: files.config.publicBaseUrl ?? "", + validate: validateRequiredPublicBaseUrl, + })); + } + + const currentSubagents = resolveSubagentsConfig(files.config.subagents, {}); + const availability = getLocalAgentProviderAvailabilitySnapshot(); + const configuredProviders = currentSubagents.providers + .filter((provider) => provider.enabled) + .map((provider) => provider.id); + const initialValues = configuredProviders.length > 0 + ? configuredProviders + : availability + .filter((provider) => provider.available) + .map((provider) => provider.name); + const providerAnswer = await prompts.multiselect({ + message: "Which Coding Agents should be available?", + options: availability.map((provider) => ({ + value: provider.name, + label: provider.name, + hint: provider.available + ? provider.note ?? "available" + : `unavailable: ${provider.reason ?? "provider preflight failed"}`, + })), + initialValues, + required: true, + }); + if (prompts.isCancel(providerAnswer)) throw new SetupCancelledError(); + const selectedProviders = providerAnswer as LocalAgentProvider[]; + const subagents = updateOnboardingSubagentsConfig( + currentSubagents, + selectedProviders, ); - const publicBaseUrl = normalizePublicBaseUrl(await textPrompt({ - message: files.config.publicBaseUrl - ? `What is the public base URL? Press Enter to keep ${files.config.publicBaseUrl}` - : "What is the public base URL?", - placeholder: files.config.publicBaseUrl ?? "https://your-tunnel-host.example.com", - defaultValue: files.config.publicBaseUrl ?? "", - validate: validateRequiredPublicBaseUrl, - })); const config: DevspaceUserConfig = { + ...files.config, host: files.config.host ?? "127.0.0.1", port, - allowedRoots, + ...(allowedRoots ? { allowedRoots } : {}), publicBaseUrl, - subagents: files.config.subagents, + subagents, }; const auth = { ownerToken: files.auth.ownerToken ?? generateOwnerToken(), }; - const configPath = writeDevspaceConfig(config); - const authPath = writeDevspaceAuth(auth); - const seededSkillPaths = resolveSubagentsFlag(config) ? ensureDevspaceDefaultSkills() : []; + writeDevspaceConfig(config); + writeDevspaceAuth(auth); const lines = [ - `Config: ${configPath}`, - `Auth: ${authPath}`, - ...seededSkillPaths.map((path) => `Default skill: ${path}`), - `Local MCP URL: http://${config.host}:${config.port}/mcp`, - ...(publicBaseUrl ? [`Public MCP URL: ${publicBaseUrl}/mcp`] : []), + ...(allowedRoots ? [`Project folders: ${allowedRoots.join(", ")}`] : []), + `Coding Agents: ${selectedProviders.join(", ")}`, + ...(publicBaseUrl ? [`ChatGPT connection URL: ${publicBaseUrl}/mcp`] : []), ]; - prompts.note(lines.join("\n"), "DevSpace configured"); - prompts.note( - [ - `Owner password: ${auth.ownerToken}`, - "Use this when ChatGPT or Claude asks you to approve DevSpace access.", - `Stored at: ${authPath}`, - ].join("\n"), - "Owner password", - ); - prompts.outro("Run `devspace serve` to start the MCP server."); + prompts.note(lines.join("\n"), "DevSpace is ready"); + if (useChatGpt) { + prompts.note( + [ + `Owner password: ${auth.ownerToken}`, + "Use this when ChatGPT asks you to approve DevSpace access.", + ].join("\n"), + "Owner password", + ); + } + if (useCodingAgents) { + prompts.note( + [ + SUBAGENT_SKILL_INSTALL_COMMAND, + "", + "The Skills CLI will let you choose which Coding Agents receive it.", + ].join("\n"), + "Install the Subagents skill", + ); + } + const nextSteps = [ + useChatGpt ? "Run `devspace serve`, then connect ChatGPT." : undefined, + useCodingAgents ? "Run the skill command above before delegating from your Coding Agents." : undefined, + ].filter(Boolean).join(" "); + prompts.outro(nextSteps); } catch (error) { if (error instanceof SetupCancelledError) { prompts.cancel("Setup cancelled"); @@ -601,11 +671,8 @@ async function textPrompt(options: TextPromptOptions): Promise { return value || options.defaultValue; } -function validatePort(value: string | undefined): string | undefined { - const port = Number(value); - return Number.isInteger(port) && port >= 1 && port <= 65535 - ? undefined - : "Enter a port between 1 and 65535."; +function isValidPort(value: unknown): value is number { + return Number.isInteger(value) && Number(value) >= 1 && Number(value) <= 65535; } function validateRequiredPublicBaseUrl(value: string | undefined): string | undefined { diff --git a/src/config.test.ts b/src/config.test.ts index 5e2b5f7f..dbf028d3 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -1,9 +1,8 @@ import assert from "node:assert/strict"; -import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { loadConfig } from "./config.js"; -import { ensureDevspaceDefaultSkills, resolveSubagentsFlag } from "./user-config.js"; const emptyConfigDir = mkdtempSync(join(tmpdir(), "devspace-empty-config-test-")); const baseEnv = { @@ -39,18 +38,6 @@ assert.deepEqual(loadConfig({ ...baseEnv, DEVSPACE_SUBAGENTS: "1" }).subagents, enabled: true, providers: [], }); -assert.equal(resolveSubagentsFlag({}, {}), undefined); -assert.equal(resolveSubagentsFlag({ subagents: true }, {}), true); -assert.equal(resolveSubagentsFlag({ subagents: true }, { DEVSPACE_SUBAGENTS: "0" }), false); -assert.equal(resolveSubagentsFlag({}, { DEVSPACE_SUBAGENTS: "1" }), true); - -const seededConfigDir = mkdtempSync(join(tmpdir(), "devspace-seeded-skills-test-")); -const seededSkillPaths = ensureDevspaceDefaultSkills({ DEVSPACE_CONFIG_DIR: seededConfigDir }); -assert.deepEqual(seededSkillPaths, [join(seededConfigDir, "skills", "subagent-delegation", "SKILL.md")]); -assert.equal(existsSync(seededSkillPaths[0]), true); -assert.match(readFileSync(seededSkillPaths[0], "utf8"), /name: subagent-delegation/); -assert.deepEqual(ensureDevspaceDefaultSkills({ DEVSPACE_CONFIG_DIR: seededConfigDir }), []); - assert.throws( () => loadConfig({ ...baseEnv, DEVSPACE_WIDGETS: "invalid" }), /Invalid DEVSPACE_WIDGETS: invalid/, diff --git a/src/local-agent-availability.test.ts b/src/local-agent-availability.test.ts index 9690843a..3dcf4ba6 100644 --- a/src/local-agent-availability.test.ts +++ b/src/local-agent-availability.test.ts @@ -10,7 +10,7 @@ import { assert.equal(availability.name, "codex"); assert.equal(typeof availability.available, "boolean"); if (availability.available) { - assert.match(availability.note ?? "", /app-server support is verified on first run/); + assert.equal(availability.note, "available"); } } @@ -41,8 +41,8 @@ import { assert.equal( formatLocalAgentProviderAvailabilitySummary([ - { name: "codex", available: true, note: "executable detected; app-server support is verified on first run" }, + { name: "codex", available: true, note: "available" }, { name: "pi", available: false, reason: "pi executable not found" }, ]), - "available: codex (executable detected; app-server support is verified on first run); unavailable: pi (pi executable not found)", + "available: codex (available); unavailable: pi (pi executable not found)", ); diff --git a/src/local-agent-availability.ts b/src/local-agent-availability.ts index 9064aa21..2c8dd6e1 100644 --- a/src/local-agent-availability.ts +++ b/src/local-agent-availability.ts @@ -85,7 +85,7 @@ function codexAvailability(env: NodeJS.ProcessEnv): LocalAgentProviderAvailabili return availability.available ? { ...availability, - note: "executable detected; app-server support is verified on first run", + note: "available", } : availability; } diff --git a/src/onboarding.test.ts b/src/onboarding.test.ts new file mode 100644 index 00000000..5d0ab4a6 --- /dev/null +++ b/src/onboarding.test.ts @@ -0,0 +1,53 @@ +import assert from "node:assert/strict"; +import { + resolveOnboardingUsage, + SUBAGENT_SKILL_INSTALL_COMMAND, + updateOnboardingSubagentsConfig, + usesChatGpt, + usesCodingAgents, +} from "./onboarding.js"; + +assert.equal(resolveOnboardingUsage(["chatgpt"]), "chatgpt"); +assert.equal(resolveOnboardingUsage(["coding-agents"]), "coding-agents"); +assert.equal(resolveOnboardingUsage(["coding-agents", "chatgpt"]), "both"); +assert.equal(usesChatGpt("both"), true); +assert.equal(usesCodingAgents("both"), true); +assert.equal(usesChatGpt("coding-agents"), false); +assert.equal(usesCodingAgents("chatgpt"), false); +assert.throws(() => resolveOnboardingUsage([]), /Choose ChatGPT, Coding Agents, or both/); + +assert.deepEqual( + updateOnboardingSubagentsConfig( + { enabled: false, providers: [] }, + ["codex", "claude"], + ), + { + enabled: true, + providers: [ + { id: "codex", enabled: true }, + { id: "claude", enabled: true }, + ], + }, +); + +const configured = { + enabled: true, + providers: [ + { id: "codex" as const, enabled: true, model: "gpt-5.4", effort: "high" }, + { id: "claude" as const, enabled: true, model: "sonnet" }, + ], +}; +assert.deepEqual( + updateOnboardingSubagentsConfig(configured, ["claude"]), + { + enabled: true, + providers: [ + { id: "codex", enabled: false, model: "gpt-5.4", effort: "high" }, + { id: "claude", enabled: true, model: "sonnet" }, + ], + }, +); +assert.equal( + SUBAGENT_SKILL_INSTALL_COMMAND, + "npx skills add Waishnav/devspace --skill subagents --global", +); diff --git a/src/onboarding.ts b/src/onboarding.ts new file mode 100644 index 00000000..2642e776 --- /dev/null +++ b/src/onboarding.ts @@ -0,0 +1,50 @@ +import type { SubagentsConfig } from "./local-agent-config.js"; +import { + LOCAL_AGENT_PROVIDERS, + type LocalAgentProvider, +} from "./local-agent-profiles.js"; + +export const SUBAGENT_SKILL_INSTALL_COMMAND = + "npx skills add Waishnav/devspace --skill subagents --global"; + +export const ONBOARDING_DESTINATIONS = ["chatgpt", "coding-agents"] as const; +export type OnboardingDestination = typeof ONBOARDING_DESTINATIONS[number]; +export type OnboardingUsage = OnboardingDestination | "both"; + +export function resolveOnboardingUsage( + destinations: readonly OnboardingDestination[], +): OnboardingUsage { + const selected = new Set(destinations); + if (selected.has("chatgpt") && selected.has("coding-agents")) return "both"; + if (selected.has("chatgpt")) return "chatgpt"; + if (selected.has("coding-agents")) return "coding-agents"; + throw new Error("Choose ChatGPT, Coding Agents, or both."); +} + +export function usesChatGpt(usage: OnboardingUsage): boolean { + return usage === "chatgpt" || usage === "both"; +} + +export function usesCodingAgents(usage: OnboardingUsage): boolean { + return usage === "coding-agents" || usage === "both"; +} + +export function updateOnboardingSubagentsConfig( + current: SubagentsConfig, + selectedProviders: readonly LocalAgentProvider[], +): SubagentsConfig { + const selected = new Set(selectedProviders); + return { + enabled: true, + providers: LOCAL_AGENT_PROVIDERS + .filter((id) => selected.has(id) || current.providers.some((provider) => provider.id === id)) + .map((id) => { + const existing = current.providers.find((provider) => provider.id === id); + return { + ...existing, + id, + enabled: selected.has(id), + }; + }), + }; +} diff --git a/src/server.test.ts b/src/server.test.ts index 8266a0fd..cb29d11c 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -20,7 +20,7 @@ import { WorkspaceRegistry } from "./workspaces.js"; const execFileAsync = promisify(execFile); test("open_workspace keeps lifecycle flags out of model output and preserves complete card metadata", async (t) => { - const providerNote = "app-server support is verified on first run"; + const providerNote = "available"; const context = await fixture(t, { localAgentProviders: [{ name: "codex", available: true, note: providerNote }], }); diff --git a/src/skills.test.ts b/src/skills.test.ts index 707dda26..9db16a10 100644 --- a/src/skills.test.ts +++ b/src/skills.test.ts @@ -31,10 +31,10 @@ try { await mkdir(join(projectClaudeSkills, "claude-project-skill"), { recursive: true }); await mkdir(join(projectRoot, ".pi", "skills", "project-skill"), { recursive: true }); await mkdir(join(agentDir, "skills", "global-skill"), { recursive: true }); - await mkdir(join(agentDir, "skills", "subagent-delegation"), { recursive: true }); + await mkdir(join(agentDir, "skills", "subagents"), { recursive: true }); await mkdir(join(explicitSkills, "duplicate"), { recursive: true }); await mkdir(join(explicitSkills, "disabled"), { recursive: true }); - await mkdir(join(explicitSkills, "subagent-delegation"), { recursive: true }); + await mkdir(join(explicitSkills, "subagents"), { recursive: true }); await mkdir(join(devspaceSkills, "devspace-local-skill"), { recursive: true }); await writeFile( @@ -126,10 +126,10 @@ try { ].join("\n"), ); await writeFile( - join(agentDir, "skills", "subagent-delegation", "SKILL.md"), + join(agentDir, "skills", "subagents", "SKILL.md"), [ "---", - "name: subagent-delegation", + "name: subagents", "description: Hidden subagent skill winner.", "---", "", @@ -137,10 +137,10 @@ try { ].join("\n"), ); await writeFile( - join(explicitSkills, "subagent-delegation", "SKILL.md"), + join(explicitSkills, "subagents", "SKILL.md"), [ "---", - "name: subagent-delegation", + "name: subagents", "description: Hidden subagent skill loser.", "---", "", @@ -184,13 +184,13 @@ try { assert.equal(loaded.skills.some((skill) => skill.name === "claude-project-skill"), true); assert.equal(loaded.skills.some((skill) => skill.name === "project-skill"), false); assert.equal(loaded.skills.some((skill) => skill.name === "devspace-local-skill"), true); - assert.equal(loaded.skills.some((skill) => skill.name === "subagent-delegation"), false); + assert.equal(loaded.skills.some((skill) => skill.name === "subagents"), false); assert.equal(loaded.skills.filter((skill) => skill.name === "duplicate-skill").length, 1); assert.equal(loaded.skills.some((skill) => skill.name === "hidden-skill"), true); assert.equal(loaded.diagnostics.some((diagnostic) => diagnostic.type === "collision"), true); assert.equal( loaded.diagnostics.some( - (diagnostic) => diagnostic.collision?.name === "subagent-delegation", + (diagnostic) => diagnostic.collision?.name === "subagents", ), false, ); @@ -204,7 +204,7 @@ try { }); assert.equal( loadWorkspaceSkills(experimentalConfig, projectRoot).skills.some( - (skill) => skill.name === "subagent-delegation", + (skill) => skill.name === "subagents", ), true, ); diff --git a/src/skills.ts b/src/skills.ts index eb9691e3..cf4fa332 100644 --- a/src/skills.ts +++ b/src/skills.ts @@ -21,15 +21,15 @@ export interface SkillReadResolution { isSkillFile: boolean; } -const SUBAGENT_DELEGATION_NAME = "subagent-delegation"; -const SUBAGENT_DELEGATION_SKILL = join(SUBAGENT_DELEGATION_NAME, "SKILL.md"); +const SUBAGENTS_SKILL_NAME = "subagents"; +const SUBAGENTS_SKILL = join(SUBAGENTS_SKILL_NAME, "SKILL.md"); function bundledSkillsDir(): string { return fileURLToPath(new URL("../skills", import.meta.url)); } -function hasSubagentDelegationSkill(skillDir: string): boolean { - return existsSync(join(skillDir, SUBAGENT_DELEGATION_SKILL)); +function hasSubagentsSkill(skillDir: string): boolean { + return existsSync(join(skillDir, SUBAGENTS_SKILL)); } export function effectiveSkillPaths(config: ServerConfig, cwd: string): string[] { @@ -39,7 +39,7 @@ export function effectiveSkillPaths(config: ServerConfig, cwd: string): string[] resolve(cwd, ".agents", "skills"), config.devspaceSkillsDir, join(config.agentDir, "skills"), - config.subagents.enabled && !hasSubagentDelegationSkill(config.devspaceSkillsDir) + config.subagents.enabled && !hasSubagentsSkill(config.devspaceSkillsDir) ? bundledSkills : undefined, ]; @@ -74,10 +74,10 @@ export function loadWorkspaceSkills(config: ServerConfig, cwd: string): LoadedSk if (config.subagents.enabled) return result; return { - skills: result.skills.filter((skill) => skill.name !== SUBAGENT_DELEGATION_NAME), + skills: result.skills.filter((skill) => skill.name !== SUBAGENTS_SKILL_NAME), diagnostics: result.diagnostics.filter((diagnostic) => { const collision = diagnostic.collision; - return !(collision?.resourceType === "skill" && collision.name === SUBAGENT_DELEGATION_NAME); + return !(collision?.resourceType === "skill" && collision.name === SUBAGENTS_SKILL_NAME); }), }; } diff --git a/src/user-config.ts b/src/user-config.ts index f2e681f2..98d05ac6 100644 --- a/src/user-config.ts +++ b/src/user-config.ts @@ -6,12 +6,9 @@ import { writeFileSync, } from "node:fs"; import { homedir } from "node:os"; -import { dirname, join, resolve } from "node:path"; +import { join, resolve } from "node:path"; import { expandHomePath } from "./roots.js"; -import { - resolveSubagentsConfig, - type StoredSubagentsConfig, -} from "./local-agent-config.js"; +import type { StoredSubagentsConfig } from "./local-agent-config.js"; export interface DevspaceUserConfig { host?: string; @@ -103,24 +100,6 @@ export function generateOwnerToken(): string { return randomBytes(32).toString("base64url"); } -export function ensureDevspaceDefaultSkills(env: NodeJS.ProcessEnv = process.env): string[] { - const targetPath = join(devspaceSkillsDir(env), "subagent-delegation", "SKILL.md"); - if (existsSync(targetPath)) return []; - - const sourcePath = new URL("../skills/subagent-delegation/SKILL.md", import.meta.url); - mkdirSync(dirname(targetPath), { recursive: true }); - writeFileSync(targetPath, readFileSync(sourcePath, "utf8"), { mode: 0o644 }); - return [targetPath]; -} - -export function resolveSubagentsFlag( - config: Pick, - env: NodeJS.ProcessEnv = process.env, -): boolean | undefined { - if (config.subagents === undefined && env.DEVSPACE_SUBAGENTS === undefined) return undefined; - return resolveSubagentsConfig(config.subagents, env).enabled; -} - function readJsonFile(filePath: string): T { try { return JSON.parse(readFileSync(filePath, "utf8")) as T;