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