@secondcontext/btx-cli 0.0.3 → 0.0.6

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.
@@ -0,0 +1,112 @@
1
+ import { projectApiGet, projectApiPost } from './api.js';
2
+ function normalizeMatchValue(value) {
3
+ return value?.trim().toLowerCase() ?? '';
4
+ }
5
+ function formatMember(member) {
6
+ const name = member.name?.trim();
7
+ const email = member.email?.trim();
8
+ if (name && email)
9
+ return `${name} <${email}> [${member.userId}]`;
10
+ if (name)
11
+ return `${name} [${member.userId}]`;
12
+ if (email)
13
+ return `${email} [${member.userId}]`;
14
+ return member.userId;
15
+ }
16
+ function formatConversation(conversation) {
17
+ return `${conversation.name} [${conversation.id}]`;
18
+ }
19
+ function resolveUniqueMatch(query, items, selectors, render, itemLabel, listCommand) {
20
+ const normalizedQuery = normalizeMatchValue(query);
21
+ if (!normalizedQuery) {
22
+ throw new Error(`Provide a ${itemLabel} value.`);
23
+ }
24
+ const exactMatches = items.filter((item) => selectors.some((selector) => normalizeMatchValue(selector(item)) === normalizedQuery));
25
+ if (exactMatches.length === 1)
26
+ return exactMatches[0];
27
+ if (exactMatches.length > 1) {
28
+ throw new Error(`Multiple ${itemLabel}s match "${query}". Narrow the query or inspect ${listCommand}:\n${exactMatches
29
+ .map((item) => ` ${render(item)}`)
30
+ .join('\n')}`);
31
+ }
32
+ const partialMatches = items.filter((item) => selectors.some((selector) => normalizeMatchValue(selector(item)).includes(normalizedQuery)));
33
+ if (partialMatches.length === 1)
34
+ return partialMatches[0];
35
+ if (partialMatches.length > 1) {
36
+ throw new Error(`Multiple ${itemLabel}s match "${query}". Narrow the query or inspect ${listCommand}:\n${partialMatches
37
+ .map((item) => ` ${render(item)}`)
38
+ .join('\n')}`);
39
+ }
40
+ throw new Error(`No ${itemLabel} matched "${query}". Run \`${listCommand}\` to inspect valid targets.`);
41
+ }
42
+ function formatScopeLabel(kind, label) {
43
+ switch (kind) {
44
+ case 'team':
45
+ return 'Team chat';
46
+ case 'dm':
47
+ return `Direct message with ${label}`;
48
+ case 'conversation':
49
+ return `Conversation: ${label}`;
50
+ }
51
+ }
52
+ export async function listProjectMembers(env) {
53
+ return projectApiGet(env, '/members');
54
+ }
55
+ export async function listChatConversations(env) {
56
+ return projectApiGet(env, '/chat/conversations');
57
+ }
58
+ export async function resolveMessageScope(env, flags) {
59
+ if (flags.peer && flags.conversation) {
60
+ throw new Error('Use either `--peer` or `--conversation`, not both.');
61
+ }
62
+ if (flags.peer) {
63
+ const members = await listProjectMembers(env);
64
+ const peer = resolveUniqueMatch(flags.peer, members, [
65
+ (member) => member.userId,
66
+ (member) => member.id,
67
+ (member) => member.email,
68
+ (member) => member.name,
69
+ ], formatMember, 'member', 'btx messages members');
70
+ return {
71
+ kind: 'dm',
72
+ label: formatScopeLabel('dm', formatMember(peer)),
73
+ peer,
74
+ requestQuery: { peer: peer.userId },
75
+ sendPayload: { recipientId: peer.userId },
76
+ };
77
+ }
78
+ if (flags.conversation) {
79
+ const { conversations } = await listChatConversations(env);
80
+ const conversation = resolveUniqueMatch(flags.conversation, conversations, [(item) => item.id, (item) => item.name], formatConversation, 'conversation', 'btx messages conversations');
81
+ return {
82
+ kind: 'conversation',
83
+ label: formatScopeLabel('conversation', formatConversation(conversation)),
84
+ conversation,
85
+ requestQuery: { conversation: conversation.id },
86
+ sendPayload: { conversationId: conversation.id },
87
+ };
88
+ }
89
+ return {
90
+ kind: 'team',
91
+ label: formatScopeLabel('team', 'Team chat'),
92
+ requestQuery: {},
93
+ sendPayload: {},
94
+ };
95
+ }
96
+ export async function listChatMessages(env, scope, options) {
97
+ const params = new URLSearchParams(scope.requestQuery);
98
+ if (options.before)
99
+ params.set('before', options.before);
100
+ if (typeof options.limit === 'number')
101
+ params.set('limit', String(options.limit));
102
+ const suffix = params.toString();
103
+ return projectApiGet(env, `/chat${suffix ? `?${suffix}` : ''}`);
104
+ }
105
+ export async function sendChatMessage(env, scope, input) {
106
+ return projectApiPost(env, '/chat', {
107
+ body: input.body,
108
+ replyToId: input.replyToId ?? null,
109
+ ...scope.sendPayload,
110
+ });
111
+ }
112
+ //# sourceMappingURL=messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-cli.d.ts","sourceRoot":"","sources":["../src/runtime-cli.ts"],"names":[],"mappings":"AAoLA,MAAM,WAAW,aAAa;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AA6hED,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,GAAE,aAA2B,GAC/B,OAAO,CAAC,IAAI,CAAC,CA0Qf"}
1
+ {"version":3,"file":"runtime-cli.d.ts","sourceRoot":"","sources":["../src/runtime-cli.ts"],"names":[],"mappings":"AAiPA,MAAM,WAAW,aAAa;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AA48ED,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,GAAE,aAA2B,GAC/B,OAAO,CAAC,IAAI,CAAC,CA+Rf"}
@@ -138,6 +138,15 @@ function isJson(flags) {
138
138
  function printJson(value) {
139
139
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
140
140
  }
141
+ function readIntFlag(flags, key, defaultValue, min = 1, max = 200) {
142
+ const raw = flags[key];
143
+ if (!raw)
144
+ return defaultValue;
145
+ const parsed = Number.parseInt(raw, 10);
146
+ if (!Number.isFinite(parsed))
147
+ return defaultValue;
148
+ return Math.max(min, Math.min(max, parsed));
149
+ }
141
150
  // ── Formatting ──────────────────────────────────────────────────────────────
142
151
  function truncate(str, max) {
143
152
  if (!str)
@@ -356,9 +365,198 @@ function formatMeetingTranscript(m) {
356
365
  console.log(`[${time}] ${speaker}: ${seg.text}`);
357
366
  }
358
367
  }
368
+ function recordingTitle(recording) {
369
+ return (recording.meetingTitle?.trim() ||
370
+ recording.originalFilename?.trim() ||
371
+ 'Untitled recording');
372
+ }
373
+ function recordingTimestamp(recording) {
374
+ return recording.meetingDate || recording.uploadedAt || recording.transcribedAt || recording.createdAt || null;
375
+ }
376
+ function formatRecordingDuration(durationMs) {
377
+ if (!durationMs || durationMs <= 0)
378
+ return '';
379
+ const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
380
+ const minutes = Math.floor(totalSeconds / 60);
381
+ const seconds = totalSeconds % 60;
382
+ const hours = Math.floor(minutes / 60);
383
+ if (hours > 0) {
384
+ return `${hours}:${String(minutes % 60).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
385
+ }
386
+ return `${minutes}:${String(seconds).padStart(2, '0')}`;
387
+ }
388
+ function formatRecordingTable(recordings) {
389
+ if (recordings.length === 0) {
390
+ console.log('No recordings found.');
391
+ return;
392
+ }
393
+ const idW = 10;
394
+ const dateW = 12;
395
+ const statusW = 12;
396
+ const sourceW = 12;
397
+ const durationW = 8;
398
+ console.log(`${padRight('ID', idW)} ${padRight('Date', dateW)} ${padRight('Status', statusW)} ${padRight('Source', sourceW)} ${padRight('Dur', durationW)} Title`);
399
+ console.log(`${'\u2500'.repeat(idW)} ${'\u2500'.repeat(dateW)} ${'\u2500'.repeat(statusW)} ${'\u2500'.repeat(sourceW)} ${'\u2500'.repeat(durationW)} ${'\u2500'.repeat(40)}`);
400
+ for (const recording of recordings) {
401
+ const date = recordingTimestamp(recording)
402
+ ? new Date(recordingTimestamp(recording)).toISOString().slice(0, 10)
403
+ : '';
404
+ console.log(`${padRight(truncate(recording.id, idW), idW)} ${padRight(date, dateW)} ${padRight(truncate(recording.status || '', statusW), statusW)} ${padRight(truncate(recording.source || '', sourceW), sourceW)} ${padRight(formatRecordingDuration(recording.durationMs), durationW)} ${truncate(recordingTitle(recording), 52)}`);
405
+ }
406
+ console.log(`\n${recordings.length} recording${recordings.length !== 1 ? 's' : ''} found`);
407
+ }
408
+ function formatRecordingDetail(recording) {
409
+ console.log(`Recording: ${recordingTitle(recording)}`);
410
+ console.log(`ID: ${recording.id}`);
411
+ if (recording.meetingId)
412
+ console.log(`Meeting ID: ${recording.meetingId}`);
413
+ if (recordingTimestamp(recording)) {
414
+ console.log(`Date: ${new Date(recordingTimestamp(recording)).toISOString().slice(0, 10)}`);
415
+ }
416
+ if (recording.source)
417
+ console.log(`Source: ${recording.source}`);
418
+ if (recording.status)
419
+ console.log(`Status: ${recording.status}`);
420
+ if (recording.kind)
421
+ console.log(`Kind: ${recording.kind}`);
422
+ if (recording.captureMode)
423
+ console.log(`Capture: ${recording.captureMode}`);
424
+ if (recording.languageHint) {
425
+ const detected = recording.detectedLanguage ? ` (detected ${recording.detectedLanguage})` : '';
426
+ console.log(`Language: ${recording.languageHint}${detected}`);
427
+ }
428
+ if (recording.durationMs)
429
+ console.log(`Duration: ${formatRecordingDuration(recording.durationMs)}`);
430
+ if (recording.originalFilename)
431
+ console.log(`Filename: ${recording.originalFilename}`);
432
+ if (recording.mimeType)
433
+ console.log(`MIME: ${recording.mimeType}`);
434
+ if (recording.error)
435
+ console.log(`Error: ${recording.error}`);
436
+ if (recording.transcriptText?.trim()) {
437
+ console.log(`\nTranscript Preview:\n ${recording.transcriptText.trim().replace(/\n/g, '\n ')}`);
438
+ }
439
+ }
440
+ function recordingSpeakerLabel(source) {
441
+ const normalized = source?.trim();
442
+ if (!normalized)
443
+ return 'Speaker';
444
+ return normalized.replace(/[_-]+/g, ' ');
445
+ }
446
+ function formatRecordingTranscript(recording) {
447
+ const transcript = recording.transcript ?? [];
448
+ const transcriptText = recording.transcriptText?.trim() ?? '';
449
+ if (transcript.length === 0 && !transcriptText) {
450
+ console.log('No transcript available for this recording.');
451
+ return;
452
+ }
453
+ console.log(`Transcript: ${recordingTitle(recording)}`);
454
+ if (recordingTimestamp(recording)) {
455
+ console.log(`Date: ${new Date(recordingTimestamp(recording)).toISOString().slice(0, 10)}`);
456
+ }
457
+ if (transcript.length > 0) {
458
+ console.log(`Segments: ${transcript.length}`);
459
+ console.log('');
460
+ for (const segment of transcript) {
461
+ const time = segment.start_time || '';
462
+ console.log(`[${time}] ${recordingSpeakerLabel(segment.speaker?.source)}: ${segment.text}`);
463
+ }
464
+ return;
465
+ }
466
+ console.log('');
467
+ console.log(transcriptText);
468
+ }
469
+ function sessionPayload(s) {
470
+ return JSON.stringify({
471
+ sessionId: s.sessionId,
472
+ label: s.label,
473
+ sessionType: s.sessionType,
474
+ runtime: s.runtime || null,
475
+ status: s.status,
476
+ messageCount: s.messageCount ?? 0,
477
+ activityCount: s.activityCount ?? 0,
478
+ summary: s.summary ?? null,
479
+ lastActivityType: s.lastActivityType ?? null,
480
+ lastActivityContent: s.lastActivityContent ?? null,
481
+ createdAt: typeof s.createdAt === 'number'
482
+ ? new Date(s.createdAt).toISOString()
483
+ : s.createdAt,
484
+ });
485
+ }
486
+ function sessionPreview(session) {
487
+ return session.summary || session.lastActivityContent || '';
488
+ }
489
+ function formatSessionTable(sessions) {
490
+ if (sessions.length === 0) {
491
+ console.log('No sessions found.');
492
+ return;
493
+ }
494
+ const idW = 12;
495
+ const dateW = 10;
496
+ const statusW = 8;
497
+ const typeW = 14;
498
+ const runtimeW = 12;
499
+ const labelW = 28;
500
+ console.log(`${padRight('ID', idW)} ${padRight('Date', dateW)} ${padRight('Status', statusW)} ${padRight('Type', typeW)} ${padRight('Runtime', runtimeW)} ${padRight('Label', labelW)} Preview`);
501
+ console.log(`${'\u2500'.repeat(idW)} ${'\u2500'.repeat(dateW)} ${'\u2500'.repeat(statusW)} ${'\u2500'.repeat(typeW)} ${'\u2500'.repeat(runtimeW)} ${'\u2500'.repeat(labelW)} ${'\u2500'.repeat(36)}`);
502
+ for (const session of sessions) {
503
+ const created = new Date(session.createdAt).toISOString().slice(0, 10);
504
+ console.log(`${padRight(truncate(session.sessionId, idW), idW)} ${padRight(created, dateW)} ${padRight(session.status, statusW)} ${padRight(truncate(session.sessionType, typeW), typeW)} ${padRight(truncate(session.runtime || '', runtimeW), runtimeW)} ${padRight(truncate(session.label, labelW), labelW)} ${truncate(sessionPreview(session), 44)}`);
505
+ }
506
+ console.log(`\n${sessions.length} session${sessions.length !== 1 ? 's' : ''} found`);
507
+ console.log('Use `btx sessions get <session-id>` for full details and recent checkpoints.');
508
+ for (const session of sessions) {
509
+ console.log(`BTX_SESSION_JSON: ${sessionPayload(session)}`);
510
+ }
511
+ }
512
+ function formatSessionDetail(session, activities) {
513
+ console.log(`Session: ${session.label}`);
514
+ console.log(`ID: ${session.sessionId}`);
515
+ console.log(`Status: ${session.status}`);
516
+ console.log(`Type: ${session.sessionType}`);
517
+ console.log(`Runtime: ${session.runtime || 'n/a'}`);
518
+ console.log(`Execution: ${session.executionMode}`);
519
+ console.log(`Started: ${new Date(session.createdAt).toISOString()}`);
520
+ if (session.updatedAt)
521
+ console.log(`Updated: ${new Date(session.updatedAt).toISOString()}`);
522
+ if (session.closedAt)
523
+ console.log(`Closed: ${session.closedAt}`);
524
+ if (session.userName || session.userEmail) {
525
+ console.log(`Owner: ${session.userName || session.userEmail}`);
526
+ }
527
+ console.log(`Messages: ${session.messageCount ?? 0}`);
528
+ console.log(`Activities: ${session.activityCount ?? 0}`);
529
+ if (session.totalCostUsd != null)
530
+ console.log(`Cost USD: ${session.totalCostUsd}`);
531
+ if (session.lastActivityType)
532
+ console.log(`Last activity: ${session.lastActivityType}`);
533
+ if (session.lastActivityAt)
534
+ console.log(`Last updated: ${session.lastActivityAt}`);
535
+ if (session.summary) {
536
+ console.log(`\nSummary:\n ${session.summary.replace(/\n/g, '\n ')}`);
537
+ }
538
+ if (session.lastActivityContent && session.lastActivityContent !== session.summary) {
539
+ console.log(`\nLatest checkpoint:\n ${session.lastActivityContent.replace(/\n/g, '\n ')}`);
540
+ }
541
+ if (activities.length > 0) {
542
+ console.log('\nRecent activity:');
543
+ for (const activity of activities) {
544
+ const date = new Date(activity.createdAt ?? Date.now()).toISOString();
545
+ const actor = activity.actorName || activity.actorEmail || 'Unknown';
546
+ const content = activity.content || '';
547
+ console.log(` [${date}] ${activity.type} (${actor})`);
548
+ if (content)
549
+ console.log(` ${content.replace(/\n/g, '\n ')}`);
550
+ }
551
+ }
552
+ else {
553
+ console.log('\nNo session activity found.');
554
+ }
555
+ console.log(`\nBTX_SESSION_JSON: ${sessionPayload(session)}`);
556
+ }
359
557
  // ── Help ─────────────────────────────────────────────────────────────────────
360
558
  const HELP = {
361
- top: `BTX CLI - manage tasks, contacts, meetings, and leads from a session.
559
+ top: `BTX CLI - manage tasks, contacts, meetings, recordings, and leads from a session.
362
560
 
363
561
  Usage:
364
562
  node "$BTX_CLI_PATH" <resource> <command> [flags]
@@ -366,10 +564,11 @@ Usage:
366
564
 
367
565
  Resources:
368
566
  tasks Create and manage project tasks
369
- sessions Add and list notes for a session
567
+ sessions Browse recent sessions, inspect summaries, add notes
370
568
  contacts View and update contacts, add notes
371
569
  orgs View organizations, add notes
372
570
  meetings View meeting recordings and transcripts
571
+ recordings View recording imports and transcripts
373
572
  leads List lead types
374
573
  user Manage your profile notes
375
574
  intro-paths Find warm intro paths in your network
@@ -380,8 +579,10 @@ Resources:
380
579
  Examples:
381
580
  node "$BTX_CLI_PATH" tasks list --status todo
382
581
  node "$BTX_CLI_PATH" tasks list --json
582
+ node "$BTX_CLI_PATH" recordings list --days 7
583
+ node "$BTX_CLI_PATH" recordings transcript <recording-id>
383
584
  node "$BTX_CLI_PATH" meetings list --days 7
384
- node "$BTX_CLI_PATH" meetings get <meeting-id> --transcript
585
+ node "$BTX_CLI_PATH" meetings transcript <meeting-id>
385
586
  node "$BTX_CLI_PATH" contacts --help
386
587
 
387
588
  All resource commands support --json for machine-readable output.`,
@@ -417,16 +618,24 @@ Examples:
417
618
  node "$BTX_CLI_PATH" tasks complete abc123
418
619
  node "$BTX_CLI_PATH" tasks notes list abc123
419
620
  node "$BTX_CLI_PATH" tasks notes add abc123 --content "Insight: ICP may be wrong on stage" --source "https://..."`,
420
- sessions: `BTX sessions - add and list notes for a session.
621
+ sessions: `BTX sessions - browse recent sessions and inspect a session's checkpoints.
421
622
 
422
623
  Commands:
624
+ list List recent sessions (metadata only)
625
+ get <session-id> Get one session with recent checkpoints
423
626
  notes list <session-id> List notes for a session
424
627
  notes add <session-id> --content "..." Add a note to a session
425
628
 
426
629
  Flags:
630
+ sessions list [--status open|closed] [--type <session-type>] [--runtime <runtime>] [--query "text"] [--limit <n>]
631
+ sessions get <session-id> [--activity-limit <n>]
427
632
  sessions notes add <session-id> --content "Label: value" [--source "https://..."]
428
633
 
429
634
  Examples:
635
+ node "$BTX_CLI_PATH" sessions list --limit 10
636
+ node "$BTX_CLI_PATH" sessions list --status closed --query "auth"
637
+ node "$BTX_CLI_PATH" sessions get abc-session-id
638
+ node "$BTX_CLI_PATH" sessions get abc-session-id --activity-limit 10
430
639
  node "$BTX_CLI_PATH" sessions notes list abc-session-id
431
640
  node "$BTX_CLI_PATH" sessions notes add abc-session-id --content "Insight: market timing is critical"
432
641
  node "$BTX_CLI_PATH" sessions notes add abc-session-id --content "Decision: pivot to enterprise" --source "https://..."`,
@@ -547,6 +756,27 @@ Examples:
547
756
  node "$BTX_CLI_PATH" meetings list --query "onboarding"
548
757
  node "$BTX_CLI_PATH" meetings get abc123
549
758
  node "$BTX_CLI_PATH" meetings transcript abc123`,
759
+ recordings: `BTX recordings - view synced recordings and transcripts.
760
+
761
+ Commands:
762
+ list List recordings (filterable by date, source, status)
763
+ get Get recording details
764
+ transcript Get the full transcript for a recording
765
+
766
+ Flags:
767
+ recordings list [--days <n>] [--query "text"] [--status <status>] [--source <source>]
768
+ recordings get <recording-id>
769
+ recordings transcript <recording-id>
770
+
771
+ The --days flag filters to recordings from the last N days (default: all).
772
+ Use "list" to browse imported or uploaded recordings, "get" for metadata,
773
+ and "transcript" for the transcript text coding agents can consume directly.
774
+
775
+ Examples:
776
+ node "$BTX_CLI_PATH" recordings list
777
+ node "$BTX_CLI_PATH" recordings list --days 7
778
+ node "$BTX_CLI_PATH" recordings list --source granola
779
+ node "$BTX_CLI_PATH" recordings transcript abc123`,
550
780
  context: `BTX context - fetch the current project business context.
551
781
 
552
782
  Commands:
@@ -1144,6 +1374,69 @@ async function taskNotesList(taskId, flags) {
1144
1374
  }
1145
1375
  }
1146
1376
  // ── Session notes commands ────────────────────────────────────────────────────
1377
+ async function fetchSessions(flags) {
1378
+ const params = new URLSearchParams();
1379
+ if (flags.status)
1380
+ params.set('status', flags.status);
1381
+ if (flags.type)
1382
+ params.set('session_type', flags.type);
1383
+ params.set('limit', String(readIntFlag(flags, 'limit', 20)));
1384
+ const suffix = params.toString();
1385
+ const result = await api('GET', `/sessions${suffix ? `?${suffix}` : ''}`);
1386
+ let sessions = result.sessions ?? [];
1387
+ if (flags.runtime) {
1388
+ const runtime = flags.runtime.toLowerCase();
1389
+ sessions = sessions.filter((session) => (session.runtime || '').toLowerCase().includes(runtime));
1390
+ }
1391
+ if (flags.query) {
1392
+ const query = flags.query.toLowerCase();
1393
+ sessions = sessions.filter((session) => {
1394
+ const haystack = [
1395
+ session.label,
1396
+ session.summary,
1397
+ session.lastActivityContent,
1398
+ session.sessionType,
1399
+ session.runtime,
1400
+ session.userName,
1401
+ session.userEmail,
1402
+ ]
1403
+ .filter(Boolean)
1404
+ .join('\n')
1405
+ .toLowerCase();
1406
+ return haystack.includes(query);
1407
+ });
1408
+ }
1409
+ return sessions;
1410
+ }
1411
+ async function fetchSession(sessionId) {
1412
+ const result = await api('GET', `/sessions/${encodeURIComponent(sessionId)}`);
1413
+ return result.session ?? null;
1414
+ }
1415
+ async function fetchSessionActivities(sessionId, limit) {
1416
+ const result = await api('GET', `/session-activities?sessionId=${encodeURIComponent(sessionId)}&limit=${limit}`);
1417
+ return Array.isArray(result) ? result : [];
1418
+ }
1419
+ async function sessionsList(flags) {
1420
+ const sessions = await fetchSessions(flags);
1421
+ if (isJson(flags)) {
1422
+ printJson(sessions);
1423
+ return;
1424
+ }
1425
+ formatSessionTable(sessions);
1426
+ }
1427
+ async function sessionsGet(sessionId, flags) {
1428
+ if (!sessionId)
1429
+ die('Usage: btx sessions get <session-id> [--activity-limit <n>]');
1430
+ const session = await fetchSession(sessionId);
1431
+ if (!session)
1432
+ die(`Session not found: ${sessionId}`);
1433
+ const activities = await fetchSessionActivities(sessionId, readIntFlag(flags, 'activity-limit', 10, 1, 50));
1434
+ if (isJson(flags)) {
1435
+ printJson({ session, activities });
1436
+ return;
1437
+ }
1438
+ formatSessionDetail(session, activities);
1439
+ }
1147
1440
  async function sessionNotesAdd(sessionId, flags) {
1148
1441
  if (!sessionId)
1149
1442
  die('Usage: btx sessions notes add <session-id> --content "..."');
@@ -1328,11 +1621,13 @@ async function globalSearch(flags) {
1328
1621
  }
1329
1622
  if (matches.length === 0) {
1330
1623
  console.log(`No tasks found matching "${query}".`);
1331
- console.log('\nTo search coding sessions, use Cmd+Shift+F in the BTX app.');
1624
+ console.log('\nFor recent coding context, use `node "$BTX_CLI_PATH" sessions list --limit 10`.');
1625
+ console.log('For full-text transcript search, use Cmd+Shift+F in the BTX app.');
1332
1626
  return;
1333
1627
  }
1334
1628
  formatTaskTable(matches);
1335
- console.log('\nTo search coding sessions, use Cmd+Shift+F in the BTX app.');
1629
+ console.log('\nFor recent coding context, use `node "$BTX_CLI_PATH" sessions list --limit 10`.');
1630
+ console.log('For full-text transcript search, use Cmd+Shift+F in the BTX app.');
1336
1631
  }
1337
1632
  // ── User profile notes commands ───────────────────────────────────────────────
1338
1633
  async function userNotesAdd(flags) {
@@ -1796,6 +2091,10 @@ async function fetchMeetings() {
1796
2091
  const result = await api('GET', '/meetings');
1797
2092
  return result?.meetings ?? [];
1798
2093
  }
2094
+ async function fetchRecordings() {
2095
+ const result = await api('GET', '/recordings');
2096
+ return result?.recordings ?? [];
2097
+ }
1799
2098
  async function meetingsList(flags) {
1800
2099
  let meetings = await fetchMeetings();
1801
2100
  if (flags.days) {
@@ -1842,6 +2141,71 @@ async function meetingsTranscript(id, flags) {
1842
2141
  }
1843
2142
  formatMeetingTranscript(meeting);
1844
2143
  }
2144
+ async function recordingsList(flags) {
2145
+ let recordings = await fetchRecordings();
2146
+ if (flags.days) {
2147
+ const cutoff = new Date();
2148
+ cutoff.setDate(cutoff.getDate() - Number(flags.days));
2149
+ const cutoffStr = cutoff.toISOString();
2150
+ recordings = recordings.filter((recording) => {
2151
+ const timestamp = recordingTimestamp(recording);
2152
+ return timestamp != null && timestamp >= cutoffStr;
2153
+ });
2154
+ }
2155
+ if (flags.status) {
2156
+ const expected = flags.status.toLowerCase();
2157
+ recordings = recordings.filter((recording) => (recording.status || '').toLowerCase() === expected);
2158
+ }
2159
+ if (flags.source) {
2160
+ const expected = flags.source.toLowerCase();
2161
+ recordings = recordings.filter((recording) => (recording.source || '').toLowerCase() === expected);
2162
+ }
2163
+ if (flags.query) {
2164
+ const q = flags.query.toLowerCase();
2165
+ recordings = recordings.filter((recording) => recordingTitle(recording).toLowerCase().includes(q) ||
2166
+ (recording.originalFilename || '').toLowerCase().includes(q) ||
2167
+ (recording.transcriptText || '').toLowerCase().includes(q) ||
2168
+ (recording.source || '').toLowerCase().includes(q) ||
2169
+ (recording.status || '').toLowerCase().includes(q));
2170
+ }
2171
+ if (isJson(flags)) {
2172
+ printJson(recordings);
2173
+ return;
2174
+ }
2175
+ formatRecordingTable(recordings);
2176
+ }
2177
+ async function recordingsGet(id, flags) {
2178
+ if (!id)
2179
+ die('Usage: btx recordings get <recording-id>');
2180
+ const recordings = await fetchRecordings();
2181
+ const recording = recordings.find((item) => item.id === id);
2182
+ if (!recording)
2183
+ die(`Recording not found: ${id}`);
2184
+ if (isJson(flags)) {
2185
+ printJson(recording);
2186
+ return;
2187
+ }
2188
+ formatRecordingDetail(recording);
2189
+ }
2190
+ async function recordingsTranscript(id, flags) {
2191
+ if (!id)
2192
+ die('Usage: btx recordings transcript <recording-id>');
2193
+ const recordings = await fetchRecordings();
2194
+ const recording = recordings.find((item) => item.id === id);
2195
+ if (!recording)
2196
+ die(`Recording not found: ${id}`);
2197
+ if (isJson(flags)) {
2198
+ printJson({
2199
+ id: recording.id,
2200
+ meetingId: recording.meetingId ?? null,
2201
+ title: recordingTitle(recording),
2202
+ transcriptText: recording.transcriptText ?? '',
2203
+ transcript: recording.transcript ?? [],
2204
+ });
2205
+ return;
2206
+ }
2207
+ formatRecordingTranscript(recording);
2208
+ }
1845
2209
  // ── Main ────────────────────────────────────────────────────────────────────
1846
2210
  export async function runRuntimeCli(args, env = process.env) {
1847
2211
  configureRuntime(env);
@@ -1862,6 +2226,7 @@ export async function runRuntimeCli(args, env = process.env) {
1862
2226
  'contacts',
1863
2227
  'orgs',
1864
2228
  'meetings',
2229
+ 'recordings',
1865
2230
  'leads',
1866
2231
  'user',
1867
2232
  'intro-paths',
@@ -1923,7 +2288,14 @@ export async function runRuntimeCli(args, env = process.env) {
1923
2288
  die(`Unknown sessions notes command: "${subCommand || '(none)'}". Run: node "$BTX_CLI_PATH" sessions --help`);
1924
2289
  }
1925
2290
  }
1926
- die(`Unknown sessions command: "${command || '(none)'}". Run: node "$BTX_CLI_PATH" sessions --help`);
2291
+ switch (command) {
2292
+ case 'list':
2293
+ return sessionsList(flags);
2294
+ case 'get':
2295
+ return sessionsGet(positional[2], flags);
2296
+ default:
2297
+ die(`Unknown sessions command: "${command || '(none)'}". Run: node "$BTX_CLI_PATH" sessions --help`);
2298
+ }
1927
2299
  }
1928
2300
  else if (resource === 'contacts') {
1929
2301
  if (command === 'notes') {
@@ -1997,6 +2369,18 @@ export async function runRuntimeCli(args, env = process.env) {
1997
2369
  die(`Unknown meetings command: "${command || '(none)'}". Run: node "$BTX_CLI_PATH" meetings --help`);
1998
2370
  }
1999
2371
  }
2372
+ else if (resource === 'recordings') {
2373
+ switch (command) {
2374
+ case 'list':
2375
+ return recordingsList(flags);
2376
+ case 'get':
2377
+ return recordingsGet(positional[2], flags);
2378
+ case 'transcript':
2379
+ return recordingsTranscript(positional[2], flags);
2380
+ default:
2381
+ die(`Unknown recordings command: "${command || '(none)'}". Run: node "$BTX_CLI_PATH" recordings --help`);
2382
+ }
2383
+ }
2000
2384
  else if (resource === 'intro-paths') {
2001
2385
  switch (command) {
2002
2386
  case 'find':