chore(workspace): move CLI to packages/cli, add @codeburn/core skeleton (phase 1)

This commit is contained in:
iamtoruk 2026-07-26 10:19:33 -07:00
parent cdd343bd78
commit dc97ab4936
357 changed files with 265 additions and 117 deletions

View file

@ -6,7 +6,9 @@ on:
paths:
- .github/workflows/build-windows-store.yml
- app/**
- src/**
- packages/cli/**
- dash/**
- scripts/**
- package.json
- package-lock.json

View file

@ -19,7 +19,7 @@ jobs:
set -e
semgrep --config .semgrep/rules/no-bracket-assign-hot-paths.yml \
--strict --json \
src/providers/ src/parser.ts > semgrep-out.json
packages/cli/src/providers/ packages/cli/src/parser.ts > semgrep-out.json
FINDINGS=$(jq '.results | length' semgrep-out.json)
if [ "$FINDINGS" -gt 0 ]; then
jq -r '.results[] | "::error file=\(.path),line=\(.start.line)::\(.extra.message)"' semgrep-out.json

2
.gitignore vendored
View file

@ -45,4 +45,4 @@ desktop/
appstore/
# WIP / not ready
src/summit.ts
packages/cli/src/summit.ts

View file

@ -18,7 +18,9 @@ cd codeburn
npm install
```
There is no separate build step required to run the dev CLI. `npm run dev` runs `tsx` against `src/cli.ts` directly.
This is an npm-workspaces monorepo: the published CLI lives in `packages/cli`, and `@codeburn/core` (a skeleton today) in `packages/core`. Run the root scripts (`npm test`, `npm run build`, `npm run dev`) from the repo root — they forward to the `codeburn` workspace.
There is no separate build step required to run the dev CLI. `npm run dev` runs `tsx` against `packages/cli/src/cli.ts` directly.
## Common Commands
@ -26,8 +28,8 @@ There is no separate build step required to run the dev CLI. `npm run dev` runs
|---|---|
| `npm test` | Runs the vitest suite (42 test files, 568 tests). |
| `npm run dev -- status` | Runs the CLI in dev mode against your real data. |
| `npm run build` | Bundles the litellm pricing snapshot, then runs `tsup` to produce `dist/cli.js`. |
| `npm run bundle-litellm` | Refreshes `src/data/litellm-snapshot.json` from the upstream litellm repo. |
| `npm run build` | Bundles the litellm pricing snapshot, then runs `tsup` to produce `packages/cli/dist/cli.js`. |
| `npm run bundle-litellm` | Refreshes `packages/cli/src/data/litellm-snapshot.json` from the upstream litellm repo. |
To test a specific suite, pass a path:
@ -45,13 +47,16 @@ npm test -- tests/providers/codex.test.ts
## Project Layout
```
src/ CLI, parsers, optimize detectors, cache layers
src/providers/ One file per AI tool integration
src/data/ Bundled litellm pricing snapshot
tests/ vitest specs
mac/ Swift menubar app
gnome/ GNOME shell extension
scripts/ Build helpers (litellm bundle)
packages/cli/ The published `codeburn` CLI (npm workspace)
packages/cli/src/ CLI, parsers, optimize detectors, cache layers
packages/cli/src/providers/ One file per AI tool integration
packages/cli/src/data/ Bundled litellm pricing snapshot
packages/cli/tests/ vitest specs
packages/core/ @codeburn/core — shared provider-agnostic core (skeleton)
dash/ React web dashboard (built into packages/cli/dist/dash)
mac/ Swift menubar app
gnome/ GNOME shell extension
scripts/ Build helpers (litellm bundle)
```
See `docs/architecture.md` for a fuller map.
@ -59,7 +64,7 @@ See `docs/architecture.md` for a fuller map.
## Coding Conventions
- TypeScript strict mode is on. Do not introduce `any` without a comment explaining why.
- Avoid bracket-assign (`obj[key] = value`) on parsed user input in hot paths inside `src/providers/` and `src/parser.ts`. There is a Semgrep rule (`.semgrep/rules/no-bracket-assign-hot-paths.yml`) enforced in CI that will fail your PR if you do. Use a `Map` or an explicit allowlist instead.
- Avoid bracket-assign (`obj[key] = value`) on parsed user input in hot paths inside `packages/cli/src/providers/` and `packages/cli/src/parser.ts`. There is a Semgrep rule (`.semgrep/rules/no-bracket-assign-hot-paths.yml`) enforced in CI that will fail your PR if you do. Use a `Map` or an explicit allowlist instead.
- Provider parsers must be deterministic given the same input. If you read the system clock or the filesystem outside the documented session paths, add a fixture-based test.
- New providers go through `src/providers/index.ts`. Lazy-load anything that pulls a heavy native dependency (sqlite, protobuf) so users without that provider are not slowed down.

View file

@ -26,15 +26,15 @@ npm run build
### 1. Update the Version
Edit `package.json` to bump the version number. Update both the `version` field at the top and the `package-lock.json` lockfile to match (npm handles this automatically):
The published CLI is the `codeburn` workspace at `packages/cli`. Edit `packages/cli/package.json` to bump the `version`; the root `package-lock.json` tracks it (npm handles this automatically):
```bash
npm version <version>
npm version <version> -w codeburn
```
For example, `npm version 0.9.8` updates both files and creates a commit.
For example, `npm version 0.9.8 -w codeburn` updates the workspace package and the lockfile.
Alternatively, edit `package.json` by hand and run `npm install` to regenerate the lockfile with the new version.
Alternatively, edit `packages/cli/package.json` by hand and run `npm install` to regenerate the lockfile with the new version.
### 2. Update the Changelog
@ -57,19 +57,19 @@ Edit `CHANGELOG.md`. Move all changes from the "Unreleased" section into a new s
Commit these changes:
```bash
git add CHANGELOG.md package.json package-lock.json
git add CHANGELOG.md packages/cli/package.json package-lock.json
git commit -m "chore: bump to 0.9.8"
```
### 3. Publish to npm
There is no GitHub Actions workflow for the CLI; the maintainer runs `npm publish` from a clean working tree:
There is no GitHub Actions workflow for the CLI; the maintainer publishes the `codeburn` workspace from a clean working tree:
```bash
npm publish
npm publish -w codeburn
```
The `prepublishOnly` script in `package.json` runs `npm run build` first, which bundles the litellm pricing snapshot and then runs `tsup` to emit `dist/cli.js`.
The `prepublishOnly` script in `packages/cli/package.json` runs `npm run build` first, which bundles the litellm pricing snapshot and then runs `tsup` to emit `packages/cli/dist/cli.js`.
If publishing for the first time on a new machine, run `npm login` first.

View file

@ -26,7 +26,7 @@ function fakeBin(name: string, body: string): string {
/** Writes the repo CLI under this test's isolated dev-root override. */
function fakeDevRepoCli(): string {
const repoRoot = join(dir, 'dev-repo')
const p = join(repoRoot, 'dist', 'cli.js')
const p = join(repoRoot, 'packages', 'cli', 'dist', 'cli.js')
mkdirSync(dirname(p), { recursive: true })
writeFileSync(p, '#!/usr/bin/env node\n', { mode: 0o755 })
chmodSync(p, 0o755)

View file

@ -258,14 +258,14 @@ export function resolveTarget(): CliTarget | null {
const devRepoRoot = process.env.CODEBURN_DEV_REPO_ROOT
if (devRepoRoot) {
// Test/advanced override, matching CODEBURN_PATH_DIRS: keep dev lookup in an isolated repo root.
const devBin = join(devRepoRoot, 'dist', 'cli.js')
const devBin = join(devRepoRoot, 'packages', 'cli', 'dist', 'cli.js')
if (isExecutableFile(devBin)) return { kind: 'external', bin: devBin }
} else {
const devBin = join(__dirname, '..', '..', '..', 'dist', 'cli.js')
const devBin = join(__dirname, '..', '..', '..', 'packages', 'cli', 'dist', 'cli.js')
if (isExecutableFile(devBin)) return { kind: 'external', bin: devBin }
// Vitest loads this source module from app/electron rather than the emitted
// app/dist/electron directory; keep the same repo CLI discoverable there.
const sourceDevBin = join(__dirname, '..', '..', 'dist', 'cli.js')
const sourceDevBin = join(__dirname, '..', '..', 'packages', 'cli', 'dist', 'cli.js')
if (isExecutableFile(sourceDevBin)) return { kind: 'external', bin: sourceDevBin }
}
}

View file

@ -25,7 +25,10 @@ import { fileURLToPath } from 'node:url'
const here = dirname(fileURLToPath(import.meta.url)) // app/scripts
const appDir = join(here, '..') // app
const root = join(appDir, '..') // repo root
const dist = join(root, 'dist')
const cliPkg = join(root, 'packages', 'cli') // the codeburn CLI workspace
const dist = join(cliPkg, 'dist')
// Workspace installs hoist production deps to the repo-root node_modules, so the
// dependency closure is copied from there.
const rootModules = join(root, 'node_modules')
const stage = join(appDir, 'build', 'cli')
@ -38,7 +41,7 @@ for (const f of ['cli.js', 'main.js']) {
rmSync(stage, { recursive: true, force: true })
mkdirSync(join(stage, 'dist'), { recursive: true })
copyFileSync(join(root, 'package.json'), join(stage, 'package.json'))
copyFileSync(join(cliPkg, 'package.json'), join(stage, 'package.json'))
copyFileSync(join(dist, 'cli.js'), join(stage, 'dist', 'cli.js'))
copyFileSync(join(dist, 'main.js'), join(stage, 'dist', 'main.js'))

View file

@ -13,7 +13,7 @@ export default defineConfig({
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
},
build: {
outDir: '../dist/dash',
outDir: '../packages/cli/dist/dash',
emptyOutDir: true,
},
server: {

View file

@ -26,9 +26,9 @@ CodeBurn is one Node.js CLI plus two GUI clients that shell out to it.
The macOS menubar (`mac/`) and the GNOME extension (`gnome/`) both invoke `codeburn status --format menubar-json --period <p>` and parse the JSON. They do not share code with the CLI; they only depend on its output contract.
## CLI (`src/`)
## CLI (`packages/cli/src/`)
`src/cli.ts` is the Commander.js entry point. The bin field in `package.json` points at `dist/cli.js`. Twelve commands are registered:
`packages/cli/src/cli.ts` is the Commander.js entry point. The bin field in `packages/cli/package.json` points at `dist/cli.js`. Twelve commands are registered:
| Command | Line | Purpose |
|---|---|---|
@ -170,10 +170,10 @@ Plain JavaScript, no bundler. Targets GNOME Shell 45-50 (`metadata.json`).
`npm run build` is two steps:
1. `node scripts/bundle-litellm.mjs` fetches the latest litellm pricing JSON and writes `src/data/litellm-snapshot.json`. The bundle script keeps a manual override for MiniMax variants. Direct (un-prefixed) entries win over prefixed ones. The result is checked in so the build is reproducible.
2. `tsup` reads `tsup.config.ts` and emits a single ESM bundle at `dist/cli.js` with a Node shebang banner. No source maps in publish builds; sourcemaps on for development.
1. `node ../../scripts/bundle-litellm.mjs` fetches the latest litellm pricing JSON and writes `packages/cli/src/data/litellm-snapshot.json`. The bundle script keeps a manual override for MiniMax variants. Direct (un-prefixed) entries win over prefixed ones. The result is checked in so the build is reproducible.
2. `tsup` reads `packages/cli/tsup.config.ts` and emits a single ESM bundle at `packages/cli/dist/cli.js` with a Node shebang banner. No source maps in publish builds; sourcemaps on for development.
The `prepublishOnly` hook in `package.json` runs `npm run build` so `npm publish` always ships fresh code.
The `prepublishOnly` hook in `packages/cli/package.json` runs `npm run build` so `npm publish -w codeburn` always ships fresh code.
## Tests

View file

@ -1,5 +1,12 @@
# Phase 0 fan-out recipe: lift `calculateCost` out of decoders
> **Path note (Phase 1):** The CLI source has moved into the `packages/cli`
> workspace. Every `src/...` and `tests/...` path in this document now lives
> under `packages/cli/` (e.g. `src/pricing-pass.ts``packages/cli/src/pricing-pass.ts`,
> `tests/providers/<name>.test.ts``packages/cli/tests/providers/<name>.test.ts`).
> Relative import paths inside `packages/cli/src` (such as `../../src/pricing-pass.js`)
> are unaffected because the source tree moved as a whole.
Phase 0 of the `@codeburn/core` extraction (issue #809) moves cost computation
out of the provider decoders and into a single host-side pricing pass
(`src/pricing-pass.ts`). Decoders emit **token counts + a `costBasis` marker**;

82
package-lock.json generated
View file

@ -1,38 +1,16 @@
{
"name": "codeburn",
"name": "codeburn-monorepo",
"version": "0.9.19",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codeburn",
"name": "codeburn-monorepo",
"version": "0.9.19",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"bonjour-service": "^1.4.1",
"chalk": "^5.4.1",
"commander": "^13.1.0",
"ink": "^7.0.0",
"react": "^19.2.5",
"selfsigned": "^5.5.0",
"strip-ansi": "^7.2.0",
"undici": "^7.27.2",
"zod": "^3.25.76"
},
"bin": {
"codeburn": "dist/cli.js"
},
"devDependencies": {
"@types/node": "^22.19.17",
"@types/react": "^19.2.14",
"@types/selfsigned": "^2.0.4",
"playwright": "^1.61.1",
"tsup": "^8.4.0",
"tsx": "^4.19.0",
"typescript": "^5.8.0",
"vitest": "^3.1.0"
},
"workspaces": [
"packages/*"
],
"engines": {
"node": ">=22.13.0"
}
@ -50,6 +28,10 @@
"node": ">=18"
}
},
"node_modules/@codeburn/core": {
"resolved": "packages/core",
"link": true
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
@ -1633,6 +1615,10 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/codeburn": {
"resolved": "packages/cli",
"link": true
},
"node_modules/commander": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
@ -4127,6 +4113,48 @@
"peerDependencies": {
"zod": "^3.25.28 || ^4"
}
},
"packages/cli": {
"name": "codeburn",
"version": "0.9.19",
"license": "MIT",
"dependencies": {
"@codeburn/core": "*",
"@modelcontextprotocol/sdk": "^1.29.0",
"bonjour-service": "^1.4.1",
"chalk": "^5.4.1",
"commander": "^13.1.0",
"ink": "^7.0.0",
"react": "^19.2.5",
"selfsigned": "^5.5.0",
"strip-ansi": "^7.2.0",
"undici": "^7.27.2",
"zod": "^3.25.76"
},
"bin": {
"codeburn": "dist/cli.js"
},
"devDependencies": {
"@types/node": "^22.19.17",
"@types/react": "^19.2.14",
"@types/selfsigned": "^2.0.4",
"playwright": "^1.61.1",
"tsup": "^8.4.0",
"tsx": "^4.19.0",
"typescript": "^5.8.0",
"vitest": "^3.1.0"
},
"engines": {
"node": ">=22.13.0"
}
},
"packages/core": {
"name": "@codeburn/core",
"version": "0.9.19",
"license": "MIT",
"engines": {
"node": ">=22.13.0"
}
}
}
}

View file

@ -1,41 +1,20 @@
{
"name": "codeburn",
"name": "codeburn-monorepo",
"version": "0.9.19",
"description": "See where your AI spend goes - by task, tool, model, and project",
"description": "CodeBurn monorepo — CLI (packages/cli) + @codeburn/core (packages/core)",
"private": true,
"type": "module",
"main": "./dist/cli.js",
"bin": {
"codeburn": "dist/cli.js"
},
"files": [
"dist"
"workspaces": [
"packages/*"
],
"scripts": {
"bundle-litellm": "node scripts/bundle-litellm.mjs",
"build": "node scripts/bundle-litellm.mjs && tsup && node -e \"const fs=require('fs'); fs.copyFileSync('src/cli.ts','dist/cli.js'); fs.chmodSync('dist/cli.js',0o755)\" && npm run build:dash",
"build:cli": "tsup && node -e \"const fs=require('fs'); fs.copyFileSync('src/cli.ts','dist/cli.js'); fs.chmodSync('dist/cli.js',0o755)\"",
"build:dash": "cd dash && npm install --no-audit --no-fund --silent && npm run build",
"dev": "NODE_OPTIONS=--no-deprecation tsx src/cli.ts",
"test": "vitest",
"prepublishOnly": "npm run build"
"bundle-litellm": "npm run bundle-litellm -w codeburn",
"build": "npm run build -w codeburn",
"build:cli": "npm run build:cli -w codeburn",
"build:dash": "npm run build:dash -w codeburn",
"dev": "npm run dev -w codeburn",
"test": "npm run test -w codeburn"
},
"keywords": [
"claude-code",
"cursor",
"codex",
"kimi",
"devin",
"ibm-bob",
"opencode",
"pi",
"codebuff",
"codewhale",
"ai-coding",
"token-usage",
"cost-tracking",
"observability",
"developer-tools"
],
"engines": {
"node": ">=22.13.0"
},
@ -48,27 +27,5 @@
"bugs": {
"url": "https://github.com/getagentseal/codeburn/issues"
},
"homepage": "https://github.com/getagentseal/codeburn#readme",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"bonjour-service": "^1.4.1",
"chalk": "^5.4.1",
"commander": "^13.1.0",
"ink": "^7.0.0",
"react": "^19.2.5",
"selfsigned": "^5.5.0",
"strip-ansi": "^7.2.0",
"undici": "^7.27.2",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/node": "^22.19.17",
"@types/react": "^19.2.14",
"@types/selfsigned": "^2.0.4",
"playwright": "^1.61.1",
"tsup": "^8.4.0",
"tsx": "^4.19.0",
"typescript": "^5.8.0",
"vitest": "^3.1.0"
}
"homepage": "https://github.com/getagentseal/codeburn#readme"
}

75
packages/cli/package.json Normal file
View file

@ -0,0 +1,75 @@
{
"name": "codeburn",
"version": "0.9.19",
"description": "See where your AI spend goes - by task, tool, model, and project",
"type": "module",
"main": "./dist/cli.js",
"bin": {
"codeburn": "dist/cli.js"
},
"files": [
"dist"
],
"scripts": {
"bundle-litellm": "node ../../scripts/bundle-litellm.mjs",
"build": "node ../../scripts/bundle-litellm.mjs && tsup && node -e \"const fs=require('fs'); fs.copyFileSync('src/cli.ts','dist/cli.js'); fs.chmodSync('dist/cli.js',0o755)\" && npm run build:dash",
"build:cli": "tsup && node -e \"const fs=require('fs'); fs.copyFileSync('src/cli.ts','dist/cli.js'); fs.chmodSync('dist/cli.js',0o755)\"",
"build:dash": "cd ../../dash && npm install --no-audit --no-fund --silent && npm run build",
"dev": "NODE_OPTIONS=--no-deprecation tsx src/cli.ts",
"test": "vitest",
"prepublishOnly": "npm run build"
},
"keywords": [
"claude-code",
"cursor",
"codex",
"kimi",
"devin",
"ibm-bob",
"opencode",
"pi",
"codebuff",
"codewhale",
"ai-coding",
"token-usage",
"cost-tracking",
"observability",
"developer-tools"
],
"engines": {
"node": ">=22.13.0"
},
"author": "AgentSeal <hello@agentseal.org>",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/getagentseal/codeburn.git"
},
"bugs": {
"url": "https://github.com/getagentseal/codeburn/issues"
},
"homepage": "https://github.com/getagentseal/codeburn#readme",
"dependencies": {
"@codeburn/core": "*",
"@modelcontextprotocol/sdk": "^1.29.0",
"bonjour-service": "^1.4.1",
"chalk": "^5.4.1",
"commander": "^13.1.0",
"ink": "^7.0.0",
"react": "^19.2.5",
"selfsigned": "^5.5.0",
"strip-ansi": "^7.2.0",
"undici": "^7.27.2",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/node": "^22.19.17",
"@types/react": "^19.2.14",
"@types/selfsigned": "^2.0.4",
"playwright": "^1.61.1",
"tsup": "^8.4.0",
"tsx": "^4.19.0",
"typescript": "^5.8.0",
"vitest": "^3.1.0"
}
}

Some files were not shown because too many files have changed in this diff Show more