@synkro-sh/cli 1.7.61 → 1.7.63

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/bootstrap.js CHANGED
@@ -15,11 +15,1316 @@ var __export = (target, all) => {
15
15
  __defProp(target, name, { get: all[name], enumerable: true });
16
16
  };
17
17
 
18
+ // cli/telemetry/metaCache.ts
19
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "fs";
20
+ import { homedir } from "os";
21
+ import { join } from "path";
22
+ import { randomUUID } from "crypto";
23
+ function ensureDir() {
24
+ if (!existsSync(SYNKRO_DIR)) {
25
+ mkdirSync(SYNKRO_DIR, { recursive: true, mode: 448 });
26
+ }
27
+ }
28
+ function readMetaCache() {
29
+ if (cached) return cached;
30
+ let meta = { ...DEFAULTS };
31
+ if (existsSync(META_PATH)) {
32
+ try {
33
+ const raw = readFileSync(META_PATH, "utf-8");
34
+ const parsed = JSON.parse(raw);
35
+ if (parsed && typeof parsed === "object") {
36
+ meta = { ...DEFAULTS, ...parsed };
37
+ }
38
+ } catch {
39
+ }
40
+ }
41
+ if (!meta.install_id) {
42
+ meta.install_id = randomUUID();
43
+ writeMetaCache(meta);
44
+ }
45
+ cached = meta;
46
+ return cached;
47
+ }
48
+ function writeMetaCache(meta) {
49
+ try {
50
+ ensureDir();
51
+ const tmp = `${META_PATH}.tmp`;
52
+ writeFileSync(tmp, JSON.stringify(meta, null, 2), { mode: 384 });
53
+ renameSync(tmp, META_PATH);
54
+ cached = { ...meta };
55
+ } catch {
56
+ }
57
+ }
58
+ function patchMetaCache(patch) {
59
+ const current = readMetaCache();
60
+ const next = { ...current, ...patch };
61
+ writeMetaCache(next);
62
+ return next;
63
+ }
64
+ var SYNKRO_DIR, META_PATH, DEFAULTS, cached;
65
+ var init_metaCache = __esm({
66
+ "cli/telemetry/metaCache.ts"() {
67
+ "use strict";
68
+ SYNKRO_DIR = join(homedir(), ".synkro");
69
+ META_PATH = join(SYNKRO_DIR, "telemetry-meta.json");
70
+ DEFAULTS = {
71
+ install_id: "",
72
+ enabled: true,
73
+ remote_flush_enabled: true
74
+ };
75
+ cached = null;
76
+ }
77
+ });
78
+
79
+ // cli/telemetry/identity.ts
80
+ import { createHash } from "crypto";
81
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
82
+ import { homedir as homedir2, hostname, platform } from "os";
83
+ import { join as join2 } from "path";
84
+ function decodeJwtPayload(token) {
85
+ const parts = token.split(".");
86
+ if (parts.length < 2) return null;
87
+ try {
88
+ const json = Buffer.from(parts[1], "base64url").toString("utf-8");
89
+ const parsed = JSON.parse(json);
90
+ if (parsed && typeof parsed === "object") return parsed;
91
+ return null;
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+ function loadCredentialsIdentity() {
97
+ if (!existsSync2(CREDS_PATH)) return {};
98
+ let raw;
99
+ try {
100
+ raw = readFileSync2(CREDS_PATH, "utf-8");
101
+ } catch {
102
+ return {};
103
+ }
104
+ let creds;
105
+ try {
106
+ creds = JSON.parse(raw);
107
+ } catch {
108
+ return {};
109
+ }
110
+ const out = {};
111
+ if (creds.user_id) out.user_id = creds.user_id;
112
+ if (creds.org_id) out.org_id = creds.org_id;
113
+ if (creds.email) out.email = creds.email;
114
+ if (creds.access_token && (!out.user_id || !out.org_id || !out.email)) {
115
+ const claims = decodeJwtPayload(creds.access_token);
116
+ if (claims) {
117
+ if (!out.user_id && claims.sub) out.user_id = claims.sub;
118
+ if (!out.org_id && claims.org_id) out.org_id = claims.org_id;
119
+ if (!out.email && claims.email) out.email = claims.email;
120
+ }
121
+ }
122
+ return out;
123
+ }
124
+ function hostnameHash() {
125
+ try {
126
+ return createHash("sha256").update(hostname()).digest("hex").slice(0, 16);
127
+ } catch {
128
+ return "";
129
+ }
130
+ }
131
+ function getIdentity() {
132
+ if (cached2) return cached2;
133
+ let cliVersion = "0.0.0";
134
+ try {
135
+ cliVersion = "1.7.63";
136
+ } catch {
137
+ }
138
+ const creds = loadCredentialsIdentity();
139
+ const meta = readMetaCache();
140
+ cached2 = {
141
+ install_id: meta.install_id,
142
+ user_id: creds.user_id,
143
+ org_id: creds.org_id,
144
+ email: creds.email,
145
+ cli_version: cliVersion,
146
+ platform: platform(),
147
+ hostname_hash: hostnameHash(),
148
+ node_version: process.version
149
+ };
150
+ return cached2;
151
+ }
152
+ var CREDS_PATH, cached2;
153
+ var init_identity = __esm({
154
+ "cli/telemetry/identity.ts"() {
155
+ "use strict";
156
+ init_metaCache();
157
+ CREDS_PATH = process.env.SYNKRO_CREDENTIALS_PATH || join2(homedir2(), ".synkro", "credentials.json");
158
+ cached2 = null;
159
+ }
160
+ });
161
+
162
+ // cli/telemetry/emit.ts
163
+ import { randomUUID as randomUUID2 } from "crypto";
164
+ import { execFileSync } from "child_process";
165
+ import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
166
+ import { homedir as homedir3 } from "os";
167
+ import { join as join3 } from "path";
168
+ function deriveGit(cwd) {
169
+ const cached4 = gitCache.get(cwd);
170
+ if (cached4) return cached4;
171
+ const info = { branch: null, sha: null };
172
+ try {
173
+ const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
174
+ cwd,
175
+ stdio: ["ignore", "pipe", "ignore"],
176
+ timeout: 500,
177
+ encoding: "utf-8"
178
+ }).trim();
179
+ if (branch) info.branch = branch;
180
+ } catch {
181
+ }
182
+ try {
183
+ const sha = execFileSync("git", ["rev-parse", "HEAD"], {
184
+ cwd,
185
+ stdio: ["ignore", "pipe", "ignore"],
186
+ timeout: 500,
187
+ encoding: "utf-8"
188
+ }).trim();
189
+ if (sha) info.sha = sha;
190
+ } catch {
191
+ }
192
+ gitCache.set(cwd, info);
193
+ return info;
194
+ }
195
+ function ensureDir2() {
196
+ if (!existsSync3(SYNKRO_DIR2)) {
197
+ mkdirSync2(SYNKRO_DIR2, { recursive: true, mode: 448 });
198
+ }
199
+ }
200
+ function redactContext(context) {
201
+ if (!context || typeof context !== "object" || Array.isArray(context)) return context;
202
+ const c = context;
203
+ if (!("reasoning" in c) && !("raw_response" in c) && typeof c.reason !== "string") return context;
204
+ const out = { ...c };
205
+ delete out.reasoning;
206
+ delete out.raw_response;
207
+ if (typeof out.reason === "string" && out.reason.length > REASON_CODE_MAX) {
208
+ out.reason = out.reason.slice(0, REASON_CODE_MAX);
209
+ }
210
+ return out;
211
+ }
212
+ function emit(eventType, context, opts) {
213
+ try {
214
+ const meta = readMetaCache();
215
+ if (!meta.enabled) return;
216
+ const identity = getIdentity();
217
+ const cwd = opts?.cwd ?? process.cwd();
218
+ const git = deriveGit(cwd);
219
+ const emitter = process.env.SYNKRO_TELEMETRY_EMITTER || "bootstrap";
220
+ const row = {
221
+ client_event_id: randomUUID2(),
222
+ event_type: eventType,
223
+ occurred_at: (/* @__PURE__ */ new Date()).toISOString(),
224
+ install_id: identity.install_id,
225
+ user_id: identity.user_id ?? null,
226
+ org_id: identity.org_id ?? null,
227
+ email: identity.email ?? null,
228
+ cli_version: identity.cli_version,
229
+ emitter,
230
+ agent_kind: opts?.agent_kind ?? null,
231
+ cc_session_id: opts?.cc_session_id ?? null,
232
+ cc_tool_use_id: opts?.cc_tool_use_id ?? null,
233
+ cc_turn_id: opts?.cc_turn_id ?? null,
234
+ cc_model: opts?.cc_model ?? null,
235
+ permission_mode: opts?.permission_mode ?? null,
236
+ cwd,
237
+ repo: opts?.repo ?? null,
238
+ git_branch: git.branch,
239
+ git_sha: git.sha,
240
+ policy_name: opts?.policy_name ?? null,
241
+ capture_depth: opts?.capture_depth ?? null,
242
+ platform: identity.platform,
243
+ hostname_hash: identity.hostname_hash || null,
244
+ node_version: identity.node_version,
245
+ context: redactContext(context)
246
+ };
247
+ ensureDir2();
248
+ appendFileSync(PENDING_PATH, JSON.stringify(row) + "\n", { mode: 384 });
249
+ } catch (err) {
250
+ const msg = err instanceof Error ? err.message : String(err);
251
+ process.stderr.write(`[synkro] telemetry emit(${eventType}) failed: ${msg}
252
+ `);
253
+ }
254
+ }
255
+ var SYNKRO_DIR2, PENDING_PATH, gitCache, REASON_CODE_MAX;
256
+ var init_emit = __esm({
257
+ "cli/telemetry/emit.ts"() {
258
+ "use strict";
259
+ init_identity();
260
+ init_metaCache();
261
+ SYNKRO_DIR2 = join3(homedir3(), ".synkro");
262
+ PENDING_PATH = join3(SYNKRO_DIR2, "telemetry-pending.jsonl");
263
+ gitCache = /* @__PURE__ */ new Map();
264
+ REASON_CODE_MAX = 200;
265
+ }
266
+ });
267
+
268
+ // cli/telemetry/db.ts
269
+ var db_exports = {};
270
+ __export(db_exports, {
271
+ closeDb: () => closeDb,
272
+ connectDb: () => connectDb,
273
+ resetDbForTests: () => resetDbForTests
274
+ });
275
+ import postgres from "postgres";
276
+ async function connectDb() {
277
+ if (cached3) return cached3;
278
+ if (initFailedAt && Date.now() - initFailedAt < INIT_RETRY_MS) return null;
279
+ try {
280
+ const sql = postgres({
281
+ host: PGLITE_HOST,
282
+ port: PGLITE_PORT,
283
+ username: PGLITE_USER,
284
+ database: PGLITE_DB,
285
+ // max:1 mirrors synkro-server — PGlite is single-threaded; multiple
286
+ // sockets just queue at the multiplexer.
287
+ max: 1,
288
+ connect_timeout: 2,
289
+ idle_timeout: 30,
290
+ onnotice: () => {
291
+ }
292
+ });
293
+ await sql`SELECT 1`;
294
+ cached3 = sql;
295
+ initFailedAt = 0;
296
+ if (!migrated) await runMigrations(sql);
297
+ return cached3;
298
+ } catch (err) {
299
+ initFailedAt = Date.now();
300
+ if (process.env.SYNKRO_TELEMETRY_DEBUG === "1") {
301
+ const msg = err instanceof Error ? err.message : String(err);
302
+ process.stderr.write(`[synkro] telemetry pglite unreachable at ${PGLITE_HOST}:${PGLITE_PORT}: ${msg}
303
+ `);
304
+ }
305
+ return null;
306
+ }
307
+ }
308
+ async function runMigrations(sql) {
309
+ try {
310
+ await sql.unsafe(MIGRATION_V1);
311
+ const rows = await sql`SELECT v FROM telemetry_meta WHERE k = 'schema_version'`;
312
+ const current = rows.length ? Number(rows[0].v) : 0;
313
+ if (current !== SCHEMA_VERSION) {
314
+ await sql`
315
+ INSERT INTO telemetry_meta (k, v) VALUES ('schema_version', ${String(SCHEMA_VERSION)})
316
+ ON CONFLICT (k) DO UPDATE SET v = EXCLUDED.v
317
+ `;
318
+ }
319
+ migrated = true;
320
+ } catch (err) {
321
+ const msg = err instanceof Error ? err.message : String(err);
322
+ process.stderr.write(`[synkro] telemetry migration failed: ${msg}
323
+ `);
324
+ }
325
+ }
326
+ async function closeDb() {
327
+ if (!cached3) return;
328
+ try {
329
+ await cached3.end({ timeout: 1 });
330
+ } catch {
331
+ }
332
+ cached3 = null;
333
+ }
334
+ function resetDbForTests() {
335
+ cached3 = null;
336
+ initFailedAt = 0;
337
+ migrated = false;
338
+ }
339
+ var PGLITE_HOST, PGLITE_PORT, PGLITE_USER, PGLITE_DB, SCHEMA_VERSION, cached3, migrated, INIT_RETRY_MS, initFailedAt, MIGRATION_V1;
340
+ var init_db = __esm({
341
+ "cli/telemetry/db.ts"() {
342
+ "use strict";
343
+ PGLITE_HOST = process.env.SYNKRO_PGLITE_HOST || "127.0.0.1";
344
+ PGLITE_PORT = parseInt(
345
+ process.env.SYNKRO_PGLITE_PORT || process.env.SYNKRO_HOST_PGLITE_PORT || "15433",
346
+ 10
347
+ );
348
+ PGLITE_USER = process.env.SYNKRO_PGLITE_USER || "postgres";
349
+ PGLITE_DB = process.env.SYNKRO_PGLITE_DB || "postgres";
350
+ SCHEMA_VERSION = 1;
351
+ cached3 = null;
352
+ migrated = false;
353
+ INIT_RETRY_MS = 3e4;
354
+ initFailedAt = 0;
355
+ MIGRATION_V1 = `
356
+ CREATE TABLE IF NOT EXISTS telemetry_events (
357
+ id BIGSERIAL PRIMARY KEY,
358
+ client_event_id TEXT NOT NULL UNIQUE,
359
+
360
+ event_type TEXT NOT NULL,
361
+ occurred_at TIMESTAMPTZ NOT NULL,
362
+
363
+ install_id TEXT NOT NULL,
364
+ user_id TEXT,
365
+ org_id TEXT,
366
+ email TEXT,
367
+
368
+ cli_version TEXT NOT NULL,
369
+ emitter TEXT NOT NULL,
370
+
371
+ agent_kind TEXT,
372
+ cc_session_id TEXT,
373
+ cc_tool_use_id TEXT,
374
+ cc_turn_id TEXT,
375
+ cc_model TEXT,
376
+ permission_mode TEXT,
377
+
378
+ cwd TEXT,
379
+ repo TEXT,
380
+ git_branch TEXT,
381
+ git_sha TEXT,
382
+
383
+ policy_name TEXT,
384
+ capture_depth TEXT,
385
+
386
+ platform TEXT NOT NULL,
387
+ hostname_hash TEXT,
388
+ node_version TEXT,
389
+
390
+ context JSONB NOT NULL
391
+ );
392
+
393
+ CREATE INDEX IF NOT EXISTS telemetry_events_by_type_time
394
+ ON telemetry_events (event_type, occurred_at DESC);
395
+ CREATE INDEX IF NOT EXISTS telemetry_events_by_session
396
+ ON telemetry_events (cc_session_id, occurred_at) WHERE cc_session_id IS NOT NULL;
397
+
398
+ CREATE TABLE IF NOT EXISTS telemetry_meta (
399
+ k TEXT PRIMARY KEY,
400
+ v TEXT NOT NULL
401
+ );
402
+
403
+ CREATE TABLE IF NOT EXISTS sessions_command_history (
404
+ id BIGSERIAL PRIMARY KEY,
405
+ command TEXT NOT NULL,
406
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
407
+ );
408
+
409
+ CREATE TABLE IF NOT EXISTS sessions_project_keys (
410
+ slug TEXT PRIMARY KEY,
411
+ project_id TEXT NOT NULL,
412
+ project_name TEXT NOT NULL,
413
+ api_key TEXT NOT NULL,
414
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
415
+ );
416
+ `;
417
+ }
418
+ });
419
+
420
+ // cli/telemetry/drain.ts
421
+ import { openSync, closeSync, fstatSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2, appendFileSync as appendFileSync2, unlinkSync, mkdirSync as mkdirSync3, renameSync as renameSync2, existsSync as existsSync4 } from "fs";
422
+ import { homedir as homedir4 } from "os";
423
+ import { join as join4 } from "path";
424
+ function ensureDir3() {
425
+ if (!existsSync4(SYNKRO_DIR3)) mkdirSync3(SYNKRO_DIR3, { recursive: true, mode: 448 });
426
+ }
427
+ function acquireQueueLock() {
428
+ ensureDir3();
429
+ try {
430
+ return openSync(QUEUE_LOCK, "wx");
431
+ } catch {
432
+ let fd;
433
+ try {
434
+ fd = openSync(QUEUE_LOCK, "r");
435
+ } catch {
436
+ try {
437
+ return openSync(QUEUE_LOCK, "wx");
438
+ } catch {
439
+ return null;
440
+ }
441
+ }
442
+ let stale = false;
443
+ try {
444
+ stale = Date.now() - fstatSync(fd).mtimeMs > STALE_LOCK_MS;
445
+ } catch {
446
+ }
447
+ try {
448
+ closeSync(fd);
449
+ } catch {
450
+ }
451
+ if (!stale) return null;
452
+ try {
453
+ unlinkSync(QUEUE_LOCK);
454
+ } catch {
455
+ }
456
+ try {
457
+ return openSync(QUEUE_LOCK, "wx");
458
+ } catch {
459
+ return null;
460
+ }
461
+ }
462
+ }
463
+ function releaseQueueLock(fd) {
464
+ try {
465
+ closeSync(fd);
466
+ } catch {
467
+ }
468
+ try {
469
+ unlinkSync(QUEUE_LOCK);
470
+ } catch {
471
+ }
472
+ }
473
+ function parseQueueFile(path) {
474
+ if (!existsSync4(path)) return { rows: [], malformed: 0 };
475
+ let raw;
476
+ try {
477
+ raw = readFileSync3(path, "utf-8");
478
+ } catch {
479
+ return { rows: [], malformed: 0 };
480
+ }
481
+ if (!raw) return { rows: [], malformed: 0 };
482
+ const rows = [];
483
+ let malformed = 0;
484
+ for (const line of raw.split("\n")) {
485
+ if (!line.trim()) continue;
486
+ try {
487
+ const r = JSON.parse(line);
488
+ if (r && r.client_event_id && r.event_type) rows.push(r);
489
+ else malformed++;
490
+ } catch {
491
+ malformed++;
492
+ }
493
+ }
494
+ return { rows, malformed };
495
+ }
496
+ function readQueue() {
497
+ return parseQueueFile(PENDING_PATH);
498
+ }
499
+ function rewriteQueue(rows) {
500
+ ensureDir3();
501
+ const body = rows.length ? rows.map((r) => JSON.stringify(r)).join("\n") + "\n" : "";
502
+ const tmp = `${PENDING_PATH}.tmp`;
503
+ try {
504
+ writeFileSync2(tmp, body, { mode: 384 });
505
+ renameSync2(tmp, PENDING_PATH);
506
+ } catch (err) {
507
+ const msg = err instanceof Error ? err.message : String(err);
508
+ process.stderr.write(`[synkro] telemetry queue rewrite failed: ${msg}
509
+ `);
510
+ }
511
+ }
512
+ function recoverProcessing() {
513
+ if (!existsSync4(PROCESSING_PATH)) return;
514
+ ensureDir3();
515
+ let body;
516
+ try {
517
+ body = readFileSync3(PROCESSING_PATH, "utf-8");
518
+ } catch {
519
+ return;
520
+ }
521
+ try {
522
+ if (body.trim()) appendFileSync2(PENDING_PATH, body.endsWith("\n") ? body : body + "\n", { mode: 384 });
523
+ unlinkSync(PROCESSING_PATH);
524
+ } catch (err) {
525
+ const msg = err instanceof Error ? err.message : String(err);
526
+ process.stderr.write(`[synkro] telemetry queue recovery failed: ${msg}
527
+ `);
528
+ }
529
+ }
530
+ function rotateQueueForProcessing() {
531
+ ensureDir3();
532
+ if (!existsSync4(PENDING_PATH)) return { rows: [], malformed: 0 };
533
+ try {
534
+ renameSync2(PENDING_PATH, PROCESSING_PATH);
535
+ } catch {
536
+ return { rows: [], malformed: 0 };
537
+ }
538
+ const { rows, malformed } = parseQueueFile(PROCESSING_PATH);
539
+ if (rows.length > MAX_QUEUE_ROWS) {
540
+ const dropped = rows.length - MAX_QUEUE_ROWS;
541
+ process.stderr.write(`[synkro] telemetry queue over ${MAX_QUEUE_ROWS}-row cap: dropping ${dropped} oldest event(s)
542
+ `);
543
+ return { rows: rows.slice(dropped), malformed };
544
+ }
545
+ return { rows, malformed };
546
+ }
547
+ function finalizeProcessing(survivors) {
548
+ if (survivors.length) {
549
+ ensureDir3();
550
+ const body = survivors.map((r) => JSON.stringify(r)).join("\n") + "\n";
551
+ try {
552
+ appendFileSync2(PENDING_PATH, body, { mode: 384 });
553
+ } catch (err) {
554
+ const msg = err instanceof Error ? err.message : String(err);
555
+ process.stderr.write(`[synkro] telemetry survivor re-append failed: ${msg}
556
+ `);
557
+ return;
558
+ }
559
+ }
560
+ try {
561
+ unlinkSync(PROCESSING_PATH);
562
+ } catch {
563
+ }
564
+ }
565
+ async function insertEvents(sql, rows, mirroredIds) {
566
+ let inserted = 0;
567
+ for (let i = 0; i < rows.length; i += CHUNK) {
568
+ const batch = rows.slice(i, i + CHUNK);
569
+ const payload = batch.map((r) => ({
570
+ client_event_id: r.client_event_id,
571
+ event_type: r.event_type,
572
+ occurred_at: r.occurred_at,
573
+ install_id: r.install_id,
574
+ user_id: r.user_id,
575
+ org_id: r.org_id,
576
+ email: r.email,
577
+ cli_version: r.cli_version,
578
+ emitter: r.emitter,
579
+ agent_kind: r.agent_kind,
580
+ cc_session_id: r.cc_session_id,
581
+ cc_tool_use_id: r.cc_tool_use_id,
582
+ cc_turn_id: r.cc_turn_id,
583
+ cc_model: r.cc_model,
584
+ permission_mode: r.permission_mode,
585
+ cwd: r.cwd,
586
+ repo: r.repo,
587
+ git_branch: r.git_branch,
588
+ git_sha: r.git_sha,
589
+ policy_name: r.policy_name,
590
+ capture_depth: r.capture_depth,
591
+ platform: r.platform,
592
+ hostname_hash: r.hostname_hash,
593
+ node_version: r.node_version,
594
+ // Pass the object, NOT a pre-stringified string: postgres.js JSON-encodes
595
+ // jsonb-bound values itself; stringifying would double-encode into a jsonb
596
+ // string scalar (context->>'k' would then return null for every key).
597
+ // Cast narrows `unknown ?? {}` (which TS widens to the universal `{}`) to a
598
+ // concrete JSON object so postgres.js's helper accepts it as a jsonb value.
599
+ context: r.context ?? {}
600
+ }));
601
+ try {
602
+ const res = await sql`
603
+ INSERT INTO telemetry_events ${sql(
604
+ payload,
605
+ "client_event_id",
606
+ "event_type",
607
+ "occurred_at",
608
+ "install_id",
609
+ "user_id",
610
+ "org_id",
611
+ "email",
612
+ "cli_version",
613
+ "emitter",
614
+ "agent_kind",
615
+ "cc_session_id",
616
+ "cc_tool_use_id",
617
+ "cc_turn_id",
618
+ "cc_model",
619
+ "permission_mode",
620
+ "cwd",
621
+ "repo",
622
+ "git_branch",
623
+ "git_sha",
624
+ "policy_name",
625
+ "capture_depth",
626
+ "platform",
627
+ "hostname_hash",
628
+ "node_version",
629
+ "context"
630
+ )}
631
+ ON CONFLICT (client_event_id) DO NOTHING
632
+ `;
633
+ inserted += res.count ?? batch.length;
634
+ if (mirroredIds) for (const r of batch) mirroredIds.add(r.client_event_id);
635
+ } catch (err) {
636
+ const msg = err instanceof Error ? err.message : String(err);
637
+ process.stderr.write(`[synkro] telemetry local mirror chunk failed (skipped ${batch.length}): ${msg}
638
+ `);
639
+ }
640
+ }
641
+ return inserted;
642
+ }
643
+ async function drainToPglite() {
644
+ const { rows } = readQueue();
645
+ if (rows.length === 0) return { ok: true, ingested: 0, pending: 0, reason: "no_pending" };
646
+ const sql = await connectDb();
647
+ if (!sql) return { ok: false, ingested: 0, pending: rows.length, reason: "db_unavailable" };
648
+ try {
649
+ const ingested = await insertEvents(sql, rows);
650
+ patchMetaCache({ last_drained_at: (/* @__PURE__ */ new Date()).toISOString() });
651
+ return { ok: true, ingested, pending: rows.length };
652
+ } catch (err) {
653
+ const msg = err instanceof Error ? err.message : String(err);
654
+ return { ok: false, ingested: 0, pending: rows.length, reason: "error", error: msg };
655
+ }
656
+ }
657
+ var SYNKRO_DIR3, QUEUE_LOCK, PROCESSING_PATH, CHUNK, STALE_LOCK_MS, MAX_QUEUE_ROWS;
658
+ var init_drain = __esm({
659
+ "cli/telemetry/drain.ts"() {
660
+ "use strict";
661
+ init_db();
662
+ init_emit();
663
+ init_metaCache();
664
+ SYNKRO_DIR3 = join4(homedir4(), ".synkro");
665
+ QUEUE_LOCK = join4(SYNKRO_DIR3, ".telemetry-queue.lock");
666
+ PROCESSING_PATH = `${PENDING_PATH}.processing`;
667
+ CHUNK = 500;
668
+ STALE_LOCK_MS = 6e4;
669
+ MAX_QUEUE_ROWS = 5e4;
670
+ }
671
+ });
672
+
673
+ // cli/telemetry/optout.ts
674
+ import { chmodSync, existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
675
+ import { homedir as homedir5 } from "os";
676
+ import { join as join5 } from "path";
677
+ function sanitize(raw, maxLen = 256) {
678
+ return raw.replace(/[^\x20-\x7E]/g, "").slice(0, maxLen);
679
+ }
680
+ function shellQuoteSingle(value) {
681
+ return `'${value.replace(/'/g, "'\\''")}'`;
682
+ }
683
+ function writeConfigEnvFlag(key, value) {
684
+ const safe = sanitize(value, 8);
685
+ const line = `${key}=${shellQuoteSingle(safe)}`;
686
+ let content = "";
687
+ if (existsSync5(CONFIG_PATH)) {
688
+ try {
689
+ content = readFileSync4(CONFIG_PATH, "utf-8");
690
+ } catch {
691
+ content = "";
692
+ }
693
+ }
694
+ const re = new RegExp(`^${key}=.*$`, "m");
695
+ if (re.test(content)) {
696
+ content = content.replace(re, line);
697
+ } else {
698
+ if (content.length > 0 && !content.endsWith("\n")) content += "\n";
699
+ content += `${line}
700
+ `;
701
+ }
702
+ try {
703
+ writeFileSync3(CONFIG_PATH, content, "utf-8");
704
+ try {
705
+ chmodSync(CONFIG_PATH, 384);
706
+ } catch {
707
+ }
708
+ } catch (err) {
709
+ const msg = err instanceof Error ? err.message : String(err);
710
+ process.stderr.write(`[synkro] telemetry config write failed: ${msg}
711
+ `);
712
+ }
713
+ }
714
+ async function getTelemetryState() {
715
+ const meta = readMetaCache();
716
+ return {
717
+ enabled: meta.enabled,
718
+ remoteFlushEnabled: meta.remote_flush_enabled
719
+ };
720
+ }
721
+ function getTelemetryStateSync() {
722
+ const meta = readMetaCache();
723
+ return {
724
+ enabled: meta.enabled,
725
+ remoteFlushEnabled: meta.remote_flush_enabled
726
+ };
727
+ }
728
+ async function setTelemetryState(opts) {
729
+ const current = await getTelemetryState();
730
+ const patch = {};
731
+ if (opts.enabled !== void 0 && opts.enabled !== current.enabled) {
732
+ emit("config_change", {
733
+ setting: "telemetry_enabled",
734
+ from: current.enabled ? "yes" : "no",
735
+ to: opts.enabled ? "yes" : "no",
736
+ source: "cli"
737
+ });
738
+ patch.enabled = opts.enabled;
739
+ writeConfigEnvFlag(KEY_ENABLED, opts.enabled ? "yes" : "no");
740
+ }
741
+ if (opts.remoteFlushEnabled !== void 0 && opts.remoteFlushEnabled !== current.remoteFlushEnabled) {
742
+ emit("config_change", {
743
+ setting: "remote_flush_enabled",
744
+ from: current.remoteFlushEnabled ? "yes" : "no",
745
+ to: opts.remoteFlushEnabled ? "yes" : "no",
746
+ source: "cli"
747
+ });
748
+ patch.remote_flush_enabled = opts.remoteFlushEnabled;
749
+ writeConfigEnvFlag(KEY_REMOTE, opts.remoteFlushEnabled ? "yes" : "no");
750
+ }
751
+ if (Object.keys(patch).length > 0) patchMetaCache(patch);
752
+ }
753
+ var SYNKRO_DIR4, CONFIG_PATH, KEY_ENABLED, KEY_REMOTE;
754
+ var init_optout = __esm({
755
+ "cli/telemetry/optout.ts"() {
756
+ "use strict";
757
+ init_emit();
758
+ init_metaCache();
759
+ SYNKRO_DIR4 = join5(homedir5(), ".synkro");
760
+ CONFIG_PATH = join5(SYNKRO_DIR4, "config.env");
761
+ KEY_ENABLED = "SYNKRO_TELEMETRY_ENABLED";
762
+ KEY_REMOTE = "SYNKRO_TELEMETRY_REMOTE_FLUSH";
763
+ }
764
+ });
765
+
766
+ // cli/telemetry/wire.ts
767
+ function isUnrecoverable(status) {
768
+ return UNRECOVERABLE_STATUSES.includes(status);
769
+ }
770
+ function batchBySizeAndCount(rows, sizeOf, opts) {
771
+ const limit = opts?.limit ?? BATCH_LIMIT;
772
+ const maxBytes = opts?.maxBytes ?? BATCH_BYTES;
773
+ const batches = [];
774
+ let cur = [];
775
+ let bytes = 0;
776
+ for (const r of rows) {
777
+ const sz = sizeOf(r);
778
+ if (cur.length > 0 && (cur.length >= limit || bytes + sz > maxBytes)) {
779
+ batches.push(cur);
780
+ cur = [];
781
+ bytes = 0;
782
+ }
783
+ cur.push(r);
784
+ bytes += sz;
785
+ }
786
+ if (cur.length) batches.push(cur);
787
+ return batches;
788
+ }
789
+ var TELEMETRY_FLUSH_PATH, BATCH_LIMIT, BATCH_BYTES, REQUEST_TIMEOUT_MS, TELEMETRY_BODY_MAX, UNRECOVERABLE_STATUSES;
790
+ var init_wire = __esm({
791
+ "cli/telemetry/wire.ts"() {
792
+ "use strict";
793
+ TELEMETRY_FLUSH_PATH = "/api/v1/telemetry/flush";
794
+ BATCH_LIMIT = 200;
795
+ BATCH_BYTES = 1024 * 1024;
796
+ REQUEST_TIMEOUT_MS = 15e3;
797
+ TELEMETRY_BODY_MAX = 4 * 1024 * 1024;
798
+ UNRECOVERABLE_STATUSES = [400, 413, 422];
799
+ }
800
+ });
801
+
802
+ // cli/telemetry/flush.ts
803
+ var flush_exports = {};
804
+ __export(flush_exports, {
805
+ flush: () => flush,
806
+ flushDetached: () => flushDetached
807
+ });
808
+ import { spawn } from "child_process";
809
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
810
+ import { homedir as homedir6 } from "os";
811
+ import { join as join6 } from "path";
812
+ function readConfigEnv() {
813
+ if (!existsSync6(CONFIG_PATH2)) return {};
814
+ const out = {};
815
+ let raw;
816
+ try {
817
+ raw = readFileSync5(CONFIG_PATH2, "utf-8");
818
+ } catch {
819
+ return {};
820
+ }
821
+ for (const line of raw.split("\n")) {
822
+ const t = line.trim();
823
+ if (!t || t.startsWith("#")) continue;
824
+ const eq = t.indexOf("=");
825
+ if (eq <= 0) continue;
826
+ const k = t.slice(0, eq).trim();
827
+ let v = t.slice(eq + 1).trim();
828
+ if (v.startsWith("'") && v.endsWith("'") || v.startsWith('"') && v.endsWith('"')) v = v.slice(1, -1);
829
+ out[k] = v;
830
+ }
831
+ return out;
832
+ }
833
+ function isLocalhost(host) {
834
+ return host === "localhost" || host === "127.0.0.1" || host === "::1";
835
+ }
836
+ function isGatewayHostAllowed(host) {
837
+ const h = host.toLowerCase();
838
+ return isLocalhost(h) || h === "synkro.sh" || h.endsWith(".synkro.sh");
839
+ }
840
+ function resolveGatewayUrl() {
841
+ const cfg = readConfigEnv();
842
+ const raw = (cfg.SYNKRO_GATEWAY_URL || process.env.SYNKRO_GATEWAY_URL || "").trim();
843
+ if (!raw) return DEFAULT_GATEWAY;
844
+ let u;
845
+ try {
846
+ u = new URL(raw);
847
+ } catch {
848
+ return DEFAULT_GATEWAY;
849
+ }
850
+ if (!isGatewayHostAllowed(u.hostname)) return DEFAULT_GATEWAY;
851
+ if (u.protocol === "https:") return raw.replace(/\/$/, "");
852
+ if (u.protocol === "http:" && isLocalhost(u.hostname)) return raw.replace(/\/$/, "");
853
+ return DEFAULT_GATEWAY;
854
+ }
855
+ function loadJwt() {
856
+ if ((process.env.SYNKRO_STORAGE_MODE || "") === "cloud") {
857
+ try {
858
+ const mcp = readFileSync5(join6(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
859
+ if (mcp) return mcp;
860
+ } catch {
861
+ }
862
+ }
863
+ try {
864
+ if (!existsSync6(CREDS_PATH2)) return null;
865
+ const creds = JSON.parse(readFileSync5(CREDS_PATH2, "utf-8"));
866
+ return creds.access_token || null;
867
+ } catch {
868
+ return null;
869
+ }
870
+ }
871
+ function rowToWire(r) {
872
+ return {
873
+ client_event_id: r.client_event_id,
874
+ event_type: r.event_type,
875
+ occurred_at: r.occurred_at,
876
+ install_id: r.install_id,
877
+ user_id: r.user_id,
878
+ org_id: r.org_id,
879
+ email: r.email,
880
+ cli_version: r.cli_version,
881
+ emitter: r.emitter,
882
+ agent_kind: r.agent_kind,
883
+ cc_session_id: r.cc_session_id,
884
+ cc_tool_use_id: r.cc_tool_use_id,
885
+ cc_turn_id: r.cc_turn_id,
886
+ cc_model: r.cc_model,
887
+ permission_mode: r.permission_mode,
888
+ cwd: r.cwd,
889
+ repo: r.repo,
890
+ git_branch: r.git_branch,
891
+ git_sha: r.git_sha,
892
+ policy_name: r.policy_name,
893
+ capture_depth: r.capture_depth,
894
+ platform: r.platform,
895
+ hostname_hash: r.hostname_hash,
896
+ node_version: r.node_version,
897
+ context: r.context ?? {}
898
+ };
899
+ }
900
+ function batchRows(rows) {
901
+ return batchBySizeAndCount(rows, (r) => JSON.stringify(rowToWire(r)).length);
902
+ }
903
+ async function flush(opts) {
904
+ const state = await getTelemetryState();
905
+ if (!state.enabled) return { sent: 0, failed: 0, mirrored: 0, skipped_reason: "disabled" };
906
+ const lockFd = acquireQueueLock();
907
+ if (lockFd === null) return { sent: 0, failed: 0, mirrored: 0, skipped_reason: "locked" };
908
+ try {
909
+ recoverProcessing();
910
+ const { rows } = rotateQueueForProcessing();
911
+ if (rows.length === 0) {
912
+ finalizeProcessing([]);
913
+ return { sent: 0, failed: 0, mirrored: 0, skipped_reason: "no_pending" };
914
+ }
915
+ const jwt2 = loadJwt();
916
+ const remoteEnabled = state.remoteFlushEnabled && !!jwt2;
917
+ const meta = readMetaCache();
918
+ let throttled = false;
919
+ if (!opts?.force && meta.last_flush_ok_at) {
920
+ const ms = Date.parse(meta.last_flush_ok_at);
921
+ if (Number.isFinite(ms) && Date.now() - ms < THROTTLE_MS) throttled = true;
922
+ }
923
+ const doRemote = remoteEnabled && !throttled;
924
+ const sql = await connectDb();
925
+ const pgliteMirrored = /* @__PURE__ */ new Set();
926
+ let mirrored = 0;
927
+ if (sql) {
928
+ try {
929
+ mirrored = await insertEvents(sql, rows, pgliteMirrored);
930
+ } catch (err) {
931
+ const m = err instanceof Error ? err.message : String(err);
932
+ process.stderr.write(`[synkro] telemetry local mirror failed: ${m}
933
+ `);
934
+ }
935
+ }
936
+ const remoteResolved = /* @__PURE__ */ new Set();
937
+ let sent = 0;
938
+ let remoteErr = "";
939
+ if (doRemote) {
940
+ const endpoint = `${resolveGatewayUrl()}${TELEMETRY_FLUSH_PATH}`;
941
+ const identity = getIdentity();
942
+ for (const batch of batchRows(rows)) {
943
+ const body = JSON.stringify({
944
+ install_id: identity.install_id,
945
+ cli_version: identity.cli_version,
946
+ platform: identity.platform,
947
+ events: batch.map(rowToWire)
948
+ });
949
+ let resp = null;
950
+ try {
951
+ resp = await fetch(endpoint, {
952
+ method: "POST",
953
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${jwt2}` },
954
+ body,
955
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
956
+ });
957
+ } catch (err) {
958
+ remoteErr = err instanceof Error ? err.message : String(err);
959
+ break;
960
+ }
961
+ if (resp.ok) {
962
+ for (const r of batch) remoteResolved.add(r.client_event_id);
963
+ sent += batch.length;
964
+ } else if (isUnrecoverable(resp.status)) {
965
+ for (const r of batch) remoteResolved.add(r.client_event_id);
966
+ remoteErr = `HTTP ${resp.status} (dropped ${batch.length})`;
967
+ process.stderr.write(`[synkro] telemetry flush dropped ${batch.length} unrecoverable event(s): ${remoteErr}
968
+ `);
969
+ } else {
970
+ remoteErr = `HTTP ${resp.status}`;
971
+ break;
972
+ }
973
+ }
974
+ }
975
+ const survivors = rows.filter(
976
+ (r) => remoteEnabled ? !remoteResolved.has(r.client_event_id) : !pgliteMirrored.has(r.client_event_id)
977
+ );
978
+ finalizeProcessing(survivors);
979
+ if (doRemote) {
980
+ const now = (/* @__PURE__ */ new Date()).toISOString();
981
+ const patch = { last_flush_at: now };
982
+ if (sent > 0) patch.last_flush_ok_at = now;
983
+ if (remoteErr) patch.last_flush_error = remoteErr;
984
+ patchMetaCache(patch);
985
+ }
986
+ const failed = survivors.length;
987
+ let skipped;
988
+ if (!doRemote) skipped = remoteEnabled ? "throttled" : state.remoteFlushEnabled ? "no_jwt" : "remote_disabled";
989
+ return { sent, failed, mirrored, skipped_reason: skipped };
990
+ } finally {
991
+ releaseQueueLock(lockFd);
992
+ }
993
+ }
994
+ function flushDetached() {
995
+ if (process.env.SYNKRO_TELEMETRY_DETACHED === "1") return;
996
+ try {
997
+ const node = process.execPath;
998
+ const script = process.argv[1];
999
+ if (!script) return;
1000
+ const child = spawn(node, [script, "telemetry", "flush", "--detached"], {
1001
+ stdio: "ignore",
1002
+ detached: true,
1003
+ env: { ...process.env, SYNKRO_TELEMETRY_EMITTER: "flush-detached", SYNKRO_TELEMETRY_DETACHED: "1" }
1004
+ });
1005
+ child.unref();
1006
+ } catch {
1007
+ }
1008
+ }
1009
+ var SYNKRO_DIR5, CONFIG_PATH2, CREDS_PATH2, THROTTLE_MS, DEFAULT_GATEWAY;
1010
+ var init_flush = __esm({
1011
+ "cli/telemetry/flush.ts"() {
1012
+ "use strict";
1013
+ init_db();
1014
+ init_drain();
1015
+ init_identity();
1016
+ init_optout();
1017
+ init_metaCache();
1018
+ init_wire();
1019
+ SYNKRO_DIR5 = join6(homedir6(), ".synkro");
1020
+ CONFIG_PATH2 = join6(SYNKRO_DIR5, "config.env");
1021
+ CREDS_PATH2 = process.env.SYNKRO_CREDENTIALS_PATH || join6(SYNKRO_DIR5, "credentials.json");
1022
+ THROTTLE_MS = 6e4;
1023
+ DEFAULT_GATEWAY = "https://api.synkro.sh";
1024
+ }
1025
+ });
1026
+
1027
+ // cli/telemetry/stats.ts
1028
+ import { existsSync as existsSync7, readFileSync as readFileSync6, statSync, writeFileSync as writeFileSync4 } from "fs";
1029
+ function countPending() {
1030
+ if (!existsSync7(PENDING_PATH)) return 0;
1031
+ try {
1032
+ const raw = readFileSync6(PENDING_PATH, "utf-8");
1033
+ if (!raw) return 0;
1034
+ let n = 0;
1035
+ for (const line of raw.split("\n")) if (line.trim()) n++;
1036
+ return n;
1037
+ } catch {
1038
+ return 0;
1039
+ }
1040
+ }
1041
+ async function getStats() {
1042
+ const identity = getIdentity();
1043
+ const state = await getTelemetryState();
1044
+ await drainToPglite();
1045
+ const meta = readMetaCache();
1046
+ const base = {
1047
+ total_events: 0,
1048
+ pending_local: countPending(),
1049
+ by_type: {},
1050
+ pglite_reachable: false,
1051
+ install_id: identity.install_id,
1052
+ enabled: state.enabled,
1053
+ remote_flush_enabled: state.remoteFlushEnabled,
1054
+ last_drained_at: meta.last_drained_at,
1055
+ // Flush state lives in metaCache, so it's available even with no pglite.
1056
+ last_flush_at: meta.last_flush_at,
1057
+ last_flush_ok_at: meta.last_flush_ok_at,
1058
+ last_flush_error: meta.last_flush_error
1059
+ };
1060
+ const sql = await connectDb();
1061
+ if (!sql) return base;
1062
+ base.pglite_reachable = true;
1063
+ try {
1064
+ const total = await sql`SELECT COUNT(*)::int AS c FROM telemetry_events`;
1065
+ base.total_events = Number(total[0]?.c ?? 0);
1066
+ const types = await sql`
1067
+ SELECT event_type, COUNT(*)::int AS c
1068
+ FROM telemetry_events
1069
+ GROUP BY event_type
1070
+ ORDER BY c DESC
1071
+ `;
1072
+ for (const row of types) base.by_type[String(row.event_type)] = Number(row.c);
1073
+ const newest = await sql`
1074
+ SELECT occurred_at FROM telemetry_events ORDER BY occurred_at DESC LIMIT 1
1075
+ `;
1076
+ if (newest.length) base.newest = new Date(newest[0].occurred_at).toISOString();
1077
+ } catch {
1078
+ }
1079
+ return base;
1080
+ }
1081
+ async function tail(opts = {}) {
1082
+ await drainToPglite();
1083
+ const sql = await connectDb();
1084
+ if (!sql) return [];
1085
+ const limit = Math.max(1, Math.min(opts.limit ?? 20, 1e3));
1086
+ try {
1087
+ if (opts.eventType && opts.sessionId) {
1088
+ return await sql`
1089
+ SELECT * FROM telemetry_events
1090
+ WHERE event_type = ${opts.eventType}
1091
+ AND cc_session_id = ${opts.sessionId}
1092
+ ORDER BY occurred_at DESC LIMIT ${limit}
1093
+ `;
1094
+ }
1095
+ if (opts.eventType) {
1096
+ return await sql`
1097
+ SELECT * FROM telemetry_events
1098
+ WHERE event_type = ${opts.eventType}
1099
+ ORDER BY occurred_at DESC LIMIT ${limit}
1100
+ `;
1101
+ }
1102
+ if (opts.sessionId) {
1103
+ return await sql`
1104
+ SELECT * FROM telemetry_events
1105
+ WHERE cc_session_id = ${opts.sessionId}
1106
+ ORDER BY occurred_at DESC LIMIT ${limit}
1107
+ `;
1108
+ }
1109
+ return await sql`
1110
+ SELECT * FROM telemetry_events
1111
+ ORDER BY occurred_at DESC LIMIT ${limit}
1112
+ `;
1113
+ } catch {
1114
+ return [];
1115
+ }
1116
+ }
1117
+ async function exportEvents(path) {
1118
+ await drainToPglite();
1119
+ const sql = await connectDb();
1120
+ if (!sql) {
1121
+ writeFileSync4(path, "", "utf-8");
1122
+ return;
1123
+ }
1124
+ const lines = [];
1125
+ try {
1126
+ const rows = await sql`SELECT * FROM telemetry_events ORDER BY occurred_at ASC`;
1127
+ for (const row of rows) {
1128
+ lines.push(JSON.stringify(row));
1129
+ }
1130
+ } catch {
1131
+ }
1132
+ writeFileSync4(path, lines.join("\n") + (lines.length > 0 ? "\n" : ""), "utf-8");
1133
+ }
1134
+ function pendingFileSize() {
1135
+ try {
1136
+ return existsSync7(PENDING_PATH) ? statSync(PENDING_PATH).size : 0;
1137
+ } catch {
1138
+ return 0;
1139
+ }
1140
+ }
1141
+ var init_stats = __esm({
1142
+ "cli/telemetry/stats.ts"() {
1143
+ "use strict";
1144
+ init_db();
1145
+ init_drain();
1146
+ init_emit();
1147
+ init_identity();
1148
+ init_optout();
1149
+ init_metaCache();
1150
+ }
1151
+ });
1152
+
1153
+ // cli/telemetry/purge.ts
1154
+ import { existsSync as existsSync8, readFileSync as readFileSync7, unlinkSync as unlinkSync2 } from "fs";
1155
+ import { randomUUID as randomUUID3 } from "crypto";
1156
+ function unlinkPendingCount() {
1157
+ if (!existsSync8(PENDING_PATH)) return 0;
1158
+ let n = 0;
1159
+ try {
1160
+ const raw = readFileSync7(PENDING_PATH, "utf-8");
1161
+ for (const line of raw.split("\n")) if (line.trim()) n++;
1162
+ } catch {
1163
+ }
1164
+ try {
1165
+ unlinkSync2(PENDING_PATH);
1166
+ } catch {
1167
+ }
1168
+ return n;
1169
+ }
1170
+ async function purge(opts = {}) {
1171
+ const keep = opts.keepInstallId ?? true;
1172
+ const pending = unlinkPendingCount();
1173
+ const sql = await connectDb();
1174
+ if (!sql) {
1175
+ if (!keep) {
1176
+ patchMetaCache({ install_id: randomUUID3() });
1177
+ }
1178
+ return {
1179
+ removed_events: pending,
1180
+ removed_meta: !keep,
1181
+ removed_pending: pending,
1182
+ pglite_reachable: false
1183
+ };
1184
+ }
1185
+ let removedEvents = 0;
1186
+ try {
1187
+ const [{ c }] = await sql`SELECT COUNT(*)::int AS c FROM telemetry_events`;
1188
+ removedEvents = Number(c ?? 0);
1189
+ await sql`TRUNCATE TABLE telemetry_events RESTART IDENTITY`;
1190
+ } catch {
1191
+ }
1192
+ let removedMeta = false;
1193
+ try {
1194
+ if (keep) {
1195
+ const meta = readMetaCache();
1196
+ await sql`DELETE FROM telemetry_meta WHERE k NOT IN ('schema_version')`;
1197
+ await sql`
1198
+ INSERT INTO telemetry_meta (k, v) VALUES ('install_id', ${meta.install_id})
1199
+ ON CONFLICT (k) DO UPDATE SET v = EXCLUDED.v
1200
+ `;
1201
+ removedMeta = true;
1202
+ } else {
1203
+ const newId = randomUUID3();
1204
+ await sql`DELETE FROM telemetry_meta WHERE k NOT IN ('schema_version')`;
1205
+ await sql`
1206
+ INSERT INTO telemetry_meta (k, v) VALUES ('install_id', ${newId})
1207
+ ON CONFLICT (k) DO UPDATE SET v = EXCLUDED.v
1208
+ `;
1209
+ patchMetaCache({ install_id: newId });
1210
+ removedMeta = true;
1211
+ }
1212
+ } catch {
1213
+ }
1214
+ return {
1215
+ removed_events: removedEvents,
1216
+ removed_meta: removedMeta,
1217
+ removed_pending: pending,
1218
+ pglite_reachable: true
1219
+ };
1220
+ }
1221
+ var init_purge = __esm({
1222
+ "cli/telemetry/purge.ts"() {
1223
+ "use strict";
1224
+ init_db();
1225
+ init_emit();
1226
+ init_metaCache();
1227
+ }
1228
+ });
1229
+
1230
+ // cli/telemetry/client.ts
1231
+ import { randomUUID as randomUUID4 } from "crypto";
1232
+ var TelemetryClient, telemetry;
1233
+ var init_client = __esm({
1234
+ "cli/telemetry/client.ts"() {
1235
+ "use strict";
1236
+ TelemetryClient = class {
1237
+ async track(_eventType, _data) {
1238
+ return randomUUID4();
1239
+ }
1240
+ async createPreferencePair(_data) {
1241
+ return randomUUID4();
1242
+ }
1243
+ async updatePreferencePair(_id, _acceptedState) {
1244
+ }
1245
+ };
1246
+ telemetry = new TelemetryClient();
1247
+ }
1248
+ });
1249
+
1250
+ // cli/telemetry/export.ts
1251
+ import { writeFileSync as writeFileSync5 } from "fs";
1252
+ async function exportPreferencePairs(outputPath) {
1253
+ writeFileSync5(outputPath, "");
1254
+ return 0;
1255
+ }
1256
+ async function exportCompletions(outputPath) {
1257
+ writeFileSync5(outputPath, "");
1258
+ return 0;
1259
+ }
1260
+ async function getTelemetryStats() {
1261
+ return {
1262
+ totalEvents: 0,
1263
+ totalPreferencePairs: 0,
1264
+ completePreferencePairs: 0,
1265
+ pipelineCompletions: 0
1266
+ };
1267
+ }
1268
+ var init_export = __esm({
1269
+ "cli/telemetry/export.ts"() {
1270
+ "use strict";
1271
+ }
1272
+ });
1273
+
1274
+ // cli/telemetry/index.ts
1275
+ var telemetry_exports = {};
1276
+ __export(telemetry_exports, {
1277
+ PENDING_PATH: () => PENDING_PATH,
1278
+ acquireQueueLock: () => acquireQueueLock,
1279
+ closeDb: () => closeDb,
1280
+ connectDb: () => connectDb,
1281
+ drainToPglite: () => drainToPglite,
1282
+ emit: () => emit,
1283
+ exportCompletions: () => exportCompletions,
1284
+ exportEvents: () => exportEvents,
1285
+ exportPreferencePairs: () => exportPreferencePairs,
1286
+ flush: () => flush,
1287
+ flushDetached: () => flushDetached,
1288
+ getIdentity: () => getIdentity,
1289
+ getStats: () => getStats,
1290
+ getTelemetryState: () => getTelemetryState,
1291
+ getTelemetryStateSync: () => getTelemetryStateSync,
1292
+ getTelemetryStats: () => getTelemetryStats,
1293
+ insertEvents: () => insertEvents,
1294
+ patchMetaCache: () => patchMetaCache,
1295
+ pendingFileSize: () => pendingFileSize,
1296
+ purge: () => purge,
1297
+ readMetaCache: () => readMetaCache,
1298
+ readQueue: () => readQueue,
1299
+ releaseQueueLock: () => releaseQueueLock,
1300
+ rewriteQueue: () => rewriteQueue,
1301
+ setTelemetryState: () => setTelemetryState,
1302
+ tail: () => tail,
1303
+ telemetry: () => telemetry,
1304
+ writeMetaCache: () => writeMetaCache
1305
+ });
1306
+ var init_telemetry = __esm({
1307
+ "cli/telemetry/index.ts"() {
1308
+ "use strict";
1309
+ init_emit();
1310
+ init_identity();
1311
+ init_db();
1312
+ init_drain();
1313
+ init_optout();
1314
+ init_flush();
1315
+ init_stats();
1316
+ init_purge();
1317
+ init_metaCache();
1318
+ init_client();
1319
+ init_export();
1320
+ }
1321
+ });
1322
+
18
1323
  // cli/installer/agentDetect.ts
19
- import { existsSync } from "fs";
20
- import { homedir } from "os";
21
- import { join } from "path";
22
- import { execFileSync, execSync } from "child_process";
1324
+ import { existsSync as existsSync9 } from "fs";
1325
+ import { homedir as homedir7 } from "os";
1326
+ import { join as join7 } from "path";
1327
+ import { execFileSync as execFileSync2, execSync } from "child_process";
23
1328
  function which(cmd2) {
24
1329
  try {
25
1330
  const result = execSync(`which ${cmd2}`, { encoding: "utf-8" }).trim();
@@ -30,7 +1335,7 @@ function which(cmd2) {
30
1335
  }
31
1336
  function getVersion(cmd2) {
32
1337
  try {
33
- const result = execFileSync(cmd2, ["--version"], { encoding: "utf-8", timeout: 5e3 }).trim();
1338
+ const result = execFileSync2(cmd2, ["--version"], { encoding: "utf-8", timeout: 5e3 }).trim();
34
1339
  const line = result.split("\n")[0];
35
1340
  return line.replace(/\s*\(Claude Code\)/, "");
36
1341
  } catch {
@@ -39,23 +1344,23 @@ function getVersion(cmd2) {
39
1344
  }
40
1345
  function detectAgents() {
41
1346
  const agents = [];
42
- const home = homedir();
1347
+ const home = homedir7();
43
1348
  const claudeBinary = which("claude");
44
- const claudeConfigDir = join(home, ".claude");
1349
+ const claudeConfigDir = join7(home, ".claude");
45
1350
  if (claudeBinary) {
46
1351
  agents.push({
47
1352
  kind: "claude_code",
48
1353
  name: "Claude Code",
49
1354
  binaryPath: claudeBinary,
50
1355
  configDir: claudeConfigDir,
51
- settingsPath: join(claudeConfigDir, "settings.json"),
1356
+ settingsPath: join7(claudeConfigDir, "settings.json"),
52
1357
  version: getVersion("claude")
53
1358
  });
54
1359
  }
55
1360
  const cursorBinary = which("cursor");
56
- const cursorConfigDir = join(home, ".cursor");
57
- const cursorApp = process.platform === "darwin" && existsSync("/Applications/Cursor.app");
58
- if (cursorBinary || cursorApp || existsSync(cursorConfigDir)) {
1361
+ const cursorConfigDir = join7(home, ".cursor");
1362
+ const cursorApp = process.platform === "darwin" && existsSync9("/Applications/Cursor.app");
1363
+ if (cursorBinary || cursorApp || existsSync9(cursorConfigDir)) {
59
1364
  let version;
60
1365
  if (cursorBinary) {
61
1366
  version = getVersion("cursor");
@@ -71,7 +1376,7 @@ function detectAgents() {
71
1376
  name: "Cursor",
72
1377
  binaryPath: cursorBinary,
73
1378
  configDir: cursorConfigDir,
74
- settingsPath: join(cursorConfigDir, "hooks.json"),
1379
+ settingsPath: join7(cursorConfigDir, "hooks.json"),
75
1380
  version
76
1381
  });
77
1382
  }
@@ -83,30 +1388,74 @@ var init_agentDetect = __esm({
83
1388
  }
84
1389
  });
85
1390
 
1391
+ // cli/installer/platform.ts
1392
+ import { existsSync as existsSync10 } from "fs";
1393
+ import { homedir as homedir8 } from "os";
1394
+ import { join as join8 } from "path";
1395
+ import { spawnSync } from "child_process";
1396
+ function resolveBunBin() {
1397
+ const finder = IS_WINDOWS ? "where" : "which";
1398
+ const r = spawnSync(finder, ["bun"], { encoding: "utf-8", timeout: 5e3 });
1399
+ const resolved = (r.stdout || "").split(/\r?\n/).map((s) => s.trim()).find(Boolean);
1400
+ if (resolved) return resolved;
1401
+ const home = process.env.USERPROFILE || homedir8();
1402
+ const candidates = IS_WINDOWS ? [join8(home, ".bun", "bin", "bun.exe"), "C:\\Program Files\\bun\\bin\\bun.exe"] : ["/opt/homebrew/bin/bun", "/usr/local/bin/bun", join8(home, ".bun", "bin", "bun")];
1403
+ for (const p of candidates) if (existsSync10(p)) return p;
1404
+ return IS_WINDOWS ? "bun.exe" : "bun";
1405
+ }
1406
+ function quoteArg(s) {
1407
+ return IS_WINDOWS ? `"${s}"` : "'" + s.replace(/'/g, "'\\''") + "'";
1408
+ }
1409
+ function ccHookCommand(scriptPath) {
1410
+ if (!IS_WINDOWS) return scriptPath;
1411
+ return `${quoteArg(resolveBunBin())} ${quoteArg(scriptPath)}`;
1412
+ }
1413
+ function isSynkroHookCommand(command) {
1414
+ if (typeof command !== "string") return false;
1415
+ return command.replace(/\\/g, "/").includes("/.synkro/hooks/");
1416
+ }
1417
+ var IS_WINDOWS;
1418
+ var init_platform = __esm({
1419
+ "cli/installer/platform.ts"() {
1420
+ "use strict";
1421
+ IS_WINDOWS = process.platform === "win32";
1422
+ }
1423
+ });
1424
+
86
1425
  // cli/installer/ccHookConfig.ts
87
- import { existsSync as existsSync2, readFileSync, writeFileSync, renameSync, mkdirSync } from "fs";
1426
+ import { existsSync as existsSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync6, renameSync as renameSync3, mkdirSync as mkdirSync4 } from "fs";
88
1427
  import { dirname } from "path";
89
1428
  function readSettings(path) {
90
- if (!existsSync2(path)) return {};
1429
+ if (!existsSync11(path)) return {};
91
1430
  try {
92
- const raw = readFileSync(path, "utf-8");
1431
+ const raw = readFileSync8(path, "utf-8");
93
1432
  return JSON.parse(raw);
94
1433
  } catch (err) {
95
1434
  throw new Error(`Failed to parse ${path}: ${err.message}`);
96
1435
  }
97
1436
  }
98
1437
  function writeSettingsAtomic(path, settings) {
99
- mkdirSync(dirname(path), { recursive: true });
1438
+ mkdirSync4(dirname(path), { recursive: true });
100
1439
  const tmpPath = `${path}.synkro.tmp`;
101
- writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
102
- renameSync(tmpPath, path);
1440
+ writeFileSync6(tmpPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1441
+ renameSync3(tmpPath, path);
103
1442
  }
104
1443
  function isSynkroEntry(entry) {
105
1444
  if (entry?.[SYNKRO_MARKER]) return true;
106
1445
  const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
107
- return hooks.some(
108
- (h) => typeof h?.command === "string" && h.command.includes("/.synkro/hooks/")
109
- );
1446
+ return hooks.some((h) => isSynkroHookCommand(h?.command));
1447
+ }
1448
+ function finalizeHookCommands(hooks) {
1449
+ for (const evt of Object.keys(hooks)) {
1450
+ const arr = hooks[evt];
1451
+ if (!Array.isArray(arr)) continue;
1452
+ for (const entry of arr) {
1453
+ if (!entry?.[SYNKRO_MARKER] || !Array.isArray(entry.hooks)) continue;
1454
+ for (const h of entry.hooks) {
1455
+ if (typeof h?.command === "string") h.command = ccHookCommand(h.command);
1456
+ }
1457
+ }
1458
+ }
110
1459
  }
111
1460
  function removeSynkroEntries(events, eventName) {
112
1461
  if (!events) return;
@@ -287,10 +1636,11 @@ function installCCHooks(settingsPath, config) {
287
1636
  delete settings.env.ANTHROPIC_BASE_URL;
288
1637
  if (Object.keys(settings.env).length === 0) delete settings.env;
289
1638
  }
1639
+ finalizeHookCommands(settings.hooks);
290
1640
  writeSettingsAtomic(settingsPath, settings);
291
1641
  }
292
1642
  function uninstallCCHooks(settingsPath) {
293
- if (!existsSync2(settingsPath)) return false;
1643
+ if (!existsSync11(settingsPath)) return false;
294
1644
  const settings = readSettings(settingsPath);
295
1645
  if (!settings.hooks) return false;
296
1646
  const events = ["PreToolUse", "PostToolUse", "SessionEnd", "SessionStart", "Stop", "UserPromptSubmit"];
@@ -316,15 +1666,16 @@ var SYNKRO_MARKER, USAGE_PROXY_URL;
316
1666
  var init_ccHookConfig = __esm({
317
1667
  "cli/installer/ccHookConfig.ts"() {
318
1668
  "use strict";
1669
+ init_platform();
319
1670
  SYNKRO_MARKER = "__synkro_managed__";
320
1671
  USAGE_PROXY_URL = `http://127.0.0.1:${process.env.SYNKRO_HOST_USAGE_PROXY_PORT || "18927"}`;
321
1672
  }
322
1673
  });
323
1674
 
324
1675
  // cli/installer/cursorHookConfig.ts
325
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, mkdirSync as mkdirSync2 } from "fs";
1676
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, renameSync as renameSync4, mkdirSync as mkdirSync5 } from "fs";
326
1677
  import { dirname as dirname2, resolve, normalize } from "path";
327
- import { homedir as homedir2 } from "os";
1678
+ import { homedir as homedir9 } from "os";
328
1679
  function shellQuote(s) {
329
1680
  return "'" + s.replace(/'/g, "'\\''") + "'";
330
1681
  }
@@ -344,7 +1695,7 @@ function validateHooksPath(path) {
344
1695
  function readHooksFile(rawPath) {
345
1696
  const safePath = validateHooksPath(rawPath);
346
1697
  try {
347
- const raw = readFileSync2(safePath, "utf-8");
1698
+ const raw = readFileSync9(safePath, "utf-8");
348
1699
  return JSON.parse(raw);
349
1700
  } catch (err) {
350
1701
  if (err?.code === "ENOENT") return { version: 1, hooks: {} };
@@ -353,14 +1704,14 @@ function readHooksFile(rawPath) {
353
1704
  }
354
1705
  function writeHooksFileAtomic(rawPath, data) {
355
1706
  const safePath = validateHooksPath(rawPath);
356
- mkdirSync2(dirname2(safePath), { recursive: true });
1707
+ mkdirSync5(dirname2(safePath), { recursive: true });
357
1708
  const tmpPath = `${safePath}.synkro.tmp`;
358
- writeFileSync2(tmpPath, JSON.stringify(data, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
359
- renameSync2(tmpPath, safePath);
1709
+ writeFileSync7(tmpPath, JSON.stringify(data, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
1710
+ renameSync4(tmpPath, safePath);
360
1711
  }
361
1712
  function isSynkroEntry2(entry) {
362
1713
  if (entry?.[SYNKRO_MARKER2]) return true;
363
- return typeof entry?.command === "string" && entry.command.includes("/.synkro/hooks/");
1714
+ return isSynkroHookCommand(entry?.command);
364
1715
  }
365
1716
  function removeSynkroEntries2(hooks, event) {
366
1717
  if (!hooks) return;
@@ -523,10 +1874,11 @@ var SYNKRO_MARKER2, ALLOWED_PARENT_DIRS, ALL_EVENTS;
523
1874
  var init_cursorHookConfig = __esm({
524
1875
  "cli/installer/cursorHookConfig.ts"() {
525
1876
  "use strict";
1877
+ init_platform();
526
1878
  SYNKRO_MARKER2 = "__synkro_managed__";
527
1879
  ALLOWED_PARENT_DIRS = [
528
- resolve(homedir2(), ".cursor"),
529
- resolve(homedir2(), ".config", "cursor")
1880
+ resolve(homedir9(), ".cursor"),
1881
+ resolve(homedir9(), ".config", "cursor")
530
1882
  ];
531
1883
  ALL_EVENTS = [
532
1884
  "sessionStart",
@@ -546,25 +1898,24 @@ var init_cursorHookConfig = __esm({
546
1898
  });
547
1899
 
548
1900
  // cli/installer/mcpConfig.ts
549
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync3, mkdirSync as mkdirSync3 } from "fs";
550
- import { homedir as homedir3 } from "os";
551
- import { dirname as dirname3, join as join2 } from "path";
552
- import { spawnSync } from "child_process";
1901
+ import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync8, renameSync as renameSync5, mkdirSync as mkdirSync6 } from "fs";
1902
+ import { homedir as homedir10 } from "os";
1903
+ import { dirname as dirname3, join as join9 } from "path";
553
1904
  import { randomBytes } from "crypto";
554
1905
  function readClaudeJson() {
555
- if (!existsSync3(CC_CONFIG_PATH)) return {};
1906
+ if (!existsSync12(CC_CONFIG_PATH)) return {};
556
1907
  try {
557
- const raw = readFileSync3(CC_CONFIG_PATH, "utf-8");
1908
+ const raw = readFileSync10(CC_CONFIG_PATH, "utf-8");
558
1909
  return JSON.parse(raw);
559
1910
  } catch (err) {
560
1911
  throw new Error(`Failed to parse ${CC_CONFIG_PATH}: ${err.message}`);
561
1912
  }
562
1913
  }
563
1914
  function writeClaudeJsonAtomic(config) {
564
- mkdirSync3(dirname3(CC_CONFIG_PATH), { recursive: true });
1915
+ mkdirSync6(dirname3(CC_CONFIG_PATH), { recursive: true });
565
1916
  const tmpPath = `${CC_CONFIG_PATH}.synkro.tmp`;
566
- writeFileSync3(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
567
- renameSync3(tmpPath, CC_CONFIG_PATH);
1917
+ writeFileSync8(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1918
+ renameSync5(tmpPath, CC_CONFIG_PATH);
568
1919
  }
569
1920
  function installMcpConfig(opts) {
570
1921
  const config = readClaudeJson();
@@ -573,7 +1924,7 @@ function installMcpConfig(opts) {
573
1924
  if (entry?.[SYNKRO_MARKER3] === true) delete config.mcpServers[name];
574
1925
  }
575
1926
  if (opts.local) {
576
- const proxyScript = join2(homedir3(), ".synkro", "hooks", "mcp-stdio-proxy.ts");
1927
+ const proxyScript = join9(homedir10(), ".synkro", "hooks", "mcp-stdio-proxy.ts");
577
1928
  config.mcpServers[SYNKRO_SERVER_NAME] = {
578
1929
  type: "stdio",
579
1930
  command: "bun",
@@ -594,7 +1945,7 @@ function installMcpConfig(opts) {
594
1945
  return { path: CC_CONFIG_PATH, url };
595
1946
  }
596
1947
  function uninstallMcpConfig() {
597
- if (!existsSync3(CC_CONFIG_PATH)) return false;
1948
+ if (!existsSync12(CC_CONFIG_PATH)) return false;
598
1949
  const config = readClaudeJson();
599
1950
  if (!config.mcpServers || Object.keys(config.mcpServers).length === 0) return false;
600
1951
  let removed = false;
@@ -610,19 +1961,19 @@ function uninstallMcpConfig() {
610
1961
  return true;
611
1962
  }
612
1963
  function readCursorMcpJson() {
613
- if (!existsSync3(CURSOR_MCP_PATH)) return {};
1964
+ if (!existsSync12(CURSOR_MCP_PATH)) return {};
614
1965
  try {
615
- const raw = readFileSync3(CURSOR_MCP_PATH, "utf-8");
1966
+ const raw = readFileSync10(CURSOR_MCP_PATH, "utf-8");
616
1967
  return JSON.parse(raw);
617
1968
  } catch (err) {
618
1969
  throw new Error(`Failed to parse ${CURSOR_MCP_PATH}: ${err.message}`);
619
1970
  }
620
1971
  }
621
1972
  function writeCursorMcpJsonAtomic(config) {
622
- mkdirSync3(dirname3(CURSOR_MCP_PATH), { recursive: true });
1973
+ mkdirSync6(dirname3(CURSOR_MCP_PATH), { recursive: true });
623
1974
  const tmpPath = `${CURSOR_MCP_PATH}.synkro.tmp`;
624
- writeFileSync3(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
625
- renameSync3(tmpPath, CURSOR_MCP_PATH);
1975
+ writeFileSync8(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1976
+ renameSync5(tmpPath, CURSOR_MCP_PATH);
626
1977
  }
627
1978
  function installCursorMcpConfig(opts) {
628
1979
  const config = readCursorMcpJson();
@@ -633,10 +1984,10 @@ function installCursorMcpConfig(opts) {
633
1984
  if (opts.local) {
634
1985
  const port = process.env.SYNKRO_MCP_PORT || "18931";
635
1986
  const url2 = `http://127.0.0.1:${port}/`;
636
- const jwtPath = join2(homedir3(), ".synkro", ".mcp-jwt");
1987
+ const jwtPath = join9(homedir10(), ".synkro", ".mcp-jwt");
637
1988
  let jwt2 = "";
638
1989
  try {
639
- jwt2 = readFileSync3(jwtPath, "utf-8").trim();
1990
+ jwt2 = readFileSync10(jwtPath, "utf-8").trim();
640
1991
  } catch {
641
1992
  }
642
1993
  config.mcpServers[SYNKRO_SERVER_NAME] = {
@@ -657,7 +2008,7 @@ function installCursorMcpConfig(opts) {
657
2008
  return { path: CURSOR_MCP_PATH, url };
658
2009
  }
659
2010
  function uninstallCursorMcpConfig() {
660
- if (!existsSync3(CURSOR_MCP_PATH)) return false;
2011
+ if (!existsSync12(CURSOR_MCP_PATH)) return false;
661
2012
  const config = readCursorMcpJson();
662
2013
  if (!config.mcpServers || Object.keys(config.mcpServers).length === 0) return false;
663
2014
  let removed = false;
@@ -672,31 +2023,25 @@ function uninstallCursorMcpConfig() {
672
2023
  writeCursorMcpJsonAtomic(config);
673
2024
  return true;
674
2025
  }
675
- function resolveBunBin() {
676
- const r = spawnSync("which", ["bun"], { encoding: "utf-8", timeout: 5e3 });
677
- const resolved = (r.stdout || "").split("\n")[0].trim();
678
- if (resolved) return resolved;
679
- for (const p of ["/opt/homebrew/bin/bun", "/usr/local/bin/bun", join2(homedir3(), ".bun", "bin", "bun")]) {
680
- if (existsSync3(p)) return p;
681
- }
682
- return "bun";
2026
+ function resolveBunBin2() {
2027
+ return resolveBunBin();
683
2028
  }
684
2029
  function readDesktopJson() {
685
- if (!existsSync3(CLAUDE_DESKTOP_CONFIG_PATH)) return {};
2030
+ if (!existsSync12(CLAUDE_DESKTOP_CONFIG_PATH)) return {};
686
2031
  try {
687
- return JSON.parse(readFileSync3(CLAUDE_DESKTOP_CONFIG_PATH, "utf-8"));
2032
+ return JSON.parse(readFileSync10(CLAUDE_DESKTOP_CONFIG_PATH, "utf-8"));
688
2033
  } catch (err) {
689
2034
  throw new Error(`Failed to parse ${CLAUDE_DESKTOP_CONFIG_PATH}: ${err.message}`);
690
2035
  }
691
2036
  }
692
2037
  function writeDesktopJsonAtomic(config) {
693
- mkdirSync3(dirname3(CLAUDE_DESKTOP_CONFIG_PATH), { recursive: true });
2038
+ mkdirSync6(dirname3(CLAUDE_DESKTOP_CONFIG_PATH), { recursive: true });
694
2039
  const tmpPath = `${CLAUDE_DESKTOP_CONFIG_PATH}.synkro.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
695
- writeFileSync3(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
696
- renameSync3(tmpPath, CLAUDE_DESKTOP_CONFIG_PATH);
2040
+ writeFileSync8(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2041
+ renameSync5(tmpPath, CLAUDE_DESKTOP_CONFIG_PATH);
697
2042
  }
698
2043
  function installClaudeDesktopMcpConfig() {
699
- if (!existsSync3(MCP_STDIO_PROXY_PATH)) {
2044
+ if (!existsSync12(MCP_STDIO_PROXY_PATH)) {
700
2045
  return {
701
2046
  ok: false,
702
2047
  path: CLAUDE_DESKTOP_CONFIG_PATH,
@@ -710,7 +2055,7 @@ function installClaudeDesktopMcpConfig() {
710
2055
  if (entry?.[SYNKRO_MARKER3] === true) delete config.mcpServers[name];
711
2056
  }
712
2057
  config.mcpServers[SYNKRO_SERVER_NAME] = {
713
- command: resolveBunBin(),
2058
+ command: resolveBunBin2(),
714
2059
  args: ["run", MCP_STDIO_PROXY_PATH],
715
2060
  env: { SYNKRO_MCP_PORT: process.env.SYNKRO_MCP_PORT || "18931" },
716
2061
  [SYNKRO_MARKER3]: true
@@ -719,7 +2064,7 @@ function installClaudeDesktopMcpConfig() {
719
2064
  return { ok: true, path: CLAUDE_DESKTOP_CONFIG_PATH, proxy: MCP_STDIO_PROXY_PATH };
720
2065
  }
721
2066
  function uninstallClaudeDesktopMcpConfig() {
722
- if (!existsSync3(CLAUDE_DESKTOP_CONFIG_PATH)) return false;
2067
+ if (!existsSync12(CLAUDE_DESKTOP_CONFIG_PATH)) return false;
723
2068
  const config = readDesktopJson();
724
2069
  if (!config.mcpServers || Object.keys(config.mcpServers).length === 0) return false;
725
2070
  let removed = false;
@@ -738,26 +2083,27 @@ var SYNKRO_MARKER3, SYNKRO_SERVER_NAME, CC_CONFIG_PATH, CURSOR_MCP_PATH, CLAUDE_
738
2083
  var init_mcpConfig = __esm({
739
2084
  "cli/installer/mcpConfig.ts"() {
740
2085
  "use strict";
2086
+ init_platform();
741
2087
  SYNKRO_MARKER3 = "__synkro_managed__";
742
2088
  SYNKRO_SERVER_NAME = "synkro-guardrails";
743
- CC_CONFIG_PATH = join2(homedir3(), ".claude.json");
744
- CURSOR_MCP_PATH = join2(homedir3(), ".cursor", "mcp.json");
745
- CLAUDE_DESKTOP_CONFIG_PATH = join2(
746
- homedir3(),
2089
+ CC_CONFIG_PATH = join9(homedir10(), ".claude.json");
2090
+ CURSOR_MCP_PATH = join9(homedir10(), ".cursor", "mcp.json");
2091
+ CLAUDE_DESKTOP_CONFIG_PATH = join9(
2092
+ homedir10(),
747
2093
  "Library",
748
2094
  "Application Support",
749
2095
  "Claude",
750
2096
  "claude_desktop_config.json"
751
2097
  );
752
- MCP_STDIO_PROXY_PATH = join2(homedir3(), ".synkro", "hooks", "mcp-stdio-proxy.ts");
2098
+ MCP_STDIO_PROXY_PATH = join9(homedir10(), ".synkro", "hooks", "mcp-stdio-proxy.ts");
753
2099
  }
754
2100
  });
755
2101
 
756
2102
  // cli/installer/skillParser.ts
757
- import { existsSync as existsSync4 } from "fs";
2103
+ import { existsSync as existsSync13 } from "fs";
758
2104
  import { resolve as resolve2 } from "path";
759
2105
  function resolveSkillPaths(skills, repoRoot) {
760
- return skills.filter((s) => s.endsWith(".md")).map((s) => resolve2(repoRoot, s)).filter((p) => existsSync4(p));
2106
+ return skills.filter((s) => s.endsWith(".md")).map((s) => resolve2(repoRoot, s)).filter((p) => existsSync13(p));
761
2107
  }
762
2108
  var init_skillParser = __esm({
763
2109
  "cli/installer/skillParser.ts"() {
@@ -773,14 +2119,133 @@ var STUB_COMMON_TS, STUB_EDIT_PRECHECK_TS, STUB_CWE_PRECHECK_TS, STUB_CVE_PRECHE
773
2119
  var init_hookScriptsTs = __esm({
774
2120
  "cli/installer/hookScriptsTs.ts"() {
775
2121
  "use strict";
776
- STUB_COMMON_TS = String.raw`import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
2122
+ STUB_COMMON_TS = String.raw`import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync, statSync, openSync, readSync, closeSync } from 'node:fs';
777
2123
  import { execSync } from 'node:child_process';
778
2124
  import { homedir } from 'node:os';
779
2125
  import { join } from 'node:path';
2126
+ import { randomUUID, createHash } from 'node:crypto';
780
2127
 
781
2128
  const HOME = homedir();
782
2129
  const PORT = process.env.SYNKRO_MCP_PORT || '18931';
783
2130
 
2131
+ // ─── Telemetry: sync JSONL append, drained later by the CLI binary ──────────
2132
+ // Matches packages/cli/cli/telemetry/emit.ts. Inlined here so each bun hook
2133
+ // is self-contained (no path-relative import to a sibling module that may not
2134
+ // exist post-install). Writes to the same ~/.synkro/telemetry-pending.jsonl
2135
+ // the CLI binary appends to; ON CONFLICT in drain.ts dedupes.
2136
+ const PENDING_JSONL = join(HOME, '.synkro', 'telemetry-pending.jsonl');
2137
+ const META_JSON = join(HOME, '.synkro', 'telemetry-meta.json');
2138
+ const CREDS_JSON = join(HOME, '.synkro', 'credentials.json');
2139
+
2140
+ interface TelemMeta { install_id?: string; enabled?: boolean }
2141
+ let _telemMetaCache: TelemMeta | null = null;
2142
+ function telemMeta(): TelemMeta {
2143
+ if (_telemMetaCache) return _telemMetaCache;
2144
+ try {
2145
+ _telemMetaCache = JSON.parse(readFileSync(META_JSON, 'utf-8')) as TelemMeta;
2146
+ } catch { _telemMetaCache = {}; }
2147
+ return _telemMetaCache;
2148
+ }
2149
+
2150
+ interface TelemCreds { user_id?: string; org_id?: string; email?: string }
2151
+ let _telemCredsCache: TelemCreds | null = null;
2152
+ function telemCreds(): TelemCreds {
2153
+ if (_telemCredsCache) return _telemCredsCache;
2154
+ try {
2155
+ const c = JSON.parse(readFileSync(CREDS_JSON, 'utf-8')) as TelemCreds;
2156
+ _telemCredsCache = { user_id: c.user_id, org_id: c.org_id, email: c.email };
2157
+ } catch { _telemCredsCache = {}; }
2158
+ return _telemCredsCache;
2159
+ }
2160
+
2161
+ interface HookEmitOpts {
2162
+ agent_kind?: 'claude_code' | 'cursor';
2163
+ cc_session_id?: string;
2164
+ cc_tool_use_id?: string;
2165
+ cc_model?: string;
2166
+ permission_mode?: string;
2167
+ cwd?: string;
2168
+ repo?: string;
2169
+ }
2170
+
2171
+ export function hookEmit(eventType: string, context: unknown, opts: HookEmitOpts = {}): void {
2172
+ try {
2173
+ const meta = telemMeta();
2174
+ if (meta.enabled === false) return;
2175
+ // Never ship rows with an empty install_id — they can't be attributed. An
2176
+ // absent meta file means the CLI hasn't bootstrapped telemetry yet; skip
2177
+ // rather than emit an orphan row (the CLI metaCache mints the id on install).
2178
+ if (!meta.install_id) return;
2179
+ const creds = telemCreds();
2180
+ const row = {
2181
+ client_event_id: randomUUID(),
2182
+ event_type: eventType,
2183
+ occurred_at: new Date().toISOString(),
2184
+ install_id: meta.install_id,
2185
+ user_id: creds.user_id ?? null,
2186
+ org_id: creds.org_id ?? null,
2187
+ email: creds.email ?? null,
2188
+ cli_version: process.env.SYNKRO_VERSION || '',
2189
+ emitter: process.env.SYNKRO_TELEMETRY_EMITTER || 'hook',
2190
+ agent_kind: opts.agent_kind ?? null,
2191
+ cc_session_id: opts.cc_session_id ?? null,
2192
+ cc_tool_use_id: opts.cc_tool_use_id ?? null,
2193
+ cc_turn_id: null,
2194
+ cc_model: opts.cc_model ?? null,
2195
+ permission_mode: opts.permission_mode ?? null,
2196
+ cwd: opts.cwd ?? null,
2197
+ repo: opts.repo ?? null,
2198
+ git_branch: null,
2199
+ git_sha: null,
2200
+ policy_name: null,
2201
+ capture_depth: null,
2202
+ platform: process.platform,
2203
+ hostname_hash: null,
2204
+ node_version: process.version || '',
2205
+ context,
2206
+ };
2207
+ try { mkdirSync(join(HOME, '.synkro'), { recursive: true }); } catch {}
2208
+ appendFileSync(PENDING_JSONL, JSON.stringify(row) + '\n');
2209
+ } catch { /* never let telemetry block a hook */ }
2210
+ }
2211
+
2212
+ // Surface name → telemetry event type. One-shot lookup for the runStub wrap.
2213
+ function eventForSurface(surface: string): { type: string; sessionPhase?: 'start' | 'end' } {
2214
+ switch (surface) {
2215
+ case 'session-start': return { type: 'session_lifecycle', sessionPhase: 'start' };
2216
+ case 'stop-summary': return { type: 'session_lifecycle', sessionPhase: 'end' };
2217
+ case 'prompt-submit': return { type: 'human_interaction' };
2218
+ case 'bash-followup': return { type: 'human_interaction' };
2219
+ case 'mcp-gate': return { type: 'mcp_tool_use' };
2220
+ case 'plan-judge': return { type: 'plan_review_result' };
2221
+ case 'transcript-sync': return { type: 'tool_call' };
2222
+ default: return { type: 'tool_call' };
2223
+ }
2224
+ }
2225
+
2226
+ // Inspect the verdict text returned by the local server to derive a coarse
2227
+ // verdict for the telemetry row. Both CC and Cursor return JSON; we parse
2228
+ // lightly and ignore failures (default 'pass').
2229
+ function parseVerdict(text: string, harness: string): 'pass' | 'deny' | 'allow' | 'warning' | 'error' | 'ask' {
2230
+ if (!text) return 'pass';
2231
+ try {
2232
+ const j = JSON.parse(text);
2233
+ if (harness === 'cursor') {
2234
+ const p = j?.permission;
2235
+ if (p === 'deny') return 'deny';
2236
+ if (p === 'allow') return 'allow';
2237
+ return 'pass';
2238
+ }
2239
+ const dec = j?.hookSpecificOutput?.permissionDecision;
2240
+ if (dec === 'deny') return 'deny';
2241
+ // 'ask' is a consent prompt, NOT a block — keep it distinct so it doesn't
2242
+ // manufacture spurious violation rows (only genuine deny/block violates).
2243
+ if (dec === 'ask') return 'ask';
2244
+ if (dec === 'allow') return 'allow';
2245
+ return 'pass';
2246
+ } catch { return 'pass'; }
2247
+ }
2248
+
784
2249
  function loadMcpJwt(): string {
785
2250
  try { return readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch { return ''; }
786
2251
  }
@@ -883,14 +2348,28 @@ interface StubOpts {
883
2348
 
884
2349
  export async function runStub(surface: string, opts: StubOpts = {}): Promise<void> {
885
2350
  const harness = isCursor(opts.harness) ? 'cursor' : 'cc';
2351
+ const startedAt = Date.now();
2352
+ process.env.SYNKRO_TELEMETRY_EMITTER = process.env.SYNKRO_TELEMETRY_EMITTER || ('hook:' + surface);
2353
+ let telemPayload: any = null;
2354
+ let telemCwd = '';
2355
+ let telemSessionId = '';
886
2356
  try {
887
2357
  const input = await readStdin();
888
2358
  if (!input.trim()) { out(failOpen(harness)); return; }
889
2359
  const payload = JSON.parse(input);
2360
+ telemPayload = payload;
2361
+ telemCwd = (typeof payload?.cwd === 'string' ? payload.cwd : '') || '';
2362
+ telemSessionId = String(payload?.session_id || payload?.conversation_id || '');
890
2363
 
891
2364
  // Cloud MCP enforcement: the gate can't reach the local server, so decide
892
- // against the org policy directly. Fail-open inside cloudMcpGate.
893
- if (surface === 'mcp-gate' && deployIsCloud()) { out(await cloudMcpGate(payload, harness)); return; }
2365
+ // against the org policy directly. Fail-open inside cloudMcpGate. Emit the
2366
+ // same mcp_tool_use (+ violation on block) the local path does, for parity.
2367
+ if (surface === 'mcp-gate' && deployIsCloud()) {
2368
+ const cloudResp = await cloudMcpGate(payload, harness);
2369
+ out(cloudResp);
2370
+ emitStubTelemetry(surface, harness, payload, cloudResp, Date.now() - startedAt, telemCwd, telemSessionId);
2371
+ return;
2372
+ }
894
2373
 
895
2374
  const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots : [];
896
2375
  const cwd = (typeof payload.cwd === 'string' && payload.cwd) || workspaceRoots[0] || '';
@@ -906,6 +2385,22 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
906
2385
  try { synkroFileText = readFileSync(join(root, 'synkro.toml'), 'utf-8'); } catch {}
907
2386
  }
908
2387
  if (!synkroFileText) {
2388
+ // Repo not onboarded — emit a minimal tool_call so usage analytics still
2389
+ // sees the hook fire, then short-circuit. No verdict, no command body.
2390
+ // transcript-sync is a background sync, not a tool invocation — omit it so
2391
+ // it doesn't inflate tool_call counts. No model derivation on this hot path.
2392
+ if (surface !== 'transcript-sync') {
2393
+ hookEmit('tool_call', {
2394
+ tool_name: String(payload?.tool_name || ''),
2395
+ hook_event: surface === 'bash-followup' ? 'PostToolUse' : 'PreToolUse',
2396
+ tool_input: {},
2397
+ verdict: 'pass',
2398
+ route: 'local',
2399
+ route_fallback_reason: 'repo_not_onboarded',
2400
+ silent: false,
2401
+ duration_ms: Date.now() - startedAt,
2402
+ }, telemPayloadOpts(payload, harness, telemCwd, telemSessionId));
2403
+ }
909
2404
  if (opts.telemetry) { out(failOpen(harness)); return; }
910
2405
  const hint = noSynkroHint(sessionId);
911
2406
  if (!hint) { out(failOpen(harness)); return; }
@@ -985,9 +2480,228 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
985
2480
  } catch (e) { /* connection refused / timeout → retry below */ }
986
2481
  if (i < attempts - 1) await new Promise((r) => setTimeout(r, 500 * (i + 1)));
987
2482
  }
988
- out(text || failOpen(harness));
989
- } catch {
2483
+ const responseText = text || failOpen(harness);
2484
+ out(responseText);
2485
+ emitStubTelemetry(surface, harness, telemPayload, responseText, Date.now() - startedAt, telemCwd, telemSessionId);
2486
+ } catch (err) {
990
2487
  out(failOpen(harness));
2488
+ const msg = err instanceof Error ? err.message : String(err);
2489
+ const stack = err instanceof Error ? (err.stack || '') : '';
2490
+ hookEmit('error', {
2491
+ category: 'hook_runtime',
2492
+ code: 'STUB_RUNSTUB_THREW',
2493
+ message: msg.slice(0, 4096),
2494
+ stack: stack.slice(0, 16384),
2495
+ emitter_context: 'runStub:' + surface,
2496
+ }, telemPayloadOpts(telemPayload, harness, telemCwd, telemSessionId));
2497
+ }
2498
+ }
2499
+
2500
+ // ─── Bounded transcript tail read + model derivation ────────────────────────
2501
+ // The CC model lives in the LAST assistant message, always near the end of the
2502
+ // transcript. Read at most the last 200KB off disk instead of slurping+splitting
2503
+ // the whole file (transcripts reach 100s of MB → O(n^2) per session on the
2504
+ // verdict hot path). Only ever called on the ACTIVE path, never when dormant.
2505
+ const TRANSCRIPT_TAIL_BYTES = 200000;
2506
+ function readTranscriptTail(path: string): string {
2507
+ try {
2508
+ const size = statSync(path).size;
2509
+ if (size <= TRANSCRIPT_TAIL_BYTES) return readFileSync(path, 'utf-8');
2510
+ const fd = openSync(path, 'r');
2511
+ try {
2512
+ const buf = Buffer.allocUnsafe(TRANSCRIPT_TAIL_BYTES);
2513
+ const n = readSync(fd, buf, 0, TRANSCRIPT_TAIL_BYTES, size - TRANSCRIPT_TAIL_BYTES);
2514
+ return buf.subarray(0, n).toString('utf-8');
2515
+ } finally { closeSync(fd); }
2516
+ } catch { return ''; }
2517
+ }
2518
+ function deriveCcModel(payload: any): string {
2519
+ const tp = (payload && payload.transcript_path) || '';
2520
+ if (!tp || !existsSync(tp)) return '';
2521
+ const lines = readTranscriptTail(tp).split('\n');
2522
+ for (let i = lines.length - 1; i >= 0; i--) {
2523
+ if (!lines[i].includes('"type":"assistant"')) continue;
2524
+ try {
2525
+ const obj = JSON.parse(lines[i]);
2526
+ if (obj?.message?.model) return String(obj.message.model);
2527
+ } catch { /* torn line (tail may start mid-record) — skip, keep scanning */ }
2528
+ }
2529
+ return '';
2530
+ }
2531
+
2532
+ // ─── Telemetry payload hygiene: scrub + byte-cap + summarize ────────────────
2533
+ // Self-contained port of _synkro-common's scrubSecrets — stub mode DELETES
2534
+ // _synkro-common.ts, so the stub cannot import it. Same hard-secret patterns +
2535
+ // credential key/value redaction; the entropy heuristic is omitted since these
2536
+ // rows never carry file bodies and every field is additionally byte-capped.
2537
+ const TE_SECRET_PATTERNS: Array<{ type: string; re: RegExp }> = [
2538
+ { type: 'anthropic-key', re: /sk-ant-[A-Za-z0-9_-]{24,}/g },
2539
+ { type: 'openai-key', re: /sk-(?:proj-)?[A-Za-z0-9]{32,}/g },
2540
+ { type: 'aws-access-key', re: /AKIA[0-9A-Z]{16}/g },
2541
+ { type: 'github-token', re: /gh[posru]_[A-Za-z0-9]{36,}/g },
2542
+ { type: 'google-api-key', re: /AIza[0-9A-Za-z_-]{35}/g },
2543
+ { type: 'slack-token', re: /xox[baprs]-[0-9A-Za-z-]{10,}/g },
2544
+ { type: 'stripe-key', re: /[rs]k_(?:live|test)_[A-Za-z0-9]{20,}/g },
2545
+ { type: 'jwt', re: /eyJ[A-Za-z0-9_-]{10,}[.][A-Za-z0-9_-]{10,}[.][A-Za-z0-9_-]{10,}/g },
2546
+ { type: 'private-key', re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g },
2547
+ { type: 'bearer-token', re: /[Bb]earer[ ]+[A-Za-z0-9._-]{20,}/g },
2548
+ ];
2549
+ function isPlaceholderSecret(v: string): boolean {
2550
+ if (/[$<>{}]/.test(v)) return true;
2551
+ const tail = v.replace(/^(sk-ant-|[rs]k_(?:live|test)_|sk-|AKIA|gh[posru]_|AIza|xox[baprs]-|Bearer[ ]+)/, '');
2552
+ return /^(x+|0+|a+|placeholder|example|changeme|your[-_a-z0-9]*|dummy|test|fake|redacted|sample)$/i.test(tail);
2553
+ }
2554
+ function scrubSecrets(text: string): string {
2555
+ if (!text) return text;
2556
+ let out = text;
2557
+ for (const { type, re } of TE_SECRET_PATTERNS) {
2558
+ out = out.replace(re, (m) => isPlaceholderSecret(m) ? m : '<redacted:' + type + '>');
2559
+ }
2560
+ out = out.replace(/((?:password|passwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|private[_-]?key)["' ]*[=:][ ]*["']?)([^ "'\n]{12,})/gi, (full, pfx, val) => {
2561
+ if (/[$<>{}]/.test(val)) return full;
2562
+ return pfx + '<redacted:credential>';
2563
+ });
2564
+ return out;
2565
+ }
2566
+ // Byte-cap (not char-cap): multi-byte content must not push a JSONL row past the
2567
+ // ~4KB append-atomicity window, above which concurrent hook appends tear.
2568
+ function cap(s: string, maxBytes: number): string {
2569
+ if (!s) return s;
2570
+ if (Buffer.byteLength(s, 'utf-8') <= maxBytes) return s;
2571
+ let out = s.slice(0, maxBytes);
2572
+ while (out && Buffer.byteLength(out, 'utf-8') > maxBytes) out = out.slice(0, -1);
2573
+ return out;
2574
+ }
2575
+ // Field byte budgets. A row's fixed envelope (ids, timestamps, cwd, model) runs
2576
+ // ~0.8-1.2KB, so no single free-text field may approach 4KB or the whole JSONL
2577
+ // row overruns the append-atomicity window. These leave margin even together.
2578
+ const TE_CMD_CAP = 1200;
2579
+ const TE_REASON_CAP = 800;
2580
+ const TE_EXCERPT_CAP = 1200;
2581
+ const TE_PROMPT_CAP = 2400;
2582
+ // Never embed a raw tool_input — Write/Edit carry whole file bodies, Bash the raw
2583
+ // command, so verbatim rows exfiltrate secrets AND overrun the 4KB row budget.
2584
+ // Emit a fixed-size fingerprint: tool name, top-level keys, serialized byte
2585
+ // length, short content hash. Bash is the one surface where the command text is
2586
+ // itself the signal, so include it scrubbed + byte-capped when asked.
2587
+ function toolInputSummary(toolName: string, toolInput: any, includeCommand: boolean): Record<string, any> {
2588
+ let ser = '';
2589
+ try { ser = JSON.stringify(toolInput == null ? {} : toolInput); } catch { ser = String(toolInput); }
2590
+ const isObj = !!toolInput && typeof toolInput === 'object' && !Array.isArray(toolInput);
2591
+ const summary: Record<string, any> = {
2592
+ tool_name: toolName || undefined,
2593
+ keys: isObj ? Object.keys(toolInput).slice(0, 24) : [],
2594
+ bytes: Buffer.byteLength(ser, 'utf-8'),
2595
+ hash: createHash('sha256').update(ser).digest('hex').slice(0, 12),
2596
+ };
2597
+ if (includeCommand && isObj && typeof toolInput.command === 'string' && toolInput.command) {
2598
+ summary.command = cap(scrubSecrets(toolInput.command), TE_CMD_CAP);
2599
+ }
2600
+ return summary;
2601
+ }
2602
+
2603
+ // cc_model is derived by the caller (bounded tail read) and passed in; this stays
2604
+ // allocation-only so the dormant hot path never touches the transcript.
2605
+ function telemPayloadOpts(payload: any, harness: string, cwd: string, sessionId: string, ccModel?: string): any {
2606
+ return {
2607
+ agent_kind: harness === 'cursor' ? 'cursor' : 'claude_code',
2608
+ cc_session_id: sessionId || undefined,
2609
+ cc_tool_use_id: payload?.tool_use_id ? String(payload.tool_use_id) : undefined,
2610
+ cc_model: ccModel || undefined,
2611
+ permission_mode: payload?.permission_mode ? String(payload.permission_mode) : undefined,
2612
+ cwd: cwd || undefined,
2613
+ };
2614
+ }
2615
+
2616
+ function emitStubTelemetry(
2617
+ surface: string, harness: string, payload: any, responseText: string,
2618
+ durationMs: number, cwd: string, sessionId: string,
2619
+ ): void {
2620
+ // transcript-sync is a background sync, not a tool invocation — no tool_call
2621
+ // row (keeps tool_call counts accurate); its usage lands via session_lifecycle.
2622
+ // Bail before any transcript read so it stays cheap.
2623
+ if (surface === 'transcript-sync') return;
2624
+
2625
+ const opts = telemPayloadOpts(payload, harness, cwd, sessionId, deriveCcModel(payload));
2626
+ const verdict = parseVerdict(responseText, harness);
2627
+ const meta = eventForSurface(surface);
2628
+
2629
+ if (meta.sessionPhase === 'start') {
2630
+ hookEmit('session_lifecycle', {
2631
+ phase: 'start', route: 'local-cc', channel_port: Number(PORT) || 18931,
2632
+ channel_reachable: true, open_findings: 0,
2633
+ transcript_path: payload?.transcript_path || undefined,
2634
+ }, opts);
2635
+ return;
2636
+ }
2637
+ if (meta.sessionPhase === 'end') {
2638
+ hookEmit('session_lifecycle', {
2639
+ phase: 'end', route: 'local-cc', channel_port: Number(PORT) || 18931,
2640
+ channel_reachable: true, open_findings: 0,
2641
+ transcript_path: payload?.transcript_path || undefined,
2642
+ }, opts);
2643
+ return;
2644
+ }
2645
+ if (meta.type === 'human_interaction') {
2646
+ const kind = surface === 'prompt-submit' ? 'prompt_submit' : 'consent_consume';
2647
+ const promptText = surface === 'prompt-submit'
2648
+ ? cap(String(payload?.prompt || payload?.message || payload?.content || ''), TE_PROMPT_CAP)
2649
+ : undefined;
2650
+ hookEmit('human_interaction', { kind, prompt_text: promptText }, opts);
2651
+ return;
2652
+ }
2653
+ if (meta.type === 'mcp_tool_use') {
2654
+ const toolName = String(payload?.tool_name || payload?.tool || '');
2655
+ hookEmit('mcp_tool_use', {
2656
+ tool_name: toolName,
2657
+ args_summary: toolInputSummary(toolName, payload?.tool_input, true),
2658
+ duration_ms: durationMs,
2659
+ success: verdict !== 'deny' && verdict !== 'error',
2660
+ }, opts);
2661
+ if (verdict === 'deny') {
2662
+ hookEmit('violation', {
2663
+ hook_type: 'bash', rule_id: '', rule_mode: 'blocking', severity: 'high',
2664
+ category: 'mcp_governance', reason: 'mcp_blocked',
2665
+ rules_checked: [], violated_rules: [],
2666
+ command: cap(scrubSecrets(toolName), TE_CMD_CAP),
2667
+ tool_name: toolName, hook_event: 'PreToolUse', tool_input: toolInputSummary(toolName, payload?.tool_input, false),
2668
+ verdict: 'block', route: 'local', silent: false, duration_ms: durationMs,
2669
+ }, opts);
2670
+ }
2671
+ return;
2672
+ }
2673
+ if (meta.type === 'plan_review_result') {
2674
+ hookEmit('plan_review_result', {
2675
+ verdict: verdict === 'deny' ? 'advisory' : 'clean',
2676
+ reason: '', plan_excerpt: cap(scrubSecrets(String(payload?.tool_input?.plan || '')), TE_EXCERPT_CAP),
2677
+ rule_ids_relevant: [], route: 'local',
2678
+ }, opts);
2679
+ return;
2680
+ }
2681
+ // Default: tool_call. Add a violation row ONLY on a genuine denial ('ask' is a
2682
+ // consent prompt, not a block). tool_input is summarized, never embedded raw.
2683
+ const toolName = String(payload?.tool_name || '');
2684
+ hookEmit('tool_call', {
2685
+ tool_name: toolName,
2686
+ hook_event: surface === 'bash-followup' ? 'PostToolUse' : 'PreToolUse',
2687
+ tool_input: toolInputSummary(toolName, payload?.tool_input, true),
2688
+ verdict, route: 'local', silent: false, duration_ms: durationMs,
2689
+ }, opts);
2690
+ if (verdict === 'deny') {
2691
+ let hookType: 'bash' | 'edit' | 'plan' | 'cve_scan' | 'cwe_scan' = 'bash';
2692
+ if (surface === 'edit-precheck') hookType = 'edit';
2693
+ else if (surface === 'cwe-precheck') hookType = 'cwe_scan';
2694
+ else if (surface === 'cve-precheck') hookType = 'cve_scan';
2695
+ else if (surface === 'plan-judge') hookType = 'plan';
2696
+ hookEmit('violation', {
2697
+ hook_type: hookType,
2698
+ rule_id: '', rule_mode: 'blocking', severity: 'high', category: 'policy',
2699
+ reason: cap('server-denied', TE_REASON_CAP),
2700
+ rules_checked: [], violated_rules: [],
2701
+ command: cap(scrubSecrets(String(payload?.tool_input?.command || filePathFromToolInput(payload?.tool_input) || '')), TE_CMD_CAP),
2702
+ tool_name: toolName, hook_event: 'PreToolUse', tool_input: toolInputSummary(toolName, payload?.tool_input, false),
2703
+ verdict: 'block', route: 'local', silent: false, duration_ms: durationMs,
2704
+ }, opts);
991
2705
  }
992
2706
  }
993
2707
  `;
@@ -1048,13 +2762,13 @@ __export(stub_exports, {
1048
2762
  saveCredentials: () => saveCredentials
1049
2763
  });
1050
2764
  import { createServer } from "http";
1051
- import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "fs";
1052
- import { homedir as homedir4, platform } from "os";
1053
- import { join as join3, dirname as dirname4 } from "path";
2765
+ import { writeFileSync as writeFileSync9, readFileSync as readFileSync11, existsSync as existsSync14, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4 } from "fs";
2766
+ import { homedir as homedir11, platform as platform2 } from "os";
2767
+ import { join as join10, dirname as dirname4 } from "path";
1054
2768
  import { execFile } from "child_process";
1055
2769
  import jwt from "jsonwebtoken";
1056
2770
  function openBrowser(url) {
1057
- const os = platform();
2771
+ const os = platform2();
1058
2772
  let bin;
1059
2773
  let args2;
1060
2774
  switch (os) {
@@ -1079,17 +2793,17 @@ function openBrowser(url) {
1079
2793
  }
1080
2794
  function saveCredentials(data) {
1081
2795
  const dir = dirname4(AUTH_FILE);
1082
- if (!existsSync5(dir)) {
1083
- mkdirSync4(dir, { recursive: true, mode: 448 });
2796
+ if (!existsSync14(dir)) {
2797
+ mkdirSync7(dir, { recursive: true, mode: 448 });
1084
2798
  }
1085
- writeFileSync4(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
2799
+ writeFileSync9(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
1086
2800
  }
1087
2801
  function loadCredentials() {
1088
- if (!existsSync5(AUTH_FILE)) {
2802
+ if (!existsSync14(AUTH_FILE)) {
1089
2803
  return null;
1090
2804
  }
1091
2805
  try {
1092
- const content = readFileSync4(AUTH_FILE, "utf8");
2806
+ const content = readFileSync11(AUTH_FILE, "utf8");
1093
2807
  return JSON.parse(content);
1094
2808
  } catch (error) {
1095
2809
  return null;
@@ -1223,22 +2937,22 @@ function createCallbackServer() {
1223
2937
  });
1224
2938
  }
1225
2939
  async function authenticate(onStatus) {
1226
- const emit = onStatus || (() => {
2940
+ const emit2 = onStatus || (() => {
1227
2941
  });
1228
2942
  try {
1229
- emit({ phase: "starting" });
2943
+ emit2({ phase: "starting" });
1230
2944
  const serverPromise = createCallbackServer();
1231
2945
  const authUrl = `${SYNKRO_WEB_AUTH_URL}/cli-auth?port=${PORT}`;
1232
2946
  openBrowser(authUrl);
1233
- emit({ phase: "browser-opened", url: authUrl });
1234
- emit({ phase: "waiting" });
2947
+ emit2({ phase: "browser-opened", url: authUrl });
2948
+ emit2({ phase: "waiting" });
1235
2949
  const data = await serverPromise;
1236
- emit({ phase: "success" });
2950
+ emit2({ phase: "success" });
1237
2951
  saveCredentials(data);
1238
2952
  return data;
1239
2953
  } catch (error) {
1240
2954
  const message = error instanceof Error ? error.message : String(error);
1241
- emit({ phase: "error", message });
2955
+ emit2({ phase: "error", message });
1242
2956
  return null;
1243
2957
  }
1244
2958
  }
@@ -1344,8 +3058,8 @@ async function ensureValidToken() {
1344
3058
  return true;
1345
3059
  }
1346
3060
  function clearCredentials() {
1347
- if (existsSync5(AUTH_FILE)) {
1348
- unlinkSync2(AUTH_FILE);
3061
+ if (existsSync14(AUTH_FILE)) {
3062
+ unlinkSync4(AUTH_FILE);
1349
3063
  }
1350
3064
  }
1351
3065
  async function getSecrets(userId, integrationId) {
@@ -1364,7 +3078,7 @@ var init_stub = __esm({
1364
3078
  PORT = 8100;
1365
3079
  RAW_WEB_AUTH_URL = process.env.SYNKRO_WEB_AUTH_URL;
1366
3080
  SYNKRO_WEB_AUTH_URL = RAW_WEB_AUTH_URL && /^https?:\/\//.test(RAW_WEB_AUTH_URL) ? RAW_WEB_AUTH_URL : "https://app.synkro.sh";
1367
- AUTH_FILE = process.env.SYNKRO_AUTH_FILE || join3(homedir4(), ".synkro", "credentials.json");
3081
+ AUTH_FILE = process.env.SYNKRO_AUTH_FILE || join10(homedir11(), ".synkro", "credentials.json");
1368
3082
  RAW_API_URL = process.env.SYNKRO_CRUD_URL || process.env.SYNKRO_API_URL;
1369
3083
  SYNKRO_API_URL = RAW_API_URL && /^https?:\/\//.test(RAW_API_URL) ? RAW_API_URL : "https://api.synkro.sh";
1370
3084
  ERROR_HTML = `
@@ -1524,9 +3238,9 @@ jobs:
1524
3238
  });
1525
3239
 
1526
3240
  // cli/installer/githubSetup.ts
1527
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
3241
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
1528
3242
  import { execSync as execSync2 } from "child_process";
1529
- import { join as join4 } from "path";
3243
+ import { join as join11 } from "path";
1530
3244
  function ghSecretSet(token, owner, repo, name, value) {
1531
3245
  execSync2(`gh secret set ${name} --repo ${owner}/${repo} --body -`, {
1532
3246
  input: value,
@@ -1572,17 +3286,17 @@ async function pushSecretsToRepo(opts, owner, repo, secrets) {
1572
3286
  ghSecretSet(opts.token, owner, repo, "SYNKRO_API_KEY", secrets.synkroApiKey);
1573
3287
  }
1574
3288
  function writeWorkflowFile(repoRootPath) {
1575
- const workflowDir = join4(repoRootPath, ".github", "workflows");
1576
- mkdirSync5(workflowDir, { recursive: true });
1577
- const workflowFile = join4(workflowDir, "synkro.yml");
1578
- writeFileSync5(workflowFile, SYNKRO_WORKFLOW_YAML, "utf-8");
3289
+ const workflowDir = join11(repoRootPath, ".github", "workflows");
3290
+ mkdirSync8(workflowDir, { recursive: true });
3291
+ const workflowFile = join11(workflowDir, "synkro.yml");
3292
+ writeFileSync10(workflowFile, SYNKRO_WORKFLOW_YAML, "utf-8");
1579
3293
  return workflowFile;
1580
3294
  }
1581
3295
  function findGitRoot(startCwd) {
1582
3296
  let cur = startCwd;
1583
3297
  while (cur && cur !== "/") {
1584
- if (existsSync6(join4(cur, ".git"))) return cur;
1585
- const parent = join4(cur, "..");
3298
+ if (existsSync15(join11(cur, ".git"))) return cur;
3299
+ const parent = join11(cur, "..");
1586
3300
  if (parent === cur) break;
1587
3301
  cur = parent;
1588
3302
  }
@@ -1602,7 +3316,7 @@ var init_githubSetup = __esm({
1602
3316
  });
1603
3317
 
1604
3318
  // cli/commands/repoConnect.ts
1605
- import { execSync as execSync3, execFileSync as execFileSync2 } from "child_process";
3319
+ import { execSync as execSync3, execFileSync as execFileSync3 } from "child_process";
1606
3320
  import { readdirSync } from "fs";
1607
3321
  import { createServer as createServer2 } from "http";
1608
3322
  import { createInterface } from "readline";
@@ -1626,7 +3340,7 @@ function detectSubdirRepos() {
1626
3340
  for (const entry of entries) {
1627
3341
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
1628
3342
  try {
1629
- const remoteUrl = execFileSync2("git", ["-C", entry.name, "remote", "get-url", "origin"], {
3343
+ const remoteUrl = execFileSync3("git", ["-C", entry.name, "remote", "get-url", "origin"], {
1630
3344
  encoding: "utf-8",
1631
3345
  timeout: 5e3,
1632
3346
  stdio: ["pipe", "pipe", "pipe"]
@@ -1931,12 +3645,12 @@ __export(claudeDesktopTap_exports, {
1931
3645
  claudeDesktopInstalled: () => claudeDesktopInstalled,
1932
3646
  runClaudeDesktopTap: () => runClaudeDesktopTap
1933
3647
  });
1934
- import { spawn } from "child_process";
1935
- import { writeFileSync as writeFileSync6, mkdtempSync, mkdirSync as mkdirSync6, readFileSync as readFileSync5, existsSync as existsSync7 } from "fs";
1936
- import { join as join5 } from "path";
1937
- import { homedir as homedir5 } from "os";
3648
+ import { spawn as spawn2 } from "child_process";
3649
+ import { writeFileSync as writeFileSync11, mkdtempSync, mkdirSync as mkdirSync9, readFileSync as readFileSync12, existsSync as existsSync16 } from "fs";
3650
+ import { join as join12 } from "path";
3651
+ import { homedir as homedir12 } from "os";
1938
3652
  function claudeDesktopInstalled() {
1939
- return process.platform === "darwin" && existsSync7("/Applications/Claude.app/Contents/MacOS/Claude");
3653
+ return process.platform === "darwin" && existsSync16("/Applications/Claude.app/Contents/MacOS/Claude");
1940
3654
  }
1941
3655
  function buildRunner(sessionDir) {
1942
3656
  return `#!/bin/bash
@@ -2050,7 +3764,7 @@ async function runClaudeDesktopTap(opts = {}) {
2050
3764
  }
2051
3765
  let token = "";
2052
3766
  try {
2053
- token = readFileSync5(JWT_PATH, "utf-8").trim();
3767
+ token = readFileSync12(JWT_PATH, "utf-8").trim();
2054
3768
  } catch {
2055
3769
  }
2056
3770
  if (!token) {
@@ -2071,16 +3785,16 @@ async function runClaudeDesktopTap(opts = {}) {
2071
3785
  } catch (err) {
2072
3786
  console.log(` \u26A0 Could not register Claude Desktop MCP: ${err.message}`);
2073
3787
  }
2074
- const cdRoot = join5(homedir5(), ".synkro", "cd-sessions");
2075
- mkdirSync6(cdRoot, { recursive: true });
2076
- const sessionDir = mkdtempSync(join5(cdRoot, "synkro-cd-"));
2077
- writeFileSync6(join5(sessionDir, "tap.py"), ADDON_PY, "utf-8");
2078
- writeFileSync6(join5(sessionDir, "mcp_proxy.py"), MCP_PROXY_PY, { mode: 493 });
2079
- writeFileSync6(join5(sessionDir, "mcp_patch.py"), MCP_PATCH_PY, "utf-8");
2080
- const runnerPath = join5(sessionDir, "run.sh");
2081
- writeFileSync6(runnerPath, buildRunner(sessionDir), { mode: 493 });
3788
+ const cdRoot = join12(homedir12(), ".synkro", "cd-sessions");
3789
+ mkdirSync9(cdRoot, { recursive: true });
3790
+ const sessionDir = mkdtempSync(join12(cdRoot, "synkro-cd-"));
3791
+ writeFileSync11(join12(sessionDir, "tap.py"), ADDON_PY, "utf-8");
3792
+ writeFileSync11(join12(sessionDir, "mcp_proxy.py"), MCP_PROXY_PY, { mode: 493 });
3793
+ writeFileSync11(join12(sessionDir, "mcp_patch.py"), MCP_PATCH_PY, "utf-8");
3794
+ const runnerPath = join12(sessionDir, "run.sh");
3795
+ writeFileSync11(runnerPath, buildRunner(sessionDir), { mode: 493 });
2082
3796
  await new Promise((resolve4) => {
2083
- const child = spawn("bash", [runnerPath], {
3797
+ const child = spawn2("bash", [runnerPath], {
2084
3798
  stdio: "inherit",
2085
3799
  env: { ...process.env, SYNKRO_CAPTURE_URL: CAPTURE_URL, SYNKRO_SCAN_URL: SCAN_URL, SYNKRO_SCAN_TURN_URL: SCAN_TURN_URL, SYNKRO_DLP_POLICY_URL: DLP_POLICY_URL, SYNKRO_TURN_VERDICTS_URL: TURN_VERDICTS_URL, SYNKRO_TURN_VERDICT_URL: TURN_VERDICT_URL, SYNKRO_MCP_EVENT_URL: MCP_EVENT_URL, SYNKRO_TAP_TOKEN: token, SYNKRO_TAP_TOKEN_FILE: JWT_PATH, SYNKRO_CD_BACKFILL: opts.backfill ? "1" : "" }
2086
3800
  });
@@ -2111,7 +3825,7 @@ var init_claudeDesktopTap = __esm({
2111
3825
  TURN_VERDICTS_URL = "http://127.0.0.1:18931/api/local/turn-verdicts";
2112
3826
  TURN_VERDICT_URL = "http://127.0.0.1:18931/api/local/turn-verdict";
2113
3827
  MCP_EVENT_URL = "http://127.0.0.1:18931/api/local/mcp/event";
2114
- JWT_PATH = join5(homedir5(), ".synkro", ".mcp-jwt");
3828
+ JWT_PATH = join12(homedir12(), ".synkro", ".mcp-jwt");
2115
3829
  ADDON_PY = `import os, json, threading, urllib.request, urllib.error, re, base64, time, hashlib
2116
3830
  from mitmproxy import http
2117
3831
 
@@ -3213,7 +4927,7 @@ __export(macKeychain_exports, {
3213
4927
  CURSOR_API_KEY_FILE: () => CURSOR_API_KEY_FILE,
3214
4928
  CURSOR_CREDS_DIR: () => CURSOR_CREDS_DIR,
3215
4929
  KeychainExportError: () => KeychainExportError,
3216
- SYNKRO_DIR: () => SYNKRO_DIR,
4930
+ SYNKRO_DIR: () => SYNKRO_DIR6,
3217
4931
  credsAreStale: () => credsAreStale,
3218
4932
  cursorApiKeyConfigured: () => cursorApiKeyConfigured,
3219
4933
  exportKeychainCreds: () => exportKeychainCreds,
@@ -3227,15 +4941,15 @@ __export(macKeychain_exports, {
3227
4941
  writeCursorApiKey: () => writeCursorApiKey,
3228
4942
  writeRefreshAgent: () => writeRefreshAgent
3229
4943
  });
3230
- import { existsSync as existsSync8, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7, chmodSync, readFileSync as readFileSync6 } from "fs";
3231
- import { homedir as homedir6, platform as platform2 } from "os";
3232
- import { join as join6 } from "path";
4944
+ import { existsSync as existsSync17, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12, chmodSync as chmodSync2, readFileSync as readFileSync13 } from "fs";
4945
+ import { homedir as homedir13, platform as platform3 } from "os";
4946
+ import { join as join13 } from "path";
3233
4947
  import { spawnSync as spawnSync2 } from "child_process";
3234
4948
  function needsKeychainBridge() {
3235
- return platform2() === "darwin";
4949
+ return platform3() === "darwin";
3236
4950
  }
3237
4951
  function readKeychainCreds() {
3238
- if (platform2() !== "darwin") return null;
4952
+ if (platform3() !== "darwin") return null;
3239
4953
  const r = spawnSync2("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-w"], {
3240
4954
  encoding: "utf-8",
3241
4955
  timeout: 5e3
@@ -3247,22 +4961,22 @@ function readKeychainCreds() {
3247
4961
  function exportKeychainCreds() {
3248
4962
  const blob = readKeychainCreds();
3249
4963
  if (!blob) return null;
3250
- mkdirSync7(CLAUDE_CREDS_DIR, { recursive: true });
3251
- chmodSync(CLAUDE_CREDS_DIR, 448);
4964
+ mkdirSync10(CLAUDE_CREDS_DIR, { recursive: true });
4965
+ chmodSync2(CLAUDE_CREDS_DIR, 448);
3252
4966
  let changed = true;
3253
4967
  try {
3254
- if (existsSync8(CLAUDE_CREDS_FILE)) {
3255
- changed = readFileSync6(CLAUDE_CREDS_FILE, "utf-8") !== blob;
4968
+ if (existsSync17(CLAUDE_CREDS_FILE)) {
4969
+ changed = readFileSync13(CLAUDE_CREDS_FILE, "utf-8") !== blob;
3256
4970
  }
3257
4971
  } catch {
3258
4972
  }
3259
- if (changed) writeFileSync7(CLAUDE_CREDS_FILE, blob, "utf-8");
3260
- chmodSync(CLAUDE_CREDS_FILE, 384);
4973
+ if (changed) writeFileSync12(CLAUDE_CREDS_FILE, blob, "utf-8");
4974
+ chmodSync2(CLAUDE_CREDS_FILE, 384);
3261
4975
  return CLAUDE_CREDS_FILE;
3262
4976
  }
3263
4977
  function cursorApiKeyConfigured() {
3264
4978
  try {
3265
- return existsSync8(CURSOR_API_KEY_FILE) && readFileSync6(CURSOR_API_KEY_FILE, "utf-8").trim().length > 0;
4979
+ return existsSync17(CURSOR_API_KEY_FILE) && readFileSync13(CURSOR_API_KEY_FILE, "utf-8").trim().length > 0;
3266
4980
  } catch {
3267
4981
  return false;
3268
4982
  }
@@ -3270,15 +4984,15 @@ function cursorApiKeyConfigured() {
3270
4984
  function writeCursorApiKey(key) {
3271
4985
  const trimmed = key.trim();
3272
4986
  if (!trimmed) return;
3273
- mkdirSync7(CURSOR_CREDS_DIR, { recursive: true });
3274
- chmodSync(CURSOR_CREDS_DIR, 448);
3275
- writeFileSync7(CURSOR_API_KEY_FILE, trimmed, "utf-8");
3276
- chmodSync(CURSOR_API_KEY_FILE, 384);
4987
+ mkdirSync10(CURSOR_CREDS_DIR, { recursive: true });
4988
+ chmodSync2(CURSOR_CREDS_DIR, 448);
4989
+ writeFileSync12(CURSOR_API_KEY_FILE, trimmed, "utf-8");
4990
+ chmodSync2(CURSOR_API_KEY_FILE, 384);
3277
4991
  }
3278
4992
  async function validateCursorApiKey() {
3279
4993
  let key;
3280
4994
  try {
3281
- key = readFileSync6(CURSOR_API_KEY_FILE, "utf-8").trim();
4995
+ key = readFileSync13(CURSOR_API_KEY_FILE, "utf-8").trim();
3282
4996
  } catch {
3283
4997
  return null;
3284
4998
  }
@@ -3297,9 +5011,9 @@ async function validateCursorApiKey() {
3297
5011
  }
3298
5012
  }
3299
5013
  function credsAreStale() {
3300
- if (!existsSync8(CLAUDE_CREDS_FILE)) return true;
5014
+ if (!existsSync17(CLAUDE_CREDS_FILE)) return true;
3301
5015
  try {
3302
- const raw = readFileSync6(CLAUDE_CREDS_FILE, "utf-8");
5016
+ const raw = readFileSync13(CLAUDE_CREDS_FILE, "utf-8");
3303
5017
  const exp = JSON.parse(raw)?.claudeAiOauth?.expiresAt ?? 0;
3304
5018
  if (!exp) return true;
3305
5019
  return Date.now() >= exp - REFRESH_EXPIRY_BUFFER_SECONDS * 1e3;
@@ -3311,11 +5025,11 @@ function xmlEscape(s) {
3311
5025
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
3312
5026
  }
3313
5027
  function writeRefreshAgent(synkroBinPath) {
3314
- if (platform2() !== "darwin") {
5028
+ if (platform3() !== "darwin") {
3315
5029
  throw new KeychainExportError("writeRefreshAgent is darwin-only");
3316
5030
  }
3317
- mkdirSync7(join6(homedir6(), "Library", "LaunchAgents"), { recursive: true });
3318
- mkdirSync7(SYNKRO_DIR, { recursive: true });
5031
+ mkdirSync10(join13(homedir13(), "Library", "LaunchAgents"), { recursive: true });
5032
+ mkdirSync10(SYNKRO_DIR6, { recursive: true });
3319
5033
  const script = `#!/bin/bash
3320
5034
  # Generated by synkro (writeRefreshAgent). Expiry-aware Claude creds refresher.
3321
5035
  # SYNKRO_BIN / SYNKRO_CF are injected by launchd as environment data \u2014 never
@@ -3340,8 +5054,8 @@ while true; do
3340
5054
  sleep "$delay"
3341
5055
  done
3342
5056
  `;
3343
- writeFileSync7(REFRESH_SCRIPT, script, "utf-8");
3344
- chmodSync(REFRESH_SCRIPT, 448);
5057
+ writeFileSync12(REFRESH_SCRIPT, script, "utf-8");
5058
+ chmodSync2(REFRESH_SCRIPT, 448);
3345
5059
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
3346
5060
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3347
5061
  <plist version="1.0">
@@ -3373,11 +5087,11 @@ done
3373
5087
  </dict>
3374
5088
  </plist>
3375
5089
  `;
3376
- writeFileSync7(LAUNCHD_PLIST, plist, "utf-8");
5090
+ writeFileSync12(LAUNCHD_PLIST, plist, "utf-8");
3377
5091
  return LAUNCHD_PLIST;
3378
5092
  }
3379
5093
  function loadRefreshAgent() {
3380
- if (platform2() !== "darwin") return;
5094
+ if (platform3() !== "darwin") return;
3381
5095
  spawnSync2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, LAUNCHD_PLIST], {
3382
5096
  encoding: "utf-8",
3383
5097
  timeout: 5e3
@@ -3393,15 +5107,15 @@ function loadRefreshAgent() {
3393
5107
  }
3394
5108
  }
3395
5109
  function uninstallRefreshAgent() {
3396
- if (platform2() !== "darwin") return;
5110
+ if (platform3() !== "darwin") return;
3397
5111
  spawnSync2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, LAUNCHD_PLIST], {
3398
5112
  encoding: "utf-8",
3399
5113
  timeout: 5e3
3400
5114
  });
3401
5115
  try {
3402
5116
  const fs = __require("fs");
3403
- if (existsSync8(LAUNCHD_PLIST)) fs.unlinkSync(LAUNCHD_PLIST);
3404
- if (existsSync8(REFRESH_SCRIPT)) fs.unlinkSync(REFRESH_SCRIPT);
5117
+ if (existsSync17(LAUNCHD_PLIST)) fs.unlinkSync(LAUNCHD_PLIST);
5118
+ if (existsSync17(REFRESH_SCRIPT)) fs.unlinkSync(REFRESH_SCRIPT);
3405
5119
  } catch {
3406
5120
  }
3407
5121
  }
@@ -3411,25 +5125,25 @@ function refreshCreds() {
3411
5125
  }
3412
5126
  function readExportedCreds() {
3413
5127
  try {
3414
- return readFileSync6(CLAUDE_CREDS_FILE, "utf-8");
5128
+ return readFileSync13(CLAUDE_CREDS_FILE, "utf-8");
3415
5129
  } catch {
3416
5130
  return null;
3417
5131
  }
3418
5132
  }
3419
- var SYNKRO_DIR, CLAUDE_CREDS_DIR, CLAUDE_CREDS_FILE, CURSOR_CREDS_DIR, CURSOR_API_KEY_FILE, KEYCHAIN_SERVICE, LAUNCHD_LABEL, LAUNCHD_PLIST, REFRESH_SCRIPT, REFRESH_LOG, REFRESH_EXPIRY_BUFFER_SECONDS, MIN_REFRESH_INTERVAL_SECONDS, MAX_REFRESH_INTERVAL_SECONDS, KeychainExportError;
5133
+ var SYNKRO_DIR6, CLAUDE_CREDS_DIR, CLAUDE_CREDS_FILE, CURSOR_CREDS_DIR, CURSOR_API_KEY_FILE, KEYCHAIN_SERVICE, LAUNCHD_LABEL, LAUNCHD_PLIST, REFRESH_SCRIPT, REFRESH_LOG, REFRESH_EXPIRY_BUFFER_SECONDS, MIN_REFRESH_INTERVAL_SECONDS, MAX_REFRESH_INTERVAL_SECONDS, KeychainExportError;
3420
5134
  var init_macKeychain = __esm({
3421
5135
  "cli/local-cc/macKeychain.ts"() {
3422
5136
  "use strict";
3423
- SYNKRO_DIR = join6(homedir6(), ".synkro");
3424
- CLAUDE_CREDS_DIR = join6(SYNKRO_DIR, "claude-creds");
3425
- CLAUDE_CREDS_FILE = join6(CLAUDE_CREDS_DIR, ".credentials.json");
3426
- CURSOR_CREDS_DIR = join6(SYNKRO_DIR, "cursor-creds");
3427
- CURSOR_API_KEY_FILE = join6(CURSOR_CREDS_DIR, "api-key");
5137
+ SYNKRO_DIR6 = join13(homedir13(), ".synkro");
5138
+ CLAUDE_CREDS_DIR = join13(SYNKRO_DIR6, "claude-creds");
5139
+ CLAUDE_CREDS_FILE = join13(CLAUDE_CREDS_DIR, ".credentials.json");
5140
+ CURSOR_CREDS_DIR = join13(SYNKRO_DIR6, "cursor-creds");
5141
+ CURSOR_API_KEY_FILE = join13(CURSOR_CREDS_DIR, "api-key");
3428
5142
  KEYCHAIN_SERVICE = "Claude Code-credentials";
3429
5143
  LAUNCHD_LABEL = "com.synkro.cli.claude-creds-refresh";
3430
- LAUNCHD_PLIST = join6(homedir6(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
3431
- REFRESH_SCRIPT = join6(SYNKRO_DIR, "claude-creds-refresh-loop.sh");
3432
- REFRESH_LOG = join6(SYNKRO_DIR, "claude-creds-refresh.log");
5144
+ LAUNCHD_PLIST = join13(homedir13(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
5145
+ REFRESH_SCRIPT = join13(SYNKRO_DIR6, "claude-creds-refresh-loop.sh");
5146
+ REFRESH_LOG = join13(SYNKRO_DIR6, "claude-creds-refresh.log");
3433
5147
  REFRESH_EXPIRY_BUFFER_SECONDS = 120;
3434
5148
  MIN_REFRESH_INTERVAL_SECONDS = 30;
3435
5149
  MAX_REFRESH_INTERVAL_SECONDS = 5 * 60;
@@ -3448,7 +5162,7 @@ var init_macKeychain = __esm({
3448
5162
  var dockerInstall_exports = {};
3449
5163
  __export(dockerInstall_exports, {
3450
5164
  DockerInstallError: () => DockerInstallError,
3451
- SYNKRO_DIR: () => SYNKRO_DIR2,
5165
+ SYNKRO_DIR: () => SYNKRO_DIR7,
3452
5166
  assertDockerAvailable: () => assertDockerAvailable,
3453
5167
  dockerInstall: () => dockerInstall,
3454
5168
  dockerRemove: () => dockerRemove,
@@ -3468,9 +5182,9 @@ __export(dockerInstall_exports, {
3468
5182
  waitForContainerReady: () => waitForContainerReady,
3469
5183
  waitForWorkersReady: () => waitForWorkersReady
3470
5184
  });
3471
- import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync7, readdirSync as readdirSync2 } from "fs";
3472
- import { homedir as homedir7 } from "os";
3473
- import { join as join7 } from "path";
5185
+ import { copyFileSync, existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync14, readdirSync as readdirSync2 } from "fs";
5186
+ import { homedir as homedir14 } from "os";
5187
+ import { join as join14 } from "path";
3474
5188
  import { execSync as execSync4, spawnSync as spawnSync3 } from "child_process";
3475
5189
  function splitWorkers(total, providers) {
3476
5190
  const t = Math.max(0, Math.floor(total));
@@ -3565,9 +5279,9 @@ function readSynkroFileConfig() {
3565
5279
  try {
3566
5280
  const root = execSync4("git rev-parse --show-toplevel 2>/dev/null", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
3567
5281
  if (!root) return { pool: "auto", warnings: [] };
3568
- const fp = join7(root, "synkro.toml");
3569
- if (!existsSync9(fp)) return { pool: "auto", warnings: [] };
3570
- const parsed = parseSynkroToml(readFileSync7(fp, "utf-8"));
5282
+ const fp = join14(root, "synkro.toml");
5283
+ if (!existsSync18(fp)) return { pool: "auto", warnings: [] };
5284
+ const parsed = parseSynkroToml(readFileSync14(fp, "utf-8"));
3571
5285
  return resolveGraderPool(parsed);
3572
5286
  } catch {
3573
5287
  }
@@ -3642,7 +5356,7 @@ function assertDockerAvailable() {
3642
5356
  }
3643
5357
  function claudeCredsHostDir() {
3644
5358
  if (needsKeychainBridge()) return CLAUDE_CREDS_DIR;
3645
- return join7(homedir7(), ".claude");
5359
+ return join14(homedir14(), ".claude");
3646
5360
  }
3647
5361
  function resolveSynkroBin() {
3648
5362
  const which2 = spawnSync3("which", ["synkro"], { encoding: "utf-8", timeout: 5e3 });
@@ -3695,19 +5409,19 @@ async function dockerInstall(opts = {}) {
3695
5409
  const claudeWorkers = opts.claudeWorkers ?? opts.workersPerPool ?? 8;
3696
5410
  const cursorWorkers = opts.cursorWorkers ?? 0;
3697
5411
  const totalWorkers = claudeWorkers + cursorWorkers;
3698
- mkdirSync8(PGDATA_PATH, { recursive: true });
3699
- mkdirSync8(BACKUP_DIR, { recursive: true });
3700
- mkdirSync8(CLAUDE_HOST_STATE_DIR, { recursive: true });
3701
- const hostClaudeJson = join7(homedir7(), ".claude.json");
3702
- if (existsSync9(hostClaudeJson)) {
5412
+ mkdirSync11(PGDATA_PATH, { recursive: true });
5413
+ mkdirSync11(BACKUP_DIR, { recursive: true });
5414
+ mkdirSync11(CLAUDE_HOST_STATE_DIR, { recursive: true });
5415
+ const hostClaudeJson = join14(homedir14(), ".claude.json");
5416
+ if (existsSync18(hostClaudeJson)) {
3703
5417
  copyFileSync(hostClaudeJson, CLAUDE_HOST_STATE_FILE);
3704
5418
  }
3705
- if (!existsSync9(MCP_JWT_PATH)) {
5419
+ if (!existsSync18(MCP_JWT_PATH)) {
3706
5420
  throw new DockerInstallError(
3707
5421
  `MCP JWT missing at ${MCP_JWT_PATH}. The installer should mint this before calling dockerInstall.`
3708
5422
  );
3709
5423
  }
3710
- mkdirSync8(CURSOR_CREDS_DIR, { recursive: true });
5424
+ mkdirSync11(CURSOR_CREDS_DIR, { recursive: true });
3711
5425
  if (needsKeychainBridge()) {
3712
5426
  const claudeCredsPath = exportKeychainCreds();
3713
5427
  if (!claudeCredsPath) {
@@ -3731,7 +5445,7 @@ async function dockerInstall(opts = {}) {
3731
5445
  console.warn(` Plist written to ${plist} \u2014 load manually with launchctl bootstrap when ready.`);
3732
5446
  }
3733
5447
  } else {
3734
- mkdirSync8(join7(homedir7(), ".claude"), { recursive: true });
5448
+ mkdirSync11(join14(homedir14(), ".claude"), { recursive: true });
3735
5449
  }
3736
5450
  const imageExistsLocally = () => spawnSync3("docker", ["image", "inspect", image], { stdio: "ignore", timeout: 3e4 }).status === 0;
3737
5451
  const skipPull = process.env.SYNKRO_SKIP_PULL === "1" || process.env.SYNKRO_SKIP_PULL === "true";
@@ -3780,11 +5494,11 @@ async function dockerInstall(opts = {}) {
3780
5494
  // sidesteps Docker Desktop for macOS's unreliable single-file bind mounts
3781
5495
  // (which previously left a dangling symlink that blocked container start).
3782
5496
  "-v",
3783
- `${SYNKRO_DIR2}:/data/synkro-host:ro`,
5497
+ `${SYNKRO_DIR7}:/data/synkro-host:ro`,
3784
5498
  "-v",
3785
5499
  `${credsDir}:/home/synkro/.claude:rw`,
3786
5500
  "-v",
3787
- `${join7(homedir7(), ".claude")}:/data/claude-host:ro`,
5501
+ `${join14(homedir14(), ".claude")}:/data/claude-host:ro`,
3788
5502
  "-v",
3789
5503
  `${CLAUDE_HOST_STATE_DIR}:/data/claude-host-state:ro`,
3790
5504
  // Cursor creds — mounted RW so the in-container refresher can rotate the
@@ -3814,6 +5528,16 @@ async function dockerInstall(opts = {}) {
3814
5528
  ...process.env.SYNKRO_ORG_ID ? ["-e", `SYNKRO_ORG_ID=${process.env.SYNKRO_ORG_ID}`] : [],
3815
5529
  ...process.env.SYNKRO_USER_ID ? ["-e", `SYNKRO_USER_ID=${process.env.SYNKRO_USER_ID}`] : [],
3816
5530
  ...process.env.SYNKRO_EMAIL ? ["-e", `SYNKRO_EMAIL=${process.env.SYNKRO_EMAIL}`] : [],
5531
+ // Container-owned cloud telemetry flush (docker/telemetryCloudFlush.ts). The
5532
+ // server drains the READ-ONLY host queue (already mounted at
5533
+ // /data/synkro-host via the RO mount above — NO new mount) into its pglite
5534
+ // and pushes unflushed rows to the D1 telemetry worker. The loop is gated on
5535
+ // SYNKRO_TELEMETRY_URL and is harmless when there's nothing to flush, so we
5536
+ // always set it (defaulting to the D1 worker). Override via env to redirect.
5537
+ "-e",
5538
+ `SYNKRO_TELEMETRY_URL=${process.env.SYNKRO_TELEMETRY_URL || "https://admin.synkro.sh"}`,
5539
+ "-e",
5540
+ "SYNKRO_TELEMETRY_QUEUE=/data/synkro-host/telemetry-pending.jsonl",
3817
5541
  image
3818
5542
  ];
3819
5543
  const run = spawnSync3("docker", args2, { encoding: "utf-8", stdio: "inherit", timeout: 6e4 });
@@ -3969,7 +5693,7 @@ async function dockerSafeStart() {
3969
5693
  return { ok: false, pgdataState: "no_container", error: "No synkro-server container found. Run `synkro install` first." };
3970
5694
  }
3971
5695
  const pgCheck = checkPgdata();
3972
- if (existsSync9(PGDATA_PATH) && readdirSync2(PGDATA_PATH).length > 0) {
5696
+ if (existsSync18(PGDATA_PATH) && readdirSync2(PGDATA_PATH).length > 0) {
3973
5697
  if (pgCheck.healthy) {
3974
5698
  console.log(` pgdata: existing data found \u2014 ${pgCheck.details}`);
3975
5699
  } else {
@@ -3978,7 +5702,7 @@ async function dockerSafeStart() {
3978
5702
  }
3979
5703
  } else {
3980
5704
  console.log(" pgdata: no existing data \u2014 fresh start.");
3981
- mkdirSync8(PGDATA_PATH, { recursive: true });
5705
+ mkdirSync11(PGDATA_PATH, { recursive: true });
3982
5706
  }
3983
5707
  console.log(" Starting container...");
3984
5708
  const start = spawnSync3("docker", ["start", CONTAINER_NAME], {
@@ -4009,7 +5733,7 @@ async function dockerSafeRestart() {
4009
5733
  return { ok: startResult.ok, stop: stopResult, start: startResult };
4010
5734
  }
4011
5735
  function checkPgdata() {
4012
- if (!existsSync9(PGDATA_PATH)) return { healthy: false, details: "pgdata directory does not exist" };
5736
+ if (!existsSync18(PGDATA_PATH)) return { healthy: false, details: "pgdata directory does not exist" };
4013
5737
  const entries = readdirSync2(PGDATA_PATH);
4014
5738
  if (entries.length === 0) return { healthy: true, details: "empty (fresh start)" };
4015
5739
  const hasPidFile = entries.includes("postmaster.pid");
@@ -4020,17 +5744,17 @@ function checkPgdata() {
4020
5744
  if (!hasPgControl) return { healthy: false, details: "pg_control/global directory missing" };
4021
5745
  return { healthy: true, details: `${entries.length} entries, WAL present, no stale PID` };
4022
5746
  }
4023
- var SYNKRO_DIR2, MCP_JWT_PATH, PGDATA_PATH, CLAUDE_HOST_STATE_DIR, CLAUDE_HOST_STATE_FILE, HOST_MCP_PORT, HOST_GRADER_PORT, HOST_CWE_PORT, HOST_PGLITE_PORT, CONTAINER_NAME, DEFAULT_IMAGE, DockerInstallError, BACKUP_DIR;
5747
+ var SYNKRO_DIR7, MCP_JWT_PATH, PGDATA_PATH, CLAUDE_HOST_STATE_DIR, CLAUDE_HOST_STATE_FILE, HOST_MCP_PORT, HOST_GRADER_PORT, HOST_CWE_PORT, HOST_PGLITE_PORT, CONTAINER_NAME, DEFAULT_IMAGE, DockerInstallError, BACKUP_DIR;
4024
5748
  var init_dockerInstall = __esm({
4025
5749
  "cli/local-cc/dockerInstall.ts"() {
4026
5750
  "use strict";
4027
5751
  init_agentDetect();
4028
5752
  init_macKeychain();
4029
- SYNKRO_DIR2 = join7(homedir7(), ".synkro");
4030
- MCP_JWT_PATH = join7(SYNKRO_DIR2, ".mcp-jwt");
4031
- PGDATA_PATH = join7(SYNKRO_DIR2, "pgdata");
4032
- CLAUDE_HOST_STATE_DIR = join7(SYNKRO_DIR2, "claude-host-state");
4033
- CLAUDE_HOST_STATE_FILE = join7(CLAUDE_HOST_STATE_DIR, ".claude.json");
5753
+ SYNKRO_DIR7 = join14(homedir14(), ".synkro");
5754
+ MCP_JWT_PATH = join14(SYNKRO_DIR7, ".mcp-jwt");
5755
+ PGDATA_PATH = join14(SYNKRO_DIR7, "pgdata");
5756
+ CLAUDE_HOST_STATE_DIR = join14(SYNKRO_DIR7, "claude-host-state");
5757
+ CLAUDE_HOST_STATE_FILE = join14(CLAUDE_HOST_STATE_DIR, ".claude.json");
4034
5758
  HOST_MCP_PORT = parseInt(process.env.SYNKRO_HOST_MCP_PORT || "18931", 10);
4035
5759
  HOST_GRADER_PORT = parseInt(process.env.SYNKRO_HOST_GRADER_PORT || "18929", 10);
4036
5760
  HOST_CWE_PORT = parseInt(process.env.SYNKRO_HOST_CWE_PORT || "18930", 10);
@@ -4045,18 +5769,18 @@ var init_dockerInstall = __esm({
4045
5769
  }
4046
5770
  cause;
4047
5771
  };
4048
- BACKUP_DIR = join7(SYNKRO_DIR2, "pgdata-backups");
5772
+ BACKUP_DIR = join14(SYNKRO_DIR7, "pgdata-backups");
4049
5773
  }
4050
5774
  });
4051
5775
 
4052
5776
  // cli/local-cc/setupToken.ts
4053
5777
  import { spawn as nodeSpawn } from "child_process";
4054
- import { readFileSync as readFileSync8, unlinkSync as unlinkSync3 } from "fs";
4055
- import { homedir as homedir8, platform as platform3 } from "os";
4056
- import { join as join8 } from "path";
5778
+ import { readFileSync as readFileSync15, unlinkSync as unlinkSync5 } from "fs";
5779
+ import { homedir as homedir15, platform as platform4 } from "os";
5780
+ import { join as join15 } from "path";
4057
5781
  function captureClaudeSetupToken() {
4058
- const tmpFile = join8(SYNKRO_DIR3, `token-capture-${Date.now()}.raw`);
4059
- const isMac = platform3() === "darwin";
5782
+ const tmpFile = join15(SYNKRO_DIR8, `token-capture-${Date.now()}.raw`);
5783
+ const isMac = platform4() === "darwin";
4060
5784
  const bin = "script";
4061
5785
  const args2 = isMac ? ["-q", tmpFile, "claude", "setup-token"] : ["-qec", "claude setup-token", tmpFile];
4062
5786
  const OAUTH_HINT = 'The browser approval did not return a token. This usually means claude.ai rejected the OAuth request (its "Authorization failed \u2014 Unsupported media type" page), most often because a browser extension (Grammarly, ad/script blockers, AI-assistant toolbars) stripped the request headers, or you approved on the wrong Claude account. Fix: retry in a clean incognito window with extensions disabled, and approve on the Claude account you want the cloud workers to use.';
@@ -4069,13 +5793,13 @@ function captureClaudeSetupToken() {
4069
5793
  proc.on("close", (code) => {
4070
5794
  let raw = "";
4071
5795
  try {
4072
- raw = readFileSync8(tmpFile, "utf-8");
5796
+ raw = readFileSync15(tmpFile, "utf-8");
4073
5797
  } catch (e) {
4074
5798
  reject(new Error(`Could not read script output file: ${e.message}`));
4075
5799
  return;
4076
5800
  }
4077
5801
  try {
4078
- unlinkSync3(tmpFile);
5802
+ unlinkSync5(tmpFile);
4079
5803
  } catch {
4080
5804
  }
4081
5805
  if (code !== 0) {
@@ -4131,11 +5855,11 @@ function captureClaudeSetupToken() {
4131
5855
  });
4132
5856
  });
4133
5857
  }
4134
- var SYNKRO_DIR3;
5858
+ var SYNKRO_DIR8;
4135
5859
  var init_setupToken = __esm({
4136
5860
  "cli/local-cc/setupToken.ts"() {
4137
5861
  "use strict";
4138
- SYNKRO_DIR3 = join8(homedir8(), ".synkro");
5862
+ SYNKRO_DIR8 = join15(homedir15(), ".synkro");
4139
5863
  }
4140
5864
  });
4141
5865
 
@@ -4145,9 +5869,9 @@ __export(codexCloudSetup_exports, {
4145
5869
  setupCodexCloud: () => setupCodexCloud
4146
5870
  });
4147
5871
  import { spawn as nodeSpawn2, spawnSync as spawnSync4 } from "child_process";
4148
- import { readFileSync as readFileSync9, mkdirSync as mkdirSync9, rmSync, existsSync as existsSync10 } from "fs";
4149
- import { homedir as homedir9 } from "os";
4150
- import { join as join9 } from "path";
5872
+ import { readFileSync as readFileSync16, mkdirSync as mkdirSync12, rmSync, existsSync as existsSync19 } from "fs";
5873
+ import { homedir as homedir16 } from "os";
5874
+ import { join as join16 } from "path";
4151
5875
  function findCodexBinary() {
4152
5876
  if (process.env.SYNKRO_CODEX_BIN) return process.env.SYNKRO_CODEX_BIN;
4153
5877
  const r = spawnSync4("which", ["codex"], { encoding: "utf-8" });
@@ -4155,7 +5879,7 @@ function findCodexBinary() {
4155
5879
  return p || null;
4156
5880
  }
4157
5881
  function runCodexLogin(codexBin) {
4158
- mkdirSync9(CODEX_CLOUD_HOME, { recursive: true, mode: 448 });
5882
+ mkdirSync12(CODEX_CLOUD_HOME, { recursive: true, mode: 448 });
4159
5883
  return new Promise((resolve4, reject) => {
4160
5884
  const proc = nodeSpawn2(codexBin, ["login"], {
4161
5885
  stdio: "inherit",
@@ -4176,13 +5900,13 @@ async function setupCodexCloud(gatewayUrl, bearerToken, onStatus) {
4176
5900
  } catch (e) {
4177
5901
  return { ok: false, error: `Codex login failed: ${e.message}` };
4178
5902
  }
4179
- const authPath = join9(CODEX_CLOUD_HOME, "auth.json");
4180
- if (!existsSync10(authPath)) {
5903
+ const authPath = join16(CODEX_CLOUD_HOME, "auth.json");
5904
+ if (!existsSync19(authPath)) {
4181
5905
  return { ok: false, error: "codex login completed but no auth.json was written \u2014 did the browser approval finish?" };
4182
5906
  }
4183
5907
  let auth;
4184
5908
  try {
4185
- auth = JSON.parse(readFileSync9(authPath, "utf-8"));
5909
+ auth = JSON.parse(readFileSync16(authPath, "utf-8"));
4186
5910
  } catch (e) {
4187
5911
  return { ok: false, error: `could not read codex auth.json: ${e.message}` };
4188
5912
  }
@@ -4211,12 +5935,12 @@ async function setupCodexCloud(gatewayUrl, bearerToken, onStatus) {
4211
5935
  onStatus?.("\u2713 Codex subscription connected to Synkro Cloud.");
4212
5936
  return { ok: true };
4213
5937
  }
4214
- var SYNKRO_DIR4, CODEX_CLOUD_HOME;
5938
+ var SYNKRO_DIR9, CODEX_CLOUD_HOME;
4215
5939
  var init_codexCloudSetup = __esm({
4216
5940
  "cli/local-cc/codexCloudSetup.ts"() {
4217
5941
  "use strict";
4218
- SYNKRO_DIR4 = join9(homedir9(), ".synkro");
4219
- CODEX_CLOUD_HOME = join9(SYNKRO_DIR4, "codex-cloud-session");
5942
+ SYNKRO_DIR9 = join16(homedir16(), ".synkro");
5943
+ CODEX_CLOUD_HOME = join16(SYNKRO_DIR9, "codex-cloud-session");
4220
5944
  }
4221
5945
  });
4222
5946
 
@@ -4229,14 +5953,14 @@ __export(setupGithub_exports, {
4229
5953
  import { createInterface as createInterface2 } from "readline/promises";
4230
5954
  import { stdin as input, stdout as output } from "process";
4231
5955
  import { execSync as execSync5 } from "child_process";
4232
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
4233
- import { homedir as homedir10, platform as platform4 } from "os";
4234
- import { join as join10 } from "path";
5956
+ import { existsSync as existsSync20, readFileSync as readFileSync17 } from "fs";
5957
+ import { homedir as homedir17, platform as platform5 } from "os";
5958
+ import { join as join17 } from "path";
4235
5959
  import { execFile as execFile2 } from "child_process";
4236
5960
  function readConfig() {
4237
- if (!existsSync11(CONFIG_PATH)) return {};
5961
+ if (!existsSync20(CONFIG_PATH3)) return {};
4238
5962
  const out = {};
4239
- for (const line of readFileSync10(CONFIG_PATH, "utf-8").split("\n")) {
5963
+ for (const line of readFileSync17(CONFIG_PATH3, "utf-8").split("\n")) {
4240
5964
  const t = line.trim();
4241
5965
  if (!t || t.startsWith("#")) continue;
4242
5966
  const eq = t.indexOf("=");
@@ -4273,7 +5997,7 @@ async function prompt(rl, q, opts = {}) {
4273
5997
  return await rl.question(q);
4274
5998
  }
4275
5999
  function openBrowser3(url) {
4276
- const os = platform4();
6000
+ const os = platform5();
4277
6001
  let bin;
4278
6002
  let args2;
4279
6003
  switch (os) {
@@ -4536,15 +6260,15 @@ Will push secrets to ${selected.length} repo(s):`);
4536
6260
  console.log(`Secrets pushed: ${SECRET_NAMES.CLAUDE_OAUTH}, ${SECRET_NAMES.SYNKRO_API_KEY}`);
4537
6261
  console.log("Open a PR on any selected repo to trigger your first Synkro scan.");
4538
6262
  }
4539
- var SYNKRO_DIR5, CONFIG_PATH;
6263
+ var SYNKRO_DIR10, CONFIG_PATH3;
4540
6264
  var init_setupGithub = __esm({
4541
6265
  "cli/commands/setupGithub.ts"() {
4542
6266
  "use strict";
4543
6267
  init_githubSetup();
4544
6268
  init_stub();
4545
6269
  init_setupToken();
4546
- SYNKRO_DIR5 = join10(homedir10(), ".synkro");
4547
- CONFIG_PATH = join10(SYNKRO_DIR5, "config.env");
6270
+ SYNKRO_DIR10 = join17(homedir17(), ".synkro");
6271
+ CONFIG_PATH3 = join17(SYNKRO_DIR10, "config.env");
4548
6272
  }
4549
6273
  });
4550
6274
 
@@ -4563,12 +6287,12 @@ __export(install_exports, {
4563
6287
  syncSkillFiles: () => syncSkillFiles,
4564
6288
  writeHookScripts: () => writeHookScripts
4565
6289
  });
4566
- import { existsSync as existsSync12, mkdirSync as mkdirSync10, writeFileSync as writeFileSync8, chmodSync as chmodSync2, readFileSync as readFileSync11, readdirSync as readdirSync3, unlinkSync as unlinkSync4, statSync } from "fs";
4567
- import { homedir as homedir11 } from "os";
4568
- import { join as join11 } from "path";
6290
+ import { existsSync as existsSync21, mkdirSync as mkdirSync13, writeFileSync as writeFileSync13, chmodSync as chmodSync3, readFileSync as readFileSync18, readdirSync as readdirSync3, unlinkSync as unlinkSync6, statSync as statSync2 } from "fs";
6291
+ import { homedir as homedir18 } from "os";
6292
+ import { join as join18 } from "path";
4569
6293
  import { execSync as execSync6 } from "child_process";
4570
6294
  import { createInterface as createInterface3 } from "readline";
4571
- import { createHash } from "crypto";
6295
+ import { createHash as createHash2 } from "crypto";
4572
6296
  function resolvePersistedHookMode() {
4573
6297
  return "stub";
4574
6298
  }
@@ -4710,37 +6434,37 @@ async function promptCodexCloudSetup() {
4710
6434
  });
4711
6435
  }
4712
6436
  function ensureSynkroDir() {
4713
- mkdirSync10(SYNKRO_DIR6, { recursive: true });
4714
- mkdirSync10(HOOKS_DIR, { recursive: true });
4715
- mkdirSync10(BIN_DIR, { recursive: true });
4716
- mkdirSync10(OFFSETS_DIR, { recursive: true });
4717
- mkdirSync10(join11(SYNKRO_DIR6, "sessions"), { recursive: true });
6437
+ mkdirSync13(SYNKRO_DIR11, { recursive: true });
6438
+ mkdirSync13(HOOKS_DIR, { recursive: true });
6439
+ mkdirSync13(BIN_DIR, { recursive: true });
6440
+ mkdirSync13(OFFSETS_DIR, { recursive: true });
6441
+ mkdirSync13(join18(SYNKRO_DIR11, "sessions"), { recursive: true });
4718
6442
  }
4719
6443
  function writeHookScripts() {
4720
- const installExtractCorePath = join11(HOOKS_DIR, "installExtractCore.ts");
4721
- const bashScriptPath = join11(HOOKS_DIR, "cc-bash-judge.ts");
4722
- const skillJudgeScriptPath = join11(HOOKS_DIR, "cc-skill-judge.ts");
4723
- const cursorSkillJudgePath = join11(HOOKS_DIR, "cursor-skill-judge.ts");
4724
- const bashFollowupScriptPath = join11(HOOKS_DIR, "cc-bash-followup.ts");
4725
- const editPrecheckScriptPath = join11(HOOKS_DIR, "cc-edit-precheck.ts");
4726
- const cwePrecheckScriptPath = join11(HOOKS_DIR, "cc-cwe-precheck.ts");
4727
- const cvePrecheckScriptPath = join11(HOOKS_DIR, "cc-cve-precheck.ts");
4728
- const planJudgeScriptPath = join11(HOOKS_DIR, "cc-plan-judge.ts");
4729
- const agentJudgeScriptPath = join11(HOOKS_DIR, "cc-agent-judge.ts");
4730
- const stopSummaryScriptPath = join11(HOOKS_DIR, "cc-stop-summary.ts");
4731
- const sessionStartScriptPath = join11(HOOKS_DIR, "cc-session-start.ts");
4732
- const transcriptSyncScriptPath = join11(HOOKS_DIR, "cc-transcript-sync.ts");
4733
- const userPromptSubmitScriptPath = join11(HOOKS_DIR, "cc-user-prompt-submit.ts");
4734
- const commonScriptPath = join11(HOOKS_DIR, "_synkro-common.ts");
4735
- const commonBashScriptPath = join11(HOOKS_DIR, "_synkro-common.sh");
4736
- const installScanScriptPath = join11(HOOKS_DIR, "cc-install-scan.ts");
4737
- const cursorBashJudgePath = join11(HOOKS_DIR, "cursor-bash-judge.ts");
4738
- const cursorEditCapturePath = join11(HOOKS_DIR, "cursor-edit-capture.ts");
4739
- const cursorAgentCapturePath = join11(HOOKS_DIR, "cursor-agent-capture.ts");
4740
- const mcpStdioProxyPath = join11(HOOKS_DIR, "mcp-stdio-proxy.ts");
4741
- const taskActivateIntentScriptPath = join11(HOOKS_DIR, "cc-task-activate-intent.ts");
4742
- const mcpGateScriptPath = join11(HOOKS_DIR, "cc-mcp-gate.ts");
4743
- const stubCommonPath = join11(HOOKS_DIR, "_synkro-stub-common.ts");
6444
+ const installExtractCorePath = join18(HOOKS_DIR, "installExtractCore.ts");
6445
+ const bashScriptPath = join18(HOOKS_DIR, "cc-bash-judge.ts");
6446
+ const skillJudgeScriptPath = join18(HOOKS_DIR, "cc-skill-judge.ts");
6447
+ const cursorSkillJudgePath = join18(HOOKS_DIR, "cursor-skill-judge.ts");
6448
+ const bashFollowupScriptPath = join18(HOOKS_DIR, "cc-bash-followup.ts");
6449
+ const editPrecheckScriptPath = join18(HOOKS_DIR, "cc-edit-precheck.ts");
6450
+ const cwePrecheckScriptPath = join18(HOOKS_DIR, "cc-cwe-precheck.ts");
6451
+ const cvePrecheckScriptPath = join18(HOOKS_DIR, "cc-cve-precheck.ts");
6452
+ const planJudgeScriptPath = join18(HOOKS_DIR, "cc-plan-judge.ts");
6453
+ const agentJudgeScriptPath = join18(HOOKS_DIR, "cc-agent-judge.ts");
6454
+ const stopSummaryScriptPath = join18(HOOKS_DIR, "cc-stop-summary.ts");
6455
+ const sessionStartScriptPath = join18(HOOKS_DIR, "cc-session-start.ts");
6456
+ const transcriptSyncScriptPath = join18(HOOKS_DIR, "cc-transcript-sync.ts");
6457
+ const userPromptSubmitScriptPath = join18(HOOKS_DIR, "cc-user-prompt-submit.ts");
6458
+ const commonScriptPath = join18(HOOKS_DIR, "_synkro-common.ts");
6459
+ const commonBashScriptPath = join18(HOOKS_DIR, "_synkro-common.sh");
6460
+ const installScanScriptPath = join18(HOOKS_DIR, "cc-install-scan.ts");
6461
+ const cursorBashJudgePath = join18(HOOKS_DIR, "cursor-bash-judge.ts");
6462
+ const cursorEditCapturePath = join18(HOOKS_DIR, "cursor-edit-capture.ts");
6463
+ const cursorAgentCapturePath = join18(HOOKS_DIR, "cursor-agent-capture.ts");
6464
+ const mcpStdioProxyPath = join18(HOOKS_DIR, "mcp-stdio-proxy.ts");
6465
+ const taskActivateIntentScriptPath = join18(HOOKS_DIR, "cc-task-activate-intent.ts");
6466
+ const mcpGateScriptPath = join18(HOOKS_DIR, "cc-mcp-gate.ts");
6467
+ const stubCommonPath = join18(HOOKS_DIR, "_synkro-stub-common.ts");
4744
6468
  const stubFiles = [
4745
6469
  [stubCommonPath, STUB_COMMON_TS],
4746
6470
  [bashScriptPath, STUB_BASH_JUDGE_TS],
@@ -4764,14 +6488,14 @@ function writeHookScripts() {
4764
6488
  [cursorAgentCapturePath, STUB_CURSOR_AGENT_CAPTURE_TS]
4765
6489
  ];
4766
6490
  for (const [p, content] of stubFiles) {
4767
- writeFileSync8(p, content, "utf-8");
4768
- chmodSync2(p, 493);
6491
+ writeFileSync13(p, content, "utf-8");
6492
+ chmodSync3(p, 493);
4769
6493
  }
4770
- writeFileSync8(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
4771
- chmodSync2(mcpStdioProxyPath, 493);
6494
+ writeFileSync13(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
6495
+ chmodSync3(mcpStdioProxyPath, 493);
4772
6496
  for (const stale of ["_synkro-common.ts", "_synkro-common.sh", "installExtractCore.ts"]) {
4773
6497
  try {
4774
- unlinkSync4(join11(HOOKS_DIR, stale));
6498
+ unlinkSync6(join18(HOOKS_DIR, stale));
4775
6499
  } catch {
4776
6500
  }
4777
6501
  }
@@ -4801,16 +6525,16 @@ function sanitizeConfigValue(raw, maxLen = 256) {
4801
6525
  if (!raw) return "";
4802
6526
  return raw.replace(/[^\x20-\x7E]/g, "").slice(0, maxLen);
4803
6527
  }
4804
- function shellQuoteSingle(value) {
6528
+ function shellQuoteSingle2(value) {
4805
6529
  return `'${value.replace(/'/g, "'\\''")}'`;
4806
6530
  }
4807
6531
  function resolveSynkroBundle() {
4808
6532
  const scriptPath = process.argv[1];
4809
- if (scriptPath && existsSync12(scriptPath)) return scriptPath;
6533
+ if (scriptPath && existsSync21(scriptPath)) return scriptPath;
4810
6534
  return null;
4811
6535
  }
4812
6536
  function writeConfigEnv(opts) {
4813
- const credsPath = join11(SYNKRO_DIR6, "credentials.json");
6537
+ const credsPath = join18(SYNKRO_DIR11, "credentials.json");
4814
6538
  const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
4815
6539
  const safeUserId = sanitizeConfigValue(opts.userId);
4816
6540
  const safeOrgId = sanitizeConfigValue(opts.orgId);
@@ -4822,29 +6546,29 @@ function writeConfigEnv(opts) {
4822
6546
  "# Synkro CLI config (managed by synkro install)",
4823
6547
  "# JWT auth \u2014 the hook scripts read SYNKRO_CREDENTIALS_PATH at runtime",
4824
6548
  "# and send Authorization: Bearer <access_token> on every gateway call.",
4825
- `SYNKRO_GATEWAY_URL=${shellQuoteSingle(safeGateway)}`,
4826
- `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
4827
- `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
4828
- `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
4829
- `SYNKRO_VERSION=${shellQuoteSingle("1.7.61")}`
6549
+ `SYNKRO_GATEWAY_URL=${shellQuoteSingle2(safeGateway)}`,
6550
+ `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle2(credsPath)}`,
6551
+ `SYNKRO_TIER=${shellQuoteSingle2(safeTier)}`,
6552
+ `SYNKRO_INFERENCE=${shellQuoteSingle2(safeInference)}`,
6553
+ `SYNKRO_VERSION=${shellQuoteSingle2("1.7.63")}`
4830
6554
  ];
4831
- if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
4832
- if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
4833
- if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
4834
- if (safeEmail) lines.push(`SYNKRO_EMAIL=${shellQuoteSingle(safeEmail)}`);
6555
+ if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle2(safeSynkroBin)}`);
6556
+ if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle2(safeUserId)}`);
6557
+ if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle2(safeOrgId)}`);
6558
+ if (safeEmail) lines.push(`SYNKRO_EMAIL=${shellQuoteSingle2(safeEmail)}`);
4835
6559
  if (opts.transcriptConsent !== void 0) {
4836
- lines.push(`SYNKRO_TRANSCRIPT_CONSENT=${shellQuoteSingle(opts.transcriptConsent ? "yes" : "no")}`);
6560
+ lines.push(`SYNKRO_TRANSCRIPT_CONSENT=${shellQuoteSingle2(opts.transcriptConsent ? "yes" : "no")}`);
4837
6561
  }
4838
- lines.push(`SYNKRO_LOCAL_INFERENCE=${shellQuoteSingle(opts.localInference ? "yes" : "no")}`);
6562
+ lines.push(`SYNKRO_LOCAL_INFERENCE=${shellQuoteSingle2(opts.localInference ? "yes" : "no")}`);
4839
6563
  const safeMode = sanitizeConfigValue(opts.deploymentMode ?? "docker", 16);
4840
- lines.push(`SYNKRO_DEPLOYMENT_MODE=${shellQuoteSingle(safeMode)}`);
4841
- lines.push(`SYNKRO_GRADING_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.gradingMode ?? "local", 16))}`);
4842
- lines.push(`SYNKRO_STORAGE_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.storageMode ?? "local", 16))}`);
4843
- lines.push(`SYNKRO_DEPLOY_LOCATION=${shellQuoteSingle(sanitizeConfigValue(opts.deployLocation ?? "local", 16))}`);
4844
- lines.push(`SYNKRO_HOOK_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.hookMode ?? "stub", 8))}`);
6564
+ lines.push(`SYNKRO_DEPLOYMENT_MODE=${shellQuoteSingle2(safeMode)}`);
6565
+ lines.push(`SYNKRO_GRADING_MODE=${shellQuoteSingle2(sanitizeConfigValue(opts.gradingMode ?? "local", 16))}`);
6566
+ lines.push(`SYNKRO_STORAGE_MODE=${shellQuoteSingle2(sanitizeConfigValue(opts.storageMode ?? "local", 16))}`);
6567
+ lines.push(`SYNKRO_DEPLOY_LOCATION=${shellQuoteSingle2(sanitizeConfigValue(opts.deployLocation ?? "local", 16))}`);
6568
+ lines.push(`SYNKRO_HOOK_MODE=${shellQuoteSingle2(sanitizeConfigValue(opts.hookMode ?? "stub", 8))}`);
4845
6569
  lines.push("");
4846
- writeFileSync8(CONFIG_PATH2, lines.join("\n"), "utf-8");
4847
- chmodSync2(CONFIG_PATH2, 384);
6570
+ writeFileSync13(CONFIG_PATH4, lines.join("\n"), "utf-8");
6571
+ chmodSync3(CONFIG_PATH4, 384);
4848
6572
  }
4849
6573
  function jwtExpired(token) {
4850
6574
  try {
@@ -4859,7 +6583,7 @@ async function getOrMintCloudToken(gatewayUrl) {
4859
6583
  assertGatewayAllowed(gatewayUrl);
4860
6584
  let stored = "";
4861
6585
  try {
4862
- stored = readFileSync11(CLOUD_JWT_PATH, "utf-8").trim();
6586
+ stored = readFileSync18(CLOUD_JWT_PATH, "utf-8").trim();
4863
6587
  } catch {
4864
6588
  }
4865
6589
  if (stored && !jwtExpired(stored)) return stored;
@@ -4875,7 +6599,7 @@ async function getOrMintCloudToken(gatewayUrl) {
4875
6599
  throw new Error(`cloud-token mint failed (${resp.status}): ${t.slice(0, 200)}`);
4876
6600
  }
4877
6601
  const { token } = await resp.json();
4878
- writeFileSync8(CLOUD_JWT_PATH, token + "\n", { mode: 384 });
6602
+ writeFileSync13(CLOUD_JWT_PATH, token + "\n", { mode: 384 });
4879
6603
  return token;
4880
6604
  }
4881
6605
  async function provisionCloudContainer(opts) {
@@ -4918,7 +6642,7 @@ async function provisionCloudContainer(opts) {
4918
6642
  let cursorApiKey = "";
4919
6643
  if (cursorTotal > 0) {
4920
6644
  try {
4921
- cursorApiKey = readFileSync11(join11(SYNKRO_DIR6, "cursor-creds", "api-key"), "utf-8").trim();
6645
+ cursorApiKey = readFileSync18(join18(SYNKRO_DIR11, "cursor-creds", "api-key"), "utf-8").trim();
4922
6646
  } catch {
4923
6647
  }
4924
6648
  }
@@ -5075,8 +6799,8 @@ async function verifyCloudGrader(jwt2) {
5075
6799
  }
5076
6800
  function readPersistedDeployLocation() {
5077
6801
  try {
5078
- if (existsSync12(CONFIG_PATH2)) {
5079
- const m = readFileSync11(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOY_LOCATION='([^']*)'/m);
6802
+ if (existsSync21(CONFIG_PATH4)) {
6803
+ const m = readFileSync18(CONFIG_PATH4, "utf-8").match(/^SYNKRO_DEPLOY_LOCATION='([^']*)'/m);
5080
6804
  if (m?.[1] === "cloud") return "cloud";
5081
6805
  }
5082
6806
  } catch {
@@ -5084,8 +6808,8 @@ function readPersistedDeployLocation() {
5084
6808
  return "local";
5085
6809
  }
5086
6810
  function updateConfigEnvLocation(location) {
5087
- if (!existsSync12(CONFIG_PATH2)) return;
5088
- let env = readFileSync11(CONFIG_PATH2, "utf-8");
6811
+ if (!existsSync21(CONFIG_PATH4)) return;
6812
+ let env = readFileSync18(CONFIG_PATH4, "utf-8");
5089
6813
  const set = (k, v) => {
5090
6814
  const re = new RegExp(`^${k}=.*$`, "m");
5091
6815
  const line = `${k}='${v}'`;
@@ -5093,14 +6817,14 @@ function updateConfigEnvLocation(location) {
5093
6817
  };
5094
6818
  set("SYNKRO_DEPLOY_LOCATION", location);
5095
6819
  set("SYNKRO_STORAGE_MODE", location === "cloud" ? "cloud" : "local");
5096
- writeFileSync8(CONFIG_PATH2, env, "utf-8");
5097
- chmodSync2(CONFIG_PATH2, 384);
6820
+ writeFileSync13(CONFIG_PATH4, env, "utf-8");
6821
+ chmodSync3(CONFIG_PATH4, 384);
5098
6822
  }
5099
6823
  async function applyMcpConfig(opts) {
5100
6824
  if (!opts.hasClaudeCode && !opts.hasCursor) return;
5101
6825
  let mcpJwt = "";
5102
6826
  try {
5103
- mcpJwt = readFileSync11(join11(SYNKRO_DIR6, ".mcp-jwt"), "utf-8").trim();
6827
+ mcpJwt = readFileSync18(join18(SYNKRO_DIR11, ".mcp-jwt"), "utf-8").trim();
5104
6828
  } catch {
5105
6829
  }
5106
6830
  if (!mcpJwt) {
@@ -5218,8 +6942,8 @@ function resolveDeploymentMode() {
5218
6942
  const envOverride = process.env.SYNKRO_DEPLOYMENT_MODE?.toLowerCase();
5219
6943
  if (envOverride === "bare-host" || envOverride === "docker") return envOverride;
5220
6944
  try {
5221
- if (existsSync12(CONFIG_PATH2)) {
5222
- const m = readFileSync11(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
6945
+ if (existsSync21(CONFIG_PATH4)) {
6946
+ const m = readFileSync18(CONFIG_PATH4, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
5223
6947
  const val = m?.[1]?.toLowerCase();
5224
6948
  if (val === "bare-host" || val === "docker") return val;
5225
6949
  }
@@ -5246,16 +6970,16 @@ function collectLocalMetadata(includeClaudeCode = true) {
5246
6970
  meta.cc_version = execSync6("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
5247
6971
  } catch {
5248
6972
  }
5249
- const claudeDir = join11(homedir11(), ".claude");
6973
+ const claudeDir = join18(homedir18(), ".claude");
5250
6974
  try {
5251
- const settings = JSON.parse(readFileSync11(join11(claudeDir, "settings.json"), "utf-8"));
6975
+ const settings = JSON.parse(readFileSync18(join18(claudeDir, "settings.json"), "utf-8"));
5252
6976
  const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
5253
6977
  if (plugins.length) meta.enabled_plugins = plugins;
5254
6978
  if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
5255
6979
  } catch {
5256
6980
  }
5257
6981
  try {
5258
- const mcpCache = JSON.parse(readFileSync11(join11(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
6982
+ const mcpCache = JSON.parse(readFileSync18(join18(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
5259
6983
  const mcpNames = Object.keys(mcpCache);
5260
6984
  if (mcpNames.length) meta.mcp_servers = mcpNames;
5261
6985
  } catch {
@@ -5267,10 +6991,10 @@ function collectLocalMetadata(includeClaudeCode = true) {
5267
6991
  } catch {
5268
6992
  }
5269
6993
  try {
5270
- const sessionsDir = join11(claudeDir, "sessions");
6994
+ const sessionsDir = join18(claudeDir, "sessions");
5271
6995
  const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
5272
6996
  for (const f of files) {
5273
- const s = JSON.parse(readFileSync11(join11(sessionsDir, f), "utf-8"));
6997
+ const s = JSON.parse(readFileSync18(join18(sessionsDir, f), "utf-8"));
5274
6998
  if (s.version) {
5275
6999
  meta.cc_version = meta.cc_version || s.version;
5276
7000
  break;
@@ -5316,15 +7040,30 @@ function assertGatewayAllowed(gatewayUrl) {
5316
7040
  if (proto !== "http:" && proto !== "https:") {
5317
7041
  throw new Error(`Gateway URL must be http(s); got ${proto}`);
5318
7042
  }
5319
- const isLocalhost = host === "localhost" || host === "127.0.0.1" || host === "::1";
7043
+ const isLocalhost2 = host === "localhost" || host === "127.0.0.1" || host === "::1";
5320
7044
  const isSynkro = host === "synkro.sh" || host.endsWith(".synkro.sh");
5321
- if (proto === "http:" && !isLocalhost) {
7045
+ if (proto === "http:" && !isLocalhost2) {
5322
7046
  throw new Error(`Gateway URL must be HTTPS for non-localhost hosts; got ${gatewayUrl}`);
5323
7047
  }
5324
- if (!isLocalhost && !isSynkro) {
7048
+ if (!isLocalhost2 && !isSynkro) {
5325
7049
  throw new Error(`Gateway host not in allowlist (synkro.sh or *.synkro.sh): ${host}`);
5326
7050
  }
5327
7051
  }
7052
+ async function promptTelemetryConsent() {
7053
+ if (!process.stdin.isTTY) return true;
7054
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
7055
+ try {
7056
+ const a = await new Promise(
7057
+ (res) => rl.question(
7058
+ "Allow Synkro to send local product analytics (events, timings, rule activity)\nto our gateway? You can inspect data locally with `synkro telemetry stats`. (Y/n) ",
7059
+ (ans) => res(ans.trim().toLowerCase())
7060
+ )
7061
+ );
7062
+ return a === "" || a === "y" || a === "yes";
7063
+ } finally {
7064
+ rl.close();
7065
+ }
7066
+ }
5328
7067
  async function installCommand(opts = {}) {
5329
7068
  if (!detectGitRepo2()) {
5330
7069
  console.error("Synkro must be installed inside a git repository.");
@@ -5447,14 +7186,25 @@ async function installCommand(opts = {}) {
5447
7186
  }
5448
7187
  }
5449
7188
  const transcriptConsent = transcriptCC || transcriptCursor;
7189
+ const telemetryConsent = await promptTelemetryConsent();
7190
+ await setTelemetryState({ enabled: true, remoteFlushEnabled: telemetryConsent });
7191
+ emit("install", {
7192
+ phase: "started",
7193
+ cli_version_to: "1.7.63",
7194
+ agents_detected: agents.map((a) => a.kind),
7195
+ with_github: false,
7196
+ with_local_cc: false,
7197
+ with_transcript_consent: transcriptConsent,
7198
+ duration_ms: 0
7199
+ });
5450
7200
  ensureSynkroDir();
5451
7201
  const hookMode = opts.hookMode || resolvePersistedHookMode();
5452
7202
  const scripts = writeHookScripts();
5453
7203
  console.log("Wrote hook scripts to ~/.synkro/hooks/\n");
5454
7204
  for (const mode of ["edit", "bash"]) {
5455
- const pidFile = join11(SYNKRO_DIR6, "daemon", mode, "daemon.pid");
7205
+ const pidFile = join18(SYNKRO_DIR11, "daemon", mode, "daemon.pid");
5456
7206
  try {
5457
- const pid = parseInt(readFileSync11(pidFile, "utf-8").trim(), 10);
7207
+ const pid = parseInt(readFileSync18(pidFile, "utf-8").trim(), 10);
5458
7208
  if (pid > 0) {
5459
7209
  process.kill(pid, "SIGTERM");
5460
7210
  console.log(`Stopped stale ${mode} grader daemon (pid ${pid})`);
@@ -5539,7 +7289,7 @@ async function installCommand(opts = {}) {
5539
7289
  if (mintResp.ok) {
5540
7290
  const minted = await mintResp.json();
5541
7291
  mcpJwt = minted.token;
5542
- writeFileSync8(join11(SYNKRO_DIR6, ".mcp-jwt"), mcpJwt + "\n", { mode: 384 });
7292
+ writeFileSync13(join18(SYNKRO_DIR11, ".mcp-jwt"), mcpJwt + "\n", { mode: 384 });
5543
7293
  } else {
5544
7294
  console.warn(" \u26A0 Could not mint MCP token \u2014 local server will reject requests until re-installed.");
5545
7295
  }
@@ -5567,7 +7317,7 @@ async function installCommand(opts = {}) {
5567
7317
  throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
5568
7318
  }
5569
7319
  const minted = await mintResp.json();
5570
- writeFileSync8(join11(SYNKRO_DIR6, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
7320
+ writeFileSync13(join18(SYNKRO_DIR11, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
5571
7321
  const mcp = installMcpConfig({ gatewayUrl, bearerToken: minted.token });
5572
7322
  console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
5573
7323
  console.log(` url: ${mcp.url}`);
@@ -5584,8 +7334,8 @@ async function installCommand(opts = {}) {
5584
7334
  if (hasCursor && !opts.noMcp) {
5585
7335
  try {
5586
7336
  if (useLocalMcp) {
5587
- const jwtPath = join11(SYNKRO_DIR6, ".mcp-jwt");
5588
- if (!existsSync12(jwtPath)) {
7337
+ const jwtPath = join18(SYNKRO_DIR11, ".mcp-jwt");
7338
+ if (!existsSync21(jwtPath)) {
5589
7339
  const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
5590
7340
  method: "POST",
5591
7341
  headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
@@ -5593,7 +7343,7 @@ async function installCommand(opts = {}) {
5593
7343
  });
5594
7344
  if (mintResp.ok) {
5595
7345
  const minted = await mintResp.json();
5596
- writeFileSync8(jwtPath, minted.token + "\n", { mode: 384 });
7346
+ writeFileSync13(jwtPath, minted.token + "\n", { mode: 384 });
5597
7347
  }
5598
7348
  }
5599
7349
  const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: "", local: true });
@@ -5613,7 +7363,7 @@ async function installCommand(opts = {}) {
5613
7363
  throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
5614
7364
  }
5615
7365
  const minted = await mintResp.json();
5616
- writeFileSync8(join11(SYNKRO_DIR6, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
7366
+ writeFileSync13(join18(SYNKRO_DIR11, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
5617
7367
  const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: minted.token });
5618
7368
  console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
5619
7369
  console.log(` url: ${mcp.url}`);
@@ -5627,7 +7377,7 @@ async function installCommand(opts = {}) {
5627
7377
  const synkroBundle = resolveSynkroBundle();
5628
7378
  const persistedMode = resolveDeploymentMode();
5629
7379
  writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode, gradingMode, storageMode, deployLocation, hookMode });
5630
- console.log(`Wrote config to ${CONFIG_PATH2}`);
7380
+ console.log(`Wrote config to ${CONFIG_PATH4}`);
5631
7381
  console.log(` inference: ${profile.inference} (server-side grading)`);
5632
7382
  if (profile.localInference) console.log(` local inference: enabled (gradingProvider=claude-code)`);
5633
7383
  if (synkroBundle) console.log(` SYNKRO_CLI_BIN=${synkroBundle}`);
@@ -5706,7 +7456,7 @@ async function installCommand(opts = {}) {
5706
7456
  const ready = await waitForContainerReady(6e4);
5707
7457
  if (ready) {
5708
7458
  console.log(" \u2713 container ready");
5709
- const mcpJwt = readFileSync11(join11(SYNKRO_DIR6, ".mcp-jwt"), "utf-8").trim();
7459
+ const mcpJwt = readFileSync18(join18(SYNKRO_DIR11, ".mcp-jwt"), "utf-8").trim();
5710
7460
  try {
5711
7461
  const ingestResp = await fetch(`http://127.0.0.1:${hostMcpPort}/api/ingest`, {
5712
7462
  method: "POST",
@@ -5754,7 +7504,7 @@ async function installCommand(opts = {}) {
5754
7504
  try {
5755
7505
  let mcpToken = "";
5756
7506
  try {
5757
- mcpToken = readFileSync11(join11(SYNKRO_DIR6, ".mcp-jwt"), "utf-8").trim();
7507
+ mcpToken = readFileSync18(join18(SYNKRO_DIR11, ".mcp-jwt"), "utf-8").trim();
5758
7508
  } catch {
5759
7509
  }
5760
7510
  if (mcpToken) {
@@ -5890,11 +7640,11 @@ function writeSynkroFileIfMissing(opts) {
5890
7640
  try {
5891
7641
  const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
5892
7642
  if (!root) return;
5893
- if (root === homedir11()) return;
5894
- const fp = join11(root, "synkro.toml");
7643
+ if (root === homedir18()) return;
7644
+ const fp = join18(root, "synkro.toml");
5895
7645
  let hasFile = false;
5896
7646
  try {
5897
- hasFile = statSync(fp).isFile();
7647
+ hasFile = statSync2(fp).isFile();
5898
7648
  } catch {
5899
7649
  }
5900
7650
  if (hasFile) {
@@ -5927,7 +7677,7 @@ function writeSynkroFileIfMissing(opts) {
5927
7677
  "cve = true",
5928
7678
  ""
5929
7679
  ].join("\n");
5930
- writeFileSync8(fp, toml, "utf-8");
7680
+ writeFileSync13(fp, toml, "utf-8");
5931
7681
  console.log(` synkro.toml: wrote ${fp} (pool=${pool}, mode=${mode})`);
5932
7682
  } catch {
5933
7683
  }
@@ -5936,9 +7686,9 @@ function readFullSynkroFile() {
5936
7686
  try {
5937
7687
  const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
5938
7688
  if (!root) return null;
5939
- const fp = join11(root, "synkro.toml");
5940
- if (!existsSync12(fp)) return null;
5941
- const parsed = parseSynkroToml2(readFileSync11(fp, "utf-8"));
7689
+ const fp = join18(root, "synkro.toml");
7690
+ if (!existsSync21(fp)) return null;
7691
+ const parsed = parseSynkroToml2(readFileSync18(fp, "utf-8"));
5942
7692
  const valid = ["claude-code", "cursor"];
5943
7693
  const harness = Array.isArray(parsed.harness) ? parsed.harness.filter((h) => valid.includes(h)) : ["claude-code", "cursor"];
5944
7694
  const resolved = resolveGraderPool(parsed);
@@ -5976,7 +7726,7 @@ function reconcileHarness() {
5976
7726
  console.log(`synkro.toml: harness=[${sf.harness.join(", ")}] pool=${poolLabel(sf.grader.pool)} mode=${sf.grader.mode}`);
5977
7727
  const scripts = writeHookScripts();
5978
7728
  console.log("Wrote hook scripts to ~/.synkro/hooks/");
5979
- const ccSettings = join11(homedir11(), ".claude", "settings.json");
7729
+ const ccSettings = join18(homedir18(), ".claude", "settings.json");
5980
7730
  if (wantCC) {
5981
7731
  installCCHooks(ccSettings, {
5982
7732
  bashJudgeScriptPath: scripts.bashScript,
@@ -5997,7 +7747,7 @@ function reconcileHarness() {
5997
7747
  });
5998
7748
  console.log(" \u2713 Claude Code hooks registered");
5999
7749
  try {
6000
- const mcpJwt = readFileSync11(join11(SYNKRO_DIR6, ".mcp-jwt"), "utf-8").trim();
7750
+ const mcpJwt = readFileSync18(join18(SYNKRO_DIR11, ".mcp-jwt"), "utf-8").trim();
6001
7751
  if (mcpJwt) {
6002
7752
  installMcpConfig({ gatewayUrl: "", bearerToken: mcpJwt, local: true });
6003
7753
  console.log(" \u2713 Claude Code MCP registered");
@@ -6009,7 +7759,7 @@ function reconcileHarness() {
6009
7759
  if (uninstallMcpConfig()) console.log(" \u2717 Claude Code MCP removed");
6010
7760
  if (uninstallClaudeDesktopMcpConfig()) console.log(" \u2717 Claude Desktop MCP removed");
6011
7761
  }
6012
- const cursorHooks = join11(homedir11(), ".cursor", "hooks.json");
7762
+ const cursorHooks = join18(homedir18(), ".cursor", "hooks.json");
6013
7763
  if (wantCursor) {
6014
7764
  installCursorHooks(cursorHooks, {
6015
7765
  bashJudgeScriptPath: scripts.cursorBashJudgeScript,
@@ -6092,7 +7842,7 @@ async function syncSkillFiles() {
6092
7842
  if (resolved.length === 0) return;
6093
7843
  const mcpPort = process.env.SYNKRO_MCP_PORT || "18931";
6094
7844
  const tasks = resolved.map((fp) => {
6095
- const content = readFileSync11(fp, "utf-8");
7845
+ const content = readFileSync18(fp, "utf-8");
6096
7846
  const source = `skill:${fp.split("/").pop()}`;
6097
7847
  if (!content.trim()) {
6098
7848
  console.log(` \u2298 skill ${source}: empty file, skipped`);
@@ -6113,15 +7863,15 @@ function discoverSkillFiles(repoRoot, excludeHashes, ingestedHashes, ingestedNam
6113
7863
  const roots = [];
6114
7864
  const add = (p) => {
6115
7865
  try {
6116
- if (p && existsSync12(p) && statSync(p).isDirectory()) roots.push(p);
7866
+ if (p && existsSync21(p) && statSync2(p).isDirectory()) roots.push(p);
6117
7867
  } catch {
6118
7868
  }
6119
7869
  };
6120
- add(join11(homedir11(), ".claude", "skills"));
6121
- add(join11(homedir11(), ".agents", "skills"));
7870
+ add(join18(homedir18(), ".claude", "skills"));
7871
+ add(join18(homedir18(), ".agents", "skills"));
6122
7872
  if (repoRoot) {
6123
- add(join11(repoRoot, ".claude", "skills"));
6124
- add(join11(repoRoot, ".agents", "skills"));
7873
+ add(join18(repoRoot, ".claude", "skills"));
7874
+ add(join18(repoRoot, ".agents", "skills"));
6125
7875
  }
6126
7876
  const out = [];
6127
7877
  const seen = /* @__PURE__ */ new Set();
@@ -6130,14 +7880,14 @@ function discoverSkillFiles(repoRoot, excludeHashes, ingestedHashes, ingestedNam
6130
7880
  if (out.length >= MAX) return;
6131
7881
  let content = "";
6132
7882
  try {
6133
- const st = statSync(file);
7883
+ const st = statSync2(file);
6134
7884
  if (!st.isFile() || st.size > 2e5) return;
6135
- content = readFileSync11(file, "utf-8");
7885
+ content = readFileSync18(file, "utf-8");
6136
7886
  } catch {
6137
7887
  return;
6138
7888
  }
6139
7889
  if (!content.trim()) return;
6140
- const hash = createHash("sha256").update(content).digest("hex");
7890
+ const hash = createHash2("sha256").update(content).digest("hex");
6141
7891
  if (seen.has(hash) || excludeHashes.has(hash)) return;
6142
7892
  seen.add(hash);
6143
7893
  const ingested = ingestedHashes.has(hash) || ingestedNames.has(normSkillName(name));
@@ -6152,16 +7902,16 @@ function discoverSkillFiles(repoRoot, excludeHashes, ingestedHashes, ingestedNam
6152
7902
  }
6153
7903
  for (const entry of entries) {
6154
7904
  if (entry.startsWith(".")) continue;
6155
- const full = join11(root, entry);
7905
+ const full = join18(root, entry);
6156
7906
  let st;
6157
7907
  try {
6158
- st = statSync(full);
7908
+ st = statSync2(full);
6159
7909
  } catch {
6160
7910
  continue;
6161
7911
  }
6162
7912
  if (st.isDirectory()) {
6163
- const skillMd = join11(full, "SKILL.md");
6164
- if (existsSync12(skillMd)) consider(skillMd, entry);
7913
+ const skillMd = join18(full, "SKILL.md");
7914
+ if (existsSync21(skillMd)) consider(skillMd, entry);
6165
7915
  } else if (/\.mdx?$/i.test(entry) && entry.toUpperCase() !== "README.MD") {
6166
7916
  consider(full, entry.replace(/\.mdx?$/i, ""));
6167
7917
  }
@@ -6170,7 +7920,7 @@ function discoverSkillFiles(repoRoot, excludeHashes, ingestedHashes, ingestedNam
6170
7920
  return out;
6171
7921
  }
6172
7922
  function discoverySetHash(found) {
6173
- return createHash("sha256").update(found.map((f) => f.hash).sort().join(",")).digest("hex");
7923
+ return createHash2("sha256").update(found.map((f) => f.hash).sort().join(",")).digest("hex");
6174
7924
  }
6175
7925
  async function promptSkillDiscovery(found) {
6176
7926
  if (!process.stdin.isTTY || found.length === 0) return [];
@@ -6210,7 +7960,7 @@ async function discoverAndIngestSkills() {
6210
7960
  if (sf?.skills?.length) {
6211
7961
  for (const fp of resolveSkillPaths(sf.skills, sf._repoRoot)) {
6212
7962
  try {
6213
- excludeHashes.add(createHash("sha256").update(readFileSync11(fp, "utf-8")).digest("hex"));
7963
+ excludeHashes.add(createHash2("sha256").update(readFileSync18(fp, "utf-8")).digest("hex"));
6214
7964
  } catch {
6215
7965
  }
6216
7966
  }
@@ -6241,13 +7991,13 @@ async function discoverAndIngestSkills() {
6241
7991
  const setHash = discoverySetHash(selectable);
6242
7992
  let prev = "";
6243
7993
  try {
6244
- prev = readFileSync11(SKILLS_DISCOVERED_PATH, "utf-8").trim();
7994
+ prev = readFileSync18(SKILLS_DISCOVERED_PATH, "utf-8").trim();
6245
7995
  } catch {
6246
7996
  }
6247
7997
  if (prev === setHash) return;
6248
7998
  const picks = await promptSkillDiscovery(found);
6249
7999
  try {
6250
- writeFileSync8(SKILLS_DISCOVERED_PATH, setHash);
8000
+ writeFileSync13(SKILLS_DISCOVERED_PATH, setHash);
6251
8001
  } catch {
6252
8002
  }
6253
8003
  if (picks.length === 0) {
@@ -6281,17 +8031,17 @@ function detectGitRepo2() {
6281
8031
  function getClaudeProjectsFolder() {
6282
8032
  const cwd = process.cwd();
6283
8033
  const sanitized = "-" + cwd.replace(/\//g, "-");
6284
- const projectsDir = join11(homedir11(), ".claude", "projects", sanitized);
6285
- return existsSync12(projectsDir) ? projectsDir : null;
8034
+ const projectsDir = join18(homedir18(), ".claude", "projects", sanitized);
8035
+ return existsSync21(projectsDir) ? projectsDir : null;
6286
8036
  }
6287
8037
  function extractSessionInsights(projectsDir) {
6288
8038
  const insights = [];
6289
8039
  const files = readdirSync3(projectsDir).filter((f) => f.endsWith(".jsonl"));
6290
8040
  for (const file of files) {
6291
8041
  const sessionId = file.replace(".jsonl", "");
6292
- const filePath = join11(projectsDir, file);
8042
+ const filePath = join18(projectsDir, file);
6293
8043
  try {
6294
- const content = readFileSync11(filePath, "utf-8");
8044
+ const content = readFileSync18(filePath, "utf-8");
6295
8045
  const lines = content.split("\n").filter(Boolean);
6296
8046
  for (let i = 0; i < lines.length; i++) {
6297
8047
  try {
@@ -6370,14 +8120,14 @@ function cursorProjectSlug(workspaceRoot) {
6370
8120
  return workspaceRoot.replace(/^[/]+/, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
6371
8121
  }
6372
8122
  function getCursorTranscriptsDir() {
6373
- const dir = join11(homedir11(), ".cursor", "projects", cursorProjectSlug(process.cwd()), "agent-transcripts");
6374
- return existsSync12(dir) ? dir : null;
8123
+ const dir = join18(homedir18(), ".cursor", "projects", cursorProjectSlug(process.cwd()), "agent-transcripts");
8124
+ return existsSync21(dir) ? dir : null;
6375
8125
  }
6376
8126
  function isSafeConvId(id) {
6377
8127
  return /^[A-Za-z0-9_-]+$/.test(id);
6378
8128
  }
6379
8129
  function parseCursorTranscriptFile(filePath) {
6380
- const content = readFileSync11(filePath, "utf-8");
8130
+ const content = readFileSync18(filePath, "utf-8");
6381
8131
  const lines = content.split("\n").filter(Boolean);
6382
8132
  const messages = [];
6383
8133
  for (let i = 0; i < lines.length; i++) {
@@ -6409,8 +8159,8 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
6409
8159
  for (let i = 0; i < convDirs.length; i++) {
6410
8160
  const convId = convDirs[i];
6411
8161
  if (!isSafeConvId(convId)) continue;
6412
- const filePath = join11(dir, convId, `${convId}.jsonl`);
6413
- if (!existsSync12(filePath)) continue;
8162
+ const filePath = join18(dir, convId, `${convId}.jsonl`);
8163
+ if (!existsSync21(filePath)) continue;
6414
8164
  try {
6415
8165
  const all = parseCursorTranscriptFile(filePath);
6416
8166
  const messages = all.length > 500 ? all.slice(-500) : all;
@@ -6432,8 +8182,8 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
6432
8182
  process.stdout.write(`\r Progress: ${i + 1}/${convDirs.length} sessions (${totalMessages} messages embedded) `);
6433
8183
  }
6434
8184
  try {
6435
- const lc = readFileSync11(filePath, "utf-8").split("\n").filter(Boolean).length;
6436
- writeFileSync8(join11(OFFSETS_DIR, convId), String(lc), "utf-8");
8185
+ const lc = readFileSync18(filePath, "utf-8").split("\n").filter(Boolean).length;
8186
+ writeFileSync13(join18(OFFSETS_DIR, convId), String(lc), "utf-8");
6437
8187
  } catch {
6438
8188
  }
6439
8189
  }
@@ -6441,7 +8191,7 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
6441
8191
  return { sessions: totalSessions, messages: totalMessages };
6442
8192
  }
6443
8193
  function parseTranscriptFile(filePath) {
6444
- const content = readFileSync11(filePath, "utf-8");
8194
+ const content = readFileSync18(filePath, "utf-8");
6445
8195
  const lines = content.split("\n").filter(Boolean);
6446
8196
  const messages = [];
6447
8197
  for (let i = 0; i < lines.length; i++) {
@@ -6489,7 +8239,7 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
6489
8239
  for (let i = 0; i < files.length; i++) {
6490
8240
  const file = files[i];
6491
8241
  const sessionId = file.replace(".jsonl", "");
6492
- const filePath = join11(projectsDir, file);
8242
+ const filePath = join18(projectsDir, file);
6493
8243
  try {
6494
8244
  const allMessages = parseTranscriptFile(filePath);
6495
8245
  const messages = allMessages.length > 500 ? allMessages.slice(-500) : allMessages;
@@ -6511,9 +8261,9 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
6511
8261
  process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
6512
8262
  }
6513
8263
  try {
6514
- const content = readFileSync11(join11(projectsDir, file), "utf-8");
8264
+ const content = readFileSync18(join18(projectsDir, file), "utf-8");
6515
8265
  const lineCount = content.split("\n").filter(Boolean).length;
6516
- writeFileSync8(join11(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
8266
+ writeFileSync13(join18(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
6517
8267
  } catch {
6518
8268
  }
6519
8269
  }
@@ -6534,7 +8284,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
6534
8284
  const sessions = [];
6535
8285
  for (const file of batch) {
6536
8286
  const sessionId = file.replace(".jsonl", "");
6537
- const filePath = join11(projectsDir, file);
8287
+ const filePath = join18(projectsDir, file);
6538
8288
  try {
6539
8289
  const allMessages = parseTranscriptFile(filePath);
6540
8290
  const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
@@ -6563,18 +8313,18 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
6563
8313
  }
6564
8314
  for (const file of batch) {
6565
8315
  const sessionId = file.replace(".jsonl", "");
6566
- const filePath = join11(projectsDir, file);
8316
+ const filePath = join18(projectsDir, file);
6567
8317
  try {
6568
- const content = readFileSync11(filePath, "utf-8");
8318
+ const content = readFileSync18(filePath, "utf-8");
6569
8319
  const lineCount = content.split("\n").filter(Boolean).length;
6570
- writeFileSync8(join11(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
8320
+ writeFileSync13(join18(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
6571
8321
  } catch {
6572
8322
  }
6573
8323
  }
6574
8324
  }
6575
8325
  return { sessions: totalSessions, messages: totalMessages };
6576
8326
  }
6577
- var SYNKRO_DIR6, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, MCP_STDIO_PROXY_SRC, OFFSETS_DIR, CLOUD_JWT_PATH, SKILLS_DISCOVERED_PATH;
8327
+ var SYNKRO_DIR11, HOOKS_DIR, BIN_DIR, CONFIG_PATH4, MCP_STDIO_PROXY_SRC, OFFSETS_DIR, CLOUD_JWT_PATH, SKILLS_DISCOVERED_PATH;
6578
8328
  var init_install = __esm({
6579
8329
  "cli/commands/install.ts"() {
6580
8330
  "use strict";
@@ -6583,6 +8333,7 @@ var init_install = __esm({
6583
8333
  init_cursorHookConfig();
6584
8334
  init_mcpConfig();
6585
8335
  init_skillParser();
8336
+ init_telemetry();
6586
8337
  init_hookScriptsTs();
6587
8338
  init_stub();
6588
8339
  init_repoConnect();
@@ -6591,10 +8342,10 @@ var init_install = __esm({
6591
8342
  init_claudeDesktopTap();
6592
8343
  init_dockerInstall();
6593
8344
  init_setupToken();
6594
- SYNKRO_DIR6 = join11(homedir11(), ".synkro");
6595
- HOOKS_DIR = join11(SYNKRO_DIR6, "hooks");
6596
- BIN_DIR = join11(SYNKRO_DIR6, "bin");
6597
- CONFIG_PATH2 = join11(SYNKRO_DIR6, "config.env");
8345
+ SYNKRO_DIR11 = join18(homedir18(), ".synkro");
8346
+ HOOKS_DIR = join18(SYNKRO_DIR11, "hooks");
8347
+ BIN_DIR = join18(SYNKRO_DIR11, "bin");
8348
+ CONFIG_PATH4 = join18(SYNKRO_DIR11, "config.env");
6598
8349
  MCP_STDIO_PROXY_SRC = `#!/usr/bin/env bun
6599
8350
  import { readFileSync } from 'node:fs';
6600
8351
  import { homedir } from 'node:os';
@@ -6705,23 +8456,23 @@ rl.on('line', async (line) => {
6705
8456
  }
6706
8457
  });
6707
8458
  `;
6708
- OFFSETS_DIR = join11(SYNKRO_DIR6, ".transcript-offsets");
6709
- CLOUD_JWT_PATH = join11(SYNKRO_DIR6, ".cloud-jwt");
6710
- SKILLS_DISCOVERED_PATH = join11(SYNKRO_DIR6, ".skills-discovered");
8459
+ OFFSETS_DIR = join18(SYNKRO_DIR11, ".transcript-offsets");
8460
+ CLOUD_JWT_PATH = join18(SYNKRO_DIR11, ".cloud-jwt");
8461
+ SKILLS_DISCOVERED_PATH = join18(SYNKRO_DIR11, ".skills-discovered");
6711
8462
  }
6712
8463
  });
6713
8464
 
6714
8465
  // cli/local-cc/install.ts
6715
- import { existsSync as existsSync13, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9, readFileSync as readFileSync12, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as unlinkSync5, openSync, fsyncSync, closeSync } from "fs";
6716
- import { join as join12 } from "path";
6717
- import { homedir as homedir12 } from "os";
8466
+ import { existsSync as existsSync22, mkdirSync as mkdirSync14, writeFileSync as writeFileSync14, readFileSync as readFileSync19, chmodSync as chmodSync4, copyFileSync as copyFileSync2, renameSync as renameSync6, unlinkSync as unlinkSync7, openSync as openSync2, fsyncSync, closeSync as closeSync2 } from "fs";
8467
+ import { join as join19 } from "path";
8468
+ import { homedir as homedir19 } from "os";
6718
8469
  import { spawnSync as spawnSync6 } from "child_process";
6719
8470
  function writePluginFiles() {
6720
8471
  for (const c of CHANNELS) {
6721
- mkdirSync11(c.sessionDir, { recursive: true });
6722
- mkdirSync11(c.pluginSettingsDir, { recursive: true });
6723
- writeFileSync9(c.pluginPkgPath, PLUGIN_PACKAGE_JSON, "utf-8");
6724
- writeFileSync9(
8472
+ mkdirSync14(c.sessionDir, { recursive: true });
8473
+ mkdirSync14(c.pluginSettingsDir, { recursive: true });
8474
+ writeFileSync14(c.pluginPkgPath, PLUGIN_PACKAGE_JSON, "utf-8");
8475
+ writeFileSync14(
6725
8476
  c.pluginSettingsPath,
6726
8477
  JSON.stringify({
6727
8478
  fastMode: true,
@@ -6736,8 +8487,8 @@ function writePluginFiles() {
6736
8487
  }, null, 2) + "\n",
6737
8488
  "utf-8"
6738
8489
  );
6739
- writeFileSync9(c.runScriptPath, c.runScriptSource, "utf-8");
6740
- chmodSync3(c.runScriptPath, 493);
8490
+ writeFileSync14(c.runScriptPath, c.runScriptSource, "utf-8");
8491
+ chmodSync4(c.runScriptPath, 493);
6741
8492
  }
6742
8493
  }
6743
8494
  function runBunInstall() {
@@ -6755,10 +8506,10 @@ function runBunInstall() {
6755
8506
  }
6756
8507
  }
6757
8508
  function safelyMutateClaudeJson(mutator) {
6758
- if (!existsSync13(CLAUDE_JSON_PATH)) {
8509
+ if (!existsSync22(CLAUDE_JSON_PATH)) {
6759
8510
  return;
6760
8511
  }
6761
- const originalText = readFileSync12(CLAUDE_JSON_PATH, "utf-8");
8512
+ const originalText = readFileSync19(CLAUDE_JSON_PATH, "utf-8");
6762
8513
  let parsed;
6763
8514
  try {
6764
8515
  parsed = JSON.parse(originalText);
@@ -6790,17 +8541,17 @@ function safelyMutateClaudeJson(mutator) {
6790
8541
  copyFileSync2(CLAUDE_JSON_PATH, CLAUDE_JSON_BACKUP_PATH);
6791
8542
  const tmpPath = `${CLAUDE_JSON_PATH}.synkro-tmp.${process.pid}`;
6792
8543
  try {
6793
- writeFileSync9(tmpPath, newText, "utf-8");
6794
- const fd = openSync(tmpPath, "r");
8544
+ writeFileSync14(tmpPath, newText, "utf-8");
8545
+ const fd = openSync2(tmpPath, "r");
6795
8546
  try {
6796
8547
  fsyncSync(fd);
6797
8548
  } finally {
6798
- closeSync(fd);
8549
+ closeSync2(fd);
6799
8550
  }
6800
- renameSync4(tmpPath, CLAUDE_JSON_PATH);
8551
+ renameSync6(tmpPath, CLAUDE_JSON_PATH);
6801
8552
  } catch (err) {
6802
8553
  try {
6803
- unlinkSync5(tmpPath);
8554
+ unlinkSync7(tmpPath);
6804
8555
  } catch {
6805
8556
  }
6806
8557
  try {
@@ -6823,7 +8574,7 @@ function writeProjectMcpJson() {
6823
8574
  }
6824
8575
  }
6825
8576
  };
6826
- writeFileSync9(c.projectMcpPath, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
8577
+ writeFileSync14(c.projectMcpPath, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
6827
8578
  }
6828
8579
  }
6829
8580
  function patchClaudeJson() {
@@ -6900,42 +8651,42 @@ var CLAUDE_JSON_BACKUP_PATH, SESSION_DIR, PLUGIN_PATH, PLUGIN_PKG_PATH, PLUGIN_S
6900
8651
  var init_install2 = __esm({
6901
8652
  "cli/local-cc/install.ts"() {
6902
8653
  "use strict";
6903
- CLAUDE_JSON_BACKUP_PATH = join12(homedir12(), ".claude.json.synkro-bak");
6904
- SESSION_DIR = join12(homedir12(), ".synkro", "cc_sessions");
6905
- PLUGIN_PATH = join12(SESSION_DIR, "synkro-channel.ts");
6906
- PLUGIN_PKG_PATH = join12(SESSION_DIR, "package.json");
6907
- PLUGIN_SETTINGS_DIR = join12(SESSION_DIR, ".claude");
6908
- PLUGIN_SETTINGS_PATH = join12(PLUGIN_SETTINGS_DIR, "settings.json");
6909
- PROJECT_MCP_PATH = join12(SESSION_DIR, ".mcp.json");
6910
- CLAUDE_JSON_PATH = join12(homedir12(), ".claude.json");
6911
- RUN_SCRIPT_PATH = join12(SESSION_DIR, "run-claude.sh");
8654
+ CLAUDE_JSON_BACKUP_PATH = join19(homedir19(), ".claude.json.synkro-bak");
8655
+ SESSION_DIR = join19(homedir19(), ".synkro", "cc_sessions");
8656
+ PLUGIN_PATH = join19(SESSION_DIR, "synkro-channel.ts");
8657
+ PLUGIN_PKG_PATH = join19(SESSION_DIR, "package.json");
8658
+ PLUGIN_SETTINGS_DIR = join19(SESSION_DIR, ".claude");
8659
+ PLUGIN_SETTINGS_PATH = join19(PLUGIN_SETTINGS_DIR, "settings.json");
8660
+ PROJECT_MCP_PATH = join19(SESSION_DIR, ".mcp.json");
8661
+ CLAUDE_JSON_PATH = join19(homedir19(), ".claude.json");
8662
+ RUN_SCRIPT_PATH = join19(SESSION_DIR, "run-claude.sh");
6912
8663
  TMUX_SESSION_NAME = "synkro-local-cc";
6913
8664
  CHANNEL_1_PORT = 8941;
6914
- SESSION_DIR_2 = join12(homedir12(), ".synkro", "cc_sessions_2");
6915
- PLUGIN_PATH_2 = join12(SESSION_DIR_2, "synkro-channel.ts");
6916
- PLUGIN_PKG_PATH_2 = join12(SESSION_DIR_2, "package.json");
6917
- PLUGIN_SETTINGS_DIR_2 = join12(SESSION_DIR_2, ".claude");
6918
- PLUGIN_SETTINGS_PATH_2 = join12(PLUGIN_SETTINGS_DIR_2, "settings.json");
6919
- PROJECT_MCP_PATH_2 = join12(SESSION_DIR_2, ".mcp.json");
6920
- RUN_SCRIPT_PATH_2 = join12(SESSION_DIR_2, "run-claude.sh");
8665
+ SESSION_DIR_2 = join19(homedir19(), ".synkro", "cc_sessions_2");
8666
+ PLUGIN_PATH_2 = join19(SESSION_DIR_2, "synkro-channel.ts");
8667
+ PLUGIN_PKG_PATH_2 = join19(SESSION_DIR_2, "package.json");
8668
+ PLUGIN_SETTINGS_DIR_2 = join19(SESSION_DIR_2, ".claude");
8669
+ PLUGIN_SETTINGS_PATH_2 = join19(PLUGIN_SETTINGS_DIR_2, "settings.json");
8670
+ PROJECT_MCP_PATH_2 = join19(SESSION_DIR_2, ".mcp.json");
8671
+ RUN_SCRIPT_PATH_2 = join19(SESSION_DIR_2, "run-claude.sh");
6921
8672
  TMUX_SESSION_NAME_2 = "synkro-local-cc-2";
6922
8673
  CHANNEL_2_PORT = 8951;
6923
- SESSION_DIR_3 = join12(homedir12(), ".synkro", "cc_sessions_3");
6924
- PLUGIN_PATH_3 = join12(SESSION_DIR_3, "synkro-channel.ts");
6925
- PLUGIN_PKG_PATH_3 = join12(SESSION_DIR_3, "package.json");
6926
- PLUGIN_SETTINGS_DIR_3 = join12(SESSION_DIR_3, ".claude");
6927
- PLUGIN_SETTINGS_PATH_3 = join12(PLUGIN_SETTINGS_DIR_3, "settings.json");
6928
- PROJECT_MCP_PATH_3 = join12(SESSION_DIR_3, ".mcp.json");
6929
- RUN_SCRIPT_PATH_3 = join12(SESSION_DIR_3, "run-claude.sh");
8674
+ SESSION_DIR_3 = join19(homedir19(), ".synkro", "cc_sessions_3");
8675
+ PLUGIN_PATH_3 = join19(SESSION_DIR_3, "synkro-channel.ts");
8676
+ PLUGIN_PKG_PATH_3 = join19(SESSION_DIR_3, "package.json");
8677
+ PLUGIN_SETTINGS_DIR_3 = join19(SESSION_DIR_3, ".claude");
8678
+ PLUGIN_SETTINGS_PATH_3 = join19(PLUGIN_SETTINGS_DIR_3, "settings.json");
8679
+ PROJECT_MCP_PATH_3 = join19(SESSION_DIR_3, ".mcp.json");
8680
+ RUN_SCRIPT_PATH_3 = join19(SESSION_DIR_3, "run-claude.sh");
6930
8681
  TMUX_SESSION_NAME_3 = "synkro-local-cc-3";
6931
8682
  CHANNEL_3_PORT = 8942;
6932
- SESSION_DIR_4 = join12(homedir12(), ".synkro", "cc_sessions_4");
6933
- PLUGIN_PATH_4 = join12(SESSION_DIR_4, "synkro-channel.ts");
6934
- PLUGIN_PKG_PATH_4 = join12(SESSION_DIR_4, "package.json");
6935
- PLUGIN_SETTINGS_DIR_4 = join12(SESSION_DIR_4, ".claude");
6936
- PLUGIN_SETTINGS_PATH_4 = join12(PLUGIN_SETTINGS_DIR_4, "settings.json");
6937
- PROJECT_MCP_PATH_4 = join12(SESSION_DIR_4, ".mcp.json");
6938
- RUN_SCRIPT_PATH_4 = join12(SESSION_DIR_4, "run-claude.sh");
8683
+ SESSION_DIR_4 = join19(homedir19(), ".synkro", "cc_sessions_4");
8684
+ PLUGIN_PATH_4 = join19(SESSION_DIR_4, "synkro-channel.ts");
8685
+ PLUGIN_PKG_PATH_4 = join19(SESSION_DIR_4, "package.json");
8686
+ PLUGIN_SETTINGS_DIR_4 = join19(SESSION_DIR_4, ".claude");
8687
+ PLUGIN_SETTINGS_PATH_4 = join19(PLUGIN_SETTINGS_DIR_4, "settings.json");
8688
+ PROJECT_MCP_PATH_4 = join19(SESSION_DIR_4, ".mcp.json");
8689
+ RUN_SCRIPT_PATH_4 = join19(SESSION_DIR_4, "run-claude.sh");
6939
8690
  TMUX_SESSION_NAME_4 = "synkro-local-cc-4";
6940
8691
  CHANNEL_4_PORT = 8952;
6941
8692
  RUN_SCRIPT_SOURCE = `#!/usr/bin/env bash
@@ -7209,9 +8960,9 @@ var disconnect_exports = {};
7209
8960
  __export(disconnect_exports, {
7210
8961
  disconnectCommand: () => disconnectCommand
7211
8962
  });
7212
- import { existsSync as existsSync14, rmSync as rmSync2, readdirSync as readdirSync4 } from "fs";
7213
- import { homedir as homedir13 } from "os";
7214
- import { join as join13 } from "path";
8963
+ import { existsSync as existsSync23, rmSync as rmSync2, readdirSync as readdirSync4 } from "fs";
8964
+ import { homedir as homedir20 } from "os";
8965
+ import { join as join20 } from "path";
7215
8966
  import { spawnSync as spawnSync7 } from "child_process";
7216
8967
  import { createInterface as createInterface4 } from "readline";
7217
8968
  async function tearDownLocalCC() {
@@ -7257,9 +9008,12 @@ function confirmPurge() {
7257
9008
  });
7258
9009
  });
7259
9010
  }
7260
- async function disconnectCommand(args2 = []) {
7261
- const purge = args2.includes("--purge");
7262
- if (purge && !await confirmPurge()) {
9011
+ async function disconnectCommand(args2 = [], opts = {}) {
9012
+ const purge2 = args2.includes("--purge");
9013
+ const flavor = opts.flavor ?? (process.argv[2] === "uninstall" ? "uninstall" : "disconnect");
9014
+ const startedAt = Date.now();
9015
+ const removed = { hooks: false, mcp: false, config: false, telemetry_db: false };
9016
+ if (purge2 && !await confirmPurge()) {
7263
9017
  console.log("\nAborted \u2014 nothing was removed.");
7264
9018
  return;
7265
9019
  }
@@ -7270,57 +9024,74 @@ async function disconnectCommand(args2 = []) {
7270
9024
  for (const agent of agents) {
7271
9025
  if (agent.kind === "claude_code") {
7272
9026
  sawClaudeCode = true;
7273
- const removed = uninstallCCHooks(agent.settingsPath);
7274
- console.log(`${removed ? "\u2713" : "\xB7"} ${agent.name}: ${removed ? "removed Synkro hook entries" : "no Synkro hooks found"}`);
9027
+ const r = uninstallCCHooks(agent.settingsPath);
9028
+ if (r) removed.hooks = true;
9029
+ console.log(`${r ? "\u2713" : "\xB7"} ${agent.name}: ${r ? "removed Synkro hook entries" : "no Synkro hooks found"}`);
7275
9030
  } else if (agent.kind === "cursor") {
7276
- const removed = uninstallCursorHooks(agent.settingsPath);
7277
- console.log(`${removed ? "\u2713" : "\xB7"} ${agent.name}: ${removed ? "removed Synkro hook entries" : "no Synkro hooks found"}`);
9031
+ const r = uninstallCursorHooks(agent.settingsPath);
9032
+ if (r) removed.hooks = true;
9033
+ console.log(`${r ? "\u2713" : "\xB7"} ${agent.name}: ${r ? "removed Synkro hook entries" : "no Synkro hooks found"}`);
7278
9034
  }
7279
9035
  }
7280
9036
  if (sawClaudeCode) {
7281
9037
  const mcpRemoved = uninstallMcpConfig();
9038
+ if (mcpRemoved) removed.mcp = true;
7282
9039
  console.log(`${mcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (CC): ${mcpRemoved ? "removed from ~/.claude.json" : "no entry found"}`);
7283
9040
  }
7284
9041
  {
7285
9042
  const cursorMcpRemoved = uninstallCursorMcpConfig();
9043
+ if (cursorMcpRemoved) removed.mcp = true;
7286
9044
  console.log(`${cursorMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Cursor): ${cursorMcpRemoved ? "removed from ~/.cursor/mcp.json" : "no entry found"}`);
7287
9045
  }
7288
9046
  {
7289
9047
  const desktopMcpRemoved = uninstallClaudeDesktopMcpConfig();
9048
+ if (desktopMcpRemoved) removed.mcp = true;
7290
9049
  console.log(`${desktopMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Claude Desktop): ${desktopMcpRemoved ? "removed from claude_desktop_config.json" : "no entry found"}`);
7291
9050
  }
7292
- if (existsSync14(SYNKRO_DIR7)) {
7293
- if (purge) {
7294
- rmSync2(SYNKRO_DIR7, { recursive: true, force: true });
7295
- console.log(`\u2713 wiped ${SYNKRO_DIR7} entirely \u2014 including all scan data and backups`);
9051
+ if (existsSync23(SYNKRO_DIR12)) {
9052
+ if (purge2) {
9053
+ emit("uninstall", {
9054
+ flavor,
9055
+ purge: purge2,
9056
+ removed: { ...removed, config: true },
9057
+ duration_ms: Date.now() - startedAt
9058
+ });
9059
+ await flush({ force: true });
9060
+ rmSync2(SYNKRO_DIR12, { recursive: true, force: true });
9061
+ removed.config = true;
9062
+ console.log(`\u2713 wiped ${SYNKRO_DIR12} entirely \u2014 including all scan data and backups`);
7296
9063
  } else {
7297
9064
  const keep = /* @__PURE__ */ new Set([
7298
- join13(SYNKRO_DIR7, "pgdata"),
7299
- join13(SYNKRO_DIR7, "pgdata-backups"),
7300
- join13(SYNKRO_DIR7, ".transcript-offsets")
9065
+ join20(SYNKRO_DIR12, "pgdata"),
9066
+ join20(SYNKRO_DIR12, "pgdata-backups"),
9067
+ join20(SYNKRO_DIR12, ".transcript-offsets")
7301
9068
  ]);
7302
9069
  const preserved = [];
7303
- for (const entry of readdirSync4(SYNKRO_DIR7)) {
7304
- const full = join13(SYNKRO_DIR7, entry);
9070
+ for (const entry of readdirSync4(SYNKRO_DIR12)) {
9071
+ const full = join20(SYNKRO_DIR12, entry);
7305
9072
  if (keep.has(full)) {
7306
9073
  preserved.push(entry);
7307
9074
  continue;
7308
9075
  }
7309
9076
  rmSync2(full, { recursive: true, force: true });
7310
9077
  }
9078
+ removed.config = true;
7311
9079
  if (preserved.length > 0) {
7312
- console.log(`\u2713 removed Synkro config from ${SYNKRO_DIR7} (kept your scan data: ${preserved.join(", ")})`);
9080
+ console.log(`\u2713 removed Synkro config from ${SYNKRO_DIR12} (kept your scan data: ${preserved.join(", ")})`);
7313
9081
  console.log(" run `synkro uninstall --purge` to delete that too");
7314
9082
  } else {
7315
- console.log(`\u2713 removed ${SYNKRO_DIR7}`);
9083
+ console.log(`\u2713 removed ${SYNKRO_DIR12}`);
7316
9084
  }
7317
9085
  }
7318
9086
  } else {
7319
- console.log(`\xB7 ${SYNKRO_DIR7} already gone`);
9087
+ console.log(`\xB7 ${SYNKRO_DIR12} already gone`);
9088
+ }
9089
+ if (!purge2) {
9090
+ emit("uninstall", { flavor, purge: purge2, removed, duration_ms: Date.now() - startedAt });
7320
9091
  }
7321
- console.log(purge ? "\nSynkro fully removed \u2014 this machine is clean, as if it was never installed." : "\nSynkro uninstalled. Your scan data is preserved.");
9092
+ console.log(purge2 ? "\nSynkro fully removed \u2014 this machine is clean, as if it was never installed." : "\nSynkro uninstalled. Your scan data is preserved.");
7322
9093
  }
7323
- var SYNKRO_DIR7;
9094
+ var SYNKRO_DIR12;
7324
9095
  var init_disconnect = __esm({
7325
9096
  "cli/commands/disconnect.ts"() {
7326
9097
  "use strict";
@@ -7331,14 +9102,15 @@ var init_disconnect = __esm({
7331
9102
  init_install2();
7332
9103
  init_dockerInstall();
7333
9104
  init_macKeychain();
7334
- SYNKRO_DIR7 = join13(homedir13(), ".synkro");
9105
+ init_telemetry();
9106
+ SYNKRO_DIR12 = join20(homedir20(), ".synkro");
7335
9107
  }
7336
9108
  });
7337
9109
 
7338
9110
  // cli/local-cc/turnLog.ts
7339
- import { appendFileSync, existsSync as existsSync15, mkdirSync as mkdirSync12, openSync as openSync2, readFileSync as readFileSync13, readSync, closeSync as closeSync2, statSync as statSync2, watchFile, unwatchFile } from "fs";
7340
- import { dirname as dirname6, join as join14 } from "path";
7341
- import { homedir as homedir14 } from "os";
9111
+ import { appendFileSync as appendFileSync3, existsSync as existsSync24, mkdirSync as mkdirSync15, openSync as openSync3, readFileSync as readFileSync20, readSync, closeSync as closeSync3, statSync as statSync3, watchFile, unwatchFile } from "fs";
9112
+ import { dirname as dirname6, join as join21 } from "path";
9113
+ import { homedir as homedir21 } from "os";
7342
9114
  function truncate(s, max = PREVIEW_MAX) {
7343
9115
  if (s.length <= max) return s;
7344
9116
  return s.slice(0, max) + "\u2026 [+" + (s.length - max) + " chars]";
@@ -7358,7 +9130,7 @@ function extractSeverity(result) {
7358
9130
  }
7359
9131
  function appendTurn(args2) {
7360
9132
  try {
7361
- mkdirSync12(dirname6(TURN_LOG_PATH), { recursive: true });
9133
+ mkdirSync15(dirname6(TURN_LOG_PATH), { recursive: true });
7362
9134
  const entry = {
7363
9135
  ts: new Date(args2.startedAt).toISOString(),
7364
9136
  role: args2.role,
@@ -7369,16 +9141,16 @@ function appendTurn(args2) {
7369
9141
  severity: args2.result ? extractSeverity(args2.result) : void 0,
7370
9142
  error: args2.error
7371
9143
  };
7372
- appendFileSync(TURN_LOG_PATH, JSON.stringify(entry) + "\n", "utf-8");
9144
+ appendFileSync3(TURN_LOG_PATH, JSON.stringify(entry) + "\n", "utf-8");
7373
9145
  } catch {
7374
9146
  }
7375
9147
  }
7376
9148
  function readRecentTurns(n = 20) {
7377
- if (!existsSync15(TURN_LOG_PATH)) return [];
9149
+ if (!existsSync24(TURN_LOG_PATH)) return [];
7378
9150
  try {
7379
- const size = statSync2(TURN_LOG_PATH).size;
9151
+ const size = statSync3(TURN_LOG_PATH).size;
7380
9152
  if (size === 0) return [];
7381
- const text = readFileSync13(TURN_LOG_PATH, "utf-8");
9153
+ const text = readFileSync20(TURN_LOG_PATH, "utf-8");
7382
9154
  const lines = text.split("\n").filter(Boolean);
7383
9155
  const lastN = lines.slice(-n).reverse();
7384
9156
  return lastN.map((line) => {
@@ -7394,15 +9166,15 @@ function readRecentTurns(n = 20) {
7394
9166
  }
7395
9167
  function followTurns(onEntry) {
7396
9168
  try {
7397
- mkdirSync12(dirname6(TURN_LOG_PATH), { recursive: true });
7398
- if (!existsSync15(TURN_LOG_PATH)) {
7399
- appendFileSync(TURN_LOG_PATH, "", "utf-8");
9169
+ mkdirSync15(dirname6(TURN_LOG_PATH), { recursive: true });
9170
+ if (!existsSync24(TURN_LOG_PATH)) {
9171
+ appendFileSync3(TURN_LOG_PATH, "", "utf-8");
7400
9172
  }
7401
9173
  } catch {
7402
9174
  }
7403
9175
  let lastSize = (() => {
7404
9176
  try {
7405
- return statSync2(TURN_LOG_PATH).size;
9177
+ return statSync3(TURN_LOG_PATH).size;
7406
9178
  } catch {
7407
9179
  return 0;
7408
9180
  }
@@ -7412,7 +9184,7 @@ function followTurns(onEntry) {
7412
9184
  if (to <= from) return;
7413
9185
  let fd = null;
7414
9186
  try {
7415
- fd = openSync2(TURN_LOG_PATH, "r");
9187
+ fd = openSync3(TURN_LOG_PATH, "r");
7416
9188
  const len = to - from;
7417
9189
  const buf = Buffer.alloc(len);
7418
9190
  readSync(fd, buf, 0, len, from);
@@ -7435,7 +9207,7 @@ function followTurns(onEntry) {
7435
9207
  } finally {
7436
9208
  if (fd !== null) {
7437
9209
  try {
7438
- closeSync2(fd);
9210
+ closeSync3(fd);
7439
9211
  } catch {
7440
9212
  }
7441
9213
  }
@@ -7457,7 +9229,7 @@ var TURN_LOG_PATH, PREVIEW_MAX;
7457
9229
  var init_turnLog = __esm({
7458
9230
  "cli/local-cc/turnLog.ts"() {
7459
9231
  "use strict";
7460
- TURN_LOG_PATH = join14(homedir14(), ".synkro", "cc_sessions", "turns.log");
9232
+ TURN_LOG_PATH = join21(homedir21(), ".synkro", "cc_sessions", "turns.log");
7461
9233
  PREVIEW_MAX = 400;
7462
9234
  }
7463
9235
  });
@@ -7538,7 +9310,7 @@ function isChannelAvailable(port = CHANNEL_PORT, timeoutMs = 500) {
7538
9310
  });
7539
9311
  }
7540
9312
  var CHANNEL_HOST, CHANNEL_PORT, DEFAULT_TIMEOUT_MS, LocalCCError;
7541
- var init_client = __esm({
9313
+ var init_client2 = __esm({
7542
9314
  "cli/local-cc/client.ts"() {
7543
9315
  "use strict";
7544
9316
  init_turnLog();
@@ -7601,14 +9373,14 @@ async function gradeCommand(args2) {
7601
9373
  var init_grade = __esm({
7602
9374
  "cli/commands/grade.ts"() {
7603
9375
  "use strict";
7604
- init_client();
9376
+ init_client2();
7605
9377
  }
7606
9378
  });
7607
9379
 
7608
9380
  // cli/local-cc/pueue.ts
7609
- import { execFileSync as execFileSync3, spawnSync as spawnSync8, spawn as spawn2 } from "child_process";
7610
- import { homedir as homedir15 } from "os";
7611
- import { join as join15 } from "path";
9381
+ import { execFileSync as execFileSync4, spawnSync as spawnSync8, spawn as spawn3 } from "child_process";
9382
+ import { homedir as homedir22 } from "os";
9383
+ import { join as join22 } from "path";
7612
9384
  import { connect as connect2 } from "net";
7613
9385
  function pueueAvailable() {
7614
9386
  const r = spawnSync8("pueue", ["--version"], { encoding: "utf-8" });
@@ -7674,7 +9446,7 @@ function startTask(opts = {}) {
7674
9446
  spawnSync8("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
7675
9447
  existing = findTask(ch);
7676
9448
  }
7677
- const runScript = join15(cwd, "run-claude.sh");
9449
+ const runScript = join22(cwd, "run-claude.sh");
7678
9450
  const args2 = [
7679
9451
  "add",
7680
9452
  "--label",
@@ -7771,7 +9543,7 @@ function assertPueueInstalled() {
7771
9543
  const status = spawnSync8("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
7772
9544
  if (status.status !== 0) {
7773
9545
  console.log(" Starting pueued daemon...");
7774
- const child = spawn2("pueued", ["-d"], { stdio: "ignore", detached: true });
9546
+ const child = spawn3("pueued", ["-d"], { stdio: "ignore", detached: true });
7775
9547
  child.unref();
7776
9548
  spawnSync8("sleep", ["1"]);
7777
9549
  const retry = spawnSync8("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
@@ -7804,12 +9576,12 @@ var init_pueue = __esm({
7804
9576
  "use strict";
7805
9577
  TASK_LABEL = "synkro-local-cc";
7806
9578
  TMUX_SESSION = "synkro-local-cc";
7807
- SESSION_DIR2 = join15(homedir15(), ".synkro", "cc_sessions");
9579
+ SESSION_DIR2 = join22(homedir22(), ".synkro", "cc_sessions");
7808
9580
  TASK_LABEL_2 = "synkro-local-cc-2";
7809
9581
  TMUX_SESSION_2 = "synkro-local-cc-2";
7810
- SESSION_DIR_22 = join15(homedir15(), ".synkro", "cc_sessions_2");
7811
- SESSION_DIR_32 = join15(homedir15(), ".synkro", "cc_sessions_3");
7812
- SESSION_DIR_42 = join15(homedir15(), ".synkro", "cc_sessions_4");
9582
+ SESSION_DIR_22 = join22(homedir22(), ".synkro", "cc_sessions_2");
9583
+ SESSION_DIR_32 = join22(homedir22(), ".synkro", "cc_sessions_3");
9584
+ SESSION_DIR_42 = join22(homedir22(), ".synkro", "cc_sessions_4");
7813
9585
  PueueError = class extends Error {
7814
9586
  constructor(message, cause) {
7815
9587
  super(message);
@@ -7824,24 +9596,24 @@ var init_pueue = __esm({
7824
9596
  });
7825
9597
 
7826
9598
  // cli/local-cc/settings.ts
7827
- import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
7828
- import { homedir as homedir16 } from "os";
7829
- import { join as join16 } from "path";
9599
+ import { existsSync as existsSync25, readFileSync as readFileSync21 } from "fs";
9600
+ import { homedir as homedir23 } from "os";
9601
+ import { join as join23 } from "path";
7830
9602
  function isLocalCCEnabled() {
7831
- if (!existsSync16(CONFIG_PATH3)) return false;
9603
+ if (!existsSync25(CONFIG_PATH5)) return false;
7832
9604
  try {
7833
- const content = readFileSync14(CONFIG_PATH3, "utf-8");
9605
+ const content = readFileSync21(CONFIG_PATH5, "utf-8");
7834
9606
  const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
7835
9607
  return match?.[1] === "yes";
7836
9608
  } catch {
7837
9609
  return false;
7838
9610
  }
7839
9611
  }
7840
- var CONFIG_PATH3;
9612
+ var CONFIG_PATH5;
7841
9613
  var init_settings = __esm({
7842
9614
  "cli/local-cc/settings.ts"() {
7843
9615
  "use strict";
7844
- CONFIG_PATH3 = join16(homedir16(), ".synkro", "config.env");
9616
+ CONFIG_PATH5 = join23(homedir23(), ".synkro", "config.env");
7845
9617
  }
7846
9618
  });
7847
9619
 
@@ -7851,10 +9623,10 @@ __export(localCc_exports, {
7851
9623
  localCcCommand: () => localCcCommand
7852
9624
  });
7853
9625
  import { spawnSync as spawnSync9 } from "child_process";
7854
- import { homedir as homedir17 } from "os";
7855
- import { join as join17 } from "path";
9626
+ import { homedir as homedir24 } from "os";
9627
+ import { join as join24 } from "path";
7856
9628
  import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from "fs";
7857
- import { existsSync as existsSync17, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
9629
+ import { existsSync as existsSync26, readFileSync as readFileSync22, writeFileSync as writeFileSync15 } from "fs";
7858
9630
  function deploymentMode() {
7859
9631
  const env = (process.env.SYNKRO_DEPLOYMENT_MODE || "").toLowerCase();
7860
9632
  if (env === "docker") return "docker";
@@ -7960,15 +9732,15 @@ TROUBLESHOOTING
7960
9732
  `);
7961
9733
  }
7962
9734
  function readGatewayUrl() {
7963
- if (existsSync17(CONFIG_PATH4)) {
7964
- const m = readFileSync15(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
9735
+ if (existsSync26(CONFIG_PATH6)) {
9736
+ const m = readFileSync22(CONFIG_PATH6, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
7965
9737
  if (m) return m[1];
7966
9738
  }
7967
9739
  return "https://api.synkro.sh";
7968
9740
  }
7969
9741
  function updateLocalInferenceFlag(enabled) {
7970
- if (!existsSync17(CONFIG_PATH4)) return;
7971
- let content = readFileSync15(CONFIG_PATH4, "utf-8");
9742
+ if (!existsSync26(CONFIG_PATH6)) return;
9743
+ let content = readFileSync22(CONFIG_PATH6, "utf-8");
7972
9744
  const flag = enabled ? "yes" : "no";
7973
9745
  if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
7974
9746
  content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
@@ -7977,7 +9749,7 @@ function updateLocalInferenceFlag(enabled) {
7977
9749
  SYNKRO_LOCAL_INFERENCE='${flag}'
7978
9750
  `;
7979
9751
  }
7980
- writeFileSync10(CONFIG_PATH4, content, "utf-8");
9752
+ writeFileSync15(CONFIG_PATH6, content, "utf-8");
7981
9753
  }
7982
9754
  async function setServerGradingProvider(provider) {
7983
9755
  await ensureValidToken();
@@ -8396,7 +10168,7 @@ async function localCcCommand(args2) {
8396
10168
  process.exit(1);
8397
10169
  }
8398
10170
  }
8399
- var SYNKRO_CONFIG_PATH, CONFIG_PATH4;
10171
+ var SYNKRO_CONFIG_PATH, CONFIG_PATH6;
8400
10172
  var init_localCc = __esm({
8401
10173
  "cli/commands/localCc.ts"() {
8402
10174
  "use strict";
@@ -8407,10 +10179,10 @@ var init_localCc = __esm({
8407
10179
  init_macKeychain();
8408
10180
  init_dockerInstall();
8409
10181
  init_install();
8410
- init_client();
10182
+ init_client2();
8411
10183
  init_stub();
8412
- SYNKRO_CONFIG_PATH = join17(homedir17(), ".synkro", "config.env");
8413
- CONFIG_PATH4 = join17(homedir17(), ".synkro", "config.env");
10184
+ SYNKRO_CONFIG_PATH = join24(homedir24(), ".synkro", "config.env");
10185
+ CONFIG_PATH6 = join24(homedir24(), ".synkro", "config.env");
8414
10186
  }
8415
10187
  });
8416
10188
 
@@ -8419,15 +10191,15 @@ var import_exports = {};
8419
10191
  __export(import_exports, {
8420
10192
  importCommand: () => importCommand
8421
10193
  });
8422
- import { existsSync as existsSync18, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "fs";
8423
- import { homedir as homedir18 } from "os";
8424
- import { join as join18 } from "path";
10194
+ import { existsSync as existsSync27, readFileSync as readFileSync23, readdirSync as readdirSync5 } from "fs";
10195
+ import { homedir as homedir25 } from "os";
10196
+ import { join as join25 } from "path";
8425
10197
  import { execSync as execSync7 } from "child_process";
8426
10198
  import { createInterface as createInterface5 } from "readline";
8427
- function readConfigEnv() {
10199
+ function readConfigEnv2() {
8428
10200
  const out = {};
8429
10201
  try {
8430
- for (const line of readFileSync16(CONFIG_PATH5, "utf-8").split("\n")) {
10202
+ for (const line of readFileSync23(CONFIG_PATH7, "utf-8").split("\n")) {
8431
10203
  const t = line.trim();
8432
10204
  if (!t || t.startsWith("#")) continue;
8433
10205
  const eq = t.indexOf("=");
@@ -8439,8 +10211,8 @@ function readConfigEnv() {
8439
10211
  }
8440
10212
  function projectsFolder() {
8441
10213
  const sanitized = process.cwd().replace(/\//g, "-");
8442
- const dir = join18(homedir18(), ".claude", "projects", sanitized);
8443
- return existsSync18(dir) ? dir : null;
10214
+ const dir = join25(homedir25(), ".claude", "projects", sanitized);
10215
+ return existsSync27(dir) ? dir : null;
8444
10216
  }
8445
10217
  function repoName() {
8446
10218
  try {
@@ -8463,7 +10235,7 @@ function extractText(content) {
8463
10235
  return "";
8464
10236
  }
8465
10237
  function parseSession(filePath, sessionId) {
8466
- const lines = readFileSync16(filePath, "utf-8").split("\n").filter(Boolean);
10238
+ const lines = readFileSync23(filePath, "utf-8").split("\n").filter(Boolean);
8467
10239
  const messages = [];
8468
10240
  const actions = [];
8469
10241
  let step = 0;
@@ -8515,7 +10287,7 @@ function ask2(q) {
8515
10287
  }));
8516
10288
  }
8517
10289
  async function importCommand() {
8518
- const config = readConfigEnv();
10290
+ const config = readConfigEnv2();
8519
10291
  const isCloud = config.SYNKRO_DEPLOY_LOCATION === "cloud" || config.SYNKRO_STORAGE_MODE === "cloud";
8520
10292
  const repo = repoName();
8521
10293
  const dir = projectsFolder();
@@ -8536,7 +10308,7 @@ async function importCommand() {
8536
10308
  return;
8537
10309
  }
8538
10310
  }
8539
- const sessions = files.map((f) => parseSession(join18(dir, f), f.replace(".jsonl", ""))).filter((s) => s.messages.length > 0);
10311
+ const sessions = files.map((f) => parseSession(join25(dir, f), f.replace(".jsonl", ""))).filter((s) => s.messages.length > 0);
8540
10312
  const totalMsgs = sessions.reduce((n, s) => n + s.messages.length, 0);
8541
10313
  let ok = 0, fail = 0;
8542
10314
  if (isCloud) {
@@ -8591,12 +10363,12 @@ async function importCommand() {
8591
10363
  console.log(`
8592
10364
  \u2713 Imported ${ok} session(s) (${totalMsgs} messages)${fail ? `, ${fail} failed` : ""}.`);
8593
10365
  }
8594
- var CONFIG_PATH5;
10366
+ var CONFIG_PATH7;
8595
10367
  var init_import = __esm({
8596
10368
  "cli/commands/import.ts"() {
8597
10369
  "use strict";
8598
10370
  init_stub();
8599
- CONFIG_PATH5 = join18(homedir18(), ".synkro", "config.env");
10371
+ CONFIG_PATH7 = join25(homedir25(), ".synkro", "config.env");
8600
10372
  }
8601
10373
  });
8602
10374
 
@@ -8638,10 +10410,10 @@ var init_packVerify = __esm({
8638
10410
  });
8639
10411
 
8640
10412
  // cli/installer/lockfile.ts
8641
- import { existsSync as existsSync19, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
8642
- import { join as join19 } from "path";
10413
+ import { existsSync as existsSync28, readFileSync as readFileSync24, writeFileSync as writeFileSync16 } from "fs";
10414
+ import { join as join26 } from "path";
8643
10415
  function lockPath(repoRoot) {
8644
- return join19(repoRoot, LOCK_FILE);
10416
+ return join26(repoRoot, LOCK_FILE);
8645
10417
  }
8646
10418
  function writeLockfile(repoRoot, entries) {
8647
10419
  const sorted = [...entries].sort((a, b) => a.ref.localeCompare(b.ref));
@@ -8659,7 +10431,7 @@ function writeLockfile(repoRoot, entries) {
8659
10431
  ""
8660
10432
  ])
8661
10433
  ].join("\n");
8662
- writeFileSync11(lockPath(repoRoot), body, "utf-8");
10434
+ writeFileSync16(lockPath(repoRoot), body, "utf-8");
8663
10435
  }
8664
10436
  var LOCK_FILE;
8665
10437
  var init_lockfile = __esm({
@@ -8674,9 +10446,9 @@ var sync_exports = {};
8674
10446
  __export(sync_exports, {
8675
10447
  syncCommand: () => syncCommand
8676
10448
  });
8677
- import { existsSync as existsSync20, mkdirSync as mkdirSync13, readdirSync as readdirSync6, rmSync as rmSync3, writeFileSync as writeFileSync12 } from "fs";
8678
- import { homedir as homedir19 } from "os";
8679
- import { join as join20 } from "path";
10449
+ import { existsSync as existsSync29, mkdirSync as mkdirSync16, readdirSync as readdirSync6, rmSync as rmSync3, writeFileSync as writeFileSync17 } from "fs";
10450
+ import { homedir as homedir26 } from "os";
10451
+ import { join as join27 } from "path";
8680
10452
  function cacheKey(ref, version) {
8681
10453
  return ref.replace(/\//g, "__").replace(/[^\w.@-]/g, "_") + "@" + version + ".json";
8682
10454
  }
@@ -8707,8 +10479,8 @@ async function syncCommand(_args = []) {
8707
10479
  }
8708
10480
  const gateway = (process.env.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
8709
10481
  const cloud = process.env.SYNKRO_DEPLOY_LOCATION === "cloud";
8710
- const cacheDir = join20(homedir19(), ".synkro", "cache", "packs");
8711
- if (!cloud) mkdirSync13(cacheDir, { recursive: true });
10482
+ const cacheDir = join27(homedir26(), ".synkro", "cache", "packs");
10483
+ if (!cloud) mkdirSync16(cacheDir, { recursive: true });
8712
10484
  console.log(`Syncing ${refs.length} standard(s) from the registry\u2026`);
8713
10485
  const lock = [];
8714
10486
  const keptCacheFiles = /* @__PURE__ */ new Set();
@@ -8736,7 +10508,7 @@ async function syncCommand(_args = []) {
8736
10508
  if (!cloud) {
8737
10509
  const fname = cacheKey(ref, data.version);
8738
10510
  keptCacheFiles.add(fname);
8739
- writeFileSync12(join20(cacheDir, fname), JSON.stringify({
10511
+ writeFileSync17(join27(cacheDir, fname), JSON.stringify({
8740
10512
  ref,
8741
10513
  version: data.version,
8742
10514
  digest: data.digest,
@@ -8748,11 +10520,11 @@ async function syncCommand(_args = []) {
8748
10520
  const ruleCount = Array.isArray(pack.rules) ? pack.rules.length : 0;
8749
10521
  console.log(` \u2713 ${ref}:${data.version} \u2014 verified (${ruleCount} rule${ruleCount === 1 ? "" : "s"})`);
8750
10522
  }
8751
- if (!cloud && existsSync20(cacheDir)) {
10523
+ if (!cloud && existsSync29(cacheDir)) {
8752
10524
  for (const f of readdirSync6(cacheDir)) {
8753
10525
  if (f.endsWith(".json") && !keptCacheFiles.has(f)) {
8754
10526
  try {
8755
- rmSync3(join20(cacheDir, f));
10527
+ rmSync3(join27(cacheDir, f));
8756
10528
  } catch {
8757
10529
  }
8758
10530
  }
@@ -8888,13 +10660,13 @@ var config_exports = {};
8888
10660
  __export(config_exports, {
8889
10661
  configCommand: () => configCommand
8890
10662
  });
8891
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync21 } from "fs";
8892
- import { join as join21 } from "path";
8893
- import { homedir as homedir20 } from "os";
8894
- function readConfigEnv2() {
8895
- if (!existsSync21(CONFIG_PATH6)) return {};
10663
+ import { readFileSync as readFileSync25, writeFileSync as writeFileSync18, existsSync as existsSync30 } from "fs";
10664
+ import { join as join28 } from "path";
10665
+ import { homedir as homedir27 } from "os";
10666
+ function readConfigEnv3() {
10667
+ if (!existsSync30(CONFIG_PATH8)) return {};
8896
10668
  const out = {};
8897
- for (const line of readFileSync18(CONFIG_PATH6, "utf-8").split("\n")) {
10669
+ for (const line of readFileSync25(CONFIG_PATH8, "utf-8").split("\n")) {
8898
10670
  const t = line.trim();
8899
10671
  if (!t || t.startsWith("#")) continue;
8900
10672
  const eq = t.indexOf("=");
@@ -8903,11 +10675,11 @@ function readConfigEnv2() {
8903
10675
  return out;
8904
10676
  }
8905
10677
  function updateConfigValue(key, value) {
8906
- if (!existsSync21(CONFIG_PATH6)) {
10678
+ if (!existsSync30(CONFIG_PATH8)) {
8907
10679
  console.error("No config found. Run `synkro install` first.");
8908
10680
  process.exit(1);
8909
10681
  }
8910
- const lines = readFileSync18(CONFIG_PATH6, "utf-8").split("\n");
10682
+ const lines = readFileSync25(CONFIG_PATH8, "utf-8").split("\n");
8911
10683
  const pattern = new RegExp(`^${key}=`);
8912
10684
  let found = false;
8913
10685
  const updated = lines.map((line) => {
@@ -8918,7 +10690,7 @@ function updateConfigValue(key, value) {
8918
10690
  return line;
8919
10691
  });
8920
10692
  if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
8921
- writeFileSync13(CONFIG_PATH6, updated.join("\n"), "utf-8");
10693
+ writeFileSync18(CONFIG_PATH8, updated.join("\n"), "utf-8");
8922
10694
  }
8923
10695
  function resolveInferenceMode(cfg) {
8924
10696
  if ((cfg.SYNKRO_GRADING_MODE || "local") === "byok") return "byok";
@@ -8926,7 +10698,7 @@ function resolveInferenceMode(cfg) {
8926
10698
  return "local";
8927
10699
  }
8928
10700
  async function reconcileContainer() {
8929
- const cfg = readConfigEnv2();
10701
+ const cfg = readConfigEnv3();
8930
10702
  const cloudOnly = (cfg.SYNKRO_GRADING_MODE || "local") === "byok" && (cfg.SYNKRO_STORAGE_MODE || "local") === "cloud";
8931
10703
  try {
8932
10704
  const { dockerInstall: dockerInstall2, dockerSafeStop: dockerSafeStop2, readContainerConfig: readContainerConfig2, assertDockerAvailable: assertDockerAvailable2, waitForContainerReady: waitForContainerReady2 } = await Promise.resolve().then(() => (init_dockerInstall(), dockerInstall_exports));
@@ -8950,7 +10722,7 @@ async function reconcileContainer() {
8950
10722
  }
8951
10723
  async function configCommand(args2) {
8952
10724
  if (args2.length === 0) {
8953
- const config2 = readConfigEnv2();
10725
+ const config2 = readConfigEnv3();
8954
10726
  console.log("Synkro config:\n");
8955
10727
  console.log(` inference: ${resolveInferenceMode(config2)}`);
8956
10728
  console.log(` storage: ${config2.SYNKRO_STORAGE_MODE || "local"}`);
@@ -8981,6 +10753,31 @@ To change:`);
8981
10753
  await reconcileContainer();
8982
10754
  return;
8983
10755
  }
10756
+ if (args2[0] === "telemetry") {
10757
+ const rest = args2.slice(1);
10758
+ if (rest.length === 0) {
10759
+ const s = await getTelemetryState();
10760
+ const label = !s.enabled ? "OFF" : s.remoteFlushEnabled ? "ON" : "local-only";
10761
+ console.log(`Telemetry: ${label}`);
10762
+ return;
10763
+ }
10764
+ if (rest.length !== 1 || !["--on", "--off", "--local-only"].includes(rest[0])) {
10765
+ console.error("Usage: synkro config telemetry [--on|--off|--local-only]");
10766
+ process.exit(1);
10767
+ }
10768
+ const flag = rest[0];
10769
+ if (flag === "--on") {
10770
+ await setTelemetryState({ enabled: true, remoteFlushEnabled: true });
10771
+ console.log("Telemetry: ON");
10772
+ } else if (flag === "--off") {
10773
+ await setTelemetryState({ enabled: false, remoteFlushEnabled: false });
10774
+ console.log("Telemetry: OFF");
10775
+ } else {
10776
+ await setTelemetryState({ enabled: true, remoteFlushEnabled: false });
10777
+ console.log("Telemetry: local-only");
10778
+ }
10779
+ return;
10780
+ }
8984
10781
  if (args2[0] === "storage") {
8985
10782
  const value = args2[1];
8986
10783
  if (value !== "local" && value !== "cloud") {
@@ -9006,7 +10803,7 @@ To change:`);
9006
10803
  process.exit(1);
9007
10804
  }
9008
10805
  const token = getAccessToken();
9009
- const config = readConfigEnv2();
10806
+ const config = readConfigEnv3();
9010
10807
  const gatewayUrl = (config.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
9011
10808
  try {
9012
10809
  const resp = await fetch(`${gatewayUrl}/api/v1/cli/me`, {
@@ -9045,25 +10842,210 @@ To change:`);
9045
10842
  }
9046
10843
  if (inferenceValue !== "cloud") await reconcileContainer();
9047
10844
  }
9048
- var SYNKRO_DIR8, CONFIG_PATH6;
10845
+ var SYNKRO_DIR13, CONFIG_PATH8;
9049
10846
  var init_config = __esm({
9050
10847
  "cli/commands/config.ts"() {
9051
10848
  "use strict";
9052
10849
  init_stub();
9053
- SYNKRO_DIR8 = join21(homedir20(), ".synkro");
9054
- CONFIG_PATH6 = join21(SYNKRO_DIR8, "config.env");
10850
+ init_optout();
10851
+ SYNKRO_DIR13 = join28(homedir27(), ".synkro");
10852
+ CONFIG_PATH8 = join28(SYNKRO_DIR13, "config.env");
10853
+ }
10854
+ });
10855
+
10856
+ // cli/commands/telemetry.ts
10857
+ var telemetry_exports2 = {};
10858
+ __export(telemetry_exports2, {
10859
+ telemetryCommand: () => telemetryCommand
10860
+ });
10861
+ import { createInterface as createInterface6 } from "readline";
10862
+ function parseFlag(args2, name) {
10863
+ const prefix = `--${name}=`;
10864
+ for (const a of args2) if (a.startsWith(prefix)) return a.slice(prefix.length);
10865
+ const idx = args2.indexOf(`--${name}`);
10866
+ if (idx >= 0 && idx + 1 < args2.length && !args2[idx + 1].startsWith("--")) {
10867
+ return args2[idx + 1];
10868
+ }
10869
+ return void 0;
10870
+ }
10871
+ function hasFlag(args2, name) {
10872
+ return args2.includes(`--${name}`);
10873
+ }
10874
+ function truncate2(s, max) {
10875
+ if (s.length <= max) return s;
10876
+ return s.slice(0, max) + `\u2026 (+${s.length - max} chars)`;
10877
+ }
10878
+ async function printStats() {
10879
+ const s = await getStats();
10880
+ console.log("Telemetry:");
10881
+ console.log(` install_id: ${s.install_id}`);
10882
+ console.log(` enabled: ${s.enabled ? "yes" : "no"}`);
10883
+ console.log(` remote_flush: ${s.remote_flush_enabled ? "yes" : "no"}`);
10884
+ console.log(` pglite (local): ${s.pglite_reachable ? "reachable" : "none (cloud mode \u2014 query central for totals)"}`);
10885
+ console.log(` local mirror events: ${s.total_events}`);
10886
+ console.log(` pending (queue): ${s.pending_local}`);
10887
+ if (s.newest) console.log(` newest event: ${s.newest}`);
10888
+ if (s.last_drained_at) console.log(` last local drain: ${s.last_drained_at}`);
10889
+ if (s.last_flush_at) console.log(` last flush attempt: ${s.last_flush_at}`);
10890
+ if (s.last_flush_ok_at) console.log(` last flush success: ${s.last_flush_ok_at}`);
10891
+ if (s.last_flush_error) console.log(` last flush error: ${s.last_flush_error}`);
10892
+ const types = Object.entries(s.by_type);
10893
+ if (types.length > 0) {
10894
+ console.log(` by type:`);
10895
+ for (const [name, count] of types) {
10896
+ console.log(` ${name.padEnd(22)} ${count}`);
10897
+ }
10898
+ }
10899
+ }
10900
+ async function printTail(args2) {
10901
+ const opts = {};
10902
+ const limitRaw = parseFlag(args2, "limit");
10903
+ if (limitRaw) {
10904
+ const n = Number.parseInt(limitRaw, 10);
10905
+ if (Number.isFinite(n) && n > 0) opts.limit = n;
10906
+ }
10907
+ const typeRaw = parseFlag(args2, "type");
10908
+ if (typeRaw) opts.eventType = typeRaw;
10909
+ const sessionRaw = parseFlag(args2, "session");
10910
+ if (sessionRaw) opts.sessionId = sessionRaw;
10911
+ const rows = await tail(opts);
10912
+ if (rows.length === 0) {
10913
+ console.log("(no events \u2014 run `synkro start` if the container is down so JSONL pending events can drain)");
10914
+ return;
10915
+ }
10916
+ for (const row of rows) {
10917
+ const session = row.cc_session_id ? ` session=${String(row.cc_session_id).slice(0, 8)}` : "";
10918
+ const ts = typeof row.occurred_at === "string" ? row.occurred_at : new Date(row.occurred_at).toISOString();
10919
+ console.log(` ${ts} ${row.event_type.padEnd(20)} ${row.emitter}${session}`);
10920
+ const contextStr = typeof row.context === "string" ? row.context : JSON.stringify(row.context);
10921
+ console.log(` context: ${truncate2(contextStr, 200)}`);
10922
+ }
10923
+ }
10924
+ async function runFlush(args2) {
10925
+ const force = hasFlag(args2, "force");
10926
+ const r = await flush({ force });
10927
+ const parts = [`remote sent: ${r.sent}`, `queued (kept): ${r.failed}`, `local mirror: ${r.mirrored}`];
10928
+ if (r.skipped_reason) parts.push(`remote skipped (${r.skipped_reason})`);
10929
+ console.log(parts.join(", "));
10930
+ }
10931
+ async function runExport(args2) {
10932
+ const path = args2[0];
10933
+ if (!path) {
10934
+ console.error("usage: synkro telemetry export <path>");
10935
+ process.exit(1);
10936
+ }
10937
+ await exportEvents(path);
10938
+ console.log(`wrote ${path}`);
10939
+ }
10940
+ function confirmYesNo(question) {
10941
+ if (!process.stdin.isTTY) return Promise.resolve(false);
10942
+ return new Promise((resolve4) => {
10943
+ const rl = createInterface6({ input: process.stdin, output: process.stdout });
10944
+ rl.question(`${question} (y/N): `, (answer) => {
10945
+ rl.close();
10946
+ const t = answer.trim().toLowerCase();
10947
+ resolve4(t === "y" || t === "yes");
10948
+ });
10949
+ });
10950
+ }
10951
+ async function runPurge(args2) {
10952
+ const keepInstallId = hasFlag(args2, "keep-install-id");
10953
+ const skipPrompt = hasFlag(args2, "yes") || hasFlag(args2, "y");
10954
+ if (!skipPrompt) {
10955
+ const msg = keepInstallId ? "Wipe all local telemetry events (keeping your install_id)?" : "Wipe all local telemetry events AND reset install_id?";
10956
+ const ok = await confirmYesNo(msg);
10957
+ if (!ok) {
10958
+ console.log("aborted");
10959
+ return;
10960
+ }
10961
+ }
10962
+ const result = await purge({ keepInstallId });
10963
+ if (!result.pglite_reachable) {
10964
+ console.log(`removed ${result.removed_pending} pending event(s) from local queue (container down \u2014 pglite events untouched)`);
10965
+ return;
10966
+ }
10967
+ console.log(`removed ${result.removed_events} pglite event(s) + ${result.removed_pending} pending; meta ${result.removed_meta ? "reset" : "unchanged"}`);
10968
+ }
10969
+ async function telemetryCommand(args2) {
10970
+ const sub = args2[0];
10971
+ const rest = args2.slice(1);
10972
+ switch (sub) {
10973
+ case "stats":
10974
+ await printStats();
10975
+ return;
10976
+ case "tail":
10977
+ await printTail(rest);
10978
+ return;
10979
+ case "flush":
10980
+ await runFlush(rest);
10981
+ return;
10982
+ case "export":
10983
+ await runExport(rest);
10984
+ return;
10985
+ case "purge":
10986
+ await runPurge(rest);
10987
+ return;
10988
+ case "on":
10989
+ await setTelemetryState({ enabled: true, remoteFlushEnabled: true });
10990
+ console.log("telemetry: on (collection + remote flush enabled)");
10991
+ return;
10992
+ case "off":
10993
+ await setTelemetryState({ enabled: false, remoteFlushEnabled: false });
10994
+ console.log("telemetry: off (no collection, no remote flush)");
10995
+ return;
10996
+ case "local-only":
10997
+ await setTelemetryState({ enabled: true, remoteFlushEnabled: false });
10998
+ console.log("telemetry: local-only (collection on, remote flush off)");
10999
+ return;
11000
+ case void 0:
11001
+ case "":
11002
+ case "help":
11003
+ case "--help":
11004
+ case "-h":
11005
+ console.log(HELP);
11006
+ return;
11007
+ default:
11008
+ console.error(`Unknown subcommand: ${sub}`);
11009
+ console.log(HELP);
11010
+ process.exit(1);
11011
+ }
11012
+ }
11013
+ var HELP;
11014
+ var init_telemetry2 = __esm({
11015
+ "cli/commands/telemetry.ts"() {
11016
+ "use strict";
11017
+ init_flush();
11018
+ init_purge();
11019
+ init_stats();
11020
+ init_optout();
11021
+ HELP = `synkro telemetry \u2014 inspect or flush local telemetry events
11022
+
11023
+ Usage:
11024
+ synkro telemetry stats show counts, queue depth, last flush
11025
+ synkro telemetry tail [opts] print recent events (most recent first)
11026
+ --limit=N max rows (default 20)
11027
+ --type=X filter by event_type
11028
+ --session=Y filter by cc_session_id
11029
+ synkro telemetry flush [--force] ship unflushed events now (bypass throttle)
11030
+ synkro telemetry export <path> dump every event as NDJSON to <path>
11031
+ synkro telemetry purge [--keep-install-id] [--yes]
11032
+ wipe local events; --keep-install-id keeps your install_id
11033
+ synkro telemetry on enable collection AND remote flush
11034
+ synkro telemetry off disable collection entirely
11035
+ synkro telemetry local-only collect locally, never flush remotely
11036
+ `;
9055
11037
  }
9056
11038
  });
9057
11039
 
9058
11040
  // cli/bootstrap.js
9059
- import { readFileSync as readFileSync19, existsSync as existsSync22 } from "fs";
11041
+ import { readFileSync as readFileSync26, existsSync as existsSync31 } from "fs";
9060
11042
  import { resolve as resolve3 } from "path";
9061
11043
  var envCandidates = [
9062
11044
  resolve3(process.env.HOME ?? "", ".synkro", "config.env")
9063
11045
  ];
9064
11046
  for (const envPath of envCandidates) {
9065
- if (!existsSync22(envPath)) continue;
9066
- const envContent = readFileSync19(envPath, "utf-8");
11047
+ if (!existsSync31(envPath)) continue;
11048
+ const envContent = readFileSync26(envPath, "utf-8");
9067
11049
  for (const line of envContent.split("\n")) {
9068
11050
  const trimmed = line.trim();
9069
11051
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -9077,8 +11059,10 @@ for (const envPath of envCandidates) {
9077
11059
  var args = process.argv.slice(2);
9078
11060
  var cmd = args[0] || "";
9079
11061
  var subArgs = args.slice(1);
11062
+ var isDetachedChild = process.env.SYNKRO_TELEMETRY_DETACHED === "1";
11063
+ var FLUSH_SKIP = /* @__PURE__ */ new Set(["grade", "version", "--version", "-v", "help", "--help", "-h", ""]);
9080
11064
  function printVersion() {
9081
- console.log("1.7.61");
11065
+ console.log("1.7.63");
9082
11066
  }
9083
11067
  function printHelp2() {
9084
11068
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
@@ -9095,6 +11079,7 @@ Commands:
9095
11079
  update Pull the latest container image and safely restart
9096
11080
  config Show or change grading + storage modes
9097
11081
  claude-desktop Monitor Claude Desktop conversations (local, macOS)
11082
+ telemetry <sub> Inspect or flush local telemetry events
9098
11083
  version Show version
9099
11084
 
9100
11085
  config:
@@ -9113,6 +11098,21 @@ Quick start:
9113
11098
  `);
9114
11099
  }
9115
11100
  async function main() {
11101
+ let emit2 = null;
11102
+ try {
11103
+ ({ emit: emit2 } = await Promise.resolve().then(() => (init_telemetry(), telemetry_exports)));
11104
+ } catch {
11105
+ }
11106
+ if (emit2 && !isDetachedChild && cmd !== "grade") {
11107
+ try {
11108
+ emit2("cli_invocation", {
11109
+ argv: process.argv.slice(2),
11110
+ subcommand: cmd || "help",
11111
+ cwd: process.cwd()
11112
+ });
11113
+ } catch {
11114
+ }
11115
+ }
9116
11116
  switch (cmd) {
9117
11117
  case "install": {
9118
11118
  const { installCommand: installCommand2, parseArgs: parseArgs2 } = await Promise.resolve().then(() => (init_install(), install_exports));
@@ -9205,6 +11205,11 @@ async function main() {
9205
11205
  await runClaudeDesktopTap2({ backfill: subArgs.includes("--backfill") });
9206
11206
  break;
9207
11207
  }
11208
+ case "telemetry": {
11209
+ const { telemetryCommand: telemetryCommand2 } = await Promise.resolve().then(() => (init_telemetry2(), telemetry_exports2));
11210
+ await telemetryCommand2(subArgs);
11211
+ break;
11212
+ }
9208
11213
  default: {
9209
11214
  console.error(`Unknown command: ${cmd}`);
9210
11215
  printHelp2();
@@ -9212,8 +11217,50 @@ async function main() {
9212
11217
  }
9213
11218
  }
9214
11219
  }
9215
- main().catch((err) => {
11220
+ async function postDispatchFlush() {
11221
+ if (FLUSH_SKIP.has(cmd)) return;
11222
+ try {
11223
+ const { flushDetached: flushDetached2 } = await Promise.resolve().then(() => (init_flush(), flush_exports));
11224
+ flushDetached2();
11225
+ } catch {
11226
+ }
11227
+ }
11228
+ async function shutdown(code) {
11229
+ try {
11230
+ const { closeDb: closeDb2 } = await Promise.resolve().then(() => (init_db(), db_exports));
11231
+ await closeDb2();
11232
+ } catch {
11233
+ }
11234
+ process.exitCode = code;
11235
+ const force = setTimeout(() => {
11236
+ let pending = 0;
11237
+ const done = () => {
11238
+ if (--pending === 0) process.exit(code);
11239
+ };
11240
+ for (const stream of [process.stdout, process.stderr]) {
11241
+ if (stream.writableLength > 0) {
11242
+ pending++;
11243
+ stream.once("drain", done);
11244
+ }
11245
+ }
11246
+ if (pending === 0) process.exit(code);
11247
+ }, 200);
11248
+ force.unref();
11249
+ }
11250
+ main().then(() => postDispatchFlush()).then(() => shutdown(0)).catch(async (err) => {
11251
+ try {
11252
+ const { emit: emit2 } = await Promise.resolve().then(() => (init_telemetry(), telemetry_exports));
11253
+ emit2("error", {
11254
+ category: "unknown",
11255
+ code: err && err.code || "UNKNOWN",
11256
+ message: String(err && err.message ? err.message : err),
11257
+ stack: err && err.stack || "",
11258
+ emitter_context: "bootstrap:" + (cmd || "unknown")
11259
+ });
11260
+ } catch {
11261
+ }
11262
+ await postDispatchFlush();
9216
11263
  console.error(err);
9217
- process.exit(1);
11264
+ await shutdown(1);
9218
11265
  });
9219
11266
  //# sourceMappingURL=bootstrap.js.map