bisync-cli 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bisync.js +439 -90
  2. package/package.json +1 -1
package/dist/bisync.js CHANGED
@@ -2,12 +2,16 @@
2
2
  // @bun
3
3
 
4
4
  // src/bin.ts
5
+ import { createHash } from "crypto";
5
6
  import { createWriteStream, mkdirSync } from "fs";
6
7
  import { readdir, stat } from "fs/promises";
7
8
  import { homedir } from "os";
8
- import { join, resolve } from "path";
9
+ import { join, relative, resolve, sep } from "path";
10
+ import { createInterface } from "readline";
11
+ import { setTimeout } from "timers/promises";
12
+ import { parseArgs as parseArgsUtil } from "util";
9
13
  // package.json
10
- var version = "0.0.7";
14
+ var version = "0.0.9";
11
15
 
12
16
  // src/bin.ts
13
17
  var CONFIG_DIR = join(homedir(), ".agent-bisync");
@@ -17,17 +21,38 @@ var CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
17
21
  var DEBUG_LOG_PATH = join(CONFIG_DIR, "debug.log");
18
22
  var CLIENT_ID = "bisync-cli";
19
23
  var MAX_LINES_PER_BATCH = 200;
20
- var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
21
- var usage = () => {
24
+ var parseArgs = () => {
25
+ const parsed = parseArgsUtil({
26
+ args: process.argv.slice(2),
27
+ options: {
28
+ verbose: { type: "boolean" },
29
+ version: { type: "boolean" },
30
+ help: { type: "boolean" },
31
+ force: { type: "boolean" },
32
+ "site-url": { type: "string" },
33
+ siteUrl: { type: "string" }
34
+ },
35
+ allowPositionals: true
36
+ });
37
+ const values = parsed.values;
38
+ const positionals = parsed.positionals;
39
+ return { values, positionals };
40
+ };
41
+ var printUsage = () => {
22
42
  console.log(`bisync <command>
23
43
 
24
44
  Commands:
25
- setup --site-url <url> Configure hooks and authenticate
26
- verify Check auth and list session ids
27
- hook <HOOK> Handle Claude hook input (stdin JSON)
45
+ auth login --site-url <url> Authenticate via device flow
46
+ auth logout Sign out of the CLI
47
+ session list List session ids
48
+ setup --site-url <url> Configure hooks and authenticate
49
+ hook <HOOK> Handle Claude hook input (stdin JSON)
28
50
 
29
51
  Global options:
30
52
  --verbose Enable verbose logging
53
+ --help Show usage
54
+
55
+ Version: ${version} (Bun ${Bun.version})
31
56
  `);
32
57
  };
33
58
  var LOG_LEVEL = "info";
@@ -46,14 +71,19 @@ var getDebugLogStream = () => {
46
71
  return null;
47
72
  }
48
73
  };
49
- var log = (level, message) => {
50
- const logMessage = `[bisync][${level}] ${message}`;
51
- getDebugLogStream()?.write(`[${new Date().toISOString()}] ${logMessage}
74
+ var log = (level, message, fields) => {
75
+ let logMessage = `[${level}] ${message}`;
76
+ if (fields) {
77
+ for (const [key, value] of Object.entries(fields)) {
78
+ logMessage += ` ${key}=${value}`;
79
+ }
80
+ }
81
+ getDebugLogStream()?.write(`[${new Date().toISOString()}]${logMessage}
52
82
  `);
53
83
  if (level < LOG_LEVEL) {
54
84
  return;
55
85
  }
56
- console.log(logMessage);
86
+ console.log(`[bisync]${logMessage}`);
57
87
  };
58
88
  var readConfig = async () => {
59
89
  return await Bun.file(CONFIG_PATH).json();
@@ -63,9 +93,34 @@ var writeConfig = async (config) => {
63
93
  createPath: true
64
94
  });
65
95
  };
96
+ var readConfigIfExists = async () => {
97
+ const configFile = Bun.file(CONFIG_PATH);
98
+ if (!await configFile.exists()) {
99
+ return null;
100
+ }
101
+ try {
102
+ return await configFile.json();
103
+ } catch {
104
+ return null;
105
+ }
106
+ };
107
+ var promptYesNo = async (question, defaultValue) => {
108
+ if (!process.stdin.isTTY) {
109
+ log("warn", `${question} Using default ${defaultValue ? "yes" : "no"} (stdin not interactive).`);
110
+ return defaultValue;
111
+ }
112
+ const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
113
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
114
+ const answer = await new Promise((resolve2) => rl.question(`${question}${suffix}`, resolve2));
115
+ rl.close();
116
+ const normalized = answer.trim().toLowerCase();
117
+ if (!normalized) {
118
+ return defaultValue;
119
+ }
120
+ return normalized === "y" || normalized === "yes";
121
+ };
66
122
  var resolveSiteUrl = (args) => {
67
- const argIndex = args.findIndex((value) => value === "--site-url" || value === "--siteUrl");
68
- const argValue = argIndex >= 0 ? args[argIndex + 1] : undefined;
123
+ const argValue = args["site-url"] ?? args.siteUrl;
69
124
  const envValue = process.env.BISYNC_SITE_URL;
70
125
  if (argValue) {
71
126
  return { siteUrl: argValue, source: "arg" };
@@ -83,8 +138,21 @@ var openBrowser = async (url) => {
83
138
  await Bun.$`open ${url}`;
84
139
  } catch {}
85
140
  };
141
+ var buildVerificationUrl = (data) => {
142
+ if (data.verification_uri_complete) {
143
+ return data.verification_uri_complete;
144
+ }
145
+ try {
146
+ const url = new URL(data.verification_uri);
147
+ url.searchParams.set("user_code", data.user_code);
148
+ return url.toString();
149
+ } catch {
150
+ const separator = data.verification_uri.includes("?") ? "&" : "?";
151
+ return `${data.verification_uri}${separator}user_code=${encodeURIComponent(data.user_code)}`;
152
+ }
153
+ };
86
154
  var deviceAuthFlow = async (siteUrl) => {
87
- log("debug", `device auth start siteUrl=${siteUrl}`);
155
+ log("debug", `device auth start`, { siteUrl });
88
156
  const codeResponse = await fetch(`${siteUrl}/api/auth/device/code`, {
89
157
  method: "POST",
90
158
  headers: { "Content-Type": "application/json" },
@@ -92,20 +160,20 @@ var deviceAuthFlow = async (siteUrl) => {
92
160
  });
93
161
  if (!codeResponse.ok) {
94
162
  const errorText = await codeResponse.text();
95
- log("debug", `device code request failed status=${codeResponse.status}`);
163
+ log("debug", `device code request failed`, { status: codeResponse.status });
96
164
  throw new Error(`Device code request failed: ${codeResponse.status} ${errorText}`);
97
165
  }
98
- log("debug", `device code request ok status=${codeResponse.status}`);
166
+ log("debug", `device code request ok`, { status: codeResponse.status });
99
167
  const codeData = await codeResponse.json();
100
- const verificationUrl = codeData.verification_uri_complete ?? codeData.verification_uri;
101
- log("info", `Authorize this device: ${verificationUrl}`);
102
- log("info", `User code: ${codeData.user_code}`);
168
+ const verificationUrl = buildVerificationUrl(codeData);
169
+ console.log(`Authorize this device: ${verificationUrl}`);
170
+ console.log(`Your Verification Code: ${codeData.user_code}`);
103
171
  await openBrowser(verificationUrl);
104
172
  const intervalMs = Math.max(1, codeData.interval ?? 5) * 1000;
105
- log("debug", `device auth polling intervalMs=${intervalMs}`);
173
+ log("debug", `device auth polling`, { intervalMs });
106
174
  let pollDelay = intervalMs;
107
175
  while (true) {
108
- await sleep(pollDelay);
176
+ await setTimeout(pollDelay);
109
177
  const tokenResponse = await fetch(`${siteUrl}/api/auth/device/token`, {
110
178
  method: "POST",
111
179
  headers: { "Content-Type": "application/json" },
@@ -117,7 +185,7 @@ var deviceAuthFlow = async (siteUrl) => {
117
185
  });
118
186
  const tokenData = await tokenResponse.json();
119
187
  if (tokenResponse.ok && tokenData.access_token) {
120
- log("debug", `device auth success status=${tokenResponse.status}`);
188
+ log("debug", `device auth success`, { status: tokenResponse.status });
121
189
  return tokenData.access_token;
122
190
  }
123
191
  if (tokenData.error === "authorization_pending") {
@@ -125,10 +193,13 @@ var deviceAuthFlow = async (siteUrl) => {
125
193
  }
126
194
  if (tokenData.error === "slow_down") {
127
195
  pollDelay += 1000;
128
- log("debug", `device auth slow_down nextDelayMs=${pollDelay}`);
196
+ log("debug", `device auth slow_down`, { nextDelayMs: pollDelay });
129
197
  continue;
130
198
  }
131
- log("debug", `device auth failed status=${tokenResponse.status} error=${tokenData.error ?? "unknown"}`);
199
+ log("debug", `device auth failed`, {
200
+ status: tokenResponse.status,
201
+ error: tokenData.error ?? "unknown"
202
+ });
132
203
  throw new Error(`Device auth failed: ${tokenData.error ?? "unknown"} ${tokenData.error_description ?? ""}`.trim());
133
204
  }
134
205
  };
@@ -149,10 +220,10 @@ var ensureHookEntry = (entries, next) => {
149
220
  return entries;
150
221
  };
151
222
  var mergeSettings = async () => {
152
- log("debug", `mergeSettings path=${CLAUDE_SETTINGS_PATH}`);
223
+ log("debug", `mergeSettings`, { path: CLAUDE_SETTINGS_PATH });
153
224
  const settingsFile = Bun.file(CLAUDE_SETTINGS_PATH);
154
225
  const settingsExists = await settingsFile.exists();
155
- log("debug", `mergeSettings existing=${settingsExists}`);
226
+ log("debug", `mergeSettings`, { existing: settingsExists });
156
227
  let current = {};
157
228
  if (settingsExists) {
158
229
  try {
@@ -191,9 +262,14 @@ var mergeSettings = async () => {
191
262
  log("debug", `mergeSettings wrote settings`);
192
263
  try {
193
264
  const info = await stat(CLAUDE_SETTINGS_PATH);
194
- log("debug", `mergeSettings file size=${info.size} mtime=${new Date(info.mtimeMs).toISOString()}`);
265
+ log("debug", `mergeSettings file`, {
266
+ size: info.size,
267
+ mtime: new Date(info.mtimeMs).toISOString()
268
+ });
195
269
  } catch (error) {
196
- log("debug", `mergeSettings stat failed: ${error instanceof Error ? error.message : String(error)}`);
270
+ log("debug", `mergeSettings stat failed`, {
271
+ error: error instanceof Error ? error.message : String(error)
272
+ });
197
273
  }
198
274
  };
199
275
  var findSessionFile = async (sessionId) => {
@@ -226,10 +302,140 @@ var findSessionFile = async (sessionId) => {
226
302
  await walk(CLAUDE_PROJECTS_DIR);
227
303
  return bestPath;
228
304
  };
229
- var appendDebugLog = async (message) => {
230
- const logFile = Bun.file(DEBUG_LOG_PATH);
231
- const content = await logFile.text();
232
- await Bun.write(DEBUG_LOG_PATH, content + message, { createPath: true });
305
+ var isDirectory = async (path) => {
306
+ try {
307
+ return (await stat(path)).isDirectory();
308
+ } catch {
309
+ return false;
310
+ }
311
+ };
312
+ var resolveSessionDir = async (sessionFile) => {
313
+ if (!sessionFile.endsWith(".jsonl")) {
314
+ return null;
315
+ }
316
+ const candidate = sessionFile.slice(0, -".jsonl".length);
317
+ return await isDirectory(candidate) ? candidate : null;
318
+ };
319
+ var findSessionDir = async (sessionId) => {
320
+ let bestPath = null;
321
+ let bestMtimeMs = 0;
322
+ const walk = async (dir) => {
323
+ let entries;
324
+ try {
325
+ entries = await readdir(dir, { withFileTypes: true });
326
+ } catch {
327
+ return;
328
+ }
329
+ for (const entry of entries) {
330
+ const fullPath = join(dir, entry.name);
331
+ if (!entry.isDirectory()) {
332
+ continue;
333
+ }
334
+ if (entry.name === sessionId) {
335
+ const info = await stat(fullPath);
336
+ if (!bestPath || info.mtimeMs > bestMtimeMs) {
337
+ bestPath = fullPath;
338
+ bestMtimeMs = info.mtimeMs;
339
+ }
340
+ }
341
+ await walk(fullPath);
342
+ }
343
+ };
344
+ await walk(CLAUDE_PROJECTS_DIR);
345
+ return bestPath;
346
+ };
347
+ var normalizePath = (value) => value.split(sep).join("/");
348
+ var collectSessionStateFiles = async (sessionDir) => {
349
+ const files = [];
350
+ const walk = async (dir) => {
351
+ let entries;
352
+ try {
353
+ entries = await readdir(dir, { withFileTypes: true });
354
+ } catch {
355
+ return;
356
+ }
357
+ for (const entry of entries) {
358
+ const fullPath = join(dir, entry.name);
359
+ if (entry.isDirectory()) {
360
+ await walk(fullPath);
361
+ continue;
362
+ }
363
+ if (!entry.isFile()) {
364
+ continue;
365
+ }
366
+ const info = await stat(fullPath);
367
+ files.push({
368
+ fullPath,
369
+ relativePath: normalizePath(relative(sessionDir, fullPath)),
370
+ size: info.size,
371
+ mtimeMs: info.mtimeMs
372
+ });
373
+ }
374
+ };
375
+ await walk(sessionDir);
376
+ return files;
377
+ };
378
+ var getStateUploadUrls = async (siteUrl, token, count) => {
379
+ const response = await fetch(`${siteUrl}/api/storage/upload-url`, {
380
+ method: "POST",
381
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
382
+ body: JSON.stringify({ count })
383
+ });
384
+ if (!response.ok) {
385
+ const errorText = await response.text();
386
+ const error = new Error(`Upload URL request failed: ${response.status} ${errorText}`);
387
+ error.status = response.status;
388
+ throw error;
389
+ }
390
+ const data = await response.json();
391
+ if (!Array.isArray(data.uploadUrls) || data.uploadUrls.length !== count) {
392
+ throw new Error("Upload URL response missing uploadUrls");
393
+ }
394
+ return data.uploadUrls;
395
+ };
396
+ var uploadStateFileToUrl = async (uploadUrl, filePath) => {
397
+ const response = await fetch(uploadUrl, {
398
+ method: "POST",
399
+ headers: { "Content-Type": "application/octet-stream" },
400
+ body: Bun.file(filePath)
401
+ });
402
+ if (!response.ok) {
403
+ const errorText = await response.text();
404
+ const error = new Error(`State upload failed: ${response.status} ${errorText}`);
405
+ error.status = response.status;
406
+ throw error;
407
+ }
408
+ const data = await response.json();
409
+ if (!data.storageId) {
410
+ throw new Error("State upload response missing storageId");
411
+ }
412
+ return data.storageId;
413
+ };
414
+ var buildStateFiles = async (sessionDir, siteUrl, token) => {
415
+ const files = await collectSessionStateFiles(sessionDir);
416
+ if (files.length === 0) {
417
+ return [];
418
+ }
419
+ const uploadUrls = await getStateUploadUrls(siteUrl, token, files.length);
420
+ const entries = [];
421
+ for (let index = 0;index < files.length; index += 1) {
422
+ const file = files[index];
423
+ const uploadUrl = uploadUrls[index];
424
+ if (!file || !uploadUrl) {
425
+ throw new Error("Upload URL list did not match file list");
426
+ }
427
+ const data = new Uint8Array(await Bun.file(file.fullPath).arrayBuffer());
428
+ const fileHash = createHash("sha256").update(data).digest("hex");
429
+ const storageId = await uploadStateFileToUrl(uploadUrl, file.fullPath);
430
+ entries.push({
431
+ path: file.relativePath,
432
+ fileHash,
433
+ storageId,
434
+ size: file.size,
435
+ mtimeMs: file.mtimeMs
436
+ });
437
+ }
438
+ return entries;
233
439
  };
234
440
  var buildLogLines = (raw) => {
235
441
  const lines = raw.split(`
@@ -272,49 +478,151 @@ var uploadLogs = async (siteUrl, token, sessionId, raw) => {
272
478
  }
273
479
  }
274
480
  };
481
+ var uploadStateFiles = async (siteUrl, token, sessionId, files) => {
482
+ const response = await fetch(`${siteUrl}/api/ingest/state`, {
483
+ method: "POST",
484
+ headers: {
485
+ "Content-Type": "application/json",
486
+ Authorization: `Bearer ${token}`,
487
+ ...process.env.BISYNC_SESSION_ID ? { "X-Bisync-Session-Id": process.env.BISYNC_SESSION_ID } : {}
488
+ },
489
+ body: JSON.stringify({
490
+ session_id: sessionId,
491
+ files
492
+ })
493
+ });
494
+ if (!response.ok) {
495
+ const errorText = await response.text();
496
+ const error = new Error(`State ingestion failed: ${response.status} ${errorText}`);
497
+ error.status = response.status;
498
+ throw error;
499
+ }
500
+ };
501
+ var signOutRemote = async (siteUrl, token) => {
502
+ try {
503
+ const response = await fetch(`${siteUrl}/api/auth/sign-out`, {
504
+ method: "POST",
505
+ headers: {
506
+ "Content-Type": "application/json",
507
+ Authorization: `Bearer ${token}`
508
+ },
509
+ body: JSON.stringify({})
510
+ });
511
+ if (!response.ok) {
512
+ const errorText = await response.text();
513
+ return {
514
+ ok: false,
515
+ error: `${response.status} ${errorText}`.trim()
516
+ };
517
+ }
518
+ return { ok: true };
519
+ } catch (error) {
520
+ return {
521
+ ok: false,
522
+ error: error instanceof Error ? error.message : String(error)
523
+ };
524
+ }
525
+ };
275
526
  var runSetup = async (args) => {
276
527
  log("debug", `runSetup start`);
277
- const force = args.includes("--force");
278
- log("debug", `runSetup force=${force}`);
528
+ const force = Boolean(args.force);
529
+ log("debug", "runSetup", { force });
279
530
  const configExists = await Bun.file(CONFIG_PATH).exists();
280
- log("debug", `config path=${CONFIG_PATH} exists=${configExists}`);
531
+ log("debug", "config", { path: CONFIG_PATH, exists: configExists });
281
532
  let existingConfig = null;
282
533
  if (configExists) {
283
534
  try {
284
535
  existingConfig = await readConfig();
285
- log("debug", `config loaded siteUrl=${existingConfig.siteUrl} token=${existingConfig.token ? "present" : "missing"}`);
536
+ log("debug", "config loaded", {
537
+ siteUrl: existingConfig.siteUrl,
538
+ token: existingConfig.token ? "present" : "missing"
539
+ });
286
540
  } catch {
287
541
  log("debug", `config load failed; continuing without existing config`);
288
542
  existingConfig = null;
289
543
  }
290
544
  }
291
545
  const resolution = resolveSiteUrl(args);
292
- log("debug", `siteUrl resolved=${resolution.siteUrl ?? "null"} source=${resolution.source}`);
546
+ log("debug", "siteUrl resolved", {
547
+ siteUrl: resolution.siteUrl ?? "null",
548
+ source: resolution.source
549
+ });
293
550
  const siteUrl = resolution.siteUrl ?? existingConfig?.siteUrl ?? null;
294
551
  if (!resolution.siteUrl && existingConfig?.siteUrl) {
295
- log("debug", `siteUrl fallback to existing config ${existingConfig.siteUrl}`);
552
+ log("debug", "siteUrl fallback to existing config", { siteUrl: existingConfig.siteUrl });
296
553
  }
297
554
  if (!siteUrl) {
298
- log("debug", `runSetup aborted: missing siteUrl`);
555
+ log("debug", "runSetup aborted", { error: "missing-site-url" });
299
556
  throw new Error("Missing site URL. Provide --site-url or BISYNC_SITE_URL.");
300
557
  }
301
558
  if (configExists && existingConfig?.token && !force) {
302
- log("debug", `using existing token; updating Claude settings only`);
559
+ log("debug", "using existing token; updating Claude settings only");
303
560
  await mergeSettings();
304
- log("info", `Updated Claude settings using existing credentials at ${CONFIG_PATH}`);
561
+ log("info", "Updated Claude settings using existing credentials", { path: CONFIG_PATH });
305
562
  return;
306
563
  }
307
564
  const token = await deviceAuthFlow(siteUrl);
308
565
  await writeConfig({ siteUrl, token, clientId: CLIENT_ID });
309
- log("debug", `config written to ${CONFIG_PATH}`);
566
+ log("debug", "config written", { path: CONFIG_PATH });
310
567
  await mergeSettings();
311
- log("info", `Configured Claude settings and saved credentials to ${CONFIG_PATH}`);
568
+ log("info", "Configured Claude settings and saved credentials", { path: CONFIG_PATH });
569
+ };
570
+ var runAuthLogin = async (args) => {
571
+ log("debug", "runAuthLogin start");
572
+ const force = Boolean(args.force);
573
+ const config = await readConfigIfExists();
574
+ const resolution = resolveSiteUrl(args);
575
+ const siteUrl = resolution.siteUrl ?? config?.siteUrl ?? null;
576
+ if (!siteUrl) {
577
+ log("error", "runAuthLogin aborted", { error: "missing-site-url" });
578
+ throw new Error("Missing site URL. Provide --site-url or BISYNC_SITE_URL.");
579
+ }
580
+ if (config?.token) {
581
+ const shouldReauth = force || await promptYesNo("Already signed in. Sign out and re-authenticate?", false);
582
+ if (!shouldReauth) {
583
+ log("info", "Keeping existing credentials.");
584
+ return;
585
+ }
586
+ const signOutResult = await signOutRemote(siteUrl, config.token);
587
+ await writeConfig({ siteUrl, token: "", clientId: CLIENT_ID });
588
+ if (!signOutResult.ok) {
589
+ log("warn", `Remote sign-out failed: ${signOutResult.error ?? "unknown error"}. Continuing with login.`);
590
+ }
591
+ }
592
+ const token = await deviceAuthFlow(siteUrl);
593
+ await writeConfig({ siteUrl, token, clientId: CLIENT_ID });
594
+ log("info", `Signed in and saved credentials to ${CONFIG_PATH}`);
312
595
  };
313
- var runVerify = async () => {
314
- log("debug", `runVerify start config=${CONFIG_PATH}`);
596
+ var runAuthLogout = async (args) => {
597
+ log("debug", `runAuthLogout start`);
598
+ const config = await readConfigIfExists();
599
+ const resolution = resolveSiteUrl(args);
600
+ const siteUrl = resolution.siteUrl ?? config?.siteUrl ?? null;
601
+ if (!siteUrl) {
602
+ if (!config?.token) {
603
+ log("info", "Already signed out.");
604
+ return;
605
+ }
606
+ throw new Error("Missing site URL. Provide --site-url or BISYNC_SITE_URL.");
607
+ }
608
+ if (!config?.token) {
609
+ await writeConfig({ siteUrl, token: "", clientId: CLIENT_ID });
610
+ log("info", "Already signed out.");
611
+ return;
612
+ }
613
+ const signOutResult = await signOutRemote(siteUrl, config.token);
614
+ await writeConfig({ siteUrl, token: "", clientId: CLIENT_ID });
615
+ if (!signOutResult.ok) {
616
+ log("warn", `Remote sign-out failed: ${signOutResult.error ?? "unknown error"}. Local credentials cleared.`);
617
+ return;
618
+ }
619
+ log("info", "Signed out.");
620
+ };
621
+ var runSessionList = async () => {
622
+ log("debug", `runSessionList start`, { config: CONFIG_PATH });
315
623
  const config = await readConfig();
316
624
  const siteUrl = process.env.BISYNC_SITE_URL ?? config.siteUrl;
317
- log("debug", `runVerify siteUrl=${siteUrl}`);
625
+ log("debug", `runSessionList`, { siteUrl });
318
626
  const response = await fetch(`${siteUrl}/api/list-sessions`, {
319
627
  method: "GET",
320
628
  headers: {
@@ -323,20 +631,21 @@ var runVerify = async () => {
323
631
  });
324
632
  if (!response.ok) {
325
633
  const errorText = await response.text();
326
- log("debug", `runVerify failed status=${response.status}`);
327
- throw new Error(`Verify failed: ${response.status} ${errorText}`);
634
+ log("debug", `runSessionList failed`, { status: response.status });
635
+ throw new Error(`Session list failed: ${response.status} ${errorText}`);
328
636
  }
637
+ log("debug", "runSessionList ok", { status: response.status });
329
638
  const data = await response.json();
330
639
  if (data.sessions.length === 0) {
331
640
  log("info", "No sessions found.");
332
641
  return;
333
642
  }
334
643
  for (const session of data.sessions) {
335
- log("info", session.externalId);
644
+ console.log(session.externalId);
336
645
  }
337
646
  };
338
647
  var runHook = async (hookName) => {
339
- log("debug", `runHook start hook=${hookName}`);
648
+ log("debug", `runHook start`, { hook: hookName });
340
649
  const stdinRaw = await new Promise((resolve2, reject) => {
341
650
  let data = "";
342
651
  process.stdin.setEncoding("utf8");
@@ -346,24 +655,19 @@ var runHook = async (hookName) => {
346
655
  process.stdin.on("end", () => resolve2(data));
347
656
  process.stdin.on("error", (error) => reject(error));
348
657
  });
349
- await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} stdin=${stdinRaw.trim()}
350
- `);
351
- if (!stdinRaw.trim()) {
352
- await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=empty-stdin
353
- `);
658
+ log("debug", `runHook`, { hook: hookName, stdin: stdinRaw.trim() });
659
+ if (!stdinRaw.trim())
354
660
  return;
355
- }
356
661
  const stdinPayload = JSON.parse(stdinRaw);
357
662
  const sessionId = stdinPayload.session_id;
358
663
  if (!sessionId) {
359
- await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=missing-session-id
360
- `);
664
+ log("debug", `runHook`, { hook: hookName, error: "missing-session-id" });
361
665
  return;
362
666
  }
363
667
  const config = await readConfig();
364
668
  const siteUrl = process.env.BISYNC_SITE_URL ?? config.siteUrl;
365
669
  const token = config.token;
366
- log("debug", `runHook siteUrl=${siteUrl}`);
670
+ log("debug", `runHook`, { siteUrl });
367
671
  let sessionFile = null;
368
672
  if (typeof stdinPayload.transcript_path === "string") {
369
673
  const resolvedPath = resolve(stdinPayload.transcript_path);
@@ -375,57 +679,102 @@ var runHook = async (hookName) => {
375
679
  sessionFile = await findSessionFile(sessionId);
376
680
  }
377
681
  if (!sessionFile) {
378
- log("debug", `runHook session file missing sessionId=${sessionId}`);
379
- await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=session-file-not-found session_id=${sessionId}
380
- `);
381
- return;
682
+ log("debug", `runHook`, { sessionId, error: "session-file-not-found" });
382
683
  }
383
- log("debug", `runHook sessionFile=${sessionFile}`);
384
- const raw = await Bun.file(sessionFile).text();
385
- if (!raw.trim()) {
386
- log("debug", `runHook session file empty sessionId=${sessionId}`);
387
- await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=empty-log session_id=${sessionId}
388
- `);
684
+ let sessionDir = null;
685
+ if (sessionFile) {
686
+ sessionDir = await resolveSessionDir(sessionFile);
687
+ }
688
+ if (!sessionDir) {
689
+ sessionDir = await findSessionDir(sessionId);
690
+ }
691
+ if (!sessionFile && !sessionDir) {
389
692
  return;
390
693
  }
391
- try {
392
- await uploadLogs(siteUrl, token, sessionId, raw);
393
- log("debug", `runHook upload ok sessionId=${sessionId}`);
394
- } catch (error) {
395
- log("debug", `runHook upload failed sessionId=${sessionId} message=${error instanceof Error ? error.message : String(error)}`);
396
- await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=upload-failed session_id=${sessionId} message=${error instanceof Error ? error.message : String(error)}
397
- `);
694
+ if (sessionFile) {
695
+ log("debug", `runHook`, { sessionFile });
696
+ const raw = await Bun.file(sessionFile).text();
697
+ if (!raw.trim()) {
698
+ log("debug", `runHook`, { sessionId, error: "empty-log" });
699
+ } else {
700
+ try {
701
+ await uploadLogs(siteUrl, token, sessionId, raw);
702
+ log("debug", `runHook`, { sessionId, error: "upload-ok" });
703
+ } catch (error) {
704
+ log("debug", `runHook`, {
705
+ sessionId,
706
+ error: error instanceof Error ? error.message : String(error)
707
+ });
708
+ }
709
+ }
710
+ }
711
+ if (sessionDir && hookName === "SessionEnd") {
712
+ log("debug", `runHook`, { sessionDir });
713
+ try {
714
+ const stateFiles = await buildStateFiles(sessionDir, siteUrl, token);
715
+ if (stateFiles.length === 0) {
716
+ log("debug", `runHook`, { sessionId, error: "no-state-files" });
717
+ return;
718
+ }
719
+ await uploadStateFiles(siteUrl, token, sessionId, stateFiles);
720
+ log("debug", `runHook`, { sessionId, error: "state-upload-ok" });
721
+ } catch (error) {
722
+ log("debug", `runHook`, {
723
+ sessionId,
724
+ error: error instanceof Error ? error.message : String(error)
725
+ });
726
+ }
398
727
  }
399
728
  };
400
729
  var main = async () => {
401
- if (process.argv.includes("--version")) {
730
+ const { values, positionals } = parseArgs();
731
+ if (values.help) {
732
+ printUsage();
733
+ process.exit(0);
734
+ }
735
+ if (values.version) {
402
736
  console.log(`\u25B6\uFE0E ${version} (Bun ${Bun.version})`);
403
737
  process.exit(0);
404
738
  }
405
- if (process.argv.includes("--verbose")) {
739
+ if (values.verbose) {
406
740
  LOG_LEVEL = "debug";
407
741
  }
408
- const rawArgs = process.argv.slice(2);
409
- const [command, ...args] = rawArgs;
410
- log("debug", `argv=${JSON.stringify(rawArgs)}`);
742
+ const [command, subcommand] = positionals;
743
+ log("debug", "argv", { argv: JSON.stringify(process.argv.slice(2)) });
411
744
  if (!command) {
412
- usage();
745
+ printUsage();
413
746
  process.exit(1);
414
747
  }
415
748
  try {
416
749
  if (command === "setup") {
417
- await runSetup(args);
418
- return;
750
+ await runSetup(values);
751
+ process.exit(0);
419
752
  }
420
- if (command === "verify") {
421
- await runVerify();
422
- return;
753
+ if (command === "auth") {
754
+ if (subcommand === "login") {
755
+ await runAuthLogin(values);
756
+ process.exit(0);
757
+ }
758
+ if (subcommand === "logout") {
759
+ await runAuthLogout(values);
760
+ process.exit(0);
761
+ }
762
+ printUsage();
763
+ process.exit(1);
764
+ }
765
+ if (command === "session") {
766
+ if (subcommand === "list") {
767
+ await runSessionList();
768
+ process.exit(0);
769
+ }
770
+ printUsage();
771
+ process.exit(1);
423
772
  }
424
773
  if (command === "hook") {
425
- await runHook(args[0] ?? "unknown");
426
- return;
774
+ await runHook(subcommand ?? "unknown");
775
+ process.exit(0);
427
776
  }
428
- usage();
777
+ printUsage();
429
778
  process.exit(1);
430
779
  } catch (error) {
431
780
  log("error", error instanceof Error ? error.message : String(error));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bisync-cli",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "bin": {
5
5
  "bisync": "dist/bisync.js"
6
6
  },