@phnx-labs/agents-cli 1.20.90 → 1.20.92
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/exec.d.ts +14 -0
- package/dist/commands/exec.js +144 -14
- package/dist/commands/feed.js +77 -4
- package/dist/commands/hooks.js +22 -6
- package/dist/commands/perf.d.ts +14 -0
- package/dist/commands/perf.js +221 -0
- package/dist/commands/projects.d.ts +12 -0
- package/dist/commands/projects.js +358 -0
- package/dist/commands/routines.js +30 -24
- package/dist/commands/secrets.d.ts +43 -4
- package/dist/commands/secrets.js +217 -32
- package/dist/commands/send.d.ts +5 -1
- package/dist/commands/send.js +1 -1
- package/dist/commands/sessions-picker.d.ts +15 -0
- package/dist/commands/sessions-picker.js +107 -13
- 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/index.js +19 -3
- package/dist/lib/activity.d.ts +80 -13
- package/dist/lib/activity.js +418 -74
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/catchup.d.ts +105 -0
- package/dist/lib/catchup.js +160 -0
- package/dist/lib/channels/providers/desktop.d.ts +49 -0
- package/dist/lib/channels/providers/desktop.js +132 -0
- package/dist/lib/channels/providers/index.js +2 -0
- package/dist/lib/daemon.js +74 -13
- package/dist/lib/devices/registry.d.ts +14 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/events.d.ts +12 -0
- package/dist/lib/events.js +122 -9
- package/dist/lib/exec.js +10 -0
- package/dist/lib/feed-broadcast.d.ts +47 -0
- package/dist/lib/feed-broadcast.js +65 -1
- package/dist/lib/feed-post.d.ts +10 -0
- package/dist/lib/feed-post.js +9 -3
- package/dist/lib/feed.d.ts +47 -1
- package/dist/lib/feed.js +38 -0
- package/dist/lib/hooks/cache.d.ts +2 -0
- package/dist/lib/hooks/cache.js +24 -4
- package/dist/lib/hosts/remote-cmd.js +4 -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/overdue.d.ts +14 -0
- package/dist/lib/overdue.js +37 -1
- package/dist/lib/perf/db.d.ts +25 -0
- package/dist/lib/perf/db.js +290 -0
- package/dist/lib/perf/spool.d.ts +18 -0
- package/dist/lib/perf/spool.js +79 -0
- package/dist/lib/perf/types.d.ts +45 -0
- package/dist/lib/perf/types.js +2 -0
- package/dist/lib/project-key.d.ts +44 -0
- package/dist/lib/project-key.js +79 -0
- package/dist/lib/project-root.js +16 -0
- package/dist/lib/project-status.d.ts +69 -0
- package/dist/lib/project-status.js +101 -0
- package/dist/lib/projects.d.ts +138 -0
- package/dist/lib/projects.js +301 -0
- package/dist/lib/remote-agents-json.d.ts +9 -0
- package/dist/lib/remote-agents-json.js +11 -5
- package/dist/lib/routines-project.js +6 -0
- package/dist/lib/routines.d.ts +30 -1
- package/dist/lib/routines.js +11 -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/list-filter.d.ts +94 -0
- package/dist/lib/secrets/list-filter.js +245 -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 +13 -0
- package/dist/lib/session/digest.js +48 -1
- package/dist/lib/session/discover.d.ts +1 -2
- package/dist/lib/session/discover.js +7 -24
- package/dist/lib/session/highlights.d.ts +82 -0
- package/dist/lib/session/highlights.js +251 -0
- package/dist/lib/session/parse.js +23 -1
- package/dist/lib/session/relative-time.d.ts +37 -0
- package/dist/lib/session/relative-time.js +96 -8
- package/dist/lib/session/remote-list.js +5 -2
- package/dist/lib/session/render.d.ts +9 -9
- package/dist/lib/session/render.js +112 -73
- package/dist/lib/session/types.d.ts +4 -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 +2 -0
- package/dist/lib/startup/command-registry.js +4 -0
- package/dist/lib/state.d.ts +11 -0
- package/dist/lib/state.js +16 -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 +1 -1
- package/package.json +4 -1
package/dist/commands/secrets.js
CHANGED
|
@@ -18,7 +18,8 @@ import { ensureDaemonStarted, isDaemonRunning } from '../lib/daemon.js';
|
|
|
18
18
|
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, verifyRemoteKeychainPush, keychainWriteFailureMessage, } from '../lib/secrets/remote.js';
|
|
19
19
|
import { remoteShellFor, buildWindowsStdinImportCommand } from '../lib/hosts/remote-cmd.js';
|
|
20
20
|
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
21
|
-
import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, isHeadlessSecretsContext, readBundle, readBundleIfDecryptable, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, } from '../lib/secrets/bundles.js';
|
|
21
|
+
import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, isHeadlessSecretsContext, readBundle, readBundleIfDecryptable, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, SECRET_TYPES, } from '../lib/secrets/bundles.js';
|
|
22
|
+
import { parseListFilters, bundleMatchesFilter, bundleExpiry, filterIsActive, describeFilter, parseSortField, sortBundles, SORT_FIELDS, REF_KINDS, DEFAULT_EXPIRING_DAYS, } from '../lib/secrets/list-filter.js';
|
|
22
23
|
import { encryptForFallback, decryptForFallback } from '../lib/secrets/filestore.js';
|
|
23
24
|
import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
|
|
24
25
|
import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
|
|
@@ -274,6 +275,35 @@ export function buildRemoteUnlockArgs(names, opts) {
|
|
|
274
275
|
...(opts.durable ? ['--durable'] : []),
|
|
275
276
|
];
|
|
276
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Build the remote `agents secrets list` argv for `list --host`. Every filter
|
|
280
|
+
* must be forwarded: `browseRemote` sends this argv verbatim, so a flag left out
|
|
281
|
+
* here is not an error — the remote just lists everything, and
|
|
282
|
+
* `secrets list --host zion --expired` reports every bundle on zion as expired.
|
|
283
|
+
* Silent and wrong beats loud and wrong only for the person who wrote the bug.
|
|
284
|
+
*
|
|
285
|
+
* Values pass through unparsed so the REMOTE validates them under its own rules,
|
|
286
|
+
* matching how `--ttl` is forwarded by buildRemoteUnlockArgs.
|
|
287
|
+
*/
|
|
288
|
+
export function buildRemoteListArgs(opts, query) {
|
|
289
|
+
return [
|
|
290
|
+
'list',
|
|
291
|
+
...(query ? [query] : []),
|
|
292
|
+
...(opts.json ? ['--json'] : []),
|
|
293
|
+
...(opts.policy ? ['--policy', opts.policy] : []),
|
|
294
|
+
...(opts.backend ? ['--backend', opts.backend] : []),
|
|
295
|
+
...(opts.type ? ['--type', opts.type] : []),
|
|
296
|
+
...(opts.kind ? ['--kind', opts.kind] : []),
|
|
297
|
+
...(opts.held ? ['--held'] : []),
|
|
298
|
+
...(opts.notHeld ? ['--not-held'] : []),
|
|
299
|
+
...(opts.expired ? ['--expired'] : []),
|
|
300
|
+
// `--expiring` is optional-value: `true` means the bare flag was passed.
|
|
301
|
+
...(opts.expiring === true ? ['--expiring'] : opts.expiring ? ['--expiring', String(opts.expiring)] : []),
|
|
302
|
+
...(opts.unused ? ['--unused', opts.unused] : []),
|
|
303
|
+
...(opts.sort ? ['--sort', opts.sort] : []),
|
|
304
|
+
...(opts.limit ? ['--limit', opts.limit] : []),
|
|
305
|
+
];
|
|
306
|
+
}
|
|
277
307
|
// SSH target validation is defined canonically in src/lib/ssh-exec.ts and
|
|
278
308
|
// re-exported here for back-compat with existing importers of these symbols.
|
|
279
309
|
export { SSH_TARGET_RE, assertValidSshTarget };
|
|
@@ -446,11 +476,11 @@ function humanAge(iso) {
|
|
|
446
476
|
return age;
|
|
447
477
|
return `${age} ago`;
|
|
448
478
|
}
|
|
449
|
-
/** Compact
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
479
|
+
/** Compact span for a fixed duration: "45m" / "19h" / "2d". Shared by
|
|
480
|
+
* `compactRemaining` (a countdown) and the POLICY column's hold-window
|
|
481
|
+
* annotation (a fixed length) so the two can never round onto different unit
|
|
482
|
+
* thresholds and disagree about the same number of milliseconds. */
|
|
483
|
+
export function compactDurationMs(ms) {
|
|
454
484
|
const mins = Math.round(ms / 60000);
|
|
455
485
|
if (mins < 60)
|
|
456
486
|
return `${mins}m`;
|
|
@@ -459,18 +489,33 @@ function compactRemaining(expiresAt) {
|
|
|
459
489
|
return `${hours}h`;
|
|
460
490
|
return `${Math.round(hours / 24)}d`;
|
|
461
491
|
}
|
|
462
|
-
/**
|
|
463
|
-
|
|
464
|
-
|
|
492
|
+
/** Compact remaining-time for the list POLICY column: "19h" / "45m" / "2d". */
|
|
493
|
+
function compactRemaining(expiresAt) {
|
|
494
|
+
const ms = expiresAt - Date.now();
|
|
495
|
+
if (ms <= 0)
|
|
496
|
+
return 'expired';
|
|
497
|
+
return compactDurationMs(ms);
|
|
498
|
+
}
|
|
499
|
+
/** The POLICY column for `secrets list`. `hold` is a duration, not a mode — it
|
|
500
|
+
* means "prompt once, then stay silent for this long" — so the column states
|
|
501
|
+
* the window (`hold 7d`) rather than the bare tier name, and appends `· held Nd`
|
|
502
|
+
* while the secrets-agent is actually caching the bundle. `always` and `never`
|
|
503
|
+
* carry no window and gain nothing here. `holdMs` is the configured global hold
|
|
504
|
+
* (`secretsHoldMs()`), passed in rather than read so this stays pure; `held`
|
|
465
505
|
* maps bundle name → expiry epoch-ms (from agentStatus()). */
|
|
466
|
-
export function renderPolicyCol(b, held) {
|
|
506
|
+
export function renderPolicyCol(b, holdMs, held) {
|
|
467
507
|
// `never` is loud on purpose — it's the only tier with no user-presence gate.
|
|
468
508
|
if (bundlePolicy(b) === 'never')
|
|
469
509
|
return chalk.red.bold('never · no prompt');
|
|
470
510
|
if (bundlePolicy(b) === 'always')
|
|
471
511
|
return chalk.yellow('always · prompt');
|
|
472
|
-
const
|
|
473
|
-
|
|
512
|
+
const window = compactDurationMs(holdMs);
|
|
513
|
+
// A lapsed entry is not held. Without this guard a stale broker row renders
|
|
514
|
+
// the countdown's `expired` sentinel as if it were a live hold.
|
|
515
|
+
const exp = liveHold(held?.get(b.name));
|
|
516
|
+
return exp !== null
|
|
517
|
+
? chalk.green(`hold ${window} · held ${compactRemaining(exp)}`)
|
|
518
|
+
: chalk.gray(`hold ${window}`);
|
|
474
519
|
}
|
|
475
520
|
/** The hold-window line at the top of `secrets status`. Names the `hold` policy
|
|
476
521
|
* the window belongs to — the rename in #1604 left this surface still saying
|
|
@@ -502,12 +547,21 @@ export function formatHoldWindow(ms) {
|
|
|
502
547
|
}
|
|
503
548
|
/** Below this width the fixed date columns no longer fit; `list` uses cards. */
|
|
504
549
|
const SECRETS_WIDE = 96;
|
|
550
|
+
/** A broker hold expiry, or null once it has lapsed. One definition of "held",
|
|
551
|
+
* shared by the POLICY column, the `--held` filter, and the JSON payload, so the
|
|
552
|
+
* three can never disagree about the same bundle. */
|
|
553
|
+
export function liveHold(expiresAt, now = Date.now()) {
|
|
554
|
+
return expiresAt !== undefined && expiresAt > now ? expiresAt : null;
|
|
555
|
+
}
|
|
556
|
+
/** Width of the POLICY column. The widest cell is `hold 30d · held 30d` (19
|
|
557
|
+
* visible chars) — `clampHoldMs` caps the window at 30d and `renderPolicyCol`
|
|
558
|
+
* drops lapsed holds, so nothing longer can be produced. */
|
|
559
|
+
const POLICY_COL_WIDTH = 20;
|
|
505
560
|
/** Format a single bundle as a table row for the `secrets list` output. */
|
|
506
|
-
function renderBundleRow(b, held, cols = terminalWidth()) {
|
|
561
|
+
function renderBundleRow(b, holdMs, held, cols = terminalWidth()) {
|
|
507
562
|
const entries = describeBundle(b);
|
|
508
563
|
const keys = entries.length;
|
|
509
|
-
const
|
|
510
|
-
const expiring = expiringCount > 0 ? chalk.yellow(String(expiringCount)) : chalk.gray('-');
|
|
564
|
+
const expiring = renderExpiringCol(b);
|
|
511
565
|
// Timestamp distinction:
|
|
512
566
|
// "?" -> legacy bundle, never written under the timestamping code.
|
|
513
567
|
// "never" -> bundle has been written but the action never happened
|
|
@@ -521,7 +575,7 @@ function renderBundleRow(b, held, cols = terminalWidth()) {
|
|
|
521
575
|
: (b.created_at ? chalk.gray('never') : chalk.gray('?'));
|
|
522
576
|
const head = `${chalk.cyan(b.name.padEnd(20))} ` +
|
|
523
577
|
`${String(keys).padEnd(5)} ` +
|
|
524
|
-
`${padVisible(renderPolicyCol(b, held),
|
|
578
|
+
`${padVisible(renderPolicyCol(b, holdMs, held), POLICY_COL_WIDTH)} ` +
|
|
525
579
|
`${padVisible(expiring, 9)} ` +
|
|
526
580
|
`${padVisible(created, 9)} ` +
|
|
527
581
|
`${padVisible(updated, 9)} ` +
|
|
@@ -542,7 +596,7 @@ function renderBundleRow(b, held, cols = terminalWidth()) {
|
|
|
542
596
|
return trailer ? `${head} ${trailer}` : head.trimEnd();
|
|
543
597
|
}
|
|
544
598
|
/** Narrow-terminal card: name + compact meta on one line, description below. */
|
|
545
|
-
function renderBundleCard(b, held, cols) {
|
|
599
|
+
function renderBundleCard(b, holdMs, held, cols) {
|
|
546
600
|
const keys = describeBundle(b).length;
|
|
547
601
|
const used = b.last_used ? relativeAge(b.last_used) : (b.created_at ? 'never' : '?');
|
|
548
602
|
const tag = b.backend === 'file'
|
|
@@ -550,7 +604,7 @@ function renderBundleCard(b, held, cols) {
|
|
|
550
604
|
: b.backend === 'vault'
|
|
551
605
|
? chalk.blue(' [synced]')
|
|
552
606
|
: '';
|
|
553
|
-
const meta = chalk.gray(`${keys} key${keys === 1 ? '' : 's'} · `) + renderPolicyCol(b, held) + chalk.gray(` · used ${used}`);
|
|
607
|
+
const meta = chalk.gray(`${keys} key${keys === 1 ? '' : 's'} · `) + renderPolicyCol(b, holdMs, held) + chalk.gray(` · used ${used}`);
|
|
554
608
|
const line1 = `${chalk.cyan(b.name)} ${meta}${tag}`;
|
|
555
609
|
if (!b.description)
|
|
556
610
|
return line1;
|
|
@@ -662,7 +716,10 @@ function renderMetaLine(meta, reveal) {
|
|
|
662
716
|
return '';
|
|
663
717
|
return ` ${parts.join(' ')}`;
|
|
664
718
|
}
|
|
665
|
-
/** Count entries in `meta` whose `expires` falls in the next 30 days.
|
|
719
|
+
/** Count entries in `meta` whose `expires` falls in the next 30 days. Excludes
|
|
720
|
+
* keys that have ALREADY expired — those are counted by `bundleExpiry().expired`
|
|
721
|
+
* and rendered separately, since a lapsed key is a different problem from one
|
|
722
|
+
* coming due. */
|
|
666
723
|
function countExpiringSoon(meta) {
|
|
667
724
|
if (!meta)
|
|
668
725
|
return 0;
|
|
@@ -676,6 +733,23 @@ function countExpiringSoon(meta) {
|
|
|
676
733
|
}
|
|
677
734
|
return n;
|
|
678
735
|
}
|
|
736
|
+
/**
|
|
737
|
+
* The EXPIRING cell. Counts keys needing attention — already expired plus due
|
|
738
|
+
* within 30 days — and colours by the worst of the two: red once anything has
|
|
739
|
+
* lapsed, yellow while everything is merely upcoming.
|
|
740
|
+
*
|
|
741
|
+
* Expired keys used to be invisible here. `countExpiringSoon` requires
|
|
742
|
+
* `d >= 0`, so a bundle whose token died last month rendered `-`, identical to
|
|
743
|
+
* one with no expiry at all; the only places it surfaced were `secrets view`
|
|
744
|
+
* and a hard abort at inject time, i.e. after it had already broken something.
|
|
745
|
+
*/
|
|
746
|
+
export function renderExpiringCol(b, now = Date.now()) {
|
|
747
|
+
const { expired, soon } = bundleExpiry(b, now);
|
|
748
|
+
const total = expired + soon;
|
|
749
|
+
if (total === 0)
|
|
750
|
+
return chalk.gray('-');
|
|
751
|
+
return expired > 0 ? chalk.red(String(total)) : chalk.yellow(String(total));
|
|
752
|
+
}
|
|
679
753
|
/**
|
|
680
754
|
* Resolve an existing import target bundle (inheriting its backend) or create a
|
|
681
755
|
* new one with the requested backend. Refuses to silently downgrade a
|
|
@@ -801,22 +875,61 @@ export function registerSecretsCommands(program) {
|
|
|
801
875
|
{ title: 'Sync commands', names: ['push', 'pull', 'remote-list'] },
|
|
802
876
|
{ title: 'Utilities', names: ['exec', 'mcp', 'generate', 'migrate-acl'] },
|
|
803
877
|
]);
|
|
804
|
-
cmd
|
|
805
|
-
.command('list')
|
|
878
|
+
const listCmd = cmd
|
|
879
|
+
.command('list [query]')
|
|
806
880
|
.alias('ls')
|
|
807
|
-
.description('List configured secrets bundles (use --host/--hosts
|
|
881
|
+
.description('List configured secrets bundles, optionally filtered (use --host/--hosts for other machines over SSH)')
|
|
808
882
|
.option('--host <target>', 'List bundles on a remote host over SSH (enrolled `agents hosts` name, ssh-config alias, or user@host)')
|
|
809
883
|
.option('--hosts <list>', 'Comma-separated hosts to list in one shot, e.g. yosemite-s0,yosemite-s1')
|
|
810
884
|
.option('--device <target>', 'Alias for --host')
|
|
811
885
|
.option('--devices <list>', 'Alias for --hosts')
|
|
812
886
|
.option('--json', 'Emit machine-readable JSON (bundle metadata only — never secret values) instead of the table')
|
|
813
|
-
.
|
|
887
|
+
.option('--policy <list>', "Only these prompt policies (comma-separated): hold, always, never. '--policy never' is the audit for bundles that read with no Touch ID at all")
|
|
888
|
+
.option('--backend <list>', 'Only these backends (comma-separated): keychain, file, vault')
|
|
889
|
+
.option('--type <list>', `Only bundles carrying a key of these types (comma-separated): ${SECRET_TYPES.join(', ')}`)
|
|
890
|
+
.option('--kind <list>', `Only bundles carrying a value of these ref kinds (comma-separated): ${REF_KINDS.join(', ')}. '--kind literal' finds raw values stored inline; '--kind exec' finds bundles that shell out`)
|
|
891
|
+
.option('--held', 'Only bundles the secrets-agent is holding right now (read silently, no Touch ID). macOS only')
|
|
892
|
+
.option('--not-held', 'Only bundles the agent is NOT holding — the next read of each will prompt. macOS only')
|
|
893
|
+
.option('--expired', 'Only bundles with at least one key whose expiry has already passed')
|
|
894
|
+
.option('--expiring [days]', `Only bundles with a key falling due within N days (default ${DEFAULT_EXPIRING_DAYS})`)
|
|
895
|
+
.option('--unused <duration>', 'Only bundles not read since this far back (e.g. 30d, 4w, 3mo). Never-used bundles always match')
|
|
896
|
+
.addOption(new Option('--sort <field>', 'Sort by: name (default), used, created, updated, expiry').choices([...SORT_FIELDS]))
|
|
897
|
+
.option('-n, --limit <n>', 'Show at most this many bundles (after filtering and sorting)')
|
|
898
|
+
.action(async (query, opts) => {
|
|
814
899
|
const targets = parseHostsOption(opts);
|
|
815
900
|
if (targets.length > 0) {
|
|
816
|
-
|
|
901
|
+
// Forward the filters — browseRemote sends this argv verbatim, so a
|
|
902
|
+
// dropped flag would make the remote silently list everything.
|
|
903
|
+
await browseRemote(targets, buildRemoteListArgs(opts, query), false);
|
|
817
904
|
return;
|
|
818
905
|
}
|
|
819
|
-
|
|
906
|
+
// Parse before touching the keychain so a typo'd flag fails instantly,
|
|
907
|
+
// and render the message rather than a stack trace (the `policy` pattern).
|
|
908
|
+
let filter;
|
|
909
|
+
let sortField;
|
|
910
|
+
let limit;
|
|
911
|
+
try {
|
|
912
|
+
filter = parseListFilters(opts, query);
|
|
913
|
+
sortField = parseSortField(opts.sort);
|
|
914
|
+
limit = opts.limit === undefined ? undefined : Number(opts.limit);
|
|
915
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) {
|
|
916
|
+
throw new Error(`Invalid --limit '${opts.limit}'. Use a whole number of bundles, e.g. --limit 10.`);
|
|
917
|
+
}
|
|
918
|
+
// The broker is macOS-only, so off darwin `held` is always empty and a
|
|
919
|
+
// hold-state filter would answer from no data — every bundle would look
|
|
920
|
+
// not-held. Refuse rather than return a confidently wrong list.
|
|
921
|
+
if (filter.held !== undefined && process.platform !== 'darwin') {
|
|
922
|
+
throw new Error('--held/--not-held need the secrets-agent, which is macOS-only. Run it on a Mac, or with --host <mac>.');
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
catch (err) {
|
|
926
|
+
console.error(chalk.red(err.message));
|
|
927
|
+
process.exit(1);
|
|
928
|
+
}
|
|
929
|
+
const all = listBundles();
|
|
930
|
+
// The configured hold window, read once for the whole listing — it is
|
|
931
|
+
// global, so every `hold` bundle renders the same span.
|
|
932
|
+
const holdMs = secretsHoldMs();
|
|
820
933
|
// Cross-reference the secrets-agent so `hold` bundles that are currently
|
|
821
934
|
// held can show "· held Nh". Soft-fails to no hint if the broker is down.
|
|
822
935
|
const held = new Map();
|
|
@@ -829,6 +942,12 @@ export function registerSecretsCommands(program) {
|
|
|
829
942
|
/* broker not running — render policy without the countdown */
|
|
830
943
|
}
|
|
831
944
|
}
|
|
945
|
+
// Filter, sort, then cap — before the --json branch, so the machine
|
|
946
|
+
// payload is the exact twin of the table (the sessions ordering rule).
|
|
947
|
+
const now = Date.now();
|
|
948
|
+
const matched = all.filter((b) => bundleMatchesFilter(b, filter, { held, now }));
|
|
949
|
+
const sorted = sortBundles(matched, sortField);
|
|
950
|
+
const bundles = limit === undefined ? sorted : sorted.slice(0, limit);
|
|
832
951
|
if (opts.json) {
|
|
833
952
|
// Discovery payload for agents: metadata only, no secret values. Gated on
|
|
834
953
|
// the explicit --json flag (not stdout.isTTY) so piping the human table to
|
|
@@ -837,36 +956,96 @@ export function registerSecretsCommands(program) {
|
|
|
837
956
|
name: b.name,
|
|
838
957
|
keys: describeBundle(b).length,
|
|
839
958
|
policy: bundlePolicy(b),
|
|
959
|
+
// The window `policy: "hold"` is shorthand for. Null on always/never,
|
|
960
|
+
// which have no window at all.
|
|
961
|
+
holdMs: bundlePolicy(b) === 'hold' ? holdMs : null,
|
|
840
962
|
backend: b.backend === 'file' ? 'file' : 'keychain',
|
|
841
963
|
allowExec: Boolean(b.allow_exec),
|
|
842
964
|
expiringSoon: countExpiringSoon(b.meta),
|
|
965
|
+
// Already-lapsed keys, which `expiringSoon` deliberately excludes. A
|
|
966
|
+
// machine caller polling for rotation needs both numbers.
|
|
967
|
+
expired: bundleExpiry(b, now).expired,
|
|
843
968
|
description: b.description ?? null,
|
|
844
969
|
createdAt: b.created_at ?? null,
|
|
845
970
|
updatedAt: b.updated_at ?? null,
|
|
846
971
|
lastUsed: b.last_used ?? null,
|
|
847
|
-
|
|
972
|
+
// Same liveness rule the POLICY column and --held use: a broker entry
|
|
973
|
+
// past its expiry is not held. Without the check this field reported a
|
|
974
|
+
// stale past timestamp as if the bundle were still warm, disagreeing
|
|
975
|
+
// with the table and the filter about the very same bundle.
|
|
976
|
+
heldExpiresAt: liveHold(held.get(b.name), now),
|
|
848
977
|
}));
|
|
849
978
|
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
850
979
|
return;
|
|
851
980
|
}
|
|
852
981
|
if (bundles.length === 0) {
|
|
853
|
-
|
|
854
|
-
|
|
982
|
+
// Distinguish "you have none" from "your filter excluded all of them",
|
|
983
|
+
// and name the axes that did it — otherwise the only way to find out
|
|
984
|
+
// which flag emptied the list is to remove them one at a time.
|
|
985
|
+
if (filterIsActive(filter)) {
|
|
986
|
+
console.log(chalk.gray(`No bundles ${describeFilter(filter)}. ${all.length} bundle${all.length === 1 ? '' : 's'} total.`));
|
|
987
|
+
console.log(chalk.gray('Try: agents secrets list (no filters)'));
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
console.log(chalk.gray('No secrets bundles configured.'));
|
|
991
|
+
console.log(chalk.gray('Try: agents secrets create <name>'));
|
|
992
|
+
}
|
|
855
993
|
return;
|
|
856
994
|
}
|
|
857
995
|
const cols = terminalWidth();
|
|
858
996
|
if (cols >= SECRETS_WIDE) {
|
|
859
|
-
console.log(chalk.bold(`${'NAME'.padEnd(20)} ${'KEYS'.padEnd(5)} ${'POLICY'.padEnd(
|
|
997
|
+
console.log(chalk.bold(`${'NAME'.padEnd(20)} ${'KEYS'.padEnd(5)} ${'POLICY'.padEnd(POLICY_COL_WIDTH)} ${'EXPIRING'.padEnd(9)} ${'CREATED'.padEnd(9)} ${'UPDATED'.padEnd(9)} ${'USED'.padEnd(7)} DESCRIPTION`));
|
|
860
998
|
for (const b of bundles) {
|
|
861
|
-
console.log(renderBundleRow(b, held, cols));
|
|
999
|
+
console.log(renderBundleRow(b, holdMs, held, cols));
|
|
862
1000
|
}
|
|
863
1001
|
}
|
|
864
1002
|
else {
|
|
865
1003
|
for (const b of bundles) {
|
|
866
|
-
console.log(renderBundleCard(b, held, cols));
|
|
1004
|
+
console.log(renderBundleCard(b, holdMs, held, cols));
|
|
867
1005
|
}
|
|
868
1006
|
}
|
|
869
1007
|
});
|
|
1008
|
+
setHelpSections(listCmd, {
|
|
1009
|
+
examples: `
|
|
1010
|
+
# Everything, newest-used first
|
|
1011
|
+
agents secrets list --sort used
|
|
1012
|
+
|
|
1013
|
+
# Find one by name or description
|
|
1014
|
+
agents secrets list github
|
|
1015
|
+
|
|
1016
|
+
# The security audit: which bundles read with NO Touch ID at all?
|
|
1017
|
+
agents secrets list --policy never
|
|
1018
|
+
|
|
1019
|
+
# Which still store a raw value inline, or can shell out?
|
|
1020
|
+
agents secrets list --kind literal
|
|
1021
|
+
agents secrets list --kind exec
|
|
1022
|
+
|
|
1023
|
+
# What has already lapsed, and what is about to
|
|
1024
|
+
agents secrets list --expired
|
|
1025
|
+
agents secrets list --expiring 7 --sort expiry
|
|
1026
|
+
|
|
1027
|
+
# What can I delete? Untouched in three months
|
|
1028
|
+
agents secrets list --unused 3mo --sort used
|
|
1029
|
+
|
|
1030
|
+
# Which bundles will prompt me on the next read (macOS)
|
|
1031
|
+
agents secrets list --not-held
|
|
1032
|
+
|
|
1033
|
+
# Combine axes — every filter narrows further
|
|
1034
|
+
agents secrets list --policy hold --backend file --expiring
|
|
1035
|
+
|
|
1036
|
+
# Same filters, on another machine, machine-readable
|
|
1037
|
+
agents secrets list --expired --host mac-mini --json
|
|
1038
|
+
`,
|
|
1039
|
+
notes: `
|
|
1040
|
+
- Filters compose: every flag you add narrows the list further.
|
|
1041
|
+
- An unknown value is an error, not an empty list — '--policy hodl' names the valid set.
|
|
1042
|
+
- --held/--not-held read live broker state, so they need macOS. Use --host <mac> from elsewhere.
|
|
1043
|
+
- --expired and --expiring are different questions: already lapsed vs coming due. The EXPIRING column counts both and turns red once anything has lapsed.
|
|
1044
|
+
- --unused matches bundles never read at all, not just old ones.
|
|
1045
|
+
- Filters apply before --json, so the JSON is the exact twin of the table.
|
|
1046
|
+
- Filters are forwarded over --host, so a remote list narrows the same way.
|
|
1047
|
+
`,
|
|
1048
|
+
});
|
|
870
1049
|
cmd
|
|
871
1050
|
.command('view [name]')
|
|
872
1051
|
.alias('show')
|
|
@@ -971,6 +1150,9 @@ export function registerSecretsCommands(program) {
|
|
|
971
1150
|
name: bundle.name,
|
|
972
1151
|
description: bundle.description ?? null,
|
|
973
1152
|
policy: bundlePolicy(bundle),
|
|
1153
|
+
// The window `policy: "hold"` is shorthand for. Null on
|
|
1154
|
+
// always/never, which have no window at all.
|
|
1155
|
+
holdMs: bundlePolicy(bundle) === 'hold' ? secretsHoldMs() : null,
|
|
974
1156
|
backend: bundle.backend === 'file' ? 'file' : 'keychain',
|
|
975
1157
|
allowExec: Boolean(bundle.allow_exec),
|
|
976
1158
|
createdAt: bundle.created_at ?? null,
|
|
@@ -995,7 +1177,10 @@ export function registerSecretsCommands(program) {
|
|
|
995
1177
|
}
|
|
996
1178
|
else {
|
|
997
1179
|
console.log(bundlePolicy(bundle) === 'hold'
|
|
998
|
-
|
|
1180
|
+
// The window comes from secretsHoldMs(), never a literal — this
|
|
1181
|
+
// line used to hardcode "7d by default" and so misstated the
|
|
1182
|
+
// window for anyone who had configured secrets.agent.holdMs.
|
|
1183
|
+
? chalk.gray(`policy: hold (ask once, then held for ${formatHoldWindow(secretsHoldMs())} — until sleep / logout; screen-lock does not drop it)`)
|
|
999
1184
|
: chalk.gray('policy: always (asks for Touch ID every time — never auto-held)'));
|
|
1000
1185
|
}
|
|
1001
1186
|
if (bundle.created_at)
|
package/dist/commands/send.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `agents send <text> --channel <name> --to <target>` — deliver a message over
|
|
3
|
-
* any registered channel (mailbox, telegram, imessage, slack, discord, apps).
|
|
3
|
+
* any registered channel (mailbox, telegram, imessage, slack, discord, desktop, apps).
|
|
4
|
+
*
|
|
5
|
+
* `desktop` is the local machine's notification centre — the only channel with no
|
|
6
|
+
* external dependency, so it still reaches the operator at their Mac when a
|
|
7
|
+
* messaging gateway is down.
|
|
4
8
|
*
|
|
5
9
|
* `agents notify <text>` — owner-facing alias. Fills channel + target from the
|
|
6
10
|
* `notify.owner` block in agents.yaml when not passed, so routines/agents ping
|
package/dist/commands/send.js
CHANGED
|
@@ -29,7 +29,7 @@ async function doSend(text, channel, to, opts) {
|
|
|
29
29
|
export function registerSendCommand(program) {
|
|
30
30
|
program
|
|
31
31
|
.command('send <text>')
|
|
32
|
-
.description('Send a message through a channel provider (mailbox, telegram, imessage, slack, discord).')
|
|
32
|
+
.description('Send a message through a channel provider (mailbox, telegram, imessage, slack, discord, desktop).')
|
|
33
33
|
.requiredOption('--channel <name>', 'channel / provider name')
|
|
34
34
|
.requiredOption('--to <target>', 'channel-specific recipient id')
|
|
35
35
|
.option('--thread <id>', 'channel thread id / timestamp')
|
|
@@ -48,6 +48,21 @@ export declare function buildPreview(session: SessionMeta): string;
|
|
|
48
48
|
* names the command that does report them instead.
|
|
49
49
|
*/
|
|
50
50
|
export declare function formatTeamLineage(session: SessionMeta): string;
|
|
51
|
+
/**
|
|
52
|
+
* The three timing facts the header reports: when the session was created, when
|
|
53
|
+
* it was last active, and how long it ran. Reads the parsed transcript when
|
|
54
|
+
* there is one and otherwise the indexed `SessionMeta`, so a remote or
|
|
55
|
+
* unindexed session — which has no local transcript to parse — still reports
|
|
56
|
+
* them instead of silently dropping the whole line.
|
|
57
|
+
*
|
|
58
|
+
* `lastActive` and `lasted` are omitted for a session whose whole life was under
|
|
59
|
+
* a minute: there they just restate `created`.
|
|
60
|
+
*/
|
|
61
|
+
export declare function extractTiming(session: Pick<SessionMeta, 'timestamp' | 'lastActivity' | 'durationMs'>, events: SessionEvent[]): {
|
|
62
|
+
createdAgo?: string;
|
|
63
|
+
lastActiveAgo?: string;
|
|
64
|
+
duration?: string;
|
|
65
|
+
};
|
|
51
66
|
/**
|
|
52
67
|
* Unique directories the session touched, compact and human-readable.
|
|
53
68
|
* Prefer `session.recentDirectoriesTouched` — the scan records it on the row, so
|
|
@@ -18,6 +18,7 @@ import { extractTodoProgress, WORKTREE_RE } from '../lib/session/state.js';
|
|
|
18
18
|
import { renderMarkdown } from '../lib/markdown.js';
|
|
19
19
|
import { itemPicker } from '../lib/picker.js';
|
|
20
20
|
import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from '../lib/session/digest.js';
|
|
21
|
+
import { extractArtifacts, extractHooks, extractLinks, extractRepos, extractSkills } from '../lib/session/highlights.js';
|
|
21
22
|
/** A session whose transcript lives on another machine (folded in over the live
|
|
22
23
|
* cross-machine fan-out): its `filePath` is on that peer's disk, so the preview
|
|
23
24
|
* can't parse it locally — it shows metadata + a "resume there" note instead.
|
|
@@ -145,7 +146,7 @@ function displayAgent(agent) {
|
|
|
145
146
|
const DOT = chalk.gray(' · ');
|
|
146
147
|
function formatHeader(session, events) {
|
|
147
148
|
const model = extractModel(events) || session.model;
|
|
148
|
-
const {
|
|
149
|
+
const { createdAgo, lastActiveAgo, duration } = extractTiming(session, events);
|
|
149
150
|
const totalMessages = session.messageCount ?? countMessages(events);
|
|
150
151
|
const totalTokens = session.tokenCount;
|
|
151
152
|
// Line 1: Agent v version · shortId · model · account
|
|
@@ -157,7 +158,7 @@ function formatHeader(session, events) {
|
|
|
157
158
|
line1.push(chalk.bold.white(shortenModel(model)));
|
|
158
159
|
if (session.account)
|
|
159
160
|
line1.push(chalk.gray(session.account));
|
|
160
|
-
// Line 2: cwd · project · branch ·
|
|
161
|
+
// Line 2: cwd · project · branch · created X ago · last active Y ago · lasted Z
|
|
161
162
|
// Project is clickable: Linear issue URL is for tickets (line 4); for the
|
|
162
163
|
// project name prefer a GitHub repo URL derived from the checkout path.
|
|
163
164
|
const line2 = [];
|
|
@@ -171,8 +172,10 @@ function formatHeader(session, events) {
|
|
|
171
172
|
}
|
|
172
173
|
if (session.gitBranch)
|
|
173
174
|
line2.push(chalk.cyan(session.gitBranch));
|
|
174
|
-
if (
|
|
175
|
-
line2.push(chalk.gray('
|
|
175
|
+
if (createdAgo)
|
|
176
|
+
line2.push(chalk.gray('created ') + chalk.white(createdAgo + ' ago'));
|
|
177
|
+
if (lastActiveAgo)
|
|
178
|
+
line2.push(chalk.gray('last active ') + chalk.white(lastActiveAgo + ' ago'));
|
|
176
179
|
if (duration)
|
|
177
180
|
line2.push(chalk.gray('lasted ') + chalk.white(duration));
|
|
178
181
|
// Line 3: N msgs · T tokens · [label ·] uuid
|
|
@@ -291,16 +294,39 @@ function extractModel(events) {
|
|
|
291
294
|
}
|
|
292
295
|
return undefined;
|
|
293
296
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
297
|
+
/** Shortest span worth reporting as a separate last-activity field — matches the
|
|
298
|
+
* listing's rule, so the pane and the row agree on what counts as a one-shot. */
|
|
299
|
+
const TIMING_SPAN_MIN_MS = 60_000;
|
|
300
|
+
/**
|
|
301
|
+
* The three timing facts the header reports: when the session was created, when
|
|
302
|
+
* it was last active, and how long it ran. Reads the parsed transcript when
|
|
303
|
+
* there is one and otherwise the indexed `SessionMeta`, so a remote or
|
|
304
|
+
* unindexed session — which has no local transcript to parse — still reports
|
|
305
|
+
* them instead of silently dropping the whole line.
|
|
306
|
+
*
|
|
307
|
+
* `lastActive` and `lasted` are omitted for a session whose whole life was under
|
|
308
|
+
* a minute: there they just restate `created`.
|
|
309
|
+
*/
|
|
310
|
+
export function extractTiming(session, events) {
|
|
311
|
+
const firstMs = Date.parse(events[0]?.timestamp ?? session.timestamp);
|
|
299
312
|
if (Number.isNaN(firstMs))
|
|
300
313
|
return {};
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
|
|
314
|
+
const createdAgo = humanDuration(Math.max(0, Date.now() - firstMs));
|
|
315
|
+
const lastMs = Date.parse(events[events.length - 1]?.timestamp ?? session.lastActivity ?? session.timestamp);
|
|
316
|
+
if (Number.isNaN(lastMs))
|
|
317
|
+
return { createdAgo };
|
|
318
|
+
// A scan-persisted duration beats the timestamp subtraction when there are no
|
|
319
|
+
// events to read, because `lastActivity` may have fallen back to file mtime.
|
|
320
|
+
const spanMs = events.length === 0 && session.durationMs !== undefined
|
|
321
|
+
? session.durationMs
|
|
322
|
+
: Math.max(0, lastMs - firstMs);
|
|
323
|
+
if (spanMs < TIMING_SPAN_MIN_MS)
|
|
324
|
+
return { createdAgo };
|
|
325
|
+
return {
|
|
326
|
+
createdAgo,
|
|
327
|
+
lastActiveAgo: humanDuration(Math.max(0, Date.now() - lastMs)),
|
|
328
|
+
duration: humanDuration(spanMs),
|
|
329
|
+
};
|
|
304
330
|
}
|
|
305
331
|
function countMessages(events) {
|
|
306
332
|
return events.filter(e => e.type === 'message').length;
|
|
@@ -417,9 +443,15 @@ function formatCompactPreview(events, session) {
|
|
|
417
443
|
}
|
|
418
444
|
// Recent activity = directories touched (not raw tool calls). Prefer a
|
|
419
445
|
// parser-supplied dirsTouched when present; else derive from event paths.
|
|
446
|
+
// Width-capped: a long Dirs line used to wrap and swamp the whole pane.
|
|
420
447
|
const dirs = directoriesTouched(session, events, changes);
|
|
421
448
|
if (dirs.length) {
|
|
422
|
-
lines.push(chalk.cyan('Dirs: ') +
|
|
449
|
+
lines.push(chalk.cyan('Dirs: ') + joinWidthCapped(dirs, termWidth - 12));
|
|
450
|
+
}
|
|
451
|
+
// Repos worked in (basename of each `.git` root under the touched paths).
|
|
452
|
+
const repos = extractRepos(events, session.cwd);
|
|
453
|
+
if (repos.length) {
|
|
454
|
+
lines.push(chalk.cyan('Repos: ') + chalk.white(repos.slice(0, 4).join(chalk.gray(' · '))));
|
|
423
455
|
}
|
|
424
456
|
const teamLine = formatTeamLineage(session);
|
|
425
457
|
if (teamLine) {
|
|
@@ -442,6 +474,41 @@ function formatCompactPreview(events, session) {
|
|
|
442
474
|
if (activity.length) {
|
|
443
475
|
lines.push(chalk.cyan('Changes: ') + activity.join(chalk.gray(' · ')));
|
|
444
476
|
}
|
|
477
|
+
// Documents the session produced (`.agents/artifacts|plans|reports`, other
|
|
478
|
+
// *.md/*.html creations) — the files a human browses later, named + clickable.
|
|
479
|
+
const artifacts = extractArtifacts(changes);
|
|
480
|
+
if (artifacts.length) {
|
|
481
|
+
const shown = artifacts.slice(0, 5).map(a => linkPath(a.path, a.basename));
|
|
482
|
+
const more = artifacts.length > 5 ? chalk.gray(` · +${artifacts.length - 5} more`) : '';
|
|
483
|
+
lines.push(chalk.cyan('Artifacts: ') + shown.join(chalk.gray(' · ')) + more);
|
|
484
|
+
}
|
|
485
|
+
// Skills invoked (plugin skills included — they ride the same Skill tool).
|
|
486
|
+
const skills = extractSkills(events);
|
|
487
|
+
if (skills.length) {
|
|
488
|
+
const shown = skills.slice(0, 5).map(s => chalk.white(s.name) + (s.count > 1 ? chalk.gray(` ×${s.count}`) : ''));
|
|
489
|
+
const more = skills.length > 5 ? chalk.gray(` · +${skills.length - 5} more`) : '';
|
|
490
|
+
lines.push(chalk.cyan('Skills: ') + shown.join(chalk.gray(' · ')) + more);
|
|
491
|
+
}
|
|
492
|
+
// Hooks fired (Claude transcripts record firings; other harnesses don't).
|
|
493
|
+
const hooks = extractHooks(events);
|
|
494
|
+
if (hooks.length) {
|
|
495
|
+
const shown = hooks.slice(0, 4).map(h => chalk.white(h.name) + (h.count > 1 ? chalk.gray(` ×${h.count}`) : '') + (h.failed ? chalk.red(` (${h.failed} failed)`) : ''));
|
|
496
|
+
const more = hooks.length > 4 ? chalk.gray(` · +${hooks.length - 4} more`) : '';
|
|
497
|
+
lines.push(chalk.cyan('Hooks: ') + shown.join(chalk.gray(' · ')) + more);
|
|
498
|
+
}
|
|
499
|
+
// Links mentioned in the conversation — clickable (OSC 8), tracker-classified.
|
|
500
|
+
const links = extractLinks(events);
|
|
501
|
+
if (links.length) {
|
|
502
|
+
const shown = links.slice(0, 5).map(l => chalk.blue(linkUrl(l.url, l.label)));
|
|
503
|
+
const more = links.length > 5 ? chalk.gray(` · +${links.length - 5} more`) : '';
|
|
504
|
+
lines.push(chalk.cyan('Links: ') + shown.join(chalk.gray(' · ')) + more);
|
|
505
|
+
}
|
|
506
|
+
// Error tally, mirroring the full summary's Errors section in one line.
|
|
507
|
+
const errorEvents = events.filter(e => e.type === 'error');
|
|
508
|
+
if (errorEvents.length) {
|
|
509
|
+
const first = errorEvents[0].tool || 'unknown';
|
|
510
|
+
lines.push(chalk.cyan('Errors: ') + chalk.red(`${errorEvents.length} failure${errorEvents.length === 1 ? '' : 's'}`) + chalk.gray(` — first: ${first}`));
|
|
511
|
+
}
|
|
445
512
|
const metadata = [
|
|
446
513
|
...toolTags,
|
|
447
514
|
subAgentCount ? `${subAgentCount} sub-agent${subAgentCount === 1 ? '' : 's'}` : '',
|
|
@@ -559,12 +626,39 @@ function encodeClaudeSlug(absPath) {
|
|
|
559
626
|
}
|
|
560
627
|
/** The `.agents/worktrees/<name>` marker, Claude-slug-encoded (`/.` → `--`). */
|
|
561
628
|
const SLUG_WORKTREE_RE = /--agents-worktrees-(.+)$/;
|
|
629
|
+
/**
|
|
630
|
+
* Join display tokens with ` · `, stopping before the line exceeds `maxWidth`
|
|
631
|
+
* and appending `… +N more` for the rest. Keeps the Dirs line on one row.
|
|
632
|
+
*/
|
|
633
|
+
function joinWidthCapped(items, maxWidth) {
|
|
634
|
+
const sep = ' · ';
|
|
635
|
+
let out = '';
|
|
636
|
+
let shown = 0;
|
|
637
|
+
for (const item of items) {
|
|
638
|
+
const next = shown === 0 ? item : out + sep + item;
|
|
639
|
+
if (shown > 0 && next.length > maxWidth)
|
|
640
|
+
break;
|
|
641
|
+
out = next;
|
|
642
|
+
shown++;
|
|
643
|
+
}
|
|
644
|
+
const remaining = items.length - shown;
|
|
645
|
+
const suffix = remaining > 0 ? ` … +${remaining} more` : '';
|
|
646
|
+
return chalk.white(out) + (suffix ? chalk.gray(suffix) : '');
|
|
647
|
+
}
|
|
562
648
|
/** Relativize a file path to its parent dir, short enough for one preview line. */
|
|
563
649
|
export function relativizeDir(filePath, cwd) {
|
|
564
650
|
const norm = filePath.replace(/\\/g, '/');
|
|
565
651
|
if (!norm || norm.includes('node_modules') || norm.includes('/.git/') || norm.includes('/plans/')) {
|
|
566
652
|
return undefined;
|
|
567
653
|
}
|
|
654
|
+
// agents-cli internals — version homes, session/run archives, the bare
|
|
655
|
+
// worktree container/root — are never meaningful "directories the user works
|
|
656
|
+
// in". Cwd is exempt: a session running inside such a dir keeps its own paths.
|
|
657
|
+
const normBase = cwd?.replace(/\\/g, '/').replace(/\/$/, '');
|
|
658
|
+
const underCwd = normBase && (norm === normBase || norm.startsWith(normBase + '/'));
|
|
659
|
+
if (!underCwd && (norm.includes('/.agents/.history/') || /\/\.agents\/worktrees(\/[^/]+)?\/?$/.test(norm))) {
|
|
660
|
+
return undefined;
|
|
661
|
+
}
|
|
568
662
|
let dir = path.posix.dirname(norm);
|
|
569
663
|
// Claude project-slug form: a leading `-`-segment (`-home-me-…`) that carries
|
|
570
664
|
// the cwd, lossily encoded. Handle it in SLUG SPACE — never lossy-decode to a
|
|
@@ -11,6 +11,8 @@ interface ResumeOptions {
|
|
|
11
11
|
ghostty?: boolean;
|
|
12
12
|
tmux?: boolean;
|
|
13
13
|
vscodium?: boolean;
|
|
14
|
+
/** --terminal-app: force macOS Terminal.app. Named to avoid reading as `run --terminal`. */
|
|
15
|
+
terminalApp?: boolean;
|
|
14
16
|
splits?: boolean;
|
|
15
17
|
}
|
|
16
18
|
export declare function registerSessionsResumeCommand(sessionsCmd: Command): void;
|