@phnx-labs/agents-cli 1.19.1 → 1.20.0

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 (109) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +70 -10
  3. package/dist/commands/browser.js +88 -16
  4. package/dist/commands/cli.d.ts +14 -0
  5. package/dist/commands/cli.js +244 -0
  6. package/dist/commands/commands.js +3 -3
  7. package/dist/commands/computer.js +18 -1
  8. package/dist/commands/doctor.d.ts +1 -1
  9. package/dist/commands/doctor.js +2 -2
  10. package/dist/commands/exec.js +3 -3
  11. package/dist/commands/factory.d.ts +3 -14
  12. package/dist/commands/factory.js +3 -3
  13. package/dist/commands/hooks.js +3 -3
  14. package/dist/commands/mcp.js +29 -0
  15. package/dist/commands/plugins.js +11 -4
  16. package/dist/commands/profiles.js +1 -1
  17. package/dist/commands/prune.js +39 -160
  18. package/dist/commands/pull.js +56 -3
  19. package/dist/commands/routines.js +106 -13
  20. package/dist/commands/secrets.js +6 -8
  21. package/dist/commands/sessions.d.ts +36 -7
  22. package/dist/commands/sessions.js +130 -53
  23. package/dist/commands/setup.d.ts +1 -0
  24. package/dist/commands/setup.js +37 -28
  25. package/dist/commands/skills.js +3 -3
  26. package/dist/commands/teams.js +13 -0
  27. package/dist/commands/versions.d.ts +4 -3
  28. package/dist/commands/versions.js +147 -124
  29. package/dist/commands/view.js +12 -12
  30. package/dist/index.js +34 -6
  31. package/dist/lib/acp/harnesses.js +8 -0
  32. package/dist/lib/agents.js +162 -9
  33. package/dist/lib/browser/cdp.d.ts +8 -1
  34. package/dist/lib/browser/cdp.js +40 -3
  35. package/dist/lib/browser/chrome.d.ts +13 -0
  36. package/dist/lib/browser/chrome.js +42 -3
  37. package/dist/lib/browser/domain-skills.d.ts +51 -0
  38. package/dist/lib/browser/domain-skills.js +157 -0
  39. package/dist/lib/browser/drivers/local.js +45 -4
  40. package/dist/lib/browser/drivers/ssh.js +1 -1
  41. package/dist/lib/browser/ipc.d.ts +8 -1
  42. package/dist/lib/browser/ipc.js +37 -28
  43. package/dist/lib/browser/profiles.d.ts +13 -0
  44. package/dist/lib/browser/profiles.js +41 -1
  45. package/dist/lib/browser/service.d.ts +3 -0
  46. package/dist/lib/browser/service.js +21 -5
  47. package/dist/lib/browser/types.d.ts +7 -0
  48. package/dist/lib/cli-resources.d.ts +109 -0
  49. package/dist/lib/cli-resources.js +255 -0
  50. package/dist/lib/cloud/rush.js +5 -5
  51. package/dist/lib/command-skills.js +0 -2
  52. package/dist/lib/computer-rpc.d.ts +3 -0
  53. package/dist/lib/computer-rpc.js +53 -0
  54. package/dist/lib/daemon.js +20 -0
  55. package/dist/lib/exec.d.ts +3 -2
  56. package/dist/lib/exec.js +62 -6
  57. package/dist/lib/hooks.js +182 -0
  58. package/dist/lib/mcp.js +6 -0
  59. package/dist/lib/migrate.js +1 -1
  60. package/dist/lib/overdue.d.ts +26 -0
  61. package/dist/lib/overdue.js +101 -0
  62. package/dist/lib/permissions.js +5 -1
  63. package/dist/lib/plugin-marketplace.js +1 -1
  64. package/dist/lib/profiles-presets.js +37 -0
  65. package/dist/lib/registry.d.ts +18 -0
  66. package/dist/lib/registry.js +44 -0
  67. package/dist/lib/resources/mcp.js +43 -1
  68. package/dist/lib/resources/types.d.ts +1 -1
  69. package/dist/lib/resources.d.ts +1 -1
  70. package/dist/lib/rotate.js +10 -4
  71. package/dist/lib/routines-format.d.ts +35 -0
  72. package/dist/lib/routines-format.js +173 -0
  73. package/dist/lib/routines.d.ts +7 -1
  74. package/dist/lib/routines.js +32 -12
  75. package/dist/lib/runner.js +19 -5
  76. package/dist/lib/scheduler.js +8 -1
  77. package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/CodeResources +0 -0
  78. package/dist/lib/secrets/{AgentsKeychain.app/Contents/Info.plist → Agents CLI.app/Contents/Info.plist } +4 -2
  79. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  80. package/dist/lib/secrets/bundles.d.ts +33 -2
  81. package/dist/lib/secrets/bundles.js +249 -26
  82. package/dist/lib/secrets/index.d.ts +10 -1
  83. package/dist/lib/secrets/index.js +143 -48
  84. package/dist/lib/session/active.d.ts +8 -0
  85. package/dist/lib/session/active.js +3 -2
  86. package/dist/lib/session/db.d.ts +10 -4
  87. package/dist/lib/session/db.js +16 -16
  88. package/dist/lib/session/parse.d.ts +1 -0
  89. package/dist/lib/session/parse.js +44 -0
  90. package/dist/lib/session/types.d.ts +1 -1
  91. package/dist/lib/session/types.js +1 -1
  92. package/dist/lib/shims.d.ts +6 -2
  93. package/dist/lib/shims.js +88 -10
  94. package/dist/lib/state.d.ts +0 -1
  95. package/dist/lib/state.js +2 -15
  96. package/dist/lib/teams/agents.js +1 -1
  97. package/dist/lib/teams/parsers.d.ts +1 -1
  98. package/dist/lib/teams/parsers.js +153 -3
  99. package/dist/lib/teams/summarizer.js +18 -2
  100. package/dist/lib/teams/worktree.js +14 -3
  101. package/dist/lib/types.d.ts +7 -4
  102. package/dist/lib/types.js +6 -3
  103. package/dist/lib/versions.d.ts +10 -2
  104. package/dist/lib/versions.js +227 -35
  105. package/package.json +9 -9
  106. package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
  107. package/npm-shrinkwrap.json +0 -3162
  108. /package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/_CodeSignature/CodeResources +0 -0
  109. /package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/embedded.provisionprofile +0 -0
package/dist/lib/hooks.js CHANGED
@@ -683,8 +683,30 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
683
683
  if (agentId === 'gemini') {
684
684
  return registerHooksForGemini(versionHome, manifest, resolveScript, managedPrefixes);
685
685
  }
686
+ if (agentId === 'antigravity') {
687
+ return registerHooksForAntigravity(versionHome, manifest, resolveScript, managedPrefixes);
688
+ }
689
+ if (agentId === 'grok') {
690
+ return registerHooksForGrok(versionHome, manifest, resolveScript, managedPrefixes);
691
+ }
686
692
  return { registered: [], errors: [] };
687
693
  }
694
+ /**
695
+ * Antigravity (agy) event names differ from agents-cli manifest names. The
696
+ * mapping below is the documented agy schema. PostToolUse has no exact
697
+ * agy equivalent — agy fires `after_model_call` after the model finishes a
698
+ * turn (which includes any tool calls in that turn), so it's the closest
699
+ * lifecycle phase but not a 1:1 match. Manifest events not in this map are
700
+ * skipped silently (the manifest may declare events for other agents).
701
+ */
702
+ const ANTIGRAVITY_EVENT_MAP = {
703
+ PreToolUse: 'before_tool_call',
704
+ // Imperfect mapping: agy has no per-tool post-event. after_model_call
705
+ // fires once at the end of the turn, after all tool calls completed.
706
+ PostToolUse: 'after_model_call',
707
+ Stop: 'on_loop_stop',
708
+ OnError: 'on_error',
709
+ };
688
710
  /**
689
711
  * Gemini has no native UserPromptSubmit event — map it to BeforeAgent,
690
712
  * the closest lifecycle phase that fires before the model sees the prompt.
@@ -982,3 +1004,163 @@ function registerHooksForGemini(versionHome, manifest, resolveScript, managedPre
982
1004
  }
983
1005
  return { registered, errors };
984
1006
  }
1007
+ /**
1008
+ * Register hooks into antigravity's (agy) settings.json. Unlike gemini, agy uses
1009
+ * a flat per-event array of `{ command }` entries (no matcher groups). Events
1010
+ * are renamed via ANTIGRAVITY_EVENT_MAP; unmapped manifest events are skipped.
1011
+ *
1012
+ * settings.json lives at `${versionHome}/.gemini/antigravity-cli/settings.json`
1013
+ * because agy nests its config under the shared `.gemini` parent dir.
1014
+ */
1015
+ function registerHooksForAntigravity(versionHome, manifest, resolveScript, managedPrefixes) {
1016
+ const registered = [];
1017
+ const errors = [];
1018
+ const configDir = path.join(versionHome, '.gemini', 'antigravity-cli');
1019
+ const settingsPath = path.join(configDir, 'settings.json');
1020
+ let config = {};
1021
+ if (fs.existsSync(settingsPath)) {
1022
+ try {
1023
+ const parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
1024
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1025
+ config = parsed;
1026
+ }
1027
+ }
1028
+ catch {
1029
+ errors.push('Failed to parse antigravity settings.json');
1030
+ return { registered, errors };
1031
+ }
1032
+ }
1033
+ if (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) {
1034
+ config.hooks = {};
1035
+ }
1036
+ const hooks = config.hooks;
1037
+ // Build set of all command paths the current manifest will register, so we
1038
+ // can garbage-collect stale managed entries left over from renamed/deleted
1039
+ // hooks. Only managed paths are considered for removal — user-added entries
1040
+ // outside managedPrefixes are preserved.
1041
+ const currentManifestPaths = new Set();
1042
+ for (const hookDef of Object.values(manifest)) {
1043
+ if (!hookDef.events || hookDef.events.length === 0)
1044
+ continue;
1045
+ // Only paths whose events map to a known agy event would actually be
1046
+ // registered, so only those should survive GC.
1047
+ const anyMapped = hookDef.events.some((e) => ANTIGRAVITY_EVENT_MAP[e]);
1048
+ if (!anyMapped)
1049
+ continue;
1050
+ const resolved = resolveScript(hookDef.script);
1051
+ if (resolved)
1052
+ currentManifestPaths.add(resolved);
1053
+ }
1054
+ for (const eventKey of Object.keys(hooks)) {
1055
+ const entries = hooks[eventKey];
1056
+ if (!Array.isArray(entries))
1057
+ continue;
1058
+ hooks[eventKey] = entries.filter((entry) => {
1059
+ if (!entry || typeof entry !== 'object')
1060
+ return true;
1061
+ const cmd = entry.command;
1062
+ if (typeof cmd !== 'string')
1063
+ return true;
1064
+ if (!isManagedHookCommand(cmd, managedPrefixes))
1065
+ return true;
1066
+ return currentManifestPaths.has(cmd);
1067
+ });
1068
+ if (hooks[eventKey].length === 0) {
1069
+ delete hooks[eventKey];
1070
+ }
1071
+ }
1072
+ for (const [name, hookDef] of Object.entries(manifest)) {
1073
+ if (!hookDef.events || hookDef.events.length === 0)
1074
+ continue;
1075
+ const commandPath = resolveScript(hookDef.script);
1076
+ if (!commandPath) {
1077
+ errors.push(`${name}: script not found in user or system hooks dir`);
1078
+ continue;
1079
+ }
1080
+ for (const event of hookDef.events) {
1081
+ const agyEvent = ANTIGRAVITY_EVENT_MAP[event];
1082
+ if (!agyEvent)
1083
+ continue; // unmapped event — silently skip
1084
+ if (!hooks[agyEvent]) {
1085
+ hooks[agyEvent] = [];
1086
+ }
1087
+ const list = hooks[agyEvent];
1088
+ const existingIdx = list.findIndex((e) => e && typeof e === 'object' && e.command === commandPath);
1089
+ const entry = { command: commandPath };
1090
+ if (existingIdx >= 0) {
1091
+ list[existingIdx] = entry;
1092
+ }
1093
+ else {
1094
+ list.push(entry);
1095
+ }
1096
+ registered.push(`${name} -> ${agyEvent}`);
1097
+ }
1098
+ }
1099
+ try {
1100
+ fs.mkdirSync(configDir, { recursive: true });
1101
+ fs.writeFileSync(settingsPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
1102
+ }
1103
+ catch (err) {
1104
+ errors.push(`Failed to write antigravity settings.json: ${err.message}`);
1105
+ }
1106
+ return { registered, errors };
1107
+ }
1108
+ /**
1109
+ * Register hooks for Grok Build.
1110
+ * Grok uses per-event JSON files under .grok/hooks/ (e.g. session-start.json).
1111
+ */
1112
+ function registerHooksForGrok(versionHome, manifest, resolveScript, managedPrefixes) {
1113
+ const registered = [];
1114
+ const errors = [];
1115
+ const grokHooksDir = path.join(versionHome, '.grok', 'hooks');
1116
+ fs.mkdirSync(grokHooksDir, { recursive: true });
1117
+ const eventMap = {
1118
+ SessionStart: 'SessionStart',
1119
+ SessionEnd: 'SessionEnd',
1120
+ UserPromptSubmit: 'UserPromptSubmit',
1121
+ PreToolUse: 'PreToolUse',
1122
+ PostToolUse: 'PostToolUse',
1123
+ PreCompact: 'PreCompact',
1124
+ Stop: 'Stop',
1125
+ Notification: 'Notification',
1126
+ };
1127
+ const grokHooks = { hooks: {} };
1128
+ for (const [name, hookDef] of Object.entries(manifest)) {
1129
+ if (!hookDef.events || hookDef.events.length === 0)
1130
+ continue;
1131
+ const commandPath = resolveScript(hookDef.script);
1132
+ if (!commandPath) {
1133
+ errors.push(`${name}: script not found`);
1134
+ continue;
1135
+ }
1136
+ const timeout = hookDef.timeout ?? 30;
1137
+ for (const ev of hookDef.events) {
1138
+ const grokEvent = eventMap[ev] || ev;
1139
+ if (!grokHooks.hooks[grokEvent]) {
1140
+ grokHooks.hooks[grokEvent] = [];
1141
+ }
1142
+ grokHooks.hooks[grokEvent].push({
1143
+ hooks: [{ type: 'command', command: commandPath, timeout }],
1144
+ });
1145
+ registered.push(`${name} -> ${grokEvent}`);
1146
+ }
1147
+ }
1148
+ const mainHooksPath = path.join(grokHooksDir, 'hooks.json');
1149
+ try {
1150
+ fs.writeFileSync(mainHooksPath, JSON.stringify(grokHooks, null, 2));
1151
+ }
1152
+ catch (e) {
1153
+ errors.push(`Failed to write hooks.json: ${e.message}`);
1154
+ }
1155
+ for (const [eventName, groups] of Object.entries(grokHooks.hooks)) {
1156
+ const fileName = eventName.toLowerCase().replace(/([a-z])([A-Z])/g, '$1-$2') + '.json';
1157
+ const eventFile = path.join(grokHooksDir, fileName);
1158
+ try {
1159
+ fs.writeFileSync(eventFile, JSON.stringify({ hooks: { [eventName]: groups } }, null, 2));
1160
+ }
1161
+ catch (e) {
1162
+ errors.push(`Failed to write ${fileName}: ${e.message}`);
1163
+ }
1164
+ }
1165
+ return { registered, errors };
1166
+ }
package/dist/lib/mcp.js CHANGED
@@ -348,6 +348,12 @@ export function installMcpServers(agentId, version, versionHome, mcpNames, optio
348
348
  installMcpToOpenCodeConfig(server, versionHome);
349
349
  applied.push(server.name);
350
350
  }
351
+ else if (agentId === 'grok') {
352
+ // Grok primarily uses [mcp_servers] in ~/.grok/config.toml (or project .grok/config.toml).
353
+ // We have the path helper; full writer can be added (reuse codex toml pattern).
354
+ // For now the general sync + toml editing via agents mcp works via the path helpers.
355
+ applied.push(server.name);
356
+ }
351
357
  }
352
358
  catch (err) {
353
359
  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
+ }
@@ -16,7 +16,11 @@ import { getPermissionsDir, getUserPermissionsDir, ensureAgentsDir } from './sta
16
16
  import { safeJoin } from './paths.js';
17
17
  const HOME = os.homedir();
18
18
  /** Agents that support the permissions subsystem. */
19
- export const PERMISSIONS_CAPABLE_AGENTS = ['claude', 'codex', 'opencode'];
19
+ // antigravity: permissions in ~/.gemini/antigravity-cli/settings.json under
20
+ // `permissions: { allow: [...], deny: [...] }`. Serializer is a follow-up.
21
+ // grok: permissions via --allow/--deny CLI flags or [permission] block in
22
+ // ~/.grok/config.toml. Serializer is a follow-up.
23
+ export const PERMISSIONS_CAPABLE_AGENTS = ['claude', 'codex', 'opencode', 'antigravity', 'grok'];
20
24
  /** Filename used for Codex Starlark deny-rules generated from permission groups. */
21
25
  export const CODEX_RULES_FILENAME = 'agents-deny.rules';
22
26
  export function containsBroadGrants(rules) {
@@ -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
  };
@@ -88,6 +88,43 @@ export const PRESETS = [
88
88
  ANTHROPIC_SMALL_FAST_MODEL: 'deepseek/deepseek-chat-v3-0324',
89
89
  },
90
90
  },
91
+ // ----- xAI Grok Build CLI (native host) -----
92
+ {
93
+ name: 'grok-fast',
94
+ description: 'xAI Grok Build CLI — fast tier. Native grok host, no OpenRouter wrapper.',
95
+ provider: 'xai',
96
+ host: 'grok',
97
+ authEnvVar: 'XAI_API_KEY',
98
+ signupUrl: 'https://console.x.ai',
99
+ env: {
100
+ // TODO: confirm model id (docs.x.ai/build/models, May 2026)
101
+ GROK_MODEL: 'grok-build-fast',
102
+ },
103
+ },
104
+ {
105
+ name: 'grok-heavy',
106
+ description: 'xAI Grok Build CLI — heavy tier (SuperGrok). Native grok host.',
107
+ provider: 'xai',
108
+ host: 'grok',
109
+ authEnvVar: 'XAI_API_KEY',
110
+ signupUrl: 'https://console.x.ai',
111
+ env: {
112
+ // TODO: confirm model id (docs.x.ai/build/models, May 2026)
113
+ GROK_MODEL: 'grok-build',
114
+ },
115
+ },
116
+ // ----- Google Antigravity CLI (native host) -----
117
+ {
118
+ name: 'agy',
119
+ description: 'Google Antigravity CLI default. Auth via Google OAuth or ANTIGRAVITY_API_KEY.',
120
+ provider: 'google',
121
+ host: 'antigravity',
122
+ authEnvVar: 'ANTIGRAVITY_API_KEY',
123
+ signupUrl: 'https://antigravity.google',
124
+ env: {
125
+ // TODO: confirm model id — antigravity defaults are managed by the CLI itself
126
+ },
127
+ },
91
128
  ];
92
129
  /** Look up a preset by name (case-sensitive). */
93
130
  export function getPreset(name) {
@@ -24,6 +24,24 @@ export declare function searchMcpRegistries(query: string, options?: {
24
24
  registry?: string;
25
25
  limit?: number;
26
26
  }): Promise<RegistrySearchResult[]>;
27
+ /**
28
+ * Convert an MCP server registry entry into an install spec suitable for
29
+ * writing into `manifest.mcp`. Returns `null` if the entry has no package we
30
+ * know how to launch (e.g. only remote endpoints, which the current manifest
31
+ * shape supports via `url`+`transport: 'http'` but isn't yet wired to the
32
+ * registry's `remotes` field).
33
+ *
34
+ * Supported package shapes:
35
+ * - npm / runtime=node → `npx -y <name>`
36
+ * - pypi / runtime=python → `uvx <name>`
37
+ * - runtime=docker → `docker run --rm -i <name>`
38
+ * - runtime=binary → `<name>` (assumed to be on PATH)
39
+ */
40
+ export declare function mcpEntryToInstallSpec(entry: McpServerEntry): {
41
+ command?: string;
42
+ url?: string;
43
+ transport: 'stdio' | 'http';
44
+ } | null;
27
45
  /** Look up detailed info for an MCP server by exact name. */
28
46
  export declare function getMcpServerInfo(serverName: string, registryName?: string): Promise<McpServerEntry | null>;
29
47
  /** Search skill registries for entries matching a query string. */
@@ -113,6 +113,50 @@ export async function searchMcpRegistries(query, options) {
113
113
  }
114
114
  return results;
115
115
  }
116
+ /**
117
+ * Convert an MCP server registry entry into an install spec suitable for
118
+ * writing into `manifest.mcp`. Returns `null` if the entry has no package we
119
+ * know how to launch (e.g. only remote endpoints, which the current manifest
120
+ * shape supports via `url`+`transport: 'http'` but isn't yet wired to the
121
+ * registry's `remotes` field).
122
+ *
123
+ * Supported package shapes:
124
+ * - npm / runtime=node → `npx -y <name>`
125
+ * - pypi / runtime=python → `uvx <name>`
126
+ * - runtime=docker → `docker run --rm -i <name>`
127
+ * - runtime=binary → `<name>` (assumed to be on PATH)
128
+ */
129
+ export function mcpEntryToInstallSpec(entry) {
130
+ const pkg = entry.packages?.[0];
131
+ if (!pkg)
132
+ return null;
133
+ // Remote transports (sse / streamable-http) need a URL the registry doesn't
134
+ // currently expose in this client's type. Skip for now; caller can fall back
135
+ // to manual --transport http with an explicit URL.
136
+ if (pkg.transport === 'sse' || pkg.transport === 'streamable-http') {
137
+ return null;
138
+ }
139
+ const reg = pkg.registry_name?.toLowerCase();
140
+ const runtime = pkg.runtime;
141
+ const name = pkg.name;
142
+ if (!name)
143
+ return null;
144
+ if (reg === 'npm' || runtime === 'node') {
145
+ return { command: `npx -y ${name}`, transport: 'stdio' };
146
+ }
147
+ if (reg === 'pypi' || runtime === 'python') {
148
+ return { command: `uvx ${name}`, transport: 'stdio' };
149
+ }
150
+ if (runtime === 'docker') {
151
+ return { command: `docker run --rm -i ${name}`, transport: 'stdio' };
152
+ }
153
+ if (runtime === 'binary') {
154
+ return { command: name, transport: 'stdio' };
155
+ }
156
+ // Unknown registry/runtime — fall back to bare name so the user gets *something*
157
+ // to inspect via `agents mcp view`, rather than a silent miss.
158
+ return { command: name, transport: 'stdio' };
159
+ }
116
160
  /** Look up detailed info for an MCP server by exact name. */
117
161
  export async function getMcpServerInfo(serverName, registryName) {
118
162
  const registries = getEnabledRegistries('mcp');
@@ -15,7 +15,7 @@ import * as yaml from 'yaml';
15
15
  import * as TOML from 'smol-toml';
16
16
  import { getSystemMcpDir, getUserMcpDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../state.js';
17
17
  /** Agents from resources/types.ts that support MCP. */
18
- const MCP_CAPABLE_AGENTS = ['claude', 'codex', 'gemini', 'cursor', 'opencode', 'openclaw'];
18
+ const MCP_CAPABLE_AGENTS = ['claude', 'codex', 'gemini', 'cursor', 'opencode', 'openclaw', 'antigravity', 'grok'];
19
19
  /**
20
20
  * Parse an MCP YAML file into an McpItem.
21
21
  */
@@ -119,6 +119,11 @@ export function getMcpConfigPath(agent, versionHome) {
119
119
  return path.join(versionHome, '.gemini', 'settings.json');
120
120
  case 'openclaw':
121
121
  return path.join(versionHome, '.openclaw', 'openclaw.json');
122
+ case 'antigravity':
123
+ // agy nests under ~/.gemini/antigravity-cli/ (shared parent with Gemini, distinct subdir).
124
+ return path.join(versionHome, '.gemini', 'antigravity-cli', 'mcp_config.json');
125
+ case 'grok':
126
+ return path.join(versionHome, '.grok', 'mcp.json');
122
127
  default:
123
128
  return null;
124
129
  }
@@ -234,6 +239,39 @@ function syncToCodexConfig(configPath, items) {
234
239
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
235
240
  fs.writeFileSync(configPath, TOML.stringify(config), 'utf-8');
236
241
  }
242
+ /**
243
+ * Write MCP servers to Grok config.toml format ([mcp_servers] section).
244
+ */
245
+ function syncToGrokConfig(configPath, items) {
246
+ let config = {};
247
+ if (fs.existsSync(configPath)) {
248
+ try {
249
+ config = TOML.parse(fs.readFileSync(configPath, 'utf-8'));
250
+ }
251
+ catch {
252
+ config = {};
253
+ }
254
+ }
255
+ const mcpServers = {};
256
+ for (const item of items) {
257
+ if (item.transport === 'stdio') {
258
+ mcpServers[item.name] = {
259
+ command: item.command,
260
+ args: item.args || [],
261
+ ...(item.env && { env: item.env }),
262
+ };
263
+ }
264
+ else if (item.transport === 'http' || item.transport === 'sse') {
265
+ mcpServers[item.name] = {
266
+ url: item.url,
267
+ ...(item.headers && { headers: item.headers }),
268
+ };
269
+ }
270
+ }
271
+ config.mcp_servers = mcpServers;
272
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
273
+ fs.writeFileSync(configPath, TOML.stringify(config), 'utf-8');
274
+ }
237
275
  /**
238
276
  * Write MCP servers to OpenCode opencode.jsonc format.
239
277
  */
@@ -462,11 +500,15 @@ export const McpHandler = {
462
500
  case 'openclaw':
463
501
  syncToOpenClawConfig(configPath, mcpItems);
464
502
  break;
503
+ case 'grok':
504
+ syncToGrokConfig(configPath, mcpItems);
505
+ break;
465
506
  }
466
507
  },
467
508
  format(agent) {
468
509
  switch (agent) {
469
510
  case 'codex':
511
+ case 'grok':
470
512
  return 'toml';
471
513
  default:
472
514
  return 'json';
@@ -5,7 +5,7 @@
5
5
  * - Union: All resources from all layers are combined
6
6
  * - Override on name conflict: Higher layer wins (project > user > system)
7
7
  */
8
- export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw';
8
+ export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'antigravity' | 'grok';
9
9
  export type Layer = 'system' | 'user' | 'project';
10
10
  export type ResourceKind = 'command' | 'hook' | 'skill' | 'rule' | 'mcp' | 'permission' | 'subagent' | 'workflow';
11
11
  /** A resolved resource with its origin layer. */
@@ -5,7 +5,7 @@
5
5
  import type { AgentId } from './types.js';
6
6
  import { type SkillParseError } from './skills.js';
7
7
  /** Resource kind — matches the subdirectory name under each repo root. */
8
- export type ResourceKind = 'commands' | 'skills' | 'hooks' | 'rules' | 'mcp' | 'permissions' | 'subagents' | 'profiles' | 'secrets';
8
+ export type ResourceKind = 'commands' | 'skills' | 'hooks' | 'rules' | 'mcp' | 'cli' | 'permissions' | 'subagents' | 'profiles' | 'secrets';
9
9
  /** A resource resolved with its origin. */
10
10
  export interface ResolvedResource {
11
11
  name: string;
@@ -10,7 +10,7 @@ import * as yaml from 'yaml';
10
10
  import { getAccountInfo } from './agents.js';
11
11
  import { readMeta, writeMeta, getHelpersDir, getUserAgentsDir } from './state.js';
12
12
  import { listInstalledVersions, getVersionHomePath, resolveVersion } from './versions.js';
13
- import { getUsageInfoByIdentity, getUsageLookupKey, isClaudeAuthValid, } from './usage.js';
13
+ import { getUsageInfoByIdentity, getUsageLookupKey, } from './usage.js';
14
14
  function getRotateDir() {
15
15
  const dir = path.join(getHelpersDir(), 'rotate');
16
16
  fs.mkdirSync(dir, { recursive: true });
@@ -233,9 +233,15 @@ async function collectRunCandidates(agent) {
233
233
  const rows = await Promise.all(versions.map(async (version) => {
234
234
  const home = getVersionHomePath(agent, version);
235
235
  const info = await getAccountInfo(agent, home);
236
- const authValid = info.email
237
- ? agent === 'claude' ? await isClaudeAuthValid(home) : true
238
- : false;
236
+ // `info.email` (from .claude.json's oauthAccount) is the auth heuristic.
237
+ // We used to additionally call isClaudeAuthValid(home), which reads
238
+ // "Claude Code-credentials-<hash>" from the system keychain. That item is
239
+ // written by Claude Code itself with its own process in the ACL, so our
240
+ // helper triggers a macOS keychain-authorization sheet on every probe —
241
+ // one per installed version, every time `agents run` cold-starts. If
242
+ // claude's stored token has actually expired, the spawned agent detects
243
+ // it at its own startup and re-auths; that's the correct UX.
244
+ const authValid = info.email != null;
239
245
  return {
240
246
  agent,
241
247
  version,
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Pure display helpers for `agents routines list`.
3
+ *
4
+ * No external dependencies. All functions are pure (no I/O, no side effects).
5
+ */
6
+ /**
7
+ * Convert a cron expression to a human-readable phrase.
8
+ *
9
+ * Handles the common patterns. For anything unrecognized, returns the raw
10
+ * expression so the user still sees something useful. NEVER throws.
11
+ */
12
+ export declare function humanizeCron(expr: string, _tz?: string): string;
13
+ /**
14
+ * Convert a next-run Date into a human phrase relative to `now`.
15
+ *
16
+ * - null → '-'
17
+ * - same calendar day → 'today 9:00 AM'
18
+ * - next calendar day → 'tomorrow 9:00 AM'
19
+ * - within 7 days → 'Mon 9:00 AM'
20
+ * - further out → 'Jun 15, 9:00 AM'
21
+ */
22
+ export declare function humanizeNextRun(date: Date | null, now: Date, tz?: string): string;
23
+ /**
24
+ * Parse a repo string into a display label and an optional hyperlink target.
25
+ *
26
+ * Rules:
27
+ * - undefined / empty → display '-', href null
28
+ * - 'owner/name' (one slash) → display 'owner/name', href 'https://github.com/owner/name/pulls'
29
+ * - 'https://...' or 'http://…' → display hostname+path, href the URL verbatim
30
+ * - anything else → display raw string, href null
31
+ */
32
+ export declare function formatRepoLink(repo: string | undefined): {
33
+ display: string;
34
+ href: string | null;
35
+ };