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