@phnx-labs/agents-cli 1.20.52 → 1.20.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +57 -2
  2. package/README.md +12 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/cloud.d.ts +3 -0
  5. package/dist/commands/cloud.js +2 -1
  6. package/dist/commands/exec.js +65 -1
  7. package/dist/commands/feed.d.ts +29 -0
  8. package/dist/commands/feed.js +237 -32
  9. package/dist/commands/memory.d.ts +9 -0
  10. package/dist/commands/memory.js +164 -0
  11. package/dist/commands/message.d.ts +11 -6
  12. package/dist/commands/message.js +140 -5
  13. package/dist/commands/routines.js +12 -0
  14. package/dist/commands/secrets-migrate.d.ts +2 -1
  15. package/dist/commands/secrets-migrate.js +88 -13
  16. package/dist/commands/secrets.js +4 -1
  17. package/dist/commands/sessions.js +10 -1
  18. package/dist/commands/worktree.js +4 -2
  19. package/dist/index.js +16 -21
  20. package/dist/lib/agents.js +249 -17
  21. package/dist/lib/answer-router.d.ts +75 -0
  22. package/dist/lib/answer-router.js +149 -0
  23. package/dist/lib/ask-classifier.d.ts +71 -0
  24. package/dist/lib/ask-classifier.js +197 -0
  25. package/dist/lib/cloud/antigravity.d.ts +0 -2
  26. package/dist/lib/cloud/antigravity.js +2 -17
  27. package/dist/lib/cloud/codex.js +3 -18
  28. package/dist/lib/cloud/rush.js +3 -15
  29. package/dist/lib/cloud/stream.js +2 -0
  30. package/dist/lib/cloud/types.d.ts +21 -0
  31. package/dist/lib/cloud/types.js +81 -0
  32. package/dist/lib/crabbox/cli.d.ts +1 -1
  33. package/dist/lib/crabbox/cli.js +12 -2
  34. package/dist/lib/crabbox/lease.d.ts +13 -0
  35. package/dist/lib/crabbox/lease.js +11 -2
  36. package/dist/lib/crabbox/progress.d.ts +62 -0
  37. package/dist/lib/crabbox/progress.js +129 -0
  38. package/dist/lib/events.js +4 -1
  39. package/dist/lib/exec.js +19 -1
  40. package/dist/lib/feed-outcome.d.ts +101 -0
  41. package/dist/lib/feed-outcome.js +244 -0
  42. package/dist/lib/feed-policy.d.ts +30 -0
  43. package/dist/lib/feed-policy.js +133 -0
  44. package/dist/lib/feed.d.ts +127 -3
  45. package/dist/lib/feed.js +416 -40
  46. package/dist/lib/git.d.ts +17 -1
  47. package/dist/lib/git.js +20 -1
  48. package/dist/lib/hooks.js +522 -12
  49. package/dist/lib/hosts/passthrough.d.ts +3 -3
  50. package/dist/lib/hosts/passthrough.js +3 -4
  51. package/dist/lib/mailbox-gc.d.ts +22 -0
  52. package/dist/lib/mailbox-gc.js +161 -0
  53. package/dist/lib/mailbox.d.ts +26 -2
  54. package/dist/lib/mailbox.js +80 -5
  55. package/dist/lib/mcp.js +82 -0
  56. package/dist/lib/memory.d.ts +55 -0
  57. package/dist/lib/memory.js +274 -0
  58. package/dist/lib/notify.d.ts +16 -0
  59. package/dist/lib/notify.js +61 -0
  60. package/dist/lib/operator.d.ts +26 -0
  61. package/dist/lib/operator.js +107 -0
  62. package/dist/lib/plugins.d.ts +35 -0
  63. package/dist/lib/plugins.js +217 -0
  64. package/dist/lib/remote-agents-json.d.ts +14 -0
  65. package/dist/lib/remote-agents-json.js +94 -0
  66. package/dist/lib/resources/mcp.js +44 -0
  67. package/dist/lib/resources/memory.d.ts +15 -0
  68. package/dist/lib/resources/memory.js +46 -0
  69. package/dist/lib/resources/types.d.ts +2 -2
  70. package/dist/lib/runner.d.ts +43 -0
  71. package/dist/lib/runner.js +323 -74
  72. package/dist/lib/sandbox.js +6 -0
  73. package/dist/lib/secrets/bundles.js +38 -13
  74. package/dist/lib/secrets/icloud-import.d.ts +12 -3
  75. package/dist/lib/secrets/icloud-import.js +37 -7
  76. package/dist/lib/secrets/index.d.ts +106 -2
  77. package/dist/lib/secrets/index.js +603 -28
  78. package/dist/lib/session/active.d.ts +18 -0
  79. package/dist/lib/session/active.js +47 -17
  80. package/dist/lib/session/db.d.ts +9 -1
  81. package/dist/lib/session/db.js +18 -3
  82. package/dist/lib/session/discover.d.ts +13 -0
  83. package/dist/lib/session/discover.js +31 -0
  84. package/dist/lib/session/parse.d.ts +8 -0
  85. package/dist/lib/session/parse.js +42 -21
  86. package/dist/lib/session/remote-active.js +8 -89
  87. package/dist/lib/session/state.d.ts +11 -0
  88. package/dist/lib/session/state.js +37 -0
  89. package/dist/lib/session/tail.d.ts +23 -4
  90. package/dist/lib/session/tail.js +34 -16
  91. package/dist/lib/session/throughput.d.ts +30 -0
  92. package/dist/lib/session/throughput.js +86 -0
  93. package/dist/lib/shim-heal.d.ts +12 -3
  94. package/dist/lib/shim-heal.js +12 -6
  95. package/dist/lib/staleness/detectors/subagents.js +57 -3
  96. package/dist/lib/staleness/writers/hooks.js +7 -3
  97. package/dist/lib/staleness/writers/subagents.js +37 -6
  98. package/dist/lib/startup/command-registry.d.ts +1 -0
  99. package/dist/lib/startup/command-registry.js +2 -0
  100. package/dist/lib/subagents.d.ts +52 -0
  101. package/dist/lib/subagents.js +315 -12
  102. package/dist/lib/teams/worktree.d.ts +8 -0
  103. package/dist/lib/teams/worktree.js +8 -0
  104. package/dist/lib/types.d.ts +10 -2
  105. package/dist/lib/versions.js +38 -48
  106. package/package.json +4 -3
  107. package/scripts/postinstall.js +61 -1
@@ -262,6 +262,153 @@ export function transformSubagentForDroid(subagentDir) {
262
262
  }
263
263
  return result;
264
264
  }
265
+ /**
266
+ * Transform a subagent into an OpenCode agent markdown file.
267
+ *
268
+ * OpenCode loads agents from ~/.config/opencode/agents/*.md (global) with
269
+ * YAML frontmatter (description required; mode: subagent for invocable
270
+ * subagents) and a prompt body. File stem becomes the agent name.
271
+ * https://opencode.ai/docs/agents/
272
+ */
273
+ export function transformSubagentForOpenCode(subagentDir) {
274
+ const agentMd = path.join(subagentDir, 'AGENT.md');
275
+ const frontmatter = parseSubagentFrontmatter(agentMd);
276
+ const body = getSubagentBody(agentMd);
277
+ if (!frontmatter) {
278
+ throw new Error(`Invalid AGENT.md in ${subagentDir}`);
279
+ }
280
+ const fm = {
281
+ description: frontmatter.description,
282
+ mode: 'subagent',
283
+ };
284
+ if (frontmatter.model)
285
+ fm.model = frontmatter.model;
286
+ let systemPrompt = body.trim();
287
+ const files = fs.readdirSync(subagentDir)
288
+ .filter(f => f.endsWith('.md') && f !== 'AGENT.md')
289
+ .sort();
290
+ for (const file of files) {
291
+ const content = fs.readFileSync(path.join(subagentDir, file), 'utf-8').trim();
292
+ const sectionName = file.replace('.md', '');
293
+ const title = sectionName.charAt(0).toUpperCase() + sectionName.slice(1).toLowerCase();
294
+ systemPrompt += `\n\n## ${title}\n\n${content}`;
295
+ }
296
+ return `---\n${yaml.stringify(fm).trim()}\n---\n\n${systemPrompt}\n`;
297
+ }
298
+ /** Managed parent agent file for Kimi (underscore prefix avoids clobbering a user subagent named agents-cli). */
299
+ export const KIMI_SUBAGENTS_PARENT_FILE = '_agents-cli.yaml';
300
+ /**
301
+ * Transform a subagent into Kimi Code agent files.
302
+ *
303
+ * Kimi's schema (agentspec) only accepts `system_prompt_path` (path relative to
304
+ * the agent YAML) — there is no inline `system_prompt` key. We write:
305
+ * ~/.kimi-code/agents/<name>.yaml — agent YAML
306
+ * ~/.kimi-code/agents/<name>.system.md — system prompt body
307
+ * plus a managed parent `_agents-cli.yaml` that lists them under agent.subagents
308
+ * for `kimi --agent-file …/_agents-cli.yaml`.
309
+ * https://moonshotai.github.io/kimi-cli/en/customization/agents.html
310
+ */
311
+ export function transformSubagentForKimi(subagentDir, fileBaseName) {
312
+ const agentMd = path.join(subagentDir, 'AGENT.md');
313
+ const frontmatter = parseSubagentFrontmatter(agentMd);
314
+ const body = getSubagentBody(agentMd);
315
+ if (!frontmatter) {
316
+ throw new Error(`Invalid AGENT.md in ${subagentDir}`);
317
+ }
318
+ let systemPrompt = body.trim();
319
+ const files = fs.readdirSync(subagentDir)
320
+ .filter(f => f.endsWith('.md') && f !== 'AGENT.md')
321
+ .sort();
322
+ for (const file of files) {
323
+ const content = fs.readFileSync(path.join(subagentDir, file), 'utf-8').trim();
324
+ const sectionName = file.replace('.md', '');
325
+ const title = sectionName.charAt(0).toUpperCase() + sectionName.slice(1).toLowerCase();
326
+ systemPrompt += `\n\n## ${title}\n\n${content}`;
327
+ }
328
+ const systemPromptFileName = `${fileBaseName}.system.md`;
329
+ const doc = {
330
+ version: 1,
331
+ agent: {
332
+ extend: 'default',
333
+ name: frontmatter.name,
334
+ description: frontmatter.description,
335
+ system_prompt_path: `./${systemPromptFileName}`,
336
+ },
337
+ };
338
+ if (frontmatter.model) {
339
+ doc.agent.model = frontmatter.model;
340
+ }
341
+ return {
342
+ yaml: yaml.stringify(doc),
343
+ systemPrompt,
344
+ systemPromptFileName,
345
+ };
346
+ }
347
+ /**
348
+ * Build the managed parent agent YAML that declares all installed subagents
349
+ * so `kimi --agent-file ~/.kimi-code/agents/_agents-cli.yaml` can launch them.
350
+ */
351
+ export function buildKimiSubagentsParentYaml(entries) {
352
+ const subagents = {};
353
+ for (const e of entries) {
354
+ subagents[e.name] = { path: e.relativePath, description: e.description };
355
+ }
356
+ return yaml.stringify({
357
+ version: 1,
358
+ agent: {
359
+ extend: 'default',
360
+ name: 'agents-cli',
361
+ description: 'Managed parent agent listing agents-cli synced subagents',
362
+ subagents,
363
+ },
364
+ });
365
+ }
366
+ /** Write a Kimi subagent YAML + sibling system-prompt md into agentsDir. */
367
+ export function writeKimiSubagentFiles(agentsDir, subagentDir, name) {
368
+ const { yaml: yml, systemPrompt, systemPromptFileName } = transformSubagentForKimi(subagentDir, name);
369
+ fs.mkdirSync(agentsDir, { recursive: true });
370
+ fs.writeFileSync(safeJoin(agentsDir, `${name}.yaml`), yml);
371
+ fs.writeFileSync(safeJoin(agentsDir, systemPromptFileName), systemPrompt);
372
+ }
373
+ /**
374
+ * Transform a subagent into a Codex custom-agent TOML file.
375
+ *
376
+ * Codex loads standalone TOML under ~/.codex/agents/*.toml. Required fields:
377
+ * name, description, developer_instructions. Optional model maps from our
378
+ * frontmatter when present.
379
+ * https://developers.openai.com/codex/subagents (custom agents section)
380
+ */
381
+ export function transformSubagentForCodex(subagentDir) {
382
+ const agentMd = path.join(subagentDir, 'AGENT.md');
383
+ const frontmatter = parseSubagentFrontmatter(agentMd);
384
+ const body = getSubagentBody(agentMd);
385
+ if (!frontmatter) {
386
+ throw new Error(`Invalid AGENT.md in ${subagentDir}`);
387
+ }
388
+ // Append other .md files into the developer_instructions body.
389
+ let instructions = body.trim();
390
+ const files = fs.readdirSync(subagentDir)
391
+ .filter(f => f.endsWith('.md') && f !== 'AGENT.md')
392
+ .sort();
393
+ for (const file of files) {
394
+ const content = fs.readFileSync(path.join(subagentDir, file), 'utf-8').trim();
395
+ const sectionName = file.replace('.md', '');
396
+ const title = sectionName.charAt(0).toUpperCase() + sectionName.slice(1).toLowerCase();
397
+ instructions += `\n\n## ${title}\n\n${content}`;
398
+ }
399
+ // Escape TOML multi-line string (""") content — only """ needs escaping.
400
+ const safeInstructions = instructions.replace(/"""/g, '\\"""');
401
+ const safeName = frontmatter.name.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
402
+ const safeDesc = frontmatter.description.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
403
+ let toml = `name = "${safeName}"\n`;
404
+ toml += `description = "${safeDesc}"\n`;
405
+ if (frontmatter.model) {
406
+ const safeModel = String(frontmatter.model).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
407
+ toml += `model = "${safeModel}"\n`;
408
+ }
409
+ toml += `developer_instructions = """\n${safeInstructions}\n"""\n`;
410
+ return toml;
411
+ }
265
412
  /**
266
413
  * Sync a subagent to an OpenClaw workspace
267
414
  * Copies full directory, renames AGENT.md to AGENTS.md
@@ -291,9 +438,10 @@ export function syncSubagentToOpenclaw(subagentDir, targetDir) {
291
438
  * Install a subagent to a specific agent's home
292
439
  */
293
440
  export function installSubagentToAgent(subagentDir, subagentName, agent, agentHome) {
294
- if (agent === 'claude') {
295
- // Claude: flatten to single .md file
296
- const agentsDir = path.join(agentHome, '.claude', 'agents');
441
+ if (agent === 'claude' || agent === 'grok') {
442
+ // Claude / Grok: flatten to single .md under ~/.claude/agents or ~/.grok/agents
443
+ // Grok discovers user agent defs from ~/.grok/agents/*.md (same shape as Claude).
444
+ const agentsDir = path.join(agentHome, agent === 'grok' ? '.grok' : '.claude', 'agents');
297
445
  if (!fs.existsSync(agentsDir)) {
298
446
  fs.mkdirSync(agentsDir, { recursive: true });
299
447
  }
@@ -306,6 +454,44 @@ export function installSubagentToAgent(subagentDir, subagentName, agent, agentHo
306
454
  return { success: false, error: String(err) };
307
455
  }
308
456
  }
457
+ else if (agent === 'codex') {
458
+ // Codex: standalone TOML under ~/.codex/agents/<name>.toml
459
+ const agentsDir = path.join(agentHome, '.codex', 'agents');
460
+ if (!fs.existsSync(agentsDir)) {
461
+ fs.mkdirSync(agentsDir, { recursive: true });
462
+ }
463
+ try {
464
+ const toml = transformSubagentForCodex(subagentDir);
465
+ fs.writeFileSync(safeJoin(agentsDir, `${subagentName}.toml`), toml);
466
+ return { success: true };
467
+ }
468
+ catch (err) {
469
+ return { success: false, error: String(err) };
470
+ }
471
+ }
472
+ else if (agent === 'kimi') {
473
+ // Kimi: YAML + sibling system-prompt md under ~/.kimi-code/agents/
474
+ try {
475
+ writeKimiSubagentFiles(path.join(agentHome, '.kimi-code', 'agents'), subagentDir, subagentName);
476
+ return { success: true };
477
+ }
478
+ catch (err) {
479
+ return { success: false, error: String(err) };
480
+ }
481
+ }
482
+ else if (agent === 'opencode') {
483
+ // OpenCode: markdown agents under ~/.config/opencode/agents/
484
+ const agentsDir = path.join(agentHome, '.config', 'opencode', 'agents');
485
+ if (!fs.existsSync(agentsDir))
486
+ fs.mkdirSync(agentsDir, { recursive: true });
487
+ try {
488
+ fs.writeFileSync(safeJoin(agentsDir, `${subagentName}.md`), transformSubagentForOpenCode(subagentDir));
489
+ return { success: true };
490
+ }
491
+ catch (err) {
492
+ return { success: false, error: String(err) };
493
+ }
494
+ }
309
495
  else if (agent === 'openclaw') {
310
496
  // OpenClaw: copy full directory
311
497
  const targetDir = safeJoin(path.join(agentHome, '.openclaw'), subagentName);
@@ -321,13 +507,37 @@ export function installSubagentToAgent(subagentDir, subagentName, agent, agentHo
321
507
  */
322
508
  export function removeSubagentFromAgent(subagentName, agent, agentHome) {
323
509
  try {
324
- if (agent === 'claude') {
325
- const targetPath = safeJoin(path.join(agentHome, '.claude', 'agents'), `${subagentName}.md`);
510
+ if (agent === 'claude' || agent === 'grok') {
511
+ const agentsRoot = agent === 'grok' ? '.grok' : '.claude';
512
+ const targetPath = safeJoin(path.join(agentHome, agentsRoot, 'agents'), `${subagentName}.md`);
513
+ if (fs.existsSync(targetPath)) {
514
+ fs.unlinkSync(targetPath);
515
+ }
516
+ return { success: true };
517
+ }
518
+ else if (agent === 'codex') {
519
+ const targetPath = safeJoin(path.join(agentHome, '.codex', 'agents'), `${subagentName}.toml`);
326
520
  if (fs.existsSync(targetPath)) {
327
521
  fs.unlinkSync(targetPath);
328
522
  }
329
523
  return { success: true };
330
524
  }
525
+ else if (agent === 'kimi') {
526
+ const agentsDir = path.join(agentHome, '.kimi-code', 'agents');
527
+ const targetPath = safeJoin(agentsDir, `${subagentName}.yaml`);
528
+ const promptPath = safeJoin(agentsDir, `${subagentName}.system.md`);
529
+ if (fs.existsSync(targetPath))
530
+ fs.unlinkSync(targetPath);
531
+ if (fs.existsSync(promptPath))
532
+ fs.unlinkSync(promptPath);
533
+ return { success: true };
534
+ }
535
+ else if (agent === 'opencode') {
536
+ const targetPath = safeJoin(path.join(agentHome, '.config', 'opencode', 'agents'), `${subagentName}.md`);
537
+ if (fs.existsSync(targetPath))
538
+ fs.unlinkSync(targetPath);
539
+ return { success: true };
540
+ }
331
541
  else if (agent === 'openclaw') {
332
542
  const targetDir = safeJoin(path.join(agentHome, '.openclaw'), subagentName);
333
543
  if (fs.existsSync(targetDir)) {
@@ -373,13 +583,14 @@ export function subagentContentMatches(installedDir, sourceDir) {
373
583
  /**
374
584
  * List subagents installed to a specific agent's home
375
585
  * Claude: scans ~/.claude/agents/{name}.md
586
+ * Kimi: scans ~/.kimi-code/agents/{name}.yaml (+ sibling .system.md)
376
587
  * OpenClaw: scans ~/.openclaw/{name}/AGENTS.md
377
588
  */
378
589
  export function listSubagentsForAgent(agentId, home) {
379
590
  const subagents = [];
380
- if (agentId === 'claude') {
381
- // Claude: flat .md files in agents/
382
- const agentsDir = path.join(home, '.claude', 'agents');
591
+ if (agentId === 'claude' || agentId === 'grok') {
592
+ // Claude / Grok: flat .md files in agents/
593
+ const agentsDir = path.join(home, agentId === 'grok' ? '.grok' : '.claude', 'agents');
383
594
  if (!fs.existsSync(agentsDir))
384
595
  return subagents;
385
596
  for (const file of fs.readdirSync(agentsDir)) {
@@ -400,6 +611,51 @@ export function listSubagentsForAgent(agentId, home) {
400
611
  });
401
612
  }
402
613
  }
614
+ else if (agentId === 'kimi') {
615
+ // Kimi: flat YAML under .kimi-code/agents/ (exclude managed parent _*.yaml)
616
+ const agentsDir = path.join(home, '.kimi-code', 'agents');
617
+ if (!fs.existsSync(agentsDir))
618
+ return subagents;
619
+ for (const file of fs.readdirSync(agentsDir)) {
620
+ if (!file.endsWith('.yaml') || file.startsWith('_'))
621
+ continue;
622
+ const filePath = path.join(agentsDir, file);
623
+ if (!fs.statSync(filePath).isFile())
624
+ continue;
625
+ const name = file.replace(/\.yaml$/, '');
626
+ let description = '';
627
+ try {
628
+ const parsed = yaml.parse(fs.readFileSync(filePath, 'utf-8'));
629
+ description = parsed?.agent?.description ?? '';
630
+ }
631
+ catch { /* leave description empty */ }
632
+ const files = [file];
633
+ const promptFile = `${name}.system.md`;
634
+ if (fs.existsSync(path.join(agentsDir, promptFile)))
635
+ files.push(promptFile);
636
+ subagents.push({
637
+ name,
638
+ path: filePath,
639
+ files,
640
+ frontmatter: { name, description },
641
+ });
642
+ }
643
+ }
644
+ else if (agentId === 'opencode') {
645
+ const agentsDir = path.join(home, '.config', 'opencode', 'agents');
646
+ if (!fs.existsSync(agentsDir))
647
+ return subagents;
648
+ for (const file of fs.readdirSync(agentsDir)) {
649
+ if (!file.endsWith('.md'))
650
+ continue;
651
+ const filePath = path.join(agentsDir, file);
652
+ if (!fs.statSync(filePath).isFile())
653
+ continue;
654
+ const name = file.replace(/\.md$/, '');
655
+ const frontmatter = parseSubagentFrontmatter(filePath) ?? { name, description: '' };
656
+ subagents.push({ name, path: filePath, files: [file], frontmatter });
657
+ }
658
+ }
403
659
  else if (agentId === 'openclaw') {
404
660
  // OpenClaw: directories with AGENTS.md
405
661
  const openclawDir = path.join(home, '.openclaw');
@@ -462,8 +718,8 @@ export function diffVersionSubagents(agent, version) {
462
718
  }
463
719
  }
464
720
  // Check what's installed
465
- if (agent === 'claude') {
466
- const agentsDir = path.join(versionHome, '.claude', 'agents');
721
+ if (agent === 'claude' || agent === 'grok') {
722
+ const agentsDir = path.join(versionHome, agent === 'grok' ? '.grok' : '.claude', 'agents');
467
723
  if (fs.existsSync(agentsDir)) {
468
724
  for (const file of fs.readdirSync(agentsDir)) {
469
725
  if (!file.endsWith('.md'))
@@ -475,6 +731,31 @@ export function diffVersionSubagents(agent, version) {
475
731
  }
476
732
  }
477
733
  }
734
+ else if (agent === 'kimi') {
735
+ const agentsDir = path.join(versionHome, '.kimi-code', 'agents');
736
+ if (fs.existsSync(agentsDir)) {
737
+ for (const file of fs.readdirSync(agentsDir)) {
738
+ if (!file.endsWith('.yaml') || file.startsWith('_'))
739
+ continue;
740
+ const name = path.basename(file, '.yaml');
741
+ if (!discovered.has(name)) {
742
+ orphans.push(name);
743
+ }
744
+ }
745
+ }
746
+ }
747
+ else if (agent === 'opencode') {
748
+ const agentsDir = path.join(versionHome, '.config', 'opencode', 'agents');
749
+ if (fs.existsSync(agentsDir)) {
750
+ for (const file of fs.readdirSync(agentsDir)) {
751
+ if (!file.endsWith('.md'))
752
+ continue;
753
+ const name = path.basename(file, '.md');
754
+ if (!discovered.has(name))
755
+ orphans.push(name);
756
+ }
757
+ }
758
+ }
478
759
  else if (agent === 'openclaw') {
479
760
  const openclawDir = path.join(versionHome, '.openclaw');
480
761
  if (fs.existsSync(openclawDir)) {
@@ -516,8 +797,30 @@ export function removeSubagentFromVersion(agent, version, subagentName) {
516
797
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
517
798
  const trashDir = path.join(getTrashSubagentsDir(), agent, version, subagentName);
518
799
  try {
519
- if (agent === 'claude') {
520
- const targetPath = path.join(versionHome, '.claude', 'agents', `${subagentName}.md`);
800
+ if (agent === 'claude' || agent === 'grok') {
801
+ const agentsRoot = agent === 'grok' ? '.grok' : '.claude';
802
+ const targetPath = path.join(versionHome, agentsRoot, 'agents', `${subagentName}.md`);
803
+ if (fs.existsSync(targetPath)) {
804
+ fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
805
+ fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.md.${stamp}`));
806
+ }
807
+ }
808
+ else if (agent === 'kimi') {
809
+ const agentsDir = path.join(versionHome, '.kimi-code', 'agents');
810
+ const yamlPath = path.join(agentsDir, `${subagentName}.yaml`);
811
+ const promptPath = path.join(agentsDir, `${subagentName}.system.md`);
812
+ if (fs.existsSync(yamlPath) || fs.existsSync(promptPath)) {
813
+ fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
814
+ if (fs.existsSync(yamlPath)) {
815
+ fs.renameSync(yamlPath, path.join(trashDir, `${subagentName}.yaml.${stamp}`));
816
+ }
817
+ if (fs.existsSync(promptPath)) {
818
+ fs.renameSync(promptPath, path.join(trashDir, `${subagentName}.system.md.${stamp}`));
819
+ }
820
+ }
821
+ }
822
+ else if (agent === 'opencode') {
823
+ const targetPath = path.join(versionHome, '.config', 'opencode', 'agents', `${subagentName}.md`);
521
824
  if (fs.existsSync(targetPath)) {
522
825
  fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
523
826
  fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.md.${stamp}`));
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Check if `dir` is inside a git repository (**async, worktree-correct**).
3
+ *
4
+ * Shells out to `git rev-parse --git-dir`, so it returns true from any
5
+ * subdirectory and for linked worktrees. Distinct from the synchronous,
6
+ * root-only `isGitRepo` in `lib/git.ts` (a `.git`-existence check): different
7
+ * semantics, so the two are intentionally **not** merged.
8
+ */
1
9
  export declare function isGitRepo(dir: string): Promise<boolean>;
2
10
  export declare function getGitRoot(dir: string): Promise<string>;
3
11
  /**
@@ -11,6 +11,14 @@ import * as path from 'path';
11
11
  import { safeJoin } from '../paths.js';
12
12
  const execFileAsync = promisify(execFile);
13
13
  const WORKTREE_NAME_RE = /^[A-Za-z0-9_-]+$/;
14
+ /**
15
+ * Check if `dir` is inside a git repository (**async, worktree-correct**).
16
+ *
17
+ * Shells out to `git rev-parse --git-dir`, so it returns true from any
18
+ * subdirectory and for linked worktrees. Distinct from the synchronous,
19
+ * root-only `isGitRepo` in `lib/git.ts` (a `.git`-existence check): different
20
+ * semantics, so the two are intentionally **not** merged.
21
+ */
14
22
  export async function isGitRepo(dir) {
15
23
  try {
16
24
  await execFileAsync('git', ['rev-parse', '--git-dir'], { cwd: dir });
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { CloudProviderId } from './cloud/types.js';
9
9
  /** Unique identifier for a supported AI coding agent. */
10
- export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid';
10
+ export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'forge';
11
11
  /** How `agents run <agent>` chooses an installed version when none is pinned. */
12
12
  export type RunStrategy = 'pinned' | 'available' | 'balanced';
13
13
  /** Per-agent run strategy config. */
@@ -148,6 +148,12 @@ export interface AgentConfig {
148
148
  subagents: Capability;
149
149
  rules: RulesCapability;
150
150
  workflows: Capability;
151
+ /**
152
+ * Portable knowledge-store memory (`agents memory` / ~/.agents/memory/).
153
+ * Distinct from `rules` (instructions). When true, sync fans facts into
154
+ * the agent version home (see memoryTargetDir).
155
+ */
156
+ memory: Capability;
151
157
  /**
152
158
  * Permission modes this agent natively supports. Modes outside this set
153
159
  * are gated by buildExecCommand: `auto` silently degrades to `edit`,
@@ -177,7 +183,7 @@ export type RulesCapability = false | {
177
183
  file: string;
178
184
  };
179
185
  /** Names of every gateable capability on AgentConfig. */
180
- export type CapabilityName = 'hooks' | 'mcp' | 'mcpHttp' | 'mcpHeaders' | 'allowlist' | 'skills' | 'commands' | 'plugins' | 'subagents' | 'rules' | 'workflows';
186
+ export type CapabilityName = 'hooks' | 'mcp' | 'mcpHttp' | 'mcpHeaders' | 'allowlist' | 'skills' | 'commands' | 'plugins' | 'subagents' | 'rules' | 'workflows' | 'memory';
181
187
  /**
182
188
  * Permission modes controlling agent autonomy.
183
189
  * plan read-only investigation; no writes, no shell side-effects
@@ -516,6 +522,8 @@ export interface DiscoveredPlugin {
516
522
  commands: string[];
517
523
  /** Subagent .md files in the plugin's agents/ directory (names without extension). */
518
524
  agentDefs: string[];
525
+ /** Memory fact basenames from the plugin's memory/ directory (without .md). */
526
+ memory: string[];
519
527
  /** Executable files in the plugin's bin/ directory. */
520
528
  bin: string[];
521
529
  /** MCP server names parsed from .mcp.json. */
@@ -42,6 +42,7 @@ import { emit } from './events.js';
42
42
  import { safeJoin } from './paths.js';
43
43
  import { readSkillSourceCommandMarker, shouldInstallCommandAsSkill } from './command-skills.js';
44
44
  import { getWriter, getDetector } from './staleness/registry.js';
45
+ import { syncMemoryToVersionHome } from './memory.js';
45
46
  /** Promisified exec for running shell commands. */
46
47
  const execAsync = promisify(exec);
47
48
  const execFileAsync = promisify(execFile);
@@ -1127,19 +1128,28 @@ export async function installVersion(agent, version, onProgress, opts) {
1127
1128
  emit('version.install', { agent, version: installedVersion });
1128
1129
  return { success: true, installedVersion };
1129
1130
  }
1130
- // Resolve the `oldest` alias to a concrete npm version up front so the rest
1131
- // of the install path treats it as an ordinary pinned install. (`latest`
1132
- // keeps its bare-package-name + post-install-rename handling below.)
1133
- if (version === 'oldest') {
1134
- const oldest = await getOldestNpmVersion(agent);
1135
- if (!oldest) {
1131
+ // Resolve the `latest`/`oldest` aliases to a concrete npm version up front so
1132
+ // the rest of the install path treats them as an ordinary pinned install.
1133
+ // This is what keeps concurrent installs safe: resolving `latest` only AFTER
1134
+ // npm finished meant the install ran in a shared, well-known
1135
+ // `versions/<agent>/latest/` scratch dir that was renamed to the real version
1136
+ // at the end — so a concurrent `agents view` reconcile
1137
+ // (reconcileStaleLatestForAgent) or a second `latest` install could rename
1138
+ // that dir out from under npm mid-extraction and corrupt the install (the
1139
+ // seeded package.json and half-extracted node_modules would vanish, yielding
1140
+ // ENOENT). A concrete dir per version has no shared name to race on.
1141
+ if (version === 'latest' || version === 'oldest') {
1142
+ const resolved = version === 'latest'
1143
+ ? await getLatestNpmVersion(agent)
1144
+ : await getOldestNpmVersion(agent);
1145
+ if (!resolved) {
1136
1146
  return {
1137
1147
  success: false,
1138
1148
  installedVersion: version,
1139
- error: `Could not resolve the oldest published version for ${agentConfig.name} from npm.`,
1149
+ error: `Could not resolve the ${version} published version for ${agentConfig.name} from npm.`,
1140
1150
  };
1141
1151
  }
1142
- version = oldest;
1152
+ version = resolved;
1143
1153
  }
1144
1154
  ensureAgentsDir();
1145
1155
  const versionDir = getVersionDir(agent, version);
@@ -1161,12 +1171,11 @@ export async function installVersion(agent, version, onProgress, opts) {
1161
1171
  private: true,
1162
1172
  };
1163
1173
  fs.writeFileSync(path.join(versionDir, 'package.json'), JSON.stringify(packageJson, null, 2));
1164
- // Install the package
1165
- const packageSpec = version === 'latest'
1166
- ? agentConfig.npmPackage
1167
- : `${agentConfig.npmPackage}@${version}`;
1168
- // The `${agentConfig.npmPackage}@` prefix is load-bearing: it ensures `version`
1169
- // (which VERSION_RE permits to start with `-`) is never passed as a standalone npm CLI flag.
1174
+ // Install the package. `version` is always concrete here (`latest`/`oldest`
1175
+ // were resolved above), so the spec is always pinned. The `@` prefix is
1176
+ // load-bearing: it ensures `version` (which VERSION_RE permits to start with
1177
+ // `-`) is never passed as a standalone npm CLI flag.
1178
+ const packageSpec = `${agentConfig.npmPackage}@${version}`;
1170
1179
  try {
1171
1180
  // Check npm is available
1172
1181
  const winShell = process.platform === 'win32';
@@ -1181,37 +1190,12 @@ export async function installVersion(agent, version, onProgress, opts) {
1181
1190
  };
1182
1191
  }
1183
1192
  onProgress?.(`Installing ${packageSpec}...`);
1184
- const { stdout } = await execFileAsync('npm', ['install', packageSpec, '--ignore-scripts'], { cwd: versionDir, shell: winShell });
1185
- // Determine the actual installed version
1186
- let installedVersion = version;
1187
- if (version === 'latest') {
1188
- const pkgJsonPath = path.join(versionDir, 'node_modules', agentConfig.npmPackage.replace(/^@/, '').split('/')[0], 'package.json');
1189
- // Try to read the actual version from installed package
1190
- try {
1191
- const installedPkgPath = path.join(versionDir, 'node_modules', agentConfig.npmPackage, 'package.json');
1192
- if (fs.existsSync(installedPkgPath)) {
1193
- const installedPkg = JSON.parse(fs.readFileSync(installedPkgPath, 'utf-8'));
1194
- installedVersion = installedPkg.version;
1195
- // Rename the directory to the actual version
1196
- if (installedVersion !== 'latest') {
1197
- const actualVersionDir = getVersionDir(agent, installedVersion);
1198
- if (!fs.existsSync(actualVersionDir)) {
1199
- fs.renameSync(versionDir, actualVersionDir);
1200
- }
1201
- else {
1202
- // Already exists — drop the 'latest' install artifacts but keep
1203
- // `home/` (may contain conversation history from sessions that
1204
- // ran while the user was on `latest`).
1205
- removeInstallArtifacts(versionDir);
1206
- }
1207
- }
1208
- }
1209
- }
1210
- catch (e) {
1211
- // Failed to determine version - this shouldn't happen
1212
- throw new Error(`Failed to determine installed version: ${e.message}`);
1213
- }
1214
- }
1193
+ await execFileAsync('npm', ['install', packageSpec, '--ignore-scripts'], { cwd: versionDir, shell: winShell });
1194
+ // `version` is concrete (the `latest`/`oldest` aliases were resolved up
1195
+ // front), so the package installed directly into its final versioned dir —
1196
+ // no post-install rename, and no shared `latest/` dir for a concurrent
1197
+ // process to move out from under us.
1198
+ const installedVersion = version;
1215
1199
  // Create versioned alias (e.g., claude@2.0.65)
1216
1200
  createVersionedAlias(agent, installedVersion);
1217
1201
  // Claude reads its global config from CLAUDE_CONFIG_DIR/.claude.json —
@@ -1241,9 +1225,9 @@ export async function installVersion(agent, version, onProgress, opts) {
1241
1225
  // binary (postinstall failed, or a platform with no published native dep)
1242
1226
  // fails there with the correct message rather than throwing here.
1243
1227
  if (agentConfig.npmPackage) {
1244
- // Recompute from installedVersion, not the (possibly renamed) `versionDir`:
1245
- // the `latest` branch above may have renamed the dir to its concrete version.
1246
- const pkgRoot = path.join(getVersionDir(agent, installedVersion), 'node_modules', agentConfig.npmPackage);
1228
+ // `installedVersion === version`, so this is exactly `versionDir` — the
1229
+ // install landed in its final dir with no rename to chase.
1230
+ const pkgRoot = path.join(versionDir, 'node_modules', agentConfig.npmPackage);
1247
1231
  try {
1248
1232
  const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8'));
1249
1233
  const postinstall = pkg?.scripts?.postinstall;
@@ -2335,6 +2319,12 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2335
2319
  const r = workflowsWriter.write({ version, versionHome, selection: workflowsToSync, cwd });
2336
2320
  result.workflows.push(...r.synced);
2337
2321
  }
2322
+ // Knowledge memory (RUSH-1330) — distinct from selection.memory which still
2323
+ // means the composed *rules* file. Always fan out ~/.agents/memory/ facts
2324
+ // into capable agent version homes on every full or partial sync.
2325
+ if (supports(agent, 'memory', version).ok) {
2326
+ syncMemoryToVersionHome(agent, versionHome, cwd);
2327
+ }
2338
2328
  // Write manifest after a full sync (no user-passed selection) so the next
2339
2329
  // launch can skip the slow path. Pattern-derived selections still count as
2340
2330
  // "full" — the agents.yaml patterns describe the intended scope, not a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.52",
3
+ "version": "1.20.53",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,6 +24,7 @@
24
24
  "dist/**/*.js",
25
25
  "dist/**/*.d.ts",
26
26
  "dist/**/*.json",
27
+ "dist/bin/**",
27
28
  "dist/lib/secrets/Agents CLI.app/**",
28
29
  "dist/lib/menubar/MenubarHelper.app/**",
29
30
  "scripts/postinstall.js",
@@ -44,10 +45,10 @@
44
45
  "url": "https://github.com/phnx-labs/agents-cli/issues"
45
46
  },
46
47
  "scripts": {
47
- "build": "tsc && rm -rf 'dist/lib/secrets/AgentsKeychain.app' 'dist/lib/secrets/Agents CLI.app' 'dist/lib/menubar/MenubarHelper.app' && ([ -d 'bin/Agents CLI.app' ] && cp -R 'bin/Agents CLI.app' 'dist/lib/secrets/Agents CLI.app' || true) && ([ -d 'bin/MenubarHelper.app' ] && mkdir -p 'dist/lib/menubar' && cp -R 'bin/MenubarHelper.app' 'dist/lib/menubar/MenubarHelper.app' || true)",
48
+ "build": "tsc && rm -rf 'dist/lib/secrets/AgentsKeychain.app' 'dist/lib/secrets/Agents CLI.app' 'dist/lib/menubar/MenubarHelper.app' && ([ -d 'bin/Agents CLI.app' ] && cp -R 'bin/Agents CLI.app' 'dist/lib/secrets/Agents CLI.app' || true) && ([ -d 'bin/MenubarHelper.app' ] && mkdir -p 'dist/lib/menubar' && cp -R 'bin/MenubarHelper.app' 'dist/lib/menubar/MenubarHelper.app' || true) && ([ -f 'bin/agents-macos' ] && mkdir -p dist/bin && cp -f 'bin/agents-macos' 'dist/bin/agents' && chmod 755 'dist/bin/agents' || true)",
48
49
  "build:bin": "scripts/build-bin.sh",
49
50
  "prepare": "npm run build",
50
- "prepack": "cp ../../README.md README.md && scripts/verify-keychain-helper.sh && scripts/verify-menubar-helper.sh",
51
+ "prepack": "cp ../../README.md README.md && scripts/verify-keychain-helper.sh && scripts/verify-menubar-helper.sh && scripts/verify-cli-binary.sh",
51
52
  "postinstall": "node scripts/postinstall.js",
52
53
  "dev": "tsx src/index.ts",
53
54
  "start": "node dist/index.js",