@phnx-labs/agents-cli 1.19.2 → 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 (103) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +69 -9
  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/commands.js +3 -3
  8. package/dist/commands/computer.js +18 -1
  9. package/dist/commands/doctor.d.ts +1 -1
  10. package/dist/commands/doctor.js +2 -2
  11. package/dist/commands/exec.js +3 -3
  12. package/dist/commands/factory.d.ts +3 -14
  13. package/dist/commands/factory.js +3 -3
  14. package/dist/commands/hooks.js +3 -3
  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 +5 -7
  21. package/dist/commands/sessions.d.ts +28 -0
  22. package/dist/commands/sessions.js +98 -33
  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 +131 -127
  29. package/dist/commands/view.js +12 -12
  30. package/dist/computer.js +0 -0
  31. package/dist/index.js +34 -6
  32. package/dist/lib/acp/harnesses.js +8 -0
  33. package/dist/lib/agents.js +110 -23
  34. package/dist/lib/browser/cdp.d.ts +8 -1
  35. package/dist/lib/browser/cdp.js +40 -3
  36. package/dist/lib/browser/chrome.d.ts +13 -0
  37. package/dist/lib/browser/chrome.js +42 -3
  38. package/dist/lib/browser/domain-skills.d.ts +51 -0
  39. package/dist/lib/browser/domain-skills.js +157 -0
  40. package/dist/lib/browser/drivers/local.js +45 -4
  41. package/dist/lib/browser/drivers/ssh.js +1 -1
  42. package/dist/lib/browser/ipc.d.ts +8 -1
  43. package/dist/lib/browser/ipc.js +37 -28
  44. package/dist/lib/browser/profiles.d.ts +13 -0
  45. package/dist/lib/browser/profiles.js +41 -1
  46. package/dist/lib/browser/service.d.ts +3 -0
  47. package/dist/lib/browser/service.js +21 -5
  48. package/dist/lib/browser/types.d.ts +7 -0
  49. package/dist/lib/cli-resources.d.ts +109 -0
  50. package/dist/lib/cli-resources.js +255 -0
  51. package/dist/lib/cloud/rush.js +5 -5
  52. package/dist/lib/command-skills.js +0 -2
  53. package/dist/lib/computer-rpc.d.ts +3 -0
  54. package/dist/lib/computer-rpc.js +53 -0
  55. package/dist/lib/daemon.js +20 -0
  56. package/dist/lib/exec.d.ts +3 -2
  57. package/dist/lib/exec.js +44 -9
  58. package/dist/lib/hooks.js +182 -0
  59. package/dist/lib/mcp.js +6 -0
  60. package/dist/lib/migrate.js +1 -1
  61. package/dist/lib/overdue.d.ts +26 -0
  62. package/dist/lib/overdue.js +101 -0
  63. package/dist/lib/permissions.js +5 -1
  64. package/dist/lib/plugin-marketplace.js +1 -1
  65. package/dist/lib/profiles-presets.js +37 -0
  66. package/dist/lib/resources/mcp.js +37 -0
  67. package/dist/lib/resources.d.ts +1 -1
  68. package/dist/lib/rotate.js +10 -4
  69. package/dist/lib/routines-format.d.ts +35 -0
  70. package/dist/lib/routines-format.js +173 -0
  71. package/dist/lib/routines.d.ts +7 -1
  72. package/dist/lib/routines.js +32 -12
  73. package/dist/lib/runner.js +19 -5
  74. package/dist/lib/scheduler.js +8 -1
  75. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  76. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  77. package/dist/lib/secrets/bundles.d.ts +22 -1
  78. package/dist/lib/secrets/bundles.js +234 -36
  79. package/dist/lib/secrets/index.d.ts +6 -11
  80. package/dist/lib/secrets/index.js +107 -87
  81. package/dist/lib/session/active.d.ts +8 -0
  82. package/dist/lib/session/active.js +3 -2
  83. package/dist/lib/session/db.d.ts +0 -4
  84. package/dist/lib/session/db.js +0 -26
  85. package/dist/lib/session/parse.d.ts +1 -0
  86. package/dist/lib/session/parse.js +44 -0
  87. package/dist/lib/session/types.d.ts +1 -1
  88. package/dist/lib/session/types.js +1 -1
  89. package/dist/lib/shims.d.ts +1 -1
  90. package/dist/lib/shims.js +66 -4
  91. package/dist/lib/state.d.ts +0 -1
  92. package/dist/lib/state.js +2 -15
  93. package/dist/lib/teams/agents.js +1 -1
  94. package/dist/lib/teams/parsers.d.ts +1 -1
  95. package/dist/lib/teams/parsers.js +153 -3
  96. package/dist/lib/teams/summarizer.js +18 -2
  97. package/dist/lib/teams/worktree.js +14 -3
  98. package/dist/lib/types.d.ts +6 -3
  99. package/dist/lib/types.js +6 -3
  100. package/dist/lib/versions.d.ts +10 -2
  101. package/dist/lib/versions.js +227 -35
  102. package/package.json +7 -7
  103. package/npm-shrinkwrap.json +0 -3162
@@ -73,11 +73,11 @@ function readLiveTerminals() {
73
73
  if (!parsed || typeof parsed !== 'object')
74
74
  return [];
75
75
  const merged = new Map();
76
- for (const slice of Object.values(parsed)) {
76
+ for (const [windowId, slice] of Object.entries(parsed)) {
77
77
  for (const e of (slice?.entries ?? [])) {
78
78
  if (!e?.sessionId || !isPidAlive(e.pid))
79
79
  continue;
80
- merged.set(e.sessionId, e);
80
+ merged.set(e.sessionId, { ...e, windowId });
81
81
  }
82
82
  }
83
83
  return Array.from(merged.values());
@@ -259,6 +259,7 @@ export async function listTerminalsActive() {
259
259
  sessionFile,
260
260
  startedAtMs: t.startedAtMs,
261
261
  status: classifyActivity(sessionFile),
262
+ windowId: t.windowId,
262
263
  };
263
264
  });
264
265
  }
@@ -155,8 +155,6 @@ export declare function getRowCount(): {
155
155
  sessions: number;
156
156
  textRows: number;
157
157
  };
158
- /** Count sessions older than the given timestamp (for dry-run previews). */
159
- export declare function countSessionsOlderThan(cutoffMs: number): number;
160
158
  /**
161
159
  * Rewrite file_path for all sessions whose path starts with oldPrefix, replacing
162
160
  * it with newPrefix + the unchanged suffix. Also clears the matching scan_ledger
@@ -167,5 +165,3 @@ export declare function countSessionsOlderThan(cutoffMs: number): number;
167
165
  * Returns the number of session rows updated.
168
166
  */
169
167
  export declare function updateSessionFilePaths(oldPrefix: string, newPrefix: string): number;
170
- /** Delete sessions older than the given timestamp. Returns the number of rows deleted. */
171
- export declare function deleteSessionsOlderThan(cutoffMs: number): number;
@@ -742,13 +742,6 @@ export function getRowCount() {
742
742
  const textRows = db.prepare(`SELECT COUNT(*) AS c FROM session_text`).get().c;
743
743
  return { sessions, textRows };
744
744
  }
745
- /** Count sessions older than the given timestamp (for dry-run previews). */
746
- export function countSessionsOlderThan(cutoffMs) {
747
- const db = getDB();
748
- const cutoffIso = new Date(cutoffMs).toISOString();
749
- const row = db.prepare(`SELECT COUNT(*) AS n FROM sessions WHERE timestamp < ?`).get(cutoffIso);
750
- return row.n;
751
- }
752
745
  /**
753
746
  * Rewrite file_path for all sessions whose path starts with oldPrefix, replacing
754
747
  * it with newPrefix + the unchanged suffix. Also clears the matching scan_ledger
@@ -775,22 +768,3 @@ export function updateSessionFilePaths(oldPrefix, newPrefix) {
775
768
  txn();
776
769
  return rows.length;
777
770
  }
778
- /** Delete sessions older than the given timestamp. Returns the number of rows deleted. */
779
- export function deleteSessionsOlderThan(cutoffMs) {
780
- const db = getDB();
781
- const cutoffIso = new Date(cutoffMs).toISOString();
782
- const rows = db.prepare(`SELECT id, file_path FROM sessions WHERE timestamp < ?`).all(cutoffIso);
783
- if (rows.length === 0)
784
- return 0;
785
- const txn = db.transaction(() => {
786
- for (const { id, file_path } of rows) {
787
- db.prepare(`DELETE FROM session_text WHERE session_id = ?`).run(id);
788
- db.prepare(`DELETE FROM sessions WHERE id = ?`).run(id);
789
- if (file_path) {
790
- db.prepare(`DELETE FROM scan_ledger WHERE file_path = ?`).run(canonicalLedgerKey(file_path));
791
- }
792
- }
793
- });
794
- txn();
795
- return rows.length;
796
- }
@@ -43,6 +43,7 @@ export declare function parseGemini(filePath: string): SessionEvent[];
43
43
  * Messages have role (user/assistant) and metadata.
44
44
  * Parts contain the actual content: text, tool, reasoning, patch, step-start/finish.
45
45
  */
46
+ export declare function parseGrok(filePath: string): SessionEvent[];
46
47
  export declare function parseOpenCode(filePath: string): SessionEvent[];
47
48
  /** Parse a Rush JSONL session file into normalized events. */
48
49
  export declare function parseRush(filePath: string): SessionEvent[];
@@ -93,6 +93,9 @@ export function parseSession(filePath, agent) {
93
93
  case 'opencode':
94
94
  events = parseOpenCode(filePath);
95
95
  break;
96
+ case 'grok':
97
+ events = parseGrok(filePath);
98
+ break;
96
99
  case 'rush':
97
100
  events = parseRush(filePath);
98
101
  break;
@@ -118,6 +121,8 @@ export function detectAgent(filePath) {
118
121
  return 'codex';
119
122
  if (filePath.includes('/.gemini/') || filePath.includes('\\.gemini\\'))
120
123
  return 'gemini';
124
+ if (filePath.includes('/.grok/') || filePath.includes('\\.grok\\'))
125
+ return 'grok';
121
126
  if (filePath.includes('/.rush/') || filePath.includes('\\.rush\\'))
122
127
  return 'rush';
123
128
  if (filePath.includes('/.hermes/') || filePath.includes('\\.hermes\\'))
@@ -645,6 +650,45 @@ function extractGeminiContent(content) {
645
650
  * Messages have role (user/assistant) and metadata.
646
651
  * Parts contain the actual content: text, tool, reasoning, patch, step-start/finish.
647
652
  */
653
+ export function parseGrok(filePath) {
654
+ // Grok sessions are rich (summary.json + events.jsonl + chat_history.jsonl + updates.jsonl)
655
+ // This is a minimal stub for now so grok appears in `agents sessions`.
656
+ // Full parser (with subagents, tool calls, etc.) can be expanded later.
657
+ try {
658
+ const content = fs.readFileSync(filePath, 'utf-8');
659
+ // If it's a summary.json, create a basic event
660
+ if (filePath.endsWith('summary.json')) {
661
+ const summary = JSON.parse(content);
662
+ return [{
663
+ timestamp: summary.created_at || new Date().toISOString(),
664
+ type: 'session_start',
665
+ content: summary.session_summary || 'Grok session',
666
+ agent: 'grok',
667
+ metadata: { sessionId: summary.id, cwd: summary.cwd },
668
+ }];
669
+ }
670
+ // For JSONL files (events, chat_history, updates), return basic parsed lines
671
+ if (filePath.endsWith('.jsonl')) {
672
+ const lines = content.trim().split('\n').filter(Boolean);
673
+ return lines.slice(0, 50).map((line, i) => {
674
+ try {
675
+ const obj = JSON.parse(line);
676
+ return {
677
+ timestamp: obj.timestamp || obj.ts || new Date().toISOString(),
678
+ type: obj.type || obj.method || 'grok_event',
679
+ content: typeof obj.content === 'string' ? obj.content : JSON.stringify(obj).slice(0, 200),
680
+ agent: 'grok',
681
+ };
682
+ }
683
+ catch {
684
+ return { timestamp: new Date().toISOString(), type: 'raw', content: line.slice(0, 200), agent: 'grok' };
685
+ }
686
+ });
687
+ }
688
+ }
689
+ catch { }
690
+ return [];
691
+ }
648
692
  export function parseOpenCode(filePath) {
649
693
  const [dbPath, sessionId] = filePath.split('#');
650
694
  if (!dbPath || !sessionId)
@@ -7,7 +7,7 @@
7
7
  * speaks these types.
8
8
  */
9
9
  /** Agents that store session data on disk and can be discovered by `agents sessions`. */
10
- export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'opencode' | 'openclaw' | 'rush' | 'hermes';
10
+ export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'opencode' | 'openclaw' | 'rush' | 'hermes' | 'grok';
11
11
  /** All agents with session discovery support, in display order. */
12
12
  export declare const SESSION_AGENTS: SessionAgentId[];
13
13
  /** A single normalized event within a session (message, tool call, thinking, etc.). */
@@ -7,4 +7,4 @@
7
7
  * speaks these types.
8
8
  */
9
9
  /** All agents with session discovery support, in display order. */
10
- export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'opencode', 'openclaw', 'rush', 'hermes'];
10
+ export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'opencode', 'openclaw', 'rush', 'hermes', 'grok'];
@@ -213,7 +213,7 @@ export declare function getPathShadowingExecutable(agent: AgentId): string | nul
213
213
  * Delete the legacy ~/.agents/shims/<cli> file if it exists, returning whether
214
214
  * anything was removed. Pre-split installs put shims under ~/.agents/shims/;
215
215
  * the new layout uses ~/.agents-system/shims/. The leftover file causes the
216
- * repair-prompt loop reported in EXAMPLE-664 — `getPathShadowingExecutable` flags
216
+ * repair-prompt loop reported in PROJ-789 — `getPathShadowingExecutable` flags
217
217
  * it as a shadow but `addShimsToPath` only edits rc files, never the file
218
218
  * itself. Removing it ends the loop.
219
219
  */
package/dist/lib/shims.js CHANGED
@@ -226,7 +226,22 @@ fi
226
226
  # written by agents-cli actually take effect.
227
227
  export CODEX_HOME="$VERSION_DIR/home/${configDirName}"
228
228
  `
229
- : '';
229
+ : agent === 'copilot'
230
+ ? `
231
+ # GitHub Copilot CLI honors COPILOT_HOME to relocate its config and state
232
+ # (settings.json, mcp-config.json, session-state/, logs/, plugins/). Point
233
+ # it at the versioned home so MCP servers, custom agents, and session
234
+ # history are isolated per copilot version.
235
+ export COPILOT_HOME="$VERSION_DIR/home/${configDirName}"
236
+ `
237
+ : agent === 'grok'
238
+ ? `
239
+ # Grok Build uses GROK_HOME to isolate its entire configuration tree
240
+ # (skills, hooks, plugins, agents, memory, sessions, config.toml, MCP, etc.).
241
+ # This gives agents-cli full versioned isolation + resource sync for grok.
242
+ export GROK_HOME="$VERSION_DIR/home/.grok"
243
+ `
244
+ : '';
230
245
  // Agents that don't natively resolve @-imports in their rules file need
231
246
  // agents-cli to recompile when the user edits a rule/preset file. The
232
247
  // check is fast (sha256 of ~8 small files) and skips the recompile when
@@ -376,7 +391,27 @@ if [[ ! "$VERSION" =~ ^(latest|[A-Za-z0-9._+-]{1,64})$ || "$VERSION" == *..* ]];
376
391
  fi
377
392
 
378
393
  VERSION_DIR="$AGENTS_USER_DIR/.history/versions/$AGENT/$VERSION"
379
- BINARY="$VERSION_DIR/node_modules/.bin/$CLI_COMMAND"
394
+
395
+ # Grok special case: binary lives in ~/.grok/downloads/, not node_modules.
396
+ # We still use the agents-cli version dir purely for GROK_HOME isolation.
397
+ if [ "$AGENT" = "grok" ]; then
398
+ # Try to find a matching binary for the pinned version in the global grok downloads dir.
399
+ GROK_DOWNLOADS="$HOME/.grok/downloads"
400
+ if [ -d "$GROK_DOWNLOADS" ]; then
401
+ # Prefer a binary whose filename contains the exact version
402
+ BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | grep -i "$VERSION" | head -1)
403
+ if [ -z "$BINARY" ]; then
404
+ # Fallback to the "current" grok binary (symlink or latest)
405
+ BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | head -1)
406
+ fi
407
+ fi
408
+ if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
409
+ # Last resort: whatever is on PATH (user may have installed grok globally)
410
+ BINARY=$(command -v grok 2>/dev/null || echo "")
411
+ fi
412
+ else
413
+ BINARY="$VERSION_DIR/node_modules/.bin/$CLI_COMMAND"
414
+ fi
380
415
 
381
416
  # Auto-install if not present
382
417
  if [ ! -x "$BINARY" ]; then
@@ -528,7 +563,14 @@ export CLAUDE_CONFIG_DIR="$HOME/.agents/.history/versions/${agent}/${version}/ho
528
563
  # and rules written by agents-cli actually take effect.
529
564
  export CODEX_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
530
565
  `
531
- : '';
566
+ : agent === 'copilot'
567
+ ? `
568
+ # Copilot honors COPILOT_HOME to relocate ~/.copilot (settings, mcp-config.json,
569
+ # session-state, logs). Point direct aliases at the versioned home so per-
570
+ # version MCP and session state are isolated.
571
+ export COPILOT_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
572
+ `
573
+ : '';
532
574
  const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
533
575
  return `#!/bin/bash
534
576
  # Auto-generated by agents-cli - do not edit
@@ -719,6 +761,26 @@ export async function switchConfigSymlink(agent, version) {
719
761
  // Already pointing to correct target, no-op
720
762
  return { success: true };
721
763
  }
764
+ // openclaw mixes user data (openclaw.json, openclaw.db, per-agent
765
+ // workspaces under ~/.openclaw/{agentId}/, memory/) with the version
766
+ // home — silently swapping the symlink to a fresh version home strips
767
+ // every running agent's config + workspace + memory. Carry the user
768
+ // data forward into the new version home before flipping the symlink
769
+ // (keep-dest preserves anything the new version already shipped).
770
+ // Other agents (Claude, Codex, etc.) keep user data outside the
771
+ // version-home dir, so this is openclaw-only by design.
772
+ if (agent === 'openclaw') {
773
+ try {
774
+ if (fs.existsSync(resolvedCurrent) && fs.statSync(resolvedCurrent).isDirectory()) {
775
+ await copyDirContents(resolvedCurrent, versionConfigPath, 'keep-dest');
776
+ }
777
+ }
778
+ catch (migrationErr) {
779
+ console.error(`Warning: openclaw data migration from ${resolvedCurrent} -> ${versionConfigPath} ` +
780
+ `failed: ${migrationErr.message}. The previous version's data is intact ` +
781
+ `at the old path; you can copy it manually if needed.`);
782
+ }
783
+ }
722
784
  // Different target - update it
723
785
  fs.unlinkSync(configPath);
724
786
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
@@ -1182,7 +1244,7 @@ export function getPathShadowingExecutable(agent) {
1182
1244
  * Delete the legacy ~/.agents/shims/<cli> file if it exists, returning whether
1183
1245
  * anything was removed. Pre-split installs put shims under ~/.agents/shims/;
1184
1246
  * the new layout uses ~/.agents-system/shims/. The leftover file causes the
1185
- * repair-prompt loop reported in EXAMPLE-664 — `getPathShadowingExecutable` flags
1247
+ * repair-prompt loop reported in PROJ-789 — `getPathShadowingExecutable` flags
1186
1248
  * it as a shadow but `addShimsToPath` only edits rc files, never the file
1187
1249
  * itself. Removing it ends the loop.
1188
1250
  */
@@ -212,7 +212,6 @@ export declare function recordVersionResources(_agent: AgentId, _version: string
212
212
  */
213
213
  export declare function ensureVersionResourcePatterns(agent: AgentId, version: string, updates: Partial<Record<Exclude<keyof VersionResources, 'rulesPreset'>, ResourcePattern[]>>): void;
214
214
  export declare function getVersionResources(agent: AgentId, version: string): VersionResources | null;
215
- export declare function clearVersionResources(agent: AgentId, version: string): void;
216
215
  /** Active rules preset for an agent@version. Defaults to "default" when unset. */
217
216
  export declare function getActiveRulesPreset(agent: AgentId, version: string): string;
218
217
  /** Persist the active rules preset for an agent@version. */
package/dist/lib/state.js CHANGED
@@ -74,7 +74,7 @@ const DRIVE_DIR = path.join(CACHE_DIR, 'drive');
74
74
  const TERMINALS_DIR = path.join(CACHE_DIR, 'terminals');
75
75
  const LOGS_DIR = path.join(CACHE_DIR, 'logs');
76
76
  const RUNTIME_STATE_DIR = path.join(CACHE_DIR, 'state');
77
- const SWARMIFY_DIR = path.join(CACHE_DIR, 'companion');
77
+ const COMPANION_CACHE_DIR = path.join(CACHE_DIR, 'companion');
78
78
  const BROWSER_RUNTIME_DIR = path.join(CACHE_DIR, 'browser');
79
79
  const HELPERS_DIR = path.join(CACHE_DIR, 'helpers');
80
80
  const DAEMON_DIR = path.join(HELPERS_DIR, 'daemon');
@@ -294,7 +294,7 @@ export function getLogsDir() { return LOGS_DIR; }
294
294
  /** Path to per-process runtime state (~/.agents/.cache/state/). */
295
295
  export function getRuntimeStateDir() { return RUNTIME_STATE_DIR; }
296
296
  /** Path to companion-extension scratch (~/.agents/.cache/companion/). */
297
- export function getCompanionDir() { return SWARMIFY_DIR; }
297
+ export function getCompanionDir() { return COMPANION_CACHE_DIR; }
298
298
  /** Path to browser runtime data — chrome-data, pids (~/.agents/.cache/browser/). */
299
299
  export function getBrowserRuntimeDir() { return BROWSER_RUNTIME_DIR; }
300
300
  /** Path to helper subprocess scratch (~/.agents/.cache/helpers/). */
@@ -626,19 +626,6 @@ export function getVersionResources(agent, version) {
626
626
  const meta = readMeta();
627
627
  return meta.versions?.[agent]?.[version] || null;
628
628
  }
629
- export function clearVersionResources(agent, version) {
630
- const meta = readMeta();
631
- if (meta.versions?.[agent]?.[version]) {
632
- delete meta.versions[agent][version];
633
- if (Object.keys(meta.versions[agent]).length === 0) {
634
- delete meta.versions[agent];
635
- }
636
- if (Object.keys(meta.versions).length === 0) {
637
- delete meta.versions;
638
- }
639
- writeMeta(meta);
640
- }
641
- }
642
629
  /** Active rules preset for an agent@version. Defaults to "default" when unset. */
643
630
  export function getActiveRulesPreset(agent, version) {
644
631
  const meta = readMeta();
@@ -171,7 +171,7 @@ export function captureProcessStartTime(pid) {
171
171
  }
172
172
  }
173
173
  /** Agent types the team runner supports. */
174
- const TEAM_AGENT_TYPES = ['codex', 'cursor', 'gemini', 'claude', 'opencode'];
174
+ const TEAM_AGENT_TYPES = ['codex', 'cursor', 'gemini', 'claude', 'opencode', 'grok', 'antigravity'];
175
175
  // Suffix appended to all prompts to ensure agents provide a summary
176
176
  const PROMPT_SUFFIX = `
177
177
 
@@ -1,5 +1,5 @@
1
1
  /** Supported agent CLI types for team spawning. */
2
- export type AgentType = 'codex' | 'gemini' | 'cursor' | 'claude' | 'opencode';
2
+ export type AgentType = 'codex' | 'gemini' | 'cursor' | 'claude' | 'opencode' | 'grok' | 'antigravity';
3
3
  /** Normalize a raw JSON event from any agent type into an array of unified event objects. */
4
4
  export declare function normalizeEvents(agentType: AgentType, raw: any): any[];
5
5
  /** Normalize a raw JSON event, returning only the first unified event (convenience wrapper). */
@@ -2,9 +2,9 @@
2
2
  * Agent event stream parsers.
3
3
  *
4
4
  * Normalizes the heterogeneous JSON event formats emitted by each agent CLI
5
- * (Claude, Codex, Gemini, Cursor, OpenCode) into a unified event schema
6
- * with consistent types: init, message, tool_use, bash, file_read, file_write,
7
- * file_create, file_delete, result, error, and others.
5
+ * (Claude, Codex, Gemini, Cursor, OpenCode, Grok, Antigravity) into a unified
6
+ * event schema with consistent types: init, message, tool_use, bash,
7
+ * file_read, file_write, file_create, file_delete, result, error, and others.
8
8
  */
9
9
  import { extractFileOpsFromBash } from './file_ops.js';
10
10
  const claudeToolUseMap = new Map();
@@ -25,6 +25,12 @@ export function normalizeEvents(agentType, raw) {
25
25
  else if (agentType === 'opencode') {
26
26
  return normalizeOpencode(raw);
27
27
  }
28
+ else if (agentType === 'grok') {
29
+ return normalizeGrok(raw);
30
+ }
31
+ else if (agentType === 'antigravity') {
32
+ return normalizeAntigravity(raw);
33
+ }
28
34
  const timestamp = new Date().toISOString();
29
35
  return [{
30
36
  type: raw.type || 'unknown',
@@ -824,6 +830,150 @@ function normalizeOpencode(raw) {
824
830
  timestamp: timestamp,
825
831
  }];
826
832
  }
833
+ // --- Grok parsing ---
834
+ // Grok's streaming-json mode emits one JSON object per token, with three event
835
+ // types:
836
+ // {"type":"thought","data":"<chunk>"} — reasoning tokens (many, small)
837
+ // {"type":"text","data":"<chunk>"} — visible response tokens (many, small)
838
+ // {"type":"end","stopReason":"EndTurn","sessionId":"<uuid>","requestId":"<uuid>"}
839
+ //
840
+ // Tool calls are NOT exposed as separate events in this format; they appear
841
+ // inside the `thought` text as XML-like markup. Extracting them reliably would
842
+ // require running a streaming XML/markup parser over concatenated thought
843
+ // chunks, which is out of scope for v1. The teams summary will show grok
844
+ // teammates' bash/file ops as empty — known limitation, fixable later by
845
+ // switching to grok's `agent` subcommand (richer event stream) once stable.
846
+ //
847
+ // Tokens are emitted as `message` events with `complete: false` so the
848
+ // summarizer can concatenate them into a final message; `thinking` events are
849
+ // already collapsed by the summarizer's groupAndFlattenEvents pathway.
850
+ function normalizeGrok(raw) {
851
+ if (!raw || typeof raw !== 'object') {
852
+ return [{
853
+ type: 'unknown',
854
+ agent: 'grok',
855
+ raw: raw,
856
+ timestamp: new Date().toISOString(),
857
+ }];
858
+ }
859
+ const eventType = raw.type || 'unknown';
860
+ const timestamp = new Date().toISOString();
861
+ if (eventType === 'thought') {
862
+ const data = typeof raw.data === 'string' ? raw.data : '';
863
+ if (!data)
864
+ return [];
865
+ return [{
866
+ type: 'thinking',
867
+ agent: 'grok',
868
+ content: data,
869
+ timestamp: timestamp,
870
+ }];
871
+ }
872
+ if (eventType === 'text') {
873
+ const data = typeof raw.data === 'string' ? raw.data : '';
874
+ if (!data)
875
+ return [];
876
+ return [{
877
+ type: 'message',
878
+ agent: 'grok',
879
+ content: data,
880
+ complete: false,
881
+ timestamp: timestamp,
882
+ }];
883
+ }
884
+ if (eventType === 'end') {
885
+ const stopReason = typeof raw.stopReason === 'string' ? raw.stopReason : '';
886
+ const status = stopReason === 'EndTurn' || stopReason === 'StopSequence' || stopReason === ''
887
+ ? 'success'
888
+ : 'error';
889
+ return [{
890
+ type: 'result',
891
+ agent: 'grok',
892
+ status: status,
893
+ stop_reason: stopReason || null,
894
+ session_id: typeof raw.sessionId === 'string' ? raw.sessionId : null,
895
+ timestamp: timestamp,
896
+ }];
897
+ }
898
+ return [{
899
+ type: eventType,
900
+ agent: 'grok',
901
+ raw: raw,
902
+ timestamp: timestamp,
903
+ }];
904
+ }
905
+ // --- Antigravity parsing ---
906
+ // Intentionally conservative. Antigravity's `agy` binary advertises an
907
+ // `--output-format json` flag in its docs, but the released binary errors with
908
+ // `flags provided but not defined: -output-format` (tracked upstream as
909
+ // google-antigravity/antigravity-cli#7, open as of May 2026). Until JSON
910
+ // streaming stabilizes, this parser treats agy output as a black box:
911
+ // - non-object input (a plain string line, or null/number) becomes a single
912
+ // `message` event with the full content and complete:true so the
913
+ // summarizer captures it without token-level concatenation
914
+ // - objects with a recognizable `type` field (e.g. `init`, `message`,
915
+ // `result`) get a minimal shape-preserving normalization
916
+ // - everything else falls through to the generic unknown-event shape
917
+ // Once agy ships stable streaming JSON, replace this with proper event
918
+ // mapping mirroring normalizeGrok / normalizeClaude.
919
+ function normalizeAntigravity(raw) {
920
+ const timestamp = new Date().toISOString();
921
+ if (typeof raw === 'string') {
922
+ if (!raw)
923
+ return [];
924
+ return [{
925
+ type: 'message',
926
+ agent: 'antigravity',
927
+ content: raw,
928
+ complete: true,
929
+ timestamp: timestamp,
930
+ }];
931
+ }
932
+ if (!raw || typeof raw !== 'object') {
933
+ return [{
934
+ type: 'unknown',
935
+ agent: 'antigravity',
936
+ raw: raw,
937
+ timestamp: timestamp,
938
+ }];
939
+ }
940
+ const eventType = raw.type || 'unknown';
941
+ if (eventType === 'init') {
942
+ return [{
943
+ type: 'init',
944
+ agent: 'antigravity',
945
+ session_id: typeof raw.sessionId === 'string' ? raw.sessionId : null,
946
+ timestamp: timestamp,
947
+ }];
948
+ }
949
+ if (eventType === 'message') {
950
+ const content = typeof raw.content === 'string' ? raw.content : '';
951
+ if (!content)
952
+ return [];
953
+ return [{
954
+ type: 'message',
955
+ agent: 'antigravity',
956
+ content: content,
957
+ complete: raw.complete !== false,
958
+ timestamp: timestamp,
959
+ }];
960
+ }
961
+ if (eventType === 'result') {
962
+ return [{
963
+ type: 'result',
964
+ agent: 'antigravity',
965
+ status: raw.status === 'error' ? 'error' : 'success',
966
+ session_id: typeof raw.sessionId === 'string' ? raw.sessionId : null,
967
+ timestamp: timestamp,
968
+ }];
969
+ }
970
+ return [{
971
+ type: eventType,
972
+ agent: 'antigravity',
973
+ raw: raw,
974
+ timestamp: timestamp,
975
+ }];
976
+ }
827
977
  /** Parse a single JSONL line into normalized events. Returns null if the line is not valid JSON. */
828
978
  export function parseEvent(agentType, line) {
829
979
  try {
@@ -159,11 +159,18 @@ export function groupAndFlattenEvents(events) {
159
159
  if (eventType === 'message' || eventType === 'thinking') {
160
160
  let count = 1;
161
161
  let combinedContent = event.content || '';
162
+ // Streaming token events (complete:false) get concatenated without a
163
+ // separator so tokens reassemble into readable prose. Whole-turn events
164
+ // get joined with newlines so distinct turns/thoughts stay separated.
165
+ const isStreaming = event.complete === false;
162
166
  let j = i + 1;
163
167
  while (j < events.length && events[j].type === eventType) {
164
168
  count++;
165
169
  if (events[j].content) {
166
- combinedContent += (combinedContent ? '\n' : '') + events[j].content;
170
+ const sep = isStreaming || events[j].complete === false
171
+ ? ''
172
+ : (combinedContent ? '\n' : '');
173
+ combinedContent += sep + events[j].content;
167
174
  }
168
175
  j++;
169
176
  }
@@ -405,7 +412,16 @@ export function summarizeEvents(agentId, agentType, status, events, duration = n
405
412
  else if (eventType === 'message') {
406
413
  const content = event.content || '';
407
414
  if (content) {
408
- summary.finalMessage = content;
415
+ // Streaming token-by-token messages (e.g., grok) arrive as many small
416
+ // `message` events with complete:false; concatenate them so the final
417
+ // turn reads as one message. Whole-turn messages (claude, codex,
418
+ // gemini) keep their complete:true semantics — last one wins.
419
+ if (event.complete === false) {
420
+ summary.finalMessage = (summary.finalMessage || '') + content;
421
+ }
422
+ else {
423
+ summary.finalMessage = content;
424
+ }
409
425
  }
410
426
  }
411
427
  else if (eventType === 'error') {
@@ -8,7 +8,9 @@ import { execFile } from 'child_process';
8
8
  import { promisify } from 'util';
9
9
  import * as fs from 'fs/promises';
10
10
  import * as path from 'path';
11
+ import { safeJoin } from '../paths.js';
11
12
  const execFileAsync = promisify(execFile);
13
+ const WORKTREE_NAME_RE = /^[A-Za-z0-9_-]+$/;
12
14
  export async function isGitRepo(dir) {
13
15
  try {
14
16
  await execFileAsync('git', ['rev-parse', '--git-dir'], { cwd: dir });
@@ -42,8 +44,11 @@ export async function hasUncommittedChanges(worktreePath) {
42
44
  * @returns The absolute path to the created worktree
43
45
  */
44
46
  export async function createWorktree(repoDir, worktreeName) {
47
+ if (!WORKTREE_NAME_RE.test(worktreeName)) {
48
+ throw new Error(`Invalid worktree name: ${worktreeName}`);
49
+ }
45
50
  const gitRoot = await getGitRoot(repoDir);
46
- const worktreePath = path.join(gitRoot, '.agents', 'worktrees', worktreeName);
51
+ const worktreePath = safeJoin(path.join(gitRoot, '.agents', 'worktrees'), worktreeName);
47
52
  const branchName = `agents/${worktreeName}`;
48
53
  await fs.mkdir(path.dirname(worktreePath), { recursive: true });
49
54
  await execFileAsync('git', ['worktree', 'add', '-b', branchName, worktreePath, 'HEAD'], {
@@ -59,8 +64,11 @@ export async function createWorktree(repoDir, worktreeName) {
59
64
  * @param deleteBranch - Whether to delete the associated branch
60
65
  */
61
66
  export async function removeWorktree(repoDir, worktreeName, deleteBranch = true) {
67
+ if (!WORKTREE_NAME_RE.test(worktreeName)) {
68
+ throw new Error(`Invalid worktree name: ${worktreeName}`);
69
+ }
62
70
  const gitRoot = await getGitRoot(repoDir);
63
- const worktreePath = path.join(gitRoot, '.agents', 'worktrees', worktreeName);
71
+ const worktreePath = safeJoin(path.join(gitRoot, '.agents', 'worktrees'), worktreeName);
64
72
  const branchName = `agents/${worktreeName}`;
65
73
  try {
66
74
  await execFileAsync('git', ['worktree', 'remove', '--force', worktreePath], { cwd: gitRoot });
@@ -86,7 +94,10 @@ export async function removeWorktree(repoDir, worktreeName, deleteBranch = true)
86
94
  * Get the worktree path for a given name.
87
95
  */
88
96
  export function getWorktreePath(gitRoot, worktreeName) {
89
- return path.join(gitRoot, '.agents', 'worktrees', worktreeName);
97
+ if (!WORKTREE_NAME_RE.test(worktreeName)) {
98
+ throw new Error(`Invalid worktree name: ${worktreeName}`);
99
+ }
100
+ return safeJoin(path.join(gitRoot, '.agents', 'worktrees'), worktreeName);
90
101
  }
91
102
  /**
92
103
  * Get the branch name for a worktree.
@@ -203,9 +203,12 @@ export interface RegistryConfig {
203
203
  /** Built-in registry endpoints shipped with agents-cli. */
204
204
  export declare const DEFAULT_REGISTRIES: Record<RegistryType, Record<string, RegistryConfig>>;
205
205
  /**
206
- * Registries that ship pre-seeded into new users' agents.yaml once, but are
207
- * not "defaults" — after seeding they behave like any user-added registry
208
- * (listable, disable-able, removable, and gone for good once removed).
206
+ * Third-party registries pre-seeded on first install for discoverability.
207
+ *
208
+ * These ship into new users' agents.yaml once, but are not "defaults" after
209
+ * seeding they behave like any user-added registry (listable, disable-able,
210
+ * removable). Removed users can `agents registry remove <name>` to opt out;
211
+ * once removed they don't come back.
209
212
  */
210
213
  export declare const SEEDED_REGISTRIES: Record<RegistryType, Record<string, RegistryConfig>>;
211
214
  /** A single installable package within an MCP server entry. */
package/dist/lib/types.js CHANGED
@@ -22,9 +22,12 @@ export const DEFAULT_REGISTRIES = {
22
22
  skill: {},
23
23
  };
24
24
  /**
25
- * Registries that ship pre-seeded into new users' agents.yaml once, but are
26
- * not "defaults" — after seeding they behave like any user-added registry
27
- * (listable, disable-able, removable, and gone for good once removed).
25
+ * Third-party registries pre-seeded on first install for discoverability.
26
+ *
27
+ * These ship into new users' agents.yaml once, but are not "defaults" after
28
+ * seeding they behave like any user-added registry (listable, disable-able,
29
+ * removable). Removed users can `agents registry remove <name>` to opt out;
30
+ * once removed they don't come back.
28
31
  */
29
32
  export const SEEDED_REGISTRIES = {
30
33
  mcp: {},