@anyslate/cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +229 -19
- package/package.json +6 -6
- package/src/commands/checkpoint.mjs +53 -13
- package/src/commands/doctor.mjs +523 -0
- package/src/commands/hook.mjs +70 -19
- package/src/commands/login.mjs +106 -14
- package/src/commands/upload-artifact.mjs +127 -21
- package/src/config.mjs +110 -12
- package/src/hooks.mjs +170 -8
- package/src/index.mjs +39 -5
- package/src/io.mjs +30 -0
- package/src/mcp-client.mjs +291 -45
- package/src/runlog.mjs +196 -0
- package/src/stdin.mjs +85 -15
- package/src/verify.mjs +262 -0
- package/src/version.mjs +21 -0
- package/templates/git/post-commit +71 -0
package/README.md
CHANGED
|
@@ -1,74 +1,284 @@
|
|
|
1
1
|
# @anyslate/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The bridge that lets your AI tools quietly tell AnySlate what you've been working on - without you having to remember to checkpoint anything.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
A small command-line tool that runs on your machine. Your AI tools (Claude Code, git, CI) call it on your behalf as you work. It records what's happening into your AnySlate **Activity feed** - file edits, shell commands, commits - automatically and continuously.
|
|
8
|
+
|
|
9
|
+
Think of it like a Fitbit, but for your AI work. You don't tell a Fitbit "I just took 47 steps." You wear it, walk around, the count updates. Same shape here. You install this once, paste a small config snippet into your AI tool's settings, and from that point on, capture happens in the background while you work.
|
|
10
|
+
|
|
11
|
+
**You almost never run a CLI command yourself.** The tool exists to be invisible plumbing that other tools call. The setup below takes 5 minutes and ends with `anyslate doctor` telling you it works.
|
|
12
|
+
|
|
13
|
+
## Where it fits in your workflow
|
|
14
|
+
|
|
15
|
+
Most days you'll never type `anyslate` yourself. Here's what actually happens:
|
|
16
|
+
|
|
17
|
+
| Trigger | Who calls the CLI | What lands |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| You open a Claude Code session | Claude Code's `SessionStart` hook | Session-start activity; the AnySlate session is created on first sight |
|
|
20
|
+
| Claude edits `routes/auth.ts` | Claude Code's `PostToolUse` hook | "Edited routes/auth.ts" in the activity ledger (**awaits your approval** - see below) |
|
|
21
|
+
| Claude runs `npm test` | Claude Code's `PostToolUse` hook | Command + output in the activity ledger |
|
|
22
|
+
| You `git commit` | Git's `post-commit` hook | Commit metadata + diff stats captured |
|
|
23
|
+
| Teammate merges your PR | GitHub webhook (no CLI needed) | PR-merged event captured |
|
|
24
|
+
| You close the laptop | Claude Code's `Stop` hook | Session-end marker captured |
|
|
25
|
+
| **You manually want to checkpoint a decision** | **You typing `anyslate checkpoint ...`** | **Captured immediately** |
|
|
26
|
+
| **You want to upload a file as an artifact** | **You typing `anyslate upload-artifact ...`** | **File stored, returns `cloud://artifact/<id>`** |
|
|
27
|
+
|
|
28
|
+
The first six rows are automatic - that's what the lifecycle hooks do. The bottom two are the rare moments where you'd actually type something yourself.
|
|
29
|
+
|
|
30
|
+
### What hook captures actually write - read this before you form expectations
|
|
31
|
+
|
|
32
|
+
Hook captures land in a dedicated **`## Activity Ledger`** section of your memory page: which files were touched, which commands ran, what exit status they returned. That is the whole contract. The ledger is capped (25 files / 15 commands per entry) and is built deterministically - no LLM runs in the hook path, so capture never bills against your AI quota.
|
|
33
|
+
|
|
34
|
+
Hook captures **do not** write to the **Decisions** or **Open Tasks** sections. Those stay human- and LLM-authored, and are populated by `anyslate checkpoint`, by the in-host MCP flow (`/anyslate-new`, `/anyslate-continue`), or by you editing the page. A hook firing on `Bash` has no decision in it to extract, and the product no longer pretends otherwise.
|
|
35
|
+
|
|
36
|
+
If you want a decision recorded, type it:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
anyslate checkpoint --note "Decided: Redis for session cache, not memcached"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Approval: file edits wait, shell commands don't
|
|
43
|
+
|
|
44
|
+
The classifier treats `Edit` / `Write` / `MultiEdit` as **high-risk** - those rows stay pending in **AI Memory → Activity** until you approve them. `Bash` and session lifecycle events classify **low-risk** and auto-promote on their own.
|
|
45
|
+
|
|
46
|
+
This is deliberate, and it means a Claude Code session that edits a lot of files produces a queue you have to work through. Budget a few seconds a day for it, or approve in bulk.
|
|
47
|
+
|
|
48
|
+
## What you get back
|
|
49
|
+
|
|
50
|
+
After the setup is in place:
|
|
51
|
+
|
|
52
|
+
- **You stop forgetting to checkpoint.** Capture happens whether you remember or not.
|
|
53
|
+
- **AnySlate sees what actually happened, not just what the AI claimed.** Git knows what files really changed. The hooks know what commands really ran.
|
|
54
|
+
- **Your work follows you across tools.** Claude Code at home, Cursor at work, terminal in between - all feed the same memory. (Cursor and Windsurf feed it through the in-host MCP server, not this CLI - see the FAQ.)
|
|
6
55
|
|
|
7
56
|
## Install
|
|
8
57
|
|
|
9
58
|
```bash
|
|
10
59
|
npm i -g @anyslate/cli
|
|
11
|
-
# or
|
|
60
|
+
# or run on demand without installing:
|
|
12
61
|
npx @anyslate/cli --help
|
|
13
62
|
```
|
|
14
63
|
|
|
15
64
|
Requires Node ≥ 20.
|
|
16
65
|
|
|
66
|
+
> **macOS users hitting `EACCES` on global install:** don't use `sudo`. Configure a user-writable npm prefix instead:
|
|
67
|
+
>
|
|
68
|
+
> ```bash
|
|
69
|
+
> mkdir -p ~/.npm-global
|
|
70
|
+
> npm config set prefix '~/.npm-global'
|
|
71
|
+
> echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.zshrc
|
|
72
|
+
> source ~/.zshrc
|
|
73
|
+
> npm install -g @anyslate/cli
|
|
74
|
+
> ```
|
|
75
|
+
|
|
17
76
|
## Authenticate
|
|
18
77
|
|
|
19
|
-
Mint an MCP token in the desktop app at **
|
|
78
|
+
Mint an MCP token in the AnySlate app: **Avatar (top-right) → API Tokens → Tokens tab → Create Token**. This is the same dialog on the desktop app and on the cloud app at `https://cloud.anyslate.io`; the dialog is titled *MCP Tokens*. MCP token minting is **available on every plan** - Free, Pro and Unlimited - with no token quota.
|
|
79
|
+
|
|
80
|
+
Copy the token (it starts with `as_mcp_`; you only see it once), then:
|
|
20
81
|
|
|
21
82
|
```bash
|
|
22
83
|
anyslate login --token as_mcp_your_token_here
|
|
23
84
|
```
|
|
24
85
|
|
|
25
|
-
|
|
86
|
+
`login` verifies the token against the server before writing anything. If the host is unreachable, the URL isn't an AnySlate MCP service, or the token is unrecognised/revoked/expired, it prints the reason, **writes nothing, and exits non-zero**. See [`anyslate login`](#anyslate-login) for the `--force` / `--no-verify` escapes.
|
|
87
|
+
|
|
88
|
+
Then confirm the whole install:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
anyslate doctor
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Environment variables
|
|
26
95
|
|
|
27
96
|
```bash
|
|
28
97
|
export ANYSLATE_MCP_TOKEN=as_mcp_your_token_here
|
|
29
|
-
export ANYSLATE_API_URL=https://mcp.anyslate.io # optional
|
|
30
|
-
export ANYSLATE_HANDLE=
|
|
98
|
+
export ANYSLATE_API_URL=https://mcp.anyslate.io # optional; service ROOT, no /mcp
|
|
99
|
+
export ANYSLATE_HANDLE=mh_xxxxxxxx # optional, scope to one capability handle
|
|
100
|
+
export ANYSLATE_DISABLE=1 # optional, disables capture for this shell
|
|
101
|
+
export ANYSLATE_HOME=/path/to/dir # optional, overrides ~/.anyslate
|
|
102
|
+
export ANYSLATE_STDIN_TIMEOUT_MS=10000 # optional, stdin idle timeout (0 disables)
|
|
31
103
|
```
|
|
32
104
|
|
|
33
105
|
Env vars override `~/.anyslate/cli.json`.
|
|
34
106
|
|
|
107
|
+
`ANYSLATE_DISABLE=1` stops `hook` / `checkpoint` / `upload-artifact` from making any network call (they exit 0). `doctor` and `login` still run, so you can diagnose and set up while capture is off. Any value counts as "on" except empty, `0`, `false`, `no`, `off`.
|
|
108
|
+
|
|
109
|
+
Capability handles are `mh_` + a 32-character id (e.g. `mh_V1StGXR8Z5jdHi6BmyT0aQx3nKpL7cWe`), minted alongside a scoped token in the **Handles** tab of the same dialog. **A handle id that isn't yours or doesn't exist is rejected server-side** - since 0.2.0 the CLI reports that as a failure instead of exiting 0 silently.
|
|
110
|
+
|
|
111
|
+
**If your token was minted with a memory scope, it already carries that scope server-side and `ANYSLATE_HANDLE` / `--handle` is ignored.** Scoped tokens cannot be widened per-call by design. To capture workspace-wide, mint an unscoped token. `anyslate doctor` reports which case you're in.
|
|
112
|
+
|
|
113
|
+
### The two URL conventions - the one thing people get wrong
|
|
114
|
+
|
|
115
|
+
AnySlate ships two clients and they want **different** URLs. This trips up nearly everyone:
|
|
116
|
+
|
|
117
|
+
| Client | Variable / flag | Wants | Example |
|
|
118
|
+
|---|---|---|---|
|
|
119
|
+
| **`@anyslate/mcp` bridge** (Claude Desktop, Cursor, Windsurf…) | `ANYSLATE_MCP_URL` | the **MCP endpoint** - *with* `/mcp` | `https://mcp.anyslate.io/mcp` |
|
|
120
|
+
| **`@anyslate/cli`** (this package) | `--api-url` / `ANYSLATE_API_URL` | the **service root** - *without* `/mcp` | `https://mcp.anyslate.io` |
|
|
121
|
+
|
|
122
|
+
The CLI appends `/mcp` itself. Pasting the bridge's URL into `--api-url` used to produce `…/mcp/mcp`, a permanent `404 {"error":"Not found"}` that masks every other diagnosis - because the 404 fires before authentication, so a wrong URL makes a valid token look invalid.
|
|
123
|
+
|
|
124
|
+
**Since 0.2.0 the CLI accepts either form and normalizes**, stripping any trailing `/mcp` segments (`/mcp`, `/mcp/mcp`, `/MCP/`, trailing slashes). Normalization happens on every read, so a `cli.json` written by an older version self-heals without re-running `login`. When it rewrites a value it tells you:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
anyslate: apiUrl "https://mcp.anyslate.io/mcp" ends in /mcp — the CLI wants the service ROOT and appends /mcp itself.
|
|
128
|
+
anyslate: using "https://mcp.anyslate.io". Run `anyslate login --api-url https://mcp.anyslate.io` to persist.
|
|
129
|
+
```
|
|
130
|
+
|
|
35
131
|
## Subcommands
|
|
36
132
|
|
|
37
133
|
```
|
|
38
|
-
anyslate hook <session-start|post-tool-use|stop> [--strict]
|
|
39
|
-
anyslate checkpoint --note "..." [--kind milestone] [--session <id>]
|
|
40
|
-
anyslate upload-artifact --session <id> --kind <kind> [--file <path>]
|
|
41
|
-
anyslate login --token <BEARER> [--handle <ID>] [--api-url <URL>]
|
|
134
|
+
anyslate hook <session-start|post-tool-use|stop> [--strict] [--session <id>] [--note <text>] [--host <hint>]
|
|
135
|
+
anyslate checkpoint --note "..." [--kind milestone] [--session <id>] [--host <hint>] [--source <source>]
|
|
136
|
+
anyslate upload-artifact --session <id> --kind <kind> [--file <path>] [--language <lang>] [--path-hint <path>]
|
|
137
|
+
anyslate login --token <BEARER> [--handle <ID>] [--api-url <URL>] [--force] [--no-verify]
|
|
138
|
+
anyslate doctor [--deep]
|
|
42
139
|
anyslate version | help
|
|
43
140
|
```
|
|
44
141
|
|
|
142
|
+
### `anyslate doctor`
|
|
143
|
+
|
|
144
|
+
The one command whose job is to fail loudly. Run it after setup and any time capture seems dead. It exits non-zero if any check FAILs.
|
|
145
|
+
|
|
146
|
+
It checks, in order: which config layer won (env var vs `cli.json`) per key; that a token is present; that the token *looks* like a token; that the URL parses and has the right shape (**before** any token verdict, because a wrong URL 404s ahead of auth and would otherwise be misread as a bad token); that the host is reachable and healthy; that the token is valid (and, if not, whether it is unrecognised, revoked or expired); that the token carries `memory:write`; the effective handle scope; that `anyslate` is on the PATH a hook subprocess would get; and whether Claude Code's hooks are actually wired in `~/.claude/settings.json`.
|
|
147
|
+
|
|
148
|
+
Reading the output:
|
|
149
|
+
|
|
150
|
+
- **PASS** - checked and observed good.
|
|
151
|
+
- **WARN** - works, but something is narrower or more fragile than you probably intend (e.g. a project-scoped token, or a config key coming from an env var you forgot you exported).
|
|
152
|
+
- **FAIL** - this is why capture isn't working. Each FAIL prints the specific remedy.
|
|
153
|
+
|
|
154
|
+
`doctor` is **not** side-effect-free: verifying the token updates its `last_used_at`, consumes rate-limit budget, and writes an audit-log row. `--deep` additionally submits a real `doctor_probe` activity, which is visible in your Activity feed - that's why it's opt-in.
|
|
155
|
+
|
|
45
156
|
### `anyslate hook ...`
|
|
46
157
|
|
|
47
|
-
Reads JSON event payload from stdin (Claude Code lifecycle event format). Fails open by default
|
|
158
|
+
Reads a JSON event payload from stdin (Claude Code lifecycle event format). **Fails open by default** - a network blip, an invalid token or a server-side rejection logs to stderr and exits 0, so your Claude Code session is never broken. Pass `--strict` to exit 1 on failure instead (useful in CI smoke tests).
|
|
159
|
+
|
|
160
|
+
Because fail-open means you won't see stderr in normal use, every run is recorded to `~/.anyslate/cli-last-run.json` and a capped ring log at `~/.anyslate/cli-runs.ndjson` (both mode 0600, token redacted). After repeated consecutive failures the `SessionStart` hook surfaces a message in Claude Code itself telling you to run `anyslate doctor`. One-off failures stay quiet.
|
|
161
|
+
|
|
162
|
+
Kind mapping: `hook session-start` → `topic_shift`; `hook stop` → `conversation_end`; `hook post-tool-use` maps `Edit`/`Write`/`MultiEdit`/`Create` and `Bash`/`Shell`/`Run`/`GitCommit` → `task_completed`, everything else → `topic_shift`.
|
|
48
163
|
|
|
49
|
-
|
|
164
|
+
Hooks never emit `artifact_produced`. That kind is a contract — "I stored an artifact, here is its id in `artifact_refs`" — and the server rejects it without one. A hook observes a file touch; it stores nothing. The touched path is carried in the `files_touched` ledger field instead, so edits auto-promote as activity entries rather than piling up an approval queue.
|
|
165
|
+
|
|
166
|
+
**Session identity is handled for you.** Claude Code's `session_id` is a UUID from its own namespace; AnySlate session ids are a different format entirely. The server resolves the incoming UUID against `conversation_id_external`, and **creates the AnySlate session on first sight** if there's no match, titling it from the host and working directory. You do **not** need to pass `--session` in the Claude Code hook config, and re-running under the same Claude Code session reuses the same AnySlate session. `--session` remains available for git hooks and CI, where you want to pin captures to one specific memory.
|
|
167
|
+
|
|
168
|
+
Fields read from the stdin payload: `session_id` (or `sessionId`), `tool_name` (or `toolName`), `tool_input` (or `toolInput`), `tool_response` (or `toolResponse`, JSON-stringified into `conversation_excerpt`), `transcript_path`, `client_checkpoint_id`. Anything else in the payload is ignored.
|
|
169
|
+
|
|
170
|
+
**The activity ledger.** From `tool_input` and `tool_response` the hook derives three structured fields — `files_touched`, `commands_run` and `exit_status` — and sends them as first-class payload fields. They record what a tool call *did*, and they land in the activity/ledger section of the memory page. They are deliberately **not** folded into `notes`: `notes` is the prose the server mines for Decisions and Open Tasks, and turning shell commands into decisions would manufacture entries you never made. Fields with nothing to record are omitted rather than sent empty. No LLM runs anywhere on this path.
|
|
50
171
|
|
|
51
172
|
### `anyslate checkpoint`
|
|
52
173
|
|
|
53
|
-
User-initiated, exits non-zero on failure. Defaults `--kind milestone`, `--source api`. Allowed kinds: `topic_shift | decision_committed | task_completed | task_added | artifact_produced | milestone | conversation_end`.
|
|
174
|
+
User-initiated, exits non-zero on failure - including on server-side rejections, not just network errors. Defaults `--kind milestone`, `--source api`. Allowed kinds: `topic_shift | decision_committed | task_completed | task_added | artifact_produced | milestone | conversation_end`.
|
|
175
|
+
|
|
176
|
+
`--host <hint>` sets the advisory `host_hint`; `--source <source>` overrides the activity source.
|
|
177
|
+
|
|
178
|
+
Low-risk items reach your memory page after a **30-second quiet period plus the next once-a-minute promoter run** - so 30-90 seconds, not instantly.
|
|
54
179
|
|
|
55
180
|
### `anyslate upload-artifact`
|
|
56
181
|
|
|
57
|
-
Reads file or stdin, calls the `upload_artifact` MCP tool.
|
|
182
|
+
Reads a file or stdin, calls the `upload_artifact` MCP tool. Prints `cloud://artifact/<id>` on stdout on success; on failure prints nothing to stdout, writes the reason to stderr and exits non-zero. Allowed kinds: `code_block | file_path | error_message | shell_command | config_snippet | url_reference | fenced_quote`. 5 MB content cap on both the `--file` and stdin paths.
|
|
183
|
+
|
|
184
|
+
Content is read as UTF-8. Binary files are not supported and are refused rather than silently mangled.
|
|
58
185
|
|
|
59
186
|
### `anyslate login`
|
|
60
187
|
|
|
61
|
-
|
|
188
|
+
Verifies, then writes `~/.anyslate/cli.json` with mode `0600`. Idempotent - preserves the apiUrl/handle if you only update the token (the preserved apiUrl is re-normalized and re-verified).
|
|
189
|
+
|
|
190
|
+
Verification is a single request that proves reachability, URL shape, token existence, revocation, expiry and scopes in one round trip. **On failure it writes nothing and exits non-zero.** Two escapes:
|
|
191
|
+
|
|
192
|
+
- `--force` - write anyway, with a warning. For when you know the server is temporarily down.
|
|
193
|
+
- `--no-verify` - skip the probe entirely. For air-gapped or offline setup.
|
|
194
|
+
|
|
195
|
+
A token missing the `memory:write` scope is a **warning**, not a block - but `anyslate hook` needs it, so heed it.
|
|
62
196
|
|
|
63
197
|
## Wiring into Claude Code
|
|
64
198
|
|
|
65
|
-
|
|
199
|
+
Claude Code is the only host today with a native lifecycle-hook surface. Edit `~/.claude/settings.json` (create it if missing) and **merge** this into any `hooks` block already there - don't replace the file:
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"hooks": {
|
|
204
|
+
"SessionStart": [
|
|
205
|
+
{ "hooks": [ { "type": "command", "command": "anyslate hook session-start" } ] }
|
|
206
|
+
],
|
|
207
|
+
"PostToolUse": [
|
|
208
|
+
{
|
|
209
|
+
"matcher": "Edit|Write|MultiEdit|Bash",
|
|
210
|
+
"hooks": [ { "type": "command", "command": "anyslate hook post-tool-use" } ]
|
|
211
|
+
}
|
|
212
|
+
],
|
|
213
|
+
"Stop": [
|
|
214
|
+
{ "hooks": [ { "type": "command", "command": "anyslate hook stop" } ] }
|
|
215
|
+
]
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Restart Claude Code, then run `anyslate doctor` - it parses this file and tells you whether the wiring took.
|
|
221
|
+
|
|
222
|
+
**If `anyslate` isn't found when the hook fires**, Claude Code spawned it with a minimal `PATH`. Use the absolute path (`command -v anyslate` prints it) or `npx --yes @anyslate/cli hook session-start`. `anyslate doctor` checks this explicitly.
|
|
223
|
+
|
|
224
|
+
## Git capture
|
|
225
|
+
|
|
226
|
+
Requires a global install (the template ships in the package). The guard on the first line means a missing template stops the recipe *before* it touches your hook file - it can never leave you with an empty one:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
# from the repo root
|
|
230
|
+
src="$(npm root -g)/@anyslate/cli/templates/git/post-commit"
|
|
231
|
+
[ -s "$src" ] && install -m 0755 "$src" .git/hooks/post-commit \
|
|
232
|
+
|| echo "template not found - install @anyslate/cli globally first (npm i -g @anyslate/cli)"
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Optional, exported before the next commit:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
export ANYSLATE_SESSION=<anyslate_session_id> # routes commits into a specific memory
|
|
239
|
+
export ANYSLATE_HOST=git_post_commit # host_hint advisory
|
|
240
|
+
export ANYSLATE_QUIET=1 # swallow CLI stderr
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`ANYSLATE_SESSION` is read by **this git template only** (it passes `--session` through); the CLI itself does not read it. It has no effect on Claude Code hooks, which don't need it - see the session-identity note above.
|
|
244
|
+
|
|
245
|
+
Commit metadata (`repo`, `branch`, `sha`, `subject`, `author`, `stats`) is JSON-encoded into the capture's `conversation_excerpt`. It is not a set of first-class queryable fields.
|
|
246
|
+
|
|
247
|
+
## FAQ
|
|
248
|
+
|
|
249
|
+
**Do I have to type `anyslate ...` regularly?**
|
|
250
|
+
No. After setup, your AI tools call it automatically. The only commands you might type yourself are `checkpoint`, `upload-artifact`, and `doctor` when something looks wrong.
|
|
251
|
+
|
|
252
|
+
**Will this slow down my AI tool?**
|
|
253
|
+
Each hook invocation is three sequential HTTPS round trips (`initialize`, `notifications/initialized`, `tools/call`) sharing one 15-second timeout budget. Typically well under a second. It is fail-open, so a network blip logs to stderr and exits cleanly rather than blocking your session.
|
|
254
|
+
|
|
255
|
+
**What if I want to disable capture for one session?**
|
|
256
|
+
Set `ANYSLATE_DISABLE=1` in the shell where you're running the AI tool. The CLI makes no network call and captures nothing for that shell.
|
|
257
|
+
|
|
258
|
+
Setting `ANYSLATE_MCP_TOKEN=` (empty) does **not** disable capture - an empty string falls through to the token in `~/.anyslate/cli.json`, so capture keeps running. Earlier versions of this README claimed otherwise; that claim was wrong. Use `ANYSLATE_DISABLE=1`.
|
|
259
|
+
|
|
260
|
+
**Where does the data live?**
|
|
261
|
+
In your AnySlate workspace. The CLI is a stateless client; it sends events to the AnySlate MCP service over HTTPS and stores nothing locally beyond your token in `~/.anyslate/cli.json` and the local run logs described under `anyslate hook` (all mode 0600).
|
|
262
|
+
|
|
263
|
+
**Can I see what was captured?**
|
|
264
|
+
Yes - **AI Memory → Activity** shows every captured row with its status (pending / merged / rejected / failed), and failures under the **Failed** tab carry the reason. High-risk items wait for approval; low-risk items promote on their own in 30-90 seconds.
|
|
265
|
+
|
|
266
|
+
**How do I undo a bad capture?**
|
|
267
|
+
Open the Activity panel, find the row, click **Reject**. Pre-promotion rejections never touch your memory pages. Post-promotion: open the memory and edit or delete the merged content.
|
|
268
|
+
|
|
269
|
+
**Does this work with tools that aren't Claude Code?**
|
|
270
|
+
Auto-firing lifecycle hooks are Claude Code only - it's the only host with a native hook surface. For Cursor / Windsurf / Cline / Claude Desktop / ChatGPT, capture happens **inside the host** via the AnySlate MCP server (`/anyslate-new`, `/anyslate-continue`). The CLI on the side handles git capture, CI capture, and manual checkpoints from anywhere a shell command can run.
|
|
271
|
+
|
|
272
|
+
**What's the privacy story?**
|
|
273
|
+
Bearer-token auth, optional capability handles that scope a token to one project or topic, HTTPS-only transport, payload caps. Tokens are revocable from the same dialog you minted them in. `notes` is clipped at **4,000 characters** and `conversation_excerpt` at **16,000 characters** - both are string lengths, not bytes, and clipping applies to the `hook` path only (`checkpoint --note` is sent as given).
|
|
274
|
+
|
|
275
|
+
**What if I'm offline?**
|
|
276
|
+
Hooks fail open - no error to your AI tool, just no capture for those events. There is no offline queue, so events missed while offline are not retroactively captured. The failure is recorded locally and surfaced by `anyslate doctor`.
|
|
66
277
|
|
|
67
278
|
## Tests
|
|
68
279
|
|
|
69
280
|
```bash
|
|
70
|
-
npm test
|
|
71
|
-
# 23 tests · node --test · no external deps
|
|
281
|
+
npm test # node --test, no external deps
|
|
72
282
|
```
|
|
73
283
|
|
|
74
284
|
## License
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anyslate/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AnySlate CLI
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "AnySlate CLI - lifecycle hooks, git/CI capture, and manual checkpoints for AI memory. Validates its connection at login, diagnoses itself with `anyslate doctor`, and fails open without failing silent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"anyslate": "./bin/anyslate.mjs"
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"bin",
|
|
11
11
|
"src/*.mjs",
|
|
12
12
|
"src/commands",
|
|
13
|
+
"templates",
|
|
13
14
|
"README.md"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|
|
@@ -32,9 +33,8 @@
|
|
|
32
33
|
"cli"
|
|
33
34
|
],
|
|
34
35
|
"license": "MIT",
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
"url": "https://
|
|
38
|
-
"directory": "cli"
|
|
36
|
+
"homepage": "https://anyslate.io",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://anyslate.io/support"
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
// `anyslate checkpoint`
|
|
1
|
+
// `anyslate checkpoint` - submit an explicit checkpoint to the Activity feed.
|
|
2
2
|
//
|
|
3
3
|
// Unlike `hook`, this is user-initiated, so we surface failures directly
|
|
4
4
|
// (exit 1 on error). Use this in CI or one-off scripts where you actually
|
|
5
5
|
// want to know if the submit failed.
|
|
6
|
+
//
|
|
7
|
+
// W2: "failure" now includes tool-level errors (HTTP 200 + result.isError).
|
|
8
|
+
// Previously a handle denial printed `{"error":"handle_not_found","status":401}`
|
|
9
|
+
// to STDOUT and exited 0 — documented as fail-closed, actually fail-open, and
|
|
10
|
+
// on the stream a caller would pipe into the next command.
|
|
11
|
+
//
|
|
12
|
+
// stdout carries results only. Every error goes to stderr.
|
|
6
13
|
|
|
7
|
-
import { loadConfig, requireToken } from '../config.mjs';
|
|
8
|
-
import { callTool } from '../mcp-client.mjs';
|
|
14
|
+
import { loadConfig, requireToken, apiUrlNormalizationNotice, DISABLED_NOTICE } from '../config.mjs';
|
|
15
|
+
import { callTool, formatCallFailure } from '../mcp-client.mjs';
|
|
16
|
+
import { recordRun } from '../runlog.mjs';
|
|
17
|
+
import { VERSION } from '../version.mjs';
|
|
18
|
+
import { makeIo } from '../io.mjs';
|
|
9
19
|
|
|
10
20
|
const ALLOWED_KINDS = new Set([
|
|
11
21
|
'topic_shift', 'decision_committed', 'task_completed', 'task_added',
|
|
@@ -14,25 +24,37 @@ const ALLOWED_KINDS = new Set([
|
|
|
14
24
|
|
|
15
25
|
/**
|
|
16
26
|
* @param {string[]} argv arguments after `checkpoint`
|
|
27
|
+
* @param {{env?: NodeJS.ProcessEnv, fetchImpl?: typeof fetch}} [deps]
|
|
17
28
|
* @returns {Promise<number>}
|
|
18
29
|
*/
|
|
19
|
-
export async function runCheckpoint(argv) {
|
|
30
|
+
export async function runCheckpoint(argv, deps = {}) {
|
|
31
|
+
const env = deps.env ?? process.env;
|
|
32
|
+
const { out, err } = makeIo(deps);
|
|
20
33
|
const flags = parseFlags(argv);
|
|
21
34
|
if (!flags.note) {
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
err.write('usage: anyslate checkpoint --note <text> [--kind <kind>] [--session <id>] [--host <hint>] [--source <source>]\n');
|
|
36
|
+
err.write(` kinds: ${[...ALLOWED_KINDS].join(', ')}\n`);
|
|
24
37
|
return 2;
|
|
25
38
|
}
|
|
26
39
|
const kind = flags.kind || 'milestone';
|
|
27
40
|
if (!ALLOWED_KINDS.has(kind)) {
|
|
28
|
-
|
|
41
|
+
err.write(`anyslate checkpoint: unsupported kind: ${kind}\n`);
|
|
29
42
|
return 2;
|
|
30
43
|
}
|
|
31
44
|
|
|
32
|
-
const cfg = loadConfig();
|
|
45
|
+
const cfg = loadConfig(env);
|
|
46
|
+
if (cfg.disabled) {
|
|
47
|
+
err.write(`${DISABLED_NOTICE}\n`);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const notice = apiUrlNormalizationNotice(cfg);
|
|
52
|
+
if (notice) err.write(notice);
|
|
53
|
+
|
|
33
54
|
const tokenCheck = requireToken(cfg);
|
|
34
55
|
if (!tokenCheck.ok) {
|
|
35
|
-
|
|
56
|
+
err.write(`anyslate checkpoint: ${tokenCheck.error}\n`);
|
|
57
|
+
recordRun({ command: 'checkpoint', ok: false, apiUrl: cfg.apiUrl, error: tokenCheck.error, version: VERSION, exitCode: 1 }, env);
|
|
36
58
|
return 1;
|
|
37
59
|
}
|
|
38
60
|
|
|
@@ -57,16 +79,34 @@ export async function runCheckpoint(argv) {
|
|
|
57
79
|
token: cfg.mcpToken,
|
|
58
80
|
toolName: 'activity_submit',
|
|
59
81
|
args,
|
|
82
|
+
fetchImpl: deps.fetchImpl,
|
|
60
83
|
});
|
|
61
84
|
if (!res.ok) {
|
|
62
|
-
const
|
|
63
|
-
|
|
85
|
+
const message = formatCallFailure('anyslate checkpoint', res);
|
|
86
|
+
err.write(message);
|
|
87
|
+
recordRun(
|
|
88
|
+
{
|
|
89
|
+
command: 'checkpoint',
|
|
90
|
+
ok: false,
|
|
91
|
+
apiUrl: cfg.apiUrl,
|
|
92
|
+
status: res.status,
|
|
93
|
+
isError: !!res.isError,
|
|
94
|
+
networkError: !!res.networkError,
|
|
95
|
+
error: message.trim(),
|
|
96
|
+
version: VERSION,
|
|
97
|
+
exitCode: 1,
|
|
98
|
+
},
|
|
99
|
+
env,
|
|
100
|
+
);
|
|
64
101
|
return 1;
|
|
65
102
|
}
|
|
66
|
-
|
|
103
|
+
recordRun({ command: 'checkpoint', ok: true, apiUrl: cfg.apiUrl, status: res.status, version: VERSION, exitCode: 0 }, env);
|
|
104
|
+
out.write(`${JSON.stringify(res.data)}\n`);
|
|
67
105
|
return 0;
|
|
68
106
|
} catch (e) {
|
|
69
|
-
|
|
107
|
+
const message = `anyslate checkpoint: request failed (${e?.message ?? e})\n`;
|
|
108
|
+
err.write(message);
|
|
109
|
+
recordRun({ command: 'checkpoint', ok: false, apiUrl: cfg.apiUrl, error: message.trim(), version: VERSION, exitCode: 1 }, env);
|
|
70
110
|
return 1;
|
|
71
111
|
}
|
|
72
112
|
}
|