@phnx-labs/agents-cli 1.22.33 → 1.22.34
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 +49 -0
- package/README.md +2 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/accounts.js +3 -3
- package/dist/commands/browser-sessions-picker.d.ts +16 -0
- package/dist/commands/browser-sessions-picker.js +179 -0
- package/dist/commands/browser.js +9 -4
- package/dist/commands/hosts.js +1 -5
- package/dist/commands/inspect.js +174 -41
- package/dist/commands/message.d.ts +6 -1
- package/dist/commands/message.js +60 -3
- package/dist/commands/sessions.d.ts +54 -4
- package/dist/commands/sessions.js +252 -46
- package/dist/commands/ssh.js +17 -5
- package/dist/commands/teams.d.ts +28 -0
- package/dist/commands/teams.js +148 -13
- package/dist/commands/upgrade.d.ts +7 -0
- package/dist/commands/upgrade.js +10 -0
- package/dist/commands/watchdog.d.ts +2 -0
- package/dist/commands/watchdog.js +112 -27
- package/dist/index.js +51 -59
- package/dist/lib/agents.js +6 -0
- package/dist/lib/browser/sessions-list.d.ts +81 -0
- package/dist/lib/browser/sessions-list.js +179 -4
- package/dist/lib/daemon.js +45 -5
- package/dist/lib/devices/connect.d.ts +33 -0
- package/dist/lib/devices/connect.js +61 -3
- package/dist/lib/devices/doctor-findings.d.ts +4 -2
- package/dist/lib/devices/doctor-findings.js +4 -2
- package/dist/lib/exec.js +63 -6
- package/dist/lib/help.d.ts +3 -2
- package/dist/lib/help.js +4 -0
- package/dist/lib/hosts/dispatch.d.ts +2 -32
- package/dist/lib/hosts/dispatch.js +6 -61
- package/dist/lib/hosts/tasks.d.ts +7 -0
- package/dist/lib/hosts/tasks.js +9 -0
- package/dist/lib/mailbox-target.d.ts +27 -0
- package/dist/lib/mailbox-target.js +21 -0
- package/dist/lib/mcp.d.ts +10 -0
- package/dist/lib/mcp.js +21 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +11 -0
- package/dist/lib/migrate.js +40 -0
- package/dist/lib/project-root.d.ts +47 -0
- package/dist/lib/project-root.js +68 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +9 -2
- package/dist/lib/secrets/agent.js +52 -7
- package/dist/lib/secrets/reaper.d.ts +24 -3
- package/dist/lib/secrets/reaper.js +55 -6
- package/dist/lib/session/discover.js +12 -0
- package/dist/lib/session/render.d.ts +2 -0
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/shims.js +9 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +8 -3
- package/dist/lib/startup/root-command.d.ts +3 -0
- package/dist/lib/startup/root-command.js +10 -0
- package/dist/lib/teams/agents.d.ts +136 -6
- package/dist/lib/teams/agents.js +324 -58
- package/dist/lib/teams/worktree.d.ts +39 -2
- package/dist/lib/teams/worktree.js +60 -4
- package/dist/lib/types.d.ts +11 -0
- package/dist/lib/versions.js +2 -2
- package/dist/lib/watchdog/history.d.ts +20 -0
- package/dist/lib/watchdog/history.js +46 -0
- package/dist/lib/watchdog/log.d.ts +16 -1
- package/dist/lib/watchdog/log.js +82 -2
- package/dist/lib/watchdog/runner.d.ts +12 -0
- package/dist/lib/watchdog/runner.js +20 -0
- package/package.json +1 -1
package/dist/commands/inspect.js
CHANGED
|
@@ -27,11 +27,11 @@ import { resolveSingleAgentTarget, AgentSpecError } from '../lib/agent-spec/inde
|
|
|
27
27
|
import { readMeta, getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../lib/state.js';
|
|
28
28
|
import { getVersionHomePath, isVersionIsolated, getIsolatedDefault, } from '../lib/versions.js';
|
|
29
29
|
import { getShimsDir, getVersionedAliasPath } from '../lib/shims.js';
|
|
30
|
-
import { getAgentResources, listResources, } from '../lib/resources.js';
|
|
30
|
+
import { getAgentResources, isDirectoryDoc, listResources, } from '../lib/resources.js';
|
|
31
31
|
import { listHookEntriesFromDir } from '../lib/hooks.js';
|
|
32
32
|
import { getResourceInventory } from '../lib/resource-inventory.js';
|
|
33
33
|
import { listMcpServerConfigs, discoverMcpConfigsFromRepo } from '../lib/mcp.js';
|
|
34
|
-
import { discoverPlugins, discoverPluginsInDir, pluginResourceGroups } from '../lib/plugins.js';
|
|
34
|
+
import { discoverPlugins, discoverPluginsInDir, pluginResourceGroups, inspectPluginCapabilities, pluginCapabilityLabels } from '../lib/plugins.js';
|
|
35
35
|
import { PLUGIN_GROUP_COLORS } from './plugins.js';
|
|
36
36
|
import { countSessionsInScope } from '../lib/session/discover.js';
|
|
37
37
|
import { isSessionTrackedAgent } from '../lib/session/types.js';
|
|
@@ -292,7 +292,36 @@ export function collectRepoKind(repo, kind) {
|
|
|
292
292
|
.map(p => pluginToItem(p, repo.label))
|
|
293
293
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
294
294
|
}
|
|
295
|
-
|
|
295
|
+
// Hooks live nested under event directories (`hooks/pre-tool-use/…`), and a
|
|
296
|
+
// script pairs with its data sidecar. repoHookItems already reads them that
|
|
297
|
+
// way for the summary view; a flat readdir here returned the event dirs
|
|
298
|
+
// themselves plus README/test scaffolding, so `--hooks` and the summary
|
|
299
|
+
// reported different counts for the same repo.
|
|
300
|
+
if (kind === 'hooks')
|
|
301
|
+
return repoHookItems(repo);
|
|
302
|
+
// A repo's `rules/` holds the COMPOSED output (AGENTS.md plus its CLAUDE.md /
|
|
303
|
+
// GEMINI.md symlinks) alongside a `subrules/` dir of the individually named
|
|
304
|
+
// fragments. The fragments are what `--rule <name>` resolves and what a reader
|
|
305
|
+
// means by "a rule"; the composed file is a build artifact. Without this,
|
|
306
|
+
// `subrules` listed as a single opaque leaf and every real rule was unreachable.
|
|
307
|
+
// Deliberately exclusive: once `subrules/` exists it is the sole home for
|
|
308
|
+
// rules (composeRules only ever resolves names under it — lib/rules/compose.ts),
|
|
309
|
+
// so loose top-level `.md` files are not listed. A repo mid-migration to the
|
|
310
|
+
// subrules convention would see those legacy files disappear from `--rules`;
|
|
311
|
+
// that is the correct signal, since composeRules would not load them either.
|
|
312
|
+
if (kind === 'rules') {
|
|
313
|
+
const subrulesDir = path.join(repo.root, kind, 'subrules');
|
|
314
|
+
if (fs.existsSync(subrulesDir))
|
|
315
|
+
return readResourceDir(subrulesDir, kind, repo.label);
|
|
316
|
+
}
|
|
317
|
+
return readResourceDir(path.join(repo.root, kind), kind, repo.label);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Enumerate one directory of resources of `kind`, skipping dotfiles, build caches,
|
|
321
|
+
* and directory docs. Shared so the rules `subrules/` branch and the default
|
|
322
|
+
* `<repo>/<kind>/` branch cannot drift apart.
|
|
323
|
+
*/
|
|
324
|
+
function readResourceDir(dir, kind, source) {
|
|
296
325
|
let entries;
|
|
297
326
|
try {
|
|
298
327
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
@@ -307,10 +336,16 @@ export function collectRepoKind(repo, kind) {
|
|
|
307
336
|
// Build/tooling caches are never resources — they only inflate counts.
|
|
308
337
|
if (entry.name === '__pycache__' || entry.name === 'node_modules')
|
|
309
338
|
continue;
|
|
339
|
+
const name = entry.name.replace(/\.(md|yaml|yml|toml|json)$/, '');
|
|
340
|
+
// README/AGENTS/CLAUDE/GEMINI describe the directory, not resources of this
|
|
341
|
+
// kind. CLAUDE.md/GEMINI.md are symlinks to AGENTS.md, so a Dirent reports
|
|
342
|
+
// isFile() === false — use !isDirectory() to catch them (mirrors resources.ts).
|
|
343
|
+
if (!entry.isDirectory() && isDirectoryDoc(kind, name))
|
|
344
|
+
continue;
|
|
310
345
|
const p = path.join(dir, entry.name);
|
|
311
346
|
items.push({
|
|
312
|
-
name
|
|
313
|
-
source
|
|
347
|
+
name,
|
|
348
|
+
source,
|
|
314
349
|
path: p,
|
|
315
350
|
linkTarget: linkTarget(p),
|
|
316
351
|
description: readDescription(p),
|
|
@@ -406,7 +441,6 @@ function renderRepoSummary(repo, options) {
|
|
|
406
441
|
const kindData = {};
|
|
407
442
|
let totalBytes = 0, totalFiles = 0;
|
|
408
443
|
let repoHookByScript = new Map();
|
|
409
|
-
let repoHookItemList = [];
|
|
410
444
|
let repoMcpConfigs = new Map();
|
|
411
445
|
if (!options.brief) {
|
|
412
446
|
for (const kind of DRILLABLE_KINDS) {
|
|
@@ -417,7 +451,6 @@ function renderRepoSummary(repo, options) {
|
|
|
417
451
|
totalFiles += size.files;
|
|
418
452
|
}
|
|
419
453
|
repoHookByScript = hookManifestByScript(hookManifestFromFile(path.join(repo.root, 'agents.yaml')));
|
|
420
|
-
repoHookItemList = repoHookItems(repo);
|
|
421
454
|
repoMcpConfigs = new Map(discoverMcpConfigsFromRepo(repo.root).map(s => [s.name, s.config]));
|
|
422
455
|
}
|
|
423
456
|
if (options.json) {
|
|
@@ -430,8 +463,7 @@ function renderRepoSummary(repo, options) {
|
|
|
430
463
|
size: options.brief ? null : { bytes: totalBytes, files: totalFiles },
|
|
431
464
|
resources: options.brief ? null : Object.fromEntries(DRILLABLE_KINDS.map(kind => {
|
|
432
465
|
const size = kindData[kind].size;
|
|
433
|
-
|
|
434
|
-
const items = kind === 'hooks' ? repoHookItemList : kindData[kind].items;
|
|
466
|
+
const items = kindData[kind].items;
|
|
435
467
|
const base = {
|
|
436
468
|
count: items.length,
|
|
437
469
|
bytes: size.bytes,
|
|
@@ -506,7 +538,7 @@ function renderRepoSummary(repo, options) {
|
|
|
506
538
|
const preview = items.length > 0 ? chalk.gray(truncate(previewNames(items, 4), 60)) : '';
|
|
507
539
|
console.log(` ${kind.padEnd(10)} ${count} ${sz} ${preview}`.trimEnd());
|
|
508
540
|
}
|
|
509
|
-
printExpandedSection('Hooks', hookRows(
|
|
541
|
+
printExpandedSection('Hooks', hookRows(kindData.hooks.items, repoHookByScript));
|
|
510
542
|
printExpandedSection('Plugins', pluginRows(kindData.plugins.items));
|
|
511
543
|
printExpandedSection('MCP', mcpRows(kindData.mcp.items, repoMcpConfigs));
|
|
512
544
|
}
|
|
@@ -745,10 +777,23 @@ function renderItemDetail(header, jsonHead, kind, query, items, options) {
|
|
|
745
777
|
const matchTag = best.matchKind === 'exact' ? 'exact' : best.matchKind === 'substring' ? 'substring' : `~${best.distance}`;
|
|
746
778
|
console.log(` ${chalk.green('✓')} ${termLink(chalk.bold.cyan(best.item.name), best.item.linkTarget)} ${chalk.gray(`[${matchTag}, ${best.item.source}]`)}`);
|
|
747
779
|
if (best.item.description) {
|
|
748
|
-
|
|
780
|
+
// Wrap to the real terminal width. This used to be truncate(desc, 100): a
|
|
781
|
+
// character count blind to the window, so a long description lost its
|
|
782
|
+
// sentence at 80 columns AND wasted the space at 200.
|
|
783
|
+
for (const line of wrapJoined(' ', best.item.description.split(/\s+/), ' ', terminalWidth())) {
|
|
784
|
+
console.log(chalk.gray(line));
|
|
785
|
+
}
|
|
749
786
|
}
|
|
750
787
|
for (const [k, v] of buildDetailRows(best.item, kind)) {
|
|
751
|
-
|
|
788
|
+
// Wrap, never truncate. These values are `, `-joined lists (commands,
|
|
789
|
+
// skills, triggers, tools) and cutting them hides real entries — a 9-command
|
|
790
|
+
// plugin would show 4. Before the detail view wrapped at all, the terminal
|
|
791
|
+
// soft-wrapped these in full, so truncating here would lose information the
|
|
792
|
+
// old output had.
|
|
793
|
+
// String(v) is belt-and-braces: pluginToItem now coerces every manifest field
|
|
794
|
+
// through manifestText, so nothing non-string should reach here. Kept because
|
|
795
|
+
// this is the choke point every future row kind flows through.
|
|
796
|
+
printWrappedJoined(` ${chalk.gray(k.padEnd(10))} `, String(v).split(', '), ', ');
|
|
752
797
|
}
|
|
753
798
|
if (others.length > 0) {
|
|
754
799
|
console.log('\n' + chalk.gray('Other matches:'));
|
|
@@ -863,21 +908,75 @@ function pluginItems() {
|
|
|
863
908
|
/**
|
|
864
909
|
* Map a discovered plugin to a resource item, surfacing the manifest description
|
|
865
910
|
* and the bundle's nested resources (skills, commands, hooks, ...) as detail rows.
|
|
911
|
+
*
|
|
912
|
+
* EVERY field read here comes from an uncontrolled `plugin.json`:
|
|
913
|
+
* `loadPluginManifest` casts parsed JSON straight to `PluginManifest` and
|
|
914
|
+
* validates only name/version (`lib/plugins.ts`), so the declared types are a
|
|
915
|
+
* hope, not a guarantee. A non-string reaching a renderer throws on `.split` /
|
|
916
|
+
* `.replace`, and `pluginToItem` runs while BUILDING THE LIST — so one malformed
|
|
917
|
+
* manifest anywhere takes down `inspect .`, `--plugins`, `--json`, and even a
|
|
918
|
+
* query for a different, valid plugin. Coerce every field through `manifestText`;
|
|
919
|
+
* never trust the annotation.
|
|
866
920
|
*/
|
|
867
921
|
function pluginToItem(plugin, source) {
|
|
868
922
|
const extra = [];
|
|
869
|
-
|
|
870
|
-
|
|
923
|
+
const version = manifestText(plugin.manifest.version);
|
|
924
|
+
if (version)
|
|
925
|
+
extra.push(['version', version]);
|
|
926
|
+
// Which execution surfaces the bundle actually carries. Detection already
|
|
927
|
+
// exists for the plugin picker; the detail view simply never asked for it.
|
|
928
|
+
const surfaces = pluginCapabilityLabels(inspectPluginCapabilities(plugin.root));
|
|
929
|
+
if (surfaces.length > 0)
|
|
930
|
+
extra.push(['surfaces', surfaces.join(', ')]);
|
|
931
|
+
const author = plugin.manifest.author;
|
|
932
|
+
const authorLabel = manifestText(typeof author === 'object' && author !== null ? author.name : author);
|
|
933
|
+
if (authorLabel)
|
|
934
|
+
extra.push(['author', authorLabel]);
|
|
935
|
+
// `.length` is truthy for a bare string too, and a string has no `.join`.
|
|
936
|
+
const deps = plugin.manifest.dependencies;
|
|
937
|
+
const depsLabel = Array.isArray(deps)
|
|
938
|
+
? deps.map(manifestText).filter(Boolean).join(', ')
|
|
939
|
+
: manifestText(deps);
|
|
940
|
+
if (depsLabel)
|
|
941
|
+
extra.push(['depends on', depsLabel]);
|
|
871
942
|
return {
|
|
872
943
|
name: plugin.name,
|
|
873
944
|
source,
|
|
874
945
|
path: plugin.root,
|
|
875
946
|
linkTarget: linkTarget(plugin.root),
|
|
876
|
-
|
|
947
|
+
// `?? ''` catches only null/undefined — a numeric or array description used
|
|
948
|
+
// to reach truncateToWidth/`.split` and kill the render for every plugin.
|
|
949
|
+
description: manifestText(plugin.manifest.description),
|
|
877
950
|
extra,
|
|
878
951
|
groups: pluginResourceGroups(plugin),
|
|
879
952
|
};
|
|
880
953
|
}
|
|
954
|
+
/**
|
|
955
|
+
* Render one uncontrolled manifest value as display text. Objects and arrays
|
|
956
|
+
* carry no sensible one-line form, so they become '' (the row is then dropped)
|
|
957
|
+
* rather than `[object Object]`; everything else stringifies.
|
|
958
|
+
*/
|
|
959
|
+
function manifestText(v) {
|
|
960
|
+
if (v === null || v === undefined)
|
|
961
|
+
return '';
|
|
962
|
+
if (typeof v === 'object')
|
|
963
|
+
return '';
|
|
964
|
+
return String(v);
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Render one uncontrolled value as a list of display strings. A scalar becomes a
|
|
968
|
+
* one-element list — the case `?? []` and `Array.isArray` both miss, and the one
|
|
969
|
+
* that threw on `.join`. Object entries drop out rather than becoming
|
|
970
|
+
* `[object Object]`.
|
|
971
|
+
*/
|
|
972
|
+
function manifestList(v) {
|
|
973
|
+
if (v === null || v === undefined)
|
|
974
|
+
return [];
|
|
975
|
+
if (Array.isArray(v))
|
|
976
|
+
return v.map(manifestText).filter(Boolean);
|
|
977
|
+
const single = manifestText(v);
|
|
978
|
+
return single ? [single] : [];
|
|
979
|
+
}
|
|
881
980
|
function entriesFromAgentResources(agent, versionHome, kind) {
|
|
882
981
|
const res = getAgentResources(agent, { home: versionHome });
|
|
883
982
|
const list = res[kind];
|
|
@@ -926,8 +1025,17 @@ function buildDetailRows(item, kind) {
|
|
|
926
1025
|
const rows = [];
|
|
927
1026
|
if (item.path && kind !== 'mcp') {
|
|
928
1027
|
const stat = safeStat(item.path);
|
|
929
|
-
|
|
930
|
-
|
|
1028
|
+
// A bundle reports its real recursive weight. `(bundle)` carried no
|
|
1029
|
+
// information, and pathSize/formatBytes already back the summary view.
|
|
1030
|
+
if (stat) {
|
|
1031
|
+
if (stat.isDirectory()) {
|
|
1032
|
+
const { bytes, files } = pathSize(item.path);
|
|
1033
|
+
rows.push(['size', `${formatBytes(bytes)} · ${files} files`]);
|
|
1034
|
+
}
|
|
1035
|
+
else {
|
|
1036
|
+
rows.push(['size', formatBytes(stat.size)]);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
931
1039
|
}
|
|
932
1040
|
// Kind-specific fields
|
|
933
1041
|
if (kind === 'skills' || kind === 'commands' || kind === 'subagents') {
|
|
@@ -937,12 +1045,18 @@ function buildDetailRows(item, kind) {
|
|
|
937
1045
|
if (typeof fm.description === 'string' && fm.description.trim() !== item.description.trim()) {
|
|
938
1046
|
rows.push(['description', truncate(fm.description, 120)]);
|
|
939
1047
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
if (
|
|
945
|
-
rows.push(['
|
|
1048
|
+
// Frontmatter is uncontrolled YAML too. These were type-guarded against a
|
|
1049
|
+
// crash but still rendered `[object Object]` for an entry that is a map,
|
|
1050
|
+
// and dropped a scalar `triggers: foo` entirely.
|
|
1051
|
+
const triggers = manifestList(fm.triggers).join(', ');
|
|
1052
|
+
if (triggers)
|
|
1053
|
+
rows.push(['triggers', triggers]);
|
|
1054
|
+
const model = manifestText(fm.model);
|
|
1055
|
+
if (model)
|
|
1056
|
+
rows.push(['model', model]);
|
|
1057
|
+
const tools = manifestList(fm.tools).join(', ');
|
|
1058
|
+
if (tools)
|
|
1059
|
+
rows.push(['tools', tools]);
|
|
946
1060
|
}
|
|
947
1061
|
}
|
|
948
1062
|
// Plugin bundles surface their nested resources (skills, commands, …) plus
|
|
@@ -966,11 +1080,14 @@ function abbrevSource(s) {
|
|
|
966
1080
|
* an optional cache tail. Plain text — the caller applies color.
|
|
967
1081
|
*/
|
|
968
1082
|
export function summarizeHook(hook) {
|
|
969
|
-
|
|
970
|
-
|
|
1083
|
+
// `hook` is an unvalidated YAML cast from agents.yaml, so `??` is not enough:
|
|
1084
|
+
// a scalar `events: PreToolUse` is neither null nor an array, and `.join` threw
|
|
1085
|
+
// — taking down bare `agents inspect <repo>`, and via the central manifest
|
|
1086
|
+
// `agents inspect <agent>` on every box. Same shape as the plugin.json bug.
|
|
1087
|
+
const events = manifestList(hook.events).join('/') || '(no event)';
|
|
1088
|
+
let matcher = manifestText(hook.matcher);
|
|
971
1089
|
if (!matcher && hook.matches?.tool_name) {
|
|
972
|
-
|
|
973
|
-
matcher = Array.isArray(tn) ? tn.join('|') : tn;
|
|
1090
|
+
matcher = manifestList(hook.matches.tool_name).join('|');
|
|
974
1091
|
}
|
|
975
1092
|
const head = matcher ? `${events}(${matcher})` : events;
|
|
976
1093
|
const parts = [head];
|
|
@@ -987,21 +1104,26 @@ export function summarizeHook(hook) {
|
|
|
987
1104
|
function summarizeMatches(m) {
|
|
988
1105
|
if (!m)
|
|
989
1106
|
return '';
|
|
1107
|
+
// Every predicate is raw YAML. `truncate` calls `.slice`, so a numeric
|
|
1108
|
+
// `prompt_contains: 12345` threw here just like the events case above.
|
|
990
1109
|
const bits = [];
|
|
991
1110
|
if (m.git_dirty)
|
|
992
1111
|
bits.push('git_dirty');
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
if (
|
|
998
|
-
bits.push(`
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
bits.push(`
|
|
1002
|
-
|
|
1003
|
-
if (
|
|
1004
|
-
bits.push(`
|
|
1112
|
+
const promptContains = manifestText(m.prompt_contains);
|
|
1113
|
+
if (promptContains)
|
|
1114
|
+
bits.push(`prompt~"${truncate(promptContains, 24)}"`);
|
|
1115
|
+
const promptMatches = manifestText(m.prompt_matches);
|
|
1116
|
+
if (promptMatches)
|
|
1117
|
+
bits.push(`prompt=/${truncate(promptMatches, 24)}/`);
|
|
1118
|
+
const argsMatch = manifestText(m.tool_args_match);
|
|
1119
|
+
if (argsMatch)
|
|
1120
|
+
bits.push(`args=/${truncate(argsMatch, 20)}/`);
|
|
1121
|
+
const cwd = manifestList(m.cwd_includes).join('|');
|
|
1122
|
+
if (cwd)
|
|
1123
|
+
bits.push(`cwd~${truncate(cwd, 24)}`);
|
|
1124
|
+
const projectHas = manifestText(m.project_has);
|
|
1125
|
+
if (projectHas)
|
|
1126
|
+
bits.push(`has ${projectHas}`);
|
|
1005
1127
|
return bits.join(' · ');
|
|
1006
1128
|
}
|
|
1007
1129
|
/** Normalize a hook cache shorthand/object to a display ttl ("5m", "1h"); null when uncached. */
|
|
@@ -1010,7 +1132,10 @@ function hookCacheTtl(cache) {
|
|
|
1010
1132
|
return null;
|
|
1011
1133
|
if (typeof cache === 'string')
|
|
1012
1134
|
return cache.replace(/-bg$/, '');
|
|
1013
|
-
|
|
1135
|
+
// A bare `cache: 5` has no `.ttl`, and `cache: {ttl: {…}}` has a non-scalar
|
|
1136
|
+
// one — String() on either rendered `(undefined cache)` / `([object Object]
|
|
1137
|
+
// cache)`. No ttl to show means no cache tail.
|
|
1138
|
+
return manifestText(cache.ttl) || null;
|
|
1014
1139
|
}
|
|
1015
1140
|
/** Compact one-liner for an MCP server: padded transport + the url (http) or command line (stdio). */
|
|
1016
1141
|
export function summarizeMcp(cfg) {
|
|
@@ -1121,6 +1246,11 @@ function loadCentralHookManifest() {
|
|
|
1121
1246
|
* data file collapsed into one entry, non-hook files like promptcuts.yaml or
|
|
1122
1247
|
* README.md filtered out) rather than a naive readdir, so names are clean and
|
|
1123
1248
|
* join cleanly against the manifest by script basename.
|
|
1249
|
+
*
|
|
1250
|
+
* `description` is intentionally blank: a hook is a script, and the only text a
|
|
1251
|
+
* readdir-based reader could scrape from one is its shebang (the old flat path
|
|
1252
|
+
* surfaced `!/usr/bin/env bash` as a description). The Hooks section shows firing
|
|
1253
|
+
* events via summarizeHook instead, which is the useful signal.
|
|
1124
1254
|
*/
|
|
1125
1255
|
function repoHookItems(repo) {
|
|
1126
1256
|
return listHookEntriesFromDir(path.join(repo.root, 'hooks')).map(h => ({
|
|
@@ -1177,7 +1307,10 @@ function readDescription(p) {
|
|
|
1177
1307
|
let filePath = p;
|
|
1178
1308
|
try {
|
|
1179
1309
|
if (fs.statSync(p).isDirectory()) {
|
|
1180
|
-
|
|
1310
|
+
// `rule.md` is the directory form of a subrule (SUBRULE_RULE_FILE in
|
|
1311
|
+
// lib/rules/compose.ts); without it the four dir-form subrules on disk
|
|
1312
|
+
// drill in with a blank preview.
|
|
1313
|
+
for (const marker of ['SKILL.md', 'WORKFLOW.md', 'AGENT.md', 'rule.md', 'README.md']) {
|
|
1181
1314
|
const c = path.join(p, marker);
|
|
1182
1315
|
if (fs.existsSync(c)) {
|
|
1183
1316
|
filePath = c;
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Cross-host is handled one layer up: `--host <h>` routes the whole command over
|
|
11
11
|
* ssh via `REMOTE_PASSTHROUGH` (see src/lib/hosts/passthrough.ts), so the box is
|
|
12
|
-
* written on the host that actually owns the agent.
|
|
12
|
+
* written on the host that actually owns the agent. A caller who doesn't already
|
|
13
|
+
* know the host (a detached `agents run --device <h> --no-follow` dispatch) is
|
|
14
|
+
* still resolved automatically: a `target` matching no local/cloud session falls
|
|
15
|
+
* through to the `~/.agents/.cache/hosts/` records `agents hosts ps` reads
|
|
16
|
+
* (RUSH-2366 follow-up — see decideHostTaskRoute in lib/mailbox-target.ts), and
|
|
17
|
+
* the message is rerouted there.
|
|
13
18
|
*
|
|
14
19
|
* For local agents, the message is tied to the agent's current open feed block
|
|
15
20
|
* (if any). The first answer to a block wins: a second concurrent answer is
|
package/dist/commands/message.js
CHANGED
|
@@ -7,7 +7,9 @@ import { getTaskById, updateTaskStatus } from '../lib/cloud/store.js';
|
|
|
7
7
|
import { resolveProvider } from '../lib/cloud/registry.js';
|
|
8
8
|
import { mailboxDir, enqueue } from '../lib/mailbox.js';
|
|
9
9
|
import { getAgentsInvocation } from '../lib/daemon.js';
|
|
10
|
-
import {
|
|
10
|
+
import { resolveTaskRef } from '../lib/hosts/tasks.js';
|
|
11
|
+
import { reconcileRunningTasks } from '../lib/hosts/reconcile.js';
|
|
12
|
+
import { resolveMessageTarget, mailboxIdForActiveSession, decideHostTaskRoute, } from '../lib/mailbox-target.js';
|
|
11
13
|
import { blockIdForSession, listBlocks, readBlock, recordAnswer, recordMessageReceipt, } from '../lib/feed.js';
|
|
12
14
|
import { verifyOperatorIdentity } from '../lib/operator.js';
|
|
13
15
|
import { resolveAnswerRoute, resumeArgv, } from '../lib/answer-router.js';
|
|
@@ -112,6 +114,40 @@ async function deliverViaResume(route, mailboxId) {
|
|
|
112
114
|
console.log(chalk.green(`Resumed ${mailboxId} with answer. `) +
|
|
113
115
|
chalk.dim(route.reason));
|
|
114
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Reroute a message to a detached `agents run --device <host> --no-follow`
|
|
119
|
+
* dispatch (RUSH-2366 follow-up). `getActiveSessions()` never sees these: the
|
|
120
|
+
* live process is on the dispatch's host, not this machine, so the local
|
|
121
|
+
* session resolver in `resolveMessageTarget` reports "no running agent" even
|
|
122
|
+
* while `agents hosts ps` shows the same dispatch running with a live remote
|
|
123
|
+
* pid — the only recovery was kill-and-redispatch, losing all context.
|
|
124
|
+
*
|
|
125
|
+
* Re-spawns `agents message <remoteRef> <text> --host <host>` through
|
|
126
|
+
* `getAgentsInvocation`, so it re-enters via the SAME `--host` REMOTE_PASSTHROUGH
|
|
127
|
+
* choke point (`lib/hosts/passthrough.ts`) any explicit `--host` caller uses,
|
|
128
|
+
* and resolves against the remote box's own active sessions.
|
|
129
|
+
*/
|
|
130
|
+
async function deliverViaHostReroute(route, text, opts) {
|
|
131
|
+
const argv = ['message', route.remoteRef, text, '--host', route.host];
|
|
132
|
+
if (opts.from)
|
|
133
|
+
argv.push('--from', opts.from);
|
|
134
|
+
if (opts.as)
|
|
135
|
+
argv.push('--as', opts.as);
|
|
136
|
+
if (opts.surface)
|
|
137
|
+
argv.push('--surface', opts.surface);
|
|
138
|
+
if (opts.ttl)
|
|
139
|
+
argv.push('--ttl', opts.ttl);
|
|
140
|
+
const inv = getAgentsInvocation(argv);
|
|
141
|
+
const child = spawn(inv.command, inv.args, { stdio: 'inherit', env: process.env });
|
|
142
|
+
const code = await new Promise((resolve) => {
|
|
143
|
+
child.on('exit', (c) => resolve(c ?? 1));
|
|
144
|
+
child.on('error', () => resolve(1));
|
|
145
|
+
});
|
|
146
|
+
if (code !== 0) {
|
|
147
|
+
die(`Delivery to '${route.remoteRef}' on host '${route.host}' exited with code ${code}. ` +
|
|
148
|
+
`Tried: agents ${argv.join(' ')}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
115
151
|
/**
|
|
116
152
|
* `message` is the agent-control plane (RUSH-2123): the answer/keystroke/
|
|
117
153
|
* injected input a running agent consumes, never a notification a human reads.
|
|
@@ -214,8 +250,29 @@ export function registerMessageCommand(program) {
|
|
|
214
250
|
die(`"${target}" matches ${res.candidates.length} running agents:\n${lines}\nRe-run with a full id.`);
|
|
215
251
|
return;
|
|
216
252
|
}
|
|
217
|
-
case 'none':
|
|
218
|
-
|
|
253
|
+
case 'none': {
|
|
254
|
+
// Not a local/cloud session — check whether it's a detached
|
|
255
|
+
// `--device ... --no-follow` dispatch, which `getActiveSessions()`
|
|
256
|
+
// never sees (its live process is on another host). Same records
|
|
257
|
+
// `agents hosts ps` reads, so the two commands never disagree.
|
|
258
|
+
// Heal first, exactly as `hosts stop`/`hosts ps` do: a detached
|
|
259
|
+
// dispatch record never self-updates, so a finished run is still
|
|
260
|
+
// stamped `status:'running'` on disk. Without this we'd route a dead
|
|
261
|
+
// task through an SSH reroute that can only fail, instead of
|
|
262
|
+
// reporting it finished here.
|
|
263
|
+
const onDisk = resolveTaskRef(target);
|
|
264
|
+
const hostRoute = decideHostTaskRoute(onDisk ? reconcileRunningTasks([onDisk])[0] : null, target);
|
|
265
|
+
if (hostRoute.kind === 'reroute') {
|
|
266
|
+
await deliverViaHostReroute(hostRoute, text, opts);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (hostRoute.kind === 'finished') {
|
|
270
|
+
die(`Task '${target}' on host '${hostRoute.host}' already ${hostRoute.status}` +
|
|
271
|
+
(hostRoute.exitCode !== undefined ? ` (exit ${hostRoute.exitCode})` : '') +
|
|
272
|
+
`. Nothing to message. View its output: \`agents hosts logs ${target}\`.`);
|
|
273
|
+
}
|
|
274
|
+
die(`No running agent or cloud task matches "${target}". List targets with \`agents sessions --active\` or \`agents hosts ps\`.`);
|
|
275
|
+
}
|
|
219
276
|
}
|
|
220
277
|
});
|
|
221
278
|
}
|
|
@@ -3,6 +3,7 @@ import { type ProjectDef } from '../lib/projects.js';
|
|
|
3
3
|
import type { SessionAgentId, SessionMeta, ViewMode } from '../lib/session/types.js';
|
|
4
4
|
import { type ActiveSession } from '../lib/session/active.js';
|
|
5
5
|
import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
|
|
6
|
+
import { type RunMeta } from '../lib/routines.js';
|
|
6
7
|
import { type PickedSession } from './sessions-picker.js';
|
|
7
8
|
import { type ToolSearchEnvelope, type ToolProgramCountEnvelope } from '../lib/session/tool-index.js';
|
|
8
9
|
interface SessionFilterOptions {
|
|
@@ -435,7 +436,60 @@ export interface RoutineChoice {
|
|
|
435
436
|
}
|
|
436
437
|
export declare function buildRoutineRunGroups(sessions: SessionMeta[]): RoutineRunGroup[];
|
|
437
438
|
export declare function buildRoutineChoices(sessions: SessionMeta[], runOnlyNames?: string[]): RoutineChoice[];
|
|
439
|
+
/**
|
|
440
|
+
* Resolve which routine the user is targeting for `--routine`. Returns `null` to
|
|
441
|
+
* ABORT the command (a bad `--routine <name>` printed an error, or the user
|
|
442
|
+
* cancelled the interactive pick). `{ name: null }` means "no routine was
|
|
443
|
+
* selected, don't filter" — the degenerate bare `--routine` piped to a
|
|
444
|
+
* non-interactive sink. `{ name }` is a chosen routine.
|
|
445
|
+
*
|
|
446
|
+
* The name universe is transcripts UNION every routine with a definition or a
|
|
447
|
+
* run record (`safeListJobsWithRuns`), so a command-only or never-ran routine —
|
|
448
|
+
* which produces no `SessionMeta` — still resolves and drills down.
|
|
449
|
+
*/
|
|
450
|
+
export declare function selectRoutineTarget(sessions: SessionMeta[], routine: boolean | string, interactive: boolean): Promise<{
|
|
451
|
+
name: string | null;
|
|
452
|
+
} | null>;
|
|
438
453
|
export declare function filterSessionsByRoutine(sessions: SessionMeta[], routine: boolean | string, interactive: boolean): Promise<SessionMeta[] | null>;
|
|
454
|
+
/** How a run executed — decides whether an agent session is even expected. */
|
|
455
|
+
export type RoutineExecutionKind = 'agent' | 'command' | 'workflow';
|
|
456
|
+
export declare function executionKind(meta: RunMeta): RoutineExecutionKind;
|
|
457
|
+
/** One canonical run record plus the indexed sessions linked to it by run id. */
|
|
458
|
+
export interface RoutineRunEntry {
|
|
459
|
+
meta: RunMeta;
|
|
460
|
+
sessions: SessionMeta[];
|
|
461
|
+
}
|
|
462
|
+
export interface RoutineDrilldown {
|
|
463
|
+
name: string;
|
|
464
|
+
/** Canonical run records (newest first), each with its linked sessions. */
|
|
465
|
+
runs: RoutineRunEntry[];
|
|
466
|
+
/** Sessions whose `routineRunId` matches no local run record — e.g. archived
|
|
467
|
+
* on another host whose run dir this box has not synced. Grouped by run id so
|
|
468
|
+
* they are still shown, honestly separated from the canonical run history. */
|
|
469
|
+
orphanSessions: RoutineRunGroup[];
|
|
470
|
+
/** Count of canonical run records (any status, incl. missed/blocked/skipped). */
|
|
471
|
+
runRecordCount: number;
|
|
472
|
+
/** Count of linked indexed sessions across all runs (+ orphans). */
|
|
473
|
+
linkedSessionCount: number;
|
|
474
|
+
/** True when any run ran an agent or any session is linked — i.e. an agent
|
|
475
|
+
* routine. False for a pure command routine, where no session is expected. */
|
|
476
|
+
isAgentRoutine: boolean;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Reconcile a routine's canonical run records (`listRuns`, local disk) with the
|
|
480
|
+
* indexed sessions for that routine (fleet-wide, keyed by `routineRunId`).
|
|
481
|
+
*/
|
|
482
|
+
export declare function buildRoutineDrilldown(name: string, sessions: SessionMeta[]): RoutineDrilldown;
|
|
483
|
+
/**
|
|
484
|
+
* Render the routine drilldown: canonical run history first, each run linked to
|
|
485
|
+
* its indexed agent session(s). This is the run/session seam made visible — a
|
|
486
|
+
* command routine shows its runs and states plainly that no session is expected;
|
|
487
|
+
* an agent routine shows the same runs plus each run's session metadata.
|
|
488
|
+
*/
|
|
489
|
+
export declare function printRoutineDrilldown(drill: RoutineDrilldown, liveIndex?: Map<string, ActiveSession>, opts?: {
|
|
490
|
+
hiddenCount?: number;
|
|
491
|
+
hiddenUnmanaged?: number;
|
|
492
|
+
}): void;
|
|
439
493
|
/** Resolve a session by id/query globally and print its compact preview (no pager).
|
|
440
494
|
* Backs `--preview` — the fast path for the "peek before resume" hot loop. */
|
|
441
495
|
export declare function renderSessionPreview(query: string, scope: {
|
|
@@ -527,10 +581,6 @@ export declare function buildOverviewGroups(pool: SessionMeta[], perProjectCap:
|
|
|
527
581
|
groups: OverviewGroup[];
|
|
528
582
|
projectCount: number;
|
|
529
583
|
};
|
|
530
|
-
export declare function printRoutineRunOverview(sessions: SessionMeta[], liveIndex: Map<string, ActiveSession> | undefined, opts?: {
|
|
531
|
-
hiddenCount?: number;
|
|
532
|
-
hiddenUnmanaged?: number;
|
|
533
|
-
}): void;
|
|
534
584
|
/**
|
|
535
585
|
* Render a resolved session to stdout — the non-follow view behind
|
|
536
586
|
* `agents logs <sessionId>`. Defaults to the concise `summary` digest (same as
|