@gobius/t3ctl 0.4.0 → 0.6.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 CHANGED
@@ -113,8 +113,48 @@ t3ctl warns you when:
113
113
  The token is optional so you can register a host before minting one, but reads
114
114
  will fail until you add it.
115
115
 
116
- The older `t3ctl host add <name> <origin> <token>` form still works and prints a
117
- deprecation notice.
116
+ ### `t3ctl host add <ssh-target> [--name <name>] [--ttl <duration>]`
117
+
118
+ Register a host given **only its ssh login** — no URL, no token, no port:
119
+
120
+ ```sh
121
+ t3ctl host add agent@goobles-agentbox
122
+ ```
123
+
124
+ That one command bootstraps the machine end to end:
125
+
126
+ 1. **Probes it over ssh** for a running T3 Code server. The server records its
127
+ port in `~/.t3/userdata/server-runtime.json` on the remote, so the port is
128
+ discovered, never assumed (it is 3773 or whatever free port the server fell
129
+ back to).
130
+ 2. **Installs the server if none is running** — `t3 service install`, T3 Code's
131
+ own per-user launchd/systemd service (no sudo; macOS and Linux). The exact
132
+ `t3` CLI version is resolved locally via npm (`latest`, falling back to
133
+ `nightly`); override with `--t3-version <version-or-tag>`. The remote npm
134
+ output streams past — a cold cache can download for a few minutes.
135
+ 3. **Mints a token for you** — `t3 auth session issue` on the remote, labeled
136
+ `t3ctl:<name>`, TTL 30d by default (`--ttl` changes it). The session id is
137
+ printed along with the exact revoke command for that host.
138
+ 4. **Tunnels to it** — an ssh ControlMaster forwards a local port to the remote
139
+ server (loopback-only by design; nothing is exposed on the remote's network).
140
+ The master outlives t3ctl, and every command silently rebuilds it when it
141
+ dies or the remote port changed.
142
+
143
+ Registering again for the same login is idempotent: it refreshes the tunnel and
144
+ reuses the stored token unless the login now lands on a *different machine*
145
+ (different `environmentId`), in which case a fresh token is minted.
146
+
147
+ Give it a name with `--name` (otherwise it is named after the remote's own
148
+ machine label, same as the origin form). Notes:
149
+
150
+ - `t3ctl host rm <name>` tears the tunnel down with the entry.
151
+ - **macOS remotes need someone logged in at the console** — a launchd agent
152
+ starts at login, so an ssh install with nobody logged in installs fine but
153
+ cannot start the server. Linux (systemd user service) has no such gap.
154
+ - If the remote lacks node for non-interactive shells, install Node there (a
155
+ version manager may need configuring for non-login shells); if a native
156
+ dependency fails to build, a C toolchain is missing (`build-essential` /
157
+ `gcc-c++` / `xcode-select --install`).
118
158
 
119
159
  ### `t3ctl host rm <name>`
120
160
 
@@ -255,6 +295,82 @@ t3ctl thread delete "scratch experiment"
255
295
 
256
296
  `delete` is not prompted and not undoable from t3ctl — check with `ls -t` first.
257
297
 
298
+ ### `t3ctl export prompts --since <date> [--until <date>]`
299
+
300
+ Every prompt you typed in a window, across every registered host, with the
301
+ project each one happened in. Read-only, and built for other tools to consume
302
+ rather than for reading yourself — time trackers, activity logs, weeknotes.
303
+
304
+ ```sh
305
+ t3ctl export prompts --since 2026-09-14 --until 2026-09-15
306
+ ```
307
+ ```
308
+ 29 prompts 2026-09-14T00:00:00.000Z -> 2026-09-15T00:00:00.000Z
309
+
310
+ agentbox 29 local state.sqlite
311
+ @clients/dsl 28
312
+ t3ctl 1
313
+ ```
314
+
315
+ | Option | |
316
+ |---|---|
317
+ | `--since <date>` | Required. Start of the window, inclusive. |
318
+ | `--until <date>` | End of the window, exclusive. Defaults to now. |
319
+ | `--host <name>` | Just this host. Defaults to all of them. |
320
+ | `--watch <path>` | Only projects under this root. Repeatable. Defaults to `~/Code`. |
321
+ | `--json` | The full records instead of the summary. |
322
+
323
+ A bare `YYYY-MM-DD` is a **UTC** day boundary, not local midnight — the stored
324
+ timestamps are UTC and an export should mean the same window wherever it runs.
325
+ Pass a full ISO instant (`2026-09-14T09:00:00+02:00`) when you want a different
326
+ edge. The window is half-open: `[since, until)`.
327
+
328
+ `--watch` is also the filter. A prompt in a project outside every watched root
329
+ is not exported, because it has no marker to file it under.
330
+
331
+ With `--json`:
332
+
333
+ ```json
334
+ {
335
+ "messages": [
336
+ {
337
+ "host": "agentbox",
338
+ "threadId": "f54ddbd8-92e1-402b-b228-a45e948e2f08",
339
+ "messageId": "8e7ea607-39db-4572-9373-92d3ac764bc7",
340
+ "createdAt": "2026-09-14T10:32:47.421Z",
341
+ "text": "which data is being fetched from /api/data?",
342
+ "workspaceRoot": "/home/agent/Code/@clients/dsl",
343
+ "marker": "@clients/dsl"
344
+ }
345
+ ],
346
+ "unreachable": []
347
+ }
348
+ ```
349
+
350
+ `text` is the prompt on one line: `<user_query>` wrappers removed, whitespace
351
+ collapsed. `marker` is the project path relative to the watched root it sits
352
+ under — prefixed with the host name for every host but the local one, since two
353
+ machines routinely hold the same repo at the same path.
354
+
355
+ A host that can't be read lands in `unreachable` and the rest still return, so
356
+ one asleep laptop doesn't cost you the export. The exit code is non-zero only
357
+ when *every* host failed.
358
+
359
+ #### How each host is read
360
+
361
+ Same rows either way; only the cost differs.
362
+
363
+ | Host | How | Cost |
364
+ |---|---|---|
365
+ | Origin on loopback | Reads `~/.t3/userdata/state.sqlite` directly | One query |
366
+ | Anything else | Snapshot, then one fetch per thread that could match | N+1 requests |
367
+
368
+ The snapshot is filtered by each thread's `updatedAt` before anything is
369
+ fetched, so a host with hundreds of idle threads still only requests the ones
370
+ active in the window. The database is in WAL mode, so reading it alongside a
371
+ running T3 Code is safe and needs no copy — which matters, because it runs to
372
+ hundreds of megabytes.
373
+
258
374
  ## Referring to projects and threads
259
375
 
260
376
  You rarely need to paste a UUID.
@@ -312,13 +428,17 @@ settled shows as `running`.
312
428
  t3ctl only ever stores an origin string, so **any transport that gives a host a
313
429
  reachable URL works.** There's nothing to configure beyond `host add`.
314
430
 
431
+ - **SSH login** — `t3ctl host add you@box` does everything: server install if
432
+ needed, token minting, and a managed tunnel (see its section above). Good for
433
+ hosts you don't want exposed at all.
315
434
  - **Tailscale** — on the host, `npx t3 serve --tailscale-serve` publishes it at
316
435
  `https://machine.tailnet.ts.net/`. Register that URL.
317
436
  - **LAN** — `npx t3 serve --host 0.0.0.0` (or a specific interface), then register
318
437
  `http://192.168.1.x:3773`. Read the URL `t3 serve` prints; it picks another port
319
438
  if the default is taken.
320
- - **SSH port-forward** — `ssh -N -L 3773:localhost:3773 you@box`, then register
321
- `http://localhost:3773`. Good for hosts you don't want exposed at all.
439
+ - **Manual SSH port-forward** — `ssh -N -L 3773:localhost:3773 you@box` in a
440
+ terminal you keep open, then register `http://localhost:3773`. The `host add
441
+ you@box` form automates exactly this and keeps the forward alive for you.
322
442
 
323
443
  **One token per host.** Tokens are issued by the server they belong to, so run
324
444
  `npx t3 auth session issue --label t3ctl --ttl 30d --token-only` on each machine
@@ -328,11 +448,12 @@ and give each host its own short name:
328
448
  t3ctl host add http://localhost:3773 eyJ2Ijox...
329
449
  t3ctl host add https://studio.tailnet-1234.ts.net eyJ2Ijox...
330
450
  t3ctl host add http://10.0.0.42:3773 eyJ2Ijox...
451
+ t3ctl host add you@elsewhere # token minted for you
331
452
  t3ctl ls -t
332
453
  ```
333
454
 
334
- `ls` then fans out to all three at once. Machines that are asleep or offline show
335
- up as `unreachable` and don't block the rest.
455
+ `ls` then fans out to all of them at once. Machines that are asleep or offline
456
+ show up as `unreachable` and don't block the rest.
336
457
 
337
458
  T3 Code's own **T3 Connect relay** (what the mobile app uses when you're off your
338
459
  tailnet) is **not planned**: the relay's `dpop-token` exchange only accepts a
@@ -349,14 +470,16 @@ Worth knowing before you build a workflow on this:
349
470
  and payloads can change without warning** and a T3 Code update may break t3ctl
350
471
  until it catches up.
351
472
  - **A host is only reachable while its T3 Code server is running.** t3ctl can't
352
- wake a machine, launch a server, or queue work for later. If the desktop app is
353
- closed and no `t3 serve` is running, that host is `unreachable`.
473
+ wake a machine or queue work for later. If the desktop app is closed and no
474
+ `t3 serve` is running, that host is `unreachable` — except an ssh-registered
475
+ host, whose boot service keeps a server running (on macOS only while someone
476
+ is logged in at that console).
354
477
  - **`thread create` doesn't run anything.** It leaves an idle thread with no
355
478
  messages — a state the desktop UI never produces. Follow it with `thread start`,
356
479
  or the thread just sits there.
357
480
  - **Not everything the API supports is wired up.** No `pin`, `unsettle`,
358
- `snooze`/`unsnooze`, no reading message content, no live tailing of a running
359
- turn. `unpin` exists without `pin` because only some of these share a payload
481
+ `snooze`/`unsnooze`, no live tailing of a running turn. `export prompts` reads
482
+ your own prompts; nothing reads agent output. `unpin` exists without `pin` because only some of these share a payload
360
483
  shape — see [CONTRIBUTING.md](CONTRIBUTING.md).
361
484
  - **`ls` fetches full snapshots.** Fine interactively; too heavy to poll in a
362
485
  loop.