@phnx-labs/agents-cli 1.20.91 → 1.20.93
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 +276 -0
- package/README.md +1 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +72 -6
- package/dist/commands/activity.js +198 -49
- package/dist/commands/beta.js +1 -0
- package/dist/commands/doctor.js +4 -2
- package/dist/commands/events.js +91 -1
- package/dist/commands/exec.d.ts +14 -0
- package/dist/commands/exec.js +144 -14
- package/dist/commands/projects.d.ts +22 -0
- package/dist/commands/projects.js +539 -0
- package/dist/commands/secrets.d.ts +17 -0
- package/dist/commands/secrets.js +198 -7
- package/dist/commands/send.d.ts +14 -12
- package/dist/commands/send.js +105 -35
- package/dist/commands/sessions-picker.d.ts +15 -0
- package/dist/commands/sessions-picker.js +37 -12
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +9 -1
- package/dist/commands/sessions.d.ts +10 -5
- package/dist/commands/sessions.js +65 -27
- package/dist/commands/sync.js +9 -3
- package/dist/commands/view.js +4 -0
- package/dist/index.js +18 -1
- package/dist/lib/activity.d.ts +77 -12
- package/dist/lib/activity.js +424 -74
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/channels/send.d.ts +83 -0
- package/dist/lib/channels/send.js +112 -0
- package/dist/lib/devices/registry.d.ts +14 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/events-ingest.d.ts +46 -0
- package/dist/lib/events-ingest.js +182 -0
- package/dist/lib/events.d.ts +15 -3
- package/dist/lib/events.js +55 -3
- package/dist/lib/feed-post.js +8 -2
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/linear-project-counts.d.ts +62 -0
- package/dist/lib/linear-project-counts.js +122 -0
- package/dist/lib/linear-projects.d.ts +50 -0
- package/dist/lib/linear-projects.js +114 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +14 -4
- package/dist/lib/menubar/install-menubar.js +20 -6
- package/dist/lib/menubar/notify-desktop.d.ts +17 -2
- package/dist/lib/menubar/notify-desktop.js +8 -2
- package/dist/lib/project-key.d.ts +44 -0
- package/dist/lib/project-key.js +79 -0
- package/dist/lib/project-probe.d.ts +75 -0
- package/dist/lib/project-probe.js +160 -0
- package/dist/lib/project-resources.d.ts +8 -0
- package/dist/lib/project-resources.js +31 -3
- package/dist/lib/project-root.js +16 -0
- package/dist/lib/project-status.d.ts +100 -0
- package/dist/lib/project-status.js +182 -0
- package/dist/lib/projects.d.ts +144 -0
- package/dist/lib/projects.js +313 -0
- package/dist/lib/remote-agents-json.d.ts +9 -0
- package/dist/lib/remote-agents-json.js +11 -5
- package/dist/lib/routine-notify.d.ts +11 -0
- package/dist/lib/routine-notify.js +22 -0
- package/dist/lib/run-notify.js +3 -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/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +53 -10
- package/dist/lib/secrets/list-filter.d.ts +20 -5
- package/dist/lib/secrets/list-filter.js +22 -6
- package/dist/lib/secrets/usage-db.d.ts +106 -0
- package/dist/lib/secrets/usage-db.js +236 -0
- package/dist/lib/session/bash-command.d.ts +53 -0
- package/dist/lib/session/bash-command.js +364 -0
- package/dist/lib/session/digest.d.ts +6 -0
- package/dist/lib/session/digest.js +19 -0
- package/dist/lib/session/relative-time.d.ts +23 -0
- package/dist/lib/session/relative-time.js +60 -8
- package/dist/lib/session/remote-active.d.ts +5 -1
- package/dist/lib/session/remote-active.js +4 -1
- package/dist/lib/session/remote-list.js +5 -2
- package/dist/lib/session/render.d.ts +2 -9
- package/dist/lib/session/render.js +25 -56
- package/dist/lib/sqlite.js +28 -1
- package/dist/lib/ssh-exec.d.ts +6 -0
- package/dist/lib/ssh-exec.js +10 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +14 -0
- package/dist/lib/state.js +19 -0
- package/dist/lib/terminal/backends/index.d.ts +10 -2
- package/dist/lib/terminal/backends/index.js +14 -2
- package/dist/lib/terminal/backends/terminal-app.d.ts +13 -0
- package/dist/lib/terminal/backends/terminal-app.js +73 -0
- package/dist/lib/terminal/index.d.ts +2 -1
- package/dist/lib/terminal/index.js +2 -1
- package/dist/lib/terminal/preferred.d.ts +89 -0
- package/dist/lib/terminal/preferred.js +87 -0
- package/dist/lib/terminal/run-surface.d.ts +82 -0
- package/dist/lib/terminal/run-surface.js +146 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/types.d.ts +6 -5
- package/dist/lib/versions.d.ts +6 -0
- package/dist/lib/versions.js +6 -4
- package/package.json +2 -1
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { collapseActivity, enrichActivityEvents, ensureActivityLogHook, filterActivityEvents,
|
|
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
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';
|
|
4
6
|
import { machineId, normalizeHost } from '../lib/machine-id.js';
|
|
5
7
|
import { shouldIncludeLocalFeed, remoteFeedHostsToDial } from './feed.js';
|
|
6
8
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
7
9
|
/** Recursion guard: a peer answering a fan-out never re-fans-out itself. */
|
|
8
10
|
export const ACTIVITY_NO_FANOUT_ENV = 'AGENTS_ACTIVITY_LOCAL';
|
|
11
|
+
/** `--group-by` values; `none` is the opt-out that `--flat` also selects. */
|
|
9
12
|
const GROUP_BY_VALUES = ['project', 'device', 'agent'];
|
|
10
|
-
|
|
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
|
+
*/
|
|
11
35
|
async function installActivityHooks() {
|
|
12
36
|
const warnings = [];
|
|
13
37
|
const hookInstall = ensureActivityLogHook();
|
|
@@ -20,22 +44,45 @@ async function installActivityHooks() {
|
|
|
20
44
|
import('../lib/versions.js'),
|
|
21
45
|
]);
|
|
22
46
|
const manifest = parseHookManifest({ warn: false });
|
|
47
|
+
const manifestNames = Object.keys(manifest);
|
|
23
48
|
for (const { agent, version } of iterHooksCapableVersions({ agent: 'claude' })) {
|
|
24
49
|
const result = registerHooksToSettings(agent, getVersionHomePath(agent, version), manifest);
|
|
25
|
-
|
|
26
|
-
|
|
50
|
+
const ours = result.errors.filter((e) => isActivityHookError(e) || !isManifestHookError(e, manifestNames));
|
|
51
|
+
if (ours.length > 0)
|
|
52
|
+
warnings.push(`${agent}@${version}: ${ours.join('; ')}`);
|
|
27
53
|
}
|
|
28
54
|
return warnings;
|
|
29
55
|
}
|
|
30
|
-
/**
|
|
31
|
-
|
|
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) {
|
|
32
79
|
return sessions.map((s) => ({
|
|
33
80
|
sessionId: s.sessionId,
|
|
34
81
|
ticket: s.ticket?.id,
|
|
35
82
|
// Where the process actually lives — the SSH-resolved provenance host, else
|
|
36
83
|
// the cross-machine `machine` id. Normalized to match the event `host` form.
|
|
37
84
|
executionHost: s.provenance?.host ? normalizeHost(s.provenance.host) : s.machine,
|
|
38
|
-
project:
|
|
85
|
+
project: resolveProject(s.cwd),
|
|
39
86
|
}));
|
|
40
87
|
}
|
|
41
88
|
/**
|
|
@@ -70,6 +117,23 @@ function renderFlat(events, opts) {
|
|
|
70
117
|
}
|
|
71
118
|
process.stdout.write('\n');
|
|
72
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
|
+
}
|
|
73
137
|
/** Print the bucketed view (`--group-by project|device|agent`). */
|
|
74
138
|
function renderGrouped(events, by, opts) {
|
|
75
139
|
const groups = groupActivity(events, by);
|
|
@@ -78,7 +142,11 @@ function renderGrouped(events, by, opts) {
|
|
|
78
142
|
const showProject = by !== 'project';
|
|
79
143
|
process.stdout.write(chalk.bold(`\n activity by ${by}\n`));
|
|
80
144
|
for (const group of groups) {
|
|
81
|
-
|
|
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`);
|
|
82
150
|
const { milestones, counts, subagentCount } = collapseActivity(group.events);
|
|
83
151
|
const shown = opts.all ? group.events : milestones;
|
|
84
152
|
for (const ev of shown) {
|
|
@@ -97,89 +165,168 @@ function renderGrouped(events, by, opts) {
|
|
|
97
165
|
}
|
|
98
166
|
process.stdout.write('\n');
|
|
99
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
|
+
}
|
|
100
207
|
export function registerActivityCommand(program) {
|
|
101
208
|
program
|
|
102
209
|
.command('activity')
|
|
103
|
-
.description('Recent agent activity -- plans, PRs, worktrees, sub-agents
|
|
210
|
+
.description('Recent agent activity across the fleet -- plans, PRs, worktrees, sub-agents, by project')
|
|
104
211
|
.option('--json', 'Emit the (enriched) event list as JSON')
|
|
105
212
|
.option('--all', 'Include routine activity (file edits) inline, not collapsed')
|
|
106
213
|
.option('--milestones', 'Only milestone events (plans, PRs, worktrees, sub-agents)')
|
|
107
|
-
.option('-n, --limit <n>', 'Cap the
|
|
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)
|
|
108
215
|
.option('--since <minutes>', 'Only events within the last N minutes', (v) => parseInt(v, 10))
|
|
109
216
|
.option('--local', 'Only this machine -- skip the cross-machine SSH fan-out')
|
|
110
217
|
.option('-H, --host <target...>', 'Scope to remote machine(s) over SSH; repeatable')
|
|
111
218
|
.option('--device <target...>', 'Alias for --host; repeatable')
|
|
112
|
-
.option('--devices-all', 'Fan out to every reachable device
|
|
219
|
+
.option('--devices-all', 'Fan out to every reachable device (the default; kept for scripts)')
|
|
113
220
|
.option('--hosts-all', 'Alias for --devices-all')
|
|
114
|
-
.option('--group-by <field>', 'Bucket the stream by project | device | agent')
|
|
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')
|
|
115
224
|
.option('--filter <text>', 'Narrow to items matching a project / device / agent / event / ticket')
|
|
116
225
|
.addHelpText('after', `
|
|
117
226
|
Examples:
|
|
118
|
-
agents activity #
|
|
119
|
-
agents activity --
|
|
227
|
+
agents activity # the whole fleet, by project
|
|
228
|
+
agents activity --local # just this machine
|
|
229
|
+
agents activity --flat # one newest-first stream
|
|
120
230
|
agents activity --host yosemite-s1 # one box over SSH
|
|
121
|
-
agents activity --
|
|
122
|
-
agents activity --
|
|
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
|
|
123
234
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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.
|
|
127
242
|
`)
|
|
128
243
|
.action(async (opts) => {
|
|
129
244
|
if (opts.device?.length)
|
|
130
245
|
opts.host = [...(opts.host ?? []), ...opts.device];
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
246
|
+
let groupBy;
|
|
247
|
+
try {
|
|
248
|
+
groupBy = resolveActivityGrouping(opts);
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
process.stderr.write(chalk.red(`${err.message}\n`));
|
|
134
252
|
process.exitCode = 1;
|
|
135
253
|
return;
|
|
136
254
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (wantRemote && explicitHosts && explicitHosts.length > 0) {
|
|
145
|
-
includeLocal = shouldIncludeLocalFeed(explicitHosts, self);
|
|
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;
|
|
146
262
|
}
|
|
263
|
+
const self = machineId();
|
|
264
|
+
const { includeLocal, wantRemote, remoteHosts } = resolveActivityScope(opts, self);
|
|
147
265
|
const warnings = includeLocal ? await installActivityHooks() : [];
|
|
148
266
|
const sinceMs = typeof opts.since === 'number' && opts.since > 0
|
|
149
267
|
? Date.now() - opts.since * 60_000
|
|
150
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.
|
|
151
273
|
let events = includeLocal
|
|
152
|
-
? readRecentActivity({ sinceMs, limit: opts.limit })
|
|
274
|
+
? readRecentActivity({ sinceMs, limit: opts.limit * LOCAL_READ_HEADROOM })
|
|
153
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 = [];
|
|
154
279
|
if (wantRemote) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
});
|
|
168
|
-
events = mergeActivityEvents(events, remote.items);
|
|
169
|
-
}
|
|
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;
|
|
170
292
|
}
|
|
171
293
|
// Enrich by joining to live LOCAL sessions (ticket/project/execution host).
|
|
172
294
|
// Remote events arrive already enriched by their own peer, so the join only
|
|
173
295
|
// fills local ones; the pure fallbacks (cwd->project, host->device) cover
|
|
174
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);
|
|
175
304
|
const sessions = includeLocal && events.length > 0 ? await getActiveSessions() : [];
|
|
176
|
-
let enriched = enrichActivityEvents(events, activityHintsFromSessions(sessions));
|
|
305
|
+
let enriched = enrichActivityEvents(events, activityHintsFromSessions(sessions, resolveProject), resolveProject, canonicalProject);
|
|
177
306
|
if (opts.milestones)
|
|
178
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
|
+
}
|
|
179
323
|
if (opts.filter)
|
|
180
324
|
enriched = filterActivityEvents(enriched, opts.filter);
|
|
181
|
-
enriched = enriched.
|
|
325
|
+
enriched = capActivityEvents(enriched, opts.limit, { all: opts.all || opts.milestones });
|
|
182
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));
|
|
183
330
|
process.stdout.write(`${JSON.stringify(enriched, null, 2)}\n`);
|
|
184
331
|
return;
|
|
185
332
|
}
|
|
@@ -187,11 +334,13 @@ Linear ticket) -- never by re-parsing transcripts. --devices-all runs the same
|
|
|
187
334
|
process.stderr.write(chalk.yellow(` ! ${w}\n`));
|
|
188
335
|
if (enriched.length === 0) {
|
|
189
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));
|
|
190
338
|
return;
|
|
191
339
|
}
|
|
192
340
|
if (groupBy)
|
|
193
341
|
renderGrouped(enriched, groupBy, opts);
|
|
194
342
|
else
|
|
195
343
|
renderFlat(enriched, opts);
|
|
344
|
+
process.stdout.write(formatUnreachableNote(unreachable));
|
|
196
345
|
});
|
|
197
346
|
}
|
package/dist/commands/beta.js
CHANGED
|
@@ -4,6 +4,7 @@ const BETA_DESCRIPTIONS = {
|
|
|
4
4
|
drive: 'Google Drive integration for reading and writing files',
|
|
5
5
|
factory: 'Cloud-based agent dispatch via Rush Factory',
|
|
6
6
|
'session-sync': 'Cross-machine session transcript sync via R2 (daemon push/pull)',
|
|
7
|
+
projects: 'Named multi-repo projects with a progress rollup (agents projects)',
|
|
7
8
|
};
|
|
8
9
|
function parseFeatures(values) {
|
|
9
10
|
const valid = new Set(ALL_BETA_FEATURES);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -12,7 +12,7 @@ import { getCliVersion } from '../lib/version.js';
|
|
|
12
12
|
import { resolveHost } from '../lib/hosts/registry.js';
|
|
13
13
|
import { sshExecAsync } from '../lib/ssh-exec.js';
|
|
14
14
|
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
15
|
-
import { machineId } from '../lib/session/sync/config.js';
|
|
15
|
+
import { machineId, normalizeHost } from '../lib/session/sync/config.js';
|
|
16
16
|
import chalk from 'chalk';
|
|
17
17
|
import { checkAllClis, collectTeamsDoctorData } from '../lib/teams/agents.js';
|
|
18
18
|
import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError, getAccountInfo } from '../lib/agents.js';
|
|
@@ -101,7 +101,9 @@ async function resolveFleetTargets(opts) {
|
|
|
101
101
|
const registry = await loadDevices();
|
|
102
102
|
const localName = machineId();
|
|
103
103
|
return Object.values(registry)
|
|
104
|
-
.
|
|
104
|
+
// Normalize names so zion/ZION/zion.local all match machineId() and we never
|
|
105
|
+
// self-SSH the local box during fleet probes (RUSH-2114).
|
|
106
|
+
.filter((d) => normalizeHost(d.name) !== localName)
|
|
105
107
|
// Control devices (a cockpit) never run agents — skip them in the fleet
|
|
106
108
|
// fan-out (an explicit --device <name> still resolves above).
|
|
107
109
|
.filter((d) => !isControlDevice(d))
|
package/dist/commands/events.js
CHANGED
|
@@ -17,6 +17,8 @@ import chalk from 'chalk';
|
|
|
17
17
|
import * as fs from 'fs';
|
|
18
18
|
import { getLogsPath } from '../lib/events.js';
|
|
19
19
|
import { readUnifiedEvents } from '../lib/event-stream.js';
|
|
20
|
+
import { ingestBatch } from '../lib/events-ingest.js';
|
|
21
|
+
import { setHelpSections } from '../lib/help.js';
|
|
20
22
|
/**
|
|
21
23
|
* Resolve `--limit` into a record cap. `0` means "no cap" — without it there is
|
|
22
24
|
* no way to read the whole stream, and any aggregation (group-by failure, count
|
|
@@ -97,8 +99,93 @@ function renderRow(r) {
|
|
|
97
99
|
const agent = r.agent ? chalk.gray(` ${r.agent}`) : '';
|
|
98
100
|
return `${time} ${originLabel(r).padEnd(24)} ${user.padEnd(22)} ${ev.padEnd(26)}${agent} ${detailFor(r)}`;
|
|
99
101
|
}
|
|
102
|
+
/** Read all of stdin. Returns '' when nothing is piped. */
|
|
103
|
+
async function readStdin() {
|
|
104
|
+
if (process.stdin.isTTY)
|
|
105
|
+
return '';
|
|
106
|
+
const chunks = [];
|
|
107
|
+
for await (const chunk of process.stdin)
|
|
108
|
+
chunks.push(Buffer.from(chunk));
|
|
109
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
110
|
+
}
|
|
111
|
+
function registerEmitSubcommand(events) {
|
|
112
|
+
const emitCmd = events
|
|
113
|
+
.command('emit')
|
|
114
|
+
.description('Record events produced outside this process (reads JSONL on stdin)')
|
|
115
|
+
.requiredOption('--source <name>', 'Producer name; stamped as `module` so `events --module <name>` filters to it')
|
|
116
|
+
.option('--dry-run', 'Validate and report without writing')
|
|
117
|
+
.option('--json', 'Report {written, rejected} as JSON')
|
|
118
|
+
.action(async (opts, cmd) => {
|
|
119
|
+
// The parent `events` command also declares `--json` (for its read view).
|
|
120
|
+
// Commander binds a flag declared on BOTH to the parent, so a bare
|
|
121
|
+
// `events emit … --json` lands on parent.opts().json and the child sees
|
|
122
|
+
// undefined — the human string would print where a caller expects JSON.
|
|
123
|
+
// Read both, same as `feed post`.
|
|
124
|
+
const wantJson = Boolean(opts?.json ?? cmd?.parent?.opts?.().json);
|
|
125
|
+
const input = await readStdin();
|
|
126
|
+
if (input.trim() === '') {
|
|
127
|
+
console.error('events emit: nothing on stdin — pipe one JSON object per line.');
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
let result;
|
|
132
|
+
try {
|
|
133
|
+
result = ingestBatch(input, { source: opts.source, dryRun: opts.dryRun });
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
console.error(`events emit: ${err instanceof Error ? err.message : String(err)}`);
|
|
137
|
+
process.exitCode = 1;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (wantJson) {
|
|
141
|
+
console.log(JSON.stringify({ written: result.written, rejected: result.rejected, routed: result.routed }, null, 2));
|
|
142
|
+
}
|
|
143
|
+
else if (result.rejected.length === 0) {
|
|
144
|
+
console.log(`${result.written} event(s) recorded${opts.dryRun ? ' (dry run)' : ''}.`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
console.log(`${result.written} event(s) recorded${opts.dryRun ? ' (dry run)' : ''}.`);
|
|
148
|
+
console.error(`rejected ${result.rejected.length} line(s):`);
|
|
149
|
+
for (const r of result.rejected)
|
|
150
|
+
console.error(` line ${r.line}: ${r.reason}`);
|
|
151
|
+
}
|
|
152
|
+
// Valid siblings are already written; a non-zero exit reports that the
|
|
153
|
+
// batch was not fully accepted without pretending nothing landed.
|
|
154
|
+
if (result.rejected.length > 0)
|
|
155
|
+
process.exitCode = 1;
|
|
156
|
+
});
|
|
157
|
+
setHelpSections(emitCmd, {
|
|
158
|
+
examples: `
|
|
159
|
+
# 1. Produce one JSON object per line (this is the whole input format).
|
|
160
|
+
printf '%s\\n' '{"event":"factory.command","commandId":"agents.newClaude"}' \\
|
|
161
|
+
| agents events emit --source factory
|
|
162
|
+
|
|
163
|
+
# 2. Flush a batch and check what landed.
|
|
164
|
+
agents events emit --source factory --json < batch.jsonl
|
|
165
|
+
|
|
166
|
+
# 3. Read it back. --limit 0 matters whenever you aggregate.
|
|
167
|
+
agents events --module factory --limit 0 --json
|
|
168
|
+
`,
|
|
169
|
+
notes: `
|
|
170
|
+
\`event\` is required and must be a known kind. \`ts\` (ISO-8601) is optional
|
|
171
|
+
and defaults to now — pass it so a batched producer records when each event
|
|
172
|
+
HAPPENED rather than when it flushed.
|
|
173
|
+
|
|
174
|
+
Envelope keys: ts, sessionId, mailboxId, terminalId, launchId, tmuxPane,
|
|
175
|
+
host, runtime, agent, tool, detail, url, project, cwd. Any other key is
|
|
176
|
+
payload and passes through the usual redaction.
|
|
177
|
+
|
|
178
|
+
A milestone kind (e.g. factory.launch) REQUIRES a sessionId — the activity
|
|
179
|
+
log is one file per session. Milestones route there; everything else goes
|
|
180
|
+
to the operational log. \`agents events\` reads both.
|
|
181
|
+
|
|
182
|
+
Rejection is per line: one bad line never discards the batch. Exit is 1 if
|
|
183
|
+
any line was rejected.
|
|
184
|
+
`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
100
187
|
export function registerEventsCommand(program) {
|
|
101
|
-
program
|
|
188
|
+
const events = program
|
|
102
189
|
.command('events')
|
|
103
190
|
.description('Read the unified event stream (operational + agent activity)')
|
|
104
191
|
.option('--module <name>', 'Only events from this group (e.g. teams, secrets, activity)')
|
|
@@ -166,6 +253,9 @@ Examples:
|
|
|
166
253
|
if (truncated)
|
|
167
254
|
console.log(chalk.yellow(capNote));
|
|
168
255
|
});
|
|
256
|
+
// `events` both reads (its own action, above) and writes (this subcommand) —
|
|
257
|
+
// the same shape as `feed` / `feed post`.
|
|
258
|
+
registerEmitSubcommand(events);
|
|
169
259
|
}
|
|
170
260
|
/** commander repeatable-option collector. */
|
|
171
261
|
function collect(value, previous) {
|
package/dist/commands/exec.d.ts
CHANGED
|
@@ -16,6 +16,20 @@ export interface RunAccountPickerRequest {
|
|
|
16
16
|
/** Distinguish a terminal account-picker marker from an explicit @version pin. */
|
|
17
17
|
export declare function parseRunAccountPickerRequest(agentSpec: string): RunAccountPickerRequest;
|
|
18
18
|
/** Return every option whose routing semantics conflict with a local account choice. */
|
|
19
|
+
/**
|
|
20
|
+
* The `--host` alias family — the flags that mean "dispatch this run to another
|
|
21
|
+
* machine over SSH". `--host` is canonical; `--device`/`--on`/`--computer` are
|
|
22
|
+
* aliases. Returns the values actually given (so callers can both test presence
|
|
23
|
+
* and read the target). Kept in ONE place because a guard that listed only a
|
|
24
|
+
* subset silently let `--terminal --device` open a local tab and drop the remote
|
|
25
|
+
* target — the drift this predicate exists to prevent.
|
|
26
|
+
*/
|
|
27
|
+
export declare function hostTargetGiven(options: {
|
|
28
|
+
host?: string;
|
|
29
|
+
device?: string;
|
|
30
|
+
on?: string;
|
|
31
|
+
computer?: string;
|
|
32
|
+
}): string[];
|
|
19
33
|
export declare function runAccountPickerConflicts(options: {
|
|
20
34
|
resume?: string | boolean;
|
|
21
35
|
strategy?: string;
|