@primitive.ai/prim 0.1.0-alpha.67 → 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-KKP2NYZ3.js +505 -0
- package/dist/{chunk-JQFN6LBO.js → chunk-LEMX3IOG.js} +6 -37
- 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-XGADQPCI.js +26 -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 +11804 -470
- 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 +64 -32
- package/dist/hooks/pre-commit.js +4 -3
- package/dist/hooks/pre-tool-use.js +118 -38
- package/dist/hooks/prim-hook.js +72 -27
- package/dist/hooks/session-end.js +5 -4
- package/dist/hooks/session-start.js +36 -25
- package/dist/index.js +15039 -2555
- package/dist/statusline-main.js +488 -96
- package/package.json +11 -5
- package/dist/chunk-CZ24W5QU.js +0 -123
- package/dist/chunk-RTLFE6EZ.js +0 -518
- package/dist/chunk-UTKQTZHL.js +0 -88
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primitive.ai/prim",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.69",
|
|
4
4
|
"description": "CLI for Primitive's decision graph — passive decision capture, conflict gate, and team presence",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,13 +48,17 @@
|
|
|
48
48
|
"SKILL.md"
|
|
49
49
|
],
|
|
50
50
|
"scripts": {
|
|
51
|
-
"build": "tsup src/index.ts src/hooks/pre-commit.ts src/hooks/post-commit.ts src/hooks/post-rewrite.ts src/hooks/prim-hook.ts src/hooks/pre-tool-use.ts src/hooks/post-tool-use.ts src/hooks/session-start.ts src/hooks/session-end.ts --format esm --clean && tsup src/statusline-main.ts src/daemon/server.ts --format esm --splitting false",
|
|
51
|
+
"build": "tsup src/index.ts src/hooks/pre-commit.ts src/hooks/post-commit.ts src/hooks/post-rewrite.ts src/hooks/prim-hook.ts src/hooks/pre-tool-use.ts src/hooks/post-tool-use.ts src/hooks/session-start.ts src/hooks/session-end.ts --format esm --clean && tsup src/statusline-main.ts src/daemon/server.ts --format esm --splitting false && tsup --config tsup.hook-runtime.config.ts",
|
|
52
|
+
"contracts:generate": "node scripts/generate-cli-contract.mjs --write",
|
|
53
|
+
"contracts:check": "node scripts/generate-cli-contract.mjs --check",
|
|
54
|
+
"contracts:provenance": "node scripts/update-cli-contract.mjs --check",
|
|
55
|
+
"contracts:update": "node scripts/update-cli-contract.mjs",
|
|
52
56
|
"postbuild": "chmod +x ./dist/index.js ./dist/statusline-main.js ./dist/hooks/pre-commit.js ./dist/hooks/post-commit.js ./dist/hooks/post-rewrite.js ./dist/hooks/prim-hook.js ./dist/hooks/pre-tool-use.js ./dist/hooks/post-tool-use.js ./dist/hooks/session-start.js ./dist/hooks/session-end.js ./dist/daemon/server.js",
|
|
53
57
|
"dev": "tsup src/index.ts src/statusline-main.ts src/hooks/pre-commit.ts src/hooks/post-commit.ts src/hooks/post-rewrite.ts src/hooks/prim-hook.ts src/hooks/pre-tool-use.ts src/hooks/post-tool-use.ts src/hooks/session-start.ts src/hooks/session-end.ts src/daemon/server.ts --format esm --watch --clean --splitting false",
|
|
54
58
|
"clean": "rm -rf dist coverage",
|
|
55
|
-
"lint": "biome check src/",
|
|
56
|
-
"format": "biome check --fix src/",
|
|
57
|
-
"format:check": "biome check src/",
|
|
59
|
+
"lint": "biome check src/ scripts/",
|
|
60
|
+
"format": "biome check --fix src/ scripts/",
|
|
61
|
+
"format:check": "biome check src/ scripts/",
|
|
58
62
|
"typecheck": "tsc --noEmit",
|
|
59
63
|
"test": "vitest run",
|
|
60
64
|
"test:watch": "vitest",
|
|
@@ -73,6 +77,8 @@
|
|
|
73
77
|
"@types/node": "^25.5.0",
|
|
74
78
|
"@types/update-notifier": "^6.0.8",
|
|
75
79
|
"@vitest/coverage-v8": "^3.1.0",
|
|
80
|
+
"ajv": "8.20.0",
|
|
81
|
+
"json-schema-to-typescript": "15.0.4",
|
|
76
82
|
"tsup": "^8.0.0",
|
|
77
83
|
"typescript": "^5.5.0",
|
|
78
84
|
"vitest": "^3.1.0"
|
package/dist/chunk-CZ24W5QU.js
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
// src/lib/bin-path.ts
|
|
2
|
-
import { existsSync, readFileSync } from "fs";
|
|
3
|
-
import { dirname, isAbsolute, join } from "path";
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
5
|
-
var PKG_NAME = "@primitive.ai/prim";
|
|
6
|
-
var ROOT_WALK_LIMIT = 6;
|
|
7
|
-
var NPX_FALLBACK = `npx --yes -p ${PKG_NAME}@latest`;
|
|
8
|
-
var resolvedRoot;
|
|
9
|
-
function locateRoot() {
|
|
10
|
-
if (resolvedRoot !== void 0) {
|
|
11
|
-
return resolvedRoot;
|
|
12
|
-
}
|
|
13
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
14
|
-
for (let depth = 0; depth < ROOT_WALK_LIMIT; depth++) {
|
|
15
|
-
const manifestPath = join(dir, "package.json");
|
|
16
|
-
if (existsSync(manifestPath)) {
|
|
17
|
-
try {
|
|
18
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
19
|
-
if (manifest.name === PKG_NAME && manifest.bin) {
|
|
20
|
-
resolvedRoot = { dir, version: manifest.version, bin: manifest.bin };
|
|
21
|
-
return resolvedRoot;
|
|
22
|
-
}
|
|
23
|
-
} catch {
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
const parent = dirname(dir);
|
|
27
|
-
if (parent === dir) {
|
|
28
|
-
break;
|
|
29
|
-
}
|
|
30
|
-
dir = parent;
|
|
31
|
-
}
|
|
32
|
-
resolvedRoot = null;
|
|
33
|
-
return resolvedRoot;
|
|
34
|
-
}
|
|
35
|
-
function binFile(bin) {
|
|
36
|
-
const root = locateRoot();
|
|
37
|
-
const rel = root?.bin[bin];
|
|
38
|
-
if (!root || !rel) {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
return isAbsolute(rel) ? rel : join(root.dir, rel);
|
|
42
|
-
}
|
|
43
|
-
function packageVersion() {
|
|
44
|
-
return locateRoot()?.version ?? null;
|
|
45
|
-
}
|
|
46
|
-
function shellQuote(value) {
|
|
47
|
-
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
48
|
-
}
|
|
49
|
-
function pinnedHookCommand(bin, args = "") {
|
|
50
|
-
const version = packageVersion();
|
|
51
|
-
if (!version) throw new Error("cannot determine Primitive package version");
|
|
52
|
-
const suffix = args ? ` ${args}` : "";
|
|
53
|
-
const fallback = `npx --yes -p ${PKG_NAME}@${version} ${bin}${suffix}`;
|
|
54
|
-
const file = binFile(bin);
|
|
55
|
-
if (!file) return fallback;
|
|
56
|
-
return `if [ -x ${shellQuote(process.execPath)} ] && [ -f ${shellQuote(file)} ]; then ${shellQuote(process.execPath)} ${shellQuote(file)}${suffix}; else ${fallback}; fi`;
|
|
57
|
-
}
|
|
58
|
-
function detachedHookShimCommand(bin, args = "") {
|
|
59
|
-
return `payload=$(cat); { trap '' HUP; export npm_config_fetch_retries=2 npm_config_fetch_retry_mintimeout=10000 npm_config_fetch_retry_maxtimeout=10000 npm_config_fetch_timeout=60000; printf '%s' "$payload" | { ${pinnedHookCommand(bin, args)}; }; } </dev/null >/dev/null 2>&1 &`;
|
|
60
|
-
}
|
|
61
|
-
function commandMatchesBin(command, bin) {
|
|
62
|
-
if (!command) {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
const c = command.trim();
|
|
66
|
-
if (c === bin || c.startsWith(`${bin} `)) {
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
const exactBin = new RegExp(`(?:^|\\s)${bin.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\s|;|$)`);
|
|
70
|
-
return c.includes(`command -v ${bin} `) || c.includes(`-p ${PKG_NAME}@`) && exactBin.test(c);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// src/lib/bin-cache.ts
|
|
74
|
-
import { chmodSync, mkdirSync, renameSync, writeFileSync } from "fs";
|
|
75
|
-
import { homedir } from "os";
|
|
76
|
-
import { join as join2 } from "path";
|
|
77
|
-
var CACHED_BINS = [
|
|
78
|
-
"prim",
|
|
79
|
-
"prim-hook",
|
|
80
|
-
"prim-pre-tool-use",
|
|
81
|
-
"prim-post-tool-use",
|
|
82
|
-
"prim-post-commit",
|
|
83
|
-
"prim-post-rewrite"
|
|
84
|
-
];
|
|
85
|
-
function binCacheDir() {
|
|
86
|
-
const base = process.env.XDG_CACHE_HOME || join2(homedir(), ".cache");
|
|
87
|
-
return join2(base, "prim", "bin");
|
|
88
|
-
}
|
|
89
|
-
function writeAtomic(path, content) {
|
|
90
|
-
const tmp = `${path}.${process.pid}.tmp`;
|
|
91
|
-
writeFileSync(tmp, content, { mode: 384 });
|
|
92
|
-
renameSync(tmp, path);
|
|
93
|
-
}
|
|
94
|
-
function warmBinCache() {
|
|
95
|
-
try {
|
|
96
|
-
if (process.env.PRIM_BIN_CACHE_HIT) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
if (process.env.PRIM_BIN_CACHE === "0") {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const dir = binCacheDir();
|
|
103
|
-
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
104
|
-
chmodSync(dir, 448);
|
|
105
|
-
writeAtomic(join2(dir, "node"), process.execPath);
|
|
106
|
-
for (const bin of CACHED_BINS) {
|
|
107
|
-
const file = binFile(bin);
|
|
108
|
-
if (file) {
|
|
109
|
-
writeAtomic(join2(dir, bin), file);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
} catch {
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export {
|
|
117
|
-
binFile,
|
|
118
|
-
packageVersion,
|
|
119
|
-
pinnedHookCommand,
|
|
120
|
-
detachedHookShimCommand,
|
|
121
|
-
commandMatchesBin,
|
|
122
|
-
warmBinCache
|
|
123
|
-
};
|
package/dist/chunk-RTLFE6EZ.js
DELETED
|
@@ -1,518 +0,0 @@
|
|
|
1
|
-
// src/lib/file-lock.ts
|
|
2
|
-
import { randomBytes } from "crypto";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
4
|
-
import { dirname, join } from "path";
|
|
5
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
6
|
-
var DEFAULT_POLL_MS = 50;
|
|
7
|
-
var DEFAULT_INIT_GRACE_MS = 2e3;
|
|
8
|
-
var DIRECTORY_MODE = 448;
|
|
9
|
-
var FILE_MODE = 384;
|
|
10
|
-
function ownerPath(lockDir) {
|
|
11
|
-
return join(lockDir, "owner.json");
|
|
12
|
-
}
|
|
13
|
-
function readOwner(lockDir) {
|
|
14
|
-
try {
|
|
15
|
-
const owner = JSON.parse(readFileSync(ownerPath(lockDir), "utf8"));
|
|
16
|
-
if (!Number.isInteger(owner.pid) || owner.pid <= 0 || typeof owner.nonce !== "string" || typeof owner.createdAt !== "number") {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
return owner;
|
|
20
|
-
} catch {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function sameOwner(a, b) {
|
|
25
|
-
return a?.pid === b.pid && a.nonce === b.nonce && a.createdAt === b.createdAt;
|
|
26
|
-
}
|
|
27
|
-
function tryTakeLock(lockDir, now) {
|
|
28
|
-
try {
|
|
29
|
-
mkdirSync(lockDir, { mode: DIRECTORY_MODE });
|
|
30
|
-
} catch (error) {
|
|
31
|
-
if (error.code === "EEXIST") return null;
|
|
32
|
-
throw error;
|
|
33
|
-
}
|
|
34
|
-
const owner = {
|
|
35
|
-
pid: process.pid,
|
|
36
|
-
nonce: randomBytes(16).toString("hex"),
|
|
37
|
-
createdAt: now
|
|
38
|
-
};
|
|
39
|
-
try {
|
|
40
|
-
writeFileSync(ownerPath(lockDir), `${JSON.stringify(owner)}
|
|
41
|
-
`, {
|
|
42
|
-
encoding: "utf8",
|
|
43
|
-
mode: FILE_MODE,
|
|
44
|
-
flag: "wx"
|
|
45
|
-
});
|
|
46
|
-
return owner;
|
|
47
|
-
} catch (error) {
|
|
48
|
-
rmSync(lockDir, { recursive: true, force: true });
|
|
49
|
-
throw error;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function processIsAlive(pid) {
|
|
53
|
-
try {
|
|
54
|
-
process.kill(pid, 0);
|
|
55
|
-
return true;
|
|
56
|
-
} catch (error) {
|
|
57
|
-
return error.code === "EPERM";
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function recoverStaleLock(lockDir, now, alive, initGraceMs) {
|
|
61
|
-
const owner = readOwner(lockDir);
|
|
62
|
-
if (owner) {
|
|
63
|
-
if (alive(owner.pid)) return false;
|
|
64
|
-
if (!sameOwner(readOwner(lockDir), owner)) return false;
|
|
65
|
-
rmSync(lockDir, { recursive: true, force: true });
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
try {
|
|
69
|
-
const before = statSync(lockDir).mtimeMs;
|
|
70
|
-
if (now - before < initGraceMs) return false;
|
|
71
|
-
if (readOwner(lockDir)) return false;
|
|
72
|
-
if (statSync(lockDir).mtimeMs !== before) return false;
|
|
73
|
-
rmSync(lockDir, { recursive: true, force: true });
|
|
74
|
-
return true;
|
|
75
|
-
} catch {
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
function abortError(signal) {
|
|
80
|
-
if (signal.reason instanceof Error) return signal.reason;
|
|
81
|
-
const error = new Error("The operation was aborted");
|
|
82
|
-
error.name = "AbortError";
|
|
83
|
-
return error;
|
|
84
|
-
}
|
|
85
|
-
function throwIfAborted(signal) {
|
|
86
|
-
if (signal?.aborted) throw abortError(signal);
|
|
87
|
-
}
|
|
88
|
-
async function defaultSleep(ms, signal) {
|
|
89
|
-
if (!signal) {
|
|
90
|
-
await new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
throwIfAborted(signal);
|
|
94
|
-
await new Promise((resolve2, reject) => {
|
|
95
|
-
const timer = setTimeout(() => {
|
|
96
|
-
signal.removeEventListener("abort", onAbort);
|
|
97
|
-
resolve2();
|
|
98
|
-
}, ms);
|
|
99
|
-
const onAbort = () => {
|
|
100
|
-
clearTimeout(timer);
|
|
101
|
-
reject(abortError(signal));
|
|
102
|
-
};
|
|
103
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
async function withFileLock(lockDir, operation, options = {}) {
|
|
107
|
-
mkdirSync(dirname(lockDir), { recursive: true, mode: DIRECTORY_MODE });
|
|
108
|
-
const nowMs = options.nowMs ?? Date.now;
|
|
109
|
-
const alive = options.processAlive ?? processIsAlive;
|
|
110
|
-
const deadline = nowMs() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
111
|
-
let owner = null;
|
|
112
|
-
while (!owner) {
|
|
113
|
-
throwIfAborted(options.signal);
|
|
114
|
-
const now = nowMs();
|
|
115
|
-
owner = tryTakeLock(lockDir, now);
|
|
116
|
-
if (owner) break;
|
|
117
|
-
recoverStaleLock(lockDir, now, alive, options.initGraceMs ?? DEFAULT_INIT_GRACE_MS);
|
|
118
|
-
if (now >= deadline) {
|
|
119
|
-
throw new Error(`timed out waiting for file lock ${lockDir}`);
|
|
120
|
-
}
|
|
121
|
-
if (options.sleep) {
|
|
122
|
-
await options.sleep(options.pollMs ?? DEFAULT_POLL_MS);
|
|
123
|
-
throwIfAborted(options.signal);
|
|
124
|
-
} else {
|
|
125
|
-
await defaultSleep(options.pollMs ?? DEFAULT_POLL_MS, options.signal);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
try {
|
|
129
|
-
throwIfAborted(options.signal);
|
|
130
|
-
return await operation();
|
|
131
|
-
} finally {
|
|
132
|
-
if (sameOwner(readOwner(lockDir), owner)) {
|
|
133
|
-
rmSync(lockDir, { recursive: true, force: true });
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// src/client.ts
|
|
139
|
-
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
140
|
-
import {
|
|
141
|
-
chmodSync,
|
|
142
|
-
existsSync as existsSync2,
|
|
143
|
-
mkdirSync as mkdirSync2,
|
|
144
|
-
readFileSync as readFileSync2,
|
|
145
|
-
renameSync,
|
|
146
|
-
rmSync as rmSync2,
|
|
147
|
-
writeFileSync as writeFileSync2
|
|
148
|
-
} from "fs";
|
|
149
|
-
import { homedir } from "os";
|
|
150
|
-
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
151
|
-
var CONFIG_DIR_MODE = 448;
|
|
152
|
-
var CREDENTIAL_FILE_MODE = 384;
|
|
153
|
-
var REFRESH_THRESHOLD_MS = 6e4;
|
|
154
|
-
var DEFAULT_API_URL = "https://api.getprimitive.ai";
|
|
155
|
-
var AUTH_EXPIRED_MESSAGE = "Authentication expired. Run `prim auth login` to re-authenticate.";
|
|
156
|
-
function loadEnvFile(cwd = process.cwd()) {
|
|
157
|
-
const envVars = {};
|
|
158
|
-
for (const file of [".env.local", ".env"]) {
|
|
159
|
-
const filePath = resolve(cwd, file);
|
|
160
|
-
if (!existsSync2(filePath)) continue;
|
|
161
|
-
const content = readFileSync2(filePath, "utf-8");
|
|
162
|
-
for (const line of content.split("\n")) {
|
|
163
|
-
const trimmed = line.trim();
|
|
164
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
165
|
-
const eqIdx = trimmed.indexOf("=");
|
|
166
|
-
if (eqIdx === -1) continue;
|
|
167
|
-
envVars[trimmed.slice(0, eqIdx).trim()] = trimmed.slice(eqIdx + 1).trim();
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return envVars;
|
|
171
|
-
}
|
|
172
|
-
var TOKEN_FILE_PATH = join2(homedir(), ".config", "prim", "token");
|
|
173
|
-
var REFRESH_TOKEN_PATH = join2(dirname2(TOKEN_FILE_PATH), "refresh_token");
|
|
174
|
-
var TOKEN_EXPIRES_PATH = join2(dirname2(TOKEN_FILE_PATH), "token_expires_at");
|
|
175
|
-
var TERMINAL_REFRESH_PATH = join2(dirname2(TOKEN_FILE_PATH), "refresh_terminal");
|
|
176
|
-
var CREDENTIAL_LOCK_PATH = join2(dirname2(TOKEN_FILE_PATH), "credentials.lock");
|
|
177
|
-
function readTrimmed(path) {
|
|
178
|
-
try {
|
|
179
|
-
const value = readFileSync2(path, "utf8").trim();
|
|
180
|
-
return value || void 0;
|
|
181
|
-
} catch {
|
|
182
|
-
return void 0;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
function resolveAuthCredential() {
|
|
186
|
-
if (process.env.PRIM_TOKEN) {
|
|
187
|
-
return { token: process.env.PRIM_TOKEN, source: "environment" };
|
|
188
|
-
}
|
|
189
|
-
const stored = readTrimmed(TOKEN_FILE_PATH);
|
|
190
|
-
if (stored) {
|
|
191
|
-
return { token: stored, source: "token_file" };
|
|
192
|
-
}
|
|
193
|
-
const envToken = loadEnvFile().PRIM_TOKEN;
|
|
194
|
-
return envToken ? { token: envToken, source: "env_file" } : void 0;
|
|
195
|
-
}
|
|
196
|
-
function getSiteUrl() {
|
|
197
|
-
return getSiteUrlForCwd(process.cwd(), process.env.PRIM_API_URL);
|
|
198
|
-
}
|
|
199
|
-
function getSiteUrlForCwd(cwd, primApiUrl) {
|
|
200
|
-
if (primApiUrl) return primApiUrl;
|
|
201
|
-
return loadEnvFile(cwd).PRIM_API_URL ?? DEFAULT_API_URL;
|
|
202
|
-
}
|
|
203
|
-
function getJwtExpiry(token) {
|
|
204
|
-
const parts = token.split(".");
|
|
205
|
-
if (parts.length !== 3) return void 0;
|
|
206
|
-
try {
|
|
207
|
-
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
|
208
|
-
return payload.exp ? payload.exp * 1e3 : void 0;
|
|
209
|
-
} catch {
|
|
210
|
-
return void 0;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
function expiresAtFor(token, expiresIn) {
|
|
214
|
-
return expiresIn === void 0 ? getJwtExpiry(token) : Date.now() + expiresIn * 1e3;
|
|
215
|
-
}
|
|
216
|
-
function ensureConfigDirectory() {
|
|
217
|
-
const directory = dirname2(TOKEN_FILE_PATH);
|
|
218
|
-
mkdirSync2(directory, { recursive: true, mode: CONFIG_DIR_MODE });
|
|
219
|
-
chmodSync(directory, CONFIG_DIR_MODE);
|
|
220
|
-
}
|
|
221
|
-
function atomicWrite(path, content) {
|
|
222
|
-
ensureConfigDirectory();
|
|
223
|
-
const temp = join2(
|
|
224
|
-
dirname2(path),
|
|
225
|
-
`.${path.slice(dirname2(path).length + 1)}.tmp-${process.pid}-${randomBytes2(8).toString("hex")}`
|
|
226
|
-
);
|
|
227
|
-
try {
|
|
228
|
-
writeFileSync2(temp, content, {
|
|
229
|
-
encoding: "utf8",
|
|
230
|
-
mode: CREDENTIAL_FILE_MODE,
|
|
231
|
-
flag: "wx"
|
|
232
|
-
});
|
|
233
|
-
renameSync(temp, path);
|
|
234
|
-
} catch (error) {
|
|
235
|
-
rmSync2(temp, { force: true });
|
|
236
|
-
throw error;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
function removeCredentialFile(path) {
|
|
240
|
-
if (!existsSync2(path)) return false;
|
|
241
|
-
rmSync2(path, { force: true });
|
|
242
|
-
return true;
|
|
243
|
-
}
|
|
244
|
-
function getTokenExpiresAt() {
|
|
245
|
-
const stored = readTrimmed(TOKEN_EXPIRES_PATH);
|
|
246
|
-
if (stored === void 0) return void 0;
|
|
247
|
-
const value = Number(stored);
|
|
248
|
-
return Number.isNaN(value) ? void 0 : value;
|
|
249
|
-
}
|
|
250
|
-
function isTokenExpiringSoon(credential) {
|
|
251
|
-
if (credential.source !== "token_file") return false;
|
|
252
|
-
const expiresAt = getTokenExpiresAt();
|
|
253
|
-
return expiresAt !== void 0 && Date.now() >= expiresAt - REFRESH_THRESHOLD_MS;
|
|
254
|
-
}
|
|
255
|
-
function refreshFingerprint(refreshToken2) {
|
|
256
|
-
return createHash("sha256").update(refreshToken2).digest("hex");
|
|
257
|
-
}
|
|
258
|
-
function terminalFingerprint() {
|
|
259
|
-
return readTrimmed(TERMINAL_REFRESH_PATH);
|
|
260
|
-
}
|
|
261
|
-
function writeTerminalFingerprint(refreshToken2) {
|
|
262
|
-
atomicWrite(TERMINAL_REFRESH_PATH, `${refreshFingerprint(refreshToken2)}
|
|
263
|
-
`);
|
|
264
|
-
}
|
|
265
|
-
function clearTerminalFingerprint() {
|
|
266
|
-
removeCredentialFile(TERMINAL_REFRESH_PATH);
|
|
267
|
-
}
|
|
268
|
-
function isSessionEnded() {
|
|
269
|
-
if (resolveAuthCredential()?.source !== "token_file") return false;
|
|
270
|
-
const refreshToken2 = readTrimmed(REFRESH_TOKEN_PATH);
|
|
271
|
-
const ended = terminalFingerprint();
|
|
272
|
-
return Boolean(refreshToken2 && ended && refreshFingerprint(refreshToken2) === ended);
|
|
273
|
-
}
|
|
274
|
-
function withCredentialLock(operation, options = {}) {
|
|
275
|
-
return withFileLock(CREDENTIAL_LOCK_PATH, operation, options);
|
|
276
|
-
}
|
|
277
|
-
function commitCredentialsUnlocked(credentials) {
|
|
278
|
-
const accessToken = credentials.accessToken.trim();
|
|
279
|
-
const refreshToken2 = credentials.refreshToken.trim();
|
|
280
|
-
if (!accessToken || !refreshToken2) {
|
|
281
|
-
throw new Error("OAuth credentials require both access and refresh tokens");
|
|
282
|
-
}
|
|
283
|
-
atomicWrite(REFRESH_TOKEN_PATH, `${refreshToken2}
|
|
284
|
-
`);
|
|
285
|
-
const expiresAt = expiresAtFor(accessToken, credentials.expiresIn);
|
|
286
|
-
if (expiresAt === void 0) removeCredentialFile(TOKEN_EXPIRES_PATH);
|
|
287
|
-
else atomicWrite(TOKEN_EXPIRES_PATH, `${expiresAt}
|
|
288
|
-
`);
|
|
289
|
-
atomicWrite(TOKEN_FILE_PATH, `${accessToken}
|
|
290
|
-
`);
|
|
291
|
-
clearTerminalFingerprint();
|
|
292
|
-
_cachedCredential = { token: accessToken, source: "token_file" };
|
|
293
|
-
}
|
|
294
|
-
async function commitCredentials(credentials, options = {}) {
|
|
295
|
-
await withCredentialLock(() => commitCredentialsUnlocked(credentials), options);
|
|
296
|
-
}
|
|
297
|
-
async function setStoredToken(token, options = {}) {
|
|
298
|
-
const value = token.trim();
|
|
299
|
-
if (!value) throw new Error("Token cannot be empty");
|
|
300
|
-
await withCredentialLock(() => {
|
|
301
|
-
removeCredentialFile(REFRESH_TOKEN_PATH);
|
|
302
|
-
removeCredentialFile(TOKEN_EXPIRES_PATH);
|
|
303
|
-
clearTerminalFingerprint();
|
|
304
|
-
atomicWrite(TOKEN_FILE_PATH, `${value}
|
|
305
|
-
`);
|
|
306
|
-
_cachedCredential = { token: value, source: "token_file" };
|
|
307
|
-
}, options);
|
|
308
|
-
}
|
|
309
|
-
async function clearStoredCredentials(options = {}) {
|
|
310
|
-
const { beforeClear, ...lockOptions } = options;
|
|
311
|
-
return withCredentialLock(async () => {
|
|
312
|
-
const refreshToken2 = readTrimmed(REFRESH_TOKEN_PATH);
|
|
313
|
-
let callbackError;
|
|
314
|
-
try {
|
|
315
|
-
await beforeClear?.(refreshToken2);
|
|
316
|
-
} catch (error) {
|
|
317
|
-
callbackError = error;
|
|
318
|
-
}
|
|
319
|
-
let removed = false;
|
|
320
|
-
for (const path of [
|
|
321
|
-
TOKEN_FILE_PATH,
|
|
322
|
-
REFRESH_TOKEN_PATH,
|
|
323
|
-
TOKEN_EXPIRES_PATH,
|
|
324
|
-
TERMINAL_REFRESH_PATH
|
|
325
|
-
]) {
|
|
326
|
-
removed = removeCredentialFile(path) || removed;
|
|
327
|
-
}
|
|
328
|
-
_cachedCredential = void 0;
|
|
329
|
-
if (callbackError) throw callbackError;
|
|
330
|
-
return removed;
|
|
331
|
-
}, lockOptions);
|
|
332
|
-
}
|
|
333
|
-
function isTerminalRefreshResponse(response, detail) {
|
|
334
|
-
if (detail.includes("invalid_grant") || detail.includes("Session has already ended")) return true;
|
|
335
|
-
try {
|
|
336
|
-
const error = JSON.parse(detail).error;
|
|
337
|
-
return error === "invalid_grant" || response.status === 401 && error === "Invalid or expired refresh token";
|
|
338
|
-
} catch {
|
|
339
|
-
return response.status === 401 && detail.includes("Invalid or expired refresh token");
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
function refreshDiagnostic(response, detail, quiet) {
|
|
343
|
-
if (quiet) return;
|
|
344
|
-
process.stderr.write(
|
|
345
|
-
`[prim] token refresh rejected by broker: ${response.status} ${response.statusText}${detail ? ` \u2014 ${detail}` : ""}
|
|
346
|
-
`
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
async function performTokenRefresh(options = {}) {
|
|
350
|
-
const selected = resolveAuthCredential();
|
|
351
|
-
if (selected?.source !== "token_file") return void 0;
|
|
352
|
-
const startingGeneration = readTrimmed(REFRESH_TOKEN_PATH);
|
|
353
|
-
if (!startingGeneration || isSessionEnded()) return void 0;
|
|
354
|
-
const startingAccessToken = selected.token;
|
|
355
|
-
return withCredentialLock(
|
|
356
|
-
async () => {
|
|
357
|
-
const currentCredential = resolveAuthCredential();
|
|
358
|
-
if (currentCredential?.source !== "token_file") return void 0;
|
|
359
|
-
const currentGeneration = readTrimmed(REFRESH_TOKEN_PATH);
|
|
360
|
-
if (!currentGeneration) return void 0;
|
|
361
|
-
if (currentGeneration !== startingGeneration) {
|
|
362
|
-
return currentCredential.token;
|
|
363
|
-
}
|
|
364
|
-
if (currentCredential.token !== startingAccessToken) {
|
|
365
|
-
return currentCredential.token;
|
|
366
|
-
}
|
|
367
|
-
if (isSessionEnded()) return void 0;
|
|
368
|
-
const response = await fetch(`${getSiteUrl()}/mcp/broker/refresh`, {
|
|
369
|
-
method: "POST",
|
|
370
|
-
headers: { "Content-Type": "application/json" },
|
|
371
|
-
body: JSON.stringify({ refresh_token: currentGeneration }),
|
|
372
|
-
signal: options.signal
|
|
373
|
-
});
|
|
374
|
-
if (!response.ok) {
|
|
375
|
-
const detail = (await response.text().catch(() => "")).slice(0, 200);
|
|
376
|
-
if (isTerminalRefreshResponse(response, detail) && readTrimmed(REFRESH_TOKEN_PATH) === currentGeneration) {
|
|
377
|
-
writeTerminalFingerprint(currentGeneration);
|
|
378
|
-
}
|
|
379
|
-
refreshDiagnostic(response, detail, options.quiet);
|
|
380
|
-
const winner = resolveAuthCredential();
|
|
381
|
-
return readTrimmed(REFRESH_TOKEN_PATH) !== currentGeneration && winner?.source === "token_file" ? winner.token : void 0;
|
|
382
|
-
}
|
|
383
|
-
let data;
|
|
384
|
-
try {
|
|
385
|
-
data = await response.json();
|
|
386
|
-
} catch {
|
|
387
|
-
if (readTrimmed(REFRESH_TOKEN_PATH) === currentGeneration) {
|
|
388
|
-
writeTerminalFingerprint(currentGeneration);
|
|
389
|
-
}
|
|
390
|
-
return void 0;
|
|
391
|
-
}
|
|
392
|
-
const record = typeof data === "object" && data !== null && !Array.isArray(data) ? data : void 0;
|
|
393
|
-
const accessToken = typeof record?.access_token === "string" ? record.access_token.trim() : "";
|
|
394
|
-
const replacementRefreshToken = typeof record?.refresh_token === "string" ? record.refresh_token.trim() : "";
|
|
395
|
-
if (!(accessToken && replacementRefreshToken)) {
|
|
396
|
-
if (readTrimmed(REFRESH_TOKEN_PATH) === currentGeneration) {
|
|
397
|
-
writeTerminalFingerprint(currentGeneration);
|
|
398
|
-
}
|
|
399
|
-
return void 0;
|
|
400
|
-
}
|
|
401
|
-
commitCredentialsUnlocked({
|
|
402
|
-
accessToken,
|
|
403
|
-
refreshToken: replacementRefreshToken,
|
|
404
|
-
expiresIn: typeof record?.expires_in === "number" && Number.isFinite(record.expires_in) ? record.expires_in : void 0
|
|
405
|
-
});
|
|
406
|
-
return accessToken;
|
|
407
|
-
},
|
|
408
|
-
{ signal: options.signal }
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
var _refreshInFlight;
|
|
412
|
-
function refreshToken(options = {}) {
|
|
413
|
-
if (_refreshInFlight) return _refreshInFlight;
|
|
414
|
-
const attempt = performTokenRefresh(options).then((token) => {
|
|
415
|
-
if (token) _cachedCredential = { token, source: "token_file" };
|
|
416
|
-
return token;
|
|
417
|
-
}).finally(() => {
|
|
418
|
-
if (_refreshInFlight === attempt) _refreshInFlight = void 0;
|
|
419
|
-
});
|
|
420
|
-
_refreshInFlight = attempt;
|
|
421
|
-
return attempt;
|
|
422
|
-
}
|
|
423
|
-
var HttpError = class extends Error {
|
|
424
|
-
status;
|
|
425
|
-
body;
|
|
426
|
-
constructor(status, message, body) {
|
|
427
|
-
super(message);
|
|
428
|
-
this.name = "HttpError";
|
|
429
|
-
this.status = status;
|
|
430
|
-
this.body = body;
|
|
431
|
-
}
|
|
432
|
-
};
|
|
433
|
-
var _cachedCredential;
|
|
434
|
-
function selectedCredential() {
|
|
435
|
-
const resolved = resolveAuthCredential();
|
|
436
|
-
if (!_cachedCredential || resolved?.token !== _cachedCredential.token || resolved?.source !== _cachedCredential.source) {
|
|
437
|
-
_cachedCredential = resolved;
|
|
438
|
-
}
|
|
439
|
-
return _cachedCredential;
|
|
440
|
-
}
|
|
441
|
-
async function request(method, path, body, options) {
|
|
442
|
-
const url = `${getSiteUrl()}${path}`;
|
|
443
|
-
let credential = selectedCredential();
|
|
444
|
-
let refreshAttempted = false;
|
|
445
|
-
if (credential && isTokenExpiringSoon(credential)) {
|
|
446
|
-
refreshAttempted = true;
|
|
447
|
-
const token = await refreshToken({
|
|
448
|
-
signal: options?.signal,
|
|
449
|
-
quiet: options?.quietRefresh
|
|
450
|
-
});
|
|
451
|
-
if (token) credential = { token, source: "token_file" };
|
|
452
|
-
}
|
|
453
|
-
if (!credential) {
|
|
454
|
-
throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
|
|
455
|
-
}
|
|
456
|
-
if (isTokenExpiringSoon(credential) && isSessionEnded()) {
|
|
457
|
-
throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
|
|
458
|
-
}
|
|
459
|
-
const doFetch = (token) => {
|
|
460
|
-
const headers = { "Content-Type": "application/json" };
|
|
461
|
-
if (token) headers.Authorization = `Bearer ${token}`;
|
|
462
|
-
return fetch(url, {
|
|
463
|
-
method,
|
|
464
|
-
headers,
|
|
465
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
466
|
-
signal: options?.signal
|
|
467
|
-
});
|
|
468
|
-
};
|
|
469
|
-
const tokenUsed = credential?.token;
|
|
470
|
-
let response = await doFetch(tokenUsed);
|
|
471
|
-
if (response.status === 401) {
|
|
472
|
-
const latest = resolveAuthCredential();
|
|
473
|
-
let retryToken = latest?.token !== tokenUsed ? latest?.token : void 0;
|
|
474
|
-
if (!retryToken && latest?.source === "token_file" && !refreshAttempted) {
|
|
475
|
-
refreshAttempted = true;
|
|
476
|
-
retryToken = await refreshToken({ signal: options?.signal, quiet: options?.quietRefresh });
|
|
477
|
-
}
|
|
478
|
-
if (retryToken) {
|
|
479
|
-
_cachedCredential = {
|
|
480
|
-
token: retryToken,
|
|
481
|
-
source: latest?.source === "token_file" ? "token_file" : latest?.source ?? "token_file"
|
|
482
|
-
};
|
|
483
|
-
response = await doFetch(retryToken);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
if (!response.ok) {
|
|
487
|
-
if (response.status === 401) {
|
|
488
|
-
throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
|
|
489
|
-
}
|
|
490
|
-
const errorBody = await response.json().catch(() => null);
|
|
491
|
-
const errorRecord = typeof errorBody === "object" && errorBody !== null && !Array.isArray(errorBody) ? errorBody : void 0;
|
|
492
|
-
const message = typeof errorRecord?.error === "string" ? errorRecord.error : `HTTP ${response.status}`;
|
|
493
|
-
throw new HttpError(response.status, message, errorBody);
|
|
494
|
-
}
|
|
495
|
-
return response.json();
|
|
496
|
-
}
|
|
497
|
-
function getClient() {
|
|
498
|
-
return {
|
|
499
|
-
get: (path, options) => request("GET", path, void 0, options),
|
|
500
|
-
post: (path, body, options) => request("POST", path, body, options)
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
export {
|
|
505
|
-
withFileLock,
|
|
506
|
-
TOKEN_FILE_PATH,
|
|
507
|
-
REFRESH_TOKEN_PATH,
|
|
508
|
-
resolveAuthCredential,
|
|
509
|
-
getSiteUrl,
|
|
510
|
-
getTokenExpiresAt,
|
|
511
|
-
isSessionEnded,
|
|
512
|
-
commitCredentials,
|
|
513
|
-
setStoredToken,
|
|
514
|
-
clearStoredCredentials,
|
|
515
|
-
refreshToken,
|
|
516
|
-
HttpError,
|
|
517
|
-
getClient
|
|
518
|
-
};
|