diff --git a/apps/site/src/lib/content.ts b/apps/site/src/lib/content.ts index 31ba69a..346da19 100644 --- a/apps/site/src/lib/content.ts +++ b/apps/site/src/lib/content.ts @@ -69,7 +69,7 @@ export const siteContent: SiteContent = { tagline: 'Run Claude Code agents on your own machine — drive them from any browser.', subhead: 'telecode is an open-source, self-hostable command center for Claude Code agents. Launch, watch, and steer agents that run on your hardware, from your phone or laptop — your code never leaves your machine.', - installCommand: 'npx telecode', + installCommand: 'npx @telecode/cli', capabilities: [ { id: 'open-source', @@ -97,7 +97,7 @@ export const siteContent: SiteContent = { stepNumber: 1, title: 'Install on your machine', body: 'Run one command where your agents should work. The daemon dials out to the relay — nothing ever reaches into your laptop.', - command: 'npx telecode', + command: 'npx @telecode/cli', }, { stepNumber: 2, diff --git a/apps/web/src/lib/onboarding.test.ts b/apps/web/src/lib/onboarding.test.ts index ef7b346..5a798ed 100644 --- a/apps/web/src/lib/onboarding.test.ts +++ b/apps/web/src/lib/onboarding.test.ts @@ -9,7 +9,7 @@ import { buildOnboardingSteps } from './onboarding'; * directly. */ const instructions = { command: 'make run', codeLocation: '.run-state/daemon.log' } as const; -const prodInstructions = { command: 'npx telecode', codeLocation: null } as const; +const prodInstructions = { command: 'npx @telecode/cli', codeLocation: null } as const; describe('buildOnboardingSteps', () => { it('starts on the pair step before a device is paired', () => { @@ -43,7 +43,7 @@ describe('buildOnboardingSteps', () => { hasSessions: false, instructions: prodInstructions, })[0]; - expect(prod?.command).toBe('npx telecode'); + expect(prod?.command).toBe('npx @telecode/cli'); expect(prod?.codeLocation).toBeNull(); }); diff --git a/apps/web/src/lib/onboarding.ts b/apps/web/src/lib/onboarding.ts index dd94cb9..35eb0d4 100644 --- a/apps/web/src/lib/onboarding.ts +++ b/apps/web/src/lib/onboarding.ts @@ -2,7 +2,7 @@ * The first-run onboarding model (Phase 4 T14): the ordered path a signed-in user walks to their first * remote launch — pair a machine, then launch a session. Pure over observable state so the `Onboarding` * stepper stays a thin renderer; exactly one step is `current` at a time, and the pair step carries the - * environment-aware command (dev `make run` vs prod `npx telecode`) so the screen never lies about how to + * environment-aware command (dev `make run` vs prod `npx @telecode/cli`) so the screen never lies about how to * start the daemon. */ diff --git a/apps/web/src/lib/pairing-instructions.ts b/apps/web/src/lib/pairing-instructions.ts index 77728a7..4983d25 100644 --- a/apps/web/src/lib/pairing-instructions.ts +++ b/apps/web/src/lib/pairing-instructions.ts @@ -13,12 +13,12 @@ interface PairingInstructions { /** * How a user starts their local agent daemon and where to read its pairing code. * - * `npx telecode` is the eventual published one-liner, but it isn't available in - * local development yet (no published bin). In a dev build the daemon is started - * by the repo's `make run`, which writes its pairing code to the daemon log - * rather than printing it on screen. Surfacing the right instruction per - * environment keeps the pairing screens honest until the CLI ships. + * `npx @telecode/cli` is the published one-liner for a paired machine. In a dev + * build there's no published bin to run, so the daemon is started by the repo's + * `make run`, which writes its pairing code to the daemon log rather than printing + * it on screen. Surfacing the right instruction per environment keeps the pairing + * screens honest. */ export const pairingInstructions: PairingInstructions = dev ? { command: 'make run', codeLocation: '.run-state/daemon.log' } - : { command: 'npx telecode', codeLocation: null }; + : { command: 'npx @telecode/cli', codeLocation: null }; diff --git a/apps/web/tests/e2e/activate.spec.ts b/apps/web/tests/e2e/activate.spec.ts index f267ecd..f6c904e 100644 --- a/apps/web/tests/e2e/activate.spec.ts +++ b/apps/web/tests/e2e/activate.spec.ts @@ -22,10 +22,10 @@ test('a signed-in user activates a device with its pairing code', async ({ page, await expect(page).toHaveURL(/\/activate$/); // The pairing instruction is environment-aware: a dev build (this suite runs against `vite dev`) - // points at `make run` and the daemon log, never the not-yet-published `npx telecode`. + // points at `make run` and the daemon log, never the published `npx @telecode/cli`. await expect(page.getByRole('main')).toContainText('make run'); await expect(page.getByRole('main')).toContainText('.run-state/daemon.log'); - await expect(page.getByRole('main')).not.toContainText('npx telecode'); + await expect(page.getByRole('main')).not.toContainText('npx @telecode/cli'); await page.getByLabel('Pairing code').fill(user_code); await page.getByRole('button', { name: 'Activate device' }).click(); diff --git a/docs/deploy-azure.md b/docs/deploy-azure.md index 2d855db..67c501c 100644 --- a/docs/deploy-azure.md +++ b/docs/deploy-azure.md @@ -9,7 +9,7 @@ This is the production runbook for hosting telecode's control plane on **Azure C `telecode.io`) is deployed separately (e.g. Vercel) and is out of scope for this guide. > **Remember:** agents do not run in the cloud. The daemon runs on each user's own machine via -> `npx telecode`; Azure only hosts the relay + web. Execution stays local — that's the product promise. +> `npx @telecode/cli`; Azure only hosts the relay + web. Execution stays local — that's the product promise. The IaC (`infra/azure/main.bicep`) provisions: an Azure Container Registry, a Container Apps Environment (+ Log Analytics), a managed identity (with AcrPull), and the two apps. `.github/workflows/deploy.yml` @@ -171,7 +171,7 @@ trigger in `deploy.yml` to make deploys manual-only.) 1. Browse `https://app.telecode.io` → the sign-in shows **"Continue with GitHub"** → GitHub OAuth → authenticated dashboard. -2. On a laptop: `npx telecode --relay-url wss://relay.telecode.io/ws` → it prints a pairing code → enter it +2. On a laptop: `npx @telecode/cli --relay-url wss://relay.telecode.io/ws` → it prints a pairing code → enter it in the web app → the device shows online. 3. Launch a session, watch output stream, and approve a tool call — this proves the full wss routing path (browser ↔ relay ↔ daemon) end-to-end. diff --git a/docs/publishing.md b/docs/publishing.md index 0027b7d..91cb229 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -1,9 +1,17 @@ # Publishing the `telecode` CLI -This is the maintainer runbook for shipping the daemon to npm so that `npx telecode` and the one-line -installer work on any machine. **telecode is not published yet** — the wiring is in place -(`packages/daemon` exposes a `telecode` bin, ships its TypeScript source, and carries `tsx` as a runtime -dependency), but the actual publish is deferred until the npm name and account are settled. +This is the maintainer runbook for shipping the daemon to npm so that `npx @telecode/cli` and the +one-line installer work on any machine. + +**Status: published.** The CLI is live on npm as **`@telecode/cli`** and the wire contract as +**`@telecode/protocol`**. The installed binary is still the `telecode` command (the `bin` name is +independent of the package name), so `npm i -g @telecode/cli` gives you a `telecode` command, and the +`curl | sh` installer hides the scope entirely. + +> **Why `@telecode/cli` and not bare `telecode`?** The unscoped `telecode` name on npm is owned by an +> unrelated, abandoned 2017 project, so we can't publish under it. We own the **`@telecode` org**, so +> scoped names are ours and can't be squatted. If the bare name is ever transferred to us, we can +> republish there and keep `@telecode/cli` as an alias. ## What's already wired @@ -16,44 +24,72 @@ dependency), but the actual publish is deferred until the npm name and account a - **Publish metadata is complete** on both `packages/daemon` and `packages/protocol`: `license` (`AGPL-3.0-only`), `repository` (with `directory`), `homepage`, `bugs`, `author`, `keywords`, `engines` (`node >=22`), and `publishConfig.access: public`. -- **Dry-run verified.** With `private` temporarily disabled, `pnpm --filter @telecode/protocol publish ---dry-run` and `pnpm --filter @telecode/daemon publish --dry-run` both succeed and produce clean tarballs - (pnpm rewrites the `workspace:*` protocol dependency to a real version range automatically). The packages - keep `private: true` in the repo as the accidental-publish guard until release. - **Installer** — `scripts/install-telecode.sh` is the `curl | sh` entry point (checks Node ≥ 22, then - `npm install -g telecode`). It is self-contained so it can be piped straight from curl. - -## The publish step (when ready) - -1. **Pick the npm name + account.** The command is `telecode`, so the published package should be named - `telecode` (not the internal `@telecode/daemon`). Reserve the name under the project's npm org. -2. **Resolve the workspace dependency.** The daemon imports `@telecode/protocol` (a `workspace:*` - dependency). A standalone publish needs that resolved one of two ways: - - **Publish `@telecode/protocol` publicly** (recommended): set it `private: false`, give it a real - version, `npm publish --access public`, then have the daemon depend on the published version range. - - **Bundle it** into the daemon publish (e.g. via `bundledDependencies` or a bundler) if keeping - protocol unpublished is preferred. -3. **Flip the daemon package for publish:** set `"name": "telecode"`, `"private": false`, and a real - `"version"`. (The rest of the manifest — `bin`, `files`, `tsx` runtime dep, and all the publish metadata - above — is already in place.) Note the daemon is referenced internally as `@telecode/daemon` by the - relay and web `devDependencies`; renaming it to `telecode` for publish means doing so as a release-time - step (or updating those references), so this flip is deliberately not committed to `main`. -4. **Publish:** `pnpm --filter telecode publish --access public` (after `pnpm install` and a green - `pnpm typecheck && pnpm lint && pnpm test`). Use `pnpm publish`, not bare `npm publish`, so the - `workspace:*` protocol dependency is rewritten to the published version. -5. **Host the installer:** serve `scripts/install-telecode.sh` at `https://telecode.io/install.sh` + `npm install -g @telecode/cli`). It is self-contained so it can be piped straight from curl. + +## The repo keeps the publish manifest reverted + +Both packages stay `private: true` with `version: 0.0.0` on `main`, and the daemon keeps its internal +name `@telecode/daemon` (the relay and web reference it under that name). The publish-time manifest +changes are **deliberately not committed** — they're applied locally, used to publish, then reverted. +This keeps `main` clean and acts as an accidental-publish guard. + +## Auth: a bypass-2FA token + +npm requires 2FA to publish to the public registry. Account 2FA via a **security key** can't produce +the rotating OTP the CLI needs, so publishing uses a **granular access token** instead: + +1. npmjs.com → **Access Tokens** → **Generate New Token** → Granular (or Classic → Automation). +2. **Read and write** permission, scoped to the **`@telecode`** org/packages, and **Bypass two-factor + authentication (2FA)** checked. +3. Store it in `~/.npmrc` (never in the repo or `.env` — npm only reads `.npmrc`): + ```sh + npm config set //registry.npmjs.org/:_authToken=npm_YOUR_TOKEN_HERE + ``` + +## The publish step + +Run from a green tree (`pnpm typecheck && pnpm lint && pnpm test`). Publish **protocol first** — the +daemon depends on it, and `pnpm publish` rewrites the `workspace:*` dependency to the published version. + +1. **Bump + unguard `@telecode/protocol`:** set `private: false` and a real `version` (e.g. `0.1.0`). + ```sh + pnpm --filter @telecode/protocol publish --access public --no-git-checks + ``` + (`--no-git-checks` because the temporary manifest edits leave the tree dirty.) +2. **Flip the daemon for publish:** in `packages/daemon/package.json` set `"name": "@telecode/cli"`, + `"private": false`, and the same `"version"`. Leave the `@telecode/protocol` dependency as + `workspace:*` — pnpm rewrites it to the published version on pack. + ```sh + cd packages/daemon && pnpm publish --access public --no-git-checks + ``` + Sanity-check the rewrite before/after with `pnpm pack` and inspect the packed `package.json` + `dependencies` — `@telecode/protocol` must be a real version, never `workspace:*`. +3. **Revert the manifest edits** so `main` stays clean: + ```sh + git checkout packages/protocol/package.json packages/daemon/package.json + ``` +4. **Host the installer:** serve `scripts/install-telecode.sh` at `https://telecode.io/install.sh` so `curl -fsSL https://telecode.io/install.sh | bash` works. -6. **Verify on a clean machine:** `npx telecode doctor` (preflight) and `npx telecode` (pairing) with no - repo checkout. +5. **Verify on a clean machine / empty dir** (npm's edge may cache a stale 404 on a brand-new package + for a few minutes — query the version-specific manifest, e.g. `npm view @telecode/cli@0.1.0`, to + confirm the publish landed): + ```sh + npm install -g @telecode/cli + telecode doctor # preflight: Node, API key, pairing, relay reachability + ``` -## Install (post-publish) +## Install (for users) ```sh -# one-liner +# one-liner (once telecode.io/install.sh is hosted) curl -fsSL https://telecode.io/install.sh | bash # or directly -npm install -g telecode +npm install -g @telecode/cli telecode doctor # preflight: Node, API key, pairing, relay reachability telecode # pair this machine, then control it from any browser + +# or run without installing +npx @telecode/cli ``` diff --git a/docs/self-hosting.md b/docs/self-hosting.md index ad9f4e6..3a1aae5 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -66,7 +66,7 @@ The relay is the only publicly reachable piece, so it ships safe to leave runnin ## 3. Point your daemon + browser at it - **Daemon (the laptop running agents):** point it at your relay with the `--relay-url` flag, e.g. - `npx telecode --relay-url wss://relay.example.com/ws` (or set `TELECODE_RELAY_URL`; the flag wins). The + `npx @telecode/cli --relay-url wss://relay.example.com/ws` (or set `TELECODE_RELAY_URL`; the flag wins). The URL must be `ws://` or `wss://`. On first run it prints a pairing code; enter it in the web app to bind the device. - **Web app (the PWA):** set `PUBLIC_TELECODE_RELAY_URL` to the same relay, and — if you enabled push — diff --git a/packages/daemon/src/main.ts b/packages/daemon/src/main.ts index 005531b..5ca98b8 100644 --- a/packages/daemon/src/main.ts +++ b/packages/daemon/src/main.ts @@ -13,7 +13,7 @@ import { createGitRepoManager } from './sessions/repo-manager'; import { createGitWorktreeManager } from './sessions/worktree-manager'; /** - * Daemon entry point (`npx telecode`). On first run it pairs this device (prints a code to enter in the + * Daemon entry point (`npx @telecode/cli`). On first run it pairs this device (prints a code to enter in the * web app), generates an X25519 keypair, and saves credentials to `~/.telecode/credentials.json`. On * later runs it loads the saved token and reconnects — no re-pairing. */ diff --git a/packages/daemon/src/packaging.test.ts b/packages/daemon/src/packaging.test.ts index c1e2e05..1f67be0 100644 --- a/packages/daemon/src/packaging.test.ts +++ b/packages/daemon/src/packaging.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest'; /** * Packaging guards for the `telecode` CLI wiring (Phase 4 T13). These lock the install path that makes - * `npx telecode` / a one-line installer work: a `bin` that exposes the `telecode` command, a runnable + * `npx @telecode/cli` / a one-line installer work: a `bin` that exposes the `telecode` command, a runnable * launcher shim, `tsx` as a runtime dependency (the daemon ships as TypeScript, no build step — mirroring * the relay), and an end-user installer script. We do not publish here (A5); these just keep the wiring * honest so a regression can't silently break the published command. diff --git a/packages/daemon/src/relay-url.test.ts b/packages/daemon/src/relay-url.test.ts index 1dc9390..c154493 100644 --- a/packages/daemon/src/relay-url.test.ts +++ b/packages/daemon/src/relay-url.test.ts @@ -5,7 +5,7 @@ import { resolveRelayUrl } from './relay-url'; /** * The daemon's relay URL resolution. Precedence: the `--relay-url` CLI flag overrides the * `TELECODE_RELAY_URL` env, which overrides the default. The default targets the hosted relay so a user who - * runs `npx telecode` connects to the managed instance with no flags; a self-hoster points at their own + * runs `npx @telecode/cli` connects to the managed instance with no flags; a self-hoster points at their own * relay via `--relay-url` / `TELECODE_RELAY_URL` (local dev sets the env in `scripts/run.sh`). The resolved * value is validated as a ws/wss URL so a typo fails fast with a clear message instead of a cryptic socket * error. diff --git a/packages/daemon/src/relay-url.ts b/packages/daemon/src/relay-url.ts index 9027b9b..113cdcb 100644 --- a/packages/daemon/src/relay-url.ts +++ b/packages/daemon/src/relay-url.ts @@ -4,7 +4,7 @@ * 2. the `TELECODE_RELAY_URL` environment variable, * 3. the default — the hosted relay (`wss://relay.telecode.io/ws`). * - * The default targets the managed instance so `npx telecode` connects with no configuration. Self-hosters + * The default targets the managed instance so `npx @telecode/cli` connects with no configuration. Self-hosters * point at their own relay via the flag or env (local dev sets `TELECODE_RELAY_URL` in `scripts/run.sh`). * The result is validated as a `ws://`/`wss://` URL so a typo fails immediately with a clear message * instead of a cryptic socket error (and so the HTTP base derived from it stays well-formed). diff --git a/scripts/install-telecode.sh b/scripts/install-telecode.sh index f4b9bca..797af58 100755 --- a/scripts/install-telecode.sh +++ b/scripts/install-telecode.sh @@ -8,13 +8,13 @@ # runs Claude Code agents locally and dials OUT to the relay — nothing ever reaches into your machine. # Self-contained on purpose (no repo checkout needed): safe to pipe straight from curl. # -# NOTE: telecode is not yet published to npm — see docs/publishing.md for the maintainer publish runbook. -# Once published, this script works as-is. +# The published package is `@telecode/cli`; its bin is the `telecode` command. See docs/publishing.md +# for the maintainer publish runbook. set -euo pipefail MIN_NODE_MAJOR=22 -PACKAGE="telecode" +PACKAGE="@telecode/cli" # Colors only when stdout is a TTY (a piped install stays plain). if [ -t 1 ]; then