@primitive.ai/prim 0.1.0-alpha.15 → 0.1.0-alpha.16
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/SKILL.md +47 -7
- package/dist/{chunk-SHLF6OL2.js → chunk-6SIEWWUL.js} +27 -34
- package/dist/chunk-BEEGFDGU.js +59 -0
- package/dist/chunk-JZGWQDM5.js +199 -0
- package/dist/chunk-PTLXSXIY.js +111 -0
- package/dist/chunk-S47B4VGC.js +122 -0
- package/dist/chunk-UTKQTZHL.js +88 -0
- package/dist/daemon/server.js +241 -0
- package/dist/hooks/post-tool-use.js +128 -0
- package/dist/hooks/pre-commit.js +18 -3
- package/dist/hooks/pre-tool-use.js +220 -0
- package/dist/hooks/prim-hook.js +51 -0
- package/dist/hooks/session-end.js +61 -0
- package/dist/hooks/session-start.js +61 -0
- package/dist/index.js +1442 -58
- package/package.json +11 -5
package/dist/index.js
CHANGED
|
@@ -1,20 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
color,
|
|
4
|
+
colorForArea,
|
|
5
|
+
stripAnsi
|
|
6
|
+
} from "./chunk-BEEGFDGU.js";
|
|
7
|
+
import {
|
|
8
|
+
checkAffectedDecisions,
|
|
9
|
+
formatDecisionsWarning,
|
|
10
|
+
getGitContext
|
|
11
|
+
} from "./chunk-S47B4VGC.js";
|
|
12
|
+
import {
|
|
13
|
+
HttpError,
|
|
3
14
|
REFRESH_TOKEN_PATH,
|
|
4
15
|
TOKEN_EXPIRES_PATH,
|
|
5
16
|
TOKEN_FILE_PATH,
|
|
6
17
|
getAuthToken,
|
|
7
18
|
getClient,
|
|
8
|
-
getGitContext,
|
|
9
19
|
getSiteUrl,
|
|
10
20
|
getTokenExpiresAt,
|
|
11
21
|
saveTokenExpiry
|
|
12
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-6SIEWWUL.js";
|
|
23
|
+
import {
|
|
24
|
+
JOURNAL_DIR,
|
|
25
|
+
SESSIONS_DIR,
|
|
26
|
+
bucketStats,
|
|
27
|
+
listBuckets,
|
|
28
|
+
readMovesFromPath
|
|
29
|
+
} from "./chunk-JZGWQDM5.js";
|
|
30
|
+
import {
|
|
31
|
+
daemonIsLive,
|
|
32
|
+
daemonRequest
|
|
33
|
+
} from "./chunk-UTKQTZHL.js";
|
|
13
34
|
|
|
14
35
|
// src/index.ts
|
|
15
|
-
import { readFileSync as
|
|
16
|
-
import { dirname as
|
|
17
|
-
import { fileURLToPath as
|
|
36
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
37
|
+
import { dirname as dirname5, resolve as resolve4 } from "path";
|
|
38
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
18
39
|
import { Command } from "commander";
|
|
19
40
|
import updateNotifier from "update-notifier";
|
|
20
41
|
|
|
@@ -112,10 +133,10 @@ function registerAuthCommands(program2) {
|
|
|
112
133
|
process.exit(1);
|
|
113
134
|
});
|
|
114
135
|
});
|
|
115
|
-
const port = await new Promise((
|
|
136
|
+
const port = await new Promise((resolve5) => {
|
|
116
137
|
server.listen(CALLBACK_PORT, LOCALHOST, () => {
|
|
117
138
|
const addr = server.address();
|
|
118
|
-
|
|
139
|
+
resolve5(typeof addr === "object" && addr ? addr.port : 0);
|
|
119
140
|
});
|
|
120
141
|
});
|
|
121
142
|
const redirectUri = `http://${LOCALHOST}:${port}/callback`;
|
|
@@ -256,8 +277,263 @@ async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
|
|
|
256
277
|
return data.access_token;
|
|
257
278
|
}
|
|
258
279
|
|
|
280
|
+
// src/commands/claude-install.ts
|
|
281
|
+
import {
|
|
282
|
+
closeSync,
|
|
283
|
+
existsSync as existsSync2,
|
|
284
|
+
fsyncSync,
|
|
285
|
+
mkdirSync as mkdirSync2,
|
|
286
|
+
openSync,
|
|
287
|
+
readFileSync as readFileSync2,
|
|
288
|
+
renameSync,
|
|
289
|
+
writeFileSync as writeFileSync2
|
|
290
|
+
} from "fs";
|
|
291
|
+
import { homedir } from "os";
|
|
292
|
+
import { dirname as dirname2, join } from "path";
|
|
293
|
+
var CAPTURE_COMMAND = "prim-hook";
|
|
294
|
+
var GATE_COMMAND = "prim-pre-tool-use";
|
|
295
|
+
var POST_TOOL_USE_COMMAND = "prim-post-tool-use";
|
|
296
|
+
var SESSION_START_COMMAND = "prim-session-start";
|
|
297
|
+
var SESSION_END_COMMAND = "prim-session-end";
|
|
298
|
+
var STATUSLINE_COMMAND = "prim statusline";
|
|
299
|
+
var STATUSLINE_REFRESH_SECONDS = 5;
|
|
300
|
+
var PRIM_COMMANDS = /* @__PURE__ */ new Set([
|
|
301
|
+
CAPTURE_COMMAND,
|
|
302
|
+
GATE_COMMAND,
|
|
303
|
+
POST_TOOL_USE_COMMAND,
|
|
304
|
+
SESSION_START_COMMAND,
|
|
305
|
+
SESSION_END_COMMAND
|
|
306
|
+
]);
|
|
307
|
+
var JSON_INDENT = 2;
|
|
308
|
+
var USER_SCOPE_PATH = join(homedir(), ".claude", "settings.json");
|
|
309
|
+
var PROJECT_SCOPE_PATH = join(process.cwd(), ".claude", "settings.json");
|
|
310
|
+
var CAPTURE_EVENTS = [
|
|
311
|
+
"SessionStart",
|
|
312
|
+
"UserPromptSubmit",
|
|
313
|
+
"PreToolUse",
|
|
314
|
+
"PostToolUse",
|
|
315
|
+
"Stop",
|
|
316
|
+
"SessionEnd",
|
|
317
|
+
"SubagentStop"
|
|
318
|
+
];
|
|
319
|
+
var REGISTRATIONS = [
|
|
320
|
+
...CAPTURE_EVENTS.map((event) => ({ event, matcher: "*", command: CAPTURE_COMMAND })),
|
|
321
|
+
{ event: "PreToolUse", matcher: "Edit|Write|MultiEdit", command: GATE_COMMAND },
|
|
322
|
+
{ event: "PostToolUse", matcher: "Edit|Write|MultiEdit", command: POST_TOOL_USE_COMMAND },
|
|
323
|
+
{ event: "SessionStart", matcher: "*", command: SESSION_START_COMMAND },
|
|
324
|
+
{ event: "SessionEnd", matcher: "*", command: SESSION_END_COMMAND }
|
|
325
|
+
];
|
|
326
|
+
function settingsPathFor(scope) {
|
|
327
|
+
return scope === "user" ? USER_SCOPE_PATH : PROJECT_SCOPE_PATH;
|
|
328
|
+
}
|
|
329
|
+
function readSettings(path) {
|
|
330
|
+
if (!existsSync2(path)) {
|
|
331
|
+
return {};
|
|
332
|
+
}
|
|
333
|
+
const raw = readFileSync2(path, "utf-8");
|
|
334
|
+
try {
|
|
335
|
+
return JSON.parse(raw);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
338
|
+
throw new Error(`${path} is not valid JSON: ${detail}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function entryHasCommand(entry, command) {
|
|
342
|
+
return entry.hooks?.some((h) => h.command === command) ?? false;
|
|
343
|
+
}
|
|
344
|
+
function canonicalEntry(reg) {
|
|
345
|
+
return { matcher: reg.matcher, hooks: [{ type: "command", command: reg.command }] };
|
|
346
|
+
}
|
|
347
|
+
function stripCommand(list, command) {
|
|
348
|
+
const out = [];
|
|
349
|
+
for (const e of list) {
|
|
350
|
+
const hooks = (e.hooks ?? []).filter((h) => h.command !== command);
|
|
351
|
+
if (hooks.length > 0) {
|
|
352
|
+
out.push({ ...e, hooks });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
function ensureRegistration(list, reg, force) {
|
|
358
|
+
const hasCanonical = list.some(
|
|
359
|
+
(e) => e.matcher === reg.matcher && e.hooks?.length === 1 && e.hooks[0].command === reg.command
|
|
360
|
+
);
|
|
361
|
+
if (hasCanonical && !force) {
|
|
362
|
+
return list;
|
|
363
|
+
}
|
|
364
|
+
return [...stripCommand(list, reg.command), canonicalEntry(reg)];
|
|
365
|
+
}
|
|
366
|
+
function isPrimStatusLine(settings) {
|
|
367
|
+
const s = settings.statusLine;
|
|
368
|
+
return s?.type === "command" && s?.command === STATUSLINE_COMMAND;
|
|
369
|
+
}
|
|
370
|
+
function applyStatusLine(settings) {
|
|
371
|
+
if (settings.statusLine && !isPrimStatusLine(settings)) {
|
|
372
|
+
return settings;
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
...settings,
|
|
376
|
+
statusLine: {
|
|
377
|
+
type: "command",
|
|
378
|
+
command: STATUSLINE_COMMAND,
|
|
379
|
+
refreshInterval: STATUSLINE_REFRESH_SECONDS
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function applyInstall(settings, options = {}) {
|
|
384
|
+
const hooks = { ...settings.hooks ?? {} };
|
|
385
|
+
for (const reg of REGISTRATIONS) {
|
|
386
|
+
hooks[reg.event] = ensureRegistration(hooks[reg.event] ?? [], reg, options.force ?? false);
|
|
387
|
+
}
|
|
388
|
+
return applyStatusLine({ ...settings, hooks });
|
|
389
|
+
}
|
|
390
|
+
function applyUninstall(settings) {
|
|
391
|
+
const source = settings.hooks ?? {};
|
|
392
|
+
const hooks = {};
|
|
393
|
+
for (const event of Object.keys(source)) {
|
|
394
|
+
let list = source[event] ?? [];
|
|
395
|
+
for (const command of PRIM_COMMANDS) {
|
|
396
|
+
list = stripCommand(list, command);
|
|
397
|
+
}
|
|
398
|
+
if (list.length > 0) {
|
|
399
|
+
hooks[event] = list;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const next = { ...settings, hooks };
|
|
403
|
+
if (isPrimStatusLine(next)) {
|
|
404
|
+
next.statusLine = void 0;
|
|
405
|
+
}
|
|
406
|
+
return next;
|
|
407
|
+
}
|
|
408
|
+
function captureInstalled(settings) {
|
|
409
|
+
return CAPTURE_EVENTS.some(
|
|
410
|
+
(event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_COMMAND))
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
function statuslineInstalled(settings) {
|
|
414
|
+
return isPrimStatusLine(settings);
|
|
415
|
+
}
|
|
416
|
+
function isGateInstalled(settings) {
|
|
417
|
+
return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_COMMAND));
|
|
418
|
+
}
|
|
419
|
+
function atomicWrite(path, content) {
|
|
420
|
+
const dir = dirname2(path);
|
|
421
|
+
if (!existsSync2(dir)) {
|
|
422
|
+
mkdirSync2(dir, { recursive: true });
|
|
423
|
+
}
|
|
424
|
+
const tmp = `${path}.tmp.${String(Date.now())}`;
|
|
425
|
+
writeFileSync2(tmp, `${JSON.stringify(content, null, JSON_INDENT)}
|
|
426
|
+
`, "utf-8");
|
|
427
|
+
const fd = openSync(tmp, "r+");
|
|
428
|
+
try {
|
|
429
|
+
fsyncSync(fd);
|
|
430
|
+
} finally {
|
|
431
|
+
closeSync(fd);
|
|
432
|
+
}
|
|
433
|
+
renameSync(tmp, path);
|
|
434
|
+
}
|
|
435
|
+
function performInstall(scope, force) {
|
|
436
|
+
const path = settingsPathFor(scope);
|
|
437
|
+
const before = readSettings(path);
|
|
438
|
+
const after = applyInstall(before, { force });
|
|
439
|
+
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
440
|
+
if (changed) {
|
|
441
|
+
atomicWrite(path, after);
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
scope,
|
|
445
|
+
path,
|
|
446
|
+
gate: isGateInstalled(after),
|
|
447
|
+
capture: captureInstalled(after),
|
|
448
|
+
statusline: statuslineInstalled(after),
|
|
449
|
+
changed
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
function performUninstall(scope) {
|
|
453
|
+
const path = settingsPathFor(scope);
|
|
454
|
+
const before = readSettings(path);
|
|
455
|
+
const after = applyUninstall(before);
|
|
456
|
+
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
457
|
+
if (changed) {
|
|
458
|
+
atomicWrite(path, after);
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
scope,
|
|
462
|
+
path,
|
|
463
|
+
gate: isGateInstalled(after),
|
|
464
|
+
capture: captureInstalled(after),
|
|
465
|
+
statusline: statuslineInstalled(after),
|
|
466
|
+
changed
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function performStatus() {
|
|
470
|
+
const statusFor = (path) => {
|
|
471
|
+
const settings = readSettings(path);
|
|
472
|
+
return {
|
|
473
|
+
path,
|
|
474
|
+
gate: isGateInstalled(settings),
|
|
475
|
+
capture: captureInstalled(settings),
|
|
476
|
+
statusline: statuslineInstalled(settings)
|
|
477
|
+
};
|
|
478
|
+
};
|
|
479
|
+
return { user: statusFor(USER_SCOPE_PATH), project: statusFor(PROJECT_SCOPE_PATH) };
|
|
480
|
+
}
|
|
481
|
+
function resolveScope(input) {
|
|
482
|
+
if (input === void 0 || input === "user") {
|
|
483
|
+
return "user";
|
|
484
|
+
}
|
|
485
|
+
if (input === "project") {
|
|
486
|
+
return "project";
|
|
487
|
+
}
|
|
488
|
+
console.error(`[prim] unknown --scope "${input}" (expected: user or project)`);
|
|
489
|
+
process.exit(1);
|
|
490
|
+
}
|
|
491
|
+
function registerClaudeCommands(program2) {
|
|
492
|
+
const claude = program2.command("claude").description("Manage the prim Claude Code integration (capture, gate, ingest, presence)");
|
|
493
|
+
claude.command("install").description("Register the prim hooks + statusline in Claude Code's settings.json").option(
|
|
494
|
+
"--scope <scope>",
|
|
495
|
+
"user (default, ~/.claude/settings.json) or project (./.claude/settings.json)"
|
|
496
|
+
).option("--force", "Replace any drifted prim hook entries").action((opts) => {
|
|
497
|
+
const scope = resolveScope(opts.scope);
|
|
498
|
+
const result = performInstall(scope, opts.force ?? false);
|
|
499
|
+
if (result.changed) {
|
|
500
|
+
console.error(
|
|
501
|
+
`[prim] Claude Code integration installed (${scope} scope) at ${result.path}`
|
|
502
|
+
);
|
|
503
|
+
} else {
|
|
504
|
+
console.error(
|
|
505
|
+
`[prim] Claude Code integration already present at ${result.path} (no changes)`
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
console.log(JSON.stringify(result, null, JSON_INDENT));
|
|
509
|
+
});
|
|
510
|
+
claude.command("uninstall").description("Remove all prim hooks + the prim statusline from settings.json").option(
|
|
511
|
+
"--scope <scope>",
|
|
512
|
+
"user (default, ~/.claude/settings.json) or project (./.claude/settings.json)"
|
|
513
|
+
).action((opts) => {
|
|
514
|
+
const scope = resolveScope(opts.scope);
|
|
515
|
+
const result = performUninstall(scope);
|
|
516
|
+
if (result.changed) {
|
|
517
|
+
console.error(`[prim] prim hooks removed from ${result.path}`);
|
|
518
|
+
} else {
|
|
519
|
+
console.error(`[prim] no prim hooks to remove at ${result.path} (nothing changed)`);
|
|
520
|
+
}
|
|
521
|
+
console.log(JSON.stringify(result, null, JSON_INDENT));
|
|
522
|
+
});
|
|
523
|
+
claude.command("status").description(
|
|
524
|
+
"Report whether each prim surface (gate, capture, statusline) is installed per scope"
|
|
525
|
+
).action(() => {
|
|
526
|
+
const result = performStatus();
|
|
527
|
+
const mark = (b) => b ? "\u2713" : "\u2717";
|
|
528
|
+
const line = (label, s) => `[prim] ${label}: gate ${mark(s.gate)} \xB7 capture ${mark(s.capture)} \xB7 statusline ${mark(s.statusline)} (${s.path})`;
|
|
529
|
+
console.error(`${line("user", result.user)}
|
|
530
|
+
${line("project", result.project)}`);
|
|
531
|
+
console.log(JSON.stringify(result, null, JSON_INDENT));
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
259
535
|
// src/commands/context.ts
|
|
260
|
-
import { readFileSync as
|
|
536
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
261
537
|
function registerContextCommands(program2) {
|
|
262
538
|
const context = program2.command("context").description("Manage contexts");
|
|
263
539
|
context.command("list").description("List contexts").option("-s, --scope <scope>", "Filter by scope: project, global, external").option("-t, --project-id <projectId>", "List contexts linked to a specific project").option("--json", "Output as JSON").action(async (opts) => {
|
|
@@ -286,7 +562,7 @@ function registerContextCommands(program2) {
|
|
|
286
562
|
const client = getClient();
|
|
287
563
|
let text = opts.text;
|
|
288
564
|
if (opts.file) {
|
|
289
|
-
text =
|
|
565
|
+
text = readFileSync3(opts.file, "utf-8");
|
|
290
566
|
}
|
|
291
567
|
const taskIds = opts.projectId ? opts.projectId.split(",").map((id) => id.trim()) : void 0;
|
|
292
568
|
const result = await client.post("/api/cli/contexts", {
|
|
@@ -309,7 +585,7 @@ function registerContextCommands(program2) {
|
|
|
309
585
|
const client = getClient();
|
|
310
586
|
let text = opts.text;
|
|
311
587
|
if (opts.file) {
|
|
312
|
-
text =
|
|
588
|
+
text = readFileSync3(opts.file, "utf-8");
|
|
313
589
|
}
|
|
314
590
|
await client.patch(`/api/cli/contexts/${contextId}`, {
|
|
315
591
|
name: opts.name,
|
|
@@ -373,9 +649,772 @@ function printContextList(contexts) {
|
|
|
373
649
|
${contexts.length} context(s)`);
|
|
374
650
|
}
|
|
375
651
|
|
|
652
|
+
// src/commands/daemon.ts
|
|
653
|
+
import { spawn } from "child_process";
|
|
654
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
655
|
+
import { homedir as homedir2 } from "os";
|
|
656
|
+
import { join as join2 } from "path";
|
|
657
|
+
var DAEMON_BIN = "prim-daemon-server";
|
|
658
|
+
var PID_PATH = join2(homedir2(), ".config", "prim", "daemon.pid");
|
|
659
|
+
var SOCK_PATH = join2(homedir2(), ".config", "prim", "sock");
|
|
660
|
+
var STOP_TIMEOUT_MS = 5e3;
|
|
661
|
+
var STOP_POLL_MS = 100;
|
|
662
|
+
var STATUS_PROBE_TIMEOUT_MS = 500;
|
|
663
|
+
var POST_START_WAIT_MS = 400;
|
|
664
|
+
var EXIT_NOT_RUNNING = 2;
|
|
665
|
+
function readPidfile() {
|
|
666
|
+
if (!existsSync3(PID_PATH)) {
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
const raw = readFileSync4(PID_PATH, "utf-8").trim();
|
|
670
|
+
const pid = Number(raw);
|
|
671
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
return { pid, alive: processIsAlive(pid) };
|
|
675
|
+
}
|
|
676
|
+
function processIsAlive(pid) {
|
|
677
|
+
try {
|
|
678
|
+
process.kill(pid, 0);
|
|
679
|
+
return true;
|
|
680
|
+
} catch {
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
function clearStaleArtifacts() {
|
|
685
|
+
try {
|
|
686
|
+
unlinkSync(PID_PATH);
|
|
687
|
+
} catch {
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
unlinkSync(SOCK_PATH);
|
|
691
|
+
} catch {
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
function sleep(ms) {
|
|
695
|
+
return new Promise((resolve5) => {
|
|
696
|
+
const timer = setTimeout(resolve5, ms);
|
|
697
|
+
timer.unref();
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
async function daemonStart(opts) {
|
|
701
|
+
const existing = readPidfile();
|
|
702
|
+
if (existing?.alive) {
|
|
703
|
+
process.stderr.write(`[prim] daemon already running (pid=${existing.pid})
|
|
704
|
+
`);
|
|
705
|
+
console.log(JSON.stringify({ started: false, pid: existing.pid }, null, 2));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (existing && !existing.alive) {
|
|
709
|
+
clearStaleArtifacts();
|
|
710
|
+
}
|
|
711
|
+
if (opts.foreground) {
|
|
712
|
+
const child2 = spawn(DAEMON_BIN, [], { stdio: "inherit" });
|
|
713
|
+
child2.on("exit", (code) => {
|
|
714
|
+
process.exit(code ?? 0);
|
|
715
|
+
});
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const child = spawn(DAEMON_BIN, [], {
|
|
719
|
+
detached: true,
|
|
720
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
721
|
+
});
|
|
722
|
+
child.unref();
|
|
723
|
+
await sleep(POST_START_WAIT_MS);
|
|
724
|
+
const after = readPidfile();
|
|
725
|
+
if (after?.alive) {
|
|
726
|
+
process.stderr.write(`[prim] daemon started (pid=${after.pid}, socket=${SOCK_PATH})
|
|
727
|
+
`);
|
|
728
|
+
console.log(JSON.stringify({ started: true, pid: after.pid }, null, 2));
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
process.stderr.write(
|
|
732
|
+
"[prim] daemon start: bin spawned but no pidfile observed (check that `prim-daemon-server` is on PATH)\n"
|
|
733
|
+
);
|
|
734
|
+
console.log(JSON.stringify({ started: false }, null, 2));
|
|
735
|
+
}
|
|
736
|
+
async function daemonStop() {
|
|
737
|
+
const existing = readPidfile();
|
|
738
|
+
if (!existing) {
|
|
739
|
+
process.stderr.write("[prim] daemon not running (no pidfile)\n");
|
|
740
|
+
console.log(JSON.stringify({ stopped: false, wasRunning: false }, null, 2));
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (!existing.alive) {
|
|
744
|
+
clearStaleArtifacts();
|
|
745
|
+
process.stderr.write("[prim] daemon not running (cleared stale pidfile)\n");
|
|
746
|
+
console.log(JSON.stringify({ stopped: false, wasRunning: false }, null, 2));
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
try {
|
|
750
|
+
process.kill(existing.pid, "SIGTERM");
|
|
751
|
+
} catch (err) {
|
|
752
|
+
process.stderr.write(
|
|
753
|
+
`[prim] could not signal pid=${existing.pid}: ${err instanceof Error ? err.message : String(err)}
|
|
754
|
+
`
|
|
755
|
+
);
|
|
756
|
+
console.log(JSON.stringify({ stopped: false, pid: existing.pid }, null, 2));
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const deadline = Date.now() + STOP_TIMEOUT_MS;
|
|
760
|
+
while (Date.now() < deadline) {
|
|
761
|
+
if (!processIsAlive(existing.pid)) {
|
|
762
|
+
clearStaleArtifacts();
|
|
763
|
+
process.stderr.write(`[prim] daemon stopped (pid=${existing.pid})
|
|
764
|
+
`);
|
|
765
|
+
console.log(JSON.stringify({ stopped: true, pid: existing.pid }, null, 2));
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
await sleep(STOP_POLL_MS);
|
|
769
|
+
}
|
|
770
|
+
process.stderr.write(
|
|
771
|
+
`[prim] daemon did not exit within ${STOP_TIMEOUT_MS}ms (pid=${existing.pid} still alive)
|
|
772
|
+
`
|
|
773
|
+
);
|
|
774
|
+
console.log(JSON.stringify({ stopped: false, pid: existing.pid }, null, 2));
|
|
775
|
+
}
|
|
776
|
+
async function daemonStatus() {
|
|
777
|
+
const pid = readPidfile();
|
|
778
|
+
if (!pid?.alive) {
|
|
779
|
+
process.stderr.write("[prim] \u2717 daemon down\n");
|
|
780
|
+
console.log(JSON.stringify({ running: false }, null, 2));
|
|
781
|
+
if (!process.exitCode) {
|
|
782
|
+
process.exitCode = EXIT_NOT_RUNNING;
|
|
783
|
+
}
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const live = await daemonIsLive(STATUS_PROBE_TIMEOUT_MS);
|
|
787
|
+
if (!live) {
|
|
788
|
+
process.stderr.write(`[prim] \u2717 daemon pid=${pid.pid} alive but socket not responding
|
|
789
|
+
`);
|
|
790
|
+
console.log(JSON.stringify({ running: true, responding: false, pid: pid.pid }, null, 2));
|
|
791
|
+
if (!process.exitCode) {
|
|
792
|
+
process.exitCode = EXIT_NOT_RUNNING;
|
|
793
|
+
}
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const snapshot = await daemonRequest(
|
|
797
|
+
"status_snapshot",
|
|
798
|
+
{},
|
|
799
|
+
{ timeoutMs: STATUS_PROBE_TIMEOUT_MS }
|
|
800
|
+
);
|
|
801
|
+
if (!snapshot) {
|
|
802
|
+
process.stderr.write("[prim] \u2713 daemon live (no snapshot)\n");
|
|
803
|
+
console.log(JSON.stringify({ running: true, responding: true }, null, 2));
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
process.stderr.write(
|
|
807
|
+
`[prim] \u2713 daemon live \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(snapshot.uptimeMs / 1e3)}s \xB7 session=${snapshot.sessionId}
|
|
808
|
+
`
|
|
809
|
+
);
|
|
810
|
+
console.log(JSON.stringify({ running: true, responding: true, ...snapshot }, null, 2));
|
|
811
|
+
}
|
|
812
|
+
async function daemonRestart(opts) {
|
|
813
|
+
await daemonStop();
|
|
814
|
+
await daemonStart(opts);
|
|
815
|
+
}
|
|
816
|
+
function registerDaemonCommands(program2) {
|
|
817
|
+
const daemon = program2.command("daemon").description("Manage the prim companion daemon (latency unlock + presence + broadcast)");
|
|
818
|
+
daemon.command("start").description("Spawn the prim-daemon-server in the background").option("--foreground", "Run in the foreground (inherit stdio); use under launchd / systemd").action(async (opts) => {
|
|
819
|
+
await daemonStart(opts);
|
|
820
|
+
});
|
|
821
|
+
daemon.command("stop").description("Send SIGTERM to the running daemon and clean up the socket").action(async () => {
|
|
822
|
+
await daemonStop();
|
|
823
|
+
});
|
|
824
|
+
daemon.command("status").description("Report daemon liveness + a snapshot if responding").action(async () => {
|
|
825
|
+
await daemonStatus();
|
|
826
|
+
});
|
|
827
|
+
daemon.command("restart").description("Stop, then start (preserves no state today)").option("--foreground", "Restart in the foreground").action(async (opts) => {
|
|
828
|
+
await daemonRestart(opts);
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// src/decisions/cascade-renderer.ts
|
|
833
|
+
var DEPENDENTS_INLINE_LIMIT = 5;
|
|
834
|
+
var KNOWLEDGE_INLINE_LIMIT = 4;
|
|
835
|
+
var ISO_DATE_LENGTH = 10;
|
|
836
|
+
var INTENT_TRUNC = 60;
|
|
837
|
+
var DEFAULT_WIDTH = 80;
|
|
838
|
+
var SOFT_WRAP_INDENT = " ";
|
|
839
|
+
function terminalWidth() {
|
|
840
|
+
return process.stdout.columns ?? DEFAULT_WIDTH;
|
|
841
|
+
}
|
|
842
|
+
function softWrap(line, opts) {
|
|
843
|
+
const width = opts?.width ?? terminalWidth();
|
|
844
|
+
const indent = opts?.indent ?? "";
|
|
845
|
+
if (stripAnsi(line).length <= width) {
|
|
846
|
+
return [line];
|
|
847
|
+
}
|
|
848
|
+
const words = line.split(" ");
|
|
849
|
+
const out = [];
|
|
850
|
+
let current = "";
|
|
851
|
+
for (const w of words) {
|
|
852
|
+
if (current === "") {
|
|
853
|
+
current = w;
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
const tentative = `${current} ${w}`;
|
|
857
|
+
if (stripAnsi(tentative).length > width) {
|
|
858
|
+
out.push(current);
|
|
859
|
+
current = `${indent}${w}`;
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
current = tentative;
|
|
863
|
+
}
|
|
864
|
+
if (current.length > 0) {
|
|
865
|
+
out.push(current);
|
|
866
|
+
}
|
|
867
|
+
return out;
|
|
868
|
+
}
|
|
869
|
+
function formatDate(ms) {
|
|
870
|
+
return new Date(ms).toISOString().slice(0, ISO_DATE_LENGTH);
|
|
871
|
+
}
|
|
872
|
+
function truncate(s, max) {
|
|
873
|
+
if (s.length <= max) {
|
|
874
|
+
return s;
|
|
875
|
+
}
|
|
876
|
+
return `${s.slice(0, max - 1)}\u2026`;
|
|
877
|
+
}
|
|
878
|
+
function bracketed(label) {
|
|
879
|
+
return `[${label}]`;
|
|
880
|
+
}
|
|
881
|
+
function knowledgeRow(files, contexts, triggerFile, triggerContextName) {
|
|
882
|
+
const tokens = [];
|
|
883
|
+
for (const ctx of contexts.slice(0, KNOWLEDGE_INLINE_LIMIT)) {
|
|
884
|
+
const star = ctx.name === triggerContextName ? " *" : "";
|
|
885
|
+
tokens.push(bracketed(`${ctx.name}${star}`));
|
|
886
|
+
}
|
|
887
|
+
for (const f of files.slice(0, Math.max(0, KNOWLEDGE_INLINE_LIMIT - contexts.length))) {
|
|
888
|
+
const star = f === triggerFile ? " *" : "";
|
|
889
|
+
tokens.push(bracketed(`${f}${star}`));
|
|
890
|
+
}
|
|
891
|
+
const overflow = files.length + contexts.length - tokens.length > 0 ? ` (+${String(files.length + contexts.length - tokens.length)} more)` : "";
|
|
892
|
+
if (tokens.length === 0) {
|
|
893
|
+
return [" (no upstream knowledge refs)"];
|
|
894
|
+
}
|
|
895
|
+
return [` ${tokens.join(" ")}${overflow}`];
|
|
896
|
+
}
|
|
897
|
+
function areaChip(area) {
|
|
898
|
+
if (!area) {
|
|
899
|
+
return color("[--]", "gray");
|
|
900
|
+
}
|
|
901
|
+
return color(`[${area}]`, colorForArea(area));
|
|
902
|
+
}
|
|
903
|
+
function countCrossAreaDependents(parentArea, dependents) {
|
|
904
|
+
if (!parentArea) {
|
|
905
|
+
const areaCounts = /* @__PURE__ */ new Map();
|
|
906
|
+
for (const d of dependents) {
|
|
907
|
+
if (d.area) {
|
|
908
|
+
areaCounts.set(d.area, (areaCounts.get(d.area) ?? 0) + 1);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (areaCounts.size <= 1) {
|
|
912
|
+
return 0;
|
|
913
|
+
}
|
|
914
|
+
let dominantCount = 0;
|
|
915
|
+
for (const c of areaCounts.values()) {
|
|
916
|
+
if (c > dominantCount) {
|
|
917
|
+
dominantCount = c;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return dependents.filter((d) => d.area).length - dominantCount;
|
|
921
|
+
}
|
|
922
|
+
let count = 0;
|
|
923
|
+
for (const d of dependents) {
|
|
924
|
+
if (d.area && d.area !== parentArea) {
|
|
925
|
+
count++;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return count;
|
|
929
|
+
}
|
|
930
|
+
function dependentsBox(dependents) {
|
|
931
|
+
if (dependents.length === 0) {
|
|
932
|
+
return [" (no downstream dependents)"];
|
|
933
|
+
}
|
|
934
|
+
const inlineCount = Math.min(dependents.length, DEPENDENTS_INLINE_LIMIT);
|
|
935
|
+
const header = `${String(dependents.length)} affected:`;
|
|
936
|
+
const lines = [` ${header}`];
|
|
937
|
+
for (const d of dependents.slice(0, inlineCount)) {
|
|
938
|
+
lines.push(` \u2022 ${areaChip(d.area)} ${truncate(d.intent, INTENT_TRUNC)}`);
|
|
939
|
+
}
|
|
940
|
+
if (dependents.length > inlineCount) {
|
|
941
|
+
lines.push(` + ${String(dependents.length - inlineCount)} more`);
|
|
942
|
+
}
|
|
943
|
+
return lines;
|
|
944
|
+
}
|
|
945
|
+
function triggerHeadline(t) {
|
|
946
|
+
const at = formatDate(t.flaggedAt);
|
|
947
|
+
if (t.type === "file_edit" && t.file) {
|
|
948
|
+
return `trigger: file '${t.file}' was edited; cascade fired at ${at}.`;
|
|
949
|
+
}
|
|
950
|
+
if (t.type === "context_edit" && t.contextName) {
|
|
951
|
+
return `trigger: context '${t.contextName}' was edited; cascade fired at ${at}.`;
|
|
952
|
+
}
|
|
953
|
+
if (t.type === "supersession") {
|
|
954
|
+
return `trigger: an upstream decision was superseded; cascade fired at ${at}.`;
|
|
955
|
+
}
|
|
956
|
+
if (t.type === "invalidation") {
|
|
957
|
+
return `trigger: an upstream decision was invalidated; cascade fired at ${at}.`;
|
|
958
|
+
}
|
|
959
|
+
if (t.type === "confirmation_request") {
|
|
960
|
+
return `trigger: asking-policy confirmation request opened at ${at}.`;
|
|
961
|
+
}
|
|
962
|
+
return `trigger: ${t.type} at ${at}.`;
|
|
963
|
+
}
|
|
964
|
+
function triggerLine(result) {
|
|
965
|
+
const t = result.trigger;
|
|
966
|
+
if (!t) {
|
|
967
|
+
return [];
|
|
968
|
+
}
|
|
969
|
+
const lines = [triggerHeadline(t)];
|
|
970
|
+
if (t.authorName) {
|
|
971
|
+
lines.push(` by ${t.authorName}`);
|
|
972
|
+
}
|
|
973
|
+
if (t.reason) {
|
|
974
|
+
lines.push(` reason: ${t.reason}`);
|
|
975
|
+
}
|
|
976
|
+
return lines;
|
|
977
|
+
}
|
|
978
|
+
function renderCascade(result) {
|
|
979
|
+
const d = result.decision;
|
|
980
|
+
const id = d.shortId ? `dec_${d.shortId}` : d.id;
|
|
981
|
+
const idColored = color(id, "orange");
|
|
982
|
+
const header = `what this would break \xB7 ${String(result.fanOut)} decision(s) \xB7 enforcing`;
|
|
983
|
+
const lines = [header, "", "knowledge"];
|
|
984
|
+
lines.push(
|
|
985
|
+
...knowledgeRow(
|
|
986
|
+
result.upstream.files,
|
|
987
|
+
result.upstream.contexts,
|
|
988
|
+
result.trigger?.file,
|
|
989
|
+
result.trigger?.contextName
|
|
990
|
+
)
|
|
991
|
+
);
|
|
992
|
+
if (result.trigger && (result.trigger.file || result.trigger.contextName)) {
|
|
993
|
+
lines.push(" |");
|
|
994
|
+
lines.push(" | refs (just edited)");
|
|
995
|
+
lines.push(" \u25BC");
|
|
996
|
+
}
|
|
997
|
+
const decisionLine = `\u2022 ${idColored} ${truncate(d.intent, INTENT_TRUNC)}`;
|
|
998
|
+
const fanOutFragment = result.fanOut > 0 ? ` \xB7 ${String(result.fanOut)} decision(s) depend on this` : "";
|
|
999
|
+
const meta = ` ${d.authorName} \xB7 ${formatDate(d.classifiedAt)}${fanOutFragment} \xB7 ${result.reversibility ?? "(unset)"} reversibility`;
|
|
1000
|
+
lines.push("", decisionLine, meta);
|
|
1001
|
+
lines.push("");
|
|
1002
|
+
lines.push("dependents");
|
|
1003
|
+
lines.push(...dependentsBox(result.downstream));
|
|
1004
|
+
const triggered = triggerLine(result);
|
|
1005
|
+
if (triggered.length > 0) {
|
|
1006
|
+
lines.push("");
|
|
1007
|
+
for (const t of triggered) {
|
|
1008
|
+
lines.push(...softWrap(t, { indent: SOFT_WRAP_INDENT }));
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
const crossArea = countCrossAreaDependents(d.area, result.downstream);
|
|
1012
|
+
const crossAreaFragment = crossArea > 0 ? ` \xB7 ${String(crossArea)} cross-area dependency` : "";
|
|
1013
|
+
const noEdgesFragment = result.downstream.length === 0 ? " (no edges yet)" : "";
|
|
1014
|
+
lines.push(
|
|
1015
|
+
`impact: ${String(result.fanOut)} decision(s) need review${noEdgesFragment}${crossAreaFragment}.`
|
|
1016
|
+
);
|
|
1017
|
+
if (result.truncated) {
|
|
1018
|
+
lines.push(
|
|
1019
|
+
" \u26A0 blast radius truncated \u2014 more refs/dependents than the server returns per request; not all shown."
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
return lines.join("\n");
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// src/decisions/cascade.ts
|
|
1026
|
+
var NOT_FOUND_RE = /not found/i;
|
|
1027
|
+
var CASCADE_TIMEOUT_MS = 1e4;
|
|
1028
|
+
var defaultDeps = { getClient };
|
|
1029
|
+
var CascadeNotFoundError = class extends Error {
|
|
1030
|
+
constructor(idOrShortId) {
|
|
1031
|
+
super(`Decision not found: ${idOrShortId}`);
|
|
1032
|
+
this.name = "CascadeNotFoundError";
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
async function fetchCascade(idOrShortId, deps = defaultDeps) {
|
|
1036
|
+
const params = new URLSearchParams({ id: idOrShortId });
|
|
1037
|
+
const client = deps.getClient();
|
|
1038
|
+
try {
|
|
1039
|
+
return await client.get(`/api/cli/decisions/cascade?${params.toString()}`, {
|
|
1040
|
+
signal: AbortSignal.timeout(CASCADE_TIMEOUT_MS)
|
|
1041
|
+
});
|
|
1042
|
+
} catch (err) {
|
|
1043
|
+
if (err instanceof Error && NOT_FOUND_RE.test(err.message)) {
|
|
1044
|
+
throw new CascadeNotFoundError(idOrShortId);
|
|
1045
|
+
}
|
|
1046
|
+
throw err;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
function formatCascadeJson(result) {
|
|
1050
|
+
return JSON.stringify(result, null, 2);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// src/decisions/recent.ts
|
|
1054
|
+
var RECENT_TIMEOUT_MS = 1e4;
|
|
1055
|
+
var defaultDeps2 = { getClient };
|
|
1056
|
+
async function fetchRecent(args, deps = defaultDeps2) {
|
|
1057
|
+
const params = new URLSearchParams();
|
|
1058
|
+
if (args.limit !== void 0) {
|
|
1059
|
+
params.set("limit", String(args.limit));
|
|
1060
|
+
}
|
|
1061
|
+
if (args.since !== void 0) {
|
|
1062
|
+
params.set("since", args.since);
|
|
1063
|
+
}
|
|
1064
|
+
const client = deps.getClient();
|
|
1065
|
+
try {
|
|
1066
|
+
const res = await client.get(`/api/cli/decisions/recent?${params.toString()}`, {
|
|
1067
|
+
signal: AbortSignal.timeout(RECENT_TIMEOUT_MS)
|
|
1068
|
+
});
|
|
1069
|
+
const result = { decisions: res.decisions };
|
|
1070
|
+
if (res.unavailable !== void 0) {
|
|
1071
|
+
result.unavailable = res.unavailable;
|
|
1072
|
+
}
|
|
1073
|
+
return result;
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1076
|
+
return { decisions: [], unavailable: `recent feed failed: ${detail}` };
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
var SHORT_ID_PREFIX = "dec_";
|
|
1080
|
+
var ZERO_PAD_TWO = 2;
|
|
1081
|
+
function pad2(n) {
|
|
1082
|
+
return n.toString().padStart(ZERO_PAD_TWO, "0");
|
|
1083
|
+
}
|
|
1084
|
+
function formatClock(ms) {
|
|
1085
|
+
const d = new Date(ms);
|
|
1086
|
+
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
1087
|
+
}
|
|
1088
|
+
function authorLabel(row) {
|
|
1089
|
+
if (!row.authorIsSelf) {
|
|
1090
|
+
return row.authorName;
|
|
1091
|
+
}
|
|
1092
|
+
switch (row.producerKind) {
|
|
1093
|
+
case "claude_code":
|
|
1094
|
+
return "Your Claude Code";
|
|
1095
|
+
case "chat":
|
|
1096
|
+
return "Your chat";
|
|
1097
|
+
case "spec_edit":
|
|
1098
|
+
return "Your spec edit";
|
|
1099
|
+
case "cli":
|
|
1100
|
+
return "Your CLI";
|
|
1101
|
+
default:
|
|
1102
|
+
return `Your ${row.authorName}`;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
var AUTHOR_WIDTH = 18;
|
|
1106
|
+
function padRight(s, width) {
|
|
1107
|
+
return s.length >= width ? `${s.slice(0, width - 1)} ` : s.padEnd(width, " ");
|
|
1108
|
+
}
|
|
1109
|
+
function formatRecentHuman(result) {
|
|
1110
|
+
if (result.unavailable !== void 0) {
|
|
1111
|
+
return `[prim] recent \xB7 feed not verified \u2014 ${result.unavailable}`;
|
|
1112
|
+
}
|
|
1113
|
+
if (result.decisions.length === 0) {
|
|
1114
|
+
return "[prim] recent \xB7 0 decisions";
|
|
1115
|
+
}
|
|
1116
|
+
const lines = [`[prim] recent \xB7 ${String(result.decisions.length)} decision(s)`];
|
|
1117
|
+
for (const row of result.decisions) {
|
|
1118
|
+
const clock = formatClock(row.classifiedAt);
|
|
1119
|
+
const author = padRight(authorLabel(row), AUTHOR_WIDTH);
|
|
1120
|
+
const areaText = row.area ? `\u2022 ${row.area}` : "\u2022";
|
|
1121
|
+
const areaPlain = padRight(areaText, 12);
|
|
1122
|
+
const areaCol = row.area ? areaPlain.replace("\u2022", color("\u2022", colorForArea(row.area))) : areaPlain;
|
|
1123
|
+
lines.push(` ${clock} ${author}${areaCol}${row.intent}`);
|
|
1124
|
+
}
|
|
1125
|
+
return lines.join("\n");
|
|
1126
|
+
}
|
|
1127
|
+
function formatRecentJson(result) {
|
|
1128
|
+
return JSON.stringify(result, null, 2);
|
|
1129
|
+
}
|
|
1130
|
+
function renderIdentifier(row) {
|
|
1131
|
+
if (row.shortId) {
|
|
1132
|
+
return `${SHORT_ID_PREFIX}${row.shortId}`;
|
|
1133
|
+
}
|
|
1134
|
+
return row.id;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// src/decisions/confirm.ts
|
|
1138
|
+
var CONFIRM_TIMEOUT_MS = 1e4;
|
|
1139
|
+
var defaultDeps3 = { getClient };
|
|
1140
|
+
var NOT_FOUND_RE2 = /not found/i;
|
|
1141
|
+
var AMBIGUOUS_RE = /ambiguous/i;
|
|
1142
|
+
var NOT_AUTHOR_RE = /author/i;
|
|
1143
|
+
var ConfirmNotFoundError = class extends Error {
|
|
1144
|
+
constructor(idOrShortId) {
|
|
1145
|
+
super(`Decision not found: ${idOrShortId}`);
|
|
1146
|
+
this.name = "ConfirmNotFoundError";
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
async function fetchConfirm(idOrShortId, confirmed, deps = defaultDeps3) {
|
|
1150
|
+
const request = { idOrShortId, confirmed };
|
|
1151
|
+
const client = deps.getClient();
|
|
1152
|
+
try {
|
|
1153
|
+
const outcome = await client.post(
|
|
1154
|
+
"/api/cli/decisions/confirm",
|
|
1155
|
+
{ id: idOrShortId, confirmed },
|
|
1156
|
+
{ signal: AbortSignal.timeout(CONFIRM_TIMEOUT_MS) }
|
|
1157
|
+
);
|
|
1158
|
+
return { request, outcome };
|
|
1159
|
+
} catch (err) {
|
|
1160
|
+
if (err instanceof Error) {
|
|
1161
|
+
if (NOT_FOUND_RE2.test(err.message)) {
|
|
1162
|
+
throw new ConfirmNotFoundError(idOrShortId);
|
|
1163
|
+
}
|
|
1164
|
+
if (AMBIGUOUS_RE.test(err.message)) {
|
|
1165
|
+
return { request, outcome: { outcome: "ambiguous" } };
|
|
1166
|
+
}
|
|
1167
|
+
if (NOT_AUTHOR_RE.test(err.message)) {
|
|
1168
|
+
return { request, outcome: { outcome: "not_author" } };
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
throw err;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
function intentWord(confirmed) {
|
|
1175
|
+
return confirmed ? "confirmed" : "rejected";
|
|
1176
|
+
}
|
|
1177
|
+
function formatConfirmHuman(result) {
|
|
1178
|
+
const { request, outcome } = result;
|
|
1179
|
+
switch (outcome.outcome) {
|
|
1180
|
+
case "confirmed":
|
|
1181
|
+
case "corrected":
|
|
1182
|
+
case "stale": {
|
|
1183
|
+
const id = renderIdentifier({ shortId: outcome.shortId, id: outcome.decisionId });
|
|
1184
|
+
if (outcome.outcome === "stale") {
|
|
1185
|
+
return `[prim] ${id} ${intentWord(request.confirmed)} \u2014 the prompt had gone stale; recorded against the current decision.`;
|
|
1186
|
+
}
|
|
1187
|
+
if (outcome.outcome === "corrected") {
|
|
1188
|
+
return `[prim] ${id} ${intentWord(request.confirmed)} with a correction.`;
|
|
1189
|
+
}
|
|
1190
|
+
return `[prim] ${id} ${intentWord(request.confirmed)}.`;
|
|
1191
|
+
}
|
|
1192
|
+
case "already_responded": {
|
|
1193
|
+
const id = renderIdentifier({ shortId: outcome.shortId, id: outcome.decisionId });
|
|
1194
|
+
const priorWord = outcome.confirmed === void 0 ? "already answered" : `already ${intentWord(outcome.confirmed)}`;
|
|
1195
|
+
const when = new Date(outcome.respondedAt).toISOString();
|
|
1196
|
+
return `[prim] ${id} was ${priorWord} (responded ${when}); nothing to change.`;
|
|
1197
|
+
}
|
|
1198
|
+
case "no_pending_prompt": {
|
|
1199
|
+
const id = renderIdentifier({ shortId: outcome.shortId, id: outcome.decisionId });
|
|
1200
|
+
return `[prim] ${id} has no pending confirmation request \u2014 nothing to acknowledge.`;
|
|
1201
|
+
}
|
|
1202
|
+
case "ambiguous":
|
|
1203
|
+
return `[prim] shortId "${request.idOrShortId}" is ambiguous in this organization \u2014 retry with the full decision id.`;
|
|
1204
|
+
default:
|
|
1205
|
+
return "[prim] only the decision's author can respond to its confirmation prompt.";
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
function formatConfirmJson(result) {
|
|
1209
|
+
return JSON.stringify(result.outcome, null, 2);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// src/decisions/show.ts
|
|
1213
|
+
var NOT_FOUND_RE3 = /not found/i;
|
|
1214
|
+
function colorStatus(status) {
|
|
1215
|
+
if (status === "under_review") {
|
|
1216
|
+
return color(status, "orange");
|
|
1217
|
+
}
|
|
1218
|
+
if (status === "active") {
|
|
1219
|
+
return color(status, "green");
|
|
1220
|
+
}
|
|
1221
|
+
return color(status, "gray");
|
|
1222
|
+
}
|
|
1223
|
+
var SHOW_TIMEOUT_MS = 1e4;
|
|
1224
|
+
var defaultDeps4 = { getClient };
|
|
1225
|
+
var DecisionNotFoundError = class extends Error {
|
|
1226
|
+
constructor(idOrShortId) {
|
|
1227
|
+
super(`Decision not found: ${idOrShortId}`);
|
|
1228
|
+
this.name = "DecisionNotFoundError";
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
async function fetchShow(idOrShortId, deps = defaultDeps4) {
|
|
1232
|
+
const params = new URLSearchParams({ id: idOrShortId });
|
|
1233
|
+
const client = deps.getClient();
|
|
1234
|
+
try {
|
|
1235
|
+
return await client.get(`/api/cli/decisions/show?${params.toString()}`, {
|
|
1236
|
+
signal: AbortSignal.timeout(SHOW_TIMEOUT_MS)
|
|
1237
|
+
});
|
|
1238
|
+
} catch (err) {
|
|
1239
|
+
if (err instanceof Error && NOT_FOUND_RE3.test(err.message)) {
|
|
1240
|
+
throw new DecisionNotFoundError(idOrShortId);
|
|
1241
|
+
}
|
|
1242
|
+
throw err;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
var GATED_FLAG_KINDS = /* @__PURE__ */ new Set(["file_edit", "supersession", "context_edit"]);
|
|
1246
|
+
function describeFlag(flag) {
|
|
1247
|
+
const detail = flag.reason ? ` \u2014 ${flag.reason}` : "";
|
|
1248
|
+
if (flag.acknowledgedAt !== void 0) {
|
|
1249
|
+
return `acknowledged ${flag.type}${detail}`;
|
|
1250
|
+
}
|
|
1251
|
+
if (flag.type === "confirmation_request") {
|
|
1252
|
+
return `pending confirmation request${detail}`;
|
|
1253
|
+
}
|
|
1254
|
+
if (GATED_FLAG_KINDS.has(flag.type)) {
|
|
1255
|
+
const verdict = flag.gateVerdict ?? "unknown";
|
|
1256
|
+
return `pending ${flag.type} (verdict: ${verdict})${detail}`;
|
|
1257
|
+
}
|
|
1258
|
+
return `pending ${flag.type}${detail}`;
|
|
1259
|
+
}
|
|
1260
|
+
function describeNode(node) {
|
|
1261
|
+
const id = renderIdentifier({ shortId: node.shortId, id: node.id });
|
|
1262
|
+
const area = node.area ? ` \u2022 ${node.area}` : "";
|
|
1263
|
+
return `${id}${area} ${node.intent} (${node.authorName})`;
|
|
1264
|
+
}
|
|
1265
|
+
function pushFiles(lines, files) {
|
|
1266
|
+
if (files.length === 0) {
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
lines.push(` files (${String(files.length)}):`);
|
|
1270
|
+
for (const file of files) {
|
|
1271
|
+
lines.push(` - ${file}`);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
function pushContexts(lines, contexts) {
|
|
1275
|
+
if (contexts.length === 0) {
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
lines.push(` contexts (${String(contexts.length)}):`);
|
|
1279
|
+
for (const ctx of contexts) {
|
|
1280
|
+
lines.push(` - ${ctx.name}`);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
function pushEdges(lines, label, arrow, nodes) {
|
|
1284
|
+
if (nodes.length === 0) {
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
lines.push(` ${label} (${String(nodes.length)}):`);
|
|
1288
|
+
for (const node of nodes) {
|
|
1289
|
+
lines.push(` ${arrow} ${describeNode(node)}`);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
function formatShowHuman(result) {
|
|
1293
|
+
const d = result.decision;
|
|
1294
|
+
const id = color(renderIdentifier({ shortId: d.shortId, id: d.id }), "orange");
|
|
1295
|
+
const confidence = d.confidence ?? "(unset)";
|
|
1296
|
+
const lines = [
|
|
1297
|
+
`[prim] ${id} \u2014 ${d.intent}`,
|
|
1298
|
+
` status: ${colorStatus(d.status)}${d.confirmed ? " (confirmed)" : ""} \xB7 confidence: ${confidence} \xB7 reversibility: ${d.reversibility ?? "(unset)"}`
|
|
1299
|
+
];
|
|
1300
|
+
if (d.supersededBy) {
|
|
1301
|
+
lines.push(` superseded by: ${d.supersededBy}`);
|
|
1302
|
+
}
|
|
1303
|
+
if (d.area) {
|
|
1304
|
+
lines.push(` area: ${color(d.area, colorForArea(d.area))}`);
|
|
1305
|
+
}
|
|
1306
|
+
if (typeof d.fanOut === "number") {
|
|
1307
|
+
lines.push(` fan-out: ${String(d.fanOut)}`);
|
|
1308
|
+
}
|
|
1309
|
+
if (d.respondedAt !== void 0) {
|
|
1310
|
+
lines.push(` responded at: ${new Date(d.respondedAt).toISOString()}`);
|
|
1311
|
+
}
|
|
1312
|
+
if (d.rationale) {
|
|
1313
|
+
lines.push(` rationale: ${d.rationale}`);
|
|
1314
|
+
}
|
|
1315
|
+
if (d.decided && d.decided.length > 0) {
|
|
1316
|
+
lines.push(` decided (${String(d.decided.length)}):`);
|
|
1317
|
+
for (const point of d.decided) {
|
|
1318
|
+
lines.push(` - ${point}`);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
if (d.alternatives.length > 0) {
|
|
1322
|
+
lines.push(` alternatives: ${d.alternatives.join(" | ")}`);
|
|
1323
|
+
}
|
|
1324
|
+
pushFiles(lines, result.files);
|
|
1325
|
+
pushContexts(lines, result.contexts);
|
|
1326
|
+
pushEdges(lines, "dependents", "\u2192", result.dependents);
|
|
1327
|
+
pushEdges(lines, "depends on", "\u2190", result.dependsOn);
|
|
1328
|
+
if (result.flags.length > 0) {
|
|
1329
|
+
lines.push(` flags (${String(result.flags.length)}):`);
|
|
1330
|
+
for (const flag of result.flags) {
|
|
1331
|
+
lines.push(` \xB7 ${describeFlag(flag)}`);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
if (result.truncated) {
|
|
1335
|
+
lines.push(" (partial \u2014 some related rows were truncated by a join cap)");
|
|
1336
|
+
}
|
|
1337
|
+
return lines.join("\n");
|
|
1338
|
+
}
|
|
1339
|
+
function formatShowJson(result) {
|
|
1340
|
+
return JSON.stringify(result, null, 2);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// src/commands/decisions.ts
|
|
1344
|
+
var EXIT_NOT_FOUND = 4;
|
|
1345
|
+
function registerDecisionsCommands(program2) {
|
|
1346
|
+
const decisions = program2.command("decisions").description("Inspect the project Decision Graph");
|
|
1347
|
+
decisions.command("check").description("Look up active decisions that reference one or more file paths").requiredOption(
|
|
1348
|
+
"--files <files>",
|
|
1349
|
+
"Comma-separated file paths to check against the Decision Graph"
|
|
1350
|
+
).action(async (opts) => {
|
|
1351
|
+
const filePaths = opts.files.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1352
|
+
const result = await checkAffectedDecisions(filePaths);
|
|
1353
|
+
const warning = formatDecisionsWarning(result);
|
|
1354
|
+
if (warning) {
|
|
1355
|
+
console.error(warning);
|
|
1356
|
+
}
|
|
1357
|
+
printJson(result);
|
|
1358
|
+
});
|
|
1359
|
+
decisions.command("recent").description("Show the team-wide chronological decision feed").option("--limit <n>", "Maximum number of rows to return (default 10)").option(
|
|
1360
|
+
"--since <duration>",
|
|
1361
|
+
"Lookback window \u2014 accepts `Nm`, `Nh`, `Nd` (minutes / hours / days) or absolute epoch ms"
|
|
1362
|
+
).action(async (opts) => {
|
|
1363
|
+
const result = await fetchRecent({
|
|
1364
|
+
limit: opts.limit ? Number.parseInt(opts.limit, 10) : void 0,
|
|
1365
|
+
since: opts.since
|
|
1366
|
+
});
|
|
1367
|
+
console.error(formatRecentHuman(result));
|
|
1368
|
+
console.log(formatRecentJson(result));
|
|
1369
|
+
});
|
|
1370
|
+
decisions.command("show <idOrShortId>").description("Show full detail for one decision (intent, rationale, flags, refs, edges)").action(async (idOrShortId) => {
|
|
1371
|
+
try {
|
|
1372
|
+
const result = await fetchShow(idOrShortId);
|
|
1373
|
+
console.error(formatShowHuman(result));
|
|
1374
|
+
console.log(formatShowJson(result));
|
|
1375
|
+
} catch (err) {
|
|
1376
|
+
if (err instanceof DecisionNotFoundError) {
|
|
1377
|
+
console.error(`[prim] ${err.message}`);
|
|
1378
|
+
process.exitCode = EXIT_NOT_FOUND;
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
throw err;
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
decisions.command("cascade <idOrShortId>").description("Render the local cascade subgraph (upstream knowledge + downstream dependents)").action(async (idOrShortId) => {
|
|
1385
|
+
try {
|
|
1386
|
+
const result = await fetchCascade(idOrShortId);
|
|
1387
|
+
console.error(renderCascade(result));
|
|
1388
|
+
console.log(formatCascadeJson(result));
|
|
1389
|
+
} catch (err) {
|
|
1390
|
+
if (err instanceof CascadeNotFoundError) {
|
|
1391
|
+
console.error(`[prim] ${err.message}`);
|
|
1392
|
+
process.exitCode = EXIT_NOT_FOUND;
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
throw err;
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
decisions.command("confirm <idOrShortId>").description("Acknowledge a confirmation prompt for the named decision").option("--reject", "Record a rejection (sets the decision's confirmed flag to false)").action(async (idOrShortId, opts) => {
|
|
1399
|
+
const confirmed = !opts.reject;
|
|
1400
|
+
try {
|
|
1401
|
+
const result = await fetchConfirm(idOrShortId, confirmed);
|
|
1402
|
+
console.error(formatConfirmHuman(result));
|
|
1403
|
+
console.log(formatConfirmJson(result));
|
|
1404
|
+
} catch (err) {
|
|
1405
|
+
if (err instanceof ConfirmNotFoundError) {
|
|
1406
|
+
console.error(`[prim] ${err.message}`);
|
|
1407
|
+
process.exitCode = EXIT_NOT_FOUND;
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
throw err;
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
|
|
376
1415
|
// src/commands/hooks.ts
|
|
377
1416
|
import { execSync } from "child_process";
|
|
378
|
-
import { existsSync as
|
|
1417
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
379
1418
|
import { resolve } from "path";
|
|
380
1419
|
import { Option } from "commander";
|
|
381
1420
|
var HOOK_SCRIPT = `#!/bin/sh
|
|
@@ -409,13 +1448,13 @@ function getGitRoot() {
|
|
|
409
1448
|
}
|
|
410
1449
|
function detectHusky(gitRoot) {
|
|
411
1450
|
const huskyDir = resolve(gitRoot, ".husky");
|
|
412
|
-
if (!
|
|
413
|
-
if (
|
|
414
|
-
if (
|
|
1451
|
+
if (!existsSync4(huskyDir)) return false;
|
|
1452
|
+
if (existsSync4(resolve(huskyDir, "_"))) return true;
|
|
1453
|
+
if (existsSync4(resolve(huskyDir, "pre-commit"))) return true;
|
|
415
1454
|
const pkgPath = resolve(gitRoot, "package.json");
|
|
416
|
-
if (
|
|
1455
|
+
if (existsSync4(pkgPath)) {
|
|
417
1456
|
try {
|
|
418
|
-
const pkg2 = JSON.parse(
|
|
1457
|
+
const pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
419
1458
|
const scripts = pkg2.scripts ?? {};
|
|
420
1459
|
if (/husky/i.test(scripts.prepare ?? "") || /husky/i.test(scripts.postinstall ?? "")) {
|
|
421
1460
|
return true;
|
|
@@ -442,20 +1481,20 @@ async function askConfirmation(question) {
|
|
|
442
1481
|
}
|
|
443
1482
|
function installToHusky(gitRoot) {
|
|
444
1483
|
const hookPath = resolve(gitRoot, ".husky", "pre-commit");
|
|
445
|
-
if (
|
|
446
|
-
const existing =
|
|
1484
|
+
if (existsSync4(hookPath)) {
|
|
1485
|
+
const existing = readFileSync5(hookPath, "utf-8");
|
|
447
1486
|
if (containsPrimHook(existing)) {
|
|
448
1487
|
console.log("Prim pre-commit hook is already installed in .husky/pre-commit.");
|
|
449
1488
|
return;
|
|
450
1489
|
}
|
|
451
1490
|
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
452
|
-
|
|
1491
|
+
writeFileSync3(hookPath, `${existing}${separator}${PRIM_HUSKY_BLOCK}
|
|
453
1492
|
`, {
|
|
454
1493
|
mode: 493
|
|
455
1494
|
});
|
|
456
1495
|
console.log("Appended prim hook block to .husky/pre-commit.");
|
|
457
1496
|
} else {
|
|
458
|
-
|
|
1497
|
+
writeFileSync3(hookPath, `#!/bin/sh
|
|
459
1498
|
|
|
460
1499
|
${PRIM_HUSKY_BLOCK}
|
|
461
1500
|
`, {
|
|
@@ -467,11 +1506,11 @@ ${PRIM_HUSKY_BLOCK}
|
|
|
467
1506
|
function installToDotGit(gitRoot) {
|
|
468
1507
|
const hooksDir = resolve(gitRoot, ".git", "hooks");
|
|
469
1508
|
const hookPath = resolve(hooksDir, "pre-commit");
|
|
470
|
-
if (!
|
|
471
|
-
|
|
1509
|
+
if (!existsSync4(hooksDir)) {
|
|
1510
|
+
mkdirSync3(hooksDir, { recursive: true });
|
|
472
1511
|
}
|
|
473
|
-
if (
|
|
474
|
-
const existing =
|
|
1512
|
+
if (existsSync4(hookPath)) {
|
|
1513
|
+
const existing = readFileSync5(hookPath, "utf-8");
|
|
475
1514
|
if (containsPrimHook(existing)) {
|
|
476
1515
|
console.log("Prim pre-commit hook is already installed at .git/hooks/pre-commit.");
|
|
477
1516
|
return;
|
|
@@ -480,7 +1519,7 @@ function installToDotGit(gitRoot) {
|
|
|
480
1519
|
console.log("To replace it, run: prim hooks uninstall && prim hooks install");
|
|
481
1520
|
return;
|
|
482
1521
|
}
|
|
483
|
-
|
|
1522
|
+
writeFileSync3(hookPath, HOOK_SCRIPT, { mode: 493 });
|
|
484
1523
|
console.log(`Installed pre-commit hook at ${hookPath}`);
|
|
485
1524
|
}
|
|
486
1525
|
function registerHooksCommands(program2) {
|
|
@@ -522,15 +1561,147 @@ function registerHooksCommands(program2) {
|
|
|
522
1561
|
hooks.command("uninstall").description("Remove the prim pre-commit hook").action(() => {
|
|
523
1562
|
const gitRoot = getGitRoot();
|
|
524
1563
|
const hookPath = resolve(gitRoot, ".git", "hooks", "pre-commit");
|
|
525
|
-
if (!
|
|
1564
|
+
if (!existsSync4(hookPath)) {
|
|
526
1565
|
console.log("No pre-commit hook found.");
|
|
527
1566
|
return;
|
|
528
1567
|
}
|
|
529
|
-
|
|
1568
|
+
unlinkSync2(hookPath);
|
|
530
1569
|
console.log(`Removed pre-commit hook at ${hookPath}`);
|
|
531
1570
|
});
|
|
532
1571
|
}
|
|
533
1572
|
|
|
1573
|
+
// src/commands/moves.ts
|
|
1574
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1575
|
+
import { join as join3 } from "path";
|
|
1576
|
+
|
|
1577
|
+
// src/flusher.ts
|
|
1578
|
+
import { renameSync as renameSync2, unlinkSync as unlinkSync3 } from "fs";
|
|
1579
|
+
var BATCH_SIZE = 500;
|
|
1580
|
+
var HTTP_TIMEOUT_MS = 1e4;
|
|
1581
|
+
var OPPORTUNISTIC_FLUSH_AFTER_MS = 6e4;
|
|
1582
|
+
async function drainPath(path) {
|
|
1583
|
+
const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
|
|
1584
|
+
try {
|
|
1585
|
+
renameSync2(path, tmpPath);
|
|
1586
|
+
} catch (err) {
|
|
1587
|
+
if (err.code === "ENOENT") {
|
|
1588
|
+
return 0;
|
|
1589
|
+
}
|
|
1590
|
+
throw err;
|
|
1591
|
+
}
|
|
1592
|
+
const moves = readMovesFromPath(tmpPath);
|
|
1593
|
+
if (moves.length === 0) {
|
|
1594
|
+
unlinkSync3(tmpPath);
|
|
1595
|
+
return 0;
|
|
1596
|
+
}
|
|
1597
|
+
const client = getClient();
|
|
1598
|
+
for (let i = 0; i < moves.length; i += BATCH_SIZE) {
|
|
1599
|
+
const batch = moves.slice(i, i + BATCH_SIZE);
|
|
1600
|
+
await client.post(
|
|
1601
|
+
"/api/cli/moves/ingest",
|
|
1602
|
+
{ batch },
|
|
1603
|
+
{ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) }
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
unlinkSync3(tmpPath);
|
|
1607
|
+
return moves.length;
|
|
1608
|
+
}
|
|
1609
|
+
async function flush() {
|
|
1610
|
+
let total = 0;
|
|
1611
|
+
for (const { path } of listBuckets()) {
|
|
1612
|
+
total += await drainPath(path);
|
|
1613
|
+
}
|
|
1614
|
+
return { flushed: total };
|
|
1615
|
+
}
|
|
1616
|
+
async function flushIfNeeded() {
|
|
1617
|
+
try {
|
|
1618
|
+
const stats = bucketStats();
|
|
1619
|
+
if (stats.length === 0) {
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
const oldest = stats.reduce((min, s) => s.mtimeMs < min ? s.mtimeMs : min, stats[0].mtimeMs);
|
|
1623
|
+
if (Date.now() - oldest > OPPORTUNISTIC_FLUSH_AFTER_MS) {
|
|
1624
|
+
await flush();
|
|
1625
|
+
}
|
|
1626
|
+
} catch {
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/commands/moves.ts
|
|
1631
|
+
var MS_PER_SECOND = 1e3;
|
|
1632
|
+
var DEFAULT_TAIL_LINES = "20";
|
|
1633
|
+
var RADIX_DECIMAL = 10;
|
|
1634
|
+
var ID_PREFIX_LEN = 8;
|
|
1635
|
+
var EVENT_COL_WIDTH = 20;
|
|
1636
|
+
var BUCKET_COL_WIDTH = 20;
|
|
1637
|
+
var TAIL_BUCKET_COL_WIDTH = 12;
|
|
1638
|
+
var DIR_MODE = 448;
|
|
1639
|
+
var FILE_MODE2 = 384;
|
|
1640
|
+
var WORKSPACE_FILE = ".prim/workspace.json";
|
|
1641
|
+
function registerMovesCommands(program2) {
|
|
1642
|
+
const moves = program2.command("moves").description("Decision Event Pipeline \u2014 local journal");
|
|
1643
|
+
moves.command("flush").description("Drain all local move journals to the server").action(async () => {
|
|
1644
|
+
const { flushed } = await flush();
|
|
1645
|
+
console.log(`[prim] flushed ${String(flushed)} move${flushed === 1 ? "" : "s"}`);
|
|
1646
|
+
});
|
|
1647
|
+
moves.command("status").description("Show per-bucket pending stats").action(() => {
|
|
1648
|
+
const stats = bucketStats();
|
|
1649
|
+
if (stats.length === 0) {
|
|
1650
|
+
console.log("[prim] journal: empty");
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
console.log(`[prim] root: ${JOURNAL_DIR}`);
|
|
1654
|
+
for (const s of stats) {
|
|
1655
|
+
const ageS = Math.round((Date.now() - s.mtimeMs) / MS_PER_SECOND);
|
|
1656
|
+
console.log(
|
|
1657
|
+
` ${s.bucket.padEnd(BUCKET_COL_WIDTH)} ${String(s.lineCount).padStart(5)} pending, ${String(s.sizeBytes).padStart(8)} bytes, last write ${String(ageS)}s ago`
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
moves.command("tail").description("Pretty-print recent journal entries across all buckets").option("-n, --lines <n>", "number of lines to tail", DEFAULT_TAIL_LINES).action((opts) => {
|
|
1662
|
+
const lines = Number.parseInt(opts.lines, RADIX_DECIMAL);
|
|
1663
|
+
if (!Number.isInteger(lines) || lines < 1) {
|
|
1664
|
+
console.error("[prim] --lines must be a positive integer");
|
|
1665
|
+
process.exitCode = 1;
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
const all = bucketStats().flatMap((s) => readMovesFromPath(s.path).map((m) => ({ bucket: s.bucket, move: m }))).sort((a, b) => a.move.capturedAt - b.move.capturedAt);
|
|
1669
|
+
if (all.length === 0) {
|
|
1670
|
+
console.log("[prim] journal: empty");
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
const tail = all.slice(-lines);
|
|
1674
|
+
for (const { bucket, move: m } of tail) {
|
|
1675
|
+
const t = new Date(m.capturedAt).toISOString();
|
|
1676
|
+
const session = m.sessionId.slice(0, ID_PREFIX_LEN) || "anon";
|
|
1677
|
+
const move = m.moveId.slice(0, ID_PREFIX_LEN);
|
|
1678
|
+
console.log(
|
|
1679
|
+
`${t} ${m.eventType.padEnd(EVENT_COL_WIDTH)} bucket=${bucket.padEnd(TAIL_BUCKET_COL_WIDTH)} session=${session} move=${move}`
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
|
|
1684
|
+
const dir = join3(process.cwd(), ".prim");
|
|
1685
|
+
if (!existsSync5(dir)) {
|
|
1686
|
+
mkdirSync4(dir, { recursive: true, mode: DIR_MODE });
|
|
1687
|
+
}
|
|
1688
|
+
const file = join3(process.cwd(), WORKSPACE_FILE);
|
|
1689
|
+
writeFileSync4(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
|
|
1690
|
+
mode: FILE_MODE2
|
|
1691
|
+
});
|
|
1692
|
+
console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
|
|
1693
|
+
});
|
|
1694
|
+
moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
|
|
1695
|
+
const file = join3(process.cwd(), WORKSPACE_FILE);
|
|
1696
|
+
if (!existsSync5(file)) {
|
|
1697
|
+
console.log("[prim] no workspace binding in cwd");
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
unlinkSync4(file);
|
|
1701
|
+
console.log(`[prim] dropped workspace binding from ${process.cwd()}`);
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
|
|
534
1705
|
// src/commands/project.ts
|
|
535
1706
|
function registerProjectCommands(program2) {
|
|
536
1707
|
const project = program2.command("project").description("Manage projects");
|
|
@@ -553,20 +1724,165 @@ function registerProjectCommands(program2) {
|
|
|
553
1724
|
});
|
|
554
1725
|
}
|
|
555
1726
|
|
|
1727
|
+
// src/commands/reconcile.ts
|
|
1728
|
+
var EXIT_OK = 0;
|
|
1729
|
+
var EXIT_USAGE = 2;
|
|
1730
|
+
var EXIT_SERVER = 3;
|
|
1731
|
+
var HTTP_CLIENT_ERROR_MIN = 400;
|
|
1732
|
+
var HTTP_SERVER_ERROR_MIN = 500;
|
|
1733
|
+
function isOk(value) {
|
|
1734
|
+
if (typeof value !== "object" || value === null) {
|
|
1735
|
+
return false;
|
|
1736
|
+
}
|
|
1737
|
+
const v = value;
|
|
1738
|
+
return v.ok === true && typeof v.bypassId === "string" && typeof v.expiresAt === "number";
|
|
1739
|
+
}
|
|
1740
|
+
function formatExpiresIn(expiresAt) {
|
|
1741
|
+
const remainingMs = expiresAt - Date.now();
|
|
1742
|
+
if (remainingMs <= 0) {
|
|
1743
|
+
return "expired";
|
|
1744
|
+
}
|
|
1745
|
+
const SECONDS_PER_MINUTE = 60;
|
|
1746
|
+
const minutes = Math.floor(remainingMs / (SECONDS_PER_MINUTE * 1e3));
|
|
1747
|
+
const seconds = Math.floor(remainingMs / 1e3 % SECONDS_PER_MINUTE);
|
|
1748
|
+
if (minutes === 0) {
|
|
1749
|
+
return `${seconds}s`;
|
|
1750
|
+
}
|
|
1751
|
+
return `${minutes}m${seconds.toString().padStart(2, "0")}s`;
|
|
1752
|
+
}
|
|
1753
|
+
function renderDecisionIdentifier(short, id) {
|
|
1754
|
+
return short ? `dec_${short}` : id;
|
|
1755
|
+
}
|
|
1756
|
+
function isDomainRejection(err) {
|
|
1757
|
+
return err instanceof HttpError && err.status >= HTTP_CLIENT_ERROR_MIN && err.status < HTTP_SERVER_ERROR_MIN;
|
|
1758
|
+
}
|
|
1759
|
+
async function performReconcile(idOrShortId, opts = {}) {
|
|
1760
|
+
const client = getClient();
|
|
1761
|
+
const body = { idOrShortId };
|
|
1762
|
+
if (opts.flag) {
|
|
1763
|
+
body.conflictFlagId = opts.flag;
|
|
1764
|
+
}
|
|
1765
|
+
let response;
|
|
1766
|
+
try {
|
|
1767
|
+
response = await client.post("/api/cli/reconcile/issue", body);
|
|
1768
|
+
} catch (err) {
|
|
1769
|
+
if (isDomainRejection(err)) {
|
|
1770
|
+
process.stderr.write(`[prim] reconcile rejected: ${err.message}
|
|
1771
|
+
`);
|
|
1772
|
+
console.log(JSON.stringify({ ok: false, status: err.status, error: err.message }, null, 2));
|
|
1773
|
+
process.exitCode = EXIT_USAGE;
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1777
|
+
process.stderr.write(`[prim] reconcile failed: ${message}
|
|
1778
|
+
`);
|
|
1779
|
+
console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
1780
|
+
process.exitCode = EXIT_SERVER;
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
if (isOk(response)) {
|
|
1784
|
+
const ident = renderDecisionIdentifier(response.decisionShortId, response.decisionId);
|
|
1785
|
+
const verb = response.reissued ? "reissued" : "issued";
|
|
1786
|
+
process.stderr.write(
|
|
1787
|
+
`[prim] reconcile bypass ${verb} for ${ident} (expires in ${formatExpiresIn(response.expiresAt)})
|
|
1788
|
+
`
|
|
1789
|
+
);
|
|
1790
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1791
|
+
process.exitCode = EXIT_OK;
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
process.stderr.write("[prim] reconcile: malformed server response\n");
|
|
1795
|
+
console.log(JSON.stringify({ ok: false, response }, null, 2));
|
|
1796
|
+
process.exitCode = EXIT_SERVER;
|
|
1797
|
+
}
|
|
1798
|
+
function registerReconcileCommands(program2) {
|
|
1799
|
+
program2.command("reconcile <idOrShortId>").description(
|
|
1800
|
+
"Issue a single-use bypass for a flagged decision (used by the cooperative reconcile loop)"
|
|
1801
|
+
).option(
|
|
1802
|
+
"--flag <conflictFlagId>",
|
|
1803
|
+
"Specific flag id to bind the bypass to (default: the decision's latest unack'd flag)"
|
|
1804
|
+
).option("--json", "(reserved; STDOUT is always JSON)").action(async (idOrShortId, opts) => {
|
|
1805
|
+
await performReconcile(idOrShortId, opts);
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// src/commands/session.ts
|
|
1810
|
+
import {
|
|
1811
|
+
existsSync as existsSync6,
|
|
1812
|
+
mkdirSync as mkdirSync5,
|
|
1813
|
+
readFileSync as readFileSync6,
|
|
1814
|
+
readdirSync,
|
|
1815
|
+
unlinkSync as unlinkSync5,
|
|
1816
|
+
writeFileSync as writeFileSync5
|
|
1817
|
+
} from "fs";
|
|
1818
|
+
import { join as join4 } from "path";
|
|
1819
|
+
var DIR_MODE2 = 448;
|
|
1820
|
+
var FILE_MODE3 = 384;
|
|
1821
|
+
function ensureDir() {
|
|
1822
|
+
if (!existsSync6(SESSIONS_DIR)) {
|
|
1823
|
+
mkdirSync5(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
function markerPath(sessionId) {
|
|
1827
|
+
return join4(SESSIONS_DIR, `${sessionId}.json`);
|
|
1828
|
+
}
|
|
1829
|
+
function registerSessionCommands(program2) {
|
|
1830
|
+
const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
|
|
1831
|
+
session.command("start <sessionId>").description("Pin a Claude Code session to an org").requiredOption("--orgId <orgId>", "Convex organization id").action((sessionId, opts) => {
|
|
1832
|
+
ensureDir();
|
|
1833
|
+
const marker = {
|
|
1834
|
+
orgId: opts.orgId,
|
|
1835
|
+
startedAt: Date.now()
|
|
1836
|
+
};
|
|
1837
|
+
writeFileSync5(markerPath(sessionId), JSON.stringify(marker, null, 2), {
|
|
1838
|
+
mode: FILE_MODE3
|
|
1839
|
+
});
|
|
1840
|
+
console.log(`[prim] session ${sessionId} bound to org ${opts.orgId}`);
|
|
1841
|
+
});
|
|
1842
|
+
session.command("list").description("List active session markers").action(() => {
|
|
1843
|
+
if (!existsSync6(SESSIONS_DIR)) {
|
|
1844
|
+
console.log("[prim] no session markers");
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
const files = readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
|
|
1848
|
+
if (files.length === 0) {
|
|
1849
|
+
console.log("[prim] no session markers");
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
for (const f of files) {
|
|
1853
|
+
const sessionId = f.replace(/\.json$/, "");
|
|
1854
|
+
try {
|
|
1855
|
+
const m = JSON.parse(readFileSync6(join4(SESSIONS_DIR, f), "utf-8"));
|
|
1856
|
+
console.log(`${sessionId} org=${m.orgId}`);
|
|
1857
|
+
} catch {
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
});
|
|
1861
|
+
session.command("drop <sessionId>").description("Remove a session marker").action((sessionId) => {
|
|
1862
|
+
const p = markerPath(sessionId);
|
|
1863
|
+
if (!existsSync6(p)) {
|
|
1864
|
+
console.log(`[prim] no marker for session ${sessionId}`);
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
unlinkSync5(p);
|
|
1868
|
+
console.log(`[prim] dropped session marker ${sessionId}`);
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
|
|
556
1872
|
// src/commands/skill.ts
|
|
557
1873
|
import {
|
|
558
|
-
closeSync,
|
|
559
|
-
existsSync as
|
|
560
|
-
fsyncSync,
|
|
561
|
-
openSync,
|
|
562
|
-
readFileSync as
|
|
563
|
-
renameSync,
|
|
564
|
-
writeFileSync as
|
|
1874
|
+
closeSync as closeSync2,
|
|
1875
|
+
existsSync as existsSync7,
|
|
1876
|
+
fsyncSync as fsyncSync2,
|
|
1877
|
+
openSync as openSync2,
|
|
1878
|
+
readFileSync as readFileSync7,
|
|
1879
|
+
renameSync as renameSync3,
|
|
1880
|
+
writeFileSync as writeFileSync6
|
|
565
1881
|
} from "fs";
|
|
566
|
-
import { dirname as
|
|
1882
|
+
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
567
1883
|
import { fileURLToPath } from "url";
|
|
568
1884
|
import { createPatch } from "diff";
|
|
569
|
-
var __dirname =
|
|
1885
|
+
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
570
1886
|
var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
|
|
571
1887
|
var SKILL_END = "<!-- END PRIM SKILL v1 -->";
|
|
572
1888
|
var TARGET_CANDIDATES = [
|
|
@@ -578,15 +1894,15 @@ var TARGET_CANDIDATES = [
|
|
|
578
1894
|
var DEFAULT_TARGET = "CLAUDE.md";
|
|
579
1895
|
function loadSkill() {
|
|
580
1896
|
let dir = __dirname;
|
|
581
|
-
while (dir !==
|
|
1897
|
+
while (dir !== dirname3(dir)) {
|
|
582
1898
|
const p = resolve2(dir, "SKILL.md");
|
|
583
|
-
if (
|
|
584
|
-
dir =
|
|
1899
|
+
if (existsSync7(p)) return readFileSync7(p, "utf-8");
|
|
1900
|
+
dir = dirname3(dir);
|
|
585
1901
|
}
|
|
586
1902
|
throw new Error("SKILL.md not found in package");
|
|
587
1903
|
}
|
|
588
1904
|
function detectTargets(cwd) {
|
|
589
|
-
return TARGET_CANDIDATES.filter((p) =>
|
|
1905
|
+
return TARGET_CANDIDATES.filter((p) => existsSync7(resolve2(cwd, p)));
|
|
590
1906
|
}
|
|
591
1907
|
function detectNewline(content) {
|
|
592
1908
|
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -612,16 +1928,16 @@ function removeBlock(existing) {
|
|
|
612
1928
|
const out = existing.slice(0, b) + existing.slice(e + SKILL_END.length);
|
|
613
1929
|
return out.replace(/(\r?\n){2,}$/, "$1");
|
|
614
1930
|
}
|
|
615
|
-
function
|
|
1931
|
+
function atomicWrite2(target, content) {
|
|
616
1932
|
const tmp = `${target}.tmp`;
|
|
617
|
-
|
|
618
|
-
const fd =
|
|
1933
|
+
writeFileSync6(tmp, content);
|
|
1934
|
+
const fd = openSync2(tmp, "r+");
|
|
619
1935
|
try {
|
|
620
|
-
|
|
1936
|
+
fsyncSync2(fd);
|
|
621
1937
|
} finally {
|
|
622
|
-
|
|
1938
|
+
closeSync2(fd);
|
|
623
1939
|
}
|
|
624
|
-
|
|
1940
|
+
renameSync3(tmp, target);
|
|
625
1941
|
}
|
|
626
1942
|
function resolveTarget(cwd, override) {
|
|
627
1943
|
if (override) return resolve2(cwd, override);
|
|
@@ -635,7 +1951,7 @@ function resolveTarget(cwd, override) {
|
|
|
635
1951
|
function runInstall(cwd, opts) {
|
|
636
1952
|
const target = resolveTarget(cwd, opts.target);
|
|
637
1953
|
if (target === null) return 1;
|
|
638
|
-
const existing =
|
|
1954
|
+
const existing = existsSync7(target) ? readFileSync7(target, "utf-8") : "";
|
|
639
1955
|
const eol = existing ? detectNewline(existing) : "\n";
|
|
640
1956
|
const block = composeBlock(loadSkill(), eol);
|
|
641
1957
|
const next = applyBlock(existing, block, eol);
|
|
@@ -647,34 +1963,34 @@ function runInstall(cwd, opts) {
|
|
|
647
1963
|
process.stdout.write(createPatch(target, existing, next, "current", "proposed"));
|
|
648
1964
|
return 0;
|
|
649
1965
|
}
|
|
650
|
-
|
|
1966
|
+
atomicWrite2(target, next);
|
|
651
1967
|
console.log(`Wrote ${Buffer.byteLength(next)} bytes to ${target}`);
|
|
652
1968
|
return 0;
|
|
653
1969
|
}
|
|
654
1970
|
function runUninstall(cwd, opts) {
|
|
655
1971
|
const target = resolveTarget(cwd, opts.target);
|
|
656
1972
|
if (target === null) return 1;
|
|
657
|
-
if (!
|
|
1973
|
+
if (!existsSync7(target)) {
|
|
658
1974
|
console.log(`Skill block not present at ${target}`);
|
|
659
1975
|
return 0;
|
|
660
1976
|
}
|
|
661
|
-
const existing =
|
|
1977
|
+
const existing = readFileSync7(target, "utf-8");
|
|
662
1978
|
const next = removeBlock(existing);
|
|
663
1979
|
if (next === null) {
|
|
664
1980
|
console.log(`Skill block not present at ${target}`);
|
|
665
1981
|
return 0;
|
|
666
1982
|
}
|
|
667
|
-
|
|
1983
|
+
atomicWrite2(target, next);
|
|
668
1984
|
console.log(`Removed skill block from ${target}`);
|
|
669
1985
|
return 0;
|
|
670
1986
|
}
|
|
671
1987
|
function runStatus(cwd, opts) {
|
|
672
1988
|
const target = resolveTarget(cwd, opts.target);
|
|
673
1989
|
if (target === null) return 1;
|
|
674
|
-
const fileExists =
|
|
1990
|
+
const fileExists = existsSync7(target);
|
|
675
1991
|
let installed = false;
|
|
676
1992
|
if (fileExists) {
|
|
677
|
-
const content =
|
|
1993
|
+
const content = readFileSync7(target, "utf-8");
|
|
678
1994
|
installed = content.includes(SKILL_BEGIN) && content.includes(SKILL_END);
|
|
679
1995
|
}
|
|
680
1996
|
if (opts.json) {
|
|
@@ -711,7 +2027,7 @@ function registerSkillCommands(program2) {
|
|
|
711
2027
|
}
|
|
712
2028
|
|
|
713
2029
|
// src/commands/spec.ts
|
|
714
|
-
import { readFileSync as
|
|
2030
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
715
2031
|
function registerSpecCommands(program2) {
|
|
716
2032
|
const spec = program2.command("spec").description("Manage spec documents");
|
|
717
2033
|
spec.command("list").description("List spec documents").option("-t, --project-id <projectId>", "List spec for a specific root project").option("--json", "Output as JSON").action(async (opts) => {
|
|
@@ -765,7 +2081,7 @@ ${contexts.length} spec(s)`);
|
|
|
765
2081
|
const client = getClient();
|
|
766
2082
|
let text = opts.text;
|
|
767
2083
|
if (opts.file) {
|
|
768
|
-
text =
|
|
2084
|
+
text = readFileSync8(opts.file, "utf-8");
|
|
769
2085
|
}
|
|
770
2086
|
const taskIds = opts.projectId ? opts.projectId.split(",").map((id) => id.trim()) : void 0;
|
|
771
2087
|
let linkedBranch;
|
|
@@ -806,7 +2122,7 @@ ${contexts.length} spec(s)`);
|
|
|
806
2122
|
const client = getClient();
|
|
807
2123
|
let text = opts.text;
|
|
808
2124
|
if (opts.file) {
|
|
809
|
-
text =
|
|
2125
|
+
text = readFileSync8(opts.file, "utf-8");
|
|
810
2126
|
}
|
|
811
2127
|
if (!(text || opts.name)) {
|
|
812
2128
|
console.error("Provide --text, --file, or --name to update.");
|
|
@@ -976,9 +2292,64 @@ ${preview}`);
|
|
|
976
2292
|
}
|
|
977
2293
|
}
|
|
978
2294
|
|
|
2295
|
+
// src/commands/statusline.ts
|
|
2296
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
2297
|
+
import { dirname as dirname4, resolve as resolve3 } from "path";
|
|
2298
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2299
|
+
var STATUSLINE_TIMEOUT_MS = 200;
|
|
2300
|
+
function readPackageVersion() {
|
|
2301
|
+
try {
|
|
2302
|
+
const here = dirname4(fileURLToPath2(import.meta.url));
|
|
2303
|
+
const candidates = [resolve3(here, "../../package.json"), resolve3(here, "../package.json")];
|
|
2304
|
+
for (const path of candidates) {
|
|
2305
|
+
try {
|
|
2306
|
+
const pkg2 = JSON.parse(readFileSync9(path, "utf-8"));
|
|
2307
|
+
if (pkg2.version) {
|
|
2308
|
+
return pkg2.version;
|
|
2309
|
+
}
|
|
2310
|
+
} catch {
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
} catch {
|
|
2314
|
+
}
|
|
2315
|
+
return "0.0.0";
|
|
2316
|
+
}
|
|
2317
|
+
function debug(msg) {
|
|
2318
|
+
if (process.env.PRIM_STATUSLINE_DEBUG === "1") {
|
|
2319
|
+
process.stderr.write(`[prim-statusline] ${msg}
|
|
2320
|
+
`);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
async function renderStatusline() {
|
|
2324
|
+
const version = readPackageVersion();
|
|
2325
|
+
const snapshot = await daemonRequest(
|
|
2326
|
+
"status_snapshot",
|
|
2327
|
+
{},
|
|
2328
|
+
{ timeoutMs: STATUSLINE_TIMEOUT_MS }
|
|
2329
|
+
);
|
|
2330
|
+
if (!snapshot) {
|
|
2331
|
+
debug("daemon snapshot missing");
|
|
2332
|
+
return `primitive ${version} (daemon: down)`;
|
|
2333
|
+
}
|
|
2334
|
+
if (snapshot.presenceStale) {
|
|
2335
|
+
return `primitive ${version} (daemon: live \xB7 presence: stale)`;
|
|
2336
|
+
}
|
|
2337
|
+
const team = typeof snapshot.onlineCount === "number" ? `team: ${String(snapshot.onlineCount)} online` : "team: \u2014";
|
|
2338
|
+
return `primitive ${version} (daemon: live \xB7 ${team})`;
|
|
2339
|
+
}
|
|
2340
|
+
function registerStatuslineCommands(program2) {
|
|
2341
|
+
program2.command("statusline").description("Render the Claude Code statusLine for the prim companion daemon").action(async () => {
|
|
2342
|
+
try {
|
|
2343
|
+
const line = await renderStatusline();
|
|
2344
|
+
process.stdout.write(line);
|
|
2345
|
+
} catch {
|
|
2346
|
+
}
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
|
|
979
2350
|
// src/index.ts
|
|
980
|
-
var __dirname2 =
|
|
981
|
-
var pkg = JSON.parse(
|
|
2351
|
+
var __dirname2 = dirname5(fileURLToPath3(import.meta.url));
|
|
2352
|
+
var pkg = JSON.parse(readFileSync10(resolve4(__dirname2, "../package.json"), "utf-8"));
|
|
982
2353
|
updateNotifier({ pkg }).notify();
|
|
983
2354
|
var program = new Command();
|
|
984
2355
|
program.name("prim").description("CLI for managing Primitive specs and contexts").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
|
|
@@ -991,9 +2362,22 @@ registerSpecCommands(program);
|
|
|
991
2362
|
registerProjectCommands(program);
|
|
992
2363
|
registerHooksCommands(program);
|
|
993
2364
|
registerSkillCommands(program);
|
|
2365
|
+
registerMovesCommands(program);
|
|
2366
|
+
registerSessionCommands(program);
|
|
2367
|
+
registerDecisionsCommands(program);
|
|
2368
|
+
registerClaudeCommands(program);
|
|
2369
|
+
registerDaemonCommands(program);
|
|
2370
|
+
registerReconcileCommands(program);
|
|
2371
|
+
registerStatuslineCommands(program);
|
|
994
2372
|
process.on("unhandledRejection", (err) => {
|
|
995
2373
|
const msg = err instanceof Error ? err.message : String(err);
|
|
996
2374
|
console.error(msg);
|
|
997
2375
|
process.exit(1);
|
|
998
2376
|
});
|
|
2377
|
+
var argv = process.argv.slice(2);
|
|
2378
|
+
var isExplicitFlush = argv[0] === "moves" && argv[1] === "flush";
|
|
2379
|
+
if (!isExplicitFlush) {
|
|
2380
|
+
flushIfNeeded().catch(() => {
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
999
2383
|
program.parse();
|