@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.
- package/CHANGELOG.md +57 -2
- package/README.md +12 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.d.ts +3 -0
- package/dist/commands/cloud.js +2 -1
- package/dist/commands/exec.js +65 -1
- package/dist/commands/feed.d.ts +29 -0
- package/dist/commands/feed.js +237 -32
- package/dist/commands/memory.d.ts +9 -0
- package/dist/commands/memory.js +164 -0
- package/dist/commands/message.d.ts +11 -6
- package/dist/commands/message.js +140 -5
- package/dist/commands/routines.js +12 -0
- package/dist/commands/secrets-migrate.d.ts +2 -1
- package/dist/commands/secrets-migrate.js +88 -13
- package/dist/commands/secrets.js +4 -1
- package/dist/commands/sessions.js +10 -1
- package/dist/commands/worktree.js +4 -2
- package/dist/index.js +16 -21
- package/dist/lib/agents.js +249 -17
- package/dist/lib/answer-router.d.ts +75 -0
- package/dist/lib/answer-router.js +149 -0
- package/dist/lib/ask-classifier.d.ts +71 -0
- package/dist/lib/ask-classifier.js +197 -0
- package/dist/lib/cloud/antigravity.d.ts +0 -2
- package/dist/lib/cloud/antigravity.js +2 -17
- package/dist/lib/cloud/codex.js +3 -18
- package/dist/lib/cloud/rush.js +3 -15
- package/dist/lib/cloud/stream.js +2 -0
- package/dist/lib/cloud/types.d.ts +21 -0
- package/dist/lib/cloud/types.js +81 -0
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +12 -2
- package/dist/lib/crabbox/lease.d.ts +13 -0
- package/dist/lib/crabbox/lease.js +11 -2
- package/dist/lib/crabbox/progress.d.ts +62 -0
- package/dist/lib/crabbox/progress.js +129 -0
- package/dist/lib/events.js +4 -1
- package/dist/lib/exec.js +19 -1
- package/dist/lib/feed-outcome.d.ts +101 -0
- package/dist/lib/feed-outcome.js +244 -0
- package/dist/lib/feed-policy.d.ts +30 -0
- package/dist/lib/feed-policy.js +133 -0
- package/dist/lib/feed.d.ts +127 -3
- package/dist/lib/feed.js +416 -40
- package/dist/lib/git.d.ts +17 -1
- package/dist/lib/git.js +20 -1
- package/dist/lib/hooks.js +522 -12
- package/dist/lib/hosts/passthrough.d.ts +3 -3
- package/dist/lib/hosts/passthrough.js +3 -4
- package/dist/lib/mailbox-gc.d.ts +22 -0
- package/dist/lib/mailbox-gc.js +161 -0
- package/dist/lib/mailbox.d.ts +26 -2
- package/dist/lib/mailbox.js +80 -5
- package/dist/lib/mcp.js +82 -0
- package/dist/lib/memory.d.ts +55 -0
- package/dist/lib/memory.js +274 -0
- package/dist/lib/notify.d.ts +16 -0
- package/dist/lib/notify.js +61 -0
- package/dist/lib/operator.d.ts +26 -0
- package/dist/lib/operator.js +107 -0
- package/dist/lib/plugins.d.ts +35 -0
- package/dist/lib/plugins.js +217 -0
- package/dist/lib/remote-agents-json.d.ts +14 -0
- package/dist/lib/remote-agents-json.js +94 -0
- package/dist/lib/resources/mcp.js +44 -0
- package/dist/lib/resources/memory.d.ts +15 -0
- package/dist/lib/resources/memory.js +46 -0
- package/dist/lib/resources/types.d.ts +2 -2
- package/dist/lib/runner.d.ts +43 -0
- package/dist/lib/runner.js +323 -74
- package/dist/lib/sandbox.js +6 -0
- package/dist/lib/secrets/bundles.js +38 -13
- package/dist/lib/secrets/icloud-import.d.ts +12 -3
- package/dist/lib/secrets/icloud-import.js +37 -7
- package/dist/lib/secrets/index.d.ts +106 -2
- package/dist/lib/secrets/index.js +603 -28
- package/dist/lib/session/active.d.ts +18 -0
- package/dist/lib/session/active.js +47 -17
- package/dist/lib/session/db.d.ts +9 -1
- package/dist/lib/session/db.js +18 -3
- package/dist/lib/session/discover.d.ts +13 -0
- package/dist/lib/session/discover.js +31 -0
- package/dist/lib/session/parse.d.ts +8 -0
- package/dist/lib/session/parse.js +42 -21
- package/dist/lib/session/remote-active.js +8 -89
- package/dist/lib/session/state.d.ts +11 -0
- package/dist/lib/session/state.js +37 -0
- package/dist/lib/session/tail.d.ts +23 -4
- package/dist/lib/session/tail.js +34 -16
- package/dist/lib/session/throughput.d.ts +30 -0
- package/dist/lib/session/throughput.js +86 -0
- package/dist/lib/shim-heal.d.ts +12 -3
- package/dist/lib/shim-heal.js +12 -6
- package/dist/lib/staleness/detectors/subagents.js +57 -3
- package/dist/lib/staleness/writers/hooks.js +7 -3
- package/dist/lib/staleness/writers/subagents.js +37 -6
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/subagents.d.ts +52 -0
- package/dist/lib/subagents.js +315 -12
- package/dist/lib/teams/worktree.d.ts +8 -0
- package/dist/lib/teams/worktree.js +8 -0
- package/dist/lib/types.d.ts +10 -2
- package/dist/lib/versions.js +38 -48
- package/package.json +4 -3
- 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
|
-
*
|
|
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]
|
|
1572
|
-
|
|
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
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
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 `
|
|
16
|
-
* richer `--host` handling in their own command actions (detached run
|
|
17
|
-
* multi-host
|
|
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 `
|
|
16
|
-
* richer `--host` handling in their own command actions (detached run
|
|
17
|
-
* multi-host
|
|
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;
|