@phnx-labs/agents-cli 1.20.78 → 1.20.82

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 +487 -0
  2. package/README.md +25 -25
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +9 -0
  5. package/dist/commands/activity.js +153 -24
  6. package/dist/commands/apply.js +11 -4
  7. package/dist/commands/cli.js +1 -1
  8. package/dist/commands/commands.d.ts +1 -1
  9. package/dist/commands/commands.js +7 -6
  10. package/dist/commands/defaults.js +3 -11
  11. package/dist/commands/doctor.d.ts +65 -0
  12. package/dist/commands/doctor.js +575 -31
  13. package/dist/commands/events.js +1 -1
  14. package/dist/commands/exec.js +24 -5
  15. package/dist/commands/feed.d.ts +4 -0
  16. package/dist/commands/feed.js +87 -11
  17. package/dist/commands/harness.js +2 -2
  18. package/dist/commands/hooks.js +1 -1
  19. package/dist/commands/import.js +8 -4
  20. package/dist/commands/logs.js +6 -0
  21. package/dist/commands/models.js +1 -1
  22. package/dist/commands/packages.js +6 -6
  23. package/dist/commands/permissions.js +1 -1
  24. package/dist/commands/profiles.js +1 -1
  25. package/dist/commands/pull.d.ts +1 -1
  26. package/dist/commands/pull.js +4 -4
  27. package/dist/commands/repo.d.ts +78 -0
  28. package/dist/commands/repo.js +187 -11
  29. package/dist/commands/routines.js +12 -2
  30. package/dist/commands/rules.js +13 -12
  31. package/dist/commands/secrets.d.ts +3 -1
  32. package/dist/commands/secrets.js +100 -32
  33. package/dist/commands/sessions-inject.d.ts +7 -4
  34. package/dist/commands/sessions-inject.js +18 -18
  35. package/dist/commands/sessions-picker.js +32 -3
  36. package/dist/commands/sessions-resume.d.ts +16 -0
  37. package/dist/commands/sessions-resume.js +14 -13
  38. package/dist/commands/sessions.d.ts +31 -0
  39. package/dist/commands/sessions.js +141 -16
  40. package/dist/commands/set.d.ts +15 -0
  41. package/dist/commands/set.js +79 -0
  42. package/dist/commands/setup-fleet.d.ts +21 -0
  43. package/dist/commands/setup-fleet.js +201 -0
  44. package/dist/commands/setup-secrets.d.ts +20 -0
  45. package/dist/commands/setup-secrets.js +223 -0
  46. package/dist/commands/setup.js +17 -3
  47. package/dist/commands/skills.js +1 -1
  48. package/dist/commands/sync.js +7 -2
  49. package/dist/commands/teams-picker.js +0 -1
  50. package/dist/commands/teams.d.ts +33 -1
  51. package/dist/commands/teams.js +208 -44
  52. package/dist/commands/versions.js +10 -5
  53. package/dist/commands/view.d.ts +1 -0
  54. package/dist/commands/view.js +11 -0
  55. package/dist/index.js +5 -8
  56. package/dist/lib/acp/harnesses.js +0 -7
  57. package/dist/lib/activity.d.ts +156 -0
  58. package/dist/lib/activity.js +282 -0
  59. package/dist/lib/agents.d.ts +9 -2
  60. package/dist/lib/agents.js +72 -63
  61. package/dist/lib/browser/service.js +3 -0
  62. package/dist/lib/browser/types.d.ts +7 -0
  63. package/dist/lib/capabilities.js +6 -2
  64. package/dist/lib/crabbox/lease.js +2 -2
  65. package/dist/lib/crabbox/setup-copy.d.ts +4 -4
  66. package/dist/lib/crabbox/setup-copy.js +4 -4
  67. package/dist/lib/daemon.d.ts +18 -0
  68. package/dist/lib/daemon.js +47 -6
  69. package/dist/lib/devices/fleet.d.ts +1 -1
  70. package/dist/lib/devices/fleet.js +1 -1
  71. package/dist/lib/doctor-diff.d.ts +20 -0
  72. package/dist/lib/doctor-diff.js +6 -1
  73. package/dist/lib/drift.d.ts +31 -11
  74. package/dist/lib/drift.js +58 -7
  75. package/dist/lib/events.d.ts +8 -1
  76. package/dist/lib/events.js +10 -1
  77. package/dist/lib/exec.js +35 -8
  78. package/dist/lib/feed-post.d.ts +28 -1
  79. package/dist/lib/feed-post.js +110 -2
  80. package/dist/lib/fleet/apply.js +8 -1
  81. package/dist/lib/fleet/auth-sync.d.ts +18 -1
  82. package/dist/lib/fleet/auth-sync.js +25 -11
  83. package/dist/lib/fleet/remote-login.js +5 -0
  84. package/dist/lib/git.d.ts +14 -14
  85. package/dist/lib/git.js +41 -44
  86. package/dist/lib/hooks.d.ts +42 -0
  87. package/dist/lib/hooks.js +137 -93
  88. package/dist/lib/hosts/dispatch.d.ts +28 -1
  89. package/dist/lib/hosts/dispatch.js +33 -5
  90. package/dist/lib/hosts/option.js +2 -2
  91. package/dist/lib/hosts/passthrough.d.ts +22 -1
  92. package/dist/lib/hosts/passthrough.js +238 -13
  93. package/dist/lib/hosts/run-target.d.ts +2 -0
  94. package/dist/lib/hosts/run-target.js +1 -0
  95. package/dist/lib/hq/floor.d.ts +1 -1
  96. package/dist/lib/hq/floor.js +6 -1
  97. package/dist/lib/mcp.js +3 -71
  98. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  99. package/dist/lib/menubar/notify-desktop.d.ts +15 -0
  100. package/dist/lib/menubar/notify-desktop.js +41 -5
  101. package/dist/lib/merged-resources.d.ts +11 -0
  102. package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
  103. package/dist/lib/permissions.d.ts +0 -32
  104. package/dist/lib/permissions.js +4 -157
  105. package/dist/lib/picker.js +1 -1
  106. package/dist/lib/platform/process.d.ts +16 -5
  107. package/dist/lib/platform/process.js +54 -0
  108. package/dist/lib/plugins.d.ts +0 -8
  109. package/dist/lib/plugins.js +4 -110
  110. package/dist/lib/project-resources.js +4 -1
  111. package/dist/lib/refresh.d.ts +5 -3
  112. package/dist/lib/refresh.js +7 -5
  113. package/dist/lib/resources/commands.js +4 -1
  114. package/dist/lib/resources/mcp.js +0 -4
  115. package/dist/lib/resources/permissions.d.ts +1 -1
  116. package/dist/lib/resources/permissions.js +1 -5
  117. package/dist/lib/resources/rules.js +4 -1
  118. package/dist/lib/resources/skills.js +4 -1
  119. package/dist/lib/resources/subagents.js +4 -0
  120. package/dist/lib/resources/types.d.ts +1 -1
  121. package/dist/lib/routines.d.ts +20 -0
  122. package/dist/lib/routines.js +6 -0
  123. package/dist/lib/run-defaults.d.ts +1 -0
  124. package/dist/lib/run-defaults.js +9 -0
  125. package/dist/lib/runner.d.ts +0 -12
  126. package/dist/lib/runner.js +49 -10
  127. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  128. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  129. package/dist/lib/secrets/agent.d.ts +16 -0
  130. package/dist/lib/secrets/agent.js +105 -3
  131. package/dist/lib/secrets/audit.d.ts +46 -0
  132. package/dist/lib/secrets/audit.js +56 -0
  133. package/dist/lib/secrets/bundles.d.ts +11 -8
  134. package/dist/lib/secrets/bundles.js +31 -23
  135. package/dist/lib/secrets/index.d.ts +2 -1
  136. package/dist/lib/secrets/index.js +8 -2
  137. package/dist/lib/secrets/remote.d.ts +75 -2
  138. package/dist/lib/secrets/remote.js +139 -5
  139. package/dist/lib/secrets/sync.js +3 -3
  140. package/dist/lib/session/active.d.ts +110 -26
  141. package/dist/lib/session/active.js +161 -50
  142. package/dist/lib/session/actor-sidecar.d.ts +23 -0
  143. package/dist/lib/session/actor-sidecar.js +101 -0
  144. package/dist/lib/session/db.d.ts +4 -1
  145. package/dist/lib/session/db.js +65 -8
  146. package/dist/lib/session/discover.d.ts +4 -0
  147. package/dist/lib/session/discover.js +13 -0
  148. package/dist/lib/session/hook-sessions.d.ts +9 -0
  149. package/dist/lib/session/hook-sessions.js +57 -8
  150. package/dist/lib/session/pid-registry.d.ts +13 -0
  151. package/dist/lib/session/render.d.ts +2 -0
  152. package/dist/lib/session/render.js +1 -1
  153. package/dist/lib/session/types.d.ts +16 -0
  154. package/dist/lib/staleness/detectors/commands.js +2 -2
  155. package/dist/lib/staleness/detectors/permissions.js +1 -45
  156. package/dist/lib/staleness/writers/commands.js +4 -5
  157. package/dist/lib/staleness/writers/hooks.js +1 -1
  158. package/dist/lib/startup/command-registry.d.ts +1 -2
  159. package/dist/lib/startup/command-registry.js +2 -4
  160. package/dist/lib/subagents-registry.js +5 -12
  161. package/dist/lib/subagents.d.ts +0 -10
  162. package/dist/lib/subagents.js +0 -12
  163. package/dist/lib/teams/agents.d.ts +0 -2
  164. package/dist/lib/teams/agents.js +10 -27
  165. package/dist/lib/teams/index.d.ts +1 -1
  166. package/dist/lib/teams/index.js +1 -1
  167. package/dist/lib/types.d.ts +17 -9
  168. package/dist/lib/usage.d.ts +28 -5
  169. package/dist/lib/usage.js +271 -8
  170. package/dist/lib/versions.js +7 -1
  171. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  172. package/dist/lib/watchdog/watchdog.js +25 -13
  173. package/package.json +1 -2
  174. package/dist/commands/check.d.ts +0 -15
  175. package/dist/commands/check.js +0 -180
  176. package/dist/commands/resources.d.ts +0 -5
@@ -16,6 +16,7 @@
16
16
  * 'error' listener — without it Node re-throws ENOENT as an uncaught exception and
17
17
  * takes the whole daemon down (the exact crash overdue.test.ts guards).
18
18
  */
19
+ import { type ChildProcess } from 'child_process';
19
20
  export interface DesktopNotification {
20
21
  /** Bold first line. */
21
22
  title: string;
@@ -36,6 +37,20 @@ export interface DesktopNotification {
36
37
  export declare function buildMenubarNotifyArgs(n: DesktopNotification): string[];
37
38
  /** AppleScript for the osascript degradation path. Exported for tests. */
38
39
  export declare function buildOsascriptNotifyArgs(n: DesktopNotification): string[];
40
+ /**
41
+ * Spawn one detached, best-effort notifier process with a bounded lifetime.
42
+ *
43
+ * The child is detached + unref'd so it never blocks the daemon, but — unlike a
44
+ * pure fire-and-forget — it is supervised: a watchdog SIGKILLs it after
45
+ * `timeoutMs` so a stalled notifier can never linger (the pile-up this fixes).
46
+ * The common path (a sub-second post + self-exit) clears the watchdog on the
47
+ * child's own 'exit', so the kill only ever fires for a genuinely hung child.
48
+ *
49
+ * `timeoutMs` is injectable so the bounded-lifetime behaviour is testable against
50
+ * a real long-running child without a multi-second wait. Returns the child so a
51
+ * caller/test can observe it; callers in this module ignore it.
52
+ */
53
+ export declare function spawnDetachedQuiet(command: string, args: string[], timeoutMs?: number): ChildProcess;
39
54
  /**
40
55
  * Fire a native desktop notification, branded via the MenubarHelper companion on
41
56
  * macOS. Best-effort — any failure is swallowed so a notification hiccup never
@@ -19,6 +19,15 @@
19
19
  import { spawn } from 'child_process';
20
20
  import * as os from 'os';
21
21
  import { resolveInstalledMenubarExecutable } from './install-menubar.js';
22
+ /**
23
+ * Hard ceiling on a one-shot notifier's lifetime. A notifier posts and exits in
24
+ * well under a second on the happy path; if delivery stalls (locked screen, a
25
+ * WindowServer/XPC hiccup) a detached GUI helper can hang indefinitely and pile
26
+ * up in the menu bar. This is above the Swift one-shot's own 0.6s flush + its 3s
27
+ * self-terminate watchdog, so Node's kill is the last-resort guarantee that runs
28
+ * only if the child never self-exits.
29
+ */
30
+ const NOTIFY_TIMEOUT_MS = 4000;
22
31
  /** Argv for the MenubarHelper one-shot notify mode. Exported for tests. */
23
32
  export function buildMenubarNotifyArgs(n) {
24
33
  const args = ['--notify', '--title', n.title, '--body', n.body];
@@ -36,14 +45,41 @@ export function buildOsascriptNotifyArgs(n) {
36
45
  script += ` subtitle "${esc(n.subtitle)}"`;
37
46
  return ['-e', script];
38
47
  }
39
- /** Spawn one detached, best-effort notifier process; swallow async ENOENT. */
40
- function spawnDetachedQuiet(command, args) {
48
+ /**
49
+ * Spawn one detached, best-effort notifier process with a bounded lifetime.
50
+ *
51
+ * The child is detached + unref'd so it never blocks the daemon, but — unlike a
52
+ * pure fire-and-forget — it is supervised: a watchdog SIGKILLs it after
53
+ * `timeoutMs` so a stalled notifier can never linger (the pile-up this fixes).
54
+ * The common path (a sub-second post + self-exit) clears the watchdog on the
55
+ * child's own 'exit', so the kill only ever fires for a genuinely hung child.
56
+ *
57
+ * `timeoutMs` is injectable so the bounded-lifetime behaviour is testable against
58
+ * a real long-running child without a multi-second wait. Returns the child so a
59
+ * caller/test can observe it; callers in this module ignore it.
60
+ */
61
+ export function spawnDetachedQuiet(command, args, timeoutMs = NOTIFY_TIMEOUT_MS) {
41
62
  const child = spawn(command, args, { detached: true, stdio: 'ignore' });
63
+ // Bound the lifetime: a stalled notifier is hard-killed after the timeout. The
64
+ // timer is unref'd so it never keeps a short-lived caller's event loop alive.
65
+ const watchdog = setTimeout(() => {
66
+ try {
67
+ child.kill('SIGKILL');
68
+ }
69
+ catch {
70
+ /* already gone */
71
+ }
72
+ }, timeoutMs);
73
+ watchdog.unref();
42
74
  // A missing binary (headless box with no osascript/notify-send, or a helper
43
- // that vanished) arrives as an async 'error' event. Without this listener Node
44
- // re-throws it as an uncaught exception and crashes the daemon swallow it.
45
- child.on('error', () => { });
75
+ // that vanished) arrives as an async 'error' event the process never started,
76
+ // so cancel the watchdog. Without a listener Node re-throws ENOENT as an
77
+ // uncaught exception and crashes the daemon; having one swallows it.
78
+ child.on('error', () => clearTimeout(watchdog));
79
+ // Fast, self-driven exit (the common path) — cancel the watchdog.
80
+ child.on('exit', () => clearTimeout(watchdog));
46
81
  child.unref();
82
+ return child;
47
83
  }
48
84
  /**
49
85
  * Fire a native desktop notification, branded via the MenubarHelper companion on
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The merged, first-wins DotAgents resource surface — every resource kind, one
3
+ * table, showing the winning layer per row.
4
+ *
5
+ * Ported verbatim from the former `agents resources` command, now surfaced as
6
+ * `agents view --merged`. `agents view agent@version` shows per-version
7
+ * resources; this shows the cross-layer merge across project → user → extras →
8
+ * system. Kept as a shared lib (not inlined into `view.ts`) so `inspect` can
9
+ * reuse it later.
10
+ */
11
+ export declare function renderMergedResources(): void;
@@ -1,10 +1,17 @@
1
1
  /**
2
- * `agents resources` show the merged DotAgents resource surface.
2
+ * The merged, first-wins DotAgents resource surface every resource kind, one
3
+ * table, showing the winning layer per row.
4
+ *
5
+ * Ported verbatim from the former `agents resources` command, now surfaced as
6
+ * `agents view --merged`. `agents view agent@version` shows per-version
7
+ * resources; this shows the cross-layer merge across project → user → extras →
8
+ * system. Kept as a shared lib (not inlined into `view.ts`) so `inspect` can
9
+ * reuse it later.
3
10
  */
4
11
  import * as path from 'path';
5
12
  import chalk from 'chalk';
6
- import { listResources } from '../lib/resources.js';
7
- import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
13
+ import { listResources } from './resources.js';
14
+ import { terminalWidth, truncateToWidth, stringWidth } from './session/width.js';
8
15
  const DRILLABLE_KINDS = [
9
16
  'skills',
10
17
  'commands',
@@ -25,22 +32,7 @@ const KIND_LABELS = {
25
32
  workflows: 'Workflows',
26
33
  subagents: 'Subagents',
27
34
  };
28
- export function registerResourcesCommand(program) {
29
- program
30
- .command('resources')
31
- .description('Show the merged DotAgents resources resolved across project, user, system, and extras')
32
- .option('--merged', 'Show the merged first-wins resource surface (default)')
33
- .addHelpText('after', `
34
- Examples:
35
- agents resources
36
- agents resources --merged
37
- `)
38
- .action((options) => {
39
- void options;
40
- renderMergedResources();
41
- });
42
- }
43
- function renderMergedResources() {
35
+ export function renderMergedResources() {
44
36
  const groups = DRILLABLE_KINDS.map((kind) => ({
45
37
  kind,
46
38
  rows: listResources(kind),
@@ -120,19 +120,6 @@ export interface CopilotPermissionsConfig {
120
120
  allowed_directories?: string[];
121
121
  }>;
122
122
  }
123
- /**
124
- * Convert canonical permission set to Gemini format.
125
- * Gemini reads tool allow/deny lists from settings.json under
126
- * `tools.core` / `tools.exclude`. Bash permissions map to ShellTool(pattern).
127
- * Blanket Bash grants map to bare "ShellTool" (no command filter).
128
- * Non-Bash tool patterns are skipped; Gemini uses different tool names.
129
- */
130
- export declare function convertToGeminiFormat(set: PermissionSet): {
131
- tools: {
132
- core: string[];
133
- exclude?: string[];
134
- };
135
- };
136
123
  export declare function convertToCopilotFormat(set: PermissionSet, location: string): CopilotPermissionsConfig;
137
124
  /** Convert canonical Bash rules into Droid's command arrays. */
138
125
  export declare function convertToDroidFormat(set: PermissionSet): {
@@ -154,25 +141,6 @@ export declare function convertToOpenClawFormat(set: PermissionSet): {
154
141
  alsoAllow: string[];
155
142
  deny: string[];
156
143
  };
157
- export type ForgePermission = 'allow' | 'deny' | 'confirm';
158
- export type ForgeOperation = 'read' | 'write' | 'command' | 'url';
159
- export type ForgePolicy = {
160
- permission: ForgePermission;
161
- rule: Partial<Record<ForgeOperation, string>>;
162
- };
163
- /**
164
- * Convert canonical permissions to ForgeCode's permissions.yaml policy list.
165
- *
166
- * ForgeCode gates built-in tools by operation family (`read`, `write`,
167
- * `command`, `url`) plus glob patterns, optionally scoped by `dir`. The policy
168
- * file is ignored unless `.forge.toml` has `restricted = true`, and MCP tools
169
- * bypass permissions.yaml entirely; agents-cli therefore maps only canonical
170
- * built-in allow/deny rules and does not try to express per-named-MCP-tool
171
- * grants.
172
- */
173
- export declare function convertToForgeFormat(set: PermissionSet): {
174
- policies: ForgePolicy[];
175
- };
176
144
  export declare function convertToHermesFormat(set: PermissionSet): {
177
145
  command_allowlist: string[];
178
146
  approvals: {
@@ -15,6 +15,7 @@ import * as TOML from 'smol-toml';
15
15
  import { getPermissionsDir, getUserPermissionsDir, ensureAgentsDir } from './state.js';
16
16
  import { safeJoin } from './paths.js';
17
17
  import { agentConfigDirName } from './agents.js';
18
+ import { supports } from './capabilities.js';
18
19
  import { updateGeminiSettings } from './gemini-settings.js';
19
20
  const HOME = os.homedir();
20
21
  // PERMISSIONS_CAPABLE_AGENTS removed — use `capableAgents('allowlist')`
@@ -504,43 +505,6 @@ function parseCanonicalPattern(permission) {
504
505
  }
505
506
  /** Blanket-Bash canonical forms that mean "allow any bash command". */
506
507
  const BLANKET_BASH_FORMS = new Set(['Bash', 'Bash(*)', 'Bash(**)']);
507
- /**
508
- * Convert canonical permission set to Gemini format.
509
- * Gemini reads tool allow/deny lists from settings.json under
510
- * `tools.core` / `tools.exclude`. Bash permissions map to ShellTool(pattern).
511
- * Blanket Bash grants map to bare "ShellTool" (no command filter).
512
- * Non-Bash tool patterns are skipped; Gemini uses different tool names.
513
- */
514
- export function convertToGeminiFormat(set) {
515
- const serialize = (permissions) => {
516
- const tools = new Set();
517
- for (const perm of permissions) {
518
- if (BLANKET_BASH_FORMS.has(perm)) {
519
- tools.add('ShellTool');
520
- continue;
521
- }
522
- const parsed = parseCanonicalPattern(perm);
523
- if (!parsed || parsed.tool !== 'bash')
524
- continue;
525
- const command = normalizeBashPattern(parsed.pattern);
526
- if (command === '*') {
527
- tools.add('ShellTool');
528
- }
529
- else {
530
- tools.add(`ShellTool(${command})`);
531
- }
532
- }
533
- return Array.from(tools);
534
- };
535
- const core = serialize(set.allow);
536
- const exclude = serialize(set.deny ?? []);
537
- return {
538
- tools: {
539
- core,
540
- ...(exclude.length ? { exclude } : {}),
541
- },
542
- };
543
- }
544
508
  /**
545
509
  * Strip Claude's `:*` subcommand-wildcard suffix and return a space-glob form.
546
510
  * "mq:*" -> "mq *", "git status" -> "git status", "*" -> "*".
@@ -710,64 +674,6 @@ export function convertToOpenClawFormat(set) {
710
674
  deny: map(set.deny ?? []),
711
675
  };
712
676
  }
713
- const FORGE_OPERATION_BY_CANONICAL = {
714
- bash: 'command',
715
- read: 'read',
716
- grep: 'read',
717
- glob: 'read',
718
- write: 'write',
719
- edit: 'write',
720
- notebookedit: 'write',
721
- webfetch: 'url',
722
- websearch: 'url',
723
- };
724
- function canonicalToForgePolicy(perm, permission) {
725
- if (BLANKET_BASH_FORMS.has(perm)) {
726
- return { permission, rule: { command: '*' } };
727
- }
728
- const parsed = parseCanonicalPattern(perm);
729
- if (!parsed)
730
- return null;
731
- const operation = FORGE_OPERATION_BY_CANONICAL[parsed.tool];
732
- if (!operation)
733
- return null;
734
- const pattern = parsed.tool === 'bash'
735
- ? normalizeBashPattern(parsed.pattern)
736
- : (parsed.tool === 'webfetch' || parsed.tool === 'websearch') && parsed.pattern.startsWith('domain:')
737
- ? `${parsed.pattern.slice('domain:'.length)}*`
738
- : parsed.pattern === '**'
739
- ? '**/*'
740
- : parsed.pattern;
741
- return { permission, rule: { [operation]: pattern } };
742
- }
743
- /**
744
- * Convert canonical permissions to ForgeCode's permissions.yaml policy list.
745
- *
746
- * ForgeCode gates built-in tools by operation family (`read`, `write`,
747
- * `command`, `url`) plus glob patterns, optionally scoped by `dir`. The policy
748
- * file is ignored unless `.forge.toml` has `restricted = true`, and MCP tools
749
- * bypass permissions.yaml entirely; agents-cli therefore maps only canonical
750
- * built-in allow/deny rules and does not try to express per-named-MCP-tool
751
- * grants.
752
- */
753
- export function convertToForgeFormat(set) {
754
- const policies = [];
755
- const seen = new Set();
756
- const add = (policy) => {
757
- if (!policy)
758
- return;
759
- const key = `${policy.permission}|${JSON.stringify(policy.rule)}`;
760
- if (seen.has(key))
761
- return;
762
- seen.add(key);
763
- policies.push(policy);
764
- };
765
- for (const perm of set.allow)
766
- add(canonicalToForgePolicy(perm, 'allow'));
767
- for (const perm of set.deny ?? [])
768
- add(canonicalToForgePolicy(perm, 'deny'));
769
- return { policies };
770
- }
771
677
  export function convertToHermesFormat(set) {
772
678
  const commands = (permissions) => {
773
679
  const out = new Set();
@@ -1467,6 +1373,9 @@ function applyPermissionsToAgent(agentId, set, scope = 'user', cwd, merge = true
1467
1373
  * This writes to {versionHome}/.{agent}/settings.json (or equivalent).
1468
1374
  */
1469
1375
  export function applyPermissionsToVersion(agentId, set, versionHome, merge = true, cwd) {
1376
+ if (!supports(agentId, 'allowlist').ok) {
1377
+ return { success: false, error: `Agent '${agentId}' does not support permissions` };
1378
+ }
1470
1379
  const configDir = path.join(versionHome, agentConfigDirName(agentId));
1471
1380
  try {
1472
1381
  fs.mkdirSync(configDir, { recursive: true });
@@ -1557,38 +1466,6 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
1557
1466
  }
1558
1467
  return { success: true };
1559
1468
  }
1560
- if (agentId === 'gemini') {
1561
- const geminiPerms = convertToGeminiFormat(set);
1562
- const settingsPath = path.join(versionHome, '.gemini', 'settings.json');
1563
- updateGeminiSettings(settingsPath, (settings) => {
1564
- // Remove stale keys written by earlier serializer versions:
1565
- // top-level `permissions`, and Gemini's pre-core/exclude `tools.allowed`.
1566
- delete settings.permissions;
1567
- const tools = (typeof settings.tools === 'object' && settings.tools !== null && !Array.isArray(settings.tools))
1568
- ? settings.tools
1569
- : {};
1570
- delete tools.allowed;
1571
- if (merge) {
1572
- const existingCore = Array.isArray(tools.core) ? tools.core : [];
1573
- const existingExclude = Array.isArray(tools.exclude) ? tools.exclude : [];
1574
- tools.core = Array.from(new Set([...existingCore, ...geminiPerms.tools.core]));
1575
- const mergedExclude = Array.from(new Set([...existingExclude, ...(geminiPerms.tools.exclude ?? [])]));
1576
- if (mergedExclude.length)
1577
- tools.exclude = mergedExclude;
1578
- else
1579
- delete tools.exclude;
1580
- }
1581
- else {
1582
- tools.core = geminiPerms.tools.core;
1583
- if (geminiPerms.tools.exclude?.length)
1584
- tools.exclude = geminiPerms.tools.exclude;
1585
- else
1586
- delete tools.exclude;
1587
- }
1588
- settings.tools = tools;
1589
- });
1590
- return { success: true };
1591
- }
1592
1469
  if (agentId === 'antigravity') {
1593
1470
  const antigravityPerms = convertToAntigravityFormat(set);
1594
1471
  const settingsPath = path.join(versionHome, '.gemini', 'antigravity-cli', 'settings.json');
@@ -1888,36 +1765,6 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
1888
1765
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
1889
1766
  return { success: true };
1890
1767
  }
1891
- if (agentId === 'forge') {
1892
- const permissionsPath = path.join(versionHome, '.forge', 'permissions.yaml');
1893
- let config = {};
1894
- if (fs.existsSync(permissionsPath)) {
1895
- const parsed = yaml.parse(fs.readFileSync(permissionsPath, 'utf-8'));
1896
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1897
- config = parsed;
1898
- }
1899
- }
1900
- const newPolicies = convertToForgeFormat(set).policies;
1901
- if (merge) {
1902
- const existingPolicies = Array.isArray(config.policies) ? config.policies : [];
1903
- const seen = new Set();
1904
- config.policies = [...existingPolicies, ...newPolicies].filter((policy) => {
1905
- if (!policy || typeof policy !== 'object' || Array.isArray(policy))
1906
- return false;
1907
- const key = JSON.stringify(policy);
1908
- if (seen.has(key))
1909
- return false;
1910
- seen.add(key);
1911
- return true;
1912
- });
1913
- }
1914
- else {
1915
- config.policies = newPolicies;
1916
- }
1917
- fs.mkdirSync(path.dirname(permissionsPath), { recursive: true });
1918
- fs.writeFileSync(permissionsPath, yaml.stringify(config), 'utf-8');
1919
- return { success: true };
1920
- }
1921
1768
  if (agentId === 'hermes') {
1922
1769
  const configPath = path.join(versionHome, '.hermes', 'config.yaml');
1923
1770
  let config = {};
@@ -214,7 +214,7 @@ export function multiItemPicker(config) {
214
214
  const theme = makeTheme({});
215
215
  const [status, setStatus] = useState('idle');
216
216
  const [searchTerm, setSearchTerm] = useState(cfg.initialSearch ?? '');
217
- const [previewOpen, setPreviewOpen] = useState(false);
217
+ const [previewOpen, setPreviewOpen] = useState(Boolean(cfg.buildPreview));
218
218
  const [selectedKeys, setSelectedKeys] = useState(new Set());
219
219
  const [active, setActive] = useState(0);
220
220
  const prefix = usePrefix({ status, theme });
@@ -41,13 +41,24 @@ export declare function backgroundSpawnOptions(opts?: {
41
41
  windowsHide: boolean;
42
42
  };
43
43
  /**
44
- * Is a process with this PID currently alive?
44
+ * Whether `pid` has stopped serving dead, or a ZOMBIE awaiting reap.
45
45
  *
46
- * Uses the signal-0 probe, which is cross-platform in Node (Windows included —
47
- * it maps to OpenProcess). Returns false on any error (no such process, or no
48
- * permission to signal it), matching the long-standing call sites that treat a
49
- * throw from `process.kill(pid, 0)` as "not running".
46
+ * `isAlive` is a bare `kill(pid, 0)`, which succeeds for a zombie. That matters
47
+ * here because {@link waitForExit} blocks the event loop, so a daemon that is
48
+ * this process's own child can never be reaped while we wait: it would read as
49
+ * alive for the entire timeout and then be hard-killed after it had already
50
+ * exited. A zombie holds no socket and serves no request, so for stop purposes
51
+ * it has exited.
50
52
  */
53
+ export declare function hasExited(pid: number): boolean;
54
+ /**
55
+ * Block until `pid` stops serving or `timeoutMs` elapses. Synchronous on purpose:
56
+ * the callers are short-lived CLI/postinstall processes that exit before any
57
+ * async timer would fire, which is exactly how a SIGTERMed daemon used to outlive
58
+ * the `stopDaemon()` that was supposed to have reaped it. Returns true if it is
59
+ * gone.
60
+ */
61
+ export declare function waitForExit(pid: number, timeoutMs: number): boolean;
51
62
  export declare function isAlive(pid: number): boolean;
52
63
  /**
53
64
  * A stable identifier for the process at `pid` as of when it started, or null if
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { execFileSync } from 'child_process';
5
5
  import { readFileSync } from 'fs';
6
+ import { sleepSync } from '../fs-atomic.js';
6
7
  /**
7
8
  * Forcefully terminate a process AND its descendant tree.
8
9
  *
@@ -70,6 +71,59 @@ export function backgroundSpawnOptions(opts = {}) {
70
71
  * permission to signal it), matching the long-standing call sites that treat a
71
72
  * throw from `process.kill(pid, 0)` as "not running".
72
73
  */
74
+ /** Poll interval while waiting for a pid to disappear. */
75
+ const EXIT_POLL_MS = 50;
76
+ /**
77
+ * Whether `pid` has stopped serving — dead, or a ZOMBIE awaiting reap.
78
+ *
79
+ * `isAlive` is a bare `kill(pid, 0)`, which succeeds for a zombie. That matters
80
+ * here because {@link waitForExit} blocks the event loop, so a daemon that is
81
+ * this process's own child can never be reaped while we wait: it would read as
82
+ * alive for the entire timeout and then be hard-killed after it had already
83
+ * exited. A zombie holds no socket and serves no request, so for stop purposes
84
+ * it has exited.
85
+ */
86
+ export function hasExited(pid) {
87
+ if (!isAlive(pid))
88
+ return true;
89
+ if (process.platform === 'win32')
90
+ return false; // no zombie state to unwrap
91
+ try {
92
+ const state = execFileSync('ps', ['-o', 'state=', '-p', String(pid)], { encoding: 'utf-8' }).trim();
93
+ return state.startsWith('Z');
94
+ }
95
+ catch (err) {
96
+ // `ps` exiting non-zero because the pid is unknown means gone. Any OTHER
97
+ // failure (ps missing from PATH, a permission error) is NOT evidence of
98
+ // death, and isAlive already said this pid is live — so fail CLOSED and keep
99
+ // treating it as alive. Failing open here would clear the pid file under a
100
+ // running daemon and recreate this very bug through a different door.
101
+ if (err?.code === 'ENOENT' && err?.syscall === 'spawnSync ps')
102
+ return false;
103
+ const out = String(err?.stdout ?? '').trim();
104
+ const errOut = String(err?.stderr ?? '').trim();
105
+ if (err?.status === 1 && out === '' && errOut === '')
106
+ return true; // no such pid
107
+ return false;
108
+ }
109
+ }
110
+ /**
111
+ * Block until `pid` stops serving or `timeoutMs` elapses. Synchronous on purpose:
112
+ * the callers are short-lived CLI/postinstall processes that exit before any
113
+ * async timer would fire, which is exactly how a SIGTERMed daemon used to outlive
114
+ * the `stopDaemon()` that was supposed to have reaped it. Returns true if it is
115
+ * gone.
116
+ */
117
+ export function waitForExit(pid, timeoutMs) {
118
+ const deadline = Date.now() + timeoutMs;
119
+ for (;;) {
120
+ if (hasExited(pid))
121
+ return true;
122
+ if (Date.now() >= deadline)
123
+ return false;
124
+ sleepSync(EXIT_POLL_MS); // blocks this thread outright; no busy-loop
125
+ }
126
+ }
73
127
  export function isAlive(pid) {
74
128
  if (!pid || pid <= 0)
75
129
  return false;
@@ -191,14 +191,6 @@ export declare function resolveOpenCodePluginSources(pluginRoot: string): string
191
191
  export declare function installOpenCodePlugin(plugin: DiscoveredPlugin, versionHome: string): boolean;
192
192
  export declare function isOpenCodePluginInstalled(pluginName: string, versionHome: string): boolean;
193
193
  export declare function removeOpenCodePlugin(pluginName: string, versionHome: string): boolean;
194
- /**
195
- * Gemini CLI extensions live under `$HOME/.gemini/extensions/<name>/` and
196
- * require a `gemini-extension.json` manifest at the extension root.
197
- */
198
- export declare function geminiExtensionsDir(versionHome: string): string;
199
- export declare function installGeminiPlugin(plugin: DiscoveredPlugin, versionHome: string): boolean;
200
- export declare function isGeminiPluginInstalled(pluginName: string, versionHome: string): boolean;
201
- export declare function removeGeminiPlugin(pluginName: string, versionHome: string): boolean;
202
194
  /**
203
195
  * Goose auto-discovers Open Plugins at `$HOME/.agents/plugins/<name>/`.
204
196
  * Under agents-cli version isolation HOME is the version home, so we install to: