@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.
- package/README.md +83 -0
- package/btx.js +3 -0
- package/dist/api.d.ts +6 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +72 -0
- package/dist/api.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +394 -0
- package/dist/bin.js.map +1 -0
- package/dist/bootstrap.d.ts +7 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +91 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +158 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime-cli.d.ts +13 -0
- package/dist/runtime-cli.d.ts.map +1 -0
- package/dist/runtime-cli.js +2088 -0
- package/dist/runtime-cli.js.map +1 -0
- package/package.json +56 -0
|
@@ -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¬eId=${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
|