@gobius/t3ctl 0.3.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +39 -8
  2. package/package.json +1 -1
  3. package/t3ctl.mjs +82 -5
package/README.md CHANGED
@@ -156,7 +156,7 @@ t3ctl project create t3ctl ~/Code/t3ctl
156
156
  ### `t3ctl thread create <project> <title>`
157
157
 
158
158
  Create a thread. This produces an **idle thread with no messages** — it does not
159
- start the agent. Use `thread start` for that.
159
+ start the agent. Use `thread send` for that.
160
160
 
161
161
  ```sh
162
162
  t3ctl thread create t3ctl "rewrite the readme for users" --branch docs/readme
@@ -176,13 +176,15 @@ Flags:
176
176
  `--model` splits on the **first** slash, so slashed model names work as-is:
177
177
  `--model opencode/github-copilot/gpt-5.4`.
178
178
 
179
- ### `t3ctl thread start <thread> <message...>`
179
+ ### `t3ctl thread send <thread> <message...>`
180
180
 
181
- Send a message and run the agent. Everything after the thread reference is the
181
+ Send a message to a thread and run the agent. This is how you continue a
182
+ conversation — it is the same thing the app does for every message you type,
183
+ first or fiftieth. Aliased as `thread start`, the name it shipped under in 0.2.0. Everything after the thread reference is the
182
184
  message — no quoting needed.
183
185
 
184
186
  ```sh
185
- t3ctl thread start "rewrite the readme" move the endpoint tables into CONTRIBUTING.md
187
+ t3ctl thread send "rewrite the readme" move the endpoint tables into CONTRIBUTING.md
186
188
  ```
187
189
 
188
190
  ```
@@ -195,13 +197,42 @@ started rewrite the readme for users
195
197
 
196
198
  Accepts `--model`, `--runtime-mode`, `--interaction-mode`, and `--host`. Unlike
197
199
  `thread create`, `--model` has no default here: the thread's existing model is
198
- reused unless you override it. Use `--interaction-mode plan` to make the agent
199
- plan instead of edit:
200
+ reused unless you override it.
201
+
202
+ ### `t3ctl thread rename <thread> <new title...>`
203
+
204
+ Rename a thread. Useful when a thread was created from a script and ended up with
205
+ a title like a filename.
206
+
207
+ ```sh
208
+ t3ctl thread rename "probe well-known" Verify hosts with the environment descriptor
209
+ ```
210
+
211
+ ### `t3ctl thread retitle <thread> [--timeout <seconds>]`
212
+
213
+ Ask the server to derive a title from the thread's own content, instead of
214
+ supplying one.
200
215
 
201
216
  ```sh
202
- t3ctl thread start "flaky release workflow" --interaction-mode plan why does the tag job race?
217
+ t3ctl thread retitle "probe well-known"
218
+ ```
219
+
220
+ This is a **request, not a guarantee**. The server records the intent and expects
221
+ something downstream to generate the title; in practice it sometimes clears the
222
+ request a few seconds later without producing one, and reports no error when it
223
+ does. So `retitle` watches the thread afterwards and tells you which happened:
224
+
225
+ ```
226
+ asked the server to retitle Build t3ctl: a CLI for controlling T3 Code hosts (seq 677390)
227
+ no title was generated — the server cleared the request without producing one.
228
+ The title is still "Build t3ctl: a CLI for controlling T3 Code hosts". Set one directly:
229
+ t3ctl thread rename 861645a3-... <title...>
203
230
  ```
204
231
 
232
+ It exits non-zero when no title appears, so a script can tell. Waits 30s by
233
+ default; `--timeout` changes that. If you need the rename to be certain, use
234
+ `thread rename`, which sets the title directly.
235
+
205
236
  ### `t3ctl thread interrupt <thread>`
206
237
 
207
238
  Stop the turn that's currently running.
@@ -249,7 +280,7 @@ Write commands act on one host. With a single host registered, that one is
249
280
  implied. With more than one, pass `--host`:
250
281
 
251
282
  ```sh
252
- t3ctl thread start --host desktop "api rate limits" pick this back up
283
+ t3ctl thread send --host desktop "api rate limits" pick this back up
253
284
  ```
254
285
 
255
286
  Otherwise you get `multiple hosts; pass --host <laptop|desktop>`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobius/t3ctl",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Controller CLI for T3 Code hosts — list and drive threads across machines",
6
6
  "keywords": [
package/t3ctl.mjs CHANGED
@@ -364,15 +364,88 @@ const cmdProject = async (args) => {
364
364
  console.log(`created project ${bold(title)} on ${host.name}\n id ${projectId}\n root ${workspaceRoot}\n seq ${sequence}`);
365
365
  };
366
366
 
367
+
368
+ // thread.meta.update also accepts regenerateTitle:true, which asks the server to
369
+ // derive a title from the thread's own content instead of taking one from us.
370
+ const cmdThreadRename = async (thread, host, title) => {
371
+ const { sequence } = await dispatch(host, {
372
+ type: 'thread.meta.update', commandId: crypto.randomUUID(), threadId: thread.id, title,
373
+ });
374
+ console.log(`renamed ${dim(thread.title || thread.id)} -> ${bold(title)}\n seq ${sequence}`);
375
+ };
376
+
377
+ // Lighter than the full snapshot; retitle polls this so it does not refetch every
378
+ // thread's history once a second.
379
+ const threadDetail = async (host, threadId) => {
380
+ const res = await fetch(`${host.origin}/api/orchestration/threads/${threadId}?turnLimit=1`, {
381
+ headers: { authorization: `Bearer ${host.token}` },
382
+ signal: AbortSignal.timeout(host.timeoutMs ?? 15000),
383
+ });
384
+ if (!res.ok) throw new Error(`${host.name}: HTTP ${res.status}`);
385
+ return (await res.json()).thread;
386
+ };
387
+
388
+ // `regenerateTitle` does NOT rename anything by itself. The server records an
389
+ // intent marker (`titleRegeneration: {requestId, startedAt}`) and expects something
390
+ // downstream to generate a title and write it back. Observed on a live server: the
391
+ // marker is sometimes cleared a few seconds later with the title untouched, and no
392
+ // error is reported anywhere — not in the event, the command receipt, or the server
393
+ // trace. So we dispatch, then watch, and say what actually happened.
394
+ const cmdThreadRetitle = async (thread, host, timeoutSeconds) => {
395
+ const before = thread.title;
396
+ const { sequence } = await dispatch(host, {
397
+ type: 'thread.meta.update', commandId: crypto.randomUUID(), threadId: thread.id,
398
+ regenerateTitle: true,
399
+ });
400
+ console.log(`asked the server to retitle ${bold(before || thread.id)}${dim(` (seq ${sequence})`)}`);
401
+
402
+ const deadline = Date.now() + timeoutSeconds * 1000;
403
+ let sawMarker = false;
404
+ while (Date.now() < deadline) {
405
+ await new Promise((r) => setTimeout(r, 1500));
406
+ const current = await threadDetail(host, thread.id);
407
+ if (current.title && current.title !== before) {
408
+ console.log(`retitled -> ${bold(current.title)}`);
409
+ return;
410
+ }
411
+ if (current.titleRegeneration) { sawMarker = true; continue; }
412
+ if (sawMarker) break; // marker appeared and was cleared, title unchanged
413
+ }
414
+
415
+ console.error(
416
+ `\x1b[33mno title was generated\x1b[0m — the server ${sawMarker ? 'cleared the request without producing one' : `did not act on it within ${timeoutSeconds}s`}.\n` +
417
+ ` The title is still "${before}". Set one directly:\n` +
418
+ ` t3ctl thread rename ${thread.id} <title...>`,
419
+ );
420
+ process.exitCode = 1;
421
+ };
422
+
367
423
  const cmdThread = async (args) => {
368
424
  const [sub, ...rest] = args;
369
425
  const { flags, pos } = parseArgs(rest);
370
- if (sub === 'start' || sub === 'interrupt') {
426
+ // `send` is the accurate name: thread.turn.start posts a message to a thread and
427
+ // runs a turn, which is exactly what the UI does for every message, first or not.
428
+ // `start` stays as an alias because it shipped in 0.2.0.
429
+ const isSend = sub === 'send' || sub === 'start';
430
+
431
+ if (sub === 'rename' || sub === 'retitle') {
432
+ const [ref, ...titleParts] = pos;
433
+ if (!ref) return usage(`usage: t3ctl thread ${sub} <thread>` + (sub === 'rename' ? ' <new title...>' : ''));
434
+ const title = titleParts.join(' ');
435
+ if (sub === 'rename' && !title) return usage('usage: t3ctl thread rename <thread> <new title...>');
436
+ const host = pickHost(flags);
437
+ const target = resolveThread(await snapshot(host), ref);
438
+ return sub === 'retitle'
439
+ ? cmdThreadRetitle(target, host, Number(flags.timeout) > 0 ? Number(flags.timeout) : 30)
440
+ : cmdThreadRename(target, host, title);
441
+ }
442
+
443
+ if (isSend || sub === 'interrupt') {
371
444
  // Validate arguments before touching the host registry, so `t3ctl thread
372
- // start` with no args prints usage instead of "no hosts registered".
445
+ // send` with no args prints usage instead of "no hosts registered".
373
446
  const [ref, ...rest2] = pos;
374
- if (!ref) return usage(`usage: t3ctl thread ${sub} <thread>` + (sub === 'start' ? ' <message...>' : ''));
375
- if (sub === 'start' && rest2.length === 0) return usage('usage: t3ctl thread start <thread> <message...>');
447
+ if (!ref) return usage(`usage: t3ctl thread ${sub} <thread>` + (isSend ? ' <message...>' : ''));
448
+ if (isSend && rest2.length === 0) return usage(`usage: t3ctl thread ${sub} <thread> <message...>`);
376
449
  const host = pickHost(flags);
377
450
  const thread = resolveThread(await snapshot(host), ref);
378
451
  if (sub === 'interrupt') return cmdThreadInterrupt(thread, host);
@@ -429,7 +502,11 @@ if (!commands[cmd]) {
429
502
  t3ctl project create <title> <root> create a project for an existing dir
430
503
  t3ctl thread create <project> <title> start a thread (--model inst/model,
431
504
  --branch, --host)
432
- t3ctl thread start <thread> <message...> send a message and run the agent
505
+ t3ctl thread send <thread> <message...> send a message and run the agent
506
+ (alias: start)
507
+ t3ctl thread rename <thread> <title...> rename a thread
508
+ t3ctl thread retitle <thread> [--timeout n] ask the server to derive a title
509
+ (warns if it produces none)
433
510
  t3ctl thread interrupt <thread> stop the running turn
434
511
  t3ctl thread settle <thread> settle a thread (also: archive,
435
512
  unarchive, unpin, delete)`);