@synkro-sh/cli 1.7.60 → 1.7.62

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.62";
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
  }
@@ -84,22 +1389,22 @@ var init_agentDetect = __esm({
84
1389
  });
85
1390
 
86
1391
  // cli/installer/ccHookConfig.ts
87
- import { existsSync as existsSync2, readFileSync, writeFileSync, renameSync, mkdirSync } from "fs";
1392
+ import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync6, renameSync as renameSync3, mkdirSync as mkdirSync4 } from "fs";
88
1393
  import { dirname } from "path";
89
1394
  function readSettings(path) {
90
- if (!existsSync2(path)) return {};
1395
+ if (!existsSync10(path)) return {};
91
1396
  try {
92
- const raw = readFileSync(path, "utf-8");
1397
+ const raw = readFileSync8(path, "utf-8");
93
1398
  return JSON.parse(raw);
94
1399
  } catch (err) {
95
1400
  throw new Error(`Failed to parse ${path}: ${err.message}`);
96
1401
  }
97
1402
  }
98
1403
  function writeSettingsAtomic(path, settings) {
99
- mkdirSync(dirname(path), { recursive: true });
1404
+ mkdirSync4(dirname(path), { recursive: true });
100
1405
  const tmpPath = `${path}.synkro.tmp`;
101
- writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
102
- renameSync(tmpPath, path);
1406
+ writeFileSync6(tmpPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1407
+ renameSync3(tmpPath, path);
103
1408
  }
104
1409
  function isSynkroEntry(entry) {
105
1410
  if (entry?.[SYNKRO_MARKER]) return true;
@@ -290,7 +1595,7 @@ function installCCHooks(settingsPath, config) {
290
1595
  writeSettingsAtomic(settingsPath, settings);
291
1596
  }
292
1597
  function uninstallCCHooks(settingsPath) {
293
- if (!existsSync2(settingsPath)) return false;
1598
+ if (!existsSync10(settingsPath)) return false;
294
1599
  const settings = readSettings(settingsPath);
295
1600
  if (!settings.hooks) return false;
296
1601
  const events = ["PreToolUse", "PostToolUse", "SessionEnd", "SessionStart", "Stop", "UserPromptSubmit"];
@@ -322,9 +1627,9 @@ var init_ccHookConfig = __esm({
322
1627
  });
323
1628
 
324
1629
  // cli/installer/cursorHookConfig.ts
325
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, mkdirSync as mkdirSync2 } from "fs";
1630
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, renameSync as renameSync4, mkdirSync as mkdirSync5 } from "fs";
326
1631
  import { dirname as dirname2, resolve, normalize } from "path";
327
- import { homedir as homedir2 } from "os";
1632
+ import { homedir as homedir8 } from "os";
328
1633
  function shellQuote(s) {
329
1634
  return "'" + s.replace(/'/g, "'\\''") + "'";
330
1635
  }
@@ -344,7 +1649,7 @@ function validateHooksPath(path) {
344
1649
  function readHooksFile(rawPath) {
345
1650
  const safePath = validateHooksPath(rawPath);
346
1651
  try {
347
- const raw = readFileSync2(safePath, "utf-8");
1652
+ const raw = readFileSync9(safePath, "utf-8");
348
1653
  return JSON.parse(raw);
349
1654
  } catch (err) {
350
1655
  if (err?.code === "ENOENT") return { version: 1, hooks: {} };
@@ -353,10 +1658,10 @@ function readHooksFile(rawPath) {
353
1658
  }
354
1659
  function writeHooksFileAtomic(rawPath, data) {
355
1660
  const safePath = validateHooksPath(rawPath);
356
- mkdirSync2(dirname2(safePath), { recursive: true });
1661
+ mkdirSync5(dirname2(safePath), { recursive: true });
357
1662
  const tmpPath = `${safePath}.synkro.tmp`;
358
- writeFileSync2(tmpPath, JSON.stringify(data, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
359
- renameSync2(tmpPath, safePath);
1663
+ writeFileSync7(tmpPath, JSON.stringify(data, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
1664
+ renameSync4(tmpPath, safePath);
360
1665
  }
361
1666
  function isSynkroEntry2(entry) {
362
1667
  if (entry?.[SYNKRO_MARKER2]) return true;
@@ -525,8 +1830,8 @@ var init_cursorHookConfig = __esm({
525
1830
  "use strict";
526
1831
  SYNKRO_MARKER2 = "__synkro_managed__";
527
1832
  ALLOWED_PARENT_DIRS = [
528
- resolve(homedir2(), ".cursor"),
529
- resolve(homedir2(), ".config", "cursor")
1833
+ resolve(homedir8(), ".cursor"),
1834
+ resolve(homedir8(), ".config", "cursor")
530
1835
  ];
531
1836
  ALL_EVENTS = [
532
1837
  "sessionStart",
@@ -546,25 +1851,25 @@ var init_cursorHookConfig = __esm({
546
1851
  });
547
1852
 
548
1853
  // 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";
1854
+ import { existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync8, renameSync as renameSync5, mkdirSync as mkdirSync6 } from "fs";
1855
+ import { homedir as homedir9 } from "os";
1856
+ import { dirname as dirname3, join as join8 } from "path";
552
1857
  import { spawnSync } from "child_process";
553
1858
  import { randomBytes } from "crypto";
554
1859
  function readClaudeJson() {
555
- if (!existsSync3(CC_CONFIG_PATH)) return {};
1860
+ if (!existsSync11(CC_CONFIG_PATH)) return {};
556
1861
  try {
557
- const raw = readFileSync3(CC_CONFIG_PATH, "utf-8");
1862
+ const raw = readFileSync10(CC_CONFIG_PATH, "utf-8");
558
1863
  return JSON.parse(raw);
559
1864
  } catch (err) {
560
1865
  throw new Error(`Failed to parse ${CC_CONFIG_PATH}: ${err.message}`);
561
1866
  }
562
1867
  }
563
1868
  function writeClaudeJsonAtomic(config) {
564
- mkdirSync3(dirname3(CC_CONFIG_PATH), { recursive: true });
1869
+ mkdirSync6(dirname3(CC_CONFIG_PATH), { recursive: true });
565
1870
  const tmpPath = `${CC_CONFIG_PATH}.synkro.tmp`;
566
- writeFileSync3(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
567
- renameSync3(tmpPath, CC_CONFIG_PATH);
1871
+ writeFileSync8(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1872
+ renameSync5(tmpPath, CC_CONFIG_PATH);
568
1873
  }
569
1874
  function installMcpConfig(opts) {
570
1875
  const config = readClaudeJson();
@@ -573,7 +1878,7 @@ function installMcpConfig(opts) {
573
1878
  if (entry?.[SYNKRO_MARKER3] === true) delete config.mcpServers[name];
574
1879
  }
575
1880
  if (opts.local) {
576
- const proxyScript = join2(homedir3(), ".synkro", "hooks", "mcp-stdio-proxy.ts");
1881
+ const proxyScript = join8(homedir9(), ".synkro", "hooks", "mcp-stdio-proxy.ts");
577
1882
  config.mcpServers[SYNKRO_SERVER_NAME] = {
578
1883
  type: "stdio",
579
1884
  command: "bun",
@@ -594,7 +1899,7 @@ function installMcpConfig(opts) {
594
1899
  return { path: CC_CONFIG_PATH, url };
595
1900
  }
596
1901
  function uninstallMcpConfig() {
597
- if (!existsSync3(CC_CONFIG_PATH)) return false;
1902
+ if (!existsSync11(CC_CONFIG_PATH)) return false;
598
1903
  const config = readClaudeJson();
599
1904
  if (!config.mcpServers || Object.keys(config.mcpServers).length === 0) return false;
600
1905
  let removed = false;
@@ -610,19 +1915,19 @@ function uninstallMcpConfig() {
610
1915
  return true;
611
1916
  }
612
1917
  function readCursorMcpJson() {
613
- if (!existsSync3(CURSOR_MCP_PATH)) return {};
1918
+ if (!existsSync11(CURSOR_MCP_PATH)) return {};
614
1919
  try {
615
- const raw = readFileSync3(CURSOR_MCP_PATH, "utf-8");
1920
+ const raw = readFileSync10(CURSOR_MCP_PATH, "utf-8");
616
1921
  return JSON.parse(raw);
617
1922
  } catch (err) {
618
1923
  throw new Error(`Failed to parse ${CURSOR_MCP_PATH}: ${err.message}`);
619
1924
  }
620
1925
  }
621
1926
  function writeCursorMcpJsonAtomic(config) {
622
- mkdirSync3(dirname3(CURSOR_MCP_PATH), { recursive: true });
1927
+ mkdirSync6(dirname3(CURSOR_MCP_PATH), { recursive: true });
623
1928
  const tmpPath = `${CURSOR_MCP_PATH}.synkro.tmp`;
624
- writeFileSync3(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
625
- renameSync3(tmpPath, CURSOR_MCP_PATH);
1929
+ writeFileSync8(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1930
+ renameSync5(tmpPath, CURSOR_MCP_PATH);
626
1931
  }
627
1932
  function installCursorMcpConfig(opts) {
628
1933
  const config = readCursorMcpJson();
@@ -633,10 +1938,10 @@ function installCursorMcpConfig(opts) {
633
1938
  if (opts.local) {
634
1939
  const port = process.env.SYNKRO_MCP_PORT || "18931";
635
1940
  const url2 = `http://127.0.0.1:${port}/`;
636
- const jwtPath = join2(homedir3(), ".synkro", ".mcp-jwt");
1941
+ const jwtPath = join8(homedir9(), ".synkro", ".mcp-jwt");
637
1942
  let jwt2 = "";
638
1943
  try {
639
- jwt2 = readFileSync3(jwtPath, "utf-8").trim();
1944
+ jwt2 = readFileSync10(jwtPath, "utf-8").trim();
640
1945
  } catch {
641
1946
  }
642
1947
  config.mcpServers[SYNKRO_SERVER_NAME] = {
@@ -657,7 +1962,7 @@ function installCursorMcpConfig(opts) {
657
1962
  return { path: CURSOR_MCP_PATH, url };
658
1963
  }
659
1964
  function uninstallCursorMcpConfig() {
660
- if (!existsSync3(CURSOR_MCP_PATH)) return false;
1965
+ if (!existsSync11(CURSOR_MCP_PATH)) return false;
661
1966
  const config = readCursorMcpJson();
662
1967
  if (!config.mcpServers || Object.keys(config.mcpServers).length === 0) return false;
663
1968
  let removed = false;
@@ -676,27 +1981,27 @@ function resolveBunBin() {
676
1981
  const r = spawnSync("which", ["bun"], { encoding: "utf-8", timeout: 5e3 });
677
1982
  const resolved = (r.stdout || "").split("\n")[0].trim();
678
1983
  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;
1984
+ for (const p of ["/opt/homebrew/bin/bun", "/usr/local/bin/bun", join8(homedir9(), ".bun", "bin", "bun")]) {
1985
+ if (existsSync11(p)) return p;
681
1986
  }
682
1987
  return "bun";
683
1988
  }
684
1989
  function readDesktopJson() {
685
- if (!existsSync3(CLAUDE_DESKTOP_CONFIG_PATH)) return {};
1990
+ if (!existsSync11(CLAUDE_DESKTOP_CONFIG_PATH)) return {};
686
1991
  try {
687
- return JSON.parse(readFileSync3(CLAUDE_DESKTOP_CONFIG_PATH, "utf-8"));
1992
+ return JSON.parse(readFileSync10(CLAUDE_DESKTOP_CONFIG_PATH, "utf-8"));
688
1993
  } catch (err) {
689
1994
  throw new Error(`Failed to parse ${CLAUDE_DESKTOP_CONFIG_PATH}: ${err.message}`);
690
1995
  }
691
1996
  }
692
1997
  function writeDesktopJsonAtomic(config) {
693
- mkdirSync3(dirname3(CLAUDE_DESKTOP_CONFIG_PATH), { recursive: true });
1998
+ mkdirSync6(dirname3(CLAUDE_DESKTOP_CONFIG_PATH), { recursive: true });
694
1999
  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);
2000
+ writeFileSync8(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2001
+ renameSync5(tmpPath, CLAUDE_DESKTOP_CONFIG_PATH);
697
2002
  }
698
2003
  function installClaudeDesktopMcpConfig() {
699
- if (!existsSync3(MCP_STDIO_PROXY_PATH)) {
2004
+ if (!existsSync11(MCP_STDIO_PROXY_PATH)) {
700
2005
  return {
701
2006
  ok: false,
702
2007
  path: CLAUDE_DESKTOP_CONFIG_PATH,
@@ -719,7 +2024,7 @@ function installClaudeDesktopMcpConfig() {
719
2024
  return { ok: true, path: CLAUDE_DESKTOP_CONFIG_PATH, proxy: MCP_STDIO_PROXY_PATH };
720
2025
  }
721
2026
  function uninstallClaudeDesktopMcpConfig() {
722
- if (!existsSync3(CLAUDE_DESKTOP_CONFIG_PATH)) return false;
2027
+ if (!existsSync11(CLAUDE_DESKTOP_CONFIG_PATH)) return false;
723
2028
  const config = readDesktopJson();
724
2029
  if (!config.mcpServers || Object.keys(config.mcpServers).length === 0) return false;
725
2030
  let removed = false;
@@ -740,24 +2045,24 @@ var init_mcpConfig = __esm({
740
2045
  "use strict";
741
2046
  SYNKRO_MARKER3 = "__synkro_managed__";
742
2047
  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(),
2048
+ CC_CONFIG_PATH = join8(homedir9(), ".claude.json");
2049
+ CURSOR_MCP_PATH = join8(homedir9(), ".cursor", "mcp.json");
2050
+ CLAUDE_DESKTOP_CONFIG_PATH = join8(
2051
+ homedir9(),
747
2052
  "Library",
748
2053
  "Application Support",
749
2054
  "Claude",
750
2055
  "claude_desktop_config.json"
751
2056
  );
752
- MCP_STDIO_PROXY_PATH = join2(homedir3(), ".synkro", "hooks", "mcp-stdio-proxy.ts");
2057
+ MCP_STDIO_PROXY_PATH = join8(homedir9(), ".synkro", "hooks", "mcp-stdio-proxy.ts");
753
2058
  }
754
2059
  });
755
2060
 
756
2061
  // cli/installer/skillParser.ts
757
- import { existsSync as existsSync4 } from "fs";
2062
+ import { existsSync as existsSync12 } from "fs";
758
2063
  import { resolve as resolve2 } from "path";
759
2064
  function resolveSkillPaths(skills, repoRoot) {
760
- return skills.filter((s) => s.endsWith(".md")).map((s) => resolve2(repoRoot, s)).filter((p) => existsSync4(p));
2065
+ return skills.filter((s) => s.endsWith(".md")).map((s) => resolve2(repoRoot, s)).filter((p) => existsSync12(p));
761
2066
  }
762
2067
  var init_skillParser = __esm({
763
2068
  "cli/installer/skillParser.ts"() {
@@ -773,14 +2078,133 @@ var STUB_COMMON_TS, STUB_EDIT_PRECHECK_TS, STUB_CWE_PRECHECK_TS, STUB_CVE_PRECHE
773
2078
  var init_hookScriptsTs = __esm({
774
2079
  "cli/installer/hookScriptsTs.ts"() {
775
2080
  "use strict";
776
- STUB_COMMON_TS = String.raw`import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
2081
+ STUB_COMMON_TS = String.raw`import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync, statSync, openSync, readSync, closeSync } from 'node:fs';
777
2082
  import { execSync } from 'node:child_process';
778
2083
  import { homedir } from 'node:os';
779
2084
  import { join } from 'node:path';
2085
+ import { randomUUID, createHash } from 'node:crypto';
780
2086
 
781
2087
  const HOME = homedir();
782
2088
  const PORT = process.env.SYNKRO_MCP_PORT || '18931';
783
2089
 
2090
+ // ─── Telemetry: sync JSONL append, drained later by the CLI binary ──────────
2091
+ // Matches packages/cli/cli/telemetry/emit.ts. Inlined here so each bun hook
2092
+ // is self-contained (no path-relative import to a sibling module that may not
2093
+ // exist post-install). Writes to the same ~/.synkro/telemetry-pending.jsonl
2094
+ // the CLI binary appends to; ON CONFLICT in drain.ts dedupes.
2095
+ const PENDING_JSONL = join(HOME, '.synkro', 'telemetry-pending.jsonl');
2096
+ const META_JSON = join(HOME, '.synkro', 'telemetry-meta.json');
2097
+ const CREDS_JSON = join(HOME, '.synkro', 'credentials.json');
2098
+
2099
+ interface TelemMeta { install_id?: string; enabled?: boolean }
2100
+ let _telemMetaCache: TelemMeta | null = null;
2101
+ function telemMeta(): TelemMeta {
2102
+ if (_telemMetaCache) return _telemMetaCache;
2103
+ try {
2104
+ _telemMetaCache = JSON.parse(readFileSync(META_JSON, 'utf-8')) as TelemMeta;
2105
+ } catch { _telemMetaCache = {}; }
2106
+ return _telemMetaCache;
2107
+ }
2108
+
2109
+ interface TelemCreds { user_id?: string; org_id?: string; email?: string }
2110
+ let _telemCredsCache: TelemCreds | null = null;
2111
+ function telemCreds(): TelemCreds {
2112
+ if (_telemCredsCache) return _telemCredsCache;
2113
+ try {
2114
+ const c = JSON.parse(readFileSync(CREDS_JSON, 'utf-8')) as TelemCreds;
2115
+ _telemCredsCache = { user_id: c.user_id, org_id: c.org_id, email: c.email };
2116
+ } catch { _telemCredsCache = {}; }
2117
+ return _telemCredsCache;
2118
+ }
2119
+
2120
+ interface HookEmitOpts {
2121
+ agent_kind?: 'claude_code' | 'cursor';
2122
+ cc_session_id?: string;
2123
+ cc_tool_use_id?: string;
2124
+ cc_model?: string;
2125
+ permission_mode?: string;
2126
+ cwd?: string;
2127
+ repo?: string;
2128
+ }
2129
+
2130
+ export function hookEmit(eventType: string, context: unknown, opts: HookEmitOpts = {}): void {
2131
+ try {
2132
+ const meta = telemMeta();
2133
+ if (meta.enabled === false) return;
2134
+ // Never ship rows with an empty install_id — they can't be attributed. An
2135
+ // absent meta file means the CLI hasn't bootstrapped telemetry yet; skip
2136
+ // rather than emit an orphan row (the CLI metaCache mints the id on install).
2137
+ if (!meta.install_id) return;
2138
+ const creds = telemCreds();
2139
+ const row = {
2140
+ client_event_id: randomUUID(),
2141
+ event_type: eventType,
2142
+ occurred_at: new Date().toISOString(),
2143
+ install_id: meta.install_id,
2144
+ user_id: creds.user_id ?? null,
2145
+ org_id: creds.org_id ?? null,
2146
+ email: creds.email ?? null,
2147
+ cli_version: process.env.SYNKRO_VERSION || '',
2148
+ emitter: process.env.SYNKRO_TELEMETRY_EMITTER || 'hook',
2149
+ agent_kind: opts.agent_kind ?? null,
2150
+ cc_session_id: opts.cc_session_id ?? null,
2151
+ cc_tool_use_id: opts.cc_tool_use_id ?? null,
2152
+ cc_turn_id: null,
2153
+ cc_model: opts.cc_model ?? null,
2154
+ permission_mode: opts.permission_mode ?? null,
2155
+ cwd: opts.cwd ?? null,
2156
+ repo: opts.repo ?? null,
2157
+ git_branch: null,
2158
+ git_sha: null,
2159
+ policy_name: null,
2160
+ capture_depth: null,
2161
+ platform: process.platform,
2162
+ hostname_hash: null,
2163
+ node_version: process.version || '',
2164
+ context,
2165
+ };
2166
+ try { mkdirSync(join(HOME, '.synkro'), { recursive: true }); } catch {}
2167
+ appendFileSync(PENDING_JSONL, JSON.stringify(row) + '\n');
2168
+ } catch { /* never let telemetry block a hook */ }
2169
+ }
2170
+
2171
+ // Surface name → telemetry event type. One-shot lookup for the runStub wrap.
2172
+ function eventForSurface(surface: string): { type: string; sessionPhase?: 'start' | 'end' } {
2173
+ switch (surface) {
2174
+ case 'session-start': return { type: 'session_lifecycle', sessionPhase: 'start' };
2175
+ case 'stop-summary': return { type: 'session_lifecycle', sessionPhase: 'end' };
2176
+ case 'prompt-submit': return { type: 'human_interaction' };
2177
+ case 'bash-followup': return { type: 'human_interaction' };
2178
+ case 'mcp-gate': return { type: 'mcp_tool_use' };
2179
+ case 'plan-judge': return { type: 'plan_review_result' };
2180
+ case 'transcript-sync': return { type: 'tool_call' };
2181
+ default: return { type: 'tool_call' };
2182
+ }
2183
+ }
2184
+
2185
+ // Inspect the verdict text returned by the local server to derive a coarse
2186
+ // verdict for the telemetry row. Both CC and Cursor return JSON; we parse
2187
+ // lightly and ignore failures (default 'pass').
2188
+ function parseVerdict(text: string, harness: string): 'pass' | 'deny' | 'allow' | 'warning' | 'error' | 'ask' {
2189
+ if (!text) return 'pass';
2190
+ try {
2191
+ const j = JSON.parse(text);
2192
+ if (harness === 'cursor') {
2193
+ const p = j?.permission;
2194
+ if (p === 'deny') return 'deny';
2195
+ if (p === 'allow') return 'allow';
2196
+ return 'pass';
2197
+ }
2198
+ const dec = j?.hookSpecificOutput?.permissionDecision;
2199
+ if (dec === 'deny') return 'deny';
2200
+ // 'ask' is a consent prompt, NOT a block — keep it distinct so it doesn't
2201
+ // manufacture spurious violation rows (only genuine deny/block violates).
2202
+ if (dec === 'ask') return 'ask';
2203
+ if (dec === 'allow') return 'allow';
2204
+ return 'pass';
2205
+ } catch { return 'pass'; }
2206
+ }
2207
+
784
2208
  function loadMcpJwt(): string {
785
2209
  try { return readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch { return ''; }
786
2210
  }
@@ -883,14 +2307,28 @@ interface StubOpts {
883
2307
 
884
2308
  export async function runStub(surface: string, opts: StubOpts = {}): Promise<void> {
885
2309
  const harness = isCursor(opts.harness) ? 'cursor' : 'cc';
2310
+ const startedAt = Date.now();
2311
+ process.env.SYNKRO_TELEMETRY_EMITTER = process.env.SYNKRO_TELEMETRY_EMITTER || ('hook:' + surface);
2312
+ let telemPayload: any = null;
2313
+ let telemCwd = '';
2314
+ let telemSessionId = '';
886
2315
  try {
887
2316
  const input = await readStdin();
888
2317
  if (!input.trim()) { out(failOpen(harness)); return; }
889
2318
  const payload = JSON.parse(input);
2319
+ telemPayload = payload;
2320
+ telemCwd = (typeof payload?.cwd === 'string' ? payload.cwd : '') || '';
2321
+ telemSessionId = String(payload?.session_id || payload?.conversation_id || '');
890
2322
 
891
2323
  // 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; }
2324
+ // against the org policy directly. Fail-open inside cloudMcpGate. Emit the
2325
+ // same mcp_tool_use (+ violation on block) the local path does, for parity.
2326
+ if (surface === 'mcp-gate' && deployIsCloud()) {
2327
+ const cloudResp = await cloudMcpGate(payload, harness);
2328
+ out(cloudResp);
2329
+ emitStubTelemetry(surface, harness, payload, cloudResp, Date.now() - startedAt, telemCwd, telemSessionId);
2330
+ return;
2331
+ }
894
2332
 
895
2333
  const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots : [];
896
2334
  const cwd = (typeof payload.cwd === 'string' && payload.cwd) || workspaceRoots[0] || '';
@@ -906,6 +2344,22 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
906
2344
  try { synkroFileText = readFileSync(join(root, 'synkro.toml'), 'utf-8'); } catch {}
907
2345
  }
908
2346
  if (!synkroFileText) {
2347
+ // Repo not onboarded — emit a minimal tool_call so usage analytics still
2348
+ // sees the hook fire, then short-circuit. No verdict, no command body.
2349
+ // transcript-sync is a background sync, not a tool invocation — omit it so
2350
+ // it doesn't inflate tool_call counts. No model derivation on this hot path.
2351
+ if (surface !== 'transcript-sync') {
2352
+ hookEmit('tool_call', {
2353
+ tool_name: String(payload?.tool_name || ''),
2354
+ hook_event: surface === 'bash-followup' ? 'PostToolUse' : 'PreToolUse',
2355
+ tool_input: {},
2356
+ verdict: 'pass',
2357
+ route: 'local',
2358
+ route_fallback_reason: 'repo_not_onboarded',
2359
+ silent: false,
2360
+ duration_ms: Date.now() - startedAt,
2361
+ }, telemPayloadOpts(payload, harness, telemCwd, telemSessionId));
2362
+ }
909
2363
  if (opts.telemetry) { out(failOpen(harness)); return; }
910
2364
  const hint = noSynkroHint(sessionId);
911
2365
  if (!hint) { out(failOpen(harness)); return; }
@@ -985,9 +2439,228 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
985
2439
  } catch (e) { /* connection refused / timeout → retry below */ }
986
2440
  if (i < attempts - 1) await new Promise((r) => setTimeout(r, 500 * (i + 1)));
987
2441
  }
988
- out(text || failOpen(harness));
989
- } catch {
2442
+ const responseText = text || failOpen(harness);
2443
+ out(responseText);
2444
+ emitStubTelemetry(surface, harness, telemPayload, responseText, Date.now() - startedAt, telemCwd, telemSessionId);
2445
+ } catch (err) {
990
2446
  out(failOpen(harness));
2447
+ const msg = err instanceof Error ? err.message : String(err);
2448
+ const stack = err instanceof Error ? (err.stack || '') : '';
2449
+ hookEmit('error', {
2450
+ category: 'hook_runtime',
2451
+ code: 'STUB_RUNSTUB_THREW',
2452
+ message: msg.slice(0, 4096),
2453
+ stack: stack.slice(0, 16384),
2454
+ emitter_context: 'runStub:' + surface,
2455
+ }, telemPayloadOpts(telemPayload, harness, telemCwd, telemSessionId));
2456
+ }
2457
+ }
2458
+
2459
+ // ─── Bounded transcript tail read + model derivation ────────────────────────
2460
+ // The CC model lives in the LAST assistant message, always near the end of the
2461
+ // transcript. Read at most the last 200KB off disk instead of slurping+splitting
2462
+ // the whole file (transcripts reach 100s of MB → O(n^2) per session on the
2463
+ // verdict hot path). Only ever called on the ACTIVE path, never when dormant.
2464
+ const TRANSCRIPT_TAIL_BYTES = 200000;
2465
+ function readTranscriptTail(path: string): string {
2466
+ try {
2467
+ const size = statSync(path).size;
2468
+ if (size <= TRANSCRIPT_TAIL_BYTES) return readFileSync(path, 'utf-8');
2469
+ const fd = openSync(path, 'r');
2470
+ try {
2471
+ const buf = Buffer.allocUnsafe(TRANSCRIPT_TAIL_BYTES);
2472
+ const n = readSync(fd, buf, 0, TRANSCRIPT_TAIL_BYTES, size - TRANSCRIPT_TAIL_BYTES);
2473
+ return buf.subarray(0, n).toString('utf-8');
2474
+ } finally { closeSync(fd); }
2475
+ } catch { return ''; }
2476
+ }
2477
+ function deriveCcModel(payload: any): string {
2478
+ const tp = (payload && payload.transcript_path) || '';
2479
+ if (!tp || !existsSync(tp)) return '';
2480
+ const lines = readTranscriptTail(tp).split('\n');
2481
+ for (let i = lines.length - 1; i >= 0; i--) {
2482
+ if (!lines[i].includes('"type":"assistant"')) continue;
2483
+ try {
2484
+ const obj = JSON.parse(lines[i]);
2485
+ if (obj?.message?.model) return String(obj.message.model);
2486
+ } catch { /* torn line (tail may start mid-record) — skip, keep scanning */ }
2487
+ }
2488
+ return '';
2489
+ }
2490
+
2491
+ // ─── Telemetry payload hygiene: scrub + byte-cap + summarize ────────────────
2492
+ // Self-contained port of _synkro-common's scrubSecrets — stub mode DELETES
2493
+ // _synkro-common.ts, so the stub cannot import it. Same hard-secret patterns +
2494
+ // credential key/value redaction; the entropy heuristic is omitted since these
2495
+ // rows never carry file bodies and every field is additionally byte-capped.
2496
+ const TE_SECRET_PATTERNS: Array<{ type: string; re: RegExp }> = [
2497
+ { type: 'anthropic-key', re: /sk-ant-[A-Za-z0-9_-]{24,}/g },
2498
+ { type: 'openai-key', re: /sk-(?:proj-)?[A-Za-z0-9]{32,}/g },
2499
+ { type: 'aws-access-key', re: /AKIA[0-9A-Z]{16}/g },
2500
+ { type: 'github-token', re: /gh[posru]_[A-Za-z0-9]{36,}/g },
2501
+ { type: 'google-api-key', re: /AIza[0-9A-Za-z_-]{35}/g },
2502
+ { type: 'slack-token', re: /xox[baprs]-[0-9A-Za-z-]{10,}/g },
2503
+ { type: 'stripe-key', re: /[rs]k_(?:live|test)_[A-Za-z0-9]{20,}/g },
2504
+ { type: 'jwt', re: /eyJ[A-Za-z0-9_-]{10,}[.][A-Za-z0-9_-]{10,}[.][A-Za-z0-9_-]{10,}/g },
2505
+ { type: 'private-key', re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g },
2506
+ { type: 'bearer-token', re: /[Bb]earer[ ]+[A-Za-z0-9._-]{20,}/g },
2507
+ ];
2508
+ function isPlaceholderSecret(v: string): boolean {
2509
+ if (/[$<>{}]/.test(v)) return true;
2510
+ const tail = v.replace(/^(sk-ant-|[rs]k_(?:live|test)_|sk-|AKIA|gh[posru]_|AIza|xox[baprs]-|Bearer[ ]+)/, '');
2511
+ return /^(x+|0+|a+|placeholder|example|changeme|your[-_a-z0-9]*|dummy|test|fake|redacted|sample)$/i.test(tail);
2512
+ }
2513
+ function scrubSecrets(text: string): string {
2514
+ if (!text) return text;
2515
+ let out = text;
2516
+ for (const { type, re } of TE_SECRET_PATTERNS) {
2517
+ out = out.replace(re, (m) => isPlaceholderSecret(m) ? m : '<redacted:' + type + '>');
2518
+ }
2519
+ out = out.replace(/((?:password|passwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|private[_-]?key)["' ]*[=:][ ]*["']?)([^ "'\n]{12,})/gi, (full, pfx, val) => {
2520
+ if (/[$<>{}]/.test(val)) return full;
2521
+ return pfx + '<redacted:credential>';
2522
+ });
2523
+ return out;
2524
+ }
2525
+ // Byte-cap (not char-cap): multi-byte content must not push a JSONL row past the
2526
+ // ~4KB append-atomicity window, above which concurrent hook appends tear.
2527
+ function cap(s: string, maxBytes: number): string {
2528
+ if (!s) return s;
2529
+ if (Buffer.byteLength(s, 'utf-8') <= maxBytes) return s;
2530
+ let out = s.slice(0, maxBytes);
2531
+ while (out && Buffer.byteLength(out, 'utf-8') > maxBytes) out = out.slice(0, -1);
2532
+ return out;
2533
+ }
2534
+ // Field byte budgets. A row's fixed envelope (ids, timestamps, cwd, model) runs
2535
+ // ~0.8-1.2KB, so no single free-text field may approach 4KB or the whole JSONL
2536
+ // row overruns the append-atomicity window. These leave margin even together.
2537
+ const TE_CMD_CAP = 1200;
2538
+ const TE_REASON_CAP = 800;
2539
+ const TE_EXCERPT_CAP = 1200;
2540
+ const TE_PROMPT_CAP = 2400;
2541
+ // Never embed a raw tool_input — Write/Edit carry whole file bodies, Bash the raw
2542
+ // command, so verbatim rows exfiltrate secrets AND overrun the 4KB row budget.
2543
+ // Emit a fixed-size fingerprint: tool name, top-level keys, serialized byte
2544
+ // length, short content hash. Bash is the one surface where the command text is
2545
+ // itself the signal, so include it scrubbed + byte-capped when asked.
2546
+ function toolInputSummary(toolName: string, toolInput: any, includeCommand: boolean): Record<string, any> {
2547
+ let ser = '';
2548
+ try { ser = JSON.stringify(toolInput == null ? {} : toolInput); } catch { ser = String(toolInput); }
2549
+ const isObj = !!toolInput && typeof toolInput === 'object' && !Array.isArray(toolInput);
2550
+ const summary: Record<string, any> = {
2551
+ tool_name: toolName || undefined,
2552
+ keys: isObj ? Object.keys(toolInput).slice(0, 24) : [],
2553
+ bytes: Buffer.byteLength(ser, 'utf-8'),
2554
+ hash: createHash('sha256').update(ser).digest('hex').slice(0, 12),
2555
+ };
2556
+ if (includeCommand && isObj && typeof toolInput.command === 'string' && toolInput.command) {
2557
+ summary.command = cap(scrubSecrets(toolInput.command), TE_CMD_CAP);
2558
+ }
2559
+ return summary;
2560
+ }
2561
+
2562
+ // cc_model is derived by the caller (bounded tail read) and passed in; this stays
2563
+ // allocation-only so the dormant hot path never touches the transcript.
2564
+ function telemPayloadOpts(payload: any, harness: string, cwd: string, sessionId: string, ccModel?: string): any {
2565
+ return {
2566
+ agent_kind: harness === 'cursor' ? 'cursor' : 'claude_code',
2567
+ cc_session_id: sessionId || undefined,
2568
+ cc_tool_use_id: payload?.tool_use_id ? String(payload.tool_use_id) : undefined,
2569
+ cc_model: ccModel || undefined,
2570
+ permission_mode: payload?.permission_mode ? String(payload.permission_mode) : undefined,
2571
+ cwd: cwd || undefined,
2572
+ };
2573
+ }
2574
+
2575
+ function emitStubTelemetry(
2576
+ surface: string, harness: string, payload: any, responseText: string,
2577
+ durationMs: number, cwd: string, sessionId: string,
2578
+ ): void {
2579
+ // transcript-sync is a background sync, not a tool invocation — no tool_call
2580
+ // row (keeps tool_call counts accurate); its usage lands via session_lifecycle.
2581
+ // Bail before any transcript read so it stays cheap.
2582
+ if (surface === 'transcript-sync') return;
2583
+
2584
+ const opts = telemPayloadOpts(payload, harness, cwd, sessionId, deriveCcModel(payload));
2585
+ const verdict = parseVerdict(responseText, harness);
2586
+ const meta = eventForSurface(surface);
2587
+
2588
+ if (meta.sessionPhase === 'start') {
2589
+ hookEmit('session_lifecycle', {
2590
+ phase: 'start', route: 'local-cc', channel_port: Number(PORT) || 18931,
2591
+ channel_reachable: true, open_findings: 0,
2592
+ transcript_path: payload?.transcript_path || undefined,
2593
+ }, opts);
2594
+ return;
2595
+ }
2596
+ if (meta.sessionPhase === 'end') {
2597
+ hookEmit('session_lifecycle', {
2598
+ phase: 'end', route: 'local-cc', channel_port: Number(PORT) || 18931,
2599
+ channel_reachable: true, open_findings: 0,
2600
+ transcript_path: payload?.transcript_path || undefined,
2601
+ }, opts);
2602
+ return;
2603
+ }
2604
+ if (meta.type === 'human_interaction') {
2605
+ const kind = surface === 'prompt-submit' ? 'prompt_submit' : 'consent_consume';
2606
+ const promptText = surface === 'prompt-submit'
2607
+ ? cap(String(payload?.prompt || payload?.message || payload?.content || ''), TE_PROMPT_CAP)
2608
+ : undefined;
2609
+ hookEmit('human_interaction', { kind, prompt_text: promptText }, opts);
2610
+ return;
2611
+ }
2612
+ if (meta.type === 'mcp_tool_use') {
2613
+ const toolName = String(payload?.tool_name || payload?.tool || '');
2614
+ hookEmit('mcp_tool_use', {
2615
+ tool_name: toolName,
2616
+ args_summary: toolInputSummary(toolName, payload?.tool_input, true),
2617
+ duration_ms: durationMs,
2618
+ success: verdict !== 'deny' && verdict !== 'error',
2619
+ }, opts);
2620
+ if (verdict === 'deny') {
2621
+ hookEmit('violation', {
2622
+ hook_type: 'bash', rule_id: '', rule_mode: 'blocking', severity: 'high',
2623
+ category: 'mcp_governance', reason: 'mcp_blocked',
2624
+ rules_checked: [], violated_rules: [],
2625
+ command: cap(scrubSecrets(toolName), TE_CMD_CAP),
2626
+ tool_name: toolName, hook_event: 'PreToolUse', tool_input: toolInputSummary(toolName, payload?.tool_input, false),
2627
+ verdict: 'block', route: 'local', silent: false, duration_ms: durationMs,
2628
+ }, opts);
2629
+ }
2630
+ return;
2631
+ }
2632
+ if (meta.type === 'plan_review_result') {
2633
+ hookEmit('plan_review_result', {
2634
+ verdict: verdict === 'deny' ? 'advisory' : 'clean',
2635
+ reason: '', plan_excerpt: cap(scrubSecrets(String(payload?.tool_input?.plan || '')), TE_EXCERPT_CAP),
2636
+ rule_ids_relevant: [], route: 'local',
2637
+ }, opts);
2638
+ return;
2639
+ }
2640
+ // Default: tool_call. Add a violation row ONLY on a genuine denial ('ask' is a
2641
+ // consent prompt, not a block). tool_input is summarized, never embedded raw.
2642
+ const toolName = String(payload?.tool_name || '');
2643
+ hookEmit('tool_call', {
2644
+ tool_name: toolName,
2645
+ hook_event: surface === 'bash-followup' ? 'PostToolUse' : 'PreToolUse',
2646
+ tool_input: toolInputSummary(toolName, payload?.tool_input, true),
2647
+ verdict, route: 'local', silent: false, duration_ms: durationMs,
2648
+ }, opts);
2649
+ if (verdict === 'deny') {
2650
+ let hookType: 'bash' | 'edit' | 'plan' | 'cve_scan' | 'cwe_scan' = 'bash';
2651
+ if (surface === 'edit-precheck') hookType = 'edit';
2652
+ else if (surface === 'cwe-precheck') hookType = 'cwe_scan';
2653
+ else if (surface === 'cve-precheck') hookType = 'cve_scan';
2654
+ else if (surface === 'plan-judge') hookType = 'plan';
2655
+ hookEmit('violation', {
2656
+ hook_type: hookType,
2657
+ rule_id: '', rule_mode: 'blocking', severity: 'high', category: 'policy',
2658
+ reason: cap('server-denied', TE_REASON_CAP),
2659
+ rules_checked: [], violated_rules: [],
2660
+ command: cap(scrubSecrets(String(payload?.tool_input?.command || filePathFromToolInput(payload?.tool_input) || '')), TE_CMD_CAP),
2661
+ tool_name: toolName, hook_event: 'PreToolUse', tool_input: toolInputSummary(toolName, payload?.tool_input, false),
2662
+ verdict: 'block', route: 'local', silent: false, duration_ms: durationMs,
2663
+ }, opts);
991
2664
  }
992
2665
  }
993
2666
  `;
@@ -1048,13 +2721,13 @@ __export(stub_exports, {
1048
2721
  saveCredentials: () => saveCredentials
1049
2722
  });
1050
2723
  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";
2724
+ import { writeFileSync as writeFileSync9, readFileSync as readFileSync11, existsSync as existsSync13, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4 } from "fs";
2725
+ import { homedir as homedir10, platform as platform2 } from "os";
2726
+ import { join as join9, dirname as dirname4 } from "path";
1054
2727
  import { execFile } from "child_process";
1055
2728
  import jwt from "jsonwebtoken";
1056
2729
  function openBrowser(url) {
1057
- const os = platform();
2730
+ const os = platform2();
1058
2731
  let bin;
1059
2732
  let args2;
1060
2733
  switch (os) {
@@ -1079,17 +2752,17 @@ function openBrowser(url) {
1079
2752
  }
1080
2753
  function saveCredentials(data) {
1081
2754
  const dir = dirname4(AUTH_FILE);
1082
- if (!existsSync5(dir)) {
1083
- mkdirSync4(dir, { recursive: true, mode: 448 });
2755
+ if (!existsSync13(dir)) {
2756
+ mkdirSync7(dir, { recursive: true, mode: 448 });
1084
2757
  }
1085
- writeFileSync4(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
2758
+ writeFileSync9(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
1086
2759
  }
1087
2760
  function loadCredentials() {
1088
- if (!existsSync5(AUTH_FILE)) {
2761
+ if (!existsSync13(AUTH_FILE)) {
1089
2762
  return null;
1090
2763
  }
1091
2764
  try {
1092
- const content = readFileSync4(AUTH_FILE, "utf8");
2765
+ const content = readFileSync11(AUTH_FILE, "utf8");
1093
2766
  return JSON.parse(content);
1094
2767
  } catch (error) {
1095
2768
  return null;
@@ -1223,22 +2896,22 @@ function createCallbackServer() {
1223
2896
  });
1224
2897
  }
1225
2898
  async function authenticate(onStatus) {
1226
- const emit = onStatus || (() => {
2899
+ const emit2 = onStatus || (() => {
1227
2900
  });
1228
2901
  try {
1229
- emit({ phase: "starting" });
2902
+ emit2({ phase: "starting" });
1230
2903
  const serverPromise = createCallbackServer();
1231
2904
  const authUrl = `${SYNKRO_WEB_AUTH_URL}/cli-auth?port=${PORT}`;
1232
2905
  openBrowser(authUrl);
1233
- emit({ phase: "browser-opened", url: authUrl });
1234
- emit({ phase: "waiting" });
2906
+ emit2({ phase: "browser-opened", url: authUrl });
2907
+ emit2({ phase: "waiting" });
1235
2908
  const data = await serverPromise;
1236
- emit({ phase: "success" });
2909
+ emit2({ phase: "success" });
1237
2910
  saveCredentials(data);
1238
2911
  return data;
1239
2912
  } catch (error) {
1240
2913
  const message = error instanceof Error ? error.message : String(error);
1241
- emit({ phase: "error", message });
2914
+ emit2({ phase: "error", message });
1242
2915
  return null;
1243
2916
  }
1244
2917
  }
@@ -1344,8 +3017,8 @@ async function ensureValidToken() {
1344
3017
  return true;
1345
3018
  }
1346
3019
  function clearCredentials() {
1347
- if (existsSync5(AUTH_FILE)) {
1348
- unlinkSync2(AUTH_FILE);
3020
+ if (existsSync13(AUTH_FILE)) {
3021
+ unlinkSync4(AUTH_FILE);
1349
3022
  }
1350
3023
  }
1351
3024
  async function getSecrets(userId, integrationId) {
@@ -1364,7 +3037,7 @@ var init_stub = __esm({
1364
3037
  PORT = 8100;
1365
3038
  RAW_WEB_AUTH_URL = process.env.SYNKRO_WEB_AUTH_URL;
1366
3039
  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");
3040
+ AUTH_FILE = process.env.SYNKRO_AUTH_FILE || join9(homedir10(), ".synkro", "credentials.json");
1368
3041
  RAW_API_URL = process.env.SYNKRO_CRUD_URL || process.env.SYNKRO_API_URL;
1369
3042
  SYNKRO_API_URL = RAW_API_URL && /^https?:\/\//.test(RAW_API_URL) ? RAW_API_URL : "https://api.synkro.sh";
1370
3043
  ERROR_HTML = `
@@ -1524,9 +3197,9 @@ jobs:
1524
3197
  });
1525
3198
 
1526
3199
  // cli/installer/githubSetup.ts
1527
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
3200
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
1528
3201
  import { execSync as execSync2 } from "child_process";
1529
- import { join as join4 } from "path";
3202
+ import { join as join10 } from "path";
1530
3203
  function ghSecretSet(token, owner, repo, name, value) {
1531
3204
  execSync2(`gh secret set ${name} --repo ${owner}/${repo} --body -`, {
1532
3205
  input: value,
@@ -1572,17 +3245,17 @@ async function pushSecretsToRepo(opts, owner, repo, secrets) {
1572
3245
  ghSecretSet(opts.token, owner, repo, "SYNKRO_API_KEY", secrets.synkroApiKey);
1573
3246
  }
1574
3247
  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");
3248
+ const workflowDir = join10(repoRootPath, ".github", "workflows");
3249
+ mkdirSync8(workflowDir, { recursive: true });
3250
+ const workflowFile = join10(workflowDir, "synkro.yml");
3251
+ writeFileSync10(workflowFile, SYNKRO_WORKFLOW_YAML, "utf-8");
1579
3252
  return workflowFile;
1580
3253
  }
1581
3254
  function findGitRoot(startCwd) {
1582
3255
  let cur = startCwd;
1583
3256
  while (cur && cur !== "/") {
1584
- if (existsSync6(join4(cur, ".git"))) return cur;
1585
- const parent = join4(cur, "..");
3257
+ if (existsSync14(join10(cur, ".git"))) return cur;
3258
+ const parent = join10(cur, "..");
1586
3259
  if (parent === cur) break;
1587
3260
  cur = parent;
1588
3261
  }
@@ -1602,7 +3275,7 @@ var init_githubSetup = __esm({
1602
3275
  });
1603
3276
 
1604
3277
  // cli/commands/repoConnect.ts
1605
- import { execSync as execSync3, execFileSync as execFileSync2 } from "child_process";
3278
+ import { execSync as execSync3, execFileSync as execFileSync3 } from "child_process";
1606
3279
  import { readdirSync } from "fs";
1607
3280
  import { createServer as createServer2 } from "http";
1608
3281
  import { createInterface } from "readline";
@@ -1626,7 +3299,7 @@ function detectSubdirRepos() {
1626
3299
  for (const entry of entries) {
1627
3300
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
1628
3301
  try {
1629
- const remoteUrl = execFileSync2("git", ["-C", entry.name, "remote", "get-url", "origin"], {
3302
+ const remoteUrl = execFileSync3("git", ["-C", entry.name, "remote", "get-url", "origin"], {
1630
3303
  encoding: "utf-8",
1631
3304
  timeout: 5e3,
1632
3305
  stdio: ["pipe", "pipe", "pipe"]
@@ -1931,12 +3604,12 @@ __export(claudeDesktopTap_exports, {
1931
3604
  claudeDesktopInstalled: () => claudeDesktopInstalled,
1932
3605
  runClaudeDesktopTap: () => runClaudeDesktopTap
1933
3606
  });
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";
3607
+ import { spawn as spawn2 } from "child_process";
3608
+ import { writeFileSync as writeFileSync11, mkdtempSync, mkdirSync as mkdirSync9, readFileSync as readFileSync12, existsSync as existsSync15 } from "fs";
3609
+ import { join as join11 } from "path";
3610
+ import { homedir as homedir11 } from "os";
1938
3611
  function claudeDesktopInstalled() {
1939
- return process.platform === "darwin" && existsSync7("/Applications/Claude.app/Contents/MacOS/Claude");
3612
+ return process.platform === "darwin" && existsSync15("/Applications/Claude.app/Contents/MacOS/Claude");
1940
3613
  }
1941
3614
  function buildRunner(sessionDir) {
1942
3615
  return `#!/bin/bash
@@ -2050,7 +3723,7 @@ async function runClaudeDesktopTap(opts = {}) {
2050
3723
  }
2051
3724
  let token = "";
2052
3725
  try {
2053
- token = readFileSync5(JWT_PATH, "utf-8").trim();
3726
+ token = readFileSync12(JWT_PATH, "utf-8").trim();
2054
3727
  } catch {
2055
3728
  }
2056
3729
  if (!token) {
@@ -2071,16 +3744,16 @@ async function runClaudeDesktopTap(opts = {}) {
2071
3744
  } catch (err) {
2072
3745
  console.log(` \u26A0 Could not register Claude Desktop MCP: ${err.message}`);
2073
3746
  }
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 });
3747
+ const cdRoot = join11(homedir11(), ".synkro", "cd-sessions");
3748
+ mkdirSync9(cdRoot, { recursive: true });
3749
+ const sessionDir = mkdtempSync(join11(cdRoot, "synkro-cd-"));
3750
+ writeFileSync11(join11(sessionDir, "tap.py"), ADDON_PY, "utf-8");
3751
+ writeFileSync11(join11(sessionDir, "mcp_proxy.py"), MCP_PROXY_PY, { mode: 493 });
3752
+ writeFileSync11(join11(sessionDir, "mcp_patch.py"), MCP_PATCH_PY, "utf-8");
3753
+ const runnerPath = join11(sessionDir, "run.sh");
3754
+ writeFileSync11(runnerPath, buildRunner(sessionDir), { mode: 493 });
2082
3755
  await new Promise((resolve4) => {
2083
- const child = spawn("bash", [runnerPath], {
3756
+ const child = spawn2("bash", [runnerPath], {
2084
3757
  stdio: "inherit",
2085
3758
  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
3759
  });
@@ -2111,7 +3784,7 @@ var init_claudeDesktopTap = __esm({
2111
3784
  TURN_VERDICTS_URL = "http://127.0.0.1:18931/api/local/turn-verdicts";
2112
3785
  TURN_VERDICT_URL = "http://127.0.0.1:18931/api/local/turn-verdict";
2113
3786
  MCP_EVENT_URL = "http://127.0.0.1:18931/api/local/mcp/event";
2114
- JWT_PATH = join5(homedir5(), ".synkro", ".mcp-jwt");
3787
+ JWT_PATH = join11(homedir11(), ".synkro", ".mcp-jwt");
2115
3788
  ADDON_PY = `import os, json, threading, urllib.request, urllib.error, re, base64, time, hashlib
2116
3789
  from mitmproxy import http
2117
3790
 
@@ -3213,7 +4886,7 @@ __export(macKeychain_exports, {
3213
4886
  CURSOR_API_KEY_FILE: () => CURSOR_API_KEY_FILE,
3214
4887
  CURSOR_CREDS_DIR: () => CURSOR_CREDS_DIR,
3215
4888
  KeychainExportError: () => KeychainExportError,
3216
- SYNKRO_DIR: () => SYNKRO_DIR,
4889
+ SYNKRO_DIR: () => SYNKRO_DIR6,
3217
4890
  credsAreStale: () => credsAreStale,
3218
4891
  cursorApiKeyConfigured: () => cursorApiKeyConfigured,
3219
4892
  exportKeychainCreds: () => exportKeychainCreds,
@@ -3227,15 +4900,15 @@ __export(macKeychain_exports, {
3227
4900
  writeCursorApiKey: () => writeCursorApiKey,
3228
4901
  writeRefreshAgent: () => writeRefreshAgent
3229
4902
  });
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";
4903
+ import { existsSync as existsSync16, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12, chmodSync as chmodSync2, readFileSync as readFileSync13 } from "fs";
4904
+ import { homedir as homedir12, platform as platform3 } from "os";
4905
+ import { join as join12 } from "path";
3233
4906
  import { spawnSync as spawnSync2 } from "child_process";
3234
4907
  function needsKeychainBridge() {
3235
- return platform2() === "darwin";
4908
+ return platform3() === "darwin";
3236
4909
  }
3237
4910
  function readKeychainCreds() {
3238
- if (platform2() !== "darwin") return null;
4911
+ if (platform3() !== "darwin") return null;
3239
4912
  const r = spawnSync2("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-w"], {
3240
4913
  encoding: "utf-8",
3241
4914
  timeout: 5e3
@@ -3247,22 +4920,22 @@ function readKeychainCreds() {
3247
4920
  function exportKeychainCreds() {
3248
4921
  const blob = readKeychainCreds();
3249
4922
  if (!blob) return null;
3250
- mkdirSync7(CLAUDE_CREDS_DIR, { recursive: true });
3251
- chmodSync(CLAUDE_CREDS_DIR, 448);
4923
+ mkdirSync10(CLAUDE_CREDS_DIR, { recursive: true });
4924
+ chmodSync2(CLAUDE_CREDS_DIR, 448);
3252
4925
  let changed = true;
3253
4926
  try {
3254
- if (existsSync8(CLAUDE_CREDS_FILE)) {
3255
- changed = readFileSync6(CLAUDE_CREDS_FILE, "utf-8") !== blob;
4927
+ if (existsSync16(CLAUDE_CREDS_FILE)) {
4928
+ changed = readFileSync13(CLAUDE_CREDS_FILE, "utf-8") !== blob;
3256
4929
  }
3257
4930
  } catch {
3258
4931
  }
3259
- if (changed) writeFileSync7(CLAUDE_CREDS_FILE, blob, "utf-8");
3260
- chmodSync(CLAUDE_CREDS_FILE, 384);
4932
+ if (changed) writeFileSync12(CLAUDE_CREDS_FILE, blob, "utf-8");
4933
+ chmodSync2(CLAUDE_CREDS_FILE, 384);
3261
4934
  return CLAUDE_CREDS_FILE;
3262
4935
  }
3263
4936
  function cursorApiKeyConfigured() {
3264
4937
  try {
3265
- return existsSync8(CURSOR_API_KEY_FILE) && readFileSync6(CURSOR_API_KEY_FILE, "utf-8").trim().length > 0;
4938
+ return existsSync16(CURSOR_API_KEY_FILE) && readFileSync13(CURSOR_API_KEY_FILE, "utf-8").trim().length > 0;
3266
4939
  } catch {
3267
4940
  return false;
3268
4941
  }
@@ -3270,15 +4943,15 @@ function cursorApiKeyConfigured() {
3270
4943
  function writeCursorApiKey(key) {
3271
4944
  const trimmed = key.trim();
3272
4945
  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);
4946
+ mkdirSync10(CURSOR_CREDS_DIR, { recursive: true });
4947
+ chmodSync2(CURSOR_CREDS_DIR, 448);
4948
+ writeFileSync12(CURSOR_API_KEY_FILE, trimmed, "utf-8");
4949
+ chmodSync2(CURSOR_API_KEY_FILE, 384);
3277
4950
  }
3278
4951
  async function validateCursorApiKey() {
3279
4952
  let key;
3280
4953
  try {
3281
- key = readFileSync6(CURSOR_API_KEY_FILE, "utf-8").trim();
4954
+ key = readFileSync13(CURSOR_API_KEY_FILE, "utf-8").trim();
3282
4955
  } catch {
3283
4956
  return null;
3284
4957
  }
@@ -3297,9 +4970,9 @@ async function validateCursorApiKey() {
3297
4970
  }
3298
4971
  }
3299
4972
  function credsAreStale() {
3300
- if (!existsSync8(CLAUDE_CREDS_FILE)) return true;
4973
+ if (!existsSync16(CLAUDE_CREDS_FILE)) return true;
3301
4974
  try {
3302
- const raw = readFileSync6(CLAUDE_CREDS_FILE, "utf-8");
4975
+ const raw = readFileSync13(CLAUDE_CREDS_FILE, "utf-8");
3303
4976
  const exp = JSON.parse(raw)?.claudeAiOauth?.expiresAt ?? 0;
3304
4977
  if (!exp) return true;
3305
4978
  return Date.now() >= exp - REFRESH_EXPIRY_BUFFER_SECONDS * 1e3;
@@ -3311,11 +4984,11 @@ function xmlEscape(s) {
3311
4984
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
3312
4985
  }
3313
4986
  function writeRefreshAgent(synkroBinPath) {
3314
- if (platform2() !== "darwin") {
4987
+ if (platform3() !== "darwin") {
3315
4988
  throw new KeychainExportError("writeRefreshAgent is darwin-only");
3316
4989
  }
3317
- mkdirSync7(join6(homedir6(), "Library", "LaunchAgents"), { recursive: true });
3318
- mkdirSync7(SYNKRO_DIR, { recursive: true });
4990
+ mkdirSync10(join12(homedir12(), "Library", "LaunchAgents"), { recursive: true });
4991
+ mkdirSync10(SYNKRO_DIR6, { recursive: true });
3319
4992
  const script = `#!/bin/bash
3320
4993
  # Generated by synkro (writeRefreshAgent). Expiry-aware Claude creds refresher.
3321
4994
  # SYNKRO_BIN / SYNKRO_CF are injected by launchd as environment data \u2014 never
@@ -3340,8 +5013,8 @@ while true; do
3340
5013
  sleep "$delay"
3341
5014
  done
3342
5015
  `;
3343
- writeFileSync7(REFRESH_SCRIPT, script, "utf-8");
3344
- chmodSync(REFRESH_SCRIPT, 448);
5016
+ writeFileSync12(REFRESH_SCRIPT, script, "utf-8");
5017
+ chmodSync2(REFRESH_SCRIPT, 448);
3345
5018
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
3346
5019
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3347
5020
  <plist version="1.0">
@@ -3373,11 +5046,11 @@ done
3373
5046
  </dict>
3374
5047
  </plist>
3375
5048
  `;
3376
- writeFileSync7(LAUNCHD_PLIST, plist, "utf-8");
5049
+ writeFileSync12(LAUNCHD_PLIST, plist, "utf-8");
3377
5050
  return LAUNCHD_PLIST;
3378
5051
  }
3379
5052
  function loadRefreshAgent() {
3380
- if (platform2() !== "darwin") return;
5053
+ if (platform3() !== "darwin") return;
3381
5054
  spawnSync2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, LAUNCHD_PLIST], {
3382
5055
  encoding: "utf-8",
3383
5056
  timeout: 5e3
@@ -3393,15 +5066,15 @@ function loadRefreshAgent() {
3393
5066
  }
3394
5067
  }
3395
5068
  function uninstallRefreshAgent() {
3396
- if (platform2() !== "darwin") return;
5069
+ if (platform3() !== "darwin") return;
3397
5070
  spawnSync2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, LAUNCHD_PLIST], {
3398
5071
  encoding: "utf-8",
3399
5072
  timeout: 5e3
3400
5073
  });
3401
5074
  try {
3402
5075
  const fs = __require("fs");
3403
- if (existsSync8(LAUNCHD_PLIST)) fs.unlinkSync(LAUNCHD_PLIST);
3404
- if (existsSync8(REFRESH_SCRIPT)) fs.unlinkSync(REFRESH_SCRIPT);
5076
+ if (existsSync16(LAUNCHD_PLIST)) fs.unlinkSync(LAUNCHD_PLIST);
5077
+ if (existsSync16(REFRESH_SCRIPT)) fs.unlinkSync(REFRESH_SCRIPT);
3405
5078
  } catch {
3406
5079
  }
3407
5080
  }
@@ -3411,25 +5084,25 @@ function refreshCreds() {
3411
5084
  }
3412
5085
  function readExportedCreds() {
3413
5086
  try {
3414
- return readFileSync6(CLAUDE_CREDS_FILE, "utf-8");
5087
+ return readFileSync13(CLAUDE_CREDS_FILE, "utf-8");
3415
5088
  } catch {
3416
5089
  return null;
3417
5090
  }
3418
5091
  }
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;
5092
+ 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
5093
  var init_macKeychain = __esm({
3421
5094
  "cli/local-cc/macKeychain.ts"() {
3422
5095
  "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");
5096
+ SYNKRO_DIR6 = join12(homedir12(), ".synkro");
5097
+ CLAUDE_CREDS_DIR = join12(SYNKRO_DIR6, "claude-creds");
5098
+ CLAUDE_CREDS_FILE = join12(CLAUDE_CREDS_DIR, ".credentials.json");
5099
+ CURSOR_CREDS_DIR = join12(SYNKRO_DIR6, "cursor-creds");
5100
+ CURSOR_API_KEY_FILE = join12(CURSOR_CREDS_DIR, "api-key");
3428
5101
  KEYCHAIN_SERVICE = "Claude Code-credentials";
3429
5102
  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");
5103
+ LAUNCHD_PLIST = join12(homedir12(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
5104
+ REFRESH_SCRIPT = join12(SYNKRO_DIR6, "claude-creds-refresh-loop.sh");
5105
+ REFRESH_LOG = join12(SYNKRO_DIR6, "claude-creds-refresh.log");
3433
5106
  REFRESH_EXPIRY_BUFFER_SECONDS = 120;
3434
5107
  MIN_REFRESH_INTERVAL_SECONDS = 30;
3435
5108
  MAX_REFRESH_INTERVAL_SECONDS = 5 * 60;
@@ -3448,7 +5121,7 @@ var init_macKeychain = __esm({
3448
5121
  var dockerInstall_exports = {};
3449
5122
  __export(dockerInstall_exports, {
3450
5123
  DockerInstallError: () => DockerInstallError,
3451
- SYNKRO_DIR: () => SYNKRO_DIR2,
5124
+ SYNKRO_DIR: () => SYNKRO_DIR7,
3452
5125
  assertDockerAvailable: () => assertDockerAvailable,
3453
5126
  dockerInstall: () => dockerInstall,
3454
5127
  dockerRemove: () => dockerRemove,
@@ -3468,9 +5141,9 @@ __export(dockerInstall_exports, {
3468
5141
  waitForContainerReady: () => waitForContainerReady,
3469
5142
  waitForWorkersReady: () => waitForWorkersReady
3470
5143
  });
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";
5144
+ import { copyFileSync, existsSync as existsSync17, mkdirSync as mkdirSync11, readFileSync as readFileSync14, readdirSync as readdirSync2 } from "fs";
5145
+ import { homedir as homedir13 } from "os";
5146
+ import { join as join13 } from "path";
3474
5147
  import { execSync as execSync4, spawnSync as spawnSync3 } from "child_process";
3475
5148
  function splitWorkers(total, providers) {
3476
5149
  const t = Math.max(0, Math.floor(total));
@@ -3565,9 +5238,9 @@ function readSynkroFileConfig() {
3565
5238
  try {
3566
5239
  const root = execSync4("git rev-parse --show-toplevel 2>/dev/null", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
3567
5240
  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"));
5241
+ const fp = join13(root, "synkro.toml");
5242
+ if (!existsSync17(fp)) return { pool: "auto", warnings: [] };
5243
+ const parsed = parseSynkroToml(readFileSync14(fp, "utf-8"));
3571
5244
  return resolveGraderPool(parsed);
3572
5245
  } catch {
3573
5246
  }
@@ -3642,7 +5315,7 @@ function assertDockerAvailable() {
3642
5315
  }
3643
5316
  function claudeCredsHostDir() {
3644
5317
  if (needsKeychainBridge()) return CLAUDE_CREDS_DIR;
3645
- return join7(homedir7(), ".claude");
5318
+ return join13(homedir13(), ".claude");
3646
5319
  }
3647
5320
  function resolveSynkroBin() {
3648
5321
  const which2 = spawnSync3("which", ["synkro"], { encoding: "utf-8", timeout: 5e3 });
@@ -3695,19 +5368,19 @@ async function dockerInstall(opts = {}) {
3695
5368
  const claudeWorkers = opts.claudeWorkers ?? opts.workersPerPool ?? 8;
3696
5369
  const cursorWorkers = opts.cursorWorkers ?? 0;
3697
5370
  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)) {
5371
+ mkdirSync11(PGDATA_PATH, { recursive: true });
5372
+ mkdirSync11(BACKUP_DIR, { recursive: true });
5373
+ mkdirSync11(CLAUDE_HOST_STATE_DIR, { recursive: true });
5374
+ const hostClaudeJson = join13(homedir13(), ".claude.json");
5375
+ if (existsSync17(hostClaudeJson)) {
3703
5376
  copyFileSync(hostClaudeJson, CLAUDE_HOST_STATE_FILE);
3704
5377
  }
3705
- if (!existsSync9(MCP_JWT_PATH)) {
5378
+ if (!existsSync17(MCP_JWT_PATH)) {
3706
5379
  throw new DockerInstallError(
3707
5380
  `MCP JWT missing at ${MCP_JWT_PATH}. The installer should mint this before calling dockerInstall.`
3708
5381
  );
3709
5382
  }
3710
- mkdirSync8(CURSOR_CREDS_DIR, { recursive: true });
5383
+ mkdirSync11(CURSOR_CREDS_DIR, { recursive: true });
3711
5384
  if (needsKeychainBridge()) {
3712
5385
  const claudeCredsPath = exportKeychainCreds();
3713
5386
  if (!claudeCredsPath) {
@@ -3731,7 +5404,7 @@ async function dockerInstall(opts = {}) {
3731
5404
  console.warn(` Plist written to ${plist} \u2014 load manually with launchctl bootstrap when ready.`);
3732
5405
  }
3733
5406
  } else {
3734
- mkdirSync8(join7(homedir7(), ".claude"), { recursive: true });
5407
+ mkdirSync11(join13(homedir13(), ".claude"), { recursive: true });
3735
5408
  }
3736
5409
  const imageExistsLocally = () => spawnSync3("docker", ["image", "inspect", image], { stdio: "ignore", timeout: 3e4 }).status === 0;
3737
5410
  const skipPull = process.env.SYNKRO_SKIP_PULL === "1" || process.env.SYNKRO_SKIP_PULL === "true";
@@ -3780,11 +5453,11 @@ async function dockerInstall(opts = {}) {
3780
5453
  // sidesteps Docker Desktop for macOS's unreliable single-file bind mounts
3781
5454
  // (which previously left a dangling symlink that blocked container start).
3782
5455
  "-v",
3783
- `${SYNKRO_DIR2}:/data/synkro-host:ro`,
5456
+ `${SYNKRO_DIR7}:/data/synkro-host:ro`,
3784
5457
  "-v",
3785
5458
  `${credsDir}:/home/synkro/.claude:rw`,
3786
5459
  "-v",
3787
- `${join7(homedir7(), ".claude")}:/data/claude-host:ro`,
5460
+ `${join13(homedir13(), ".claude")}:/data/claude-host:ro`,
3788
5461
  "-v",
3789
5462
  `${CLAUDE_HOST_STATE_DIR}:/data/claude-host-state:ro`,
3790
5463
  // Cursor creds — mounted RW so the in-container refresher can rotate the
@@ -3814,6 +5487,16 @@ async function dockerInstall(opts = {}) {
3814
5487
  ...process.env.SYNKRO_ORG_ID ? ["-e", `SYNKRO_ORG_ID=${process.env.SYNKRO_ORG_ID}`] : [],
3815
5488
  ...process.env.SYNKRO_USER_ID ? ["-e", `SYNKRO_USER_ID=${process.env.SYNKRO_USER_ID}`] : [],
3816
5489
  ...process.env.SYNKRO_EMAIL ? ["-e", `SYNKRO_EMAIL=${process.env.SYNKRO_EMAIL}`] : [],
5490
+ // Container-owned cloud telemetry flush (docker/telemetryCloudFlush.ts). The
5491
+ // server drains the READ-ONLY host queue (already mounted at
5492
+ // /data/synkro-host via the RO mount above — NO new mount) into its pglite
5493
+ // and pushes unflushed rows to the D1 telemetry worker. The loop is gated on
5494
+ // SYNKRO_TELEMETRY_URL and is harmless when there's nothing to flush, so we
5495
+ // always set it (defaulting to the D1 worker). Override via env to redirect.
5496
+ "-e",
5497
+ `SYNKRO_TELEMETRY_URL=${process.env.SYNKRO_TELEMETRY_URL || "https://admin.synkro.sh"}`,
5498
+ "-e",
5499
+ "SYNKRO_TELEMETRY_QUEUE=/data/synkro-host/telemetry-pending.jsonl",
3817
5500
  image
3818
5501
  ];
3819
5502
  const run = spawnSync3("docker", args2, { encoding: "utf-8", stdio: "inherit", timeout: 6e4 });
@@ -3969,7 +5652,7 @@ async function dockerSafeStart() {
3969
5652
  return { ok: false, pgdataState: "no_container", error: "No synkro-server container found. Run `synkro install` first." };
3970
5653
  }
3971
5654
  const pgCheck = checkPgdata();
3972
- if (existsSync9(PGDATA_PATH) && readdirSync2(PGDATA_PATH).length > 0) {
5655
+ if (existsSync17(PGDATA_PATH) && readdirSync2(PGDATA_PATH).length > 0) {
3973
5656
  if (pgCheck.healthy) {
3974
5657
  console.log(` pgdata: existing data found \u2014 ${pgCheck.details}`);
3975
5658
  } else {
@@ -3978,7 +5661,7 @@ async function dockerSafeStart() {
3978
5661
  }
3979
5662
  } else {
3980
5663
  console.log(" pgdata: no existing data \u2014 fresh start.");
3981
- mkdirSync8(PGDATA_PATH, { recursive: true });
5664
+ mkdirSync11(PGDATA_PATH, { recursive: true });
3982
5665
  }
3983
5666
  console.log(" Starting container...");
3984
5667
  const start = spawnSync3("docker", ["start", CONTAINER_NAME], {
@@ -4009,7 +5692,7 @@ async function dockerSafeRestart() {
4009
5692
  return { ok: startResult.ok, stop: stopResult, start: startResult };
4010
5693
  }
4011
5694
  function checkPgdata() {
4012
- if (!existsSync9(PGDATA_PATH)) return { healthy: false, details: "pgdata directory does not exist" };
5695
+ if (!existsSync17(PGDATA_PATH)) return { healthy: false, details: "pgdata directory does not exist" };
4013
5696
  const entries = readdirSync2(PGDATA_PATH);
4014
5697
  if (entries.length === 0) return { healthy: true, details: "empty (fresh start)" };
4015
5698
  const hasPidFile = entries.includes("postmaster.pid");
@@ -4020,17 +5703,17 @@ function checkPgdata() {
4020
5703
  if (!hasPgControl) return { healthy: false, details: "pg_control/global directory missing" };
4021
5704
  return { healthy: true, details: `${entries.length} entries, WAL present, no stale PID` };
4022
5705
  }
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;
5706
+ 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
5707
  var init_dockerInstall = __esm({
4025
5708
  "cli/local-cc/dockerInstall.ts"() {
4026
5709
  "use strict";
4027
5710
  init_agentDetect();
4028
5711
  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");
5712
+ SYNKRO_DIR7 = join13(homedir13(), ".synkro");
5713
+ MCP_JWT_PATH = join13(SYNKRO_DIR7, ".mcp-jwt");
5714
+ PGDATA_PATH = join13(SYNKRO_DIR7, "pgdata");
5715
+ CLAUDE_HOST_STATE_DIR = join13(SYNKRO_DIR7, "claude-host-state");
5716
+ CLAUDE_HOST_STATE_FILE = join13(CLAUDE_HOST_STATE_DIR, ".claude.json");
4034
5717
  HOST_MCP_PORT = parseInt(process.env.SYNKRO_HOST_MCP_PORT || "18931", 10);
4035
5718
  HOST_GRADER_PORT = parseInt(process.env.SYNKRO_HOST_GRADER_PORT || "18929", 10);
4036
5719
  HOST_CWE_PORT = parseInt(process.env.SYNKRO_HOST_CWE_PORT || "18930", 10);
@@ -4045,18 +5728,18 @@ var init_dockerInstall = __esm({
4045
5728
  }
4046
5729
  cause;
4047
5730
  };
4048
- BACKUP_DIR = join7(SYNKRO_DIR2, "pgdata-backups");
5731
+ BACKUP_DIR = join13(SYNKRO_DIR7, "pgdata-backups");
4049
5732
  }
4050
5733
  });
4051
5734
 
4052
5735
  // cli/local-cc/setupToken.ts
4053
5736
  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";
5737
+ import { readFileSync as readFileSync15, unlinkSync as unlinkSync5 } from "fs";
5738
+ import { homedir as homedir14, platform as platform4 } from "os";
5739
+ import { join as join14 } from "path";
4057
5740
  function captureClaudeSetupToken() {
4058
- const tmpFile = join8(SYNKRO_DIR3, `token-capture-${Date.now()}.raw`);
4059
- const isMac = platform3() === "darwin";
5741
+ const tmpFile = join14(SYNKRO_DIR8, `token-capture-${Date.now()}.raw`);
5742
+ const isMac = platform4() === "darwin";
4060
5743
  const bin = "script";
4061
5744
  const args2 = isMac ? ["-q", tmpFile, "claude", "setup-token"] : ["-qec", "claude setup-token", tmpFile];
4062
5745
  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 +5752,13 @@ function captureClaudeSetupToken() {
4069
5752
  proc.on("close", (code) => {
4070
5753
  let raw = "";
4071
5754
  try {
4072
- raw = readFileSync8(tmpFile, "utf-8");
5755
+ raw = readFileSync15(tmpFile, "utf-8");
4073
5756
  } catch (e) {
4074
5757
  reject(new Error(`Could not read script output file: ${e.message}`));
4075
5758
  return;
4076
5759
  }
4077
5760
  try {
4078
- unlinkSync3(tmpFile);
5761
+ unlinkSync5(tmpFile);
4079
5762
  } catch {
4080
5763
  }
4081
5764
  if (code !== 0) {
@@ -4131,11 +5814,11 @@ function captureClaudeSetupToken() {
4131
5814
  });
4132
5815
  });
4133
5816
  }
4134
- var SYNKRO_DIR3;
5817
+ var SYNKRO_DIR8;
4135
5818
  var init_setupToken = __esm({
4136
5819
  "cli/local-cc/setupToken.ts"() {
4137
5820
  "use strict";
4138
- SYNKRO_DIR3 = join8(homedir8(), ".synkro");
5821
+ SYNKRO_DIR8 = join14(homedir14(), ".synkro");
4139
5822
  }
4140
5823
  });
4141
5824
 
@@ -4145,9 +5828,9 @@ __export(codexCloudSetup_exports, {
4145
5828
  setupCodexCloud: () => setupCodexCloud
4146
5829
  });
4147
5830
  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";
5831
+ import { readFileSync as readFileSync16, mkdirSync as mkdirSync12, rmSync, existsSync as existsSync18 } from "fs";
5832
+ import { homedir as homedir15 } from "os";
5833
+ import { join as join15 } from "path";
4151
5834
  function findCodexBinary() {
4152
5835
  if (process.env.SYNKRO_CODEX_BIN) return process.env.SYNKRO_CODEX_BIN;
4153
5836
  const r = spawnSync4("which", ["codex"], { encoding: "utf-8" });
@@ -4155,7 +5838,7 @@ function findCodexBinary() {
4155
5838
  return p || null;
4156
5839
  }
4157
5840
  function runCodexLogin(codexBin) {
4158
- mkdirSync9(CODEX_CLOUD_HOME, { recursive: true, mode: 448 });
5841
+ mkdirSync12(CODEX_CLOUD_HOME, { recursive: true, mode: 448 });
4159
5842
  return new Promise((resolve4, reject) => {
4160
5843
  const proc = nodeSpawn2(codexBin, ["login"], {
4161
5844
  stdio: "inherit",
@@ -4176,13 +5859,13 @@ async function setupCodexCloud(gatewayUrl, bearerToken, onStatus) {
4176
5859
  } catch (e) {
4177
5860
  return { ok: false, error: `Codex login failed: ${e.message}` };
4178
5861
  }
4179
- const authPath = join9(CODEX_CLOUD_HOME, "auth.json");
4180
- if (!existsSync10(authPath)) {
5862
+ const authPath = join15(CODEX_CLOUD_HOME, "auth.json");
5863
+ if (!existsSync18(authPath)) {
4181
5864
  return { ok: false, error: "codex login completed but no auth.json was written \u2014 did the browser approval finish?" };
4182
5865
  }
4183
5866
  let auth;
4184
5867
  try {
4185
- auth = JSON.parse(readFileSync9(authPath, "utf-8"));
5868
+ auth = JSON.parse(readFileSync16(authPath, "utf-8"));
4186
5869
  } catch (e) {
4187
5870
  return { ok: false, error: `could not read codex auth.json: ${e.message}` };
4188
5871
  }
@@ -4211,12 +5894,12 @@ async function setupCodexCloud(gatewayUrl, bearerToken, onStatus) {
4211
5894
  onStatus?.("\u2713 Codex subscription connected to Synkro Cloud.");
4212
5895
  return { ok: true };
4213
5896
  }
4214
- var SYNKRO_DIR4, CODEX_CLOUD_HOME;
5897
+ var SYNKRO_DIR9, CODEX_CLOUD_HOME;
4215
5898
  var init_codexCloudSetup = __esm({
4216
5899
  "cli/local-cc/codexCloudSetup.ts"() {
4217
5900
  "use strict";
4218
- SYNKRO_DIR4 = join9(homedir9(), ".synkro");
4219
- CODEX_CLOUD_HOME = join9(SYNKRO_DIR4, "codex-cloud-session");
5901
+ SYNKRO_DIR9 = join15(homedir15(), ".synkro");
5902
+ CODEX_CLOUD_HOME = join15(SYNKRO_DIR9, "codex-cloud-session");
4220
5903
  }
4221
5904
  });
4222
5905
 
@@ -4229,14 +5912,14 @@ __export(setupGithub_exports, {
4229
5912
  import { createInterface as createInterface2 } from "readline/promises";
4230
5913
  import { stdin as input, stdout as output } from "process";
4231
5914
  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";
5915
+ import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
5916
+ import { homedir as homedir16, platform as platform5 } from "os";
5917
+ import { join as join16 } from "path";
4235
5918
  import { execFile as execFile2 } from "child_process";
4236
5919
  function readConfig() {
4237
- if (!existsSync11(CONFIG_PATH)) return {};
5920
+ if (!existsSync19(CONFIG_PATH3)) return {};
4238
5921
  const out = {};
4239
- for (const line of readFileSync10(CONFIG_PATH, "utf-8").split("\n")) {
5922
+ for (const line of readFileSync17(CONFIG_PATH3, "utf-8").split("\n")) {
4240
5923
  const t = line.trim();
4241
5924
  if (!t || t.startsWith("#")) continue;
4242
5925
  const eq = t.indexOf("=");
@@ -4273,7 +5956,7 @@ async function prompt(rl, q, opts = {}) {
4273
5956
  return await rl.question(q);
4274
5957
  }
4275
5958
  function openBrowser3(url) {
4276
- const os = platform4();
5959
+ const os = platform5();
4277
5960
  let bin;
4278
5961
  let args2;
4279
5962
  switch (os) {
@@ -4536,15 +6219,15 @@ Will push secrets to ${selected.length} repo(s):`);
4536
6219
  console.log(`Secrets pushed: ${SECRET_NAMES.CLAUDE_OAUTH}, ${SECRET_NAMES.SYNKRO_API_KEY}`);
4537
6220
  console.log("Open a PR on any selected repo to trigger your first Synkro scan.");
4538
6221
  }
4539
- var SYNKRO_DIR5, CONFIG_PATH;
6222
+ var SYNKRO_DIR10, CONFIG_PATH3;
4540
6223
  var init_setupGithub = __esm({
4541
6224
  "cli/commands/setupGithub.ts"() {
4542
6225
  "use strict";
4543
6226
  init_githubSetup();
4544
6227
  init_stub();
4545
6228
  init_setupToken();
4546
- SYNKRO_DIR5 = join10(homedir10(), ".synkro");
4547
- CONFIG_PATH = join10(SYNKRO_DIR5, "config.env");
6229
+ SYNKRO_DIR10 = join16(homedir16(), ".synkro");
6230
+ CONFIG_PATH3 = join16(SYNKRO_DIR10, "config.env");
4548
6231
  }
4549
6232
  });
4550
6233
 
@@ -4563,12 +6246,12 @@ __export(install_exports, {
4563
6246
  syncSkillFiles: () => syncSkillFiles,
4564
6247
  writeHookScripts: () => writeHookScripts
4565
6248
  });
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";
6249
+ import { existsSync as existsSync20, mkdirSync as mkdirSync13, writeFileSync as writeFileSync13, chmodSync as chmodSync3, readFileSync as readFileSync18, readdirSync as readdirSync3, unlinkSync as unlinkSync6, statSync as statSync2 } from "fs";
6250
+ import { homedir as homedir17 } from "os";
6251
+ import { join as join17 } from "path";
4569
6252
  import { execSync as execSync6 } from "child_process";
4570
6253
  import { createInterface as createInterface3 } from "readline";
4571
- import { createHash } from "crypto";
6254
+ import { createHash as createHash2 } from "crypto";
4572
6255
  function resolvePersistedHookMode() {
4573
6256
  return "stub";
4574
6257
  }
@@ -4710,37 +6393,37 @@ async function promptCodexCloudSetup() {
4710
6393
  });
4711
6394
  }
4712
6395
  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 });
6396
+ mkdirSync13(SYNKRO_DIR11, { recursive: true });
6397
+ mkdirSync13(HOOKS_DIR, { recursive: true });
6398
+ mkdirSync13(BIN_DIR, { recursive: true });
6399
+ mkdirSync13(OFFSETS_DIR, { recursive: true });
6400
+ mkdirSync13(join17(SYNKRO_DIR11, "sessions"), { recursive: true });
4718
6401
  }
4719
6402
  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");
6403
+ const installExtractCorePath = join17(HOOKS_DIR, "installExtractCore.ts");
6404
+ const bashScriptPath = join17(HOOKS_DIR, "cc-bash-judge.ts");
6405
+ const skillJudgeScriptPath = join17(HOOKS_DIR, "cc-skill-judge.ts");
6406
+ const cursorSkillJudgePath = join17(HOOKS_DIR, "cursor-skill-judge.ts");
6407
+ const bashFollowupScriptPath = join17(HOOKS_DIR, "cc-bash-followup.ts");
6408
+ const editPrecheckScriptPath = join17(HOOKS_DIR, "cc-edit-precheck.ts");
6409
+ const cwePrecheckScriptPath = join17(HOOKS_DIR, "cc-cwe-precheck.ts");
6410
+ const cvePrecheckScriptPath = join17(HOOKS_DIR, "cc-cve-precheck.ts");
6411
+ const planJudgeScriptPath = join17(HOOKS_DIR, "cc-plan-judge.ts");
6412
+ const agentJudgeScriptPath = join17(HOOKS_DIR, "cc-agent-judge.ts");
6413
+ const stopSummaryScriptPath = join17(HOOKS_DIR, "cc-stop-summary.ts");
6414
+ const sessionStartScriptPath = join17(HOOKS_DIR, "cc-session-start.ts");
6415
+ const transcriptSyncScriptPath = join17(HOOKS_DIR, "cc-transcript-sync.ts");
6416
+ const userPromptSubmitScriptPath = join17(HOOKS_DIR, "cc-user-prompt-submit.ts");
6417
+ const commonScriptPath = join17(HOOKS_DIR, "_synkro-common.ts");
6418
+ const commonBashScriptPath = join17(HOOKS_DIR, "_synkro-common.sh");
6419
+ const installScanScriptPath = join17(HOOKS_DIR, "cc-install-scan.ts");
6420
+ const cursorBashJudgePath = join17(HOOKS_DIR, "cursor-bash-judge.ts");
6421
+ const cursorEditCapturePath = join17(HOOKS_DIR, "cursor-edit-capture.ts");
6422
+ const cursorAgentCapturePath = join17(HOOKS_DIR, "cursor-agent-capture.ts");
6423
+ const mcpStdioProxyPath = join17(HOOKS_DIR, "mcp-stdio-proxy.ts");
6424
+ const taskActivateIntentScriptPath = join17(HOOKS_DIR, "cc-task-activate-intent.ts");
6425
+ const mcpGateScriptPath = join17(HOOKS_DIR, "cc-mcp-gate.ts");
6426
+ const stubCommonPath = join17(HOOKS_DIR, "_synkro-stub-common.ts");
4744
6427
  const stubFiles = [
4745
6428
  [stubCommonPath, STUB_COMMON_TS],
4746
6429
  [bashScriptPath, STUB_BASH_JUDGE_TS],
@@ -4764,14 +6447,14 @@ function writeHookScripts() {
4764
6447
  [cursorAgentCapturePath, STUB_CURSOR_AGENT_CAPTURE_TS]
4765
6448
  ];
4766
6449
  for (const [p, content] of stubFiles) {
4767
- writeFileSync8(p, content, "utf-8");
4768
- chmodSync2(p, 493);
6450
+ writeFileSync13(p, content, "utf-8");
6451
+ chmodSync3(p, 493);
4769
6452
  }
4770
- writeFileSync8(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
4771
- chmodSync2(mcpStdioProxyPath, 493);
6453
+ writeFileSync13(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
6454
+ chmodSync3(mcpStdioProxyPath, 493);
4772
6455
  for (const stale of ["_synkro-common.ts", "_synkro-common.sh", "installExtractCore.ts"]) {
4773
6456
  try {
4774
- unlinkSync4(join11(HOOKS_DIR, stale));
6457
+ unlinkSync6(join17(HOOKS_DIR, stale));
4775
6458
  } catch {
4776
6459
  }
4777
6460
  }
@@ -4801,16 +6484,16 @@ function sanitizeConfigValue(raw, maxLen = 256) {
4801
6484
  if (!raw) return "";
4802
6485
  return raw.replace(/[^\x20-\x7E]/g, "").slice(0, maxLen);
4803
6486
  }
4804
- function shellQuoteSingle(value) {
6487
+ function shellQuoteSingle2(value) {
4805
6488
  return `'${value.replace(/'/g, "'\\''")}'`;
4806
6489
  }
4807
6490
  function resolveSynkroBundle() {
4808
6491
  const scriptPath = process.argv[1];
4809
- if (scriptPath && existsSync12(scriptPath)) return scriptPath;
6492
+ if (scriptPath && existsSync20(scriptPath)) return scriptPath;
4810
6493
  return null;
4811
6494
  }
4812
6495
  function writeConfigEnv(opts) {
4813
- const credsPath = join11(SYNKRO_DIR6, "credentials.json");
6496
+ const credsPath = join17(SYNKRO_DIR11, "credentials.json");
4814
6497
  const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
4815
6498
  const safeUserId = sanitizeConfigValue(opts.userId);
4816
6499
  const safeOrgId = sanitizeConfigValue(opts.orgId);
@@ -4822,29 +6505,29 @@ function writeConfigEnv(opts) {
4822
6505
  "# Synkro CLI config (managed by synkro install)",
4823
6506
  "# JWT auth \u2014 the hook scripts read SYNKRO_CREDENTIALS_PATH at runtime",
4824
6507
  "# 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.60")}`
6508
+ `SYNKRO_GATEWAY_URL=${shellQuoteSingle2(safeGateway)}`,
6509
+ `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle2(credsPath)}`,
6510
+ `SYNKRO_TIER=${shellQuoteSingle2(safeTier)}`,
6511
+ `SYNKRO_INFERENCE=${shellQuoteSingle2(safeInference)}`,
6512
+ `SYNKRO_VERSION=${shellQuoteSingle2("1.7.62")}`
4830
6513
  ];
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)}`);
6514
+ if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle2(safeSynkroBin)}`);
6515
+ if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle2(safeUserId)}`);
6516
+ if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle2(safeOrgId)}`);
6517
+ if (safeEmail) lines.push(`SYNKRO_EMAIL=${shellQuoteSingle2(safeEmail)}`);
4835
6518
  if (opts.transcriptConsent !== void 0) {
4836
- lines.push(`SYNKRO_TRANSCRIPT_CONSENT=${shellQuoteSingle(opts.transcriptConsent ? "yes" : "no")}`);
6519
+ lines.push(`SYNKRO_TRANSCRIPT_CONSENT=${shellQuoteSingle2(opts.transcriptConsent ? "yes" : "no")}`);
4837
6520
  }
4838
- lines.push(`SYNKRO_LOCAL_INFERENCE=${shellQuoteSingle(opts.localInference ? "yes" : "no")}`);
6521
+ lines.push(`SYNKRO_LOCAL_INFERENCE=${shellQuoteSingle2(opts.localInference ? "yes" : "no")}`);
4839
6522
  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))}`);
6523
+ lines.push(`SYNKRO_DEPLOYMENT_MODE=${shellQuoteSingle2(safeMode)}`);
6524
+ lines.push(`SYNKRO_GRADING_MODE=${shellQuoteSingle2(sanitizeConfigValue(opts.gradingMode ?? "local", 16))}`);
6525
+ lines.push(`SYNKRO_STORAGE_MODE=${shellQuoteSingle2(sanitizeConfigValue(opts.storageMode ?? "local", 16))}`);
6526
+ lines.push(`SYNKRO_DEPLOY_LOCATION=${shellQuoteSingle2(sanitizeConfigValue(opts.deployLocation ?? "local", 16))}`);
6527
+ lines.push(`SYNKRO_HOOK_MODE=${shellQuoteSingle2(sanitizeConfigValue(opts.hookMode ?? "stub", 8))}`);
4845
6528
  lines.push("");
4846
- writeFileSync8(CONFIG_PATH2, lines.join("\n"), "utf-8");
4847
- chmodSync2(CONFIG_PATH2, 384);
6529
+ writeFileSync13(CONFIG_PATH4, lines.join("\n"), "utf-8");
6530
+ chmodSync3(CONFIG_PATH4, 384);
4848
6531
  }
4849
6532
  function jwtExpired(token) {
4850
6533
  try {
@@ -4859,7 +6542,7 @@ async function getOrMintCloudToken(gatewayUrl) {
4859
6542
  assertGatewayAllowed(gatewayUrl);
4860
6543
  let stored = "";
4861
6544
  try {
4862
- stored = readFileSync11(CLOUD_JWT_PATH, "utf-8").trim();
6545
+ stored = readFileSync18(CLOUD_JWT_PATH, "utf-8").trim();
4863
6546
  } catch {
4864
6547
  }
4865
6548
  if (stored && !jwtExpired(stored)) return stored;
@@ -4875,7 +6558,7 @@ async function getOrMintCloudToken(gatewayUrl) {
4875
6558
  throw new Error(`cloud-token mint failed (${resp.status}): ${t.slice(0, 200)}`);
4876
6559
  }
4877
6560
  const { token } = await resp.json();
4878
- writeFileSync8(CLOUD_JWT_PATH, token + "\n", { mode: 384 });
6561
+ writeFileSync13(CLOUD_JWT_PATH, token + "\n", { mode: 384 });
4879
6562
  return token;
4880
6563
  }
4881
6564
  async function provisionCloudContainer(opts) {
@@ -4918,7 +6601,7 @@ async function provisionCloudContainer(opts) {
4918
6601
  let cursorApiKey = "";
4919
6602
  if (cursorTotal > 0) {
4920
6603
  try {
4921
- cursorApiKey = readFileSync11(join11(SYNKRO_DIR6, "cursor-creds", "api-key"), "utf-8").trim();
6604
+ cursorApiKey = readFileSync18(join17(SYNKRO_DIR11, "cursor-creds", "api-key"), "utf-8").trim();
4922
6605
  } catch {
4923
6606
  }
4924
6607
  }
@@ -5075,8 +6758,8 @@ async function verifyCloudGrader(jwt2) {
5075
6758
  }
5076
6759
  function readPersistedDeployLocation() {
5077
6760
  try {
5078
- if (existsSync12(CONFIG_PATH2)) {
5079
- const m = readFileSync11(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOY_LOCATION='([^']*)'/m);
6761
+ if (existsSync20(CONFIG_PATH4)) {
6762
+ const m = readFileSync18(CONFIG_PATH4, "utf-8").match(/^SYNKRO_DEPLOY_LOCATION='([^']*)'/m);
5080
6763
  if (m?.[1] === "cloud") return "cloud";
5081
6764
  }
5082
6765
  } catch {
@@ -5084,8 +6767,8 @@ function readPersistedDeployLocation() {
5084
6767
  return "local";
5085
6768
  }
5086
6769
  function updateConfigEnvLocation(location) {
5087
- if (!existsSync12(CONFIG_PATH2)) return;
5088
- let env = readFileSync11(CONFIG_PATH2, "utf-8");
6770
+ if (!existsSync20(CONFIG_PATH4)) return;
6771
+ let env = readFileSync18(CONFIG_PATH4, "utf-8");
5089
6772
  const set = (k, v) => {
5090
6773
  const re = new RegExp(`^${k}=.*$`, "m");
5091
6774
  const line = `${k}='${v}'`;
@@ -5093,14 +6776,14 @@ function updateConfigEnvLocation(location) {
5093
6776
  };
5094
6777
  set("SYNKRO_DEPLOY_LOCATION", location);
5095
6778
  set("SYNKRO_STORAGE_MODE", location === "cloud" ? "cloud" : "local");
5096
- writeFileSync8(CONFIG_PATH2, env, "utf-8");
5097
- chmodSync2(CONFIG_PATH2, 384);
6779
+ writeFileSync13(CONFIG_PATH4, env, "utf-8");
6780
+ chmodSync3(CONFIG_PATH4, 384);
5098
6781
  }
5099
6782
  async function applyMcpConfig(opts) {
5100
6783
  if (!opts.hasClaudeCode && !opts.hasCursor) return;
5101
6784
  let mcpJwt = "";
5102
6785
  try {
5103
- mcpJwt = readFileSync11(join11(SYNKRO_DIR6, ".mcp-jwt"), "utf-8").trim();
6786
+ mcpJwt = readFileSync18(join17(SYNKRO_DIR11, ".mcp-jwt"), "utf-8").trim();
5104
6787
  } catch {
5105
6788
  }
5106
6789
  if (!mcpJwt) {
@@ -5218,8 +6901,8 @@ function resolveDeploymentMode() {
5218
6901
  const envOverride = process.env.SYNKRO_DEPLOYMENT_MODE?.toLowerCase();
5219
6902
  if (envOverride === "bare-host" || envOverride === "docker") return envOverride;
5220
6903
  try {
5221
- if (existsSync12(CONFIG_PATH2)) {
5222
- const m = readFileSync11(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
6904
+ if (existsSync20(CONFIG_PATH4)) {
6905
+ const m = readFileSync18(CONFIG_PATH4, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
5223
6906
  const val = m?.[1]?.toLowerCase();
5224
6907
  if (val === "bare-host" || val === "docker") return val;
5225
6908
  }
@@ -5246,16 +6929,16 @@ function collectLocalMetadata(includeClaudeCode = true) {
5246
6929
  meta.cc_version = execSync6("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
5247
6930
  } catch {
5248
6931
  }
5249
- const claudeDir = join11(homedir11(), ".claude");
6932
+ const claudeDir = join17(homedir17(), ".claude");
5250
6933
  try {
5251
- const settings = JSON.parse(readFileSync11(join11(claudeDir, "settings.json"), "utf-8"));
6934
+ const settings = JSON.parse(readFileSync18(join17(claudeDir, "settings.json"), "utf-8"));
5252
6935
  const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
5253
6936
  if (plugins.length) meta.enabled_plugins = plugins;
5254
6937
  if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
5255
6938
  } catch {
5256
6939
  }
5257
6940
  try {
5258
- const mcpCache = JSON.parse(readFileSync11(join11(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
6941
+ const mcpCache = JSON.parse(readFileSync18(join17(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
5259
6942
  const mcpNames = Object.keys(mcpCache);
5260
6943
  if (mcpNames.length) meta.mcp_servers = mcpNames;
5261
6944
  } catch {
@@ -5267,10 +6950,10 @@ function collectLocalMetadata(includeClaudeCode = true) {
5267
6950
  } catch {
5268
6951
  }
5269
6952
  try {
5270
- const sessionsDir = join11(claudeDir, "sessions");
6953
+ const sessionsDir = join17(claudeDir, "sessions");
5271
6954
  const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
5272
6955
  for (const f of files) {
5273
- const s = JSON.parse(readFileSync11(join11(sessionsDir, f), "utf-8"));
6956
+ const s = JSON.parse(readFileSync18(join17(sessionsDir, f), "utf-8"));
5274
6957
  if (s.version) {
5275
6958
  meta.cc_version = meta.cc_version || s.version;
5276
6959
  break;
@@ -5316,15 +6999,30 @@ function assertGatewayAllowed(gatewayUrl) {
5316
6999
  if (proto !== "http:" && proto !== "https:") {
5317
7000
  throw new Error(`Gateway URL must be http(s); got ${proto}`);
5318
7001
  }
5319
- const isLocalhost = host === "localhost" || host === "127.0.0.1" || host === "::1";
7002
+ const isLocalhost2 = host === "localhost" || host === "127.0.0.1" || host === "::1";
5320
7003
  const isSynkro = host === "synkro.sh" || host.endsWith(".synkro.sh");
5321
- if (proto === "http:" && !isLocalhost) {
7004
+ if (proto === "http:" && !isLocalhost2) {
5322
7005
  throw new Error(`Gateway URL must be HTTPS for non-localhost hosts; got ${gatewayUrl}`);
5323
7006
  }
5324
- if (!isLocalhost && !isSynkro) {
7007
+ if (!isLocalhost2 && !isSynkro) {
5325
7008
  throw new Error(`Gateway host not in allowlist (synkro.sh or *.synkro.sh): ${host}`);
5326
7009
  }
5327
7010
  }
7011
+ async function promptTelemetryConsent() {
7012
+ if (!process.stdin.isTTY) return true;
7013
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
7014
+ try {
7015
+ const a = await new Promise(
7016
+ (res) => rl.question(
7017
+ "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) ",
7018
+ (ans) => res(ans.trim().toLowerCase())
7019
+ )
7020
+ );
7021
+ return a === "" || a === "y" || a === "yes";
7022
+ } finally {
7023
+ rl.close();
7024
+ }
7025
+ }
5328
7026
  async function installCommand(opts = {}) {
5329
7027
  if (!detectGitRepo2()) {
5330
7028
  console.error("Synkro must be installed inside a git repository.");
@@ -5447,14 +7145,25 @@ async function installCommand(opts = {}) {
5447
7145
  }
5448
7146
  }
5449
7147
  const transcriptConsent = transcriptCC || transcriptCursor;
7148
+ const telemetryConsent = await promptTelemetryConsent();
7149
+ await setTelemetryState({ enabled: true, remoteFlushEnabled: telemetryConsent });
7150
+ emit("install", {
7151
+ phase: "started",
7152
+ cli_version_to: "1.7.62",
7153
+ agents_detected: agents.map((a) => a.kind),
7154
+ with_github: false,
7155
+ with_local_cc: false,
7156
+ with_transcript_consent: transcriptConsent,
7157
+ duration_ms: 0
7158
+ });
5450
7159
  ensureSynkroDir();
5451
7160
  const hookMode = opts.hookMode || resolvePersistedHookMode();
5452
7161
  const scripts = writeHookScripts();
5453
7162
  console.log("Wrote hook scripts to ~/.synkro/hooks/\n");
5454
7163
  for (const mode of ["edit", "bash"]) {
5455
- const pidFile = join11(SYNKRO_DIR6, "daemon", mode, "daemon.pid");
7164
+ const pidFile = join17(SYNKRO_DIR11, "daemon", mode, "daemon.pid");
5456
7165
  try {
5457
- const pid = parseInt(readFileSync11(pidFile, "utf-8").trim(), 10);
7166
+ const pid = parseInt(readFileSync18(pidFile, "utf-8").trim(), 10);
5458
7167
  if (pid > 0) {
5459
7168
  process.kill(pid, "SIGTERM");
5460
7169
  console.log(`Stopped stale ${mode} grader daemon (pid ${pid})`);
@@ -5539,7 +7248,7 @@ async function installCommand(opts = {}) {
5539
7248
  if (mintResp.ok) {
5540
7249
  const minted = await mintResp.json();
5541
7250
  mcpJwt = minted.token;
5542
- writeFileSync8(join11(SYNKRO_DIR6, ".mcp-jwt"), mcpJwt + "\n", { mode: 384 });
7251
+ writeFileSync13(join17(SYNKRO_DIR11, ".mcp-jwt"), mcpJwt + "\n", { mode: 384 });
5543
7252
  } else {
5544
7253
  console.warn(" \u26A0 Could not mint MCP token \u2014 local server will reject requests until re-installed.");
5545
7254
  }
@@ -5567,7 +7276,7 @@ async function installCommand(opts = {}) {
5567
7276
  throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
5568
7277
  }
5569
7278
  const minted = await mintResp.json();
5570
- writeFileSync8(join11(SYNKRO_DIR6, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
7279
+ writeFileSync13(join17(SYNKRO_DIR11, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
5571
7280
  const mcp = installMcpConfig({ gatewayUrl, bearerToken: minted.token });
5572
7281
  console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
5573
7282
  console.log(` url: ${mcp.url}`);
@@ -5584,8 +7293,8 @@ async function installCommand(opts = {}) {
5584
7293
  if (hasCursor && !opts.noMcp) {
5585
7294
  try {
5586
7295
  if (useLocalMcp) {
5587
- const jwtPath = join11(SYNKRO_DIR6, ".mcp-jwt");
5588
- if (!existsSync12(jwtPath)) {
7296
+ const jwtPath = join17(SYNKRO_DIR11, ".mcp-jwt");
7297
+ if (!existsSync20(jwtPath)) {
5589
7298
  const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
5590
7299
  method: "POST",
5591
7300
  headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
@@ -5593,7 +7302,7 @@ async function installCommand(opts = {}) {
5593
7302
  });
5594
7303
  if (mintResp.ok) {
5595
7304
  const minted = await mintResp.json();
5596
- writeFileSync8(jwtPath, minted.token + "\n", { mode: 384 });
7305
+ writeFileSync13(jwtPath, minted.token + "\n", { mode: 384 });
5597
7306
  }
5598
7307
  }
5599
7308
  const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: "", local: true });
@@ -5613,7 +7322,7 @@ async function installCommand(opts = {}) {
5613
7322
  throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
5614
7323
  }
5615
7324
  const minted = await mintResp.json();
5616
- writeFileSync8(join11(SYNKRO_DIR6, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
7325
+ writeFileSync13(join17(SYNKRO_DIR11, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
5617
7326
  const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: minted.token });
5618
7327
  console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
5619
7328
  console.log(` url: ${mcp.url}`);
@@ -5627,7 +7336,7 @@ async function installCommand(opts = {}) {
5627
7336
  const synkroBundle = resolveSynkroBundle();
5628
7337
  const persistedMode = resolveDeploymentMode();
5629
7338
  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}`);
7339
+ console.log(`Wrote config to ${CONFIG_PATH4}`);
5631
7340
  console.log(` inference: ${profile.inference} (server-side grading)`);
5632
7341
  if (profile.localInference) console.log(` local inference: enabled (gradingProvider=claude-code)`);
5633
7342
  if (synkroBundle) console.log(` SYNKRO_CLI_BIN=${synkroBundle}`);
@@ -5706,7 +7415,7 @@ async function installCommand(opts = {}) {
5706
7415
  const ready = await waitForContainerReady(6e4);
5707
7416
  if (ready) {
5708
7417
  console.log(" \u2713 container ready");
5709
- const mcpJwt = readFileSync11(join11(SYNKRO_DIR6, ".mcp-jwt"), "utf-8").trim();
7418
+ const mcpJwt = readFileSync18(join17(SYNKRO_DIR11, ".mcp-jwt"), "utf-8").trim();
5710
7419
  try {
5711
7420
  const ingestResp = await fetch(`http://127.0.0.1:${hostMcpPort}/api/ingest`, {
5712
7421
  method: "POST",
@@ -5754,7 +7463,7 @@ async function installCommand(opts = {}) {
5754
7463
  try {
5755
7464
  let mcpToken = "";
5756
7465
  try {
5757
- mcpToken = readFileSync11(join11(SYNKRO_DIR6, ".mcp-jwt"), "utf-8").trim();
7466
+ mcpToken = readFileSync18(join17(SYNKRO_DIR11, ".mcp-jwt"), "utf-8").trim();
5758
7467
  } catch {
5759
7468
  }
5760
7469
  if (mcpToken) {
@@ -5890,11 +7599,11 @@ function writeSynkroFileIfMissing(opts) {
5890
7599
  try {
5891
7600
  const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
5892
7601
  if (!root) return;
5893
- if (root === homedir11()) return;
5894
- const fp = join11(root, "synkro.toml");
7602
+ if (root === homedir17()) return;
7603
+ const fp = join17(root, "synkro.toml");
5895
7604
  let hasFile = false;
5896
7605
  try {
5897
- hasFile = statSync(fp).isFile();
7606
+ hasFile = statSync2(fp).isFile();
5898
7607
  } catch {
5899
7608
  }
5900
7609
  if (hasFile) {
@@ -5927,7 +7636,7 @@ function writeSynkroFileIfMissing(opts) {
5927
7636
  "cve = true",
5928
7637
  ""
5929
7638
  ].join("\n");
5930
- writeFileSync8(fp, toml, "utf-8");
7639
+ writeFileSync13(fp, toml, "utf-8");
5931
7640
  console.log(` synkro.toml: wrote ${fp} (pool=${pool}, mode=${mode})`);
5932
7641
  } catch {
5933
7642
  }
@@ -5936,9 +7645,9 @@ function readFullSynkroFile() {
5936
7645
  try {
5937
7646
  const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
5938
7647
  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"));
7648
+ const fp = join17(root, "synkro.toml");
7649
+ if (!existsSync20(fp)) return null;
7650
+ const parsed = parseSynkroToml2(readFileSync18(fp, "utf-8"));
5942
7651
  const valid = ["claude-code", "cursor"];
5943
7652
  const harness = Array.isArray(parsed.harness) ? parsed.harness.filter((h) => valid.includes(h)) : ["claude-code", "cursor"];
5944
7653
  const resolved = resolveGraderPool(parsed);
@@ -5976,7 +7685,7 @@ function reconcileHarness() {
5976
7685
  console.log(`synkro.toml: harness=[${sf.harness.join(", ")}] pool=${poolLabel(sf.grader.pool)} mode=${sf.grader.mode}`);
5977
7686
  const scripts = writeHookScripts();
5978
7687
  console.log("Wrote hook scripts to ~/.synkro/hooks/");
5979
- const ccSettings = join11(homedir11(), ".claude", "settings.json");
7688
+ const ccSettings = join17(homedir17(), ".claude", "settings.json");
5980
7689
  if (wantCC) {
5981
7690
  installCCHooks(ccSettings, {
5982
7691
  bashJudgeScriptPath: scripts.bashScript,
@@ -5997,7 +7706,7 @@ function reconcileHarness() {
5997
7706
  });
5998
7707
  console.log(" \u2713 Claude Code hooks registered");
5999
7708
  try {
6000
- const mcpJwt = readFileSync11(join11(SYNKRO_DIR6, ".mcp-jwt"), "utf-8").trim();
7709
+ const mcpJwt = readFileSync18(join17(SYNKRO_DIR11, ".mcp-jwt"), "utf-8").trim();
6001
7710
  if (mcpJwt) {
6002
7711
  installMcpConfig({ gatewayUrl: "", bearerToken: mcpJwt, local: true });
6003
7712
  console.log(" \u2713 Claude Code MCP registered");
@@ -6009,7 +7718,7 @@ function reconcileHarness() {
6009
7718
  if (uninstallMcpConfig()) console.log(" \u2717 Claude Code MCP removed");
6010
7719
  if (uninstallClaudeDesktopMcpConfig()) console.log(" \u2717 Claude Desktop MCP removed");
6011
7720
  }
6012
- const cursorHooks = join11(homedir11(), ".cursor", "hooks.json");
7721
+ const cursorHooks = join17(homedir17(), ".cursor", "hooks.json");
6013
7722
  if (wantCursor) {
6014
7723
  installCursorHooks(cursorHooks, {
6015
7724
  bashJudgeScriptPath: scripts.cursorBashJudgeScript,
@@ -6092,7 +7801,7 @@ async function syncSkillFiles() {
6092
7801
  if (resolved.length === 0) return;
6093
7802
  const mcpPort = process.env.SYNKRO_MCP_PORT || "18931";
6094
7803
  const tasks = resolved.map((fp) => {
6095
- const content = readFileSync11(fp, "utf-8");
7804
+ const content = readFileSync18(fp, "utf-8");
6096
7805
  const source = `skill:${fp.split("/").pop()}`;
6097
7806
  if (!content.trim()) {
6098
7807
  console.log(` \u2298 skill ${source}: empty file, skipped`);
@@ -6113,15 +7822,15 @@ function discoverSkillFiles(repoRoot, excludeHashes, ingestedHashes, ingestedNam
6113
7822
  const roots = [];
6114
7823
  const add = (p) => {
6115
7824
  try {
6116
- if (p && existsSync12(p) && statSync(p).isDirectory()) roots.push(p);
7825
+ if (p && existsSync20(p) && statSync2(p).isDirectory()) roots.push(p);
6117
7826
  } catch {
6118
7827
  }
6119
7828
  };
6120
- add(join11(homedir11(), ".claude", "skills"));
6121
- add(join11(homedir11(), ".agents", "skills"));
7829
+ add(join17(homedir17(), ".claude", "skills"));
7830
+ add(join17(homedir17(), ".agents", "skills"));
6122
7831
  if (repoRoot) {
6123
- add(join11(repoRoot, ".claude", "skills"));
6124
- add(join11(repoRoot, ".agents", "skills"));
7832
+ add(join17(repoRoot, ".claude", "skills"));
7833
+ add(join17(repoRoot, ".agents", "skills"));
6125
7834
  }
6126
7835
  const out = [];
6127
7836
  const seen = /* @__PURE__ */ new Set();
@@ -6130,14 +7839,14 @@ function discoverSkillFiles(repoRoot, excludeHashes, ingestedHashes, ingestedNam
6130
7839
  if (out.length >= MAX) return;
6131
7840
  let content = "";
6132
7841
  try {
6133
- const st = statSync(file);
7842
+ const st = statSync2(file);
6134
7843
  if (!st.isFile() || st.size > 2e5) return;
6135
- content = readFileSync11(file, "utf-8");
7844
+ content = readFileSync18(file, "utf-8");
6136
7845
  } catch {
6137
7846
  return;
6138
7847
  }
6139
7848
  if (!content.trim()) return;
6140
- const hash = createHash("sha256").update(content).digest("hex");
7849
+ const hash = createHash2("sha256").update(content).digest("hex");
6141
7850
  if (seen.has(hash) || excludeHashes.has(hash)) return;
6142
7851
  seen.add(hash);
6143
7852
  const ingested = ingestedHashes.has(hash) || ingestedNames.has(normSkillName(name));
@@ -6152,16 +7861,16 @@ function discoverSkillFiles(repoRoot, excludeHashes, ingestedHashes, ingestedNam
6152
7861
  }
6153
7862
  for (const entry of entries) {
6154
7863
  if (entry.startsWith(".")) continue;
6155
- const full = join11(root, entry);
7864
+ const full = join17(root, entry);
6156
7865
  let st;
6157
7866
  try {
6158
- st = statSync(full);
7867
+ st = statSync2(full);
6159
7868
  } catch {
6160
7869
  continue;
6161
7870
  }
6162
7871
  if (st.isDirectory()) {
6163
- const skillMd = join11(full, "SKILL.md");
6164
- if (existsSync12(skillMd)) consider(skillMd, entry);
7872
+ const skillMd = join17(full, "SKILL.md");
7873
+ if (existsSync20(skillMd)) consider(skillMd, entry);
6165
7874
  } else if (/\.mdx?$/i.test(entry) && entry.toUpperCase() !== "README.MD") {
6166
7875
  consider(full, entry.replace(/\.mdx?$/i, ""));
6167
7876
  }
@@ -6170,7 +7879,7 @@ function discoverSkillFiles(repoRoot, excludeHashes, ingestedHashes, ingestedNam
6170
7879
  return out;
6171
7880
  }
6172
7881
  function discoverySetHash(found) {
6173
- return createHash("sha256").update(found.map((f) => f.hash).sort().join(",")).digest("hex");
7882
+ return createHash2("sha256").update(found.map((f) => f.hash).sort().join(",")).digest("hex");
6174
7883
  }
6175
7884
  async function promptSkillDiscovery(found) {
6176
7885
  if (!process.stdin.isTTY || found.length === 0) return [];
@@ -6210,7 +7919,7 @@ async function discoverAndIngestSkills() {
6210
7919
  if (sf?.skills?.length) {
6211
7920
  for (const fp of resolveSkillPaths(sf.skills, sf._repoRoot)) {
6212
7921
  try {
6213
- excludeHashes.add(createHash("sha256").update(readFileSync11(fp, "utf-8")).digest("hex"));
7922
+ excludeHashes.add(createHash2("sha256").update(readFileSync18(fp, "utf-8")).digest("hex"));
6214
7923
  } catch {
6215
7924
  }
6216
7925
  }
@@ -6241,13 +7950,13 @@ async function discoverAndIngestSkills() {
6241
7950
  const setHash = discoverySetHash(selectable);
6242
7951
  let prev = "";
6243
7952
  try {
6244
- prev = readFileSync11(SKILLS_DISCOVERED_PATH, "utf-8").trim();
7953
+ prev = readFileSync18(SKILLS_DISCOVERED_PATH, "utf-8").trim();
6245
7954
  } catch {
6246
7955
  }
6247
7956
  if (prev === setHash) return;
6248
7957
  const picks = await promptSkillDiscovery(found);
6249
7958
  try {
6250
- writeFileSync8(SKILLS_DISCOVERED_PATH, setHash);
7959
+ writeFileSync13(SKILLS_DISCOVERED_PATH, setHash);
6251
7960
  } catch {
6252
7961
  }
6253
7962
  if (picks.length === 0) {
@@ -6281,17 +7990,17 @@ function detectGitRepo2() {
6281
7990
  function getClaudeProjectsFolder() {
6282
7991
  const cwd = process.cwd();
6283
7992
  const sanitized = "-" + cwd.replace(/\//g, "-");
6284
- const projectsDir = join11(homedir11(), ".claude", "projects", sanitized);
6285
- return existsSync12(projectsDir) ? projectsDir : null;
7993
+ const projectsDir = join17(homedir17(), ".claude", "projects", sanitized);
7994
+ return existsSync20(projectsDir) ? projectsDir : null;
6286
7995
  }
6287
7996
  function extractSessionInsights(projectsDir) {
6288
7997
  const insights = [];
6289
7998
  const files = readdirSync3(projectsDir).filter((f) => f.endsWith(".jsonl"));
6290
7999
  for (const file of files) {
6291
8000
  const sessionId = file.replace(".jsonl", "");
6292
- const filePath = join11(projectsDir, file);
8001
+ const filePath = join17(projectsDir, file);
6293
8002
  try {
6294
- const content = readFileSync11(filePath, "utf-8");
8003
+ const content = readFileSync18(filePath, "utf-8");
6295
8004
  const lines = content.split("\n").filter(Boolean);
6296
8005
  for (let i = 0; i < lines.length; i++) {
6297
8006
  try {
@@ -6370,14 +8079,14 @@ function cursorProjectSlug(workspaceRoot) {
6370
8079
  return workspaceRoot.replace(/^[/]+/, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
6371
8080
  }
6372
8081
  function getCursorTranscriptsDir() {
6373
- const dir = join11(homedir11(), ".cursor", "projects", cursorProjectSlug(process.cwd()), "agent-transcripts");
6374
- return existsSync12(dir) ? dir : null;
8082
+ const dir = join17(homedir17(), ".cursor", "projects", cursorProjectSlug(process.cwd()), "agent-transcripts");
8083
+ return existsSync20(dir) ? dir : null;
6375
8084
  }
6376
8085
  function isSafeConvId(id) {
6377
8086
  return /^[A-Za-z0-9_-]+$/.test(id);
6378
8087
  }
6379
8088
  function parseCursorTranscriptFile(filePath) {
6380
- const content = readFileSync11(filePath, "utf-8");
8089
+ const content = readFileSync18(filePath, "utf-8");
6381
8090
  const lines = content.split("\n").filter(Boolean);
6382
8091
  const messages = [];
6383
8092
  for (let i = 0; i < lines.length; i++) {
@@ -6409,8 +8118,8 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
6409
8118
  for (let i = 0; i < convDirs.length; i++) {
6410
8119
  const convId = convDirs[i];
6411
8120
  if (!isSafeConvId(convId)) continue;
6412
- const filePath = join11(dir, convId, `${convId}.jsonl`);
6413
- if (!existsSync12(filePath)) continue;
8121
+ const filePath = join17(dir, convId, `${convId}.jsonl`);
8122
+ if (!existsSync20(filePath)) continue;
6414
8123
  try {
6415
8124
  const all = parseCursorTranscriptFile(filePath);
6416
8125
  const messages = all.length > 500 ? all.slice(-500) : all;
@@ -6432,8 +8141,8 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
6432
8141
  process.stdout.write(`\r Progress: ${i + 1}/${convDirs.length} sessions (${totalMessages} messages embedded) `);
6433
8142
  }
6434
8143
  try {
6435
- const lc = readFileSync11(filePath, "utf-8").split("\n").filter(Boolean).length;
6436
- writeFileSync8(join11(OFFSETS_DIR, convId), String(lc), "utf-8");
8144
+ const lc = readFileSync18(filePath, "utf-8").split("\n").filter(Boolean).length;
8145
+ writeFileSync13(join17(OFFSETS_DIR, convId), String(lc), "utf-8");
6437
8146
  } catch {
6438
8147
  }
6439
8148
  }
@@ -6441,7 +8150,7 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
6441
8150
  return { sessions: totalSessions, messages: totalMessages };
6442
8151
  }
6443
8152
  function parseTranscriptFile(filePath) {
6444
- const content = readFileSync11(filePath, "utf-8");
8153
+ const content = readFileSync18(filePath, "utf-8");
6445
8154
  const lines = content.split("\n").filter(Boolean);
6446
8155
  const messages = [];
6447
8156
  for (let i = 0; i < lines.length; i++) {
@@ -6489,7 +8198,7 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
6489
8198
  for (let i = 0; i < files.length; i++) {
6490
8199
  const file = files[i];
6491
8200
  const sessionId = file.replace(".jsonl", "");
6492
- const filePath = join11(projectsDir, file);
8201
+ const filePath = join17(projectsDir, file);
6493
8202
  try {
6494
8203
  const allMessages = parseTranscriptFile(filePath);
6495
8204
  const messages = allMessages.length > 500 ? allMessages.slice(-500) : allMessages;
@@ -6511,9 +8220,9 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
6511
8220
  process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
6512
8221
  }
6513
8222
  try {
6514
- const content = readFileSync11(join11(projectsDir, file), "utf-8");
8223
+ const content = readFileSync18(join17(projectsDir, file), "utf-8");
6515
8224
  const lineCount = content.split("\n").filter(Boolean).length;
6516
- writeFileSync8(join11(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
8225
+ writeFileSync13(join17(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
6517
8226
  } catch {
6518
8227
  }
6519
8228
  }
@@ -6534,7 +8243,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
6534
8243
  const sessions = [];
6535
8244
  for (const file of batch) {
6536
8245
  const sessionId = file.replace(".jsonl", "");
6537
- const filePath = join11(projectsDir, file);
8246
+ const filePath = join17(projectsDir, file);
6538
8247
  try {
6539
8248
  const allMessages = parseTranscriptFile(filePath);
6540
8249
  const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
@@ -6563,18 +8272,18 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
6563
8272
  }
6564
8273
  for (const file of batch) {
6565
8274
  const sessionId = file.replace(".jsonl", "");
6566
- const filePath = join11(projectsDir, file);
8275
+ const filePath = join17(projectsDir, file);
6567
8276
  try {
6568
- const content = readFileSync11(filePath, "utf-8");
8277
+ const content = readFileSync18(filePath, "utf-8");
6569
8278
  const lineCount = content.split("\n").filter(Boolean).length;
6570
- writeFileSync8(join11(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
8279
+ writeFileSync13(join17(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
6571
8280
  } catch {
6572
8281
  }
6573
8282
  }
6574
8283
  }
6575
8284
  return { sessions: totalSessions, messages: totalMessages };
6576
8285
  }
6577
- var SYNKRO_DIR6, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, MCP_STDIO_PROXY_SRC, OFFSETS_DIR, CLOUD_JWT_PATH, SKILLS_DISCOVERED_PATH;
8286
+ var SYNKRO_DIR11, HOOKS_DIR, BIN_DIR, CONFIG_PATH4, MCP_STDIO_PROXY_SRC, OFFSETS_DIR, CLOUD_JWT_PATH, SKILLS_DISCOVERED_PATH;
6578
8287
  var init_install = __esm({
6579
8288
  "cli/commands/install.ts"() {
6580
8289
  "use strict";
@@ -6583,6 +8292,7 @@ var init_install = __esm({
6583
8292
  init_cursorHookConfig();
6584
8293
  init_mcpConfig();
6585
8294
  init_skillParser();
8295
+ init_telemetry();
6586
8296
  init_hookScriptsTs();
6587
8297
  init_stub();
6588
8298
  init_repoConnect();
@@ -6591,10 +8301,10 @@ var init_install = __esm({
6591
8301
  init_claudeDesktopTap();
6592
8302
  init_dockerInstall();
6593
8303
  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");
8304
+ SYNKRO_DIR11 = join17(homedir17(), ".synkro");
8305
+ HOOKS_DIR = join17(SYNKRO_DIR11, "hooks");
8306
+ BIN_DIR = join17(SYNKRO_DIR11, "bin");
8307
+ CONFIG_PATH4 = join17(SYNKRO_DIR11, "config.env");
6598
8308
  MCP_STDIO_PROXY_SRC = `#!/usr/bin/env bun
6599
8309
  import { readFileSync } from 'node:fs';
6600
8310
  import { homedir } from 'node:os';
@@ -6705,23 +8415,23 @@ rl.on('line', async (line) => {
6705
8415
  }
6706
8416
  });
6707
8417
  `;
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");
8418
+ OFFSETS_DIR = join17(SYNKRO_DIR11, ".transcript-offsets");
8419
+ CLOUD_JWT_PATH = join17(SYNKRO_DIR11, ".cloud-jwt");
8420
+ SKILLS_DISCOVERED_PATH = join17(SYNKRO_DIR11, ".skills-discovered");
6711
8421
  }
6712
8422
  });
6713
8423
 
6714
8424
  // 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";
8425
+ import { existsSync as existsSync21, 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";
8426
+ import { join as join18 } from "path";
8427
+ import { homedir as homedir18 } from "os";
6718
8428
  import { spawnSync as spawnSync6 } from "child_process";
6719
8429
  function writePluginFiles() {
6720
8430
  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(
8431
+ mkdirSync14(c.sessionDir, { recursive: true });
8432
+ mkdirSync14(c.pluginSettingsDir, { recursive: true });
8433
+ writeFileSync14(c.pluginPkgPath, PLUGIN_PACKAGE_JSON, "utf-8");
8434
+ writeFileSync14(
6725
8435
  c.pluginSettingsPath,
6726
8436
  JSON.stringify({
6727
8437
  fastMode: true,
@@ -6736,8 +8446,8 @@ function writePluginFiles() {
6736
8446
  }, null, 2) + "\n",
6737
8447
  "utf-8"
6738
8448
  );
6739
- writeFileSync9(c.runScriptPath, c.runScriptSource, "utf-8");
6740
- chmodSync3(c.runScriptPath, 493);
8449
+ writeFileSync14(c.runScriptPath, c.runScriptSource, "utf-8");
8450
+ chmodSync4(c.runScriptPath, 493);
6741
8451
  }
6742
8452
  }
6743
8453
  function runBunInstall() {
@@ -6755,10 +8465,10 @@ function runBunInstall() {
6755
8465
  }
6756
8466
  }
6757
8467
  function safelyMutateClaudeJson(mutator) {
6758
- if (!existsSync13(CLAUDE_JSON_PATH)) {
8468
+ if (!existsSync21(CLAUDE_JSON_PATH)) {
6759
8469
  return;
6760
8470
  }
6761
- const originalText = readFileSync12(CLAUDE_JSON_PATH, "utf-8");
8471
+ const originalText = readFileSync19(CLAUDE_JSON_PATH, "utf-8");
6762
8472
  let parsed;
6763
8473
  try {
6764
8474
  parsed = JSON.parse(originalText);
@@ -6790,17 +8500,17 @@ function safelyMutateClaudeJson(mutator) {
6790
8500
  copyFileSync2(CLAUDE_JSON_PATH, CLAUDE_JSON_BACKUP_PATH);
6791
8501
  const tmpPath = `${CLAUDE_JSON_PATH}.synkro-tmp.${process.pid}`;
6792
8502
  try {
6793
- writeFileSync9(tmpPath, newText, "utf-8");
6794
- const fd = openSync(tmpPath, "r");
8503
+ writeFileSync14(tmpPath, newText, "utf-8");
8504
+ const fd = openSync2(tmpPath, "r");
6795
8505
  try {
6796
8506
  fsyncSync(fd);
6797
8507
  } finally {
6798
- closeSync(fd);
8508
+ closeSync2(fd);
6799
8509
  }
6800
- renameSync4(tmpPath, CLAUDE_JSON_PATH);
8510
+ renameSync6(tmpPath, CLAUDE_JSON_PATH);
6801
8511
  } catch (err) {
6802
8512
  try {
6803
- unlinkSync5(tmpPath);
8513
+ unlinkSync7(tmpPath);
6804
8514
  } catch {
6805
8515
  }
6806
8516
  try {
@@ -6823,7 +8533,7 @@ function writeProjectMcpJson() {
6823
8533
  }
6824
8534
  }
6825
8535
  };
6826
- writeFileSync9(c.projectMcpPath, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
8536
+ writeFileSync14(c.projectMcpPath, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
6827
8537
  }
6828
8538
  }
6829
8539
  function patchClaudeJson() {
@@ -6900,42 +8610,42 @@ var CLAUDE_JSON_BACKUP_PATH, SESSION_DIR, PLUGIN_PATH, PLUGIN_PKG_PATH, PLUGIN_S
6900
8610
  var init_install2 = __esm({
6901
8611
  "cli/local-cc/install.ts"() {
6902
8612
  "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");
8613
+ CLAUDE_JSON_BACKUP_PATH = join18(homedir18(), ".claude.json.synkro-bak");
8614
+ SESSION_DIR = join18(homedir18(), ".synkro", "cc_sessions");
8615
+ PLUGIN_PATH = join18(SESSION_DIR, "synkro-channel.ts");
8616
+ PLUGIN_PKG_PATH = join18(SESSION_DIR, "package.json");
8617
+ PLUGIN_SETTINGS_DIR = join18(SESSION_DIR, ".claude");
8618
+ PLUGIN_SETTINGS_PATH = join18(PLUGIN_SETTINGS_DIR, "settings.json");
8619
+ PROJECT_MCP_PATH = join18(SESSION_DIR, ".mcp.json");
8620
+ CLAUDE_JSON_PATH = join18(homedir18(), ".claude.json");
8621
+ RUN_SCRIPT_PATH = join18(SESSION_DIR, "run-claude.sh");
6912
8622
  TMUX_SESSION_NAME = "synkro-local-cc";
6913
8623
  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");
8624
+ SESSION_DIR_2 = join18(homedir18(), ".synkro", "cc_sessions_2");
8625
+ PLUGIN_PATH_2 = join18(SESSION_DIR_2, "synkro-channel.ts");
8626
+ PLUGIN_PKG_PATH_2 = join18(SESSION_DIR_2, "package.json");
8627
+ PLUGIN_SETTINGS_DIR_2 = join18(SESSION_DIR_2, ".claude");
8628
+ PLUGIN_SETTINGS_PATH_2 = join18(PLUGIN_SETTINGS_DIR_2, "settings.json");
8629
+ PROJECT_MCP_PATH_2 = join18(SESSION_DIR_2, ".mcp.json");
8630
+ RUN_SCRIPT_PATH_2 = join18(SESSION_DIR_2, "run-claude.sh");
6921
8631
  TMUX_SESSION_NAME_2 = "synkro-local-cc-2";
6922
8632
  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");
8633
+ SESSION_DIR_3 = join18(homedir18(), ".synkro", "cc_sessions_3");
8634
+ PLUGIN_PATH_3 = join18(SESSION_DIR_3, "synkro-channel.ts");
8635
+ PLUGIN_PKG_PATH_3 = join18(SESSION_DIR_3, "package.json");
8636
+ PLUGIN_SETTINGS_DIR_3 = join18(SESSION_DIR_3, ".claude");
8637
+ PLUGIN_SETTINGS_PATH_3 = join18(PLUGIN_SETTINGS_DIR_3, "settings.json");
8638
+ PROJECT_MCP_PATH_3 = join18(SESSION_DIR_3, ".mcp.json");
8639
+ RUN_SCRIPT_PATH_3 = join18(SESSION_DIR_3, "run-claude.sh");
6930
8640
  TMUX_SESSION_NAME_3 = "synkro-local-cc-3";
6931
8641
  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");
8642
+ SESSION_DIR_4 = join18(homedir18(), ".synkro", "cc_sessions_4");
8643
+ PLUGIN_PATH_4 = join18(SESSION_DIR_4, "synkro-channel.ts");
8644
+ PLUGIN_PKG_PATH_4 = join18(SESSION_DIR_4, "package.json");
8645
+ PLUGIN_SETTINGS_DIR_4 = join18(SESSION_DIR_4, ".claude");
8646
+ PLUGIN_SETTINGS_PATH_4 = join18(PLUGIN_SETTINGS_DIR_4, "settings.json");
8647
+ PROJECT_MCP_PATH_4 = join18(SESSION_DIR_4, ".mcp.json");
8648
+ RUN_SCRIPT_PATH_4 = join18(SESSION_DIR_4, "run-claude.sh");
6939
8649
  TMUX_SESSION_NAME_4 = "synkro-local-cc-4";
6940
8650
  CHANNEL_4_PORT = 8952;
6941
8651
  RUN_SCRIPT_SOURCE = `#!/usr/bin/env bash
@@ -7209,9 +8919,9 @@ var disconnect_exports = {};
7209
8919
  __export(disconnect_exports, {
7210
8920
  disconnectCommand: () => disconnectCommand
7211
8921
  });
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";
8922
+ import { existsSync as existsSync22, rmSync as rmSync2, readdirSync as readdirSync4 } from "fs";
8923
+ import { homedir as homedir19 } from "os";
8924
+ import { join as join19 } from "path";
7215
8925
  import { spawnSync as spawnSync7 } from "child_process";
7216
8926
  import { createInterface as createInterface4 } from "readline";
7217
8927
  async function tearDownLocalCC() {
@@ -7257,9 +8967,12 @@ function confirmPurge() {
7257
8967
  });
7258
8968
  });
7259
8969
  }
7260
- async function disconnectCommand(args2 = []) {
7261
- const purge = args2.includes("--purge");
7262
- if (purge && !await confirmPurge()) {
8970
+ async function disconnectCommand(args2 = [], opts = {}) {
8971
+ const purge2 = args2.includes("--purge");
8972
+ const flavor = opts.flavor ?? (process.argv[2] === "uninstall" ? "uninstall" : "disconnect");
8973
+ const startedAt = Date.now();
8974
+ const removed = { hooks: false, mcp: false, config: false, telemetry_db: false };
8975
+ if (purge2 && !await confirmPurge()) {
7263
8976
  console.log("\nAborted \u2014 nothing was removed.");
7264
8977
  return;
7265
8978
  }
@@ -7270,57 +8983,74 @@ async function disconnectCommand(args2 = []) {
7270
8983
  for (const agent of agents) {
7271
8984
  if (agent.kind === "claude_code") {
7272
8985
  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"}`);
8986
+ const r = uninstallCCHooks(agent.settingsPath);
8987
+ if (r) removed.hooks = true;
8988
+ console.log(`${r ? "\u2713" : "\xB7"} ${agent.name}: ${r ? "removed Synkro hook entries" : "no Synkro hooks found"}`);
7275
8989
  } 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"}`);
8990
+ const r = uninstallCursorHooks(agent.settingsPath);
8991
+ if (r) removed.hooks = true;
8992
+ console.log(`${r ? "\u2713" : "\xB7"} ${agent.name}: ${r ? "removed Synkro hook entries" : "no Synkro hooks found"}`);
7278
8993
  }
7279
8994
  }
7280
8995
  if (sawClaudeCode) {
7281
8996
  const mcpRemoved = uninstallMcpConfig();
8997
+ if (mcpRemoved) removed.mcp = true;
7282
8998
  console.log(`${mcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (CC): ${mcpRemoved ? "removed from ~/.claude.json" : "no entry found"}`);
7283
8999
  }
7284
9000
  {
7285
9001
  const cursorMcpRemoved = uninstallCursorMcpConfig();
9002
+ if (cursorMcpRemoved) removed.mcp = true;
7286
9003
  console.log(`${cursorMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Cursor): ${cursorMcpRemoved ? "removed from ~/.cursor/mcp.json" : "no entry found"}`);
7287
9004
  }
7288
9005
  {
7289
9006
  const desktopMcpRemoved = uninstallClaudeDesktopMcpConfig();
9007
+ if (desktopMcpRemoved) removed.mcp = true;
7290
9008
  console.log(`${desktopMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Claude Desktop): ${desktopMcpRemoved ? "removed from claude_desktop_config.json" : "no entry found"}`);
7291
9009
  }
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`);
9010
+ if (existsSync22(SYNKRO_DIR12)) {
9011
+ if (purge2) {
9012
+ emit("uninstall", {
9013
+ flavor,
9014
+ purge: purge2,
9015
+ removed: { ...removed, config: true },
9016
+ duration_ms: Date.now() - startedAt
9017
+ });
9018
+ await flush({ force: true });
9019
+ rmSync2(SYNKRO_DIR12, { recursive: true, force: true });
9020
+ removed.config = true;
9021
+ console.log(`\u2713 wiped ${SYNKRO_DIR12} entirely \u2014 including all scan data and backups`);
7296
9022
  } else {
7297
9023
  const keep = /* @__PURE__ */ new Set([
7298
- join13(SYNKRO_DIR7, "pgdata"),
7299
- join13(SYNKRO_DIR7, "pgdata-backups"),
7300
- join13(SYNKRO_DIR7, ".transcript-offsets")
9024
+ join19(SYNKRO_DIR12, "pgdata"),
9025
+ join19(SYNKRO_DIR12, "pgdata-backups"),
9026
+ join19(SYNKRO_DIR12, ".transcript-offsets")
7301
9027
  ]);
7302
9028
  const preserved = [];
7303
- for (const entry of readdirSync4(SYNKRO_DIR7)) {
7304
- const full = join13(SYNKRO_DIR7, entry);
9029
+ for (const entry of readdirSync4(SYNKRO_DIR12)) {
9030
+ const full = join19(SYNKRO_DIR12, entry);
7305
9031
  if (keep.has(full)) {
7306
9032
  preserved.push(entry);
7307
9033
  continue;
7308
9034
  }
7309
9035
  rmSync2(full, { recursive: true, force: true });
7310
9036
  }
9037
+ removed.config = true;
7311
9038
  if (preserved.length > 0) {
7312
- console.log(`\u2713 removed Synkro config from ${SYNKRO_DIR7} (kept your scan data: ${preserved.join(", ")})`);
9039
+ console.log(`\u2713 removed Synkro config from ${SYNKRO_DIR12} (kept your scan data: ${preserved.join(", ")})`);
7313
9040
  console.log(" run `synkro uninstall --purge` to delete that too");
7314
9041
  } else {
7315
- console.log(`\u2713 removed ${SYNKRO_DIR7}`);
9042
+ console.log(`\u2713 removed ${SYNKRO_DIR12}`);
7316
9043
  }
7317
9044
  }
7318
9045
  } else {
7319
- console.log(`\xB7 ${SYNKRO_DIR7} already gone`);
9046
+ console.log(`\xB7 ${SYNKRO_DIR12} already gone`);
7320
9047
  }
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.");
9048
+ if (!purge2) {
9049
+ emit("uninstall", { flavor, purge: purge2, removed, duration_ms: Date.now() - startedAt });
9050
+ }
9051
+ 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
9052
  }
7323
- var SYNKRO_DIR7;
9053
+ var SYNKRO_DIR12;
7324
9054
  var init_disconnect = __esm({
7325
9055
  "cli/commands/disconnect.ts"() {
7326
9056
  "use strict";
@@ -7331,14 +9061,15 @@ var init_disconnect = __esm({
7331
9061
  init_install2();
7332
9062
  init_dockerInstall();
7333
9063
  init_macKeychain();
7334
- SYNKRO_DIR7 = join13(homedir13(), ".synkro");
9064
+ init_telemetry();
9065
+ SYNKRO_DIR12 = join19(homedir19(), ".synkro");
7335
9066
  }
7336
9067
  });
7337
9068
 
7338
9069
  // 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";
9070
+ import { appendFileSync as appendFileSync3, existsSync as existsSync23, mkdirSync as mkdirSync15, openSync as openSync3, readFileSync as readFileSync20, readSync, closeSync as closeSync3, statSync as statSync3, watchFile, unwatchFile } from "fs";
9071
+ import { dirname as dirname6, join as join20 } from "path";
9072
+ import { homedir as homedir20 } from "os";
7342
9073
  function truncate(s, max = PREVIEW_MAX) {
7343
9074
  if (s.length <= max) return s;
7344
9075
  return s.slice(0, max) + "\u2026 [+" + (s.length - max) + " chars]";
@@ -7358,7 +9089,7 @@ function extractSeverity(result) {
7358
9089
  }
7359
9090
  function appendTurn(args2) {
7360
9091
  try {
7361
- mkdirSync12(dirname6(TURN_LOG_PATH), { recursive: true });
9092
+ mkdirSync15(dirname6(TURN_LOG_PATH), { recursive: true });
7362
9093
  const entry = {
7363
9094
  ts: new Date(args2.startedAt).toISOString(),
7364
9095
  role: args2.role,
@@ -7369,16 +9100,16 @@ function appendTurn(args2) {
7369
9100
  severity: args2.result ? extractSeverity(args2.result) : void 0,
7370
9101
  error: args2.error
7371
9102
  };
7372
- appendFileSync(TURN_LOG_PATH, JSON.stringify(entry) + "\n", "utf-8");
9103
+ appendFileSync3(TURN_LOG_PATH, JSON.stringify(entry) + "\n", "utf-8");
7373
9104
  } catch {
7374
9105
  }
7375
9106
  }
7376
9107
  function readRecentTurns(n = 20) {
7377
- if (!existsSync15(TURN_LOG_PATH)) return [];
9108
+ if (!existsSync23(TURN_LOG_PATH)) return [];
7378
9109
  try {
7379
- const size = statSync2(TURN_LOG_PATH).size;
9110
+ const size = statSync3(TURN_LOG_PATH).size;
7380
9111
  if (size === 0) return [];
7381
- const text = readFileSync13(TURN_LOG_PATH, "utf-8");
9112
+ const text = readFileSync20(TURN_LOG_PATH, "utf-8");
7382
9113
  const lines = text.split("\n").filter(Boolean);
7383
9114
  const lastN = lines.slice(-n).reverse();
7384
9115
  return lastN.map((line) => {
@@ -7394,15 +9125,15 @@ function readRecentTurns(n = 20) {
7394
9125
  }
7395
9126
  function followTurns(onEntry) {
7396
9127
  try {
7397
- mkdirSync12(dirname6(TURN_LOG_PATH), { recursive: true });
7398
- if (!existsSync15(TURN_LOG_PATH)) {
7399
- appendFileSync(TURN_LOG_PATH, "", "utf-8");
9128
+ mkdirSync15(dirname6(TURN_LOG_PATH), { recursive: true });
9129
+ if (!existsSync23(TURN_LOG_PATH)) {
9130
+ appendFileSync3(TURN_LOG_PATH, "", "utf-8");
7400
9131
  }
7401
9132
  } catch {
7402
9133
  }
7403
9134
  let lastSize = (() => {
7404
9135
  try {
7405
- return statSync2(TURN_LOG_PATH).size;
9136
+ return statSync3(TURN_LOG_PATH).size;
7406
9137
  } catch {
7407
9138
  return 0;
7408
9139
  }
@@ -7412,7 +9143,7 @@ function followTurns(onEntry) {
7412
9143
  if (to <= from) return;
7413
9144
  let fd = null;
7414
9145
  try {
7415
- fd = openSync2(TURN_LOG_PATH, "r");
9146
+ fd = openSync3(TURN_LOG_PATH, "r");
7416
9147
  const len = to - from;
7417
9148
  const buf = Buffer.alloc(len);
7418
9149
  readSync(fd, buf, 0, len, from);
@@ -7435,7 +9166,7 @@ function followTurns(onEntry) {
7435
9166
  } finally {
7436
9167
  if (fd !== null) {
7437
9168
  try {
7438
- closeSync2(fd);
9169
+ closeSync3(fd);
7439
9170
  } catch {
7440
9171
  }
7441
9172
  }
@@ -7457,7 +9188,7 @@ var TURN_LOG_PATH, PREVIEW_MAX;
7457
9188
  var init_turnLog = __esm({
7458
9189
  "cli/local-cc/turnLog.ts"() {
7459
9190
  "use strict";
7460
- TURN_LOG_PATH = join14(homedir14(), ".synkro", "cc_sessions", "turns.log");
9191
+ TURN_LOG_PATH = join20(homedir20(), ".synkro", "cc_sessions", "turns.log");
7461
9192
  PREVIEW_MAX = 400;
7462
9193
  }
7463
9194
  });
@@ -7538,7 +9269,7 @@ function isChannelAvailable(port = CHANNEL_PORT, timeoutMs = 500) {
7538
9269
  });
7539
9270
  }
7540
9271
  var CHANNEL_HOST, CHANNEL_PORT, DEFAULT_TIMEOUT_MS, LocalCCError;
7541
- var init_client = __esm({
9272
+ var init_client2 = __esm({
7542
9273
  "cli/local-cc/client.ts"() {
7543
9274
  "use strict";
7544
9275
  init_turnLog();
@@ -7601,14 +9332,14 @@ async function gradeCommand(args2) {
7601
9332
  var init_grade = __esm({
7602
9333
  "cli/commands/grade.ts"() {
7603
9334
  "use strict";
7604
- init_client();
9335
+ init_client2();
7605
9336
  }
7606
9337
  });
7607
9338
 
7608
9339
  // 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";
9340
+ import { execFileSync as execFileSync4, spawnSync as spawnSync8, spawn as spawn3 } from "child_process";
9341
+ import { homedir as homedir21 } from "os";
9342
+ import { join as join21 } from "path";
7612
9343
  import { connect as connect2 } from "net";
7613
9344
  function pueueAvailable() {
7614
9345
  const r = spawnSync8("pueue", ["--version"], { encoding: "utf-8" });
@@ -7674,7 +9405,7 @@ function startTask(opts = {}) {
7674
9405
  spawnSync8("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
7675
9406
  existing = findTask(ch);
7676
9407
  }
7677
- const runScript = join15(cwd, "run-claude.sh");
9408
+ const runScript = join21(cwd, "run-claude.sh");
7678
9409
  const args2 = [
7679
9410
  "add",
7680
9411
  "--label",
@@ -7771,7 +9502,7 @@ function assertPueueInstalled() {
7771
9502
  const status = spawnSync8("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
7772
9503
  if (status.status !== 0) {
7773
9504
  console.log(" Starting pueued daemon...");
7774
- const child = spawn2("pueued", ["-d"], { stdio: "ignore", detached: true });
9505
+ const child = spawn3("pueued", ["-d"], { stdio: "ignore", detached: true });
7775
9506
  child.unref();
7776
9507
  spawnSync8("sleep", ["1"]);
7777
9508
  const retry = spawnSync8("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
@@ -7804,12 +9535,12 @@ var init_pueue = __esm({
7804
9535
  "use strict";
7805
9536
  TASK_LABEL = "synkro-local-cc";
7806
9537
  TMUX_SESSION = "synkro-local-cc";
7807
- SESSION_DIR2 = join15(homedir15(), ".synkro", "cc_sessions");
9538
+ SESSION_DIR2 = join21(homedir21(), ".synkro", "cc_sessions");
7808
9539
  TASK_LABEL_2 = "synkro-local-cc-2";
7809
9540
  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");
9541
+ SESSION_DIR_22 = join21(homedir21(), ".synkro", "cc_sessions_2");
9542
+ SESSION_DIR_32 = join21(homedir21(), ".synkro", "cc_sessions_3");
9543
+ SESSION_DIR_42 = join21(homedir21(), ".synkro", "cc_sessions_4");
7813
9544
  PueueError = class extends Error {
7814
9545
  constructor(message, cause) {
7815
9546
  super(message);
@@ -7824,24 +9555,24 @@ var init_pueue = __esm({
7824
9555
  });
7825
9556
 
7826
9557
  // 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";
9558
+ import { existsSync as existsSync24, readFileSync as readFileSync21 } from "fs";
9559
+ import { homedir as homedir22 } from "os";
9560
+ import { join as join22 } from "path";
7830
9561
  function isLocalCCEnabled() {
7831
- if (!existsSync16(CONFIG_PATH3)) return false;
9562
+ if (!existsSync24(CONFIG_PATH5)) return false;
7832
9563
  try {
7833
- const content = readFileSync14(CONFIG_PATH3, "utf-8");
9564
+ const content = readFileSync21(CONFIG_PATH5, "utf-8");
7834
9565
  const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
7835
9566
  return match?.[1] === "yes";
7836
9567
  } catch {
7837
9568
  return false;
7838
9569
  }
7839
9570
  }
7840
- var CONFIG_PATH3;
9571
+ var CONFIG_PATH5;
7841
9572
  var init_settings = __esm({
7842
9573
  "cli/local-cc/settings.ts"() {
7843
9574
  "use strict";
7844
- CONFIG_PATH3 = join16(homedir16(), ".synkro", "config.env");
9575
+ CONFIG_PATH5 = join22(homedir22(), ".synkro", "config.env");
7845
9576
  }
7846
9577
  });
7847
9578
 
@@ -7851,10 +9582,10 @@ __export(localCc_exports, {
7851
9582
  localCcCommand: () => localCcCommand
7852
9583
  });
7853
9584
  import { spawnSync as spawnSync9 } from "child_process";
7854
- import { homedir as homedir17 } from "os";
7855
- import { join as join17 } from "path";
9585
+ import { homedir as homedir23 } from "os";
9586
+ import { join as join23 } from "path";
7856
9587
  import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from "fs";
7857
- import { existsSync as existsSync17, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
9588
+ import { existsSync as existsSync25, readFileSync as readFileSync22, writeFileSync as writeFileSync15 } from "fs";
7858
9589
  function deploymentMode() {
7859
9590
  const env = (process.env.SYNKRO_DEPLOYMENT_MODE || "").toLowerCase();
7860
9591
  if (env === "docker") return "docker";
@@ -7960,15 +9691,15 @@ TROUBLESHOOTING
7960
9691
  `);
7961
9692
  }
7962
9693
  function readGatewayUrl() {
7963
- if (existsSync17(CONFIG_PATH4)) {
7964
- const m = readFileSync15(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
9694
+ if (existsSync25(CONFIG_PATH6)) {
9695
+ const m = readFileSync22(CONFIG_PATH6, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
7965
9696
  if (m) return m[1];
7966
9697
  }
7967
9698
  return "https://api.synkro.sh";
7968
9699
  }
7969
9700
  function updateLocalInferenceFlag(enabled) {
7970
- if (!existsSync17(CONFIG_PATH4)) return;
7971
- let content = readFileSync15(CONFIG_PATH4, "utf-8");
9701
+ if (!existsSync25(CONFIG_PATH6)) return;
9702
+ let content = readFileSync22(CONFIG_PATH6, "utf-8");
7972
9703
  const flag = enabled ? "yes" : "no";
7973
9704
  if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
7974
9705
  content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
@@ -7977,7 +9708,7 @@ function updateLocalInferenceFlag(enabled) {
7977
9708
  SYNKRO_LOCAL_INFERENCE='${flag}'
7978
9709
  `;
7979
9710
  }
7980
- writeFileSync10(CONFIG_PATH4, content, "utf-8");
9711
+ writeFileSync15(CONFIG_PATH6, content, "utf-8");
7981
9712
  }
7982
9713
  async function setServerGradingProvider(provider) {
7983
9714
  await ensureValidToken();
@@ -8396,7 +10127,7 @@ async function localCcCommand(args2) {
8396
10127
  process.exit(1);
8397
10128
  }
8398
10129
  }
8399
- var SYNKRO_CONFIG_PATH, CONFIG_PATH4;
10130
+ var SYNKRO_CONFIG_PATH, CONFIG_PATH6;
8400
10131
  var init_localCc = __esm({
8401
10132
  "cli/commands/localCc.ts"() {
8402
10133
  "use strict";
@@ -8407,10 +10138,10 @@ var init_localCc = __esm({
8407
10138
  init_macKeychain();
8408
10139
  init_dockerInstall();
8409
10140
  init_install();
8410
- init_client();
10141
+ init_client2();
8411
10142
  init_stub();
8412
- SYNKRO_CONFIG_PATH = join17(homedir17(), ".synkro", "config.env");
8413
- CONFIG_PATH4 = join17(homedir17(), ".synkro", "config.env");
10143
+ SYNKRO_CONFIG_PATH = join23(homedir23(), ".synkro", "config.env");
10144
+ CONFIG_PATH6 = join23(homedir23(), ".synkro", "config.env");
8414
10145
  }
8415
10146
  });
8416
10147
 
@@ -8419,15 +10150,15 @@ var import_exports = {};
8419
10150
  __export(import_exports, {
8420
10151
  importCommand: () => importCommand
8421
10152
  });
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";
10153
+ import { existsSync as existsSync26, readFileSync as readFileSync23, readdirSync as readdirSync5 } from "fs";
10154
+ import { homedir as homedir24 } from "os";
10155
+ import { join as join24 } from "path";
8425
10156
  import { execSync as execSync7 } from "child_process";
8426
10157
  import { createInterface as createInterface5 } from "readline";
8427
- function readConfigEnv() {
10158
+ function readConfigEnv2() {
8428
10159
  const out = {};
8429
10160
  try {
8430
- for (const line of readFileSync16(CONFIG_PATH5, "utf-8").split("\n")) {
10161
+ for (const line of readFileSync23(CONFIG_PATH7, "utf-8").split("\n")) {
8431
10162
  const t = line.trim();
8432
10163
  if (!t || t.startsWith("#")) continue;
8433
10164
  const eq = t.indexOf("=");
@@ -8439,8 +10170,8 @@ function readConfigEnv() {
8439
10170
  }
8440
10171
  function projectsFolder() {
8441
10172
  const sanitized = process.cwd().replace(/\//g, "-");
8442
- const dir = join18(homedir18(), ".claude", "projects", sanitized);
8443
- return existsSync18(dir) ? dir : null;
10173
+ const dir = join24(homedir24(), ".claude", "projects", sanitized);
10174
+ return existsSync26(dir) ? dir : null;
8444
10175
  }
8445
10176
  function repoName() {
8446
10177
  try {
@@ -8463,7 +10194,7 @@ function extractText(content) {
8463
10194
  return "";
8464
10195
  }
8465
10196
  function parseSession(filePath, sessionId) {
8466
- const lines = readFileSync16(filePath, "utf-8").split("\n").filter(Boolean);
10197
+ const lines = readFileSync23(filePath, "utf-8").split("\n").filter(Boolean);
8467
10198
  const messages = [];
8468
10199
  const actions = [];
8469
10200
  let step = 0;
@@ -8515,7 +10246,7 @@ function ask2(q) {
8515
10246
  }));
8516
10247
  }
8517
10248
  async function importCommand() {
8518
- const config = readConfigEnv();
10249
+ const config = readConfigEnv2();
8519
10250
  const isCloud = config.SYNKRO_DEPLOY_LOCATION === "cloud" || config.SYNKRO_STORAGE_MODE === "cloud";
8520
10251
  const repo = repoName();
8521
10252
  const dir = projectsFolder();
@@ -8536,7 +10267,7 @@ async function importCommand() {
8536
10267
  return;
8537
10268
  }
8538
10269
  }
8539
- const sessions = files.map((f) => parseSession(join18(dir, f), f.replace(".jsonl", ""))).filter((s) => s.messages.length > 0);
10270
+ const sessions = files.map((f) => parseSession(join24(dir, f), f.replace(".jsonl", ""))).filter((s) => s.messages.length > 0);
8540
10271
  const totalMsgs = sessions.reduce((n, s) => n + s.messages.length, 0);
8541
10272
  let ok = 0, fail = 0;
8542
10273
  if (isCloud) {
@@ -8591,12 +10322,12 @@ async function importCommand() {
8591
10322
  console.log(`
8592
10323
  \u2713 Imported ${ok} session(s) (${totalMsgs} messages)${fail ? `, ${fail} failed` : ""}.`);
8593
10324
  }
8594
- var CONFIG_PATH5;
10325
+ var CONFIG_PATH7;
8595
10326
  var init_import = __esm({
8596
10327
  "cli/commands/import.ts"() {
8597
10328
  "use strict";
8598
10329
  init_stub();
8599
- CONFIG_PATH5 = join18(homedir18(), ".synkro", "config.env");
10330
+ CONFIG_PATH7 = join24(homedir24(), ".synkro", "config.env");
8600
10331
  }
8601
10332
  });
8602
10333
 
@@ -8638,10 +10369,10 @@ var init_packVerify = __esm({
8638
10369
  });
8639
10370
 
8640
10371
  // cli/installer/lockfile.ts
8641
- import { existsSync as existsSync19, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
8642
- import { join as join19 } from "path";
10372
+ import { existsSync as existsSync27, readFileSync as readFileSync24, writeFileSync as writeFileSync16 } from "fs";
10373
+ import { join as join25 } from "path";
8643
10374
  function lockPath(repoRoot) {
8644
- return join19(repoRoot, LOCK_FILE);
10375
+ return join25(repoRoot, LOCK_FILE);
8645
10376
  }
8646
10377
  function writeLockfile(repoRoot, entries) {
8647
10378
  const sorted = [...entries].sort((a, b) => a.ref.localeCompare(b.ref));
@@ -8659,7 +10390,7 @@ function writeLockfile(repoRoot, entries) {
8659
10390
  ""
8660
10391
  ])
8661
10392
  ].join("\n");
8662
- writeFileSync11(lockPath(repoRoot), body, "utf-8");
10393
+ writeFileSync16(lockPath(repoRoot), body, "utf-8");
8663
10394
  }
8664
10395
  var LOCK_FILE;
8665
10396
  var init_lockfile = __esm({
@@ -8674,9 +10405,9 @@ var sync_exports = {};
8674
10405
  __export(sync_exports, {
8675
10406
  syncCommand: () => syncCommand
8676
10407
  });
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";
10408
+ import { existsSync as existsSync28, mkdirSync as mkdirSync16, readdirSync as readdirSync6, rmSync as rmSync3, writeFileSync as writeFileSync17 } from "fs";
10409
+ import { homedir as homedir25 } from "os";
10410
+ import { join as join26 } from "path";
8680
10411
  function cacheKey(ref, version) {
8681
10412
  return ref.replace(/\//g, "__").replace(/[^\w.@-]/g, "_") + "@" + version + ".json";
8682
10413
  }
@@ -8707,8 +10438,8 @@ async function syncCommand(_args = []) {
8707
10438
  }
8708
10439
  const gateway = (process.env.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
8709
10440
  const cloud = process.env.SYNKRO_DEPLOY_LOCATION === "cloud";
8710
- const cacheDir = join20(homedir19(), ".synkro", "cache", "packs");
8711
- if (!cloud) mkdirSync13(cacheDir, { recursive: true });
10441
+ const cacheDir = join26(homedir25(), ".synkro", "cache", "packs");
10442
+ if (!cloud) mkdirSync16(cacheDir, { recursive: true });
8712
10443
  console.log(`Syncing ${refs.length} standard(s) from the registry\u2026`);
8713
10444
  const lock = [];
8714
10445
  const keptCacheFiles = /* @__PURE__ */ new Set();
@@ -8736,7 +10467,7 @@ async function syncCommand(_args = []) {
8736
10467
  if (!cloud) {
8737
10468
  const fname = cacheKey(ref, data.version);
8738
10469
  keptCacheFiles.add(fname);
8739
- writeFileSync12(join20(cacheDir, fname), JSON.stringify({
10470
+ writeFileSync17(join26(cacheDir, fname), JSON.stringify({
8740
10471
  ref,
8741
10472
  version: data.version,
8742
10473
  digest: data.digest,
@@ -8748,11 +10479,11 @@ async function syncCommand(_args = []) {
8748
10479
  const ruleCount = Array.isArray(pack.rules) ? pack.rules.length : 0;
8749
10480
  console.log(` \u2713 ${ref}:${data.version} \u2014 verified (${ruleCount} rule${ruleCount === 1 ? "" : "s"})`);
8750
10481
  }
8751
- if (!cloud && existsSync20(cacheDir)) {
10482
+ if (!cloud && existsSync28(cacheDir)) {
8752
10483
  for (const f of readdirSync6(cacheDir)) {
8753
10484
  if (f.endsWith(".json") && !keptCacheFiles.has(f)) {
8754
10485
  try {
8755
- rmSync3(join20(cacheDir, f));
10486
+ rmSync3(join26(cacheDir, f));
8756
10487
  } catch {
8757
10488
  }
8758
10489
  }
@@ -8888,13 +10619,13 @@ var config_exports = {};
8888
10619
  __export(config_exports, {
8889
10620
  configCommand: () => configCommand
8890
10621
  });
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 {};
10622
+ import { readFileSync as readFileSync25, writeFileSync as writeFileSync18, existsSync as existsSync29 } from "fs";
10623
+ import { join as join27 } from "path";
10624
+ import { homedir as homedir26 } from "os";
10625
+ function readConfigEnv3() {
10626
+ if (!existsSync29(CONFIG_PATH8)) return {};
8896
10627
  const out = {};
8897
- for (const line of readFileSync18(CONFIG_PATH6, "utf-8").split("\n")) {
10628
+ for (const line of readFileSync25(CONFIG_PATH8, "utf-8").split("\n")) {
8898
10629
  const t = line.trim();
8899
10630
  if (!t || t.startsWith("#")) continue;
8900
10631
  const eq = t.indexOf("=");
@@ -8903,11 +10634,11 @@ function readConfigEnv2() {
8903
10634
  return out;
8904
10635
  }
8905
10636
  function updateConfigValue(key, value) {
8906
- if (!existsSync21(CONFIG_PATH6)) {
10637
+ if (!existsSync29(CONFIG_PATH8)) {
8907
10638
  console.error("No config found. Run `synkro install` first.");
8908
10639
  process.exit(1);
8909
10640
  }
8910
- const lines = readFileSync18(CONFIG_PATH6, "utf-8").split("\n");
10641
+ const lines = readFileSync25(CONFIG_PATH8, "utf-8").split("\n");
8911
10642
  const pattern = new RegExp(`^${key}=`);
8912
10643
  let found = false;
8913
10644
  const updated = lines.map((line) => {
@@ -8918,7 +10649,7 @@ function updateConfigValue(key, value) {
8918
10649
  return line;
8919
10650
  });
8920
10651
  if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
8921
- writeFileSync13(CONFIG_PATH6, updated.join("\n"), "utf-8");
10652
+ writeFileSync18(CONFIG_PATH8, updated.join("\n"), "utf-8");
8922
10653
  }
8923
10654
  function resolveInferenceMode(cfg) {
8924
10655
  if ((cfg.SYNKRO_GRADING_MODE || "local") === "byok") return "byok";
@@ -8926,7 +10657,7 @@ function resolveInferenceMode(cfg) {
8926
10657
  return "local";
8927
10658
  }
8928
10659
  async function reconcileContainer() {
8929
- const cfg = readConfigEnv2();
10660
+ const cfg = readConfigEnv3();
8930
10661
  const cloudOnly = (cfg.SYNKRO_GRADING_MODE || "local") === "byok" && (cfg.SYNKRO_STORAGE_MODE || "local") === "cloud";
8931
10662
  try {
8932
10663
  const { dockerInstall: dockerInstall2, dockerSafeStop: dockerSafeStop2, readContainerConfig: readContainerConfig2, assertDockerAvailable: assertDockerAvailable2, waitForContainerReady: waitForContainerReady2 } = await Promise.resolve().then(() => (init_dockerInstall(), dockerInstall_exports));
@@ -8950,7 +10681,7 @@ async function reconcileContainer() {
8950
10681
  }
8951
10682
  async function configCommand(args2) {
8952
10683
  if (args2.length === 0) {
8953
- const config2 = readConfigEnv2();
10684
+ const config2 = readConfigEnv3();
8954
10685
  console.log("Synkro config:\n");
8955
10686
  console.log(` inference: ${resolveInferenceMode(config2)}`);
8956
10687
  console.log(` storage: ${config2.SYNKRO_STORAGE_MODE || "local"}`);
@@ -8981,6 +10712,31 @@ To change:`);
8981
10712
  await reconcileContainer();
8982
10713
  return;
8983
10714
  }
10715
+ if (args2[0] === "telemetry") {
10716
+ const rest = args2.slice(1);
10717
+ if (rest.length === 0) {
10718
+ const s = await getTelemetryState();
10719
+ const label = !s.enabled ? "OFF" : s.remoteFlushEnabled ? "ON" : "local-only";
10720
+ console.log(`Telemetry: ${label}`);
10721
+ return;
10722
+ }
10723
+ if (rest.length !== 1 || !["--on", "--off", "--local-only"].includes(rest[0])) {
10724
+ console.error("Usage: synkro config telemetry [--on|--off|--local-only]");
10725
+ process.exit(1);
10726
+ }
10727
+ const flag = rest[0];
10728
+ if (flag === "--on") {
10729
+ await setTelemetryState({ enabled: true, remoteFlushEnabled: true });
10730
+ console.log("Telemetry: ON");
10731
+ } else if (flag === "--off") {
10732
+ await setTelemetryState({ enabled: false, remoteFlushEnabled: false });
10733
+ console.log("Telemetry: OFF");
10734
+ } else {
10735
+ await setTelemetryState({ enabled: true, remoteFlushEnabled: false });
10736
+ console.log("Telemetry: local-only");
10737
+ }
10738
+ return;
10739
+ }
8984
10740
  if (args2[0] === "storage") {
8985
10741
  const value = args2[1];
8986
10742
  if (value !== "local" && value !== "cloud") {
@@ -9006,7 +10762,7 @@ To change:`);
9006
10762
  process.exit(1);
9007
10763
  }
9008
10764
  const token = getAccessToken();
9009
- const config = readConfigEnv2();
10765
+ const config = readConfigEnv3();
9010
10766
  const gatewayUrl = (config.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
9011
10767
  try {
9012
10768
  const resp = await fetch(`${gatewayUrl}/api/v1/cli/me`, {
@@ -9045,25 +10801,210 @@ To change:`);
9045
10801
  }
9046
10802
  if (inferenceValue !== "cloud") await reconcileContainer();
9047
10803
  }
9048
- var SYNKRO_DIR8, CONFIG_PATH6;
10804
+ var SYNKRO_DIR13, CONFIG_PATH8;
9049
10805
  var init_config = __esm({
9050
10806
  "cli/commands/config.ts"() {
9051
10807
  "use strict";
9052
10808
  init_stub();
9053
- SYNKRO_DIR8 = join21(homedir20(), ".synkro");
9054
- CONFIG_PATH6 = join21(SYNKRO_DIR8, "config.env");
10809
+ init_optout();
10810
+ SYNKRO_DIR13 = join27(homedir26(), ".synkro");
10811
+ CONFIG_PATH8 = join27(SYNKRO_DIR13, "config.env");
10812
+ }
10813
+ });
10814
+
10815
+ // cli/commands/telemetry.ts
10816
+ var telemetry_exports2 = {};
10817
+ __export(telemetry_exports2, {
10818
+ telemetryCommand: () => telemetryCommand
10819
+ });
10820
+ import { createInterface as createInterface6 } from "readline";
10821
+ function parseFlag(args2, name) {
10822
+ const prefix = `--${name}=`;
10823
+ for (const a of args2) if (a.startsWith(prefix)) return a.slice(prefix.length);
10824
+ const idx = args2.indexOf(`--${name}`);
10825
+ if (idx >= 0 && idx + 1 < args2.length && !args2[idx + 1].startsWith("--")) {
10826
+ return args2[idx + 1];
10827
+ }
10828
+ return void 0;
10829
+ }
10830
+ function hasFlag(args2, name) {
10831
+ return args2.includes(`--${name}`);
10832
+ }
10833
+ function truncate2(s, max) {
10834
+ if (s.length <= max) return s;
10835
+ return s.slice(0, max) + `\u2026 (+${s.length - max} chars)`;
10836
+ }
10837
+ async function printStats() {
10838
+ const s = await getStats();
10839
+ console.log("Telemetry:");
10840
+ console.log(` install_id: ${s.install_id}`);
10841
+ console.log(` enabled: ${s.enabled ? "yes" : "no"}`);
10842
+ console.log(` remote_flush: ${s.remote_flush_enabled ? "yes" : "no"}`);
10843
+ console.log(` pglite (local): ${s.pglite_reachable ? "reachable" : "none (cloud mode \u2014 query central for totals)"}`);
10844
+ console.log(` local mirror events: ${s.total_events}`);
10845
+ console.log(` pending (queue): ${s.pending_local}`);
10846
+ if (s.newest) console.log(` newest event: ${s.newest}`);
10847
+ if (s.last_drained_at) console.log(` last local drain: ${s.last_drained_at}`);
10848
+ if (s.last_flush_at) console.log(` last flush attempt: ${s.last_flush_at}`);
10849
+ if (s.last_flush_ok_at) console.log(` last flush success: ${s.last_flush_ok_at}`);
10850
+ if (s.last_flush_error) console.log(` last flush error: ${s.last_flush_error}`);
10851
+ const types = Object.entries(s.by_type);
10852
+ if (types.length > 0) {
10853
+ console.log(` by type:`);
10854
+ for (const [name, count] of types) {
10855
+ console.log(` ${name.padEnd(22)} ${count}`);
10856
+ }
10857
+ }
10858
+ }
10859
+ async function printTail(args2) {
10860
+ const opts = {};
10861
+ const limitRaw = parseFlag(args2, "limit");
10862
+ if (limitRaw) {
10863
+ const n = Number.parseInt(limitRaw, 10);
10864
+ if (Number.isFinite(n) && n > 0) opts.limit = n;
10865
+ }
10866
+ const typeRaw = parseFlag(args2, "type");
10867
+ if (typeRaw) opts.eventType = typeRaw;
10868
+ const sessionRaw = parseFlag(args2, "session");
10869
+ if (sessionRaw) opts.sessionId = sessionRaw;
10870
+ const rows = await tail(opts);
10871
+ if (rows.length === 0) {
10872
+ console.log("(no events \u2014 run `synkro start` if the container is down so JSONL pending events can drain)");
10873
+ return;
10874
+ }
10875
+ for (const row of rows) {
10876
+ const session = row.cc_session_id ? ` session=${String(row.cc_session_id).slice(0, 8)}` : "";
10877
+ const ts = typeof row.occurred_at === "string" ? row.occurred_at : new Date(row.occurred_at).toISOString();
10878
+ console.log(` ${ts} ${row.event_type.padEnd(20)} ${row.emitter}${session}`);
10879
+ const contextStr = typeof row.context === "string" ? row.context : JSON.stringify(row.context);
10880
+ console.log(` context: ${truncate2(contextStr, 200)}`);
10881
+ }
10882
+ }
10883
+ async function runFlush(args2) {
10884
+ const force = hasFlag(args2, "force");
10885
+ const r = await flush({ force });
10886
+ const parts = [`remote sent: ${r.sent}`, `queued (kept): ${r.failed}`, `local mirror: ${r.mirrored}`];
10887
+ if (r.skipped_reason) parts.push(`remote skipped (${r.skipped_reason})`);
10888
+ console.log(parts.join(", "));
10889
+ }
10890
+ async function runExport(args2) {
10891
+ const path = args2[0];
10892
+ if (!path) {
10893
+ console.error("usage: synkro telemetry export <path>");
10894
+ process.exit(1);
10895
+ }
10896
+ await exportEvents(path);
10897
+ console.log(`wrote ${path}`);
10898
+ }
10899
+ function confirmYesNo(question) {
10900
+ if (!process.stdin.isTTY) return Promise.resolve(false);
10901
+ return new Promise((resolve4) => {
10902
+ const rl = createInterface6({ input: process.stdin, output: process.stdout });
10903
+ rl.question(`${question} (y/N): `, (answer) => {
10904
+ rl.close();
10905
+ const t = answer.trim().toLowerCase();
10906
+ resolve4(t === "y" || t === "yes");
10907
+ });
10908
+ });
10909
+ }
10910
+ async function runPurge(args2) {
10911
+ const keepInstallId = hasFlag(args2, "keep-install-id");
10912
+ const skipPrompt = hasFlag(args2, "yes") || hasFlag(args2, "y");
10913
+ if (!skipPrompt) {
10914
+ const msg = keepInstallId ? "Wipe all local telemetry events (keeping your install_id)?" : "Wipe all local telemetry events AND reset install_id?";
10915
+ const ok = await confirmYesNo(msg);
10916
+ if (!ok) {
10917
+ console.log("aborted");
10918
+ return;
10919
+ }
10920
+ }
10921
+ const result = await purge({ keepInstallId });
10922
+ if (!result.pglite_reachable) {
10923
+ console.log(`removed ${result.removed_pending} pending event(s) from local queue (container down \u2014 pglite events untouched)`);
10924
+ return;
10925
+ }
10926
+ console.log(`removed ${result.removed_events} pglite event(s) + ${result.removed_pending} pending; meta ${result.removed_meta ? "reset" : "unchanged"}`);
10927
+ }
10928
+ async function telemetryCommand(args2) {
10929
+ const sub = args2[0];
10930
+ const rest = args2.slice(1);
10931
+ switch (sub) {
10932
+ case "stats":
10933
+ await printStats();
10934
+ return;
10935
+ case "tail":
10936
+ await printTail(rest);
10937
+ return;
10938
+ case "flush":
10939
+ await runFlush(rest);
10940
+ return;
10941
+ case "export":
10942
+ await runExport(rest);
10943
+ return;
10944
+ case "purge":
10945
+ await runPurge(rest);
10946
+ return;
10947
+ case "on":
10948
+ await setTelemetryState({ enabled: true, remoteFlushEnabled: true });
10949
+ console.log("telemetry: on (collection + remote flush enabled)");
10950
+ return;
10951
+ case "off":
10952
+ await setTelemetryState({ enabled: false, remoteFlushEnabled: false });
10953
+ console.log("telemetry: off (no collection, no remote flush)");
10954
+ return;
10955
+ case "local-only":
10956
+ await setTelemetryState({ enabled: true, remoteFlushEnabled: false });
10957
+ console.log("telemetry: local-only (collection on, remote flush off)");
10958
+ return;
10959
+ case void 0:
10960
+ case "":
10961
+ case "help":
10962
+ case "--help":
10963
+ case "-h":
10964
+ console.log(HELP);
10965
+ return;
10966
+ default:
10967
+ console.error(`Unknown subcommand: ${sub}`);
10968
+ console.log(HELP);
10969
+ process.exit(1);
10970
+ }
10971
+ }
10972
+ var HELP;
10973
+ var init_telemetry2 = __esm({
10974
+ "cli/commands/telemetry.ts"() {
10975
+ "use strict";
10976
+ init_flush();
10977
+ init_purge();
10978
+ init_stats();
10979
+ init_optout();
10980
+ HELP = `synkro telemetry \u2014 inspect or flush local telemetry events
10981
+
10982
+ Usage:
10983
+ synkro telemetry stats show counts, queue depth, last flush
10984
+ synkro telemetry tail [opts] print recent events (most recent first)
10985
+ --limit=N max rows (default 20)
10986
+ --type=X filter by event_type
10987
+ --session=Y filter by cc_session_id
10988
+ synkro telemetry flush [--force] ship unflushed events now (bypass throttle)
10989
+ synkro telemetry export <path> dump every event as NDJSON to <path>
10990
+ synkro telemetry purge [--keep-install-id] [--yes]
10991
+ wipe local events; --keep-install-id keeps your install_id
10992
+ synkro telemetry on enable collection AND remote flush
10993
+ synkro telemetry off disable collection entirely
10994
+ synkro telemetry local-only collect locally, never flush remotely
10995
+ `;
9055
10996
  }
9056
10997
  });
9057
10998
 
9058
10999
  // cli/bootstrap.js
9059
- import { readFileSync as readFileSync19, existsSync as existsSync22 } from "fs";
11000
+ import { readFileSync as readFileSync26, existsSync as existsSync30 } from "fs";
9060
11001
  import { resolve as resolve3 } from "path";
9061
11002
  var envCandidates = [
9062
11003
  resolve3(process.env.HOME ?? "", ".synkro", "config.env")
9063
11004
  ];
9064
11005
  for (const envPath of envCandidates) {
9065
- if (!existsSync22(envPath)) continue;
9066
- const envContent = readFileSync19(envPath, "utf-8");
11006
+ if (!existsSync30(envPath)) continue;
11007
+ const envContent = readFileSync26(envPath, "utf-8");
9067
11008
  for (const line of envContent.split("\n")) {
9068
11009
  const trimmed = line.trim();
9069
11010
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -9077,8 +11018,10 @@ for (const envPath of envCandidates) {
9077
11018
  var args = process.argv.slice(2);
9078
11019
  var cmd = args[0] || "";
9079
11020
  var subArgs = args.slice(1);
11021
+ var isDetachedChild = process.env.SYNKRO_TELEMETRY_DETACHED === "1";
11022
+ var FLUSH_SKIP = /* @__PURE__ */ new Set(["grade", "version", "--version", "-v", "help", "--help", "-h", ""]);
9080
11023
  function printVersion() {
9081
- console.log("1.7.60");
11024
+ console.log("1.7.62");
9082
11025
  }
9083
11026
  function printHelp2() {
9084
11027
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
@@ -9095,6 +11038,7 @@ Commands:
9095
11038
  update Pull the latest container image and safely restart
9096
11039
  config Show or change grading + storage modes
9097
11040
  claude-desktop Monitor Claude Desktop conversations (local, macOS)
11041
+ telemetry <sub> Inspect or flush local telemetry events
9098
11042
  version Show version
9099
11043
 
9100
11044
  config:
@@ -9113,6 +11057,21 @@ Quick start:
9113
11057
  `);
9114
11058
  }
9115
11059
  async function main() {
11060
+ let emit2 = null;
11061
+ try {
11062
+ ({ emit: emit2 } = await Promise.resolve().then(() => (init_telemetry(), telemetry_exports)));
11063
+ } catch {
11064
+ }
11065
+ if (emit2 && !isDetachedChild && cmd !== "grade") {
11066
+ try {
11067
+ emit2("cli_invocation", {
11068
+ argv: process.argv.slice(2),
11069
+ subcommand: cmd || "help",
11070
+ cwd: process.cwd()
11071
+ });
11072
+ } catch {
11073
+ }
11074
+ }
9116
11075
  switch (cmd) {
9117
11076
  case "install": {
9118
11077
  const { installCommand: installCommand2, parseArgs: parseArgs2 } = await Promise.resolve().then(() => (init_install(), install_exports));
@@ -9205,6 +11164,11 @@ async function main() {
9205
11164
  await runClaudeDesktopTap2({ backfill: subArgs.includes("--backfill") });
9206
11165
  break;
9207
11166
  }
11167
+ case "telemetry": {
11168
+ const { telemetryCommand: telemetryCommand2 } = await Promise.resolve().then(() => (init_telemetry2(), telemetry_exports2));
11169
+ await telemetryCommand2(subArgs);
11170
+ break;
11171
+ }
9208
11172
  default: {
9209
11173
  console.error(`Unknown command: ${cmd}`);
9210
11174
  printHelp2();
@@ -9212,8 +11176,50 @@ async function main() {
9212
11176
  }
9213
11177
  }
9214
11178
  }
9215
- main().catch((err) => {
11179
+ async function postDispatchFlush() {
11180
+ if (FLUSH_SKIP.has(cmd)) return;
11181
+ try {
11182
+ const { flushDetached: flushDetached2 } = await Promise.resolve().then(() => (init_flush(), flush_exports));
11183
+ flushDetached2();
11184
+ } catch {
11185
+ }
11186
+ }
11187
+ async function shutdown(code) {
11188
+ try {
11189
+ const { closeDb: closeDb2 } = await Promise.resolve().then(() => (init_db(), db_exports));
11190
+ await closeDb2();
11191
+ } catch {
11192
+ }
11193
+ process.exitCode = code;
11194
+ const force = setTimeout(() => {
11195
+ let pending = 0;
11196
+ const done = () => {
11197
+ if (--pending === 0) process.exit(code);
11198
+ };
11199
+ for (const stream of [process.stdout, process.stderr]) {
11200
+ if (stream.writableLength > 0) {
11201
+ pending++;
11202
+ stream.once("drain", done);
11203
+ }
11204
+ }
11205
+ if (pending === 0) process.exit(code);
11206
+ }, 200);
11207
+ force.unref();
11208
+ }
11209
+ main().then(() => postDispatchFlush()).then(() => shutdown(0)).catch(async (err) => {
11210
+ try {
11211
+ const { emit: emit2 } = await Promise.resolve().then(() => (init_telemetry(), telemetry_exports));
11212
+ emit2("error", {
11213
+ category: "unknown",
11214
+ code: err && err.code || "UNKNOWN",
11215
+ message: String(err && err.message ? err.message : err),
11216
+ stack: err && err.stack || "",
11217
+ emitter_context: "bootstrap:" + (cmd || "unknown")
11218
+ });
11219
+ } catch {
11220
+ }
11221
+ await postDispatchFlush();
9216
11222
  console.error(err);
9217
- process.exit(1);
11223
+ await shutdown(1);
9218
11224
  });
9219
11225
  //# sourceMappingURL=bootstrap.js.map