@phnx-labs/agents-cli 1.20.64 → 1.20.65

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 (155) hide show
  1. package/CHANGELOG.md +39 -3
  2. package/README.md +37 -2
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.d.ts +12 -0
  5. package/dist/commands/apply.js +274 -0
  6. package/dist/commands/browser.js +2 -2
  7. package/dist/commands/cloud.js +32 -2
  8. package/dist/commands/doctor.js +4 -1
  9. package/dist/commands/exec.js +94 -49
  10. package/dist/commands/feed.js +25 -11
  11. package/dist/commands/hosts.js +44 -6
  12. package/dist/commands/mcp.js +55 -5
  13. package/dist/commands/monitors.d.ts +12 -0
  14. package/dist/commands/monitors.js +740 -0
  15. package/dist/commands/output.js +2 -2
  16. package/dist/commands/routines.js +23 -2
  17. package/dist/commands/secrets.d.ts +16 -0
  18. package/dist/commands/secrets.js +215 -64
  19. package/dist/commands/serve.js +31 -0
  20. package/dist/commands/sessions-export.js +8 -3
  21. package/dist/commands/sessions.d.ts +16 -0
  22. package/dist/commands/sessions.js +36 -6
  23. package/dist/commands/ssh.js +45 -2
  24. package/dist/commands/versions.js +7 -3
  25. package/dist/commands/view.d.ts +26 -0
  26. package/dist/commands/view.js +32 -9
  27. package/dist/commands/webhook.js +10 -2
  28. package/dist/index.js +34 -14
  29. package/dist/lib/agents.d.ts +18 -0
  30. package/dist/lib/agents.js +53 -3
  31. package/dist/lib/auto-dispatch-provider.js +7 -2
  32. package/dist/lib/auto-dispatch.d.ts +3 -0
  33. package/dist/lib/auto-dispatch.js +3 -0
  34. package/dist/lib/browser/chrome.js +2 -2
  35. package/dist/lib/cloud/antigravity.js +2 -2
  36. package/dist/lib/cloud/host.d.ts +59 -0
  37. package/dist/lib/cloud/host.js +224 -0
  38. package/dist/lib/cloud/registry.js +4 -0
  39. package/dist/lib/cloud/types.d.ts +6 -4
  40. package/dist/lib/computer-rpc.js +3 -1
  41. package/dist/lib/crabbox/cli.js +5 -1
  42. package/dist/lib/crabbox/runtimes.js +11 -2
  43. package/dist/lib/daemon.d.ts +20 -4
  44. package/dist/lib/daemon.js +62 -19
  45. package/dist/lib/devices/fleet.d.ts +3 -2
  46. package/dist/lib/devices/fleet.js +9 -0
  47. package/dist/lib/devices/registry.d.ts +15 -0
  48. package/dist/lib/devices/registry.js +9 -0
  49. package/dist/lib/exec.d.ts +19 -2
  50. package/dist/lib/exec.js +41 -13
  51. package/dist/lib/fleet/apply.d.ts +63 -0
  52. package/dist/lib/fleet/apply.js +214 -0
  53. package/dist/lib/fleet/auth-sync.d.ts +67 -0
  54. package/dist/lib/fleet/auth-sync.js +142 -0
  55. package/dist/lib/fleet/manifest.d.ts +29 -0
  56. package/dist/lib/fleet/manifest.js +127 -0
  57. package/dist/lib/fleet/types.d.ts +129 -0
  58. package/dist/lib/fleet/types.js +13 -0
  59. package/dist/lib/git.d.ts +27 -0
  60. package/dist/lib/git.js +34 -2
  61. package/dist/lib/hosts/dispatch.d.ts +29 -8
  62. package/dist/lib/hosts/dispatch.js +46 -18
  63. package/dist/lib/hosts/passthrough.js +2 -0
  64. package/dist/lib/hosts/providers/devices.d.ts +27 -0
  65. package/dist/lib/hosts/providers/devices.js +98 -0
  66. package/dist/lib/hosts/registry.d.ts +10 -16
  67. package/dist/lib/hosts/registry.js +17 -50
  68. package/dist/lib/hosts/remote-cmd.d.ts +23 -0
  69. package/dist/lib/hosts/remote-cmd.js +71 -0
  70. package/dist/lib/hosts/run-target.d.ts +84 -0
  71. package/dist/lib/hosts/run-target.js +99 -0
  72. package/dist/lib/hosts/types.d.ts +23 -5
  73. package/dist/lib/hosts/types.js +22 -4
  74. package/dist/lib/linear-autoclose.d.ts +30 -0
  75. package/dist/lib/linear-autoclose.js +22 -0
  76. package/dist/lib/mcp.d.ts +27 -1
  77. package/dist/lib/mcp.js +126 -12
  78. package/dist/lib/monitors/config.d.ts +161 -0
  79. package/dist/lib/monitors/config.js +372 -0
  80. package/dist/lib/monitors/dispatch.d.ts +28 -0
  81. package/dist/lib/monitors/dispatch.js +91 -0
  82. package/dist/lib/monitors/engine.d.ts +61 -0
  83. package/dist/lib/monitors/engine.js +201 -0
  84. package/dist/lib/monitors/sources/command.d.ts +11 -0
  85. package/dist/lib/monitors/sources/command.js +31 -0
  86. package/dist/lib/monitors/sources/device.d.ts +13 -0
  87. package/dist/lib/monitors/sources/device.js +35 -0
  88. package/dist/lib/monitors/sources/file.d.ts +14 -0
  89. package/dist/lib/monitors/sources/file.js +57 -0
  90. package/dist/lib/monitors/sources/http.d.ts +10 -0
  91. package/dist/lib/monitors/sources/http.js +34 -0
  92. package/dist/lib/monitors/sources/index.d.ts +14 -0
  93. package/dist/lib/monitors/sources/index.js +31 -0
  94. package/dist/lib/monitors/sources/poll.d.ts +9 -0
  95. package/dist/lib/monitors/sources/poll.js +9 -0
  96. package/dist/lib/monitors/sources/types.d.ts +18 -0
  97. package/dist/lib/monitors/sources/types.js +9 -0
  98. package/dist/lib/monitors/sources/webhook.d.ts +23 -0
  99. package/dist/lib/monitors/sources/webhook.js +47 -0
  100. package/dist/lib/monitors/sources/ws.d.ts +14 -0
  101. package/dist/lib/monitors/sources/ws.js +45 -0
  102. package/dist/lib/monitors/state.d.ts +69 -0
  103. package/dist/lib/monitors/state.js +144 -0
  104. package/dist/lib/platform/exec.d.ts +16 -0
  105. package/dist/lib/platform/exec.js +17 -0
  106. package/dist/lib/plugins.js +101 -2
  107. package/dist/lib/redact.d.ts +14 -1
  108. package/dist/lib/redact.js +47 -1
  109. package/dist/lib/remote-agents-json.js +7 -1
  110. package/dist/lib/rotate.d.ts +6 -3
  111. package/dist/lib/rotate.js +0 -1
  112. package/dist/lib/routines.d.ts +16 -0
  113. package/dist/lib/routines.js +19 -0
  114. package/dist/lib/runner.d.ts +1 -0
  115. package/dist/lib/runner.js +102 -9
  116. package/dist/lib/secrets/agent.d.ts +48 -10
  117. package/dist/lib/secrets/agent.js +123 -15
  118. package/dist/lib/secrets/bundles.d.ts +26 -0
  119. package/dist/lib/secrets/bundles.js +59 -8
  120. package/dist/lib/secrets/mcp.js +4 -2
  121. package/dist/lib/secrets/remote.d.ts +17 -0
  122. package/dist/lib/secrets/remote.js +40 -0
  123. package/dist/lib/self-update.d.ts +20 -0
  124. package/dist/lib/self-update.js +54 -1
  125. package/dist/lib/serve/control.d.ts +95 -0
  126. package/dist/lib/serve/control.js +260 -0
  127. package/dist/lib/serve/server.d.ts +35 -1
  128. package/dist/lib/serve/server.js +106 -76
  129. package/dist/lib/serve/stream.d.ts +43 -0
  130. package/dist/lib/serve/stream.js +116 -0
  131. package/dist/lib/serve/token.d.ts +35 -0
  132. package/dist/lib/serve/token.js +85 -0
  133. package/dist/lib/session/bundle.d.ts +14 -0
  134. package/dist/lib/session/bundle.js +12 -1
  135. package/dist/lib/session/remote-list.js +5 -1
  136. package/dist/lib/session/state.d.ts +7 -25
  137. package/dist/lib/session/state.js +16 -6
  138. package/dist/lib/session/sync/config.js +8 -2
  139. package/dist/lib/session/types.d.ts +30 -0
  140. package/dist/lib/ssh-tunnel.d.ts +19 -1
  141. package/dist/lib/ssh-tunnel.js +86 -7
  142. package/dist/lib/startup/command-registry.d.ts +2 -0
  143. package/dist/lib/startup/command-registry.js +4 -0
  144. package/dist/lib/state.d.ts +5 -0
  145. package/dist/lib/state.js +12 -0
  146. package/dist/lib/tmux/session.d.ts +7 -0
  147. package/dist/lib/tmux/session.js +3 -1
  148. package/dist/lib/triggers/webhook.d.ts +18 -0
  149. package/dist/lib/triggers/webhook.js +105 -0
  150. package/dist/lib/types.d.ts +26 -1
  151. package/dist/lib/usage.js +7 -5
  152. package/dist/lib/versions.js +14 -11
  153. package/dist/lib/workflows.d.ts +20 -0
  154. package/dist/lib/workflows.js +24 -0
  155. package/package.json +2 -1
@@ -16,7 +16,7 @@ import { truncate, padRight } from '../lib/format.js';
16
16
  import ora from 'ora';
17
17
  import { SESSION_AGENTS } from '../lib/session/types.js';
18
18
  import { discoverArtifacts, readArtifact, resolveArtifact } from '../lib/session/artifacts.js';
19
- import { looksLikePath, toComparablePath, homeDir, needsWindowsShell, findExecutable } from '../lib/platform/index.js';
19
+ import { looksLikePath, toComparablePath, homeDir, needsWindowsShell, findExecutable, composeWin32CommandLine } from '../lib/platform/index.js';
20
20
  import { getActiveSessions } from '../lib/session/active.js';
21
21
  import { enumerateGhosttyTabs, assignGhosttyTabs } from '../lib/session/ghostty-tabs.js';
22
22
  import { mapPanesToTargets, listClients } from '../lib/tmux/session.js';
@@ -25,6 +25,7 @@ import { machineId, normalizeHost } from '../lib/session/sync/config.js';
25
25
  import { gatherRemoteActive, NO_FANOUT_ENV } from '../lib/session/remote-active.js';
26
26
  import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
27
27
  import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/session/width.js';
28
+ import { inferSessionState } from '../lib/session/state.js';
28
29
  import { discoverSessions, countSessionsInScope, resolveSessionById, searchContentIndex, getSessionRoots } from '../lib/session/discover.js';
29
30
  import { filterTeamSessions } from '../lib/session/team-filter.js';
30
31
  import { parseSession } from '../lib/session/parse.js';
@@ -1323,9 +1324,9 @@ async function renderSession(session, mode, filters, options = {}) {
1323
1324
  return;
1324
1325
  }
1325
1326
  const spinner = ora(`Parsing ${session.agent} session...`).start();
1326
- let events = parseSession(session.filePath, session.agent);
1327
+ const parsedEvents = parseSession(session.filePath, session.agent);
1327
1328
  spinner.stop();
1328
- events = filterEvents(events, filters);
1329
+ let events = filterEvents(parsedEvents, filters);
1329
1330
  const agentColor = colorAgent(session.agent);
1330
1331
  console.log('');
1331
1332
  if (mode === 'summary') {
@@ -1367,7 +1368,11 @@ async function renderSession(session, mode, filters, options = {}) {
1367
1368
  // engine (plan text, PR, worktree, ticket). Pre-1.20.51 emitted a bare event
1368
1369
  // array; consumers that JSON.parse this now read `output.events` for the
1369
1370
  // array. See issue #743 (plan surfaced) and CHANGELOG for the shape change.
1370
- process.stdout.write(renderJson(events, session));
1371
+ // `todos` (RUSH-1503) is computed from the UNFILTERED transcript so the
1372
+ // checklist reflects true session state regardless of any `--include` filter;
1373
+ // it lets the Factory panel read the CLI's checklist instead of re-parsing.
1374
+ const todos = inferSessionState(parsedEvents, { cwd: session.cwd }).todos;
1375
+ process.stdout.write(renderJson(events, todos ? { ...session, todos } : session));
1371
1376
  }
1372
1377
  function renderTopicCell(label, topic, query, visibleWidth, paddedWidth) {
1373
1378
  const lbl = (label ?? '').trim();
@@ -1613,11 +1618,35 @@ export async function resumeSessionInPlace(session) {
1613
1618
  }
1614
1619
  await spawnResumeCommand(resume, cwd);
1615
1620
  }
1621
+ /**
1622
+ * Map a resume argv to the spawn(command, args, {shell}) triple.
1623
+ *
1624
+ * On Windows the agent launcher is a `.cmd`/PATHEXT shim and needs shell:true.
1625
+ * With shell:true, Node concatenates args into the cmd.exe line unescaped
1626
+ * (DEP0190 + injection). A session.id derived from a filename can carry
1627
+ * metacharacters (`&|<>`); compose a fully-quoted line and pass an EMPTY args
1628
+ * array so cmd.exe cannot reparse them (RUSH-1753). See composeWin32CommandLine.
1629
+ *
1630
+ * `platform` is injectable so the win32 shell path is unit-testable on any host.
1631
+ */
1632
+ export function resumeSpawnInvocation(cmd, platform = process.platform) {
1633
+ const shell = needsWindowsShell(cmd[0], platform);
1634
+ if (shell) {
1635
+ return {
1636
+ command: composeWin32CommandLine(cmd[0], cmd.slice(1)),
1637
+ args: [],
1638
+ shell: true,
1639
+ };
1640
+ }
1641
+ return { command: cmd[0], args: cmd.slice(1), shell: false };
1642
+ }
1616
1643
  /**
1617
1644
  * Spawn a resume command as a foreground takeover (inherited stdio), resolving
1618
1645
  * when it exits. On Windows the agent launcher is a `.cmd`/PATHEXT shim that
1619
1646
  * `spawn` can't exec directly — a bare-name `shell:false` spawn throws
1620
1647
  * `EFTYPE`/`ENOENT` there — so we go through the shell via `needsWindowsShell`.
1648
+ * When shell:true, argv is composed via resumeSpawnInvocation (quoted line +
1649
+ * empty args) so an untrusted session.id cannot inject through cmd.exe.
1621
1650
  * The spawn is guarded because such a failure can be thrown synchronously;
1622
1651
  * without the guard it would surface under an unrelated "Failed to discover
1623
1652
  * sessions" catch upstream instead of a truthful launch error.
@@ -1626,10 +1655,11 @@ function spawnResumeCommand(cmd, cwd) {
1626
1655
  return new Promise((resolve) => {
1627
1656
  let child;
1628
1657
  try {
1629
- child = spawn(cmd[0], cmd.slice(1), {
1658
+ const { command, args, shell } = resumeSpawnInvocation(cmd);
1659
+ child = spawn(command, args, {
1630
1660
  cwd,
1631
1661
  stdio: 'inherit',
1632
- shell: needsWindowsShell(cmd[0]),
1662
+ shell,
1633
1663
  });
1634
1664
  }
1635
1665
  catch (err) {
@@ -15,9 +15,11 @@ import * as os from 'os';
15
15
  import * as path from 'path';
16
16
  import chalk from 'chalk';
17
17
  import ora from 'ora';
18
- import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
18
+ import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../lib/secrets/bundles.js';
19
19
  import { machineId } from '../lib/session/sync/config.js';
20
20
  import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, } from '../lib/devices/registry.js';
21
+ import { addControlToken } from '../lib/serve/token.js';
22
+ import { DEFAULT_SERVE_PORT } from '../lib/serve/server.js';
21
23
  import { nodeToDeviceInput, parseTailscaleStatus, tailscaleStatusJson, } from '../lib/devices/tailscale.js';
22
24
  import { localLoginUser, planDeviceReconciliation, runDeviceSync, withDefaultUser } from '../lib/devices/sync.js';
23
25
  import { resolveDeviceTarget, splitUserHost } from '../lib/devices/resolve-target.js';
@@ -400,6 +402,47 @@ Typical workflow:
400
402
  process.exit(1);
401
403
  }
402
404
  });
405
+ devicesCmd
406
+ .command('pair-ios [name]')
407
+ .description('Pair an iPhone/iPad cockpit (RUSH-1733): mint a control token for `agents serve --control` and mark the device control-only. The token is shown ONCE — enter it in the app. Run this on the anchor.')
408
+ .option('--port <n>', 'Anchor control port to advertise to the app', String(DEFAULT_SERVE_PORT))
409
+ .action(async (name, opts) => {
410
+ const label = (name || 'iphone').trim();
411
+ // Mint the bearer token — hash-only on disk, raw shown once (see serve/token.ts).
412
+ const { id, token } = addControlToken(label);
413
+ // If a device with this name is already registered (e.g. discovered over
414
+ // Tailscale as an `unknown`-platform node), mark it control-only so the
415
+ // fleet stops trying to dial it for sessions/placement.
416
+ let marked = false;
417
+ let unknownName = false;
418
+ if (name) {
419
+ const existing = await getDevice(name);
420
+ if (existing) {
421
+ await upsertDevice(name, { role: 'control' });
422
+ marked = true;
423
+ }
424
+ else {
425
+ unknownName = true;
426
+ }
427
+ }
428
+ const port = parseInt(opts.port ?? '', 10) || DEFAULT_SERVE_PORT;
429
+ console.log(chalk.green(`Paired cockpit '${label}'`) + chalk.gray(` (token id ${id})`));
430
+ if (marked)
431
+ console.log(chalk.gray(`Marked device '${name}' role=control — the fleet won't dial it.`));
432
+ if (unknownName) {
433
+ console.log(chalk.yellow(`Note: no registered device named '${name}' — token minted, but no device was marked role=control.`) +
434
+ chalk.gray(` Run \`agents devices sync\` first if this phone should appear in the fleet.`));
435
+ }
436
+ console.log();
437
+ console.log(chalk.bold('Control token (shown once — enter it in the app):'));
438
+ console.log(' ' + chalk.cyan(token));
439
+ console.log();
440
+ console.log('On the anchor, expose the control server on your tailnet:');
441
+ console.log(chalk.gray(` agents serve --control --bind <anchor-tailnet-ip> --port ${port}`));
442
+ console.log('Then point the app at:');
443
+ console.log(chalk.gray(` http://<anchor-tailnet-ip>:${port} (Bearer: the token above)`));
444
+ console.log(chalk.yellow('Keep the control server on the tailnet — never public Funnel.'));
445
+ });
403
446
  devicesCmd
404
447
  .command('rm <name>')
405
448
  .alias('remove')
@@ -544,7 +587,7 @@ async function runAskpass() {
544
587
  process.exit(1);
545
588
  }
546
589
  try {
547
- const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh' });
590
+ const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh', agentOnly: isHeadlessSecretsContext() });
548
591
  const value = env[key];
549
592
  if (value === undefined) {
550
593
  console.error(`askpass: key '${key}' not found in bundle '${bundle}'`);
@@ -4,7 +4,7 @@ import ora from 'ora';
4
4
  import * as fs from 'fs';
5
5
  import * as path from 'path';
6
6
  import { select, confirm, checkbox } from '@inquirer/prompts';
7
- import { AGENTS, ALL_AGENT_IDS, getAccountEmail, getAccountInfo, agentLabel, warnAgentDeprecated, isSelfUpdatingAgent, } from '../lib/agents.js';
7
+ import { AGENTS, ALL_AGENT_IDS, accountOrgBadge, getAccountEmail, getAccountInfo, agentLabel, warnAgentDeprecated, isSelfUpdatingAgent, } from '../lib/agents.js';
8
8
  import { formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
9
9
  import { viewAction } from './view.js';
10
10
  import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
@@ -32,8 +32,12 @@ function fixSessionFilePaths(agent, version, oldVersionDir) {
32
32
  }
33
33
  function formatAccountHint(info, usage) {
34
34
  const parts = [];
35
- if (info.email)
36
- parts.push(info.email);
35
+ if (info.email) {
36
+ // Same-email accounts can live in different orgs (personal Max vs a Team
37
+ // seat) — the badge is what makes the picker disambiguate them.
38
+ const badge = accountOrgBadge(info);
39
+ parts.push(badge ? `${info.email} (${badge})` : info.email);
40
+ }
37
41
  const usageSummary = formatUsageSummary(info.plan, usage);
38
42
  if (usageSummary)
39
43
  parts.push(usageSummary);
@@ -7,8 +7,23 @@
7
7
  * rules, hooks, and promptcuts synced to that version.
8
8
  */
9
9
  import type { Command } from 'commander';
10
+ import type { AccountInfo } from '../lib/agents.js';
10
11
  import type { AgentId } from '../lib/types.js';
11
12
  import { type ProfileSummary } from '../lib/profiles.js';
13
+ /**
14
+ * Text for the account (email) column. Prefers the real email; otherwise, for a
15
+ * signed-in agent whose credential carries no email but does carry an opaque
16
+ * account id (Kimi's user_id), show `id:<user_id>` so distinct accounts read
17
+ * distinctly instead of a generic "signed in". Falls back to "signed in" when we
18
+ * have neither (Antigravity), and empty when signed out.
19
+ *
20
+ * When the account carries a Claude organizationType, the org badge is appended
21
+ * — "email (Turing Labs · Team)" — so two installs signed into the same email
22
+ * under different orgs (personal Max vs a Team seat) read distinctly. The
23
+ * suffix is plain text inside the label so the existing column-width pass
24
+ * measures and pads it for free.
25
+ */
26
+ export declare function accountColumnLabel(info?: Pick<AccountInfo, 'email' | 'accountId' | 'signedIn' | 'organizationType' | 'organizationName'> | null): string;
12
27
  type SyncState = 'synced' | 'new' | 'modified' | 'deleted';
13
28
  /** Per-section filter flags. When any are true, only those sections render. */
14
29
  export interface ViewSectionFilter {
@@ -32,6 +47,8 @@ export interface ViewJsonVersion {
32
47
  signedIn: boolean;
33
48
  email: string | null;
34
49
  accountId?: string | null;
50
+ organizationType?: string | null;
51
+ organizationName?: string | null;
35
52
  plan: string | null;
36
53
  usageStatus: 'available' | 'rate_limited' | 'out_of_credits' | null;
37
54
  overageCredits?: {
@@ -86,6 +103,15 @@ export declare function parseResourceSections(options: {
86
103
  resources?: string | boolean;
87
104
  detailed?: boolean;
88
105
  } & ViewSectionFilter, jsonMode: boolean): Set<ResourceSection>;
106
+ /**
107
+ * Identity key for duplicate-install detection. Prefers accountKey — which
108
+ * encodes account AND org — over the bare email: two installs can share an
109
+ * email yet belong to different orgs (a personal Max plan and a Team seat),
110
+ * and grouping those by email alone would propose pruning a live account.
111
+ * Falls back to the lowercased email for agents whose credentials expose no
112
+ * identity key. Null when there is no usable identity.
113
+ */
114
+ export declare function pruneGroupKey(info: Pick<AccountInfo, 'accountKey' | 'email'>): string | null;
89
115
  /**
90
116
  * Prune older installed versions that share an email with a newer installed
91
117
  * version. Keeps the highest semver per email, skips the global default (with
@@ -4,7 +4,7 @@ import { visibleWidth, termLink } from '../lib/format.js';
4
4
  import ora from 'ora';
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
- import { AGENTS, ALL_AGENT_IDS, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
7
+ import { AGENTS, ALL_AGENT_IDS, accountOrgBadge, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
8
8
  import { deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
9
9
  import { readManifest } from '../lib/manifest.js';
10
10
  import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, } from '../lib/versions.js';
@@ -34,12 +34,20 @@ const SIGNED_IN_LABEL = 'signed in';
34
34
  * account id (Kimi's user_id), show `id:<user_id>` so distinct accounts read
35
35
  * distinctly instead of a generic "signed in". Falls back to "signed in" when we
36
36
  * have neither (Antigravity), and empty when signed out.
37
+ *
38
+ * When the account carries a Claude organizationType, the org badge is appended
39
+ * — "email (Turing Labs · Team)" — so two installs signed into the same email
40
+ * under different orgs (personal Max vs a Team seat) read distinctly. The
41
+ * suffix is plain text inside the label so the existing column-width pass
42
+ * measures and pads it for free.
37
43
  */
38
- function accountColumnLabel(info) {
44
+ export function accountColumnLabel(info) {
39
45
  if (!info)
40
46
  return '';
41
- if (info.email)
42
- return info.email;
47
+ if (info.email) {
48
+ const badge = accountOrgBadge(info);
49
+ return badge ? `${info.email} (${badge})` : info.email;
50
+ }
43
51
  if (info.signedIn)
44
52
  return info.accountId ? `id:${info.accountId}` : SIGNED_IN_LABEL;
45
53
  return '';
@@ -1149,6 +1157,8 @@ async function collectAgentsJson(filterAgentId, resourceSections) {
1149
1157
  signedIn: info.signedIn,
1150
1158
  email: info.email,
1151
1159
  accountId: info.accountId,
1160
+ organizationType: info.organizationType ?? null,
1161
+ organizationName: info.organizationName ?? null,
1152
1162
  plan: info.plan,
1153
1163
  usageStatus: info.usageStatus,
1154
1164
  overageCredits: info.overageCredits,
@@ -1184,6 +1194,17 @@ async function collectAgentsJson(filterAgentId, resourceSections) {
1184
1194
  }
1185
1195
  return out;
1186
1196
  }
1197
+ /**
1198
+ * Identity key for duplicate-install detection. Prefers accountKey — which
1199
+ * encodes account AND org — over the bare email: two installs can share an
1200
+ * email yet belong to different orgs (a personal Max plan and a Team seat),
1201
+ * and grouping those by email alone would propose pruning a live account.
1202
+ * Falls back to the lowercased email for agents whose credentials expose no
1203
+ * identity key. Null when there is no usable identity.
1204
+ */
1205
+ export function pruneGroupKey(info) {
1206
+ return info.accountKey ?? info.email?.toLowerCase() ?? null;
1207
+ }
1187
1208
  async function buildAgentPrunePlan(agentId) {
1188
1209
  const dirInfos = listInstalledVersionDirs(agentId);
1189
1210
  const entries = await Promise.all(dirInfos.map(async ({ version, hasBinary }) => {
@@ -1198,16 +1219,18 @@ async function buildAgentPrunePlan(agentId) {
1198
1219
  // working binary — those are the things that compete for "the live install
1199
1220
  // for this account."
1200
1221
  const installed = entries.filter((e) => e.hasBinary);
1201
- const byEmail = new Map();
1222
+ const byAccount = new Map();
1202
1223
  for (const e of installed) {
1203
1224
  if (!e.info.email)
1204
1225
  continue;
1205
- const key = e.info.email.toLowerCase();
1206
- const list = byEmail.get(key) ?? [];
1226
+ const key = pruneGroupKey(e.info);
1227
+ if (!key)
1228
+ continue;
1229
+ const list = byAccount.get(key) ?? [];
1207
1230
  list.push(e);
1208
- byEmail.set(key, list);
1231
+ byAccount.set(key, list);
1209
1232
  }
1210
- for (const [, group] of byEmail) {
1233
+ for (const [, group] of byAccount) {
1211
1234
  if (group.length < 2)
1212
1235
  continue;
1213
1236
  const sorted = [...group].sort((a, b) => compareVersions(b.version, a.version));
@@ -1,6 +1,8 @@
1
+ import * as path from 'path';
1
2
  import chalk from 'chalk';
2
- import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
3
- import { startWebhookServer } from '../lib/triggers/webhook.js';
3
+ import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../lib/secrets/bundles.js';
4
+ import { createFileDeliveryStore, startWebhookServer } from '../lib/triggers/webhook.js';
5
+ import { getRuntimeStateDir } from '../lib/state.js';
4
6
  const DEFAULT_HOST = '127.0.0.1';
5
7
  const DEFAULT_PORT = 8787;
6
8
  function positiveInt(value, fallback) {
@@ -10,6 +12,9 @@ function positiveInt(value, fallback) {
10
12
  function readWebhookSecrets(bundleName) {
11
13
  const { env } = readAndResolveBundleEnv(bundleName, {
12
14
  caller: 'webhook serve',
15
+ // `webhook serve` is a long-running background server; when started detached
16
+ // (no TTY) it must resolve broker-only rather than pop an unanswerable prompt.
17
+ agentOnly: isHeadlessSecretsContext(),
13
18
  });
14
19
  const secrets = {};
15
20
  if (env.GITHUB_WEBHOOK_SECRET)
@@ -69,6 +74,9 @@ export function registerWebhookCommand(program) {
69
74
  port,
70
75
  secrets,
71
76
  rateLimitPerMinute: rateLimit,
77
+ // Durable delivery dedup: replays survive a receiver restart (an
78
+ // in-memory store would forget every seen delivery on restart).
79
+ deliveryStore: createFileDeliveryStore(path.join(getRuntimeStateDir(), 'webhook', 'deliveries.json')),
72
80
  onDelivery: (webhook, fired) => {
73
81
  console.log(`${new Date().toISOString()} ${webhook.source}:${webhook.event} ` +
74
82
  `${fired.length ? `fired ${fired.map((f) => f.jobName).join(', ')}` : 'no match'}`);
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
26
  const packageJsonPath = path.join(__dirname, '..', 'package.json');
27
27
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
28
28
  const VERSION = packageJson.version;
29
- import { NPM_PACKAGE_NAME, deriveGlobalPrefix, detectPackageManager, installPackageIntoPrefix, installPackageWithBun, verifyInstalledVersion, refreshAliasShims, } from './lib/self-update.js';
29
+ import { NPM_PACKAGE_NAME, deriveGlobalPrefix, detectPackageManager, installPackageIntoPrefix, installPackageWithBun, verifyInstalledVersion, refreshAliasShims, downloadVerifiedTarball, } from './lib/self-update.js';
30
30
  // Detect dev/working-tree builds and default the noisy startup steps off.
31
31
  // Three cases trip this:
32
32
  // 1. Dev install (scripts/install.sh) — package.json version stamped 0.0.0-dev.<sha>
@@ -50,7 +50,7 @@ if (IS_DEV_BUILD) {
50
50
  // module on each invocation (which loaded the whole ~50-module tree before the
51
51
  // first byte of output), the registry maps a command name to a thunk that
52
52
  // imports only what that command needs. See src/lib/startup/command-registry.ts.
53
- import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
53
+ import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadMonitors, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
54
54
  import { applyGlobalHelpConventions } from './lib/help.js';
55
55
  import { renderWhatsNew } from './lib/whats-new.js';
56
56
  import { emit, redactArgs } from './lib/events.js';
@@ -315,10 +315,12 @@ async function fetchNpmPackageMetadata(versionOrTag = 'latest', timeoutMs = 5000
315
315
  throw new Error('Could not reach npm registry');
316
316
  }
317
317
  const data = await response.json();
318
- if (typeof data.version !== 'string' || typeof data.dist?.integrity !== 'string') {
319
- throw new Error('npm registry response did not include version and integrity');
318
+ if (typeof data.version !== 'string' ||
319
+ typeof data.dist?.integrity !== 'string' ||
320
+ typeof data.dist?.tarball !== 'string') {
321
+ throw new Error('npm registry response did not include version, integrity, and tarball');
320
322
  }
321
- return { version: data.version, integrity: data.dist.integrity };
323
+ return { version: data.version, integrity: data.dist.integrity, tarball: data.dist.tarball };
322
324
  }
323
325
  function printResolvedPackage(metadata) {
324
326
  console.log(chalk.gray(`Resolved: ${NPM_PACKAGE_NAME}@${metadata.version}`));
@@ -326,16 +328,32 @@ function printResolvedPackage(metadata) {
326
328
  }
327
329
  async function installResolvedPackage(metadata) {
328
330
  const packageRoot = path.resolve(__dirname, '..');
329
- const spec = `${NPM_PACKAGE_NAME}@${metadata.version}`;
330
- // Upgrade with the package manager that owns this install. A bun global
331
- // install lives at <bunGlobalDir>/node_modules/... (no `lib` segment), so an
332
- // `npm install --prefix` would write to <bunGlobalDir>/lib/node_modules and
333
- // never touch the running copy npm exits 0, the verify below fails.
334
- if (detectPackageManager(packageRoot) === 'bun') {
335
- await installPackageWithBun(spec);
331
+ // Download the published tarball and prove its bytes match the registry
332
+ // integrity BEFORE installing anything. A `name@version` spec would let the
333
+ // package manager fetch and install whatever the registry serves with no
334
+ // hash check on our side; instead we verify here and install the LOCAL, now
335
+ // trusted .tgz. A mismatch throws and nothing below runs fail closed.
336
+ const tarball = await downloadVerifiedTarball(metadata.tarball, metadata.integrity);
337
+ try {
338
+ // Upgrade with the package manager that owns this install. A bun global
339
+ // install lives at <bunGlobalDir>/node_modules/... (no `lib` segment), so an
340
+ // `npm install --prefix` would write to <bunGlobalDir>/lib/node_modules and
341
+ // never touch the running copy — npm exits 0, the verify below fails.
342
+ if (detectPackageManager(packageRoot) === 'bun') {
343
+ await installPackageWithBun(tarball);
344
+ }
345
+ else {
346
+ await installPackageIntoPrefix(tarball, deriveGlobalPrefix(packageRoot));
347
+ }
336
348
  }
337
- else {
338
- await installPackageIntoPrefix(spec, deriveGlobalPrefix(packageRoot));
349
+ finally {
350
+ // Best-effort cleanup of the verified tarball and its temp dir.
351
+ try {
352
+ fs.rmSync(path.dirname(tarball), { recursive: true, force: true });
353
+ }
354
+ catch {
355
+ /* leave it for the OS temp sweep */
356
+ }
339
357
  }
340
358
  verifyInstalledVersion(packageRoot, metadata.version);
341
359
  refreshAliasShims(packageRoot);
@@ -669,6 +687,7 @@ async function registerAllEagerCommands() {
669
687
  await reg(loadPackages);
670
688
  await reg(loadDaemon);
671
689
  await reg(loadRoutines);
690
+ await reg(loadMonitors);
672
691
  await reg(loadRun);
673
692
  await reg(loadDefaults);
674
693
  await reg(loadModels);
@@ -676,6 +695,7 @@ async function registerAllEagerCommands() {
676
695
  await reg(loadTrash);
677
696
  await reg(loadRestore);
678
697
  await reg(loadDoctor);
698
+ await reg(loadApply);
679
699
  await reg(loadCheck);
680
700
  await reg(loadStatus);
681
701
  registerExecAliasCommand(program);
@@ -127,7 +127,25 @@ export interface AccountInfo {
127
127
  } | null;
128
128
  lastActive: Date | null;
129
129
  signedIn: boolean;
130
+ organizationType?: string | null;
131
+ organizationName?: string | null;
130
132
  }
133
+ /**
134
+ * Human-readable label for a Claude account's organizationType as read from
135
+ * .claude.json's oauthAccount ("claude_team" -> "Team"). Unrecognized values
136
+ * (future tiers) are rendered by stripping the "claude_" prefix and
137
+ * title-casing the rest — an unfamiliar-but-visible label beats silence.
138
+ * Returns null for missing input.
139
+ */
140
+ export declare function formatClaudeOrgLabel(orgType: string | null | undefined): string | null;
141
+ /**
142
+ * Short badge identifying which org an account belongs to: "Turing Labs · Team"
143
+ * for multi-seat orgs, just the tier label ("Max") for personal plans — a
144
+ * personal org's name is auto-generated boilerplate ("<email>'s Organization"),
145
+ * not identity. Returns null when the account carries no organizationType
146
+ * (signed out, non-Claude agents, configs predating the field).
147
+ */
148
+ export declare function accountOrgBadge(info?: Pick<AccountInfo, 'organizationType' | 'organizationName'> | null): string | null;
131
149
  /** Return the email address associated with the agent's auth config, or null. */
132
150
  export declare function getAccountEmail(agentId: AgentId, home?: string): Promise<string | null>;
133
151
  /** Decrypted contents of Droid's auth.v2.file (subset we consume). */
@@ -17,7 +17,7 @@ import * as os from 'os';
17
17
  import * as TOML from 'smol-toml';
18
18
  import * as yaml from 'yaml';
19
19
  import chalk from 'chalk';
20
- import { needsWindowsShell } from './platform/index.js';
20
+ import { execFileShellSpec } from './platform/index.js';
21
21
  import { latestFileMtimeMs } from './fs-walk.js';
22
22
  import { damerauLevenshtein } from './fuzzy.js';
23
23
  import { getCacheDir, getVersionsDir, getShimsDir, getCliVersionCachePath } from './state.js';
@@ -943,6 +943,48 @@ export function ensureSkillsDir(agentId) {
943
943
  export function agentConfigDirName(agentId) {
944
944
  return path.relative(HOME, AGENTS[agentId].configDir);
945
945
  }
946
+ /**
947
+ * Human-readable label for a Claude account's organizationType as read from
948
+ * .claude.json's oauthAccount ("claude_team" -> "Team"). Unrecognized values
949
+ * (future tiers) are rendered by stripping the "claude_" prefix and
950
+ * title-casing the rest — an unfamiliar-but-visible label beats silence.
951
+ * Returns null for missing input.
952
+ */
953
+ export function formatClaudeOrgLabel(orgType) {
954
+ if (!orgType)
955
+ return null;
956
+ const known = {
957
+ claude_max: 'Max',
958
+ claude_pro: 'Pro',
959
+ claude_team: 'Team',
960
+ claude_enterprise: 'Enterprise',
961
+ claude_free: 'Free',
962
+ };
963
+ if (known[orgType])
964
+ return known[orgType];
965
+ return orgType
966
+ .replace(/^claude_/, '')
967
+ .split('_')
968
+ .filter(Boolean)
969
+ .map((w) => w[0].toUpperCase() + w.slice(1))
970
+ .join(' ');
971
+ }
972
+ /**
973
+ * Short badge identifying which org an account belongs to: "Turing Labs · Team"
974
+ * for multi-seat orgs, just the tier label ("Max") for personal plans — a
975
+ * personal org's name is auto-generated boilerplate ("<email>'s Organization"),
976
+ * not identity. Returns null when the account carries no organizationType
977
+ * (signed out, non-Claude agents, configs predating the field).
978
+ */
979
+ export function accountOrgBadge(info) {
980
+ const label = formatClaudeOrgLabel(info?.organizationType);
981
+ if (!label)
982
+ return null;
983
+ const isMultiSeat = info?.organizationType === 'claude_team' || info?.organizationType === 'claude_enterprise';
984
+ if (isMultiSeat && info?.organizationName)
985
+ return `${info.organizationName} · ${label}`;
986
+ return label;
987
+ }
946
988
  /** Return the email address associated with the agent's auth config, or null. */
947
989
  export async function getAccountEmail(agentId, home) {
948
990
  const info = await getAccountInfo(agentId, home);
@@ -1248,6 +1290,8 @@ export async function getAccountInfo(agentId, home) {
1248
1290
  overageCredits,
1249
1291
  lastActive,
1250
1292
  signedIn: !!email,
1293
+ organizationType: oa?.organizationType ?? null,
1294
+ organizationName: oa?.organizationName ?? null,
1251
1295
  };
1252
1296
  }
1253
1297
  case 'codex': {
@@ -1659,7 +1703,10 @@ export async function registerMcp(agentId, name, command, scope = 'user', transp
1659
1703
  // On Windows a bare command name / `.cmd` wrapper (the npm-installed agent
1660
1704
  // CLI) can't be exec'd directly — it needs shell:true for PATHEXT/cmd. Off
1661
1705
  // Windows this is always false, so the no-shell argv path is unchanged.
1662
- await execFileAsync(bin, args, { ...(env ? { env } : {}), shell: needsWindowsShell(bin) });
1706
+ // RUSH-1752: when shell is needed, compose a fully-quoted command line and
1707
+ // pass EMPTY argv so user-controlled MCP command/args never reach cmd.exe unescaped.
1708
+ const spec = execFileShellSpec(bin, args);
1709
+ await execFileAsync(spec.command, spec.args, { ...(env ? { env } : {}), shell: spec.shell });
1663
1710
  return { success: true };
1664
1711
  }
1665
1712
  catch (err) {
@@ -1687,7 +1734,10 @@ export async function unregisterMcp(agentId, name, options) {
1687
1734
  try {
1688
1735
  const bin = options?.binary || agent.cliCommand;
1689
1736
  const env = options?.home ? { ...process.env, HOME: options.home } : undefined;
1690
- await execFileAsync(bin, ['mcp', 'remove', name], { ...(env ? { env } : {}), shell: needsWindowsShell(bin) });
1737
+ // RUSH-1752: same shell-safe path as registerMcp attacker-controlled MCP
1738
+ // `name` must not reach cmd.exe unescaped when shell:true is required.
1739
+ const spec = execFileShellSpec(bin, ['mcp', 'remove', name]);
1740
+ await execFileAsync(spec.command, spec.args, { ...(env ? { env } : {}), shell: spec.shell });
1691
1741
  return { success: true };
1692
1742
  }
1693
1743
  catch (err) {
@@ -9,7 +9,7 @@
9
9
  import { resolveProvider } from './cloud/registry.js';
10
10
  export function createProviderDispatcher() {
11
11
  return {
12
- async dispatch({ agent, prompt, repo, provider }) {
12
+ async dispatch({ agent, prompt, repo, provider, host }) {
13
13
  const p = resolveProvider(provider, agent);
14
14
  const caps = p.capabilities();
15
15
  if (!caps.available) {
@@ -18,7 +18,12 @@ export function createProviderDispatcher() {
18
18
  if (!caps.dispatch) {
19
19
  throw new Error(`cloud provider '${p.id}' does not support dispatch`);
20
20
  }
21
- const task = await p.dispatch({ agent, prompt, repo });
21
+ // The host provider clones nothing a repo target is meaningless there
22
+ // and its dispatch() rejects it; a ticket project pinned to 'host' relies
23
+ // on the machine's own checkout (the project's `host` names the machine).
24
+ const task = p.id === 'host'
25
+ ? await p.dispatch({ agent, prompt, providerOptions: { host } })
26
+ : await p.dispatch({ agent, prompt, repo });
22
27
  return { id: task.id };
23
28
  },
24
29
  };
@@ -22,6 +22,7 @@ export interface AutoDispatchProject {
22
22
  autoDispatch?: boolean;
23
23
  maxAgents?: number;
24
24
  provider?: string;
25
+ host?: string;
25
26
  }
26
27
  /** A Linear issue that is a candidate for dispatch. */
27
28
  export interface DelegatedIssue {
@@ -36,6 +37,7 @@ export interface PlannedDispatch {
36
37
  projectId: string;
37
38
  repoSlug?: string;
38
39
  provider?: string;
40
+ host?: string;
39
41
  issueId: string;
40
42
  identifier: string;
41
43
  title: string;
@@ -73,6 +75,7 @@ export interface Dispatcher {
73
75
  prompt: string;
74
76
  repo?: string;
75
77
  provider?: string;
78
+ host?: string;
76
79
  }): Promise<{
77
80
  id: string;
78
81
  }>;
@@ -46,6 +46,7 @@ export function readAutoDispatchProjects() {
46
46
  autoDispatch: o.autoDispatch === true,
47
47
  maxAgents: typeof o.maxAgents === 'number' ? o.maxAgents : undefined,
48
48
  provider: typeof o.provider === 'string' ? o.provider : undefined,
49
+ host: typeof o.host === 'string' ? o.host : undefined,
49
50
  });
50
51
  }
51
52
  return out;
@@ -76,6 +77,7 @@ export function planAutoDispatch(projects, inFlightByProject, delegatedTodoByPro
76
77
  projectId: p.id,
77
78
  repoSlug: p.repoSlug,
78
79
  provider: p.provider,
80
+ host: p.host,
79
81
  issueId: issue.id,
80
82
  identifier: issue.identifier,
81
83
  title: issue.title,
@@ -122,6 +124,7 @@ export async function autoDispatchTick(deps) {
122
124
  prompt: dispatchPrompt(d.identifier, d.title),
123
125
  repo: d.repoSlug,
124
126
  provider: d.provider,
127
+ host: d.host,
125
128
  });
126
129
  // Bookkeeping only — dispatch already happened; moving to Doing keeps the
127
130
  // ticket out of the next Todo poll. A failure here is non-fatal.