@phnx-labs/agents-cli 1.22.115 → 1.22.117
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 +161 -14
- package/README.md +1 -1
- package/dist/commands/browser.js +11 -0
- package/dist/commands/exec.d.ts +18 -0
- package/dist/commands/exec.js +263 -152
- package/dist/commands/feed.js +65 -14
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +88 -7
- package/dist/commands/sessions.d.ts +21 -2
- package/dist/commands/sessions.js +157 -3
- package/dist/commands/setup-secrets.d.ts +2 -2
- package/dist/commands/setup-term.d.ts +25 -0
- package/dist/commands/setup-term.js +71 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +12 -4
- package/dist/commands/ssh.js +1 -69
- package/dist/lib/accounting/rotate.d.ts +63 -1
- package/dist/lib/accounting/rotate.js +56 -0
- package/dist/lib/accounts/add.js +2 -2
- package/dist/lib/accounts/slots.js +32 -2
- package/dist/lib/answer-router.d.ts +11 -2
- package/dist/lib/answer-router.js +26 -2
- package/dist/lib/auth-mint.d.ts +5 -4
- package/dist/lib/auth-mint.js +4 -3
- package/dist/lib/browser/drivers/arc.d.ts +1 -1
- package/dist/lib/browser/service.d.ts +10 -0
- package/dist/lib/browser/service.js +208 -36
- package/dist/lib/browser/types.d.ts +18 -0
- package/dist/lib/config-keys.d.ts +1 -1
- package/dist/lib/config-keys.js +5 -0
- package/dist/lib/device-config.js +61 -0
- package/dist/lib/devices/doctor-findings.js +2 -6
- package/dist/lib/feed/answer.d.ts +153 -4
- package/dist/lib/feed/answer.js +716 -105
- package/dist/lib/feed/feed.d.ts +61 -1
- package/dist/lib/feed/feed.js +226 -14
- package/dist/lib/feed/hub-server.d.ts +58 -3
- package/dist/lib/feed/hub-server.js +306 -54
- package/dist/lib/feed/pr-status.d.ts +8 -0
- package/dist/lib/feed/pr-status.js +9 -1
- package/dist/lib/feed-outcome.d.ts +1 -1
- package/dist/lib/feed-outcome.js +9 -2
- package/dist/lib/feed-policy.js +9 -3
- package/dist/lib/fleet/auth-sync.d.ts +2 -55
- package/dist/lib/fleet/auth-sync.js +2 -89
- package/dist/lib/harness-auth-capabilities.js +7 -2
- package/dist/lib/hosts/dispatch.d.ts +20 -1
- package/dist/lib/hosts/dispatch.js +52 -30
- package/dist/lib/hosts/remote-cmd.d.ts +21 -0
- package/dist/lib/hosts/remote-cmd.js +27 -2
- package/dist/lib/mailbox.d.ts +12 -0
- package/dist/lib/mailbox.js +16 -2
- package/dist/lib/menubar/snapshot.d.ts +51 -0
- package/dist/lib/menubar/snapshot.js +42 -3
- package/dist/lib/open-url.js +2 -2
- package/dist/lib/placement.d.ts +2 -0
- package/dist/lib/placement.js +5 -0
- package/dist/lib/projects.d.ts +23 -0
- package/dist/lib/projects.js +78 -0
- package/dist/lib/secrets-cli.d.ts +3 -3
- package/dist/lib/secrets-cli.js +1 -1
- package/dist/lib/session/active.d.ts +1 -0
- package/dist/lib/session/active.js +8 -0
- package/dist/lib/session/db.d.ts +67 -3
- package/dist/lib/session/db.js +381 -126
- package/dist/lib/session/prompt.d.ts +23 -7
- package/dist/lib/session/prompt.js +46 -8
- package/dist/lib/session/remote/remote-list.d.ts +20 -0
- package/dist/lib/session/remote/remote-list.js +22 -6
- package/dist/lib/session/remote/watch.d.ts +12 -0
- package/dist/lib/session/remote/watch.js +9 -0
- package/dist/lib/session/remote-preview-cache.d.ts +29 -0
- package/dist/lib/session/remote-preview-cache.js +373 -0
- package/dist/lib/session/tail.d.ts +50 -0
- package/dist/lib/session/tail.js +219 -0
- package/dist/lib/setup-tool-install.js +2 -1
- package/dist/lib/setup-tool-status.d.ts +1 -1
- package/dist/lib/setup-tool-status.js +7 -1
- package/dist/lib/signin-badge.d.ts +19 -4
- package/dist/lib/signin-badge.js +29 -11
- package/dist/lib/term-driver.d.ts +24 -0
- package/dist/lib/term-driver.js +36 -0
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/index.js +1 -1
- package/dist/lib/terminal/inject.d.ts +38 -0
- package/dist/lib/terminal/inject.js +55 -9
- package/dist/lib/terminal/transport.d.ts +15 -5
- package/dist/lib/terminal/transport.js +61 -11
- package/package.json +1 -1
- package/dist/lib/fleet/remote-login.d.ts +0 -170
- package/dist/lib/fleet/remote-login.js +0 -568
package/dist/commands/feed.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { ensureFeedPublishHook, listAskStats, listBlocks, recordNotified, buildDeclaredBlock, publishBlock, } from '../lib/feed/feed.js';
|
|
2
|
+
import { ensureFeedPublishHook, listAskStats, listBlocks, recordNotified, buildDeclaredBlock, deriveBlockState, publishBlock, } from '../lib/feed/feed.js';
|
|
3
3
|
import { ensureActivityLogHook, readRecentActivity, formatActivityLine, formatProgressUpdate, mergeActivityEvents, parseActivityPayload, } from '../lib/feed/activity.js';
|
|
4
4
|
import { projectKeyFromCwd } from '../lib/project-key.js';
|
|
5
5
|
import { postFeedStatus } from '../lib/feed-post.js';
|
|
@@ -16,7 +16,7 @@ import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
|
|
|
16
16
|
import { loadPolicy, applyPolicyToBlock, isPhoneUrgent } from '../lib/feed-policy.js';
|
|
17
17
|
import { notifyUrgentBlock } from '../lib/notify.js';
|
|
18
18
|
import { registerFeedWatchCommand } from './feed-watch.js';
|
|
19
|
-
import { claimAndRouteAttentionAnswer, forwardFeedAnswer } from '../lib/feed/answer.js';
|
|
19
|
+
import { AnswerError, answerOwnerIsLocal, checkAnswerDelivery, claimAndRouteAttentionAnswer, forwardFeedAnswer, parseAttentionKey, } from '../lib/feed/answer.js';
|
|
20
20
|
import { gcMailbox } from '../lib/mailbox-gc.js';
|
|
21
21
|
import { isValidMailboxId } from '../lib/mailbox.js';
|
|
22
22
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
@@ -171,7 +171,10 @@ function renderBlock(b, localHost, indent = '') {
|
|
|
171
171
|
const cost = b.costOfDelay ? chalk.gray(`cost:${b.costOfDelay}`) : '';
|
|
172
172
|
const rank = b.delayRank ? chalk.gray(`rank:${Math.round(b.delayRank.score)}`) : '';
|
|
173
173
|
// Shared fleet-comms glyphs: ▲ open ask, ✓ answered (see comms-render GLYPH).
|
|
174
|
-
|
|
174
|
+
// A PENDING claim carries an answer record while the block is still open, so
|
|
175
|
+
// the delivered glyph is driven by the canonical state, not by `b.answer`
|
|
176
|
+
// being set — a claimed-but-undelivered ask must keep reading as needs-you.
|
|
177
|
+
const marker = deriveBlockState(b) !== 'open'
|
|
175
178
|
? chalk.green(GLYPH.delivered)
|
|
176
179
|
: b.kind === 'control'
|
|
177
180
|
? chalk.red('!')
|
|
@@ -197,7 +200,9 @@ function renderBlock(b, localHost, indent = '') {
|
|
|
197
200
|
if (b.answer) {
|
|
198
201
|
const verified = b.answer.verified ? chalk.green(GLYPH.delivered) : chalk.yellow('?');
|
|
199
202
|
const who = b.answer.answeredFrom + (b.answer.answeredBy ? ` (${b.answer.answeredBy})` : '');
|
|
200
|
-
|
|
203
|
+
const pending = deriveBlockState(b) === 'open';
|
|
204
|
+
const label = pending ? chalk.yellow('claimed (delivery unconfirmed)') : chalk.green('answered');
|
|
205
|
+
console.log(`${indent} ${label} by ${who} ${verified}`);
|
|
201
206
|
}
|
|
202
207
|
if (b.parkedAt) {
|
|
203
208
|
console.log(`${indent} ${chalk.red('hard-parked')} ${relTime(b.parkedAt)}`);
|
|
@@ -314,6 +319,23 @@ export async function loadSessionMetasForFeedEnrichment(load) {
|
|
|
314
319
|
throw err;
|
|
315
320
|
}
|
|
316
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* One human line per answer outcome. The three states the operator has to tell
|
|
324
|
+
* apart are a real receipt, a hand-off no rail can confirm, and a refusal —
|
|
325
|
+
* never collapsed into a single "Delivered" (PHNX-3999).
|
|
326
|
+
*/
|
|
327
|
+
function renderAnswerResult(result) {
|
|
328
|
+
switch (result.status) {
|
|
329
|
+
case 'failed':
|
|
330
|
+
return `Not delivered: ${result.reason ?? 'unknown failure'} — answering again is safe.`;
|
|
331
|
+
case 'unknown':
|
|
332
|
+
return `Delivery unconfirmed: ${result.reason ?? 'no rail reported a receipt.'} Run \`agents feed answer ${result.attentionKey} --check\` rather than resending.`;
|
|
333
|
+
case 'already_answered':
|
|
334
|
+
return `Already answered — ${result.receipt?.status ?? 'claimed'}${result.receipt ? ` as ${result.receipt.msgId}` : ''} at ${result.receipt?.at ?? result.attempt}.`;
|
|
335
|
+
default:
|
|
336
|
+
return `Delivered ${result.receipt?.msgId} (${result.receipt?.status}).`;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
317
339
|
export function registerFeedCommand(program) {
|
|
318
340
|
const feed = program
|
|
319
341
|
.command('feed')
|
|
@@ -334,23 +356,52 @@ export function registerFeedCommand(program) {
|
|
|
334
356
|
.option('--choice <choice-id>', 'Stable choice id from the attention item')
|
|
335
357
|
.option('--text <answer>', 'Free-text answer')
|
|
336
358
|
.option('--as <operator>', 'Verified operator id for high-consequence answers')
|
|
359
|
+
.option('--check', 'Read-only: report this item\'s delivery state without claiming, routing or resending')
|
|
360
|
+
.option('--attempt <at>', 'With --check: the attempt timestamp being checked, so a newer one is reported as such')
|
|
337
361
|
.option('--json', 'Emit the delivery result as JSON')
|
|
338
362
|
.action(async (attentionKey, opts, invoked) => {
|
|
363
|
+
const wantsJson = Boolean(opts.json || invoked.parent?.opts()?.json);
|
|
339
364
|
try {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (opts.
|
|
365
|
+
// The key's own host decides the rail; `answerOwnerIsLocal` normalizes
|
|
366
|
+
// both sides so a `.local` suffix is not read as a different machine.
|
|
367
|
+
const { host: ownerHost } = parseAttentionKey(attentionKey);
|
|
368
|
+
const local = answerOwnerIsLocal(ownerHost);
|
|
369
|
+
if (opts.check && (opts.choice != null || opts.text != null)) {
|
|
370
|
+
throw new AnswerError('--check is read-only; it takes no --choice or --text.', 'empty_answer');
|
|
371
|
+
}
|
|
372
|
+
if (opts.attempt != null && !opts.check) {
|
|
373
|
+
throw new AnswerError('--attempt only applies to --check.', 'empty_answer');
|
|
374
|
+
}
|
|
375
|
+
const result = opts.check
|
|
376
|
+
? (local
|
|
377
|
+
? checkAnswerDelivery(attentionKey, undefined, opts.attempt)
|
|
378
|
+
: await forwardFeedAnswer({ host: ownerHost, attentionKey, check: true, attempt: opts.attempt, operatorId: opts.as }))
|
|
379
|
+
: local
|
|
380
|
+
? await claimAndRouteAttentionAnswer({
|
|
381
|
+
attentionKey, choiceId: opts.choice, text: opts.text,
|
|
382
|
+
operator: { id: opts.as, verified: Boolean(opts.as), label: opts.as },
|
|
383
|
+
})
|
|
384
|
+
: await forwardFeedAnswer({ host: ownerHost, attentionKey, choiceId: opts.choice, text: opts.text, operatorId: opts.as });
|
|
385
|
+
if (wantsJson)
|
|
348
386
|
console.log(JSON.stringify(result));
|
|
349
387
|
else
|
|
350
|
-
console.log(
|
|
388
|
+
console.log(renderAnswerResult(result));
|
|
351
389
|
}
|
|
352
390
|
catch (error) {
|
|
353
|
-
|
|
391
|
+
// A failure is a first-class outcome for the operator UI, not a parse
|
|
392
|
+
// error: `--json` still gets a result object it can branch on, with a
|
|
393
|
+
// non-zero exit so a scripted caller sees the failure too.
|
|
394
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
395
|
+
if (wantsJson) {
|
|
396
|
+
const failure = {
|
|
397
|
+
status: 'failed', delivery: 'failed', resolved: false, reason: message,
|
|
398
|
+
code: error instanceof AnswerError ? error.code : 'rail_failed', attentionKey,
|
|
399
|
+
};
|
|
400
|
+
console.log(JSON.stringify(failure));
|
|
401
|
+
process.exitCode = 1;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
invoked.error(`error: ${message}`);
|
|
354
405
|
}
|
|
355
406
|
});
|
|
356
407
|
feed
|
|
@@ -153,6 +153,17 @@ export interface SessionPreviewDigest {
|
|
|
153
153
|
firstError?: string;
|
|
154
154
|
toolHistogram: ReturnType<typeof toolHistogram>;
|
|
155
155
|
test: ReturnType<typeof detectTestResult>;
|
|
156
|
+
/**
|
|
157
|
+
* True when this digest was built WITHOUT parsing the transcript (PHNX-3999):
|
|
158
|
+
* the file exceeded {@link PREVIEW_DIGEST_MAX_PARSE_BYTES} and no cached
|
|
159
|
+
* digest existed yet. `firstUser`/`lastAssistant` fall back to the already-
|
|
160
|
+
* indexed `SessionMeta.firstUserMessage`/`lastUserMessage` (cheap, no parse)
|
|
161
|
+
* rather than being silently empty; every event-derived field (toolCalls,
|
|
162
|
+
* changedFiles, artifacts, etc.) stays at its zero-value default because it
|
|
163
|
+
* genuinely was not computed, not because nothing happened.
|
|
164
|
+
*/
|
|
165
|
+
partial?: boolean;
|
|
166
|
+
partialReason?: string;
|
|
156
167
|
}
|
|
157
168
|
/** Fold a harness-normalized event stream into the stable preview data model. */
|
|
158
169
|
export declare function buildSessionPreviewDigest(events: SessionEvent[], session: SessionMeta): SessionPreviewDigest;
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
|
-
import { truncate, humanDuration } from '../lib/format.js';
|
|
11
|
+
import { truncate, humanDuration, formatBytes } from '../lib/format.js';
|
|
12
12
|
import { sessionDisplayAgent } from '../lib/session/types.js';
|
|
13
13
|
import { fetchPeerPreviewDigest } from '../lib/session/remote-list.js';
|
|
14
14
|
import { parseSession, sanitizeForTerminal, SNAPSHOT_TODO_TOOLS } from '../lib/session/parse.js';
|
|
15
|
+
import { readSessionTail, readSessionHead } from '../lib/session/tail.js';
|
|
15
16
|
import { safeTeamText } from '../lib/session/team-filter.js';
|
|
16
|
-
import { cleanSessionPrompt, extractSessionTopic, isSyntheticUserMessage } from '../lib/session/prompt.js';
|
|
17
|
+
import { cleanSessionPrompt, extractSessionTopic, isSyntheticUserMessage, firstUserMessageFromEvents } from '../lib/session/prompt.js';
|
|
17
18
|
import { linkPath, linkUrl, relativeToCwd, shortenModel } from '../lib/session/render.js';
|
|
18
19
|
import { linearIssueUrl } from '../lib/session/linear.js';
|
|
19
20
|
import { extractTodoProgress, WORKTREE_RE } from '../lib/session/state.js';
|
|
@@ -324,7 +325,14 @@ export function loadSessionPreviewDigest(session) {
|
|
|
324
325
|
archived.plugins = getSessionPlugins(session.id);
|
|
325
326
|
return { digest: archived, events: [] };
|
|
326
327
|
}
|
|
327
|
-
|
|
328
|
+
// No file on disk AND no archived digest: a metadata-only row (a Rush
|
|
329
|
+
// dispatch/audit row, a synthesized attach-only entry, a live session not
|
|
330
|
+
// yet indexed) with nothing to read (PHNX-3999). Callers that only
|
|
331
|
+
// destructure `digest` (the picker's "not indexed here" note) are
|
|
332
|
+
// unaffected; a JSON caller reading `error` gets a truthful reason instead
|
|
333
|
+
// of a `preview: null` that looks identical to "this session genuinely has
|
|
334
|
+
// no content yet".
|
|
335
|
+
return { events: [], error: 'no local transcript for this session (metadata-only entry, no archived digest)' };
|
|
328
336
|
}
|
|
329
337
|
const safe = sanitizeMeta(session);
|
|
330
338
|
let events = [];
|
|
@@ -340,9 +348,53 @@ export function loadSessionPreviewDigest(session) {
|
|
|
340
348
|
fileSize: sourceStamp.size,
|
|
341
349
|
});
|
|
342
350
|
if (!digest) {
|
|
343
|
-
|
|
344
|
-
|
|
351
|
+
if (sourceStamp.size > PREVIEW_DIGEST_MAX_PARSE_BYTES) {
|
|
352
|
+
// A full `parseSession` on a cache miss is a synchronous, unbounded
|
|
353
|
+
// whole-file parse with no time/byte cap of its own (PHNX-3999) — real
|
|
354
|
+
// 6.3 MiB and 35.7 MiB screenshot-heavy transcripts on this fleet both
|
|
355
|
+
// lacked a computed digest/timeline, consistent with this path not
|
|
356
|
+
// finishing in a reasonable request budget for either. `PREVIEW_DIGEST_MAX_PARSE_BYTES`
|
|
357
|
+
// is deliberately smaller than the daemon's own background-work ceiling
|
|
358
|
+
// (see that constant's own doc) so this catches both real cases.
|
|
359
|
+
//
|
|
360
|
+
// This IS genuinely partial, not empty: rather than parsing nothing,
|
|
361
|
+
// `readSessionTail` (`tail.ts`) reads only the LAST 128 KiB of the file
|
|
362
|
+
// (already the live-view's own bounded reader, reused verbatim — no new
|
|
363
|
+
// parse logic) for a real recent-events window on the two harnesses it
|
|
364
|
+
// supports (Claude/Codex); event-derived fields below (toolCalls,
|
|
365
|
+
// toolTags, etc.) reflect that tail window, not the whole session, which
|
|
366
|
+
// `partialReason` states explicitly.
|
|
367
|
+
//
|
|
368
|
+
// `firstUser` is NEVER set from the tail fold: a tail window's "first
|
|
369
|
+
// user message IN THAT WINDOW" is a mid-session follow-up on any
|
|
370
|
+
// multi-turn session, not the session's actual original request, and
|
|
371
|
+
// there is no honest way to tell the two apart from the tail alone.
|
|
372
|
+
// Falling back to it would silently mislabel a follow-up as the
|
|
373
|
+
// original ask. Preference order for the CANONICAL original request:
|
|
374
|
+
// (1) the already-indexed `SessionMeta.firstUserMessage` (zero
|
|
375
|
+
// extra I/O); (2) failing that, a bounded HEAD read (`readSessionHead`,
|
|
376
|
+
// `tail.ts` — the mirror of the tail reader, first ~32 KiB from byte 0,
|
|
377
|
+
// where a session's opening turn always lives) so a row with no indexed
|
|
378
|
+
// value yet still gets the REAL original request rather than nothing.
|
|
379
|
+
// Only when neither is available does `firstUser` stay empty —
|
|
380
|
+
// `partial`/`partialReason` already say why detail is missing, which is
|
|
381
|
+
// the truthful signal, never a guessed value. The digest is cached
|
|
382
|
+
// against this stamp so the bound is paid once per transcript version,
|
|
383
|
+
// not once per call.
|
|
384
|
+
events = readSessionTail(session.filePath, session.agent);
|
|
345
385
|
digest = buildSessionPreviewDigest(events, safe);
|
|
386
|
+
if (session.firstUserMessage) {
|
|
387
|
+
digest.firstUser = session.firstUserMessage;
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
// Reuse the canonical extractor (rejects synthetic/system-injected
|
|
391
|
+
// turns, unwraps a Grok/Cursor <user_query> wrapper) rather than a
|
|
392
|
+
// bespoke inline find — the same rules SessionMeta.firstUserMessage
|
|
393
|
+
// itself was built with.
|
|
394
|
+
digest.firstUser = firstUserMessageFromEvents(readSessionHead(session.filePath, session.agent)) ?? '';
|
|
395
|
+
}
|
|
396
|
+
digest.partial = true;
|
|
397
|
+
digest.partialReason = `transcript is ${formatBytes(sourceStamp.size)}, over the ${formatBytes(PREVIEW_DIGEST_MAX_PARSE_BYTES)} bounded-parse limit for an uncached preview; digest reflects only the last ~128 KiB (tail) of the transcript, not the whole session`;
|
|
346
398
|
writeSessionPreviewCache({
|
|
347
399
|
id: session.id,
|
|
348
400
|
fileMtimeMs: sourceStamp.mtimeMs,
|
|
@@ -350,8 +402,20 @@ export function loadSessionPreviewDigest(session) {
|
|
|
350
402
|
preview: digest,
|
|
351
403
|
});
|
|
352
404
|
}
|
|
353
|
-
|
|
354
|
-
|
|
405
|
+
else {
|
|
406
|
+
try {
|
|
407
|
+
events = parseSession(session.filePath, session.agent);
|
|
408
|
+
digest = buildSessionPreviewDigest(events, safe);
|
|
409
|
+
writeSessionPreviewCache({
|
|
410
|
+
id: session.id,
|
|
411
|
+
fileMtimeMs: sourceStamp.mtimeMs,
|
|
412
|
+
fileSize: sourceStamp.size,
|
|
413
|
+
preview: digest,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
return { events, error: sanitizeForTerminal(err?.message ?? String(err)) };
|
|
418
|
+
}
|
|
355
419
|
}
|
|
356
420
|
}
|
|
357
421
|
digest.plugins = getSessionPlugins(session.id);
|
|
@@ -711,6 +775,23 @@ const DIRS_TOUCHED_MAX = 5;
|
|
|
711
775
|
// enough to cover any real session's edits, low enough that a runaway rewrite
|
|
712
776
|
// can't bloat the cached JSON. The `changes` counts stay the true totals.
|
|
713
777
|
const CHANGED_FILES_MAX = 200;
|
|
778
|
+
/**
|
|
779
|
+
* Bound for an uncached `loadSessionPreviewDigest` parse (PHNX-3999).
|
|
780
|
+
*
|
|
781
|
+
* Deliberately SMALLER than the daemon's own
|
|
782
|
+
* `TIMELINE_PASS_MAX_WHOLE_FILE_BYTES` (16 MiB, `timeline-pass.ts`) — that
|
|
783
|
+
* number bounds BACKGROUND work the daemon tick can afford to spend; this one
|
|
784
|
+
* bounds a SYNCHRONOUS request an interactive caller (the Menu, with its own
|
|
785
|
+
* end-to-end latency budget) is actively waiting on. Two real screenshot-heavy
|
|
786
|
+
* transcripts on this fleet (6.3 MiB and 35.7 MiB) both lacked any computed
|
|
787
|
+
* digest/timeline — a 16 MiB threshold here would still miss the smaller one.
|
|
788
|
+
* The exact per-byte cost of `parseSession` is not measured in this change;
|
|
789
|
+
* 4 MiB is a conservative invariant (well under the 6.3 MiB failure case,
|
|
790
|
+
* comfortably above ordinary non-screenshot transcript sizes), not a timing
|
|
791
|
+
* guarantee — verify against real transcripts before relying on a specific
|
|
792
|
+
* elapsed-time bound.
|
|
793
|
+
*/
|
|
794
|
+
const PREVIEW_DIGEST_MAX_PARSE_BYTES = 4 * 1024 * 1024;
|
|
714
795
|
/** Fold a harness-normalized event stream into the stable preview data model. */
|
|
715
796
|
export function buildSessionPreviewDigest(events, session) {
|
|
716
797
|
let firstUser = '';
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { type Command } from 'commander';
|
|
2
2
|
import { type ProjectDef } from '../lib/projects.js';
|
|
3
|
-
import type { SessionAgentId, SessionMeta, ViewMode } from '../lib/session/types.js';
|
|
3
|
+
import type { SessionAgentId, SessionEvent, SessionMeta, ViewMode } from '../lib/session/types.js';
|
|
4
4
|
import { type ActiveSession } from '../lib/session/active.js';
|
|
5
5
|
export { activeSessionProjectKey, backfillActiveRowsFromIndex, backfillActiveRowsFromMeta, isRunningLiveSession, serializeActiveSessionsForJson, serializeSessionsJson, type BackfillMeta } from '../lib/session/active.js';
|
|
6
6
|
import { loadLocalActiveSessions } from '../lib/session/session-cache.js';
|
|
7
7
|
import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
|
|
8
8
|
import { type RemoteAgentsJsonParseResult } from '../lib/remote-agents-json.js';
|
|
9
9
|
import { type RunMeta } from '../lib/scheduling/routines.js';
|
|
10
|
-
import { type PickedSession } from './sessions-picker.js';
|
|
10
|
+
import { type PickedSession, type SessionPreviewDigest } from './sessions-picker.js';
|
|
11
11
|
import { type ComputerRunRow } from '../lib/computer/sessions-list.js';
|
|
12
12
|
import { type ToolSearchEnvelope, type ToolProgramCountEnvelope } from '../lib/session/tool-index.js';
|
|
13
13
|
interface SessionFilterOptions {
|
|
@@ -466,6 +466,23 @@ export declare function printRoutineDrilldown(drill: RoutineDrilldown, liveIndex
|
|
|
466
466
|
hiddenCount?: number;
|
|
467
467
|
hiddenUnmanaged?: number;
|
|
468
468
|
}): void;
|
|
469
|
+
export interface SessionDetailMessage {
|
|
470
|
+
role: 'user' | 'assistant';
|
|
471
|
+
text: string;
|
|
472
|
+
at: string | null;
|
|
473
|
+
}
|
|
474
|
+
export declare function buildSessionDetailBlock(session: SessionMeta, digest: SessionPreviewDigest | undefined, events: SessionEvent[], sourceStamp?: {
|
|
475
|
+
fileMtimeMs: number;
|
|
476
|
+
fileSize: number;
|
|
477
|
+
}): {
|
|
478
|
+
request: unknown;
|
|
479
|
+
timeline: unknown;
|
|
480
|
+
files: unknown;
|
|
481
|
+
messages: SessionDetailMessage[];
|
|
482
|
+
sourceRevision: string | null;
|
|
483
|
+
partial: boolean;
|
|
484
|
+
reason: string | null;
|
|
485
|
+
};
|
|
469
486
|
/** Resolve a session by id/query globally and print its compact preview (no pager).
|
|
470
487
|
* Backs `--preview` — the fast path for the "peek before resume" hot loop. */
|
|
471
488
|
export declare function renderSessionPreview(query: string, scope: {
|
|
@@ -474,6 +491,8 @@ export declare function renderSessionPreview(query: string, scope: {
|
|
|
474
491
|
local?: boolean;
|
|
475
492
|
hosts?: string[];
|
|
476
493
|
json?: boolean;
|
|
494
|
+
refresh?: boolean;
|
|
495
|
+
revision?: string;
|
|
477
496
|
}): Promise<void>;
|
|
478
497
|
/**
|
|
479
498
|
* The one-line live status banner shown above a session preview: the glyph, the
|
|
@@ -35,7 +35,9 @@ import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
|
|
|
35
35
|
import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/session/width.js';
|
|
36
36
|
import { inferSessionState } from '../lib/session/state.js';
|
|
37
37
|
import { discoverSessions, queryIndexedSessions, countSessionsInScope, resolveSessionById, isCompleteSessionId, looksLikeSessionId, searchContentIndex, getSessionRoots, scopeToManaged } from '../lib/session/discover.js';
|
|
38
|
-
import { findSessionsById, querySessions, readSessionContent, readArchivedSessionPreview } from '../lib/session/db.js';
|
|
38
|
+
import { findSessionsById, querySessions, readSessionContent, readArchivedSessionPreview, readSessionTimelineAny } from '../lib/session/db.js';
|
|
39
|
+
import { foldTimeline, emptyTimelineState, projectTimeline, projectSessionFiles } from '../lib/session/timeline.js';
|
|
40
|
+
import { readSessionTail } from '../lib/session/tail.js';
|
|
39
41
|
import { liveSessionMetas, fleetExecutionMachineById, reconcileLiveMetaMachine } from '../lib/session/live-metadata.js';
|
|
40
42
|
import { sessionHeadline } from '../lib/session/title.js';
|
|
41
43
|
import { filterTeamSessions, shouldShowTeamSessions, safeTeamText, groupSessionsByTeam, NO_TEAM_GROUP_KEY, } from '../lib/session/team-filter.js';
|
|
@@ -1982,9 +1984,151 @@ function canonicalSessionsCommand(query, options) {
|
|
|
1982
1984
|
a.push(JSON.stringify(q));
|
|
1983
1985
|
return 'ag ' + a.join(' ');
|
|
1984
1986
|
}
|
|
1987
|
+
/** Bound on the `details.messages` array and on each message's `text` — this
|
|
1988
|
+
* rides a cached JSON payload (and, for the remote fast path, an SSH capture
|
|
1989
|
+
* with its own byte cap), so it is capped independently of either. */
|
|
1990
|
+
const SESSION_DETAIL_MAX_MESSAGES = 8;
|
|
1991
|
+
const SESSION_DETAIL_MESSAGE_MAX_CHARS = 4_000;
|
|
1992
|
+
/**
|
|
1993
|
+
* The additive `details` block on `sessions preview --json` (PHNX-3999):
|
|
1994
|
+
* `request`/`timeline`/`files` are read FIRST from the existing
|
|
1995
|
+
* `session_timelines` projection (`readSessionTimelineAny` — the daemon's own
|
|
1996
|
+
* bounded, incrementally-folded cache, `SessionTimelineProjection` in
|
|
1997
|
+
* `lib/session/db.ts`), so a session the daemon has already reached costs
|
|
1998
|
+
* nothing extra here. When that projection is missing (daemon hasn't reached
|
|
1999
|
+
* this session yet), this does an ON-DEMAND fold using the EXACT same pure,
|
|
2000
|
+
* already-redacting pipeline the daemon uses (`foldTimeline` /
|
|
2001
|
+
* `projectTimeline` / `projectSessionFiles`, `timeline.ts`) over whatever
|
|
2002
|
+
* events are cheaply available: the fresh-parse `events` from
|
|
2003
|
+
* `loadSessionPreviewDigest` when there is one, else a bounded 128 KiB tail
|
|
2004
|
+
* read (`readSessionTail`, Claude/Codex only — the same bounded reader the
|
|
2005
|
+
* local preview's over-cap fallback uses) so a WARM cache hit (no fresh parse)
|
|
2006
|
+
* still gets real, on-demand content instead of falling back to a 2-message
|
|
2007
|
+
* digest summary. This is genuinely bounded either way: it folds only events
|
|
2008
|
+
* already in hand or a fixed 128 KiB window, never a fresh whole-file parse.
|
|
2009
|
+
* `partial`/`reason` say when this happened (`daemon-pending: on-demand
|
|
2010
|
+
* bounded fold`) versus a session with no transcript at all.
|
|
2011
|
+
*/
|
|
2012
|
+
function sessionTranscriptStamp(session) {
|
|
2013
|
+
try {
|
|
2014
|
+
if (!session.filePath)
|
|
2015
|
+
return undefined;
|
|
2016
|
+
const stat = fs.statSync(session.filePath);
|
|
2017
|
+
return { fileMtimeMs: stat.mtimeMs, fileSize: stat.size };
|
|
2018
|
+
}
|
|
2019
|
+
catch {
|
|
2020
|
+
return undefined;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
export function buildSessionDetailBlock(session, digest, events, sourceStamp) {
|
|
2024
|
+
const bound = (text) => redactSecrets(sanitizeForTerminal(text)).slice(0, SESSION_DETAIL_MESSAGE_MAX_CHARS);
|
|
2025
|
+
const stamp = sourceStamp ?? sessionTranscriptStamp(session);
|
|
2026
|
+
const canDateEvents = sourceStamp !== undefined || events.length === 0;
|
|
2027
|
+
const daemonProjection = stamp ? readSessionTimelineAny(session.id, stamp) : undefined;
|
|
2028
|
+
// A bounded tail read backs `messages` on EVERY warm cache hit (no fresh
|
|
2029
|
+
// parse this call), REGARDLESS of whether the daemon projection is already
|
|
2030
|
+
// present — this is what fixes a real bug review caught: a warm digest hit
|
|
2031
|
+
// whose daemon timeline HAD landed still fell through to the 2-message
|
|
2032
|
+
// digest summary below, because the tail-read used to be gated on the
|
|
2033
|
+
// on-demand-fold branch only. `messages` and the on-demand fold are two
|
|
2034
|
+
// independent uses of the same cheap, bounded events window.
|
|
2035
|
+
let foldEvents = events;
|
|
2036
|
+
if (foldEvents.length === 0 && session.filePath) {
|
|
2037
|
+
foldEvents = readSessionTail(session.filePath, session.agent);
|
|
2038
|
+
}
|
|
2039
|
+
let projection = daemonProjection;
|
|
2040
|
+
let onDemand = false;
|
|
2041
|
+
if (!projection && foldEvents.length > 0) {
|
|
2042
|
+
const state = foldTimeline(foldEvents, emptyTimelineState());
|
|
2043
|
+
projection = {
|
|
2044
|
+
request: state.request,
|
|
2045
|
+
timeline: projectTimeline(state, undefined),
|
|
2046
|
+
files: projectSessionFiles(state),
|
|
2047
|
+
};
|
|
2048
|
+
onDemand = true;
|
|
2049
|
+
}
|
|
2050
|
+
let messages;
|
|
2051
|
+
if (foldEvents.length > 0) {
|
|
2052
|
+
messages = foldEvents
|
|
2053
|
+
.filter((e) => e.type === 'message' && !e._synthetic && Boolean(e.content) && (e.role === 'user' || e.role === 'assistant'))
|
|
2054
|
+
.slice(-SESSION_DETAIL_MAX_MESSAGES)
|
|
2055
|
+
.map(e => ({ role: e.role, text: bound(e.content), at: e.timestamp ?? null }));
|
|
2056
|
+
}
|
|
2057
|
+
else {
|
|
2058
|
+
messages = [];
|
|
2059
|
+
if (digest?.firstUser)
|
|
2060
|
+
messages.push({ role: 'user', text: bound(digest.firstUser), at: session.timestamp ?? null });
|
|
2061
|
+
if (digest?.lastAssistant)
|
|
2062
|
+
messages.push({ role: 'assistant', text: bound(digest.lastAssistant), at: session.lastActivity ?? null });
|
|
2063
|
+
}
|
|
2064
|
+
const endStamp = sessionTranscriptStamp(session);
|
|
2065
|
+
const unchanged = canDateEvents && stamp !== undefined && endStamp !== undefined
|
|
2066
|
+
&& stamp.fileMtimeMs === endStamp.fileMtimeMs && stamp.fileSize === endStamp.fileSize;
|
|
2067
|
+
const partial = Boolean(digest?.partial) || !daemonProjection || !unchanged;
|
|
2068
|
+
const reason = digest?.partialReason
|
|
2069
|
+
?? (onDemand
|
|
2070
|
+
? 'background timeline pass has not reached this session yet; request/timeline/files below are an on-demand bounded fold, not the full-history daemon projection'
|
|
2071
|
+
: (!projection ? 'no transcript available to fold request/timeline/files for this session' : null));
|
|
2072
|
+
return {
|
|
2073
|
+
request: projection?.request ?? null,
|
|
2074
|
+
timeline: projection?.timeline ?? null,
|
|
2075
|
+
files: projection?.files ?? null,
|
|
2076
|
+
messages,
|
|
2077
|
+
sourceRevision: unchanged ? new Date(stamp.fileMtimeMs).toISOString() : null,
|
|
2078
|
+
partial,
|
|
2079
|
+
reason,
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
1985
2082
|
/** Resolve a session by id/query globally and print its compact preview (no pager).
|
|
1986
2083
|
* Backs `--preview` — the fast path for the "peek before resume" hot loop. */
|
|
1987
2084
|
export async function renderSessionPreview(query, scope) {
|
|
2085
|
+
// Exact ID + exactly one named, non-local device: the canonical bounded
|
|
2086
|
+
// preview loader (PHNX-3999) answers in ONE ssh hop plus a durable local
|
|
2087
|
+
// cache, instead of the general resolver's metadata fan-out followed by a
|
|
2088
|
+
// second render hop. Scoped tightly on purpose — a short id/label is not a
|
|
2089
|
+
// stable cache key across devices, and this never touches local discovery.
|
|
2090
|
+
//
|
|
2091
|
+
// Gated on `isCompleteSessionId`, NOT a bare-UUID regex: measured over this
|
|
2092
|
+
// fleet's own index, a "full session id" is a bare UUID for most harnesses
|
|
2093
|
+
// but `session_<uuid>` for kimi/rush and `ses_<ulid>` for opencode
|
|
2094
|
+
// (discover.ts). A bare-UUID-only check would silently skip this fast path
|
|
2095
|
+
// (falling through to the slower general resolver, never wrong, but wrong
|
|
2096
|
+
// to claim as "the fast path" for those harnesses) for a real, exact,
|
|
2097
|
+
// caller-supplied full id on those three.
|
|
2098
|
+
if (scope.json && !scope.local && scope.hosts?.length === 1
|
|
2099
|
+
&& scope.hosts[0] !== machineId() && isCompleteSessionId(query.trim())) {
|
|
2100
|
+
const { getRemoteSessionPreview } = await import('../lib/session/remote-preview-cache.js');
|
|
2101
|
+
const result = await getRemoteSessionPreview(query.trim(), scope.hosts[0], {
|
|
2102
|
+
refresh: scope.refresh,
|
|
2103
|
+
revision: scope.revision,
|
|
2104
|
+
});
|
|
2105
|
+
const envelope = result.envelope;
|
|
2106
|
+
console.log(JSON.stringify({
|
|
2107
|
+
schemaVersion: 1,
|
|
2108
|
+
session: envelope?.session ?? null,
|
|
2109
|
+
// `active` is a live snapshot the PEER computed at the moment it ran
|
|
2110
|
+
// `sessions preview --local --json` — meaningful only for that instant.
|
|
2111
|
+
// When this response is served from the durable cache (`cache.source ===
|
|
2112
|
+
// 'cache'`, which can be up to 45s old, or indefinitely old under a
|
|
2113
|
+
// matched --revision), presenting that stale snapshot under the same
|
|
2114
|
+
// `active` key would read as the session's CURRENT working/idle status
|
|
2115
|
+
// when it may no longer be. Null it out rather than relabel it with an
|
|
2116
|
+
// age stamp: the caller already has `cache.fetchedAt`/`cache.stale` to
|
|
2117
|
+
// reason about staleness, and getting a fresh `active` read costs
|
|
2118
|
+
// another SSH round trip this fast path exists specifically to avoid.
|
|
2119
|
+
active: result.cache.source === 'live' ? (envelope?.active ?? null) : null,
|
|
2120
|
+
preview: envelope?.preview ?? null,
|
|
2121
|
+
error: envelope?.error ?? (envelope ? null : result.cache.reason),
|
|
2122
|
+
// The peer's own `sessions preview --local --json` already computed
|
|
2123
|
+
// `details` (request/timeline/files/messages) with its own bounded reader
|
|
2124
|
+
// (see buildSessionDetailBlock) — this hop just forwards it verbatim,
|
|
2125
|
+
// never re-derives it, so a metadata-only/no-transcript peer row's
|
|
2126
|
+
// explicit unavailable reason survives the hop unchanged.
|
|
2127
|
+
details: envelope?.details ?? null,
|
|
2128
|
+
cache: result.cache,
|
|
2129
|
+
}));
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
1988
2132
|
let outcome = await resolveSessionMetadataValue(query, scope);
|
|
1989
2133
|
// resolveSessionMetadataValue already consults the live registry for a running
|
|
1990
2134
|
// session with no transcript row (RUSH-2682), so a live session resolves above.
|
|
@@ -2059,7 +2203,8 @@ export async function renderSessionPreview(query, scope) {
|
|
|
2059
2203
|
}
|
|
2060
2204
|
catch { /* plain preview on any probe failure */ }
|
|
2061
2205
|
if (scope.json) {
|
|
2062
|
-
const
|
|
2206
|
+
const sourceStamp = sessionTranscriptStamp(session);
|
|
2207
|
+
const { digest, error, events } = loadSessionPreviewDigest(session);
|
|
2063
2208
|
console.log(JSON.stringify({
|
|
2064
2209
|
schemaVersion: 1,
|
|
2065
2210
|
session: {
|
|
@@ -2095,6 +2240,7 @@ export async function renderSessionPreview(query, scope) {
|
|
|
2095
2240
|
} : null,
|
|
2096
2241
|
preview: digest ?? null,
|
|
2097
2242
|
error: error ?? null,
|
|
2243
|
+
details: buildSessionDetailBlock(session, digest, events, sourceStamp),
|
|
2098
2244
|
}));
|
|
2099
2245
|
return;
|
|
2100
2246
|
}
|
|
@@ -5370,7 +5516,9 @@ export function registerSessionsCommands(program) {
|
|
|
5370
5516
|
.option('-p, --project <name>', 'Narrow the ID to one project')
|
|
5371
5517
|
.option('--local', 'Only this machine; do not resolve the ID across the fleet')
|
|
5372
5518
|
.option('-D, --device <target...>', 'Resolve only on the named device(s)')
|
|
5373
|
-
.option('--json', 'Output the session preview as JSON')
|
|
5519
|
+
.option('--json', 'Output the session preview as JSON')
|
|
5520
|
+
.option('--refresh', 'Bypass the durable remote-preview cache and negative backoff for one bounded fetch (full ID + single --device only)')
|
|
5521
|
+
.option('--revision <cursor>', 'Opaque caller-owned activity cursor (any stable value YOU track, e.g. your own feed\'s lastActivityMs) -- passing the SAME value as your last call confirms nothing changed and serves the cache with zero SSH indefinitely; a different value fetches once, still subject to backoff unless --refresh is also set (full ID + single --device only)');
|
|
5374
5522
|
setHelpSections(previewCmd, {
|
|
5375
5523
|
examples: `
|
|
5376
5524
|
# Preview by the 8-character ID shown in agents sessions
|
|
@@ -5382,11 +5530,15 @@ export function registerSessionsCommands(program) {
|
|
|
5382
5530
|
# Stay on this machine or restrict the authoritative lookup to one peer
|
|
5383
5531
|
agents sessions preview 407b8dd5 --local
|
|
5384
5532
|
agents sessions preview 407b8dd5 --device zion
|
|
5533
|
+
|
|
5534
|
+
# Full ID + one --device: durable-cached fast path (PHNX-3999); force a fresh fetch
|
|
5535
|
+
agents sessions preview c70ecdea-6210-4039-9845-246a3a7a9942 --device zion --json --refresh
|
|
5385
5536
|
`,
|
|
5386
5537
|
notes: `
|
|
5387
5538
|
- Full UUIDs are globally unique and may stop the fleet lookup at the first exact hit.
|
|
5388
5539
|
- Short IDs wait for every selected device so ambiguity is never hidden.
|
|
5389
5540
|
- Active status is refreshed through the bounded live-state TTL; transcript-derived details use the durable session index.
|
|
5541
|
+
- A full UUID with exactly one --device and --json is served from a local durable cache (~45s fresh window); the JSON envelope's "cache" field reports fresh/stale/offline state. --refresh forces one bounded re-fetch.
|
|
5390
5542
|
`,
|
|
5391
5543
|
});
|
|
5392
5544
|
previewCmd.action(async (id) => {
|
|
@@ -5398,6 +5550,8 @@ export function registerSessionsCommands(program) {
|
|
|
5398
5550
|
local: options.local,
|
|
5399
5551
|
hosts: hosts.length > 0 ? hosts : undefined,
|
|
5400
5552
|
json: options.json,
|
|
5553
|
+
refresh: options.refresh,
|
|
5554
|
+
revision: options.revision,
|
|
5401
5555
|
});
|
|
5402
5556
|
});
|
|
5403
5557
|
registerSessionsTailCommand(sessionsCmd);
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* `npm i -g`. Users do not set extra env vars.
|
|
9
9
|
*/
|
|
10
10
|
import type { Command } from 'commander';
|
|
11
|
-
export declare const SECRETS_CLI_PACKAGE = "@phnx-labs/secrets-cli@0.1.
|
|
12
|
-
export declare const INSTALL_HINT = "agents clis install secrets # or: npm i -g @phnx-labs/secrets-cli@0.1.
|
|
11
|
+
export declare const SECRETS_CLI_PACKAGE = "@phnx-labs/secrets-cli@0.1.5";
|
|
12
|
+
export declare const INSTALL_HINT = "agents clis install secrets # or: npm i -g @phnx-labs/secrets-cli@0.1.5";
|
|
13
13
|
export declare function setupSecretsPrefsPath(): string;
|
|
14
14
|
/** True when the standalone `secrets` executable resolves ($SECRETS_BIN or PATH). */
|
|
15
15
|
export declare function isSecretsCliInstalled(): boolean;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents setup term` — install the standalone `term` CLI if missing (PHNX-4092).
|
|
3
|
+
*
|
|
4
|
+
* The PTY engine lives in `@phnx-labs/term-cli` (extracted PHNX-4091);
|
|
5
|
+
* agents-cli never rebundles it. The setup-token mint behind `agents accounts
|
|
6
|
+
* add` / `agents accounts login` (auth-mint.ts → term-driver.ts) spawns `term`
|
|
7
|
+
* on demand and fails loud when it is absent, so onboarding installs it here
|
|
8
|
+
* like every other standalone tool.
|
|
9
|
+
*
|
|
10
|
+
* Unlike browser/computer/secrets there is nothing to configure — no profile,
|
|
11
|
+
* no OS permission, no migrate step. A missing binary is a routine install
|
|
12
|
+
* (`agents clis install term` via the system `clis/term.yaml`, then a pinned
|
|
13
|
+
* `npm i -g`, both handled by installSetupTool), and once it is on PATH the
|
|
14
|
+
* tool is ready.
|
|
15
|
+
*/
|
|
16
|
+
import type { Command } from 'commander';
|
|
17
|
+
/** True when the standalone `term` executable resolves ($TERM_BIN or PATH). */
|
|
18
|
+
export declare function isTermCliInstalled(): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Install the standalone `term` CLI if missing. Returns whether `term` is on
|
|
21
|
+
* PATH afterwards. There is no further onboarding — presence is readiness.
|
|
22
|
+
*/
|
|
23
|
+
export declare function runTermWizard(): Promise<boolean>;
|
|
24
|
+
/** Register `agents setup term` under the parent `setup` command. */
|
|
25
|
+
export declare function registerSetupTermCommand(setupCmd: Command): void;
|