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
56 changes: 41 additions & 15 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
name: Publish to npm

# Staged publishing: the workflow submits the tarball, a maintainer approves it
# with 2FA. Trusted publishers created after 2026-09-03 are staging-only, so
# "npm publish" is rejected by the registry even with a valid OIDC token — the
# supported path is "npm stage publish" plus manual approval.
on:
release:
types: [published]
Expand All @@ -19,26 +23,48 @@ jobs:
with:
node-version: 24.x
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
package-manager-cache: false

- run: npm ci
# Staged publishing needs npm >= 11.15.0; the version bundled with Node may
# be older (11.6.x rejects "npm stage" as an unknown command). Pinned to the
# 11.x line rather than @latest so the release path cannot jump a major.
- name: Ensure a staging-capable npm CLI
run: |
npm install -g npm@^11.15.0
npm --version

- name: Set version from tag
# The release tag is the source of truth for what users install. A mismatch
# is a hard failure: silently rewriting the version ships an unreviewed tree.
- name: Check the tag matches the committed version
run: |
VERSION="${GITHUB_REF_NAME#v}"
CURRENT="$(node -p "require('./package.json').version")"
if [ "$CURRENT" != "$VERSION" ]; then npm version "$VERSION" --no-git-tag-version; fi
VERSION="$(node -p "require('./package.json').version")"
if [ "$GITHUB_REF_NAME" != "v$VERSION" ]; then
echo "::error::Tag $GITHUB_REF_NAME does not match package.json version $VERSION. Tag v$VERSION or bump the package."
exit 1
fi
echo "Staging @fiale-plus/jev-cli@$VERSION"

- run: npm ci
- run: npm run build
- run: npm test

- name: Publish
# Trusted publishing (OIDC): no NPM_TOKEN needed. Configure once at
# npmjs.com → package Settings → Trusted Publisher → GitHub Actions
# (org fiale-plus, repo jev-cli, workflow npm-publish.yml).
run: npm publish --provenance --access public
- name: Stage on npm
# Provenance attestations are generated automatically for trusted
# publishers; --access public keeps the scoped package readable.
run: npm stage publish --access public

- name: Verify publication
- name: Report what to approve
run: |
VERSION="${GITHUB_REF_NAME#v}"
sleep 10
npm view @fiale-plus/jev-cli@"$VERSION"
VERSION="$(node -p "require('./package.json').version")"
npm stage list @fiale-plus/jev-cli || true
{
echo "### Staged @fiale-plus/jev-cli@$VERSION"
echo
echo "Approve with 2FA to move it to prod:"
echo
echo '```'
echo "npm stage approve <stage-id>"
echo '```'
echo
echo "Or use the Staged Packages tab on npmjs.com."
} >> "$GITHUB_STEP_SUMMARY"
27 changes: 21 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,43 @@ Unofficial CLI over the official TypeSafe SDK (`@typesafe-ai/sdk`).

```bash
npm run build # tsc -> dist/
npm test # unit tests (mocked fetch with real Response)
npm test # unit + contract tests (mocked fetch with real Response)
npm run test:integration # offline CLI tests (help, lint, validation — no key needed)
npm run dev -- <args> # run CLI directly via tsx
npm run stub # deterministic offline stub API on 127.0.0.1:8787
npm run evaluate -- --records out/ --labels examples/verify/labels.jsonl
```

Requires Node.js >= 22.

## Architecture

- `src/api/client.ts` — thin wrapper: `createClient`/`systemOne` over `TypeSafeClient`, cost estimate
- `src/commands/requests.ts` — `noul`/`choice`/`score`/`ask` request builders (same upstream shape)
- `src/commands/requests.ts` — `noul`/`choice`/`score`/`ask` request builders (same upstream shape), records
- `src/commands/ops.ts` — `batch` (bounded-concurrency JSONL), `lint` (structural only), `models`
- `src/cli/` — strict parseArgs, structural lint, formatters (json/table), help text
- `src/utils/` — `resolveApiKey` (flag > `TYPESAFE_API_KEY`), state readers (text/json, single source), numeric parsing
- `src/commands/gate.ts` — offline policy evaluation over a saved response or record
- `src/commands/replay.ts` — re-emit a stored record, marked `replayed: true`, no API call
- `src/commands/packs.ts` — pack loading/validation from `packs/`, listed by `jev packs`
- `src/commands/doctor.ts` — local config checks, `--live` for auth/models/latency/cost
- `src/cli/` — strict parseArgs, structural lint, policy schema + evaluation, records, formatters, help text
- `src/utils/` — `resolveApiKey` (flag > `TYPESAFE_API_KEY`), state readers, numeric parsing, canonical hashing, package root/version
- `packs/` — versioned question sets + policies (`verify`, `screen`, `route`); shipped in the tarball
- `src/tests/` — node:test runner, fixtures in `tests/fixtures/`
- `tools/` — `stub-server.mjs` (offline API), `evaluate.mjs` (policy scoring over labeled records)
- `examples/` — inputs and labels only; never committed model output

## Conventions

- Official SDK owns transport, retries, errors, types — never duplicate
- ESM (`"type": "module"`) with `.js` import extensions
- API speaks camelCase bodies; CLI flags use kebab-case
- Successful inference exits 0; policy lives in the caller
- Tests mock `global.fetch` with real `Response` objects (SDK clones responses)
- Gate exit codes are decisions: 0 accept, 2 review, 3 deny, 4 abstain (1 = error). Judgment on stdout, decision in the status
- Policy evaluation is offline and fail-closed: a missing or wrong-typed answer abstains; a choice answer needs a normalized probability map (no confidence substitution); a score must sit inside its reported scale; a label outside `accept` never accepts
- Records hash the state (type-tagged) and never store it; a record from a stub carries a `stub:` model so it cannot pass as real. `jev replay` is not a rerun
- Gating a record against its pack checks identity: changed questions exit 1, changed thresholds warn
- `--version` reads `package.json` at runtime; the publish workflow fails when a release tag disagrees with it, and stages with `npm stage publish` for manual 2FA approval
- Tests mock `global.fetch` with real `Response` objects (SDK clones responses); `contract.test.ts` spawns the real CLI
- Structural lint only; question-design advice lives in the official skill
- No eval/calibration in core; no threshold flags; no exit-code gating
- No eval/calibration or threshold flags on the model-calling path; evaluation runs over saved records
- No MCP surface: deliberately out of scope
Loading
Loading