@phnx-labs/agents-cli 1.20.51 → 1.20.53

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 (142) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +12 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/browser.js +215 -7
  5. package/dist/commands/cloud.d.ts +3 -0
  6. package/dist/commands/cloud.js +8 -1
  7. package/dist/commands/events.d.ts +1 -1
  8. package/dist/commands/events.js +2 -3
  9. package/dist/commands/exec.js +82 -3
  10. package/dist/commands/factory.js +8 -0
  11. package/dist/commands/feed.d.ts +38 -0
  12. package/dist/commands/feed.js +274 -0
  13. package/dist/commands/logs.d.ts +5 -1
  14. package/dist/commands/logs.js +248 -3
  15. package/dist/commands/mcp.js +7 -0
  16. package/dist/commands/memory.d.ts +9 -0
  17. package/dist/commands/memory.js +164 -0
  18. package/dist/commands/message.d.ts +11 -6
  19. package/dist/commands/message.js +140 -5
  20. package/dist/commands/routines.js +12 -0
  21. package/dist/commands/secrets-migrate.d.ts +2 -1
  22. package/dist/commands/secrets-migrate.js +88 -13
  23. package/dist/commands/secrets.d.ts +22 -0
  24. package/dist/commands/secrets.js +176 -42
  25. package/dist/commands/sessions.js +10 -1
  26. package/dist/commands/teams.js +4 -0
  27. package/dist/commands/worktree.js +4 -2
  28. package/dist/index.js +21 -22
  29. package/dist/lib/agents.js +249 -17
  30. package/dist/lib/answer-router.d.ts +75 -0
  31. package/dist/lib/answer-router.js +149 -0
  32. package/dist/lib/ask-classifier.d.ts +71 -0
  33. package/dist/lib/ask-classifier.js +197 -0
  34. package/dist/lib/browser/login-detection.d.ts +94 -0
  35. package/dist/lib/browser/login-detection.js +274 -0
  36. package/dist/lib/browser/profiles.d.ts +17 -8
  37. package/dist/lib/browser/profiles.js +27 -8
  38. package/dist/lib/browser/secret-ref.d.ts +10 -0
  39. package/dist/lib/browser/secret-ref.js +14 -0
  40. package/dist/lib/browser/service.js +14 -12
  41. package/dist/lib/cloud/antigravity.d.ts +0 -2
  42. package/dist/lib/cloud/antigravity.js +2 -17
  43. package/dist/lib/cloud/codex.js +3 -18
  44. package/dist/lib/cloud/rush.d.ts +15 -0
  45. package/dist/lib/cloud/rush.js +10 -16
  46. package/dist/lib/cloud/stream.js +2 -0
  47. package/dist/lib/cloud/types.d.ts +21 -0
  48. package/dist/lib/cloud/types.js +81 -0
  49. package/dist/lib/crabbox/cli.d.ts +1 -1
  50. package/dist/lib/crabbox/cli.js +12 -2
  51. package/dist/lib/crabbox/lease.d.ts +19 -0
  52. package/dist/lib/crabbox/lease.js +22 -11
  53. package/dist/lib/crabbox/progress.d.ts +62 -0
  54. package/dist/lib/crabbox/progress.js +129 -0
  55. package/dist/lib/crabbox/runtimes.d.ts +38 -1
  56. package/dist/lib/crabbox/runtimes.js +98 -5
  57. package/dist/lib/daemon.d.ts +12 -9
  58. package/dist/lib/daemon.js +32 -17
  59. package/dist/lib/events.d.ts +31 -5
  60. package/dist/lib/events.js +291 -101
  61. package/dist/lib/exec.js +20 -1
  62. package/dist/lib/feed-outcome.d.ts +101 -0
  63. package/dist/lib/feed-outcome.js +244 -0
  64. package/dist/lib/feed-policy.d.ts +30 -0
  65. package/dist/lib/feed-policy.js +133 -0
  66. package/dist/lib/feed.d.ts +180 -0
  67. package/dist/lib/feed.js +627 -0
  68. package/dist/lib/git.d.ts +17 -1
  69. package/dist/lib/git.js +20 -1
  70. package/dist/lib/hooks.js +529 -14
  71. package/dist/lib/hosts/passthrough.d.ts +3 -3
  72. package/dist/lib/hosts/passthrough.js +3 -3
  73. package/dist/lib/mailbox-gc.d.ts +22 -0
  74. package/dist/lib/mailbox-gc.js +161 -0
  75. package/dist/lib/mailbox.d.ts +26 -2
  76. package/dist/lib/mailbox.js +80 -5
  77. package/dist/lib/mcp.js +82 -0
  78. package/dist/lib/memory.d.ts +55 -0
  79. package/dist/lib/memory.js +274 -0
  80. package/dist/lib/notify.d.ts +16 -0
  81. package/dist/lib/notify.js +61 -0
  82. package/dist/lib/operator.d.ts +26 -0
  83. package/dist/lib/operator.js +107 -0
  84. package/dist/lib/plugins.d.ts +35 -0
  85. package/dist/lib/plugins.js +217 -0
  86. package/dist/lib/remote-agents-json.d.ts +14 -0
  87. package/dist/lib/remote-agents-json.js +94 -0
  88. package/dist/lib/resources/mcp.js +44 -0
  89. package/dist/lib/resources/memory.d.ts +15 -0
  90. package/dist/lib/resources/memory.js +46 -0
  91. package/dist/lib/resources/types.d.ts +2 -2
  92. package/dist/lib/rotate.js +2 -0
  93. package/dist/lib/runner.d.ts +43 -0
  94. package/dist/lib/runner.js +323 -74
  95. package/dist/lib/sandbox.js +6 -0
  96. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  97. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  98. package/dist/lib/secrets/agent.d.ts +21 -0
  99. package/dist/lib/secrets/agent.js +63 -1
  100. package/dist/lib/secrets/bundles.d.ts +33 -1
  101. package/dist/lib/secrets/bundles.js +76 -21
  102. package/dist/lib/secrets/icloud-import.d.ts +79 -0
  103. package/dist/lib/secrets/icloud-import.js +203 -0
  104. package/dist/lib/secrets/index.d.ts +142 -2
  105. package/dist/lib/secrets/index.js +695 -30
  106. package/dist/lib/secrets/remote.js +1 -1
  107. package/dist/lib/secrets/sync.js +1 -1
  108. package/dist/lib/session/active.d.ts +18 -0
  109. package/dist/lib/session/active.js +47 -17
  110. package/dist/lib/session/db.d.ts +9 -1
  111. package/dist/lib/session/db.js +18 -3
  112. package/dist/lib/session/discover.d.ts +13 -0
  113. package/dist/lib/session/discover.js +32 -2
  114. package/dist/lib/session/parse.d.ts +8 -0
  115. package/dist/lib/session/parse.js +42 -21
  116. package/dist/lib/session/remote-active.js +8 -89
  117. package/dist/lib/session/state.d.ts +11 -0
  118. package/dist/lib/session/state.js +50 -1
  119. package/dist/lib/session/tail.d.ts +23 -4
  120. package/dist/lib/session/tail.js +34 -16
  121. package/dist/lib/session/throughput.d.ts +30 -0
  122. package/dist/lib/session/throughput.js +86 -0
  123. package/dist/lib/shim-heal.d.ts +12 -3
  124. package/dist/lib/shim-heal.js +12 -6
  125. package/dist/lib/staleness/detectors/subagents.js +57 -3
  126. package/dist/lib/staleness/writers/hooks.js +7 -3
  127. package/dist/lib/staleness/writers/subagents.js +37 -6
  128. package/dist/lib/startup/command-registry.d.ts +2 -0
  129. package/dist/lib/startup/command-registry.js +4 -0
  130. package/dist/lib/state.d.ts +2 -0
  131. package/dist/lib/state.js +25 -8
  132. package/dist/lib/subagents.d.ts +52 -0
  133. package/dist/lib/subagents.js +315 -12
  134. package/dist/lib/teams/agents.js +6 -3
  135. package/dist/lib/teams/worktree.d.ts +8 -0
  136. package/dist/lib/teams/worktree.js +8 -0
  137. package/dist/lib/types.d.ts +20 -2
  138. package/dist/lib/versions.js +38 -48
  139. package/dist/lib/whats-new.d.ts +5 -3
  140. package/dist/lib/whats-new.js +25 -5
  141. package/package.json +4 -3
  142. package/scripts/postinstall.js +61 -1
package/dist/lib/hooks.js CHANGED
@@ -994,6 +994,18 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
994
994
  if (agentId === 'kimi') {
995
995
  return registerHooksForKimi(versionHome, manifest, resolveScript, managedPrefixes);
996
996
  }
997
+ if (agentId === 'copilot') {
998
+ return registerHooksForCopilot(versionHome, manifest, resolveScript, managedPrefixes);
999
+ }
1000
+ if (agentId === 'kiro') {
1001
+ return registerHooksForKiro(versionHome, manifest, resolveScript, managedPrefixes);
1002
+ }
1003
+ if (agentId === 'goose') {
1004
+ return registerHooksForGoose(versionHome, manifest, resolveScript, managedPrefixes);
1005
+ }
1006
+ if (agentId === 'cursor') {
1007
+ return registerHooksForCursor(versionHome, manifest, resolveScript, managedPrefixes);
1008
+ }
997
1009
  return { registered: [], errors: [] };
998
1010
  }
999
1011
  /**
@@ -1028,9 +1040,11 @@ function registerHooksForClaude(versionHome, manifest, resolveScript, managedPre
1028
1040
  const configDir = path.join(versionHome, configDirName);
1029
1041
  const settingsPath = path.join(configDir, 'settings.json');
1030
1042
  let config = {};
1043
+ let existingRaw;
1031
1044
  if (fs.existsSync(settingsPath)) {
1032
1045
  try {
1033
- config = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
1046
+ existingRaw = fs.readFileSync(settingsPath, 'utf-8');
1047
+ config = JSON.parse(existingRaw);
1034
1048
  }
1035
1049
  catch {
1036
1050
  errors.push('Failed to parse settings.json');
@@ -1111,7 +1125,10 @@ function registerHooksForClaude(versionHome, manifest, resolveScript, managedPre
1111
1125
  }
1112
1126
  try {
1113
1127
  fs.mkdirSync(configDir, { recursive: true });
1114
- fs.writeFileSync(settingsPath, JSON.stringify(config, null, 2), 'utf-8');
1128
+ const nextRaw = JSON.stringify(config, null, 2);
1129
+ if (existingRaw !== nextRaw) {
1130
+ fs.writeFileSync(settingsPath, nextRaw, 'utf-8');
1131
+ }
1115
1132
  }
1116
1133
  catch (err) {
1117
1134
  errors.push(`Failed to write settings.json: ${err.message}`);
@@ -1508,9 +1525,13 @@ function registerHooksForAntigravity(versionHome, manifest, resolveScript, manag
1508
1525
  if (!hooks[agyEvent]) {
1509
1526
  hooks[agyEvent] = [];
1510
1527
  }
1528
+ // Antigravity settings entries: command + optional matcher (tool scope).
1529
+ // Without matcher every PreToolUse guard fires on ALL tools (RUSH-1353).
1511
1530
  const list = hooks[agyEvent];
1512
1531
  const existingIdx = list.findIndex((e) => e && typeof e === 'object' && e.command === commandPath);
1513
1532
  const entry = { command: commandPath };
1533
+ if (hookDef.matcher)
1534
+ entry.matcher = hookDef.matcher;
1514
1535
  if (existingIdx >= 0) {
1515
1536
  list[existingIdx] = entry;
1516
1537
  }
@@ -1529,9 +1550,43 @@ function registerHooksForAntigravity(versionHome, manifest, resolveScript, manag
1529
1550
  }
1530
1551
  return { registered, errors };
1531
1552
  }
1553
+ /**
1554
+ * Grok events that accept a `matcher` field. Per the Grok hooks guide
1555
+ * (docs/user-guide/10-hooks.md, "Key Fields"): the matcher applies to the tool
1556
+ * events — `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionDenied`
1557
+ * (matches the tool name) — and to `Notification` (matches the notification
1558
+ * type). The lifecycle events `SessionStart`, `SessionEnd`, `Stop`,
1559
+ * `UserPromptSubmit` REJECT a matcher (they raise loading errors); other events
1560
+ * ignore it. Of the events this registrar emits (see GROK_EVENT_MAP), only these
1561
+ * three accept a matcher, so we emit it for these alone.
1562
+ */
1563
+ const GROK_MATCHER_EVENTS = new Set(['PreToolUse', 'PostToolUse', 'Notification']);
1564
+ /**
1565
+ * Tool-name matcher aliases for tools Grok does NOT auto-alias. Grok maps common
1566
+ * Claude tool names to its own (Bash → run_terminal_command, Read → read_file,
1567
+ * etc. — docs/user-guide/10-hooks.md, "Tool Name Aliases") and "a matcher keeps
1568
+ * its original name too", so most matchers need no translation. But there is no
1569
+ * alias for `ExitPlanMode`: Grok's plan tools are `enter_plan_mode` /
1570
+ * `exit_plan_mode` (docs/user-guide/19-plan-mode.md line 14), so a bare
1571
+ * `ExitPlanMode` matcher would never fire. Broaden it to a regex that matches
1572
+ * both names. Keep this an explicit, minimal map — not a general translation
1573
+ * engine.
1574
+ */
1575
+ const GROK_MATCHER_ALIASES = {
1576
+ ExitPlanMode: 'ExitPlanMode|exit_plan_mode',
1577
+ };
1532
1578
  /**
1533
1579
  * Register hooks for Grok Build.
1534
- * Grok uses per-event JSON files under .grok/hooks/ (e.g. session-start.json).
1580
+ *
1581
+ * Grok merges ALL of `~/.grok/hooks/*.json` (docs/user-guide/10-hooks.md, "Hook
1582
+ * Locations"), so this registrar writes exactly ONE manifest file, `hooks.json`,
1583
+ * and prunes any stale per-event files (`pretooluse.json`, …) that an older
1584
+ * build wrote — otherwise every hook would run twice per event, forever, on
1585
+ * already-synced installs.
1586
+ *
1587
+ * Matchers are grouped one-per-distinct-matcher (like the Claude writer) and are
1588
+ * emitted only for the events that accept them (GROK_MATCHER_EVENTS); tool-name
1589
+ * matchers Grok does not auto-alias are translated via GROK_MATCHER_ALIASES.
1535
1590
  */
1536
1591
  function registerHooksForGrok(versionHome, manifest, resolveScript, managedPrefixes) {
1537
1592
  const registered = [];
@@ -1563,12 +1618,33 @@ function registerHooksForGrok(versionHome, manifest, resolveScript, managedPrefi
1563
1618
  if (!grokHooks.hooks[grokEvent]) {
1564
1619
  grokHooks.hooks[grokEvent] = [];
1565
1620
  }
1566
- grokHooks.hooks[grokEvent].push({
1567
- hooks: [{ type: 'command', command: commandPath, timeout }],
1568
- });
1621
+ const groups = grokHooks.hooks[grokEvent];
1622
+ // Emit the matcher only for events that accept one; translate tool names
1623
+ // Grok does not auto-alias. Empty/omitted matcher matches everything.
1624
+ let matcher;
1625
+ if (GROK_MATCHER_EVENTS.has(grokEvent) && hookDef.matcher) {
1626
+ matcher = GROK_MATCHER_ALIASES[hookDef.matcher] ?? hookDef.matcher;
1627
+ }
1628
+ // Group by matcher the way the Claude writer does: one group per distinct
1629
+ // matcher, hooks appended into the group — not one group per hook.
1630
+ let group = groups.find((g) => (g.matcher ?? '') === (matcher ?? ''));
1631
+ if (!group) {
1632
+ group = matcher ? { matcher, hooks: [] } : { hooks: [] };
1633
+ groups.push(group);
1634
+ }
1635
+ const hookEntry = { type: 'command', command: commandPath, timeout };
1636
+ const existingIdx = group.hooks.findIndex((h) => h.command === commandPath);
1637
+ if (existingIdx >= 0) {
1638
+ group.hooks[existingIdx] = hookEntry;
1639
+ }
1640
+ else {
1641
+ group.hooks.push(hookEntry);
1642
+ }
1569
1643
  registered.push(`${name} -> ${grokEvent}`);
1570
1644
  }
1571
1645
  }
1646
+ // Single source of truth: hooks.json. Written fresh from the manifest every
1647
+ // sync, so the registrar owns it outright.
1572
1648
  const mainHooksPath = path.join(grokHooksDir, 'hooks.json');
1573
1649
  try {
1574
1650
  fs.writeFileSync(mainHooksPath, JSON.stringify(grokHooks, null, 2));
@@ -1576,18 +1652,66 @@ function registerHooksForGrok(versionHome, manifest, resolveScript, managedPrefi
1576
1652
  catch (e) {
1577
1653
  errors.push(`Failed to write hooks.json: ${e.message}`);
1578
1654
  }
1579
- for (const [eventName, groups] of Object.entries(grokHooks.hooks)) {
1580
- const fileName = eventName.toLowerCase().replace(/([a-z])([A-Z])/g, '$1-$2') + '.json';
1581
- const eventFile = path.join(grokHooksDir, fileName);
1582
- try {
1583
- fs.writeFileSync(eventFile, JSON.stringify({ hooks: { [eventName]: groups } }, null, 2));
1584
- }
1585
- catch (e) {
1586
- errors.push(`Failed to write ${fileName}: ${e.message}`);
1655
+ // Clean up stale per-event files (pretooluse.json, session-start.json, …) that
1656
+ // older builds double-wrote. Grok merges every *.json in this dir, so leaving
1657
+ // them would run each hook twice per event. Only remove files whose contents
1658
+ // are entirely managed by us (a lone `hooks` object referencing our managed
1659
+ // command paths); a user's own custom *.json is left untouched.
1660
+ try {
1661
+ for (const file of fs.readdirSync(grokHooksDir)) {
1662
+ if (!file.endsWith('.json') || file === 'hooks.json')
1663
+ continue;
1664
+ const filePath = path.join(grokHooksDir, file);
1665
+ if (isManagedGrokHookFile(filePath, managedPrefixes)) {
1666
+ fs.rmSync(filePath, { force: true });
1667
+ }
1587
1668
  }
1588
1669
  }
1670
+ catch (e) {
1671
+ errors.push(`Failed to prune stale grok hook files: ${e.message}`);
1672
+ }
1589
1673
  return { registered, errors };
1590
1674
  }
1675
+ /**
1676
+ * A per-event Grok hook file is "ours" (safe to prune) when it is a
1677
+ * `{ hooks: { <Event>: [...] } }` document in which every command entry points
1678
+ * at a managed path (see isManagedHookCommand). This is exactly the shape older
1679
+ * builds double-wrote; a user's hand-authored *.json that mixes in unmanaged
1680
+ * commands is preserved. A file we can't parse is treated as not-ours.
1681
+ */
1682
+ function isManagedGrokHookFile(filePath, managedPrefixes) {
1683
+ let parsed;
1684
+ try {
1685
+ parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
1686
+ }
1687
+ catch {
1688
+ return false;
1689
+ }
1690
+ if (!parsed || typeof parsed !== 'object')
1691
+ return false;
1692
+ const hooks = parsed.hooks;
1693
+ if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks))
1694
+ return false;
1695
+ const commands = [];
1696
+ for (const groups of Object.values(hooks)) {
1697
+ if (!Array.isArray(groups))
1698
+ return false;
1699
+ for (const group of groups) {
1700
+ const entries = group.hooks;
1701
+ if (!Array.isArray(entries))
1702
+ return false;
1703
+ for (const entry of entries) {
1704
+ const cmd = entry.command;
1705
+ if (typeof cmd !== 'string')
1706
+ return false;
1707
+ commands.push(cmd);
1708
+ }
1709
+ }
1710
+ }
1711
+ if (commands.length === 0)
1712
+ return false;
1713
+ return commands.every((cmd) => isManagedHookCommand(cmd, managedPrefixes));
1714
+ }
1591
1715
  function registerHooksForKimi(versionHome, manifest, resolveScript, managedPrefixes) {
1592
1716
  const registered = [];
1593
1717
  const errors = [];
@@ -1671,3 +1795,394 @@ function registerHooksForKimi(versionHome, manifest, resolveScript, managedPrefi
1671
1795
  }
1672
1796
  return { registered, errors };
1673
1797
  }
1798
+ /**
1799
+ * Canonical hooks.yaml event names → Copilot camelCase event names.
1800
+ * Copilot accepts both camelCase (native) and PascalCase (VS Code-compatible).
1801
+ * We emit camelCase, the format documented at
1802
+ * https://docs.github.com/en/copilot/reference/hooks-configuration.
1803
+ * Unmapped events are skipped so a Claude-only event does not land in the file.
1804
+ */
1805
+ const COPILOT_EVENT_MAP = {
1806
+ SessionStart: 'sessionStart',
1807
+ SessionEnd: 'sessionEnd',
1808
+ UserPromptSubmit: 'userPromptSubmitted',
1809
+ PreToolUse: 'preToolUse',
1810
+ PostToolUse: 'postToolUse',
1811
+ PostToolUseFailure: 'postToolUseFailure',
1812
+ Stop: 'agentStop',
1813
+ SubagentStart: 'subagentStart',
1814
+ SubagentStop: 'subagentStop',
1815
+ OnError: 'errorOccurred',
1816
+ PreCompact: 'preCompact',
1817
+ Notification: 'notification',
1818
+ PermissionRequest: 'permissionRequest',
1819
+ };
1820
+ /**
1821
+ * Copilot events that accept a `matcher` field (regex, full-string match).
1822
+ * See "Matcher filtering" in the Copilot hooks reference.
1823
+ */
1824
+ const COPILOT_MATCHER_EVENTS = new Set([
1825
+ 'preToolUse',
1826
+ 'postToolUse',
1827
+ 'permissionRequest',
1828
+ 'preCompact',
1829
+ 'notification',
1830
+ 'subagentStart',
1831
+ ]);
1832
+ /** Managed filename under ~/.copilot/hooks/ — we own this file entirely. */
1833
+ const COPILOT_MANAGED_HOOKS_FILE = 'agents-cli-hooks.json';
1834
+ /**
1835
+ * Register hooks for GitHub Copilot CLI.
1836
+ *
1837
+ * Copilot loads every `*.json` under `~/.copilot/hooks/` (and project
1838
+ * `.github/hooks/`). Schema: `{ "version": 1, "hooks": { event: [entries] } }`
1839
+ * with command entries `{ type: "command", bash|command, timeoutSec?, matcher? }`.
1840
+ *
1841
+ * We rewrite a single managed file (`agents-cli-hooks.json`) on every sync so
1842
+ * GC is trivial — user-authored sibling JSON files are never touched.
1843
+ */
1844
+ function registerHooksForCopilot(versionHome, manifest, resolveScript, _managedPrefixes) {
1845
+ const registered = [];
1846
+ const errors = [];
1847
+ const copilotHooksDir = path.join(versionHome, '.copilot', 'hooks');
1848
+ fs.mkdirSync(copilotHooksDir, { recursive: true });
1849
+ const hooks = {};
1850
+ for (const [name, hookDef] of Object.entries(manifest)) {
1851
+ if (!hookDef.events || hookDef.events.length === 0)
1852
+ continue;
1853
+ const commandPath = resolveHookCommand(name, hookDef, resolveScript);
1854
+ if (!commandPath) {
1855
+ errors.push(`${name}: script not found in user or system hooks dir`);
1856
+ continue;
1857
+ }
1858
+ const timeoutSec = hookDef.timeout ?? 30;
1859
+ for (const event of hookDef.events) {
1860
+ const copilotEvent = COPILOT_EVENT_MAP[event];
1861
+ if (!copilotEvent)
1862
+ continue; // unmapped — skip silently
1863
+ if (!hooks[copilotEvent])
1864
+ hooks[copilotEvent] = [];
1865
+ const entry = {
1866
+ type: 'command',
1867
+ command: commandPath,
1868
+ timeoutSec,
1869
+ };
1870
+ if (COPILOT_MATCHER_EVENTS.has(copilotEvent) && hookDef.matcher) {
1871
+ entry.matcher = hookDef.matcher;
1872
+ }
1873
+ // De-dupe on (event, command, matcher) so repeated sync is idempotent.
1874
+ const existingIdx = hooks[copilotEvent].findIndex((h) => h.command === entry.command && (h.matcher ?? '') === (entry.matcher ?? ''));
1875
+ if (existingIdx >= 0) {
1876
+ hooks[copilotEvent][existingIdx] = entry;
1877
+ }
1878
+ else {
1879
+ hooks[copilotEvent].push(entry);
1880
+ }
1881
+ registered.push(`${name} -> ${copilotEvent}`);
1882
+ }
1883
+ }
1884
+ const outPath = path.join(copilotHooksDir, COPILOT_MANAGED_HOOKS_FILE);
1885
+ try {
1886
+ // Always rewrite: empty manifest → empty hooks object (GC of prior managed entries).
1887
+ fs.writeFileSync(outPath, JSON.stringify({ version: 1, hooks }, null, 2) + '\n', 'utf-8');
1888
+ }
1889
+ catch (err) {
1890
+ errors.push(`Failed to write ${COPILOT_MANAGED_HOOKS_FILE}: ${err.message}`);
1891
+ }
1892
+ return { registered, errors };
1893
+ }
1894
+ /**
1895
+ * Canonical hooks.yaml event names → Kiro v3 trigger names.
1896
+ * Kiro v3 uses PascalCase triggers under `.kiro/hooks/*.json`
1897
+ * (https://kiro.dev/docs/cli/v3/hooks/). Unmapped events are skipped.
1898
+ */
1899
+ const KIRO_EVENT_MAP = {
1900
+ SessionStart: 'SessionStart',
1901
+ Stop: 'Stop',
1902
+ PreToolUse: 'PreToolUse',
1903
+ PostToolUse: 'PostToolUse',
1904
+ UserPromptSubmit: 'UserPromptSubmit',
1905
+ };
1906
+ /**
1907
+ * Kiro triggers that evaluate the `matcher` regex (tool name, file path, or
1908
+ * prompt text depending on the trigger). Lifecycle SessionStart/Stop always fire.
1909
+ */
1910
+ const KIRO_MATCHER_EVENTS = new Set([
1911
+ 'PreToolUse',
1912
+ 'PostToolUse',
1913
+ 'UserPromptSubmit',
1914
+ ]);
1915
+ /** Managed filename under ~/.kiro/hooks/ — we own this file entirely. */
1916
+ const KIRO_MANAGED_HOOKS_FILE = 'agents-cli-hooks.json';
1917
+ /**
1918
+ * Register hooks for Kiro CLI (v3 standalone hooks format).
1919
+ *
1920
+ * Each file under `~/.kiro/hooks/*.json` is:
1921
+ * { "version": "v1", "hooks": [ { name, trigger, matcher?, action, timeout?, enabled? } ] }
1922
+ *
1923
+ * We rewrite a single managed file so GC is a rewrite and user-authored sibling
1924
+ * JSON files are never touched. Embedded agent-config hooks (2.x) still work
1925
+ * in Kiro but we only write the v3 path.
1926
+ */
1927
+ function registerHooksForKiro(versionHome, manifest, resolveScript, _managedPrefixes) {
1928
+ const registered = [];
1929
+ const errors = [];
1930
+ const kiroHooksDir = path.join(versionHome, '.kiro', 'hooks');
1931
+ fs.mkdirSync(kiroHooksDir, { recursive: true });
1932
+ const hooks = [];
1933
+ for (const [name, hookDef] of Object.entries(manifest)) {
1934
+ if (!hookDef.events || hookDef.events.length === 0)
1935
+ continue;
1936
+ const commandPath = resolveHookCommand(name, hookDef, resolveScript);
1937
+ if (!commandPath) {
1938
+ errors.push(`${name}: script not found in user or system hooks dir`);
1939
+ continue;
1940
+ }
1941
+ const timeout = hookDef.timeout ?? 60;
1942
+ for (const event of hookDef.events) {
1943
+ const trigger = KIRO_EVENT_MAP[event];
1944
+ if (!trigger)
1945
+ continue;
1946
+ const entry = {
1947
+ name,
1948
+ trigger,
1949
+ action: { type: 'command', command: commandPath },
1950
+ timeout,
1951
+ enabled: true,
1952
+ };
1953
+ if (KIRO_MATCHER_EVENTS.has(trigger) && hookDef.matcher) {
1954
+ entry.matcher = hookDef.matcher;
1955
+ }
1956
+ // De-dupe on (name, trigger, matcher)
1957
+ const existingIdx = hooks.findIndex((h) => h.name === entry.name &&
1958
+ h.trigger === entry.trigger &&
1959
+ (h.matcher ?? '') === (entry.matcher ?? ''));
1960
+ if (existingIdx >= 0) {
1961
+ hooks[existingIdx] = entry;
1962
+ }
1963
+ else {
1964
+ hooks.push(entry);
1965
+ }
1966
+ registered.push(`${name} -> ${trigger}`);
1967
+ }
1968
+ }
1969
+ const outPath = path.join(kiroHooksDir, KIRO_MANAGED_HOOKS_FILE);
1970
+ try {
1971
+ fs.writeFileSync(outPath, JSON.stringify({ version: 'v1', hooks }, null, 2) + '\n', 'utf-8');
1972
+ }
1973
+ catch (err) {
1974
+ errors.push(`Failed to write ${KIRO_MANAGED_HOOKS_FILE}: ${err.message}`);
1975
+ }
1976
+ return { registered, errors };
1977
+ }
1978
+ /**
1979
+ * Canonical hooks.yaml event names that goose supports (Open Plugins PascalCase).
1980
+ * Unmapped events are skipped. Goose ≥ 1.34.0.
1981
+ */
1982
+ const GOOSE_EVENT_MAP = {
1983
+ SessionStart: 'SessionStart',
1984
+ SessionEnd: 'SessionEnd',
1985
+ Stop: 'Stop',
1986
+ UserPromptSubmit: 'UserPromptSubmit',
1987
+ PreToolUse: 'PreToolUse',
1988
+ PostToolUse: 'PostToolUse',
1989
+ PostToolUseFailure: 'PostToolUseFailure',
1990
+ BeforeReadFile: 'BeforeReadFile',
1991
+ AfterFileEdit: 'AfterFileEdit',
1992
+ BeforeShellExecution: 'BeforeShellExecution',
1993
+ AfterShellExecution: 'AfterShellExecution',
1994
+ // SubagentStart/SubagentStop are not emitted by Goose — do not advertise them.
1995
+ };
1996
+ /** Managed Open Plugins directory name under ~/.agents/plugins/. */
1997
+ const GOOSE_MANAGED_PLUGIN_NAME = 'agents-cli-hooks';
1998
+ /**
1999
+ * Register hooks for Goose (block-goose-cli ≥ 1.34.0).
2000
+ *
2001
+ * Goose auto-discovers Open Plugins under `$HOME/.agents/plugins/<name>/` that
2002
+ * contain `hooks/hooks.json` (HOME is the version home under the agents-cli
2003
+ * shim). Schema (Open Plugins / Claude-shaped):
2004
+ * { "hooks": { Event: [ { matcher?, hooks: [{ type: "command", command }] } ] } }
2005
+ *
2006
+ * We own a single managed plugin (`agents-cli-hooks`) under the version home
2007
+ * and rewrite its hooks.json on every sync. Command paths are absolute
2008
+ * (portable ~/ form) pointing at the already-copied hook scripts.
2009
+ */
2010
+ function registerHooksForGoose(versionHome, manifest, resolveScript, _managedPrefixes) {
2011
+ const registered = [];
2012
+ const errors = [];
2013
+ const eventHooks = {};
2014
+ for (const [name, hookDef] of Object.entries(manifest)) {
2015
+ if (!hookDef.events || hookDef.events.length === 0)
2016
+ continue;
2017
+ const commandPath = resolveHookCommand(name, hookDef, resolveScript);
2018
+ if (!commandPath) {
2019
+ errors.push(`${name}: script not found in user or system hooks dir`);
2020
+ continue;
2021
+ }
2022
+ const timeout = hookDef.timeout ?? 60;
2023
+ const cmd = { type: 'command', command: commandPath, timeout };
2024
+ for (const event of hookDef.events) {
2025
+ const gooseEvent = GOOSE_EVENT_MAP[event];
2026
+ if (!gooseEvent)
2027
+ continue;
2028
+ if (!eventHooks[gooseEvent])
2029
+ eventHooks[gooseEvent] = [];
2030
+ const groups = eventHooks[gooseEvent];
2031
+ const matcher = hookDef.matcher || undefined;
2032
+ let group = groups.find((g) => (g.matcher || undefined) === matcher);
2033
+ if (!group) {
2034
+ group = { hooks: [] };
2035
+ if (matcher)
2036
+ group.matcher = matcher;
2037
+ groups.push(group);
2038
+ }
2039
+ const existingIdx = group.hooks.findIndex((h) => h.command === cmd.command);
2040
+ if (existingIdx >= 0) {
2041
+ group.hooks[existingIdx] = cmd;
2042
+ }
2043
+ else {
2044
+ group.hooks.push(cmd);
2045
+ }
2046
+ registered.push(`${name} -> ${gooseEvent}`);
2047
+ }
2048
+ }
2049
+ // Goose discovers Open Plugins at ~/.agents/plugins/<name>/ when HOME is the
2050
+ // version home (shim sets HOME). Write under versionHome so each installed
2051
+ // goose version gets its own managed plugin and tests stay hermetic.
2052
+ const pluginRoot = path.join(versionHome, '.agents', 'plugins', GOOSE_MANAGED_PLUGIN_NAME);
2053
+ const hooksDir = path.join(pluginRoot, 'hooks');
2054
+ const outPath = path.join(hooksDir, 'hooks.json');
2055
+ try {
2056
+ fs.mkdirSync(hooksDir, { recursive: true });
2057
+ // Minimal plugin marker so the directory is a valid Open Plugins bundle.
2058
+ const markerPath = path.join(pluginRoot, '.agents-cli-managed');
2059
+ if (!fs.existsSync(markerPath)) {
2060
+ fs.writeFileSync(markerPath, 'managed by agents-cli hooks sync\n', 'utf-8');
2061
+ }
2062
+ fs.writeFileSync(outPath, JSON.stringify({ hooks: eventHooks }, null, 2) + '\n', 'utf-8');
2063
+ }
2064
+ catch (err) {
2065
+ errors.push(`Failed to write goose hooks plugin: ${err.message}`);
2066
+ }
2067
+ return { registered, errors };
2068
+ }
2069
+ /**
2070
+ * Canonical → Cursor CLI camelCase events.
2071
+ *
2072
+ * Only events verified to fire in cursor-agent CLI (not full IDE parity).
2073
+ * Ticket RUSH-1326 note (2026-06): working = sessionStart, stop, preToolUse,
2074
+ * postToolUse, beforeShellExecution, afterShellExecution, beforeReadFile,
2075
+ * afterFileEdit. Also map SessionEnd / beforeSubmitPrompt / preCompact /
2076
+ * subagent* which Cursor documents for agent hooks.
2077
+ * Unmapped events (e.g. afterAgentResponse) are skipped.
2078
+ */
2079
+ const CURSOR_EVENT_MAP = {
2080
+ SessionStart: 'sessionStart',
2081
+ SessionEnd: 'sessionEnd',
2082
+ Stop: 'stop',
2083
+ UserPromptSubmit: 'beforeSubmitPrompt',
2084
+ PreToolUse: 'preToolUse',
2085
+ PostToolUse: 'postToolUse',
2086
+ PostToolUseFailure: 'postToolUseFailure',
2087
+ PreCompact: 'preCompact',
2088
+ SubagentStart: 'subagentStart',
2089
+ SubagentStop: 'subagentStop',
2090
+ BeforeShellExecution: 'beforeShellExecution',
2091
+ AfterShellExecution: 'afterShellExecution',
2092
+ BeforeReadFile: 'beforeReadFile',
2093
+ AfterFileEdit: 'afterFileEdit',
2094
+ };
2095
+ /**
2096
+ * Register hooks for Cursor CLI (`cursor-agent`).
2097
+ *
2098
+ * Writes `~/.cursor/hooks.json` (under version home):
2099
+ * { "version": 1, "hooks": { event: [{ command, timeout?, matcher? }] } }
2100
+ *
2101
+ * GC: rewrite managed entries by command path under managedPrefixes; preserve
2102
+ * user-authored entries whose command is outside managed roots.
2103
+ */
2104
+ function registerHooksForCursor(versionHome, manifest, resolveScript, managedPrefixes) {
2105
+ const registered = [];
2106
+ const errors = [];
2107
+ const configDir = path.join(versionHome, '.cursor');
2108
+ const hooksPath = path.join(configDir, 'hooks.json');
2109
+ let existing = { version: 1, hooks: {} };
2110
+ if (fs.existsSync(hooksPath)) {
2111
+ try {
2112
+ existing = JSON.parse(fs.readFileSync(hooksPath, 'utf-8'));
2113
+ if (!existing.hooks || typeof existing.hooks !== 'object')
2114
+ existing.hooks = {};
2115
+ }
2116
+ catch {
2117
+ errors.push('Failed to parse existing hooks.json');
2118
+ return { registered, errors };
2119
+ }
2120
+ }
2121
+ // Desired managed entries keyed by event|command|matcher so a matcher or
2122
+ // event change drops the stale entry instead of retaining it by command path alone.
2123
+ const desiredManaged = new Set();
2124
+ for (const [hookName, hookDef] of Object.entries(manifest)) {
2125
+ if (!hookDef.events || hookDef.events.length === 0)
2126
+ continue;
2127
+ const resolved = resolveHookCommand(hookName, hookDef, resolveScript);
2128
+ if (!resolved)
2129
+ continue;
2130
+ for (const event of hookDef.events) {
2131
+ const cursorEvent = CURSOR_EVENT_MAP[event];
2132
+ if (!cursorEvent)
2133
+ continue;
2134
+ desiredManaged.add(`${cursorEvent}|${resolved}|${hookDef.matcher ?? ''}`);
2135
+ }
2136
+ }
2137
+ // GC managed entries that are no longer in the manifest (by event+command+matcher)
2138
+ const hooks = {};
2139
+ for (const [event, entries] of Object.entries(existing.hooks || {})) {
2140
+ if (!Array.isArray(entries))
2141
+ continue;
2142
+ hooks[event] = entries.filter((e) => {
2143
+ if (typeof e?.command !== 'string')
2144
+ return true;
2145
+ if (!isManagedHookCommand(e.command, managedPrefixes))
2146
+ return true;
2147
+ return desiredManaged.has(`${event}|${e.command}|${e.matcher ?? ''}`);
2148
+ });
2149
+ if (hooks[event].length === 0)
2150
+ delete hooks[event];
2151
+ }
2152
+ for (const [name, hookDef] of Object.entries(manifest)) {
2153
+ if (!hookDef.events || hookDef.events.length === 0)
2154
+ continue;
2155
+ const commandPath = resolveHookCommand(name, hookDef, resolveScript);
2156
+ if (!commandPath) {
2157
+ errors.push(`${name}: script not found in user or system hooks dir`);
2158
+ continue;
2159
+ }
2160
+ const timeout = hookDef.timeout ?? 30;
2161
+ for (const event of hookDef.events) {
2162
+ const cursorEvent = CURSOR_EVENT_MAP[event];
2163
+ if (!cursorEvent)
2164
+ continue;
2165
+ if (!hooks[cursorEvent])
2166
+ hooks[cursorEvent] = [];
2167
+ const entry = { command: commandPath, timeout };
2168
+ if (hookDef.matcher)
2169
+ entry.matcher = hookDef.matcher;
2170
+ const existingIdx = hooks[cursorEvent].findIndex((h) => h.command === entry.command && (h.matcher ?? '') === (entry.matcher ?? ''));
2171
+ if (existingIdx >= 0) {
2172
+ hooks[cursorEvent][existingIdx] = entry;
2173
+ }
2174
+ else {
2175
+ hooks[cursorEvent].push(entry);
2176
+ }
2177
+ registered.push(`${name} -> ${cursorEvent}`);
2178
+ }
2179
+ }
2180
+ try {
2181
+ fs.mkdirSync(configDir, { recursive: true });
2182
+ fs.writeFileSync(hooksPath, JSON.stringify({ version: 1, hooks }, null, 2) + '\n', 'utf-8');
2183
+ }
2184
+ catch (err) {
2185
+ errors.push(`Failed to write hooks.json: ${err.message}`);
2186
+ }
2187
+ return { registered, errors };
2188
+ }
@@ -12,9 +12,9 @@
12
12
  * long-running case — `teams start --watch` — dispatches detached so the remote
13
13
  * supervisor outlives a dropped connection.
14
14
  *
15
- * `run` and `sessions` are deliberately absent from the table below: they own
16
- * richer `--host` handling in their own command actions (detached run dispatch;
17
- * multi-host session fan-out) and must fall through to it.
15
+ * `run`, `sessions`, and `feed` are deliberately absent from the table below:
16
+ * they own richer `--host` handling in their own command actions (detached run
17
+ * dispatch or multi-host fan-out) and must fall through to it.
18
18
  */
19
19
  /** Pull the value of `--host`/`-H`/`--remote-cwd` (any form) out of an argv. */
20
20
  export declare function flagValue(args: string[], long: string, short?: string): string | undefined;
@@ -12,9 +12,9 @@
12
12
  * long-running case — `teams start --watch` — dispatches detached so the remote
13
13
  * supervisor outlives a dropped connection.
14
14
  *
15
- * `run` and `sessions` are deliberately absent from the table below: they own
16
- * richer `--host` handling in their own command actions (detached run dispatch;
17
- * multi-host session fan-out) and must fall through to it.
15
+ * `run`, `sessions`, and `feed` are deliberately absent from the table below:
16
+ * they own richer `--host` handling in their own command actions (detached run
17
+ * dispatch or multi-host fan-out) and must fall through to it.
18
18
  */
19
19
  import chalk from 'chalk';
20
20
  import { assertValidSshTarget, sshStream } from '../ssh-exec.js';
@@ -0,0 +1,22 @@
1
+ export interface GcResult {
2
+ boxesScanned: number;
3
+ deadBoxes: number;
4
+ messagesDroppedExpired: number;
5
+ messagesDroppedDead: number;
6
+ consumedPruned: number;
7
+ blocksRemoved: number;
8
+ }
9
+ export interface GcOptions {
10
+ root?: string;
11
+ /** Feed store root. Defaults to getFeedDir(). */
12
+ feedRoot?: string;
13
+ now?: Date;
14
+ /** Age in minutes after which consumed entries are pruned. Default 24h. */
15
+ maxConsumedAgeMinutes?: number;
16
+ }
17
+ /**
18
+ * Sweep all mailboxes. For dead boxes (not in `activeBoxIds`), archive every
19
+ * pending message as `dropped: dead` and remove any feed block tied to that
20
+ * mailbox. For live boxes, drop expired messages and prune old consumed files.
21
+ */
22
+ export declare function gcMailbox(activeBoxIds: Set<string>, options?: GcOptions): GcResult;