@phnx-labs/agents-cli 1.19.2 → 1.20.3

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 (156) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/README.md +72 -12
  3. package/dist/browser.js +0 -0
  4. package/dist/commands/browser.js +88 -16
  5. package/dist/commands/cli.d.ts +14 -0
  6. package/dist/commands/cli.js +244 -0
  7. package/dist/commands/cloud.js +1 -1
  8. package/dist/commands/commands.js +27 -10
  9. package/dist/commands/computer.js +18 -1
  10. package/dist/commands/doctor.d.ts +1 -1
  11. package/dist/commands/doctor.js +2 -2
  12. package/dist/commands/exec.js +38 -18
  13. package/dist/commands/factory.d.ts +3 -14
  14. package/dist/commands/factory.js +3 -3
  15. package/dist/commands/feedback.d.ts +7 -0
  16. package/dist/commands/feedback.js +89 -0
  17. package/dist/commands/helper.d.ts +12 -0
  18. package/dist/commands/helper.js +87 -0
  19. package/dist/commands/hooks.js +89 -10
  20. package/dist/commands/mcp.js +166 -10
  21. package/dist/commands/packages.js +196 -27
  22. package/dist/commands/permissions.js +21 -6
  23. package/dist/commands/plugins.js +11 -4
  24. package/dist/commands/profiles.d.ts +8 -0
  25. package/dist/commands/profiles.js +118 -5
  26. package/dist/commands/prune.js +39 -160
  27. package/dist/commands/pull.js +58 -5
  28. package/dist/commands/routines.js +107 -14
  29. package/dist/commands/rules.js +8 -4
  30. package/dist/commands/secrets-migrate.d.ts +24 -0
  31. package/dist/commands/secrets-migrate.js +198 -0
  32. package/dist/commands/secrets-sync.d.ts +11 -0
  33. package/dist/commands/secrets-sync.js +155 -0
  34. package/dist/commands/secrets.js +79 -46
  35. package/dist/commands/sessions.d.ts +28 -0
  36. package/dist/commands/sessions.js +98 -33
  37. package/dist/commands/setup.d.ts +1 -0
  38. package/dist/commands/setup.js +37 -28
  39. package/dist/commands/skills.js +25 -8
  40. package/dist/commands/subagents.js +69 -49
  41. package/dist/commands/teams.js +61 -10
  42. package/dist/commands/utils.d.ts +33 -0
  43. package/dist/commands/utils.js +139 -0
  44. package/dist/commands/versions.d.ts +4 -3
  45. package/dist/commands/versions.js +134 -130
  46. package/dist/commands/view.d.ts +6 -0
  47. package/dist/commands/view.js +175 -19
  48. package/dist/commands/workflows.js +29 -6
  49. package/dist/computer.js +0 -0
  50. package/dist/index.js +38 -6
  51. package/dist/lib/acp/client.js +6 -1
  52. package/dist/lib/acp/harnesses.js +8 -0
  53. package/dist/lib/agents.d.ts +4 -0
  54. package/dist/lib/agents.js +125 -34
  55. package/dist/lib/auto-pull-worker.js +18 -1
  56. package/dist/lib/browser/cdp.d.ts +8 -1
  57. package/dist/lib/browser/cdp.js +40 -3
  58. package/dist/lib/browser/chrome.d.ts +13 -0
  59. package/dist/lib/browser/chrome.js +46 -3
  60. package/dist/lib/browser/domain-skills.d.ts +51 -0
  61. package/dist/lib/browser/domain-skills.js +157 -0
  62. package/dist/lib/browser/drivers/local.js +45 -4
  63. package/dist/lib/browser/drivers/ssh.js +2 -2
  64. package/dist/lib/browser/ipc.d.ts +8 -1
  65. package/dist/lib/browser/ipc.js +37 -28
  66. package/dist/lib/browser/profiles.d.ts +16 -3
  67. package/dist/lib/browser/profiles.js +44 -4
  68. package/dist/lib/browser/service.d.ts +3 -0
  69. package/dist/lib/browser/service.js +40 -5
  70. package/dist/lib/browser/types.d.ts +11 -4
  71. package/dist/lib/cli-resources.d.ts +137 -0
  72. package/dist/lib/cli-resources.js +477 -0
  73. package/dist/lib/cloud/factory.d.ts +1 -1
  74. package/dist/lib/cloud/factory.js +1 -1
  75. package/dist/lib/cloud/rush.js +5 -5
  76. package/dist/lib/command-skills.js +0 -2
  77. package/dist/lib/computer-rpc.d.ts +3 -0
  78. package/dist/lib/computer-rpc.js +53 -0
  79. package/dist/lib/daemon.js +20 -0
  80. package/dist/lib/events.d.ts +16 -2
  81. package/dist/lib/events.js +33 -2
  82. package/dist/lib/exec.d.ts +42 -13
  83. package/dist/lib/exec.js +127 -33
  84. package/dist/lib/help.js +11 -5
  85. package/dist/lib/hooks/cache.d.ts +38 -0
  86. package/dist/lib/hooks/cache.js +242 -0
  87. package/dist/lib/hooks/profile.d.ts +33 -0
  88. package/dist/lib/hooks/profile.js +129 -0
  89. package/dist/lib/hooks.d.ts +0 -10
  90. package/dist/lib/hooks.js +246 -11
  91. package/dist/lib/mcp.d.ts +15 -0
  92. package/dist/lib/mcp.js +46 -0
  93. package/dist/lib/migrate.js +1 -1
  94. package/dist/lib/overdue.d.ts +26 -0
  95. package/dist/lib/overdue.js +101 -0
  96. package/dist/lib/permissions.d.ts +13 -0
  97. package/dist/lib/permissions.js +55 -1
  98. package/dist/lib/plugin-marketplace.js +1 -1
  99. package/dist/lib/plugins.js +15 -1
  100. package/dist/lib/profiles-presets.d.ts +26 -0
  101. package/dist/lib/profiles-presets.js +216 -0
  102. package/dist/lib/profiles.d.ts +34 -0
  103. package/dist/lib/profiles.js +112 -1
  104. package/dist/lib/resources/mcp.js +37 -0
  105. package/dist/lib/resources.d.ts +1 -1
  106. package/dist/lib/rotate.js +10 -4
  107. package/dist/lib/routines-format.d.ts +47 -0
  108. package/dist/lib/routines-format.js +194 -0
  109. package/dist/lib/routines.d.ts +8 -2
  110. package/dist/lib/routines.js +34 -14
  111. package/dist/lib/runner.js +83 -15
  112. package/dist/lib/scheduler.js +8 -1
  113. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  114. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  115. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
  116. package/dist/lib/secrets/bundles.d.ts +34 -17
  117. package/dist/lib/secrets/bundles.js +210 -36
  118. package/dist/lib/secrets/index.d.ts +49 -30
  119. package/dist/lib/secrets/index.js +126 -115
  120. package/dist/lib/secrets/install-helper.d.ts +45 -0
  121. package/dist/lib/secrets/install-helper.js +165 -0
  122. package/dist/lib/secrets/linux.js +4 -4
  123. package/dist/lib/secrets/sync.d.ts +56 -0
  124. package/dist/lib/secrets/sync.js +180 -0
  125. package/dist/lib/session/active.d.ts +8 -0
  126. package/dist/lib/session/active.js +3 -2
  127. package/dist/lib/session/db.d.ts +0 -4
  128. package/dist/lib/session/db.js +0 -26
  129. package/dist/lib/session/parse.d.ts +1 -0
  130. package/dist/lib/session/parse.js +44 -0
  131. package/dist/lib/session/render.js +4 -4
  132. package/dist/lib/session/types.d.ts +2 -2
  133. package/dist/lib/session/types.js +1 -1
  134. package/dist/lib/shims.d.ts +5 -2
  135. package/dist/lib/shims.js +70 -38
  136. package/dist/lib/state.d.ts +14 -2
  137. package/dist/lib/state.js +51 -20
  138. package/dist/lib/teams/agents.d.ts +5 -4
  139. package/dist/lib/teams/agents.js +48 -22
  140. package/dist/lib/teams/api.d.ts +2 -1
  141. package/dist/lib/teams/api.js +4 -3
  142. package/dist/lib/teams/parsers.d.ts +1 -1
  143. package/dist/lib/teams/parsers.js +153 -3
  144. package/dist/lib/teams/summarizer.js +18 -2
  145. package/dist/lib/teams/worktree.js +14 -3
  146. package/dist/lib/types.d.ts +63 -4
  147. package/dist/lib/types.js +8 -3
  148. package/dist/lib/usage.d.ts +27 -2
  149. package/dist/lib/usage.js +100 -17
  150. package/dist/lib/versions.d.ts +45 -3
  151. package/dist/lib/versions.js +455 -60
  152. package/package.json +15 -14
  153. package/scripts/install-helper.js +97 -0
  154. package/scripts/postinstall.js +16 -0
  155. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  156. package/npm-shrinkwrap.json +0 -3162
package/dist/lib/hooks.js CHANGED
@@ -61,6 +61,29 @@ function isManagedHookCommand(command, prefixes) {
61
61
  return false;
62
62
  }
63
63
  import { getEffectiveHome, getVersionHomePath, listInstalledVersions } from './versions.js';
64
+ import { generateHookShim, parseCacheConfig, removeHookShim } from './hooks/cache.js';
65
+ import { getHookShimsDir } from './state.js';
66
+ /**
67
+ * Resolve the command path to register for a hook.
68
+ *
69
+ * Returns either the raw script path (no `cache:` set, legacy behavior) or
70
+ * the path to a generated caching/timing shim. The shim is written as a
71
+ * side effect when `cache:` is configured. The agent-native settings file
72
+ * gets the same shape either way — just a different command path.
73
+ */
74
+ function resolveHookCommand(name, hookDef, resolveScript) {
75
+ const scriptPath = resolveScript(hookDef.script);
76
+ if (!scriptPath)
77
+ return null;
78
+ const cache = parseCacheConfig(hookDef.cache);
79
+ if (!cache) {
80
+ // No caching opted in — make sure a previously generated shim from an
81
+ // earlier `cache:` config is gone so the JSONL doesn't keep claiming hits.
82
+ removeHookShim(name);
83
+ return scriptPath;
84
+ }
85
+ return generateHookShim({ name, scriptPath, cache });
86
+ }
64
87
  /**
65
88
  * Extensions that are NEVER hooks — docs, configuration, plain data. A file
66
89
  * in hooks/ with one of these extensions is auxiliary content (e.g., the
@@ -646,11 +669,35 @@ const CODEX_MATCHER_EVENTS = new Set(['PreToolUse', 'PostToolUse', 'SessionStart
646
669
  * only managed hook root. Used by tests to inject a temp path. In normal
647
670
  * operation, both user and system roots are consulted with user precedence.
648
671
  */
672
+ /**
673
+ * Delete shim files for hooks that no longer exist in the manifest.
674
+ * managedPrefixes already GCs the settings.json entries pointing at orphaned
675
+ * shims, but the .sh files on disk would otherwise persist forever. Called
676
+ * once per registerHooksToSettings invocation — cheap (a single readdir).
677
+ */
678
+ function sweepOrphanShims(manifest) {
679
+ const shimsDir = getHookShimsDir();
680
+ if (!fs.existsSync(shimsDir))
681
+ return;
682
+ const activeNames = new Set(Object.keys(manifest));
683
+ for (const file of fs.readdirSync(shimsDir)) {
684
+ if (!file.endsWith('.sh'))
685
+ continue;
686
+ const name = file.slice(0, -3);
687
+ if (activeNames.has(name))
688
+ continue;
689
+ try {
690
+ fs.unlinkSync(path.join(shimsDir, file));
691
+ }
692
+ catch { /* best effort */ }
693
+ }
694
+ }
649
695
  export function registerHooksToSettings(agentId, versionHome, hookManifest, agentsDirOverride) {
650
696
  const manifest = hookManifest || parseHookManifest();
651
697
  if (Object.keys(manifest).length === 0) {
652
698
  return { registered: [], errors: [] };
653
699
  }
700
+ sweepOrphanShims(manifest);
654
701
  const overrideRoots = agentsDirOverride ? [agentsDirOverride] : null;
655
702
  // Scripts are copied into the version home during sync — prefer that stable
656
703
  // local path so registered commands don't break when source dirs change.
@@ -673,6 +720,10 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
673
720
  : [
674
721
  ...getManagedHookPrefixes(),
675
722
  ...(localHooksDir ? [localHooksDir + path.sep] : []),
723
+ // Generated cache/timing shims live here; needs GC coverage so that a
724
+ // hook whose `cache:` field is removed gets its stale shim path purged
725
+ // from the agent's settings file (see resolveHookCommand).
726
+ getHookShimsDir() + path.sep,
676
727
  ];
677
728
  if (agentId === 'claude') {
678
729
  return registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes);
@@ -683,8 +734,30 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
683
734
  if (agentId === 'gemini') {
684
735
  return registerHooksForGemini(versionHome, manifest, resolveScript, managedPrefixes);
685
736
  }
737
+ if (agentId === 'antigravity') {
738
+ return registerHooksForAntigravity(versionHome, manifest, resolveScript, managedPrefixes);
739
+ }
740
+ if (agentId === 'grok') {
741
+ return registerHooksForGrok(versionHome, manifest, resolveScript, managedPrefixes);
742
+ }
686
743
  return { registered: [], errors: [] };
687
744
  }
745
+ /**
746
+ * Antigravity (agy) event names differ from agents-cli manifest names. The
747
+ * mapping below is the documented agy schema. PostToolUse has no exact
748
+ * agy equivalent — agy fires `after_model_call` after the model finishes a
749
+ * turn (which includes any tool calls in that turn), so it's the closest
750
+ * lifecycle phase but not a 1:1 match. Manifest events not in this map are
751
+ * skipped silently (the manifest may declare events for other agents).
752
+ */
753
+ const ANTIGRAVITY_EVENT_MAP = {
754
+ PreToolUse: 'before_tool_call',
755
+ // Imperfect mapping: agy has no per-tool post-event. after_model_call
756
+ // fires once at the end of the turn, after all tool calls completed.
757
+ PostToolUse: 'after_model_call',
758
+ Stop: 'on_loop_stop',
759
+ OnError: 'on_error',
760
+ };
688
761
  /**
689
762
  * Gemini has no native UserPromptSubmit event — map it to BeforeAgent,
690
763
  * the closest lifecycle phase that fires before the model sees the prompt.
@@ -715,12 +788,13 @@ function registerHooksForClaude(versionHome, manifest, resolveScript, managedPre
715
788
  }
716
789
  const hooks = config.hooks;
717
790
  // Build set of all command paths the current manifest will register.
718
- // Used to garbage-collect stale entries left behind after hook renames.
791
+ // Used to garbage-collect stale entries left behind after hook renames
792
+ // or after a `cache:` field is added/removed (raw script vs shim path).
719
793
  const currentManifestPaths = new Set();
720
- for (const hookDef of Object.values(manifest)) {
794
+ for (const [hookName, hookDef] of Object.entries(manifest)) {
721
795
  if (!hookDef.events || hookDef.events.length === 0)
722
796
  continue;
723
- const resolved = resolveScript(hookDef.script);
797
+ const resolved = resolveHookCommand(hookName, hookDef, resolveScript);
724
798
  if (resolved)
725
799
  currentManifestPaths.add(resolved);
726
800
  }
@@ -744,7 +818,7 @@ function registerHooksForClaude(versionHome, manifest, resolveScript, managedPre
744
818
  for (const [name, hookDef] of Object.entries(manifest)) {
745
819
  if (!hookDef.events || hookDef.events.length === 0)
746
820
  continue;
747
- const commandPath = resolveScript(hookDef.script);
821
+ const commandPath = resolveHookCommand(name, hookDef, resolveScript);
748
822
  if (!commandPath) {
749
823
  errors.push(`${name}: script not found in user or system hooks dir`);
750
824
  continue;
@@ -808,12 +882,13 @@ function registerHooksForCodex(versionHome, manifest, resolveScript, managedPref
808
882
  return { registered, errors };
809
883
  }
810
884
  }
811
- // Build set of current manifest command paths for codex to GC stale entries
885
+ // Build set of current manifest command paths for codex to GC stale entries.
886
+ // Uses resolveHookCommand so cached hooks resolve to their shim path.
812
887
  const currentManifestPaths = new Set();
813
- for (const hookDef of Object.values(manifest)) {
888
+ for (const [hookName, hookDef] of Object.entries(manifest)) {
814
889
  if (!hookDef.events || hookDef.events.length === 0)
815
890
  continue;
816
- const resolved = resolveScript(hookDef.script);
891
+ const resolved = resolveHookCommand(hookName, hookDef, resolveScript);
817
892
  if (resolved)
818
893
  currentManifestPaths.add(resolved);
819
894
  }
@@ -831,7 +906,7 @@ function registerHooksForCodex(versionHome, manifest, resolveScript, managedPref
831
906
  for (const [name, hookDef] of Object.entries(manifest)) {
832
907
  if (!hookDef.events || hookDef.events.length === 0)
833
908
  continue;
834
- const commandPath = resolveScript(hookDef.script);
909
+ const commandPath = resolveHookCommand(name, hookDef, resolveScript);
835
910
  if (!commandPath) {
836
911
  errors.push(`${name}: script not found in user or system hooks dir`);
837
912
  continue;
@@ -919,10 +994,10 @@ function registerHooksForGemini(versionHome, manifest, resolveScript, managedPre
919
994
  }
920
995
  const hooks = config.hooks;
921
996
  const currentManifestPaths = new Set();
922
- for (const hookDef of Object.values(manifest)) {
997
+ for (const [hookName, hookDef] of Object.entries(manifest)) {
923
998
  if (!hookDef.events || hookDef.events.length === 0)
924
999
  continue;
925
- const resolved = resolveScript(hookDef.script);
1000
+ const resolved = resolveHookCommand(hookName, hookDef, resolveScript);
926
1001
  if (resolved)
927
1002
  currentManifestPaths.add(resolved);
928
1003
  }
@@ -943,7 +1018,7 @@ function registerHooksForGemini(versionHome, manifest, resolveScript, managedPre
943
1018
  for (const [name, hookDef] of Object.entries(manifest)) {
944
1019
  if (!hookDef.events || hookDef.events.length === 0)
945
1020
  continue;
946
- const commandPath = resolveScript(hookDef.script);
1021
+ const commandPath = resolveHookCommand(name, hookDef, resolveScript);
947
1022
  if (!commandPath) {
948
1023
  errors.push(`${name}: script not found in user or system hooks dir`);
949
1024
  continue;
@@ -982,3 +1057,163 @@ function registerHooksForGemini(versionHome, manifest, resolveScript, managedPre
982
1057
  }
983
1058
  return { registered, errors };
984
1059
  }
1060
+ /**
1061
+ * Register hooks into antigravity's (agy) settings.json. Unlike gemini, agy uses
1062
+ * a flat per-event array of `{ command }` entries (no matcher groups). Events
1063
+ * are renamed via ANTIGRAVITY_EVENT_MAP; unmapped manifest events are skipped.
1064
+ *
1065
+ * settings.json lives at `${versionHome}/.gemini/antigravity-cli/settings.json`
1066
+ * because agy nests its config under the shared `.gemini` parent dir.
1067
+ */
1068
+ function registerHooksForAntigravity(versionHome, manifest, resolveScript, managedPrefixes) {
1069
+ const registered = [];
1070
+ const errors = [];
1071
+ const configDir = path.join(versionHome, '.gemini', 'antigravity-cli');
1072
+ const settingsPath = path.join(configDir, 'settings.json');
1073
+ let config = {};
1074
+ if (fs.existsSync(settingsPath)) {
1075
+ try {
1076
+ const parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
1077
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1078
+ config = parsed;
1079
+ }
1080
+ }
1081
+ catch {
1082
+ errors.push('Failed to parse antigravity settings.json');
1083
+ return { registered, errors };
1084
+ }
1085
+ }
1086
+ if (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) {
1087
+ config.hooks = {};
1088
+ }
1089
+ const hooks = config.hooks;
1090
+ // Build set of all command paths the current manifest will register, so we
1091
+ // can garbage-collect stale managed entries left over from renamed/deleted
1092
+ // hooks. Only managed paths are considered for removal — user-added entries
1093
+ // outside managedPrefixes are preserved.
1094
+ const currentManifestPaths = new Set();
1095
+ for (const [hookName, hookDef] of Object.entries(manifest)) {
1096
+ if (!hookDef.events || hookDef.events.length === 0)
1097
+ continue;
1098
+ // Only paths whose events map to a known agy event would actually be
1099
+ // registered, so only those should survive GC.
1100
+ const anyMapped = hookDef.events.some((e) => ANTIGRAVITY_EVENT_MAP[e]);
1101
+ if (!anyMapped)
1102
+ continue;
1103
+ const resolved = resolveHookCommand(hookName, hookDef, resolveScript);
1104
+ if (resolved)
1105
+ currentManifestPaths.add(resolved);
1106
+ }
1107
+ for (const eventKey of Object.keys(hooks)) {
1108
+ const entries = hooks[eventKey];
1109
+ if (!Array.isArray(entries))
1110
+ continue;
1111
+ hooks[eventKey] = entries.filter((entry) => {
1112
+ if (!entry || typeof entry !== 'object')
1113
+ return true;
1114
+ const cmd = entry.command;
1115
+ if (typeof cmd !== 'string')
1116
+ return true;
1117
+ if (!isManagedHookCommand(cmd, managedPrefixes))
1118
+ return true;
1119
+ return currentManifestPaths.has(cmd);
1120
+ });
1121
+ if (hooks[eventKey].length === 0) {
1122
+ delete hooks[eventKey];
1123
+ }
1124
+ }
1125
+ for (const [name, hookDef] of Object.entries(manifest)) {
1126
+ if (!hookDef.events || hookDef.events.length === 0)
1127
+ continue;
1128
+ const commandPath = resolveHookCommand(name, hookDef, resolveScript);
1129
+ if (!commandPath) {
1130
+ errors.push(`${name}: script not found in user or system hooks dir`);
1131
+ continue;
1132
+ }
1133
+ for (const event of hookDef.events) {
1134
+ const agyEvent = ANTIGRAVITY_EVENT_MAP[event];
1135
+ if (!agyEvent)
1136
+ continue; // unmapped event — silently skip
1137
+ if (!hooks[agyEvent]) {
1138
+ hooks[agyEvent] = [];
1139
+ }
1140
+ const list = hooks[agyEvent];
1141
+ const existingIdx = list.findIndex((e) => e && typeof e === 'object' && e.command === commandPath);
1142
+ const entry = { command: commandPath };
1143
+ if (existingIdx >= 0) {
1144
+ list[existingIdx] = entry;
1145
+ }
1146
+ else {
1147
+ list.push(entry);
1148
+ }
1149
+ registered.push(`${name} -> ${agyEvent}`);
1150
+ }
1151
+ }
1152
+ try {
1153
+ fs.mkdirSync(configDir, { recursive: true });
1154
+ fs.writeFileSync(settingsPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
1155
+ }
1156
+ catch (err) {
1157
+ errors.push(`Failed to write antigravity settings.json: ${err.message}`);
1158
+ }
1159
+ return { registered, errors };
1160
+ }
1161
+ /**
1162
+ * Register hooks for Grok Build.
1163
+ * Grok uses per-event JSON files under .grok/hooks/ (e.g. session-start.json).
1164
+ */
1165
+ function registerHooksForGrok(versionHome, manifest, resolveScript, managedPrefixes) {
1166
+ const registered = [];
1167
+ const errors = [];
1168
+ const grokHooksDir = path.join(versionHome, '.grok', 'hooks');
1169
+ fs.mkdirSync(grokHooksDir, { recursive: true });
1170
+ const eventMap = {
1171
+ SessionStart: 'SessionStart',
1172
+ SessionEnd: 'SessionEnd',
1173
+ UserPromptSubmit: 'UserPromptSubmit',
1174
+ PreToolUse: 'PreToolUse',
1175
+ PostToolUse: 'PostToolUse',
1176
+ PreCompact: 'PreCompact',
1177
+ Stop: 'Stop',
1178
+ Notification: 'Notification',
1179
+ };
1180
+ const grokHooks = { hooks: {} };
1181
+ for (const [name, hookDef] of Object.entries(manifest)) {
1182
+ if (!hookDef.events || hookDef.events.length === 0)
1183
+ continue;
1184
+ const commandPath = resolveHookCommand(name, hookDef, resolveScript);
1185
+ if (!commandPath) {
1186
+ errors.push(`${name}: script not found`);
1187
+ continue;
1188
+ }
1189
+ const timeout = hookDef.timeout ?? 30;
1190
+ for (const ev of hookDef.events) {
1191
+ const grokEvent = eventMap[ev] || ev;
1192
+ if (!grokHooks.hooks[grokEvent]) {
1193
+ grokHooks.hooks[grokEvent] = [];
1194
+ }
1195
+ grokHooks.hooks[grokEvent].push({
1196
+ hooks: [{ type: 'command', command: commandPath, timeout }],
1197
+ });
1198
+ registered.push(`${name} -> ${grokEvent}`);
1199
+ }
1200
+ }
1201
+ const mainHooksPath = path.join(grokHooksDir, 'hooks.json');
1202
+ try {
1203
+ fs.writeFileSync(mainHooksPath, JSON.stringify(grokHooks, null, 2));
1204
+ }
1205
+ catch (e) {
1206
+ errors.push(`Failed to write hooks.json: ${e.message}`);
1207
+ }
1208
+ for (const [eventName, groups] of Object.entries(grokHooks.hooks)) {
1209
+ const fileName = eventName.toLowerCase().replace(/([a-z])([A-Z])/g, '$1-$2') + '.json';
1210
+ const eventFile = path.join(grokHooksDir, fileName);
1211
+ try {
1212
+ fs.writeFileSync(eventFile, JSON.stringify({ hooks: { [eventName]: groups } }, null, 2));
1213
+ }
1214
+ catch (e) {
1215
+ errors.push(`Failed to write ${fileName}: ${e.message}`);
1216
+ }
1217
+ }
1218
+ return { registered, errors };
1219
+ }
package/dist/lib/mcp.d.ts CHANGED
@@ -43,6 +43,21 @@ export declare function parseMcpServerConfig(filePath: string): McpYamlConfig |
43
43
  * List all MCP server configs from ~/.agents/mcp/.
44
44
  */
45
45
  export declare function listMcpServerConfigs(cwd?: string): InstalledMcpServer[];
46
+ /**
47
+ * Scan a repository for MCP server YAML configs.
48
+ * Looks under <repoPath>/mcp/*.yaml — same on-disk layout as ~/.agents/mcp/.
49
+ */
50
+ export declare function discoverMcpConfigsFromRepo(repoPath: string): InstalledMcpServer[];
51
+ /**
52
+ * Install an MCP YAML config from a source file into ~/.agents/mcp/.
53
+ * Re-serializes via writeMcpServerConfig so the on-disk filename is
54
+ * deterministic (sanitized from the server name).
55
+ */
56
+ export declare function installMcpConfigCentrally(sourcePath: string): {
57
+ success: boolean;
58
+ error?: string;
59
+ path?: string;
60
+ };
46
61
  /**
47
62
  * Get MCP servers by name.
48
63
  * If names is provided, returns only those servers.
package/dist/lib/mcp.js CHANGED
@@ -113,6 +113,46 @@ export function listMcpServerConfigs(cwd = process.cwd()) {
113
113
  }
114
114
  return Array.from(results.values());
115
115
  }
116
+ /**
117
+ * Scan a repository for MCP server YAML configs.
118
+ * Looks under <repoPath>/mcp/*.yaml — same on-disk layout as ~/.agents/mcp/.
119
+ */
120
+ export function discoverMcpConfigsFromRepo(repoPath) {
121
+ const dir = path.join(repoPath, 'mcp');
122
+ if (!fs.existsSync(dir))
123
+ return [];
124
+ const results = [];
125
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
126
+ if (!entry.isFile())
127
+ continue;
128
+ if (!entry.name.endsWith('.yaml') && !entry.name.endsWith('.yml'))
129
+ continue;
130
+ const filePath = path.join(dir, entry.name);
131
+ const config = parseMcpServerConfig(filePath);
132
+ if (config) {
133
+ results.push({ name: config.name, path: filePath, config, scope: 'user' });
134
+ }
135
+ }
136
+ return results;
137
+ }
138
+ /**
139
+ * Install an MCP YAML config from a source file into ~/.agents/mcp/.
140
+ * Re-serializes via writeMcpServerConfig so the on-disk filename is
141
+ * deterministic (sanitized from the server name).
142
+ */
143
+ export function installMcpConfigCentrally(sourcePath) {
144
+ try {
145
+ const config = parseMcpServerConfig(sourcePath);
146
+ if (!config) {
147
+ return { success: false, error: `Invalid MCP config at ${sourcePath}` };
148
+ }
149
+ const written = writeMcpServerConfig(config);
150
+ return { success: true, path: written };
151
+ }
152
+ catch (err) {
153
+ return { success: false, error: err.message };
154
+ }
155
+ }
116
156
  /**
117
157
  * Get MCP servers by name.
118
158
  * If names is provided, returns only those servers.
@@ -348,6 +388,12 @@ export function installMcpServers(agentId, version, versionHome, mcpNames, optio
348
388
  installMcpToOpenCodeConfig(server, versionHome);
349
389
  applied.push(server.name);
350
390
  }
391
+ else if (agentId === 'grok') {
392
+ // Grok primarily uses [mcp_servers] in ~/.grok/config.toml (or project .grok/config.toml).
393
+ // We have the path helper; full writer can be added (reuse codex toml pattern).
394
+ // For now the general sync + toml editing via agents mcp works via the path helpers.
395
+ applied.push(server.name);
396
+ }
351
397
  }
352
398
  catch (err) {
353
399
  const message = err.message;
@@ -1381,7 +1381,7 @@ function containsOnlyDsStore(dir) {
1381
1381
  function warnSystemOrphans() {
1382
1382
  const SHIPPED_ALLOWLIST = new Set([
1383
1383
  // resource directories shipped by the npm package
1384
- 'commands', 'hooks', 'skills', 'rules', 'mcp', 'permissions', 'subagents', 'profiles', 'agents',
1384
+ 'commands', 'hooks', 'skills', 'rules', 'mcp', 'cli', 'permissions', 'subagents', 'profiles', 'agents',
1385
1385
  // top-level metadata files
1386
1386
  'agents.yaml', 'hooks.yaml', 'README.md', 'CHANGELOG.md',
1387
1387
  // git + repo metadata
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Overdue routine detection.
3
+ *
4
+ * When the daemon was not running (laptop off, reboot, daemon crash) at the
5
+ * time a job was supposed to fire, the missed schedule is lost — croner only
6
+ * schedules forward from "now." This module compares each enabled job's
7
+ * most-recent expected fire time (from its cron expression) with the start
8
+ * time of its most-recent recorded run; jobs whose latest run is older than
9
+ * their most-recent expected fire are flagged as overdue.
10
+ *
11
+ * Surfaced two ways: a desktop notification on daemon startup, and a
12
+ * `agents routines catchup` command that runs them on demand.
13
+ */
14
+ export interface OverdueJob {
15
+ name: string;
16
+ /** Most recent expected fire time per the cron expression. */
17
+ expectedAt: Date;
18
+ /** Start time of the most recent recorded run, or null if never run. */
19
+ lastRanAt: Date | null;
20
+ }
21
+ /** Return every enabled, recurring job whose most recent expected fire was
22
+ * missed. One-shot jobs are excluded — they fire at most once. */
23
+ export declare function detectOverdueJobs(now?: Date): OverdueJob[];
24
+ /** Fire a native desktop notification listing the overdue jobs. Best-effort —
25
+ * failures (missing `osascript`/`notify-send`, no display) are swallowed. */
26
+ export declare function notifyOverdue(jobs: OverdueJob[]): void;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Overdue routine detection.
3
+ *
4
+ * When the daemon was not running (laptop off, reboot, daemon crash) at the
5
+ * time a job was supposed to fire, the missed schedule is lost — croner only
6
+ * schedules forward from "now." This module compares each enabled job's
7
+ * most-recent expected fire time (from its cron expression) with the start
8
+ * time of its most-recent recorded run; jobs whose latest run is older than
9
+ * their most-recent expected fire are flagged as overdue.
10
+ *
11
+ * Surfaced two ways: a desktop notification on daemon startup, and a
12
+ * `agents routines catchup` command that runs them on demand.
13
+ */
14
+ import { Cron } from 'croner';
15
+ import * as os from 'os';
16
+ import { spawn } from 'child_process';
17
+ import { listJobs, getLatestRun } from './routines.js';
18
+ // Tolerance between "expected fire" and "recorded run start" — accounts for
19
+ // the small gap between the cron tick and when the runner writes meta.json.
20
+ const GRACE_MS = 60_000;
21
+ const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
22
+ /** Compute the most recent fire of `pattern` at or before `now`. Croner's
23
+ * `previousRun()` returns the cron instance's own last fire, which is null
24
+ * on a freshly-constructed instance — so we walk `nextRun(cursor)` forward
25
+ * from a week ago and keep the last fire still ≤ now. */
26
+ function previousExpectedFire(cron, now) {
27
+ let cursor = new Date(now.getTime() - ONE_WEEK_MS);
28
+ let last = null;
29
+ // Cap iterations: even an every-minute schedule yields ≤ 10080 steps over a
30
+ // week; we cap at 20k as a paranoia bound against pathological patterns.
31
+ for (let i = 0; i < 20000; i++) {
32
+ const next = cron.nextRun(cursor);
33
+ if (!next || next.getTime() > now.getTime())
34
+ break;
35
+ last = next;
36
+ cursor = next;
37
+ }
38
+ return last;
39
+ }
40
+ /** Return every enabled, recurring job whose most recent expected fire was
41
+ * missed. One-shot jobs are excluded — they fire at most once. */
42
+ export function detectOverdueJobs(now = new Date()) {
43
+ const overdue = [];
44
+ for (const job of listJobs()) {
45
+ if (!job.enabled || job.runOnce)
46
+ continue;
47
+ let expected = null;
48
+ try {
49
+ const cronOptions = { paused: true };
50
+ if (job.timezone)
51
+ cronOptions.timezone = job.timezone;
52
+ const cron = new Cron(job.schedule, cronOptions);
53
+ expected = previousExpectedFire(cron, now);
54
+ cron.stop();
55
+ }
56
+ catch {
57
+ // Invalid cron expression — skip rather than crash the daemon.
58
+ continue;
59
+ }
60
+ if (!expected)
61
+ continue;
62
+ const latest = getLatestRun(job.name);
63
+ const lastRanAt = latest ? new Date(latest.startedAt) : null;
64
+ const isOverdue = !lastRanAt || lastRanAt.getTime() < expected.getTime() - GRACE_MS;
65
+ if (isOverdue) {
66
+ overdue.push({ name: job.name, expectedAt: expected, lastRanAt });
67
+ }
68
+ }
69
+ return overdue;
70
+ }
71
+ /** Fire a native desktop notification listing the overdue jobs. Best-effort —
72
+ * failures (missing `osascript`/`notify-send`, no display) are swallowed. */
73
+ export function notifyOverdue(jobs) {
74
+ if (jobs.length === 0)
75
+ return;
76
+ const title = jobs.length === 1
77
+ ? `Routine overdue: ${jobs[0].name}`
78
+ : `${jobs.length} routines overdue`;
79
+ const body = jobs.length === 1
80
+ ? `Missed ${jobs[0].expectedAt.toLocaleString()}. Run: agents routines catchup`
81
+ : `${jobs.map((j) => j.name).join(', ')} — agents routines catchup`;
82
+ const platform = os.platform();
83
+ try {
84
+ if (platform === 'darwin') {
85
+ const safeTitle = title.replace(/"/g, '\\"');
86
+ const safeBody = body.replace(/"/g, '\\"');
87
+ const child = spawn('osascript', ['-e', `display notification "${safeBody}" with title "${safeTitle}"`], { detached: true, stdio: 'ignore' });
88
+ child.unref();
89
+ }
90
+ else if (platform === 'linux') {
91
+ const child = spawn('notify-send', [title, body], {
92
+ detached: true,
93
+ stdio: 'ignore',
94
+ });
95
+ child.unref();
96
+ }
97
+ }
98
+ catch {
99
+ // Notification is best-effort; nothing to do.
100
+ }
101
+ }
@@ -97,6 +97,19 @@ export declare function removePermissionSet(name: string): {
97
97
  * Claude uses: { permissions: { allow: ["Bash(*)", "Read(**)"], deny: [] } }
98
98
  */
99
99
  export declare function convertToClaudeFormat(set: PermissionSet): ClaudePermissions;
100
+ /**
101
+ * Convert canonical permission set to Gemini format.
102
+ * Gemini reads tool allow-lists from settings.json under `tools.allowed`.
103
+ * Bash permissions map to run_shell_command(prefix) — the prefix is extracted
104
+ * from the canonical "Bash(cmd:*)" pattern by stripping the trailing ":*".
105
+ * Blanket Bash grants map to bare "run_shell_command" (no prefix filter).
106
+ * Non-Bash tool patterns are skipped; Gemini uses different tool names.
107
+ */
108
+ export declare function convertToGeminiFormat(set: PermissionSet): {
109
+ tools: {
110
+ allowed: string[];
111
+ };
112
+ };
100
113
  /**
101
114
  * Convert canonical permission set to OpenCode format.
102
115
  * OpenCode uses: { permission: { bash: { "git *": "allow", "rm *": "deny" } } }
@@ -14,9 +14,14 @@ import * as yaml from 'yaml';
14
14
  import * as TOML from 'smol-toml';
15
15
  import { getPermissionsDir, getUserPermissionsDir, ensureAgentsDir } from './state.js';
16
16
  import { safeJoin } from './paths.js';
17
+ import { updateGeminiSettings } from './gemini-settings.js';
17
18
  const HOME = os.homedir();
18
19
  /** Agents that support the permissions subsystem. */
19
- export const PERMISSIONS_CAPABLE_AGENTS = ['claude', 'codex', 'opencode'];
20
+ // antigravity: permissions in ~/.gemini/antigravity-cli/settings.json under
21
+ // `permissions: { allow: [...], deny: [...] }`. Serializer is a follow-up.
22
+ // grok: permissions via --allow/--deny CLI flags or [permission] block in
23
+ // ~/.grok/config.toml. Serializer is a follow-up.
24
+ export const PERMISSIONS_CAPABLE_AGENTS = ['claude', 'codex', 'opencode', 'antigravity', 'grok', 'gemini'];
20
25
  /** Filename used for Codex Starlark deny-rules generated from permission groups. */
21
26
  export const CODEX_RULES_FILENAME = 'agents-deny.rules';
22
27
  export function containsBroadGrants(rules) {
@@ -412,6 +417,35 @@ function parseCanonicalPattern(permission) {
412
417
  }
413
418
  /** Blanket-Bash canonical forms that mean "allow any bash command". */
414
419
  const BLANKET_BASH_FORMS = new Set(['Bash', 'Bash(*)', 'Bash(**)']);
420
+ /**
421
+ * Convert canonical permission set to Gemini format.
422
+ * Gemini reads tool allow-lists from settings.json under `tools.allowed`.
423
+ * Bash permissions map to run_shell_command(prefix) — the prefix is extracted
424
+ * from the canonical "Bash(cmd:*)" pattern by stripping the trailing ":*".
425
+ * Blanket Bash grants map to bare "run_shell_command" (no prefix filter).
426
+ * Non-Bash tool patterns are skipped; Gemini uses different tool names.
427
+ */
428
+ export function convertToGeminiFormat(set) {
429
+ const allowed = new Set();
430
+ for (const perm of set.allow) {
431
+ if (BLANKET_BASH_FORMS.has(perm)) {
432
+ allowed.add('run_shell_command');
433
+ continue;
434
+ }
435
+ const parsed = parseCanonicalPattern(perm);
436
+ if (!parsed || parsed.tool !== 'bash')
437
+ continue;
438
+ const colonIdx = parsed.pattern.lastIndexOf(':');
439
+ const prefix = colonIdx > 0 ? parsed.pattern.slice(0, colonIdx) : parsed.pattern;
440
+ if (prefix === '*' || prefix === '**') {
441
+ allowed.add('run_shell_command');
442
+ }
443
+ else {
444
+ allowed.add(`run_shell_command(${prefix})`);
445
+ }
446
+ }
447
+ return { tools: { allowed: Array.from(allowed) } };
448
+ }
415
449
  /**
416
450
  * Convert canonical permission set to OpenCode format.
417
451
  * OpenCode uses: { permission: { bash: { "git *": "allow", "rm *": "deny" } } }
@@ -877,6 +911,26 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
877
911
  }
878
912
  return { success: true };
879
913
  }
914
+ if (agentId === 'gemini') {
915
+ const geminiPerms = convertToGeminiFormat(set);
916
+ const settingsPath = path.join(versionHome, '.gemini', 'settings.json');
917
+ updateGeminiSettings(settingsPath, (settings) => {
918
+ // Remove stale permissions key written by earlier versions of this serializer.
919
+ delete settings.permissions;
920
+ const tools = (typeof settings.tools === 'object' && settings.tools !== null && !Array.isArray(settings.tools))
921
+ ? settings.tools
922
+ : {};
923
+ if (merge) {
924
+ const existing = Array.isArray(tools.allowed) ? tools.allowed : [];
925
+ tools.allowed = Array.from(new Set([...existing, ...geminiPerms.tools.allowed]));
926
+ }
927
+ else {
928
+ tools.allowed = geminiPerms.tools.allowed;
929
+ }
930
+ settings.tools = tools;
931
+ });
932
+ return { success: true };
933
+ }
880
934
  return { success: false, error: `Agent '${agentId}' does not support permissions` };
881
935
  }
882
936
  catch (err) {
@@ -111,7 +111,7 @@ export function registerMarketplace(agent, versionHome) {
111
111
  }
112
112
  }
113
113
  known[MARKETPLACE_NAME] = {
114
- source: { source: 'local', path: root },
114
+ source: { source: 'directory', path: root },
115
115
  installLocation: root,
116
116
  lastUpdated: new Date().toISOString(),
117
117
  };