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