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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ jobs:
- name: Build
run: pnpm turbo run build

# Drift gate (2.L · ADR-0051): the published CLI bundle inlines the @relavium/* engine and externalizes
# every third-party dep — its external import closure must equal the declared `dependencies`, or a global
# `npm i -g relavium` would fail to resolve a missing dep. Needs the build above.
- name: CLI bundle closure matches its declared dependencies
run: pnpm lint:bundle-closure

# Drift gate: the committed migration must match src/schema.ts. If a schema change
# (or an upstream @relavium/shared enum change the CHECKs derive from) wasn't
# regenerated via `pnpm --filter @relavium/db db:generate`, regenerating here produces
Expand Down
147 changes: 147 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Relavium CLI release — pack once, smoke the exact artifact on every OS, then publish it (2.L · ADR-0051).
#
# Flow: `pack` builds the engine-inlined bundle and `pnpm pack`s it (resolving catalog:/workspace: to
# concrete versions) into a tarball artifact → `smoke` installs THAT SAME tarball globally on ubuntu /
# macOS / Windows and asserts the binary works (prebuilt native addons load, the bundled engine runs) →
# `publish` (only on a `v*` tag, gated on green smoke) publishes the very artifact that was smoked, with
# npm provenance. The actual publish needs the maintainer's `NPM_TOKEN` secret — it is maintainer-gated,
# like branch protection. Verifying cross-OS BEFORE publish is the Phase-3 go/no-go #7 gate.
#
# Third-party actions are pinned to a commit SHA (the `# vX.Y.Z` comment tracks the release), matching ci.yml.
name: Release CLI

on:
push:
tags: ['v*']
# Manual dry-run: build + smoke cross-OS without publishing (publish is gated on a tag below).
workflow_dispatch:
Comment thread
coderabbitai[bot] marked this conversation as resolved.

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

# Permissions are declared per job (least privilege) rather than workflow-wide.

jobs:
pack:
name: build + pack the tarball
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
# On a release tag, the tag must equal the CLI's package version — else we'd publish a mismatched
# version. Fail fast (before build + the cross-OS smoke). Skipped on a manual dry-run (no tag).
- name: Tag matches the CLI package version
if: startsWith(github.ref, 'refs/tags/v')
shell: bash
run: |
TAG="${GITHUB_REF_NAME#v}"
PKG="$(node -p "require('./apps/cli/package.json').version")"
test "$TAG" = "$PKG" || { echo "::error::release tag v$TAG != apps/cli/package.json version $PKG"; exit 1; }
- name: Set up pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
cache: pnpm
- name: Install (frozen lockfile)
run: pnpm install --frozen-lockfile
# Build the engine packages first (turbo resolves the dependency order) so `pnpm pack`'s prepack can
# inline them into the bundle.
- name: Build
run: pnpm turbo run build --filter=relavium...
# `pnpm pack` (not `npm pack`) resolves catalog:/workspace: in the published manifest; its `prepack`
# rebuilds the bundle, copies the drizzle migrations beside it, AND runs the bundle-closure guard — so
# the guard validates exactly the dist that gets tarballed. The tarball is platform-independent (native
# addons resolve per-OS at install), so one tarball is smoked on, and published to, every target.
- name: Pack
working-directory: apps/cli
run: pnpm pack --pack-destination "${{ runner.temp }}/artifact"
- name: Upload tarball
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: relavium-tarball
path: ${{ runner.temp }}/artifact/*.tgz
if-no-files-found: error

smoke:
name: install-smoke (${{ matrix.os }})
needs: pack
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
permissions:
contents: read
steps:
# Checkout only for the fixture workflows the smoke runs; no build here — we install the published tarball.
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Download tarball
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: relavium-tarball
path: artifact
# One bash script across all three OSes (GitHub provides Git Bash on Windows) so the asserts are uniform.
# These commands exercise the bundle + BOTH prebuilt native addons (better-sqlite3 opens history.db with
# the shipped migrations; @napi-rs/keyring's accessor loads) WITHOUT touching the OS credential store, so
# the Windows leg is headless-safe.
- name: Install globally + smoke
shell: bash
run: |
set -euo pipefail
TGZ=$(ls artifact/*.tgz)
npm install -g "$TGZ"
echo "## relavium --help" && relavium --help >/dev/null
echo "## relavium provider list" && relavium provider list
echo "## relavium run (sequential) --json (exit 0)"
relavium run apps/cli/src/harness/fixtures/sequential.relavium.yaml --input n=21 --json
echo "## relavium run (human-gate) --json (exit 3 = gate-paused)"
set +e; relavium run apps/cli/src/harness/fixtures/human-gate.relavium.yaml --json >/dev/null; ec=$?; set -e
test "$ec" -eq 3 || { echo "expected exit 3, got $ec"; exit 1; }
echo "## relavium gate list" && relavium gate list
echo "## unknown runId → exit 2"
set +e; relavium logs nope >/dev/null 2>&1; ec=$?; set -e
test "$ec" -eq 2 || { echo "expected exit 2, got $ec"; exit 1; }
echo "✓ smoke passed on ${{ matrix.os }}"

publish:
name: publish to npm (provenance)
needs: smoke
# Only a real release tag publishes; a manual workflow_dispatch run stops after the cross-OS smoke.
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
id-token: write # npm provenance attestation
steps:
- name: Set up Node (npm registry)
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
registry-url: https://registry.npmjs.org
- name: Download the smoked tarball
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: relavium-tarball
path: artifact
# Publish the EXACT artifact the smoke matrix proved — not a fresh build. catalog:/workspace: are already
# resolved in the packed manifest, so `npm publish <tarball>` is correct here (pnpm-pack did the resolving).
- name: Publish
shell: bash
run: npm publish "$(ls artifact/*.tgz)" --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ build/
out/
*.tsbuildinfo
.turbo/
# The CLI bundle ships @relavium/db's migrations beside its bundle; the copy is a build artifact
# (2.L / ADR-0051). The source of truth stays tracked at packages/db/drizzle.
/apps/cli/drizzle/

# Tauri / Rust
target/
Expand Down
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ resolution (2.B) landed (PR #40), `relavium run` is wired to the engine
landed (2.F — PR #42, ADR-0049), the engine regression harness (2.K — PR #43)
completes M3, and durable run history landed (2.H — PR #44, ADR-0050); the provider/key
commands with OS-keychain storage landed (2.C — PR #45, behind ADR-0019 + ADR-0006); the `ink`
streaming TUI landed (2.E — PR #46, behind ADR-0047); and the interactive human-gate prompt + the
streaming TUI landed (2.E — PR #46, behind ADR-0047); the interactive human-gate prompt + the
out-of-band `relavium gate` cross-process resume landed (2.G — PR #47, behind ADR-0047), **fully closing
2.K's deferred gate-resume half**. The next pickup is 2.I (`list` / `logs` / `status` / `gate list` over
durable history). For live status, per-PR history,
2.K's deferred gate-resume half**; and the read commands `list` / `logs` / `status` / `gate list` over durable
history landed (2.I — PR #48, no new ADR). The next pickup is 2.L (packaging & install verification — the last
gate-closing spine PR). For live status, per-PR history,
milestone dates, and open obligations, see the canonical home
[docs/roadmap/current.md](docs/roadmap/current.md); [README.md](README.md) is the
public overview.
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ local-first BYOK — workflow parsing, DAG execution, live streaming, checkpoint
multi-provider failover, cost governance, and multimodal media I/O. **Phase 2 (the CLI) is
underway** — the CLI skeleton, config resolution, `relavium run` (wired to the engine), its
`--json` CI machine-output contract, the engine regression harness, durable local run history, the
provider/key commands (API keys in the OS keychain), the live `ink` streaming TUI, and the human-gate
prompt + out-of-band `relavium gate` resume have landed (milestone **M3** reached). For live status and the full roadmap, see
provider/key commands (API keys in the OS keychain), the live `ink` streaming TUI, the human-gate
prompt + out-of-band `relavium gate` resume, and the read commands (`list` / `logs` / `status` / `gate list`)
over durable history have landed (milestone **M3** reached). For live status and the full roadmap, see
[docs/roadmap/current.md](docs/roadmap/current.md) and the
[roadmap](docs/roadmap/README.md).

Expand Down
57 changes: 52 additions & 5 deletions apps/cli/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,54 @@
# `apps/cli` — placeholder
# relavium

Terminal CLI (`relavium`) — the engine's first real consumer + regression harness.
> Run agent workflows from your terminal — a product of [HodeTech](https://github.com/HodeTech).

> **Not built yet.** Directory placeholder; built out in
> [Phase 2 — CLI](../../docs/roadmap/phases/phase-2-cli.md). No `package.json` yet, so
> pnpm/Turborepo do not treat it as a workspace.
`relavium` is the command-line surface of the [Relavium](https://github.com/HodeTech/Relavium)
local-first AI agent platform. It runs git-committable `.relavium.yaml` workflows on the same
pure-TypeScript engine as the desktop and VS Code surfaces — every step debuggable, every token and
dollar tracked, nothing leaving your machine unless you choose it.

## Install

```bash
npm install -g relavium
```

Requires **Node.js ≥ 20.12**. The package ships an engine-inlined bundle and installs prebuilt native
binaries, so no C/C++ toolchain is needed.

## Quick start

```bash
# run a workflow, streaming live progress in the terminal
relavium run ./workflows/code-review.relavium.yaml --input file=./src/index.ts

# CI / scripting: a stable NDJSON RunEvent stream, deterministic exit codes
relavium run ./workflows/code-review.relavium.yaml --input file=src/index.ts --json

# store a provider key in the OS keychain (read from stdin, never argv)
echo "$ANTHROPIC_API_KEY" | relavium provider set-key anthropic
```

## Commands

| Command | Purpose |
|---|---|
| `relavium run <workflow> [--input k=v]` | Execute a workflow; streams progress (or `--json` NDJSON). |
| `relavium list [--agents]` | List discovered workflows (or agents) with last-run status. |
| `relavium logs <runId>` | Replay a past run's event stream. |
| `relavium status` | Show active/paused runs and their per-node status. |
| `relavium gate <runId> --approve\|--reject\|--input …` | Resolve a pending human gate. |
| `relavium gate list [<runId>]` | List pending human gates. |
| `relavium provider <list\|add\|set-key\|remove-key\|test>` | Manage providers + API keys (OS keychain). |

**Exit codes** (CI-friendly): `0` completed · `1` failed · `2` invalid invocation · `3` paused at a
human gate. Provider keys resolve from the OS keychain → `RELAVIUM_<PROVIDER>_API_KEY` env var → error.

## Documentation

The full command reference, the `--json` machine contract, and the CI guide live in the
[Relavium docs](https://github.com/HodeTech/Relavium/tree/main/docs/reference/cli/commands.md).

## License

Proprietary — © HodeTech, all rights reserved. See [LICENSE](https://github.com/HodeTech/Relavium/blob/main/LICENSE).
42 changes: 34 additions & 8 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,35 +1,61 @@
{
"name": "relavium",
"version": "0.0.0",
"private": true,
"version": "0.1.0",
"description": "Relavium CLI (`relavium`) — run agent workflows from the terminal; the engine's first real consumer.",
"license": "SEE LICENSE IN LICENSE",
"type": "module",
"description": "Relavium CLI (`relavium`) — the terminal surface and the engine's regression harness (build phase 2).",
"bin": {
"relavium": "./dist/index.js"
},
"files": [
"dist"
"dist",
"drizzle"
],
"keywords": [
"relavium",
"ai",
"agent",
"workflow",
"cli",
"llm"
],
"repository": {
"type": "git",
"url": "git+https://github.com/HodeTech/Relavium.git",
"directory": "apps/cli"
},
"engines": {
"node": ">=20.12.0"
},
"scripts": {
"build": "tsup",
"prepack": "tsup && pnpm -w run lint:bundle-closure",
"typecheck": "tsc -p tsconfig.json --noEmit",
"lint": "eslint src",
"test": "vitest run"
},
"dependencies": {
"@anthropic-ai/sdk": "catalog:",
"@clack/prompts": "catalog:",
"@google/genai": "catalog:",
"@jitl/quickjs-singlefile-mjs-release-sync": "catalog:",
"@napi-rs/keyring": "catalog:",
"@relavium/core": "workspace:*",
"@relavium/db": "workspace:*",
"@relavium/llm": "workspace:*",
"@relavium/shared": "workspace:*",
"better-sqlite3": "catalog:",
"commander": "catalog:",
"drizzle-orm": "catalog:",
"ink": "catalog:",
"openai": "catalog:",
"quickjs-emscripten-core": "catalog:",
"react": "catalog:",
"smol-toml": "catalog:",
"yaml": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@relavium/core": "workspace:*",
"@relavium/db": "workspace:*",
"@relavium/llm": "workspace:*",
"@relavium/shared": "workspace:*",
"@types/node": "catalog:",
"@types/react": "catalog:",
"eslint": "catalog:",
Expand Down
50 changes: 46 additions & 4 deletions apps/cli/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,57 @@
import { cpSync, rmSync } from 'node:fs';

import { defineConfig } from 'tsup';

// Build the CLI to a single ESM `bin` (ADR-0047). Pure-JS deps (commander; smol-toml at
// 2.B) are inlined; the native dep (@napi-rs/keyring at 2.C) is externalized in packaging
// (2.L). The shebang is prepended so the published `dist/index.js` is directly executable.
/**
* The CLI distribution bundle (2.L, [ADR-0051](../../docs/decisions/0051-cli-distribution-thin-bundle-private-engine.md)).
*
* INLINE only the proprietary `@relavium/*` engine packages (`noExternal`); EXTERNALIZE every third-party
* dependency — they are declared in `package.json` and installed normally by npm (the prebuilt native addons
* `better-sqlite3` + `@napi-rs/keyring` cannot be bundled; the quickjs WASM + the vendor SDKs + `ink`/`react`
* are bundler-hostile). `tools/bundle-closure/check.mjs` guards that this `external` list equals the declared
* runtime `dependencies`, so the two can never drift.
*
* `sourcemap: false` + `minify: true`: the published `dist/` must ship **no** original engine source — a source
* map would embed the inlined engine's TypeScript into the tarball and defeat keeping it unpublished (ADR-0051).
* The shebang banner makes `dist/index.js` directly executable as the `relavium` bin.
*/
const THIRD_PARTY_EXTERNAL = [
'@anthropic-ai/sdk',
'@clack/prompts',
'@google/genai',
'@jitl/quickjs-singlefile-mjs-release-sync',
'@napi-rs/keyring',
'better-sqlite3',
'commander',
'drizzle-orm',
'ink',
'openai',
'quickjs-emscripten-core',
'react',
'smol-toml',
'yaml',
'zod',
];

export default defineConfig({
entry: { index: 'src/index.ts' },
format: ['esm'],
target: 'node20',
platform: 'node',
clean: true,
sourcemap: true,
sourcemap: false,
minify: true,
dts: false,
noExternal: [/^@relavium\//],
external: THIRD_PARTY_EXTERNAL,
banner: { js: '#!/usr/bin/env node' },
// The inlined `@relavium/db` resolves its drizzle migrations via `new URL('../drizzle', import.meta.url)`,
// which — once bundled — points beside THIS bundle, not the db package. So ship the migration set alongside
// `dist/` (`files: ["dist","drizzle"]`); `<pkg>/drizzle` is then what the bundled db code finds at runtime.
onSuccess: async () => {
// Clean first: cpSync merges (recursive) and never deletes, so a migration removed/renamed in the source
// would otherwise linger here and ship stale. Recreate from the source of truth each build.
rmSync('./drizzle', { recursive: true, force: true });
cpSync('../../packages/db/drizzle', './drizzle', { recursive: true });
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
Comment on lines +51 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Stale migration files can accumulate in ./drizzle over time (e.g., when migrations are deleted or renamed in the source package) because cpSync with recursive: true merges files and does not delete extraneous ones in the destination. To prevent stale migrations from being packaged into the npm bundle, clean the destination directory before copying. Additionally, copy the repository LICENSE file to the package directory so that the "license": "SEE LICENSE IN LICENSE" field in package.json resolves correctly in the published package.

  onSuccess: async () => {
    rmSync('./drizzle', { recursive: true, force: true });
    cpSync('../../packages/db/drizzle', './drizzle', { recursive: true });
    try {
      cpSync('../../LICENSE', './LICENSE', { force: true });
    } catch (err) {
      // Ignore if root LICENSE is missing during local dev builds
    }
  },

});
Loading
Loading