@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.
- package/README.md +39 -1
- package/SKILL.md +1 -1
- package/dist/{chunk-XPVNH3J6.js → chunk-2KW7DVFJ.js} +121 -130
- package/dist/chunk-3EEFL6NJ.js +529 -0
- package/dist/chunk-4A2XB2PC.js +310 -0
- package/dist/{chunk-G5D3FF3D.js → chunk-BQUJ4AFJ.js} +1 -83
- package/dist/chunk-CUE2LKLR.js +155 -0
- package/dist/chunk-FI3LIRCB.js +262 -0
- package/dist/{chunk-CPSE3Y3T.js → chunk-H64F6QHV.js} +27 -8
- package/dist/{chunk-I5BHJQH2.js → chunk-KKP2NYZ3.js} +206 -30
- package/dist/chunk-LEMX3IOG.js +79 -0
- package/dist/{chunk-U6L2RU67.js → chunk-LMRYZL3D.js} +52 -45
- package/dist/{chunk-XUAP572Z.js → chunk-RO3F6JYV.js} +8 -7
- package/dist/{chunk-TDRXHBEX.js → chunk-TJTVHDJ5.js} +41 -18
- package/dist/{chunk-TCDUH7AN.js → chunk-WB7AUZ4C.js} +17 -0
- package/dist/{chunk-UN5O5SYG.js → chunk-YWO7O6QG.js} +14 -53
- package/dist/chunk-Z7YPFWAZ.js +917 -0
- package/dist/{chunk-BDGQEJA5.js → chunk-ZOSNEO5N.js} +2 -2
- package/dist/daemon/server.js +11634 -391
- 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 +7 -4
- package/dist/hooks/post-rewrite.js +7 -4
- package/dist/hooks/post-tool-use.js +21 -19
- package/dist/hooks/pre-commit.js +4 -3
- package/dist/hooks/pre-tool-use.js +59 -18
- package/dist/hooks/prim-hook.js +39 -56
- package/dist/hooks/session-end.js +3 -2
- package/dist/hooks/session-start.js +15 -12
- package/dist/index.js +15050 -2479
- package/dist/statusline-main.js +477 -89
- package/package.json +11 -5
- package/dist/chunk-K3AYW6VZ.js +0 -300
- package/dist/chunk-RTLFE6EZ.js +0 -518
- package/dist/chunk-UTKQTZHL.js +0 -88
package/dist/statusline-main.js
CHANGED
|
@@ -3,73 +3,396 @@
|
|
|
3
3
|
// src/commands/statusline.ts
|
|
4
4
|
import { readFileSync as readFileSync5 } from "fs";
|
|
5
5
|
import { dirname as dirname5, resolve as resolve3 } from "path";
|
|
6
|
-
import { fileURLToPath
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
7
|
|
|
8
8
|
// src/client.ts
|
|
9
|
-
import { createHash
|
|
9
|
+
import { createHash as createHash2 } from "crypto";
|
|
10
|
+
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync as rmSync3 } from "fs";
|
|
11
|
+
import { dirname as dirname3 } from "path";
|
|
12
|
+
|
|
13
|
+
// src/lib/atomic-file.ts
|
|
14
|
+
import { randomUUID } from "crypto";
|
|
10
15
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
constants,
|
|
17
|
+
closeSync,
|
|
18
|
+
fchmodSync,
|
|
19
|
+
fsyncSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
openSync,
|
|
15
22
|
renameSync,
|
|
16
|
-
rmSync
|
|
17
|
-
writeFileSync
|
|
23
|
+
rmSync,
|
|
24
|
+
writeFileSync
|
|
18
25
|
} from "fs";
|
|
26
|
+
import { dirname } from "path";
|
|
27
|
+
|
|
28
|
+
// src/lib/credentials.ts
|
|
29
|
+
import { createHash } from "crypto";
|
|
30
|
+
import { readFileSync } from "fs";
|
|
31
|
+
import { join as join2 } from "path";
|
|
32
|
+
|
|
33
|
+
// src/lib/paths.ts
|
|
19
34
|
import { homedir } from "os";
|
|
20
|
-
import {
|
|
35
|
+
import { isAbsolute, join, normalize } from "path";
|
|
36
|
+
var UNSAFE_CONFIG_PATH_CHARACTERS = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
37
|
+
function absolutePath(value, variable) {
|
|
38
|
+
if (value !== void 0 && UNSAFE_CONFIG_PATH_CHARACTERS.test(value)) {
|
|
39
|
+
throw new Error(`${variable} contains unsafe characters`);
|
|
40
|
+
}
|
|
41
|
+
if (!value || value.trim() !== value || !isAbsolute(value)) return;
|
|
42
|
+
const normalized = normalize(value);
|
|
43
|
+
return normalized === value ? value : void 0;
|
|
44
|
+
}
|
|
45
|
+
function resolvePrimConfigDirectory(options = {}) {
|
|
46
|
+
const env = options.env ?? process.env;
|
|
47
|
+
const explicit = absolutePath(env.PRIM_CONFIG_DIR, "PRIM_CONFIG_DIR");
|
|
48
|
+
if (explicit) return { path: explicit, source: "explicit" };
|
|
49
|
+
const xdgConfigHome = absolutePath(env.XDG_CONFIG_HOME, "XDG_CONFIG_HOME");
|
|
50
|
+
if (xdgConfigHome) return { path: join(xdgConfigHome, "prim"), source: "xdg" };
|
|
51
|
+
const home = absolutePath(options.homeDir ?? homedir(), "HOME");
|
|
52
|
+
if (!home) {
|
|
53
|
+
throw new Error("cannot resolve Primitive config directory: HOME is not an absolute path");
|
|
54
|
+
}
|
|
55
|
+
return { path: join(home, ".config", "prim"), source: "default" };
|
|
56
|
+
}
|
|
57
|
+
function primConfigDirectory(options = {}) {
|
|
58
|
+
return resolvePrimConfigDirectory(options).path;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/lib/workos-connect.ts
|
|
62
|
+
var DISCOVERY_MAX_BYTES = 16 * 1024;
|
|
63
|
+
var WORKOS_CONNECT_RESPONSE_MAX_BYTES = 64 * 1024;
|
|
64
|
+
var MAX_DEVICE_LIFETIME_SECONDS = 60 * 60;
|
|
65
|
+
var MAX_TOKEN_LIFETIME_SECONDS = 24 * 60 * 60;
|
|
66
|
+
var MAX_URL_LENGTH = 2048;
|
|
67
|
+
var MAX_CLIENT_ID_LENGTH = 256;
|
|
68
|
+
var CONNECT_CLIENT_ID_PATTERN = /^client_[A-Za-z0-9_-]+$/u;
|
|
69
|
+
var WorkosConnectProtocolError = class extends Error {
|
|
70
|
+
code;
|
|
71
|
+
constructor(code, message) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = "WorkosConnectProtocolError";
|
|
74
|
+
this.code = code;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
function recordFrom(value, label) {
|
|
78
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
79
|
+
throw new WorkosConnectProtocolError("invalid_response", `${label} must be an object`);
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
function exactString(value, field, maxLength) {
|
|
84
|
+
const containsControl = typeof value === "string" && [...value].some((character) => {
|
|
85
|
+
const code = character.charCodeAt(0);
|
|
86
|
+
return code <= 31 || code === 127;
|
|
87
|
+
});
|
|
88
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maxLength || value.trim() !== value || containsControl) {
|
|
89
|
+
throw new WorkosConnectProtocolError("invalid_response", `${field} is invalid`);
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
function canonicalIssuer(value) {
|
|
94
|
+
const issuer = exactString(value, "issuer", MAX_URL_LENGTH);
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = new URL(issuer);
|
|
98
|
+
} catch {
|
|
99
|
+
throw new WorkosConnectProtocolError("invalid_response", "issuer must be an HTTPS origin");
|
|
100
|
+
}
|
|
101
|
+
if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash || parsed.origin !== issuer) {
|
|
102
|
+
throw new WorkosConnectProtocolError("invalid_response", "issuer must be an HTTPS origin");
|
|
103
|
+
}
|
|
104
|
+
return issuer;
|
|
105
|
+
}
|
|
106
|
+
function parseWorkosConnectCredentialMetadata(value) {
|
|
107
|
+
const record = recordFrom(value, "credential metadata");
|
|
108
|
+
if (record.version !== 1 || record.family !== "workos_connect") {
|
|
109
|
+
throw new WorkosConnectProtocolError("invalid_metadata", "credential metadata is invalid");
|
|
110
|
+
}
|
|
111
|
+
const issuer = canonicalIssuer(record.issuer);
|
|
112
|
+
const clientId = exactString(record.clientId, "clientId", MAX_CLIENT_ID_LENGTH);
|
|
113
|
+
const accessTokenHash = exactString(record.accessTokenHash, "accessTokenHash", 64);
|
|
114
|
+
const refreshTokenHash = exactString(record.refreshTokenHash, "refreshTokenHash", 64);
|
|
115
|
+
if (!CONNECT_CLIENT_ID_PATTERN.test(clientId)) {
|
|
116
|
+
throw new WorkosConnectProtocolError("invalid_metadata", "credential clientId is invalid");
|
|
117
|
+
}
|
|
118
|
+
if (!/^[a-f0-9]{64}$/u.test(accessTokenHash)) {
|
|
119
|
+
throw new WorkosConnectProtocolError(
|
|
120
|
+
"invalid_metadata",
|
|
121
|
+
"credential accessTokenHash is invalid"
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (!/^[a-f0-9]{64}$/u.test(refreshTokenHash)) {
|
|
125
|
+
throw new WorkosConnectProtocolError(
|
|
126
|
+
"invalid_metadata",
|
|
127
|
+
"credential refreshTokenHash is invalid"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
version: 1,
|
|
132
|
+
family: "workos_connect",
|
|
133
|
+
issuer,
|
|
134
|
+
clientId,
|
|
135
|
+
accessTokenHash,
|
|
136
|
+
refreshTokenHash
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/lib/credentials.ts
|
|
141
|
+
var CONFIG_DIRECTORY = primConfigDirectory();
|
|
142
|
+
var TOKEN_FILE_PATH = join2(CONFIG_DIRECTORY, "token");
|
|
143
|
+
var REFRESH_TOKEN_PATH = join2(CONFIG_DIRECTORY, "refresh_token");
|
|
144
|
+
var TOKEN_EXPIRES_PATH = join2(CONFIG_DIRECTORY, "token_expires_at");
|
|
145
|
+
var TERMINAL_REFRESH_PATH = join2(CONFIG_DIRECTORY, "refresh_terminal");
|
|
146
|
+
var CREDENTIAL_METADATA_PATH = join2(CONFIG_DIRECTORY, "credential_metadata.json");
|
|
147
|
+
var CREDENTIAL_FAMILY_PATH = join2(CONFIG_DIRECTORY, "credential_family.json");
|
|
148
|
+
var CREDENTIAL_MIGRATION_PATH = join2(CONFIG_DIRECTORY, "credential_migration.json");
|
|
149
|
+
var CREDENTIAL_LOCK_PATH = join2(CONFIG_DIRECTORY, "credentials.lock");
|
|
150
|
+
var MAX_CREDENTIAL_METADATA_BYTES = 4096;
|
|
151
|
+
var CREDENTIAL_MIGRATION_VERSION = 1;
|
|
152
|
+
var CREDENTIAL_MIGRATION_STATE = "family_bound";
|
|
153
|
+
function readTrimmed(path) {
|
|
154
|
+
try {
|
|
155
|
+
const value = readFileSync(path, "utf8").trim();
|
|
156
|
+
return value || void 0;
|
|
157
|
+
} catch {
|
|
158
|
+
return void 0;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function resolveAuthCredential(options = {}) {
|
|
162
|
+
const envToken = (options.env ?? process.env).PRIM_TOKEN?.trim();
|
|
163
|
+
if (envToken) return { token: envToken, source: "environment" };
|
|
164
|
+
const stored = readTrimmed(options.tokenFilePath ?? TOKEN_FILE_PATH);
|
|
165
|
+
if (!stored) return void 0;
|
|
166
|
+
const metadata = readStoredCredentialMetadata(
|
|
167
|
+
options.metadataPath ?? CREDENTIAL_METADATA_PATH,
|
|
168
|
+
options.familyPath ?? CREDENTIAL_FAMILY_PATH,
|
|
169
|
+
options.migrationPath ?? CREDENTIAL_MIGRATION_PATH
|
|
170
|
+
);
|
|
171
|
+
if (metadata.state === "legacy_broker") {
|
|
172
|
+
if (metadata.metadata) {
|
|
173
|
+
const refreshToken2 = readTrimmed(options.refreshTokenPath ?? REFRESH_TOKEN_PATH);
|
|
174
|
+
if (!refreshToken2 || metadata.metadata.accessTokenHash !== credentialFingerprint(stored) || metadata.metadata.refreshTokenHash !== credentialFingerprint(refreshToken2)) {
|
|
175
|
+
return void 0;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { token: stored, source: "token_file" };
|
|
179
|
+
}
|
|
180
|
+
if (metadata.state === "invalid") return void 0;
|
|
181
|
+
const refreshToken = readTrimmed(options.refreshTokenPath ?? REFRESH_TOKEN_PATH);
|
|
182
|
+
if (!refreshToken || metadata.metadata.accessTokenHash !== credentialFingerprint(stored) || metadata.metadata.refreshTokenHash !== credentialFingerprint(refreshToken)) {
|
|
183
|
+
return void 0;
|
|
184
|
+
}
|
|
185
|
+
return { token: stored, source: "token_file" };
|
|
186
|
+
}
|
|
187
|
+
function credentialFingerprint(value) {
|
|
188
|
+
return createHash("sha256").update(value).digest("hex");
|
|
189
|
+
}
|
|
190
|
+
function parseLegacyBrokerCredentialMetadata(value) {
|
|
191
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
192
|
+
throw new Error("credential family is invalid");
|
|
193
|
+
}
|
|
194
|
+
const record = value;
|
|
195
|
+
if (record.version !== 1 || record.family !== "legacy_broker" || typeof record.accessTokenHash !== "string" || typeof record.refreshTokenHash !== "string" || !/^[a-f0-9]{64}$/u.test(record.accessTokenHash) || !/^[a-f0-9]{64}$/u.test(record.refreshTokenHash)) {
|
|
196
|
+
throw new Error("credential family is invalid");
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
version: 1,
|
|
200
|
+
family: "legacy_broker",
|
|
201
|
+
accessTokenHash: record.accessTokenHash,
|
|
202
|
+
refreshTokenHash: record.refreshTokenHash
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function readBoundedCredentialJson(path) {
|
|
206
|
+
let raw;
|
|
207
|
+
try {
|
|
208
|
+
raw = readFileSync(path, "utf8");
|
|
209
|
+
} catch (error) {
|
|
210
|
+
return error.code === "ENOENT" ? void 0 : null;
|
|
211
|
+
}
|
|
212
|
+
if (Buffer.byteLength(raw, "utf8") > MAX_CREDENTIAL_METADATA_BYTES) return null;
|
|
213
|
+
try {
|
|
214
|
+
return JSON.parse(raw);
|
|
215
|
+
} catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function readCredentialFamily(path) {
|
|
220
|
+
const parsed = readBoundedCredentialJson(path);
|
|
221
|
+
if (parsed === void 0) return void 0;
|
|
222
|
+
if (parsed === null) return { state: "invalid" };
|
|
223
|
+
try {
|
|
224
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && parsed.family === "legacy_broker") {
|
|
225
|
+
return { state: "legacy_broker", metadata: parseLegacyBrokerCredentialMetadata(parsed) };
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
state: "workos_connect",
|
|
229
|
+
metadata: parseWorkosConnectCredentialMetadata(parsed)
|
|
230
|
+
};
|
|
231
|
+
} catch {
|
|
232
|
+
return { state: "invalid" };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function isCredentialMigrationSentinel(value) {
|
|
236
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
237
|
+
const record = value;
|
|
238
|
+
const keys = Object.keys(record).sort();
|
|
239
|
+
return keys.length === 2 && keys[0] === "state" && keys[1] === "version" && record.version === CREDENTIAL_MIGRATION_VERSION && record.state === CREDENTIAL_MIGRATION_STATE;
|
|
240
|
+
}
|
|
241
|
+
function readCredentialMigrationState(path = CREDENTIAL_MIGRATION_PATH) {
|
|
242
|
+
const parsed = readBoundedCredentialJson(path);
|
|
243
|
+
if (parsed === void 0) return "absent";
|
|
244
|
+
return parsed !== null && isCredentialMigrationSentinel(parsed) ? "valid" : "invalid";
|
|
245
|
+
}
|
|
246
|
+
function readStoredCredentialMetadata(metadataPath = CREDENTIAL_METADATA_PATH, familyPath = CREDENTIAL_FAMILY_PATH, migrationPath = CREDENTIAL_MIGRATION_PATH) {
|
|
247
|
+
const family = readCredentialFamily(familyPath);
|
|
248
|
+
if (family) return family;
|
|
249
|
+
const parsed = readBoundedCredentialJson(metadataPath);
|
|
250
|
+
if (parsed === void 0) {
|
|
251
|
+
return readCredentialMigrationState(migrationPath) === "absent" ? { state: "legacy_broker" } : { state: "invalid" };
|
|
252
|
+
}
|
|
253
|
+
if (parsed === null) return { state: "invalid" };
|
|
254
|
+
try {
|
|
255
|
+
return {
|
|
256
|
+
state: "workos_connect",
|
|
257
|
+
metadata: parseWorkosConnectCredentialMetadata(parsed)
|
|
258
|
+
};
|
|
259
|
+
} catch {
|
|
260
|
+
return { state: "invalid" };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function decodeJwtPayload(token) {
|
|
264
|
+
const parts = token.split(".");
|
|
265
|
+
if (parts.length !== 3) return void 0;
|
|
266
|
+
try {
|
|
267
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
268
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) return void 0;
|
|
269
|
+
return payload;
|
|
270
|
+
} catch {
|
|
271
|
+
return void 0;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
21
274
|
|
|
22
275
|
// src/lib/file-lock.ts
|
|
23
276
|
import { randomBytes } from "crypto";
|
|
24
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
25
|
-
import { dirname, join } from "path";
|
|
277
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
278
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
279
|
+
var syncSleeper = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
26
280
|
|
|
27
|
-
// src/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
281
|
+
// src/lib/terminal-safe.ts
|
|
282
|
+
function replaceIsolatedSurrogates(value) {
|
|
283
|
+
let out = "";
|
|
284
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
285
|
+
const unit = value.charCodeAt(index);
|
|
286
|
+
if (unit >= 55296 && unit <= 56319) {
|
|
287
|
+
const next = value.charCodeAt(index + 1);
|
|
288
|
+
if (next >= 56320 && next <= 57343) {
|
|
289
|
+
out += value[index] + value[index + 1];
|
|
290
|
+
index += 1;
|
|
291
|
+
} else {
|
|
292
|
+
out += "\uFFFD";
|
|
293
|
+
}
|
|
294
|
+
} else if (unit >= 56320 && unit <= 57343) {
|
|
295
|
+
out += "\uFFFD";
|
|
296
|
+
} else {
|
|
297
|
+
out += value[index];
|
|
41
298
|
}
|
|
42
299
|
}
|
|
43
|
-
return
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
function isUnsafeTerminalCodePoint(point) {
|
|
303
|
+
const c0OrC1 = point <= 31 || point >= 127 && point <= 159;
|
|
304
|
+
const bidiOrZeroWidth = point === 173 || // soft hyphen
|
|
305
|
+
point === 1564 || // Arabic letter mark
|
|
306
|
+
point === 6158 || // Mongolian vowel separator
|
|
307
|
+
point >= 8203 && point <= 8207 || // zero-width + directional marks
|
|
308
|
+
point >= 8234 && point <= 8238 || // bidi embeddings/overrides
|
|
309
|
+
point >= 8288 && point <= 8303 || // word joiner + bidi isolates/controls
|
|
310
|
+
point >= 65529 && point <= 65531 || // interlinear annotation controls
|
|
311
|
+
point === 65279 || // zero-width no-break space / BOM
|
|
312
|
+
point >= 917504 && point <= 917631;
|
|
313
|
+
return c0OrC1 || bidiOrZeroWidth;
|
|
44
314
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
var
|
|
49
|
-
|
|
315
|
+
function terminalSafeText(value) {
|
|
316
|
+
return Array.from(replaceIsolatedSurrogates(value)).filter((character) => !isUnsafeTerminalCodePoint(character.codePointAt(0) ?? 0)).join("");
|
|
317
|
+
}
|
|
318
|
+
var stripControlChars = terminalSafeText;
|
|
319
|
+
|
|
320
|
+
// src/client.ts
|
|
321
|
+
var DEFAULT_API_URL = "https://api.getprimitive.ai";
|
|
50
322
|
function getSiteUrl() {
|
|
51
|
-
return
|
|
323
|
+
return getSiteUrlForEnvironment(process.env.PRIM_API_URL);
|
|
52
324
|
}
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
return loadEnvFile(cwd).PRIM_API_URL ?? DEFAULT_API_URL;
|
|
325
|
+
function getSiteUrlForEnvironment(primApiUrl) {
|
|
326
|
+
return primApiUrl?.trim() || DEFAULT_API_URL;
|
|
56
327
|
}
|
|
328
|
+
var CLI_MANAGEMENT_RESPONSE_MAX_BYTES = 2 * 1024 * 1024;
|
|
57
329
|
|
|
58
330
|
// src/daemon/client.ts
|
|
59
331
|
import { createConnection } from "net";
|
|
60
|
-
import {
|
|
61
|
-
|
|
62
|
-
|
|
332
|
+
import { join as join5 } from "path";
|
|
333
|
+
|
|
334
|
+
// src/daemon/principal.ts
|
|
335
|
+
import { createHash as createHash3 } from "crypto";
|
|
336
|
+
var PRINCIPAL_COMPONENT_MAX_LENGTH = 512;
|
|
337
|
+
function boundedClaim(value) {
|
|
338
|
+
return typeof value === "string" && value.length > 0 && value.length <= PRINCIPAL_COMPONENT_MAX_LENGTH ? value : void 0;
|
|
339
|
+
}
|
|
340
|
+
function sha256(value) {
|
|
341
|
+
return createHash3("sha256").update(value).digest("hex");
|
|
342
|
+
}
|
|
343
|
+
function resolveDaemonPrincipal(token = resolveAuthCredential()?.token) {
|
|
344
|
+
if (!token) return void 0;
|
|
345
|
+
const payload = decodeJwtPayload(token);
|
|
346
|
+
const subject = boundedClaim(payload?.sub);
|
|
347
|
+
const organizationId = boundedClaim(payload?.org_id);
|
|
348
|
+
if (!(subject && organizationId)) return void 0;
|
|
349
|
+
const issuer = boundedClaim(payload?.iss) ?? "unknown-issuer";
|
|
350
|
+
return {
|
|
351
|
+
principalId: sha256(`${issuer}\0${subject}`),
|
|
352
|
+
organizationId,
|
|
353
|
+
credentialFingerprint: sha256(token)
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/daemon/protocol.ts
|
|
358
|
+
var DAEMON_JSON_REQUEST_MAX_BYTES = 64 * 1024;
|
|
359
|
+
var DAEMON_JSON_RESPONSE_MAX_BYTES = 8 * 1024 * 1024;
|
|
360
|
+
function isRecord(value) {
|
|
361
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
362
|
+
}
|
|
363
|
+
function parseDaemonResponseEnvelope(value) {
|
|
364
|
+
if (!isRecord(value)) return void 0;
|
|
365
|
+
if (!(typeof value.id === "number" && Number.isSafeInteger(value.id) && value.id >= 0)) {
|
|
366
|
+
return void 0;
|
|
367
|
+
}
|
|
368
|
+
if (typeof value.ok !== "boolean") return void 0;
|
|
369
|
+
if (value.error !== void 0 && typeof value.error !== "string") return void 0;
|
|
370
|
+
return {
|
|
371
|
+
id: value.id,
|
|
372
|
+
ok: value.ok,
|
|
373
|
+
...value.result === void 0 ? {} : { result: value.result },
|
|
374
|
+
...value.error === void 0 ? {} : { error: value.error }
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/daemon/client.ts
|
|
379
|
+
var SOCK_PATH = join5(primConfigDirectory(), "sock");
|
|
63
380
|
var DEFAULT_TIMEOUT_MS = 250;
|
|
64
381
|
var nextRequestId = 1;
|
|
65
382
|
function daemonRequest(method, params = {}, opts = {}) {
|
|
66
383
|
const id = nextRequestId++;
|
|
67
|
-
const
|
|
384
|
+
const caller = resolveDaemonPrincipal();
|
|
385
|
+
const request = {
|
|
386
|
+
id,
|
|
387
|
+
method,
|
|
388
|
+
params,
|
|
389
|
+
...caller ? { caller } : {}
|
|
390
|
+
};
|
|
68
391
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
69
392
|
return new Promise((resolve4) => {
|
|
70
393
|
let settled = false;
|
|
71
394
|
let socket;
|
|
72
|
-
let buffer =
|
|
395
|
+
let buffer = Buffer.alloc(0);
|
|
73
396
|
const settle = (value) => {
|
|
74
397
|
if (settled) {
|
|
75
398
|
return;
|
|
@@ -106,15 +429,20 @@ function daemonRequest(method, params = {}, opts = {}) {
|
|
|
106
429
|
}
|
|
107
430
|
});
|
|
108
431
|
socket.on("data", (chunk) => {
|
|
109
|
-
buffer
|
|
110
|
-
|
|
432
|
+
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
|
|
433
|
+
if (buffer.length > DAEMON_JSON_RESPONSE_MAX_BYTES) {
|
|
434
|
+
clearTimeout(timer);
|
|
435
|
+
settle(null);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const newlineIdx = buffer.indexOf(10);
|
|
111
439
|
if (newlineIdx === -1) {
|
|
112
440
|
return;
|
|
113
441
|
}
|
|
114
|
-
const line = buffer.
|
|
442
|
+
const line = buffer.subarray(0, newlineIdx).toString("utf8");
|
|
115
443
|
try {
|
|
116
|
-
const res = JSON.parse(line);
|
|
117
|
-
if (res.id !== id) {
|
|
444
|
+
const res = parseDaemonResponseEnvelope(JSON.parse(line));
|
|
445
|
+
if (!res || res.id !== id) {
|
|
118
446
|
clearTimeout(timer);
|
|
119
447
|
settle(null);
|
|
120
448
|
return;
|
|
@@ -137,16 +465,18 @@ function daemonRequest(method, params = {}, opts = {}) {
|
|
|
137
465
|
|
|
138
466
|
// src/lib/activation.ts
|
|
139
467
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
140
|
-
import { existsSync as existsSync3, readFileSync as
|
|
141
|
-
import { homedir as
|
|
142
|
-
import { join as
|
|
468
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
469
|
+
import { homedir as homedir2 } from "os";
|
|
470
|
+
import { join as join6 } from "path";
|
|
143
471
|
|
|
144
472
|
// src/lib/git.ts
|
|
145
473
|
import { execFileSync } from "child_process";
|
|
146
|
-
import { createHash as
|
|
474
|
+
import { createHash as createHash4 } from "crypto";
|
|
147
475
|
import { lstatSync, realpathSync } from "fs";
|
|
148
|
-
import { basename, dirname as
|
|
476
|
+
import { basename, dirname as dirname4, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "path";
|
|
149
477
|
var GIT_TIMEOUT_MS = 1e3;
|
|
478
|
+
var GITHUB_OWNER_PATTERN = /^(?![A-Za-z0-9-]*--)[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/u;
|
|
479
|
+
var GITHUB_REPOSITORY_PATTERN = /^[A-Za-z0-9._-]{1,100}$/u;
|
|
150
480
|
function gitToplevel(cwd) {
|
|
151
481
|
try {
|
|
152
482
|
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
@@ -159,10 +489,41 @@ function gitToplevel(cwd) {
|
|
|
159
489
|
return null;
|
|
160
490
|
}
|
|
161
491
|
}
|
|
492
|
+
function gitValue(cwd, args) {
|
|
493
|
+
try {
|
|
494
|
+
const value = execFileSync("git", args, {
|
|
495
|
+
cwd,
|
|
496
|
+
encoding: "utf-8",
|
|
497
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
498
|
+
timeout: GIT_TIMEOUT_MS
|
|
499
|
+
}).trim();
|
|
500
|
+
return value || void 0;
|
|
501
|
+
} catch {
|
|
502
|
+
return void 0;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function githubRepositoryFullName(cwd) {
|
|
506
|
+
const remote = gitValue(cwd, ["config", "--get", "remote.origin.url"]);
|
|
507
|
+
if (!remote || [...remote].some((char) => char < " " || char === "\x7F")) return null;
|
|
508
|
+
const match = /^(?:git@github\.com:([^?#]+)|(?:https|ssh):\/\/(?:[^/@]+@)?github\.com(?::\d+)?\/([^?#]+))$/i.exec(
|
|
509
|
+
remote
|
|
510
|
+
);
|
|
511
|
+
const path = match?.[1] ?? match?.[2];
|
|
512
|
+
if (!path) return null;
|
|
513
|
+
const parts = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").split("/");
|
|
514
|
+
if (parts.length !== 2 || !GITHUB_OWNER_PATTERN.test(parts[0] ?? "") || !GITHUB_REPOSITORY_PATTERN.test(parts[1] ?? "")) {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
return `${parts[0]}/${parts[1]}`;
|
|
518
|
+
}
|
|
162
519
|
|
|
163
520
|
// src/lib/activation.ts
|
|
164
521
|
var PRIM_ACTIVE_KEY = "prim.active";
|
|
522
|
+
var PRIM_REPO_SYNC_ID_KEY = "prim.repoSyncId";
|
|
523
|
+
var PRIM_REPO_SYNC_REPOSITORY_KEY = "prim.repoSyncRepository";
|
|
524
|
+
var PRIM_REPO_BINDING_STATE_KEY = "prim.repoBindingState";
|
|
165
525
|
var GIT_TIMEOUT_MS2 = 1e3;
|
|
526
|
+
var REPO_SYNC_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
166
527
|
var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json"];
|
|
167
528
|
var PRIM_HOOK_BINS = ["prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
|
|
168
529
|
function repoActiveFlag(cwd) {
|
|
@@ -180,14 +541,45 @@ function repoActiveFlag(cwd) {
|
|
|
180
541
|
return void 0;
|
|
181
542
|
}
|
|
182
543
|
}
|
|
544
|
+
function isValidRepoSyncId(value) {
|
|
545
|
+
return typeof value === "string" && REPO_SYNC_ID_RE.test(value);
|
|
546
|
+
}
|
|
547
|
+
function localGitConfigValue(cwd, key) {
|
|
548
|
+
try {
|
|
549
|
+
const value = execFileSync2("git", ["config", "--local", "--get", key], {
|
|
550
|
+
cwd,
|
|
551
|
+
encoding: "utf-8",
|
|
552
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
553
|
+
timeout: GIT_TIMEOUT_MS2
|
|
554
|
+
}).trim();
|
|
555
|
+
return value || void 0;
|
|
556
|
+
} catch {
|
|
557
|
+
return void 0;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function repoSyncId(cwd) {
|
|
561
|
+
const value = localGitConfigValue(cwd, PRIM_REPO_SYNC_ID_KEY);
|
|
562
|
+
if (!isValidRepoSyncId(value)) return void 0;
|
|
563
|
+
const repositoryFullName = localGitConfigValue(cwd, PRIM_REPO_SYNC_REPOSITORY_KEY);
|
|
564
|
+
return repositoryFullName === githubRepositoryFullName(cwd) ? value : void 0;
|
|
565
|
+
}
|
|
566
|
+
function repositoryBindingState(cwd) {
|
|
567
|
+
const rawId = localGitConfigValue(cwd, PRIM_REPO_SYNC_ID_KEY);
|
|
568
|
+
const marker = localGitConfigValue(cwd, PRIM_REPO_BINDING_STATE_KEY);
|
|
569
|
+
if (rawId !== void 0 && !isValidRepoSyncId(rawId)) return "invalid";
|
|
570
|
+
if (marker === "unbound" || marker === "invalid") return marker;
|
|
571
|
+
if (marker === "connected") return repoSyncId(cwd) ? "connected" : "invalid";
|
|
572
|
+
if (marker !== void 0) return "invalid";
|
|
573
|
+
return rawId === void 0 ? "unbound" : repoSyncId(cwd) ? void 0 : "unbound";
|
|
574
|
+
}
|
|
183
575
|
function hasProjectPrimInstall(cwd) {
|
|
184
576
|
const root = gitToplevel(cwd);
|
|
185
|
-
if (root === null || root ===
|
|
577
|
+
if (root === null || root === homedir2()) return false;
|
|
186
578
|
for (const rel of PROJECT_INSTALL_FILES) {
|
|
187
|
-
const path =
|
|
579
|
+
const path = join6(root, rel);
|
|
188
580
|
if (existsSync3(path)) {
|
|
189
581
|
try {
|
|
190
|
-
const content =
|
|
582
|
+
const content = readFileSync4(path, "utf-8");
|
|
191
583
|
if (PRIM_HOOK_BINS.some((bin) => content.includes(bin))) return true;
|
|
192
584
|
} catch {
|
|
193
585
|
}
|
|
@@ -205,30 +597,6 @@ function decisionIngestionStatus(cwd) {
|
|
|
205
597
|
return isRepoActiveForCapture(cwd) ? "enabled" : "disabled";
|
|
206
598
|
}
|
|
207
599
|
|
|
208
|
-
// src/lib/bin-cache.ts
|
|
209
|
-
import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
210
|
-
import { homedir as homedir4 } from "os";
|
|
211
|
-
import { join as join6 } from "path";
|
|
212
|
-
|
|
213
|
-
// src/lib/bin-path.ts
|
|
214
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
215
|
-
import { dirname as dirname4, isAbsolute as isAbsolute2, join as join5 } from "path";
|
|
216
|
-
import { fileURLToPath } from "url";
|
|
217
|
-
var PKG_NAME = "@primitive.ai/prim";
|
|
218
|
-
var NPX_FALLBACK = `npx --yes -p ${PKG_NAME}@latest`;
|
|
219
|
-
|
|
220
|
-
// src/lib/ansi.ts
|
|
221
|
-
function stripControlChars(text) {
|
|
222
|
-
let out = "";
|
|
223
|
-
for (const ch of text) {
|
|
224
|
-
const code = ch.codePointAt(0) ?? 0;
|
|
225
|
-
if (code > 31 && !(code >= 127 && code <= 159)) {
|
|
226
|
-
out += ch;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return out;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
600
|
// src/lib/presence.ts
|
|
233
601
|
var DECISION_ORIGIN = "https://app.getprimitive.ai";
|
|
234
602
|
function validDecisionUrl(value) {
|
|
@@ -280,32 +648,44 @@ function formatTeammatesWithArea(teammates, cap, plainLinks = false) {
|
|
|
280
648
|
}
|
|
281
649
|
|
|
282
650
|
// src/lib/statusline-render.ts
|
|
651
|
+
function repositoryBindingDiagnosticLabel(state) {
|
|
652
|
+
if (state === "unbound") return "repository: unbound (enforcement not evaluating)";
|
|
653
|
+
if (state === "invalid") return "repository binding: invalid (run `prim doctor`)";
|
|
654
|
+
return void 0;
|
|
655
|
+
}
|
|
283
656
|
var STATUSLINE_NAME_CAP = 3;
|
|
284
657
|
function formatStatusline(version, snapshot, resolveIngestionStatus, options = {}) {
|
|
285
658
|
const ingestionSuffix = (status) => status === void 0 ? "" : ` \xB7 Decision ingestion ${status}`;
|
|
286
659
|
const ingestionStatus = snapshot === null || snapshot.healthy === false ? options.includeIngestionWhenUnavailable ? resolveIngestionStatus() : void 0 : resolveIngestionStatus();
|
|
660
|
+
if (snapshot?.healthy === false && snapshot.needsReauth) {
|
|
661
|
+
return `primitive ${version} (daemon: paused \xB7 run \`prim auth login\`${ingestionSuffix(ingestionStatus)})`;
|
|
662
|
+
}
|
|
663
|
+
const repositoryLabel = repositoryBindingDiagnosticLabel(
|
|
664
|
+
snapshot !== null && snapshot.healthy !== false || options.includeIngestionWhenUnavailable === true ? options.resolveRepositoryBindingState?.() : void 0
|
|
665
|
+
);
|
|
666
|
+
const repositorySuffix = repositoryLabel ? ` \xB7 ${repositoryLabel}` : "";
|
|
287
667
|
if (!snapshot) {
|
|
288
|
-
return `primitive ${version} (daemon: down${ingestionSuffix(ingestionStatus)})`;
|
|
668
|
+
return `primitive ${version} (daemon: down${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
|
|
289
669
|
}
|
|
290
670
|
if (snapshot.healthy === false) {
|
|
291
|
-
if (snapshot.needsReauth) {
|
|
292
|
-
return `primitive ${version} (daemon: paused \xB7 run \`prim auth login\`${ingestionSuffix(ingestionStatus)})`;
|
|
293
|
-
}
|
|
294
671
|
if (snapshot.ingestion?.healthy === false) {
|
|
295
672
|
const pending = snapshot.ingestion.pendingCount;
|
|
296
673
|
const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
|
|
297
|
-
return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""}${ingestionSuffix(ingestionStatus)})`;
|
|
674
|
+
return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""}${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
|
|
298
675
|
}
|
|
299
676
|
if (snapshot.heartbeat?.healthy === false) {
|
|
300
|
-
return `primitive ${version} (daemon: degraded \xB7 presence: unavailable${ingestionSuffix(ingestionStatus)})`;
|
|
677
|
+
return `primitive ${version} (daemon: degraded \xB7 presence: unavailable${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
|
|
301
678
|
}
|
|
302
|
-
return `primitive ${version} (daemon: starting${ingestionSuffix(ingestionStatus)})`;
|
|
679
|
+
return `primitive ${version} (daemon: starting${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
|
|
303
680
|
}
|
|
304
681
|
if (snapshot.envMismatch) {
|
|
305
|
-
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: other env)`;
|
|
682
|
+
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus}${repositorySuffix} \xB7 presence: other env)`;
|
|
683
|
+
}
|
|
684
|
+
if (snapshot.principalMismatch) {
|
|
685
|
+
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus}${repositorySuffix} \xB7 presence: other account)`;
|
|
306
686
|
}
|
|
307
687
|
if (snapshot.presenceStale) {
|
|
308
|
-
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: stale)`;
|
|
688
|
+
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus}${repositorySuffix} \xB7 presence: stale)`;
|
|
309
689
|
}
|
|
310
690
|
let team;
|
|
311
691
|
if (snapshot.onlineTeammates !== void 0) {
|
|
@@ -317,14 +697,14 @@ function formatStatusline(version, snapshot, resolveIngestionStatus, options = {
|
|
|
317
697
|
} else {
|
|
318
698
|
team = "team: \u2014";
|
|
319
699
|
}
|
|
320
|
-
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 ${team})`;
|
|
700
|
+
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus}${repositorySuffix} \xB7 ${team})`;
|
|
321
701
|
}
|
|
322
702
|
|
|
323
703
|
// src/commands/statusline.ts
|
|
324
704
|
var STATUSLINE_TIMEOUT_MS = 200;
|
|
325
705
|
function readPackageVersion() {
|
|
326
706
|
try {
|
|
327
|
-
const here = dirname5(
|
|
707
|
+
const here = dirname5(fileURLToPath(import.meta.url));
|
|
328
708
|
const candidates = [
|
|
329
709
|
// Schema-v2 runtimes staged a manifest beside this standalone bundle.
|
|
330
710
|
// Keep resolving it while those legacy launchers transition to schema v3.
|
|
@@ -353,6 +733,12 @@ function debug(msg) {
|
|
|
353
733
|
}
|
|
354
734
|
async function renderStatusline() {
|
|
355
735
|
const version = readPackageVersion();
|
|
736
|
+
const cwd = process.cwd();
|
|
737
|
+
let ingestion;
|
|
738
|
+
const resolveIngestion = () => {
|
|
739
|
+
ingestion ??= decisionIngestionStatus(cwd);
|
|
740
|
+
return ingestion;
|
|
741
|
+
};
|
|
356
742
|
const snapshot = await daemonRequest(
|
|
357
743
|
"status_snapshot",
|
|
358
744
|
// callerEnv lets the daemon withhold presence when it is bound to a different
|
|
@@ -363,7 +749,9 @@ async function renderStatusline() {
|
|
|
363
749
|
if (!snapshot) {
|
|
364
750
|
debug("daemon snapshot missing");
|
|
365
751
|
}
|
|
366
|
-
return formatStatusline(version, snapshot,
|
|
752
|
+
return formatStatusline(version, snapshot, resolveIngestion, {
|
|
753
|
+
resolveRepositoryBindingState: () => resolveIngestion() === "enabled" ? repositoryBindingState(cwd) : void 0
|
|
754
|
+
});
|
|
367
755
|
}
|
|
368
756
|
|
|
369
757
|
// src/statusline-main.ts
|