@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.
- package/CHANGELOG.md +312 -1
- package/README.md +242 -75
- package/dist/account-args.js +7 -2
- package/dist/account-commands.js +56 -106
- package/dist/account-pool.d.ts +19 -15
- package/dist/account-pool.js +47 -96
- package/dist/account-scope.d.ts +65 -0
- package/dist/account-scope.js +246 -0
- package/dist/acp-journal.js +58 -6
- package/dist/acp-login.d.ts +13 -5
- package/dist/acp-login.js +37 -8
- package/dist/acp-models.d.ts +6 -1
- package/dist/acp-models.js +29 -19
- package/dist/acp.js +120 -14
- package/dist/agent-commands.d.ts +4 -1
- package/dist/agent-commands.js +110 -42
- package/dist/agent-config-return.d.ts +46 -2
- package/dist/agent-config-return.js +110 -4
- package/dist/agent-credentials.js +7 -3
- package/dist/agent-repository.d.ts +16 -3
- package/dist/agent-repository.js +149 -74
- package/dist/agent-setup.d.ts +14 -3
- package/dist/agent-setup.js +78 -36
- package/dist/agent-target-commands.d.ts +20 -1
- package/dist/agent-target-commands.js +73 -31
- package/dist/agent-targets.js +6 -1
- package/dist/args.d.ts +1 -1
- package/dist/args.js +95 -54
- package/dist/bin.js +5 -1
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -14
- package/dist/claude-cloud-login.d.ts +24 -2
- package/dist/claude-cloud-login.js +151 -11
- package/dist/cli-proxy-hub.d.ts +61 -25
- package/dist/cli-proxy-hub.js +202 -54
- package/dist/cli-proxy.d.ts +44 -1
- package/dist/cli-proxy.js +177 -28
- package/dist/client-settings.d.ts +39 -0
- package/dist/client-settings.js +91 -0
- package/dist/codex-cloud.d.ts +14 -4
- package/dist/codex-cloud.js +135 -31
- package/dist/codex-linux.d.ts +14 -0
- package/dist/codex-linux.js +69 -0
- package/dist/command-center-actions.js +18 -5
- package/dist/command-center-model.d.ts +2 -0
- package/dist/command-center-model.js +6 -1
- package/dist/command-center.js +3 -3
- package/dist/commands.js +173 -24
- package/dist/conversation-commands.d.ts +5 -0
- package/dist/conversation-commands.js +8 -5
- package/dist/conversation-terminal.d.ts +10 -2
- package/dist/conversation-terminal.js +78 -13
- package/dist/conversations.d.ts +86 -1
- package/dist/conversations.js +306 -23
- package/dist/desktop-linux.js +16 -3
- package/dist/device-bridge.d.ts +1 -1
- package/dist/device-bridge.js +6 -4
- package/dist/device-helper.js +43 -22
- package/dist/errors.d.ts +29 -2
- package/dist/errors.js +50 -7
- package/dist/filesystem-commands.d.ts +16 -9
- package/dist/filesystem-commands.js +133 -57
- package/dist/format.d.ts +2 -1
- package/dist/format.js +10 -1
- package/dist/handlers.js +3 -1
- package/dist/initial-prompt.d.ts +8 -0
- package/dist/initial-prompt.js +43 -1
- package/dist/large-media.d.ts +14 -0
- package/dist/large-media.js +100 -0
- package/dist/linux-terminal.d.ts +5 -3
- package/dist/linux-terminal.js +6 -20
- package/dist/local-sessions.d.ts +26 -2
- package/dist/local-sessions.js +113 -24
- package/dist/machine-setup.d.ts +11 -1
- package/dist/machine-setup.js +69 -22
- package/dist/machine-status.d.ts +67 -0
- package/dist/machine-status.js +140 -0
- package/dist/main.js +11 -13
- package/dist/menubar.d.ts +6 -5
- package/dist/menubar.js +38 -28
- package/dist/move-worktrees-restore.py +144 -0
- package/dist/move-worktrees.d.ts +69 -0
- package/dist/move-worktrees.js +252 -0
- package/dist/move.d.ts +10 -1
- package/dist/move.js +111 -16
- package/dist/onboarding.d.ts +11 -7
- package/dist/onboarding.js +63 -29
- package/dist/prepared-repository.d.ts +204 -0
- package/dist/prepared-repository.js +860 -0
- package/dist/prepared-repository.py +1307 -0
- package/dist/project-preparation.d.ts +62 -0
- package/dist/project-preparation.js +316 -0
- package/dist/project-sync-manifest.d.ts +4 -0
- package/dist/project-sync-manifest.js +5 -1
- package/dist/project-sync-transfer.js +1 -1
- package/dist/project-sync.d.ts +22 -4
- package/dist/project-sync.js +34 -8
- package/dist/project-template-setup.d.ts +33 -1
- package/dist/project-template-setup.js +59 -2
- package/dist/secret-ask.d.ts +72 -0
- package/dist/secret-ask.js +351 -0
- package/dist/seed-phases.d.ts +8 -1
- package/dist/seed-phases.js +17 -2
- package/dist/setup-cache.d.ts +7 -0
- package/dist/setup-cache.js +32 -0
- package/dist/t3-launch.d.ts +2 -1
- package/dist/t3-launch.js +12 -6
- package/dist/t3-mode.d.ts +8 -2
- package/dist/t3-mode.js +19 -4
- package/dist/t3-phone.d.ts +4 -2
- package/dist/t3-phone.js +12 -4
- package/dist/t3-proxy.d.ts +81 -0
- package/dist/t3-proxy.js +325 -0
- package/dist/terminal-apps.d.ts +168 -0
- package/dist/terminal-apps.js +574 -0
- package/dist/terminal-onboarding.js +6 -1
- package/dist/this-computer.d.ts +11 -0
- package/dist/this-computer.js +36 -0
- package/dist/thread-on-computer.js +4 -1
- package/dist/update.d.ts +2 -0
- package/dist/update.js +43 -9
- package/dist/worktree-twin.d.ts +10 -2
- package/dist/worktree-twin.js +27 -9
- package/package.json +5 -5
package/dist/agent-setup.d.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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;
|
package/dist/agent-setup.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Setting Shardflux up from a coding agent (0.17.1+): the
|
|
3
|
-
* steps of both answers. `shard setup --guide` prints it; so does a bare `shard` without a terminal or in a
|
|
4
|
-
* agent's shell, where the command center would wait for keys nobody presses. Local only: no network call and
|
|
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
|
-
* -
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
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})
|
|
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: '
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
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: '
|
|
56
|
-
ask: '
|
|
57
|
-
|
|
58
|
-
|
|
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: '
|
|
63
|
-
|
|
64
|
-
|
|
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: '
|
|
95
|
+
step: 'Open the first conversation',
|
|
68
96
|
commands: ['shard ./'],
|
|
69
|
-
note: '
|
|
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
|
|
104
|
-
` ${wrap(
|
|
142
|
+
'Set up Shardflux for your user. How:',
|
|
143
|
+
...g.rules.map((r) => ` - ${wrap(r, ' ', 98)}`),
|
|
105
144
|
'',
|
|
106
|
-
|
|
145
|
+
'First ask, as multiple choice:',
|
|
146
|
+
` ${wrap(`${g.question} Choices: ${g.choices.join(' | ')}`, ' ')}`,
|
|
107
147
|
'',
|
|
108
|
-
|
|
109
|
-
...g.saas.map((s, i) => stepText(i + 2, s)),
|
|
148
|
+
stepText(1, g.account),
|
|
110
149
|
'',
|
|
111
|
-
'
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
/**
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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'),
|
package/dist/agent-targets.js
CHANGED
|
@@ -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
|
-
//
|
|
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