@synkro-sh/cli 1.7.94 → 1.8.0
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 +1782 -116
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -145,9 +145,9 @@ function hostnameHash() {
|
|
|
145
145
|
}
|
|
146
146
|
function getIdentity() {
|
|
147
147
|
if (cached2) return cached2;
|
|
148
|
-
let
|
|
148
|
+
let cliVersion2 = "0.0.0";
|
|
149
149
|
try {
|
|
150
|
-
|
|
150
|
+
cliVersion2 = "1.8.0";
|
|
151
151
|
} catch {
|
|
152
152
|
}
|
|
153
153
|
const creds = loadCredentialsIdentity();
|
|
@@ -157,7 +157,7 @@ function getIdentity() {
|
|
|
157
157
|
user_id: creds.user_id,
|
|
158
158
|
org_id: creds.org_id,
|
|
159
159
|
email: creds.email,
|
|
160
|
-
cli_version:
|
|
160
|
+
cli_version: cliVersion2,
|
|
161
161
|
platform: platform(),
|
|
162
162
|
hostname_hash: hostnameHash(),
|
|
163
163
|
node_version: process.version,
|
|
@@ -183,8 +183,8 @@ import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2 } fr
|
|
|
183
183
|
import { homedir as homedir3 } from "os";
|
|
184
184
|
import { join as join3 } from "path";
|
|
185
185
|
function deriveGit(cwd) {
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
186
|
+
const cached5 = gitCache.get(cwd);
|
|
187
|
+
if (cached5) return cached5;
|
|
188
188
|
const info = { branch: null, sha: null };
|
|
189
189
|
try {
|
|
190
190
|
const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
@@ -1777,6 +1777,27 @@ function uninstallCCHooks(settingsPath) {
|
|
|
1777
1777
|
writeSettingsAtomic(settingsPath, settings);
|
|
1778
1778
|
return true;
|
|
1779
1779
|
}
|
|
1780
|
+
function inspectCCHooks(settingsPath) {
|
|
1781
|
+
if (!existsSync11(settingsPath)) {
|
|
1782
|
+
return { installed: false, preToolUseBash: false, postToolUseEdit: false, sessionEnd: false, sessionStart: false };
|
|
1783
|
+
}
|
|
1784
|
+
const settings = readSettings(settingsPath);
|
|
1785
|
+
const pre = settings.hooks?.PreToolUse ?? [];
|
|
1786
|
+
const post = settings.hooks?.PostToolUse ?? [];
|
|
1787
|
+
const sessionEndHooks = settings.hooks?.SessionEnd ?? [];
|
|
1788
|
+
const sessionStartHooks = settings.hooks?.SessionStart ?? [];
|
|
1789
|
+
const preToolUseBash = pre.some((e) => e?.[SYNKRO_MARKER] === true);
|
|
1790
|
+
const postToolUseEdit = post.some((e) => e?.[SYNKRO_MARKER] === true);
|
|
1791
|
+
const sessionEnd = sessionEndHooks.some((e) => e?.[SYNKRO_MARKER] === true);
|
|
1792
|
+
const sessionStart = sessionStartHooks.some((e) => e?.[SYNKRO_MARKER] === true);
|
|
1793
|
+
return {
|
|
1794
|
+
installed: preToolUseBash || postToolUseEdit || sessionEnd || sessionStart,
|
|
1795
|
+
preToolUseBash,
|
|
1796
|
+
postToolUseEdit,
|
|
1797
|
+
sessionEnd,
|
|
1798
|
+
sessionStart
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1780
1801
|
var SYNKRO_MARKER, USAGE_PROXY_URL;
|
|
1781
1802
|
var init_ccHookConfig = __esm({
|
|
1782
1803
|
"cli/installer/ccHookConfig.ts"() {
|
|
@@ -1985,6 +2006,74 @@ function uninstallCursorHooks(hooksJsonPath) {
|
|
|
1985
2006
|
writeHooksFileAtomic(hooksJsonPath, file);
|
|
1986
2007
|
return true;
|
|
1987
2008
|
}
|
|
2009
|
+
function preToolUseUsesScript(hooks, scriptBasename) {
|
|
2010
|
+
return (hooks ?? []).some(
|
|
2011
|
+
(e) => isSynkroEntry2(e) && typeof e.command === "string" && e.command.includes(scriptBasename)
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
function inspectCursorHooks(hooksJsonPath) {
|
|
2015
|
+
let file;
|
|
2016
|
+
try {
|
|
2017
|
+
file = readHooksFile(hooksJsonPath);
|
|
2018
|
+
} catch {
|
|
2019
|
+
return {
|
|
2020
|
+
installed: false,
|
|
2021
|
+
sessionStart: false,
|
|
2022
|
+
sessionEnd: false,
|
|
2023
|
+
beforeSubmitPrompt: false,
|
|
2024
|
+
stop: false,
|
|
2025
|
+
beforeShellExecution: false,
|
|
2026
|
+
afterShellExecution: false,
|
|
2027
|
+
preToolUse: false,
|
|
2028
|
+
preToolUseBash: false,
|
|
2029
|
+
preToolUseEdit: false,
|
|
2030
|
+
preToolUseCwe: false,
|
|
2031
|
+
preToolUseCve: false,
|
|
2032
|
+
preToolUseAgent: false,
|
|
2033
|
+
preToolUsePlan: false,
|
|
2034
|
+
afterFileEdit: false,
|
|
2035
|
+
postToolUse: false,
|
|
2036
|
+
beforeMCPExecution: false
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
const h = file.hooks ?? {};
|
|
2040
|
+
const sessionStart = (h.sessionStart ?? []).some((e) => isSynkroEntry2(e));
|
|
2041
|
+
const sessionEnd = (h.sessionEnd ?? []).some((e) => isSynkroEntry2(e));
|
|
2042
|
+
const beforeSubmitPrompt = (h.beforeSubmitPrompt ?? []).some((e) => isSynkroEntry2(e));
|
|
2043
|
+
const stop = (h.stop ?? []).some((e) => isSynkroEntry2(e));
|
|
2044
|
+
const beforeShellExecution = (h.beforeShellExecution ?? []).some((e) => isSynkroEntry2(e));
|
|
2045
|
+
const afterShellExecution = (h.afterShellExecution ?? []).some((e) => isSynkroEntry2(e));
|
|
2046
|
+
const pre = h.preToolUse ?? [];
|
|
2047
|
+
const preToolUseBash = preToolUseUsesScript(pre, "cc-bash-judge") || preToolUseUsesScript(pre, "cursor-bash-judge");
|
|
2048
|
+
const preToolUseEdit = preToolUseUsesScript(pre, "cc-edit-precheck") || preToolUseUsesScript(pre, "cursor-edit-precheck");
|
|
2049
|
+
const preToolUseCwe = preToolUseUsesScript(pre, "cc-cwe-precheck");
|
|
2050
|
+
const preToolUseCve = preToolUseUsesScript(pre, "cc-cve-precheck");
|
|
2051
|
+
const preToolUseAgent = preToolUseUsesScript(pre, "cc-agent-judge");
|
|
2052
|
+
const preToolUsePlan = preToolUseUsesScript(pre, "cc-plan-judge");
|
|
2053
|
+
const preToolUse = preToolUseBash || preToolUseEdit || preToolUseCwe || preToolUseCve || preToolUseAgent || preToolUsePlan;
|
|
2054
|
+
const afterFileEdit = (h.afterFileEdit ?? []).some((e) => isSynkroEntry2(e));
|
|
2055
|
+
const postToolUse = (h.postToolUse ?? []).some((e) => isSynkroEntry2(e));
|
|
2056
|
+
const beforeMCPExecution = (h.beforeMCPExecution ?? []).some((e) => isSynkroEntry2(e));
|
|
2057
|
+
return {
|
|
2058
|
+
installed: sessionStart || sessionEnd || beforeSubmitPrompt || stop || beforeShellExecution || afterShellExecution || preToolUse || afterFileEdit || postToolUse || beforeMCPExecution,
|
|
2059
|
+
sessionStart,
|
|
2060
|
+
sessionEnd,
|
|
2061
|
+
beforeSubmitPrompt,
|
|
2062
|
+
stop,
|
|
2063
|
+
beforeShellExecution,
|
|
2064
|
+
afterShellExecution,
|
|
2065
|
+
preToolUse,
|
|
2066
|
+
preToolUseBash,
|
|
2067
|
+
preToolUseEdit,
|
|
2068
|
+
preToolUseCwe,
|
|
2069
|
+
preToolUseCve,
|
|
2070
|
+
preToolUseAgent,
|
|
2071
|
+
preToolUsePlan,
|
|
2072
|
+
afterFileEdit,
|
|
2073
|
+
postToolUse,
|
|
2074
|
+
beforeMCPExecution
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
1988
2077
|
var SYNKRO_MARKER2, ALLOWED_PARENT_DIRS, ALL_EVENTS;
|
|
1989
2078
|
var init_cursorHookConfig = __esm({
|
|
1990
2079
|
"cli/installer/cursorHookConfig.ts"() {
|
|
@@ -2128,6 +2217,52 @@ function uninstallCodexHooks(hooksJsonPath) {
|
|
|
2128
2217
|
writeHooksFileAtomic2(hooksJsonPath, file);
|
|
2129
2218
|
return true;
|
|
2130
2219
|
}
|
|
2220
|
+
function inspectCodexHooks(hooksJsonPath) {
|
|
2221
|
+
let file;
|
|
2222
|
+
try {
|
|
2223
|
+
file = readHooksFile2(hooksJsonPath);
|
|
2224
|
+
} catch {
|
|
2225
|
+
return {
|
|
2226
|
+
installed: false,
|
|
2227
|
+
preToolUse: false,
|
|
2228
|
+
preToolUseEdit: false,
|
|
2229
|
+
preToolUseCwe: false,
|
|
2230
|
+
preToolUseCve: false,
|
|
2231
|
+
postToolUse: false,
|
|
2232
|
+
postToolUseEdit: false,
|
|
2233
|
+
sessionStart: false,
|
|
2234
|
+
sessionEnd: false
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
const h = file.hooks ?? {};
|
|
2238
|
+
const preToolUse = (h.PreToolUse ?? []).some((e) => isSynkroEntry3(e));
|
|
2239
|
+
const editEntries = (h.PreToolUse ?? []).filter(
|
|
2240
|
+
(e) => typeof e?.matcher === "string" && (e.matcher.includes("apply_patch") || e.matcher.includes("ApplyPatch") || e.matcher.includes("Edit"))
|
|
2241
|
+
);
|
|
2242
|
+
const entryUsesScript = (needle) => editEntries.some(
|
|
2243
|
+
(entry) => (entry.hooks ?? []).some((handler) => String(handler?.command || "").includes(needle))
|
|
2244
|
+
);
|
|
2245
|
+
const preToolUseEdit = entryUsesScript("cc-edit-precheck");
|
|
2246
|
+
const preToolUseCwe = entryUsesScript("cc-cwe-precheck");
|
|
2247
|
+
const preToolUseCve = entryUsesScript("cc-cve-precheck");
|
|
2248
|
+
const postToolUse = (h.PostToolUse ?? []).some((e) => isSynkroEntry3(e));
|
|
2249
|
+
const postToolUseEdit = (h.PostToolUse ?? []).some(
|
|
2250
|
+
(entry) => entry.matcher === CODEX_EDIT_MATCHER && (entry.hooks ?? []).some((handler) => String(handler?.command || "").includes("cc-edit-followup"))
|
|
2251
|
+
);
|
|
2252
|
+
const sessionStart = (h.SessionStart ?? []).some((e) => isSynkroEntry3(e));
|
|
2253
|
+
const sessionEnd = (h.SessionEnd ?? []).some((e) => isSynkroEntry3(e));
|
|
2254
|
+
return {
|
|
2255
|
+
installed: preToolUseEdit && preToolUseCwe && preToolUseCve && postToolUseEdit,
|
|
2256
|
+
preToolUse,
|
|
2257
|
+
preToolUseEdit,
|
|
2258
|
+
preToolUseCwe,
|
|
2259
|
+
preToolUseCve,
|
|
2260
|
+
postToolUse,
|
|
2261
|
+
postToolUseEdit,
|
|
2262
|
+
sessionStart,
|
|
2263
|
+
sessionEnd
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2131
2266
|
var SYNKRO_MARKER3, CODEX_BASH_MATCHER, M_BASH, CODEX_EDIT_MATCHER, M_EDIT, M_AGENT, M_MCP, M_ACTIVATE, ALL_EVENTS2;
|
|
2132
2267
|
var init_codexHookConfig = __esm({
|
|
2133
2268
|
"cli/installer/codexHookConfig.ts"() {
|
|
@@ -2189,7 +2324,7 @@ function buildCodexHookTrustEdits(hooks) {
|
|
|
2189
2324
|
return [...edits.values()];
|
|
2190
2325
|
}
|
|
2191
2326
|
function queryCodexHookTrust(codexBinary = "codex", cwd = process.cwd(), autoTrust = false) {
|
|
2192
|
-
return new Promise((
|
|
2327
|
+
return new Promise((resolve7) => {
|
|
2193
2328
|
let settled = false;
|
|
2194
2329
|
let stdout = "";
|
|
2195
2330
|
let pending = "";
|
|
@@ -2207,7 +2342,7 @@ function queryCodexHookTrust(codexBinary = "codex", cwd = process.cwd(), autoTru
|
|
|
2207
2342
|
child.kill();
|
|
2208
2343
|
} catch {
|
|
2209
2344
|
}
|
|
2210
|
-
|
|
2345
|
+
resolve7(summary);
|
|
2211
2346
|
};
|
|
2212
2347
|
const timer = setTimeout(() => finish(null), 1e4);
|
|
2213
2348
|
try {
|
|
@@ -2763,9 +2898,9 @@ var init_hookScriptsTs = __esm({
|
|
|
2763
2898
|
"cli/installer/hookScriptsTs.ts"() {
|
|
2764
2899
|
"use strict";
|
|
2765
2900
|
STUB_COMMON_TS = String.raw`import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, statSync, realpathSync, openSync, readSync, closeSync } from 'node:fs';
|
|
2766
|
-
import { execSync } from 'node:child_process';
|
|
2901
|
+
import { execFileSync, execSync } from 'node:child_process';
|
|
2767
2902
|
import { homedir } from 'node:os';
|
|
2768
|
-
import { basename, join, resolve, relative, isAbsolute } from 'node:path';
|
|
2903
|
+
import { basename, dirname, join, resolve, relative, isAbsolute } from 'node:path';
|
|
2769
2904
|
import { randomUUID, createHash } from 'node:crypto';
|
|
2770
2905
|
|
|
2771
2906
|
const HOME = homedir();
|
|
@@ -3001,6 +3136,453 @@ function gitRoot(cwd: string): string {
|
|
|
3001
3136
|
} catch { return ''; }
|
|
3002
3137
|
}
|
|
3003
3138
|
|
|
3139
|
+
function taskRepoContext(root: string): any {
|
|
3140
|
+
if (!root) return undefined;
|
|
3141
|
+
try {
|
|
3142
|
+
const run = (args: string[]) => execFileSync('git', args, {
|
|
3143
|
+
cwd: root,
|
|
3144
|
+
timeout: 2000,
|
|
3145
|
+
encoding: 'utf-8',
|
|
3146
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
3147
|
+
}).trim();
|
|
3148
|
+
const branch = run(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
3149
|
+
const sha = run(['rev-parse', 'HEAD']);
|
|
3150
|
+
const remote = run(['remote', 'get-url', 'origin']);
|
|
3151
|
+
if (!branch || !sha || !remote) return undefined;
|
|
3152
|
+
let baseBranch = branch === 'HEAD' ? '' : branch;
|
|
3153
|
+
let baseSha = sha;
|
|
3154
|
+
try {
|
|
3155
|
+
const remoteHead = run(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD']);
|
|
3156
|
+
if (remoteHead.startsWith('origin/')) {
|
|
3157
|
+
baseBranch = remoteHead.slice('origin/'.length);
|
|
3158
|
+
baseSha = run(['rev-parse', remoteHead]);
|
|
3159
|
+
}
|
|
3160
|
+
} catch {}
|
|
3161
|
+
if (!baseBranch || baseBranch === branch) {
|
|
3162
|
+
for (const candidate of ['main', 'master']) {
|
|
3163
|
+
try {
|
|
3164
|
+
let candidateSha = '';
|
|
3165
|
+
try { candidateSha = run(['rev-parse', '--verify', 'refs/remotes/origin/' + candidate]); }
|
|
3166
|
+
catch { candidateSha = run(['rev-parse', '--verify', 'refs/heads/' + candidate]); }
|
|
3167
|
+
if (candidateSha) {
|
|
3168
|
+
baseBranch = candidate;
|
|
3169
|
+
baseSha = candidateSha;
|
|
3170
|
+
break;
|
|
3171
|
+
}
|
|
3172
|
+
} catch {
|
|
3173
|
+
continue;
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
if (!baseBranch || !baseSha) return undefined;
|
|
3178
|
+
let worktree = false;
|
|
3179
|
+
try { worktree = statSync(join(root, '.git')).isFile(); } catch {}
|
|
3180
|
+
const commonRoot = sharedRepoRoot(root);
|
|
3181
|
+
return { root, commonRoot, remote, branch, sha, baseBranch, baseSha, worktree };
|
|
3182
|
+
} catch { return undefined; }
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
function taskScmBlockResponse(harness: string, reason: string): string {
|
|
3186
|
+
const message = '[synkro:scm] ' + reason;
|
|
3187
|
+
if (harness === 'cursor') return JSON.stringify({ permission: 'deny', user_message: message, agent_message: message });
|
|
3188
|
+
return JSON.stringify({
|
|
3189
|
+
systemMessage: message,
|
|
3190
|
+
hookSpecificOutput: {
|
|
3191
|
+
hookEventName: 'PreToolUse',
|
|
3192
|
+
permissionDecision: 'deny',
|
|
3193
|
+
permissionDecisionReason: message,
|
|
3194
|
+
additionalContext: message + ' Resolve the repository state, then retry the tool call.',
|
|
3195
|
+
},
|
|
3196
|
+
});
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
function safeTaskBranch(value: string): boolean {
|
|
3200
|
+
return /^[a-z][a-z0-9]*-[0-9]+\/[a-z0-9][a-z0-9-]{0,47}$/.test(value);
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
function gitOutput(root: string, args: string[], timeout = 5000): string {
|
|
3204
|
+
return execFileSync('git', args, {
|
|
3205
|
+
cwd: root,
|
|
3206
|
+
timeout,
|
|
3207
|
+
encoding: 'utf-8',
|
|
3208
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
3209
|
+
}).trim();
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
function gitSucceeds(root: string, args: string[]): boolean {
|
|
3213
|
+
try { execFileSync('git', args, { cwd: root, timeout: 5000, stdio: 'ignore' }); return true; }
|
|
3214
|
+
catch { return false; }
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
function samePath(left: string, right: string): boolean {
|
|
3218
|
+
try { return realpathSync(left) === realpathSync(right); }
|
|
3219
|
+
catch { return resolve(left) === resolve(right); }
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
function sharedRepoRoot(root: string): string {
|
|
3223
|
+
const raw = gitOutput(root, ['rev-parse', '--git-common-dir']);
|
|
3224
|
+
const common = resolve(root, raw);
|
|
3225
|
+
if (basename(common) !== '.git') {
|
|
3226
|
+
throw new Error('task worktrees require a non-bare repository with shared Git metadata');
|
|
3227
|
+
}
|
|
3228
|
+
return dirname(common);
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
function taskWorktreePath(root: string, taskId: string): string {
|
|
3232
|
+
return join(sharedRepoRoot(root), '.synkro-worktrees', taskId);
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
function codexDesktopOrigin(): boolean {
|
|
3236
|
+
const origin = String(process.env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE || '').toLowerCase();
|
|
3237
|
+
const bundle = String(process.env.__CFBundleIdentifier || '').toLowerCase();
|
|
3238
|
+
return origin === 'codex desktop' || bundle === 'com.openai.codex';
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
function ignoreManagedWorktreeDirectory(root: string): void {
|
|
3242
|
+
const common = join(sharedRepoRoot(root), '.git');
|
|
3243
|
+
const excludePath = join(common, 'info', 'exclude');
|
|
3244
|
+
const entry = '.synkro-worktrees/';
|
|
3245
|
+
let existing = '';
|
|
3246
|
+
try { existing = readFileSync(excludePath, 'utf-8'); } catch {}
|
|
3247
|
+
if (existing.split(/\r?\n/).includes(entry)) return;
|
|
3248
|
+
mkdirSync(dirname(excludePath), { recursive: true });
|
|
3249
|
+
appendFileSync(excludePath, (existing && !existing.endsWith('\n') ? '\n' : '') + entry + '\n');
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
interface TaskWorktreeRecord { path: string; branch: string }
|
|
3253
|
+
|
|
3254
|
+
function taskWorktreeRecords(root: string): TaskWorktreeRecord[] {
|
|
3255
|
+
const records: TaskWorktreeRecord[] = [];
|
|
3256
|
+
let current: TaskWorktreeRecord = { path: '', branch: '' };
|
|
3257
|
+
const flush = () => {
|
|
3258
|
+
if (current.path) records.push(current);
|
|
3259
|
+
current = { path: '', branch: '' };
|
|
3260
|
+
};
|
|
3261
|
+
for (const line of (gitOutput(root, ['worktree', 'list', '--porcelain']) + '\n').split('\n')) {
|
|
3262
|
+
if (!line) { flush(); continue; }
|
|
3263
|
+
if (line.startsWith('worktree ')) current.path = line.slice('worktree '.length);
|
|
3264
|
+
else if (line.startsWith('branch refs/heads/')) current.branch = line.slice('branch refs/heads/'.length);
|
|
3265
|
+
}
|
|
3266
|
+
return records;
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
function gitOperationInProgress(root: string): boolean {
|
|
3270
|
+
if (gitSucceeds(root, ['rev-parse', '--verify', '--quiet', 'MERGE_HEAD'])) return true;
|
|
3271
|
+
for (const state of ['rebase-merge', 'rebase-apply']) {
|
|
3272
|
+
try {
|
|
3273
|
+
const raw = gitOutput(root, ['rev-parse', '--git-path', state]);
|
|
3274
|
+
const path = isAbsolute(raw) ? raw : resolve(root, raw);
|
|
3275
|
+
if (existsSync(path)) return true;
|
|
3276
|
+
} catch {}
|
|
3277
|
+
}
|
|
3278
|
+
return false;
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
function validateTaskWorktree(root: string, currentRoot: string, op: any): string {
|
|
3282
|
+
const expected = taskWorktreePath(root, String(op.taskId || ''));
|
|
3283
|
+
const supplied = String(op.worktreePath || '');
|
|
3284
|
+
const managed = Boolean(supplied) && samePath(supplied, expected);
|
|
3285
|
+
const nativeCurrent = Boolean(supplied) && samePath(supplied, currentRoot);
|
|
3286
|
+
if (!managed && !nativeCurrent) {
|
|
3287
|
+
throw new Error('task worktree path does not match the managed or current native workspace');
|
|
3288
|
+
}
|
|
3289
|
+
const record = taskWorktreeRecords(root).find((item) => samePath(item.path, supplied));
|
|
3290
|
+
if (!record || record.branch !== String(op.branchName || '')) {
|
|
3291
|
+
throw new Error('task worktree is stale or no longer owns the canonical branch');
|
|
3292
|
+
}
|
|
3293
|
+
if (gitOutput(supplied, ['rev-parse', '--abbrev-ref', 'HEAD']) !== String(op.branchName || '')) {
|
|
3294
|
+
throw new Error('task worktree is not on the canonical branch');
|
|
3295
|
+
}
|
|
3296
|
+
return supplied;
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
async function completeTaskScm(sessionId: string, op: any, ok: boolean, error: string, worktreePath: string): Promise<void> {
|
|
3300
|
+
try {
|
|
3301
|
+
await fetch('http://127.0.0.1:' + PORT + '/api/local/task-scm/complete', {
|
|
3302
|
+
method: 'POST',
|
|
3303
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + loadMcpJwt() },
|
|
3304
|
+
body: JSON.stringify({ taskId: op.taskId, sessionId, kind: op.kind, ok, error, worktreePath }),
|
|
3305
|
+
signal: AbortSignal.timeout(5000),
|
|
3306
|
+
});
|
|
3307
|
+
} catch {}
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
function executeTaskScm(root: string, op: any): string {
|
|
3311
|
+
if (!op || typeof op !== 'object') throw new Error('invalid SCM operation');
|
|
3312
|
+
if (!/^task_[a-z0-9]{8}$/i.test(String(op.taskId || ''))) throw new Error('invalid task id');
|
|
3313
|
+
const branch = String(op.branchName || '');
|
|
3314
|
+
if (!safeTaskBranch(branch)) throw new Error('invalid canonical task branch');
|
|
3315
|
+
const boundRoot = String(op.repoRoot || '');
|
|
3316
|
+
if (!boundRoot || (!samePath(boundRoot, root) && !samePath(sharedRepoRoot(boundRoot), sharedRepoRoot(root)))) {
|
|
3317
|
+
throw new Error('task workspace does not match this repository');
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
if (op.kind === 'branch') {
|
|
3321
|
+
if (op.prepareHandoffBranch === true) {
|
|
3322
|
+
if (gitOperationInProgress(root)) {
|
|
3323
|
+
throw new Error('finish the active merge or rebase before task automation');
|
|
3324
|
+
}
|
|
3325
|
+
const originalBranch = gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
3326
|
+
const head = gitOutput(root, ['rev-parse', 'HEAD']);
|
|
3327
|
+
if (!op.startSha || head !== String(op.startSha)) {
|
|
3328
|
+
throw new Error('Codex local checkout HEAD does not match the task creation revision');
|
|
3329
|
+
}
|
|
3330
|
+
if (gitSucceeds(boundRoot, ['show-ref', '--verify', '--quiet', 'refs/heads/' + branch])) {
|
|
3331
|
+
throw new Error('the canonical task branch already exists in another workspace');
|
|
3332
|
+
}
|
|
3333
|
+
if (!op.baseSha || !gitSucceeds(boundRoot, ['cat-file', '-e', String(op.baseSha) + '^{commit}'])) {
|
|
3334
|
+
throw new Error('task base revision is unavailable in this repository');
|
|
3335
|
+
}
|
|
3336
|
+
gitOutput(root, ['branch', branch, String(op.baseSha)], 30000);
|
|
3337
|
+
if (gitOutput(root, ['rev-parse', 'HEAD']) !== head
|
|
3338
|
+
|| gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']) !== originalBranch) {
|
|
3339
|
+
throw new Error('Git changed the shared checkout while reserving the task branch');
|
|
3340
|
+
}
|
|
3341
|
+
return '';
|
|
3342
|
+
}
|
|
3343
|
+
if (op.adoptExistingWorktree === true) {
|
|
3344
|
+
const supplied = String(op.worktreePath || '');
|
|
3345
|
+
if (!supplied || !samePath(root, supplied)) {
|
|
3346
|
+
throw new Error('Codex native worktree does not match the current session workspace');
|
|
3347
|
+
}
|
|
3348
|
+
const record = taskWorktreeRecords(boundRoot).find((item) => samePath(item.path, root));
|
|
3349
|
+
if (!record) throw new Error('Codex native worktree is not registered with Git');
|
|
3350
|
+
if (gitOperationInProgress(root)) {
|
|
3351
|
+
throw new Error('finish the active merge or rebase before task automation');
|
|
3352
|
+
}
|
|
3353
|
+
if (gitOutput(root, ['status', '--porcelain'])) {
|
|
3354
|
+
throw new Error('Codex native worktree must be clean before it can be assigned to a new task');
|
|
3355
|
+
}
|
|
3356
|
+
const head = gitOutput(root, ['rev-parse', 'HEAD']);
|
|
3357
|
+
if (!op.startSha || head !== String(op.startSha)) {
|
|
3358
|
+
throw new Error('Codex native worktree HEAD does not match the task creation revision');
|
|
3359
|
+
}
|
|
3360
|
+
const currentBranch = gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
3361
|
+
if (currentBranch !== branch) {
|
|
3362
|
+
if (gitSucceeds(boundRoot, ['show-ref', '--verify', '--quiet', 'refs/heads/' + branch])) {
|
|
3363
|
+
throw new Error('the canonical task branch already exists in another workspace');
|
|
3364
|
+
}
|
|
3365
|
+
if (!op.baseSha || !gitSucceeds(boundRoot, ['cat-file', '-e', String(op.baseSha) + '^{commit}'])) {
|
|
3366
|
+
throw new Error('task base revision is unavailable in this repository');
|
|
3367
|
+
}
|
|
3368
|
+
gitOutput(root, ['switch', '-c', branch, String(op.baseSha)], 30000);
|
|
3369
|
+
}
|
|
3370
|
+
if (gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']) !== branch) {
|
|
3371
|
+
throw new Error('Codex native worktree did not adopt the canonical task branch');
|
|
3372
|
+
}
|
|
3373
|
+
return root;
|
|
3374
|
+
}
|
|
3375
|
+
const worktreePath = taskWorktreePath(boundRoot, String(op.taskId));
|
|
3376
|
+
const existing = taskWorktreeRecords(boundRoot).find((item) => samePath(item.path, worktreePath));
|
|
3377
|
+
if (existing) {
|
|
3378
|
+
if (existing.branch !== branch || gitOutput(worktreePath, ['rev-parse', '--abbrev-ref', 'HEAD']) !== branch) {
|
|
3379
|
+
throw new Error('managed task worktree exists on a conflicting branch');
|
|
3380
|
+
}
|
|
3381
|
+
return worktreePath;
|
|
3382
|
+
}
|
|
3383
|
+
if (existsSync(worktreePath)) {
|
|
3384
|
+
throw new Error('managed task worktree path exists but is not registered with Git');
|
|
3385
|
+
}
|
|
3386
|
+
if (gitOperationInProgress(root)) {
|
|
3387
|
+
throw new Error('finish the active merge or rebase before task automation');
|
|
3388
|
+
}
|
|
3389
|
+
const head = gitOutput(root, ['rev-parse', 'HEAD']);
|
|
3390
|
+
if (!op.startSha || head !== String(op.startSha)) {
|
|
3391
|
+
throw new Error('repository HEAD changed after task creation');
|
|
3392
|
+
}
|
|
3393
|
+
if (gitSucceeds(root, ['show-ref', '--verify', '--quiet', 'refs/heads/' + branch])) {
|
|
3394
|
+
throw new Error('the canonical task branch already exists in another workspace');
|
|
3395
|
+
}
|
|
3396
|
+
if (!op.baseSha) throw new Error('task base revision is missing');
|
|
3397
|
+
if (!gitSucceeds(root, ['cat-file', '-e', String(op.baseSha) + '^{commit}'])) {
|
|
3398
|
+
throw new Error('task base revision is unavailable in this repository');
|
|
3399
|
+
}
|
|
3400
|
+
const originalBranch = gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
3401
|
+
ignoreManagedWorktreeDirectory(boundRoot);
|
|
3402
|
+
mkdirSync(dirname(worktreePath), { recursive: true });
|
|
3403
|
+
gitOutput(root, ['worktree', 'add', '-b', branch, worktreePath, String(op.baseSha)], 30000);
|
|
3404
|
+
if (gitOutput(root, ['rev-parse', 'HEAD']) !== head
|
|
3405
|
+
|| gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']) !== originalBranch) {
|
|
3406
|
+
throw new Error('Git changed the originating checkout while creating the task worktree');
|
|
3407
|
+
}
|
|
3408
|
+
const record = taskWorktreeRecords(boundRoot).find((item) => samePath(item.path, worktreePath));
|
|
3409
|
+
if (!record || record.branch !== branch
|
|
3410
|
+
|| gitOutput(worktreePath, ['rev-parse', '--abbrev-ref', 'HEAD']) !== branch) {
|
|
3411
|
+
throw new Error('Git did not register the canonical task worktree');
|
|
3412
|
+
}
|
|
3413
|
+
return worktreePath;
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
if (op.kind !== 'push') throw new Error('unknown SCM operation');
|
|
3417
|
+
const worktreePath = validateTaskWorktree(boundRoot, root, op);
|
|
3418
|
+
if (gitOperationInProgress(worktreePath)) {
|
|
3419
|
+
throw new Error('finish the active merge or rebase in the task worktree before pushing');
|
|
3420
|
+
}
|
|
3421
|
+
if (gitOutput(worktreePath, ['status', '--porcelain'])) {
|
|
3422
|
+
gitOutput(worktreePath, ['add', '-A']);
|
|
3423
|
+
if (!gitSucceeds(worktreePath, ['diff', '--cached', '--quiet'])) {
|
|
3424
|
+
gitOutput(worktreePath, ['commit', '-m', String(op.commitMessage || 'Synkro task')], 30000);
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
gitOutput(worktreePath, ['push', '-u', 'origin', branch], 60000);
|
|
3428
|
+
return worktreePath;
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
interface TaskScmReconcileResult { reason: string; context: string }
|
|
3432
|
+
|
|
3433
|
+
function taskScmWorkspaceContext(workspace: any): string {
|
|
3434
|
+
if (!workspace || typeof workspace !== 'object') return '';
|
|
3435
|
+
const taskId = String(workspace.taskId || '');
|
|
3436
|
+
const linearRef = String(workspace.linearRef || '');
|
|
3437
|
+
const branchName = String(workspace.branchName || '');
|
|
3438
|
+
const worktreePath = String(workspace.worktreePath || '');
|
|
3439
|
+
if (!taskId || !branchName) return '';
|
|
3440
|
+
return '[synkro:task-workspace] task=' + taskId
|
|
3441
|
+
+ (linearRef ? ' linear=' + linearRef : '')
|
|
3442
|
+
+ ' branch=' + branchName
|
|
3443
|
+
+ (worktreePath ? ' worktree=' + worktreePath : ' worktree=pending-native-handoff');
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
function shellTaskWorkspaceArg(value: string): string {
|
|
3447
|
+
return "'" + value.replace(/'/g, "'\"'\"'") + "'";
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
function taskScmWorkspaceInstruction(harness: string, sessionId: string, workspace: any): string {
|
|
3451
|
+
const context = taskScmWorkspaceContext(workspace);
|
|
3452
|
+
const worktreePath = String(workspace?.worktreePath || '');
|
|
3453
|
+
const branchName = String(workspace?.branchName || '');
|
|
3454
|
+
if (!context || !branchName) return '';
|
|
3455
|
+
const requirement = 'Do not continue in the shared checkout, use a per-command workdir, prefix commands with cd, '
|
|
3456
|
+
+ 'or ask the user to perform the transition. Synkro keeps substantive tools blocked until this exact session reports '
|
|
3457
|
+
+ 'the task worktree and canonical branch.';
|
|
3458
|
+
if (harness === 'cc') {
|
|
3459
|
+
if (!worktreePath) return context + '\n[synkro:workspace-handoff] Task worktree creation failed closed; retry workspace preparation.';
|
|
3460
|
+
return context + '\n[synkro:workspace-handoff] Invoke Claude Code EnterWorktree now for the existing worktree '
|
|
3461
|
+
+ JSON.stringify(worktreePath) + ' on branch ' + JSON.stringify(branchName) + '. ' + requirement;
|
|
3462
|
+
}
|
|
3463
|
+
if (harness === 'cursor') {
|
|
3464
|
+
if (!worktreePath) return context + '\n[synkro:workspace-handoff] Task worktree creation failed closed; retry workspace preparation.';
|
|
3465
|
+
return context + '\n[synkro:workspace-handoff] Resume this exact Cursor chat in the existing task worktree now: cursor-agent --resume '
|
|
3466
|
+
+ shellTaskWorkspaceArg(sessionId) + ' --workspace ' + shellTaskWorkspaceArg(worktreePath)
|
|
3467
|
+
+ '. Confirm the resumed workspace is on branch ' + JSON.stringify(branchName) + '. ' + requirement;
|
|
3468
|
+
}
|
|
3469
|
+
if (!worktreePath) {
|
|
3470
|
+
return context + '\n[synkro:workspace-handoff] Invoke Codex\'s native task/worktree Handoff now for this exact task. '
|
|
3471
|
+
+ 'Check out branch ' + JSON.stringify(branchName) + ' in a new worktree and continue this same chat there. '
|
|
3472
|
+
+ 'Do not switch the shared Local checkout or ask the user to switch branches. '
|
|
3473
|
+
+ 'Synkro keeps substantive tools blocked until this exact session reports the new worktree path and canonical branch.';
|
|
3474
|
+
}
|
|
3475
|
+
return context + '\n[synkro:workspace-handoff] Invoke Codex\'s native task/worktree Handoff now for this exact task, '
|
|
3476
|
+
+ 'using the existing worktree ' + JSON.stringify(worktreePath) + ' on branch ' + JSON.stringify(branchName) + '. '
|
|
3477
|
+
+ 'The Codex Environment panel must show that worktree and branch before retrying the blocked action. ' + requirement;
|
|
3478
|
+
}
|
|
3479
|
+
|
|
3480
|
+
function withTaskScmContext(responseText: string, harness: string, context: string): string {
|
|
3481
|
+
if (!context) return responseText;
|
|
3482
|
+
try {
|
|
3483
|
+
const response = JSON.parse(responseText || '{}') as any;
|
|
3484
|
+
if (harness === 'cursor') {
|
|
3485
|
+
response.agent_message = [response.agent_message, context].filter(Boolean).join('\n');
|
|
3486
|
+
return JSON.stringify(response);
|
|
3487
|
+
}
|
|
3488
|
+
response.systemMessage = [response.systemMessage, context].filter(Boolean).join('\n');
|
|
3489
|
+
response.hookSpecificOutput = response.hookSpecificOutput || { hookEventName: 'PreToolUse' };
|
|
3490
|
+
response.hookSpecificOutput.additionalContext = [response.hookSpecificOutput.additionalContext, context]
|
|
3491
|
+
.filter(Boolean).join('\n');
|
|
3492
|
+
return JSON.stringify(response);
|
|
3493
|
+
} catch { return responseText; }
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
async function reconcileTaskScm(
|
|
3497
|
+
root: string,
|
|
3498
|
+
sessionId: string,
|
|
3499
|
+
harness: string,
|
|
3500
|
+
payload?: any,
|
|
3501
|
+
): Promise<TaskScmReconcileResult> {
|
|
3502
|
+
if (!root || !sessionId) return { reason: '', context: '' };
|
|
3503
|
+
try {
|
|
3504
|
+
let branch = '';
|
|
3505
|
+
let repoRoot = '';
|
|
3506
|
+
let isWorktree = false;
|
|
3507
|
+
try { branch = gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']); } catch {}
|
|
3508
|
+
try { repoRoot = sharedRepoRoot(root); } catch {}
|
|
3509
|
+
try { isWorktree = statSync(join(root, '.git')).isFile(); } catch {}
|
|
3510
|
+
const nativeCodexWorktree = harness === 'codex' && codexDesktopOrigin();
|
|
3511
|
+
const parentSessionId = codexForkedFromSession(payload, harness, sessionId);
|
|
3512
|
+
const claimWorkspace = (reportedBranch: string) => fetch(
|
|
3513
|
+
'http://127.0.0.1:' + PORT + '/api/local/task-scm/claim', {
|
|
3514
|
+
method: 'POST',
|
|
3515
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + loadMcpJwt() },
|
|
3516
|
+
body: JSON.stringify({
|
|
3517
|
+
sessionId, cwd: root, repoRoot, branch: reportedBranch, harness,
|
|
3518
|
+
nativeCodexWorktree, isWorktree, parentSessionId,
|
|
3519
|
+
}),
|
|
3520
|
+
signal: AbortSignal.timeout(5000),
|
|
3521
|
+
},
|
|
3522
|
+
);
|
|
3523
|
+
const claim = await claimWorkspace(branch);
|
|
3524
|
+
if (!claim.ok) return { reason: '', context: '' };
|
|
3525
|
+
let result = await claim.json() as any;
|
|
3526
|
+
for (let attempt = 0; result?.retry === true && attempt < 50; attempt++) {
|
|
3527
|
+
await new Promise((resolveRetry) => setTimeout(resolveRetry, 100));
|
|
3528
|
+
try { branch = gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']); } catch {}
|
|
3529
|
+
const retried = await claimWorkspace(branch);
|
|
3530
|
+
if (!retried.ok) break;
|
|
3531
|
+
result = await retried.json() as any;
|
|
3532
|
+
}
|
|
3533
|
+
if (!result?.op) {
|
|
3534
|
+
const workspace = result?.workspace && typeof result.workspace === 'object'
|
|
3535
|
+
? { ...result.workspace }
|
|
3536
|
+
: result?.workspace;
|
|
3537
|
+
return {
|
|
3538
|
+
reason: result?.waiting
|
|
3539
|
+
? (taskScmWorkspaceInstruction(harness, sessionId, workspace)
|
|
3540
|
+
|| String(result.reason || 'task source-control preparation is pending'))
|
|
3541
|
+
: '',
|
|
3542
|
+
context: taskScmWorkspaceContext(workspace),
|
|
3543
|
+
};
|
|
3544
|
+
}
|
|
3545
|
+
try {
|
|
3546
|
+
const worktreePath = executeTaskScm(root, result.op);
|
|
3547
|
+
await completeTaskScm(sessionId, result.op, true, '', worktreePath);
|
|
3548
|
+
if (result.op.kind === 'branch') {
|
|
3549
|
+
if (result.op.adoptExistingWorktree === true) {
|
|
3550
|
+
const rebound = await claimWorkspace(String(result.op.branchName || ''));
|
|
3551
|
+
if (rebound.ok) {
|
|
3552
|
+
const reboundResult = await rebound.json() as any;
|
|
3553
|
+
if (!reboundResult?.waiting && reboundResult?.workspace) {
|
|
3554
|
+
return { reason: '', context: taskScmWorkspaceContext(reboundResult.workspace) };
|
|
3555
|
+
}
|
|
3556
|
+
if (reboundResult?.waiting) {
|
|
3557
|
+
return {
|
|
3558
|
+
reason: String(reboundResult.reason || 'Codex native worktree binding is pending'),
|
|
3559
|
+
context: taskScmWorkspaceContext(reboundResult.workspace),
|
|
3560
|
+
};
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
return { reason: 'Codex native worktree branch was created but task binding is still pending', context: '' };
|
|
3564
|
+
}
|
|
3565
|
+
const workspace = {
|
|
3566
|
+
taskId: result.op.taskId,
|
|
3567
|
+
linearRef: result.op.linearRef,
|
|
3568
|
+
branchName: result.op.branchName,
|
|
3569
|
+
worktreePath,
|
|
3570
|
+
bindingState: 'handoff_pending',
|
|
3571
|
+
};
|
|
3572
|
+
return {
|
|
3573
|
+
reason: taskScmWorkspaceInstruction(harness, sessionId, workspace),
|
|
3574
|
+
context: '',
|
|
3575
|
+
};
|
|
3576
|
+
}
|
|
3577
|
+
return { reason: '', context: '' };
|
|
3578
|
+
} catch (error) {
|
|
3579
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3580
|
+
await completeTaskScm(sessionId, result.op, false, message, root);
|
|
3581
|
+
return { reason: message, context: '' };
|
|
3582
|
+
}
|
|
3583
|
+
} catch { return { reason: '', context: '' }; }
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3004
3586
|
// Once-per-session onboarding hint for repos with no synkro.toml file.
|
|
3005
3587
|
function noSynkroHint(sessionId: string): string | null {
|
|
3006
3588
|
const dir = join(HOME, '.synkro', '.no-synkro-hint');
|
|
@@ -3022,13 +3604,11 @@ function filePathFromToolInput(ti: any): string {
|
|
|
3022
3604
|
// The container reconstructs the proposed post-edit content by finding the edit's old_string
|
|
3023
3605
|
// inside baseContent, then computes the changed line-range + per-requirement file snapshot off
|
|
3024
3606
|
// it. The old flat 64KB HEAD silently truncated any edit past ~the first 1000 lines of a big
|
|
3025
|
-
// file (e.g. an 890KB server file): old_string wasn't present,
|
|
3026
|
-
//
|
|
3027
|
-
//
|
|
3028
|
-
//
|
|
3029
|
-
//
|
|
3030
|
-
// keys off — are preserved) instead of the head. This also fixes CWE/CVE scans that previously
|
|
3031
|
-
// never saw anything below 64KB in a large file.
|
|
3607
|
+
// file (e.g. an 890KB server file): old_string wasn't present, so reconstruction and the durable
|
|
3608
|
+
// edit fact were incomplete. Ship the WHOLE file (covers virtually all source, incl. large
|
|
3609
|
+
// single-file servers) so reconstruction, audit diffs, and policy/security scans are exact. Only
|
|
3610
|
+
// a pathological >2MB file falls back to a line-aligned WINDOW around the edit (front-padded with
|
|
3611
|
+
// newlines so ABSOLUTE line numbers are preserved) instead of the head.
|
|
3032
3612
|
const BASE_CONTENT_WHOLE_CAP = 2000000;
|
|
3033
3613
|
function editAnchorString(ti: any): string {
|
|
3034
3614
|
if (!ti || typeof ti !== 'object') return '';
|
|
@@ -3436,11 +4016,19 @@ function readJsonlHeader(path: string): string {
|
|
|
3436
4016
|
let fd = -1;
|
|
3437
4017
|
try {
|
|
3438
4018
|
fd = openSync(path, 'r');
|
|
3439
|
-
const
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
4019
|
+
const chunks: Buffer[] = [];
|
|
4020
|
+
let position = 0;
|
|
4021
|
+
while (position < 2 * 1024 * 1024) {
|
|
4022
|
+
const buf = Buffer.alloc(Math.min(65536, 2 * 1024 * 1024 - position));
|
|
4023
|
+
const count = readSync(fd, buf, 0, buf.length, position);
|
|
4024
|
+
if (count <= 0) break;
|
|
4025
|
+
const chunk = buf.subarray(0, count);
|
|
4026
|
+
const newline = chunk.indexOf(10);
|
|
4027
|
+
chunks.push(newline >= 0 ? chunk.subarray(0, newline) : chunk);
|
|
4028
|
+
position += count;
|
|
4029
|
+
if (newline >= 0) break;
|
|
4030
|
+
}
|
|
4031
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
3444
4032
|
} catch { return ''; }
|
|
3445
4033
|
finally { if (fd >= 0) { try { closeSync(fd); } catch {} } }
|
|
3446
4034
|
}
|
|
@@ -3518,6 +4106,37 @@ export function resolveHookTranscriptPath(payload: any, harness: string, session
|
|
|
3518
4106
|
return found;
|
|
3519
4107
|
}
|
|
3520
4108
|
|
|
4109
|
+
function codexForkedFromSession(payload: any, harness: string, sessionId: string): string {
|
|
4110
|
+
if (harness !== 'codex' || !safeCodexSessionId(sessionId)) return '';
|
|
4111
|
+
const cachePath = join(
|
|
4112
|
+
HOME,
|
|
4113
|
+
'.synkro',
|
|
4114
|
+
'codex-parent-' + createHash('sha256').update(sessionId).digest('hex').slice(0, 16),
|
|
4115
|
+
);
|
|
4116
|
+
try {
|
|
4117
|
+
const cached = readFileSync(cachePath, 'utf-8').trim();
|
|
4118
|
+
if (cached === '-') return '';
|
|
4119
|
+
if (safeCodexSessionId(cached)) return cached;
|
|
4120
|
+
} catch {}
|
|
4121
|
+
|
|
4122
|
+
let parent = '';
|
|
4123
|
+
const transcriptPath = resolveHookTranscriptPath(payload, harness, sessionId);
|
|
4124
|
+
if (transcriptPath) {
|
|
4125
|
+
try {
|
|
4126
|
+
const first = JSON.parse(readJsonlHeader(transcriptPath));
|
|
4127
|
+
const candidate = String(first?.payload?.forked_from_id || first?.payload?.forkedFromId || '');
|
|
4128
|
+
if (first?.type === 'session_meta' && safeCodexSessionId(candidate) && candidate !== sessionId) {
|
|
4129
|
+
parent = candidate;
|
|
4130
|
+
}
|
|
4131
|
+
} catch {}
|
|
4132
|
+
}
|
|
4133
|
+
try {
|
|
4134
|
+
mkdirSync(join(HOME, '.synkro'), { recursive: true });
|
|
4135
|
+
writeFileSync(cachePath, parent || '-', { encoding: 'utf-8', mode: 0o600 });
|
|
4136
|
+
} catch {}
|
|
4137
|
+
return parent;
|
|
4138
|
+
}
|
|
4139
|
+
|
|
3521
4140
|
export async function runStub(surface: string, opts: StubOpts = {}): Promise<void> {
|
|
3522
4141
|
const harness = isCursor(opts.harness) ? 'cursor' : isCodex(opts.harness) ? 'codex' : 'cc';
|
|
3523
4142
|
const startedAt = Date.now();
|
|
@@ -3564,6 +4183,7 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
|
|
|
3564
4183
|
const cwd = (typeof payload.cwd === 'string' && payload.cwd) || workspaceRoots[0] || '';
|
|
3565
4184
|
const sessionId = String(payload.session_id || payload.conversation_id || '');
|
|
3566
4185
|
const root = gitRoot(cwd);
|
|
4186
|
+
telemCwd = root || cwd;
|
|
3567
4187
|
|
|
3568
4188
|
// Dormancy: a repo is onboarded only if it has a synkro.toml FILE at its git
|
|
3569
4189
|
// root. Guard root !== HOME — ~/.synkro is the config DIRECTORY, so without
|
|
@@ -3598,6 +4218,13 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
|
|
|
3598
4218
|
return;
|
|
3599
4219
|
}
|
|
3600
4220
|
|
|
4221
|
+
const scm = await reconcileTaskScm(root || cwd, sessionId, harness, payload);
|
|
4222
|
+
const substantiveTool = /^(?:Bash|Edit|Write|MultiEdit|NotebookEdit|apply_patch|file_change)$/i.test(String(payload.tool_name || ''));
|
|
4223
|
+
if (scm.reason && substantiveTool) {
|
|
4224
|
+
out(taskScmBlockResponse(harness, scm.reason));
|
|
4225
|
+
return;
|
|
4226
|
+
}
|
|
4227
|
+
|
|
3601
4228
|
if (harness === 'codex' && opts.needsFile && payload.__synkro_codex_patch_error) {
|
|
3602
4229
|
const reason = String(payload.__synkro_codex_patch_error);
|
|
3603
4230
|
out(JSON.stringify({
|
|
@@ -3613,7 +4240,17 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
|
|
|
3613
4240
|
}
|
|
3614
4241
|
|
|
3615
4242
|
// Gather host-only inputs the container can't read.
|
|
3616
|
-
const envelope: any = {
|
|
4243
|
+
const envelope: any = {
|
|
4244
|
+
payload,
|
|
4245
|
+
harness,
|
|
4246
|
+
cwd: root || cwd,
|
|
4247
|
+
sessionId,
|
|
4248
|
+
synkroFileText,
|
|
4249
|
+
taskWorkspaceContext: scm.context || undefined,
|
|
4250
|
+
};
|
|
4251
|
+
// Task creation is the one moment the container needs a stable Git snapshot.
|
|
4252
|
+
// Capture it on the MCP gate only; ordinary tool hooks avoid extra Git calls.
|
|
4253
|
+
if (surface === 'mcp-gate') envelope.repoContext = taskRepoContext(root || cwd);
|
|
3617
4254
|
|
|
3618
4255
|
if (opts.needsFile) {
|
|
3619
4256
|
const normalizedEdits = harness === 'codex' && Array.isArray(payload.__synkro_codex_edits)
|
|
@@ -3694,15 +4331,10 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
|
|
|
3694
4331
|
envelope.planText = String(ti.plan || ti.content || payload.plan || '');
|
|
3695
4332
|
}
|
|
3696
4333
|
|
|
3697
|
-
//
|
|
3698
|
-
// result (the server waits up to 30s for the completion grade), so it needs a
|
|
3699
|
-
// wait above that budget — not the 6s telemetry default — or the stub abandons
|
|
3700
|
-
// the request before the result lands and the completion shows up a hook late.
|
|
3701
|
-
// prompt-submit is the opposite: it's INTERACTIVE (blocks the user's prompt from
|
|
4334
|
+
// prompt-submit is INTERACTIVE (blocks the user's prompt from
|
|
3702
4335
|
// being sent) under a short 5s hook timeout, so it gets ONE short attempt (below)
|
|
3703
4336
|
// and a tight per-attempt budget that fits inside 5s — never the 6s×3 telemetry path.
|
|
3704
4337
|
const timeoutMs = surface === 'cwe-precheck' ? 48000
|
|
3705
|
-
: surface === 'bash-followup' ? 32000
|
|
3706
4338
|
: surface === 'prompt-submit' ? 3500
|
|
3707
4339
|
: (opts.telemetry ? 6000 : 28000);
|
|
3708
4340
|
// Cloud has no local container to reach. Post the SAME envelope to the org's grader
|
|
@@ -3727,8 +4359,8 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
|
|
|
3727
4359
|
// latency: retry a few times with backoff. Right after a server restart the
|
|
3728
4360
|
// event loop can be briefly busy / not-yet-listening and a single POST silently
|
|
3729
4361
|
// drops the event (this was the followup flakiness). Double-delivery is safe here
|
|
3730
|
-
//
|
|
3731
|
-
//
|
|
4362
|
+
// because the server deduplicates durable hook events and native evidence.
|
|
4363
|
+
// Blocking hooks make ONE attempt: their response IS the verdict
|
|
3732
4364
|
// and a retry would double-grade. Normal case: first attempt wins, no delay.
|
|
3733
4365
|
// prompt-submit is EXCLUDED from the retry: it runs on the interactive prompt
|
|
3734
4366
|
// path under a 5s hook timeout, and 3×6s+backoff (19.5s) blew that budget and
|
|
@@ -3744,9 +4376,13 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
|
|
|
3744
4376
|
if (i < attempts - 1) await new Promise((r) => setTimeout(r, 500 * (i + 1)));
|
|
3745
4377
|
}
|
|
3746
4378
|
const rawResponseText = text || failOpen(harness);
|
|
3747
|
-
|
|
4379
|
+
// A completion-report sync can arm the push after the pre-scan reconciliation ran.
|
|
4380
|
+
// Reconcile once more so the same Stop/PostToolUse hook can push immediately.
|
|
4381
|
+
await reconcileTaskScm(root || cwd, sessionId, harness, payload);
|
|
4382
|
+
const contractResponseText = opts.stopContract && harness === 'codex'
|
|
3748
4383
|
? codexStopResponse(rawResponseText)
|
|
3749
4384
|
: rawResponseText;
|
|
4385
|
+
const responseText = withTaskScmContext(contractResponseText, harness, scm.context);
|
|
3750
4386
|
out(responseText);
|
|
3751
4387
|
emitStubTelemetry(surface, harness, telemPayload, responseText, Date.now() - startedAt, telemCwd, telemSessionId);
|
|
3752
4388
|
} catch (err) {
|
|
@@ -4129,7 +4765,7 @@ function createCallbackServer() {
|
|
|
4129
4765
|
"Access-Control-Allow-Headers": "Content-Type",
|
|
4130
4766
|
"Vary": "Origin"
|
|
4131
4767
|
};
|
|
4132
|
-
return new Promise((
|
|
4768
|
+
return new Promise((resolve7, reject) => {
|
|
4133
4769
|
const server = createServer((req, res) => {
|
|
4134
4770
|
if (req.method === "OPTIONS") {
|
|
4135
4771
|
const origin = req.headers.origin;
|
|
@@ -4218,7 +4854,7 @@ function createCallbackServer() {
|
|
|
4218
4854
|
res.end(JSON.stringify({ ok: true }));
|
|
4219
4855
|
setTimeout(() => {
|
|
4220
4856
|
server.close();
|
|
4221
|
-
|
|
4857
|
+
resolve7(authData);
|
|
4222
4858
|
}, 200);
|
|
4223
4859
|
});
|
|
4224
4860
|
req.on("error", (e) => {
|
|
@@ -4549,7 +5185,7 @@ function detectSubdirRepos() {
|
|
|
4549
5185
|
}
|
|
4550
5186
|
}
|
|
4551
5187
|
function ask(rl, question) {
|
|
4552
|
-
return new Promise((
|
|
5188
|
+
return new Promise((resolve7) => rl.question(question, resolve7));
|
|
4553
5189
|
}
|
|
4554
5190
|
async function linkRepo(repo, linkedNames) {
|
|
4555
5191
|
try {
|
|
@@ -4788,7 +5424,7 @@ async function runClaudeDesktopTap(opts = {}) {
|
|
|
4788
5424
|
writeFileSync12(join13(sessionDir, "mcp_patch.py"), MCP_PATCH_PY, "utf-8");
|
|
4789
5425
|
const runnerPath = join13(sessionDir, "run.sh");
|
|
4790
5426
|
writeFileSync12(runnerPath, buildRunner(sessionDir), { mode: 493 });
|
|
4791
|
-
await new Promise((
|
|
5427
|
+
await new Promise((resolve7) => {
|
|
4792
5428
|
const child = spawn3("bash", [runnerPath], {
|
|
4793
5429
|
stdio: "inherit",
|
|
4794
5430
|
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" : "" }
|
|
@@ -4804,7 +5440,7 @@ async function runClaudeDesktopTap(opts = {}) {
|
|
|
4804
5440
|
child.on("exit", () => {
|
|
4805
5441
|
process.off("SIGINT", forward);
|
|
4806
5442
|
process.off("SIGTERM", forward);
|
|
4807
|
-
|
|
5443
|
+
resolve7();
|
|
4808
5444
|
});
|
|
4809
5445
|
});
|
|
4810
5446
|
}
|
|
@@ -6610,10 +7246,16 @@ async function dockerInstall(opts = {}) {
|
|
|
6610
7246
|
// Pass through the batch-size lever if the operator set it. Defaults
|
|
6611
7247
|
// inside the container to 5; clamped to [1, 20] by synkro-server.ts.
|
|
6612
7248
|
...process.env.SYNKRO_MAX_BATCH_SIZE ? ["-e", `SYNKRO_MAX_BATCH_SIZE=${process.env.SYNKRO_MAX_BATCH_SIZE}`] : [],
|
|
6613
|
-
//
|
|
6614
|
-
|
|
6615
|
-
//
|
|
6616
|
-
...
|
|
7249
|
+
// Explicit model overrides are preserved across local install/update for
|
|
7250
|
+
// every provider and the isolated route lane. The server reports the resolved values
|
|
7251
|
+
// in /healthz so operators can audit exactly what is spending tokens.
|
|
7252
|
+
...[
|
|
7253
|
+
"SYNKRO_CLAUDE_MODEL",
|
|
7254
|
+
"SYNKRO_CURSOR_MODEL",
|
|
7255
|
+
"SYNKRO_CODEX_MODEL",
|
|
7256
|
+
"SYNKRO_CONDUCTOR_MODEL",
|
|
7257
|
+
"SYNKRO_ROUTE_MODEL"
|
|
7258
|
+
].flatMap((key) => process.env[key] ? ["-e", `${key}=${process.env[key]}`] : []),
|
|
6617
7259
|
// Fix-poll kill switch. Default ON in the image; a benchmark/headless run
|
|
6618
7260
|
// (e.g. sec-code-bench) sets SYNKRO_FIX_POLL=0 so ask-mode violations skip the
|
|
6619
7261
|
// interactive AskUserQuestion poll and fall through to generate-the-fix. Only
|
|
@@ -6871,7 +7513,7 @@ var init_dockerInstall = __esm({
|
|
|
6871
7513
|
HOST_PGLITE_PORT = parseInt(process.env.SYNKRO_HOST_PGLITE_PORT || "15433", 10);
|
|
6872
7514
|
CONTAINER_NAME = resolveContainerName();
|
|
6873
7515
|
defaultImageVersion = () => {
|
|
6874
|
-
if (true) return "1.
|
|
7516
|
+
if (true) return "1.8.0";
|
|
6875
7517
|
try {
|
|
6876
7518
|
const pkg = JSON.parse(readFileSync17(new URL("../../package.json", import.meta.url), "utf8"));
|
|
6877
7519
|
if (pkg.version) return pkg.version;
|
|
@@ -6903,7 +7545,7 @@ function captureClaudeSetupToken() {
|
|
|
6903
7545
|
const bin = "script";
|
|
6904
7546
|
const args2 = isMac ? ["-q", tmpFile, "claude", "setup-token"] : ["-qec", "claude setup-token", tmpFile];
|
|
6905
7547
|
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.';
|
|
6906
|
-
return new Promise((
|
|
7548
|
+
return new Promise((resolve7, reject) => {
|
|
6907
7549
|
const proc = nodeSpawn(bin, args2, {
|
|
6908
7550
|
stdio: "inherit",
|
|
6909
7551
|
env: { ...process.env, FORCE_COLOR: "3", COLORTERM: "truecolor", TERM: "xterm-256color" }
|
|
@@ -6970,7 +7612,7 @@ function captureClaudeSetupToken() {
|
|
|
6970
7612
|
reject(new Error(`Captured no setup token from claude setup-token output. ${reason}`));
|
|
6971
7613
|
return;
|
|
6972
7614
|
}
|
|
6973
|
-
|
|
7615
|
+
resolve7(token);
|
|
6974
7616
|
});
|
|
6975
7617
|
});
|
|
6976
7618
|
}
|
|
@@ -6996,13 +7638,13 @@ function findCodexBinary() {
|
|
|
6996
7638
|
function runCodexLogin(codexBin, codexHome) {
|
|
6997
7639
|
mkdirSync13(codexHome, { recursive: true, mode: 448 });
|
|
6998
7640
|
writeFileSync15(join17(codexHome, "config.toml"), 'cli_auth_credentials_store = "file"\n', { mode: 384 });
|
|
6999
|
-
return new Promise((
|
|
7641
|
+
return new Promise((resolve7, reject) => {
|
|
7000
7642
|
const proc = nodeSpawn2(codexBin, ["login"], {
|
|
7001
7643
|
stdio: "inherit",
|
|
7002
7644
|
env: { ...process.env, CODEX_HOME: codexHome }
|
|
7003
7645
|
});
|
|
7004
7646
|
proc.on("error", (err) => reject(new Error(`failed to spawn codex login: ${err.message}`)));
|
|
7005
|
-
proc.on("close", (code) => code === 0 ?
|
|
7647
|
+
proc.on("close", (code) => code === 0 ? resolve7() : reject(new Error(`codex login exited with code ${code}`)));
|
|
7006
7648
|
});
|
|
7007
7649
|
}
|
|
7008
7650
|
async function setupCodexCloud(gatewayUrl, bearerToken, onStatus) {
|
|
@@ -7564,6 +8206,94 @@ var init_codexTranscriptMessages = __esm({
|
|
|
7564
8206
|
}
|
|
7565
8207
|
});
|
|
7566
8208
|
|
|
8209
|
+
// cli/scanning/claudeTranscriptUsage.ts
|
|
8210
|
+
function tokenCount(value) {
|
|
8211
|
+
const parsed = Number(value);
|
|
8212
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;
|
|
8213
|
+
}
|
|
8214
|
+
function isoDay(value, fallbackDay) {
|
|
8215
|
+
if (typeof value === "string") {
|
|
8216
|
+
const parsed = new Date(value);
|
|
8217
|
+
if (Number.isFinite(parsed.getTime())) return parsed.toISOString().slice(0, 10);
|
|
8218
|
+
}
|
|
8219
|
+
return fallbackDay;
|
|
8220
|
+
}
|
|
8221
|
+
function parseClaudeTranscriptUsage(transcript, fallbackDay = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), options = {}) {
|
|
8222
|
+
const seen = options.seenStableIds ?? /* @__PURE__ */ new Set();
|
|
8223
|
+
const rollups = /* @__PURE__ */ new Map();
|
|
8224
|
+
const usage = {
|
|
8225
|
+
input_tokens: 0,
|
|
8226
|
+
output_tokens: 0,
|
|
8227
|
+
cache_creation_input_tokens: 0,
|
|
8228
|
+
cache_read_input_tokens: 0
|
|
8229
|
+
};
|
|
8230
|
+
let model = "";
|
|
8231
|
+
let turns = 0;
|
|
8232
|
+
let lineIndex = 0;
|
|
8233
|
+
for (const line of transcript.split("\n")) {
|
|
8234
|
+
lineIndex += 1;
|
|
8235
|
+
const text = line.trim();
|
|
8236
|
+
if (!text) continue;
|
|
8237
|
+
try {
|
|
8238
|
+
const entry = JSON.parse(text);
|
|
8239
|
+
const message = entry?.message;
|
|
8240
|
+
if (message?.role !== "assistant" || !message.usage || typeof message.usage !== "object") {
|
|
8241
|
+
continue;
|
|
8242
|
+
}
|
|
8243
|
+
const stableId = typeof entry.uuid === "string" && entry.uuid ? `uuid:${entry.uuid}` : typeof message.id === "string" && message.id ? `message:${message.id}:${String(entry.timestamp || "")}` : `${options.sourceId || "transcript"}:line:${lineIndex}`;
|
|
8244
|
+
if (seen.has(stableId)) continue;
|
|
8245
|
+
seen.add(stableId);
|
|
8246
|
+
const counts = {
|
|
8247
|
+
input_tokens: tokenCount(message.usage.input_tokens),
|
|
8248
|
+
output_tokens: tokenCount(message.usage.output_tokens),
|
|
8249
|
+
cache_creation_input_tokens: tokenCount(message.usage.cache_creation_input_tokens),
|
|
8250
|
+
cache_read_input_tokens: tokenCount(message.usage.cache_read_input_tokens)
|
|
8251
|
+
};
|
|
8252
|
+
const countTotal = counts.input_tokens + counts.output_tokens + counts.cache_creation_input_tokens + counts.cache_read_input_tokens;
|
|
8253
|
+
if (countTotal === 0) continue;
|
|
8254
|
+
const entryModel = typeof message.model === "string" && message.model ? message.model : "unknown";
|
|
8255
|
+
if (entryModel !== "<synthetic>") model = entryModel;
|
|
8256
|
+
const day = isoDay(entry.timestamp, fallbackDay);
|
|
8257
|
+
const key = `${day}\0${entryModel}`;
|
|
8258
|
+
const row = rollups.get(key) ?? {
|
|
8259
|
+
day,
|
|
8260
|
+
model: entryModel,
|
|
8261
|
+
turns: 0,
|
|
8262
|
+
input_tokens: 0,
|
|
8263
|
+
output_tokens: 0,
|
|
8264
|
+
cache_creation_input_tokens: 0,
|
|
8265
|
+
cache_read_input_tokens: 0
|
|
8266
|
+
};
|
|
8267
|
+
row.turns += 1;
|
|
8268
|
+
row.input_tokens += counts.input_tokens;
|
|
8269
|
+
row.output_tokens += counts.output_tokens;
|
|
8270
|
+
row.cache_creation_input_tokens += counts.cache_creation_input_tokens;
|
|
8271
|
+
row.cache_read_input_tokens += counts.cache_read_input_tokens;
|
|
8272
|
+
rollups.set(key, row);
|
|
8273
|
+
turns += 1;
|
|
8274
|
+
usage.input_tokens += counts.input_tokens;
|
|
8275
|
+
usage.output_tokens += counts.output_tokens;
|
|
8276
|
+
usage.cache_creation_input_tokens += counts.cache_creation_input_tokens;
|
|
8277
|
+
usage.cache_read_input_tokens += counts.cache_read_input_tokens;
|
|
8278
|
+
} catch {
|
|
8279
|
+
}
|
|
8280
|
+
}
|
|
8281
|
+
if (turns === 0) return null;
|
|
8282
|
+
return {
|
|
8283
|
+
usage,
|
|
8284
|
+
model: model || "unknown",
|
|
8285
|
+
rollups: [...rollups.values()].sort(
|
|
8286
|
+
(a, b) => a.day.localeCompare(b.day) || a.model.localeCompare(b.model)
|
|
8287
|
+
),
|
|
8288
|
+
turns
|
|
8289
|
+
};
|
|
8290
|
+
}
|
|
8291
|
+
var init_claudeTranscriptUsage = __esm({
|
|
8292
|
+
"cli/scanning/claudeTranscriptUsage.ts"() {
|
|
8293
|
+
"use strict";
|
|
8294
|
+
}
|
|
8295
|
+
});
|
|
8296
|
+
|
|
7567
8297
|
// cli/commands/install.ts
|
|
7568
8298
|
var install_exports = {};
|
|
7569
8299
|
__export(install_exports, {
|
|
@@ -7581,7 +8311,7 @@ __export(install_exports, {
|
|
|
7581
8311
|
});
|
|
7582
8312
|
import { existsSync as existsSync22, mkdirSync as mkdirSync15, writeFileSync as writeFileSync17, chmodSync as chmodSync5, readFileSync as readFileSync21, readdirSync as readdirSync4, unlinkSync as unlinkSync7, statSync as statSync2 } from "fs";
|
|
7583
8313
|
import { homedir as homedir21 } from "os";
|
|
7584
|
-
import { join as join19, isAbsolute, resolve as resolve4 } from "path";
|
|
8314
|
+
import { basename, join as join19, isAbsolute, resolve as resolve4, sep } from "path";
|
|
7585
8315
|
import { execSync as execSync4, spawn as spawn5 } from "child_process";
|
|
7586
8316
|
import { createInterface as createInterface2 } from "readline";
|
|
7587
8317
|
import { createHash as createHash4 } from "crypto";
|
|
@@ -7616,20 +8346,20 @@ async function promptAgentSelection(detected) {
|
|
|
7616
8346
|
detected.forEach((a, i) => console.log(` ${i + 1}. ${a.name}`));
|
|
7617
8347
|
console.log(` ${detected.length + 1}. Both / all (default)`);
|
|
7618
8348
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
7619
|
-
const ask3 = () => new Promise((
|
|
8349
|
+
const ask3 = () => new Promise((resolve7) => {
|
|
7620
8350
|
rl.question(`Pick [1-${detected.length + 1}] (default: all): `, (answer) => {
|
|
7621
8351
|
const t = answer.trim().toLowerCase();
|
|
7622
8352
|
if (t === "" || t === String(detected.length + 1) || t === "both" || t === "all") {
|
|
7623
8353
|
rl.close();
|
|
7624
|
-
return
|
|
8354
|
+
return resolve7(detected);
|
|
7625
8355
|
}
|
|
7626
8356
|
const n = parseInt(t, 10);
|
|
7627
8357
|
if (Number.isInteger(n) && n >= 1 && n <= detected.length) {
|
|
7628
8358
|
rl.close();
|
|
7629
|
-
return
|
|
8359
|
+
return resolve7([detected[n - 1]]);
|
|
7630
8360
|
}
|
|
7631
8361
|
console.log("Invalid choice. Try again.");
|
|
7632
|
-
|
|
8362
|
+
resolve7(ask3());
|
|
7633
8363
|
});
|
|
7634
8364
|
});
|
|
7635
8365
|
return ask3();
|
|
@@ -7652,12 +8382,12 @@ async function promptCursorApiKey(opts) {
|
|
|
7652
8382
|
return;
|
|
7653
8383
|
}
|
|
7654
8384
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
7655
|
-
const key = await new Promise((
|
|
8385
|
+
const key = await new Promise((resolve7) => {
|
|
7656
8386
|
rl.question(
|
|
7657
8387
|
"Cursor grading needs a Cursor API key (cursor.com \u2192 Settings \u2192 API Keys).\nPaste it now, or press Enter to skip (Cursor workers stay idle until set): ",
|
|
7658
8388
|
(answer) => {
|
|
7659
8389
|
rl.close();
|
|
7660
|
-
|
|
8390
|
+
resolve7(answer.trim());
|
|
7661
8391
|
}
|
|
7662
8392
|
);
|
|
7663
8393
|
});
|
|
@@ -7672,7 +8402,7 @@ async function promptDeployLocation(current = "local") {
|
|
|
7672
8402
|
if (!process.stdin.isTTY) return current;
|
|
7673
8403
|
const other = current === "cloud" ? "local" : "cloud";
|
|
7674
8404
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
7675
|
-
return new Promise((
|
|
8405
|
+
return new Promise((resolve7) => {
|
|
7676
8406
|
rl.question(
|
|
7677
8407
|
`Where should Synkro run?
|
|
7678
8408
|
local \u2014 a grading container on this machine (Docker)
|
|
@@ -7681,7 +8411,7 @@ Each worker uses the account credentials you authorize. Choose [${current}] / ${
|
|
|
7681
8411
|
(answer) => {
|
|
7682
8412
|
rl.close();
|
|
7683
8413
|
const a = answer.trim().toLowerCase();
|
|
7684
|
-
|
|
8414
|
+
resolve7(a === "cloud" ? "cloud" : a === "local" ? "local" : current);
|
|
7685
8415
|
}
|
|
7686
8416
|
);
|
|
7687
8417
|
});
|
|
@@ -7840,7 +8570,7 @@ function writeConfigEnv(opts) {
|
|
|
7840
8570
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle2(credsPath)}`,
|
|
7841
8571
|
`SYNKRO_TIER=${shellQuoteSingle2(safeTier)}`,
|
|
7842
8572
|
`SYNKRO_INFERENCE=${shellQuoteSingle2(safeInference)}`,
|
|
7843
|
-
`SYNKRO_VERSION=${shellQuoteSingle2("1.
|
|
8573
|
+
`SYNKRO_VERSION=${shellQuoteSingle2("1.8.0")}`
|
|
7844
8574
|
];
|
|
7845
8575
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle2(safeSynkroBin)}`);
|
|
7846
8576
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle2(safeUserId)}`);
|
|
@@ -8018,6 +8748,11 @@ async function provisionCloudContainer(opts) {
|
|
|
8018
8748
|
cursor_workers: cursorWorkers,
|
|
8019
8749
|
codex_workers: codexWorkers,
|
|
8020
8750
|
conductor_provider: selectedKind,
|
|
8751
|
+
claude_model: process.env.SYNKRO_CLAUDE_MODEL || "",
|
|
8752
|
+
cursor_model: process.env.SYNKRO_CURSOR_MODEL || "",
|
|
8753
|
+
codex_model: process.env.SYNKRO_CODEX_MODEL || "",
|
|
8754
|
+
conductor_model: process.env.SYNKRO_CONDUCTOR_MODEL || "",
|
|
8755
|
+
route_model: process.env.SYNKRO_ROUTE_MODEL || "",
|
|
8021
8756
|
cursor_api_key: cursorApiKey,
|
|
8022
8757
|
// never logged; gateway stores it as the org secret
|
|
8023
8758
|
connected_repo: repo,
|
|
@@ -8582,7 +9317,7 @@ async function installCommand(opts = {}) {
|
|
|
8582
9317
|
await setTelemetryState({ enabled: true, remoteFlushEnabled: telemetryConsent });
|
|
8583
9318
|
emit("install", {
|
|
8584
9319
|
phase: "started",
|
|
8585
|
-
cli_version_to: "1.
|
|
9320
|
+
cli_version_to: "1.8.0",
|
|
8586
9321
|
agents_detected: agents.map((a) => a.kind),
|
|
8587
9322
|
with_github: false,
|
|
8588
9323
|
with_local_cc: false,
|
|
@@ -9649,8 +10384,8 @@ function ensureReachabilityGitHook() {
|
|
|
9649
10384
|
}
|
|
9650
10385
|
return "updated";
|
|
9651
10386
|
}
|
|
9652
|
-
const
|
|
9653
|
-
writeFileSync17(hookPath, cur +
|
|
10387
|
+
const sep3 = cur.endsWith("\n") ? "" : "\n";
|
|
10388
|
+
writeFileSync17(hookPath, cur + sep3 + "\n" + block + "\n");
|
|
9654
10389
|
try {
|
|
9655
10390
|
chmodSync5(hookPath, 493);
|
|
9656
10391
|
} catch {
|
|
@@ -9677,10 +10412,33 @@ function detectGitRepo2() {
|
|
|
9677
10412
|
}
|
|
9678
10413
|
function getClaudeProjectsFolder() {
|
|
9679
10414
|
const cwd = process.cwd();
|
|
9680
|
-
const sanitized =
|
|
10415
|
+
const sanitized = cwd.replace(/\//g, "-");
|
|
9681
10416
|
const projectsDir = join19(homedir21(), ".claude", "projects", sanitized);
|
|
9682
10417
|
return existsSync22(projectsDir) ? projectsDir : null;
|
|
9683
10418
|
}
|
|
10419
|
+
function getClaudeTranscriptFileEntries(projectsDir) {
|
|
10420
|
+
let relativeFiles = [];
|
|
10421
|
+
try {
|
|
10422
|
+
relativeFiles = readdirSync4(projectsDir, { recursive: true, encoding: "utf-8" });
|
|
10423
|
+
} catch {
|
|
10424
|
+
return [];
|
|
10425
|
+
}
|
|
10426
|
+
return relativeFiles.filter((file) => file.endsWith(".jsonl")).map((file) => {
|
|
10427
|
+
const parts = file.split(sep);
|
|
10428
|
+
const subagentsIndex = parts.lastIndexOf("subagents");
|
|
10429
|
+
if (subagentsIndex > 0) {
|
|
10430
|
+
return {
|
|
10431
|
+
filePath: join19(projectsDir, file),
|
|
10432
|
+
sessionId: basename(file, ".jsonl"),
|
|
10433
|
+
parentSessionId: parts[subagentsIndex - 1]
|
|
10434
|
+
};
|
|
10435
|
+
}
|
|
10436
|
+
return {
|
|
10437
|
+
filePath: join19(projectsDir, file),
|
|
10438
|
+
sessionId: basename(file, ".jsonl")
|
|
10439
|
+
};
|
|
10440
|
+
});
|
|
10441
|
+
}
|
|
9684
10442
|
function extractSessionInsights(projectsDir) {
|
|
9685
10443
|
const insights = [];
|
|
9686
10444
|
const files = readdirSync4(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
@@ -9766,13 +10524,13 @@ function extractTextContent(content) {
|
|
|
9766
10524
|
function getCodexTranscriptFiles(repo) {
|
|
9767
10525
|
const sessionsDir = join19(process.env.CODEX_HOME || join19(homedir21(), ".codex"), "sessions");
|
|
9768
10526
|
if (!existsSync22(sessionsDir)) return [];
|
|
9769
|
-
let
|
|
10527
|
+
let relative2 = [];
|
|
9770
10528
|
try {
|
|
9771
|
-
|
|
10529
|
+
relative2 = readdirSync4(sessionsDir, { recursive: true, encoding: "utf-8" });
|
|
9772
10530
|
} catch {
|
|
9773
10531
|
return [];
|
|
9774
10532
|
}
|
|
9775
|
-
return
|
|
10533
|
+
return relative2.filter((p) => p.endsWith(".jsonl")).map((p) => join19(sessionsDir, p)).filter((filePath) => {
|
|
9776
10534
|
try {
|
|
9777
10535
|
const first = readFileSync21(filePath, "utf-8").split("\n", 1)[0];
|
|
9778
10536
|
const meta = JSON.parse(first);
|
|
@@ -9980,23 +10738,40 @@ function parseTranscriptFile(filePath) {
|
|
|
9980
10738
|
async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
9981
10739
|
const projectsDir = getClaudeProjectsFolder();
|
|
9982
10740
|
if (!projectsDir) return { sessions: 0, messages: 0 };
|
|
9983
|
-
const files =
|
|
10741
|
+
const files = getClaudeTranscriptFileEntries(projectsDir);
|
|
9984
10742
|
if (files.length === 0) return { sessions: 0, messages: 0 };
|
|
9985
10743
|
console.log(` Found ${files.length} CC session transcripts, importing + embedding...`);
|
|
9986
10744
|
let totalSessions = 0;
|
|
9987
10745
|
let totalMessages = 0;
|
|
10746
|
+
const seenStableIds = /* @__PURE__ */ new Set();
|
|
9988
10747
|
for (let i = 0; i < files.length; i++) {
|
|
9989
10748
|
const file = files[i];
|
|
9990
|
-
const sessionId = file.
|
|
9991
|
-
const filePath =
|
|
10749
|
+
const sessionId = file.sessionId;
|
|
10750
|
+
const filePath = file.filePath;
|
|
9992
10751
|
try {
|
|
10752
|
+
const transcript = readFileSync21(filePath, "utf-8");
|
|
10753
|
+
const transcriptUsage = parseClaudeTranscriptUsage(
|
|
10754
|
+
transcript,
|
|
10755
|
+
statSync2(filePath).mtime.toISOString().slice(0, 10),
|
|
10756
|
+
{ seenStableIds, sourceId: file.parentSessionId ? `${file.parentSessionId}:subagent:${sessionId}` : sessionId }
|
|
10757
|
+
);
|
|
9993
10758
|
const allMessages = parseTranscriptFile(filePath);
|
|
9994
10759
|
const messages = allMessages.length > 500 ? allMessages.slice(-500) : allMessages;
|
|
9995
10760
|
if (messages.length === 0) continue;
|
|
9996
10761
|
const resp = await fetch(`http://127.0.0.1:${mcpPort}/api/conversation-sync`, {
|
|
9997
10762
|
method: "POST",
|
|
9998
10763
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${mcpToken}` },
|
|
9999
|
-
body: JSON.stringify({
|
|
10764
|
+
body: JSON.stringify({
|
|
10765
|
+
session_id: sessionId,
|
|
10766
|
+
parent_session_id: file.parentSessionId,
|
|
10767
|
+
repo,
|
|
10768
|
+
messages,
|
|
10769
|
+
session_usage: transcriptUsage?.usage,
|
|
10770
|
+
usage_rollups: transcriptUsage?.rollups ?? [],
|
|
10771
|
+
model: transcriptUsage?.model,
|
|
10772
|
+
harness: "claude-code",
|
|
10773
|
+
usage_cumulative: true
|
|
10774
|
+
}),
|
|
10000
10775
|
signal: AbortSignal.timeout(15e3)
|
|
10001
10776
|
});
|
|
10002
10777
|
if (resp.ok) {
|
|
@@ -10010,9 +10785,10 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
10010
10785
|
process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
|
|
10011
10786
|
}
|
|
10012
10787
|
try {
|
|
10013
|
-
const content = readFileSync21(
|
|
10788
|
+
const content = readFileSync21(filePath, "utf-8");
|
|
10014
10789
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
10015
|
-
|
|
10790
|
+
const offsetId = file.parentSessionId ? `${file.parentSessionId}_${sessionId}` : sessionId;
|
|
10791
|
+
writeFileSync17(join19(OFFSETS_DIR, offsetId), String(lineCount), "utf-8");
|
|
10016
10792
|
} catch {
|
|
10017
10793
|
}
|
|
10018
10794
|
}
|
|
@@ -10022,23 +10798,39 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
10022
10798
|
async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
10023
10799
|
const projectsDir = getClaudeProjectsFolder();
|
|
10024
10800
|
if (!projectsDir) return { sessions: 0, messages: 0 };
|
|
10025
|
-
const files =
|
|
10801
|
+
const files = getClaudeTranscriptFileEntries(projectsDir);
|
|
10026
10802
|
if (files.length === 0) return { sessions: 0, messages: 0 };
|
|
10027
10803
|
console.log(`Found ${files.length} CC session transcripts, syncing...`);
|
|
10028
10804
|
const maxMessagesPerSession = 500;
|
|
10029
10805
|
let totalSessions = 0;
|
|
10030
10806
|
let totalMessages = 0;
|
|
10807
|
+
const seenStableIds = /* @__PURE__ */ new Set();
|
|
10031
10808
|
for (let i = 0; i < files.length; i += 5) {
|
|
10032
10809
|
const batch = files.slice(i, i + 5);
|
|
10033
10810
|
const sessions = [];
|
|
10034
10811
|
for (const file of batch) {
|
|
10035
|
-
const sessionId = file.
|
|
10036
|
-
const filePath =
|
|
10812
|
+
const sessionId = file.sessionId;
|
|
10813
|
+
const filePath = file.filePath;
|
|
10037
10814
|
try {
|
|
10815
|
+
const transcript = readFileSync21(filePath, "utf-8");
|
|
10816
|
+
const transcriptUsage = parseClaudeTranscriptUsage(
|
|
10817
|
+
transcript,
|
|
10818
|
+
statSync2(filePath).mtime.toISOString().slice(0, 10),
|
|
10819
|
+
{ seenStableIds, sourceId: file.parentSessionId ? `${file.parentSessionId}:subagent:${sessionId}` : sessionId }
|
|
10820
|
+
);
|
|
10038
10821
|
const allMessages = parseTranscriptFile(filePath);
|
|
10039
10822
|
const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
|
|
10040
10823
|
if (messages.length > 0) {
|
|
10041
|
-
sessions.push({
|
|
10824
|
+
sessions.push({
|
|
10825
|
+
cc_session_id: sessionId,
|
|
10826
|
+
parent_session_id: file.parentSessionId,
|
|
10827
|
+
messages,
|
|
10828
|
+
model: transcriptUsage?.model,
|
|
10829
|
+
session_usage: transcriptUsage?.usage,
|
|
10830
|
+
usage_rollups: transcriptUsage?.rollups ?? [],
|
|
10831
|
+
harness: "claude-code",
|
|
10832
|
+
usage_cumulative: true
|
|
10833
|
+
});
|
|
10042
10834
|
}
|
|
10043
10835
|
} catch {
|
|
10044
10836
|
}
|
|
@@ -10061,12 +10853,13 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
10061
10853
|
} catch {
|
|
10062
10854
|
}
|
|
10063
10855
|
for (const file of batch) {
|
|
10064
|
-
const sessionId = file.
|
|
10065
|
-
const filePath =
|
|
10856
|
+
const sessionId = file.sessionId;
|
|
10857
|
+
const filePath = file.filePath;
|
|
10066
10858
|
try {
|
|
10067
10859
|
const content = readFileSync21(filePath, "utf-8");
|
|
10068
10860
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
10069
|
-
|
|
10861
|
+
const offsetId = file.parentSessionId ? `${file.parentSessionId}_${sessionId}` : sessionId;
|
|
10862
|
+
writeFileSync17(join19(OFFSETS_DIR, offsetId), String(lineCount), "utf-8");
|
|
10070
10863
|
} catch {
|
|
10071
10864
|
}
|
|
10072
10865
|
}
|
|
@@ -10149,6 +10942,7 @@ var init_install = __esm({
|
|
|
10149
10942
|
init_graderSmoke();
|
|
10150
10943
|
init_codexTranscriptUsage();
|
|
10151
10944
|
init_codexTranscriptMessages();
|
|
10945
|
+
init_claudeTranscriptUsage();
|
|
10152
10946
|
SYNKRO_DIR11 = join19(homedir21(), ".synkro");
|
|
10153
10947
|
HOOKS_DIR = join19(SYNKRO_DIR11, "hooks");
|
|
10154
10948
|
CONFIG_PATH4 = join19(SYNKRO_DIR11, "config.env");
|
|
@@ -10811,10 +11605,10 @@ function confirmPurge() {
|
|
|
10811
11605
|
return Promise.resolve(false);
|
|
10812
11606
|
}
|
|
10813
11607
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
10814
|
-
return new Promise((
|
|
11608
|
+
return new Promise((resolve7) => {
|
|
10815
11609
|
rl.question(" Type 'yes' to wipe everything (anything else cancels): ", (answer) => {
|
|
10816
11610
|
rl.close();
|
|
10817
|
-
|
|
11611
|
+
resolve7(answer.trim().toLowerCase() === "yes");
|
|
10818
11612
|
});
|
|
10819
11613
|
});
|
|
10820
11614
|
}
|
|
@@ -11067,7 +11861,7 @@ async function submitToChannel(role, payload, opts = {}) {
|
|
|
11067
11861
|
const port = opts.port ?? CHANNEL_PORT;
|
|
11068
11862
|
const startedAt = Date.now();
|
|
11069
11863
|
try {
|
|
11070
|
-
const result = await new Promise((
|
|
11864
|
+
const result = await new Promise((resolve7, reject) => {
|
|
11071
11865
|
const req = httpRequest({
|
|
11072
11866
|
host: CHANNEL_HOST,
|
|
11073
11867
|
port,
|
|
@@ -11093,7 +11887,7 @@ async function submitToChannel(role, payload, opts = {}) {
|
|
|
11093
11887
|
reject(new LocalCCError(parsed.error));
|
|
11094
11888
|
return;
|
|
11095
11889
|
}
|
|
11096
|
-
|
|
11890
|
+
resolve7(String(parsed.result ?? ""));
|
|
11097
11891
|
} catch (err) {
|
|
11098
11892
|
reject(new LocalCCError(`malformed channel response: ${text.slice(0, 200)}`, err));
|
|
11099
11893
|
}
|
|
@@ -11119,14 +11913,14 @@ async function submitToChannel(role, payload, opts = {}) {
|
|
|
11119
11913
|
}
|
|
11120
11914
|
}
|
|
11121
11915
|
function isChannelAvailable(port = CHANNEL_PORT, timeoutMs = 500) {
|
|
11122
|
-
return new Promise((
|
|
11916
|
+
return new Promise((resolve7) => {
|
|
11123
11917
|
const sock = connect(port, CHANNEL_HOST);
|
|
11124
11918
|
const done = (ok) => {
|
|
11125
11919
|
try {
|
|
11126
11920
|
sock.destroy();
|
|
11127
11921
|
} catch {
|
|
11128
11922
|
}
|
|
11129
|
-
|
|
11923
|
+
resolve7(ok);
|
|
11130
11924
|
};
|
|
11131
11925
|
sock.once("connect", () => done(true));
|
|
11132
11926
|
sock.once("error", () => done(false));
|
|
@@ -11158,10 +11952,10 @@ __export(grade_exports, {
|
|
|
11158
11952
|
gradeCommand: () => gradeCommand
|
|
11159
11953
|
});
|
|
11160
11954
|
async function readStdin() {
|
|
11161
|
-
return new Promise((
|
|
11955
|
+
return new Promise((resolve7, reject) => {
|
|
11162
11956
|
const chunks = [];
|
|
11163
11957
|
process.stdin.on("data", (c) => chunks.push(c));
|
|
11164
|
-
process.stdin.on("end", () =>
|
|
11958
|
+
process.stdin.on("end", () => resolve7(Buffer.concat(chunks).toString("utf-8")));
|
|
11165
11959
|
process.stdin.on("error", reject);
|
|
11166
11960
|
});
|
|
11167
11961
|
}
|
|
@@ -11415,7 +12209,7 @@ function spawnClaudeJudge(file, claudeToken, promptHeader) {
|
|
|
11415
12209
|
Diff:
|
|
11416
12210
|
${hunks}`;
|
|
11417
12211
|
const fullPrompt = promptHeader + userMessage;
|
|
11418
|
-
return new Promise((
|
|
12212
|
+
return new Promise((resolve7) => {
|
|
11419
12213
|
const t0 = Date.now();
|
|
11420
12214
|
const proc = spawn6(
|
|
11421
12215
|
"claude",
|
|
@@ -11443,7 +12237,7 @@ ${hunks}`;
|
|
|
11443
12237
|
const latencyMs = Date.now() - t0;
|
|
11444
12238
|
if (code !== 0) {
|
|
11445
12239
|
console.warn(` claude exited ${code}: ${(stderr || stdout).slice(0, 500)}`);
|
|
11446
|
-
|
|
12240
|
+
resolve7({ findings: [], latencyMs });
|
|
11447
12241
|
return;
|
|
11448
12242
|
}
|
|
11449
12243
|
try {
|
|
@@ -11462,10 +12256,10 @@ ${hunks}`;
|
|
|
11462
12256
|
description: f.description,
|
|
11463
12257
|
fix: f.fix
|
|
11464
12258
|
}));
|
|
11465
|
-
|
|
12259
|
+
resolve7({ findings, latencyMs });
|
|
11466
12260
|
} catch (parseErr) {
|
|
11467
12261
|
console.warn(` failed to parse claude response: ${stdout.slice(0, 300)}`);
|
|
11468
|
-
|
|
12262
|
+
resolve7({ findings: [], latencyMs });
|
|
11469
12263
|
}
|
|
11470
12264
|
});
|
|
11471
12265
|
});
|
|
@@ -11514,7 +12308,7 @@ ${JSON.stringify(findings, null, 2)}
|
|
|
11514
12308
|
`;
|
|
11515
12309
|
}
|
|
11516
12310
|
function spawnOpusConsolidator(findings, claudeToken) {
|
|
11517
|
-
return new Promise((
|
|
12311
|
+
return new Promise((resolve7) => {
|
|
11518
12312
|
const prompt = buildConsolidationPrompt(findings);
|
|
11519
12313
|
const proc = spawn6(
|
|
11520
12314
|
"claude",
|
|
@@ -11541,7 +12335,7 @@ function spawnOpusConsolidator(findings, claudeToken) {
|
|
|
11541
12335
|
proc.on("close", (code) => {
|
|
11542
12336
|
if (code !== 0) {
|
|
11543
12337
|
console.warn(` opus consolidation exited ${code}: ${(stderr || stdout).slice(0, 300)}`);
|
|
11544
|
-
|
|
12338
|
+
resolve7(fallbackReview(findings));
|
|
11545
12339
|
return;
|
|
11546
12340
|
}
|
|
11547
12341
|
try {
|
|
@@ -11562,10 +12356,10 @@ function spawnOpusConsolidator(findings, claudeToken) {
|
|
|
11562
12356
|
const order = ["low", "medium", "high", "critical"];
|
|
11563
12357
|
return order.indexOf(f.severity) > order.indexOf(max) ? f.severity : max;
|
|
11564
12358
|
}, "low");
|
|
11565
|
-
|
|
12359
|
+
resolve7({ summary: review.summary || "", comments, severity: maxSeverity });
|
|
11566
12360
|
} catch {
|
|
11567
12361
|
console.warn(` failed to parse opus response, using fallback`);
|
|
11568
|
-
|
|
12362
|
+
resolve7(fallbackReview(findings));
|
|
11569
12363
|
}
|
|
11570
12364
|
});
|
|
11571
12365
|
});
|
|
@@ -12048,14 +12842,14 @@ function ensureRunning(opts = {}) {
|
|
|
12048
12842
|
return startTask(opts);
|
|
12049
12843
|
}
|
|
12050
12844
|
function probePort(host, port, timeoutMs = 500) {
|
|
12051
|
-
return new Promise((
|
|
12845
|
+
return new Promise((resolve7) => {
|
|
12052
12846
|
const sock = connect2(port, host);
|
|
12053
12847
|
const done = (ok) => {
|
|
12054
12848
|
try {
|
|
12055
12849
|
sock.destroy();
|
|
12056
12850
|
} catch {
|
|
12057
12851
|
}
|
|
12058
|
-
|
|
12852
|
+
resolve7(ok);
|
|
12059
12853
|
};
|
|
12060
12854
|
sock.once("connect", () => done(true));
|
|
12061
12855
|
sock.once("error", () => done(false));
|
|
@@ -12605,7 +13399,7 @@ function cmdLogs(rest) {
|
|
|
12605
13399
|
if (!raw) console.log(" " + colorize("(use --raw / -r to see full payloads, --live / -f to follow)", 90));
|
|
12606
13400
|
return;
|
|
12607
13401
|
}
|
|
12608
|
-
return new Promise((
|
|
13402
|
+
return new Promise((resolve7) => {
|
|
12609
13403
|
console.log(" " + colorize("\u2014 following new turns (Ctrl-C to exit) \u2014", 90));
|
|
12610
13404
|
const stop = followTurns((t) => {
|
|
12611
13405
|
console.log(" " + formatTurn(t, raw));
|
|
@@ -12613,7 +13407,7 @@ function cmdLogs(rest) {
|
|
|
12613
13407
|
const onSigint = () => {
|
|
12614
13408
|
stop();
|
|
12615
13409
|
process.removeListener("SIGINT", onSigint);
|
|
12616
|
-
|
|
13410
|
+
resolve7();
|
|
12617
13411
|
};
|
|
12618
13412
|
process.on("SIGINT", onSigint);
|
|
12619
13413
|
});
|
|
@@ -12745,9 +13539,9 @@ var import_exports = {};
|
|
|
12745
13539
|
__export(import_exports, {
|
|
12746
13540
|
importCommand: () => importCommand
|
|
12747
13541
|
});
|
|
12748
|
-
import { existsSync as existsSync29, readFileSync as readFileSync27, readdirSync as readdirSync6 } from "fs";
|
|
13542
|
+
import { existsSync as existsSync29, readFileSync as readFileSync27, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
|
|
12749
13543
|
import { homedir as homedir28 } from "os";
|
|
12750
|
-
import { join as join27 } from "path";
|
|
13544
|
+
import { basename as basename2, join as join27, sep as sep2 } from "path";
|
|
12751
13545
|
import { execSync as execSync6 } from "child_process";
|
|
12752
13546
|
import { createInterface as createInterface4 } from "readline";
|
|
12753
13547
|
function readMcpJwt() {
|
|
@@ -12775,6 +13569,23 @@ function projectsFolder() {
|
|
|
12775
13569
|
const dir = join27(homedir28(), ".claude", "projects", sanitized);
|
|
12776
13570
|
return existsSync29(dir) ? dir : null;
|
|
12777
13571
|
}
|
|
13572
|
+
function transcriptFiles(projectsDir) {
|
|
13573
|
+
let relativeFiles = [];
|
|
13574
|
+
try {
|
|
13575
|
+
relativeFiles = readdirSync6(projectsDir, { recursive: true, encoding: "utf-8" });
|
|
13576
|
+
} catch {
|
|
13577
|
+
return [];
|
|
13578
|
+
}
|
|
13579
|
+
return relativeFiles.filter((file) => file.endsWith(".jsonl")).map((file) => {
|
|
13580
|
+
const parts = file.split(sep2);
|
|
13581
|
+
const subagentsIndex = parts.lastIndexOf("subagents");
|
|
13582
|
+
return {
|
|
13583
|
+
filePath: join27(projectsDir, file),
|
|
13584
|
+
sessionId: basename2(file, ".jsonl"),
|
|
13585
|
+
parentSessionId: subagentsIndex > 0 ? parts[subagentsIndex - 1] : void 0
|
|
13586
|
+
};
|
|
13587
|
+
});
|
|
13588
|
+
}
|
|
12778
13589
|
function repoName() {
|
|
12779
13590
|
try {
|
|
12780
13591
|
const url = execSync6("git config --get remote.origin.url", { encoding: "utf-8" }).trim();
|
|
@@ -12811,8 +13622,18 @@ function extractToolResultText(content, e) {
|
|
|
12811
13622
|
}
|
|
12812
13623
|
return t;
|
|
12813
13624
|
}
|
|
12814
|
-
function parseSession(
|
|
12815
|
-
const
|
|
13625
|
+
function parseSession(file, seenStableIds) {
|
|
13626
|
+
const { filePath, sessionId, parentSessionId } = file;
|
|
13627
|
+
const transcript = readFileSync27(filePath, "utf-8");
|
|
13628
|
+
const lines = transcript.split("\n").filter(Boolean);
|
|
13629
|
+
const transcriptUsage = parseClaudeTranscriptUsage(
|
|
13630
|
+
transcript,
|
|
13631
|
+
statSync4(filePath).mtime.toISOString().slice(0, 10),
|
|
13632
|
+
{
|
|
13633
|
+
seenStableIds,
|
|
13634
|
+
sourceId: parentSessionId ? `${parentSessionId}:subagent:${sessionId}` : sessionId
|
|
13635
|
+
}
|
|
13636
|
+
);
|
|
12816
13637
|
const messages = [];
|
|
12817
13638
|
const actions = [];
|
|
12818
13639
|
let step = 0;
|
|
@@ -12861,13 +13682,23 @@ function parseSession(filePath, sessionId) {
|
|
|
12861
13682
|
}
|
|
12862
13683
|
messages.push(msg);
|
|
12863
13684
|
}
|
|
12864
|
-
return {
|
|
13685
|
+
return {
|
|
13686
|
+
cc_session_id: sessionId,
|
|
13687
|
+
parent_session_id: parentSessionId,
|
|
13688
|
+
messages,
|
|
13689
|
+
actions,
|
|
13690
|
+
model: transcriptUsage?.model,
|
|
13691
|
+
session_usage: transcriptUsage?.usage,
|
|
13692
|
+
usage_rollups: transcriptUsage?.rollups ?? [],
|
|
13693
|
+
harness: "claude-code",
|
|
13694
|
+
usage_cumulative: true
|
|
13695
|
+
};
|
|
12865
13696
|
}
|
|
12866
13697
|
function ask2(q) {
|
|
12867
13698
|
const rl = createInterface4({ input: process.stdin, output: process.stdout });
|
|
12868
|
-
return new Promise((
|
|
13699
|
+
return new Promise((resolve7) => rl.question(q, (a) => {
|
|
12869
13700
|
rl.close();
|
|
12870
|
-
|
|
13701
|
+
resolve7(/^y(es)?$/i.test(a.trim()));
|
|
12871
13702
|
}));
|
|
12872
13703
|
}
|
|
12873
13704
|
async function importCommand() {
|
|
@@ -12879,7 +13710,7 @@ async function importCommand() {
|
|
|
12879
13710
|
console.log("No Claude Code transcripts found for this repo (~/.claude/projects).");
|
|
12880
13711
|
return;
|
|
12881
13712
|
}
|
|
12882
|
-
const files =
|
|
13713
|
+
const files = transcriptFiles(dir);
|
|
12883
13714
|
if (!files.length) {
|
|
12884
13715
|
console.log("No sessions to import.");
|
|
12885
13716
|
return;
|
|
@@ -12892,7 +13723,8 @@ async function importCommand() {
|
|
|
12892
13723
|
return;
|
|
12893
13724
|
}
|
|
12894
13725
|
}
|
|
12895
|
-
const
|
|
13726
|
+
const seenStableIds = /* @__PURE__ */ new Set();
|
|
13727
|
+
const sessions = files.map((file) => parseSession(file, seenStableIds)).filter((s) => s.messages.length > 0);
|
|
12896
13728
|
const totalMsgs = sessions.reduce((n, s) => n + s.messages.length, 0);
|
|
12897
13729
|
let ok = 0, fail = 0;
|
|
12898
13730
|
if (isCloud) {
|
|
@@ -12938,7 +13770,19 @@ async function importCommand() {
|
|
|
12938
13770
|
const r = await fetch(`http://127.0.0.1:${port}/api/ingest`, {
|
|
12939
13771
|
method: "POST",
|
|
12940
13772
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${mcpJwt2}` },
|
|
12941
|
-
body: JSON.stringify({
|
|
13773
|
+
body: JSON.stringify({
|
|
13774
|
+
capture_type: "transcript_sync",
|
|
13775
|
+
session_id: s.cc_session_id,
|
|
13776
|
+
parent_session_id: s.parent_session_id,
|
|
13777
|
+
repo,
|
|
13778
|
+
messages: s.messages,
|
|
13779
|
+
actions: s.actions,
|
|
13780
|
+
model: s.model,
|
|
13781
|
+
session_usage: s.session_usage,
|
|
13782
|
+
usage_rollups: s.usage_rollups,
|
|
13783
|
+
harness: s.harness,
|
|
13784
|
+
usage_cumulative: true
|
|
13785
|
+
}),
|
|
12942
13786
|
// A full session can carry thousands of turns — 15s timed out mid-import.
|
|
12943
13787
|
signal: AbortSignal.timeout(12e4)
|
|
12944
13788
|
});
|
|
@@ -12967,6 +13811,7 @@ var init_import = __esm({
|
|
|
12967
13811
|
"cli/commands/import.ts"() {
|
|
12968
13812
|
"use strict";
|
|
12969
13813
|
init_stub();
|
|
13814
|
+
init_claudeTranscriptUsage();
|
|
12970
13815
|
CONFIG_PATH7 = join27(homedir28(), ".synkro", "config.env");
|
|
12971
13816
|
}
|
|
12972
13817
|
});
|
|
@@ -12986,8 +13831,8 @@ function canonicalize(pack) {
|
|
|
12986
13831
|
docs: pack.docs ?? []
|
|
12987
13832
|
});
|
|
12988
13833
|
}
|
|
12989
|
-
function computeDigest(
|
|
12990
|
-
return "sha256:" + crypto.createHash("sha256").update(
|
|
13834
|
+
function computeDigest(canonical2) {
|
|
13835
|
+
return "sha256:" + crypto.createHash("sha256").update(canonical2, "utf8").digest("hex");
|
|
12991
13836
|
}
|
|
12992
13837
|
function verifySignature(digest, signatureB64, publicKeyPem) {
|
|
12993
13838
|
try {
|
|
@@ -14216,12 +15061,12 @@ async function runExport(args2) {
|
|
|
14216
15061
|
}
|
|
14217
15062
|
function confirmYesNo(question) {
|
|
14218
15063
|
if (!process.stdin.isTTY) return Promise.resolve(false);
|
|
14219
|
-
return new Promise((
|
|
15064
|
+
return new Promise((resolve7) => {
|
|
14220
15065
|
const rl = createInterface5({ input: process.stdin, output: process.stdout });
|
|
14221
15066
|
rl.question(`${question} (y/N): `, (answer) => {
|
|
14222
15067
|
rl.close();
|
|
14223
15068
|
const t = answer.trim().toLowerCase();
|
|
14224
|
-
|
|
15069
|
+
resolve7(t === "y" || t === "yes");
|
|
14225
15070
|
});
|
|
14226
15071
|
});
|
|
14227
15072
|
}
|
|
@@ -14314,15 +15159,824 @@ Usage:
|
|
|
14314
15159
|
}
|
|
14315
15160
|
});
|
|
14316
15161
|
|
|
15162
|
+
// cli/inventory/identity.ts
|
|
15163
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
15164
|
+
import { existsSync as existsSync36, mkdirSync as mkdirSync19, readFileSync as readFileSync35, renameSync as renameSync9, writeFileSync as writeFileSync24 } from "fs";
|
|
15165
|
+
import { homedir as homedir35 } from "os";
|
|
15166
|
+
import { dirname as dirname9, join as join35 } from "path";
|
|
15167
|
+
function operationalIdentityPath() {
|
|
15168
|
+
return process.env.SYNKRO_OPERATIONAL_IDENTITY_PATH || join35(homedir35(), ".synkro", "installation.json");
|
|
15169
|
+
}
|
|
15170
|
+
function validIdentity(value) {
|
|
15171
|
+
if (!value || typeof value !== "object") return false;
|
|
15172
|
+
const row = value;
|
|
15173
|
+
return typeof row.installation_id === "string" && UUID_RE.test(row.installation_id) && typeof row.created_at === "string" && Number.isFinite(Date.parse(row.created_at));
|
|
15174
|
+
}
|
|
15175
|
+
function writeIdentity(path, identity) {
|
|
15176
|
+
mkdirSync19(dirname9(path), { recursive: true, mode: 448 });
|
|
15177
|
+
const temp = `${path}.${process.pid}.${randomUUID5()}.tmp`;
|
|
15178
|
+
writeFileSync24(temp, JSON.stringify(identity, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
15179
|
+
renameSync9(temp, path);
|
|
15180
|
+
}
|
|
15181
|
+
function getOperationalInstallationIdentity(path = operationalIdentityPath()) {
|
|
15182
|
+
const prior = cached4.get(path);
|
|
15183
|
+
if (prior) return prior;
|
|
15184
|
+
if (existsSync36(path)) {
|
|
15185
|
+
try {
|
|
15186
|
+
const parsed = JSON.parse(readFileSync35(path, "utf8"));
|
|
15187
|
+
if (validIdentity(parsed)) {
|
|
15188
|
+
cached4.set(path, parsed);
|
|
15189
|
+
return parsed;
|
|
15190
|
+
}
|
|
15191
|
+
} catch {
|
|
15192
|
+
}
|
|
15193
|
+
}
|
|
15194
|
+
const identity = { installation_id: randomUUID5(), created_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
15195
|
+
writeIdentity(path, identity);
|
|
15196
|
+
cached4.set(path, identity);
|
|
15197
|
+
return identity;
|
|
15198
|
+
}
|
|
15199
|
+
var UUID_RE, cached4;
|
|
15200
|
+
var init_identity2 = __esm({
|
|
15201
|
+
"cli/inventory/identity.ts"() {
|
|
15202
|
+
"use strict";
|
|
15203
|
+
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
15204
|
+
cached4 = /* @__PURE__ */ new Map();
|
|
15205
|
+
}
|
|
15206
|
+
});
|
|
15207
|
+
|
|
15208
|
+
// cli/inventory/collector.ts
|
|
15209
|
+
import { createHash as createHash5 } from "crypto";
|
|
15210
|
+
import {
|
|
15211
|
+
existsSync as existsSync37,
|
|
15212
|
+
readFileSync as readFileSync36,
|
|
15213
|
+
readdirSync as readdirSync9,
|
|
15214
|
+
statSync as statSync5
|
|
15215
|
+
} from "fs";
|
|
15216
|
+
import { arch, homedir as homedir36, hostname as hostname2, platform as platform5, release } from "os";
|
|
15217
|
+
import { basename as basename3, join as join36, relative, resolve as resolve5 } from "path";
|
|
15218
|
+
import { fileURLToPath } from "url";
|
|
15219
|
+
function sha256(value) {
|
|
15220
|
+
return createHash5("sha256").update(value).digest("hex");
|
|
15221
|
+
}
|
|
15222
|
+
function pseudonymousHostnameHash(installationId, host) {
|
|
15223
|
+
return sha256(`${installationId}:${host}`).slice(0, 32);
|
|
15224
|
+
}
|
|
15225
|
+
function cliVersion() {
|
|
15226
|
+
try {
|
|
15227
|
+
return "1.8.0";
|
|
15228
|
+
} catch {
|
|
15229
|
+
return "0.0.0";
|
|
15230
|
+
}
|
|
15231
|
+
}
|
|
15232
|
+
function readJson(path) {
|
|
15233
|
+
try {
|
|
15234
|
+
if (!existsSync37(path)) return null;
|
|
15235
|
+
const parsed = JSON.parse(readFileSync36(path, "utf8"));
|
|
15236
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
15237
|
+
} catch {
|
|
15238
|
+
return null;
|
|
15239
|
+
}
|
|
15240
|
+
}
|
|
15241
|
+
function readText(path) {
|
|
15242
|
+
try {
|
|
15243
|
+
if (!existsSync37(path)) return "";
|
|
15244
|
+
return readFileSync36(path, "utf8");
|
|
15245
|
+
} catch {
|
|
15246
|
+
return "";
|
|
15247
|
+
}
|
|
15248
|
+
}
|
|
15249
|
+
function urlOrigin(raw) {
|
|
15250
|
+
if (typeof raw !== "string") return void 0;
|
|
15251
|
+
try {
|
|
15252
|
+
const parsed = new URL(raw);
|
|
15253
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
|
|
15254
|
+
return parsed.origin;
|
|
15255
|
+
} catch {
|
|
15256
|
+
return void 0;
|
|
15257
|
+
}
|
|
15258
|
+
}
|
|
15259
|
+
function canonical(raw) {
|
|
15260
|
+
return raw.trim().toLowerCase().replace(/[\s.]+/g, "-").replace(/[^a-z0-9_:@/+-]/g, "") || "unknown";
|
|
15261
|
+
}
|
|
15262
|
+
function safePackageName(command, args2) {
|
|
15263
|
+
if (typeof command !== "string" || !command.trim()) return void 0;
|
|
15264
|
+
const runner = basename3(command.trim()).replace(/\.exe$/i, "");
|
|
15265
|
+
if (Array.isArray(args2) && ["npx", "bunx", "uvx"].includes(runner)) {
|
|
15266
|
+
const pkg = args2.find((arg) => typeof arg === "string" && !arg.startsWith("-"));
|
|
15267
|
+
if (typeof pkg === "string") {
|
|
15268
|
+
const candidate = pkg.trim();
|
|
15269
|
+
if (/^(?:@[a-z0-9_.-]+\/)?[a-z0-9_.-]+(?:@[a-z0-9_.+~-]+)?$/i.test(candidate)) {
|
|
15270
|
+
return candidate;
|
|
15271
|
+
}
|
|
15272
|
+
return basename3(candidate);
|
|
15273
|
+
}
|
|
15274
|
+
}
|
|
15275
|
+
return runner;
|
|
15276
|
+
}
|
|
15277
|
+
function toolNames(entry) {
|
|
15278
|
+
const raw = entry.enabledTools ?? entry.enabled_tools ?? entry.allowedTools ?? entry.allowed_tools ?? entry.tools;
|
|
15279
|
+
if (!Array.isArray(raw)) return [];
|
|
15280
|
+
return [...new Set(raw.map((tool) => typeof tool === "string" ? tool : tool?.name).filter((tool) => typeof tool === "string" && !!tool.trim()))];
|
|
15281
|
+
}
|
|
15282
|
+
function mcpArtifactsFromJson(harness, config, configScope = "user") {
|
|
15283
|
+
if (!config || !config.mcpServers || typeof config.mcpServers !== "object") return [];
|
|
15284
|
+
const artifacts = [];
|
|
15285
|
+
for (const [name, raw] of Object.entries(config.mcpServers)) {
|
|
15286
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
15287
|
+
const entry = raw;
|
|
15288
|
+
const transport = typeof entry.type === "string" ? entry.type : entry.command ? "stdio" : entry.url ? "streamable-http" : "unknown";
|
|
15289
|
+
const tools = toolNames(entry);
|
|
15290
|
+
const scopeId = canonical(configScope);
|
|
15291
|
+
const id = `${canonical(name)}@${scopeId}`;
|
|
15292
|
+
const safeShape = {
|
|
15293
|
+
name,
|
|
15294
|
+
transport,
|
|
15295
|
+
origin: urlOrigin(entry.url),
|
|
15296
|
+
package_name: safePackageName(entry.command, entry.args),
|
|
15297
|
+
enabled: entry.enabled !== false && entry.disabled !== true,
|
|
15298
|
+
tools,
|
|
15299
|
+
scope: configScope
|
|
15300
|
+
};
|
|
15301
|
+
artifacts.push({
|
|
15302
|
+
harness,
|
|
15303
|
+
type: "mcp_server",
|
|
15304
|
+
canonical_id: id,
|
|
15305
|
+
display_name: String(name),
|
|
15306
|
+
enabled: safeShape.enabled,
|
|
15307
|
+
config_scope: configScope,
|
|
15308
|
+
transport: String(transport),
|
|
15309
|
+
endpoint_origin: safeShape.origin,
|
|
15310
|
+
package_name: safeShape.package_name,
|
|
15311
|
+
config_hash: sha256(JSON.stringify(safeShape)),
|
|
15312
|
+
metadata: tools.length ? { tool_names: tools } : void 0
|
|
15313
|
+
});
|
|
15314
|
+
for (const tool of tools) {
|
|
15315
|
+
artifacts.push({
|
|
15316
|
+
harness,
|
|
15317
|
+
type: "mcp_tool",
|
|
15318
|
+
canonical_id: `${id}:${canonical(tool)}`,
|
|
15319
|
+
display_name: tool,
|
|
15320
|
+
enabled: safeShape.enabled,
|
|
15321
|
+
config_scope: configScope,
|
|
15322
|
+
metadata: { source: String(name) }
|
|
15323
|
+
});
|
|
15324
|
+
}
|
|
15325
|
+
}
|
|
15326
|
+
return artifacts;
|
|
15327
|
+
}
|
|
15328
|
+
function claudeDesktopConfigCandidates(home, targetPlatform) {
|
|
15329
|
+
if (targetPlatform === "darwin") {
|
|
15330
|
+
return [join36(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")];
|
|
15331
|
+
}
|
|
15332
|
+
if (targetPlatform === "linux") {
|
|
15333
|
+
return [
|
|
15334
|
+
join36(home, ".config", "Claude", "claude_desktop_config.json"),
|
|
15335
|
+
join36(home, ".config", "claude", "claude_desktop_config.json")
|
|
15336
|
+
];
|
|
15337
|
+
}
|
|
15338
|
+
if (targetPlatform === "win32" && process.env.APPDATA) {
|
|
15339
|
+
return [join36(process.env.APPDATA, "Claude", "claude_desktop_config.json")];
|
|
15340
|
+
}
|
|
15341
|
+
return [];
|
|
15342
|
+
}
|
|
15343
|
+
function claudeManagedMcpConfigCandidates(targetPlatform) {
|
|
15344
|
+
if (targetPlatform === "darwin") return ["/Library/Application Support/ClaudeCode/managed-mcp.json"];
|
|
15345
|
+
if (targetPlatform === "linux") return ["/etc/claude-code/managed-mcp.json"];
|
|
15346
|
+
if (targetPlatform === "win32" && process.env.ProgramFiles) {
|
|
15347
|
+
return [join36(process.env.ProgramFiles, "ClaudeCode", "managed-mcp.json")];
|
|
15348
|
+
}
|
|
15349
|
+
return [];
|
|
15350
|
+
}
|
|
15351
|
+
function discoveredProjectRoots(claudeState, currentDirectory, explicit = [], cursorRoots = []) {
|
|
15352
|
+
const roots = /* @__PURE__ */ new Set();
|
|
15353
|
+
const add = (value) => {
|
|
15354
|
+
if (typeof value !== "string" || !value.trim()) return;
|
|
15355
|
+
const path = resolve5(value);
|
|
15356
|
+
if (existsSync37(path)) roots.add(path);
|
|
15357
|
+
};
|
|
15358
|
+
add(currentDirectory);
|
|
15359
|
+
for (const path of explicit) add(path);
|
|
15360
|
+
for (const path of cursorRoots) add(path);
|
|
15361
|
+
if (claudeState?.projects && typeof claudeState.projects === "object") {
|
|
15362
|
+
for (const path of Object.keys(claudeState.projects)) add(path);
|
|
15363
|
+
}
|
|
15364
|
+
return [...roots];
|
|
15365
|
+
}
|
|
15366
|
+
function cursorWorkspaceStorageCandidates(home, targetPlatform) {
|
|
15367
|
+
if (targetPlatform === "darwin") return [join36(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage")];
|
|
15368
|
+
if (targetPlatform === "linux") return [join36(home, ".config", "Cursor", "User", "workspaceStorage")];
|
|
15369
|
+
if (targetPlatform === "win32" && process.env.APPDATA) {
|
|
15370
|
+
return [join36(process.env.APPDATA, "Cursor", "User", "workspaceStorage")];
|
|
15371
|
+
}
|
|
15372
|
+
return [];
|
|
15373
|
+
}
|
|
15374
|
+
function cursorWorkspaceRoots(home, targetPlatform) {
|
|
15375
|
+
const roots = /* @__PURE__ */ new Set();
|
|
15376
|
+
for (const storage of cursorWorkspaceStorageCandidates(home, targetPlatform)) {
|
|
15377
|
+
if (!existsSync37(storage)) continue;
|
|
15378
|
+
let entries = [];
|
|
15379
|
+
try {
|
|
15380
|
+
entries = readdirSync9(storage, { withFileTypes: true });
|
|
15381
|
+
} catch {
|
|
15382
|
+
continue;
|
|
15383
|
+
}
|
|
15384
|
+
for (const entry of entries) {
|
|
15385
|
+
if (!entry.isDirectory() || entry.isSymbolicLink?.()) continue;
|
|
15386
|
+
const state = readJson(join36(storage, entry.name, "workspace.json"));
|
|
15387
|
+
const raw = state?.folder;
|
|
15388
|
+
if (typeof raw !== "string" || !raw.trim()) continue;
|
|
15389
|
+
try {
|
|
15390
|
+
const path = raw.startsWith("file:") ? fileURLToPath(raw) : raw;
|
|
15391
|
+
if (existsSync37(path)) roots.add(resolve5(path));
|
|
15392
|
+
} catch {
|
|
15393
|
+
}
|
|
15394
|
+
}
|
|
15395
|
+
}
|
|
15396
|
+
return [...roots];
|
|
15397
|
+
}
|
|
15398
|
+
function codexMcpArtifacts(content) {
|
|
15399
|
+
const artifacts = [];
|
|
15400
|
+
const sections = [...content.matchAll(/^\s*\[\s*([^\]]+)\s*\]\s*$/gm)];
|
|
15401
|
+
for (let index = 0; index < sections.length && artifacts.length < 1e3; index++) {
|
|
15402
|
+
const section = sections[index];
|
|
15403
|
+
const key = section[1].trim();
|
|
15404
|
+
const root = key.match(/^mcp_servers\s*\.\s*(?:"((?:[^"\\]|\\.)+)"|'([^']+)'|([A-Za-z0-9_-]+))$/);
|
|
15405
|
+
if (!root) continue;
|
|
15406
|
+
let name = root[1] || root[2] || root[3];
|
|
15407
|
+
if (root[1]) {
|
|
15408
|
+
try {
|
|
15409
|
+
name = JSON.parse(`"${root[1]}"`);
|
|
15410
|
+
} catch {
|
|
15411
|
+
continue;
|
|
15412
|
+
}
|
|
15413
|
+
}
|
|
15414
|
+
const start = (section.index ?? 0) + section[0].length;
|
|
15415
|
+
const end = sections[index + 1]?.index ?? content.length;
|
|
15416
|
+
const block = content.slice(start, end);
|
|
15417
|
+
const stringValue = (key2) => {
|
|
15418
|
+
const found = block.match(new RegExp(`^\\s*${key2}\\s*=\\s*("(?:[^"\\\\]|\\\\.)*")`, "m"));
|
|
15419
|
+
if (!found) return void 0;
|
|
15420
|
+
try {
|
|
15421
|
+
return JSON.parse(found[1]);
|
|
15422
|
+
} catch {
|
|
15423
|
+
return void 0;
|
|
15424
|
+
}
|
|
15425
|
+
};
|
|
15426
|
+
const enabled = !/^\s*enabled\s*=\s*false\s*$/m.test(block);
|
|
15427
|
+
const url = stringValue("url");
|
|
15428
|
+
const command = stringValue("command");
|
|
15429
|
+
const safeShape = { name, enabled, origin: urlOrigin(url), package_name: safePackageName(command, []) };
|
|
15430
|
+
artifacts.push({
|
|
15431
|
+
harness: "codex",
|
|
15432
|
+
type: "mcp_server",
|
|
15433
|
+
canonical_id: `${canonical(name)}@user`,
|
|
15434
|
+
display_name: name,
|
|
15435
|
+
enabled,
|
|
15436
|
+
config_scope: "user",
|
|
15437
|
+
transport: command ? "stdio" : url ? "streamable-http" : "unknown",
|
|
15438
|
+
endpoint_origin: safeShape.origin,
|
|
15439
|
+
package_name: safeShape.package_name,
|
|
15440
|
+
config_hash: sha256(JSON.stringify(safeShape))
|
|
15441
|
+
});
|
|
15442
|
+
}
|
|
15443
|
+
return artifacts;
|
|
15444
|
+
}
|
|
15445
|
+
function flattenHookEntries(value) {
|
|
15446
|
+
if (!Array.isArray(value)) return [];
|
|
15447
|
+
const out = [];
|
|
15448
|
+
for (const entry of value) {
|
|
15449
|
+
if (!entry || typeof entry !== "object") continue;
|
|
15450
|
+
const record = entry;
|
|
15451
|
+
if (typeof record.command === "string") out.push(record);
|
|
15452
|
+
if (Array.isArray(record.hooks)) out.push(...flattenHookEntries(record.hooks));
|
|
15453
|
+
}
|
|
15454
|
+
return out;
|
|
15455
|
+
}
|
|
15456
|
+
function hookArtifacts(harness, config) {
|
|
15457
|
+
if (!config?.hooks || typeof config.hooks !== "object") return [];
|
|
15458
|
+
const artifacts = [];
|
|
15459
|
+
for (const [event, rawEntries] of Object.entries(config.hooks)) {
|
|
15460
|
+
for (const entry of flattenHookEntries(rawEntries)) {
|
|
15461
|
+
const commandHash = sha256(entry.command);
|
|
15462
|
+
const managed = entry.__synkro_managed__ === true || /[\\/]\.synkro[\\/]hooks[\\/]/.test(entry.command);
|
|
15463
|
+
artifacts.push({
|
|
15464
|
+
harness,
|
|
15465
|
+
type: "hook",
|
|
15466
|
+
canonical_id: `${canonical(event)}:${commandHash.slice(0, 20)}`,
|
|
15467
|
+
display_name: `${event} \xB7 ${basename3(String(entry.command).split(/\s+/)[0] || "hook")}`,
|
|
15468
|
+
enabled: entry.enabled !== false,
|
|
15469
|
+
config_scope: "user",
|
|
15470
|
+
package_name: basename3(String(entry.command).split(/\s+/)[0] || "") || void 0,
|
|
15471
|
+
config_hash: commandHash,
|
|
15472
|
+
metadata: { managed, events: [event] }
|
|
15473
|
+
});
|
|
15474
|
+
}
|
|
15475
|
+
}
|
|
15476
|
+
return artifacts;
|
|
15477
|
+
}
|
|
15478
|
+
function parseFrontmatter(content) {
|
|
15479
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
15480
|
+
if (!match) return {};
|
|
15481
|
+
const value = (key) => match[1].match(new RegExp(`^${key}:\\s*["']?([^"'\\n]+)`, "m"))?.[1]?.trim();
|
|
15482
|
+
return { name: value("name"), version: value("version") };
|
|
15483
|
+
}
|
|
15484
|
+
function skillArtifacts(harness, root) {
|
|
15485
|
+
if (!existsSync37(root)) return [];
|
|
15486
|
+
const manifests = [];
|
|
15487
|
+
const visit = (dir) => {
|
|
15488
|
+
let entries;
|
|
15489
|
+
try {
|
|
15490
|
+
entries = readdirSync9(dir, { withFileTypes: true });
|
|
15491
|
+
} catch {
|
|
15492
|
+
return;
|
|
15493
|
+
}
|
|
15494
|
+
for (const entry of entries) {
|
|
15495
|
+
if (entry.isSymbolicLink?.()) continue;
|
|
15496
|
+
const path = join36(dir, entry.name);
|
|
15497
|
+
if (entry.isFile() && entry.name === "SKILL.md") manifests.push(path);
|
|
15498
|
+
else if (entry.isDirectory()) visit(path);
|
|
15499
|
+
}
|
|
15500
|
+
};
|
|
15501
|
+
visit(root);
|
|
15502
|
+
return manifests.map((path) => {
|
|
15503
|
+
const content = readText(path);
|
|
15504
|
+
const frontmatter = parseFrontmatter(content);
|
|
15505
|
+
const rel = relative(root, path).replaceAll("\\", "/");
|
|
15506
|
+
const name = frontmatter.name || basename3(join36(path, "..")) || "skill";
|
|
15507
|
+
return {
|
|
15508
|
+
harness,
|
|
15509
|
+
type: "skill",
|
|
15510
|
+
canonical_id: `${canonical(name)}:${sha256(rel).slice(0, 16)}`,
|
|
15511
|
+
display_name: name,
|
|
15512
|
+
version: frontmatter.version,
|
|
15513
|
+
enabled: true,
|
|
15514
|
+
config_scope: "user",
|
|
15515
|
+
config_hash: content ? sha256(content) : void 0,
|
|
15516
|
+
metadata: { source: "SKILL.md" }
|
|
15517
|
+
};
|
|
15518
|
+
});
|
|
15519
|
+
}
|
|
15520
|
+
function cursorExtensionArtifacts(root) {
|
|
15521
|
+
if (!existsSync37(root)) return [];
|
|
15522
|
+
let dirs = [];
|
|
15523
|
+
try {
|
|
15524
|
+
dirs = readdirSync9(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.isSymbolicLink());
|
|
15525
|
+
} catch {
|
|
15526
|
+
return [];
|
|
15527
|
+
}
|
|
15528
|
+
const artifacts = [];
|
|
15529
|
+
for (const dir of dirs) {
|
|
15530
|
+
const pkg = readJson(join36(root, dir.name, "package.json"));
|
|
15531
|
+
if (!pkg) continue;
|
|
15532
|
+
const publisher = typeof pkg.publisher === "string" ? pkg.publisher : void 0;
|
|
15533
|
+
const name = typeof pkg.name === "string" ? pkg.name : dir.name;
|
|
15534
|
+
const id = publisher ? `${publisher}.${name}` : name;
|
|
15535
|
+
artifacts.push({
|
|
15536
|
+
harness: "cursor",
|
|
15537
|
+
type: "extension",
|
|
15538
|
+
canonical_id: canonical(id),
|
|
15539
|
+
display_name: String(pkg.displayName || name),
|
|
15540
|
+
version: typeof pkg.version === "string" ? pkg.version : void 0,
|
|
15541
|
+
enabled: true,
|
|
15542
|
+
config_scope: "user",
|
|
15543
|
+
config_hash: sha256(JSON.stringify({ id, version: pkg.version })),
|
|
15544
|
+
metadata: publisher ? { publisher } : void 0
|
|
15545
|
+
});
|
|
15546
|
+
}
|
|
15547
|
+
return artifacts;
|
|
15548
|
+
}
|
|
15549
|
+
function deploymentMode2(home) {
|
|
15550
|
+
const raw = readText(join36(home, ".synkro", "config.env"));
|
|
15551
|
+
const value = (key) => raw.match(new RegExp(`^${key}=['"]?([^'"\\n]*)`, "m"))?.[1]?.toLowerCase();
|
|
15552
|
+
if (value("SYNKRO_GRADING_MODE") === "byok") return "byok";
|
|
15553
|
+
if (value("SYNKRO_STORAGE_MODE") === "cloud") return "cloud";
|
|
15554
|
+
return "local";
|
|
15555
|
+
}
|
|
15556
|
+
function telemetryHealth(home) {
|
|
15557
|
+
const meta = readJson(join36(home, ".synkro", "telemetry-meta.json"));
|
|
15558
|
+
const health = {};
|
|
15559
|
+
if (meta?.last_flush_ok_at && Number.isFinite(Date.parse(meta.last_flush_ok_at))) health.telemetry_last_flush_at = meta.last_flush_ok_at;
|
|
15560
|
+
if (meta?.last_flush_error) health.telemetry_last_error = "flush_failed";
|
|
15561
|
+
const queue = join36(home, ".synkro", "telemetry-pending.jsonl");
|
|
15562
|
+
try {
|
|
15563
|
+
const size = statSync5(queue).size;
|
|
15564
|
+
health.telemetry_backlog = size <= 5 * 1024 * 1024 ? readFileSync36(queue, "utf8").split("\n").filter(Boolean).length : Math.ceil(size / 1024);
|
|
15565
|
+
} catch {
|
|
15566
|
+
}
|
|
15567
|
+
return health;
|
|
15568
|
+
}
|
|
15569
|
+
function harnessSnapshot(agent) {
|
|
15570
|
+
if (agent.kind === "claude_code") {
|
|
15571
|
+
const config2 = readJson(agent.settingsPath);
|
|
15572
|
+
const coverage2 = inspectCCHooks(agent.settingsPath);
|
|
15573
|
+
return {
|
|
15574
|
+
row: {
|
|
15575
|
+
harness: "claude_code",
|
|
15576
|
+
installed: true,
|
|
15577
|
+
enabled: coverage2.installed,
|
|
15578
|
+
version: agent.version,
|
|
15579
|
+
permission_mode: config2?.permissions?.defaultMode,
|
|
15580
|
+
hook_coverage: coverage2,
|
|
15581
|
+
hook_config_hash: sha256(JSON.stringify(coverage2)),
|
|
15582
|
+
config_scope: "user"
|
|
15583
|
+
},
|
|
15584
|
+
config: config2
|
|
15585
|
+
};
|
|
15586
|
+
}
|
|
15587
|
+
if (agent.kind === "cursor") {
|
|
15588
|
+
const config2 = readJson(agent.settingsPath);
|
|
15589
|
+
const coverage2 = inspectCursorHooks(agent.settingsPath);
|
|
15590
|
+
return {
|
|
15591
|
+
row: {
|
|
15592
|
+
harness: "cursor",
|
|
15593
|
+
installed: true,
|
|
15594
|
+
enabled: coverage2.installed,
|
|
15595
|
+
version: agent.version,
|
|
15596
|
+
hook_coverage: coverage2,
|
|
15597
|
+
hook_config_hash: sha256(JSON.stringify(coverage2)),
|
|
15598
|
+
config_scope: "user"
|
|
15599
|
+
},
|
|
15600
|
+
config: config2
|
|
15601
|
+
};
|
|
15602
|
+
}
|
|
15603
|
+
const config = readJson(agent.settingsPath);
|
|
15604
|
+
const coverage = inspectCodexHooks(agent.settingsPath);
|
|
15605
|
+
const toml = readText(join36(agent.configDir, "config.toml"));
|
|
15606
|
+
const permission = toml.match(/^\s*approval_policy\s*=\s*["']([^"']+)/m)?.[1];
|
|
15607
|
+
return {
|
|
15608
|
+
row: {
|
|
15609
|
+
harness: "codex",
|
|
15610
|
+
installed: true,
|
|
15611
|
+
enabled: coverage.installed,
|
|
15612
|
+
version: agent.version,
|
|
15613
|
+
permission_mode: permission,
|
|
15614
|
+
hook_coverage: coverage,
|
|
15615
|
+
hook_config_hash: sha256(JSON.stringify(coverage)),
|
|
15616
|
+
config_scope: "user"
|
|
15617
|
+
},
|
|
15618
|
+
config
|
|
15619
|
+
};
|
|
15620
|
+
}
|
|
15621
|
+
function collectOperationalInventory(options = {}) {
|
|
15622
|
+
const home = options.homeDir ?? homedir36();
|
|
15623
|
+
const detected = options.detectedAgents ?? detectAgents();
|
|
15624
|
+
const identity = getOperationalInstallationIdentity(options.identityPath);
|
|
15625
|
+
const targetPlatform = options.platformName ?? platform5();
|
|
15626
|
+
const codexHome = options.homeDir ? join36(home, ".codex") : process.env.CODEX_HOME || join36(home, ".codex");
|
|
15627
|
+
const harnesses = [];
|
|
15628
|
+
const artifacts = [];
|
|
15629
|
+
for (const agent of detected) {
|
|
15630
|
+
const { row, config } = harnessSnapshot(agent);
|
|
15631
|
+
harnesses.push(row);
|
|
15632
|
+
artifacts.push(...hookArtifacts(row.harness, config));
|
|
15633
|
+
}
|
|
15634
|
+
const claudeJson = readJson(join36(home, ".claude.json"));
|
|
15635
|
+
artifacts.push(...mcpArtifactsFromJson("claude_code", claudeJson));
|
|
15636
|
+
if (claudeJson?.projects && typeof claudeJson.projects === "object") {
|
|
15637
|
+
for (const [projectPath, project] of Object.entries(claudeJson.projects)) {
|
|
15638
|
+
if (!project || typeof project !== "object") continue;
|
|
15639
|
+
artifacts.push(...mcpArtifactsFromJson("claude_code", project, `local:${sha256(projectPath).slice(0, 16)}`));
|
|
15640
|
+
}
|
|
15641
|
+
}
|
|
15642
|
+
artifacts.push(...mcpArtifactsFromJson("cursor", readJson(join36(home, ".cursor", "mcp.json"))));
|
|
15643
|
+
artifacts.push(...codexMcpArtifacts(readText(join36(codexHome, "config.toml"))));
|
|
15644
|
+
const projectRoots = discoveredProjectRoots(
|
|
15645
|
+
claudeJson,
|
|
15646
|
+
options.currentDirectory ?? process.cwd(),
|
|
15647
|
+
options.projectRoots,
|
|
15648
|
+
cursorWorkspaceRoots(home, targetPlatform)
|
|
15649
|
+
);
|
|
15650
|
+
for (const projectRoot of projectRoots) {
|
|
15651
|
+
const scopeHash = sha256(projectRoot).slice(0, 16);
|
|
15652
|
+
artifacts.push(...mcpArtifactsFromJson(
|
|
15653
|
+
"claude_code",
|
|
15654
|
+
readJson(join36(projectRoot, ".mcp.json")),
|
|
15655
|
+
`project:${scopeHash}`
|
|
15656
|
+
));
|
|
15657
|
+
const cursorProjectConfig = join36(projectRoot, ".cursor", "mcp.json");
|
|
15658
|
+
if (resolve5(cursorProjectConfig) !== resolve5(join36(home, ".cursor", "mcp.json"))) {
|
|
15659
|
+
artifacts.push(...mcpArtifactsFromJson(
|
|
15660
|
+
"cursor",
|
|
15661
|
+
readJson(cursorProjectConfig),
|
|
15662
|
+
`project:${scopeHash}`
|
|
15663
|
+
));
|
|
15664
|
+
}
|
|
15665
|
+
}
|
|
15666
|
+
for (const managedPath of claudeManagedMcpConfigCandidates(targetPlatform)) {
|
|
15667
|
+
artifacts.push(...mcpArtifactsFromJson("claude_code", readJson(managedPath), "managed"));
|
|
15668
|
+
}
|
|
15669
|
+
const desktopConfigPath = claudeDesktopConfigCandidates(home, targetPlatform).find((path) => existsSync37(path));
|
|
15670
|
+
if (desktopConfigPath) {
|
|
15671
|
+
const desktopConfig = readJson(desktopConfigPath);
|
|
15672
|
+
harnesses.push({
|
|
15673
|
+
harness: "claude_desktop",
|
|
15674
|
+
installed: true,
|
|
15675
|
+
enabled: true,
|
|
15676
|
+
config_scope: "user"
|
|
15677
|
+
});
|
|
15678
|
+
artifacts.push(...mcpArtifactsFromJson("claude_desktop", desktopConfig));
|
|
15679
|
+
}
|
|
15680
|
+
const claudeSettings = readJson(join36(home, ".claude", "settings.json"));
|
|
15681
|
+
if (claudeSettings?.enabledPlugins && typeof claudeSettings.enabledPlugins === "object") {
|
|
15682
|
+
for (const [name, enabled] of Object.entries(claudeSettings.enabledPlugins)) {
|
|
15683
|
+
artifacts.push({
|
|
15684
|
+
harness: "claude_code",
|
|
15685
|
+
type: "plugin",
|
|
15686
|
+
canonical_id: canonical(name),
|
|
15687
|
+
display_name: name,
|
|
15688
|
+
enabled: enabled === true,
|
|
15689
|
+
config_scope: "user",
|
|
15690
|
+
metadata: { source: "enabledPlugins" }
|
|
15691
|
+
});
|
|
15692
|
+
}
|
|
15693
|
+
}
|
|
15694
|
+
artifacts.push(...skillArtifacts("claude_code", join36(home, ".claude", "skills")));
|
|
15695
|
+
artifacts.push(...skillArtifacts("cursor", join36(home, ".cursor", "skills")));
|
|
15696
|
+
artifacts.push(...skillArtifacts("codex", join36(codexHome, "skills")));
|
|
15697
|
+
artifacts.push(...cursorExtensionArtifacts(join36(home, ".cursor", "extensions")));
|
|
15698
|
+
const uniqueArtifacts = /* @__PURE__ */ new Map();
|
|
15699
|
+
for (const artifact of artifacts) {
|
|
15700
|
+
const key = `${artifact.harness || "global"}:${artifact.type}:${artifact.canonical_id}`;
|
|
15701
|
+
uniqueArtifacts.set(key, artifact);
|
|
15702
|
+
}
|
|
15703
|
+
const codingHarnesses = harnesses.filter((row) => row.harness === "claude_code" || row.harness === "cursor" || row.harness === "codex");
|
|
15704
|
+
const health = telemetryHealth(home) ?? {};
|
|
15705
|
+
health.scanners = {
|
|
15706
|
+
hook_runtime: codingHarnesses.length === 0 ? "unknown" : codingHarnesses.every((row) => row.enabled) ? "ok" : "degraded"
|
|
15707
|
+
};
|
|
15708
|
+
return {
|
|
15709
|
+
schema_version: 1,
|
|
15710
|
+
collected_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15711
|
+
installation: {
|
|
15712
|
+
install_id: identity.installation_id,
|
|
15713
|
+
hostname_hash: pseudonymousHostnameHash(identity.installation_id, hostname2()),
|
|
15714
|
+
platform: targetPlatform,
|
|
15715
|
+
os_version: release(),
|
|
15716
|
+
arch: arch(),
|
|
15717
|
+
cli_version: cliVersion(),
|
|
15718
|
+
node_version: process.version,
|
|
15719
|
+
bun_version: process.versions.bun,
|
|
15720
|
+
deployment_mode: deploymentMode2(home)
|
|
15721
|
+
},
|
|
15722
|
+
harnesses,
|
|
15723
|
+
artifacts: [...uniqueArtifacts.values()],
|
|
15724
|
+
health
|
|
15725
|
+
};
|
|
15726
|
+
}
|
|
15727
|
+
var init_collector = __esm({
|
|
15728
|
+
"cli/inventory/collector.ts"() {
|
|
15729
|
+
"use strict";
|
|
15730
|
+
init_agentDetect();
|
|
15731
|
+
init_ccHookConfig();
|
|
15732
|
+
init_cursorHookConfig();
|
|
15733
|
+
init_codexHookConfig();
|
|
15734
|
+
init_identity2();
|
|
15735
|
+
}
|
|
15736
|
+
});
|
|
15737
|
+
|
|
15738
|
+
// cli/inventory/sync.ts
|
|
15739
|
+
var sync_exports2 = {};
|
|
15740
|
+
__export(sync_exports2, {
|
|
15741
|
+
inventorySnapshotChunks: () => inventorySnapshotChunks,
|
|
15742
|
+
inventorySyncTarget: () => inventorySyncTarget,
|
|
15743
|
+
resolveInventoryGateway: () => resolveInventoryGateway,
|
|
15744
|
+
resolveLocalInventoryUrl: () => resolveLocalInventoryUrl,
|
|
15745
|
+
shouldSyncInventory: () => shouldSyncInventory,
|
|
15746
|
+
syncOperationalInventory: () => syncOperationalInventory,
|
|
15747
|
+
syncOperationalInventoryDetached: () => syncOperationalInventoryDetached
|
|
15748
|
+
});
|
|
15749
|
+
import { createHash as createHash6, randomUUID as randomUUID6 } from "crypto";
|
|
15750
|
+
import { spawn as spawn8 } from "child_process";
|
|
15751
|
+
import {
|
|
15752
|
+
existsSync as existsSync38,
|
|
15753
|
+
mkdirSync as mkdirSync20,
|
|
15754
|
+
readFileSync as readFileSync37,
|
|
15755
|
+
renameSync as renameSync10,
|
|
15756
|
+
writeFileSync as writeFileSync25
|
|
15757
|
+
} from "fs";
|
|
15758
|
+
import { homedir as homedir37 } from "os";
|
|
15759
|
+
import { dirname as dirname10, join as join37 } from "path";
|
|
15760
|
+
function syncStatePath() {
|
|
15761
|
+
return process.env.SYNKRO_INVENTORY_SYNC_STATE_PATH || join37(homedir37(), ".synkro", "inventory-sync.json");
|
|
15762
|
+
}
|
|
15763
|
+
function readState(path = syncStatePath()) {
|
|
15764
|
+
try {
|
|
15765
|
+
const parsed = JSON.parse(readFileSync37(path, "utf8"));
|
|
15766
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
15767
|
+
} catch {
|
|
15768
|
+
return {};
|
|
15769
|
+
}
|
|
15770
|
+
}
|
|
15771
|
+
function writeState(state, path = syncStatePath()) {
|
|
15772
|
+
try {
|
|
15773
|
+
mkdirSync20(dirname10(path), { recursive: true, mode: 448 });
|
|
15774
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
15775
|
+
writeFileSync25(temp, JSON.stringify(state, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
15776
|
+
renameSync10(temp, path);
|
|
15777
|
+
} catch {
|
|
15778
|
+
}
|
|
15779
|
+
}
|
|
15780
|
+
function shouldSyncInventory(state, now = Date.now(), target) {
|
|
15781
|
+
if (target && state.last_target !== target) return true;
|
|
15782
|
+
const lastOk = state.last_ok_at ? Date.parse(state.last_ok_at) : 0;
|
|
15783
|
+
if (Number.isFinite(lastOk) && lastOk > 0 && now - lastOk < SUCCESS_INTERVAL_MS) return false;
|
|
15784
|
+
const lastAttempt = state.last_attempt_at ? Date.parse(state.last_attempt_at) : 0;
|
|
15785
|
+
return !Number.isFinite(lastAttempt) || lastAttempt <= 0 || now - lastAttempt >= FAILURE_RETRY_MS;
|
|
15786
|
+
}
|
|
15787
|
+
function readConfig() {
|
|
15788
|
+
const path = join37(homedir37(), ".synkro", "config.env");
|
|
15789
|
+
const out = {};
|
|
15790
|
+
try {
|
|
15791
|
+
for (const rawLine of readFileSync37(path, "utf8").split("\n")) {
|
|
15792
|
+
const line = rawLine.trim();
|
|
15793
|
+
if (!line || line.startsWith("#")) continue;
|
|
15794
|
+
const index = line.indexOf("=");
|
|
15795
|
+
if (index <= 0) continue;
|
|
15796
|
+
const key = line.slice(0, index).trim();
|
|
15797
|
+
let value = line.slice(index + 1).trim();
|
|
15798
|
+
if (value.startsWith("'") && value.endsWith("'") || value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
|
|
15799
|
+
out[key] = value;
|
|
15800
|
+
}
|
|
15801
|
+
} catch {
|
|
15802
|
+
}
|
|
15803
|
+
return out;
|
|
15804
|
+
}
|
|
15805
|
+
function inventorySyncTarget(config, env = process.env) {
|
|
15806
|
+
return (config.SYNKRO_STORAGE_MODE || env.SYNKRO_STORAGE_MODE) === "cloud" ? "cloud" : "local";
|
|
15807
|
+
}
|
|
15808
|
+
function resolveLocalInventoryUrl(rawPort) {
|
|
15809
|
+
const parsed = Number.parseInt(rawPort || "", 10);
|
|
15810
|
+
const port = Number.isInteger(parsed) && parsed >= 1 && parsed <= 65535 ? parsed : 18931;
|
|
15811
|
+
return `http://127.0.0.1:${port}/api/local/inventory/snapshot`;
|
|
15812
|
+
}
|
|
15813
|
+
function localhost(host) {
|
|
15814
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
15815
|
+
}
|
|
15816
|
+
function resolveInventoryGateway(raw) {
|
|
15817
|
+
if (!raw) return DEFAULT_GATEWAY2;
|
|
15818
|
+
try {
|
|
15819
|
+
const url = new URL(raw);
|
|
15820
|
+
const host = url.hostname.toLowerCase();
|
|
15821
|
+
const allowedHost = localhost(host) || host === "synkro.sh" || host.endsWith(".synkro.sh");
|
|
15822
|
+
const allowedProtocol = url.protocol === "https:" || url.protocol === "http:" && localhost(host);
|
|
15823
|
+
return allowedHost && allowedProtocol ? raw.replace(/\/$/, "") : DEFAULT_GATEWAY2;
|
|
15824
|
+
} catch {
|
|
15825
|
+
return DEFAULT_GATEWAY2;
|
|
15826
|
+
}
|
|
15827
|
+
}
|
|
15828
|
+
async function loadToken() {
|
|
15829
|
+
try {
|
|
15830
|
+
const durable = readFileSync37(join37(homedir37(), ".synkro", ".mcp-jwt"), "utf8").trim();
|
|
15831
|
+
if (durable) return durable;
|
|
15832
|
+
} catch {
|
|
15833
|
+
}
|
|
15834
|
+
try {
|
|
15835
|
+
const auth = await Promise.resolve().then(() => (init_stub(), stub_exports));
|
|
15836
|
+
if (!auth.isAuthenticated()) return null;
|
|
15837
|
+
await auth.ensureValidToken();
|
|
15838
|
+
return auth.getAccessToken();
|
|
15839
|
+
} catch {
|
|
15840
|
+
return null;
|
|
15841
|
+
}
|
|
15842
|
+
}
|
|
15843
|
+
function stable(value) {
|
|
15844
|
+
if (Array.isArray(value)) return value.map(stable);
|
|
15845
|
+
if (!value || typeof value !== "object") return value;
|
|
15846
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, child]) => [key, stable(child)]));
|
|
15847
|
+
}
|
|
15848
|
+
function inventorySnapshotChunks(snapshot, maxBytes = INVENTORY_CHUNK_BYTES) {
|
|
15849
|
+
const { collected_at: _heartbeat, ...material } = snapshot;
|
|
15850
|
+
const payloadHash = createHash6("sha256").update(JSON.stringify(stable(material))).digest("hex");
|
|
15851
|
+
const snapshotId = randomUUID6();
|
|
15852
|
+
const base = { ...snapshot, artifacts: [] };
|
|
15853
|
+
const baseBytes = Buffer.byteLength(JSON.stringify(base));
|
|
15854
|
+
const chunks = [];
|
|
15855
|
+
let artifacts = [];
|
|
15856
|
+
let bytes = baseBytes;
|
|
15857
|
+
for (const artifact of snapshot.artifacts) {
|
|
15858
|
+
const artifactBytes = Buffer.byteLength(JSON.stringify(artifact)) + 1;
|
|
15859
|
+
if (artifacts.length > 0 && bytes + artifactBytes > maxBytes) {
|
|
15860
|
+
chunks.push({ ...snapshot, artifacts });
|
|
15861
|
+
artifacts = [];
|
|
15862
|
+
bytes = baseBytes;
|
|
15863
|
+
}
|
|
15864
|
+
artifacts.push(artifact);
|
|
15865
|
+
bytes += artifactBytes;
|
|
15866
|
+
}
|
|
15867
|
+
if (artifacts.length > 0 || chunks.length === 0) chunks.push({ ...snapshot, artifacts });
|
|
15868
|
+
return { snapshotId, payloadHash, chunks };
|
|
15869
|
+
}
|
|
15870
|
+
async function postInventoryChunks(url, headers, snapshot) {
|
|
15871
|
+
const delivery = inventorySnapshotChunks(snapshot);
|
|
15872
|
+
let response = null;
|
|
15873
|
+
for (let index = 0; index < delivery.chunks.length; index++) {
|
|
15874
|
+
response = await fetch(url, {
|
|
15875
|
+
method: "POST",
|
|
15876
|
+
headers: {
|
|
15877
|
+
...headers,
|
|
15878
|
+
"Content-Type": "application/json",
|
|
15879
|
+
"X-Synkro-Inventory-Snapshot-Id": delivery.snapshotId,
|
|
15880
|
+
"X-Synkro-Inventory-Payload-Hash": delivery.payloadHash,
|
|
15881
|
+
"X-Synkro-Inventory-Chunk-Index": String(index),
|
|
15882
|
+
"X-Synkro-Inventory-Chunk-Count": String(delivery.chunks.length),
|
|
15883
|
+
"X-Synkro-Inventory-Artifact-Count": String(snapshot.artifacts.length)
|
|
15884
|
+
},
|
|
15885
|
+
body: JSON.stringify(delivery.chunks[index]),
|
|
15886
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
|
|
15887
|
+
});
|
|
15888
|
+
if (!response.ok) return response;
|
|
15889
|
+
}
|
|
15890
|
+
return response;
|
|
15891
|
+
}
|
|
15892
|
+
async function syncOperationalInventory(options = {}) {
|
|
15893
|
+
const path = syncStatePath();
|
|
15894
|
+
const state = readState(path);
|
|
15895
|
+
const config = readConfig();
|
|
15896
|
+
const target = inventorySyncTarget(config);
|
|
15897
|
+
if (!options.force && !shouldSyncInventory(state, Date.now(), target)) return { ok: true, skipped: "throttled", target };
|
|
15898
|
+
const attemptedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
15899
|
+
writeState({ ...state, last_attempt_at: attemptedAt, last_target: target }, path);
|
|
15900
|
+
try {
|
|
15901
|
+
const snapshot = collectOperationalInventory();
|
|
15902
|
+
if (target === "local") {
|
|
15903
|
+
const response2 = await postInventoryChunks(resolveLocalInventoryUrl(
|
|
15904
|
+
process.env.SYNKRO_HOST_MCP_PORT || process.env.SYNKRO_MCP_PORT || config.SYNKRO_MCP_PORT
|
|
15905
|
+
), {}, snapshot);
|
|
15906
|
+
if (!response2.ok) {
|
|
15907
|
+
const error = `http_${response2.status}`;
|
|
15908
|
+
writeState({ ...state, last_attempt_at: attemptedAt, last_error: error, last_target: target }, path);
|
|
15909
|
+
return { ok: false, status: response2.status, error, target };
|
|
15910
|
+
}
|
|
15911
|
+
writeState({ last_attempt_at: attemptedAt, last_ok_at: (/* @__PURE__ */ new Date()).toISOString(), last_target: target }, path);
|
|
15912
|
+
return { ok: true, status: response2.status, target };
|
|
15913
|
+
}
|
|
15914
|
+
const token = await loadToken();
|
|
15915
|
+
if (!token) {
|
|
15916
|
+
writeState({ ...state, last_attempt_at: attemptedAt, last_error: "no_auth", last_target: target }, path);
|
|
15917
|
+
return { ok: false, skipped: "no_auth", target };
|
|
15918
|
+
}
|
|
15919
|
+
const gateway = resolveInventoryGateway(
|
|
15920
|
+
process.env.SYNKRO_GATEWAY_URL || config.SYNKRO_GATEWAY_URL || process.env.SYNKRO_API_URL
|
|
15921
|
+
);
|
|
15922
|
+
const response = await postInventoryChunks(
|
|
15923
|
+
`${gateway}/api/v1/cli/inventory/snapshot`,
|
|
15924
|
+
{ Authorization: `Bearer ${token}` },
|
|
15925
|
+
snapshot
|
|
15926
|
+
);
|
|
15927
|
+
if (!response.ok) {
|
|
15928
|
+
const error = `http_${response.status}`;
|
|
15929
|
+
writeState({ ...state, last_attempt_at: attemptedAt, last_error: error, last_target: target }, path);
|
|
15930
|
+
return { ok: false, status: response.status, error, target };
|
|
15931
|
+
}
|
|
15932
|
+
writeState({ last_attempt_at: attemptedAt, last_ok_at: (/* @__PURE__ */ new Date()).toISOString(), last_target: target }, path);
|
|
15933
|
+
return { ok: true, status: response.status, target };
|
|
15934
|
+
} catch {
|
|
15935
|
+
writeState({ ...state, last_attempt_at: attemptedAt, last_error: "sync_failed", last_target: target }, path);
|
|
15936
|
+
return { ok: false, error: "sync_failed", target };
|
|
15937
|
+
}
|
|
15938
|
+
}
|
|
15939
|
+
function syncOperationalInventoryDetached() {
|
|
15940
|
+
if (process.env.SYNKRO_INVENTORY_DETACHED === "1") return;
|
|
15941
|
+
const path = syncStatePath();
|
|
15942
|
+
const state = readState(path);
|
|
15943
|
+
const target = inventorySyncTarget(readConfig());
|
|
15944
|
+
if (!shouldSyncInventory(state, Date.now(), target)) return;
|
|
15945
|
+
writeState({ ...state, last_attempt_at: (/* @__PURE__ */ new Date()).toISOString(), last_target: target }, path);
|
|
15946
|
+
try {
|
|
15947
|
+
const script = process.argv[1];
|
|
15948
|
+
if (!script || !existsSync38(script)) return;
|
|
15949
|
+
const child = spawn8(process.execPath, [script, "inventory-sync", "--detached"], {
|
|
15950
|
+
detached: true,
|
|
15951
|
+
stdio: "ignore",
|
|
15952
|
+
env: { ...process.env, SYNKRO_INVENTORY_DETACHED: "1" }
|
|
15953
|
+
});
|
|
15954
|
+
child.unref();
|
|
15955
|
+
} catch {
|
|
15956
|
+
}
|
|
15957
|
+
}
|
|
15958
|
+
var DEFAULT_GATEWAY2, SUCCESS_INTERVAL_MS, FAILURE_RETRY_MS, REQUEST_TIMEOUT_MS2, INVENTORY_CHUNK_BYTES;
|
|
15959
|
+
var init_sync2 = __esm({
|
|
15960
|
+
"cli/inventory/sync.ts"() {
|
|
15961
|
+
"use strict";
|
|
15962
|
+
init_collector();
|
|
15963
|
+
DEFAULT_GATEWAY2 = "https://api.synkro.sh";
|
|
15964
|
+
SUCCESS_INTERVAL_MS = 30 * 6e4;
|
|
15965
|
+
FAILURE_RETRY_MS = 5 * 6e4;
|
|
15966
|
+
REQUEST_TIMEOUT_MS2 = 15e3;
|
|
15967
|
+
INVENTORY_CHUNK_BYTES = 15e5;
|
|
15968
|
+
}
|
|
15969
|
+
});
|
|
15970
|
+
|
|
14317
15971
|
// cli/bootstrap.js
|
|
14318
|
-
import { readFileSync as
|
|
14319
|
-
import { resolve as
|
|
15972
|
+
import { readFileSync as readFileSync38, existsSync as existsSync39 } from "fs";
|
|
15973
|
+
import { resolve as resolve6 } from "path";
|
|
14320
15974
|
var envCandidates = [
|
|
14321
|
-
|
|
15975
|
+
resolve6(process.env.HOME ?? "", ".synkro", "config.env")
|
|
14322
15976
|
];
|
|
14323
15977
|
for (const envPath of envCandidates) {
|
|
14324
|
-
if (!
|
|
14325
|
-
const envContent =
|
|
15978
|
+
if (!existsSync39(envPath)) continue;
|
|
15979
|
+
const envContent = readFileSync38(envPath, "utf-8");
|
|
14326
15980
|
for (const line of envContent.split("\n")) {
|
|
14327
15981
|
const trimmed = line.trim();
|
|
14328
15982
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -14337,9 +15991,9 @@ var args = process.argv.slice(2);
|
|
|
14337
15991
|
var cmd2 = args[0] || "";
|
|
14338
15992
|
var subArgs = args.slice(1);
|
|
14339
15993
|
var isDetachedChild = process.env.SYNKRO_TELEMETRY_DETACHED === "1";
|
|
14340
|
-
var FLUSH_SKIP = /* @__PURE__ */ new Set(["grade", "version", "--version", "-v", "help", "--help", "-h", ""]);
|
|
15994
|
+
var FLUSH_SKIP = /* @__PURE__ */ new Set(["grade", "inventory-sync", "version", "--version", "-v", "help", "--help", "-h", ""]);
|
|
14341
15995
|
function printVersion() {
|
|
14342
|
-
console.log("1.
|
|
15996
|
+
console.log("1.8.0");
|
|
14343
15997
|
}
|
|
14344
15998
|
function printHelp2() {
|
|
14345
15999
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|
|
@@ -14527,6 +16181,13 @@ async function main() {
|
|
|
14527
16181
|
await telemetryCommand2(subArgs);
|
|
14528
16182
|
break;
|
|
14529
16183
|
}
|
|
16184
|
+
// Internal detached worker. Operational inventory is independent of product
|
|
16185
|
+
// telemetry and always fail-open; it intentionally stays out of help output.
|
|
16186
|
+
case "inventory-sync": {
|
|
16187
|
+
const { syncOperationalInventory: syncOperationalInventory2 } = await Promise.resolve().then(() => (init_sync2(), sync_exports2));
|
|
16188
|
+
await syncOperationalInventory2({ force: subArgs.includes("--detached") || subArgs.includes("--force") });
|
|
16189
|
+
break;
|
|
16190
|
+
}
|
|
14530
16191
|
default: {
|
|
14531
16192
|
console.error(`Unknown command: ${cmd2}`);
|
|
14532
16193
|
printHelp2();
|
|
@@ -14541,6 +16202,11 @@ async function postDispatchFlush() {
|
|
|
14541
16202
|
flushDetached2();
|
|
14542
16203
|
} catch {
|
|
14543
16204
|
}
|
|
16205
|
+
try {
|
|
16206
|
+
const { syncOperationalInventoryDetached: syncOperationalInventoryDetached2 } = await Promise.resolve().then(() => (init_sync2(), sync_exports2));
|
|
16207
|
+
syncOperationalInventoryDetached2();
|
|
16208
|
+
} catch {
|
|
16209
|
+
}
|
|
14544
16210
|
}
|
|
14545
16211
|
async function shutdown(code) {
|
|
14546
16212
|
try {
|