@phnx-labs/agents-cli 1.20.72 → 1.20.73

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.
Files changed (219) hide show
  1. package/CHANGELOG.md +299 -0
  2. package/README.md +40 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +12 -0
  5. package/dist/commands/activity.js +68 -0
  6. package/dist/commands/apply.d.ts +13 -0
  7. package/dist/commands/apply.js +28 -3
  8. package/dist/commands/browser.js +1 -1
  9. package/dist/commands/cloud.js +21 -11
  10. package/dist/commands/doctor.js +8 -4
  11. package/dist/commands/events.d.ts +11 -10
  12. package/dist/commands/events.js +30 -21
  13. package/dist/commands/exec.d.ts +34 -0
  14. package/dist/commands/exec.js +337 -39
  15. package/dist/commands/feed.d.ts +13 -0
  16. package/dist/commands/feed.js +130 -29
  17. package/dist/commands/hq.d.ts +2 -0
  18. package/dist/commands/hq.js +58 -0
  19. package/dist/commands/lease.d.ts +19 -0
  20. package/dist/commands/lease.js +138 -11
  21. package/dist/commands/login.d.ts +2 -0
  22. package/dist/commands/login.js +123 -0
  23. package/dist/commands/logs.js +1 -1
  24. package/dist/commands/mine.d.ts +27 -0
  25. package/dist/commands/mine.js +207 -0
  26. package/dist/commands/monitors.js +79 -49
  27. package/dist/commands/plugins.js +1 -0
  28. package/dist/commands/profiles.js +158 -0
  29. package/dist/commands/repo.js +2 -60
  30. package/dist/commands/resources.d.ts +5 -0
  31. package/dist/commands/resources.js +95 -0
  32. package/dist/commands/routines.d.ts +2 -0
  33. package/dist/commands/routines.js +139 -37
  34. package/dist/commands/secrets.js +234 -47
  35. package/dist/commands/send.d.ts +15 -0
  36. package/dist/commands/send.js +62 -0
  37. package/dist/commands/sessions-tail.d.ts +1 -0
  38. package/dist/commands/sessions-tail.js +20 -9
  39. package/dist/commands/sessions.js +18 -3
  40. package/dist/commands/setup-mine.d.ts +18 -0
  41. package/dist/commands/setup-mine.js +106 -0
  42. package/dist/commands/setup-share.js +2 -2
  43. package/dist/commands/setup.js +3 -1
  44. package/dist/commands/share.d.ts +8 -1
  45. package/dist/commands/share.js +122 -37
  46. package/dist/commands/ssh.d.ts +9 -0
  47. package/dist/commands/ssh.js +196 -26
  48. package/dist/commands/sync.js +1 -1
  49. package/dist/commands/teams.js +11 -2
  50. package/dist/index.js +79 -10
  51. package/dist/lib/activity.d.ts +124 -0
  52. package/dist/lib/activity.js +542 -0
  53. package/dist/lib/agents.js +16 -5
  54. package/dist/lib/artifact-actions.d.ts +1 -1
  55. package/dist/lib/artifact-actions.js +1 -1
  56. package/dist/lib/ask-classifier.d.ts +2 -1
  57. package/dist/lib/ask-classifier.js +3 -3
  58. package/dist/lib/auth-health.d.ts +32 -4
  59. package/dist/lib/auth-health.js +40 -1
  60. package/dist/lib/auto-pull.js +8 -1
  61. package/dist/lib/brand.d.ts +40 -0
  62. package/dist/lib/brand.js +122 -0
  63. package/dist/lib/browser/drivers/ssh.js +4 -7
  64. package/dist/lib/browser/service.js +6 -8
  65. package/dist/lib/channels/providers/index.d.ts +2 -0
  66. package/dist/lib/channels/providers/index.js +20 -0
  67. package/dist/lib/channels/providers/mailbox.d.ts +2 -0
  68. package/dist/lib/channels/providers/mailbox.js +26 -0
  69. package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
  70. package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
  71. package/dist/lib/channels/providers/rush.d.ts +6 -0
  72. package/dist/lib/channels/providers/rush.js +55 -0
  73. package/dist/lib/channels/registry.d.ts +42 -0
  74. package/dist/lib/channels/registry.js +20 -0
  75. package/dist/lib/channels/resolve.d.ts +11 -0
  76. package/dist/lib/channels/resolve.js +12 -0
  77. package/dist/lib/cli-resources.d.ts +18 -0
  78. package/dist/lib/cli-resources.js +53 -4
  79. package/dist/lib/cloud/codex.js +6 -3
  80. package/dist/lib/cloud/factory.d.ts +1 -0
  81. package/dist/lib/cloud/factory.js +10 -4
  82. package/dist/lib/cloud/rush.d.ts +3 -1
  83. package/dist/lib/cloud/rush.js +6 -1
  84. package/dist/lib/cloud/types.d.ts +2 -0
  85. package/dist/lib/codex-home.d.ts +35 -0
  86. package/dist/lib/codex-home.js +136 -0
  87. package/dist/lib/crabbox/cli.d.ts +44 -0
  88. package/dist/lib/crabbox/cli.js +120 -20
  89. package/dist/lib/crabbox/lease.d.ts +52 -1
  90. package/dist/lib/crabbox/lease.js +117 -16
  91. package/dist/lib/crabbox/progress.d.ts +31 -0
  92. package/dist/lib/crabbox/progress.js +76 -0
  93. package/dist/lib/crabbox/runtimes.d.ts +4 -0
  94. package/dist/lib/crabbox/runtimes.js +33 -8
  95. package/dist/lib/crabbox/setup-copy.d.ts +87 -0
  96. package/dist/lib/crabbox/setup-copy.js +127 -0
  97. package/dist/lib/daemon.d.ts +5 -5
  98. package/dist/lib/daemon.js +9 -9
  99. package/dist/lib/drive-sync.js +6 -3
  100. package/dist/lib/event-stream.d.ts +36 -0
  101. package/dist/lib/event-stream.js +68 -0
  102. package/dist/lib/events.d.ts +1 -1
  103. package/dist/lib/exec.d.ts +11 -0
  104. package/dist/lib/exec.js +23 -2
  105. package/dist/lib/feed-policy.d.ts +1 -1
  106. package/dist/lib/feed-policy.js +14 -9
  107. package/dist/lib/feed-ranking.d.ts +32 -0
  108. package/dist/lib/feed-ranking.js +224 -0
  109. package/dist/lib/feed.d.ts +35 -2
  110. package/dist/lib/feed.js +66 -3
  111. package/dist/lib/fleet/auth-sync.d.ts +53 -0
  112. package/dist/lib/fleet/auth-sync.js +92 -0
  113. package/dist/lib/fleet/remote-login.d.ts +187 -0
  114. package/dist/lib/fleet/remote-login.js +556 -0
  115. package/dist/lib/format.d.ts +30 -3
  116. package/dist/lib/format.js +34 -5
  117. package/dist/lib/fs-atomic.d.ts +7 -2
  118. package/dist/lib/fs-atomic.js +8 -12
  119. package/dist/lib/git.d.ts +16 -0
  120. package/dist/lib/git.js +79 -2
  121. package/dist/lib/heal.js +11 -3
  122. package/dist/lib/hosts/progress.d.ts +28 -10
  123. package/dist/lib/hosts/progress.js +79 -22
  124. package/dist/lib/hosts/remote-cmd.js +4 -0
  125. package/dist/lib/hq/floor.d.ts +87 -0
  126. package/dist/lib/hq/floor.js +226 -0
  127. package/dist/lib/menubar/install-menubar.js +14 -20
  128. package/dist/lib/notify.d.ts +1 -0
  129. package/dist/lib/notify.js +3 -3
  130. package/dist/lib/open-url.d.ts +2 -0
  131. package/dist/lib/open-url.js +19 -0
  132. package/dist/lib/openclaw-keychain.d.ts +56 -0
  133. package/dist/lib/openclaw-keychain.js +236 -0
  134. package/dist/lib/overdue.js +10 -0
  135. package/dist/lib/permissions.d.ts +44 -1
  136. package/dist/lib/permissions.js +284 -7
  137. package/dist/lib/project-launch.d.ts +6 -12
  138. package/dist/lib/project-launch.js +13 -228
  139. package/dist/lib/project-resources.d.ts +7 -0
  140. package/dist/lib/project-resources.js +291 -0
  141. package/dist/lib/pty-client.d.ts +27 -0
  142. package/dist/lib/pty-client.js +136 -10
  143. package/dist/lib/refresh.js +14 -10
  144. package/dist/lib/resource-profiles.d.ts +26 -0
  145. package/dist/lib/resource-profiles.js +157 -0
  146. package/dist/lib/resources/permissions.js +7 -1
  147. package/dist/lib/resources/types.d.ts +1 -1
  148. package/dist/lib/resources.d.ts +1 -1
  149. package/dist/lib/resources.js +32 -3
  150. package/dist/lib/routines.d.ts +11 -0
  151. package/dist/lib/routines.js +56 -15
  152. package/dist/lib/runner.d.ts +1 -0
  153. package/dist/lib/runner.js +66 -5
  154. package/dist/lib/secrets/bundles.d.ts +16 -3
  155. package/dist/lib/secrets/bundles.js +206 -37
  156. package/dist/lib/secrets/icloud-import.d.ts +2 -2
  157. package/dist/lib/secrets/icloud-import.js +9 -6
  158. package/dist/lib/secrets/mcp.js +1 -1
  159. package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
  160. package/dist/lib/secrets/vault-age-helper.js +34 -0
  161. package/dist/lib/secrets/vault.d.ts +49 -0
  162. package/dist/lib/secrets/vault.js +397 -0
  163. package/dist/lib/self-heal/checks/path.js +3 -1
  164. package/dist/lib/self-heal/checks/shadowing.js +10 -2
  165. package/dist/lib/self-heal/checks/shims.js +19 -11
  166. package/dist/lib/session/cloud.d.ts +2 -2
  167. package/dist/lib/session/cloud.js +2 -2
  168. package/dist/lib/session/db.d.ts +4 -0
  169. package/dist/lib/session/db.js +44 -7
  170. package/dist/lib/session/discover.d.ts +2 -0
  171. package/dist/lib/session/discover.js +114 -3
  172. package/dist/lib/session/stream-render.d.ts +3 -0
  173. package/dist/lib/session/stream-render.js +130 -0
  174. package/dist/lib/session/types.d.ts +6 -0
  175. package/dist/lib/share/analytics.d.ts +13 -0
  176. package/dist/lib/share/analytics.js +45 -0
  177. package/dist/lib/share/config.d.ts +19 -5
  178. package/dist/lib/share/config.js +44 -8
  179. package/dist/lib/share/provision.d.ts +58 -6
  180. package/dist/lib/share/provision.js +97 -22
  181. package/dist/lib/share/publish.d.ts +36 -13
  182. package/dist/lib/share/publish.js +55 -8
  183. package/dist/lib/share/worker-template.js +83 -17
  184. package/dist/lib/shims.d.ts +9 -0
  185. package/dist/lib/shims.js +91 -13
  186. package/dist/lib/ssh-exec.d.ts +14 -0
  187. package/dist/lib/ssh-exec.js +57 -0
  188. package/dist/lib/staleness/detectors/hooks.js +25 -1
  189. package/dist/lib/staleness/detectors/permissions.js +66 -0
  190. package/dist/lib/staleness/detectors/workflows.js +20 -0
  191. package/dist/lib/staleness/writers/hooks.js +58 -4
  192. package/dist/lib/staleness/writers/permissions.js +2 -2
  193. package/dist/lib/staleness/writers/skills.js +1 -1
  194. package/dist/lib/staleness/writers/sources.d.ts +10 -1
  195. package/dist/lib/staleness/writers/sources.js +68 -1
  196. package/dist/lib/star-nudge.d.ts +45 -0
  197. package/dist/lib/star-nudge.js +91 -0
  198. package/dist/lib/startup/command-registry.d.ts +6 -1
  199. package/dist/lib/startup/command-registry.js +17 -3
  200. package/dist/lib/state.d.ts +2 -0
  201. package/dist/lib/state.js +3 -0
  202. package/dist/lib/subagents-registry.d.ts +2 -0
  203. package/dist/lib/subagents-registry.js +3 -0
  204. package/dist/lib/teams/parsers.js +214 -6
  205. package/dist/lib/teams/supervisor.d.ts +7 -0
  206. package/dist/lib/teams/supervisor.js +2 -1
  207. package/dist/lib/template.d.ts +1 -1
  208. package/dist/lib/template.js +1 -1
  209. package/dist/lib/triggers/webhook.js +36 -3
  210. package/dist/lib/types.d.ts +73 -0
  211. package/dist/lib/version.d.ts +40 -0
  212. package/dist/lib/version.js +94 -16
  213. package/dist/lib/versions.d.ts +11 -0
  214. package/dist/lib/versions.js +208 -76
  215. package/dist/lib/workflows.d.ts +2 -0
  216. package/dist/lib/workflows.js +88 -0
  217. package/package.json +2 -1
  218. package/dist/commands/daemon.d.ts +0 -10
  219. package/dist/commands/daemon.js +0 -121
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { ensureFeedPublishHook, listBlocks, recordNotified } from '../lib/feed.js';
2
+ import { ensureFeedPublishHook, listAskStats, listBlocks, recordNotified } from '../lib/feed.js';
3
+ import { ensureActivityLogHook, readRecentActivity, formatActivityLine } from '../lib/activity.js';
3
4
  import { enrichBlocksFromSessions, groupBlocksByOutcome, isUnambiguousOutcomeAnswer, openBlocksForOutcome, stampBlockOutcomes, } from '../lib/feed-outcome.js';
4
5
  import { classifyBlock, filterBlocksForFeed, suppressionDigest, } from '../lib/ask-classifier.js';
5
6
  import { machineId, normalizeHost } from '../lib/machine-id.js';
@@ -12,6 +13,9 @@ import { isValidMailboxId } from '../lib/mailbox.js';
12
13
  import { getActiveSessions } from '../lib/session/active.js';
13
14
  import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
14
15
  import { GLYPH, masthead } from '../lib/comms-render.js';
16
+ import { discoverSessions } from '../lib/session/discover.js';
17
+ import { resolveProvider } from '../lib/cloud/registry.js';
18
+ import { buildSessionSignals, rankFeedBlocks, synthesizeControlCards, } from '../lib/feed-ranking.js';
15
19
  export const FEED_NO_FANOUT_ENV = 'AGENTS_FEED_LOCAL';
16
20
  /** Right-hand masthead summary: `N blocks · M agents`. */
17
21
  export function formatFeedMastheadRight(blocks) {
@@ -58,6 +62,36 @@ export function mergeFeedBlocks(...groups) {
58
62
  }
59
63
  return [...byIdentity.values()].sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts));
60
64
  }
65
+ function matchesControlTarget(signal, target) {
66
+ return [
67
+ signal.mailboxId,
68
+ signal.sessionId,
69
+ signal.cloudTaskId,
70
+ signal.pid !== undefined ? String(signal.pid) : undefined,
71
+ ].some((value) => value === target);
72
+ }
73
+ export async function controlFeedSession(action, target, signals) {
74
+ const signal = signals.find((s) => matchesControlTarget(s, target));
75
+ if (!signal)
76
+ throw new Error(`No live feed session matches '${target}'.`);
77
+ if (signal.cloudProvider && signal.cloudTaskId) {
78
+ const provider = resolveProvider(signal.cloudProvider);
79
+ await provider.cancel(signal.cloudTaskId);
80
+ return `${action === 'pause' ? 'paused' : 'killed'} cloud task ${signal.cloudTaskId}`;
81
+ }
82
+ if (!signal.pid) {
83
+ throw new Error(`Session '${target}' has no local pid or cancellable cloud task.`);
84
+ }
85
+ if (action === 'pause') {
86
+ if (process.platform === 'win32') {
87
+ throw new Error('Pause is not supported for local Windows processes; use --kill.');
88
+ }
89
+ process.kill(signal.pid, 'SIGSTOP');
90
+ return `paused pid ${signal.pid}`;
91
+ }
92
+ process.kill(signal.pid, 'SIGTERM');
93
+ return `killed pid ${signal.pid}`;
94
+ }
61
95
  function hostToken(host) {
62
96
  return normalizeHost(host.split('@').pop() || host);
63
97
  }
@@ -69,6 +103,16 @@ export function remoteFeedHostsToDial(hosts, self) {
69
103
  return undefined;
70
104
  return hosts.filter((host) => hostToken(host) !== self);
71
105
  }
106
+ export function prepareLocalFeedBlocks(localBlocks, opts) {
107
+ const filter = filterBlocksForFeed(localBlocks, {
108
+ apply: opts.includeLocal && (!opts.all || opts.dispatch === true),
109
+ });
110
+ return {
111
+ visible: opts.all ? localBlocks : filter.surfaced,
112
+ dispatch: filter.surfaced,
113
+ filter,
114
+ };
115
+ }
72
116
  function renderBlock(b, localHost, indent = '') {
73
117
  const host = b.host !== localHost ? chalk.yellow(` [${b.host}]`) : '';
74
118
  const runtime = chalk.gray(b.runtime);
@@ -76,13 +120,16 @@ function renderBlock(b, localHost, indent = '') {
76
120
  const cls = b.blockClass ? chalk.gray(`(${b.blockClass})`) : '';
77
121
  const consequence = b.consequence && b.consequence !== 'normal' ? chalk.red(`[${b.consequence}]`) : '';
78
122
  const cost = b.costOfDelay ? chalk.gray(`cost:${b.costOfDelay}`) : '';
123
+ const rank = b.delayRank ? chalk.gray(`rank:${Math.round(b.delayRank.score)}`) : '';
79
124
  // Shared fleet-comms glyphs: ▲ open ask, ✓ answered (see comms-render GLYPH).
80
125
  const marker = b.answer
81
126
  ? chalk.green(GLYPH.delivered)
82
- : !b.parkedAt
83
- ? chalk.yellow(GLYPH.ask)
84
- : ' ';
85
- console.log(`${indent}${marker} ${chalk.cyan(b.mailboxId)}${host} ${runtime} ${age} ${cls} ${consequence} ${cost}`.trimEnd());
127
+ : b.kind === 'control'
128
+ ? chalk.red('!')
129
+ : !b.parkedAt
130
+ ? chalk.yellow(GLYPH.ask)
131
+ : ' ';
132
+ console.log(`${indent}${marker} ${chalk.cyan(b.mailboxId)}${host} ${runtime} ${age} ${cls} ${consequence} ${cost} ${rank}`.trimEnd());
86
133
  for (const question of b.questions) {
87
134
  const header = question.header ? chalk.gray(`[${question.header}] `) : '';
88
135
  console.log(`${indent} ${header}${question.text}`);
@@ -119,7 +166,15 @@ function renderBlock(b, localHost, indent = '') {
119
166
  if (b.notifiedAt) {
120
167
  console.log(`${indent} ${chalk.dim('notified')} ${relTime(b.notifiedAt)}`);
121
168
  }
122
- if (!b.answer && !b.parkedAt) {
169
+ if (b.runaway) {
170
+ console.log(`${indent} ${chalk.red('runaway:')} ${b.runaway.reason}`);
171
+ console.log(`${indent} ${chalk.dim(`control: ag feed --pause ${b.mailboxId} · ag feed --kill ${b.mailboxId}`)}`);
172
+ }
173
+ if (b.needy) {
174
+ console.log(`${indent} ${chalk.yellow('needy:')} ${b.needy.askCountLastHour}/${b.needy.threshold} asks in the last hour`);
175
+ console.log(`${indent} ${chalk.dim(`inspect: ag sessions ${b.sessionId}`)}`);
176
+ }
177
+ if (!b.answer && !b.parkedAt && b.kind !== 'control') {
123
178
  console.log(`${indent} ${chalk.dim(formatFeedReplyHint(b.mailboxId))}`);
124
179
  }
125
180
  console.log();
@@ -169,6 +224,8 @@ export function registerFeedCommand(program) {
169
224
  .option('-H, --host <target...>', 'Scope to remote machine(s) over SSH; repeatable')
170
225
  .option('--device <target...>', 'Alias for --host; repeatable')
171
226
  .option('--dispatch', 'Run stall suppression + default-on-no-answer policy and urgent notifications')
227
+ .option('--pause <id>', 'Pause a runaway/needy local process (SIGSTOP) or cancel a cloud task')
228
+ .option('--kill <id>', 'Kill a runaway/needy local process (SIGTERM) or cancel a cloud task')
172
229
  .action(async (opts) => {
173
230
  if (opts.device?.length)
174
231
  opts.host = [...(opts.host ?? []), ...opts.device];
@@ -176,11 +233,16 @@ export function registerFeedCommand(program) {
176
233
  const includeLocal = shouldIncludeLocalFeed(opts.host, self);
177
234
  const setupWarnings = [];
178
235
  if (includeLocal) {
236
+ // Feed and activity hooks are independent -- install both, and register
237
+ // the manifest as long as at least one wrote its entries (don't couple
238
+ // activity registration to the feed hook succeeding).
179
239
  const hookInstall = ensureFeedPublishHook();
180
- if (hookInstall.error) {
240
+ const activityInstall = ensureActivityLogHook();
241
+ if (hookInstall.error)
181
242
  setupWarnings.push(hookInstall.error);
182
- }
183
- else {
243
+ if (activityInstall.error)
244
+ setupWarnings.push(activityInstall.error);
245
+ if (!hookInstall.error || !activityInstall.error) {
184
246
  const [{ iterHooksCapableVersions, parseHookManifest, registerHooksToSettings }, { getVersionHomePath }] = await Promise.all([
185
247
  import('../lib/hooks.js'),
186
248
  import('../lib/versions.js'),
@@ -199,6 +261,17 @@ export function registerFeedCommand(program) {
199
261
  if (includeLocal) {
200
262
  sessions = await getActiveSessions();
201
263
  }
264
+ const sessionMetas = includeLocal && sessions.length > 0 ? await discoverSessions({ all: true, limit: 5000 }) : [];
265
+ const localSignals = buildSessionSignals(sessions, sessionMetas);
266
+ if (opts.pause || opts.kill) {
267
+ if (!includeLocal) {
268
+ throw new Error('Feed controls run on the local machine. Re-run against the target host with --local.');
269
+ }
270
+ const action = opts.pause ? 'pause' : 'kill';
271
+ const target = opts.pause ?? opts.kill ?? '';
272
+ console.log(await controlFeedSession(action, target, localSignals));
273
+ return;
274
+ }
202
275
  if (opts.dispatch && includeLocal) {
203
276
  // Liveness sweep: drop messages to dead agents and retire stale blocks
204
277
  // before we render the feed.
@@ -208,8 +281,26 @@ export function registerFeedCommand(program) {
208
281
  console.log(chalk.yellow(`gc: ${gcResult.messagesDroppedDead} dead messages, ${gcResult.blocksRemoved} stale blocks removed`));
209
282
  }
210
283
  }
211
- const localBlocks = includeLocal ? listBlocks() : [];
212
- let blocks = localBlocks;
284
+ let localBlocks = includeLocal
285
+ ? [...listBlocks(), ...synthesizeControlCards(localSignals, listAskStats())]
286
+ : [];
287
+ // Fill missing ticket/PR/worktree from live session meta before local
288
+ // policy mutates the store, so outcome keys land even when the publish
289
+ // hook had no deliverable stamp.
290
+ if (sessions.length > 0) {
291
+ localBlocks = enrichBlocksFromSessions(localBlocks, sessionHintsFromActive(sessions));
292
+ }
293
+ // Stall suppression (RUSH-1477) must only mutate blocks owned by this
294
+ // machine. Remote peers run their own `feed --json`; never enqueue a
295
+ // policy answer into a local mailbox for a remote agent.
296
+ const preparedLocal = prepareLocalFeedBlocks(localBlocks, {
297
+ includeLocal,
298
+ all: opts.all,
299
+ dispatch: opts.dispatch,
300
+ });
301
+ const visibleLocalBlocks = preparedLocal.visible;
302
+ const dispatchBlocks = preparedLocal.dispatch;
303
+ let blocks = visibleLocalBlocks;
213
304
  const forceLocal = opts.local === true || process.env[FEED_NO_FANOUT_ENV] === '1';
214
305
  if (!forceLocal) {
215
306
  const remoteHosts = remoteFeedHostsToDial(opts.host, self);
@@ -221,31 +312,18 @@ export function registerFeedCommand(program) {
221
312
  hosts: remoteHosts,
222
313
  parse: parseRemoteFeed,
223
314
  });
224
- blocks = mergeFeedBlocks(localBlocks, remote.items);
315
+ blocks = mergeFeedBlocks(visibleLocalBlocks, remote.items);
225
316
  }
226
317
  }
227
- // Fill missing ticket/PR/worktree from live session meta so outcome keys
228
- // land even when the publish hook had no deliverable stamp.
229
- if (sessions.length > 0) {
230
- blocks = enrichBlocksFromSessions(blocks, sessionHintsFromActive(sessions));
231
- }
232
- // Stall suppression (RUSH-1477): auto-answer "should I…?" / "what's next?"
233
- // so they never render. Applied on every feed read unless --all; --dispatch
234
- // also applies so unattended ticks drain the stall backlog.
235
- const filter = filterBlocksForFeed(blocks, {
236
- apply: !opts.all || opts.dispatch === true,
237
- });
238
- if (!opts.all) {
239
- blocks = filter.surfaced;
240
- }
241
- const digest = suppressionDigest(filter);
318
+ blocks = rankFeedBlocks(blocks, localSignals);
319
+ const digest = suppressionDigest(preparedLocal.filter);
242
320
  if (digest && !opts.json) {
243
321
  console.log(chalk.dim(digest));
244
322
  }
245
323
  if (opts.dispatch) {
246
324
  const policy = loadPolicy();
247
325
  const now = new Date();
248
- for (const b of blocks) {
326
+ for (const b of dispatchBlocks) {
249
327
  // Wrap per-block policy so one malformed block (e.g. a crafted
250
328
  // mailboxId that throws in mailboxDir) can't abort the whole loop and
251
329
  // strand every remaining block's dispatch.
@@ -285,6 +363,8 @@ export function registerFeedCommand(program) {
285
363
  }
286
364
  if (blocks.length === 0) {
287
365
  console.log(chalk.gray(digest ? 'No open blocks after stall suppression.' : 'No open blocks.'));
366
+ if (includeLocal)
367
+ renderActivityLane();
288
368
  return;
289
369
  }
290
370
  // Shared fleet-comms masthead (same family as `agents mailboxes`).
@@ -300,8 +380,29 @@ export function registerFeedCommand(program) {
300
380
  renderBlock(b, self);
301
381
  return;
302
382
  }
303
- const groups = groupBlocksByOutcome(blocks);
383
+ const groups = groupBlocksByOutcome(blocks).sort((a, b) => {
384
+ const ar = Math.max(...a.blocks.map((block) => block.delayRank?.score ?? 0));
385
+ const br = Math.max(...b.blocks.map((block) => block.delayRank?.score ?? 0));
386
+ return br - ar;
387
+ });
304
388
  for (const g of groups)
305
389
  renderOutcomeGroup(g, self);
390
+ if (includeLocal)
391
+ renderActivityLane();
306
392
  });
307
393
  }
394
+ /**
395
+ * Print a compact "recent activity" lane under the feed: the last few milestone
396
+ * events (plans, PRs, worktrees, sub-agents) from the append-only activity logs.
397
+ * Read-only tail of the logs -- no transcript re-parsing. Silent when empty.
398
+ */
399
+ function renderActivityLane() {
400
+ const events = readRecentActivity({ sinceMs: Date.now() - 24 * 60 * 60 * 1000, limit: 6 })
401
+ .filter((e) => e.tier === 'milestone');
402
+ if (events.length === 0)
403
+ return;
404
+ console.log(chalk.bold('\n recent activity'));
405
+ for (const ev of events)
406
+ console.log(formatActivityLine(ev, { showHost: true }));
407
+ console.log(chalk.gray(' → agents activity for the full stream'));
408
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerHqCommand(program: Command): void;
@@ -0,0 +1,58 @@
1
+ import chalk from 'chalk';
2
+ import { getActiveSessions } from '../lib/session/active.js';
3
+ import { AgentManager } from '../lib/teams/agents.js';
4
+ import { handleStatus, handleTasks } from '../lib/teams/api.js';
5
+ import { listAskStats, listBlocks } from '../lib/feed.js';
6
+ import { buildSessionSignals, synthesizeControlCards } from '../lib/feed-ranking.js';
7
+ import { discoverSessions } from '../lib/session/discover.js';
8
+ import { buildHqFloor } from '../lib/hq/floor.js';
9
+ import { die } from '../lib/format.js';
10
+ async function collectFloorSnapshot() {
11
+ const manager = new AgentManager();
12
+ const [sessions, tasksResult] = await Promise.all([
13
+ getActiveSessions(),
14
+ handleTasks(manager, 1000),
15
+ ]);
16
+ const teammatesByTeam = new Map();
17
+ await Promise.all(tasksResult.tasks.map(async (team) => {
18
+ const status = await handleStatus(manager, team.task_name, 'all');
19
+ teammatesByTeam.set(team.task_name, status.agents);
20
+ }));
21
+ const sessionMetas = sessions.length > 0 ? await discoverSessions({ all: true, limit: 5000 }) : [];
22
+ const blocks = [
23
+ ...listBlocks(),
24
+ ...synthesizeControlCards(buildSessionSignals(sessions, sessionMetas), listAskStats()),
25
+ ];
26
+ return buildHqFloor({
27
+ sessions,
28
+ teams: tasksResult.tasks,
29
+ teammatesByTeam,
30
+ blocks,
31
+ });
32
+ }
33
+ export function registerHqCommand(program) {
34
+ const hq = program
35
+ .command('hq')
36
+ .description('Machine-readable bridge for Agents HQ floor management.');
37
+ hq
38
+ .command('floor')
39
+ .description('Emit the live floor snapshot: rooms, agents, ambient events, and runnable actions.')
40
+ .option('--json', 'Output machine-readable JSON')
41
+ .action(async (opts) => {
42
+ try {
43
+ const snapshot = await collectFloorSnapshot();
44
+ if (opts.json || !process.stdout.isTTY) {
45
+ console.log(JSON.stringify(snapshot, null, 2));
46
+ return;
47
+ }
48
+ console.log(chalk.bold('Agents HQ floor'));
49
+ console.log(chalk.gray(`${snapshot.counters.agents} agents, ${snapshot.counters.rooms} rooms, ${snapshot.counters.needsInput} need input`));
50
+ for (const room of snapshot.rooms) {
51
+ console.log(`${chalk.cyan(room.name)} ${chalk.gray(`${room.counts.agents} agents, ${room.counts.running} running, ${room.counts.needsInput} need input`)}`);
52
+ }
53
+ }
54
+ catch (err) {
55
+ die(err.message);
56
+ }
57
+ });
58
+ }
@@ -8,6 +8,25 @@
8
8
  * so a box a concurrent run just reused is never stopped.
9
9
  */
10
10
  import type { Command } from 'commander';
11
+ import { type CrabboxBox } from '../lib/crabbox/cli.js';
12
+ /** Compact human duration: "45s", "12m", "2h", "1h 5m". Clamps negatives to 0. */
13
+ export declare function fmtDurationShort(secs: number): string;
14
+ /** Idle time since the box was last touched, e.g. "idle 5m" / "idle ?". */
15
+ export declare function fmtIdleShort(box: CrabboxBox, nowSecs: number): string;
16
+ /** Time until the lease expires, e.g. "expires 42m" / "expires ?" / "expired". */
17
+ export declare function fmtExpiresShort(box: CrabboxBox, nowSecs: number): string;
18
+ /** Reachable address for a leased box: tailnet FQDN/IP first, else public IP. */
19
+ export declare function boxAddress(box: CrabboxBox): string | undefined;
20
+ /** Human status: "ready" when usable, else the raw bootstrap state/status. */
21
+ export declare function boxStatus(box: CrabboxBox): string;
22
+ /**
23
+ * Warm boxes eligible for reuse: `ready` and the lease has not expired.
24
+ * Sorted most-recently-touched first so `--reuse` / the auto-pick lands on the
25
+ * freshest box (an untouched `lastTouchedAt` sorts last).
26
+ */
27
+ export declare function reusableBoxes(boxes: CrabboxBox[], nowSecs: number): CrabboxBox[];
28
+ /** One aligned row for the reuse picker / `agents lease list`. */
29
+ export declare function formatBoxRow(box: CrabboxBox, nowSecs: number): string;
11
30
  /** Validate a Hetzner token against the live API. Exported for unit tests (fetch injectable). */
12
31
  export declare function validateHetznerToken(token: string, fetchImpl?: typeof fetch): Promise<'valid' | 'invalid' | 'unreachable'>;
13
32
  /**
@@ -7,9 +7,9 @@
7
7
  * that have been untouched for a safety window are eligible (see `isReapSafe`),
8
8
  * so a box a concurrent run just reused is never stopped.
9
9
  */
10
- import { spawn } from 'child_process';
11
10
  import chalk from 'chalk';
12
- import { crabboxList, reapSafeOrphans, reapOrphans, setLeaseSecretsBundle } from '../lib/crabbox/cli.js';
11
+ import { openUrl } from '../lib/open-url.js';
12
+ import { crabboxList, crabboxStop, reapSafeOrphans, reapOrphans, setLeaseSecretsBundle } from '../lib/crabbox/cli.js';
13
13
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
14
14
  import { bundleExists, readBundle, writeBundle, keychainRef, bundleItemStore } from '../lib/secrets/bundles.js';
15
15
  import { secretsKeychainItem } from '../lib/secrets/index.js';
@@ -19,21 +19,104 @@ function fmtIdle(box) {
19
19
  const iso = new Date(box.lastTouchedAt * 1000).toISOString().slice(0, 16).replace('T', ' ');
20
20
  return `idle since ${iso}Z`;
21
21
  }
22
+ // ── Shared box helpers (consumed by exec.ts's reuse picker + ssh.ts's devices
23
+ // section, so the reuse/format logic lives in exactly one place) ─────────────
24
+ /** Compact human duration: "45s", "12m", "2h", "1h 5m". Clamps negatives to 0. */
25
+ export function fmtDurationShort(secs) {
26
+ const s = Math.max(0, Math.round(secs));
27
+ if (s < 60)
28
+ return `${s}s`;
29
+ const m = Math.round(s / 60);
30
+ if (m < 60)
31
+ return `${m}m`;
32
+ const h = Math.floor(m / 60);
33
+ const rem = m % 60;
34
+ return rem ? `${h}h ${rem}m` : `${h}h`;
35
+ }
36
+ /** Idle time since the box was last touched, e.g. "idle 5m" / "idle ?". */
37
+ export function fmtIdleShort(box, nowSecs) {
38
+ if (box.lastTouchedAt === null)
39
+ return 'idle ?';
40
+ return `idle ${fmtDurationShort(nowSecs - box.lastTouchedAt)}`;
41
+ }
42
+ /** Time until the lease expires, e.g. "expires 42m" / "expires ?" / "expired". */
43
+ export function fmtExpiresShort(box, nowSecs) {
44
+ if (box.expiresAt === null)
45
+ return 'expires ?';
46
+ const left = box.expiresAt - nowSecs;
47
+ return left <= 0 ? 'expired' : `expires ${fmtDurationShort(left)}`;
48
+ }
49
+ /** Reachable address for a leased box: tailnet FQDN/IP first, else public IP. */
50
+ export function boxAddress(box) {
51
+ return box.tailscaleFQDN || box.tailscaleIPv4 || box.ip || undefined;
52
+ }
53
+ /** Human status: "ready" when usable, else the raw bootstrap state/status. */
54
+ export function boxStatus(box) {
55
+ return box.ready ? 'ready' : box.state || box.status || 'pending';
56
+ }
57
+ /**
58
+ * Warm boxes eligible for reuse: `ready` and the lease has not expired.
59
+ * Sorted most-recently-touched first so `--reuse` / the auto-pick lands on the
60
+ * freshest box (an untouched `lastTouchedAt` sorts last).
61
+ */
62
+ export function reusableBoxes(boxes, nowSecs) {
63
+ return boxes
64
+ .filter((b) => b.ready && (b.expiresAt === null || b.expiresAt > nowSecs))
65
+ .sort((a, b) => (b.lastTouchedAt ?? 0) - (a.lastTouchedAt ?? 0));
66
+ }
67
+ /** One aligned row for the reuse picker / `agents lease list`. */
68
+ export function formatBoxRow(box, nowSecs) {
69
+ const slug = box.slug.padEnd(16);
70
+ const cls = (box.class ?? '?').padEnd(10);
71
+ const addr = (boxAddress(box) ?? '—').padEnd(24);
72
+ const status = boxStatus(box).padEnd(8);
73
+ const idle = fmtIdleShort(box, nowSecs).padEnd(12);
74
+ return `${slug} ${cls} ${addr} ${status} ${idle} ${fmtExpiresShort(box, nowSecs)}`;
75
+ }
22
76
  const HETZNER_BUNDLE = 'hetzner.com';
23
77
  const HCLOUD_KEY = 'HCLOUD_TOKEN';
24
78
  const HETZNER_CONSOLE_URL = 'https://console.hetzner.cloud/';
25
- /** Best-effort: open a URL in the user's default browser. Never throws. */
26
- function openUrl(url) {
27
- const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
28
- const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
79
+ const TAILSCALE_BUNDLE = 'tailscale.com';
80
+ const TAILSCALE_KEY = 'CRABBOX_TAILSCALE_AUTH_KEY';
81
+ const TAILSCALE_KEYS_URL = 'https://login.tailscale.com/admin/settings/keys';
82
+ /**
83
+ * Optional Tailscale setup for private-network leases (`--tailscale`). Collects
84
+ * an EPHEMERAL, pre-authorized, `tag:crabbox` auth key and stores it in the
85
+ * `tailscale.com` keychain bundle under `CRABBOX_TAILSCALE_AUTH_KEY` (the exact
86
+ * key `crabboxEnv` auto-injects). Blank input skips — public-IP leases still
87
+ * work with no Tailscale key. Never throws for cancel; mirrors the Hetzner
88
+ * capture above but does no live validation (Tailscale has no cheap probe).
89
+ */
90
+ async function captureTailscaleAuthKey() {
91
+ console.error(chalk.bold('\nOptional: private-network leases over Tailscale'));
92
+ console.error(chalk.dim('Mint an EPHEMERAL, pre-authorized auth key tagged `tag:crabbox` in the Tailscale admin,'));
93
+ console.error(chalk.dim('then paste it to reach leased boxes only over your tailnet (`--tailscale`). Leave blank to skip.\n'));
94
+ const { password } = await import('@inquirer/prompts');
95
+ let key;
29
96
  try {
30
- const p = spawn(cmd, args, { stdio: 'ignore', detached: true });
31
- p.on('error', () => { });
32
- p.unref();
97
+ openUrl(TAILSCALE_KEYS_URL);
98
+ key = (await password({ message: 'Paste a Tailscale auth key (blank to skip):', mask: true })).trim();
33
99
  }
34
- catch {
35
- /* best-effort */
100
+ catch (e) {
101
+ if (isPromptCancelled(e)) {
102
+ console.error(chalk.yellow('Skipped Tailscale setup.'));
103
+ return;
104
+ }
105
+ throw e;
106
+ }
107
+ if (!key) {
108
+ console.error(chalk.dim('Skipped Tailscale setup — public-IP leases only.'));
109
+ return;
36
110
  }
111
+ const bundle = bundleExists(TAILSCALE_BUNDLE)
112
+ ? readBundle(TAILSCALE_BUNDLE)
113
+ : { name: TAILSCALE_BUNDLE, description: 'Tailscale ephemeral auth key for crabbox tailnet leases', vars: {} };
114
+ const store = bundleItemStore(bundle.backend);
115
+ store.set(secretsKeychainItem(TAILSCALE_BUNDLE, TAILSCALE_KEY), key);
116
+ bundle.vars[TAILSCALE_KEY] = keychainRef(TAILSCALE_KEY);
117
+ writeBundle(bundle);
118
+ console.error(chalk.green(`✔ Stored Tailscale auth key in keychain bundle '${TAILSCALE_BUNDLE}'.`));
119
+ console.error(chalk.dim(' Add --tailscale to a lease (reuse defaults to it) to reach the box over your tailnet.'));
37
120
  }
38
121
  /** Validate a Hetzner token against the live API. Exported for unit tests (fetch injectable). */
39
122
  export async function validateHetznerToken(token, fetchImpl = fetch) {
@@ -103,6 +186,8 @@ export async function runLeaseSetup(opts = {}) {
103
186
  setLeaseSecretsBundle(HETZNER_BUNDLE);
104
187
  console.error(chalk.green(`\n✔ Stored in keychain bundle '${HETZNER_BUNDLE}' and set as the default lease provider.`));
105
188
  console.error(chalk.dim(' Run `agents run <agent> "…" --lease` — no env var, no flag needed.'));
189
+ // Also offer to capture a Tailscale auth key for private-network leases.
190
+ await captureTailscaleAuthKey();
106
191
  return true;
107
192
  }
108
193
  console.error(chalk.yellow('lease setup: no valid token after 3 attempts — aborted.'));
@@ -128,6 +213,48 @@ export function registerLeaseCommand(program) {
128
213
  const ok = await runLeaseSetup({ provider: opts.provider });
129
214
  process.exit(ok ? 0 : 1);
130
215
  });
216
+ lease
217
+ .command('list')
218
+ .alias('ls')
219
+ .description('List warm crabbox boxes you can reuse with `agents run --box <slug>`.')
220
+ .option('--json', 'Output JSON', false)
221
+ .action((opts) => {
222
+ const boxOpts = { secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE };
223
+ let boxes;
224
+ try {
225
+ boxes = crabboxList(boxOpts);
226
+ }
227
+ catch (e) {
228
+ console.error(chalk.red(`lease list: ${e.message}`));
229
+ process.exit(1);
230
+ return;
231
+ }
232
+ if (opts.json) {
233
+ console.log(JSON.stringify(boxes, null, 2));
234
+ return;
235
+ }
236
+ if (boxes.length === 0) {
237
+ console.error(chalk.gray('No crabbox boxes. `agents run <agent> "…" --lease` provisions one.'));
238
+ return;
239
+ }
240
+ const nowSecs = Math.floor(Date.now() / 1000);
241
+ console.log(chalk.bold(`Warm boxes (${boxes.length})`));
242
+ for (const b of boxes)
243
+ console.log(' ' + formatBoxRow(b, nowSecs));
244
+ console.log(chalk.gray(' Reuse: agents run <agent> "…" --box <slug> · Stop: agents lease stop <slug>'));
245
+ });
246
+ lease
247
+ .command('stop <slug>')
248
+ .description('Stop (release) a leased crabbox box now.')
249
+ .action((slug) => {
250
+ const boxOpts = { secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE };
251
+ const ok = crabboxStop(slug, boxOpts);
252
+ if (ok)
253
+ console.error(chalk.green(`Stopped box ${slug}.`));
254
+ else
255
+ console.error(chalk.red(`Could not stop box ${slug} (already gone, or crabbox is unavailable).`));
256
+ process.exit(ok ? 0 : 1);
257
+ });
131
258
  lease
132
259
  .command('gc')
133
260
  .description('Stop expired, idle lease boxes that are holding your provider quota. Safe: never stops a box in active use.')
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerLoginCommands(program: Command): void;
@@ -0,0 +1,123 @@
1
+ import chalk from 'chalk';
2
+ import * as fs from 'fs';
3
+ import { readStdinSync } from '../lib/format.js';
4
+ import { clearVaultKey, createVault, getVaultSession, joinVault, unlock, vaultExists, vaultPath, } from '../lib/secrets/vault.js';
5
+ import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
6
+ async function promptPassword(message) {
7
+ if (!isInteractiveTerminal()) {
8
+ throw new Error('A password is required. Run from a TTY or pass --password-stdin.');
9
+ }
10
+ const { password } = await import('@inquirer/prompts');
11
+ return password({ message, mask: true });
12
+ }
13
+ async function readPassword(opts, message) {
14
+ if (opts.passwordStdin) {
15
+ const value = readStdinSync();
16
+ if (!value)
17
+ throw new Error('No password received on stdin.');
18
+ return value.replace(/\r?\n$/, '');
19
+ }
20
+ return promptPassword(message);
21
+ }
22
+ async function chooseFreshLoginMode(opts) {
23
+ if (opts.create && opts.join)
24
+ throw new Error('--create and --join are mutually exclusive.');
25
+ if (opts.create)
26
+ return 'create';
27
+ if (opts.join)
28
+ return 'join';
29
+ if (!isInteractiveTerminal()) {
30
+ throw new Error('No synced secrets file exists. Pass --create or --join <vault.age> in a non-interactive shell.');
31
+ }
32
+ const { select } = await import('@inquirer/prompts');
33
+ return select({
34
+ message: 'No synced secrets login found. What do you want to do?',
35
+ choices: [
36
+ { name: 'Create a new login', value: 'create' },
37
+ { name: 'Join with an existing synced secrets file', value: 'join' },
38
+ ],
39
+ });
40
+ }
41
+ function formatRemaining(expiresAt) {
42
+ const ms = expiresAt - Date.now();
43
+ if (ms <= 0)
44
+ return 'expired';
45
+ const mins = Math.round(ms / 60000);
46
+ if (mins < 60)
47
+ return `${mins} minute${mins === 1 ? '' : 's'}`;
48
+ const hours = Math.round(mins / 60);
49
+ if (hours < 24)
50
+ return `${hours} hour${hours === 1 ? '' : 's'}`;
51
+ const days = Math.round(hours / 24);
52
+ return `${days} day${days === 1 ? '' : 's'}`;
53
+ }
54
+ export function registerLoginCommands(program) {
55
+ program
56
+ .command('login')
57
+ .description('Unlock synced secrets for this shell session')
58
+ .option('--create', 'Create a new encrypted synced-secrets file at ~/.agents/vault.age')
59
+ .option('--join <path>', 'Copy and unlock an existing vault.age file')
60
+ .option('--force', 'Replace an existing synced-secrets file when used with --create or --join')
61
+ .option('--password-stdin', 'Read the master password from stdin')
62
+ .action(async (opts) => {
63
+ try {
64
+ if (opts.force && !opts.create && !opts.join) {
65
+ throw new Error('--force can only be used with --create or --join.');
66
+ }
67
+ if (vaultExists() && !opts.create && !opts.join) {
68
+ const password = await readPassword(opts, 'Master password');
69
+ unlock(password);
70
+ const session = getVaultSession();
71
+ const ttl = session.loggedIn ? formatRemaining(session.expiresAt) : '8 hours';
72
+ console.log(chalk.green(`Logged in. Synced secrets unlocked for ${ttl}.`));
73
+ return;
74
+ }
75
+ const mode = await chooseFreshLoginMode(opts);
76
+ if (mode === 'join') {
77
+ let source = opts.join;
78
+ if (!source) {
79
+ if (!isInteractiveTerminal())
80
+ throw new Error('--join needs a vault.age path.');
81
+ const { input } = await import('@inquirer/prompts');
82
+ source = await input({
83
+ message: 'Path to existing vault.age',
84
+ validate: (value) => fs.existsSync(value) || 'File not found.',
85
+ });
86
+ }
87
+ const password = await readPassword(opts, 'Master password');
88
+ joinVault(password, source, { overwrite: opts.force });
89
+ console.log(chalk.green(`Logged in. Copied synced secrets file to ${vaultPath()}.`));
90
+ return;
91
+ }
92
+ const password = await readPassword(opts, 'Choose a master password');
93
+ createVault(password, { overwrite: opts.force });
94
+ console.log(chalk.green(`Logged in. Created synced secrets file at ${vaultPath()}.`));
95
+ }
96
+ catch (err) {
97
+ if (isPromptCancelled(err))
98
+ return;
99
+ console.error(chalk.red(err.message));
100
+ process.exit(1);
101
+ }
102
+ });
103
+ program
104
+ .command('logout')
105
+ .description('Forget the cached synced-secrets key')
106
+ .action(() => {
107
+ clearVaultKey();
108
+ console.log(chalk.green('Logged out. Synced secrets are locked.'));
109
+ });
110
+ program
111
+ .command('whoami')
112
+ .description('Show synced-secrets login status')
113
+ .action(() => {
114
+ const session = getVaultSession();
115
+ if (!session.loggedIn) {
116
+ console.log(chalk.yellow('Not logged in.'));
117
+ console.log(chalk.gray(`File: ${vaultPath()}${vaultExists() ? '' : ' (not created)'}`));
118
+ return;
119
+ }
120
+ console.log(chalk.green(`Logged in (${formatRemaining(session.expiresAt)} remaining).`));
121
+ console.log(chalk.gray(`File: ${vaultPath()}`));
122
+ });
123
+ }