@phnx-labs/agents-cli 1.22.114 → 1.22.115

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 (176) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +22 -102
  3. package/dist/browser.js +0 -0
  4. package/dist/cli/command-registry.js +0 -7
  5. package/dist/commands/accounts.d.ts +3 -77
  6. package/dist/commands/accounts.js +13 -366
  7. package/dist/commands/audit.js +1 -1
  8. package/dist/commands/auth.js +0 -2
  9. package/dist/commands/browser.d.ts +53 -0
  10. package/dist/commands/browser.js +199 -9
  11. package/dist/commands/doctor.d.ts +2 -3
  12. package/dist/commands/doctor.js +0 -11
  13. package/dist/commands/events.d.ts +1 -3
  14. package/dist/commands/events.js +4 -7
  15. package/dist/commands/exec.d.ts +0 -1
  16. package/dist/commands/exec.js +2 -7
  17. package/dist/commands/feed-watch.js +52 -8
  18. package/dist/commands/focus.js +1 -1
  19. package/dist/commands/go.d.ts +0 -17
  20. package/dist/commands/go.js +2 -19
  21. package/dist/commands/logs.js +1 -1
  22. package/dist/commands/mcp.js +8 -83
  23. package/dist/commands/memory.js +4 -47
  24. package/dist/commands/message.js +4 -4
  25. package/dist/commands/plugins.js +1 -93
  26. package/dist/commands/repo.js +0 -44
  27. package/dist/commands/resume.js +4 -1
  28. package/dist/commands/secrets-passthrough.js +2 -2
  29. package/dist/commands/send.d.ts +4 -4
  30. package/dist/commands/send.js +6 -51
  31. package/dist/commands/sessions-backup-setup.js +1 -1
  32. package/dist/commands/sessions-resume.js +0 -1
  33. package/dist/commands/sessions-share.d.ts +5 -7
  34. package/dist/commands/sessions-share.js +98 -49
  35. package/dist/commands/sessions.js +1 -8
  36. package/dist/commands/setup-browser.js +18 -2
  37. package/dist/commands/setup-computer.js +20 -6
  38. package/dist/commands/setup-secrets.js +24 -4
  39. package/dist/commands/setup-terminal.d.ts +3 -0
  40. package/dist/commands/setup-terminal.js +22 -0
  41. package/dist/commands/setup.d.ts +1 -1
  42. package/dist/commands/setup.js +20 -10
  43. package/dist/commands/skills.js +0 -8
  44. package/dist/commands/ssh.d.ts +6 -0
  45. package/dist/commands/ssh.js +92 -233
  46. package/dist/commands/sync.js +14 -5
  47. package/dist/commands/teams.js +1 -1
  48. package/dist/commands/traces.js +1 -1
  49. package/dist/lib/accounts/add.d.ts +0 -5
  50. package/dist/lib/accounts/add.js +1 -7
  51. package/dist/lib/artifacts-client.d.ts +20 -0
  52. package/dist/lib/artifacts-client.js +46 -0
  53. package/dist/lib/auth-mint.js +2 -2
  54. package/dist/lib/browser/runtime-state.d.ts +55 -0
  55. package/dist/lib/browser/runtime-state.js +99 -18
  56. package/dist/lib/browser/service.js +21 -1
  57. package/dist/lib/cli-resources.js +3 -1
  58. package/dist/lib/cloud/dispatch.js +1 -1
  59. package/dist/lib/cloudflare/creds.d.ts +10 -0
  60. package/dist/lib/cloudflare/creds.js +46 -0
  61. package/dist/lib/cloudflare/provision.d.ts +35 -0
  62. package/dist/lib/cloudflare/provision.js +144 -0
  63. package/dist/lib/computer/sessions-list.d.ts +55 -0
  64. package/dist/lib/computer/sessions-list.js +168 -1
  65. package/dist/lib/daemon/daemon.js +8 -1
  66. package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
  67. package/dist/lib/daemon/feed-stream-service.js +40 -0
  68. package/dist/lib/daemon-services.d.ts +1 -1
  69. package/dist/lib/daemon-services.js +5 -0
  70. package/dist/lib/devices/connect.d.ts +49 -5
  71. package/dist/lib/devices/connect.js +169 -21
  72. package/dist/lib/feed/envelope.d.ts +79 -0
  73. package/dist/lib/feed/envelope.js +23 -0
  74. package/dist/lib/feed/events.d.ts +6 -0
  75. package/dist/lib/feed/events.js +8 -0
  76. package/dist/lib/feed/hub-server.d.ts +86 -0
  77. package/dist/lib/feed/hub-server.js +334 -0
  78. package/dist/lib/feed/hub.d.ts +95 -0
  79. package/dist/lib/feed/hub.js +255 -0
  80. package/dist/lib/feed/tool-activity.d.ts +108 -0
  81. package/dist/lib/feed/tool-activity.js +313 -0
  82. package/dist/lib/feed/tools.d.ts +198 -0
  83. package/dist/lib/feed/tools.js +265 -0
  84. package/dist/lib/feed/watch.d.ts +50 -50
  85. package/dist/lib/feed/watch.js +147 -16
  86. package/dist/lib/format.d.ts +1 -1
  87. package/dist/lib/format.js +1 -1
  88. package/dist/lib/git.d.ts +0 -16
  89. package/dist/lib/git.js +0 -58
  90. package/dist/lib/helper-versions.js +1 -1
  91. package/dist/lib/hosts/remote-cmd.d.ts +51 -1
  92. package/dist/lib/hosts/remote-cmd.js +125 -8
  93. package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
  94. package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
  95. package/dist/lib/mcp.js +17 -11
  96. package/dist/lib/probe.d.ts +4 -1
  97. package/dist/lib/probe.js +5 -2
  98. package/dist/lib/pwsh.d.ts +33 -0
  99. package/dist/lib/pwsh.js +56 -0
  100. package/dist/lib/redact.d.ts +8 -0
  101. package/dist/lib/redact.js +11 -0
  102. package/dist/lib/refresh.d.ts +6 -2
  103. package/dist/lib/refresh.js +92 -72
  104. package/dist/lib/secrets-cli.d.ts +11 -0
  105. package/dist/lib/secrets-cli.js +30 -0
  106. package/dist/lib/secrets-client.js +3 -2
  107. package/dist/lib/session/detached.d.ts +7 -0
  108. package/dist/lib/session/detached.js +29 -0
  109. package/dist/lib/session/remote/peer-stream.d.ts +24 -2
  110. package/dist/lib/session/remote/peer-stream.js +33 -6
  111. package/dist/lib/session/sync/backend.d.ts +3 -3
  112. package/dist/lib/session/sync/backend.js +3 -3
  113. package/dist/lib/session/sync/provision.d.ts +1 -1
  114. package/dist/lib/session/sync/provision.js +2 -2
  115. package/dist/lib/sessions-client.js +0 -3
  116. package/dist/lib/setup-tool-install.d.ts +3 -0
  117. package/dist/lib/setup-tool-install.js +26 -0
  118. package/dist/lib/setup-tool-status.d.ts +22 -0
  119. package/dist/lib/setup-tool-status.js +215 -0
  120. package/dist/lib/share-runtime.d.ts +11 -0
  121. package/dist/lib/share-runtime.js +63 -0
  122. package/dist/lib/smart-launch.d.ts +1 -5
  123. package/dist/lib/smart-launch.js +3 -11
  124. package/dist/lib/ssh-exec.d.ts +44 -0
  125. package/dist/lib/ssh-exec.js +119 -0
  126. package/dist/lib/startup/command-registry.d.ts +6 -4
  127. package/dist/lib/startup/command-registry.js +10 -7
  128. package/dist/lib/state.js +2 -2
  129. package/dist/lib/storage/selection.d.ts +2 -2
  130. package/dist/lib/storage/selection.js +2 -2
  131. package/dist/lib/sync-umbrella.d.ts +5 -0
  132. package/dist/lib/sync-umbrella.js +18 -10
  133. package/dist/lib/traces/backend.d.ts +1 -2
  134. package/dist/lib/traces/backend.js +1 -2
  135. package/dist/lib/traces/provision.d.ts +1 -1
  136. package/dist/lib/traces/provision.js +2 -2
  137. package/dist/lib/types.d.ts +8 -6
  138. package/package.json +2 -3
  139. package/dist/commands/artifacts-setup.d.ts +0 -53
  140. package/dist/commands/artifacts-setup.js +0 -161
  141. package/dist/commands/artifacts.d.ts +0 -18
  142. package/dist/commands/artifacts.js +0 -58
  143. package/dist/commands/attach.d.ts +0 -12
  144. package/dist/commands/attach.js +0 -86
  145. package/dist/commands/auth-mint.d.ts +0 -12
  146. package/dist/commands/auth-mint.js +0 -108
  147. package/dist/commands/reconnect.d.ts +0 -46
  148. package/dist/commands/reconnect.js +0 -115
  149. package/dist/commands/share.d.ts +0 -293
  150. package/dist/commands/share.js +0 -1424
  151. package/dist/lib/share/analytics.d.ts +0 -13
  152. package/dist/lib/share/analytics.js +0 -45
  153. package/dist/lib/share/backend.d.ts +0 -120
  154. package/dist/lib/share/backend.js +0 -176
  155. package/dist/lib/share/capture.d.ts +0 -31
  156. package/dist/lib/share/capture.js +0 -174
  157. package/dist/lib/share/config.d.ts +0 -72
  158. package/dist/lib/share/config.js +0 -211
  159. package/dist/lib/share/delete.d.ts +0 -123
  160. package/dist/lib/share/delete.js +0 -173
  161. package/dist/lib/share/html.d.ts +0 -20
  162. package/dist/lib/share/html.js +0 -88
  163. package/dist/lib/share/http-error.d.ts +0 -53
  164. package/dist/lib/share/http-error.js +0 -65
  165. package/dist/lib/share/og.d.ts +0 -26
  166. package/dist/lib/share/og.js +0 -84
  167. package/dist/lib/share/provision.d.ts +0 -127
  168. package/dist/lib/share/provision.js +0 -285
  169. package/dist/lib/share/publish.d.ts +0 -379
  170. package/dist/lib/share/publish.js +0 -818
  171. package/dist/lib/share/worker-template.d.ts +0 -27
  172. package/dist/lib/share/worker-template.js +0 -2424
  173. package/dist/lib/storage/index.d.ts +0 -14
  174. package/dist/lib/storage/index.js +0 -14
  175. package/dist/lib/storage/visibility.d.ts +0 -82
  176. package/dist/lib/storage/visibility.js +0 -99
package/dist/lib/mcp.js CHANGED
@@ -380,25 +380,31 @@ function installMcpViaClaude(binaryPath, server, versionHome) {
380
380
  * Uses: codex mcp add <name> -- <cmd> [args...]
381
381
  */
382
382
  function installMcpViaCodex(binaryPath, server, versionHome) {
383
- if (server.config.transport === 'stdio') {
383
+ let args;
384
+ if (server.config.transport === 'http') {
385
+ if (!server.config.url)
386
+ throw new Error(`HTTP MCP '${server.name}' has no url`);
387
+ // codex mcp add <name> --url <url>
388
+ args = ['mcp', 'add', server.name, '--url', server.config.url];
389
+ }
390
+ else {
384
391
  // codex mcp add -- <name> <cmd> [args...]
385
- const args = [
392
+ args = [
386
393
  'mcp', 'add',
387
394
  '--',
388
395
  server.name,
389
396
  server.config.command,
390
397
  ...(server.config.args || [])
391
398
  ];
392
- // RUSH-1752: user-controlled MCP command/args must not reach cmd.exe unquoted.
393
- const spec = execFileShellSpec(binaryPath, args);
394
- execFileSync(spec.command, spec.args, {
395
- stdio: 'pipe',
396
- timeout: 30000,
397
- env: { ...process.env, HOME: versionHome },
398
- shell: spec.shell,
399
- });
400
399
  }
401
- // Note: Codex may not support HTTP MCPs
400
+ // RUSH-1752: user-controlled MCP command/args must not reach cmd.exe unquoted.
401
+ const spec = execFileShellSpec(binaryPath, args);
402
+ execFileSync(spec.command, spec.args, {
403
+ stdio: 'pipe',
404
+ timeout: 30000,
405
+ env: { ...process.env, HOME: versionHome },
406
+ shell: spec.shell,
407
+ });
402
408
  }
403
409
  export async function registerMcpCommandToTargets(targets, name, commandSpec, scope = 'user', transport = 'stdio') {
404
410
  const results = [];
@@ -3,6 +3,9 @@
3
3
  * timeout — matching the `execFileAsync` contract the version probe had — and
4
4
  * reaps the probe's whole process group on every settle path.
5
5
  */
6
- export declare function probeCapture(cmd: string, args: string[], timeoutMs: number): Promise<{
6
+ export declare function probeCapture(cmd: string, args: string[], timeoutMs: number, options?: {
7
+ acceptedExitCodes?: number[];
8
+ maxOutputBytes?: number;
9
+ }): Promise<{
7
10
  stdout: string;
8
11
  }>;
package/dist/lib/probe.js CHANGED
@@ -59,7 +59,7 @@ function reapGroup(pid) {
59
59
  * timeout — matching the `execFileAsync` contract the version probe had — and
60
60
  * reaps the probe's whole process group on every settle path.
61
61
  */
62
- export function probeCapture(cmd, args, timeoutMs) {
62
+ export function probeCapture(cmd, args, timeoutMs, options = {}) {
63
63
  return new Promise((resolve, reject) => {
64
64
  const child = spawn(cmd, args, {
65
65
  detached: GROUP_REAP,
@@ -92,6 +92,9 @@ export function probeCapture(cmd, args, timeoutMs) {
92
92
  child.stdout?.setEncoding('utf8');
93
93
  child.stdout?.on('data', (d) => {
94
94
  out += d;
95
+ if (Buffer.byteLength(out) > (options.maxOutputBytes ?? 1024 * 1024)) {
96
+ settle(new Error(`probe output exceeded limit: ${cmd}`));
97
+ }
95
98
  });
96
99
  child.on('error', (e) => settle(e));
97
100
  // 'exit', not 'close': a forked grandchild inherits the stdout pipe, and
@@ -99,7 +102,7 @@ export function probeCapture(cmd, args, timeoutMs) {
99
102
  // process this helper exists to reap. Settle when the probed binary
100
103
  // itself exits; one tick's grace lets its final stdout chunks land.
101
104
  child.on('exit', (code) => {
102
- setImmediate(() => settle(code === 0 ? null : new Error(`probe exited ${code}: ${cmd} ${args.join(' ')}`)));
105
+ setImmediate(() => settle(code !== null && (options.acceptedExitCodes ?? [0]).includes(code) ? null : new Error(`probe exited ${code}: ${cmd} ${args.join(' ')}`)));
103
106
  });
104
107
  });
105
108
  }
@@ -9,3 +9,36 @@
9
9
  * spawns powershell.exe with an argv array).
10
10
  */
11
11
  export declare function encodePwshBase64(script: string): string;
12
+ /** Single-quote a value for PowerShell's own parser (embedded `'` doubled). */
13
+ export declare function pwshLiteral(value: string): string;
14
+ /**
15
+ * PowerShell statements that run a NATIVE program with exact argv.
16
+ *
17
+ * Windows has no argv array: a process receives ONE string and splits it itself.
18
+ * PowerShell 5.1 rebuilds that string when it invokes a native program, and its
19
+ * serializer is lossy — measured on a real peer, an EMPTY argument is dropped and
20
+ * an embedded `"` is discarded. So the arguments are pre-escaped by the caller to
21
+ * the `CommandLineToArgvW` rules (`quoteWin32ExecArg`) and handed to .NET as one
22
+ * `Arguments` string, which `CreateProcess` receives essentially verbatim. No shell
23
+ * is involved, so no `%VAR%` expansion and no newline sensitivity.
24
+ *
25
+ * `UseShellExecute = $false` with no redirection leaves the child on the inherited
26
+ * handles, which is what lets a binary stdout stream through unchanged.
27
+ *
28
+ * `WorkingDirectory` is set explicitly from `(Get-Location).ProviderPath`, and that
29
+ * is not belt-and-braces: PowerShell's `Set-Location` moves the SHELL's location
30
+ * without updating the .NET process's OS working directory, so a child launched
31
+ * through `ProcessStartInfo` would otherwise start in whatever directory the
32
+ * process began in — silently ignoring a caller's `cwd` (`--remote-cwd`). The
33
+ * `ProviderPath` form is what yields a real filesystem path rather than a
34
+ * PowerShell provider path.
35
+ *
36
+ * @param fileNameExpr a PowerShell EXPRESSION for the executable (a quoted literal,
37
+ * or something like `$__c.Source`) — not a raw path, so a caller that resolved the
38
+ * target on the peer can pass its variable.
39
+ * @param argumentsExpr a PowerShell EXPRESSION producing the argument string —
40
+ * `pwshLiteral(line)` for a static one, or a concatenation when part of it is
41
+ * only known on the peer (a resolved interpreter entry, say).
42
+ * @param exitVar variable name for the child, so a caller can chain on it.
43
+ */
44
+ export declare function pwshNativeExecStatements(fileNameExpr: string, argumentsExpr: string, exitVar?: string): string[];
package/dist/lib/pwsh.js CHANGED
@@ -11,3 +11,59 @@
11
11
  export function encodePwshBase64(script) {
12
12
  return Buffer.from(script, 'utf16le').toString('base64');
13
13
  }
14
+ /** Single-quote a value for PowerShell's own parser (embedded `'` doubled). */
15
+ export function pwshLiteral(value) {
16
+ return `'${value.replace(/'/g, "''")}'`;
17
+ }
18
+ /**
19
+ * PowerShell statements that run a NATIVE program with exact argv.
20
+ *
21
+ * Windows has no argv array: a process receives ONE string and splits it itself.
22
+ * PowerShell 5.1 rebuilds that string when it invokes a native program, and its
23
+ * serializer is lossy — measured on a real peer, an EMPTY argument is dropped and
24
+ * an embedded `"` is discarded. So the arguments are pre-escaped by the caller to
25
+ * the `CommandLineToArgvW` rules (`quoteWin32ExecArg`) and handed to .NET as one
26
+ * `Arguments` string, which `CreateProcess` receives essentially verbatim. No shell
27
+ * is involved, so no `%VAR%` expansion and no newline sensitivity.
28
+ *
29
+ * `UseShellExecute = $false` with no redirection leaves the child on the inherited
30
+ * handles, which is what lets a binary stdout stream through unchanged.
31
+ *
32
+ * `WorkingDirectory` is set explicitly from `(Get-Location).ProviderPath`, and that
33
+ * is not belt-and-braces: PowerShell's `Set-Location` moves the SHELL's location
34
+ * without updating the .NET process's OS working directory, so a child launched
35
+ * through `ProcessStartInfo` would otherwise start in whatever directory the
36
+ * process began in — silently ignoring a caller's `cwd` (`--remote-cwd`). The
37
+ * `ProviderPath` form is what yields a real filesystem path rather than a
38
+ * PowerShell provider path.
39
+ *
40
+ * @param fileNameExpr a PowerShell EXPRESSION for the executable (a quoted literal,
41
+ * or something like `$__c.Source`) — not a raw path, so a caller that resolved the
42
+ * target on the peer can pass its variable.
43
+ * @param argumentsExpr a PowerShell EXPRESSION producing the argument string —
44
+ * `pwshLiteral(line)` for a static one, or a concatenation when part of it is
45
+ * only known on the peer (a resolved interpreter entry, say).
46
+ * @param exitVar variable name for the child, so a caller can chain on it.
47
+ */
48
+ export function pwshNativeExecStatements(fileNameExpr, argumentsExpr, exitVar = '$zp') {
49
+ // Deliberately terse. The whole script is base64'd into ONE ssh command line,
50
+ // and OpenSSH-for-Windows caps that far below cmd.exe's 8191 — measured on a
51
+ // real peer, it fails between ~2.8k and ~4.1k characters. UTF-16LE + base64
52
+ // inflates by ~2.7x, so every character here costs almost three on the wire.
53
+ // `System.` is implicit for these types, and `-EA` is the canonical alias.
54
+ return [
55
+ `$zi=New-Object Diagnostics.ProcessStartInfo`,
56
+ `$zi.FileName=${fileNameExpr}`,
57
+ `$zi.Arguments=${argumentsExpr}`,
58
+ `$zi.UseShellExecute=$false`,
59
+ `$zi.WorkingDirectory=(Get-Location).ProviderPath`,
60
+ // The CALLER sets `$ErrorActionPreference='Stop'` for the whole script, so it
61
+ // is not repeated here — every character costs ~2.7 on the wire after base64.
62
+ // The null guard below is what makes a swallowed Start failure impossible to
63
+ // mistake for success: without it the handle is $null, the code is $null, and
64
+ // `exit $null` reports 0.
65
+ `${exitVar}=[Diagnostics.Process]::Start($zi)`,
66
+ `if($null -eq ${exitVar}){throw "start failed: $($zi.FileName)"}`,
67
+ `${exitVar}.WaitForExit()`,
68
+ ];
69
+ }
@@ -23,3 +23,11 @@ export declare function redactSecrets(text: string, knownValues?: readonly strin
23
23
  * credential verbatim even when its format matches no pattern.
24
24
  */
25
25
  export declare function knownSecretValuesFromEnv(env?: NodeJS.ProcessEnv): string[];
26
+ /**
27
+ * Mask email addresses in already-rendered text. A published page's sensitive-
28
+ * content scan refuses a body containing an email (`artifacts share` does the
29
+ * same), and almost every real transcript carries a few — git author addresses,
30
+ * `gh api user`, a pasted log. Masking them here means the page genuinely does
31
+ * not carry them, rather than training callers to pass `--force`.
32
+ */
33
+ export declare function redactEmails(text: string): string;
@@ -185,3 +185,14 @@ export function knownSecretValuesFromEnv(env = process.env) {
185
185
  }
186
186
  return out;
187
187
  }
188
+ const EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
189
+ /**
190
+ * Mask email addresses in already-rendered text. A published page's sensitive-
191
+ * content scan refuses a body containing an email (`artifacts share` does the
192
+ * same), and almost every real transcript carries a few — git author addresses,
193
+ * `gh api user`, a pasted log. Masking them here means the page genuinely does
194
+ * not carry them, rather than training callers to pass `--force`.
195
+ */
196
+ export function redactEmails(text) {
197
+ return text.replace(EMAIL_RE, '[EMAIL]');
198
+ }
@@ -6,10 +6,10 @@
6
6
  * The reconcile stage behind `agents sync` (the umbrella `--local` path calls
7
7
  * this; see sync-umbrella.ts) and any other caller that needs to re-derive local
8
8
  * state from declared configuration. Does NOT do any git operations — that lives
9
- * in `agents repo pull`. (Also still reachable via the deprecated, hidden
10
- * `agents repo refresh` alias.)
9
+ * in `agents repo pull`.
11
10
  */
12
11
  import type { AgentId } from './types.js';
12
+ import { type ResourceSelection } from './installations/versions.js';
13
13
  interface RefreshOptions {
14
14
  /** Limit operations to a single agent (claude/codex/etc). Default: all installed. */
15
15
  agentFilter?: AgentId;
@@ -22,6 +22,10 @@ interface RefreshOptions {
22
22
  * (`agents sync --json` / fleet fan-out) so stdout stays a single JSON object.
23
23
  */
24
24
  quiet?: boolean;
25
+ /** Limit reconciliation to the requested resource kinds/names. */
26
+ selection?: ResourceSelection;
27
+ /** Explicit consent for selected plugins that add executable surfaces. */
28
+ allowExecSurfaces?: boolean;
25
29
  }
26
30
  /**
27
31
  * Re-materialize local state from declared configuration: install CLI versions,
@@ -6,8 +6,7 @@
6
6
  * The reconcile stage behind `agents sync` (the umbrella `--local` path calls
7
7
  * this; see sync-umbrella.ts) and any other caller that needs to re-derive local
8
8
  * state from declared configuration. Does NOT do any git operations — that lives
9
- * in `agents repo pull`. (Also still reachable via the deprecated, hidden
10
- * `agents repo refresh` alias.)
9
+ * in `agents repo pull`.
11
10
  */
12
11
  import * as fs from 'fs';
13
12
  import * as path from 'path';
@@ -50,7 +49,7 @@ function migratePromptcutsToRoot(agentsDir, quiet = false) {
50
49
  }
51
50
  }
52
51
  export async function refresh(options = {}) {
53
- const { agentFilter, skipPrompts = false, skipClis = false, quiet = false } = options;
52
+ const { agentFilter, skipPrompts = false, skipClis = false, quiet = false, selection: requestedSelection, allowExecSurfaces = false, } = options;
54
53
  const agentsDir = getUserAgentsDir();
55
54
  // Gate every human progress line so --json / fleet fan-out can parse stdout.
56
55
  const log = (...args) => { if (!quiet)
@@ -59,7 +58,8 @@ export async function refresh(options = {}) {
59
58
  // synced list cannot also mean "declined and here is why" (RUSH-2700).
60
59
  const declined = [];
61
60
  const reconciled = [];
62
- migratePromptcutsToRoot(agentsDir, quiet);
61
+ if (!requestedSelection)
62
+ migratePromptcutsToRoot(agentsDir, quiet);
63
63
  const manifest = readManifest(agentsDir);
64
64
  if (!manifest) {
65
65
  log(chalk.gray(`No ${MANIFEST_FILENAME} found`));
@@ -94,17 +94,23 @@ export async function refresh(options = {}) {
94
94
  }
95
95
  }
96
96
  // 2. Register MCP servers
97
- if (manifest?.mcp && Object.keys(manifest.mcp).length > 0) {
97
+ if ((!requestedSelection || requestedSelection.mcp) && manifest?.mcp && Object.keys(manifest.mcp).length > 0) {
98
98
  log(chalk.bold('\nMCP Servers:\n'));
99
99
  for (const [name, config] of Object.entries(manifest.mcp)) {
100
- if (!config.command || config.transport === 'http')
100
+ if (Array.isArray(requestedSelection?.mcp) && !requestedSelection.mcp.includes(name))
101
101
  continue;
102
+ const transport = config.transport || 'stdio';
103
+ const commandOrUrl = transport === 'http' ? config.url : config.command;
104
+ if (!commandOrUrl) {
105
+ log(` ${chalk.cyan(name)}: ${chalk.yellow(`missing ${transport === 'http' ? 'url' : 'command'}`)}`);
106
+ continue;
107
+ }
102
108
  const scopedAgents = (config.agents ? [...config.agents] : [...capableAgents('mcp')]).filter((id) => !agentFilter || id === agentFilter);
103
109
  const scopedVersions = config.agentVersions
104
110
  ? Object.fromEntries(Object.entries(config.agentVersions).filter(([agentId]) => !agentFilter || agentId === agentFilter))
105
111
  : undefined;
106
112
  const targets = resolveConfiguredAgentTargets(scopedAgents, scopedVersions, capableAgents('mcp'));
107
- const results = await registerMcpToTargets(targets, name, config.command, config.scope || 'user', config.transport || 'stdio');
113
+ const results = await registerMcpToTargets(targets, name, commandOrUrl, config.scope || 'user', transport);
108
114
  for (const result of results) {
109
115
  if (result.success) {
110
116
  const label = result.version
@@ -116,22 +122,25 @@ export async function refresh(options = {}) {
116
122
  }
117
123
  }
118
124
  // 3. Sync resources into version homes.
119
- // Unattended (`skipPrompts` / `agents sync --yes --local`): every installed
120
- // version — otherwise non-default homes keep stale hooks after a system
121
- // update (fleet multi-harness: only the default version was refreshed).
122
- // Interactive: default only; re-run `agents sync <agent>@all` for the rest.
125
+ // Unattended (`skipPrompts` / `agents sync --yes --local`) and explicit
126
+ // resource selectors: every installed version. Otherwise non-default homes
127
+ // keep stale resources after a system update or named plugin sync.
128
+ // Interactive full reconcile: default only.
123
129
  const cliStates = await getAllCliStates();
124
130
  const agentsToSync = agentFilter ? [agentFilter] : MANAGED_AGENT_IDS;
125
131
  const available = getAvailableResources();
126
132
  for (const agentId of agentsToSync) {
127
- if (!cliStates[agentId]?.installed && listInstalledVersions(agentId).length === 0)
133
+ const installedVersions = listInstalledVersions(agentId);
134
+ if (!cliStates[agentId]?.installed && installedVersions.length === 0)
128
135
  continue;
129
136
  const defaultVer = getGlobalDefault(agentId);
130
- if (!defaultVer)
137
+ if (!defaultVer && !requestedSelection)
131
138
  continue;
132
- const versionsToSync = skipPrompts
133
- ? listInstalledVersions(agentId)
139
+ const versionsToSync = requestedSelection || skipPrompts
140
+ ? installedVersions
134
141
  : [defaultVer];
142
+ if (versionsToSync.length === 0)
143
+ continue;
135
144
  // Interactive-only: getActuallySyncedResources walks every skill tree with
136
145
  // content compares (~1s/agent on a full install). The unattended path
137
146
  // (`skipPrompts` / `agents sync --yes`) never reads these — it always
@@ -139,7 +148,7 @@ export async function refresh(options = {}) {
139
148
  let actuallySynced;
140
149
  let newResources;
141
150
  let hasAnySynced = false;
142
- if (!skipPrompts) {
151
+ if (!skipPrompts && !requestedSelection) {
143
152
  actuallySynced = getActuallySyncedResources(agentId, defaultVer);
144
153
  newResources = getNewResources(available, actuallySynced, getProjectOnlyResources());
145
154
  hasAnySynced = actuallySynced.commands.length > 0 ||
@@ -153,7 +162,10 @@ export async function refresh(options = {}) {
153
162
  try {
154
163
  let selection;
155
164
  let forceFullSync = false;
156
- if (skipPrompts) {
165
+ if (requestedSelection) {
166
+ selection = requestedSelection;
167
+ }
168
+ else if (skipPrompts) {
157
169
  forceFullSync = true;
158
170
  }
159
171
  else if (!hasAnySynced) {
@@ -176,7 +188,11 @@ export async function refresh(options = {}) {
176
188
  for (const ver of versionsToSync) {
177
189
  // Pass the already-built `available` so each version does not re-scan
178
190
  // resource trees (RUSH-2320 #5).
179
- const syncResult = syncResourcesToVersion(agentId, ver, selection, { available, ...(forceFullSync ? { force: true } : {}) });
191
+ const syncResult = syncResourcesToVersion(agentId, ver, selection, {
192
+ available,
193
+ allowExecSurfaces,
194
+ ...(forceFullSync || requestedSelection ? { force: true } : {}),
195
+ });
180
196
  reconciled.push({ agent: agentId, version: ver });
181
197
  if (syncResult.commands)
182
198
  kinds.add('commands');
@@ -214,7 +230,7 @@ export async function refresh(options = {}) {
214
230
  }
215
231
  }
216
232
  // 4. Register hooks as lifecycle events (same version set as resource sync)
217
- const hookManifest = parseHookManifest();
233
+ const hookManifest = requestedSelection ? {} : parseHookManifest();
218
234
  if (Object.keys(hookManifest).length > 0) {
219
235
  let hookRegistered = 0;
220
236
  const hookAgents = new Set(capableAgents('hooks'));
@@ -242,25 +258,27 @@ export async function refresh(options = {}) {
242
258
  // 5. Auto-add shims to PATH
243
259
  // Refresh the gh overload shim so `gh pr checks` escapes the GraphQL rate limit
244
260
  // for every user, transparently (PHNX-3501). Idempotent; POSIX-only in v1.
245
- try {
246
- ensureGhOverloadShim();
247
- }
248
- catch {
249
- // Never let a shim-write hiccup break sync — real gh stays fine without it.
250
- }
251
- if (!isShimsInPath()) {
252
- const pathResult = addShimsToPath();
253
- if (pathResult.success && !pathResult.alreadyPresent) {
254
- log(chalk.green(`\nAdded shims to ${pathResult.location}`));
255
- log(chalk.gray(pathResult.reloadHint));
261
+ if (!requestedSelection) {
262
+ try {
263
+ ensureGhOverloadShim();
264
+ }
265
+ catch {
266
+ // Never let a shim-write hiccup break sync — real gh stays fine without it.
256
267
  }
257
- else if (!pathResult.success) {
258
- log(chalk.yellow('\nCould not auto-add shims to PATH:'));
259
- log(chalk.gray(getPathSetupInstructions()));
268
+ if (!isShimsInPath()) {
269
+ const pathResult = addShimsToPath();
270
+ if (pathResult.success && !pathResult.alreadyPresent) {
271
+ log(chalk.green(`\nAdded shims to ${pathResult.location}`));
272
+ log(chalk.gray(pathResult.reloadHint));
273
+ }
274
+ else if (!pathResult.success) {
275
+ log(chalk.yellow('\nCould not auto-add shims to PATH:'));
276
+ log(chalk.gray(getPathSetupInstructions()));
277
+ }
260
278
  }
261
279
  }
262
280
  // 6. Prompt for missing default versions
263
- if (!skipPrompts) {
281
+ if (!skipPrompts && !requestedSelection) {
264
282
  const agentsNeedingDefault = [];
265
283
  for (const agentId of agentsToSync) {
266
284
  const versions = listInstalledVersions(agentId);
@@ -307,53 +325,55 @@ export async function refresh(options = {}) {
307
325
  }
308
326
  }
309
327
  // 7. Install declared host-CLIs
310
- try {
311
- const { statuses, errors } = listCliStatus(process.cwd());
312
- for (const err of errors) {
313
- log(chalk.yellow(` CLI manifest parse error: ${err.file}: ${err.reason}`));
314
- }
315
- const missing = statuses.filter((s) => !s.installed);
316
- if (missing.length > 0) {
317
- log(chalk.bold('\nDeclared CLIs missing from this host:'));
318
- for (const s of missing) {
319
- const method = selectInstallMethod(s.manifest);
320
- const action = method ? describeMethod(method) : chalk.red('no compatible install method');
321
- log(` ${chalk.cyan(s.manifest.name.padEnd(20))} ${chalk.gray(action)}`);
328
+ if (!requestedSelection) {
329
+ try {
330
+ const { statuses, errors } = listCliStatus(process.cwd());
331
+ for (const err of errors) {
332
+ log(chalk.yellow(` CLI manifest parse error: ${err.file}: ${err.reason}`));
322
333
  }
323
- log('');
324
- if (!skipPrompts) {
325
- const proceed = await confirm({ message: `Install ${missing.length} missing CLI(s) now?`, default: true });
326
- if (proceed) {
327
- for (const s of missing) {
328
- log(chalk.bold(`\n→ ${s.manifest.name}`));
329
- const result = installCli(s.manifest);
330
- if (result.error) {
331
- log(chalk.red(` ${result.error}`));
332
- continue;
333
- }
334
- if (result.installed) {
335
- log(chalk.green(` installed`));
336
- if (s.manifest.postInstall) {
337
- log(chalk.gray(s.manifest.postInstall.trim().split('\n').map((l) => ' ' + l).join('\n')));
334
+ const missing = statuses.filter((s) => !s.installed);
335
+ if (missing.length > 0) {
336
+ log(chalk.bold('\nDeclared CLIs missing from this host:'));
337
+ for (const s of missing) {
338
+ const method = selectInstallMethod(s.manifest);
339
+ const action = method ? describeMethod(method) : chalk.red('no compatible install method');
340
+ log(` ${chalk.cyan(s.manifest.name.padEnd(20))} ${chalk.gray(action)}`);
341
+ }
342
+ log('');
343
+ if (!skipPrompts) {
344
+ const proceed = await confirm({ message: `Install ${missing.length} missing CLI(s) now?`, default: true });
345
+ if (proceed) {
346
+ for (const s of missing) {
347
+ log(chalk.bold(`\n→ ${s.manifest.name}`));
348
+ const result = installCli(s.manifest);
349
+ if (result.error) {
350
+ log(chalk.red(` ${result.error}`));
351
+ continue;
352
+ }
353
+ if (result.installed) {
354
+ log(chalk.green(` installed`));
355
+ if (s.manifest.postInstall) {
356
+ log(chalk.gray(s.manifest.postInstall.trim().split('\n').map((l) => ' ' + l).join('\n')));
357
+ }
358
+ }
359
+ else {
360
+ log(chalk.yellow(` install ran but \`${describeCheck(s.manifest.check)}\` still fails`));
338
361
  }
339
362
  }
340
- else {
341
- log(chalk.yellow(` install ran but \`${describeCheck(s.manifest.check)}\` still fails`));
342
- }
363
+ }
364
+ else {
365
+ log(chalk.gray(`Skipped. Run 'agents cli install' later.`));
343
366
  }
344
367
  }
345
368
  else {
346
- log(chalk.gray(`Skipped. Run 'agents cli install' later.`));
369
+ log(chalk.gray(`Run 'agents cli install' to install them.`));
347
370
  }
348
371
  }
349
- else {
350
- log(chalk.gray(`Run 'agents cli install' to install them.`));
351
- }
352
372
  }
353
- }
354
- catch (err) {
355
- if (!isPromptCancelled(err)) {
356
- log(chalk.yellow(`CLI install skipped: ${err.message}`));
373
+ catch (err) {
374
+ if (!isPromptCancelled(err)) {
375
+ log(chalk.yellow(`CLI install skipped: ${err.message}`));
376
+ }
357
377
  }
358
378
  }
359
379
  // A resource agents-cli refused to write is reported, never swallowed —
@@ -0,0 +1,11 @@
1
+ export declare const SECRETS_CLI_NAME = "secrets";
2
+ export declare const SECRETS_CLI_PACKAGE = "@phnx-labs/secrets-cli";
3
+ /** Published standalone `agents setup secrets` installs; bump with the protocol. */
4
+ export declare const SECRETS_CLI_VERSION = "0.1.4";
5
+ export declare const SECRETS_CLI_SPEC = "@phnx-labs/secrets-cli@0.1.4";
6
+ export declare const SECRETS_CLI_INSTALL_HINT = "npm i -g @phnx-labs/secrets-cli@0.1.4";
7
+ /**
8
+ * True when `$SECRETS_BIN` is set or a real `secrets` executable is on PATH
9
+ * outside agents-cli's shims dir. Does not spawn the binary.
10
+ */
11
+ export declare function isSecretsPresent(): boolean;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The standalone `secrets` CLI that agents-cli talks to (PHNX-3989).
3
+ *
4
+ * The engine lives in `@phnx-labs/secrets-cli`. This module is the single pin
5
+ * and presence check shared by the process client (`secrets-client.ts`) and
6
+ * `agents setup secrets` — it carries no storage. Presence is `findInPath`,
7
+ * which skips `~/.agents/.cache/shims`: a leftover alias there `exec`s
8
+ * `agents secrets` and would recurse (agi-cli#3532).
9
+ *
10
+ * Keep this file free of `cli-resources` imports: `secrets-client.ts` loads it,
11
+ * and a cycle through the host-CLI parser would pull yaml/spawn into the
12
+ * process-client module graph.
13
+ */
14
+ import { findInPath } from './agent-spec/agents.js';
15
+ export const SECRETS_CLI_NAME = 'secrets';
16
+ export const SECRETS_CLI_PACKAGE = '@phnx-labs/secrets-cli';
17
+ /** Published standalone `agents setup secrets` installs; bump with the protocol. */
18
+ export const SECRETS_CLI_VERSION = '0.1.4';
19
+ export const SECRETS_CLI_SPEC = `${SECRETS_CLI_PACKAGE}@${SECRETS_CLI_VERSION}`;
20
+ export const SECRETS_CLI_INSTALL_HINT = `npm i -g ${SECRETS_CLI_SPEC}`;
21
+ /**
22
+ * True when `$SECRETS_BIN` is set or a real `secrets` executable is on PATH
23
+ * outside agents-cli's shims dir. Does not spawn the binary.
24
+ */
25
+ export function isSecretsPresent() {
26
+ const explicit = process.env.SECRETS_BIN?.trim();
27
+ if (explicit)
28
+ return true;
29
+ return findInPath(SECRETS_CLI_NAME) !== null;
30
+ }
@@ -49,6 +49,7 @@
49
49
  */
50
50
  import { spawn, spawnSync } from 'node:child_process';
51
51
  import { findInPath } from './agent-spec/agents.js';
52
+ import { SECRETS_CLI_INSTALL_HINT } from './secrets-cli.js';
52
53
  import { getUserAgentsDir } from './state.js';
53
54
  /**
54
55
  * Wire contract, mirrored from `secrets-cli/src/protocol.ts`. Both sides MUST
@@ -216,8 +217,8 @@ export function resolveSecretsBin() {
216
217
  const resolved = explicit && explicit.length > 0 ? explicit : findInPath('secrets');
217
218
  if (!resolved) {
218
219
  throw new SecretsClientError('SECRETS_BIN_MISSING', 'The standalone `secrets` CLI was not found. Install it with:\n' +
219
- ' npm i -g @phnx-labs/secrets-cli\n' +
220
- 'or point $SECRETS_BIN at its executable.');
220
+ ` ${SECRETS_CLI_INSTALL_HINT}\n` +
221
+ 'or run `agents setup secrets`.');
221
222
  }
222
223
  cachedBin = resolved;
223
224
  return resolved;
@@ -20,6 +20,13 @@ export declare function clearDetachRecord(sessionId: string): void;
20
20
  export declare function listDetachRecords(): DetachRecord[];
21
21
  /** True while the recorded headless continuation is still the live process we spawned. */
22
22
  export declare function isHeadlessAlive(rec: DetachRecord): boolean;
23
+ /**
24
+ * Stop the detached continuation before a local foreground resume starts.
25
+ * The start-time check in {@link isHeadlessAlive} prevents signalling a reused
26
+ * PID; a continuation that ignores SIGTERM fails closed instead of running
27
+ * alongside a second process against the same transcript.
28
+ */
29
+ export declare function takeOverDetachedSession(sessionId: string): Promise<boolean>;
23
30
  /**
24
31
  * Presence for a session id from the detach store alone:
25
32
  * - no record -> undefined (caller decides: `attached` for a live
@@ -77,6 +77,35 @@ export function isHeadlessAlive(rec) {
77
77
  }
78
78
  return true;
79
79
  }
80
+ /**
81
+ * Stop the detached continuation before a local foreground resume starts.
82
+ * The start-time check in {@link isHeadlessAlive} prevents signalling a reused
83
+ * PID; a continuation that ignores SIGTERM fails closed instead of running
84
+ * alongside a second process against the same transcript.
85
+ */
86
+ export async function takeOverDetachedSession(sessionId) {
87
+ const rec = readDetachRecord(sessionId);
88
+ if (!rec)
89
+ return false;
90
+ if (isHeadlessAlive(rec)) {
91
+ try {
92
+ process.kill(rec.headlessPid, 'SIGTERM');
93
+ }
94
+ catch (err) {
95
+ if (isHeadlessAlive(rec))
96
+ throw err;
97
+ }
98
+ const deadline = Date.now() + 2_000;
99
+ while (isHeadlessAlive(rec) && Date.now() < deadline) {
100
+ await new Promise((resolve) => setTimeout(resolve, 25));
101
+ }
102
+ if (isHeadlessAlive(rec)) {
103
+ throw new Error(`Detached continuation ${rec.headlessPid} did not stop; refusing to resume a duplicate process.`);
104
+ }
105
+ }
106
+ clearDetachRecord(sessionId);
107
+ return true;
108
+ }
80
109
  /**
81
110
  * Presence for a session id from the detach store alone:
82
111
  * - no record -> undefined (caller decides: `attached` for a live