@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.
- package/README.md +39 -8
- package/package.json +1 -1
- 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
|
|
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
|
|
179
|
+
### `t3ctl thread send <thread> <message...>`
|
|
180
180
|
|
|
181
|
-
Send a message and run the agent.
|
|
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
|
|
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.
|
|
199
|
-
|
|
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
|
|
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
|
|
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
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
|
-
|
|
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
|
-
//
|
|
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>` + (
|
|
375
|
-
if (
|
|
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
|
|
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)`);
|