@shardflux/cli 0.28.0 → 0.28.2

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 (46) hide show
  1. package/CHANGELOG.md +54 -17
  2. package/README.md +91 -12
  3. package/dist/account-commands.d.ts +2 -4
  4. package/dist/account-commands.js +2 -6
  5. package/dist/agent-git-commands.d.ts +1 -2
  6. package/dist/agent-git-commands.js +2 -21
  7. package/dist/agent-setup.d.ts +4 -2
  8. package/dist/agent-setup.js +6 -4
  9. package/dist/agent-shell.d.ts +2 -0
  10. package/dist/agent-shell.js +5 -0
  11. package/dist/args.d.ts +1 -1
  12. package/dist/args.js +5 -1
  13. package/dist/board-args.js +7 -0
  14. package/dist/board-commands.d.ts +1 -1
  15. package/dist/board-commands.js +46 -6
  16. package/dist/cli-proxy.d.ts +1 -13
  17. package/dist/cli-proxy.js +2 -27
  18. package/dist/codex-cloud.js +3 -1
  19. package/dist/commands.d.ts +12 -0
  20. package/dist/commands.js +121 -4
  21. package/dist/conversations.d.ts +2 -0
  22. package/dist/conversations.js +1 -1
  23. package/dist/device-bridge.d.ts +4 -1
  24. package/dist/device-bridge.js +9 -2
  25. package/dist/device-folders.d.ts +2 -1
  26. package/dist/device-folders.js +3 -3
  27. package/dist/format.js +3 -0
  28. package/dist/forwarder-refresh.d.ts +15 -0
  29. package/dist/forwarder-refresh.js +32 -0
  30. package/dist/handler-loader.d.ts +3 -0
  31. package/dist/handler-loader.js +43 -0
  32. package/dist/land.d.ts +1 -1
  33. package/dist/land.js +6 -5
  34. package/dist/linux-helper.js +1 -2
  35. package/dist/machine-setup-guard.d.ts +9 -0
  36. package/dist/machine-setup-guard.js +22 -0
  37. package/dist/machine-setup.d.ts +1 -8
  38. package/dist/machine-setup.js +2 -22
  39. package/dist/main.js +17 -16
  40. package/dist/orchestrator-commands.js +1 -3
  41. package/dist/startup-housekeeping.d.ts +5 -0
  42. package/dist/startup-housekeeping.js +30 -0
  43. package/dist/t3-provider.js +4 -2
  44. package/dist/worktree-twin-engine.d.ts +1 -1
  45. package/dist/worktree-twin-engine.js +9 -6
  46. package/package.json +12 -7
@@ -0,0 +1,43 @@
1
+ // Exact command ownership, in the same override order as HANDLERS. The parity test catches additions
2
+ // or moves without loading every command module during an ordinary command.
3
+ const groups = [
4
+ [["projects sync list", "projects sync add", "projects sync pause", "projects sync remove", "projects sync run"], async () => (await import("./project-preparation.js")).PROJECT_PREPARATION_HANDLERS],
5
+ [["projects link"], async () => (await import("./device-folders.js")).PROJECT_LINK_HANDLERS],
6
+ [["move"], async () => (await import("./move.js")).MOVE_HANDLERS],
7
+ [["device approvals", "device requests", "device approve", "device deny"], async () => (await import("./device-approvals.js")).DEVICE_HANDLERS],
8
+ [["agents codex status", "agents codex enable", "agents codex disable"], async () => (await import("./codex-cloud.js")).CODEX_CLOUD_HANDLERS],
9
+ [["cli-proxy", "cli-proxy import", "cli-proxy export", "cli-proxy use", "cli-proxy forward"], async () => (await import("./cli-proxy.js")).CLI_PROXY_HANDLERS],
10
+ [["agents computers"], async () => (await import("./thread-on-computer.js")).THREAD_HANDLERS],
11
+ [["conversations resume", "conversations reader", "conversations list", "conversations feed", "conversations get", "conversations permissions", "conversations answer", "conversations watch", "conversations send", "conversations start", "conversations wait", "conversations land", "files pull", "conversations move", "conversations archive", "conversations unarchive", "conversations seen", "conversations delete"], async () => (await import("./conversation-commands.js")).CONVERSATION_HANDLERS],
12
+ [["orchestrator say", "orchestrator list", "orchestrator retry", "orchestrator dismiss", "orchestrator restore"], async () => (await import("./orchestrator-commands.js")).ORCHESTRATOR_HANDLERS],
13
+ [["board", "task add", "task show", "task edit", "task rm", "task steer", "task land", "task clear-done", "needs-you", "needs-you answer", "approvals", "approvals set", "approvals allow", "approvals remove"], async () => (await import("./board-commands.js")).BOARD_HANDLERS],
14
+ [["conversations transcript", "conversations events"], async () => (await import("./conversation-transcript.js")).TRANSCRIPT_HANDLERS],
15
+ [["conversations try"], async () => (await import("./conversation-local.js")).CONVERSATION_HANDLERS],
16
+ [["conversations landing"], async () => (await import("./worktree-twin-engine.js")).TWIN_HANDLERS],
17
+ [["secrets log", "credentials list", "credentials enable", "credentials approve", "credentials deny", "credentials forget"], async () => (await import("./credential-prompts.js")).CREDENTIAL_HANDLERS],
18
+ [["agents credentials login", "agents credentials sync", "agents credentials status", "agents credentials forget"], async () => (await import("./agent-credentials.js")).AGENT_CREDENTIAL_HANDLERS],
19
+ [["agents accounts", "agents accounts list", "agents accounts usage", "agents accounts add", "agents accounts remove", "agents accounts use", "accounts", "accounts list", "accounts usage", "accounts add", "accounts remove", "accounts use"], async () => (await import("./account-pool.js")).ACCOUNT_POOL_HANDLERS],
20
+ [["agents models"], async () => (await import("./account-pool.js")).AGENT_MODELS_HANDLERS],
21
+ [["agents list", "login", "whoami", "version", "feedback", "usage", "workspaces labels", "workspaces hint", "workspaces idle", "workspaces exec-start", "workspaces tunnel", "workspaces exec-read", "workspaces exec-cancel", "workspaces exec-input", "workspaces retention", "projects retention", "workspaces open", "workspaces list", "workspaces get", "workspaces inputs", "workspaces exec", "files read-many", "files read", "files write", "files delete", "files ls", "files stat", "files search", "files patch", "workspaces suspend", "workspaces resume", "workspaces delete", "workspaces close", "workspaces upgrade", "workspaces reset", "workspaces save-as-template", "workspaces changes", "templates list", "templates get", "templates init", "templates build", "templates export", "templates test", "templates builds list", "templates builds get", "templates builds log", "templates builds cancel", "templates languages", "templates packages search", "templates files", "templates diff", "templates draft open", "templates draft status", "templates draft state", "templates draft test", "templates draft publish", "templates draft discard", "workspaces fork", "workspaces resize", "workspaces sessions", "workspaces port expose", "workspaces port ls", "workspaces port close", "workspaces port token", "workspaces port link", "workspaces port callback", "workspaces computer status", "workspaces computer screenshot", "workspaces computer act", "workspaces computer click", "workspaces computer type", "workspaces computer paste-text", "workspaces computer key", "workspaces computer scroll", "workspaces computer move", "workspaces computer drag", "workspaces computer mouse-down", "workspaces computer mouse-up", "workspaces computer hold-key", "workspaces computer wait", "workspaces computer zoom", "workspaces computer cursor", "workspaces computer start", "workspaces computer stop", "workspaces computer stream", "workspaces computer enable", "workspaces computer disable", "workspaces computer inherit", "templates computer-use", "workspaces secrets", "secrets import", "secrets create", "secrets list", "secrets get", "secrets update", "secrets rotate", "secrets versions", "secrets delete", "secrets access-log", "operations list", "operations get", "operations progress", "operations cancel", "operations wait", "executions get"], async () => (await import("./commands.js")).KEY_HANDLERS],
22
+ [["auth register", "auth verify-email", "auth resend-verification", "auth login", "auth mfa", "auth status", "auth logout", "auth sessions", "auth step-up", "auth password change", "auth password reset", "auth password reset-confirm", "auth email change", "auth email confirm", "auth totp enroll", "auth totp confirm", "auth totp disable", "auth totp recovery-codes", "setup", "signup", "verify", "env", "orgs list", "orgs create", "orgs get", "orgs use", "orgs export", "orgs workspaces", "orgs deletion", "orgs delete", "projects list", "projects create", "projects get", "projects use", "api-keys list", "api-keys create", "api-keys revoke", "members list", "members set-role", "members remove", "invitations list", "invitations create", "invitations revoke", "invitations accept", "billing plans", "billing status", "billing upgrade", "billing portal", "billing invoices", "billing alerts", "billing overage", "audit list", "audit export", "account status", "account export", "account delete", "account cancel-deletion", "egress get", "egress set", "egress history", "egress clear", "volumes list", "status", "volumes open", "volumes create", "volumes get", "volumes delete", "volumes attachments", "volumes attach", "volumes detach", "templates publish", "templates archive"], async () => (await import("./account-commands.js")).ACCOUNT_HANDLERS],
23
+ [["agents open"], async () => (await import("./agent-commands.js")).AGENT_HANDLERS],
24
+ [["agents new", "agents fork", "agents park", "agents logs"], async () => (await import("./command-center-actions.js")).CENTER_HANDLERS],
25
+ [["agents targets", "agents terminal-run", "agents open claude-desktop", "agents open codex-app", "agents open t3", "agents open web", "ssh-proxy"], async () => (await import("./agent-target-commands.js")).TARGET_HANDLERS],
26
+ [["agents merge", "agents keep", "agents discard", "agents worktree", "agents restore"], async () => (await import("./agent-git-commands.js")).AGENT_GIT_HANDLERS],
27
+ [["menubar start", "menubar stop", "menubar status", "menubar account"], async () => (await import("./menubar.js")).MENUBAR_HANDLERS],
28
+ [["machine", "machine open", "machine save", "machine rollback", "template", "template save", "template rollback", "template delete"], async () => (await import("./filesystem-commands.js")).FILESYSTEM_HANDLERS],
29
+ [["machine setup"], async () => (await import("./machine-setup.js")).MACHINE_SETUP_HANDLERS],
30
+ [["settings"], async () => (await import("./client-settings.js")).SETTINGS_HANDLERS],
31
+ [["update"], async () => (await import("./update.js")).UPDATE_HANDLERS],
32
+ [["agents config-sync"], async () => (await import("./agent-config-sync.js")).AGENT_CONFIG_HANDLERS],
33
+ [["route", "ask"], async () => (await import("./router-commands.js")).ROUTER_HANDLERS],
34
+ [["transcribe"], async () => (await import("./transcribe-commands.js")).TRANSCRIBE_HANDLERS],
35
+ [["drive ls", "drive get", "drive put", "drive rm", "models", "models primary"], async () => (await import("./drive-commands.js")).DRIVE_HANDLERS],
36
+ [["brain ls", "brain get"], async () => (await import("./brain-commands.js")).BRAIN_HANDLERS],
37
+ ];
38
+ const owners = new Map(groups.flatMap(([paths, load]) => paths.map(path => [path, load])));
39
+ export const HANDLER_PATHS = [...owners.keys()];
40
+ export async function handlerFor(path) {
41
+ const load = owners.get(path);
42
+ return load ? (await load())[path] : undefined;
43
+ }
package/dist/land.d.ts CHANGED
@@ -97,7 +97,7 @@ export interface LandDeps {
97
97
  tasks: Pick<ReturnType<typeof conversationClient>['tasks'], 'get' | 'recordLanding' | 'steer'>;
98
98
  conversations: Pick<ReturnType<typeof conversationClient>['conversations'], 'get' | 'land'>;
99
99
  };
100
- hubCell(projectId: string): Promise<{
100
+ hubCell(projectId: string, workspaceIds?: readonly string[]): Promise<{
101
101
  cell: TwinCell;
102
102
  close(): void;
103
103
  }>;
package/dist/land.js CHANGED
@@ -127,11 +127,11 @@ function message(o) {
127
127
  return hub;
128
128
  }
129
129
  export function landDeps(ctx) {
130
- return { client: conversationClient(ctx), hubCell: p => projectHubCell(ctx, p, ctx.signal, 'shard-land'), folder: (p, c, r) => linkedFolder(ctx, p, c, r),
130
+ return { client: conversationClient(ctx), hubCell: (p, ids) => projectHubCell(ctx, p, ctx.signal, 'shard-land', ids), folder: (p, c, r) => linkedFolder(ctx, p, c, r),
131
131
  device: deviceId(ctx.store.dir), stateDir: ctx.store.dir, signal: ctx.signal };
132
132
  }
133
133
  async function land(d, l, onConflict) {
134
- const vm = await d.hubCell(l.projectId);
134
+ const vm = await d.hubCell(l.projectId, l.workspaceIds);
135
135
  try {
136
136
  const input = { result: l.result, sources: l.sources.map(s => s.branch), title: l.title, ...(d.hub ? { hub: d.hub } : {}) };
137
137
  let hub = await landOnHub(vm.cell, input, d.signal);
@@ -228,7 +228,7 @@ export async function landTask(d, taskId, opts = {}) {
228
228
  const repository = workers.find(w => w.repo_remote)?.repo_remote ?? (task.master_id ? (await client.conversations.get(task.master_id).catch(() => null))?.repo_remote : undefined) ?? null;
229
229
  const folder = opts.path ?? d.folder(task.project_id, undefined, repository ?? undefined);
230
230
  const done = await land(d, { projectId: task.project_id, title, name: taskId, result: resultBranch(taskId), sources: workers.map(w => ({ branch: w.branch, conversation_id: w.id })),
231
- base: workers.find(w => w.base_commit)?.base_commit ?? null, folder, repository }, async (hub) => {
231
+ base: workers.find(w => w.base_commit)?.base_commit ?? null, folder, repository, workspaceIds: workers.map(w => w.workspace_id).filter(Boolean) }, async (hub) => {
232
232
  const what = hub.stage === 'integrate' ? `${hub.source} into ${resultBranch(taskId)}` : `${resultBranch(taskId)} into ${hub.into}`;
233
233
  await client.tasks.steer(taskId, `Land could not merge ${what}: ${list(hub.conflicts ?? [])} changed on both sides. Merge ${hub.into} from the project hub and every worker branch into ${resultBranch(taskId)}, resolve the conflicts so both changes work (ask the person only if the choice is theirs), run the tests, push ${resultBranch(taskId)} to the hub and seal done again.`, signal);
234
234
  });
@@ -267,9 +267,10 @@ export async function landConversation(d, id, opts = {}) {
267
267
  const c = await client.conversations.get(id);
268
268
  if (c.changes?.uncommitted)
269
269
  throw new CodedError('uncommitted_changes', 'The conversation has uncommitted changes.', 'ask its agent to commit them, then land');
270
- const folder = opts.path ?? d.folder(c.project_id, c.id);
270
+ const repository = c.repo_remote ?? null;
271
+ const folder = opts.path ?? d.folder(c.project_id, c.id, repository ?? undefined);
271
272
  const done = await land(d, { projectId: c.project_id, title: c.title || c.branch, name: c.name ?? c.branch.replace(/^shard\//, ''), result: c.branch, sources: [],
272
- base: c.base_commit ?? null, folder }, () => Promise.resolve());
273
+ base: c.base_commit ?? null, folder, repository, workspaceIds: [c.workspace_id] }, () => Promise.resolve());
273
274
  let recorded = false, record_error;
274
275
  try {
275
276
  await client.conversations.land(id, 'merge', { into: done.hub.into, ...(d.signal ? { signal: d.signal } : {}) });
@@ -4,7 +4,6 @@ import { readFileSync, realpathSync } from 'node:fs';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { CLI_VERSION } from "./args.js";
6
6
  import { writePrivateFile } from "./credentials.js";
7
- import { runDeviceHelper } from "./device-helper.js";
8
7
  export const HELPER_UNIT = 'shardflux-helper.service';
9
8
  export const helperUnitPath = (home) => join(home, '.config/systemd/user', HELPER_UNIT);
10
9
  const quote = (s) => '"' + s.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%').replaceAll('\n', '\\n').replaceAll('\r', '\\r') + '"';
@@ -83,6 +82,6 @@ export function startLinuxHelper(ctx) {
83
82
  writePrivateFile(ctx.store.dir, notice, '1\n');
84
83
  }
85
84
  const abort = new AbortController();
86
- const work = runDeviceHelper({ ...ctx, signal: AbortSignal.any([ctx.signal, abort.signal]) }, { service: true }).catch(() => undefined);
85
+ const work = import("./device-helper.js").then(({ runDeviceHelper }) => runDeviceHelper({ ...ctx, signal: AbortSignal.any([ctx.signal, abort.signal]) }, { service: true })).catch(() => undefined);
87
86
  return async () => { abort.abort(); await work; };
88
87
  }
@@ -0,0 +1,9 @@
1
+ import type { Values } from './args.js';
2
+ type Env = Record<string, string | undefined>;
3
+ export declare const SETUP_VM_ENV = "SHARDFLUX_MACHINE_SETUP_VM";
4
+ /**
5
+ * While `SHARDFLUX_MACHINE_SETUP_VM` is set (the setup agent's own commands), shard only runs commands on that VM: exec,
6
+ * file writes from standard input, reads and listings. Returns the refusal, or undefined when the command may run.
7
+ */
8
+ export declare function setupGuard(env: Env, command: string, values: Values, positionals: string[]): string | undefined;
9
+ export {};
@@ -0,0 +1,22 @@
1
+ export const SETUP_VM_ENV = 'SHARDFLUX_MACHINE_SETUP_VM';
2
+ /**
3
+ * While `SHARDFLUX_MACHINE_SETUP_VM` is set (the setup agent's own commands), shard only runs commands on that VM: exec,
4
+ * file writes from standard input, reads and listings. Returns the refusal, or undefined when the command may run.
5
+ */
6
+ export function setupGuard(env, command, values, positionals) {
7
+ const vm = env[SETUP_VM_ENV]?.trim();
8
+ if (!vm)
9
+ return undefined;
10
+ const allowed = ['workspaces exec', 'files write', 'files read', 'files ls', 'files stat', 'version'];
11
+ if (!allowed.includes(command))
12
+ return `machine setup runs only shard ws exec, files write and files read on ${vm}`;
13
+ if (command === 'version')
14
+ return undefined;
15
+ if (positionals[0] !== vm)
16
+ return `machine setup runs commands only on ${vm}`;
17
+ if (command === 'files write' && values.from !== undefined && values.from !== '-')
18
+ return 'machine setup writes files only from standard input (--from -)';
19
+ if (command === 'files read' && values.out !== undefined)
20
+ return 'machine setup reads files only to standard output';
21
+ return undefined;
22
+ }
@@ -4,8 +4,7 @@ import { AgentStateStore } from './agent-state.js';
4
4
  import { type Values } from './args.js';
5
5
  export type Runner = 'claude' | 'codex' | 'cloud';
6
6
  type Env = Record<string, string | undefined>;
7
- /** Set for the local agent's processes: the `shard` it runs only reaches this machine VM (see `setupGuard`). */
8
- export declare const SETUP_VM_ENV = "SHARDFLUX_MACHINE_SETUP_VM";
7
+ export { SETUP_VM_ENV, setupGuard } from './machine-setup-guard.js';
9
8
  export declare const DEFAULT_AGENT_TIMEOUT_MS: number;
10
9
  export interface ChildResult {
11
10
  code: number | null;
@@ -71,11 +70,6 @@ export declare const SECRET_PATHS: readonly ["~/.ssh/**", "~/.aws/**", "~/.azure
71
70
  export declare function claudeArgs(key: string, help: string): string[];
72
71
  /** `codex exec` argv: writes only in `workDir` (and temp folders), network on for `shard`, the task on standard input. */
73
72
  export declare function codexArgs(workDir: string, help: string): string[];
74
- /**
75
- * While `SHARDFLUX_MACHINE_SETUP_VM` is set (the setup agent's own commands), shard only runs commands on that VM: exec,
76
- * file writes from standard input, reads and listings. Returns the refusal, or undefined when the command may run.
77
- */
78
- export declare function setupGuard(env: Env, command: string, values: Values, positionals: string[]): string | undefined;
79
73
  export interface Inventory {
80
74
  /** The home whose files were inventoried; a local agent's login shell can use a different home. */
81
75
  source_home?: string;
@@ -187,4 +181,3 @@ export declare function machineSetup(ctx: Ctx, values: Values, deps?: SetupDeps)
187
181
  */
188
182
  export declare function startMachineSetupInBackground(ctx: Ctx): boolean;
189
183
  export declare const MACHINE_SETUP_HANDLERS: Record<string, Handler>;
190
- export {};
@@ -31,7 +31,8 @@ import { progress as stderrProgress } from "./progress.js";
31
31
  import { writeProgress } from "./seed-phases.js";
32
32
  import { parseDuration } from "./args.js";
33
33
  /** Set for the local agent's processes: the `shard` it runs only reaches this machine VM (see `setupGuard`). */
34
- export const SETUP_VM_ENV = 'SHARDFLUX_MACHINE_SETUP_VM';
34
+ import { SETUP_VM_ENV } from "./machine-setup-guard.js";
35
+ export { SETUP_VM_ENV, setupGuard } from "./machine-setup-guard.js";
35
36
  export const DEFAULT_AGENT_TIMEOUT_MS = 10 * 60_000;
36
37
  const SAVE_TIMEOUT_MS = 10 * 60_000;
37
38
  /**
@@ -228,27 +229,6 @@ export function codexArgs(workDir, help) {
228
229
  '-',
229
230
  ];
230
231
  }
231
- /**
232
- * While `SHARDFLUX_MACHINE_SETUP_VM` is set (the setup agent's own commands), shard only runs commands on that VM: exec,
233
- * file writes from standard input, reads and listings. Returns the refusal, or undefined when the command may run.
234
- */
235
- export function setupGuard(env, command, values, positionals) {
236
- const vm = env[SETUP_VM_ENV]?.trim();
237
- if (!vm)
238
- return undefined;
239
- const allowed = ['workspaces exec', 'files write', 'files read', 'files ls', 'files stat', 'version'];
240
- if (!allowed.includes(command))
241
- return `machine setup runs only shard ws exec, files write and files read on ${vm}`;
242
- if (command === 'version')
243
- return undefined;
244
- if (positionals[0] !== vm)
245
- return `machine setup runs commands only on ${vm}`;
246
- if (command === 'files write' && values.from !== undefined && values.from !== '-')
247
- return 'machine setup writes files only from standard input (--from -)';
248
- if (command === 'files read' && values.out !== undefined)
249
- return 'machine setup reads files only to standard output';
250
- return undefined;
251
- }
252
232
  function setupCapacity(ws) {
253
233
  const disk = ws.grants?.disk_gib;
254
234
  if (typeof disk !== 'number' || !Number.isFinite(disk) || disk <= 0)
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";
@@ -726,6 +716,7 @@ export async function run(argv, io, opts = {}) {
726
716
  };
727
717
  // Bare shard without a terminal (0.17.1+): the setup guide, read from local state only (never waits).
728
718
  if (parsed.command === GUIDE_COMMAND) {
719
+ const { setupGuideText, agentSetupGuide } = await import("./agent-setup.js");
729
720
  io.stdout.write(setupGuideText(agentSetupGuide(ctx)));
730
721
  return EXIT.ok;
731
722
  }
@@ -733,14 +724,24 @@ export async function run(argv, io, opts = {}) {
733
724
  const refused = setupGuard(io.env, command, parsed.values, parsed.positionals);
734
725
  if (refused)
735
726
  return report(new UsageError(refused));
736
- 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(' '));
737
738
  if (!handler)
738
739
  return report(new UsageError(`unknown command "${parsed.command.path.join(' ')}"`));
739
740
  // Long-running helpers other programs start (an editor, launchd, the menu bar) skip the per-command housekeeping.
740
741
  if (!['acp', 'device-helper'].includes(parsed.command.path[0]) && command !== 'menubar refresh' && command !== 'cli-proxy forward' && !io.env[SETUP_VM_ENV]) {
741
742
  // An updated shard restarts this computer's CLIProxyAPI forwarder on itself (a no-op unless the version changed).
742
- await refreshForwarderAgent(io.env, ctx.store.dir).catch(() => false);
743
- await housekeeping(ctx);
743
+ await import("./forwarder-refresh.js").then(({ refreshForwarderAgent }) => refreshForwarderAgent(io.env, ctx.store.dir)).catch(() => false);
744
+ await housekeepingIfTracked(ctx);
744
745
  startHelper();
745
746
  }
746
747
  const running = handler(ctx, parsed.values, parsed.positionals).then((code) => ({ code }), (err) => ({ err }));
@@ -6,7 +6,7 @@
6
6
  * a session) reaches the orchestrator of the member who created that key (person-scope.ts). Text output is one line
7
7
  * per input, tab-separated, id first; --json prints the API's objects.
8
8
  */
9
- import { inputSettled, ORCHESTRATOR_TEXT_MAX } from '@shardflux/sdk';
9
+ import { inputSettled } from '@shardflux/sdk';
10
10
  import { parseDuration } from "./args.js";
11
11
  import { out, str } from "./commands.js";
12
12
  import { EXIT, UsageError } from "./errors.js";
@@ -202,8 +202,6 @@ export const ORCHESTRATOR_HANDLERS = {
202
202
  const text = words.join(' ');
203
203
  if (!text.trim())
204
204
  throw new UsageError('missing <words...> for "shard orchestrator say"');
205
- if (text.length > ORCHESTRATOR_TEXT_MAX)
206
- throw new UsageError(`your words are ${text.length} characters; one input takes up to ${ORCHESTRATOR_TEXT_MAX}`);
207
205
  const selection = selectionOf(values);
208
206
  const wait = values.wait === true;
209
207
  const timeoutMs = str(values.timeout) === undefined ? SAY_WAIT_MS : parseDuration(str(values.timeout), 'timeout');
@@ -0,0 +1,5 @@
1
+ import type { Ctx } from './commands.js';
2
+ /** Help/version starts also clean saved work, without requiring credentials when nothing is due. */
3
+ export declare function startupHousekeeping(ctx: Pick<Ctx, 'io' | 'store' | 'signal'>): Promise<void>;
4
+ /** Keep the command's already selected auth behavior, loading Git machinery only for tracked repositories. */
5
+ export declare function housekeepingIfTracked(ctx: Pick<Ctx, 'io' | 'apiUrl' | 'signal' | 'store' | 'cloud'>): Promise<void>;
@@ -0,0 +1,30 @@
1
+ import { Shardflux } from '@shardflux/sdk';
2
+ import { AgentStateStore } from "./agent-state.js";
3
+ import { resolveApiUrl } from "./args.js";
4
+ import { UsageError } from "./errors.js";
5
+ /** Help/version starts also clean saved work, without requiring credentials when nothing is due. */
6
+ export async function startupHousekeeping(ctx) {
7
+ try {
8
+ const state = new AgentStateStore(ctx.store.dir).read();
9
+ if (!state.git_repositories || !Object.keys(state.git_repositories).length)
10
+ return;
11
+ const apiUrl = resolveApiUrl(undefined, ctx.io.env.SHARDFLUX_API_URL ?? ctx.io.env.SHARDFLUX_BASE_URL);
12
+ let cloud;
13
+ await (await import("./agent-git-commands.js")).housekeeping({ ...ctx, apiUrl, cloud: () => {
14
+ const key = ctx.io.env.SHARDFLUX_API_KEY?.trim() || ctx.store.profile(apiUrl).api_key?.token;
15
+ if (!key)
16
+ throw new UsageError('saved agents need a project key for housekeeping; run shard setup');
17
+ return cloud ??= new Shardflux({ apiKey: key, baseUrl: apiUrl, versionCheck: false });
18
+ } });
19
+ }
20
+ catch (error) {
21
+ ctx.io.stderr.write(`shard housekeeping: ${error instanceof Error ? error.message : String(error)}\n`);
22
+ }
23
+ }
24
+ /** Keep the command's already selected auth behavior, loading Git machinery only for tracked repositories. */
25
+ export async function housekeepingIfTracked(ctx) {
26
+ const repos = new AgentStateStore(ctx.store.dir).read().git_repositories;
27
+ if (!repos || !Object.keys(repos).length)
28
+ return;
29
+ await (await import("./agent-git-commands.js")).housekeeping(ctx);
30
+ }
@@ -3,13 +3,15 @@ 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
- import { installedTargets } from "./agent-targets.js";
6
+ import { applicationDirs, t3Apps } from "./agent-targets.js";
7
7
  import { t3ArchiveSupportsAcp } from "./t3-asar.js";
8
8
  const ID = 'shardflux';
9
9
  const provider = { driver: 'acpRegistry', displayName: 'Shardflux', enabled: true, config: { source: 'local', commandPath: 'shard', commandArgs: ['acp'] } };
10
10
  /** Every shard invocation registers the one installed T3 provider without adding terminal noise (macOS and Linux). */
11
11
  export async function registerInstalledT3(env, dir, signal, platform = process.platform) {
12
- if (!installedTargets(env, platform).targets.includes('t3'))
12
+ // Registration only needs T3 presence; probing other apps or choosing a
13
+ // preferred T3 version would inspect unrelated bundles on every command.
14
+ if (!t3Apps(applicationDirs(env, platform), platform, env).length)
13
15
  return;
14
16
  try {
15
17
  await configureT3Provider(env, dir, false, signal);
@@ -123,7 +123,7 @@ export declare function runTwinLoop(deps: LoopDeps, signal: AbortSignal): Promis
123
123
  * A VM of the project that reads the project hub: an awake one reads it without waking anything; else the project's
124
124
  * small space reader (woken). The twin lands from it when a conversation's VM is gone; `shard task land` merges there.
125
125
  */
126
- export declare function projectHubCell(ctx: Ctx, projectId: string, signal?: AbortSignal, agentLabel?: string): Promise<{
126
+ export declare function projectHubCell(ctx: Ctx, projectId: string, signal?: AbortSignal, agentLabel?: string, workspaceIds?: readonly string[]): Promise<{
127
127
  cell: TwinCell;
128
128
  close(): void;
129
129
  }>;
@@ -508,14 +508,17 @@ const openCell = (ws, agentLabel) => {
508
508
  * A VM of the project that reads the project hub: an awake one reads it without waking anything; else the project's
509
509
  * small space reader (woken). The twin lands from it when a conversation's VM is gone; `shard task land` merges there.
510
510
  */
511
- export async function projectHubCell(ctx, projectId, signal, agentLabel = 'shard-twin') {
511
+ export async function projectHubCell(ctx, projectId, signal, agentLabel = 'shard-twin', workspaceIds) {
512
512
  const client = conversationClient(ctx);
513
- const all = (await listConversations(ctx, projectId)).filter(c => !ended(c))
514
- .sort((a, b) => (b.last_activity_at ?? '').localeCompare(a.last_activity_at ?? ''));
515
- for (const c of all) {
513
+ // A manual landing uses its own result's computer. Borrowing an unrelated thread races that person's deletion:
514
+ // staging 2026-10-17 cut off a real landing when a newer home thread's VM was deleted. If the result's computers
515
+ // are gone or asleep, the persistent space reader owns the operation instead; do not wake a finished agent.
516
+ const ids = workspaceIds ?? (await listConversations(ctx, projectId)).filter(c => !ended(c))
517
+ .sort((a, b) => (b.last_activity_at ?? '').localeCompare(a.last_activity_at ?? '')).map(c => c.workspace_id);
518
+ for (const id of new Set(ids)) {
516
519
  try {
517
- const ws = await client.workspaces.get(c.workspace_id);
518
- if (!ws.data.deleted_at && ws.ready)
520
+ const ws = await client.workspaces.get(id);
521
+ if (ws.data.project_id === projectId && !ws.data.deleted_at && ws.ready)
519
522
  return openCell(ws, agentLabel);
520
523
  }
521
524
  catch { /* another may be awake */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shardflux/cli",
3
- "version": "0.28.0",
3
+ "version": "0.28.2",
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",
@@ -31,7 +31,12 @@
31
31
  "import": "./dist/index.js",
32
32
  "default": "./dist/index.js"
33
33
  },
34
- "./bin": "./dist/bin.js"
34
+ "./bin": "./dist/bin.js",
35
+ "./main": {
36
+ "types": "./dist/main.d.ts",
37
+ "import": "./dist/main.js",
38
+ "default": "./dist/main.js"
39
+ }
35
40
  },
36
41
  "files": [
37
42
  "dist",
@@ -48,7 +53,7 @@
48
53
  "snappyjs": "0.7.0",
49
54
  "ws": "8.22.0",
50
55
  "yaml": "2.9.1",
51
- "@shardflux/sdk": "^0.30.0"
56
+ "@shardflux/sdk": "^0.30.2"
52
57
  },
53
58
  "devDependencies": {
54
59
  "@eslint/js": "10.0.1",
@@ -60,10 +65,10 @@
60
65
  "typescript-eslint": "8.70.1"
61
66
  },
62
67
  "optionalDependencies": {
63
- "@shardflux/desktop-darwin-arm64": "0.9.1",
64
- "@shardflux/desktop-darwin-x64": "0.9.1",
65
- "@shardflux/desktop-linux-arm64": "0.9.1",
66
- "@shardflux/desktop-linux-x64": "0.9.1"
68
+ "@shardflux/desktop-darwin-arm64": "0.9.3",
69
+ "@shardflux/desktop-darwin-x64": "0.9.3",
70
+ "@shardflux/desktop-linux-arm64": "0.9.3",
71
+ "@shardflux/desktop-linux-x64": "0.9.3"
67
72
  },
68
73
  "scripts": {
69
74
  "build": "node scripts/build.mjs",