@hasna/hooks 0.9.4 → 0.9.6
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 +40 -2
- package/bin/hooks-mcp.js +330 -149
- package/bin/index.js +974 -350
- package/bin/native-safety-entry.js +1136 -0
- package/bin/serve.js +559 -165
- package/dist/index.d.ts +1 -1
- package/dist/index.js +902 -290
- package/dist/lib/native-safety.d.ts +15 -0
- package/dist/lib/resolver-types.d.ts +1 -1
- package/dist/lib/sync.d.ts +3 -2
- package/dist/lib/transport.d.ts +5 -1
- package/dist/openapi.d.ts +314 -45
- package/dist/sdk/authority.d.ts +31 -0
- package/dist/sdk/generated.d.ts +97 -0
- package/dist/sdk/index.d.ts +16 -17
- package/dist/sdk/index.js +447 -47
- package/dist/sdk/registry-client.d.ts +75 -0
- package/hooks/native-safety-entry.ts +28 -0
- package/package.json +8 -5
- package/scripts/generate-sdk.ts +60 -0
- package/scripts/validate-package.ts +104 -2
- package/scripts/verify-generated-artifacts.ts +237 -0
package/bin/hooks-mcp.js
CHANGED
|
@@ -206,6 +206,7 @@ var init_registry = __esm(() => {
|
|
|
206
206
|
},
|
|
207
207
|
{
|
|
208
208
|
name: "workspace-repos-guard",
|
|
209
|
+
timeoutSeconds: 10,
|
|
209
210
|
displayName: "Workspace Repos Guard",
|
|
210
211
|
description: "Blocks writes and deletes that break the protected repo-checkout structure (~/.hasna/repos/clones canonical, plus legacy ~/workspace/repos during the migration window): writes to a protected root, top-level entry creation, non-org folders, and any delete under a protected root",
|
|
211
212
|
version: "0.1.0",
|
|
@@ -224,7 +225,7 @@ var init_registry = __esm(() => {
|
|
|
224
225
|
matcher: "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$",
|
|
225
226
|
tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
|
|
226
227
|
rewritesInput: true,
|
|
227
|
-
timeoutSeconds:
|
|
228
|
+
timeoutSeconds: 10
|
|
228
229
|
},
|
|
229
230
|
{
|
|
230
231
|
name: "checktests",
|
|
@@ -1129,9 +1130,9 @@ var init_migrations = __esm(() => {
|
|
|
1129
1130
|
});
|
|
1130
1131
|
|
|
1131
1132
|
// src/db/legacy-import.ts
|
|
1132
|
-
import { existsSync as
|
|
1133
|
-
import { join as
|
|
1134
|
-
import { homedir as
|
|
1133
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
1134
|
+
import { join as join6 } from "path";
|
|
1135
|
+
import { homedir as homedir3 } from "os";
|
|
1135
1136
|
function ensureMetaTable(db) {
|
|
1136
1137
|
db.exec(`
|
|
1137
1138
|
CREATE TABLE IF NOT EXISTS _meta (
|
|
@@ -1154,7 +1155,7 @@ function nanoid() {
|
|
|
1154
1155
|
function importJsonlFile(db, filePath) {
|
|
1155
1156
|
let count = 0;
|
|
1156
1157
|
try {
|
|
1157
|
-
const lines =
|
|
1158
|
+
const lines = readFileSync3(filePath, "utf-8").split(`
|
|
1158
1159
|
`).filter(Boolean);
|
|
1159
1160
|
for (const line of lines) {
|
|
1160
1161
|
try {
|
|
@@ -1180,7 +1181,7 @@ function importJsonlFile(db, filePath) {
|
|
|
1180
1181
|
function importErrorsLog(db, filePath) {
|
|
1181
1182
|
let count = 0;
|
|
1182
1183
|
try {
|
|
1183
|
-
const lines =
|
|
1184
|
+
const lines = readFileSync3(filePath, "utf-8").split(`
|
|
1184
1185
|
`).filter(Boolean);
|
|
1185
1186
|
const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
|
|
1186
1187
|
for (const line of lines) {
|
|
@@ -1205,25 +1206,25 @@ function importErrorsLog(db, filePath) {
|
|
|
1205
1206
|
} catch {}
|
|
1206
1207
|
return count;
|
|
1207
1208
|
}
|
|
1208
|
-
function runLegacyImport(db, homeDir =
|
|
1209
|
+
function runLegacyImport(db, homeDir = homedir3()) {
|
|
1209
1210
|
try {
|
|
1210
1211
|
if (isAlreadyDone(db))
|
|
1211
1212
|
return;
|
|
1212
1213
|
let total = 0;
|
|
1213
|
-
const claudeProjectsDir =
|
|
1214
|
-
if (
|
|
1214
|
+
const claudeProjectsDir = join6(homeDir, ".claude", "projects");
|
|
1215
|
+
if (existsSync5(claudeProjectsDir)) {
|
|
1215
1216
|
try {
|
|
1216
1217
|
const projectDirs = readdirSync2(claudeProjectsDir);
|
|
1217
1218
|
for (const dir of projectDirs) {
|
|
1218
|
-
const projectDir =
|
|
1219
|
+
const projectDir = join6(claudeProjectsDir, dir);
|
|
1219
1220
|
try {
|
|
1220
1221
|
const files = readdirSync2(projectDir);
|
|
1221
1222
|
for (const file of files) {
|
|
1222
1223
|
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
1223
|
-
total += importJsonlFile(db,
|
|
1224
|
+
total += importJsonlFile(db, join6(projectDir, file));
|
|
1224
1225
|
}
|
|
1225
1226
|
if (file === "errors.log") {
|
|
1226
|
-
total += importErrorsLog(db,
|
|
1227
|
+
total += importErrorsLog(db, join6(projectDir, file));
|
|
1227
1228
|
}
|
|
1228
1229
|
}
|
|
1229
1230
|
} catch {}
|
|
@@ -1270,8 +1271,8 @@ __export(exports_db, {
|
|
|
1270
1271
|
allowLocalStore: () => allowLocalStore
|
|
1271
1272
|
});
|
|
1272
1273
|
import { Database } from "bun:sqlite";
|
|
1273
|
-
import { existsSync as
|
|
1274
|
-
import { join as
|
|
1274
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, cpSync } from "fs";
|
|
1275
|
+
import { join as join7 } from "path";
|
|
1275
1276
|
function refuseLocalStore(message) {
|
|
1276
1277
|
localStoreRefusal = message;
|
|
1277
1278
|
}
|
|
@@ -1286,8 +1287,8 @@ function localStoreRefusalMessage() {
|
|
|
1286
1287
|
}
|
|
1287
1288
|
function resolveDataDir() {
|
|
1288
1289
|
const effective = getEffectiveDataRoot();
|
|
1289
|
-
const oldDir =
|
|
1290
|
-
if (!
|
|
1290
|
+
const oldDir = join7(getHomeDir(), ".hooks");
|
|
1291
|
+
if (!existsSync6(effective) && existsSync6(oldDir)) {
|
|
1291
1292
|
mkdirSync2(effective, { recursive: true });
|
|
1292
1293
|
cpSync(oldDir, effective, { recursive: true });
|
|
1293
1294
|
}
|
|
@@ -1298,11 +1299,11 @@ function getDbPath() {
|
|
|
1298
1299
|
if (explicitDb)
|
|
1299
1300
|
return explicitDb;
|
|
1300
1301
|
const dataDir2 = resolveDataDir();
|
|
1301
|
-
return
|
|
1302
|
+
return join7(dataDir2, "hooks.db");
|
|
1302
1303
|
}
|
|
1303
1304
|
function ensureDir(dbPath) {
|
|
1304
1305
|
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
1305
|
-
if (dir && !
|
|
1306
|
+
if (dir && !existsSync6(dir)) {
|
|
1306
1307
|
mkdirSync2(dir, { recursive: true });
|
|
1307
1308
|
}
|
|
1308
1309
|
}
|
|
@@ -1312,7 +1313,7 @@ function getDb() {
|
|
|
1312
1313
|
if (instance)
|
|
1313
1314
|
return instance;
|
|
1314
1315
|
const dbPath = getDbPath();
|
|
1315
|
-
const isNew = dbPath === ":memory:" || !
|
|
1316
|
+
const isNew = dbPath === ":memory:" || !existsSync6(dbPath);
|
|
1316
1317
|
ensureDir(dbPath);
|
|
1317
1318
|
instance = new Database(dbPath);
|
|
1318
1319
|
instance.exec("PRAGMA busy_timeout=5000");
|
|
@@ -1418,14 +1419,14 @@ var init_redact = __esm(() => {
|
|
|
1418
1419
|
];
|
|
1419
1420
|
});
|
|
1420
1421
|
|
|
1421
|
-
//
|
|
1422
|
+
// ../contracts/dist/client/transport.js
|
|
1422
1423
|
import { isIP } from "net";
|
|
1423
1424
|
import { spawnSync } from "child_process";
|
|
1424
|
-
import { closeSync as
|
|
1425
|
-
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
1426
|
-
import { createRequire } from "module";
|
|
1425
|
+
import { closeSync as closeSync22, fstatSync as fstatSync2, openSync as openSync22, readFileSync as readFileSync22 } from "fs";
|
|
1426
|
+
import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
|
|
1427
1427
|
import { hostname as osHostname } from "os";
|
|
1428
|
-
import { isAbsolute as
|
|
1428
|
+
import { isAbsolute as isAbsolute3, join as join22 } from "path";
|
|
1429
|
+
import { createRequire } from "module";
|
|
1429
1430
|
function envToken(name) {
|
|
1430
1431
|
return name.toUpperCase().replace(/-/g, "_");
|
|
1431
1432
|
}
|
|
@@ -1448,21 +1449,21 @@ function homeDir(env) {
|
|
|
1448
1449
|
}
|
|
1449
1450
|
function absoluteOverride(env, key) {
|
|
1450
1451
|
const value = env[key]?.trim();
|
|
1451
|
-
return value &&
|
|
1452
|
+
return value && isAbsolute3(value) ? value : null;
|
|
1452
1453
|
}
|
|
1453
1454
|
function hasnaHomeDir(env) {
|
|
1454
1455
|
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
1455
1456
|
if (override)
|
|
1456
1457
|
return override;
|
|
1457
1458
|
const home = homeDir(env);
|
|
1458
|
-
return home ?
|
|
1459
|
+
return home ? join22(home, HASNA_HOME_DIR) : null;
|
|
1459
1460
|
}
|
|
1460
1461
|
function appConfigDir(name, env) {
|
|
1461
1462
|
const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
|
|
1462
1463
|
if (configRoot)
|
|
1463
|
-
return
|
|
1464
|
+
return join22(configRoot, name);
|
|
1464
1465
|
const root = hasnaHomeDir(env);
|
|
1465
|
-
return root ?
|
|
1466
|
+
return root ? join22(root, name, CONFIG_SUBDIR) : null;
|
|
1466
1467
|
}
|
|
1467
1468
|
function credentialDiskSourceList(name, env, profile = null) {
|
|
1468
1469
|
if (!SAFE_APP_SLUG.test(name))
|
|
@@ -1471,7 +1472,7 @@ function credentialDiskSourceList(name, env, profile = null) {
|
|
|
1471
1472
|
if (!directory)
|
|
1472
1473
|
return [];
|
|
1473
1474
|
const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
|
|
1474
|
-
return [{ path:
|
|
1475
|
+
return [{ path: join22(directory, file), tier: "disk" }];
|
|
1475
1476
|
}
|
|
1476
1477
|
function credentialDiskSources(name, env) {
|
|
1477
1478
|
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
@@ -1494,9 +1495,9 @@ function parseEnvFile(text) {
|
|
|
1494
1495
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
1495
1496
|
continue;
|
|
1496
1497
|
let value = withoutExport.slice(equals + 1).trim();
|
|
1497
|
-
const
|
|
1498
|
-
if (
|
|
1499
|
-
if (value.length < 2 || !value.endsWith(
|
|
1498
|
+
const quote2 = value[0];
|
|
1499
|
+
if (quote2 === '"' || quote2 === "'") {
|
|
1500
|
+
if (value.length < 2 || !value.endsWith(quote2)) {
|
|
1500
1501
|
unusable.add(key);
|
|
1501
1502
|
continue;
|
|
1502
1503
|
}
|
|
@@ -1525,7 +1526,7 @@ function readAppConfigFile(path) {
|
|
|
1525
1526
|
};
|
|
1526
1527
|
let fd = -1;
|
|
1527
1528
|
try {
|
|
1528
|
-
fd =
|
|
1529
|
+
fd = openSync22(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
|
|
1529
1530
|
} catch (error) {
|
|
1530
1531
|
const code = error.code;
|
|
1531
1532
|
if (code === "ENOENT" || code === "ENOTDIR")
|
|
@@ -1535,7 +1536,7 @@ function readAppConfigFile(path) {
|
|
|
1535
1536
|
unsafe(`the path could not be opened (${code ?? "unknown error"})`);
|
|
1536
1537
|
}
|
|
1537
1538
|
try {
|
|
1538
|
-
const before =
|
|
1539
|
+
const before = fstatSync2(fd);
|
|
1539
1540
|
if (!before.isFile())
|
|
1540
1541
|
unsafe("the path is not a regular file");
|
|
1541
1542
|
if (!configFileModeAllowed(before.mode)) {
|
|
@@ -1546,31 +1547,39 @@ function readAppConfigFile(path) {
|
|
|
1546
1547
|
unsafe("the file is not owned by the current user");
|
|
1547
1548
|
if (before.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
1548
1549
|
unsafe("the file exceeds the size limit");
|
|
1549
|
-
const bytes =
|
|
1550
|
-
const after =
|
|
1550
|
+
const bytes = readFileSync22(fd);
|
|
1551
|
+
const after = fstatSync2(fd);
|
|
1551
1552
|
if (!configFileReadsCoherent(before, after)) {
|
|
1552
1553
|
unsafe("the file changed while being read");
|
|
1553
1554
|
}
|
|
1554
1555
|
return parseEnvFile(bytes.toString("utf8"));
|
|
1555
1556
|
} finally {
|
|
1556
1557
|
if (fd !== -1)
|
|
1557
|
-
|
|
1558
|
+
closeSync22(fd);
|
|
1558
1559
|
}
|
|
1559
1560
|
}
|
|
1560
|
-
function readCredentialFile(path, apiKeyKeys) {
|
|
1561
|
+
function readCredentialFile(path, apiKeyKeys, pointerKey) {
|
|
1561
1562
|
const parsed = readAppConfigFile(path);
|
|
1562
1563
|
if (!parsed)
|
|
1563
1564
|
return null;
|
|
1564
|
-
for (const key of apiKeyKeys) {
|
|
1565
|
+
for (const key of [...apiKeyKeys, pointerKey]) {
|
|
1565
1566
|
if (parsed.unusable.has(key)) {
|
|
1566
1567
|
throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
|
|
1567
1568
|
}
|
|
1568
1569
|
}
|
|
1569
1570
|
const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
1571
|
+
const pointer = parsed.values.get(pointerKey)?.trim();
|
|
1572
|
+
if (pointer !== undefined) {
|
|
1573
|
+
if (!VAULT_POINTER_SHAPE.test(pointer))
|
|
1574
|
+
throw new CredentialFileUnsafeError(path, `${pointerKey} must name a vault item`);
|
|
1575
|
+
if (values.length)
|
|
1576
|
+
throw new CredentialFileUnsafeError(path, "a credential file cannot select both a literal key and a vault reference");
|
|
1577
|
+
return { apiKey: "", pointerVaultKey: pointer };
|
|
1578
|
+
}
|
|
1570
1579
|
if (new Set(values).size > 1) {
|
|
1571
1580
|
throw new CredentialFileUnsafeError(path, "credential aliases disagree");
|
|
1572
1581
|
}
|
|
1573
|
-
return values[0]
|
|
1582
|
+
return values[0] === undefined ? null : { apiKey: values[0] };
|
|
1574
1583
|
}
|
|
1575
1584
|
function appConfigDiskValue(name, env, keys) {
|
|
1576
1585
|
const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
|
|
@@ -1827,12 +1836,13 @@ function resolveCredential(name, env, options = {}) {
|
|
|
1827
1836
|
}
|
|
1828
1837
|
const paths = profileDiskSources(name, env, profile);
|
|
1829
1838
|
for (const path of paths) {
|
|
1830
|
-
const value = readCredentialFile(path, apiKeyKeys);
|
|
1839
|
+
const value = readCredentialFile(path, apiKeyKeys, pointerKeyName);
|
|
1831
1840
|
if (value) {
|
|
1832
|
-
|
|
1841
|
+
if (!value.pointerVaultKey)
|
|
1842
|
+
assertUsableCredential(name, path, value.apiKey);
|
|
1833
1843
|
return sealCredential({
|
|
1834
|
-
|
|
1835
|
-
tier: "profile",
|
|
1844
|
+
...value,
|
|
1845
|
+
tier: value.pointerVaultKey ? "pointer" : "profile",
|
|
1836
1846
|
source: path,
|
|
1837
1847
|
deliberate: true,
|
|
1838
1848
|
diskCandidates: paths,
|
|
@@ -1840,7 +1850,7 @@ function resolveCredential(name, env, options = {}) {
|
|
|
1840
1850
|
});
|
|
1841
1851
|
}
|
|
1842
1852
|
}
|
|
1843
|
-
throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
|
|
1853
|
+
throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} or ${pointerKeyName} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
|
|
1844
1854
|
}
|
|
1845
1855
|
const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
|
|
1846
1856
|
const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
|
|
@@ -1865,17 +1875,27 @@ function resolveCredential(name, env, options = {}) {
|
|
|
1865
1875
|
});
|
|
1866
1876
|
}
|
|
1867
1877
|
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
1868
|
-
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
1878
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys, pointerKeyName) })).filter((hit) => hit.value !== null);
|
|
1869
1879
|
if (diskHits.length > 0) {
|
|
1870
1880
|
const winner = diskHits[0];
|
|
1871
|
-
|
|
1881
|
+
if (winner.value.pointerVaultKey) {
|
|
1882
|
+
return sealCredential({
|
|
1883
|
+
...winner.value,
|
|
1884
|
+
tier: "pointer",
|
|
1885
|
+
source: winner.src.path,
|
|
1886
|
+
deliberate: false,
|
|
1887
|
+
diskCandidates: diskPaths,
|
|
1888
|
+
warning: null
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
assertUsableCredential(name, winner.src.path, winner.value.apiKey);
|
|
1872
1892
|
const divergentSources = [
|
|
1873
|
-
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
1874
|
-
...envHit && envHit.value !== winner.value ? [envHit.key] : []
|
|
1893
|
+
...diskHits.slice(1).filter((hit) => hit.value.apiKey !== winner.value.apiKey || hit.value.pointerVaultKey !== winner.value.pointerVaultKey).map((hit) => hit.src.path),
|
|
1894
|
+
...envHit && envHit.value !== winner.value.apiKey ? [envHit.key] : []
|
|
1875
1895
|
];
|
|
1876
1896
|
const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
|
|
1877
1897
|
return sealCredential({
|
|
1878
|
-
apiKey: winner.value,
|
|
1898
|
+
apiKey: winner.value.apiKey,
|
|
1879
1899
|
tier: winner.src.tier,
|
|
1880
1900
|
source: winner.src.path,
|
|
1881
1901
|
deliberate: false,
|
|
@@ -2098,8 +2118,9 @@ function credentialDiskSourcesForMessage(name, env) {
|
|
|
2098
2118
|
const paths = credentialDiskSources(name, env);
|
|
2099
2119
|
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
2100
2120
|
}
|
|
2101
|
-
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM, CREDENTIAL_SEAL, AMBIENT_ENVIRONMENT,
|
|
2121
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", MAX_PACKAGE_BYTES, CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM, CREDENTIAL_SEAL, AMBIENT_ENVIRONMENT, requireSecretsSdk, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
|
|
2102
2122
|
var init_transport = __esm(() => {
|
|
2123
|
+
MAX_PACKAGE_BYTES = 1024 * 1024;
|
|
2103
2124
|
CredentialResolutionError = class CredentialResolutionError extends Error {
|
|
2104
2125
|
appName;
|
|
2105
2126
|
attempted;
|
|
@@ -2127,7 +2148,6 @@ var init_transport = __esm(() => {
|
|
|
2127
2148
|
INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
2128
2149
|
CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
2129
2150
|
AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
2130
|
-
SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
2131
2151
|
requireSecretsSdk = createRequire(import.meta.url);
|
|
2132
2152
|
ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
2133
2153
|
DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
@@ -2209,12 +2229,12 @@ var init_local_opt_in = __esm(() => {
|
|
|
2209
2229
|
});
|
|
2210
2230
|
|
|
2211
2231
|
// src/lib/hook-env.ts
|
|
2212
|
-
import { dirname as
|
|
2213
|
-
import { existsSync as
|
|
2232
|
+
import { dirname as dirname6, isAbsolute as isAbsolute4 } from "path";
|
|
2233
|
+
import { existsSync as existsSync11, realpathSync as realpathSync3, statSync } from "fs";
|
|
2214
2234
|
import { tmpdir } from "os";
|
|
2215
2235
|
function isDeniedEnvName(name) {
|
|
2216
2236
|
const upper = name.toUpperCase();
|
|
2217
|
-
if (DENY_PREFIXES.some((
|
|
2237
|
+
if (DENY_PREFIXES.some((prefix2) => upper.startsWith(prefix2)))
|
|
2218
2238
|
return true;
|
|
2219
2239
|
if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
|
|
2220
2240
|
return true;
|
|
@@ -2223,13 +2243,13 @@ function isDeniedEnvName(name) {
|
|
|
2223
2243
|
function isInterpreterInjectionEnvName(name) {
|
|
2224
2244
|
if (INTERPRETER_INJECTION.has(name))
|
|
2225
2245
|
return true;
|
|
2226
|
-
return INTERPRETER_INJECTION_PREFIXES.some((
|
|
2246
|
+
return INTERPRETER_INJECTION_PREFIXES.some((prefix2) => name.startsWith(prefix2));
|
|
2227
2247
|
}
|
|
2228
2248
|
function isBashFunctionEnvName(name) {
|
|
2229
2249
|
return name.startsWith("BASH_FUNC_");
|
|
2230
2250
|
}
|
|
2231
2251
|
function isUnsafePathEntry(entry, home) {
|
|
2232
|
-
if (entry === "" || !
|
|
2252
|
+
if (entry === "" || !isAbsolute4(entry))
|
|
2233
2253
|
return true;
|
|
2234
2254
|
if (home && (entry === home || entry.startsWith(home + "/")))
|
|
2235
2255
|
return true;
|
|
@@ -2239,7 +2259,7 @@ function isUnsafePathEntry(entry, home) {
|
|
|
2239
2259
|
return true;
|
|
2240
2260
|
const canonical = (path) => {
|
|
2241
2261
|
try {
|
|
2242
|
-
return
|
|
2262
|
+
return realpathSync3(path);
|
|
2243
2263
|
} catch {
|
|
2244
2264
|
return path;
|
|
2245
2265
|
}
|
|
@@ -2258,9 +2278,9 @@ function isUnsafePathEntry(entry, home) {
|
|
|
2258
2278
|
}
|
|
2259
2279
|
function sanitizeHookPath(pathValue, home) {
|
|
2260
2280
|
const baseline = PATH_BASELINE.slice();
|
|
2261
|
-
if (process.platform === "darwin" &&
|
|
2281
|
+
if (process.platform === "darwin" && existsSync11("/opt/homebrew/bin"))
|
|
2262
2282
|
baseline.push("/opt/homebrew/bin");
|
|
2263
|
-
const bunDir =
|
|
2283
|
+
const bunDir = dirname6(process.execPath);
|
|
2264
2284
|
const seen = new Set;
|
|
2265
2285
|
const out = [];
|
|
2266
2286
|
for (const dir of baseline) {
|
|
@@ -2385,7 +2405,7 @@ __export(exports_run, {
|
|
|
2385
2405
|
});
|
|
2386
2406
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
2387
2407
|
import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
2388
|
-
import { basename as basename2, dirname as
|
|
2408
|
+
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute5, join as join10 } from "path";
|
|
2389
2409
|
function extensionOf(scriptPath) {
|
|
2390
2410
|
const base = basename2(scriptPath);
|
|
2391
2411
|
const dot = base.lastIndexOf(".");
|
|
@@ -2454,7 +2474,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
2454
2474
|
return chunks.join("");
|
|
2455
2475
|
}
|
|
2456
2476
|
async function executeVerifiedScript(options) {
|
|
2457
|
-
const scriptDir =
|
|
2477
|
+
const scriptDir = dirname7(options.scriptPath);
|
|
2458
2478
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
2459
2479
|
const tempPath = join10(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
2460
2480
|
let proc = null;
|
|
@@ -2465,7 +2485,7 @@ async function executeVerifiedScript(options) {
|
|
|
2465
2485
|
}, options.timeout) : null;
|
|
2466
2486
|
try {
|
|
2467
2487
|
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
2468
|
-
proc = Bun.spawn([...interpreter.command,
|
|
2488
|
+
proc = Bun.spawn([...interpreter.command, isAbsolute5(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
2469
2489
|
stdin: new Response(options.stdin),
|
|
2470
2490
|
stdout: "pipe",
|
|
2471
2491
|
stderr: "pipe",
|
|
@@ -2664,19 +2684,150 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
2664
2684
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2665
2685
|
import { z as z2 } from "zod";
|
|
2666
2686
|
init_registry();
|
|
2667
|
-
import { existsSync as
|
|
2668
|
-
import { join as join11, dirname as
|
|
2687
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
|
|
2688
|
+
import { join as join11, dirname as dirname8 } from "path";
|
|
2669
2689
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2670
2690
|
|
|
2671
2691
|
// src/lib/installer.ts
|
|
2672
2692
|
init_registry();
|
|
2673
2693
|
init_resolve();
|
|
2674
2694
|
init_manifest();
|
|
2675
|
-
import { existsSync as
|
|
2676
|
-
import { join as
|
|
2677
|
-
import { homedir as
|
|
2695
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync6, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
2696
|
+
import { join as join8, dirname as dirname5 } from "path";
|
|
2697
|
+
import { homedir as homedir4 } from "os";
|
|
2678
2698
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2679
2699
|
|
|
2700
|
+
// src/lib/native-safety.ts
|
|
2701
|
+
import { createHash } from "crypto";
|
|
2702
|
+
import { closeSync, constants, existsSync as existsSync4, fstatSync, lstatSync, openSync, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "fs";
|
|
2703
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join5 } from "path";
|
|
2704
|
+
import { homedir as homedir2 } from "os";
|
|
2705
|
+
function isNativeSafetyName(name) {
|
|
2706
|
+
return name === "trash-guard" || name === "workspace-repos-guard";
|
|
2707
|
+
}
|
|
2708
|
+
var supervisorSource = String.raw`async function supervise(encoded) {
|
|
2709
|
+
const fs = require("node:fs");
|
|
2710
|
+
const path = require("node:path");
|
|
2711
|
+
const crypto = require("node:crypto");
|
|
2712
|
+
const cp = require("node:child_process");
|
|
2713
|
+
const fail = () => { process.stderr.write("Native safety guard refused: verification or execution failed.\n"); process.exit(2); };
|
|
2714
|
+
try {
|
|
2715
|
+
const b = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
2716
|
+
const check = (file, hash, maximum) => {
|
|
2717
|
+
if (!path.isAbsolute(file) || fs.realpathSync(file) !== file) throw Error();
|
|
2718
|
+
for (let parent = path.dirname(file);;) {
|
|
2719
|
+
const s = fs.lstatSync(parent);
|
|
2720
|
+
if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022)) throw Error();
|
|
2721
|
+
if (parent === path.dirname(parent)) break;
|
|
2722
|
+
parent = path.dirname(parent);
|
|
2723
|
+
}
|
|
2724
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
2725
|
+
try {
|
|
2726
|
+
const s = fs.fstatSync(fd);
|
|
2727
|
+
if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022) || s.size > maximum) throw Error();
|
|
2728
|
+
const bytes = fs.readFileSync(fd);
|
|
2729
|
+
if (crypto.createHash("sha256").update(bytes).digest("hex") !== hash) throw Error();
|
|
2730
|
+
return bytes;
|
|
2731
|
+
} finally { fs.closeSync(fd); }
|
|
2732
|
+
};
|
|
2733
|
+
check(b.runtime, b.runtimeSHA256, 200_000_000);
|
|
2734
|
+
const bytes = check(b.worker, b.workerSHA256, 256_000);
|
|
2735
|
+
const deadline = setTimeout(fail, 1500);
|
|
2736
|
+
const chunks = []; let size = 0;
|
|
2737
|
+
for await (const chunk of process.stdin) {
|
|
2738
|
+
size += chunk.length;
|
|
2739
|
+
if (size > 1_048_576) throw Error();
|
|
2740
|
+
chunks.push(chunk);
|
|
2741
|
+
}
|
|
2742
|
+
clearTimeout(deadline);
|
|
2743
|
+
const input = Buffer.concat(chunks);
|
|
2744
|
+
const child = cp.spawnSync(b.runtime, ["--config=/dev/null", "--no-env-file", "--no-macros", "--no-install", "--eval", bytes.toString("utf8"), b.name], {
|
|
2745
|
+
input, env: { HOME: b.home, PATH: b.path }, cwd: "/", timeout: 2500, killSignal: "SIGKILL", maxBuffer: 1_048_576,
|
|
2746
|
+
});
|
|
2747
|
+
if (child.error || child.status !== 0 || child.signal || child.stderr.length) throw Error();
|
|
2748
|
+
const result = JSON.parse(child.stdout.toString("utf8"));
|
|
2749
|
+
if (!result || Array.isArray(result) || Object.keys(result).length !== 1) throw Error();
|
|
2750
|
+
if (result.verdict === null) return;
|
|
2751
|
+
const output = result.verdict?.hookSpecificOutput;
|
|
2752
|
+
if (!output || Object.keys(result.verdict).length !== 1 || output.hookEventName !== "PreToolUse") throw Error();
|
|
2753
|
+
if (output.permissionDecision === "deny") {
|
|
2754
|
+
if (Object.keys(output).sort().join() !== "hookEventName,permissionDecision,permissionDecisionReason" || typeof output.permissionDecisionReason !== "string" || !output.permissionDecisionReason.trim()) throw Error();
|
|
2755
|
+
} else if (output.permissionDecision === "allow") {
|
|
2756
|
+
if (b.name !== "trash-guard" || Object.keys(output).sort().join() !== "hookEventName,permissionDecision,updatedInput" || !output.updatedInput || typeof output.updatedInput.command !== "string" || !output.updatedInput.command.trim()) throw Error();
|
|
2757
|
+
} else throw Error();
|
|
2758
|
+
process.stdout.write(JSON.stringify(result.verdict));
|
|
2759
|
+
} catch { fail(); }
|
|
2760
|
+
}`;
|
|
2761
|
+
var quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'";
|
|
2762
|
+
var program = `(${supervisorSource})(process.argv[1]);`;
|
|
2763
|
+
var shell = `cd / || exit 2
|
|
2764
|
+
exec 3<&0 || exit 2
|
|
2765
|
+
/usr/bin/env -i HOME="$3" PATH="$4" "$2" --config=/dev/null --no-env-file --no-macros --no-install --eval ${quote(program)} "$1" <&3 3<&- 2>/dev/null &
|
|
2766
|
+
runner=$!
|
|
2767
|
+
exec 3<&-
|
|
2768
|
+
(trap 'kill "$sleeper" 2>/dev/null; exit 0' TERM; /bin/sleep 6 & sleeper=$!; wait "$sleeper"; kill -KILL "$runner" 2>/dev/null) &
|
|
2769
|
+
watchdog=$!
|
|
2770
|
+
wait "$runner"; result=$?
|
|
2771
|
+
kill "$watchdog" 2>/dev/null; wait "$watchdog" 2>/dev/null
|
|
2772
|
+
if [ "$result" -ne 0 ]; then printf '%s\\n' 'Native safety guard refused: verification or execution failed.' >&2; exit 2; fi`;
|
|
2773
|
+
var prefix = `/usr/bin/env -i /bin/sh -c ${quote(shell)} hasna-native-safety-v1 `;
|
|
2774
|
+
function validateBinding(b) {
|
|
2775
|
+
if (!b || !isNativeSafetyName(b.name) || ![b.runtime, b.worker, b.home].every((p) => typeof p === "string" && isAbsolute2(p) && !/[\0\r\n]/.test(p)) || ![b.runtimeSHA256, b.workerSHA256].every((h) => /^[0-9a-f]{64}$/.test(h)) || typeof b.path !== "string" || b.path.split(":").some((p) => !isAbsolute2(p) || /[\0\r\n]/.test(p)))
|
|
2776
|
+
throw new Error("Invalid native safety binding");
|
|
2777
|
+
}
|
|
2778
|
+
function buildNativeSafetyCommand(binding) {
|
|
2779
|
+
validateBinding(binding);
|
|
2780
|
+
const encoded = Buffer.from(JSON.stringify(binding)).toString("base64url");
|
|
2781
|
+
return prefix + [encoded, binding.runtime, binding.home, binding.path].map(quote).join(" ");
|
|
2782
|
+
}
|
|
2783
|
+
function nativeSafetyRegistration(command) {
|
|
2784
|
+
if (!command.startsWith(prefix))
|
|
2785
|
+
return;
|
|
2786
|
+
const encoded = command.slice(prefix.length).match(/^'([A-Za-z0-9_-]+)' /)?.[1];
|
|
2787
|
+
if (!encoded || encoded.length > 16384)
|
|
2788
|
+
return;
|
|
2789
|
+
try {
|
|
2790
|
+
const binding = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
2791
|
+
return buildNativeSafetyCommand(binding) === command ? binding.name : undefined;
|
|
2792
|
+
} catch {
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
function pinEligibleFile(file, maximum, executable = false) {
|
|
2797
|
+
if (!isAbsolute2(file) || realpathSync2(file) !== file)
|
|
2798
|
+
throw new Error("Native safety requires canonical files");
|
|
2799
|
+
for (let parent = dirname3(file);; ) {
|
|
2800
|
+
const s = lstatSync(parent);
|
|
2801
|
+
if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || s.mode & 18)
|
|
2802
|
+
throw new Error("Native safety requires trusted, non-writable file ancestry");
|
|
2803
|
+
if (parent === dirname3(parent))
|
|
2804
|
+
break;
|
|
2805
|
+
parent = dirname3(parent);
|
|
2806
|
+
}
|
|
2807
|
+
const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
2808
|
+
try {
|
|
2809
|
+
const s = fstatSync(fd);
|
|
2810
|
+
if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || s.mode & 18 || s.size > maximum || executable && !(s.mode & 64))
|
|
2811
|
+
throw new Error("Native safety requires an eligible installed file");
|
|
2812
|
+
const bytes = readFileSync2(fd);
|
|
2813
|
+
if (bytes.length > maximum)
|
|
2814
|
+
throw new Error("Native safety file exceeds its bound");
|
|
2815
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
2816
|
+
} finally {
|
|
2817
|
+
closeSync(fd);
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
function installedNativeSafetyCommand(name) {
|
|
2821
|
+
const home = realpathSync2(homedir2());
|
|
2822
|
+
const runtime = realpathSync2(process.execPath);
|
|
2823
|
+
const candidates = [join5(import.meta.dir, "../../bin/native-safety-entry.js"), join5(import.meta.dir, "../bin/native-safety-entry.js"), join5(import.meta.dir, "native-safety-entry.js")];
|
|
2824
|
+
const worker = candidates.find(existsSync4);
|
|
2825
|
+
if (!worker)
|
|
2826
|
+
throw new Error("Bundled native safety runner is missing; build or reinstall @hasna/hooks before installing its guards.");
|
|
2827
|
+
const canonicalWorker = realpathSync2(worker);
|
|
2828
|
+
return buildNativeSafetyCommand({ name, runtime, runtimeSHA256: pinEligibleFile(runtime, 200000000, true), worker: canonicalWorker, workerSHA256: pinEligibleFile(canonicalWorker, 256000), home, path: `${dirname3(runtime)}:${join5(home, ".bun/bin")}:/usr/local/bin:/usr/bin:/bin` });
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2680
2831
|
// src/lib/registration.ts
|
|
2681
2832
|
init_resolve();
|
|
2682
2833
|
|
|
@@ -2741,6 +2892,9 @@ var REGISTRATION_COMMAND_RE = /^(?:hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?|
|
|
|
2741
2892
|
function registrationName(command) {
|
|
2742
2893
|
if (typeof command !== "string")
|
|
2743
2894
|
return;
|
|
2895
|
+
const native = nativeSafetyRegistration(command);
|
|
2896
|
+
if (native)
|
|
2897
|
+
return native;
|
|
2744
2898
|
if (/^hooks run mementos-context(?:\s|$)/.test(command.trim())) {
|
|
2745
2899
|
try {
|
|
2746
2900
|
retainedMementosRegistration([command.trim()]);
|
|
@@ -2795,8 +2949,7 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
|
|
|
2795
2949
|
if (!Array.isArray(hooks))
|
|
2796
2950
|
return false;
|
|
2797
2951
|
return hooks.some((entry) => entry?.hooks?.some((h) => {
|
|
2798
|
-
|
|
2799
|
-
if (!match || match[1] !== name)
|
|
2952
|
+
if (registrationName(h?.command) !== name)
|
|
2800
2953
|
return false;
|
|
2801
2954
|
if (!matcher)
|
|
2802
2955
|
return true;
|
|
@@ -2808,13 +2961,13 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
|
|
|
2808
2961
|
}
|
|
2809
2962
|
|
|
2810
2963
|
// src/lib/store.ts
|
|
2811
|
-
import { createHash } from "crypto";
|
|
2812
|
-
import { existsSync as
|
|
2964
|
+
import { createHash as createHash2 } from "crypto";
|
|
2965
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
2813
2966
|
init_db();
|
|
2814
2967
|
init_config();
|
|
2815
2968
|
import { randomBytes } from "crypto";
|
|
2816
2969
|
function sha256Of(input) {
|
|
2817
|
-
return
|
|
2970
|
+
return createHash2("sha256").update(input).digest("hex");
|
|
2818
2971
|
}
|
|
2819
2972
|
function storeDb() {
|
|
2820
2973
|
return isLocalStoreRefused() ? null : getDb();
|
|
@@ -2870,11 +3023,11 @@ function assertLockShape(parsed, path) {
|
|
|
2870
3023
|
}
|
|
2871
3024
|
function readLock() {
|
|
2872
3025
|
const path = getLockPath();
|
|
2873
|
-
if (!
|
|
3026
|
+
if (!existsSync7(path))
|
|
2874
3027
|
return { hooks: {} };
|
|
2875
3028
|
let raw;
|
|
2876
3029
|
try {
|
|
2877
|
-
raw =
|
|
3030
|
+
raw = readFileSync4(path, "utf-8");
|
|
2878
3031
|
} catch {
|
|
2879
3032
|
throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
|
|
2880
3033
|
}
|
|
@@ -2946,16 +3099,16 @@ function checkScriptHash(name, actual) {
|
|
|
2946
3099
|
}
|
|
2947
3100
|
|
|
2948
3101
|
// src/lib/codex-settings.ts
|
|
2949
|
-
import { closeSync, existsSync as
|
|
2950
|
-
import { dirname as
|
|
3102
|
+
import { closeSync as closeSync2, existsSync as existsSync8, lstatSync as lstatSync2, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
3103
|
+
import { dirname as dirname4 } from "path";
|
|
2951
3104
|
import { randomUUID } from "crypto";
|
|
2952
3105
|
var snapshots = new WeakMap;
|
|
2953
3106
|
function current(path) {
|
|
2954
3107
|
try {
|
|
2955
|
-
const stat =
|
|
3108
|
+
const stat = lstatSync2(path);
|
|
2956
3109
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
|
|
2957
3110
|
throw new Error("invalid_file");
|
|
2958
|
-
return
|
|
3111
|
+
return readFileSync5(path, "utf8");
|
|
2959
3112
|
} catch (error) {
|
|
2960
3113
|
if (error.code === "ENOENT")
|
|
2961
3114
|
return null;
|
|
@@ -2987,11 +3140,11 @@ function writeCodexSettings(path, settings) {
|
|
|
2987
3140
|
const snapshot = snapshots.get(settings);
|
|
2988
3141
|
if (!snapshot || snapshot.path !== path)
|
|
2989
3142
|
throw new Error("Codex settings require a complete current read before writing.");
|
|
2990
|
-
mkdirSync4(
|
|
3143
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
2991
3144
|
const lock = `${path}.hooks-write.lock`;
|
|
2992
3145
|
let fd;
|
|
2993
3146
|
try {
|
|
2994
|
-
fd =
|
|
3147
|
+
fd = openSync2(lock, "wx", 384);
|
|
2995
3148
|
} catch {
|
|
2996
3149
|
throw new Error("Codex settings are being edited; retry after the active writer finishes.");
|
|
2997
3150
|
}
|
|
@@ -3007,16 +3160,16 @@ function writeCodexSettings(path, settings) {
|
|
|
3007
3160
|
renameSync2(temp, path);
|
|
3008
3161
|
snapshots.set(settings, { path, raw: next });
|
|
3009
3162
|
} finally {
|
|
3010
|
-
if (
|
|
3163
|
+
if (existsSync8(temp))
|
|
3011
3164
|
unlinkSync(temp);
|
|
3012
|
-
|
|
3165
|
+
closeSync2(fd);
|
|
3013
3166
|
unlinkSync(lock);
|
|
3014
3167
|
}
|
|
3015
3168
|
}
|
|
3016
3169
|
|
|
3017
3170
|
// src/lib/installer.ts
|
|
3018
|
-
var __dirname2 =
|
|
3019
|
-
var HOOKS_DIR =
|
|
3171
|
+
var __dirname2 = dirname5(fileURLToPath2(import.meta.url));
|
|
3172
|
+
var HOOKS_DIR = existsSync9(join8(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join8(__dirname2, "..", "..", "hooks") : join8(__dirname2, "..", "hooks");
|
|
3020
3173
|
function normalizeHookName(name) {
|
|
3021
3174
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
3022
3175
|
}
|
|
@@ -3028,8 +3181,7 @@ function removeHookEntriesByName(entries, hookName) {
|
|
|
3028
3181
|
if (!Array.isArray(entry.hooks))
|
|
3029
3182
|
return [entry];
|
|
3030
3183
|
const kept = entry.hooks.filter((h) => {
|
|
3031
|
-
|
|
3032
|
-
return !(match && match[1] === hookName);
|
|
3184
|
+
return registrationName(h.command) !== hookName;
|
|
3033
3185
|
});
|
|
3034
3186
|
return kept.length === entry.hooks.length ? [entry] : kept.length ? [{ ...entry, hooks: kept }] : [];
|
|
3035
3187
|
});
|
|
@@ -3105,29 +3257,29 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
|
|
|
3105
3257
|
return globalOverride;
|
|
3106
3258
|
const dir = getTargetSettingsDir(target);
|
|
3107
3259
|
if (target === "codex")
|
|
3108
|
-
return
|
|
3260
|
+
return join8(scope === "project" ? process.cwd() : homedir4(), dir, "hooks.json");
|
|
3109
3261
|
if (scope === "project") {
|
|
3110
|
-
return target === "codewith" ?
|
|
3262
|
+
return target === "codewith" ? join8(process.cwd(), dir, "config.toml") : join8(process.cwd(), dir, "settings.json");
|
|
3111
3263
|
}
|
|
3112
|
-
return target === "codewith" ?
|
|
3264
|
+
return target === "codewith" ? join8(homedir4(), dir, "config.toml") : join8(homedir4(), dir, "settings.json");
|
|
3113
3265
|
}
|
|
3114
3266
|
function getHookPath(name) {
|
|
3115
3267
|
const shortName = shortHookName(name);
|
|
3116
3268
|
const resolved = resolveHookDir(shortName);
|
|
3117
3269
|
if (resolved)
|
|
3118
3270
|
return resolved;
|
|
3119
|
-
return
|
|
3271
|
+
return join8(HOOKS_DIR, normalizeHookName(shortName));
|
|
3120
3272
|
}
|
|
3121
3273
|
function hookExists(name) {
|
|
3122
|
-
return
|
|
3274
|
+
return existsSync9(getHookPath(name));
|
|
3123
3275
|
}
|
|
3124
3276
|
function readSettings(scope = "global", target = "claude") {
|
|
3125
3277
|
const path = getSettingsPath(scope, target);
|
|
3126
3278
|
if (target === "codex")
|
|
3127
3279
|
return readCodexSettings(path);
|
|
3128
3280
|
try {
|
|
3129
|
-
if (
|
|
3130
|
-
return JSON.parse(
|
|
3281
|
+
if (existsSync9(path)) {
|
|
3282
|
+
return JSON.parse(readFileSync6(path, "utf-8"));
|
|
3131
3283
|
}
|
|
3132
3284
|
} catch (error) {
|
|
3133
3285
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -3138,8 +3290,8 @@ function writeSettings(settings, scope = "global", target = "claude") {
|
|
|
3138
3290
|
const path = getSettingsPath(scope, target);
|
|
3139
3291
|
if (target === "codex")
|
|
3140
3292
|
return writeCodexSettings(path, settings);
|
|
3141
|
-
const dir =
|
|
3142
|
-
if (!
|
|
3293
|
+
const dir = dirname5(path);
|
|
3294
|
+
if (!existsSync9(dir)) {
|
|
3143
3295
|
mkdirSync5(dir, { recursive: true });
|
|
3144
3296
|
}
|
|
3145
3297
|
writeFileSync4(path, JSON.stringify(settings, null, 2) + `
|
|
@@ -3167,6 +3319,9 @@ function codewithTimeout(name) {
|
|
|
3167
3319
|
switch (name) {
|
|
3168
3320
|
case "mementos-context":
|
|
3169
3321
|
return 12;
|
|
3322
|
+
case "trash-guard":
|
|
3323
|
+
case "workspace-repos-guard":
|
|
3324
|
+
return 10;
|
|
3170
3325
|
case "knowledge-context":
|
|
3171
3326
|
return 6;
|
|
3172
3327
|
case "session-start":
|
|
@@ -3176,7 +3331,6 @@ function codewithTimeout(name) {
|
|
|
3176
3331
|
case "prompt-guard":
|
|
3177
3332
|
return 3;
|
|
3178
3333
|
case "worktree-guard":
|
|
3179
|
-
case "trash-guard":
|
|
3180
3334
|
case "stop-sync":
|
|
3181
3335
|
return 5;
|
|
3182
3336
|
default:
|
|
@@ -3205,11 +3359,13 @@ function codewithStatusMessage(name) {
|
|
|
3205
3359
|
}
|
|
3206
3360
|
function buildCodewithTomlFragment(name, profile, mementos) {
|
|
3207
3361
|
const shortName = shortHookName(name);
|
|
3208
|
-
|
|
3362
|
+
if (isNativeSafetyName(shortName) && profile)
|
|
3363
|
+
throw new Error("Bundled native safety guards do not select registry profiles");
|
|
3364
|
+
const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
|
|
3209
3365
|
if (!meta)
|
|
3210
3366
|
throw new Error(`Hook '${shortName}' not found`);
|
|
3211
3367
|
validateMementosRegistration(shortName, mementos);
|
|
3212
|
-
const command = (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
|
|
3368
|
+
const command = isNativeSafetyName(shortName) ? installedNativeSafetyCommand(shortName) : (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
|
|
3213
3369
|
const matcher = codewithMatcher(meta.matcher);
|
|
3214
3370
|
const fragments = [];
|
|
3215
3371
|
for (const event of getHookEvents(meta)) {
|
|
@@ -3234,17 +3390,17 @@ function buildCodewithTomlFragment(name, profile, mementos) {
|
|
|
3234
3390
|
function readCodewithConfig(scope, configPath) {
|
|
3235
3391
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
3236
3392
|
try {
|
|
3237
|
-
return
|
|
3393
|
+
return existsSync9(path) ? readFileSync6(path, "utf-8") : "";
|
|
3238
3394
|
} catch {
|
|
3239
3395
|
return "";
|
|
3240
3396
|
}
|
|
3241
3397
|
}
|
|
3242
3398
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
3243
3399
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
3244
|
-
const dir =
|
|
3245
|
-
if (!
|
|
3400
|
+
const dir = dirname5(path);
|
|
3401
|
+
if (!existsSync9(dir))
|
|
3246
3402
|
mkdirSync5(dir, { recursive: true });
|
|
3247
|
-
const existing =
|
|
3403
|
+
const existing = existsSync9(path) ? readFileSync6(path, "utf-8") : "";
|
|
3248
3404
|
const sep2 = existing.trim() ? `
|
|
3249
3405
|
|
|
3250
3406
|
` : "";
|
|
@@ -3252,7 +3408,7 @@ function appendCodewithFragment(fragment, scope, configPath) {
|
|
|
3252
3408
|
return path;
|
|
3253
3409
|
}
|
|
3254
3410
|
function detectConflict(name, scope, target) {
|
|
3255
|
-
const meta = resolveHookMeta(name);
|
|
3411
|
+
const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
3256
3412
|
if (!meta || !meta.matcher)
|
|
3257
3413
|
return;
|
|
3258
3414
|
const events = new Set(getHookEvents(meta));
|
|
@@ -3272,7 +3428,7 @@ function detectConflict(name, scope, target) {
|
|
|
3272
3428
|
return;
|
|
3273
3429
|
}
|
|
3274
3430
|
function detectRewriteConflict(name, scope, target, lookup = getHook) {
|
|
3275
|
-
const meta = resolveHookMeta(name);
|
|
3431
|
+
const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
3276
3432
|
if (!meta?.matcher || !meta.rewritesInput)
|
|
3277
3433
|
return;
|
|
3278
3434
|
if (!getHookEvents(meta).includes("PreToolUse"))
|
|
@@ -3293,7 +3449,10 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
|
|
|
3293
3449
|
}
|
|
3294
3450
|
function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
|
|
3295
3451
|
const shortName = shortHookName(name);
|
|
3296
|
-
if (
|
|
3452
|
+
if (isNativeSafetyName(shortName) && (profile || target === "gemini" || target === "codewith" && codewithMode === "write")) {
|
|
3453
|
+
return { hook: shortName, success: false, error: "Bundled native safety guards support Claude/Codex and managed Codewith fragments, without registry profiles.", target };
|
|
3454
|
+
}
|
|
3455
|
+
if (!isNativeSafetyName(shortName) && !hookExists(shortName)) {
|
|
3297
3456
|
return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
|
|
3298
3457
|
}
|
|
3299
3458
|
if (target === "codewith") {
|
|
@@ -3385,7 +3544,7 @@ function installHook(name, options = {}) {
|
|
|
3385
3544
|
}
|
|
3386
3545
|
if (target === "all") {
|
|
3387
3546
|
const shortName = shortHookName(name);
|
|
3388
|
-
const meta = resolveHookMeta(shortName);
|
|
3547
|
+
const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
|
|
3389
3548
|
if (meta) {
|
|
3390
3549
|
const unsupportedTargets = ["claude", "codewith"].filter((agentTarget) => getHookEvents(meta).some((event) => !isEventSupported(event, agentTarget)));
|
|
3391
3550
|
if (unsupportedTargets.length > 0) {
|
|
@@ -3418,7 +3577,7 @@ function installHook(name, options = {}) {
|
|
|
3418
3577
|
return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
|
|
3419
3578
|
}
|
|
3420
3579
|
function registerHook(name, scope = "global", target = "claude", profile, mementos) {
|
|
3421
|
-
const meta = resolveHookMeta(name);
|
|
3580
|
+
const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
3422
3581
|
if (!meta)
|
|
3423
3582
|
return;
|
|
3424
3583
|
const eventKeys = getHookEvents(meta).map((event) => {
|
|
@@ -3435,8 +3594,17 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
3435
3594
|
const settings = readSettings(scope, target);
|
|
3436
3595
|
if (!settings.hooks)
|
|
3437
3596
|
settings.hooks = {};
|
|
3597
|
+
if (isNativeSafetyName(name)) {
|
|
3598
|
+
for (const groups of Object.values(settings.hooks))
|
|
3599
|
+
for (const group of groups)
|
|
3600
|
+
for (const hook of group.hooks ?? []) {
|
|
3601
|
+
if (typeof hook.command === "string" && /(?:^|\s)hasna-native-safety-v\d+(?:\s|$)/.test(hook.command) && !registrationName(hook.command)) {
|
|
3602
|
+
throw new Error("Unknown managed safety definition; reconcile its exact version before replacing it.");
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3438
3606
|
const retained = name === "mementos-context" && mementos === undefined ? retainedMementosRegistration(Object.values(settings.hooks).flatMap((groups) => Array.isArray(groups) ? groups.flatMap((group) => Array.isArray(group.hooks) ? group.hooks.map((hook) => hook.command).filter((command) => typeof command === "string") : []) : [])) : undefined;
|
|
3439
|
-
const hookCommand = (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
|
|
3607
|
+
const hookCommand = isNativeSafetyName(name) ? installedNativeSafetyCommand(name) : (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
|
|
3440
3608
|
if (target === "codex") {
|
|
3441
3609
|
const positions = [];
|
|
3442
3610
|
for (const [event, groups] of Object.entries(settings.hooks)) {
|
|
@@ -3444,9 +3612,11 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
3444
3612
|
for (let index = 0;index < entry.hooks.length; index++) {
|
|
3445
3613
|
const command = entry.hooks[index]?.command;
|
|
3446
3614
|
const match = typeof command === "string" && command.trim().match(/^(?:hooks run ([\w-]+)(?:\s|$)|hook-([\w-]+)$)/);
|
|
3447
|
-
if (match && (match[1] ?? match[2]) === name) {
|
|
3615
|
+
if (match && (match[1] ?? match[2]) === name || registrationName(command) === name) {
|
|
3448
3616
|
if (registrationName(command) !== name)
|
|
3449
3617
|
throw new Error("Unrecognized Codex registration; reconcile it before updating this hook.");
|
|
3618
|
+
if (isNativeSafetyName(name) && entry.hooks[index].async !== undefined && entry.hooks[index].async !== false)
|
|
3619
|
+
throw new Error("Asynchronous safety hooks cannot block; reconcile this registration before updating it.");
|
|
3450
3620
|
positions.push({ event, entry, index });
|
|
3451
3621
|
}
|
|
3452
3622
|
}
|
|
@@ -3507,12 +3677,20 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
|
|
|
3507
3677
|
if (target === "codewith") {
|
|
3508
3678
|
const config = readCodewithConfig(scope);
|
|
3509
3679
|
const registered2 = [];
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3680
|
+
try {
|
|
3681
|
+
const parsed = Bun.TOML.parse(config);
|
|
3682
|
+
for (const groups of Object.values(parsed.hooks ?? {})) {
|
|
3683
|
+
if (!Array.isArray(groups))
|
|
3684
|
+
continue;
|
|
3685
|
+
for (const group of groups)
|
|
3686
|
+
for (const hook of group.hooks ?? []) {
|
|
3687
|
+
const name = registrationName(hook.command);
|
|
3688
|
+
if (name)
|
|
3689
|
+
registered2.push(name);
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
} catch {
|
|
3693
|
+
return [];
|
|
3516
3694
|
}
|
|
3517
3695
|
return [...new Set(registered2)];
|
|
3518
3696
|
}
|
|
@@ -3558,12 +3736,12 @@ function removeHookForTarget(name, scope, target) {
|
|
|
3558
3736
|
|
|
3559
3737
|
// src/lib/profiles.ts
|
|
3560
3738
|
init_app_home();
|
|
3561
|
-
import { existsSync as
|
|
3562
|
-
import { join as
|
|
3739
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
|
|
3740
|
+
import { join as join9 } from "path";
|
|
3563
3741
|
function getProfilesDir() {
|
|
3564
|
-
const newDir =
|
|
3565
|
-
const oldDir =
|
|
3566
|
-
if (!
|
|
3742
|
+
const newDir = join9(getEffectiveDataRoot(), "profiles");
|
|
3743
|
+
const oldDir = join9(getHomeDir(), ".hooks", "profiles");
|
|
3744
|
+
if (!existsSync10(newDir) && existsSync10(oldDir)) {
|
|
3567
3745
|
mkdirSync6(newDir, { recursive: true });
|
|
3568
3746
|
cpSync2(oldDir, newDir, { recursive: true });
|
|
3569
3747
|
}
|
|
@@ -3571,12 +3749,12 @@ function getProfilesDir() {
|
|
|
3571
3749
|
}
|
|
3572
3750
|
function ensureProfilesDir() {
|
|
3573
3751
|
const dir = getProfilesDir();
|
|
3574
|
-
if (!
|
|
3752
|
+
if (!existsSync10(dir)) {
|
|
3575
3753
|
mkdirSync6(dir, { recursive: true });
|
|
3576
3754
|
}
|
|
3577
3755
|
}
|
|
3578
3756
|
function profilePath(id) {
|
|
3579
|
-
return
|
|
3757
|
+
return join9(getProfilesDir(), `${id}.json`);
|
|
3580
3758
|
}
|
|
3581
3759
|
function shortUuid() {
|
|
3582
3760
|
return crypto.randomUUID().slice(0, 8);
|
|
@@ -3602,23 +3780,23 @@ function createProfile(input) {
|
|
|
3602
3780
|
function getProfile(id) {
|
|
3603
3781
|
const path = profilePath(id);
|
|
3604
3782
|
try {
|
|
3605
|
-
if (!
|
|
3783
|
+
if (!existsSync10(path))
|
|
3606
3784
|
return null;
|
|
3607
|
-
return JSON.parse(
|
|
3785
|
+
return JSON.parse(readFileSync7(path, "utf-8"));
|
|
3608
3786
|
} catch {
|
|
3609
3787
|
return null;
|
|
3610
3788
|
}
|
|
3611
3789
|
}
|
|
3612
3790
|
function listProfiles() {
|
|
3613
3791
|
const dir = getProfilesDir();
|
|
3614
|
-
if (!
|
|
3792
|
+
if (!existsSync10(dir))
|
|
3615
3793
|
return [];
|
|
3616
3794
|
try {
|
|
3617
3795
|
const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
|
|
3618
3796
|
const profiles = [];
|
|
3619
3797
|
for (const file of files) {
|
|
3620
3798
|
try {
|
|
3621
|
-
const content =
|
|
3799
|
+
const content = readFileSync7(join9(dir, file), "utf-8");
|
|
3622
3800
|
profiles.push(JSON.parse(content));
|
|
3623
3801
|
} catch {}
|
|
3624
3802
|
}
|
|
@@ -3670,6 +3848,7 @@ function rethrowHooksAuthorityFailure(error) {
|
|
|
3670
3848
|
}
|
|
3671
3849
|
throw new Error(`REMOTE_API_URL_INVALID: ${message} local mode fallback is disabled`, { cause: error });
|
|
3672
3850
|
}
|
|
3851
|
+
var authorityBindings = new WeakMap;
|
|
3673
3852
|
function resolveHooksTransport(env = process.env, options = {}) {
|
|
3674
3853
|
if (selectsHooksLocalStore(env)) {
|
|
3675
3854
|
announceLocal(options.notice, "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority");
|
|
@@ -3683,7 +3862,7 @@ function resolveHooksTransport(env = process.env, options = {}) {
|
|
|
3683
3862
|
try {
|
|
3684
3863
|
credential = resolveCredential("hooks", resolverEnv, credentialOptions);
|
|
3685
3864
|
resolution = resolveClientTransport("hooks", resolverEnv, {
|
|
3686
|
-
credentials: credential ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
|
|
3865
|
+
credentials: credential && credential.tier !== "pointer" ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
|
|
3687
3866
|
});
|
|
3688
3867
|
} catch (error) {
|
|
3689
3868
|
rethrowHooksAuthorityFailure(error);
|
|
@@ -3691,18 +3870,20 @@ function resolveHooksTransport(env = process.env, options = {}) {
|
|
|
3691
3870
|
if (!credential) {
|
|
3692
3871
|
throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved; " + "local mode fallback is disabled");
|
|
3693
3872
|
}
|
|
3873
|
+
const authority = {
|
|
3874
|
+
origin: hooksRegistryOrigin(resolution.baseUrl),
|
|
3875
|
+
apiKey: credential.apiKey,
|
|
3876
|
+
apiUrlSource: resolution.apiUrlSource,
|
|
3877
|
+
apiKeySource: credential.source,
|
|
3878
|
+
apiKeyTier: credential.tier,
|
|
3879
|
+
v1BaseUrl: resolution.baseUrl,
|
|
3880
|
+
warning: resolution.warning
|
|
3881
|
+
};
|
|
3882
|
+
authorityBindings.set(authority, { env, options, credential });
|
|
3694
3883
|
return {
|
|
3695
3884
|
mode: "remote",
|
|
3696
3885
|
source: `${credential.source}+${resolution.apiUrlSource ?? "default"}`,
|
|
3697
|
-
authority
|
|
3698
|
-
origin: hooksRegistryOrigin(resolution.baseUrl),
|
|
3699
|
-
apiKey: credential.apiKey,
|
|
3700
|
-
apiUrlSource: resolution.apiUrlSource,
|
|
3701
|
-
apiKeySource: credential.source,
|
|
3702
|
-
apiKeyTier: credential.tier,
|
|
3703
|
-
v1BaseUrl: resolution.baseUrl,
|
|
3704
|
-
warning: resolution.warning
|
|
3705
|
-
}
|
|
3886
|
+
authority
|
|
3706
3887
|
};
|
|
3707
3888
|
}
|
|
3708
3889
|
|
|
@@ -4150,12 +4331,12 @@ function coerceForSqlite(value) {
|
|
|
4150
4331
|
return String(value);
|
|
4151
4332
|
}
|
|
4152
4333
|
// src/mcp/server.ts
|
|
4153
|
-
var __dirname3 =
|
|
4334
|
+
var __dirname3 = dirname8(fileURLToPath3(import.meta.url));
|
|
4154
4335
|
var pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
4155
4336
|
try {
|
|
4156
4337
|
for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
|
|
4157
4338
|
const p = join11(__dirname3, rel);
|
|
4158
|
-
if (
|
|
4339
|
+
if (existsSync12(p)) {
|
|
4159
4340
|
pkg = JSON.parse(readFileSync8(p, "utf-8"));
|
|
4160
4341
|
break;
|
|
4161
4342
|
}
|
|
@@ -4165,7 +4346,7 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
|
4165
4346
|
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
4166
4347
|
const resolved = resolveHook2(name);
|
|
4167
4348
|
const hookScript = resolveScriptPath2(name);
|
|
4168
|
-
if (!resolved || !hookScript || !
|
|
4349
|
+
if (!resolved || !hookScript || !existsSync12(hookScript)) {
|
|
4169
4350
|
throw new Error(`Hook '${name}' not found`);
|
|
4170
4351
|
}
|
|
4171
4352
|
const content = readFileSync8(hookScript);
|
|
@@ -4374,7 +4555,7 @@ function createHooksServer() {
|
|
|
4374
4555
|
const settingsPath = getSettingsPath(scope);
|
|
4375
4556
|
const issues = [];
|
|
4376
4557
|
const healthy = [];
|
|
4377
|
-
const settingsExist =
|
|
4558
|
+
const settingsExist = existsSync12(settingsPath);
|
|
4378
4559
|
if (!settingsExist) {
|
|
4379
4560
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
4380
4561
|
}
|
|
@@ -4384,7 +4565,7 @@ function createHooksServer() {
|
|
|
4384
4565
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
4385
4566
|
let hookHealthy = true;
|
|
4386
4567
|
if (custom) {
|
|
4387
|
-
if (!
|
|
4568
|
+
if (!existsSync12(custom.scriptPath)) {
|
|
4388
4569
|
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
4389
4570
|
hookHealthy = false;
|
|
4390
4571
|
}
|
|
@@ -4394,7 +4575,7 @@ function createHooksServer() {
|
|
|
4394
4575
|
continue;
|
|
4395
4576
|
}
|
|
4396
4577
|
const hookDir = getHookPath(name);
|
|
4397
|
-
if (!
|
|
4578
|
+
if (!existsSync12(join11(hookDir, "src", "hook.ts"))) {
|
|
4398
4579
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
4399
4580
|
hookHealthy = false;
|
|
4400
4581
|
}
|
|
@@ -4440,7 +4621,7 @@ function createHooksServer() {
|
|
|
4440
4621
|
const hookPath = getHookPath(name);
|
|
4441
4622
|
const readmePath = join11(hookPath, "README.md");
|
|
4442
4623
|
let readme = "";
|
|
4443
|
-
if (
|
|
4624
|
+
if (existsSync12(readmePath)) {
|
|
4444
4625
|
readme = readFileSync8(readmePath, "utf-8");
|
|
4445
4626
|
}
|
|
4446
4627
|
if (verbose) {
|
|
@@ -4624,7 +4805,7 @@ function createHooksServer() {
|
|
|
4624
4805
|
const ctx = {
|
|
4625
4806
|
scope,
|
|
4626
4807
|
settings_path: settingsPath,
|
|
4627
|
-
settings_exists:
|
|
4808
|
+
settings_exists: existsSync12(settingsPath),
|
|
4628
4809
|
registered_hooks: hooks,
|
|
4629
4810
|
hook_count: hooks.length,
|
|
4630
4811
|
healthy,
|
|
@@ -4770,7 +4951,7 @@ function createHooksServer() {
|
|
|
4770
4951
|
const settingsPath = getSettingsPath(scope);
|
|
4771
4952
|
let settings = {};
|
|
4772
4953
|
try {
|
|
4773
|
-
if (
|
|
4954
|
+
if (existsSync12(settingsPath))
|
|
4774
4955
|
settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
|
|
4775
4956
|
} catch {}
|
|
4776
4957
|
if (!settings.hooks)
|
|
@@ -4780,8 +4961,8 @@ function createHooksServer() {
|
|
|
4780
4961
|
disabled.push(name);
|
|
4781
4962
|
settings.hooks.__disabled = disabled;
|
|
4782
4963
|
const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync7 } = await import("fs");
|
|
4783
|
-
const { dirname:
|
|
4784
|
-
mkdirSync7(
|
|
4964
|
+
const { dirname: dirname9 } = await import("path");
|
|
4965
|
+
mkdirSync7(dirname9(settingsPath), { recursive: true });
|
|
4785
4966
|
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
4786
4967
|
`);
|
|
4787
4968
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
|
|
@@ -4793,7 +4974,7 @@ function createHooksServer() {
|
|
|
4793
4974
|
const settingsPath = getSettingsPath(scope);
|
|
4794
4975
|
let settings = {};
|
|
4795
4976
|
try {
|
|
4796
|
-
if (
|
|
4977
|
+
if (existsSync12(settingsPath))
|
|
4797
4978
|
settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
|
|
4798
4979
|
} catch {}
|
|
4799
4980
|
if (settings.hooks?.__disabled) {
|