@shardflux/cli 0.17.0 → 0.18.0-next.5

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/t3-mode.js CHANGED
@@ -3,11 +3,12 @@
3
3
  * thread, so the thread is made through T3's own server API, with a short-lived admin session that T3's own CLI
4
4
  * issues and revokes, and T3 is brought to the front.
5
5
  *
6
- * - **ACP mode** (a T3 build with ACP providers, 0.0.46+): T3's server runs on this Mac and Shardflux is one of its
7
- * providers (`shard acp`, registered by every `shard` run). The thread is launched on the running desktop server
8
- * (`server-runtime.json`) over its WebSocket RPC (`orchestration.launchThread`) with the Shardflux provider, which
9
- * then gives the thread its own cloud VM. The session is issued by the T3 server the app bundles
10
- * (`ELECTRON_RUN_AS_NODE=1 <app>/Contents/MacOS/<exe> <app.asar>/apps/server/dist/bin.mjs auth session issue`).
6
+ * - **ACP mode** (a T3 build with ACP providers, 0.0.46+): T3's server runs on this computer (macOS or Linux) and
7
+ * Shardflux is one of its providers (`shard acp`, registered by every `shard` run). The thread is launched on the
8
+ * running desktop server (`server-runtime.json`) over its WebSocket RPC (`orchestration.launchThread`) with the
9
+ * Shardflux provider, which then gives the thread its own cloud VM. The session is issued by the T3 server the app
10
+ * bundles (`ELECTRON_RUN_AS_NODE=1 <app>/Contents/MacOS/<exe> <app.asar>/apps/server/dist/bin.mjs auth session
11
+ * issue`; on Linux the app's `t3code` with its `resources/app.asar`, or T3's own `~/.t3/bin/t3` for an AppImage).
11
12
  * - **SSH mode** (stable 0.0.45): T3's server runs in the project VM. `T3_THREAD_SCRIPT` runs there and dispatches
12
13
  * `thread.create` and `thread.turn.start` over loopback (POST /api/orchestration/dispatch).
13
14
  *
@@ -19,13 +20,13 @@ import { spawn, spawnSync } from 'node:child_process';
19
20
  import { randomUUID } from 'node:crypto';
20
21
  import { readFileSync, realpathSync } from 'node:fs';
21
22
  import { writePrivateFile } from "./credentials.js";
22
- import { homedir } from 'node:os';
23
23
  import { basename, join } from 'node:path';
24
24
  import { setTimeout as delay } from 'node:timers/promises';
25
25
  import { CodedError, UsageError, describeError } from "./errors.js";
26
26
  import { AgentStateStore } from "./agent-state.js";
27
27
  import { applicationDirs, desktopAppPath, t3Apps } from "./agent-targets.js";
28
- import { configureT3Provider, t3BundleSupportsAcp } from "./t3-provider.js";
28
+ import { configureT3Provider, t3BundleSupportsAcp, t3HomeDir } from "./t3-provider.js";
29
+ import { linuxLastUsed, linuxServerCli, linuxSupportsAcp, openLinuxT3 } from "./t3-linux.js";
29
30
  import { acpSignedIn } from "./acp-login.js";
30
31
  import { AcpModelCache } from "./acp-models.js";
31
32
  import { nativeModelOptions } from "./acp-catalog.js";
@@ -36,6 +37,7 @@ import { uploadInitialImages } from "./initial-prompt.js";
36
37
  export const T3_BUNDLE_ID = 'com.t3tools.t3code';
37
38
  /** 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. */
38
39
  export const OPEN_IN_TARGETS = { claude: ['terminal', 'claude-desktop', 'web', 't3'], codex: ['codex-app', 't3'] };
40
+ export const openInTargets = (app, platform = process.platform) => platform === 'linux' && app === 'codex' ? ['terminal', 't3'] : OPEN_IN_TARGETS[app];
39
41
  /** T3 0.0.45's built-in defaults (DEFAULT_MODEL_BY_PROVIDER), used only when T3 in the VM has not described its models yet. */
40
42
  const T3_DEFAULT_MODEL = { claude: 'claude-fable-5-1', codex: 'gpt-6-astra' };
41
43
  const T3_START_WAIT_MS = 30_000;
@@ -46,11 +48,16 @@ const isoDate = (raw) => {
46
48
  const parsed = SPOTLIGHT_DATE.test(raw) ? new Date(raw.replace(' ', 'T').replace(' ', '')) : null;
47
49
  return parsed && !Number.isNaN(parsed.getTime()) ? parsed.toISOString() : null;
48
50
  };
49
- /** Native lookups that tests replace: Spotlight's last-used dates of app bundles, one `mdls` for all of them. */
51
+ /**
52
+ * Native lookups that tests replace: Spotlight's last-used dates of app bundles, one `mdls` for all of them (macOS), or
53
+ * when each program last started, from its access time (Linux, t3-linux.ts).
54
+ */
50
55
  export const t3Native = {
51
- lastUsedMany(paths, env) {
56
+ lastUsedMany(paths, env, platform = process.platform) {
52
57
  if (!paths.length)
53
58
  return {};
59
+ if (platform !== 'darwin')
60
+ return Object.fromEntries(paths.map(path => [path, linuxLastUsed(path)]));
54
61
  const r = spawnSync('mdls', ['-name', 'kMDItemLastUsedDate', '-raw', ...paths], { env, encoding: 'utf8', input: '', timeout: 3000 });
55
62
  // -raw separates the values with NUL; a bundle Spotlight has no date for prints "(null)".
56
63
  const values = r.status === 0 ? r.stdout.split('\0').map(v => v.trim()) : [];
@@ -76,16 +83,16 @@ export function lastUsedDates(ctx, paths, now = Date.now()) {
76
83
  catch { /* a cache only */ }
77
84
  return dates;
78
85
  }
86
+ /** Whether an installed T3 runs the Shardflux provider: its server has the ACP Registry driver (t3-asar.ts, t3-linux.ts). */
87
+ const supportsAcp = (app) => app.linux ? linuxSupportsAcp({ ...app, linux: app.linux }) : t3BundleSupportsAcp(app.path);
79
88
  /** The installed T3 Code, preferring a build with ACP providers when several channels are installed. */
80
- export function t3Install(env, platform = process.platform) {
81
- const apps = t3Apps(applicationDirs(env), platform);
82
- const acp = apps.find(app => t3BundleSupportsAcp(app.path));
89
+ export function t3Install(env, platform = process.platform, dirs = applicationDirs(env, platform)) {
90
+ const apps = t3Apps(dirs, platform, env);
91
+ const acp = apps.find(supportsAcp);
83
92
  const app = acp ?? apps[0];
84
- return app ? { path: app.path, version: app.version, mode: acp ? 'acp' : 'ssh' } : undefined;
85
- }
86
- export function t3HomeDir(env) {
87
- return env.T3CODE_HOME || join(env.HOME || homedir(), '.t3');
93
+ return app ? { path: app.path, version: app.version, mode: acp ? 'acp' : 'ssh', ...(app.linux ? { linux: app.linux } : {}) } : undefined;
88
94
  }
95
+ export { t3HomeDir };
89
96
  /** The Shardflux provider instance in T3's server settings (read only; every `shard` run writes it, see t3-provider.ts). */
90
97
  export function t3ProviderRegistered(env) {
91
98
  try {
@@ -137,7 +144,7 @@ export function openInFor(ctx, project, known = {}) {
137
144
  const claudeDesktop = () => known.clients ? known.clients.includes('claude-desktop') : desktopAppPath(dirs, 'Claude.app', 'com.anthropic.claudefordesktop') !== undefined;
138
145
  const valid = (app) => {
139
146
  const value = saved[app];
140
- if (typeof value !== 'string' || !OPEN_IN_TARGETS[app].includes(value))
147
+ if (typeof value !== 'string' || !openInTargets(app).includes(value))
141
148
  return undefined;
142
149
  return value === 't3' ? (t3() ? value : undefined) : value === 'claude-desktop' ? (claudeDesktop() ? value : undefined) : value;
143
150
  };
@@ -171,8 +178,8 @@ export function saveOpenIn(ctx, spec, status, clients) {
171
178
  const [app, target, extra] = part.split('=').map(s => s.trim());
172
179
  if (app !== 'claude' && app !== 'codex' || !target || extra !== undefined)
173
180
  throw new UsageError('--open-in takes app=target pairs, e.g. claude=t3,codex=t3.');
174
- if (!OPEN_IN_TARGETS[app].includes(target))
175
- throw new UsageError(`--open-in: ${app} opens in ${OPEN_IN_TARGETS[app].join(', ')}.`);
181
+ if (!openInTargets(app).includes(target))
182
+ throw new UsageError(`--open-in: ${app} opens in ${openInTargets(app).join(', ')}.`);
176
183
  if (target === 't3' && !status.installed)
177
184
  throw t3NotInstalled();
178
185
  if (!available(target, status, clients))
@@ -203,11 +210,16 @@ export function t3ThreadTitle(prompt) {
203
210
  return line.length > 80 ? `${line.slice(0, 79).trimEnd()}…` : line;
204
211
  }
205
212
  /**
206
- * Opens T3 Code (or brings it to the front): the install `shard` chose (`open -a <path>`). Every T3 channel shares one
207
- * bundle id, so `open -b` could start another installed channel.
213
+ * Opens T3 Code (or brings it to the front): the install `shard` chose. On macOS `open -a <path>` (every T3 channel
214
+ * shares one bundle id, so `open -b` could start another installed channel); on Linux its desktop entry with
215
+ * `gtk-launch`, else its program (t3-linux.ts).
208
216
  */
209
- export async function openT3(env, appPath) {
210
- const child = spawn('open', ['-a', appPath], { env, stdio: 'ignore' });
217
+ export async function openT3(env, install) {
218
+ if (!install)
219
+ throw t3NotInstalled();
220
+ if (install.linux)
221
+ return openLinuxT3(env, { path: install.path, version: install.version, linux: install.linux });
222
+ const child = spawn('open', ['-a', install.path], { env, stdio: 'ignore' });
211
223
  await new Promise((resolve, reject) => {
212
224
  const timer = setTimeout(() => { child.kill('SIGKILL'); reject(new Error('T3 Code did not open in time')); }, 10_000);
213
225
  child.once('error', e => { clearTimeout(timer); reject(e); });
@@ -307,7 +319,7 @@ export async function localT3Server(ctx, install, waitMs = T3_START_WAIT_MS) {
307
319
  const found = await runningServer(base, ctx.signal);
308
320
  if (found)
309
321
  return found;
310
- await openT3(ctx.io.env, install.path);
322
+ await openT3(ctx.io.env, install);
311
323
  const deadline = Date.now() + waitMs;
312
324
  while (Date.now() < deadline) {
313
325
  await delay(500, undefined, { signal: ctx.signal });
@@ -317,7 +329,17 @@ export async function localT3Server(ctx, install, waitMs = T3_START_WAIT_MS) {
317
329
  }
318
330
  throw t3Unreachable(`T3 Code’s server did not answer within ${Math.round(waitMs / 1000)} s.`);
319
331
  }
320
- function serverCli(install) {
332
+ /**
333
+ * The server CLI the app bundles: on macOS the bundle's executable with `<app.asar>/apps/server/dist/bin.mjs`; on Linux
334
+ * the app's own executable the same way, or for an AppImage T3's own launcher (linuxServerCli).
335
+ */
336
+ function serverCli(install, env) {
337
+ if (install.linux) {
338
+ const cli = linuxServerCli({ path: install.path, version: install.version, linux: install.linux }, t3HomeDir(env));
339
+ if (!cli)
340
+ throw t3Unreachable(`T3 Code has not written its command line launcher (${join(t3HomeDir(env), 'bin', 't3')}) yet.`, 'Open T3 Code once, then run this again.');
341
+ return cli;
342
+ }
321
343
  const r = spawnSync('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleExecutable', join(install.path, 'Contents', 'Info.plist')], { encoding: 'utf8', input: '', timeout: 2000 });
322
344
  const exe = r.status === 0 ? r.stdout.trim() : '';
323
345
  if (!exe || exe.includes('/'))
@@ -326,7 +348,7 @@ function serverCli(install) {
326
348
  }
327
349
  /** Runs the T3 server CLI the app bundles (Electron as Node), the same code the desktop runs as its backend. */
328
350
  function t3Cli(ctx, install, args) {
329
- const cli = serverCli(install);
351
+ const cli = serverCli(install, ctx.io.env);
330
352
  return new Promise((resolve, reject) => {
331
353
  const child = spawn(cli.command, [...cli.args, ...args], { env: { ...ctx.io.env, ELECTRON_RUN_AS_NODE: '1' }, stdio: ['ignore', 'pipe', 'pipe'], signal: ctx.signal });
332
354
  let stdout = '', stderr = '';
@@ -601,7 +623,7 @@ export async function startAcpThread(ctx, install, folder, request, waitMs = CON
601
623
  const id = objectValue(launched).threadId;
602
624
  const t3ThreadId = typeof id === 'string' ? id : threadId;
603
625
  // The thread exists from here on: T3 comes to the front first, and a failure after this names the thread.
604
- const opened = await openT3(ctx.io.env, install.path).then(() => undefined, (error) => error);
626
+ const opened = await openT3(ctx.io.env, install).then(() => undefined, (error) => error);
605
627
  let conversation = null;
606
628
  try {
607
629
  conversation = await threadConversation(ctx, server, session, t3ThreadId, realpathSync(folder), known, waitMs);
@@ -1,6 +1,8 @@
1
- /** Every shard invocation registers the one installed T3 provider without adding terminal noise. */
2
- export declare function registerInstalledT3(env: Record<string, string | undefined>, dir: string, signal?: AbortSignal): Promise<void>;
3
- /** A version number alone is insufficient: the installed 0.0.45 desktop lacks the upstream ACP driver. */
1
+ /** Every shard invocation registers the one installed T3 provider without adding terminal noise (macOS and Linux). */
2
+ export declare function registerInstalledT3(env: Record<string, string | undefined>, dir: string, signal?: AbortSignal, platform?: NodeJS.Platform): Promise<void>;
3
+ /** Whether a macOS T3 bundle's server has the ACP Registry driver (its Contents/Resources/app.asar; see t3-asar.ts). */
4
4
  export declare function t3BundleSupportsAcp(app: string): boolean;
5
+ /** T3's home, as the desktop resolves it on macOS and Linux: T3CODE_HOME (trimmed), else ~/.t3. */
6
+ export declare function t3HomeDir(env: Record<string, string | undefined>): string;
5
7
  /** Desktop 0.0.45 watches this server settings file, including symlink targets. No encrypted stores. */
6
8
  export declare function configureT3Provider(env: Record<string, string | undefined>, dir: string, remove?: boolean, signal?: AbortSignal): Promise<boolean>;
@@ -1,70 +1,33 @@
1
- import { readFileSync, realpathSync, openSync, readSync, closeSync, statSync } from 'node:fs';
1
+ import { readFileSync, realpathSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { writePrivateFile } from "./credentials.js";
5
5
  import { withTargetLock } from "./target-lock.js";
6
6
  import { installedTargets } from "./agent-targets.js";
7
+ import { t3ArchiveSupportsAcp } from "./t3-asar.js";
7
8
  const ID = 'shardflux';
8
9
  const provider = { driver: 'acpRegistry', displayName: 'Shardflux', enabled: true, config: { source: 'local', commandPath: 'shard', commandArgs: ['acp'] } };
9
- const support = new Map();
10
- /** Every shard invocation registers the one installed T3 provider without adding terminal noise. */
11
- export async function registerInstalledT3(env, dir, signal) {
12
- if (process.platform !== 'darwin')
13
- return;
14
- if (!installedTargets(env).targets.includes('t3'))
10
+ /** Every shard invocation registers the one installed T3 provider without adding terminal noise (macOS and Linux). */
11
+ export async function registerInstalledT3(env, dir, signal, platform = process.platform) {
12
+ if (!installedTargets(env, platform).targets.includes('t3'))
15
13
  return;
16
14
  try {
17
15
  await configureT3Provider(env, dir, false, signal);
18
16
  }
19
17
  catch { /* User-owned malformed or conflicting settings stay intact. */ }
20
18
  }
21
- /** A version number alone is insufficient: the installed 0.0.45 desktop lacks the upstream ACP driver. */
19
+ /** Whether a macOS T3 bundle's server has the ACP Registry driver (its Contents/Resources/app.asar; see t3-asar.ts). */
22
20
  export function t3BundleSupportsAcp(app) {
23
- const archive = join(app, 'Contents/Resources/app.asar');
24
- let fd;
25
- try {
26
- const stat = statSync(archive), stamp = `${stat.size}:${stat.mtimeMs}`;
27
- if (support.get(archive)?.stamp === stamp)
28
- return support.get(archive).capable;
29
- fd = openSync(archive, 'r');
30
- const sizes = Buffer.alloc(16);
31
- if (readSync(fd, sizes, 0, 16, 0) !== 16)
32
- return false;
33
- const headerSize = sizes.readUInt32LE(12);
34
- if (headerSize < 1 || headerSize > 16 << 20)
35
- return false;
36
- const header = Buffer.alloc(headerSize);
37
- if (readSync(fd, header, 0, headerSize, 16) !== headerSize)
38
- return false;
39
- let entry = JSON.parse(header.toString());
40
- for (const part of ['apps', 'server', 'dist', 'bin.mjs']) {
41
- const child = entry.files?.[part];
42
- if (!child)
43
- return false;
44
- entry = child;
45
- }
46
- const size = entry.size, offset = Number(entry.offset) + 8 + sizes.readUInt32LE(4);
47
- if (!Number.isSafeInteger(size) || !size || size < 1 || size > 64 << 20 || !Number.isSafeInteger(offset) || offset < 16 || offset + size > stat.size)
48
- return false;
49
- const bytes = entry.unpacked ? readFileSync(join(`${archive}.unpacked`, 'apps/server/dist/bin.mjs')) : Buffer.alloc(size);
50
- if (!entry.unpacked && readSync(fd, bytes, 0, size, offset) !== size)
51
- return false;
52
- const capable = /ProviderDriverKind\.make\(["']acpRegistry["']\)/.test(bytes.toString());
53
- support.set(archive, { stamp, capable });
54
- return capable;
55
- }
56
- catch {
57
- return false;
58
- }
59
- finally {
60
- if (fd !== undefined)
61
- closeSync(fd);
62
- }
21
+ return t3ArchiveSupportsAcp(join(app, 'Contents/Resources/app.asar'));
22
+ }
23
+ /** T3's home, as the desktop resolves it on macOS and Linux: T3CODE_HOME (trimmed), else ~/.t3. */
24
+ export function t3HomeDir(env) {
25
+ return env.T3CODE_HOME?.trim() || join(env.HOME || homedir(), '.t3');
63
26
  }
64
27
  /** Desktop 0.0.45 watches this server settings file, including symlink targets. No encrypted stores. */
65
28
  export async function configureT3Provider(env, dir, remove = false, signal) {
66
29
  return withTargetLock(dir, () => {
67
- let file = join(env.T3CODE_HOME || join(env.HOME || homedir(), '.t3'), 'userdata', 'settings.json');
30
+ let file = join(t3HomeDir(env), 'userdata', 'settings.json');
68
31
  let raw = '{}';
69
32
  try {
70
33
  file = realpathSync(file);
package/dist/update.js CHANGED
@@ -264,6 +264,22 @@ export async function update(ctx, values, deps = {}) {
264
264
  * runs the code from before the update).
265
265
  */
266
266
  async function refreshMenubar(ctx, menubar, runtime, run, env) {
267
+ if (menubar.platform === 'linux') {
268
+ const { LinuxDesktopManager } = await import("./desktop-linux.js");
269
+ const desktop = new LinuxDesktopManager({ home: ctx.io.env.HOME, env: ctx.io.env });
270
+ const before = await desktop.status(ctx.store.dir);
271
+ if (!before.installed || new AgentStateStore(ctx.store.dir).read().menubar_stopped === true)
272
+ return { status: 'off' };
273
+ if (runtime && run && env) {
274
+ const result = await run([runtime.node, runtime.script, '--api-url', ctx.apiUrl, 'menubar', 'start', '--json'], { ...env, SHARDFLUX_CONFIG_DIR: ctx.store.dir, SHARDFLUX_NO_UPDATE_CHECK: '1' }, ctx.signal);
275
+ if (result.code !== 0)
276
+ return { status: 'failed' };
277
+ }
278
+ else
279
+ await desktop.start(ctx.store.dir, ctx.apiUrl);
280
+ const after = await desktop.status(ctx.store.dir);
281
+ return after.version !== before.version ? { status: 'restarted', version: after.version } : { status: 'current' };
282
+ }
267
283
  if (menubar.platform !== 'darwin')
268
284
  return { status: 'off' };
269
285
  if (new AgentStateStore(ctx.store.dir).read().menubar_stopped === true)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shardflux/cli",
3
- "version": "0.17.0",
3
+ "version": "0.18.0-next.5",
4
4
  "type": "module",
5
5
  "description": "shard: the Shardflux command line. Create an account, sign in, set up keys and billing, then open, run commands in, move files to, suspend, resume and fork serverless agent workspaces.",
6
6
  "license": "Apache-2.0",
@@ -43,29 +43,17 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
+ "@shardflux/sdk": "0.24.0-next.5",
46
47
  "classic-level": "3.0.0",
47
48
  "smol-toml": "1.9.0",
48
49
  "snappyjs": "0.7.0",
49
- "yaml": "2.9.1",
50
- "@shardflux/sdk": "^0.23.0"
51
- },
52
- "devDependencies": {
53
- "@eslint/js": "10.0.1",
54
- "@types/node": "24.13.6",
55
- "eslint": "10.11.0",
56
- "playwright": "1.62.1",
57
- "typescript": "5.9.3",
58
- "typescript-eslint": "8.70.1"
50
+ "yaml": "2.9.1"
59
51
  },
60
52
  "optionalDependencies": {
61
- "@shardflux/menubar-darwin": "^0.5.0"
62
- },
63
- "scripts": {
64
- "build": "node scripts/build.mjs",
65
- "typecheck": "tsc -p tsconfig.json --noEmit",
66
- "lint": "eslint .",
67
- "test": "node --test test/*.test.ts"
53
+ "@shardflux/menubar-darwin": "^0.5.0",
54
+ "@shardflux/desktop-linux-x64": "0.1.0-next.5",
55
+ "@shardflux/desktop-linux-arm64": "0.1.0-next.5"
68
56
  },
69
57
  "main": "./dist/index.js",
70
58
  "types": "./dist/index.d.ts"
71
- }
59
+ }