@shardflux/cli 0.15.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +5 -3
- package/dist/account-args.js +3 -3
- package/dist/account-commands.d.ts +2 -0
- package/dist/account-commands.js +18 -3
- package/dist/agent-commands.js +22 -8
- package/dist/agent-target-commands.d.ts +14 -0
- package/dist/agent-target-commands.js +73 -32
- package/dist/args.d.ts +1 -1
- package/dist/args.js +2 -2
- package/dist/format.js +3 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.1
|
|
4
|
+
|
|
5
|
+
- `shard setup`, `shard signup` and `shard api-keys create` make keys with every workspace tool, `computer` included, so the saved key can watch and screenshot a workspace's desktop (`shard ws computer stream`, `screenshot`), as the first-run demo does. `shard setup` replaces a saved key from an earlier setup (every tool but `computer`) with one that has every tool; the old key keeps working until revoked. `--tool` lists `computer`.
|
|
6
|
+
|
|
7
|
+
- `shard agents open --in auto` opens a Claude conversation where you work: in Claude Desktop when it is installed (its own session, `claude://code/<session>`), else Claude Code in this terminal. With `--json` it prints `{target: "claude-desktop", url, app_bundle_id}` or `{target: "terminal", command}`. claude.ai opens with `--in web` (or a saved web target). Codex conversations open in the Codex app as before.
|
|
8
|
+
|
|
9
|
+
- `shard setup` and `shard signup` print "Starting your Codex remote device…" while they wait for it.
|
|
10
|
+
|
|
3
11
|
## 0.15.0
|
|
4
12
|
|
|
5
13
|
- The onboarding activity report includes the conversation's turn state for Claude and Codex, including while its VM is suspended.
|
package/README.md
CHANGED
|
@@ -258,10 +258,12 @@ shard auth totp recovery-codes
|
|
|
258
258
|
"<your name>'s organization" (`--org-name` to choose); with several it lists them;
|
|
259
259
|
2. the project: `--project`, the saved default, or the organization's only one; with none it creates
|
|
260
260
|
"Default" (`--project-name`);
|
|
261
|
-
3. an API key with every workspace tool (`--tool` repeatable to narrow it), named
|
|
261
|
+
3. an API key with every workspace tool, `computer` included (0.15.1+; `--tool` repeatable to narrow it), named
|
|
262
262
|
`shard-cli <hostname>` (`--key-name`), saved in the credentials file and checked against the API.
|
|
263
263
|
|
|
264
|
-
It is idempotent: a saved key that still works is kept (`--new-key` makes a new one).
|
|
264
|
+
It is idempotent: a saved key that still works is kept (`--new-key` makes a new one). A saved key from an earlier
|
|
265
|
+
`shard setup` (every tool but `computer`) is replaced with one that has every tool (0.15.1+); the old key keeps working
|
|
266
|
+
until you revoke it. It prints what
|
|
265
267
|
it found or created and the next command. The key is never printed; `shard env` prints it for tools
|
|
266
268
|
that read `SHARDFLUX_API_KEY`:
|
|
267
269
|
|
|
@@ -1261,7 +1263,7 @@ shard agents new --app claude --path ./ --prompt "What is wrong on this page?" -
|
|
|
1261
1263
|
|
|
1262
1264
|
`--image <file|->` (0.15.0+, repeatable) attaches up to five PNG, JPEG, GIF or WebP images of up to 10 MiB each to that first message. They are written into the conversation VM under `/home/user/.shardflux/attachments/` and named in the message: Claude Code attaches them as images and Codex opens them with its image viewer. `--image -` reads one image, or PNG images back to back, from standard input; use a file for `--prompt-file` when images come from standard input.
|
|
1263
1265
|
|
|
1264
|
-
Use `shard agents open <workspace> --in auto --json` to resolve
|
|
1266
|
+
Use `shard agents open <workspace> --in auto --json` to resolve where a conversation opens: a saved target first; then, for Claude (0.15.1+), the conversation itself in Claude Desktop when it is installed (`{"target":"claude-desktop","url":"claude://code/session_…","app_bundle_id":…}`), else Claude Code in the terminal (`{"target":"terminal","command":["shard","agents","open","<workspace>"]}`; without `--json` it attaches in this terminal). Codex opens its Cloud thread in the Codex app. claude.ai opens with `--in web`. The caller opens the `url` or app, or runs the command in a terminal. Explicit `--in terminal` retains terminal attachment. The menu uses this resolver for new and existing conversations, without provider URLs in Swift.
|
|
1265
1267
|
|
|
1266
1268
|
## Codex Cloud (0.15.0+, unreleased)
|
|
1267
1269
|
|
package/dist/account-args.js
CHANGED
|
@@ -58,7 +58,7 @@ export const ACCOUNT_COMMANDS = [
|
|
|
58
58
|
name: { type: 'string', value: '<display name>', description: 'Your name as others see it (setup names the organization after it).' },
|
|
59
59
|
codex: { type: 'boolean', description: 'Use the Codex login on this machine (default; --no-codex signs up with --email).' },
|
|
60
60
|
setup: { type: 'boolean', description: 'Run shard setup afterwards (default; --no-setup skips it).' },
|
|
61
|
-
tool: { type: 'string', multiple: true, value: '<tool>', description: 'Workspace tool the saved API key may use (repeatable; exec, files, pty, process, git, browser). Default: all.' },
|
|
61
|
+
tool: { type: 'string', multiple: true, value: '<tool>', description: 'Workspace tool the saved API key may use (repeatable; exec, files, pty, process, git, browser, computer). Default: all.' },
|
|
62
62
|
...ACCEPT_TERMS,
|
|
63
63
|
...SECOND_FACTOR,
|
|
64
64
|
},
|
|
@@ -210,7 +210,7 @@ export const ACCOUNT_COMMANDS = [
|
|
|
210
210
|
'org-name': { type: 'string', value: '<name>', description: 'Name of the organization to create when you have none.' },
|
|
211
211
|
'project-name': { type: 'string', value: '<name>', description: 'Name of the project to create when the organization has none (default Default).' },
|
|
212
212
|
'key-name': { type: 'string', value: '<name>', description: 'Name of the API key (default "shard-cli <hostname>").' },
|
|
213
|
-
tool: { type: 'string', multiple: true, value: '<tool>', description: 'Workspace tool the key may use (repeatable; exec, files, pty, process, git, browser). Default: all.' },
|
|
213
|
+
tool: { type: 'string', multiple: true, value: '<tool>', description: 'Workspace tool the key may use (repeatable; exec, files, pty, process, git, browser, computer). Default: all.' },
|
|
214
214
|
'new-key': { type: 'boolean', description: 'Create and save a new key even when the saved one works (the old key stays valid until revoked).' },
|
|
215
215
|
codex: { type: 'boolean', description: 'Set up the account Codex Cloud executor with this machine’s Codex login (default when signed in); --no-codex skips it.' },
|
|
216
216
|
},
|
|
@@ -281,7 +281,7 @@ export const ACCOUNT_COMMANDS = [
|
|
|
281
281
|
description: 'Owners and admins grant workspace tools; billing members create keys without tools. The secret cannot be shown again.',
|
|
282
282
|
positionals: [{ name: 'name', required: true }],
|
|
283
283
|
options: {
|
|
284
|
-
tool: { type: 'string', multiple: true, value: '<tool>', description: 'Workspace tool the key may use (repeatable). Default: all
|
|
284
|
+
tool: { type: 'string', multiple: true, value: '<tool>', description: 'Workspace tool the key may use (repeatable; exec, files, pty, process, git, browser, computer). Default: all.' },
|
|
285
285
|
'read-only': { type: 'boolean', description: 'No workspace tools (read-only API use).' },
|
|
286
286
|
expires: { type: 'string', value: '<30d|90d|365d|never>', description: 'Expiry (default never).' },
|
|
287
287
|
save: { type: 'boolean', description: 'Save it in the credentials file for the workspace commands instead of printing it.' },
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Handler } from './commands.js';
|
|
2
2
|
export declare const DEFAULT_API_URL = "https://api.shardflux.dev";
|
|
3
|
+
/** What `shard setup` gave a key before 0.15.1: every tool but `computer`. Such a saved key is replaced with a full one. */
|
|
4
|
+
export declare function outdatedDefaultKey(permissions: readonly string[]): boolean;
|
|
3
5
|
/**
|
|
4
6
|
* The organization name setup creates: "<display name or email local part>'s organization", within the API's rules
|
|
5
7
|
* for names (1-200 characters, no control characters or line breaks).
|
package/dist/account-commands.js
CHANGED
|
@@ -27,7 +27,15 @@ import { apiKeyTable, attachmentTable, auditTable, authSessionTable, billingStat
|
|
|
27
27
|
export const DEFAULT_API_URL = 'https://api.shardflux.dev';
|
|
28
28
|
const TOTP = /^\d{6}$/;
|
|
29
29
|
const ROLES = ['owner', 'admin', 'member', 'billing'];
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Every tool permission a key can carry, `computer` included (0.15.1+): the SDK's list before 0.21.1 left it out, so a
|
|
32
|
+
* key made by `shard setup` could not watch or use its workspace's desktop (computer stream 403).
|
|
33
|
+
*/
|
|
34
|
+
const TOOL_NAMES = [...new Set([...API_KEY_TOOL_PERMISSIONS, 'computer'])];
|
|
35
|
+
/** What `shard setup` gave a key before 0.15.1: every tool but `computer`. Such a saved key is replaced with a full one. */
|
|
36
|
+
export function outdatedDefaultKey(permissions) {
|
|
37
|
+
return !permissions.includes('computer') && TOOL_NAMES.every((t) => t === 'computer' || permissions.includes(t));
|
|
38
|
+
}
|
|
31
39
|
const DEFAULT_CHECKOUT_WAIT_MS = 15 * 60_000;
|
|
32
40
|
const CHECKOUT_POLL_MS = 2_000;
|
|
33
41
|
const NEXT_WORKSPACE = 'shard ws open demo --template python-node-browser';
|
|
@@ -307,7 +315,7 @@ async function checkKey(ctx, token) {
|
|
|
307
315
|
const cloud = new Shardflux({ ...ctx.accountOptions(), apiKey: token });
|
|
308
316
|
const me = await cloud.me();
|
|
309
317
|
if (me.type === 'api_key' && me.api_key)
|
|
310
|
-
return { ok: true, organizationId: me.api_key.organization_id, projectId: me.api_key.project_id };
|
|
318
|
+
return { ok: true, organizationId: me.api_key.organization_id, projectId: me.api_key.project_id, toolPermissions: me.api_key.tool_permissions ?? [] };
|
|
311
319
|
return { ok: false, reason: 'not a project API key' };
|
|
312
320
|
}
|
|
313
321
|
catch (err) {
|
|
@@ -622,7 +630,12 @@ async function runSetup(ctx, values, account) {
|
|
|
622
630
|
else
|
|
623
631
|
throw new UsageError(`${org.name} has ${projects.length} projects; pick one with --project <id|slug>:\n${choices(projects)}\n`, 'or save a default first: shard projects use <id|slug>');
|
|
624
632
|
}
|
|
625
|
-
|
|
633
|
+
// A key from an earlier `shard setup` (every tool but computer) is replaced unless --tool asks for a set: the desktop
|
|
634
|
+
// stream and screenshots need `computer`. The old key stays valid until revoked.
|
|
635
|
+
const outdated = keyCheck?.ok === true && strs(values.tool).length === 0 && values['read-only'] !== true && outdatedDefaultKey(keyCheck.toolPermissions);
|
|
636
|
+
if (outdated && !ctx.json)
|
|
637
|
+
ctx.io.stderr.write(`note: the saved API key ${profile.api_key.key_id} cannot use computer use; creating one with every tool.\n`);
|
|
638
|
+
const keep = keyCheck?.ok === true && profile.api_key?.project_id === project.id && !outdated;
|
|
626
639
|
let key;
|
|
627
640
|
let permissions = tools;
|
|
628
641
|
if (keep)
|
|
@@ -655,6 +668,8 @@ async function runSetup(ctx, values, account) {
|
|
|
655
668
|
const cloud = new Shardflux({ apiKey: key.token, baseUrl: ctx.apiUrl });
|
|
656
669
|
await syncAgentCredentials(cloud, { home: credentialHome, redact: value => ctx.addSecret(value) });
|
|
657
670
|
await cloud.codex.enable({ signal: ctx.signal });
|
|
671
|
+
if (!ctx.json)
|
|
672
|
+
ctx.io.stderr.write('Starting your Codex remote device…\n');
|
|
658
673
|
codex = { ...await cloud.codex.waitConnector({ timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal }) };
|
|
659
674
|
}
|
|
660
675
|
const envNote = ctx.io.env.SHARDFLUX_API_KEY?.trim() ? 'note: SHARDFLUX_API_KEY is set in this environment and wins over the saved key; unset it to use the saved key.\n' : '';
|
package/dist/agent-commands.js
CHANGED
|
@@ -11,7 +11,7 @@ import { prepareProjectTemplate } from "./project-template-setup.js";
|
|
|
11
11
|
import { projectIdentity, projectLabels } from "./project-identity.js";
|
|
12
12
|
import { ensureProjectGit } from "./project-git.js";
|
|
13
13
|
import { json } from "./format.js";
|
|
14
|
-
import { resolveWorkspace } from "./commands.js";
|
|
14
|
+
import { out, resolveWorkspace } from "./commands.js";
|
|
15
15
|
import { savedMachine } from "./filesystem-state.js";
|
|
16
16
|
import { startAgentConfigTunnels, syncAgentConfig } from "./agent-config-sync.js";
|
|
17
17
|
import { rememberGitAgent } from "./agent-git-commands.js";
|
|
@@ -22,7 +22,7 @@ import { progress } from "./progress.js";
|
|
|
22
22
|
import { rememberLaunch } from "./agent-live.js";
|
|
23
23
|
import { prepareAgentMachine } from "./agent-machine.js";
|
|
24
24
|
import { installedTargets } from "./agent-targets.js";
|
|
25
|
-
import { openDesktopWorkspace, queueT3Prewarm } from "./agent-target-commands.js";
|
|
25
|
+
import { openClaudeDesktopSession, openDesktopWorkspace, queueT3Prewarm } from "./agent-target-commands.js";
|
|
26
26
|
import { submitInitialPrompt, prepareInitialCodexProject } from "./initial-prompt.js";
|
|
27
27
|
import { conversationWorkspace, conversationsUnavailable, getConversation, listConversations, resumeConversation } from "./conversations.js";
|
|
28
28
|
import { conversationTerminal } from "./conversation-terminal.js";
|
|
@@ -272,12 +272,26 @@ export const AGENT_HANDLERS = {
|
|
|
272
272
|
delete next.in;
|
|
273
273
|
return AGENT_HANDLERS['agents open'](ctx, next, [ws.key]);
|
|
274
274
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
275
|
+
if (saved && detected.targets.includes(saved)) {
|
|
276
|
+
const { HANDLERS } = await import("./handlers.js");
|
|
277
|
+
const handler = HANDLERS[`agents open ${saved}`];
|
|
278
|
+
if (!handler)
|
|
279
|
+
throw new UsageError('Choose an installed open target with shard agents targets.');
|
|
280
|
+
return handler(ctx, { ...values, in: saved }, [ws.key]);
|
|
281
|
+
}
|
|
282
|
+
// Claude opens where people work (0.15.1+): the conversation in Claude Desktop when it is installed, else Claude
|
|
283
|
+
// Code in a terminal. The browser (claude.ai) only when it is the chosen target (--in web).
|
|
284
|
+
if (detected.clients.includes('claude-desktop')) {
|
|
285
|
+
return openClaudeDesktopSession(ctx, ws);
|
|
286
|
+
}
|
|
287
|
+
if (ctx.json) {
|
|
288
|
+
// An app runs this in the person's terminal (the menu bar opens Terminal with it).
|
|
289
|
+
out(ctx, `shard agents open ${ws.key}\n`, { target: 'terminal', workspace: ws.data, command: ['shard', 'agents', 'open', ws.key] });
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
const next = { ...values };
|
|
293
|
+
delete next.in;
|
|
294
|
+
return AGENT_HANDLERS['agents open'](ctx, next, [ws.key]);
|
|
281
295
|
}
|
|
282
296
|
if (values.in !== undefined && values.in !== 'terminal') {
|
|
283
297
|
const { HANDLERS } = await import("./handlers.js");
|
|
@@ -8,6 +8,20 @@ export declare function installDeviceKey(cell: CellClient, key: string, signal?:
|
|
|
8
8
|
/** Host trust is established over the authenticated files API, before sending any SSH protocol bytes. */
|
|
9
9
|
export declare function prepareSsh(ws: Workspace, ctx: Ctx, targets: AgentTargets, host: string): Promise<CellClient>;
|
|
10
10
|
export declare function proxyStream(io: Io, ended?: () => void, outputGate?: Promise<unknown>): Duplex;
|
|
11
|
+
/**
|
|
12
|
+
* The Remote Control URL (https://claude.ai/code/session_…) of the workspace's live Claude session. A Claude process
|
|
13
|
+
* this computer just started can accept its first prompt before Remote Control registers, so an open of our recent
|
|
14
|
+
* launch probes the guest for up to 30 s; history browsing stays passive.
|
|
15
|
+
*/
|
|
16
|
+
export declare function remoteControlUrl(ctx: Ctx, ws: Workspace): Promise<string>;
|
|
17
|
+
/** Claude Desktop's own link to a Remote Control session: claude://code/<session id> opens it in its Code tab. */
|
|
18
|
+
export declare function claudeDesktopLink(remoteControl: string): string;
|
|
19
|
+
export declare const CLAUDE_DESKTOP_BUNDLE = "com.anthropic.claudefordesktop";
|
|
20
|
+
/**
|
|
21
|
+
* Opens the conversation itself (its live Remote Control session) in Claude Desktop (0.15.1+), the way `--in auto`
|
|
22
|
+
* opens a Claude conversation when Claude Desktop is installed. --json prints {target, url, app_bundle_id} for apps.
|
|
23
|
+
*/
|
|
24
|
+
export declare function openClaudeDesktopSession(ctx: Ctx, ws: Workspace): Promise<number>;
|
|
11
25
|
export declare function openDesktopWorkspace(ctx: Ctx, ws: Workspace, target: DesktopTarget, t3Version?: string, prewarmed?: boolean): Promise<number>;
|
|
12
26
|
export declare function queueT3Prewarm(ctx: Ctx, ws: Workspace, version: string): Promise<void>;
|
|
13
27
|
export declare const TARGET_HANDLERS: Record<string, Handler>;
|
|
@@ -132,6 +132,78 @@ export function proxyStream(io, ended, outputGate) {
|
|
|
132
132
|
source.once('error', (error) => stream.destroy(error));
|
|
133
133
|
return stream;
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The Remote Control URL (https://claude.ai/code/session_…) of the workspace's live Claude session. A Claude process
|
|
137
|
+
* this computer just started can accept its first prompt before Remote Control registers, so an open of our recent
|
|
138
|
+
* launch probes the guest for up to 30 s; history browsing stays passive.
|
|
139
|
+
*/
|
|
140
|
+
export async function remoteControlUrl(ctx, ws) {
|
|
141
|
+
let cursor;
|
|
142
|
+
let url;
|
|
143
|
+
do {
|
|
144
|
+
const page = await ctx.cloud().agents.list({ limit: 200, ...(cursor ? { cursor } : {}) });
|
|
145
|
+
url = page.data.find(a => a.workspace_id === ws.id && a.app === 'claude' && a.state !== 'exited' && a.remote_control_url)?.remote_control_url;
|
|
146
|
+
cursor = page.nextCursor ?? undefined;
|
|
147
|
+
} while (!url && cursor && !ctx.signal.aborted);
|
|
148
|
+
// A new Claude process can accept its first prompt before Remote Control registers.
|
|
149
|
+
// Only an explicit open of our recent launch probes the guest; history browsing stays passive.
|
|
150
|
+
const launchedAt = new AgentStateStore(ctx.store.dir).read().agent_launches?.[ws.id];
|
|
151
|
+
if (!url && typeof launchedAt === 'number' && Date.now() - launchedAt < LAUNCH_WINDOW_MS && ws.ready) {
|
|
152
|
+
const waitMs = Math.min(ctx.wakeTimeoutMs, RECENT_LAUNCH_URL_WAIT_MS);
|
|
153
|
+
const deadline = Date.now() + waitMs;
|
|
154
|
+
const cell = ws.cell({ agentLabel: ctx.agentLabel });
|
|
155
|
+
while (!url && Date.now() < deadline) {
|
|
156
|
+
const result = await cell.exec.run(['sf-agent', 'list'], {
|
|
157
|
+
timeoutMs: Math.min(10_000, Math.max(1, deadline - Date.now())), signal: ctx.signal, maxOutputBytes: 1 << 20,
|
|
158
|
+
});
|
|
159
|
+
if (result.exitCode !== 0)
|
|
160
|
+
break;
|
|
161
|
+
url = guestSessions(result.stdout).find(a => a.app === 'claude' && a.state !== 'exited' && a.remote_control_url)?.remote_control_url;
|
|
162
|
+
if (!url && Date.now() < deadline)
|
|
163
|
+
await delay(Math.min(250, Math.max(1, deadline - Date.now())), undefined, { signal: ctx.signal });
|
|
164
|
+
}
|
|
165
|
+
if (!url)
|
|
166
|
+
throw new Error(`Claude in ${ws.key} did not publish a Remote Control URL within ${Math.ceil(waitMs / 1000)} s; run shard agents open ${ws.key} again`);
|
|
167
|
+
}
|
|
168
|
+
if (!url)
|
|
169
|
+
throw new UsageError(`${ws.key} has no active Remote Control URL; start Claude with Remote Control first`);
|
|
170
|
+
const parsed = new URL(url);
|
|
171
|
+
if (parsed.protocol !== 'https:' || parsed.hostname !== 'claude.ai' || !parsed.pathname.startsWith('/code/') || parsed.username || parsed.password)
|
|
172
|
+
throw new Error('agent returned an invalid Claude Remote Control URL');
|
|
173
|
+
return url;
|
|
174
|
+
}
|
|
175
|
+
/** Claude Desktop's own link to a Remote Control session: claude://code/<session id> opens it in its Code tab. */
|
|
176
|
+
export function claudeDesktopLink(remoteControl) {
|
|
177
|
+
const id = new URL(remoteControl).pathname.split('/')[2] ?? '';
|
|
178
|
+
if (!/^(cse|session)_[A-Za-z0-9_-]+$/.test(id))
|
|
179
|
+
throw new Error('agent returned an invalid Claude Remote Control URL');
|
|
180
|
+
return `claude://code/${id}`;
|
|
181
|
+
}
|
|
182
|
+
export const CLAUDE_DESKTOP_BUNDLE = 'com.anthropic.claudefordesktop';
|
|
183
|
+
/**
|
|
184
|
+
* Opens the conversation itself (its live Remote Control session) in Claude Desktop (0.15.1+), the way `--in auto`
|
|
185
|
+
* opens a Claude conversation when Claude Desktop is installed. --json prints {target, url, app_bundle_id} for apps.
|
|
186
|
+
*/
|
|
187
|
+
export async function openClaudeDesktopSession(ctx, ws) {
|
|
188
|
+
await ensureCloudClaude(ctx, ws);
|
|
189
|
+
const session = await remoteControlUrl(ctx, ws);
|
|
190
|
+
const url = claudeDesktopLink(session);
|
|
191
|
+
if (!ctx.json)
|
|
192
|
+
await openLink(url, ctx.io.env);
|
|
193
|
+
out(ctx, `${url}\n`, { target: 'claude-desktop', workspace: ws.data, url, remote_control_url: session, app_bundle_id: CLAUDE_DESKTOP_BUNDLE });
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
async function openLink(url, env) {
|
|
197
|
+
const child = spawn('open', [url], { env, stdio: 'ignore' });
|
|
198
|
+
await new Promise((resolve, reject) => {
|
|
199
|
+
const timer = setTimeout(() => { child.kill('SIGKILL'); reject(new Error('Claude Desktop did not open in time')); }, 10_000);
|
|
200
|
+
child.once('error', e => { clearTimeout(timer); reject(e); });
|
|
201
|
+
child.once('exit', code => { clearTimeout(timer); if (code === 0)
|
|
202
|
+
resolve();
|
|
203
|
+
else
|
|
204
|
+
reject(new Error(`could not open ${url}`)); });
|
|
205
|
+
});
|
|
206
|
+
}
|
|
135
207
|
const openTarget = (target) => async (ctx, values, [ref = 'main']) => {
|
|
136
208
|
const detected = installedTargets(ctx.io.env);
|
|
137
209
|
if (target !== 'web' && !detected.targets.includes(target))
|
|
@@ -142,38 +214,7 @@ const openTarget = (target) => async (ctx, values, [ref = 'main']) => {
|
|
|
142
214
|
if (target !== 'codex-app')
|
|
143
215
|
await ensureCloudClaude(ctx, ws);
|
|
144
216
|
if (target === 'web') {
|
|
145
|
-
|
|
146
|
-
let url;
|
|
147
|
-
do {
|
|
148
|
-
const page = await ctx.cloud().agents.list({ limit: 200, ...(cursor ? { cursor } : {}) });
|
|
149
|
-
url = page.data.find(a => a.workspace_id === ws.id && a.app === 'claude' && a.state !== 'exited' && a.remote_control_url)?.remote_control_url;
|
|
150
|
-
cursor = page.nextCursor ?? undefined;
|
|
151
|
-
} while (!url && cursor && !ctx.signal.aborted);
|
|
152
|
-
// A new Claude process can accept its first prompt before Remote Control registers.
|
|
153
|
-
// Only an explicit open of our recent launch probes the guest; history browsing stays passive.
|
|
154
|
-
const launchedAt = new AgentStateStore(ctx.store.dir).read().agent_launches?.[ws.id];
|
|
155
|
-
if (!url && typeof launchedAt === 'number' && Date.now() - launchedAt < LAUNCH_WINDOW_MS && ws.ready) {
|
|
156
|
-
const waitMs = Math.min(ctx.wakeTimeoutMs, RECENT_LAUNCH_URL_WAIT_MS);
|
|
157
|
-
const deadline = Date.now() + waitMs;
|
|
158
|
-
const cell = ws.cell({ agentLabel: ctx.agentLabel });
|
|
159
|
-
while (!url && Date.now() < deadline) {
|
|
160
|
-
const result = await cell.exec.run(['sf-agent', 'list'], {
|
|
161
|
-
timeoutMs: Math.min(10_000, Math.max(1, deadline - Date.now())), signal: ctx.signal, maxOutputBytes: 1 << 20,
|
|
162
|
-
});
|
|
163
|
-
if (result.exitCode !== 0)
|
|
164
|
-
break;
|
|
165
|
-
url = guestSessions(result.stdout).find(a => a.app === 'claude' && a.state !== 'exited' && a.remote_control_url)?.remote_control_url;
|
|
166
|
-
if (!url && Date.now() < deadline)
|
|
167
|
-
await delay(Math.min(250, Math.max(1, deadline - Date.now())), undefined, { signal: ctx.signal });
|
|
168
|
-
}
|
|
169
|
-
if (!url)
|
|
170
|
-
throw new Error(`Claude in ${ws.key} did not publish a Remote Control URL within ${Math.ceil(waitMs / 1000)} s; run shard agents open ${ws.key} again`);
|
|
171
|
-
}
|
|
172
|
-
if (!url)
|
|
173
|
-
throw new UsageError(`${ws.key} has no active Remote Control URL; start Claude with Remote Control first`);
|
|
174
|
-
const parsed = new URL(url);
|
|
175
|
-
if (parsed.protocol !== 'https:' || parsed.hostname !== 'claude.ai' || !parsed.pathname.startsWith('/code/') || parsed.username || parsed.password)
|
|
176
|
-
throw new Error('agent returned an invalid Claude Remote Control URL');
|
|
217
|
+
const url = await remoteControlUrl(ctx, ws);
|
|
177
218
|
if (!ctx.json && !openUrl(url, ctx.io.env))
|
|
178
219
|
throw new Error(`could not open ${url}`);
|
|
179
220
|
out(ctx, `${url}\n`, { target, workspace: ws.data, url });
|
package/dist/args.d.ts
CHANGED
package/dist/args.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { parseArgs } from 'node:util';
|
|
13
13
|
import { ACCOUNT_COMMANDS } from "./account-args.js";
|
|
14
14
|
import { UsageError } from "./errors.js";
|
|
15
|
-
export const CLI_VERSION = '0.15.
|
|
15
|
+
export const CLI_VERSION = '0.15.1';
|
|
16
16
|
export const GLOBAL_OPTIONS = {
|
|
17
17
|
'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
|
|
18
18
|
json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
|
|
@@ -1207,7 +1207,7 @@ const AGENT_COMMANDS = [
|
|
|
1207
1207
|
},
|
|
1208
1208
|
})),
|
|
1209
1209
|
...['status', 'enable', 'disable'].map(action => ({ path: ['agents', 'codex', action], summary: `${action} the account's billed Codex Cloud connector`, positionals: [], options: {} })),
|
|
1210
|
-
{ path: ['agents', 'open'], summary: 'Open a conversation (Codex Cloud or the native Claude session)', description: 'Codex opens its existing Cloud thread without waking its VM. Claude attaches its native session.', positionals: [{ name: 'agent', required: true }], options: { ...AGENT_OPTIONS, app: { type: 'string', value: '<claude|codex>', description: 'Agent app (default remembered choice).' }, in: { type: 'string', value: '<terminal|auto|claude-desktop|codex-app|web|t3>', description: 'Where to open a
|
|
1210
|
+
{ path: ['agents', 'open'], summary: 'Open a conversation (Codex Cloud or the native Claude session)', description: 'Codex opens its existing Cloud thread without waking its VM. Claude attaches its native session.', positionals: [{ name: 'agent', required: true }], options: { ...AGENT_OPTIONS, app: { type: 'string', value: '<claude|codex>', description: 'Agent app (default remembered choice).' }, in: { type: 'string', value: '<terminal|auto|claude-desktop|codex-app|web|t3>', description: 'Where to open it. auto (0.15.1+): the saved target, else a Claude conversation in Claude Desktop when it is installed, else in this terminal; web opens claude.ai.' } } },
|
|
1211
1211
|
{ path: ['agents', 'new'], summary: 'Start a conversation in a fresh VM; Codex opens a genuine Cloud thread', positionals: [{ name: 'agent' }], options: {
|
|
1212
1212
|
...AGENT_OPTIONS,
|
|
1213
1213
|
computer: COMPUTER_OPTION,
|
package/dist/format.js
CHANGED
|
@@ -813,10 +813,12 @@ export function keyStatus(k, now = Date.now()) {
|
|
|
813
813
|
return 'expired';
|
|
814
814
|
return 'active';
|
|
815
815
|
}
|
|
816
|
+
/** Every workspace tool a key can carry (`computer` since the API's computer use). */
|
|
817
|
+
const ALL_TOOLS = ['exec', 'files', 'pty', 'process', 'git', 'browser', 'computer'];
|
|
816
818
|
export function toolsText(tools) {
|
|
817
819
|
if (tools.length === 0)
|
|
818
820
|
return '(none: read-only)';
|
|
819
|
-
return
|
|
821
|
+
return ALL_TOOLS.every((t) => tools.includes(t)) ? 'all' : tools.join(',');
|
|
820
822
|
}
|
|
821
823
|
export function apiKeyTable(rows, savedId) {
|
|
822
824
|
return table(['', 'ID', 'KEY ID', 'NAME', 'TOOLS', 'STATUS', 'CREATED', 'EXPIRES', 'LAST USED'], rows.map((k) => [mark(k.id === savedId), k.id, k.key_id, k.name, toolsText(k.tool_permissions), keyStatus(k), time(k.created_at), time(k.expires_at), time(k.last_used_at)]));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardflux/cli",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.1",
|
|
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",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"typescript-eslint": "8.70.1"
|
|
59
59
|
},
|
|
60
60
|
"optionalDependencies": {
|
|
61
|
-
"@shardflux/menubar-darwin": "^0.3.
|
|
61
|
+
"@shardflux/menubar-darwin": "^0.3.1"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"build": "node scripts/build.mjs",
|