@phnx-labs/agents-cli 1.22.24 → 1.22.25
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 +167 -1
- package/README.md +14 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.js +9 -5
- package/dist/commands/doctor.d.ts +24 -0
- package/dist/commands/doctor.js +100 -9
- package/dist/commands/exec.js +19 -16
- package/dist/commands/feed.d.ts +5 -0
- package/dist/commands/feed.js +21 -1
- package/dist/commands/focus.js +2 -2
- package/dist/commands/menubar.js +8 -0
- package/dist/commands/routines.d.ts +3 -0
- package/dist/commands/routines.js +70 -47
- package/dist/commands/run-cloud.js +1 -1
- package/dist/commands/sessions-browser.d.ts +1 -1
- package/dist/commands/sessions-browser.js +27 -7
- package/dist/commands/sessions-resume.js +3 -2
- package/dist/commands/sessions.d.ts +13 -1
- package/dist/commands/sessions.js +24 -2
- package/dist/commands/setup-watchdog.js +5 -10
- package/dist/commands/setup.js +1 -1
- package/dist/commands/teams.js +3 -2
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +26 -66
- package/dist/index.js +36 -1
- package/dist/lib/agents.js +126 -21
- package/dist/lib/cloud/cursor.d.ts +79 -0
- package/dist/lib/cloud/cursor.js +228 -0
- package/dist/lib/cloud/registry.js +2 -0
- package/dist/lib/cloud/types.d.ts +7 -2
- package/dist/lib/cloud/types.js +14 -0
- package/dist/lib/crabbox/cli.d.ts +2 -2
- package/dist/lib/crabbox/config.d.ts +7 -8
- package/dist/lib/crabbox/config.js +14 -14
- package/dist/lib/crabbox/lease.d.ts +11 -4
- package/dist/lib/crabbox/lease.js +40 -8
- package/dist/lib/crabbox/setup-copy.d.ts +5 -0
- package/dist/lib/crabbox/setup-copy.js +17 -1
- package/dist/lib/daemon.js +27 -1
- package/dist/lib/device-config.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +7 -1
- package/dist/lib/devices/doctor-findings.js +40 -1
- package/dist/lib/events.d.ts +9 -0
- package/dist/lib/events.js +58 -0
- package/dist/lib/exec.d.ts +3 -3
- package/dist/lib/exec.js +24 -10
- package/dist/lib/feed-outcome.d.ts +3 -0
- package/dist/lib/feed-outcome.js +18 -10
- package/dist/lib/feed.d.ts +4 -0
- package/dist/lib/hosts/passthrough.d.ts +21 -0
- package/dist/lib/hosts/passthrough.js +39 -12
- package/dist/lib/mcp.js +5 -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/menubar/snapshot.d.ts +15 -0
- package/dist/lib/menubar/snapshot.js +40 -0
- package/dist/lib/plugins.js +13 -1
- package/dist/lib/resources/mcp.js +3 -0
- package/dist/lib/routine-process-cleanup.d.ts +9 -0
- package/dist/lib/routine-process-cleanup.js +73 -0
- package/dist/lib/runner.js +5 -5
- 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/bundles.js +1 -20
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +13 -0
- package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
- package/dist/lib/secrets/rc-hygiene.js +14 -1
- package/dist/lib/session/active.d.ts +4 -0
- package/dist/lib/session/db.js +8 -2
- package/dist/lib/session/remote-list.d.ts +2 -0
- package/dist/lib/session/remote-list.js +1 -0
- package/dist/lib/session/session-cache.d.ts +4 -4
- package/dist/lib/session/session-cache.js +4 -4
- package/dist/lib/shims.js +21 -1
- package/dist/lib/signin-badge.js +2 -0
- package/dist/lib/startup/command-registry.d.ts +15 -0
- package/dist/lib/startup/command-registry.js +42 -0
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/versions.js +16 -1
- package/dist/lib/watchdog/service.d.ts +17 -0
- package/dist/lib/watchdog/service.js +39 -0
- package/package.json +1 -1
package/dist/commands/feed.js
CHANGED
|
@@ -16,6 +16,7 @@ import { notifyUrgentBlock } from '../lib/notify.js';
|
|
|
16
16
|
import { gcMailbox } from '../lib/mailbox-gc.js';
|
|
17
17
|
import { isValidMailboxId } from '../lib/mailbox.js';
|
|
18
18
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
19
|
+
import { backfillActiveRowsFromMeta } from './sessions.js';
|
|
19
20
|
import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
|
|
20
21
|
import { GLYPH, masthead } from '../lib/comms-render.js';
|
|
21
22
|
import { discoverSessions } from '../lib/session/discover.js';
|
|
@@ -154,7 +155,7 @@ export function prepareLocalFeedBlocks(localBlocks, opts) {
|
|
|
154
155
|
}
|
|
155
156
|
function renderBlock(b, localHost, indent = '') {
|
|
156
157
|
const host = b.host !== localHost ? chalk.yellow(` [${b.host}]`) : '';
|
|
157
|
-
const runtime = chalk.gray(b
|
|
158
|
+
const runtime = chalk.gray(formatFeedRuntime(b));
|
|
158
159
|
const age = chalk.gray(relTime(b.ts));
|
|
159
160
|
const cls = b.blockClass ? chalk.gray(`(${b.blockClass})`) : '';
|
|
160
161
|
const consequence = b.consequence && b.consequence !== 'normal' ? chalk.red(`[${b.consequence}]`) : '';
|
|
@@ -218,6 +219,21 @@ function renderBlock(b, localHost, indent = '') {
|
|
|
218
219
|
}
|
|
219
220
|
console.log();
|
|
220
221
|
}
|
|
222
|
+
/** Specific host app + routine provenance for one feed row. */
|
|
223
|
+
export function formatFeedRuntime(block) {
|
|
224
|
+
const host = {
|
|
225
|
+
ghostty: 'Ghostty',
|
|
226
|
+
iterm: 'iTerm',
|
|
227
|
+
terminal: 'terminal',
|
|
228
|
+
kitty: 'Kitty',
|
|
229
|
+
codium: 'VSCodium',
|
|
230
|
+
code: 'VS Code',
|
|
231
|
+
cursor: 'Cursor',
|
|
232
|
+
}[block.runtime.toLowerCase()] ?? block.runtime;
|
|
233
|
+
if (block.origin !== 'routine')
|
|
234
|
+
return host;
|
|
235
|
+
return `${host} · routine${block.routineName ? `:${block.routineName}` : ''}`;
|
|
236
|
+
}
|
|
221
237
|
/** Human summary line for one outcome rollup. */
|
|
222
238
|
export function formatOutcomeHeader(group) {
|
|
223
239
|
const { agents, open, answered, parked } = group.counts;
|
|
@@ -251,6 +267,9 @@ export function sessionHintsFromActive(sessions) {
|
|
|
251
267
|
prUrl: s.pr?.url,
|
|
252
268
|
worktreeSlug: s.worktree?.slug,
|
|
253
269
|
project: s.cwd ? projectKeyFromCwd(s.cwd) : undefined,
|
|
270
|
+
host: s.host,
|
|
271
|
+
origin: s.origin,
|
|
272
|
+
routineName: s.routineName,
|
|
254
273
|
}));
|
|
255
274
|
}
|
|
256
275
|
/**
|
|
@@ -489,6 +508,7 @@ export function registerFeedCommand(program) {
|
|
|
489
508
|
console.error(chalk.yellow('Feed: session index is locked; skipping local outcome enrichment'));
|
|
490
509
|
}
|
|
491
510
|
sessionMetas = loaded.metas;
|
|
511
|
+
backfillActiveRowsFromMeta(sessions, new Map(sessionMetas.map((meta) => [meta.id, meta])));
|
|
492
512
|
}
|
|
493
513
|
const localSignals = buildSessionSignals(sessions, sessionMetas);
|
|
494
514
|
if (opts.pause || opts.kill) {
|
package/dist/commands/focus.js
CHANGED
|
@@ -16,7 +16,7 @@ import fs from 'node:fs';
|
|
|
16
16
|
import chalk from 'chalk';
|
|
17
17
|
import { confirm } from '@inquirer/prompts';
|
|
18
18
|
import { gatherLiveTargets, pickLiveTarget, pickLiveTargets, jumpTo, probeAttachRail, refuseFallback } from './go.js';
|
|
19
|
-
import { buildSessionRecoveryCommand, filterSessionsByQuery, formatLiveStatusHeadline, formatPickerLabel, pickerColumnsFor, resumeSessionInPlace, resolveSessionMetadataValue, resolveSessionAgentName, requestedLiveStatuses, } from './sessions.js';
|
|
19
|
+
import { buildSessionRecoveryCommand, filterSessionsByQuery, formatLiveStatusHeadline, formatPickerLabel, isRunningLiveSession, pickerColumnsFor, resumeSessionInPlace, resolveSessionMetadataValue, resolveSessionAgentName, requestedLiveStatuses, } from './sessions.js';
|
|
20
20
|
import { resolveBackend, CONFIRM_THRESHOLD } from './sessions-resume.js';
|
|
21
21
|
import { runOnPeer } from '../lib/session/remote-list.js';
|
|
22
22
|
import { discoverSessions } from '../lib/session/discover.js';
|
|
@@ -276,7 +276,7 @@ export async function focusAction(id, opts) {
|
|
|
276
276
|
}
|
|
277
277
|
/** A retained pane is not attachable merely because tmux can still display it. */
|
|
278
278
|
export function isAttachableLiveSession(session) {
|
|
279
|
-
return session
|
|
279
|
+
return isRunningLiveSession(session);
|
|
280
280
|
}
|
|
281
281
|
const FOCUS_AGENT_SHORTHANDS = ['claude', 'codex', 'kimi', 'antigravity', 'grok', 'opencode'];
|
|
282
282
|
function focusOptionAgent(opts) {
|
package/dist/commands/menubar.js
CHANGED
|
@@ -137,6 +137,14 @@ export function registerMenubarCommands(program) {
|
|
|
137
137
|
again, run \`agents menubar disable\`.
|
|
138
138
|
`,
|
|
139
139
|
});
|
|
140
|
+
menubar
|
|
141
|
+
.command('snapshot', { hidden: true })
|
|
142
|
+
.description('Emit the consolidated AGI Menu polling snapshot.')
|
|
143
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
144
|
+
.action(async () => {
|
|
145
|
+
const { computeMenubarSnapshot } = await import('../lib/menubar/snapshot.js');
|
|
146
|
+
process.stdout.write(`${JSON.stringify(await computeMenubarSnapshot())}\n`);
|
|
147
|
+
});
|
|
140
148
|
menubar
|
|
141
149
|
.command('enable')
|
|
142
150
|
.description('Install and start AGI Menu (launches at login)')
|
|
@@ -46,5 +46,8 @@ export declare function groupRoutineJobsByDevice(jobs: JobConfig[], registry: De
|
|
|
46
46
|
*/
|
|
47
47
|
export declare function groupRoutineJobsByProject(jobs: JobConfig[], knownProjectNames: Set<string>): RoutineListGroup[];
|
|
48
48
|
export declare function buildRunsJson(runs: RunMeta[]): Record<string, unknown>[];
|
|
49
|
+
/** Build the exact structured routine rows shared by `routines list --json`
|
|
50
|
+
* and the one-process AGI Menu snapshot. */
|
|
51
|
+
export declare function buildRoutineListJson(): Record<string, unknown>[];
|
|
49
52
|
/** Register the `agents routines` command tree. */
|
|
50
53
|
export declare function registerRoutinesCommands(program: Command): void;
|
|
@@ -405,6 +405,72 @@ function runMetaJson(run) {
|
|
|
405
405
|
export function buildRunsJson(runs) {
|
|
406
406
|
return runs.map(runMetaJson);
|
|
407
407
|
}
|
|
408
|
+
/** Build the exact structured routine rows shared by `routines list --json`
|
|
409
|
+
* and the one-process AGI Menu snapshot. */
|
|
410
|
+
export function buildRoutineListJson() {
|
|
411
|
+
try {
|
|
412
|
+
monitorRunningJobs();
|
|
413
|
+
}
|
|
414
|
+
catch { /* best-effort orphan reap */ }
|
|
415
|
+
const jobs = listAllJobs(process.cwd());
|
|
416
|
+
if (jobs.length === 0)
|
|
417
|
+
return [];
|
|
418
|
+
const scheduler = new JobScheduler(async () => { });
|
|
419
|
+
scheduler.loadAll();
|
|
420
|
+
try {
|
|
421
|
+
const overdueSet = new Set();
|
|
422
|
+
try {
|
|
423
|
+
for (const job of detectOverdueJobs())
|
|
424
|
+
overdueSet.add(job.name);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// Best-effort indicator; never block the list on detection errors.
|
|
428
|
+
}
|
|
429
|
+
const now = new Date();
|
|
430
|
+
const knownProjectNames = new Set(listProjectDefs().map((project) => project.name));
|
|
431
|
+
return jobs.map((job) => {
|
|
432
|
+
const latestRun = localLatestRun(job);
|
|
433
|
+
const enabledDevices = devicesWithRoutineEnabled(job.name);
|
|
434
|
+
return {
|
|
435
|
+
name: job.name,
|
|
436
|
+
agent: job.agent ?? null,
|
|
437
|
+
workflow: job.workflow ?? null,
|
|
438
|
+
command: job.command ?? null,
|
|
439
|
+
repo: job.repo ?? null,
|
|
440
|
+
schedule: job.schedule ?? null,
|
|
441
|
+
scheduleHuman: fireConditionLabel(job),
|
|
442
|
+
trigger: job.trigger ?? null,
|
|
443
|
+
timezone: job.timezone ?? null,
|
|
444
|
+
devices: enabledDevices,
|
|
445
|
+
enabledDevices,
|
|
446
|
+
host: job.host ?? null,
|
|
447
|
+
hostStrategy: resolveHostStrategy(job),
|
|
448
|
+
source: job.source ?? null,
|
|
449
|
+
sourceRepo: job.source?.repo ?? job.repo ?? null,
|
|
450
|
+
sourceBranch: job.source?.branch ?? null,
|
|
451
|
+
runOnce: Boolean(job.runOnce),
|
|
452
|
+
catchup: job.catchup !== false,
|
|
453
|
+
oneShot: isOneShotRoutine(job),
|
|
454
|
+
expired: isPastOneShotRoutine(job, now),
|
|
455
|
+
runsHere: jobRunsOnThisDevice(job),
|
|
456
|
+
enabled: job.enabled,
|
|
457
|
+
overdue: overdueSet.has(job.name),
|
|
458
|
+
nextRun: nextRunForDisplay(job, scheduler)?.toISOString() ?? null,
|
|
459
|
+
nextRunHuman: nextRunLabel(job, scheduler, now),
|
|
460
|
+
lastStatus: latestRun?.status ?? null,
|
|
461
|
+
exitCode: latestRun?.exitCode ?? null,
|
|
462
|
+
failureReason: latestRun?.errorMessage ?? null,
|
|
463
|
+
lastRunStartedAt: latestRun?.startedAt ?? null,
|
|
464
|
+
lastRunCompletedAt: latestRun?.completedAt ?? null,
|
|
465
|
+
projects: job.projects ?? [],
|
|
466
|
+
projectGroup: computeProjectGroup(job.projects, knownProjectNames),
|
|
467
|
+
};
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
scheduler.stopAll();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
408
474
|
/** Detect Ctrl+C or premature stream close during an interactive prompt. */
|
|
409
475
|
function isPromptCancelled(err) {
|
|
410
476
|
return (err instanceof Error &&
|
|
@@ -554,6 +620,10 @@ export function registerRoutinesCommands(program) {
|
|
|
554
620
|
console.error(chalk.red(`Unsupported --group-by '${options.groupBy}'. Use: project (default) or device`));
|
|
555
621
|
process.exit(1);
|
|
556
622
|
}
|
|
623
|
+
if (options.json) {
|
|
624
|
+
process.stdout.write(JSON.stringify(buildRoutineListJson()) + '\n');
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
557
627
|
try {
|
|
558
628
|
monitorRunningJobs();
|
|
559
629
|
}
|
|
@@ -579,53 +649,6 @@ export function registerRoutinesCommands(program) {
|
|
|
579
649
|
catch {
|
|
580
650
|
// Best-effort indicator; never block the list on detection errors.
|
|
581
651
|
}
|
|
582
|
-
// Machine-readable path: same data the table renders, but structured.
|
|
583
|
-
// The menu bar helper relies on this so it never reimplements cron math.
|
|
584
|
-
if (options.json) {
|
|
585
|
-
const nowJson = new Date();
|
|
586
|
-
const knownProjectNames = new Set(listProjectDefs().map((p) => p.name));
|
|
587
|
-
const payload = jobs.map((job) => {
|
|
588
|
-
const latestRun = localLatestRun(job);
|
|
589
|
-
const enabledDevices = devicesWithRoutineEnabled(job.name);
|
|
590
|
-
return {
|
|
591
|
-
name: job.name,
|
|
592
|
-
agent: job.agent ?? null,
|
|
593
|
-
workflow: job.workflow ?? null,
|
|
594
|
-
command: job.command ?? null,
|
|
595
|
-
repo: job.repo ?? null,
|
|
596
|
-
schedule: job.schedule ?? null,
|
|
597
|
-
scheduleHuman: fireConditionLabel(job),
|
|
598
|
-
trigger: job.trigger ?? null,
|
|
599
|
-
timezone: job.timezone ?? null,
|
|
600
|
-
devices: enabledDevices,
|
|
601
|
-
enabledDevices,
|
|
602
|
-
host: job.host ?? null,
|
|
603
|
-
hostStrategy: resolveHostStrategy(job),
|
|
604
|
-
source: job.source ?? null,
|
|
605
|
-
sourceRepo: job.source?.repo ?? job.repo ?? null,
|
|
606
|
-
sourceBranch: job.source?.branch ?? null,
|
|
607
|
-
runOnce: Boolean(job.runOnce),
|
|
608
|
-
catchup: job.catchup !== false,
|
|
609
|
-
oneShot: isOneShotRoutine(job),
|
|
610
|
-
expired: isPastOneShotRoutine(job, nowJson),
|
|
611
|
-
runsHere: jobRunsOnThisDevice(job),
|
|
612
|
-
enabled: job.enabled,
|
|
613
|
-
overdue: overdueSet.has(job.name),
|
|
614
|
-
nextRun: nextRunForDisplay(job, scheduler)?.toISOString() ?? null,
|
|
615
|
-
nextRunHuman: nextRunLabel(job, scheduler, nowJson),
|
|
616
|
-
lastStatus: latestRun?.status ?? null,
|
|
617
|
-
exitCode: latestRun?.exitCode ?? null,
|
|
618
|
-
failureReason: latestRun?.errorMessage ?? null,
|
|
619
|
-
lastRunStartedAt: latestRun?.startedAt ?? null,
|
|
620
|
-
lastRunCompletedAt: latestRun?.completedAt ?? null,
|
|
621
|
-
projects: job.projects ?? [],
|
|
622
|
-
projectGroup: computeProjectGroup(job.projects, knownProjectNames),
|
|
623
|
-
};
|
|
624
|
-
});
|
|
625
|
-
scheduler.stopAll();
|
|
626
|
-
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
652
|
console.log(chalk.bold('Scheduled Jobs\n'));
|
|
630
653
|
// OSC 8 hyperlink helper — renders as a clickable link in supporting terminals.
|
|
631
654
|
// Guarded on process.stdout.isTTY so that piped/redirected output never
|
|
@@ -91,7 +91,7 @@ export function resolveRunCloudProvider(agentId, explicitProvider) {
|
|
|
91
91
|
return resolveProvider(explicitProvider);
|
|
92
92
|
if (!nativeProviderForAgent(agentId)) {
|
|
93
93
|
throw new RunCloudError(`${agentId} has no native cloud. Cloud-capable agents: ${cloudCapableAgentIds().join(', ')}. ` +
|
|
94
|
-
`Override with --provider <id> (rush | codex | factory | antigravity | host).`);
|
|
94
|
+
`Override with --provider <id> (rush | codex | cursor | factory | antigravity | host).`);
|
|
95
95
|
}
|
|
96
96
|
return resolveProvider(undefined, agentId);
|
|
97
97
|
}
|
|
@@ -147,7 +147,7 @@ export declare function liveSessionToMeta(a: ActiveSession, self: string): Sessi
|
|
|
147
147
|
* `--active --json` listed it. Live sessions the pool lacks are appended as their
|
|
148
148
|
* own rows, then the whole set is grouped local-machine-first.
|
|
149
149
|
*/
|
|
150
|
-
export declare function mergeLiveIntoPool(rows: SessionMeta[], live: Map<string, ActiveSession>, self: string): SessionMeta[];
|
|
150
|
+
export declare function mergeLiveIntoPool(rows: SessionMeta[], live: Map<string, ActiveSession>, self: string, includeUnindexed?: boolean): SessionMeta[];
|
|
151
151
|
/**
|
|
152
152
|
* Whether to render the host-program column. It belongs to the running view
|
|
153
153
|
* only, so this gates on the FILTER — not on the live index being populated.
|
|
@@ -22,7 +22,7 @@ import { enrichTeamOrigins, safeTeamText } from '../lib/session/team-filter.js';
|
|
|
22
22
|
import { listFavorites, toggleFavorite } from '../lib/session/favorites.js';
|
|
23
23
|
import { machineId, normalizeHost } from '../lib/session/sync/config.js';
|
|
24
24
|
import { buildPreview } from './sessions-picker.js';
|
|
25
|
-
import { formatPickerLabel, pickerColumnsFor, ticketLabel, mergeLocalFirst, gatherActiveSessions, liveHostLabel, LIVE_ROW_PREFIX, cleanPreview, handlePickedSession, shouldIncludeLocal, remoteHostsToDial, matchesTeam, formatLiveStatusHeadline, matchesLiveStatus, parseAgentFilter, } from './sessions.js';
|
|
25
|
+
import { formatPickerLabel, pickerColumnsFor, ticketLabel, mergeLocalFirst, gatherActiveSessions, liveHostLabel, LIVE_ROW_PREFIX, cleanPreview, handlePickedSession, shouldIncludeLocal, remoteHostsToDial, matchesTeam, formatLiveStatusHeadline, isRunningLiveSession, matchesLiveStatus, parseAgentFilter, } from './sessions.js';
|
|
26
26
|
/**
|
|
27
27
|
* Complete a seed into the filter the picker actually runs on.
|
|
28
28
|
*
|
|
@@ -73,6 +73,8 @@ function poolCacheKey(f) {
|
|
|
73
73
|
}
|
|
74
74
|
/** Pool size when a team filter is active; one team's rows can sit anywhere. */
|
|
75
75
|
const WHOLE_TEAM_POOL_LIMIT = 5000;
|
|
76
|
+
/** A cold peer may need to resolve an installed-version alias and scan its index. */
|
|
77
|
+
const BROWSER_PEER_TIMEOUT_MS = 30_000;
|
|
76
78
|
/** Ordered window cycle for the `W` key. `undefined` = all time. */
|
|
77
79
|
const WINDOW_CYCLE = [undefined, '1d', '7d', '30d'];
|
|
78
80
|
/** Return the next value in `[undefined, ...options]`, wrapping. */
|
|
@@ -309,7 +311,9 @@ async function fetchRawPool(f, self, local, hosts, fixedFilters = false) {
|
|
|
309
311
|
// --in-team itself, which a peer on an older build would reject as an
|
|
310
312
|
// unknown option and fail the whole fan-out.
|
|
311
313
|
const forwarded = remotePoolArgs(f, fixedFilters);
|
|
312
|
-
const remoteResult = await gatherRemoteList(forwarded, remoteHosts
|
|
314
|
+
const remoteResult = await gatherRemoteList(forwarded, remoteHosts, {
|
|
315
|
+
timeoutMs: BROWSER_PEER_TIMEOUT_MS,
|
|
316
|
+
});
|
|
313
317
|
unreachable = remoteResult.unreachable;
|
|
314
318
|
if (remoteResult.sessions.length > 0)
|
|
315
319
|
rows = mergeLocalFirst([...rows, ...remoteResult.sessions], self);
|
|
@@ -423,7 +427,12 @@ export function liveSessionToMeta(a, self) {
|
|
|
423
427
|
* `--active --json` listed it. Live sessions the pool lacks are appended as their
|
|
424
428
|
* own rows, then the whole set is grouped local-machine-first.
|
|
425
429
|
*/
|
|
426
|
-
export function mergeLiveIntoPool(rows, live, self) {
|
|
430
|
+
export function mergeLiveIntoPool(rows, live, self, includeUnindexed = true) {
|
|
431
|
+
// A latest/oldest selector is resolved by the peer's transcript query. A live
|
|
432
|
+
// row absent from that result has no proof it belongs to the resolved version,
|
|
433
|
+
// so callers using an alias fail closed instead of widening the selector.
|
|
434
|
+
if (!includeUnindexed)
|
|
435
|
+
return rows;
|
|
427
436
|
const known = new Set(rows.map((r) => r.id));
|
|
428
437
|
const extra = [];
|
|
429
438
|
for (const [key, a] of live) {
|
|
@@ -483,8 +492,15 @@ export function applyFilters(rows, live, f, self, favorites) {
|
|
|
483
492
|
const cwd = process.cwd();
|
|
484
493
|
out = out.filter((r) => !!r.cwd && (r.cwd === cwd || r.cwd.startsWith(cwd + '/')));
|
|
485
494
|
}
|
|
486
|
-
|
|
487
|
-
|
|
495
|
+
// The live registry retains dead rows so explicit --closed/--crashed filters
|
|
496
|
+
// can recover them. Bare --active means currently active; a lifecycle filter
|
|
497
|
+
// intentionally replaces that default predicate with its exact status union.
|
|
498
|
+
if (f.running && f.statuses.length === 0) {
|
|
499
|
+
out = out.filter((r) => {
|
|
500
|
+
const active = live.get(r.id);
|
|
501
|
+
return !!active && isRunningLiveSession(active);
|
|
502
|
+
});
|
|
503
|
+
}
|
|
488
504
|
if (f.statuses.length > 0) {
|
|
489
505
|
out = out.filter((r) => {
|
|
490
506
|
const active = live.get(r.id);
|
|
@@ -507,8 +523,9 @@ export async function collectSessionCandidates(initial, opts = {}) {
|
|
|
507
523
|
const { sessions } = await gatherActiveSessions({ local: opts.local ?? false, hosts });
|
|
508
524
|
liveById = indexLiveRows(sessions, self);
|
|
509
525
|
}
|
|
526
|
+
const includeUnindexedLive = !filter.agent?.match(/@(latest|oldest)$/);
|
|
510
527
|
const rows = filter.running || opts.includeLive
|
|
511
|
-
? mergeLiveIntoPool(pool.rows, liveById, self)
|
|
528
|
+
? mergeLiveIntoPool(pool.rows, liveById, self, includeUnindexedLive)
|
|
512
529
|
: pool.rows;
|
|
513
530
|
const sessions = applyFilters(rows, liveById, filter, self, listFavorites());
|
|
514
531
|
return { sessions, liveById, self, unreachable: pool.unreachable };
|
|
@@ -618,7 +635,10 @@ export async function runSessionBrowser(initial = {}, opts = {}) {
|
|
|
618
635
|
liveCache = live;
|
|
619
636
|
// Live sessions the transcript pool lacks become rows of their own, so the
|
|
620
637
|
// running view lists every active session, not just the ones already indexed.
|
|
621
|
-
const
|
|
638
|
+
const includeUnindexedLive = !f.agent?.match(/@(latest|oldest)$/);
|
|
639
|
+
const rows = f.running && live
|
|
640
|
+
? mergeLiveIntoPool(pool.rows, live, self, includeUnindexedLive)
|
|
641
|
+
: pool.rows;
|
|
622
642
|
agentsInPool = distinct(rows.map((r) => r.agent));
|
|
623
643
|
devicesInPool = distinct(rows.map((r) => r.machine ?? self));
|
|
624
644
|
// Both ends of the lineage seed the cycle: teams a row spawned, and teams a
|
|
@@ -14,7 +14,8 @@ import { discoverSessions } from '../lib/session/discover.js';
|
|
|
14
14
|
import { filterTeamSessions } from '../lib/session/team-filter.js';
|
|
15
15
|
import { multiItemPicker, itemPicker } from '../lib/picker.js';
|
|
16
16
|
import { buildPreview } from './sessions-picker.js';
|
|
17
|
-
import {
|
|
17
|
+
import { formatPickerLabel, pickerColumnsFor, buildSessionRecoveryCommand, resumeSessionInPlace, parseAgentFilter, } from './sessions.js';
|
|
18
|
+
import { sessionMatchesQuery } from './sessions-browser.js';
|
|
18
19
|
import { openSurfaces, availableBackends, detectCurrentBackend, currentContext, } from '../lib/terminal/index.js';
|
|
19
20
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
20
21
|
import { setHelpSections } from '../lib/help.js';
|
|
@@ -111,7 +112,7 @@ async function sessionsResumeAction(query, options) {
|
|
|
111
112
|
chosen = await multiItemPicker({
|
|
112
113
|
message: 'Select sessions to resume:',
|
|
113
114
|
items: sessions,
|
|
114
|
-
filter: (q) => (q.trim() ?
|
|
115
|
+
filter: (q) => (q.trim() ? sessions.filter((s) => sessionMatchesQuery(s, q)) : sessions),
|
|
115
116
|
labelFor: (s, q) => formatPickerLabel(s, q, cols),
|
|
116
117
|
keyFor: (s) => s.id,
|
|
117
118
|
buildPreview,
|
|
@@ -131,7 +131,7 @@ export declare function formatActiveRowDescription(s: ActiveSession): string;
|
|
|
131
131
|
*/
|
|
132
132
|
export declare function indexActiveBySessionId(active: ActiveSession[]): Map<string, ActiveSession>;
|
|
133
133
|
/** The SessionMeta fields the live-row backfill reads — the enrichment a running process cannot report. */
|
|
134
|
-
type BackfillMeta = Pick<SessionMeta, 'version' | 'timestamp' | 'label' | 'ticketId' | 'prUrl' | 'prNumber'>;
|
|
134
|
+
export type BackfillMeta = Pick<SessionMeta, 'version' | 'timestamp' | 'label' | 'ticketId' | 'prUrl' | 'prNumber' | 'origin' | 'routineName'>;
|
|
135
135
|
/**
|
|
136
136
|
* Backfill display-only fields onto live rows from the indexed SessionMeta, by
|
|
137
137
|
* full session id (RUSH-2205). A running process reports no agent version, and a
|
|
@@ -141,6 +141,8 @@ type BackfillMeta = Pick<SessionMeta, 'version' | 'timestamp' | 'label' | 'ticke
|
|
|
141
141
|
* the DB read that builds `metaById` is the caller's concern.
|
|
142
142
|
*/
|
|
143
143
|
export declare function backfillActiveRowsFromMeta(sessions: ActiveSession[], metaById: Map<string, BackfillMeta>): void;
|
|
144
|
+
/** Add indexed display metadata to active rows for non-renderer consumers. */
|
|
145
|
+
export declare function backfillActiveRowsFromIndex(sessions: ActiveSession[]): void;
|
|
144
146
|
/**
|
|
145
147
|
* The live decoration for a listing row: a status glyph and the latest-turn
|
|
146
148
|
* preview, when the session is still running. `●` running / `◐` waiting on the
|
|
@@ -184,6 +186,16 @@ export declare function liveStatusWord(a: ActiveSession | undefined): string;
|
|
|
184
186
|
* stays excluded rather than inventing a human who can answer.
|
|
185
187
|
*/
|
|
186
188
|
export declare function isAwaitingUser(s: ActiveSession): boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Whether a row belongs in an unqualified `--active` view.
|
|
191
|
+
*
|
|
192
|
+
* The live registry deliberately retains terminally-dead rows long enough for
|
|
193
|
+
* `--closed` / `--crashed` and recovery to find them. Presence in that registry
|
|
194
|
+
* therefore is not, by itself, evidence that a session is still active.
|
|
195
|
+
* Explicit lifecycle filters bypass this predicate and select those retained
|
|
196
|
+
* rows through {@link matchesLiveStatus} instead.
|
|
197
|
+
*/
|
|
198
|
+
export declare function isRunningLiveSession(s: ActiveSession): boolean;
|
|
187
199
|
/**
|
|
188
200
|
* The tracker/PR ref for a session's dedicated column: the ticket id when known,
|
|
189
201
|
* else `PR#<n>`, else empty. Pulled out of the trailing badge blob so refs align
|
|
@@ -411,6 +411,10 @@ export function backfillActiveRowsFromMeta(sessions, metaById) {
|
|
|
411
411
|
if (!Number.isNaN(ts))
|
|
412
412
|
s.startedAtMs = ts;
|
|
413
413
|
}
|
|
414
|
+
if (!s.origin && m.origin)
|
|
415
|
+
s.origin = m.origin;
|
|
416
|
+
if (!s.routineName && m.routineName)
|
|
417
|
+
s.routineName = m.routineName;
|
|
414
418
|
}
|
|
415
419
|
}
|
|
416
420
|
/**
|
|
@@ -435,6 +439,10 @@ function loadBackfillMetaFor(sessions) {
|
|
|
435
439
|
}
|
|
436
440
|
return byId;
|
|
437
441
|
}
|
|
442
|
+
/** Add indexed display metadata to active rows for non-renderer consumers. */
|
|
443
|
+
export function backfillActiveRowsFromIndex(sessions) {
|
|
444
|
+
backfillActiveRowsFromMeta(sessions, loadBackfillMetaFor(sessions));
|
|
445
|
+
}
|
|
438
446
|
/**
|
|
439
447
|
* The live decoration for a listing row: a status glyph and the latest-turn
|
|
440
448
|
* preview, when the session is still running. `●` running / `◐` waiting on the
|
|
@@ -527,6 +535,20 @@ export function isAwaitingUser(s) {
|
|
|
527
535
|
return false;
|
|
528
536
|
return s.status === 'input_required' || s.activity === 'waiting_input';
|
|
529
537
|
}
|
|
538
|
+
/**
|
|
539
|
+
* Whether a row belongs in an unqualified `--active` view.
|
|
540
|
+
*
|
|
541
|
+
* The live registry deliberately retains terminally-dead rows long enough for
|
|
542
|
+
* `--closed` / `--crashed` and recovery to find them. Presence in that registry
|
|
543
|
+
* therefore is not, by itself, evidence that a session is still active.
|
|
544
|
+
* Explicit lifecycle filters bypass this predicate and select those retained
|
|
545
|
+
* rows through {@link matchesLiveStatus} instead.
|
|
546
|
+
*/
|
|
547
|
+
export function isRunningLiveSession(s) {
|
|
548
|
+
if (s.status === 'closed' || s.status === 'crashed')
|
|
549
|
+
return false;
|
|
550
|
+
return s.pidAlive !== false;
|
|
551
|
+
}
|
|
530
552
|
/** Width of the live status column — `crashed` is the longest word it renders. */
|
|
531
553
|
const LIVE_STATUS_W = 8;
|
|
532
554
|
/** The colored, space-padded status cell for a listing row (empty when not live). */
|
|
@@ -1275,14 +1297,14 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
|
1275
1297
|
// gate: exit non-zero when the union contains a session awaiting the user.
|
|
1276
1298
|
const statusFiltered = opts.statuses?.length
|
|
1277
1299
|
? merged.filter((session) => opts.statuses.some((status) => matchesLiveStatus(session, status)))
|
|
1278
|
-
: merged;
|
|
1300
|
+
: merged.filter(isRunningLiveSession);
|
|
1279
1301
|
const sessions = statusFiltered;
|
|
1280
1302
|
// Backfill agent version + ticket/PR/label/created onto the live rows from the
|
|
1281
1303
|
// historical index (RUSH-2205) — a running process reports none of these, and
|
|
1282
1304
|
// an orphan row usually lacks them. Done before both the JSON and human paths
|
|
1283
1305
|
// so every consumer (incl. the SSH fan-out's remote --json) sees enriched rows;
|
|
1284
1306
|
// transcripts sync across the fleet, so a remote row resolves from the local DB.
|
|
1285
|
-
|
|
1307
|
+
backfillActiveRowsFromIndex(sessions);
|
|
1286
1308
|
if (asJson) {
|
|
1287
1309
|
// Resolve who is watching each local tmux pane before serializing: `viewingIn`
|
|
1288
1310
|
// is how a consumer distinguishes a session someone is looking at from one
|
|
@@ -3,17 +3,12 @@ import { spawnSync } from 'node:child_process';
|
|
|
3
3
|
import { getCliLaunch } from '../lib/cli-entry.js';
|
|
4
4
|
import { loadDevices } from '../lib/devices/registry.js';
|
|
5
5
|
import { machineId, normalizeHost } from '../lib/machine-id.js';
|
|
6
|
-
import {
|
|
7
|
-
import { readJob, setJobEnabled } from '../lib/routines.js';
|
|
6
|
+
import { getConfigValue, setConfigValue } from '../lib/device-config.js';
|
|
8
7
|
import { isPromptCancelled, requireInteractiveSelection } from './utils.js';
|
|
9
|
-
const ROUTINE = 'watchdog';
|
|
10
8
|
export async function runWatchdogSetupWizard() {
|
|
11
|
-
if (!readJob(ROUTINE)) {
|
|
12
|
-
throw new Error("Built-in routine 'watchdog' is missing. Run: agents repo pull system");
|
|
13
|
-
}
|
|
14
9
|
const registry = await loadDevices();
|
|
15
10
|
const devices = [...new Set([...Object.keys(registry), machineId()].map(normalizeHost))].sort();
|
|
16
|
-
const enabled = new Set(
|
|
11
|
+
const enabled = new Set(devices.filter((device) => getConfigValue('watchdog.enabled', { device }).value === true));
|
|
17
12
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
18
13
|
requireInteractiveSelection('watchdog devices', ['agents watchdog on', 'agents watchdog on --host <device>']);
|
|
19
14
|
}
|
|
@@ -27,10 +22,10 @@ export async function runWatchdogSetupWizard() {
|
|
|
27
22
|
for (const device of devices) {
|
|
28
23
|
const on = selectedSet.has(device);
|
|
29
24
|
if (device === normalizeHost(machineId())) {
|
|
30
|
-
|
|
25
|
+
setConfigValue('watchdog.enabled', on);
|
|
31
26
|
continue;
|
|
32
27
|
}
|
|
33
|
-
const launch = getCliLaunch(['
|
|
28
|
+
const launch = getCliLaunch(['watchdog', on ? 'on' : 'off', '--host', device]);
|
|
34
29
|
const result = spawnSync(launch.command, launch.args, { stdio: 'inherit', env: process.env });
|
|
35
30
|
if ((result.status ?? 1) !== 0)
|
|
36
31
|
throw new Error(`Could not configure watchdog on ${device}`);
|
|
@@ -49,6 +44,6 @@ export async function runWatchdogSetupWizard() {
|
|
|
49
44
|
}
|
|
50
45
|
export function registerSetupWatchdogCommand(setup) {
|
|
51
46
|
setup.command('watchdog')
|
|
52
|
-
.description('Choose the devices where the
|
|
47
|
+
.description('Choose the devices where the daemon watchdog pass runs.')
|
|
53
48
|
.action(runWatchdogSetupWizard);
|
|
54
49
|
}
|
package/dist/commands/setup.js
CHANGED
|
@@ -319,7 +319,7 @@ export function registerSetupCommand(program) {
|
|
|
319
319
|
agents setup share # provision or join a Cloudflare share endpoint
|
|
320
320
|
agents setup secrets # choose secrets backend/policy defaults + import
|
|
321
321
|
agents setup fleet # discover Tailscale devices + configure SSH access
|
|
322
|
-
agents setup watchdog # choose which devices run the watchdog
|
|
322
|
+
agents setup watchdog # choose which devices run the daemon watchdog pass
|
|
323
323
|
|
|
324
324
|
To install CLIs from agents.yaml and sync resources into version homes:
|
|
325
325
|
agents sync --local -y
|
package/dist/commands/teams.js
CHANGED
|
@@ -47,8 +47,9 @@ const AGENT_NAMES = {
|
|
|
47
47
|
antigravity: 'Antigravity',
|
|
48
48
|
kimi: 'Kimi',
|
|
49
49
|
droid: 'Droid',
|
|
50
|
+
warp: 'Warp',
|
|
50
51
|
};
|
|
51
|
-
const VALID_AGENTS = ['claude', 'codex', 'cursor', 'opencode', 'grok', 'antigravity', 'kimi', 'droid'];
|
|
52
|
+
const VALID_AGENTS = ['claude', 'codex', 'cursor', 'opencode', 'grok', 'antigravity', 'kimi', 'droid', 'warp'];
|
|
52
53
|
// 'full' kept as historical alias for 'skip'; normalized to 'skip' downstream.
|
|
53
54
|
const VALID_MODES = ['plan', 'edit', 'auto', 'skip', 'full'];
|
|
54
55
|
const VALID_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'auto'];
|
|
@@ -1279,7 +1280,7 @@ export function registerTeamsCommands(program) {
|
|
|
1279
1280
|
.alias('a')
|
|
1280
1281
|
.description("Add a teammate to work on a task. Runs in background; returns immediately. Use 'status' to check in.")
|
|
1281
1282
|
.option('-n, --name <name>', 'Friendly name for this teammate (e.g. alice). Required if using --after. Unique within team.')
|
|
1282
|
-
.option('-m, --mode <mode>', `Permissions: plan (read-only) | edit (can write files) | auto (smart classifier auto-approves safe ops) | skip (bypass all permission prompts). 'full' accepted as alias for skip. Teammates run headless: plan works headless on claude/codex/droid/opencode; kimi/grok/
|
|
1283
|
+
.option('-m, --mode <mode>', `Permissions: plan (read-only) | edit (can write files) | auto (smart classifier auto-approves safe ops) | skip (bypass all permission prompts). 'full' accepted as alias for skip. Teammates run headless: plan works headless on claude/codex/cursor/droid/opencode; kimi/grok/antigravity have no headless plan mode and auto-downgrade a plan request to auto.`, 'edit')
|
|
1283
1284
|
.option('-e, --effort <effort>', `Reasoning intensity: ${VALID_EFFORTS.join('|')}`, 'medium')
|
|
1284
1285
|
.option('--model <model>', 'Cost tier (cheap|default|best|ultra) or a concrete id (e.g. claude-opus-4-8); tiers resolve per harness+version to a supported model')
|
|
1285
1286
|
.option('--env <key=value>', 'Set an environment variable for this teammate (repeatable for multiple vars)', (val, prev) => [...prev, val], [])
|
|
@@ -10,12 +10,11 @@
|
|
|
10
10
|
* agents watchdog --nudge one tick, actually injects (explicit opt-in)
|
|
11
11
|
* agents watchdog --watch manual poll loop (dry unless --nudge)
|
|
12
12
|
* agents watchdog --json machine-readable tick output (for the menu-bar)
|
|
13
|
-
* agents watchdog on|off turn the
|
|
13
|
+
* agents watchdog on|off turn the device-local daemon pass on/off
|
|
14
14
|
* agents watchdog policy <id> <p> per-session policy: off | keep | handsoff
|
|
15
15
|
*
|
|
16
|
-
* The
|
|
17
|
-
*
|
|
18
|
-
* repo; on/off only changes this device's routine membership.
|
|
16
|
+
* The agents daemon is the sole automatic watchdog scheduler. The menu bar reads
|
|
17
|
+
* persisted state; it never runs a tick.
|
|
19
18
|
*/
|
|
20
19
|
import type { Command } from 'commander';
|
|
21
20
|
/** Register the `agents watchdog` command tree. */
|