diff --git a/README.md b/README.md index 4e6c99a..f18188e 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@
-Chorus +Polyphony -# Chorus +# Polyphony **A second opinion (and a third) before you ship AI-written code — using the AI subscriptions you already pay for.** 2–3 _different_ AI tools review the same change in parallel, only green-lighting when they agree. Runs on your existing Claude Pro / ChatGPT Plus / Gemini Advanced — typical review costs **$0** out of pocket. -[![CI](https://github.com/chorus-codes/chorus/actions/workflows/ci.yml/badge.svg)](https://github.com/chorus-codes/chorus/actions/workflows/ci.yml) -[![npm](https://img.shields.io/npm/v/chorus-codes?color=22c55e)](https://www.npmjs.com/package/chorus-codes) +> Formerly **Chorus**. The `chorus` CLI binary, `~/.chorus/` config directory, and `chorus` MCP registration all still work — see [Compatibility](#compatibility) below. + +[![CI](https://github.com/crypticpy/chorus/actions/workflows/ci.yml/badge.svg)](https://github.com/crypticpy/chorus/actions/workflows/ci.yml) +[![npm](https://img.shields.io/npm/v/@crypticpy/polyphony?color=22c55e)](https://www.npmjs.com/package/@crypticpy/polyphony) [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](./LICENSE) [![Status](https://img.shields.io/badge/status-v0.8-brightgreen)](./ROADMAP.md) [![Node](https://img.shields.io/badge/node-%E2%89%A520-339933)](https://nodejs.org/) @@ -22,7 +24,7 @@
-Six AI reviewers streaming verdicts in parallel — Chorus running live +Six AI reviewers streaming verdicts in parallel — Polyphony running live **One AI writes. Three review. You ship only when they agree — using AI subscriptions you already pay for.** @@ -30,7 +32,7 @@ --- -## The problem Chorus solves +## The problem Polyphony solves 🤖 **AI coding tools are confident — and wrong about 5% of the time** in subtle ways that are hard to spot until production. @@ -40,16 +42,16 @@ Asking GPT to review GPT's work is theatre — same training, same biases. 💸 **Multi-AI review on raw API keys gets expensive fast.** Every diff × 3 reviewers × pay-per-token = real money. So nobody does it routinely. -### Chorus fixes all three +### Polyphony fixes all three ✅ **Different vendors review each other.** Claude writes, GPT and Gemini check it. Different blind spots cover each other. Disagreement = red flag _before_ you merge. ✅ **Uses your existing AI subscriptions.** -You're already paying for Claude Pro / ChatGPT Plus / Gemini Advanced (~$20/mo each). Chorus drives them headlessly through their CLIs — every multi-AI review costs **$0 out of pocket**, just counts against the quota you already have. Per-token API users save 10-100× vs running the same prompts directly. +You're already paying for Claude Pro / ChatGPT Plus / Gemini Advanced (~$20/mo each). Polyphony drives them headlessly through their CLIs — every multi-AI review costs **$0 out of pocket**, just counts against the quota you already have. Per-token API users save 10-100× vs running the same prompts directly. ✅ **Local-first, zero markup.** -Your code never reaches a new vendor. Chorus runs on your laptop, talks to the AI tools you already trust, and shuts up. Open source, Apache-2.0. +Your code never reaches a new vendor. Polyphony runs on your laptop, talks to the AI tools you already trust, and shuts up. Open source, Apache-2.0. That's the whole pitch. @@ -59,23 +61,23 @@ That's the whole pitch. 🚨 **You asked Claude to write a `divide(a, b)` helper.** It says "looks correct!" You ship. Production crashes at 2am because nobody handled `b = 0`. -_With Chorus: GPT or Gemini would have flagged it in the review pass before you merged._ +_With Polyphony: GPT or Gemini would have flagged it in the review pass before you merged._ 🔧 **You're refactoring a critical path.** Your AI rewrote 200 lines and says it's behaviour-equivalent. You're tired and skeptical. -_Run it through Chorus. Three different AIs all saying "yes, equivalent" lets you sleep._ +_Run it through Polyphony. Three different AIs all saying "yes, equivalent" lets you sleep._ 🏗️ **Big architectural call** — queue vs polling, sync vs async, this DB vs that one. -Write a paragraph, hit Chorus. _Three different models give you three angles you hadn't thought of._ +Write a paragraph, hit Polyphony. _Three different models give you three angles you hadn't thought of._ 📝 **Reviewing a 600-line PR.** -You're short on time. Paste the diff into Chorus. _Three reviewers spot the obvious bugs in 90 seconds. Your job becomes the 5% they couldn't catch._ +You're short on time. Paste the diff into Polyphony. _Three reviewers spot the obvious bugs in 90 seconds. Your job becomes the 5% they couldn't catch._ ⚔️ **Test-driven development where neither AI cheats.** _One AI writes tests blind to the code; another AI writes code to pass them._ Use the `red-green` template. 🐛 **Hunting a flaky bug.** -Reproduces 1-in-20, no obvious pattern. Drop the failing test + suspect code into Chorus. +Reproduces 1-in-20, no obvious pattern. Drop the failing test + suspect code into Polyphony. _Each reviewer attacks the bug from a different angle — race? clock skew? off-by-one? — and you land on the cause faster than walking it alone._ --- @@ -83,11 +85,13 @@ _Each reviewer attacks the bug from a different angle — race? clock skew? off- ## Quick start ```bash -npm i -g chorus-codes # install (no sudo — see below) -chorus init # finds AI tools you already have, wires up MCP -chorus start --ui # opens http://localhost:5050 +npm i -g @crypticpy/polyphony # install (no sudo — see below) +chorus init # finds AI tools you already have, wires up MCP +chorus start --ui # opens http://localhost:5050 ``` +> The CLI binary is `chorus` (kept stable for muscle memory + existing docs). `polyphony` is also installed as an alias — both run the same code. Use whichever you like. + Paste a task. Hit submit. Watch the AIs argue. > **Don't `sudo npm install -g`** if you use nvm, fnm, asdf, or any per-user @@ -104,7 +108,7 @@ expects regardless of how you installed. ### Or drive it from any AI CLI you already use -`chorus init` registers Chorus as an MCP server with every CLI / IDE it detects (Claude Code, Codex, Gemini CLI, Cursor, Windsurf, Kimi, OpenCode). After that, just ask the assistant in plain English: +`chorus init` registers Polyphony as an MCP server with every CLI / IDE it detects (Claude Code, Codex, Gemini CLI, Cursor, Windsurf, Kimi, OpenCode). The MCP server is registered under the name `chorus` so existing prompts keep working. After init, just ask the assistant in plain English: ``` > Use chorus to review the staged diff against main @@ -182,8 +186,8 @@ Pick whichever vendor you already pay for. Or skip CLIs entirely and add an Open From inside Claude / Cursor
-Claude Code calling Chorus
-Any AI tool that speaks MCP can trigger a Chorus run. +Claude Code calling Polyphony
+Any AI tool that speaks MCP can trigger a Polyphony run. @@ -200,7 +204,7 @@ function divide(a, b) { } ``` -Submit to Chorus with the **Code Review** template (1 writer + 2 reviewers, both must agree to ship): +Submit to Polyphony with the **Code Review** template (1 writer + 2 reviewers, both must agree to ship): | Step | What happens | | ----------------------------- | ----------------------------------------------------------- | @@ -233,7 +237,7 @@ Make your own by dropping a YAML file in `~/.chorus/templates/`. Or duplicate on The `audit-*` templates run in two phases: -1. **Audit phase** — point Chorus at a repo, pick a preset (code review, architecture review, de-slopify, monolith breakdown, engineering review), and one writer produces a checklist of concrete changes. You approve the items you want. +1. **Audit phase** — point Polyphony at a repo, pick a preset (code review, architecture review, de-slopify, monolith breakdown, engineering review), and one writer produces a checklist of concrete changes. You approve the items you want. 2. **Orchestrate phase** — approved items fan out to worker voices on per-worker git branches. Each worker gets its own slot, model, and persona. Output: an `orchestrate-manifest.json` with diff stats and per-worker status, plus Checkout / Open-PR buttons in the cockpit. ### Verify phase + TDD loop @@ -269,7 +273,7 @@ quorum: ## Reviewer personas -Each reviewer can wear a "hat" — a focus area Chorus prepends to their prompt: +Each reviewer can wear a "hat" — a focus area Polyphony prepends to their prompt: | Persona | What they look for | | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | @@ -285,7 +289,7 @@ Different personas reviewing the same change = wider net. ## Why "different vendors" matters -You can run Chorus with three Claudes. We let you. But the value drops a lot. +You can run Polyphony with three Claudes. We let you. But the value drops a lot. A second Claude reviewing the first Claude's work is theatre — same training, same blind spots. Mix vendors (Claude + GPT + Gemini) and you get genuinely different angles, because they were trained on different data with different biases. @@ -303,7 +307,7 @@ A typical review = **$0** out of pocket. Counts against the quota you already ha **Using API keys** (pay-per-use) A typical code-review run = **$0.30 to $1.50**, depending on diff size. If reviewers disagree and retry, 2–3× worst case. -Chorus adds **zero markup**. We don't see your tokens. +Polyphony adds **zero markup**. We don't see your tokens. --- @@ -325,7 +329,7 @@ Configure on first run, or anytime at _Settings → Permissions_. ## Compared to other code-review tools -| | **Chorus** | CodeRabbit | Greptile | Cursor Review | GitHub Copilot | +| | **Polyphony** | CodeRabbit | Greptile | Cursor Review | GitHub Copilot | | -------------------------------------------------------------- | :-----------: | :--------: | :------: | :-----------: | :------------: | | Multiple AI vendors review the same change | ✅ | ❌ | ❌ | ❌ | ❌ | | Uses your existing AI subscriptions | ✅ | ❌ | ❌ | ❌ | ❌ | @@ -333,7 +337,7 @@ Configure on first run, or anytime at _Settings → Permissions_. | Open source (modify + self-host) | ✅ Apache-2.0 | ❌ | ❌ | ❌ | ❌ | | Custom review patterns | ✅ | partial | ❌ | ❌ | ❌ | -**The unique thing:** your code never goes to a new vendor. Chorus just orchestrates the AI tools you already use. +**The unique thing:** your code never goes to a new vendor. Polyphony just orchestrates the AI tools you already use. --- @@ -384,7 +388,7 @@ older Node + Windows combos), a self-contained crash log is written to ## Telemetry -Chorus pings home once on startup and once every 24h. The payload is fixed: +Polyphony pings home once on startup and once every 24h. The payload is fixed: ```json { @@ -433,7 +437,7 @@ Full picture in [ROADMAP.md](./ROADMAP.md). flowchart TB User([👤 You]) Cockpit[Cockpit
:5050 · web UI] - Daemon[Chorus daemon
:7707 · local server] + Daemon[Polyphony daemon
:7707 · local server] DB[(SQLite
~/.chorus/chorus.db)] MCP[MCP server
for editor integrations] @@ -447,7 +451,7 @@ flowchart TB Daemon -->|spawn| Claude Daemon -->|spawn| Codex Daemon -->|spawn| Gemini - User -.->|"call Chorus from your AI"| MCP --> Daemon + User -.->|"call Polyphony from your AI"| MCP --> Daemon classDef user fill:#fef3c7,stroke:#f59e0b,color:#000 classDef chorus fill:#dbeafe,stroke:#3b82f6,color:#000 @@ -461,9 +465,9 @@ flowchart TB - **Daemon** — small local server (port 7707) that spawns AI tools as subprocesses, parses their output, and tracks state in a SQLite database at `~/.chorus/chorus.db`. - **Cockpit** — the web UI at port 5050 (Next.js). Templates, chats, voices, settings. -- **MCP server** — lets _other_ AI tools (Claude Code, Cursor, etc.) call Chorus programmatically. +- **MCP server** — lets _other_ AI tools (Claude Code, Cursor, etc.) call Polyphony programmatically. -Each AI runs as an isolated subprocess. Chorus reads their structured output (stream-JSON), compares against the template's quorum rule, and emits a verdict. Nothing leaves your machine except the calls to the AI vendors you already use. +Each AI runs as an isolated subprocess. Polyphony reads their structured output (stream-JSON), compares against the template's quorum rule, and emits a verdict. Nothing leaves your machine except the calls to the AI vendors you already use. Code layout: @@ -490,7 +494,7 @@ pnpm test # full suite Read [`AGENTS.md`](./AGENTS.md) first — Next.js 16 has breaking changes from older versions. Coverage target on new code: 80%+. -We dogfood: PRs to Chorus go through Chorus before merging. +We dogfood: PRs to Polyphony go through Polyphony before merging. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full guide. @@ -504,6 +508,20 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full guide. --- +## Compatibility + +Polyphony was published as `chorus-codes` in v0.7. The v0.8 rebrand keeps everything you typed before working: + +- **CLI binary** — `chorus` still works. `polyphony` is installed as an alias; both run the same code. +- **Config directory** — `~/.chorus/` is still the canonical location for `chorus.db`, templates, logs, secrets, and the install-id. +- **MCP server** — `chorus init` still registers under the name `chorus` in your editors (Claude Code, Codex, Cursor, etc.), so any prompt that says "ask chorus to…" or any `chorus.create_chat` tool call keeps working. +- **Env vars** — `CHORUS_TELEMETRY`, `CHORUS_DB_PATH`, `CHORUS_CODEX_HOME`, `CHORUS_LOG_LEVEL` are unchanged. +- **GitHub repo** — still `crypticpy/chorus`. The repo name will move in a follow-up. + +If you're on the old `chorus-codes` npm package, switch with: `npm uninstall -g chorus-codes && npm i -g @crypticpy/polyphony`. No other change required. + +--- + ## License [Apache-2.0](./LICENSE). Use it however you want, including commercially. diff --git a/package.json b/package.json index f6393e9..95d22b1 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,16 @@ { - "name": "chorus-codes", + "name": "@crypticpy/polyphony", "version": "0.8.26", - "description": "Driver-agnostic multi-LLM peer review for code decisions. Bring your own CLI; Chorus convenes 2-4 other LLMs to review the work before you ship.", + "description": "Driver-agnostic multi-LLM peer review for code decisions. Bring your own CLI; Polyphony convenes 2-4 other LLMs to review the work before you ship.", "license": "Apache-2.0", - "author": "99x Agency", - "homepage": "https://chorus.codes", + "author": "crypticpy", + "homepage": "https://github.com/crypticpy/chorus", "repository": { "type": "git", - "url": "https://github.com/chorus-codes/chorus.git" + "url": "https://github.com/crypticpy/chorus.git" }, "bin": { + "polyphony": "./bin/chorus.mjs", "chorus": "./bin/chorus.mjs" }, "publishConfig": { diff --git a/src/app/api/daemon/[...path]/route.ts b/src/app/api/daemon/[...path]/route.ts index a4797f5..2f4cdb7 100644 --- a/src/app/api/daemon/[...path]/route.ts +++ b/src/app/api/daemon/[...path]/route.ts @@ -60,10 +60,9 @@ async function proxy(req: NextRequest, ctx: ProxyContext): Promise { // while the daemon itself only exposes the versioned shape. Exact // segment check — `startsWith("api/v1")` would naively match // `api/v10/...` or `api/v1foo/...` and skip prepending. - const isPrefixed = segments === API_PREFIX || segments.startsWith(`${API_PREFIX}/`); - const versionedSegments = isPrefixed - ? segments - : `${API_PREFIX}/${segments}`; + const isPrefixed = + segments === API_PREFIX || segments.startsWith(`${API_PREFIX}/`); + const versionedSegments = isPrefixed ? segments : `${API_PREFIX}/${segments}`; const search = req.nextUrl.search; const daemonUrl = await getDaemonUrl(); const target = `${daemonUrl}/${versionedSegments}${search}`; @@ -87,8 +86,7 @@ async function proxy(req: NextRequest, ctx: ProxyContext): Promise { // see DELETE through fetchFromDaemon. We also drop the upstream // Content-Type for empty requests because Fastify rejects // application/json + empty body with FST_ERR_CTP_EMPTY_JSON_BODY. - const hasContentLength = - Number(req.headers.get("content-length") ?? "0") > 0; + const hasContentLength = Number(req.headers.get("content-length") ?? "0") > 0; const isChunked = (req.headers.get("transfer-encoding") ?? "") .toLowerCase() .includes("chunked"); @@ -142,7 +140,7 @@ async function proxy(req: NextRequest, ctx: ProxyContext): Promise { error: { code: "daemon_unreachable", message: - "Chorus daemon is not running on this host. Start it with `chorus start`.", + "Polyphony daemon is not running on this host. Start it with `chorus start`.", }, }, { status: 502 }, diff --git a/src/app/onboarding/page.tsx b/src/app/onboarding/page.tsx index 2ac2c9e..078f59d 100644 --- a/src/app/onboarding/page.tsx +++ b/src/app/onboarding/page.tsx @@ -55,14 +55,15 @@ export default function OnboardingPage() { // OpenCode AND the binary is installed. The user picks which // subscription models chorus should expose as voices; persisted in // submit.ts. - const [opencodeModels, setOpencodeModels] = useState( + const [opencodeModels, setOpencodeModels] = + useState(null); + const [opencodeModelsError, setOpencodeModelsError] = useState( null, ); - const [opencodeModelsError, setOpencodeModelsError] = useState(null); const [opencodeModelsLoading, setOpencodeModelsLoading] = useState(false); - const [selectedOpencodeModels, setSelectedOpencodeModels] = useState>( - new Set(), - ); + const [selectedOpencodeModels, setSelectedOpencodeModels] = useState< + Set + >(new Set()); useEffect(() => { // `cancelled` guards every setter against unmount-during-fetch @@ -192,8 +193,7 @@ export default function OnboardingPage() { if (!value) { setManualError((prev) => ({ ...prev, - [id]: - "Enter the full path to the CLI program (e.g. /usr/local/bin/claude).", + [id]: "Enter the full path to the CLI program (e.g. /usr/local/bin/claude).", })); return; } @@ -264,7 +264,9 @@ export default function OnboardingPage() { const handleSubmit = () => { setError(null); if (filledCount === 0) { - setError("Pick at least one CLI or paste at least one API key to continue."); + setError( + "Pick at least one CLI or paste at least one API key to continue.", + ); return; } startTransition(async () => { @@ -295,7 +297,7 @@ export default function OnboardingPage() {

- Welcome to Chorus + Welcome to Polyphony

Connect at least one model to begin @@ -304,9 +306,9 @@ export default function OnboardingPage() {

- Chorus runs your prompt past 2–4 LLMs of different lineages and - synthesises consensus. Pick the CLI subscriptions you already have, - or paste API keys. You can change these later in Settings. + Polyphony runs your prompt past 2–4 LLMs of different lineages and + synthesises consensus. Pick the CLI subscriptions you already have, or + paste API keys. You can change these later in Settings.

{ +export async function submitOnboarding( + args: OnboardingSubmitArgs, +): Promise { for (const cliId of args.selectedClis) { const cli = CLIS.find((c) => c.id === cliId); if (!cli) continue; @@ -64,9 +59,8 @@ export async function submitOnboarding(args: OnboardingSubmitArgs): Promise { - const { listVoices, updateVoice, createVoice } = await import( - "@/lib/api/voices" - ); + const { listVoices, updateVoice, createVoice } = + await import("@/lib/api/voices"); const existing = new Map( (await listVoices({ provider: "opencode-cli" }).catch(() => [])).map( (v) => [v.id, v] as const, @@ -110,5 +104,5 @@ async function persistOpencodePicks(args: OnboardingSubmitArgs): Promise { export function describeError(err: unknown): string { return err instanceof DaemonError ? err.message - : "Could not save. Is the Chorus daemon running?"; + : "Could not save. Is the Polyphony daemon running?"; } diff --git a/src/app/page.tsx b/src/app/page.tsx index b441ca1..f297af2 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -39,18 +39,18 @@ async function getHomePageData(): Promise { const error = err instanceof DaemonError ? err.message - : "Failed to reach the Chorus daemon."; + : "Failed to reach the Polyphony daemon."; return { stats: null, templates: [], secrets: [], settings: null, error }; } } export default async function HomePage() { - const { stats, templates, secrets, settings, error } = await getHomePageData(); + const { stats, templates, secrets, settings, error } = + await getHomePageData(); // First-run gate: redirect to /onboarding if user has no credentials // and hasn't explicitly marked the wizard as completed. - const onboarded = - Boolean(settings?.onboarded) || secrets.length > 0; + const onboarded = Boolean(settings?.onboarded) || secrets.length > 0; if (!error && !onboarded) { redirect("/onboarding"); } @@ -171,4 +171,3 @@ function ActiveHome({ stats }: ActiveHomeProps) { ); } - diff --git a/src/cli/commands/start.ts b/src/cli/commands/start.ts index 14b393a..8f4ff5b 100644 --- a/src/cli/commands/start.ts +++ b/src/cli/commands/start.ts @@ -44,7 +44,7 @@ export function registerStartCommand(program: Command): void { "--daemon-only", "Skip cockpit (Next.js UI). Used by MCP auto-start.", ) - .description("Start the Chorus daemon (PM2-style fork)") + .description("Start the Polyphony daemon (PM2-style fork)") .action(async (options: { ui?: boolean; daemonOnly?: boolean }) => { try { const chorusDir = path.join(os.homedir(), ".chorus"); @@ -249,7 +249,7 @@ async function alreadyRunningHealthy( console.log( header( sym.ok, - "Chorus is already running", + "Polyphony is already running", `version ${live.version || pkg.version}`, ), ); @@ -274,7 +274,7 @@ async function alreadyRunningHealthy( console.log(""); console.log( c.dim( - " Daemon-only mode. Run `chorus start --ui` to bring up the cockpit.", + " Daemon-only mode. Run `polyphony start --ui` (or `chorus start --ui`) to bring up the cockpit.", ), ); console.log(""); @@ -312,7 +312,7 @@ async function spawnCockpitForExistingDaemon(chorusDir: string): Promise { console.log(""); console.log( c.red( - " ✗ Cockpit UI not found. Try `npm install -g chorus-codes` to repair.", + " ✗ Cockpit UI not found. Try `npm install -g @crypticpy/polyphony` to repair.", ), ); console.log(""); @@ -515,7 +515,7 @@ function warnIfTmuxMissing(): void { console.log(""); console.log( c.dim( - ` ${sym.info} tmux not detected. Chorus runs headless by default — this is fine.`, + ` ${sym.info} tmux not detected. Polyphony runs headless by default — this is fine.`, ), ); console.log(c.dim(" Optional backup mode: install tmux, then open")); @@ -635,7 +635,7 @@ async function spawnDaemonAndCockpit( " The published install should ship a built UI. Try reinstalling:", ), ); - console.log(` ${c.bold("npm install -g chorus-codes")}`); + console.log(` ${c.bold("npm install -g @crypticpy/polyphony")}`); } console.log( c.dim( @@ -655,7 +655,11 @@ async function spawnDaemonAndCockpit( console.log(""); console.log( - header(sym.ok, `Chorus started v${pkg.version}`, `daemon PID ${child.pid}`), + header( + sym.ok, + `Polyphony started v${pkg.version}`, + `daemon PID ${child.pid}`, + ), ); // Path of the resolved binary helps users diagnose multi-install // confusion (sudo npm install vs nvm-managed npm). Quiet by default @@ -692,7 +696,7 @@ async function checkForUpdate(): Promise { if (!latest) return; if (!versionGreater(latest, pkg.version)) return; console.log( - ` ${c.dim("•")} ${c.cyan(`chorus ${latest}`)} ${c.dim("is available — run")} ${c.cyan("chorus update")}`, + ` ${c.dim("•")} ${c.cyan(`Polyphony ${latest}`)} ${c.dim("is available — run")} ${c.cyan("polyphony update")}`, ); console.log(""); } catch { diff --git a/src/cli/commands/stop.ts b/src/cli/commands/stop.ts index 8dd1227..c3a7009 100644 --- a/src/cli/commands/stop.ts +++ b/src/cli/commands/stop.ts @@ -1,19 +1,15 @@ -import type { Command } from 'commander'; -import fs from 'fs'; -import os from 'os'; -import path from 'path'; +import type { Command } from "commander"; +import fs from "fs"; +import os from "os"; +import path from "path"; import { clearDaemonInfo, DEFAULT_COCKPIT_PORT, DEFAULT_DAEMON_PORT, readDaemonInfo, -} from '../../lib/daemon-discovery.js'; -import { - findPidsOnPort, - isPortInUse, - killAndVerify, -} from '../port-utils.js'; -import { c, header, sym } from '../ui.js'; +} from "../../lib/daemon-discovery.js"; +import { findPidsOnPort, isPortInUse, killAndVerify } from "../port-utils.js"; +import { c, header, sym } from "../ui.js"; /** * Two-stage shutdown per managed process: SIGTERM, wait up to 1.5s, @@ -29,13 +25,13 @@ import { c, header, sym } from '../ui.js'; */ export function registerStopCommand(program: Command): void { program - .command('stop') - .description('Stop the Chorus daemon and cockpit') + .command("stop") + .description("Stop the Polyphony daemon and cockpit") .action(async () => { try { - const chorusDir = path.join(os.homedir(), '.chorus'); - const daemonPidFile = path.join(chorusDir, 'daemon.pid'); - const webPidFile = path.join(chorusDir, 'web.pid'); + const chorusDir = path.join(os.homedir(), ".chorus"); + const daemonPidFile = path.join(chorusDir, "daemon.pid"); + const webPidFile = path.join(chorusDir, "web.pid"); const info = readDaemonInfo(); const daemonPort = info?.daemonPort ?? DEFAULT_DAEMON_PORT; @@ -53,36 +49,41 @@ export function registerStopCommand(program: Command): void { !daemonPortInUse && !cockpitPortInUse ) { - console.log(''); - console.log(header(sym.info, 'Chorus is not running', 'nothing to stop')); - console.log(''); + console.log(""); + console.log( + header(sym.info, "Polyphony is not running", "nothing to stop"), + ); + console.log(""); return; } - console.log(''); - console.log(header(sym.pointer, 'Stopping Chorus...')); - console.log(''); + console.log(""); + console.log(header(sym.pointer, "Stopping Polyphony...")); + console.log(""); // Prefer daemon.json (v0.8), fall back to pidfiles (v0.7). if (info) { - await stopByPid('Daemon', info.daemonPid); + await stopByPid("Daemon", info.daemonPid); if (info.cockpitPid) { - await stopByPid('Cockpit', info.cockpitPid); + await stopByPid("Cockpit", info.cockpitPid); } } - await stopByPidFile('Daemon', daemonPidFile); - await stopByPidFile('Cockpit', webPidFile); + await stopByPidFile("Daemon", daemonPidFile); + await stopByPidFile("Cockpit", webPidFile); // Port-based sweep — kills any chorus-owned listener that // escaped the pidfile path. - await sweepPort(daemonPort, 'Daemon'); - await sweepPort(cockpitPort, 'Cockpit'); + await sweepPort(daemonPort, "Daemon"); + await sweepPort(cockpitPort, "Cockpit"); clearDaemonInfo(); - console.log(''); + console.log(""); } catch (error) { - console.error(`${sym.err} ${c.red('Error stopping chorus:')}`, error); + console.error( + `${sym.err} ${c.red("Error stopping Polyphony:")}`, + error, + ); process.exit(1); } }); @@ -98,7 +99,7 @@ async function stopByPid(label: string, pid: number): Promise { async function stopByPidFile(label: string, pidFile: string): Promise { if (!fs.existsSync(pidFile)) return; - const pid = parseInt(fs.readFileSync(pidFile, 'utf-8'), 10); + const pid = parseInt(fs.readFileSync(pidFile, "utf-8"), 10); if (!Number.isFinite(pid) || pid <= 0) { fs.unlinkSync(pidFile); return; diff --git a/src/cli/commands/update.ts b/src/cli/commands/update.ts index fbb6330..e88a4ac 100644 --- a/src/cli/commands/update.ts +++ b/src/cli/commands/update.ts @@ -1,9 +1,9 @@ -import { spawn } from 'child_process'; -import type { Command } from 'commander'; -import fs from 'fs'; -import path from 'path'; -import { pkg } from '../shared.js'; -import { c, header, sym } from '../ui.js'; +import { spawn } from "child_process"; +import type { Command } from "commander"; +import fs from "fs"; +import path from "path"; +import { pkg } from "../shared.js"; +import { c, header, sym } from "../ui.js"; /** * `chorus update` — self-locating npm install. @@ -24,55 +24,55 @@ import { c, header, sym } from '../ui.js'; */ export function registerUpdateCommand(program: Command): void { program - .command('update') - .description('Update chorus to the latest version on npm') - .option('--check', 'Only check for updates; do not install') + .command("update") + .description("Update Polyphony to the latest version on npm") + .option("--check", "Only check for updates; do not install") .action(async (options: { check?: boolean }) => { try { const current = pkg.version; const latest = await fetchLatestVersion(); if (latest === null) { - console.log(''); + console.log(""); console.log( header( sym.err, "Couldn't reach npm registry", - 'check your network connection and retry', + "check your network connection and retry", ), ); - console.log(''); + console.log(""); process.exit(1); } if (options.check) { if (versionGreater(latest, current)) { - console.log(''); + console.log(""); console.log( header( sym.info, - `chorus ${latest} is available`, + `Polyphony ${latest} is available`, `you have ${current}`, ), ); - console.log(` Run ${c.cyan('chorus update')} to upgrade`); - console.log(''); - } else { - console.log(''); console.log( - header(sym.ok, `chorus ${current} is up to date`), + ` Run ${c.cyan("polyphony update")} ${c.dim("(or `chorus update`)")} to upgrade`, ); - console.log(''); + console.log(""); + } else { + console.log(""); + console.log(header(sym.ok, `Polyphony ${current} is up to date`)); + console.log(""); } return; } if (!versionGreater(latest, current)) { - console.log(''); + console.log(""); console.log( - header(sym.ok, `chorus ${current} is already up to date`), + header(sym.ok, `Polyphony ${current} is already up to date`), ); - console.log(''); + console.log(""); return; } @@ -89,80 +89,86 @@ export function registerUpdateCommand(program: Command): void { if (prefix) { const probe = checkPrefixUsable(prefix); if (!probe.ok) { - console.log(''); + console.log(""); console.log( header( sym.err, - "Can't update chorus at this prefix", + "Can't update Polyphony at this prefix", probe.reason, ), ); - console.log(''); - console.log(c.dim(' Migrate to a Linux-side npm prefix:')); + console.log(""); + console.log(c.dim(" Migrate to a Linux-side npm prefix:")); console.log(` mkdir -p ~/.npm-global`); console.log(` npm config set prefix ~/.npm-global`); console.log( ` echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc`, ); console.log(` source ~/.bashrc`); - console.log(` npm install -g chorus-codes`); - console.log(''); - console.log(c.dim(' After that, future `chorus update` calls work normally.')); - console.log(''); + console.log(` npm install -g @crypticpy/polyphony`); + console.log(""); + console.log( + c.dim( + " After that, future `polyphony update` calls work normally.", + ), + ); + console.log(""); process.exit(1); } } - console.log(''); + console.log(""); console.log( header( sym.pointer, - `Updating chorus ${current} → ${latest}`, - prefix ? `prefix ${prefix}` : 'using npm default prefix', + `Updating Polyphony ${current} → ${latest}`, + prefix ? `prefix ${prefix}` : "using npm default prefix", ), ); - console.log(''); + console.log(""); - const args = ['install', '-g', `chorus-codes@${latest}`]; + const args = ["install", "-g", `@crypticpy/polyphony@${latest}`]; if (prefix) { - args.push('--prefix', prefix); + args.push("--prefix", prefix); } // Hand stdio to npm so the user sees its progress + any errors // (EACCES, network, etc.). spawn rather than execFile so we can // stream output as it happens. - const child = spawn('npm', args, { stdio: 'inherit' }); + const child = spawn("npm", args, { stdio: "inherit" }); await new Promise((resolve, reject) => { - child.on('exit', (code) => { + child.on("exit", (code) => { if (code === 0) resolve(); else reject(new Error(`npm install exited with code ${code}`)); }); - child.on('error', reject); + child.on("error", reject); }); - console.log(''); + console.log(""); console.log( header( sym.ok, - `Updated to chorus ${latest}`, - 'restart any running daemon: chorus stop && chorus start', + `Updated to Polyphony ${latest}`, + "restart any running daemon: polyphony stop && polyphony start", ), ); - console.log(''); + console.log(""); } catch (error) { - console.log(''); + console.log(""); const message = error instanceof Error ? error.message : String(error); - console.error(`${sym.err} ${c.red('Update failed:')} ${message}`); - console.log(''); + console.error(`${sym.err} ${c.red("Update failed:")} ${message}`); + console.log(""); console.log( - c.dim(' If this is a permissions error, your npm prefix may not be writable.'), + c.dim( + " If this is a permissions error, your npm prefix may not be writable.", + ), ); console.log( c.dim( - ' Try: npm config set prefix ~/.npm-global, then add ~/.npm-global/bin to PATH.', + " Try: npm config set prefix ~/.npm-global, then add ~/.npm-global/bin to PATH.", ), ); - console.log(''); + console.log(""); process.exit(1); } }); @@ -182,14 +188,14 @@ export function registerUpdateCommand(program: Command): void { export function detectNpmPrefix(): string | null { const start = __dirname; const segments = start.split(path.sep); - const nmIdx = segments.lastIndexOf('node_modules'); + const nmIdx = segments.lastIndexOf("node_modules"); if (nmIdx === -1) return null; const parent = segments.slice(0, nmIdx).join(path.sep); // Normalise: on POSIX `lib/node_modules/...`, the prefix is up one // more from `lib`. On Windows there's no `lib` segment. - if (parent.endsWith(path.sep + 'lib') || parent === 'lib') { - const prefix = parent.slice(0, -('lib'.length + path.sep.length)); + if (parent.endsWith(path.sep + "lib") || parent === "lib") { + const prefix = parent.slice(0, -("lib".length + path.sep.length)); return prefix.length > 0 ? prefix : null; } return parent; @@ -204,19 +210,24 @@ export function detectNpmPrefix(): string | null { * "couldn't check" message instead of crashing. */ export async function fetchLatestVersion( - packageName = 'chorus-codes', + packageName = "@crypticpy/polyphony", ): Promise { try { + // Scoped names (`@scope/pkg`) need the `/` percent-encoded as `%2F` + // for npm's dist-tags endpoint to resolve them — otherwise the + // registry interprets the slash as a path separator and 404s. + // encodeURIComponent is a no-op for unscoped names like `chorus-codes`. + const encodedName = encodeURIComponent(packageName); const ac = new AbortController(); const timer = setTimeout(() => ac.abort(), 5000); const res = await fetch( - `https://registry.npmjs.org/-/package/${packageName}/dist-tags`, + `https://registry.npmjs.org/-/package/${encodedName}/dist-tags`, { signal: ac.signal }, ); clearTimeout(timer); if (!res.ok) return null; const data = (await res.json()) as { latest?: string }; - return typeof data.latest === 'string' ? data.latest : null; + return typeof data.latest === "string" ? data.latest : null; } catch { return null; } @@ -230,7 +241,7 @@ export async function fetchLatestVersion( */ export function versionGreater(a: string, b: string): boolean { const parse = (v: string): number[] => - v.split('.').map((n) => Number.parseInt(n, 10) || 0); + v.split(".").map((n) => Number.parseInt(n, 10) || 0); const aa = parse(a); const bb = parse(b); const len = Math.max(aa.length, bb.length); @@ -277,18 +288,18 @@ export function checkPrefixUsable( // not exist yet on a fresh prefix; mkdir + write + unlink is the // safest test. try { - const targetDir = path.join(prefix, 'lib', 'node_modules'); + const targetDir = path.join(prefix, "lib", "node_modules"); fs.mkdirSync(targetDir, { recursive: true }); const probePath = path.join( targetDir, `.chorus-update-probe-${process.pid}-${Date.now()}`, ); - fs.writeFileSync(probePath, 'probe'); + fs.writeFileSync(probePath, "probe"); fs.unlinkSync(probePath); return { ok: true }; } catch (err) { const code = (err as NodeJS.ErrnoException).code; - if (code === 'EACCES' || code === 'EPERM' || code === 'EROFS') { + if (code === "EACCES" || code === "EPERM" || code === "EROFS") { return { ok: false, reason: `prefix isn't writable by the current user (${code} on ${prefix}/lib/node_modules).`, @@ -321,4 +332,3 @@ export function resolveChorusBinaryPath(): string | null { return entry; } } - diff --git a/src/cli/index.ts b/src/cli/index.ts index 4113d6c..7af211a 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -39,7 +39,7 @@ program.addHelpText("beforeAll", () => { if (!initialised) { return [ "", - ` ${sym.rocket} ${c.bold("Welcome to Chorus")} ${c.dim("— two commands to get going:")}`, + ` ${sym.rocket} ${c.bold("Welcome to Polyphony")} ${c.dim("— two commands to get going:")}`, "", ` ${c.cyan("1.")} ${c.bold("chorus init")} ${c.dim("register MCP with your editors + seed templates + detect CLIs")}`, ` ${c.cyan("2.")} ${c.bold("chorus start")} ${c.dim("bring up the daemon + cockpit")}`, @@ -70,7 +70,7 @@ registerBabysitCommand(program); program .command("ui") - .description("Open the Chorus web UI in default browser") + .description("Open the Polyphony web UI in default browser") .action(async () => { try { const env = detectRuntimeEnv(); @@ -95,7 +95,7 @@ program program .command("connect [orchestrator]") .description( - "Pre-approve all Chorus MCP tools in your orchestrator (default: claude)", + "Pre-approve all Polyphony MCP tools in your orchestrator (default: claude)", ) .action(async (orchestrator?: string) => { const { runConnect } = await import("./connect.js"); diff --git a/src/daemon/index.ts b/src/daemon/index.ts index bc097c9..6c0b78a 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -1,5 +1,5 @@ /** - * Chorus daemon — Fastify HTTP server. + * Polyphony daemon — Fastify HTTP server. * * Boots the DB, seeds builtin personas/voices/templates, registers route * groups, and starts the reaper. Routes live in `routes/*.ts`; the @@ -312,7 +312,7 @@ async function main(): Promise { // Keep the human-readable startup line — the install script + // onboarding grep for it. Structured line above is what `chorus logs` // consumes. - console.log(`Chorus daemon listening on http://${HOST}:${PORT}`); + console.log(`Polyphony daemon listening on http://${HOST}:${PORT}`); // Anonymous opt-out telemetry. First send is delayed 5s so the // listener is definitely up; subsequent sends every 24h. All three diff --git a/src/daemon/routes/system.ts b/src/daemon/routes/system.ts index efdc40b..1348c89 100644 --- a/src/daemon/routes/system.ts +++ b/src/daemon/routes/system.ts @@ -7,15 +7,15 @@ * endpoint needs the chorus binary path so the editor's MCP config can * point at the right entry script. */ -import type { FastifyInstance } from 'fastify'; -import { chats } from '../../lib/db/index.js'; +import type { FastifyInstance } from "fastify"; +import { chats } from "../../lib/db/index.js"; import { successResponse, errorResponse, listEnvelope, type ApiResponse, type ListEnvelope, -} from '../api-response.js'; +} from "../api-response.js"; export interface SystemRouteDeps { /** Absolute path to bin/chorus.mjs — used by /orchestrators/:name/connect. */ @@ -53,15 +53,19 @@ export function registerSystemRoutes( // List blocked chats — consumed by the MCP `list_blocked` tool. There // is no cockpit /blocked page today; older comments referenced one // that never landed. - fastify.get<{ Reply: ApiResponse> }>('/blocked', async () => { - try { - const items = await chats.list({ status: 'blocked' }); - return successResponse(listEnvelope(items)); - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return errorResponse('db_error', message); - } - }); + fastify.get<{ Reply: ApiResponse> }>( + "/blocked", + async () => { + try { + const items = await chats.list({ status: "blocked" }); + return successResponse(listEnvelope(items)); + } catch (error) { + const message = + error instanceof Error ? error.message : "Unknown error"; + return errorResponse("db_error", message); + } + }, + ); // ─── Update availability check ──────────────────────────────────────── // Polls npm's dist-tags endpoint (~30 bytes) and returns whether a @@ -76,46 +80,50 @@ export function registerSystemRoutes( latest: string | null; updateAvailable: boolean; }>; - }>('/update-check', async () => { + }>("/update-check", async () => { try { - const { fetchLatestVersion, versionGreater } = await import( - '../../cli/commands/update.js' - ); + const { fetchLatestVersion, versionGreater } = + await import("../../cli/commands/update.js"); const current = deps.version; const latest = await getCachedLatestVersion(() => - fetchLatestVersion('chorus-codes'), + fetchLatestVersion("@crypticpy/polyphony"), ); const updateAvailable = latest !== null && versionGreater(latest, current); return successResponse({ current, latest, updateAvailable }); } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return errorResponse('internal', message); + const message = error instanceof Error ? error.message : "Unknown error"; + return errorResponse("internal", message); } }); // ─── CLI health snapshot ────────────────────────────────────────────── - fastify.get<{ Reply: ApiResponse> }>('/cli/health', async () => { - try { - const { getAllHealth } = await import('../../lib/cli-health.js'); - const items = await getAllHealth(); - return successResponse(listEnvelope(items)); - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return errorResponse('internal', message); - } - }); + fastify.get<{ Reply: ApiResponse> }>( + "/cli/health", + async () => { + try { + const { getAllHealth } = await import("../../lib/cli-health.js"); + const items = await getAllHealth(); + return successResponse(listEnvelope(items)); + } catch (error) { + const message = + error instanceof Error ? error.message : "Unknown error"; + return errorResponse("internal", message); + } + }, + ); // ─── Onboarding: detect installed CLIs + validate manual paths ──────── fastify.get<{ Reply: ApiResponse> }>( - '/onboard/detect-clis', + "/onboard/detect-clis", async () => { try { - const { detectAllClis } = await import('../../lib/cli-detect.js'); + const { detectAllClis } = await import("../../lib/cli-detect.js"); return successResponse(listEnvelope(detectAllClis())); } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return errorResponse('internal', message); + const message = + error instanceof Error ? error.message : "Unknown error"; + return errorResponse("internal", message); } }, ); @@ -123,17 +131,17 @@ export function registerSystemRoutes( fastify.post<{ Body: { id: string; path: string }; Reply: ApiResponse; - }>('/onboard/validate-cli-path', async (req) => { + }>("/onboard/validate-cli-path", async (req) => { try { const { id, path: customPath } = req.body || {}; - if (!id || typeof customPath !== 'string') { - return errorResponse('bad_request', 'id and path are required'); + if (!id || typeof customPath !== "string") { + return errorResponse("bad_request", "id and path are required"); } - const { validateCliPath } = await import('../../lib/cli-detect.js'); + const { validateCliPath } = await import("../../lib/cli-detect.js"); return successResponse(validateCliPath(id as never, customPath)); } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return errorResponse('internal', message); + const message = error instanceof Error ? error.message : "Unknown error"; + return errorResponse("internal", message); } }); @@ -150,23 +158,22 @@ export function registerSystemRoutes( fastify.post<{ Body: { id: string; path: string }; Reply: ApiResponse; - }>('/onboard/save-cli-path', async (req) => { + }>("/onboard/save-cli-path", async (req) => { try { const { id, path: customPath } = req.body || {}; - if (!id || typeof customPath !== 'string') { - return errorResponse('bad_request', 'id and path are required'); + if (!id || typeof customPath !== "string") { + return errorResponse("bad_request", "id and path are required"); } - const { validateCliPath, clearDetectionCache } = await import( - '../../lib/cli-detect.js' - ); + const { validateCliPath, clearDetectionCache } = + await import("../../lib/cli-detect.js"); const validation = validateCliPath(id as never, customPath); if (!validation.found) { return errorResponse( - 'validation', - validation.reason ?? 'path failed validation', + "validation", + validation.reason ?? "path failed validation", ); } - const { cliPaths } = await import('../../lib/cli-paths.js'); + const { cliPaths } = await import("../../lib/cli-paths.js"); await cliPaths.set(id as never, validation.path!); // Refresh sync caches so subsequent detection + spawns honour the // new path immediately, not after the next boot. @@ -175,8 +182,8 @@ export function registerSystemRoutes( // Also refresh the headless spawn PATH so the new dirname is // prepended without requiring a daemon restart. try { - const { buildRuntimePath } = await import('../../lib/runtime-path.js'); - const { setSpawnPath } = await import('../headless.js'); + const { buildRuntimePath } = await import("../../lib/runtime-path.js"); + const { setSpawnPath } = await import("../headless.js"); const merged = await buildRuntimePath({ additionalDirs: cliPaths.cachedDirs(), }); @@ -187,18 +194,18 @@ export function registerSystemRoutes( } return successResponse({ id, path: validation.path }); } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return errorResponse('internal', message); + const message = error instanceof Error ? error.message : "Unknown error"; + return errorResponse("internal", message); } }); /** Read every saved manual CLI path. Drives the cockpit's "saved" badge * and `chorus doctor`. Empty values are omitted. */ fastify.get<{ Reply: ApiResponse }>( - '/onboard/cli-paths', + "/onboard/cli-paths", async () => { try { - const { cliPaths } = await import('../../lib/cli-paths.js'); + const { cliPaths } = await import("../../lib/cli-paths.js"); const all = await cliPaths.listAll(); const compact: Record = {}; for (const [id, p] of Object.entries(all)) { @@ -206,8 +213,9 @@ export function registerSystemRoutes( } return successResponse(compact); } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return errorResponse('internal', message); + const message = + error instanceof Error ? error.message : "Unknown error"; + return errorResponse("internal", message); } }, ); @@ -217,38 +225,41 @@ export function registerSystemRoutes( fastify.delete<{ Params: { id: string }; Reply: ApiResponse; - }>('/onboard/cli-paths/:id', async (req) => { + }>("/onboard/cli-paths/:id", async (req) => { try { - const { cliPaths } = await import('../../lib/cli-paths.js'); - const { clearDetectionCache } = await import('../../lib/cli-detect.js'); + const { cliPaths } = await import("../../lib/cli-paths.js"); + const { clearDetectionCache } = await import("../../lib/cli-detect.js"); await cliPaths.clear(req.params.id as never); clearDetectionCache(); return successResponse({ id: req.params.id, cleared: true }); } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return errorResponse('internal', message); + const message = error instanceof Error ? error.message : "Unknown error"; + return errorResponse("internal", message); } }); // ─── Orchestrators (editors that call chorus via MCP) ──────────────── - fastify.get<{ Reply: ApiResponse> }>('/orchestrators', async () => { - try { - const { listOrchestrators } = await import('../orchestrators/index.js'); - return successResponse(listEnvelope(listOrchestrators())); - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return errorResponse('internal', message); - } - }); + fastify.get<{ Reply: ApiResponse> }>( + "/orchestrators", + async () => { + try { + const { listOrchestrators } = await import("../orchestrators/index.js"); + return successResponse(listEnvelope(listOrchestrators())); + } catch (error) { + const message = + error instanceof Error ? error.message : "Unknown error"; + return errorResponse("internal", message); + } + }, + ); fastify.post<{ Params: { name: string }; Reply: ApiResponse; - }>('/orchestrators/:name/connect', async (request) => { + }>("/orchestrators/:name/connect", async (request) => { try { - const { connectByName, listOrchestrators } = await import( - '../orchestrators/index.js' - ); + const { connectByName, listOrchestrators } = + await import("../orchestrators/index.js"); const result = await connectByName(request.params.name, { binPath: deps.chorusBinPath, }); @@ -257,8 +268,8 @@ export function registerSystemRoutes( ); return successResponse({ ...result, status }); } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return errorResponse('validation', message); + const message = error instanceof Error ? error.message : "Unknown error"; + return errorResponse("validation", message); } }); @@ -273,44 +284,46 @@ export function registerSystemRoutes( flat: string[]; defaultPicks: string[]; }>; - }>('/orchestrators/opencode/models', async () => { + }>("/orchestrators/opencode/models", async () => { try { // Resolve the actual installed path rather than spawning bare // 'opencode' — when the daemon's $PATH doesn't include the // installer's bin dir (common: opencode lives at // ~/.opencode/bin, not in /usr/local/bin), bare lookup ENOENTs // even when detection found the binary fine. - const { detectAllClis } = await import('../../lib/cli-detect.js'); - const opencode = detectAllClis().find((c) => c.id === 'opencode-cli'); + const { detectAllClis } = await import("../../lib/cli-detect.js"); + const opencode = detectAllClis().find((c) => c.id === "opencode-cli"); if (!opencode?.found || !opencode.path) { return errorResponse( - 'cli_failed', - 'opencode CLI not found on this host. Install from https://opencode.ai or set its path manually in onboarding.', + "cli_failed", + "opencode CLI not found on this host. Install from https://opencode.ai or set its path manually in onboarding.", ); } - const { execFile } = await import('node:child_process'); - const { promisify } = await import('node:util'); + const { execFile } = await import("node:child_process"); + const { promisify } = await import("node:util"); const run = promisify(execFile); - const { stdout } = await run(opencode.path, ['models'], { timeout: 10_000 }); + const { stdout } = await run(opencode.path, ["models"], { + timeout: 10_000, + }); const flat = stdout - .split('\n') + .split("\n") .map((l) => l.trim()) .filter((l) => l.length > 0); const gateways: Record = {}; for (const m of flat) { - const slash = m.indexOf('/'); - const gw = slash > 0 ? m.slice(0, slash) : 'other'; + const slash = m.indexOf("/"); + const gw = slash > 0 ? m.slice(0, slash) : "other"; if (!gateways[gw]) gateways[gw] = []; gateways[gw].push(m); } // Fleet defaults — kimi + deepseek via Go subscription. Only suggest // those that actually appear in the user's `opencode models` output. - const FLEET = ['opencode-go/kimi-k2.6', 'opencode-go/deepseek-v4-pro']; + const FLEET = ["opencode-go/kimi-k2.6", "opencode-go/deepseek-v4-pro"]; const defaultPicks = FLEET.filter((m) => flat.includes(m)); return successResponse({ gateways, flat, defaultPicks }); } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return errorResponse('cli_failed', message); + const message = error instanceof Error ? error.message : "Unknown error"; + return errorResponse("cli_failed", message); } }); } diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts index f0e92ae..daf330d 100644 --- a/src/lib/api/client.ts +++ b/src/lib/api/client.ts @@ -142,7 +142,7 @@ export async function fetchFromDaemon( throw new DaemonError( "connection_failed", 0, - "Failed to connect to Chorus daemon. Is it running?", + "Failed to connect to Polyphony daemon. Is it running?", ); } diff --git a/src/mcp/client.ts b/src/mcp/client.ts index 489c814..be0f345 100644 --- a/src/mcp/client.ts +++ b/src/mcp/client.ts @@ -175,7 +175,7 @@ async function daemonFetchWithRetry( return daemonFetchWithRetry(path, options, false); } throw new Error( - "Chorus daemon not running and auto-start failed. Run 'chorus start' first " + + "Polyphony daemon not running and auto-start failed. Run 'polyphony start' (or 'chorus start') first " + "(set CHORUS_AUTOSTART=0 to disable auto-start prompts).", ); }