@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
@@ -98,6 +98,7 @@ export function buildDiscoveredPlugin(pluginRoot, manifest, spec = { kind: 'user
98
98
  scripts: discoverPluginScripts(pluginRoot),
99
99
  commands: discoverPluginCommands(pluginRoot),
100
100
  agentDefs: discoverPluginAgentDefs(pluginRoot),
101
+ memory: discoverPluginMemory(pluginRoot),
101
102
  bin: discoverPluginBin(pluginRoot),
102
103
  mcpServers: discoverPluginMcpServers(pluginRoot),
103
104
  lspServers: discoverPluginLspServers(pluginRoot),
@@ -118,6 +119,7 @@ export function pluginResourceGroups(plugin) {
118
119
  { label: 'commands', items: plugin.commands.map((c) => `/${plugin.name}:${c}`) },
119
120
  { label: 'subagents', items: plugin.agentDefs },
120
121
  { label: 'hooks', items: plugin.hooks },
122
+ { label: 'memory', items: plugin.memory },
121
123
  { label: 'mcp', items: plugin.mcpServers },
122
124
  { label: 'lsp', items: plugin.lspServers },
123
125
  { label: 'monitors', items: plugin.monitors },
@@ -225,6 +227,22 @@ export function pluginSupportsAgent(plugin, agent) {
225
227
  return true;
226
228
  }
227
229
  // ─── Discovery helpers ────────────────────────────────────────────────────────
230
+ /** Fact basenames (no .md) from a plugin's memory/ directory. */
231
+ export function discoverPluginMemory(pluginRoot) {
232
+ const dir = path.join(pluginRoot, 'memory');
233
+ if (!fs.existsSync(dir))
234
+ return [];
235
+ try {
236
+ return fs
237
+ .readdirSync(dir)
238
+ .filter((f) => f.endsWith('.md') && f.toLowerCase() !== 'memory.md')
239
+ .map((f) => f.replace(/\.md$/i, ''))
240
+ .sort();
241
+ }
242
+ catch {
243
+ return [];
244
+ }
245
+ }
228
246
  function discoverPluginSkills(pluginRoot) {
229
247
  const skillsDir = path.join(pluginRoot, 'skills');
230
248
  if (!fs.existsSync(skillsDir))
@@ -475,6 +493,24 @@ export function syncPluginToVersion(plugin, agent, versionHome, options = {}) {
475
493
  if (!pluginSupportsAgent(plugin, agent)) {
476
494
  return result;
477
495
  }
496
+ // OpenCode uses TS/JS modules under ~/.config/opencode/plugins/, not the
497
+ // Claude marketplace layout. Install those modules and return early.
498
+ if (agent === 'opencode') {
499
+ const ok = installOpenCodePlugin(plugin, versionHome);
500
+ result.success = ok;
501
+ if (ok)
502
+ result.skills.push(plugin.name);
503
+ return result;
504
+ }
505
+ // Goose loads Open Plugins from $HOME/.agents/plugins/<name>/ (same layout as
506
+ // agents-cli's source tree). Under the shim HOME is the version home.
507
+ if (agent === 'goose') {
508
+ const ok = installGoosePlugin(plugin, versionHome);
509
+ result.success = ok;
510
+ if (ok)
511
+ result.skills.push(plugin.name);
512
+ return result;
513
+ }
478
514
  const userConfig = loadUserConfig(plugin.name);
479
515
  // Route every marketplace op through the plugin's own marketplace, so a plugin
480
516
  // discovered in an extra/project repo installs under its own
@@ -740,6 +776,167 @@ function migrateLegacyFlatLayout(plugin, agent, versionHome) {
740
776
  catch { /* ignore */ }
741
777
  }
742
778
  }
779
+ // ─── OpenCode plugins (TS/JS modules) ─────────────────────────────────────────
780
+ /**
781
+ * OpenCode loads JS/TS modules from `$HOME/.config/opencode/plugins/` (global)
782
+ * and `<project>/.opencode/plugins/` (project). Under agents-cli version
783
+ * isolation HOME is the version home, so we write:
784
+ * {versionHome}/.config/opencode/plugins/
785
+ *
786
+ * Claude-style marketplace plugins are NOT auto-converted; we install modules
787
+ * from (in order):
788
+ * 1. pluginRoot/opencode/*.{ts,js,mjs,cjs}
789
+ * 2. pluginRoot/plugins/*.{ts,js,mjs,cjs}
790
+ * 3. pluginRoot/*.{ts,js,mjs,cjs} (excluding *.test.* / *.spec.*)
791
+ * If none exist, install still succeeds by writing a marker + copying any
792
+ * package.json so empty plugins don't break sync; opencode simply has nothing
793
+ * to load until a real module appears.
794
+ */
795
+ export function openCodePluginsDir(versionHome) {
796
+ return path.join(versionHome, '.config', 'opencode', 'plugins');
797
+ }
798
+ // OpenCode's local-plugin loader only auto-loads direct *.ts / *.js files under
799
+ // plugins/ — not nested dirs and not .mjs/.cjs (see opencode.ai/docs/plugins).
800
+ const OPENCODE_MODULE_RE = /\.(ts|js)$/i;
801
+ const OPENCODE_TEST_RE = /\.(test|spec)\./i;
802
+ function listOpenCodeModules(dir) {
803
+ if (!fs.existsSync(dir))
804
+ return [];
805
+ // readdir without recursion — only loader-visible direct children.
806
+ return fs.readdirSync(dir, { withFileTypes: true })
807
+ .filter((e) => e.isFile() && OPENCODE_MODULE_RE.test(e.name) && !OPENCODE_TEST_RE.test(e.name) && !e.name.startsWith('.'))
808
+ .map((e) => e.name);
809
+ }
810
+ /** Resolve source module files for an agents-cli plugin to install into OpenCode. */
811
+ export function resolveOpenCodePluginSources(pluginRoot) {
812
+ for (const sub of ['opencode', 'plugins']) {
813
+ const dir = path.join(pluginRoot, sub);
814
+ const files = listOpenCodeModules(dir);
815
+ if (files.length > 0)
816
+ return files.map((f) => path.join(dir, f));
817
+ }
818
+ return listOpenCodeModules(pluginRoot).map((f) => path.join(pluginRoot, f));
819
+ }
820
+ export function installOpenCodePlugin(plugin, versionHome) {
821
+ const destDir = openCodePluginsDir(versionHome);
822
+ fs.mkdirSync(destDir, { recursive: true });
823
+ const sources = resolveOpenCodePluginSources(plugin.root);
824
+ const destPluginDir = path.join(destDir, plugin.name);
825
+ // Clean previous install of this plugin name (file or dir)
826
+ const bareTs = path.join(destDir, `${plugin.name}.ts`);
827
+ const bareJs = path.join(destDir, `${plugin.name}.js`);
828
+ for (const p of [bareTs, bareJs, destPluginDir]) {
829
+ try {
830
+ if (fs.existsSync(p))
831
+ fs.rmSync(p, { recursive: true, force: true });
832
+ }
833
+ catch { /* best effort */ }
834
+ }
835
+ if (sources.length === 0) {
836
+ // No TS/JS modules — still create a managed marker so isPluginSynced can
837
+ // track that we processed the plugin (and so re-sync is idempotent).
838
+ fs.mkdirSync(destPluginDir, { recursive: true });
839
+ fs.writeFileSync(path.join(destPluginDir, '.agents-cli-managed'), `plugin=${plugin.name}\n# no opencode modules found under ${plugin.root}\n`, 'utf-8');
840
+ return true;
841
+ }
842
+ // Install each module as a direct file under plugins/ so the loader sees it.
843
+ // Multi-module plugins get `<name>` or `<name>-<stem>` basenames (never a
844
+ // nested directory — OpenCode does not scan nested plugin dirs).
845
+ for (let i = 0; i < sources.length; i++) {
846
+ const src = sources[i];
847
+ const ext = path.extname(src);
848
+ const stem = path.basename(src, ext);
849
+ const destName = sources.length === 1
850
+ ? `${plugin.name}${ext}`
851
+ : (stem === 'index' || stem === plugin.name
852
+ ? `${plugin.name}${ext}`
853
+ : `${plugin.name}-${stem}${ext}`);
854
+ fs.copyFileSync(src, path.join(destDir, destName));
855
+ }
856
+ // Marker file so isOpenCodePluginInstalled / remove can track multi-file installs.
857
+ fs.mkdirSync(destPluginDir, { recursive: true });
858
+ fs.writeFileSync(path.join(destPluginDir, '.agents-cli-managed'), `plugin=${plugin.name}\nfiles=${sources.map((s) => path.basename(s)).join(',')}\n`, 'utf-8');
859
+ return true;
860
+ }
861
+ export function isOpenCodePluginInstalled(pluginName, versionHome) {
862
+ const destDir = openCodePluginsDir(versionHome);
863
+ if (!fs.existsSync(destDir))
864
+ return false;
865
+ for (const candidate of [
866
+ path.join(destDir, `${pluginName}.ts`),
867
+ path.join(destDir, `${pluginName}.js`),
868
+ path.join(destDir, `${pluginName}.mjs`),
869
+ path.join(destDir, `${pluginName}.cjs`),
870
+ path.join(destDir, pluginName),
871
+ ]) {
872
+ if (fs.existsSync(candidate))
873
+ return true;
874
+ }
875
+ return false;
876
+ }
877
+ export function removeOpenCodePlugin(pluginName, versionHome) {
878
+ const destDir = openCodePluginsDir(versionHome);
879
+ let removed = false;
880
+ for (const candidate of [
881
+ path.join(destDir, `${pluginName}.ts`),
882
+ path.join(destDir, `${pluginName}.js`),
883
+ path.join(destDir, pluginName),
884
+ ]) {
885
+ if (fs.existsSync(candidate)) {
886
+ fs.rmSync(candidate, { recursive: true, force: true });
887
+ removed = true;
888
+ }
889
+ }
890
+ // Flat multi-module installs: <name>-<stem>.ts/js next to the marker dir.
891
+ if (fs.existsSync(destDir)) {
892
+ for (const entry of fs.readdirSync(destDir)) {
893
+ if (entry.startsWith(`${pluginName}-`) && /\.(ts|js)$/i.test(entry)) {
894
+ try {
895
+ fs.rmSync(path.join(destDir, entry), { force: true });
896
+ removed = true;
897
+ }
898
+ catch { /* ignore */ }
899
+ }
900
+ }
901
+ }
902
+ return removed;
903
+ }
904
+ // ─── Goose plugins (Open Plugins under .agents/plugins/) ─────────────────────
905
+ /**
906
+ * Goose auto-discovers Open Plugins at `$HOME/.agents/plugins/<name>/`.
907
+ * Under agents-cli version isolation HOME is the version home, so we install to:
908
+ * {versionHome}/.agents/plugins/<name>/
909
+ *
910
+ * Full plugin directory copy (not marketplace) — same layout goose and
911
+ * agents-cli share for source plugins.
912
+ */
913
+ export function goosePluginsDir(versionHome) {
914
+ return path.join(versionHome, '.agents', 'plugins');
915
+ }
916
+ export function installGoosePlugin(plugin, versionHome) {
917
+ const destRoot = path.join(goosePluginsDir(versionHome), plugin.name);
918
+ try {
919
+ if (fs.existsSync(destRoot)) {
920
+ fs.rmSync(destRoot, { recursive: true, force: true });
921
+ }
922
+ fs.cpSync(plugin.root, destRoot, { recursive: true });
923
+ fs.writeFileSync(path.join(destRoot, '.agents-cli-managed'), `plugin=${plugin.name}\n`, 'utf-8');
924
+ return true;
925
+ }
926
+ catch {
927
+ return false;
928
+ }
929
+ }
930
+ export function isGoosePluginInstalled(pluginName, versionHome) {
931
+ return fs.existsSync(path.join(goosePluginsDir(versionHome), pluginName));
932
+ }
933
+ export function removeGoosePlugin(pluginName, versionHome) {
934
+ const destRoot = path.join(goosePluginsDir(versionHome), pluginName);
935
+ if (!fs.existsSync(destRoot))
936
+ return false;
937
+ fs.rmSync(destRoot, { recursive: true, force: true });
938
+ return true;
939
+ }
743
940
  // ─── Sync status ──────────────────────────────────────────────────────────────
744
941
  /**
745
942
  * Check if a plugin is synced to a version. True when the plugin lives at the
@@ -749,6 +946,12 @@ function migrateLegacyFlatLayout(plugin, agent, versionHome) {
749
946
  export function isPluginSynced(plugin, agent, versionHome) {
750
947
  if (!isCapable(agent, 'plugins'))
751
948
  return false;
949
+ if (agent === 'opencode') {
950
+ return isOpenCodePluginInstalled(plugin.name, versionHome);
951
+ }
952
+ if (agent === 'goose') {
953
+ return isGoosePluginInstalled(plugin.name, versionHome);
954
+ }
752
955
  const spec = marketplaceSpecForName(plugin.marketplace);
753
956
  if (!isInstalledInMarketplace(plugin.name, spec, agent, versionHome))
754
957
  return false;
@@ -776,6 +979,20 @@ export function removePluginFromVersion(pluginName, pluginRoot, agent, versionHo
776
979
  permissions: 0,
777
980
  mcp: 0,
778
981
  };
982
+ // OpenCode: remove TS/JS modules from ~/.config/opencode/plugins/.
983
+ if (agent === 'opencode') {
984
+ if (removeOpenCodePlugin(pluginName, versionHome)) {
985
+ result.skills.push(pluginName);
986
+ }
987
+ return result;
988
+ }
989
+ // Goose: remove Open Plugin directory from versionHome/.agents/plugins/.
990
+ if (agent === 'goose') {
991
+ if (removeGoosePlugin(pluginName, versionHome)) {
992
+ result.skills.push(pluginName);
993
+ }
994
+ return result;
995
+ }
779
996
  // 1. Remove the plugin from every marketplace it's installed under. A name can
780
997
  // appear in more than one (collision across repos), so we sweep them all.
781
998
  let removedAny = false;
@@ -0,0 +1,14 @@
1
+ export interface RemoteAgentsJsonOptions<T> {
2
+ args: string[];
3
+ noFanoutEnv: string;
4
+ hosts?: string[];
5
+ parse: (stdout: string, machine: string) => T[];
6
+ }
7
+ export interface RemoteAgentsJsonResult<T> {
8
+ items: T[];
9
+ deviceCount: number;
10
+ }
11
+ /** Build the command one peer runs, with a guard that prevents recursive fan-out. */
12
+ export declare function remoteAgentsJsonCommand(args: string[], noFanoutEnv: string, os?: string): string;
13
+ /** Query explicit hosts, or every registered online peer when hosts is omitted. */
14
+ export declare function gatherRemoteAgentsJson<T>(options: RemoteAgentsJsonOptions<T>): Promise<RemoteAgentsJsonResult<T>>;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Shared cross-machine fan-out for JSON-producing `agents` commands.
3
+ *
4
+ * Registered online devices are queried in parallel over the canonical SSH
5
+ * transport. A recursion-guard environment variable makes each peer answer for
6
+ * itself, and version-skewed or unreachable peers are skipped without hiding
7
+ * healthy results from the rest of the fleet.
8
+ */
9
+ import { spawn } from 'child_process';
10
+ import chalk from 'chalk';
11
+ import { SSH_OPTS, controlOpts, assertValidSshTarget, shellQuote } from './ssh-exec.js';
12
+ import { sshTargetFor } from './devices/connect.js';
13
+ import { resolveExplicitTargets } from './devices/resolve-target.js';
14
+ import { loadDevices } from './devices/registry.js';
15
+ import { remoteShellFor, buildWindowsAgentsCommand } from './hosts/remote-cmd.js';
16
+ import { machineId, normalizeHost } from './machine-id.js';
17
+ const REMOTE_TIMEOUT_MS = 12_000;
18
+ /** Build the command one peer runs, with a guard that prevents recursive fan-out. */
19
+ export function remoteAgentsJsonCommand(args, noFanoutEnv, os) {
20
+ if (remoteShellFor(os) === 'powershell') {
21
+ return buildWindowsAgentsCommand({ args, env: { [noFanoutEnv]: '1' } });
22
+ }
23
+ const inner = `${noFanoutEnv}=1 agents ${args.map(shellQuote).join(' ')}`;
24
+ return `bash -lc ${shellQuote(inner)}`;
25
+ }
26
+ function sshCapture(target, remoteCmd) {
27
+ assertValidSshTarget(target);
28
+ return new Promise((resolve) => {
29
+ const args = [...SSH_OPTS, ...controlOpts(), target, remoteCmd];
30
+ const child = spawn('ssh', args, { stdio: ['ignore', 'pipe', 'ignore'] });
31
+ let stdout = '';
32
+ let settled = false;
33
+ const done = (code) => {
34
+ if (settled)
35
+ return;
36
+ settled = true;
37
+ clearTimeout(timer);
38
+ resolve({ code, stdout });
39
+ };
40
+ const timer = setTimeout(() => {
41
+ child.kill('SIGKILL');
42
+ done(null);
43
+ }, REMOTE_TIMEOUT_MS);
44
+ child.stdout.on('data', (data) => { stdout += data.toString(); });
45
+ child.on('error', () => done(null));
46
+ child.on('close', (code) => done(code));
47
+ });
48
+ }
49
+ /** Query explicit hosts, or every registered online peer when hosts is omitted. */
50
+ export async function gatherRemoteAgentsJson(options) {
51
+ const self = machineId();
52
+ const targets = [];
53
+ if (options.hosts && options.hosts.length > 0) {
54
+ targets.push(...await resolveExplicitTargets(options.hosts));
55
+ }
56
+ else {
57
+ let devices;
58
+ try {
59
+ devices = await loadDevices();
60
+ }
61
+ catch {
62
+ return { items: [], deviceCount: 0 };
63
+ }
64
+ for (const device of Object.values(devices)) {
65
+ if (device.tailscale?.online !== true)
66
+ continue;
67
+ if (normalizeHost(device.name) === self)
68
+ continue;
69
+ if (!['windows', 'linux', 'macos'].includes(device.platform))
70
+ continue;
71
+ try {
72
+ targets.push({
73
+ target: sshTargetFor(device),
74
+ machine: normalizeHost(device.name),
75
+ name: device.name,
76
+ os: device.platform,
77
+ });
78
+ }
79
+ catch {
80
+ // A registered profile without a dialable address is not a peer yet.
81
+ }
82
+ }
83
+ }
84
+ const results = await Promise.all(targets.map(async (target) => {
85
+ const command = remoteAgentsJsonCommand(options.args, options.noFanoutEnv, target.os);
86
+ const result = await sshCapture(target.target, command);
87
+ if (result.code !== 0) {
88
+ process.stderr.write(chalk.gray(` ${target.name}: unreachable or no agents CLI — skipped\n`));
89
+ return [];
90
+ }
91
+ return options.parse(result.stdout, target.machine);
92
+ }));
93
+ return { items: results.flat(), deviceCount: targets.length };
94
+ }
@@ -123,6 +123,10 @@ export function getMcpConfigPath(agent, versionHome) {
123
123
  return path.join(versionHome, '.gemini', 'antigravity-cli', 'mcp_config.json');
124
124
  case 'grok':
125
125
  return path.join(versionHome, '.grok', 'mcp.json');
126
+ case 'hermes':
127
+ return path.join(versionHome, '.hermes', 'config.yaml');
128
+ case 'forge':
129
+ return path.join(versionHome, '.forge', '.mcp.json');
126
130
  default:
127
131
  return null;
128
132
  }
@@ -271,6 +275,38 @@ function syncToGrokConfig(configPath, items) {
271
275
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
272
276
  fs.writeFileSync(configPath, TOML.stringify(config), 'utf-8');
273
277
  }
278
+ function syncToHermesConfig(configPath, items) {
279
+ let config = {};
280
+ if (fs.existsSync(configPath)) {
281
+ try {
282
+ const parsed = yaml.parse(fs.readFileSync(configPath, 'utf-8'));
283
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
284
+ config = parsed;
285
+ }
286
+ }
287
+ catch {
288
+ config = {};
289
+ }
290
+ }
291
+ const mcpServers = {};
292
+ for (const item of items) {
293
+ if (item.transport === 'stdio') {
294
+ mcpServers[item.name] = {
295
+ command: item.command,
296
+ args: item.args || [],
297
+ ...(item.env && { env: item.env }),
298
+ };
299
+ }
300
+ else {
301
+ mcpServers[item.name] = {
302
+ url: item.url,
303
+ };
304
+ }
305
+ }
306
+ config.mcp_servers = mcpServers;
307
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
308
+ fs.writeFileSync(configPath, yaml.stringify(config), 'utf-8');
309
+ }
274
310
  /**
275
311
  * Write MCP servers to OpenCode opencode.jsonc format.
276
312
  */
@@ -502,6 +538,12 @@ export const McpHandler = {
502
538
  case 'grok':
503
539
  syncToGrokConfig(configPath, mcpItems);
504
540
  break;
541
+ case 'hermes':
542
+ syncToHermesConfig(configPath, mcpItems);
543
+ break;
544
+ case 'forge':
545
+ syncToCursorConfig(configPath, mcpItems);
546
+ break;
505
547
  }
506
548
  },
507
549
  format(agent) {
@@ -509,6 +551,8 @@ export const McpHandler = {
509
551
  case 'codex':
510
552
  case 'grok':
511
553
  return 'toml';
554
+ case 'hermes':
555
+ return 'yaml';
512
556
  default:
513
557
  return 'json';
514
558
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Memory resource handler — knowledge/facts store (not rules/instructions).
3
+ *
4
+ * Canonical source: layered `memory/` dirs under project/user/system agents
5
+ * roots. Sync copies into each capable agent's version home (see memory.ts).
6
+ */
7
+ import type { ResourceHandler } from './types.js';
8
+ export interface MemoryItem {
9
+ name: string;
10
+ content: string;
11
+ summary: string;
12
+ }
13
+ export declare const MemoryHandler: ResourceHandler<MemoryItem>;
14
+ /** Convenience: list fact names for a cwd. */
15
+ export declare function listMemoryNames(cwd?: string): string[];
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Memory resource handler — knowledge/facts store (not rules/instructions).
3
+ *
4
+ * Canonical source: layered `memory/` dirs under project/user/system agents
5
+ * roots. Sync copies into each capable agent's version home (see memory.ts).
6
+ */
7
+ import * as fs from 'fs';
8
+ import { listMemoryFacts, readMemoryFact, syncMemoryToVersionHome, memoryTargetDir, } from '../memory.js';
9
+ function toItem(fact) {
10
+ let content = '';
11
+ try {
12
+ content = fs.readFileSync(fact.path, 'utf-8');
13
+ }
14
+ catch {
15
+ content = '';
16
+ }
17
+ return {
18
+ name: fact.name,
19
+ item: { name: fact.name, content, summary: fact.summary },
20
+ layer: fact.layer,
21
+ path: fact.path,
22
+ };
23
+ }
24
+ export const MemoryHandler = {
25
+ kind: 'memory',
26
+ listAll(_agent, cwd) {
27
+ return listMemoryFacts(cwd).map(toItem);
28
+ },
29
+ resolve(_agent, name, cwd) {
30
+ const fact = readMemoryFact(name, cwd);
31
+ return fact ? toItem(fact) : null;
32
+ },
33
+ sync(agent, versionHome, cwd) {
34
+ syncMemoryToVersionHome(agent, versionHome, cwd);
35
+ },
36
+ format(_agent) {
37
+ return 'md';
38
+ },
39
+ targetDir(agent) {
40
+ return memoryTargetDir(agent);
41
+ },
42
+ };
43
+ /** Convenience: list fact names for a cwd. */
44
+ export function listMemoryNames(cwd) {
45
+ return listMemoryFacts(cwd).map((f) => f.name);
46
+ }
@@ -5,9 +5,9 @@
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' | 'antigravity' | 'grok' | 'kimi';
8
+ export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'antigravity' | 'grok' | 'kimi' | 'hermes' | 'forge';
9
9
  export type Layer = 'system' | 'user' | 'project';
10
- export type ResourceKind = 'command' | 'hook' | 'skill' | 'rule' | 'mcp' | 'permission' | 'subagent' | 'workflow';
10
+ export type ResourceKind = 'command' | 'hook' | 'skill' | 'rule' | 'mcp' | 'permission' | 'subagent' | 'workflow' | 'memory';
11
11
  /** A resolved resource with its origin layer. */
12
12
  export interface ResolvedItem<T> {
13
13
  name: string;
@@ -5,10 +5,19 @@
5
5
  * sandboxed or unsandboxed environments, captures stdout to log files,
6
6
  * enforces timeouts, and extracts the final assistant report from the
7
7
  * agent's stream-JSON output.
8
+ *
9
+ * Version/account selection mirrors `agents run`: when a routine does not pin
10
+ * `version:`, the runner uses the configured run strategy (default `balanced`)
11
+ * to pick a healthy install, pins the absolute binary via `getBinaryPath`, and
12
+ * arms same-agent failover across other healthy accounts when a rate/usage
13
+ * limit is detected mid-run (foreground `executeJob` only — detached daemon
14
+ * fires once with the pre-flight pick).
8
15
  */
9
16
  import type { JobConfig, RunMeta } from './routines.js';
10
17
  import type { AgentId } from './types.js';
18
+ import { type FallbackEntry } from './exec.js';
11
19
  import type { LoopDeps } from './loop.js';
20
+ import { type RotateResult } from './rotate.js';
12
21
  /** Result of a completed job execution, including metadata and optional report. */
13
22
  export interface RunResult {
14
23
  meta: RunMeta;
@@ -16,6 +25,37 @@ export interface RunResult {
16
25
  }
17
26
  /** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
18
27
  export declare function buildJobCommand(config: JobConfig, resolvedPrompt: string): string[];
28
+ /** Pre-flight version/account selection for a routine job. */
29
+ export interface RoutineLaunchPlan {
30
+ /** Ordered attempts: primary first, then same-agent failover accounts. */
31
+ chain: FallbackEntry[];
32
+ /** Full rotation result when strategy selected among healthy accounts; null when pinned. */
33
+ rotation: RotateResult | null;
34
+ /** True when `config.version` pinned the target (no rotation). */
35
+ pinned: boolean;
36
+ }
37
+ /**
38
+ * Resolve the version/account chain for a routine the same way `agents run`
39
+ * does: honor an explicit `version:` pin; otherwise use the configured run
40
+ * strategy (default `balanced`) so credit-exhausted / rate-limited accounts
41
+ * are skipped pre-flight, and synthesize a same-agent failover chain from the
42
+ * other healthy accounts for mid-run rate limits.
43
+ *
44
+ * Workflows are left alone — `agents run <workflow>` owns selection.
45
+ */
46
+ export declare function resolveRoutineLaunch(config: JobConfig, cwd?: string): Promise<RoutineLaunchPlan>;
47
+ /**
48
+ * Rewrite `cmd[0]` to the absolute binary for `agent@version` when installed.
49
+ * Bypasses the bare-name shim so a sandboxed HOME / missing default pin cannot
50
+ * surface as "agents: no version of X configured".
51
+ */
52
+ export declare function pinJobBinary(cmd: string[], agent: AgentId, version: string | undefined): string[];
53
+ /**
54
+ * Merge sandbox/base env with the canonical per-version exec env
55
+ * (CLAUDE_CONFIG_DIR / CODEX_HOME / …) so routines share account isolation
56
+ * with `agents run`.
57
+ */
58
+ export declare function buildRoutineSpawnEnv(baseEnv: Record<string, string>, agent: AgentId, version: string | undefined, timezone?: string): Record<string, string>;
19
59
  /**
20
60
  * Execute a job synchronously (waits for completion or timeout before resolving).
21
61
  *
@@ -24,6 +64,9 @@ export declare function buildJobCommand(config: JobConfig, resolvedPrompt: strin
24
64
  * workflow `loop:` blocks (issue #400). The optional `deps` parameter provides
25
65
  * injectable seams (runIteration, sleep, writeCheckpoint) used by tests; production
26
66
  * callers omit it and get the defaults.
67
+ *
68
+ * Single-shot path: pre-flight version/account selection + mid-run rate-limit
69
+ * failover across healthy same-agent accounts (RUSH-1016).
27
70
  */
28
71
  export declare function executeJob(config: JobConfig, deps?: LoopDeps): Promise<RunResult>;
29
72
  /** Spawn a job as a detached process and return immediately with run metadata. */