@phnx-labs/agents-cli 1.20.76 → 1.20.77

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 (85) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/dist/bin/agents +0 -0
  3. package/dist/commands/exec.js +58 -16
  4. package/dist/commands/routines.js +271 -13
  5. package/dist/commands/secrets.js +59 -17
  6. package/dist/commands/sessions-browser.js +11 -1
  7. package/dist/commands/sessions-picker.d.ts +14 -1
  8. package/dist/commands/sessions-picker.js +168 -15
  9. package/dist/commands/sessions.d.ts +23 -4
  10. package/dist/commands/sessions.js +128 -38
  11. package/dist/commands/ssh.d.ts +13 -0
  12. package/dist/commands/ssh.js +39 -2
  13. package/dist/index.js +2 -1
  14. package/dist/lib/activity.d.ts +5 -4
  15. package/dist/lib/activity.js +450 -36
  16. package/dist/lib/cloud/session-index.js +2 -2
  17. package/dist/lib/daemon.d.ts +10 -1
  18. package/dist/lib/daemon.js +99 -12
  19. package/dist/lib/devices/fleet.d.ts +16 -1
  20. package/dist/lib/devices/fleet.js +10 -2
  21. package/dist/lib/events.d.ts +1 -1
  22. package/dist/lib/exec.d.ts +16 -0
  23. package/dist/lib/exec.js +27 -4
  24. package/dist/lib/feed.d.ts +7 -1
  25. package/dist/lib/feed.js +96 -6
  26. package/dist/lib/heal.d.ts +7 -4
  27. package/dist/lib/heal.js +10 -22
  28. package/dist/lib/hosts/dispatch.d.ts +9 -0
  29. package/dist/lib/hosts/dispatch.js +24 -4
  30. package/dist/lib/hosts/passthrough.js +7 -2
  31. package/dist/lib/hosts/remote-cmd.d.ts +11 -0
  32. package/dist/lib/hosts/remote-cmd.js +31 -8
  33. package/dist/lib/hosts/remote-session-id.d.ts +45 -0
  34. package/dist/lib/hosts/remote-session-id.js +84 -0
  35. package/dist/lib/hosts/run-target.d.ts +4 -0
  36. package/dist/lib/hosts/run-target.js +5 -1
  37. package/dist/lib/hosts/session-index.js +3 -3
  38. package/dist/lib/menubar/install-menubar.d.ts +9 -0
  39. package/dist/lib/menubar/install-menubar.js +14 -0
  40. package/dist/lib/menubar/notify-desktop.d.ts +44 -0
  41. package/dist/lib/menubar/notify-desktop.js +78 -0
  42. package/dist/lib/overdue.d.ts +4 -5
  43. package/dist/lib/overdue.js +8 -41
  44. package/dist/lib/routine-notify.d.ts +76 -0
  45. package/dist/lib/routine-notify.js +190 -0
  46. package/dist/lib/routines-placement.d.ts +38 -0
  47. package/dist/lib/routines-placement.js +79 -0
  48. package/dist/lib/routines-project.d.ts +97 -0
  49. package/dist/lib/routines-project.js +349 -0
  50. package/dist/lib/routines.d.ts +68 -0
  51. package/dist/lib/routines.js +74 -7
  52. package/dist/lib/runner.d.ts +12 -2
  53. package/dist/lib/runner.js +150 -25
  54. package/dist/lib/sandbox.js +10 -0
  55. package/dist/lib/secrets/account-token.d.ts +20 -0
  56. package/dist/lib/secrets/account-token.js +64 -0
  57. package/dist/lib/secrets/agent.d.ts +9 -4
  58. package/dist/lib/secrets/agent.js +30 -20
  59. package/dist/lib/secrets/bundles.d.ts +20 -6
  60. package/dist/lib/secrets/bundles.js +70 -34
  61. package/dist/lib/secrets/index.d.ts +11 -2
  62. package/dist/lib/secrets/index.js +21 -9
  63. package/dist/lib/secrets/session-store.d.ts +6 -2
  64. package/dist/lib/secrets/session-store.js +65 -15
  65. package/dist/lib/secrets/unlock-hints.d.ts +27 -0
  66. package/dist/lib/secrets/unlock-hints.js +36 -0
  67. package/dist/lib/session/active.d.ts +10 -0
  68. package/dist/lib/session/active.js +67 -4
  69. package/dist/lib/session/db.d.ts +6 -0
  70. package/dist/lib/session/db.js +83 -6
  71. package/dist/lib/session/discover.d.ts +13 -1
  72. package/dist/lib/session/discover.js +91 -3
  73. package/dist/lib/session/parse.js +11 -2
  74. package/dist/lib/session/prompt.d.ts +7 -0
  75. package/dist/lib/session/prompt.js +37 -0
  76. package/dist/lib/session/provenance.d.ts +7 -0
  77. package/dist/lib/session/render.js +18 -16
  78. package/dist/lib/session/state.d.ts +4 -0
  79. package/dist/lib/session/state.js +93 -11
  80. package/dist/lib/session/types.d.ts +25 -1
  81. package/dist/lib/session/types.js +8 -0
  82. package/dist/lib/state.d.ts +7 -3
  83. package/dist/lib/state.js +20 -5
  84. package/dist/lib/types.d.ts +10 -0
  85. package/package.json +1 -1
@@ -6,12 +6,14 @@
6
6
  * response) and delegates to the generic `itemPicker` for the interactive UI.
7
7
  */
8
8
  import fs from 'node:fs';
9
+ import path from 'node:path';
9
10
  import chalk from 'chalk';
10
11
  import { truncate, humanDuration } from '../lib/format.js';
11
12
  import { parseSession, sanitizeForTerminal } from '../lib/session/parse.js';
12
13
  import { cleanSessionPrompt, extractSessionTopic } from '../lib/session/prompt.js';
13
14
  import { linkPath, linkUrl, relativeToCwd } from '../lib/session/render.js';
14
15
  import { linearIssueUrl } from '../lib/session/linear.js';
16
+ import { extractTodoProgress } from '../lib/session/state.js';
15
17
  import { renderMarkdown } from '../lib/markdown.js';
16
18
  import { itemPicker } from '../lib/picker.js';
17
19
  import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from '../lib/session/digest.js';
@@ -23,6 +25,32 @@ import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } fr
23
25
  function remoteMachineOf(session) {
24
26
  return session._remote ? session.machine : undefined;
25
27
  }
28
+ /**
29
+ * Compact checklist tally for list rows and previews (RUSH-2045).
30
+ * Example: `✓6/8 · A5 wiring runner`. Empty string when there is no list.
31
+ * Consumes `SessionMeta.todos` / `ActiveSession.todos` as populated by the
32
+ * state engine — does not re-parse transcripts.
33
+ */
34
+ export function formatTodoCompact(todos) {
35
+ if (!todos || !Number.isFinite(todos.total) || todos.total < 1)
36
+ return '';
37
+ const done = Number.isFinite(todos.done) ? Math.max(0, todos.done) : 0;
38
+ const tally = `✓${done}/${todos.total}`;
39
+ const step = todos.activeForm?.replace(/\s+/g, ' ').trim();
40
+ return step ? `${tally} · ${step}` : tally;
41
+ }
42
+ /**
43
+ * Best-effort GitHub repo URL from a checkout path shaped like
44
+ * `…/github.com/<owner>/<repo>/…`. Used to make the project name clickable
45
+ * when no Linear project URL is available.
46
+ */
47
+ export function githubRepoUrlFromCwd(cwd) {
48
+ if (!cwd)
49
+ return undefined;
50
+ const norm = cwd.replace(/\\/g, '/');
51
+ const m = norm.match(/\/github\.com\/([^/]+\/[^/]+)/);
52
+ return m ? `https://github.com/${m[1]}` : undefined;
53
+ }
26
54
  /**
27
55
  * SessionMeta originates in discover.ts (gitBranch, cwd, label, etc. read from
28
56
  * untrusted session files). parseSession sanitizes event payloads at its
@@ -31,6 +59,17 @@ function remoteMachineOf(session) {
31
59
  */
32
60
  function sanitizeMeta(s) {
33
61
  const clean = (v) => (v == null ? v : sanitizeForTerminal(v));
62
+ const todos = s.todos
63
+ ? {
64
+ ...s.todos,
65
+ activeForm: clean(s.todos.activeForm),
66
+ items: s.todos.items.map((it) => ({
67
+ ...it,
68
+ content: sanitizeForTerminal(it.content),
69
+ activeForm: clean(it.activeForm),
70
+ })),
71
+ }
72
+ : s.todos;
34
73
  return {
35
74
  ...s,
36
75
  id: sanitizeForTerminal(s.id),
@@ -45,6 +84,7 @@ function sanitizeMeta(s) {
45
84
  label: clean(s.label),
46
85
  ticketId: clean(s.ticketId),
47
86
  prUrl: clean(s.prUrl),
87
+ todos,
48
88
  };
49
89
  }
50
90
  const previewCache = new Map();
@@ -62,7 +102,8 @@ export function buildPreview(session) {
62
102
  if (remote) {
63
103
  const note = ' ' + chalk.gray(`on `) + chalk.bold.white(remote)
64
104
  + chalk.gray(` — enter to resume there, or space then enter to read it over SSH`);
65
- const output = [formatHeader(safe, []), '', note].join('\n');
105
+ const metaBody = formatMetaOnlyBody(safe);
106
+ const output = [formatHeader(safe, []), '', note, metaBody].filter(Boolean).join('\n');
66
107
  previewCache.set(cacheKey, output);
67
108
  return output;
68
109
  }
@@ -70,7 +111,8 @@ export function buildPreview(session) {
70
111
  // entry (e.g. `sessions go`). Show the header + a clean note, not a parse error.
71
112
  if (!session.filePath || !fs.existsSync(session.filePath)) {
72
113
  const note = ' ' + chalk.gray('Live session — full transcript not indexed here.');
73
- const output = [formatHeader(safe, []), '', note].filter(Boolean).join('\n');
114
+ const metaBody = formatMetaOnlyBody(safe);
115
+ const output = [formatHeader(safe, []), '', note, metaBody].filter(Boolean).join('\n');
74
116
  previewCache.set(cacheKey, output);
75
117
  return output;
76
118
  }
@@ -99,19 +141,27 @@ function formatHeader(session, events) {
99
141
  const { startedAgo, duration } = extractTiming(events);
100
142
  const totalMessages = session.messageCount ?? countMessages(events);
101
143
  const totalTokens = session.tokenCount;
102
- // Line 1: Agent v version · model · account
144
+ // Line 1: Agent v version · shortId · model · account
103
145
  const line1 = [];
104
146
  line1.push(chalk.gray(`${displayAgent(session.agent)}${session.version ? ` v${session.version}` : ''}`));
147
+ if (session.shortId)
148
+ line1.push(chalk.dim(session.shortId));
105
149
  if (model)
106
150
  line1.push(chalk.bold.white(model));
107
151
  if (session.account)
108
152
  line1.push(chalk.gray(session.account));
109
- // Line 2: cwd · branch · started X ago · lasted Y
153
+ // Line 2: cwd · project · branch · started X ago · lasted Y
154
+ // Project is clickable: Linear issue URL is for tickets (line 4); for the
155
+ // project name prefer a GitHub repo URL derived from the checkout path.
110
156
  const line2 = [];
111
157
  if (session.cwd) {
112
158
  const label = relativeToCwd(session.cwd);
113
159
  line2.push(chalk.bold.white(linkPath(session.cwd, label)));
114
160
  }
161
+ if (session.project) {
162
+ const repoUrl = githubRepoUrlFromCwd(session.cwd);
163
+ line2.push(chalk.cyan(repoUrl ? linkUrl(repoUrl, session.project) : session.project));
164
+ }
115
165
  if (session.gitBranch)
116
166
  line2.push(chalk.cyan(session.gitBranch));
117
167
  if (startedAgo)
@@ -128,7 +178,10 @@ function formatHeader(session, events) {
128
178
  }
129
179
  if (session.label)
130
180
  line3.push(chalk.white(session.label));
131
- line3.push(chalk.gray(linkPath(session.filePath, session.id)));
181
+ if (session.filePath)
182
+ line3.push(chalk.gray(linkPath(session.filePath, session.id)));
183
+ else
184
+ line3.push(chalk.gray(session.id));
132
185
  // Line 4: ticket + PR — clickable when a URL is resolvable (OSC 8 hyperlink),
133
186
  // plain text otherwise. Only rendered when the session carries either.
134
187
  const line4 = [];
@@ -147,6 +200,24 @@ function formatHeader(session, events) {
147
200
  ...(line4.length ? [line4.join(DOT)] : []),
148
201
  ].join('\n');
149
202
  }
203
+ /**
204
+ * Body lines available from SessionMeta alone (no transcript parse) — used for
205
+ * remote / unindexed sessions so checklist progress still surfaces when the
206
+ * parser teammate (or a prior scan) has populated `session.todos`.
207
+ */
208
+ function formatMetaOnlyBody(session) {
209
+ const lines = [];
210
+ if (session.topic) {
211
+ lines.push(chalk.cyan('Prompt: ') + chalk.white(truncate(session.topic.trim(), (process.stdout.columns || 80) - 12)));
212
+ }
213
+ const compact = formatTodoCompact(session.todos);
214
+ if (compact) {
215
+ lines.push(chalk.cyan('Todos: ') + chalk.white(compact));
216
+ }
217
+ if (lines.length === 0)
218
+ return '';
219
+ return lines.map(l => ' ' + l).join('\n');
220
+ }
150
221
  function extractModel(events) {
151
222
  for (let i = events.length - 1; i >= 0; i--) {
152
223
  const m = events[i].model;
@@ -204,6 +275,7 @@ function stripTags(text) {
204
275
  const LAST_RESPONSE_MAX_LINES = 15;
205
276
  const LAST_RESPONSE_MAX_LINES_WITH_TODOS = 8;
206
277
  const TODOS_MAX_ITEMS = 5;
278
+ const DIRS_TOUCHED_MAX = 5;
207
279
  function formatCompactPreview(events, session) {
208
280
  let firstUser = '';
209
281
  let lastAssistant = '';
@@ -211,7 +283,8 @@ function formatCompactPreview(events, session) {
211
283
  const toolCounts = {};
212
284
  let toolCalls = 0;
213
285
  let planFile = '';
214
- let latestTodos = null;
286
+ /** Latest checklist from the transcript (Claude TodoWrite / Codex update_plan). */
287
+ let latestTodos;
215
288
  for (const event of events) {
216
289
  if (event.type === 'message') {
217
290
  if (event.role === 'user' && !firstUser && event.content) {
@@ -232,25 +305,53 @@ function formatCompactPreview(events, session) {
232
305
  if (!planFile && p && /\/plans\/[^/]+\.md$/.test(p)) {
233
306
  planFile = p;
234
307
  }
235
- if (tool === 'TodoWrite' && Array.isArray(event.args?.todos)) {
236
- latestTodos = event.args.todos;
308
+ // Claude TodoWrite (`todos`) and Codex update_plan (`plan`) — same source as
309
+ // extractTodoProgress in the state engine. Prefer the most recent write.
310
+ if (tool === 'TodoWrite' || tool === 'update_plan') {
311
+ const progress = extractTodoProgress(event.args);
312
+ if (progress)
313
+ latestTodos = progress;
237
314
  }
238
315
  if (tool)
239
316
  toolCounts[tool] = (toolCounts[tool] ?? 0) + 1;
240
317
  toolCalls++;
241
318
  }
242
319
  }
320
+ // Prefer the transcript-derived list when we just re-parsed the file (freshest
321
+ // checklist write); fall back to SessionMeta.todos for rows where the scan/
322
+ // fan-out attached progress but the event stream has no TodoWrite yet.
323
+ const todos = latestTodos ?? session.todos;
243
324
  // Digest signals folded into the preview: change lifecycle, tool mix, tests.
244
325
  const changes = classifyFileChanges(events);
245
326
  const chg = changeCounts(changes);
246
327
  const lines = [];
247
328
  const termWidth = process.stdout.columns || 80;
329
+ // Originating user prompt (first non-system user turn).
248
330
  if (firstUser) {
249
331
  const first = extractSessionTopic(firstUser) || cleanSessionPrompt(firstUser).split('\n').find(l => l.trim()) || '';
250
332
  if (first) {
251
333
  lines.push(chalk.cyan('Prompt: ') + chalk.white(truncate(first.trim(), termWidth - 12)));
252
334
  }
253
335
  }
336
+ else if (session.topic) {
337
+ lines.push(chalk.cyan('Prompt: ') + chalk.white(truncate(session.topic.trim(), termWidth - 12)));
338
+ }
339
+ // Compact checklist: ✓done/total · current step (RUSH-2045).
340
+ const compact = formatTodoCompact(todos);
341
+ if (compact) {
342
+ lines.push(chalk.cyan('Todos: ') + chalk.white(compact));
343
+ }
344
+ const todosRendered = todos?.items?.length ? renderTodos(todos.items, termWidth) : [];
345
+ if (todosRendered.length > 0) {
346
+ for (const l of todosRendered)
347
+ lines.push(' ' + l);
348
+ }
349
+ // Recent activity = directories touched (not raw tool calls). Prefer a
350
+ // parser-supplied dirsTouched when present; else derive from event paths.
351
+ const dirs = directoriesTouched(session, events, changes);
352
+ if (dirs.length) {
353
+ lines.push(chalk.cyan('Dirs: ') + chalk.white(dirs.join(chalk.gray(' · '))));
354
+ }
254
355
  const activity = [];
255
356
  const changed = chg.created + chg.modified + chg.deleted;
256
357
  if (changed) {
@@ -287,14 +388,8 @@ function formatCompactPreview(events, session) {
287
388
  const basename = planFile.split('/').pop() || planFile;
288
389
  lines.push(chalk.cyan('Plan: ') + chalk.white(linkPath(planFile, basename)));
289
390
  }
290
- const todosRendered = latestTodos ? renderTodos(latestTodos, termWidth) : [];
291
- if (todosRendered.length > 0) {
292
- lines.push(chalk.cyan('Todos:'));
293
- for (const l of todosRendered)
294
- lines.push(' ' + l);
295
- }
296
391
  if (lastAssistant) {
297
- const maxLines = todosRendered.length > 0 ? LAST_RESPONSE_MAX_LINES_WITH_TODOS : LAST_RESPONSE_MAX_LINES;
392
+ const maxLines = todosRendered.length > 0 || compact ? LAST_RESPONSE_MAX_LINES_WITH_TODOS : LAST_RESPONSE_MAX_LINES;
298
393
  const rendered = renderLastResponse(lastAssistant, maxLines);
299
394
  if (rendered.length > 0) {
300
395
  lines.push('');
@@ -308,6 +403,64 @@ function formatCompactPreview(events, session) {
308
403
  }
309
404
  return lines.map(l => ' ' + l).join('\n');
310
405
  }
406
+ /**
407
+ * Unique directories the session touched, compact and human-readable.
408
+ * Prefer `session.dirsTouched` when the parser teammate has populated it;
409
+ * otherwise derive from file-change + tool paths already available here.
410
+ */
411
+ function directoriesTouched(session, events, changes) {
412
+ const fromMeta = session.dirsTouched;
413
+ if (Array.isArray(fromMeta) && fromMeta.length > 0) {
414
+ return fromMeta
415
+ .map((d) => sanitizeForTerminal(String(d).trim()))
416
+ .filter(Boolean)
417
+ .slice(0, DIRS_TOUCHED_MAX);
418
+ }
419
+ const counts = new Map();
420
+ const bump = (raw) => {
421
+ const dir = relativizeDir(raw, session.cwd);
422
+ if (!dir)
423
+ return;
424
+ counts.set(dir, (counts.get(dir) ?? 0) + 1);
425
+ };
426
+ for (const ch of changes)
427
+ bump(ch.path);
428
+ for (const event of events) {
429
+ if (event.type !== 'tool_use' || event._local)
430
+ continue;
431
+ const p = event.path || event.args?.file_path || event.args?.path || '';
432
+ if (p)
433
+ bump(p);
434
+ }
435
+ return [...counts.entries()]
436
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
437
+ .map(([d]) => d)
438
+ .slice(0, DIRS_TOUCHED_MAX);
439
+ }
440
+ /** Relativize a file path to its parent dir, short enough for one preview line. */
441
+ function relativizeDir(filePath, cwd) {
442
+ const norm = filePath.replace(/\\/g, '/');
443
+ if (!norm || norm.includes('node_modules') || norm.includes('/.git/') || norm.includes('/plans/')) {
444
+ return undefined;
445
+ }
446
+ let dir = path.posix.dirname(norm);
447
+ if (cwd) {
448
+ const base = cwd.replace(/\\/g, '/').replace(/\/$/, '');
449
+ if (dir === base)
450
+ return '.';
451
+ if (dir.startsWith(base + '/'))
452
+ dir = dir.slice(base.length + 1);
453
+ }
454
+ // Collapse home prefix.
455
+ const home = (process.env.HOME || '').replace(/\\/g, '/');
456
+ if (home && dir.startsWith(home + '/'))
457
+ dir = '~' + dir.slice(home.length);
458
+ // Drop ultra-deep absolute noise; keep last 3 segments.
459
+ const parts = dir.split('/').filter(Boolean);
460
+ if (parts.length > 3 && dir.startsWith('/'))
461
+ dir = parts.slice(-3).join('/');
462
+ return dir || undefined;
463
+ }
311
464
  function renderLastResponse(content, maxLines = LAST_RESPONSE_MAX_LINES) {
312
465
  const cleaned = stripTags(content).trim();
313
466
  if (!cleaned)
@@ -9,6 +9,18 @@ import { type PickedSession } from './sessions-picker.js';
9
9
  * a captured transcript tail. Collapses runs of whitespace.
10
10
  */
11
11
  export declare function cleanPreview(text: string): string;
12
+ /**
13
+ * Identity + checklist + live snippet for an --active / cross-machine row.
14
+ * Surfaces agent-adjacent identity the flat table already has (label, project)
15
+ * with a clickable project when a GitHub URL is resolvable, then the checklist
16
+ * tally and the latest-turn snippet.
17
+ *
18
+ * Free-text fields are cleaned individually; OSC 8 hyperlinks are applied
19
+ * *after* cleaning so `cleanPreview` does not strip the clickable targets
20
+ * (RUSH-2045 review). Ticket is clickable via {@link signalBadges}, not here,
21
+ * so the id is not printed twice.
22
+ */
23
+ export declare function formatActiveRowDescription(s: ActiveSession): string;
12
24
  /**
13
25
  * Index live sessions by their full session UUID so a historical `SessionMeta`
14
26
  * row (`meta.id`) can be matched to the session that is still running now.
@@ -175,6 +187,12 @@ export declare function renderSessionLog(session: SessionMeta, mode?: ViewMode):
175
187
  */
176
188
  export declare function renderSessionLogJson(session: SessionMeta): Promise<void>;
177
189
  /** Column-visibility flags for the picker row, computed once over the whole pool. */
190
+ /** The SSH-launch origin for a picker row, resolved from the live session's
191
+ * provenance (transport 'ssh'). `device` is set when the client IP matched a
192
+ * registered device; absent for an unresolved IP (renders a bare `ssh`). */
193
+ export interface SshOriginTag {
194
+ device?: string;
195
+ }
178
196
  export interface PickerColumns {
179
197
  /** Render the machine column (only when the pool spans more than one machine). */
180
198
  showMachine?: boolean;
@@ -205,7 +223,7 @@ export declare function machineLabeler(machines: string[]): (m: string) => strin
205
223
  * than one box, the ticket column only when some row carries a PR/ticket ref.
206
224
  */
207
225
  export declare function pickerColumnsFor(sessions: SessionMeta[]): PickerColumns;
208
- export declare function formatPickerLabel(s: SessionMeta, query: string, cols?: PickerColumns): string;
226
+ export declare function formatPickerLabel(s: SessionMeta, query: string, cols?: PickerColumns, ssh?: SshOriginTag): string;
209
227
  /**
210
228
  * Pick a hint to show above the picker. Deterministic (keys off the pool size)
211
229
  * so it stays fixed across the picker's re-renders within a single run.
@@ -259,9 +277,10 @@ export interface SessionQueryResolution {
259
277
  /**
260
278
  * The single entry point for turning a `sessions <query>` argument into rows.
261
279
  *
262
- * A complete session id resolves by id alone. Anything else keeps the existing
263
- * ladder: id lookup first (so a short id still wins), then the ranked
264
- * metadata+content search.
280
+ * An id-shaped query a complete id OR a hex short-id/prefix (looksLikeSessionId)
281
+ * resolves by id alone, through the index, and never falls back to content
282
+ * search (a bare id must not surface every transcript that merely mentions it).
283
+ * A genuine search phrase keeps the ranked metadata+content search.
265
284
  */
266
285
  export declare function resolveSessionQuery(pool: SessionMeta[], query: string): SessionQueryResolution;
267
286
  /** Filter and rank sessions by a multi-term search query across metadata and content. */