@primitive.ai/prim 0.1.0-alpha.9 → 0.1.0-alpha.90
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/README.md +295 -37
- package/SKILL.md +269 -0
- package/dist/chunk-2JRX7ZR4.js +247 -0
- package/dist/chunk-3EEFL6NJ.js +529 -0
- package/dist/chunk-4GLVJLEQ.js +55 -0
- package/dist/chunk-6IWNH776.js +77 -0
- package/dist/chunk-C7T7B45R.js +1400 -0
- package/dist/chunk-CUE2LKLR.js +155 -0
- package/dist/chunk-EZXEWFDG.js +210 -0
- package/dist/chunk-FI3LIRCB.js +262 -0
- package/dist/chunk-LEMX3IOG.js +79 -0
- package/dist/chunk-LMRYZL3D.js +209 -0
- package/dist/chunk-MAZWKJGL.js +18548 -0
- package/dist/chunk-PMRDE2DA.js +310 -0
- package/dist/chunk-PPQYQEIR.js +815 -0
- package/dist/chunk-R26A7TWY.js +36 -0
- package/dist/chunk-RO3F6JYV.js +126 -0
- package/dist/chunk-UZPBB7V4.js +505 -0
- package/dist/chunk-WB7AUZ4C.js +52 -0
- package/dist/chunk-YWO7O6QG.js +327 -0
- package/dist/chunk-Z7YPFWAZ.js +917 -0
- package/dist/daemon/server.js +21633 -0
- package/dist/hook-runtime/daemon/server.js +21633 -0
- package/dist/hook-runtime/hooks/post-commit.js +19258 -0
- package/dist/hook-runtime/hooks/post-rewrite.js +19273 -0
- package/dist/hook-runtime/hooks/post-tool-use.js +25846 -0
- package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
- package/dist/hook-runtime/hooks/pre-tool-use.js +25284 -0
- package/dist/hook-runtime/hooks/prim-hook.js +25870 -0
- package/dist/hook-runtime/hooks/session-end.js +495 -0
- package/dist/hook-runtime/hooks/session-start.js +28180 -0
- package/dist/hook-runtime/index.js +50516 -0
- package/dist/hook-runtime/statusline-main.js +765 -0
- package/dist/hooks/post-commit.js +188 -0
- package/dist/hooks/post-rewrite.js +201 -0
- package/dist/hooks/post-tool-use.js +263 -0
- package/dist/hooks/pre-commit.js +16 -101
- package/dist/hooks/pre-tool-use.js +367 -0
- package/dist/hooks/prim-hook.js +207 -0
- package/dist/hooks/session-end.js +70 -0
- package/dist/hooks/session-start.js +289 -0
- package/dist/index.js +9749 -425
- package/dist/statusline-main.js +765 -0
- package/package.json +35 -13
- package/dist/chunk-3APLWTLB.js +0 -165
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
scheduleCollectScopeRefresh
|
|
3
|
+
} from "./chunk-MAZWKJGL.js";
|
|
4
|
+
|
|
5
|
+
// src/ingest-response.ts
|
|
6
|
+
var IngestAcknowledgementError = class extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "IngestAcknowledgementError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function requireDurableIngestAcknowledgement(value, expected, cwd = process.cwd()) {
|
|
13
|
+
if (typeof value !== "object" || value === null) {
|
|
14
|
+
throw new IngestAcknowledgementError("ingest response was not an object");
|
|
15
|
+
}
|
|
16
|
+
const response = value;
|
|
17
|
+
if (response.disposition !== "persisted") {
|
|
18
|
+
throw new IngestAcknowledgementError("ingest response did not confirm persistence");
|
|
19
|
+
}
|
|
20
|
+
if (response.acknowledged !== expected) {
|
|
21
|
+
throw new IngestAcknowledgementError(
|
|
22
|
+
`ingest acknowledged ${String(response.acknowledged)} of ${String(expected)} moves`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
if (typeof response.collectScopeVersion === "number" && Number.isSafeInteger(response.collectScopeVersion) && response.collectScopeVersion >= 0) {
|
|
26
|
+
const roots = typeof cwd === "string" ? [cwd] : cwd;
|
|
27
|
+
for (const root of new Set(roots)) {
|
|
28
|
+
scheduleCollectScopeRefresh(root, response.collectScopeVersion);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
requireDurableIngestAcknowledgement
|
|
36
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getClient,
|
|
3
|
+
getSiteUrl,
|
|
4
|
+
terminalSafeLine
|
|
5
|
+
} from "./chunk-Z7YPFWAZ.js";
|
|
6
|
+
import {
|
|
7
|
+
daemonRequest
|
|
8
|
+
} from "./chunk-CUE2LKLR.js";
|
|
9
|
+
|
|
10
|
+
// src/daemon/proxy.ts
|
|
11
|
+
var DAEMON_HTTP_TIMEOUT_MS = 1e4;
|
|
12
|
+
var DAEMON_PROBE_TIMEOUT_MS = 250;
|
|
13
|
+
async function daemonOrDirect(method, params, direct) {
|
|
14
|
+
const fromDaemon = await daemonRequest(
|
|
15
|
+
method,
|
|
16
|
+
{ ...params, callerEnv: getSiteUrl() },
|
|
17
|
+
{ timeoutMs: DAEMON_PROBE_TIMEOUT_MS }
|
|
18
|
+
);
|
|
19
|
+
if (fromDaemon !== null) {
|
|
20
|
+
return fromDaemon;
|
|
21
|
+
}
|
|
22
|
+
return await direct();
|
|
23
|
+
}
|
|
24
|
+
async function daemonOrDirectGet(method, path, client, timeoutMs = DAEMON_HTTP_TIMEOUT_MS) {
|
|
25
|
+
return await daemonOrDirect(
|
|
26
|
+
method,
|
|
27
|
+
{ path },
|
|
28
|
+
async () => await client.get(path, {
|
|
29
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/hooks/decisions-check.ts
|
|
35
|
+
var DECISIONS_CHECK_TIMEOUT_MS = 1e4;
|
|
36
|
+
var MAX_FILES_PER_REQUEST = 25;
|
|
37
|
+
var defaultDeps = { getClient };
|
|
38
|
+
function chunk(items, size) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (let i = 0; i < items.length; i += size) {
|
|
41
|
+
out.push(items.slice(i, i + size));
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
async function fetchAffecting(client, batch) {
|
|
46
|
+
const params = new URLSearchParams();
|
|
47
|
+
for (const file of batch) {
|
|
48
|
+
params.append("files", file);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return await daemonOrDirectGet(
|
|
52
|
+
"decisions_affecting",
|
|
53
|
+
`/api/cli/decisions/affecting?${params.toString()}`,
|
|
54
|
+
client,
|
|
55
|
+
DECISIONS_CHECK_TIMEOUT_MS
|
|
56
|
+
);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
59
|
+
return { decisions: [], truncated: false, unavailable: `decision check failed: ${detail}` };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function checkAffectedDecisions(filePaths, deps = defaultDeps) {
|
|
63
|
+
if (filePaths.length === 0) {
|
|
64
|
+
return { decisions: [], truncated: false };
|
|
65
|
+
}
|
|
66
|
+
const client = deps.getClient();
|
|
67
|
+
const responses = await Promise.all(
|
|
68
|
+
chunk(filePaths, MAX_FILES_PER_REQUEST).map((batch) => fetchAffecting(client, batch))
|
|
69
|
+
);
|
|
70
|
+
const byId = /* @__PURE__ */ new Map();
|
|
71
|
+
let truncated = false;
|
|
72
|
+
let unavailable;
|
|
73
|
+
for (const res of responses) {
|
|
74
|
+
if (res.unavailable !== void 0 && unavailable === void 0) {
|
|
75
|
+
unavailable = res.unavailable;
|
|
76
|
+
}
|
|
77
|
+
truncated ||= res.truncated === true;
|
|
78
|
+
for (const d of res.decisions) {
|
|
79
|
+
const existing = byId.get(d.id);
|
|
80
|
+
if (existing) {
|
|
81
|
+
existing.matchedFiles = [.../* @__PURE__ */ new Set([...existing.matchedFiles, ...d.matchedFiles])];
|
|
82
|
+
} else {
|
|
83
|
+
byId.set(d.id, { ...d, matchedFiles: [...d.matchedFiles] });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const result = { decisions: [...byId.values()], truncated };
|
|
88
|
+
if (unavailable !== void 0) {
|
|
89
|
+
result.unavailable = unavailable;
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
var FILES_PREVIEW_LIMIT = 3;
|
|
94
|
+
function formatDecisionsWarning(result) {
|
|
95
|
+
const lines = [];
|
|
96
|
+
if (result.unavailable !== void 0) {
|
|
97
|
+
lines.push(`[prim] decision check not verified \u2014 ${terminalSafeLine(result.unavailable)}`);
|
|
98
|
+
}
|
|
99
|
+
if (result.decisions.length > 0) {
|
|
100
|
+
lines.push(
|
|
101
|
+
`[prim] ${String(result.decisions.length)} active decision(s) reference staged files:`
|
|
102
|
+
);
|
|
103
|
+
for (const d of result.decisions) {
|
|
104
|
+
const statusMark = d.status === "under_review" ? " (under review)" : "";
|
|
105
|
+
const preview = d.matchedFiles.slice(0, FILES_PREVIEW_LIMIT).map(terminalSafeLine).join(", ");
|
|
106
|
+
const overflow = d.matchedFiles.length > FILES_PREVIEW_LIMIT ? ` (+${String(d.matchedFiles.length - FILES_PREVIEW_LIMIT)} more)` : "";
|
|
107
|
+
lines.push(` \xB7 ${terminalSafeLine(d.intent)}${statusMark}`);
|
|
108
|
+
lines.push(` files: ${preview}${overflow}`);
|
|
109
|
+
if (d.rationale) {
|
|
110
|
+
lines.push(` rationale: ${terminalSafeLine(d.rationale)}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (result.truncated) {
|
|
115
|
+
lines.push(
|
|
116
|
+
"[prim] result truncated \u2014 more files than the server checks per request; not all decisions shown"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return lines.join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export {
|
|
123
|
+
daemonOrDirectGet,
|
|
124
|
+
checkAffectedDecisions,
|
|
125
|
+
formatDecisionsWarning
|
|
126
|
+
};
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatStatusline,
|
|
3
|
+
packageVersion
|
|
4
|
+
} from "./chunk-PMRDE2DA.js";
|
|
5
|
+
import {
|
|
6
|
+
decisionIngestionStatus,
|
|
7
|
+
gitToplevel,
|
|
8
|
+
repositoryBindingState
|
|
9
|
+
} from "./chunk-MAZWKJGL.js";
|
|
10
|
+
import {
|
|
11
|
+
getSiteUrl,
|
|
12
|
+
isSessionEnded,
|
|
13
|
+
terminalSafeText,
|
|
14
|
+
withFileLock
|
|
15
|
+
} from "./chunk-Z7YPFWAZ.js";
|
|
16
|
+
import {
|
|
17
|
+
daemonPrincipalsMatch,
|
|
18
|
+
daemonRequest,
|
|
19
|
+
resolveDaemonPrincipal
|
|
20
|
+
} from "./chunk-CUE2LKLR.js";
|
|
21
|
+
import {
|
|
22
|
+
primConfigDirectory
|
|
23
|
+
} from "./chunk-3EEFL6NJ.js";
|
|
24
|
+
|
|
25
|
+
// src/hooks/codex-context.ts
|
|
26
|
+
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
27
|
+
import {
|
|
28
|
+
chmodSync,
|
|
29
|
+
existsSync,
|
|
30
|
+
mkdirSync,
|
|
31
|
+
readFileSync,
|
|
32
|
+
readdirSync,
|
|
33
|
+
renameSync,
|
|
34
|
+
rmSync,
|
|
35
|
+
statSync,
|
|
36
|
+
unlinkSync,
|
|
37
|
+
writeFileSync
|
|
38
|
+
} from "fs";
|
|
39
|
+
import { join } from "path";
|
|
40
|
+
|
|
41
|
+
// src/daemon/decision-digest-cache.ts
|
|
42
|
+
import { randomBytes } from "crypto";
|
|
43
|
+
var DECISION_DIGEST_CACHE_LIMIT = 100;
|
|
44
|
+
var DECISION_DIGEST_CACHE_WINDOW = "24h";
|
|
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
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/hooks/codex-context.ts
|
|
55
|
+
var CODEX_CONTEXT_TIMEOUT_MS = 250;
|
|
56
|
+
var CODEX_DIGEST_LIMIT = DECISION_DIGEST_CACHE_LIMIT;
|
|
57
|
+
var CODEX_DIGEST_VISIBLE_CAP = 3;
|
|
58
|
+
var CODEX_DIGEST_OVERLAP_MS = 6e4;
|
|
59
|
+
var CODEX_DIGEST_MAX_SEEN_IDS = 128;
|
|
60
|
+
var CODEX_DRAFT_DIGEST_MAX_SEEN_IDS = 4096;
|
|
61
|
+
var CODEX_DIGEST_STATE_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
62
|
+
var CODEX_DIGEST_STATE_MAX_FILES = 256;
|
|
63
|
+
var STATE_VERSION = 1;
|
|
64
|
+
var STATE_DIRECTORY = ["codex", "decision-digests"];
|
|
65
|
+
var DRAFT_STATE_VERSION = 2;
|
|
66
|
+
var DRAFT_STATE_DIRECTORY = ["codex", "decision-draft-deliveries"];
|
|
67
|
+
var NO_CURSOR = -1;
|
|
68
|
+
function hasVisibleCodexMessage(output) {
|
|
69
|
+
if (typeof output !== "object" || output === null || Array.isArray(output)) return false;
|
|
70
|
+
const candidate = output;
|
|
71
|
+
return Boolean(
|
|
72
|
+
candidate.systemMessage?.trim() || candidate.hookSpecificOutput?.permissionDecisionReason?.trim() || candidate.hookSpecificOutput?.additionalContext?.trim()
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
function appendContext(existing, context) {
|
|
76
|
+
return existing ? `${existing}
|
|
77
|
+
|
|
78
|
+
${context}` : context;
|
|
79
|
+
}
|
|
80
|
+
function appendCodexContext(output, context) {
|
|
81
|
+
if (!context) return output;
|
|
82
|
+
const candidate = output;
|
|
83
|
+
const next = { ...output };
|
|
84
|
+
if (candidate.systemMessage) {
|
|
85
|
+
next.systemMessage = appendContext(candidate.systemMessage, context);
|
|
86
|
+
} else if (candidate.hookSpecificOutput?.permissionDecisionReason) {
|
|
87
|
+
next.systemMessage = context;
|
|
88
|
+
} else if (candidate.hookSpecificOutput?.additionalContext) {
|
|
89
|
+
next.hookSpecificOutput = {
|
|
90
|
+
...candidate.hookSpecificOutput,
|
|
91
|
+
additionalContext: appendContext(candidate.hookSpecificOutput.additionalContext, context)
|
|
92
|
+
};
|
|
93
|
+
} else {
|
|
94
|
+
next.systemMessage = context;
|
|
95
|
+
}
|
|
96
|
+
return next;
|
|
97
|
+
}
|
|
98
|
+
function stateRoot() {
|
|
99
|
+
return join(primConfigDirectory(), ...STATE_DIRECTORY);
|
|
100
|
+
}
|
|
101
|
+
function draftStateRoot() {
|
|
102
|
+
return join(primConfigDirectory(), ...DRAFT_STATE_DIRECTORY);
|
|
103
|
+
}
|
|
104
|
+
function workspaceFor(cwd) {
|
|
105
|
+
try {
|
|
106
|
+
return gitToplevel(cwd) ?? cwd;
|
|
107
|
+
} catch {
|
|
108
|
+
return cwd;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function statePath(args) {
|
|
112
|
+
const identity = `${args.siteUrl}\0${workspaceFor(args.cwd)}\0${args.sessionId}`;
|
|
113
|
+
const key = createHash("sha256").update(identity).digest("hex");
|
|
114
|
+
return join(stateRoot(), `${key}.json`);
|
|
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
|
+
}
|
|
121
|
+
function parseState(value) {
|
|
122
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
123
|
+
const record = value;
|
|
124
|
+
if (record.version !== STATE_VERSION || typeof record.sessionId !== "string" || typeof record.siteUrl !== "string" || typeof record.workspace !== "string" || typeof record.startedAt !== "number" || typeof record.watermarkMs !== "number" || !Array.isArray(record.seenIds) || !record.seenIds.every((id) => typeof id === "string") || typeof record.updatedAt !== "number") {
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
version: STATE_VERSION,
|
|
129
|
+
sessionId: record.sessionId,
|
|
130
|
+
siteUrl: record.siteUrl,
|
|
131
|
+
workspace: record.workspace,
|
|
132
|
+
startedAt: record.startedAt,
|
|
133
|
+
watermarkMs: record.watermarkMs,
|
|
134
|
+
seenIds: record.seenIds.slice(-CODEX_DIGEST_MAX_SEEN_IDS),
|
|
135
|
+
lastReport: typeof record.lastReport === "string" ? record.lastReport : void 0,
|
|
136
|
+
updatedAt: record.updatedAt
|
|
137
|
+
};
|
|
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
|
+
}
|
|
157
|
+
function readState(path) {
|
|
158
|
+
if (!existsSync(path)) return void 0;
|
|
159
|
+
try {
|
|
160
|
+
return parseState(JSON.parse(readFileSync(path, "utf8")));
|
|
161
|
+
} catch {
|
|
162
|
+
return void 0;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
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) {
|
|
174
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
175
|
+
chmodSync(directory, 448);
|
|
176
|
+
const temporary = join(
|
|
177
|
+
directory,
|
|
178
|
+
`.${path.slice(directory.length + 1)}.tmp-${process.pid}-${randomBytes2(8).toString("hex")}`
|
|
179
|
+
);
|
|
180
|
+
try {
|
|
181
|
+
writeFileSync(temporary, `${JSON.stringify(state)}
|
|
182
|
+
`, {
|
|
183
|
+
encoding: "utf8",
|
|
184
|
+
mode: 384,
|
|
185
|
+
flag: "wx"
|
|
186
|
+
});
|
|
187
|
+
chmodSync(temporary, 384);
|
|
188
|
+
renameSync(temporary, path);
|
|
189
|
+
} finally {
|
|
190
|
+
try {
|
|
191
|
+
unlinkSync(temporary);
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function cleanupStateFiles(directory, now) {
|
|
197
|
+
if (!existsSync(directory)) return;
|
|
198
|
+
let names;
|
|
199
|
+
try {
|
|
200
|
+
names = readdirSync(directory);
|
|
201
|
+
} catch {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const files = [];
|
|
205
|
+
const residue = [];
|
|
206
|
+
for (const name of names) {
|
|
207
|
+
const path = join(directory, name);
|
|
208
|
+
try {
|
|
209
|
+
const mtimeMs = statSync(path).mtimeMs;
|
|
210
|
+
if (name.endsWith(".json")) {
|
|
211
|
+
files.push({ path, mtimeMs });
|
|
212
|
+
} else if ((name.includes(".json.tmp-") || name.endsWith(".json.lock")) && now - mtimeMs > CODEX_DIGEST_STATE_RETENTION_MS) {
|
|
213
|
+
residue.push(path);
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const stale = files.filter((file) => now - file.mtimeMs > CODEX_DIGEST_STATE_RETENTION_MS);
|
|
219
|
+
const excess = files.filter((file) => !stale.some((candidate) => candidate.path === file.path)).sort((a, b) => a.mtimeMs - b.mtimeMs).slice(0, Math.max(0, files.length - CODEX_DIGEST_STATE_MAX_FILES));
|
|
220
|
+
for (const file of [...stale, ...excess]) {
|
|
221
|
+
try {
|
|
222
|
+
unlinkSync(file.path);
|
|
223
|
+
} catch {
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
for (const path of residue) {
|
|
227
|
+
try {
|
|
228
|
+
if (now - statSync(path).mtimeMs > CODEX_DIGEST_STATE_RETENTION_MS) {
|
|
229
|
+
rmSync(path, { recursive: true, force: true });
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function isChangeDecision(row) {
|
|
236
|
+
return row.intentKind === void 0 || row.intentKind === "change";
|
|
237
|
+
}
|
|
238
|
+
function safeInline(value, fallback) {
|
|
239
|
+
const clean = terminalSafeText(value ?? "").replace(/\s+/gu, " ").trim();
|
|
240
|
+
if (!clean) return fallback;
|
|
241
|
+
return clean.length <= 240 ? clean : `${clean.slice(0, 239)}\u2026`;
|
|
242
|
+
}
|
|
243
|
+
function renderDecisionDigest(rows, options = {}) {
|
|
244
|
+
if (rows.length === 0) return void 0;
|
|
245
|
+
const visible = rows.slice(0, CODEX_DIGEST_VISIBLE_CAP).map((row) => {
|
|
246
|
+
const author = safeInline(row.authorName, "(unknown)").replace(/[;;;‒-―−]/gu, "-");
|
|
247
|
+
const intent = safeInline(row.intent, "(untitled Decision)").replace(/[`“-‟″‶〝〞]/gu, "'");
|
|
248
|
+
return `${author} \u2014 \u201C${intent}\u201D`;
|
|
249
|
+
});
|
|
250
|
+
const overflow = rows.length > CODEX_DIGEST_VISIBLE_CAP ? ` +${String(rows.length - CODEX_DIGEST_VISIBLE_CAP)}${options.pageTruncated === true ? "+" : ""}` : "";
|
|
251
|
+
return `[prim] Decisions captured since last message: ${visible.join("; ")}${overflow}`;
|
|
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
|
+
}
|
|
282
|
+
function reauthSnapshot(sessionId) {
|
|
283
|
+
return {
|
|
284
|
+
pid: 0,
|
|
285
|
+
uptimeMs: 0,
|
|
286
|
+
sessionId,
|
|
287
|
+
healthy: false,
|
|
288
|
+
needsReauth: true
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function mergeState(latest, args) {
|
|
292
|
+
const seen = new Set(latest?.seenIds ?? []);
|
|
293
|
+
for (const id of args.seenIds) seen.add(id);
|
|
294
|
+
return {
|
|
295
|
+
version: STATE_VERSION,
|
|
296
|
+
sessionId: args.sessionId,
|
|
297
|
+
siteUrl: args.siteUrl,
|
|
298
|
+
workspace: args.workspace,
|
|
299
|
+
startedAt: latest?.startedAt ?? args.startedAt,
|
|
300
|
+
// Advance only on an observed page (server classifiedAt time), never the
|
|
301
|
+
// client clock; an unavailable feed keeps the previous cursor — or the
|
|
302
|
+
// NO_CURSOR sentinel, so the startup backlog survives until the first
|
|
303
|
+
// successful fetch.
|
|
304
|
+
watermarkMs: args.feedAvailable ? Math.max(latest?.watermarkMs ?? NO_CURSOR, args.watermarkMs) : latest?.watermarkMs ?? NO_CURSOR,
|
|
305
|
+
seenIds: [...seen].slice(-CODEX_DIGEST_MAX_SEEN_IDS),
|
|
306
|
+
lastReport: args.report,
|
|
307
|
+
updatedAt: args.now
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
async function commitState(path, args) {
|
|
311
|
+
try {
|
|
312
|
+
await withFileLock(
|
|
313
|
+
`${path}.lock`,
|
|
314
|
+
() => {
|
|
315
|
+
const latest = readState(path);
|
|
316
|
+
writeStateFile(path, stateRoot(), mergeState(latest, args));
|
|
317
|
+
cleanupStateFiles(stateRoot(), args.now);
|
|
318
|
+
},
|
|
319
|
+
{ timeoutMs: 50, pollMs: 10 }
|
|
320
|
+
);
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
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
|
+
}
|
|
357
|
+
async function prepareCodexContext(options) {
|
|
358
|
+
const siteUrl = getSiteUrl();
|
|
359
|
+
const workspace = workspaceFor(options.cwd);
|
|
360
|
+
const path = statePath({ cwd: options.cwd, sessionId: options.sessionId, siteUrl });
|
|
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;
|
|
372
|
+
const startup = options.startup === true;
|
|
373
|
+
const includeDigest = options.includeDigest !== false;
|
|
374
|
+
const startedAt = previous?.startedAt ?? Date.now();
|
|
375
|
+
const terminalAuth = isSessionEnded();
|
|
376
|
+
let snapshot = null;
|
|
377
|
+
let recent = null;
|
|
378
|
+
let recentDrafts = null;
|
|
379
|
+
if (terminalAuth) {
|
|
380
|
+
snapshot = reauthSnapshot(options.sessionId);
|
|
381
|
+
} else {
|
|
382
|
+
[snapshot, recent, recentDrafts] = await Promise.all([
|
|
383
|
+
daemonRequest(
|
|
384
|
+
"status_snapshot",
|
|
385
|
+
{ callerEnv: siteUrl },
|
|
386
|
+
{ timeoutMs: CODEX_CONTEXT_TIMEOUT_MS }
|
|
387
|
+
),
|
|
388
|
+
includeDigest ? daemonRequest(
|
|
389
|
+
"decision_digest_snapshot",
|
|
390
|
+
{ callerEnv: siteUrl },
|
|
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 }
|
|
397
|
+
) : Promise.resolve(null)
|
|
398
|
+
]);
|
|
399
|
+
if (principal !== void 0 && !daemonPrincipalsMatch(principal, resolveDaemonPrincipal())) {
|
|
400
|
+
snapshot = null;
|
|
401
|
+
recent = null;
|
|
402
|
+
recentDrafts = null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
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
|
+
});
|
|
415
|
+
const reportChanged = startup || previous?.lastReport !== report;
|
|
416
|
+
let feedAvailable = false;
|
|
417
|
+
let freshRows = [];
|
|
418
|
+
let pageTruncated = false;
|
|
419
|
+
let pageWatermark = NO_CURSOR;
|
|
420
|
+
if (!terminalAuth && includeDigest && recent !== null) {
|
|
421
|
+
feedAvailable = recent.unavailable === void 0 && Array.isArray(recent.decisions);
|
|
422
|
+
if (feedAvailable) {
|
|
423
|
+
const seen = new Set(previous?.seenIds ?? []);
|
|
424
|
+
const lowerBound = previous !== void 0 && previous.watermarkMs >= 0 ? Math.max(0, previous.watermarkMs - CODEX_DIGEST_OVERLAP_MS) : 0;
|
|
425
|
+
freshRows = recent.decisions.filter(
|
|
426
|
+
(row) => isChangeDecision(row) && !seen.has(row.id) && typeof row.classifiedAt === "number" && row.classifiedAt >= lowerBound
|
|
427
|
+
);
|
|
428
|
+
pageTruncated = recent.decisions.length >= CODEX_DIGEST_LIMIT;
|
|
429
|
+
pageWatermark = recent.decisions.reduce(
|
|
430
|
+
(max, row) => typeof row.classifiedAt === "number" && row.classifiedAt > max ? row.classifiedAt : max,
|
|
431
|
+
NO_CURSOR
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const digest = renderDecisionDigest(freshRows, { pageTruncated });
|
|
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;
|
|
451
|
+
let acknowledged = false;
|
|
452
|
+
return {
|
|
453
|
+
context,
|
|
454
|
+
decisionDigest: combinedDigest || void 0,
|
|
455
|
+
feedAvailable,
|
|
456
|
+
acknowledge: async (handedOff) => {
|
|
457
|
+
if (!handedOff || acknowledged) return;
|
|
458
|
+
acknowledged = true;
|
|
459
|
+
await commitState(path, {
|
|
460
|
+
sessionId: options.sessionId,
|
|
461
|
+
siteUrl,
|
|
462
|
+
workspace,
|
|
463
|
+
startedAt,
|
|
464
|
+
watermarkMs: pageWatermark,
|
|
465
|
+
seenIds: freshRows.map((row) => row.id),
|
|
466
|
+
report,
|
|
467
|
+
feedAvailable,
|
|
468
|
+
now: Date.now()
|
|
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
|
+
);
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export {
|
|
502
|
+
hasVisibleCodexMessage,
|
|
503
|
+
appendCodexContext,
|
|
504
|
+
prepareCodexContext
|
|
505
|
+
};
|