bisync-cli 0.0.10 → 0.0.12

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.
Files changed (2) hide show
  1. package/dist/bisync.js +517 -247
  2. package/package.json +2 -2
package/dist/bisync.js CHANGED
@@ -2,207 +2,52 @@
2
2
  // @bun
3
3
 
4
4
  // src/bin.ts
5
- import { createHash } from "crypto";
6
- import { createWriteStream, mkdirSync } from "fs";
7
- import { readdir, stat } from "fs/promises";
8
- import { homedir } from "os";
9
- import { join, relative, resolve, sep } from "path";
5
+ import { appendFileSync, mkdirSync } from "fs";
6
+ import { homedir as homedir3 } from "os";
7
+ import { join as join3, resolve as resolve2 } from "path";
10
8
  import { createInterface } from "readline";
11
9
  import { setTimeout } from "timers/promises";
12
10
  import { parseArgs as parseArgsUtil } from "util";
13
11
  // package.json
14
- var version = "0.0.9";
15
-
16
- // src/bin.ts
17
- var CONFIG_DIR = join(homedir(), ".agent-bisync");
18
- var CONFIG_PATH = join(CONFIG_DIR, "config.json");
19
- var CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
20
- var CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
21
- var DEBUG_LOG_PATH = join(CONFIG_DIR, "debug.log");
22
- var CLIENT_ID = "bisync-cli";
23
- var MAX_LINES_PER_BATCH = 200;
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 = () => {
42
- console.log(`bisync <command>
43
-
44
- Commands:
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)
12
+ var version = "0.0.11";
50
13
 
51
- Global options:
52
- --verbose Enable verbose logging
53
- --help Show usage
14
+ // src/claude.ts
15
+ import { readdir, stat } from "fs/promises";
16
+ import { homedir } from "os";
17
+ import { join, relative, sep } from "path";
54
18
 
55
- Version: ${version} (Bun ${Bun.version})
56
- `);
57
- };
58
- var LOG_LEVEL = "info";
59
- var debugLogStream = null;
60
- var getDebugLogStream = () => {
61
- if (debugLogStream)
62
- return debugLogStream;
63
- try {
64
- mkdirSync(CONFIG_DIR, { recursive: true });
65
- debugLogStream = createWriteStream(DEBUG_LOG_PATH, { flags: "a" });
66
- debugLogStream.on("error", () => {
67
- debugLogStream = null;
68
- });
69
- return debugLogStream;
70
- } catch {
71
- return null;
72
- }
73
- };
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}
82
- `);
83
- if (level < LOG_LEVEL) {
84
- return;
85
- }
86
- console.log(`[bisync]${logMessage}`);
87
- };
88
- var readConfig = async () => {
89
- return await Bun.file(CONFIG_PATH).json();
90
- };
91
- var writeConfig = async (config) => {
92
- await Bun.write(CONFIG_PATH, JSON.stringify(config, null, 2), {
93
- createPath: true
94
- });
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
- };
122
- var resolveSiteUrl = (args) => {
123
- const argValue = args["site-url"] ?? args.siteUrl;
124
- const envValue = process.env.BISYNC_SITE_URL;
125
- if (argValue) {
126
- return { siteUrl: argValue, source: "arg" };
127
- }
128
- if (envValue) {
129
- return { siteUrl: envValue, source: "env" };
130
- }
131
- return { siteUrl: null, source: "missing" };
132
- };
133
- var openBrowser = async (url) => {
134
- if (process.platform !== "darwin") {
135
- return;
136
- }
137
- try {
138
- await Bun.$`open ${url}`;
139
- } catch {}
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
- };
154
- var deviceAuthFlow = async (siteUrl) => {
155
- log("debug", `device auth start`, { siteUrl });
156
- const codeResponse = await fetch(`${siteUrl}/api/auth/device/code`, {
157
- method: "POST",
158
- headers: { "Content-Type": "application/json" },
159
- body: JSON.stringify({ client_id: CLIENT_ID })
160
- });
161
- if (!codeResponse.ok) {
162
- const errorText = await codeResponse.text();
163
- log("debug", `device code request failed`, { status: codeResponse.status });
164
- throw new Error(`Device code request failed: ${codeResponse.status} ${errorText}`);
19
+ // src/shared.ts
20
+ import { createHash } from "crypto";
21
+ var buildStateFilesFromCollection = async (files, kind, siteUrl, token, helpers) => {
22
+ if (files.length === 0) {
23
+ return [];
165
24
  }
166
- log("debug", `device code request ok`, { status: codeResponse.status });
167
- const codeData = await codeResponse.json();
168
- const verificationUrl = buildVerificationUrl(codeData);
169
- console.log(`Authorize this device: ${verificationUrl}`);
170
- console.log(`Your Verification Code: ${codeData.user_code}`);
171
- await openBrowser(verificationUrl);
172
- const intervalMs = Math.max(1, codeData.interval ?? 5) * 1000;
173
- log("debug", `device auth polling`, { intervalMs });
174
- let pollDelay = intervalMs;
175
- while (true) {
176
- await setTimeout(pollDelay);
177
- const tokenResponse = await fetch(`${siteUrl}/api/auth/device/token`, {
178
- method: "POST",
179
- headers: { "Content-Type": "application/json" },
180
- body: JSON.stringify({
181
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
182
- device_code: codeData.device_code,
183
- client_id: CLIENT_ID
184
- })
185
- });
186
- const tokenData = await tokenResponse.json();
187
- if (tokenResponse.ok && tokenData.access_token) {
188
- log("debug", `device auth success`, { status: tokenResponse.status });
189
- return tokenData.access_token;
190
- }
191
- if (tokenData.error === "authorization_pending") {
192
- continue;
193
- }
194
- if (tokenData.error === "slow_down") {
195
- pollDelay += 1000;
196
- log("debug", `device auth slow_down`, { nextDelayMs: pollDelay });
197
- continue;
25
+ const uploadUrls = await helpers.getStateUploadUrls(siteUrl, token, files.length);
26
+ const entries = [];
27
+ for (let index = 0;index < files.length; index += 1) {
28
+ const file = files[index];
29
+ const uploadUrl = uploadUrls[index];
30
+ if (!file || !uploadUrl) {
31
+ throw new Error("Upload URL list did not match file list");
198
32
  }
199
- log("debug", `device auth failed`, {
200
- status: tokenResponse.status,
201
- error: tokenData.error ?? "unknown"
33
+ const data = new Uint8Array(await Bun.file(file.fullPath).arrayBuffer());
34
+ const fileHash = createHash("sha256").update(data).digest("hex");
35
+ const storageId = await helpers.uploadStateFileToUrl(uploadUrl, file.fullPath);
36
+ entries.push({
37
+ path: file.relativePath,
38
+ fileHash,
39
+ storageId,
40
+ size: file.size,
41
+ mtimeMs: file.mtimeMs,
42
+ kind
202
43
  });
203
- throw new Error(`Device auth failed: ${tokenData.error ?? "unknown"} ${tokenData.error_description ?? ""}`.trim());
204
44
  }
45
+ return entries;
205
46
  };
47
+
48
+ // src/claude.ts
49
+ var CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
50
+ var CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
206
51
  var ensureHookEntry = (entries, next) => {
207
52
  const sameMatcher = (entry) => (entry.matcher ?? null) === (next.matcher ?? null);
208
53
  const command = next.hooks[0]?.command;
@@ -219,7 +64,7 @@ var ensureHookEntry = (entries, next) => {
219
64
  }
220
65
  return entries;
221
66
  };
222
- var mergeSettings = async () => {
67
+ var mergeClaudeSettings = async (log, options) => {
223
68
  log("debug", `mergeSettings`, { path: CLAUDE_SETTINGS_PATH });
224
69
  const settingsFile = Bun.file(CLAUDE_SETTINGS_PATH);
225
70
  const settingsExists = await settingsFile.exists();
@@ -256,7 +101,14 @@ var mergeSettings = async () => {
256
101
  enabledPlugins: current.enabledPlugins ?? {},
257
102
  hooks
258
103
  };
259
- await Bun.write(CLAUDE_SETTINGS_PATH, JSON.stringify(nextConfig, null, 2), {
104
+ const serialized = JSON.stringify(nextConfig, null, 2);
105
+ if (options?.dry) {
106
+ console.log(`# ${CLAUDE_SETTINGS_PATH}`);
107
+ console.log(serialized);
108
+ log("debug", `mergeSettings dry run complete`);
109
+ return;
110
+ }
111
+ await Bun.write(CLAUDE_SETTINGS_PATH, serialized, {
260
112
  createPath: true
261
113
  });
262
114
  log("debug", `mergeSettings wrote settings`);
@@ -272,7 +124,7 @@ var mergeSettings = async () => {
272
124
  });
273
125
  }
274
126
  };
275
- var findSessionFile = async (sessionId) => {
127
+ var findClaudeSessionFile = async (sessionId) => {
276
128
  let bestPath = null;
277
129
  let bestMtimeMs = 0;
278
130
  const targetName = `${sessionId}.jsonl`;
@@ -309,14 +161,14 @@ var isDirectory = async (path) => {
309
161
  return false;
310
162
  }
311
163
  };
312
- var resolveSessionDir = async (sessionFile) => {
164
+ var resolveClaudeSessionDir = async (sessionFile) => {
313
165
  if (!sessionFile.endsWith(".jsonl")) {
314
166
  return null;
315
167
  }
316
168
  const candidate = sessionFile.slice(0, -".jsonl".length);
317
169
  return await isDirectory(candidate) ? candidate : null;
318
170
  };
319
- var findSessionDir = async (sessionId) => {
171
+ var findClaudeSessionDir = async (sessionId) => {
320
172
  let bestPath = null;
321
173
  let bestMtimeMs = 0;
322
174
  const walk = async (dir) => {
@@ -345,7 +197,7 @@ var findSessionDir = async (sessionId) => {
345
197
  return bestPath;
346
198
  };
347
199
  var normalizePath = (value) => value.split(sep).join("/");
348
- var collectSessionStateFiles = async (sessionDir) => {
200
+ var collectClaudeStateFiles = async (sessionDir) => {
349
201
  const files = [];
350
202
  const walk = async (dir) => {
351
203
  let entries;
@@ -375,6 +227,329 @@ var collectSessionStateFiles = async (sessionDir) => {
375
227
  await walk(sessionDir);
376
228
  return files;
377
229
  };
230
+ var buildClaudeStateFiles = async (sessionDir, siteUrl, token, helpers) => {
231
+ const files = await collectClaudeStateFiles(sessionDir);
232
+ return buildStateFilesFromCollection(files, "claude-state", siteUrl, token, helpers);
233
+ };
234
+
235
+ // src/codex.ts
236
+ import { stat as stat2 } from "fs/promises";
237
+ import { homedir as homedir2, userInfo } from "os";
238
+ import { join as join2, resolve } from "path";
239
+ var resolveHomeDir = () => {
240
+ const envHome = process.env.HOME ?? process.env.USERPROFILE;
241
+ if (envHome) {
242
+ return envHome;
243
+ }
244
+ try {
245
+ return userInfo().homedir;
246
+ } catch {
247
+ return homedir2();
248
+ }
249
+ };
250
+ var CODEX_HOME = process.env.CODEX_HOME ? resolve(process.env.CODEX_HOME) : join2(resolveHomeDir(), ".codex");
251
+ var CODEX_CONFIG_PATH = join2(CODEX_HOME, "config.toml");
252
+ var CODEX_SESSIONS_DIR = join2(CODEX_HOME, "sessions");
253
+ var CODEX_ARCHIVED_SESSIONS_DIR = join2(CODEX_HOME, "archived_sessions");
254
+ var CODEX_STATE_FILES = ["state.sqlite", "session_index.jsonl", "history.jsonl"];
255
+ var CODEX_NOTIFY_COMMAND = ["bisync", "hook", "Codex"];
256
+ var getCodexPaths = () => ({
257
+ home: CODEX_HOME,
258
+ sessionsDir: CODEX_SESSIONS_DIR,
259
+ archivedSessionsDir: CODEX_ARCHIVED_SESSIONS_DIR
260
+ });
261
+ var mergeCodexConfig = async (force, log, options) => {
262
+ let current = {};
263
+ const configFile = Bun.file(CODEX_CONFIG_PATH);
264
+ if (await configFile.exists()) {
265
+ try {
266
+ const parsed = Bun.TOML.parse(await configFile.text());
267
+ if (parsed && typeof parsed === "object") {
268
+ current = parsed;
269
+ }
270
+ } catch (error) {
271
+ log("debug", "mergeCodexConfig parse failed", {
272
+ error: error instanceof Error ? error.message : String(error)
273
+ });
274
+ }
275
+ }
276
+ const existingNotify = current.notify;
277
+ const isArray = Array.isArray(existingNotify);
278
+ const matches = isArray && existingNotify.length === CODEX_NOTIFY_COMMAND.length && existingNotify.every((value, index) => value === CODEX_NOTIFY_COMMAND[index]);
279
+ const hasNotify = Object.prototype.hasOwnProperty.call(current, "notify");
280
+ if (!force && hasNotify) {
281
+ if (matches) {
282
+ log("debug", "mergeCodexConfig notify already configured");
283
+ return;
284
+ }
285
+ log("debug", "mergeCodexConfig existing notify preserved");
286
+ return;
287
+ }
288
+ const nextConfig = {
289
+ ...current,
290
+ notify: CODEX_NOTIFY_COMMAND
291
+ };
292
+ try {
293
+ const tomlStringify = Bun.TOML.stringify ?? ((obj) => {
294
+ const lines = [];
295
+ for (const [key, value] of Object.entries(obj)) {
296
+ if (Array.isArray(value)) {
297
+ const items = value.map((v) => typeof v === "string" ? `"${v.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"` : String(v));
298
+ lines.push(`${key} = [${items.join(", ")}]`);
299
+ } else if (typeof value === "string") {
300
+ lines.push(`${key} = "${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`);
301
+ } else if (typeof value === "number" || typeof value === "boolean") {
302
+ lines.push(`${key} = ${value}`);
303
+ }
304
+ }
305
+ return lines.join(`
306
+ `) + `
307
+ `;
308
+ });
309
+ const serialized = tomlStringify(nextConfig);
310
+ if (options?.dry) {
311
+ console.log(`# ${CODEX_CONFIG_PATH}`);
312
+ console.log(serialized);
313
+ log("debug", "mergeCodexConfig dry run complete");
314
+ return;
315
+ }
316
+ await Bun.write(CODEX_CONFIG_PATH, serialized, { createPath: true });
317
+ log("debug", "mergeCodexConfig wrote config");
318
+ } catch (error) {
319
+ log("debug", "mergeCodexConfig write failed", {
320
+ error: error instanceof Error ? error.message : String(error)
321
+ });
322
+ }
323
+ };
324
+ var findCodexRolloutFile = async (threadId, options) => {
325
+ const log = options?.log;
326
+ const roots = [CODEX_SESSIONS_DIR, CODEX_ARCHIVED_SESSIONS_DIR];
327
+ const glob = new Bun.Glob(`**/rollout-*-${threadId}.jsonl`);
328
+ let bestPath = null;
329
+ let bestMtimeMs = 0;
330
+ for (const root of roots) {
331
+ try {
332
+ for await (const match of glob.scan({ cwd: root, absolute: true })) {
333
+ const info = await stat2(match);
334
+ if (!bestPath || info.mtimeMs > bestMtimeMs) {
335
+ bestPath = match;
336
+ bestMtimeMs = info.mtimeMs;
337
+ }
338
+ }
339
+ } catch (error) {
340
+ log?.("debug", "findCodexRolloutFile scan failed", {
341
+ root,
342
+ error: error instanceof Error ? error.message : String(error)
343
+ });
344
+ }
345
+ }
346
+ return bestPath;
347
+ };
348
+ var collectCodexStateFiles = async () => {
349
+ const files = [];
350
+ for (const name of CODEX_STATE_FILES) {
351
+ const fullPath = join2(CODEX_HOME, name);
352
+ try {
353
+ const info = await stat2(fullPath);
354
+ if (!info.isFile()) {
355
+ continue;
356
+ }
357
+ files.push({
358
+ fullPath,
359
+ relativePath: name,
360
+ size: info.size,
361
+ mtimeMs: info.mtimeMs
362
+ });
363
+ } catch {}
364
+ }
365
+ return files;
366
+ };
367
+ var buildCodexStateFiles = async (siteUrl, token, helpers) => {
368
+ const files = await collectCodexStateFiles();
369
+ return buildStateFilesFromCollection(files, "codex-state", siteUrl, token, helpers);
370
+ };
371
+
372
+ // src/bin.ts
373
+ var CONFIG_DIR = join3(homedir3(), ".agent-bisync");
374
+ var CONFIG_PATH = join3(CONFIG_DIR, "config.json");
375
+ var DEBUG_LOG_PATH = join3(CONFIG_DIR, "debug.log");
376
+ var CLIENT_ID = "bisync-cli";
377
+ var MAX_LINES_PER_BATCH = 200;
378
+ var parseArgs = () => {
379
+ const parsed = parseArgsUtil({
380
+ args: process.argv.slice(2),
381
+ options: {
382
+ verbose: { type: "boolean" },
383
+ version: { type: "boolean" },
384
+ help: { type: "boolean" },
385
+ force: { type: "boolean" },
386
+ dry: { type: "boolean" },
387
+ "site-url": { type: "string" },
388
+ siteUrl: { type: "string" }
389
+ },
390
+ allowPositionals: true
391
+ });
392
+ const values = parsed.values;
393
+ const positionals = parsed.positionals;
394
+ return { values, positionals };
395
+ };
396
+ var printUsage = () => {
397
+ console.log(`bisync <command>
398
+
399
+ Commands:
400
+ auth login --site-url <url> Authenticate via device flow
401
+ auth logout Sign out of the CLI
402
+ session list List session ids
403
+ setup [claude|codex] --site-url <url> Configure hooks and authenticate
404
+ hook <HOOK> Handle Claude/Codex hook input (stdin JSON)
405
+
406
+ Global options:
407
+ --verbose Enable verbose logging
408
+ --dry Print updated configs instead of writing files
409
+ --help Show usage
410
+
411
+ Version: ${version} (Bun ${Bun.version})
412
+ `);
413
+ };
414
+ var LOG_LEVEL = "info";
415
+ var appendDebugLog = (line) => {
416
+ try {
417
+ mkdirSync(CONFIG_DIR, { recursive: true });
418
+ appendFileSync(DEBUG_LOG_PATH, line);
419
+ } catch {}
420
+ };
421
+ var log = (level, message, fields) => {
422
+ let logMessage = `[${level}] ${message}`;
423
+ if (fields) {
424
+ for (const [key, value] of Object.entries(fields)) {
425
+ logMessage += ` ${key}=${value}`;
426
+ }
427
+ }
428
+ appendDebugLog(`[${new Date().toISOString()}]${logMessage}
429
+ `);
430
+ if (level < LOG_LEVEL) {
431
+ return;
432
+ }
433
+ console.log(`[bisync]${logMessage}`);
434
+ };
435
+ var readConfig = async () => {
436
+ return await Bun.file(CONFIG_PATH).json();
437
+ };
438
+ var writeConfig = async (config) => {
439
+ await Bun.write(CONFIG_PATH, JSON.stringify(config, null, 2), {
440
+ createPath: true
441
+ });
442
+ };
443
+ var readConfigIfExists = async () => {
444
+ const configFile = Bun.file(CONFIG_PATH);
445
+ if (!await configFile.exists()) {
446
+ return null;
447
+ }
448
+ try {
449
+ return await configFile.json();
450
+ } catch {
451
+ return null;
452
+ }
453
+ };
454
+ var promptYesNo = async (question, defaultValue) => {
455
+ if (!process.stdin.isTTY) {
456
+ log("warn", `${question} Using default ${defaultValue ? "yes" : "no"} (stdin not interactive).`);
457
+ return defaultValue;
458
+ }
459
+ const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
460
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
461
+ const answer = await new Promise((resolve3) => rl.question(`${question}${suffix}`, resolve3));
462
+ rl.close();
463
+ const normalized = answer.trim().toLowerCase();
464
+ if (!normalized) {
465
+ return defaultValue;
466
+ }
467
+ return normalized === "y" || normalized === "yes";
468
+ };
469
+ var resolveSiteUrl = (args) => {
470
+ const argValue = args["site-url"] ?? args.siteUrl;
471
+ const envValue = process.env.BISYNC_SITE_URL;
472
+ if (argValue) {
473
+ return { siteUrl: argValue, source: "arg" };
474
+ }
475
+ if (envValue) {
476
+ return { siteUrl: envValue, source: "env" };
477
+ }
478
+ return { siteUrl: null, source: "missing" };
479
+ };
480
+ var openBrowser = async (url) => {
481
+ if (process.platform !== "darwin") {
482
+ return;
483
+ }
484
+ try {
485
+ await Bun.$`open ${url}`;
486
+ } catch {}
487
+ };
488
+ var buildVerificationUrl = (data) => {
489
+ if (data.verification_uri_complete) {
490
+ return data.verification_uri_complete;
491
+ }
492
+ try {
493
+ const url = new URL(data.verification_uri);
494
+ url.searchParams.set("user_code", data.user_code);
495
+ return url.toString();
496
+ } catch {
497
+ const separator = data.verification_uri.includes("?") ? "&" : "?";
498
+ return `${data.verification_uri}${separator}user_code=${encodeURIComponent(data.user_code)}`;
499
+ }
500
+ };
501
+ var deviceAuthFlow = async (siteUrl) => {
502
+ log("debug", `device auth start`, { siteUrl });
503
+ const codeResponse = await fetch(`${siteUrl}/api/auth/device/code`, {
504
+ method: "POST",
505
+ headers: { "Content-Type": "application/json" },
506
+ body: JSON.stringify({ client_id: CLIENT_ID })
507
+ });
508
+ if (!codeResponse.ok) {
509
+ const errorText = await codeResponse.text();
510
+ log("debug", `device code request failed`, { status: codeResponse.status });
511
+ throw new Error(`Device code request failed: ${codeResponse.status} ${errorText}`);
512
+ }
513
+ log("debug", `device code request ok`, { status: codeResponse.status });
514
+ const codeData = await codeResponse.json();
515
+ const verificationUrl = buildVerificationUrl(codeData);
516
+ console.log(`Authorize this device: ${verificationUrl}`);
517
+ console.log(`Your Verification Code: ${codeData.user_code}`);
518
+ await openBrowser(verificationUrl);
519
+ const intervalMs = Math.max(1, codeData.interval ?? 5) * 1000;
520
+ log("debug", `device auth polling`, { intervalMs });
521
+ let pollDelay = intervalMs;
522
+ while (true) {
523
+ await setTimeout(pollDelay);
524
+ const tokenResponse = await fetch(`${siteUrl}/api/auth/device/token`, {
525
+ method: "POST",
526
+ headers: { "Content-Type": "application/json" },
527
+ body: JSON.stringify({
528
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
529
+ device_code: codeData.device_code,
530
+ client_id: CLIENT_ID
531
+ })
532
+ });
533
+ const tokenData = await tokenResponse.json();
534
+ if (tokenResponse.ok && tokenData.access_token) {
535
+ log("debug", `device auth success`, { status: tokenResponse.status });
536
+ return tokenData.access_token;
537
+ }
538
+ if (tokenData.error === "authorization_pending") {
539
+ continue;
540
+ }
541
+ if (tokenData.error === "slow_down") {
542
+ pollDelay += 1000;
543
+ log("debug", `device auth slow_down`, { nextDelayMs: pollDelay });
544
+ continue;
545
+ }
546
+ log("debug", `device auth failed`, {
547
+ status: tokenResponse.status,
548
+ error: tokenData.error ?? "unknown"
549
+ });
550
+ throw new Error(`Device auth failed: ${tokenData.error ?? "unknown"} ${tokenData.error_description ?? ""}`.trim());
551
+ }
552
+ };
378
553
  var getStateUploadUrls = async (siteUrl, token, count) => {
379
554
  const response = await fetch(`${siteUrl}/api/storage/upload-url`, {
380
555
  method: "POST",
@@ -411,32 +586,6 @@ var uploadStateFileToUrl = async (uploadUrl, filePath) => {
411
586
  }
412
587
  return data.storageId;
413
588
  };
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;
439
- };
440
589
  var buildLogLines = (raw) => {
441
590
  const lines = raw.split(`
442
591
  `).filter((line) => line.trim().length > 0);
@@ -449,6 +598,11 @@ var buildLogLines = (raw) => {
449
598
  ts = parsed.ts;
450
599
  } else if (typeof parsed.timestamp === "number") {
451
600
  ts = parsed.timestamp;
601
+ } else if (typeof parsed.timestamp === "string") {
602
+ const parsedTs = Date.parse(parsed.timestamp);
603
+ if (!Number.isNaN(parsedTs)) {
604
+ ts = parsedTs;
605
+ }
452
606
  }
453
607
  } catch {}
454
608
  return { ts, log: line, encoding: "utf8" };
@@ -523,9 +677,25 @@ var signOutRemote = async (siteUrl, token) => {
523
677
  };
524
678
  }
525
679
  };
526
- var runSetup = async (args) => {
527
- log("debug", `runSetup start`);
680
+ var describeSetupTarget = (target) => {
681
+ if (target === "claude")
682
+ return "Claude";
683
+ if (target === "codex")
684
+ return "Codex";
685
+ return "Claude + Codex";
686
+ };
687
+ var updateSettingsForTarget = async (target, force, dry) => {
688
+ if (target === "claude" || target === "all") {
689
+ await mergeClaudeSettings(log, { dry });
690
+ }
691
+ if (target === "codex" || target === "all") {
692
+ await mergeCodexConfig(force, log, { dry });
693
+ }
694
+ };
695
+ var runSetup = async (args, target) => {
696
+ log("debug", `runSetup start`, { target });
528
697
  const force = Boolean(args.force);
698
+ const dry = Boolean(args.dry);
529
699
  log("debug", "runSetup", { force });
530
700
  const configExists = await Bun.file(CONFIG_PATH).exists();
531
701
  log("debug", "config", { path: CONFIG_PATH, exists: configExists });
@@ -556,16 +726,37 @@ var runSetup = async (args) => {
556
726
  throw new Error("Missing site URL. Provide --site-url or BISYNC_SITE_URL.");
557
727
  }
558
728
  if (configExists && existingConfig?.token && !force) {
559
- log("debug", "using existing token; updating Claude settings only");
560
- await mergeSettings();
561
- log("info", "Updated Claude settings using existing credentials", { path: CONFIG_PATH });
729
+ log("debug", "using existing token; updating settings only", { target });
730
+ await updateSettingsForTarget(target, force, dry);
731
+ if (dry) {
732
+ log("info", `Printed ${describeSetupTarget(target)} settings (dry run)`, {
733
+ path: CONFIG_PATH
734
+ });
735
+ } else {
736
+ log("info", `Updated ${describeSetupTarget(target)} settings using existing credentials`, {
737
+ path: CONFIG_PATH
738
+ });
739
+ }
562
740
  return;
563
741
  }
564
742
  const token = await deviceAuthFlow(siteUrl);
565
- await writeConfig({ siteUrl, token, clientId: CLIENT_ID });
566
- log("debug", "config written", { path: CONFIG_PATH });
567
- await mergeSettings();
568
- log("info", "Configured Claude settings and saved credentials", { path: CONFIG_PATH });
743
+ if (dry) {
744
+ console.log(`# ${CONFIG_PATH}`);
745
+ console.log(JSON.stringify({ siteUrl, token, clientId: CLIENT_ID }, null, 2));
746
+ } else {
747
+ await writeConfig({ siteUrl, token, clientId: CLIENT_ID });
748
+ log("debug", "config written", { path: CONFIG_PATH });
749
+ }
750
+ await updateSettingsForTarget(target, force, dry);
751
+ if (dry) {
752
+ log("info", `Printed ${describeSetupTarget(target)} settings (dry run)`, {
753
+ path: CONFIG_PATH
754
+ });
755
+ } else {
756
+ log("info", `Configured ${describeSetupTarget(target)} settings and saved credentials`, {
757
+ path: CONFIG_PATH
758
+ });
759
+ }
569
760
  };
570
761
  var runAuthLogin = async (args) => {
571
762
  log("debug", "runAuthLogin start");
@@ -644,22 +835,91 @@ var runSessionList = async () => {
644
835
  console.log(session.externalId);
645
836
  }
646
837
  };
647
- var runHook = async (hookName) => {
838
+ var runHook = async (hookName, payloadArg) => {
648
839
  log("debug", `runHook start`, { hook: hookName });
649
- const stdinRaw = await new Promise((resolve2, reject) => {
840
+ const stdinRaw = payloadArg && payloadArg.trim().length > 0 ? "" : await new Promise((resolve3, reject) => {
650
841
  let data = "";
651
842
  process.stdin.setEncoding("utf8");
652
843
  process.stdin.on("data", (chunk) => {
653
844
  data += chunk;
654
845
  });
655
- process.stdin.on("end", () => resolve2(data));
846
+ process.stdin.on("end", () => resolve3(data));
656
847
  process.stdin.on("error", (error) => reject(error));
657
848
  });
658
- log("debug", `runHook`, { hook: hookName, stdin: stdinRaw.trim() });
659
- if (!stdinRaw.trim())
849
+ const rawPayload = stdinRaw.trim() || payloadArg?.trim() || "";
850
+ log("debug", `runHook`, { hook: hookName, stdin: stdinRaw.trim(), arg: payloadArg ?? "" });
851
+ if (!rawPayload)
852
+ return;
853
+ let payload;
854
+ try {
855
+ payload = JSON.parse(rawPayload);
856
+ } catch (error) {
857
+ log("debug", `runHook`, {
858
+ hook: hookName,
859
+ error: error instanceof Error ? error.message : String(error)
860
+ });
660
861
  return;
661
- const stdinPayload = JSON.parse(stdinRaw);
662
- const sessionId = stdinPayload.session_id;
862
+ }
863
+ if (hookName === "Codex") {
864
+ const threadId = (typeof payload["thread-id"] === "string" ? payload["thread-id"] : undefined) ?? (typeof payload.thread_id === "string" ? payload.thread_id : undefined) ?? (typeof payload.threadId === "string" ? payload.threadId : undefined);
865
+ if (!threadId) {
866
+ log("debug", `runHook`, { hook: hookName, error: "missing-thread-id" });
867
+ return;
868
+ }
869
+ const config2 = await readConfig();
870
+ const siteUrl2 = process.env.BISYNC_SITE_URL ?? config2.siteUrl;
871
+ const token2 = config2.token;
872
+ const codexPaths = getCodexPaths();
873
+ const sessionsExists = await Bun.file(codexPaths.sessionsDir).exists();
874
+ const archivedExists = await Bun.file(codexPaths.archivedSessionsDir).exists();
875
+ log("debug", `runHook`, {
876
+ threadId,
877
+ codexHome: codexPaths.home,
878
+ sessionsDir: codexPaths.sessionsDir,
879
+ sessionsExists,
880
+ archivedSessionsDir: codexPaths.archivedSessionsDir,
881
+ archivedExists
882
+ });
883
+ const rolloutFile = await findCodexRolloutFile(threadId, { log });
884
+ if (!rolloutFile) {
885
+ log("debug", `runHook`, { threadId, error: "rollout-not-found" });
886
+ return;
887
+ }
888
+ log("debug", `runHook`, { threadId, rolloutFile });
889
+ const raw = await Bun.file(rolloutFile).text();
890
+ if (!raw.trim()) {
891
+ log("debug", `runHook`, { threadId, error: "empty-log" });
892
+ } else {
893
+ try {
894
+ await uploadLogs(siteUrl2, token2, threadId, raw);
895
+ log("debug", `runHook`, { threadId, error: "upload-ok" });
896
+ } catch (error) {
897
+ log("debug", `runHook`, {
898
+ threadId,
899
+ error: error instanceof Error ? error.message : String(error)
900
+ });
901
+ }
902
+ }
903
+ try {
904
+ const stateFiles = await buildCodexStateFiles(siteUrl2, token2, {
905
+ getStateUploadUrls,
906
+ uploadStateFileToUrl
907
+ });
908
+ if (stateFiles.length === 0) {
909
+ log("debug", `runHook`, { threadId, error: "no-state-files" });
910
+ } else {
911
+ await uploadStateFiles(siteUrl2, token2, threadId, stateFiles);
912
+ log("debug", `runHook`, { threadId, error: "state-upload-ok" });
913
+ }
914
+ } catch (error) {
915
+ log("debug", `runHook`, {
916
+ threadId,
917
+ error: error instanceof Error ? error.message : String(error)
918
+ });
919
+ }
920
+ return;
921
+ }
922
+ const sessionId = typeof payload.session_id === "string" ? payload.session_id : undefined;
663
923
  if (!sessionId) {
664
924
  log("debug", `runHook`, { hook: hookName, error: "missing-session-id" });
665
925
  return;
@@ -669,24 +929,25 @@ var runHook = async (hookName) => {
669
929
  const token = config.token;
670
930
  log("debug", `runHook`, { siteUrl });
671
931
  let sessionFile = null;
672
- if (typeof stdinPayload.transcript_path === "string") {
673
- const resolvedPath = resolve(stdinPayload.transcript_path);
932
+ const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : undefined;
933
+ if (transcriptPath) {
934
+ const resolvedPath = resolve2(transcriptPath);
674
935
  if (await Bun.file(resolvedPath).exists()) {
675
936
  sessionFile = resolvedPath;
676
937
  }
677
938
  }
678
939
  if (!sessionFile) {
679
- sessionFile = await findSessionFile(sessionId);
940
+ sessionFile = await findClaudeSessionFile(sessionId);
680
941
  }
681
942
  if (!sessionFile) {
682
943
  log("debug", `runHook`, { sessionId, error: "session-file-not-found" });
683
944
  }
684
945
  let sessionDir = null;
685
946
  if (sessionFile) {
686
- sessionDir = await resolveSessionDir(sessionFile);
947
+ sessionDir = await resolveClaudeSessionDir(sessionFile);
687
948
  }
688
949
  if (!sessionDir) {
689
- sessionDir = await findSessionDir(sessionId);
950
+ sessionDir = await findClaudeSessionDir(sessionId);
690
951
  }
691
952
  if (!sessionFile && !sessionDir) {
692
953
  return;
@@ -711,7 +972,10 @@ var runHook = async (hookName) => {
711
972
  if (sessionDir && hookName === "SessionEnd") {
712
973
  log("debug", `runHook`, { sessionDir });
713
974
  try {
714
- const stateFiles = await buildStateFiles(sessionDir, siteUrl, token);
975
+ const stateFiles = await buildClaudeStateFiles(sessionDir, siteUrl, token, {
976
+ getStateUploadUrls,
977
+ uploadStateFileToUrl
978
+ });
715
979
  if (stateFiles.length === 0) {
716
980
  log("debug", `runHook`, { sessionId, error: "no-state-files" });
717
981
  return;
@@ -739,7 +1003,7 @@ var main = async () => {
739
1003
  if (values.verbose) {
740
1004
  LOG_LEVEL = "debug";
741
1005
  }
742
- const [command, subcommand] = positionals;
1006
+ const [command, subcommand, ...rest] = positionals;
743
1007
  log("debug", "argv", { argv: JSON.stringify(process.argv.slice(2)) });
744
1008
  if (!command) {
745
1009
  printUsage();
@@ -747,7 +1011,12 @@ var main = async () => {
747
1011
  }
748
1012
  try {
749
1013
  if (command === "setup") {
750
- await runSetup(values);
1014
+ if (subcommand && subcommand !== "claude" && subcommand !== "codex") {
1015
+ printUsage();
1016
+ process.exit(1);
1017
+ }
1018
+ const target = subcommand ?? "all";
1019
+ await runSetup(values, target);
751
1020
  process.exit(0);
752
1021
  }
753
1022
  if (command === "auth") {
@@ -771,7 +1040,8 @@ var main = async () => {
771
1040
  process.exit(1);
772
1041
  }
773
1042
  if (command === "hook") {
774
- await runHook(subcommand ?? "unknown");
1043
+ const payloadArg = rest.length > 0 ? rest.join(" ") : null;
1044
+ await runHook(subcommand ?? "unknown", payloadArg);
775
1045
  process.exit(0);
776
1046
  }
777
1047
  printUsage();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bisync-cli",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "bin": {
5
5
  "bisync": "dist/bisync.js"
6
6
  },
@@ -9,7 +9,7 @@
9
9
  ],
10
10
  "type": "module",
11
11
  "scripts": {
12
- "build": "bun build src/bin.ts --outfile dist/bisync.js"
12
+ "build": "bun build --target bun src/bin.ts --outfile dist/bisync.js"
13
13
  },
14
14
  "devDependencies": {
15
15
  "@types/bun": "^1.3.5"