@synkro-sh/cli 1.8.0 → 1.10.2
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 +1402 -238
- package/dist/bootstrap.js.map +1 -1
- package/package.json +6 -4
package/dist/bootstrap.js
CHANGED
|
@@ -147,7 +147,7 @@ function getIdentity() {
|
|
|
147
147
|
if (cached2) return cached2;
|
|
148
148
|
let cliVersion2 = "0.0.0";
|
|
149
149
|
try {
|
|
150
|
-
cliVersion2 = "1.
|
|
150
|
+
cliVersion2 = "1.10.2";
|
|
151
151
|
} catch {
|
|
152
152
|
}
|
|
153
153
|
const creds = loadCredentialsIdentity();
|
|
@@ -234,7 +234,7 @@ function emit(eventType, context, opts) {
|
|
|
234
234
|
const cwd = opts?.cwd ?? process.cwd();
|
|
235
235
|
const git = deriveGit(cwd);
|
|
236
236
|
const emitter = process.env.SYNKRO_TELEMETRY_EMITTER || "bootstrap";
|
|
237
|
-
const
|
|
237
|
+
const row2 = {
|
|
238
238
|
client_event_id: randomUUID2(),
|
|
239
239
|
event_type: eventType,
|
|
240
240
|
occurred_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -263,7 +263,7 @@ function emit(eventType, context, opts) {
|
|
|
263
263
|
context: redactContext(context)
|
|
264
264
|
};
|
|
265
265
|
ensureDir2();
|
|
266
|
-
appendFileSync(PENDING_PATH, JSON.stringify(
|
|
266
|
+
appendFileSync(PENDING_PATH, JSON.stringify(row2) + "\n", { mode: 384 });
|
|
267
267
|
} catch (err) {
|
|
268
268
|
const msg = err instanceof Error ? err.message : String(err);
|
|
269
269
|
process.stderr.write(`[synkro] telemetry emit(${eventType}) failed: ${msg}
|
|
@@ -297,9 +297,9 @@ function shiftPlaceholders(text, offset) {
|
|
|
297
297
|
function bulkFragment(rows, columns) {
|
|
298
298
|
if (!rows.length || !columns.length) throw new Error("empty SQL helper");
|
|
299
299
|
const params = [];
|
|
300
|
-
const tuples = rows.map((
|
|
300
|
+
const tuples = rows.map((row2) => {
|
|
301
301
|
const placeholders = columns.map((column) => {
|
|
302
|
-
let value =
|
|
302
|
+
let value = row2[column];
|
|
303
303
|
let cast = "";
|
|
304
304
|
if (column === "context" && typeof value === "object" && value !== null) {
|
|
305
305
|
value = JSON.stringify(value);
|
|
@@ -740,11 +740,19 @@ async function insertEvents(sql, rows, mirroredIds) {
|
|
|
740
740
|
async function drainToPglite() {
|
|
741
741
|
const queued = readQueue().rows;
|
|
742
742
|
if (queued.length === 0) return { ok: true, ingested: 0, pending: 0, reason: "no_pending" };
|
|
743
|
-
const
|
|
743
|
+
const bounded = queued.length > MIRROR_MAX_PER_DRAIN ? queued.slice(-MIRROR_MAX_PER_DRAIN) : queued;
|
|
744
|
+
const candidates = bounded.filter((r) => !mirroredMemo.has(r.client_event_id));
|
|
745
|
+
if (candidates.length === 0) return { ok: true, ingested: 0, pending: queued.length, reason: "no_pending" };
|
|
744
746
|
const sql = await connectDb();
|
|
745
747
|
if (!sql) return { ok: false, ingested: 0, pending: queued.length, reason: "db_unavailable" };
|
|
746
748
|
try {
|
|
747
|
-
const
|
|
749
|
+
const ids = candidates.map((r) => r.client_event_id);
|
|
750
|
+
const present = await sql`
|
|
751
|
+
SELECT client_event_id FROM telemetry_events WHERE client_event_id = ANY(${ids})`;
|
|
752
|
+
if (mirroredMemo.size > MIRRORED_MEMO_CAP) mirroredMemo.clear();
|
|
753
|
+
for (const row2 of present) mirroredMemo.add(row2.client_event_id);
|
|
754
|
+
const fresh = candidates.filter((r) => !mirroredMemo.has(r.client_event_id));
|
|
755
|
+
const ingested = fresh.length ? await insertEvents(sql, fresh, mirroredMemo) : 0;
|
|
748
756
|
patchMetaCache({ last_drained_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
749
757
|
return { ok: true, ingested, pending: queued.length };
|
|
750
758
|
} catch (err) {
|
|
@@ -752,7 +760,7 @@ async function drainToPglite() {
|
|
|
752
760
|
return { ok: false, ingested: 0, pending: queued.length, reason: "error", error: msg };
|
|
753
761
|
}
|
|
754
762
|
}
|
|
755
|
-
var SYNKRO_DIR3, QUEUE_LOCK, PROCESSING_PATH, CHUNK, MIRROR_MAX_PER_DRAIN, STALE_LOCK_MS, MAX_QUEUE_ROWS;
|
|
763
|
+
var SYNKRO_DIR3, QUEUE_LOCK, PROCESSING_PATH, CHUNK, MIRROR_MAX_PER_DRAIN, STALE_LOCK_MS, MAX_QUEUE_ROWS, mirroredMemo, MIRRORED_MEMO_CAP;
|
|
756
764
|
var init_drain = __esm({
|
|
757
765
|
"cli/telemetry/drain.ts"() {
|
|
758
766
|
"use strict";
|
|
@@ -766,6 +774,8 @@ var init_drain = __esm({
|
|
|
766
774
|
MIRROR_MAX_PER_DRAIN = 5e3;
|
|
767
775
|
STALE_LOCK_MS = 6e4;
|
|
768
776
|
MAX_QUEUE_ROWS = 5e4;
|
|
777
|
+
mirroredMemo = /* @__PURE__ */ new Set();
|
|
778
|
+
MIRRORED_MEMO_CAP = 5e4;
|
|
769
779
|
}
|
|
770
780
|
});
|
|
771
781
|
|
|
@@ -1169,7 +1179,7 @@ async function getStats() {
|
|
|
1169
1179
|
GROUP BY event_type
|
|
1170
1180
|
ORDER BY c DESC
|
|
1171
1181
|
`;
|
|
1172
|
-
for (const
|
|
1182
|
+
for (const row2 of types) base.by_type[String(row2.event_type)] = Number(row2.c);
|
|
1173
1183
|
const newest = await sql`
|
|
1174
1184
|
SELECT occurred_at FROM telemetry_events ORDER BY occurred_at DESC LIMIT 1
|
|
1175
1185
|
`;
|
|
@@ -1224,8 +1234,8 @@ async function exportEvents(path) {
|
|
|
1224
1234
|
const lines = [];
|
|
1225
1235
|
try {
|
|
1226
1236
|
const rows = await sql`SELECT * FROM telemetry_events ORDER BY occurred_at ASC`;
|
|
1227
|
-
for (const
|
|
1228
|
-
lines.push(JSON.stringify(
|
|
1237
|
+
for (const row2 of rows) {
|
|
1238
|
+
lines.push(JSON.stringify(row2));
|
|
1229
1239
|
}
|
|
1230
1240
|
} catch {
|
|
1231
1241
|
}
|
|
@@ -1684,6 +1694,18 @@ function installCCHooks(settingsPath, config) {
|
|
|
1684
1694
|
[SYNKRO_MARKER]: true
|
|
1685
1695
|
});
|
|
1686
1696
|
}
|
|
1697
|
+
if (config.mcpFollowupScriptPath) {
|
|
1698
|
+
settings.hooks.PostToolUse.push({
|
|
1699
|
+
matcher: "mcp__.*",
|
|
1700
|
+
hooks: [
|
|
1701
|
+
{
|
|
1702
|
+
type: "command",
|
|
1703
|
+
command: config.mcpFollowupScriptPath
|
|
1704
|
+
}
|
|
1705
|
+
],
|
|
1706
|
+
[SYNKRO_MARKER]: true
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1687
1709
|
if (config.taskActivateIntentScriptPath) {
|
|
1688
1710
|
settings.hooks.PreToolUse.push({
|
|
1689
1711
|
matcher: "mcp__synkro[-_]guardrails__activate_standard",
|
|
@@ -1730,7 +1752,12 @@ function installCCHooks(settingsPath, config) {
|
|
|
1730
1752
|
{
|
|
1731
1753
|
type: "command",
|
|
1732
1754
|
command: config.userPromptSubmitScriptPath,
|
|
1733
|
-
|
|
1755
|
+
// 15s, not 5: the stub is fire-and-forget (no server round-trip on the
|
|
1756
|
+
// interactive path), so the budget only covers LOCAL work — but the SCM
|
|
1757
|
+
// reconcile runs git twice, and on a large dirty checkout with worktrees
|
|
1758
|
+
// and file churn that alone can spike past 5s. A trip here discards the
|
|
1759
|
+
// whole hook output, so size the budget to the slow-git tail.
|
|
1760
|
+
timeout: 15
|
|
1734
1761
|
}
|
|
1735
1762
|
],
|
|
1736
1763
|
[SYNKRO_MARKER]: true
|
|
@@ -2880,8 +2907,8 @@ Print a short readiness summary: rules now active, security scanning on, the gra
|
|
|
2880
2907
|
// cli/installer/skillParser.ts
|
|
2881
2908
|
import { existsSync as existsSync15 } from "fs";
|
|
2882
2909
|
import { resolve as resolve3 } from "path";
|
|
2883
|
-
function resolveSkillPaths(skills,
|
|
2884
|
-
return skills.filter((s) => s.endsWith(".md")).map((s) => resolve3(
|
|
2910
|
+
function resolveSkillPaths(skills, repoRoot3) {
|
|
2911
|
+
return skills.filter((s) => s.endsWith(".md")).map((s) => resolve3(repoRoot3, s)).filter((p) => existsSync15(p));
|
|
2885
2912
|
}
|
|
2886
2913
|
var init_skillParser = __esm({
|
|
2887
2914
|
"cli/installer/skillParser.ts"() {
|
|
@@ -2893,12 +2920,12 @@ var init_skillParser = __esm({
|
|
|
2893
2920
|
function stubHook(surface, optsLiteral) {
|
|
2894
2921
|
return "#!/usr/bin/env bun\nimport { runStub } from './_synkro-stub-common.ts';\nrunStub(" + JSON.stringify(surface) + ", " + optsLiteral + ");\n";
|
|
2895
2922
|
}
|
|
2896
|
-
var STUB_COMMON_TS, STUB_EDIT_PRECHECK_TS, STUB_EDIT_FOLLOWUP_TS, STUB_CWE_PRECHECK_TS, STUB_CVE_PRECHECK_TS, STUB_BASH_JUDGE_TS, STUB_SKILL_JUDGE_TS, STUB_INSTALL_SCAN_TS, STUB_AGENT_JUDGE_TS, STUB_MCP_GATE_TS, STUB_PLAN_JUDGE_TS, STUB_STOP_SUMMARY_TS, STUB_SESSION_START_TS, STUB_TRANSCRIPT_SYNC_TS, STUB_CODEX_CWE_STOP_TS, STUB_CODEX_CVE_STOP_TS, STUB_SUBAGENT_START_TS, STUB_SUBAGENT_STOP_TS, STUB_USER_PROMPT_SUBMIT_TS, STUB_BASH_FOLLOWUP_TS, STUB_PROMPT_ROUTE_TS, STUB_TASK_ACTIVATE_INTENT_TS, STUB_CURSOR_BASH_JUDGE_TS, STUB_CURSOR_SKILL_JUDGE_TS, STUB_CURSOR_EDIT_CAPTURE_TS, STUB_CURSOR_AGENT_CAPTURE_TS;
|
|
2923
|
+
var STUB_COMMON_TS, STUB_EDIT_PRECHECK_TS, STUB_EDIT_FOLLOWUP_TS, STUB_CWE_PRECHECK_TS, STUB_CVE_PRECHECK_TS, STUB_BASH_JUDGE_TS, STUB_SKILL_JUDGE_TS, STUB_INSTALL_SCAN_TS, STUB_AGENT_JUDGE_TS, STUB_MCP_GATE_TS, STUB_MCP_FOLLOWUP_TS, STUB_PLAN_JUDGE_TS, STUB_STOP_SUMMARY_TS, STUB_SESSION_START_TS, STUB_TRANSCRIPT_SYNC_TS, STUB_CODEX_CWE_STOP_TS, STUB_CODEX_CVE_STOP_TS, STUB_SUBAGENT_START_TS, STUB_SUBAGENT_STOP_TS, STUB_USER_PROMPT_SUBMIT_TS, STUB_BASH_FOLLOWUP_TS, STUB_PROMPT_ROUTE_TS, STUB_TASK_ACTIVATE_INTENT_TS, STUB_CURSOR_BASH_JUDGE_TS, STUB_CURSOR_SKILL_JUDGE_TS, STUB_CURSOR_EDIT_CAPTURE_TS, STUB_CURSOR_AGENT_CAPTURE_TS;
|
|
2897
2924
|
var init_hookScriptsTs = __esm({
|
|
2898
2925
|
"cli/installer/hookScriptsTs.ts"() {
|
|
2899
2926
|
"use strict";
|
|
2900
|
-
STUB_COMMON_TS = String.raw`import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, statSync, realpathSync, openSync, readSync, closeSync } from 'node:fs';
|
|
2901
|
-
import { execFileSync, execSync } from 'node:child_process';
|
|
2927
|
+
STUB_COMMON_TS = String.raw`import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, statSync, realpathSync, openSync, readSync, closeSync, unlinkSync } from 'node:fs';
|
|
2928
|
+
import { execFileSync, execSync, spawn } from 'node:child_process';
|
|
2902
2929
|
import { homedir } from 'node:os';
|
|
2903
2930
|
import { basename, dirname, join, resolve, relative, isAbsolute } from 'node:path';
|
|
2904
2931
|
import { randomUUID, createHash } from 'node:crypto';
|
|
@@ -3136,6 +3163,28 @@ function gitRoot(cwd: string): string {
|
|
|
3136
3163
|
} catch { return ''; }
|
|
3137
3164
|
}
|
|
3138
3165
|
|
|
3166
|
+
// synkro.toml is per-machine and untracked, so a LINKED git worktree never has
|
|
3167
|
+
// its own copy — its root is a fresh checkout. Resolve the main checkout through
|
|
3168
|
+
// the worktree's .git pointer file (a linked worktree's .git is a FILE reading
|
|
3169
|
+
// "gitdir: <main>/.git/worktrees/<name>") so worktrees inherit the repo's
|
|
3170
|
+
// config instead of silently skipping all grading. Opt-in semantics survive: a
|
|
3171
|
+
// repo whose MAIN root has no synkro.toml stays dormant. Pure fs — no
|
|
3172
|
+
// subprocess on the hook hot path.
|
|
3173
|
+
function mainWorktreeRoot(root: string): string {
|
|
3174
|
+
try {
|
|
3175
|
+
const dotGit = join(root, '.git');
|
|
3176
|
+
if (statSync(dotGit).isDirectory()) return root;
|
|
3177
|
+
const pointer = readFileSync(dotGit, 'utf-8');
|
|
3178
|
+
const match = pointer.match(/^gitdir:\s*(.+?)\s*$/m);
|
|
3179
|
+
if (!match) return root;
|
|
3180
|
+
const gitDir = isAbsolute(match[1]) ? match[1] : join(root, match[1]);
|
|
3181
|
+
const marker = join('.git', 'worktrees') + '/';
|
|
3182
|
+
const at = gitDir.lastIndexOf('/' + marker);
|
|
3183
|
+
if (at === -1) return root;
|
|
3184
|
+
return gitDir.slice(0, at) || root;
|
|
3185
|
+
} catch { return root; }
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3139
3188
|
function taskRepoContext(root: string): any {
|
|
3140
3189
|
if (!root) return undefined;
|
|
3141
3190
|
try {
|
|
@@ -3182,16 +3231,144 @@ function taskRepoContext(root: string): any {
|
|
|
3182
3231
|
} catch { return undefined; }
|
|
3183
3232
|
}
|
|
3184
3233
|
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3234
|
+
// One task-workspace message per TOOL CALL, not per hook. A single Bash call fans
|
|
3235
|
+
// out to install-scan + bash-judge + skill-judge (an Edit fans out to four), and
|
|
3236
|
+
// every hook is its own process, so the dedupe has to live on disk rather than in
|
|
3237
|
+
// a Map the way the server-side status-line dedupe does (see stableHookEventIdentity
|
|
3238
|
+
// in scanning/scanRouter). Atomic first-writer-wins via an exclusive create: the
|
|
3239
|
+
// hook that wins says it in full, the rest stay terse.
|
|
3240
|
+
const TOOL_CALL_MARK_DIR = join(HOME, '.synkro', 'tool-call-marks');
|
|
3241
|
+
const TOOL_CALL_MARK_WINDOW_MS = 15000;
|
|
3242
|
+
|
|
3243
|
+
function pruneToolCallMarks(now: number): void {
|
|
3244
|
+
try {
|
|
3245
|
+
const entries = readdirSync(TOOL_CALL_MARK_DIR);
|
|
3246
|
+
if (entries.length < 200) return;
|
|
3247
|
+
for (const name of entries) {
|
|
3248
|
+
const path = join(TOOL_CALL_MARK_DIR, name);
|
|
3249
|
+
try {
|
|
3250
|
+
if (now - statSync(path).mtimeMs > TOOL_CALL_MARK_WINDOW_MS) unlinkSync(path);
|
|
3251
|
+
} catch {}
|
|
3252
|
+
}
|
|
3253
|
+
} catch {}
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
// The user's answer to "move into the task worktree, or stay here?". Written by the
|
|
3257
|
+
// "synkro workspace stay" command (the CLI owns ~/.synkro, so nothing hand-writes it)
|
|
3258
|
+
// and read here. Keyed by task alone: the decision resets when the active task
|
|
3259
|
+
// changes, which is the scope the workspace gate is asking about.
|
|
3260
|
+
const WORKSPACE_CHOICE_DIR = join(HOME, '.synkro', 'workspace-choice');
|
|
3261
|
+
|
|
3262
|
+
// The user answers the workspace question in plain language on their next
|
|
3263
|
+
// prompt. Writing the marker is exactly what the CLI's "workspace stay"
|
|
3264
|
+
// command does — but the consent matcher only accepts one literal spelling,
|
|
3265
|
+
// and on a machine where PATH shadows the binary (observed: a venv python
|
|
3266
|
+
// named synkro, and an older global without the command) that spelling cannot
|
|
3267
|
+
// succeed, which deadlocked the ask. Patterns are DIRECTIVES, never
|
|
3268
|
+
// questions: a trailing question mark rejects, and the bare verb matches only
|
|
3269
|
+
// as a short standalone reply. Callers gate on a PENDING ask, so ordinary
|
|
3270
|
+
// conversation containing "stay" is never scanned against this.
|
|
3271
|
+
function isWorkspaceStayIntent(prompt: string): boolean {
|
|
3272
|
+
const normalized = String(prompt || '').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
3273
|
+
if (!normalized || normalized.length > 240) return false;
|
|
3274
|
+
if (/\?\s*$/.test(normalized)) return false;
|
|
3275
|
+
if (/^(?:yes[,.\s]+)?(?:please\s+)?stay(?:\s+(?:here|put))?(?:\s*(?:pls|please))?[.!]?$/.test(normalized)) return true;
|
|
3276
|
+
return [
|
|
3277
|
+
/\b(?:stay|remain|keep working|keep going|continue)\b[^?]{0,60}\b(?:current|same|this)\s+(?:worktree|workspace|checkout|directory)\b/,
|
|
3278
|
+
/\b(?:do not|don't|dont|no need to)\s+(?:move|switch|change)\b[^?]{0,40}\b(?:worktrees?|workspaces?|checkouts?)\b/,
|
|
3279
|
+
].some((pattern) => pattern.test(normalized));
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
function taskWorkspaceStayRecorded(taskId: string): boolean {
|
|
3283
|
+
if (!/^task_[a-z0-9]{8}$/i.test(String(taskId || ''))) return false;
|
|
3284
|
+
try { return existsSync(join(WORKSPACE_CHOICE_DIR, taskId + '.stay')); } catch { return false; }
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
// Our own context string, so the shape is stable: '... task=<id> ...'.
|
|
3288
|
+
function taskIdFromScmContext(context: string): string {
|
|
3289
|
+
const marker = ' task=';
|
|
3290
|
+
const at = String(context || '').indexOf(marker);
|
|
3291
|
+
if (at === -1) return '';
|
|
3292
|
+
const rest = context.slice(at + marker.length);
|
|
3293
|
+
const end = rest.indexOf(' ');
|
|
3294
|
+
return (end === -1 ? rest : rest.slice(0, end)).trim();
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
// Consent must never deadlock behind the block it resolves: the command that records
|
|
3298
|
+
// the answer is allowed through for the task currently being asked about, and nothing
|
|
3299
|
+
// else is.
|
|
3300
|
+
function isWorkspaceConsentCommand(payload: any, taskId: string): boolean {
|
|
3301
|
+
if (!taskId || String(payload?.tool_name || '') !== 'Bash') return false;
|
|
3302
|
+
const input = payload?.tool_input && typeof payload.tool_input === 'object' ? payload.tool_input : {};
|
|
3303
|
+
const command = String(input.command || input.cmd || '').trim();
|
|
3304
|
+
return command === 'synkro workspace stay ' + taskId;
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
function firstHookForToolCall(sessionId: string, payload: any): boolean {
|
|
3308
|
+
const p = payload && typeof payload === 'object' ? payload : {};
|
|
3309
|
+
const id = String(p.tool_use_id || p.tool_call_id || p.call_id || p.event_id || '').trim();
|
|
3310
|
+
// Fallback when the harness omits a call id: tool name + input is identical across
|
|
3311
|
+
// the surfaces of one call and distinct across different calls. Identical
|
|
3312
|
+
// back-to-back commands can collide inside the window, which costs one repeated
|
|
3313
|
+
// line and nothing else.
|
|
3314
|
+
const material = id || (String(p.tool_name || '') + ':' + (p.tool_input ? JSON.stringify(p.tool_input) : ''));
|
|
3315
|
+
if (!sessionId || !material) return true;
|
|
3316
|
+
const key = createHash('sha256').update(sessionId + '\0' + material).digest('hex').slice(0, 24);
|
|
3317
|
+
const marker = join(TOOL_CALL_MARK_DIR, key);
|
|
3318
|
+
const now = Date.now();
|
|
3319
|
+
try { mkdirSync(TOOL_CALL_MARK_DIR, { recursive: true }); } catch {}
|
|
3320
|
+
pruneToolCallMarks(now);
|
|
3321
|
+
try {
|
|
3322
|
+
writeFileSync(marker, '', { flag: 'wx', mode: 0o600 });
|
|
3323
|
+
return true;
|
|
3324
|
+
} catch (err: any) {
|
|
3325
|
+
if (err && err.code === 'EEXIST') {
|
|
3326
|
+
// Outside the burst window this is a genuinely new call reusing the fallback
|
|
3327
|
+
// key, so re-arm the marker and let it speak.
|
|
3328
|
+
try {
|
|
3329
|
+
if (now - statSync(marker).mtimeMs > TOOL_CALL_MARK_WINDOW_MS) {
|
|
3330
|
+
writeFileSync(marker, '', { mode: 0o600 });
|
|
3331
|
+
return true;
|
|
3332
|
+
}
|
|
3333
|
+
} catch {}
|
|
3334
|
+
return false;
|
|
3335
|
+
}
|
|
3336
|
+
// Any other failure (read-only home, quota) must not silence the message.
|
|
3337
|
+
return true;
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
function taskScmBlockResponse(harness: string, reason: string, verbose = true): string {
|
|
3342
|
+
// Every fanned-out hook still denies — suppressing the decision itself would let the
|
|
3343
|
+
// tool through if the winning hook's response were ever dropped. Only the TEXT
|
|
3344
|
+
// collapses: the later surfaces of one tool call deny with no systemMessage and no
|
|
3345
|
+
// additionalContext, so the transcript carries one workspace message, not three.
|
|
3346
|
+
const tag = synkroOriginTag('synkro:scm', harness);
|
|
3347
|
+
const message = tag + ' ' + reason;
|
|
3348
|
+
if (harness === 'cursor') {
|
|
3349
|
+
return verbose
|
|
3350
|
+
? JSON.stringify({ permission: 'deny', user_message: message, agent_message: message })
|
|
3351
|
+
: JSON.stringify({ permission: 'deny', user_message: '', agent_message: '' });
|
|
3352
|
+
}
|
|
3353
|
+
if (!verbose) {
|
|
3354
|
+
return JSON.stringify({
|
|
3355
|
+
systemMessage: '',
|
|
3356
|
+
hookSpecificOutput: {
|
|
3357
|
+
hookEventName: 'PreToolUse',
|
|
3358
|
+
permissionDecision: 'deny',
|
|
3359
|
+
// Only surfaces if this hook's denial is the one the harness reports.
|
|
3360
|
+
permissionDecisionReason: tag + ' blocked — see the workspace message above',
|
|
3361
|
+
additionalContext: '',
|
|
3362
|
+
},
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3188
3365
|
return JSON.stringify({
|
|
3189
3366
|
systemMessage: message,
|
|
3190
3367
|
hookSpecificOutput: {
|
|
3191
3368
|
hookEventName: 'PreToolUse',
|
|
3192
3369
|
permissionDecision: 'deny',
|
|
3193
3370
|
permissionDecisionReason: message,
|
|
3194
|
-
additionalContext: message
|
|
3371
|
+
additionalContext: message,
|
|
3195
3372
|
},
|
|
3196
3373
|
});
|
|
3197
3374
|
}
|
|
@@ -3228,8 +3405,36 @@ function sharedRepoRoot(root: string): string {
|
|
|
3228
3405
|
return dirname(common);
|
|
3229
3406
|
}
|
|
3230
3407
|
|
|
3231
|
-
|
|
3232
|
-
|
|
3408
|
+
// Claude Code only permits switching BETWEEN worktrees when the target lives under
|
|
3409
|
+
// <repo>/.claude/worktrees, so a cc session that activates a second task can never
|
|
3410
|
+
// reach another .synkro-worktrees path. Provisioning cc task worktrees under
|
|
3411
|
+
// .claude/worktrees keeps mid-session task switching working; every other harness
|
|
3412
|
+
// keeps the original location.
|
|
3413
|
+
const CC_MANAGED_WORKTREE_DIR = join('.claude', 'worktrees');
|
|
3414
|
+
const LEGACY_MANAGED_WORKTREE_DIR = '.synkro-worktrees';
|
|
3415
|
+
|
|
3416
|
+
function managedWorktreeDir(harness: string): string {
|
|
3417
|
+
return harness === 'cc' ? CC_MANAGED_WORKTREE_DIR : LEGACY_MANAGED_WORKTREE_DIR;
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3420
|
+
function taskWorktreePath(root: string, taskId: string, harness: string): string {
|
|
3421
|
+
return join(sharedRepoRoot(root), managedWorktreeDir(harness), taskId);
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
function taskWorktreeCandidatePaths(root: string, taskId: string): string[] {
|
|
3425
|
+
const shared = sharedRepoRoot(root);
|
|
3426
|
+
return [CC_MANAGED_WORKTREE_DIR, LEGACY_MANAGED_WORKTREE_DIR]
|
|
3427
|
+
.map((dir) => join(shared, dir, taskId));
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
// A task bound before the cc relocation keeps the worktree it already owns: the
|
|
3431
|
+
// canonical branch is checked out there, so provisioning a second one would fail
|
|
3432
|
+
// on 'the canonical task branch already exists in another workspace'.
|
|
3433
|
+
function resolveTaskWorktreePath(root: string, taskId: string, harness: string): string {
|
|
3434
|
+
const records = taskWorktreeRecords(root);
|
|
3435
|
+
const registered = taskWorktreeCandidatePaths(root, taskId)
|
|
3436
|
+
.find((candidate) => records.some((item) => samePath(item.path, candidate)));
|
|
3437
|
+
return registered || taskWorktreePath(root, taskId, harness);
|
|
3233
3438
|
}
|
|
3234
3439
|
|
|
3235
3440
|
function codexDesktopOrigin(): boolean {
|
|
@@ -3238,10 +3443,13 @@ function codexDesktopOrigin(): boolean {
|
|
|
3238
3443
|
return origin === 'codex desktop' || bundle === 'com.openai.codex';
|
|
3239
3444
|
}
|
|
3240
3445
|
|
|
3241
|
-
function ignoreManagedWorktreeDirectory(root: string): void {
|
|
3242
|
-
const
|
|
3446
|
+
function ignoreManagedWorktreeDirectory(root: string, worktreePath: string): void {
|
|
3447
|
+
const shared = sharedRepoRoot(root);
|
|
3448
|
+
const common = join(shared, '.git');
|
|
3243
3449
|
const excludePath = join(common, 'info', 'exclude');
|
|
3244
|
-
|
|
3450
|
+
// Ignore the directory the worktree actually landed in, which differs per harness.
|
|
3451
|
+
const entry = relative(shared, dirname(worktreePath)) + '/';
|
|
3452
|
+
if (entry.startsWith('..')) return;
|
|
3245
3453
|
let existing = '';
|
|
3246
3454
|
try { existing = readFileSync(excludePath, 'utf-8'); } catch {}
|
|
3247
3455
|
if (existing.split(/\r?\n/).includes(entry)) return;
|
|
@@ -3279,9 +3487,11 @@ function gitOperationInProgress(root: string): boolean {
|
|
|
3279
3487
|
}
|
|
3280
3488
|
|
|
3281
3489
|
function validateTaskWorktree(root: string, currentRoot: string, op: any): string {
|
|
3282
|
-
|
|
3490
|
+
// Accept either managed location: a task bound before the cc relocation still
|
|
3491
|
+
// pushes from its legacy .synkro-worktrees path.
|
|
3492
|
+
const expected = taskWorktreeCandidatePaths(root, String(op.taskId || ''));
|
|
3283
3493
|
const supplied = String(op.worktreePath || '');
|
|
3284
|
-
const managed = Boolean(supplied) && samePath(supplied,
|
|
3494
|
+
const managed = Boolean(supplied) && expected.some((candidate) => samePath(supplied, candidate));
|
|
3285
3495
|
const nativeCurrent = Boolean(supplied) && samePath(supplied, currentRoot);
|
|
3286
3496
|
if (!managed && !nativeCurrent) {
|
|
3287
3497
|
throw new Error('task worktree path does not match the managed or current native workspace');
|
|
@@ -3307,7 +3517,7 @@ async function completeTaskScm(sessionId: string, op: any, ok: boolean, error: s
|
|
|
3307
3517
|
} catch {}
|
|
3308
3518
|
}
|
|
3309
3519
|
|
|
3310
|
-
function executeTaskScm(root: string, op: any): string {
|
|
3520
|
+
function executeTaskScm(root: string, op: any, harness: string): string {
|
|
3311
3521
|
if (!op || typeof op !== 'object') throw new Error('invalid SCM operation');
|
|
3312
3522
|
if (!/^task_[a-z0-9]{8}$/i.test(String(op.taskId || ''))) throw new Error('invalid task id');
|
|
3313
3523
|
const branch = String(op.branchName || '');
|
|
@@ -3372,7 +3582,7 @@ function executeTaskScm(root: string, op: any): string {
|
|
|
3372
3582
|
}
|
|
3373
3583
|
return root;
|
|
3374
3584
|
}
|
|
3375
|
-
const worktreePath =
|
|
3585
|
+
const worktreePath = resolveTaskWorktreePath(boundRoot, String(op.taskId), harness);
|
|
3376
3586
|
const existing = taskWorktreeRecords(boundRoot).find((item) => samePath(item.path, worktreePath));
|
|
3377
3587
|
if (existing) {
|
|
3378
3588
|
if (existing.branch !== branch || gitOutput(worktreePath, ['rev-parse', '--abbrev-ref', 'HEAD']) !== branch) {
|
|
@@ -3398,7 +3608,7 @@ function executeTaskScm(root: string, op: any): string {
|
|
|
3398
3608
|
throw new Error('task base revision is unavailable in this repository');
|
|
3399
3609
|
}
|
|
3400
3610
|
const originalBranch = gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
3401
|
-
ignoreManagedWorktreeDirectory(boundRoot);
|
|
3611
|
+
ignoreManagedWorktreeDirectory(boundRoot, worktreePath);
|
|
3402
3612
|
mkdirSync(dirname(worktreePath), { recursive: true });
|
|
3403
3613
|
gitOutput(root, ['worktree', 'add', '-b', branch, worktreePath, String(op.baseSha)], 30000);
|
|
3404
3614
|
if (gitOutput(root, ['rev-parse', 'HEAD']) !== head
|
|
@@ -3430,14 +3640,23 @@ function executeTaskScm(root: string, op: any): string {
|
|
|
3430
3640
|
|
|
3431
3641
|
interface TaskScmReconcileResult { reason: string; context: string }
|
|
3432
3642
|
|
|
3433
|
-
|
|
3643
|
+
// Who acted and where it ran: a message in a shared terminal should say which
|
|
3644
|
+
// harness produced it and whether this install is grading locally or in the cloud.
|
|
3645
|
+
function synkroOriginTag(prefix: string, harness: string): string {
|
|
3646
|
+
return '[' + prefix
|
|
3647
|
+
+ (harness ? ':' + harness : '')
|
|
3648
|
+
+ ':' + (deployIsCloud() ? 'cloud' : 'local')
|
|
3649
|
+
+ ']';
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
function taskScmWorkspaceContext(workspace: any, harness = ''): string {
|
|
3434
3653
|
if (!workspace || typeof workspace !== 'object') return '';
|
|
3435
3654
|
const taskId = String(workspace.taskId || '');
|
|
3436
3655
|
const linearRef = String(workspace.linearRef || '');
|
|
3437
3656
|
const branchName = String(workspace.branchName || '');
|
|
3438
3657
|
const worktreePath = String(workspace.worktreePath || '');
|
|
3439
3658
|
if (!taskId || !branchName) return '';
|
|
3440
|
-
return '
|
|
3659
|
+
return synkroOriginTag('synkro:task-workspace', harness) + ' task=' + taskId
|
|
3441
3660
|
+ (linearRef ? ' linear=' + linearRef : '')
|
|
3442
3661
|
+ ' branch=' + branchName
|
|
3443
3662
|
+ (worktreePath ? ' worktree=' + worktreePath : ' worktree=pending-native-handoff');
|
|
@@ -3448,7 +3667,7 @@ function shellTaskWorkspaceArg(value: string): string {
|
|
|
3448
3667
|
}
|
|
3449
3668
|
|
|
3450
3669
|
function taskScmWorkspaceInstruction(harness: string, sessionId: string, workspace: any): string {
|
|
3451
|
-
const context = taskScmWorkspaceContext(workspace);
|
|
3670
|
+
const context = taskScmWorkspaceContext(workspace, harness);
|
|
3452
3671
|
const worktreePath = String(workspace?.worktreePath || '');
|
|
3453
3672
|
const branchName = String(workspace?.branchName || '');
|
|
3454
3673
|
if (!context || !branchName) return '';
|
|
@@ -3456,9 +3675,14 @@ function taskScmWorkspaceInstruction(harness: string, sessionId: string, workspa
|
|
|
3456
3675
|
+ 'or ask the user to perform the transition. Synkro keeps substantive tools blocked until this exact session reports '
|
|
3457
3676
|
+ 'the task worktree and canonical branch.';
|
|
3458
3677
|
if (harness === 'cc') {
|
|
3459
|
-
if (!worktreePath) return context + '\
|
|
3460
|
-
|
|
3461
|
-
|
|
3678
|
+
if (!worktreePath) return context + '\nTask worktree creation failed closed; retry workspace preparation.';
|
|
3679
|
+
// Ask, do not command. The user may legitimately want to keep working where they
|
|
3680
|
+
// are, and Synkro should not move them without their say-so.
|
|
3681
|
+
return context + '\nAsk the user whether to move this task into its isolated worktree '
|
|
3682
|
+
+ JSON.stringify(worktreePath) + ' on branch ' + JSON.stringify(branchName)
|
|
3683
|
+
+ ', or keep working in the current workspace. Do not decide for them. '
|
|
3684
|
+
+ 'To move: invoke EnterWorktree on that path. '
|
|
3685
|
+
+ 'To stay: run "synkro workspace stay ' + String(workspace?.taskId || '') + '".';
|
|
3462
3686
|
}
|
|
3463
3687
|
if (harness === 'cursor') {
|
|
3464
3688
|
if (!worktreePath) return context + '\n[synkro:workspace-handoff] Task worktree creation failed closed; retry workspace preparation.';
|
|
@@ -3477,6 +3701,26 @@ function taskScmWorkspaceInstruction(harness: string, sessionId: string, workspa
|
|
|
3477
3701
|
+ 'The Codex Environment panel must show that worktree and branch before retrying the blocked action. ' + requirement;
|
|
3478
3702
|
}
|
|
3479
3703
|
|
|
3704
|
+
// CC validates hookSpecificOutput.hookEventName against the event that actually fired.
|
|
3705
|
+
// Server responses and the SCM-context injection historically stamped 'PreToolUse'
|
|
3706
|
+
// unconditionally; on any other surface CC rejects the whole output ("expected
|
|
3707
|
+
// 'UserPromptSubmit' but got 'PreToolUse'") and the hook's work is discarded. The
|
|
3708
|
+
// payload's hook_event_name is the ground truth, so re-stamp at the last mile.
|
|
3709
|
+
// Only CC-shaped responses carry hookSpecificOutput, so no harness check is needed.
|
|
3710
|
+
function withActualHookEvent(responseText: string, payload: any): string {
|
|
3711
|
+
const actual = String((payload && (payload.hook_event_name || payload.hookEventName)) || '');
|
|
3712
|
+
if (!actual) return responseText;
|
|
3713
|
+
try {
|
|
3714
|
+
const response = JSON.parse(responseText || '{}');
|
|
3715
|
+
const hso = response && response.hookSpecificOutput;
|
|
3716
|
+
if (hso && typeof hso === 'object' && hso.hookEventName !== actual) {
|
|
3717
|
+
hso.hookEventName = actual;
|
|
3718
|
+
return JSON.stringify(response);
|
|
3719
|
+
}
|
|
3720
|
+
} catch { /* non-JSON hook output - leave untouched */ }
|
|
3721
|
+
return responseText;
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3480
3724
|
function withTaskScmContext(responseText: string, harness: string, context: string): string {
|
|
3481
3725
|
if (!context) return responseText;
|
|
3482
3726
|
try {
|
|
@@ -3539,11 +3783,11 @@ async function reconcileTaskScm(
|
|
|
3539
3783
|
? (taskScmWorkspaceInstruction(harness, sessionId, workspace)
|
|
3540
3784
|
|| String(result.reason || 'task source-control preparation is pending'))
|
|
3541
3785
|
: '',
|
|
3542
|
-
context: taskScmWorkspaceContext(workspace),
|
|
3786
|
+
context: taskScmWorkspaceContext(workspace, harness),
|
|
3543
3787
|
};
|
|
3544
3788
|
}
|
|
3545
3789
|
try {
|
|
3546
|
-
const worktreePath = executeTaskScm(root, result.op);
|
|
3790
|
+
const worktreePath = executeTaskScm(root, result.op, harness);
|
|
3547
3791
|
await completeTaskScm(sessionId, result.op, true, '', worktreePath);
|
|
3548
3792
|
if (result.op.kind === 'branch') {
|
|
3549
3793
|
if (result.op.adoptExistingWorktree === true) {
|
|
@@ -3551,12 +3795,12 @@ async function reconcileTaskScm(
|
|
|
3551
3795
|
if (rebound.ok) {
|
|
3552
3796
|
const reboundResult = await rebound.json() as any;
|
|
3553
3797
|
if (!reboundResult?.waiting && reboundResult?.workspace) {
|
|
3554
|
-
return { reason: '', context: taskScmWorkspaceContext(reboundResult.workspace) };
|
|
3798
|
+
return { reason: '', context: taskScmWorkspaceContext(reboundResult.workspace, harness) };
|
|
3555
3799
|
}
|
|
3556
3800
|
if (reboundResult?.waiting) {
|
|
3557
3801
|
return {
|
|
3558
3802
|
reason: String(reboundResult.reason || 'Codex native worktree binding is pending'),
|
|
3559
|
-
context: taskScmWorkspaceContext(reboundResult.workspace),
|
|
3803
|
+
context: taskScmWorkspaceContext(reboundResult.workspace, harness),
|
|
3560
3804
|
};
|
|
3561
3805
|
}
|
|
3562
3806
|
}
|
|
@@ -4188,10 +4432,19 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
|
|
|
4188
4432
|
// Dormancy: a repo is onboarded only if it has a synkro.toml FILE at its git
|
|
4189
4433
|
// root. Guard root !== HOME — ~/.synkro is the config DIRECTORY, so without
|
|
4190
4434
|
// this a home-rooted cwd (dotfiles in git, non-git dir under home) could
|
|
4191
|
-
// look onboarded.
|
|
4435
|
+
// look onboarded. A linked worktree checked out at a commit predating the
|
|
4436
|
+
// tracked synkro.toml has no copy of its own — fall back to the main
|
|
4437
|
+
// checkout's config rather than silently skipping enforcement there.
|
|
4192
4438
|
let synkroFileText = '';
|
|
4193
|
-
if (root && root !== HOME
|
|
4194
|
-
|
|
4439
|
+
if (root && root !== HOME) {
|
|
4440
|
+
let configRoot = root;
|
|
4441
|
+
if (!existsSync(join(configRoot, 'synkro.toml'))) {
|
|
4442
|
+
const mainRoot = mainWorktreeRoot(root);
|
|
4443
|
+
if (mainRoot !== root && mainRoot !== HOME && existsSync(join(mainRoot, 'synkro.toml'))) {
|
|
4444
|
+
configRoot = mainRoot;
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
try { synkroFileText = readFileSync(join(configRoot, 'synkro.toml'), 'utf-8'); } catch {}
|
|
4195
4448
|
}
|
|
4196
4449
|
if (!synkroFileText) {
|
|
4197
4450
|
// Repo not onboarded — emit a minimal tool_call so usage analytics still
|
|
@@ -4220,8 +4473,26 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
|
|
|
4220
4473
|
|
|
4221
4474
|
const scm = await reconcileTaskScm(root || cwd, sessionId, harness, payload);
|
|
4222
4475
|
const substantiveTool = /^(?:Bash|Edit|Write|MultiEdit|NotebookEdit|apply_patch|file_change)$/i.test(String(payload.tool_name || ''));
|
|
4223
|
-
|
|
4224
|
-
|
|
4476
|
+
const scmTaskId = taskIdFromScmContext(scm.context);
|
|
4477
|
+
// The user can settle the workspace question in plain language: while the
|
|
4478
|
+
// ask is pending for this session's task, a stay-directive on the user's
|
|
4479
|
+
// prompt records the same durable marker the CLI command writes. Reconcile
|
|
4480
|
+
// just told us WHICH task is being asked, so no extra state is needed and
|
|
4481
|
+
// ordinary prompts outside a pending ask are never scanned.
|
|
4482
|
+
if (surface === 'prompt-submit' && scm.reason && scmTaskId && !taskWorkspaceStayRecorded(scmTaskId)) {
|
|
4483
|
+
const promptText = String(payload.prompt || payload.user_message || '');
|
|
4484
|
+
if (isWorkspaceStayIntent(promptText)) {
|
|
4485
|
+
try {
|
|
4486
|
+
mkdirSync(WORKSPACE_CHOICE_DIR, { recursive: true });
|
|
4487
|
+
writeFileSync(join(WORKSPACE_CHOICE_DIR, scmTaskId + '.stay'), new Date().toISOString() + '\n');
|
|
4488
|
+
} catch { /* fail-open: the CLI command and the exact-string path remain */ }
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
// The user was asked and chose to keep working here, or is answering right now.
|
|
4492
|
+
const workspaceConsentSettled = Boolean(scmTaskId)
|
|
4493
|
+
&& (taskWorkspaceStayRecorded(scmTaskId) || isWorkspaceConsentCommand(payload, scmTaskId));
|
|
4494
|
+
if (scm.reason && substantiveTool && !workspaceConsentSettled) {
|
|
4495
|
+
out(taskScmBlockResponse(harness, scm.reason, firstHookForToolCall(sessionId, payload)));
|
|
4225
4496
|
return;
|
|
4226
4497
|
}
|
|
4227
4498
|
|
|
@@ -4368,12 +4639,30 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
|
|
|
4368
4639
|
// Dropping an occasional prompt tick is harmless; stalling the user is not.
|
|
4369
4640
|
const attempts = (opts.telemetry && surface !== 'prompt-submit') ? 3 : 1;
|
|
4370
4641
|
let text = '';
|
|
4371
|
-
|
|
4642
|
+
if (surface === 'prompt-submit' && process.env.SYNKRO_PROMPT_SUBMIT_SYNC !== '1') {
|
|
4643
|
+
// Fire-and-forget: prompt-submit's reply carries no verdict (telemetry-class,
|
|
4644
|
+
// response ignored) and the SCM workspace context is computed host-side, so
|
|
4645
|
+
// the server round-trip must never hold the user's KEYSTROKE hostage to DB
|
|
4646
|
+
// tail latency — a busy single-threaded PGLite stretch was tripping the 5s
|
|
4647
|
+
// hook budget on every prompt. A detached child delivers the envelope with
|
|
4648
|
+
// retries the interactive path could never afford; delivery reliability goes
|
|
4649
|
+
// UP versus the old single attempt racing a 5s clock.
|
|
4372
4650
|
try {
|
|
4373
|
-
const
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4651
|
+
const spoolDir = join(HOME, '.synkro', 'prompt-spool');
|
|
4652
|
+
mkdirSync(spoolDir, { recursive: true });
|
|
4653
|
+
const spoolPath = join(spoolDir, randomUUID() + '.json');
|
|
4654
|
+
writeFileSync(spoolPath, body);
|
|
4655
|
+
const child = spawn(process.execPath, [import.meta.path, '--deliver-spool', spoolPath], { detached: true, stdio: 'ignore' });
|
|
4656
|
+
child.unref();
|
|
4657
|
+
} catch { /* best-effort - the prompt must never block on delivery */ }
|
|
4658
|
+
} else {
|
|
4659
|
+
for (let i = 0; i < attempts; i++) {
|
|
4660
|
+
try {
|
|
4661
|
+
const resp = await fetch(url, { method: 'POST', headers, body, signal: AbortSignal.timeout(timeoutMs) });
|
|
4662
|
+
if (resp.ok) { text = (await resp.text()).trim(); break; }
|
|
4663
|
+
} catch (e) { /* connection refused / timeout → retry below */ }
|
|
4664
|
+
if (i < attempts - 1) await new Promise((r) => setTimeout(r, 500 * (i + 1)));
|
|
4665
|
+
}
|
|
4377
4666
|
}
|
|
4378
4667
|
const rawResponseText = text || failOpen(harness);
|
|
4379
4668
|
// A completion-report sync can arm the push after the pre-scan reconciliation ran.
|
|
@@ -4382,7 +4671,19 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
|
|
|
4382
4671
|
const contractResponseText = opts.stopContract && harness === 'codex'
|
|
4383
4672
|
? codexStopResponse(rawResponseText)
|
|
4384
4673
|
: rawResponseText;
|
|
4385
|
-
|
|
4674
|
+
// The informational workspace banner rides prompt-submit ONLY. Tool-call and stop
|
|
4675
|
+
// surfaces stay quiet: a pending workspace question already blocks the next
|
|
4676
|
+
// substantive tool with taskScmBlockResponse (which carries the full instructions
|
|
4677
|
+
// itself), and the server still receives scm.context via the envelope's
|
|
4678
|
+
// taskWorkspaceContext on every surface — so nothing enforcement- or data-bearing
|
|
4679
|
+
// is lost by silencing the per-tool-call and per-stop repeats.
|
|
4680
|
+
let scmContext = scm.context && surface === 'prompt-submit' && firstHookForToolCall(sessionId, payload) ? scm.context : '';
|
|
4681
|
+
// Say plainly that work is continuing outside the task worktree by the user's choice,
|
|
4682
|
+
// so the state is never mistaken for a binding that silently failed.
|
|
4683
|
+
if (scmContext && scmTaskId && taskWorkspaceStayRecorded(scmTaskId)) {
|
|
4684
|
+
scmContext += ' workspace=staying-here-by-user-choice';
|
|
4685
|
+
}
|
|
4686
|
+
const responseText = withActualHookEvent(withTaskScmContext(contractResponseText, harness, scmContext), payload);
|
|
4386
4687
|
out(responseText);
|
|
4387
4688
|
emitStubTelemetry(surface, harness, telemPayload, responseText, Date.now() - startedAt, telemCwd, telemSessionId);
|
|
4388
4689
|
} catch (err) {
|
|
@@ -4615,6 +4916,31 @@ function emitStubTelemetry(
|
|
|
4615
4916
|
}, opts);
|
|
4616
4917
|
}
|
|
4617
4918
|
}
|
|
4919
|
+
|
|
4920
|
+
// ─── Detached prompt-submit delivery child ──────────────────────────────────
|
|
4921
|
+
// The interactive parent spools the envelope and exits immediately; this child
|
|
4922
|
+
// (spawned detached from runStub's prompt-submit path) delivers it with retries
|
|
4923
|
+
// and a budget the 5s hook clock could never allow. Runs only when this file is
|
|
4924
|
+
// executed directly with --deliver-spool; a plain import never reaches it.
|
|
4925
|
+
async function deliverSpooledEnvelope(path: string): Promise<void> {
|
|
4926
|
+
let body = '';
|
|
4927
|
+
try { body = readFileSync(path, 'utf-8'); } catch { return; }
|
|
4928
|
+
const url = 'http://127.0.0.1:' + PORT + '/api/scan/prompt-submit';
|
|
4929
|
+
const headers = { 'Content-Type': 'application/json', Authorization: 'Bearer ' + loadMcpJwt() };
|
|
4930
|
+
for (let i = 0; i < 3; i++) {
|
|
4931
|
+
try {
|
|
4932
|
+
const resp = await fetch(url, { method: 'POST', headers, body, signal: AbortSignal.timeout(20000) });
|
|
4933
|
+
if (resp.ok) break;
|
|
4934
|
+
} catch { /* server busy or restarting - retry below */ }
|
|
4935
|
+
if (i < 2) await new Promise((r) => setTimeout(r, 2000 * (i + 1)));
|
|
4936
|
+
}
|
|
4937
|
+
try { unlinkSync(path); } catch { /* already collected */ }
|
|
4938
|
+
}
|
|
4939
|
+
|
|
4940
|
+
if (import.meta.main && process.argv[2] === '--deliver-spool' && process.argv[3]) {
|
|
4941
|
+
await deliverSpooledEnvelope(process.argv[3]);
|
|
4942
|
+
process.exit(0);
|
|
4943
|
+
}
|
|
4618
4944
|
`;
|
|
4619
4945
|
STUB_EDIT_PRECHECK_TS = stubHook("edit-precheck", "{ needsFile: true, needsTranscript: true }");
|
|
4620
4946
|
STUB_EDIT_FOLLOWUP_TS = stubHook("edit-followup", "{ needsFile: true, needsTranscript: true, postEdit: true }");
|
|
@@ -4625,6 +4951,7 @@ function emitStubTelemetry(
|
|
|
4625
4951
|
STUB_INSTALL_SCAN_TS = stubHook("install-scan", "{ needsTranscript: true }");
|
|
4626
4952
|
STUB_AGENT_JUDGE_TS = stubHook("agent-judge", "{ needsTranscript: true }");
|
|
4627
4953
|
STUB_MCP_GATE_TS = stubHook("mcp-gate", "{ needsTranscript: true }");
|
|
4954
|
+
STUB_MCP_FOLLOWUP_TS = stubHook("mcp-followup", "{ telemetry: true }");
|
|
4628
4955
|
STUB_PLAN_JUDGE_TS = stubHook("plan-judge", "{ needsPlan: true }");
|
|
4629
4956
|
STUB_STOP_SUMMARY_TS = stubHook("stop-summary", "{ needsTranscript: true, fullTranscript: true }");
|
|
4630
4957
|
STUB_SESSION_START_TS = stubHook("session-start", "{ telemetry: true }");
|
|
@@ -7281,8 +7608,8 @@ async function dockerInstall(opts = {}) {
|
|
|
7281
7608
|
"SYNKRO_TELEMETRY_QUEUE=/data/synkro-host/telemetry-pending.jsonl",
|
|
7282
7609
|
image
|
|
7283
7610
|
];
|
|
7284
|
-
const
|
|
7285
|
-
if (
|
|
7611
|
+
const run2 = spawnSync3("docker", args2, { encoding: "utf-8", stdio: "inherit", timeout: 6e4 });
|
|
7612
|
+
if (run2.status !== 0) {
|
|
7286
7613
|
throw new DockerInstallError(`docker run failed (image ${image})`);
|
|
7287
7614
|
}
|
|
7288
7615
|
return {
|
|
@@ -7513,7 +7840,7 @@ var init_dockerInstall = __esm({
|
|
|
7513
7840
|
HOST_PGLITE_PORT = parseInt(process.env.SYNKRO_HOST_PGLITE_PORT || "15433", 10);
|
|
7514
7841
|
CONTAINER_NAME = resolveContainerName();
|
|
7515
7842
|
defaultImageVersion = () => {
|
|
7516
|
-
if (true) return "1.
|
|
7843
|
+
if (true) return "1.10.2";
|
|
7517
7844
|
try {
|
|
7518
7845
|
const pkg = JSON.parse(readFileSync17(new URL("../../package.json", import.meta.url), "utf8"));
|
|
7519
7846
|
if (pkg.version) return pkg.version;
|
|
@@ -8221,7 +8548,7 @@ function isoDay(value, fallbackDay) {
|
|
|
8221
8548
|
function parseClaudeTranscriptUsage(transcript, fallbackDay = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), options = {}) {
|
|
8222
8549
|
const seen = options.seenStableIds ?? /* @__PURE__ */ new Set();
|
|
8223
8550
|
const rollups = /* @__PURE__ */ new Map();
|
|
8224
|
-
const
|
|
8551
|
+
const usage2 = {
|
|
8225
8552
|
input_tokens: 0,
|
|
8226
8553
|
output_tokens: 0,
|
|
8227
8554
|
cache_creation_input_tokens: 0,
|
|
@@ -8255,7 +8582,7 @@ function parseClaudeTranscriptUsage(transcript, fallbackDay = (/* @__PURE__ */ n
|
|
|
8255
8582
|
if (entryModel !== "<synthetic>") model = entryModel;
|
|
8256
8583
|
const day = isoDay(entry.timestamp, fallbackDay);
|
|
8257
8584
|
const key = `${day}\0${entryModel}`;
|
|
8258
|
-
const
|
|
8585
|
+
const row2 = rollups.get(key) ?? {
|
|
8259
8586
|
day,
|
|
8260
8587
|
model: entryModel,
|
|
8261
8588
|
turns: 0,
|
|
@@ -8264,23 +8591,23 @@ function parseClaudeTranscriptUsage(transcript, fallbackDay = (/* @__PURE__ */ n
|
|
|
8264
8591
|
cache_creation_input_tokens: 0,
|
|
8265
8592
|
cache_read_input_tokens: 0
|
|
8266
8593
|
};
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
rollups.set(key,
|
|
8594
|
+
row2.turns += 1;
|
|
8595
|
+
row2.input_tokens += counts.input_tokens;
|
|
8596
|
+
row2.output_tokens += counts.output_tokens;
|
|
8597
|
+
row2.cache_creation_input_tokens += counts.cache_creation_input_tokens;
|
|
8598
|
+
row2.cache_read_input_tokens += counts.cache_read_input_tokens;
|
|
8599
|
+
rollups.set(key, row2);
|
|
8273
8600
|
turns += 1;
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8601
|
+
usage2.input_tokens += counts.input_tokens;
|
|
8602
|
+
usage2.output_tokens += counts.output_tokens;
|
|
8603
|
+
usage2.cache_creation_input_tokens += counts.cache_creation_input_tokens;
|
|
8604
|
+
usage2.cache_read_input_tokens += counts.cache_read_input_tokens;
|
|
8278
8605
|
} catch {
|
|
8279
8606
|
}
|
|
8280
8607
|
}
|
|
8281
8608
|
if (turns === 0) return null;
|
|
8282
8609
|
return {
|
|
8283
|
-
usage,
|
|
8610
|
+
usage: usage2,
|
|
8284
8611
|
model: model || "unknown",
|
|
8285
8612
|
rollups: [...rollups.values()].sort(
|
|
8286
8613
|
(a, b) => a.day.localeCompare(b.day) || a.model.localeCompare(b.model)
|
|
@@ -8473,6 +8800,7 @@ function writeHookScripts() {
|
|
|
8473
8800
|
const mcpStdioProxyPath = join19(HOOKS_DIR, "mcp-stdio-proxy.ts");
|
|
8474
8801
|
const taskActivateIntentScriptPath = join19(HOOKS_DIR, "cc-task-activate-intent.ts");
|
|
8475
8802
|
const mcpGateScriptPath = join19(HOOKS_DIR, "cc-mcp-gate.ts");
|
|
8803
|
+
const mcpFollowupScriptPath = join19(HOOKS_DIR, "cc-mcp-followup.ts");
|
|
8476
8804
|
const stubCommonPath = join19(HOOKS_DIR, "_synkro-stub-common.ts");
|
|
8477
8805
|
const stubFiles = [
|
|
8478
8806
|
[stubCommonPath, STUB_COMMON_TS],
|
|
@@ -8498,6 +8826,7 @@ function writeHookScripts() {
|
|
|
8498
8826
|
[installScanScriptPath, STUB_INSTALL_SCAN_TS],
|
|
8499
8827
|
[taskActivateIntentScriptPath, STUB_TASK_ACTIVATE_INTENT_TS],
|
|
8500
8828
|
[mcpGateScriptPath, STUB_MCP_GATE_TS],
|
|
8829
|
+
[mcpFollowupScriptPath, STUB_MCP_FOLLOWUP_TS],
|
|
8501
8830
|
[cursorBashJudgePath, STUB_CURSOR_BASH_JUDGE_TS],
|
|
8502
8831
|
[cursorEditCapturePath, STUB_CURSOR_EDIT_CAPTURE_TS],
|
|
8503
8832
|
[cursorAgentCapturePath, STUB_CURSOR_AGENT_CAPTURE_TS]
|
|
@@ -8538,7 +8867,8 @@ function writeHookScripts() {
|
|
|
8538
8867
|
cursorEditCaptureScript: cursorEditCapturePath,
|
|
8539
8868
|
cursorAgentCaptureScript: cursorAgentCapturePath,
|
|
8540
8869
|
taskActivateIntentScript: taskActivateIntentScriptPath,
|
|
8541
|
-
mcpGateScript: mcpGateScriptPath
|
|
8870
|
+
mcpGateScript: mcpGateScriptPath,
|
|
8871
|
+
mcpFollowupScript: mcpFollowupScriptPath
|
|
8542
8872
|
};
|
|
8543
8873
|
}
|
|
8544
8874
|
function sanitizeConfigValue(raw, maxLen = 256) {
|
|
@@ -8570,7 +8900,7 @@ function writeConfigEnv(opts) {
|
|
|
8570
8900
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle2(credsPath)}`,
|
|
8571
8901
|
`SYNKRO_TIER=${shellQuoteSingle2(safeTier)}`,
|
|
8572
8902
|
`SYNKRO_INFERENCE=${shellQuoteSingle2(safeInference)}`,
|
|
8573
|
-
`SYNKRO_VERSION=${shellQuoteSingle2("1.
|
|
8903
|
+
`SYNKRO_VERSION=${shellQuoteSingle2("1.10.2")}`
|
|
8574
8904
|
];
|
|
8575
8905
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle2(safeSynkroBin)}`);
|
|
8576
8906
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle2(safeUserId)}`);
|
|
@@ -9317,7 +9647,7 @@ async function installCommand(opts = {}) {
|
|
|
9317
9647
|
await setTelemetryState({ enabled: true, remoteFlushEnabled: telemetryConsent });
|
|
9318
9648
|
emit("install", {
|
|
9319
9649
|
phase: "started",
|
|
9320
|
-
cli_version_to: "1.
|
|
9650
|
+
cli_version_to: "1.10.2",
|
|
9321
9651
|
agents_detected: agents.map((a) => a.kind),
|
|
9322
9652
|
with_github: false,
|
|
9323
9653
|
with_local_cc: false,
|
|
@@ -9361,6 +9691,7 @@ async function installCommand(opts = {}) {
|
|
|
9361
9691
|
installScanScriptPath: scripts.installScanScript,
|
|
9362
9692
|
taskActivateIntentScriptPath: scripts.taskActivateIntentScript,
|
|
9363
9693
|
mcpGateScriptPath: scripts.mcpGateScript,
|
|
9694
|
+
mcpFollowupScriptPath: scripts.mcpFollowupScript,
|
|
9364
9695
|
skipTranscriptSync: !transcriptCC
|
|
9365
9696
|
});
|
|
9366
9697
|
console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
|
|
@@ -10178,7 +10509,7 @@ async function syncSkillFiles() {
|
|
|
10178
10509
|
function normSkillName(name) {
|
|
10179
10510
|
return name.toLowerCase().replace(/\.mdx?$/, "");
|
|
10180
10511
|
}
|
|
10181
|
-
function discoverSkillFiles(
|
|
10512
|
+
function discoverSkillFiles(repoRoot3, excludeHashes, ingestedHashes, ingestedNames) {
|
|
10182
10513
|
const roots = [];
|
|
10183
10514
|
const add = (p) => {
|
|
10184
10515
|
try {
|
|
@@ -10188,9 +10519,9 @@ function discoverSkillFiles(repoRoot2, excludeHashes, ingestedHashes, ingestedNa
|
|
|
10188
10519
|
};
|
|
10189
10520
|
add(join19(homedir21(), ".claude", "skills"));
|
|
10190
10521
|
add(join19(homedir21(), ".agents", "skills"));
|
|
10191
|
-
if (
|
|
10192
|
-
add(join19(
|
|
10193
|
-
add(join19(
|
|
10522
|
+
if (repoRoot3) {
|
|
10523
|
+
add(join19(repoRoot3, ".claude", "skills"));
|
|
10524
|
+
add(join19(repoRoot3, ".agents", "skills"));
|
|
10194
10525
|
}
|
|
10195
10526
|
const out = [];
|
|
10196
10527
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -10273,7 +10604,7 @@ Found ${found.length} skill${found.length === 1 ? "" : "s"} in your Claude Code
|
|
|
10273
10604
|
async function discoverAndIngestSkills() {
|
|
10274
10605
|
try {
|
|
10275
10606
|
const sf = readFullSynkroFile();
|
|
10276
|
-
const
|
|
10607
|
+
const repoRoot3 = sf?._repoRoot || detectGitRepo2();
|
|
10277
10608
|
const mcpPort = process.env.SYNKRO_MCP_PORT || "18931";
|
|
10278
10609
|
const excludeHashes = /* @__PURE__ */ new Set();
|
|
10279
10610
|
if (sf?.skills?.length) {
|
|
@@ -10297,7 +10628,7 @@ async function discoverAndIngestSkills() {
|
|
|
10297
10628
|
}
|
|
10298
10629
|
} catch {
|
|
10299
10630
|
}
|
|
10300
|
-
const found = discoverSkillFiles(
|
|
10631
|
+
const found = discoverSkillFiles(repoRoot3, excludeHashes, ingestedHashes, ingestedNames);
|
|
10301
10632
|
if (found.length === 0) return;
|
|
10302
10633
|
const selectable = found.filter((f) => !f.ingested);
|
|
10303
10634
|
if (selectable.length === 0) {
|
|
@@ -10333,18 +10664,18 @@ async function discoverAndIngestSkills() {
|
|
|
10333
10664
|
}
|
|
10334
10665
|
}
|
|
10335
10666
|
function resolveSynkroBinPath() {
|
|
10336
|
-
const
|
|
10667
|
+
const run2 = (cmd3) => {
|
|
10337
10668
|
try {
|
|
10338
10669
|
return execSync4(cmd3, { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
10339
10670
|
} catch {
|
|
10340
10671
|
return "";
|
|
10341
10672
|
}
|
|
10342
10673
|
};
|
|
10343
|
-
const p =
|
|
10674
|
+
const p = run2("command -v synkro").split("\n")[0].trim();
|
|
10344
10675
|
return p && isAbsolute(p) ? p : "";
|
|
10345
10676
|
}
|
|
10346
10677
|
function ensureReachabilityGitHook() {
|
|
10347
|
-
const
|
|
10678
|
+
const run2 = (cmd3) => {
|
|
10348
10679
|
try {
|
|
10349
10680
|
return execSync4(cmd3, { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
10350
10681
|
} catch {
|
|
@@ -10352,9 +10683,9 @@ function ensureReachabilityGitHook() {
|
|
|
10352
10683
|
}
|
|
10353
10684
|
};
|
|
10354
10685
|
try {
|
|
10355
|
-
const root =
|
|
10686
|
+
const root = run2("git rev-parse --show-toplevel");
|
|
10356
10687
|
if (!root) return null;
|
|
10357
|
-
let hooksDir =
|
|
10688
|
+
let hooksDir = run2("git config --get core.hooksPath");
|
|
10358
10689
|
hooksDir = hooksDir ? isAbsolute(hooksDir) ? hooksDir : join19(root, hooksDir) : join19(root, ".git", "hooks");
|
|
10359
10690
|
if (!existsSync22(hooksDir)) mkdirSync15(hooksDir, { recursive: true });
|
|
10360
10691
|
const hookPath = join19(hooksDir, "post-commit");
|
|
@@ -10396,18 +10727,18 @@ function ensureReachabilityGitHook() {
|
|
|
10396
10727
|
}
|
|
10397
10728
|
}
|
|
10398
10729
|
function detectGitRepo2() {
|
|
10399
|
-
const
|
|
10730
|
+
const run2 = (cmd3) => {
|
|
10400
10731
|
try {
|
|
10401
10732
|
return execSync4(cmd3, { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
10402
10733
|
} catch {
|
|
10403
10734
|
return "";
|
|
10404
10735
|
}
|
|
10405
10736
|
};
|
|
10406
|
-
const remoteUrl =
|
|
10737
|
+
const remoteUrl = run2("git remote get-url origin");
|
|
10407
10738
|
if (remoteUrl) {
|
|
10408
10739
|
return remoteUrl.replace(/^git@[^:]+:/, "").replace(/^https?:\/\/[^/]+\//, "").replace(/\.git$/, "");
|
|
10409
10740
|
}
|
|
10410
|
-
const root =
|
|
10741
|
+
const root = run2("git rev-parse --show-toplevel");
|
|
10411
10742
|
return root ? root.split("/").pop() || null : null;
|
|
10412
10743
|
}
|
|
10413
10744
|
function getClaudeProjectsFolder() {
|
|
@@ -13856,10 +14187,10 @@ var init_packVerify = __esm({
|
|
|
13856
14187
|
// cli/installer/lockfile.ts
|
|
13857
14188
|
import { existsSync as existsSync30, readFileSync as readFileSync28, writeFileSync as writeFileSync20 } from "fs";
|
|
13858
14189
|
import { join as join28 } from "path";
|
|
13859
|
-
function lockPath(
|
|
13860
|
-
return join28(
|
|
14190
|
+
function lockPath(repoRoot3) {
|
|
14191
|
+
return join28(repoRoot3, LOCK_FILE);
|
|
13861
14192
|
}
|
|
13862
|
-
function writeLockfile(
|
|
14193
|
+
function writeLockfile(repoRoot3, entries) {
|
|
13863
14194
|
const sorted = [...entries].sort((a, b) => a.ref.localeCompare(b.ref));
|
|
13864
14195
|
const body = [
|
|
13865
14196
|
"# synkro.lock \u2014 generated by `synkro sync`. Commit this file.",
|
|
@@ -13875,7 +14206,7 @@ function writeLockfile(repoRoot2, entries) {
|
|
|
13875
14206
|
""
|
|
13876
14207
|
])
|
|
13877
14208
|
].join("\n");
|
|
13878
|
-
writeFileSync20(lockPath(
|
|
14209
|
+
writeFileSync20(lockPath(repoRoot3), body, "utf-8");
|
|
13879
14210
|
}
|
|
13880
14211
|
var LOCK_FILE;
|
|
13881
14212
|
var init_lockfile = __esm({
|
|
@@ -14087,6 +14418,822 @@ var init_whoami = __esm({
|
|
|
14087
14418
|
}
|
|
14088
14419
|
});
|
|
14089
14420
|
|
|
14421
|
+
// cli/commands/workspace.ts
|
|
14422
|
+
var workspace_exports = {};
|
|
14423
|
+
__export(workspace_exports, {
|
|
14424
|
+
workspaceCommand: () => workspaceCommand
|
|
14425
|
+
});
|
|
14426
|
+
import { existsSync as existsSync33, mkdirSync as mkdirSync19, readdirSync as readdirSync8, rmSync as rmSync5, writeFileSync as writeFileSync22 } from "fs";
|
|
14427
|
+
import { join as join31 } from "path";
|
|
14428
|
+
import { homedir as homedir31 } from "os";
|
|
14429
|
+
function markerPath(taskId) {
|
|
14430
|
+
return join31(WORKSPACE_CHOICE_DIR, `${taskId}.stay`);
|
|
14431
|
+
}
|
|
14432
|
+
function usage() {
|
|
14433
|
+
console.log(`synkro workspace \u2014 record where a task's work happens
|
|
14434
|
+
|
|
14435
|
+
Usage:
|
|
14436
|
+
synkro workspace stay <taskId> keep working in the current checkout
|
|
14437
|
+
synkro workspace clear <taskId> forget the choice (the gate asks again)
|
|
14438
|
+
synkro workspace status [taskId] show recorded choices
|
|
14439
|
+
|
|
14440
|
+
The gate asks once per task. "stay" is remembered until the choice is cleared or
|
|
14441
|
+
the active task changes.`);
|
|
14442
|
+
}
|
|
14443
|
+
async function workspaceCommand(args2) {
|
|
14444
|
+
const sub = String(args2[0] || "").trim();
|
|
14445
|
+
const taskId = String(args2[1] || "").trim();
|
|
14446
|
+
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
14447
|
+
usage();
|
|
14448
|
+
return;
|
|
14449
|
+
}
|
|
14450
|
+
if (sub === "status") {
|
|
14451
|
+
let recorded = [];
|
|
14452
|
+
try {
|
|
14453
|
+
recorded = existsSync33(WORKSPACE_CHOICE_DIR) ? readdirSync8(WORKSPACE_CHOICE_DIR).filter((name) => name.endsWith(".stay")) : [];
|
|
14454
|
+
} catch {
|
|
14455
|
+
recorded = [];
|
|
14456
|
+
}
|
|
14457
|
+
if (taskId) {
|
|
14458
|
+
const on = recorded.includes(`${taskId}.stay`);
|
|
14459
|
+
console.log(`${taskId}: ${on ? "stay recorded" : "no choice recorded"}`);
|
|
14460
|
+
return;
|
|
14461
|
+
}
|
|
14462
|
+
if (recorded.length === 0) {
|
|
14463
|
+
console.log("No workspace choices recorded.");
|
|
14464
|
+
return;
|
|
14465
|
+
}
|
|
14466
|
+
console.log("Staying in the current checkout for:");
|
|
14467
|
+
for (const name of recorded.sort()) console.log(` ${name.replace(/\.stay$/, "")}`);
|
|
14468
|
+
return;
|
|
14469
|
+
}
|
|
14470
|
+
if (sub !== "stay" && sub !== "clear") {
|
|
14471
|
+
console.error(`Unknown workspace subcommand: ${sub}`);
|
|
14472
|
+
usage();
|
|
14473
|
+
process.exitCode = 2;
|
|
14474
|
+
return;
|
|
14475
|
+
}
|
|
14476
|
+
if (!TASK_ID.test(taskId)) {
|
|
14477
|
+
console.error(taskId ? `Not a task id: ${taskId} (expected task_ followed by 8 characters)` : `Usage: synkro workspace ${sub} <taskId>`);
|
|
14478
|
+
process.exitCode = 2;
|
|
14479
|
+
return;
|
|
14480
|
+
}
|
|
14481
|
+
if (sub === "clear") {
|
|
14482
|
+
try {
|
|
14483
|
+
rmSync5(markerPath(taskId), { force: true });
|
|
14484
|
+
} catch {
|
|
14485
|
+
}
|
|
14486
|
+
console.log(`Cleared the workspace choice for ${taskId}.`);
|
|
14487
|
+
return;
|
|
14488
|
+
}
|
|
14489
|
+
try {
|
|
14490
|
+
mkdirSync19(WORKSPACE_CHOICE_DIR, { recursive: true });
|
|
14491
|
+
writeFileSync22(markerPath(taskId), `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
14492
|
+
`, "utf-8");
|
|
14493
|
+
} catch (error) {
|
|
14494
|
+
console.error(`Could not record the workspace choice: ${error?.message || error}`);
|
|
14495
|
+
process.exitCode = 1;
|
|
14496
|
+
return;
|
|
14497
|
+
}
|
|
14498
|
+
console.log(`Staying in the current checkout for ${taskId}. Synkro will not ask again for this task.`);
|
|
14499
|
+
}
|
|
14500
|
+
var WORKSPACE_CHOICE_DIR, TASK_ID;
|
|
14501
|
+
var init_workspace = __esm({
|
|
14502
|
+
"cli/commands/workspace.ts"() {
|
|
14503
|
+
"use strict";
|
|
14504
|
+
WORKSPACE_CHOICE_DIR = join31(homedir31(), ".synkro", "workspace-choice");
|
|
14505
|
+
TASK_ID = /^task_[a-z0-9]{8}$/i;
|
|
14506
|
+
}
|
|
14507
|
+
});
|
|
14508
|
+
|
|
14509
|
+
// cli/ui/tmux.ts
|
|
14510
|
+
import { execFile as execFile2, spawnSync as spawnSync11 } from "child_process";
|
|
14511
|
+
import { promisify } from "util";
|
|
14512
|
+
function runnerArgs(runner, argv) {
|
|
14513
|
+
return runner.kind === "container" ? ["docker", "exec", "-u", CONTAINER_USER, runner.container, ...argv] : argv;
|
|
14514
|
+
}
|
|
14515
|
+
function runnerInteractiveArgs(runner, argv) {
|
|
14516
|
+
return runner.kind === "container" ? ["docker", "exec", "-it", "-u", CONTAINER_USER, runner.container, ...argv] : argv;
|
|
14517
|
+
}
|
|
14518
|
+
async function run(runner, argv) {
|
|
14519
|
+
const [cmd3, ...args2] = runnerArgs(runner, argv);
|
|
14520
|
+
try {
|
|
14521
|
+
const { stdout, stderr } = await execFileAsync(cmd3, args2, { timeout: 8e3, maxBuffer: 1024 * 1024 });
|
|
14522
|
+
return { ok: true, stdout: String(stdout || ""), stderr: String(stderr || "") };
|
|
14523
|
+
} catch (error) {
|
|
14524
|
+
return { ok: false, stdout: String(error?.stdout || ""), stderr: String(error?.stderr || error?.message || "") };
|
|
14525
|
+
}
|
|
14526
|
+
}
|
|
14527
|
+
function runInherit(argv) {
|
|
14528
|
+
const [cmd3, ...args2] = argv;
|
|
14529
|
+
const result = spawnSync11(cmd3, args2, { stdio: "inherit" });
|
|
14530
|
+
return result.status ?? 1;
|
|
14531
|
+
}
|
|
14532
|
+
function slugify(name) {
|
|
14533
|
+
return String(name || "").toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "agent";
|
|
14534
|
+
}
|
|
14535
|
+
function agentSession(name) {
|
|
14536
|
+
return AGENT_PREFIX + slugify(name);
|
|
14537
|
+
}
|
|
14538
|
+
function buildSpawnAgent(opts) {
|
|
14539
|
+
const session = agentSession(opts.name);
|
|
14540
|
+
return [
|
|
14541
|
+
["tmux", "new-session", "-d", "-s", session, "-c", opts.cwd, opts.command],
|
|
14542
|
+
["tmux", "set-option", "-t", session, "status", "off"],
|
|
14543
|
+
["tmux", "set-option", "-t", session, "-q", "@synkro_harness", opts.harness],
|
|
14544
|
+
["tmux", "set-option", "-t", session, "-q", "@synkro_space", opts.space],
|
|
14545
|
+
["tmux", "set-option", "-t", session, "-q", "@synkro_backend", opts.backend],
|
|
14546
|
+
// Keep the pane visible after exit so the sidebar can render 'done'
|
|
14547
|
+
// instead of the agent silently vanishing.
|
|
14548
|
+
["tmux", "set-option", "-t", session, "remain-on-exit", "on"]
|
|
14549
|
+
];
|
|
14550
|
+
}
|
|
14551
|
+
function buildListAgents() {
|
|
14552
|
+
return ["tmux", "list-sessions", "-F", ["#{session_name}", "#{?pane_dead,dead,alive}", "#{@synkro_harness}", "#{@synkro_space}", "#{@synkro_backend}", "#{@synkro_pueue}"].join(FIELD_SEP)];
|
|
14553
|
+
}
|
|
14554
|
+
function buildCapture(session, lines = 14) {
|
|
14555
|
+
return ["tmux", "capture-pane", "-p", "-t", session, "-S", String(-lines)];
|
|
14556
|
+
}
|
|
14557
|
+
function buildKillSession(session) {
|
|
14558
|
+
return ["tmux", "kill-session", "-t", session];
|
|
14559
|
+
}
|
|
14560
|
+
function buildSetOption(session, option, value) {
|
|
14561
|
+
return ["tmux", "set-option", "-t", session, "-q", option, value];
|
|
14562
|
+
}
|
|
14563
|
+
function buildSendText(session, text) {
|
|
14564
|
+
return [
|
|
14565
|
+
["tmux", "send-keys", "-t", session, "-l", text],
|
|
14566
|
+
["tmux", "send-keys", "-t", session, "Enter"]
|
|
14567
|
+
];
|
|
14568
|
+
}
|
|
14569
|
+
function buildInterrupt(session) {
|
|
14570
|
+
return ["tmux", "send-keys", "-t", session, "Escape"];
|
|
14571
|
+
}
|
|
14572
|
+
function buildCenterAttachCommand(runner, session) {
|
|
14573
|
+
const argv = runnerInteractiveArgs(runner, ["tmux", "attach-session", "-t", session]);
|
|
14574
|
+
return "env TMUX= " + argv.map(shellQuote3).join(" ");
|
|
14575
|
+
}
|
|
14576
|
+
function shellQuote3(value) {
|
|
14577
|
+
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : "'" + value.replace(/'/g, "'\\''") + "'";
|
|
14578
|
+
}
|
|
14579
|
+
function parseAgentSessions(output, backend) {
|
|
14580
|
+
return String(output || "").split("\n").map((line) => line.split(FIELD_SEP)).filter((cols) => cols[0]?.startsWith(AGENT_PREFIX)).map((cols) => ({
|
|
14581
|
+
session: cols[0],
|
|
14582
|
+
name: cols[0].slice(AGENT_PREFIX.length),
|
|
14583
|
+
dead: cols[1] === "dead",
|
|
14584
|
+
harness: cols[2] || "claude",
|
|
14585
|
+
space: cols[3] || "",
|
|
14586
|
+
backend: cols[4] || backend,
|
|
14587
|
+
pueue: cols[5] || ""
|
|
14588
|
+
}));
|
|
14589
|
+
}
|
|
14590
|
+
function parseWorktrees(porcelain) {
|
|
14591
|
+
const rows = [];
|
|
14592
|
+
let current = {};
|
|
14593
|
+
for (const line of String(porcelain || "").split("\n")) {
|
|
14594
|
+
if (line.startsWith("worktree ")) current = { path: line.slice(9).trim() };
|
|
14595
|
+
else if (line.startsWith("branch ")) current.branch = line.slice(7).replace("refs/heads/", "").trim();
|
|
14596
|
+
else if (line.trim() === "" && current.path) {
|
|
14597
|
+
rows.push({
|
|
14598
|
+
path: current.path,
|
|
14599
|
+
branch: current.branch || "detached",
|
|
14600
|
+
name: current.path.split("/").filter(Boolean).pop() || current.path
|
|
14601
|
+
});
|
|
14602
|
+
current = {};
|
|
14603
|
+
}
|
|
14604
|
+
}
|
|
14605
|
+
if (current.path) {
|
|
14606
|
+
rows.push({
|
|
14607
|
+
path: current.path,
|
|
14608
|
+
branch: current.branch || "detached",
|
|
14609
|
+
name: current.path.split("/").filter(Boolean).pop() || current.path
|
|
14610
|
+
});
|
|
14611
|
+
}
|
|
14612
|
+
return rows;
|
|
14613
|
+
}
|
|
14614
|
+
var execFileAsync, AGENT_PREFIX, UI_SESSION, CONTAINER_USER, FIELD_SEP;
|
|
14615
|
+
var init_tmux = __esm({
|
|
14616
|
+
"cli/ui/tmux.ts"() {
|
|
14617
|
+
"use strict";
|
|
14618
|
+
execFileAsync = promisify(execFile2);
|
|
14619
|
+
AGENT_PREFIX = "synkro-agent-";
|
|
14620
|
+
UI_SESSION = "synkro-ui";
|
|
14621
|
+
CONTAINER_USER = "synkro";
|
|
14622
|
+
FIELD_SEP = "|";
|
|
14623
|
+
}
|
|
14624
|
+
});
|
|
14625
|
+
|
|
14626
|
+
// cli/ui/launch.ts
|
|
14627
|
+
function welcomeCommand() {
|
|
14628
|
+
const banner = [
|
|
14629
|
+
"",
|
|
14630
|
+
" synkro ui",
|
|
14631
|
+
" governed agents, one screen",
|
|
14632
|
+
"",
|
|
14633
|
+
" enter attach selected agent",
|
|
14634
|
+
" n new agent in selected space",
|
|
14635
|
+
" c new container agent",
|
|
14636
|
+
" g/s/y consent: track / skip / stay",
|
|
14637
|
+
" T new tab q quit",
|
|
14638
|
+
""
|
|
14639
|
+
].join("\\n");
|
|
14640
|
+
return "printf " + shellQuote3(banner + "\\n") + "; tail -f /dev/null";
|
|
14641
|
+
}
|
|
14642
|
+
function sidebarCommand(bootPath, centerPane, repoCwd) {
|
|
14643
|
+
const env = [
|
|
14644
|
+
"SYNKRO_UI_CENTER=" + shellQuote3(centerPane),
|
|
14645
|
+
"SYNKRO_UI_OUTER=" + UI_SESSION,
|
|
14646
|
+
"SYNKRO_UI_BOOT=" + shellQuote3(bootPath),
|
|
14647
|
+
"SYNKRO_UI_REPO=" + shellQuote3(repoCwd)
|
|
14648
|
+
].join(" ");
|
|
14649
|
+
return "env " + env + " node " + shellQuote3(bootPath) + " ui --sidebar";
|
|
14650
|
+
}
|
|
14651
|
+
async function styleOuterSession() {
|
|
14652
|
+
const style = [
|
|
14653
|
+
["set-option", "-t", UI_SESSION, "status-position", "top"],
|
|
14654
|
+
["set-option", "-t", UI_SESSION, "status-style", "bg=colour233,fg=colour245"],
|
|
14655
|
+
["set-option", "-t", UI_SESSION, "status-left", " synkro "],
|
|
14656
|
+
["set-option", "-t", UI_SESSION, "status-left-style", "fg=colour135,bold"],
|
|
14657
|
+
["set-option", "-t", UI_SESSION, "status-right", " + (T new tab) "],
|
|
14658
|
+
["set-option", "-t", UI_SESSION, "status-right-style", "fg=colour240"],
|
|
14659
|
+
["set-option", "-t", UI_SESSION, "-w", "window-status-format", " #W "],
|
|
14660
|
+
["set-option", "-t", UI_SESSION, "-w", "window-status-current-format", "#[bg=colour135,fg=colour233,bold] #W #[default]"],
|
|
14661
|
+
["set-option", "-t", UI_SESSION, "pane-border-style", "fg=colour236"],
|
|
14662
|
+
["set-option", "-t", UI_SESSION, "pane-active-border-style", "fg=colour135"],
|
|
14663
|
+
// Tab keys without the prefix: Alt+t new tab, Alt+arrows to move.
|
|
14664
|
+
["bind-key", "-n", "M-t", "new-window"],
|
|
14665
|
+
["bind-key", "-n", "M-Right", "next-window"],
|
|
14666
|
+
["bind-key", "-n", "M-Left", "previous-window"]
|
|
14667
|
+
];
|
|
14668
|
+
for (const argv of style) await run(HOST, ["tmux", ...argv]);
|
|
14669
|
+
}
|
|
14670
|
+
async function buildTab(bootPath, repoCwd, windowTarget) {
|
|
14671
|
+
if (windowTarget === void 0) {
|
|
14672
|
+
await run(HOST, ["tmux", "new-session", "-d", "-s", UI_SESSION, "-x", "220", "-y", "55", welcomeCommand()]);
|
|
14673
|
+
windowTarget = UI_SESSION + ":0";
|
|
14674
|
+
} else {
|
|
14675
|
+
const created = await run(HOST, ["tmux", "new-window", "-t", UI_SESSION, "-P", "-F", "#{window_id}", welcomeCommand()]);
|
|
14676
|
+
windowTarget = created.stdout.trim() || windowTarget;
|
|
14677
|
+
}
|
|
14678
|
+
await run(HOST, ["tmux", "rename-window", "-t", windowTarget, "space"]);
|
|
14679
|
+
const split = await run(HOST, [
|
|
14680
|
+
"tmux",
|
|
14681
|
+
"split-window",
|
|
14682
|
+
"-hb",
|
|
14683
|
+
"-t",
|
|
14684
|
+
windowTarget,
|
|
14685
|
+
"-l",
|
|
14686
|
+
SIDEBAR_WIDTH,
|
|
14687
|
+
"-P",
|
|
14688
|
+
"-F",
|
|
14689
|
+
"#{pane_id}",
|
|
14690
|
+
"tail -f /dev/null"
|
|
14691
|
+
]);
|
|
14692
|
+
const sidebarPane = split.stdout.trim();
|
|
14693
|
+
const panes = await run(HOST, ["tmux", "list-panes", "-t", windowTarget, "-F", "#{pane_id}"]);
|
|
14694
|
+
const centerPane = panes.stdout.split("\n").map((line) => line.trim()).filter(Boolean).find((id) => id !== sidebarPane) || "";
|
|
14695
|
+
await run(HOST, ["tmux", "respawn-pane", "-k", "-t", sidebarPane, sidebarCommand(bootPath, centerPane, repoCwd)]);
|
|
14696
|
+
}
|
|
14697
|
+
async function uiSessionExists() {
|
|
14698
|
+
const result = await run(HOST, ["tmux", "has-session", "-t", UI_SESSION]);
|
|
14699
|
+
return result.ok;
|
|
14700
|
+
}
|
|
14701
|
+
async function launchUi(bootPath, repoCwd) {
|
|
14702
|
+
if (!await uiSessionExists()) {
|
|
14703
|
+
await buildTab(bootPath, repoCwd);
|
|
14704
|
+
await styleOuterSession();
|
|
14705
|
+
}
|
|
14706
|
+
return runInherit(process.env.TMUX ? ["tmux", "switch-client", "-t", UI_SESSION] : ["tmux", "attach-session", "-t", UI_SESSION]);
|
|
14707
|
+
}
|
|
14708
|
+
var SIDEBAR_WIDTH, HOST;
|
|
14709
|
+
var init_launch = __esm({
|
|
14710
|
+
"cli/ui/launch.ts"() {
|
|
14711
|
+
"use strict";
|
|
14712
|
+
init_tmux();
|
|
14713
|
+
SIDEBAR_WIDTH = "34";
|
|
14714
|
+
HOST = { kind: "host" };
|
|
14715
|
+
}
|
|
14716
|
+
});
|
|
14717
|
+
|
|
14718
|
+
// cli/ui/model.ts
|
|
14719
|
+
function detectAsk(tail2) {
|
|
14720
|
+
for (const marker of BLOCK_MARKERS) {
|
|
14721
|
+
if (marker.pattern.test(String(tail2 || ""))) return marker.ask;
|
|
14722
|
+
}
|
|
14723
|
+
return null;
|
|
14724
|
+
}
|
|
14725
|
+
function deriveStatus(input) {
|
|
14726
|
+
if (input.dead) return { status: "done" };
|
|
14727
|
+
const ask3 = detectAsk(input.tail);
|
|
14728
|
+
if (ask3) return { status: "blocked", ask: ask3 };
|
|
14729
|
+
return { status: input.changed ? "working" : "idle" };
|
|
14730
|
+
}
|
|
14731
|
+
function hashTail(tail2) {
|
|
14732
|
+
let hash = 0;
|
|
14733
|
+
const text = String(tail2 || "");
|
|
14734
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
14735
|
+
hash = (hash << 5) - hash + text.charCodeAt(index) | 0;
|
|
14736
|
+
}
|
|
14737
|
+
return String(hash);
|
|
14738
|
+
}
|
|
14739
|
+
function mapAgentToTask(spacePath, tasks) {
|
|
14740
|
+
const normalized = String(spacePath || "").replace(/\/+$/, "");
|
|
14741
|
+
if (!normalized) return void 0;
|
|
14742
|
+
const hit = tasks.find((task) => task.worktree && task.worktree.replace(/\/+$/, "") === normalized);
|
|
14743
|
+
return hit?.linear || void 0;
|
|
14744
|
+
}
|
|
14745
|
+
async function fetchConductorTasks(baseUrl) {
|
|
14746
|
+
try {
|
|
14747
|
+
const controller = new AbortController();
|
|
14748
|
+
const timer = setTimeout(() => controller.abort(), 900);
|
|
14749
|
+
const response = await fetch(baseUrl + "/api/local/conductor/repositories", { signal: controller.signal });
|
|
14750
|
+
clearTimeout(timer);
|
|
14751
|
+
if (!response.ok) return [];
|
|
14752
|
+
const payload = await response.json().catch(() => null);
|
|
14753
|
+
return Array.isArray(payload?.tasks) ? payload.tasks.map((task) => ({
|
|
14754
|
+
worktree: task?.worktree || null,
|
|
14755
|
+
linear: task?.linear?.key || null,
|
|
14756
|
+
status: String(task?.status || "")
|
|
14757
|
+
})) : [];
|
|
14758
|
+
} catch {
|
|
14759
|
+
return [];
|
|
14760
|
+
}
|
|
14761
|
+
}
|
|
14762
|
+
async function discoverHostSpaces(repoCwd) {
|
|
14763
|
+
const result = await run({ kind: "host" }, ["git", "-C", repoCwd, "worktree", "list", "--porcelain"]);
|
|
14764
|
+
if (!result.ok) return [];
|
|
14765
|
+
return parseWorktrees(result.stdout).map((row2) => ({
|
|
14766
|
+
name: row2.name,
|
|
14767
|
+
branch: row2.branch,
|
|
14768
|
+
path: row2.path,
|
|
14769
|
+
backend: "host"
|
|
14770
|
+
}));
|
|
14771
|
+
}
|
|
14772
|
+
async function discoverContainerSpaces(runner) {
|
|
14773
|
+
if (runner.kind !== "container") return [];
|
|
14774
|
+
const result = await run(runner, ["sh", "-c", "ls -1 /home/synkro/work 2>/dev/null"]);
|
|
14775
|
+
if (!result.ok) return [];
|
|
14776
|
+
return result.stdout.split("\n").map((line) => line.trim()).filter((name) => /^ui-/.test(name)).map((name) => ({
|
|
14777
|
+
name,
|
|
14778
|
+
branch: "container",
|
|
14779
|
+
path: "/home/synkro/work/" + name,
|
|
14780
|
+
backend: "container"
|
|
14781
|
+
}));
|
|
14782
|
+
}
|
|
14783
|
+
async function discoverAgents(runner, backend, previousHashes) {
|
|
14784
|
+
const listed = await run(runner, buildListAgents());
|
|
14785
|
+
const rows = listed.ok ? parseAgentSessions(listed.stdout, backend) : [];
|
|
14786
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
14787
|
+
const agents = [];
|
|
14788
|
+
for (const row2 of rows) {
|
|
14789
|
+
const capture = row2.dead ? { ok: true, stdout: "" } : await run(runner, buildCapture(row2.session));
|
|
14790
|
+
const tail2 = capture.ok ? capture.stdout : "";
|
|
14791
|
+
const nextHash = hashTail(tail2);
|
|
14792
|
+
const changed = previousHashes.has(row2.session) && previousHashes.get(row2.session) !== nextHash;
|
|
14793
|
+
hashes.set(row2.session, nextHash);
|
|
14794
|
+
const derived = deriveStatus({ dead: row2.dead, tail: tail2, changed });
|
|
14795
|
+
agents.push({
|
|
14796
|
+
name: row2.name,
|
|
14797
|
+
session: row2.session,
|
|
14798
|
+
harness: row2.harness,
|
|
14799
|
+
space: row2.space,
|
|
14800
|
+
backend: row2.backend,
|
|
14801
|
+
status: derived.status,
|
|
14802
|
+
ask: derived.ask
|
|
14803
|
+
});
|
|
14804
|
+
}
|
|
14805
|
+
return { agents, hashes };
|
|
14806
|
+
}
|
|
14807
|
+
var BLOCK_MARKERS;
|
|
14808
|
+
var init_model = __esm({
|
|
14809
|
+
"cli/ui/model.ts"() {
|
|
14810
|
+
"use strict";
|
|
14811
|
+
init_tmux();
|
|
14812
|
+
BLOCK_MARKERS = [
|
|
14813
|
+
{ pattern: /needs a tracking decision|Task tracking suggestion|tracking decision for "/i, ask: "tracking" },
|
|
14814
|
+
{ pattern: /\[synkro:task-workspace|\[synkro:scm|keep working in the current workspace/i, ask: "workspace" },
|
|
14815
|
+
{ pattern: /⛔/, ask: "tracking" }
|
|
14816
|
+
];
|
|
14817
|
+
}
|
|
14818
|
+
});
|
|
14819
|
+
|
|
14820
|
+
// cli/ui/consent.ts
|
|
14821
|
+
function actionsForAsk(ask3) {
|
|
14822
|
+
return ask3 === "workspace" ? ["stay"] : ["track", "skip"];
|
|
14823
|
+
}
|
|
14824
|
+
var CONSENT_PHRASES;
|
|
14825
|
+
var init_consent = __esm({
|
|
14826
|
+
"cli/ui/consent.ts"() {
|
|
14827
|
+
"use strict";
|
|
14828
|
+
CONSENT_PHRASES = {
|
|
14829
|
+
/** Settles the conductor tracking ask by durable decline. */
|
|
14830
|
+
skip: "skip the task tracking, continue without a task",
|
|
14831
|
+
/** Settles the task-workspace ask in place. */
|
|
14832
|
+
stay: "stay in the current worktree",
|
|
14833
|
+
/** Asks the agent to run the two-phase create flow (draft → approve). */
|
|
14834
|
+
track: "yes, track this work \u2014 draft the requirements and create the task"
|
|
14835
|
+
};
|
|
14836
|
+
}
|
|
14837
|
+
});
|
|
14838
|
+
|
|
14839
|
+
// cli/ui/render.ts
|
|
14840
|
+
function pad(text, width) {
|
|
14841
|
+
return text.length >= width ? text.slice(0, width) : text + " ".repeat(width - text.length);
|
|
14842
|
+
}
|
|
14843
|
+
function row(selected, width, content) {
|
|
14844
|
+
const body = stripForPad(" " + content, width);
|
|
14845
|
+
return selected ? STYLE.select + body + STYLE.reset : body;
|
|
14846
|
+
}
|
|
14847
|
+
function stripForPad(text, width) {
|
|
14848
|
+
let visible = 0;
|
|
14849
|
+
let out = "";
|
|
14850
|
+
let index = 0;
|
|
14851
|
+
while (index < text.length && visible < width) {
|
|
14852
|
+
if (text.startsWith(ESC, index)) {
|
|
14853
|
+
const end = text.indexOf("m", index);
|
|
14854
|
+
if (end === -1) break;
|
|
14855
|
+
out += text.slice(index, end + 1);
|
|
14856
|
+
index = end + 1;
|
|
14857
|
+
} else {
|
|
14858
|
+
out += text[index];
|
|
14859
|
+
index += 1;
|
|
14860
|
+
visible += 1;
|
|
14861
|
+
}
|
|
14862
|
+
}
|
|
14863
|
+
return out + " ".repeat(Math.max(0, width - visible));
|
|
14864
|
+
}
|
|
14865
|
+
function renderSidebar(state, width = 32, height = 40) {
|
|
14866
|
+
const lines = [];
|
|
14867
|
+
lines.push("");
|
|
14868
|
+
lines.push(" " + STYLE.header + "spaces" + STYLE.reset);
|
|
14869
|
+
lines.push("");
|
|
14870
|
+
state.spaces.forEach((space, index) => {
|
|
14871
|
+
const selected = state.section === "spaces" && index === state.spaceIndex;
|
|
14872
|
+
const badge = space.backend === "container" ? STYLE.accent + "\u25A3 " + STYLE.reset : "";
|
|
14873
|
+
lines.push(row(selected, width, STYLE.done + "\u25CF" + STYLE.reset + " " + badge + STYLE.bold + space.name + STYLE.reset));
|
|
14874
|
+
lines.push(row(selected, width, " " + STYLE.branch + space.branch + STYLE.reset));
|
|
14875
|
+
});
|
|
14876
|
+
if (state.spaces.length === 0) lines.push(row(false, width, STYLE.dim + "no spaces found" + STYLE.reset));
|
|
14877
|
+
lines.push("");
|
|
14878
|
+
lines.push(" " + STYLE.header + "agents" + STYLE.reset);
|
|
14879
|
+
lines.push("");
|
|
14880
|
+
state.agents.forEach((agent, index) => {
|
|
14881
|
+
const selected = state.section === "agents" && index === state.agentIndex;
|
|
14882
|
+
const dot = DOT[agent.status] || DOT.idle;
|
|
14883
|
+
const badge = agent.backend === "container" ? STYLE.accent + "\u25A3 " + STYLE.reset : "";
|
|
14884
|
+
const linear = agent.linear ? " " + STYLE.dim + agent.linear + STYLE.reset : "";
|
|
14885
|
+
lines.push(row(selected, width, dot + " " + badge + STYLE.bold + agent.name + STYLE.reset + linear));
|
|
14886
|
+
const statusStyle = agent.status === "blocked" ? STYLE.blocked : STYLE.dim;
|
|
14887
|
+
lines.push(row(selected, width, " " + statusStyle + agent.status + STYLE.reset + STYLE.dim + " \xB7 " + agent.harness + STYLE.reset));
|
|
14888
|
+
});
|
|
14889
|
+
if (state.agents.length === 0) lines.push(row(false, width, STYLE.dim + "no agents \u2014 n to spawn" + STYLE.reset));
|
|
14890
|
+
const selectedAgent = state.section === "agents" ? state.agents[state.agentIndex] : void 0;
|
|
14891
|
+
if (selectedAgent?.status === "blocked" && selectedAgent.ask) {
|
|
14892
|
+
lines.push("");
|
|
14893
|
+
lines.push(row(false, width, STYLE.blocked + "\u26D4 needs consent" + STYLE.reset));
|
|
14894
|
+
for (const action of actionsForAsk(selectedAgent.ask)) {
|
|
14895
|
+
const key = action === "track" ? "g" : action === "skip" ? "s" : "y";
|
|
14896
|
+
lines.push(row(false, width, STYLE.dim + " " + key + " \u2014 " + action + STYLE.reset));
|
|
14897
|
+
}
|
|
14898
|
+
}
|
|
14899
|
+
while (lines.length < height - 4) lines.push(pad("", width));
|
|
14900
|
+
lines.push(pad("", width));
|
|
14901
|
+
if (state.message) lines.push(row(false, width, STYLE.accent + state.message.slice(0, width - 2) + STYLE.reset));
|
|
14902
|
+
else lines.push(row(false, width, STYLE.dim + "n new \xB7 enter attach \xB7 x kill" + STYLE.reset));
|
|
14903
|
+
lines.push(row(false, width, STYLE.dim + "T tab \xB7 i interrupt \xB7 q quit" + STYLE.reset));
|
|
14904
|
+
lines.push(row(false, width, STYLE.dim + state.backendNote + STYLE.reset));
|
|
14905
|
+
return lines.slice(0, height).join("\n");
|
|
14906
|
+
}
|
|
14907
|
+
var ESC, STYLE, DOT;
|
|
14908
|
+
var init_render = __esm({
|
|
14909
|
+
"cli/ui/render.ts"() {
|
|
14910
|
+
"use strict";
|
|
14911
|
+
init_consent();
|
|
14912
|
+
ESC = "\x1B[";
|
|
14913
|
+
STYLE = {
|
|
14914
|
+
reset: ESC + "0m",
|
|
14915
|
+
dim: ESC + "2m",
|
|
14916
|
+
bold: ESC + "1m",
|
|
14917
|
+
header: ESC + "38;5;245m",
|
|
14918
|
+
select: ESC + "48;5;236m",
|
|
14919
|
+
working: ESC + "38;5;214m",
|
|
14920
|
+
idle: ESC + "38;5;244m",
|
|
14921
|
+
blocked: ESC + "38;5;203m",
|
|
14922
|
+
done: ESC + "38;5;114m",
|
|
14923
|
+
accent: ESC + "38;5;135m",
|
|
14924
|
+
branch: ESC + "38;5;140m"
|
|
14925
|
+
};
|
|
14926
|
+
DOT = {
|
|
14927
|
+
working: STYLE.working + "\u25CF" + STYLE.reset,
|
|
14928
|
+
idle: STYLE.idle + "\u25CB" + STYLE.reset,
|
|
14929
|
+
blocked: STYLE.blocked + "\u25CF" + STYLE.reset,
|
|
14930
|
+
done: STYLE.done + "\u25CF" + STYLE.reset
|
|
14931
|
+
};
|
|
14932
|
+
}
|
|
14933
|
+
});
|
|
14934
|
+
|
|
14935
|
+
// cli/ui/pueue.ts
|
|
14936
|
+
function buildEnsureGroup() {
|
|
14937
|
+
return ["pueue", "group", "add", PUEUE_GROUP];
|
|
14938
|
+
}
|
|
14939
|
+
function buildGroupParallel() {
|
|
14940
|
+
return ["pueue", "parallel", "-g", PUEUE_GROUP, "32"];
|
|
14941
|
+
}
|
|
14942
|
+
function buildSentinel(session) {
|
|
14943
|
+
const loop = "while tmux has-session -t " + shellQuote3(session) + " 2>/dev/null; do sleep 10; done";
|
|
14944
|
+
return ["pueue", "add", "-g", PUEUE_GROUP, "-l", session, "--", loop];
|
|
14945
|
+
}
|
|
14946
|
+
function buildRemove(taskId) {
|
|
14947
|
+
return ["pueue", "remove", taskId];
|
|
14948
|
+
}
|
|
14949
|
+
async function pueueAvailable2(runner) {
|
|
14950
|
+
const result = await run(runner, ["sh", "-c", "command -v pueue >/dev/null 2>&1 && pueue status >/dev/null 2>&1 && echo ok"]);
|
|
14951
|
+
return result.ok && result.stdout.includes("ok");
|
|
14952
|
+
}
|
|
14953
|
+
async function registerAgent(runner, session) {
|
|
14954
|
+
if (!await pueueAvailable2(runner)) return "";
|
|
14955
|
+
await run(runner, buildEnsureGroup());
|
|
14956
|
+
await run(runner, buildGroupParallel());
|
|
14957
|
+
const added = await run(runner, buildSentinel(session));
|
|
14958
|
+
const match = added.stdout.match(/id\s+(\d+)/i) || added.stderr.match(/id\s+(\d+)/i);
|
|
14959
|
+
return match ? match[1] : "";
|
|
14960
|
+
}
|
|
14961
|
+
async function releaseAgent(runner, pueueId) {
|
|
14962
|
+
if (!pueueId) return;
|
|
14963
|
+
await run(runner, buildRemove(pueueId)).catch?.(() => {
|
|
14964
|
+
});
|
|
14965
|
+
}
|
|
14966
|
+
var PUEUE_GROUP;
|
|
14967
|
+
var init_pueue2 = __esm({
|
|
14968
|
+
"cli/ui/pueue.ts"() {
|
|
14969
|
+
"use strict";
|
|
14970
|
+
init_tmux();
|
|
14971
|
+
PUEUE_GROUP = "synkro-ui";
|
|
14972
|
+
}
|
|
14973
|
+
});
|
|
14974
|
+
|
|
14975
|
+
// cli/ui/backend.ts
|
|
14976
|
+
async function detectContainerBackend() {
|
|
14977
|
+
const runner = { kind: "container", container: CONTAINER_NAME2 };
|
|
14978
|
+
const probe = await run({ kind: "host" }, [
|
|
14979
|
+
"docker",
|
|
14980
|
+
"exec",
|
|
14981
|
+
CONTAINER_NAME2,
|
|
14982
|
+
"sh",
|
|
14983
|
+
"-c",
|
|
14984
|
+
"command -v tmux >/dev/null && command -v claude >/dev/null && ls " + shellQuote3(AUTH_SEED) + " >/dev/null 2>&1 && echo ready"
|
|
14985
|
+
]);
|
|
14986
|
+
if (probe.ok && probe.stdout.includes("ready")) {
|
|
14987
|
+
return { runner, backend: "container", note: "runtime: container (" + CONTAINER_NAME2 + ")" };
|
|
14988
|
+
}
|
|
14989
|
+
return { runner: { kind: "host" }, backend: "host", note: "runtime: host (container unavailable)" };
|
|
14990
|
+
}
|
|
14991
|
+
async function provisionContainerWorkspace(runner, slug) {
|
|
14992
|
+
const dir = CONTAINER_WORK + "/ui-" + slug;
|
|
14993
|
+
await run(runner, [
|
|
14994
|
+
"sh",
|
|
14995
|
+
"-c",
|
|
14996
|
+
"mkdir -p " + shellQuote3(dir) + " && cp -n " + shellQuote3(AUTH_SEED) + " " + shellQuote3(dir + "/.claude.json") + " 2>/dev/null; true"
|
|
14997
|
+
]);
|
|
14998
|
+
return dir;
|
|
14999
|
+
}
|
|
15000
|
+
async function spawnAgent(info, request) {
|
|
15001
|
+
const slug = slugify(request.name);
|
|
15002
|
+
const session = agentSession(slug);
|
|
15003
|
+
const runner = request.backend === "container" ? { kind: "container", container: CONTAINER_NAME2 } : { kind: "host" };
|
|
15004
|
+
let cwd = request.cwd;
|
|
15005
|
+
if (request.backend === "container") {
|
|
15006
|
+
cwd = request.cwd.startsWith(CONTAINER_WORK) ? request.cwd : await provisionContainerWorkspace(runner, slug);
|
|
15007
|
+
}
|
|
15008
|
+
const command = request.harness === "codex" ? "codex" : "claude";
|
|
15009
|
+
for (const argv of buildSpawnAgent({ name: slug, cwd, command, harness: request.harness, space: request.spaceName, backend: request.backend })) {
|
|
15010
|
+
const result = await run(runner, argv);
|
|
15011
|
+
if (!result.ok && argv[1] === "new-session") {
|
|
15012
|
+
return { ok: false, session, error: result.stderr.trim() || "tmux new-session failed" };
|
|
15013
|
+
}
|
|
15014
|
+
}
|
|
15015
|
+
const pueueId = await registerAgent(runner, session);
|
|
15016
|
+
if (pueueId) await run(runner, buildSetOption(session, "@synkro_pueue", pueueId));
|
|
15017
|
+
return { ok: true, session };
|
|
15018
|
+
}
|
|
15019
|
+
async function killAgent(backend, session, pueueId) {
|
|
15020
|
+
const runner = backend === "container" ? { kind: "container", container: CONTAINER_NAME2 } : { kind: "host" };
|
|
15021
|
+
await run(runner, buildKillSession(session));
|
|
15022
|
+
await releaseAgent(runner, pueueId);
|
|
15023
|
+
}
|
|
15024
|
+
var CONTAINER_NAME2, CONTAINER_WORK, AUTH_SEED;
|
|
15025
|
+
var init_backend = __esm({
|
|
15026
|
+
"cli/ui/backend.ts"() {
|
|
15027
|
+
"use strict";
|
|
15028
|
+
init_tmux();
|
|
15029
|
+
init_pueue2();
|
|
15030
|
+
CONTAINER_NAME2 = "synkro-server";
|
|
15031
|
+
CONTAINER_WORK = "/home/synkro/work";
|
|
15032
|
+
AUTH_SEED = CONTAINER_WORK + "/claude-1/.claude.json";
|
|
15033
|
+
}
|
|
15034
|
+
});
|
|
15035
|
+
|
|
15036
|
+
// cli/ui/sidebar.ts
|
|
15037
|
+
function runnerFor(backend) {
|
|
15038
|
+
return backend === "container" ? { kind: "container", container: CONTAINER_NAME2 } : { kind: "host" };
|
|
15039
|
+
}
|
|
15040
|
+
async function runSidebar() {
|
|
15041
|
+
const centerPane = process.env.SYNKRO_UI_CENTER || "";
|
|
15042
|
+
const outerSession = process.env.SYNKRO_UI_OUTER || "synkro-ui";
|
|
15043
|
+
const bootPath = process.env.SYNKRO_UI_BOOT || process.argv[1];
|
|
15044
|
+
const repoCwd = process.env.SYNKRO_UI_REPO || process.cwd();
|
|
15045
|
+
const info = await detectContainerBackend();
|
|
15046
|
+
const state = {
|
|
15047
|
+
spaces: [],
|
|
15048
|
+
agents: [],
|
|
15049
|
+
section: "agents",
|
|
15050
|
+
spaceIndex: 0,
|
|
15051
|
+
agentIndex: 0,
|
|
15052
|
+
backendNote: info.note,
|
|
15053
|
+
message: ""
|
|
15054
|
+
};
|
|
15055
|
+
let hashes = /* @__PURE__ */ new Map();
|
|
15056
|
+
let lastFrame = "";
|
|
15057
|
+
let spawnCounter = 1;
|
|
15058
|
+
const host = { kind: "host" };
|
|
15059
|
+
const containerRunner = { kind: "container", container: CONTAINER_NAME2 };
|
|
15060
|
+
async function refresh() {
|
|
15061
|
+
const [hostSpaces, containerSpaces, conductorTasks] = await Promise.all([
|
|
15062
|
+
discoverHostSpaces(repoCwd),
|
|
15063
|
+
info.backend === "container" ? discoverContainerSpaces(containerRunner) : Promise.resolve([]),
|
|
15064
|
+
fetchConductorTasks(CONDUCTOR_URL)
|
|
15065
|
+
]);
|
|
15066
|
+
state.spaces = [...hostSpaces, ...containerSpaces];
|
|
15067
|
+
const hostAgents = await discoverAgents(host, "host", hashes);
|
|
15068
|
+
const containerAgents = info.backend === "container" ? await discoverAgents(containerRunner, "container", hashes) : { agents: [], hashes: /* @__PURE__ */ new Map() };
|
|
15069
|
+
hashes = new Map([...hostAgents.hashes, ...containerAgents.hashes]);
|
|
15070
|
+
state.agents = [...hostAgents.agents, ...containerAgents.agents].map((agent) => ({
|
|
15071
|
+
...agent,
|
|
15072
|
+
linear: mapAgentToTask(agent.space, conductorTasks)
|
|
15073
|
+
}));
|
|
15074
|
+
state.spaceIndex = Math.min(state.spaceIndex, Math.max(0, state.spaces.length - 1));
|
|
15075
|
+
state.agentIndex = Math.min(state.agentIndex, Math.max(0, state.agents.length - 1));
|
|
15076
|
+
}
|
|
15077
|
+
function draw() {
|
|
15078
|
+
const rows = Number(process.stdout.rows || 42);
|
|
15079
|
+
const cols = Number(process.stdout.columns || 32);
|
|
15080
|
+
const frame = renderSidebar(state, cols, rows);
|
|
15081
|
+
if (frame === lastFrame) return;
|
|
15082
|
+
lastFrame = frame;
|
|
15083
|
+
process.stdout.write("\x1B[2J\x1B[H" + frame);
|
|
15084
|
+
}
|
|
15085
|
+
async function attachSelected() {
|
|
15086
|
+
const agent = state.agents[state.agentIndex];
|
|
15087
|
+
if (!agent || !centerPane) return;
|
|
15088
|
+
const command = buildCenterAttachCommand(runnerFor(agent.backend), agent.session);
|
|
15089
|
+
await run(host, ["tmux", "respawn-pane", "-k", "-t", centerPane, command]);
|
|
15090
|
+
await run(host, ["tmux", "rename-window", "-t", outerSession, agent.name]);
|
|
15091
|
+
state.message = "attached " + agent.name;
|
|
15092
|
+
}
|
|
15093
|
+
async function spawnInSelectedSpace() {
|
|
15094
|
+
const space = state.spaces[state.spaceIndex] || state.spaces[0];
|
|
15095
|
+
if (!space) {
|
|
15096
|
+
state.message = "no space selected";
|
|
15097
|
+
return;
|
|
15098
|
+
}
|
|
15099
|
+
const name = space.name + "-" + spawnCounter++;
|
|
15100
|
+
const result = await spawnAgent(info, {
|
|
15101
|
+
name,
|
|
15102
|
+
harness: "claude",
|
|
15103
|
+
spaceName: space.name,
|
|
15104
|
+
cwd: space.path,
|
|
15105
|
+
backend: space.backend === "container" && info.backend === "container" ? "container" : "host"
|
|
15106
|
+
});
|
|
15107
|
+
state.message = result.ok ? "spawned " + name : "spawn failed: " + (result.error || "").slice(0, 24);
|
|
15108
|
+
}
|
|
15109
|
+
async function spawnContainerAgent() {
|
|
15110
|
+
if (info.backend !== "container") {
|
|
15111
|
+
state.message = "container runtime unavailable";
|
|
15112
|
+
return;
|
|
15113
|
+
}
|
|
15114
|
+
const name = "box-" + spawnCounter++;
|
|
15115
|
+
const result = await spawnAgent(info, { name, harness: "claude", spaceName: name, cwd: "", backend: "container" });
|
|
15116
|
+
state.message = result.ok ? "spawned \u25A3 " + name : "spawn failed: " + (result.error || "").slice(0, 24);
|
|
15117
|
+
}
|
|
15118
|
+
async function consent(action) {
|
|
15119
|
+
const agent = state.agents[state.agentIndex];
|
|
15120
|
+
if (!agent || agent.status !== "blocked" || !agent.ask) return;
|
|
15121
|
+
if (!actionsForAsk(agent.ask).includes(action)) return;
|
|
15122
|
+
const runner = runnerFor(agent.backend);
|
|
15123
|
+
for (const argv of buildSendText(agent.session, CONSENT_PHRASES[action])) await run(runner, argv);
|
|
15124
|
+
state.message = action + " \u2192 " + agent.name;
|
|
15125
|
+
}
|
|
15126
|
+
async function newTab() {
|
|
15127
|
+
await run(host, ["node", String(bootPath), "ui", "--new-tab", outerSession]);
|
|
15128
|
+
}
|
|
15129
|
+
process.stdin.setRawMode?.(true);
|
|
15130
|
+
process.stdin.resume();
|
|
15131
|
+
process.stdin.on("data", (chunk) => {
|
|
15132
|
+
const key = chunk.toString("utf8");
|
|
15133
|
+
void (async () => {
|
|
15134
|
+
const list = state.section === "spaces" ? state.spaces : state.agents;
|
|
15135
|
+
if (key === " ") state.section = state.section === "spaces" ? "agents" : "spaces";
|
|
15136
|
+
else if (key === "j" || key === "\x1B[B") {
|
|
15137
|
+
if (state.section === "spaces") state.spaceIndex = Math.min(state.spaceIndex + 1, Math.max(0, list.length - 1));
|
|
15138
|
+
else state.agentIndex = Math.min(state.agentIndex + 1, Math.max(0, list.length - 1));
|
|
15139
|
+
} else if (key === "k" || key === "\x1B[A") {
|
|
15140
|
+
if (state.section === "spaces") state.spaceIndex = Math.max(0, state.spaceIndex - 1);
|
|
15141
|
+
else state.agentIndex = Math.max(0, state.agentIndex - 1);
|
|
15142
|
+
} else if (key === "\r") {
|
|
15143
|
+
if (state.section === "agents") await attachSelected();
|
|
15144
|
+
else state.message = "space: " + (state.spaces[state.spaceIndex]?.name || "");
|
|
15145
|
+
} else if (key === "n") await spawnInSelectedSpace();
|
|
15146
|
+
else if (key === "c") await spawnContainerAgent();
|
|
15147
|
+
else if (key === "x") {
|
|
15148
|
+
const agent = state.agents[state.agentIndex];
|
|
15149
|
+
if (agent) {
|
|
15150
|
+
await killAgent(agent.backend, agent.session, "");
|
|
15151
|
+
state.message = "killed " + agent.name;
|
|
15152
|
+
}
|
|
15153
|
+
} else if (key === "i") {
|
|
15154
|
+
const agent = state.agents[state.agentIndex];
|
|
15155
|
+
if (agent) await run(runnerFor(agent.backend), buildInterrupt(agent.session));
|
|
15156
|
+
} else if (key === "g") await consent("track");
|
|
15157
|
+
else if (key === "s") await consent("skip");
|
|
15158
|
+
else if (key === "y") await consent("stay");
|
|
15159
|
+
else if (key === "T") await newTab();
|
|
15160
|
+
else if (key === "q" || key === "") {
|
|
15161
|
+
await run(host, ["tmux", "kill-session", "-t", outerSession]);
|
|
15162
|
+
process.exit(0);
|
|
15163
|
+
}
|
|
15164
|
+
await refresh();
|
|
15165
|
+
draw();
|
|
15166
|
+
})();
|
|
15167
|
+
});
|
|
15168
|
+
await refresh();
|
|
15169
|
+
draw();
|
|
15170
|
+
setInterval(() => {
|
|
15171
|
+
void refresh().then(draw);
|
|
15172
|
+
}, POLL_MS);
|
|
15173
|
+
}
|
|
15174
|
+
var POLL_MS, CONDUCTOR_URL;
|
|
15175
|
+
var init_sidebar = __esm({
|
|
15176
|
+
"cli/ui/sidebar.ts"() {
|
|
15177
|
+
"use strict";
|
|
15178
|
+
init_model();
|
|
15179
|
+
init_render();
|
|
15180
|
+
init_consent();
|
|
15181
|
+
init_backend();
|
|
15182
|
+
init_tmux();
|
|
15183
|
+
POLL_MS = 2e3;
|
|
15184
|
+
CONDUCTOR_URL = "http://127.0.0.1:" + (process.env.SYNKRO_HOST_MCP_PORT || "18931");
|
|
15185
|
+
}
|
|
15186
|
+
});
|
|
15187
|
+
|
|
15188
|
+
// cli/commands/ui.ts
|
|
15189
|
+
var ui_exports = {};
|
|
15190
|
+
__export(ui_exports, {
|
|
15191
|
+
uiCommand: () => uiCommand
|
|
15192
|
+
});
|
|
15193
|
+
import { execSync as execSync7 } from "child_process";
|
|
15194
|
+
function repoRoot() {
|
|
15195
|
+
try {
|
|
15196
|
+
return execSync7("git rev-parse --show-toplevel", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim() || process.cwd();
|
|
15197
|
+
} catch {
|
|
15198
|
+
return process.cwd();
|
|
15199
|
+
}
|
|
15200
|
+
}
|
|
15201
|
+
function tmuxPresent() {
|
|
15202
|
+
try {
|
|
15203
|
+
execSync7("tmux -V", { stdio: ["pipe", "pipe", "pipe"] });
|
|
15204
|
+
return true;
|
|
15205
|
+
} catch {
|
|
15206
|
+
return false;
|
|
15207
|
+
}
|
|
15208
|
+
}
|
|
15209
|
+
async function uiCommand(args2) {
|
|
15210
|
+
const bootPath = String(process.argv[1] || "");
|
|
15211
|
+
if (args2.includes("--sidebar")) {
|
|
15212
|
+
await runSidebar();
|
|
15213
|
+
await new Promise(() => {
|
|
15214
|
+
});
|
|
15215
|
+
return;
|
|
15216
|
+
}
|
|
15217
|
+
if (args2.includes("--new-tab")) {
|
|
15218
|
+
await buildTab(bootPath, repoRoot(), "new");
|
|
15219
|
+
return;
|
|
15220
|
+
}
|
|
15221
|
+
if (!tmuxPresent()) {
|
|
15222
|
+
console.error("synkro ui needs tmux. Install it (brew install tmux) and rerun.");
|
|
15223
|
+
process.exitCode = 1;
|
|
15224
|
+
return;
|
|
15225
|
+
}
|
|
15226
|
+
const code = await launchUi(bootPath, repoRoot());
|
|
15227
|
+
process.exitCode = code;
|
|
15228
|
+
}
|
|
15229
|
+
var init_ui = __esm({
|
|
15230
|
+
"cli/commands/ui.ts"() {
|
|
15231
|
+
"use strict";
|
|
15232
|
+
init_launch();
|
|
15233
|
+
init_sidebar();
|
|
15234
|
+
}
|
|
15235
|
+
});
|
|
15236
|
+
|
|
14090
15237
|
// cli/commands/refresh.ts
|
|
14091
15238
|
var refresh_exports = {};
|
|
14092
15239
|
__export(refresh_exports, {
|
|
@@ -14124,11 +15271,11 @@ __export(linear_exports, {
|
|
|
14124
15271
|
linearCommand: () => linearCommand
|
|
14125
15272
|
});
|
|
14126
15273
|
import { readFileSync as readFileSync30 } from "fs";
|
|
14127
|
-
import { homedir as
|
|
14128
|
-
import { join as
|
|
15274
|
+
import { homedir as homedir32 } from "os";
|
|
15275
|
+
import { join as join32 } from "path";
|
|
14129
15276
|
function mcpJwt() {
|
|
14130
15277
|
try {
|
|
14131
|
-
return readFileSync30(
|
|
15278
|
+
return readFileSync30(join32(SYNKRO_DIR14, ".mcp-jwt"), "utf-8").trim();
|
|
14132
15279
|
} catch {
|
|
14133
15280
|
return "";
|
|
14134
15281
|
}
|
|
@@ -14167,7 +15314,7 @@ var SYNKRO_DIR14, PORT2, BASE;
|
|
|
14167
15314
|
var init_linear = __esm({
|
|
14168
15315
|
"cli/commands/linear.ts"() {
|
|
14169
15316
|
"use strict";
|
|
14170
|
-
SYNKRO_DIR14 =
|
|
15317
|
+
SYNKRO_DIR14 = join32(homedir32(), ".synkro");
|
|
14171
15318
|
PORT2 = process.env.SYNKRO_MCP_PORT || "18931";
|
|
14172
15319
|
BASE = `http://127.0.0.1:${PORT2}`;
|
|
14173
15320
|
}
|
|
@@ -14316,33 +15463,33 @@ var init_cveReachability = __esm({
|
|
|
14316
15463
|
});
|
|
14317
15464
|
|
|
14318
15465
|
// cli/reachability/reachabilityScan.ts
|
|
14319
|
-
import { spawnSync as
|
|
14320
|
-
import { readFileSync as readFileSync32, writeFileSync as
|
|
14321
|
-
import { join as
|
|
14322
|
-
import { homedir as
|
|
15466
|
+
import { spawnSync as spawnSync12, execFileSync as execFileSync5 } from "child_process";
|
|
15467
|
+
import { readFileSync as readFileSync32, writeFileSync as writeFileSync23, existsSync as existsSync34, readdirSync as readdirSync9 } from "fs";
|
|
15468
|
+
import { join as join33 } from "path";
|
|
15469
|
+
import { homedir as homedir33 } from "os";
|
|
14323
15470
|
import { createRequire } from "module";
|
|
14324
|
-
function walkSourceFiles(
|
|
15471
|
+
function walkSourceFiles(repoRoot3, maxFiles = 4e3, maxBytes = 5e5) {
|
|
14325
15472
|
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".turbo", "out", ".cache", ".synkro", ".claude", "vendor", "__tests__", "test-results"]);
|
|
14326
15473
|
const EXT = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
14327
15474
|
const files = [];
|
|
14328
|
-
const stack = [
|
|
15475
|
+
const stack = [repoRoot3];
|
|
14329
15476
|
while (stack.length && files.length < maxFiles) {
|
|
14330
15477
|
const dir = stack.pop();
|
|
14331
15478
|
let ents;
|
|
14332
15479
|
try {
|
|
14333
|
-
ents =
|
|
15480
|
+
ents = readdirSync9(dir, { withFileTypes: true });
|
|
14334
15481
|
} catch {
|
|
14335
15482
|
continue;
|
|
14336
15483
|
}
|
|
14337
15484
|
for (const e of ents) {
|
|
14338
15485
|
if (files.length >= maxFiles) break;
|
|
14339
|
-
const full =
|
|
15486
|
+
const full = join33(dir, e.name);
|
|
14340
15487
|
if (e.isDirectory()) {
|
|
14341
15488
|
if (!SKIP.has(e.name) && !e.name.startsWith(".")) stack.push(full);
|
|
14342
15489
|
continue;
|
|
14343
15490
|
}
|
|
14344
15491
|
if (!EXT.test(e.name) || e.name.endsWith(".d.ts")) continue;
|
|
14345
|
-
const rel = full.startsWith(
|
|
15492
|
+
const rel = full.startsWith(repoRoot3 + "/") ? full.slice(repoRoot3.length + 1) : full;
|
|
14346
15493
|
try {
|
|
14347
15494
|
const content = readFileSync32(full, "utf8");
|
|
14348
15495
|
if (content.length <= maxBytes) files.push({ path: rel, content });
|
|
@@ -14360,15 +15507,15 @@ function cleanVersion(spec) {
|
|
|
14360
15507
|
const c = s.replace(/^[\^~>=<\s]+/, "");
|
|
14361
15508
|
return /^\d[\w.\-+]*$/.test(c) ? c : null;
|
|
14362
15509
|
}
|
|
14363
|
-
function gatherManifestVersions(
|
|
15510
|
+
function gatherManifestVersions(repoRoot3) {
|
|
14364
15511
|
const out = {};
|
|
14365
|
-
const dirs = [
|
|
14366
|
-
const pkgsDir =
|
|
14367
|
-
if (
|
|
15512
|
+
const dirs = [repoRoot3];
|
|
15513
|
+
const pkgsDir = join33(repoRoot3, "packages");
|
|
15514
|
+
if (existsSync34(pkgsDir)) {
|
|
14368
15515
|
try {
|
|
14369
|
-
for (const d of
|
|
14370
|
-
const pd =
|
|
14371
|
-
if (
|
|
15516
|
+
for (const d of readdirSync9(pkgsDir)) {
|
|
15517
|
+
const pd = join33(pkgsDir, d);
|
|
15518
|
+
if (existsSync34(join33(pd, "package.json"))) dirs.push(pd);
|
|
14372
15519
|
}
|
|
14373
15520
|
} catch {
|
|
14374
15521
|
}
|
|
@@ -14377,7 +15524,7 @@ function gatherManifestVersions(repoRoot2) {
|
|
|
14377
15524
|
for (const dir of dirs) {
|
|
14378
15525
|
let pkg;
|
|
14379
15526
|
try {
|
|
14380
|
-
pkg = JSON.parse(readFileSync32(
|
|
15527
|
+
pkg = JSON.parse(readFileSync32(join33(dir, "package.json"), "utf8"));
|
|
14381
15528
|
} catch {
|
|
14382
15529
|
continue;
|
|
14383
15530
|
}
|
|
@@ -14393,32 +15540,32 @@ function gatherManifestVersions(repoRoot2) {
|
|
|
14393
15540
|
}
|
|
14394
15541
|
return out;
|
|
14395
15542
|
}
|
|
14396
|
-
function findJelly(
|
|
15543
|
+
function findJelly(repoRoot3) {
|
|
14397
15544
|
try {
|
|
14398
15545
|
const pkgJson = require2.resolve("@cs-au-dk/jelly/package.json");
|
|
14399
15546
|
const dir = pkgJson.slice(0, pkgJson.length - "package.json".length);
|
|
14400
15547
|
const pkg = JSON.parse(readFileSync32(pkgJson, "utf8"));
|
|
14401
15548
|
const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin && (pkg.bin.jelly || pkg.bin[Object.keys(pkg.bin)[0]]);
|
|
14402
15549
|
if (bin) {
|
|
14403
|
-
const p =
|
|
14404
|
-
if (
|
|
15550
|
+
const p = join33(dir, bin);
|
|
15551
|
+
if (existsSync34(p)) return p;
|
|
14405
15552
|
}
|
|
14406
15553
|
} catch {
|
|
14407
15554
|
}
|
|
14408
|
-
for (const base of [
|
|
14409
|
-
const b =
|
|
14410
|
-
if (
|
|
15555
|
+
for (const base of [repoRoot3, process.cwd()]) {
|
|
15556
|
+
const b = join33(base, "node_modules", ".bin", "jelly");
|
|
15557
|
+
if (existsSync34(b)) return b;
|
|
14411
15558
|
}
|
|
14412
15559
|
return null;
|
|
14413
15560
|
}
|
|
14414
|
-
function findEntries(
|
|
14415
|
-
const dirs = [
|
|
14416
|
-
const pkgsDir =
|
|
14417
|
-
if (
|
|
15561
|
+
function findEntries(repoRoot3) {
|
|
15562
|
+
const dirs = [repoRoot3];
|
|
15563
|
+
const pkgsDir = join33(repoRoot3, "packages");
|
|
15564
|
+
if (existsSync34(pkgsDir)) {
|
|
14418
15565
|
try {
|
|
14419
|
-
for (const d of
|
|
14420
|
-
const pd =
|
|
14421
|
-
if (
|
|
15566
|
+
for (const d of readdirSync9(pkgsDir)) {
|
|
15567
|
+
const pd = join33(pkgsDir, d);
|
|
15568
|
+
if (existsSync34(join33(pd, "package.json"))) dirs.push(pd);
|
|
14422
15569
|
}
|
|
14423
15570
|
} catch {
|
|
14424
15571
|
}
|
|
@@ -14426,12 +15573,12 @@ function findEntries(repoRoot2) {
|
|
|
14426
15573
|
const entries = [];
|
|
14427
15574
|
for (const dir of dirs) {
|
|
14428
15575
|
try {
|
|
14429
|
-
const pkg = JSON.parse(readFileSync32(
|
|
15576
|
+
const pkg = JSON.parse(readFileSync32(join33(dir, "package.json"), "utf8"));
|
|
14430
15577
|
const cands = [pkg.source, pkg.module, pkg.main, "src/index.ts", "src/index.js", "src/main.ts", "src/server.ts", "index.ts", "index.js"];
|
|
14431
15578
|
for (const c of cands) {
|
|
14432
15579
|
if (typeof c !== "string") continue;
|
|
14433
|
-
const f =
|
|
14434
|
-
if (
|
|
15580
|
+
const f = join33(dir, c);
|
|
15581
|
+
if (existsSync34(f)) {
|
|
14435
15582
|
entries.push(f);
|
|
14436
15583
|
break;
|
|
14437
15584
|
}
|
|
@@ -14441,9 +15588,9 @@ function findEntries(repoRoot2) {
|
|
|
14441
15588
|
}
|
|
14442
15589
|
return entries.slice(0, 40);
|
|
14443
15590
|
}
|
|
14444
|
-
function currentCommit(
|
|
15591
|
+
function currentCommit(repoRoot3) {
|
|
14445
15592
|
try {
|
|
14446
|
-
return execFileSync5("git", ["rev-parse", "HEAD"], { cwd:
|
|
15593
|
+
return execFileSync5("git", ["rev-parse", "HEAD"], { cwd: repoRoot3, encoding: "utf8" }).trim();
|
|
14447
15594
|
} catch {
|
|
14448
15595
|
return "";
|
|
14449
15596
|
}
|
|
@@ -14462,9 +15609,9 @@ function parseApiUsage(log) {
|
|
|
14462
15609
|
for (const k of Object.keys(map)) out[k] = { reachableApis: [...map[k]].slice(0, 60) };
|
|
14463
15610
|
return out;
|
|
14464
15611
|
}
|
|
14465
|
-
function runReachabilityScan(
|
|
14466
|
-
const commit = currentCommit(
|
|
14467
|
-
if (!opts.force && commit &&
|
|
15612
|
+
function runReachabilityScan(repoRoot3, opts = {}) {
|
|
15613
|
+
const commit = currentCommit(repoRoot3);
|
|
15614
|
+
if (!opts.force && commit && existsSync34(REACHABILITY_PATH)) {
|
|
14468
15615
|
try {
|
|
14469
15616
|
const prev = JSON.parse(readFileSync32(REACHABILITY_PATH, "utf8"));
|
|
14470
15617
|
if (prev.commit === commit) return { ok: true, cached: true, packages: Object.keys(prev.packages || {}).length };
|
|
@@ -14513,13 +15660,13 @@ function runReachabilityScan(repoRoot2, opts = {}) {
|
|
|
14513
15660
|
}
|
|
14514
15661
|
};
|
|
14515
15662
|
let tool = "ast";
|
|
14516
|
-
const jelly = findJelly(
|
|
15663
|
+
const jelly = findJelly(repoRoot3);
|
|
14517
15664
|
if (jelly) {
|
|
14518
|
-
const entries = findEntries(
|
|
15665
|
+
const entries = findEntries(repoRoot3);
|
|
14519
15666
|
if (entries.length > 0) {
|
|
14520
|
-
const r =
|
|
15667
|
+
const r = spawnSync12(
|
|
14521
15668
|
process.execPath,
|
|
14522
|
-
[jelly, "-b",
|
|
15669
|
+
[jelly, "-b", repoRoot3, "--api-usage", ...entries],
|
|
14523
15670
|
{ encoding: "utf8", timeout: opts.timeoutMs ?? 18e4, maxBuffer: 2e8 }
|
|
14524
15671
|
);
|
|
14525
15672
|
const jp = parseApiUsage((r.stdout || "") + (r.stderr || ""));
|
|
@@ -14529,7 +15676,7 @@ function runReachabilityScan(repoRoot2, opts = {}) {
|
|
|
14529
15676
|
}
|
|
14530
15677
|
}
|
|
14531
15678
|
}
|
|
14532
|
-
const astPackages = extractAllPackageUsage(walkSourceFiles(
|
|
15679
|
+
const astPackages = extractAllPackageUsage(walkSourceFiles(repoRoot3));
|
|
14533
15680
|
for (const [k, v] of Object.entries(astPackages)) {
|
|
14534
15681
|
addAll(k, v.apis);
|
|
14535
15682
|
addSites(k, v.sites);
|
|
@@ -14553,9 +15700,9 @@ function runReachabilityScan(repoRoot2, opts = {}) {
|
|
|
14553
15700
|
packages[k] = entry;
|
|
14554
15701
|
}
|
|
14555
15702
|
if (Object.keys(packages).length === 0) return { ok: false, reason: "no package usage found (no jelly output, no AST imports)" };
|
|
14556
|
-
const file = { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), commit, tool, packages, versions: gatherManifestVersions(
|
|
15703
|
+
const file = { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), commit, tool, packages, versions: gatherManifestVersions(repoRoot3) };
|
|
14557
15704
|
try {
|
|
14558
|
-
|
|
15705
|
+
writeFileSync23(REACHABILITY_PATH, JSON.stringify(file, null, 2));
|
|
14559
15706
|
} catch (e) {
|
|
14560
15707
|
return { ok: false, reason: "write failed: " + String(e.message || e) };
|
|
14561
15708
|
}
|
|
@@ -14567,7 +15714,7 @@ var init_reachabilityScan = __esm({
|
|
|
14567
15714
|
"use strict";
|
|
14568
15715
|
init_cveReachability();
|
|
14569
15716
|
require2 = createRequire(import.meta.url);
|
|
14570
|
-
REACHABILITY_PATH =
|
|
15717
|
+
REACHABILITY_PATH = join33(homedir33(), ".synkro", "reachability.json");
|
|
14571
15718
|
}
|
|
14572
15719
|
});
|
|
14573
15720
|
|
|
@@ -14576,13 +15723,13 @@ var reachabilityScan_exports = {};
|
|
|
14576
15723
|
__export(reachabilityScan_exports, {
|
|
14577
15724
|
reachabilityScanCommand: () => reachabilityScanCommand
|
|
14578
15725
|
});
|
|
14579
|
-
import { readFileSync as readFileSync33, existsSync as
|
|
14580
|
-
import { join as
|
|
14581
|
-
import { homedir as
|
|
15726
|
+
import { readFileSync as readFileSync33, existsSync as existsSync35 } from "fs";
|
|
15727
|
+
import { join as join34 } from "path";
|
|
15728
|
+
import { homedir as homedir34 } from "os";
|
|
14582
15729
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
14583
15730
|
function readConfigEnv4() {
|
|
14584
|
-
const p =
|
|
14585
|
-
if (!
|
|
15731
|
+
const p = join34(SYNKRO_DIR15, "config.env");
|
|
15732
|
+
if (!existsSync35(p)) return {};
|
|
14586
15733
|
const out = {};
|
|
14587
15734
|
for (const line of readFileSync33(p, "utf-8").split("\n")) {
|
|
14588
15735
|
const t = line.trim();
|
|
@@ -14592,7 +15739,7 @@ function readConfigEnv4() {
|
|
|
14592
15739
|
}
|
|
14593
15740
|
return out;
|
|
14594
15741
|
}
|
|
14595
|
-
function
|
|
15742
|
+
function repoRoot2() {
|
|
14596
15743
|
try {
|
|
14597
15744
|
return execFileSync6("git", ["rev-parse", "--show-toplevel"], { encoding: "utf-8" }).trim();
|
|
14598
15745
|
} catch {
|
|
@@ -14600,14 +15747,14 @@ function repoRoot() {
|
|
|
14600
15747
|
}
|
|
14601
15748
|
}
|
|
14602
15749
|
function repoSlug(root) {
|
|
14603
|
-
const
|
|
15750
|
+
const run2 = (a) => {
|
|
14604
15751
|
try {
|
|
14605
15752
|
return execFileSync6("git", a, { encoding: "utf-8" }).trim();
|
|
14606
15753
|
} catch {
|
|
14607
15754
|
return "";
|
|
14608
15755
|
}
|
|
14609
15756
|
};
|
|
14610
|
-
const remote =
|
|
15757
|
+
const remote = run2(["remote", "get-url", "origin"]);
|
|
14611
15758
|
if (remote) return remote.replace(/^git@[^:]+:/, "").replace(/^https?:\/\/[^/]+\//, "").replace(/\.git$/, "");
|
|
14612
15759
|
return root.split("/").pop() || root;
|
|
14613
15760
|
}
|
|
@@ -14616,10 +15763,10 @@ async function pushToCloud(cfg, repo) {
|
|
|
14616
15763
|
while (gwBase.endsWith("/")) gwBase = gwBase.slice(0, -1);
|
|
14617
15764
|
let jwt2 = "";
|
|
14618
15765
|
try {
|
|
14619
|
-
jwt2 = readFileSync33(
|
|
15766
|
+
jwt2 = readFileSync33(join34(SYNKRO_DIR15, ".mcp-jwt"), "utf-8").trim();
|
|
14620
15767
|
} catch {
|
|
14621
15768
|
}
|
|
14622
|
-
if (!jwt2 || !
|
|
15769
|
+
if (!jwt2 || !existsSync35(REACHABILITY_PATH)) return;
|
|
14623
15770
|
const body = readFileSync33(REACHABILITY_PATH, "utf-8");
|
|
14624
15771
|
try {
|
|
14625
15772
|
const resp = await fetch(gwBase + "/api/v1/reachability?repo=" + encodeURIComponent(repo), {
|
|
@@ -14634,7 +15781,7 @@ async function pushToCloud(cfg, repo) {
|
|
|
14634
15781
|
}
|
|
14635
15782
|
}
|
|
14636
15783
|
async function reachabilityScanCommand(args2 = []) {
|
|
14637
|
-
const root =
|
|
15784
|
+
const root = repoRoot2();
|
|
14638
15785
|
const force = args2.includes("--force");
|
|
14639
15786
|
const quiet = args2.includes("--quiet");
|
|
14640
15787
|
const res = runReachabilityScan(root, { force });
|
|
@@ -14652,7 +15799,7 @@ var init_reachabilityScan2 = __esm({
|
|
|
14652
15799
|
"cli/commands/reachabilityScan.ts"() {
|
|
14653
15800
|
"use strict";
|
|
14654
15801
|
init_reachabilityScan();
|
|
14655
|
-
SYNKRO_DIR15 =
|
|
15802
|
+
SYNKRO_DIR15 = join34(homedir34(), ".synkro");
|
|
14656
15803
|
}
|
|
14657
15804
|
});
|
|
14658
15805
|
|
|
@@ -14782,11 +15929,11 @@ var config_exports = {};
|
|
|
14782
15929
|
__export(config_exports, {
|
|
14783
15930
|
configCommand: () => configCommand
|
|
14784
15931
|
});
|
|
14785
|
-
import { readFileSync as readFileSync34, writeFileSync as
|
|
14786
|
-
import { join as
|
|
14787
|
-
import { homedir as
|
|
15932
|
+
import { readFileSync as readFileSync34, writeFileSync as writeFileSync24, existsSync as existsSync36 } from "fs";
|
|
15933
|
+
import { join as join35 } from "path";
|
|
15934
|
+
import { homedir as homedir35 } from "os";
|
|
14788
15935
|
function readConfigEnv5() {
|
|
14789
|
-
if (!
|
|
15936
|
+
if (!existsSync36(CONFIG_PATH9)) return {};
|
|
14790
15937
|
const out = {};
|
|
14791
15938
|
for (const line of readFileSync34(CONFIG_PATH9, "utf-8").split("\n")) {
|
|
14792
15939
|
const t = line.trim();
|
|
@@ -14797,7 +15944,7 @@ function readConfigEnv5() {
|
|
|
14797
15944
|
return out;
|
|
14798
15945
|
}
|
|
14799
15946
|
function updateConfigValue(key, value) {
|
|
14800
|
-
if (!
|
|
15947
|
+
if (!existsSync36(CONFIG_PATH9)) {
|
|
14801
15948
|
console.error("No config found. Run `synkro install` first.");
|
|
14802
15949
|
process.exit(1);
|
|
14803
15950
|
}
|
|
@@ -14812,7 +15959,7 @@ function updateConfigValue(key, value) {
|
|
|
14812
15959
|
return line;
|
|
14813
15960
|
});
|
|
14814
15961
|
if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
|
|
14815
|
-
|
|
15962
|
+
writeFileSync24(CONFIG_PATH9, updated.join("\n"), "utf-8");
|
|
14816
15963
|
}
|
|
14817
15964
|
function resolveInferenceMode(cfg) {
|
|
14818
15965
|
if ((cfg.SYNKRO_GRADING_MODE || "local") === "byok") return "byok";
|
|
@@ -14970,8 +16117,8 @@ var init_config = __esm({
|
|
|
14970
16117
|
"use strict";
|
|
14971
16118
|
init_stub();
|
|
14972
16119
|
init_optout();
|
|
14973
|
-
SYNKRO_DIR16 =
|
|
14974
|
-
CONFIG_PATH9 =
|
|
16120
|
+
SYNKRO_DIR16 = join35(homedir35(), ".synkro");
|
|
16121
|
+
CONFIG_PATH9 = join35(SYNKRO_DIR16, "config.env");
|
|
14975
16122
|
}
|
|
14976
16123
|
});
|
|
14977
16124
|
|
|
@@ -15035,11 +16182,11 @@ async function printTail(args2) {
|
|
|
15035
16182
|
console.log("(no events \u2014 run `synkro start` if the container is down so JSONL pending events can drain)");
|
|
15036
16183
|
return;
|
|
15037
16184
|
}
|
|
15038
|
-
for (const
|
|
15039
|
-
const session =
|
|
15040
|
-
const ts = typeof
|
|
15041
|
-
console.log(` ${ts} ${
|
|
15042
|
-
const contextStr = typeof
|
|
16185
|
+
for (const row2 of rows) {
|
|
16186
|
+
const session = row2.cc_session_id ? ` session=${String(row2.cc_session_id).slice(0, 8)}` : "";
|
|
16187
|
+
const ts = typeof row2.occurred_at === "string" ? row2.occurred_at : new Date(row2.occurred_at).toISOString();
|
|
16188
|
+
console.log(` ${ts} ${row2.event_type.padEnd(20)} ${row2.emitter}${session}`);
|
|
16189
|
+
const contextStr = typeof row2.context === "string" ? row2.context : JSON.stringify(row2.context);
|
|
15043
16190
|
console.log(` context: ${truncate2(contextStr, 200)}`);
|
|
15044
16191
|
}
|
|
15045
16192
|
}
|
|
@@ -15161,27 +16308,27 @@ Usage:
|
|
|
15161
16308
|
|
|
15162
16309
|
// cli/inventory/identity.ts
|
|
15163
16310
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
15164
|
-
import { existsSync as
|
|
15165
|
-
import { homedir as
|
|
15166
|
-
import { dirname as dirname9, join as
|
|
16311
|
+
import { existsSync as existsSync37, mkdirSync as mkdirSync20, readFileSync as readFileSync35, renameSync as renameSync9, writeFileSync as writeFileSync25 } from "fs";
|
|
16312
|
+
import { homedir as homedir36 } from "os";
|
|
16313
|
+
import { dirname as dirname9, join as join36 } from "path";
|
|
15167
16314
|
function operationalIdentityPath() {
|
|
15168
|
-
return process.env.SYNKRO_OPERATIONAL_IDENTITY_PATH ||
|
|
16315
|
+
return process.env.SYNKRO_OPERATIONAL_IDENTITY_PATH || join36(homedir36(), ".synkro", "installation.json");
|
|
15169
16316
|
}
|
|
15170
16317
|
function validIdentity(value) {
|
|
15171
16318
|
if (!value || typeof value !== "object") return false;
|
|
15172
|
-
const
|
|
15173
|
-
return typeof
|
|
16319
|
+
const row2 = value;
|
|
16320
|
+
return typeof row2.installation_id === "string" && UUID_RE.test(row2.installation_id) && typeof row2.created_at === "string" && Number.isFinite(Date.parse(row2.created_at));
|
|
15174
16321
|
}
|
|
15175
16322
|
function writeIdentity(path, identity) {
|
|
15176
|
-
|
|
16323
|
+
mkdirSync20(dirname9(path), { recursive: true, mode: 448 });
|
|
15177
16324
|
const temp = `${path}.${process.pid}.${randomUUID5()}.tmp`;
|
|
15178
|
-
|
|
16325
|
+
writeFileSync25(temp, JSON.stringify(identity, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
15179
16326
|
renameSync9(temp, path);
|
|
15180
16327
|
}
|
|
15181
16328
|
function getOperationalInstallationIdentity(path = operationalIdentityPath()) {
|
|
15182
16329
|
const prior = cached4.get(path);
|
|
15183
16330
|
if (prior) return prior;
|
|
15184
|
-
if (
|
|
16331
|
+
if (existsSync37(path)) {
|
|
15185
16332
|
try {
|
|
15186
16333
|
const parsed = JSON.parse(readFileSync35(path, "utf8"));
|
|
15187
16334
|
if (validIdentity(parsed)) {
|
|
@@ -15208,13 +16355,13 @@ var init_identity2 = __esm({
|
|
|
15208
16355
|
// cli/inventory/collector.ts
|
|
15209
16356
|
import { createHash as createHash5 } from "crypto";
|
|
15210
16357
|
import {
|
|
15211
|
-
existsSync as
|
|
16358
|
+
existsSync as existsSync38,
|
|
15212
16359
|
readFileSync as readFileSync36,
|
|
15213
|
-
readdirSync as
|
|
16360
|
+
readdirSync as readdirSync10,
|
|
15214
16361
|
statSync as statSync5
|
|
15215
16362
|
} from "fs";
|
|
15216
|
-
import { arch, homedir as
|
|
15217
|
-
import { basename as basename3, join as
|
|
16363
|
+
import { arch, homedir as homedir37, hostname as hostname2, platform as platform5, release } from "os";
|
|
16364
|
+
import { basename as basename3, join as join37, relative, resolve as resolve5 } from "path";
|
|
15218
16365
|
import { fileURLToPath } from "url";
|
|
15219
16366
|
function sha256(value) {
|
|
15220
16367
|
return createHash5("sha256").update(value).digest("hex");
|
|
@@ -15224,14 +16371,14 @@ function pseudonymousHostnameHash(installationId, host) {
|
|
|
15224
16371
|
}
|
|
15225
16372
|
function cliVersion() {
|
|
15226
16373
|
try {
|
|
15227
|
-
return "1.
|
|
16374
|
+
return "1.10.2";
|
|
15228
16375
|
} catch {
|
|
15229
16376
|
return "0.0.0";
|
|
15230
16377
|
}
|
|
15231
16378
|
}
|
|
15232
16379
|
function readJson(path) {
|
|
15233
16380
|
try {
|
|
15234
|
-
if (!
|
|
16381
|
+
if (!existsSync38(path)) return null;
|
|
15235
16382
|
const parsed = JSON.parse(readFileSync36(path, "utf8"));
|
|
15236
16383
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
15237
16384
|
} catch {
|
|
@@ -15240,7 +16387,7 @@ function readJson(path) {
|
|
|
15240
16387
|
}
|
|
15241
16388
|
function readText(path) {
|
|
15242
16389
|
try {
|
|
15243
|
-
if (!
|
|
16390
|
+
if (!existsSync38(path)) return "";
|
|
15244
16391
|
return readFileSync36(path, "utf8");
|
|
15245
16392
|
} catch {
|
|
15246
16393
|
return "";
|
|
@@ -15327,16 +16474,16 @@ function mcpArtifactsFromJson(harness, config, configScope = "user") {
|
|
|
15327
16474
|
}
|
|
15328
16475
|
function claudeDesktopConfigCandidates(home, targetPlatform) {
|
|
15329
16476
|
if (targetPlatform === "darwin") {
|
|
15330
|
-
return [
|
|
16477
|
+
return [join37(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")];
|
|
15331
16478
|
}
|
|
15332
16479
|
if (targetPlatform === "linux") {
|
|
15333
16480
|
return [
|
|
15334
|
-
|
|
15335
|
-
|
|
16481
|
+
join37(home, ".config", "Claude", "claude_desktop_config.json"),
|
|
16482
|
+
join37(home, ".config", "claude", "claude_desktop_config.json")
|
|
15336
16483
|
];
|
|
15337
16484
|
}
|
|
15338
16485
|
if (targetPlatform === "win32" && process.env.APPDATA) {
|
|
15339
|
-
return [
|
|
16486
|
+
return [join37(process.env.APPDATA, "Claude", "claude_desktop_config.json")];
|
|
15340
16487
|
}
|
|
15341
16488
|
return [];
|
|
15342
16489
|
}
|
|
@@ -15344,7 +16491,7 @@ function claudeManagedMcpConfigCandidates(targetPlatform) {
|
|
|
15344
16491
|
if (targetPlatform === "darwin") return ["/Library/Application Support/ClaudeCode/managed-mcp.json"];
|
|
15345
16492
|
if (targetPlatform === "linux") return ["/etc/claude-code/managed-mcp.json"];
|
|
15346
16493
|
if (targetPlatform === "win32" && process.env.ProgramFiles) {
|
|
15347
|
-
return [
|
|
16494
|
+
return [join37(process.env.ProgramFiles, "ClaudeCode", "managed-mcp.json")];
|
|
15348
16495
|
}
|
|
15349
16496
|
return [];
|
|
15350
16497
|
}
|
|
@@ -15353,7 +16500,7 @@ function discoveredProjectRoots(claudeState, currentDirectory, explicit = [], cu
|
|
|
15353
16500
|
const add = (value) => {
|
|
15354
16501
|
if (typeof value !== "string" || !value.trim()) return;
|
|
15355
16502
|
const path = resolve5(value);
|
|
15356
|
-
if (
|
|
16503
|
+
if (existsSync38(path)) roots.add(path);
|
|
15357
16504
|
};
|
|
15358
16505
|
add(currentDirectory);
|
|
15359
16506
|
for (const path of explicit) add(path);
|
|
@@ -15364,31 +16511,31 @@ function discoveredProjectRoots(claudeState, currentDirectory, explicit = [], cu
|
|
|
15364
16511
|
return [...roots];
|
|
15365
16512
|
}
|
|
15366
16513
|
function cursorWorkspaceStorageCandidates(home, targetPlatform) {
|
|
15367
|
-
if (targetPlatform === "darwin") return [
|
|
15368
|
-
if (targetPlatform === "linux") return [
|
|
16514
|
+
if (targetPlatform === "darwin") return [join37(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage")];
|
|
16515
|
+
if (targetPlatform === "linux") return [join37(home, ".config", "Cursor", "User", "workspaceStorage")];
|
|
15369
16516
|
if (targetPlatform === "win32" && process.env.APPDATA) {
|
|
15370
|
-
return [
|
|
16517
|
+
return [join37(process.env.APPDATA, "Cursor", "User", "workspaceStorage")];
|
|
15371
16518
|
}
|
|
15372
16519
|
return [];
|
|
15373
16520
|
}
|
|
15374
16521
|
function cursorWorkspaceRoots(home, targetPlatform) {
|
|
15375
16522
|
const roots = /* @__PURE__ */ new Set();
|
|
15376
16523
|
for (const storage of cursorWorkspaceStorageCandidates(home, targetPlatform)) {
|
|
15377
|
-
if (!
|
|
16524
|
+
if (!existsSync38(storage)) continue;
|
|
15378
16525
|
let entries = [];
|
|
15379
16526
|
try {
|
|
15380
|
-
entries =
|
|
16527
|
+
entries = readdirSync10(storage, { withFileTypes: true });
|
|
15381
16528
|
} catch {
|
|
15382
16529
|
continue;
|
|
15383
16530
|
}
|
|
15384
16531
|
for (const entry of entries) {
|
|
15385
16532
|
if (!entry.isDirectory() || entry.isSymbolicLink?.()) continue;
|
|
15386
|
-
const state = readJson(
|
|
16533
|
+
const state = readJson(join37(storage, entry.name, "workspace.json"));
|
|
15387
16534
|
const raw = state?.folder;
|
|
15388
16535
|
if (typeof raw !== "string" || !raw.trim()) continue;
|
|
15389
16536
|
try {
|
|
15390
16537
|
const path = raw.startsWith("file:") ? fileURLToPath(raw) : raw;
|
|
15391
|
-
if (
|
|
16538
|
+
if (existsSync38(path)) roots.add(resolve5(path));
|
|
15392
16539
|
} catch {
|
|
15393
16540
|
}
|
|
15394
16541
|
}
|
|
@@ -15482,18 +16629,18 @@ function parseFrontmatter(content) {
|
|
|
15482
16629
|
return { name: value("name"), version: value("version") };
|
|
15483
16630
|
}
|
|
15484
16631
|
function skillArtifacts(harness, root) {
|
|
15485
|
-
if (!
|
|
16632
|
+
if (!existsSync38(root)) return [];
|
|
15486
16633
|
const manifests = [];
|
|
15487
16634
|
const visit = (dir) => {
|
|
15488
16635
|
let entries;
|
|
15489
16636
|
try {
|
|
15490
|
-
entries =
|
|
16637
|
+
entries = readdirSync10(dir, { withFileTypes: true });
|
|
15491
16638
|
} catch {
|
|
15492
16639
|
return;
|
|
15493
16640
|
}
|
|
15494
16641
|
for (const entry of entries) {
|
|
15495
16642
|
if (entry.isSymbolicLink?.()) continue;
|
|
15496
|
-
const path =
|
|
16643
|
+
const path = join37(dir, entry.name);
|
|
15497
16644
|
if (entry.isFile() && entry.name === "SKILL.md") manifests.push(path);
|
|
15498
16645
|
else if (entry.isDirectory()) visit(path);
|
|
15499
16646
|
}
|
|
@@ -15503,7 +16650,7 @@ function skillArtifacts(harness, root) {
|
|
|
15503
16650
|
const content = readText(path);
|
|
15504
16651
|
const frontmatter = parseFrontmatter(content);
|
|
15505
16652
|
const rel = relative(root, path).replaceAll("\\", "/");
|
|
15506
|
-
const name = frontmatter.name || basename3(
|
|
16653
|
+
const name = frontmatter.name || basename3(join37(path, "..")) || "skill";
|
|
15507
16654
|
return {
|
|
15508
16655
|
harness,
|
|
15509
16656
|
type: "skill",
|
|
@@ -15518,16 +16665,16 @@ function skillArtifacts(harness, root) {
|
|
|
15518
16665
|
});
|
|
15519
16666
|
}
|
|
15520
16667
|
function cursorExtensionArtifacts(root) {
|
|
15521
|
-
if (!
|
|
16668
|
+
if (!existsSync38(root)) return [];
|
|
15522
16669
|
let dirs = [];
|
|
15523
16670
|
try {
|
|
15524
|
-
dirs =
|
|
16671
|
+
dirs = readdirSync10(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.isSymbolicLink());
|
|
15525
16672
|
} catch {
|
|
15526
16673
|
return [];
|
|
15527
16674
|
}
|
|
15528
16675
|
const artifacts = [];
|
|
15529
16676
|
for (const dir of dirs) {
|
|
15530
|
-
const pkg = readJson(
|
|
16677
|
+
const pkg = readJson(join37(root, dir.name, "package.json"));
|
|
15531
16678
|
if (!pkg) continue;
|
|
15532
16679
|
const publisher = typeof pkg.publisher === "string" ? pkg.publisher : void 0;
|
|
15533
16680
|
const name = typeof pkg.name === "string" ? pkg.name : dir.name;
|
|
@@ -15547,18 +16694,18 @@ function cursorExtensionArtifacts(root) {
|
|
|
15547
16694
|
return artifacts;
|
|
15548
16695
|
}
|
|
15549
16696
|
function deploymentMode2(home) {
|
|
15550
|
-
const raw = readText(
|
|
16697
|
+
const raw = readText(join37(home, ".synkro", "config.env"));
|
|
15551
16698
|
const value = (key) => raw.match(new RegExp(`^${key}=['"]?([^'"\\n]*)`, "m"))?.[1]?.toLowerCase();
|
|
15552
16699
|
if (value("SYNKRO_GRADING_MODE") === "byok") return "byok";
|
|
15553
16700
|
if (value("SYNKRO_STORAGE_MODE") === "cloud") return "cloud";
|
|
15554
16701
|
return "local";
|
|
15555
16702
|
}
|
|
15556
16703
|
function telemetryHealth(home) {
|
|
15557
|
-
const meta = readJson(
|
|
16704
|
+
const meta = readJson(join37(home, ".synkro", "telemetry-meta.json"));
|
|
15558
16705
|
const health = {};
|
|
15559
16706
|
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
16707
|
if (meta?.last_flush_error) health.telemetry_last_error = "flush_failed";
|
|
15561
|
-
const queue =
|
|
16708
|
+
const queue = join37(home, ".synkro", "telemetry-pending.jsonl");
|
|
15562
16709
|
try {
|
|
15563
16710
|
const size = statSync5(queue).size;
|
|
15564
16711
|
health.telemetry_backlog = size <= 5 * 1024 * 1024 ? readFileSync36(queue, "utf8").split("\n").filter(Boolean).length : Math.ceil(size / 1024);
|
|
@@ -15602,7 +16749,7 @@ function harnessSnapshot(agent) {
|
|
|
15602
16749
|
}
|
|
15603
16750
|
const config = readJson(agent.settingsPath);
|
|
15604
16751
|
const coverage = inspectCodexHooks(agent.settingsPath);
|
|
15605
|
-
const toml = readText(
|
|
16752
|
+
const toml = readText(join37(agent.configDir, "config.toml"));
|
|
15606
16753
|
const permission = toml.match(/^\s*approval_policy\s*=\s*["']([^"']+)/m)?.[1];
|
|
15607
16754
|
return {
|
|
15608
16755
|
row: {
|
|
@@ -15619,19 +16766,19 @@ function harnessSnapshot(agent) {
|
|
|
15619
16766
|
};
|
|
15620
16767
|
}
|
|
15621
16768
|
function collectOperationalInventory(options = {}) {
|
|
15622
|
-
const home = options.homeDir ??
|
|
16769
|
+
const home = options.homeDir ?? homedir37();
|
|
15623
16770
|
const detected = options.detectedAgents ?? detectAgents();
|
|
15624
16771
|
const identity = getOperationalInstallationIdentity(options.identityPath);
|
|
15625
16772
|
const targetPlatform = options.platformName ?? platform5();
|
|
15626
|
-
const codexHome = options.homeDir ?
|
|
16773
|
+
const codexHome = options.homeDir ? join37(home, ".codex") : process.env.CODEX_HOME || join37(home, ".codex");
|
|
15627
16774
|
const harnesses = [];
|
|
15628
16775
|
const artifacts = [];
|
|
15629
16776
|
for (const agent of detected) {
|
|
15630
|
-
const { row, config } = harnessSnapshot(agent);
|
|
15631
|
-
harnesses.push(
|
|
15632
|
-
artifacts.push(...hookArtifacts(
|
|
16777
|
+
const { row: row2, config } = harnessSnapshot(agent);
|
|
16778
|
+
harnesses.push(row2);
|
|
16779
|
+
artifacts.push(...hookArtifacts(row2.harness, config));
|
|
15633
16780
|
}
|
|
15634
|
-
const claudeJson = readJson(
|
|
16781
|
+
const claudeJson = readJson(join37(home, ".claude.json"));
|
|
15635
16782
|
artifacts.push(...mcpArtifactsFromJson("claude_code", claudeJson));
|
|
15636
16783
|
if (claudeJson?.projects && typeof claudeJson.projects === "object") {
|
|
15637
16784
|
for (const [projectPath, project] of Object.entries(claudeJson.projects)) {
|
|
@@ -15639,8 +16786,8 @@ function collectOperationalInventory(options = {}) {
|
|
|
15639
16786
|
artifacts.push(...mcpArtifactsFromJson("claude_code", project, `local:${sha256(projectPath).slice(0, 16)}`));
|
|
15640
16787
|
}
|
|
15641
16788
|
}
|
|
15642
|
-
artifacts.push(...mcpArtifactsFromJson("cursor", readJson(
|
|
15643
|
-
artifacts.push(...codexMcpArtifacts(readText(
|
|
16789
|
+
artifacts.push(...mcpArtifactsFromJson("cursor", readJson(join37(home, ".cursor", "mcp.json"))));
|
|
16790
|
+
artifacts.push(...codexMcpArtifacts(readText(join37(codexHome, "config.toml"))));
|
|
15644
16791
|
const projectRoots = discoveredProjectRoots(
|
|
15645
16792
|
claudeJson,
|
|
15646
16793
|
options.currentDirectory ?? process.cwd(),
|
|
@@ -15651,11 +16798,11 @@ function collectOperationalInventory(options = {}) {
|
|
|
15651
16798
|
const scopeHash = sha256(projectRoot).slice(0, 16);
|
|
15652
16799
|
artifacts.push(...mcpArtifactsFromJson(
|
|
15653
16800
|
"claude_code",
|
|
15654
|
-
readJson(
|
|
16801
|
+
readJson(join37(projectRoot, ".mcp.json")),
|
|
15655
16802
|
`project:${scopeHash}`
|
|
15656
16803
|
));
|
|
15657
|
-
const cursorProjectConfig =
|
|
15658
|
-
if (resolve5(cursorProjectConfig) !== resolve5(
|
|
16804
|
+
const cursorProjectConfig = join37(projectRoot, ".cursor", "mcp.json");
|
|
16805
|
+
if (resolve5(cursorProjectConfig) !== resolve5(join37(home, ".cursor", "mcp.json"))) {
|
|
15659
16806
|
artifacts.push(...mcpArtifactsFromJson(
|
|
15660
16807
|
"cursor",
|
|
15661
16808
|
readJson(cursorProjectConfig),
|
|
@@ -15666,7 +16813,7 @@ function collectOperationalInventory(options = {}) {
|
|
|
15666
16813
|
for (const managedPath of claudeManagedMcpConfigCandidates(targetPlatform)) {
|
|
15667
16814
|
artifacts.push(...mcpArtifactsFromJson("claude_code", readJson(managedPath), "managed"));
|
|
15668
16815
|
}
|
|
15669
|
-
const desktopConfigPath = claudeDesktopConfigCandidates(home, targetPlatform).find((path) =>
|
|
16816
|
+
const desktopConfigPath = claudeDesktopConfigCandidates(home, targetPlatform).find((path) => existsSync38(path));
|
|
15670
16817
|
if (desktopConfigPath) {
|
|
15671
16818
|
const desktopConfig = readJson(desktopConfigPath);
|
|
15672
16819
|
harnesses.push({
|
|
@@ -15677,7 +16824,7 @@ function collectOperationalInventory(options = {}) {
|
|
|
15677
16824
|
});
|
|
15678
16825
|
artifacts.push(...mcpArtifactsFromJson("claude_desktop", desktopConfig));
|
|
15679
16826
|
}
|
|
15680
|
-
const claudeSettings = readJson(
|
|
16827
|
+
const claudeSettings = readJson(join37(home, ".claude", "settings.json"));
|
|
15681
16828
|
if (claudeSettings?.enabledPlugins && typeof claudeSettings.enabledPlugins === "object") {
|
|
15682
16829
|
for (const [name, enabled] of Object.entries(claudeSettings.enabledPlugins)) {
|
|
15683
16830
|
artifacts.push({
|
|
@@ -15691,19 +16838,19 @@ function collectOperationalInventory(options = {}) {
|
|
|
15691
16838
|
});
|
|
15692
16839
|
}
|
|
15693
16840
|
}
|
|
15694
|
-
artifacts.push(...skillArtifacts("claude_code",
|
|
15695
|
-
artifacts.push(...skillArtifacts("cursor",
|
|
15696
|
-
artifacts.push(...skillArtifacts("codex",
|
|
15697
|
-
artifacts.push(...cursorExtensionArtifacts(
|
|
16841
|
+
artifacts.push(...skillArtifacts("claude_code", join37(home, ".claude", "skills")));
|
|
16842
|
+
artifacts.push(...skillArtifacts("cursor", join37(home, ".cursor", "skills")));
|
|
16843
|
+
artifacts.push(...skillArtifacts("codex", join37(codexHome, "skills")));
|
|
16844
|
+
artifacts.push(...cursorExtensionArtifacts(join37(home, ".cursor", "extensions")));
|
|
15698
16845
|
const uniqueArtifacts = /* @__PURE__ */ new Map();
|
|
15699
16846
|
for (const artifact of artifacts) {
|
|
15700
16847
|
const key = `${artifact.harness || "global"}:${artifact.type}:${artifact.canonical_id}`;
|
|
15701
16848
|
uniqueArtifacts.set(key, artifact);
|
|
15702
16849
|
}
|
|
15703
|
-
const codingHarnesses = harnesses.filter((
|
|
16850
|
+
const codingHarnesses = harnesses.filter((row2) => row2.harness === "claude_code" || row2.harness === "cursor" || row2.harness === "codex");
|
|
15704
16851
|
const health = telemetryHealth(home) ?? {};
|
|
15705
16852
|
health.scanners = {
|
|
15706
|
-
hook_runtime: codingHarnesses.length === 0 ? "unknown" : codingHarnesses.every((
|
|
16853
|
+
hook_runtime: codingHarnesses.length === 0 ? "unknown" : codingHarnesses.every((row2) => row2.enabled) ? "ok" : "degraded"
|
|
15707
16854
|
};
|
|
15708
16855
|
return {
|
|
15709
16856
|
schema_version: 1,
|
|
@@ -15749,16 +16896,16 @@ __export(sync_exports2, {
|
|
|
15749
16896
|
import { createHash as createHash6, randomUUID as randomUUID6 } from "crypto";
|
|
15750
16897
|
import { spawn as spawn8 } from "child_process";
|
|
15751
16898
|
import {
|
|
15752
|
-
existsSync as
|
|
15753
|
-
mkdirSync as
|
|
16899
|
+
existsSync as existsSync39,
|
|
16900
|
+
mkdirSync as mkdirSync21,
|
|
15754
16901
|
readFileSync as readFileSync37,
|
|
15755
16902
|
renameSync as renameSync10,
|
|
15756
|
-
writeFileSync as
|
|
16903
|
+
writeFileSync as writeFileSync26
|
|
15757
16904
|
} from "fs";
|
|
15758
|
-
import { homedir as
|
|
15759
|
-
import { dirname as dirname10, join as
|
|
16905
|
+
import { homedir as homedir38 } from "os";
|
|
16906
|
+
import { dirname as dirname10, join as join38 } from "path";
|
|
15760
16907
|
function syncStatePath() {
|
|
15761
|
-
return process.env.SYNKRO_INVENTORY_SYNC_STATE_PATH ||
|
|
16908
|
+
return process.env.SYNKRO_INVENTORY_SYNC_STATE_PATH || join38(homedir38(), ".synkro", "inventory-sync.json");
|
|
15762
16909
|
}
|
|
15763
16910
|
function readState(path = syncStatePath()) {
|
|
15764
16911
|
try {
|
|
@@ -15770,9 +16917,9 @@ function readState(path = syncStatePath()) {
|
|
|
15770
16917
|
}
|
|
15771
16918
|
function writeState(state, path = syncStatePath()) {
|
|
15772
16919
|
try {
|
|
15773
|
-
|
|
16920
|
+
mkdirSync21(dirname10(path), { recursive: true, mode: 448 });
|
|
15774
16921
|
const temp = `${path}.${process.pid}.tmp`;
|
|
15775
|
-
|
|
16922
|
+
writeFileSync26(temp, JSON.stringify(state, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
15776
16923
|
renameSync10(temp, path);
|
|
15777
16924
|
} catch {
|
|
15778
16925
|
}
|
|
@@ -15785,7 +16932,7 @@ function shouldSyncInventory(state, now = Date.now(), target) {
|
|
|
15785
16932
|
return !Number.isFinite(lastAttempt) || lastAttempt <= 0 || now - lastAttempt >= FAILURE_RETRY_MS;
|
|
15786
16933
|
}
|
|
15787
16934
|
function readConfig() {
|
|
15788
|
-
const path =
|
|
16935
|
+
const path = join38(homedir38(), ".synkro", "config.env");
|
|
15789
16936
|
const out = {};
|
|
15790
16937
|
try {
|
|
15791
16938
|
for (const rawLine of readFileSync37(path, "utf8").split("\n")) {
|
|
@@ -15827,7 +16974,7 @@ function resolveInventoryGateway(raw) {
|
|
|
15827
16974
|
}
|
|
15828
16975
|
async function loadToken() {
|
|
15829
16976
|
try {
|
|
15830
|
-
const durable = readFileSync37(
|
|
16977
|
+
const durable = readFileSync37(join38(homedir38(), ".synkro", ".mcp-jwt"), "utf8").trim();
|
|
15831
16978
|
if (durable) return durable;
|
|
15832
16979
|
} catch {
|
|
15833
16980
|
}
|
|
@@ -15945,7 +17092,7 @@ function syncOperationalInventoryDetached() {
|
|
|
15945
17092
|
writeState({ ...state, last_attempt_at: (/* @__PURE__ */ new Date()).toISOString(), last_target: target }, path);
|
|
15946
17093
|
try {
|
|
15947
17094
|
const script = process.argv[1];
|
|
15948
|
-
if (!script || !
|
|
17095
|
+
if (!script || !existsSync39(script)) return;
|
|
15949
17096
|
const child = spawn8(process.execPath, [script, "inventory-sync", "--detached"], {
|
|
15950
17097
|
detached: true,
|
|
15951
17098
|
stdio: "ignore",
|
|
@@ -15969,13 +17116,13 @@ var init_sync2 = __esm({
|
|
|
15969
17116
|
});
|
|
15970
17117
|
|
|
15971
17118
|
// cli/bootstrap.js
|
|
15972
|
-
import { readFileSync as readFileSync38, existsSync as
|
|
17119
|
+
import { readFileSync as readFileSync38, existsSync as existsSync40 } from "fs";
|
|
15973
17120
|
import { resolve as resolve6 } from "path";
|
|
15974
17121
|
var envCandidates = [
|
|
15975
17122
|
resolve6(process.env.HOME ?? "", ".synkro", "config.env")
|
|
15976
17123
|
];
|
|
15977
17124
|
for (const envPath of envCandidates) {
|
|
15978
|
-
if (!
|
|
17125
|
+
if (!existsSync40(envPath)) continue;
|
|
15979
17126
|
const envContent = readFileSync38(envPath, "utf-8");
|
|
15980
17127
|
for (const line of envContent.split("\n")) {
|
|
15981
17128
|
const trimmed = line.trim();
|
|
@@ -15993,7 +17140,7 @@ var subArgs = args.slice(1);
|
|
|
15993
17140
|
var isDetachedChild = process.env.SYNKRO_TELEMETRY_DETACHED === "1";
|
|
15994
17141
|
var FLUSH_SKIP = /* @__PURE__ */ new Set(["grade", "inventory-sync", "version", "--version", "-v", "help", "--help", "-h", ""]);
|
|
15995
17142
|
function printVersion() {
|
|
15996
|
-
console.log("1.
|
|
17143
|
+
console.log("1.10.2");
|
|
15997
17144
|
}
|
|
15998
17145
|
function printHelp2() {
|
|
15999
17146
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|
|
@@ -16012,9 +17159,16 @@ Commands:
|
|
|
16012
17159
|
claude-desktop Monitor Claude Desktop conversations (local, macOS)
|
|
16013
17160
|
telemetry <sub> Inspect or flush local telemetry events
|
|
16014
17161
|
whoami Show resolved identity + where grading runs
|
|
17162
|
+
workspace <sub> Answer the task-workspace question (stay/clear/status)
|
|
17163
|
+
ui Governed multiplexer: spaces + agents, real sessions in tabs
|
|
16015
17164
|
refresh Refresh the login session (keeps you signed in; run on a schedule)
|
|
16016
17165
|
version Show version
|
|
16017
17166
|
|
|
17167
|
+
workspace:
|
|
17168
|
+
synkro workspace stay <taskId> keep working in the current checkout
|
|
17169
|
+
synkro workspace clear <taskId> forget the choice (Synkro asks again)
|
|
17170
|
+
synkro workspace status [taskId] show recorded choices
|
|
17171
|
+
|
|
16018
17172
|
config:
|
|
16019
17173
|
synkro config show current settings
|
|
16020
17174
|
synkro config grading <local|byok> where grading runs
|
|
@@ -16118,6 +17272,16 @@ async function main() {
|
|
|
16118
17272
|
await whoamiCommand2(subArgs);
|
|
16119
17273
|
break;
|
|
16120
17274
|
}
|
|
17275
|
+
case "workspace": {
|
|
17276
|
+
const { workspaceCommand: workspaceCommand2 } = await Promise.resolve().then(() => (init_workspace(), workspace_exports));
|
|
17277
|
+
await workspaceCommand2(subArgs);
|
|
17278
|
+
break;
|
|
17279
|
+
}
|
|
17280
|
+
case "ui": {
|
|
17281
|
+
const { uiCommand: uiCommand2 } = await Promise.resolve().then(() => (init_ui(), ui_exports));
|
|
17282
|
+
await uiCommand2(subArgs);
|
|
17283
|
+
break;
|
|
17284
|
+
}
|
|
16121
17285
|
case "refresh": {
|
|
16122
17286
|
const { refreshCommand: refreshCommand2 } = await Promise.resolve().then(() => (init_refresh(), refresh_exports));
|
|
16123
17287
|
await refreshCommand2(subArgs);
|
|
@@ -16230,7 +17394,7 @@ async function shutdown(code) {
|
|
|
16230
17394
|
}, 200);
|
|
16231
17395
|
force.unref();
|
|
16232
17396
|
}
|
|
16233
|
-
main().then(() => postDispatchFlush()).then(() => shutdown(0)).catch(async (err) => {
|
|
17397
|
+
main().then(() => postDispatchFlush()).then(() => shutdown(typeof process.exitCode === "number" ? process.exitCode : 0)).catch(async (err) => {
|
|
16234
17398
|
try {
|
|
16235
17399
|
const { emit: emit2 } = await Promise.resolve().then(() => (init_telemetry(), telemetry_exports));
|
|
16236
17400
|
emit2("error", {
|