From 71b318e9b9325941667edd543819950e4f6612de Mon Sep 17 00:00:00 2001 From: Yash Datta Date: Wed, 29 Jul 2026 21:48:49 +0800 Subject: [PATCH] devops: publish from the Highflame org as @highflame/codeoid, 0.4.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Packages the release-prep change that was sitting uncommitted in the working tree. The rename itself is not my work — I committed it as-is and verified it, rather than authoring it. What it does: - the npm package becomes `@highflame/codeoid`, with the workspaces renamed to match (`@highflame/codeoid-protocol`, `@highflame/codeoid-core`) and every import updated - `publishConfig.access: public`, since a scoped package defaults to restricted and would otherwise fail to publish - version 0.4.0 - README / CHANGELOG / RELEASING / workflow updates, including the note that the unscoped `codeoid` package is deprecated and stops at 0.3.4 The command stays `codeoid`. Verified before committing, because a rename is exactly the change that looks fine and breaks resolution: - zero remaining `@codeoid/` references anywhere under `src/`, `packages/`, or `web/src/` — the rename is total, not partial - biome clean over 346 files; `tsc --noEmit` clean across the root and both workspaces - `bun run build` succeeds, so workspace resolution actually works under the new names rather than only type-checking - full suite: 2172 pass, 12 skip, 0 fail Kept as its own commit and PR deliberately. It touches package identity and version — a publishing decision with consequences for anyone installing the old name — and folding it into a feature branch would have buried that under unrelated review. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 5 + .github/workflows/release.yml | 65 +++---- CHANGELOG.md | 52 ++++++ README.md | 10 +- RELEASING.md | 112 ++++++++++-- bun.lock | 23 +-- docs/COMPARISON.md | 2 +- docs/POSITIONING.md | 101 +++++++++++ docs/conductor-build-plan.md | 2 +- docs/conductor-frontends-design.md | 4 +- docs/mobile-app-design.md | 271 ++++++++++++++++++++++++++++ docs/provider-followups-handoff.md | 256 ++++++++++++++++++++++++++ package.json | 16 +- packages/core/README.md | 8 +- packages/core/package.json | 6 +- packages/core/src/approvals.test.ts | 2 +- packages/core/src/approvals.ts | 2 +- packages/core/src/client.ts | 4 +- packages/core/src/identity.test.ts | 2 +- packages/core/src/identity.ts | 2 +- packages/core/src/messages.test.ts | 2 +- packages/core/src/messages.ts | 2 +- packages/core/src/resume.test.ts | 2 +- packages/core/src/resume.ts | 2 +- packages/core/src/slash.ts | 2 +- packages/protocol/README.md | 8 +- packages/protocol/package.json | 4 +- packages/protocol/src/schemas.ts | 4 +- scripts/check-versions.ts | 81 +++++++++ scripts/release-smoke.sh | 8 +- scripts/set-version.ts | 62 +++++++ src/daemon/local-auth.ts | 2 +- src/daemon/pipeline/pack-service.ts | 2 +- src/daemon/server.ts | 2 +- src/daemon/verifier.ts | 2 +- src/frontends/telegram/index.ts | 2 +- src/protocol/index.ts | 6 +- src/protocol/scopes.ts | 4 +- src/protocol/types.ts | 4 +- src/tests/collaboration.test.ts | 2 +- src/tests/local-auth.test.ts | 21 ++- src/tests/local-mode-server.test.ts | 2 +- src/tui/ansi/render-message.ts | 2 +- web/bun.lock | 19 +- web/package.json | 8 +- web/src/components/prompt/slash.ts | 6 +- web/src/lib/approvals.ts | 4 +- web/src/lib/format.ts | 8 +- web/src/lib/identity.ts | 4 +- web/src/lib/sanitize-url.ts | 4 +- web/src/lib/usage-days.ts | 6 +- web/src/lib/ws.ts | 8 +- web/src/protocol/types.ts | 6 +- web/src/state/messages.ts | 4 +- web/src/state/resume.ts | 4 +- 55 files changed, 1082 insertions(+), 172 deletions(-) create mode 100644 docs/POSITIONING.md create mode 100644 docs/mobile-app-design.md create mode 100644 docs/provider-followups-handoff.md create mode 100644 scripts/check-versions.ts create mode 100644 scripts/set-version.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e1ec413..3146190 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,11 @@ jobs: - name: Lint run: bun run lint + # Catch npm-version drift between the CLI and its workspace packages on + # the PR, not at tag time when the release is already in flight. + - name: Version lockstep + run: bun run check:versions + - name: Typecheck run: bun run typecheck diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 41a2b8e..80f1769 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: permissions: contents: write # create the GitHub Release - id-token: write # npm provenance (public repo) + id-token: write # npm OIDC Trusted Publishing + provenance jobs: npm: @@ -34,55 +34,36 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile + # Every publishable package ships at the SAME version, and the tag is that + # version. One gate replaces per-package version bookkeeping: if this + # passes, all three publishes below are known-correct and known-new. + - name: Verify workspace versions are in lockstep with the tag + run: bun run check:versions "${GITHUB_REF_NAME}" + - name: Build web UI run: cd web && bun install --frozen-lockfile && bun run build - name: Test run: bun run test - - name: Verify package version matches the tag - run: | - PKG="$(node -p "require('./package.json').version")" - TAG="${GITHUB_REF_NAME#v}" - if [ "$PKG" != "$TAG" ]; then - echo "::error::package.json version ($PKG) does not match tag ($TAG)" - exit 1 - fi - - # @codeoid/protocol is a workspace dependency of codeoid; publish it FIRST - # (only when its version is new) so codeoid's published dep resolves. - # NOTE: requires a separate npm Trusted Publisher configured for - # @codeoid/protocol at npmjs.com (same OIDC setup as codeoid) before the - # first release that introduces a new protocol version. - - name: Publish @codeoid/protocol (OIDC) — only if version is new - run: | - cd packages/protocol - VER="$(node -p "require('./package.json').version")" - if npm view "@codeoid/protocol@$VER" version >/dev/null 2>&1; then - echo "@codeoid/protocol@$VER already published — skipping" - else - npm publish --provenance --access public - fi + # Publish order is dependency order: protocol → core → the CLI, so each + # tarball's declared deps are resolvable on the registry the moment it + # lands. No NODE_AUTH_TOKEN anywhere — auth is the GitHub OIDC id-token + # via each package's Trusted Publisher (configured at npmjs.com), and + # provenance is generated from that same identity. `--access public` is + # not passed: every manifest carries `publishConfig.access`, so a manual + # publish and this pipeline run the exact same command. + # + # A new package NAME requires one manual bootstrap publish before a + # Trusted Publisher can be configured for it — see RELEASING.md. + - name: Publish @highflame/codeoid-protocol + run: cd packages/protocol && npm publish --provenance - # @codeoid/core (client transport + store semantics — consumed by the - # mobile app from the registry; web uses the in-repo copy). Publishes - # after protocol (its peer dep) and needs its own Trusted Publisher + - # one-time manual bootstrap publish, same as protocol. - - name: Publish @codeoid/core (OIDC) — only if version is new - run: | - cd packages/core - VER="$(node -p "require('./package.json').version")" - if npm view "@codeoid/core@$VER" version >/dev/null 2>&1; then - echo "@codeoid/core@$VER already published — skipping" - else - npm publish --provenance --access public - fi + - name: Publish @highflame/codeoid-core + run: cd packages/core && npm publish --provenance - # No NODE_AUTH_TOKEN: auth comes from the GitHub OIDC id-token via npm's - # Trusted Publisher (configured on the package at npmjs.com). Provenance - # is generated automatically from the same OIDC identity. - - name: Publish codeoid to npm (OIDC Trusted Publishing) - run: npm publish --provenance --access public + - name: Publish @highflame/codeoid + run: npm publish --provenance - name: Create GitHub Release uses: softprops/action-gh-release@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index a739508..97ba8fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,58 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.4.0] - 2026-07-29 + +codeoid moves to the Highflame npm org. npm has no way to transfer a package +between scopes, so all three published packages are renamed rather than moved, +and their versions are unified onto one line so a single tag cuts the whole +release. + +### Changed + +- **Renamed on npm** — the packages now ship from the `@highflame` scope: + + | Before | After | + | ------------------- | ----------------------------- | + | `codeoid` | `@highflame/codeoid` | + | `@codeoid/protocol` | `@highflame/codeoid-protocol` | + | `@codeoid/core` | `@highflame/codeoid-core` | + + Install with `bun install -g @highflame/codeoid`. The binary is still + `codeoid`, and no runtime behaviour changed. The predecessor packages are + deprecated on npm and frozen at `0.3.4` / `0.2.0` / `0.2.0`; existing installs + keep resolving. Downstream consumers (the mobile app, any external importer) + need to update their dependency names — the import specifiers changed with the + package names. + +- **Lockstep versioning.** All three packages now ship at the same version from + one `vX.Y.Z` tag, replacing the per-package "publish only if the version is + new" logic in the release workflow. `bun run check:versions` enforces it on + every PR, in `bun run smoke`, and against the tag at release time; + `bun run version:set ` is the only sanctioned way to bump. The wire + protocol version (`PROTOCOL_VERSION`) remains a separate, independently-moving + number. + +### Fixed + +- **The client core was an undeclared dependency of the CLI.** The TUI renderer + and the Telegram frontend both statically import `formatCollaborationCost` from + the client core (added in 8e709ec, after `0.3.4`), but it was never listed in + the CLI's `dependencies` — only the protocol package was. A registry install + would have had no client core in `node_modules` and failed to resolve the + import on any code path reaching either module, `codeoid attach` among them. + Caught before it shipped: `0.3.4` predates the import and is unaffected. Now + declared, and verified by installing the packed tarball and resolving that + module. + +- **`web/` no longer needs the registry to install.** `web/` is a separate + install root, so the workspace packages come in as `file:` copies — but the + core package's peer range on the protocol package was still resolved from the + registry, meaning a fresh `bun install` in `web/` depended on what had been + published rather than on the working tree (and broke outright for a + not-yet-published package name). An `overrides` entry pins that peer to the + in-repo copy. + ## [0.3.4] - 2026-07-23 The governed pipeline becomes reliable end-to-end. Phase boundaries are now diff --git a/README.md b/README.md index bad6228..7b92921 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Memory persists across sessions, so each agent inherits what the last one learne Every action is auditable, and with [ZeroID](https://github.com/highflame-ai/zeroid) every agent and sub-agent carries a cryptographic identity. ```bash -bun install -g codeoid +bun install -g @highflame/codeoid codeoid start --local # no account, no login, no config — running in seconds ``` @@ -67,12 +67,16 @@ Don't mix the two. Codeoid runs on Bun, so install it with Bun (`npm` also works, as long as Bun is on your `PATH` — it's the runtime): ```bash -bun install -g codeoid # or: npm install -g codeoid -codeoid --help # confirm it's on your PATH +bun install -g @highflame/codeoid # or: npm install -g @highflame/codeoid +codeoid --help # confirm it's on your PATH ``` If `codeoid` isn't found afterward, your global-bin directory isn't on `PATH` — add Bun's (`~/.bun/bin`) to it, or use path **B**. +> **Renamed.** codeoid used to publish as the unscoped `codeoid` package. It now +> ships from the Highflame org as **`@highflame/codeoid`**; the old package is +> deprecated on npm and stops at `0.3.4`. The command is still `codeoid`. + **B. From source — to hack on it.** Clone, then **run `bun install` before anything else**: diff --git a/RELEASING.md b/RELEASING.md index 64a7e58..164e5f3 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,21 +1,41 @@ # Releasing codeoid -Releases are automated. Pushing a `vX.Y.Z` tag publishes `codeoid` to npm with -[provenance](https://docs.npmjs.com/generating-provenance-statements) and opens a -GitHub Release — see [`.github/workflows/release.yml`](.github/workflows/release.yml). +Three packages ship from this repo, all under the `@highflame` npm scope: -## One-time setup +| Package | Directory | Who consumes it | +| ------------------------------ | ------------------- | ------------------------------------------------ | +| `@highflame/codeoid` | repo root | end users — installs the `codeoid` CLI + daemon | +| `@highflame/codeoid-protocol` | `packages/protocol` | the daemon, the web UI, the mobile app | +| `@highflame/codeoid-core` | `packages/core` | the daemon, the web UI, the mobile app | -Add an **`NPM_TOKEN`** repository secret (Settings → Secrets and variables → -Actions → New repository secret): +## The one rule: lockstep versions -1. On npmjs.com → **Access Tokens** → create a **Granular** (or Automation) token - with publish rights for the `codeoid` package. -2. Save it as `NPM_TOKEN`. +**All three packages always ship at the same version, cut from one `vX.Y.Z` tag.** + +That is the entire contract the pipeline rests on. One tag produces three +publishes in dependency order, with no per-package "is this version new?" +bookkeeping and no drift to reconcile by hand. `bun run check:versions` enforces +it — in CI on every PR, in `bun run smoke`, and again in the release workflow +against the tag itself — so a partial bump fails long before it reaches npm. + +Bump with the script, never by hand; it rewrites all three `version` fields plus +the internal `^` dependency and peer ranges in one pass: + +```bash +bun run version:set 0.5.0 +bun install && (cd web && bun install) # refresh both lockfiles +``` + +> **The wire protocol version is a separate number.** `PROTOCOL_VERSION` in +> [`packages/protocol/src/types.ts`](packages/protocol/src/types.ts) moves only on +> wire-breaking changes, and stays in lockstep with the `codeoid-protocol` crate +> in [codeoid-ui](https://github.com/highflame-ai/codeoid-ui). npm versions being +> in lockstep says nothing about wire compatibility, and bumping one is never a +> reason to bump the other — the handshake negotiates. ## Cutting a release -1. Bump `version` in [`package.json`](package.json) (SemVer). +1. `bun run version:set X.Y.Z`, then refresh both lockfiles. 2. Move the `## [Unreleased]` notes into a new `## [X.Y.Z]` section in [`CHANGELOG.md`](CHANGELOG.md). 3. Open a PR, get CI green, merge to `main`. @@ -27,21 +47,75 @@ Actions → New repository secret): git push origin vX.Y.Z ``` -The workflow then builds the web UI, runs the test suite, verifies the tag matches -`package.json`, `npm publish`es, and creates the GitHub Release. Install with: +[`.github/workflows/release.yml`](.github/workflows/release.yml) then builds the +web UI, runs the test suite, verifies the three versions match the tag, publishes +protocol → core → CLI with +[provenance](https://docs.npmjs.com/generating-provenance-statements), and opens a +GitHub Release. Install with: ```bash -bun install -g codeoid # or: bunx codeoid +bun install -g @highflame/codeoid # or: bunx @highflame/codeoid ``` +The binary is still called `codeoid`. + +## One-time setup — npm Trusted Publishing (OIDC) + +There are **no npm tokens** in this repo. Publishing authenticates with the +GitHub Actions OIDC id-token against a Trusted Publisher configured on each +package at npmjs.com, and provenance is generated from that same identity. + +npm can only attach a Trusted Publisher to a package that **already exists**, so +each new package name needs exactly one manual bootstrap publish first. This is a +per-package-name chore, not a per-release one. + +**Prerequisite:** publish rights on the `@highflame` npm org (an org member with +at least write access to the packages, or an org admin who can create them). + +```bash +npm login # must be the @highflame org member + +bun install +cd web && bun install --frozen-lockfile && bun run build && cd .. +bun run check:versions + +# Dependency order. No --provenance: that requires a CI OIDC token and will +# fail locally. No --access public: publishConfig.access covers it. +cd packages/protocol && npm publish && cd ../.. +cd packages/core && npm publish && cd ../.. +npm publish # @highflame/codeoid — runs prepublishOnly (web build) +``` + +Then, for **each** of the three packages, at +`npmjs.com/package//access` → **Trusted Publisher** → *GitHub Actions*: + +| Field | Value | +| ----------------- | --------------- | +| Organization | `highflame-ai` | +| Repository | `codeoid` | +| Workflow filename | `release.yml` | +| Environment | *(leave empty)* | + +After that, every subsequent release goes through the tag → workflow path above +and nothing needs a token again. + ## Notes -- **codeoid runs under [Bun](https://bun.sh).** The published package ships `src/` +- **codeoid runs under [Bun](https://bun.sh).** The published CLI ships `src/` plus the prebuilt `web/dist`, and points `bin` at `src/cli.ts` (Bun executes TypeScript directly — no bundling step, and the web UI path resolves inside the package). -- The **wire protocol** is versioned independently via `PROTOCOL_VERSION` in - [`src/protocol/types.ts`](src/protocol/types.ts). Bump it only on wire-breaking - changes, and keep it in lockstep with the `codeoid-protocol` crate in - [codeoid-ui](https://github.com/highflame-ai/codeoid-ui). App versions and the protocol - version move independently; the handshake negotiates compatibility. +- **`web/` is its own install root** with its own lockfile, so the workspace + packages come in as `file:` copies rather than workspace links. Bun *copies* + them at install time, so after editing `packages/protocol` or `packages/core` + you must re-run `bun install` in `web/` to refresh the copy. The `overrides` + entry in [`web/package.json`](web/package.json) pins the transitive + `@highflame/codeoid-protocol` peer to the in-repo copy too — without it, a + fresh `bun install` in `web/` would try to satisfy that peer from the registry + and the web build would silently depend on what is published rather than on the + working tree. +- **Predecessor packages.** codeoid was previously published from a personal + account as `codeoid`, `@codeoid/protocol`, and `@codeoid/core`. npm has no way + to move a package between scopes, so those names were retired in favour of the + `@highflame` ones and deprecated in place (existing installs keep resolving). + The last version published under the old names was `0.3.4` / `0.2.0` / `0.2.0`. diff --git a/bun.lock b/bun.lock index 020dd20..aa43504 100644 --- a/bun.lock +++ b/bun.lock @@ -3,12 +3,13 @@ "configVersion": 0, "workspaces": { "": { - "name": "codeoid", + "name": "@highflame/codeoid", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.220", - "@codeoid/protocol": "^0.2.0", "@google/generative-ai": "^0.24.1", "@grammyjs/auto-retry": "^2.0.2", + "@highflame/codeoid-core": "^0.4.0", + "@highflame/codeoid-protocol": "^0.4.0", "@highflame/sdk": "^0.3.18", "@xenova/transformers": "^2.17.2", "commander": "^13.0.0", @@ -32,15 +33,15 @@ }, }, "packages/core": { - "name": "@codeoid/core", - "version": "0.2.0", + "name": "@highflame/codeoid-core", + "version": "0.4.0", "peerDependencies": { - "@codeoid/protocol": "^0.1.0", + "@highflame/codeoid-protocol": "^0.4.0", }, }, "packages/protocol": { - "name": "@codeoid/protocol", - "version": "0.2.0", + "name": "@highflame/codeoid-protocol", + "version": "0.4.0", "peerDependencies": { "zod": "^4.0.0", }, @@ -140,10 +141,6 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], - "@codeoid/core": ["@codeoid/core@workspace:packages/core"], - - "@codeoid/protocol": ["@codeoid/protocol@workspace:packages/protocol"], - "@earendil-works/pi-agent-core": ["@earendil-works/pi-agent-core@0.80.6", "", { "dependencies": { "@earendil-works/pi-ai": "^0.80.6", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" } }, "sha512-Lvn89ko42h5ETUb6Z0Ku6ldskEqXaTdQBYvSa0+7bdG9V6rUEpXptv5e0OVZ1HDcvi8s6/2lGCQWsxKX+DFHNw=="], "@earendil-works/pi-ai": ["@earendil-works/pi-ai@0.80.6", "", { "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@google/genai": "1.52.0", "@mistralai/mistralai": "2.2.6", "@opentelemetry/api": "1.9.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.26.0", "partial-json": "0.1.7", "typebox": "1.1.38" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-7xfLk8sANBp+bpPEbjoOZTbPxsa+++b1JXAoSJsNa3vbs9AHHEclmvg54XLQcxH+fuwaeti/g2jeIfJ+mVYLpA=="], @@ -164,6 +161,10 @@ "@grammyjs/types": ["@grammyjs/types@3.25.0", "", {}, "sha512-iN9i5p+8ZOu9OMxWNcguojQfz4K/PDyMPOnL7PPCON+SoA/F8OKMH3uR7CVUkYfdNe0GCz8QOzAWrnqusQYFOg=="], + "@highflame/codeoid-core": ["@highflame/codeoid-core@workspace:packages/core"], + + "@highflame/codeoid-protocol": ["@highflame/codeoid-protocol@workspace:packages/protocol"], + "@highflame/sdk": ["@highflame/sdk@0.3.18", "", { "peerDependencies": { "@aws/bedrock-ai-agents-strands": ">=1.0.0", "@azure/ai-projects": ">=2.0.0", "@langchain/core": ">=0.3.0", "@langchain/langgraph": ">=1.0.0", "@opentelemetry/api": "^1.4.0", "@strands-agents/sdk": ">=1.0.0" }, "optionalPeers": ["@aws/bedrock-ai-agents-strands", "@azure/ai-projects", "@langchain/core", "@langchain/langgraph", "@opentelemetry/api", "@strands-agents/sdk"] }, "sha512-+crXG4VCkrmAfBUGe4eDIE0ICCVLswL5MsXKCHu7ZnBv/s/ubziNrAdqAZPtRAkpwo6FomtvDqHSxk7ORuR0+g=="], "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], diff --git a/docs/COMPARISON.md b/docs/COMPARISON.md index e427089..9056049 100644 --- a/docs/COMPARISON.md +++ b/docs/COMPARISON.md @@ -29,7 +29,7 @@ Codeoid is the latter, which is why the rows below weigh memory, attribution, an | **Built-in diff review + PR workflow** | ❌ | ✅ | ✅ | ~ | ❌ | ✅ editable diff viewer, inline comments, commit/push, PR status | ❌ not built — roadmap | | **Remote multi-machine reach** | ~ hosted Remote Control (Max plan) | ❌ | ❌ | ❌ | ~ server deploy | ✅ host registry + cloud relay routes by host key; wakes offline hosts | ~ one daemon, reached directly or through Telegram; no relay | | **Scheduled / unattended automations** | ❌ | ❌ | ❌ | ❌ | ~ | ✅ cron-scheduled agent sessions, versioned | ~ durable dispatch queue with lease reclaim and crash recovery, but no scheduler yet | -| **Programmable outward API** (drive it from other agents) | ~ Agent SDK | ❌ | ❌ | ❌ | ❌ | ✅ npm SDK + MCP server (workspaces, agents, tasks, hosts) | ~ `@codeoid/core` + `@codeoid/protocol` on npm, but transport-level; no control-plane MCP server | +| **Programmable outward API** (drive it from other agents) | ~ Agent SDK | ❌ | ❌ | ❌ | ❌ | ✅ npm SDK + MCP server (workspaces, agents, tasks, hosts) | ~ `@highflame/codeoid-core` + `@highflame/codeoid-protocol` on npm, but transport-level; no control-plane MCP server | | **Self-hostable, no vendor cloud** | ~ Bedrock/Vertex supported; CLI itself is closed | ❌ | ❌ | ✅ | ~ Docker for server deploy | ❌ relay, orgs, and automations require Superset cloud | ✅ `bun:sqlite` + WASM embeddings, no external services | | **OS-level sandbox** (filesystem + network isolation) | ~ permission modes | ❌ | ❌ | ❌ | ✅ secure OS sandbox | ❌ worktree isolation only; presets actively bypass agent sandboxes | ~ approval + autonomous budget, not OS-level | | **Credential brokering** (hide secrets from the agent) | ❌ | ❌ | ❌ | ❌ | ✅ broker access, hide creds | ❌ | ~ scoped ZeroID identity tokens | diff --git a/docs/POSITIONING.md b/docs/POSITIONING.md new file mode 100644 index 0000000..a9cedc1 --- /dev/null +++ b/docs/POSITIONING.md @@ -0,0 +1,101 @@ +# codeoid — Positioning & Competitive Strategy + +> **Internal working doc — untracked, not published.** Living reference; update as features land and the market moves. +> Last updated: **2026-07-14**. The landscape here moves in weeks — re-verify dated claims before relying on them. + +--- + +## One-sentence positioning + +**The identity-first, guardrailed, self-hostable control plane for heterogeneous coding agents — never-lossy memory, any harness, any OS, behind your own gateway.** + +Lead with *governed + local + neutral + never-lossy*. Demote remote/mobile/parallel-sessions to substrate — the platforms already commoditized those. + +--- + +## What codeoid is + +A local-first, MIT, Bun/TypeScript daemon that runs N parallel coding-agent sessions across repos, each on a pluggable backend (`claude` default; `openai`, `gemini`, `codex`, `pi`, `gemini-cli`), reachable from a Rust/Ratatui TUI, a cross-OS web UI, or Telegram, with device handoff. Daemon owns all state; clients are pure renderers. Verbatim cross-session workspace memory; per-agent + sub-agent cryptographic identity via ZeroID. + +--- + +## The four moats (lead here) + +1. **Never-lossy verbatim memory — an *architecture*, not a stance.** + Every tool call / result / reasoning block stored verbatim as an episode; hybrid recall (vector 0.55 + FTS5/BM25 0.25 + recency 0.12 + path-overlap 0.08); workspace-scoped (shared across git worktrees); auto-injected memory index; `recall()/recall_file()/timeline()`. Everyone else sits on the opposite pole — Cursor/Windsurf/Copilot/mem0 *extract facts*; Anthropic *summarizes* (compaction) and *clears* tool results (context editing). They can't "add verbatim" without rebuilding their pipeline. A Dec-2025 controlled ablation ([arXiv 2601.00821](https://arxiv.org/abs/2601.00821)) found verbatim beats extracted by ~16 pts (LoCoMo) — the corner codeoid occupies is under-served, not wrong. + +2. **True meta-harness.** + Claude / Codex / Gemini / OpenAI / pi (+ planned hermes/local) behind one `SessionProvider` interface; cross-backend fork (a Claude session forks onto Codex and resumes with full context). Anthropic *structurally cannot* do this; happy/vibetunnel don't; only Omnigent does — and it lacks the other three moats. + +3. **Identity + governance (the uncontested intersection).** + ZeroID SPIFFE/WIMSE identity on every agent and attenuated sub-agent; every tool call attributable. **No** platform vendor, bridge, or cockpit does per-agent identity. Omnigent does credential *hiding* (egress proxy), not attributable *identity* — a different axis. This rides the hottest enterprise-security trend of 2025-26 (Cisco→Astrix ~$400M closed Jun 2026; Aembit & Okta-for-AI-Agents GA; SPIFFE-for-agents; IETF WIMSE WG; MCP audit guidance demanding per-tool-call attribution). **codeoid's identity runs on ZeroID — our own stack — so it's uncopyable, and codeoid is the natural reference client for Highflame agent security.** + +4. **Lightweight-by-architecture + embedded + cross-OS.** + Bun-only; `bun:sqlite` (no external DB); `@xenova/transformers` WASM embeddings (no embedding service); no tmux, no Python, no Postgres/Redis. Install Bun → `bun install` → run. Contrast Omnigent's prereq chain (below). This is the *deployment* half of the sovereignty wedge: a regulated team can drop codeoid on a locked-down / headless / any-distro box with no toolchain. Web UI works on any OS — vs Claude Desktop's 2-week-old Debian-only beta. + +### Deployment footprint: codeoid vs Omnigent + +| | codeoid | Omnigent | +|---|---|---| +| Runtime | Bun only | Python 3.12+ **and** uv **and** git | +| Agent execution | Claude Agent SDK **in-process** + provider APIs | shells out to **tmux-wrapped PTY** per harness | +| Sandbox tooling | none yet (roadmap; will be opt-in) | **bwrap** (Linux, mandatory) / seatbelt | +| DB / memory | embedded `bun:sqlite` + WASM embeddings | — | +| Node/npm | only for codex/pi/gemini CLIs | **Node 22+** required for npm harnesses | +| Windows | web UI + daemon run | "degraded mode" (no native wrappers/sandbox) | +| External services | none | Docker (server deploy), optional Databricks | + +Honest caveat: part of Omnigent's weight *buys* the OS sandbox — the gap on our roadmap. When we add sandboxing we take on bwrap/seatbelt too, but we keep it **opt-in per session** so the zero-dep path stays trivial. + +--- + +## What's commoditized (do NOT lead here) + +- **Remote / mobile / multi-frontend + device handoff.** Anthropic shipped native **Remote Control** (laptop↔phone↔browser, 32 sessions), **Channels** (official Telegram/Discord/iMessage), **Agent View** (multi-session dashboard), **Session Memory** — Oct 2025→mid-2026. OSS: [happy](https://github.com/slopus/happy) (22k★, mobile+web+voice+E2E), vibetunnel, Omnara (YC). + - *Nuance that keeps a wedge:* Anthropic Remote Control is **Claude-only, Max-plan ($100-200/mo), requires api.anthropic.com, disabled behind an LLM gateway / on Bedrock/Vertex/Foundry, off-by-default on Enterprise**. happy is a *client wrapper* (Claude Code + Codex), **not a meta-harness** — no provider abstraction, memory, or identity. So "remote from any device, any harness, behind your gateway, no plan lock-in" is still open. +- **Parallel worktree sessions.** Table stakes: [Vibe Kanban](https://github.com/BloopAI/vibe-kanban) (27k★), [Claude Squad](https://github.com/smtg-ai/claude-squad) (8k★), Conductor (Mac), Crystal/Nimbalyst, Sculptor (containers), Cursor 2.0 (8-way), Warp. +- **Basic multi-model routing.** Cline, Aider, OpenHands, Kilo, OpenCode (185k★). + +--- + +## Roadmap, tiered by moat vs pace + +**Tier 1 — moat-defining (do first):** +- **Highflame guardrail hooks + ZeroID enforcement** — the single highest-leverage build: identity + inline guardrails + audit = the governed control plane nobody else has, uncopyable because we own the stack. +- **Local models + hermes** — fully-local / air-gapped / sovereign; where Anthropic & happy structurally can't go. Ties to our own hermes/anyagent work. +- **Sandboxing** — closes the biggest gap (worktrees share the host); feeds the security story. Keep it **opt-in per session** (bwrap/seatbelt) so the lightweight path survives. + +**Tier 2 — control-plane completeness (necessary):** +- **Conductor over all sessions** — finish the provider-agnostic conductor; it's what makes "control plane" true vs "session list." +- **Cross-OS web UI** — already true; make it a *headline*, not a footnote. + +**Tier 3 — table stakes (build thin, don't out-polish incumbents):** +- **Mobile app** — a thin renderer of the unique daemon (identity/memory/meta-harness); do **not** enter a polish race with happy (22k★). +- **Fully-local voice** — exploits a real gap (Claude Desktop has no voice on Linux); privacy + offline. Convenience, not moat. +- **Pre-installed feature-dev skills** — adoption/time-to-value; differentiates only if the skills are opinionated (e.g. the Highflame feature-dev workflow). + +--- + +## Strategic through-line + +One sentence no competitor can say: *"the identity-first, guardrailed, sandboxed, fully-local, never-lossy control plane that runs any harness on any OS behind your own gateway."* Everything in Tier 1 builds toward that. Convenience features are hygiene — necessary, but not where effort should concentrate. + +--- + +## Risks (name them honestly) + +- **Traction:** effectively a solo project, no visible adoption signal vs 8k–185k★ peers and $300M–$4B-funded incumbents. Engineering quality ≠ distribution. +- **Segment mortality:** Terragon (closest multi-surface analog) shut down Jan 2026; Roo Code shut May 2026; Crystal renamed. "Nice cockpit" isn't survivable alone. +- **Platform cadence:** Anthropic went from none→all of the commodity features in ~8 months. Never bet the moat on a feature they can ship. +- **Memory scaling:** verbatim-forever → store growth, retrieval precision decay (hard negatives), context-rot on injection, staleness (recalled diff wrong after later commits), stored prompt-injection / secret capture. Hardening path: verbatim-on-disk + compact index for injection, commit-aware staleness, RRF/reranker, injection+secret hygiene. +- **Surface spread:** a lot of roadmap for a solo project — concentrate on Tier 1. + +--- + +## Key sources (as of 2026-07-14) + +- Omnigent (Databricks, Apache-2.0, ~7.2k★, created 2026-06-11): [repo](https://github.com/omnigent-ai/omnigent) · [Databricks blog](https://www.databricks.com/blog/introducing-omnigent-meta-harness-combine-control-and-share-your-agents) +- Claude native overlap: [Remote Control](https://code.claude.com/docs/en/remote-control) · [Channels](https://code.claude.com/docs/en/channels) · [Claude Code on web](https://claude.com/blog/claude-code-on-the-web) · [Desktop on Linux (beta)](https://code.claude.com/docs/en/desktop-linux) +- Memory: [Anthropic context management](https://www.anthropic.com/news/context-management) · [Copilot Memory](https://docs.github.com/en/copilot/concepts/agents/copilot-memory) · [Zep/Graphiti](https://arxiv.org/abs/2501.13956) · verbatim ablation [arXiv 2601.00821](https://arxiv.org/abs/2601.00821) +- Agent identity: [SPIFFE for agents (HashiCorp)](https://www.hashicorp.com/en/blog/spiffe-securing-the-identity-of-agentic-ai-and-non-human-actors) · [IETF WIMSE](https://datatracker.ietf.org/wg/wimse/about/) · [Cisco→Astrix](https://blogs.cisco.com/news/cisco-announces-intent-to-acquire-astrix-security) +- Cockpits: [happy](https://github.com/slopus/happy) · [Vibe Kanban](https://github.com/BloopAI/vibe-kanban) · [Claude Squad](https://github.com/smtg-ai/claude-squad) · [container-use](https://github.com/dagger/container-use) diff --git a/docs/conductor-build-plan.md b/docs/conductor-build-plan.md index 13a17b0..013d262 100644 --- a/docs/conductor-build-plan.md +++ b/docs/conductor-build-plan.md @@ -374,7 +374,7 @@ the conductor purely by attaching to the conductor session. This *unblocks* mobi it builds its whole P0–P4 on today's attach/scrollback/session-list surface and surfaces the conductor the moment our P3+ lands. -**Shared prerequisite — extract `@codeoid/protocol` + `@codeoid/core`** (wire types +**Shared prerequisite — extract `@highflame/codeoid-protocol` + `@highflame/codeoid-core`** (wire types + WS client + reducers) from `codeoid` once, up front. Mobile P0 requires it; the conductor and `web/` benefit too (ends today's type triplication). Do it regardless of which track leads. diff --git a/docs/conductor-frontends-design.md b/docs/conductor-frontends-design.md index 23918c1..54288e7 100644 --- a/docs/conductor-frontends-design.md +++ b/docs/conductor-frontends-design.md @@ -265,7 +265,7 @@ The Rust `SessionInfo` is missing the `role` field entirely; add it first so the ## 11. Protocol addition (the one required daemon change) -Everything above rests on a single **additive, wire-additive, provider-agnostic** read+subscribe surface in `@codeoid/protocol`, mirrored in the Rust `codeoid-protocol` crate. +Everything above rests on a single **additive, wire-additive, provider-agnostic** read+subscribe surface in `@highflame/codeoid-protocol`, mirrored in the Rust `codeoid-protocol` crate. The daemon already holds every field; this only exposes it. Gate it with a new `fleet:read` scope. @@ -326,7 +326,7 @@ Per [conductor-build-plan.md](./conductor-build-plan.md), `main` is ruleset-prot User decision (2026-07-17): **docked-first** — skip the drawer/modal stepping-stone and go straight to the docked conductor surface. **P5.0 — The contract.** -Add `fleet.subscribe` / `fleet.snapshot.result` / `fleet.update` + the `fleet:read` scope to `@codeoid/protocol`; mirror in the Rust crate *and* add the missing `role` field. +Add `fleet.subscribe` / `fleet.snapshot.result` / `fleet.update` + the `fleet:read` scope to `@highflame/codeoid-protocol`; mirror in the Rust crate *and* add the missing `role` field. Daemon exposes the read surface from `dispatch_tasks` / `dispatch_events` + session population, pushing the *whole subtree* (not just active-session children). Ship the capability matrix as data. Files: daemon `src/daemon/{fleet.ts,server.ts,store.ts}` · `packages/protocol` · `crates/codeoid-protocol`. diff --git a/docs/mobile-app-design.md b/docs/mobile-app-design.md new file mode 100644 index 0000000..1ca1a58 --- /dev/null +++ b/docs/mobile-app-design.md @@ -0,0 +1,271 @@ +# Codeoid Mobile App — Design & Tech-Stack Decision + +> Status: **proposal** · Supersedes the stack recommendation in issue #43 · Depends on #42 (OAuth) · Conductor-aware (draft PR #51) +> +> Scope: a native iOS + Android client for the Codeoid daemon. The bar is *world-class* — faster and better-feeling than any coding/work app in this category — with **voice-first** control as a first-class mode, and an information architecture that graduates cleanly into the **conductor** (personal-assistant-over-a-fleet) model. + +--- + +## 0. TL;DR — the decisions + +| Decision | Recommendation | Why in one line | +| --- | --- | --- | +| **UI runtime** | **Expo + React Native (SDK 57 / RN 0.86)** — *not* Capacitor | Every hard requirement (fast virtualized streaming chat, voice-first, native push, OTA) has a mature, purpose-built answer in RN today; Capacitor caps the "feels native" ceiling exactly where our bar is highest. | +| **Web-UI reuse** | Reuse the **logic**, rewrite the **pixels**. Extract `@highflame/codeoid-protocol` + `@highflame/codeoid-core` (wire types, WS client, reducers, formatters) and share across daemon-web + mobile. | The valuable, portable 90% is the protocol/state layer, not the `.tsx`. Capacitor reuses the *wrong* 90%. | +| **Conductor readiness** | Build around the existing attach/scrollback model; make "the conductor" a pinnable **home surface**, session list a secondary **fleet view**. | Conductor is a *session role* driven by MCP tools — **zero new client↔daemon wire types**. The app that talks to one session already talks to the conductor. | +| **Auth** | Daemon URL → ZeroID API key **or** Google OAuth→ZeroID; token in device **Keychain/Keystore** (`expo-secure-store`), never localStorage. | Matches the daemon's existing two auth paths; #42 unblocks the OAuth leg. | +| **Push** | Ship a small **OSS relay** that holds APNs/FCM creds; daemon POSTs an (E2E-encrypted) event, relay fans out. Add `push.register`/`push.unregister` to the protocol. | A self-hosted daemon can't push to Apple/Google directly. This relay is also the natural **hosted/paid** surface. | +| **Open source?** | **Yes — open-source the client under a permissive license (Apache-2.0/MIT), open-core.** Monetize the **hosted relay + conductor cloud + ZeroID SaaS**, not the app binary. | This is a control plane holding keys to the user's machine; for this audience, a closed client is a trust non-starter. The moat is the daemon + conductor + identity + hosted relay, never the pixels. | + +--- + +## 1. Goals & non-goals + +**Goals** +- A genuinely native-feeling iOS + Android app: 120 Hz-smooth virtualized streaming transcript, instant session switching, no webview tells. +- **Voice-first**: dictate prompts, hear agent progress, and **approve/deny tool calls by voice** — usable hands-free (walking, driving, away from keyboard). +- Connect to any daemon by URL, authenticate with a ZeroID key **or** OAuth, store credentials in the platform secure store. +- **Push notifications** for the one thing a phone is uniquely good at: *"a session is blocked waiting on your approval."* +- An IA that becomes the **conductor** ("talk to your assistant; it runs the fleet") without a rewrite. + +**Non-goals (v1)** +- Editing files on-device / full IDE surface (read-only file peek is enough; the daemon is the source of truth). +- Replacing the Solid web UI or the Rust TUI — this is a *third* first-class frontend, peer to them. +- A bespoke sub-100 ms audio DSP pipeline (we lean on vendor voice SDKs; revisit only if latency becomes the product). + +--- + +## 2. Where this diverges from issue #43 (and why) + +Issue #43 recommends **Capacitor wrapping the existing SolidJS web UI** ("~90% reuse"), with React Native listed as the deferred alternative. This proposal **reverses that** for the end-state, while keeping #43's good instincts (PWA as a fast validation step, keychain tokens, deep-link OAuth, a push-token registration endpoint). + +The reversal turns on our own bar — *"world-class, better than any work/code app, really fast, full voice-first."* Capacitor's documented, structural failure modes land precisely on those axes: +- **Chat composer + keyboard**: WKWebView keyboard-resize choreography in a text-field-centric app is the #1 "this feels webby" tell, even with modern plugins. +- **Long virtualized transcript**: webview scroll physics on a streaming, virtualized list are good-but-not-native; we're targeting native. +- **Voice**: mic/audio-session control routed through plugins is a worse foundation than a first-class audio runtime for a voice-first product. +- **OTA**: no story as clean as EAS Update. + +The "90% reuse" claim is real but **reuses the wrong 90%** — the UI pixels. The *actually* portable and valuable 90% (wire types, WS client, state reducers, formatters) ports to **React Native too**; RN only forces a rewrite of the `.tsx` components, which are the cheap, replaceable layer (see §4). Net: Capacitor buys weeks of speed today and a forced RN migration later once it succeeds. + +**Kept from #43:** PWA (manifest + service worker on `web/`) as a 1-week installable validation build and permanent desktop-web "add to home screen" path; Keychain token storage; `codeoid://` deep-link OAuth callback; a daemon push-token registration endpoint (we make it a protocol message, §7). + +--- + +## 3. Tech-stack decision — Expo / React Native + +**Chosen: Expo SDK 57 + React Native 0.86** (New Architecture only; Hermes V1 default). + +State of the stack (mid-2026), the parts that matter for us: +- **New Architecture is finished, not in-flight** — bridgeless is the only thing that ships; two consecutive RN releases (0.83, 0.86) with zero breaking changes. Hermes V1 is the default compiler/GC. +- **Streaming chat is now a solved category in RN:** + - **FlashList v2** (Shopify) — rewritten for the New Architecture, `maintainVisibleContentPosition` on by default, purpose-built for chat/feed UIs including non-inverted chat. + - **react-native-streamdown** (Software Mansion Labs) — parses *incomplete* markdown on a background thread via worklets, so the JS thread stays free during token floods. GFM + LaTeX + code. This is exactly our transcript's hardest problem. +- **Voice is best-in-class here:** `expo-audio` `useAudioStream` (realtime mic), `expo-speech-recognition` (New-Arch TurboModule over iOS `SFSpeechRecognizer` / Android `SpeechRecognizer`), `expo-speech` (TTS), plus first-class **LiveKit** and **ElevenLabs Agents** Expo plugins for full conversational voice. No other cross-platform stack has this density of voice-AI SDKs. +- **OTA that a 2-person team needs:** EAS Update with Hermes-bytecode diffing (~75 % smaller downloads), policy-compliant under Apple §3.3.1(B) for JS-only fixes (new features still go through review). +- **Reuse from our TS codebase:** business logic, protocol client, types, and streaming/formatting utilities port nearly verbatim; only the view layer is rewritten in React idioms. + +Alternatives, and why not: + +| Option | Verdict | Reason for a 2-dev, TS-owning team | +| --- | --- | --- | +| **Capacitor 8** | Runner-up / stopgap only | Ships the Solid UI as-is in weeks, but a permanently lower native ceiling on keyboard/scroll/voice — and success forces an RN migration anyway. | +| **Flutter 3.44** | Skip | Excellent runtime (Impeller), but **0 % reuse** (Dart), a second language + toolchain, no OTA without third-party Shorebird, and no equivalent to off-thread streaming markdown. | +| **KMP + Compose MP 1.11** | Skip | Best "share logic, native UI" story *for a Kotlin team*; for us it's Kotlin+Gradle+Xcode with zero Solid/TS reuse and the thinnest voice-SDK ecosystem. iOS text input only went native-backed in 1.11 — telling for a text-field app. | +| **Dual native (SwiftUI + Compose)** | Skip unless voice latency *is* the product | ~1.8× sustained effort, two review pipelines, no OTA, both devs fluent in Swift *and* Kotlin. | +| **Solid → native (Lynx / NativeScript / solid-native)** | Disqualified | No production-viable path in 2026: `solid-native` dead (last push Nov 2023), Lynx-for-Solid a stalled discussion with no core-team engagement, NativeScript+Solid has no documented production users. Our Solid investment transfers as *skills*, not code. | +| **Tauri v2 mobile** | Skip | Official notifications are **local-only (no server push)**; Rust-side native modules are the wrong skill axis. Desktop-first tech. | + +--- + +## 4. Code-reuse strategy — extract `@highflame/codeoid-core` + +Today the wire contract is **triplicated**: the canonical `src/protocol/types.ts` (daemon), a hand-maintained partial mirror in `web/src/protocol/types.ts` ("keep them in sync"), and a serde port in the Rust `codeoid-protocol` crate. A React Native app naïvely becomes a **fourth** hand-maintained copy. Fix that *before* building the app. + +**Extract two publishable TS packages** (Bun/pnpm workspace inside `codeoid`): + +- **`@highflame/codeoid-protocol`** — pure wire types + `PROTOCOL_VERSION` + scopes, lifted from `src/protocol/types.ts`. The daemon, `web/`, and the mobile app all *import* it. Kills the manual web mirror. (The Rust crate stays a separate hand-tracked port — same as today; it already proves the extraction is clean.) +- **`@highflame/codeoid-core`** — framework-agnostic client logic: + - `ws.ts` `CodeoidClient` — connect, first-frame auth handshake, request/response correlation, **exponential backoff + full jitter** reconnect, 20 s heartbeat. Uses only the WHATWG `WebSocket` global (RN ships one), so it drops in as-is; the `window`/`document` resume listeners are already `typeof window` guarded → swap for `AppState`/`NetInfo` on RN. + - The **reducer bodies** from `state/messages.ts` / `sessions.ts` (upsert-by-`messageId`, in-place delta patch, dedupe-on-replay). These are already written to *"mirror the Rust TUI's MessageStore semantics"* — extract the pure logic, leave `createStore`/`produce` behind. + - `lib/{format,identity,approvals,usage-days,sanitize-url}.ts` and `components/prompt/slash.ts` — all pure, already unit-tested. (The color helpers return Tailwind class strings; swap for RN style tokens.) + +**What the mobile app builds new (thin):** +- The React view layer (transcript rows, tool-call/diff/markdown rendering, approval bar, session list, composer) — FlashList v2 + react-native-streamdown. +- State bindings: wrap the extracted reducers in the RN state lib of choice (Zustand/Legend-State), keeping reducer semantics identical to web + TUI. +- Platform glue: `expo-secure-store` token storage (replaces `localStorage` in `lib/auth.ts`), `AppState`/`NetInfo` resume, push handlers, voice modules. + +**Repo shape:** keep the daemon in `codeoid` and extract `packages/protocol` + `packages/core` there (web consumes them locally). Put the app in a **new `codeoid-mobile` Expo repo** depending on the published packages — mirroring how the Rust TUI (`codeoid-ui`) is already a separate repo tracking the same spec. This keeps RN/Metro tooling out of the Bun daemon repo while ending type drift. + +**Attach/streaming model the app must honor (unchanged on the wire):** +- On `session.attach` the daemon sends a **full `scrollback.replay`** (complete `SessionMessage[]`, deltas pre-merged) — no pagination exists. Streaming afterward is `session.message.delta` patched in place. +- **Caveat / must-handle:** issue **#84** — a large replay frame can exceed the WS backpressure limit and the daemon auto-closes the client (it prunes suspended mobile webviews aggressively). On flaky mobile links this is a real lockout risk. The app should tolerate `4003`/backpressure closes and reconnect; longer-term, a `readSince(timestamp)` incremental-catch-up path exists in `scrollback.ts` but is **not wired to any protocol message** — wiring it is the right server-side fix for mobile and should be tracked alongside this work. + +--- + +## 5. Connection & auth + +Flow (matches the daemon's existing surface): +1. **Daemon URL** entry (e.g. `https://myserver.example.com` or `http://192.168.1.x:7400`). App calls `GET /config` to discover `zeroid_url`, `GET /health` for version/compat. +2. **Auth — two paths, both already server-supported:** + - **ZeroID API key** (`zid_sk_…`): POST `grant_type=api_key` to the daemon's same-origin `/oauth2/token` proxy → `access_token`. Works *today*; good for power users / headless setups. + - **Google OAuth → ZeroID** (RFC 8693 token-exchange, PKCE): `/auth/authorize` in an in-app browser, `codeoid://` deep-link callback. **Blocked on #42** (web button + auth-code-grant confirmation). This is the low-friction default the mobile ticket wants. +3. **Token storage:** access token in **Keychain/Keystore via `expo-secure-store`**. Re-mint on reconnect (JWTs are short-lived; the daemon closes `4003` on expiry every message, so long sockets can't outlive the token). +4. WS connects to the daemon origin root with `{type:"auth", token}` as the first frame (10 s auth deadline). Scopes returned in `auth.ok` are the capability set (`session:*`, `fs:read`). + +**Identity is a feature, not plumbing.** Every message carries `MessageIdentity {sub (WIMSE/SPIFFE URI), name, type}`; sessions carry `agentUri` + a `subagents[]` chain and a full delegation context (`delegationDepth`, `delegatedBy`, `accountId`, `projectId`). Surface this per-message and in the header — it's a genuine differentiator versus every competitor (see §9), and it's what makes the conductor's delegation tree legible on a phone. + +--- + +## 6. Voice-first design (local-first) + +The differentiator — and, per the competitor scan (§12), the clearest whitespace: **nobody verified shipping a private, on-device voice loop.** The reference the user cited (`iris`) turned out to be a useful *counter-example* on this point: iris is not local STT/TTS at all — it streams mic audio to Google's Gemini Live cloud and just uses Google's voice instead of ElevenLabs. Its **only** on-device piece is the "Hey Iris" wake word (an openWakeWord-style ONNX pipeline). So iris informs *what to reuse*, not the voice architecture itself. + +**Recommended v1 loop (lowest risk, best effort/UX ratio) — all on-device:** +`push-to-talk (or VAD auto-endpoint) → system STT → app logic → system TTS`, using: +- **STT:** `expo-speech-recognition` (jamsch) — a New-Architecture TurboModule wrapping iOS `SFSpeechRecognizer` and Android `SpeechRecognizer`, with **on-device mode + interim/continuous results**. iOS 26's `SpeechAnalyzer`/`SpeechTranscriber` is the quality ceiling (OS-managed model assets = ~zero app-size cost; ~2× faster than Whisper Large-v3-Turbo) but has **no streaming RN wrapper yet** — a small custom Expo Module when we want it. For heavier offline needs, `react-native-sherpa-onnx` (STT/TTS/VAD/KWS in one TurboModule) or `whisper.rn` (Core ML on iOS; **Android is CPU-only and slower** — lean on Android's system recognizer there). +- **TTS:** system `AVSpeechSynthesizer` / Android `TextToSpeech` via `expo-speech` for v1 (instant, ~0 MB). **Kokoro-82M** (~80 MB, faster-than-realtime, via `react-native-executorch`) is the "neural upgrade" — the one on-device TTS people are actually shipping. +- **VAD:** Silero VAD (~2 MB ONNX, <1 ms/30 ms chunk) for auto-endpointing so the user doesn't tap to stop; WebRTC VAD as a cheap first gate. +- **Wake word:** **not v1.** A third-party always-on wake word runs on the app processor (no access to the OS "Hey Siri" DSP path) — real background-mic battery drain for marginal benefit. PTT + VAD gives ~90% of the UX. When we do add "hey codeoid": **openWakeWord's pretrained models are CC-BY-NC (non-commercial) — we'd train our own** (the embedding/melspec models are Apache-2.0); Picovoice Porcupine's free tier caps at 3 active users/month (commercial beyond that is enterprise pricing). iris's `wakeword-models/` shows exactly how to train a custom phrase. + +**Conversational / hands-free (v1.5):** for a true away-from-keyboard mode, add full-duplex voice. Local-only conversational quality still trails cloud on noisy/accented/long-form audio, so this is the one place a **cloud fallback is defensible** — but keep on-device dictation as the always-available, private floor, and make cloud voice opt-in (LiveKit / ElevenLabs Agents / OpenAI Realtime all have Expo plugins). The app speaks a **summary** of each turn and reads out **approval requests**; the user responds by voice: *"approve," "deny," "show me the diff first."* + +**What ports from iris (framework-agnostic patterns worth copying):** +- **Arm the wake word only while idle** — cheap on-device detection gates the expensive path, so "nothing leaves the device while asleep." Right shape for mobile battery + privacy. +- **In-code confirmation state machine** (iris's `hermesGate`: *propose → model reads back → user must actually speak → only then act*, with a TTL). This maps directly onto **voice approvals of tool calls** — enforce "confirm before side-effecting" in code, not by trusting the model. High-value for the conductor's confirm-before-send. +- **Barge-in via flush-all-playback** — stop every queued audio buffer + reset the clock on interrupt. +- **Non-blocking dispatch over a stream** — start work, return immediately, stream progress, speak on completion. + +**Reference — OpenSuperWhisper (`starmel/OpenSuperWhisper`, MIT):** a 100%-local, hold-to-record dictation app that proves the private-voice UX works well. It's **macOS-only Swift**, so the *code doesn't port*, but two things do: (1) it validates **whisper.cpp + Whisper GGML `.bin` models** (and **NVIDIA Parakeet** as a fast alternative) as the local-STT family — and those models are the *public* weights from the whisper.cpp Hugging Face repo, not anything proprietary to the app, so "using their models" just means using Whisper/Parakeet, which is exactly what `whisper.rn` (mobile) and whisper-WASM / transformers.js (web) already consume; (2) its hold-to-record dictation-into-the-active-field interaction is the right v1 UX to copy. **Parakeet** (Parakeet-TDT) is worth benchmarking against Whisper-small/Moonshine — it's near-SOTA on English speed/accuracy and has ONNX/CoreML ports. + +**Local voice in the web UI too — not just mobile (per the direction to add it there).** This is the right call: it makes **local voice a cross-client codeoid capability** rather than a mobile-only feature, which is a *stronger* wedge against Happy's cloud-only voice (§12). The one trap to avoid: the browser **Web Speech API is not local** — Chrome ships the audio to Google's cloud. To keep the "your voice never leaves your machine" guarantee in the web UI, run Whisper **in-browser** via **`@xenova/transformers` (transformers.js, ONNX + WebGPU)** or a **whisper.cpp WASM** build (Moonshine ONNX is a lighter option). Note the org already uses `@xenova/transformers` server-side (memory embeddings), so it's familiar. Ship it as **push-to-talk dictation into the composer** first, mirroring OpenSuperWhisper. Architecturally, put the STT behind a small **`@highflame/codeoid-core` `SpeechProvider` interface** with platform adapters — transformers.js/WASM for web, `whisper.rn`/system recognizer for mobile — so web, mobile, and (later) the TUI share one local-voice contract and one "off by default, on-device only" guarantee. + +Voice maps onto existing protocol state with **no wire changes**: `waiting_approval` status + a `tool.state.phase === "waiting_confirmation"` carrying an `approvalId` → spoken prompt → `session.approve {approvalId, approved, updatedInput?}`. + +--- + +## 7. Push notifications — the relay problem + +The single most valuable thing a phone adds is: *"a session is blocked, waiting on your approval"* — delivered when the app is **not** attached. Today no push exists (`web/src/state/desktop-notifications.ts` only fires a local browser notification for the *focused* session while the tab is hidden; there are no `push.*` protocol types). + +**Constraint:** a self-hosted daemon on the user's box cannot push to APNs/FCM directly — Apple/Google delivery requires provider credentials the daemon shouldn't hold, and on iOS there is **no UnifiedPush**; you must go through APNs. + +**Hard iOS constraints that shape the whole design** (from the push research): +- **No background sockets.** iOS suspends the app shortly after backgrounding and gives no way to keep a TCP/WebSocket alive — so the persistent daemon socket is a *foreground-only* luxury; background delivery **must** be APNs. +- **No UnifiedPush on iOS** — APNs is mandatory. Silent/background push is best-effort and throttled, so never build "stay synced silently" on it; use **user-visible alert pushes** for anything the user must see. +- **4 KB payload cap** on APNs/FCM. + +**Design (the Home Assistant / ntfy pattern — content-blind by architecture):** +- A small **relay service** (developer-operated) holds the APNs + FCM credentials — they can't ship to a user's daemon. The daemon POSTs an event to the relay; the relay fans out to the device token. Store **token + a per-day counter only** (HA's minimal-retention posture). +- **Content-blindness — prefer the ntfy poll-back pattern over rolling our own crypto:** the push carries only a routing id (a wake-up), and the iOS **Notification Service Extension fetches the real (encrypted) body from the daemon/relay and decrypts it** (key shared with the app via an App Group / Keychain). The relay never sees content and the 4 KB cap stops mattering. (Pattern A — encrypt a small body directly into the push — is fine for short alerts within 4 KB.) +- **Protocol additions:** `push.register` / `push.unregister` (device token, platform, relay endpoint) — the app registers its token with the daemon so the daemon knows where to POST. +- **Actionable Approve/Deny without opening the app:** define the notification action **without** `.foreground`; iOS launches the app in the background to handle it. But the background window is seconds and the phone usually **isn't on the daemon's LAN**, so the action should **POST the decision to the relay** (which the daemon is already connected to), *not* attempt a direct socket to the user's machine. The daemon picks up the `session.approve` via its own outbound relay connection. +- **Fast path to ship:** since the app is Expo/RN, **Expo Push** abstracts APNs+FCM for v1 (free, receipt tracking) — at the cost of routing through Expo's servers. Graduate to a direct `.p8` APNs + FCM HTTP v1 relay (a few hundred lines) when we want the content-blind self-host story. +- **Android:** send **notification messages (not data-only)** for time-sensitive pushes (data-only is unreliable in Doze). Offer **UnifiedPush** as a second transport for de-Googled users (RFC 8291 E2E built in); iOS always uses our APNs relay. +- **Self-hosters** can run their own relay (OSS it); most users point at **our hosted relay** — the natural paid surface (§8, and a proven model: Nabu Casa bundles exactly this). + +--- + +## 8. Conductor-forward design (draft PR #51) + +The conductor is *"an identity-native fleet supervisor"* — a session created with `role: "conductor"` that drives the fleet by calling an in-process `codeoid_fleet` MCP server (list / spawn / send / watch / summarize / interrupt sessions, cross-thread recall). Crucially for us: **it introduces no new client↔daemon message kinds** — the conductor is just a session, and its fleet actions render as ordinary tool calls in the transcript. Addressing is by **identity/delegation** (owner → conductor → child sessions), one revocation root, `delegation_depth` capped. + +**What that means for the app — design for it now, ship it later:** +- **IA:** make **"the conductor" a pinnable home surface** — a persistent assistant chat you open to. The **session list becomes a secondary "fleet view."** The user talks to the conductor in natural language (*"continue the authz latest_only fix"*); it resolves the fuzzy reference to the right session across every workspace (the "session resolution" linchpin in PR #51). Voice-first + conductor = *"talk to your assistant, it runs the fleet"* — the world-class vision. +- **Approvals are the crown jewel here:** the conductor's **confirm-before-send** on any write into a user-owned session flows through the same `approvalId` mechanism — which we're already turning into native push + voice approvals. A phone that lets you supervise an autonomous fleet by approving/denying its cross-repo actions with your voice is the product no competitor has. +- **No blocking dependency:** because the conductor needs no wire changes, the app can build entirely on today's attach/scrollback/session-list model and light up the conductor surface the moment PR #51's later phases land. Watch the build plan (P0–P8) — *if* a later phase adds protocol messages we adopt them, but none exist today. + +--- + +## 9. Open source — the recommendation + +**Open-source the app. Permissive license (Apache-2.0, or MIT to match `codeoid`/`codeoid-ui`). Open-core.** This is the headline decision, and it's a clear yes. + +**Why:** +- **This is a control plane holding keys to the user's machine.** The app asks for a ZeroID credential and remote access to the user's coding sessions. For the exact developer/security audience Codeoid targets, a **closed-source client asking for that is a trust non-starter**; OSS = auditability, which this audience demands. +- **Consistency + ethos.** `codeoid` and `codeoid-ui` are already public MIT. The product's entire premise is *"you run the daemon."* An OSS client matches that and unlocks contribution, sideload/F-Droid, and community trust. +- **Every credible peer is OSS or source-available** in this exact shape (self-hosted backend + open client): Happy (the OSS Claude Code mobile leader — MIT, ~22k stars), Home Assistant companion apps, plus the broader precedent set — Bitwarden, Element X, Tailscale clients. The pattern *"code is open, the App Store binary is the convenient path"* (OsmAnd, Bitwarden, HA) is the norm and a proven trust multiplier. +- **The moat was never the pixels.** It's the daemon, the conductor + cross-session memory, ZeroID identity/delegation, and the hosted relay. Open-sourcing the client gives away nothing defensible. This is **almost exactly the Tailscale shape** — open clients + open relay code, proprietary coordination/control plane as the paid product. + +**License split (researched):** +- **Client → Apache-2.0.** Permissive kills the GPL-vs-App-Store conflict outright (the VLC saga: Apple pulled VLC in 2011 over GPL §6/§10 "no further restrictions" vs Apple's EULA device caps + ToS; even VLC had to relicense to LGPL/MPL to return). Apache-2.0's **explicit patent grant** (vs MIT's merely-implied one) is worth the small NOTICE-file overhead in the patent-heavy mobile world. It also lets **us** publish the App Store binary and add Apple's EULA on top without any conflict. +- **Server (relay + conductor cloud + identity/team) → AGPL-3.0 or proprietary, in separate repos.** AGPL closes the SaaS-rehosting loophole (a competitor hosting it must publish changes) — the Plausible/Elastic move. This is the standard "permissive client + copyleft server" combo. +- **Governance → DCO, not a CLA.** Because the commercial value lives in *separate* server repos (open-core, not dual-licensing the client), a `Signed-off-by` DCO documents provenance without the CLA/relicensing controversy (MongoDB's CLA-enabled AGPL→SSPL rug-pull is why contributors distrust CLAs). Add a CLA only if we ever want to fold community client code into a proprietary client build. +- **Distribution:** we publish the App Store binary; Android via **F-Droid** (reproducible build and/or our own F-Droid repo, like Bitwarden) + APK sideload. + +**Monetization (open-core), so OSS ≠ no revenue:** +- **Hosted push relay** — the convenience most users won't self-host (§7); the relay *must* hold APNs/FCM creds that can't ship to users, which is a clean, defensible reason to host it. Content-blind (poll-back), so we carry no content liability. +- **Conductor cloud** — hosted fleet supervision / cross-session memory / always-on assistant. +- **ZeroID SaaS + team/enterprise** — identity, delegation policy, audit, SSO. This is where Highflame-adjacent value accrues. +- Precedent that this sustains revenue: **Tailscale** (OSS clients, paid coordination), **Home Assistant / Nabu Casa** (OSS + cloud subscription that literally sells remote access + push), **Bitwarden** ($100M raise, ~100% YoY, OSS + hosted/enterprise). **Do not model on Signal** — donations + a founder loan is not a business at this scale. + +**What stays closed:** essentially nothing in the *client*. The mobile app, `@highflame/codeoid-protocol`, and `@highflame/codeoid-core` are all Apache-2.0; the revenue services are the separate AGPL/proprietary server repos. + +--- + +## 10. Phased build plan + +| Phase | Deliverable | Depends on | +| --- | --- | --- | +| **P0 — Extract core** | `@highflame/codeoid-protocol` + `@highflame/codeoid-core` published from `codeoid`; `web/` migrated off its manual type mirror onto the package (proves the extraction, ends drift). | — | +| **P0.5 — PWA (optional stopgap)** | `manifest.json` + service worker on `web/` for installability + validation. (Kept from #43.) | — | +| **P1 — RN skeleton + auth + attach** | `codeoid-mobile` Expo app: daemon-URL entry, `/config` discovery, **API-key auth** (works today), Keychain storage, WS connect, `session.list` + attach + full scrollback render, reconnect via `AppState`/`NetInfo`. | P0 | +| **P2 — Streaming transcript + approvals + push** | FlashList v2 + react-native-streamdown transcript, tool-call state machine + diff render, **approval bar**; `push.register`/`push.unregister` + **relay** (Expo Push → direct APNs/FCM) + actionable Approve/Deny (background action POSTs to relay). | P1; server: push endpoint | +| **P3 — OAuth login** | Google OAuth→ZeroID with `codeoid://` deep-link callback (the friction-free default). | #42 | +| **P4 — Voice** | Dictation (`expo-speech-recognition`), then conversational hands-free mode (LiveKit/ElevenLabs) with spoken summaries + **voice approvals**. | P2 | +| **P5 — Conductor surface** | Conductor-as-home IA, fleet view, cross-workspace session resolution surfaced; light up as PR #51 phases land. | PR #51 | + +--- + +## 11. Open questions / risks + +- **#84 backpressure lockout** — ✅ **fixed in PR #100** (chunked, drain-paced scrollback replay; also speeds first paint in web + TUI). The remaining, optional server improvement is wiring `scrollback.readSince()` to an incremental-catch-up message to cut reconnect bandwidth on flaky mobile links — schedule with P2. +- **OAuth (#42)** gates the friction-free login; P1 ships on the API-key path so mobile isn't blocked on it. +- **Actionable-notification background approval** on iOS: resolved direction (§7) — the action POSTs the decision to the relay (phone usually isn't on the daemon LAN), daemon picks it up over its outbound relay connection. Confirm the NSE decrypt + App-Group key sharing during P2. +- **Voice vendor lock-in / cost** for the *conversational* mode (LiveKit vs ElevenLabs vs OpenAI Realtime) — prototype in P4; keep **on-device dictation as the always-available, private floor** so the product works with zero cloud voice. +- **Android on-device STT** is the weak leg (whisper.cpp is CPU-only on Android) — prefer the system `SpeechRecognizer` / ML Kit GenAI there rather than fighting Whisper. + +--- + +## 12. Competitive positioning — the wedge + +The landscape scan (mid-2026) shows a category that is **consolidating around closed incumbents while the OSS field thins**: +- **Happy** (`slopus/happy`, MIT, Expo, ~22k stars) is the OSS mobile leader — E2E-encrypted blind relay (genuinely, for message content), multi-session, cross-device resume, "voice-to-action," push. **The one to beat.** But a code-level teardown (Appendix A) confirms three structural gaps: its **voice is cloud-only (ElevenLabs over LiveKit) and breaks its own E2E** (plaintext session text + tool-call arguments go to the cloud); it has **no per-message identity/provenance** (only an unsigned free-text `sentFrom`; the richer protocol is frozen and unused); and it is **relay-dependent** (self-hosting works but is discouraged and hardwired to the vendor's cloud). +- **Omnara** (YC) validated this exact positioning (command-center, voice, push-on-approval) then **archived its OSS repo (Feb 2026)** and pivoted to a hosted voice-first service — a signal about OSS monetization difficulty *and* an opening for a credible self-hosted alternative. +- **Pure-play OSS orchestrators died in 2026** (Terragon shut down; Vibe Kanban's Bloop shut down — "mostly free users, no business model"). The survivors are desktop-only (Conductor, Sculptor) or incumbent-backed and closed: **Anthropic Claude Code Remote Control** (Feb 2026, QR-pair, Max-first), **OpenAI Codex in ChatGPT** (May 2026, push approvals, QR-pair), **GitHub Copilot via GitHub Mobile** (async issue→PR), **Cursor iOS** (June 2026, cloud dictation). + +**Table stakes** (everyone has them): streaming transcript on mobile, push + one-tap approve/reject, QR/URL pairing to a local session, multi-session, diff/PR review, provider breadth. Basic voice *input* is drifting into table stakes; voice *quality* is not. + +**The wedge — a combination no current competitor holds:** self-hosted + OSS + **on-device voice** + **per-message identity/provenance** (ZeroID/WIMSE) + **mobile-native conductor** ("supervise a fleet from your pocket," which the desktop orchestrators don't do on mobile). Happy owns OSS+mobile+E2E but has neither identity/provenance nor local voice; the incumbents own distribution but are closed, cloud-tied, and provenance-blind. That intersection is the defensible, world-class position. + +*(Every load-bearing claim in §6/§7/§9/§12 is now backed by dated sources from the completed research pass; the earlier "truncated research" caveat no longer applies.)* + +--- + +## Appendix A — Competitive teardown: Happy (code-verified) + +Read from a local checkout of `slopus/happy` (a pnpm monorepo: `happy-app` [Expo SDK ~55 / RN 0.83 / Zustand], `happy-cli` [the `happy` binary, wraps Claude Agent SDK + Codex app-server over stdio + Gemini/ACP], `happy-server` [Fastify 5 + Prisma/Postgres, `happy-server-self-host`], `happy-agent`, `@slopus/happy-wire` [shared Zod schemas], plus an Electron IDE `codium`). MIT throughout. Claims below are verified in source, not marketing. + +- **Transport / ownership:** everything connects over **Socket.IO** at `/v1/updates`; the **CLI owns the live agent + authoritative state** and pushes encrypted updates the server *persists* (Prisma) for resume/scrollback. The app is a viewer/controller. The server never runs an agent. The CLI uses the **Claude Agent SDK** (default permission mode is literally `'yolo'` in `runClaude.ts`) and a hand-rolled **Codex `app-server` JSON-RPC-over-stdio** client (`codexAppServerClient.ts`) — no PTY in the CLI (PTY lives only in the Electron `codium`). +- **Encryption (real, careful):** libsodium HMAC-SHA512 key tree → per-session/machine/artifact data keys sealed via anonymous box; **AES-256-GCM** for content, XSalsa20-Poly1305 for legacy/blobs; **Ed25519 challenge/response** auth where the account identity *is* the public key; QR pairing carries the master secret (`handy://`). The relay is **genuinely blind to conversation content**. +- **…but NOT blind to everything:** the server sees routing metadata, the social graph, push tokens, and `UserKVStore.key` names (plaintext "for indexing"); and it **can decrypt your stored GitHub token + third-party API keys** (`GithubUser.token`, `ServiceAccountToken.token` — encrypted with the *server's* own `HANDY_MASTER_SECRET`, not E2E). +- **Voice = 100% cloud, breaks E2E (the big one):** ElevenLabs Conversational AI over LiveKit WebRTC. `contextFormatters.ts` ships agent text, user text, and **tool-call names + JSON arguments** to ElevenLabs; approvals return over the LiveKit data channel. It's also **server-gated paywalled** (20-min free tier, RevenueCat check). No on-device STT/TTS anywhere (tree-wide grep confirmed). +- **Push leaks + a privacy contradiction:** Expo Push; three triggers only (`done`/`permission`/`question`). The current path **POSTs plaintext title/body — including the working-directory folder name — to the server**, which forwards to Expo. `PRIVACY.md` claims pushes "never touch the backend"; the code contradicts it. (Presence-based suppression is a nice touch.) +- **No identity/provenance:** only an unsigned free-text `sentFrom`; the typed protocol carrying roles/turns/subagents is frozen ("not used in production, do not add consumers"). No signing, attestation, or delegation. +- **Single-user, no fleet, no cross-session memory:** one Ed25519 key = one account; a friend graph but no orgs/RBAC/tenancy; the daemon tracks N *independent* sessions with no supervisor; state is per-session with no shared long-term memory. +- **Telemetry contradiction:** README says "no tracking," app ships **PostHog** (opt-out). +- **codeoid's advantage, stated plainly:** we invert every structural gap — per-message ZeroID/WIMSE identity + delegation, **local-first voice** across web + mobile, self-hosted-first (connect to *your* daemon URL; relay only for push), the conductor/fleet supervisor, multi-tenancy, cross-session memory, and no credential-custody or telemetry gaps. + +## Appendix B — Borrowed-ideas backlog (from Happy) + +Happy did real engineering worth copying. Concrete, prioritized: + +| Idea | What to copy | Where it lands | +| --- | --- | --- | +| **Shared wire package** | Single Zod-schema package consumed by every client, **validated at every boundary**. Validates our `@highflame/codeoid-protocol`/`@highflame/codeoid-core` extraction (§4). | P0 — and add Zod runtime validation of inbound messages (the daemon currently does `parsed as ClientMessage` with no runtime check). | +| **Session fork / rewind** | Branch a session at a precise message id (their `claudeUuid`/`codexItemId`). | Daemon feature; great mobile affordance ("rewind to here and retry"). | +| **Push discipline** | Narrow to `done`/`permission`/`question` triggers + **presence-based suppression** (skip push if a client is foregrounded). | P2 relay (§7). | +| **Layered at-rest crypto** | Key-tree → per-record sealed keys → AES-GCM; separate keys for attachments. | Only where we persist/relay blobs (push relay, any hosted sync) — our trust model needs less since the daemon is the user's. | +| **Codex app-server client** | Their stdio JSON-RPC integration (SDK lacks approval support). | Reference for codeoid's multi-provider layer. | +| **Ops maturity** | Redis-streams Socket.IO scale-out, Prometheus/pino, offline reconnect, `caffeinate`/lid-state handling so long runs don't sleep. | Daemon hardening. | +| **Monetization mechanics** | Free-tier + hard-cap + server-side subscription check (RevenueCat) gating a cloud feature. | Template for gating the hosted relay / conductor cloud (§9). | +| **One Expo codebase → 4 platforms** | iOS/Android/web + macOS (Tauri). | Validates the RN pick; macOS-via-Tauri is a bonus target. | diff --git a/docs/provider-followups-handoff.md b/docs/provider-followups-handoff.md new file mode 100644 index 0000000..4f885fa --- /dev/null +++ b/docs/provider-followups-handoff.md @@ -0,0 +1,256 @@ +# Handoff — two remaining meta-harness follow-ups + +> Untracked design/handoff doc (like `docs/mobile-app-design.md`). Not committed. +> Written 2026-07-10 to let a fresh session pick up two scoped-but-large pieces +> without re-deriving context. Repos: `~/Workspace/codeoid` (daemon + web) and +> `~/Workspace/codeoid-ui` (Rust TUI). Commit identity: **saucam**. User owns +> merges — drive to green + review, never merge unprompted. + +--- + +## 0. Where we are (context) + +codeoid is a **meta-harness**: the daemon owns all session state; clients (web, +Rust TUI, Telegram, mobile) are pure renderers over a wire protocol. It drives +*native* agent harnesses at full fidelity rather than reimplementing models on +raw APIs — that's the differentiating bet (pi extensions, Claude Code skills/MCP, +each harness's own caching/session semantics all survive). + +**Recently shipped (all merged, `origin/main`):** + +- **#131** provider extension surface — `session.ui_request`/`ui_response`/ + `ui_resolved` dialogs, `session.commands`, `custom_message` + `PartsView`, + `session.part_action`, `tool_start.patchableKeys`, and the **ProviderRegistry** + (factories, replacing a hardcoded `switch`). Wire-additive; capabilities + `ui.dialogs` + `commands.dynamic`. +- **#132** **pi (pi.dev) as a second backend** — `PiProvider` over `pi --mode rpc` + (`src/daemon/providers/pi/{rpc,translate,bridge,index}.ts`). pi has **no native + permission system**, so codeoid injects a **bridge extension** (`bridge.ts`, + temp-file, `pi -e`) that routes every pi `tool_call` through `canUseTool`. + Missing bridge → turn fails **CLOSED**. pi session file is the backing id. +- **#133** **mid-session provider switching** — `session.set_provider` + + `/provider `; generic loop in `Session.switchProvider` + optional + **`seedFromHistory?(history)`** on the provider interface. Stateless backends + no-op it; warm backends (claude, pi) prepend `renderHistorySeed(...)` to their + first post-switch prompt. **This is the seam both pieces below build on.** +- **#134** web backend switcher (ProviderPicker) + provider chips. +- **#135** **pi subprocess env hardening** — `src/daemon/providers/env.ts`: + `buildSubprocessEnv` (shared) + `buildPiEnv`, with a DENY list for + `CODEOID_`/`ZEROID_`/`TELEGRAM_` that beats pattern matches (`CODEOID_API_KEY` + is the ZeroID root key and ends in `_API_KEY`). `buildAgentEnv` delegates. +- codeoid-ui **#14/#15** mirror the protocol + add `/provider`, `/new --provider`, + backend tags. **#16** swapped CodeRabbit → gemini-review-bot (also codeoid #136, + codeoid-mobile #2). Reviews now run via `.github/workflows/gemini-review.yml` + (jgunnink/gemini-review-bot pinned to the v1.3.0 SHA, `gemini-flash-latest` + default, `timeout-minutes: 10`). + +**Key architecture facts to hold:** + +- Provider interface: `src/daemon/providers/interface.ts` — `AgentProvider` / + `SessionProvider`, `ProviderEvent` union (`interface.ts:142`), `TurnOpts` + (carries `canUseTool`, `requestUserInput`, `history`), `seedFromHistory?` + (`interface.ts:267`). +- Canonical history: `src/daemon/providers/canonical.ts` — + `CanonicalHistoryAccumulator` (`:242`) turns the `ProviderEvent` stream into + `CanonicalTurn[]`. `CanonicalTurn` (`:36`) already carries `thinking` + + `toolCalls: CanonicalToolCall[]`. Converters `toAnthropicMessages` (`:199`), + `toGeminiContent` (`:127`), `toOpenAIMessages` (`:163`) — **all Phase-1 textual** + (tool calls flattened via `toolCallToText`). `renderHistorySeed` (`:353`). +- Session owns approvals/scrollback/transcript. Tool gating lives in + `Session.#makeCanUseToolFn` (`session.ts:2293`) → `#shouldAutoApprove` + (`:2203`), which consults mode/budget/`isSafeTool`. +- Claude provider consumes Claude Code **hooks** (`PreToolUse`, `SubagentStart`, + `SubagentStop`) INTERNALLY (`claude/index.ts:327`) for audit + Bash-compression + rewrite + subagent identity — but there is **no codeoid-level user-pluggable + hook bus**. That's piece 1. + +**Repo conventions:** +- Daemon/protocol/core: `bun run test` / `typecheck` / `lint` (biome). Web: + `cd web && bun run test / typecheck / lint`. codeoid-ui: `cargo test --workspace`, + `cargo fmt --all -- --check` (hard gate), `cargo clippy --all-targets` (advisory). +- **codecov patch gate is 80%** on both repos — write tests as you go. +- Protocol changes are **wire-additive** (no `PROTOCOL_VERSION` bump); capability + strings gate new behavior; the schema exhaustiveness test in + `packages/protocol/src/schemas.test.ts` forces a sample for every new + `ClientMessage`, and `protocol.test.ts` forces a `DaemonMessage` switch arm. + codeoid-ui has a wire rename-audit (`tests/wire_format.rs`) + roundtrip tests. +- Offline tests: pi uses a fake-pi fixture (`src/tests/fixtures/fake-pi.ts` + spawned via a wrapper script); mock provider is + `src/daemon/providers/mock/session-provider.ts` (extended in #133 with + `seedFromHistory` capture + `seedFromHistoryError`). +- Start each piece: `git checkout main && git pull && git checkout -b `. + +--- + +## 1. Daemon-native hook bus + +### Problem +pi's extension hooks (tool_call block/mutate, before_agent_start, context +mutation, session lifecycle) are the single best idea we found in the pi harness. +They work today **only for pi sessions** because they run inside the pi process. +Claude/gemini/openai sessions get no equivalent user-configurable hooks. A +**daemon-native hook bus** gives every backend the same extensibility, keyed by +provider-neutral `ProviderEvent`s the daemon already sees — so a user's +"block writes to .env", "git-checkpoint per turn", "inject context", "audit to +webhook" rules apply uniformly regardless of backend. + +This is distinct from pi's in-process extensions (those keep working for pi). This +is codeoid's OWN hook layer, sitting at the daemon between the provider event +stream and Session's handling. + +### Design sketch +A `HookBus` the daemon constructs once (like `ProviderRegistry` / +`CompressionRegistry`) and Session consults at well-defined points. Hooks are +**config-declared** (start with `providers`-style config: shell-command hooks à la +Claude Code's `settings.json` hooks, matched by event + tool-name matcher), so v1 +needs no plugin-loading machinery — just a typed dispatch over the events Session +already produces. + +Hook points to expose (map to existing Session seams — do NOT invent new event +plumbing where one exists): +- **`tool_call`** (can block / mutate input) — hook into `#makeCanUseToolFn` + (`session.ts:2293`) alongside the existing approval gate. A hook returning + `{block, reason}` short-circuits to a deny; a hook mutating `input` feeds the + same `updatedInput` path `patchableKeys` uses. This is the load-bearing one and + the natural parity with pi's `tool_call` + codeoid's own bridge. +- **`tool_result`** (observe / patch) — after `tool_complete` in + `#handleProviderEvent`. +- **`before_turn` / `after_turn`** — around `#sendInner` / `turn_done`. `before_turn` + can inject a `systemPromptAppend` addition or a `custom_message`. +- **session lifecycle** (`session_start`, `session_end`, `provider_switched`, + `rotated`) — cheap observability hooks; you already emit info messages at these + points. + +v1 hook *kinds* (keep small): `command` (spawn a shell command with the event +JSON on stdin, honor an allow/deny/mutate JSON response — mirror Claude Code's +hook contract so users can reuse mental models) and maybe `webhook` (POST). Do +NOT build an in-process JS plugin loader in v1 — that's a much bigger security +surface; note it as a future kind. + +Security: hooks run arbitrary user code by design, but they run in the DAEMON's +trust context, so a hook command must get the **hardened subprocess env** +(`buildSubprocessEnv` from #135, `providers/env.ts`) — never raw `process.env`. +This is the #1 review risk; wire it from the start. + +### Files +- New: `src/daemon/hooks/bus.ts` (the `HookBus`, event dispatch, command/webhook + kinds), `src/daemon/hooks/types.ts` (hook event shapes — reuse `ProviderEvent` + data where possible), config schema in `src/config.ts` (`hooks:` block, gate + optional like `dispatch`/`providers`). +- Touch: `src/daemon/session.ts` (dispatch at the seams above — thread the bus in + via `SessionCreateOptions`, retain it like `#providersRegistry` did in #133), + `src/daemon/session-manager.ts` (build the bus once, pass to sessions), + `src/daemon/server.ts` (construct at startup). +- Optional wire surface: a `hooks.config`-style read-only snapshot verb so + clients can DISPLAY configured hooks (parallel to `claude.config`), and info + messages when a hook blocks/mutates a tool so the user sees why. Additive. + +### Tests +- Unit: bus dispatch + command-kind exec with a fake hook script (fixture pattern + like fake-pi) — assert block short-circuits the tool, mutate reaches the + provider, env is hardened (no `CODEOID_*` leaks — reuse the #135 deny-list + test shape). +- Integration over the mock provider: a `tool_call` hook that blocks a tool; + assert the tool never executes and an info message explains it. A `before_turn` + hook that injects a `systemPromptAppend`. +- Config schema fidelity + env-override tests. + +### Open questions (decide, don't block) +- Ordering vs. the approval gate: hooks-then-approval or approval-then-hooks? Lean + **hooks first** (a hook block is a policy deny that shouldn't even prompt the + user), matching pi's `tool_call` semantics. +- Do hooks fire for auto-approved safe tools (Read/Grep/Glob)? Probably yes for + observe-hooks, and a block-hook on a safe tool must still win. Keep the gate + uniform. +- Conductor/worker sessions: inherit tenant hooks or run hook-free? Default to + inherit; note it. + +--- + +## 2. Phase-2 native-structured canonical history + +### Problem +`renderHistorySeed` (#133) and every `toMessages` converter are +**Phase-1 textual**: `CanonicalToolCall`s are flattened to prose +(`toolCallToText`), `thinking` isn't carried into provider payloads, and +provider-native structures (Anthropic `tool_use`/`tool_result` blocks) don't +survive. So a **switched** session gets a faithful *transcript*, not a native +continuation. Phase 2 raises fidelity: emit native structured messages so the +incoming backend sees real tool-call turns, not a narrated summary. + +The data is ALREADY captured natively — `CanonicalTurn` (`canonical.ts:36`) +carries `content`, `toolCalls: CanonicalToolCall[]` (id/name/input/output/success), +and `thinking`. The accumulator (`handleEvent`, `:266`) populates all of it. Only +the **rendering/conversion** side is lossy. That's the whole scope: better +converters, not new capture. + +### Design sketch +- **`toAnthropicMessages`** (`canonical.ts:199`) — the highest-value target since + claude is the default backend. Replace the flattened `[Tool calls executed…]` + text with real content blocks: assistant messages with `tool_use` blocks + (`{type:"tool_use", id, name, input}`) + following `user` messages with + `tool_result` blocks (`{type:"tool_result", tool_use_id, content}`). The + comment at `:213` literally says "Phase 2: replace with tool_use + tool_result + content blocks" — that's the marker. Carry `thinking` as a `thinking` block + where the target supports it. +- **`toGeminiContent` / `toOpenAIMessages`** — same idea in each provider's native + function-call shape (Gemini `functionCall`/`functionResponse` parts; OpenAI + `tool_calls` + `role:"tool"` messages). +- **`seedFromHistory`** for warm backends: today it prepends a rendered string + because neither the Claude SDK nor pi RPC accepts arbitrary native-history + injection cleanly. Investigate whether the Claude Agent SDK's session + resume/import can accept a synthesized native transcript (higher fidelity than + the string prepend). If not, keep the string seed but make it use the richer + structured text. **The switch loop itself (`Session.switchProvider`) does not + change** — only what `seedFromHistory` / the converters produce. +- The **stateless** providers (gemini, openai) benefit immediately and cleanly: + they call the converter every turn already, so upgrading the converter upgrades + their fidelity with zero switch-loop changes. + +### Files +- Core: `src/daemon/providers/canonical.ts` — rewrite the three `to*` converters + + `renderHistorySeed` (or add structured variants). Keep `CanonicalTurn` / + `CanonicalToolCall` shapes (they're sufficient); only extend if you find a + genuinely missing field (e.g. tool_use ids that must round-trip — they're + already on `CanonicalToolCall.id`). +- Consumers: `gemini/index.ts`, `openai/index.ts` (they call the converters), + `claude/index.ts` + `pi/index.ts` (`seedFromHistory`). +- The mock/fake-pi fixtures already emit `tool_start`/`tool_complete` with ids, so + the accumulator produces real `toolCalls` — good for tests. + +### Tests +- Converter units: feed a `CanonicalTurn[]` with tool calls + thinking, assert the + Anthropic output has real `tool_use`/`tool_result` blocks with matching + `tool_use_id`, Gemini has `functionCall`/`functionResponse`, OpenAI has + `tool_calls` + tool-role messages. (`canonical.ts` already has a `handleEvent` + test path via provider E2E — extend it.) +- Round-trip a claude→pi switch (mock or fake-pi) and assert the seed carries + structured tool history, not the `[Tool: …]` flattened form. +- Guard the `HISTORY_SEED_MAX_CHARS` truncation still applies to the richer form. + +### Open questions +- Fidelity vs. cost: native tool_use replay is more tokens than a summary. Consider + a config knob (summary vs. native) or size-based fallback (native under N turns, + summary above). Note it; default native for the switch case since that's the + point. +- Does the Claude SDK accept a synthesized native transcript on resume? If yes, + that's the real fidelity win (seed becomes a native import, not a prompt + prefix). If no, structured-text seed is the ceiling for warm backends — document + it honestly (the fidelity contract is already stated in `docs/providers-pi.md` + and the `session.set_provider` protocol comment; update both if the ceiling + changes). + +--- + +## Suggested order +Do the **hook bus first** — it's more self-contained (new subsystem, few edits to +existing hot paths) and doesn't touch the switch/seed path. Phase-2 history is a +focused rewrite of `canonical.ts` converters that benefits from a quiet main. +Neither depends on the other. Each is one PR. + +## Fast start for a fresh session +1. Read this file + `docs/multi-provider-meta-harness.md` + + `src/daemon/providers/interface.ts` + `canonical.ts`. +2. Recall the memory: `project-pi-provider-in-codeoid` in the auto-memory has the + compressed workstream history and the "remaining backlog" line. +3. `cd ~/Workspace/codeoid && git checkout main && git pull && git checkout -b feat/hook-bus`. diff --git a/package.json b/package.json index 9c946a2..ce6a41a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "codeoid", - "version": "0.3.4", - "description": "Identity-first control plane for AI coding agents \u2014 multi-session, multi-frontend, with cross-session memory", + "name": "@highflame/codeoid", + "version": "0.4.0", + "description": "Identity-first control plane for AI coding agents — multi-session, multi-frontend, with cross-session memory", "type": "module", "workspaces": [ "packages/*" @@ -21,6 +21,9 @@ "SECURITY.md", "docs/architecture.png" ], + "publishConfig": { + "access": "public" + }, "engines": { "bun": ">=1.0.0" }, @@ -55,14 +58,17 @@ "test:integration": "bun test src/integration", "test:web": "cd web && bun run test", "typecheck": "bun x tsc --noEmit && bun x tsc --noEmit -p packages/protocol && bun x tsc --noEmit -p packages/core", + "check:versions": "bun scripts/check-versions.ts", + "version:set": "bun scripts/set-version.ts", "smoke": "bash scripts/release-smoke.sh", "prepublishOnly": "bun run build:web" }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.220", - "@codeoid/protocol": "^0.2.0", "@google/generative-ai": "^0.24.1", "@grammyjs/auto-retry": "^2.0.2", + "@highflame/codeoid-core": "^0.4.0", + "@highflame/codeoid-protocol": "^0.4.0", "@highflame/sdk": "^0.3.18", "@xenova/transformers": "^2.17.2", "commander": "^13.0.0", @@ -85,4 +91,4 @@ "@earendil-works/pi-coding-agent": "0.80.6", "@google/gemini-cli": "0.50.0" } -} \ No newline at end of file +} diff --git a/packages/core/README.md b/packages/core/README.md index 56c3879..68b5534 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,4 +1,4 @@ -# @codeoid/core +# @highflame/codeoid-core Framework-agnostic client core for the [Codeoid](https://github.com/highflame-ai/codeoid) daemon — everything a frontend needs except the pixels: @@ -20,8 +20,8 @@ daemon — everything a frontend needs except the pixels: `identityLabel`, …), approval scanning, slash-command parsing. ```ts -import { CodeoidClient, MessageStore, ResumeCursors } from "@codeoid/core"; -import { CAPABILITIES } from "@codeoid/protocol"; +import { CodeoidClient, MessageStore, ResumeCursors } from "@highflame/codeoid-core"; +import { CAPABILITIES } from "@highflame/codeoid-protocol"; const store = new MessageStore(); const cursors = new ResumeCursors(); @@ -36,4 +36,4 @@ await client.connect(); ``` Ships TypeScript source (every consumer transpiles TS — Bun, Vite, Metro). -`@codeoid/protocol` is a peer dependency. +`@highflame/codeoid-protocol` is a peer dependency. diff --git a/packages/core/package.json b/packages/core/package.json index e2a69a0..ab161db 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { - "name": "@codeoid/core", - "version": "0.2.0", + "name": "@highflame/codeoid-core", + "version": "0.4.0", "description": "Framework-agnostic Codeoid client core — WebSocket transport (auth handshake, reconnect, heartbeat), message store semantics, resume cursors, and display helpers. Shared by the web UI and the mobile client.", "license": "MIT", "type": "module", @@ -23,6 +23,6 @@ "access": "public" }, "peerDependencies": { - "@codeoid/protocol": "^0.2.0" + "@highflame/codeoid-protocol": "^0.4.0" } } diff --git a/packages/core/src/approvals.test.ts b/packages/core/src/approvals.test.ts index fb4d33a..081b175 100644 --- a/packages/core/src/approvals.test.ts +++ b/packages/core/src/approvals.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "bun:test"; import { findPendingApproval } from "./approvals.js"; -import type { SessionMessage, SessionStatus, ToolState } from "@codeoid/protocol"; +import type { SessionMessage, SessionStatus, ToolState } from "@highflame/codeoid-protocol"; const identity = { sub: "spiffe://x/agent/a", name: "you", type: "human" as const }; diff --git a/packages/core/src/approvals.ts b/packages/core/src/approvals.ts index 683e0a3..2067605 100644 --- a/packages/core/src/approvals.ts +++ b/packages/core/src/approvals.ts @@ -25,7 +25,7 @@ * first). */ -import type { SessionMessage, SessionStatus } from "@codeoid/protocol"; +import type { SessionMessage, SessionStatus } from "@highflame/codeoid-protocol"; const APPROVAL_POSSIBLE: ReadonlySet = new Set([ "waiting_approval", diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 8bb7d92..e2005db 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -24,8 +24,8 @@ import type { DaemonMessage, ResponseErrorMsg, ResponseOkMsg, -} from "@codeoid/protocol"; -import { PROTOCOL_VERSION } from "@codeoid/protocol"; +} from "@highflame/codeoid-protocol"; +import { PROTOCOL_VERSION } from "@highflame/codeoid-protocol"; export type ClientStatus = | { kind: "idle" } diff --git a/packages/core/src/identity.test.ts b/packages/core/src/identity.test.ts index 78b29cd..283c0be 100644 --- a/packages/core/src/identity.test.ts +++ b/packages/core/src/identity.test.ts @@ -5,7 +5,7 @@ import { shortSub, truncateWimseUri, } from "./identity.js"; -import type { MessageIdentity, SessionInfo } from "@codeoid/protocol"; +import type { MessageIdentity, SessionInfo } from "@highflame/codeoid-protocol"; describe("shortSub", () => { it("extracts the trailing segment of a SPIFFE URI", () => { diff --git a/packages/core/src/identity.ts b/packages/core/src/identity.ts index 84bdbd7..53a4eb3 100644 --- a/packages/core/src/identity.ts +++ b/packages/core/src/identity.ts @@ -6,7 +6,7 @@ * proof of provenance. Colour mapping stays in each frontend. */ -import type { MessageIdentity, SessionInfo } from "@codeoid/protocol"; +import type { MessageIdentity, SessionInfo } from "@highflame/codeoid-protocol"; /** * Last path segment of a SPIFFE / WIMSE URI. diff --git a/packages/core/src/messages.test.ts b/packages/core/src/messages.test.ts index aaab428..c25dbe2 100644 --- a/packages/core/src/messages.test.ts +++ b/packages/core/src/messages.test.ts @@ -11,7 +11,7 @@ import type { ScrollbackReplayMsg, SessionMessage, SessionMessageDelta, -} from "@codeoid/protocol"; +} from "@highflame/codeoid-protocol"; import { MessageStore, dedupeReplay, mergeDeltaInto } from "./messages.js"; import { ResumeCursors } from "./resume.js"; diff --git a/packages/core/src/messages.ts b/packages/core/src/messages.ts index 506f97e..071f59d 100644 --- a/packages/core/src/messages.ts +++ b/packages/core/src/messages.ts @@ -28,7 +28,7 @@ import type { SessionMessage, SessionMessageDelta, ToolState, -} from "@codeoid/protocol"; +} from "@highflame/codeoid-protocol"; import type { ResumeCursors } from "./resume.js"; // ============================================================================= diff --git a/packages/core/src/resume.test.ts b/packages/core/src/resume.test.ts index e77b00d..52a5a92 100644 --- a/packages/core/src/resume.test.ts +++ b/packages/core/src/resume.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it } from "bun:test"; import { ResumeCursors } from "./resume.js"; -import type { ScrollbackReplayMsg } from "@codeoid/protocol"; +import type { ScrollbackReplayMsg } from "@highflame/codeoid-protocol"; function replayFrame(overrides: Partial = {}): ScrollbackReplayMsg { return { diff --git a/packages/core/src/resume.ts b/packages/core/src/resume.ts index 870e051..bb9e851 100644 --- a/packages/core/src/resume.ts +++ b/packages/core/src/resume.ts @@ -18,7 +18,7 @@ * `MessageStore` and pass it to `MessageStore.ingest()`. */ -import type { ScrollbackReplayMsg } from "@codeoid/protocol"; +import type { ScrollbackReplayMsg } from "@highflame/codeoid-protocol"; interface Cursor { key: string; diff --git a/packages/core/src/slash.ts b/packages/core/src/slash.ts index bce9365..0fcd67d 100644 --- a/packages/core/src/slash.ts +++ b/packages/core/src/slash.ts @@ -18,7 +18,7 @@ import type { ClientMessage, SessionInfo, SessionMode, -} from "@codeoid/protocol"; +} from "@highflame/codeoid-protocol"; export type SlashCommand = | { kind: "new"; name: string; workdir?: string } diff --git a/packages/protocol/README.md b/packages/protocol/README.md index 963dc7c..592c9b7 100644 --- a/packages/protocol/README.md +++ b/packages/protocol/README.md @@ -1,15 +1,15 @@ -# @codeoid/protocol +# @highflame/codeoid-protocol The Codeoid client↔daemon wire protocol: the message/event types (`ClientMessage`, `DaemonMessage`, `SessionMessage`, deltas, `SessionInfo`, …), the `PROTOCOL_VERSION` constant, and the permission `SCOPES`. Single source of truth shared by the daemon, the web UI, and the mobile client -— import these from `@codeoid/protocol` instead of copying them. +— import these from `@highflame/codeoid-protocol` instead of copying them. ```ts -import type { ClientMessage, DaemonMessage } from "@codeoid/protocol"; -import { PROTOCOL_VERSION, SCOPES } from "@codeoid/protocol"; +import type { ClientMessage, DaemonMessage } from "@highflame/codeoid-protocol"; +import { PROTOCOL_VERSION, SCOPES } from "@highflame/codeoid-protocol"; ``` Ships TypeScript source: every consumer (Bun, Vite, Metro) transpiles TS, so no diff --git a/packages/protocol/package.json b/packages/protocol/package.json index c452e69..c4846b7 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -1,6 +1,6 @@ { - "name": "@codeoid/protocol", - "version": "0.2.0", + "name": "@highflame/codeoid-protocol", + "version": "0.4.0", "description": "Codeoid client↔daemon wire protocol — message/event types + permission scopes. Shared by the daemon, web UI, and mobile client.", "license": "MIT", "type": "module", diff --git a/packages/protocol/src/schemas.ts b/packages/protocol/src/schemas.ts index f8c1362..1e7e7f4 100644 --- a/packages/protocol/src/schemas.ts +++ b/packages/protocol/src/schemas.ts @@ -3,8 +3,8 @@ * surface. The daemon validates every frame against these before acting on * it; clients may use them to pre-validate. * - * Import via the subpath export — `@codeoid/protocol/schemas` — which keeps - * the root package (`@codeoid/protocol`) dependency-free for type-only + * Import via the subpath export — `@highflame/codeoid-protocol/schemas` — which keeps + * the root package (`@highflame/codeoid-protocol`) dependency-free for type-only * consumers. `zod` is an optional peer dependency: only installs that import * this module need it. * diff --git a/scripts/check-versions.ts b/scripts/check-versions.ts new file mode 100644 index 0000000..608283a --- /dev/null +++ b/scripts/check-versions.ts @@ -0,0 +1,81 @@ +#!/usr/bin/env bun +/** + * check-versions — asserts the workspace's npm versions move in lockstep. + * + * Every publishable package in this repo ships at the SAME version, cut from one + * `vX.Y.Z` tag. That is the whole contract the release pipeline rests on: one + * tag → three publishes, no per-package "is this version new?" bookkeeping and + * no drift to reconcile by hand. + * + * What it enforces: + * • all publishable packages declare the same `version`; + * • the CLI's dependency ranges on the workspace packages are `^`, + * so a published tarball resolves the siblings it was actually built + * against; + * • `@highflame/codeoid-core`'s peer range on `@highflame/codeoid-protocol` + * tracks the same version. + * + * Optionally also checks the versions against an expected one — the release + * workflow passes the tag so a mistyped tag fails before anything is published. + * + * Usage: + * bun run check:versions # internal coherence only + * bun run check:versions 0.4.0 # ...and must equal 0.4.0 + * + * NOTE: the wire protocol version is a SEPARATE, independently-moving number + * (`PROTOCOL_VERSION` in packages/protocol/src/types.ts). npm versions being in + * lockstep says nothing about wire compatibility, and bumping one must never be + * taken as a reason to bump the other. + */ + +interface Manifest { + name: string; + version: string; + dependencies?: Record; + peerDependencies?: Record; +} + +const ROOT = new URL("..", import.meta.url).pathname; + +const read = async (rel: string): Promise => + (await Bun.file(`${ROOT}${rel}/package.json`).json()) as Manifest; + +const root = await read("."); +const protocol = await read("packages/protocol"); +const core = await read("packages/core"); + +const errors: string[] = []; +const expected = process.argv[2]?.replace(/^v/, ""); +const version = root.version; + +for (const pkg of [protocol, core]) { + if (pkg.version !== version) { + errors.push(`${pkg.name}@${pkg.version} is not in lockstep with ${root.name}@${version}`); + } +} + +if (expected && version !== expected) { + errors.push(`${root.name}@${version} does not match the expected version ${expected}`); +} + +const wantRange = `^${version}`; +for (const dep of [protocol.name, core.name]) { + const got = root.dependencies?.[dep]; + if (got !== wantRange) { + errors.push(`${root.name} depends on ${dep}@${got ?? ""} — expected ${wantRange}`); + } +} + +const peer = core.peerDependencies?.[protocol.name]; +if (peer !== wantRange) { + errors.push(`${core.name} peer-depends on ${protocol.name}@${peer ?? ""} — expected ${wantRange}`); +} + +if (errors.length > 0) { + console.error("version lockstep violated:"); + for (const e of errors) console.error(` • ${e}`); + console.error("\nFix with: bun run version:set "); + process.exit(1); +} + +console.log(`versions coherent @ ${version} (${[root.name, protocol.name, core.name].join(", ")})`); diff --git a/scripts/release-smoke.sh b/scripts/release-smoke.sh index e90c820..a808a29 100755 --- a/scripts/release-smoke.sh +++ b/scripts/release-smoke.sh @@ -5,9 +5,10 @@ # Runs everything CI runs (lint / typecheck / test / build for the daemon and # the web app) PLUS the layers CI can't: # -# • version coherence — the built `codeoid --version` matches package.json -# (guards against the hand-synced version string drifting from the -# published package); +# • version coherence — the built `codeoid --version` matches package.json, +# and every publishable workspace package is in lockstep with it (guards +# against the hand-synced version string drifting from the published +# package, and against a partial `version:set`); # • daemon boot probe — the actual built bundle starts and binds its port # (catches runtime-boot breakage a `bun build` alone won't: bad dynamic # imports, top-level-await regressions, ESM path mistakes); @@ -105,6 +106,7 @@ echo # ── Daemon gates (mirror CI) ────────────────────────────────────────────────── run_stage "daemon: lint" bun run lint +run_stage "workspace: versions" bun run check:versions run_stage "daemon: typecheck" bun run typecheck run_stage "daemon: test" bun run test run_stage "daemon: build" bun run build diff --git a/scripts/set-version.ts b/scripts/set-version.ts new file mode 100644 index 0000000..9160f55 --- /dev/null +++ b/scripts/set-version.ts @@ -0,0 +1,62 @@ +#!/usr/bin/env bun +/** + * set-version — bumps every publishable package in the workspace to one version. + * + * The counterpart to `check-versions`: this is the only sanctioned way to bump, + * so the lockstep invariant is produced mechanically rather than remembered. + * It rewrites, in one pass: + * + * • `version` in the CLI, protocol, and core manifests; + * • the CLI's `^` dependency ranges on the two workspace packages; + * • core's `^` peer range on protocol. + * + * Usage: + * bun run version:set 0.5.0 + * + * Then: refresh the lockfiles (`bun install && cd web && bun install`), move the + * CHANGELOG `## [Unreleased]` notes under `## [0.5.0]`, and open the PR. The tag + * comes after the merge — see RELEASING.md. + */ + +const version = process.argv[2]?.replace(/^v/, ""); + +if (!version || !/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(version)) { + console.error("usage: bun run version:set "); + process.exit(1); +} + +const ROOT = new URL("..", import.meta.url).pathname; +const PROTOCOL = "@highflame/codeoid-protocol"; +const CORE = "@highflame/codeoid-core"; + +/** + * Rewrites the manifest as TEXT, not as a parsed-and-redumped object: a JSON + * round-trip would reformat the whole file (key order survives, but array and + * unicode formatting do not) and bury a two-line version bump in noise. + */ +async function patch(rel: string, edits: Array<[RegExp, string]>): Promise { + const path = `${ROOT}${rel}/package.json`; + let text = await Bun.file(path).text(); + for (const [pattern, replacement] of edits) { + if (!pattern.test(text)) { + console.error(`${rel}/package.json: no match for ${pattern} — manifest shape changed?`); + process.exit(1); + } + text = text.replace(pattern, replacement); + } + await Bun.write(path, text); + console.log(` ${rel}/package.json → ${version}`); +} + +const versionField = (): [RegExp, string] => [/("version":\s*")[^"]+(")/, `$1${version}$2`]; +const range = (dep: string): [RegExp, string] => [ + new RegExp(`("${dep.replace("/", "\\/")}":\\s*")\\^[^"]+(")`), + `$1^${version}$2`, +]; + +console.log(`setting workspace version to ${version}`); +await patch(".", [versionField(), range(PROTOCOL), range(CORE)]); +await patch("packages/protocol", [versionField()]); +await patch("packages/core", [versionField(), range(PROTOCOL)]); + +console.log("\nnext: bun install && (cd web && bun install), then update CHANGELOG.md"); diff --git a/src/daemon/local-auth.ts b/src/daemon/local-auth.ts index 81894d4..a10832b 100644 --- a/src/daemon/local-auth.ts +++ b/src/daemon/local-auth.ts @@ -45,7 +45,7 @@ import { randomBytes, timingSafeEqual } from "node:crypto"; import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { dirname } from "node:path"; -import { ALL_SCOPES, type AuthContext } from "@codeoid/protocol"; +import { ALL_SCOPES, type AuthContext } from "@highflame/codeoid-protocol"; import type { AuthMode, TokenVerifier } from "./verifier.js"; /** diff --git a/src/daemon/pipeline/pack-service.ts b/src/daemon/pipeline/pack-service.ts index dcd6e1e..e1e7d6c 100644 --- a/src/daemon/pipeline/pack-service.ts +++ b/src/daemon/pipeline/pack-service.ts @@ -17,7 +17,7 @@ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, statSync, symlinkSync } from "node:fs"; import { homedir } from "node:os"; import { join, resolve } from "node:path"; -import type { AvailablePackWire, PackWire, RegistryWire } from "@codeoid/protocol"; +import type { AvailablePackWire, PackWire, RegistryWire } from "@highflame/codeoid-protocol"; import type { Pack } from "./interface"; import { loadPack, type LoadedPack, type RoleDef } from "./pack"; import { loadSubagents, type PackSubagent } from "./subagents"; diff --git a/src/daemon/server.ts b/src/daemon/server.ts index 463e2b2..16485da 100644 --- a/src/daemon/server.ts +++ b/src/daemon/server.ts @@ -43,7 +43,7 @@ import { import { createHookBus } from "./hooks/bus.js"; import type { CodeoidConfig } from "../config.js"; import { CAPABILITIES, PROTOCOL_VERSION, type AuthContext, type DaemonMessage } from "../protocol/types.js"; -import { parseAuthMsg, parseClientMessage } from "@codeoid/protocol/schemas"; +import { parseAuthMsg, parseClientMessage } from "@highflame/codeoid-protocol/schemas"; import type { AttachedClient } from "./session.js"; import type { Frontend, FrontendContext } from "../frontends/types.js"; import type { Server } from "node:http"; diff --git a/src/daemon/verifier.ts b/src/daemon/verifier.ts index 2a9464d..7292737 100644 --- a/src/daemon/verifier.ts +++ b/src/daemon/verifier.ts @@ -26,7 +26,7 @@ * walking the import graph. */ -import type { AuthContext } from "@codeoid/protocol"; +import type { AuthContext } from "@highflame/codeoid-protocol"; /** * Which issuer authenticated this daemon's connections. diff --git a/src/frontends/telegram/index.ts b/src/frontends/telegram/index.ts index b0e51d6..532b1cb 100644 --- a/src/frontends/telegram/index.ts +++ b/src/frontends/telegram/index.ts @@ -19,7 +19,7 @@ import { Bot, type Context, InlineKeyboard } from "grammy"; import { autoRetry } from "@grammyjs/auto-retry"; import { randomUUID } from "node:crypto"; -import { formatCollaborationCost } from "@codeoid/core"; +import { formatCollaborationCost } from "@highflame/codeoid-core"; import { getManifest, getSnapshot } from "../../daemon/settings/store.js"; import { ALL_SCOPES_STRING } from "../../protocol/scopes.js"; import type { Frontend, FrontendContext } from "../types.js"; diff --git a/src/protocol/index.ts b/src/protocol/index.ts index abf7034..1954556 100644 --- a/src/protocol/index.ts +++ b/src/protocol/index.ts @@ -1,5 +1,5 @@ -// Re-export shim. The canonical protocol now lives in the @codeoid/protocol +// Re-export shim. The canonical protocol now lives in the @highflame/codeoid-protocol // package (packages/protocol) so the daemon, web UI, and mobile client share // one source of truth. Kept so existing "../protocol/…" imports keep resolving; -// new code should import from "@codeoid/protocol" directly. -export * from "@codeoid/protocol"; +// new code should import from "@highflame/codeoid-protocol" directly. +export * from "@highflame/codeoid-protocol"; diff --git a/src/protocol/scopes.ts b/src/protocol/scopes.ts index feb7800..c7fe4ed 100644 --- a/src/protocol/scopes.ts +++ b/src/protocol/scopes.ts @@ -1,3 +1,3 @@ -// Re-export shim — the permission scopes now live in @codeoid/protocol +// Re-export shim — the permission scopes now live in @highflame/codeoid-protocol // (packages/protocol/src/scopes.ts). See ./index.ts for the rationale. -export * from "@codeoid/protocol"; +export * from "@highflame/codeoid-protocol"; diff --git a/src/protocol/types.ts b/src/protocol/types.ts index d734240..67111df 100644 --- a/src/protocol/types.ts +++ b/src/protocol/types.ts @@ -1,3 +1,3 @@ -// Re-export shim — the wire types now live in @codeoid/protocol +// Re-export shim — the wire types now live in @highflame/codeoid-protocol // (packages/protocol/src/types.ts). See ./index.ts for the rationale. -export * from "@codeoid/protocol"; +export * from "@highflame/codeoid-protocol"; diff --git a/src/tests/collaboration.test.ts b/src/tests/collaboration.test.ts index e055e83..d1fe101 100644 --- a/src/tests/collaboration.test.ts +++ b/src/tests/collaboration.test.ts @@ -38,7 +38,7 @@ import { ProviderRegistry } from "../daemon/providers/registry.js"; import type { ProviderEvent } from "../daemon/providers/interface.js"; import { Blackboard } from "../daemon/blackboard/service.js"; import { BlackboardStore } from "../daemon/blackboard/store.js"; -import { parseClientMessage } from "@codeoid/protocol/schemas"; +import { parseClientMessage } from "@highflame/codeoid-protocol/schemas"; import { SessionManager } from "../daemon/session-manager.js"; import { Store } from "../daemon/store.js"; import { TranscriptStore } from "../daemon/transcript.js"; diff --git a/src/tests/local-auth.test.ts b/src/tests/local-auth.test.ts index bb81710..adad73a 100644 --- a/src/tests/local-auth.test.ts +++ b/src/tests/local-auth.test.ts @@ -21,7 +21,7 @@ import { afterEach, describe, expect, it } from "bun:test"; import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; -import { ALL_SCOPES } from "@codeoid/protocol"; +import { ALL_SCOPES } from "@highflame/codeoid-protocol"; import { assertLocalBindAllowed, isLocalToken, @@ -298,13 +298,20 @@ function importGraph(entry: string): { files: Map; packages: Set describe("local mode is offline-capable by construction", () => { const LOCAL_AUTH = resolve(import.meta.dir, "../daemon/local-auth.ts"); + // codeoid's own packages live in the @highflame scope too, so the scope prefix + // alone no longer distinguishes "us" from "the ZeroID SDK". Allowlist ours by + // name and treat every other @highflame specifier as a violation — that keeps + // the check strict enough to catch a convenience import of any Highflame + // platform package, not just the SDK we know about today. + const OWN_PACKAGES = new Set(["@highflame/codeoid-protocol", "@highflame/codeoid-core"]); + it("never reaches @highflame/sdk, directly or transitively", () => { const { files, packages } = importGraph(LOCAL_AUTH); - // No package specifier anywhere in the graph is the ZeroID SDK. This is the - // assertion that fails if someone adds a convenience import to auth.ts (or - // anything that pulls it in) from local-auth.ts. - const sdkImports = [...packages].filter((p) => p.startsWith("@highflame/")); + // No package specifier anywhere in the graph is a Highflame platform package. + // This is the assertion that fails if someone adds a convenience import to + // auth.ts (or anything that pulls it in) from local-auth.ts. + const sdkImports = [...packages].filter((p) => p.startsWith("@highflame/") && !OWN_PACKAGES.has(p)); expect(sdkImports).toEqual([]); // Belt and suspenders: no visited SOURCE mentions it either, which also @@ -320,8 +327,8 @@ describe("local mode is offline-capable by construction", () => { expect(packages.has("node:crypto")).toBe(true); }); - it("keeps @codeoid/protocol — its one package dependency — SDK-free", async () => { - // The graph walker stops at package boundaries, and @codeoid/protocol is + it("keeps @highflame/codeoid-protocol — its one package dependency — SDK-free", async () => { + // The graph walker stops at package boundaries, and @highflame/codeoid-protocol is // the only non-node package local-auth depends on. Scan it directly so the // invariant covers the whole reachable set. const glob = new Bun.Glob("**/*.ts"); diff --git a/src/tests/local-mode-server.test.ts b/src/tests/local-mode-server.test.ts index 00de826..c1acc95 100644 --- a/src/tests/local-mode-server.test.ts +++ b/src/tests/local-mode-server.test.ts @@ -30,7 +30,7 @@ import { mintLocalToken, readLocalTokenFile, } from "../daemon/local-auth.js"; -import { ALL_SCOPES } from "@codeoid/protocol"; +import { ALL_SCOPES } from "@highflame/codeoid-protocol"; import type { AuthOkMsg, DaemonMessage } from "../protocol/types.js"; const TOKEN = mintLocalToken(); diff --git a/src/tui/ansi/render-message.ts b/src/tui/ansi/render-message.ts index 5bc5b34..6bec1cf 100644 --- a/src/tui/ansi/render-message.ts +++ b/src/tui/ansi/render-message.ts @@ -28,7 +28,7 @@ import type { ToolInfo, ToolState, } from "../../protocol/types.js"; -import { formatCollaborationCost } from "@codeoid/core"; +import { formatCollaborationCost } from "@highflame/codeoid-core"; import { renderMarkdown, type Segment } from "../markdown.js"; import { computeDiff, truncateToolOutput } from "../diff.js"; import { fileUri, maybeLink } from "../osc8.js"; diff --git a/web/bun.lock b/web/bun.lock index 007f78d..fb0296d 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -5,9 +5,9 @@ "": { "name": "web", "dependencies": { - "@codeoid/core": "file:../packages/core", - "@codeoid/protocol": "file:../packages/protocol", "@floating-ui/dom": "^1.7.6", + "@highflame/codeoid-core": "file:../packages/core", + "@highflame/codeoid-protocol": "file:../packages/protocol", "@tanstack/solid-virtual": "^3.13.24", "@types/diff": "^8.0.0", "diff": "^9.0.0", @@ -33,6 +33,9 @@ }, }, }, + "overrides": { + "@highflame/codeoid-protocol": "file:../packages/protocol", + }, "packages": { "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="], @@ -82,10 +85,6 @@ "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], - "@codeoid/core": ["@codeoid/core@file:../packages/core", { "peerDependencies": { "@codeoid/protocol": "^0.1.0" } }], - - "@codeoid/protocol": ["@codeoid/protocol@file:../packages/protocol", { "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }], - "@csstools/color-helpers": ["@csstools/color-helpers@6.1.0", "", {}, "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg=="], "@csstools/css-calc": ["@csstools/css-calc@3.2.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg=="], @@ -176,6 +175,10 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + "@highflame/codeoid-core": ["@highflame/codeoid-core@file:../packages/core", { "peerDependencies": { "@highflame/codeoid-protocol": "^0.4.0" } }], + + "@highflame/codeoid-protocol": ["@highflame/codeoid-protocol@file:../packages/protocol", { "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }], + "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], @@ -922,10 +925,10 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@codeoid/core/@codeoid/protocol": ["@codeoid/protocol@file:../packages/protocol", { "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }], - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@highflame/codeoid-core/@highflame/codeoid-protocol": ["@highflame/codeoid-protocol@file:../packages/protocol", {}], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], diff --git a/web/package.json b/web/package.json index 5671eae..05211f6 100644 --- a/web/package.json +++ b/web/package.json @@ -11,9 +11,13 @@ "lint": "eslint src", "test": "NODE_OPTIONS=\"--max-old-space-size=4096\" vitest run --pool=forks --poolOptions.forks.singleFork=true" }, + "//overrides": "web/ is its own install root (separate lockfile, vitest+vite toolchain), so the workspace packages come in as file: copies. @highflame/codeoid-core peer-depends on @highflame/codeoid-protocol by RANGE, which bun tries to satisfy from the registry — pin it to the in-repo copy so `bun install` here never depends on what has been published. Keeps the web build reproducible against the working tree, which is the whole point of the file: deps.", + "overrides": { + "@highflame/codeoid-protocol": "file:../packages/protocol" + }, "dependencies": { - "@codeoid/core": "file:../packages/core", - "@codeoid/protocol": "file:../packages/protocol", + "@highflame/codeoid-core": "file:../packages/core", + "@highflame/codeoid-protocol": "file:../packages/protocol", "@floating-ui/dom": "^1.7.6", "@tanstack/solid-virtual": "^3.13.24", "@types/diff": "^8.0.0", diff --git a/web/src/components/prompt/slash.ts b/web/src/components/prompt/slash.ts index d6d6a4e..3b248a6 100644 --- a/web/src/components/prompt/slash.ts +++ b/web/src/components/prompt/slash.ts @@ -1,7 +1,7 @@ /** * Re-export shim — the slash-command parser/dispatcher lives in - * `@codeoid/core` (SlashContext is dependency-injected, so the logic is + * `@highflame/codeoid-core` (SlashContext is dependency-injected, so the logic is * frontend-agnostic). */ -export { dispatchSlash, parseSlash } from "@codeoid/core"; -export type { SlashCommand, SlashContext } from "@codeoid/core"; +export { dispatchSlash, parseSlash } from "@highflame/codeoid-core"; +export type { SlashCommand, SlashContext } from "@highflame/codeoid-core"; diff --git a/web/src/lib/approvals.ts b/web/src/lib/approvals.ts index 265aa7b..195b4bd 100644 --- a/web/src/lib/approvals.ts +++ b/web/src/lib/approvals.ts @@ -1,5 +1,5 @@ /** - * Re-export shim — approval scanning lives in `@codeoid/core` so every + * Re-export shim — approval scanning lives in `@highflame/codeoid-core` so every * frontend surfaces the same pending-approval semantics. */ -export { findPendingApproval } from "@codeoid/core"; +export { findPendingApproval } from "@highflame/codeoid-core"; diff --git a/web/src/lib/format.ts b/web/src/lib/format.ts index 48db2ea..113787e 100644 --- a/web/src/lib/format.ts +++ b/web/src/lib/format.ts @@ -1,8 +1,8 @@ /** - * Display formatters — the data formatting lives in `@codeoid/core` (shared + * Display formatters — the data formatting lives in `@highflame/codeoid-core` (shared * with the TUI and mobile); only the Tailwind colour mapping is web-local. */ -import { ctxWindowSeverity } from "@codeoid/core"; +import { ctxWindowSeverity } from "@highflame/codeoid-core"; export { ctxWindowSeverity, @@ -14,8 +14,8 @@ export { formatPercent, formatTokens, relativeTime, -} from "@codeoid/core"; -export type { CtxSeverity } from "@codeoid/core"; +} from "@highflame/codeoid-core"; +export type { CtxSeverity } from "@highflame/codeoid-core"; /** * Context-window utilization colour cue — maps the shared severity to this diff --git a/web/src/lib/identity.ts b/web/src/lib/identity.ts index 28af120..d17e6c1 100644 --- a/web/src/lib/identity.ts +++ b/web/src/lib/identity.ts @@ -1,5 +1,5 @@ /** - * Identity helpers — provenance labelling lives in `@codeoid/core` (shared + * Identity helpers — provenance labelling lives in `@highflame/codeoid-core` (shared * with the TUI and mobile); only the Tailwind colour mapping is web-local. */ import type { IdentityType } from "../protocol/types"; @@ -9,7 +9,7 @@ export { sessionAgentLabel, shortSub, truncateWimseUri, -} from "@codeoid/core"; +} from "@highflame/codeoid-core"; /** Tailwind classes for the role pill — match the TUI palette intent. */ export function roleColorClass(role: string): string { diff --git a/web/src/lib/sanitize-url.ts b/web/src/lib/sanitize-url.ts index be9f5c0..352c2f1 100644 --- a/web/src/lib/sanitize-url.ts +++ b/web/src/lib/sanitize-url.ts @@ -1,4 +1,4 @@ /** - * Re-export shim — URI sanitizers live in `@codeoid/core`. + * Re-export shim — URI sanitizers live in `@highflame/codeoid-core`. */ -export { safeImageUri, safeLinkUri } from "@codeoid/core"; +export { safeImageUri, safeLinkUri } from "@highflame/codeoid-core"; diff --git a/web/src/lib/usage-days.ts b/web/src/lib/usage-days.ts index 3879542..61ced1a 100644 --- a/web/src/lib/usage-days.ts +++ b/web/src/lib/usage-days.ts @@ -1,5 +1,5 @@ /** - * Re-export shim — UTC day-bucket helpers live in `@codeoid/core`. + * Re-export shim — UTC day-bucket helpers live in `@highflame/codeoid-core`. */ -export { padDays, utcDayKey } from "@codeoid/core"; -export type { DayCostBucket } from "@codeoid/core"; +export { padDays, utcDayKey } from "@highflame/codeoid-core"; +export type { DayCostBucket } from "@highflame/codeoid-core"; diff --git a/web/src/lib/ws.ts b/web/src/lib/ws.ts index 0908312..f610c97 100644 --- a/web/src/lib/ws.ts +++ b/web/src/lib/ws.ts @@ -1,8 +1,8 @@ /** - * Re-export shim — the transport now lives in `@codeoid/core` (the same + * Re-export shim — the transport now lives in `@highflame/codeoid-core` (the same * `CodeoidClient` the mobile app uses; web passes its capabilities at the * construction site in state/connection.ts). Kept so existing "../lib/ws" - * imports resolve unchanged; new code may import from `@codeoid/core`. + * imports resolve unchanged; new code may import from `@highflame/codeoid-core`. */ -export { CodeoidClient } from "@codeoid/core"; -export type { ClientStatus, ConnectOptions } from "@codeoid/core"; +export { CodeoidClient } from "@highflame/codeoid-core"; +export type { ClientStatus, ConnectOptions } from "@highflame/codeoid-core"; diff --git a/web/src/protocol/types.ts b/web/src/protocol/types.ts index 9dbdadc..37fc9d6 100644 --- a/web/src/protocol/types.ts +++ b/web/src/protocol/types.ts @@ -1,13 +1,13 @@ /** * Re-export shim. The wire protocol now comes from the shared - * `@codeoid/protocol` package (../../packages/protocol) — the same source of + * `@highflame/codeoid-protocol` package (../../packages/protocol) — the same source of * truth the daemon uses — instead of the hand-maintained mirror that lived * here ("keep them in sync" is finally nobody's job). Kept so the existing * `../protocol/types` imports across web/src resolve unchanged; new code may - * import from `@codeoid/protocol` directly. + * import from `@highflame/codeoid-protocol` directly. * * NOTE (bun `file:` semantics): the dependency is COPIED into node_modules at * install time, not symlinked. After editing `packages/protocol`, run * `bun install` in `web/` to refresh the copy. CI installs fresh every run. */ -export * from "@codeoid/protocol"; +export * from "@highflame/codeoid-protocol"; diff --git a/web/src/state/messages.ts b/web/src/state/messages.ts index 8102ad3..9ca02d1 100644 --- a/web/src/state/messages.ts +++ b/web/src/state/messages.ts @@ -16,7 +16,7 @@ import { batch, createMemo, createRoot } from "solid-js"; import { createStore, produce } from "solid-js/store"; -import { dedupeReplay, mergeDeltaInto } from "@codeoid/core"; +import { dedupeReplay, mergeDeltaInto } from "@highflame/codeoid-core"; import type { SessionMessage, SessionMessageDelta } from "../protocol/types"; @@ -181,7 +181,7 @@ export function applyDelta(delta: SessionMessageDelta): void { const target = buf[idx]; if (!target) return; - // The merge semantics live in @codeoid/core so every frontend + // The merge semantics live in @highflame/codeoid-core so every frontend // accumulates transcripts identically; inside produce() the store // proxy records the same fine-grained paths the inline code did. mergeDeltaInto(target, delta); diff --git a/web/src/state/resume.ts b/web/src/state/resume.ts index 287c176..25697ce 100644 --- a/web/src/state/resume.ts +++ b/web/src/state/resume.ts @@ -1,9 +1,9 @@ /** - * Web binding for `@codeoid/core`'s ResumeCursors — one singleton per app + * Web binding for `@highflame/codeoid-core`'s ResumeCursors — one singleton per app * (the web UI holds exactly one daemon connection). Keeps the existing * function-style API so call sites stay unchanged. */ -import { ResumeCursors } from "@codeoid/core"; +import { ResumeCursors } from "@highflame/codeoid-core"; import type { ScrollbackReplayMsg } from "../protocol/types"; const cursors = new ResumeCursors();