diff --git a/apps/landing/src/components/landing/Activation.astro b/apps/landing/src/components/landing/Activation.astro index 5742a519..7392baf3 100644 --- a/apps/landing/src/components/landing/Activation.astro +++ b/apps/landing/src/components/landing/Activation.astro @@ -11,15 +11,16 @@ import { exampleCaplets } from "../../data/landing"; ---
-
+
-

Setup

+

First Caplet

- Start with the smallest useful Caplet. + Start where auth cannot get in the way.

- caplets setup wires the agent integrations you choose. Add OSV first because it needs no auth; bring in GitHub or Sourcegraph after the discovery path feels right. + Copy setup from the hero, then add OSV first because it needs no auth. Bring in GitHub or Sourcegraph after the discovery path feels right.

+ - - -
-
-
-
- { - primaryHeroCommand && ( - <> - - {primaryHeroCommand.command} - + + + + {setupOptions.map((option) => {option.label})} + + { + setupOptions.map((option) => ( + +
+
+
+
+ {option.lines.map((line) => {line})} +
+
- - ) - } -
-

Requires Node 24+. Run setup from the install section next.

-
+
+
+ + )) + } + + +
+ +
diff --git a/apps/landing/src/data/landing.ts b/apps/landing/src/data/landing.ts index 994d05e2..e0508ee0 100644 --- a/apps/landing/src/data/landing.ts +++ b/apps/landing/src/data/landing.ts @@ -11,6 +11,17 @@ export const heroCommands = [ export const quickstartCommand = heroCommands.map((item) => item.command).join("\n"); +export const manualSetupCommands = { + install: "npm install -g caplets", + setup: "caplets setup", +} as const; + +export const manualSetupCommand = Object.values(manualSetupCommands).join("\n"); + +export const agentSetupPrompt = `Read and follow this Caplets bootstrap skill: https://raw.githubusercontent.com/spiritledsoftware/caplets/main/skills/installing-caplets/SKILL.md + +Set up Caplets for this environment. Detect the environment first. Do not install packages, modify config, start remote login, or write files until you have asked me the setup questions, shown the exact commands and files/config areas you plan to change, and I approve that plan.`; + export const proofStats = [ { value: "10/10", diff --git a/apps/landing/src/scripts/copy.ts b/apps/landing/src/scripts/copy.ts index 64682b24..85697637 100644 --- a/apps/landing/src/scripts/copy.ts +++ b/apps/landing/src/scripts/copy.ts @@ -27,7 +27,8 @@ async function copyValue(button: HTMLButtonElement) { ? mobileValue : button.dataset.copyValue; if (!value) return; - const copyValue = attributedLandingCommand(value); + const copyValue = + button.dataset.copyAttribution === "false" ? value : attributedLandingCommand(value); if (!navigator.clipboard?.writeText) { setCopyFeedback(button, "Copy unavailable", 2200); diff --git a/apps/landing/test/activation-links.test.ts b/apps/landing/test/activation-links.test.ts index d5918582..2d5d8299 100644 --- a/apps/landing/test/activation-links.test.ts +++ b/apps/landing/test/activation-links.test.ts @@ -16,4 +16,26 @@ describe("activation links", () => { 'href="https://github.com/spiritledsoftware/caplets/tree/main/caplets"', ); }); + + it("offers manual and agent setup modes without rendering the full agent prompt", () => { + const componentSource = readFileSync( + join(repoRoot, "apps/landing/src/components/landing/Hero.astro"), + "utf8", + ); + const dataSource = readFileSync(join(repoRoot, "apps/landing/src/data/landing.ts"), "utf8"); + + expect(componentSource).toContain('id="setup"'); + expect(componentSource).toContain(" { ); }); + it("can copy raw prompt text without command attribution", async () => { + document.body.innerHTML = ` + +
+ `; + + await import("../src/scripts/copy"); + document.querySelector("button")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + await Promise.resolve(); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith("Read this setup skill"); + }); + it("loads without initializing providers when env is absent", async () => { await expect(import("../src/scripts/observability")).resolves.toBeDefined(); }); diff --git a/packages/core/src/cli.ts b/packages/core/src/cli.ts index 6ac55d9f..30428568 100644 --- a/packages/core/src/cli.ts +++ b/packages/core/src/cli.ts @@ -2641,18 +2641,20 @@ export function createProgram(io: CliIO = {}): Command { .command(cliCommands.doctor) .description("Diagnose Caplets local, remote, and project-sync configuration.") .option("--json", "print JSON output") - .action(async (options: { json?: boolean }) => { + .option("--format ", "output format: plain, markdown, md, or json", parseOutputFormat) + .action(async (options: { json?: boolean; format?: CliOutputFormat }) => { const doctorOptions = { env, ...(io.fetch ? { fetch: io.fetch } : {}), ...(io.authDir ? { authDir: io.authDir } : {}), ...(io.daemon ? { daemon: io.daemon } : {}), }; - if (options.json) { + const format = options.format ?? (options.json ? "json" : "plain"); + if (format === "json") { writeOut(`${JSON.stringify(await doctorJsonReport(doctorOptions), null, 2)}\n`); return; } - writeOut(await formatDoctorReport(doctorOptions)); + writeOut(await formatDoctorReport(doctorOptions, format)); }); const vault = program.command(cliCommands.vault).description("Manage Caplets Vault values."); diff --git a/packages/core/src/cli/doctor.ts b/packages/core/src/cli/doctor.ts index 0d82d1d3..d73dfe33 100644 --- a/packages/core/src/cli/doctor.ts +++ b/packages/core/src/cli/doctor.ts @@ -106,8 +106,13 @@ export async function doctorJsonReport(options: DoctorOptions = {}): Promise { +export async function formatDoctorReport( + options: DoctorOptions = {}, + format: "plain" | "markdown" = "plain", +): Promise { const report = await doctorJsonReport(options); + if (format === "markdown") return formatDoctorMarkdownReport(report); + const lines = [ "Server hosting", ` Configured: ${yesNo(Boolean(report.server.configured))}`, @@ -200,6 +205,101 @@ export async function formatDoctorReport(options: DoctorOptions = {}): Promise>).map( + (issue) => `- ${issue.capletId}: ${issue.reason} ${issue.key} (${issue.recoveryCommand})`, + ) + : []), + "", + "## Exposure", + `- Default: ${report.exposure.default ?? "unknown"}`, + `- Discovery timeout: ${report.exposure.discoveryTimeoutMs ?? "unknown"}ms`, + `- Discovery concurrency: ${report.exposure.discoveryConcurrency ?? "unknown"}`, + `- Callable native tools: ${report.exposure.callableNativeToolCount ?? 0}`, + ...(Array.isArray(report.exposure.caplets) + ? (report.exposure.caplets as Array>).map( + (caplet) => + `- ${caplet.id}: ${caplet.exposure} (${caplet.callable ? "callable" : `hidden: ${caplet.hiddenReason}`})`, + ) + : []), + "", + "## Code Mode", + `- Types generation: ${doctorOk(report.codeMode.typesGeneration)}`, + `- Diagnostics: ${doctorOk(report.codeMode.diagnostics)}`, + `- Sandbox smoke: ${doctorOk(report.codeMode.sandboxSmoke)}`, + `- Log storage: ${doctorOk(report.codeMode.logStorage)}`, + `- Callable index: ${doctorOk(report.codeMode.callableIndex)}`, + `- Observed output shapes: ${doctorOk(report.codeMode.observedOutputShapes)}`, + ...(observedOutputShapePath(report.codeMode.observedOutputShapes) + ? [ + `- Observed output shape cache: ${observedOutputShapePath(report.codeMode.observedOutputShapes)}`, + ] + : []), + ]; + return `${lines.join("\n")}\n`; +} + function resolveVaultSection( env: NodeJS.ProcessEnv | Record, cwd: string = process.cwd(), diff --git a/packages/core/test/doctor-cli.test.ts b/packages/core/test/doctor-cli.test.ts index 5c375340..319dc9b8 100644 --- a/packages/core/test/doctor-cli.test.ts +++ b/packages/core/test/doctor-cli.test.ts @@ -95,6 +95,32 @@ describe("caplets doctor", () => { }); }); + it("supports shared format aliases", async () => { + const jsonOut: string[] = []; + const markdownOut: string[] = []; + const plainOut: string[] = []; + + await runCli(["doctor", "--format", "json"], { + env: {}, + writeOut: (value) => jsonOut.push(value), + }); + await runCli(["doctor", "--format", "md"], { + env: {}, + writeOut: (value) => markdownOut.push(value), + }); + await runCli(["doctor", "--format", "plain"], { + env: {}, + writeOut: (value) => plainOut.push(value), + }); + + expect(JSON.parse(jsonOut.join(""))).toMatchObject({ + server: { configured: false }, + remote: { configured: false }, + }); + expect(markdownOut.join("")).toContain("## Server hosting"); + expect(plainOut.join("")).toContain("Server hosting"); + }); + it("reports ambiguous Cloud Remote Profiles instead of throwing", async () => { const path = tempCloudAuthPath(); const authDir = dirname(path); diff --git a/skills/installing-caplets/SKILL.md b/skills/installing-caplets/SKILL.md new file mode 100644 index 00000000..41523bf7 --- /dev/null +++ b/skills/installing-caplets/SKILL.md @@ -0,0 +1,144 @@ +--- +name: installing-caplets +description: Use when a user wants to install, set up, bootstrap, configure, wire, or troubleshoot Caplets for an agent, MCP client, Pi, OpenCode, Codex, Claude Code, self-hosted remote runtime, or local daemon-first setup. +--- + +# Installing Caplets + +## Bootstrap promise + +Run a never-surprise install. Detect first, ask before mutating, show one exact plan, then execute only after the user approves that plan. + +This skill may be loaded from a raw GitHub URL. When a troubleshooting branch says to read a reference, fetch the absolute URL shown in that branch. + +## Immediate read-only probe + +Run broad read-only detection immediately. Do not install packages, edit config, start login, or write files in this phase. + +Use the relevant safe probes for the current OS and shell: + +```sh +pwd +uname -a 2>/dev/null || true +command -v node npm pnpm bun npx caplets pi opencode codex claude 2>/dev/null || true +node --version 2>/dev/null || true +npm --version 2>/dev/null || true +pnpm --version 2>/dev/null || true +bun --version 2>/dev/null || true +caplets --version 2>/dev/null || true +caplets doctor --format json 2>/dev/null || caplets doctor 2>/dev/null || true +``` + +Also inspect only relevant sections of existing agent config files when present. Look for Caplets entries, MCP server entries, native plugin/package entries, and top-level `caplets` settings. Redact secrets in summaries. Do not dump full config files into chat. + +Completion criterion: you can state whether the Caplets CLI exists, which package managers exist, which agent/client appears active or installed, whether a Caplets config/daemon already exists, and whether the current directory appears project-specific (`.caplets/` or project config present). + +## Ask setup questions + +Ask after detection and before mutation. Recommend the detected answer, but let the user choose. + +Ask only what is needed: + +1. **Target agent/client.** Include detected current agent first. Name first-class targets: Pi, OpenCode, Codex, Claude Code, and generic MCP clients. You may mention popular MCP clients as generic clients when detection suggests them, but do not invent unsupported client IDs. +2. **Runtime shape.** Offer local daemon-first setup and self-hosted remote setup. Hide Caplets Cloud until Cloud is actually available. +3. **Package manager/install method** if `caplets` is missing. Detect available package managers, then ask whether to use a global install such as `npm install -g caplets`, an equivalent preferred package manager, or temporary `npx` use. +4. **Scope.** Default to user/global setup. Ask about project-local setup only when `.caplets/`, `CAPLETS_PROJECT_CONFIG`, or the user’s request indicates repo-specific Caplets. +5. **Remote URL** only for self-hosted remote setup. Do not ask for secrets, tokens, passwords, API keys, or credential values in chat. + +Completion criterion: every mutating choice in the plan has an explicit user answer or a clearly accepted default from the question flow. + +## Build the exact plan + +Prefer explicit non-interactive commands. Use interactive `caplets setup` only as a fallback when explicit setup cannot represent the chosen target. + +Before executing, show a plan with: + +- commands to run, in order; +- files or config areas expected to change; +- whether the command is local daemon-first or self-hosted remote; +- restart/reload steps for the agent; +- rollback notes for changed config when practical. + +Use dry runs when the CLI is already available: + +```sh +caplets setup --dry-run --format json +``` + +Common explicit setup commands after approval: + +```sh +caplets setup pi --yes --format json +caplets setup opencode --yes --format json +caplets setup codex --yes --format json +caplets setup claude-code --yes --format json +caplets setup mcp-client --client --yes --format json +``` + +For self-hosted remote setup, establish trust first, then configure attach: + +```sh +caplets remote login +caplets setup --remote-url --yes --format json +``` + +For generic remote MCP clients, the CLI may require writing an output config rather than guessing the client’s storage: + +```sh +caplets setup mcp-client --remote-url --output ./caplets.mcp.json --yes --format json +``` + +Completion criterion: the user approves the exact plan after seeing the commands and expected mutations. If they change any choice, update the plan and ask for approval again. + +## Execute and verify + +Run the approved commands. If a command prompts interactively despite the plan, pause and ask unless the prompt is already answered by the approved plan and is not asking for secrets. + +Verify both CLI/runtime and agent wiring: + +```sh +caplets --version +caplets doctor --format json 2>/dev/null || caplets doctor +``` + +Then verify the selected target: + +- **Pi:** confirm `@caplets/pi` is installed/configured or `caplets setup pi` reported success; tell the user to restart/reload Pi if needed and look for Caplets native tools such as `caplets__code_mode` or the status widget when applicable. +- **OpenCode:** confirm `@caplets/opencode` plugin/defaults or setup output; tell the user to restart OpenCode if plugin tools were newly added. +- **Codex/Claude Code/generic MCP:** confirm the Caplets MCP server entry exists and runs `caplets attach `; tell the user to restart/reload the client and confirm the MCP server connects. +- **Self-hosted remote:** confirm `caplets remote status` or `caplets doctor` shows a saved remote profile for the URL before claiming remote setup is usable. + +Success requires CLI + agent check. Do not report completion from package installation alone. + +After setup succeeds, offer optional starter Caplets as a follow-up, not as part of first install. Example no-auth starter: + +```sh +caplets install spiritledsoftware/caplets osv +``` + +## Troubleshooting branches + +On failure, read only the matching reference by URL, then run focused diagnostics and propose one fix plan. + +- CLI missing, package-manager failure, Node version, or PATH issue: `https://raw.githubusercontent.com/spiritledsoftware/caplets/main/skills/installing-caplets/references/troubleshooting-cli.md` +- Daemon start, health, port, logs, or `caplets doctor` daemon failure: `https://raw.githubusercontent.com/spiritledsoftware/caplets/main/skills/installing-caplets/references/troubleshooting-daemon.md` +- Agent/MCP/native config, add-mcp, restart, or tool visibility failure: `https://raw.githubusercontent.com/spiritledsoftware/caplets/main/skills/installing-caplets/references/troubleshooting-agent-config.md` +- Self-hosted remote login, attach URL, approval, or remote profile failure: `https://raw.githubusercontent.com/spiritledsoftware/caplets/main/skills/installing-caplets/references/troubleshooting-remote.md` + +Safe auto-fixes without a second approval are limited to reversible/no-user-data changes inside Caplets-owned state: retry a daemon start, rerun the already-approved `caplets setup --yes` after a transient failure, refresh detection, or correct a Caplets-owned generated config entry that the approved plan already covered. + +Return to an exact plan for approval before installing packages, editing third-party agent config beyond the approved plan, changing shell profile/PATH files, logging into remotes, deleting files, overwriting existing config, or handling secrets. + +## Secret handling + +Never ask the user to paste secrets, tokens, API keys, passwords, OAuth codes, or credential values into chat. Route credentials through Caplets-owned flows such as Remote Login, provider OAuth/device flows, Vault, or environment setup. It is fine to ask for non-secret URLs, package-manager preferences, target agents, and scope choices. + +## Landing-page prompt + +Use this copyable prompt for raw bootstrap distribution: + +```text +Read and follow this Caplets bootstrap skill: https://raw.githubusercontent.com/spiritledsoftware/caplets/main/skills/installing-caplets/SKILL.md + +Set up Caplets for this environment. Detect the environment first. Do not install packages, modify config, start remote login, or write files until you have asked me the setup questions, shown the exact commands and files/config areas you plan to change, and I approve that plan. +``` diff --git a/skills/installing-caplets/references/troubleshooting-agent-config.md b/skills/installing-caplets/references/troubleshooting-agent-config.md new file mode 100644 index 00000000..95ffbd89 --- /dev/null +++ b/skills/installing-caplets/references/troubleshooting-agent-config.md @@ -0,0 +1,37 @@ +# Troubleshooting Agent Configuration + +Use this reference when Caplets CLI/daemon is healthy but the selected agent does not show Caplets tools, the MCP server is disconnected, native plugins are missing, or setup cannot write agent config. + +## Diagnose + +Start with the setup output and inspect only relevant config sections: + +- MCP entries named `caplets`. +- Commands shaped as `caplets attach `. +- Pi package/settings entries for `npm:@caplets/pi` or top-level `caplets` settings. +- OpenCode plugin entries for `@caplets/opencode`. + +Run a dry-run for the selected target when possible: + +```sh +caplets setup --dry-run --format json +``` + +For generic MCP clients, use the exact client ID supported by `caplets setup mcp-client --client ` or write a fallback config: + +```sh +caplets setup mcp-client --output ./caplets.mcp.json --dry-run --format json +``` + +## Fix patterns + +- **Config write failed:** keep the daemon if it is healthy. Propose rerunning the integration-only setup for the chosen client. +- **Unsupported client ID:** do not invent IDs. Ask the user to choose a supported client or use `--output ./caplets.mcp.json` and import manually. +- **MCP server present but disconnected:** confirm the command is `caplets attach `, the URL is reachable or logged in, and the client was restarted/reloaded. +- **Pi native tools missing:** confirm `caplets setup pi` or `pi install npm:@caplets/pi` occurred, then restart/reload Pi. Look for `caplets__code_mode` or a Caplets status widget when applicable. +- **OpenCode native tools missing:** confirm the plugin entry and restart OpenCode; newly added native tools may require restart. +- **PATH differs inside agent:** prefer daemon-first setup because the agent only launches `caplets attach`; if it still cannot find `caplets`, propose an absolute command path or fixing the agent launch environment. + +## Approval boundary + +Editing third-party agent config requires user approval unless the exact edit was already in the approved plan. Restart/reload instructions do not require approval. diff --git a/skills/installing-caplets/references/troubleshooting-cli.md b/skills/installing-caplets/references/troubleshooting-cli.md new file mode 100644 index 00000000..9752cc43 --- /dev/null +++ b/skills/installing-caplets/references/troubleshooting-cli.md @@ -0,0 +1,36 @@ +# Troubleshooting Caplets CLI Installation + +Use this reference when `caplets` is missing, package installation fails, Node/package-manager checks fail, or the CLI installs but is not on PATH. + +## Diagnose + +Run read-only checks first: + +```sh +command -v caplets node npm pnpm bun npx 2>/dev/null || true +node --version 2>/dev/null || true +npm --version 2>/dev/null || true +pnpm --version 2>/dev/null || true +bun --version 2>/dev/null || true +npm config get prefix 2>/dev/null || true +npm bin -g 2>/dev/null || true +``` + +If `caplets` exists, verify it before reinstalling: + +```sh +caplets --version +caplets doctor --format json 2>/dev/null || caplets doctor +``` + +## Fix patterns + +- **No Node/npm:** explain that Caplets is distributed as an npm CLI. Ask before installing Node or changing system package managers. +- **Old Node:** Caplets requires modern Node. Ask the user how they manage Node (`nvm`, `fnm`, `volta`, system package manager) before changing it. +- **Global install rejected by permissions:** prefer a user-owned Node prefix or the user’s version manager. Do not use `sudo npm install -g` unless the user explicitly asks. +- **CLI installed but not found:** identify the global bin path and propose the exact shell profile edit. Ask before editing dotfiles. +- **User prefers no global mutation:** use `npx -y caplets ...` for the approved commands, then ask whether to install persistently later. + +## Approval boundary + +Package installation, Node installation, and shell profile edits require an exact plan and user approval. Retrying a failed read-only command does not. diff --git a/skills/installing-caplets/references/troubleshooting-daemon.md b/skills/installing-caplets/references/troubleshooting-daemon.md new file mode 100644 index 00000000..44ec8de4 --- /dev/null +++ b/skills/installing-caplets/references/troubleshooting-daemon.md @@ -0,0 +1,29 @@ +# Troubleshooting Caplets Daemon Setup + +Use this reference for local daemon-first failures: daemon install/start/status, health checks, port conflicts, and daemon sections in `caplets doctor`. + +## Diagnose + +```sh +caplets doctor --format json 2>/dev/null || caplets doctor +caplets daemon status 2>/dev/null || true +caplets daemon logs 2>/dev/null || true +``` + +If doctor reports a health URL, check it without exposing unrelated network data: + +```sh +curl -fsS 2>/dev/null || true +``` + +## Fix patterns + +- **Daemon not running:** retry the already-approved daemon start/setup command once. This is a safe auto-fix when the user already approved local setup. +- **Port conflict:** do not kill processes automatically. Identify the conflict and propose either changing Caplets serve defaults or freeing the port. +- **Config invalid:** keep the last known-good behavior in mind. Show the relevant Caplets config path and error, then propose the smallest edit. +- **Permission/cache issue:** diagnose the Caplets-owned cache/config path. Ask before deleting or changing ownership. +- **Health fails after start:** collect daemon logs and doctor output, then propose one fix plan. + +## Success check + +Local setup is not verified until `caplets doctor` reports a healthy daemon or setup output confirms the daemon URL and the selected agent is configured to run `caplets attach ` or native daemon defaults. diff --git a/skills/installing-caplets/references/troubleshooting-remote.md b/skills/installing-caplets/references/troubleshooting-remote.md new file mode 100644 index 00000000..3b59fac2 --- /dev/null +++ b/skills/installing-caplets/references/troubleshooting-remote.md @@ -0,0 +1,28 @@ +# Troubleshooting Self-Hosted Remote Setup + +Use this reference for self-hosted remote attach, `caplets remote login `, approval flows, Remote Profiles, and remote URL health failures. Caplets Cloud is hidden until it is available. + +## Diagnose + +```sh +caplets remote status 2>/dev/null || true +caplets doctor --format json 2>/dev/null || caplets doctor +``` + +Check only the non-secret remote URL and profile status. Do not ask the user to paste credentials or tokens. + +## Fix patterns + +- **No Remote Profile:** run `caplets remote login ` only after the user approved the remote setup plan. +- **Approval pending:** tell the user to approve from the self-hosted server/operator side using the instructions printed by the CLI. Do not ask them to paste possession secrets into chat. +- **Wrong URL/base path:** verify the URL the user provided and use the same URL in `caplets remote login ` and `caplets setup --remote-url `. +- **Remote MCP client config before login:** login first, then write agent config. A config pointing at an untrusted remote will look broken. +- **Generic remote MCP client:** if the CLI refuses to guess a client storage path, write an output config and have the user import it: + ```sh + caplets setup mcp-client --remote-url --output ./caplets.mcp.json --yes --format json + ``` +- **Credential leak risk:** Remote credentials belong in Caplets Remote Profiles, not environment variables, settings files, source code, or chat. + +## Success check + +Remote setup is verified only after Remote Login succeeds, `caplets remote status` or `caplets doctor` shows the profile, and the selected agent config points to `caplets attach ` or native remote settings using that same URL.