@primitive.ai/prim 0.1.0-alpha.68 → 0.1.0-alpha.69

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.
Files changed (44) hide show
  1. package/README.md +39 -1
  2. package/SKILL.md +1 -1
  3. package/dist/{chunk-XPVNH3J6.js → chunk-2KW7DVFJ.js} +121 -130
  4. package/dist/chunk-3EEFL6NJ.js +529 -0
  5. package/dist/chunk-4A2XB2PC.js +310 -0
  6. package/dist/{chunk-G5D3FF3D.js → chunk-BQUJ4AFJ.js} +1 -83
  7. package/dist/chunk-CUE2LKLR.js +155 -0
  8. package/dist/chunk-FI3LIRCB.js +262 -0
  9. package/dist/{chunk-CPSE3Y3T.js → chunk-H64F6QHV.js} +27 -8
  10. package/dist/{chunk-I5BHJQH2.js → chunk-KKP2NYZ3.js} +206 -30
  11. package/dist/chunk-LEMX3IOG.js +79 -0
  12. package/dist/{chunk-U6L2RU67.js → chunk-LMRYZL3D.js} +52 -45
  13. package/dist/{chunk-XUAP572Z.js → chunk-RO3F6JYV.js} +8 -7
  14. package/dist/{chunk-TDRXHBEX.js → chunk-TJTVHDJ5.js} +41 -18
  15. package/dist/{chunk-TCDUH7AN.js → chunk-WB7AUZ4C.js} +17 -0
  16. package/dist/{chunk-UN5O5SYG.js → chunk-YWO7O6QG.js} +14 -53
  17. package/dist/chunk-Z7YPFWAZ.js +917 -0
  18. package/dist/{chunk-BDGQEJA5.js → chunk-ZOSNEO5N.js} +2 -2
  19. package/dist/daemon/server.js +11634 -391
  20. package/dist/hook-runtime/daemon/server.js +13353 -0
  21. package/dist/hook-runtime/hooks/post-commit.js +1128 -0
  22. package/dist/hook-runtime/hooks/post-rewrite.js +1148 -0
  23. package/dist/hook-runtime/hooks/post-tool-use.js +7586 -0
  24. package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
  25. package/dist/hook-runtime/hooks/pre-tool-use.js +7099 -0
  26. package/dist/hook-runtime/hooks/prim-hook.js +7691 -0
  27. package/dist/hook-runtime/hooks/session-end.js +495 -0
  28. package/dist/hook-runtime/hooks/session-start.js +11284 -0
  29. package/dist/hook-runtime/index.js +42775 -0
  30. package/dist/hook-runtime/statusline-main.js +765 -0
  31. package/dist/hooks/post-commit.js +7 -4
  32. package/dist/hooks/post-rewrite.js +7 -4
  33. package/dist/hooks/post-tool-use.js +21 -19
  34. package/dist/hooks/pre-commit.js +4 -3
  35. package/dist/hooks/pre-tool-use.js +59 -18
  36. package/dist/hooks/prim-hook.js +39 -56
  37. package/dist/hooks/session-end.js +3 -2
  38. package/dist/hooks/session-start.js +15 -12
  39. package/dist/index.js +15050 -2479
  40. package/dist/statusline-main.js +477 -89
  41. package/package.json +11 -5
  42. package/dist/chunk-K3AYW6VZ.js +0 -300
  43. package/dist/chunk-RTLFE6EZ.js +0 -518
  44. package/dist/chunk-UTKQTZHL.js +0 -88
@@ -1,23 +1,29 @@
1
1
  import {
2
2
  formatStatusline,
3
- packageVersion,
4
- stripControlChars
5
- } from "./chunk-K3AYW6VZ.js";
3
+ packageVersion
4
+ } from "./chunk-4A2XB2PC.js";
6
5
  import {
7
6
  decisionIngestionStatus,
8
- gitToplevel
9
- } from "./chunk-TDRXHBEX.js";
7
+ gitToplevel,
8
+ repositoryBindingState
9
+ } from "./chunk-TJTVHDJ5.js";
10
10
  import {
11
11
  getSiteUrl,
12
12
  isSessionEnded,
13
+ terminalSafeText,
13
14
  withFileLock
14
- } from "./chunk-RTLFE6EZ.js";
15
+ } from "./chunk-Z7YPFWAZ.js";
15
16
  import {
16
- daemonRequest
17
- } from "./chunk-UTKQTZHL.js";
17
+ daemonPrincipalsMatch,
18
+ daemonRequest,
19
+ resolveDaemonPrincipal
20
+ } from "./chunk-CUE2LKLR.js";
21
+ import {
22
+ primConfigDirectory
23
+ } from "./chunk-3EEFL6NJ.js";
18
24
 
19
25
  // src/hooks/codex-context.ts
20
- import { createHash, randomBytes } from "crypto";
26
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
21
27
  import {
22
28
  chmodSync,
23
29
  existsSync,
@@ -30,13 +36,20 @@ import {
30
36
  unlinkSync,
31
37
  writeFileSync
32
38
  } from "fs";
33
- import { homedir } from "os";
34
39
  import { join } from "path";
35
40
 
36
41
  // src/daemon/decision-digest-cache.ts
42
+ import { randomBytes } from "crypto";
37
43
  var DECISION_DIGEST_CACHE_LIMIT = 100;
38
44
  var DECISION_DIGEST_CACHE_WINDOW = "24h";
39
45
  var DECISION_DIGEST_CACHE_PATH = `/api/cli/decisions/recent?limit=${String(DECISION_DIGEST_CACHE_LIMIT)}&since=${DECISION_DIGEST_CACHE_WINDOW}`;
46
+ var DECISION_DRAFT_DIGEST_CACHE_PATH = `${DECISION_DIGEST_CACHE_PATH}&drafts=true`;
47
+ var SAFE_PRIVATE_DRAFT_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u;
48
+ function isActionablePrivateDraft(row) {
49
+ if (typeof row !== "object" || row === null || Array.isArray(row)) return false;
50
+ const candidate = row;
51
+ return typeof candidate.id === "string" && SAFE_PRIVATE_DRAFT_ID.test(candidate.id) && typeof candidate.intent === "string" && candidate.authorIsSelf === true && candidate.stage === "draft" && (candidate.intentKind === void 0 || candidate.intentKind === "change");
52
+ }
40
53
 
41
54
  // src/hooks/codex-context.ts
42
55
  var CODEX_CONTEXT_TIMEOUT_MS = 250;
@@ -44,10 +57,13 @@ var CODEX_DIGEST_LIMIT = DECISION_DIGEST_CACHE_LIMIT;
44
57
  var CODEX_DIGEST_VISIBLE_CAP = 3;
45
58
  var CODEX_DIGEST_OVERLAP_MS = 6e4;
46
59
  var CODEX_DIGEST_MAX_SEEN_IDS = 128;
60
+ var CODEX_DRAFT_DIGEST_MAX_SEEN_IDS = 4096;
47
61
  var CODEX_DIGEST_STATE_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
48
62
  var CODEX_DIGEST_STATE_MAX_FILES = 256;
49
63
  var STATE_VERSION = 1;
50
- var STATE_DIRECTORY = [".config", "prim", "codex", "decision-digests"];
64
+ var STATE_DIRECTORY = ["codex", "decision-digests"];
65
+ var DRAFT_STATE_VERSION = 2;
66
+ var DRAFT_STATE_DIRECTORY = ["codex", "decision-draft-deliveries"];
51
67
  var NO_CURSOR = -1;
52
68
  function hasVisibleCodexMessage(output) {
53
69
  if (typeof output !== "object" || output === null || Array.isArray(output)) return false;
@@ -80,7 +96,10 @@ function appendCodexContext(output, context) {
80
96
  return next;
81
97
  }
82
98
  function stateRoot() {
83
- return join(homedir(), ...STATE_DIRECTORY);
99
+ return join(primConfigDirectory(), ...STATE_DIRECTORY);
100
+ }
101
+ function draftStateRoot() {
102
+ return join(primConfigDirectory(), ...DRAFT_STATE_DIRECTORY);
84
103
  }
85
104
  function workspaceFor(cwd) {
86
105
  try {
@@ -94,6 +113,11 @@ function statePath(args) {
94
113
  const key = createHash("sha256").update(identity).digest("hex");
95
114
  return join(stateRoot(), `${key}.json`);
96
115
  }
116
+ function draftStatePath(args) {
117
+ const identity = `${args.siteUrl}\0${workspaceFor(args.cwd)}\0${args.sessionId}\0${args.principalId}\0${args.organizationId}\0${args.credentialFingerprint}`;
118
+ const key = createHash("sha256").update(identity).digest("hex");
119
+ return join(draftStateRoot(), `${key}.json`);
120
+ }
97
121
  function parseState(value) {
98
122
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
99
123
  const record = value;
@@ -112,6 +136,24 @@ function parseState(value) {
112
136
  updatedAt: record.updatedAt
113
137
  };
114
138
  }
139
+ function parseDraftState(value) {
140
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
141
+ const record = value;
142
+ if (record.version !== DRAFT_STATE_VERSION || typeof record.sessionId !== "string" || typeof record.siteUrl !== "string" || typeof record.workspace !== "string" || typeof record.principalId !== "string" || typeof record.organizationId !== "string" || typeof record.credentialFingerprint !== "string" || !Array.isArray(record.seenDraftIds) || !record.seenDraftIds.every((id) => typeof id === "string") || typeof record.updatedAt !== "number") {
143
+ return void 0;
144
+ }
145
+ return {
146
+ version: DRAFT_STATE_VERSION,
147
+ sessionId: record.sessionId,
148
+ siteUrl: record.siteUrl,
149
+ workspace: record.workspace,
150
+ principalId: record.principalId,
151
+ organizationId: record.organizationId,
152
+ credentialFingerprint: record.credentialFingerprint,
153
+ seenDraftIds: record.seenDraftIds.slice(-CODEX_DRAFT_DIGEST_MAX_SEEN_IDS),
154
+ updatedAt: record.updatedAt
155
+ };
156
+ }
115
157
  function readState(path) {
116
158
  if (!existsSync(path)) return void 0;
117
159
  try {
@@ -120,13 +162,20 @@ function readState(path) {
120
162
  return void 0;
121
163
  }
122
164
  }
123
- function writeState(path, state) {
124
- const directory = stateRoot();
165
+ function readDraftState(path) {
166
+ if (!existsSync(path)) return void 0;
167
+ try {
168
+ return parseDraftState(JSON.parse(readFileSync(path, "utf8")));
169
+ } catch {
170
+ return void 0;
171
+ }
172
+ }
173
+ function writeStateFile(path, directory, state) {
125
174
  mkdirSync(directory, { recursive: true, mode: 448 });
126
175
  chmodSync(directory, 448);
127
176
  const temporary = join(
128
177
  directory,
129
- `.${path.slice(directory.length + 1)}.tmp-${process.pid}-${randomBytes(8).toString("hex")}`
178
+ `.${path.slice(directory.length + 1)}.tmp-${process.pid}-${randomBytes2(8).toString("hex")}`
130
179
  );
131
180
  try {
132
181
  writeFileSync(temporary, `${JSON.stringify(state)}
@@ -144,8 +193,7 @@ function writeState(path, state) {
144
193
  }
145
194
  }
146
195
  }
147
- function cleanupStateFiles(now) {
148
- const directory = stateRoot();
196
+ function cleanupStateFiles(directory, now) {
149
197
  if (!existsSync(directory)) return;
150
198
  let names;
151
199
  try {
@@ -188,7 +236,7 @@ function isChangeDecision(row) {
188
236
  return row.intentKind === void 0 || row.intentKind === "change";
189
237
  }
190
238
  function safeInline(value, fallback) {
191
- const clean = stripControlChars(value ?? "").replace(/\s+/gu, " ").trim();
239
+ const clean = terminalSafeText(value ?? "").replace(/\s+/gu, " ").trim();
192
240
  if (!clean) return fallback;
193
241
  return clean.length <= 240 ? clean : `${clean.slice(0, 239)}\u2026`;
194
242
  }
@@ -196,12 +244,41 @@ function renderDecisionDigest(rows, options = {}) {
196
244
  if (rows.length === 0) return void 0;
197
245
  const visible = rows.slice(0, CODEX_DIGEST_VISIBLE_CAP).map((row) => {
198
246
  const author = safeInline(row.authorName, "(unknown)").replace(/[;;;‒-―−]/gu, "-");
199
- const intent = safeInline(row.intent, "(untitled Decision)").replace(/[“-‟″‶〝〞]/gu, "'");
247
+ const intent = safeInline(row.intent, "(untitled Decision)").replace(/[`“-‟″‶〝〞]/gu, "'");
200
248
  return `${author} \u2014 \u201C${intent}\u201D`;
201
249
  });
202
250
  const overflow = rows.length > CODEX_DIGEST_VISIBLE_CAP ? ` +${String(rows.length - CODEX_DIGEST_VISIBLE_CAP)}${options.pageTruncated === true ? "+" : ""}` : "";
203
251
  return `[prim] Decisions captured since last message: ${visible.join("; ")}${overflow}`;
204
252
  }
253
+ function actionablePrivateDrafts(rows) {
254
+ const seen = /* @__PURE__ */ new Set();
255
+ const actionable = [];
256
+ for (const row of rows) {
257
+ if (!isActionablePrivateDraft(row) || seen.has(row.id)) continue;
258
+ seen.add(row.id);
259
+ actionable.push(row);
260
+ }
261
+ return actionable;
262
+ }
263
+ function renderDecisionDraftDigest(rows, options = {}) {
264
+ const eligible = actionablePrivateDrafts(rows);
265
+ if (eligible.length === 0) return void 0;
266
+ const visible = eligible.slice(0, CODEX_DIGEST_VISIBLE_CAP);
267
+ const lines = visible.flatMap((row) => {
268
+ const intent = safeInline(row.intent, "(untitled Decision)").replace(/[“-‟″‶〝〞]/gu, "'");
269
+ return [
270
+ `[prim] private Decision ${row.id} \xB7 stage: draft \xB7 \u201C${intent}\u201D`,
271
+ `[prim] Run \`prim decisions publish ${row.id}\` to share it with your team.`
272
+ ];
273
+ });
274
+ if (eligible.length > visible.length) {
275
+ const suffix = options.pageTruncated === true ? "+" : "";
276
+ lines.push(
277
+ `[prim] +${String(eligible.length - visible.length)}${suffix} private Decision drafts remain for later messages.`
278
+ );
279
+ }
280
+ return { message: lines.join("\n"), deliveredIds: visible.map((row) => row.id) };
281
+ }
205
282
  function reauthSnapshot(sessionId) {
206
283
  return {
207
284
  pid: 0,
@@ -236,29 +313,73 @@ async function commitState(path, args) {
236
313
  `${path}.lock`,
237
314
  () => {
238
315
  const latest = readState(path);
239
- writeState(path, mergeState(latest, args));
240
- cleanupStateFiles(args.now);
316
+ writeStateFile(path, stateRoot(), mergeState(latest, args));
317
+ cleanupStateFiles(stateRoot(), args.now);
241
318
  },
242
319
  { timeoutMs: 50, pollMs: 10 }
243
320
  );
244
321
  } catch {
245
322
  }
246
323
  }
324
+ function mergeDraftState(latest, args) {
325
+ const seen = new Set(latest?.seenDraftIds ?? []);
326
+ for (const id of args.deliveredIds) seen.add(id);
327
+ return {
328
+ version: DRAFT_STATE_VERSION,
329
+ sessionId: args.sessionId,
330
+ siteUrl: args.siteUrl,
331
+ workspace: args.workspace,
332
+ principalId: args.principalId,
333
+ organizationId: args.organizationId,
334
+ credentialFingerprint: args.credentialFingerprint,
335
+ seenDraftIds: [...seen].slice(-CODEX_DRAFT_DIGEST_MAX_SEEN_IDS),
336
+ updatedAt: args.now
337
+ };
338
+ }
339
+ async function commitDraftState(path, args) {
340
+ try {
341
+ let next;
342
+ await withFileLock(
343
+ `${path}.lock`,
344
+ () => {
345
+ const latest = readDraftState(path);
346
+ next = mergeDraftState(latest, args);
347
+ writeStateFile(path, draftStateRoot(), next);
348
+ cleanupStateFiles(draftStateRoot(), args.now);
349
+ },
350
+ { timeoutMs: 50, pollMs: 10 }
351
+ );
352
+ return next;
353
+ } catch {
354
+ return void 0;
355
+ }
356
+ }
247
357
  async function prepareCodexContext(options) {
248
358
  const siteUrl = getSiteUrl();
249
359
  const workspace = workspaceFor(options.cwd);
250
360
  const path = statePath({ cwd: options.cwd, sessionId: options.sessionId, siteUrl });
251
361
  const previous = readState(path);
362
+ const principal = resolveDaemonPrincipal();
363
+ const privatePath = principal ? draftStatePath({
364
+ cwd: options.cwd,
365
+ sessionId: options.sessionId,
366
+ siteUrl,
367
+ principalId: principal.principalId,
368
+ organizationId: principal.organizationId,
369
+ credentialFingerprint: principal.credentialFingerprint
370
+ }) : void 0;
371
+ const previousDraft = privatePath ? readDraftState(privatePath) : void 0;
252
372
  const startup = options.startup === true;
253
373
  const includeDigest = options.includeDigest !== false;
254
374
  const startedAt = previous?.startedAt ?? Date.now();
255
375
  const terminalAuth = isSessionEnded();
256
376
  let snapshot = null;
257
377
  let recent = null;
378
+ let recentDrafts = null;
258
379
  if (terminalAuth) {
259
380
  snapshot = reauthSnapshot(options.sessionId);
260
381
  } else {
261
- [snapshot, recent] = await Promise.all([
382
+ [snapshot, recent, recentDrafts] = await Promise.all([
262
383
  daemonRequest(
263
384
  "status_snapshot",
264
385
  { callerEnv: siteUrl },
@@ -268,15 +389,29 @@ async function prepareCodexContext(options) {
268
389
  "decision_digest_snapshot",
269
390
  { callerEnv: siteUrl },
270
391
  { timeoutMs: CODEX_CONTEXT_TIMEOUT_MS }
392
+ ) : Promise.resolve(null),
393
+ includeDigest && principal !== void 0 ? daemonRequest(
394
+ "decision_draft_digest_snapshot",
395
+ { callerEnv: siteUrl },
396
+ { timeoutMs: CODEX_CONTEXT_TIMEOUT_MS }
271
397
  ) : Promise.resolve(null)
272
398
  ]);
399
+ if (principal !== void 0 && !daemonPrincipalsMatch(principal, resolveDaemonPrincipal())) {
400
+ snapshot = null;
401
+ recent = null;
402
+ recentDrafts = null;
403
+ }
273
404
  }
274
- const report = formatStatusline(
275
- packageVersion() ?? "0.0.0",
276
- snapshot,
277
- () => decisionIngestionStatus(options.cwd),
278
- { includeIngestionWhenUnavailable: true, plainLinks: true }
279
- );
405
+ let ingestionStatus;
406
+ const resolveIngestionStatus = () => {
407
+ ingestionStatus ??= decisionIngestionStatus(options.cwd);
408
+ return ingestionStatus;
409
+ };
410
+ const report = formatStatusline(packageVersion() ?? "0.0.0", snapshot, resolveIngestionStatus, {
411
+ includeIngestionWhenUnavailable: true,
412
+ plainLinks: true,
413
+ resolveRepositoryBindingState: () => resolveIngestionStatus() === "enabled" ? repositoryBindingState(options.cwd) : void 0
414
+ });
280
415
  const reportChanged = startup || previous?.lastReport !== report;
281
416
  let feedAvailable = false;
282
417
  let freshRows = [];
@@ -298,11 +433,25 @@ async function prepareCodexContext(options) {
298
433
  }
299
434
  }
300
435
  const digest = renderDecisionDigest(freshRows, { pageTruncated });
301
- const context = [reportChanged ? report : void 0, digest].filter((value) => value !== void 0).join("\n\n") || void 0;
436
+ let draftDigest;
437
+ let draftPageToken;
438
+ let draftPageActionableIds = [];
439
+ if (!terminalAuth && includeDigest && privatePath !== void 0 && recentDrafts !== null && recentDrafts.unavailable === void 0 && Array.isArray(recentDrafts.decisions)) {
440
+ const allActionableDrafts = actionablePrivateDrafts(recentDrafts.decisions);
441
+ const seenDrafts = new Set(previousDraft?.seenDraftIds ?? []);
442
+ const freshDrafts = allActionableDrafts.filter((row) => !seenDrafts.has(row.id));
443
+ draftDigest = renderDecisionDraftDigest(freshDrafts, {
444
+ pageTruncated: recentDrafts.pageDone === false
445
+ });
446
+ draftPageToken = typeof recentDrafts.pageToken === "string" ? recentDrafts.pageToken : void 0;
447
+ draftPageActionableIds = allActionableDrafts.map((row) => row.id);
448
+ }
449
+ const combinedDigest = [digest, draftDigest?.message].filter((value) => value !== void 0).join("\n\n");
450
+ const context = [reportChanged ? report : void 0, combinedDigest || void 0].filter((value) => value !== void 0).join("\n\n") || void 0;
302
451
  let acknowledged = false;
303
452
  return {
304
453
  context,
305
- decisionDigest: digest,
454
+ decisionDigest: combinedDigest || void 0,
306
455
  feedAvailable,
307
456
  acknowledge: async (handedOff) => {
308
457
  if (!handedOff || acknowledged) return;
@@ -318,6 +467,33 @@ async function prepareCodexContext(options) {
318
467
  feedAvailable,
319
468
  now: Date.now()
320
469
  });
470
+ if (privatePath === void 0 || principal === void 0 || draftPageToken === void 0) {
471
+ return;
472
+ }
473
+ let persistedDraft = previousDraft;
474
+ if (draftDigest?.deliveredIds.length) {
475
+ persistedDraft = await commitDraftState(privatePath, {
476
+ sessionId: options.sessionId,
477
+ siteUrl,
478
+ workspace,
479
+ principalId: principal.principalId,
480
+ organizationId: principal.organizationId,
481
+ credentialFingerprint: principal.credentialFingerprint,
482
+ deliveredIds: draftDigest.deliveredIds,
483
+ now: Date.now()
484
+ });
485
+ }
486
+ const seenDrafts = new Set(persistedDraft?.seenDraftIds ?? []);
487
+ const deliveredOnThisPage = draftPageActionableIds.filter((id) => seenDrafts.has(id));
488
+ await daemonRequest(
489
+ "decision_draft_digest_acknowledge",
490
+ {
491
+ callerEnv: siteUrl,
492
+ pageToken: draftPageToken,
493
+ deliveredIds: deliveredOnThisPage
494
+ },
495
+ { timeoutMs: CODEX_CONTEXT_TIMEOUT_MS }
496
+ );
321
497
  }
322
498
  };
323
499
  }
@@ -0,0 +1,79 @@
1
+ import {
2
+ terminalSafeLine
3
+ } from "./chunk-Z7YPFWAZ.js";
4
+
5
+ // src/lib/ansi.ts
6
+ var ANSI_CODES = {
7
+ yellow: "\x1B[33m",
8
+ magenta: "\x1B[35m",
9
+ blue: "\x1B[34m",
10
+ cyan: "\x1B[36m",
11
+ green: "\x1B[32m",
12
+ red: "\x1B[31m",
13
+ // 256-color approximation of the orange trigger marker (xterm 208).
14
+ orange: "\x1B[38;5;208m",
15
+ gray: "\x1B[90m"
16
+ };
17
+ var ANSI_RESET = "\x1B[0m";
18
+ var ANSI_DIM = "\x1B[2m";
19
+ var ANSI_BOLD = "\x1B[1m";
20
+ function supportsColor() {
21
+ if (process.env.NO_COLOR !== void 0 && process.env.NO_COLOR !== "") {
22
+ return false;
23
+ }
24
+ return process.stderr.isTTY === true;
25
+ }
26
+ function color(text, c) {
27
+ if (!supportsColor()) {
28
+ return text;
29
+ }
30
+ return `${ANSI_CODES[c]}${text}${ANSI_RESET}`;
31
+ }
32
+ function dim(text) {
33
+ if (!supportsColor()) {
34
+ return text;
35
+ }
36
+ return `${ANSI_DIM}${text}${ANSI_RESET}`;
37
+ }
38
+ function bold(text) {
39
+ if (!supportsColor()) {
40
+ return text;
41
+ }
42
+ return `${ANSI_BOLD}${text}${ANSI_RESET}`;
43
+ }
44
+ var AREA_COLORS = {
45
+ auth: "yellow",
46
+ data: "magenta",
47
+ mobile: "blue",
48
+ infra: "cyan",
49
+ ui: "green",
50
+ billing: "orange",
51
+ api: "blue",
52
+ docs: "gray",
53
+ testing: "gray"
54
+ };
55
+ function colorForArea(area) {
56
+ if (!area) {
57
+ return "gray";
58
+ }
59
+ return AREA_COLORS[area] ?? "gray";
60
+ }
61
+ var MAX_HEALTH_ERROR_LENGTH = 240;
62
+ function boundedHealthError(value) {
63
+ if (!value) return void 0;
64
+ const clean = terminalSafeLine(value);
65
+ if (!clean) return void 0;
66
+ return clean.length <= MAX_HEALTH_ERROR_LENGTH ? clean : `${clean.slice(0, MAX_HEALTH_ERROR_LENGTH - 1)}\u2026`;
67
+ }
68
+ function stripAnsi(text) {
69
+ return text.replace(/\u001b\[[0-9;]*m/g, "");
70
+ }
71
+
72
+ export {
73
+ color,
74
+ dim,
75
+ bold,
76
+ colorForArea,
77
+ boundedHealthError,
78
+ stripAnsi
79
+ };
@@ -1,9 +1,13 @@
1
1
  import {
2
- getClient
3
- } from "./chunk-RTLFE6EZ.js";
2
+ getClient,
3
+ isTerminalSafeText,
4
+ terminalSafeLine,
5
+ terminalSafeText
6
+ } from "./chunk-Z7YPFWAZ.js";
4
7
 
5
8
  // src/decisions/feedback.ts
6
- var FEEDBACK_PROTOCOL_VERSION = 1;
9
+ var FEEDBACK_PROTOCOL_VERSION = 2;
10
+ var FEEDBACK_MIN_PROTOCOL_VERSION = 1;
7
11
  var FEEDBACK_DEADLINE_MS = 3e3;
8
12
  var MAX_FEEDBACK_EVENTS = 40;
9
13
  var MAX_FEEDBACK_MESSAGE_CODE_POINTS = 8e3;
@@ -15,38 +19,15 @@ var SHORT_ID = /^[0-9a-f]{8}$/u;
15
19
  var MAX_EVENT_ID_CHARS = 128;
16
20
  var MAX_RAW_INTENT_CODE_UNITS = 512;
17
21
  var MAX_FEEDBACK_WEB_URL_CHARS = 2048;
22
+ var SAFE_PUBLISH_DECISION_ID = /^[A-Za-z0-9_-]+$/u;
18
23
  function isRecord(value) {
19
24
  return typeof value === "object" && value !== null && !Array.isArray(value);
20
25
  }
21
- function isUnsafeControl(point) {
22
- const c0OrC1 = point <= 31 || point >= 127 && point <= 159;
23
- const bidiControl = point === 1564 || point === 8206 || point === 8207 || point >= 8234 && point <= 8238 || point >= 8294 && point <= 8297;
24
- return c0OrC1 || bidiControl;
25
- }
26
- function replaceIsolatedSurrogates(value) {
27
- let out = "";
28
- for (let index = 0; index < value.length; index += 1) {
29
- const unit = value.charCodeAt(index);
30
- if (unit >= 55296 && unit <= 56319) {
31
- const next = value.charCodeAt(index + 1);
32
- if (next >= 56320 && next <= 57343) {
33
- out += value[index] + value[index + 1];
34
- index += 1;
35
- } else {
36
- out += "\uFFFD";
37
- }
38
- } else if (unit >= 56320 && unit <= 57343) {
39
- out += "\uFFFD";
40
- } else {
41
- out += value[index];
42
- }
43
- }
44
- return out;
26
+ function isSupportedProtocolVersion(value) {
27
+ return Number.isSafeInteger(value) && Number(value) >= FEEDBACK_MIN_PROTOCOL_VERSION && Number(value) <= FEEDBACK_PROTOCOL_VERSION;
45
28
  }
46
29
  function normalizeFeedbackIntent(value) {
47
- const safeUnicode = replaceIsolatedSurrogates(value);
48
- const whitespace = safeUnicode.replace(/\s+/gu, " ");
49
- const withoutControls = Array.from(whitespace).filter((character) => !isUnsafeControl(character.codePointAt(0) ?? 0)).join("").replace(/\s+/gu, " ").trim();
30
+ const withoutControls = terminalSafeLine(value);
50
31
  const points = Array.from(withoutControls);
51
32
  if (points.length <= MAX_FEEDBACK_INTENT_CODE_POINTS) {
52
33
  return withoutControls;
@@ -54,7 +35,7 @@ function normalizeFeedbackIntent(value) {
54
35
  return `${points.slice(0, MAX_FEEDBACK_INTENT_CODE_POINTS - 1).join("")}\u2026`;
55
36
  }
56
37
  function parseFeedbackWebUrl(value) {
57
- if (typeof value !== "string" || value.length === 0 || value.length > MAX_FEEDBACK_WEB_URL_CHARS || !/^https:\/\//iu.test(value) || /\s/u.test(value) || replaceIsolatedSurrogates(value) !== value || Array.from(value).some((character) => isUnsafeControl(character.codePointAt(0) ?? 0))) {
38
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_FEEDBACK_WEB_URL_CHARS || !/^https:\/\//iu.test(value) || /\s/u.test(value) || !isTerminalSafeText(value)) {
58
39
  return void 0;
59
40
  }
60
41
  try {
@@ -67,9 +48,15 @@ function parseFeedbackWebUrl(value) {
67
48
  }
68
49
  return value;
69
50
  }
70
- function parseEvent(value) {
51
+ function parseFeedbackDecisionId(value) {
52
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_EVENT_ID_CHARS || terminalSafeText(value) !== value || !SAFE_PUBLISH_DECISION_ID.test(value)) {
53
+ return void 0;
54
+ }
55
+ return value;
56
+ }
57
+ function parseEvent(value, protocolVersion) {
71
58
  if (!isRecord(value)) return void 0;
72
- if (typeof value.eventId !== "string" || value.eventId.length === 0 || value.eventId.length > MAX_EVENT_ID_CHARS || Array.from(value.eventId).some((character) => isUnsafeControl(character.codePointAt(0) ?? 0))) {
59
+ if (typeof value.eventId !== "string" || value.eventId.length === 0 || value.eventId.length > MAX_EVENT_ID_CHARS || terminalSafeText(value.eventId) !== value.eventId) {
73
60
  return void 0;
74
61
  }
75
62
  if (!Number.isSafeInteger(value.leaseVersion) || Number(value.leaseVersion) < 1) {
@@ -85,40 +72,58 @@ function parseEvent(value) {
85
72
  if (!intent) return void 0;
86
73
  const webUrl = value.webUrl === void 0 ? void 0 : parseFeedbackWebUrl(value.webUrl);
87
74
  if (value.webUrl !== void 0 && webUrl === void 0) return void 0;
75
+ const kind = value.kind === void 0 && protocolVersion === 1 ? "confirm_prompt" : value.kind;
76
+ if (kind !== "confirm_prompt" && kind !== "publish_prompt") return void 0;
77
+ if (protocolVersion === 1 && kind === "publish_prompt") return void 0;
78
+ const decisionId = value.decisionId === void 0 ? void 0 : parseFeedbackDecisionId(value.decisionId);
79
+ if (value.decisionId !== void 0 && decisionId === void 0) return void 0;
80
+ if (decisionId !== void 0 && (protocolVersion !== 2 || kind !== "publish_prompt")) {
81
+ return void 0;
82
+ }
83
+ if (protocolVersion === 2 && kind === "publish_prompt" && decisionId === void 0) {
84
+ return void 0;
85
+ }
88
86
  return {
89
87
  eventId: value.eventId,
90
88
  leaseVersion: Number(value.leaseVersion),
91
89
  shortId: value.shortId,
92
90
  intent,
93
- ...webUrl === void 0 ? {} : { webUrl }
91
+ ...webUrl === void 0 ? {} : { webUrl },
92
+ ...decisionId === void 0 ? {} : { decisionId },
93
+ kind
94
94
  };
95
95
  }
96
96
  function parseFeedbackLease(value) {
97
- if (!isRecord(value) || value.protocolVersion !== FEEDBACK_PROTOCOL_VERSION) return void 0;
97
+ if (!isRecord(value) || !isSupportedProtocolVersion(value.protocolVersion)) return void 0;
98
+ const protocolVersion = value.protocolVersion;
98
99
  if (value.status === "empty") {
99
- return value.hasMore === false ? { events: [], hasMore: false } : void 0;
100
+ return value.hasMore === false ? { protocolVersion, events: [], hasMore: false } : void 0;
100
101
  }
101
102
  if (value.status === "unavailable") {
102
- return value.reason === "organization_unbound" ? { events: [], hasMore: false } : void 0;
103
+ return value.reason === "organization_unbound" ? { protocolVersion, events: [], hasMore: false } : void 0;
103
104
  }
104
105
  if (value.status !== "leased" || typeof value.hasMore !== "boolean") return void 0;
105
106
  if (!Array.isArray(value.events) || value.events.length === 0 || value.events.length > MAX_FEEDBACK_EVENTS) {
106
107
  return void 0;
107
108
  }
108
- const events = value.events.map(parseEvent);
109
+ const events = value.events.map((event) => parseEvent(event, protocolVersion));
109
110
  if (events.some((event) => event === void 0)) return void 0;
110
111
  const parsedEvents = events;
111
112
  if (new Set(parsedEvents.map((event) => event.eventId)).size !== parsedEvents.length) {
112
113
  return void 0;
113
114
  }
114
- return { events: parsedEvents, hasMore: value.hasMore };
115
+ return { protocolVersion, events: parsedEvents, hasMore: value.hasMore };
115
116
  }
116
117
  function renderFeedback(lease) {
117
118
  const lines = [];
118
119
  const deliveries = [];
119
120
  let pointCount = 0;
120
121
  for (const event of lease.events) {
121
- const line = `[prim] response \u2192 created Decision (dec_${event.shortId}): ${event.intent}${event.webUrl ? ` (${event.webUrl})` : ""}`;
122
+ const decisionId = event.kind === "publish_prompt" ? parseFeedbackDecisionId(event.decisionId) : void 0;
123
+ if (event.kind === "publish_prompt" && decisionId === void 0) continue;
124
+ const identifier = `dec_${event.shortId}`;
125
+ const detail = `${event.intent}${event.webUrl ? ` (${event.webUrl})` : ""}`;
126
+ const line = event.kind === "publish_prompt" ? `[prim] publish this Decision draft (${identifier})? ${detail} Run \`prim decisions publish ${decisionId}\` to share it with your team.` : `[prim] response \u2192 created Decision (${identifier}): ${detail}`;
122
127
  const extra = Array.from(line).length + (lines.length === 0 ? 0 : 1);
123
128
  if (pointCount + extra > MAX_FEEDBACK_MESSAGE_CODE_POINTS) break;
124
129
  lines.push(line);
@@ -126,7 +131,7 @@ function renderFeedback(lease) {
126
131
  pointCount += extra;
127
132
  }
128
133
  if (lines.length === 0) return void 0;
129
- return { systemMessage: lines.join("\n"), deliveries };
134
+ return { protocolVersion: lease.protocolVersion, systemMessage: lines.join("\n"), deliveries };
130
135
  }
131
136
  async function leaseDecisionFeedback(input, dependencies = {}) {
132
137
  try {
@@ -152,12 +157,14 @@ async function leaseDecisionFeedback(input, dependencies = {}) {
152
157
  }
153
158
  }
154
159
  async function acknowledgeDecisionFeedback(input, dependencies = {}) {
155
- if (input.deliveries.length === 0 || input.deliveries.length > MAX_FEEDBACK_EVENTS) return false;
160
+ if (!isSupportedProtocolVersion(input.protocolVersion) || input.deliveries.length === 0 || input.deliveries.length > MAX_FEEDBACK_EVENTS) {
161
+ return false;
162
+ }
156
163
  try {
157
164
  const response = await (dependencies.client ?? getClient()).post(
158
165
  ACK_PATH,
159
166
  {
160
- protocolVersion: FEEDBACK_PROTOCOL_VERSION,
167
+ protocolVersion: input.protocolVersion,
161
168
  workspaceId: input.workspaceId,
162
169
  deliveries: input.deliveries
163
170
  },
@@ -165,7 +172,7 @@ async function acknowledgeDecisionFeedback(input, dependencies = {}) {
165
172
  );
166
173
  const acknowledgedEventIds = isRecord(response) ? response.acknowledgedEventIds : void 0;
167
174
  const expectedEventIds = new Set(input.deliveries.map((delivery) => delivery.eventId));
168
- const valid = isRecord(response) && response.protocolVersion === FEEDBACK_PROTOCOL_VERSION && response.status === "acked" && Array.isArray(acknowledgedEventIds) && acknowledgedEventIds.length <= input.deliveries.length && acknowledgedEventIds.every(
175
+ const valid = isRecord(response) && response.protocolVersion === input.protocolVersion && response.status === "acked" && Array.isArray(acknowledgedEventIds) && acknowledgedEventIds.length <= input.deliveries.length && acknowledgedEventIds.every(
169
176
  (id) => typeof id === "string" && id.length > 0 && id.length <= MAX_EVENT_ID_CHARS && expectedEventIds.has(id)
170
177
  ) && new Set(acknowledgedEventIds).size === acknowledgedEventIds.length;
171
178
  if (!valid) {
@@ -180,7 +187,7 @@ async function acknowledgeDecisionFeedback(input, dependencies = {}) {
180
187
  }
181
188
  }
182
189
  function parseFeedbackCapability(value) {
183
- if (!isRecord(value) || value.protocolVersion !== FEEDBACK_PROTOCOL_VERSION) return void 0;
190
+ if (!isRecord(value) || !isSupportedProtocolVersion(value.protocolVersion)) return void 0;
184
191
  if (value.status === "available") return { status: "available" };
185
192
  if (value.status === "unavailable" && value.reason === "organization_unbound") {
186
193
  return { status: "unavailable", reason: "organization_unbound" };