@hasna/hooks 0.9.3 → 0.9.5
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 +13 -2
- package/bin/hooks-mcp.js +103 -53
- package/bin/index.js +407 -128
- package/bin/serve.js +230 -38
- package/dist/index.d.ts +1 -1
- package/dist/index.js +344 -65
- 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/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js +275 -46
- package/package.json +3 -3
- package/scripts/validate-package.ts +70 -2
package/README.md
CHANGED
|
@@ -181,6 +181,17 @@ export HASNA_HOOKS_API_KEY_REF=<vault-key-name> # resolved through the vault a
|
|
|
181
181
|
secrets exec <vault-key-name> --as HASNA_HOOKS_API_KEY -- hooks serve
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
+
The owner-only `~/.hasna/hooks/config/credentials` file also accepts a reference:
|
|
185
|
+
|
|
186
|
+
```dotenv
|
|
187
|
+
HASNA_HOOKS_API_URL=https://registry.example.com
|
|
188
|
+
HASNA_HOOKS_API_KEY_REF=your-team/hooks/live/api-key
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Keep this file at mode `0400` or `0600`, and configure Secrets through its normal independent credential resolver. Hooks resolves the selected reference through the Secrets SDK for each registry request and each server publish-key check. References retain their existing precedence: a locked Keychain remains a refusal ahead of a file reference, and a missing reference never falls through to a stale literal key. A file cannot contain both a literal key and a reference.
|
|
192
|
+
|
|
193
|
+
`hooks list` displays catalog metadata and does not prove that the remote registry is reachable. Use `hooks sync --dry-run` to exercise registry reads without changing installed hooks. SDK consumers use `createHooksClient()`; callers working directly with transport metadata must await `resolveHooksRequestAuthority()` before sending a request. The server uses `resolveHooksServePublishKeyAsync()` for references.
|
|
194
|
+
|
|
184
195
|
**Cloudflare provisioning.** `hooks cf deploy` creates the D1 database and R2 bucket via the Cloudflare API, then prints the exact wrangler commands for the worker upload (the worker needs the workerd target, which only wrangler can bundle):
|
|
185
196
|
|
|
186
197
|
```bash
|
|
@@ -193,8 +204,8 @@ The worker (`src/cf/worker.ts`) implements the same API routes against D1 + R2,
|
|
|
193
204
|
|
|
194
205
|
## Storage
|
|
195
206
|
|
|
196
|
-
Hooks stores data
|
|
197
|
-
|
|
207
|
+
When local mode is explicitly selected, Hooks stores data in `~/.hasna/hooks/`
|
|
208
|
+
and uses SQLite for hook event history. The package owns its database schema and
|
|
198
209
|
migrations; it does not depend on the deprecated shared runtime or its CLI.
|
|
199
210
|
The repo includes its own PostgreSQL migration definitions for the optional
|
|
200
211
|
`hooks storage push|pull|sync` commands. Use the `hooks log` commands to inspect
|
package/bin/hooks-mcp.js
CHANGED
|
@@ -1418,14 +1418,14 @@ var init_redact = __esm(() => {
|
|
|
1418
1418
|
];
|
|
1419
1419
|
});
|
|
1420
1420
|
|
|
1421
|
-
//
|
|
1421
|
+
// ../contracts/dist/client/transport.js
|
|
1422
1422
|
import { isIP } from "net";
|
|
1423
1423
|
import { spawnSync } from "child_process";
|
|
1424
|
-
import { closeSync as closeSync2, fstatSync, openSync as openSync2, readFileSync as
|
|
1425
|
-
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
1426
|
-
import { createRequire } from "module";
|
|
1424
|
+
import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync22 } from "fs";
|
|
1425
|
+
import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
|
|
1427
1426
|
import { hostname as osHostname } from "os";
|
|
1428
|
-
import { isAbsolute as isAbsolute2, join as
|
|
1427
|
+
import { isAbsolute as isAbsolute2, join as join22 } from "path";
|
|
1428
|
+
import { createRequire } from "module";
|
|
1429
1429
|
function envToken(name) {
|
|
1430
1430
|
return name.toUpperCase().replace(/-/g, "_");
|
|
1431
1431
|
}
|
|
@@ -1455,14 +1455,14 @@ function hasnaHomeDir(env) {
|
|
|
1455
1455
|
if (override)
|
|
1456
1456
|
return override;
|
|
1457
1457
|
const home = homeDir(env);
|
|
1458
|
-
return home ?
|
|
1458
|
+
return home ? join22(home, HASNA_HOME_DIR) : null;
|
|
1459
1459
|
}
|
|
1460
1460
|
function appConfigDir(name, env) {
|
|
1461
1461
|
const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
|
|
1462
1462
|
if (configRoot)
|
|
1463
|
-
return
|
|
1463
|
+
return join22(configRoot, name);
|
|
1464
1464
|
const root = hasnaHomeDir(env);
|
|
1465
|
-
return root ?
|
|
1465
|
+
return root ? join22(root, name, CONFIG_SUBDIR) : null;
|
|
1466
1466
|
}
|
|
1467
1467
|
function credentialDiskSourceList(name, env, profile = null) {
|
|
1468
1468
|
if (!SAFE_APP_SLUG.test(name))
|
|
@@ -1471,7 +1471,7 @@ function credentialDiskSourceList(name, env, profile = null) {
|
|
|
1471
1471
|
if (!directory)
|
|
1472
1472
|
return [];
|
|
1473
1473
|
const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
|
|
1474
|
-
return [{ path:
|
|
1474
|
+
return [{ path: join22(directory, file), tier: "disk" }];
|
|
1475
1475
|
}
|
|
1476
1476
|
function credentialDiskSources(name, env) {
|
|
1477
1477
|
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
@@ -1525,7 +1525,7 @@ function readAppConfigFile(path) {
|
|
|
1525
1525
|
};
|
|
1526
1526
|
let fd = -1;
|
|
1527
1527
|
try {
|
|
1528
|
-
fd = openSync2(path,
|
|
1528
|
+
fd = openSync2(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
|
|
1529
1529
|
} catch (error) {
|
|
1530
1530
|
const code = error.code;
|
|
1531
1531
|
if (code === "ENOENT" || code === "ENOTDIR")
|
|
@@ -1535,7 +1535,7 @@ function readAppConfigFile(path) {
|
|
|
1535
1535
|
unsafe(`the path could not be opened (${code ?? "unknown error"})`);
|
|
1536
1536
|
}
|
|
1537
1537
|
try {
|
|
1538
|
-
const before =
|
|
1538
|
+
const before = fstatSync2(fd);
|
|
1539
1539
|
if (!before.isFile())
|
|
1540
1540
|
unsafe("the path is not a regular file");
|
|
1541
1541
|
if (!configFileModeAllowed(before.mode)) {
|
|
@@ -1546,8 +1546,8 @@ function readAppConfigFile(path) {
|
|
|
1546
1546
|
unsafe("the file is not owned by the current user");
|
|
1547
1547
|
if (before.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
1548
1548
|
unsafe("the file exceeds the size limit");
|
|
1549
|
-
const bytes =
|
|
1550
|
-
const after =
|
|
1549
|
+
const bytes = readFileSync22(fd);
|
|
1550
|
+
const after = fstatSync2(fd);
|
|
1551
1551
|
if (!configFileReadsCoherent(before, after)) {
|
|
1552
1552
|
unsafe("the file changed while being read");
|
|
1553
1553
|
}
|
|
@@ -1557,20 +1557,28 @@ function readAppConfigFile(path) {
|
|
|
1557
1557
|
closeSync2(fd);
|
|
1558
1558
|
}
|
|
1559
1559
|
}
|
|
1560
|
-
function readCredentialFile(path, apiKeyKeys) {
|
|
1560
|
+
function readCredentialFile(path, apiKeyKeys, pointerKey) {
|
|
1561
1561
|
const parsed = readAppConfigFile(path);
|
|
1562
1562
|
if (!parsed)
|
|
1563
1563
|
return null;
|
|
1564
|
-
for (const key of apiKeyKeys) {
|
|
1564
|
+
for (const key of [...apiKeyKeys, pointerKey]) {
|
|
1565
1565
|
if (parsed.unusable.has(key)) {
|
|
1566
1566
|
throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
|
|
1567
1567
|
}
|
|
1568
1568
|
}
|
|
1569
1569
|
const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
1570
|
+
const pointer = parsed.values.get(pointerKey)?.trim();
|
|
1571
|
+
if (pointer !== undefined) {
|
|
1572
|
+
if (!VAULT_POINTER_SHAPE.test(pointer))
|
|
1573
|
+
throw new CredentialFileUnsafeError(path, `${pointerKey} must name a vault item`);
|
|
1574
|
+
if (values.length)
|
|
1575
|
+
throw new CredentialFileUnsafeError(path, "a credential file cannot select both a literal key and a vault reference");
|
|
1576
|
+
return { apiKey: "", pointerVaultKey: pointer };
|
|
1577
|
+
}
|
|
1570
1578
|
if (new Set(values).size > 1) {
|
|
1571
1579
|
throw new CredentialFileUnsafeError(path, "credential aliases disagree");
|
|
1572
1580
|
}
|
|
1573
|
-
return values[0]
|
|
1581
|
+
return values[0] === undefined ? null : { apiKey: values[0] };
|
|
1574
1582
|
}
|
|
1575
1583
|
function appConfigDiskValue(name, env, keys) {
|
|
1576
1584
|
const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
|
|
@@ -1827,12 +1835,13 @@ function resolveCredential(name, env, options = {}) {
|
|
|
1827
1835
|
}
|
|
1828
1836
|
const paths = profileDiskSources(name, env, profile);
|
|
1829
1837
|
for (const path of paths) {
|
|
1830
|
-
const value = readCredentialFile(path, apiKeyKeys);
|
|
1838
|
+
const value = readCredentialFile(path, apiKeyKeys, pointerKeyName);
|
|
1831
1839
|
if (value) {
|
|
1832
|
-
|
|
1840
|
+
if (!value.pointerVaultKey)
|
|
1841
|
+
assertUsableCredential(name, path, value.apiKey);
|
|
1833
1842
|
return sealCredential({
|
|
1834
|
-
|
|
1835
|
-
tier: "profile",
|
|
1843
|
+
...value,
|
|
1844
|
+
tier: value.pointerVaultKey ? "pointer" : "profile",
|
|
1836
1845
|
source: path,
|
|
1837
1846
|
deliberate: true,
|
|
1838
1847
|
diskCandidates: paths,
|
|
@@ -1840,7 +1849,7 @@ function resolveCredential(name, env, options = {}) {
|
|
|
1840
1849
|
});
|
|
1841
1850
|
}
|
|
1842
1851
|
}
|
|
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);
|
|
1852
|
+
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
1853
|
}
|
|
1845
1854
|
const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
|
|
1846
1855
|
const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
|
|
@@ -1865,17 +1874,27 @@ function resolveCredential(name, env, options = {}) {
|
|
|
1865
1874
|
});
|
|
1866
1875
|
}
|
|
1867
1876
|
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
1868
|
-
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
1877
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys, pointerKeyName) })).filter((hit) => hit.value !== null);
|
|
1869
1878
|
if (diskHits.length > 0) {
|
|
1870
1879
|
const winner = diskHits[0];
|
|
1871
|
-
|
|
1880
|
+
if (winner.value.pointerVaultKey) {
|
|
1881
|
+
return sealCredential({
|
|
1882
|
+
...winner.value,
|
|
1883
|
+
tier: "pointer",
|
|
1884
|
+
source: winner.src.path,
|
|
1885
|
+
deliberate: false,
|
|
1886
|
+
diskCandidates: diskPaths,
|
|
1887
|
+
warning: null
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
assertUsableCredential(name, winner.src.path, winner.value.apiKey);
|
|
1872
1891
|
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] : []
|
|
1892
|
+
...diskHits.slice(1).filter((hit) => hit.value.apiKey !== winner.value.apiKey || hit.value.pointerVaultKey !== winner.value.pointerVaultKey).map((hit) => hit.src.path),
|
|
1893
|
+
...envHit && envHit.value !== winner.value.apiKey ? [envHit.key] : []
|
|
1875
1894
|
];
|
|
1876
1895
|
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
1896
|
return sealCredential({
|
|
1878
|
-
apiKey: winner.value,
|
|
1897
|
+
apiKey: winner.value.apiKey,
|
|
1879
1898
|
tier: winner.src.tier,
|
|
1880
1899
|
source: winner.src.path,
|
|
1881
1900
|
deliberate: false,
|
|
@@ -2098,8 +2117,9 @@ function credentialDiskSourcesForMessage(name, env) {
|
|
|
2098
2117
|
const paths = credentialDiskSources(name, env);
|
|
2099
2118
|
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
2100
2119
|
}
|
|
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,
|
|
2120
|
+
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
2121
|
var init_transport = __esm(() => {
|
|
2122
|
+
MAX_PACKAGE_BYTES = 1024 * 1024;
|
|
2103
2123
|
CredentialResolutionError = class CredentialResolutionError extends Error {
|
|
2104
2124
|
appName;
|
|
2105
2125
|
attempted;
|
|
@@ -2127,7 +2147,6 @@ var init_transport = __esm(() => {
|
|
|
2127
2147
|
INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
2128
2148
|
CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
2129
2149
|
AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
2130
|
-
SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
2131
2150
|
requireSecretsSdk = createRequire(import.meta.url);
|
|
2132
2151
|
ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
2133
2152
|
DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
@@ -2385,7 +2404,7 @@ __export(exports_run, {
|
|
|
2385
2404
|
});
|
|
2386
2405
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
2387
2406
|
import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
2388
|
-
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute4, join as
|
|
2407
|
+
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute4, join as join9 } from "path";
|
|
2389
2408
|
function extensionOf(scriptPath) {
|
|
2390
2409
|
const base = basename2(scriptPath);
|
|
2391
2410
|
const dot = base.lastIndexOf(".");
|
|
@@ -2456,7 +2475,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
2456
2475
|
async function executeVerifiedScript(options) {
|
|
2457
2476
|
const scriptDir = dirname6(options.scriptPath);
|
|
2458
2477
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
2459
|
-
const tempPath =
|
|
2478
|
+
const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
2460
2479
|
let proc = null;
|
|
2461
2480
|
let timedOut = false;
|
|
2462
2481
|
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
@@ -2664,8 +2683,8 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
2664
2683
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2665
2684
|
import { z as z2 } from "zod";
|
|
2666
2685
|
init_registry();
|
|
2667
|
-
import { existsSync as existsSync11, readFileSync as
|
|
2668
|
-
import { join as
|
|
2686
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
|
|
2687
|
+
import { join as join10, dirname as dirname7 } from "path";
|
|
2669
2688
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2670
2689
|
|
|
2671
2690
|
// src/lib/installer.ts
|
|
@@ -2708,7 +2727,7 @@ function mementosCommandSuffix(value) {
|
|
|
2708
2727
|
return value.enabled ? ` --enable-mementos${value.secretRef ? ` --openrouter-secret-ref ${value.secretRef}` : ""}` : " --disable-mementos";
|
|
2709
2728
|
}
|
|
2710
2729
|
function retainedMementosRegistration(commands) {
|
|
2711
|
-
const values = commands.filter((command) => /^hooks run mementos-context(?:\s|$)/.test(command)).map((command) => {
|
|
2730
|
+
const values = commands.map((command) => command.trim()).filter((command) => /^hooks run mementos-context(?:\s|$)/.test(command)).map((command) => {
|
|
2712
2731
|
const args = command.trim().split(/\s+/).slice(3);
|
|
2713
2732
|
const flags = {};
|
|
2714
2733
|
const seen = new Set;
|
|
@@ -3436,8 +3455,36 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
3436
3455
|
if (!settings.hooks)
|
|
3437
3456
|
settings.hooks = {};
|
|
3438
3457
|
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
|
-
removeHookFromAllEvents(settings, name);
|
|
3440
3458
|
const hookCommand = (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
|
|
3459
|
+
if (target === "codex") {
|
|
3460
|
+
const positions = [];
|
|
3461
|
+
for (const [event, groups] of Object.entries(settings.hooks)) {
|
|
3462
|
+
for (const entry of groups) {
|
|
3463
|
+
for (let index = 0;index < entry.hooks.length; index++) {
|
|
3464
|
+
const command = entry.hooks[index]?.command;
|
|
3465
|
+
const match = typeof command === "string" && command.trim().match(/^(?:hooks run ([\w-]+)(?:\s|$)|hook-([\w-]+)$)/);
|
|
3466
|
+
if (match && (match[1] ?? match[2]) === name) {
|
|
3467
|
+
if (registrationName(command) !== name)
|
|
3468
|
+
throw new Error("Unrecognized Codex registration; reconcile it before updating this hook.");
|
|
3469
|
+
positions.push({ event, entry, index });
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
if (positions.length) {
|
|
3475
|
+
if (positions.length !== uniqueEventKeys.length || uniqueEventKeys.some((event) => positions.filter((position) => position.event === event).length !== 1) || positions.some((position) => (position.entry.matcher ?? "") !== (meta.matcher ?? ""))) {
|
|
3476
|
+
throw new Error("Codex hook events, matchers or duplicate registrations require explicit reconciliation; no settings were changed.");
|
|
3477
|
+
}
|
|
3478
|
+
for (const { entry, index } of positions) {
|
|
3479
|
+
entry.hooks[index] = { ...entry.hooks[index], type: "command", command: hookCommand };
|
|
3480
|
+
if (typeof meta.timeoutSeconds === "number" && meta.timeoutSeconds > 0)
|
|
3481
|
+
entry.hooks[index].timeout = meta.timeoutSeconds;
|
|
3482
|
+
}
|
|
3483
|
+
writeSettings(settings, scope, target);
|
|
3484
|
+
return;
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
removeHookFromAllEvents(settings, name);
|
|
3441
3488
|
for (const eventKey of uniqueEventKeys) {
|
|
3442
3489
|
if (!settings.hooks[eventKey])
|
|
3443
3490
|
settings.hooks[eventKey] = [];
|
|
@@ -3642,6 +3689,7 @@ function rethrowHooksAuthorityFailure(error) {
|
|
|
3642
3689
|
}
|
|
3643
3690
|
throw new Error(`REMOTE_API_URL_INVALID: ${message} local mode fallback is disabled`, { cause: error });
|
|
3644
3691
|
}
|
|
3692
|
+
var authorityBindings = new WeakMap;
|
|
3645
3693
|
function resolveHooksTransport(env = process.env, options = {}) {
|
|
3646
3694
|
if (selectsHooksLocalStore(env)) {
|
|
3647
3695
|
announceLocal(options.notice, "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority");
|
|
@@ -3655,7 +3703,7 @@ function resolveHooksTransport(env = process.env, options = {}) {
|
|
|
3655
3703
|
try {
|
|
3656
3704
|
credential = resolveCredential("hooks", resolverEnv, credentialOptions);
|
|
3657
3705
|
resolution = resolveClientTransport("hooks", resolverEnv, {
|
|
3658
|
-
credentials: credential ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
|
|
3706
|
+
credentials: credential && credential.tier !== "pointer" ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
|
|
3659
3707
|
});
|
|
3660
3708
|
} catch (error) {
|
|
3661
3709
|
rethrowHooksAuthorityFailure(error);
|
|
@@ -3663,18 +3711,20 @@ function resolveHooksTransport(env = process.env, options = {}) {
|
|
|
3663
3711
|
if (!credential) {
|
|
3664
3712
|
throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved; " + "local mode fallback is disabled");
|
|
3665
3713
|
}
|
|
3714
|
+
const authority = {
|
|
3715
|
+
origin: hooksRegistryOrigin(resolution.baseUrl),
|
|
3716
|
+
apiKey: credential.apiKey,
|
|
3717
|
+
apiUrlSource: resolution.apiUrlSource,
|
|
3718
|
+
apiKeySource: credential.source,
|
|
3719
|
+
apiKeyTier: credential.tier,
|
|
3720
|
+
v1BaseUrl: resolution.baseUrl,
|
|
3721
|
+
warning: resolution.warning
|
|
3722
|
+
};
|
|
3723
|
+
authorityBindings.set(authority, { env, options, credential });
|
|
3666
3724
|
return {
|
|
3667
3725
|
mode: "remote",
|
|
3668
3726
|
source: `${credential.source}+${resolution.apiUrlSource ?? "default"}`,
|
|
3669
|
-
authority
|
|
3670
|
-
origin: hooksRegistryOrigin(resolution.baseUrl),
|
|
3671
|
-
apiKey: credential.apiKey,
|
|
3672
|
-
apiUrlSource: resolution.apiUrlSource,
|
|
3673
|
-
apiKeySource: credential.source,
|
|
3674
|
-
apiKeyTier: credential.tier,
|
|
3675
|
-
v1BaseUrl: resolution.baseUrl,
|
|
3676
|
-
warning: resolution.warning
|
|
3677
|
-
}
|
|
3727
|
+
authority
|
|
3678
3728
|
};
|
|
3679
3729
|
}
|
|
3680
3730
|
|
|
@@ -4126,9 +4176,9 @@ var __dirname3 = dirname7(fileURLToPath3(import.meta.url));
|
|
|
4126
4176
|
var pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
4127
4177
|
try {
|
|
4128
4178
|
for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
|
|
4129
|
-
const p =
|
|
4179
|
+
const p = join10(__dirname3, rel);
|
|
4130
4180
|
if (existsSync11(p)) {
|
|
4131
|
-
pkg = JSON.parse(
|
|
4181
|
+
pkg = JSON.parse(readFileSync7(p, "utf-8"));
|
|
4132
4182
|
break;
|
|
4133
4183
|
}
|
|
4134
4184
|
}
|
|
@@ -4140,7 +4190,7 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
|
4140
4190
|
if (!resolved || !hookScript || !existsSync11(hookScript)) {
|
|
4141
4191
|
throw new Error(`Hook '${name}' not found`);
|
|
4142
4192
|
}
|
|
4143
|
-
const content =
|
|
4193
|
+
const content = readFileSync7(hookScript);
|
|
4144
4194
|
const sha = sha256Of(content);
|
|
4145
4195
|
const check = checkScriptHash(name, sha);
|
|
4146
4196
|
if (!check.ok) {
|
|
@@ -4366,14 +4416,14 @@ function createHooksServer() {
|
|
|
4366
4416
|
continue;
|
|
4367
4417
|
}
|
|
4368
4418
|
const hookDir = getHookPath(name);
|
|
4369
|
-
if (!existsSync11(
|
|
4419
|
+
if (!existsSync11(join10(hookDir, "src", "hook.ts"))) {
|
|
4370
4420
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
4371
4421
|
hookHealthy = false;
|
|
4372
4422
|
}
|
|
4373
4423
|
}
|
|
4374
4424
|
if (meta && settingsExist) {
|
|
4375
4425
|
try {
|
|
4376
|
-
const settings = JSON.parse(
|
|
4426
|
+
const settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
|
|
4377
4427
|
if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
|
|
4378
4428
|
const eventName = meta.event.split(":")[0];
|
|
4379
4429
|
issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
|
|
@@ -4410,10 +4460,10 @@ function createHooksServer() {
|
|
|
4410
4460
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
4411
4461
|
}
|
|
4412
4462
|
const hookPath = getHookPath(name);
|
|
4413
|
-
const readmePath =
|
|
4463
|
+
const readmePath = join10(hookPath, "README.md");
|
|
4414
4464
|
let readme = "";
|
|
4415
4465
|
if (existsSync11(readmePath)) {
|
|
4416
|
-
readme =
|
|
4466
|
+
readme = readFileSync7(readmePath, "utf-8");
|
|
4417
4467
|
}
|
|
4418
4468
|
if (verbose) {
|
|
4419
4469
|
return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
|
|
@@ -4743,7 +4793,7 @@ function createHooksServer() {
|
|
|
4743
4793
|
let settings = {};
|
|
4744
4794
|
try {
|
|
4745
4795
|
if (existsSync11(settingsPath))
|
|
4746
|
-
settings = JSON.parse(
|
|
4796
|
+
settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
|
|
4747
4797
|
} catch {}
|
|
4748
4798
|
if (!settings.hooks)
|
|
4749
4799
|
settings.hooks = {};
|
|
@@ -4766,7 +4816,7 @@ function createHooksServer() {
|
|
|
4766
4816
|
let settings = {};
|
|
4767
4817
|
try {
|
|
4768
4818
|
if (existsSync11(settingsPath))
|
|
4769
|
-
settings = JSON.parse(
|
|
4819
|
+
settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
|
|
4770
4820
|
} catch {}
|
|
4771
4821
|
if (settings.hooks?.__disabled) {
|
|
4772
4822
|
settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
|