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.
- package/dist/bisync.js +107 -18
- package/package.json +1 -1
- 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.
|
|
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
|
|
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
|
|
78
|
+
return {
|
|
79
|
+
siteUrl: viteConvexUrl.replace(".convex.cloud", ".convex.site"),
|
|
80
|
+
source: "vite"
|
|
81
|
+
};
|
|
51
82
|
}
|
|
52
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
340
|
+
log("info", "No sessions found.");
|
|
264
341
|
return;
|
|
265
342
|
}
|
|
266
343
|
for (const session of data.sessions) {
|
|
267
|
-
|
|
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
|
-
|
|
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
|
-
|
|
440
|
+
log("error", error instanceof Error ? error.message : String(error));
|
|
352
441
|
process.exit(1);
|
|
353
442
|
}
|
|
354
443
|
};
|
package/package.json
CHANGED
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
|
|
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
|
|
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
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
440
|
+
log("info", "No sessions found.");
|
|
342
441
|
return;
|
|
343
442
|
}
|
|
344
443
|
|
|
345
444
|
for (const session of data.sessions) {
|
|
346
|
-
|
|
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
|
-
|
|
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
|
-
|
|
563
|
+
log("error", error instanceof Error ? error.message : String(error));
|
|
448
564
|
process.exit(1);
|
|
449
565
|
}
|
|
450
566
|
};
|