@gobius/t3ctl 0.5.0 → 0.7.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 +167 -12
- package/dist/t3ctl.js +796 -28
- package/dist/t3ctl.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -110,11 +110,73 @@ t3ctl warns you when:
|
|
|
110
110
|
meaning it points at a different machine than it used to and the stored token
|
|
111
111
|
belongs to the old one
|
|
112
112
|
|
|
113
|
+
### Two servers on one machine
|
|
114
|
+
|
|
115
|
+
Nothing stops two T3 Code servers from running against the same `~/.t3` — for
|
|
116
|
+
example the boot service on 3773 and a desktop-launched server on 3774. They
|
|
117
|
+
share one database but each keeps its own in-memory view, so a thread created
|
|
118
|
+
through one is invisible to the other, and the app then fails with
|
|
119
|
+
`Thread '…' does not exist for command '…'`. Both report the same
|
|
120
|
+
`environmentId`, so the descriptor can't tell them apart.
|
|
121
|
+
|
|
122
|
+
Before every write, t3ctl compares the host with the live server recorded in
|
|
123
|
+
`~/.t3/userdata/server-runtime.json` (the one that started last). If that is a
|
|
124
|
+
different port serving the same environment, the write is refused and nothing is
|
|
125
|
+
sent; `ls` and `hosts` print the same warning. Stop the stale server, or re-point
|
|
126
|
+
the host — its token carries over, since both ports use one auth database:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
t3ctl host add http://127.0.0.1:3774 --name agentbox
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The check runs for loopback hosts, and for ssh hosts on writes (one extra ssh
|
|
133
|
+
round-trip to read the remote runtime file).
|
|
134
|
+
|
|
113
135
|
The token is optional so you can register a host before minting one, but reads
|
|
114
136
|
will fail until you add it.
|
|
115
137
|
|
|
116
|
-
|
|
117
|
-
|
|
138
|
+
### `t3ctl host add <ssh-target> [--name <name>] [--ttl <duration>]`
|
|
139
|
+
|
|
140
|
+
Register a host given **only its ssh login** — no URL, no token, no port:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
t3ctl host add agent@goobles-agentbox
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
That one command bootstraps the machine end to end:
|
|
147
|
+
|
|
148
|
+
1. **Probes it over ssh** for a running T3 Code server. The server records its
|
|
149
|
+
port in `~/.t3/userdata/server-runtime.json` on the remote, so the port is
|
|
150
|
+
discovered, never assumed (it is 3773 or whatever free port the server fell
|
|
151
|
+
back to).
|
|
152
|
+
2. **Installs the server if none is running** — `t3 service install`, T3 Code's
|
|
153
|
+
own per-user launchd/systemd service (no sudo; macOS and Linux). The exact
|
|
154
|
+
`t3` CLI version is resolved locally via npm (`latest`, falling back to
|
|
155
|
+
`nightly`); override with `--t3-version <version-or-tag>`. The remote npm
|
|
156
|
+
output streams past — a cold cache can download for a few minutes.
|
|
157
|
+
3. **Mints a token for you** — `t3 auth session issue` on the remote, labeled
|
|
158
|
+
`t3ctl:<name>`, TTL 30d by default (`--ttl` changes it). The session id is
|
|
159
|
+
printed along with the exact revoke command for that host.
|
|
160
|
+
4. **Tunnels to it** — an ssh ControlMaster forwards a local port to the remote
|
|
161
|
+
server (loopback-only by design; nothing is exposed on the remote's network).
|
|
162
|
+
The master outlives t3ctl, and every command silently rebuilds it when it
|
|
163
|
+
dies or the remote port changed.
|
|
164
|
+
|
|
165
|
+
Registering again for the same login is idempotent: it refreshes the tunnel and
|
|
166
|
+
reuses the stored token unless the login now lands on a *different machine*
|
|
167
|
+
(different `environmentId`), in which case a fresh token is minted.
|
|
168
|
+
|
|
169
|
+
Give it a name with `--name` (otherwise it is named after the remote's own
|
|
170
|
+
machine label, same as the origin form). Notes:
|
|
171
|
+
|
|
172
|
+
- `t3ctl host rm <name>` tears the tunnel down with the entry.
|
|
173
|
+
- **macOS remotes need someone logged in at the console** — a launchd agent
|
|
174
|
+
starts at login, so an ssh install with nobody logged in installs fine but
|
|
175
|
+
cannot start the server. Linux (systemd user service) has no such gap.
|
|
176
|
+
- If the remote lacks node for non-interactive shells, install Node there (a
|
|
177
|
+
version manager may need configuring for non-login shells); if a native
|
|
178
|
+
dependency fails to build, a C toolchain is missing (`build-essential` /
|
|
179
|
+
`gcc-c++` / `xcode-select --install`).
|
|
118
180
|
|
|
119
181
|
### `t3ctl host rm <name>`
|
|
120
182
|
|
|
@@ -167,6 +229,7 @@ Flags:
|
|
|
167
229
|
| Flag | Default | Meaning |
|
|
168
230
|
|---|---|---|
|
|
169
231
|
| `--model <instance>/<model>` | `claudeAgent/claude-opus-5` | Provider instance and model |
|
|
232
|
+
| `--option <id>=<value>` | none | Model option, e.g. `effort=high`; repeatable |
|
|
170
233
|
| `--branch <name>` | none | Git branch for the thread |
|
|
171
234
|
| `--worktree <path>` | none | Explicit worktree path |
|
|
172
235
|
| `--runtime-mode <mode>` | `full-access` | `approval-required`, `auto-accept-edits`, `auto`, `full-access` |
|
|
@@ -176,6 +239,14 @@ Flags:
|
|
|
176
239
|
`--model` splits on the **first** slash, so slashed model names work as-is:
|
|
177
240
|
`--model opencode/github-copilot/gpt-5.4`.
|
|
178
241
|
|
|
242
|
+
`--option` sets one of the model's options, the same ones the app's model picker
|
|
243
|
+
shows. The ids come from the provider, e.g. `effort`, `fastMode` and
|
|
244
|
+
`contextWindow` for Claude. `true` and `false` are sent as booleans:
|
|
245
|
+
|
|
246
|
+
```sh
|
|
247
|
+
t3ctl thread create t3ctl "tidy the tests" --model claudeAgent/claude-opus-5-5 --option effort=medium --option fastMode=false
|
|
248
|
+
```
|
|
249
|
+
|
|
179
250
|
### `t3ctl thread send <thread> <message...>`
|
|
180
251
|
|
|
181
252
|
Send a message to a thread and run the agent. This is how you continue a
|
|
@@ -195,9 +266,10 @@ started rewrite the readme for users
|
|
|
195
266
|
seq 4471
|
|
196
267
|
```
|
|
197
268
|
|
|
198
|
-
Accepts `--model`, `--runtime-mode`, `--interaction-mode`, and `--host`. Unlike
|
|
269
|
+
Accepts `--model`, `--option`, `--runtime-mode`, `--interaction-mode`, and `--host`. Unlike
|
|
199
270
|
`thread create`, `--model` has no default here: the thread's existing model is
|
|
200
|
-
reused unless you override it.
|
|
271
|
+
reused unless you override it. `--option` changes options on top of the thread's
|
|
272
|
+
model, or on top of `--model` when you pass one.
|
|
201
273
|
|
|
202
274
|
### `t3ctl thread rename <thread> <new title...>`
|
|
203
275
|
|
|
@@ -255,6 +327,82 @@ t3ctl thread delete "scratch experiment"
|
|
|
255
327
|
|
|
256
328
|
`delete` is not prompted and not undoable from t3ctl — check with `ls -t` first.
|
|
257
329
|
|
|
330
|
+
### `t3ctl export prompts --since <date> [--until <date>]`
|
|
331
|
+
|
|
332
|
+
Every prompt you typed in a window, across every registered host, with the
|
|
333
|
+
project each one happened in. Read-only, and built for other tools to consume
|
|
334
|
+
rather than for reading yourself — time trackers, activity logs, weeknotes.
|
|
335
|
+
|
|
336
|
+
```sh
|
|
337
|
+
t3ctl export prompts --since 2026-09-14 --until 2026-09-15
|
|
338
|
+
```
|
|
339
|
+
```
|
|
340
|
+
29 prompts 2026-09-14T00:00:00.000Z -> 2026-09-15T00:00:00.000Z
|
|
341
|
+
|
|
342
|
+
agentbox 29 local state.sqlite
|
|
343
|
+
@clients/dsl 28
|
|
344
|
+
t3ctl 1
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
| Option | |
|
|
348
|
+
|---|---|
|
|
349
|
+
| `--since <date>` | Required. Start of the window, inclusive. |
|
|
350
|
+
| `--until <date>` | End of the window, exclusive. Defaults to now. |
|
|
351
|
+
| `--host <name>` | Just this host. Defaults to all of them. |
|
|
352
|
+
| `--watch <path>` | Only projects under this root. Repeatable. Defaults to `~/Code`. |
|
|
353
|
+
| `--json` | The full records instead of the summary. |
|
|
354
|
+
|
|
355
|
+
A bare `YYYY-MM-DD` is a **UTC** day boundary, not local midnight — the stored
|
|
356
|
+
timestamps are UTC and an export should mean the same window wherever it runs.
|
|
357
|
+
Pass a full ISO instant (`2026-09-14T09:00:00+02:00`) when you want a different
|
|
358
|
+
edge. The window is half-open: `[since, until)`.
|
|
359
|
+
|
|
360
|
+
`--watch` is also the filter. A prompt in a project outside every watched root
|
|
361
|
+
is not exported, because it has no marker to file it under.
|
|
362
|
+
|
|
363
|
+
With `--json`:
|
|
364
|
+
|
|
365
|
+
```json
|
|
366
|
+
{
|
|
367
|
+
"messages": [
|
|
368
|
+
{
|
|
369
|
+
"host": "agentbox",
|
|
370
|
+
"threadId": "f54ddbd8-92e1-402b-b228-a45e948e2f08",
|
|
371
|
+
"messageId": "8e7ea607-39db-4572-9373-92d3ac764bc7",
|
|
372
|
+
"createdAt": "2026-09-14T10:32:47.421Z",
|
|
373
|
+
"text": "which data is being fetched from /api/data?",
|
|
374
|
+
"workspaceRoot": "/home/agent/Code/@clients/dsl",
|
|
375
|
+
"marker": "@clients/dsl"
|
|
376
|
+
}
|
|
377
|
+
],
|
|
378
|
+
"unreachable": []
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
`text` is the prompt on one line: `<user_query>` wrappers removed, whitespace
|
|
383
|
+
collapsed. `marker` is the project path relative to the watched root it sits
|
|
384
|
+
under — prefixed with the host name for every host but the local one, since two
|
|
385
|
+
machines routinely hold the same repo at the same path.
|
|
386
|
+
|
|
387
|
+
A host that can't be read lands in `unreachable` and the rest still return, so
|
|
388
|
+
one asleep laptop doesn't cost you the export. The exit code is non-zero only
|
|
389
|
+
when *every* host failed.
|
|
390
|
+
|
|
391
|
+
#### How each host is read
|
|
392
|
+
|
|
393
|
+
Same rows either way; only the cost differs.
|
|
394
|
+
|
|
395
|
+
| Host | How | Cost |
|
|
396
|
+
|---|---|---|
|
|
397
|
+
| Origin on loopback | Reads `~/.t3/userdata/state.sqlite` directly | One query |
|
|
398
|
+
| Anything else | Snapshot, then one fetch per thread that could match | N+1 requests |
|
|
399
|
+
|
|
400
|
+
The snapshot is filtered by each thread's `updatedAt` before anything is
|
|
401
|
+
fetched, so a host with hundreds of idle threads still only requests the ones
|
|
402
|
+
active in the window. The database is in WAL mode, so reading it alongside a
|
|
403
|
+
running T3 Code is safe and needs no copy — which matters, because it runs to
|
|
404
|
+
hundreds of megabytes.
|
|
405
|
+
|
|
258
406
|
## Referring to projects and threads
|
|
259
407
|
|
|
260
408
|
You rarely need to paste a UUID.
|
|
@@ -312,13 +460,17 @@ settled shows as `running`.
|
|
|
312
460
|
t3ctl only ever stores an origin string, so **any transport that gives a host a
|
|
313
461
|
reachable URL works.** There's nothing to configure beyond `host add`.
|
|
314
462
|
|
|
463
|
+
- **SSH login** — `t3ctl host add you@box` does everything: server install if
|
|
464
|
+
needed, token minting, and a managed tunnel (see its section above). Good for
|
|
465
|
+
hosts you don't want exposed at all.
|
|
315
466
|
- **Tailscale** — on the host, `npx t3 serve --tailscale-serve` publishes it at
|
|
316
467
|
`https://machine.tailnet.ts.net/`. Register that URL.
|
|
317
468
|
- **LAN** — `npx t3 serve --host 0.0.0.0` (or a specific interface), then register
|
|
318
469
|
`http://192.168.1.x:3773`. Read the URL `t3 serve` prints; it picks another port
|
|
319
470
|
if the default is taken.
|
|
320
|
-
- **SSH port-forward** — `ssh -N -L 3773:localhost:3773 you@box
|
|
321
|
-
`http://localhost:3773`.
|
|
471
|
+
- **Manual SSH port-forward** — `ssh -N -L 3773:localhost:3773 you@box` in a
|
|
472
|
+
terminal you keep open, then register `http://localhost:3773`. The `host add
|
|
473
|
+
you@box` form automates exactly this and keeps the forward alive for you.
|
|
322
474
|
|
|
323
475
|
**One token per host.** Tokens are issued by the server they belong to, so run
|
|
324
476
|
`npx t3 auth session issue --label t3ctl --ttl 30d --token-only` on each machine
|
|
@@ -328,11 +480,12 @@ and give each host its own short name:
|
|
|
328
480
|
t3ctl host add http://localhost:3773 eyJ2Ijox...
|
|
329
481
|
t3ctl host add https://studio.tailnet-1234.ts.net eyJ2Ijox...
|
|
330
482
|
t3ctl host add http://10.0.0.42:3773 eyJ2Ijox...
|
|
483
|
+
t3ctl host add you@elsewhere # token minted for you
|
|
331
484
|
t3ctl ls -t
|
|
332
485
|
```
|
|
333
486
|
|
|
334
|
-
`ls` then fans out to all
|
|
335
|
-
up as `unreachable` and don't block the rest.
|
|
487
|
+
`ls` then fans out to all of them at once. Machines that are asleep or offline
|
|
488
|
+
show up as `unreachable` and don't block the rest.
|
|
336
489
|
|
|
337
490
|
T3 Code's own **T3 Connect relay** (what the mobile app uses when you're off your
|
|
338
491
|
tailnet) is **not planned**: the relay's `dpop-token` exchange only accepts a
|
|
@@ -349,14 +502,16 @@ Worth knowing before you build a workflow on this:
|
|
|
349
502
|
and payloads can change without warning** and a T3 Code update may break t3ctl
|
|
350
503
|
until it catches up.
|
|
351
504
|
- **A host is only reachable while its T3 Code server is running.** t3ctl can't
|
|
352
|
-
wake a machine
|
|
353
|
-
|
|
505
|
+
wake a machine or queue work for later. If the desktop app is closed and no
|
|
506
|
+
`t3 serve` is running, that host is `unreachable` — except an ssh-registered
|
|
507
|
+
host, whose boot service keeps a server running (on macOS only while someone
|
|
508
|
+
is logged in at that console).
|
|
354
509
|
- **`thread create` doesn't run anything.** It leaves an idle thread with no
|
|
355
510
|
messages — a state the desktop UI never produces. Follow it with `thread start`,
|
|
356
511
|
or the thread just sits there.
|
|
357
512
|
- **Not everything the API supports is wired up.** No `pin`, `unsettle`,
|
|
358
|
-
`snooze`/`unsnooze`, no
|
|
359
|
-
|
|
513
|
+
`snooze`/`unsnooze`, no live tailing of a running turn. `export prompts` reads
|
|
514
|
+
your own prompts; nothing reads agent output. `unpin` exists without `pin` because only some of these share a payload
|
|
360
515
|
shape — see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
361
516
|
- **`ls` fetches full snapshots.** Fine interactively; too heavy to poll in a
|
|
362
517
|
loop.
|