@phnx-labs/agents-cli 1.22.45 → 1.22.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +303 -0
  2. package/LICENSE +102 -182
  3. package/README.md +54 -17
  4. package/dist/cli/command-registry.d.ts +2 -1
  5. package/dist/cli/command-registry.js +6 -2
  6. package/dist/commands/accounts.js +56 -12
  7. package/dist/commands/auth.d.ts +2 -0
  8. package/dist/commands/auth.js +229 -0
  9. package/dist/commands/browser-sessions-picker.js +9 -7
  10. package/dist/commands/browser.js +266 -15
  11. package/dist/commands/config.js +26 -4
  12. package/dist/commands/cost.d.ts +1 -1
  13. package/dist/commands/detach.d.ts +13 -0
  14. package/dist/commands/detach.js +11 -8
  15. package/dist/commands/exec.js +55 -38
  16. package/dist/commands/feed-watch.d.ts +2 -0
  17. package/dist/commands/feed-watch.js +28 -0
  18. package/dist/commands/feed.js +27 -0
  19. package/dist/commands/feedback.js +3 -19
  20. package/dist/commands/insights.d.ts +1 -1
  21. package/dist/commands/insights.js +2 -2
  22. package/dist/commands/lease.js +3 -3
  23. package/dist/commands/routines.js +1 -1
  24. package/dist/commands/sessions-picker-factory.d.ts +2 -1
  25. package/dist/commands/sessions-picker-factory.js +1 -1
  26. package/dist/commands/sessions-stop.d.ts +24 -0
  27. package/dist/commands/sessions-stop.js +66 -0
  28. package/dist/commands/sessions-trace.d.ts +82 -0
  29. package/dist/commands/sessions-trace.js +387 -0
  30. package/dist/commands/sessions.js +4 -0
  31. package/dist/commands/ssh.d.ts +15 -0
  32. package/dist/commands/ssh.js +194 -16
  33. package/dist/commands/teams.js +34 -2
  34. package/dist/commands/view.d.ts +2 -0
  35. package/dist/commands/view.js +19 -32
  36. package/dist/commands/watchdog.js +9 -14
  37. package/dist/lib/account-capabilities.d.ts +4 -0
  38. package/dist/lib/account-capabilities.js +13 -9
  39. package/dist/lib/account-registry.d.ts +2 -0
  40. package/dist/lib/account-registry.js +30 -2
  41. package/dist/lib/accounting/rotate.d.ts +9 -0
  42. package/dist/lib/accounting/rotate.js +12 -2
  43. package/dist/lib/accounting/usage.d.ts +92 -6
  44. package/dist/lib/accounting/usage.js +329 -75
  45. package/dist/lib/agent-modes.js +29 -1
  46. package/dist/lib/agent-spec/agents.d.ts +4 -3
  47. package/dist/lib/agent-spec/agents.js +20 -7
  48. package/dist/lib/analytics/mix-commands.d.ts +1 -1
  49. package/dist/lib/analytics/mix-commands.js +2 -2
  50. package/dist/lib/auth-health.d.ts +13 -3
  51. package/dist/lib/auth-health.js +50 -8
  52. package/dist/lib/browser/devices.d.ts +19 -0
  53. package/dist/lib/browser/devices.js +23 -0
  54. package/dist/lib/browser/ipc.d.ts +1 -1
  55. package/dist/lib/browser/ipc.js +33 -7
  56. package/dist/lib/browser/profiles.d.ts +106 -1
  57. package/dist/lib/browser/profiles.js +300 -17
  58. package/dist/lib/browser/remote-control.d.ts +26 -2
  59. package/dist/lib/browser/remote-control.js +33 -3
  60. package/dist/lib/browser/runtime-state.d.ts +9 -0
  61. package/dist/lib/browser/runtime-state.js +24 -8
  62. package/dist/lib/browser/service.d.ts +37 -0
  63. package/dist/lib/browser/service.js +78 -20
  64. package/dist/lib/browser/sessions-list.d.ts +1 -3
  65. package/dist/lib/browser/sessions-list.js +3 -16
  66. package/dist/lib/browser/types.d.ts +12 -1
  67. package/dist/lib/cli-resources.js +17 -15
  68. package/dist/lib/codex-policy.d.ts +11 -1
  69. package/dist/lib/codex-policy.js +20 -5
  70. package/dist/lib/config-keys.d.ts +4 -3
  71. package/dist/lib/config-keys.js +17 -7
  72. package/dist/lib/daemon/runner.js +15 -2
  73. package/dist/lib/daemon-ticks.js +2 -2
  74. package/dist/lib/device-config.d.ts +25 -8
  75. package/dist/lib/device-config.js +90 -17
  76. package/dist/lib/devices/config-migration.d.ts +7 -0
  77. package/dist/lib/devices/config-migration.js +67 -3
  78. package/dist/lib/devices/harness-inventory.js +20 -3
  79. package/dist/lib/devices/health.d.ts +27 -2
  80. package/dist/lib/devices/health.js +39 -9
  81. package/dist/lib/devices/interactive-host.d.ts +15 -0
  82. package/dist/lib/devices/interactive-host.js +53 -0
  83. package/dist/lib/devices/registry.d.ts +61 -4
  84. package/dist/lib/devices/registry.js +94 -38
  85. package/dist/lib/devices/stats-cache.d.ts +6 -0
  86. package/dist/lib/devices/stats-cache.js +10 -2
  87. package/dist/lib/exec.d.ts +25 -3
  88. package/dist/lib/exec.js +75 -22
  89. package/dist/lib/feed/answer.d.ts +29 -0
  90. package/dist/lib/feed/answer.js +145 -0
  91. package/dist/lib/feed/attention.d.ts +83 -0
  92. package/dist/lib/feed/attention.js +0 -0
  93. package/dist/lib/feed/feed.d.ts +109 -0
  94. package/dist/lib/feed/feed.js +154 -3
  95. package/dist/lib/feed/pr-status.d.ts +14 -0
  96. package/dist/lib/feed/pr-status.js +47 -0
  97. package/dist/lib/feed/watch.d.ts +59 -0
  98. package/dist/lib/feed/watch.js +180 -0
  99. package/dist/lib/fleet/capture.js +6 -0
  100. package/dist/lib/fleet/remote-login.js +8 -3
  101. package/dist/lib/fleet/types.d.ts +17 -0
  102. package/dist/lib/harness/adapter.d.ts +11 -0
  103. package/dist/lib/harness/adapters/claude.js +38 -24
  104. package/dist/lib/harness/adapters/codex.js +2 -4
  105. package/dist/lib/hooks/cache.js +8 -0
  106. package/dist/lib/hooks/match.d.ts +3 -0
  107. package/dist/lib/hooks/match.js +25 -0
  108. package/dist/lib/hosts/passthrough.js +16 -0
  109. package/dist/lib/hosts/registry.js +7 -0
  110. package/dist/lib/identity/client.d.ts +53 -0
  111. package/dist/lib/identity/client.js +106 -0
  112. package/dist/lib/identity/index.d.ts +115 -0
  113. package/dist/lib/identity/index.js +82 -0
  114. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  115. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  116. package/dist/lib/menubar/install-menubar.d.ts +13 -0
  117. package/dist/lib/menubar/install-menubar.js +26 -2
  118. package/dist/lib/open-url.d.ts +60 -2
  119. package/dist/lib/open-url.js +167 -12
  120. package/dist/lib/probe.d.ts +8 -0
  121. package/dist/lib/probe.js +105 -0
  122. package/dist/lib/refresh-coordinator.d.ts +1 -1
  123. package/dist/lib/refresh-coordinator.js +1 -1
  124. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  125. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
  126. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  127. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  128. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
  129. package/dist/lib/session/active.d.ts +67 -0
  130. package/dist/lib/session/active.js +69 -1
  131. package/dist/lib/session/db.js +4 -2
  132. package/dist/lib/session/discover.d.ts +2 -0
  133. package/dist/lib/session/discover.js +12 -6
  134. package/dist/lib/session/prompt.d.ts +35 -0
  135. package/dist/lib/session/prompt.js +56 -0
  136. package/dist/lib/session/remote/watch.d.ts +1 -1
  137. package/dist/lib/session/remote/watch.js +6 -1
  138. package/dist/lib/session/render.js +7 -3
  139. package/dist/lib/session/session-cache.js +5 -1
  140. package/dist/lib/session/trajectory-compare.d.ts +47 -0
  141. package/dist/lib/session/trajectory-compare.js +116 -0
  142. package/dist/lib/session/trajectory-html.d.ts +15 -0
  143. package/dist/lib/session/trajectory-html.js +750 -0
  144. package/dist/lib/session/trajectory-lineage.d.ts +92 -0
  145. package/dist/lib/session/trajectory-lineage.js +195 -0
  146. package/dist/lib/session/trajectory-text.d.ts +37 -0
  147. package/dist/lib/session/trajectory-text.js +277 -0
  148. package/dist/lib/session/trajectory.d.ts +102 -0
  149. package/dist/lib/session/trajectory.js +335 -0
  150. package/dist/lib/startup/command-registry.d.ts +5 -2
  151. package/dist/lib/startup/command-registry.js +8 -5
  152. package/dist/lib/state.d.ts +1 -1
  153. package/dist/lib/state.js +4 -2
  154. package/dist/lib/teams/agents.js +9 -0
  155. package/dist/lib/terminal/inject.d.ts +19 -0
  156. package/dist/lib/terminal/inject.js +19 -9
  157. package/dist/lib/tmux/session.d.ts +46 -13
  158. package/dist/lib/tmux/session.js +173 -27
  159. package/dist/lib/types.d.ts +16 -0
  160. package/dist/lib/usage-backoff.d.ts +2 -1
  161. package/dist/lib/usage-backoff.js +60 -19
  162. package/dist/lib/usage-refresh.d.ts +17 -3
  163. package/dist/lib/usage-refresh.js +57 -11
  164. package/dist/lib/view-types.d.ts +2 -2
  165. package/dist/lib/watchdog/log.d.ts +1 -1
  166. package/dist/lib/watchdog/log.js +1 -1
  167. package/dist/lib/watchdog/runner.d.ts +36 -45
  168. package/dist/lib/watchdog/runner.js +150 -119
  169. package/dist/lib/watchdog/service.d.ts +0 -1
  170. package/dist/lib/watchdog/service.js +0 -1
  171. package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
  172. package/dist/lib/watchdog/watchdog-agent.js +59 -0
  173. package/dist/lib/watchdog/watchdog.d.ts +11 -2
  174. package/dist/lib/watchdog/watchdog.js +37 -104
  175. package/package.json +2 -2
  176. package/dist/commands/usage.d.ts +0 -27
  177. package/dist/commands/usage.js +0 -96
@@ -3,7 +3,7 @@ import { password } from '@inquirer/prompts';
3
3
  import { setHelpSections } from '../lib/help.js';
4
4
  import { readMeta, updateMeta } from '../lib/state.js';
5
5
  import { ALL_AGENT_IDS, getAccountInfo, resolveAgentName } from '../lib/agents.js';
6
- import { getVersionHomePath, listInstalledVersions } from '../lib/installations/versions.js';
6
+ import { getGlobalDefault, getVersionHomePath, listInstalledVersions } from '../lib/installations/versions.js';
7
7
  import { assertNativeAccountNameable, nativeAccountCapability, nativeIdentityKey } from '../lib/account-capabilities.js';
8
8
  import { collectRunCandidates } from '../lib/accounting/rotate.js';
9
9
  import { isInteractiveTerminal } from './utils.js';
@@ -16,7 +16,10 @@ import { runDevicesAccounts } from './ssh.js';
16
16
  import { discoverNativeAccounts } from '../lib/account-catalog.js';
17
17
  import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
18
18
  import { getAccountProvider, listAccountProviders } from '../lib/account-provider-registry.js';
19
- import { accountBindings, addAccount, addNativeAccount, bindAccount, findAccount, findUnifiedAccount, inspectAccount, listNativeAccounts, readAccountRegistry, removeAccount, renameAccount, setAccountSecret, unbindAccount } from '../lib/account-registry.js';
19
+ import { accountBindings, addAccount, addNativeAccount, bindAccount, findAccount, findUnifiedAccount, inspectAccount, labelNativeAccount, listNativeAccounts, readAccountRegistry, removeAccount, renameAccount, setAccountSecret, unbindAccount } from '../lib/account-registry.js';
20
+ function cleanCommandError(command, err) {
21
+ command.error(err instanceof Error ? err.message : String(err), { exitCode: 1, code: 'accounts.error' });
22
+ }
20
23
  function parseInstallation(raw) {
21
24
  const at = raw.lastIndexOf('@');
22
25
  if (at < 1 || at === raw.length - 1)
@@ -273,12 +276,43 @@ export function registerAccountsCommand(program) {
273
276
  });
274
277
  accounts.command('name <source> <name>')
275
278
  .description('Name a signed-in native installation without copying its OAuth credentials')
276
- .action(async (source, name) => {
277
- const identity = await nativeIdentityFromSource(source);
278
- const account = addNativeAccount(name, identity.agent, identity.identityKey, identity.identityLabel, identity.scope);
279
- console.log(chalk.green(`Named ${source} as ${account.name}.`));
280
- if (account.scope === 'device')
281
- console.log(chalk.gray(`${identity.agent} authentication is device-scoped; attach '${account.name}' to '${identity.agent}', not an individual version.`));
279
+ .action(async (source, name, _o, command) => {
280
+ try {
281
+ const identity = await nativeIdentityFromSource(source);
282
+ const account = addNativeAccount(name, identity.agent, identity.identityKey, identity.identityLabel, identity.scope);
283
+ console.log(chalk.green(`Named ${source} as ${account.name}.`));
284
+ if (account.scope === 'device')
285
+ console.log(chalk.gray(`${identity.agent} authentication is device-scoped; attach '${account.name}' to '${identity.agent}', not an individual version.`));
286
+ }
287
+ catch (err) {
288
+ cleanCommandError(command, err);
289
+ }
290
+ });
291
+ accounts.command('label <harness> [label]')
292
+ .description('Label a native login independently of its installed harness version')
293
+ .option('--account <email-or-id>', 'Native identity to label when the harness has multiple logins')
294
+ .action(async (harness, label, o, command) => {
295
+ try {
296
+ const agent = parseHarness(harness);
297
+ assertNativeAccountNameable(agent);
298
+ const candidates = (await collectRunCandidates(agent)).filter(candidate => candidate.signedIn);
299
+ const needle = o.account?.toLowerCase();
300
+ const selected = needle
301
+ ? candidates.find(candidate => candidate.email?.toLowerCase() === needle || candidate.accountKey?.toLowerCase() === needle)
302
+ : candidates.find(candidate => candidate.version === getGlobalDefault(agent)) ?? (candidates.length === 1 ? candidates[0] : undefined);
303
+ if (!selected)
304
+ throw new Error(needle
305
+ ? `Unknown ${agent} account '${o.account}'.`
306
+ : `Multiple ${agent} accounts are connected; pass --account <email|id>.`);
307
+ const identityKey = selected.accountKey ?? selected.email?.toLowerCase();
308
+ if (!identityKey)
309
+ throw new Error(`${agent} account '${selected.version}' has no stable identity.`);
310
+ const account = labelNativeAccount(agent, identityKey, selected.email ?? undefined, label, nativeAccountCapability(agent).scope);
311
+ console.log(chalk.green(`Labeled ${agent} account ${selected.email ?? identityKey} as '${account.name}'.`));
312
+ }
313
+ catch (err) {
314
+ cleanCommandError(command, err);
315
+ }
282
316
  });
283
317
  accounts.command('attach <account> <target>')
284
318
  .description('Attach a named account to a native installation or custom harness')
@@ -329,15 +363,25 @@ export function registerAccountsCommand(program) {
329
363
  accounts.command('remove <name>').description('Remove an account and its device-local credential').action((name) => removeAccount(name));
330
364
  accounts.command('set-default <agent> <name>')
331
365
  .description('Use this account for a harness when --account is omitted')
332
- .action((agentRaw, name) => {
333
- const { agent, account } = setDefaultAccount(agentRaw, name);
334
- console.log(chalk.green(`${agent} now uses account '${account.name}' unless --account overrides it.`));
366
+ .action((agentRaw, name, _o, command) => {
367
+ try {
368
+ const { agent, account } = setDefaultAccount(agentRaw, name);
369
+ console.log(chalk.green(`${agent} now uses account '${account.name}' unless --account overrides it.`));
370
+ }
371
+ catch (err) {
372
+ cleanCommandError(command, err);
373
+ }
335
374
  });
336
375
  const switchCmd = accounts.command('switch <harness> [account]')
337
376
  .description('Pick the default account for a harness')
338
377
  .option('--json', 'Machine-readable account list or the resulting default')
339
378
  .action(async (harness, account, o, command) => {
340
- await runAccountsSwitch(harness, account, { json: !!(o.json || command.optsWithGlobals().json) });
379
+ try {
380
+ await runAccountsSwitch(harness, account, { json: !!(o.json || command.optsWithGlobals().json) });
381
+ }
382
+ catch (err) {
383
+ cleanCommandError(command, err);
384
+ }
341
385
  });
342
386
  accounts.command('clear-default <agent>')
343
387
  .description('Return a harness to native login or balanced account selection')
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerAuthCommand(program: Command): void;
@@ -0,0 +1,229 @@
1
+ import chalk from 'chalk';
2
+ import { PhoenixApiError, clearSession, createSpace, createSpaceInvite, fetchWhoAmI, listSpaceMembers, listSpaces, pollDeviceToken, readSession, removeSpaceMember, resolveMemberFromList, resolveSpaceFromList, slugify, startDeviceAuthorization, updateSpaceMemberRole, writeSession, } from '../lib/identity/index.js';
3
+ import { setHelpSections } from '../lib/help.js';
4
+ import { runOrDie } from '../lib/format.js';
5
+ /**
6
+ * `agents auth` — sign in to Phoenix ID, the account layer behind team spaces.
7
+ * Signing in is optional: every local feature works with no account. Everything
8
+ * here goes through `lib/identity`; this file builds no URLs and reads no
9
+ * credential files of its own.
10
+ */
11
+ function sleep(ms) {
12
+ return new Promise((resolve) => setTimeout(resolve, ms));
13
+ }
14
+ async function login() {
15
+ const grant = await startDeviceAuthorization();
16
+ console.log('');
17
+ console.log(` Your code: ${chalk.bold.cyan(grant.user_code)}`);
18
+ console.log(` Open: ${chalk.underline(grant.verification_uri_complete)}`);
19
+ console.log('');
20
+ console.log(chalk.gray(' Waiting for you to approve it in the browser…'));
21
+ // The server sets the pace; `slow_down` widens it (RFC 8628 §3.5).
22
+ let interval = Math.max(1, grant.interval) * 1000;
23
+ const deadline = Date.now() + grant.expires_in * 1000;
24
+ while (Date.now() < deadline) {
25
+ await sleep(interval);
26
+ const poll = await pollDeviceToken(grant.device_code);
27
+ if (poll.status === 'authorized') {
28
+ writeSession({ access_token: poll.access_token, email: poll.user.email, userId: poll.user.id });
29
+ console.log(chalk.green(`\n Signed in as ${poll.user.email}.`));
30
+ return;
31
+ }
32
+ if (poll.status === 'slow_down') {
33
+ interval += 5000;
34
+ continue;
35
+ }
36
+ if (poll.status === 'denied')
37
+ throw new Error('Sign-in was denied in the browser.');
38
+ if (poll.status === 'expired')
39
+ throw new Error("That code expired. Run 'agents auth login' again.");
40
+ }
41
+ throw new Error("Timed out waiting for approval. Run 'agents auth login' again.");
42
+ }
43
+ async function whoami(json) {
44
+ const session = readSession();
45
+ if (!session) {
46
+ if (json) {
47
+ console.log(JSON.stringify({ signedIn: false }, null, 2));
48
+ return;
49
+ }
50
+ console.log(chalk.gray("Not signed in. Run 'agents auth login'."));
51
+ process.exitCode = 1;
52
+ return;
53
+ }
54
+ try {
55
+ const me = await fetchWhoAmI();
56
+ if (json) {
57
+ console.log(JSON.stringify({ signedIn: true, ...me }, null, 2));
58
+ return;
59
+ }
60
+ console.log(`${chalk.bold(me.email)} ${chalk.gray(me.userId)}`);
61
+ }
62
+ catch (err) {
63
+ if (err instanceof PhoenixApiError && err.status === 401) {
64
+ throw new Error("Your session is no longer valid. Run 'agents auth login' again.");
65
+ }
66
+ throw err;
67
+ }
68
+ }
69
+ function printSpaces(spaces) {
70
+ if (!spaces.length) {
71
+ console.log(chalk.gray(" No spaces yet. Create one with 'agents auth space create <name>'."));
72
+ return;
73
+ }
74
+ for (const space of spaces) {
75
+ console.log(` ${chalk.cyan(space.slug)} ${space.name} ${chalk.gray(space.user_role)}`);
76
+ }
77
+ }
78
+ /** Resolve a space reference (or the caller's only space) to a concrete space. */
79
+ async function requireSpace(ref) {
80
+ const spaces = await listSpaces();
81
+ const space = resolveSpaceFromList(spaces, ref);
82
+ if (space)
83
+ return space;
84
+ if (!ref) {
85
+ throw new Error(spaces.length
86
+ ? `You are in ${spaces.length} spaces — name one: ${spaces.map((s) => s.slug).join(', ')}.`
87
+ : "You are not in a space yet. Create one with 'agents auth space create <name>'.");
88
+ }
89
+ throw new Error(`No space named '${ref}'.`);
90
+ }
91
+ export function registerAuthCommand(program) {
92
+ const auth = program
93
+ .command('auth')
94
+ .description('Sign in to Phoenix ID — the account layer behind team spaces');
95
+ setHelpSections(auth, {
96
+ examples: `agents auth login # device-code sign-in via your browser
97
+ agents auth whoami # who this machine is signed in as
98
+ agents auth space create "Design Team" # start a space
99
+ agents auth space invite ada@example.com # add a teammate
100
+ agents auth logout # clear this machine only`,
101
+ notes: `Signing in is optional — every local feature works with no account; team spaces are what it unlocks.
102
+ Sign-in is Google-only and opens a Phoenix-branded page; the CLI never sees a password.
103
+ The session lives in this machine's agents state dir, so logging out here signs out nothing else.
104
+ Point at a different backend with PHOENIX_ID_BASE (defaults to the production service).`,
105
+ });
106
+ auth
107
+ .command('login')
108
+ .description('Sign in with the device-code flow')
109
+ .action(() => runOrDie(() => login()));
110
+ auth
111
+ .command('whoami')
112
+ .description('Show the signed-in account')
113
+ .option('--json', 'Machine-readable output')
114
+ .action((o, command) => {
115
+ const json = !!o.json || !!command.optsWithGlobals().json;
116
+ return runOrDie(() => whoami(json), { json });
117
+ });
118
+ auth
119
+ .command('logout')
120
+ .description("Clear this machine's session (no other device is affected)")
121
+ .action(() => runOrDie(() => {
122
+ const session = readSession();
123
+ clearSession();
124
+ console.log(session ? chalk.green(`Signed out ${session.email ?? 'this machine'}.`) : chalk.gray('Already signed out.'));
125
+ }));
126
+ const space = auth.command('space').description('Spaces — share work with teammates');
127
+ space
128
+ .command('list', { isDefault: true })
129
+ .description('Spaces you belong to')
130
+ .option('--json', 'Machine-readable output')
131
+ .action((o, command) => {
132
+ const json = !!o.json || !!command.optsWithGlobals().json;
133
+ return runOrDie(async () => {
134
+ const spaces = await listSpaces();
135
+ if (json)
136
+ return console.log(JSON.stringify(spaces, null, 2));
137
+ printSpaces(spaces);
138
+ }, { json });
139
+ });
140
+ space
141
+ .command('create <name>')
142
+ .description('Create a space')
143
+ .option('--slug <slug>', 'URL-safe name (defaults to a slug of <name>)')
144
+ .option('--json', 'Machine-readable output')
145
+ .action((name, o, command) => {
146
+ const json = !!o.json || !!command.optsWithGlobals().json;
147
+ return runOrDie(async () => {
148
+ const created = await createSpace({ name, slug: o.slug ?? slugify(name) });
149
+ if (json)
150
+ return console.log(JSON.stringify(created, null, 2));
151
+ console.log(chalk.green(`Created ${created.name} (${created.slug}).`));
152
+ }, { json });
153
+ });
154
+ space
155
+ .command('members [space]')
156
+ .description('Who is in a space')
157
+ .option('--json', 'Machine-readable output')
158
+ .action((ref, o, command) => {
159
+ const json = !!o.json || !!command.optsWithGlobals().json;
160
+ return runOrDie(async () => {
161
+ const target = await requireSpace(ref);
162
+ const members = await listSpaceMembers(target.id);
163
+ if (json)
164
+ return console.log(JSON.stringify(members, null, 2));
165
+ for (const m of members)
166
+ console.log(` ${m.email} ${chalk.gray(m.role)}`);
167
+ }, { json });
168
+ });
169
+ space
170
+ .command('invite <email>')
171
+ .description('Invite someone to a space')
172
+ .option('--space <space>', 'Which space (defaults to your only one)')
173
+ .option('--role <role>', 'admin or member', 'member')
174
+ .option('--json', 'Machine-readable output')
175
+ .action((email, o, command) => {
176
+ const json = !!o.json || !!command.optsWithGlobals().json;
177
+ return runOrDie(async () => {
178
+ if (o.role !== 'admin' && o.role !== 'member') {
179
+ throw new Error(`--role must be admin or member (got '${o.role}').`);
180
+ }
181
+ const target = await requireSpace(o.space);
182
+ const result = await createSpaceInvite(target.id, { email, role: o.role });
183
+ if (json)
184
+ return console.log(JSON.stringify(result, null, 2));
185
+ console.log(result.member_added
186
+ ? chalk.green(`Added ${email} to ${target.name} as ${o.role}.`)
187
+ : chalk.green(`Invited ${email} to ${target.name} as ${o.role}. Invite code: ${result.invite_code}`));
188
+ }, { json });
189
+ });
190
+ space
191
+ .command('role <email> <role>')
192
+ .description('Change a member\'s role (owner only for admin)')
193
+ .option('--space <space>', 'Which space (defaults to your only one)')
194
+ .option('--json', 'Machine-readable output')
195
+ .action((email, role, o, command) => {
196
+ const json = !!o.json || !!command.optsWithGlobals().json;
197
+ return runOrDie(async () => {
198
+ if (role !== 'admin' && role !== 'member') {
199
+ throw new Error(`role must be admin or member (got '${role}').`);
200
+ }
201
+ const target = await requireSpace(o.space);
202
+ const member = resolveMemberFromList(await listSpaceMembers(target.id), email);
203
+ if (!member)
204
+ throw new Error(`${email} is not in ${target.name}.`);
205
+ const updated = await updateSpaceMemberRole(target.id, member.user_id, role);
206
+ if (json)
207
+ return console.log(JSON.stringify(updated, null, 2));
208
+ console.log(chalk.green(`${email} is now ${role} in ${target.name}.`));
209
+ }, { json });
210
+ });
211
+ space
212
+ .command('remove <email>')
213
+ .description('Remove a member (or yourself) from a space')
214
+ .option('--space <space>', 'Which space (defaults to your only one)')
215
+ .option('--json', 'Machine-readable output')
216
+ .action((email, o, command) => {
217
+ const json = !!o.json || !!command.optsWithGlobals().json;
218
+ return runOrDie(async () => {
219
+ const target = await requireSpace(o.space);
220
+ const member = resolveMemberFromList(await listSpaceMembers(target.id), email);
221
+ if (!member)
222
+ throw new Error(`${email} is not in ${target.name}.`);
223
+ await removeSpaceMember(target.id, member.user_id);
224
+ if (json)
225
+ return console.log(JSON.stringify({ removed: true, email, space: target.slug }, null, 2));
226
+ console.log(chalk.green(`Removed ${email} from ${target.name}.`));
227
+ }, { json });
228
+ });
229
+ }
@@ -10,12 +10,13 @@
10
10
  * second renderer. Interactive routing and the browse loop live in
11
11
  * `sessions-picker-factory.ts` (shared with the computer twin).
12
12
  */
13
+ import { showFile } from '../lib/open-url.js';
13
14
  import chalk from 'chalk';
14
15
  import { itemPicker } from '../lib/picker.js';
15
16
  import { isPromptCancelled } from './utils.js';
16
17
  import { buildPreview } from './sessions-picker.js';
17
18
  import { createSessionsPickerCommand } from './sessions-picker-factory.js';
18
- import { runBrowserSessions, buildBrowserSessionRows, matchesBrowserSessionRow, openArtifact, formatBytes, } from '../lib/browser/sessions-list.js';
19
+ import { runBrowserSessions, buildBrowserSessionRows, matchesBrowserSessionRow, formatBytes, } from '../lib/browser/sessions-list.js';
19
20
  import { formatRelativeTime } from '../lib/session/relative-time.js';
20
21
  const KIND_LABEL = {
21
22
  screenshot: 'shots',
@@ -89,11 +90,12 @@ function formatArtifactLabel(a) {
89
90
  const age = formatRelativeTime(new Date(a.mtimeMs).toISOString());
90
91
  return `${age.padEnd(11)} ${a.name.padEnd(34)} ${formatBytes(a.bytes).padStart(8)}`;
91
92
  }
92
- /** Open one artifact in the OS default app, printing its path (matches the
93
- * non-interactive `--open` behavior) and any open failure. */
94
- function openAndReport(a) {
93
+ /** Open one artifact, printing its path (matches the non-interactive `--open`
94
+ * behavior) and any open failure. Routes through the same viewer seam as
95
+ * `--open`, so the interactive and non-interactive halves cannot diverge. */
96
+ async function openAndReport(a) {
95
97
  console.log(a.path);
96
- if (!openArtifact(a.path))
98
+ if ((await showFile(a.path)).via === 'none')
97
99
  console.error(`Could not open ${a.path}`);
98
100
  }
99
101
  /** Second-level picker over one row's captures, newest first. Enter opens the
@@ -136,12 +138,12 @@ const browserSessionsPicker = createSessionsPickerCommand({
136
138
  return;
137
139
  }
138
140
  if (row.artifacts.length === 1) {
139
- openAndReport(row.artifacts[0]);
141
+ await openAndReport(row.artifacts[0]);
140
142
  return;
141
143
  }
142
144
  const artifact = await pickArtifact(row);
143
145
  if (artifact)
144
- openAndReport(artifact);
146
+ await openAndReport(artifact);
145
147
  },
146
148
  });
147
149
  /** True when the interactive picker should open instead of the printed table: