@phnx-labs/agents-cli 1.22.6 → 1.22.8
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 +131 -1
- package/README.md +7 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +61 -0
- package/dist/commands/exec.js +83 -16
- package/dist/commands/feed.d.ts +2 -1
- package/dist/commands/feed.js +47 -20
- package/dist/commands/focus.js +22 -1
- package/dist/commands/harness.d.ts +0 -1
- package/dist/commands/harness.js +60 -4
- package/dist/commands/models.js +2 -2
- package/dist/commands/monitors.js +2 -2
- package/dist/commands/projects.js +122 -107
- package/dist/commands/routines.js +2 -2
- package/dist/commands/run-account-picker.js +2 -0
- package/dist/commands/secrets.js +1 -1
- package/dist/commands/sessions-backfill.d.ts +33 -0
- package/dist/commands/sessions-backfill.js +83 -1
- package/dist/commands/sessions-stats.d.ts +36 -0
- package/dist/commands/sessions-stats.js +263 -0
- package/dist/commands/sessions.d.ts +1 -1
- package/dist/commands/sessions.js +21 -1
- package/dist/commands/snapshot.d.ts +11 -0
- package/dist/commands/snapshot.js +107 -0
- package/dist/commands/teams.js +2 -1
- package/dist/commands/view.d.ts +9 -1
- package/dist/commands/view.js +63 -12
- package/dist/index.js +4 -2
- package/dist/lib/activity.js +2 -2
- package/dist/lib/agents.js +68 -11
- package/dist/lib/analytics/recipes.js +11 -5
- package/dist/lib/browser/ipc.js +2 -0
- package/dist/lib/browser/profiles.d.ts +15 -7
- package/dist/lib/browser/profiles.js +53 -12
- package/dist/lib/browser/remote-control.d.ts +35 -0
- package/dist/lib/browser/remote-control.js +48 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +19 -1
- package/dist/lib/browser/types.d.ts +14 -2
- package/dist/lib/byok-usage.d.ts +38 -0
- package/dist/lib/byok-usage.js +117 -0
- package/dist/lib/capabilities.js +1 -1
- package/dist/lib/device-config.js +8 -0
- package/dist/lib/exec.d.ts +20 -0
- package/dist/lib/exec.js +73 -8
- package/dist/lib/feed-outcome.d.ts +1 -0
- package/dist/lib/feed-outcome.js +2 -0
- package/dist/lib/feed-post.js +1 -1
- package/dist/lib/feed-ranking.d.ts +1 -0
- package/dist/lib/feed-ranking.js +4 -0
- package/dist/lib/feed.d.ts +4 -1
- package/dist/lib/feed.js +25 -0
- package/dist/lib/hosts/passthrough.d.ts +10 -1
- package/dist/lib/hosts/passthrough.js +23 -3
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/mcp.js +6 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/model-tiers.js +4 -1
- package/dist/lib/models.js +63 -0
- package/dist/lib/placement.d.ts +82 -0
- package/dist/lib/placement.js +188 -0
- package/dist/lib/profiles.d.ts +73 -10
- package/dist/lib/profiles.js +133 -15
- package/dist/lib/project-focus.d.ts +9 -0
- package/dist/lib/project-focus.js +23 -0
- package/dist/lib/project-key.d.ts +1 -1
- package/dist/lib/project-key.js +1 -1
- package/dist/lib/project-probe.d.ts +18 -0
- package/dist/lib/project-probe.js +46 -0
- package/dist/lib/project-status.d.ts +56 -0
- package/dist/lib/project-status.js +125 -18
- package/dist/lib/resources/mcp.js +3 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/rotate.d.ts +19 -4
- package/dist/lib/rotate.js +24 -1
- package/dist/lib/routines.d.ts +2 -0
- package/dist/lib/runner.d.ts +3 -0
- package/dist/lib/runner.js +90 -7
- 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/audit.js +1 -1
- package/dist/lib/secrets/index.d.ts +1 -0
- package/dist/lib/secrets/index.js +29 -15
- package/dist/lib/secrets/remote.d.ts +1 -1
- package/dist/lib/secrets/remote.js +1 -1
- package/dist/lib/session/active.d.ts +2 -0
- package/dist/lib/session/bash-command.d.ts +2 -3
- package/dist/lib/session/db.d.ts +92 -1
- package/dist/lib/session/db.js +230 -1
- package/dist/lib/session/digest.d.ts +1 -1
- package/dist/lib/session/digest.js +1 -1
- package/dist/lib/share/publish.js +24 -0
- package/dist/lib/snapshot.d.ts +103 -0
- package/dist/lib/snapshot.js +99 -0
- package/dist/lib/startup/command-registry.d.ts +1 -1
- package/dist/lib/startup/command-registry.js +2 -2
- package/dist/lib/subagents-registry.js +3 -0
- package/dist/lib/types.d.ts +11 -2
- package/dist/lib/usage.d.ts +5 -0
- package/dist/lib/usage.js +3 -3
- package/package.json +1 -1
- package/dist/commands/activity.d.ts +0 -87
- package/dist/commands/activity.js +0 -346
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.8",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `agents activity` -- the agent-activity lane: a running stream of what agents
|
|
3
|
-
* DID (plan created, PR opened, worktree created, sub-agent spawned, files
|
|
4
|
-
* edited), read from the append-only per-session activity logs.
|
|
5
|
-
*
|
|
6
|
-
* Complements `agents feed` (what agents are WAITING on). Events are emitted at
|
|
7
|
-
* hook time by 11-activity-log.py, so this reader never re-parses transcripts --
|
|
8
|
-
* it tails the logs and collapses routine work to counts, surfacing milestones
|
|
9
|
-
* individually and newest-first.
|
|
10
|
-
*
|
|
11
|
-
* Fleet-wide and project-grouped BY DEFAULT: the question this command answers
|
|
12
|
-
* is "what are my agents doing", and agents run on every box, so a local-only
|
|
13
|
-
* flat stream answered a narrower question than anyone asked. Each run fans the
|
|
14
|
-
* same `activity --json` payload out across the fleet the way `agents feed`
|
|
15
|
-
* does, merges every peer's stream host-tagged, joins each item to live sessions
|
|
16
|
-
* (project / ticket / execution host), and buckets it by project -- one level,
|
|
17
|
-
* no sub-grouping, with the machines named in each project's header.
|
|
18
|
-
*
|
|
19
|
-
* `--local` scopes back to this machine, `--host <box>` to named peers, and
|
|
20
|
-
* `--flat` (or `--group-by none`) restores the single newest-first stream.
|
|
21
|
-
*/
|
|
22
|
-
import type { Command } from 'commander';
|
|
23
|
-
import { type ActivityGroupBy } from '../lib/activity.js';
|
|
24
|
-
/** Recursion guard: a peer answering a fan-out never re-fans-out itself. */
|
|
25
|
-
export declare const ACTIVITY_NO_FANOUT_ENV = "AGENTS_ACTIVITY_LOCAL";
|
|
26
|
-
interface ActivityOpts {
|
|
27
|
-
json?: boolean;
|
|
28
|
-
all?: boolean;
|
|
29
|
-
milestones?: boolean;
|
|
30
|
-
limit: number;
|
|
31
|
-
since?: number;
|
|
32
|
-
local?: boolean;
|
|
33
|
-
host?: string[];
|
|
34
|
-
device?: string[];
|
|
35
|
-
devicesAll?: boolean;
|
|
36
|
-
hostsAll?: boolean;
|
|
37
|
-
groupBy?: string;
|
|
38
|
-
flat?: boolean;
|
|
39
|
-
project?: string;
|
|
40
|
-
filter?: string;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Does this hook-registration error concern one of the activity-log hooks?
|
|
44
|
-
* `registerHooksToSettings` prefixes each error with the manifest hook name
|
|
45
|
-
* (`<name>: script not found`), so the entry names are the discriminator.
|
|
46
|
-
*/
|
|
47
|
-
export declare function isActivityHookError(error: string): boolean;
|
|
48
|
-
/**
|
|
49
|
-
* Is this error attributable to a named manifest hook at all? Errors that name
|
|
50
|
-
* no hook — `Failed to parse settings.json`, `Failed to write
|
|
51
|
-
* agents-cli-hooks.ts: …` — are agent-level aborts, not per-hook noise, and
|
|
52
|
-
* must never be filtered out.
|
|
53
|
-
*/
|
|
54
|
-
export declare function isManifestHookError(error: string, manifestNames: string[]): boolean;
|
|
55
|
-
/**
|
|
56
|
-
* One compact trailing note for peers that didn't answer the fan-out. Now that
|
|
57
|
-
* every run dials the fleet, an offline box is routine -- but never silent: the
|
|
58
|
-
* reader has to know the timeline is missing a machine, not that the machine
|
|
59
|
-
* was idle. Empty string when everything answered.
|
|
60
|
-
*/
|
|
61
|
-
export declare function formatUnreachableNote(skipped: string[]): string;
|
|
62
|
-
/**
|
|
63
|
-
* Resolve `--group-by` / `--flat` into the grouping dimension, or `undefined`
|
|
64
|
-
* for a flat stream. Throws on an unknown value so the caller can exit 1 --
|
|
65
|
-
* a mistyped dimension must not silently fall back to the default.
|
|
66
|
-
*/
|
|
67
|
-
export declare function resolveActivityGrouping(opts: Pick<ActivityOpts, 'groupBy' | 'flat'>): ActivityGroupBy | undefined;
|
|
68
|
-
/** Which machines an invocation reads from. Pure -- no SSH, no device registry. */
|
|
69
|
-
export interface ActivityScope {
|
|
70
|
-
/** Read this box's own activity logs. */
|
|
71
|
-
includeLocal: boolean;
|
|
72
|
-
/** Dial peers over SSH. */
|
|
73
|
-
wantRemote: boolean;
|
|
74
|
-
/** Peers to dial; `undefined` means "every reachable device". */
|
|
75
|
-
remoteHosts?: string[];
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Decide the read scope. Fleet-wide is the DEFAULT -- agents run on every box,
|
|
79
|
-
* so "what are my agents doing" is a fleet question -- and three things narrow
|
|
80
|
-
* it: `--local`, an explicit `--host`/`--device` list, and the recursion guard
|
|
81
|
-
* a peer answering a fan-out carries (else every peer would re-fan the fleet).
|
|
82
|
-
* `--devices-all`/`--hosts-all` are kept as explicit no-ops for scripts that
|
|
83
|
-
* already pass them.
|
|
84
|
-
*/
|
|
85
|
-
export declare function resolveActivityScope(opts: Pick<ActivityOpts, 'local' | 'host'>, self: string, noFanoutEnv?: boolean): ActivityScope;
|
|
86
|
-
export declare function registerActivityCommand(program: Command): void;
|
|
87
|
-
export {};
|
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { ACTIVITY_HOOK_DEFINITIONS, collapseActivity, enrichActivityEvents, capActivityEvents, ensureActivityLogHook, filterActivityByProject, filterActivityEvents, formatActivityGroupMeta, formatEnrichedActivityLine, formatProgressUpdate, groupActivity, mergeActivityEvents, parseActivityPayload, readRecentActivity, styleForEvent, tierForEvent, } from '../lib/activity.js';
|
|
3
|
-
import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
|
|
4
|
-
import { resolveProjectKey } from '../lib/project-key.js';
|
|
5
|
-
import { listProjectDefs, projectNameForCwd, resolveProjectNameForCwd } from '../lib/projects.js';
|
|
6
|
-
import { machineId, normalizeHost } from '../lib/machine-id.js';
|
|
7
|
-
import { shouldIncludeLocalFeed, remoteFeedHostsToDial } from './feed.js';
|
|
8
|
-
import { getActiveSessions } from '../lib/session/active.js';
|
|
9
|
-
/** Recursion guard: a peer answering a fan-out never re-fans-out itself. */
|
|
10
|
-
export const ACTIVITY_NO_FANOUT_ENV = 'AGENTS_ACTIVITY_LOCAL';
|
|
11
|
-
/** `--group-by` values; `none` is the opt-out that `--flat` also selects. */
|
|
12
|
-
const GROUP_BY_VALUES = ['project', 'device', 'agent'];
|
|
13
|
-
const GROUP_BY_NONE = 'none';
|
|
14
|
-
/** Grouping when neither `--group-by` nor `--flat` is given. */
|
|
15
|
-
const DEFAULT_GROUP_BY = 'project';
|
|
16
|
-
/**
|
|
17
|
-
* How much further back than `--limit` the LOCAL log read goes, so routine
|
|
18
|
-
* churn on this box can't crowd the fleet's milestones out of the window
|
|
19
|
-
* ({@link capActivityEvents} does the real capping).
|
|
20
|
-
*/
|
|
21
|
-
const LOCAL_READ_HEADROOM = 5;
|
|
22
|
-
/**
|
|
23
|
-
* Wire the activity-log hooks into each hooks-capable version's settings.
|
|
24
|
-
*
|
|
25
|
-
* Only a failure that leaves THIS command's log unwritten is worth a warning.
|
|
26
|
-
* `registerHooksToSettings` reports every unresolved hook in the manifest — a
|
|
27
|
-
* missing `inject-session-id` script, someone else's half-installed plugin —
|
|
28
|
-
* and echoing all of them printed five wrapped lines of unrelated noise above
|
|
29
|
-
* the timeline on every run. Those belong to `agents doctor`, which exists to
|
|
30
|
-
* find them; here they are dropped. What still surfaces: an activity-hook
|
|
31
|
-
* failure, and any error attributable to NO manifest hook — a corrupt
|
|
32
|
-
* settings.json or an unwritable hooks file aborts registration for the whole
|
|
33
|
-
* agent, which is exactly a failure that leaves the activity log unwritten.
|
|
34
|
-
*/
|
|
35
|
-
async function installActivityHooks() {
|
|
36
|
-
const warnings = [];
|
|
37
|
-
const hookInstall = ensureActivityLogHook();
|
|
38
|
-
if (hookInstall.error) {
|
|
39
|
-
warnings.push(hookInstall.error);
|
|
40
|
-
return warnings;
|
|
41
|
-
}
|
|
42
|
-
const [{ iterHooksCapableVersions, parseHookManifest, registerHooksToSettings }, { getVersionHomePath }] = await Promise.all([
|
|
43
|
-
import('../lib/hooks.js'),
|
|
44
|
-
import('../lib/versions.js'),
|
|
45
|
-
]);
|
|
46
|
-
const manifest = parseHookManifest({ warn: false });
|
|
47
|
-
const manifestNames = Object.keys(manifest);
|
|
48
|
-
for (const { agent, version } of iterHooksCapableVersions({ agent: 'claude' })) {
|
|
49
|
-
const result = registerHooksToSettings(agent, getVersionHomePath(agent, version), manifest);
|
|
50
|
-
const ours = result.errors.filter((e) => isActivityHookError(e) || !isManifestHookError(e, manifestNames));
|
|
51
|
-
if (ours.length > 0)
|
|
52
|
-
warnings.push(`${agent}@${version}: ${ours.join('; ')}`);
|
|
53
|
-
}
|
|
54
|
-
return warnings;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Does this hook-registration error concern one of the activity-log hooks?
|
|
58
|
-
* `registerHooksToSettings` prefixes each error with the manifest hook name
|
|
59
|
-
* (`<name>: script not found`), so the entry names are the discriminator.
|
|
60
|
-
*/
|
|
61
|
-
export function isActivityHookError(error) {
|
|
62
|
-
return Object.keys(ACTIVITY_HOOK_DEFINITIONS).some((name) => error.startsWith(`${name}:`));
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Is this error attributable to a named manifest hook at all? Errors that name
|
|
66
|
-
* no hook — `Failed to parse settings.json`, `Failed to write
|
|
67
|
-
* agents-cli-hooks.ts: …` — are agent-level aborts, not per-hook noise, and
|
|
68
|
-
* must never be filtered out.
|
|
69
|
-
*/
|
|
70
|
-
export function isManifestHookError(error, manifestNames) {
|
|
71
|
-
return manifestNames.some((name) => error.startsWith(`${name}:`));
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Session facts (project / ticket / execution host) keyed for the read-time
|
|
75
|
-
* join. These sessions are this machine's, so their cwds get the canonical
|
|
76
|
-
* defined-project-first resolution rather than the bare cwd fold.
|
|
77
|
-
*/
|
|
78
|
-
function activityHintsFromSessions(sessions, resolveProject = resolveProjectKey) {
|
|
79
|
-
return sessions.map((s) => ({
|
|
80
|
-
sessionId: s.sessionId,
|
|
81
|
-
ticket: s.ticket?.id,
|
|
82
|
-
// Where the process actually lives — the SSH-resolved provenance host, else
|
|
83
|
-
// the cross-machine `machine` id. Normalized to match the event `host` form.
|
|
84
|
-
executionHost: s.provenance?.host ? normalizeHost(s.provenance.host) : s.machine,
|
|
85
|
-
project: resolveProject(s.cwd),
|
|
86
|
-
}));
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Render one enriched event. Deliberate progress posts (`status.posted`) use the
|
|
90
|
-
* rich multi-line {@link formatProgressUpdate} (joining the ticket the event's
|
|
91
|
-
* session carries); every other milestone keeps the compact enriched line.
|
|
92
|
-
*/
|
|
93
|
-
function renderEnrichedEntry(ev, opts = {}) {
|
|
94
|
-
if (ev.event === 'status.posted') {
|
|
95
|
-
const body = formatProgressUpdate(ev, { joined: { ticketId: ev.ticket } });
|
|
96
|
-
return opts.indent ? body.split('\n').map((l) => `${opts.indent}${l}`).join('\n') : body;
|
|
97
|
-
}
|
|
98
|
-
return formatEnrichedActivityLine(ev, opts);
|
|
99
|
-
}
|
|
100
|
-
/** Print one flat, newest-first stream (the default view). */
|
|
101
|
-
function renderFlat(events, opts) {
|
|
102
|
-
const { milestones, counts, subagentCount } = collapseActivity(events);
|
|
103
|
-
process.stdout.write(chalk.bold('\n recent activity\n'));
|
|
104
|
-
const shown = opts.all ? events : milestones;
|
|
105
|
-
for (const ev of shown) {
|
|
106
|
-
process.stdout.write(`${renderEnrichedEntry(ev, { showHost: true, showProject: true })}\n`);
|
|
107
|
-
}
|
|
108
|
-
if (!opts.all) {
|
|
109
|
-
const parts = Object.entries(counts)
|
|
110
|
-
.sort((a, b) => b[1] - a[1])
|
|
111
|
-
.map(([event, n]) => `${styleForEvent(event).label} ×${n}`);
|
|
112
|
-
if (parts.length > 0)
|
|
113
|
-
process.stdout.write(chalk.gray(`\n · ${parts.join(' ')}\n`));
|
|
114
|
-
}
|
|
115
|
-
if (subagentCount > 0) {
|
|
116
|
-
process.stdout.write(chalk.magenta(`\n ⑂ ${subagentCount} sub-agent${subagentCount === 1 ? '' : 's'} spawned\n`));
|
|
117
|
-
}
|
|
118
|
-
process.stdout.write('\n');
|
|
119
|
-
}
|
|
120
|
-
/** Max peers named in the unreachable note before the rest collapse to `+N`. */
|
|
121
|
-
const UNREACHABLE_NAME_LIMIT = 4;
|
|
122
|
-
/**
|
|
123
|
-
* One compact trailing note for peers that didn't answer the fan-out. Now that
|
|
124
|
-
* every run dials the fleet, an offline box is routine -- but never silent: the
|
|
125
|
-
* reader has to know the timeline is missing a machine, not that the machine
|
|
126
|
-
* was idle. Empty string when everything answered.
|
|
127
|
-
*/
|
|
128
|
-
export function formatUnreachableNote(skipped) {
|
|
129
|
-
if (skipped.length === 0)
|
|
130
|
-
return '';
|
|
131
|
-
const named = skipped.slice(0, UNREACHABLE_NAME_LIMIT);
|
|
132
|
-
const rest = skipped.length - named.length;
|
|
133
|
-
const list = rest > 0 ? `${named.join(', ')} +${rest}` : named.join(', ');
|
|
134
|
-
const noun = skipped.length === 1 ? 'device' : 'devices';
|
|
135
|
-
return chalk.gray(` · ${skipped.length} ${noun} unreachable: ${list}\n\n`);
|
|
136
|
-
}
|
|
137
|
-
/** Print the bucketed view (`--group-by project|device|agent`). */
|
|
138
|
-
function renderGrouped(events, by, opts) {
|
|
139
|
-
const groups = groupActivity(events, by);
|
|
140
|
-
// The grouping dimension is redundant on each line -- drop it from the tags.
|
|
141
|
-
const showHost = by !== 'device';
|
|
142
|
-
const showProject = by !== 'project';
|
|
143
|
-
process.stdout.write(chalk.bold(`\n activity by ${by}\n`));
|
|
144
|
-
for (const group of groups) {
|
|
145
|
-
// `▸ <label> <meta>` -- label leads in cyan so projects scan down the
|
|
146
|
-
// left edge (mirrors the `agents sessions` overview), the counts and the
|
|
147
|
-
// machines that touched this project trail behind it in gray.
|
|
148
|
-
const meta = formatActivityGroupMeta(group, { showDevices: by !== 'device' });
|
|
149
|
-
process.stdout.write(`\n ${chalk.cyan('▸')} ${chalk.cyan.bold(group.label)} ${chalk.gray(meta)}\n`);
|
|
150
|
-
const { milestones, counts, subagentCount } = collapseActivity(group.events);
|
|
151
|
-
const shown = opts.all ? group.events : milestones;
|
|
152
|
-
for (const ev of shown) {
|
|
153
|
-
process.stdout.write(`${renderEnrichedEntry(ev, { showHost, showProject, indent: ' ' })}\n`);
|
|
154
|
-
}
|
|
155
|
-
if (!opts.all) {
|
|
156
|
-
const parts = Object.entries(counts)
|
|
157
|
-
.sort((a, b) => b[1] - a[1])
|
|
158
|
-
.map(([event, n]) => `${styleForEvent(event).label} ×${n}`);
|
|
159
|
-
if (parts.length > 0)
|
|
160
|
-
process.stdout.write(chalk.gray(` · ${parts.join(' ')}\n`));
|
|
161
|
-
}
|
|
162
|
-
if (subagentCount > 0) {
|
|
163
|
-
process.stdout.write(chalk.magenta(` ⑂ ${subagentCount} sub-agent${subagentCount === 1 ? '' : 's'} spawned\n`));
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
process.stdout.write('\n');
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Resolve `--group-by` / `--flat` into the grouping dimension, or `undefined`
|
|
170
|
-
* for a flat stream. Throws on an unknown value so the caller can exit 1 --
|
|
171
|
-
* a mistyped dimension must not silently fall back to the default.
|
|
172
|
-
*/
|
|
173
|
-
export function resolveActivityGrouping(opts) {
|
|
174
|
-
if (opts.flat)
|
|
175
|
-
return undefined;
|
|
176
|
-
if (opts.groupBy === undefined)
|
|
177
|
-
return DEFAULT_GROUP_BY;
|
|
178
|
-
if (opts.groupBy === GROUP_BY_NONE)
|
|
179
|
-
return undefined;
|
|
180
|
-
if (!GROUP_BY_VALUES.includes(opts.groupBy)) {
|
|
181
|
-
throw new Error(`--group-by must be one of: ${[...GROUP_BY_VALUES, GROUP_BY_NONE].join(', ')}`);
|
|
182
|
-
}
|
|
183
|
-
return opts.groupBy;
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Decide the read scope. Fleet-wide is the DEFAULT -- agents run on every box,
|
|
187
|
-
* so "what are my agents doing" is a fleet question -- and three things narrow
|
|
188
|
-
* it: `--local`, an explicit `--host`/`--device` list, and the recursion guard
|
|
189
|
-
* a peer answering a fan-out carries (else every peer would re-fan the fleet).
|
|
190
|
-
* `--devices-all`/`--hosts-all` are kept as explicit no-ops for scripts that
|
|
191
|
-
* already pass them.
|
|
192
|
-
*/
|
|
193
|
-
export function resolveActivityScope(opts, self, noFanoutEnv = process.env[ACTIVITY_NO_FANOUT_ENV] === '1') {
|
|
194
|
-
if (opts.local === true || noFanoutEnv)
|
|
195
|
-
return { includeLocal: true, wantRemote: false };
|
|
196
|
-
const explicitHosts = opts.host;
|
|
197
|
-
if (explicitHosts && explicitHosts.length > 0) {
|
|
198
|
-
const remoteHosts = remoteFeedHostsToDial(explicitHosts, self) ?? [];
|
|
199
|
-
return {
|
|
200
|
-
includeLocal: shouldIncludeLocalFeed(explicitHosts, self),
|
|
201
|
-
wantRemote: remoteHosts.length > 0,
|
|
202
|
-
remoteHosts,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
return { includeLocal: true, wantRemote: true };
|
|
206
|
-
}
|
|
207
|
-
export function registerActivityCommand(program) {
|
|
208
|
-
program
|
|
209
|
-
.command('activity')
|
|
210
|
-
.description('Recent agent activity across the fleet -- plans, PRs, worktrees, sub-agents, by project')
|
|
211
|
-
.option('--json', 'Emit the (enriched) event list as JSON')
|
|
212
|
-
.option('--all', 'Include routine activity (file edits) inline, not collapsed')
|
|
213
|
-
.option('--milestones', 'Only milestone events (plans, PRs, worktrees, sub-agents)')
|
|
214
|
-
.option('-n, --limit <n>', 'Cap the milestones shown (routine work rides along as counts); with --all, caps every event', (v) => parseInt(v, 10), 40)
|
|
215
|
-
.option('--since <minutes>', 'Only events within the last N minutes', (v) => parseInt(v, 10))
|
|
216
|
-
.option('--local', 'Only this machine -- skip the cross-machine SSH fan-out')
|
|
217
|
-
.option('-H, --host <target...>', 'Scope to remote machine(s) over SSH; repeatable')
|
|
218
|
-
.option('--device <target...>', 'Alias for --host; repeatable')
|
|
219
|
-
.option('--devices-all', 'Fan out to every reachable device (the default; kept for scripts)')
|
|
220
|
-
.option('--hosts-all', 'Alias for --devices-all')
|
|
221
|
-
.option('--group-by <field>', 'Bucket the stream by project | device | agent | none (default: project)')
|
|
222
|
-
.option('--flat', 'One newest-first stream instead of project buckets')
|
|
223
|
-
.option('--project <name>', 'Only items under one project; exact match on the resolved (defined-project-first) label')
|
|
224
|
-
.option('--filter <text>', 'Narrow to items matching a project / device / agent / event / ticket')
|
|
225
|
-
.addHelpText('after', `
|
|
226
|
-
Examples:
|
|
227
|
-
agents activity # the whole fleet, by project
|
|
228
|
-
agents activity --local # just this machine
|
|
229
|
-
agents activity --flat # one newest-first stream
|
|
230
|
-
agents activity --host yosemite-s1 # one box over SSH
|
|
231
|
-
agents activity --project rush # one defined project, fleet-wide
|
|
232
|
-
agents activity --filter RUSH-2100 # one ticket, fleet-wide
|
|
233
|
-
agents activity --group-by device # by machine instead of project
|
|
234
|
-
|
|
235
|
-
Fleet-wide and project-grouped by default: the same 'activity --json' runs on
|
|
236
|
-
every reachable peer and the streams merge host-tagged. Each item is enriched by
|
|
237
|
-
JOINING to live sessions (project, execution host, Linear ticket) -- never by
|
|
238
|
-
re-parsing transcripts. Project labels are canonical: a cwd inside a defined
|
|
239
|
-
project (~/.agents/projects/<name>.yaml) reads as that project's name, so a
|
|
240
|
-
multi-repo project is one bucket; anything else folds to its repository. Each
|
|
241
|
-
project header names the machines its work ran on.
|
|
242
|
-
`)
|
|
243
|
-
.action(async (opts) => {
|
|
244
|
-
if (opts.device?.length)
|
|
245
|
-
opts.host = [...(opts.host ?? []), ...opts.device];
|
|
246
|
-
let groupBy;
|
|
247
|
-
try {
|
|
248
|
-
groupBy = resolveActivityGrouping(opts);
|
|
249
|
-
}
|
|
250
|
-
catch (err) {
|
|
251
|
-
process.stderr.write(chalk.red(`${err.message}\n`));
|
|
252
|
-
process.exitCode = 1;
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
// `parseInt` yields NaN for a non-numeric -n, and every downstream slice
|
|
256
|
-
// against NaN is empty — an unreadable flag would otherwise render as
|
|
257
|
-
// "No recent agent activity", which is a lie about the fleet.
|
|
258
|
-
if (!Number.isFinite(opts.limit) || opts.limit <= 0) {
|
|
259
|
-
process.stderr.write(chalk.red('--limit must be a positive number\n'));
|
|
260
|
-
process.exitCode = 1;
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
const self = machineId();
|
|
264
|
-
const { includeLocal, wantRemote, remoteHosts } = resolveActivityScope(opts, self);
|
|
265
|
-
const warnings = includeLocal ? await installActivityHooks() : [];
|
|
266
|
-
const sinceMs = typeof opts.since === 'number' && opts.since > 0
|
|
267
|
-
? Date.now() - opts.since * 60_000
|
|
268
|
-
: undefined;
|
|
269
|
-
// Each peer contributes its own newest `-n` window, so the merged pool is
|
|
270
|
-
// already several times the cap. The local read is the one source that
|
|
271
|
-
// would otherwise be capped twice, and reading further back costs nothing
|
|
272
|
-
// (the files are tailed either way) — so it gets the headroom.
|
|
273
|
-
let events = includeLocal
|
|
274
|
-
? readRecentActivity({ sinceMs, limit: opts.limit * LOCAL_READ_HEADROOM })
|
|
275
|
-
: [];
|
|
276
|
-
// Peers that were dialed but never answered, reported once at the end
|
|
277
|
-
// rather than as a line each above the timeline.
|
|
278
|
-
let unreachable = [];
|
|
279
|
-
if (wantRemote) {
|
|
280
|
-
const remoteArgs = ['activity', '--json', '-n', String(opts.limit)];
|
|
281
|
-
if (typeof opts.since === 'number' && opts.since > 0)
|
|
282
|
-
remoteArgs.push('--since', String(opts.since));
|
|
283
|
-
const remote = await gatherRemoteAgentsJson({
|
|
284
|
-
args: remoteArgs,
|
|
285
|
-
noFanoutEnv: ACTIVITY_NO_FANOUT_ENV,
|
|
286
|
-
hosts: remoteHosts?.length ? remoteHosts : undefined,
|
|
287
|
-
parse: parseActivityPayload,
|
|
288
|
-
quiet: true,
|
|
289
|
-
});
|
|
290
|
-
events = mergeActivityEvents(events, remote.items);
|
|
291
|
-
unreachable = remote.skipped;
|
|
292
|
-
}
|
|
293
|
-
// Enrich by joining to live LOCAL sessions (ticket/project/execution host).
|
|
294
|
-
// Remote events arrive already enriched by their own peer, so the join only
|
|
295
|
-
// fills local ones; the pure fallbacks (cwd->project, host->device) cover
|
|
296
|
-
// everything either way. Skip the ps/lsof scan when there's nothing to show.
|
|
297
|
-
// Project labels are canonical: a defined project's name wins over the
|
|
298
|
-
// repo-level key — including over a stale or def-skewed pre-baked stamp
|
|
299
|
-
// (the def match is a pure prefix compare; foreign paths just don't match),
|
|
300
|
-
// so a multi-repo project buckets as one even across version skew.
|
|
301
|
-
const projectDefs = listProjectDefs();
|
|
302
|
-
const canonicalProject = (cwd) => projectNameForCwd(cwd ?? undefined, projectDefs);
|
|
303
|
-
const resolveProject = (cwd) => resolveProjectNameForCwd(cwd, projectDefs);
|
|
304
|
-
const sessions = includeLocal && events.length > 0 ? await getActiveSessions() : [];
|
|
305
|
-
let enriched = enrichActivityEvents(events, activityHintsFromSessions(sessions, resolveProject), resolveProject, canonicalProject);
|
|
306
|
-
if (opts.milestones)
|
|
307
|
-
enriched = enriched.filter((e) => tierForEvent(e.event) === 'milestone');
|
|
308
|
-
if (opts.project) {
|
|
309
|
-
const unfiltered = enriched;
|
|
310
|
-
enriched = filterActivityByProject(enriched, opts.project);
|
|
311
|
-
// An exact-name flag that matches nothing deserves a useful answer, not
|
|
312
|
-
// a generic "no activity" that lies about the fleet.
|
|
313
|
-
if (enriched.length === 0 && unfiltered.length > 0) {
|
|
314
|
-
const seen = [...new Set(unfiltered.map((e) => e.project).filter((p) => !!p))].sort();
|
|
315
|
-
const defined = projectDefs.map((d) => d.name);
|
|
316
|
-
const parts = [
|
|
317
|
-
seen.length ? `seen: ${seen.join(', ')}` : '',
|
|
318
|
-
defined.length ? `defined: ${defined.join(', ')}` : '',
|
|
319
|
-
].filter(Boolean);
|
|
320
|
-
process.stderr.write(chalk.gray(` · no events for project "${opts.project}" (exact match)${parts.length ? ` — ${parts.join(' · ')}` : ''}\n`));
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
if (opts.filter)
|
|
324
|
-
enriched = filterActivityEvents(enriched, opts.filter);
|
|
325
|
-
enriched = capActivityEvents(enriched, opts.limit, { all: opts.all || opts.milestones });
|
|
326
|
-
if (opts.json) {
|
|
327
|
-
// stdout stays clean JSON, but an unanswered peer is never a silent drop.
|
|
328
|
-
if (unreachable.length > 0)
|
|
329
|
-
process.stderr.write(formatUnreachableNote(unreachable));
|
|
330
|
-
process.stdout.write(`${JSON.stringify(enriched, null, 2)}\n`);
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
for (const w of warnings)
|
|
334
|
-
process.stderr.write(chalk.yellow(` ! ${w}\n`));
|
|
335
|
-
if (enriched.length === 0) {
|
|
336
|
-
process.stdout.write(chalk.gray('No recent agent activity. Events appear here as agents create plans, open PRs, and spawn sub-agents.\n'));
|
|
337
|
-
process.stdout.write(formatUnreachableNote(unreachable));
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
if (groupBy)
|
|
341
|
-
renderGrouped(enriched, groupBy, opts);
|
|
342
|
-
else
|
|
343
|
-
renderFlat(enriched, opts);
|
|
344
|
-
process.stdout.write(formatUnreachableNote(unreachable));
|
|
345
|
-
});
|
|
346
|
-
}
|