bisync-cli 0.0.7 → 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 -100
  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" };
@@ -73,16 +130,6 @@ var resolveSiteUrl = (args) => {
73
130
  if (envValue) {
74
131
  return { siteUrl: envValue, source: "env" };
75
132
  }
76
- const viteConvexUrl = process.env.VITE_CONVEX_URL;
77
- if (viteConvexUrl?.endsWith(".convex.cloud")) {
78
- return {
79
- siteUrl: viteConvexUrl.replace(".convex.cloud", ".convex.site"),
80
- source: "vite"
81
- };
82
- }
83
- if (viteConvexUrl) {
84
- return { siteUrl: viteConvexUrl, source: "vite" };
85
- }
86
133
  return { siteUrl: null, source: "missing" };
87
134
  };
88
135
  var openBrowser = async (url) => {
@@ -93,8 +140,21 @@ var openBrowser = async (url) => {
93
140
  await Bun.$`open ${url}`;
94
141
  } catch {}
95
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
+ };
96
156
  var deviceAuthFlow = async (siteUrl) => {
97
- log("debug", `device auth start siteUrl=${siteUrl}`);
157
+ log("debug", `device auth start`, { siteUrl });
98
158
  const codeResponse = await fetch(`${siteUrl}/api/auth/device/code`, {
99
159
  method: "POST",
100
160
  headers: { "Content-Type": "application/json" },
@@ -102,20 +162,20 @@ var deviceAuthFlow = async (siteUrl) => {
102
162
  });
103
163
  if (!codeResponse.ok) {
104
164
  const errorText = await codeResponse.text();
105
- log("debug", `device code request failed status=${codeResponse.status}`);
165
+ log("debug", `device code request failed`, { status: codeResponse.status });
106
166
  throw new Error(`Device code request failed: ${codeResponse.status} ${errorText}`);
107
167
  }
108
- log("debug", `device code request ok status=${codeResponse.status}`);
168
+ log("debug", `device code request ok`, { status: codeResponse.status });
109
169
  const codeData = await codeResponse.json();
110
- const verificationUrl = codeData.verification_uri_complete ?? codeData.verification_uri;
111
- log("info", `Authorize this device: ${verificationUrl}`);
112
- 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}`);
113
173
  await openBrowser(verificationUrl);
114
174
  const intervalMs = Math.max(1, codeData.interval ?? 5) * 1000;
115
- log("debug", `device auth polling intervalMs=${intervalMs}`);
175
+ log("debug", `device auth polling`, { intervalMs });
116
176
  let pollDelay = intervalMs;
117
177
  while (true) {
118
- await sleep(pollDelay);
178
+ await setTimeout(pollDelay);
119
179
  const tokenResponse = await fetch(`${siteUrl}/api/auth/device/token`, {
120
180
  method: "POST",
121
181
  headers: { "Content-Type": "application/json" },
@@ -127,7 +187,7 @@ var deviceAuthFlow = async (siteUrl) => {
127
187
  });
128
188
  const tokenData = await tokenResponse.json();
129
189
  if (tokenResponse.ok && tokenData.access_token) {
130
- log("debug", `device auth success status=${tokenResponse.status}`);
190
+ log("debug", `device auth success`, { status: tokenResponse.status });
131
191
  return tokenData.access_token;
132
192
  }
133
193
  if (tokenData.error === "authorization_pending") {
@@ -135,10 +195,13 @@ var deviceAuthFlow = async (siteUrl) => {
135
195
  }
136
196
  if (tokenData.error === "slow_down") {
137
197
  pollDelay += 1000;
138
- log("debug", `device auth slow_down nextDelayMs=${pollDelay}`);
198
+ log("debug", `device auth slow_down`, { nextDelayMs: pollDelay });
139
199
  continue;
140
200
  }
141
- 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
+ });
142
205
  throw new Error(`Device auth failed: ${tokenData.error ?? "unknown"} ${tokenData.error_description ?? ""}`.trim());
143
206
  }
144
207
  };
@@ -159,10 +222,10 @@ var ensureHookEntry = (entries, next) => {
159
222
  return entries;
160
223
  };
161
224
  var mergeSettings = async () => {
162
- log("debug", `mergeSettings path=${CLAUDE_SETTINGS_PATH}`);
225
+ log("debug", `mergeSettings`, { path: CLAUDE_SETTINGS_PATH });
163
226
  const settingsFile = Bun.file(CLAUDE_SETTINGS_PATH);
164
227
  const settingsExists = await settingsFile.exists();
165
- log("debug", `mergeSettings existing=${settingsExists}`);
228
+ log("debug", `mergeSettings`, { existing: settingsExists });
166
229
  let current = {};
167
230
  if (settingsExists) {
168
231
  try {
@@ -201,9 +264,14 @@ var mergeSettings = async () => {
201
264
  log("debug", `mergeSettings wrote settings`);
202
265
  try {
203
266
  const info = await stat(CLAUDE_SETTINGS_PATH);
204
- 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
+ });
205
271
  } catch (error) {
206
- 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
+ });
207
275
  }
208
276
  };
209
277
  var findSessionFile = async (sessionId) => {
@@ -236,10 +304,103 @@ var findSessionFile = async (sessionId) => {
236
304
  await walk(CLAUDE_PROJECTS_DIR);
237
305
  return bestPath;
238
306
  };
239
- var appendDebugLog = async (message) => {
240
- const logFile = Bun.file(DEBUG_LOG_PATH);
241
- const content = await logFile.text();
242
- 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;
243
404
  };
244
405
  var buildLogLines = (raw) => {
245
406
  const lines = raw.split(`
@@ -282,49 +443,154 @@ var uploadLogs = async (siteUrl, token, sessionId, raw) => {
282
443
  }
283
444
  }
284
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
+ };
285
494
  var runSetup = async (args) => {
286
495
  log("debug", `runSetup start`);
287
- const force = args.includes("--force");
288
- log("debug", `runSetup force=${force}`);
496
+ const force = Boolean(args.force);
497
+ log("debug", "runSetup", { force });
289
498
  const configExists = await Bun.file(CONFIG_PATH).exists();
290
- log("debug", `config path=${CONFIG_PATH} exists=${configExists}`);
499
+ log("debug", "config", { path: CONFIG_PATH, exists: configExists });
291
500
  let existingConfig = null;
292
501
  if (configExists) {
293
502
  try {
294
503
  existingConfig = await readConfig();
295
- 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
+ });
296
508
  } catch {
297
509
  log("debug", `config load failed; continuing without existing config`);
298
510
  existingConfig = null;
299
511
  }
300
512
  }
301
513
  const resolution = resolveSiteUrl(args);
302
- 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
+ });
303
518
  const siteUrl = resolution.siteUrl ?? existingConfig?.siteUrl ?? null;
304
519
  if (!resolution.siteUrl && existingConfig?.siteUrl) {
305
- log("debug", `siteUrl fallback to existing config ${existingConfig.siteUrl}`);
520
+ log("debug", "siteUrl fallback to existing config", { siteUrl: existingConfig.siteUrl });
306
521
  }
307
522
  if (!siteUrl) {
308
- log("debug", `runSetup aborted: missing siteUrl`);
523
+ log("debug", "runSetup aborted", { error: "missing-site-url" });
309
524
  throw new Error("Missing site URL. Provide --site-url or BISYNC_SITE_URL.");
310
525
  }
311
526
  if (configExists && existingConfig?.token && !force) {
312
- log("debug", `using existing token; updating Claude settings only`);
527
+ log("debug", "using existing token; updating Claude settings only");
313
528
  await mergeSettings();
314
- log("info", `Updated Claude settings using existing credentials at ${CONFIG_PATH}`);
529
+ log("info", "Updated Claude settings using existing credentials", { path: CONFIG_PATH });
315
530
  return;
316
531
  }
317
532
  const token = await deviceAuthFlow(siteUrl);
318
533
  await writeConfig({ siteUrl, token, clientId: CLIENT_ID });
319
- log("debug", `config written to ${CONFIG_PATH}`);
534
+ log("debug", "config written", { path: CONFIG_PATH });
320
535
  await mergeSettings();
321
- log("info", `Configured Claude settings and saved credentials to ${CONFIG_PATH}`);
536
+ log("info", "Configured Claude settings and saved credentials", { path: CONFIG_PATH });
322
537
  };
323
- var runVerify = async () => {
324
- log("debug", `runVerify start config=${CONFIG_PATH}`);
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.");
588
+ };
589
+ var runSessionList = async () => {
590
+ log("debug", `runSessionList start`, { config: CONFIG_PATH });
325
591
  const config = await readConfig();
326
592
  const siteUrl = process.env.BISYNC_SITE_URL ?? config.siteUrl;
327
- log("debug", `runVerify siteUrl=${siteUrl}`);
593
+ log("debug", `runSessionList`, { siteUrl });
328
594
  const response = await fetch(`${siteUrl}/api/list-sessions`, {
329
595
  method: "GET",
330
596
  headers: {
@@ -333,20 +599,21 @@ var runVerify = async () => {
333
599
  });
334
600
  if (!response.ok) {
335
601
  const errorText = await response.text();
336
- log("debug", `runVerify failed status=${response.status}`);
337
- 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}`);
338
604
  }
605
+ log("debug", "runSessionList ok", { status: response.status });
339
606
  const data = await response.json();
340
607
  if (data.sessions.length === 0) {
341
608
  log("info", "No sessions found.");
342
609
  return;
343
610
  }
344
611
  for (const session of data.sessions) {
345
- log("info", session.externalId);
612
+ console.log(session.externalId);
346
613
  }
347
614
  };
348
615
  var runHook = async (hookName) => {
349
- log("debug", `runHook start hook=${hookName}`);
616
+ log("debug", `runHook start`, { hook: hookName });
350
617
  const stdinRaw = await new Promise((resolve2, reject) => {
351
618
  let data = "";
352
619
  process.stdin.setEncoding("utf8");
@@ -356,24 +623,19 @@ var runHook = async (hookName) => {
356
623
  process.stdin.on("end", () => resolve2(data));
357
624
  process.stdin.on("error", (error) => reject(error));
358
625
  });
359
- await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} stdin=${stdinRaw.trim()}
360
- `);
361
- if (!stdinRaw.trim()) {
362
- await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=empty-stdin
363
- `);
626
+ log("debug", `runHook`, { hook: hookName, stdin: stdinRaw.trim() });
627
+ if (!stdinRaw.trim())
364
628
  return;
365
- }
366
629
  const stdinPayload = JSON.parse(stdinRaw);
367
630
  const sessionId = stdinPayload.session_id;
368
631
  if (!sessionId) {
369
- await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=missing-session-id
370
- `);
632
+ log("debug", `runHook`, { hook: hookName, error: "missing-session-id" });
371
633
  return;
372
634
  }
373
635
  const config = await readConfig();
374
636
  const siteUrl = process.env.BISYNC_SITE_URL ?? config.siteUrl;
375
637
  const token = config.token;
376
- log("debug", `runHook siteUrl=${siteUrl}`);
638
+ log("debug", `runHook`, { siteUrl });
377
639
  let sessionFile = null;
378
640
  if (typeof stdinPayload.transcript_path === "string") {
379
641
  const resolvedPath = resolve(stdinPayload.transcript_path);
@@ -385,57 +647,102 @@ var runHook = async (hookName) => {
385
647
  sessionFile = await findSessionFile(sessionId);
386
648
  }
387
649
  if (!sessionFile) {
388
- log("debug", `runHook session file missing sessionId=${sessionId}`);
389
- await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=session-file-not-found session_id=${sessionId}
390
- `);
391
- return;
650
+ log("debug", `runHook`, { sessionId, error: "session-file-not-found" });
392
651
  }
393
- log("debug", `runHook sessionFile=${sessionFile}`);
394
- const raw = await Bun.file(sessionFile).text();
395
- if (!raw.trim()) {
396
- log("debug", `runHook session file empty sessionId=${sessionId}`);
397
- await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=empty-log session_id=${sessionId}
398
- `);
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) {
399
660
  return;
400
661
  }
401
- try {
402
- await uploadLogs(siteUrl, token, sessionId, raw);
403
- log("debug", `runHook upload ok sessionId=${sessionId}`);
404
- } catch (error) {
405
- log("debug", `runHook upload failed sessionId=${sessionId} message=${error instanceof Error ? error.message : String(error)}`);
406
- await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=upload-failed session_id=${sessionId} message=${error instanceof Error ? error.message : String(error)}
407
- `);
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
+ }
408
695
  }
409
696
  };
410
697
  var main = async () => {
411
- 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) {
412
704
  console.log(`\u25B6\uFE0E ${version} (Bun ${Bun.version})`);
413
705
  process.exit(0);
414
706
  }
415
- if (process.argv.includes("--verbose")) {
707
+ if (values.verbose) {
416
708
  LOG_LEVEL = "debug";
417
709
  }
418
- const rawArgs = process.argv.slice(2);
419
- const [command, ...args] = rawArgs;
420
- log("debug", `argv=${JSON.stringify(rawArgs)}`);
710
+ const [command, subcommand] = positionals;
711
+ log("debug", "argv", { argv: JSON.stringify(process.argv.slice(2)) });
421
712
  if (!command) {
422
- usage();
713
+ printUsage();
423
714
  process.exit(1);
424
715
  }
425
716
  try {
426
717
  if (command === "setup") {
427
- await runSetup(args);
428
- return;
718
+ await runSetup(values);
719
+ process.exit(0);
429
720
  }
430
- if (command === "verify") {
431
- await runVerify();
432
- 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);
433
740
  }
434
741
  if (command === "hook") {
435
- await runHook(args[0] ?? "unknown");
436
- return;
742
+ await runHook(subcommand ?? "unknown");
743
+ process.exit(0);
437
744
  }
438
- usage();
745
+ printUsage();
439
746
  process.exit(1);
440
747
  } catch (error) {
441
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.7",
3
+ "version": "0.0.9",
4
4
  "bin": {
5
5
  "bisync": "dist/bisync.js"
6
6
  },