@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 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
  "&lt;your name&gt;'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). It prints what
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 the remembered app destination. The result includes a `url` or `app_bundle_id` when that app route is available; the caller opens it. Explicit `--in terminal` retains terminal attachment. The menu uses this resolver for new and existing conversations, without provider URLs in Swift.
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
 
@@ -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 six.' },
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).
@@ -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
- const TOOL_NAMES = API_KEY_TOOL_PERMISSIONS;
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
- const keep = keyCheck?.ok === true && profile.api_key?.project_id === project.id;
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' : '';
@@ -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
- const target = saved && detected.targets.includes(saved) ? saved : 'web';
276
- const { HANDLERS } = await import("./handlers.js");
277
- const handler = HANDLERS[`agents open ${target}`];
278
- if (!handler)
279
- throw new UsageError('Choose an installed open target with shard agents targets.');
280
- return handler(ctx, { ...values, in: target }, [ws.key]);
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
- let cursor;
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
@@ -1,4 +1,4 @@
1
- export declare const CLI_VERSION = "0.15.0";
1
+ export declare const CLI_VERSION = "0.15.1";
2
2
  export interface OptionSpec {
3
3
  type: 'string' | 'boolean';
4
4
  short?: string;
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.0';
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 legacy session.' } } },
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 tools.length === 6 ? 'all' : tools.join(',');
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.0",
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.0"
61
+ "@shardflux/menubar-darwin": "^0.3.1"
62
62
  },
63
63
  "scripts": {
64
64
  "build": "node scripts/build.mjs",