bisync-cli 0.0.2 → 0.0.3
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 +355 -0
- package/package.json +1 -1
- package/src/bin.ts +8 -1
package/dist/bisync.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/bin.ts
|
|
5
|
+
import { readdir, stat } from "fs/promises";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { join, resolve } from "path";
|
|
8
|
+
// package.json
|
|
9
|
+
var version = "0.0.3";
|
|
10
|
+
|
|
11
|
+
// src/bin.ts
|
|
12
|
+
var CONFIG_DIR = join(homedir(), ".agent-bisync");
|
|
13
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
14
|
+
var CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
15
|
+
var CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
|
|
16
|
+
var DEBUG_DIR = join(homedir(), ".bysync");
|
|
17
|
+
var DEBUG_LOG_PATH = join(DEBUG_DIR, "debug.log");
|
|
18
|
+
var CLIENT_ID = "bisync-cli";
|
|
19
|
+
var MAX_LINES_PER_BATCH = 200;
|
|
20
|
+
var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
21
|
+
var usage = () => {
|
|
22
|
+
console.log(`bisync <command>
|
|
23
|
+
|
|
24
|
+
Commands:
|
|
25
|
+
setup --site-url <url> Configure hooks and authenticate
|
|
26
|
+
verify Check auth and list session ids
|
|
27
|
+
hook <HOOK> Handle Claude hook input (stdin JSON)
|
|
28
|
+
`);
|
|
29
|
+
};
|
|
30
|
+
var readConfig = async () => {
|
|
31
|
+
return await Bun.file(CONFIG_PATH).json();
|
|
32
|
+
};
|
|
33
|
+
var writeConfig = async (config) => {
|
|
34
|
+
await Bun.write(CONFIG_PATH, JSON.stringify(config, null, 2), {
|
|
35
|
+
createPath: true
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
var resolveSiteUrl = (args) => {
|
|
39
|
+
const argIndex = args.findIndex((value) => value === "--site-url" || value === "--siteUrl");
|
|
40
|
+
const argValue = argIndex >= 0 ? args[argIndex + 1] : undefined;
|
|
41
|
+
const envValue = process.env.BISYNC_SITE_URL;
|
|
42
|
+
if (argValue) {
|
|
43
|
+
return argValue;
|
|
44
|
+
}
|
|
45
|
+
if (envValue) {
|
|
46
|
+
return envValue;
|
|
47
|
+
}
|
|
48
|
+
const viteConvexUrl = process.env.VITE_CONVEX_URL;
|
|
49
|
+
if (viteConvexUrl?.endsWith(".convex.cloud")) {
|
|
50
|
+
return viteConvexUrl.replace(".convex.cloud", ".convex.site");
|
|
51
|
+
}
|
|
52
|
+
return viteConvexUrl ?? null;
|
|
53
|
+
};
|
|
54
|
+
var openBrowser = async (url) => {
|
|
55
|
+
if (process.platform !== "darwin") {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
await Bun.$`open ${url}`;
|
|
60
|
+
} catch {}
|
|
61
|
+
};
|
|
62
|
+
var deviceAuthFlow = async (siteUrl) => {
|
|
63
|
+
const codeResponse = await fetch(`${siteUrl}/api/auth/device/code`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/json" },
|
|
66
|
+
body: JSON.stringify({ client_id: CLIENT_ID })
|
|
67
|
+
});
|
|
68
|
+
if (!codeResponse.ok) {
|
|
69
|
+
const errorText = await codeResponse.text();
|
|
70
|
+
throw new Error(`Device code request failed: ${codeResponse.status} ${errorText}`);
|
|
71
|
+
}
|
|
72
|
+
const codeData = await codeResponse.json();
|
|
73
|
+
const verificationUrl = codeData.verification_uri_complete ?? codeData.verification_uri;
|
|
74
|
+
console.log(`Authorize this device: ${verificationUrl}`);
|
|
75
|
+
console.log(`User code: ${codeData.user_code}`);
|
|
76
|
+
await openBrowser(verificationUrl);
|
|
77
|
+
const intervalMs = Math.max(1, codeData.interval ?? 5) * 1000;
|
|
78
|
+
let pollDelay = intervalMs;
|
|
79
|
+
while (true) {
|
|
80
|
+
await sleep(pollDelay);
|
|
81
|
+
const tokenResponse = await fetch(`${siteUrl}/api/auth/device/token`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "Content-Type": "application/json" },
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
86
|
+
device_code: codeData.device_code,
|
|
87
|
+
client_id: CLIENT_ID
|
|
88
|
+
})
|
|
89
|
+
});
|
|
90
|
+
const tokenData = await tokenResponse.json();
|
|
91
|
+
if (tokenResponse.ok && tokenData.access_token) {
|
|
92
|
+
return tokenData.access_token;
|
|
93
|
+
}
|
|
94
|
+
if (tokenData.error === "authorization_pending") {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (tokenData.error === "slow_down") {
|
|
98
|
+
pollDelay += 1000;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
throw new Error(`Device auth failed: ${tokenData.error ?? "unknown"} ${tokenData.error_description ?? ""}`.trim());
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var ensureHookEntry = (entries, next) => {
|
|
105
|
+
const sameMatcher = (entry) => (entry.matcher ?? null) === (next.matcher ?? null);
|
|
106
|
+
const command = next.hooks[0]?.command;
|
|
107
|
+
if (!command) {
|
|
108
|
+
return entries;
|
|
109
|
+
}
|
|
110
|
+
const existing = entries.find(sameMatcher);
|
|
111
|
+
if (!existing) {
|
|
112
|
+
entries.push(next);
|
|
113
|
+
return entries;
|
|
114
|
+
}
|
|
115
|
+
if (!existing.hooks.some((hook) => hook.command === command)) {
|
|
116
|
+
existing.hooks.push({ type: "command", command });
|
|
117
|
+
}
|
|
118
|
+
return entries;
|
|
119
|
+
};
|
|
120
|
+
var mergeSettings = async () => {
|
|
121
|
+
const current = await Bun.file(CLAUDE_SETTINGS_PATH).json().catch(() => ({}));
|
|
122
|
+
const hooks = current.hooks ?? {};
|
|
123
|
+
const definitions = [
|
|
124
|
+
{ name: "SessionStart" },
|
|
125
|
+
{ name: "SessionEnd" },
|
|
126
|
+
{ name: "UserPromptSubmit" },
|
|
127
|
+
{ name: "PostToolUse", matcher: "*" },
|
|
128
|
+
{ name: "Stop", matcher: "*" }
|
|
129
|
+
];
|
|
130
|
+
for (const definition of definitions) {
|
|
131
|
+
const command = `bisync hook ${definition.name}`;
|
|
132
|
+
const entries = hooks[definition.name] ?? [];
|
|
133
|
+
ensureHookEntry(entries, {
|
|
134
|
+
matcher: definition.matcher,
|
|
135
|
+
hooks: [{ type: "command", command }]
|
|
136
|
+
});
|
|
137
|
+
hooks[definition.name] = entries;
|
|
138
|
+
}
|
|
139
|
+
const nextConfig = {
|
|
140
|
+
...current,
|
|
141
|
+
enabledPlugins: current.enabledPlugins ?? {},
|
|
142
|
+
hooks
|
|
143
|
+
};
|
|
144
|
+
await Bun.write(CLAUDE_SETTINGS_PATH, JSON.stringify(nextConfig, null, 2), {
|
|
145
|
+
createPath: true
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
var findSessionFile = async (sessionId) => {
|
|
149
|
+
let bestPath = null;
|
|
150
|
+
let bestMtimeMs = 0;
|
|
151
|
+
const targetName = `${sessionId}.jsonl`;
|
|
152
|
+
const walk = async (dir) => {
|
|
153
|
+
let entries;
|
|
154
|
+
try {
|
|
155
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
156
|
+
} catch {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
const fullPath = join(dir, entry.name);
|
|
161
|
+
if (entry.isDirectory()) {
|
|
162
|
+
await walk(fullPath);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (!entry.isFile() || entry.name !== targetName) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const info = await stat(fullPath);
|
|
169
|
+
if (!bestPath || info.mtimeMs > bestMtimeMs) {
|
|
170
|
+
bestPath = fullPath;
|
|
171
|
+
bestMtimeMs = info.mtimeMs;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
await walk(CLAUDE_PROJECTS_DIR);
|
|
176
|
+
return bestPath;
|
|
177
|
+
};
|
|
178
|
+
var appendDebugLog = async (message) => {
|
|
179
|
+
const logFile = Bun.file(DEBUG_LOG_PATH);
|
|
180
|
+
const content = await logFile.text();
|
|
181
|
+
await logFile.write(content + message);
|
|
182
|
+
};
|
|
183
|
+
var buildLogLines = (raw) => {
|
|
184
|
+
const lines = raw.split(`
|
|
185
|
+
`).filter((line) => line.trim().length > 0);
|
|
186
|
+
const baseNow = Date.now();
|
|
187
|
+
return lines.map((line, index) => {
|
|
188
|
+
let ts = baseNow + index;
|
|
189
|
+
try {
|
|
190
|
+
const parsed = JSON.parse(line);
|
|
191
|
+
if (typeof parsed.ts === "number") {
|
|
192
|
+
ts = parsed.ts;
|
|
193
|
+
} else if (typeof parsed.timestamp === "number") {
|
|
194
|
+
ts = parsed.timestamp;
|
|
195
|
+
}
|
|
196
|
+
} catch {}
|
|
197
|
+
return { ts, log: line, encoding: "utf8" };
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
var uploadLogs = async (siteUrl, token, sessionId, raw) => {
|
|
201
|
+
const lines = buildLogLines(raw);
|
|
202
|
+
for (let i = 0;i < lines.length; i += MAX_LINES_PER_BATCH) {
|
|
203
|
+
const batch = lines.slice(i, i + MAX_LINES_PER_BATCH);
|
|
204
|
+
const response = await fetch(`${siteUrl}/api/ingest/logs`, {
|
|
205
|
+
method: "POST",
|
|
206
|
+
headers: {
|
|
207
|
+
"Content-Type": "application/json",
|
|
208
|
+
Authorization: `Bearer ${token}`
|
|
209
|
+
},
|
|
210
|
+
body: JSON.stringify({
|
|
211
|
+
session_id: sessionId,
|
|
212
|
+
lines: batch
|
|
213
|
+
})
|
|
214
|
+
});
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
const errorText = await response.text();
|
|
217
|
+
const error = new Error(`Log ingestion failed: ${response.status} ${errorText}`);
|
|
218
|
+
error.status = response.status;
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
var runSetup = async (args) => {
|
|
224
|
+
const force = args.includes("--force");
|
|
225
|
+
const configExists = await Bun.file(CONFIG_PATH).exists();
|
|
226
|
+
let existingConfig = null;
|
|
227
|
+
if (configExists) {
|
|
228
|
+
try {
|
|
229
|
+
existingConfig = await readConfig();
|
|
230
|
+
} catch {
|
|
231
|
+
existingConfig = null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const siteUrl = resolveSiteUrl(args) ?? existingConfig?.siteUrl ?? null;
|
|
235
|
+
if (!siteUrl) {
|
|
236
|
+
throw new Error("Missing site URL. Provide --site-url or BISYNC_SITE_URL.");
|
|
237
|
+
}
|
|
238
|
+
if (configExists && existingConfig?.token && !force) {
|
|
239
|
+
await mergeSettings();
|
|
240
|
+
console.log(`Updated Claude settings using existing credentials at ${CONFIG_PATH}`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const token = await deviceAuthFlow(siteUrl);
|
|
244
|
+
await writeConfig({ siteUrl, token, clientId: CLIENT_ID });
|
|
245
|
+
await mergeSettings();
|
|
246
|
+
console.log(`Configured Claude settings and saved credentials to ${CONFIG_PATH}`);
|
|
247
|
+
};
|
|
248
|
+
var runVerify = async () => {
|
|
249
|
+
const config = await readConfig();
|
|
250
|
+
const siteUrl = process.env.BISYNC_SITE_URL ?? config.siteUrl;
|
|
251
|
+
const response = await fetch(`${siteUrl}/api/list-sessions`, {
|
|
252
|
+
method: "GET",
|
|
253
|
+
headers: {
|
|
254
|
+
Authorization: `Bearer ${config.token}`
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
if (!response.ok) {
|
|
258
|
+
const errorText = await response.text();
|
|
259
|
+
throw new Error(`Verify failed: ${response.status} ${errorText}`);
|
|
260
|
+
}
|
|
261
|
+
const data = await response.json();
|
|
262
|
+
if (data.sessions.length === 0) {
|
|
263
|
+
console.log("No sessions found.");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
for (const session of data.sessions) {
|
|
267
|
+
console.log(session.externalId);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
var runHook = async (hookName) => {
|
|
271
|
+
const stdinRaw = await new Promise((resolve2, reject) => {
|
|
272
|
+
let data = "";
|
|
273
|
+
process.stdin.setEncoding("utf8");
|
|
274
|
+
process.stdin.on("data", (chunk) => {
|
|
275
|
+
data += chunk;
|
|
276
|
+
});
|
|
277
|
+
process.stdin.on("end", () => resolve2(data));
|
|
278
|
+
process.stdin.on("error", (error) => reject(error));
|
|
279
|
+
});
|
|
280
|
+
await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} stdin=${stdinRaw.trim()}
|
|
281
|
+
`);
|
|
282
|
+
if (!stdinRaw.trim()) {
|
|
283
|
+
await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=empty-stdin
|
|
284
|
+
`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const stdinPayload = JSON.parse(stdinRaw);
|
|
288
|
+
const sessionId = stdinPayload.session_id;
|
|
289
|
+
if (!sessionId) {
|
|
290
|
+
await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=missing-session-id
|
|
291
|
+
`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const config = await readConfig();
|
|
295
|
+
const siteUrl = process.env.BISYNC_SITE_URL ?? config.siteUrl;
|
|
296
|
+
const token = config.token;
|
|
297
|
+
let sessionFile = null;
|
|
298
|
+
if (typeof stdinPayload.transcript_path === "string") {
|
|
299
|
+
const resolvedPath = resolve(stdinPayload.transcript_path);
|
|
300
|
+
if (await Bun.file(resolvedPath).exists()) {
|
|
301
|
+
sessionFile = resolvedPath;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (!sessionFile) {
|
|
305
|
+
sessionFile = await findSessionFile(sessionId);
|
|
306
|
+
}
|
|
307
|
+
if (!sessionFile) {
|
|
308
|
+
await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=session-file-not-found session_id=${sessionId}
|
|
309
|
+
`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const raw = await Bun.file(sessionFile).text();
|
|
313
|
+
if (!raw.trim()) {
|
|
314
|
+
await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=empty-log session_id=${sessionId}
|
|
315
|
+
`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
await uploadLogs(siteUrl, token, sessionId, raw);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=upload-failed session_id=${sessionId} message=${error instanceof Error ? error.message : String(error)}
|
|
322
|
+
`);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
var main = async () => {
|
|
326
|
+
if (process.argv.includes("--version")) {
|
|
327
|
+
console.log(`\u25B6\uFE0E ${version} (Bun ${Bun.version})`);
|
|
328
|
+
process.exit(0);
|
|
329
|
+
}
|
|
330
|
+
const [command, ...args] = process.argv.slice(2);
|
|
331
|
+
if (!command) {
|
|
332
|
+
usage();
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
if (command === "setup") {
|
|
337
|
+
await runSetup(args);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (command === "verify") {
|
|
341
|
+
await runVerify();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (command === "hook") {
|
|
345
|
+
await runHook(args[0] ?? "unknown");
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
usage();
|
|
349
|
+
process.exit(1);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.error(error instanceof Error ? error.message : error);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
await main();
|
package/package.json
CHANGED
package/src/bin.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { readdir, stat } from "node:fs/promises";
|
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { join, resolve } from "node:path";
|
|
9
9
|
|
|
10
|
+
import { version } from "../package.json" with { type: "json" };
|
|
11
|
+
|
|
10
12
|
const CONFIG_DIR = join(homedir(), ".agent-bisync");
|
|
11
13
|
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
12
14
|
const CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
@@ -414,6 +416,11 @@ const runHook = async (hookName: string) => {
|
|
|
414
416
|
};
|
|
415
417
|
|
|
416
418
|
const main = async () => {
|
|
419
|
+
if (process.argv.includes("--version")) {
|
|
420
|
+
console.log(`▶︎ ${version} (Bun ${Bun.version})`);
|
|
421
|
+
process.exit(0);
|
|
422
|
+
}
|
|
423
|
+
|
|
417
424
|
const [command, ...args] = process.argv.slice(2);
|
|
418
425
|
if (!command) {
|
|
419
426
|
usage();
|
|
@@ -442,4 +449,4 @@ const main = async () => {
|
|
|
442
449
|
}
|
|
443
450
|
};
|
|
444
451
|
|
|
445
|
-
|
|
452
|
+
await main();
|