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
64 changes: 42 additions & 22 deletions .github/workflows/manifest-drift.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
name: manifest drift
name: upstream drift

# Weekly check that src/manifest.json still matches the latest HEY CLI release.
# Only `hey version`, `hey commands` and `hey <command> --help` are run; no login, no mailbox access.
# Compares the HEY command surface on basecamp/hey-cli main (built from source) against
# src/manifest.json and fails, with a readable diff, when commands or flags differ.
# Help-text-only changes are listed in the job summary but do not fail the job.
# Only `hey commands --json` and `hey <command> --help` run; no login, no mailbox access.
on:
schedule:
- cron: "17 13 * * 1"
- cron: "17 13 * * *"
workflow_dispatch:
inputs:
ref:
description: basecamp/hey-cli branch or tag to compare against
default: main
push:
branches: [main]
paths:
- src/manifest.json
- scripts/**
- .github/workflows/manifest-drift.yml
pull_request:
paths:
- src/manifest.json
- scripts/**
- .github/workflows/manifest-drift.yml

permissions:
contents: read
Expand All @@ -20,25 +37,28 @@ jobs:
node-version: 22
cache: npm
- run: npm ci
- name: Install latest HEY CLI release
- name: Check out basecamp/hey-cli
uses: actions/checkout@v4
with:
repository: basecamp/hey-cli
ref: ${{ github.event.inputs.ref || 'main' }}
path: hey-cli
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-go@v5
with:
go-version-file: hey-cli/go.mod
cache: false
- name: Compare HEY's command list with src/manifest.json
env:
GH_TOKEN: ${{ github.token }}
HEY_REF: ${{ github.event.inputs.ref || 'main' }}
run: node scripts/check-drift.js --source hey-cli --ref "$HEY_REF"
- name: Note newer HEY releases
if: always()
run: |
set -euo pipefail
tag=$(gh release view -R basecamp/hey-cli --json tagName -q .tagName)
version=${tag#v}
mkdir -p "$RUNNER_TEMP/hey"
cd "$RUNNER_TEMP/hey"
gh release download "$tag" -R basecamp/hey-cli -p "hey_${version}_linux_amd64.tar.gz" -p checksums.txt
grep " hey_${version}_linux_amd64.tar.gz\$" checksums.txt | sha256sum -c -
tar xzf "hey_${version}_linux_amd64.tar.gz" hey
echo "HEY_BIN=$RUNNER_TEMP/hey/hey" >> "$GITHUB_ENV"
- run: npm run refresh-manifest
env:
HOME: ${{ runner.temp }}/hey-home
- name: Fail if the manifest is out of date
run: |
if ! git diff --exit-code -- src/manifest.json; then
echo "::error::src/manifest.json is stale. Run 'npm run refresh-manifest' with the latest hey and commit the result."
exit 1
ours=$(node -p "require('./src/manifest.json').hey_version.split('+')[0]")
latest=$(git -C hey-cli tag --list 'v*' --sort=-v:refname | head -n1)
if [ -n "$latest" ] && [ "${latest#v}" != "$ours" ]; then
echo "::warning title=HEY release::basecamp/hey-cli has released $latest; src/manifest.json builds on v$ours. Re-run npm run manifest:main (or check-drift --ref $latest --write) and update the README/CHANGELOG coverage wording."
fi
8 changes: 5 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,18 @@ A thin Node (ESM, Node 20+) wrapper around the HEY CLI (`hey`, github.com/baseca
- `src/hooks.js`: `hey-axi setup hooks`: session-start (via axi-sdk-js) and session-end hooks for Claude Code, Codex and OpenCode.
- `src/text.js`: `heyToAxi()` (rewrites `hey <cmd>` suggestions to `hey-axi <cmd>`) and `shellWord()`.
- `src/router.js`: `normalizeCatalog`, `resolveCommand`, `listCommands`. Routing is **data-driven** from `src/manifest.json`; don't add per-command branches.
- `src/manifest.json`: generated snapshot of `hey commands --json` plus usage/examples parsed from `hey <command> --help`. **Never hand-edit it**; run `npm run refresh-manifest`.
- `src/manifest.json`: generated snapshot of `hey commands --json` plus usage/examples parsed from `hey <command> --help`. **Never hand-edit it**; run `npm run refresh-manifest` (installed `hey`) or `npm run manifest:main` (built from basecamp/hey-cli main; `npm run check-drift` compares without writing).
- `src/args.js`: flag-aware scanning (which flags take a value), global and hey-axi flags, `stripAxiFlags`, `validateFlags`, raw output selectors.
- `src/policy.js`: run modes (interactive / stream / raw / json), the send gate, the secret gate, person-only commands (refused unless `--interactive`), content requirements, no-op detection (`noopFor`). New special cases go here.
- `src/errors.js`: `translateFailure()` maps a failed run to `{ok:false,error,kind,code,hint,meta,help}`, an exit code (0/1/2) and stderr warnings.
- `scripts/refresh-manifest.js`: regenerates the manifest.
- `scripts/refresh-manifest.js`: regenerates the manifest from the installed `hey`.
- `scripts/check-drift.js` / `scripts/manifest-lib.js`: build HEY from basecamp/hey-cli (main by default), compare its surface with the manifest (CI: `.github/workflows/manifest-drift.yml`), `--write` to update.
- `src/send.js`: send outcomes (`sent: true`, Undo Send `held`, `not_delivered`) and the recipient pre-check.
- `skills/hey-axi/`: the shareable agent skill. `SKILL.md` is hand-written except its `generated:home` block. That block and `references/commands.md` are generated by `scripts/gen-skill.js` (`npm run skill:gen`); `npm run skill:check` runs in CI. `test/skill.test.js` checks the spec rules, that every example command resolves, and that the generated parts are current.
- `test/helpers.js`: `makeFakeHey` / `makeFakeHeyScript` (fake binaries that log their arguments) and `runAxi`.

## Rules
1. **Never run the real HEY CLI against a mailbox in tests, and never send email.** Every test uses the fake `hey` through `HEY_BIN`. The only real-`hey` calls anywhere are `hey version`, `hey commands` and `hey <command> --help` in `refresh-manifest`. Hook tests use a temporary `HOME`/cwd, never your real agent config.
1. **Never run the real HEY CLI against a mailbox in tests, and never send email.** Every test uses the fake `hey` through `HEY_BIN`. The only real-`hey` calls anywhere are `hey version`, `hey commands` and `hey <command> --help` in `refresh-manifest` and `check-drift`. Hook tests use a temporary `HOME`/cwd, never your real agent config.
2. Commands that deliver email must never send without `--allow-send`/`HEY_AXI_ALLOW_SEND=1`. Without it, `compose`/`reply` are auto-staged with `--draft` (and marked `sent: false`, `saved_as: draft`), and `forward`/`draft send`/`bulk-reply send` are refused (`src/policy.js`). Don't loosen this without the owner's sign-off.
3. Forward argv to HEY **in its original order**. Strip only hey-axi's own flags (`--allow-send`, `--allow-secret`, `--interactive`, `--fields`, `--full`) and `--json`/`--quiet` (hey-axi adds `--json` itself and implements `--quiet`).
4. Output: TOON on stdout for both success and errors (`ok: false`). Default output is shaped (`src/shape.js`), and `--full` returns HEY's envelope untouched. Raw and stream modes relay HEY's stdout but report failures as structured errors. Never pass raw HEY stderr through. Suggestions name `hey-axi`, not `hey`, and HEY's breadcrumbs must stay.
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@

All notable changes to hey-axi. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow [semver](https://semver.org/).

## 0.3.2 (2026-10-05)

Follows HEY CLI's unreleased `main` branch, adds an upstream-drift check, and closes three gaps from the catalog review of 0.3.1. The rules that were already in place stay as they were: nothing is sent without `--allow-send`, tests use a fake HEY, and HEY's next-command hints are still shown.

**HEY version covered:** v1.7.0 plus the commands on basecamp/hey-cli `main` as of 2026-10-03 (commit `8bf9310`). That branch is not released yet. `src/manifest.json` was regenerated by building HEY from that commit and running `hey commands --json` / `hey <command> --help` (its `hey_version` reads `1.7.0+main.8bf9310`). With HEY v1.7.0 installed, everything from 0.3.1 works as before, and the main-only commands below fail with `kind: hey_outdated` until HEY is upgraded.

### Added
- **HEY main commands:** `contact deliver <contact-id> --to imbox|feed|papertrail|screened-out` (screens a contact in and routes their mail; `--to` is required and checked before HEY runs), and `event delete --occurrence <date> --apply-to current|future` for recurring events (the two flags go together; `--apply-to` is checked for `event edit` too). Per-command help shows required flags, allowed values and flags that go together. 155 runnable command paths (was 154); help text for 33 commands follows HEY main's wording.
- **Send outcomes from HEY main:** with `--allow-send`, a confirmed send is marked `sent: true` and keeps the delivered message's `id`/`topic_id` when HEY reports them. A message held for Undo Send (`delayed: true`) gets a `held` line saying it hasn't gone out yet and the thread won't show it until it does. A send HEY refused and kept as a draft (`code: not_delivered`, usually the sending limit) is an error with `kind: not_delivered`, `sent: false`, the draft id, and a note not to repeat the send. HEY v1.7.0 output is still read as before (v1.7.0 reports a refused send as sent, so that case can't be detected there).
- **Recipient check:** `compose`, `reply`, `forward` and `draft edit` refuse (exit 2, `sent: false`) recipients HEY would drop without saying so (no domain or top-level domain), before HEY runs. HEY main's own "not a valid email address" refusal is reported the same way.
- **`kind: hey_outdated`:** when the installed HEY doesn't know a command or flag that's in the catalog, the error says so and suggests `hey-axi version` / `hey-axi upgrade` instead of a generic usage error.
- **Upstream drift check:** `npm run check-drift` builds HEY from basecamp/hey-cli `main` (or `--ref <tag>`, `--source <dir>`, `--hey <bin>`) and exits 1 with a readable diff when its commands or flags differ from `src/manifest.json`; help-text changes are listed (they fail only with `--strict`). `npm run manifest:main` rewrites the manifest from that build. The `upstream drift` GitHub Action runs it daily and on changes to the manifest or scripts, and warns when HEY publishes a newer release.

### Changed
- **axi-sdk-js 0.1.13** (was 0.1.12): its generated OpenCode plugin loads on OpenCode 2.
- **OpenCode session-end plugin:** hey-axi's own `axi-hey-axi-session-end.js` plugin used the OpenCode 1 named export only. It now also has the OpenCode 2 default export (`id`, `setup`, `server`). Re-run `hey-axi setup hooks` (or `--project`) to update installed plugins; it replaces the old ones in place.
- `event delete --occurrence` failing with not found is no longer treated as "already deleted" (a missing occurrence isn't proof the event is gone).

### Fixed
- **Home view "nothing" (catalog review):** an empty Imbox page is only called "nothing" when HEY says there are no more pages or a total of 0. Otherwise it says `0 threads on this page …; HEY reported no total and no further pages` and suggests `--all` to check every page.
- **Generic error fallback (catalog review):** errors without a specific fix no longer say "run --help". They show the command's usage line and an example (or `hey-axi doctor` when there's no command), and HEY usage errors include them inline.
- **"Already done" for single ids (catalog review):** `seen 2` is only a no-op when HEY's message is about id 2 (or every requested id); "1 already seen" for a single id, or a message naming a different id, stays an error. A bare count is only accepted as "all N" matching the number of ids.
- **Error cleanup:** a sentence naming a networking library (`superagent`, `undici`, `axios`, `node-fetch`, …) is dropped from the error and its `meta`, and HEY's `code` is kept only when it's a HEY-style code (snake_case, not a library name).

## 0.3.1 (2026-10-03)

Closes the remaining gaps from the independent catalog review of 0.3.0. The rules that were already in place stay as they were: nothing is sent without `--allow-send`, tests use a fake HEY, HEY's next-command hints are still shown, `--interactive` stays opt-in, and the home view stays account-wide unless `setup scope` narrows it.
Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

An agent-friendly wrapper for [HEY CLI](https://github.com/basecamp/hey-cli) (`hey`), Basecamp's command line for HEY email, calendars, todos, habits, time tracking and journals.

hey-axi runs `hey`, asks for its JSON response envelope, and prints it as [TOON](https://github.com/toon-format/toon). It follows the [AXI](https://axi.md) principles: a live home view, minimal default fields, truncated long text, definitive empty states, and unknown flags rejected up front. On our synthetic benchmark that's **~89% fewer tokens than `hey --json`** ([docs/benchmarks.md](docs/benchmarks.md)). It also adds a few safety rails. It covers **every command in HEY CLI v1.7.0** (154 runnable command paths, including the `login`/`logout` aliases, plus the `box <id>`-style shortcuts) by routing from a snapshot of HEY's own `hey commands --json` catalog. See [CHANGELOG.md](CHANGELOG.md) for what changed in each release.
hey-axi runs `hey`, asks for its JSON response envelope, and prints it as [TOON](https://github.com/toon-format/toon). It follows the [AXI](https://axi.md) principles: a live home view, minimal default fields, truncated long text, definitive empty states, and unknown flags rejected up front. On our synthetic benchmark that's **~89% fewer tokens than `hey --json`** ([docs/benchmarks.md](docs/benchmarks.md)). It also adds a few safety rails. It covers **every command in HEY CLI v1.7.0 plus the commands on HEY's unreleased `main` branch as of 2026-10-03** (commit [`8bf9310`](https://github.com/basecamp/hey-cli/commit/8bf9310c0b1df7448e39ed1f81826ae795bd8b5c)): 155 runnable command paths, including the `login`/`logout` aliases, plus the `box <id>`-style shortcuts. It routes from a snapshot of HEY's own `hey commands --json` catalog. The main-only additions (`contact deliver`, `event delete --occurrence/--apply-to`) need a HEY built from main, or the next HEY release; with HEY v1.7.0 they fail with `kind: hey_outdated`. See [CHANGELOG.md](CHANGELOG.md) for what changed in each release.

## Install

Expand Down Expand Up @@ -72,6 +72,9 @@ hey-axi move --help # usage, flags, examples, notes (no H
- `compose` and `reply` are **saved as drafts**. hey-axi adds `--draft`, and the output starts with `sent: false`, `saved_as: draft` and an `axi_notice` (also printed to stderr).
- `forward`, `draft send` and `bulk-reply send` have no draft mode in HEY, so they're refused (exit 2) with the safe alternative (`reply … --to`, `draft show`, `bulk-reply preview`).
- If you pass `--draft` or `--dry-run` yourself, hey-axi adds and marks nothing.
- With `--allow-send`, a send HEY confirms is marked `sent: true` (and names the delivered message's `id`/`topic_id` when HEY reports them, as HEY main does). If HEY holds it for Undo Send (`delayed: true`), a `held` line says it hasn't gone out yet and the thread won't show it until it does.
- If HEY refuses to send and keeps the message as a draft (HEY main's `not_delivered`, usually the sending limit), the result is an error with `kind: not_delivered`, `sent: false` and the draft id, and says not to repeat the send. HEY v1.7.0 reports such a send as sent, so it can't be detected there.
- Recipients HEY would silently drop (no domain or top-level domain, like `bob` or `bob@example`) are refused (exit 2, `sent: false`) before HEY runs, for `compose`, `reply`, `forward` and `draft edit`. HEY main's own "not a valid email address" refusal is reported the same way.
- **Credentials stay out of agent context.** `auth token` needs `--allow-secret` or `HEY_AXI_ALLOW_SECRETS=1`.
- **Content-first.** `compose`, `reply`, `draft edit`, `journal write`, `contact note set` and `bulk-reply send` must be given their content (`--message`, `--message -` for stdin, or a positional) and are refused (exit 2) otherwise, so HEY never opens an editor.
- hey-axi's own flags (`--allow-send`, `--allow-secret`, `--interactive`, `--fields`, `--full`) are never forwarded to HEY. Switches take no value: `--draft=false` or `--allow-send=false` is refused (exit 2) rather than guessed at.
Expand All @@ -83,7 +86,7 @@ Errors are printed as structured TOON on stdout: `ok: false`, `error`, a `kind`,
| Exit | Meaning |
|---|---|
| 0 | success (including no-op mutations and empty results) |
| 1 | the command failed; `kind` says why: `not_found`, `auth`, `forbidden`, `rate_limited`, `network`, `api_error`, `ambiguous`, `hey_missing`, `command_error` |
| 1 | the command failed; `kind` says why: `not_found`, `auth`, `forbidden`, `rate_limited`, `network`, `api_error`, `ambiguous`, `hey_missing`, `hey_outdated` (the installed HEY is older than the command needs), `not_delivered` (HEY kept a send as a draft), `command_error` |
| 2 | usage error (`kind: usage`): unknown command/flag/field, bad flag value, missing or extra arguments, missing required flag or content, a switch given a value (`--draft=false`), blocked send, a person-only command without `--interactive`, or a usage error reported by HEY |

## Agent integrations: session hook or skill
Expand Down Expand Up @@ -120,13 +123,16 @@ See [docs/listing.md](docs/listing.md) for how hey-axi gets listed on skills.sh

## Keeping up with HEY releases

`src/manifest.json` is a snapshot of `hey commands --json`. To regenerate it from whichever `hey` you have installed:
`src/manifest.json` is a snapshot of `hey commands --json` plus usage parsed from `hey <command> --help`. The current snapshot was built from HEY's unreleased `main` branch (commit `8bf9310`, 2026-10-03), so its `hey_version` reads `1.7.0+main.8bf9310`. To regenerate it:

```bash
npm run refresh-manifest # runs only `hey version` and `hey commands`
npm run refresh-manifest # from whichever `hey` you have installed (runs only `hey version`, `hey commands`, `hey <command> --help`)
npm run manifest:main # clone basecamp/hey-cli main, build it (needs Go and git), and rewrite the manifest from it
npm run check-drift # same build, but only compare: exits 1 with a readable diff when commands or flags differ
node scripts/check-drift.js --ref v1.8.0 --write # a tagged release instead of main
```

hey-axi routes only from the snapshot, so an unknown command is rejected immediately (exit 2, with suggestions) without running HEY. New HEY commands become available after a refresh and a hey-axi release. A weekly GitHub Action (`manifest-drift`) fails when the snapshot falls behind the latest HEY release.
hey-axi routes only from the snapshot, so an unknown command is rejected immediately (exit 2, with suggestions) without running HEY. New HEY commands become available after a refresh and a hey-axi release. The `upstream drift` GitHub Action builds HEY from basecamp/hey-cli `main` daily (and on changes to the manifest or scripts) and fails when its commands or flags differ from the snapshot; help-text-only changes are listed in the job summary without failing. It also warns when HEY publishes a release newer than the one the snapshot builds on.

## Development

Expand Down
Loading
Loading