@hasna/hooks 0.2.6 → 0.2.8
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/bin/index.js +645 -43
- package/dist/index.js +89 -5
- package/hooks/hook-affected-tests/LICENSE +191 -0
- package/hooks/hook-affected-tests/README.md +37 -0
- package/hooks/hook-affected-tests/package.json +50 -0
- package/hooks/hook-affected-tests/src/hook.ts +148 -0
- package/hooks/hook-affected-tests/tsconfig.json +25 -0
- package/hooks/hook-agentmessages/bin/cli.ts +125 -0
- package/hooks/hook-announce-start/LICENSE +191 -0
- package/hooks/hook-announce-start/README.md +35 -0
- package/hooks/hook-announce-start/package.json +51 -0
- package/hooks/hook-announce-start/src/hook.ts +137 -0
- package/hooks/hook-announce-start/tsconfig.json +25 -0
- package/hooks/hook-announce-stop/LICENSE +191 -0
- package/hooks/hook-announce-stop/README.md +35 -0
- package/hooks/hook-announce-stop/package.json +51 -0
- package/hooks/hook-announce-stop/src/hook.ts +160 -0
- package/hooks/hook-announce-stop/tsconfig.json +25 -0
- package/hooks/hook-checkdocs/bun.lock +25 -0
- package/hooks/hook-commandlog/src/hook.ts +15 -38
- package/hooks/hook-conflict-detect/LICENSE +191 -0
- package/hooks/hook-conflict-detect/README.md +38 -0
- package/hooks/hook-conflict-detect/package.json +50 -0
- package/hooks/hook-conflict-detect/src/hook.ts +116 -0
- package/hooks/hook-conflict-detect/tsconfig.json +25 -0
- package/hooks/hook-costwatch/src/hook.ts +39 -42
- package/hooks/hook-dm-inject/LICENSE +191 -0
- package/hooks/hook-dm-inject/README.md +42 -0
- package/hooks/hook-dm-inject/package.json +51 -0
- package/hooks/hook-dm-inject/src/hook.ts +115 -0
- package/hooks/hook-dm-inject/tsconfig.json +25 -0
- package/hooks/hook-errornotify/src/hook.ts +20 -65
- package/hooks/hook-failure-to-task/LICENSE +191 -0
- package/hooks/hook-failure-to-task/README.md +40 -0
- package/hooks/hook-failure-to-task/package.json +51 -0
- package/hooks/hook-failure-to-task/src/hook.ts +171 -0
- package/hooks/hook-failure-to-task/tsconfig.json +25 -0
- package/hooks/hook-filelock/LICENSE +191 -0
- package/hooks/hook-filelock/README.md +44 -0
- package/hooks/hook-filelock/package.json +50 -0
- package/hooks/hook-filelock/src/hook.ts +147 -0
- package/hooks/hook-filelock/tsconfig.json +25 -0
- package/hooks/hook-sessionlog/src/hook.ts +11 -52
- package/hooks/hook-typecheck-gate/LICENSE +191 -0
- package/hooks/hook-typecheck-gate/README.md +46 -0
- package/hooks/hook-typecheck-gate/package.json +50 -0
- package/hooks/hook-typecheck-gate/src/hook.ts +152 -0
- package/hooks/hook-typecheck-gate/tsconfig.json +25 -0
- package/package.json +1 -1
package/bin/index.js
CHANGED
|
@@ -5,25 +5,43 @@ var __getProtoOf = Object.getPrototypeOf;
|
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
function __accessProp(key) {
|
|
9
|
+
return this[key];
|
|
10
|
+
}
|
|
11
|
+
var __toESMCache_node;
|
|
12
|
+
var __toESMCache_esm;
|
|
8
13
|
var __toESM = (mod, isNodeMode, target) => {
|
|
14
|
+
var canCache = mod != null && typeof mod === "object";
|
|
15
|
+
if (canCache) {
|
|
16
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
17
|
+
var cached = cache.get(mod);
|
|
18
|
+
if (cached)
|
|
19
|
+
return cached;
|
|
20
|
+
}
|
|
9
21
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
22
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
23
|
for (let key of __getOwnPropNames(mod))
|
|
12
24
|
if (!__hasOwnProp.call(to, key))
|
|
13
25
|
__defProp(to, key, {
|
|
14
|
-
get: (
|
|
26
|
+
get: __accessProp.bind(mod, key),
|
|
15
27
|
enumerable: true
|
|
16
28
|
});
|
|
29
|
+
if (canCache)
|
|
30
|
+
cache.set(mod, to);
|
|
17
31
|
return to;
|
|
18
32
|
};
|
|
19
33
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
34
|
+
var __returnValue = (v) => v;
|
|
35
|
+
function __exportSetter(name, newValue) {
|
|
36
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
37
|
+
}
|
|
20
38
|
var __export = (target, all) => {
|
|
21
39
|
for (var name in all)
|
|
22
40
|
__defProp(target, name, {
|
|
23
41
|
get: all[name],
|
|
24
42
|
enumerable: true,
|
|
25
43
|
configurable: true,
|
|
26
|
-
set: (
|
|
44
|
+
set: __exportSetter.bind(all, name)
|
|
27
45
|
});
|
|
28
46
|
};
|
|
29
47
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -2147,7 +2165,7 @@ var init_registry = __esm(() => {
|
|
|
2147
2165
|
{
|
|
2148
2166
|
name: "sessionlog",
|
|
2149
2167
|
displayName: "Session Log",
|
|
2150
|
-
description: "Logs every tool call to .
|
|
2168
|
+
description: "Logs every tool call to SQLite (~/.hooks/hooks.db)",
|
|
2151
2169
|
version: "0.1.0",
|
|
2152
2170
|
category: "Observability",
|
|
2153
2171
|
event: "PostToolUse",
|
|
@@ -2157,7 +2175,7 @@ var init_registry = __esm(() => {
|
|
|
2157
2175
|
{
|
|
2158
2176
|
name: "commandlog",
|
|
2159
2177
|
displayName: "Command Log",
|
|
2160
|
-
description: "Logs every
|
|
2178
|
+
description: "Logs every Bash command to SQLite (~/.hooks/hooks.db)",
|
|
2161
2179
|
version: "0.1.0",
|
|
2162
2180
|
category: "Observability",
|
|
2163
2181
|
event: "PostToolUse",
|
|
@@ -2167,7 +2185,7 @@ var init_registry = __esm(() => {
|
|
|
2167
2185
|
{
|
|
2168
2186
|
name: "costwatch",
|
|
2169
2187
|
displayName: "Cost Watch",
|
|
2170
|
-
description: "Estimates session token usage and warns
|
|
2188
|
+
description: "Estimates session token usage, persists cost history to SQLite, and warns on budget overrun",
|
|
2171
2189
|
version: "0.1.0",
|
|
2172
2190
|
category: "Observability",
|
|
2173
2191
|
event: "Stop",
|
|
@@ -2177,7 +2195,7 @@ var init_registry = __esm(() => {
|
|
|
2177
2195
|
{
|
|
2178
2196
|
name: "errornotify",
|
|
2179
2197
|
displayName: "Error Notify",
|
|
2180
|
-
description: "Detects tool failures and logs errors to
|
|
2198
|
+
description: "Detects tool failures and logs errors to SQLite (~/.hooks/hooks.db)",
|
|
2181
2199
|
version: "0.1.0",
|
|
2182
2200
|
category: "Observability",
|
|
2183
2201
|
event: "PostToolUse",
|
|
@@ -2203,6 +2221,86 @@ var init_registry = __esm(() => {
|
|
|
2203
2221
|
event: "PostToolUse",
|
|
2204
2222
|
matcher: "",
|
|
2205
2223
|
tags: ["tasks", "completion", "gate", "quality", "agent-teams"]
|
|
2224
|
+
},
|
|
2225
|
+
{
|
|
2226
|
+
name: "typecheck-gate",
|
|
2227
|
+
displayName: "Typecheck Gate",
|
|
2228
|
+
description: "Runs TypeScript type checking on Stop and blocks if type errors are found",
|
|
2229
|
+
version: "0.1.0",
|
|
2230
|
+
category: "Code Quality",
|
|
2231
|
+
event: "Stop",
|
|
2232
|
+
matcher: "",
|
|
2233
|
+
tags: ["typescript", "typecheck", "types", "quality", "gate"]
|
|
2234
|
+
},
|
|
2235
|
+
{
|
|
2236
|
+
name: "affected-tests",
|
|
2237
|
+
displayName: "Affected Tests",
|
|
2238
|
+
description: "Maps edited files to their test files and runs them automatically after edits",
|
|
2239
|
+
version: "0.1.0",
|
|
2240
|
+
category: "Code Quality",
|
|
2241
|
+
event: "PostToolUse",
|
|
2242
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
2243
|
+
tags: ["tests", "test-runner", "affected", "quality"]
|
|
2244
|
+
},
|
|
2245
|
+
{
|
|
2246
|
+
name: "conflict-detect",
|
|
2247
|
+
displayName: "Conflict Detect",
|
|
2248
|
+
description: "Blocks edits on files with unresolved git merge conflict markers",
|
|
2249
|
+
version: "0.1.0",
|
|
2250
|
+
category: "Git Safety",
|
|
2251
|
+
event: "PreToolUse",
|
|
2252
|
+
matcher: "Edit|Write",
|
|
2253
|
+
tags: ["git", "conflicts", "merge", "safety"]
|
|
2254
|
+
},
|
|
2255
|
+
{
|
|
2256
|
+
name: "failure-to-task",
|
|
2257
|
+
displayName: "Failure to Task",
|
|
2258
|
+
description: "Creates a todo task automatically when test or build commands fail",
|
|
2259
|
+
version: "0.1.0",
|
|
2260
|
+
category: "Workflow Automation",
|
|
2261
|
+
event: "PostToolUse",
|
|
2262
|
+
matcher: "Bash",
|
|
2263
|
+
tags: ["failures", "tasks", "tests", "build", "automation"]
|
|
2264
|
+
},
|
|
2265
|
+
{
|
|
2266
|
+
name: "filelock",
|
|
2267
|
+
displayName: "File Lock",
|
|
2268
|
+
description: "Auto-checks file locks before edits to prevent multi-agent conflicts",
|
|
2269
|
+
version: "0.1.0",
|
|
2270
|
+
category: "Agent Teams",
|
|
2271
|
+
event: "PreToolUse",
|
|
2272
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
2273
|
+
tags: ["locking", "coordination", "multi-agent", "conflict"]
|
|
2274
|
+
},
|
|
2275
|
+
{
|
|
2276
|
+
name: "announce-start",
|
|
2277
|
+
displayName: "Announce Start",
|
|
2278
|
+
description: "On session start, auto-registers agent, reads messages, and announces to space",
|
|
2279
|
+
version: "0.1.0",
|
|
2280
|
+
category: "Agent Teams",
|
|
2281
|
+
event: "Notification",
|
|
2282
|
+
matcher: "",
|
|
2283
|
+
tags: ["announcement", "start", "register", "messages", "agent-teams"]
|
|
2284
|
+
},
|
|
2285
|
+
{
|
|
2286
|
+
name: "announce-stop",
|
|
2287
|
+
displayName: "Announce Stop",
|
|
2288
|
+
description: "On Stop, releases file locks, posts session summary, and updates task statuses",
|
|
2289
|
+
version: "0.1.0",
|
|
2290
|
+
category: "Agent Teams",
|
|
2291
|
+
event: "Stop",
|
|
2292
|
+
matcher: "",
|
|
2293
|
+
tags: ["announcement", "stop", "locks", "summary", "agent-teams"]
|
|
2294
|
+
},
|
|
2295
|
+
{
|
|
2296
|
+
name: "dm-inject",
|
|
2297
|
+
displayName: "DM Inject",
|
|
2298
|
+
description: "Injects unread direct messages into agent context on Notification events",
|
|
2299
|
+
version: "0.1.0",
|
|
2300
|
+
category: "Agent Teams",
|
|
2301
|
+
event: "Notification",
|
|
2302
|
+
matcher: "",
|
|
2303
|
+
tags: ["messages", "dm", "context", "inject", "agent-teams"]
|
|
2206
2304
|
}
|
|
2207
2305
|
];
|
|
2208
2306
|
});
|
|
@@ -4152,6 +4250,264 @@ var init_profiles = __esm(() => {
|
|
|
4152
4250
|
PROFILES_DIR = join2(homedir2(), ".hooks", "profiles");
|
|
4153
4251
|
});
|
|
4154
4252
|
|
|
4253
|
+
// src/db/schema.ts
|
|
4254
|
+
var CREATE_HOOK_EVENTS_TABLE = `
|
|
4255
|
+
CREATE TABLE IF NOT EXISTS hook_events (
|
|
4256
|
+
id TEXT PRIMARY KEY,
|
|
4257
|
+
timestamp TEXT NOT NULL,
|
|
4258
|
+
session_id TEXT NOT NULL,
|
|
4259
|
+
hook_name TEXT NOT NULL,
|
|
4260
|
+
event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification')),
|
|
4261
|
+
tool_name TEXT,
|
|
4262
|
+
tool_input TEXT,
|
|
4263
|
+
result TEXT CHECK (result IN ('continue', 'block', NULL)),
|
|
4264
|
+
error TEXT,
|
|
4265
|
+
duration_ms INTEGER,
|
|
4266
|
+
project_dir TEXT,
|
|
4267
|
+
metadata TEXT
|
|
4268
|
+
)
|
|
4269
|
+
`, CREATE_INDEXES;
|
|
4270
|
+
var init_schema = __esm(() => {
|
|
4271
|
+
CREATE_INDEXES = [
|
|
4272
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_timestamp ON hook_events (timestamp)`,
|
|
4273
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_session_id ON hook_events (session_id)`,
|
|
4274
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_hook_name ON hook_events (hook_name)`,
|
|
4275
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_event_type ON hook_events (event_type)`,
|
|
4276
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_errors ON hook_events (timestamp) WHERE error IS NOT NULL`
|
|
4277
|
+
];
|
|
4278
|
+
});
|
|
4279
|
+
|
|
4280
|
+
// src/db/migrations/001_initial.ts
|
|
4281
|
+
function up(db) {
|
|
4282
|
+
db.exec(CREATE_HOOK_EVENTS_TABLE);
|
|
4283
|
+
for (const idx of CREATE_INDEXES) {
|
|
4284
|
+
db.exec(idx);
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
var init_001_initial = __esm(() => {
|
|
4288
|
+
init_schema();
|
|
4289
|
+
});
|
|
4290
|
+
|
|
4291
|
+
// src/db/migrations/index.ts
|
|
4292
|
+
function ensureMigrationsTable(db) {
|
|
4293
|
+
db.exec(`
|
|
4294
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
4295
|
+
version TEXT PRIMARY KEY,
|
|
4296
|
+
applied_at TEXT NOT NULL
|
|
4297
|
+
)
|
|
4298
|
+
`);
|
|
4299
|
+
}
|
|
4300
|
+
function getApplied(db) {
|
|
4301
|
+
const rows = db.query("SELECT version FROM schema_migrations").all();
|
|
4302
|
+
return new Set(rows.map((r) => r.version));
|
|
4303
|
+
}
|
|
4304
|
+
function runMigrations(db) {
|
|
4305
|
+
ensureMigrationsTable(db);
|
|
4306
|
+
const applied = getApplied(db);
|
|
4307
|
+
for (const migration of MIGRATIONS) {
|
|
4308
|
+
if (applied.has(migration.version))
|
|
4309
|
+
continue;
|
|
4310
|
+
migration.up(db);
|
|
4311
|
+
db.run("INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)", [
|
|
4312
|
+
migration.version,
|
|
4313
|
+
new Date().toISOString()
|
|
4314
|
+
]);
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
var MIGRATIONS;
|
|
4318
|
+
var init_migrations = __esm(() => {
|
|
4319
|
+
init_001_initial();
|
|
4320
|
+
MIGRATIONS = [{ version: "001_initial", up }];
|
|
4321
|
+
});
|
|
4322
|
+
|
|
4323
|
+
// src/db/legacy-import.ts
|
|
4324
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
4325
|
+
import { join as join3 } from "path";
|
|
4326
|
+
import { homedir as homedir3 } from "os";
|
|
4327
|
+
function ensureMetaTable(db) {
|
|
4328
|
+
db.exec(`
|
|
4329
|
+
CREATE TABLE IF NOT EXISTS _meta (
|
|
4330
|
+
key TEXT PRIMARY KEY,
|
|
4331
|
+
value TEXT NOT NULL
|
|
4332
|
+
)
|
|
4333
|
+
`);
|
|
4334
|
+
}
|
|
4335
|
+
function isAlreadyDone(db) {
|
|
4336
|
+
ensureMetaTable(db);
|
|
4337
|
+
const row = db.query("SELECT value FROM _meta WHERE key = ?").get(META_KEY);
|
|
4338
|
+
return row?.value === "1";
|
|
4339
|
+
}
|
|
4340
|
+
function markDone(db) {
|
|
4341
|
+
db.run("INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)", [META_KEY, "1"]);
|
|
4342
|
+
}
|
|
4343
|
+
function nanoid() {
|
|
4344
|
+
return crypto.randomUUID().replace(/-/g, "").slice(0, 21);
|
|
4345
|
+
}
|
|
4346
|
+
function importJsonlFile(db, filePath) {
|
|
4347
|
+
let count = 0;
|
|
4348
|
+
try {
|
|
4349
|
+
const lines = readFileSync3(filePath, "utf-8").split(`
|
|
4350
|
+
`).filter(Boolean);
|
|
4351
|
+
for (const line of lines) {
|
|
4352
|
+
try {
|
|
4353
|
+
const entry = JSON.parse(line);
|
|
4354
|
+
db.run(`INSERT OR IGNORE INTO hook_events
|
|
4355
|
+
(id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, project_dir)
|
|
4356
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
4357
|
+
nanoid(),
|
|
4358
|
+
entry.timestamp ?? new Date().toISOString(),
|
|
4359
|
+
entry.session_id ?? "legacy",
|
|
4360
|
+
"sessionlog",
|
|
4361
|
+
"PostToolUse",
|
|
4362
|
+
entry.tool_name ?? null,
|
|
4363
|
+
entry.tool_input ? String(entry.tool_input).slice(0, 500) : null,
|
|
4364
|
+
null
|
|
4365
|
+
]);
|
|
4366
|
+
count++;
|
|
4367
|
+
} catch {}
|
|
4368
|
+
}
|
|
4369
|
+
} catch {}
|
|
4370
|
+
return count;
|
|
4371
|
+
}
|
|
4372
|
+
function importErrorsLog(db, filePath) {
|
|
4373
|
+
let count = 0;
|
|
4374
|
+
try {
|
|
4375
|
+
const lines = readFileSync3(filePath, "utf-8").split(`
|
|
4376
|
+
`).filter(Boolean);
|
|
4377
|
+
const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
|
|
4378
|
+
for (const line of lines) {
|
|
4379
|
+
try {
|
|
4380
|
+
const m = line.match(linePattern);
|
|
4381
|
+
if (!m)
|
|
4382
|
+
continue;
|
|
4383
|
+
const [, timestamp, sessionPrefix, , errorMsg] = m;
|
|
4384
|
+
db.run(`INSERT OR IGNORE INTO hook_events
|
|
4385
|
+
(id, timestamp, session_id, hook_name, event_type, error)
|
|
4386
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [
|
|
4387
|
+
nanoid(),
|
|
4388
|
+
timestamp,
|
|
4389
|
+
sessionPrefix ? `legacy-${sessionPrefix}` : "legacy",
|
|
4390
|
+
"errornotify",
|
|
4391
|
+
"PostToolUse",
|
|
4392
|
+
errorMsg.slice(0, 500)
|
|
4393
|
+
]);
|
|
4394
|
+
count++;
|
|
4395
|
+
} catch {}
|
|
4396
|
+
}
|
|
4397
|
+
} catch {}
|
|
4398
|
+
return count;
|
|
4399
|
+
}
|
|
4400
|
+
function runLegacyImport(db) {
|
|
4401
|
+
try {
|
|
4402
|
+
if (isAlreadyDone(db))
|
|
4403
|
+
return;
|
|
4404
|
+
let total = 0;
|
|
4405
|
+
const claudeProjectsDir = join3(homedir3(), ".claude", "projects");
|
|
4406
|
+
if (existsSync3(claudeProjectsDir)) {
|
|
4407
|
+
try {
|
|
4408
|
+
const projectDirs = readdirSync2(claudeProjectsDir);
|
|
4409
|
+
for (const dir of projectDirs) {
|
|
4410
|
+
const projectDir = join3(claudeProjectsDir, dir);
|
|
4411
|
+
try {
|
|
4412
|
+
const files = readdirSync2(projectDir);
|
|
4413
|
+
for (const file of files) {
|
|
4414
|
+
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
4415
|
+
total += importJsonlFile(db, join3(projectDir, file));
|
|
4416
|
+
}
|
|
4417
|
+
if (file === "errors.log") {
|
|
4418
|
+
total += importErrorsLog(db, join3(projectDir, file));
|
|
4419
|
+
}
|
|
4420
|
+
}
|
|
4421
|
+
} catch {}
|
|
4422
|
+
}
|
|
4423
|
+
} catch {}
|
|
4424
|
+
}
|
|
4425
|
+
markDone(db);
|
|
4426
|
+
if (total > 0) {
|
|
4427
|
+
process.stderr.write(`[hooks] Imported ${total} legacy log entries into SQLite.
|
|
4428
|
+
`);
|
|
4429
|
+
}
|
|
4430
|
+
} catch (err) {
|
|
4431
|
+
process.stderr.write(`[hooks] Legacy import failed (non-fatal): ${err}
|
|
4432
|
+
`);
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
4435
|
+
var META_KEY = "legacy_import_done";
|
|
4436
|
+
var init_legacy_import = () => {};
|
|
4437
|
+
|
|
4438
|
+
// src/db/retention.ts
|
|
4439
|
+
function runRetention(db, days) {
|
|
4440
|
+
const envDays = parseInt(process.env.HOOKS_RETENTION_DAYS ?? "30");
|
|
4441
|
+
const retentionDays = days ?? (isNaN(envDays) || envDays <= 0 ? 30 : envDays);
|
|
4442
|
+
const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000).toISOString();
|
|
4443
|
+
try {
|
|
4444
|
+
db.run("DELETE FROM hook_events WHERE timestamp < ?", [cutoff]);
|
|
4445
|
+
const changes = db.query("SELECT changes() as changes").get()?.changes ?? 0;
|
|
4446
|
+
return changes;
|
|
4447
|
+
} catch {
|
|
4448
|
+
return 0;
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4451
|
+
|
|
4452
|
+
// src/db/index.ts
|
|
4453
|
+
var exports_db = {};
|
|
4454
|
+
__export(exports_db, {
|
|
4455
|
+
getDbPath: () => getDbPath,
|
|
4456
|
+
getDb: () => getDb,
|
|
4457
|
+
createTestDb: () => createTestDb,
|
|
4458
|
+
closeDb: () => closeDb
|
|
4459
|
+
});
|
|
4460
|
+
import { Database } from "bun:sqlite";
|
|
4461
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
4462
|
+
import { join as join4 } from "path";
|
|
4463
|
+
import { homedir as homedir4 } from "os";
|
|
4464
|
+
function getDbPath() {
|
|
4465
|
+
if (process.env.HOOKS_DB_PATH) {
|
|
4466
|
+
return process.env.HOOKS_DB_PATH;
|
|
4467
|
+
}
|
|
4468
|
+
const dataDir = process.env.HOOKS_DATA_DIR ?? join4(homedir4(), ".hooks");
|
|
4469
|
+
return join4(dataDir, "hooks.db");
|
|
4470
|
+
}
|
|
4471
|
+
function ensureDir(dbPath) {
|
|
4472
|
+
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
4473
|
+
if (dir && !existsSync4(dir)) {
|
|
4474
|
+
mkdirSync3(dir, { recursive: true });
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
function getDb() {
|
|
4478
|
+
if (instance)
|
|
4479
|
+
return instance;
|
|
4480
|
+
const dbPath = getDbPath();
|
|
4481
|
+
const isNew = dbPath === ":memory:" || !existsSync4(dbPath);
|
|
4482
|
+
ensureDir(dbPath);
|
|
4483
|
+
instance = new Database(dbPath);
|
|
4484
|
+
instance.exec("PRAGMA journal_mode=WAL");
|
|
4485
|
+
instance.exec("PRAGMA foreign_keys=ON");
|
|
4486
|
+
runMigrations(instance);
|
|
4487
|
+
runRetention(instance);
|
|
4488
|
+
if (isNew) {
|
|
4489
|
+
runLegacyImport(instance);
|
|
4490
|
+
}
|
|
4491
|
+
return instance;
|
|
4492
|
+
}
|
|
4493
|
+
function closeDb() {
|
|
4494
|
+
if (instance) {
|
|
4495
|
+
instance.close();
|
|
4496
|
+
instance = null;
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
function createTestDb() {
|
|
4500
|
+
const db = new Database(":memory:");
|
|
4501
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
4502
|
+
db.exec("PRAGMA foreign_keys=ON");
|
|
4503
|
+
return db;
|
|
4504
|
+
}
|
|
4505
|
+
var instance = null;
|
|
4506
|
+
var init_db = __esm(() => {
|
|
4507
|
+
init_migrations();
|
|
4508
|
+
init_legacy_import();
|
|
4509
|
+
});
|
|
4510
|
+
|
|
4155
4511
|
// src/mcp/server.ts
|
|
4156
4512
|
var exports_server = {};
|
|
4157
4513
|
__export(exports_server, {
|
|
@@ -4165,8 +4521,8 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
4165
4521
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4166
4522
|
import { z } from "zod";
|
|
4167
4523
|
import { createServer } from "http";
|
|
4168
|
-
import { existsSync as
|
|
4169
|
-
import { join as
|
|
4524
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
4525
|
+
import { join as join5, dirname as dirname2 } from "path";
|
|
4170
4526
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4171
4527
|
function formatInstallResults(results, extra) {
|
|
4172
4528
|
const installed = results.filter((r) => r.success).map((r) => r.hook);
|
|
@@ -4263,7 +4619,7 @@ function createHooksServer() {
|
|
|
4263
4619
|
const settingsPath = getSettingsPath(scope);
|
|
4264
4620
|
const issues = [];
|
|
4265
4621
|
const healthy = [];
|
|
4266
|
-
const settingsExist =
|
|
4622
|
+
const settingsExist = existsSync5(settingsPath);
|
|
4267
4623
|
if (!settingsExist) {
|
|
4268
4624
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
4269
4625
|
}
|
|
@@ -4276,13 +4632,13 @@ function createHooksServer() {
|
|
|
4276
4632
|
continue;
|
|
4277
4633
|
}
|
|
4278
4634
|
const hookDir = getHookPath(name);
|
|
4279
|
-
if (!
|
|
4635
|
+
if (!existsSync5(join5(hookDir, "src", "hook.ts"))) {
|
|
4280
4636
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
4281
4637
|
hookHealthy = false;
|
|
4282
4638
|
}
|
|
4283
4639
|
if (meta && settingsExist) {
|
|
4284
4640
|
try {
|
|
4285
|
-
const settings = JSON.parse(
|
|
4641
|
+
const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
|
|
4286
4642
|
const eventHooks = settings.hooks?.[meta.event] || [];
|
|
4287
4643
|
const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
|
|
4288
4644
|
const match = h.command?.match(/^hooks run (\w+)/);
|
|
@@ -4313,10 +4669,10 @@ function createHooksServer() {
|
|
|
4313
4669
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
4314
4670
|
}
|
|
4315
4671
|
const hookPath = getHookPath(name);
|
|
4316
|
-
const readmePath =
|
|
4672
|
+
const readmePath = join5(hookPath, "README.md");
|
|
4317
4673
|
let readme = "";
|
|
4318
|
-
if (
|
|
4319
|
-
readme =
|
|
4674
|
+
if (existsSync5(readmePath)) {
|
|
4675
|
+
readme = readFileSync4(readmePath, "utf-8");
|
|
4320
4676
|
}
|
|
4321
4677
|
return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
|
|
4322
4678
|
}
|
|
@@ -4365,8 +4721,8 @@ function createHooksServer() {
|
|
|
4365
4721
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
4366
4722
|
}
|
|
4367
4723
|
const hookDir = getHookPath(name);
|
|
4368
|
-
const hookScript =
|
|
4369
|
-
if (!
|
|
4724
|
+
const hookScript = join5(hookDir, "src", "hook.ts");
|
|
4725
|
+
if (!existsSync5(hookScript)) {
|
|
4370
4726
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook script not found: ${hookScript}` }) }] };
|
|
4371
4727
|
}
|
|
4372
4728
|
let hookInput = { ...input };
|
|
@@ -4457,7 +4813,7 @@ function createHooksServer() {
|
|
|
4457
4813
|
const ctx = {
|
|
4458
4814
|
scope,
|
|
4459
4815
|
settings_path: settingsPath,
|
|
4460
|
-
settings_exists:
|
|
4816
|
+
settings_exists: existsSync5(settingsPath),
|
|
4461
4817
|
registered_hooks: hooks,
|
|
4462
4818
|
hook_count: hooks.length,
|
|
4463
4819
|
healthy,
|
|
@@ -4495,8 +4851,8 @@ function createHooksServer() {
|
|
|
4495
4851
|
const input = { tool_name, tool_input };
|
|
4496
4852
|
const results = await Promise.all(matchingHooks.map(async (name) => {
|
|
4497
4853
|
const hookDir = getHookPath(name);
|
|
4498
|
-
const hookScript =
|
|
4499
|
-
if (!
|
|
4854
|
+
const hookScript = join5(hookDir, "src", "hook.ts");
|
|
4855
|
+
if (!existsSync5(hookScript))
|
|
4500
4856
|
return { name, decision: "approve", error: "script not found" };
|
|
4501
4857
|
const proc = Bun.spawn(["bun", "run", hookScript], {
|
|
4502
4858
|
stdin: new Response(JSON.stringify(input)),
|
|
@@ -4564,8 +4920,8 @@ function createHooksServer() {
|
|
|
4564
4920
|
const meta = getHook(name);
|
|
4565
4921
|
if (!meta)
|
|
4566
4922
|
return { name, error: `Hook '${name}' not found` };
|
|
4567
|
-
const hookScript =
|
|
4568
|
-
if (!
|
|
4923
|
+
const hookScript = join5(getHookPath(name), "src", "hook.ts");
|
|
4924
|
+
if (!existsSync5(hookScript))
|
|
4569
4925
|
return { name, error: "script not found" };
|
|
4570
4926
|
const proc = Bun.spawn(["bun", "run", hookScript], {
|
|
4571
4927
|
stdin: new Response(JSON.stringify(input)),
|
|
@@ -4598,8 +4954,8 @@ function createHooksServer() {
|
|
|
4598
4954
|
const settingsPath = getSettingsPath(scope);
|
|
4599
4955
|
let settings = {};
|
|
4600
4956
|
try {
|
|
4601
|
-
if (
|
|
4602
|
-
settings = JSON.parse(
|
|
4957
|
+
if (existsSync5(settingsPath))
|
|
4958
|
+
settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
|
|
4603
4959
|
} catch {}
|
|
4604
4960
|
if (!settings.hooks)
|
|
4605
4961
|
settings.hooks = {};
|
|
@@ -4607,9 +4963,9 @@ function createHooksServer() {
|
|
|
4607
4963
|
if (!disabled.includes(name))
|
|
4608
4964
|
disabled.push(name);
|
|
4609
4965
|
settings.hooks.__disabled = disabled;
|
|
4610
|
-
const { writeFileSync: writeFileSync3, mkdirSync:
|
|
4966
|
+
const { writeFileSync: writeFileSync3, mkdirSync: mkdirSync4 } = await import("fs");
|
|
4611
4967
|
const { dirname: dirname3 } = await import("path");
|
|
4612
|
-
|
|
4968
|
+
mkdirSync4(dirname3(settingsPath), { recursive: true });
|
|
4613
4969
|
writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
4614
4970
|
`);
|
|
4615
4971
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
|
|
@@ -4621,8 +4977,8 @@ function createHooksServer() {
|
|
|
4621
4977
|
const settingsPath = getSettingsPath(scope);
|
|
4622
4978
|
let settings = {};
|
|
4623
4979
|
try {
|
|
4624
|
-
if (
|
|
4625
|
-
settings = JSON.parse(
|
|
4980
|
+
if (existsSync5(settingsPath))
|
|
4981
|
+
settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
|
|
4626
4982
|
} catch {}
|
|
4627
4983
|
if (settings.hooks?.__disabled) {
|
|
4628
4984
|
settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
|
|
@@ -4645,6 +5001,104 @@ function createHooksServer() {
|
|
|
4645
5001
|
const profiles = listProfiles();
|
|
4646
5002
|
return { content: [{ type: "text", text: JSON.stringify(profiles) }] };
|
|
4647
5003
|
});
|
|
5004
|
+
server.tool("hooks_log_list", "List hook events from SQLite (~/.hooks/hooks.db). Filter by hook name, session ID, or time range.", {
|
|
5005
|
+
hook_name: z.string().optional().describe("Filter by hook name (e.g. 'sessionlog', 'costwatch')"),
|
|
5006
|
+
session_id: z.string().optional().describe("Filter by session ID prefix"),
|
|
5007
|
+
limit: z.number().default(50).describe("Max number of events to return"),
|
|
5008
|
+
since: z.string().optional().describe("ISO timestamp or duration string (e.g. '1h', '30m', '7d') to filter from")
|
|
5009
|
+
}, async ({ hook_name, session_id, limit, since }) => {
|
|
5010
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
5011
|
+
const db = getDb2();
|
|
5012
|
+
function parseDuration(s) {
|
|
5013
|
+
const m = s.match(/^(\d+)(s|m|h|d)$/);
|
|
5014
|
+
if (!m)
|
|
5015
|
+
return null;
|
|
5016
|
+
const n = parseInt(m[1]);
|
|
5017
|
+
const ms = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]];
|
|
5018
|
+
return new Date(Date.now() - n * ms).toISOString();
|
|
5019
|
+
}
|
|
5020
|
+
let sql = "SELECT * FROM hook_events WHERE 1=1";
|
|
5021
|
+
const params = [];
|
|
5022
|
+
if (hook_name) {
|
|
5023
|
+
sql += " AND hook_name = ?";
|
|
5024
|
+
params.push(hook_name);
|
|
5025
|
+
}
|
|
5026
|
+
if (session_id) {
|
|
5027
|
+
sql += " AND session_id LIKE ?";
|
|
5028
|
+
params.push(`${session_id}%`);
|
|
5029
|
+
}
|
|
5030
|
+
if (since) {
|
|
5031
|
+
const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
|
|
5032
|
+
if (ts) {
|
|
5033
|
+
sql += " AND timestamp >= ?";
|
|
5034
|
+
params.push(ts);
|
|
5035
|
+
}
|
|
5036
|
+
}
|
|
5037
|
+
sql += " ORDER BY timestamp DESC LIMIT ?";
|
|
5038
|
+
params.push(limit);
|
|
5039
|
+
const rows = db.query(sql).all(...params);
|
|
5040
|
+
return { content: [{ type: "text", text: JSON.stringify({ events: rows, count: rows.length }) }] };
|
|
5041
|
+
});
|
|
5042
|
+
server.tool("hooks_log_tail", "Show the most recent hook events from SQLite.", {
|
|
5043
|
+
n: z.number().default(20).describe("Number of most recent events to return")
|
|
5044
|
+
}, async ({ n }) => {
|
|
5045
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
5046
|
+
const db = getDb2();
|
|
5047
|
+
const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(n);
|
|
5048
|
+
return { content: [{ type: "text", text: JSON.stringify({ events: rows, count: rows.length }) }] };
|
|
5049
|
+
});
|
|
5050
|
+
server.tool("hooks_log_errors", "Show hook events that contain errors, optionally filtered by time range.", {
|
|
5051
|
+
since: z.string().default("24h").describe("Duration string (e.g. '1h', '30m', '7d') or ISO timestamp"),
|
|
5052
|
+
limit: z.number().default(50).describe("Max number of error events to return")
|
|
5053
|
+
}, async ({ since, limit }) => {
|
|
5054
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
5055
|
+
const db = getDb2();
|
|
5056
|
+
function parseDuration(s) {
|
|
5057
|
+
const m = s.match(/^(\d+)(s|m|h|d)$/);
|
|
5058
|
+
if (!m)
|
|
5059
|
+
return s;
|
|
5060
|
+
const n = parseInt(m[1]);
|
|
5061
|
+
const ms = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]];
|
|
5062
|
+
return new Date(Date.now() - n * ms).toISOString();
|
|
5063
|
+
}
|
|
5064
|
+
const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
|
|
5065
|
+
const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(ts, limit);
|
|
5066
|
+
return { content: [{ type: "text", text: JSON.stringify({ events: rows, count: rows.length }) }] };
|
|
5067
|
+
});
|
|
5068
|
+
server.tool("hooks_log_summary", "Summarize hook execution: counts per hook, error rates, and recent activity.", {
|
|
5069
|
+
since: z.string().default("24h").describe("Duration string (e.g. '1h', '24h', '7d') or ISO timestamp")
|
|
5070
|
+
}, async ({ since }) => {
|
|
5071
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
5072
|
+
const db = getDb2();
|
|
5073
|
+
function parseDuration(s) {
|
|
5074
|
+
const m = s.match(/^(\d+)(s|m|h|d)$/);
|
|
5075
|
+
if (!m)
|
|
5076
|
+
return s;
|
|
5077
|
+
const n = parseInt(m[1]);
|
|
5078
|
+
const ms = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]];
|
|
5079
|
+
return new Date(Date.now() - n * ms).toISOString();
|
|
5080
|
+
}
|
|
5081
|
+
const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
|
|
5082
|
+
const totals = db.query("SELECT hook_name, COUNT(*) as total, SUM(CASE WHEN error IS NOT NULL THEN 1 ELSE 0 END) as errors FROM hook_events WHERE timestamp >= ? GROUP BY hook_name ORDER BY total DESC").all(ts);
|
|
5083
|
+
const summary = totals.map((r) => ({
|
|
5084
|
+
hook_name: r.hook_name,
|
|
5085
|
+
total: r.total,
|
|
5086
|
+
errors: r.errors,
|
|
5087
|
+
error_rate: r.total > 0 ? (r.errors / r.total * 100).toFixed(1) + "%" : "0%"
|
|
5088
|
+
}));
|
|
5089
|
+
const grandTotal = totals.reduce((s, r) => s + r.total, 0);
|
|
5090
|
+
const grandErrors = totals.reduce((s, r) => s + r.errors, 0);
|
|
5091
|
+
return {
|
|
5092
|
+
content: [{
|
|
5093
|
+
type: "text",
|
|
5094
|
+
text: JSON.stringify({
|
|
5095
|
+
since: ts,
|
|
5096
|
+
hooks: summary,
|
|
5097
|
+
totals: { events: grandTotal, errors: grandErrors, hooks_active: totals.length }
|
|
5098
|
+
})
|
|
5099
|
+
}]
|
|
5100
|
+
};
|
|
5101
|
+
});
|
|
4648
5102
|
return server;
|
|
4649
5103
|
}
|
|
4650
5104
|
async function startSSEServer(port = MCP_PORT) {
|
|
@@ -4690,7 +5144,7 @@ var init_server = __esm(() => {
|
|
|
4690
5144
|
init_installer();
|
|
4691
5145
|
init_profiles();
|
|
4692
5146
|
__dirname3 = dirname2(fileURLToPath2(import.meta.url));
|
|
4693
|
-
pkg = JSON.parse(
|
|
5147
|
+
pkg = JSON.parse(readFileSync4(join5(__dirname3, "..", "..", "package.json"), "utf-8"));
|
|
4694
5148
|
});
|
|
4695
5149
|
|
|
4696
5150
|
// src/cli/index.tsx
|
|
@@ -4714,8 +5168,8 @@ var {
|
|
|
4714
5168
|
|
|
4715
5169
|
// src/cli/index.tsx
|
|
4716
5170
|
import chalk2 from "chalk";
|
|
4717
|
-
import { existsSync as
|
|
4718
|
-
import { join as
|
|
5171
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
5172
|
+
import { join as join6, dirname as dirname3 } from "path";
|
|
4719
5173
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4720
5174
|
|
|
4721
5175
|
// src/cli/components/App.tsx
|
|
@@ -5893,8 +6347,8 @@ init_installer();
|
|
|
5893
6347
|
init_profiles();
|
|
5894
6348
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
5895
6349
|
var __dirname4 = dirname3(fileURLToPath3(import.meta.url));
|
|
5896
|
-
var pkgPath =
|
|
5897
|
-
var pkg2 = JSON.parse(
|
|
6350
|
+
var pkgPath = existsSync6(join6(__dirname4, "..", "package.json")) ? join6(__dirname4, "..", "package.json") : join6(__dirname4, "..", "..", "package.json");
|
|
6351
|
+
var pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
5898
6352
|
var program2 = new Command;
|
|
5899
6353
|
function resolveScope(options) {
|
|
5900
6354
|
if (options.project)
|
|
@@ -5964,8 +6418,8 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
5964
6418
|
process.exit(1);
|
|
5965
6419
|
}
|
|
5966
6420
|
const hookDir = getHookPath(hook);
|
|
5967
|
-
const hookScript =
|
|
5968
|
-
if (!
|
|
6421
|
+
const hookScript = join6(hookDir, "src", "hook.ts");
|
|
6422
|
+
if (!existsSync6(hookScript)) {
|
|
5969
6423
|
console.error(JSON.stringify({ error: `Hook script not found: ${hookScript}` }));
|
|
5970
6424
|
process.exit(1);
|
|
5971
6425
|
}
|
|
@@ -6265,7 +6719,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
6265
6719
|
const settingsPath = getSettingsPath(scope);
|
|
6266
6720
|
const issues = [];
|
|
6267
6721
|
const healthy = [];
|
|
6268
|
-
const settingsExist =
|
|
6722
|
+
const settingsExist = existsSync6(settingsPath);
|
|
6269
6723
|
if (!settingsExist) {
|
|
6270
6724
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
6271
6725
|
}
|
|
@@ -6279,14 +6733,14 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
6279
6733
|
continue;
|
|
6280
6734
|
}
|
|
6281
6735
|
const hookDir = getHookPath(name);
|
|
6282
|
-
const hookScript =
|
|
6283
|
-
if (!
|
|
6736
|
+
const hookScript = join6(hookDir, "src", "hook.ts");
|
|
6737
|
+
if (!existsSync6(hookScript)) {
|
|
6284
6738
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
6285
6739
|
hookHealthy = false;
|
|
6286
6740
|
}
|
|
6287
6741
|
if (meta && settingsExist) {
|
|
6288
6742
|
try {
|
|
6289
|
-
const settings = JSON.parse(
|
|
6743
|
+
const settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
|
|
6290
6744
|
const eventHooks = settings.hooks?.[meta.event] || [];
|
|
6291
6745
|
const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
|
|
6292
6746
|
const match = h.command?.match(/^hooks run (\w+)/);
|
|
@@ -6384,10 +6838,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
6384
6838
|
return;
|
|
6385
6839
|
}
|
|
6386
6840
|
const hookPath = getHookPath(hook);
|
|
6387
|
-
const readmePath =
|
|
6841
|
+
const readmePath = join6(hookPath, "README.md");
|
|
6388
6842
|
let readme = "";
|
|
6389
|
-
if (
|
|
6390
|
-
readme =
|
|
6843
|
+
if (existsSync6(readmePath)) {
|
|
6844
|
+
readme = readFileSync5(readmePath, "utf-8");
|
|
6391
6845
|
}
|
|
6392
6846
|
if (options.json) {
|
|
6393
6847
|
console.log(JSON.stringify({ ...meta, readme }));
|
|
@@ -6569,9 +7023,9 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
6569
7023
|
if (file === "-") {
|
|
6570
7024
|
raw = await new Response(Bun.stdin.stream()).text();
|
|
6571
7025
|
} else {
|
|
6572
|
-
const { readFileSync:
|
|
7026
|
+
const { readFileSync: readFileSync6 } = await import("fs");
|
|
6573
7027
|
try {
|
|
6574
|
-
raw =
|
|
7028
|
+
raw = readFileSync6(file, "utf-8");
|
|
6575
7029
|
} catch {
|
|
6576
7030
|
if (options.json) {
|
|
6577
7031
|
console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
|
|
@@ -6602,6 +7056,154 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
6602
7056
|
console.log(chalk2.dim(` Skipped ${result.skipped} (already exist or invalid)`));
|
|
6603
7057
|
}
|
|
6604
7058
|
});
|
|
7059
|
+
var logCmd = program2.command("log").description("Query hook event logs from SQLite (~/.hooks/hooks.db)");
|
|
7060
|
+
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) => {
|
|
7061
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
7062
|
+
const db = getDb2();
|
|
7063
|
+
const limit = parseInt(options.limit) || 50;
|
|
7064
|
+
let sql = "SELECT * FROM hook_events WHERE 1=1";
|
|
7065
|
+
const params = [];
|
|
7066
|
+
if (options.hook) {
|
|
7067
|
+
sql += " AND hook_name = ?";
|
|
7068
|
+
params.push(options.hook);
|
|
7069
|
+
}
|
|
7070
|
+
if (options.session) {
|
|
7071
|
+
sql += " AND session_id LIKE ?";
|
|
7072
|
+
params.push(`${options.session}%`);
|
|
7073
|
+
}
|
|
7074
|
+
sql += " ORDER BY timestamp DESC LIMIT ?";
|
|
7075
|
+
params.push(String(limit));
|
|
7076
|
+
const rows = db.query(sql).all(...params);
|
|
7077
|
+
if (options.json) {
|
|
7078
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
7079
|
+
return;
|
|
7080
|
+
}
|
|
7081
|
+
if (rows.length === 0) {
|
|
7082
|
+
console.log(chalk2.dim("No events found."));
|
|
7083
|
+
return;
|
|
7084
|
+
}
|
|
7085
|
+
console.log(chalk2.bold(`
|
|
7086
|
+
Hook Events (${rows.length})
|
|
7087
|
+
`));
|
|
7088
|
+
for (const row of rows) {
|
|
7089
|
+
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
7090
|
+
const err = row.error ? chalk2.red(` ERR: ${row.error.slice(0, 60)}`) : "";
|
|
7091
|
+
const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
|
|
7092
|
+
console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))}${tool}${err}`);
|
|
7093
|
+
}
|
|
7094
|
+
console.log();
|
|
7095
|
+
});
|
|
7096
|
+
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) => {
|
|
7097
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
7098
|
+
const db = getDb2();
|
|
7099
|
+
const limit = parseInt(options.limit) || 50;
|
|
7100
|
+
const q = `%${text}%`;
|
|
7101
|
+
const rows = db.query("SELECT * FROM hook_events WHERE tool_input LIKE ? OR error LIKE ? ORDER BY timestamp DESC LIMIT ?").all(q, q, limit);
|
|
7102
|
+
if (options.json) {
|
|
7103
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
7104
|
+
return;
|
|
7105
|
+
}
|
|
7106
|
+
if (rows.length === 0) {
|
|
7107
|
+
console.log(chalk2.dim(`No events matching "${text}".`));
|
|
7108
|
+
return;
|
|
7109
|
+
}
|
|
7110
|
+
console.log(chalk2.bold(`
|
|
7111
|
+
Search results for "${text}" (${rows.length})
|
|
7112
|
+
`));
|
|
7113
|
+
for (const row of rows) {
|
|
7114
|
+
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
7115
|
+
const snippet = (row.tool_input || row.error || "").slice(0, 80);
|
|
7116
|
+
console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.dim(snippet)}`);
|
|
7117
|
+
}
|
|
7118
|
+
console.log();
|
|
7119
|
+
});
|
|
7120
|
+
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) => {
|
|
7121
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
7122
|
+
const db = getDb2();
|
|
7123
|
+
const limit = parseInt(options.n) || 20;
|
|
7124
|
+
const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(limit);
|
|
7125
|
+
if (options.json) {
|
|
7126
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
7127
|
+
return;
|
|
7128
|
+
}
|
|
7129
|
+
if (rows.length === 0) {
|
|
7130
|
+
console.log(chalk2.dim("No events yet."));
|
|
7131
|
+
return;
|
|
7132
|
+
}
|
|
7133
|
+
console.log(chalk2.bold(`
|
|
7134
|
+
Last ${rows.length} events
|
|
7135
|
+
`));
|
|
7136
|
+
for (const row of rows) {
|
|
7137
|
+
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
7138
|
+
const err = row.error ? chalk2.red(` \u2717 ${row.error.slice(0, 60)}`) : "";
|
|
7139
|
+
const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
|
|
7140
|
+
console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))}${tool}${err}`);
|
|
7141
|
+
}
|
|
7142
|
+
console.log();
|
|
7143
|
+
});
|
|
7144
|
+
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) => {
|
|
7145
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
7146
|
+
const db = getDb2();
|
|
7147
|
+
const limit = parseInt(options.limit) || 50;
|
|
7148
|
+
function parseDuration(s) {
|
|
7149
|
+
const m = s.match(/^(\d+)(s|m|h|d)$/);
|
|
7150
|
+
if (!m)
|
|
7151
|
+
return 86400000;
|
|
7152
|
+
const n = parseInt(m[1]);
|
|
7153
|
+
switch (m[2]) {
|
|
7154
|
+
case "s":
|
|
7155
|
+
return n * 1000;
|
|
7156
|
+
case "m":
|
|
7157
|
+
return n * 60 * 1000;
|
|
7158
|
+
case "h":
|
|
7159
|
+
return n * 60 * 60 * 1000;
|
|
7160
|
+
case "d":
|
|
7161
|
+
return n * 24 * 60 * 60 * 1000;
|
|
7162
|
+
default:
|
|
7163
|
+
return 86400000;
|
|
7164
|
+
}
|
|
7165
|
+
}
|
|
7166
|
+
const since = new Date(Date.now() - parseDuration(options.since)).toISOString();
|
|
7167
|
+
const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(since, limit);
|
|
7168
|
+
if (options.json) {
|
|
7169
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
7170
|
+
return;
|
|
7171
|
+
}
|
|
7172
|
+
if (rows.length === 0) {
|
|
7173
|
+
console.log(chalk2.dim(`No errors in the last ${options.since}.`));
|
|
7174
|
+
return;
|
|
7175
|
+
}
|
|
7176
|
+
console.log(chalk2.bold(`
|
|
7177
|
+
Errors (last ${options.since}, ${rows.length} found)
|
|
7178
|
+
`));
|
|
7179
|
+
for (const row of rows) {
|
|
7180
|
+
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
7181
|
+
console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.red(row.error.slice(0, 100))}`);
|
|
7182
|
+
}
|
|
7183
|
+
console.log();
|
|
7184
|
+
});
|
|
7185
|
+
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) => {
|
|
7186
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
7187
|
+
const db = getDb2();
|
|
7188
|
+
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();
|
|
7189
|
+
const count = countRow?.n ?? 0;
|
|
7190
|
+
if (count === 0) {
|
|
7191
|
+
console.log(chalk2.dim("Nothing to clear."));
|
|
7192
|
+
return;
|
|
7193
|
+
}
|
|
7194
|
+
if (!options.yes) {
|
|
7195
|
+
const scope = options.hook ? `hook "${options.hook}"` : "all hooks";
|
|
7196
|
+
console.log(chalk2.yellow(`About to delete ${count} event(s) for ${scope}.`));
|
|
7197
|
+
console.log(chalk2.dim("Re-run with --yes to confirm."));
|
|
7198
|
+
return;
|
|
7199
|
+
}
|
|
7200
|
+
if (options.hook) {
|
|
7201
|
+
db.run("DELETE FROM hook_events WHERE hook_name = ?", [options.hook]);
|
|
7202
|
+
} else {
|
|
7203
|
+
db.run("DELETE FROM hook_events");
|
|
7204
|
+
}
|
|
7205
|
+
console.log(chalk2.green(`\u2713 Cleared ${count} event(s).`));
|
|
7206
|
+
});
|
|
6605
7207
|
program2.command("mcp").option("-s, --stdio", "Use stdio transport (for agent MCP integration)", false).option("-p, --port <port>", "Port for SSE transport", "39427").description("Start MCP server for AI agent integration").action(async (options) => {
|
|
6606
7208
|
if (options.stdio) {
|
|
6607
7209
|
const { startStdioServer: startStdioServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|