@dadado/agent-kit-cli 6.3.0 → 6.4.0

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 (45) hide show
  1. package/dashboard/dashboard-data.mjs +14 -2
  2. package/dashboard/dashboard.html +139 -4
  3. package/dashboard/lib/chat-sessions.d.mts +63 -0
  4. package/dashboard/lib/chat-sessions.mjs +430 -0
  5. package/dashboard/serve.mjs +5 -1
  6. package/dist/{add-VA2W7I6P.js → add-HFDX3U3R.js} +2 -2
  7. package/dist/{chunk-6GYKOHIL.js → chunk-3OM7JODZ.js} +2 -2
  8. package/dist/{chunk-Z7RYIHHX.js → chunk-6OTJEQEC.js} +3 -3
  9. package/dist/{chunk-MAPOQEA2.js → chunk-6RRG3WOE.js} +8 -1
  10. package/dist/{chunk-6KUSG7BJ.js → chunk-7BEHUBEJ.js} +3 -1
  11. package/dist/{chunk-VQBI3JVS.js → chunk-ANPSYWQO.js} +2 -2
  12. package/dist/{chunk-WMVMBPCR.js → chunk-AXLXEAVJ.js} +1 -1
  13. package/dist/{chunk-25J2SMJQ.js → chunk-E465FAWM.js} +1 -1
  14. package/dist/{chunk-U26MRIWN.js → chunk-H46W7NHC.js} +1 -1
  15. package/dist/{chunk-NNKOWUWM.js → chunk-HHR56LJQ.js} +64 -28
  16. package/dist/{chunk-SV2BR2LZ.js → chunk-IMIVTDMK.js} +7 -7
  17. package/dist/chunk-IRTS7WEA.js +374 -0
  18. package/dist/{chunk-MO63KZBP.js → chunk-KX3OUY4W.js} +8 -6
  19. package/dist/chunk-MWTXI6C5.js +223 -0
  20. package/dist/{chunk-JEI6HACW.js → chunk-NJVWTROP.js} +24 -377
  21. package/dist/{chunk-OVOXRH5W.js → chunk-Q4PCMRM5.js} +1 -1
  22. package/dist/{chunk-GUQYNUV4.js → chunk-R7DA4KXG.js} +1 -1
  23. package/dist/{chunk-OWWFKGKS.js → chunk-SE7ZIRLX.js} +2 -2
  24. package/dist/{chunk-KVY425NM.js → chunk-TZ5XYIEF.js} +4 -2
  25. package/dist/{chunk-Z7ABX5QI.js → chunk-WOHZ4T5V.js} +4 -2
  26. package/dist/{chunk-YRFSTHR5.js → chunk-XEEB4YSA.js} +13 -219
  27. package/dist/{chunk-QTBTKEXK.js → chunk-ZPZVROS5.js} +1 -1
  28. package/dist/{contribute-NLM2BN6Z.js → contribute-PP7HMPW7.js} +3 -3
  29. package/dist/{countdown-QXJD6TDB.js → countdown-LAUZMI7U.js} +10 -8
  30. package/dist/{diff-HHG4JEDE.js → diff-HYWC4G6Y.js} +4 -4
  31. package/dist/{doctor-TWF2M3S3.js → doctor-2X3HBRJJ.js} +8 -8
  32. package/dist/engine-info-4ZWEIHSX.js +89 -0
  33. package/dist/{ground-control-NMYYIVCV.js → ground-control-WKV2BMOW.js} +5 -4
  34. package/dist/{guard-EHXF76ID.js → guard-ZZRQW3DJ.js} +107 -8
  35. package/dist/{hook-VP7FHRSZ.js → hook-UEYR4M3Z.js} +10 -4
  36. package/dist/index.js +26 -22
  37. package/dist/{init-UOTV4ASJ.js → init-UF5VEIJX.js} +11 -11
  38. package/dist/{install-VCHPXQV3.js → install-6ILVA22C.js} +11 -11
  39. package/dist/{run-R66G276Q.js → run-2EKD2BEC.js} +12 -9
  40. package/dist/{run-plan-24Z5K4P3.js → run-plan-G72XMC5Z.js} +11 -7
  41. package/dist/sessions-O7MYEMCT.js +343 -0
  42. package/dist/{setup-global-M6LKGJHQ.js → setup-global-ZZEY5KWE.js} +5 -5
  43. package/dist/{status-5UKVIKOI.js → status-4LK2QXVQ.js} +6 -6
  44. package/dist/{update-VQBGEUWS.js → update-FPR2NZND.js} +9 -9
  45. package/package.json +1 -1
@@ -7,11 +7,13 @@
7
7
  import { execFileSync } from "node:child_process";
8
8
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
9
9
  import { dirname, join, resolve } from "node:path";
10
+ import { collectChats } from "./lib/chat-sessions.mjs";
10
11
  import { collectGateExitTickets } from "./lib/gate-exit-tickets.mjs";
11
12
  import {
12
13
  allowlistConfig,
13
14
  MAX_STRING,
14
15
  parseGitStatusShort,
16
+ resolveBroadcastAuth,
15
17
  resolveSnapshotRepoRoot,
16
18
  truncateStr,
17
19
  } from "./lib/guards.mjs";
@@ -133,7 +135,7 @@ function redactTerminalOutput(text) {
133
135
 
134
136
  const SNAPSHOT = {
135
137
  _schema: {
136
- version: "1.6.0",
138
+ version: "1.7.0",
137
139
  description: "Mission Control dashboard data model",
138
140
  fields: {
139
141
  generatedAt: "ISO-8601 timestamp of snapshot generation",
@@ -154,6 +156,8 @@ const SNAPSHOT = {
154
156
  "Newest headless agent-kit run / tick logs from <state root>/loop-logs (AGENT_KIT_STATE_ROOT: .cursor by default, or .agent-kit; no ~/.cursor dependency): format, session id, capped last agent text, result, LOOP_TICK_RESULT line, HITL gate ids and reply stamps (1.5.0)",
155
157
  gateExit:
156
158
  "Pending operator tickets (read-only, never fetches): {marker: absent|ok|invalid, base, tickets[] (stamp, headline, askId, labels, trigger, openedAt, url, branch, branchState unmerged|merged|none, inMarker), skipped[] (latest launcher skip records), truncated} from .cursor/context/gate-exit.json and gate-exit/<stamp> branches; same contract as agent-kit ground-control status (1.6.0)",
159
+ chats:
160
+ "Saved agent chats of this workspace: {withheld, vendors[]} with one envelope per vendor {vendor: claude|codex|cursor, status: ok|unsupported|none, projectDir, sessions[] ({id, title, titleSource: custom|ai|first-prompt|none, modified unix ms, sizeBytes}, newest first, capped at 200), total}; same contract as agent-kit sessions --json. withheld is true (vendors empty) in broadcast mode: chat titles never leave the machine (1.7.0)",
157
161
  processes:
158
162
  "Running process snapshots (node, serve.mjs, git operations) with elapsed time and a generated narration per process",
159
163
  skills: "Available skills discovered in .cursor/skills/",
@@ -162,7 +166,7 @@ const SNAPSHOT = {
162
166
  },
163
167
  },
164
168
  generatedAt: new Date().toISOString(),
165
- dashboardDataVersion: "1.6.0",
169
+ dashboardDataVersion: "1.7.0",
166
170
  plans: [],
167
171
  system: {
168
172
  repoRoot: ROOT,
@@ -175,6 +179,7 @@ const SNAPSHOT = {
175
179
  terminals: [],
176
180
  runLogs: [],
177
181
  gateExit: { marker: "absent", base: "staging", tickets: [], skipped: [], truncated: false },
182
+ chats: { withheld: false, vendors: [] },
178
183
  processes: [],
179
184
  skills: [],
180
185
  health: { status: "ok", checks: [] },
@@ -789,6 +794,13 @@ if (withinSnapshotBudget(200)) {
789
794
  SNAPSHOT.gateExit = collectGateExitTickets(ROOT, { redact: redactTerminalOutput });
790
795
  }
791
796
 
797
+ // 7d. Saved agent chats (~/.claude/projects/<slug>/<uuid>.jsonl), read-only. Titles are private:
798
+ // a broadcast (non-loopback, token-gated) server never gets them (serve.mjs strips them too).
799
+ SNAPSHOT.chats = collectChats({
800
+ cwd: ROOT,
801
+ withhold: resolveBroadcastAuth(process.env).broadcast !== false,
802
+ });
803
+
792
804
  // 8. Config
793
805
  const configPath = join(ROOT, ".cursor", "context", "config.json");
794
806
  if (existsSync(configPath)) {
@@ -49,7 +49,7 @@ html[data-dashboard-skin="legacy"] {
49
49
  --border-active: #2a3a5a;
50
50
  --text-primary: #e2e8f0;
51
51
  --text-secondary: #8899aa;
52
- --text-muted: #6d8094; /* landing uses #7f93a8 (AA over marketing gradient); see docs/design-system.md */
52
+ --text-muted: #7f93a8; /* same as the landing; >= 4.5:1 (WCAG AA) on every bg-* surface incl. hover; see docs/design-system.md */
53
53
  --green: #22c55e;
54
54
  --green-bg: rgba(34,197,94,0.12);
55
55
  --yellow: #eab308;
@@ -122,7 +122,7 @@ html[data-dashboard-skin="cursor"] {
122
122
  --border-active: #3a3a3a;
123
123
  --text-primary: #e4e4e4;
124
124
  --text-secondary: #a0a0a0;
125
- --text-muted: #6e6e6e;
125
+ --text-muted: #8b8b8b; /* >= 4.5:1 (WCAG AA) on every bg-* surface incl. hover; see docs/design-system.md */
126
126
  }
127
127
  html, body {
128
128
  height: 100%;
@@ -934,6 +934,31 @@ body.mc-fullscreen .top-tabs-row {
934
934
  }
935
935
  .terminal-item:hover { background: var(--bg-card-hover); }
936
936
 
937
+ /* ===== Chats (saved agent sessions) ===== */
938
+ .chat-list { display: flex; flex-direction: column; gap: 4px; }
939
+ .chat-item {
940
+ display: flex;
941
+ align-items: center;
942
+ gap: 10px;
943
+ padding: 6px 10px;
944
+ border-radius: 6px;
945
+ font-size: 12px;
946
+ color: var(--text-secondary);
947
+ }
948
+ .chat-item:hover { background: var(--bg-card-hover); }
949
+ .chat-title {
950
+ flex: 1;
951
+ min-width: 0;
952
+ overflow: hidden;
953
+ text-overflow: ellipsis;
954
+ white-space: nowrap;
955
+ color: var(--text-primary);
956
+ }
957
+ .chat-title[data-untitled="true"] { color: var(--text-muted); font-style: italic; }
958
+ .chat-age,
959
+ .chat-id { color: var(--text-muted); font-family: var(--mc-font-mono); white-space: nowrap; }
960
+ .chat-note { font-size: 12px; color: var(--text-muted); margin: 8px 10px 0; }
961
+
937
962
  /* ===== Healthcenter ===== */
938
963
  .healthcenter {
939
964
  display: flex;
@@ -1230,7 +1255,7 @@ body.mc-fullscreen .top-tabs-row {
1230
1255
  .git-hygiene-paths {
1231
1256
  font-family: var(--mc-font-mono);
1232
1257
  font-size: 11px;
1233
- color: var(--text-muted);
1258
+ color: var(--text-secondary); /* muted fails AA on the --yellow-bg tint */
1234
1259
  margin-top: 4px;
1235
1260
  word-break: break-all;
1236
1261
  }
@@ -3711,6 +3736,10 @@ html[data-monitor-density="comfortable"] .live-activity-feed .monitor-row .monit
3711
3736
  Terminals
3712
3737
  <span class="top-tab-badge" id="navTerminalsBadge">0</span>
3713
3738
  </a>
3739
+ <a class="top-tab nav-more-item" role="menuitem" href="#" data-section="chats" tabindex="0" onclick="return showSection('chats')" aria-label="Chats section">
3740
+ Chats
3741
+ <span class="top-tab-badge" id="navChatsBadge">0</span>
3742
+ </a>
3714
3743
  <a class="top-tab nav-more-item" role="menuitem" href="#" data-section="processes" tabindex="0" onclick="return showSection('processes')" aria-label="Processes section">
3715
3744
  Processes
3716
3745
  <span class="top-tab-badge" id="navProcessesBadge">0</span>
@@ -3882,6 +3911,7 @@ const SECTION_IDS = [
3882
3911
  'devops',
3883
3912
  'memory',
3884
3913
  'terminals',
3914
+ 'chats',
3885
3915
  'processes',
3886
3916
  'config',
3887
3917
  ];
@@ -6321,6 +6351,98 @@ function showTerminalPopup(content, title) {
6321
6351
  document.body.appendChild(overlay);
6322
6352
  }
6323
6353
 
6354
+ /**
6355
+ * POSIX single-quote for a pasted shell command.
6356
+ * Mirror of `shellQuote` in dashboard/lib/chat-sessions.mjs (inline HTML cannot import it).
6357
+ */
6358
+ function chatShellQuote(value) {
6359
+ const s = String(value ?? '');
6360
+ if (/^[A-Za-z0-9_./-]+$/.test(s)) return s;
6361
+ return `'${s.replace(/'/g, `'\\''`)}'`;
6362
+ }
6363
+
6364
+ /** Mirror of `claudeResumeCommand` in dashboard/lib/chat-sessions.mjs. */
6365
+ function chatResumeCommand(workspace, id) {
6366
+ return `cd ${chatShellQuote(workspace)} && claude --resume ${chatShellQuote(id)}`;
6367
+ }
6368
+
6369
+ /** Claude Code envelope of the snapshot `chats` field (null when absent or withheld). */
6370
+ function claudeChatsEnvelope(d) {
6371
+ const vendors = Array.isArray(d?.chats?.vendors) ? d.chats.vendors : [];
6372
+ return vendors.find((v) => v && v.vendor === 'claude') || null;
6373
+ }
6374
+
6375
+ function chatsTotal(d) {
6376
+ const claude = claudeChatsEnvelope(d);
6377
+ return claude && claude.status === 'ok' && Number.isFinite(claude.total) ? claude.total : 0;
6378
+ }
6379
+
6380
+ /** Chats section body. Titles are user content: escapeHtml / escapeAttr only. */
6381
+ function renderChatsSection(d) {
6382
+ const chats = d?.chats || null;
6383
+ const workspace = d?.system?.repoRoot || '';
6384
+ const claude = claudeChatsEnvelope(d);
6385
+ const sessions = claude && claude.status === 'ok' && Array.isArray(claude.sessions) ? claude.sessions : [];
6386
+ const total = chatsTotal(d);
6387
+ const unsupported = (Array.isArray(chats?.vendors) ? chats.vendors : [])
6388
+ .filter((v) => v && v.status === 'unsupported')
6389
+ .map((v) => ({ codex: 'Codex', cursor: 'Cursor' }[v.vendor] || String(v.vendor || '')));
6390
+ let subtitle = `${total} saved`;
6391
+ if (sessions.length < total) subtitle = `${sessions.length} newest of ${total}`;
6392
+ let body;
6393
+ if (chats?.withheld === true) {
6394
+ subtitle = 'hidden';
6395
+ body = renderEmptyStateCta({
6396
+ iconKind: 'chats',
6397
+ headline: 'Chats stay private',
6398
+ support: 'This Mission Control is broadcast on the network, so saved chat titles are left out. Open it on this machine to see them.',
6399
+ });
6400
+ } else if (sessions.length === 0) {
6401
+ body = renderEmptyStateCta({
6402
+ iconKind: 'chats',
6403
+ headline: 'No saved chats',
6404
+ support: 'The agent CLI saves each session per folder. Start claude in this workspace folder and it shows up here, ready to resume.',
6405
+ });
6406
+ } else {
6407
+ body = `<div class="chat-list">${sessions.map((s) => {
6408
+ const id = String(s.id || '');
6409
+ const untitled = !s.title;
6410
+ const title = untitled ? 'Untitled chat' : String(s.title);
6411
+ const modifiedIso = Number.isFinite(s.modified) ? new Date(s.modified).toISOString() : '';
6412
+ const cmd = chatResumeCommand(workspace, id);
6413
+ return `
6414
+ <div class="chat-item">
6415
+ <span class="chat-title"${untitled ? ' data-untitled="true"' : ''} title="${escapeAttr(title)}">${escapeHtml(title)}</span>
6416
+ <span class="chat-age" title="${escapeAttr(modifiedIso)}">${escapeHtml(relativeTime(modifiedIso))}</span>
6417
+ <span class="chat-id" title="${escapeAttr(id)}">${escapeHtml(id.slice(0, 8))}</span>
6418
+ <button type="button" class="process-copy-btn chat-resume-btn" data-chat-resume="${escapeAttr(cmd)}" aria-label="${escapeAttr(`Copy the resume command for chat ${title}`)}" title="${escapeAttr(copyActionTitle('resume command', 'terminal'))}">Copy resume</button>
6419
+ </div>`;
6420
+ }).join('')}</div>`;
6421
+ }
6422
+ const note = unsupported.length
6423
+ ? `<p class="chat-note">${escapeHtml(unsupported.map((name) => `${name}: not supported yet`).join(' \u00b7 '))}</p>`
6424
+ : '';
6425
+ return `
6426
+ <div class="content-section" id="section-chats">
6427
+ <div class="section-title">
6428
+ Chats
6429
+ <span class="section-subtitle">${escapeHtml(subtitle)}</span>
6430
+ </div>
6431
+ ${body}
6432
+ ${note}
6433
+ </div>
6434
+ `;
6435
+ }
6436
+
6437
+ function wireChatResumeButtons() {
6438
+ document.querySelectorAll('.chat-resume-btn[data-chat-resume]').forEach((btn) => {
6439
+ btn.addEventListener('click', (e) => {
6440
+ e.stopPropagation();
6441
+ copyForPaste(btn.dataset.chatResume, 'resume command', 'terminal');
6442
+ });
6443
+ });
6444
+ }
6445
+
6324
6446
  function wireTerminalExpandButtons() {
6325
6447
  document.querySelectorAll('.terminal-expand-btn[data-terminal-id]').forEach((btn) => {
6326
6448
  btn.addEventListener('click', (e) => {
@@ -7107,7 +7229,7 @@ function nowMetaIconSvg(kind, opts) {
7107
7229
  * features >= 1 unit. Detail that cannot meet the floor is drawn filled
7108
7230
  * (fill="currentColor" stroke="none"), never as a sub-stroke stroked shape.
7109
7231
  * Static path markup only; never concatenate untrusted text into the SVG.
7110
- * @param {'current-mission'|'monitor'|'field-report'|'checklist'|'more-sections'|'overview'|'plans'|'activity'|'agents'|'skills'|'skins'|'commands'|'health'|'git'|'devops'|'memory'|'terminals'|'processes'|'config'} kind
7232
+ * @param {'current-mission'|'monitor'|'field-report'|'checklist'|'more-sections'|'overview'|'plans'|'activity'|'agents'|'skills'|'skins'|'commands'|'health'|'git'|'devops'|'memory'|'terminals'|'chats'|'processes'|'config'} kind
7111
7233
  * @param {{ decorative?: boolean }} [opts] decorative true (default): aria-hidden next to a visible label.
7112
7234
  * decorative false: role=img + aria-label + title for icon-only controls (e.g. more-sections).
7113
7235
  */
@@ -7132,6 +7254,7 @@ function spaceIconSvg(kind, opts) {
7132
7254
  devops: 'DevOps',
7133
7255
  memory: 'Memory',
7134
7256
  terminals: 'Terminals',
7257
+ chats: 'Chats',
7135
7258
  processes: 'Processes',
7136
7259
  config: 'Config',
7137
7260
  };
@@ -7212,6 +7335,9 @@ function spaceIconSvg(kind, opts) {
7212
7335
  terminals:
7213
7336
  '<path d="M1.5 3.5h13a1 1 0 011 1v7a1 1 0 01-1 1H1.5a1 1 0 01-1-1v-7a1 1 0 011-1z"/>' +
7214
7337
  '<path d="M5.5 12.5v2M10.5 12.5v2M4 14.5h8"/>',
7338
+ // Speech bubble (chats) — Feather message-square on the 16 grid
7339
+ chats:
7340
+ '<path d="M14 10a1.5 1.5 0 01-1.5 1.5H5L2 14.5v-11A1.5 1.5 0 013.5 2h9A1.5 1.5 0 0114 3.5z"/>',
7215
7341
  // Layers (processes)
7216
7342
  processes:
7217
7343
  '<path d="M8 2.5l6 3-6 3-6-3 6-3z"/>' +
@@ -8158,6 +8284,11 @@ function renderUnsafe() {
8158
8284
  const navTerminalsBadge = document.getElementById('navTerminalsBadge');
8159
8285
  navTerminalsBadge.textContent = d.terminals.length;
8160
8286
  navTerminalsBadge.className = 'top-tab-badge';
8287
+ const navChatsBadge = document.getElementById('navChatsBadge');
8288
+ if (navChatsBadge) {
8289
+ navChatsBadge.textContent = chatsTotal(d);
8290
+ navChatsBadge.className = 'top-tab-badge';
8291
+ }
8161
8292
  const navProcessesBadge = document.getElementById('navProcessesBadge');
8162
8293
  navProcessesBadge.textContent = d.processes.length;
8163
8294
  navProcessesBadge.className = 'top-tab-badge';
@@ -8732,6 +8863,9 @@ function renderUnsafe() {
8732
8863
  </div>
8733
8864
  `);
8734
8865
 
8866
+ // ===== Chats =====
8867
+ parts.push(renderChatsSection(d));
8868
+
8735
8869
  // ===== Processes =====
8736
8870
  const processes = d.processes || [];
8737
8871
  // Detached audit-session visibility (phase3): host-wide agent-kit-audit-* PTYs.
@@ -8894,6 +9028,7 @@ function renderUnsafe() {
8894
9028
  contentEl.innerHTML = parts.join('');
8895
9029
 
8896
9030
  wireTerminalExpandButtons();
9031
+ wireChatResumeButtons();
8897
9032
 
8898
9033
  updateTransportChrome();
8899
9034
 
@@ -0,0 +1,63 @@
1
+ export const CHAT_SESSIONS_CAP: number;
2
+ export const CHAT_TITLE_MAX: number;
3
+ export const CHAT_NAMED_TITLE_MAX: number;
4
+ export const CHAT_FULL_SCAN_MAX_BYTES: number;
5
+ export const CHAT_HEAD_BYTES: number;
6
+ export const CHAT_TAIL_BYTES: number;
7
+ export const CHAT_SCAN_BUDGET_BYTES: number;
8
+ export const CHAT_VENDORS: readonly ["claude", "codex", "cursor"];
9
+
10
+ export type ChatVendor = "claude" | "codex" | "cursor";
11
+ export type ChatTitleSource = "custom" | "ai" | "first-prompt" | "none";
12
+
13
+ export interface ChatSession {
14
+ id: string;
15
+ title: string | null;
16
+ titleSource: ChatTitleSource;
17
+ /** Last write, unix milliseconds. */
18
+ modified: number;
19
+ sizeBytes: number;
20
+ }
21
+
22
+ export interface ChatSessionsEnvelope {
23
+ vendor: ChatVendor;
24
+ status: "ok" | "unsupported" | "none";
25
+ projectDir: string | null;
26
+ sessions: ChatSession[];
27
+ total: number;
28
+ }
29
+
30
+ export interface ChatsSnapshot {
31
+ withheld: boolean;
32
+ vendors: ChatSessionsEnvelope[];
33
+ }
34
+
35
+ export interface ChatTitle {
36
+ title: string | null;
37
+ titleSource: ChatTitleSource;
38
+ }
39
+
40
+ export interface ListChatSessionsOptions {
41
+ cwd: string;
42
+ home?: string | null;
43
+ cap?: number;
44
+ /** Bytes read in full before older transcripts fall back to a small head + tail window. */
45
+ scanBudgetBytes?: number;
46
+ cache?: Map<string, ChatTitle>;
47
+ }
48
+
49
+ export function claudeProjectSlug(cwd: string): string;
50
+ export function claudeProjectDir(cwd: string, opts?: { home?: string | null }): string | null;
51
+ export function cleanChatTitle(value: unknown, max?: number): string;
52
+ export function readChatTitle(
53
+ filePath: string,
54
+ window?: { fullScanMaxBytes?: number; headBytes?: number; tailBytes?: number },
55
+ ): ChatTitle;
56
+ export function listClaudeSessions(opts: ListChatSessionsOptions): ChatSessionsEnvelope;
57
+ export function listChatSessions(opts: ListChatSessionsOptions): ChatSessionsEnvelope[];
58
+ export function collectChats(opts: ListChatSessionsOptions & { withhold?: boolean }): ChatsSnapshot;
59
+ export function withholdChats<T extends Record<string, unknown>>(snapshot: T): T;
60
+ export function withholdChatsFromPayload(payload: string): string;
61
+ export function shellQuote(value: string): string;
62
+ export function claudeResumeCommand(workspace: string, id: string): string;
63
+ export function formatChatAge(modifiedMs: number, nowMs?: number): string;