Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/site/src/lib/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/lib/onboarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
});

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/

Expand Down
12 changes: 6 additions & 6 deletions apps/web/src/lib/pairing-instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
4 changes: 2 additions & 2 deletions apps/web/tests/e2e/activate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions docs/deploy-azure.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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.
Expand Down
104 changes: 70 additions & 34 deletions docs/publishing.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
```
2 changes: 1 addition & 1 deletion docs/self-hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 —
Expand Down
2 changes: 1 addition & 1 deletion packages/daemon/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/daemon/src/packaging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion packages/daemon/src/relay-url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion packages/daemon/src/relay-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
6 changes: 3 additions & 3 deletions scripts/install-telecode.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading