@basestream/cli 0.1.0 → 0.1.3

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.

Potentially problematic release.


This version of @basestream/cli might be problematic. Click here for more details.

package/dist/cli.mjs ADDED
@@ -0,0 +1,730 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/cli/config.ts
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import os from "node:os";
16
+ function ensureDirs() {
17
+ for (const dir of [BASESTREAM_DIR, BUFFER_DIR, SESSION_DIR]) {
18
+ fs.mkdirSync(dir, { recursive: true });
19
+ }
20
+ }
21
+ function readConfig() {
22
+ if (!fs.existsSync(CONFIG_FILE)) return null;
23
+ try {
24
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
25
+ if (process.env.BASESTREAM_URL) {
26
+ config.baseUrl = process.env.BASESTREAM_URL;
27
+ }
28
+ return config;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ function writeConfig(config) {
34
+ ensureDirs();
35
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
36
+ }
37
+ var BASESTREAM_DIR, BUFFER_DIR, CONFIG_FILE, SESSION_DIR;
38
+ var init_config = __esm({
39
+ "src/cli/config.ts"() {
40
+ "use strict";
41
+ BASESTREAM_DIR = path.join(os.homedir(), ".basestream");
42
+ BUFFER_DIR = path.join(BASESTREAM_DIR, "buffer");
43
+ CONFIG_FILE = path.join(BASESTREAM_DIR, "config.json");
44
+ SESSION_DIR = path.join(BASESTREAM_DIR, "sessions");
45
+ }
46
+ });
47
+
48
+ // src/cli/util.ts
49
+ function check(msg) {
50
+ console.log(` ${c.green("\u2713")} ${c.green(msg)}`);
51
+ }
52
+ function warn(msg) {
53
+ console.log(` ${c.yellow("!")} ${c.yellow(msg)}`);
54
+ }
55
+ function fail(msg) {
56
+ console.log(` ${c.red("\u2717")} ${c.red(msg)}`);
57
+ }
58
+ var c;
59
+ var init_util = __esm({
60
+ "src/cli/util.ts"() {
61
+ "use strict";
62
+ c = {
63
+ green: (s) => `\x1B[32m${s}\x1B[0m`,
64
+ yellow: (s) => `\x1B[33m${s}\x1B[0m`,
65
+ red: (s) => `\x1B[31m${s}\x1B[0m`,
66
+ cyan: (s) => `\x1B[36m${s}\x1B[0m`,
67
+ dim: (s) => `\x1B[2m${s}\x1B[0m`,
68
+ bold: (s) => `\x1B[1m${s}\x1B[0m`
69
+ };
70
+ }
71
+ });
72
+
73
+ // src/cli/commands/sync.ts
74
+ var sync_exports = {};
75
+ __export(sync_exports, {
76
+ sync: () => sync,
77
+ syncEntries: () => syncEntries
78
+ });
79
+ import fs4 from "node:fs";
80
+ import path4 from "node:path";
81
+ async function syncEntries(config) {
82
+ const cfg = config || readConfig();
83
+ if (!cfg?.apiKey) {
84
+ fail("Not authenticated. Run: basestream login");
85
+ process.exit(1);
86
+ }
87
+ ensureDirs();
88
+ const files = fs4.readdirSync(BUFFER_DIR).filter((f) => f.endsWith(".json")).sort();
89
+ if (files.length === 0) return;
90
+ const entries = [];
91
+ const successFiles = [];
92
+ for (const file of files) {
93
+ try {
94
+ const raw = fs4.readFileSync(path4.join(BUFFER_DIR, file), "utf-8");
95
+ const entry = JSON.parse(raw);
96
+ entries.push(entry);
97
+ successFiles.push(file);
98
+ } catch {
99
+ }
100
+ }
101
+ if (entries.length === 0) return;
102
+ const res = await fetch(`${cfg.baseUrl}/api/sync`, {
103
+ method: "POST",
104
+ headers: {
105
+ "Content-Type": "application/json",
106
+ Authorization: `Bearer ${cfg.apiKey}`
107
+ },
108
+ body: JSON.stringify({ entries })
109
+ });
110
+ if (!res.ok) {
111
+ const body = await res.text().catch(() => "");
112
+ throw new Error(`Sync failed (${res.status}): ${body}`);
113
+ }
114
+ const result = await res.json();
115
+ for (const file of successFiles) {
116
+ fs4.unlinkSync(path4.join(BUFFER_DIR, file));
117
+ }
118
+ return result;
119
+ }
120
+ async function sync() {
121
+ const cfg = readConfig();
122
+ if (!cfg?.apiKey) {
123
+ fail("Not authenticated. Run: basestream login");
124
+ process.exit(1);
125
+ }
126
+ ensureDirs();
127
+ const files = fs4.readdirSync(BUFFER_DIR).filter((f) => f.endsWith(".json"));
128
+ if (files.length === 0) {
129
+ console.log(` ${c.dim("No entries to sync.")}`);
130
+ return;
131
+ }
132
+ console.log(` ${c.dim(`Syncing ${files.length} entries...`)}`);
133
+ try {
134
+ const result = await syncEntries(cfg);
135
+ if (result) {
136
+ check(
137
+ `Synced ${result.synced} entries (${result.created} new, ${result.updated} updated)`
138
+ );
139
+ }
140
+ } catch (err) {
141
+ fail(
142
+ `Sync failed: ${err instanceof Error ? err.message : String(err)}`
143
+ );
144
+ process.exit(1);
145
+ }
146
+ }
147
+ var init_sync = __esm({
148
+ "src/cli/commands/sync.ts"() {
149
+ "use strict";
150
+ init_config();
151
+ init_util();
152
+ }
153
+ });
154
+
155
+ // src/cli/commands/init.ts
156
+ init_config();
157
+ init_util();
158
+ import fs2 from "node:fs";
159
+ import path2 from "node:path";
160
+ import os2 from "node:os";
161
+ import { execSync as execSync2 } from "node:child_process";
162
+
163
+ // src/cli/commands/login.ts
164
+ init_config();
165
+ init_util();
166
+ import http from "node:http";
167
+ import { execSync } from "node:child_process";
168
+ var DEFAULT_BASE_URL = "https://www.basestream.ai";
169
+ function openBrowser(url) {
170
+ try {
171
+ const platform = process.platform;
172
+ if (platform === "darwin") execSync(`open "${url}"`);
173
+ else if (platform === "win32") execSync(`start "${url}"`);
174
+ else execSync(`xdg-open "${url}"`);
175
+ } catch {
176
+ console.log(` ${c.dim(`Open this URL in your browser: ${url}`)}`);
177
+ }
178
+ }
179
+ async function login() {
180
+ const existing = readConfig();
181
+ const baseUrl = existing?.baseUrl || process.env.BASESTREAM_URL || DEFAULT_BASE_URL;
182
+ console.log(
183
+ ` ${c.dim("Opening browser for authentication...")}`
184
+ );
185
+ const apiKey = await new Promise((resolve, reject) => {
186
+ const server = http.createServer((req, res) => {
187
+ const url = new URL(req.url || "/", `http://localhost`);
188
+ const key = url.searchParams.get("key");
189
+ const orgId = url.searchParams.get("orgId");
190
+ if (key) {
191
+ res.writeHead(200, { "Content-Type": "text/html" });
192
+ res.end(`<!DOCTYPE html>
193
+ <html lang="en">
194
+ <head>
195
+ <meta charset="utf-8">
196
+ <meta name="viewport" content="width=device-width, initial-scale=1">
197
+ <title>Basestream CLI \u2014 Authenticated</title>
198
+ <link rel="preconnect" href="https://fonts.googleapis.com">
199
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
200
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600&family=Newsreader:wght@500;600&display=swap" rel="stylesheet">
201
+ </head>
202
+ <body style="font-family: 'DM Sans', -apple-system, system-ui, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #faf8f4; color: #1a1714;">
203
+ <div style="text-align: center;">
204
+ <div style="display: flex; align-items: center; justify-content: center; gap: 0.625rem; margin-bottom: 2rem;">
205
+ <div style="display: flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; background: #1a1714; border-radius: 8px;">
206
+ <svg width="16" height="16" viewBox="0 0 22 22" fill="none"><path d="M7.39855 11.2475C10.1937 11.2476 12.4598 13.5124 12.4598 16.3052C12.4596 19.0979 10.1936 21.3616 7.39855 21.3618H0V16.3052C0 15.7395 0.094025 15.1955 0.265523 14.6876C0.304389 14.5726 0.345701 14.4588 0.392462 14.3476C0.602402 13.8482 0.890874 13.39 1.24144 12.9873C1.25986 12.9662 1.27857 12.9453 1.29734 12.9245C1.39051 12.821 1.48832 12.7218 1.58965 12.6263C1.60303 12.6137 1.61572 12.6004 1.62924 12.5879C1.73452 12.4908 1.8444 12.3987 1.95765 12.3107C1.97096 12.3004 1.98383 12.2895 1.99725 12.2793C2.1117 12.1922 2.23063 12.1108 2.35244 12.0336C2.36662 12.0246 2.3801 12.0145 2.39437 12.0056C2.44032 11.9771 2.48719 11.95 2.53412 11.9229C2.56226 11.9067 2.59065 11.8909 2.61913 11.8752C2.64893 11.8587 2.67864 11.8421 2.7088 11.8263C2.75348 11.8028 2.79844 11.7797 2.84389 11.7576C2.88103 11.7394 2.91922 11.7235 2.95686 11.7063C2.98716 11.6925 3.01707 11.6777 3.04769 11.6644C3.09301 11.6447 3.13912 11.6269 3.18511 11.6085C3.21662 11.5959 3.24764 11.582 3.27945 11.5701C3.32936 11.5513 3.38019 11.5349 3.43084 11.5177C3.45418 11.5097 3.47722 11.5008 3.50071 11.4932C3.55658 11.4751 3.61289 11.4581 3.66958 11.442C3.69166 11.4357 3.71376 11.4293 3.73596 11.4233C3.79907 11.4063 3.86289 11.3914 3.92695 11.3767C3.94209 11.3733 3.95718 11.3696 3.97237 11.3663C4.11504 11.335 4.25985 11.3095 4.40675 11.2906C4.43232 11.2873 4.45794 11.2842 4.48362 11.2812C4.54425 11.2744 4.60519 11.2685 4.66645 11.2638C4.67847 11.2629 4.69052 11.2623 4.70256 11.2614L4.80038 11.2545C4.88671 11.2501 4.97383 11.2475 5.06125 11.2475H7.39855ZM16.9388 11.2475C19.734 11.2476 22 13.5124 22 16.3052C21.9998 19.0979 19.7338 21.3616 16.9388 21.3618H13.2832C14.4337 20.2108 15.1709 18.4482 15.1709 16.4718C15.1709 14.2926 14.2749 12.3735 12.9163 11.2475H16.9388ZM7.39855 0.00116458C10.1937 0.0013425 12.4598 2.26497 12.4598 5.05775C12.4597 7.85045 10.1937 10.1142 7.39855 10.1143H0V5.05775C0 4.89236 0.00775963 4.7288 0.0232915 4.56747C0.0274579 4.52418 0.0331791 4.48116 0.038431 4.4382C0.0524815 4.32328 0.0692046 4.20938 0.0908369 4.09698L0.102483 4.03875C0.106227 4.02046 0.110188 4.00223 0.114128 3.98401C0.127164 3.92376 0.142054 3.8641 0.157218 3.80467C0.164069 3.77781 0.1709 3.75099 0.17818 3.72431C0.188513 3.68644 0.199603 3.64886 0.210788 3.61135C0.225482 3.56207 0.241227 3.51324 0.257371 3.46461C0.269763 3.42728 0.281404 3.38975 0.294638 3.35281C0.30559 3.32224 0.318053 3.29227 0.329575 3.26198C0.343044 3.22656 0.356098 3.19103 0.370335 3.156C0.387188 3.11454 0.404827 3.07346 0.422741 3.03256C0.44039 2.99225 0.458814 2.95235 0.477476 2.9126C0.491731 2.88224 0.505727 2.85179 0.520565 2.82177C0.537576 2.78734 0.555205 2.75326 0.572971 2.71928C0.592296 2.68233 0.610995 2.64506 0.6312 2.60865C0.657644 2.56099 0.68596 2.51444 0.713885 2.46774C0.725232 2.44876 0.736069 2.42949 0.747658 2.41067C0.773311 2.36902 0.800032 2.32807 0.826849 2.28723C0.843934 2.26121 0.860541 2.23488 0.87809 2.2092C0.953313 2.0991 1.03336 1.99256 1.11683 1.88894C1.18114 1.80912 1.24702 1.73059 1.31597 1.65486C1.47484 1.48036 1.64557 1.31664 1.82722 1.16574C1.85016 1.14669 1.87382 1.12849 1.89709 1.10984C1.99837 1.02867 2.10292 0.951415 2.21036 0.87809C2.23606 0.86056 2.26236 0.843915 2.28839 0.826849C2.32925 0.800052 2.37017 0.773292 2.41184 0.747658C2.43349 0.734341 2.45518 0.721061 2.47705 0.708062C2.52886 0.677257 2.58129 0.647377 2.63427 0.61839C2.64707 0.61139 2.65984 0.604319 2.6727 0.597427C2.72986 0.566794 2.78777 0.537396 2.84622 0.50892C2.86043 0.502002 2.87503 0.49591 2.88931 0.489122C2.94744 0.461478 3.00584 0.434234 3.06516 0.408766C3.0837 0.400809 3.10241 0.393216 3.12106 0.385475C3.17763 0.361989 3.23467 0.3394 3.29226 0.317929C3.30779 0.31214 3.32324 0.306101 3.33884 0.300461C3.39807 0.279043 3.45795 0.25915 3.51818 0.239903C3.54179 0.232361 3.56547 0.224979 3.58922 0.217776C3.63126 0.205025 3.67367 0.193353 3.71616 0.181674C3.7556 0.170838 3.79513 0.16013 3.83495 0.15023C3.9786 0.114503 4.12462 0.0848882 4.27283 0.0617225C4.28096 0.0604516 4.28914 0.059461 4.29728 0.0582288C4.36902 0.047376 4.44121 0.038089 4.5139 0.030279C4.5248 0.0291076 4.53558 0.0267227 4.5465 0.0256207C4.55542 0.0247215 4.56436 0.0241444 4.57329 0.0232915L4.57096 0.0244561C4.73223 0.00896687 4.89591 0.00117115 5.06125 0.00116458H7.39855ZM16.9388 0C19.734 0.000177881 22 2.26497 22 5.05775C21.9999 7.85045 19.7339 10.1142 16.9388 10.1143H12.9175C14.2754 8.98827 15.1708 7.06961 15.1709 4.89122C15.1709 2.91428 14.433 1.15104 13.282 0H16.9388Z" fill="#FAF8F4"/></svg>
207
+ </div>
208
+ <span style="font-size: 1rem; font-weight: 600;">basestream</span>
209
+ </div>
210
+ <div style="background: #fffdf9; border: 1px solid #e8e4de; border-radius: 16px; padding: 2rem; display: inline-block;">
211
+ <h1 style="font-family: 'Newsreader', Georgia, serif; font-size: 1.25rem; color: #6b7e6b; margin-bottom: 0.5rem;">&#10003; Authenticated</h1>
212
+ <p style="color: #8a8379; font-size: 0.875rem;">You can close this tab and return to your terminal.</p>
213
+ </div>
214
+ </div>
215
+ </body>
216
+ </html>`);
217
+ server.close();
218
+ writeConfig({
219
+ apiKey: key,
220
+ baseUrl,
221
+ orgId: orgId || void 0
222
+ });
223
+ resolve(key);
224
+ } else {
225
+ res.writeHead(400);
226
+ res.end("Missing key parameter");
227
+ }
228
+ });
229
+ server.listen(0, "127.0.0.1", () => {
230
+ const addr = server.address();
231
+ if (!addr || typeof addr === "string") {
232
+ reject(new Error("Failed to start callback server"));
233
+ return;
234
+ }
235
+ const callbackPort = addr.port;
236
+ const authUrl = `${baseUrl}/api/auth/cli?callback=http://127.0.0.1:${callbackPort}`;
237
+ openBrowser(authUrl);
238
+ });
239
+ setTimeout(() => {
240
+ server.close();
241
+ reject(new Error("Authentication timed out"));
242
+ }, 12e4);
243
+ });
244
+ check("Authenticated with Basestream");
245
+ return apiKey;
246
+ }
247
+
248
+ // src/cli/commands/init.ts
249
+ var CLAUDE_SETTINGS_PATH = path2.join(
250
+ os2.homedir(),
251
+ ".claude",
252
+ "settings.json"
253
+ );
254
+ var HOOK_COMMAND = "npx @basestream/cli _hook-stop";
255
+ var HOOK_MARKER = "_hook-stop";
256
+ function detectClaudeCode() {
257
+ try {
258
+ const version = execSync2("claude --version 2>/dev/null", {
259
+ encoding: "utf-8"
260
+ }).trim();
261
+ return version || null;
262
+ } catch {
263
+ return null;
264
+ }
265
+ }
266
+ function injectClaudeCodeHook() {
267
+ const settingsDir = path2.dirname(CLAUDE_SETTINGS_PATH);
268
+ fs2.mkdirSync(settingsDir, { recursive: true });
269
+ let settings = {};
270
+ if (fs2.existsSync(CLAUDE_SETTINGS_PATH)) {
271
+ try {
272
+ settings = JSON.parse(fs2.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
273
+ } catch {
274
+ }
275
+ }
276
+ if (!settings.hooks || typeof settings.hooks !== "object") {
277
+ settings.hooks = {};
278
+ }
279
+ const hooks = settings.hooks;
280
+ if (Array.isArray(hooks.Stop)) {
281
+ const existing = hooks.Stop;
282
+ const alreadyInstalled = existing.some(
283
+ (entry) => entry.hooks?.some((h) => h.command?.includes(HOOK_MARKER))
284
+ );
285
+ if (alreadyInstalled) {
286
+ check("Claude Code hook already installed");
287
+ return;
288
+ }
289
+ }
290
+ if (!Array.isArray(hooks.Stop)) {
291
+ hooks.Stop = [];
292
+ }
293
+ hooks.Stop.push({
294
+ matcher: "*",
295
+ hooks: [
296
+ {
297
+ type: "command",
298
+ command: HOOK_COMMAND,
299
+ timeout: 30
300
+ }
301
+ ]
302
+ });
303
+ fs2.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
304
+ check("Injected tracking hook into ~/.claude/settings.json");
305
+ }
306
+ async function init() {
307
+ console.log();
308
+ const ccVersion = detectClaudeCode();
309
+ if (ccVersion) {
310
+ console.log(` ${c.dim(`Detected: Claude Code ${ccVersion}`)}`);
311
+ } else {
312
+ warn("Claude Code not detected \u2014 hook will activate when installed");
313
+ }
314
+ console.log();
315
+ injectClaudeCodeHook();
316
+ ensureDirs();
317
+ check(`Created ~/.basestream/buffer/`);
318
+ const existing = readConfig();
319
+ if (existing?.apiKey) {
320
+ check("Already authenticated");
321
+ } else {
322
+ console.log();
323
+ await login();
324
+ }
325
+ console.log();
326
+ console.log(
327
+ ` ${c.dim("That's it. Work normally \u2014 every session is now tracked.")}`
328
+ );
329
+ console.log();
330
+ }
331
+
332
+ // src/cli/commands/status.ts
333
+ init_config();
334
+ init_util();
335
+ import fs3 from "node:fs";
336
+ import path3 from "node:path";
337
+ function groupBy(arr, fn) {
338
+ const result = {};
339
+ for (const item of arr) {
340
+ const key = fn(item);
341
+ (result[key] ??= []).push(item);
342
+ }
343
+ return result;
344
+ }
345
+ function formatOutcome(outcome) {
346
+ switch (outcome) {
347
+ case "COMPLETED":
348
+ return c.green("completed");
349
+ case "IN_PROGRESS":
350
+ return c.yellow("in progress");
351
+ case "BLOCKED":
352
+ return c.red("blocked");
353
+ default:
354
+ return c.dim(outcome.toLowerCase());
355
+ }
356
+ }
357
+ async function fetchRemoteEntries(config) {
358
+ try {
359
+ const res = await fetch(
360
+ `${config.baseUrl}/api/entries?period=week&limit=100`,
361
+ {
362
+ headers: { Authorization: `Bearer ${config.apiKey}` }
363
+ }
364
+ );
365
+ if (!res.ok) return [];
366
+ const data = await res.json();
367
+ return data.entries || [];
368
+ } catch {
369
+ return [];
370
+ }
371
+ }
372
+ function readLocalEntries() {
373
+ ensureDirs();
374
+ const files = fs3.readdirSync(BUFFER_DIR).filter((f) => f.endsWith(".json"));
375
+ const entries = [];
376
+ for (const file of files) {
377
+ try {
378
+ entries.push(
379
+ JSON.parse(fs3.readFileSync(path3.join(BUFFER_DIR, file), "utf-8"))
380
+ );
381
+ } catch {
382
+ }
383
+ }
384
+ return entries;
385
+ }
386
+ async function status() {
387
+ const config = readConfig();
388
+ const entries = readLocalEntries();
389
+ if (config?.apiKey) {
390
+ const remote = await fetchRemoteEntries(config);
391
+ const seen = new Set(entries.map((e) => e.sessionId).filter(Boolean));
392
+ for (const re of remote) {
393
+ if (!re.sessionId || !seen.has(re.sessionId)) {
394
+ entries.push(re);
395
+ }
396
+ }
397
+ }
398
+ if (entries.length === 0) {
399
+ console.log();
400
+ console.log(` ${c.dim("No sessions logged this week.")}`);
401
+ console.log(
402
+ ` ${c.dim("Start using Claude Code \u2014 entries will appear here.")}`
403
+ );
404
+ console.log();
405
+ return;
406
+ }
407
+ console.log();
408
+ console.log(
409
+ ` ${c.yellow(`This week \u2014 ${entries.length} sessions logged`)}`
410
+ );
411
+ console.log();
412
+ const byProject = groupBy(entries, (e) => e.projectName || "unknown");
413
+ for (const [project, projectEntries] of Object.entries(byProject)) {
414
+ const commits = projectEntries.reduce(
415
+ (sum, e) => sum + (e.commitShas?.length || 0),
416
+ 0
417
+ );
418
+ const latestOutcome = projectEntries[0]?.outcome || "IN_PROGRESS";
419
+ const outcomeStr = formatOutcome(latestOutcome);
420
+ const parts = [`${projectEntries.length} sessions`];
421
+ if (commits > 0) {
422
+ parts.push(`${commits} commit${commits !== 1 ? "s" : ""}`);
423
+ }
424
+ parts.push(outcomeStr);
425
+ console.log(
426
+ ` ${c.bold(project)} ${c.dim(parts.join(" \xB7 "))}`
427
+ );
428
+ }
429
+ const localOnly = readLocalEntries();
430
+ if (localOnly.length > 0) {
431
+ console.log();
432
+ console.log(
433
+ ` ${c.dim(`${localOnly.length} entries pending sync`)}`
434
+ );
435
+ }
436
+ console.log();
437
+ }
438
+
439
+ // src/cli/index.ts
440
+ init_sync();
441
+
442
+ // src/cli/commands/hook-stop.ts
443
+ init_config();
444
+ import fs5 from "node:fs";
445
+ import path5 from "node:path";
446
+ import { execSync as execSync3 } from "node:child_process";
447
+ function parseTranscriptLine(line) {
448
+ try {
449
+ return JSON.parse(line);
450
+ } catch {
451
+ return null;
452
+ }
453
+ }
454
+ function extractGitInfo(cwd) {
455
+ const result = {};
456
+ try {
457
+ result.branch = execSync3("git rev-parse --abbrev-ref HEAD", {
458
+ cwd,
459
+ encoding: "utf-8",
460
+ stdio: ["pipe", "pipe", "pipe"]
461
+ }).trim();
462
+ } catch {
463
+ }
464
+ try {
465
+ result.repo = execSync3("git remote get-url origin", {
466
+ cwd,
467
+ encoding: "utf-8",
468
+ stdio: ["pipe", "pipe", "pipe"]
469
+ }).trim();
470
+ } catch {
471
+ }
472
+ result.projectName = path5.basename(cwd);
473
+ return result;
474
+ }
475
+ function readSessionAccumulator(sessionId) {
476
+ const sessionFile = path5.join(SESSION_DIR, `${sessionId}.json`);
477
+ if (!fs5.existsSync(sessionFile)) return null;
478
+ try {
479
+ const raw = JSON.parse(fs5.readFileSync(sessionFile, "utf-8"));
480
+ raw.filesWritten = new Set(raw.filesWritten || []);
481
+ raw.commitShas = new Set(raw.commitShas || []);
482
+ return raw;
483
+ } catch {
484
+ return null;
485
+ }
486
+ }
487
+ function writeSessionAccumulator(acc) {
488
+ const sessionFile = path5.join(SESSION_DIR, `${acc.sessionId}.json`);
489
+ const serializable = {
490
+ ...acc,
491
+ filesWritten: Array.from(acc.filesWritten),
492
+ commitShas: Array.from(acc.commitShas)
493
+ };
494
+ fs5.writeFileSync(sessionFile, JSON.stringify(serializable, null, 2));
495
+ }
496
+ function analyzeTranscript(transcriptPath, acc) {
497
+ if (!fs5.existsSync(transcriptPath)) return acc;
498
+ const content = fs5.readFileSync(transcriptPath, "utf-8");
499
+ const lines = content.split("\n").filter(Boolean);
500
+ for (const line of lines) {
501
+ const entry = parseTranscriptLine(line);
502
+ if (!entry) continue;
503
+ const message = entry.message;
504
+ const contentBlocks = Array.isArray(message?.content) ? message.content : [];
505
+ if (entry.type === "assistant" && contentBlocks.length > 0) {
506
+ const hasText = contentBlocks.some(
507
+ (b) => b.type === "text" || b.type === "thinking"
508
+ );
509
+ if (hasText) acc.turns++;
510
+ }
511
+ if (entry.type === "assistant") {
512
+ for (const block of contentBlocks) {
513
+ if (block.type === "tool_use") {
514
+ const toolName = block.name || "unknown";
515
+ acc.toolCalls.push({
516
+ tool: toolName,
517
+ timestamp: entry.timestamp
518
+ });
519
+ if (toolName === "Write" || toolName === "Edit" || toolName === "NotebookEdit") {
520
+ const input = block.input;
521
+ const filePath = input?.file_path || input?.path;
522
+ if (filePath) acc.filesWritten.add(filePath);
523
+ }
524
+ }
525
+ }
526
+ }
527
+ if (entry.type === "user") {
528
+ for (const block of contentBlocks) {
529
+ if (block.type === "tool_result") {
530
+ const resultContent = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c2) => c2.text || "").join("\n") : "";
531
+ const shaMatch = resultContent.match(/\[([a-f0-9]{7,12})\]\s/);
532
+ if (shaMatch) {
533
+ acc.commitShas.add(shaMatch[1]);
534
+ }
535
+ }
536
+ }
537
+ }
538
+ }
539
+ return acc;
540
+ }
541
+ function categorizeWork(acc) {
542
+ const files = Array.from(acc.filesWritten);
543
+ const fileCount = files.length;
544
+ let category = "OTHER";
545
+ const testFiles = files.filter(
546
+ (f) => f.includes(".test.") || f.includes(".spec.") || f.includes("__tests__")
547
+ );
548
+ const docFiles = files.filter(
549
+ (f) => f.endsWith(".md") || f.includes("/docs/")
550
+ );
551
+ const configFiles = files.filter(
552
+ (f) => f.includes("Dockerfile") || f.includes(".yml") || f.includes(".yaml") || f.includes("ci")
553
+ );
554
+ if (testFiles.length > fileCount / 2) category = "TESTING";
555
+ else if (docFiles.length > fileCount / 2) category = "DOCS";
556
+ else if (configFiles.length > fileCount / 2) category = "DEVOPS";
557
+ else if (fileCount > 0) category = "FEATURE";
558
+ let complexity = "LOW";
559
+ if (fileCount === 0) complexity = "LOW";
560
+ else if (fileCount <= 2) complexity = "LOW";
561
+ else if (fileCount <= 6) complexity = "MEDIUM";
562
+ else complexity = "HIGH";
563
+ return { category, complexity };
564
+ }
565
+ function flushToBuffer(acc) {
566
+ const { category, complexity } = categorizeWork(acc);
567
+ const now = /* @__PURE__ */ new Date();
568
+ const startTime = new Date(acc.startedAt);
569
+ const durationMin = Math.round(
570
+ (now.getTime() - startTime.getTime()) / 6e4
571
+ );
572
+ const entry = {
573
+ sessionId: acc.sessionId,
574
+ tool: "claude-code",
575
+ projectName: acc.projectName || path5.basename(acc.cwd),
576
+ repo: acc.gitRepo || null,
577
+ branch: acc.gitBranch || null,
578
+ summary: buildSummary(acc, category),
579
+ category,
580
+ why: null,
581
+ // Hook-based: no "why" available (CLAUDE.md rules would provide this)
582
+ whatChanged: Array.from(acc.filesWritten).map(
583
+ (f) => path5.relative(acc.cwd, f) || f
584
+ ),
585
+ outcome: "IN_PROGRESS",
586
+ filesTouched: acc.filesWritten.size,
587
+ complexity,
588
+ toolVersion: null,
589
+ model: null,
590
+ sessionDurationMin: durationMin > 0 ? durationMin : 1,
591
+ turns: acc.turns,
592
+ prUrl: null,
593
+ ticketUrl: null,
594
+ commitShas: Array.from(acc.commitShas),
595
+ visibility: "TEAM",
596
+ bufferedAt: now.toISOString()
597
+ };
598
+ ensureDirs();
599
+ const filename = `${Date.now()}-${acc.sessionId.slice(0, 8)}.json`;
600
+ fs5.writeFileSync(
601
+ path5.join(BUFFER_DIR, filename),
602
+ JSON.stringify(entry, null, 2)
603
+ );
604
+ }
605
+ function buildSummary(acc, category) {
606
+ const fileCount = acc.filesWritten.size;
607
+ const commitCount = acc.commitShas.size;
608
+ const parts = [];
609
+ if (category !== "OTHER") {
610
+ parts.push(category.toLowerCase());
611
+ }
612
+ if (fileCount > 0) {
613
+ parts.push(
614
+ `${fileCount} file${fileCount !== 1 ? "s" : ""} modified`
615
+ );
616
+ }
617
+ if (commitCount > 0) {
618
+ parts.push(
619
+ `${commitCount} commit${commitCount !== 1 ? "s" : ""}`
620
+ );
621
+ }
622
+ if (acc.projectName) {
623
+ parts.push(`in ${acc.projectName}`);
624
+ }
625
+ return parts.length > 0 ? parts.join(", ") : `Claude Code session in ${acc.projectName || "unknown project"}`;
626
+ }
627
+ async function hookStop() {
628
+ let payload;
629
+ try {
630
+ const chunks = [];
631
+ for await (const chunk of process.stdin) {
632
+ chunks.push(chunk);
633
+ }
634
+ payload = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
635
+ } catch {
636
+ process.exit(0);
637
+ }
638
+ const { session_id, transcript_path, cwd } = payload;
639
+ if (!session_id) process.exit(0);
640
+ ensureDirs();
641
+ let acc = readSessionAccumulator(session_id);
642
+ if (!acc) {
643
+ const gitInfo = extractGitInfo(cwd);
644
+ acc = {
645
+ sessionId: session_id,
646
+ cwd,
647
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
648
+ lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(),
649
+ turns: 0,
650
+ toolCalls: [],
651
+ filesWritten: /* @__PURE__ */ new Set(),
652
+ commitShas: /* @__PURE__ */ new Set(),
653
+ gitBranch: gitInfo.branch,
654
+ gitRepo: gitInfo.repo,
655
+ projectName: gitInfo.projectName
656
+ };
657
+ }
658
+ if (transcript_path) {
659
+ acc = analyzeTranscript(transcript_path, acc);
660
+ }
661
+ acc.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
662
+ writeSessionAccumulator(acc);
663
+ if (acc.filesWritten.size > 0 || acc.commitShas.size > 0 || acc.turns >= 3) {
664
+ flushToBuffer(acc);
665
+ const config = readConfig();
666
+ if (config?.apiKey) {
667
+ try {
668
+ const { syncEntries: syncEntries2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
669
+ await syncEntries2(config);
670
+ } catch {
671
+ }
672
+ }
673
+ }
674
+ }
675
+
676
+ // src/cli/index.ts
677
+ var HELP = `
678
+ basestream \u2014 AI work intelligence for teams
679
+
680
+ Usage:
681
+ npx @basestream/cli init Set up everything (hooks, auth, buffer)
682
+
683
+ Commands:
684
+ basestream init Detect AI tools, inject hooks, authenticate
685
+ basestream login Authenticate with your Basestream account
686
+ basestream status Show this week's logged sessions
687
+ basestream sync Manually sync buffered entries to Basestream
688
+
689
+ Options:
690
+ --help, -h Show this help message
691
+ --version, -v Show version
692
+ `;
693
+ async function main() {
694
+ const command = process.argv[2];
695
+ if (command === "--help" || command === "-h") {
696
+ console.log(HELP.trim());
697
+ process.exit(0);
698
+ }
699
+ if (command === "--version" || command === "-v") {
700
+ console.log(true ? "0.1.3" : "dev");
701
+ process.exit(0);
702
+ }
703
+ switch (command || "init") {
704
+ case "init":
705
+ await init();
706
+ process.exit(0);
707
+ break;
708
+ case "login":
709
+ await login();
710
+ process.exit(0);
711
+ break;
712
+ case "status":
713
+ await status();
714
+ break;
715
+ case "sync":
716
+ await sync();
717
+ break;
718
+ case "_hook-stop":
719
+ await hookStop();
720
+ break;
721
+ default:
722
+ console.error(`Unknown command: ${command}`);
723
+ console.log(HELP.trim());
724
+ process.exit(1);
725
+ }
726
+ }
727
+ main().catch((err) => {
728
+ console.error(err.message || err);
729
+ process.exit(1);
730
+ });