@gobius/t3ctl 0.4.0 → 0.5.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/dist/t3ctl.js +557 -0
- package/dist/t3ctl.js.map +1 -0
- package/package.json +15 -3
- package/t3ctl.mjs +0 -520
package/dist/t3ctl.js
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// t3ctl — a controller CLI for T3 Code hosts.
|
|
3
|
+
// Peer of the mobile app: pairs once per host, then reads/controls remotely.
|
|
4
|
+
// Spike scope: host registry + read-only listing.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
const CONFIG_DIR = path.join(os.homedir(), '.config', 't3ctl');
|
|
10
|
+
const HOSTS_FILE = path.join(CONFIG_DIR, 'hosts.json');
|
|
11
|
+
const readHosts = () => {
|
|
12
|
+
if (!fs.existsSync(HOSTS_FILE))
|
|
13
|
+
return [];
|
|
14
|
+
return JSON.parse(fs.readFileSync(HOSTS_FILE, 'utf8')).hosts ?? [];
|
|
15
|
+
};
|
|
16
|
+
const writeHosts = (hosts) => {
|
|
17
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
18
|
+
fs.writeFileSync(HOSTS_FILE, JSON.stringify({ hosts }, null, 2), { mode: 0o600 });
|
|
19
|
+
};
|
|
20
|
+
const snapshot = async (host) => {
|
|
21
|
+
const res = await fetch(`${host.origin}/api/orchestration/snapshot`, {
|
|
22
|
+
headers: host.token ? { authorization: `Bearer ${host.token}` } : {},
|
|
23
|
+
signal: AbortSignal.timeout(host.timeoutMs ?? 15000),
|
|
24
|
+
});
|
|
25
|
+
if (!res.ok)
|
|
26
|
+
throw new Error(`${host.name}: HTTP ${res.status} ${await res.text().catch(() => '')}`.trim());
|
|
27
|
+
return (await res.json());
|
|
28
|
+
};
|
|
29
|
+
// Derived status. Order matters: most urgent wins.
|
|
30
|
+
const threadStatus = (t) => {
|
|
31
|
+
if (t.deletedAt)
|
|
32
|
+
return 'deleted';
|
|
33
|
+
if (t.archivedAt)
|
|
34
|
+
return 'archived';
|
|
35
|
+
if (t.session?.activeTurnId || t.session?.status === 'running')
|
|
36
|
+
return 'running';
|
|
37
|
+
if (t.session?.status === 'error' || t.latestTurn?.state === 'error')
|
|
38
|
+
return 'error';
|
|
39
|
+
if (t.snoozedUntil && new Date(t.snoozedUntil) > new Date())
|
|
40
|
+
return 'snoozed';
|
|
41
|
+
if (t.proposedPlans?.length)
|
|
42
|
+
return 'needs-review';
|
|
43
|
+
if (t.settledAt && !t.unsettledAt)
|
|
44
|
+
return 'settled';
|
|
45
|
+
return 'idle';
|
|
46
|
+
};
|
|
47
|
+
const ICON = {
|
|
48
|
+
running: '\x1b[32m●\x1b[0m', error: '\x1b[31m✕\x1b[0m', 'needs-review': '\x1b[33m◆\x1b[0m',
|
|
49
|
+
snoozed: '\x1b[90m☾\x1b[0m', settled: '\x1b[90m✓\x1b[0m', idle: '\x1b[90m·\x1b[0m',
|
|
50
|
+
archived: '\x1b[90m▪\x1b[0m', deleted: '\x1b[90m✗\x1b[0m',
|
|
51
|
+
};
|
|
52
|
+
/** `catch` binds `unknown`; every call site wants the same string out of it. */
|
|
53
|
+
const errorMessage = (e) => (e instanceof Error ? e.message : String(e));
|
|
54
|
+
const dim = (s) => `\x1b[90m${s}\x1b[0m`;
|
|
55
|
+
// Usage errors are user errors: print to stderr and exit non-zero so scripts
|
|
56
|
+
// can tell them apart from success.
|
|
57
|
+
const usage = (message) => {
|
|
58
|
+
console.error(message);
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
};
|
|
61
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
62
|
+
const collect = async (hosts) => {
|
|
63
|
+
const results = await Promise.allSettled(hosts.map(async (h) => ({ host: h, snap: await snapshot(h) })));
|
|
64
|
+
const ok = [];
|
|
65
|
+
const failed = [];
|
|
66
|
+
results.forEach((r, i) => {
|
|
67
|
+
const host = hosts[i];
|
|
68
|
+
if (!host)
|
|
69
|
+
return;
|
|
70
|
+
if (r.status === 'fulfilled')
|
|
71
|
+
ok.push(r.value);
|
|
72
|
+
else
|
|
73
|
+
failed.push({ host, error: errorMessage(r.reason) });
|
|
74
|
+
});
|
|
75
|
+
return { ok, failed };
|
|
76
|
+
};
|
|
77
|
+
const cmdLs = async ({ threads: showThreads, all: showAll, json: asJson }) => {
|
|
78
|
+
const hosts = readHosts();
|
|
79
|
+
if (!hosts.length)
|
|
80
|
+
return console.error('No hosts registered. Run: t3ctl host add <origin> <token>');
|
|
81
|
+
const { ok, failed } = await collect(hosts);
|
|
82
|
+
if (asJson) {
|
|
83
|
+
const out = ok.flatMap(({ host, snap }) => snap.projects
|
|
84
|
+
.filter((p) => showAll || !p.deletedAt)
|
|
85
|
+
.map((p) => ({
|
|
86
|
+
host: host.name, id: p.id, title: p.title, workspaceRoot: p.workspaceRoot,
|
|
87
|
+
threads: snap.threads.filter((t) => t.projectId === p.id)
|
|
88
|
+
.filter((t) => showAll || (!t.deletedAt && !t.archivedAt))
|
|
89
|
+
.map((t) => ({ id: t.id, title: t.title, branch: t.branch, status: threadStatus(t), provider: t.session?.providerName ?? null, updatedAt: t.updatedAt })),
|
|
90
|
+
})));
|
|
91
|
+
console.log(JSON.stringify({ projects: out, unreachable: failed.map((f) => ({ host: f.host.name, error: f.error })) }, null, 2));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
for (const { host, snap } of ok) {
|
|
95
|
+
console.log(`\n${bold(host.name)} ${dim(host.origin)}`);
|
|
96
|
+
const projects = snap.projects.filter((p) => showAll || !p.deletedAt)
|
|
97
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
98
|
+
for (const p of projects) {
|
|
99
|
+
const threads = snap.threads.filter((t) => t.projectId === p.id)
|
|
100
|
+
.filter((t) => showAll || (!t.deletedAt && !t.archivedAt))
|
|
101
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
102
|
+
if (!threads.length && !showAll)
|
|
103
|
+
continue;
|
|
104
|
+
const counts = {};
|
|
105
|
+
for (const t of threads)
|
|
106
|
+
counts[threadStatus(t)] = (counts[threadStatus(t)] ?? 0) + 1;
|
|
107
|
+
const badge = Object.entries(counts).map(([k, v]) => `${ICON[k] ?? '?'}${v}`).join(' ');
|
|
108
|
+
console.log(` ${bold(p.title)} ${badge} ${dim(p.workspaceRoot.replace(os.homedir(), '~'))}`);
|
|
109
|
+
if (!showThreads)
|
|
110
|
+
continue;
|
|
111
|
+
for (const t of threads) {
|
|
112
|
+
const s = threadStatus(t);
|
|
113
|
+
const meta = [t.branch, t.session?.providerName].filter(Boolean).join(' ');
|
|
114
|
+
console.log(` ${ICON[s] ?? '?'} ${(t.title || '(untitled)').slice(0, 62).padEnd(62)} ${dim(meta)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const f of failed)
|
|
119
|
+
console.error(`\n\x1b[31munreachable\x1b[0m ${f.host.name}: ${f.error}`);
|
|
120
|
+
};
|
|
121
|
+
// ---- host registry ------------------------------------------------------
|
|
122
|
+
// The descriptor at /.well-known/t3/environment is UNAUTHENTICATED, so probing
|
|
123
|
+
// it answers "is anyone home, and is it T3 Code?" without a token — a wrong
|
|
124
|
+
// origin fails here instead of as a baffling 401 on the first real call.
|
|
125
|
+
// Schema: ExecutionEnvironmentDescriptor in packages/contracts/src/environment.ts.
|
|
126
|
+
const DESCRIPTOR_PATH = '/.well-known/t3/environment';
|
|
127
|
+
const probe = async (origin, timeoutMs = 5000) => {
|
|
128
|
+
let res;
|
|
129
|
+
try {
|
|
130
|
+
res = await fetch(`${origin}${DESCRIPTOR_PATH}`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
// Node's fetch reports a bare "fetch failed"; the cause carries the real reason.
|
|
134
|
+
const e = error;
|
|
135
|
+
const why = e.name === 'TimeoutError' ? `no response in ${timeoutMs}ms`
|
|
136
|
+
: (e.cause?.message ?? e.message ?? String(error));
|
|
137
|
+
throw new Error(`cannot reach ${origin}: ${why}`);
|
|
138
|
+
}
|
|
139
|
+
const notT3 = (why) => new Error(`not a T3 Code server (${DESCRIPTOR_PATH} ${why})`);
|
|
140
|
+
if (!res.ok)
|
|
141
|
+
throw notT3(`returned HTTP ${res.status}`);
|
|
142
|
+
let d;
|
|
143
|
+
try {
|
|
144
|
+
d = (await res.json());
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
throw notT3('is not JSON');
|
|
148
|
+
}
|
|
149
|
+
const required = ['environmentId', 'label', 'serverVersion'];
|
|
150
|
+
const missing = required.filter((k) => typeof d[k] !== 'string' || !d[k]);
|
|
151
|
+
if (missing.length)
|
|
152
|
+
throw notT3(`is missing ${missing.join(', ')}`);
|
|
153
|
+
return {
|
|
154
|
+
environmentId: d['environmentId'],
|
|
155
|
+
label: d['label'],
|
|
156
|
+
serverVersion: d['serverVersion'],
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
const shortId = (id) => (id ? id.slice(0, 8) : '-');
|
|
160
|
+
const warn = (message) => console.error(`\x1b[33mwarning\x1b[0m ${message}`);
|
|
161
|
+
// The name is what you type in --host, so derive a typeable slug from the label
|
|
162
|
+
// rather than using the label verbatim.
|
|
163
|
+
const slugify = (label) => label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'host';
|
|
164
|
+
const uniqueName = (base, hosts) => {
|
|
165
|
+
if (!hosts.some((h) => h.name === base))
|
|
166
|
+
return base;
|
|
167
|
+
for (let n = 2;; n++)
|
|
168
|
+
if (!hosts.some((h) => h.name === `${base}-${n}`))
|
|
169
|
+
return `${base}-${n}`;
|
|
170
|
+
};
|
|
171
|
+
const isOrigin = (value) => /^https?:\/\//i.test(value ?? '');
|
|
172
|
+
const parseHostAdd = (pos) => {
|
|
173
|
+
const [origin, token] = pos;
|
|
174
|
+
return origin && isOrigin(origin) ? { origin, token: token ?? null } : null;
|
|
175
|
+
};
|
|
176
|
+
const HOST_ADD_USAGE = 'usage: t3ctl host add <origin> [token] [--name <name>]\n' +
|
|
177
|
+
' an origin needs a scheme, e.g. http://localhost:3773';
|
|
178
|
+
const cmdHostAdd = async (pos, flags, hosts) => {
|
|
179
|
+
// Validate before any network or registry work so a bare `host add` prints
|
|
180
|
+
// usage instead of stalling on a probe.
|
|
181
|
+
const parsed = parseHostAdd(pos);
|
|
182
|
+
if (!parsed)
|
|
183
|
+
return usage(HOST_ADD_USAGE);
|
|
184
|
+
const origin = parsed.origin.replace(/\/$/, '');
|
|
185
|
+
const descriptor = await probe(origin);
|
|
186
|
+
const existing = hosts.find((h) => h.origin === origin);
|
|
187
|
+
// A changed environmentId on a known origin means the origin now points at a
|
|
188
|
+
// different machine — the stored token almost certainly belongs to the old one.
|
|
189
|
+
if (existing?.environmentId && existing.environmentId !== descriptor.environmentId) {
|
|
190
|
+
warn(`${origin} is now a DIFFERENT environment\n` +
|
|
191
|
+
` was ${existing.environmentId} (${existing.label ?? 'unknown'})\n` +
|
|
192
|
+
` now ${descriptor.environmentId} (${descriptor.label})\n` +
|
|
193
|
+
` the token stored for "${existing.name}" was issued by the old one and will likely fail`);
|
|
194
|
+
}
|
|
195
|
+
const name = flags.name ?? existing?.name ??
|
|
196
|
+
uniqueName(slugify(descriptor.label), hosts);
|
|
197
|
+
const token = parsed.token ?? existing?.token ?? null;
|
|
198
|
+
const others = hosts.filter((h) => h.name !== name && h.origin !== origin && h.serverVersion);
|
|
199
|
+
const skewed = [...new Set(others.flatMap((h) => (h.serverVersion ? [h.serverVersion] : [])))]
|
|
200
|
+
.filter((v) => v !== descriptor.serverVersion);
|
|
201
|
+
if (skewed.length)
|
|
202
|
+
warn(`serverVersion ${descriptor.serverVersion} differs from other hosts: ${skewed.join(', ')}`);
|
|
203
|
+
writeHosts(hosts.filter((h) => h.name !== name && h.origin !== origin).concat({
|
|
204
|
+
name, origin, token,
|
|
205
|
+
environmentId: descriptor.environmentId,
|
|
206
|
+
label: descriptor.label,
|
|
207
|
+
serverVersion: descriptor.serverVersion,
|
|
208
|
+
}));
|
|
209
|
+
console.log(`added ${bold(name)} -> ${origin}\n label ${descriptor.label}\n` +
|
|
210
|
+
` env ${descriptor.environmentId}\n version ${descriptor.serverVersion}`);
|
|
211
|
+
if (!token)
|
|
212
|
+
warn(`no token stored for ${name} — reads will fail until you run: t3ctl host add ${origin} <token>`);
|
|
213
|
+
};
|
|
214
|
+
const cmdHostsList = async (hosts) => {
|
|
215
|
+
if (!hosts.length)
|
|
216
|
+
return console.log('(no hosts)');
|
|
217
|
+
const probes = await Promise.allSettled(hosts.map((h) => probe(h.origin)));
|
|
218
|
+
const drifted = [];
|
|
219
|
+
hosts.forEach((h, i) => {
|
|
220
|
+
const p = probes[i];
|
|
221
|
+
const live = p?.status === 'fulfilled' ? p.value : null;
|
|
222
|
+
if (live && h.environmentId && h.environmentId !== live.environmentId)
|
|
223
|
+
drifted.push({ h, live });
|
|
224
|
+
const icon = live ? ICON.running : ICON.error;
|
|
225
|
+
const label = live?.label ?? h.label ?? '-';
|
|
226
|
+
const env = shortId(live?.environmentId ?? h.environmentId);
|
|
227
|
+
const version = live?.serverVersion ?? h.serverVersion ?? '-';
|
|
228
|
+
// Values for an unreachable host are whatever was last stored, so dim the
|
|
229
|
+
// whole row to keep remembered data visually distinct from probed data.
|
|
230
|
+
const cell = (text, width) => (live ? text.padEnd(width) : dim(text.padEnd(width)));
|
|
231
|
+
console.log(`${icon} ${live ? bold(h.name.padEnd(14)) : dim(h.name.padEnd(14))} ${cell(label, 18)} ${dim(env.padEnd(9))} ${cell(version, 28)} ${dim(h.origin)}` +
|
|
232
|
+
(live || !p || p.status !== 'rejected' ? '' : ` ${dim(errorMessage(p.reason))}`));
|
|
233
|
+
});
|
|
234
|
+
for (const { h, live } of drifted) {
|
|
235
|
+
warn(`${h.name} (${h.origin}) is now a DIFFERENT environment\n` +
|
|
236
|
+
` was ${h.environmentId} (${h.label ?? 'unknown'})\n now ${live.environmentId} (${live.label})`);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
// ---- writes -------------------------------------------------------------
|
|
240
|
+
// Commands are dispatched directly (no envelope). The CLIENT mints every id;
|
|
241
|
+
// commandId is the idempotency key, so retries are safe.
|
|
242
|
+
// Schemas: packages/contracts/src/orchestration.ts in pingdotgg/t3code.
|
|
243
|
+
const pickHost = (flags) => {
|
|
244
|
+
const hosts = readHosts();
|
|
245
|
+
if (flags.host) {
|
|
246
|
+
const h = hosts.find((x) => x.name === flags.host);
|
|
247
|
+
if (!h)
|
|
248
|
+
throw new Error(`no such host: ${flags.host}`);
|
|
249
|
+
return h;
|
|
250
|
+
}
|
|
251
|
+
if (!hosts.length)
|
|
252
|
+
throw new Error('no hosts registered — run: t3ctl host add <origin> <token>');
|
|
253
|
+
if (hosts.length > 1)
|
|
254
|
+
throw new Error(`multiple hosts; pass --host <${hosts.map((h) => h.name).join('|')}>`);
|
|
255
|
+
const only = hosts[0];
|
|
256
|
+
if (!only)
|
|
257
|
+
throw new Error('no hosts registered — run: t3ctl host add <origin> <token>');
|
|
258
|
+
return only;
|
|
259
|
+
};
|
|
260
|
+
const dispatch = async (host, command) => {
|
|
261
|
+
const res = await fetch(`${host.origin}/api/orchestration/dispatch`, {
|
|
262
|
+
method: 'POST',
|
|
263
|
+
headers: { ...(host.token ? { authorization: `Bearer ${host.token}` } : {}), 'content-type': 'application/json' },
|
|
264
|
+
body: JSON.stringify(command),
|
|
265
|
+
signal: AbortSignal.timeout(host.timeoutMs ?? 15000),
|
|
266
|
+
});
|
|
267
|
+
const body = await res.text();
|
|
268
|
+
if (!res.ok)
|
|
269
|
+
throw new Error(`${command.type} failed: HTTP ${res.status} ${body}`);
|
|
270
|
+
return body ? JSON.parse(body) : { sequence: 0 };
|
|
271
|
+
};
|
|
272
|
+
const resolveProject = (snap, ref) => snap.projects.find((p) => p.id === ref) ??
|
|
273
|
+
snap.projects.find((p) => !p.deletedAt && p.title === ref) ??
|
|
274
|
+
snap.projects.find((p) => !p.deletedAt && p.workspaceRoot === path.resolve(ref.replace(/^~/, os.homedir())));
|
|
275
|
+
// Commands whose entire payload is {commandId, threadId}. Verified against
|
|
276
|
+
// packages/contracts/src/orchestration.ts — note `unsettle` is NOT one of
|
|
277
|
+
// these (it carries extra fields), so it is deliberately absent.
|
|
278
|
+
const SIMPLE_THREAD_COMMANDS = ['settle', 'archive', 'unarchive', 'unpin', 'delete'];
|
|
279
|
+
const resolveThread = (snap, ref) => {
|
|
280
|
+
const live = snap.threads.filter((t) => !t.deletedAt);
|
|
281
|
+
const byId = live.find((t) => t.id === ref);
|
|
282
|
+
if (byId)
|
|
283
|
+
return byId;
|
|
284
|
+
const exact = live.filter((t) => t.title === ref);
|
|
285
|
+
if (exact[0] && exact.length === 1)
|
|
286
|
+
return exact[0];
|
|
287
|
+
const fuzzy = live.filter((t) => (t.title ?? '').toLowerCase().includes(ref.toLowerCase()));
|
|
288
|
+
if (fuzzy[0] && fuzzy.length === 1)
|
|
289
|
+
return fuzzy[0];
|
|
290
|
+
if (fuzzy.length > 1) {
|
|
291
|
+
throw new Error(`"${ref}" matches ${fuzzy.length} threads:\n` +
|
|
292
|
+
fuzzy.slice(0, 8).map((t) => ` ${t.id} ${t.title}`).join('\n'));
|
|
293
|
+
}
|
|
294
|
+
throw new Error(`no thread matching "${ref}"`);
|
|
295
|
+
};
|
|
296
|
+
// thread.turn.start is ONE command carrying the first message inline — this is
|
|
297
|
+
// what the UI fires immediately after thread.create, which is why a thread with
|
|
298
|
+
// no messages is a state the UI never produces. The client-side schema requires
|
|
299
|
+
// runtimeMode/interactionMode explicitly (the server-side one defaults them).
|
|
300
|
+
const cmdThreadStart = async (thread, host, text, flags) => {
|
|
301
|
+
const command = {
|
|
302
|
+
type: 'thread.turn.start',
|
|
303
|
+
commandId: crypto.randomUUID(),
|
|
304
|
+
threadId: thread.id,
|
|
305
|
+
message: { messageId: crypto.randomUUID(), role: 'user', text, attachments: [] },
|
|
306
|
+
runtimeMode: flags['runtime-mode'] ?? thread.runtimeMode ?? 'full-access',
|
|
307
|
+
interactionMode: flags['interaction-mode'] ?? 'default',
|
|
308
|
+
createdAt: new Date().toISOString(),
|
|
309
|
+
};
|
|
310
|
+
if (flags.model) {
|
|
311
|
+
const slash = flags.model.indexOf('/');
|
|
312
|
+
if (slash < 1)
|
|
313
|
+
throw new Error(`--model must be <instance>/<model>, got "${flags.model}"`);
|
|
314
|
+
command.modelSelection = { instanceId: flags.model.slice(0, slash), model: flags.model.slice(slash + 1) };
|
|
315
|
+
}
|
|
316
|
+
else if (thread.modelSelection) {
|
|
317
|
+
command.modelSelection = thread.modelSelection;
|
|
318
|
+
}
|
|
319
|
+
const { sequence } = await dispatch(host, command);
|
|
320
|
+
const m = command['modelSelection'];
|
|
321
|
+
console.log(`started ${bold(thread.title || thread.id)}\n id ${thread.id}` +
|
|
322
|
+
(m ? `\n model ${m.instanceId}/${m.model}` : '') +
|
|
323
|
+
`\n mode ${command.runtimeMode} / ${command.interactionMode}\n seq ${sequence}`);
|
|
324
|
+
};
|
|
325
|
+
const cmdThreadInterrupt = async (thread, host) => {
|
|
326
|
+
const { sequence } = await dispatch(host, {
|
|
327
|
+
type: 'thread.turn.interrupt', commandId: crypto.randomUUID(), threadId: thread.id,
|
|
328
|
+
});
|
|
329
|
+
console.log(`interrupted ${bold(thread.title || thread.id)}\n seq ${sequence}`);
|
|
330
|
+
};
|
|
331
|
+
// thread.meta.update also accepts regenerateTitle:true, which asks the server to
|
|
332
|
+
// derive a title from the thread's own content instead of taking one from us.
|
|
333
|
+
const cmdThreadRename = async (thread, host, title) => {
|
|
334
|
+
const { sequence } = await dispatch(host, {
|
|
335
|
+
type: 'thread.meta.update', commandId: crypto.randomUUID(), threadId: thread.id, title,
|
|
336
|
+
});
|
|
337
|
+
console.log(`renamed ${dim(thread.title || thread.id)} -> ${bold(title)}\n seq ${sequence}`);
|
|
338
|
+
};
|
|
339
|
+
// Lighter than the full snapshot; retitle polls this so it does not refetch every
|
|
340
|
+
// thread's history once a second.
|
|
341
|
+
const threadDetail = async (host, threadId) => {
|
|
342
|
+
const res = await fetch(`${host.origin}/api/orchestration/threads/${threadId}?turnLimit=1`, {
|
|
343
|
+
headers: { authorization: `Bearer ${host.token}` },
|
|
344
|
+
signal: AbortSignal.timeout(host.timeoutMs ?? 15000),
|
|
345
|
+
});
|
|
346
|
+
if (!res.ok)
|
|
347
|
+
throw new Error(`${host.name}: HTTP ${res.status}`);
|
|
348
|
+
return (await res.json()).thread;
|
|
349
|
+
};
|
|
350
|
+
// `regenerateTitle` does NOT rename anything by itself. The server records an
|
|
351
|
+
// intent marker (`titleRegeneration: {requestId, startedAt}`) and expects something
|
|
352
|
+
// downstream to generate a title and write it back. Observed on a live server: the
|
|
353
|
+
// marker is sometimes cleared a few seconds later with the title untouched, and no
|
|
354
|
+
// error is reported anywhere — not in the event, the command receipt, or the server
|
|
355
|
+
// trace. So we dispatch, then watch, and say what actually happened.
|
|
356
|
+
const cmdThreadRetitle = async (thread, host, timeoutSeconds) => {
|
|
357
|
+
const before = thread.title;
|
|
358
|
+
const { sequence } = await dispatch(host, {
|
|
359
|
+
type: 'thread.meta.update', commandId: crypto.randomUUID(), threadId: thread.id,
|
|
360
|
+
regenerateTitle: true,
|
|
361
|
+
});
|
|
362
|
+
console.log(`asked the server to retitle ${bold(before || thread.id)}${dim(` (seq ${sequence})`)}`);
|
|
363
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
364
|
+
let sawMarker = false;
|
|
365
|
+
while (Date.now() < deadline) {
|
|
366
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
367
|
+
const current = await threadDetail(host, thread.id);
|
|
368
|
+
if (current.title && current.title !== before) {
|
|
369
|
+
console.log(`retitled -> ${bold(current.title)}`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (current.titleRegeneration) {
|
|
373
|
+
sawMarker = true;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (sawMarker)
|
|
377
|
+
break; // marker appeared and was cleared, title unchanged
|
|
378
|
+
}
|
|
379
|
+
console.error(`\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` +
|
|
380
|
+
` The title is still "${before}". Set one directly:\n` +
|
|
381
|
+
` t3ctl thread rename ${thread.id} <title...>`);
|
|
382
|
+
process.exitCode = 1;
|
|
383
|
+
};
|
|
384
|
+
const cmdProjectCreate = async (title, root, flags) => {
|
|
385
|
+
const host = pickHost(flags);
|
|
386
|
+
const workspaceRoot = path.resolve(root.replace(/^~/, os.homedir()));
|
|
387
|
+
if (!fs.existsSync(workspaceRoot))
|
|
388
|
+
throw new Error(`workspace root does not exist: ${workspaceRoot}`);
|
|
389
|
+
const projectId = crypto.randomUUID();
|
|
390
|
+
const { sequence } = await dispatch(host, {
|
|
391
|
+
type: 'project.create', commandId: crypto.randomUUID(),
|
|
392
|
+
projectId, title, workspaceRoot, createdAt: new Date().toISOString(),
|
|
393
|
+
});
|
|
394
|
+
console.log(`created project ${bold(title)} on ${host.name}\n id ${projectId}\n root ${workspaceRoot}\n seq ${sequence}`);
|
|
395
|
+
};
|
|
396
|
+
const cmdThreadCreate = async (projectRef, title, flags) => {
|
|
397
|
+
const host = pickHost(flags);
|
|
398
|
+
const project = resolveProject(await snapshot(host), projectRef);
|
|
399
|
+
if (!project)
|
|
400
|
+
throw new Error(`no project matching "${projectRef}" on ${host.name}`);
|
|
401
|
+
// instanceId is the segment before the FIRST slash; the model keeps the rest,
|
|
402
|
+
// because opencode model ids are themselves slashed ("github-copilot/gpt-5.4").
|
|
403
|
+
const raw = flags.model ?? 'claudeAgent/claude-opus-5';
|
|
404
|
+
const slash = raw.indexOf('/');
|
|
405
|
+
if (slash < 1)
|
|
406
|
+
throw new Error(`--model must be <instance>/<model>, got "${raw}"`);
|
|
407
|
+
const modelSelection = { instanceId: raw.slice(0, slash), model: raw.slice(slash + 1) };
|
|
408
|
+
const threadId = crypto.randomUUID();
|
|
409
|
+
const { sequence } = await dispatch(host, {
|
|
410
|
+
type: 'thread.create', commandId: crypto.randomUUID(),
|
|
411
|
+
threadId, projectId: project.id, title, modelSelection,
|
|
412
|
+
runtimeMode: flags['runtime-mode'] ?? 'full-access',
|
|
413
|
+
interactionMode: flags['interaction-mode'] ?? 'default',
|
|
414
|
+
branch: flags.branch ?? null,
|
|
415
|
+
worktreePath: flags.worktree ?? null,
|
|
416
|
+
createdAt: new Date().toISOString(),
|
|
417
|
+
});
|
|
418
|
+
console.log(`created thread ${bold(title)} in ${project.title} on ${host.name}\n id ${threadId}\n model ${modelSelection.instanceId}/${modelSelection.model}\n seq ${sequence}`);
|
|
419
|
+
};
|
|
420
|
+
const cmdThreadSimple = async (verb, ref, flags) => {
|
|
421
|
+
const host = pickHost(flags);
|
|
422
|
+
const thread = resolveThread(await snapshot(host), ref);
|
|
423
|
+
const { sequence } = await dispatch(host, {
|
|
424
|
+
type: `thread.${verb}`, commandId: crypto.randomUUID(), threadId: thread.id,
|
|
425
|
+
});
|
|
426
|
+
console.log(`${verb}d ${bold(thread.title || thread.id)}\n id ${thread.id}\n seq ${sequence}`);
|
|
427
|
+
};
|
|
428
|
+
// ---- cli ----------------------------------------------------------------
|
|
429
|
+
// commander's option names arrive camelCased; the command implementations were
|
|
430
|
+
// written against the kebab-case spellings, so translate once here rather than
|
|
431
|
+
// touching every call site.
|
|
432
|
+
const FLAG_NAMES = {
|
|
433
|
+
host: 'host', model: 'model', branch: 'branch', worktree: 'worktree',
|
|
434
|
+
name: 'name', timeout: 'timeout',
|
|
435
|
+
runtimeMode: 'runtime-mode', interactionMode: 'interaction-mode',
|
|
436
|
+
};
|
|
437
|
+
// Only carry options that were actually supplied. Emitting every key
|
|
438
|
+
// unconditionally made `'name' in flags` always true, which made host add bail
|
|
439
|
+
// on every invocation.
|
|
440
|
+
const toFlags = (o) => Object.fromEntries(Object.entries(FLAG_NAMES)
|
|
441
|
+
.filter(([from]) => o[from] !== undefined)
|
|
442
|
+
.map(([from, to]) => [to, o[from]]));
|
|
443
|
+
const resolve = async (ref, o) => {
|
|
444
|
+
const host = pickHost(toFlags(o));
|
|
445
|
+
return { host, thread: resolveThread(await snapshot(host), ref) };
|
|
446
|
+
};
|
|
447
|
+
// Compiled to dist/t3ctl.js, so package.json is one level up — true in the repo
|
|
448
|
+
// and in the published tarball, which ships dist/ alongside package.json.
|
|
449
|
+
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
450
|
+
const program = new Command();
|
|
451
|
+
program
|
|
452
|
+
.name('t3ctl')
|
|
453
|
+
.description('Control T3 Code hosts — list and drive coding-agent threads across every machine you run T3 Code on.')
|
|
454
|
+
.version(pkg.version, '-v, --version', 'print the t3ctl version')
|
|
455
|
+
.showHelpAfterError('(run `t3ctl --help` or `t3ctl <command> --help`)')
|
|
456
|
+
.configureHelp({ showGlobalOptions: true });
|
|
457
|
+
const hostOption = (cmd) => cmd.option('--host <name>', 'which registered host to talk to (required when several are registered)');
|
|
458
|
+
program.command('ls')
|
|
459
|
+
.description('list projects and threads across all registered hosts')
|
|
460
|
+
.option('-t, --threads', 'expand the threads under each project')
|
|
461
|
+
.option('-a, --all', 'include archived and deleted items')
|
|
462
|
+
.option('--json', 'emit JSON instead of a table')
|
|
463
|
+
.action((o) => cmdLs(o));
|
|
464
|
+
const host = program.command('host').description('manage the host registry');
|
|
465
|
+
host.command('add')
|
|
466
|
+
.argument('<origin>', 'base URL of the host, e.g. https://box.tailnet.ts.net:3773')
|
|
467
|
+
.argument('[token]', 'bearer token from `t3 auth session issue` on that host')
|
|
468
|
+
.description('register a host, probing /.well-known/t3/environment first')
|
|
469
|
+
.option('--name <name>', 'override the label detected from the host')
|
|
470
|
+
.action((origin, token, o) => cmdHostAdd([origin, token].filter(Boolean), toFlags(o), readHosts()));
|
|
471
|
+
host.command('rm')
|
|
472
|
+
.argument('<name>', 'registered host name')
|
|
473
|
+
.description('remove a host from the registry')
|
|
474
|
+
.action((name) => {
|
|
475
|
+
const hosts = readHosts();
|
|
476
|
+
if (!hosts.some((h) => h.name === name))
|
|
477
|
+
throw new Error(`no such host: ${name}`);
|
|
478
|
+
writeHosts(hosts.filter((h) => h.name !== name));
|
|
479
|
+
console.log(`removed ${name}`);
|
|
480
|
+
});
|
|
481
|
+
host.command('ls').description('list registered hosts and probe each one')
|
|
482
|
+
.action(() => cmdHostsList(readHosts()));
|
|
483
|
+
program.command('hosts').description('list registered hosts and probe each one (alias of `host ls`)')
|
|
484
|
+
.action(() => cmdHostsList(readHosts()));
|
|
485
|
+
const project = program.command('project').description('manage projects');
|
|
486
|
+
hostOption(project.command('create')
|
|
487
|
+
.argument('<title>', 'name for the project as it appears in T3 Code')
|
|
488
|
+
.argument('<workspace-root>', 'existing directory the project maps to')
|
|
489
|
+
.description('create a project for an existing directory'))
|
|
490
|
+
.action((title, root, o) => cmdProjectCreate(title, root, toFlags(o)));
|
|
491
|
+
const thread = program.command('thread').description('create and drive threads');
|
|
492
|
+
hostOption(thread.command('create')
|
|
493
|
+
.argument('<project>', 'project id, title, or workspace root')
|
|
494
|
+
.argument('<title...>', 'thread title; everything after the project is used')
|
|
495
|
+
.description('create a thread (idle — use `thread send` to run it)')
|
|
496
|
+
.option('--model <instance/model>', 'e.g. claudeAgent/claude-opus-5', 'claudeAgent/claude-opus-5')
|
|
497
|
+
.option('--branch <branch>', 'git branch to associate with the thread')
|
|
498
|
+
.option('--worktree <path>', 'git worktree the thread should run in')
|
|
499
|
+
.option('--runtime-mode <mode>', 'approval-required | auto-accept-edits | auto | full-access', 'full-access')
|
|
500
|
+
.option('--interaction-mode <mode>', 'default | plan', 'default'))
|
|
501
|
+
.action((ref, title, o) => cmdThreadCreate(ref, title.join(' '), toFlags(o)));
|
|
502
|
+
hostOption(thread.command('send')
|
|
503
|
+
.alias('start')
|
|
504
|
+
.argument('<thread>', 'thread id, exact title, or unique substring')
|
|
505
|
+
.argument('<message...>', 'the message; everything after the thread is sent')
|
|
506
|
+
.description('send a message to a thread and run the agent')
|
|
507
|
+
.option('--model <instance/model>', "override the thread's model for this turn")
|
|
508
|
+
.option('--runtime-mode <mode>', 'approval-required | auto-accept-edits | auto | full-access')
|
|
509
|
+
.option('--interaction-mode <mode>', 'default | plan'))
|
|
510
|
+
.action(async (ref, message, o) => {
|
|
511
|
+
const { host: h, thread: t } = await resolve(ref, o);
|
|
512
|
+
return cmdThreadStart(t, h, message.join(' '), toFlags(o));
|
|
513
|
+
});
|
|
514
|
+
hostOption(thread.command('rename')
|
|
515
|
+
.argument('<thread>', 'thread id, exact title, or unique substring')
|
|
516
|
+
.argument('<title...>', 'the new title')
|
|
517
|
+
.description('set a thread title directly'))
|
|
518
|
+
.action(async (ref, title, o) => {
|
|
519
|
+
const { host: h, thread: t } = await resolve(ref, o);
|
|
520
|
+
return cmdThreadRename(t, h, title.join(' '));
|
|
521
|
+
});
|
|
522
|
+
hostOption(thread.command('retitle')
|
|
523
|
+
.argument('<thread>', 'thread id, exact title, or unique substring')
|
|
524
|
+
.description('ask the server to derive a title; warns if it produces none')
|
|
525
|
+
.option('--timeout <seconds>', 'how long to wait for a title', '30'))
|
|
526
|
+
.action(async (ref, o) => {
|
|
527
|
+
const { host: h, thread: t } = await resolve(ref, o);
|
|
528
|
+
return cmdThreadRetitle(t, h, Number(o.timeout) > 0 ? Number(o.timeout) : 30);
|
|
529
|
+
});
|
|
530
|
+
hostOption(thread.command('interrupt')
|
|
531
|
+
.argument('<thread>', 'thread id, exact title, or unique substring')
|
|
532
|
+
.description('stop the turn currently running in a thread'))
|
|
533
|
+
.action(async (ref, o) => {
|
|
534
|
+
const { host: h, thread: t } = await resolve(ref, o);
|
|
535
|
+
return cmdThreadInterrupt(t, h);
|
|
536
|
+
});
|
|
537
|
+
const VERB_HELP = {
|
|
538
|
+
settle: 'mark a thread done so it drops out of the active list',
|
|
539
|
+
archive: 'hide a thread from the default listing (reversible)',
|
|
540
|
+
unarchive: 'bring an archived thread back into the default listing',
|
|
541
|
+
unpin: 'remove a thread from the pinned section',
|
|
542
|
+
delete: 'delete a thread',
|
|
543
|
+
};
|
|
544
|
+
for (const verb of SIMPLE_THREAD_COMMANDS) {
|
|
545
|
+
hostOption(thread.command(verb)
|
|
546
|
+
.argument('<thread>', 'thread id, exact title, or unique substring')
|
|
547
|
+
.description(VERB_HELP[verb] ?? `${verb} a thread`))
|
|
548
|
+
.action((ref, o) => cmdThreadSimple(verb, ref, toFlags(o)));
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
await program.parseAsync(process.argv);
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
console.error(`\x1b[31merror\x1b[0m ${error instanceof Error ? error.message : String(error)}`);
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
//# sourceMappingURL=t3ctl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"t3ctl.js","sourceRoot":"","sources":["../src/t3ctl.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobius/t3ctl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Controller CLI for T3 Code hosts — list and drive threads across machines",
|
|
6
6
|
"keywords": [
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"url": "https://github.com/Goobles/t3ctl/issues"
|
|
22
22
|
},
|
|
23
23
|
"bin": {
|
|
24
|
-
"t3ctl": "t3ctl.
|
|
24
|
+
"t3ctl": "./dist/t3ctl.js"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
|
-
"
|
|
27
|
+
"dist",
|
|
28
28
|
"README.md",
|
|
29
29
|
"LICENSE"
|
|
30
30
|
],
|
|
@@ -33,5 +33,17 @@
|
|
|
33
33
|
},
|
|
34
34
|
"publishConfig": {
|
|
35
35
|
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"commander": "^15.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^26.4.1",
|
|
42
|
+
"typescript": "^7.0.2"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"test": "node --test test/*.test.mjs"
|
|
36
48
|
}
|
|
37
49
|
}
|
package/t3ctl.mjs
DELETED
|
@@ -1,520 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// t3ctl — a controller CLI for T3 Code hosts.
|
|
3
|
-
// Peer of the mobile app: pairs once per host, then reads/controls remotely.
|
|
4
|
-
// Spike scope: host registry + read-only listing.
|
|
5
|
-
|
|
6
|
-
import fs from 'node:fs';
|
|
7
|
-
import os from 'node:os';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
|
|
10
|
-
const CONFIG_DIR = path.join(os.homedir(), '.config', 't3ctl');
|
|
11
|
-
const HOSTS_FILE = path.join(CONFIG_DIR, 'hosts.json');
|
|
12
|
-
|
|
13
|
-
const readHosts = () => {
|
|
14
|
-
if (!fs.existsSync(HOSTS_FILE)) return [];
|
|
15
|
-
return JSON.parse(fs.readFileSync(HOSTS_FILE, 'utf8')).hosts ?? [];
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const writeHosts = (hosts) => {
|
|
19
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
20
|
-
fs.writeFileSync(HOSTS_FILE, JSON.stringify({ hosts }, null, 2), { mode: 0o600 });
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const snapshot = async (host) => {
|
|
24
|
-
const res = await fetch(`${host.origin}/api/orchestration/snapshot`, {
|
|
25
|
-
headers: host.token ? { authorization: `Bearer ${host.token}` } : {},
|
|
26
|
-
signal: AbortSignal.timeout(host.timeoutMs ?? 15000),
|
|
27
|
-
});
|
|
28
|
-
if (!res.ok) throw new Error(`${host.name}: HTTP ${res.status} ${await res.text().catch(() => '')}`.trim());
|
|
29
|
-
return res.json();
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// Derived status. Order matters: most urgent wins.
|
|
33
|
-
const threadStatus = (t) => {
|
|
34
|
-
if (t.deletedAt) return 'deleted';
|
|
35
|
-
if (t.archivedAt) return 'archived';
|
|
36
|
-
if (t.session?.activeTurnId || t.session?.status === 'running') return 'running';
|
|
37
|
-
if (t.session?.status === 'error' || t.latestTurn?.state === 'error') return 'error';
|
|
38
|
-
if (t.snoozedUntil && new Date(t.snoozedUntil) > new Date()) return 'snoozed';
|
|
39
|
-
if (t.proposedPlans?.length) return 'needs-review';
|
|
40
|
-
if (t.settledAt && !t.unsettledAt) return 'settled';
|
|
41
|
-
return 'idle';
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const ICON = {
|
|
45
|
-
running: '\x1b[32m●\x1b[0m', error: '\x1b[31m✕\x1b[0m', 'needs-review': '\x1b[33m◆\x1b[0m',
|
|
46
|
-
snoozed: '\x1b[90m☾\x1b[0m', settled: '\x1b[90m✓\x1b[0m', idle: '\x1b[90m·\x1b[0m',
|
|
47
|
-
archived: '\x1b[90m▪\x1b[0m', deleted: '\x1b[90m✗\x1b[0m',
|
|
48
|
-
};
|
|
49
|
-
const dim = (s) => `\x1b[90m${s}\x1b[0m`;
|
|
50
|
-
// Usage errors are user errors: print to stderr and exit non-zero so scripts
|
|
51
|
-
// can tell them apart from success.
|
|
52
|
-
const usage = (message) => {
|
|
53
|
-
console.error(message);
|
|
54
|
-
process.exitCode = 1;
|
|
55
|
-
};
|
|
56
|
-
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
57
|
-
|
|
58
|
-
const collect = async (hosts) => {
|
|
59
|
-
const results = await Promise.allSettled(hosts.map(async (h) => ({ host: h, snap: await snapshot(h) })));
|
|
60
|
-
const ok = [], failed = [];
|
|
61
|
-
results.forEach((r, i) => r.status === 'fulfilled' ? ok.push(r.value) : failed.push({ host: hosts[i], error: r.reason.message }));
|
|
62
|
-
return { ok, failed };
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const cmdLs = async (args) => {
|
|
66
|
-
const showThreads = args.includes('--threads') || args.includes('-t');
|
|
67
|
-
const showAll = args.includes('--all') || args.includes('-a');
|
|
68
|
-
const asJson = args.includes('--json');
|
|
69
|
-
const hosts = readHosts();
|
|
70
|
-
if (!hosts.length) return console.error('No hosts registered. Run: t3ctl host add <origin> <token>');
|
|
71
|
-
|
|
72
|
-
const { ok, failed } = await collect(hosts);
|
|
73
|
-
|
|
74
|
-
if (asJson) {
|
|
75
|
-
const out = ok.flatMap(({ host, snap }) => snap.projects
|
|
76
|
-
.filter((p) => showAll || !p.deletedAt)
|
|
77
|
-
.map((p) => ({
|
|
78
|
-
host: host.name, id: p.id, title: p.title, workspaceRoot: p.workspaceRoot,
|
|
79
|
-
threads: snap.threads.filter((t) => t.projectId === p.id)
|
|
80
|
-
.filter((t) => showAll || (!t.deletedAt && !t.archivedAt))
|
|
81
|
-
.map((t) => ({ id: t.id, title: t.title, branch: t.branch, status: threadStatus(t), provider: t.session?.providerName ?? null, updatedAt: t.updatedAt })),
|
|
82
|
-
})));
|
|
83
|
-
console.log(JSON.stringify({ projects: out, unreachable: failed.map((f) => ({ host: f.host.name, error: f.error })) }, null, 2));
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
for (const { host, snap } of ok) {
|
|
88
|
-
console.log(`\n${bold(host.name)} ${dim(host.origin)}`);
|
|
89
|
-
const projects = snap.projects.filter((p) => showAll || !p.deletedAt)
|
|
90
|
-
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
91
|
-
|
|
92
|
-
for (const p of projects) {
|
|
93
|
-
const threads = snap.threads.filter((t) => t.projectId === p.id)
|
|
94
|
-
.filter((t) => showAll || (!t.deletedAt && !t.archivedAt))
|
|
95
|
-
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
96
|
-
if (!threads.length && !showAll) continue;
|
|
97
|
-
|
|
98
|
-
const counts = {};
|
|
99
|
-
for (const t of threads) counts[threadStatus(t)] = (counts[threadStatus(t)] ?? 0) + 1;
|
|
100
|
-
const badge = Object.entries(counts).map(([k, v]) => `${ICON[k] ?? '?'}${v}`).join(' ');
|
|
101
|
-
|
|
102
|
-
console.log(` ${bold(p.title)} ${badge} ${dim(p.workspaceRoot.replace(os.homedir(), '~'))}`);
|
|
103
|
-
if (!showThreads) continue;
|
|
104
|
-
for (const t of threads) {
|
|
105
|
-
const s = threadStatus(t);
|
|
106
|
-
const meta = [t.branch, t.session?.providerName].filter(Boolean).join(' ');
|
|
107
|
-
console.log(` ${ICON[s] ?? '?'} ${(t.title || '(untitled)').slice(0, 62).padEnd(62)} ${dim(meta)}`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
for (const f of failed) console.error(`\n\x1b[31munreachable\x1b[0m ${f.host.name}: ${f.error}`);
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
// ---- host registry ------------------------------------------------------
|
|
115
|
-
// The descriptor at /.well-known/t3/environment is UNAUTHENTICATED, so probing
|
|
116
|
-
// it answers "is anyone home, and is it T3 Code?" without a token — a wrong
|
|
117
|
-
// origin fails here instead of as a baffling 401 on the first real call.
|
|
118
|
-
// Schema: ExecutionEnvironmentDescriptor in packages/contracts/src/environment.ts.
|
|
119
|
-
|
|
120
|
-
const DESCRIPTOR_PATH = '/.well-known/t3/environment';
|
|
121
|
-
|
|
122
|
-
const probe = async (origin, timeoutMs = 5000) => {
|
|
123
|
-
let res;
|
|
124
|
-
try {
|
|
125
|
-
res = await fetch(`${origin}${DESCRIPTOR_PATH}`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
126
|
-
} catch (error) {
|
|
127
|
-
// Node's fetch reports a bare "fetch failed"; the cause carries the real reason.
|
|
128
|
-
const why = error.name === 'TimeoutError' ? `no response in ${timeoutMs}ms`
|
|
129
|
-
: (error.cause?.message ?? error.message);
|
|
130
|
-
throw new Error(`cannot reach ${origin}: ${why}`);
|
|
131
|
-
}
|
|
132
|
-
const notT3 = (why) => new Error(`not a T3 Code server (${DESCRIPTOR_PATH} ${why})`);
|
|
133
|
-
if (!res.ok) throw notT3(`returned HTTP ${res.status}`);
|
|
134
|
-
let d;
|
|
135
|
-
try { d = await res.json(); } catch { throw notT3('is not JSON'); }
|
|
136
|
-
const missing = ['environmentId', 'label', 'serverVersion'].filter((k) => typeof d?.[k] !== 'string' || !d[k]);
|
|
137
|
-
if (missing.length) throw notT3(`is missing ${missing.join(', ')}`);
|
|
138
|
-
return { environmentId: d.environmentId, label: d.label, serverVersion: d.serverVersion };
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const shortId = (id) => (id ? id.slice(0, 8) : '-');
|
|
142
|
-
const warn = (message) => console.error(`\x1b[33mwarning\x1b[0m ${message}`);
|
|
143
|
-
|
|
144
|
-
// The name is what you type in --host, so derive a typeable slug from the label
|
|
145
|
-
// rather than using the label verbatim.
|
|
146
|
-
const slugify = (label) => label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'host';
|
|
147
|
-
|
|
148
|
-
const uniqueName = (base, hosts) => {
|
|
149
|
-
if (!hosts.some((h) => h.name === base)) return base;
|
|
150
|
-
for (let n = 2; ; n++) if (!hosts.some((h) => h.name === `${base}-${n}`)) return `${base}-${n}`;
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const isOrigin = (value) => /^https?:\/\//i.test(value ?? '');
|
|
154
|
-
|
|
155
|
-
// Two accepted shapes, told apart by the scheme:
|
|
156
|
-
// host add <origin> [token] [--name <n>] current
|
|
157
|
-
// host add <name> <origin> <token> legacy, still works
|
|
158
|
-
const parseHostAdd = (pos) => {
|
|
159
|
-
if (isOrigin(pos[0])) return { origin: pos[0], token: pos[1] ?? null, legacy: false };
|
|
160
|
-
if (isOrigin(pos[1])) return { name: pos[0], origin: pos[1], token: pos[2] ?? null, legacy: true };
|
|
161
|
-
return null;
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const HOST_ADD_USAGE = 'usage: t3ctl host add <origin> [token] [--name <name>]\n' +
|
|
165
|
-
' an origin needs a scheme, e.g. http://localhost:3773';
|
|
166
|
-
|
|
167
|
-
const cmdHostAdd = async (pos, flags, hosts) => {
|
|
168
|
-
// Validate before any network or registry work so a bare `host add` prints
|
|
169
|
-
// usage instead of stalling on a probe.
|
|
170
|
-
const parsed = parseHostAdd(pos);
|
|
171
|
-
if (!parsed) return usage(HOST_ADD_USAGE);
|
|
172
|
-
if ('name' in flags && !flags.name) return usage(HOST_ADD_USAGE);
|
|
173
|
-
if (parsed.legacy) warn('"host add <name> <origin> <token>" is deprecated — use: t3ctl host add <origin> [token] [--name <name>]');
|
|
174
|
-
|
|
175
|
-
const origin = parsed.origin.replace(/\/$/, '');
|
|
176
|
-
const descriptor = await probe(origin);
|
|
177
|
-
const existing = hosts.find((h) => h.origin === origin);
|
|
178
|
-
|
|
179
|
-
// A changed environmentId on a known origin means the origin now points at a
|
|
180
|
-
// different machine — the stored token almost certainly belongs to the old one.
|
|
181
|
-
if (existing?.environmentId && existing.environmentId !== descriptor.environmentId) {
|
|
182
|
-
warn(`${origin} is now a DIFFERENT environment\n` +
|
|
183
|
-
` was ${existing.environmentId} (${existing.label ?? 'unknown'})\n` +
|
|
184
|
-
` now ${descriptor.environmentId} (${descriptor.label})\n` +
|
|
185
|
-
` the token stored for "${existing.name}" was issued by the old one and will likely fail`);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const name = flags.name ?? parsed.name ?? existing?.name ??
|
|
189
|
-
uniqueName(slugify(descriptor.label), hosts);
|
|
190
|
-
const token = parsed.token ?? existing?.token ?? null;
|
|
191
|
-
|
|
192
|
-
const others = hosts.filter((h) => h.name !== name && h.origin !== origin && h.serverVersion);
|
|
193
|
-
const skewed = [...new Set(others.map((h) => h.serverVersion))].filter((v) => v !== descriptor.serverVersion);
|
|
194
|
-
if (skewed.length) warn(`serverVersion ${descriptor.serverVersion} differs from other hosts: ${skewed.join(', ')}`);
|
|
195
|
-
|
|
196
|
-
writeHosts(hosts.filter((h) => h.name !== name && h.origin !== origin).concat({
|
|
197
|
-
name, origin, token,
|
|
198
|
-
environmentId: descriptor.environmentId,
|
|
199
|
-
label: descriptor.label,
|
|
200
|
-
serverVersion: descriptor.serverVersion,
|
|
201
|
-
}));
|
|
202
|
-
console.log(`added ${bold(name)} -> ${origin}\n label ${descriptor.label}\n` +
|
|
203
|
-
` env ${descriptor.environmentId}\n version ${descriptor.serverVersion}`);
|
|
204
|
-
if (!token) warn(`no token stored for ${name} — reads will fail until you run: t3ctl host add ${origin} <token>`);
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
const cmdHostsList = async (hosts) => {
|
|
208
|
-
if (!hosts.length) return console.log('(no hosts)');
|
|
209
|
-
const probes = await Promise.allSettled(hosts.map((h) => probe(h.origin)));
|
|
210
|
-
const drifted = [];
|
|
211
|
-
hosts.forEach((h, i) => {
|
|
212
|
-
const p = probes[i];
|
|
213
|
-
const live = p.status === 'fulfilled' ? p.value : null;
|
|
214
|
-
if (live && h.environmentId && h.environmentId !== live.environmentId) drifted.push({ h, live });
|
|
215
|
-
const icon = live ? ICON.running : ICON.error;
|
|
216
|
-
const label = live?.label ?? h.label ?? '-';
|
|
217
|
-
const env = shortId(live?.environmentId ?? h.environmentId);
|
|
218
|
-
const version = live?.serverVersion ?? h.serverVersion ?? '-';
|
|
219
|
-
// Values for an unreachable host are whatever was last stored, so dim the
|
|
220
|
-
// whole row to keep remembered data visually distinct from probed data.
|
|
221
|
-
const cell = (text, width) => (live ? text.padEnd(width) : dim(text.padEnd(width)));
|
|
222
|
-
console.log(`${icon} ${live ? bold(h.name.padEnd(14)) : dim(h.name.padEnd(14))} ${cell(label, 18)} ${dim(env.padEnd(9))} ${cell(version, 28)} ${dim(h.origin)}` +
|
|
223
|
-
(live ? '' : ` ${dim(p.reason.message)}`));
|
|
224
|
-
});
|
|
225
|
-
for (const { h, live } of drifted) {
|
|
226
|
-
warn(`${h.name} (${h.origin}) is now a DIFFERENT environment\n` +
|
|
227
|
-
` was ${h.environmentId} (${h.label ?? 'unknown'})\n now ${live.environmentId} (${live.label})`);
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
const cmdHost = async (args) => {
|
|
232
|
-
const [sub, ...rest] = args;
|
|
233
|
-
const { flags, pos } = parseArgs(rest);
|
|
234
|
-
const hosts = readHosts();
|
|
235
|
-
if (sub === 'add') return cmdHostAdd(pos, flags, hosts);
|
|
236
|
-
if (sub === 'rm') {
|
|
237
|
-
if (!pos[0]) return usage('usage: t3ctl host rm <name>');
|
|
238
|
-
writeHosts(hosts.filter((h) => h.name !== pos[0]));
|
|
239
|
-
console.log(`removed ${pos[0]}`);
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
return cmdHostsList(hosts);
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// ---- writes -------------------------------------------------------------
|
|
247
|
-
// Commands are dispatched directly (no envelope). The CLIENT mints every id;
|
|
248
|
-
// commandId is the idempotency key, so retries are safe.
|
|
249
|
-
// Schemas: packages/contracts/src/orchestration.ts in pingdotgg/t3code.
|
|
250
|
-
|
|
251
|
-
const VALUE_FLAGS = ['host', 'model', 'branch', 'runtime-mode', 'interaction-mode', 'worktree', 'name'];
|
|
252
|
-
|
|
253
|
-
const parseArgs = (argv) => {
|
|
254
|
-
const flags = {}, pos = [];
|
|
255
|
-
for (let i = 0; i < argv.length; i++) {
|
|
256
|
-
const a = argv[i];
|
|
257
|
-
if (!a.startsWith('--')) { pos.push(a); continue; }
|
|
258
|
-
const k = a.slice(2);
|
|
259
|
-
flags[k] = VALUE_FLAGS.includes(k) ? argv[++i] : true;
|
|
260
|
-
}
|
|
261
|
-
return { flags, pos };
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
const pickHost = (flags) => {
|
|
265
|
-
const hosts = readHosts();
|
|
266
|
-
if (flags.host) {
|
|
267
|
-
const h = hosts.find((x) => x.name === flags.host);
|
|
268
|
-
if (!h) throw new Error(`no such host: ${flags.host}`);
|
|
269
|
-
return h;
|
|
270
|
-
}
|
|
271
|
-
if (!hosts.length) throw new Error('no hosts registered — run: t3ctl host add <origin> <token>');
|
|
272
|
-
if (hosts.length > 1) throw new Error(`multiple hosts; pass --host <${hosts.map((h) => h.name).join('|')}>`);
|
|
273
|
-
return hosts[0];
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
const dispatch = async (host, command) => {
|
|
277
|
-
const res = await fetch(`${host.origin}/api/orchestration/dispatch`, {
|
|
278
|
-
method: 'POST',
|
|
279
|
-
headers: { ...(host.token ? { authorization: `Bearer ${host.token}` } : {}), 'content-type': 'application/json' },
|
|
280
|
-
body: JSON.stringify(command),
|
|
281
|
-
signal: AbortSignal.timeout(host.timeoutMs ?? 15000),
|
|
282
|
-
});
|
|
283
|
-
const body = await res.text();
|
|
284
|
-
if (!res.ok) throw new Error(`${command.type} failed: HTTP ${res.status} ${body}`);
|
|
285
|
-
return body ? JSON.parse(body) : {};
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
const resolveProject = (snap, ref) =>
|
|
289
|
-
snap.projects.find((p) => p.id === ref) ??
|
|
290
|
-
snap.projects.find((p) => !p.deletedAt && p.title === ref) ??
|
|
291
|
-
snap.projects.find((p) => !p.deletedAt && p.workspaceRoot === path.resolve(ref.replace(/^~/, os.homedir())));
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
// Commands whose entire payload is {commandId, threadId}. Verified against
|
|
295
|
-
// packages/contracts/src/orchestration.ts — note `unsettle` is NOT one of
|
|
296
|
-
// these (it carries extra fields), so it is deliberately absent.
|
|
297
|
-
const SIMPLE_THREAD_COMMANDS = ['settle', 'archive', 'unarchive', 'unpin', 'delete'];
|
|
298
|
-
|
|
299
|
-
const resolveThread = (snap, ref) => {
|
|
300
|
-
const live = snap.threads.filter((t) => !t.deletedAt);
|
|
301
|
-
const byId = live.find((t) => t.id === ref);
|
|
302
|
-
if (byId) return byId;
|
|
303
|
-
const exact = live.filter((t) => t.title === ref);
|
|
304
|
-
if (exact.length === 1) return exact[0];
|
|
305
|
-
const fuzzy = live.filter((t) => (t.title ?? '').toLowerCase().includes(ref.toLowerCase()));
|
|
306
|
-
if (fuzzy.length === 1) return fuzzy[0];
|
|
307
|
-
if (fuzzy.length > 1) {
|
|
308
|
-
throw new Error(`"${ref}" matches ${fuzzy.length} threads:\n` +
|
|
309
|
-
fuzzy.slice(0, 8).map((t) => ` ${t.id} ${t.title}`).join('\n'));
|
|
310
|
-
}
|
|
311
|
-
throw new Error(`no thread matching "${ref}"`);
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
// thread.turn.start is ONE command carrying the first message inline — this is
|
|
316
|
-
// what the UI fires immediately after thread.create, which is why a thread with
|
|
317
|
-
// no messages is a state the UI never produces. The client-side schema requires
|
|
318
|
-
// runtimeMode/interactionMode explicitly (the server-side one defaults them).
|
|
319
|
-
const cmdThreadStart = async (thread, host, text, flags) => {
|
|
320
|
-
const command = {
|
|
321
|
-
type: 'thread.turn.start',
|
|
322
|
-
commandId: crypto.randomUUID(),
|
|
323
|
-
threadId: thread.id,
|
|
324
|
-
message: { messageId: crypto.randomUUID(), role: 'user', text, attachments: [] },
|
|
325
|
-
runtimeMode: flags['runtime-mode'] ?? thread.runtimeMode ?? 'full-access',
|
|
326
|
-
interactionMode: flags['interaction-mode'] ?? 'default',
|
|
327
|
-
createdAt: new Date().toISOString(),
|
|
328
|
-
};
|
|
329
|
-
if (flags.model) {
|
|
330
|
-
const slash = flags.model.indexOf('/');
|
|
331
|
-
if (slash < 1) throw new Error(`--model must be <instance>/<model>, got "${flags.model}"`);
|
|
332
|
-
command.modelSelection = { instanceId: flags.model.slice(0, slash), model: flags.model.slice(slash + 1) };
|
|
333
|
-
} else if (thread.modelSelection) {
|
|
334
|
-
command.modelSelection = thread.modelSelection;
|
|
335
|
-
}
|
|
336
|
-
const { sequence } = await dispatch(host, command);
|
|
337
|
-
const m = command.modelSelection;
|
|
338
|
-
console.log(`started ${bold(thread.title || thread.id)}\n id ${thread.id}` +
|
|
339
|
-
(m ? `\n model ${m.instanceId}/${m.model}` : '') +
|
|
340
|
-
`\n mode ${command.runtimeMode} / ${command.interactionMode}\n seq ${sequence}`);
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
const cmdThreadInterrupt = async (thread, host) => {
|
|
344
|
-
const { sequence } = await dispatch(host, {
|
|
345
|
-
type: 'thread.turn.interrupt', commandId: crypto.randomUUID(), threadId: thread.id,
|
|
346
|
-
});
|
|
347
|
-
console.log(`interrupted ${bold(thread.title || thread.id)}\n seq ${sequence}`);
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
const cmdProject = async (args) => {
|
|
351
|
-
const [sub, ...rest] = args;
|
|
352
|
-
const { flags, pos } = parseArgs(rest);
|
|
353
|
-
if (sub !== 'create') return usage('usage: t3ctl project create <title> <workspace-root> [--host <name>]');
|
|
354
|
-
const [title, root] = pos;
|
|
355
|
-
if (!title || !root) return usage('usage: t3ctl project create <title> <workspace-root> [--host <name>]');
|
|
356
|
-
const host = pickHost(flags);
|
|
357
|
-
const workspaceRoot = path.resolve(root.replace(/^~/, os.homedir()));
|
|
358
|
-
if (!fs.existsSync(workspaceRoot)) return console.error(`workspace root does not exist: ${workspaceRoot}`);
|
|
359
|
-
const projectId = crypto.randomUUID();
|
|
360
|
-
const { sequence } = await dispatch(host, {
|
|
361
|
-
type: 'project.create', commandId: crypto.randomUUID(),
|
|
362
|
-
projectId, title, workspaceRoot, createdAt: new Date().toISOString(),
|
|
363
|
-
});
|
|
364
|
-
console.log(`created project ${bold(title)} on ${host.name}\n id ${projectId}\n root ${workspaceRoot}\n seq ${sequence}`);
|
|
365
|
-
};
|
|
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
|
-
|
|
423
|
-
const cmdThread = async (args) => {
|
|
424
|
-
const [sub, ...rest] = args;
|
|
425
|
-
const { flags, pos } = parseArgs(rest);
|
|
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') {
|
|
444
|
-
// Validate arguments before touching the host registry, so `t3ctl thread
|
|
445
|
-
// send` with no args prints usage instead of "no hosts registered".
|
|
446
|
-
const [ref, ...rest2] = pos;
|
|
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...>`);
|
|
449
|
-
const host = pickHost(flags);
|
|
450
|
-
const thread = resolveThread(await snapshot(host), ref);
|
|
451
|
-
if (sub === 'interrupt') return cmdThreadInterrupt(thread, host);
|
|
452
|
-
return cmdThreadStart(thread, host, rest2.join(' '), flags);
|
|
453
|
-
}
|
|
454
|
-
if (SIMPLE_THREAD_COMMANDS.includes(sub)) {
|
|
455
|
-
if (!pos.length) return usage(`usage: t3ctl thread ${sub} <thread>`);
|
|
456
|
-
const host = pickHost(flags);
|
|
457
|
-
const thread = resolveThread(await snapshot(host), pos.join(' '));
|
|
458
|
-
const { sequence } = await dispatch(host, {
|
|
459
|
-
type: `thread.${sub}`, commandId: crypto.randomUUID(), threadId: thread.id,
|
|
460
|
-
});
|
|
461
|
-
console.log(`${sub}d ${bold(thread.title || thread.id)}\n id ${thread.id}\n seq ${sequence}`);
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
if (sub !== 'create') return usage('usage: t3ctl thread create <project> <title> [--model <instance>/<model>] [--branch <b>] [--host <name>]\n t3ctl thread <settle|archive|unarchive|unpin|delete> <thread>');
|
|
465
|
-
const [projectRef, ...titleParts] = pos;
|
|
466
|
-
const title = titleParts.join(' ');
|
|
467
|
-
if (!projectRef || !title) return usage('usage: t3ctl thread create <project> <title> [--model <instance>/<model>] [--branch <b>] [--host <name>]');
|
|
468
|
-
const host = pickHost(flags);
|
|
469
|
-
const project = resolveProject(await snapshot(host), projectRef);
|
|
470
|
-
if (!project) throw new Error(`no project matching "${projectRef}" on ${host.name}`);
|
|
471
|
-
|
|
472
|
-
// instanceId is the segment before the first slash; model keeps the rest
|
|
473
|
-
// (opencode models look like "github-copilot/gpt-5.4").
|
|
474
|
-
const raw = flags.model ?? 'claudeAgent/claude-opus-5';
|
|
475
|
-
const slash = raw.indexOf('/');
|
|
476
|
-
if (slash < 1) throw new Error(`--model must be <instance>/<model>, got "${raw}"`);
|
|
477
|
-
const modelSelection = { instanceId: raw.slice(0, slash), model: raw.slice(slash + 1) };
|
|
478
|
-
|
|
479
|
-
const threadId = crypto.randomUUID();
|
|
480
|
-
const { sequence } = await dispatch(host, {
|
|
481
|
-
type: 'thread.create', commandId: crypto.randomUUID(),
|
|
482
|
-
threadId, projectId: project.id, title, modelSelection,
|
|
483
|
-
runtimeMode: flags['runtime-mode'] ?? 'full-access',
|
|
484
|
-
interactionMode: flags['interaction-mode'] ?? 'default',
|
|
485
|
-
branch: flags.branch ?? null,
|
|
486
|
-
worktreePath: flags.worktree ?? null,
|
|
487
|
-
createdAt: new Date().toISOString(),
|
|
488
|
-
});
|
|
489
|
-
console.log(`created thread ${bold(title)} in ${project.title} on ${host.name}\n id ${threadId}\n model ${modelSelection.instanceId}/${modelSelection.model}\n seq ${sequence}`);
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
const [cmd, ...rest] = process.argv.slice(2);
|
|
493
|
-
const commands = { ls: cmdLs, host: cmdHost, hosts: () => cmdHost([]), project: cmdProject, thread: cmdThread };
|
|
494
|
-
if (!commands[cmd]) {
|
|
495
|
-
console.log(`t3ctl — control T3 Code hosts
|
|
496
|
-
|
|
497
|
-
t3ctl ls [-t|--threads] [-a|--all] [--json] list projects and threads across hosts
|
|
498
|
-
t3ctl host add <origin> [token] [--name <n>] register a host (probes it first)
|
|
499
|
-
t3ctl host rm <name> remove a host
|
|
500
|
-
t3ctl hosts list hosts and probe each one
|
|
501
|
-
|
|
502
|
-
t3ctl project create <title> <root> create a project for an existing dir
|
|
503
|
-
t3ctl thread create <project> <title> start a thread (--model inst/model,
|
|
504
|
-
--branch, --host)
|
|
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)
|
|
510
|
-
t3ctl thread interrupt <thread> stop the running turn
|
|
511
|
-
t3ctl thread settle <thread> settle a thread (also: archive,
|
|
512
|
-
unarchive, unpin, delete)`);
|
|
513
|
-
process.exit(cmd ? 1 : 0);
|
|
514
|
-
}
|
|
515
|
-
try {
|
|
516
|
-
await commands[cmd](rest);
|
|
517
|
-
} catch (error) {
|
|
518
|
-
console.error(`\x1b[31merror\x1b[0m ${error instanceof Error ? error.message : String(error)}`);
|
|
519
|
-
process.exit(1);
|
|
520
|
-
}
|