bisync-cli 0.0.3 → 0.0.5

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 (3) hide show
  1. package/dist/bisync.js +107 -18
  2. package/package.json +1 -1
  3. package/src/bin.ts +136 -20
package/dist/bisync.js CHANGED
@@ -2,19 +2,19 @@
2
2
  // @bun
3
3
 
4
4
  // src/bin.ts
5
+ import { createWriteStream, mkdirSync } from "fs";
5
6
  import { readdir, stat } from "fs/promises";
6
7
  import { homedir } from "os";
7
8
  import { join, resolve } from "path";
8
9
  // package.json
9
- var version = "0.0.3";
10
+ var version = "0.0.4";
10
11
 
11
12
  // src/bin.ts
12
13
  var CONFIG_DIR = join(homedir(), ".agent-bisync");
13
14
  var CONFIG_PATH = join(CONFIG_DIR, "config.json");
14
15
  var CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
15
16
  var CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
16
- var DEBUG_DIR = join(homedir(), ".bysync");
17
- var DEBUG_LOG_PATH = join(DEBUG_DIR, "debug.log");
17
+ var DEBUG_LOG_PATH = join(CONFIG_DIR, "debug.log");
18
18
  var CLIENT_ID = "bisync-cli";
19
19
  var MAX_LINES_PER_BATCH = 200;
20
20
  var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
@@ -25,8 +25,36 @@ Commands:
25
25
  setup --site-url <url> Configure hooks and authenticate
26
26
  verify Check auth and list session ids
27
27
  hook <HOOK> Handle Claude hook input (stdin JSON)
28
+
29
+ Global options:
30
+ --verbose Enable verbose logging
28
31
  `);
29
32
  };
33
+ var LOG_LEVEL = "info";
34
+ var debugLogStream = null;
35
+ var getDebugLogStream = () => {
36
+ if (debugLogStream)
37
+ return debugLogStream;
38
+ try {
39
+ mkdirSync(CONFIG_DIR, { recursive: true });
40
+ debugLogStream = createWriteStream(DEBUG_LOG_PATH, { flags: "a" });
41
+ debugLogStream.on("error", () => {
42
+ debugLogStream = null;
43
+ });
44
+ return debugLogStream;
45
+ } catch {
46
+ return null;
47
+ }
48
+ };
49
+ var log = (level, message) => {
50
+ const logMessage = `[bisync][${level}] ${message}`;
51
+ getDebugLogStream()?.write(`[${new Date().toISOString()}] ${logMessage}
52
+ `);
53
+ if (level < LOG_LEVEL) {
54
+ return;
55
+ }
56
+ console.log(logMessage);
57
+ };
30
58
  var readConfig = async () => {
31
59
  return await Bun.file(CONFIG_PATH).json();
32
60
  };
@@ -40,16 +68,22 @@ var resolveSiteUrl = (args) => {
40
68
  const argValue = argIndex >= 0 ? args[argIndex + 1] : undefined;
41
69
  const envValue = process.env.BISYNC_SITE_URL;
42
70
  if (argValue) {
43
- return argValue;
71
+ return { siteUrl: argValue, source: "arg" };
44
72
  }
45
73
  if (envValue) {
46
- return envValue;
74
+ return { siteUrl: envValue, source: "env" };
47
75
  }
48
76
  const viteConvexUrl = process.env.VITE_CONVEX_URL;
49
77
  if (viteConvexUrl?.endsWith(".convex.cloud")) {
50
- return viteConvexUrl.replace(".convex.cloud", ".convex.site");
78
+ return {
79
+ siteUrl: viteConvexUrl.replace(".convex.cloud", ".convex.site"),
80
+ source: "vite"
81
+ };
51
82
  }
52
- return viteConvexUrl ?? null;
83
+ if (viteConvexUrl) {
84
+ return { siteUrl: viteConvexUrl, source: "vite" };
85
+ }
86
+ return { siteUrl: null, source: "missing" };
53
87
  };
54
88
  var openBrowser = async (url) => {
55
89
  if (process.platform !== "darwin") {
@@ -60,6 +94,7 @@ var openBrowser = async (url) => {
60
94
  } catch {}
61
95
  };
62
96
  var deviceAuthFlow = async (siteUrl) => {
97
+ log("debug", `device auth start siteUrl=${siteUrl}`);
63
98
  const codeResponse = await fetch(`${siteUrl}/api/auth/device/code`, {
64
99
  method: "POST",
65
100
  headers: { "Content-Type": "application/json" },
@@ -67,14 +102,17 @@ var deviceAuthFlow = async (siteUrl) => {
67
102
  });
68
103
  if (!codeResponse.ok) {
69
104
  const errorText = await codeResponse.text();
105
+ log("debug", `device code request failed status=${codeResponse.status}`);
70
106
  throw new Error(`Device code request failed: ${codeResponse.status} ${errorText}`);
71
107
  }
108
+ log("debug", `device code request ok status=${codeResponse.status}`);
72
109
  const codeData = await codeResponse.json();
73
110
  const verificationUrl = codeData.verification_uri_complete ?? codeData.verification_uri;
74
- console.log(`Authorize this device: ${verificationUrl}`);
75
- console.log(`User code: ${codeData.user_code}`);
111
+ log("info", `Authorize this device: ${verificationUrl}`);
112
+ log("info", `User code: ${codeData.user_code}`);
76
113
  await openBrowser(verificationUrl);
77
114
  const intervalMs = Math.max(1, codeData.interval ?? 5) * 1000;
115
+ log("debug", `device auth polling intervalMs=${intervalMs}`);
78
116
  let pollDelay = intervalMs;
79
117
  while (true) {
80
118
  await sleep(pollDelay);
@@ -89,6 +127,7 @@ var deviceAuthFlow = async (siteUrl) => {
89
127
  });
90
128
  const tokenData = await tokenResponse.json();
91
129
  if (tokenResponse.ok && tokenData.access_token) {
130
+ log("debug", `device auth success status=${tokenResponse.status}`);
92
131
  return tokenData.access_token;
93
132
  }
94
133
  if (tokenData.error === "authorization_pending") {
@@ -96,8 +135,10 @@ var deviceAuthFlow = async (siteUrl) => {
96
135
  }
97
136
  if (tokenData.error === "slow_down") {
98
137
  pollDelay += 1000;
138
+ log("debug", `device auth slow_down nextDelayMs=${pollDelay}`);
99
139
  continue;
100
140
  }
141
+ log("debug", `device auth failed status=${tokenResponse.status} error=${tokenData.error ?? "unknown"}`);
101
142
  throw new Error(`Device auth failed: ${tokenData.error ?? "unknown"} ${tokenData.error_description ?? ""}`.trim());
102
143
  }
103
144
  };
@@ -118,7 +159,20 @@ var ensureHookEntry = (entries, next) => {
118
159
  return entries;
119
160
  };
120
161
  var mergeSettings = async () => {
121
- const current = await Bun.file(CLAUDE_SETTINGS_PATH).json().catch(() => ({}));
162
+ log("debug", `mergeSettings path=${CLAUDE_SETTINGS_PATH}`);
163
+ const settingsFile = Bun.file(CLAUDE_SETTINGS_PATH);
164
+ const settingsExists = await settingsFile.exists();
165
+ log("debug", `mergeSettings existing=${settingsExists}`);
166
+ let current = {};
167
+ if (settingsExists) {
168
+ try {
169
+ current = await settingsFile.json();
170
+ log("debug", `mergeSettings read ok`);
171
+ } catch (error) {
172
+ log("debug", `mergeSettings read failed: ${error instanceof Error ? error.message : String(error)}`);
173
+ current = {};
174
+ }
175
+ }
122
176
  const hooks = current.hooks ?? {};
123
177
  const definitions = [
124
178
  { name: "SessionStart" },
@@ -144,6 +198,13 @@ var mergeSettings = async () => {
144
198
  await Bun.write(CLAUDE_SETTINGS_PATH, JSON.stringify(nextConfig, null, 2), {
145
199
  createPath: true
146
200
  });
201
+ log("debug", `mergeSettings wrote settings`);
202
+ try {
203
+ const info = await stat(CLAUDE_SETTINGS_PATH);
204
+ log("debug", `mergeSettings file size=${info.size} mtime=${new Date(info.mtimeMs).toISOString()}`);
205
+ } catch (error) {
206
+ log("debug", `mergeSettings stat failed: ${error instanceof Error ? error.message : String(error)}`);
207
+ }
147
208
  };
148
209
  var findSessionFile = async (sessionId) => {
149
210
  let bestPath = null;
@@ -178,7 +239,7 @@ var findSessionFile = async (sessionId) => {
178
239
  var appendDebugLog = async (message) => {
179
240
  const logFile = Bun.file(DEBUG_LOG_PATH);
180
241
  const content = await logFile.text();
181
- await logFile.write(content + message);
242
+ await Bun.write(DEBUG_LOG_PATH, content + message, { createPath: true });
182
243
  };
183
244
  var buildLogLines = (raw) => {
184
245
  const lines = raw.split(`
@@ -221,33 +282,48 @@ var uploadLogs = async (siteUrl, token, sessionId, raw) => {
221
282
  }
222
283
  };
223
284
  var runSetup = async (args) => {
285
+ log("debug", `runSetup start`);
224
286
  const force = args.includes("--force");
287
+ log("debug", `runSetup force=${force}`);
225
288
  const configExists = await Bun.file(CONFIG_PATH).exists();
289
+ log("debug", `config path=${CONFIG_PATH} exists=${configExists}`);
226
290
  let existingConfig = null;
227
291
  if (configExists) {
228
292
  try {
229
293
  existingConfig = await readConfig();
294
+ log("debug", `config loaded siteUrl=${existingConfig.siteUrl} token=${existingConfig.token ? "present" : "missing"}`);
230
295
  } catch {
296
+ log("debug", `config load failed; continuing without existing config`);
231
297
  existingConfig = null;
232
298
  }
233
299
  }
234
- const siteUrl = resolveSiteUrl(args) ?? existingConfig?.siteUrl ?? null;
300
+ const resolution = resolveSiteUrl(args);
301
+ log("debug", `siteUrl resolved=${resolution.siteUrl ?? "null"} source=${resolution.source}`);
302
+ const siteUrl = resolution.siteUrl ?? existingConfig?.siteUrl ?? null;
303
+ if (!resolution.siteUrl && existingConfig?.siteUrl) {
304
+ log("debug", `siteUrl fallback to existing config ${existingConfig.siteUrl}`);
305
+ }
235
306
  if (!siteUrl) {
307
+ log("debug", `runSetup aborted: missing siteUrl`);
236
308
  throw new Error("Missing site URL. Provide --site-url or BISYNC_SITE_URL.");
237
309
  }
238
310
  if (configExists && existingConfig?.token && !force) {
311
+ log("debug", `using existing token; updating Claude settings only`);
239
312
  await mergeSettings();
240
- console.log(`Updated Claude settings using existing credentials at ${CONFIG_PATH}`);
313
+ log("info", `Updated Claude settings using existing credentials at ${CONFIG_PATH}`);
241
314
  return;
242
315
  }
243
316
  const token = await deviceAuthFlow(siteUrl);
244
317
  await writeConfig({ siteUrl, token, clientId: CLIENT_ID });
318
+ log("debug", `config written to ${CONFIG_PATH}`);
245
319
  await mergeSettings();
246
- console.log(`Configured Claude settings and saved credentials to ${CONFIG_PATH}`);
320
+ log("info", `Configured Claude settings and saved credentials to ${CONFIG_PATH}`);
247
321
  };
248
322
  var runVerify = async () => {
323
+ log("debug", `runVerify start config=${CONFIG_PATH}`);
249
324
  const config = await readConfig();
250
325
  const siteUrl = process.env.BISYNC_SITE_URL ?? config.siteUrl;
326
+ log("debug", `runVerify siteUrl=${siteUrl}`);
251
327
  const response = await fetch(`${siteUrl}/api/list-sessions`, {
252
328
  method: "GET",
253
329
  headers: {
@@ -256,18 +332,20 @@ var runVerify = async () => {
256
332
  });
257
333
  if (!response.ok) {
258
334
  const errorText = await response.text();
335
+ log("debug", `runVerify failed status=${response.status}`);
259
336
  throw new Error(`Verify failed: ${response.status} ${errorText}`);
260
337
  }
261
338
  const data = await response.json();
262
339
  if (data.sessions.length === 0) {
263
- console.log("No sessions found.");
340
+ log("info", "No sessions found.");
264
341
  return;
265
342
  }
266
343
  for (const session of data.sessions) {
267
- console.log(session.externalId);
344
+ log("info", session.externalId);
268
345
  }
269
346
  };
270
347
  var runHook = async (hookName) => {
348
+ log("debug", `runHook start hook=${hookName}`);
271
349
  const stdinRaw = await new Promise((resolve2, reject) => {
272
350
  let data = "";
273
351
  process.stdin.setEncoding("utf8");
@@ -294,6 +372,7 @@ var runHook = async (hookName) => {
294
372
  const config = await readConfig();
295
373
  const siteUrl = process.env.BISYNC_SITE_URL ?? config.siteUrl;
296
374
  const token = config.token;
375
+ log("debug", `runHook siteUrl=${siteUrl}`);
297
376
  let sessionFile = null;
298
377
  if (typeof stdinPayload.transcript_path === "string") {
299
378
  const resolvedPath = resolve(stdinPayload.transcript_path);
@@ -305,19 +384,24 @@ var runHook = async (hookName) => {
305
384
  sessionFile = await findSessionFile(sessionId);
306
385
  }
307
386
  if (!sessionFile) {
387
+ log("debug", `runHook session file missing sessionId=${sessionId}`);
308
388
  await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=session-file-not-found session_id=${sessionId}
309
389
  `);
310
390
  return;
311
391
  }
392
+ log("debug", `runHook sessionFile=${sessionFile}`);
312
393
  const raw = await Bun.file(sessionFile).text();
313
394
  if (!raw.trim()) {
395
+ log("debug", `runHook session file empty sessionId=${sessionId}`);
314
396
  await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=empty-log session_id=${sessionId}
315
397
  `);
316
398
  return;
317
399
  }
318
400
  try {
319
401
  await uploadLogs(siteUrl, token, sessionId, raw);
402
+ log("debug", `runHook upload ok sessionId=${sessionId}`);
320
403
  } catch (error) {
404
+ log("debug", `runHook upload failed sessionId=${sessionId} message=${error instanceof Error ? error.message : String(error)}`);
321
405
  await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=upload-failed session_id=${sessionId} message=${error instanceof Error ? error.message : String(error)}
322
406
  `);
323
407
  }
@@ -327,7 +411,12 @@ var main = async () => {
327
411
  console.log(`\u25B6\uFE0E ${version} (Bun ${Bun.version})`);
328
412
  process.exit(0);
329
413
  }
330
- const [command, ...args] = process.argv.slice(2);
414
+ if (process.argv.includes("--verbose")) {
415
+ LOG_LEVEL = "debug";
416
+ }
417
+ const rawArgs = process.argv.slice(2);
418
+ const [command, ...args] = rawArgs;
419
+ log("debug", `argv=${JSON.stringify(rawArgs)}`);
331
420
  if (!command) {
332
421
  usage();
333
422
  process.exit(1);
@@ -348,7 +437,7 @@ var main = async () => {
348
437
  usage();
349
438
  process.exit(1);
350
439
  } catch (error) {
351
- console.error(error instanceof Error ? error.message : error);
440
+ log("error", error instanceof Error ? error.message : String(error));
352
441
  process.exit(1);
353
442
  }
354
443
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bisync-cli",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "bin": {
5
5
  "bisync": "dist/bisync.js"
6
6
  },
package/src/bin.ts CHANGED
@@ -2,7 +2,9 @@
2
2
  /// <reference types="bun" />
3
3
 
4
4
  import type { Dirent } from "node:fs";
5
+ import type { WriteStream } from "node:fs";
5
6
 
7
+ import { createWriteStream, mkdirSync } from "node:fs";
6
8
  import { readdir, stat } from "node:fs/promises";
7
9
  import { homedir } from "node:os";
8
10
  import { join, resolve } from "node:path";
@@ -13,8 +15,7 @@ const CONFIG_DIR = join(homedir(), ".agent-bisync");
13
15
  const CONFIG_PATH = join(CONFIG_DIR, "config.json");
14
16
  const CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
15
17
  const CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
16
- const DEBUG_DIR = join(homedir(), ".bysync");
17
- const DEBUG_LOG_PATH = join(DEBUG_DIR, "debug.log");
18
+ const DEBUG_LOG_PATH = join(CONFIG_DIR, "debug.log");
18
19
  const CLIENT_ID = "bisync-cli";
19
20
  const MAX_LINES_PER_BATCH = 200;
20
21
 
@@ -24,6 +25,11 @@ type Config = {
24
25
  clientId: string;
25
26
  };
26
27
 
28
+ type SiteUrlResolution = {
29
+ siteUrl: string | null;
30
+ source: "arg" | "env" | "vite" | "missing";
31
+ };
32
+
27
33
  const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
28
34
 
29
35
  const usage = () => {
@@ -33,9 +39,39 @@ Commands:
33
39
  setup --site-url <url> Configure hooks and authenticate
34
40
  verify Check auth and list session ids
35
41
  hook <HOOK> Handle Claude hook input (stdin JSON)
42
+
43
+ Global options:
44
+ --verbose Enable verbose logging
36
45
  `);
37
46
  };
38
47
 
48
+ let LOG_LEVEL = "info";
49
+ let debugLogStream: WriteStream | null = null;
50
+
51
+ const getDebugLogStream = (): WriteStream | null => {
52
+ if (debugLogStream) return debugLogStream;
53
+ try {
54
+ mkdirSync(CONFIG_DIR, { recursive: true });
55
+ debugLogStream = createWriteStream(DEBUG_LOG_PATH, { flags: "a" });
56
+ debugLogStream.on("error", () => {
57
+ // Silently discard write errors to avoid breaking the CLI.
58
+ debugLogStream = null;
59
+ });
60
+ return debugLogStream;
61
+ } catch {
62
+ return null;
63
+ }
64
+ };
65
+
66
+ const log = (level: "debug" | "info" | "warn" | "error", message: string) => {
67
+ const logMessage = `[bisync][${level}] ${message}`;
68
+ getDebugLogStream()?.write(`[${new Date().toISOString()}] ${logMessage}\n`);
69
+ if (level < LOG_LEVEL) {
70
+ return;
71
+ }
72
+ console.log(logMessage);
73
+ };
74
+
39
75
  const readConfig = async (): Promise<Config> => {
40
76
  return await Bun.file(CONFIG_PATH).json();
41
77
  };
@@ -46,21 +82,27 @@ const writeConfig = async (config: Config) => {
46
82
  });
47
83
  };
48
84
 
49
- const resolveSiteUrl = (args: string[]) => {
85
+ const resolveSiteUrl = (args: string[]): SiteUrlResolution => {
50
86
  const argIndex = args.findIndex((value) => value === "--site-url" || value === "--siteUrl");
51
87
  const argValue = argIndex >= 0 ? args[argIndex + 1] : undefined;
52
88
  const envValue = process.env.BISYNC_SITE_URL;
53
89
  if (argValue) {
54
- return argValue;
90
+ return { siteUrl: argValue, source: "arg" };
55
91
  }
56
92
  if (envValue) {
57
- return envValue;
93
+ return { siteUrl: envValue, source: "env" };
58
94
  }
59
95
  const viteConvexUrl = process.env.VITE_CONVEX_URL;
60
96
  if (viteConvexUrl?.endsWith(".convex.cloud")) {
61
- return viteConvexUrl.replace(".convex.cloud", ".convex.site");
97
+ return {
98
+ siteUrl: viteConvexUrl.replace(".convex.cloud", ".convex.site"),
99
+ source: "vite",
100
+ };
101
+ }
102
+ if (viteConvexUrl) {
103
+ return { siteUrl: viteConvexUrl, source: "vite" };
62
104
  }
63
- return viteConvexUrl ?? null;
105
+ return { siteUrl: null, source: "missing" };
64
106
  };
65
107
 
66
108
  const openBrowser = async (url: string) => {
@@ -75,6 +117,7 @@ const openBrowser = async (url: string) => {
75
117
  };
76
118
 
77
119
  const deviceAuthFlow = async (siteUrl: string) => {
120
+ log("debug", `device auth start siteUrl=${siteUrl}`);
78
121
  const codeResponse = await fetch(`${siteUrl}/api/auth/device/code`, {
79
122
  method: "POST",
80
123
  headers: { "Content-Type": "application/json" },
@@ -83,9 +126,12 @@ const deviceAuthFlow = async (siteUrl: string) => {
83
126
 
84
127
  if (!codeResponse.ok) {
85
128
  const errorText = await codeResponse.text();
129
+ log("debug", `device code request failed status=${codeResponse.status}`);
86
130
  throw new Error(`Device code request failed: ${codeResponse.status} ${errorText}`);
87
131
  }
88
132
 
133
+ log("debug", `device code request ok status=${codeResponse.status}`);
134
+
89
135
  const codeData: {
90
136
  device_code: string;
91
137
  user_code: string;
@@ -95,11 +141,12 @@ const deviceAuthFlow = async (siteUrl: string) => {
95
141
  } = await codeResponse.json();
96
142
 
97
143
  const verificationUrl = codeData.verification_uri_complete ?? codeData.verification_uri;
98
- console.log(`Authorize this device: ${verificationUrl}`);
99
- console.log(`User code: ${codeData.user_code}`);
144
+ log("info", `Authorize this device: ${verificationUrl}`);
145
+ log("info", `User code: ${codeData.user_code}`);
100
146
  await openBrowser(verificationUrl);
101
147
 
102
148
  const intervalMs = Math.max(1, codeData.interval ?? 5) * 1000;
149
+ log("debug", `device auth polling intervalMs=${intervalMs}`);
103
150
  let pollDelay = intervalMs;
104
151
 
105
152
  while (true) {
@@ -121,6 +168,7 @@ const deviceAuthFlow = async (siteUrl: string) => {
121
168
  } = await tokenResponse.json();
122
169
 
123
170
  if (tokenResponse.ok && tokenData.access_token) {
171
+ log("debug", `device auth success status=${tokenResponse.status}`);
124
172
  return tokenData.access_token;
125
173
  }
126
174
 
@@ -129,9 +177,14 @@ const deviceAuthFlow = async (siteUrl: string) => {
129
177
  }
130
178
  if (tokenData.error === "slow_down") {
131
179
  pollDelay += 1000;
180
+ log("debug", `device auth slow_down nextDelayMs=${pollDelay}`);
132
181
  continue;
133
182
  }
134
183
 
184
+ log(
185
+ "debug",
186
+ `device auth failed status=${tokenResponse.status} error=${tokenData.error ?? "unknown"}`,
187
+ );
135
188
  throw new Error(
136
189
  `Device auth failed: ${tokenData.error ?? "unknown"} ${
137
190
  tokenData.error_description ?? ""
@@ -172,9 +225,23 @@ const ensureHookEntry = (entries: ClaudeHookEntry[], next: ClaudeHookEntry) => {
172
225
  };
173
226
 
174
227
  const mergeSettings = async () => {
175
- const current: ClaudeSettings = await Bun.file(CLAUDE_SETTINGS_PATH)
176
- .json()
177
- .catch(() => ({}));
228
+ log("debug", `mergeSettings path=${CLAUDE_SETTINGS_PATH}`);
229
+ const settingsFile = Bun.file(CLAUDE_SETTINGS_PATH);
230
+ const settingsExists = await settingsFile.exists();
231
+ log("debug", `mergeSettings existing=${settingsExists}`);
232
+ let current: ClaudeSettings = {};
233
+ if (settingsExists) {
234
+ try {
235
+ current = await settingsFile.json();
236
+ log("debug", `mergeSettings read ok`);
237
+ } catch (error) {
238
+ log(
239
+ "debug",
240
+ `mergeSettings read failed: ${error instanceof Error ? error.message : String(error)}`,
241
+ );
242
+ current = {};
243
+ }
244
+ }
178
245
 
179
246
  const hooks = current.hooks ?? {};
180
247
  const definitions: Array<{ name: string; matcher?: string }> = [
@@ -203,6 +270,19 @@ const mergeSettings = async () => {
203
270
  await Bun.write(CLAUDE_SETTINGS_PATH, JSON.stringify(nextConfig, null, 2), {
204
271
  createPath: true,
205
272
  });
273
+ log("debug", `mergeSettings wrote settings`);
274
+ try {
275
+ const info = await stat(CLAUDE_SETTINGS_PATH);
276
+ log(
277
+ "debug",
278
+ `mergeSettings file size=${info.size} mtime=${new Date(info.mtimeMs).toISOString()}`,
279
+ );
280
+ } catch (error) {
281
+ log(
282
+ "debug",
283
+ `mergeSettings stat failed: ${error instanceof Error ? error.message : String(error)}`,
284
+ );
285
+ }
206
286
  };
207
287
 
208
288
  const findSessionFile = async (sessionId: string) => {
@@ -242,7 +322,7 @@ const findSessionFile = async (sessionId: string) => {
242
322
  const appendDebugLog = async (message: string) => {
243
323
  const logFile = Bun.file(DEBUG_LOG_PATH);
244
324
  const content = await logFile.text();
245
- await logFile.write(content + message);
325
+ await Bun.write(DEBUG_LOG_PATH, content + message, { createPath: true });
246
326
  };
247
327
 
248
328
  const buildLogLines = (raw: string) => {
@@ -290,37 +370,55 @@ const uploadLogs = async (siteUrl: string, token: string, sessionId: string, raw
290
370
  };
291
371
 
292
372
  const runSetup = async (args: string[]) => {
373
+ log("debug", `runSetup start`);
293
374
  const force = args.includes("--force");
375
+ log("debug", `runSetup force=${force}`);
294
376
  const configExists = await Bun.file(CONFIG_PATH).exists();
377
+ log("debug", `config path=${CONFIG_PATH} exists=${configExists}`);
295
378
  let existingConfig: Config | null = null;
296
379
  if (configExists) {
297
380
  try {
298
381
  existingConfig = await readConfig();
382
+ log(
383
+ "debug",
384
+ `config loaded siteUrl=${existingConfig.siteUrl} token=${existingConfig.token ? "present" : "missing"}`,
385
+ );
299
386
  } catch {
387
+ log("debug", `config load failed; continuing without existing config`);
300
388
  existingConfig = null;
301
389
  }
302
390
  }
303
391
 
304
- const siteUrl = resolveSiteUrl(args) ?? existingConfig?.siteUrl ?? null;
392
+ const resolution = resolveSiteUrl(args);
393
+ log("debug", `siteUrl resolved=${resolution.siteUrl ?? "null"} source=${resolution.source}`);
394
+ const siteUrl = resolution.siteUrl ?? existingConfig?.siteUrl ?? null;
395
+ if (!resolution.siteUrl && existingConfig?.siteUrl) {
396
+ log("debug", `siteUrl fallback to existing config ${existingConfig.siteUrl}`);
397
+ }
305
398
  if (!siteUrl) {
399
+ log("debug", `runSetup aborted: missing siteUrl`);
306
400
  throw new Error("Missing site URL. Provide --site-url or BISYNC_SITE_URL.");
307
401
  }
308
402
 
309
403
  if (configExists && existingConfig?.token && !force) {
404
+ log("debug", `using existing token; updating Claude settings only`);
310
405
  await mergeSettings();
311
- console.log(`Updated Claude settings using existing credentials at ${CONFIG_PATH}`);
406
+ log("info", `Updated Claude settings using existing credentials at ${CONFIG_PATH}`);
312
407
  return;
313
408
  }
314
409
 
315
410
  const token = await deviceAuthFlow(siteUrl);
316
411
  await writeConfig({ siteUrl, token, clientId: CLIENT_ID });
412
+ log("debug", `config written to ${CONFIG_PATH}`);
317
413
  await mergeSettings();
318
- console.log(`Configured Claude settings and saved credentials to ${CONFIG_PATH}`);
414
+ log("info", `Configured Claude settings and saved credentials to ${CONFIG_PATH}`);
319
415
  };
320
416
 
321
417
  const runVerify = async () => {
418
+ log("debug", `runVerify start config=${CONFIG_PATH}`);
322
419
  const config = await readConfig();
323
420
  const siteUrl = process.env.BISYNC_SITE_URL ?? config.siteUrl;
421
+ log("debug", `runVerify siteUrl=${siteUrl}`);
324
422
  const response = await fetch(`${siteUrl}/api/list-sessions`, {
325
423
  method: "GET",
326
424
  headers: {
@@ -330,6 +428,7 @@ const runVerify = async () => {
330
428
 
331
429
  if (!response.ok) {
332
430
  const errorText = await response.text();
431
+ log("debug", `runVerify failed status=${response.status}`);
333
432
  throw new Error(`Verify failed: ${response.status} ${errorText}`);
334
433
  }
335
434
 
@@ -338,16 +437,17 @@ const runVerify = async () => {
338
437
  };
339
438
 
340
439
  if (data.sessions.length === 0) {
341
- console.log("No sessions found.");
440
+ log("info", "No sessions found.");
342
441
  return;
343
442
  }
344
443
 
345
444
  for (const session of data.sessions) {
346
- console.log(session.externalId);
445
+ log("info", session.externalId);
347
446
  }
348
447
  };
349
448
 
350
449
  const runHook = async (hookName: string) => {
450
+ log("debug", `runHook start hook=${hookName}`);
351
451
  const stdinRaw = await new Promise<string>((resolve, reject) => {
352
452
  let data = "";
353
453
  process.stdin.setEncoding("utf8");
@@ -380,6 +480,7 @@ const runHook = async (hookName: string) => {
380
480
  const config = await readConfig();
381
481
  const siteUrl = process.env.BISYNC_SITE_URL ?? config.siteUrl;
382
482
  const token = config.token;
483
+ log("debug", `runHook siteUrl=${siteUrl}`);
383
484
 
384
485
  let sessionFile: string | null = null;
385
486
  if (typeof stdinPayload.transcript_path === "string") {
@@ -392,14 +493,18 @@ const runHook = async (hookName: string) => {
392
493
  sessionFile = await findSessionFile(sessionId);
393
494
  }
394
495
  if (!sessionFile) {
496
+ log("debug", `runHook session file missing sessionId=${sessionId}`);
395
497
  await appendDebugLog(
396
498
  `[${new Date().toISOString()}] hook=${hookName} error=session-file-not-found session_id=${sessionId}\n`,
397
499
  );
398
500
  return;
399
501
  }
400
502
 
503
+ log("debug", `runHook sessionFile=${sessionFile}`);
504
+
401
505
  const raw = await Bun.file(sessionFile).text();
402
506
  if (!raw.trim()) {
507
+ log("debug", `runHook session file empty sessionId=${sessionId}`);
403
508
  await appendDebugLog(
404
509
  `[${new Date().toISOString()}] hook=${hookName} error=empty-log session_id=${sessionId}\n`,
405
510
  );
@@ -408,7 +513,12 @@ const runHook = async (hookName: string) => {
408
513
 
409
514
  try {
410
515
  await uploadLogs(siteUrl, token, sessionId, raw);
516
+ log("debug", `runHook upload ok sessionId=${sessionId}`);
411
517
  } catch (error) {
518
+ log(
519
+ "debug",
520
+ `runHook upload failed sessionId=${sessionId} message=${error instanceof Error ? error.message : String(error)}`,
521
+ );
412
522
  await appendDebugLog(
413
523
  `[${new Date().toISOString()}] hook=${hookName} error=upload-failed session_id=${sessionId} message=${error instanceof Error ? error.message : String(error)}\n`,
414
524
  );
@@ -421,7 +531,13 @@ const main = async () => {
421
531
  process.exit(0);
422
532
  }
423
533
 
424
- const [command, ...args] = process.argv.slice(2);
534
+ if (process.argv.includes("--verbose")) {
535
+ LOG_LEVEL = "debug";
536
+ }
537
+
538
+ const rawArgs = process.argv.slice(2);
539
+ const [command, ...args] = rawArgs;
540
+ log("debug", `argv=${JSON.stringify(rawArgs)}`);
425
541
  if (!command) {
426
542
  usage();
427
543
  process.exit(1);
@@ -444,7 +560,7 @@ const main = async () => {
444
560
  usage();
445
561
  process.exit(1);
446
562
  } catch (error) {
447
- console.error(error instanceof Error ? error.message : error);
563
+ log("error", error instanceof Error ? error.message : String(error));
448
564
  process.exit(1);
449
565
  }
450
566
  };