@shardflux/cli 0.16.0 → 0.18.0-next.4

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 (74) hide show
  1. package/CHANGELOG.md +67 -2
  2. package/README.md +47 -8
  3. package/dist/agent-commands.d.ts +9 -1
  4. package/dist/agent-commands.js +65 -16
  5. package/dist/agent-models.d.ts +61 -0
  6. package/dist/agent-models.js +79 -0
  7. package/dist/agent-state.d.ts +10 -0
  8. package/dist/agent-state.js +13 -0
  9. package/dist/agent-target-commands.d.ts +6 -1
  10. package/dist/agent-target-commands.js +113 -16
  11. package/dist/agent-targets.d.ts +17 -5
  12. package/dist/agent-targets.js +27 -11
  13. package/dist/args.d.ts +5 -1
  14. package/dist/args.js +49 -9
  15. package/dist/cli-proxy-forward.d.ts +45 -0
  16. package/dist/cli-proxy-forward.js +432 -0
  17. package/dist/cli-proxy-hub.d.ts +273 -0
  18. package/dist/cli-proxy-hub.js +749 -0
  19. package/dist/cli-proxy.d.ts +325 -0
  20. package/dist/cli-proxy.js +1016 -0
  21. package/dist/codex-cloud.d.ts +2 -1
  22. package/dist/codex-cloud.js +16 -6
  23. package/dist/command-center-actions.js +16 -1
  24. package/dist/conversation-local.js +2 -2
  25. package/dist/conversation-stash.js +4 -3
  26. package/dist/conversation-terminal.d.ts +5 -0
  27. package/dist/conversation-terminal.js +45 -6
  28. package/dist/conversations.d.ts +18 -2
  29. package/dist/conversations.js +12 -5
  30. package/dist/desktop-linux.d.ts +40 -0
  31. package/dist/desktop-linux.js +186 -0
  32. package/dist/device-approvals.d.ts +12 -0
  33. package/dist/device-approvals.js +75 -0
  34. package/dist/device-helper.d.ts +3 -1
  35. package/dist/device-helper.js +5 -2
  36. package/dist/errors.d.ts +2 -0
  37. package/dist/errors.js +1 -1
  38. package/dist/git-remote-bin.js +0 -0
  39. package/dist/handlers.js +3 -1
  40. package/dist/index.d.ts +4 -0
  41. package/dist/index.js +3 -0
  42. package/dist/linux-helper.d.ts +12 -0
  43. package/dist/linux-helper.js +88 -0
  44. package/dist/linux-terminal.d.ts +11 -0
  45. package/dist/linux-terminal.js +42 -0
  46. package/dist/main.js +30 -6
  47. package/dist/menubar.js +44 -4
  48. package/dist/onboarding.d.ts +2 -0
  49. package/dist/onboarding.js +59 -9
  50. package/dist/t3-asar.d.ts +10 -0
  51. package/dist/t3-asar.js +91 -0
  52. package/dist/t3-launch.d.ts +37 -0
  53. package/dist/t3-launch.js +133 -0
  54. package/dist/t3-linux.d.ts +68 -0
  55. package/dist/t3-linux.js +343 -0
  56. package/dist/t3-mode.d.ts +129 -0
  57. package/dist/t3-mode.js +676 -0
  58. package/dist/t3-phone.d.ts +17 -0
  59. package/dist/t3-phone.js +15 -0
  60. package/dist/t3-provider.d.ts +5 -3
  61. package/dist/t3-provider.js +12 -49
  62. package/dist/t3-thread-script.d.ts +18 -0
  63. package/dist/t3-thread-script.js +245 -0
  64. package/dist/terminal-onboarding.d.ts +11 -0
  65. package/dist/terminal-onboarding.js +75 -0
  66. package/dist/thread-on-computer.d.ts +2 -2
  67. package/dist/thread-on-computer.js +13 -7
  68. package/dist/update.d.ts +1 -1
  69. package/dist/update.js +28 -1
  70. package/dist/worktree-twin-engine.d.ts +31 -1
  71. package/dist/worktree-twin-engine.js +253 -40
  72. package/dist/worktree-twin.d.ts +17 -0
  73. package/dist/worktree-twin.js +147 -22
  74. package/package.json +7 -19
@@ -0,0 +1,343 @@
1
+ /**
2
+ * T3 Code on Linux (0.17.0+): finding the installed desktop app, its version and route, and opening it.
3
+ *
4
+ * T3 ships a .deb and an AppImage (electron-builder, 0.0.45 and the nightlies, verified from the release assets):
5
+ * - .deb (package `t3code`): the app in `/opt/T3 Code (Alpha)/` (`/opt/T3 Code (Nightly)/` for nightlies) with the
6
+ * executable `t3code`, `resources/app.asar` and `resources/package-type` ("deb"); the entry
7
+ * `/usr/share/applications/t3code.desktop` (`Name=T3 Code (Alpha)`, `Exec="/opt/T3 Code (Alpha)/t3code" %U`,
8
+ * `StartupWMClass=t3code`, `MimeType=x-scheme-handler/t3code;…`); `/usr/bin/t3code` is an update-alternatives link
9
+ * made by its postinst. dpkg records nightlies as `0.0.46~nightly.<date>.<run>`.
10
+ * - AppImage: `T3-Code-<version>-<x86_64|arm64>.AppImage`, a squashfs image that is never mounted here; AppImageLauncher
11
+ * and Gear Lever integrate it with a copy of the same entry (`X-AppImage-Version`, `Exec=<the AppImage> %U`).
12
+ *
13
+ * Entries are found in the XDG application folders (also snapd's and flatpak's exports) and keyed on what the entry
14
+ * says (its Name, and StartupWMClass, the t3code URL scheme or a T3 flatpak id), never on its file name; only files
15
+ * whose name could be one are read, so a computer without T3 costs a few directory listings and no subprocess.
16
+ */
17
+ import { spawn, spawnSync } from 'node:child_process';
18
+ import { accessSync, constants, openSync, readFileSync, readSync, closeSync, readdirSync, realpathSync, statSync } from 'node:fs';
19
+ import { homedir } from 'node:os';
20
+ import { basename, delimiter, dirname, isAbsolute, join, relative } from 'node:path';
21
+ import { T3_SERVER_ENTRY, asarVersion, t3ArchiveSupportsAcp } from "./t3-asar.js";
22
+ const RELEASE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
23
+ const MAYBE_ENTRY = /t3|^appimagekit_|^gearlever_/i;
24
+ const APPIMAGE = /^t3[-_ ]?code.*\.appimage$/i;
25
+ /** `T3-Code-<version>-<arch>.AppImage`, also as AppImageLauncher renames it (`…-x86_64_<hash>.AppImage`). */
26
+ const APPIMAGE_VERSION = /^T3[-_ ]?Code-(\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)-(?:x86_64|x64|amd64|arm64|aarch64|armv7l)(?![0-9A-Za-z])/i;
27
+ const FLATPAK_ID = 'com.t3tools.t3code';
28
+ /** The launcher T3 (nightlies since October 2026) writes to `<T3 home>/bin/t3` for its bundled CLI; for an AppImage it mounts the image itself. */
29
+ export const T3_LAUNCHER_MARKER = "Written by T3 Code: runs the desktop app's bundled t3 CLI.";
30
+ /**
31
+ * The folders scanned on Linux, in XDG precedence: the data home's and each data dir's `applications`, snapd's and
32
+ * flatpak's exported entries, then the usual AppImage folders. SHARDFLUX_APPLICATIONS_DIRS replaces all of them.
33
+ */
34
+ export function linuxApplicationDirs(env) {
35
+ const home = env.HOME || homedir();
36
+ const dataHome = env.XDG_DATA_HOME && isAbsolute(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : join(home, '.local', 'share');
37
+ const dataDirs = (env.XDG_DATA_DIRS || '/usr/local/share:/usr/share').split(':').filter(d => isAbsolute(d));
38
+ return [...new Set([dataHome, ...dataDirs, '/usr/local/share', '/usr/share'].map(d => join(d, 'applications')).concat('/var/lib/snapd/desktop/applications', '/var/lib/flatpak/exports/share/applications', join(dataHome, 'flatpak', 'exports', 'share', 'applications'), join(home, 'Applications'), join(home, 'AppImages'), join(home, '.local', 'bin'), join(home, 'Downloads')))];
39
+ }
40
+ /** The `[Desktop Entry]` group's unlocalized keys (string escapes undone), first value of each key. */
41
+ export function desktopEntry(text) {
42
+ const keys = {};
43
+ let main = false;
44
+ for (const raw of text.split(/\r?\n/)) {
45
+ const line = raw.trim();
46
+ if (!line || line.startsWith('#'))
47
+ continue;
48
+ if (line.startsWith('[')) {
49
+ main = line === '[Desktop Entry]';
50
+ continue;
51
+ }
52
+ const eq = line.indexOf('=');
53
+ if (!main || eq < 1)
54
+ continue;
55
+ const key = line.slice(0, eq).trim();
56
+ if (!key.includes('[') && !(key in keys))
57
+ keys[key] = line.slice(eq + 1).trim().replace(/\\([sntr\\])/g, (_, c) => ({ s: ' ', n: '\n', t: '\t', r: '\r', '\\': '\\' })[c]);
58
+ }
59
+ return keys;
60
+ }
61
+ /** An entry's Exec as argv (the Desktop Entry quoting rules), field codes and flatpak's @@ markers removed; undefined when malformed. */
62
+ export function desktopExec(value) {
63
+ const args = [];
64
+ let current = '', started = false, quoted = false;
65
+ for (let i = 0; i < value.length; i++) {
66
+ const c = value[i];
67
+ if (quoted) {
68
+ if (c === '\\' && i + 1 < value.length && '"`$\\'.includes(value[i + 1]))
69
+ current += value[++i];
70
+ else if (c === '"')
71
+ quoted = false;
72
+ else
73
+ current += c;
74
+ }
75
+ else if (c === '"') {
76
+ quoted = true;
77
+ started = true;
78
+ }
79
+ else if (c === ' ' || c === '\t') {
80
+ if (started)
81
+ args.push(current);
82
+ current = '';
83
+ started = false;
84
+ }
85
+ else {
86
+ current += c;
87
+ started = true;
88
+ }
89
+ }
90
+ if (quoted)
91
+ return undefined;
92
+ if (started)
93
+ args.push(current);
94
+ return args.filter(a => !/^%[fFuUdDnNickvm]$/.test(a) && !/^@@[uf]?$/.test(a)).map(a => a.replace(/%(%|[a-zA-Z])/g, (_, c) => c === '%' ? '%' : ''));
95
+ }
96
+ /** Whether an entry is T3 Code's: its Name, and its window class, URL scheme or flatpak id. */
97
+ function isT3Entry(e) {
98
+ if ((e.Type ?? 'Application') !== 'Application' || !/^T3 Code(?: \(.+\))?$/.test(e.Name ?? ''))
99
+ return false;
100
+ return e.StartupWMClass === 't3code' || (e.MimeType ?? '').split(';').includes('x-scheme-handler/t3code') || e['X-Flatpak'] === FLATPAK_ID;
101
+ }
102
+ const executable = (path) => { try {
103
+ accessSync(path, constants.X_OK);
104
+ return statSync(path).isFile();
105
+ }
106
+ catch {
107
+ return false;
108
+ } };
109
+ const real = (path) => { try {
110
+ return realpathSync(path);
111
+ }
112
+ catch {
113
+ return path;
114
+ } };
115
+ const onPath = (name, env) => isAbsolute(name) ? (executable(name) ? name : undefined) : (env.PATH ?? '').split(delimiter).filter(Boolean).map(dir => join(dir, name)).find(executable);
116
+ const release = (value) => value && RELEASE.test(value) ? value : null;
117
+ /** A type 2 AppImage: an ELF file with `AI\x02` at byte 8. */
118
+ function isAppImage(path) {
119
+ if (/\.appimage$/i.test(path))
120
+ return true;
121
+ let fd;
122
+ try {
123
+ fd = openSync(path, 'r');
124
+ const b = Buffer.alloc(11);
125
+ return readSync(fd, b, 0, 11, 0) === 11 && b.subarray(0, 4).equals(Buffer.from('\x7fELF', 'latin1')) && b.subarray(8, 11).equals(Buffer.from('AI\x02', 'latin1'));
126
+ }
127
+ catch {
128
+ return false;
129
+ }
130
+ finally {
131
+ if (fd !== undefined)
132
+ closeSync(fd);
133
+ }
134
+ }
135
+ /** dpkg's version of the `t3code` package, in T3's own form (dpkg writes `0.0.46~nightly…` for `0.0.46-nightly…`). */
136
+ function dpkgVersion(env) {
137
+ const r = spawnSync('dpkg-query', ['-W', '-f=${Version}', 't3code'], { env, encoding: 'utf8', input: '', timeout: 2000 });
138
+ return r.status === 0 ? release(r.stdout.trim().replace(/^\d+:/, '').replaceAll('~', '-')) : null;
139
+ }
140
+ /** The resources/app.asar of an unpacked Electron app, one or two folders below `root` (a flatpak's files). */
141
+ function findAsar(root) {
142
+ const at = (dir) => { const asar = join(dir, 'resources', 'app.asar'); try {
143
+ return statSync(asar).isFile() ? asar : undefined;
144
+ }
145
+ catch {
146
+ return undefined;
147
+ } };
148
+ const children = (dir) => { try {
149
+ return readdirSync(dir, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => join(dir, d.name));
150
+ }
151
+ catch {
152
+ return [];
153
+ } };
154
+ return at(root) ?? children(root).map(at).find(Boolean) ?? children(root).flatMap(children).slice(0, 64).map(at).find(Boolean);
155
+ }
156
+ /** The install a T3 entry describes, from its program: an AppImage, a flatpak, a snap, or an unpacked app folder. */
157
+ function fromEntry(e, id, dir, env) {
158
+ const exec = desktopExec(e.Exec ?? '');
159
+ if (!exec?.length)
160
+ return undefined;
161
+ let i = 0;
162
+ if (basename(exec[0]) === 'env') {
163
+ i = 1;
164
+ while (i < exec.length && /^[A-Za-z_]\w*=/.test(exec[i]))
165
+ i++;
166
+ }
167
+ const program = exec[i];
168
+ if (!program)
169
+ return undefined;
170
+ const entryVersion = release(e['X-AppImage-Version']);
171
+ if (e['X-Flatpak']) {
172
+ const appId = e['X-Flatpak'];
173
+ const root = dir.endsWith(join('exports', 'share', 'applications')) ? dirname(dirname(dirname(dir))) : '/var/lib/flatpak';
174
+ const files = join(root, 'app', appId, 'current', 'active', 'files');
175
+ const asar = findAsar(files) ?? null;
176
+ const command = exec.find(a => a.startsWith('--command='))?.slice('--command='.length);
177
+ const exe = asar ? join(dirname(dirname(asar)), basename(command || 't3code')) : files;
178
+ return { path: exe, version: (asar ? asarVersion(asar) : null) ?? entryVersion, linux: { format: 'flatpak', desktop_id: id, exec, asar: executable(exe) ? asar : null, flatpak: { id: appId, files } } };
179
+ }
180
+ const resolved = onPath(program, env);
181
+ if (!resolved)
182
+ return undefined;
183
+ if (e['X-SnapInstanceName'] || resolved.startsWith('/snap/bin/') || dir === '/var/lib/snapd/desktop/applications') {
184
+ const name = e['X-SnapInstanceName'] || basename(resolved);
185
+ let version = null;
186
+ try {
187
+ version = release(/^version:\s*['"]?([^'"\s]+)/m.exec(readFileSync(join('/snap', name, 'current', 'meta', 'snap.yaml'), 'utf8'))?.[1]);
188
+ }
189
+ catch { /* no snap metadata */ }
190
+ return { path: resolved, version, linux: { format: 'snap', desktop_id: id, exec, asar: null } };
191
+ }
192
+ const target = real(resolved);
193
+ if (isAppImage(target)) {
194
+ return { path: target, version: entryVersion ?? release(APPIMAGE_VERSION.exec(basename(target))?.[1]), linux: { format: 'appimage', desktop_id: id, exec, asar: null } };
195
+ }
196
+ const asar = join(dirname(target), 'resources', 'app.asar');
197
+ let found = false;
198
+ try {
199
+ found = statSync(asar).isFile();
200
+ }
201
+ catch { /* not an unpacked Electron app */ }
202
+ let deb = false;
203
+ try {
204
+ deb = readFileSync(join(dirname(target), 'resources', 'package-type'), 'utf8').trim() === 'deb';
205
+ }
206
+ catch { /* not from the .deb */ }
207
+ const version = (found ? asarVersion(asar) : null) ?? (deb ? dpkgVersion(env) : null) ?? entryVersion;
208
+ return { path: target, version, linux: { format: deb ? 'deb' : 'directory', desktop_id: id, exec, asar: found ? asar : null } };
209
+ }
210
+ /** The installed T3 Code apps on Linux, from the desktop entries and AppImages in `dirs` (linuxApplicationDirs). */
211
+ export function linuxT3Apps(dirs, env) {
212
+ const found = [], ids = new Set(), loose = [];
213
+ for (const dir of dirs) {
214
+ let names;
215
+ try {
216
+ names = readdirSync(dir);
217
+ }
218
+ catch {
219
+ continue;
220
+ }
221
+ for (const name of names) {
222
+ if (APPIMAGE.test(name)) {
223
+ loose.push(join(dir, name));
224
+ continue;
225
+ }
226
+ if (!name.endsWith('.desktop') || !MAYBE_ENTRY.test(name) || ids.has(name))
227
+ continue;
228
+ let text;
229
+ try {
230
+ text = readFileSync(join(dir, name), 'utf8');
231
+ }
232
+ catch {
233
+ continue;
234
+ }
235
+ // The first entry with an id wins (XDG precedence); a Hidden one removes that id.
236
+ ids.add(name);
237
+ const e = desktopEntry(text);
238
+ if (e.Hidden === 'true' || !isT3Entry(e) || (e.TryExec && !onPath(e.TryExec, env)))
239
+ continue;
240
+ const app = fromEntry(e, name, dir, env);
241
+ if (app && !found.some(f => f.path === app.path))
242
+ found.push(app);
243
+ }
244
+ }
245
+ for (const file of loose) {
246
+ const path = real(file);
247
+ if (!executable(path) || found.some(f => f.path === path))
248
+ continue;
249
+ found.push({ path, version: release(APPIMAGE_VERSION.exec(basename(path))?.[1]), linux: { format: 'appimage', desktop_id: null, exec: [path], asar: null } });
250
+ }
251
+ return found;
252
+ }
253
+ /** `0.0.46` and later (nightlies of 0.0.46 included). */
254
+ const atLeast046 = (version) => {
255
+ const [major = 0, minor = 0, patch = 0] = /^(\d+)\.(\d+)\.(\d+)/.exec(version ?? '')?.slice(1).map(Number) ?? [];
256
+ return major > 0 || minor > 0 || patch >= 46;
257
+ };
258
+ /**
259
+ * Whether this install runs the Shardflux provider (ACP mode): the server in its app.asar has the ACP Registry driver
260
+ * (deb, unpacked installs, flatpak). An AppImage is never mounted to look: its version decides (0.0.46+ have the
261
+ * driver), and T3's own launcher issues its sessions. A snap starts threads over SSH.
262
+ */
263
+ export function linuxSupportsAcp(app) {
264
+ if (app.linux.format === 'appimage')
265
+ return !!app.version && atLeast046(app.version);
266
+ return app.linux.format !== 'snap' && !!app.linux.asar && t3ArchiveSupportsAcp(app.linux.asar);
267
+ }
268
+ /**
269
+ * The bundled T3 server CLI (Electron as Node, ELECTRON_RUN_AS_NODE=1 set by the caller): the app's executable with
270
+ * the entry inside its app.asar; a flatpak's through `flatpak run`; an AppImage's through T3's own launcher
271
+ * (`<T3 home>/bin/t3`, which mounts the image). Undefined when there is none yet.
272
+ */
273
+ export function linuxServerCli(app, t3Home) {
274
+ const { format, asar, flatpak } = app.linux;
275
+ if ((format === 'deb' || format === 'directory') && asar)
276
+ return { command: app.path, args: [join(asar, T3_SERVER_ENTRY)] };
277
+ if (format === 'flatpak' && flatpak && asar) {
278
+ const inside = (path) => `/app/${relative(flatpak.files, path)}`;
279
+ return { command: 'flatpak', args: ['run', `--command=${inside(app.path)}`, '--env=ELECTRON_RUN_AS_NODE=1', flatpak.id, inside(join(asar, T3_SERVER_ENTRY))] };
280
+ }
281
+ if (format === 'appimage') {
282
+ const launcher = join(t3Home, 'bin', 't3');
283
+ try {
284
+ if (executable(launcher) && readFileSync(launcher, 'utf8').includes(T3_LAUNCHER_MARKER))
285
+ return { command: launcher, args: [] };
286
+ }
287
+ catch { /* not written yet */ }
288
+ }
289
+ return undefined;
290
+ }
291
+ /**
292
+ * How T3 is opened (or brought to the front, as a second launch of T3 hands over to the running one): `gtk-launch`
293
+ * with its desktop entry when there is one, else its own program, started detached.
294
+ */
295
+ export function linuxOpenCommands(app, env) {
296
+ const { desktop_id: id, exec, format, flatpak } = app.linux;
297
+ const gtk = id ? onPath('gtk-launch', env) : undefined;
298
+ return [
299
+ ...(gtk && id ? [{ command: gtk, args: [id], detached: false }] : []),
300
+ format === 'flatpak' && flatpak ? { command: 'flatpak', args: ['run', flatpak.id], detached: true } : { command: exec[0], args: exec.slice(1), detached: true },
301
+ ];
302
+ }
303
+ /** Opens T3 Code on Linux. Nothing here ever stops a T3 process: a launcher that hangs is left alone. */
304
+ export async function openLinuxT3(env, app) {
305
+ let last;
306
+ for (const { command, args, detached } of linuxOpenCommands(app, env)) {
307
+ try {
308
+ await new Promise((resolve, reject) => {
309
+ const child = spawn(command, args, { env, stdio: 'ignore', detached });
310
+ if (detached) {
311
+ child.once('spawn', () => { child.unref(); resolve(); });
312
+ child.once('error', reject);
313
+ return;
314
+ }
315
+ const timer = setTimeout(() => { child.unref(); reject(new Error(`${basename(command)} did not answer in time`)); }, 10_000);
316
+ child.once('error', e => { clearTimeout(timer); reject(e); });
317
+ child.once('exit', code => { clearTimeout(timer); if (code === 0)
318
+ resolve();
319
+ else
320
+ reject(new Error(`${basename(command)} exited ${code}`)); });
321
+ });
322
+ return;
323
+ }
324
+ catch (error) {
325
+ last = error;
326
+ }
327
+ }
328
+ throw new Error(`could not open T3 Code (${last instanceof Error ? last.message : String(last)})`);
329
+ }
330
+ /**
331
+ * When T3 last started, as the filesystem records it: the program's access time once it is later than its modification
332
+ * time (dpkg installs both at the build time; relatime updates the access time on the first start, then daily). Null
333
+ * before the first start and on noatime mounts.
334
+ */
335
+ export function linuxLastUsed(path) {
336
+ try {
337
+ const s = statSync(path);
338
+ return s.atimeMs > s.mtimeMs + 1000 ? new Date(s.atimeMs).toISOString() : null;
339
+ }
340
+ catch {
341
+ return null;
342
+ }
343
+ }
@@ -0,0 +1,129 @@
1
+ import type { Workspace } from '@shardflux/sdk';
2
+ import type { Ctx } from './commands.js';
3
+ import { CodedError } from './errors.js';
4
+ import { t3HomeDir } from './t3-provider.js';
5
+ import type { T3LinuxApp } from './t3-linux.js';
6
+ import type { InitialImage } from './initial-prompt.js';
7
+ export declare const T3_BUNDLE_ID = "com.t3tools.t3code";
8
+ export type T3Mode = 'acp' | 'ssh';
9
+ export type AgentApp = 'claude' | 'codex';
10
+ export type OpenIn = 'terminal' | 'claude-desktop' | 'web' | 'codex-app' | 't3';
11
+ /** Where each app's new conversations can open: Claude in a terminal, Claude Desktop, claude.ai or T3; Codex in the Codex app or T3. */
12
+ export declare const OPEN_IN_TARGETS: Record<AgentApp, readonly OpenIn[]>;
13
+ export declare const openInTargets: (app: AgentApp, platform?: NodeJS.Platform) => readonly OpenIn[];
14
+ /** The T3 Code `shard` uses: its .app bundle (macOS) or program (Linux, with how it was installed in `linux`). */
15
+ export interface T3Install {
16
+ path: string;
17
+ version: string | null;
18
+ mode: T3Mode;
19
+ linux?: T3LinuxApp;
20
+ }
21
+ export interface T3Status {
22
+ installed: boolean;
23
+ mode: T3Mode | null;
24
+ connected: boolean;
25
+ version: string | null;
26
+ last_used_at: string | null;
27
+ }
28
+ export interface T3ThreadRequest {
29
+ app: AgentApp;
30
+ prompt: string;
31
+ model?: string;
32
+ effort?: string;
33
+ images?: readonly InitialImage[];
34
+ }
35
+ /** Why a requested `--effort` reached T3 as none: the model offers no such level, or its levels are not known yet. */
36
+ export type EffortReason = 'not_offered' | 'models_not_known';
37
+ export interface T3ThreadResult {
38
+ mode: T3Mode;
39
+ t3_thread_id: string;
40
+ conversation_id: string | null;
41
+ workspace: {
42
+ workspace_key: string;
43
+ id: string;
44
+ } | null;
45
+ model: string | null;
46
+ effort: string | null;
47
+ effort_reason?: EffortReason;
48
+ }
49
+ /**
50
+ * Native lookups that tests replace: Spotlight's last-used dates of app bundles, one `mdls` for all of them (macOS), or
51
+ * when each program last started, from its access time (Linux, t3-linux.ts).
52
+ */
53
+ export declare const t3Native: {
54
+ lastUsedMany(paths: readonly string[], env: Record<string, string | undefined>, platform?: NodeJS.Platform): Record<string, string | null>;
55
+ };
56
+ /** Spotlight takes about 40 ms per call, so the dates are kept for a few minutes (app-usage.json in the config dir). */
57
+ export declare const LAST_USED_TTL_MS: number;
58
+ export declare function lastUsedDates(ctx: Ctx, paths: readonly string[], now?: number): Record<string, string | null>;
59
+ /** The installed T3 Code, preferring a build with ACP providers when several channels are installed. */
60
+ export declare function t3Install(env: Record<string, string | undefined>, platform?: NodeJS.Platform, dirs?: string[]): T3Install | undefined;
61
+ export { t3HomeDir };
62
+ /** The Shardflux provider instance in T3's server settings (read only; every `shard` run writes it, see t3-provider.ts). */
63
+ export declare function t3ProviderRegistered(env: Record<string, string | undefined>): boolean;
64
+ /**
65
+ * T3 is connected when a new thread can start in it now: in ACP mode the Shardflux provider is in T3's settings and
66
+ * this computer is signed in; in SSH mode this project has a T3 SSH environment set up from this computer (signed in).
67
+ * `last_used_at` is looked up only when asked for (`agents targets`).
68
+ */
69
+ export declare function t3Status(ctx: Ctx, opts?: {
70
+ project?: string | null;
71
+ install?: T3Install | null;
72
+ lastUsed?: boolean;
73
+ }): T3Status;
74
+ export interface OpenInChoice {
75
+ target: OpenIn;
76
+ saved: boolean;
77
+ }
78
+ /**
79
+ * Where each app's new conversations open for `project`: the choice saved with `agents targets --open-in` while it is
80
+ * available, else T3 when it is connected and the most recently used of the installed desktop apps, else a terminal
81
+ * (Claude) or the Codex app. Lookups run only when an answer needs them: none for a saved terminal, web or Codex app
82
+ * choice, and Spotlight only when T3 is installed and connected (cached, see lastUsedDates).
83
+ */
84
+ export declare function openInFor(ctx: Ctx, project: string | null, known?: {
85
+ clients?: readonly string[];
86
+ }): Record<AgentApp, OpenInChoice>;
87
+ export declare function openInChoices(ctx: Ctx, project: string | null, known?: {
88
+ clients?: readonly string[];
89
+ }): Record<AgentApp, OpenIn>;
90
+ /** `--open-in claude=t3,codex=t3`: validated against what is installed, then saved in agents.json. */
91
+ export declare function saveOpenIn(ctx: Ctx, spec: string, status: T3Status, clients: readonly string[]): void;
92
+ /** A T3 thread was started, then a later step failed: the error names the thread so a client opens it, never starts another. */
93
+ export declare function t3OpenFailed(error: unknown, ref: {
94
+ t3_thread_id: string;
95
+ conversation_id?: string | null;
96
+ workspace_key?: string | null;
97
+ }): CodedError;
98
+ export declare const t3NotInstalled: () => CodedError;
99
+ /** The first line of the prompt, as T3 titles a thread before it names it. */
100
+ export declare function t3ThreadTitle(prompt: string): string;
101
+ /**
102
+ * Opens T3 Code (or brings it to the front): the install `shard` chose. On macOS `open -a <path>` (every T3 channel
103
+ * shares one bundle id, so `open -b` could start another installed channel); on Linux its desktop entry with
104
+ * `gtk-launch`, else its program (t3-linux.ts).
105
+ */
106
+ export declare function openT3(env: Record<string, string | undefined>, install: Pick<T3Install, 'path' | 'version' | 'linux'> | undefined): Promise<void>;
107
+ /** Starts the thread in the VM's T3 server (T3_THREAD_SCRIPT); the prompt travels on stdin, never in argv. */
108
+ export declare function startSshThread(ctx: Ctx, ws: Workspace, version: string | undefined, projectPath: string, request: T3ThreadRequest): Promise<T3ThreadResult>;
109
+ interface Descriptor {
110
+ serverVersion?: string;
111
+ orchestrationProtocolVersion?: number;
112
+ }
113
+ export interface LocalT3Server {
114
+ origin: string;
115
+ descriptor: Descriptor;
116
+ base: string;
117
+ }
118
+ interface T3Session {
119
+ token: string;
120
+ sessionId: string;
121
+ }
122
+ /** The running desktop server; T3 is opened (which starts it) when none answers. */
123
+ export declare function localT3Server(ctx: Ctx, install: T3Install, waitMs?: number): Promise<LocalT3Server>;
124
+ /** One Effect RPC request over T3's WebSocket (JSON frames: Request → Exit), authorized by a ticket from the session. */
125
+ export declare function t3Rpc(server: LocalT3Server, session: T3Session, tag: string, payload: unknown, signal: AbortSignal, timeoutMs?: number): Promise<unknown>;
126
+ /** Starts a T3 thread on the Shardflux provider (ACP mode) and brings T3 to the front. */
127
+ export declare function startAcpThread(ctx: Ctx, install: T3Install, folder: string, request: T3ThreadRequest, waitMs?: number): Promise<T3ThreadResult>;
128
+ /** The T3 thread of a Shardflux conversation started in T3 (ACP mode): the one remembered, else a search of recent Shardflux threads. */
129
+ export declare function findAcpThread(ctx: Ctx, install: T3Install, conversationId: string): Promise<string | null>;