@afk-watch/cli
v0.4.0
Published
Installer for afk.watch integrations - wires Claude Code hooks and optional hosted pickup.
Readme
@afk-watch/cli
Installer for the afk.watch agent integrations. It wires the Claude Code
lifecycle hooks (scripts/afk-hook.mjs, issue #36), installs the /afk-watch
skill, and can generate the opt-in hosted pickup workflow, so runs show up in the
app without hand-editing settings.json. Every path has a symmetric uninstall. The idempotent
settings.json merge at its core (src/settings.ts) is runtime-agnostic and will
back future runtimes too (issue #35).
Use
npx @afk-watch/cli@latest init --key key_live_xxx --test- Stores the key in
~/.config/afk.watch/config.json. - Installs the hook handler to
~/.config/afk.watch/afk-hook.mjsand merges the four afk hooks into your Claude Codesettings.json- idempotently (re-running never duplicates), preserving your existing hooks and settings. - Installs the afk-watch skill to
~/.claude/skills/afk-watch/, which is what gives you/afk-watch [issue].--projectputs it in./.claude/skills/instead, matching where the settings went. A re-run replaces it, so a CLI upgrade refreshes the skill rather than leaving a stale copy. - Checks issue resolution in the current repo, by importing the handler it just
installed, and prints what a session here would report against - or why it wouldn't
be reported at all (you're on
main, no GitHub remote, …).--testproves the key and push delivery; this proves the path a real session takes. See ADR 0008. --testfires a confirmation run so your phone lights up right away.
Once installed, hand the agent an issue directly:
/afk-watch #33 # claim issue 33, work it, report the run
/afk-watch # find open `ready-for-agent` issues and claim the oldestThe skill claims an issue by swapping ready-for-agent for in-progress before
starting work, so a second pass can't pick up the same one. That is an advisory lock,
not a real one - two runs started in the same moment can still race.
afk uninstall # remove the hooks, handler, skill, and key
afk uninstall --keep-key # ...but keep the stored key
afk uninstall --pickup # ...and remove the generated hosted workflowOptional automatic local pickup
init installs the skill but cannot create a Claude Desktop scheduled task. Open
the repository you want watched in Claude Desktop and say exactly:
Create a scheduled task named afk.watch pickup that runs /afk-watch every 5 minutes in this folder using auto permission mode.
Use auto permission mode: it is the safest unattended mode that can edit files,
run commands, commit, and open PRs, while still blocking actions it judges risky.
Do not use bypassPermissions. Pickup works only while Claude Desktop is open and
the machine is awake; closing the lid stops it. /loop is session-scoped and is not
the unattended mechanism.
Optional automatic hosted pickup
If pickup must continue while your machine is off, opt in per repository:
npx @afk-watch/cli@latest init --pickupRun it from inside the repository. This writes .github/workflows/afk-pickup.yml at
the repository root - the only place GitHub reads workflows from - without
touching sibling workflows, and errors out rather than writing anything when there is
no repository.
The generated file carries its own afk-template-version and an
afk-template-sha256 of the rest of its body, which is how the CLI tells its own
output from yours. An untouched copy - of any version - is a no-op to re-install, is
upgraded in place when a newer template ships, and is removed by
afk uninstall --pickup. Edit it and the digest stops matching: from then on neither
command writes or deletes that file. init tells you a newer template exists (and
flags a security-relevant one) so you can merge it yourself; uninstall reminds you
the file keeps firing on ready-for-agent until you delete it. Run from outside a
repository, uninstall skips the workflow half with an explanation and still removes
the hooks, handler, skill, and stored key.
Hosted pickup runs on ubuntu-latest and needs two repository Actions secrets:
AFK_KEY and ANTHROPIC_API_KEY. A Claude Pro/Max subscription does not
authenticate hosted CI. Anthropic usage is billed per token in addition to GitHub
Actions minutes. Commit and merge the workflow to the repository's default branch;
GitHub does not deliver issues events to a workflow that exists only on a feature
branch. It also needs Settings > Actions > General > "Allow GitHub Actions to create
and approve pull requests", because the runner is deleted when the job ends: the
workflow has the agent commit to afk-pickup/issue-<n>-<run id>-<attempt> (a fresh
branch per run, so it never collides with what an earlier pickup pushed), push, and
open a pull request, and verifies that before reporting done. Work left
uncommitted, unpushed, or without an open pull request carrying those exact commits
fails the run instead of being reported as finished - though a run that pushed its
work and then left a stray file behind still names the pull request holding the
commits, rather than claiming everything was lost.
That pull request runs no CI. GitHub does not fire pull_request or push
workflow runs for anything created with the built-in GITHUB_TOKEN, and this path
keeps to two secrets - no PAT, no GitHub App - so there is nothing else to open it
with. It arrives with no checks on it. The agent has to run the project's own
validation inside the job and report which commands it ran, but that is its word,
not a check run: trigger the repository's checks yourself and confirm they pass
before merging. Every report repeats this on the line that names the pull request.
Because each run has its own branch, a second pickup of an issue whose pull request
is still open would produce two pull requests for the same work. So it refuses: the
run starts nothing, spends no tokens, reports the open pull request's URL, and swaps
ready-for-agent for in-progress. Nothing is ever closed, deleted, or
force-pushed on your behalf - you decide whether that pull request merges or
closes, and re-applying ready-for-agent afterwards picks the issue up again.
Hosted pickup is opt-in because of that billing and secret setup; plain init keeps
the local/manual paths only. It requires CLI 0.4.0 or newer.
Issue text is agent instructions. The agent is handed the issue text and runs
unattended with a token that can write contents, issues, and pull requests. Event
fields never reach a shell command, but nothing stops issue prose from steering an
agent asked to follow it. So the workflow picks up an issue only when GitHub reports
its author's author_association as OWNER, MEMBER, or COLLABORATOR.
Only the title and body from that gated event reach the agent, and it is told not to fetch or obey issue comments, other issues, or reviews. Comments are not trusted pickup input - anyone can comment on an admitted issue whatever their association, so a thread-reading agent would take instructions from exactly the people the gate excludes. Put what you want done in the issue body before you tap.
That is the relationship GitHub declares in the event payload, not a check that
the author has write access here: MEMBER means a member of the owning organization,
which in an org whose base permission is Read (the default) includes people with no
write access to this repository, and COLLABORATOR covers read-only invitees. It
narrows pickup to people already inside your org or repo; it does not verify they
could push the change themselves.
Everything else is excluded - CONTRIBUTOR, FIRST_TIME_CONTRIBUTOR, and NONE,
and NONE is also what GitHub reports for issues filed by a GitHub App or bot, so
automated backlog items are never picked up either. In every excluded case the label
stays on and you work it by hand (locally with /afk-watch, or by re-filing it in
your own words). Residual risk stands for everyone the gate admits: they can steer
the agent through issue text.
If setup fails before the afk.watch run is created (a missing secret, an unreachable
API), the ready-for-agent label is left in place, so a tap from the phone is never
silently swallowed.
Options
| | |
|---|---|
| --key <key> | your API key (or set AFK_KEY). Prompted on a terminal if omitted. |
| --global | write to ~/.claude/settings.json (default; applies everywhere). |
| --project | write to ./.claude/settings.json (this repo only). |
| --test | fire a post-install confirmation run. |
| --pickup | install or upgrade the opt-in hosted Actions workflow in this repository; with uninstall, remove it. Either way, a file you edited is left alone. |
| --api <url> | API base (default https://api.afk.watch/v1). |
| --agent <name> | agent label shown on runs (default claude-code). |
| -y, --yes | don't prompt; use flags/env and defaults. |
The curl form (served from the API origin) just runs this CLI:
curl -fsSL https://afk.watch/install.sh | AFK_KEY=key_live_xxx shDevelop
npm run build # tsc → dist/, bundle the hook, skill, hosted workflow, and shebang
npm test # vitest (pure merge, config, init/uninstall, arg parsing,
# the hook handler end to end - src/hook.test.ts, and
# what the tarball ships - src/packaging.test.ts, which
# packs the package, so it is the slow one)
npm run typecheckEditing templates/afk-pickup.yml invalidates the # afk-template-sha256: line in
its own header, which is what marks a generated file as ours. npm test fails with
the replacement line printed in full - paste it over the old one. Bump
# afk-template-version: in the same edit whenever the change ships to users, so an
installed copy can say which version it came from.
Unlike the rest of the workspace (consumed as TS source by bundler-aware tools), this
package is executed by the end user's Node, so it builds to plain ESM with NodeNext
resolution and explicit .js import extensions. See
docs/adr/0004-cli-installer-distribution.md.
