@primitive.ai/prim 0.1.0-alpha.7 → 0.1.0-alpha.70
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 +296 -37
- package/SKILL.md +269 -0
- package/dist/chunk-2KW7DVFJ.js +1400 -0
- package/dist/chunk-3EEFL6NJ.js +529 -0
- package/dist/chunk-4A2XB2PC.js +310 -0
- package/dist/chunk-4GLVJLEQ.js +55 -0
- package/dist/chunk-BQUJ4AFJ.js +771 -0
- package/dist/chunk-CUE2LKLR.js +155 -0
- package/dist/chunk-FI3LIRCB.js +262 -0
- package/dist/chunk-H64F6QHV.js +247 -0
- package/dist/chunk-KKP2NYZ3.js +505 -0
- package/dist/chunk-LEMX3IOG.js +79 -0
- package/dist/chunk-LMRYZL3D.js +209 -0
- package/dist/chunk-RO3F6JYV.js +126 -0
- package/dist/chunk-TJTVHDJ5.js +321 -0
- package/dist/chunk-UVIWZWM6.js +211 -0
- package/dist/chunk-WB7AUZ4C.js +52 -0
- package/dist/chunk-XGADQPCI.js +26 -0
- package/dist/chunk-YWO7O6QG.js +327 -0
- package/dist/chunk-Z7YPFWAZ.js +917 -0
- package/dist/chunk-ZOSNEO5N.js +77 -0
- package/dist/daemon/server.js +13353 -0
- package/dist/hook-runtime/daemon/server.js +13353 -0
- package/dist/hook-runtime/hooks/post-commit.js +1128 -0
- package/dist/hook-runtime/hooks/post-rewrite.js +1148 -0
- package/dist/hook-runtime/hooks/post-tool-use.js +7586 -0
- package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
- package/dist/hook-runtime/hooks/pre-tool-use.js +7099 -0
- package/dist/hook-runtime/hooks/prim-hook.js +7691 -0
- package/dist/hook-runtime/hooks/session-end.js +495 -0
- package/dist/hook-runtime/hooks/session-start.js +11284 -0
- package/dist/hook-runtime/index.js +42775 -0
- package/dist/hook-runtime/statusline-main.js +765 -0
- package/dist/hooks/post-commit.js +179 -0
- package/dist/hooks/post-rewrite.js +197 -0
- package/dist/hooks/post-tool-use.js +250 -0
- package/dist/hooks/pre-commit.js +21 -74
- package/dist/hooks/pre-tool-use.js +348 -0
- package/dist/hooks/prim-hook.js +195 -0
- package/dist/hooks/session-end.js +70 -0
- package/dist/hooks/session-start.js +271 -0
- package/dist/index.js +18920 -518
- package/dist/statusline-main.js +765 -0
- package/package.json +35 -13
- package/dist/chunk-3APLWTLB.js +0 -165
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getClient,
|
|
3
|
+
isTerminalSafeText,
|
|
4
|
+
terminalSafeLine,
|
|
5
|
+
terminalSafeText
|
|
6
|
+
} from "./chunk-Z7YPFWAZ.js";
|
|
7
|
+
|
|
8
|
+
// src/decisions/feedback.ts
|
|
9
|
+
var FEEDBACK_PROTOCOL_VERSION = 2;
|
|
10
|
+
var FEEDBACK_MIN_PROTOCOL_VERSION = 1;
|
|
11
|
+
var FEEDBACK_DEADLINE_MS = 3e3;
|
|
12
|
+
var MAX_FEEDBACK_EVENTS = 40;
|
|
13
|
+
var MAX_FEEDBACK_MESSAGE_CODE_POINTS = 8e3;
|
|
14
|
+
var MAX_FEEDBACK_INTENT_CODE_POINTS = 180;
|
|
15
|
+
var LEASE_PATH = "/api/cli/decisions/feedback/lease";
|
|
16
|
+
var ACK_PATH = "/api/cli/decisions/feedback/ack";
|
|
17
|
+
var STATUS_PATH = "/api/cli/decisions/feedback/status";
|
|
18
|
+
var SHORT_ID = /^[0-9a-f]{8}$/u;
|
|
19
|
+
var MAX_EVENT_ID_CHARS = 128;
|
|
20
|
+
var MAX_RAW_INTENT_CODE_UNITS = 512;
|
|
21
|
+
var MAX_FEEDBACK_WEB_URL_CHARS = 2048;
|
|
22
|
+
var SAFE_PUBLISH_DECISION_ID = /^[A-Za-z0-9_-]+$/u;
|
|
23
|
+
function isRecord(value) {
|
|
24
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
function isSupportedProtocolVersion(value) {
|
|
27
|
+
return Number.isSafeInteger(value) && Number(value) >= FEEDBACK_MIN_PROTOCOL_VERSION && Number(value) <= FEEDBACK_PROTOCOL_VERSION;
|
|
28
|
+
}
|
|
29
|
+
function normalizeFeedbackIntent(value) {
|
|
30
|
+
const withoutControls = terminalSafeLine(value);
|
|
31
|
+
const points = Array.from(withoutControls);
|
|
32
|
+
if (points.length <= MAX_FEEDBACK_INTENT_CODE_POINTS) {
|
|
33
|
+
return withoutControls;
|
|
34
|
+
}
|
|
35
|
+
return `${points.slice(0, MAX_FEEDBACK_INTENT_CODE_POINTS - 1).join("")}\u2026`;
|
|
36
|
+
}
|
|
37
|
+
function parseFeedbackWebUrl(value) {
|
|
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)) {
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const url = new URL(value);
|
|
43
|
+
if (url.protocol !== "https:" || !url.hostname || url.username.length > 0 || url.password.length > 0) {
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
return void 0;
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
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) {
|
|
58
|
+
if (!isRecord(value)) return void 0;
|
|
59
|
+
if (typeof value.eventId !== "string" || value.eventId.length === 0 || value.eventId.length > MAX_EVENT_ID_CHARS || terminalSafeText(value.eventId) !== value.eventId) {
|
|
60
|
+
return void 0;
|
|
61
|
+
}
|
|
62
|
+
if (!Number.isSafeInteger(value.leaseVersion) || Number(value.leaseVersion) < 1) {
|
|
63
|
+
return void 0;
|
|
64
|
+
}
|
|
65
|
+
if (typeof value.shortId !== "string" || !SHORT_ID.test(value.shortId)) {
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
if (typeof value.intent !== "string" || value.intent.length > MAX_RAW_INTENT_CODE_UNITS) {
|
|
69
|
+
return void 0;
|
|
70
|
+
}
|
|
71
|
+
const intent = normalizeFeedbackIntent(value.intent);
|
|
72
|
+
if (!intent) return void 0;
|
|
73
|
+
const webUrl = value.webUrl === void 0 ? void 0 : parseFeedbackWebUrl(value.webUrl);
|
|
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
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
eventId: value.eventId,
|
|
88
|
+
leaseVersion: Number(value.leaseVersion),
|
|
89
|
+
shortId: value.shortId,
|
|
90
|
+
intent,
|
|
91
|
+
...webUrl === void 0 ? {} : { webUrl },
|
|
92
|
+
...decisionId === void 0 ? {} : { decisionId },
|
|
93
|
+
kind
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function parseFeedbackLease(value) {
|
|
97
|
+
if (!isRecord(value) || !isSupportedProtocolVersion(value.protocolVersion)) return void 0;
|
|
98
|
+
const protocolVersion = value.protocolVersion;
|
|
99
|
+
if (value.status === "empty") {
|
|
100
|
+
return value.hasMore === false ? { protocolVersion, events: [], hasMore: false } : void 0;
|
|
101
|
+
}
|
|
102
|
+
if (value.status === "unavailable") {
|
|
103
|
+
return value.reason === "organization_unbound" ? { protocolVersion, events: [], hasMore: false } : void 0;
|
|
104
|
+
}
|
|
105
|
+
if (value.status !== "leased" || typeof value.hasMore !== "boolean") return void 0;
|
|
106
|
+
if (!Array.isArray(value.events) || value.events.length === 0 || value.events.length > MAX_FEEDBACK_EVENTS) {
|
|
107
|
+
return void 0;
|
|
108
|
+
}
|
|
109
|
+
const events = value.events.map((event) => parseEvent(event, protocolVersion));
|
|
110
|
+
if (events.some((event) => event === void 0)) return void 0;
|
|
111
|
+
const parsedEvents = events;
|
|
112
|
+
if (new Set(parsedEvents.map((event) => event.eventId)).size !== parsedEvents.length) {
|
|
113
|
+
return void 0;
|
|
114
|
+
}
|
|
115
|
+
return { protocolVersion, events: parsedEvents, hasMore: value.hasMore };
|
|
116
|
+
}
|
|
117
|
+
function renderFeedback(lease) {
|
|
118
|
+
const lines = [];
|
|
119
|
+
const deliveries = [];
|
|
120
|
+
let pointCount = 0;
|
|
121
|
+
for (const event of lease.events) {
|
|
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}`;
|
|
127
|
+
const extra = Array.from(line).length + (lines.length === 0 ? 0 : 1);
|
|
128
|
+
if (pointCount + extra > MAX_FEEDBACK_MESSAGE_CODE_POINTS) break;
|
|
129
|
+
lines.push(line);
|
|
130
|
+
deliveries.push({ eventId: event.eventId, leaseVersion: event.leaseVersion });
|
|
131
|
+
pointCount += extra;
|
|
132
|
+
}
|
|
133
|
+
if (lines.length === 0) return void 0;
|
|
134
|
+
return { protocolVersion: lease.protocolVersion, systemMessage: lines.join("\n"), deliveries };
|
|
135
|
+
}
|
|
136
|
+
async function leaseDecisionFeedback(input, dependencies = {}) {
|
|
137
|
+
try {
|
|
138
|
+
const response = await (dependencies.client ?? getClient()).post(
|
|
139
|
+
LEASE_PATH,
|
|
140
|
+
{
|
|
141
|
+
protocolVersion: FEEDBACK_PROTOCOL_VERSION,
|
|
142
|
+
workspaceId: input.workspaceId,
|
|
143
|
+
currentSessionId: input.currentSessionId
|
|
144
|
+
},
|
|
145
|
+
{ signal: input.signal, quietRefresh: true }
|
|
146
|
+
);
|
|
147
|
+
const parsed = parseFeedbackLease(response);
|
|
148
|
+
if (!parsed) {
|
|
149
|
+
dependencies.onError?.(
|
|
150
|
+
new Error("server returned an unsupported decision-feedback contract")
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return parsed;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
dependencies.onError?.(error);
|
|
156
|
+
return void 0;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function acknowledgeDecisionFeedback(input, dependencies = {}) {
|
|
160
|
+
if (!isSupportedProtocolVersion(input.protocolVersion) || input.deliveries.length === 0 || input.deliveries.length > MAX_FEEDBACK_EVENTS) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const response = await (dependencies.client ?? getClient()).post(
|
|
165
|
+
ACK_PATH,
|
|
166
|
+
{
|
|
167
|
+
protocolVersion: input.protocolVersion,
|
|
168
|
+
workspaceId: input.workspaceId,
|
|
169
|
+
deliveries: input.deliveries
|
|
170
|
+
},
|
|
171
|
+
{ signal: input.signal, quietRefresh: true }
|
|
172
|
+
);
|
|
173
|
+
const acknowledgedEventIds = isRecord(response) ? response.acknowledgedEventIds : void 0;
|
|
174
|
+
const expectedEventIds = new Set(input.deliveries.map((delivery) => delivery.eventId));
|
|
175
|
+
const valid = isRecord(response) && response.protocolVersion === input.protocolVersion && response.status === "acked" && Array.isArray(acknowledgedEventIds) && acknowledgedEventIds.length <= input.deliveries.length && acknowledgedEventIds.every(
|
|
176
|
+
(id) => typeof id === "string" && id.length > 0 && id.length <= MAX_EVENT_ID_CHARS && expectedEventIds.has(id)
|
|
177
|
+
) && new Set(acknowledgedEventIds).size === acknowledgedEventIds.length;
|
|
178
|
+
if (!valid) {
|
|
179
|
+
dependencies.onError?.(
|
|
180
|
+
new Error("server returned an unsupported decision-feedback acknowledgement")
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return valid;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
dependencies.onError?.(error);
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function parseFeedbackCapability(value) {
|
|
190
|
+
if (!isRecord(value) || !isSupportedProtocolVersion(value.protocolVersion)) return void 0;
|
|
191
|
+
if (value.status === "available") return { status: "available" };
|
|
192
|
+
if (value.status === "unavailable" && value.reason === "organization_unbound") {
|
|
193
|
+
return { status: "unavailable", reason: "organization_unbound" };
|
|
194
|
+
}
|
|
195
|
+
return void 0;
|
|
196
|
+
}
|
|
197
|
+
async function fetchFeedbackCapability(signal, client = getClient()) {
|
|
198
|
+
const result = parseFeedbackCapability(await client.get(STATUS_PATH, { signal }));
|
|
199
|
+
if (!result) throw new Error("server returned an unsupported decision-feedback contract");
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export {
|
|
204
|
+
FEEDBACK_DEADLINE_MS,
|
|
205
|
+
renderFeedback,
|
|
206
|
+
leaseDecisionFeedback,
|
|
207
|
+
acknowledgeDecisionFeedback,
|
|
208
|
+
fetchFeedbackCapability
|
|
209
|
+
};
|
|
@@ -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,321 @@
|
|
|
1
|
+
// src/lib/git.ts
|
|
2
|
+
import { execFileSync } from "child_process";
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
import { lstatSync, realpathSync } from "fs";
|
|
5
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from "path";
|
|
6
|
+
var GIT_TIMEOUT_MS = 1e3;
|
|
7
|
+
var GITHUB_OWNER_PATTERN = /^(?![A-Za-z0-9-]*--)[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/u;
|
|
8
|
+
var GITHUB_REPOSITORY_PATTERN = /^[A-Za-z0-9._-]{1,100}$/u;
|
|
9
|
+
function gitToplevel(cwd) {
|
|
10
|
+
try {
|
|
11
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
12
|
+
cwd,
|
|
13
|
+
encoding: "utf-8",
|
|
14
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
15
|
+
timeout: GIT_TIMEOUT_MS
|
|
16
|
+
}).trim();
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function gitValue(cwd, args) {
|
|
22
|
+
try {
|
|
23
|
+
const value = execFileSync("git", args, {
|
|
24
|
+
cwd,
|
|
25
|
+
encoding: "utf-8",
|
|
26
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
27
|
+
timeout: GIT_TIMEOUT_MS
|
|
28
|
+
}).trim();
|
|
29
|
+
return value || void 0;
|
|
30
|
+
} catch {
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function githubRepositoryFullName(cwd) {
|
|
35
|
+
const remote = gitValue(cwd, ["config", "--get", "remote.origin.url"]);
|
|
36
|
+
if (!remote || [...remote].some((char) => char < " " || char === "\x7F")) return null;
|
|
37
|
+
const match = /^(?:git@github\.com:([^?#]+)|(?:https|ssh):\/\/(?:[^/@]+@)?github\.com(?::\d+)?\/([^?#]+))$/i.exec(
|
|
38
|
+
remote
|
|
39
|
+
);
|
|
40
|
+
const path = match?.[1] ?? match?.[2];
|
|
41
|
+
if (!path) return null;
|
|
42
|
+
const parts = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").split("/");
|
|
43
|
+
if (parts.length !== 2 || !GITHUB_OWNER_PATTERN.test(parts[0] ?? "") || !GITHUB_REPOSITORY_PATTERN.test(parts[1] ?? "")) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return `${parts[0]}/${parts[1]}`;
|
|
47
|
+
}
|
|
48
|
+
function canonicalGitRoot(cwd) {
|
|
49
|
+
try {
|
|
50
|
+
const top = gitToplevel(cwd);
|
|
51
|
+
return top ? realpathSync.native(top) : null;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function normalizeOriginRemote(remote) {
|
|
57
|
+
const value = remote.trim();
|
|
58
|
+
if (!value) return void 0;
|
|
59
|
+
const scp = !value.includes("://") ? /^(?:[^@/\s]+@)?(?<host>[^:/\s]+):(?<path>.+)$/.exec(value)?.groups : void 0;
|
|
60
|
+
if (scp?.host && scp.path) {
|
|
61
|
+
const path = scp.path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
|
|
62
|
+
return path ? `${scp.host.toLowerCase()}/${path}` : void 0;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const url = new URL(value);
|
|
66
|
+
url.username = "";
|
|
67
|
+
url.password = "";
|
|
68
|
+
url.search = "";
|
|
69
|
+
url.hash = "";
|
|
70
|
+
const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
|
|
71
|
+
if (url.host && path) return `${url.host.toLowerCase()}/${path}`;
|
|
72
|
+
return void 0;
|
|
73
|
+
} catch {
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function opaqueRepoKey(source, value) {
|
|
78
|
+
return `repo_v1_${createHash("sha256").update(`${source}\0${value}`).digest("hex")}`;
|
|
79
|
+
}
|
|
80
|
+
function resolveRepositoryContext(cwd = process.cwd()) {
|
|
81
|
+
const top = gitToplevel(cwd);
|
|
82
|
+
if (!top) return null;
|
|
83
|
+
let repoRoot;
|
|
84
|
+
try {
|
|
85
|
+
repoRoot = realpathSync.native(top);
|
|
86
|
+
} catch {
|
|
87
|
+
repoRoot = resolve(top);
|
|
88
|
+
}
|
|
89
|
+
const origin = gitValue(repoRoot, ["config", "--get", "remote.origin.url"]);
|
|
90
|
+
const repoFullName = githubRepositoryFullName(repoRoot) ?? void 0;
|
|
91
|
+
const normalizedOrigin = origin ? normalizeOriginRemote(origin) : void 0;
|
|
92
|
+
if (normalizedOrigin) {
|
|
93
|
+
return {
|
|
94
|
+
repoRoot,
|
|
95
|
+
repoKey: opaqueRepoKey("origin", normalizedOrigin),
|
|
96
|
+
identitySource: "origin",
|
|
97
|
+
repoFullName
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const roots = gitValue(repoRoot, ["rev-list", "--max-parents=0", "HEAD"])?.split(/\s+/).filter(Boolean).sort();
|
|
101
|
+
if (roots && roots.length > 0) {
|
|
102
|
+
return {
|
|
103
|
+
repoRoot,
|
|
104
|
+
repoKey: opaqueRepoKey("root_commit", roots.join("\n")),
|
|
105
|
+
identitySource: "root_commit",
|
|
106
|
+
repoFullName
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return { repoRoot, repoFullName };
|
|
110
|
+
}
|
|
111
|
+
function nearestExistingAncestor(path) {
|
|
112
|
+
const suffix = [];
|
|
113
|
+
let cursor = path;
|
|
114
|
+
while (true) {
|
|
115
|
+
try {
|
|
116
|
+
lstatSync(cursor);
|
|
117
|
+
break;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
120
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") return null;
|
|
121
|
+
const parent = dirname(cursor);
|
|
122
|
+
if (parent === cursor) return null;
|
|
123
|
+
suffix.unshift(cursor.slice(parent.length + (parent.endsWith(sep) ? 0 : 1)));
|
|
124
|
+
cursor = parent;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
return { real: realpathSync.native(cursor), suffix };
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function canonicalRepositoryPathScoped(filePath, context, baseDir) {
|
|
134
|
+
if (!filePath || filePath !== filePath.trim() || filePath.includes("\0")) {
|
|
135
|
+
return { ok: false, reason: "invalid_path" };
|
|
136
|
+
}
|
|
137
|
+
const absolute = resolve(baseDir, filePath);
|
|
138
|
+
const resolved = nearestExistingAncestor(absolute);
|
|
139
|
+
if (!resolved) return { ok: false, reason: "invalid_path" };
|
|
140
|
+
const target = resolve(resolved.real, ...resolved.suffix);
|
|
141
|
+
const rel = relative(context.repoRoot, target);
|
|
142
|
+
if (!rel || rel === ".") return { ok: false, reason: "invalid_path" };
|
|
143
|
+
if (isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`)) {
|
|
144
|
+
return { ok: false, reason: "outside_repository" };
|
|
145
|
+
}
|
|
146
|
+
return { ok: true, file: sep === "/" ? rel : rel.split(sep).join("/") };
|
|
147
|
+
}
|
|
148
|
+
function canonicalRepositoryPathLegacy(filePath, cwd, repositoryRoot = canonicalGitRoot(cwd)) {
|
|
149
|
+
if (!repositoryRoot || !filePath || [...filePath].some((char) => char < " " || char === "\x7F" || "~*?[]{}$`\\".includes(char)))
|
|
150
|
+
return null;
|
|
151
|
+
const absolute = resolve(cwd, filePath);
|
|
152
|
+
let target;
|
|
153
|
+
try {
|
|
154
|
+
const stat = lstatSync(absolute);
|
|
155
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return null;
|
|
156
|
+
target = realpathSync.native(absolute);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (!(error instanceof Error) || error.code !== "ENOENT") {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const parent = realpathSync.native(dirname(absolute));
|
|
163
|
+
if (!lstatSync(parent).isDirectory()) return null;
|
|
164
|
+
target = resolve(parent, basename(absolute));
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const rel = relative(repositoryRoot, target);
|
|
170
|
+
if (!rel || isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`)) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return sep === "/" ? rel : rel.split(sep).join("/");
|
|
174
|
+
}
|
|
175
|
+
function canonicalRepositoryPath(filePath, contextOrCwd, baseDirOrRoot) {
|
|
176
|
+
if (typeof contextOrCwd === "string") {
|
|
177
|
+
return canonicalRepositoryPathLegacy(filePath, contextOrCwd, baseDirOrRoot);
|
|
178
|
+
}
|
|
179
|
+
return canonicalRepositoryPathScoped(
|
|
180
|
+
filePath,
|
|
181
|
+
contextOrCwd,
|
|
182
|
+
baseDirOrRoot ?? contextOrCwd.repoRoot
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/lib/activation.ts
|
|
187
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
188
|
+
import { existsSync, readFileSync } from "fs";
|
|
189
|
+
import { homedir } from "os";
|
|
190
|
+
import { join } from "path";
|
|
191
|
+
var PRIM_ACTIVE_KEY = "prim.active";
|
|
192
|
+
var PRIM_REPO_SYNC_ID_KEY = "prim.repoSyncId";
|
|
193
|
+
var PRIM_REPO_SYNC_REPOSITORY_KEY = "prim.repoSyncRepository";
|
|
194
|
+
var PRIM_REPO_BINDING_STATE_KEY = "prim.repoBindingState";
|
|
195
|
+
var GIT_TIMEOUT_MS2 = 1e3;
|
|
196
|
+
var REPO_SYNC_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
197
|
+
var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json"];
|
|
198
|
+
var PRIM_HOOK_BINS = ["prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
|
|
199
|
+
function repoActiveFlag(cwd) {
|
|
200
|
+
try {
|
|
201
|
+
const value = execFileSync2("git", ["config", "--get", PRIM_ACTIVE_KEY], {
|
|
202
|
+
cwd,
|
|
203
|
+
encoding: "utf-8",
|
|
204
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
205
|
+
timeout: GIT_TIMEOUT_MS2
|
|
206
|
+
}).trim();
|
|
207
|
+
if (value === "true") return "true";
|
|
208
|
+
if (value === "false") return "false";
|
|
209
|
+
return void 0;
|
|
210
|
+
} catch {
|
|
211
|
+
return void 0;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function isRepoActive(cwd) {
|
|
215
|
+
return repoActiveFlag(cwd) === "true";
|
|
216
|
+
}
|
|
217
|
+
function setRepoActive(cwd, active) {
|
|
218
|
+
execFileSync2("git", ["config", "--local", PRIM_ACTIVE_KEY, active ? "true" : "false"], {
|
|
219
|
+
cwd,
|
|
220
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
221
|
+
timeout: GIT_TIMEOUT_MS2
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
function isValidRepoSyncId(value) {
|
|
225
|
+
return typeof value === "string" && REPO_SYNC_ID_RE.test(value);
|
|
226
|
+
}
|
|
227
|
+
function localGitConfigValue(cwd, key) {
|
|
228
|
+
try {
|
|
229
|
+
const value = execFileSync2("git", ["config", "--local", "--get", key], {
|
|
230
|
+
cwd,
|
|
231
|
+
encoding: "utf-8",
|
|
232
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
233
|
+
timeout: GIT_TIMEOUT_MS2
|
|
234
|
+
}).trim();
|
|
235
|
+
return value || void 0;
|
|
236
|
+
} catch {
|
|
237
|
+
return void 0;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function repoSyncId(cwd) {
|
|
241
|
+
const value = localGitConfigValue(cwd, PRIM_REPO_SYNC_ID_KEY);
|
|
242
|
+
if (!isValidRepoSyncId(value)) return void 0;
|
|
243
|
+
const repositoryFullName = localGitConfigValue(cwd, PRIM_REPO_SYNC_REPOSITORY_KEY);
|
|
244
|
+
return repositoryFullName === githubRepositoryFullName(cwd) ? value : void 0;
|
|
245
|
+
}
|
|
246
|
+
function setRepoSyncId(cwd, value, repositoryFullName) {
|
|
247
|
+
if (!isValidRepoSyncId(value)) {
|
|
248
|
+
throw new Error("invalid repository binding returned by server");
|
|
249
|
+
}
|
|
250
|
+
execFileSync2("git", ["config", "--local", PRIM_REPO_SYNC_ID_KEY, value], {
|
|
251
|
+
cwd,
|
|
252
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
253
|
+
timeout: GIT_TIMEOUT_MS2
|
|
254
|
+
});
|
|
255
|
+
execFileSync2("git", ["config", "--local", PRIM_REPO_SYNC_REPOSITORY_KEY, repositoryFullName], {
|
|
256
|
+
cwd,
|
|
257
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
258
|
+
timeout: GIT_TIMEOUT_MS2
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function setRepositoryBindingState(cwd, state) {
|
|
262
|
+
if (state !== "connected" && state !== "unbound" && state !== "invalid") {
|
|
263
|
+
throw new Error("invalid repository binding state");
|
|
264
|
+
}
|
|
265
|
+
execFileSync2("git", ["config", "--local", PRIM_REPO_BINDING_STATE_KEY, state], {
|
|
266
|
+
cwd,
|
|
267
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
268
|
+
timeout: GIT_TIMEOUT_MS2
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
function repositoryBindingState(cwd) {
|
|
272
|
+
const rawId = localGitConfigValue(cwd, PRIM_REPO_SYNC_ID_KEY);
|
|
273
|
+
const marker = localGitConfigValue(cwd, PRIM_REPO_BINDING_STATE_KEY);
|
|
274
|
+
if (rawId !== void 0 && !isValidRepoSyncId(rawId)) return "invalid";
|
|
275
|
+
if (marker === "unbound" || marker === "invalid") return marker;
|
|
276
|
+
if (marker === "connected") return repoSyncId(cwd) ? "connected" : "invalid";
|
|
277
|
+
if (marker !== void 0) return "invalid";
|
|
278
|
+
return rawId === void 0 ? "unbound" : repoSyncId(cwd) ? void 0 : "unbound";
|
|
279
|
+
}
|
|
280
|
+
function hasProjectPrimInstall(cwd) {
|
|
281
|
+
const root = gitToplevel(cwd);
|
|
282
|
+
if (root === null || root === homedir()) return false;
|
|
283
|
+
for (const rel of PROJECT_INSTALL_FILES) {
|
|
284
|
+
const path = join(root, rel);
|
|
285
|
+
if (existsSync(path)) {
|
|
286
|
+
try {
|
|
287
|
+
const content = readFileSync(path, "utf-8");
|
|
288
|
+
if (PRIM_HOOK_BINS.some((bin) => content.includes(bin))) return true;
|
|
289
|
+
} catch {
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
function isRepoActiveForCapture(cwd) {
|
|
296
|
+
const flag = repoActiveFlag(cwd);
|
|
297
|
+
if (flag === "true") return true;
|
|
298
|
+
if (flag === "false") return false;
|
|
299
|
+
return hasProjectPrimInstall(cwd);
|
|
300
|
+
}
|
|
301
|
+
function decisionIngestionStatus(cwd) {
|
|
302
|
+
return isRepoActiveForCapture(cwd) ? "enabled" : "disabled";
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export {
|
|
306
|
+
gitToplevel,
|
|
307
|
+
githubRepositoryFullName,
|
|
308
|
+
canonicalGitRoot,
|
|
309
|
+
resolveRepositoryContext,
|
|
310
|
+
canonicalRepositoryPath,
|
|
311
|
+
repoActiveFlag,
|
|
312
|
+
isRepoActive,
|
|
313
|
+
setRepoActive,
|
|
314
|
+
isValidRepoSyncId,
|
|
315
|
+
repoSyncId,
|
|
316
|
+
setRepoSyncId,
|
|
317
|
+
setRepositoryBindingState,
|
|
318
|
+
repositoryBindingState,
|
|
319
|
+
isRepoActiveForCapture,
|
|
320
|
+
decisionIngestionStatus
|
|
321
|
+
};
|