@secondcontext/btx-cli 0.0.1

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,2088 @@
1
+ // BTX CLI - self-contained script for managing BTX project data from coding agents.
2
+ // Reads BTX_ACCESS_TOKEN, BTX_REFRESH_TOKEN, BTX_SUPABASE_URL, BTX_SUPABASE_ANON_KEY,
3
+ // BTX_PROJECT_ID, BTX_API_URL, BTX_SESSION_ID, BTX_CLI_PATH from environment.
4
+ import { randomUUID } from 'node:crypto';
5
+ // ── Config ──────────────────────────────────────────────────────────────────
6
+ let TOKEN = undefined;
7
+ let REFRESH_TOKEN = '';
8
+ let SUPABASE_URL = '';
9
+ let SUPABASE_ANON_KEY = '';
10
+ let PROJECT_ID = undefined;
11
+ let API_URL = 'https://btx.secondcontext.com';
12
+ let IS_DEV = false;
13
+ let SESSION_ID = '';
14
+ let CLI_COMMAND_HINT = 'btx';
15
+ function configureRuntime(env = process.env) {
16
+ TOKEN = env.BTX_ACCESS_TOKEN;
17
+ REFRESH_TOKEN = env.BTX_REFRESH_TOKEN || '';
18
+ SUPABASE_URL = env.BTX_SUPABASE_URL || '';
19
+ SUPABASE_ANON_KEY = env.BTX_SUPABASE_ANON_KEY || '';
20
+ PROJECT_ID = env.BTX_PROJECT_ID;
21
+ API_URL = env.BTX_API_URL || 'https://btx.secondcontext.com';
22
+ IS_DEV = API_URL.includes('localhost') || API_URL.includes('127.0.0.1');
23
+ SESSION_ID = env.BTX_SESSION_ID || '';
24
+ CLI_COMMAND_HINT = env.BTX_CLI_COMMAND || (env.BTX_CLI_PATH ? 'node "$BTX_CLI_PATH"' : 'btx');
25
+ }
26
+ function die(msg) {
27
+ process.stderr.write(`Error: ${renderCliText(msg)}\n`);
28
+ process.exit(1);
29
+ }
30
+ function renderCliText(value) {
31
+ return value.replaceAll('node "$BTX_CLI_PATH"', CLI_COMMAND_HINT);
32
+ }
33
+ function taskDeepLink(taskId) {
34
+ const protocol = IS_DEV ? 'btx-dev' : 'btx';
35
+ return `${protocol}://navigate?panel=tasks&taskId=${taskId}`;
36
+ }
37
+ function contactDeepLink(contactId) {
38
+ const protocol = IS_DEV ? 'btx-dev' : 'btx';
39
+ return `${protocol}://navigate?panel=contacts&contactId=${contactId}`;
40
+ }
41
+ function orgDeepLink(orgId) {
42
+ const protocol = IS_DEV ? 'btx-dev' : 'btx';
43
+ return `${protocol}://navigate?panel=companies&orgId=${orgId}`;
44
+ }
45
+ function noteDeepLink(noteId) {
46
+ const protocol = IS_DEV ? 'btx-dev' : 'btx';
47
+ return `${protocol}://navigate?panel=notes&noteId=${noteId}`;
48
+ }
49
+ function meetingDeepLink(meetingId) {
50
+ const protocol = IS_DEV ? 'btx-dev' : 'btx';
51
+ return `${protocol}://navigate?panel=meetings&meetingId=${meetingId}`;
52
+ }
53
+ // Auth is checked inside main() so --help works without a session
54
+ // ── HTTP client ─────────────────────────────────────────────────────────────
55
+ async function refreshAccessToken() {
56
+ if (!REFRESH_TOKEN || !SUPABASE_URL || !SUPABASE_ANON_KEY)
57
+ return false;
58
+ try {
59
+ const res = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`, {
60
+ method: 'POST',
61
+ headers: { 'content-type': 'application/json', apikey: SUPABASE_ANON_KEY },
62
+ body: JSON.stringify({ refresh_token: REFRESH_TOKEN })
63
+ });
64
+ if (!res.ok)
65
+ return false;
66
+ const data = await res.json();
67
+ if (data.access_token) {
68
+ TOKEN = data.access_token;
69
+ return true;
70
+ }
71
+ return false;
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ async function apiRequest(method, path, body) {
78
+ const url = `${API_URL}/api/projects/${PROJECT_ID}${path}`;
79
+ const opts = {
80
+ method,
81
+ headers: {
82
+ 'content-type': 'application/json',
83
+ authorization: `Bearer ${TOKEN}`
84
+ }
85
+ };
86
+ if (body !== undefined)
87
+ opts.body = JSON.stringify(body);
88
+ return fetch(url, opts);
89
+ }
90
+ async function api(method, path, body) {
91
+ let res = await apiRequest(method, path, body);
92
+ // On 401, try refreshing the token once and retry
93
+ if (res.status === 401) {
94
+ const refreshed = await refreshAccessToken();
95
+ if (refreshed) {
96
+ res = await apiRequest(method, path, body);
97
+ }
98
+ if (res.status === 401) {
99
+ throw new Error('BTX auth token expired. Please start a new chat session.');
100
+ }
101
+ }
102
+ if (res.status === 429)
103
+ throw new Error('Rate limited. Please wait a moment and try again.');
104
+ const json = (await res.json());
105
+ if (!res.ok || !json.ok)
106
+ throw new Error(JSON.stringify(json.error) || `Request failed (${res.status})`);
107
+ return json.data;
108
+ }
109
+ // ── Arg parsing ─────────────────────────────────────────────────────────────
110
+ function parseArgs(argv) {
111
+ const positional = [];
112
+ const flags = {};
113
+ let i = 0;
114
+ while (i < argv.length) {
115
+ const arg = argv[i];
116
+ if (arg.startsWith('--')) {
117
+ const key = arg.slice(2);
118
+ const next = argv[i + 1];
119
+ if (next !== undefined && !next.startsWith('--')) {
120
+ flags[key] = next;
121
+ i += 2;
122
+ }
123
+ else {
124
+ flags[key] = 'true';
125
+ i += 1;
126
+ }
127
+ }
128
+ else {
129
+ positional.push(arg);
130
+ i += 1;
131
+ }
132
+ }
133
+ return { positional, flags };
134
+ }
135
+ function isJson(flags) {
136
+ return flags.json === 'true';
137
+ }
138
+ function printJson(value) {
139
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
140
+ }
141
+ // ── Formatting ──────────────────────────────────────────────────────────────
142
+ function truncate(str, max) {
143
+ if (!str)
144
+ return '';
145
+ return str.length > max ? str.slice(0, max - 1) + '\u2026' : str;
146
+ }
147
+ function padRight(str, len) {
148
+ return (str || '').padEnd(len);
149
+ }
150
+ function taskPayload(t) {
151
+ return JSON.stringify({
152
+ id: t.id,
153
+ title: t.title,
154
+ status: t.status,
155
+ category: t.category,
156
+ priority: t.priority,
157
+ description: t.description || null
158
+ });
159
+ }
160
+ function formatTaskTable(tasks) {
161
+ if (tasks.length === 0) {
162
+ console.log('No tasks found.');
163
+ return;
164
+ }
165
+ const idW = 10;
166
+ const statusW = 12;
167
+ const prioW = 8;
168
+ const catW = 10;
169
+ console.log(`${padRight('ID', idW)} ${padRight('Status', statusW)} ${padRight('Priority', prioW)} ${padRight('Category', catW)} Title`);
170
+ console.log(`${'\u2500'.repeat(idW)} ${'\u2500'.repeat(statusW)} ${'\u2500'.repeat(prioW)} ${'\u2500'.repeat(catW)} ${'\u2500'.repeat(40)}`);
171
+ for (const t of tasks) {
172
+ const id = truncate(t.id, idW);
173
+ const title = truncate(t.title.replace(/\n/g, ' '), 50);
174
+ console.log(`${padRight(id, idW)} ${padRight(t.status, statusW)} ${padRight(t.priority, prioW)} ${padRight(t.category, catW)} ${title}`);
175
+ }
176
+ console.log(`\n${tasks.length} task${tasks.length !== 1 ? 's' : ''} found`);
177
+ for (const t of tasks) {
178
+ console.log(`BTX_TASK_JSON: ${taskPayload(t)}`);
179
+ }
180
+ }
181
+ function formatTaskDetail(t) {
182
+ console.log(`Task: ${t.title}`);
183
+ console.log(`ID: ${t.id}`);
184
+ console.log(`Status: ${t.status}`);
185
+ console.log(`Priority: ${t.priority}`);
186
+ console.log(`Category: ${t.category}`);
187
+ console.log(`Created: ${new Date(t.detectedAt).toISOString().slice(0, 10)}`);
188
+ if (t.description) {
189
+ console.log(`\nDescription:\n ${t.description.replace(/\n/g, '\n ')}`);
190
+ }
191
+ if (t.plan) {
192
+ console.log(`\nPlan:\n ${t.plan.replace(/\n/g, '\n ')}`);
193
+ }
194
+ console.log(`\nBTX_TASK_JSON: ${taskPayload(t)}`);
195
+ }
196
+ // ── Contact / Lead formatters ────────────────────────────────────────────────
197
+ function contactPayload(c) {
198
+ return JSON.stringify({
199
+ id: c.id,
200
+ name: c.name,
201
+ email: c.email || null,
202
+ company: c.company || null,
203
+ role: c.role || null,
204
+ stage: c.stage,
205
+ leadTypeId: c.leadTypeId || null,
206
+ notes: c.notes || null,
207
+ createdAt: c.createdAt ? new Date(c.createdAt).toISOString().slice(0, 10) : null
208
+ });
209
+ }
210
+ function formatContactTable(contacts, { showNote } = {}) {
211
+ if (contacts.length === 0) {
212
+ console.log('No contacts found.');
213
+ return;
214
+ }
215
+ const idW = 10;
216
+ const stageW = 12;
217
+ const dateW = 10;
218
+ const companyW = 16;
219
+ console.log(`${padRight('ID', idW)} ${padRight('Added', dateW)} ${padRight('Stage', stageW)} ${padRight('Company', companyW)} Name`);
220
+ console.log(`${'\u2500'.repeat(idW)} ${'\u2500'.repeat(dateW)} ${'\u2500'.repeat(stageW)} ${'\u2500'.repeat(companyW)} ${'\u2500'.repeat(30)}`);
221
+ for (const c of contacts) {
222
+ const id = truncate(c.id, idW);
223
+ const name = truncate(c.name, 40);
224
+ const company = truncate(c.company || '', companyW);
225
+ const added = c.createdAt ? new Date(c.createdAt).toISOString().slice(0, 10) : '';
226
+ console.log(`${padRight(id, idW)} ${padRight(added, dateW)} ${padRight(c.stage, stageW)} ${padRight(company, companyW)} ${name}`);
227
+ }
228
+ console.log(`\n${contacts.length} contact${contacts.length !== 1 ? 's' : ''} found (sorted newest first)`);
229
+ if (showNote)
230
+ console.log(showNote);
231
+ for (const c of contacts) {
232
+ console.log(`BTX_CONTACT_JSON: ${contactPayload(c)}`);
233
+ }
234
+ }
235
+ function formatContactDetail(c) {
236
+ console.log(`Contact: ${c.name}`);
237
+ console.log(`ID: ${c.id}`);
238
+ console.log(`Stage: ${c.stage}`);
239
+ if (c.email)
240
+ console.log(`Email: ${c.email}`);
241
+ if (c.company)
242
+ console.log(`Company: ${c.company}`);
243
+ if (c.role)
244
+ console.log(`Role: ${c.role}`);
245
+ if (c.linkedinUrl)
246
+ console.log(`LinkedIn: ${c.linkedinUrl}`);
247
+ if (c.leadTypeId)
248
+ console.log(`Lead Type ID: ${c.leadTypeId}`);
249
+ if (c.relevanceScore != null)
250
+ console.log(`Relevance: ${c.relevanceScore}`);
251
+ if (c.rankCategory)
252
+ console.log(`Rank: ${c.rankCategory}`);
253
+ if (c.createdAt)
254
+ console.log(`Added: ${new Date(c.createdAt).toISOString().slice(0, 10)}`);
255
+ if (c.whyMatch)
256
+ console.log(`\nWhy Match:\n ${c.whyMatch.replace(/\n/g, '\n ')}`);
257
+ if (c.outreachAngle)
258
+ console.log(`\nOutreach Angle:\n ${c.outreachAngle.replace(/\n/g, '\n ')}`);
259
+ if (c.notes)
260
+ console.log(`\nNotes:\n ${c.notes.replace(/\n/g, '\n ')}`);
261
+ console.log(`\nDeep link: ${contactDeepLink(c.id)}`);
262
+ console.log(`BTX_CONTACT_JSON: ${contactPayload(c)}`);
263
+ }
264
+ function formatLeadTypeTable(leads) {
265
+ if (leads.length === 0) {
266
+ console.log('No lead types found.');
267
+ return;
268
+ }
269
+ const idW = 10;
270
+ const catW = 12;
271
+ console.log(`${padRight('ID', idW)} ${padRight('Category', catW)} Title`);
272
+ console.log(`${'\u2500'.repeat(idW)} ${'\u2500'.repeat(catW)} ${'\u2500'.repeat(40)}`);
273
+ for (const lt of leads) {
274
+ const id = truncate(lt.id, idW);
275
+ const title = truncate(lt.title, 50);
276
+ console.log(`${padRight(id, idW)} ${padRight(lt.category, catW)} ${title}`);
277
+ }
278
+ console.log(`\n${leads.length} lead type${leads.length !== 1 ? 's' : ''} found`);
279
+ }
280
+ function formatLeadTypeDetail(lt) {
281
+ console.log(`Lead Type: ${lt.title}`);
282
+ console.log(`ID: ${lt.id}`);
283
+ console.log(`Category: ${lt.category}`);
284
+ if (lt.location)
285
+ console.log(`Location: ${lt.location}`);
286
+ if (lt.description)
287
+ console.log(`\nDescription:\n ${lt.description.replace(/\n/g, '\n ')}`);
288
+ if (lt.searchDescription)
289
+ console.log(`\nSearch Description:\n ${lt.searchDescription.replace(/\n/g, '\n ')}`);
290
+ }
291
+ // ── Meeting formatters ──────────────────────────────────────────────────────
292
+ function formatMeetingTable(meetings) {
293
+ if (meetings.length === 0) {
294
+ console.log('No meetings found.');
295
+ return;
296
+ }
297
+ const idW = 10;
298
+ const dateW = 12;
299
+ const sourceW = 10;
300
+ const attendeeW = 6;
301
+ console.log(`${padRight('ID', idW)} ${padRight('Date', dateW)} ${padRight('Source', sourceW)} ${padRight('Ppl', attendeeW)} Title`);
302
+ console.log(`${'\u2500'.repeat(idW)} ${'\u2500'.repeat(dateW)} ${'\u2500'.repeat(sourceW)} ${'\u2500'.repeat(attendeeW)} ${'\u2500'.repeat(40)}`);
303
+ for (const m of meetings) {
304
+ const id = truncate(m.id, idW);
305
+ const title = truncate(m.title, 50);
306
+ const date = m.meetingDate ? new Date(m.meetingDate).toISOString().slice(0, 10) : '';
307
+ const ppl = String((m.attendees || []).length);
308
+ console.log(`${padRight(id, idW)} ${padRight(date, dateW)} ${padRight(m.source || '', sourceW)} ${padRight(ppl, attendeeW)} ${title}`);
309
+ }
310
+ console.log(`\n${meetings.length} meeting${meetings.length !== 1 ? 's' : ''} found`);
311
+ }
312
+ function formatMeetingDetail(m) {
313
+ console.log(`Meeting: ${m.title}`);
314
+ console.log(`ID: ${m.id}`);
315
+ if (m.meetingDate)
316
+ console.log(`Date: ${new Date(m.meetingDate).toISOString().slice(0, 10)}`);
317
+ console.log(`Source: ${m.source}`);
318
+ if (m.attendees && m.attendees.length) {
319
+ const names = m.attendees.map((a) => a.name + (a.email ? ` <${a.email}>` : '')).join(', ');
320
+ console.log(`Attendees: ${names}`);
321
+ }
322
+ if (m.summary)
323
+ console.log(`\nSummary:\n ${m.summary.replace(/\n/g, '\n ')}`);
324
+ if (m.aiSummary)
325
+ console.log(`\nAI Summary:\n ${m.aiSummary.replace(/\n/g, '\n ')}`);
326
+ if (m.intel && m.intel.takeaways && m.intel.takeaways.length) {
327
+ console.log(`\nTakeaways:`);
328
+ for (const t of m.intel.takeaways)
329
+ console.log(` - ${t}`);
330
+ }
331
+ if (m.actionItems && m.actionItems.length) {
332
+ console.log(`\nAction Items:`);
333
+ for (const a of m.actionItems)
334
+ console.log(` - ${a}`);
335
+ }
336
+ if (m.keyPoints && m.keyPoints.length) {
337
+ console.log(`\nKey Points:`);
338
+ for (const k of m.keyPoints)
339
+ console.log(` - ${k}`);
340
+ }
341
+ console.log(`\nDeep link: ${meetingDeepLink(m.id)}`);
342
+ }
343
+ function formatMeetingTranscript(m) {
344
+ if (!m.transcript || m.transcript.length === 0) {
345
+ console.log('No transcript available for this meeting.');
346
+ return;
347
+ }
348
+ console.log(`Transcript: ${m.title}`);
349
+ if (m.meetingDate)
350
+ console.log(`Date: ${new Date(m.meetingDate).toISOString().slice(0, 10)}`);
351
+ console.log(`Segments: ${m.transcript.length}`);
352
+ console.log('');
353
+ for (const seg of m.transcript) {
354
+ const speaker = seg.speaker && seg.speaker.source === 'speaker' ? 'You' : 'Other';
355
+ const time = seg.start_time || '';
356
+ console.log(`[${time}] ${speaker}: ${seg.text}`);
357
+ }
358
+ }
359
+ // ── Help ─────────────────────────────────────────────────────────────────────
360
+ const HELP = {
361
+ top: `BTX CLI - manage tasks, contacts, meetings, and leads from a session.
362
+
363
+ Usage:
364
+ node "$BTX_CLI_PATH" <resource> <command> [flags]
365
+ node "$BTX_CLI_PATH" <resource> --help
366
+
367
+ Resources:
368
+ tasks Create and manage project tasks
369
+ sessions Add and list notes for a session
370
+ contacts View and update contacts, add notes
371
+ orgs View organizations, add notes
372
+ meetings View meeting recordings and transcripts
373
+ leads List lead types
374
+ user Manage your profile notes
375
+ intro-paths Find warm intro paths in your network
376
+ search Search tasks by keyword
377
+ context Fetch the current project business context
378
+ pages Create and list project note pages
379
+
380
+ Examples:
381
+ node "$BTX_CLI_PATH" tasks list --status todo
382
+ node "$BTX_CLI_PATH" tasks list --json
383
+ node "$BTX_CLI_PATH" meetings list --days 7
384
+ node "$BTX_CLI_PATH" meetings get <meeting-id> --transcript
385
+ node "$BTX_CLI_PATH" contacts --help
386
+
387
+ All resource commands support --json for machine-readable output.`,
388
+ tasks: `BTX tasks - create and manage project tasks.
389
+
390
+ Commands:
391
+ list List tasks (filterable)
392
+ get Get full details for a task
393
+ create Create a new task
394
+ update Update task fields
395
+ complete Mark a task as done
396
+ notes list <task-id> List notes for a task
397
+ notes add <task-id> --content "..." Add a note to a task
398
+
399
+ Flags:
400
+ tasks list [--status todo|in_progress|done|dismissed] [--category code|marketing|sales|customer|product|ops] [--query "text"]
401
+ tasks get <task-id>
402
+ tasks create --title "..." --description "..." --category <cat> [--priority high|medium|low]
403
+ tasks update <task-id> [--title "..."] [--description "..."] [--status <status>] [--priority <p>] [--category <cat>] [--plan "markdown"]
404
+ tasks complete <task-id>
405
+ tasks notes list <task-id>
406
+ tasks notes add <task-id> --content "Label: value" [--source "https://..."]
407
+
408
+ Examples:
409
+ node "$BTX_CLI_PATH" tasks list
410
+ node "$BTX_CLI_PATH" tasks list --status todo
411
+ node "$BTX_CLI_PATH" tasks list --query "auth" --status in_progress
412
+ node "$BTX_CLI_PATH" tasks get abc123
413
+ node "$BTX_CLI_PATH" tasks create --title "Fix login bug" --description "Users can't log in with SSO" --category code
414
+ node "$BTX_CLI_PATH" tasks create --title "Write onboarding email" --description "..." --category marketing --priority high
415
+ node "$BTX_CLI_PATH" tasks update abc123 --status in_progress
416
+ node "$BTX_CLI_PATH" tasks update abc123 --plan "1. Read auth.ts\\n2. Fix token refresh"
417
+ node "$BTX_CLI_PATH" tasks complete abc123
418
+ node "$BTX_CLI_PATH" tasks notes list abc123
419
+ 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.
421
+
422
+ Commands:
423
+ notes list <session-id> List notes for a session
424
+ notes add <session-id> --content "..." Add a note to a session
425
+
426
+ Flags:
427
+ sessions notes add <session-id> --content "Label: value" [--source "https://..."]
428
+
429
+ Examples:
430
+ node "$BTX_CLI_PATH" sessions notes list abc-session-id
431
+ node "$BTX_CLI_PATH" sessions notes add abc-session-id --content "Insight: market timing is critical"
432
+ node "$BTX_CLI_PATH" sessions notes add abc-session-id --content "Decision: pivot to enterprise" --source "https://..."`,
433
+ contacts: `BTX contacts - create, view and update contacts, add research notes.
434
+
435
+ Commands:
436
+ create Create a new contact
437
+ list List contacts (filterable)
438
+ get Get full details for a contact
439
+ update Update contact fields
440
+ notes list List notes for a contact
441
+ notes add Add a note to a contact
442
+
443
+ Flags:
444
+ contacts create --name "..." [--email "..."] [--company "..."] [--role "..."] [--linkedin "..."] [--stage lead|contacted|replied|scheduled|interviewed|converted|lost] [--notes "..."] [--outreach-angle "..."]
445
+ contacts list [--stage lead|contacted|replied|scheduled|interviewed|converted|lost] [--query "text"] [--limit <n>]
446
+ contacts get <contact-id>
447
+ contacts update <contact-id> [--stage <stage>] [--email "..."] [--company "..."] [--role "..."] [--notes "..."] [--outreach-angle "..."]
448
+ contacts notes list <contact-id>
449
+ contacts notes add <contact-id> --content "Label: value" [--source "https://..."]
450
+
451
+ Examples:
452
+ node "$BTX_CLI_PATH" contacts create --name "Jane Smith" --company "Acme Corp" --role "CTO" --email "jane@acme.com"
453
+ node "$BTX_CLI_PATH" contacts list
454
+ node "$BTX_CLI_PATH" contacts list --stage lead --limit 20
455
+ node "$BTX_CLI_PATH" contacts list --query "acme"
456
+ node "$BTX_CLI_PATH" contacts get abc123
457
+ node "$BTX_CLI_PATH" contacts update abc123 --stage contacted
458
+ node "$BTX_CLI_PATH" contacts notes list abc123
459
+ node "$BTX_CLI_PATH" contacts notes add abc123 --content "Role: VP Engineering" --source "https://linkedin.com/..."
460
+ node "$BTX_CLI_PATH" contacts notes add abc123 --content "Stack: Python, AWS, Postgres" --source "https://their-blog.com"`,
461
+ orgs: `BTX orgs - create, view organizations and add company-level notes.
462
+
463
+ Commands:
464
+ create Create a new organization
465
+ list List organizations
466
+ get Get full details for an organization
467
+ notes list List notes for an organization
468
+ notes add Add a note to an organization
469
+
470
+ Flags:
471
+ orgs create --name "..." [--type company|partner|competitor|investor|other] [--website "..."] [--stage "..."]
472
+ orgs list [--type <type>]
473
+ orgs get <org-id>
474
+ orgs notes list <org-id>
475
+ orgs notes add <org-id> --content "Label: value" [--source "https://..."]
476
+
477
+ Examples:
478
+ node "$BTX_CLI_PATH" orgs create --name "Acme Corp" --type company --website "https://acme.com"
479
+ node "$BTX_CLI_PATH" orgs list
480
+ node "$BTX_CLI_PATH" orgs get abc123
481
+ node "$BTX_CLI_PATH" orgs notes list abc123
482
+ node "$BTX_CLI_PATH" orgs notes add abc123 --content "Funding: Series B, $45M (2024)" --source "https://techcrunch.com/..."
483
+ node "$BTX_CLI_PATH" orgs notes add abc123 --content "Size: ~1,100 employees, 21 offices"`,
484
+ leads: `BTX leads - list and inspect lead types.
485
+
486
+ Commands:
487
+ list List all lead types
488
+ get Get full details for a lead type
489
+
490
+ Examples:
491
+ node "$BTX_CLI_PATH" leads list
492
+ node "$BTX_CLI_PATH" leads get abc123`,
493
+ user: `BTX user - manage your profile notes (facts about you, not contacts).
494
+
495
+ Commands:
496
+ notes list List your profile notes
497
+ notes add Add a profile note
498
+
499
+ Flags:
500
+ user notes add --content "Label: value" [--source "https://..."]
501
+
502
+ Examples:
503
+ node "$BTX_CLI_PATH" user notes list
504
+ node "$BTX_CLI_PATH" user notes add --content "Location: San Francisco, CA"
505
+ node "$BTX_CLI_PATH" user notes add --content "Network: YC W22 alumni, South Park Commons member"
506
+ node "$BTX_CLI_PATH" user notes add --content "Expertise: Enterprise SaaS, API design, Go/React"`,
507
+ 'intro-paths': `BTX intro-paths - find warm intro paths to a person via your network.
508
+
509
+ Commands:
510
+ find Search for intro paths to a contact or person
511
+
512
+ Flags:
513
+ intro-paths find --contact-id <id>
514
+ intro-paths find --name "Person Name" [--company "Company"] [--role "Role"]
515
+
516
+ Examples:
517
+ node "$BTX_CLI_PATH" intro-paths find --contact-id abc123
518
+ node "$BTX_CLI_PATH" intro-paths find --name "Jane Smith" --company "Acme Corp"
519
+ node "$BTX_CLI_PATH" intro-paths find --name "Jane Smith" --company "Acme Corp" --role "VP Sales"`,
520
+ search: `BTX search - search tasks by keyword.
521
+
522
+ Flags:
523
+ search --query "text" [--status todo|in_progress|done|dismissed]
524
+
525
+ Examples:
526
+ node "$BTX_CLI_PATH" search --query "auth"
527
+ node "$BTX_CLI_PATH" search --query "onboarding" --status todo`,
528
+ meetings: `BTX meetings - view meeting recordings and transcripts.
529
+
530
+ Commands:
531
+ list List meetings (filterable by date range)
532
+ get Get meeting details (summary, attendees, takeaways)
533
+ transcript Get the full transcript for a meeting
534
+
535
+ Flags:
536
+ meetings list [--days <n>] [--query "text"]
537
+ meetings get <meeting-id>
538
+ meetings transcript <meeting-id>
539
+
540
+ The --days flag filters to meetings from the last N days (default: all).
541
+ Use "list" to browse recent meetings, "get" for summaries and takeaways,
542
+ and "transcript" for the full conversation text.
543
+
544
+ Examples:
545
+ node "$BTX_CLI_PATH" meetings list
546
+ node "$BTX_CLI_PATH" meetings list --days 7
547
+ node "$BTX_CLI_PATH" meetings list --query "onboarding"
548
+ node "$BTX_CLI_PATH" meetings get abc123
549
+ node "$BTX_CLI_PATH" meetings transcript abc123`,
550
+ context: `BTX context - fetch the current project business context.
551
+
552
+ Commands:
553
+ get Print the project's business context
554
+
555
+ Examples:
556
+ node "$BTX_CLI_PATH" context get`,
557
+ pages: `BTX pages - create, update, and list project note pages.
558
+
559
+ Commands:
560
+ create Create a new note page
561
+ update Update an existing note page
562
+ list List all note pages
563
+ get Get a specific note page by ID
564
+
565
+ Flags:
566
+ pages create --title "..." [--body "markdown content"]
567
+ pages update <note-id> [--title "..."] [--body "markdown content"]
568
+ pages list
569
+ pages get <note-id>
570
+
571
+ Body format:
572
+ The --body flag accepts markdown: # headings, ## subheadings, - bullet lists,
573
+ **bold**, *italic*, and regular paragraphs. Content is converted to rich text.
574
+
575
+ Examples:
576
+ node "$BTX_CLI_PATH" pages create --title "Session summary" --body "## What changed\\n- Refactored auth\\n- Added tests"
577
+ node "$BTX_CLI_PATH" pages update abc123 --body "## Updated notes\\n- Fixed edge case"
578
+ node "$BTX_CLI_PATH" pages list
579
+ node "$BTX_CLI_PATH" pages get abc123`
580
+ };
581
+ // ── Commands ────────────────────────────────────────────────────────────────
582
+ async function fetchTasks() {
583
+ const result = await api('GET', '/tasks');
584
+ return result?.tasks ?? [];
585
+ }
586
+ async function tasksList(flags) {
587
+ let tasks = await fetchTasks();
588
+ if (flags.status) {
589
+ tasks = tasks.filter((t) => t.status === flags.status);
590
+ }
591
+ if (flags.category) {
592
+ tasks = tasks.filter((t) => t.category === flags.category);
593
+ }
594
+ if (flags.query) {
595
+ const q = flags.query.toLowerCase();
596
+ tasks = tasks.filter((t) => t.title.toLowerCase().includes(q) ||
597
+ (t.description && t.description.toLowerCase().includes(q)));
598
+ }
599
+ if (isJson(flags)) {
600
+ printJson(tasks);
601
+ return;
602
+ }
603
+ formatTaskTable(tasks);
604
+ }
605
+ async function tasksGet(id, flags) {
606
+ if (!id)
607
+ die('Usage: btx tasks get <task-id>');
608
+ const tasks = await fetchTasks();
609
+ const task = tasks.find((t) => t.id === id);
610
+ if (!task)
611
+ die(`Task not found: ${id}`);
612
+ if (isJson(flags)) {
613
+ printJson(task);
614
+ return;
615
+ }
616
+ formatTaskDetail(task);
617
+ }
618
+ async function tasksCreate(flags) {
619
+ if (!flags.title)
620
+ die('--title is required');
621
+ if (!flags.description)
622
+ die('--description is required');
623
+ if (!flags.category)
624
+ die('--category is required');
625
+ const validCategories = ['code', 'marketing', 'sales', 'customer', 'product', 'ops'];
626
+ if (!validCategories.includes(flags.category)) {
627
+ die(`Invalid category: ${flags.category}. Must be one of: ${validCategories.join(', ')}`);
628
+ }
629
+ const validPriorities = ['high', 'medium', 'low'];
630
+ const priority = flags.priority || 'medium';
631
+ if (!validPriorities.includes(priority)) {
632
+ die(`Invalid priority: ${priority}. Must be one of: ${validPriorities.join(', ')}`);
633
+ }
634
+ const task = {
635
+ id: randomUUID(),
636
+ title: flags.title,
637
+ description: flags.description,
638
+ category: flags.category,
639
+ priority,
640
+ status: 'todo',
641
+ actionType: 'info',
642
+ detectedAt: Date.now()
643
+ };
644
+ await api('PATCH', '/tasks', task);
645
+ // Log task_created activity
646
+ if (SESSION_ID) {
647
+ try {
648
+ await api('POST', '/task-activities', {
649
+ taskLocalId: task.id,
650
+ type: 'status_changed',
651
+ metadata: { from_status: undefined, to_status: 'todo', btx_session_id: SESSION_ID }
652
+ });
653
+ }
654
+ catch {
655
+ // Non-fatal
656
+ }
657
+ }
658
+ if (isJson(flags)) {
659
+ printJson({ task, deepLink: taskDeepLink(task.id) });
660
+ return;
661
+ }
662
+ console.log(`Task created successfully.`);
663
+ console.log(`ID: ${task.id}`);
664
+ console.log(`Title: ${task.title}`);
665
+ console.log(`Deep link: ${taskDeepLink(task.id)}`);
666
+ console.log(`BTX_TASK_JSON: ${taskPayload(task)}`);
667
+ }
668
+ async function tasksUpdate(id, flags) {
669
+ if (!id)
670
+ die('Usage: btx tasks update <task-id> [--field value ...]');
671
+ const tasks = await fetchTasks();
672
+ const existing = tasks.find((t) => t.id === id);
673
+ if (!existing)
674
+ die(`Task not found: ${id}`);
675
+ const validStatuses = ['todo', 'in_progress', 'done', 'dismissed'];
676
+ const validPriorities = ['high', 'medium', 'low'];
677
+ const validCategories = ['code', 'marketing', 'sales', 'customer', 'product', 'ops'];
678
+ if (flags.status && !validStatuses.includes(flags.status)) {
679
+ die(`Invalid status: ${flags.status}. Must be one of: ${validStatuses.join(', ')}`);
680
+ }
681
+ if (flags.priority && !validPriorities.includes(flags.priority)) {
682
+ die(`Invalid priority: ${flags.priority}. Must be one of: ${validPriorities.join(', ')}`);
683
+ }
684
+ if (flags.category && !validCategories.includes(flags.category)) {
685
+ die(`Invalid category: ${flags.category}. Must be one of: ${validCategories.join(', ')}`);
686
+ }
687
+ const updatable = ['title', 'description', 'status', 'priority', 'category', 'plan'];
688
+ const changed = [];
689
+ for (const key of updatable) {
690
+ if (flags[key] !== undefined) {
691
+ existing[key] = flags[key];
692
+ changed.push(key);
693
+ }
694
+ }
695
+ if (changed.length === 0)
696
+ die('No fields to update. Use --title, --description, --status, --priority, --category, or --plan.');
697
+ await api('PATCH', '/tasks', existing);
698
+ // Log status change activity
699
+ if (flags.status) {
700
+ try {
701
+ await api('POST', '/task-activities', {
702
+ taskLocalId: id,
703
+ type: 'status_changed',
704
+ metadata: {
705
+ from_status: tasks.find((t) => t.id === id)?.status,
706
+ to_status: flags.status,
707
+ btx_session_id: SESSION_ID || undefined
708
+ }
709
+ });
710
+ }
711
+ catch {
712
+ // Non-fatal
713
+ }
714
+ }
715
+ // Log plan activity
716
+ if (flags.plan) {
717
+ try {
718
+ await api('POST', '/task-activities', {
719
+ taskLocalId: id,
720
+ type: 'plan_generated',
721
+ content: flags.plan,
722
+ metadata: { btx_session_id: SESSION_ID || undefined }
723
+ });
724
+ }
725
+ catch {
726
+ // Non-fatal
727
+ }
728
+ }
729
+ if (isJson(flags)) {
730
+ printJson({ task: existing, updatedFields: changed, deepLink: taskDeepLink(id) });
731
+ return;
732
+ }
733
+ console.log(`Task updated successfully.`);
734
+ console.log(`Updated fields: ${changed.join(', ')}`);
735
+ console.log(`Deep link: ${taskDeepLink(id)}`);
736
+ console.log(`BTX_TASK_JSON: ${taskPayload(existing)}`);
737
+ }
738
+ async function tasksComplete(id, flags) {
739
+ if (!id)
740
+ throw new Error('Usage: btx tasks complete <task-id>');
741
+ const tasks = await fetchTasks();
742
+ const existing = tasks.find((t) => t.id === id);
743
+ if (!existing)
744
+ throw new Error(`Task not found: ${id}`);
745
+ existing.status = 'done';
746
+ await api('PATCH', '/tasks', existing);
747
+ // Log plan_executed activity
748
+ try {
749
+ await api('POST', '/task-activities', {
750
+ taskLocalId: id,
751
+ type: 'plan_executed',
752
+ metadata: { btx_session_id: SESSION_ID || undefined }
753
+ });
754
+ }
755
+ catch {
756
+ // Non-fatal
757
+ }
758
+ if (isJson(flags)) {
759
+ printJson({ task: existing, deepLink: taskDeepLink(id) });
760
+ return;
761
+ }
762
+ console.log(`Task completed.`);
763
+ console.log(`Deep link: ${taskDeepLink(id)}`);
764
+ console.log(`BTX_TASK_JSON: ${taskPayload(existing)}`);
765
+ }
766
+ // ── Organizations commands ────────────────────────────────────────────────────
767
+ async function fetchOrganizations(type) {
768
+ const path = type ? `/organizations?type=${type}` : '/organizations';
769
+ const result = await api('GET', path);
770
+ return result?.organizations ?? [];
771
+ }
772
+ async function orgsList(flags) {
773
+ const type = flags.type || null;
774
+ const orgs = await fetchOrganizations(type);
775
+ if (isJson(flags)) {
776
+ printJson(orgs);
777
+ return;
778
+ }
779
+ if (orgs.length === 0) {
780
+ console.log('No organizations found.');
781
+ return;
782
+ }
783
+ console.log(`\n${orgs.length} organization${orgs.length !== 1 ? 's' : ''} found:\n`);
784
+ for (const o of orgs) {
785
+ const meta = o.metadata || {};
786
+ console.log(` ${o.name}`);
787
+ console.log(` ID: ${o.id}`);
788
+ console.log(` Type: ${o.type} | Stage: ${o.stage}`);
789
+ if (o.website)
790
+ console.log(` Website: ${o.website}`);
791
+ if (meta.industry)
792
+ console.log(` Industry: ${meta.industry}`);
793
+ if (meta.hq)
794
+ console.log(` HQ: ${meta.hq}`);
795
+ console.log(` ${orgDeepLink(o.id)}`);
796
+ console.log();
797
+ }
798
+ }
799
+ async function orgsGet(id) {
800
+ if (!id)
801
+ die('Usage: btx orgs get <org-id>');
802
+ const orgs = await fetchOrganizations();
803
+ const org = orgs.find((o) => o.id === id);
804
+ if (!org)
805
+ die(`Organization ${id} not found`);
806
+ console.log(JSON.stringify(org, null, 2));
807
+ }
808
+ async function orgNotesAdd(orgId, flags) {
809
+ if (!orgId)
810
+ die('Usage: btx orgs notes add <org-id> --content "..."');
811
+ const content = flags.content;
812
+ if (!content)
813
+ die('--content is required');
814
+ const metadata = {};
815
+ if (flags.source)
816
+ metadata.source_url = flags.source;
817
+ const result = await api('POST', '/organization-activities', {
818
+ orgId,
819
+ type: 'note',
820
+ content,
821
+ metadata
822
+ });
823
+ if (isJson(flags)) {
824
+ printJson({ id: result?.id ?? null, orgId, content, metadata, deepLink: orgDeepLink(orgId) });
825
+ return;
826
+ }
827
+ console.log(`Note saved for organization ${orgId}`);
828
+ if (result?.id)
829
+ console.log(` Activity ID: ${result.id}`);
830
+ console.log(` ${orgDeepLink(orgId)}`);
831
+ }
832
+ async function orgsCreate(flags) {
833
+ if (!flags.name)
834
+ die('--name is required');
835
+ const validTypes = ['company', 'partner', 'competitor', 'investor', 'other'];
836
+ const type = flags.type || 'company';
837
+ if (!validTypes.includes(type)) {
838
+ die(`Invalid type: ${type}. Must be one of: ${validTypes.join(', ')}`);
839
+ }
840
+ const org = {
841
+ id: randomUUID(),
842
+ name: flags.name,
843
+ type,
844
+ stage: flags.stage || 'active',
845
+ website: flags.website || null,
846
+ metadata: {},
847
+ };
848
+ await api('POST', '/organizations', org);
849
+ if (isJson(flags)) {
850
+ printJson({ organization: org, deepLink: orgDeepLink(org.id) });
851
+ return;
852
+ }
853
+ console.log(`Organization created successfully.`);
854
+ console.log(`ID: ${org.id}`);
855
+ console.log(`Name: ${org.name}`);
856
+ console.log(`Type: ${org.type}`);
857
+ console.log(`Deep link: ${orgDeepLink(org.id)}`);
858
+ }
859
+ async function orgNotesList(orgId, flags) {
860
+ if (!orgId)
861
+ die('Usage: btx orgs notes list <org-id>');
862
+ const activities = await api('GET', `/organization-activities?orgId=${orgId}&limit=100`);
863
+ const notes = (activities ?? []).filter((a) => a.type === 'note');
864
+ if (isJson(flags)) {
865
+ printJson(notes);
866
+ return;
867
+ }
868
+ if (notes.length === 0) {
869
+ console.log(`No notes found for organization ${orgId}.`);
870
+ return;
871
+ }
872
+ console.log(`\n${notes.length} note${notes.length !== 1 ? 's' : ''} for organization ${orgId}:\n`);
873
+ for (const n of notes) {
874
+ const source = n.metadata?.source_url ? ` [${n.metadata.source_url}]` : '';
875
+ console.log(` ${n.content}${source}`);
876
+ }
877
+ console.log();
878
+ }
879
+ // ── Contacts commands ─────────────────────────────────────────────────────────
880
+ async function fetchContacts() {
881
+ const result = await api('GET', '/contacts');
882
+ return result?.contacts ?? [];
883
+ }
884
+ async function contactsList(flags) {
885
+ let contacts = await fetchContacts();
886
+ if (flags.stage) {
887
+ contacts = contacts.filter((c) => c.stage === flags.stage);
888
+ }
889
+ if (flags['lead-type']) {
890
+ contacts = contacts.filter((c) => c.leadTypeId === flags['lead-type']);
891
+ }
892
+ if (flags.query) {
893
+ const q = flags.query.toLowerCase();
894
+ contacts = contacts.filter((c) => (c.name || '').toLowerCase().includes(q) ||
895
+ (c.company || '').toLowerCase().includes(q) ||
896
+ (c.role || '').toLowerCase().includes(q) ||
897
+ (c.notes || '').toLowerCase().includes(q));
898
+ }
899
+ const total = contacts.length;
900
+ const limit = flags.limit ? parseInt(flags.limit, 10) : null;
901
+ if (limit && limit > 0)
902
+ contacts = contacts.slice(0, limit);
903
+ if (isJson(flags)) {
904
+ printJson(contacts);
905
+ return;
906
+ }
907
+ const note = limit && total > limit ? `Showing ${limit} of ${total} (use --limit to adjust)` : null;
908
+ formatContactTable(contacts, { showNote: note });
909
+ }
910
+ async function contactsGet(id, flags) {
911
+ if (!id)
912
+ die('Usage: btx contacts get <contact-id>');
913
+ const contacts = await fetchContacts();
914
+ const contact = contacts.find((c) => c.id === id);
915
+ if (!contact)
916
+ die(`Contact not found: ${id}`);
917
+ if (isJson(flags)) {
918
+ printJson(contact);
919
+ return;
920
+ }
921
+ formatContactDetail(contact);
922
+ }
923
+ async function contactsUpdate(id, flags) {
924
+ if (!id)
925
+ die('Usage: btx contacts update <contact-id> [--field value ...]');
926
+ const contacts = await fetchContacts();
927
+ const existing = contacts.find((c) => c.id === id);
928
+ if (!existing)
929
+ die(`Contact not found: ${id}`);
930
+ const validStages = [
931
+ 'lead',
932
+ 'contacted',
933
+ 'replied',
934
+ 'scheduled',
935
+ 'interviewed',
936
+ 'converted',
937
+ 'lost'
938
+ ];
939
+ if (flags.stage && !validStages.includes(flags.stage)) {
940
+ die(`Invalid stage: ${flags.stage}. Must be one of: ${validStages.join(', ')}`);
941
+ }
942
+ const updatable = ['stage', 'notes', 'outreach-angle', 'email', 'company', 'role'];
943
+ const changed = [];
944
+ if (flags.stage !== undefined) {
945
+ existing.stage = flags.stage;
946
+ changed.push('stage');
947
+ }
948
+ if (flags.notes !== undefined) {
949
+ existing.notes = flags.notes;
950
+ changed.push('notes');
951
+ }
952
+ if (flags['outreach-angle'] !== undefined) {
953
+ existing.outreachAngle = flags['outreach-angle'];
954
+ changed.push('outreachAngle');
955
+ }
956
+ if (flags.email !== undefined) {
957
+ existing.email = flags.email;
958
+ changed.push('email');
959
+ }
960
+ if (flags.company !== undefined) {
961
+ existing.company = flags.company;
962
+ changed.push('company');
963
+ }
964
+ if (flags.role !== undefined) {
965
+ existing.role = flags.role;
966
+ changed.push('role');
967
+ }
968
+ if (changed.length === 0)
969
+ die(`No fields to update. Use: ${updatable.map((f) => '--' + f).join(', ')}`);
970
+ await api('POST', '/contacts', existing);
971
+ if (isJson(flags)) {
972
+ printJson({ contact: existing, updatedFields: changed, deepLink: contactDeepLink(existing.id) });
973
+ return;
974
+ }
975
+ console.log(`Contact updated successfully.`);
976
+ console.log(`Updated fields: ${changed.join(', ')}`);
977
+ console.log(`Deep link: ${contactDeepLink(existing.id)}`);
978
+ console.log(`BTX_CONTACT_JSON: ${contactPayload(existing)}`);
979
+ }
980
+ async function contactsCreate(flags) {
981
+ if (!flags.name)
982
+ die('--name is required');
983
+ const contact = {
984
+ id: randomUUID(),
985
+ name: flags.name,
986
+ stage: flags.stage || 'lead',
987
+ email: flags.email || null,
988
+ company: flags.company || null,
989
+ role: flags.role || null,
990
+ linkedinUrl: flags.linkedin || null,
991
+ notes: flags.notes || null,
992
+ outreachAngle: flags['outreach-angle'] || null,
993
+ };
994
+ await api('POST', '/contacts', contact);
995
+ if (isJson(flags)) {
996
+ printJson({ contact, deepLink: contactDeepLink(contact.id) });
997
+ return;
998
+ }
999
+ console.log(`Contact created successfully.`);
1000
+ console.log(`ID: ${contact.id}`);
1001
+ console.log(`Name: ${contact.name}`);
1002
+ console.log(`Deep link: ${contactDeepLink(contact.id)}`);
1003
+ console.log(`BTX_CONTACT_JSON: ${contactPayload(contact)}`);
1004
+ }
1005
+ // ── Contact notes commands ────────────────────────────────────────────────────
1006
+ function noteSimilarity(a, b) {
1007
+ // Rough word-overlap ratio. Good enough to catch near-duplicates.
1008
+ const words = (s) => new Set(s
1009
+ .toLowerCase()
1010
+ .replace(/[^a-z0-9\s]/g, '')
1011
+ .split(/\s+/)
1012
+ .filter(Boolean));
1013
+ const wa = words(a);
1014
+ const wb = words(b);
1015
+ if (wa.size === 0 || wb.size === 0)
1016
+ return 0;
1017
+ let overlap = 0;
1018
+ for (const w of wa)
1019
+ if (wb.has(w))
1020
+ overlap++;
1021
+ return overlap / Math.min(wa.size, wb.size);
1022
+ }
1023
+ function isNoteActivity(activity) {
1024
+ return (activity.type === 'note' && typeof activity.content === 'string' && activity.content.length > 0);
1025
+ }
1026
+ function activityDate(activity) {
1027
+ return new Date(activity.createdAt ?? Date.now()).toISOString().slice(0, 10);
1028
+ }
1029
+ async function contactNotesAdd(contactId, flags) {
1030
+ if (!contactId)
1031
+ die('Usage: btx contacts notes add <contact-id> --content "..."');
1032
+ if (!flags.content)
1033
+ die('--content is required');
1034
+ // Dedup check. Fetch existing notes and warn if very similar content already exists.
1035
+ try {
1036
+ const existing = await api('GET', `/contact-activities?contactId=${contactId}&limit=50`);
1037
+ const notes = (Array.isArray(existing) ? existing : []).filter(isNoteActivity);
1038
+ for (const n of notes) {
1039
+ if (noteSimilarity(flags.content, n.content) >= 0.7) {
1040
+ const date = activityDate(n);
1041
+ console.log(`Skipped. Similar note already exists (${date}): ${n.content}`);
1042
+ return;
1043
+ }
1044
+ }
1045
+ }
1046
+ catch {
1047
+ // Non-fatal. Proceed with save.
1048
+ }
1049
+ const metadata = { btx_session_id: SESSION_ID || undefined };
1050
+ if (flags.source)
1051
+ metadata.source_url = flags.source;
1052
+ const data = await api('POST', '/contact-activities', {
1053
+ contactId,
1054
+ type: 'note',
1055
+ content: flags.content,
1056
+ metadata
1057
+ });
1058
+ if (isJson(flags)) {
1059
+ printJson({ id: data.id ?? null, contactId, content: flags.content, metadata });
1060
+ return;
1061
+ }
1062
+ console.log(`Note saved.`);
1063
+ console.log(`Contact: ${contactId}`);
1064
+ console.log(`Deep link: ${contactDeepLink(contactId)}`);
1065
+ console.log(`BTX_CONTACT_NOTE_JSON: ${JSON.stringify({ id: data.id, contactId, content: flags.content })}`);
1066
+ }
1067
+ async function contactNotesList(contactId, flags) {
1068
+ if (!contactId)
1069
+ die('Usage: btx contacts notes list <contact-id>');
1070
+ const result = await api('GET', `/contact-activities?contactId=${contactId}&limit=50`);
1071
+ const notes = (Array.isArray(result) ? result : []).filter(isNoteActivity);
1072
+ if (isJson(flags)) {
1073
+ printJson(notes);
1074
+ return;
1075
+ }
1076
+ if (notes.length === 0) {
1077
+ console.log('No notes yet for this contact.');
1078
+ return;
1079
+ }
1080
+ console.log(`${notes.length} note${notes.length !== 1 ? 's' : ''} for contact ${contactId}:\n`);
1081
+ for (const n of notes) {
1082
+ const date = activityDate(n);
1083
+ console.log(`[${date}] ${n.content}`);
1084
+ console.log();
1085
+ }
1086
+ }
1087
+ // ── Task notes commands ──────────────────────────────────────────────────────
1088
+ async function taskNotesAdd(taskId, flags) {
1089
+ if (!taskId)
1090
+ die('Usage: btx tasks notes add <task-id> --content "..."');
1091
+ if (!flags.content)
1092
+ die('--content is required');
1093
+ // Dedup check. Fetch existing notes and warn if very similar content already exists.
1094
+ try {
1095
+ const existing = await api('GET', `/task-activities?taskLocalId=${taskId}&limit=50`);
1096
+ const notes = (Array.isArray(existing) ? existing : []).filter(isNoteActivity);
1097
+ for (const n of notes) {
1098
+ if (noteSimilarity(flags.content, n.content) >= 0.7) {
1099
+ const date = activityDate(n);
1100
+ console.log(`Skipped. Similar note already exists (${date}): ${n.content}`);
1101
+ return;
1102
+ }
1103
+ }
1104
+ }
1105
+ catch {
1106
+ // Non-fatal. Proceed with save.
1107
+ }
1108
+ const metadata = { btx_session_id: SESSION_ID || undefined };
1109
+ if (flags.source)
1110
+ metadata.source_url = flags.source;
1111
+ const data = await api('POST', '/task-activities', {
1112
+ taskLocalId: taskId,
1113
+ type: 'note',
1114
+ content: flags.content,
1115
+ metadata
1116
+ });
1117
+ if (isJson(flags)) {
1118
+ printJson({ id: data.id ?? null, taskLocalId: taskId, content: flags.content, metadata });
1119
+ return;
1120
+ }
1121
+ console.log(`Note saved.`);
1122
+ console.log(`Task: ${taskId}`);
1123
+ console.log(`Deep link: btx://navigate?panel=tasks&taskId=${taskId}`);
1124
+ console.log(`BTX_TASK_NOTE_JSON: ${JSON.stringify({ id: data.id, taskLocalId: taskId, content: flags.content })}`);
1125
+ }
1126
+ async function taskNotesList(taskId, flags) {
1127
+ if (!taskId)
1128
+ die('Usage: btx tasks notes list <task-id>');
1129
+ const result = await api('GET', `/task-activities?taskLocalId=${taskId}&limit=50`);
1130
+ const notes = (Array.isArray(result) ? result : []).filter(isNoteActivity);
1131
+ if (isJson(flags)) {
1132
+ printJson(notes);
1133
+ return;
1134
+ }
1135
+ if (notes.length === 0) {
1136
+ console.log('No notes yet for this task.');
1137
+ return;
1138
+ }
1139
+ console.log(`${notes.length} note${notes.length !== 1 ? 's' : ''} for task ${taskId}:\n`);
1140
+ for (const n of notes) {
1141
+ const date = activityDate(n);
1142
+ console.log(`[${date}] ${n.content}`);
1143
+ console.log();
1144
+ }
1145
+ }
1146
+ // ── Session notes commands ────────────────────────────────────────────────────
1147
+ async function sessionNotesAdd(sessionId, flags) {
1148
+ if (!sessionId)
1149
+ die('Usage: btx sessions notes add <session-id> --content "..."');
1150
+ if (!flags.content)
1151
+ die('--content is required');
1152
+ // Dedup check. Fetch existing notes and warn if very similar content already exists.
1153
+ try {
1154
+ const existing = await api('GET', `/session-activities?sessionId=${sessionId}&limit=50`);
1155
+ const notes = (Array.isArray(existing) ? existing : []).filter(isNoteActivity);
1156
+ for (const n of notes) {
1157
+ if (noteSimilarity(flags.content, n.content) >= 0.7) {
1158
+ const date = activityDate(n);
1159
+ console.log(`Skipped. Similar note already exists (${date}): ${n.content}`);
1160
+ return;
1161
+ }
1162
+ }
1163
+ }
1164
+ catch {
1165
+ // Non-fatal. Proceed with save.
1166
+ }
1167
+ const metadata = { btx_session_id: SESSION_ID || undefined };
1168
+ if (flags.source)
1169
+ metadata.source_url = flags.source;
1170
+ const data = await api('POST', '/session-activities', {
1171
+ sessionId,
1172
+ type: 'note',
1173
+ content: flags.content,
1174
+ metadata
1175
+ });
1176
+ if (isJson(flags)) {
1177
+ printJson({ id: data.id ?? null, sessionId, content: flags.content, metadata });
1178
+ return;
1179
+ }
1180
+ console.log(`Note saved.`);
1181
+ console.log(`Session: ${sessionId}`);
1182
+ console.log(`BTX_SESSION_NOTE_JSON: ${JSON.stringify({ id: data.id, sessionId, content: flags.content })}`);
1183
+ }
1184
+ async function sessionNotesList(sessionId, flags) {
1185
+ if (!sessionId)
1186
+ die('Usage: btx sessions notes list <session-id>');
1187
+ const result = await api('GET', `/session-activities?sessionId=${sessionId}&limit=50`);
1188
+ const notes = (Array.isArray(result) ? result : []).filter(isNoteActivity);
1189
+ if (isJson(flags)) {
1190
+ printJson(notes);
1191
+ return;
1192
+ }
1193
+ if (notes.length === 0) {
1194
+ console.log('No notes yet for this session.');
1195
+ return;
1196
+ }
1197
+ console.log(`${notes.length} note${notes.length !== 1 ? 's' : ''} for session ${sessionId}:\n`);
1198
+ for (const n of notes) {
1199
+ const date = activityDate(n);
1200
+ console.log(`[${date}] ${n.content}`);
1201
+ console.log();
1202
+ }
1203
+ }
1204
+ // ── Leads commands ────────────────────────────────────────────────────────────
1205
+ async function fetchLeadTypes() {
1206
+ return (await api('GET', '/lead-types')) ?? [];
1207
+ }
1208
+ async function leadsList(flags) {
1209
+ const leads = await fetchLeadTypes();
1210
+ if (isJson(flags)) {
1211
+ printJson(leads);
1212
+ return;
1213
+ }
1214
+ formatLeadTypeTable(leads);
1215
+ }
1216
+ async function leadsGet(id, flags) {
1217
+ if (!id)
1218
+ die('Usage: btx leads get <lead-id>');
1219
+ const leads = await fetchLeadTypes();
1220
+ const lead = leads.find((lt) => lt.id === id);
1221
+ if (!lead)
1222
+ die(`Lead type not found: ${id}`);
1223
+ if (isJson(flags)) {
1224
+ printJson(lead);
1225
+ return;
1226
+ }
1227
+ formatLeadTypeDetail(lead);
1228
+ }
1229
+ // ── Intro paths commands ─────────────────────────────────────────────────────
1230
+ function introPathPayload(path) {
1231
+ return JSON.stringify({
1232
+ connectionId: path.connection.id,
1233
+ connectionName: path.connection.name,
1234
+ connectionCompany: path.connection.company || null,
1235
+ connectionRole: path.connection.role || null,
1236
+ pathType: path.pathType,
1237
+ score: path.score,
1238
+ evidence: path.evidence,
1239
+ suggestedApproach: path.suggestedApproach || null,
1240
+ exaFindings: path.exaFindings || []
1241
+ });
1242
+ }
1243
+ function formatIntroPathResults(result) {
1244
+ const target = result.target;
1245
+ console.log(`\nIntro paths to ${target.name}${target.company ? ` (${target.company})` : ''}${target.role ? ` - ${target.role}` : ''}`);
1246
+ console.log(`Searched ${result.contactsSearched} contacts in your network.\n`);
1247
+ if (result.paths.length === 0) {
1248
+ console.log(result.noPathSuggestion || 'No warm intro paths found.');
1249
+ return;
1250
+ }
1251
+ const scoreW = 6;
1252
+ const typeW = 18;
1253
+ const nameW = 24;
1254
+ const companyW = 20;
1255
+ console.log(`${padRight('Score', scoreW)} ${padRight('Type', typeW)} ${padRight('Connection', nameW)} ${padRight('Company', companyW)} Evidence`);
1256
+ console.log(`${'\u2500'.repeat(scoreW)} ${'\u2500'.repeat(typeW)} ${'\u2500'.repeat(nameW)} ${'\u2500'.repeat(companyW)} ${'\u2500'.repeat(40)}`);
1257
+ for (const p of result.paths) {
1258
+ const score = (p.score * 100).toFixed(0) + '%';
1259
+ const name = truncate(p.connection.name, nameW);
1260
+ const company = truncate(p.connection.company || '', companyW);
1261
+ const typeLabel = p.pathType.replace(/_/g, ' ');
1262
+ console.log(`${padRight(score, scoreW)} ${padRight(typeLabel, typeW)} ${padRight(name, nameW)} ${padRight(company, companyW)} ${truncate(p.evidence, 50)}`);
1263
+ }
1264
+ console.log(`\n${result.paths.length} intro path${result.paths.length !== 1 ? 's' : ''} found.\n`);
1265
+ // Detailed output per path
1266
+ for (let i = 0; i < result.paths.length; i++) {
1267
+ const p = result.paths[i];
1268
+ console.log(`--- Path ${i + 1}: via ${p.connection.name} ---`);
1269
+ console.log(` Company: ${p.connection.company || 'N/A'}`);
1270
+ console.log(` Role: ${p.connection.role || 'N/A'}`);
1271
+ if (p.connection.linkedinUrl)
1272
+ console.log(` LinkedIn: ${p.connection.linkedinUrl}`);
1273
+ console.log(` Type: ${p.pathType.replace(/_/g, ' ')}`);
1274
+ console.log(` Score: ${(p.score * 100).toFixed(0)}%`);
1275
+ console.log(` Evidence: ${p.evidence}`);
1276
+ if (p.suggestedApproach)
1277
+ console.log(` Approach: ${p.suggestedApproach}`);
1278
+ if (p.exaFindings?.length) {
1279
+ console.log(` Web signals:`);
1280
+ for (const f of p.exaFindings)
1281
+ console.log(` - ${f}`);
1282
+ }
1283
+ console.log();
1284
+ console.log(`BTX_INTRO_PATH_JSON: ${introPathPayload(p)}`);
1285
+ }
1286
+ }
1287
+ async function introPathsFind(flags) {
1288
+ const contactId = flags['contact-id'] || flags.contact;
1289
+ const name = flags.name;
1290
+ const company = flags.company;
1291
+ const role = flags.role;
1292
+ if (!contactId && !name) {
1293
+ die('Usage: btx intro-paths find --contact-id <id> OR --name "Person Name" [--company "..."] [--role "..."]');
1294
+ }
1295
+ const body = {};
1296
+ if (contactId)
1297
+ body.contactId = contactId;
1298
+ if (name)
1299
+ body.targetName = name;
1300
+ if (company)
1301
+ body.targetCompany = company;
1302
+ if (role)
1303
+ body.targetRole = role;
1304
+ const result = await api('POST', '/intro-paths', body);
1305
+ if (isJson(flags)) {
1306
+ printJson(result);
1307
+ return;
1308
+ }
1309
+ formatIntroPathResults(result);
1310
+ }
1311
+ // ── Global search ────────────────────────────────────────────────────────────
1312
+ async function globalSearch(flags) {
1313
+ const query = flags.query || flags.q;
1314
+ if (!query)
1315
+ die('Usage: btx search --query "search text" [--status todo|in_progress|done]');
1316
+ const q = query.toLowerCase();
1317
+ const tasks = await fetchTasks();
1318
+ const matches = tasks.filter((t) => {
1319
+ if (flags.status && t.status !== flags.status)
1320
+ return false;
1321
+ return ((t.title || '').toLowerCase().includes(q) ||
1322
+ (t.description || '').toLowerCase().includes(q) ||
1323
+ (t.plan || '').toLowerCase().includes(q));
1324
+ });
1325
+ if (isJson(flags)) {
1326
+ printJson(matches);
1327
+ return;
1328
+ }
1329
+ if (matches.length === 0) {
1330
+ console.log(`No tasks found matching "${query}".`);
1331
+ console.log('\nTo search coding sessions, use Cmd+Shift+F in the BTX app.');
1332
+ return;
1333
+ }
1334
+ formatTaskTable(matches);
1335
+ console.log('\nTo search coding sessions, use Cmd+Shift+F in the BTX app.');
1336
+ }
1337
+ // ── User profile notes commands ───────────────────────────────────────────────
1338
+ async function userNotesAdd(flags) {
1339
+ if (!flags.content)
1340
+ die('--content is required');
1341
+ // Dedup check. Fetch existing notes and warn if very similar content already exists.
1342
+ try {
1343
+ const existing = await api('GET', '/user-activities?limit=200');
1344
+ const notes = (Array.isArray(existing) ? existing : []).filter(isNoteActivity);
1345
+ for (const n of notes) {
1346
+ if (noteSimilarity(flags.content, n.content) >= 0.7) {
1347
+ const date = activityDate(n);
1348
+ console.log(`Skipped. Similar user note already exists (${date}): ${n.content}`);
1349
+ return;
1350
+ }
1351
+ }
1352
+ }
1353
+ catch {
1354
+ // Non-fatal. Proceed with save.
1355
+ }
1356
+ const metadata = { btx_session_id: SESSION_ID || undefined };
1357
+ if (flags.source)
1358
+ metadata.source_url = flags.source;
1359
+ const data = await api('POST', '/user-activities', {
1360
+ type: 'note',
1361
+ content: flags.content,
1362
+ metadata
1363
+ });
1364
+ if (isJson(flags)) {
1365
+ printJson({ id: data.id ?? null, content: flags.content, metadata });
1366
+ return;
1367
+ }
1368
+ console.log(`User profile note saved.`);
1369
+ console.log(`BTX_USER_NOTE_JSON: ${JSON.stringify({ id: data.id, content: flags.content })}`);
1370
+ }
1371
+ async function userNotesList(flags) {
1372
+ const result = await api('GET', '/user-activities?limit=200');
1373
+ const notes = (Array.isArray(result) ? result : []).filter(isNoteActivity);
1374
+ if (isJson(flags)) {
1375
+ printJson(notes);
1376
+ return;
1377
+ }
1378
+ if (notes.length === 0) {
1379
+ console.log('No user profile notes yet.');
1380
+ return;
1381
+ }
1382
+ console.log(`${notes.length} user profile note${notes.length !== 1 ? 's' : ''}:\n`);
1383
+ for (const n of notes) {
1384
+ const date = activityDate(n);
1385
+ console.log(`[${date}] ${n.content}`);
1386
+ console.log();
1387
+ }
1388
+ }
1389
+ // ── Context commands ─────────────────────────────────────────────────────────
1390
+ async function contextGet(flags) {
1391
+ const data = await api('GET', '/context');
1392
+ const name = data.name || 'Project';
1393
+ const ctx = data.context;
1394
+ if (isJson(flags)) {
1395
+ printJson(data);
1396
+ return;
1397
+ }
1398
+ if (!ctx) {
1399
+ console.log('No business context set for this project.');
1400
+ return;
1401
+ }
1402
+ console.log(`# ${name} - Business Context\n`);
1403
+ if (ctx.productSummary)
1404
+ console.log(`## Product\n${ctx.productSummary}\n`);
1405
+ if (ctx.problemStatement)
1406
+ console.log(`## Problem\n${ctx.problemStatement}\n`);
1407
+ if (ctx.targetMarket)
1408
+ console.log(`## Target Market\n${ctx.targetMarket}\n`);
1409
+ if (ctx.differentiator)
1410
+ console.log(`## Differentiator\n${ctx.differentiator}\n`);
1411
+ if (ctx.alternatives)
1412
+ console.log(`## Alternatives\n${ctx.alternatives}\n`);
1413
+ if (ctx.valueProps?.length) {
1414
+ console.log(`## Value Propositions`);
1415
+ for (const vp of ctx.valueProps)
1416
+ console.log(`- ${vp}`);
1417
+ console.log();
1418
+ }
1419
+ if (ctx.tone)
1420
+ console.log(`## Tone\n${ctx.tone}\n`);
1421
+ if (ctx.businessStage)
1422
+ console.log(`## Stage\n${ctx.businessStage}\n`);
1423
+ if (ctx.currentFocus)
1424
+ console.log(`## Current Focus\n${ctx.currentFocus}\n`);
1425
+ }
1426
+ // ── Pages commands ───────────────────────────────────────────────────────────
1427
+ function parseInlineMarks(text) {
1428
+ // Parse **bold** and *italic* into Tiptap text nodes with marks
1429
+ const nodes = [];
1430
+ const re = /(\*\*(.+?)\*\*|\*(.+?)\*)/g;
1431
+ let last = 0;
1432
+ let m;
1433
+ while ((m = re.exec(text)) !== null) {
1434
+ if (m.index > last)
1435
+ nodes.push({ type: 'text', text: text.slice(last, m.index) });
1436
+ if (m[2]) {
1437
+ nodes.push({ type: 'text', text: m[2], marks: [{ type: 'bold' }] });
1438
+ }
1439
+ else if (m[3]) {
1440
+ nodes.push({ type: 'text', text: m[3], marks: [{ type: 'italic' }] });
1441
+ }
1442
+ last = m.index + m[0].length;
1443
+ }
1444
+ if (last < text.length)
1445
+ nodes.push({ type: 'text', text: text.slice(last) });
1446
+ return nodes.length > 0 ? nodes : [{ type: 'text', text }];
1447
+ }
1448
+ function markdownToTiptap(text) {
1449
+ if (!text)
1450
+ return { type: 'doc', content: [{ type: 'paragraph', content: [] }] };
1451
+ const lines = text.split('\n');
1452
+ const nodes = [];
1453
+ let i = 0;
1454
+ while (i < lines.length) {
1455
+ const line = lines[i];
1456
+ // Headings: # ## ###
1457
+ const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
1458
+ if (headingMatch) {
1459
+ nodes.push({
1460
+ type: 'heading',
1461
+ attrs: { level: headingMatch[1].length },
1462
+ content: parseInlineMarks(headingMatch[2])
1463
+ });
1464
+ i++;
1465
+ continue;
1466
+ }
1467
+ // Bullet list: collect consecutive lines starting with - or *
1468
+ if (/^\s*[-*]\s/.test(line)) {
1469
+ const items = [];
1470
+ while (i < lines.length && /^\s*[-*]\s/.test(lines[i])) {
1471
+ const itemText = lines[i].replace(/^\s*[-*]\s+/, '');
1472
+ items.push({
1473
+ type: 'listItem',
1474
+ content: [{ type: 'paragraph', content: parseInlineMarks(itemText) }]
1475
+ });
1476
+ i++;
1477
+ }
1478
+ nodes.push({ type: 'bulletList', content: items });
1479
+ continue;
1480
+ }
1481
+ // Empty line → empty paragraph
1482
+ if (!line.trim()) {
1483
+ nodes.push({ type: 'paragraph', content: [] });
1484
+ i++;
1485
+ continue;
1486
+ }
1487
+ // Regular paragraph
1488
+ nodes.push({ type: 'paragraph', content: parseInlineMarks(line) });
1489
+ i++;
1490
+ }
1491
+ return { type: 'doc', content: nodes.length > 0 ? nodes : [{ type: 'paragraph', content: [] }] };
1492
+ }
1493
+ async function pagesCreate(flags) {
1494
+ const title = flags.title;
1495
+ if (!title)
1496
+ die('--title is required');
1497
+ const id = randomUUID();
1498
+ const now = Date.now();
1499
+ const content = markdownToTiptap(flags.body || '');
1500
+ const sessionRefs = SESSION_ID ? [{ id: SESSION_ID, label: title }] : [];
1501
+ await api('POST', '/notes', { id, title, content, sessionRefs, createdAt: now });
1502
+ if (isJson(flags)) {
1503
+ printJson({ page: { id, title, content, updatedAt: now }, deepLink: noteDeepLink(id) });
1504
+ return;
1505
+ }
1506
+ console.log(`Page created: ${title}`);
1507
+ console.log(`ID: ${id}`);
1508
+ console.log(`Deep link: ${noteDeepLink(id)}`);
1509
+ }
1510
+ async function pagesUpdate(noteId, flags) {
1511
+ if (!noteId)
1512
+ die('Usage: btx pages update <note-id> --title "..." --body "..."');
1513
+ const result = await api('GET', '/notes');
1514
+ const notes = result?.notes ?? [];
1515
+ const existing = notes.find((n) => n.id === noteId);
1516
+ if (!existing)
1517
+ die(`Page ${noteId} not found`);
1518
+ const patch = { id: noteId };
1519
+ const changed = [];
1520
+ if (flags.title !== undefined) {
1521
+ patch.title = flags.title;
1522
+ changed.push('title');
1523
+ }
1524
+ if (flags.body !== undefined) {
1525
+ patch.content = markdownToTiptap(flags.body);
1526
+ changed.push('body');
1527
+ }
1528
+ if (changed.length === 0)
1529
+ die('No fields to update. Use: --title "..." --body "..."');
1530
+ await api('POST', '/notes', patch);
1531
+ if (isJson(flags)) {
1532
+ printJson({ pageId: noteId, updatedFields: changed, deepLink: noteDeepLink(noteId) });
1533
+ return;
1534
+ }
1535
+ console.log(`Page updated: ${existing.title || '(untitled)'}`);
1536
+ console.log(`Updated fields: ${changed.join(', ')}`);
1537
+ console.log(`Deep link: ${noteDeepLink(noteId)}`);
1538
+ }
1539
+ async function pagesList(flags) {
1540
+ const result = await api('GET', '/notes');
1541
+ const notes = result?.notes ?? [];
1542
+ if (isJson(flags)) {
1543
+ printJson(notes);
1544
+ return;
1545
+ }
1546
+ if (notes.length === 0) {
1547
+ console.log('No note pages found.');
1548
+ return;
1549
+ }
1550
+ console.log(`\n${notes.length} page${notes.length !== 1 ? 's' : ''}:\n`);
1551
+ for (const n of notes) {
1552
+ const date = new Date(n.updatedAt).toISOString().slice(0, 10);
1553
+ console.log(` [${date}] ${n.title || '(untitled)'}`);
1554
+ console.log(` ID: ${n.id}`);
1555
+ console.log(` ${noteDeepLink(n.id)}`);
1556
+ console.log();
1557
+ }
1558
+ }
1559
+ function tiptapToMarkdown(doc) {
1560
+ if (!doc || !doc.content)
1561
+ return '';
1562
+ const lines = [];
1563
+ for (const node of doc.content) {
1564
+ if (node.type === 'heading') {
1565
+ const prefix = '#'.repeat(node.attrs?.level || 1);
1566
+ lines.push(`${prefix} ${inlineToText(node.content)}`);
1567
+ }
1568
+ else if (node.type === 'bulletList') {
1569
+ for (const item of node.content || []) {
1570
+ const text = item.content?.map((p) => inlineToText(p.content)).join(' ') || '';
1571
+ lines.push(`- ${text}`);
1572
+ }
1573
+ }
1574
+ else if (node.type === 'paragraph') {
1575
+ lines.push(inlineToText(node.content));
1576
+ }
1577
+ else {
1578
+ lines.push(inlineToText(node.content));
1579
+ }
1580
+ }
1581
+ return lines.join('\n');
1582
+ }
1583
+ function inlineToText(nodes) {
1584
+ if (!nodes)
1585
+ return '';
1586
+ return nodes
1587
+ .map((n) => {
1588
+ const text = n.text || '';
1589
+ if (!n.marks)
1590
+ return text;
1591
+ let wrapped = text;
1592
+ for (const mark of n.marks) {
1593
+ if (mark.type === 'bold')
1594
+ wrapped = `**${wrapped}**`;
1595
+ else if (mark.type === 'italic')
1596
+ wrapped = `*${wrapped}*`;
1597
+ }
1598
+ return wrapped;
1599
+ })
1600
+ .join('');
1601
+ }
1602
+ async function pagesGet(noteId, flags) {
1603
+ if (!noteId)
1604
+ die('Usage: btx pages get <note-id>');
1605
+ const result = await api('GET', '/notes');
1606
+ const notes = result?.notes ?? [];
1607
+ const note = notes.find((n) => n.id === noteId);
1608
+ if (!note)
1609
+ die(`Page ${noteId} not found`);
1610
+ if (isJson(flags)) {
1611
+ printJson(note);
1612
+ return;
1613
+ }
1614
+ console.log(`Title: ${note.title || '(untitled)'}`);
1615
+ console.log(`ID: ${note.id}`);
1616
+ console.log(`Updated: ${new Date(note.updatedAt).toISOString().slice(0, 10)}`);
1617
+ console.log(`Deep link: ${noteDeepLink(note.id)}`);
1618
+ const body = tiptapToMarkdown(note.content);
1619
+ if (body)
1620
+ console.log(`\nBody:\n${body}`);
1621
+ }
1622
+ // ── Hiring Types ───────────────────────────────────────────────────────────
1623
+ async function hiringTypesList(flags) {
1624
+ const result = await api('GET', '/hiring-types');
1625
+ const types = result?.hiringTypes ?? [];
1626
+ if (isJson(flags)) {
1627
+ printJson(types);
1628
+ return;
1629
+ }
1630
+ if (!types.length) {
1631
+ console.log('No hiring types found.');
1632
+ return;
1633
+ }
1634
+ const maxTitle = Math.max(5, ...types.map((t) => (t.title || '').length));
1635
+ const maxCat = Math.max(8, ...types.map((t) => (t.category || '').length));
1636
+ const header = `${'TITLE'.padEnd(maxTitle)} ${'CATEGORY'.padEnd(maxCat)} ${'STATUS'.padEnd(10)} ID`;
1637
+ console.log(header);
1638
+ console.log('-'.repeat(header.length + 20));
1639
+ for (const t of types) {
1640
+ console.log(`${(t.title || '').padEnd(maxTitle)} ${(t.category || '').padEnd(maxCat)} ${(t.status || '').padEnd(10)} ${t.id}`);
1641
+ }
1642
+ console.log(`\n${types.length} hiring type(s)`);
1643
+ }
1644
+ async function hiringTypesGet(id, flags) {
1645
+ if (!id)
1646
+ die('Usage: btx hiring-types get <hiring-type-id>');
1647
+ const result = await api('GET', '/hiring-types');
1648
+ const types = result?.hiringTypes ?? [];
1649
+ const ht = types.find((t) => t.id === id);
1650
+ if (!ht)
1651
+ die(`Hiring type not found: ${id}`);
1652
+ if (isJson(flags)) {
1653
+ printJson(ht);
1654
+ return;
1655
+ }
1656
+ console.log(`Title: ${ht.title || '(untitled)'}`);
1657
+ console.log(`ID: ${ht.id}`);
1658
+ console.log(`Category: ${ht.category || '-'}`);
1659
+ console.log(`Status: ${ht.status || '-'}`);
1660
+ console.log(`Location: ${ht.location || '-'}`);
1661
+ console.log(`Source: ${ht.source || '-'}`);
1662
+ if (ht.description)
1663
+ console.log(`\nDescription:\n${ht.description}`);
1664
+ if (ht.searchDescription)
1665
+ console.log(`\nSearch Description:\n${ht.searchDescription}`);
1666
+ }
1667
+ async function hiringTypesCreate(flags) {
1668
+ if (!flags.title)
1669
+ die('Usage: btx hiring-types create --title "..." [--description "..."] [--category Engineer|Designer|...] [--location "..."]');
1670
+ const body = {
1671
+ id: randomUUID(),
1672
+ title: flags.title,
1673
+ description: flags.description || null,
1674
+ searchDescription: flags['search-description'] || null,
1675
+ category: flags.category || 'Engineer',
1676
+ location: flags.location || null,
1677
+ source: 'manual',
1678
+ status: flags.status || 'confirmed'
1679
+ };
1680
+ await api('POST', '/hiring-types', body);
1681
+ if (isJson(flags)) {
1682
+ printJson(body);
1683
+ return;
1684
+ }
1685
+ console.log(`Created hiring type: ${body.title} (${body.id})`);
1686
+ }
1687
+ async function hiringTypesUpdate(id, flags) {
1688
+ if (!id)
1689
+ die('Usage: btx hiring-types update <hiring-type-id> [--title "..."] [--description "..."] ...');
1690
+ const result = await api('GET', '/hiring-types');
1691
+ const types = result?.hiringTypes ?? [];
1692
+ const existing = types.find((t) => t.id === id);
1693
+ if (!existing)
1694
+ die(`Hiring type not found: ${id}`);
1695
+ const body = {
1696
+ id,
1697
+ title: flags.title || existing.title,
1698
+ description: flags.description !== undefined ? flags.description : existing.description,
1699
+ searchDescription: flags['search-description'] !== undefined
1700
+ ? flags['search-description']
1701
+ : existing.searchDescription,
1702
+ category: flags.category || existing.category,
1703
+ location: flags.location !== undefined ? flags.location : existing.location,
1704
+ source: existing.source,
1705
+ status: flags.status || existing.status
1706
+ };
1707
+ await api('POST', '/hiring-types', body);
1708
+ if (isJson(flags)) {
1709
+ printJson(body);
1710
+ return;
1711
+ }
1712
+ console.log(`Updated hiring type: ${body.title} (${id})`);
1713
+ }
1714
+ async function hiringTypesDelete(id, flags) {
1715
+ if (!id)
1716
+ die('Usage: btx hiring-types delete <hiring-type-id>');
1717
+ await api('DELETE', `/hiring-types/${id}`);
1718
+ if (isJson(flags)) {
1719
+ printJson({ id, deleted: true });
1720
+ return;
1721
+ }
1722
+ console.log(`Deleted hiring type: ${id}`);
1723
+ }
1724
+ // ── Hiring Candidates ──────────────────────────────────────────────────────
1725
+ async function hiringCandidatesList(flags) {
1726
+ const result = await api('GET', '/hiring-candidates');
1727
+ let candidates = result?.candidates ?? [];
1728
+ if (flags['hiring-type']) {
1729
+ candidates = candidates.filter((c) => c.hiringTypeId === flags['hiring-type']);
1730
+ }
1731
+ if (flags.stage) {
1732
+ candidates = candidates.filter((c) => c.stage === flags.stage);
1733
+ }
1734
+ if (flags.query) {
1735
+ const q = flags.query.toLowerCase();
1736
+ candidates = candidates.filter((c) => (c.name || '').toLowerCase().includes(q) ||
1737
+ (c.company || '').toLowerCase().includes(q) ||
1738
+ (c.role || '').toLowerCase().includes(q));
1739
+ }
1740
+ if (isJson(flags)) {
1741
+ printJson(candidates);
1742
+ return;
1743
+ }
1744
+ if (!candidates.length) {
1745
+ console.log('No hiring candidates found.');
1746
+ return;
1747
+ }
1748
+ const total = candidates.length;
1749
+ const limit = flags.limit ? parseInt(flags.limit, 10) : null;
1750
+ if (limit && limit > 0)
1751
+ candidates = candidates.slice(0, limit);
1752
+ const maxName = Math.max(4, ...candidates.map((c) => (c.name || '').length));
1753
+ const maxCompany = Math.max(7, ...candidates.map((c) => (c.company || '').length));
1754
+ const maxRole = Math.max(4, ...candidates.map((c) => (c.role || '').length));
1755
+ const header = `${'NAME'.padEnd(maxName)} ${'COMPANY'.padEnd(maxCompany)} ${'ROLE'.padEnd(maxRole)} ${'STAGE'.padEnd(14)} ID`;
1756
+ console.log(header);
1757
+ console.log('-'.repeat(header.length + 20));
1758
+ for (const c of candidates) {
1759
+ console.log(`${(c.name || '').padEnd(maxName)} ${(c.company || '').padEnd(maxCompany)} ${(c.role || '').padEnd(maxRole)} ${(c.stage || '').padEnd(14)} ${c.id}`);
1760
+ }
1761
+ const note = limit && total > limit ? ` (showing ${limit} of ${total})` : '';
1762
+ console.log(`\n${candidates.length} candidate(s)${note}`);
1763
+ }
1764
+ async function hiringCandidatesGet(id, flags) {
1765
+ if (!id)
1766
+ die('Usage: btx hiring-candidates get <candidate-id>');
1767
+ const result = await api('GET', '/hiring-candidates');
1768
+ const candidates = result?.candidates ?? [];
1769
+ const c = candidates.find((c) => c.id === id);
1770
+ if (!c)
1771
+ die(`Candidate not found: ${id}`);
1772
+ if (isJson(flags)) {
1773
+ printJson(c);
1774
+ return;
1775
+ }
1776
+ console.log(`Name: ${c.name || '-'}`);
1777
+ console.log(`ID: ${c.id}`);
1778
+ console.log(`Company: ${c.company || '-'}`);
1779
+ console.log(`Role: ${c.role || '-'}`);
1780
+ console.log(`Email: ${c.email || '-'}`);
1781
+ console.log(`Location: ${c.location || '-'}`);
1782
+ console.log(`Stage: ${c.stage || '-'}`);
1783
+ console.log(`Source: ${c.source || '-'}`);
1784
+ console.log(`LinkedIn: ${c.linkedinUrl || '-'}`);
1785
+ console.log(`Favorite: ${c.isFavorite ? 'Yes' : 'No'}`);
1786
+ console.log(`Hiring Type: ${c.hiringTypeId || '-'}`);
1787
+ if (c.whyMatch)
1788
+ console.log(`\nWhy Match:\n${c.whyMatch}`);
1789
+ if (c.outreachAngle)
1790
+ console.log(`\nOutreach Angle:\n${c.outreachAngle}`);
1791
+ if (c.feedback)
1792
+ console.log(`\nFeedback: ${c.feedback}${c.feedbackReason ? ' - ' + c.feedbackReason : ''}`);
1793
+ }
1794
+ // ── Meeting commands ────────────────────────────────────────────────────────
1795
+ async function fetchMeetings() {
1796
+ const result = await api('GET', '/meetings');
1797
+ return result?.meetings ?? [];
1798
+ }
1799
+ async function meetingsList(flags) {
1800
+ let meetings = await fetchMeetings();
1801
+ if (flags.days) {
1802
+ const cutoff = new Date();
1803
+ cutoff.setDate(cutoff.getDate() - Number(flags.days));
1804
+ const cutoffStr = cutoff.toISOString();
1805
+ meetings = meetings.filter((m) => m.meetingDate && m.meetingDate >= cutoffStr);
1806
+ }
1807
+ if (flags.query) {
1808
+ const q = flags.query.toLowerCase();
1809
+ meetings = meetings.filter((m) => m.title.toLowerCase().includes(q) ||
1810
+ (m.summary && m.summary.toLowerCase().includes(q)) ||
1811
+ (m.aiSummary && m.aiSummary.toLowerCase().includes(q)));
1812
+ }
1813
+ if (isJson(flags)) {
1814
+ printJson(meetings);
1815
+ return;
1816
+ }
1817
+ formatMeetingTable(meetings);
1818
+ }
1819
+ async function meetingsGet(id, flags) {
1820
+ if (!id)
1821
+ die('Usage: btx meetings get <meeting-id>');
1822
+ const meetings = await fetchMeetings();
1823
+ const meeting = meetings.find((m) => m.id === id);
1824
+ if (!meeting)
1825
+ die(`Meeting not found: ${id}`);
1826
+ if (isJson(flags)) {
1827
+ printJson(meeting);
1828
+ return;
1829
+ }
1830
+ formatMeetingDetail(meeting);
1831
+ }
1832
+ async function meetingsTranscript(id, flags) {
1833
+ if (!id)
1834
+ die('Usage: btx meetings transcript <meeting-id>');
1835
+ const meetings = await fetchMeetings();
1836
+ const meeting = meetings.find((m) => m.id === id);
1837
+ if (!meeting)
1838
+ die(`Meeting not found: ${id}`);
1839
+ if (isJson(flags)) {
1840
+ printJson(meeting.transcript ?? []);
1841
+ return;
1842
+ }
1843
+ formatMeetingTranscript(meeting);
1844
+ }
1845
+ // ── Main ────────────────────────────────────────────────────────────────────
1846
+ export async function runRuntimeCli(args, env = process.env) {
1847
+ configureRuntime(env);
1848
+ const { positional, flags } = parseArgs(args);
1849
+ const resource = positional[0];
1850
+ const command = positional[1];
1851
+ // ── Help (no auth required) ──────────────────────────────────────────────
1852
+ if (!resource) {
1853
+ console.log(renderCliText(HELP.top));
1854
+ return;
1855
+ }
1856
+ if (HELP[resource] && (flags.help || !command || command === '--help')) {
1857
+ // Show resource-level help if: --help flag, or no command given, or command is '--help'
1858
+ // Exception: 'search' and 'intro-paths find' don't have sub-commands
1859
+ const resourcesWithSubCommands = [
1860
+ 'tasks',
1861
+ 'sessions',
1862
+ 'contacts',
1863
+ 'orgs',
1864
+ 'meetings',
1865
+ 'leads',
1866
+ 'user',
1867
+ 'intro-paths',
1868
+ 'context',
1869
+ 'pages'
1870
+ ];
1871
+ const noSubCommandResources = ['search'];
1872
+ if (noSubCommandResources.includes(resource) && !flags.help) {
1873
+ // Fall through to normal routing
1874
+ }
1875
+ else if (resourcesWithSubCommands.includes(resource) && (flags.help || !command)) {
1876
+ console.log(renderCliText(HELP[resource]));
1877
+ return;
1878
+ }
1879
+ }
1880
+ // ── Auth (required for all data commands) ───────────────────────────────
1881
+ if (!TOKEN)
1882
+ die('BTX_ACCESS_TOKEN is not set. Are you running inside a BTX session?');
1883
+ if (!PROJECT_ID)
1884
+ die('BTX_PROJECT_ID is not set. Are you running inside a BTX session?');
1885
+ if (resource === 'tasks') {
1886
+ if (command === 'notes') {
1887
+ const subCommand = positional[2];
1888
+ const taskId = positional[3];
1889
+ switch (subCommand) {
1890
+ case 'add':
1891
+ return taskNotesAdd(taskId, flags);
1892
+ case 'list':
1893
+ return taskNotesList(taskId, flags);
1894
+ default:
1895
+ die(`Unknown tasks notes command: "${subCommand || '(none)'}". Run: node "$BTX_CLI_PATH" tasks --help`);
1896
+ }
1897
+ }
1898
+ switch (command) {
1899
+ case 'list':
1900
+ return tasksList(flags);
1901
+ case 'get':
1902
+ return tasksGet(positional[2], flags);
1903
+ case 'create':
1904
+ return tasksCreate(flags);
1905
+ case 'update':
1906
+ return tasksUpdate(positional[2], flags);
1907
+ case 'complete':
1908
+ return tasksComplete(positional[2], flags);
1909
+ default:
1910
+ die(`Unknown tasks command: "${command || '(none)'}". Run: node "$BTX_CLI_PATH" tasks --help`);
1911
+ }
1912
+ }
1913
+ else if (resource === 'sessions') {
1914
+ if (command === 'notes') {
1915
+ const subCommand = positional[2];
1916
+ const sessionId = positional[3];
1917
+ switch (subCommand) {
1918
+ case 'add':
1919
+ return sessionNotesAdd(sessionId, flags);
1920
+ case 'list':
1921
+ return sessionNotesList(sessionId, flags);
1922
+ default:
1923
+ die(`Unknown sessions notes command: "${subCommand || '(none)'}". Run: node "$BTX_CLI_PATH" sessions --help`);
1924
+ }
1925
+ }
1926
+ die(`Unknown sessions command: "${command || '(none)'}". Run: node "$BTX_CLI_PATH" sessions --help`);
1927
+ }
1928
+ else if (resource === 'contacts') {
1929
+ if (command === 'notes') {
1930
+ const subCommand = positional[2];
1931
+ const contactId = positional[3];
1932
+ switch (subCommand) {
1933
+ case 'add':
1934
+ return contactNotesAdd(contactId, flags);
1935
+ case 'list':
1936
+ return contactNotesList(contactId, flags);
1937
+ default:
1938
+ die(`Unknown contacts notes command: "${subCommand || '(none)'}". Run: node "$BTX_CLI_PATH" contacts --help`);
1939
+ }
1940
+ }
1941
+ switch (command) {
1942
+ case 'list':
1943
+ return contactsList(flags);
1944
+ case 'get':
1945
+ return contactsGet(positional[2], flags);
1946
+ case 'create':
1947
+ return contactsCreate(flags);
1948
+ case 'update':
1949
+ return contactsUpdate(positional[2], flags);
1950
+ default:
1951
+ die(`Unknown contacts command: "${command || '(none)'}". Run: node "$BTX_CLI_PATH" contacts --help`);
1952
+ }
1953
+ }
1954
+ else if (resource === 'orgs') {
1955
+ if (command === 'notes') {
1956
+ const subCommand = positional[2];
1957
+ const orgId = positional[3];
1958
+ switch (subCommand) {
1959
+ case 'add':
1960
+ return orgNotesAdd(orgId, flags);
1961
+ case 'list':
1962
+ return orgNotesList(orgId, flags);
1963
+ default:
1964
+ die(`Unknown orgs notes command: "${subCommand || '(none)'}". Run: node "$BTX_CLI_PATH" orgs --help`);
1965
+ }
1966
+ }
1967
+ switch (command) {
1968
+ case 'list':
1969
+ return orgsList(flags);
1970
+ case 'get':
1971
+ return orgsGet(positional[2]);
1972
+ case 'create':
1973
+ return orgsCreate(flags);
1974
+ default:
1975
+ die(`Unknown orgs command: "${command || '(none)'}". Run: node "$BTX_CLI_PATH" orgs --help`);
1976
+ }
1977
+ }
1978
+ else if (resource === 'leads') {
1979
+ switch (command) {
1980
+ case 'list':
1981
+ return leadsList(flags);
1982
+ case 'get':
1983
+ return leadsGet(positional[2], flags);
1984
+ default:
1985
+ die(`Unknown leads command: "${command || '(none)'}". Run: node "$BTX_CLI_PATH" leads --help`);
1986
+ }
1987
+ }
1988
+ else if (resource === 'meetings') {
1989
+ switch (command) {
1990
+ case 'list':
1991
+ return meetingsList(flags);
1992
+ case 'get':
1993
+ return meetingsGet(positional[2], flags);
1994
+ case 'transcript':
1995
+ return meetingsTranscript(positional[2], flags);
1996
+ default:
1997
+ die(`Unknown meetings command: "${command || '(none)'}". Run: node "$BTX_CLI_PATH" meetings --help`);
1998
+ }
1999
+ }
2000
+ else if (resource === 'intro-paths') {
2001
+ switch (command) {
2002
+ case 'find':
2003
+ return introPathsFind(flags);
2004
+ default:
2005
+ die(`Unknown intro-paths command: "${command || '(none)'}". Run: node "$BTX_CLI_PATH" intro-paths --help`);
2006
+ }
2007
+ }
2008
+ else if (resource === 'user') {
2009
+ if (command === 'notes') {
2010
+ const subCommand = positional[2];
2011
+ switch (subCommand) {
2012
+ case 'add':
2013
+ return userNotesAdd(flags);
2014
+ case 'list':
2015
+ return userNotesList(flags);
2016
+ default:
2017
+ die(`Unknown user notes command: "${subCommand || '(none)'}". Run: node "$BTX_CLI_PATH" user --help`);
2018
+ }
2019
+ }
2020
+ die(`Unknown user command: "${command || '(none)'}". Run: node "$BTX_CLI_PATH" user --help`);
2021
+ }
2022
+ else if (resource === 'pages') {
2023
+ switch (command) {
2024
+ case 'create':
2025
+ return pagesCreate(flags);
2026
+ case 'update':
2027
+ return pagesUpdate(positional[2], flags);
2028
+ case 'list':
2029
+ return pagesList(flags);
2030
+ case 'get':
2031
+ return pagesGet(positional[2], flags);
2032
+ default:
2033
+ die(`Unknown pages command: "${command || '(none)'}". Run: node "$BTX_CLI_PATH" pages --help`);
2034
+ }
2035
+ }
2036
+ else if (resource === 'search') {
2037
+ return globalSearch(flags);
2038
+ }
2039
+ else if (resource === 'context') {
2040
+ switch (command) {
2041
+ case 'get':
2042
+ return contextGet(flags);
2043
+ default:
2044
+ die(`Unknown context command: "${command || '(none)'}". Run: node "$BTX_CLI_PATH" context --help`);
2045
+ }
2046
+ }
2047
+ else if (resource === 'hiring-types') {
2048
+ switch (command) {
2049
+ case 'list':
2050
+ return hiringTypesList(flags);
2051
+ case 'get':
2052
+ return hiringTypesGet(positional[2], flags);
2053
+ case 'create':
2054
+ return hiringTypesCreate(flags);
2055
+ case 'update':
2056
+ return hiringTypesUpdate(positional[2], flags);
2057
+ case 'delete':
2058
+ return hiringTypesDelete(positional[2], flags);
2059
+ case '--help':
2060
+ console.log('hiring-types list List all hiring types');
2061
+ console.log('hiring-types get <id> Get a specific hiring type');
2062
+ console.log('hiring-types create --title "..." --description "..." --search-description "..." [--category Engineer] [--location "..."]');
2063
+ console.log('hiring-types update <id> [--title "..."] [--status draft|confirmed] [--description "..."]');
2064
+ console.log('hiring-types delete <id> Delete a hiring type');
2065
+ return;
2066
+ default:
2067
+ die(`Unknown hiring-types command: "${command || '(none)'}". Run: node "$BTX_CLI_PATH" hiring-types --help`);
2068
+ }
2069
+ }
2070
+ else if (resource === 'hiring-candidates') {
2071
+ switch (command) {
2072
+ case 'list':
2073
+ return hiringCandidatesList(flags);
2074
+ case 'get':
2075
+ return hiringCandidatesGet(positional[2], flags);
2076
+ case '--help':
2077
+ console.log('hiring-candidates list [--hiring-type <id>] [--stage sourced|screened|interviewing|offer|hired|passed]');
2078
+ console.log('hiring-candidates get <id> Get a specific candidate');
2079
+ return;
2080
+ default:
2081
+ die(`Unknown hiring-candidates command: "${command || '(none)'}". Run: node "$BTX_CLI_PATH" hiring-candidates --help`);
2082
+ }
2083
+ }
2084
+ else {
2085
+ die(`Unknown resource: "${resource}". Run: node "$BTX_CLI_PATH" --help`);
2086
+ }
2087
+ }
2088
+ //# sourceMappingURL=runtime-cli.js.map