@hasna/hooks 0.9.0 → 0.9.1
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 +33 -5
- package/bin/hooks-mcp.js +4862 -0
- package/bin/index.js +283 -109
- package/bin/serve.js +23 -12
- package/dist/db/index.d.ts +8 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2870 -2757
- package/dist/lib/db-writer.d.ts +30 -1
- package/dist/lib/local-opt-in.d.ts +26 -0
- package/dist/lib/sync.d.ts +17 -1
- package/dist/sdk/index.d.ts +103 -0
- package/dist/sdk/index.js +1050 -0
- package/dist/storage.js +15 -1
- package/hooks/hook-trash-guard/README.md +20 -5
- package/hooks/hook-trash-guard/package.json +1 -1
- package/hooks/hook-trash-guard/src/hook.ts +45 -17
- package/package.json +7 -2
- package/scripts/validate-package.ts +31 -4
package/bin/index.js
CHANGED
|
@@ -2087,10 +2087,10 @@ var init_registry = __esm(() => {
|
|
|
2087
2087
|
name: "trash-guard",
|
|
2088
2088
|
displayName: "Trash Guard",
|
|
2089
2089
|
description: "Rewrites rm issued through Bash into `trash guard`, so the delete lands in a recoverable trash store; refuses the delete when there is nothing to redirect to (trash absent, a delete verb it cannot rewrite, or the protected class: /, ~, ~/.hasna, ~/.ssh, ~/.aws)",
|
|
2090
|
-
version: "0.1.
|
|
2090
|
+
version: "0.1.1",
|
|
2091
2091
|
category: "Git Safety",
|
|
2092
2092
|
event: "PreToolUse",
|
|
2093
|
-
matcher: "Bash",
|
|
2093
|
+
matcher: "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$",
|
|
2094
2094
|
tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
|
|
2095
2095
|
rewritesInput: true,
|
|
2096
2096
|
timeoutSeconds: 5
|
|
@@ -4919,14 +4919,30 @@ function runRetention(db, days) {
|
|
|
4919
4919
|
// src/db/index.ts
|
|
4920
4920
|
var exports_db = {};
|
|
4921
4921
|
__export(exports_db, {
|
|
4922
|
+
refuseLocalStore: () => refuseLocalStore,
|
|
4923
|
+
localStoreRefusalMessage: () => localStoreRefusalMessage,
|
|
4924
|
+
isLocalStoreRefused: () => isLocalStoreRefused,
|
|
4922
4925
|
getDbPath: () => getDbPath,
|
|
4923
4926
|
getDb: () => getDb,
|
|
4924
4927
|
createTestDb: () => createTestDb,
|
|
4925
|
-
closeDb: () => closeDb
|
|
4928
|
+
closeDb: () => closeDb,
|
|
4929
|
+
allowLocalStore: () => allowLocalStore
|
|
4926
4930
|
});
|
|
4927
4931
|
import { Database } from "bun:sqlite";
|
|
4928
4932
|
import { existsSync as existsSync8, mkdirSync as mkdirSync2, cpSync } from "fs";
|
|
4929
4933
|
import { join as join9 } from "path";
|
|
4934
|
+
function refuseLocalStore(message) {
|
|
4935
|
+
localStoreRefusal = message;
|
|
4936
|
+
}
|
|
4937
|
+
function allowLocalStore() {
|
|
4938
|
+
localStoreRefusal = null;
|
|
4939
|
+
}
|
|
4940
|
+
function isLocalStoreRefused() {
|
|
4941
|
+
return localStoreRefusal !== null;
|
|
4942
|
+
}
|
|
4943
|
+
function localStoreRefusalMessage() {
|
|
4944
|
+
return localStoreRefusal;
|
|
4945
|
+
}
|
|
4930
4946
|
function resolveDataDir() {
|
|
4931
4947
|
const effective = getEffectiveDataRoot();
|
|
4932
4948
|
const oldDir = join9(getHomeDir(), ".hooks");
|
|
@@ -4950,6 +4966,8 @@ function ensureDir(dbPath) {
|
|
|
4950
4966
|
}
|
|
4951
4967
|
}
|
|
4952
4968
|
function getDb() {
|
|
4969
|
+
if (localStoreRefusal !== null)
|
|
4970
|
+
throw new Error(localStoreRefusal);
|
|
4953
4971
|
if (instance)
|
|
4954
4972
|
return instance;
|
|
4955
4973
|
const dbPath = getDbPath();
|
|
@@ -4987,7 +5005,7 @@ function createTestDb() {
|
|
|
4987
5005
|
db.exec("PRAGMA foreign_keys=ON");
|
|
4988
5006
|
return db;
|
|
4989
5007
|
}
|
|
4990
|
-
var instance = null;
|
|
5008
|
+
var instance = null, localStoreRefusal = null;
|
|
4991
5009
|
var init_db = __esm(() => {
|
|
4992
5010
|
init_app_home();
|
|
4993
5011
|
init_migrations();
|
|
@@ -5026,6 +5044,9 @@ async function sha256File(path) {
|
|
|
5026
5044
|
const buf = await readFile2(path);
|
|
5027
5045
|
return sha256Of(buf);
|
|
5028
5046
|
}
|
|
5047
|
+
function storeDb() {
|
|
5048
|
+
return isLocalStoreRefused() ? null : getDb();
|
|
5049
|
+
}
|
|
5029
5050
|
function getHookRecord(db, name) {
|
|
5030
5051
|
return db.query("SELECT * FROM hooks WHERE name = ?").get(name);
|
|
5031
5052
|
}
|
|
@@ -5126,26 +5147,28 @@ function removePinnedHook(name) {
|
|
|
5126
5147
|
}
|
|
5127
5148
|
function pinInstalledHook(name, version, sha256, source, sourceRef) {
|
|
5128
5149
|
const now2 = new Date().toISOString();
|
|
5129
|
-
const db =
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5150
|
+
const db = storeDb();
|
|
5151
|
+
if (db) {
|
|
5152
|
+
upsertHookRecord(db, {
|
|
5153
|
+
name,
|
|
5154
|
+
version,
|
|
5155
|
+
sha256,
|
|
5156
|
+
source_type: source,
|
|
5157
|
+
source_ref: sourceRef ?? null,
|
|
5158
|
+
last_verified_at: now2
|
|
5159
|
+
});
|
|
5160
|
+
}
|
|
5138
5161
|
setPinnedHook(name, { version, sha256, source });
|
|
5139
5162
|
}
|
|
5140
5163
|
function removeHookFromStore(name) {
|
|
5141
5164
|
const removedPin = removePinnedHook(name);
|
|
5142
|
-
const db =
|
|
5143
|
-
const removedRecord = removeHookRecord(db, name);
|
|
5165
|
+
const db = storeDb();
|
|
5166
|
+
const removedRecord = db ? removeHookRecord(db, name) : false;
|
|
5144
5167
|
return { removedPin, removedRecord };
|
|
5145
5168
|
}
|
|
5146
5169
|
function checkScriptHash(name, actual) {
|
|
5147
|
-
const db =
|
|
5148
|
-
const record = getHookRecord(db, name);
|
|
5170
|
+
const db = storeDb();
|
|
5171
|
+
const record = db ? getHookRecord(db, name) : null;
|
|
5149
5172
|
const pin = getPinnedHook(name);
|
|
5150
5173
|
const expected = record?.sha256 ?? pin?.sha256 ?? null;
|
|
5151
5174
|
if (expected !== null && expected !== actual) {
|
|
@@ -5155,24 +5178,28 @@ function checkScriptHash(name, actual) {
|
|
|
5155
5178
|
const now3 = new Date().toISOString();
|
|
5156
5179
|
const version = pin?.version ?? record?.version ?? "0.0.0";
|
|
5157
5180
|
const source = pin?.source ?? record?.source_type ?? "local";
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5181
|
+
if (db) {
|
|
5182
|
+
upsertHookRecord(db, {
|
|
5183
|
+
name,
|
|
5184
|
+
version,
|
|
5185
|
+
sha256: actual,
|
|
5186
|
+
source_type: source,
|
|
5187
|
+
last_verified_at: now3
|
|
5188
|
+
});
|
|
5189
|
+
}
|
|
5165
5190
|
setPinnedHook(name, { version, sha256: actual, source });
|
|
5166
5191
|
return { ok: true, pinned: false, expected: null, actual, name };
|
|
5167
5192
|
}
|
|
5168
5193
|
const now2 = new Date().toISOString();
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5194
|
+
if (db) {
|
|
5195
|
+
upsertHookRecord(db, {
|
|
5196
|
+
name,
|
|
5197
|
+
version: record?.version ?? pin?.version ?? "0.0.0",
|
|
5198
|
+
sha256: expected,
|
|
5199
|
+
source_type: record?.source_type ?? pin?.source ?? "local",
|
|
5200
|
+
last_verified_at: now2
|
|
5201
|
+
});
|
|
5202
|
+
}
|
|
5176
5203
|
return { ok: true, pinned: true, expected, actual, name };
|
|
5177
5204
|
}
|
|
5178
5205
|
async function verifyScriptHash(name, scriptPath) {
|
|
@@ -5180,14 +5207,16 @@ async function verifyScriptHash(name, scriptPath) {
|
|
|
5180
5207
|
}
|
|
5181
5208
|
function retrustHook(name, scriptPath, version, source) {
|
|
5182
5209
|
const actual = sha256Of(readFileSync3(scriptPath));
|
|
5183
|
-
const db =
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5210
|
+
const db = storeDb();
|
|
5211
|
+
if (db) {
|
|
5212
|
+
upsertHookRecord(db, {
|
|
5213
|
+
name,
|
|
5214
|
+
version,
|
|
5215
|
+
sha256: actual,
|
|
5216
|
+
source_type: source,
|
|
5217
|
+
last_verified_at: new Date().toISOString()
|
|
5218
|
+
});
|
|
5219
|
+
}
|
|
5191
5220
|
setPinnedHook(name, { version, sha256: actual, source });
|
|
5192
5221
|
return { ok: true, pinned: true, expected: actual, actual, name };
|
|
5193
5222
|
}
|
|
@@ -6091,7 +6120,7 @@ var init_profiles = __esm(() => {
|
|
|
6091
6120
|
init_app_home();
|
|
6092
6121
|
});
|
|
6093
6122
|
|
|
6094
|
-
//
|
|
6123
|
+
// ../../node_modules/.bun/@hasna+contracts@1.0.2+e0db24ae03127387/node_modules/@hasna/contracts/dist/client/transport.js
|
|
6095
6124
|
import { isIP as isIP2 } from "net";
|
|
6096
6125
|
import { spawnSync } from "child_process";
|
|
6097
6126
|
import { closeSync, fstatSync, openSync, readFileSync as readFileSync6 } from "fs";
|
|
@@ -6771,7 +6800,7 @@ function credentialDiskSourcesForMessage(name, env) {
|
|
|
6771
6800
|
const paths = credentialDiskSources(name, env);
|
|
6772
6801
|
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
6773
6802
|
}
|
|
6774
|
-
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_BYTES2, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, AMBIENT_ENVIRONMENT2,
|
|
6803
|
+
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_BYTES2, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, AMBIENT_ENVIRONMENT2, SECRETS_PACKAGE_SPECIFIER, requireSecretsSdk2, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, IDEMPOTENT_METHODS2, AUTHORITY_OVERRIDE_HEADERS2;
|
|
6775
6804
|
var init_transport = __esm(() => {
|
|
6776
6805
|
CredentialResolutionError = class CredentialResolutionError extends Error {
|
|
6777
6806
|
appName;
|
|
@@ -6800,7 +6829,7 @@ var init_transport = __esm(() => {
|
|
|
6800
6829
|
INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
|
|
6801
6830
|
CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
|
|
6802
6831
|
AMBIENT_ENVIRONMENT2 = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
6803
|
-
|
|
6832
|
+
SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
6804
6833
|
requireSecretsSdk2 = createRequire2(import.meta.url);
|
|
6805
6834
|
ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
6806
6835
|
DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
@@ -6844,6 +6873,15 @@ function hasHooksEnvAuthorityIntent(env = process.env) {
|
|
|
6844
6873
|
function selectsHooksLocalStore(env = process.env) {
|
|
6845
6874
|
return !hasHooksEnvAuthorityIntent(env) && isHooksLocalOptIn(env);
|
|
6846
6875
|
}
|
|
6876
|
+
function hooksFailClosedLine(detail) {
|
|
6877
|
+
return `REMOTE_API_CONFIG_MISSING: hooks: no registry credential resolved from ${HOOKS_CREDENTIAL_TIERS} ` + `(with HASNA_HOOKS_API_URL or the fleet gateway default), and local mode is not enabled ` + `(${HOOKS_LOCAL_OPT_IN_HINT}) \u2014 refusing to silently fall back to local storage.` + (detail ? ` ${detail}` : "");
|
|
6878
|
+
}
|
|
6879
|
+
function hooksHostedRouteLocalStoreRefusal(authority) {
|
|
6880
|
+
return "REMOTE_COMMAND_UNSUPPORTED: this command is local-only \u2014 it reads the on-box SQLite store " + `(hooks.db), which the hosted hooks registry authority (${authority}) does not serve; local SQLite ` + `is opt-in only (${HOOKS_LOCAL_OPT_IN_HINT}) and is disabled by default \u2014 refusing to open a local ` + "store on the hosted route.";
|
|
6881
|
+
}
|
|
6882
|
+
function hooksEventSinkRefusal() {
|
|
6883
|
+
return "REMOTE_COMMAND_UNSUPPORTED: hook event not recorded \u2014 the hook event log is local-only (on-box " + "SQLite hooks.db) and the hosted hooks registry has no event route; local SQLite is opt-in only " + `(${HOOKS_LOCAL_OPT_IN_HINT}) and is disabled by default. Nothing was written.`;
|
|
6884
|
+
}
|
|
6847
6885
|
function hooksResolverEnv(env) {
|
|
6848
6886
|
const blanks = hooksAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
6849
6887
|
if (blanks.length === 0)
|
|
@@ -6868,7 +6906,7 @@ function hooksResolverInputs(env, credentials = {}) {
|
|
|
6868
6906
|
}
|
|
6869
6907
|
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
6870
6908
|
}
|
|
6871
|
-
var HOOKS_LOCAL_OPT_IN_ENV_KEYS, CONTRACTS_AMBIENT_ENVIRONMENT;
|
|
6909
|
+
var HOOKS_LOCAL_OPT_IN_ENV_KEYS, HOOKS_CREDENTIAL_TIERS = "the Keychain item hasna.credentials.hooks.api-key, ~/.hasna/hooks/config/credentials, or HASNA_HOOKS_API_KEY", HOOKS_LOCAL_OPT_IN_HINT = "HASNA_HOOKS_LOCAL=1 (alias HOOKS_LOCAL=1)", CONTRACTS_AMBIENT_ENVIRONMENT;
|
|
6872
6910
|
var init_local_opt_in = __esm(() => {
|
|
6873
6911
|
init_transport();
|
|
6874
6912
|
HOOKS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_HOOKS_LOCAL", "HOOKS_LOCAL"];
|
|
@@ -6971,14 +7009,35 @@ var init_transport2 = __esm(() => {
|
|
|
6971
7009
|
var exports_db_writer = {};
|
|
6972
7010
|
__export(exports_db_writer, {
|
|
6973
7011
|
writeHookEvent: () => writeHookEvent,
|
|
7012
|
+
resolveHookEventSink: () => resolveHookEventSink,
|
|
6974
7013
|
resolveEventType: () => resolveEventType,
|
|
6975
7014
|
recordHookRun: () => recordHookRun,
|
|
6976
|
-
normalizeEventType: () => normalizeEventType
|
|
7015
|
+
normalizeEventType: () => normalizeEventType,
|
|
7016
|
+
__resetHookEventSinkNotice: () => __resetHookEventSinkNotice
|
|
6977
7017
|
});
|
|
7018
|
+
function resolveHookEventSink(env = process.env) {
|
|
7019
|
+
if (isLocalStoreRefused())
|
|
7020
|
+
return { kind: "refused", reason: localStoreRefusalMessage() ?? hooksEventSinkRefusal() };
|
|
7021
|
+
if (selectsHooksLocalStore(env))
|
|
7022
|
+
return { kind: "local" };
|
|
7023
|
+
return { kind: "refused", reason: hooksEventSinkRefusal() };
|
|
7024
|
+
}
|
|
7025
|
+
function __resetHookEventSinkNotice() {
|
|
7026
|
+
sinkRefusalPrinted = false;
|
|
7027
|
+
}
|
|
6978
7028
|
function nanoid2() {
|
|
6979
7029
|
return crypto.randomUUID().replace(/-/g, "").slice(0, 21);
|
|
6980
7030
|
}
|
|
6981
7031
|
function writeHookEvent(event) {
|
|
7032
|
+
const sink = resolveHookEventSink();
|
|
7033
|
+
if (sink.kind !== "local") {
|
|
7034
|
+
if (!sinkRefusalPrinted) {
|
|
7035
|
+
sinkRefusalPrinted = true;
|
|
7036
|
+
process.stderr.write(`[hooks db-writer] ${sink.reason}
|
|
7037
|
+
`);
|
|
7038
|
+
}
|
|
7039
|
+
return;
|
|
7040
|
+
}
|
|
6982
7041
|
try {
|
|
6983
7042
|
const db = getDb();
|
|
6984
7043
|
const id = nanoid2();
|
|
@@ -7047,9 +7106,10 @@ function recordHookRun(record) {
|
|
|
7047
7106
|
metadata: JSON.stringify(metadata)
|
|
7048
7107
|
});
|
|
7049
7108
|
}
|
|
7050
|
-
var ALLOWED_EVENT_TYPES;
|
|
7109
|
+
var sinkRefusalPrinted = false, ALLOWED_EVENT_TYPES;
|
|
7051
7110
|
var init_db_writer = __esm(() => {
|
|
7052
7111
|
init_db();
|
|
7112
|
+
init_local_opt_in();
|
|
7053
7113
|
init_redact();
|
|
7054
7114
|
ALLOWED_EVENT_TYPES = new Set([
|
|
7055
7115
|
"PreToolUse",
|
|
@@ -7065,7 +7125,8 @@ var init_db_writer = __esm(() => {
|
|
|
7065
7125
|
|
|
7066
7126
|
// src/lib/hook-env.ts
|
|
7067
7127
|
import { dirname as dirname4, isAbsolute as isAbsolute3 } from "path";
|
|
7068
|
-
import { existsSync as existsSync12, statSync } from "fs";
|
|
7128
|
+
import { existsSync as existsSync12, realpathSync as realpathSync2, statSync } from "fs";
|
|
7129
|
+
import { tmpdir } from "os";
|
|
7069
7130
|
function isDeniedEnvName(name) {
|
|
7070
7131
|
const upper = name.toUpperCase();
|
|
7071
7132
|
if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
|
|
@@ -7091,6 +7152,19 @@ function isUnsafePathEntry(entry, home) {
|
|
|
7091
7152
|
return true;
|
|
7092
7153
|
if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
|
|
7093
7154
|
return true;
|
|
7155
|
+
const canonical = (path) => {
|
|
7156
|
+
try {
|
|
7157
|
+
return realpathSync2(path);
|
|
7158
|
+
} catch {
|
|
7159
|
+
return path;
|
|
7160
|
+
}
|
|
7161
|
+
};
|
|
7162
|
+
const resolved = canonical(entry);
|
|
7163
|
+
for (const root of [home, tmpdir(), "/tmp", "/var/tmp"].filter((value) => Boolean(value))) {
|
|
7164
|
+
const blocked = canonical(root);
|
|
7165
|
+
if (resolved === blocked || resolved.startsWith(blocked + "/"))
|
|
7166
|
+
return true;
|
|
7167
|
+
}
|
|
7094
7168
|
try {
|
|
7095
7169
|
if ((statSync(entry).mode & 2) !== 0)
|
|
7096
7170
|
return true;
|
|
@@ -7210,6 +7284,7 @@ var init_hook_env = __esm(() => {
|
|
|
7210
7284
|
DENY_CONTAINS = ["DATABASE_URL"];
|
|
7211
7285
|
PATH_BASELINE = ["/usr/bin", "/bin", "/usr/local/bin", "/sbin", "/usr/sbin"];
|
|
7212
7286
|
CONFIG_PROJECTIONS = [
|
|
7287
|
+
["HASNA_HOOKS_LOCAL", "HOOKS_LOCAL"],
|
|
7213
7288
|
["HASNA_HOOKS_DATA_DIR", "HOOKS_DATA_DIR"],
|
|
7214
7289
|
["HASNA_HOOKS_DB_PATH", "HOOKS_DB_PATH"],
|
|
7215
7290
|
["HASNA_HOOKS_LOCK_PATH", "HOOKS_LOCK_PATH"],
|
|
@@ -7389,7 +7464,7 @@ __export(exports_custom_install, {
|
|
|
7389
7464
|
});
|
|
7390
7465
|
import { existsSync as existsSync13, mkdirSync as mkdirSync6, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
|
|
7391
7466
|
import { join as join14 } from "path";
|
|
7392
|
-
import { tmpdir } from "os";
|
|
7467
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
7393
7468
|
function manifestAt(dir) {
|
|
7394
7469
|
const path = join14(dir, "manifest.json");
|
|
7395
7470
|
if (!existsSync13(path))
|
|
@@ -7414,7 +7489,7 @@ function writeFromManifest(manifest, srcDir, source, kind) {
|
|
|
7414
7489
|
return { name, version: manifest.version, kind, source, dir, scriptPath };
|
|
7415
7490
|
}
|
|
7416
7491
|
async function cloneToTemp(gitUrl) {
|
|
7417
|
-
const tmp = mkdtempSync(join14(
|
|
7492
|
+
const tmp = mkdtempSync(join14(tmpdir2(), "hooks-install-"));
|
|
7418
7493
|
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join14(tmp, "repo")], {
|
|
7419
7494
|
stdout: "pipe",
|
|
7420
7495
|
stderr: "pipe"
|
|
@@ -7501,6 +7576,7 @@ __export(exports_sync, {
|
|
|
7501
7576
|
syncHooks: () => syncHooks,
|
|
7502
7577
|
stageSyncArtifacts: () => stageSyncArtifacts,
|
|
7503
7578
|
planSync: () => planSync,
|
|
7579
|
+
installPinnedFromBundled: () => installPinnedFromBundled,
|
|
7504
7580
|
fetchPinnedHook: () => fetchPinnedHook,
|
|
7505
7581
|
commitSyncArtifacts: () => commitSyncArtifacts
|
|
7506
7582
|
});
|
|
@@ -7664,7 +7740,7 @@ async function stageSyncArtifacts(apiUrl, apiKey, names, remoteLock) {
|
|
|
7664
7740
|
return staged;
|
|
7665
7741
|
}
|
|
7666
7742
|
async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
|
|
7667
|
-
const db = getDb();
|
|
7743
|
+
const db = isLocalStoreRefused() ? null : getDb();
|
|
7668
7744
|
for (const item of staged) {
|
|
7669
7745
|
const entry = remoteLock.hooks[item.name];
|
|
7670
7746
|
if (!entry)
|
|
@@ -7684,6 +7760,8 @@ async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
|
|
|
7684
7760
|
lock.hooks[item.name] = { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" };
|
|
7685
7761
|
}
|
|
7686
7762
|
writeLock(lock);
|
|
7763
|
+
if (!db)
|
|
7764
|
+
return;
|
|
7687
7765
|
db.exec("BEGIN");
|
|
7688
7766
|
try {
|
|
7689
7767
|
for (const item of staged) {
|
|
@@ -7746,23 +7824,56 @@ async function fetchPinnedHook(name, version, apiUrl, apiKey) {
|
|
|
7746
7824
|
if (actual !== expectedSha) {
|
|
7747
7825
|
throw new Error(`sha256 mismatch after write for '${name}@${version}': expected ${expectedSha}, on disk ${actual}`);
|
|
7748
7826
|
}
|
|
7749
|
-
const db = getDb();
|
|
7750
7827
|
setPinnedHook(name, { version, sha256: expectedSha, source: entry.source ?? "remote", pinned: true });
|
|
7751
|
-
|
|
7828
|
+
if (!isLocalStoreRefused()) {
|
|
7829
|
+
upsertHookRecord(getDb(), {
|
|
7830
|
+
name,
|
|
7831
|
+
version,
|
|
7832
|
+
sha256: expectedSha,
|
|
7833
|
+
source_type: entry.source ?? "remote",
|
|
7834
|
+
source_ref: apiUrl,
|
|
7835
|
+
last_verified_at: new Date().toISOString()
|
|
7836
|
+
});
|
|
7837
|
+
}
|
|
7838
|
+
return {
|
|
7752
7839
|
name,
|
|
7753
7840
|
version,
|
|
7754
7841
|
sha256: expectedSha,
|
|
7755
|
-
|
|
7842
|
+
source: entry.source ?? "remote",
|
|
7756
7843
|
source_ref: apiUrl,
|
|
7844
|
+
artifact,
|
|
7845
|
+
scriptPath
|
|
7846
|
+
};
|
|
7847
|
+
}
|
|
7848
|
+
async function installPinnedFromBundled(name, version) {
|
|
7849
|
+
const bundled = collectBundledCatalog().find((entry) => entry.name === name);
|
|
7850
|
+
if (!bundled) {
|
|
7851
|
+
throw new Error(`Hook '${name}' is not in the bundled registry`);
|
|
7852
|
+
}
|
|
7853
|
+
if (bundled.version !== version) {
|
|
7854
|
+
throw new Error(`Hook '${name}' version ${version} is not in the bundled registry (available: ${bundled.version})`);
|
|
7855
|
+
}
|
|
7856
|
+
const scriptPath = resolveScriptPath(name);
|
|
7857
|
+
if (!scriptPath) {
|
|
7858
|
+
throw new Error(`Hook '${name}' has no script in the bundled registry`);
|
|
7859
|
+
}
|
|
7860
|
+
const db = getDb();
|
|
7861
|
+
setPinnedHook(name, { version, sha256: bundled.sha256, source: "bundled", pinned: true });
|
|
7862
|
+
upsertHookRecord(db, {
|
|
7863
|
+
name,
|
|
7864
|
+
version,
|
|
7865
|
+
sha256: bundled.sha256,
|
|
7866
|
+
source_type: "bundled",
|
|
7867
|
+
source_ref: "bundled registry",
|
|
7757
7868
|
last_verified_at: new Date().toISOString()
|
|
7758
7869
|
});
|
|
7759
7870
|
return {
|
|
7760
7871
|
name,
|
|
7761
7872
|
version,
|
|
7762
|
-
sha256:
|
|
7763
|
-
source:
|
|
7764
|
-
source_ref:
|
|
7765
|
-
artifact,
|
|
7873
|
+
sha256: bundled.sha256,
|
|
7874
|
+
source: "bundled",
|
|
7875
|
+
source_ref: "bundled registry",
|
|
7876
|
+
artifact: null,
|
|
7766
7877
|
scriptPath
|
|
7767
7878
|
};
|
|
7768
7879
|
}
|
|
@@ -8595,7 +8706,7 @@ function startServeServer(options) {
|
|
|
8595
8706
|
return handleServeRequest(req, resolveHooksServePublishKey());
|
|
8596
8707
|
}
|
|
8597
8708
|
});
|
|
8598
|
-
console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
|
|
8709
|
+
console.error(`hooks registry listening on http://${host}:${server.port} (publish requires an API key)`);
|
|
8599
8710
|
return server;
|
|
8600
8711
|
}
|
|
8601
8712
|
var DEFAULT_SERVE_PORT = 39428, SERVE_HOST = "127.0.0.1", SERVE_SERVICE_NAME = "hooks-registry";
|
|
@@ -8691,6 +8802,32 @@ async function provisionCloudflareResources(options) {
|
|
|
8691
8802
|
}
|
|
8692
8803
|
var CF_API = "https://api.cloudflare.com/client/v4";
|
|
8693
8804
|
|
|
8805
|
+
// src/mcp/authority.ts
|
|
8806
|
+
var exports_authority = {};
|
|
8807
|
+
__export(exports_authority, {
|
|
8808
|
+
resolveHooksMcpAuthority: () => resolveHooksMcpAuthority,
|
|
8809
|
+
decideHooksMcpAuthority: () => decideHooksMcpAuthority
|
|
8810
|
+
});
|
|
8811
|
+
function resolveHooksMcpAuthority(env = process.env) {
|
|
8812
|
+
const transport = resolveHooksTransport(env);
|
|
8813
|
+
if (transport.mode === "local" || !transport.authority) {
|
|
8814
|
+
return { route: "local", source: "local-opt-in" };
|
|
8815
|
+
}
|
|
8816
|
+
return { route: "hosted", v1BaseUrl: transport.authority.v1BaseUrl, source: transport.source };
|
|
8817
|
+
}
|
|
8818
|
+
function decideHooksMcpAuthority(env = process.env) {
|
|
8819
|
+
const authority = resolveHooksMcpAuthority(env);
|
|
8820
|
+
if (authority.route === "hosted") {
|
|
8821
|
+
refuseLocalStore(hooksHostedRouteLocalStoreRefusal(authority.v1BaseUrl));
|
|
8822
|
+
}
|
|
8823
|
+
return authority;
|
|
8824
|
+
}
|
|
8825
|
+
var init_authority = __esm(() => {
|
|
8826
|
+
init_db();
|
|
8827
|
+
init_local_opt_in();
|
|
8828
|
+
init_transport2();
|
|
8829
|
+
});
|
|
8830
|
+
|
|
8694
8831
|
// src/mcp/server.ts
|
|
8695
8832
|
var exports_server = {};
|
|
8696
8833
|
__export(exports_server, {
|
|
@@ -9632,6 +9769,7 @@ async function startSSEServer(options = {}) {
|
|
|
9632
9769
|
if (!loopback && !authToken) {
|
|
9633
9770
|
throw new Error(`Refusing to serve MCP SSE on non-loopback host '${host}' without an auth token. Set HASNA_HOOKS_MCP_TOKEN (or HOOKS_MCP_TOKEN) to authorize a non-loopback bind.`);
|
|
9634
9771
|
}
|
|
9772
|
+
decideHooksMcpAuthority();
|
|
9635
9773
|
const server = createHooksServer();
|
|
9636
9774
|
const transports = new Map;
|
|
9637
9775
|
function authorized2(headers) {
|
|
@@ -9681,6 +9819,13 @@ async function startSSEServer(options = {}) {
|
|
|
9681
9819
|
});
|
|
9682
9820
|
}
|
|
9683
9821
|
async function startStdioServer() {
|
|
9822
|
+
try {
|
|
9823
|
+
decideHooksMcpAuthority();
|
|
9824
|
+
} catch (err) {
|
|
9825
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
9826
|
+
`);
|
|
9827
|
+
process.exit(1);
|
|
9828
|
+
}
|
|
9684
9829
|
try {
|
|
9685
9830
|
const server = createHooksServer();
|
|
9686
9831
|
const transport = new StdioServerTransport;
|
|
@@ -9701,6 +9846,7 @@ var init_server = __esm(() => {
|
|
|
9701
9846
|
init_registration();
|
|
9702
9847
|
init_store();
|
|
9703
9848
|
init_redact();
|
|
9849
|
+
init_authority();
|
|
9704
9850
|
init_storage();
|
|
9705
9851
|
__dirname3 = dirname7(fileURLToPath4(import.meta.url));
|
|
9706
9852
|
pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
@@ -10892,11 +11038,11 @@ function setPath(input, path, replacement) {
|
|
|
10892
11038
|
if (last && last in cursor)
|
|
10893
11039
|
cursor[last] = replacement;
|
|
10894
11040
|
}
|
|
11041
|
+
var MAX_PACKAGE_BYTES = 1024 * 1024;
|
|
10895
11042
|
var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
10896
11043
|
var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
10897
11044
|
var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
10898
11045
|
var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
10899
|
-
var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
10900
11046
|
var requireSecretsSdk = createRequire(import.meta.url);
|
|
10901
11047
|
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
10902
11048
|
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
@@ -12637,6 +12783,7 @@ init_app_home();
|
|
|
12637
12783
|
init_config();
|
|
12638
12784
|
init_local_opt_in();
|
|
12639
12785
|
init_transport2();
|
|
12786
|
+
init_db();
|
|
12640
12787
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
12641
12788
|
var __dirname4 = dirname8(fileURLToPath5(import.meta.url));
|
|
12642
12789
|
var pkgPath = existsSync15(join17(__dirname4, "..", "package.json")) ? join17(__dirname4, "..", "package.json") : join17(__dirname4, "..", "..", "package.json");
|
|
@@ -12671,6 +12818,19 @@ function truncateText2(value, max = 96) {
|
|
|
12671
12818
|
function projectLogRows2(rows) {
|
|
12672
12819
|
return rows.map((row) => projectEventRowForRead({ ...row }));
|
|
12673
12820
|
}
|
|
12821
|
+
async function openLocalStoreOrExit(json2) {
|
|
12822
|
+
try {
|
|
12823
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
12824
|
+
return getDb2();
|
|
12825
|
+
} catch (error) {
|
|
12826
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
12827
|
+
if (json2)
|
|
12828
|
+
console.log(JSON.stringify({ error: message }));
|
|
12829
|
+
else
|
|
12830
|
+
console.error(chalk2.red(`\u2717 ${message}`));
|
|
12831
|
+
process.exit(1);
|
|
12832
|
+
}
|
|
12833
|
+
}
|
|
12674
12834
|
function readToken(tokenFile) {
|
|
12675
12835
|
if (tokenFile) {
|
|
12676
12836
|
try {
|
|
@@ -12735,6 +12895,11 @@ function suggestHooks(name, max = 3) {
|
|
|
12735
12895
|
}
|
|
12736
12896
|
program2.name("hooks").description("Install hooks for AI coding agents").version(pkg2.version);
|
|
12737
12897
|
program2.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
|
|
12898
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
12899
|
+
console.error(chalk2.red("The interactive hook browser requires a TTY terminal.") + `
|
|
12900
|
+
` + chalk2.dim("Use `hooks search <query>`, `hooks list`, or `hooks categories` for non-interactive use."));
|
|
12901
|
+
process.exit(1);
|
|
12902
|
+
}
|
|
12738
12903
|
render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
|
|
12739
12904
|
});
|
|
12740
12905
|
program2.command("init").description("Register a new agent profile with a unique ID").option("-a, --agent <type>", "Agent type: claude, gemini, custom", "claude").option("-n, --name <name>", "Optional display name for the agent").option("-j, --json", "Output as JSON", false).option("--cloudflare", "Configure a remote registry (Cloudflare worker)", false).option("--api-url <url>", "Remote registry API URL (with --cloudflare)").option("--api-key <ref>", "Vault key NAME for the registry API key (never the value; with --cloudflare)").action(async (options) => {
|
|
@@ -12970,6 +13135,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
12970
13135
|
const { readCustomManifest: readCustomManifest2, HOOK_NAME_RE: HOOK_NAME_RE2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
12971
13136
|
const { resolveHooksTransport: resolveHooksTransport2 } = await Promise.resolve().then(() => (init_transport2(), exports_transport));
|
|
12972
13137
|
const { sha256Of: sha256Of2, pinInstalledHook: pinInstalledHook2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
13138
|
+
const { installPinnedFromBundled: installPinnedFromBundled2, fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
12973
13139
|
const { readFileSync: readFileSyncFs } = await import("fs");
|
|
12974
13140
|
function parseNameVersion(arg) {
|
|
12975
13141
|
if (isCustomSource2(arg))
|
|
@@ -13062,17 +13228,13 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
13062
13228
|
for (const { arg, pinned } of pinnedRequests) {
|
|
13063
13229
|
try {
|
|
13064
13230
|
const transport = resolveHooksTransport2();
|
|
13065
|
-
|
|
13066
|
-
throw new Error(`Cannot install '${arg}': no remote registry configured (set HASNA_HOOKS_API_URL and HASNA_HOOKS_API_KEY, or opt into local mode with HASNA_HOOKS_LOCAL=1)`);
|
|
13067
|
-
}
|
|
13068
|
-
const { origin, apiKey } = transport.authority;
|
|
13069
|
-
const { fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
13070
|
-
const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version, origin, apiKey);
|
|
13231
|
+
const pinnedInstall = transport.mode === "remote" && transport.authority ? await fetchPinnedHook2(pinned.name, pinned.version, transport.authority.origin, transport.authority.apiKey) : await installPinnedFromBundled2(pinned.name, pinned.version);
|
|
13071
13232
|
installedPinned.push(pinnedInstall.name);
|
|
13072
13233
|
if (options.json) {
|
|
13073
13234
|
console.log(JSON.stringify({ pinned: { request: arg, name: pinnedInstall.name, version: pinnedInstall.version, sha256: pinnedInstall.sha256, source: pinnedInstall.source } }));
|
|
13074
13235
|
} else {
|
|
13075
|
-
|
|
13236
|
+
const registryLabel = transport.mode === "remote" ? "registry" : "bundled registry";
|
|
13237
|
+
console.log(chalk2.green(`\u2713 Installed '${pinnedInstall.name}' v${pinnedInstall.version} from ${registryLabel} (sha256 ${pinnedInstall.sha256.slice(0, 12)}\u2026)`));
|
|
13076
13238
|
}
|
|
13077
13239
|
} catch (error) {
|
|
13078
13240
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -13547,8 +13709,9 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
13547
13709
|
}
|
|
13548
13710
|
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
13549
13711
|
const { sha256File: sha256File2, setPinnedHook: setPinnedHook2, upsertHookRecord: upsertHookRecord2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
13550
|
-
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
13712
|
+
const { getDb: getDb2, isLocalStoreRefused: isLocalStoreRefused2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
13551
13713
|
const { resolveHooksTransport: resolveHooksTransport2 } = await Promise.resolve().then(() => (init_transport2(), exports_transport));
|
|
13714
|
+
const { installPinnedFromBundled: installPinnedFromBundled2, fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
13552
13715
|
const { isCustomSource: isCustomSource2 } = await Promise.resolve().then(() => (init_custom_install(), exports_custom_install));
|
|
13553
13716
|
const { HOOK_NAME_RE: HOOK_NAME_RE2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
13554
13717
|
function parseNameVersion(arg) {
|
|
@@ -13571,12 +13734,7 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
13571
13734
|
if (pinned) {
|
|
13572
13735
|
try {
|
|
13573
13736
|
const transport = resolveHooksTransport2();
|
|
13574
|
-
|
|
13575
|
-
throw new Error(`Cannot update '${arg}': no remote registry configured (set HASNA_HOOKS_API_URL and HASNA_HOOKS_API_KEY, or opt into local mode with HASNA_HOOKS_LOCAL=1)`);
|
|
13576
|
-
}
|
|
13577
|
-
const { origin, apiKey } = transport.authority;
|
|
13578
|
-
const { fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
13579
|
-
const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version, origin, apiKey);
|
|
13737
|
+
const pinnedInstall = transport.mode === "remote" && transport.authority ? await fetchPinnedHook2(pinned.name, pinned.version, transport.authority.origin, transport.authority.apiKey) : await installPinnedFromBundled2(pinned.name, pinned.version);
|
|
13580
13738
|
const registered = installHook(pinnedInstall.name, { scope, overwrite: true });
|
|
13581
13739
|
if (!registered.success) {
|
|
13582
13740
|
results.push({ hook: arg, success: false, error: registered.error ?? "registration failed" });
|
|
@@ -13601,13 +13759,15 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
13601
13759
|
if (result.success && resolved) {
|
|
13602
13760
|
const hash = await sha256File2(resolved.scriptPath);
|
|
13603
13761
|
setPinnedHook2(resolved.name, { version: resolved.version, sha256: hash, source: resolved.source });
|
|
13604
|
-
|
|
13605
|
-
|
|
13606
|
-
|
|
13607
|
-
|
|
13608
|
-
|
|
13609
|
-
|
|
13610
|
-
|
|
13762
|
+
if (!isLocalStoreRefused2()) {
|
|
13763
|
+
upsertHookRecord2(getDb2(), {
|
|
13764
|
+
name: resolved.name,
|
|
13765
|
+
version: resolved.version,
|
|
13766
|
+
sha256: hash,
|
|
13767
|
+
source_type: resolved.source,
|
|
13768
|
+
last_verified_at: new Date().toISOString()
|
|
13769
|
+
});
|
|
13770
|
+
}
|
|
13611
13771
|
results.push({ ...result, pinned: { version: resolved.version, sha256: hash } });
|
|
13612
13772
|
continue;
|
|
13613
13773
|
}
|
|
@@ -13885,8 +14045,7 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
13885
14045
|
});
|
|
13886
14046
|
var logCmd = program2.command("log").description(`Query hook event logs from SQLite (${getReportedDbPath()})`);
|
|
13887
14047
|
logCmd.command("list").description("List hook events").option("--hook <name>", "Filter by hook name").option("--session <id>", "Filter by session ID").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
13888
|
-
const
|
|
13889
|
-
const db = getDb2();
|
|
14048
|
+
const db = await openLocalStoreOrExit(options.json);
|
|
13890
14049
|
const limit = parseInt(options.limit) || 50;
|
|
13891
14050
|
let sql = "SELECT * FROM hook_events WHERE 1=1";
|
|
13892
14051
|
const params = [];
|
|
@@ -13923,8 +14082,7 @@ logCmd.command("list").description("List hook events").option("--hook <name>", "
|
|
|
13923
14082
|
Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
|
|
13924
14083
|
});
|
|
13925
14084
|
logCmd.command("search <text>").description("Search hook events by tool_input or error text").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (text, options) => {
|
|
13926
|
-
const
|
|
13927
|
-
const db = getDb2();
|
|
14085
|
+
const db = await openLocalStoreOrExit(options.json);
|
|
13928
14086
|
const limit = parseInt(options.limit) || 50;
|
|
13929
14087
|
const q = `%${text}%`;
|
|
13930
14088
|
const rows = db.query("SELECT * FROM hook_events WHERE tool_input LIKE ? OR error LIKE ? ORDER BY timestamp DESC LIMIT ?").all(q, q, limit);
|
|
@@ -13949,8 +14107,7 @@ logCmd.command("search <text>").description("Search hook events by tool_input or
|
|
|
13949
14107
|
Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
|
|
13950
14108
|
});
|
|
13951
14109
|
logCmd.command("tail").description("Show most recent hook events").option("-n <n>", "Number of rows", "20").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
13952
|
-
const
|
|
13953
|
-
const db = getDb2();
|
|
14110
|
+
const db = await openLocalStoreOrExit(options.json);
|
|
13954
14111
|
const limit = parseInt(options.n) || 20;
|
|
13955
14112
|
const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(limit);
|
|
13956
14113
|
const projected = projectLogRows2(rows);
|
|
@@ -13975,8 +14132,7 @@ logCmd.command("tail").description("Show most recent hook events").option("-n <n
|
|
|
13975
14132
|
Compact rows shown. Use --json for full event records or -n <n> to change row count.`));
|
|
13976
14133
|
});
|
|
13977
14134
|
logCmd.command("errors").description("Show hook events that contain errors").option("--since <duration>", "Only show errors since this duration (e.g. 1h, 30m, 7d)", "24h").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
13978
|
-
const
|
|
13979
|
-
const db = getDb2();
|
|
14135
|
+
const db = await openLocalStoreOrExit(options.json);
|
|
13980
14136
|
const limit = parseInt(options.limit) || 50;
|
|
13981
14137
|
function parseDuration(s) {
|
|
13982
14138
|
const m = s.match(/^(\d+)(s|m|h|d)$/);
|
|
@@ -14018,8 +14174,7 @@ logCmd.command("errors").description("Show hook events that contain errors").opt
|
|
|
14018
14174
|
Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
|
|
14019
14175
|
});
|
|
14020
14176
|
logCmd.command("clear").description("Delete hook event logs").option("--hook <name>", "Only delete events for this hook").option("-y, --yes", "Skip confirmation prompt", false).action(async (options) => {
|
|
14021
|
-
const
|
|
14022
|
-
const db = getDb2();
|
|
14177
|
+
const db = await openLocalStoreOrExit(false);
|
|
14023
14178
|
const countRow = options.hook ? db.query("SELECT COUNT(*) as n FROM hook_events WHERE hook_name = ?").get(options.hook) : db.query("SELECT COUNT(*) as n FROM hook_events").get();
|
|
14024
14179
|
const count = countRow?.n ?? 0;
|
|
14025
14180
|
if (count === 0) {
|
|
@@ -14041,19 +14196,28 @@ logCmd.command("clear").description("Delete hook event logs").option("--hook <na
|
|
|
14041
14196
|
});
|
|
14042
14197
|
var storageCmd = program2.command("storage").description("Sync local hook data with storage PostgreSQL");
|
|
14043
14198
|
storageCmd.command("status").description("Show storage sync status").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
14044
|
-
|
|
14045
|
-
|
|
14046
|
-
|
|
14047
|
-
|
|
14048
|
-
|
|
14049
|
-
|
|
14050
|
-
|
|
14199
|
+
try {
|
|
14200
|
+
const { getStorageStatus: getStorageStatus2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
14201
|
+
const status = getStorageStatus2();
|
|
14202
|
+
if (options.json) {
|
|
14203
|
+
console.log(JSON.stringify(status, null, 2));
|
|
14204
|
+
return;
|
|
14205
|
+
}
|
|
14206
|
+
console.log(chalk2.bold(`
|
|
14051
14207
|
Storage Status
|
|
14052
14208
|
`));
|
|
14053
|
-
|
|
14054
|
-
|
|
14055
|
-
|
|
14056
|
-
|
|
14209
|
+
console.log(` Configured: ${status.configured ? chalk2.green(`yes (${status.activeEnv})`) : chalk2.red("no")}`);
|
|
14210
|
+
console.log(` Backend: ${status.backend}`);
|
|
14211
|
+
console.log(` Tables: ${status.tables.join(", ")}`);
|
|
14212
|
+
console.log(` Sync rows: ${status.sync.length}`);
|
|
14213
|
+
} catch (error) {
|
|
14214
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
14215
|
+
if (options.json)
|
|
14216
|
+
console.log(JSON.stringify({ error: message }));
|
|
14217
|
+
else
|
|
14218
|
+
console.error(chalk2.red(`\u2717 ${message}`));
|
|
14219
|
+
process.exitCode = 1;
|
|
14220
|
+
}
|
|
14057
14221
|
});
|
|
14058
14222
|
storageCmd.command("push").description("Push local hook data to storage PostgreSQL").option("-t, --tables <tables>", "Comma-separated table names").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
14059
14223
|
try {
|
|
@@ -14263,6 +14427,13 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
|
|
|
14263
14427
|
const { startSSEServer: startSSEServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
14264
14428
|
await startSSEServer2({ port: options.port ? parseInt(options.port) : 39427 });
|
|
14265
14429
|
} else {
|
|
14430
|
+
const { decideHooksMcpAuthority: decideHooksMcpAuthority2 } = await Promise.resolve().then(() => (init_authority(), exports_authority));
|
|
14431
|
+
try {
|
|
14432
|
+
decideHooksMcpAuthority2();
|
|
14433
|
+
} catch (error) {
|
|
14434
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
14435
|
+
process.exit(1);
|
|
14436
|
+
}
|
|
14266
14437
|
const { createHooksServer: createHooksServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
14267
14438
|
const { resolveMcpHttpPort: resolveMcpHttpPort2, startMcpHttpServer: startMcpHttpServer2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
14268
14439
|
const args = options.port ? ["--port", options.port] : [];
|
|
@@ -14271,7 +14442,6 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
|
|
|
14271
14442
|
});
|
|
14272
14443
|
registerEventsCommands(program2, { source: "hooks" });
|
|
14273
14444
|
var API_INDEPENDENT_COMMANDS = new Set([
|
|
14274
|
-
"run",
|
|
14275
14445
|
"serve",
|
|
14276
14446
|
"mcp",
|
|
14277
14447
|
"cf",
|
|
@@ -14283,11 +14453,11 @@ var API_INDEPENDENT_COMMANDS = new Set([
|
|
|
14283
14453
|
"events"
|
|
14284
14454
|
]);
|
|
14285
14455
|
function failClosedForMissingApiEnv(detail) {
|
|
14286
|
-
console.error(chalk2.red(
|
|
14456
|
+
console.error(chalk2.red(hooksFailClosedLine() + `
|
|
14287
14457
|
` + `The remote registry requires a STRICT pair \u2014 set HASNA_HOOKS_API_URL and HASNA_HOOKS_API_KEY, or the
|
|
14288
14458
|
` + `Keychain items hasna.credentials.hooks.api-url / .api-key, or ~/.hasna/hooks/config/credentials.
|
|
14289
|
-
|
|
14290
|
-
|
|
14459
|
+
config.json (api_url / api_key_ref) is RETIRED and no longer read. Or set
|
|
14460
|
+
HASNA_HOOKS_LOCAL=1 to explicitly opt into local mode (bundled registry + local store).
|
|
14291
14461
|
` + (detail ? `${detail}
|
|
14292
14462
|
` : "") + "Refusing to silently fall back to local storage."));
|
|
14293
14463
|
process.exit(1);
|
|
@@ -14311,21 +14481,25 @@ function enforceTransportGate() {
|
|
|
14311
14481
|
return;
|
|
14312
14482
|
if (token !== null && API_INDEPENDENT_COMMANDS.has(token))
|
|
14313
14483
|
return;
|
|
14314
|
-
if (hasHooksEnvAuthorityIntent(process.env))
|
|
14484
|
+
if (hasHooksEnvAuthorityIntent(process.env)) {
|
|
14485
|
+
refuseLocalStore(hooksHostedRouteLocalStoreRefusal("configured by HASNA_HOOKS_API_URL / HASNA_HOOKS_API_KEY"));
|
|
14315
14486
|
return;
|
|
14487
|
+
}
|
|
14316
14488
|
if (isHooksLocalOptIn(process.env)) {
|
|
14317
14489
|
announceHooksLocalMode();
|
|
14318
14490
|
return;
|
|
14319
14491
|
}
|
|
14492
|
+
let transport;
|
|
14320
14493
|
try {
|
|
14321
|
-
|
|
14322
|
-
if (transport.mode !== "remote" || !transport.authority) {
|
|
14323
|
-
failClosedForMissingApiEnv();
|
|
14324
|
-
}
|
|
14494
|
+
transport = resolveHooksTransport(process.env);
|
|
14325
14495
|
} catch (error) {
|
|
14326
14496
|
const message = error instanceof Error ? error.message : String(error);
|
|
14327
14497
|
failClosedForMissingApiEnv(message);
|
|
14328
14498
|
}
|
|
14499
|
+
if (transport.mode !== "remote" || !transport.authority) {
|
|
14500
|
+
failClosedForMissingApiEnv();
|
|
14501
|
+
}
|
|
14502
|
+
refuseLocalStore(hooksHostedRouteLocalStoreRefusal(transport.authority.v1BaseUrl));
|
|
14329
14503
|
}
|
|
14330
14504
|
enforceTransportGate();
|
|
14331
14505
|
program2.parse();
|