@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.
- package/CHANGELOG.md +57 -2
- package/README.md +12 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.d.ts +3 -0
- package/dist/commands/cloud.js +2 -1
- package/dist/commands/exec.js +65 -1
- package/dist/commands/feed.d.ts +29 -0
- package/dist/commands/feed.js +237 -32
- package/dist/commands/memory.d.ts +9 -0
- package/dist/commands/memory.js +164 -0
- package/dist/commands/message.d.ts +11 -6
- package/dist/commands/message.js +140 -5
- package/dist/commands/routines.js +12 -0
- package/dist/commands/secrets-migrate.d.ts +2 -1
- package/dist/commands/secrets-migrate.js +88 -13
- package/dist/commands/secrets.js +4 -1
- package/dist/commands/sessions.js +10 -1
- package/dist/commands/worktree.js +4 -2
- package/dist/index.js +16 -21
- package/dist/lib/agents.js +249 -17
- package/dist/lib/answer-router.d.ts +75 -0
- package/dist/lib/answer-router.js +149 -0
- package/dist/lib/ask-classifier.d.ts +71 -0
- package/dist/lib/ask-classifier.js +197 -0
- package/dist/lib/cloud/antigravity.d.ts +0 -2
- package/dist/lib/cloud/antigravity.js +2 -17
- package/dist/lib/cloud/codex.js +3 -18
- package/dist/lib/cloud/rush.js +3 -15
- package/dist/lib/cloud/stream.js +2 -0
- package/dist/lib/cloud/types.d.ts +21 -0
- package/dist/lib/cloud/types.js +81 -0
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +12 -2
- package/dist/lib/crabbox/lease.d.ts +13 -0
- package/dist/lib/crabbox/lease.js +11 -2
- package/dist/lib/crabbox/progress.d.ts +62 -0
- package/dist/lib/crabbox/progress.js +129 -0
- package/dist/lib/events.js +4 -1
- package/dist/lib/exec.js +19 -1
- package/dist/lib/feed-outcome.d.ts +101 -0
- package/dist/lib/feed-outcome.js +244 -0
- package/dist/lib/feed-policy.d.ts +30 -0
- package/dist/lib/feed-policy.js +133 -0
- package/dist/lib/feed.d.ts +127 -3
- package/dist/lib/feed.js +416 -40
- package/dist/lib/git.d.ts +17 -1
- package/dist/lib/git.js +20 -1
- package/dist/lib/hooks.js +522 -12
- package/dist/lib/hosts/passthrough.d.ts +3 -3
- package/dist/lib/hosts/passthrough.js +3 -4
- package/dist/lib/mailbox-gc.d.ts +22 -0
- package/dist/lib/mailbox-gc.js +161 -0
- package/dist/lib/mailbox.d.ts +26 -2
- package/dist/lib/mailbox.js +80 -5
- package/dist/lib/mcp.js +82 -0
- package/dist/lib/memory.d.ts +55 -0
- package/dist/lib/memory.js +274 -0
- package/dist/lib/notify.d.ts +16 -0
- package/dist/lib/notify.js +61 -0
- package/dist/lib/operator.d.ts +26 -0
- package/dist/lib/operator.js +107 -0
- package/dist/lib/plugins.d.ts +35 -0
- package/dist/lib/plugins.js +217 -0
- package/dist/lib/remote-agents-json.d.ts +14 -0
- package/dist/lib/remote-agents-json.js +94 -0
- package/dist/lib/resources/mcp.js +44 -0
- package/dist/lib/resources/memory.d.ts +15 -0
- package/dist/lib/resources/memory.js +46 -0
- package/dist/lib/resources/types.d.ts +2 -2
- package/dist/lib/runner.d.ts +43 -0
- package/dist/lib/runner.js +323 -74
- package/dist/lib/sandbox.js +6 -0
- package/dist/lib/secrets/bundles.js +38 -13
- package/dist/lib/secrets/icloud-import.d.ts +12 -3
- package/dist/lib/secrets/icloud-import.js +37 -7
- package/dist/lib/secrets/index.d.ts +106 -2
- package/dist/lib/secrets/index.js +603 -28
- package/dist/lib/session/active.d.ts +18 -0
- package/dist/lib/session/active.js +47 -17
- package/dist/lib/session/db.d.ts +9 -1
- package/dist/lib/session/db.js +18 -3
- package/dist/lib/session/discover.d.ts +13 -0
- package/dist/lib/session/discover.js +31 -0
- package/dist/lib/session/parse.d.ts +8 -0
- package/dist/lib/session/parse.js +42 -21
- package/dist/lib/session/remote-active.js +8 -89
- package/dist/lib/session/state.d.ts +11 -0
- package/dist/lib/session/state.js +37 -0
- package/dist/lib/session/tail.d.ts +23 -4
- package/dist/lib/session/tail.js +34 -16
- package/dist/lib/session/throughput.d.ts +30 -0
- package/dist/lib/session/throughput.js +86 -0
- package/dist/lib/shim-heal.d.ts +12 -3
- package/dist/lib/shim-heal.js +12 -6
- package/dist/lib/staleness/detectors/subagents.js +57 -3
- package/dist/lib/staleness/writers/hooks.js +7 -3
- package/dist/lib/staleness/writers/subagents.js +37 -6
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/subagents.d.ts +52 -0
- package/dist/lib/subagents.js +315 -12
- package/dist/lib/teams/worktree.d.ts +8 -0
- package/dist/lib/teams/worktree.js +8 -0
- package/dist/lib/types.d.ts +10 -2
- package/dist/lib/versions.js +38 -48
- package/package.json +4 -3
- package/scripts/postinstall.js +61 -1
package/dist/lib/plugins.js
CHANGED
|
@@ -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;
|
package/dist/lib/runner.d.ts
CHANGED
|
@@ -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. */
|