@synkro-sh/cli 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap.js +1291 -226
- 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.9.0";
|
|
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);
|
|
@@ -1169,7 +1169,7 @@ async function getStats() {
|
|
|
1169
1169
|
GROUP BY event_type
|
|
1170
1170
|
ORDER BY c DESC
|
|
1171
1171
|
`;
|
|
1172
|
-
for (const
|
|
1172
|
+
for (const row2 of types) base.by_type[String(row2.event_type)] = Number(row2.c);
|
|
1173
1173
|
const newest = await sql`
|
|
1174
1174
|
SELECT occurred_at FROM telemetry_events ORDER BY occurred_at DESC LIMIT 1
|
|
1175
1175
|
`;
|
|
@@ -1224,8 +1224,8 @@ async function exportEvents(path) {
|
|
|
1224
1224
|
const lines = [];
|
|
1225
1225
|
try {
|
|
1226
1226
|
const rows = await sql`SELECT * FROM telemetry_events ORDER BY occurred_at ASC`;
|
|
1227
|
-
for (const
|
|
1228
|
-
lines.push(JSON.stringify(
|
|
1227
|
+
for (const row2 of rows) {
|
|
1228
|
+
lines.push(JSON.stringify(row2));
|
|
1229
1229
|
}
|
|
1230
1230
|
} catch {
|
|
1231
1231
|
}
|
|
@@ -2880,8 +2880,8 @@ Print a short readiness summary: rules now active, security scanning on, the gra
|
|
|
2880
2880
|
// cli/installer/skillParser.ts
|
|
2881
2881
|
import { existsSync as existsSync15 } from "fs";
|
|
2882
2882
|
import { resolve as resolve3 } from "path";
|
|
2883
|
-
function resolveSkillPaths(skills,
|
|
2884
|
-
return skills.filter((s) => s.endsWith(".md")).map((s) => resolve3(
|
|
2883
|
+
function resolveSkillPaths(skills, repoRoot3) {
|
|
2884
|
+
return skills.filter((s) => s.endsWith(".md")).map((s) => resolve3(repoRoot3, s)).filter((p) => existsSync15(p));
|
|
2885
2885
|
}
|
|
2886
2886
|
var init_skillParser = __esm({
|
|
2887
2887
|
"cli/installer/skillParser.ts"() {
|
|
@@ -2897,7 +2897,7 @@ var STUB_COMMON_TS, STUB_EDIT_PRECHECK_TS, STUB_EDIT_FOLLOWUP_TS, STUB_CWE_PRECH
|
|
|
2897
2897
|
var init_hookScriptsTs = __esm({
|
|
2898
2898
|
"cli/installer/hookScriptsTs.ts"() {
|
|
2899
2899
|
"use strict";
|
|
2900
|
-
STUB_COMMON_TS = String.raw`import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, statSync, realpathSync, openSync, readSync, closeSync } from 'node:fs';
|
|
2900
|
+
STUB_COMMON_TS = String.raw`import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, statSync, realpathSync, openSync, readSync, closeSync, unlinkSync } from 'node:fs';
|
|
2901
2901
|
import { execFileSync, execSync } from 'node:child_process';
|
|
2902
2902
|
import { homedir } from 'node:os';
|
|
2903
2903
|
import { basename, dirname, join, resolve, relative, isAbsolute } from 'node:path';
|
|
@@ -3136,6 +3136,28 @@ function gitRoot(cwd: string): string {
|
|
|
3136
3136
|
} catch { return ''; }
|
|
3137
3137
|
}
|
|
3138
3138
|
|
|
3139
|
+
// synkro.toml is per-machine and untracked, so a LINKED git worktree never has
|
|
3140
|
+
// its own copy — its root is a fresh checkout. Resolve the main checkout through
|
|
3141
|
+
// the worktree's .git pointer file (a linked worktree's .git is a FILE reading
|
|
3142
|
+
// "gitdir: <main>/.git/worktrees/<name>") so worktrees inherit the repo's
|
|
3143
|
+
// config instead of silently skipping all grading. Opt-in semantics survive: a
|
|
3144
|
+
// repo whose MAIN root has no synkro.toml stays dormant. Pure fs — no
|
|
3145
|
+
// subprocess on the hook hot path.
|
|
3146
|
+
function mainWorktreeRoot(root: string): string {
|
|
3147
|
+
try {
|
|
3148
|
+
const dotGit = join(root, '.git');
|
|
3149
|
+
if (statSync(dotGit).isDirectory()) return root;
|
|
3150
|
+
const pointer = readFileSync(dotGit, 'utf-8');
|
|
3151
|
+
const match = pointer.match(/^gitdir:\s*(.+?)\s*$/m);
|
|
3152
|
+
if (!match) return root;
|
|
3153
|
+
const gitDir = isAbsolute(match[1]) ? match[1] : join(root, match[1]);
|
|
3154
|
+
const marker = join('.git', 'worktrees') + '/';
|
|
3155
|
+
const at = gitDir.lastIndexOf('/' + marker);
|
|
3156
|
+
if (at === -1) return root;
|
|
3157
|
+
return gitDir.slice(0, at) || root;
|
|
3158
|
+
} catch { return root; }
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3139
3161
|
function taskRepoContext(root: string): any {
|
|
3140
3162
|
if (!root) return undefined;
|
|
3141
3163
|
try {
|
|
@@ -3182,16 +3204,144 @@ function taskRepoContext(root: string): any {
|
|
|
3182
3204
|
} catch { return undefined; }
|
|
3183
3205
|
}
|
|
3184
3206
|
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3207
|
+
// One task-workspace message per TOOL CALL, not per hook. A single Bash call fans
|
|
3208
|
+
// out to install-scan + bash-judge + skill-judge (an Edit fans out to four), and
|
|
3209
|
+
// every hook is its own process, so the dedupe has to live on disk rather than in
|
|
3210
|
+
// a Map the way the server-side status-line dedupe does (see stableHookEventIdentity
|
|
3211
|
+
// in scanning/scanRouter). Atomic first-writer-wins via an exclusive create: the
|
|
3212
|
+
// hook that wins says it in full, the rest stay terse.
|
|
3213
|
+
const TOOL_CALL_MARK_DIR = join(HOME, '.synkro', 'tool-call-marks');
|
|
3214
|
+
const TOOL_CALL_MARK_WINDOW_MS = 15000;
|
|
3215
|
+
|
|
3216
|
+
function pruneToolCallMarks(now: number): void {
|
|
3217
|
+
try {
|
|
3218
|
+
const entries = readdirSync(TOOL_CALL_MARK_DIR);
|
|
3219
|
+
if (entries.length < 200) return;
|
|
3220
|
+
for (const name of entries) {
|
|
3221
|
+
const path = join(TOOL_CALL_MARK_DIR, name);
|
|
3222
|
+
try {
|
|
3223
|
+
if (now - statSync(path).mtimeMs > TOOL_CALL_MARK_WINDOW_MS) unlinkSync(path);
|
|
3224
|
+
} catch {}
|
|
3225
|
+
}
|
|
3226
|
+
} catch {}
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
// The user's answer to "move into the task worktree, or stay here?". Written by the
|
|
3230
|
+
// "synkro workspace stay" command (the CLI owns ~/.synkro, so nothing hand-writes it)
|
|
3231
|
+
// and read here. Keyed by task alone: the decision resets when the active task
|
|
3232
|
+
// changes, which is the scope the workspace gate is asking about.
|
|
3233
|
+
const WORKSPACE_CHOICE_DIR = join(HOME, '.synkro', 'workspace-choice');
|
|
3234
|
+
|
|
3235
|
+
// The user answers the workspace question in plain language on their next
|
|
3236
|
+
// prompt. Writing the marker is exactly what the CLI's "workspace stay"
|
|
3237
|
+
// command does — but the consent matcher only accepts one literal spelling,
|
|
3238
|
+
// and on a machine where PATH shadows the binary (observed: a venv python
|
|
3239
|
+
// named synkro, and an older global without the command) that spelling cannot
|
|
3240
|
+
// succeed, which deadlocked the ask. Patterns are DIRECTIVES, never
|
|
3241
|
+
// questions: a trailing question mark rejects, and the bare verb matches only
|
|
3242
|
+
// as a short standalone reply. Callers gate on a PENDING ask, so ordinary
|
|
3243
|
+
// conversation containing "stay" is never scanned against this.
|
|
3244
|
+
function isWorkspaceStayIntent(prompt: string): boolean {
|
|
3245
|
+
const normalized = String(prompt || '').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
3246
|
+
if (!normalized || normalized.length > 240) return false;
|
|
3247
|
+
if (/\?\s*$/.test(normalized)) return false;
|
|
3248
|
+
if (/^(?:yes[,.\s]+)?(?:please\s+)?stay(?:\s+(?:here|put))?(?:\s*(?:pls|please))?[.!]?$/.test(normalized)) return true;
|
|
3249
|
+
return [
|
|
3250
|
+
/\b(?:stay|remain|keep working|keep going|continue)\b[^?]{0,60}\b(?:current|same|this)\s+(?:worktree|workspace|checkout|directory)\b/,
|
|
3251
|
+
/\b(?:do not|don't|dont|no need to)\s+(?:move|switch|change)\b[^?]{0,40}\b(?:worktrees?|workspaces?|checkouts?)\b/,
|
|
3252
|
+
].some((pattern) => pattern.test(normalized));
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
function taskWorkspaceStayRecorded(taskId: string): boolean {
|
|
3256
|
+
if (!/^task_[a-z0-9]{8}$/i.test(String(taskId || ''))) return false;
|
|
3257
|
+
try { return existsSync(join(WORKSPACE_CHOICE_DIR, taskId + '.stay')); } catch { return false; }
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
// Our own context string, so the shape is stable: '... task=<id> ...'.
|
|
3261
|
+
function taskIdFromScmContext(context: string): string {
|
|
3262
|
+
const marker = ' task=';
|
|
3263
|
+
const at = String(context || '').indexOf(marker);
|
|
3264
|
+
if (at === -1) return '';
|
|
3265
|
+
const rest = context.slice(at + marker.length);
|
|
3266
|
+
const end = rest.indexOf(' ');
|
|
3267
|
+
return (end === -1 ? rest : rest.slice(0, end)).trim();
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
// Consent must never deadlock behind the block it resolves: the command that records
|
|
3271
|
+
// the answer is allowed through for the task currently being asked about, and nothing
|
|
3272
|
+
// else is.
|
|
3273
|
+
function isWorkspaceConsentCommand(payload: any, taskId: string): boolean {
|
|
3274
|
+
if (!taskId || String(payload?.tool_name || '') !== 'Bash') return false;
|
|
3275
|
+
const input = payload?.tool_input && typeof payload.tool_input === 'object' ? payload.tool_input : {};
|
|
3276
|
+
const command = String(input.command || input.cmd || '').trim();
|
|
3277
|
+
return command === 'synkro workspace stay ' + taskId;
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
function firstHookForToolCall(sessionId: string, payload: any): boolean {
|
|
3281
|
+
const p = payload && typeof payload === 'object' ? payload : {};
|
|
3282
|
+
const id = String(p.tool_use_id || p.tool_call_id || p.call_id || p.event_id || '').trim();
|
|
3283
|
+
// Fallback when the harness omits a call id: tool name + input is identical across
|
|
3284
|
+
// the surfaces of one call and distinct across different calls. Identical
|
|
3285
|
+
// back-to-back commands can collide inside the window, which costs one repeated
|
|
3286
|
+
// line and nothing else.
|
|
3287
|
+
const material = id || (String(p.tool_name || '') + ':' + (p.tool_input ? JSON.stringify(p.tool_input) : ''));
|
|
3288
|
+
if (!sessionId || !material) return true;
|
|
3289
|
+
const key = createHash('sha256').update(sessionId + '\0' + material).digest('hex').slice(0, 24);
|
|
3290
|
+
const marker = join(TOOL_CALL_MARK_DIR, key);
|
|
3291
|
+
const now = Date.now();
|
|
3292
|
+
try { mkdirSync(TOOL_CALL_MARK_DIR, { recursive: true }); } catch {}
|
|
3293
|
+
pruneToolCallMarks(now);
|
|
3294
|
+
try {
|
|
3295
|
+
writeFileSync(marker, '', { flag: 'wx', mode: 0o600 });
|
|
3296
|
+
return true;
|
|
3297
|
+
} catch (err: any) {
|
|
3298
|
+
if (err && err.code === 'EEXIST') {
|
|
3299
|
+
// Outside the burst window this is a genuinely new call reusing the fallback
|
|
3300
|
+
// key, so re-arm the marker and let it speak.
|
|
3301
|
+
try {
|
|
3302
|
+
if (now - statSync(marker).mtimeMs > TOOL_CALL_MARK_WINDOW_MS) {
|
|
3303
|
+
writeFileSync(marker, '', { mode: 0o600 });
|
|
3304
|
+
return true;
|
|
3305
|
+
}
|
|
3306
|
+
} catch {}
|
|
3307
|
+
return false;
|
|
3308
|
+
}
|
|
3309
|
+
// Any other failure (read-only home, quota) must not silence the message.
|
|
3310
|
+
return true;
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
function taskScmBlockResponse(harness: string, reason: string, verbose = true): string {
|
|
3315
|
+
// Every fanned-out hook still denies — suppressing the decision itself would let the
|
|
3316
|
+
// tool through if the winning hook's response were ever dropped. Only the TEXT
|
|
3317
|
+
// collapses: the later surfaces of one tool call deny with no systemMessage and no
|
|
3318
|
+
// additionalContext, so the transcript carries one workspace message, not three.
|
|
3319
|
+
const tag = synkroOriginTag('synkro:scm', harness);
|
|
3320
|
+
const message = tag + ' ' + reason;
|
|
3321
|
+
if (harness === 'cursor') {
|
|
3322
|
+
return verbose
|
|
3323
|
+
? JSON.stringify({ permission: 'deny', user_message: message, agent_message: message })
|
|
3324
|
+
: JSON.stringify({ permission: 'deny', user_message: '', agent_message: '' });
|
|
3325
|
+
}
|
|
3326
|
+
if (!verbose) {
|
|
3327
|
+
return JSON.stringify({
|
|
3328
|
+
systemMessage: '',
|
|
3329
|
+
hookSpecificOutput: {
|
|
3330
|
+
hookEventName: 'PreToolUse',
|
|
3331
|
+
permissionDecision: 'deny',
|
|
3332
|
+
// Only surfaces if this hook's denial is the one the harness reports.
|
|
3333
|
+
permissionDecisionReason: tag + ' blocked — see the workspace message above',
|
|
3334
|
+
additionalContext: '',
|
|
3335
|
+
},
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
3188
3338
|
return JSON.stringify({
|
|
3189
3339
|
systemMessage: message,
|
|
3190
3340
|
hookSpecificOutput: {
|
|
3191
3341
|
hookEventName: 'PreToolUse',
|
|
3192
3342
|
permissionDecision: 'deny',
|
|
3193
3343
|
permissionDecisionReason: message,
|
|
3194
|
-
additionalContext: message
|
|
3344
|
+
additionalContext: message,
|
|
3195
3345
|
},
|
|
3196
3346
|
});
|
|
3197
3347
|
}
|
|
@@ -3228,8 +3378,36 @@ function sharedRepoRoot(root: string): string {
|
|
|
3228
3378
|
return dirname(common);
|
|
3229
3379
|
}
|
|
3230
3380
|
|
|
3231
|
-
|
|
3232
|
-
|
|
3381
|
+
// Claude Code only permits switching BETWEEN worktrees when the target lives under
|
|
3382
|
+
// <repo>/.claude/worktrees, so a cc session that activates a second task can never
|
|
3383
|
+
// reach another .synkro-worktrees path. Provisioning cc task worktrees under
|
|
3384
|
+
// .claude/worktrees keeps mid-session task switching working; every other harness
|
|
3385
|
+
// keeps the original location.
|
|
3386
|
+
const CC_MANAGED_WORKTREE_DIR = join('.claude', 'worktrees');
|
|
3387
|
+
const LEGACY_MANAGED_WORKTREE_DIR = '.synkro-worktrees';
|
|
3388
|
+
|
|
3389
|
+
function managedWorktreeDir(harness: string): string {
|
|
3390
|
+
return harness === 'cc' ? CC_MANAGED_WORKTREE_DIR : LEGACY_MANAGED_WORKTREE_DIR;
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
function taskWorktreePath(root: string, taskId: string, harness: string): string {
|
|
3394
|
+
return join(sharedRepoRoot(root), managedWorktreeDir(harness), taskId);
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
function taskWorktreeCandidatePaths(root: string, taskId: string): string[] {
|
|
3398
|
+
const shared = sharedRepoRoot(root);
|
|
3399
|
+
return [CC_MANAGED_WORKTREE_DIR, LEGACY_MANAGED_WORKTREE_DIR]
|
|
3400
|
+
.map((dir) => join(shared, dir, taskId));
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
// A task bound before the cc relocation keeps the worktree it already owns: the
|
|
3404
|
+
// canonical branch is checked out there, so provisioning a second one would fail
|
|
3405
|
+
// on 'the canonical task branch already exists in another workspace'.
|
|
3406
|
+
function resolveTaskWorktreePath(root: string, taskId: string, harness: string): string {
|
|
3407
|
+
const records = taskWorktreeRecords(root);
|
|
3408
|
+
const registered = taskWorktreeCandidatePaths(root, taskId)
|
|
3409
|
+
.find((candidate) => records.some((item) => samePath(item.path, candidate)));
|
|
3410
|
+
return registered || taskWorktreePath(root, taskId, harness);
|
|
3233
3411
|
}
|
|
3234
3412
|
|
|
3235
3413
|
function codexDesktopOrigin(): boolean {
|
|
@@ -3238,10 +3416,13 @@ function codexDesktopOrigin(): boolean {
|
|
|
3238
3416
|
return origin === 'codex desktop' || bundle === 'com.openai.codex';
|
|
3239
3417
|
}
|
|
3240
3418
|
|
|
3241
|
-
function ignoreManagedWorktreeDirectory(root: string): void {
|
|
3242
|
-
const
|
|
3419
|
+
function ignoreManagedWorktreeDirectory(root: string, worktreePath: string): void {
|
|
3420
|
+
const shared = sharedRepoRoot(root);
|
|
3421
|
+
const common = join(shared, '.git');
|
|
3243
3422
|
const excludePath = join(common, 'info', 'exclude');
|
|
3244
|
-
|
|
3423
|
+
// Ignore the directory the worktree actually landed in, which differs per harness.
|
|
3424
|
+
const entry = relative(shared, dirname(worktreePath)) + '/';
|
|
3425
|
+
if (entry.startsWith('..')) return;
|
|
3245
3426
|
let existing = '';
|
|
3246
3427
|
try { existing = readFileSync(excludePath, 'utf-8'); } catch {}
|
|
3247
3428
|
if (existing.split(/\r?\n/).includes(entry)) return;
|
|
@@ -3279,9 +3460,11 @@ function gitOperationInProgress(root: string): boolean {
|
|
|
3279
3460
|
}
|
|
3280
3461
|
|
|
3281
3462
|
function validateTaskWorktree(root: string, currentRoot: string, op: any): string {
|
|
3282
|
-
|
|
3463
|
+
// Accept either managed location: a task bound before the cc relocation still
|
|
3464
|
+
// pushes from its legacy .synkro-worktrees path.
|
|
3465
|
+
const expected = taskWorktreeCandidatePaths(root, String(op.taskId || ''));
|
|
3283
3466
|
const supplied = String(op.worktreePath || '');
|
|
3284
|
-
const managed = Boolean(supplied) && samePath(supplied,
|
|
3467
|
+
const managed = Boolean(supplied) && expected.some((candidate) => samePath(supplied, candidate));
|
|
3285
3468
|
const nativeCurrent = Boolean(supplied) && samePath(supplied, currentRoot);
|
|
3286
3469
|
if (!managed && !nativeCurrent) {
|
|
3287
3470
|
throw new Error('task worktree path does not match the managed or current native workspace');
|
|
@@ -3307,7 +3490,7 @@ async function completeTaskScm(sessionId: string, op: any, ok: boolean, error: s
|
|
|
3307
3490
|
} catch {}
|
|
3308
3491
|
}
|
|
3309
3492
|
|
|
3310
|
-
function executeTaskScm(root: string, op: any): string {
|
|
3493
|
+
function executeTaskScm(root: string, op: any, harness: string): string {
|
|
3311
3494
|
if (!op || typeof op !== 'object') throw new Error('invalid SCM operation');
|
|
3312
3495
|
if (!/^task_[a-z0-9]{8}$/i.test(String(op.taskId || ''))) throw new Error('invalid task id');
|
|
3313
3496
|
const branch = String(op.branchName || '');
|
|
@@ -3372,7 +3555,7 @@ function executeTaskScm(root: string, op: any): string {
|
|
|
3372
3555
|
}
|
|
3373
3556
|
return root;
|
|
3374
3557
|
}
|
|
3375
|
-
const worktreePath =
|
|
3558
|
+
const worktreePath = resolveTaskWorktreePath(boundRoot, String(op.taskId), harness);
|
|
3376
3559
|
const existing = taskWorktreeRecords(boundRoot).find((item) => samePath(item.path, worktreePath));
|
|
3377
3560
|
if (existing) {
|
|
3378
3561
|
if (existing.branch !== branch || gitOutput(worktreePath, ['rev-parse', '--abbrev-ref', 'HEAD']) !== branch) {
|
|
@@ -3398,7 +3581,7 @@ function executeTaskScm(root: string, op: any): string {
|
|
|
3398
3581
|
throw new Error('task base revision is unavailable in this repository');
|
|
3399
3582
|
}
|
|
3400
3583
|
const originalBranch = gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
3401
|
-
ignoreManagedWorktreeDirectory(boundRoot);
|
|
3584
|
+
ignoreManagedWorktreeDirectory(boundRoot, worktreePath);
|
|
3402
3585
|
mkdirSync(dirname(worktreePath), { recursive: true });
|
|
3403
3586
|
gitOutput(root, ['worktree', 'add', '-b', branch, worktreePath, String(op.baseSha)], 30000);
|
|
3404
3587
|
if (gitOutput(root, ['rev-parse', 'HEAD']) !== head
|
|
@@ -3430,14 +3613,23 @@ function executeTaskScm(root: string, op: any): string {
|
|
|
3430
3613
|
|
|
3431
3614
|
interface TaskScmReconcileResult { reason: string; context: string }
|
|
3432
3615
|
|
|
3433
|
-
|
|
3616
|
+
// Who acted and where it ran: a message in a shared terminal should say which
|
|
3617
|
+
// harness produced it and whether this install is grading locally or in the cloud.
|
|
3618
|
+
function synkroOriginTag(prefix: string, harness: string): string {
|
|
3619
|
+
return '[' + prefix
|
|
3620
|
+
+ (harness ? ':' + harness : '')
|
|
3621
|
+
+ ':' + (deployIsCloud() ? 'cloud' : 'local')
|
|
3622
|
+
+ ']';
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
function taskScmWorkspaceContext(workspace: any, harness = ''): string {
|
|
3434
3626
|
if (!workspace || typeof workspace !== 'object') return '';
|
|
3435
3627
|
const taskId = String(workspace.taskId || '');
|
|
3436
3628
|
const linearRef = String(workspace.linearRef || '');
|
|
3437
3629
|
const branchName = String(workspace.branchName || '');
|
|
3438
3630
|
const worktreePath = String(workspace.worktreePath || '');
|
|
3439
3631
|
if (!taskId || !branchName) return '';
|
|
3440
|
-
return '
|
|
3632
|
+
return synkroOriginTag('synkro:task-workspace', harness) + ' task=' + taskId
|
|
3441
3633
|
+ (linearRef ? ' linear=' + linearRef : '')
|
|
3442
3634
|
+ ' branch=' + branchName
|
|
3443
3635
|
+ (worktreePath ? ' worktree=' + worktreePath : ' worktree=pending-native-handoff');
|
|
@@ -3448,7 +3640,7 @@ function shellTaskWorkspaceArg(value: string): string {
|
|
|
3448
3640
|
}
|
|
3449
3641
|
|
|
3450
3642
|
function taskScmWorkspaceInstruction(harness: string, sessionId: string, workspace: any): string {
|
|
3451
|
-
const context = taskScmWorkspaceContext(workspace);
|
|
3643
|
+
const context = taskScmWorkspaceContext(workspace, harness);
|
|
3452
3644
|
const worktreePath = String(workspace?.worktreePath || '');
|
|
3453
3645
|
const branchName = String(workspace?.branchName || '');
|
|
3454
3646
|
if (!context || !branchName) return '';
|
|
@@ -3456,9 +3648,14 @@ function taskScmWorkspaceInstruction(harness: string, sessionId: string, workspa
|
|
|
3456
3648
|
+ 'or ask the user to perform the transition. Synkro keeps substantive tools blocked until this exact session reports '
|
|
3457
3649
|
+ 'the task worktree and canonical branch.';
|
|
3458
3650
|
if (harness === 'cc') {
|
|
3459
|
-
if (!worktreePath) return context + '\
|
|
3460
|
-
|
|
3461
|
-
|
|
3651
|
+
if (!worktreePath) return context + '\nTask worktree creation failed closed; retry workspace preparation.';
|
|
3652
|
+
// Ask, do not command. The user may legitimately want to keep working where they
|
|
3653
|
+
// are, and Synkro should not move them without their say-so.
|
|
3654
|
+
return context + '\nAsk the user whether to move this task into its isolated worktree '
|
|
3655
|
+
+ JSON.stringify(worktreePath) + ' on branch ' + JSON.stringify(branchName)
|
|
3656
|
+
+ ', or keep working in the current workspace. Do not decide for them. '
|
|
3657
|
+
+ 'To move: invoke EnterWorktree on that path. '
|
|
3658
|
+
+ 'To stay: run "synkro workspace stay ' + String(workspace?.taskId || '') + '".';
|
|
3462
3659
|
}
|
|
3463
3660
|
if (harness === 'cursor') {
|
|
3464
3661
|
if (!worktreePath) return context + '\n[synkro:workspace-handoff] Task worktree creation failed closed; retry workspace preparation.';
|
|
@@ -3539,11 +3736,11 @@ async function reconcileTaskScm(
|
|
|
3539
3736
|
? (taskScmWorkspaceInstruction(harness, sessionId, workspace)
|
|
3540
3737
|
|| String(result.reason || 'task source-control preparation is pending'))
|
|
3541
3738
|
: '',
|
|
3542
|
-
context: taskScmWorkspaceContext(workspace),
|
|
3739
|
+
context: taskScmWorkspaceContext(workspace, harness),
|
|
3543
3740
|
};
|
|
3544
3741
|
}
|
|
3545
3742
|
try {
|
|
3546
|
-
const worktreePath = executeTaskScm(root, result.op);
|
|
3743
|
+
const worktreePath = executeTaskScm(root, result.op, harness);
|
|
3547
3744
|
await completeTaskScm(sessionId, result.op, true, '', worktreePath);
|
|
3548
3745
|
if (result.op.kind === 'branch') {
|
|
3549
3746
|
if (result.op.adoptExistingWorktree === true) {
|
|
@@ -3551,12 +3748,12 @@ async function reconcileTaskScm(
|
|
|
3551
3748
|
if (rebound.ok) {
|
|
3552
3749
|
const reboundResult = await rebound.json() as any;
|
|
3553
3750
|
if (!reboundResult?.waiting && reboundResult?.workspace) {
|
|
3554
|
-
return { reason: '', context: taskScmWorkspaceContext(reboundResult.workspace) };
|
|
3751
|
+
return { reason: '', context: taskScmWorkspaceContext(reboundResult.workspace, harness) };
|
|
3555
3752
|
}
|
|
3556
3753
|
if (reboundResult?.waiting) {
|
|
3557
3754
|
return {
|
|
3558
3755
|
reason: String(reboundResult.reason || 'Codex native worktree binding is pending'),
|
|
3559
|
-
context: taskScmWorkspaceContext(reboundResult.workspace),
|
|
3756
|
+
context: taskScmWorkspaceContext(reboundResult.workspace, harness),
|
|
3560
3757
|
};
|
|
3561
3758
|
}
|
|
3562
3759
|
}
|
|
@@ -4188,10 +4385,19 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
|
|
|
4188
4385
|
// Dormancy: a repo is onboarded only if it has a synkro.toml FILE at its git
|
|
4189
4386
|
// root. Guard root !== HOME — ~/.synkro is the config DIRECTORY, so without
|
|
4190
4387
|
// this a home-rooted cwd (dotfiles in git, non-git dir under home) could
|
|
4191
|
-
// look onboarded.
|
|
4388
|
+
// look onboarded. A linked worktree checked out at a commit predating the
|
|
4389
|
+
// tracked synkro.toml has no copy of its own — fall back to the main
|
|
4390
|
+
// checkout's config rather than silently skipping enforcement there.
|
|
4192
4391
|
let synkroFileText = '';
|
|
4193
|
-
if (root && root !== HOME
|
|
4194
|
-
|
|
4392
|
+
if (root && root !== HOME) {
|
|
4393
|
+
let configRoot = root;
|
|
4394
|
+
if (!existsSync(join(configRoot, 'synkro.toml'))) {
|
|
4395
|
+
const mainRoot = mainWorktreeRoot(root);
|
|
4396
|
+
if (mainRoot !== root && mainRoot !== HOME && existsSync(join(mainRoot, 'synkro.toml'))) {
|
|
4397
|
+
configRoot = mainRoot;
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
try { synkroFileText = readFileSync(join(configRoot, 'synkro.toml'), 'utf-8'); } catch {}
|
|
4195
4401
|
}
|
|
4196
4402
|
if (!synkroFileText) {
|
|
4197
4403
|
// Repo not onboarded — emit a minimal tool_call so usage analytics still
|
|
@@ -4220,8 +4426,26 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
|
|
|
4220
4426
|
|
|
4221
4427
|
const scm = await reconcileTaskScm(root || cwd, sessionId, harness, payload);
|
|
4222
4428
|
const substantiveTool = /^(?:Bash|Edit|Write|MultiEdit|NotebookEdit|apply_patch|file_change)$/i.test(String(payload.tool_name || ''));
|
|
4223
|
-
|
|
4224
|
-
|
|
4429
|
+
const scmTaskId = taskIdFromScmContext(scm.context);
|
|
4430
|
+
// The user can settle the workspace question in plain language: while the
|
|
4431
|
+
// ask is pending for this session's task, a stay-directive on the user's
|
|
4432
|
+
// prompt records the same durable marker the CLI command writes. Reconcile
|
|
4433
|
+
// just told us WHICH task is being asked, so no extra state is needed and
|
|
4434
|
+
// ordinary prompts outside a pending ask are never scanned.
|
|
4435
|
+
if (surface === 'prompt-submit' && scm.reason && scmTaskId && !taskWorkspaceStayRecorded(scmTaskId)) {
|
|
4436
|
+
const promptText = String(payload.prompt || payload.user_message || '');
|
|
4437
|
+
if (isWorkspaceStayIntent(promptText)) {
|
|
4438
|
+
try {
|
|
4439
|
+
mkdirSync(WORKSPACE_CHOICE_DIR, { recursive: true });
|
|
4440
|
+
writeFileSync(join(WORKSPACE_CHOICE_DIR, scmTaskId + '.stay'), new Date().toISOString() + '\n');
|
|
4441
|
+
} catch { /* fail-open: the CLI command and the exact-string path remain */ }
|
|
4442
|
+
}
|
|
4443
|
+
}
|
|
4444
|
+
// The user was asked and chose to keep working here, or is answering right now.
|
|
4445
|
+
const workspaceConsentSettled = Boolean(scmTaskId)
|
|
4446
|
+
&& (taskWorkspaceStayRecorded(scmTaskId) || isWorkspaceConsentCommand(payload, scmTaskId));
|
|
4447
|
+
if (scm.reason && substantiveTool && !workspaceConsentSettled) {
|
|
4448
|
+
out(taskScmBlockResponse(harness, scm.reason, firstHookForToolCall(sessionId, payload)));
|
|
4225
4449
|
return;
|
|
4226
4450
|
}
|
|
4227
4451
|
|
|
@@ -4382,7 +4606,15 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
|
|
|
4382
4606
|
const contractResponseText = opts.stopContract && harness === 'codex'
|
|
4383
4607
|
? codexStopResponse(rawResponseText)
|
|
4384
4608
|
: rawResponseText;
|
|
4385
|
-
|
|
4609
|
+
// Only the first hook of this tool call carries the workspace context; the other
|
|
4610
|
+
// surfaces of the same call would otherwise repeat it verbatim 2-4 times.
|
|
4611
|
+
let scmContext = scm.context && firstHookForToolCall(sessionId, payload) ? scm.context : '';
|
|
4612
|
+
// Say plainly that work is continuing outside the task worktree by the user's choice,
|
|
4613
|
+
// so the state is never mistaken for a binding that silently failed.
|
|
4614
|
+
if (scmContext && scmTaskId && taskWorkspaceStayRecorded(scmTaskId)) {
|
|
4615
|
+
scmContext += ' workspace=staying-here-by-user-choice';
|
|
4616
|
+
}
|
|
4617
|
+
const responseText = withTaskScmContext(contractResponseText, harness, scmContext);
|
|
4386
4618
|
out(responseText);
|
|
4387
4619
|
emitStubTelemetry(surface, harness, telemPayload, responseText, Date.now() - startedAt, telemCwd, telemSessionId);
|
|
4388
4620
|
} catch (err) {
|
|
@@ -7281,8 +7513,8 @@ async function dockerInstall(opts = {}) {
|
|
|
7281
7513
|
"SYNKRO_TELEMETRY_QUEUE=/data/synkro-host/telemetry-pending.jsonl",
|
|
7282
7514
|
image
|
|
7283
7515
|
];
|
|
7284
|
-
const
|
|
7285
|
-
if (
|
|
7516
|
+
const run2 = spawnSync3("docker", args2, { encoding: "utf-8", stdio: "inherit", timeout: 6e4 });
|
|
7517
|
+
if (run2.status !== 0) {
|
|
7286
7518
|
throw new DockerInstallError(`docker run failed (image ${image})`);
|
|
7287
7519
|
}
|
|
7288
7520
|
return {
|
|
@@ -7513,7 +7745,7 @@ var init_dockerInstall = __esm({
|
|
|
7513
7745
|
HOST_PGLITE_PORT = parseInt(process.env.SYNKRO_HOST_PGLITE_PORT || "15433", 10);
|
|
7514
7746
|
CONTAINER_NAME = resolveContainerName();
|
|
7515
7747
|
defaultImageVersion = () => {
|
|
7516
|
-
if (true) return "1.
|
|
7748
|
+
if (true) return "1.9.0";
|
|
7517
7749
|
try {
|
|
7518
7750
|
const pkg = JSON.parse(readFileSync17(new URL("../../package.json", import.meta.url), "utf8"));
|
|
7519
7751
|
if (pkg.version) return pkg.version;
|
|
@@ -8221,7 +8453,7 @@ function isoDay(value, fallbackDay) {
|
|
|
8221
8453
|
function parseClaudeTranscriptUsage(transcript, fallbackDay = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), options = {}) {
|
|
8222
8454
|
const seen = options.seenStableIds ?? /* @__PURE__ */ new Set();
|
|
8223
8455
|
const rollups = /* @__PURE__ */ new Map();
|
|
8224
|
-
const
|
|
8456
|
+
const usage2 = {
|
|
8225
8457
|
input_tokens: 0,
|
|
8226
8458
|
output_tokens: 0,
|
|
8227
8459
|
cache_creation_input_tokens: 0,
|
|
@@ -8255,7 +8487,7 @@ function parseClaudeTranscriptUsage(transcript, fallbackDay = (/* @__PURE__ */ n
|
|
|
8255
8487
|
if (entryModel !== "<synthetic>") model = entryModel;
|
|
8256
8488
|
const day = isoDay(entry.timestamp, fallbackDay);
|
|
8257
8489
|
const key = `${day}\0${entryModel}`;
|
|
8258
|
-
const
|
|
8490
|
+
const row2 = rollups.get(key) ?? {
|
|
8259
8491
|
day,
|
|
8260
8492
|
model: entryModel,
|
|
8261
8493
|
turns: 0,
|
|
@@ -8264,23 +8496,23 @@ function parseClaudeTranscriptUsage(transcript, fallbackDay = (/* @__PURE__ */ n
|
|
|
8264
8496
|
cache_creation_input_tokens: 0,
|
|
8265
8497
|
cache_read_input_tokens: 0
|
|
8266
8498
|
};
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
rollups.set(key,
|
|
8499
|
+
row2.turns += 1;
|
|
8500
|
+
row2.input_tokens += counts.input_tokens;
|
|
8501
|
+
row2.output_tokens += counts.output_tokens;
|
|
8502
|
+
row2.cache_creation_input_tokens += counts.cache_creation_input_tokens;
|
|
8503
|
+
row2.cache_read_input_tokens += counts.cache_read_input_tokens;
|
|
8504
|
+
rollups.set(key, row2);
|
|
8273
8505
|
turns += 1;
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8506
|
+
usage2.input_tokens += counts.input_tokens;
|
|
8507
|
+
usage2.output_tokens += counts.output_tokens;
|
|
8508
|
+
usage2.cache_creation_input_tokens += counts.cache_creation_input_tokens;
|
|
8509
|
+
usage2.cache_read_input_tokens += counts.cache_read_input_tokens;
|
|
8278
8510
|
} catch {
|
|
8279
8511
|
}
|
|
8280
8512
|
}
|
|
8281
8513
|
if (turns === 0) return null;
|
|
8282
8514
|
return {
|
|
8283
|
-
usage,
|
|
8515
|
+
usage: usage2,
|
|
8284
8516
|
model: model || "unknown",
|
|
8285
8517
|
rollups: [...rollups.values()].sort(
|
|
8286
8518
|
(a, b) => a.day.localeCompare(b.day) || a.model.localeCompare(b.model)
|
|
@@ -8570,7 +8802,7 @@ function writeConfigEnv(opts) {
|
|
|
8570
8802
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle2(credsPath)}`,
|
|
8571
8803
|
`SYNKRO_TIER=${shellQuoteSingle2(safeTier)}`,
|
|
8572
8804
|
`SYNKRO_INFERENCE=${shellQuoteSingle2(safeInference)}`,
|
|
8573
|
-
`SYNKRO_VERSION=${shellQuoteSingle2("1.
|
|
8805
|
+
`SYNKRO_VERSION=${shellQuoteSingle2("1.9.0")}`
|
|
8574
8806
|
];
|
|
8575
8807
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle2(safeSynkroBin)}`);
|
|
8576
8808
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle2(safeUserId)}`);
|
|
@@ -9317,7 +9549,7 @@ async function installCommand(opts = {}) {
|
|
|
9317
9549
|
await setTelemetryState({ enabled: true, remoteFlushEnabled: telemetryConsent });
|
|
9318
9550
|
emit("install", {
|
|
9319
9551
|
phase: "started",
|
|
9320
|
-
cli_version_to: "1.
|
|
9552
|
+
cli_version_to: "1.9.0",
|
|
9321
9553
|
agents_detected: agents.map((a) => a.kind),
|
|
9322
9554
|
with_github: false,
|
|
9323
9555
|
with_local_cc: false,
|
|
@@ -10178,7 +10410,7 @@ async function syncSkillFiles() {
|
|
|
10178
10410
|
function normSkillName(name) {
|
|
10179
10411
|
return name.toLowerCase().replace(/\.mdx?$/, "");
|
|
10180
10412
|
}
|
|
10181
|
-
function discoverSkillFiles(
|
|
10413
|
+
function discoverSkillFiles(repoRoot3, excludeHashes, ingestedHashes, ingestedNames) {
|
|
10182
10414
|
const roots = [];
|
|
10183
10415
|
const add = (p) => {
|
|
10184
10416
|
try {
|
|
@@ -10188,9 +10420,9 @@ function discoverSkillFiles(repoRoot2, excludeHashes, ingestedHashes, ingestedNa
|
|
|
10188
10420
|
};
|
|
10189
10421
|
add(join19(homedir21(), ".claude", "skills"));
|
|
10190
10422
|
add(join19(homedir21(), ".agents", "skills"));
|
|
10191
|
-
if (
|
|
10192
|
-
add(join19(
|
|
10193
|
-
add(join19(
|
|
10423
|
+
if (repoRoot3) {
|
|
10424
|
+
add(join19(repoRoot3, ".claude", "skills"));
|
|
10425
|
+
add(join19(repoRoot3, ".agents", "skills"));
|
|
10194
10426
|
}
|
|
10195
10427
|
const out = [];
|
|
10196
10428
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -10273,7 +10505,7 @@ Found ${found.length} skill${found.length === 1 ? "" : "s"} in your Claude Code
|
|
|
10273
10505
|
async function discoverAndIngestSkills() {
|
|
10274
10506
|
try {
|
|
10275
10507
|
const sf = readFullSynkroFile();
|
|
10276
|
-
const
|
|
10508
|
+
const repoRoot3 = sf?._repoRoot || detectGitRepo2();
|
|
10277
10509
|
const mcpPort = process.env.SYNKRO_MCP_PORT || "18931";
|
|
10278
10510
|
const excludeHashes = /* @__PURE__ */ new Set();
|
|
10279
10511
|
if (sf?.skills?.length) {
|
|
@@ -10297,7 +10529,7 @@ async function discoverAndIngestSkills() {
|
|
|
10297
10529
|
}
|
|
10298
10530
|
} catch {
|
|
10299
10531
|
}
|
|
10300
|
-
const found = discoverSkillFiles(
|
|
10532
|
+
const found = discoverSkillFiles(repoRoot3, excludeHashes, ingestedHashes, ingestedNames);
|
|
10301
10533
|
if (found.length === 0) return;
|
|
10302
10534
|
const selectable = found.filter((f) => !f.ingested);
|
|
10303
10535
|
if (selectable.length === 0) {
|
|
@@ -10333,18 +10565,18 @@ async function discoverAndIngestSkills() {
|
|
|
10333
10565
|
}
|
|
10334
10566
|
}
|
|
10335
10567
|
function resolveSynkroBinPath() {
|
|
10336
|
-
const
|
|
10568
|
+
const run2 = (cmd3) => {
|
|
10337
10569
|
try {
|
|
10338
10570
|
return execSync4(cmd3, { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
10339
10571
|
} catch {
|
|
10340
10572
|
return "";
|
|
10341
10573
|
}
|
|
10342
10574
|
};
|
|
10343
|
-
const p =
|
|
10575
|
+
const p = run2("command -v synkro").split("\n")[0].trim();
|
|
10344
10576
|
return p && isAbsolute(p) ? p : "";
|
|
10345
10577
|
}
|
|
10346
10578
|
function ensureReachabilityGitHook() {
|
|
10347
|
-
const
|
|
10579
|
+
const run2 = (cmd3) => {
|
|
10348
10580
|
try {
|
|
10349
10581
|
return execSync4(cmd3, { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
10350
10582
|
} catch {
|
|
@@ -10352,9 +10584,9 @@ function ensureReachabilityGitHook() {
|
|
|
10352
10584
|
}
|
|
10353
10585
|
};
|
|
10354
10586
|
try {
|
|
10355
|
-
const root =
|
|
10587
|
+
const root = run2("git rev-parse --show-toplevel");
|
|
10356
10588
|
if (!root) return null;
|
|
10357
|
-
let hooksDir =
|
|
10589
|
+
let hooksDir = run2("git config --get core.hooksPath");
|
|
10358
10590
|
hooksDir = hooksDir ? isAbsolute(hooksDir) ? hooksDir : join19(root, hooksDir) : join19(root, ".git", "hooks");
|
|
10359
10591
|
if (!existsSync22(hooksDir)) mkdirSync15(hooksDir, { recursive: true });
|
|
10360
10592
|
const hookPath = join19(hooksDir, "post-commit");
|
|
@@ -10396,18 +10628,18 @@ function ensureReachabilityGitHook() {
|
|
|
10396
10628
|
}
|
|
10397
10629
|
}
|
|
10398
10630
|
function detectGitRepo2() {
|
|
10399
|
-
const
|
|
10631
|
+
const run2 = (cmd3) => {
|
|
10400
10632
|
try {
|
|
10401
10633
|
return execSync4(cmd3, { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
10402
10634
|
} catch {
|
|
10403
10635
|
return "";
|
|
10404
10636
|
}
|
|
10405
10637
|
};
|
|
10406
|
-
const remoteUrl =
|
|
10638
|
+
const remoteUrl = run2("git remote get-url origin");
|
|
10407
10639
|
if (remoteUrl) {
|
|
10408
10640
|
return remoteUrl.replace(/^git@[^:]+:/, "").replace(/^https?:\/\/[^/]+\//, "").replace(/\.git$/, "");
|
|
10409
10641
|
}
|
|
10410
|
-
const root =
|
|
10642
|
+
const root = run2("git rev-parse --show-toplevel");
|
|
10411
10643
|
return root ? root.split("/").pop() || null : null;
|
|
10412
10644
|
}
|
|
10413
10645
|
function getClaudeProjectsFolder() {
|
|
@@ -13856,10 +14088,10 @@ var init_packVerify = __esm({
|
|
|
13856
14088
|
// cli/installer/lockfile.ts
|
|
13857
14089
|
import { existsSync as existsSync30, readFileSync as readFileSync28, writeFileSync as writeFileSync20 } from "fs";
|
|
13858
14090
|
import { join as join28 } from "path";
|
|
13859
|
-
function lockPath(
|
|
13860
|
-
return join28(
|
|
14091
|
+
function lockPath(repoRoot3) {
|
|
14092
|
+
return join28(repoRoot3, LOCK_FILE);
|
|
13861
14093
|
}
|
|
13862
|
-
function writeLockfile(
|
|
14094
|
+
function writeLockfile(repoRoot3, entries) {
|
|
13863
14095
|
const sorted = [...entries].sort((a, b) => a.ref.localeCompare(b.ref));
|
|
13864
14096
|
const body = [
|
|
13865
14097
|
"# synkro.lock \u2014 generated by `synkro sync`. Commit this file.",
|
|
@@ -13875,7 +14107,7 @@ function writeLockfile(repoRoot2, entries) {
|
|
|
13875
14107
|
""
|
|
13876
14108
|
])
|
|
13877
14109
|
].join("\n");
|
|
13878
|
-
writeFileSync20(lockPath(
|
|
14110
|
+
writeFileSync20(lockPath(repoRoot3), body, "utf-8");
|
|
13879
14111
|
}
|
|
13880
14112
|
var LOCK_FILE;
|
|
13881
14113
|
var init_lockfile = __esm({
|
|
@@ -14087,6 +14319,822 @@ var init_whoami = __esm({
|
|
|
14087
14319
|
}
|
|
14088
14320
|
});
|
|
14089
14321
|
|
|
14322
|
+
// cli/commands/workspace.ts
|
|
14323
|
+
var workspace_exports = {};
|
|
14324
|
+
__export(workspace_exports, {
|
|
14325
|
+
workspaceCommand: () => workspaceCommand
|
|
14326
|
+
});
|
|
14327
|
+
import { existsSync as existsSync33, mkdirSync as mkdirSync19, readdirSync as readdirSync8, rmSync as rmSync5, writeFileSync as writeFileSync22 } from "fs";
|
|
14328
|
+
import { join as join31 } from "path";
|
|
14329
|
+
import { homedir as homedir31 } from "os";
|
|
14330
|
+
function markerPath(taskId) {
|
|
14331
|
+
return join31(WORKSPACE_CHOICE_DIR, `${taskId}.stay`);
|
|
14332
|
+
}
|
|
14333
|
+
function usage() {
|
|
14334
|
+
console.log(`synkro workspace \u2014 record where a task's work happens
|
|
14335
|
+
|
|
14336
|
+
Usage:
|
|
14337
|
+
synkro workspace stay <taskId> keep working in the current checkout
|
|
14338
|
+
synkro workspace clear <taskId> forget the choice (the gate asks again)
|
|
14339
|
+
synkro workspace status [taskId] show recorded choices
|
|
14340
|
+
|
|
14341
|
+
The gate asks once per task. "stay" is remembered until the choice is cleared or
|
|
14342
|
+
the active task changes.`);
|
|
14343
|
+
}
|
|
14344
|
+
async function workspaceCommand(args2) {
|
|
14345
|
+
const sub = String(args2[0] || "").trim();
|
|
14346
|
+
const taskId = String(args2[1] || "").trim();
|
|
14347
|
+
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
14348
|
+
usage();
|
|
14349
|
+
return;
|
|
14350
|
+
}
|
|
14351
|
+
if (sub === "status") {
|
|
14352
|
+
let recorded = [];
|
|
14353
|
+
try {
|
|
14354
|
+
recorded = existsSync33(WORKSPACE_CHOICE_DIR) ? readdirSync8(WORKSPACE_CHOICE_DIR).filter((name) => name.endsWith(".stay")) : [];
|
|
14355
|
+
} catch {
|
|
14356
|
+
recorded = [];
|
|
14357
|
+
}
|
|
14358
|
+
if (taskId) {
|
|
14359
|
+
const on = recorded.includes(`${taskId}.stay`);
|
|
14360
|
+
console.log(`${taskId}: ${on ? "stay recorded" : "no choice recorded"}`);
|
|
14361
|
+
return;
|
|
14362
|
+
}
|
|
14363
|
+
if (recorded.length === 0) {
|
|
14364
|
+
console.log("No workspace choices recorded.");
|
|
14365
|
+
return;
|
|
14366
|
+
}
|
|
14367
|
+
console.log("Staying in the current checkout for:");
|
|
14368
|
+
for (const name of recorded.sort()) console.log(` ${name.replace(/\.stay$/, "")}`);
|
|
14369
|
+
return;
|
|
14370
|
+
}
|
|
14371
|
+
if (sub !== "stay" && sub !== "clear") {
|
|
14372
|
+
console.error(`Unknown workspace subcommand: ${sub}`);
|
|
14373
|
+
usage();
|
|
14374
|
+
process.exitCode = 2;
|
|
14375
|
+
return;
|
|
14376
|
+
}
|
|
14377
|
+
if (!TASK_ID.test(taskId)) {
|
|
14378
|
+
console.error(taskId ? `Not a task id: ${taskId} (expected task_ followed by 8 characters)` : `Usage: synkro workspace ${sub} <taskId>`);
|
|
14379
|
+
process.exitCode = 2;
|
|
14380
|
+
return;
|
|
14381
|
+
}
|
|
14382
|
+
if (sub === "clear") {
|
|
14383
|
+
try {
|
|
14384
|
+
rmSync5(markerPath(taskId), { force: true });
|
|
14385
|
+
} catch {
|
|
14386
|
+
}
|
|
14387
|
+
console.log(`Cleared the workspace choice for ${taskId}.`);
|
|
14388
|
+
return;
|
|
14389
|
+
}
|
|
14390
|
+
try {
|
|
14391
|
+
mkdirSync19(WORKSPACE_CHOICE_DIR, { recursive: true });
|
|
14392
|
+
writeFileSync22(markerPath(taskId), `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
14393
|
+
`, "utf-8");
|
|
14394
|
+
} catch (error) {
|
|
14395
|
+
console.error(`Could not record the workspace choice: ${error?.message || error}`);
|
|
14396
|
+
process.exitCode = 1;
|
|
14397
|
+
return;
|
|
14398
|
+
}
|
|
14399
|
+
console.log(`Staying in the current checkout for ${taskId}. Synkro will not ask again for this task.`);
|
|
14400
|
+
}
|
|
14401
|
+
var WORKSPACE_CHOICE_DIR, TASK_ID;
|
|
14402
|
+
var init_workspace = __esm({
|
|
14403
|
+
"cli/commands/workspace.ts"() {
|
|
14404
|
+
"use strict";
|
|
14405
|
+
WORKSPACE_CHOICE_DIR = join31(homedir31(), ".synkro", "workspace-choice");
|
|
14406
|
+
TASK_ID = /^task_[a-z0-9]{8}$/i;
|
|
14407
|
+
}
|
|
14408
|
+
});
|
|
14409
|
+
|
|
14410
|
+
// cli/ui/tmux.ts
|
|
14411
|
+
import { execFile as execFile2, spawnSync as spawnSync11 } from "child_process";
|
|
14412
|
+
import { promisify } from "util";
|
|
14413
|
+
function runnerArgs(runner, argv) {
|
|
14414
|
+
return runner.kind === "container" ? ["docker", "exec", "-u", CONTAINER_USER, runner.container, ...argv] : argv;
|
|
14415
|
+
}
|
|
14416
|
+
function runnerInteractiveArgs(runner, argv) {
|
|
14417
|
+
return runner.kind === "container" ? ["docker", "exec", "-it", "-u", CONTAINER_USER, runner.container, ...argv] : argv;
|
|
14418
|
+
}
|
|
14419
|
+
async function run(runner, argv) {
|
|
14420
|
+
const [cmd3, ...args2] = runnerArgs(runner, argv);
|
|
14421
|
+
try {
|
|
14422
|
+
const { stdout, stderr } = await execFileAsync(cmd3, args2, { timeout: 8e3, maxBuffer: 1024 * 1024 });
|
|
14423
|
+
return { ok: true, stdout: String(stdout || ""), stderr: String(stderr || "") };
|
|
14424
|
+
} catch (error) {
|
|
14425
|
+
return { ok: false, stdout: String(error?.stdout || ""), stderr: String(error?.stderr || error?.message || "") };
|
|
14426
|
+
}
|
|
14427
|
+
}
|
|
14428
|
+
function runInherit(argv) {
|
|
14429
|
+
const [cmd3, ...args2] = argv;
|
|
14430
|
+
const result = spawnSync11(cmd3, args2, { stdio: "inherit" });
|
|
14431
|
+
return result.status ?? 1;
|
|
14432
|
+
}
|
|
14433
|
+
function slugify(name) {
|
|
14434
|
+
return String(name || "").toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "agent";
|
|
14435
|
+
}
|
|
14436
|
+
function agentSession(name) {
|
|
14437
|
+
return AGENT_PREFIX + slugify(name);
|
|
14438
|
+
}
|
|
14439
|
+
function buildSpawnAgent(opts) {
|
|
14440
|
+
const session = agentSession(opts.name);
|
|
14441
|
+
return [
|
|
14442
|
+
["tmux", "new-session", "-d", "-s", session, "-c", opts.cwd, opts.command],
|
|
14443
|
+
["tmux", "set-option", "-t", session, "status", "off"],
|
|
14444
|
+
["tmux", "set-option", "-t", session, "-q", "@synkro_harness", opts.harness],
|
|
14445
|
+
["tmux", "set-option", "-t", session, "-q", "@synkro_space", opts.space],
|
|
14446
|
+
["tmux", "set-option", "-t", session, "-q", "@synkro_backend", opts.backend],
|
|
14447
|
+
// Keep the pane visible after exit so the sidebar can render 'done'
|
|
14448
|
+
// instead of the agent silently vanishing.
|
|
14449
|
+
["tmux", "set-option", "-t", session, "remain-on-exit", "on"]
|
|
14450
|
+
];
|
|
14451
|
+
}
|
|
14452
|
+
function buildListAgents() {
|
|
14453
|
+
return ["tmux", "list-sessions", "-F", ["#{session_name}", "#{?pane_dead,dead,alive}", "#{@synkro_harness}", "#{@synkro_space}", "#{@synkro_backend}", "#{@synkro_pueue}"].join(FIELD_SEP)];
|
|
14454
|
+
}
|
|
14455
|
+
function buildCapture(session, lines = 14) {
|
|
14456
|
+
return ["tmux", "capture-pane", "-p", "-t", session, "-S", String(-lines)];
|
|
14457
|
+
}
|
|
14458
|
+
function buildKillSession(session) {
|
|
14459
|
+
return ["tmux", "kill-session", "-t", session];
|
|
14460
|
+
}
|
|
14461
|
+
function buildSetOption(session, option, value) {
|
|
14462
|
+
return ["tmux", "set-option", "-t", session, "-q", option, value];
|
|
14463
|
+
}
|
|
14464
|
+
function buildSendText(session, text) {
|
|
14465
|
+
return [
|
|
14466
|
+
["tmux", "send-keys", "-t", session, "-l", text],
|
|
14467
|
+
["tmux", "send-keys", "-t", session, "Enter"]
|
|
14468
|
+
];
|
|
14469
|
+
}
|
|
14470
|
+
function buildInterrupt(session) {
|
|
14471
|
+
return ["tmux", "send-keys", "-t", session, "Escape"];
|
|
14472
|
+
}
|
|
14473
|
+
function buildCenterAttachCommand(runner, session) {
|
|
14474
|
+
const argv = runnerInteractiveArgs(runner, ["tmux", "attach-session", "-t", session]);
|
|
14475
|
+
return "env TMUX= " + argv.map(shellQuote3).join(" ");
|
|
14476
|
+
}
|
|
14477
|
+
function shellQuote3(value) {
|
|
14478
|
+
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : "'" + value.replace(/'/g, "'\\''") + "'";
|
|
14479
|
+
}
|
|
14480
|
+
function parseAgentSessions(output, backend) {
|
|
14481
|
+
return String(output || "").split("\n").map((line) => line.split(FIELD_SEP)).filter((cols) => cols[0]?.startsWith(AGENT_PREFIX)).map((cols) => ({
|
|
14482
|
+
session: cols[0],
|
|
14483
|
+
name: cols[0].slice(AGENT_PREFIX.length),
|
|
14484
|
+
dead: cols[1] === "dead",
|
|
14485
|
+
harness: cols[2] || "claude",
|
|
14486
|
+
space: cols[3] || "",
|
|
14487
|
+
backend: cols[4] || backend,
|
|
14488
|
+
pueue: cols[5] || ""
|
|
14489
|
+
}));
|
|
14490
|
+
}
|
|
14491
|
+
function parseWorktrees(porcelain) {
|
|
14492
|
+
const rows = [];
|
|
14493
|
+
let current = {};
|
|
14494
|
+
for (const line of String(porcelain || "").split("\n")) {
|
|
14495
|
+
if (line.startsWith("worktree ")) current = { path: line.slice(9).trim() };
|
|
14496
|
+
else if (line.startsWith("branch ")) current.branch = line.slice(7).replace("refs/heads/", "").trim();
|
|
14497
|
+
else if (line.trim() === "" && current.path) {
|
|
14498
|
+
rows.push({
|
|
14499
|
+
path: current.path,
|
|
14500
|
+
branch: current.branch || "detached",
|
|
14501
|
+
name: current.path.split("/").filter(Boolean).pop() || current.path
|
|
14502
|
+
});
|
|
14503
|
+
current = {};
|
|
14504
|
+
}
|
|
14505
|
+
}
|
|
14506
|
+
if (current.path) {
|
|
14507
|
+
rows.push({
|
|
14508
|
+
path: current.path,
|
|
14509
|
+
branch: current.branch || "detached",
|
|
14510
|
+
name: current.path.split("/").filter(Boolean).pop() || current.path
|
|
14511
|
+
});
|
|
14512
|
+
}
|
|
14513
|
+
return rows;
|
|
14514
|
+
}
|
|
14515
|
+
var execFileAsync, AGENT_PREFIX, UI_SESSION, CONTAINER_USER, FIELD_SEP;
|
|
14516
|
+
var init_tmux = __esm({
|
|
14517
|
+
"cli/ui/tmux.ts"() {
|
|
14518
|
+
"use strict";
|
|
14519
|
+
execFileAsync = promisify(execFile2);
|
|
14520
|
+
AGENT_PREFIX = "synkro-agent-";
|
|
14521
|
+
UI_SESSION = "synkro-ui";
|
|
14522
|
+
CONTAINER_USER = "synkro";
|
|
14523
|
+
FIELD_SEP = "|";
|
|
14524
|
+
}
|
|
14525
|
+
});
|
|
14526
|
+
|
|
14527
|
+
// cli/ui/launch.ts
|
|
14528
|
+
function welcomeCommand() {
|
|
14529
|
+
const banner = [
|
|
14530
|
+
"",
|
|
14531
|
+
" synkro ui",
|
|
14532
|
+
" governed agents, one screen",
|
|
14533
|
+
"",
|
|
14534
|
+
" enter attach selected agent",
|
|
14535
|
+
" n new agent in selected space",
|
|
14536
|
+
" c new container agent",
|
|
14537
|
+
" g/s/y consent: track / skip / stay",
|
|
14538
|
+
" T new tab q quit",
|
|
14539
|
+
""
|
|
14540
|
+
].join("\\n");
|
|
14541
|
+
return "printf " + shellQuote3(banner + "\\n") + "; tail -f /dev/null";
|
|
14542
|
+
}
|
|
14543
|
+
function sidebarCommand(bootPath, centerPane, repoCwd) {
|
|
14544
|
+
const env = [
|
|
14545
|
+
"SYNKRO_UI_CENTER=" + shellQuote3(centerPane),
|
|
14546
|
+
"SYNKRO_UI_OUTER=" + UI_SESSION,
|
|
14547
|
+
"SYNKRO_UI_BOOT=" + shellQuote3(bootPath),
|
|
14548
|
+
"SYNKRO_UI_REPO=" + shellQuote3(repoCwd)
|
|
14549
|
+
].join(" ");
|
|
14550
|
+
return "env " + env + " node " + shellQuote3(bootPath) + " ui --sidebar";
|
|
14551
|
+
}
|
|
14552
|
+
async function styleOuterSession() {
|
|
14553
|
+
const style = [
|
|
14554
|
+
["set-option", "-t", UI_SESSION, "status-position", "top"],
|
|
14555
|
+
["set-option", "-t", UI_SESSION, "status-style", "bg=colour233,fg=colour245"],
|
|
14556
|
+
["set-option", "-t", UI_SESSION, "status-left", " synkro "],
|
|
14557
|
+
["set-option", "-t", UI_SESSION, "status-left-style", "fg=colour135,bold"],
|
|
14558
|
+
["set-option", "-t", UI_SESSION, "status-right", " + (T new tab) "],
|
|
14559
|
+
["set-option", "-t", UI_SESSION, "status-right-style", "fg=colour240"],
|
|
14560
|
+
["set-option", "-t", UI_SESSION, "-w", "window-status-format", " #W "],
|
|
14561
|
+
["set-option", "-t", UI_SESSION, "-w", "window-status-current-format", "#[bg=colour135,fg=colour233,bold] #W #[default]"],
|
|
14562
|
+
["set-option", "-t", UI_SESSION, "pane-border-style", "fg=colour236"],
|
|
14563
|
+
["set-option", "-t", UI_SESSION, "pane-active-border-style", "fg=colour135"],
|
|
14564
|
+
// Tab keys without the prefix: Alt+t new tab, Alt+arrows to move.
|
|
14565
|
+
["bind-key", "-n", "M-t", "new-window"],
|
|
14566
|
+
["bind-key", "-n", "M-Right", "next-window"],
|
|
14567
|
+
["bind-key", "-n", "M-Left", "previous-window"]
|
|
14568
|
+
];
|
|
14569
|
+
for (const argv of style) await run(HOST, ["tmux", ...argv]);
|
|
14570
|
+
}
|
|
14571
|
+
async function buildTab(bootPath, repoCwd, windowTarget) {
|
|
14572
|
+
if (windowTarget === void 0) {
|
|
14573
|
+
await run(HOST, ["tmux", "new-session", "-d", "-s", UI_SESSION, "-x", "220", "-y", "55", welcomeCommand()]);
|
|
14574
|
+
windowTarget = UI_SESSION + ":0";
|
|
14575
|
+
} else {
|
|
14576
|
+
const created = await run(HOST, ["tmux", "new-window", "-t", UI_SESSION, "-P", "-F", "#{window_id}", welcomeCommand()]);
|
|
14577
|
+
windowTarget = created.stdout.trim() || windowTarget;
|
|
14578
|
+
}
|
|
14579
|
+
await run(HOST, ["tmux", "rename-window", "-t", windowTarget, "space"]);
|
|
14580
|
+
const split = await run(HOST, [
|
|
14581
|
+
"tmux",
|
|
14582
|
+
"split-window",
|
|
14583
|
+
"-hb",
|
|
14584
|
+
"-t",
|
|
14585
|
+
windowTarget,
|
|
14586
|
+
"-l",
|
|
14587
|
+
SIDEBAR_WIDTH,
|
|
14588
|
+
"-P",
|
|
14589
|
+
"-F",
|
|
14590
|
+
"#{pane_id}",
|
|
14591
|
+
"tail -f /dev/null"
|
|
14592
|
+
]);
|
|
14593
|
+
const sidebarPane = split.stdout.trim();
|
|
14594
|
+
const panes = await run(HOST, ["tmux", "list-panes", "-t", windowTarget, "-F", "#{pane_id}"]);
|
|
14595
|
+
const centerPane = panes.stdout.split("\n").map((line) => line.trim()).filter(Boolean).find((id) => id !== sidebarPane) || "";
|
|
14596
|
+
await run(HOST, ["tmux", "respawn-pane", "-k", "-t", sidebarPane, sidebarCommand(bootPath, centerPane, repoCwd)]);
|
|
14597
|
+
}
|
|
14598
|
+
async function uiSessionExists() {
|
|
14599
|
+
const result = await run(HOST, ["tmux", "has-session", "-t", UI_SESSION]);
|
|
14600
|
+
return result.ok;
|
|
14601
|
+
}
|
|
14602
|
+
async function launchUi(bootPath, repoCwd) {
|
|
14603
|
+
if (!await uiSessionExists()) {
|
|
14604
|
+
await buildTab(bootPath, repoCwd);
|
|
14605
|
+
await styleOuterSession();
|
|
14606
|
+
}
|
|
14607
|
+
return runInherit(process.env.TMUX ? ["tmux", "switch-client", "-t", UI_SESSION] : ["tmux", "attach-session", "-t", UI_SESSION]);
|
|
14608
|
+
}
|
|
14609
|
+
var SIDEBAR_WIDTH, HOST;
|
|
14610
|
+
var init_launch = __esm({
|
|
14611
|
+
"cli/ui/launch.ts"() {
|
|
14612
|
+
"use strict";
|
|
14613
|
+
init_tmux();
|
|
14614
|
+
SIDEBAR_WIDTH = "34";
|
|
14615
|
+
HOST = { kind: "host" };
|
|
14616
|
+
}
|
|
14617
|
+
});
|
|
14618
|
+
|
|
14619
|
+
// cli/ui/model.ts
|
|
14620
|
+
function detectAsk(tail2) {
|
|
14621
|
+
for (const marker of BLOCK_MARKERS) {
|
|
14622
|
+
if (marker.pattern.test(String(tail2 || ""))) return marker.ask;
|
|
14623
|
+
}
|
|
14624
|
+
return null;
|
|
14625
|
+
}
|
|
14626
|
+
function deriveStatus(input) {
|
|
14627
|
+
if (input.dead) return { status: "done" };
|
|
14628
|
+
const ask3 = detectAsk(input.tail);
|
|
14629
|
+
if (ask3) return { status: "blocked", ask: ask3 };
|
|
14630
|
+
return { status: input.changed ? "working" : "idle" };
|
|
14631
|
+
}
|
|
14632
|
+
function hashTail(tail2) {
|
|
14633
|
+
let hash = 0;
|
|
14634
|
+
const text = String(tail2 || "");
|
|
14635
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
14636
|
+
hash = (hash << 5) - hash + text.charCodeAt(index) | 0;
|
|
14637
|
+
}
|
|
14638
|
+
return String(hash);
|
|
14639
|
+
}
|
|
14640
|
+
function mapAgentToTask(spacePath, tasks) {
|
|
14641
|
+
const normalized = String(spacePath || "").replace(/\/+$/, "");
|
|
14642
|
+
if (!normalized) return void 0;
|
|
14643
|
+
const hit = tasks.find((task) => task.worktree && task.worktree.replace(/\/+$/, "") === normalized);
|
|
14644
|
+
return hit?.linear || void 0;
|
|
14645
|
+
}
|
|
14646
|
+
async function fetchConductorTasks(baseUrl) {
|
|
14647
|
+
try {
|
|
14648
|
+
const controller = new AbortController();
|
|
14649
|
+
const timer = setTimeout(() => controller.abort(), 900);
|
|
14650
|
+
const response = await fetch(baseUrl + "/api/local/conductor/repositories", { signal: controller.signal });
|
|
14651
|
+
clearTimeout(timer);
|
|
14652
|
+
if (!response.ok) return [];
|
|
14653
|
+
const payload = await response.json().catch(() => null);
|
|
14654
|
+
return Array.isArray(payload?.tasks) ? payload.tasks.map((task) => ({
|
|
14655
|
+
worktree: task?.worktree || null,
|
|
14656
|
+
linear: task?.linear?.key || null,
|
|
14657
|
+
status: String(task?.status || "")
|
|
14658
|
+
})) : [];
|
|
14659
|
+
} catch {
|
|
14660
|
+
return [];
|
|
14661
|
+
}
|
|
14662
|
+
}
|
|
14663
|
+
async function discoverHostSpaces(repoCwd) {
|
|
14664
|
+
const result = await run({ kind: "host" }, ["git", "-C", repoCwd, "worktree", "list", "--porcelain"]);
|
|
14665
|
+
if (!result.ok) return [];
|
|
14666
|
+
return parseWorktrees(result.stdout).map((row2) => ({
|
|
14667
|
+
name: row2.name,
|
|
14668
|
+
branch: row2.branch,
|
|
14669
|
+
path: row2.path,
|
|
14670
|
+
backend: "host"
|
|
14671
|
+
}));
|
|
14672
|
+
}
|
|
14673
|
+
async function discoverContainerSpaces(runner) {
|
|
14674
|
+
if (runner.kind !== "container") return [];
|
|
14675
|
+
const result = await run(runner, ["sh", "-c", "ls -1 /home/synkro/work 2>/dev/null"]);
|
|
14676
|
+
if (!result.ok) return [];
|
|
14677
|
+
return result.stdout.split("\n").map((line) => line.trim()).filter((name) => /^ui-/.test(name)).map((name) => ({
|
|
14678
|
+
name,
|
|
14679
|
+
branch: "container",
|
|
14680
|
+
path: "/home/synkro/work/" + name,
|
|
14681
|
+
backend: "container"
|
|
14682
|
+
}));
|
|
14683
|
+
}
|
|
14684
|
+
async function discoverAgents(runner, backend, previousHashes) {
|
|
14685
|
+
const listed = await run(runner, buildListAgents());
|
|
14686
|
+
const rows = listed.ok ? parseAgentSessions(listed.stdout, backend) : [];
|
|
14687
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
14688
|
+
const agents = [];
|
|
14689
|
+
for (const row2 of rows) {
|
|
14690
|
+
const capture = row2.dead ? { ok: true, stdout: "" } : await run(runner, buildCapture(row2.session));
|
|
14691
|
+
const tail2 = capture.ok ? capture.stdout : "";
|
|
14692
|
+
const nextHash = hashTail(tail2);
|
|
14693
|
+
const changed = previousHashes.has(row2.session) && previousHashes.get(row2.session) !== nextHash;
|
|
14694
|
+
hashes.set(row2.session, nextHash);
|
|
14695
|
+
const derived = deriveStatus({ dead: row2.dead, tail: tail2, changed });
|
|
14696
|
+
agents.push({
|
|
14697
|
+
name: row2.name,
|
|
14698
|
+
session: row2.session,
|
|
14699
|
+
harness: row2.harness,
|
|
14700
|
+
space: row2.space,
|
|
14701
|
+
backend: row2.backend,
|
|
14702
|
+
status: derived.status,
|
|
14703
|
+
ask: derived.ask
|
|
14704
|
+
});
|
|
14705
|
+
}
|
|
14706
|
+
return { agents, hashes };
|
|
14707
|
+
}
|
|
14708
|
+
var BLOCK_MARKERS;
|
|
14709
|
+
var init_model = __esm({
|
|
14710
|
+
"cli/ui/model.ts"() {
|
|
14711
|
+
"use strict";
|
|
14712
|
+
init_tmux();
|
|
14713
|
+
BLOCK_MARKERS = [
|
|
14714
|
+
{ pattern: /needs a tracking decision|Task tracking suggestion|tracking decision for "/i, ask: "tracking" },
|
|
14715
|
+
{ pattern: /\[synkro:task-workspace|\[synkro:scm|keep working in the current workspace/i, ask: "workspace" },
|
|
14716
|
+
{ pattern: /⛔/, ask: "tracking" }
|
|
14717
|
+
];
|
|
14718
|
+
}
|
|
14719
|
+
});
|
|
14720
|
+
|
|
14721
|
+
// cli/ui/consent.ts
|
|
14722
|
+
function actionsForAsk(ask3) {
|
|
14723
|
+
return ask3 === "workspace" ? ["stay"] : ["track", "skip"];
|
|
14724
|
+
}
|
|
14725
|
+
var CONSENT_PHRASES;
|
|
14726
|
+
var init_consent = __esm({
|
|
14727
|
+
"cli/ui/consent.ts"() {
|
|
14728
|
+
"use strict";
|
|
14729
|
+
CONSENT_PHRASES = {
|
|
14730
|
+
/** Settles the conductor tracking ask by durable decline. */
|
|
14731
|
+
skip: "skip the task tracking, continue without a task",
|
|
14732
|
+
/** Settles the task-workspace ask in place. */
|
|
14733
|
+
stay: "stay in the current worktree",
|
|
14734
|
+
/** Asks the agent to run the two-phase create flow (draft → approve). */
|
|
14735
|
+
track: "yes, track this work \u2014 draft the requirements and create the task"
|
|
14736
|
+
};
|
|
14737
|
+
}
|
|
14738
|
+
});
|
|
14739
|
+
|
|
14740
|
+
// cli/ui/render.ts
|
|
14741
|
+
function pad(text, width) {
|
|
14742
|
+
return text.length >= width ? text.slice(0, width) : text + " ".repeat(width - text.length);
|
|
14743
|
+
}
|
|
14744
|
+
function row(selected, width, content) {
|
|
14745
|
+
const body = stripForPad(" " + content, width);
|
|
14746
|
+
return selected ? STYLE.select + body + STYLE.reset : body;
|
|
14747
|
+
}
|
|
14748
|
+
function stripForPad(text, width) {
|
|
14749
|
+
let visible = 0;
|
|
14750
|
+
let out = "";
|
|
14751
|
+
let index = 0;
|
|
14752
|
+
while (index < text.length && visible < width) {
|
|
14753
|
+
if (text.startsWith(ESC, index)) {
|
|
14754
|
+
const end = text.indexOf("m", index);
|
|
14755
|
+
if (end === -1) break;
|
|
14756
|
+
out += text.slice(index, end + 1);
|
|
14757
|
+
index = end + 1;
|
|
14758
|
+
} else {
|
|
14759
|
+
out += text[index];
|
|
14760
|
+
index += 1;
|
|
14761
|
+
visible += 1;
|
|
14762
|
+
}
|
|
14763
|
+
}
|
|
14764
|
+
return out + " ".repeat(Math.max(0, width - visible));
|
|
14765
|
+
}
|
|
14766
|
+
function renderSidebar(state, width = 32, height = 40) {
|
|
14767
|
+
const lines = [];
|
|
14768
|
+
lines.push("");
|
|
14769
|
+
lines.push(" " + STYLE.header + "spaces" + STYLE.reset);
|
|
14770
|
+
lines.push("");
|
|
14771
|
+
state.spaces.forEach((space, index) => {
|
|
14772
|
+
const selected = state.section === "spaces" && index === state.spaceIndex;
|
|
14773
|
+
const badge = space.backend === "container" ? STYLE.accent + "\u25A3 " + STYLE.reset : "";
|
|
14774
|
+
lines.push(row(selected, width, STYLE.done + "\u25CF" + STYLE.reset + " " + badge + STYLE.bold + space.name + STYLE.reset));
|
|
14775
|
+
lines.push(row(selected, width, " " + STYLE.branch + space.branch + STYLE.reset));
|
|
14776
|
+
});
|
|
14777
|
+
if (state.spaces.length === 0) lines.push(row(false, width, STYLE.dim + "no spaces found" + STYLE.reset));
|
|
14778
|
+
lines.push("");
|
|
14779
|
+
lines.push(" " + STYLE.header + "agents" + STYLE.reset);
|
|
14780
|
+
lines.push("");
|
|
14781
|
+
state.agents.forEach((agent, index) => {
|
|
14782
|
+
const selected = state.section === "agents" && index === state.agentIndex;
|
|
14783
|
+
const dot = DOT[agent.status] || DOT.idle;
|
|
14784
|
+
const badge = agent.backend === "container" ? STYLE.accent + "\u25A3 " + STYLE.reset : "";
|
|
14785
|
+
const linear = agent.linear ? " " + STYLE.dim + agent.linear + STYLE.reset : "";
|
|
14786
|
+
lines.push(row(selected, width, dot + " " + badge + STYLE.bold + agent.name + STYLE.reset + linear));
|
|
14787
|
+
const statusStyle = agent.status === "blocked" ? STYLE.blocked : STYLE.dim;
|
|
14788
|
+
lines.push(row(selected, width, " " + statusStyle + agent.status + STYLE.reset + STYLE.dim + " \xB7 " + agent.harness + STYLE.reset));
|
|
14789
|
+
});
|
|
14790
|
+
if (state.agents.length === 0) lines.push(row(false, width, STYLE.dim + "no agents \u2014 n to spawn" + STYLE.reset));
|
|
14791
|
+
const selectedAgent = state.section === "agents" ? state.agents[state.agentIndex] : void 0;
|
|
14792
|
+
if (selectedAgent?.status === "blocked" && selectedAgent.ask) {
|
|
14793
|
+
lines.push("");
|
|
14794
|
+
lines.push(row(false, width, STYLE.blocked + "\u26D4 needs consent" + STYLE.reset));
|
|
14795
|
+
for (const action of actionsForAsk(selectedAgent.ask)) {
|
|
14796
|
+
const key = action === "track" ? "g" : action === "skip" ? "s" : "y";
|
|
14797
|
+
lines.push(row(false, width, STYLE.dim + " " + key + " \u2014 " + action + STYLE.reset));
|
|
14798
|
+
}
|
|
14799
|
+
}
|
|
14800
|
+
while (lines.length < height - 4) lines.push(pad("", width));
|
|
14801
|
+
lines.push(pad("", width));
|
|
14802
|
+
if (state.message) lines.push(row(false, width, STYLE.accent + state.message.slice(0, width - 2) + STYLE.reset));
|
|
14803
|
+
else lines.push(row(false, width, STYLE.dim + "n new \xB7 enter attach \xB7 x kill" + STYLE.reset));
|
|
14804
|
+
lines.push(row(false, width, STYLE.dim + "T tab \xB7 i interrupt \xB7 q quit" + STYLE.reset));
|
|
14805
|
+
lines.push(row(false, width, STYLE.dim + state.backendNote + STYLE.reset));
|
|
14806
|
+
return lines.slice(0, height).join("\n");
|
|
14807
|
+
}
|
|
14808
|
+
var ESC, STYLE, DOT;
|
|
14809
|
+
var init_render = __esm({
|
|
14810
|
+
"cli/ui/render.ts"() {
|
|
14811
|
+
"use strict";
|
|
14812
|
+
init_consent();
|
|
14813
|
+
ESC = "\x1B[";
|
|
14814
|
+
STYLE = {
|
|
14815
|
+
reset: ESC + "0m",
|
|
14816
|
+
dim: ESC + "2m",
|
|
14817
|
+
bold: ESC + "1m",
|
|
14818
|
+
header: ESC + "38;5;245m",
|
|
14819
|
+
select: ESC + "48;5;236m",
|
|
14820
|
+
working: ESC + "38;5;214m",
|
|
14821
|
+
idle: ESC + "38;5;244m",
|
|
14822
|
+
blocked: ESC + "38;5;203m",
|
|
14823
|
+
done: ESC + "38;5;114m",
|
|
14824
|
+
accent: ESC + "38;5;135m",
|
|
14825
|
+
branch: ESC + "38;5;140m"
|
|
14826
|
+
};
|
|
14827
|
+
DOT = {
|
|
14828
|
+
working: STYLE.working + "\u25CF" + STYLE.reset,
|
|
14829
|
+
idle: STYLE.idle + "\u25CB" + STYLE.reset,
|
|
14830
|
+
blocked: STYLE.blocked + "\u25CF" + STYLE.reset,
|
|
14831
|
+
done: STYLE.done + "\u25CF" + STYLE.reset
|
|
14832
|
+
};
|
|
14833
|
+
}
|
|
14834
|
+
});
|
|
14835
|
+
|
|
14836
|
+
// cli/ui/pueue.ts
|
|
14837
|
+
function buildEnsureGroup() {
|
|
14838
|
+
return ["pueue", "group", "add", PUEUE_GROUP];
|
|
14839
|
+
}
|
|
14840
|
+
function buildGroupParallel() {
|
|
14841
|
+
return ["pueue", "parallel", "-g", PUEUE_GROUP, "32"];
|
|
14842
|
+
}
|
|
14843
|
+
function buildSentinel(session) {
|
|
14844
|
+
const loop = "while tmux has-session -t " + shellQuote3(session) + " 2>/dev/null; do sleep 10; done";
|
|
14845
|
+
return ["pueue", "add", "-g", PUEUE_GROUP, "-l", session, "--", loop];
|
|
14846
|
+
}
|
|
14847
|
+
function buildRemove(taskId) {
|
|
14848
|
+
return ["pueue", "remove", taskId];
|
|
14849
|
+
}
|
|
14850
|
+
async function pueueAvailable2(runner) {
|
|
14851
|
+
const result = await run(runner, ["sh", "-c", "command -v pueue >/dev/null 2>&1 && pueue status >/dev/null 2>&1 && echo ok"]);
|
|
14852
|
+
return result.ok && result.stdout.includes("ok");
|
|
14853
|
+
}
|
|
14854
|
+
async function registerAgent(runner, session) {
|
|
14855
|
+
if (!await pueueAvailable2(runner)) return "";
|
|
14856
|
+
await run(runner, buildEnsureGroup());
|
|
14857
|
+
await run(runner, buildGroupParallel());
|
|
14858
|
+
const added = await run(runner, buildSentinel(session));
|
|
14859
|
+
const match = added.stdout.match(/id\s+(\d+)/i) || added.stderr.match(/id\s+(\d+)/i);
|
|
14860
|
+
return match ? match[1] : "";
|
|
14861
|
+
}
|
|
14862
|
+
async function releaseAgent(runner, pueueId) {
|
|
14863
|
+
if (!pueueId) return;
|
|
14864
|
+
await run(runner, buildRemove(pueueId)).catch?.(() => {
|
|
14865
|
+
});
|
|
14866
|
+
}
|
|
14867
|
+
var PUEUE_GROUP;
|
|
14868
|
+
var init_pueue2 = __esm({
|
|
14869
|
+
"cli/ui/pueue.ts"() {
|
|
14870
|
+
"use strict";
|
|
14871
|
+
init_tmux();
|
|
14872
|
+
PUEUE_GROUP = "synkro-ui";
|
|
14873
|
+
}
|
|
14874
|
+
});
|
|
14875
|
+
|
|
14876
|
+
// cli/ui/backend.ts
|
|
14877
|
+
async function detectContainerBackend() {
|
|
14878
|
+
const runner = { kind: "container", container: CONTAINER_NAME2 };
|
|
14879
|
+
const probe = await run({ kind: "host" }, [
|
|
14880
|
+
"docker",
|
|
14881
|
+
"exec",
|
|
14882
|
+
CONTAINER_NAME2,
|
|
14883
|
+
"sh",
|
|
14884
|
+
"-c",
|
|
14885
|
+
"command -v tmux >/dev/null && command -v claude >/dev/null && ls " + shellQuote3(AUTH_SEED) + " >/dev/null 2>&1 && echo ready"
|
|
14886
|
+
]);
|
|
14887
|
+
if (probe.ok && probe.stdout.includes("ready")) {
|
|
14888
|
+
return { runner, backend: "container", note: "runtime: container (" + CONTAINER_NAME2 + ")" };
|
|
14889
|
+
}
|
|
14890
|
+
return { runner: { kind: "host" }, backend: "host", note: "runtime: host (container unavailable)" };
|
|
14891
|
+
}
|
|
14892
|
+
async function provisionContainerWorkspace(runner, slug) {
|
|
14893
|
+
const dir = CONTAINER_WORK + "/ui-" + slug;
|
|
14894
|
+
await run(runner, [
|
|
14895
|
+
"sh",
|
|
14896
|
+
"-c",
|
|
14897
|
+
"mkdir -p " + shellQuote3(dir) + " && cp -n " + shellQuote3(AUTH_SEED) + " " + shellQuote3(dir + "/.claude.json") + " 2>/dev/null; true"
|
|
14898
|
+
]);
|
|
14899
|
+
return dir;
|
|
14900
|
+
}
|
|
14901
|
+
async function spawnAgent(info, request) {
|
|
14902
|
+
const slug = slugify(request.name);
|
|
14903
|
+
const session = agentSession(slug);
|
|
14904
|
+
const runner = request.backend === "container" ? { kind: "container", container: CONTAINER_NAME2 } : { kind: "host" };
|
|
14905
|
+
let cwd = request.cwd;
|
|
14906
|
+
if (request.backend === "container") {
|
|
14907
|
+
cwd = request.cwd.startsWith(CONTAINER_WORK) ? request.cwd : await provisionContainerWorkspace(runner, slug);
|
|
14908
|
+
}
|
|
14909
|
+
const command = request.harness === "codex" ? "codex" : "claude";
|
|
14910
|
+
for (const argv of buildSpawnAgent({ name: slug, cwd, command, harness: request.harness, space: request.spaceName, backend: request.backend })) {
|
|
14911
|
+
const result = await run(runner, argv);
|
|
14912
|
+
if (!result.ok && argv[1] === "new-session") {
|
|
14913
|
+
return { ok: false, session, error: result.stderr.trim() || "tmux new-session failed" };
|
|
14914
|
+
}
|
|
14915
|
+
}
|
|
14916
|
+
const pueueId = await registerAgent(runner, session);
|
|
14917
|
+
if (pueueId) await run(runner, buildSetOption(session, "@synkro_pueue", pueueId));
|
|
14918
|
+
return { ok: true, session };
|
|
14919
|
+
}
|
|
14920
|
+
async function killAgent(backend, session, pueueId) {
|
|
14921
|
+
const runner = backend === "container" ? { kind: "container", container: CONTAINER_NAME2 } : { kind: "host" };
|
|
14922
|
+
await run(runner, buildKillSession(session));
|
|
14923
|
+
await releaseAgent(runner, pueueId);
|
|
14924
|
+
}
|
|
14925
|
+
var CONTAINER_NAME2, CONTAINER_WORK, AUTH_SEED;
|
|
14926
|
+
var init_backend = __esm({
|
|
14927
|
+
"cli/ui/backend.ts"() {
|
|
14928
|
+
"use strict";
|
|
14929
|
+
init_tmux();
|
|
14930
|
+
init_pueue2();
|
|
14931
|
+
CONTAINER_NAME2 = "synkro-server";
|
|
14932
|
+
CONTAINER_WORK = "/home/synkro/work";
|
|
14933
|
+
AUTH_SEED = CONTAINER_WORK + "/claude-1/.claude.json";
|
|
14934
|
+
}
|
|
14935
|
+
});
|
|
14936
|
+
|
|
14937
|
+
// cli/ui/sidebar.ts
|
|
14938
|
+
function runnerFor(backend) {
|
|
14939
|
+
return backend === "container" ? { kind: "container", container: CONTAINER_NAME2 } : { kind: "host" };
|
|
14940
|
+
}
|
|
14941
|
+
async function runSidebar() {
|
|
14942
|
+
const centerPane = process.env.SYNKRO_UI_CENTER || "";
|
|
14943
|
+
const outerSession = process.env.SYNKRO_UI_OUTER || "synkro-ui";
|
|
14944
|
+
const bootPath = process.env.SYNKRO_UI_BOOT || process.argv[1];
|
|
14945
|
+
const repoCwd = process.env.SYNKRO_UI_REPO || process.cwd();
|
|
14946
|
+
const info = await detectContainerBackend();
|
|
14947
|
+
const state = {
|
|
14948
|
+
spaces: [],
|
|
14949
|
+
agents: [],
|
|
14950
|
+
section: "agents",
|
|
14951
|
+
spaceIndex: 0,
|
|
14952
|
+
agentIndex: 0,
|
|
14953
|
+
backendNote: info.note,
|
|
14954
|
+
message: ""
|
|
14955
|
+
};
|
|
14956
|
+
let hashes = /* @__PURE__ */ new Map();
|
|
14957
|
+
let lastFrame = "";
|
|
14958
|
+
let spawnCounter = 1;
|
|
14959
|
+
const host = { kind: "host" };
|
|
14960
|
+
const containerRunner = { kind: "container", container: CONTAINER_NAME2 };
|
|
14961
|
+
async function refresh() {
|
|
14962
|
+
const [hostSpaces, containerSpaces, conductorTasks] = await Promise.all([
|
|
14963
|
+
discoverHostSpaces(repoCwd),
|
|
14964
|
+
info.backend === "container" ? discoverContainerSpaces(containerRunner) : Promise.resolve([]),
|
|
14965
|
+
fetchConductorTasks(CONDUCTOR_URL)
|
|
14966
|
+
]);
|
|
14967
|
+
state.spaces = [...hostSpaces, ...containerSpaces];
|
|
14968
|
+
const hostAgents = await discoverAgents(host, "host", hashes);
|
|
14969
|
+
const containerAgents = info.backend === "container" ? await discoverAgents(containerRunner, "container", hashes) : { agents: [], hashes: /* @__PURE__ */ new Map() };
|
|
14970
|
+
hashes = new Map([...hostAgents.hashes, ...containerAgents.hashes]);
|
|
14971
|
+
state.agents = [...hostAgents.agents, ...containerAgents.agents].map((agent) => ({
|
|
14972
|
+
...agent,
|
|
14973
|
+
linear: mapAgentToTask(agent.space, conductorTasks)
|
|
14974
|
+
}));
|
|
14975
|
+
state.spaceIndex = Math.min(state.spaceIndex, Math.max(0, state.spaces.length - 1));
|
|
14976
|
+
state.agentIndex = Math.min(state.agentIndex, Math.max(0, state.agents.length - 1));
|
|
14977
|
+
}
|
|
14978
|
+
function draw() {
|
|
14979
|
+
const rows = Number(process.stdout.rows || 42);
|
|
14980
|
+
const cols = Number(process.stdout.columns || 32);
|
|
14981
|
+
const frame = renderSidebar(state, cols, rows);
|
|
14982
|
+
if (frame === lastFrame) return;
|
|
14983
|
+
lastFrame = frame;
|
|
14984
|
+
process.stdout.write("\x1B[2J\x1B[H" + frame);
|
|
14985
|
+
}
|
|
14986
|
+
async function attachSelected() {
|
|
14987
|
+
const agent = state.agents[state.agentIndex];
|
|
14988
|
+
if (!agent || !centerPane) return;
|
|
14989
|
+
const command = buildCenterAttachCommand(runnerFor(agent.backend), agent.session);
|
|
14990
|
+
await run(host, ["tmux", "respawn-pane", "-k", "-t", centerPane, command]);
|
|
14991
|
+
await run(host, ["tmux", "rename-window", "-t", outerSession, agent.name]);
|
|
14992
|
+
state.message = "attached " + agent.name;
|
|
14993
|
+
}
|
|
14994
|
+
async function spawnInSelectedSpace() {
|
|
14995
|
+
const space = state.spaces[state.spaceIndex] || state.spaces[0];
|
|
14996
|
+
if (!space) {
|
|
14997
|
+
state.message = "no space selected";
|
|
14998
|
+
return;
|
|
14999
|
+
}
|
|
15000
|
+
const name = space.name + "-" + spawnCounter++;
|
|
15001
|
+
const result = await spawnAgent(info, {
|
|
15002
|
+
name,
|
|
15003
|
+
harness: "claude",
|
|
15004
|
+
spaceName: space.name,
|
|
15005
|
+
cwd: space.path,
|
|
15006
|
+
backend: space.backend === "container" && info.backend === "container" ? "container" : "host"
|
|
15007
|
+
});
|
|
15008
|
+
state.message = result.ok ? "spawned " + name : "spawn failed: " + (result.error || "").slice(0, 24);
|
|
15009
|
+
}
|
|
15010
|
+
async function spawnContainerAgent() {
|
|
15011
|
+
if (info.backend !== "container") {
|
|
15012
|
+
state.message = "container runtime unavailable";
|
|
15013
|
+
return;
|
|
15014
|
+
}
|
|
15015
|
+
const name = "box-" + spawnCounter++;
|
|
15016
|
+
const result = await spawnAgent(info, { name, harness: "claude", spaceName: name, cwd: "", backend: "container" });
|
|
15017
|
+
state.message = result.ok ? "spawned \u25A3 " + name : "spawn failed: " + (result.error || "").slice(0, 24);
|
|
15018
|
+
}
|
|
15019
|
+
async function consent(action) {
|
|
15020
|
+
const agent = state.agents[state.agentIndex];
|
|
15021
|
+
if (!agent || agent.status !== "blocked" || !agent.ask) return;
|
|
15022
|
+
if (!actionsForAsk(agent.ask).includes(action)) return;
|
|
15023
|
+
const runner = runnerFor(agent.backend);
|
|
15024
|
+
for (const argv of buildSendText(agent.session, CONSENT_PHRASES[action])) await run(runner, argv);
|
|
15025
|
+
state.message = action + " \u2192 " + agent.name;
|
|
15026
|
+
}
|
|
15027
|
+
async function newTab() {
|
|
15028
|
+
await run(host, ["node", String(bootPath), "ui", "--new-tab", outerSession]);
|
|
15029
|
+
}
|
|
15030
|
+
process.stdin.setRawMode?.(true);
|
|
15031
|
+
process.stdin.resume();
|
|
15032
|
+
process.stdin.on("data", (chunk) => {
|
|
15033
|
+
const key = chunk.toString("utf8");
|
|
15034
|
+
void (async () => {
|
|
15035
|
+
const list = state.section === "spaces" ? state.spaces : state.agents;
|
|
15036
|
+
if (key === " ") state.section = state.section === "spaces" ? "agents" : "spaces";
|
|
15037
|
+
else if (key === "j" || key === "\x1B[B") {
|
|
15038
|
+
if (state.section === "spaces") state.spaceIndex = Math.min(state.spaceIndex + 1, Math.max(0, list.length - 1));
|
|
15039
|
+
else state.agentIndex = Math.min(state.agentIndex + 1, Math.max(0, list.length - 1));
|
|
15040
|
+
} else if (key === "k" || key === "\x1B[A") {
|
|
15041
|
+
if (state.section === "spaces") state.spaceIndex = Math.max(0, state.spaceIndex - 1);
|
|
15042
|
+
else state.agentIndex = Math.max(0, state.agentIndex - 1);
|
|
15043
|
+
} else if (key === "\r") {
|
|
15044
|
+
if (state.section === "agents") await attachSelected();
|
|
15045
|
+
else state.message = "space: " + (state.spaces[state.spaceIndex]?.name || "");
|
|
15046
|
+
} else if (key === "n") await spawnInSelectedSpace();
|
|
15047
|
+
else if (key === "c") await spawnContainerAgent();
|
|
15048
|
+
else if (key === "x") {
|
|
15049
|
+
const agent = state.agents[state.agentIndex];
|
|
15050
|
+
if (agent) {
|
|
15051
|
+
await killAgent(agent.backend, agent.session, "");
|
|
15052
|
+
state.message = "killed " + agent.name;
|
|
15053
|
+
}
|
|
15054
|
+
} else if (key === "i") {
|
|
15055
|
+
const agent = state.agents[state.agentIndex];
|
|
15056
|
+
if (agent) await run(runnerFor(agent.backend), buildInterrupt(agent.session));
|
|
15057
|
+
} else if (key === "g") await consent("track");
|
|
15058
|
+
else if (key === "s") await consent("skip");
|
|
15059
|
+
else if (key === "y") await consent("stay");
|
|
15060
|
+
else if (key === "T") await newTab();
|
|
15061
|
+
else if (key === "q" || key === "") {
|
|
15062
|
+
await run(host, ["tmux", "kill-session", "-t", outerSession]);
|
|
15063
|
+
process.exit(0);
|
|
15064
|
+
}
|
|
15065
|
+
await refresh();
|
|
15066
|
+
draw();
|
|
15067
|
+
})();
|
|
15068
|
+
});
|
|
15069
|
+
await refresh();
|
|
15070
|
+
draw();
|
|
15071
|
+
setInterval(() => {
|
|
15072
|
+
void refresh().then(draw);
|
|
15073
|
+
}, POLL_MS);
|
|
15074
|
+
}
|
|
15075
|
+
var POLL_MS, CONDUCTOR_URL;
|
|
15076
|
+
var init_sidebar = __esm({
|
|
15077
|
+
"cli/ui/sidebar.ts"() {
|
|
15078
|
+
"use strict";
|
|
15079
|
+
init_model();
|
|
15080
|
+
init_render();
|
|
15081
|
+
init_consent();
|
|
15082
|
+
init_backend();
|
|
15083
|
+
init_tmux();
|
|
15084
|
+
POLL_MS = 2e3;
|
|
15085
|
+
CONDUCTOR_URL = "http://127.0.0.1:" + (process.env.SYNKRO_HOST_MCP_PORT || "18931");
|
|
15086
|
+
}
|
|
15087
|
+
});
|
|
15088
|
+
|
|
15089
|
+
// cli/commands/ui.ts
|
|
15090
|
+
var ui_exports = {};
|
|
15091
|
+
__export(ui_exports, {
|
|
15092
|
+
uiCommand: () => uiCommand
|
|
15093
|
+
});
|
|
15094
|
+
import { execSync as execSync7 } from "child_process";
|
|
15095
|
+
function repoRoot() {
|
|
15096
|
+
try {
|
|
15097
|
+
return execSync7("git rev-parse --show-toplevel", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim() || process.cwd();
|
|
15098
|
+
} catch {
|
|
15099
|
+
return process.cwd();
|
|
15100
|
+
}
|
|
15101
|
+
}
|
|
15102
|
+
function tmuxPresent() {
|
|
15103
|
+
try {
|
|
15104
|
+
execSync7("tmux -V", { stdio: ["pipe", "pipe", "pipe"] });
|
|
15105
|
+
return true;
|
|
15106
|
+
} catch {
|
|
15107
|
+
return false;
|
|
15108
|
+
}
|
|
15109
|
+
}
|
|
15110
|
+
async function uiCommand(args2) {
|
|
15111
|
+
const bootPath = String(process.argv[1] || "");
|
|
15112
|
+
if (args2.includes("--sidebar")) {
|
|
15113
|
+
await runSidebar();
|
|
15114
|
+
await new Promise(() => {
|
|
15115
|
+
});
|
|
15116
|
+
return;
|
|
15117
|
+
}
|
|
15118
|
+
if (args2.includes("--new-tab")) {
|
|
15119
|
+
await buildTab(bootPath, repoRoot(), "new");
|
|
15120
|
+
return;
|
|
15121
|
+
}
|
|
15122
|
+
if (!tmuxPresent()) {
|
|
15123
|
+
console.error("synkro ui needs tmux. Install it (brew install tmux) and rerun.");
|
|
15124
|
+
process.exitCode = 1;
|
|
15125
|
+
return;
|
|
15126
|
+
}
|
|
15127
|
+
const code = await launchUi(bootPath, repoRoot());
|
|
15128
|
+
process.exitCode = code;
|
|
15129
|
+
}
|
|
15130
|
+
var init_ui = __esm({
|
|
15131
|
+
"cli/commands/ui.ts"() {
|
|
15132
|
+
"use strict";
|
|
15133
|
+
init_launch();
|
|
15134
|
+
init_sidebar();
|
|
15135
|
+
}
|
|
15136
|
+
});
|
|
15137
|
+
|
|
14090
15138
|
// cli/commands/refresh.ts
|
|
14091
15139
|
var refresh_exports = {};
|
|
14092
15140
|
__export(refresh_exports, {
|
|
@@ -14124,11 +15172,11 @@ __export(linear_exports, {
|
|
|
14124
15172
|
linearCommand: () => linearCommand
|
|
14125
15173
|
});
|
|
14126
15174
|
import { readFileSync as readFileSync30 } from "fs";
|
|
14127
|
-
import { homedir as
|
|
14128
|
-
import { join as
|
|
15175
|
+
import { homedir as homedir32 } from "os";
|
|
15176
|
+
import { join as join32 } from "path";
|
|
14129
15177
|
function mcpJwt() {
|
|
14130
15178
|
try {
|
|
14131
|
-
return readFileSync30(
|
|
15179
|
+
return readFileSync30(join32(SYNKRO_DIR14, ".mcp-jwt"), "utf-8").trim();
|
|
14132
15180
|
} catch {
|
|
14133
15181
|
return "";
|
|
14134
15182
|
}
|
|
@@ -14167,7 +15215,7 @@ var SYNKRO_DIR14, PORT2, BASE;
|
|
|
14167
15215
|
var init_linear = __esm({
|
|
14168
15216
|
"cli/commands/linear.ts"() {
|
|
14169
15217
|
"use strict";
|
|
14170
|
-
SYNKRO_DIR14 =
|
|
15218
|
+
SYNKRO_DIR14 = join32(homedir32(), ".synkro");
|
|
14171
15219
|
PORT2 = process.env.SYNKRO_MCP_PORT || "18931";
|
|
14172
15220
|
BASE = `http://127.0.0.1:${PORT2}`;
|
|
14173
15221
|
}
|
|
@@ -14316,33 +15364,33 @@ var init_cveReachability = __esm({
|
|
|
14316
15364
|
});
|
|
14317
15365
|
|
|
14318
15366
|
// cli/reachability/reachabilityScan.ts
|
|
14319
|
-
import { spawnSync as
|
|
14320
|
-
import { readFileSync as readFileSync32, writeFileSync as
|
|
14321
|
-
import { join as
|
|
14322
|
-
import { homedir as
|
|
15367
|
+
import { spawnSync as spawnSync12, execFileSync as execFileSync5 } from "child_process";
|
|
15368
|
+
import { readFileSync as readFileSync32, writeFileSync as writeFileSync23, existsSync as existsSync34, readdirSync as readdirSync9 } from "fs";
|
|
15369
|
+
import { join as join33 } from "path";
|
|
15370
|
+
import { homedir as homedir33 } from "os";
|
|
14323
15371
|
import { createRequire } from "module";
|
|
14324
|
-
function walkSourceFiles(
|
|
15372
|
+
function walkSourceFiles(repoRoot3, maxFiles = 4e3, maxBytes = 5e5) {
|
|
14325
15373
|
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".turbo", "out", ".cache", ".synkro", ".claude", "vendor", "__tests__", "test-results"]);
|
|
14326
15374
|
const EXT = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
14327
15375
|
const files = [];
|
|
14328
|
-
const stack = [
|
|
15376
|
+
const stack = [repoRoot3];
|
|
14329
15377
|
while (stack.length && files.length < maxFiles) {
|
|
14330
15378
|
const dir = stack.pop();
|
|
14331
15379
|
let ents;
|
|
14332
15380
|
try {
|
|
14333
|
-
ents =
|
|
15381
|
+
ents = readdirSync9(dir, { withFileTypes: true });
|
|
14334
15382
|
} catch {
|
|
14335
15383
|
continue;
|
|
14336
15384
|
}
|
|
14337
15385
|
for (const e of ents) {
|
|
14338
15386
|
if (files.length >= maxFiles) break;
|
|
14339
|
-
const full =
|
|
15387
|
+
const full = join33(dir, e.name);
|
|
14340
15388
|
if (e.isDirectory()) {
|
|
14341
15389
|
if (!SKIP.has(e.name) && !e.name.startsWith(".")) stack.push(full);
|
|
14342
15390
|
continue;
|
|
14343
15391
|
}
|
|
14344
15392
|
if (!EXT.test(e.name) || e.name.endsWith(".d.ts")) continue;
|
|
14345
|
-
const rel = full.startsWith(
|
|
15393
|
+
const rel = full.startsWith(repoRoot3 + "/") ? full.slice(repoRoot3.length + 1) : full;
|
|
14346
15394
|
try {
|
|
14347
15395
|
const content = readFileSync32(full, "utf8");
|
|
14348
15396
|
if (content.length <= maxBytes) files.push({ path: rel, content });
|
|
@@ -14360,15 +15408,15 @@ function cleanVersion(spec) {
|
|
|
14360
15408
|
const c = s.replace(/^[\^~>=<\s]+/, "");
|
|
14361
15409
|
return /^\d[\w.\-+]*$/.test(c) ? c : null;
|
|
14362
15410
|
}
|
|
14363
|
-
function gatherManifestVersions(
|
|
15411
|
+
function gatherManifestVersions(repoRoot3) {
|
|
14364
15412
|
const out = {};
|
|
14365
|
-
const dirs = [
|
|
14366
|
-
const pkgsDir =
|
|
14367
|
-
if (
|
|
15413
|
+
const dirs = [repoRoot3];
|
|
15414
|
+
const pkgsDir = join33(repoRoot3, "packages");
|
|
15415
|
+
if (existsSync34(pkgsDir)) {
|
|
14368
15416
|
try {
|
|
14369
|
-
for (const d of
|
|
14370
|
-
const pd =
|
|
14371
|
-
if (
|
|
15417
|
+
for (const d of readdirSync9(pkgsDir)) {
|
|
15418
|
+
const pd = join33(pkgsDir, d);
|
|
15419
|
+
if (existsSync34(join33(pd, "package.json"))) dirs.push(pd);
|
|
14372
15420
|
}
|
|
14373
15421
|
} catch {
|
|
14374
15422
|
}
|
|
@@ -14377,7 +15425,7 @@ function gatherManifestVersions(repoRoot2) {
|
|
|
14377
15425
|
for (const dir of dirs) {
|
|
14378
15426
|
let pkg;
|
|
14379
15427
|
try {
|
|
14380
|
-
pkg = JSON.parse(readFileSync32(
|
|
15428
|
+
pkg = JSON.parse(readFileSync32(join33(dir, "package.json"), "utf8"));
|
|
14381
15429
|
} catch {
|
|
14382
15430
|
continue;
|
|
14383
15431
|
}
|
|
@@ -14393,32 +15441,32 @@ function gatherManifestVersions(repoRoot2) {
|
|
|
14393
15441
|
}
|
|
14394
15442
|
return out;
|
|
14395
15443
|
}
|
|
14396
|
-
function findJelly(
|
|
15444
|
+
function findJelly(repoRoot3) {
|
|
14397
15445
|
try {
|
|
14398
15446
|
const pkgJson = require2.resolve("@cs-au-dk/jelly/package.json");
|
|
14399
15447
|
const dir = pkgJson.slice(0, pkgJson.length - "package.json".length);
|
|
14400
15448
|
const pkg = JSON.parse(readFileSync32(pkgJson, "utf8"));
|
|
14401
15449
|
const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin && (pkg.bin.jelly || pkg.bin[Object.keys(pkg.bin)[0]]);
|
|
14402
15450
|
if (bin) {
|
|
14403
|
-
const p =
|
|
14404
|
-
if (
|
|
15451
|
+
const p = join33(dir, bin);
|
|
15452
|
+
if (existsSync34(p)) return p;
|
|
14405
15453
|
}
|
|
14406
15454
|
} catch {
|
|
14407
15455
|
}
|
|
14408
|
-
for (const base of [
|
|
14409
|
-
const b =
|
|
14410
|
-
if (
|
|
15456
|
+
for (const base of [repoRoot3, process.cwd()]) {
|
|
15457
|
+
const b = join33(base, "node_modules", ".bin", "jelly");
|
|
15458
|
+
if (existsSync34(b)) return b;
|
|
14411
15459
|
}
|
|
14412
15460
|
return null;
|
|
14413
15461
|
}
|
|
14414
|
-
function findEntries(
|
|
14415
|
-
const dirs = [
|
|
14416
|
-
const pkgsDir =
|
|
14417
|
-
if (
|
|
15462
|
+
function findEntries(repoRoot3) {
|
|
15463
|
+
const dirs = [repoRoot3];
|
|
15464
|
+
const pkgsDir = join33(repoRoot3, "packages");
|
|
15465
|
+
if (existsSync34(pkgsDir)) {
|
|
14418
15466
|
try {
|
|
14419
|
-
for (const d of
|
|
14420
|
-
const pd =
|
|
14421
|
-
if (
|
|
15467
|
+
for (const d of readdirSync9(pkgsDir)) {
|
|
15468
|
+
const pd = join33(pkgsDir, d);
|
|
15469
|
+
if (existsSync34(join33(pd, "package.json"))) dirs.push(pd);
|
|
14422
15470
|
}
|
|
14423
15471
|
} catch {
|
|
14424
15472
|
}
|
|
@@ -14426,12 +15474,12 @@ function findEntries(repoRoot2) {
|
|
|
14426
15474
|
const entries = [];
|
|
14427
15475
|
for (const dir of dirs) {
|
|
14428
15476
|
try {
|
|
14429
|
-
const pkg = JSON.parse(readFileSync32(
|
|
15477
|
+
const pkg = JSON.parse(readFileSync32(join33(dir, "package.json"), "utf8"));
|
|
14430
15478
|
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
15479
|
for (const c of cands) {
|
|
14432
15480
|
if (typeof c !== "string") continue;
|
|
14433
|
-
const f =
|
|
14434
|
-
if (
|
|
15481
|
+
const f = join33(dir, c);
|
|
15482
|
+
if (existsSync34(f)) {
|
|
14435
15483
|
entries.push(f);
|
|
14436
15484
|
break;
|
|
14437
15485
|
}
|
|
@@ -14441,9 +15489,9 @@ function findEntries(repoRoot2) {
|
|
|
14441
15489
|
}
|
|
14442
15490
|
return entries.slice(0, 40);
|
|
14443
15491
|
}
|
|
14444
|
-
function currentCommit(
|
|
15492
|
+
function currentCommit(repoRoot3) {
|
|
14445
15493
|
try {
|
|
14446
|
-
return execFileSync5("git", ["rev-parse", "HEAD"], { cwd:
|
|
15494
|
+
return execFileSync5("git", ["rev-parse", "HEAD"], { cwd: repoRoot3, encoding: "utf8" }).trim();
|
|
14447
15495
|
} catch {
|
|
14448
15496
|
return "";
|
|
14449
15497
|
}
|
|
@@ -14462,9 +15510,9 @@ function parseApiUsage(log) {
|
|
|
14462
15510
|
for (const k of Object.keys(map)) out[k] = { reachableApis: [...map[k]].slice(0, 60) };
|
|
14463
15511
|
return out;
|
|
14464
15512
|
}
|
|
14465
|
-
function runReachabilityScan(
|
|
14466
|
-
const commit = currentCommit(
|
|
14467
|
-
if (!opts.force && commit &&
|
|
15513
|
+
function runReachabilityScan(repoRoot3, opts = {}) {
|
|
15514
|
+
const commit = currentCommit(repoRoot3);
|
|
15515
|
+
if (!opts.force && commit && existsSync34(REACHABILITY_PATH)) {
|
|
14468
15516
|
try {
|
|
14469
15517
|
const prev = JSON.parse(readFileSync32(REACHABILITY_PATH, "utf8"));
|
|
14470
15518
|
if (prev.commit === commit) return { ok: true, cached: true, packages: Object.keys(prev.packages || {}).length };
|
|
@@ -14513,13 +15561,13 @@ function runReachabilityScan(repoRoot2, opts = {}) {
|
|
|
14513
15561
|
}
|
|
14514
15562
|
};
|
|
14515
15563
|
let tool = "ast";
|
|
14516
|
-
const jelly = findJelly(
|
|
15564
|
+
const jelly = findJelly(repoRoot3);
|
|
14517
15565
|
if (jelly) {
|
|
14518
|
-
const entries = findEntries(
|
|
15566
|
+
const entries = findEntries(repoRoot3);
|
|
14519
15567
|
if (entries.length > 0) {
|
|
14520
|
-
const r =
|
|
15568
|
+
const r = spawnSync12(
|
|
14521
15569
|
process.execPath,
|
|
14522
|
-
[jelly, "-b",
|
|
15570
|
+
[jelly, "-b", repoRoot3, "--api-usage", ...entries],
|
|
14523
15571
|
{ encoding: "utf8", timeout: opts.timeoutMs ?? 18e4, maxBuffer: 2e8 }
|
|
14524
15572
|
);
|
|
14525
15573
|
const jp = parseApiUsage((r.stdout || "") + (r.stderr || ""));
|
|
@@ -14529,7 +15577,7 @@ function runReachabilityScan(repoRoot2, opts = {}) {
|
|
|
14529
15577
|
}
|
|
14530
15578
|
}
|
|
14531
15579
|
}
|
|
14532
|
-
const astPackages = extractAllPackageUsage(walkSourceFiles(
|
|
15580
|
+
const astPackages = extractAllPackageUsage(walkSourceFiles(repoRoot3));
|
|
14533
15581
|
for (const [k, v] of Object.entries(astPackages)) {
|
|
14534
15582
|
addAll(k, v.apis);
|
|
14535
15583
|
addSites(k, v.sites);
|
|
@@ -14553,9 +15601,9 @@ function runReachabilityScan(repoRoot2, opts = {}) {
|
|
|
14553
15601
|
packages[k] = entry;
|
|
14554
15602
|
}
|
|
14555
15603
|
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(
|
|
15604
|
+
const file = { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), commit, tool, packages, versions: gatherManifestVersions(repoRoot3) };
|
|
14557
15605
|
try {
|
|
14558
|
-
|
|
15606
|
+
writeFileSync23(REACHABILITY_PATH, JSON.stringify(file, null, 2));
|
|
14559
15607
|
} catch (e) {
|
|
14560
15608
|
return { ok: false, reason: "write failed: " + String(e.message || e) };
|
|
14561
15609
|
}
|
|
@@ -14567,7 +15615,7 @@ var init_reachabilityScan = __esm({
|
|
|
14567
15615
|
"use strict";
|
|
14568
15616
|
init_cveReachability();
|
|
14569
15617
|
require2 = createRequire(import.meta.url);
|
|
14570
|
-
REACHABILITY_PATH =
|
|
15618
|
+
REACHABILITY_PATH = join33(homedir33(), ".synkro", "reachability.json");
|
|
14571
15619
|
}
|
|
14572
15620
|
});
|
|
14573
15621
|
|
|
@@ -14576,13 +15624,13 @@ var reachabilityScan_exports = {};
|
|
|
14576
15624
|
__export(reachabilityScan_exports, {
|
|
14577
15625
|
reachabilityScanCommand: () => reachabilityScanCommand
|
|
14578
15626
|
});
|
|
14579
|
-
import { readFileSync as readFileSync33, existsSync as
|
|
14580
|
-
import { join as
|
|
14581
|
-
import { homedir as
|
|
15627
|
+
import { readFileSync as readFileSync33, existsSync as existsSync35 } from "fs";
|
|
15628
|
+
import { join as join34 } from "path";
|
|
15629
|
+
import { homedir as homedir34 } from "os";
|
|
14582
15630
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
14583
15631
|
function readConfigEnv4() {
|
|
14584
|
-
const p =
|
|
14585
|
-
if (!
|
|
15632
|
+
const p = join34(SYNKRO_DIR15, "config.env");
|
|
15633
|
+
if (!existsSync35(p)) return {};
|
|
14586
15634
|
const out = {};
|
|
14587
15635
|
for (const line of readFileSync33(p, "utf-8").split("\n")) {
|
|
14588
15636
|
const t = line.trim();
|
|
@@ -14592,7 +15640,7 @@ function readConfigEnv4() {
|
|
|
14592
15640
|
}
|
|
14593
15641
|
return out;
|
|
14594
15642
|
}
|
|
14595
|
-
function
|
|
15643
|
+
function repoRoot2() {
|
|
14596
15644
|
try {
|
|
14597
15645
|
return execFileSync6("git", ["rev-parse", "--show-toplevel"], { encoding: "utf-8" }).trim();
|
|
14598
15646
|
} catch {
|
|
@@ -14600,14 +15648,14 @@ function repoRoot() {
|
|
|
14600
15648
|
}
|
|
14601
15649
|
}
|
|
14602
15650
|
function repoSlug(root) {
|
|
14603
|
-
const
|
|
15651
|
+
const run2 = (a) => {
|
|
14604
15652
|
try {
|
|
14605
15653
|
return execFileSync6("git", a, { encoding: "utf-8" }).trim();
|
|
14606
15654
|
} catch {
|
|
14607
15655
|
return "";
|
|
14608
15656
|
}
|
|
14609
15657
|
};
|
|
14610
|
-
const remote =
|
|
15658
|
+
const remote = run2(["remote", "get-url", "origin"]);
|
|
14611
15659
|
if (remote) return remote.replace(/^git@[^:]+:/, "").replace(/^https?:\/\/[^/]+\//, "").replace(/\.git$/, "");
|
|
14612
15660
|
return root.split("/").pop() || root;
|
|
14613
15661
|
}
|
|
@@ -14616,10 +15664,10 @@ async function pushToCloud(cfg, repo) {
|
|
|
14616
15664
|
while (gwBase.endsWith("/")) gwBase = gwBase.slice(0, -1);
|
|
14617
15665
|
let jwt2 = "";
|
|
14618
15666
|
try {
|
|
14619
|
-
jwt2 = readFileSync33(
|
|
15667
|
+
jwt2 = readFileSync33(join34(SYNKRO_DIR15, ".mcp-jwt"), "utf-8").trim();
|
|
14620
15668
|
} catch {
|
|
14621
15669
|
}
|
|
14622
|
-
if (!jwt2 || !
|
|
15670
|
+
if (!jwt2 || !existsSync35(REACHABILITY_PATH)) return;
|
|
14623
15671
|
const body = readFileSync33(REACHABILITY_PATH, "utf-8");
|
|
14624
15672
|
try {
|
|
14625
15673
|
const resp = await fetch(gwBase + "/api/v1/reachability?repo=" + encodeURIComponent(repo), {
|
|
@@ -14634,7 +15682,7 @@ async function pushToCloud(cfg, repo) {
|
|
|
14634
15682
|
}
|
|
14635
15683
|
}
|
|
14636
15684
|
async function reachabilityScanCommand(args2 = []) {
|
|
14637
|
-
const root =
|
|
15685
|
+
const root = repoRoot2();
|
|
14638
15686
|
const force = args2.includes("--force");
|
|
14639
15687
|
const quiet = args2.includes("--quiet");
|
|
14640
15688
|
const res = runReachabilityScan(root, { force });
|
|
@@ -14652,7 +15700,7 @@ var init_reachabilityScan2 = __esm({
|
|
|
14652
15700
|
"cli/commands/reachabilityScan.ts"() {
|
|
14653
15701
|
"use strict";
|
|
14654
15702
|
init_reachabilityScan();
|
|
14655
|
-
SYNKRO_DIR15 =
|
|
15703
|
+
SYNKRO_DIR15 = join34(homedir34(), ".synkro");
|
|
14656
15704
|
}
|
|
14657
15705
|
});
|
|
14658
15706
|
|
|
@@ -14782,11 +15830,11 @@ var config_exports = {};
|
|
|
14782
15830
|
__export(config_exports, {
|
|
14783
15831
|
configCommand: () => configCommand
|
|
14784
15832
|
});
|
|
14785
|
-
import { readFileSync as readFileSync34, writeFileSync as
|
|
14786
|
-
import { join as
|
|
14787
|
-
import { homedir as
|
|
15833
|
+
import { readFileSync as readFileSync34, writeFileSync as writeFileSync24, existsSync as existsSync36 } from "fs";
|
|
15834
|
+
import { join as join35 } from "path";
|
|
15835
|
+
import { homedir as homedir35 } from "os";
|
|
14788
15836
|
function readConfigEnv5() {
|
|
14789
|
-
if (!
|
|
15837
|
+
if (!existsSync36(CONFIG_PATH9)) return {};
|
|
14790
15838
|
const out = {};
|
|
14791
15839
|
for (const line of readFileSync34(CONFIG_PATH9, "utf-8").split("\n")) {
|
|
14792
15840
|
const t = line.trim();
|
|
@@ -14797,7 +15845,7 @@ function readConfigEnv5() {
|
|
|
14797
15845
|
return out;
|
|
14798
15846
|
}
|
|
14799
15847
|
function updateConfigValue(key, value) {
|
|
14800
|
-
if (!
|
|
15848
|
+
if (!existsSync36(CONFIG_PATH9)) {
|
|
14801
15849
|
console.error("No config found. Run `synkro install` first.");
|
|
14802
15850
|
process.exit(1);
|
|
14803
15851
|
}
|
|
@@ -14812,7 +15860,7 @@ function updateConfigValue(key, value) {
|
|
|
14812
15860
|
return line;
|
|
14813
15861
|
});
|
|
14814
15862
|
if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
|
|
14815
|
-
|
|
15863
|
+
writeFileSync24(CONFIG_PATH9, updated.join("\n"), "utf-8");
|
|
14816
15864
|
}
|
|
14817
15865
|
function resolveInferenceMode(cfg) {
|
|
14818
15866
|
if ((cfg.SYNKRO_GRADING_MODE || "local") === "byok") return "byok";
|
|
@@ -14970,8 +16018,8 @@ var init_config = __esm({
|
|
|
14970
16018
|
"use strict";
|
|
14971
16019
|
init_stub();
|
|
14972
16020
|
init_optout();
|
|
14973
|
-
SYNKRO_DIR16 =
|
|
14974
|
-
CONFIG_PATH9 =
|
|
16021
|
+
SYNKRO_DIR16 = join35(homedir35(), ".synkro");
|
|
16022
|
+
CONFIG_PATH9 = join35(SYNKRO_DIR16, "config.env");
|
|
14975
16023
|
}
|
|
14976
16024
|
});
|
|
14977
16025
|
|
|
@@ -15035,11 +16083,11 @@ async function printTail(args2) {
|
|
|
15035
16083
|
console.log("(no events \u2014 run `synkro start` if the container is down so JSONL pending events can drain)");
|
|
15036
16084
|
return;
|
|
15037
16085
|
}
|
|
15038
|
-
for (const
|
|
15039
|
-
const session =
|
|
15040
|
-
const ts = typeof
|
|
15041
|
-
console.log(` ${ts} ${
|
|
15042
|
-
const contextStr = typeof
|
|
16086
|
+
for (const row2 of rows) {
|
|
16087
|
+
const session = row2.cc_session_id ? ` session=${String(row2.cc_session_id).slice(0, 8)}` : "";
|
|
16088
|
+
const ts = typeof row2.occurred_at === "string" ? row2.occurred_at : new Date(row2.occurred_at).toISOString();
|
|
16089
|
+
console.log(` ${ts} ${row2.event_type.padEnd(20)} ${row2.emitter}${session}`);
|
|
16090
|
+
const contextStr = typeof row2.context === "string" ? row2.context : JSON.stringify(row2.context);
|
|
15043
16091
|
console.log(` context: ${truncate2(contextStr, 200)}`);
|
|
15044
16092
|
}
|
|
15045
16093
|
}
|
|
@@ -15161,27 +16209,27 @@ Usage:
|
|
|
15161
16209
|
|
|
15162
16210
|
// cli/inventory/identity.ts
|
|
15163
16211
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
15164
|
-
import { existsSync as
|
|
15165
|
-
import { homedir as
|
|
15166
|
-
import { dirname as dirname9, join as
|
|
16212
|
+
import { existsSync as existsSync37, mkdirSync as mkdirSync20, readFileSync as readFileSync35, renameSync as renameSync9, writeFileSync as writeFileSync25 } from "fs";
|
|
16213
|
+
import { homedir as homedir36 } from "os";
|
|
16214
|
+
import { dirname as dirname9, join as join36 } from "path";
|
|
15167
16215
|
function operationalIdentityPath() {
|
|
15168
|
-
return process.env.SYNKRO_OPERATIONAL_IDENTITY_PATH ||
|
|
16216
|
+
return process.env.SYNKRO_OPERATIONAL_IDENTITY_PATH || join36(homedir36(), ".synkro", "installation.json");
|
|
15169
16217
|
}
|
|
15170
16218
|
function validIdentity(value) {
|
|
15171
16219
|
if (!value || typeof value !== "object") return false;
|
|
15172
|
-
const
|
|
15173
|
-
return typeof
|
|
16220
|
+
const row2 = value;
|
|
16221
|
+
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
16222
|
}
|
|
15175
16223
|
function writeIdentity(path, identity) {
|
|
15176
|
-
|
|
16224
|
+
mkdirSync20(dirname9(path), { recursive: true, mode: 448 });
|
|
15177
16225
|
const temp = `${path}.${process.pid}.${randomUUID5()}.tmp`;
|
|
15178
|
-
|
|
16226
|
+
writeFileSync25(temp, JSON.stringify(identity, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
15179
16227
|
renameSync9(temp, path);
|
|
15180
16228
|
}
|
|
15181
16229
|
function getOperationalInstallationIdentity(path = operationalIdentityPath()) {
|
|
15182
16230
|
const prior = cached4.get(path);
|
|
15183
16231
|
if (prior) return prior;
|
|
15184
|
-
if (
|
|
16232
|
+
if (existsSync37(path)) {
|
|
15185
16233
|
try {
|
|
15186
16234
|
const parsed = JSON.parse(readFileSync35(path, "utf8"));
|
|
15187
16235
|
if (validIdentity(parsed)) {
|
|
@@ -15208,13 +16256,13 @@ var init_identity2 = __esm({
|
|
|
15208
16256
|
// cli/inventory/collector.ts
|
|
15209
16257
|
import { createHash as createHash5 } from "crypto";
|
|
15210
16258
|
import {
|
|
15211
|
-
existsSync as
|
|
16259
|
+
existsSync as existsSync38,
|
|
15212
16260
|
readFileSync as readFileSync36,
|
|
15213
|
-
readdirSync as
|
|
16261
|
+
readdirSync as readdirSync10,
|
|
15214
16262
|
statSync as statSync5
|
|
15215
16263
|
} from "fs";
|
|
15216
|
-
import { arch, homedir as
|
|
15217
|
-
import { basename as basename3, join as
|
|
16264
|
+
import { arch, homedir as homedir37, hostname as hostname2, platform as platform5, release } from "os";
|
|
16265
|
+
import { basename as basename3, join as join37, relative, resolve as resolve5 } from "path";
|
|
15218
16266
|
import { fileURLToPath } from "url";
|
|
15219
16267
|
function sha256(value) {
|
|
15220
16268
|
return createHash5("sha256").update(value).digest("hex");
|
|
@@ -15224,14 +16272,14 @@ function pseudonymousHostnameHash(installationId, host) {
|
|
|
15224
16272
|
}
|
|
15225
16273
|
function cliVersion() {
|
|
15226
16274
|
try {
|
|
15227
|
-
return "1.
|
|
16275
|
+
return "1.9.0";
|
|
15228
16276
|
} catch {
|
|
15229
16277
|
return "0.0.0";
|
|
15230
16278
|
}
|
|
15231
16279
|
}
|
|
15232
16280
|
function readJson(path) {
|
|
15233
16281
|
try {
|
|
15234
|
-
if (!
|
|
16282
|
+
if (!existsSync38(path)) return null;
|
|
15235
16283
|
const parsed = JSON.parse(readFileSync36(path, "utf8"));
|
|
15236
16284
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
15237
16285
|
} catch {
|
|
@@ -15240,7 +16288,7 @@ function readJson(path) {
|
|
|
15240
16288
|
}
|
|
15241
16289
|
function readText(path) {
|
|
15242
16290
|
try {
|
|
15243
|
-
if (!
|
|
16291
|
+
if (!existsSync38(path)) return "";
|
|
15244
16292
|
return readFileSync36(path, "utf8");
|
|
15245
16293
|
} catch {
|
|
15246
16294
|
return "";
|
|
@@ -15327,16 +16375,16 @@ function mcpArtifactsFromJson(harness, config, configScope = "user") {
|
|
|
15327
16375
|
}
|
|
15328
16376
|
function claudeDesktopConfigCandidates(home, targetPlatform) {
|
|
15329
16377
|
if (targetPlatform === "darwin") {
|
|
15330
|
-
return [
|
|
16378
|
+
return [join37(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")];
|
|
15331
16379
|
}
|
|
15332
16380
|
if (targetPlatform === "linux") {
|
|
15333
16381
|
return [
|
|
15334
|
-
|
|
15335
|
-
|
|
16382
|
+
join37(home, ".config", "Claude", "claude_desktop_config.json"),
|
|
16383
|
+
join37(home, ".config", "claude", "claude_desktop_config.json")
|
|
15336
16384
|
];
|
|
15337
16385
|
}
|
|
15338
16386
|
if (targetPlatform === "win32" && process.env.APPDATA) {
|
|
15339
|
-
return [
|
|
16387
|
+
return [join37(process.env.APPDATA, "Claude", "claude_desktop_config.json")];
|
|
15340
16388
|
}
|
|
15341
16389
|
return [];
|
|
15342
16390
|
}
|
|
@@ -15344,7 +16392,7 @@ function claudeManagedMcpConfigCandidates(targetPlatform) {
|
|
|
15344
16392
|
if (targetPlatform === "darwin") return ["/Library/Application Support/ClaudeCode/managed-mcp.json"];
|
|
15345
16393
|
if (targetPlatform === "linux") return ["/etc/claude-code/managed-mcp.json"];
|
|
15346
16394
|
if (targetPlatform === "win32" && process.env.ProgramFiles) {
|
|
15347
|
-
return [
|
|
16395
|
+
return [join37(process.env.ProgramFiles, "ClaudeCode", "managed-mcp.json")];
|
|
15348
16396
|
}
|
|
15349
16397
|
return [];
|
|
15350
16398
|
}
|
|
@@ -15353,7 +16401,7 @@ function discoveredProjectRoots(claudeState, currentDirectory, explicit = [], cu
|
|
|
15353
16401
|
const add = (value) => {
|
|
15354
16402
|
if (typeof value !== "string" || !value.trim()) return;
|
|
15355
16403
|
const path = resolve5(value);
|
|
15356
|
-
if (
|
|
16404
|
+
if (existsSync38(path)) roots.add(path);
|
|
15357
16405
|
};
|
|
15358
16406
|
add(currentDirectory);
|
|
15359
16407
|
for (const path of explicit) add(path);
|
|
@@ -15364,31 +16412,31 @@ function discoveredProjectRoots(claudeState, currentDirectory, explicit = [], cu
|
|
|
15364
16412
|
return [...roots];
|
|
15365
16413
|
}
|
|
15366
16414
|
function cursorWorkspaceStorageCandidates(home, targetPlatform) {
|
|
15367
|
-
if (targetPlatform === "darwin") return [
|
|
15368
|
-
if (targetPlatform === "linux") return [
|
|
16415
|
+
if (targetPlatform === "darwin") return [join37(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage")];
|
|
16416
|
+
if (targetPlatform === "linux") return [join37(home, ".config", "Cursor", "User", "workspaceStorage")];
|
|
15369
16417
|
if (targetPlatform === "win32" && process.env.APPDATA) {
|
|
15370
|
-
return [
|
|
16418
|
+
return [join37(process.env.APPDATA, "Cursor", "User", "workspaceStorage")];
|
|
15371
16419
|
}
|
|
15372
16420
|
return [];
|
|
15373
16421
|
}
|
|
15374
16422
|
function cursorWorkspaceRoots(home, targetPlatform) {
|
|
15375
16423
|
const roots = /* @__PURE__ */ new Set();
|
|
15376
16424
|
for (const storage of cursorWorkspaceStorageCandidates(home, targetPlatform)) {
|
|
15377
|
-
if (!
|
|
16425
|
+
if (!existsSync38(storage)) continue;
|
|
15378
16426
|
let entries = [];
|
|
15379
16427
|
try {
|
|
15380
|
-
entries =
|
|
16428
|
+
entries = readdirSync10(storage, { withFileTypes: true });
|
|
15381
16429
|
} catch {
|
|
15382
16430
|
continue;
|
|
15383
16431
|
}
|
|
15384
16432
|
for (const entry of entries) {
|
|
15385
16433
|
if (!entry.isDirectory() || entry.isSymbolicLink?.()) continue;
|
|
15386
|
-
const state = readJson(
|
|
16434
|
+
const state = readJson(join37(storage, entry.name, "workspace.json"));
|
|
15387
16435
|
const raw = state?.folder;
|
|
15388
16436
|
if (typeof raw !== "string" || !raw.trim()) continue;
|
|
15389
16437
|
try {
|
|
15390
16438
|
const path = raw.startsWith("file:") ? fileURLToPath(raw) : raw;
|
|
15391
|
-
if (
|
|
16439
|
+
if (existsSync38(path)) roots.add(resolve5(path));
|
|
15392
16440
|
} catch {
|
|
15393
16441
|
}
|
|
15394
16442
|
}
|
|
@@ -15482,18 +16530,18 @@ function parseFrontmatter(content) {
|
|
|
15482
16530
|
return { name: value("name"), version: value("version") };
|
|
15483
16531
|
}
|
|
15484
16532
|
function skillArtifacts(harness, root) {
|
|
15485
|
-
if (!
|
|
16533
|
+
if (!existsSync38(root)) return [];
|
|
15486
16534
|
const manifests = [];
|
|
15487
16535
|
const visit = (dir) => {
|
|
15488
16536
|
let entries;
|
|
15489
16537
|
try {
|
|
15490
|
-
entries =
|
|
16538
|
+
entries = readdirSync10(dir, { withFileTypes: true });
|
|
15491
16539
|
} catch {
|
|
15492
16540
|
return;
|
|
15493
16541
|
}
|
|
15494
16542
|
for (const entry of entries) {
|
|
15495
16543
|
if (entry.isSymbolicLink?.()) continue;
|
|
15496
|
-
const path =
|
|
16544
|
+
const path = join37(dir, entry.name);
|
|
15497
16545
|
if (entry.isFile() && entry.name === "SKILL.md") manifests.push(path);
|
|
15498
16546
|
else if (entry.isDirectory()) visit(path);
|
|
15499
16547
|
}
|
|
@@ -15503,7 +16551,7 @@ function skillArtifacts(harness, root) {
|
|
|
15503
16551
|
const content = readText(path);
|
|
15504
16552
|
const frontmatter = parseFrontmatter(content);
|
|
15505
16553
|
const rel = relative(root, path).replaceAll("\\", "/");
|
|
15506
|
-
const name = frontmatter.name || basename3(
|
|
16554
|
+
const name = frontmatter.name || basename3(join37(path, "..")) || "skill";
|
|
15507
16555
|
return {
|
|
15508
16556
|
harness,
|
|
15509
16557
|
type: "skill",
|
|
@@ -15518,16 +16566,16 @@ function skillArtifacts(harness, root) {
|
|
|
15518
16566
|
});
|
|
15519
16567
|
}
|
|
15520
16568
|
function cursorExtensionArtifacts(root) {
|
|
15521
|
-
if (!
|
|
16569
|
+
if (!existsSync38(root)) return [];
|
|
15522
16570
|
let dirs = [];
|
|
15523
16571
|
try {
|
|
15524
|
-
dirs =
|
|
16572
|
+
dirs = readdirSync10(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.isSymbolicLink());
|
|
15525
16573
|
} catch {
|
|
15526
16574
|
return [];
|
|
15527
16575
|
}
|
|
15528
16576
|
const artifacts = [];
|
|
15529
16577
|
for (const dir of dirs) {
|
|
15530
|
-
const pkg = readJson(
|
|
16578
|
+
const pkg = readJson(join37(root, dir.name, "package.json"));
|
|
15531
16579
|
if (!pkg) continue;
|
|
15532
16580
|
const publisher = typeof pkg.publisher === "string" ? pkg.publisher : void 0;
|
|
15533
16581
|
const name = typeof pkg.name === "string" ? pkg.name : dir.name;
|
|
@@ -15547,18 +16595,18 @@ function cursorExtensionArtifacts(root) {
|
|
|
15547
16595
|
return artifacts;
|
|
15548
16596
|
}
|
|
15549
16597
|
function deploymentMode2(home) {
|
|
15550
|
-
const raw = readText(
|
|
16598
|
+
const raw = readText(join37(home, ".synkro", "config.env"));
|
|
15551
16599
|
const value = (key) => raw.match(new RegExp(`^${key}=['"]?([^'"\\n]*)`, "m"))?.[1]?.toLowerCase();
|
|
15552
16600
|
if (value("SYNKRO_GRADING_MODE") === "byok") return "byok";
|
|
15553
16601
|
if (value("SYNKRO_STORAGE_MODE") === "cloud") return "cloud";
|
|
15554
16602
|
return "local";
|
|
15555
16603
|
}
|
|
15556
16604
|
function telemetryHealth(home) {
|
|
15557
|
-
const meta = readJson(
|
|
16605
|
+
const meta = readJson(join37(home, ".synkro", "telemetry-meta.json"));
|
|
15558
16606
|
const health = {};
|
|
15559
16607
|
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
16608
|
if (meta?.last_flush_error) health.telemetry_last_error = "flush_failed";
|
|
15561
|
-
const queue =
|
|
16609
|
+
const queue = join37(home, ".synkro", "telemetry-pending.jsonl");
|
|
15562
16610
|
try {
|
|
15563
16611
|
const size = statSync5(queue).size;
|
|
15564
16612
|
health.telemetry_backlog = size <= 5 * 1024 * 1024 ? readFileSync36(queue, "utf8").split("\n").filter(Boolean).length : Math.ceil(size / 1024);
|
|
@@ -15602,7 +16650,7 @@ function harnessSnapshot(agent) {
|
|
|
15602
16650
|
}
|
|
15603
16651
|
const config = readJson(agent.settingsPath);
|
|
15604
16652
|
const coverage = inspectCodexHooks(agent.settingsPath);
|
|
15605
|
-
const toml = readText(
|
|
16653
|
+
const toml = readText(join37(agent.configDir, "config.toml"));
|
|
15606
16654
|
const permission = toml.match(/^\s*approval_policy\s*=\s*["']([^"']+)/m)?.[1];
|
|
15607
16655
|
return {
|
|
15608
16656
|
row: {
|
|
@@ -15619,19 +16667,19 @@ function harnessSnapshot(agent) {
|
|
|
15619
16667
|
};
|
|
15620
16668
|
}
|
|
15621
16669
|
function collectOperationalInventory(options = {}) {
|
|
15622
|
-
const home = options.homeDir ??
|
|
16670
|
+
const home = options.homeDir ?? homedir37();
|
|
15623
16671
|
const detected = options.detectedAgents ?? detectAgents();
|
|
15624
16672
|
const identity = getOperationalInstallationIdentity(options.identityPath);
|
|
15625
16673
|
const targetPlatform = options.platformName ?? platform5();
|
|
15626
|
-
const codexHome = options.homeDir ?
|
|
16674
|
+
const codexHome = options.homeDir ? join37(home, ".codex") : process.env.CODEX_HOME || join37(home, ".codex");
|
|
15627
16675
|
const harnesses = [];
|
|
15628
16676
|
const artifacts = [];
|
|
15629
16677
|
for (const agent of detected) {
|
|
15630
|
-
const { row, config } = harnessSnapshot(agent);
|
|
15631
|
-
harnesses.push(
|
|
15632
|
-
artifacts.push(...hookArtifacts(
|
|
16678
|
+
const { row: row2, config } = harnessSnapshot(agent);
|
|
16679
|
+
harnesses.push(row2);
|
|
16680
|
+
artifacts.push(...hookArtifacts(row2.harness, config));
|
|
15633
16681
|
}
|
|
15634
|
-
const claudeJson = readJson(
|
|
16682
|
+
const claudeJson = readJson(join37(home, ".claude.json"));
|
|
15635
16683
|
artifacts.push(...mcpArtifactsFromJson("claude_code", claudeJson));
|
|
15636
16684
|
if (claudeJson?.projects && typeof claudeJson.projects === "object") {
|
|
15637
16685
|
for (const [projectPath, project] of Object.entries(claudeJson.projects)) {
|
|
@@ -15639,8 +16687,8 @@ function collectOperationalInventory(options = {}) {
|
|
|
15639
16687
|
artifacts.push(...mcpArtifactsFromJson("claude_code", project, `local:${sha256(projectPath).slice(0, 16)}`));
|
|
15640
16688
|
}
|
|
15641
16689
|
}
|
|
15642
|
-
artifacts.push(...mcpArtifactsFromJson("cursor", readJson(
|
|
15643
|
-
artifacts.push(...codexMcpArtifacts(readText(
|
|
16690
|
+
artifacts.push(...mcpArtifactsFromJson("cursor", readJson(join37(home, ".cursor", "mcp.json"))));
|
|
16691
|
+
artifacts.push(...codexMcpArtifacts(readText(join37(codexHome, "config.toml"))));
|
|
15644
16692
|
const projectRoots = discoveredProjectRoots(
|
|
15645
16693
|
claudeJson,
|
|
15646
16694
|
options.currentDirectory ?? process.cwd(),
|
|
@@ -15651,11 +16699,11 @@ function collectOperationalInventory(options = {}) {
|
|
|
15651
16699
|
const scopeHash = sha256(projectRoot).slice(0, 16);
|
|
15652
16700
|
artifacts.push(...mcpArtifactsFromJson(
|
|
15653
16701
|
"claude_code",
|
|
15654
|
-
readJson(
|
|
16702
|
+
readJson(join37(projectRoot, ".mcp.json")),
|
|
15655
16703
|
`project:${scopeHash}`
|
|
15656
16704
|
));
|
|
15657
|
-
const cursorProjectConfig =
|
|
15658
|
-
if (resolve5(cursorProjectConfig) !== resolve5(
|
|
16705
|
+
const cursorProjectConfig = join37(projectRoot, ".cursor", "mcp.json");
|
|
16706
|
+
if (resolve5(cursorProjectConfig) !== resolve5(join37(home, ".cursor", "mcp.json"))) {
|
|
15659
16707
|
artifacts.push(...mcpArtifactsFromJson(
|
|
15660
16708
|
"cursor",
|
|
15661
16709
|
readJson(cursorProjectConfig),
|
|
@@ -15666,7 +16714,7 @@ function collectOperationalInventory(options = {}) {
|
|
|
15666
16714
|
for (const managedPath of claudeManagedMcpConfigCandidates(targetPlatform)) {
|
|
15667
16715
|
artifacts.push(...mcpArtifactsFromJson("claude_code", readJson(managedPath), "managed"));
|
|
15668
16716
|
}
|
|
15669
|
-
const desktopConfigPath = claudeDesktopConfigCandidates(home, targetPlatform).find((path) =>
|
|
16717
|
+
const desktopConfigPath = claudeDesktopConfigCandidates(home, targetPlatform).find((path) => existsSync38(path));
|
|
15670
16718
|
if (desktopConfigPath) {
|
|
15671
16719
|
const desktopConfig = readJson(desktopConfigPath);
|
|
15672
16720
|
harnesses.push({
|
|
@@ -15677,7 +16725,7 @@ function collectOperationalInventory(options = {}) {
|
|
|
15677
16725
|
});
|
|
15678
16726
|
artifacts.push(...mcpArtifactsFromJson("claude_desktop", desktopConfig));
|
|
15679
16727
|
}
|
|
15680
|
-
const claudeSettings = readJson(
|
|
16728
|
+
const claudeSettings = readJson(join37(home, ".claude", "settings.json"));
|
|
15681
16729
|
if (claudeSettings?.enabledPlugins && typeof claudeSettings.enabledPlugins === "object") {
|
|
15682
16730
|
for (const [name, enabled] of Object.entries(claudeSettings.enabledPlugins)) {
|
|
15683
16731
|
artifacts.push({
|
|
@@ -15691,19 +16739,19 @@ function collectOperationalInventory(options = {}) {
|
|
|
15691
16739
|
});
|
|
15692
16740
|
}
|
|
15693
16741
|
}
|
|
15694
|
-
artifacts.push(...skillArtifacts("claude_code",
|
|
15695
|
-
artifacts.push(...skillArtifacts("cursor",
|
|
15696
|
-
artifacts.push(...skillArtifacts("codex",
|
|
15697
|
-
artifacts.push(...cursorExtensionArtifacts(
|
|
16742
|
+
artifacts.push(...skillArtifacts("claude_code", join37(home, ".claude", "skills")));
|
|
16743
|
+
artifacts.push(...skillArtifacts("cursor", join37(home, ".cursor", "skills")));
|
|
16744
|
+
artifacts.push(...skillArtifacts("codex", join37(codexHome, "skills")));
|
|
16745
|
+
artifacts.push(...cursorExtensionArtifacts(join37(home, ".cursor", "extensions")));
|
|
15698
16746
|
const uniqueArtifacts = /* @__PURE__ */ new Map();
|
|
15699
16747
|
for (const artifact of artifacts) {
|
|
15700
16748
|
const key = `${artifact.harness || "global"}:${artifact.type}:${artifact.canonical_id}`;
|
|
15701
16749
|
uniqueArtifacts.set(key, artifact);
|
|
15702
16750
|
}
|
|
15703
|
-
const codingHarnesses = harnesses.filter((
|
|
16751
|
+
const codingHarnesses = harnesses.filter((row2) => row2.harness === "claude_code" || row2.harness === "cursor" || row2.harness === "codex");
|
|
15704
16752
|
const health = telemetryHealth(home) ?? {};
|
|
15705
16753
|
health.scanners = {
|
|
15706
|
-
hook_runtime: codingHarnesses.length === 0 ? "unknown" : codingHarnesses.every((
|
|
16754
|
+
hook_runtime: codingHarnesses.length === 0 ? "unknown" : codingHarnesses.every((row2) => row2.enabled) ? "ok" : "degraded"
|
|
15707
16755
|
};
|
|
15708
16756
|
return {
|
|
15709
16757
|
schema_version: 1,
|
|
@@ -15749,16 +16797,16 @@ __export(sync_exports2, {
|
|
|
15749
16797
|
import { createHash as createHash6, randomUUID as randomUUID6 } from "crypto";
|
|
15750
16798
|
import { spawn as spawn8 } from "child_process";
|
|
15751
16799
|
import {
|
|
15752
|
-
existsSync as
|
|
15753
|
-
mkdirSync as
|
|
16800
|
+
existsSync as existsSync39,
|
|
16801
|
+
mkdirSync as mkdirSync21,
|
|
15754
16802
|
readFileSync as readFileSync37,
|
|
15755
16803
|
renameSync as renameSync10,
|
|
15756
|
-
writeFileSync as
|
|
16804
|
+
writeFileSync as writeFileSync26
|
|
15757
16805
|
} from "fs";
|
|
15758
|
-
import { homedir as
|
|
15759
|
-
import { dirname as dirname10, join as
|
|
16806
|
+
import { homedir as homedir38 } from "os";
|
|
16807
|
+
import { dirname as dirname10, join as join38 } from "path";
|
|
15760
16808
|
function syncStatePath() {
|
|
15761
|
-
return process.env.SYNKRO_INVENTORY_SYNC_STATE_PATH ||
|
|
16809
|
+
return process.env.SYNKRO_INVENTORY_SYNC_STATE_PATH || join38(homedir38(), ".synkro", "inventory-sync.json");
|
|
15762
16810
|
}
|
|
15763
16811
|
function readState(path = syncStatePath()) {
|
|
15764
16812
|
try {
|
|
@@ -15770,9 +16818,9 @@ function readState(path = syncStatePath()) {
|
|
|
15770
16818
|
}
|
|
15771
16819
|
function writeState(state, path = syncStatePath()) {
|
|
15772
16820
|
try {
|
|
15773
|
-
|
|
16821
|
+
mkdirSync21(dirname10(path), { recursive: true, mode: 448 });
|
|
15774
16822
|
const temp = `${path}.${process.pid}.tmp`;
|
|
15775
|
-
|
|
16823
|
+
writeFileSync26(temp, JSON.stringify(state, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
15776
16824
|
renameSync10(temp, path);
|
|
15777
16825
|
} catch {
|
|
15778
16826
|
}
|
|
@@ -15785,7 +16833,7 @@ function shouldSyncInventory(state, now = Date.now(), target) {
|
|
|
15785
16833
|
return !Number.isFinite(lastAttempt) || lastAttempt <= 0 || now - lastAttempt >= FAILURE_RETRY_MS;
|
|
15786
16834
|
}
|
|
15787
16835
|
function readConfig() {
|
|
15788
|
-
const path =
|
|
16836
|
+
const path = join38(homedir38(), ".synkro", "config.env");
|
|
15789
16837
|
const out = {};
|
|
15790
16838
|
try {
|
|
15791
16839
|
for (const rawLine of readFileSync37(path, "utf8").split("\n")) {
|
|
@@ -15827,7 +16875,7 @@ function resolveInventoryGateway(raw) {
|
|
|
15827
16875
|
}
|
|
15828
16876
|
async function loadToken() {
|
|
15829
16877
|
try {
|
|
15830
|
-
const durable = readFileSync37(
|
|
16878
|
+
const durable = readFileSync37(join38(homedir38(), ".synkro", ".mcp-jwt"), "utf8").trim();
|
|
15831
16879
|
if (durable) return durable;
|
|
15832
16880
|
} catch {
|
|
15833
16881
|
}
|
|
@@ -15945,7 +16993,7 @@ function syncOperationalInventoryDetached() {
|
|
|
15945
16993
|
writeState({ ...state, last_attempt_at: (/* @__PURE__ */ new Date()).toISOString(), last_target: target }, path);
|
|
15946
16994
|
try {
|
|
15947
16995
|
const script = process.argv[1];
|
|
15948
|
-
if (!script || !
|
|
16996
|
+
if (!script || !existsSync39(script)) return;
|
|
15949
16997
|
const child = spawn8(process.execPath, [script, "inventory-sync", "--detached"], {
|
|
15950
16998
|
detached: true,
|
|
15951
16999
|
stdio: "ignore",
|
|
@@ -15969,13 +17017,13 @@ var init_sync2 = __esm({
|
|
|
15969
17017
|
});
|
|
15970
17018
|
|
|
15971
17019
|
// cli/bootstrap.js
|
|
15972
|
-
import { readFileSync as readFileSync38, existsSync as
|
|
17020
|
+
import { readFileSync as readFileSync38, existsSync as existsSync40 } from "fs";
|
|
15973
17021
|
import { resolve as resolve6 } from "path";
|
|
15974
17022
|
var envCandidates = [
|
|
15975
17023
|
resolve6(process.env.HOME ?? "", ".synkro", "config.env")
|
|
15976
17024
|
];
|
|
15977
17025
|
for (const envPath of envCandidates) {
|
|
15978
|
-
if (!
|
|
17026
|
+
if (!existsSync40(envPath)) continue;
|
|
15979
17027
|
const envContent = readFileSync38(envPath, "utf-8");
|
|
15980
17028
|
for (const line of envContent.split("\n")) {
|
|
15981
17029
|
const trimmed = line.trim();
|
|
@@ -15993,7 +17041,7 @@ var subArgs = args.slice(1);
|
|
|
15993
17041
|
var isDetachedChild = process.env.SYNKRO_TELEMETRY_DETACHED === "1";
|
|
15994
17042
|
var FLUSH_SKIP = /* @__PURE__ */ new Set(["grade", "inventory-sync", "version", "--version", "-v", "help", "--help", "-h", ""]);
|
|
15995
17043
|
function printVersion() {
|
|
15996
|
-
console.log("1.
|
|
17044
|
+
console.log("1.9.0");
|
|
15997
17045
|
}
|
|
15998
17046
|
function printHelp2() {
|
|
15999
17047
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|
|
@@ -16012,9 +17060,16 @@ Commands:
|
|
|
16012
17060
|
claude-desktop Monitor Claude Desktop conversations (local, macOS)
|
|
16013
17061
|
telemetry <sub> Inspect or flush local telemetry events
|
|
16014
17062
|
whoami Show resolved identity + where grading runs
|
|
17063
|
+
workspace <sub> Answer the task-workspace question (stay/clear/status)
|
|
17064
|
+
ui Governed multiplexer: spaces + agents, real sessions in tabs
|
|
16015
17065
|
refresh Refresh the login session (keeps you signed in; run on a schedule)
|
|
16016
17066
|
version Show version
|
|
16017
17067
|
|
|
17068
|
+
workspace:
|
|
17069
|
+
synkro workspace stay <taskId> keep working in the current checkout
|
|
17070
|
+
synkro workspace clear <taskId> forget the choice (Synkro asks again)
|
|
17071
|
+
synkro workspace status [taskId] show recorded choices
|
|
17072
|
+
|
|
16018
17073
|
config:
|
|
16019
17074
|
synkro config show current settings
|
|
16020
17075
|
synkro config grading <local|byok> where grading runs
|
|
@@ -16118,6 +17173,16 @@ async function main() {
|
|
|
16118
17173
|
await whoamiCommand2(subArgs);
|
|
16119
17174
|
break;
|
|
16120
17175
|
}
|
|
17176
|
+
case "workspace": {
|
|
17177
|
+
const { workspaceCommand: workspaceCommand2 } = await Promise.resolve().then(() => (init_workspace(), workspace_exports));
|
|
17178
|
+
await workspaceCommand2(subArgs);
|
|
17179
|
+
break;
|
|
17180
|
+
}
|
|
17181
|
+
case "ui": {
|
|
17182
|
+
const { uiCommand: uiCommand2 } = await Promise.resolve().then(() => (init_ui(), ui_exports));
|
|
17183
|
+
await uiCommand2(subArgs);
|
|
17184
|
+
break;
|
|
17185
|
+
}
|
|
16121
17186
|
case "refresh": {
|
|
16122
17187
|
const { refreshCommand: refreshCommand2 } = await Promise.resolve().then(() => (init_refresh(), refresh_exports));
|
|
16123
17188
|
await refreshCommand2(subArgs);
|
|
@@ -16230,7 +17295,7 @@ async function shutdown(code) {
|
|
|
16230
17295
|
}, 200);
|
|
16231
17296
|
force.unref();
|
|
16232
17297
|
}
|
|
16233
|
-
main().then(() => postDispatchFlush()).then(() => shutdown(0)).catch(async (err) => {
|
|
17298
|
+
main().then(() => postDispatchFlush()).then(() => shutdown(typeof process.exitCode === "number" ? process.exitCode : 0)).catch(async (err) => {
|
|
16234
17299
|
try {
|
|
16235
17300
|
const { emit: emit2 } = await Promise.resolve().then(() => (init_telemetry(), telemetry_exports));
|
|
16236
17301
|
emit2("error", {
|