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