@shardflux/cli 0.20.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +312 -1
  2. package/README.md +242 -75
  3. package/dist/account-args.js +7 -2
  4. package/dist/account-commands.js +56 -106
  5. package/dist/account-pool.d.ts +19 -15
  6. package/dist/account-pool.js +47 -96
  7. package/dist/account-scope.d.ts +65 -0
  8. package/dist/account-scope.js +246 -0
  9. package/dist/acp-journal.js +58 -6
  10. package/dist/acp-login.d.ts +13 -5
  11. package/dist/acp-login.js +37 -8
  12. package/dist/acp-models.d.ts +6 -1
  13. package/dist/acp-models.js +29 -19
  14. package/dist/acp.js +120 -14
  15. package/dist/agent-commands.d.ts +4 -1
  16. package/dist/agent-commands.js +110 -42
  17. package/dist/agent-config-return.d.ts +46 -2
  18. package/dist/agent-config-return.js +110 -4
  19. package/dist/agent-credentials.js +7 -3
  20. package/dist/agent-repository.d.ts +16 -3
  21. package/dist/agent-repository.js +149 -74
  22. package/dist/agent-setup.d.ts +14 -3
  23. package/dist/agent-setup.js +78 -36
  24. package/dist/agent-target-commands.d.ts +20 -1
  25. package/dist/agent-target-commands.js +73 -31
  26. package/dist/agent-targets.js +6 -1
  27. package/dist/args.d.ts +1 -1
  28. package/dist/args.js +95 -54
  29. package/dist/bin.js +5 -1
  30. package/dist/browser.d.ts +13 -2
  31. package/dist/browser.js +30 -14
  32. package/dist/claude-cloud-login.d.ts +24 -2
  33. package/dist/claude-cloud-login.js +151 -11
  34. package/dist/cli-proxy-hub.d.ts +61 -25
  35. package/dist/cli-proxy-hub.js +202 -54
  36. package/dist/cli-proxy.d.ts +44 -1
  37. package/dist/cli-proxy.js +177 -28
  38. package/dist/client-settings.d.ts +39 -0
  39. package/dist/client-settings.js +91 -0
  40. package/dist/codex-cloud.d.ts +14 -4
  41. package/dist/codex-cloud.js +135 -31
  42. package/dist/codex-linux.d.ts +14 -0
  43. package/dist/codex-linux.js +69 -0
  44. package/dist/command-center-actions.js +18 -5
  45. package/dist/command-center-model.d.ts +2 -0
  46. package/dist/command-center-model.js +6 -1
  47. package/dist/command-center.js +3 -3
  48. package/dist/commands.js +173 -24
  49. package/dist/conversation-commands.d.ts +5 -0
  50. package/dist/conversation-commands.js +8 -5
  51. package/dist/conversation-terminal.d.ts +10 -2
  52. package/dist/conversation-terminal.js +78 -13
  53. package/dist/conversations.d.ts +86 -1
  54. package/dist/conversations.js +306 -23
  55. package/dist/desktop-linux.js +16 -3
  56. package/dist/device-bridge.d.ts +1 -1
  57. package/dist/device-bridge.js +6 -4
  58. package/dist/device-helper.js +43 -22
  59. package/dist/errors.d.ts +29 -2
  60. package/dist/errors.js +50 -7
  61. package/dist/filesystem-commands.d.ts +16 -9
  62. package/dist/filesystem-commands.js +133 -57
  63. package/dist/format.d.ts +2 -1
  64. package/dist/format.js +10 -1
  65. package/dist/handlers.js +3 -1
  66. package/dist/initial-prompt.d.ts +8 -0
  67. package/dist/initial-prompt.js +43 -1
  68. package/dist/large-media.d.ts +14 -0
  69. package/dist/large-media.js +100 -0
  70. package/dist/linux-terminal.d.ts +5 -3
  71. package/dist/linux-terminal.js +6 -20
  72. package/dist/local-sessions.d.ts +26 -2
  73. package/dist/local-sessions.js +113 -24
  74. package/dist/machine-setup.d.ts +11 -1
  75. package/dist/machine-setup.js +69 -22
  76. package/dist/machine-status.d.ts +67 -0
  77. package/dist/machine-status.js +140 -0
  78. package/dist/main.js +11 -13
  79. package/dist/menubar.d.ts +6 -5
  80. package/dist/menubar.js +38 -28
  81. package/dist/move-worktrees-restore.py +144 -0
  82. package/dist/move-worktrees.d.ts +69 -0
  83. package/dist/move-worktrees.js +252 -0
  84. package/dist/move.d.ts +10 -1
  85. package/dist/move.js +111 -16
  86. package/dist/onboarding.d.ts +11 -7
  87. package/dist/onboarding.js +63 -29
  88. package/dist/prepared-repository.d.ts +204 -0
  89. package/dist/prepared-repository.js +860 -0
  90. package/dist/prepared-repository.py +1307 -0
  91. package/dist/project-preparation.d.ts +62 -0
  92. package/dist/project-preparation.js +316 -0
  93. package/dist/project-sync-manifest.d.ts +4 -0
  94. package/dist/project-sync-manifest.js +5 -1
  95. package/dist/project-sync-transfer.js +1 -1
  96. package/dist/project-sync.d.ts +22 -4
  97. package/dist/project-sync.js +34 -8
  98. package/dist/project-template-setup.d.ts +33 -1
  99. package/dist/project-template-setup.js +59 -2
  100. package/dist/secret-ask.d.ts +72 -0
  101. package/dist/secret-ask.js +351 -0
  102. package/dist/seed-phases.d.ts +8 -1
  103. package/dist/seed-phases.js +17 -2
  104. package/dist/setup-cache.d.ts +7 -0
  105. package/dist/setup-cache.js +32 -0
  106. package/dist/t3-launch.d.ts +2 -1
  107. package/dist/t3-launch.js +12 -6
  108. package/dist/t3-mode.d.ts +8 -2
  109. package/dist/t3-mode.js +19 -4
  110. package/dist/t3-phone.d.ts +4 -2
  111. package/dist/t3-phone.js +12 -4
  112. package/dist/t3-proxy.d.ts +81 -0
  113. package/dist/t3-proxy.js +325 -0
  114. package/dist/terminal-apps.d.ts +168 -0
  115. package/dist/terminal-apps.js +574 -0
  116. package/dist/terminal-onboarding.js +6 -1
  117. package/dist/this-computer.d.ts +11 -0
  118. package/dist/this-computer.js +36 -0
  119. package/dist/thread-on-computer.js +4 -1
  120. package/dist/update.d.ts +2 -0
  121. package/dist/update.js +43 -9
  122. package/dist/worktree-twin.d.ts +10 -2
  123. package/dist/worktree-twin.js +27 -9
  124. package/package.json +5 -5
@@ -5,25 +5,36 @@
5
5
  */
6
6
  export declare const AGENT_SHELL_VARIABLES: readonly ["CLAUDECODE", "CODEX_SANDBOX", "CODEX_THREAD_ID", "CURSOR_AGENT", "GEMINI_CLI"];
7
7
  export declare function codingAgentShell(env: Record<string, string | undefined>): boolean;
8
- export declare const SETUP_QUESTION = "Is Shardflux for a SaaS platform (your product runs VMs for its own customers through the API, SDK or MCP), or for your personal setup (your own cloud coding agents: each Claude or Codex conversation gets its own VM)?";
8
+ /**
9
+ * How the agent runs the setup (0.20.1+): it does the work, the person only decides. Onboarding testers (2026-10-12)
10
+ * found agents handing them commands, links and codes, writing paragraphs and asking open questions.
11
+ */
12
+ export declare const SETUP_RULES: readonly ["Do every permitted step yourself: run each command and open each link. Never ask your user to paste a secret or sign-in code into the chat.", "Your user answers your questions, agrees to the Terms, approves sign-ins in the browser tabs shard opens, and types secrets into the page shard opens (you never see them).", "If your permission system or auto-mode classifier refuses an action, stop that action. Do not change permissions, wrap it in a script or use another tool to bypass the refusal. Explain the reason and give your user the exact refused command to run in their own terminal; continue independent permitted steps.", "After a refused action, ask for only its non-secret result and resume permitted steps when your user confirms it. Never ask for credentials, credential files or raw logs. Plugin installation needs confirmation that the plugin is installed; machine opening needs its workspace key, not local files or environment variables.", "To check saved account state without printing credentials, use shard setup --guide --json: guide.signed_in.email and guide.api_key_saved. It is a local check, not proof that a key works. If reading setup output or checking state is refused, have your user run this command and confirm those fields; do not read redirected output or credential files instead.", "A command exiting successfully is not proof of setup. Check its non-secret result and the requested behavior; keep any blocked or unobserved step unverified.", "Ask one question at a time as multiple choice, recommended answer first (use your question tool if you have one).", "Keep each message to one or two short sentences: what you are doing or what you need, never how it works.", "Commands that wait for your user in the browser (sign-ins, --ask) take up to 10 minutes: run them with a long timeout or in the background, and tell your user once what to do there."];
13
+ export declare const SETUP_QUESTION = "What are you setting up Shardflux for?";
14
+ /** The answers to SETUP_QUESTION, the usual one first. */
15
+ export declare const SETUP_CHOICES: readonly ["Your own cloud coding agents: each Claude or Codex conversation gets its own VM", "Your product: it runs VMs for its customers through the API, SDK or MCP"];
9
16
  export declare const AGENT_SETUP_DOCS = "https://docs.shardflux.dev/guides/agent-setup.md";
10
- /** One step of a path: what it does, what to ask the user first (if anything), and the commands in order. */
17
+ /** One step of a path: what it does, the one question to ask first (if any) with its answers, and the commands. */
11
18
  export interface GuideStep {
12
19
  step: string;
13
20
  ask?: string;
21
+ /** Multiple-choice answers to `ask`, the recommended one first. */
22
+ choices?: string[];
14
23
  commands: string[];
15
24
  note?: string;
16
25
  }
17
26
  /** What `shard setup --guide --json` prints. */
18
27
  export interface SetupGuide {
28
+ rules: string[];
19
29
  question: string;
30
+ choices: string[];
20
31
  signed_in: {
21
32
  email: string | null;
22
33
  } | null;
23
34
  api_key_saved: boolean;
24
35
  account: GuideStep;
25
- saas: GuideStep[];
26
36
  personal: GuideStep[];
37
+ saas: GuideStep[];
27
38
  terms_url: string;
28
39
  privacy_url: string;
29
40
  docs: string;
@@ -1,13 +1,16 @@
1
1
  /**
2
- * Setting Shardflux up from a coding agent (0.17.1+): the one question the agent asks its user first, and the ordered
3
- * steps of both answers. `shard setup --guide` prints it; so does a bare `shard` without a terminal or in a coding
4
- * agent's shell, where the command center would wait for keys nobody presses. Local only: no network call and nothing
5
- * to answer, so it returns at once.
2
+ * Setting Shardflux up from a coding agent (0.17.1+): how the agent runs it, the one question it asks first, and the
3
+ * ordered steps of both answers. `shard setup --guide` prints it; so does a bare `shard` without a terminal or in a
4
+ * coding agent's shell, where the command center would wait for keys nobody presses. Local only: no network call and
5
+ * nothing to answer, so it returns at once.
6
6
  *
7
- * - SaaS platform: the user's product runs VMs for its own customers (API, SDK, MCP). An account, an API key, the
7
+ * - Own cloud coding agents (the usual answer): sign in, connect Claude and Codex, set the machine up from this
8
+ * computer, choose where conversations open, set up the project and open the first conversation (0.20.1+).
9
+ * - A product (SaaS platform) running VMs for its own customers (API, SDK, MCP): an account, an API key, the
8
10
  * integration and templates for the customers' workspaces; no agent sign-ins, menu bar or machine.
9
- * - Personal setup: the user's own cloud coding agents. Sign in, connect Claude and Codex, build the machine from what
10
- * the user asks for, set up the project and hand over `shard ./`.
11
+ *
12
+ * The agent does permitted steps; the person answers multiple-choice questions, approves browser sign-ins and types
13
+ * secrets into the page shard opens. A real permission refusal requires a precise user handoff (SETUP_RULES).
11
14
  */
12
15
  import { legalFields } from "./legal.js";
13
16
  /**
@@ -19,7 +22,27 @@ export const AGENT_SHELL_VARIABLES = ['CLAUDECODE', 'CODEX_SANDBOX', 'CODEX_THRE
19
22
  export function codingAgentShell(env) {
20
23
  return AGENT_SHELL_VARIABLES.some((name) => (env[name]?.trim() ?? '') !== '');
21
24
  }
22
- export const SETUP_QUESTION = 'Is Shardflux for a SaaS platform (your product runs VMs for its own customers through the API, SDK or MCP), or for your personal setup (your own cloud coding agents: each Claude or Codex conversation gets its own VM)?';
25
+ /**
26
+ * How the agent runs the setup (0.20.1+): it does the work, the person only decides. Onboarding testers (2026-10-12)
27
+ * found agents handing them commands, links and codes, writing paragraphs and asking open questions.
28
+ */
29
+ export const SETUP_RULES = [
30
+ 'Do every permitted step yourself: run each command and open each link. Never ask your user to paste a secret or sign-in code into the chat.',
31
+ 'Your user answers your questions, agrees to the Terms, approves sign-ins in the browser tabs shard opens, and types secrets into the page shard opens (you never see them).',
32
+ 'If your permission system or auto-mode classifier refuses an action, stop that action. Do not change permissions, wrap it in a script or use another tool to bypass the refusal. Explain the reason and give your user the exact refused command to run in their own terminal; continue independent permitted steps.',
33
+ 'After a refused action, ask for only its non-secret result and resume permitted steps when your user confirms it. Never ask for credentials, credential files or raw logs. Plugin installation needs confirmation that the plugin is installed; machine opening needs its workspace key, not local files or environment variables.',
34
+ 'To check saved account state without printing credentials, use shard setup --guide --json: guide.signed_in.email and guide.api_key_saved. It is a local check, not proof that a key works. If reading setup output or checking state is refused, have your user run this command and confirm those fields; do not read redirected output or credential files instead.',
35
+ 'A command exiting successfully is not proof of setup. Check its non-secret result and the requested behavior; keep any blocked or unobserved step unverified.',
36
+ 'Ask one question at a time as multiple choice, recommended answer first (use your question tool if you have one).',
37
+ 'Keep each message to one or two short sentences: what you are doing or what you need, never how it works.',
38
+ 'Commands that wait for your user in the browser (sign-ins, --ask) take up to 10 minutes: run them with a long timeout or in the background, and tell your user once what to do there.',
39
+ ];
40
+ export const SETUP_QUESTION = 'What are you setting up Shardflux for?';
41
+ /** The answers to SETUP_QUESTION, the usual one first. */
42
+ export const SETUP_CHOICES = [
43
+ 'Your own cloud coding agents: each Claude or Codex conversation gets its own VM',
44
+ 'Your product: it runs VMs for its customers through the API, SDK or MCP',
45
+ ];
23
46
  export const AGENT_SETUP_DOCS = 'https://docs.shardflux.dev/guides/agent-setup.md';
24
47
  const { terms_url: TERMS, privacy_url: PRIVACY } = legalFields();
25
48
  export function setupGuide(state) {
@@ -27,46 +50,61 @@ export function setupGuide(state) {
27
50
  ? { step: 'Account', commands: ['shard setup --json'], note: `Signed in${state.email ? ` as ${state.email}` : ''}. shard setup saves the organization, project and API key (safe to run again).` }
28
51
  : {
29
52
  step: 'Account',
30
- ask: `Do you agree to the Terms of Service (${TERMS}) and the Privacy Policy (${PRIVACY})? Continue once they do.`,
53
+ ask: `Do you agree to the Terms of Service (${TERMS}) and the Privacy Policy (${PRIVACY})?`,
54
+ choices: ['Yes, I agree', 'No'],
31
55
  commands: ['shard signup --accept-terms --json'],
32
- note: 'It signs up or signs in with this computer’s Codex login and saves an API key. Without a Codex login, or for an account they already have: shard auth login --browser --no-wait --json (give your user the link and the code), then shard auth login --browser --json (returns once they approved) and shard setup --json.',
56
+ note: 'Signs up or signs in with this computer’s Codex login. Without one, or for an existing account: shard auth login --browser --json opens the sign-in page; tell your user to approve it there (if your command times out, run it again: it waits for the same request), then shard setup --json.',
33
57
  };
34
58
  return {
59
+ rules: [...SETUP_RULES],
35
60
  question: SETUP_QUESTION,
61
+ choices: [...SETUP_CHOICES],
36
62
  signed_in: state.signedIn ? { email: state.email } : null,
37
63
  api_key_saved: state.apiKey,
38
64
  account,
39
- saas: [
40
- { step: 'API key for your product', commands: ['eval "$(shard env)"'], note: 'Exports SHARDFLUX_API_KEY for the SDKs, the MCP server and your product’s server. Keep it in server-side secrets, never in client code.' },
41
- {
42
- step: 'Integrate',
43
- commands: ['npm install @shardflux/sdk', 'pip install shardflux'],
44
- note: 'Or the HTTP API (https://api.shardflux.dev/v1) or the MCP server (https://mcp.shardflux.dev/mcp). Your customers’ workspaces start from a template: shard templates build template.yaml. Start at https://docs.shardflux.dev/quickstart.md',
45
- },
46
- ],
47
65
  personal: [
48
66
  {
49
67
  step: 'Connect Claude and Codex',
50
- ask: 'Which agents do you use: Claude, Codex or both?',
51
- commands: ['shard setup --inspect --connect --json --apps claude,codex'],
52
- note: 'Claude: your user runs shard agents credentials login claude in their own terminal, once (a claude.ai link and a code). Codex: codex login on this computer; --connect copies it and starts the account’s Codex remote device. Pass only the apps they use.',
68
+ ask: 'Which coding agents do you use?',
69
+ choices: ['Claude Code and Codex', 'Claude Code', 'Codex'],
70
+ commands: ['shard setup --inspect --json', 'shard agents credentials login claude --json', 'shard setup --inspect --connect --json --apps claude,codex'],
71
+ note: 'Recommend what setup --inspect finds on this computer. The Claude sign-in opens claude.ai and a page for its code in your user’s browser. Codex uses this computer’s Codex login: run codex login yourself if it is missing. Pass only the apps they use.',
72
+ },
73
+ {
74
+ step: 'Set up the machine',
75
+ ask: 'Set up your cloud machine from this computer? It copies your tools, runtimes, shell, git identity and dotfiles.',
76
+ choices: ['Yes, copy this computer', 'Not now'],
77
+ commands: ['shard machine setup --from-this-computer --json', 'shard machine setup --status --json'],
78
+ note: 'This computer’s Claude Code or Codex does it read-only and saves the machine, in up to 10 minutes: tell your user once, then wait. Their tools carry over once machine.state in --status is ready; when it is failed, tell them its line and run its machine.action.command. Anything else they want on it: shard machine open --json, shard ws exec <key> -- bash -lc \'<install>\', shard files write <key> <path> --ask for a secret file (your user types it in the page), then shard machine save --wait --json.',
53
79
  },
54
80
  {
55
- step: 'Build the machine',
56
- ask: 'What should every cloud agent start with? Languages and runtimes, CLIs and tools, dotfiles and shell, secrets and credentials, editor and agent config.',
57
- commands: ['shard machine setup --from-this-computer --json', 'shard machine open --json', "shard ws exec <key> -- bash -lc '<install commands>'", 'shard files write <key> <path> --from -', 'shard machine save --wait --json'],
58
- note: 'machine setup recreates this computer’s tools, runtimes, shell, git identity and dotfiles on the machine with this computer’s Claude Code or Codex, and saves it (0.20.0+). For the rest: machine open prints the machine VM’s workspace key. Install with ws exec, write dotfiles and secret files with files write (contents on standard input, never in arguments), then save: every new conversation starts from it.',
81
+ step: 'Choose where conversations open',
82
+ ask: 'Where should your conversations open?',
83
+ choices: ['The terminal you use now', 'Claude app', 'Codex app', 'T3 Code'],
84
+ commands: ['shard agents targets --json', 'shard agents targets --open-in <target> --terminal <app> --json'],
85
+ note: 'Offer only what targets --json lists as installed, the terminal shard runs in first (for example Ghostty, iTerm2, Warp).',
59
86
  },
60
87
  {
61
88
  step: 'Set up the project',
62
- ask: 'Which repository, which project secrets, and what does a good first run look like (tests, a dev server)?',
63
- commands: ['shard ./ --json --prompt "Set up this project and run its tests"', 'shard conversations wait <conversation-id> --timeout 90', 'shard template --json'],
64
- note: 'Run it in the project folder (shard ./ codex … for Codex): the conversation gets its own VM, and setup installs the dependencies and saves the project template. Run the wait again while it says Still working. Project secrets: shard secrets import --from <hsec|1password|bitwarden|keychain> NAME, or printf %s "$VALUE" | shard secrets create NAME.',
89
+ ask: 'Does this project need secrets (API keys, tokens)?',
90
+ choices: ['No', 'Yes, I’ll type them in a secure page', 'Yes, from my vault (1Password, Bitwarden, Keychain, hsec)'],
91
+ commands: ['shard secrets create <NAME> --ask --json', 'shard secrets import --from <vault> <item> --as <NAME> --json', 'shard ./ --json --prompt "Set up this project and run its tests"', 'shard conversations wait <conversation-id> --timeout 90', 'shard template --json'],
92
+ note: 'secrets create --ask opens a page where your user types the value; you only see that it was saved. Run shard ./ in the project folder (shard ./ codex … for Codex): its VM installs the dependencies and saves the project template. Run the wait again while it says Still working.',
65
93
  },
66
94
  {
67
- step: 'Hand over',
95
+ step: 'Open the first conversation',
68
96
  commands: ['shard ./'],
69
- note: 'Your user runs it in their own terminal (shard ./ codex for Codex). Claude conversations continue on claude.ai and in the Claude app; Codex threads in the Codex app, on the web and on the phone.',
97
+ note: 'It opens in the app your user chose. Tell them, in one sentence, that shard ./ in any project folder starts the next one.',
98
+ },
99
+ ],
100
+ saas: [
101
+ { step: 'API key for your product', commands: ['eval "$(shard env)"'], note: 'Exports SHARDFLUX_API_KEY for the SDKs, the MCP server and your product’s server. Keep it in server-side secrets, never in client code.' },
102
+ {
103
+ step: 'Integrate',
104
+ ask: 'How will your product call Shardflux?',
105
+ choices: ['TypeScript SDK', 'Python SDK', 'HTTP API', 'MCP server'],
106
+ commands: ['npm install @shardflux/sdk', 'pip install shardflux'],
107
+ note: 'Or the HTTP API (https://api.shardflux.dev/v1) or the MCP server (https://mcp.shardflux.dev/mcp). Your customers’ workspaces start from a template: shard templates build template.yaml. Start at https://docs.shardflux.dev/quickstart.md',
70
108
  },
71
109
  ],
72
110
  terms_url: TERMS,
@@ -94,23 +132,27 @@ function stepText(n, s) {
94
132
  const head = `${n}. ${s.step}.${s.ask ? ` Ask: ${s.ask}` : ''}`;
95
133
  return [
96
134
  ` ${wrap(head, ' ')}`,
135
+ ...(s.choices ? [` ${wrap(`Choices: ${s.choices.join(' | ')}`, ' ', 96)}`] : []),
97
136
  ...s.commands.map((c) => ` ${c}`),
98
137
  ...(s.note ? [` ${wrap(s.note, ' ', 96)}`] : []),
99
138
  ].join('\n');
100
139
  }
101
140
  export function setupGuideText(g) {
102
141
  return [
103
- 'Set up Shardflux. Ask your user one question first:',
104
- ` ${wrap(g.question, ' ')}`,
142
+ 'Set up Shardflux for your user. How:',
143
+ ...g.rules.map((r) => ` - ${wrap(r, ' ', 98)}`),
105
144
  '',
106
- stepText(1, g.account),
145
+ 'First ask, as multiple choice:',
146
+ ` ${wrap(`${g.question} Choices: ${g.choices.join(' | ')}`, ' ')}`,
107
147
  '',
108
- 'SaaS platform:',
109
- ...g.saas.map((s, i) => stepText(i + 2, s)),
148
+ stepText(1, g.account),
110
149
  '',
111
- 'Personal setup:',
150
+ 'Own cloud coding agents:',
112
151
  ...g.personal.map((s, i) => stepText(i + 2, s)),
113
152
  '',
153
+ 'Product (SaaS platform):',
154
+ ...g.saas.map((s, i) => stepText(i + 2, s)),
155
+ '',
114
156
  `Every step in detail: ${g.docs}. Every command: shard --help.`,
115
157
  ].join('\n') + '\n';
116
158
  }
@@ -1,9 +1,10 @@
1
1
  import { Duplex } from 'node:stream';
2
- import type { CellClient, Workspace } from '@shardflux/sdk';
2
+ import type { CellClient, T3Link, Workspace } from '@shardflux/sdk';
3
3
  import type { Conversation } from './conversations.js';
4
4
  import type { Ctx, Handler, Io } from './commands.js';
5
5
  import { AgentTargets } from './agent-targets.js';
6
6
  import type { DesktopTarget } from './agent-targets.js';
7
+ import { CodedError } from './errors.js';
7
8
  import type { T3Install, T3ThreadRequest } from './t3-mode.js';
8
9
  /** Optimistic file replacement preserves keys installed by other devices connecting at the same time. */
9
10
  export declare function installDeviceKey(cell: CellClient, key: string, signal?: AbortSignal): Promise<void>;
@@ -35,5 +36,23 @@ export declare function openClaudeDesktopSession(ctx: Ctx, ws: Workspace, conver
35
36
  export declare function openDesktopWorkspace(ctx: Ctx, ws: Workspace, target: DesktopTarget, t3Version?: string, prewarmed?: boolean, thread?: T3ThreadRequest, install?: T3Install, conversation?: string | null): Promise<number>;
36
37
  /** T3 Code's Connections page (its t3code:// link), where a project linked with T3 Connect is listed. */
37
38
  export declare const T3_CONNECTIONS_PAGE = "t3code://app/settings/connections";
39
+ /** A workspace's link as `agents open t3` reports it (`t3_link`): the PUT answer, or the outcome the status read gave. */
40
+ export type T3LinkView = Omit<T3Link, 'status'> & {
41
+ status: T3Link['status'] | 'error';
42
+ last_error?: string;
43
+ needs_login?: boolean;
44
+ };
45
+ /** How long `agents open t3` waits for a link in progress to be linked or refused (at most the wake timeout). */
46
+ export declare const T3_LINK_WAIT_MS = 30000;
47
+ /**
48
+ * The link T3 Code needs is refused (`t3_link_failed`, with the API's remedy: for T3 Connect's environment limit, its
49
+ * Connections page) or still linking after T3_LINK_WAIT_MS (`t3_link_pending`). Both name the workspace, conversation
50
+ * and any started thread, so a client opens what exists again rather than starting another.
51
+ */
52
+ export declare function t3LinkNotReady(link: T3LinkView, ref: {
53
+ workspace_key: string;
54
+ conversation_id: string | null;
55
+ t3_thread_id: string | null;
56
+ }): CodedError;
38
57
  export declare function queueT3Prewarm(ctx: Ctx, ws: Workspace, version: string): Promise<void>;
39
58
  export declare const TARGET_HANDLERS: Record<string, Handler>;
@@ -5,24 +5,26 @@ import { ensureCloudClaude } from "./claude-cloud-login.js";
5
5
  import { spawn } from 'node:child_process';
6
6
  import { Readable, Duplex } from 'node:stream';
7
7
  import { ShardfluxApiError, WorkspaceTunnels } from '@shardflux/sdk';
8
- import { getConversation } from "./conversations.js";
8
+ import { conversationForOpen } from "./conversations.js";
9
9
  import { out, resolveWorkspace } from "./commands.js";
10
10
  import { MODEL_CHOICES } from "./agent-models.js";
11
11
  import { AgentTargets, installedTargets, targetLabel } from "./agent-targets.js";
12
- import { join, relative } from 'node:path';
12
+ import { join, relative, resolve } from 'node:path';
13
13
  import { projectIdentity } from "./project-identity.js";
14
14
  import { projectWorkspacePath } from "./agent-repository.js";
15
- import { UsageError, describeError } from "./errors.js";
15
+ import { CodedError, UsageError, describeError } from "./errors.js";
16
16
  import { t3PhoneLink } from "./t3-phone.js";
17
17
  import { prepareDesktopRouter } from "./conversation-router.js";
18
18
  import { openUrl } from "./browser.js";
19
19
  import { agentMachineName, prepareAgentMachine } from "./agent-machine.js";
20
20
  import { prewarmT3 } from "./t3-runtime.js";
21
+ import { followT3ProxyInVm } from "./t3-proxy.js";
21
22
  import { AgentStateStore } from "./agent-state.js";
22
23
  import { workspaceRepositoryPath } from "./agent-repository.js";
23
- import { openInChoices, openT3, saveOpenIn, startSshThread, t3Install, t3NotInstalled, t3OpenFailed, t3Status } from "./t3-mode.js";
24
+ import { openInChoices, openInTargets, openT3, saveOpenIn, startSshThread, t3Install, t3NotInstalled, t3OpenFailed, t3Status } from "./t3-mode.js";
24
25
  import { openT3Conversation, t3Fields, withT3Context } from "./t3-launch.js";
25
- import { conversationPooled, pooledNeedsT3, targetsAccounts } from "./account-pool.js";
26
+ import { targetsAccounts } from "./account-pool.js";
27
+ import { detectTerminals, openInTerminal, parseTerminalChoice, reportTerminalOpen, saveTerminalChoice, terminalName, terminalReport } from "./terminal-apps.js";
26
28
  const missing = (e) => e instanceof ShardfluxApiError && e.status === 404;
27
29
  const keyOf = (ref) => ref.includes('/') ? ref : `${projectIdentity('./').slug}/${ref}`;
28
30
  /** How long opening a Claude conversation this computer just started waits for Claude to publish its Remote Control URL. */
@@ -220,22 +222,23 @@ async function openLink(url, env) {
220
222
  });
221
223
  }
222
224
  const openTarget = (target) => async (ctx, values, [ref = 'main']) => {
225
+ // A conversation started in T3 (its Shardflux provider) opens its own thread there (0.17.0+); a Codex Cloud one opens
226
+ // its Codex chat (0.23.0+). openT3Conversation checks T3 Code itself where it needs it.
227
+ let conversation = await conversationForOpen(ctx, /^conv_[a-z0-9]{26}$/.test(ref) ? ref : keyOf(ref));
228
+ if (target === 't3' && conversation)
229
+ return openT3Conversation(ctx, conversation.id, values);
230
+ // A Codex Cloud conversation opens its existing thread in the Codex app, not an SSH environment (0.23.0+).
231
+ if (target === 'codex-app' && conversation?.route === 'codex-cloud')
232
+ return (await import("./codex-cloud.js")).openCodexConversation(ctx, conversation);
223
233
  const detected = installedTargets(ctx.io.env);
224
234
  if (target === 't3' && !detected.targets.includes('t3'))
225
235
  throw t3NotInstalled();
236
+ if (target === 'codex-app' && !detected.targets.includes(target) && detected.clients.includes('codex-app')) {
237
+ throw new UsageError('On Linux the Codex app opens Codex Cloud conversations (shard agents open codex-app <conversation>).', `Open this one with: shard agents open ${ref}`);
238
+ }
226
239
  if (target !== 'web' && !detected.targets.includes(target))
227
240
  throw new UsageError(`${target} is not installed on this machine`);
228
- // A conversation started in T3 (its Shardflux provider) opens its own thread there (0.17.0+).
229
- if (target === 't3' && /^conv_[a-z0-9]{26}$/.test(ref))
230
- return openT3Conversation(ctx, ref, values);
231
241
  // A conversation id opens that conversation's own session on its computer (claude.ai, the desktop apps).
232
- let conversation = /^conv_[a-z0-9]{26}$/.test(ref) ? await getConversation(ctx, ref) : undefined;
233
- // A pooled thread (0.18.0+) has no provider login on its session: it opens remotely in T3 Code.
234
- if (conversation && conversationPooled(conversation)) {
235
- if (!detected.targets.includes('t3'))
236
- throw pooledNeedsT3(ref);
237
- return openT3Conversation(ctx, ref, values);
238
- }
239
242
  // A conversation whose VM was removed restores into a fresh one first (0.18.1+).
240
243
  let ws;
241
244
  if (conversation) {
@@ -257,7 +260,7 @@ const openTarget = (target) => async (ctx, values, [ref = 'main']) => {
257
260
  await ensureCloudClaude(ctx, ws);
258
261
  if (target === 'web') {
259
262
  const url = await remoteControlUrl(ctx, ws, conversation);
260
- if (!ctx.json && !openUrl(url, ctx.io.env))
263
+ if (!ctx.json && !(await openUrl(url, ctx.io.env)))
261
264
  throw new Error(`could not open ${url}`);
262
265
  out(ctx, `${url}\n`, { target, workspace: ws.data, url, ...(conversation ? { conversation_id: conversation.id } : {}) });
263
266
  return 0;
@@ -274,6 +277,9 @@ export async function openDesktopWorkspace(ctx, ws, target, t3Version, prewarmed
274
277
  await prepareAgentMachine(ctx, ws);
275
278
  if (target === 't3' && t3Version && !prewarmed)
276
279
  await queueT3Prewarm(ctx, ws, t3Version);
280
+ // The VM's T3 Code follows the user's T3 proxy setup, before its server starts for a thread or the T3 Connect link.
281
+ if (target === 't3')
282
+ await followT3ProxyInVm(ctx, ws);
277
283
  const targets = new AgentTargets(ctx.io.env, ctx.store.dir);
278
284
  const entry = (await (ws.labels.shard_router === 'project' ? targets.configureProject(ws.key, ctx.apiUrl, [target], ctx.signal) : targets.configure(ws.key, ctx.apiUrl, [target], false, ctx.signal))).find(e => e.workspace === ws.key);
279
285
  const instructions = target === 't3'
@@ -296,8 +302,13 @@ export async function openDesktopWorkspace(ctx, ws, target, t3Version, prewarmed
296
302
  // The first time a project is set up for T3 Code, T3 opens on its Connections page once the project is linked: the
297
303
  // project is listed there (or T3's own Sign in to T3 Connect, when T3 Code is signed out).
298
304
  if (target === 't3') {
299
- if (fresh && phone?.link.status === 'linking')
300
- phone = await waitT3Linked(ctx, ws, phone);
305
+ // T3 Code lists the project only once its T3 Connect link is linked (0.23.0+): a refused or still pending link is
306
+ // reported as such, naming what exists, instead of an opened T3 that does not show it.
307
+ if (phone?.link.status === 'linking')
308
+ phone = await waitT3Link(ctx, ws, phone);
309
+ const ref = { workspace_key: ws.key, conversation_id: conversation ?? (ws.labels.shard_conversation || null), t3_thread_id: started?.t3_thread_id ?? null };
310
+ if (phone?.link.status === 'error' || phone?.link.status === 'linking')
311
+ throw t3LinkNotReady(phone.link, ref);
301
312
  const page = fresh && phone?.link.status === 'linked' ? T3_CONNECTIONS_PAGE : undefined;
302
313
  // The thread exists once startSshThread returns: a failure to show T3 names it rather than inviting a retry.
303
314
  try {
@@ -327,7 +338,7 @@ export const T3_CONNECTIONS_PAGE = 't3code://app/settings/connections';
327
338
  */
328
339
  async function linkT3Phone(ctx, ws) {
329
340
  try {
330
- const { link, signIn, needsLogin } = await t3PhoneLink(ctx, ws.id);
341
+ const { link, signIn, needsLogin } = await t3PhoneLink(ctx, ws.id, ws.data.project_id);
331
342
  return { line: t3LinkLine(link, signIn, needsLogin), link: signIn ? { ...link, sign_in: { ...link.sign_in, ...signIn } } : needsLogin ? { ...link, needs_login: true } : link };
332
343
  }
333
344
  catch (error) {
@@ -343,25 +354,40 @@ const t3LinkLine = (link, signIn, needsLogin) => signIn
343
354
  : needsLogin ? `To have ${link.label} appear in T3 Code and the T3 app, sign in with shard auth login, then run this again.\n`
344
355
  : link.status === 'linked' ? `${link.label} is linked to your T3 account.\n`
345
356
  : `Linking ${link.label} to your T3 account…\n`;
346
- /** A project linking for the first time: T3 lists it once linked, so its Connections page waits for that (at most 20 s). */
347
- async function waitT3Linked(ctx, ws, phone) {
348
- const until = Date.now() + 20_000;
349
- while (Date.now() < until && !ctx.signal?.aborted) {
350
- await delay(2000);
357
+ /** How long `agents open t3` waits for a link in progress to be linked or refused (at most the wake timeout). */
358
+ export const T3_LINK_WAIT_MS = 30_000;
359
+ /** A link in progress: its outcome from the account's T3 status, read every 2 s for at most T3_LINK_WAIT_MS. */
360
+ async function waitT3Link(ctx, ws, phone) {
361
+ const until = Date.now() + Math.min(T3_LINK_WAIT_MS, ctx.wakeTimeoutMs ?? T3_LINK_WAIT_MS);
362
+ for (;;) {
351
363
  try {
352
364
  const known = (await ctx.cloud().t3.status()).links.find(l => l.workspace_id === ws.id);
353
- if (known?.status === 'linked') {
354
- const link = { ...phone.link, status: 'linked' };
365
+ if (known && known.status !== 'linking') {
366
+ const link = { ...phone.link, status: known.status === 'linked' ? 'linked' : known.status === 'needs_sign_in' ? 'needs_sign_in' : 'error', ...(known.last_error ? { last_error: known.last_error } : {}) };
355
367
  return { line: t3LinkLine(link), link };
356
368
  }
357
- if (known && known.status !== 'linking')
358
- return phone;
359
369
  }
360
370
  catch {
361
371
  return phone;
362
372
  }
373
+ if (Date.now() >= until || ctx.signal?.aborted)
374
+ return phone;
375
+ await delay(2000);
363
376
  }
364
- return phone;
377
+ }
378
+ /**
379
+ * The link T3 Code needs is refused (`t3_link_failed`, with the API's remedy: for T3 Connect's environment limit, its
380
+ * Connections page) or still linking after T3_LINK_WAIT_MS (`t3_link_pending`). Both name the workspace, conversation
381
+ * and any started thread, so a client opens what exists again rather than starting another.
382
+ */
383
+ export function t3LinkNotReady(link, ref) {
384
+ const again = `shard agents open t3 ${ref.conversation_id ?? ref.workspace_key}`;
385
+ const details = { ...ref, t3_link: link };
386
+ if (link.status !== 'error') {
387
+ return new CodedError('t3_link_pending', `Linking ${link.label} to your T3 account. T3 Code lists it once it is linked.`, `Open it in a moment: ${again}`, details);
388
+ }
389
+ const limit = /environment limit/i.test(link.last_error ?? '');
390
+ return new CodedError('t3_link_failed', link.last_error ?? `T3 Connect could not link ${link.label}.`, limit ? `In T3 Code open Settings → Connections (${T3_CONNECTIONS_PAGE}), remove an environment you no longer need, then: ${again}` : `Then open it again: ${again}`, limit ? { ...details, recovery: { title: 'Open T3 Code Connections', url: T3_CONNECTIONS_PAGE } } : details);
365
391
  }
366
392
  export async function queueT3Prewarm(ctx, ws, version) {
367
393
  try {
@@ -410,9 +436,18 @@ export const TARGET_HANDLERS = {
410
436
  const accounts = ctx.json ? targetsAccounts(ctx) : undefined;
411
437
  const project = targetsProject(ref, typeof values.path === 'string' ? values.path : undefined);
412
438
  const t3 = t3Status(ctx, { project, lastUsed: true });
439
+ // Terminals (0.20.1+): the installed ones, the one this runs in, and the app terminal conversations open in.
440
+ const terminals = detectTerminals(ctx.io.env, process.platform, { spotlight: true });
441
+ const terminal = typeof values.terminal === 'string' ? parseTerminalChoice(values.terminal, terminals) : undefined;
413
442
  if (typeof values['open-in'] === 'string')
414
443
  saveOpenIn(ctx, values['open-in'], t3, found.clients);
415
- const detected = { ...found, t3, open_in: openInChoices(ctx, project, { clients: found.clients }), ...(accounts ? { accounts: await accounts } : {}) };
444
+ if (terminal !== undefined)
445
+ saveTerminalChoice(ctx.store.dir, terminal);
446
+ const terminalFields = terminalReport(ctx.store.dir, ctx.io.env, terminals);
447
+ // open_in_targets (0.21.1+): every place each app's conversations can open from this computer, so a client offers
448
+ // exactly what --open-in takes.
449
+ const detected = { ...found, t3, open_in: openInChoices(ctx, project, { clients: found.clients }),
450
+ open_in_targets: { claude: [...openInTargets('claude')], codex: [...openInTargets('codex')] }, ...terminalFields, ...(accounts ? { accounts: await accounts } : {}) };
416
451
  const targets = new AgentTargets(ctx.io.env, ctx.store.dir);
417
452
  const key = ref ? keyOf(ref) : undefined;
418
453
  const before = values.remove === true ? await targets.configure(undefined, ctx.apiUrl, [], false, ctx.signal) : [];
@@ -421,7 +456,8 @@ export const TARGET_HANDLERS = {
421
456
  : [];
422
457
  const detection = `${detected.clients.join(', ') || 'No installed agent clients'}\nOpen targets: ${detected.targets.join(', ')}\n` +
423
458
  (t3.installed ? `T3 Code ${t3.version ?? ''}: ${t3.mode === 'acp' ? 'Shardflux provider' : 'SSH environments'}, ${t3.connected ? 'connected' : 'not connected'}\n`.replace(' :', ':') : '') +
424
- `New conversations open in: Claude → ${detected.open_in.claude}, Codex → ${detected.open_in.codex}\n`;
459
+ `New conversations open in: Claude → ${detected.open_in.claude}, Codex → ${detected.open_in.codex}\n` +
460
+ (terminalFields.terminal ? `Terminal conversations open in: ${terminalName(terminalFields.terminal.id)} (${terminalFields.terminal.mode === 'tab' ? 'a new tab' : 'a new window'}${terminalFields.terminal.saved ? '' : ', automatic'})\n` : '');
425
461
  if (values.remove === true) {
426
462
  const removed = before.filter(e => !entries.some(n => n.host === e.host));
427
463
  out(ctx, removed.length ? `Removed ${removed.map(e => `${e.workspace} (${e.host})`).join(', ')} from ${targetFiles(targets, removed).map(f => tilde(targets, f)).join(', ')}\n` : 'No managed targets to remove.\n', { ...detected, entries, removed, models: MODEL_CHOICES });
@@ -438,6 +474,12 @@ export const TARGET_HANDLERS = {
438
474
  ['claude-desktop', 'codex-app', 't3'].filter(t => t === 't3' ? !t3Installed : !entry.targets.includes(t)).map(t => ` ${{ 'claude-desktop': 'Claude Desktop', 'codex-app': 'Codex app', t3: 'T3 Code' }[t].padEnd(15)} not installed on this computer\n`).join(''), { ...detected, entries, configured: configured.map(({ target, file, name }) => ({ target, file, name })), models: MODEL_CHOICES });
439
475
  return Promise.resolve(0);
440
476
  },
477
+ /** An app's own commands (sign-ins, the command center) in the person's terminal app (0.20.1+, the menu bar). */
478
+ async 'agents terminal-run'(ctx, values, argv) {
479
+ const cwd = typeof values.cwd === 'string' ? resolve(values.cwd) : process.cwd() !== '/' ? process.cwd() : ctx.io.env.HOME || process.cwd();
480
+ reportTerminalOpen(ctx, await openInTerminal(ctx.store.dir, ctx.io.env, argv, { cwd, window: values.window === true }));
481
+ return 0;
482
+ },
441
483
  'agents open claude-desktop': openTarget('claude-desktop'),
442
484
  'agents open codex-app': openTarget('codex-app'),
443
485
  'agents open t3': openTarget('t3'),
@@ -8,6 +8,7 @@ import { UsageError } from "./errors.js";
8
8
  import { projectWorkspacePath } from "./agent-repository.js";
9
9
  import { withTargetLock } from "./target-lock.js";
10
10
  import { linuxApplicationDirs, linuxSupportsAcp, linuxT3Apps } from "./t3-linux.js";
11
+ import { linuxCodexApps } from "./codex-linux.js";
11
12
  import { t3ArchiveSupportsAcp } from "./t3-asar.js";
12
13
  import { compareT3Versions } from "./t3-version.js";
13
14
  const PERSISTED_TARGETS = ['claude-desktop', 'codex-app'];
@@ -132,7 +133,11 @@ export function installedTargets(env, platform = process.platform, appDirs) {
132
133
  const clients = [hasApp('Claude.app', 'com.anthropic.claudefordesktop') && 'claude-desktop', (hasApp('Codex.app', 'com.openai.codex') || hasApp('ChatGPT.app', 'com.openai.codex')) && 'codex-app', executable('claude') && 'claude', executable('codex') && 'codex'].filter((v) => !!v);
133
134
  if (t3)
134
135
  clients.push('t3');
135
- // Linux has T3 Code (0.17.0+); Claude Desktop and the Codex app are macOS apps.
136
+ // The Codex app on Linux (0.23.0+): the entry that handles codex:// (OpenAI's ChatGPT app for Linux, or a community
137
+ // build). It opens Codex Cloud conversations; its SSH environments stay a macOS target.
138
+ if (platform === 'linux' && linuxCodexApps(dirs, env).length > 0)
139
+ clients.push('codex-app');
140
+ // Linux has T3 Code (0.17.0+); Claude Desktop and the Codex app's SSH environments are macOS targets.
136
141
  const desktop = platform === 'darwin' ? clients.filter(c => c.endsWith('-app') || c === 'claude-desktop' || c === 't3') : platform === 'linux' && t3 ? ['t3'] : [];
137
142
  return { targets: [...desktop, 'web'], clients, versions };
138
143
  }
package/dist/args.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const CLI_VERSION = "0.20.0";
1
+ export declare const CLI_VERSION = "0.23.0";
2
2
  export interface OptionSpec {
3
3
  type: 'string' | 'boolean';
4
4
  short?: string;