@shardflux/cli 0.25.0 → 0.28.1

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 (93) hide show
  1. package/CHANGELOG.md +154 -1
  2. package/README.md +212 -36
  3. package/dist/account-commands.d.ts +2 -4
  4. package/dist/account-commands.js +2 -6
  5. package/dist/account-pool.d.ts +11 -1
  6. package/dist/account-pool.js +48 -0
  7. package/dist/agent-commands.js +4 -3
  8. package/dist/agent-git-commands.d.ts +1 -2
  9. package/dist/agent-git-commands.js +2 -21
  10. package/dist/agent-models.d.ts +31 -1
  11. package/dist/agent-models.js +27 -4
  12. package/dist/agent-setup.d.ts +4 -2
  13. package/dist/agent-setup.js +6 -4
  14. package/dist/agent-shell.d.ts +2 -0
  15. package/dist/agent-shell.js +5 -0
  16. package/dist/args.d.ts +1 -1
  17. package/dist/args.js +67 -12
  18. package/dist/board-args.js +8 -3
  19. package/dist/board-commands.d.ts +7 -3
  20. package/dist/board-commands.js +22 -12
  21. package/dist/brain-commands.d.ts +2 -0
  22. package/dist/brain-commands.js +78 -0
  23. package/dist/cli-proxy.d.ts +1 -13
  24. package/dist/cli-proxy.js +2 -27
  25. package/dist/client-settings.d.ts +2 -1
  26. package/dist/client-settings.js +14 -2
  27. package/dist/codex-cloud.d.ts +2 -0
  28. package/dist/codex-cloud.js +17 -12
  29. package/dist/commands.js +8 -0
  30. package/dist/conversation-commands.d.ts +5 -1
  31. package/dist/conversation-commands.js +58 -6
  32. package/dist/conversation-terminal.js +2 -2
  33. package/dist/desktop-linux.d.ts +12 -1
  34. package/dist/desktop-linux.js +47 -18
  35. package/dist/device-approvals.js +24 -2
  36. package/dist/device-bridge.d.ts +24 -4
  37. package/dist/device-bridge.js +181 -8
  38. package/dist/device-folders.d.ts +6 -1
  39. package/dist/device-folders.js +6 -2
  40. package/dist/device-helper.d.ts +8 -0
  41. package/dist/device-helper.js +71 -12
  42. package/dist/device-mode.d.ts +44 -0
  43. package/dist/device-mode.js +119 -0
  44. package/dist/drive-commands.d.ts +2 -2
  45. package/dist/drive-commands.js +1 -1
  46. package/dist/format.js +3 -0
  47. package/dist/forwarder-refresh.d.ts +15 -0
  48. package/dist/forwarder-refresh.js +32 -0
  49. package/dist/handler-loader.d.ts +3 -0
  50. package/dist/handler-loader.js +43 -0
  51. package/dist/handlers.js +4 -2
  52. package/dist/land.d.ts +10 -5
  53. package/dist/land.js +56 -9
  54. package/dist/linux-helper.js +1 -2
  55. package/dist/mac-app.d.ts +11 -0
  56. package/dist/mac-app.js +11 -0
  57. package/dist/machine-setup-guard.d.ts +9 -0
  58. package/dist/machine-setup-guard.js +22 -0
  59. package/dist/machine-setup.d.ts +1 -8
  60. package/dist/machine-setup.js +2 -22
  61. package/dist/main.js +19 -16
  62. package/dist/menubar.d.ts +18 -13
  63. package/dist/menubar.js +60 -50
  64. package/dist/orchestrator-args.d.ts +7 -0
  65. package/dist/orchestrator-args.js +64 -0
  66. package/dist/orchestrator-commands.d.ts +43 -0
  67. package/dist/orchestrator-commands.js +260 -0
  68. package/dist/person-scope.d.ts +20 -0
  69. package/dist/person-scope.js +61 -0
  70. package/dist/project-hub-seed.d.ts +20 -0
  71. package/dist/project-hub-seed.js +59 -0
  72. package/dist/registry.d.ts +1 -1
  73. package/dist/registry.js +2 -1
  74. package/dist/router-commands.d.ts +12 -21
  75. package/dist/router-commands.js +5 -59
  76. package/dist/startup-housekeeping.d.ts +5 -0
  77. package/dist/startup-housekeeping.js +30 -0
  78. package/dist/t3-launch.js +2 -2
  79. package/dist/t3-provider.js +4 -2
  80. package/dist/task-land-hub.py +16 -1
  81. package/dist/this-computer.d.ts +4 -2
  82. package/dist/this-computer.js +20 -2
  83. package/dist/thread-on-computer.js +2 -2
  84. package/dist/timing.js +3 -1
  85. package/dist/transcribe-commands.js +6 -2
  86. package/dist/update.js +4 -2
  87. package/dist/wait-status.d.ts +1 -1
  88. package/dist/wait-status.js +2 -0
  89. package/dist/worktree-twin-engine.d.ts +1 -1
  90. package/dist/worktree-twin-engine.js +10 -7
  91. package/dist/worktree-twin.d.ts +6 -0
  92. package/dist/worktree-twin.js +8 -1
  93. package/package.json +12 -6
package/dist/main.js CHANGED
@@ -14,27 +14,17 @@ import { homedir, hostname } from 'node:os';
14
14
  import { statSync } from 'node:fs';
15
15
  import { SDK_VERSION, Shardflux, ShardfluxAccount, aboutSeconds, isSessionToken } from '@shardflux/sdk';
16
16
  import { CLI_VERSION, CENTER_COMMAND, GUIDE_COMMAND, ONBOARDING_COMMAND, PROJECT_COMMAND, parseCommandLine, resolveApiUrl, resolveWake, resolveWakeTimeout } from "./args.js";
17
- import { commandCenter } from "./command-center.js";
18
- import { launchProjectCommand } from "./conversation-terminal.js";
19
- import { runAcp } from "./acp.js";
20
17
  import { registerInstalledT3 } from "./t3-provider.js";
21
- import { refreshForwarderAgent } from "./cli-proxy.js";
22
- import { runAcpLogin } from "./acp-login.js";
23
- import { terminalOnboarding } from "./terminal-onboarding.js";
24
- import { codingAgentShell, setupGuideText } from "./agent-setup.js";
25
- import { agentSetupGuide } from "./account-commands.js";
26
- import { tryLinuxDesktop } from "./desktop-linux.js";
18
+ import { codingAgentShell } from "./agent-shell.js";
27
19
  import { startLinuxHelper } from "./linux-helper.js";
28
- import { runDeviceHelper } from "./device-helper.js";
29
- import { housekeeping, startupHousekeeping } from "./agent-git-commands.js";
20
+ import { housekeepingIfTracked, startupHousekeeping } from "./startup-housekeeping.js";
30
21
  import { CredentialStore, configDir } from "./credentials.js";
31
22
  import { recordInstall } from "./update.js";
32
23
  import { refreshMenubarInBackground, runMenubarRefresh } from "./menubar.js";
33
24
  import { NPM_CLIENTS, npmLatestAll, npmRegistry, withRegistryLatest } from "./registry.js";
34
25
  import { AuthConfigError, EXIT, ExecutionPendingError, InterruptedError, UsageError, describeError, exitCodeFor, ownHint, redact } from "./errors.js";
35
26
  import { errorText, feedbackCommand, json, time } from "./format.js";
36
- import { HANDLERS } from "./handlers.js";
37
- import { SETUP_VM_ENV, setupGuard } from "./machine-setup.js";
27
+ import { SETUP_VM_ENV, setupGuard } from "./machine-setup-guard.js";
38
28
  import { makeFetch } from "./http.js";
39
29
  import { Input } from "./input.js";
40
30
  import { TimingRecorder, notices, timingJson, timingText } from "./timing.js";
@@ -137,6 +127,8 @@ function hintFor(info, cred, file, command = '') {
137
127
  const current = typeof info.details?.current_revision === 'string' ? info.details.current_revision : 'unknown';
138
128
  return `the file changed since that revision (current revision: ${current}); read it again, then retry with --expected-revision ${current}`;
139
129
  }
130
+ if (reason === 'workspace_disk_full' || info.code === 'workspace_disk_full')
131
+ return 'free space with shard files delete <id|key> <path>, or run a cleanup command; retry the write after space is available';
140
132
  if (reason === 'edit_not_found')
141
133
  return `--old${index} does not occur in the file (it must match exactly, including whitespace)`;
142
134
  if (reason === 'edit_ambiguous')
@@ -724,6 +716,7 @@ export async function run(argv, io, opts = {}) {
724
716
  };
725
717
  // Bare shard without a terminal (0.17.1+): the setup guide, read from local state only (never waits).
726
718
  if (parsed.command === GUIDE_COMMAND) {
719
+ const { setupGuideText, agentSetupGuide } = await import("./agent-setup.js");
727
720
  io.stdout.write(setupGuideText(agentSetupGuide(ctx)));
728
721
  return EXIT.ok;
729
722
  }
@@ -731,14 +724,24 @@ export async function run(argv, io, opts = {}) {
731
724
  const refused = setupGuard(io.env, command, parsed.values, parsed.positionals);
732
725
  if (refused)
733
726
  return report(new UsageError(refused));
734
- const handler = parsed.command.path[0] === 'acp' ? (ctx) => parsed.values.login === true ? runAcpLogin(ctx) : runAcp(ctx) : parsed.command.path[0] === 'device-helper' ? (ctx) => runDeviceHelper(ctx, { service: parsed.values.service === true }) : command === 'menubar refresh' ? runMenubarRefresh : parsed.command === PROJECT_COMMAND ? launchProjectCommand : parsed.command === ONBOARDING_COMMAND ? async (ctx, values) => await tryLinuxDesktop(ctx) ? 0 : terminalOnboarding(ctx, values, { startHelper }) : parsed.command === CENTER_COMMAND ? commandCenter : HANDLERS[parsed.command.path.join(' ')];
727
+ // Load the selected interactive surface only after help/version and credential parsing. Plain commands do
728
+ // not need the terminal UI, ACP session machinery, device bridge or onboarding just to start.
729
+ const handler = parsed.command.path[0] === 'acp'
730
+ ? async (ctx) => parsed.values.login === true ? (await import("./acp-login.js")).runAcpLogin(ctx) : (await import("./acp.js")).runAcp(ctx)
731
+ : parsed.command.path[0] === 'device-helper'
732
+ ? async (ctx) => (await import("./device-helper.js")).runDeviceHelper(ctx, { service: parsed.values.service === true })
733
+ : command === 'menubar refresh' ? runMenubarRefresh
734
+ : parsed.command === PROJECT_COMMAND ? (await import("./conversation-terminal.js")).launchProjectCommand
735
+ : parsed.command === ONBOARDING_COMMAND ? async (ctx, values) => await (await import("./desktop-linux.js")).tryLinuxDesktop(ctx) ? 0 : (await import("./terminal-onboarding.js")).terminalOnboarding(ctx, values, { startHelper })
736
+ : parsed.command === CENTER_COMMAND ? (await import("./command-center.js")).commandCenter
737
+ : await (await import("./handler-loader.js")).handlerFor(parsed.command.path.join(' '));
735
738
  if (!handler)
736
739
  return report(new UsageError(`unknown command "${parsed.command.path.join(' ')}"`));
737
740
  // Long-running helpers other programs start (an editor, launchd, the menu bar) skip the per-command housekeeping.
738
741
  if (!['acp', 'device-helper'].includes(parsed.command.path[0]) && command !== 'menubar refresh' && command !== 'cli-proxy forward' && !io.env[SETUP_VM_ENV]) {
739
742
  // An updated shard restarts this computer's CLIProxyAPI forwarder on itself (a no-op unless the version changed).
740
- await refreshForwarderAgent(io.env, ctx.store.dir).catch(() => false);
741
- await housekeeping(ctx);
743
+ await import("./forwarder-refresh.js").then(({ refreshForwarderAgent }) => refreshForwarderAgent(io.env, ctx.store.dir)).catch(() => false);
744
+ await housekeepingIfTracked(ctx);
742
745
  startHelper();
743
746
  }
744
747
  const running = handler(ctx, parsed.values, parsed.positionals).then((code) => ({ code }), (err) => ({ err }));
package/dist/menubar.d.ts CHANGED
@@ -30,12 +30,6 @@ export declare function compareAppVersions(left: string, right: string): number;
30
30
  export declare const SELF_UPDATING_APP = "0.3.2";
31
31
  /** CFBundleShortVersionString of a Shardflux app from its XML Info.plist, read without any tool (null: not readable). */
32
32
  export declare function plistVersion(app: string): string | null;
33
- /**
34
- * Whether any of these processes shows a window: the menu, the launcher, Settings, onboarding, an approval. The status
35
- * item itself (a strip at the top of the screen on the status layer, owned by the app before macOS 26) does not count.
36
- * Null when the probe could not answer: then nothing may be replaced.
37
- */
38
- export declare function windowsOpen(text: string): boolean | null;
39
33
  export declare function parseNativeStatus(text: string): NativeStatus;
40
34
  /**
41
35
  * The app's executables keep their execute permission (0.13.6+). @shardflux/menubar-darwin 0.1.1 and 0.1.2 reached npm
@@ -56,17 +50,15 @@ export declare class MenubarManager {
56
50
  get platform(): NodeJS.Platform;
57
51
  /** The version of the app this CLI's npm package carries (what `start` installs). */
58
52
  packagedVersion(): Promise<string>;
59
- /** Whether the app shows any window (see windowsOpen); null when that could not be read. */
60
- windowsOpen(pids: number[]): Promise<boolean | null>;
61
53
  status(configDir?: string): Promise<MenubarStatus>;
62
54
  stop(configDir?: string): Promise<MenubarStatus>;
63
55
  login(value: 'on' | 'off', configDir?: string): Promise<MenubarStatus>;
64
56
  start(configDir: string, apiUrl: string): Promise<MenubarStatus>;
65
57
  /**
66
58
  * Brings an installed app up to the one this CLI's npm package carries, without disturbing the person: nothing when
67
- * the versions are equal (each swap of an ad-hoc signed app costs its privacy grants), the copy on disk alone when it
68
- * is not running, and a running app only when it shows no window and no `shard update` runs. A running app that
69
- * swaps itself in (SELF_UPDATING_APP or later) is left to do so: it also knows when it is in the middle of something.
59
+ * the versions are equal, the copy on disk alone when it is not running. A running app swaps itself in when it is
60
+ * idle (every app from SELF_UPDATING_APP on: the menu bar app, and the one Shardflux app that replaces it, which runs
61
+ * `shard menubar start` when nothing is open); an older running one is restarted unless a `shard update` runs.
70
62
  */
71
63
  refresh(configDir: string, apiUrl: string): Promise<{
72
64
  result: 'current' | 'not_installed' | 'installed' | 'restarted' | 'deferred' | 'busy';
@@ -79,8 +71,21 @@ export declare class MenubarManager {
79
71
  export declare function ensureMenubar(dir: string, apiUrl: string, manager?: MenubarManager, platform?: NodeJS.Platform): Promise<string | undefined>;
80
72
  /** How often a command may start a background refresh while the app is busy (a window open). */
81
73
  export declare const REFRESH_RETRY_MS: number;
82
- /** Where this CLI's npm package keeps the app it carries, or null when the optional package is missing. */
83
- export declare function packagedAppPath(): string | null;
74
+ /**
75
+ * Where this CLI's npm package keeps the app it carries, or null when the optional package is missing. The links npm
76
+ * leaves out of the package are put back first (restorePackagedAppLinks), so the path is the complete, signed app.
77
+ */
78
+ export declare function packagedAppPath(arch?: string): string | null;
79
+ /**
80
+ * Puts back the symbolic links of the package's Shardflux.app that npm leaves out of a package (0.27.0+): an Electron
81
+ * app's frameworks are made of them (Versions/Current -> A, ...), and without them the app neither starts nor verifies.
82
+ * The package lists them in links.json; its postinstall restores them too, but pnpm and bun skip install scripts. Only
83
+ * relative links inside the app; a path that exists is left as it is. `missing`: links that could not be created.
84
+ */
85
+ export declare function restorePackagedAppLinks(packageDir: string): {
86
+ restored: string[];
87
+ missing: string[];
88
+ };
84
89
  export interface BackgroundRefreshOptions {
85
90
  dir: string;
86
91
  home: string;
package/dist/menubar.js CHANGED
@@ -1,15 +1,16 @@
1
1
  import { desktopAccount } from "./desktop-account.js";
2
2
  import { execFile, spawn } from 'node:child_process';
3
- import { chmodSync, closeSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, renameSync, rmSync, writeSync } from 'node:fs';
3
+ import { chmodSync, closeSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, renameSync, rmSync, symlinkSync, writeSync } from 'node:fs';
4
4
  import { createRequire } from 'node:module';
5
5
  import { homedir } from 'node:os';
6
- import { dirname, join } from 'node:path';
6
+ import { dirname, isAbsolute, join, resolve as resolvePath, sep } from 'node:path';
7
7
  import { promisify } from 'node:util';
8
8
  import { compareVersions } from '@shardflux/sdk';
9
9
  import { AgentStateStore } from "./agent-state.js";
10
10
  import { UsageError } from "./errors.js";
11
11
  import { json } from "./format.js";
12
12
  import { LinuxDesktopManager, linuxDesktopPackage } from "./desktop-linux.js";
13
+ import { macAppPackage } from "./mac-app.js";
13
14
  const execute = promisify(execFile);
14
15
  /**
15
16
  * Every native tool a default MenubarManager runs (plutil, codesign, xattr, open, the app's --control) goes through
@@ -56,35 +57,6 @@ export function plistVersion(app) {
56
57
  return null;
57
58
  }
58
59
  }
59
- const WINDOW_PROBE = `ObjC.import('CoreGraphics');
60
- function run(argv) {
61
- const pids = argv.map(Number);
62
- const list = ObjC.deepUnwrap(ObjC.castRefToObject($.CGWindowListCopyWindowInfo($.kCGWindowListOptionOnScreenOnly, $.kCGNullWindowID))) || [];
63
- return JSON.stringify(list.filter(w => pids.includes(w.kCGWindowOwnerPID)).map(w => ({ pid: w.kCGWindowOwnerPID, layer: w.kCGWindowLayer,
64
- x: w.kCGWindowBounds.X, y: w.kCGWindowBounds.Y, width: w.kCGWindowBounds.Width, height: w.kCGWindowBounds.Height, alpha: w.kCGWindowAlpha })));
65
- }`;
66
- /**
67
- * Whether any of these processes shows a window: the menu, the launcher, Settings, onboarding, an approval. The status
68
- * item itself (a strip at the top of the screen on the status layer, owned by the app before macOS 26) does not count.
69
- * Null when the probe could not answer: then nothing may be replaced.
70
- */
71
- export function windowsOpen(text) {
72
- let rows;
73
- try {
74
- rows = JSON.parse(text);
75
- }
76
- catch {
77
- return null;
78
- }
79
- if (!Array.isArray(rows))
80
- return null;
81
- const windows = rows;
82
- if (!windows.every(w => w && typeof w === 'object' && ['layer', 'y', 'width', 'height'].every(k => typeof w[k] === 'number')))
83
- return null;
84
- // A status item is a strip on the status layer at the top of the screen (39 pt high on a notched display).
85
- const statusItem = (w) => w.layer === 25 && w.y <= 0 && w.height <= 50;
86
- return windows.some(w => w.layer >= 0 && !statusItem(w) && (w.alpha ?? 1) > 0 && w.width > 1 && w.height > 1);
87
- }
88
60
  export function parseNativeStatus(text) {
89
61
  const value = JSON.parse(text);
90
62
  if (!value || typeof value !== 'object')
@@ -129,6 +101,8 @@ export class MenubarManager {
129
101
  const app = packagedAppPath();
130
102
  if (!app)
131
103
  throw new UsageError('The macOS app package is missing. Reinstall shard with optional dependencies enabled, then run shard menubar start.');
104
+ if (restorePackagedAppLinks(dirname(app)).missing.length)
105
+ throw new UsageError(`The Shardflux app in ${dirname(app)} is incomplete and that folder is not writable. Reinstall shard (npm i -g shardflux), then run shard.`);
132
106
  return app;
133
107
  });
134
108
  }
@@ -150,17 +124,6 @@ export class MenubarManager {
150
124
  compareBundleVersions(version, version);
151
125
  return version;
152
126
  }
153
- /** Whether the app shows any window (see windowsOpen); null when that could not be read. */
154
- async windowsOpen(pids) {
155
- if (!pids.length)
156
- return false;
157
- try {
158
- return windowsOpen(await this.#run('/usr/bin/osascript', ['-l', 'JavaScript', '-e', WINDOW_PROBE, ...pids.map(String)]));
159
- }
160
- catch {
161
- return null;
162
- }
163
- }
164
127
  /**
165
128
  * The app's --control for `configDir`: its pids are the app running for that config directory, wherever it runs from
166
129
  * (menu bar 0.8.4+, one app per config directory), and any app running from the installed bundle.
@@ -213,7 +176,9 @@ export class MenubarManager {
213
176
  const staged = join(stage, 'Shardflux.app'), backup = join(stage, 'previous.app');
214
177
  let committed = false, restored = false;
215
178
  try {
216
- cpSync(source, staged, { recursive: true, dereference: false });
179
+ // Relative symlinks stay relative (an app's frameworks are made of them): Node's default rewrites them to absolute
180
+ // paths into the npm package, which breaks the bundle's seal and its helpers (0.27.0+).
181
+ cpSync(source, staged, { recursive: true, dereference: false, verbatimSymlinks: true });
217
182
  if (current.running)
218
183
  await this.stop(configDir);
219
184
  if (current.installed)
@@ -265,9 +230,9 @@ export class MenubarManager {
265
230
  }
266
231
  /**
267
232
  * Brings an installed app up to the one this CLI's npm package carries, without disturbing the person: nothing when
268
- * the versions are equal (each swap of an ad-hoc signed app costs its privacy grants), the copy on disk alone when it
269
- * is not running, and a running app only when it shows no window and no `shard update` runs. A running app that
270
- * swaps itself in (SELF_UPDATING_APP or later) is left to do so: it also knows when it is in the middle of something.
233
+ * the versions are equal, the copy on disk alone when it is not running. A running app swaps itself in when it is
234
+ * idle (every app from SELF_UPDATING_APP on: the menu bar app, and the one Shardflux app that replaces it, which runs
235
+ * `shard menubar start` when nothing is open); an older running one is restarted unless a `shard update` runs.
271
236
  */
272
237
  async refresh(configDir, apiUrl) {
273
238
  this.#requireMac();
@@ -280,7 +245,7 @@ export class MenubarManager {
280
245
  if (current.running) {
281
246
  if (compareAppVersions(from, SELF_UPDATING_APP) >= 0)
282
247
  return { result: 'deferred', from, to };
283
- if (updateRunning(configDir) || (await this.windowsOpen(current.pids)) !== false)
248
+ if (updateRunning(configDir))
284
249
  return { result: 'busy', from, to };
285
250
  await this.start(configDir, apiUrl);
286
251
  return { result: 'restarted', from, to };
@@ -332,14 +297,59 @@ const REFRESH_MARK = 'menubar-refresh.json';
332
297
  const REFRESH_LOCK = 'menubar-refresh.lock';
333
298
  /** How often a command may start a background refresh while the app is busy (a window open). */
334
299
  export const REFRESH_RETRY_MS = 5 * 60_000;
335
- /** Where this CLI's npm package keeps the app it carries, or null when the optional package is missing. */
336
- export function packagedAppPath() {
300
+ /**
301
+ * Where this CLI's npm package keeps the app it carries, or null when the optional package is missing. The links npm
302
+ * leaves out of the package are put back first (restorePackagedAppLinks), so the path is the complete, signed app.
303
+ */
304
+ export function packagedAppPath(arch = process.arch) {
305
+ let dir;
337
306
  try {
338
- return join(dirname(createRequire(import.meta.url).resolve('@shardflux/menubar-darwin/package.json')), 'Shardflux.app');
307
+ dir = dirname(createRequire(import.meta.url).resolve(`${macAppPackage(arch)}/package.json`));
339
308
  }
340
309
  catch {
341
310
  return null;
342
311
  }
312
+ restorePackagedAppLinks(dir);
313
+ return join(dir, 'Shardflux.app');
314
+ }
315
+ /**
316
+ * Puts back the symbolic links of the package's Shardflux.app that npm leaves out of a package (0.27.0+): an Electron
317
+ * app's frameworks are made of them (Versions/Current -> A, ...), and without them the app neither starts nor verifies.
318
+ * The package lists them in links.json; its postinstall restores them too, but pnpm and bun skip install scripts. Only
319
+ * relative links inside the app; a path that exists is left as it is. `missing`: links that could not be created.
320
+ */
321
+ export function restorePackagedAppLinks(packageDir) {
322
+ const restored = [], missing = [];
323
+ let spec;
324
+ try {
325
+ spec = JSON.parse(readFileSync(join(packageDir, 'links.json'), 'utf8'));
326
+ }
327
+ catch {
328
+ return { restored, missing };
329
+ }
330
+ const app = join(packageDir, 'Shardflux.app');
331
+ const links = spec.links;
332
+ for (const entry of Array.isArray(links) ? links : []) {
333
+ const [path, target] = Array.isArray(entry) ? entry : [];
334
+ if (typeof path !== 'string' || typeof target !== 'string' || !path || !target || isAbsolute(path) || isAbsolute(target))
335
+ continue;
336
+ const at = resolvePath(app, path);
337
+ if (!at.startsWith(app + sep) || !resolvePath(dirname(at), target).startsWith(app + sep))
338
+ continue;
339
+ try {
340
+ lstatSync(at);
341
+ continue;
342
+ }
343
+ catch { /* missing: put it back */ }
344
+ try {
345
+ symlinkSync(target, at);
346
+ restored.push(path);
347
+ }
348
+ catch {
349
+ missing.push(path);
350
+ }
351
+ }
352
+ return { restored, missing };
343
353
  }
344
354
  /**
345
355
  * Every `shard` command (0.15.2+): when the npm package carries a newer app than the installed one (two Info.plist
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Your orchestrator's command table (0.27.0+, SDK 0.29.0): `shard orchestrator say|list|retry|dismiss`, and `restore`
3
+ * (0.28.0+, SDK 0.30.0). Pure data like
4
+ * args.ts, which merges it into COMMANDS; orchestrator-commands.ts has the handlers.
5
+ */
6
+ import type { CommandSpec } from './args.js';
7
+ export declare const ORCHESTRATOR_COMMANDS: CommandSpec[];
@@ -0,0 +1,64 @@
1
+ const ORG = {
2
+ org: { type: 'string', value: '<id|slug>', description: 'Organization whose orchestrator it is (default: the saved one, shard orgs use, else your only organization). Slugs need your signed-in session.' },
3
+ };
4
+ const WHO = 'Your orchestrator is yours: one agent on its own Shardflux computer, the same on every device. Uses your signed-in session, or SHARDFLUX_API_KEY when it is set (a project key you created: a new orchestrator is filed under its project).';
5
+ export const ORCHESTRATOR_COMMANDS = [
6
+ {
7
+ path: ['orchestrator', 'say'],
8
+ summary: 'Say something to your orchestrator; one of its threads hears each input at once (0.27.0+)',
9
+ positionals: [{ name: 'words', variadic: true, words: true, required: true }],
10
+ options: {
11
+ task: { type: 'string', value: '<task-id>', description: 'Point at a task (task_…).' },
12
+ report: { type: 'string', value: '<report-id>', description: 'Point at a report (rep_…).' },
13
+ 'needs-you': { type: 'string', value: '<item-id>', description: 'Point at something that waits on you (perm_…, oq_…).' },
14
+ input: { type: 'string', value: '<input-id>', description: 'Point at an earlier input (in_…).' },
15
+ conversation: { type: 'string', value: '<conversation-id>', description: 'Point at a conversation (conv_…).' },
16
+ wait: { type: 'boolean', description: 'Follow it until it is done, merged, failed or waiting, then print what it became.' },
17
+ timeout: { type: 'string', value: '<duration>', description: 'With --wait: stop following after this long (default 10m). The input goes on.' },
18
+ ...ORG,
19
+ },
20
+ description: [
21
+ 'Your exact words go to your orchestrator, with the row you point at. It answers at once: the input id and its state (queued, with its place in line; every input waits its turn in order). One of your orchestrator’s threads hears each input at once, all in parallel: a follow-up reaches the thread of the request it is about (the input reads merged), the row you point at reaches the thread of that row’s input, and anything new gets a thread of its own, which answers it, puts tasks on the board (shard board) or asks you something (shard needs-you, oq_…).',
22
+ '--wait follows it and prints its curated result: the answer’s headline and its points under their group headings (with when each happened) or its facts, a media line per file it shows (an image with its size in pixels), with their sources and follow-ups, the tasks it created, the line it waits on (a question names the shard needs-you answer command), or the input it was added to. Exit 6 when it failed (shard orchestrator retry sends it again), 5 when --timeout passed first.',
23
+ WHO,
24
+ 'With --json: {input}, as it was stored, or with --wait as it ended.',
25
+ ].join('\n\n'),
26
+ examples: ['shard orchestrator say Lets check on our language model training progress --wait', 'shard orchestrator say --task task_h7g4d7tqyv2pz3a2npyo4rpgzq use postgres instead', 'shard orchestrator say "what is blocking pricing?" --wait --json'],
27
+ },
28
+ {
29
+ path: ['orchestrator', 'list'],
30
+ summary: 'List what you said to your orchestrator and what each became, newest first (0.27.0+)',
31
+ positionals: [],
32
+ options: { all: { type: 'boolean', description: 'Also list the inputs you dismissed.' }, ...ORG },
33
+ description: [
34
+ 'One line per input: its id, its state (queued with its place in line, working, waiting with what it waits on, done, merged, failed), your words, and what it became (the answer’s headline, its tasks, the input it was added to, or why it failed). Inputs still in flight are always listed.',
35
+ WHO,
36
+ 'With --json: {inputs, cursor}, every input as the API answered it.',
37
+ ].join('\n\n'),
38
+ examples: ['shard orchestrator list', 'shard orchestrator list --json | jq -r \'.inputs[] | select(.state=="done") | .answer.headline\''],
39
+ },
40
+ {
41
+ path: ['orchestrator', 'retry'],
42
+ summary: 'Send a failed input to your orchestrator again (0.27.0+)',
43
+ positionals: [{ name: 'input-id', required: true }],
44
+ options: ORG,
45
+ description: 'It goes back in line and a new thread takes it. Prints its id and state. Only a failed input is sent again.',
46
+ },
47
+ {
48
+ path: ['orchestrator', 'dismiss'],
49
+ summary: 'Take an input out of your orchestrator’s list on every device (0.27.0+)',
50
+ positionals: [{ name: 'input-id', required: true }],
51
+ options: ORG,
52
+ description: 'What it created stays on the board. shard orchestrator list --all still shows it; shard orchestrator restore brings it back.',
53
+ },
54
+ {
55
+ path: ['orchestrator', 'restore'],
56
+ summary: 'Bring a dismissed input back into your orchestrator’s list on every device (0.28.0+)',
57
+ positionals: [{ name: 'input-id', required: true }],
58
+ options: ORG,
59
+ description: [
60
+ 'Undoes shard orchestrator dismiss: the input is listed again with what it became. Restoring an input that is not dismissed changes nothing. Questions the dismiss closed stay closed.',
61
+ 'With --json: {input}.',
62
+ ].join('\n\n'),
63
+ },
64
+ ];
@@ -0,0 +1,43 @@
1
+ import type { OrchestratorInput, OrchestratorSelection } from '@shardflux/sdk';
2
+ import type { Values } from './args.js';
3
+ import type { Handler } from './commands.js';
4
+ /** --wait's default: how long `say --wait` follows an input before it stops (the input goes on). */
5
+ export declare const SAY_WAIT_MS: number;
6
+ /** The row the person points at, from --task, --report, --needs-you, --input and --conversation; undefined for none. */
7
+ export declare function selectionOf(values: Values): OrchestratorSelection | undefined;
8
+ /** An input's state with what explains it: "queued (2 in line)", "waiting (Waiting for your computer: list ~/code)". */
9
+ export declare function stateLabel(i: OrchestratorInput): string;
10
+ /** What an input became, in a few words: the answer's headline, its tasks, the input it went to, or why it failed. */
11
+ export declare function becameText(i: OrchestratorInput): string;
12
+ /** One input as `shard orchestrator list` prints it: id, state, your words, what it became. */
13
+ export declare function inputLine(i: OrchestratorInput): string;
14
+ /**
15
+ * When an answer's point happened, short and in this computer's local time: "14:02" today, "Oct 16 14:02" this year,
16
+ * else "2025-10-16 14:02". A time without an offset is the person's local time and prints as written.
17
+ */
18
+ export declare function shortTime(at: string, now?: Date): string;
19
+ /** An answer's body: its points under their group headings when the agent wrote points (0.27.0+), else its facts. */
20
+ export declare function answerLines(answer: NonNullable<OrchestratorInput['answer']>, now?: Date): string[];
21
+ /** One file an answer shows (0.27.0+): ` media: loss-curve.png (image, 1200×800, 84.0 KB)`, the size in pixels once known. */
22
+ export declare function mediaLine(m: NonNullable<NonNullable<OrchestratorInput['answer']>['media']>[number]): string;
23
+ /**
24
+ * What `say --wait` prints once an input is done, merged, failed or waiting: its curated result.
25
+ *
26
+ * Training is 73% done, on pace to finish at 16:40
27
+ * Moved
28
+ * - Loss is down to 1.84: from 2.31 at step 5,000 (14:02)
29
+ * - Checkpoint saved: at step 7,000
30
+ * Still open
31
+ * - Eval has not run: it starts after the last checkpoint
32
+ * media: loss-curve.png (image, 1200×800, 84.0 KB)
33
+ * Sources: train.log (~/code/lm-train/train.log), wandb.ai (https://wandb.ai/acme/lm)
34
+ * Follow-ups: Check again in an hour
35
+ * Tasks: task_… (shard task show <id>)
36
+ *
37
+ * An answer written as facts prints them as ` - <fact>` lines instead of the points. Each file the answer shows prints
38
+ * one media line after them (shard drive get <path> --sha256 <sha256> reads it; --json has both).
39
+ */
40
+ export declare function resultText(i: OrchestratorInput, now?: Date): string;
41
+ /** Where `say --wait` stops following: nothing more happens to the input, or it waits on you or your computer. */
42
+ export declare const stopsWaiting: (i: OrchestratorInput) => boolean;
43
+ export declare const ORCHESTRATOR_HANDLERS: Record<string, Handler>;