bisync-cli 0.0.8 → 0.0.9

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