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