bisync-cli 0.0.6 → 0.0.8
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 +1 -11
- package/package.json +4 -2
- package/src/bin.ts +0 -571
package/dist/bisync.js
CHANGED
|
@@ -7,7 +7,7 @@ import { readdir, stat } from "fs/promises";
|
|
|
7
7
|
import { homedir } from "os";
|
|
8
8
|
import { join, resolve } from "path";
|
|
9
9
|
// package.json
|
|
10
|
-
var version = "0.0.
|
|
10
|
+
var version = "0.0.7";
|
|
11
11
|
|
|
12
12
|
// src/bin.ts
|
|
13
13
|
var CONFIG_DIR = join(homedir(), ".agent-bisync");
|
|
@@ -73,16 +73,6 @@ var resolveSiteUrl = (args) => {
|
|
|
73
73
|
if (envValue) {
|
|
74
74
|
return { siteUrl: envValue, source: "env" };
|
|
75
75
|
}
|
|
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
76
|
return { siteUrl: null, source: "missing" };
|
|
87
77
|
};
|
|
88
78
|
var openBrowser = async (url) => {
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bisync-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"bin": {
|
|
5
5
|
"bisync": "dist/bisync.js"
|
|
6
6
|
},
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
7
10
|
"type": "module",
|
|
8
11
|
"scripts": {
|
|
9
|
-
"dev": "bun run dev",
|
|
10
12
|
"build": "bun build src/bin.ts --outfile dist/bisync.js"
|
|
11
13
|
},
|
|
12
14
|
"devDependencies": {
|
package/src/bin.ts
DELETED
|
@@ -1,571 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/// <reference types="bun" />
|
|
3
|
-
|
|
4
|
-
import type { Dirent } from "node:fs";
|
|
5
|
-
import type { WriteStream } from "node:fs";
|
|
6
|
-
|
|
7
|
-
import { createWriteStream, mkdirSync } from "node:fs";
|
|
8
|
-
import { readdir, stat } from "node:fs/promises";
|
|
9
|
-
import { homedir } from "node:os";
|
|
10
|
-
import { join, resolve } from "node:path";
|
|
11
|
-
|
|
12
|
-
import { version } from "../package.json" with { type: "json" };
|
|
13
|
-
|
|
14
|
-
const CONFIG_DIR = join(homedir(), ".agent-bisync");
|
|
15
|
-
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
16
|
-
const CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
17
|
-
const CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
|
|
18
|
-
const DEBUG_LOG_PATH = join(CONFIG_DIR, "debug.log");
|
|
19
|
-
const CLIENT_ID = "bisync-cli";
|
|
20
|
-
const MAX_LINES_PER_BATCH = 200;
|
|
21
|
-
|
|
22
|
-
type Config = {
|
|
23
|
-
siteUrl: string;
|
|
24
|
-
token: string;
|
|
25
|
-
clientId: string;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
type SiteUrlResolution = {
|
|
29
|
-
siteUrl: string | null;
|
|
30
|
-
source: "arg" | "env" | "vite" | "missing";
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
34
|
-
|
|
35
|
-
const usage = () => {
|
|
36
|
-
console.log(`bisync <command>
|
|
37
|
-
|
|
38
|
-
Commands:
|
|
39
|
-
setup --site-url <url> Configure hooks and authenticate
|
|
40
|
-
verify Check auth and list session ids
|
|
41
|
-
hook <HOOK> Handle Claude hook input (stdin JSON)
|
|
42
|
-
|
|
43
|
-
Global options:
|
|
44
|
-
--verbose Enable verbose logging
|
|
45
|
-
`);
|
|
46
|
-
};
|
|
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
|
-
|
|
75
|
-
const readConfig = async (): Promise<Config> => {
|
|
76
|
-
return await Bun.file(CONFIG_PATH).json();
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const writeConfig = async (config: Config) => {
|
|
80
|
-
await Bun.write(CONFIG_PATH, JSON.stringify(config, null, 2), {
|
|
81
|
-
createPath: true,
|
|
82
|
-
});
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const resolveSiteUrl = (args: string[]): SiteUrlResolution => {
|
|
86
|
-
const argIndex = args.findIndex((value) => value === "--site-url" || value === "--siteUrl");
|
|
87
|
-
const argValue = argIndex >= 0 ? args[argIndex + 1] : undefined;
|
|
88
|
-
const envValue = process.env.BISYNC_SITE_URL;
|
|
89
|
-
if (argValue) {
|
|
90
|
-
return { siteUrl: argValue, source: "arg" };
|
|
91
|
-
}
|
|
92
|
-
if (envValue) {
|
|
93
|
-
return { siteUrl: envValue, source: "env" };
|
|
94
|
-
}
|
|
95
|
-
const viteConvexUrl = process.env.VITE_CONVEX_URL;
|
|
96
|
-
if (viteConvexUrl?.endsWith(".convex.cloud")) {
|
|
97
|
-
return {
|
|
98
|
-
siteUrl: viteConvexUrl.replace(".convex.cloud", ".convex.site"),
|
|
99
|
-
source: "vite",
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
if (viteConvexUrl) {
|
|
103
|
-
return { siteUrl: viteConvexUrl, source: "vite" };
|
|
104
|
-
}
|
|
105
|
-
return { siteUrl: null, source: "missing" };
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const openBrowser = async (url: string) => {
|
|
109
|
-
if (process.platform !== "darwin") {
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
try {
|
|
113
|
-
await Bun.$`open ${url}`;
|
|
114
|
-
} catch {
|
|
115
|
-
// Ignore failures to open browser.
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const deviceAuthFlow = async (siteUrl: string) => {
|
|
120
|
-
log("debug", `device auth start siteUrl=${siteUrl}`);
|
|
121
|
-
const codeResponse = await fetch(`${siteUrl}/api/auth/device/code`, {
|
|
122
|
-
method: "POST",
|
|
123
|
-
headers: { "Content-Type": "application/json" },
|
|
124
|
-
body: JSON.stringify({ client_id: CLIENT_ID }),
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
if (!codeResponse.ok) {
|
|
128
|
-
const errorText = await codeResponse.text();
|
|
129
|
-
log("debug", `device code request failed status=${codeResponse.status}`);
|
|
130
|
-
throw new Error(`Device code request failed: ${codeResponse.status} ${errorText}`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
log("debug", `device code request ok status=${codeResponse.status}`);
|
|
134
|
-
|
|
135
|
-
const codeData: {
|
|
136
|
-
device_code: string;
|
|
137
|
-
user_code: string;
|
|
138
|
-
verification_uri: string;
|
|
139
|
-
verification_uri_complete?: string;
|
|
140
|
-
interval?: number;
|
|
141
|
-
} = await codeResponse.json();
|
|
142
|
-
|
|
143
|
-
const verificationUrl = codeData.verification_uri_complete ?? codeData.verification_uri;
|
|
144
|
-
log("info", `Authorize this device: ${verificationUrl}`);
|
|
145
|
-
log("info", `User code: ${codeData.user_code}`);
|
|
146
|
-
await openBrowser(verificationUrl);
|
|
147
|
-
|
|
148
|
-
const intervalMs = Math.max(1, codeData.interval ?? 5) * 1000;
|
|
149
|
-
log("debug", `device auth polling intervalMs=${intervalMs}`);
|
|
150
|
-
let pollDelay = intervalMs;
|
|
151
|
-
|
|
152
|
-
while (true) {
|
|
153
|
-
await sleep(pollDelay);
|
|
154
|
-
const tokenResponse = await fetch(`${siteUrl}/api/auth/device/token`, {
|
|
155
|
-
method: "POST",
|
|
156
|
-
headers: { "Content-Type": "application/json" },
|
|
157
|
-
body: JSON.stringify({
|
|
158
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
159
|
-
device_code: codeData.device_code,
|
|
160
|
-
client_id: CLIENT_ID,
|
|
161
|
-
}),
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
const tokenData: {
|
|
165
|
-
access_token: string;
|
|
166
|
-
error?: string;
|
|
167
|
-
error_description?: string;
|
|
168
|
-
} = await tokenResponse.json();
|
|
169
|
-
|
|
170
|
-
if (tokenResponse.ok && tokenData.access_token) {
|
|
171
|
-
log("debug", `device auth success status=${tokenResponse.status}`);
|
|
172
|
-
return tokenData.access_token;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (tokenData.error === "authorization_pending") {
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
if (tokenData.error === "slow_down") {
|
|
179
|
-
pollDelay += 1000;
|
|
180
|
-
log("debug", `device auth slow_down nextDelayMs=${pollDelay}`);
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
log(
|
|
185
|
-
"debug",
|
|
186
|
-
`device auth failed status=${tokenResponse.status} error=${tokenData.error ?? "unknown"}`,
|
|
187
|
-
);
|
|
188
|
-
throw new Error(
|
|
189
|
-
`Device auth failed: ${tokenData.error ?? "unknown"} ${
|
|
190
|
-
tokenData.error_description ?? ""
|
|
191
|
-
}`.trim(),
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
type ClaudeHookEntry = {
|
|
197
|
-
matcher?: string;
|
|
198
|
-
hooks: Array<{
|
|
199
|
-
type: "command";
|
|
200
|
-
command: string;
|
|
201
|
-
}>;
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
type ClaudeSettings = {
|
|
205
|
-
enabledPlugins?: Record<string, boolean>;
|
|
206
|
-
hooks?: Record<string, ClaudeHookEntry[]>;
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
const ensureHookEntry = (entries: ClaudeHookEntry[], next: ClaudeHookEntry) => {
|
|
210
|
-
const sameMatcher = (entry: ClaudeHookEntry) =>
|
|
211
|
-
(entry.matcher ?? null) === (next.matcher ?? null);
|
|
212
|
-
const command = next.hooks[0]?.command;
|
|
213
|
-
if (!command) {
|
|
214
|
-
return entries;
|
|
215
|
-
}
|
|
216
|
-
const existing = entries.find(sameMatcher);
|
|
217
|
-
if (!existing) {
|
|
218
|
-
entries.push(next);
|
|
219
|
-
return entries;
|
|
220
|
-
}
|
|
221
|
-
if (!existing.hooks.some((hook) => hook.command === command)) {
|
|
222
|
-
existing.hooks.push({ type: "command", command });
|
|
223
|
-
}
|
|
224
|
-
return entries;
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
const mergeSettings = async () => {
|
|
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
|
-
}
|
|
245
|
-
|
|
246
|
-
const hooks = current.hooks ?? {};
|
|
247
|
-
const definitions: Array<{ name: string; matcher?: string }> = [
|
|
248
|
-
{ name: "SessionStart" },
|
|
249
|
-
{ name: "SessionEnd" },
|
|
250
|
-
{ name: "UserPromptSubmit" },
|
|
251
|
-
{ name: "PostToolUse", matcher: "*" },
|
|
252
|
-
{ name: "Stop", matcher: "*" },
|
|
253
|
-
];
|
|
254
|
-
|
|
255
|
-
for (const definition of definitions) {
|
|
256
|
-
const command = `bisync hook ${definition.name}`;
|
|
257
|
-
const entries = hooks[definition.name] ?? [];
|
|
258
|
-
ensureHookEntry(entries, {
|
|
259
|
-
matcher: definition.matcher,
|
|
260
|
-
hooks: [{ type: "command", command }],
|
|
261
|
-
});
|
|
262
|
-
hooks[definition.name] = entries;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const nextConfig: ClaudeSettings = {
|
|
266
|
-
...current,
|
|
267
|
-
enabledPlugins: current.enabledPlugins ?? {},
|
|
268
|
-
hooks,
|
|
269
|
-
};
|
|
270
|
-
await Bun.write(CLAUDE_SETTINGS_PATH, JSON.stringify(nextConfig, null, 2), {
|
|
271
|
-
createPath: true,
|
|
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
|
-
}
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
const findSessionFile = async (sessionId: string) => {
|
|
289
|
-
let bestPath: string | null = null;
|
|
290
|
-
let bestMtimeMs = 0;
|
|
291
|
-
const targetName = `${sessionId}.jsonl`;
|
|
292
|
-
|
|
293
|
-
const walk = async (dir: string) => {
|
|
294
|
-
let entries: Dirent[];
|
|
295
|
-
try {
|
|
296
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
297
|
-
} catch {
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
for (const entry of entries) {
|
|
302
|
-
const fullPath = join(dir, entry.name);
|
|
303
|
-
if (entry.isDirectory()) {
|
|
304
|
-
await walk(fullPath);
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
if (!entry.isFile() || entry.name !== targetName) {
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
const info = await stat(fullPath);
|
|
311
|
-
if (!bestPath || info.mtimeMs > bestMtimeMs) {
|
|
312
|
-
bestPath = fullPath;
|
|
313
|
-
bestMtimeMs = info.mtimeMs;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
await walk(CLAUDE_PROJECTS_DIR);
|
|
319
|
-
return bestPath;
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
const appendDebugLog = async (message: string) => {
|
|
323
|
-
const logFile = Bun.file(DEBUG_LOG_PATH);
|
|
324
|
-
const content = await logFile.text();
|
|
325
|
-
await Bun.write(DEBUG_LOG_PATH, content + message, { createPath: true });
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
const buildLogLines = (raw: string) => {
|
|
329
|
-
const lines = raw.split("\n").filter((line) => line.trim().length > 0);
|
|
330
|
-
const baseNow = Date.now();
|
|
331
|
-
return lines.map((line, index) => {
|
|
332
|
-
let ts = baseNow + index;
|
|
333
|
-
try {
|
|
334
|
-
const parsed = JSON.parse(line) as { ts?: number; timestamp?: number };
|
|
335
|
-
if (typeof parsed.ts === "number") {
|
|
336
|
-
ts = parsed.ts;
|
|
337
|
-
} else if (typeof parsed.timestamp === "number") {
|
|
338
|
-
ts = parsed.timestamp;
|
|
339
|
-
}
|
|
340
|
-
} catch {
|
|
341
|
-
// Ignore JSON parse errors; fall back to base timestamp.
|
|
342
|
-
}
|
|
343
|
-
return { ts, log: line, encoding: "utf8" as const };
|
|
344
|
-
});
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
const uploadLogs = async (siteUrl: string, token: string, sessionId: string, raw: string) => {
|
|
348
|
-
const lines = buildLogLines(raw);
|
|
349
|
-
for (let i = 0; i < lines.length; i += MAX_LINES_PER_BATCH) {
|
|
350
|
-
const batch = lines.slice(i, i + MAX_LINES_PER_BATCH);
|
|
351
|
-
const response = await fetch(`${siteUrl}/api/ingest/logs`, {
|
|
352
|
-
method: "POST",
|
|
353
|
-
headers: {
|
|
354
|
-
"Content-Type": "application/json",
|
|
355
|
-
Authorization: `Bearer ${token}`,
|
|
356
|
-
...(process.env.BISYNC_SESSION_ID
|
|
357
|
-
? { "X-Bisync-Session-Id": process.env.BISYNC_SESSION_ID }
|
|
358
|
-
: {}),
|
|
359
|
-
},
|
|
360
|
-
body: JSON.stringify({
|
|
361
|
-
session_id: sessionId,
|
|
362
|
-
lines: batch,
|
|
363
|
-
}),
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
if (!response.ok) {
|
|
367
|
-
const errorText = await response.text();
|
|
368
|
-
const error = new Error(`Log ingestion failed: ${response.status} ${errorText}`);
|
|
369
|
-
(error as Error & { status?: number }).status = response.status;
|
|
370
|
-
throw error;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
const runSetup = async (args: string[]) => {
|
|
376
|
-
log("debug", `runSetup start`);
|
|
377
|
-
const force = args.includes("--force");
|
|
378
|
-
log("debug", `runSetup force=${force}`);
|
|
379
|
-
const configExists = await Bun.file(CONFIG_PATH).exists();
|
|
380
|
-
log("debug", `config path=${CONFIG_PATH} exists=${configExists}`);
|
|
381
|
-
let existingConfig: Config | null = null;
|
|
382
|
-
if (configExists) {
|
|
383
|
-
try {
|
|
384
|
-
existingConfig = await readConfig();
|
|
385
|
-
log(
|
|
386
|
-
"debug",
|
|
387
|
-
`config loaded siteUrl=${existingConfig.siteUrl} token=${existingConfig.token ? "present" : "missing"}`,
|
|
388
|
-
);
|
|
389
|
-
} catch {
|
|
390
|
-
log("debug", `config load failed; continuing without existing config`);
|
|
391
|
-
existingConfig = null;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const resolution = resolveSiteUrl(args);
|
|
396
|
-
log("debug", `siteUrl resolved=${resolution.siteUrl ?? "null"} source=${resolution.source}`);
|
|
397
|
-
const siteUrl = resolution.siteUrl ?? existingConfig?.siteUrl ?? null;
|
|
398
|
-
if (!resolution.siteUrl && existingConfig?.siteUrl) {
|
|
399
|
-
log("debug", `siteUrl fallback to existing config ${existingConfig.siteUrl}`);
|
|
400
|
-
}
|
|
401
|
-
if (!siteUrl) {
|
|
402
|
-
log("debug", `runSetup aborted: missing siteUrl`);
|
|
403
|
-
throw new Error("Missing site URL. Provide --site-url or BISYNC_SITE_URL.");
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (configExists && existingConfig?.token && !force) {
|
|
407
|
-
log("debug", `using existing token; updating Claude settings only`);
|
|
408
|
-
await mergeSettings();
|
|
409
|
-
log("info", `Updated Claude settings using existing credentials at ${CONFIG_PATH}`);
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const token = await deviceAuthFlow(siteUrl);
|
|
414
|
-
await writeConfig({ siteUrl, token, clientId: CLIENT_ID });
|
|
415
|
-
log("debug", `config written to ${CONFIG_PATH}`);
|
|
416
|
-
await mergeSettings();
|
|
417
|
-
log("info", `Configured Claude settings and saved credentials to ${CONFIG_PATH}`);
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
const runVerify = async () => {
|
|
421
|
-
log("debug", `runVerify start config=${CONFIG_PATH}`);
|
|
422
|
-
const config = await readConfig();
|
|
423
|
-
const siteUrl = process.env.BISYNC_SITE_URL ?? config.siteUrl;
|
|
424
|
-
log("debug", `runVerify siteUrl=${siteUrl}`);
|
|
425
|
-
const response = await fetch(`${siteUrl}/api/list-sessions`, {
|
|
426
|
-
method: "GET",
|
|
427
|
-
headers: {
|
|
428
|
-
Authorization: `Bearer ${config.token}`,
|
|
429
|
-
},
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
if (!response.ok) {
|
|
433
|
-
const errorText = await response.text();
|
|
434
|
-
log("debug", `runVerify failed status=${response.status}`);
|
|
435
|
-
throw new Error(`Verify failed: ${response.status} ${errorText}`);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const data = (await response.json()) as {
|
|
439
|
-
sessions: { externalId: string }[];
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
if (data.sessions.length === 0) {
|
|
443
|
-
log("info", "No sessions found.");
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
for (const session of data.sessions) {
|
|
448
|
-
log("info", session.externalId);
|
|
449
|
-
}
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
const runHook = async (hookName: string) => {
|
|
453
|
-
log("debug", `runHook start hook=${hookName}`);
|
|
454
|
-
const stdinRaw = await new Promise<string>((resolve, reject) => {
|
|
455
|
-
let data = "";
|
|
456
|
-
process.stdin.setEncoding("utf8");
|
|
457
|
-
process.stdin.on("data", (chunk) => {
|
|
458
|
-
data += chunk;
|
|
459
|
-
});
|
|
460
|
-
process.stdin.on("end", () => resolve(data));
|
|
461
|
-
process.stdin.on("error", (error) => reject(error));
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} stdin=${stdinRaw.trim()}\n`);
|
|
465
|
-
|
|
466
|
-
if (!stdinRaw.trim()) {
|
|
467
|
-
await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=empty-stdin\n`);
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
const stdinPayload = JSON.parse(stdinRaw) as {
|
|
472
|
-
session_id?: string;
|
|
473
|
-
transcript_path?: string;
|
|
474
|
-
};
|
|
475
|
-
const sessionId = stdinPayload.session_id;
|
|
476
|
-
if (!sessionId) {
|
|
477
|
-
await appendDebugLog(
|
|
478
|
-
`[${new Date().toISOString()}] hook=${hookName} error=missing-session-id\n`,
|
|
479
|
-
);
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const config = await readConfig();
|
|
484
|
-
const siteUrl = process.env.BISYNC_SITE_URL ?? config.siteUrl;
|
|
485
|
-
const token = config.token;
|
|
486
|
-
log("debug", `runHook siteUrl=${siteUrl}`);
|
|
487
|
-
|
|
488
|
-
let sessionFile: string | null = null;
|
|
489
|
-
if (typeof stdinPayload.transcript_path === "string") {
|
|
490
|
-
const resolvedPath = resolve(stdinPayload.transcript_path);
|
|
491
|
-
if (await Bun.file(resolvedPath).exists()) {
|
|
492
|
-
sessionFile = resolvedPath;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
if (!sessionFile) {
|
|
496
|
-
sessionFile = await findSessionFile(sessionId);
|
|
497
|
-
}
|
|
498
|
-
if (!sessionFile) {
|
|
499
|
-
log("debug", `runHook session file missing sessionId=${sessionId}`);
|
|
500
|
-
await appendDebugLog(
|
|
501
|
-
`[${new Date().toISOString()}] hook=${hookName} error=session-file-not-found session_id=${sessionId}\n`,
|
|
502
|
-
);
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
log("debug", `runHook sessionFile=${sessionFile}`);
|
|
507
|
-
|
|
508
|
-
const raw = await Bun.file(sessionFile).text();
|
|
509
|
-
if (!raw.trim()) {
|
|
510
|
-
log("debug", `runHook session file empty sessionId=${sessionId}`);
|
|
511
|
-
await appendDebugLog(
|
|
512
|
-
`[${new Date().toISOString()}] hook=${hookName} error=empty-log session_id=${sessionId}\n`,
|
|
513
|
-
);
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
try {
|
|
518
|
-
await uploadLogs(siteUrl, token, sessionId, raw);
|
|
519
|
-
log("debug", `runHook upload ok sessionId=${sessionId}`);
|
|
520
|
-
} catch (error) {
|
|
521
|
-
log(
|
|
522
|
-
"debug",
|
|
523
|
-
`runHook upload failed sessionId=${sessionId} message=${error instanceof Error ? error.message : String(error)}`,
|
|
524
|
-
);
|
|
525
|
-
await appendDebugLog(
|
|
526
|
-
`[${new Date().toISOString()}] hook=${hookName} error=upload-failed session_id=${sessionId} message=${error instanceof Error ? error.message : String(error)}\n`,
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
|
-
};
|
|
530
|
-
|
|
531
|
-
const main = async () => {
|
|
532
|
-
if (process.argv.includes("--version")) {
|
|
533
|
-
console.log(`▶︎ ${version} (Bun ${Bun.version})`);
|
|
534
|
-
process.exit(0);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if (process.argv.includes("--verbose")) {
|
|
538
|
-
LOG_LEVEL = "debug";
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const rawArgs = process.argv.slice(2);
|
|
542
|
-
const [command, ...args] = rawArgs;
|
|
543
|
-
log("debug", `argv=${JSON.stringify(rawArgs)}`);
|
|
544
|
-
if (!command) {
|
|
545
|
-
usage();
|
|
546
|
-
process.exit(1);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
try {
|
|
550
|
-
if (command === "setup") {
|
|
551
|
-
await runSetup(args);
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
if (command === "verify") {
|
|
555
|
-
await runVerify();
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
if (command === "hook") {
|
|
559
|
-
await runHook(args[0] ?? "unknown");
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
usage();
|
|
564
|
-
process.exit(1);
|
|
565
|
-
} catch (error) {
|
|
566
|
-
log("error", error instanceof Error ? error.message : String(error));
|
|
567
|
-
process.exit(1);
|
|
568
|
-
}
|
|
569
|
-
};
|
|
570
|
-
|
|
571
|
-
await main();
|