@thedevlabs-io/labd 0.3.1
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/AGENTS.md +264 -0
- package/README.md +276 -0
- package/dist/labd.js +4301 -0
- package/dist/labd.js.map +7 -0
- package/package.json +54 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# AGENTS.md - labd-cli
|
|
2
|
+
|
|
3
|
+
Guidance for AI coding agents (and humans) working in this repository. This is
|
|
4
|
+
the single source of truth for the project's conventions; `CLAUDE.md` imports
|
|
5
|
+
this file so Claude Code and other tools share one set of rules.
|
|
6
|
+
|
|
7
|
+
## What this is
|
|
8
|
+
|
|
9
|
+
**labd-cli** is labd in a terminal - the third front door to the same assistant
|
|
10
|
+
that lives in the learning portal and in the VS Code extension.
|
|
11
|
+
|
|
12
|
+
The word "same" is load-bearing. This is **not** a second assistant that happens
|
|
13
|
+
to look similar. It talks to the same server at `agent.thedevlabs.io`, over the
|
|
14
|
+
same protocol, with the same prompt, the same memory, the same credit ledger, and
|
|
15
|
+
the same permission model. A learner who asks labd something in their editor and
|
|
16
|
+
then asks a follow-up in their terminal is talking to one assistant.
|
|
17
|
+
|
|
18
|
+
That is why the interesting constraint in this repo is what it must NOT contain:
|
|
19
|
+
|
|
20
|
+
- **no system prompt.** It lives in `@labd/protocol`, and the server builds it.
|
|
21
|
+
- **no model choice, and no way to learn which model answered.** The protocol has
|
|
22
|
+
no model field; upstream errors are summarised by the server, never forwarded.
|
|
23
|
+
- **no second definition of the permission matrix.** `needsApproval` comes from
|
|
24
|
+
the protocol. `ask` / `auto` / `agent` mean exactly what they mean in the editor.
|
|
25
|
+
- **no second definition of what commands are refused.** `classifyCommand` comes
|
|
26
|
+
from the protocol too.
|
|
27
|
+
|
|
28
|
+
If you are about to add assistant *behaviour* here - how it teaches, what it
|
|
29
|
+
refuses, what it is told about the project - stop. It belongs in the
|
|
30
|
+
[labd](https://github.com/thedevlabs-io/labd) repo, in `packages/protocol`, where
|
|
31
|
+
both other surfaces get it too. This repo gathers context, renders a terminal, and
|
|
32
|
+
executes tools.
|
|
33
|
+
|
|
34
|
+
## Layout
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
packages/core/ the agent loop, tools, permission checks, sessions, checkpoints
|
|
38
|
+
packages/tui/ terminal rendering - differential redraw, markdown, cards
|
|
39
|
+
packages/cli/ the binary: config, auth, extensions, and the four run modes
|
|
40
|
+
vendor/labd/ submodule - the labd repo, for @labd/protocol
|
|
41
|
+
test/ vitest; `e2e.test.ts` runs the built binary against a fake server
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The core/tui split is not tidiness. `packages/core` has no idea a terminal exists,
|
|
45
|
+
which is what lets the interactive session, `labd -p`, and RPC mode drive one loop -
|
|
46
|
+
and what makes the safety-critical parts testable without a screen.
|
|
47
|
+
|
|
48
|
+
## The protocol comes from a submodule
|
|
49
|
+
|
|
50
|
+
`@labd/protocol` is a private workspace package in the labd repo, so it is
|
|
51
|
+
consumed here as a git submodule pinned to a commit:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git submodule update --init # after cloning
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Why a submodule rather than a copy: the protocol defines the permission matrix,
|
|
58
|
+
the command denylist, and the path normalisation this CLI enforces. A copy would
|
|
59
|
+
drift, and the way it would drift is by disagreeing with the editor about what
|
|
60
|
+
labd is allowed to do. Bumping the pin is how this repo picks up a protocol change,
|
|
61
|
+
and it should be its own commit so a promotion says plainly that the contract moved.
|
|
62
|
+
|
|
63
|
+
**CI needs a credential for it.** The labd repo is private and the automatic
|
|
64
|
+
`GITHUB_TOKEN` is scoped to one repository, so CI cannot clone the submodule with
|
|
65
|
+
it - the failure reads as "Repository not found", which looks like a broken
|
|
66
|
+
submodule rather than a missing secret. The workflow therefore fetches it in its
|
|
67
|
+
own step and fails with an explanation. Add either a fine-grained PAT with read
|
|
68
|
+
access to `thedevlabs-io/labd` as the repository secret `SUBMODULE_TOKEN`, or a
|
|
69
|
+
deploy key on that repo with its private key as the secret.
|
|
70
|
+
|
|
71
|
+
## The terminal is a different PLACE, not a second editor
|
|
72
|
+
|
|
73
|
+
`surface: 'cli'` exists for one reason. The editor's prompt says labd can see "the
|
|
74
|
+
lines they have on screen", their selection, and the errors the editor reports.
|
|
75
|
+
None of that is true in a terminal, and a CLI reporting `surface: 'vscode'` would
|
|
76
|
+
get a model confidently reasoning about a screen that is not there.
|
|
77
|
+
|
|
78
|
+
So the CLI's honest context is the **project**: its name, the branch and recent
|
|
79
|
+
commits, the conventions files, the names of any skills. Everything else labd reads
|
|
80
|
+
on purpose, with a tool, and says which file it read. Resist adding anything
|
|
81
|
+
screen-shaped - "the last file you edited" is a guess dressed as context.
|
|
82
|
+
|
|
83
|
+
The RULES for gathering it are the protocol's, not ours: which files are secrets
|
|
84
|
+
(`SECRETISH`), which directories are off limits, which conventions files to read
|
|
85
|
+
and how much of them, what a skill looks like, how a branch is described. This
|
|
86
|
+
repo keeps only the filesystem half - opening the files - so the editor and the
|
|
87
|
+
terminal cannot tell the model two different stories about the same project.
|
|
88
|
+
|
|
89
|
+
When a session ends - `/quit`, the end of `labd -p`, an RPC host closing stdin -
|
|
90
|
+
the conversation is handed to `/v1/conversations/close` with tool results
|
|
91
|
+
stubbed, exactly as the editor does when a chat is left. That call is what makes
|
|
92
|
+
"same memory" true here; do not make it optional.
|
|
93
|
+
|
|
94
|
+
## Permissions - the reason this repo exists
|
|
95
|
+
|
|
96
|
+
pi.dev, the closest thing to prior art for a minimal terminal harness, is explicit
|
|
97
|
+
that it has **no permission system**: it runs with the invoking user's rights and
|
|
98
|
+
tells you to containerise it. That is a reasonable stance for a tool used by the
|
|
99
|
+
person who wrote it. labd is used by learners in repositories they did not write,
|
|
100
|
+
so the permission model is a feature rather than an omission, and these rules hold:
|
|
101
|
+
|
|
102
|
+
| mode | changes files | runs a command |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `ask` | asks | asks |
|
|
105
|
+
| `auto` (default) | applies | asks |
|
|
106
|
+
| `agent` | applies | runs |
|
|
107
|
+
|
|
108
|
+
"Changes files" is all four writes: `edit_file`, `create_file`, `move_file`, and
|
|
109
|
+
`delete_file`. Each is snapshotted, so `/undo` puts the whole turn back.
|
|
110
|
+
|
|
111
|
+
`auto` is the default because `ask` made a real piece of work - reorganise this,
|
|
112
|
+
rename that and fix the callers - into thirty approvals, which is a reason to give
|
|
113
|
+
up halfway rather than a safety property. One exception, in `unattendedMode`: a
|
|
114
|
+
run with nobody watching (`labd -p`, `--mode rpc`) falls back to `ask` unless a
|
|
115
|
+
mode was actually chosen in a settings file or on the command line. A script has
|
|
116
|
+
no one to show a diff to.
|
|
117
|
+
|
|
118
|
+
- **The mode decides who clicks; it never decides what labd will do.**
|
|
119
|
+
`classifyCommand` refuses `sudo`, deploys, publishes, `git push`, disk wipes,
|
|
120
|
+
backgrounding, anything that never exits, anything that hides what it runs, and
|
|
121
|
+
anything naming a credentials file - in **all three** modes, `agent` included.
|
|
122
|
+
- **The mode is never sent to the server.** Not in `ChatRequest`, not in the
|
|
123
|
+
prompt. A setting that governs how much we check the model's work is exactly the
|
|
124
|
+
lever an instruction smuggled into a file would reach for. `e2e.test.ts` asserts
|
|
125
|
+
it never appears on the wire.
|
|
126
|
+
- **A project cannot set the mode.** `.labd/settings.json` may set a timeout or a
|
|
127
|
+
server URL; `mode` is read from the GLOBAL settings and the command line only.
|
|
128
|
+
A repository that could set `mode: "agent"` would be a repository that runs
|
|
129
|
+
commands on the machine of anyone who clones it and types `labd`. See
|
|
130
|
+
`config.ts`, and the test that pins it.
|
|
131
|
+
- **A project cannot load its own code until trusted.** `.labd/extensions` needs
|
|
132
|
+
`labd trust` first. `git clone && labd` must not run a stranger's extension.
|
|
133
|
+
- **A project cannot redirect the token until trusted.** `serverUrl` in
|
|
134
|
+
`.labd/settings.json` is honoured only for a trusted project; otherwise it is
|
|
135
|
+
ignored and the banner says so. The bearer token goes wherever that URL points,
|
|
136
|
+
so a checked-in URL is a checked-in token exfiltration for anyone who clones and
|
|
137
|
+
types `labd`. Same threat class as project extensions, same gate.
|
|
138
|
+
- **Extensions add commands and observers, never tools.** The tool list is the
|
|
139
|
+
protocol's: the server offers the model exactly the capabilities this client
|
|
140
|
+
declares, from one schema both surfaces share. A `registerTool` here would have
|
|
141
|
+
no route to the model and would need its own approval rule, which is a second
|
|
142
|
+
definition of the permission matrix. An extension tool is therefore a protocol
|
|
143
|
+
change, and belongs in labd first. `registerCommand` cannot shadow a built-in,
|
|
144
|
+
because `/mode` and `/undo` have to mean what the learner expects.
|
|
145
|
+
- **Unattended means declined.** `labd -p` has no way to ask, so every call that
|
|
146
|
+
needs approval is refused and the exit code is 2. There is deliberately no
|
|
147
|
+
`--yes`: the way to let an automated run write is to choose a mode in a settings
|
|
148
|
+
file, which is a decision with a record, not a flag buried in a shell script.
|
|
149
|
+
|
|
150
|
+
## Writes land on disk, so checkpoints are not optional
|
|
151
|
+
|
|
152
|
+
The extension edits an unsaved buffer, so a wrong edit costs one keystroke. There
|
|
153
|
+
is no buffer here. Four rules follow, and `checkpoints.ts` explains each at the
|
|
154
|
+
point it applies:
|
|
155
|
+
|
|
156
|
+
- capture happens immediately before the write, off the text about to be replaced;
|
|
157
|
+
- **capture can never fail a write** - an unsnapshotted file is recorded as such
|
|
158
|
+
and refused later, rather than blocking the edit;
|
|
159
|
+
- conflicts are decided by **content hash**, never mtime, and a file the learner
|
|
160
|
+
edited since is left alone with a reason;
|
|
161
|
+
- a restore is itself reversible, because an undo that cannot be undone is a trap.
|
|
162
|
+
|
|
163
|
+
`run_command` is the exception nothing covers: a command's side effects are on disk
|
|
164
|
+
before it exits. Say so rather than implying otherwise - the tool description tells
|
|
165
|
+
the model to prefer `edit_file`, and the approval card tells the learner.
|
|
166
|
+
|
|
167
|
+
## Sessions are trees
|
|
168
|
+
|
|
169
|
+
Borrowed from pi, and worth keeping. Every node has an `id` and a `parentId`; the
|
|
170
|
+
conversation is the path from the root to `head`. Branching moves the head and
|
|
171
|
+
keeps both branches in one append-only JSONL file. Three things fall out of it:
|
|
172
|
+
retrying an answer without losing the first, compaction that is lossy on the wire
|
|
173
|
+
and lossless on disk, and a crash costing at most the last line.
|
|
174
|
+
|
|
175
|
+
Do not "simplify" this into a list. The list version cannot do any of the three.
|
|
176
|
+
|
|
177
|
+
## Terminal rules
|
|
178
|
+
|
|
179
|
+
- **Never assume a TTY.** Piped, redirected, or in CI, colour is off and the live
|
|
180
|
+
region is a no-op. Escape codes in a log file are worse than plain text.
|
|
181
|
+
- **The scrollback is not ours.** Finished output is printed and belongs to the
|
|
182
|
+
terminal; only the status line is a live region. That is what keeps search,
|
|
183
|
+
selection, and copy-paste working.
|
|
184
|
+
- **stdout is the answer. Everything else is stderr.** A pipeline must receive
|
|
185
|
+
only what labd meant as the answer.
|
|
186
|
+
- **Escape sequences are written as `\u001b` escapes**, never as literal bytes, so
|
|
187
|
+
source files stay greppable ASCII.
|
|
188
|
+
|
|
189
|
+
## Distribution
|
|
190
|
+
|
|
191
|
+
Three artefacts, one source:
|
|
192
|
+
|
|
193
|
+
- **`dist/labd.js`** - ESM, zero runtime dependencies, what npm's `bin` points at.
|
|
194
|
+
- **`dist/labd.cjs`** - the same code as CommonJS. It exists because Node's
|
|
195
|
+
single-executable support runs its entry as CJS and refuses an ESM one outright.
|
|
196
|
+
This is why `bin/labd.ts` must never use top-level await.
|
|
197
|
+
- **`dist/bin/labd-<os>-<arch>`** - a standalone executable, built by injecting the
|
|
198
|
+
CJS bundle into a copy of the `node` binary. Not cross-compilable, so the release
|
|
199
|
+
workflow builds one per runner. ~115 MB, ~40 MB compressed: the honest price of
|
|
200
|
+
"works on a machine with nothing installed".
|
|
201
|
+
|
|
202
|
+
macOS needs the signing dance in `build-binaries.mjs`, and it is not optional -
|
|
203
|
+
injecting invalidates the signature, and macOS kills an invalidly-signed binary
|
|
204
|
+
rather than running it. The build also RUNS the binary before calling it done,
|
|
205
|
+
because every one of those steps fails by producing something that does not start
|
|
206
|
+
rather than by erroring.
|
|
207
|
+
|
|
208
|
+
`npm install -g git+ssh://...` cannot work: `dist/` is not committed and npm does
|
|
209
|
+
not fetch submodules for a git dependency. `scripts/prepare.mjs` says so rather
|
|
210
|
+
than letting the install appear to succeed with no binary - which is what it did
|
|
211
|
+
before, reporting "added 1 package" and installing nothing.
|
|
212
|
+
|
|
213
|
+
## Sub-agents - deliberately outside
|
|
214
|
+
|
|
215
|
+
There is no `spawn_agent` tool, and adding one would be a permission mistake. Such
|
|
216
|
+
a tool has to decide what mode the children run in: inherit the parent's and one
|
|
217
|
+
approval becomes N unattended writes; give children their own and the model has
|
|
218
|
+
been handed the setting it is specifically not allowed to influence.
|
|
219
|
+
|
|
220
|
+
`--mode rpc` already makes every labd process a driveable agent, so orchestration
|
|
221
|
+
lives in a script the operator writes - where the children's mode is set by a
|
|
222
|
+
person, each child asks that script for approval over the wire, and there is no RPC
|
|
223
|
+
method for changing a mode. `examples/orchestrate.mjs` is the reference, and
|
|
224
|
+
`test/rpc-e2e.test.ts` runs two children at once to prove it.
|
|
225
|
+
|
|
226
|
+
## Branches - promotion is by pull request, always
|
|
227
|
+
|
|
228
|
+
`develop` → `staging` → `production`, one step at a time, and **every step is a
|
|
229
|
+
pull request** - the same rule as the other Dev Labs repos, for the same reason:
|
|
230
|
+
the PR is the record of what shipped and when, and it is the only place a bad
|
|
231
|
+
promotion gets caught before it is live.
|
|
232
|
+
|
|
233
|
+
Never push directly to `staging` or `production`, and never move them with
|
|
234
|
+
`git branch -f` or a force push. `.githooks/pre-push` refuses the direct push;
|
|
235
|
+
turn it on once per clone:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
git config core.hooksPath .githooks
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Testing
|
|
242
|
+
|
|
243
|
+
`npm test` runs vitest. `npm run check` runs the typecheck too.
|
|
244
|
+
|
|
245
|
+
What is worth testing here is not "does it answer" - that needs a model - but the
|
|
246
|
+
properties that would fail silently:
|
|
247
|
+
|
|
248
|
+
- **containment** (`paths.test.ts`): traversal, absolute paths, symlinks out of the
|
|
249
|
+
tree, decorated paths. Every path labd touches is model output.
|
|
250
|
+
- **the permission properties** (`permissions.test.ts`): a project cannot set the
|
|
251
|
+
mode or the server URL, an untrusted project cannot load code, an extension
|
|
252
|
+
cannot shadow a built-in command, `agent` does not widen the denylist.
|
|
253
|
+
- **the session tree** (`session.test.ts`): branching, non-destructive compaction,
|
|
254
|
+
a truncated last line.
|
|
255
|
+
- **checkpoints** (`tools.test.ts`): a conflicting edit is refused, a created file
|
|
256
|
+
is deleted rather than blanked, a capture failure never blocks a write.
|
|
257
|
+
- **framing** (`rpc.test.ts`): splitting on `\n` only. There is a test that
|
|
258
|
+
demonstrates why `node:readline` is wrong here; if someone switches to it, that
|
|
259
|
+
test explains the bug.
|
|
260
|
+
- **the whole binary** (`e2e.test.ts`): a fake agent server, a real subprocess.
|
|
261
|
+
This is where "the mode never reaches the wire" and "unattended declines" are
|
|
262
|
+
actually proven.
|
|
263
|
+
|
|
264
|
+
`e2e.test.ts` needs `npm run build` first.
|
package/README.md
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# labd-cli
|
|
2
|
+
|
|
3
|
+
labd in your terminal — the Dev Labs assistant, in the project you're standing in.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
$ labd
|
|
7
|
+
[o_o] labd in labd-cli on develop
|
|
8
|
+
labd changes files on its own - every answer can be put back in one go - and still asks before running a command.
|
|
9
|
+
|
|
10
|
+
> why is session.test.ts failing?
|
|
11
|
+
|
|
12
|
+
● Read test/session.test.ts
|
|
13
|
+
? Run a command
|
|
14
|
+
to see which of the eleven cases fails
|
|
15
|
+
|
|
16
|
+
npx vitest run test/session.test.ts
|
|
17
|
+
|
|
18
|
+
On auto, commands still ask - a command can do anything your shell can, and put back isn't one of them.
|
|
19
|
+
run this command? [y/N]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This is the same assistant as the [VS Code extension](https://github.com/thedevlabs-io/labd)
|
|
23
|
+
and the one docked in the learning portal — same server, same prompt, same memory,
|
|
24
|
+
same permission model. Ask something in your editor, follow up in your terminal.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
**macOS / Linux** — a standalone executable. Nothing else needs to be installed:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
curl -fsSL https://raw.githubusercontent.com/thedevlabs-io/labd-cli/production/install.sh | sh
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Windows** — same, in PowerShell:
|
|
35
|
+
|
|
36
|
+
```powershell
|
|
37
|
+
irm https://raw.githubusercontent.com/thedevlabs-io/labd-cli/production/install.ps1 | iex
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**With npm**, if you already have Node 22+:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install -g @thedevlabs-io/labd
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then connect:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
labd login # approve a short code in your browser
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
<details>
|
|
53
|
+
<summary>Other ways, and what each is for</summary>
|
|
54
|
+
|
|
55
|
+
| how | when |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `install.sh` / `install.ps1` | a machine with nothing on it. Downloads a ~40 MB archive containing a single executable with the Node runtime inside it. |
|
|
58
|
+
| `npm install -g @thedevlabs-io/labd` | you have Node. Smaller download, and `npm update -g @thedevlabs-io/labd` upgrades it. |
|
|
59
|
+
| `npm pack` → share the `.tgz` | an air-gapped machine, or pinning an exact build. `npm install -g thedevlabs-io-labd-0.3.1.tgz`. |
|
|
60
|
+
| copy `dist/labd.js` | you built from source. It has **zero runtime dependencies**, so that one file plus Node 22+ is a working install anywhere. |
|
|
61
|
+
| clone + `npm link` | working on labd itself. |
|
|
62
|
+
|
|
63
|
+
`npm install -g git+ssh://…` does **not** work, and says so rather than half-installing: `dist/` is not committed, and npm does not fetch the git submodule that the build needs.
|
|
64
|
+
|
|
65
|
+
While this repository is private, the install scripts need a credential to read release assets. They use the GitHub CLI when it is signed in, or `GH_TOKEN` if you set one, and explain that rather than failing with a 404.
|
|
66
|
+
|
|
67
|
+
**Building from source:**
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git clone git@github.com:thedevlabs-io/labd-cli.git
|
|
71
|
+
cd labd-cli
|
|
72
|
+
git submodule update --init # the protocol lives in the labd repo
|
|
73
|
+
npm install
|
|
74
|
+
npm run build # dist/labd.js (and dist/labd.cjs, for binaries)
|
|
75
|
+
npm link # puts `labd` on your PATH
|
|
76
|
+
npm run build:binary # optional: a standalone executable for this platform
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
</details>
|
|
80
|
+
|
|
81
|
+
## Using it
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
labd # a session in the current project
|
|
85
|
+
labd "why is this test failing" # one question, then exit
|
|
86
|
+
labd -p "..." | pbcopy # print mode, for pipes and scripts
|
|
87
|
+
cat build.log | labd -p "why?" # piped input joins the question
|
|
88
|
+
labd --mode rpc # JSON-RPC over stdio, to embed it
|
|
89
|
+
labd -c # pick up the most recent session here
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| flag | |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `-p`, `--print` | answer and exit instead of starting a session |
|
|
95
|
+
| `--json` | newline-delimited JSON events instead of prose (implies `-p`) |
|
|
96
|
+
| `--mode <ask\|auto\|agent>` | the permission mode for this run; `--mode rpc` is the transport |
|
|
97
|
+
| `--think <low\|medium\|high>` | reasoning effort to ask for (the server decides what it honours) |
|
|
98
|
+
| `--resume <id>` | continue an earlier session; an id that matches nothing is an error, not a new session |
|
|
99
|
+
| `-c`, `--continue` | continue the most recent session in this project |
|
|
100
|
+
| `-C`, `--cwd <dir>` | run as though started in `<dir>` |
|
|
101
|
+
| `-e`, `--extension <path>` | load an extension file (repeatable; `/reload` keeps it) |
|
|
102
|
+
| `--no-extensions` | load none, now and on `/reload` |
|
|
103
|
+
| `--no-color` / `--color` | force colour off or on |
|
|
104
|
+
| `-v`, `--version` | |
|
|
105
|
+
|
|
106
|
+
Subcommands: `login`, `logout` (revokes the token server-side too), `sessions`
|
|
107
|
+
(earlier sessions here), `trust` (let this project load its own extensions and
|
|
108
|
+
name its own server).
|
|
109
|
+
|
|
110
|
+
Inside a session: `@path` tags a file into your question, Tab completes both those
|
|
111
|
+
and `/commands`, ctrl-c stops an answer without leaving.
|
|
112
|
+
|
|
113
|
+
| command | |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `/mode [ask\|auto\|agent]` | how much labd does without asking |
|
|
116
|
+
| `/think [low\|medium\|high]` | how hard it thinks (a request — the server decides) |
|
|
117
|
+
| `/undo` | put back the files the last answer changed |
|
|
118
|
+
| `/redo` | reapply what `/undo` put back |
|
|
119
|
+
| `/tree` | every point in this session; branch from any of them |
|
|
120
|
+
| `/resume <id>` | continue from a point in `/tree` |
|
|
121
|
+
| `/sessions`, `/session` | earlier sessions here; where this one is stored |
|
|
122
|
+
| `/usage` | what's left of this month's allowance |
|
|
123
|
+
| `/trust` | let this project load its own extensions |
|
|
124
|
+
| `/reload` | reload extensions and the file list |
|
|
125
|
+
| `/quit` | leave - the session is saved, and `labd --resume <id>` continues it |
|
|
126
|
+
|
|
127
|
+
When you leave, the conversation is handed to the server to be distilled into
|
|
128
|
+
memory, exactly as the editor does when you start a new chat - which is what
|
|
129
|
+
makes "same memory" true in the terminal. Tool results are stripped before it
|
|
130
|
+
leaves, so no file contents travel for that.
|
|
131
|
+
|
|
132
|
+
## The three modes
|
|
133
|
+
|
|
134
|
+
| mode | changes files | runs a command |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| `ask` | asks first | asks first |
|
|
137
|
+
| `auto` (default) | on its own | asks first |
|
|
138
|
+
| `agent` | on its own | on its own |
|
|
139
|
+
|
|
140
|
+
Changing a file means any of the four: editing one, creating one, moving one, or
|
|
141
|
+
deleting one. `auto` splits where it does because the two columns are not equally
|
|
142
|
+
reversible: every write is snapshotted and `/undo` puts the whole turn back, while
|
|
143
|
+
a command has already happened.
|
|
144
|
+
|
|
145
|
+
`labd -p` and `--mode rpc` are the exception. With nobody watching they ask -
|
|
146
|
+
which unattended means refusing - unless you have actually chosen a mode.
|
|
147
|
+
|
|
148
|
+
**Some things are refused in every mode, `agent` included** — anything needing
|
|
149
|
+
`sudo`, deploying, publishing, `git push`, wiping a disk, backgrounding a process,
|
|
150
|
+
anything that never exits, and anything that reads a credentials file. The mode
|
|
151
|
+
decides who clicks; it never decides what labd is willing to do.
|
|
152
|
+
|
|
153
|
+
Two more rules worth knowing before you point this at a repository you didn't write:
|
|
154
|
+
|
|
155
|
+
- **A project can't set the mode.** `.labd/settings.json` may set a timeout; `mode`
|
|
156
|
+
comes from your global settings or the command line only. Otherwise cloning a
|
|
157
|
+
repo and typing `labd` would be running whatever it decided to allow itself.
|
|
158
|
+
- **A project can't load its own code until you say so.** `.labd/extensions` needs
|
|
159
|
+
`labd trust` in that directory first.
|
|
160
|
+
- **A project can't point your token at another server until you say so.** A
|
|
161
|
+
`serverUrl` in `.labd/settings.json` is ignored - and the banner says so - until
|
|
162
|
+
the project is trusted.
|
|
163
|
+
|
|
164
|
+
## Files it keeps
|
|
165
|
+
|
|
166
|
+
| path | |
|
|
167
|
+
|---|---|
|
|
168
|
+
| `~/.labd/auth.json` | your token, mode `0600`, never in the project |
|
|
169
|
+
| `~/.labd/settings.json` | global settings, including `mode` |
|
|
170
|
+
| `~/.labd/sessions/` | JSONL session trees, bucketed by project |
|
|
171
|
+
| `~/.labd/checkpoints/` | snapshots, so `/undo` works |
|
|
172
|
+
| `.labd/settings.json` | per-project settings (not `mode`) |
|
|
173
|
+
| `.labd/extensions/` | per-project extensions, loaded once trusted |
|
|
174
|
+
|
|
175
|
+
`settings.json` keys, global or per project: `timeout` as `commandTimeoutSeconds`
|
|
176
|
+
(5–600), `effort` (`low`/`medium`/`high`), `checkpoints` (`true` by default), and
|
|
177
|
+
`serverUrl` — honoured from a project file only once that project is trusted.
|
|
178
|
+
`mode` is read from the global file only.
|
|
179
|
+
|
|
180
|
+
`LABD_HOME`, `LABD_SERVER_URL`, and `LABD_TOKEN` override the corresponding thing;
|
|
181
|
+
the last is how you run labd in CI without pairing. `NO_COLOR` turns colour off,
|
|
182
|
+
`FORCE_COLOR` turns it on (`FORCE_COLOR=0` off) and wins over everything else, and
|
|
183
|
+
`LABD_NO_MOTION` skips the opening animation.
|
|
184
|
+
|
|
185
|
+
## Sub-agents and orchestration
|
|
186
|
+
|
|
187
|
+
labd has no `spawn_agent` tool, and that is a decision rather than a gap. It does
|
|
188
|
+
have `--mode rpc`, which makes every labd process a driveable agent on
|
|
189
|
+
stdin/stdout — so an orchestrator is a script that spawns several of them:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
node examples/orchestrate.mjs "what does this file do?" src/a.ts src/b.ts src/c.ts
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
[`examples/orchestrate.mjs`](examples/orchestrate.mjs) is ~140 lines: spawn, send
|
|
196
|
+
`labd/prompt`, read the notification stream, run three at a time.
|
|
197
|
+
|
|
198
|
+
**Why not a tool inside labd.** A `spawn_agent` tool has to decide what mode the
|
|
199
|
+
children run in, and every answer is bad. Inherit the parent's mode and one
|
|
200
|
+
approval becomes N unattended writes. Give children their own mode and the model
|
|
201
|
+
has been handed the one setting it is specifically not allowed to influence.
|
|
202
|
+
Outside, the mode of each child is set by whoever wrote the script, each child
|
|
203
|
+
asks *that script* for approval over the wire, and the orchestrator is code you
|
|
204
|
+
can read. (Pi reaches the same conclusion and suggests tmux.)
|
|
205
|
+
|
|
206
|
+
The protocol is small:
|
|
207
|
+
|
|
208
|
+
| you send | |
|
|
209
|
+
|---|---|
|
|
210
|
+
| `labd/prompt` `{question}` | run a turn; resolves with `{text, wrote, ran}`. A second prompt sent before the first resolves is queued behind it |
|
|
211
|
+
| `labd/session` | id, file, node count, head |
|
|
212
|
+
| `labd/capabilities` | mode, root, methods |
|
|
213
|
+
| labd sends | |
|
|
214
|
+
| `labd/ready` | first, on start: protocol, framing, session id, mode, root |
|
|
215
|
+
| `labd/text`, `labd/status`, `labd/done`, `labd/error` | notifications as the turn runs |
|
|
216
|
+
| `labd/tool` `{name, status, input, detail, preview, output}` | each tool step; `preview` is the diff or the command, `output` what came back |
|
|
217
|
+
| `labd/command_output` `{line}` | a running command's own output, as it happens |
|
|
218
|
+
| `labd/approve` `{call, preview}` | **a request to you** — answer `{approved: true\|false}` |
|
|
219
|
+
|
|
220
|
+
Closing stdin ends the session: any turn in flight finishes, the conversation is
|
|
221
|
+
handed over for memory, and the process exits 0.
|
|
222
|
+
|
|
223
|
+
Two things to get right, both of which have tests in this repo:
|
|
224
|
+
|
|
225
|
+
- **Split records on `\n` and nothing else.** Not `readline`: it also splits on
|
|
226
|
+
U+2028 and U+2029, which occur inside real JSON strings, and it will cut a valid
|
|
227
|
+
record in half.
|
|
228
|
+
- **Answer `labd/approve`.** Ignore it and the child refuses after two minutes,
|
|
229
|
+
which is the safe direction but a slow one. There is deliberately no RPC method
|
|
230
|
+
for *changing* the mode: a host that could raise it would be a host that turns
|
|
231
|
+
approvals off for the person who launched labd.
|
|
232
|
+
|
|
233
|
+
## Extensions
|
|
234
|
+
|
|
235
|
+
A module that gets an API object, and can add slash commands and watch what labd
|
|
236
|
+
does:
|
|
237
|
+
|
|
238
|
+
```js
|
|
239
|
+
// ~/.labd/extensions/branches.mjs
|
|
240
|
+
export default function (labd) {
|
|
241
|
+
labd.registerCommand('branches', {
|
|
242
|
+
describe: 'list local branches',
|
|
243
|
+
run: async () => execSync('git branch', { cwd: labd.root }).toString(),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
labd.on('tool', (step) => { /* observe; handlers cannot veto */ });
|
|
247
|
+
labd.on('turn', ({ question }) => {});
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
A command can't shadow a built-in (`/mode` has to mean `/mode`). Extensions
|
|
252
|
+
cannot add **tools** the model can call: the tool list is the protocol's, shared
|
|
253
|
+
with the editor, so a new tool is a change to the labd repo rather than to a file
|
|
254
|
+
in `~/.labd/extensions`.
|
|
255
|
+
|
|
256
|
+
## Development
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
npm run build # bundle to dist/labd.js
|
|
260
|
+
npm test # vitest (build first — the e2e suite spawns the binary)
|
|
261
|
+
npm run check # typecheck + tests
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Conventions, and the reasoning behind the permission model, are in
|
|
265
|
+
[AGENTS.md](AGENTS.md). Promotion is `develop` → `staging` → `production`, one
|
|
266
|
+
pull request per step; turn the guard on with
|
|
267
|
+
`git config core.hooksPath .githooks`.
|
|
268
|
+
|
|
269
|
+
## Prior art
|
|
270
|
+
|
|
271
|
+
The shape of this owes a lot to [pi](https://pi.dev) (`earendil-works/pi`) — the
|
|
272
|
+
layered packages, sessions as trees, the print/RPC/SDK modes, and the framing
|
|
273
|
+
detail that stdio JSONL must be split on `\n` and nothing else. Where labd-cli
|
|
274
|
+
differs is the thing pi deliberately leaves out: pi has no permission system and
|
|
275
|
+
tells you to containerise it, which is right for a tool you built for yourself and
|
|
276
|
+
wrong for one a learner points at a repository they just cloned.
|