@phnx-labs/agents-cli 1.20.52 → 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 (107) hide show
  1. package/CHANGELOG.md +57 -2
  2. package/README.md +12 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/cloud.d.ts +3 -0
  5. package/dist/commands/cloud.js +2 -1
  6. package/dist/commands/exec.js +65 -1
  7. package/dist/commands/feed.d.ts +29 -0
  8. package/dist/commands/feed.js +237 -32
  9. package/dist/commands/memory.d.ts +9 -0
  10. package/dist/commands/memory.js +164 -0
  11. package/dist/commands/message.d.ts +11 -6
  12. package/dist/commands/message.js +140 -5
  13. package/dist/commands/routines.js +12 -0
  14. package/dist/commands/secrets-migrate.d.ts +2 -1
  15. package/dist/commands/secrets-migrate.js +88 -13
  16. package/dist/commands/secrets.js +4 -1
  17. package/dist/commands/sessions.js +10 -1
  18. package/dist/commands/worktree.js +4 -2
  19. package/dist/index.js +16 -21
  20. package/dist/lib/agents.js +249 -17
  21. package/dist/lib/answer-router.d.ts +75 -0
  22. package/dist/lib/answer-router.js +149 -0
  23. package/dist/lib/ask-classifier.d.ts +71 -0
  24. package/dist/lib/ask-classifier.js +197 -0
  25. package/dist/lib/cloud/antigravity.d.ts +0 -2
  26. package/dist/lib/cloud/antigravity.js +2 -17
  27. package/dist/lib/cloud/codex.js +3 -18
  28. package/dist/lib/cloud/rush.js +3 -15
  29. package/dist/lib/cloud/stream.js +2 -0
  30. package/dist/lib/cloud/types.d.ts +21 -0
  31. package/dist/lib/cloud/types.js +81 -0
  32. package/dist/lib/crabbox/cli.d.ts +1 -1
  33. package/dist/lib/crabbox/cli.js +12 -2
  34. package/dist/lib/crabbox/lease.d.ts +13 -0
  35. package/dist/lib/crabbox/lease.js +11 -2
  36. package/dist/lib/crabbox/progress.d.ts +62 -0
  37. package/dist/lib/crabbox/progress.js +129 -0
  38. package/dist/lib/events.js +4 -1
  39. package/dist/lib/exec.js +19 -1
  40. package/dist/lib/feed-outcome.d.ts +101 -0
  41. package/dist/lib/feed-outcome.js +244 -0
  42. package/dist/lib/feed-policy.d.ts +30 -0
  43. package/dist/lib/feed-policy.js +133 -0
  44. package/dist/lib/feed.d.ts +127 -3
  45. package/dist/lib/feed.js +416 -40
  46. package/dist/lib/git.d.ts +17 -1
  47. package/dist/lib/git.js +20 -1
  48. package/dist/lib/hooks.js +522 -12
  49. package/dist/lib/hosts/passthrough.d.ts +3 -3
  50. package/dist/lib/hosts/passthrough.js +3 -4
  51. package/dist/lib/mailbox-gc.d.ts +22 -0
  52. package/dist/lib/mailbox-gc.js +161 -0
  53. package/dist/lib/mailbox.d.ts +26 -2
  54. package/dist/lib/mailbox.js +80 -5
  55. package/dist/lib/mcp.js +82 -0
  56. package/dist/lib/memory.d.ts +55 -0
  57. package/dist/lib/memory.js +274 -0
  58. package/dist/lib/notify.d.ts +16 -0
  59. package/dist/lib/notify.js +61 -0
  60. package/dist/lib/operator.d.ts +26 -0
  61. package/dist/lib/operator.js +107 -0
  62. package/dist/lib/plugins.d.ts +35 -0
  63. package/dist/lib/plugins.js +217 -0
  64. package/dist/lib/remote-agents-json.d.ts +14 -0
  65. package/dist/lib/remote-agents-json.js +94 -0
  66. package/dist/lib/resources/mcp.js +44 -0
  67. package/dist/lib/resources/memory.d.ts +15 -0
  68. package/dist/lib/resources/memory.js +46 -0
  69. package/dist/lib/resources/types.d.ts +2 -2
  70. package/dist/lib/runner.d.ts +43 -0
  71. package/dist/lib/runner.js +323 -74
  72. package/dist/lib/sandbox.js +6 -0
  73. package/dist/lib/secrets/bundles.js +38 -13
  74. package/dist/lib/secrets/icloud-import.d.ts +12 -3
  75. package/dist/lib/secrets/icloud-import.js +37 -7
  76. package/dist/lib/secrets/index.d.ts +106 -2
  77. package/dist/lib/secrets/index.js +603 -28
  78. package/dist/lib/session/active.d.ts +18 -0
  79. package/dist/lib/session/active.js +47 -17
  80. package/dist/lib/session/db.d.ts +9 -1
  81. package/dist/lib/session/db.js +18 -3
  82. package/dist/lib/session/discover.d.ts +13 -0
  83. package/dist/lib/session/discover.js +31 -0
  84. package/dist/lib/session/parse.d.ts +8 -0
  85. package/dist/lib/session/parse.js +42 -21
  86. package/dist/lib/session/remote-active.js +8 -89
  87. package/dist/lib/session/state.d.ts +11 -0
  88. package/dist/lib/session/state.js +37 -0
  89. package/dist/lib/session/tail.d.ts +23 -4
  90. package/dist/lib/session/tail.js +34 -16
  91. package/dist/lib/session/throughput.d.ts +30 -0
  92. package/dist/lib/session/throughput.js +86 -0
  93. package/dist/lib/shim-heal.d.ts +12 -3
  94. package/dist/lib/shim-heal.js +12 -6
  95. package/dist/lib/staleness/detectors/subagents.js +57 -3
  96. package/dist/lib/staleness/writers/hooks.js +7 -3
  97. package/dist/lib/staleness/writers/subagents.js +37 -6
  98. package/dist/lib/startup/command-registry.d.ts +1 -0
  99. package/dist/lib/startup/command-registry.js +2 -0
  100. package/dist/lib/subagents.d.ts +52 -0
  101. package/dist/lib/subagents.js +315 -12
  102. package/dist/lib/teams/worktree.d.ts +8 -0
  103. package/dist/lib/teams/worktree.js +8 -0
  104. package/dist/lib/types.d.ts +10 -2
  105. package/dist/lib/versions.js +38 -48
  106. package/package.json +4 -3
  107. 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
  /**
@@ -1513,9 +1525,13 @@ function registerHooksForAntigravity(versionHome, manifest, resolveScript, manag
1513
1525
  if (!hooks[agyEvent]) {
1514
1526
  hooks[agyEvent] = [];
1515
1527
  }
1528
+ // Antigravity settings entries: command + optional matcher (tool scope).
1529
+ // Without matcher every PreToolUse guard fires on ALL tools (RUSH-1353).
1516
1530
  const list = hooks[agyEvent];
1517
1531
  const existingIdx = list.findIndex((e) => e && typeof e === 'object' && e.command === commandPath);
1518
1532
  const entry = { command: commandPath };
1533
+ if (hookDef.matcher)
1534
+ entry.matcher = hookDef.matcher;
1519
1535
  if (existingIdx >= 0) {
1520
1536
  list[existingIdx] = entry;
1521
1537
  }
@@ -1534,9 +1550,43 @@ function registerHooksForAntigravity(versionHome, manifest, resolveScript, manag
1534
1550
  }
1535
1551
  return { registered, errors };
1536
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
+ };
1537
1578
  /**
1538
1579
  * Register hooks for Grok Build.
1539
- * 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.
1540
1590
  */
1541
1591
  function registerHooksForGrok(versionHome, manifest, resolveScript, managedPrefixes) {
1542
1592
  const registered = [];
@@ -1568,12 +1618,33 @@ function registerHooksForGrok(versionHome, manifest, resolveScript, managedPrefi
1568
1618
  if (!grokHooks.hooks[grokEvent]) {
1569
1619
  grokHooks.hooks[grokEvent] = [];
1570
1620
  }
1571
- grokHooks.hooks[grokEvent].push({
1572
- hooks: [{ type: 'command', command: commandPath, timeout }],
1573
- });
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
+ }
1574
1643
  registered.push(`${name} -> ${grokEvent}`);
1575
1644
  }
1576
1645
  }
1646
+ // Single source of truth: hooks.json. Written fresh from the manifest every
1647
+ // sync, so the registrar owns it outright.
1577
1648
  const mainHooksPath = path.join(grokHooksDir, 'hooks.json');
1578
1649
  try {
1579
1650
  fs.writeFileSync(mainHooksPath, JSON.stringify(grokHooks, null, 2));
@@ -1581,18 +1652,66 @@ function registerHooksForGrok(versionHome, manifest, resolveScript, managedPrefi
1581
1652
  catch (e) {
1582
1653
  errors.push(`Failed to write hooks.json: ${e.message}`);
1583
1654
  }
1584
- for (const [eventName, groups] of Object.entries(grokHooks.hooks)) {
1585
- const fileName = eventName.toLowerCase().replace(/([a-z])([A-Z])/g, '$1-$2') + '.json';
1586
- const eventFile = path.join(grokHooksDir, fileName);
1587
- try {
1588
- fs.writeFileSync(eventFile, JSON.stringify({ hooks: { [eventName]: groups } }, null, 2));
1589
- }
1590
- catch (e) {
1591
- 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
+ }
1592
1668
  }
1593
1669
  }
1670
+ catch (e) {
1671
+ errors.push(`Failed to prune stale grok hook files: ${e.message}`);
1672
+ }
1594
1673
  return { registered, errors };
1595
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
+ }
1596
1715
  function registerHooksForKimi(versionHome, manifest, resolveScript, managedPrefixes) {
1597
1716
  const registered = [];
1598
1717
  const errors = [];
@@ -1676,3 +1795,394 @@ function registerHooksForKimi(versionHome, manifest, resolveScript, managedPrefi
1676
1795
  }
1677
1796
  return { registered, errors };
1678
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';
@@ -34,7 +34,6 @@ const REMOTE_PASSTHROUGH = {
34
34
  sync: { nonInteractive: ['--yes'] },
35
35
  teams: {},
36
36
  message: {},
37
- feed: {},
38
37
  };
39
38
  /** `--no-tty` is stripped like the routing flags but carries no value. */
40
39
  const STRIP_SPECS = [...HOST_ROUTING_SPECS, { long: 'no-tty', takesValue: false }];
@@ -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;