@phnx-labs/agents-cli 1.22.24 → 1.22.26
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 +350 -1
- package/README.md +31 -6
- package/dist/bin/agents +0 -0
- package/dist/browser.js +14 -4
- package/dist/commands/apply.js +52 -8
- package/dist/commands/browser.js +35 -0
- package/dist/commands/cloud.js +9 -5
- package/dist/commands/doctor.d.ts +24 -0
- package/dist/commands/doctor.js +108 -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/insights.d.ts +25 -19
- package/dist/commands/insights.js +107 -33
- package/dist/commands/menubar.js +8 -0
- package/dist/commands/reconnect.d.ts +46 -0
- package/dist/commands/reconnect.js +109 -0
- package/dist/commands/routines.d.ts +3 -0
- package/dist/commands/routines.js +72 -49
- package/dist/commands/run-cloud.js +1 -1
- package/dist/commands/secrets.d.ts +2 -8
- package/dist/commands/secrets.js +29 -105
- 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 +28 -2
- package/dist/commands/setup-secrets.d.ts +1 -0
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup-watchdog.js +5 -10
- package/dist/commands/setup.d.ts +26 -3
- package/dist/commands/setup.js +106 -47
- package/dist/commands/teams.d.ts +6 -0
- package/dist/commands/teams.js +46 -2
- package/dist/commands/trends.d.ts +8 -0
- package/dist/commands/trends.js +10 -156
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +26 -66
- package/dist/index.js +37 -2
- package/dist/lib/agents.d.ts +11 -0
- package/dist/lib/agents.js +155 -23
- package/dist/lib/analytics/dashboard.d.ts +10 -6
- package/dist/lib/analytics/dashboard.js +6 -4
- package/dist/lib/analytics/mix-commands.d.ts +53 -0
- package/dist/lib/analytics/mix-commands.js +229 -0
- package/dist/lib/analytics/recipes.d.ts +19 -14
- package/dist/lib/analytics/recipes.js +4 -2
- package/dist/lib/browser/ipc.d.ts +26 -0
- package/dist/lib/browser/ipc.js +139 -24
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +1 -1
- package/dist/lib/browser/stream.d.ts +14 -0
- package/dist/lib/browser/stream.js +71 -0
- package/dist/lib/channels/owner-sink.d.ts +27 -0
- package/dist/lib/channels/owner-sink.js +93 -0
- 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 +13 -1
- package/dist/lib/devices/doctor-findings.js +73 -2
- 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/fleet/apply.d.ts +59 -3
- package/dist/lib/fleet/apply.js +183 -6
- package/dist/lib/fleet/types.d.ts +21 -2
- package/dist/lib/hooks/cache.js +15 -0
- package/dist/lib/hosts/passthrough.d.ts +44 -0
- package/dist/lib/hosts/passthrough.js +84 -12
- package/dist/lib/hosts/ready.d.ts +2 -0
- package/dist/lib/hosts/ready.js +10 -1
- package/dist/lib/hosts/reconnect.d.ts +14 -12
- package/dist/lib/hosts/reconnect.js +41 -40
- 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/routines.js +14 -2
- package/dist/lib/runner.d.ts +0 -3
- package/dist/lib/runner.js +6 -19
- 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/push.d.ts +94 -0
- package/dist/lib/secrets/push.js +145 -0
- package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
- package/dist/lib/secrets/rc-hygiene.js +14 -1
- package/dist/lib/secrets/reaper.d.ts +15 -1
- package/dist/lib/secrets/reaper.js +30 -3
- package/dist/lib/session/active.d.ts +4 -0
- package/dist/lib/session/db.d.ts +21 -3
- package/dist/lib/session/db.js +229 -15
- package/dist/lib/session/discover.d.ts +1 -0
- package/dist/lib/session/discover.js +115 -19
- package/dist/lib/session/insights.d.ts +18 -0
- package/dist/lib/session/insights.js +143 -1
- 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/session/tool-index.js +133 -22
- package/dist/lib/session/tool-store.d.ts +26 -2
- package/dist/lib/session/tool-store.js +36 -17
- package/dist/lib/shims.js +21 -1
- package/dist/lib/signin-badge.js +2 -0
- package/dist/lib/ssh-exec.js +8 -2
- package/dist/lib/startup/command-registry.d.ts +16 -0
- package/dist/lib/startup/command-registry.js +46 -0
- package/dist/lib/teams/agents.d.ts +13 -0
- package/dist/lib/teams/agents.js +76 -8
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/placement-probe.d.ts +21 -0
- package/dist/lib/teams/placement-probe.js +135 -0
- package/dist/lib/teams/scheduler.d.ts +74 -1
- package/dist/lib/teams/scheduler.js +187 -10
- 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
|
@@ -12,7 +12,7 @@ import * as path from 'path';
|
|
|
12
12
|
import * as yaml from 'yaml';
|
|
13
13
|
import { isDaemonRunning, signalDaemonReload, startDaemon, stopDaemon, readDaemonLog, getDaemonStatus, } from '../lib/daemon.js';
|
|
14
14
|
import { assertSchedulerEnabled } from '../lib/device-config.js';
|
|
15
|
-
import { resolveAgentName, isAgentHardDeprecated, hardDeprecationError } from '../lib/agents.js';
|
|
15
|
+
import { resolveAgentName, isAgentHardDeprecated, hardDeprecationError, ROUTINE_AGENT_IDS } from '../lib/agents.js';
|
|
16
16
|
import { humanizeCron, humanizeNextRun, formatRepoLink, REPO_DISPLAY_MAX } from '../lib/routines-format.js';
|
|
17
17
|
import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, routineStats, getLatestRun, getRunDir, getJobPath, parseAtTime, hasCompletedOneShotRun, isOneShotLikeSchedule, isOneShotRoutine, isPastOneShotRoutine, jobRunsOnThisDevice, checkJobDeviceEligibility, normalizeTriggerEvent, parseHostStrategy, resolveHostStrategy, HOST_STRATEGIES, computeProjectGroup, computeProjectGroupKind, projectGroupKey, projectGroupTitle, projectGroupOrder, normalizeProjects, } from '../lib/routines.js';
|
|
18
18
|
import { listProjectDefs, isSafeProjectName } from '../lib/projects.js';
|
|
@@ -21,7 +21,7 @@ import { fireWebhookJobs, matchJobsToWebhook } from '../lib/triggers/webhook.js'
|
|
|
21
21
|
import { getRoutinesDir } from '../lib/state.js';
|
|
22
22
|
import { IS_WINDOWS } from '../lib/platform/index.js';
|
|
23
23
|
import { safeJoin } from '../lib/paths.js';
|
|
24
|
-
import { executeJob, monitorRunningJobs
|
|
24
|
+
import { executeJob, monitorRunningJobs } from '../lib/runner.js';
|
|
25
25
|
import { JobScheduler } from '../lib/scheduler.js';
|
|
26
26
|
import { detectOverdueJobs } from '../lib/overdue.js';
|
|
27
27
|
import { runCatchup } from '../lib/catchup.js';
|
|
@@ -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
|
}
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { type Command } from 'commander';
|
|
9
9
|
import { SSH_TARGET_RE, assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
10
10
|
import { quoteWin32ExecArg } from '../lib/platform/index.js';
|
|
11
|
+
import { bundleEnvToDotenv } from '../lib/secrets/push.js';
|
|
12
|
+
export { bundleEnvToDotenv };
|
|
11
13
|
import { type SecretsBackend, type SecretsBundle, type SecretsPolicy } from '../lib/secrets/bundles.js';
|
|
12
14
|
import { type SecretsListFilterOpts } from '../lib/secrets/list-filter.js';
|
|
13
15
|
import { type BundleUsageSummary } from '../lib/secrets/usage-db.js';
|
|
@@ -78,14 +80,6 @@ export { SSH_TARGET_RE, assertValidSshTarget };
|
|
|
78
80
|
* the executed command.
|
|
79
81
|
*/
|
|
80
82
|
export declare function buildSecretsExecEnv(parentEnv: NodeJS.ProcessEnv, secretEnv: Record<string, string>): NodeJS.ProcessEnv;
|
|
81
|
-
/**
|
|
82
|
-
* Serialize a resolved env map to `.env` lines that round-trip losslessly through
|
|
83
|
-
* `parseDotenv` on the remote: `KEY="VALUE"`. parseDotenv strips exactly one outer
|
|
84
|
-
* quote pair and takes the inner bytes verbatim (no unescaping), so any single-line
|
|
85
|
-
* value survives unchanged with no escaping. Newlines would break its line-based
|
|
86
|
-
* parse, so multi-line values are rejected rather than silently corrupted.
|
|
87
|
-
*/
|
|
88
|
-
export declare function bundleEnvToDotenv(env: Record<string, string>): string;
|
|
89
83
|
/**
|
|
90
84
|
* Encrypt a resolved env map to an offline bundle file using AES-256-GCM
|
|
91
85
|
* (the same EncFile envelope as the per-item file store). Inner plaintext is
|
package/dist/commands/secrets.js
CHANGED
|
@@ -12,12 +12,12 @@ import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/widt
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import * as os from 'os';
|
|
14
14
|
import * as path from 'path';
|
|
15
|
-
import { SSH_TARGET_RE, assertValidSshTarget
|
|
15
|
+
import { SSH_TARGET_RE, assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
16
16
|
import { quoteWin32ExecArg, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
17
17
|
import { ensureDaemonStarted, isDaemonRunning } from '../lib/daemon.js';
|
|
18
|
-
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget,
|
|
19
|
-
import {
|
|
20
|
-
|
|
18
|
+
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, } from '../lib/secrets/remote.js';
|
|
19
|
+
import { resolveBundleForPush, pushResolvedBundleToHost, bundleEnvToDotenv } from '../lib/secrets/push.js';
|
|
20
|
+
export { bundleEnvToDotenv };
|
|
21
21
|
import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, healKeychainBundleMetadataAclOnce, isHeadlessSecretsContext, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, readBundleIfDecryptable, reAclBundleItems, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, SECRET_TYPES, } from '../lib/secrets/bundles.js';
|
|
22
22
|
import { parseListFilters, bundleMatchesFilter, bundleExpiry, filterIsActive, describeFilter, parseSortField, sortBundles, SORT_FIELDS, REF_KINDS, DEFAULT_EXPIRING_DAYS, } from '../lib/secrets/list-filter.js';
|
|
23
23
|
import { encryptForFallback, decryptForFallback } from '../lib/secrets/filestore.js';
|
|
@@ -354,24 +354,6 @@ function getCliVersion() {
|
|
|
354
354
|
return '0.0.0';
|
|
355
355
|
}
|
|
356
356
|
}
|
|
357
|
-
/**
|
|
358
|
-
* Serialize a resolved env map to `.env` lines that round-trip losslessly through
|
|
359
|
-
* `parseDotenv` on the remote: `KEY="VALUE"`. parseDotenv strips exactly one outer
|
|
360
|
-
* quote pair and takes the inner bytes verbatim (no unescaping), so any single-line
|
|
361
|
-
* value survives unchanged with no escaping. Newlines would break its line-based
|
|
362
|
-
* parse, so multi-line values are rejected rather than silently corrupted.
|
|
363
|
-
*/
|
|
364
|
-
export function bundleEnvToDotenv(env) {
|
|
365
|
-
const lines = [];
|
|
366
|
-
for (const [k, v] of Object.entries(env)) {
|
|
367
|
-
if (/[\r\n]/.test(v)) {
|
|
368
|
-
throw new Error(`Key '${k}' has a multi-line value; the SSH .env transport can't carry newlines. ` +
|
|
369
|
-
`Set it directly on the remote with 'agents secrets add ${k} --value-stdin'.`);
|
|
370
|
-
}
|
|
371
|
-
lines.push(`${k}="${v}"`);
|
|
372
|
-
}
|
|
373
|
-
return lines.join('\n') + '\n';
|
|
374
|
-
}
|
|
375
357
|
/**
|
|
376
358
|
* Encrypt a resolved env map to an offline bundle file using AES-256-GCM
|
|
377
359
|
* (the same EncFile envelope as the per-item file store). Inner plaintext is
|
|
@@ -2115,100 +2097,42 @@ Examples:
|
|
|
2115
2097
|
if (hosts.length > 0) {
|
|
2116
2098
|
for (const h of hosts)
|
|
2117
2099
|
assertValidSshTarget(h);
|
|
2118
|
-
const
|
|
2100
|
+
const parsedBackend = parseBackendOpt(opts.remoteBackend);
|
|
2101
|
+
// `--remote-backend` documents keychain|file only, and parseBackendOpt
|
|
2102
|
+
// exits on anything else — but its return type still admits 'vault',
|
|
2103
|
+
// which has no remote-push path. Refuse it here rather than let it fall
|
|
2104
|
+
// through to the keychain branch and half-work.
|
|
2105
|
+
if (parsedBackend === 'vault') {
|
|
2106
|
+
console.error(chalk.red("--remote-backend vault is not supported; use 'keychain' or 'file'."));
|
|
2107
|
+
process.exit(1);
|
|
2108
|
+
}
|
|
2109
|
+
const remoteBackend = parsedBackend;
|
|
2119
2110
|
// For a file-backed remote bundle a passphrase is OPTIONAL. The file
|
|
2120
2111
|
// store is passphrase-free by default: with AGENTS_SECRETS_PASSPHRASE
|
|
2121
2112
|
// unset the remote `import --backend file` auto-provisions the remote's
|
|
2122
2113
|
// own machine-local key (0600 under ~/.agents/.secrets-key/), so reads
|
|
2123
2114
|
// are HEADLESS. We forward the LOCAL AGENTS_SECRETS_PASSPHRASE only when
|
|
2124
|
-
// the operator opts in by setting it
|
|
2125
|
-
//
|
|
2126
|
-
// lands in argv / `ps` / the remote shell history. Forcing a shared
|
|
2127
|
-
// passphrase would defeat headless reads, so we no longer require one.
|
|
2115
|
+
// the operator opts in by setting it. Forcing a shared passphrase would
|
|
2116
|
+
// defeat headless reads, so we no longer require one.
|
|
2128
2117
|
const remotePassphrase = remoteBackend === 'file' ? (process.env.AGENTS_SECRETS_PASSPHRASE ?? '') : '';
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
const
|
|
2132
|
-
|
|
2133
|
-
// land in its chosen backend, reading the .env off ssh stdin (never
|
|
2134
|
-
// parsed by a remote shell — `--from -` replaces the POSIX-only
|
|
2135
|
-
// `/dev/stdin`). The keychain path is built OS-aware via
|
|
2136
|
-
// `remoteSecretsRaw` (bash -lc on POSIX, PowerShell on Windows), so it
|
|
2137
|
-
// works on macOS, Linux AND Windows targets. `import` auto-creates the
|
|
2138
|
-
// bundle, so no separate `create` (the old `|| true` was a POSIXism
|
|
2139
|
-
// that broke on PowerShell: `'true' is not recognized`).
|
|
2118
|
+
// Resolve ONCE for N hosts — reading a bundle can prompt, and doing it
|
|
2119
|
+
// per host would prompt per host.
|
|
2120
|
+
const resolvedForPush = resolveBundleForPush(resolvedBundleName, 'ssh export');
|
|
2121
|
+
const keyCount = resolvedForPush.keyCount;
|
|
2140
2122
|
let failures = 0;
|
|
2141
2123
|
for (const host of hosts) {
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
// emit broken PowerShell.
|
|
2150
|
-
if (remoteShellFor(resolveRemoteOsSync(host.split('@').pop() ?? host)) === 'powershell') {
|
|
2151
|
-
failures++;
|
|
2152
|
-
console.error(chalk.red(`${host}: file backend export to a Windows target is not yet supported.`));
|
|
2153
|
-
continue;
|
|
2154
|
-
}
|
|
2155
|
-
const { remoteCmd, input } = buildRemoteFileImportCommand(resolvedBundleName, dotenv, {
|
|
2156
|
-
passphrase: remotePassphrase,
|
|
2157
|
-
force: opts.force,
|
|
2158
|
-
});
|
|
2159
|
-
res = sshExec(host, remoteCmd, { input });
|
|
2160
|
-
}
|
|
2161
|
-
else if (remoteShellFor(resolveRemoteOsSync(host.split('@').pop() ?? host)) === 'powershell') {
|
|
2162
|
-
// Keychain on a Windows target: the `agents.ps1` shim doesn't
|
|
2163
|
-
// forward ssh-piped stdin to node, so `--from -` would hang.
|
|
2164
|
-
// Bridge the piped .env through PowerShell into a temp file and
|
|
2165
|
-
// import `--from <file>` (deleted afterwards). Same hardened ssh
|
|
2166
|
-
// engine, .env still only ever crosses the wire over ssh stdin.
|
|
2167
|
-
res = sshExec(host, buildWindowsStdinImportCommand(resolvedBundleName, { force: opts.force }), { input: dotenv });
|
|
2168
|
-
}
|
|
2169
|
-
else {
|
|
2170
|
-
// Keychain on a POSIX target: OS-aware wrapping + hardened ssh
|
|
2171
|
-
// engine (BatchMode, ConnectTimeout, keepalive, control-socket
|
|
2172
|
-
// reuse) via the same path the READ inverse (`remoteResolveEnv`)
|
|
2173
|
-
// uses. `--from -` reads the .env off ssh stdin.
|
|
2174
|
-
res = remoteSecretsRaw(host, ['import', resolvedBundleName, '--from', '-', ...(opts.force ? ['--force'] : [])], { input: dotenv, osLookupName: host });
|
|
2175
|
-
}
|
|
2176
|
-
if (res.code === null) {
|
|
2177
|
-
failures++;
|
|
2178
|
-
console.error(chalk.red(`${host}: ${res.stderr.trim() || (res.timedOut ? 'ssh timed out' : 'ssh failed')}`));
|
|
2179
|
-
continue;
|
|
2180
|
-
}
|
|
2181
|
-
if (res.code !== 0) {
|
|
2124
|
+
const out = pushResolvedBundleToHost(resolvedForPush, resolvedBundleName, host, {
|
|
2125
|
+
remoteBackend,
|
|
2126
|
+
force: opts.force,
|
|
2127
|
+
passphrase: remotePassphrase,
|
|
2128
|
+
operation: 'ssh export',
|
|
2129
|
+
});
|
|
2130
|
+
if (!out.ok) {
|
|
2182
2131
|
failures++;
|
|
2183
|
-
|
|
2184
|
-
console.error(chalk.red(`${host}: remote import failed (exit ${res.code})${msg ? `: ${msg}` : ''}`));
|
|
2132
|
+
console.error(chalk.red(`${host}: ${out.message}`));
|
|
2185
2133
|
continue;
|
|
2186
2134
|
}
|
|
2187
|
-
|
|
2188
|
-
// the bundle metadata but no READABLE value items: the remote login
|
|
2189
|
-
// keychain is locked in the non-interactive SSH context, so Security
|
|
2190
|
-
// accepts the write but the biometry-ACL'd item is unreadable, and the
|
|
2191
|
-
// remote `import` still exits 0. Read the bundle back the same way a
|
|
2192
|
-
// release will (drops the plaintext, keeps only key presence; the
|
|
2193
|
-
// remote's headless `agentOnly` guard fails fast, so no Touch ID) and
|
|
2194
|
-
// FAIL LOUDLY if the keys didn't materialize — rather than leave a
|
|
2195
|
-
// metadata-only bundle that breaks later with "stored item not found".
|
|
2196
|
-
// The file backend is headless-readable by construction, so skip it.
|
|
2197
|
-
if (remoteBackend === 'keychain') {
|
|
2198
|
-
const verdict = verifyRemoteKeychainPush(host, resolvedBundleName, Object.keys(env), { osLookupName: host });
|
|
2199
|
-
if (!verdict.ok) {
|
|
2200
|
-
failures++;
|
|
2201
|
-
if (verdict.kind === 'locked-keychain') {
|
|
2202
|
-
console.error(chalk.red(keychainWriteFailureMessage(host, resolvedBundleName, verdict.reason)));
|
|
2203
|
-
}
|
|
2204
|
-
else {
|
|
2205
|
-
console.error(chalk.red(`${host}: pushed '${resolvedBundleName}' but could not verify it on the remote: ${verdict.reason}`));
|
|
2206
|
-
}
|
|
2207
|
-
continue;
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
const remoteMsg = (res.stdout || '').trim().split('\n').map((l) => l.trim()).filter(Boolean).pop();
|
|
2211
|
-
console.log(chalk.green(`${host} -> '${resolvedBundleName}': ${remoteMsg || `${keyCount} key(s) exported`}`));
|
|
2135
|
+
console.log(chalk.green(`${host} -> '${resolvedBundleName}': ${out.message}`));
|
|
2212
2136
|
}
|
|
2213
2137
|
emitSecretAudit({ event: 'secrets.export', bundle: resolvedBundleName, operation: `export --host ${hosts.join(',')}`, source: 'ssh', host: hosts.join(','), status: failures > 0 ? 'error' : 'success', keyCount });
|
|
2214
2138
|
if (failures > 0)
|
|
@@ -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
|
|
@@ -57,6 +57,7 @@ import { registerSessionsFavoriteCommand } from './sessions-favorite.js';
|
|
|
57
57
|
import { isFavorite, listFavorites } from '../lib/session/favorites.js';
|
|
58
58
|
import { registerGoCommand } from './go.js';
|
|
59
59
|
import { registerFocusCommand } from './focus.js';
|
|
60
|
+
import { registerReconnectCommand } from './reconnect.js';
|
|
60
61
|
import { registerDetachCommand } from './detach.js';
|
|
61
62
|
import { registerAttachCommand } from './attach.js';
|
|
62
63
|
import { registerSessionsInjectCommand } from './sessions-inject.js';
|
|
@@ -66,6 +67,7 @@ import { registerSessionsImportCommand } from './sessions-import.js';
|
|
|
66
67
|
import { registerSessionsMigrateCommand, registerSessionsMigrationsCommand } from './sessions-migrate.js';
|
|
67
68
|
import { registerSessionsBackfillCommand } from './sessions-backfill.js';
|
|
68
69
|
import { registerSessionsStatsCommand } from './sessions-stats.js';
|
|
70
|
+
import { registerSessionsInsightsCommand } from './insights.js';
|
|
69
71
|
import { registerSessionsOptimizeCommand } from './sessions-optimize.js';
|
|
70
72
|
import { runBrowserSessions } from '../lib/browser/sessions-list.js';
|
|
71
73
|
import { countToolProgramOccurrences, parseToolProgramCountClause, readToolIndexCoverage, searchToolCalls, TOOL_QUERY_MAX_CLAUSE_BYTES, TOOL_QUERY_MAX_CLAUSES, TOOL_QUERY_MAX_RESULT_SESSIONS, serializeToolSearchEnvelope, toolSearchRemoteReceiveBudget, } from '../lib/session/tool-index.js';
|
|
@@ -411,6 +413,10 @@ export function backfillActiveRowsFromMeta(sessions, metaById) {
|
|
|
411
413
|
if (!Number.isNaN(ts))
|
|
412
414
|
s.startedAtMs = ts;
|
|
413
415
|
}
|
|
416
|
+
if (!s.origin && m.origin)
|
|
417
|
+
s.origin = m.origin;
|
|
418
|
+
if (!s.routineName && m.routineName)
|
|
419
|
+
s.routineName = m.routineName;
|
|
414
420
|
}
|
|
415
421
|
}
|
|
416
422
|
/**
|
|
@@ -435,6 +441,10 @@ function loadBackfillMetaFor(sessions) {
|
|
|
435
441
|
}
|
|
436
442
|
return byId;
|
|
437
443
|
}
|
|
444
|
+
/** Add indexed display metadata to active rows for non-renderer consumers. */
|
|
445
|
+
export function backfillActiveRowsFromIndex(sessions) {
|
|
446
|
+
backfillActiveRowsFromMeta(sessions, loadBackfillMetaFor(sessions));
|
|
447
|
+
}
|
|
438
448
|
/**
|
|
439
449
|
* The live decoration for a listing row: a status glyph and the latest-turn
|
|
440
450
|
* preview, when the session is still running. `●` running / `◐` waiting on the
|
|
@@ -527,6 +537,20 @@ export function isAwaitingUser(s) {
|
|
|
527
537
|
return false;
|
|
528
538
|
return s.status === 'input_required' || s.activity === 'waiting_input';
|
|
529
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* Whether a row belongs in an unqualified `--active` view.
|
|
542
|
+
*
|
|
543
|
+
* The live registry deliberately retains terminally-dead rows long enough for
|
|
544
|
+
* `--closed` / `--crashed` and recovery to find them. Presence in that registry
|
|
545
|
+
* therefore is not, by itself, evidence that a session is still active.
|
|
546
|
+
* Explicit lifecycle filters bypass this predicate and select those retained
|
|
547
|
+
* rows through {@link matchesLiveStatus} instead.
|
|
548
|
+
*/
|
|
549
|
+
export function isRunningLiveSession(s) {
|
|
550
|
+
if (s.status === 'closed' || s.status === 'crashed')
|
|
551
|
+
return false;
|
|
552
|
+
return s.pidAlive !== false;
|
|
553
|
+
}
|
|
530
554
|
/** Width of the live status column — `crashed` is the longest word it renders. */
|
|
531
555
|
const LIVE_STATUS_W = 8;
|
|
532
556
|
/** The colored, space-padded status cell for a listing row (empty when not live). */
|
|
@@ -1275,14 +1299,14 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
|
1275
1299
|
// gate: exit non-zero when the union contains a session awaiting the user.
|
|
1276
1300
|
const statusFiltered = opts.statuses?.length
|
|
1277
1301
|
? merged.filter((session) => opts.statuses.some((status) => matchesLiveStatus(session, status)))
|
|
1278
|
-
: merged;
|
|
1302
|
+
: merged.filter(isRunningLiveSession);
|
|
1279
1303
|
const sessions = statusFiltered;
|
|
1280
1304
|
// Backfill agent version + ticket/PR/label/created onto the live rows from the
|
|
1281
1305
|
// historical index (RUSH-2205) — a running process reports none of these, and
|
|
1282
1306
|
// an orphan row usually lacks them. Done before both the JSON and human paths
|
|
1283
1307
|
// so every consumer (incl. the SSH fan-out's remote --json) sees enriched rows;
|
|
1284
1308
|
// transcripts sync across the fleet, so a remote row resolves from the local DB.
|
|
1285
|
-
|
|
1309
|
+
backfillActiveRowsFromIndex(sessions);
|
|
1286
1310
|
if (asJson) {
|
|
1287
1311
|
// Resolve who is watching each local tmux pane before serializing: `viewingIn`
|
|
1288
1312
|
// is how a consumer distinguishes a session someone is looking at from one
|
|
@@ -4060,6 +4084,7 @@ export function registerSessionsCommands(program) {
|
|
|
4060
4084
|
registerSessionsFavoriteCommand(sessionsCmd);
|
|
4061
4085
|
registerGoCommand(sessionsCmd);
|
|
4062
4086
|
registerFocusCommand(sessionsCmd);
|
|
4087
|
+
registerReconnectCommand(sessionsCmd);
|
|
4063
4088
|
registerDetachCommand(sessionsCmd);
|
|
4064
4089
|
registerAttachCommand(sessionsCmd);
|
|
4065
4090
|
registerSessionsInjectCommand(sessionsCmd);
|
|
@@ -4070,6 +4095,7 @@ export function registerSessionsCommands(program) {
|
|
|
4070
4095
|
registerSessionsMigrationsCommand(sessionsCmd);
|
|
4071
4096
|
registerSessionsBackfillCommand(sessionsCmd);
|
|
4072
4097
|
registerSessionsStatsCommand(sessionsCmd);
|
|
4098
|
+
registerSessionsInsightsCommand(sessionsCmd);
|
|
4073
4099
|
registerSessionsOptimizeCommand(sessionsCmd);
|
|
4074
4100
|
// Observe-umbrella alias (Phase 3): roster → sessions --active.
|
|
4075
4101
|
registerSessionsObserveAliases(program);
|
|
@@ -15,6 +15,7 @@ interface SetupSecretsOptions {
|
|
|
15
15
|
iUnderstand?: boolean;
|
|
16
16
|
force?: boolean;
|
|
17
17
|
}
|
|
18
|
+
export declare function setupSecretsPrefsPath(): string;
|
|
18
19
|
/** The wizard's `--policy` shares the canonical parser with `agents secrets
|
|
19
20
|
* policy`, so it accepts `hold` (and the `daily`/`session` aliases) rather than
|
|
20
21
|
* carrying its own copy that knows only the retired vocabulary. `parsePolicyOpt`
|
|
@@ -15,7 +15,7 @@ import { parsePolicyOpt } from './secrets.js';
|
|
|
15
15
|
function defaultBackendForPlatform(platform = process.platform) {
|
|
16
16
|
return platform === 'darwin' ? 'keychain' : 'file';
|
|
17
17
|
}
|
|
18
|
-
function setupSecretsPrefsPath() {
|
|
18
|
+
export function setupSecretsPrefsPath() {
|
|
19
19
|
return path.join(getHistoryDir(), 'setup', 'secrets.json');
|
|
20
20
|
}
|
|
21
21
|
function parseBackend(raw) {
|