@hasna/hooks 0.2.5 → 0.2.7
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 +749 -37
- package/dist/index.js +4 -4
- package/hooks/hook-agentmessages/bin/cli.ts +125 -0
- package/hooks/hook-checkdocs/bun.lock +25 -0
- package/hooks/hook-commandlog/src/hook.ts +15 -38
- package/hooks/hook-costwatch/src/hook.ts +39 -42
- package/hooks/hook-errornotify/src/hook.ts +20 -65
- package/hooks/hook-sessionlog/src/hook.ts +11 -52
- package/package.json +1 -1
package/bin/index.js
CHANGED
|
@@ -2147,7 +2147,7 @@ var init_registry = __esm(() => {
|
|
|
2147
2147
|
{
|
|
2148
2148
|
name: "sessionlog",
|
|
2149
2149
|
displayName: "Session Log",
|
|
2150
|
-
description: "Logs every tool call to .
|
|
2150
|
+
description: "Logs every tool call to SQLite (~/.hooks/hooks.db)",
|
|
2151
2151
|
version: "0.1.0",
|
|
2152
2152
|
category: "Observability",
|
|
2153
2153
|
event: "PostToolUse",
|
|
@@ -2157,7 +2157,7 @@ var init_registry = __esm(() => {
|
|
|
2157
2157
|
{
|
|
2158
2158
|
name: "commandlog",
|
|
2159
2159
|
displayName: "Command Log",
|
|
2160
|
-
description: "Logs every
|
|
2160
|
+
description: "Logs every Bash command to SQLite (~/.hooks/hooks.db)",
|
|
2161
2161
|
version: "0.1.0",
|
|
2162
2162
|
category: "Observability",
|
|
2163
2163
|
event: "PostToolUse",
|
|
@@ -2167,7 +2167,7 @@ var init_registry = __esm(() => {
|
|
|
2167
2167
|
{
|
|
2168
2168
|
name: "costwatch",
|
|
2169
2169
|
displayName: "Cost Watch",
|
|
2170
|
-
description: "Estimates session token usage and warns
|
|
2170
|
+
description: "Estimates session token usage, persists cost history to SQLite, and warns on budget overrun",
|
|
2171
2171
|
version: "0.1.0",
|
|
2172
2172
|
category: "Observability",
|
|
2173
2173
|
event: "Stop",
|
|
@@ -2177,7 +2177,7 @@ var init_registry = __esm(() => {
|
|
|
2177
2177
|
{
|
|
2178
2178
|
name: "errornotify",
|
|
2179
2179
|
displayName: "Error Notify",
|
|
2180
|
-
description: "Detects tool failures and logs errors to
|
|
2180
|
+
description: "Detects tool failures and logs errors to SQLite (~/.hooks/hooks.db)",
|
|
2181
2181
|
version: "0.1.0",
|
|
2182
2182
|
category: "Observability",
|
|
2183
2183
|
event: "PostToolUse",
|
|
@@ -4152,6 +4152,264 @@ var init_profiles = __esm(() => {
|
|
|
4152
4152
|
PROFILES_DIR = join2(homedir2(), ".hooks", "profiles");
|
|
4153
4153
|
});
|
|
4154
4154
|
|
|
4155
|
+
// src/db/schema.ts
|
|
4156
|
+
var CREATE_HOOK_EVENTS_TABLE = `
|
|
4157
|
+
CREATE TABLE IF NOT EXISTS hook_events (
|
|
4158
|
+
id TEXT PRIMARY KEY,
|
|
4159
|
+
timestamp TEXT NOT NULL,
|
|
4160
|
+
session_id TEXT NOT NULL,
|
|
4161
|
+
hook_name TEXT NOT NULL,
|
|
4162
|
+
event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification')),
|
|
4163
|
+
tool_name TEXT,
|
|
4164
|
+
tool_input TEXT,
|
|
4165
|
+
result TEXT CHECK (result IN ('continue', 'block', NULL)),
|
|
4166
|
+
error TEXT,
|
|
4167
|
+
duration_ms INTEGER,
|
|
4168
|
+
project_dir TEXT,
|
|
4169
|
+
metadata TEXT
|
|
4170
|
+
)
|
|
4171
|
+
`, CREATE_INDEXES;
|
|
4172
|
+
var init_schema = __esm(() => {
|
|
4173
|
+
CREATE_INDEXES = [
|
|
4174
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_timestamp ON hook_events (timestamp)`,
|
|
4175
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_session_id ON hook_events (session_id)`,
|
|
4176
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_hook_name ON hook_events (hook_name)`,
|
|
4177
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_event_type ON hook_events (event_type)`,
|
|
4178
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_errors ON hook_events (timestamp) WHERE error IS NOT NULL`
|
|
4179
|
+
];
|
|
4180
|
+
});
|
|
4181
|
+
|
|
4182
|
+
// src/db/migrations/001_initial.ts
|
|
4183
|
+
function up(db) {
|
|
4184
|
+
db.exec(CREATE_HOOK_EVENTS_TABLE);
|
|
4185
|
+
for (const idx of CREATE_INDEXES) {
|
|
4186
|
+
db.exec(idx);
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
var init_001_initial = __esm(() => {
|
|
4190
|
+
init_schema();
|
|
4191
|
+
});
|
|
4192
|
+
|
|
4193
|
+
// src/db/migrations/index.ts
|
|
4194
|
+
function ensureMigrationsTable(db) {
|
|
4195
|
+
db.exec(`
|
|
4196
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
4197
|
+
version TEXT PRIMARY KEY,
|
|
4198
|
+
applied_at TEXT NOT NULL
|
|
4199
|
+
)
|
|
4200
|
+
`);
|
|
4201
|
+
}
|
|
4202
|
+
function getApplied(db) {
|
|
4203
|
+
const rows = db.query("SELECT version FROM schema_migrations").all();
|
|
4204
|
+
return new Set(rows.map((r) => r.version));
|
|
4205
|
+
}
|
|
4206
|
+
function runMigrations(db) {
|
|
4207
|
+
ensureMigrationsTable(db);
|
|
4208
|
+
const applied = getApplied(db);
|
|
4209
|
+
for (const migration of MIGRATIONS) {
|
|
4210
|
+
if (applied.has(migration.version))
|
|
4211
|
+
continue;
|
|
4212
|
+
migration.up(db);
|
|
4213
|
+
db.run("INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)", [
|
|
4214
|
+
migration.version,
|
|
4215
|
+
new Date().toISOString()
|
|
4216
|
+
]);
|
|
4217
|
+
}
|
|
4218
|
+
}
|
|
4219
|
+
var MIGRATIONS;
|
|
4220
|
+
var init_migrations = __esm(() => {
|
|
4221
|
+
init_001_initial();
|
|
4222
|
+
MIGRATIONS = [{ version: "001_initial", up }];
|
|
4223
|
+
});
|
|
4224
|
+
|
|
4225
|
+
// src/db/legacy-import.ts
|
|
4226
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
4227
|
+
import { join as join3 } from "path";
|
|
4228
|
+
import { homedir as homedir3 } from "os";
|
|
4229
|
+
function ensureMetaTable(db) {
|
|
4230
|
+
db.exec(`
|
|
4231
|
+
CREATE TABLE IF NOT EXISTS _meta (
|
|
4232
|
+
key TEXT PRIMARY KEY,
|
|
4233
|
+
value TEXT NOT NULL
|
|
4234
|
+
)
|
|
4235
|
+
`);
|
|
4236
|
+
}
|
|
4237
|
+
function isAlreadyDone(db) {
|
|
4238
|
+
ensureMetaTable(db);
|
|
4239
|
+
const row = db.query("SELECT value FROM _meta WHERE key = ?").get(META_KEY);
|
|
4240
|
+
return row?.value === "1";
|
|
4241
|
+
}
|
|
4242
|
+
function markDone(db) {
|
|
4243
|
+
db.run("INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)", [META_KEY, "1"]);
|
|
4244
|
+
}
|
|
4245
|
+
function nanoid() {
|
|
4246
|
+
return crypto.randomUUID().replace(/-/g, "").slice(0, 21);
|
|
4247
|
+
}
|
|
4248
|
+
function importJsonlFile(db, filePath) {
|
|
4249
|
+
let count = 0;
|
|
4250
|
+
try {
|
|
4251
|
+
const lines = readFileSync3(filePath, "utf-8").split(`
|
|
4252
|
+
`).filter(Boolean);
|
|
4253
|
+
for (const line of lines) {
|
|
4254
|
+
try {
|
|
4255
|
+
const entry = JSON.parse(line);
|
|
4256
|
+
db.run(`INSERT OR IGNORE INTO hook_events
|
|
4257
|
+
(id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, project_dir)
|
|
4258
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
4259
|
+
nanoid(),
|
|
4260
|
+
entry.timestamp ?? new Date().toISOString(),
|
|
4261
|
+
entry.session_id ?? "legacy",
|
|
4262
|
+
"sessionlog",
|
|
4263
|
+
"PostToolUse",
|
|
4264
|
+
entry.tool_name ?? null,
|
|
4265
|
+
entry.tool_input ? String(entry.tool_input).slice(0, 500) : null,
|
|
4266
|
+
null
|
|
4267
|
+
]);
|
|
4268
|
+
count++;
|
|
4269
|
+
} catch {}
|
|
4270
|
+
}
|
|
4271
|
+
} catch {}
|
|
4272
|
+
return count;
|
|
4273
|
+
}
|
|
4274
|
+
function importErrorsLog(db, filePath) {
|
|
4275
|
+
let count = 0;
|
|
4276
|
+
try {
|
|
4277
|
+
const lines = readFileSync3(filePath, "utf-8").split(`
|
|
4278
|
+
`).filter(Boolean);
|
|
4279
|
+
const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
|
|
4280
|
+
for (const line of lines) {
|
|
4281
|
+
try {
|
|
4282
|
+
const m = line.match(linePattern);
|
|
4283
|
+
if (!m)
|
|
4284
|
+
continue;
|
|
4285
|
+
const [, timestamp, sessionPrefix, , errorMsg] = m;
|
|
4286
|
+
db.run(`INSERT OR IGNORE INTO hook_events
|
|
4287
|
+
(id, timestamp, session_id, hook_name, event_type, error)
|
|
4288
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [
|
|
4289
|
+
nanoid(),
|
|
4290
|
+
timestamp,
|
|
4291
|
+
sessionPrefix ? `legacy-${sessionPrefix}` : "legacy",
|
|
4292
|
+
"errornotify",
|
|
4293
|
+
"PostToolUse",
|
|
4294
|
+
errorMsg.slice(0, 500)
|
|
4295
|
+
]);
|
|
4296
|
+
count++;
|
|
4297
|
+
} catch {}
|
|
4298
|
+
}
|
|
4299
|
+
} catch {}
|
|
4300
|
+
return count;
|
|
4301
|
+
}
|
|
4302
|
+
function runLegacyImport(db) {
|
|
4303
|
+
try {
|
|
4304
|
+
if (isAlreadyDone(db))
|
|
4305
|
+
return;
|
|
4306
|
+
let total = 0;
|
|
4307
|
+
const claudeProjectsDir = join3(homedir3(), ".claude", "projects");
|
|
4308
|
+
if (existsSync3(claudeProjectsDir)) {
|
|
4309
|
+
try {
|
|
4310
|
+
const projectDirs = readdirSync2(claudeProjectsDir);
|
|
4311
|
+
for (const dir of projectDirs) {
|
|
4312
|
+
const projectDir = join3(claudeProjectsDir, dir);
|
|
4313
|
+
try {
|
|
4314
|
+
const files = readdirSync2(projectDir);
|
|
4315
|
+
for (const file of files) {
|
|
4316
|
+
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
4317
|
+
total += importJsonlFile(db, join3(projectDir, file));
|
|
4318
|
+
}
|
|
4319
|
+
if (file === "errors.log") {
|
|
4320
|
+
total += importErrorsLog(db, join3(projectDir, file));
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
} catch {}
|
|
4324
|
+
}
|
|
4325
|
+
} catch {}
|
|
4326
|
+
}
|
|
4327
|
+
markDone(db);
|
|
4328
|
+
if (total > 0) {
|
|
4329
|
+
process.stderr.write(`[hooks] Imported ${total} legacy log entries into SQLite.
|
|
4330
|
+
`);
|
|
4331
|
+
}
|
|
4332
|
+
} catch (err) {
|
|
4333
|
+
process.stderr.write(`[hooks] Legacy import failed (non-fatal): ${err}
|
|
4334
|
+
`);
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
var META_KEY = "legacy_import_done";
|
|
4338
|
+
var init_legacy_import = () => {};
|
|
4339
|
+
|
|
4340
|
+
// src/db/retention.ts
|
|
4341
|
+
function runRetention(db, days) {
|
|
4342
|
+
const envDays = parseInt(process.env.HOOKS_RETENTION_DAYS ?? "30");
|
|
4343
|
+
const retentionDays = days ?? (isNaN(envDays) || envDays <= 0 ? 30 : envDays);
|
|
4344
|
+
const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000).toISOString();
|
|
4345
|
+
try {
|
|
4346
|
+
db.run("DELETE FROM hook_events WHERE timestamp < ?", [cutoff]);
|
|
4347
|
+
const changes = db.query("SELECT changes() as changes").get()?.changes ?? 0;
|
|
4348
|
+
return changes;
|
|
4349
|
+
} catch {
|
|
4350
|
+
return 0;
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
|
|
4354
|
+
// src/db/index.ts
|
|
4355
|
+
var exports_db = {};
|
|
4356
|
+
__export(exports_db, {
|
|
4357
|
+
getDbPath: () => getDbPath,
|
|
4358
|
+
getDb: () => getDb,
|
|
4359
|
+
createTestDb: () => createTestDb,
|
|
4360
|
+
closeDb: () => closeDb
|
|
4361
|
+
});
|
|
4362
|
+
import { Database } from "bun:sqlite";
|
|
4363
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
4364
|
+
import { join as join4 } from "path";
|
|
4365
|
+
import { homedir as homedir4 } from "os";
|
|
4366
|
+
function getDbPath() {
|
|
4367
|
+
if (process.env.HOOKS_DB_PATH) {
|
|
4368
|
+
return process.env.HOOKS_DB_PATH;
|
|
4369
|
+
}
|
|
4370
|
+
const dataDir = process.env.HOOKS_DATA_DIR ?? join4(homedir4(), ".hooks");
|
|
4371
|
+
return join4(dataDir, "hooks.db");
|
|
4372
|
+
}
|
|
4373
|
+
function ensureDir(dbPath) {
|
|
4374
|
+
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
4375
|
+
if (dir && !existsSync4(dir)) {
|
|
4376
|
+
mkdirSync3(dir, { recursive: true });
|
|
4377
|
+
}
|
|
4378
|
+
}
|
|
4379
|
+
function getDb() {
|
|
4380
|
+
if (instance)
|
|
4381
|
+
return instance;
|
|
4382
|
+
const dbPath = getDbPath();
|
|
4383
|
+
const isNew = dbPath === ":memory:" || !existsSync4(dbPath);
|
|
4384
|
+
ensureDir(dbPath);
|
|
4385
|
+
instance = new Database(dbPath);
|
|
4386
|
+
instance.exec("PRAGMA journal_mode=WAL");
|
|
4387
|
+
instance.exec("PRAGMA foreign_keys=ON");
|
|
4388
|
+
runMigrations(instance);
|
|
4389
|
+
runRetention(instance);
|
|
4390
|
+
if (isNew) {
|
|
4391
|
+
runLegacyImport(instance);
|
|
4392
|
+
}
|
|
4393
|
+
return instance;
|
|
4394
|
+
}
|
|
4395
|
+
function closeDb() {
|
|
4396
|
+
if (instance) {
|
|
4397
|
+
instance.close();
|
|
4398
|
+
instance = null;
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
function createTestDb() {
|
|
4402
|
+
const db = new Database(":memory:");
|
|
4403
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
4404
|
+
db.exec("PRAGMA foreign_keys=ON");
|
|
4405
|
+
return db;
|
|
4406
|
+
}
|
|
4407
|
+
var instance = null;
|
|
4408
|
+
var init_db = __esm(() => {
|
|
4409
|
+
init_migrations();
|
|
4410
|
+
init_legacy_import();
|
|
4411
|
+
});
|
|
4412
|
+
|
|
4155
4413
|
// src/mcp/server.ts
|
|
4156
4414
|
var exports_server = {};
|
|
4157
4415
|
__export(exports_server, {
|
|
@@ -4165,8 +4423,8 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
4165
4423
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4166
4424
|
import { z } from "zod";
|
|
4167
4425
|
import { createServer } from "http";
|
|
4168
|
-
import { existsSync as
|
|
4169
|
-
import { join as
|
|
4426
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
4427
|
+
import { join as join5, dirname as dirname2 } from "path";
|
|
4170
4428
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4171
4429
|
function formatInstallResults(results, extra) {
|
|
4172
4430
|
const installed = results.filter((r) => r.success).map((r) => r.hook);
|
|
@@ -4183,13 +4441,20 @@ function createHooksServer() {
|
|
|
4183
4441
|
name: "@hasna/hooks",
|
|
4184
4442
|
version: pkg.version
|
|
4185
4443
|
});
|
|
4186
|
-
server.tool("hooks_list", "List all available hooks, optionally filtered by category
|
|
4444
|
+
server.tool("hooks_list", "List all available hooks, optionally filtered by category. Use compact:true to get minimal output (name+event+matcher only) \u2014 saves tokens.", {
|
|
4445
|
+
category: z.string().optional().describe("Filter by category name (e.g. 'Git Safety', 'Code Quality', 'Security')"),
|
|
4446
|
+
compact: z.boolean().default(false).describe("Return minimal fields only: name, event, matcher. Reduces token usage.")
|
|
4447
|
+
}, async ({ category, compact }) => {
|
|
4448
|
+
const slim = (hooks) => compact ? hooks.map((h) => ({ name: h.name, event: h.event, matcher: h.matcher })) : hooks;
|
|
4187
4449
|
if (category) {
|
|
4188
4450
|
const cat = CATEGORIES.find((c) => c.toLowerCase() === category.toLowerCase());
|
|
4189
4451
|
if (!cat) {
|
|
4190
4452
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Unknown category: ${category}`, available: [...CATEGORIES] }) }] };
|
|
4191
4453
|
}
|
|
4192
|
-
return { content: [{ type: "text", text: JSON.stringify(getHooksByCategory(cat)) }] };
|
|
4454
|
+
return { content: [{ type: "text", text: JSON.stringify(slim(getHooksByCategory(cat))) }] };
|
|
4455
|
+
}
|
|
4456
|
+
if (compact) {
|
|
4457
|
+
return { content: [{ type: "text", text: JSON.stringify(slim(HOOKS)) }] };
|
|
4193
4458
|
}
|
|
4194
4459
|
const result = {};
|
|
4195
4460
|
for (const cat of CATEGORIES) {
|
|
@@ -4197,9 +4462,13 @@ function createHooksServer() {
|
|
|
4197
4462
|
}
|
|
4198
4463
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
4199
4464
|
});
|
|
4200
|
-
server.tool("hooks_search", "Search for hooks by name, description, or tags
|
|
4465
|
+
server.tool("hooks_search", "Search for hooks by name, description, or tags. Use compact:true for minimal output to save tokens.", {
|
|
4466
|
+
query: z.string().describe("Search query"),
|
|
4467
|
+
compact: z.boolean().default(false).describe("Return minimal fields only: name, event, matcher.")
|
|
4468
|
+
}, async ({ query, compact }) => {
|
|
4201
4469
|
const results = searchHooks(query);
|
|
4202
|
-
|
|
4470
|
+
const out = compact ? results.map((h) => ({ name: h.name, event: h.event, matcher: h.matcher })) : results;
|
|
4471
|
+
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
4203
4472
|
});
|
|
4204
4473
|
server.tool("hooks_info", "Get detailed information about a specific hook including install status", { name: z.string().describe("Hook name (e.g. 'gitguard', 'checkpoint')") }, async ({ name }) => {
|
|
4205
4474
|
const meta = getHook(name);
|
|
@@ -4252,7 +4521,7 @@ function createHooksServer() {
|
|
|
4252
4521
|
const settingsPath = getSettingsPath(scope);
|
|
4253
4522
|
const issues = [];
|
|
4254
4523
|
const healthy = [];
|
|
4255
|
-
const settingsExist =
|
|
4524
|
+
const settingsExist = existsSync5(settingsPath);
|
|
4256
4525
|
if (!settingsExist) {
|
|
4257
4526
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
4258
4527
|
}
|
|
@@ -4265,13 +4534,13 @@ function createHooksServer() {
|
|
|
4265
4534
|
continue;
|
|
4266
4535
|
}
|
|
4267
4536
|
const hookDir = getHookPath(name);
|
|
4268
|
-
if (!
|
|
4537
|
+
if (!existsSync5(join5(hookDir, "src", "hook.ts"))) {
|
|
4269
4538
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
4270
4539
|
hookHealthy = false;
|
|
4271
4540
|
}
|
|
4272
4541
|
if (meta && settingsExist) {
|
|
4273
4542
|
try {
|
|
4274
|
-
const settings = JSON.parse(
|
|
4543
|
+
const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
|
|
4275
4544
|
const eventHooks = settings.hooks?.[meta.event] || [];
|
|
4276
4545
|
const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
|
|
4277
4546
|
const match = h.command?.match(/^hooks run (\w+)/);
|
|
@@ -4286,7 +4555,7 @@ function createHooksServer() {
|
|
|
4286
4555
|
if (hookHealthy)
|
|
4287
4556
|
healthy.push(name);
|
|
4288
4557
|
}
|
|
4289
|
-
return { content: [{ type: "text", text: JSON.stringify({ healthy, issues, registered, scope }) }] };
|
|
4558
|
+
return { content: [{ type: "text", text: JSON.stringify({ healthy: issues.length === 0, healthy_hooks: healthy, issues, registered, scope }) }] };
|
|
4290
4559
|
});
|
|
4291
4560
|
server.tool("hooks_categories", "List all hook categories with counts", {}, async () => {
|
|
4292
4561
|
const result = CATEGORIES.map((cat) => ({
|
|
@@ -4302,10 +4571,10 @@ function createHooksServer() {
|
|
|
4302
4571
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
4303
4572
|
}
|
|
4304
4573
|
const hookPath = getHookPath(name);
|
|
4305
|
-
const readmePath =
|
|
4574
|
+
const readmePath = join5(hookPath, "README.md");
|
|
4306
4575
|
let readme = "";
|
|
4307
|
-
if (
|
|
4308
|
-
readme =
|
|
4576
|
+
if (existsSync5(readmePath)) {
|
|
4577
|
+
readme = readFileSync4(readmePath, "utf-8");
|
|
4309
4578
|
}
|
|
4310
4579
|
return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
|
|
4311
4580
|
}
|
|
@@ -4339,7 +4608,7 @@ function createHooksServer() {
|
|
|
4339
4608
|
const registered = getRegisteredHooks(scope);
|
|
4340
4609
|
const result = registered.map((name) => {
|
|
4341
4610
|
const meta = getHook(name);
|
|
4342
|
-
return { name, event: meta?.event, version: meta?.version, description: meta?.description };
|
|
4611
|
+
return { name, event: meta?.event, matcher: meta?.matcher ?? "", version: meta?.version, description: meta?.description };
|
|
4343
4612
|
});
|
|
4344
4613
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
4345
4614
|
});
|
|
@@ -4354,8 +4623,8 @@ function createHooksServer() {
|
|
|
4354
4623
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
4355
4624
|
}
|
|
4356
4625
|
const hookDir = getHookPath(name);
|
|
4357
|
-
const hookScript =
|
|
4358
|
-
if (!
|
|
4626
|
+
const hookScript = join5(hookDir, "src", "hook.ts");
|
|
4627
|
+
if (!existsSync5(hookScript)) {
|
|
4359
4628
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook script not found: ${hookScript}` }) }] };
|
|
4360
4629
|
}
|
|
4361
4630
|
let hookInput = { ...input };
|
|
@@ -4426,6 +4695,203 @@ function createHooksServer() {
|
|
|
4426
4695
|
const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
|
|
4427
4696
|
return { content: [{ type: "text", text: JSON.stringify({ updated, failed, total: results.length }) }] };
|
|
4428
4697
|
});
|
|
4698
|
+
server.tool("hooks_context", "Get full agent context in one call: installed hooks (with event+matcher), active profile, settings path, and doctor status. Call this once at session start instead of making 4 separate calls.", {
|
|
4699
|
+
scope: z.enum(["global", "project"]).default("global").describe("Scope to inspect"),
|
|
4700
|
+
profile: z.string().optional().describe("Agent profile ID to include in context")
|
|
4701
|
+
}, async ({ scope, profile }) => {
|
|
4702
|
+
const settingsPath = getSettingsPath(scope);
|
|
4703
|
+
const registered = getRegisteredHooks(scope);
|
|
4704
|
+
const hooks = registered.map((name) => {
|
|
4705
|
+
const meta = getHook(name);
|
|
4706
|
+
return { name, event: meta?.event, matcher: meta?.matcher ?? "", version: meta?.version };
|
|
4707
|
+
});
|
|
4708
|
+
const issues = [];
|
|
4709
|
+
for (const name of registered) {
|
|
4710
|
+
if (!hookExists(name)) {
|
|
4711
|
+
issues.push({ hook: name, issue: "Hook not found in package", severity: "error" });
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4714
|
+
const healthy = issues.length === 0;
|
|
4715
|
+
const ctx = {
|
|
4716
|
+
scope,
|
|
4717
|
+
settings_path: settingsPath,
|
|
4718
|
+
settings_exists: existsSync5(settingsPath),
|
|
4719
|
+
registered_hooks: hooks,
|
|
4720
|
+
hook_count: hooks.length,
|
|
4721
|
+
healthy,
|
|
4722
|
+
issues,
|
|
4723
|
+
version: pkg.version
|
|
4724
|
+
};
|
|
4725
|
+
if (profile) {
|
|
4726
|
+
const p = getProfile(profile);
|
|
4727
|
+
ctx.profile = p ?? null;
|
|
4728
|
+
}
|
|
4729
|
+
return { content: [{ type: "text", text: JSON.stringify(ctx) }] };
|
|
4730
|
+
});
|
|
4731
|
+
server.tool("hooks_preview", "Simulate which installed PreToolUse hooks would fire for a given tool call and what decision each returns. Use this to understand your hook environment before taking an action.", {
|
|
4732
|
+
tool_name: z.string().describe("Tool name to simulate (e.g. 'Bash', 'Write', 'Edit')"),
|
|
4733
|
+
tool_input: z.record(z.string(), z.unknown()).default(() => ({})).describe("Tool input to pass to matching hooks"),
|
|
4734
|
+
scope: z.enum(["global", "project"]).default("global").describe("Scope to check"),
|
|
4735
|
+
timeout_ms: z.number().default(5000).describe("Per-hook timeout in milliseconds")
|
|
4736
|
+
}, async ({ tool_name, tool_input, scope, timeout_ms }) => {
|
|
4737
|
+
const registered = getRegisteredHooks(scope);
|
|
4738
|
+
const matchingHooks = registered.filter((name) => {
|
|
4739
|
+
const meta = getHook(name);
|
|
4740
|
+
if (!meta || meta.event !== "PreToolUse")
|
|
4741
|
+
return false;
|
|
4742
|
+
if (!meta.matcher)
|
|
4743
|
+
return true;
|
|
4744
|
+
try {
|
|
4745
|
+
return new RegExp(meta.matcher).test(tool_name);
|
|
4746
|
+
} catch {
|
|
4747
|
+
return false;
|
|
4748
|
+
}
|
|
4749
|
+
});
|
|
4750
|
+
if (matchingHooks.length === 0) {
|
|
4751
|
+
return { content: [{ type: "text", text: JSON.stringify({ tool_name, matching_hooks: [], result: "no_hooks_match", decision: "approve" }) }] };
|
|
4752
|
+
}
|
|
4753
|
+
const input = { tool_name, tool_input };
|
|
4754
|
+
const results = await Promise.all(matchingHooks.map(async (name) => {
|
|
4755
|
+
const hookDir = getHookPath(name);
|
|
4756
|
+
const hookScript = join5(hookDir, "src", "hook.ts");
|
|
4757
|
+
if (!existsSync5(hookScript))
|
|
4758
|
+
return { name, decision: "approve", error: "script not found" };
|
|
4759
|
+
const proc = Bun.spawn(["bun", "run", hookScript], {
|
|
4760
|
+
stdin: new Response(JSON.stringify(input)),
|
|
4761
|
+
stdout: "pipe",
|
|
4762
|
+
stderr: "pipe",
|
|
4763
|
+
env: process.env
|
|
4764
|
+
});
|
|
4765
|
+
const timeout = new Promise((r) => setTimeout(() => r(null), timeout_ms));
|
|
4766
|
+
const res = await Promise.race([
|
|
4767
|
+
Promise.all([new Response(proc.stdout).text(), proc.exited]).then(([stdout]) => ({ stdout, timedOut: false })),
|
|
4768
|
+
timeout.then(() => {
|
|
4769
|
+
proc.kill();
|
|
4770
|
+
return { stdout: "", timedOut: true };
|
|
4771
|
+
})
|
|
4772
|
+
]);
|
|
4773
|
+
if (res.timedOut)
|
|
4774
|
+
return { name, decision: "approve", timedOut: true };
|
|
4775
|
+
let output = {};
|
|
4776
|
+
try {
|
|
4777
|
+
output = JSON.parse(res.stdout);
|
|
4778
|
+
} catch {}
|
|
4779
|
+
return { name, decision: output.decision ?? "approve", reason: output.reason, raw: output };
|
|
4780
|
+
}));
|
|
4781
|
+
const blocked = results.find((r) => r.decision === "block");
|
|
4782
|
+
return {
|
|
4783
|
+
content: [{
|
|
4784
|
+
type: "text",
|
|
4785
|
+
text: JSON.stringify({
|
|
4786
|
+
tool_name,
|
|
4787
|
+
matching_hooks: matchingHooks,
|
|
4788
|
+
results,
|
|
4789
|
+
decision: blocked ? "block" : "approve",
|
|
4790
|
+
blocked_by: blocked?.name ?? null,
|
|
4791
|
+
blocked_reason: blocked?.reason ?? null
|
|
4792
|
+
})
|
|
4793
|
+
}]
|
|
4794
|
+
};
|
|
4795
|
+
});
|
|
4796
|
+
server.tool("hooks_setup", "Single-shot agent onboarding: create an agent profile + install recommended hooks in one call. Ideal for agents setting up hooks at session start.", {
|
|
4797
|
+
agent_type: z.enum(["claude", "gemini", "custom"]).default("claude").describe("Type of AI agent"),
|
|
4798
|
+
name: z.string().optional().describe("Optional display name for the agent"),
|
|
4799
|
+
hooks: z.array(z.string()).optional().describe("Hook names to install (omit for sensible defaults: gitguard, checkpoint, checktests, protectfiles)"),
|
|
4800
|
+
scope: z.enum(["global", "project"]).default("global").describe("Install scope")
|
|
4801
|
+
}, async ({ agent_type, name, hooks, scope }) => {
|
|
4802
|
+
const profile = createProfile({ agent_type, name });
|
|
4803
|
+
const toInstall = hooks && hooks.length > 0 ? hooks : ["gitguard", "checkpoint", "checktests", "protectfiles"];
|
|
4804
|
+
const results = toInstall.map((h) => installHook(h, { scope, overwrite: false, profile: profile.agent_id }));
|
|
4805
|
+
const installed = results.filter((r) => r.success).map((r) => r.hook);
|
|
4806
|
+
const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
|
|
4807
|
+
return {
|
|
4808
|
+
content: [{
|
|
4809
|
+
type: "text",
|
|
4810
|
+
text: JSON.stringify({ profile, installed, failed, scope, run_with: `hooks run <name> --profile ${profile.agent_id}` })
|
|
4811
|
+
}]
|
|
4812
|
+
};
|
|
4813
|
+
});
|
|
4814
|
+
server.tool("hooks_batch_run", "Run multiple hooks in parallel in a single call. Returns all results at once \u2014 more efficient than N separate hooks_run calls.", {
|
|
4815
|
+
hooks: z.array(z.object({
|
|
4816
|
+
name: z.string().describe("Hook name"),
|
|
4817
|
+
input: z.record(z.string(), z.unknown()).default(() => ({})).describe("Hook input JSON")
|
|
4818
|
+
})).describe("List of hooks to run with their inputs"),
|
|
4819
|
+
timeout_ms: z.number().default(1e4).describe("Per-hook timeout in milliseconds")
|
|
4820
|
+
}, async ({ hooks, timeout_ms }) => {
|
|
4821
|
+
const results = await Promise.all(hooks.map(async ({ name, input }) => {
|
|
4822
|
+
const meta = getHook(name);
|
|
4823
|
+
if (!meta)
|
|
4824
|
+
return { name, error: `Hook '${name}' not found` };
|
|
4825
|
+
const hookScript = join5(getHookPath(name), "src", "hook.ts");
|
|
4826
|
+
if (!existsSync5(hookScript))
|
|
4827
|
+
return { name, error: "script not found" };
|
|
4828
|
+
const proc = Bun.spawn(["bun", "run", hookScript], {
|
|
4829
|
+
stdin: new Response(JSON.stringify(input)),
|
|
4830
|
+
stdout: "pipe",
|
|
4831
|
+
stderr: "pipe",
|
|
4832
|
+
env: process.env
|
|
4833
|
+
});
|
|
4834
|
+
const timeout = new Promise((r) => setTimeout(() => r(null), timeout_ms));
|
|
4835
|
+
const res = await Promise.race([
|
|
4836
|
+
Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]).then(([stdout, stderr, exitCode]) => ({ stdout, stderr, exitCode, timedOut: false })),
|
|
4837
|
+
timeout.then(() => {
|
|
4838
|
+
proc.kill();
|
|
4839
|
+
return { stdout: "", stderr: "", exitCode: -1, timedOut: true };
|
|
4840
|
+
})
|
|
4841
|
+
]);
|
|
4842
|
+
let output = {};
|
|
4843
|
+
try {
|
|
4844
|
+
output = JSON.parse(res.stdout);
|
|
4845
|
+
} catch {
|
|
4846
|
+
output = res.stdout ? { raw: res.stdout } : {};
|
|
4847
|
+
}
|
|
4848
|
+
return { name, output, exitCode: res.exitCode, ...res.timedOut ? { timedOut: true } : {} };
|
|
4849
|
+
}));
|
|
4850
|
+
return { content: [{ type: "text", text: JSON.stringify({ results, count: results.length }) }] };
|
|
4851
|
+
});
|
|
4852
|
+
server.tool("hooks_disable", "Temporarily disable a registered hook without removing it. Stores disabled list in settings under hooks.__disabled.", {
|
|
4853
|
+
name: z.string().describe("Hook name to disable"),
|
|
4854
|
+
scope: z.enum(["global", "project"]).default("global").describe("Scope")
|
|
4855
|
+
}, async ({ name, scope }) => {
|
|
4856
|
+
const settingsPath = getSettingsPath(scope);
|
|
4857
|
+
let settings = {};
|
|
4858
|
+
try {
|
|
4859
|
+
if (existsSync5(settingsPath))
|
|
4860
|
+
settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
|
|
4861
|
+
} catch {}
|
|
4862
|
+
if (!settings.hooks)
|
|
4863
|
+
settings.hooks = {};
|
|
4864
|
+
const disabled = settings.hooks.__disabled ?? [];
|
|
4865
|
+
if (!disabled.includes(name))
|
|
4866
|
+
disabled.push(name);
|
|
4867
|
+
settings.hooks.__disabled = disabled;
|
|
4868
|
+
const { writeFileSync: writeFileSync3, mkdirSync: mkdirSync4 } = await import("fs");
|
|
4869
|
+
const { dirname: dirname3 } = await import("path");
|
|
4870
|
+
mkdirSync4(dirname3(settingsPath), { recursive: true });
|
|
4871
|
+
writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
4872
|
+
`);
|
|
4873
|
+
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
|
|
4874
|
+
});
|
|
4875
|
+
server.tool("hooks_enable", "Re-enable a previously disabled hook.", {
|
|
4876
|
+
name: z.string().describe("Hook name to enable"),
|
|
4877
|
+
scope: z.enum(["global", "project"]).default("global").describe("Scope")
|
|
4878
|
+
}, async ({ name, scope }) => {
|
|
4879
|
+
const settingsPath = getSettingsPath(scope);
|
|
4880
|
+
let settings = {};
|
|
4881
|
+
try {
|
|
4882
|
+
if (existsSync5(settingsPath))
|
|
4883
|
+
settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
|
|
4884
|
+
} catch {}
|
|
4885
|
+
if (settings.hooks?.__disabled) {
|
|
4886
|
+
settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
|
|
4887
|
+
if (settings.hooks.__disabled.length === 0)
|
|
4888
|
+
delete settings.hooks.__disabled;
|
|
4889
|
+
const { writeFileSync: writeFileSync3 } = await import("fs");
|
|
4890
|
+
writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
4891
|
+
`);
|
|
4892
|
+
}
|
|
4893
|
+
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: false, scope }) }] };
|
|
4894
|
+
});
|
|
4429
4895
|
server.tool("hooks_init", "Register a new agent profile \u2014 returns a unique agent_id for use with hook installation and execution", {
|
|
4430
4896
|
agent_type: z.enum(["claude", "gemini", "custom"]).default("claude").describe("Type of AI agent"),
|
|
4431
4897
|
name: z.string().optional().describe("Optional display name for the agent")
|
|
@@ -4437,6 +4903,104 @@ function createHooksServer() {
|
|
|
4437
4903
|
const profiles = listProfiles();
|
|
4438
4904
|
return { content: [{ type: "text", text: JSON.stringify(profiles) }] };
|
|
4439
4905
|
});
|
|
4906
|
+
server.tool("hooks_log_list", "List hook events from SQLite (~/.hooks/hooks.db). Filter by hook name, session ID, or time range.", {
|
|
4907
|
+
hook_name: z.string().optional().describe("Filter by hook name (e.g. 'sessionlog', 'costwatch')"),
|
|
4908
|
+
session_id: z.string().optional().describe("Filter by session ID prefix"),
|
|
4909
|
+
limit: z.number().default(50).describe("Max number of events to return"),
|
|
4910
|
+
since: z.string().optional().describe("ISO timestamp or duration string (e.g. '1h', '30m', '7d') to filter from")
|
|
4911
|
+
}, async ({ hook_name, session_id, limit, since }) => {
|
|
4912
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
4913
|
+
const db = getDb2();
|
|
4914
|
+
function parseDuration(s) {
|
|
4915
|
+
const m = s.match(/^(\d+)(s|m|h|d)$/);
|
|
4916
|
+
if (!m)
|
|
4917
|
+
return null;
|
|
4918
|
+
const n = parseInt(m[1]);
|
|
4919
|
+
const ms = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]];
|
|
4920
|
+
return new Date(Date.now() - n * ms).toISOString();
|
|
4921
|
+
}
|
|
4922
|
+
let sql = "SELECT * FROM hook_events WHERE 1=1";
|
|
4923
|
+
const params = [];
|
|
4924
|
+
if (hook_name) {
|
|
4925
|
+
sql += " AND hook_name = ?";
|
|
4926
|
+
params.push(hook_name);
|
|
4927
|
+
}
|
|
4928
|
+
if (session_id) {
|
|
4929
|
+
sql += " AND session_id LIKE ?";
|
|
4930
|
+
params.push(`${session_id}%`);
|
|
4931
|
+
}
|
|
4932
|
+
if (since) {
|
|
4933
|
+
const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
|
|
4934
|
+
if (ts) {
|
|
4935
|
+
sql += " AND timestamp >= ?";
|
|
4936
|
+
params.push(ts);
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
sql += " ORDER BY timestamp DESC LIMIT ?";
|
|
4940
|
+
params.push(limit);
|
|
4941
|
+
const rows = db.query(sql).all(...params);
|
|
4942
|
+
return { content: [{ type: "text", text: JSON.stringify({ events: rows, count: rows.length }) }] };
|
|
4943
|
+
});
|
|
4944
|
+
server.tool("hooks_log_tail", "Show the most recent hook events from SQLite.", {
|
|
4945
|
+
n: z.number().default(20).describe("Number of most recent events to return")
|
|
4946
|
+
}, async ({ n }) => {
|
|
4947
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
4948
|
+
const db = getDb2();
|
|
4949
|
+
const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(n);
|
|
4950
|
+
return { content: [{ type: "text", text: JSON.stringify({ events: rows, count: rows.length }) }] };
|
|
4951
|
+
});
|
|
4952
|
+
server.tool("hooks_log_errors", "Show hook events that contain errors, optionally filtered by time range.", {
|
|
4953
|
+
since: z.string().default("24h").describe("Duration string (e.g. '1h', '30m', '7d') or ISO timestamp"),
|
|
4954
|
+
limit: z.number().default(50).describe("Max number of error events to return")
|
|
4955
|
+
}, async ({ since, limit }) => {
|
|
4956
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
4957
|
+
const db = getDb2();
|
|
4958
|
+
function parseDuration(s) {
|
|
4959
|
+
const m = s.match(/^(\d+)(s|m|h|d)$/);
|
|
4960
|
+
if (!m)
|
|
4961
|
+
return s;
|
|
4962
|
+
const n = parseInt(m[1]);
|
|
4963
|
+
const ms = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]];
|
|
4964
|
+
return new Date(Date.now() - n * ms).toISOString();
|
|
4965
|
+
}
|
|
4966
|
+
const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
|
|
4967
|
+
const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(ts, limit);
|
|
4968
|
+
return { content: [{ type: "text", text: JSON.stringify({ events: rows, count: rows.length }) }] };
|
|
4969
|
+
});
|
|
4970
|
+
server.tool("hooks_log_summary", "Summarize hook execution: counts per hook, error rates, and recent activity.", {
|
|
4971
|
+
since: z.string().default("24h").describe("Duration string (e.g. '1h', '24h', '7d') or ISO timestamp")
|
|
4972
|
+
}, async ({ since }) => {
|
|
4973
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
4974
|
+
const db = getDb2();
|
|
4975
|
+
function parseDuration(s) {
|
|
4976
|
+
const m = s.match(/^(\d+)(s|m|h|d)$/);
|
|
4977
|
+
if (!m)
|
|
4978
|
+
return s;
|
|
4979
|
+
const n = parseInt(m[1]);
|
|
4980
|
+
const ms = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]];
|
|
4981
|
+
return new Date(Date.now() - n * ms).toISOString();
|
|
4982
|
+
}
|
|
4983
|
+
const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
|
|
4984
|
+
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);
|
|
4985
|
+
const summary = totals.map((r) => ({
|
|
4986
|
+
hook_name: r.hook_name,
|
|
4987
|
+
total: r.total,
|
|
4988
|
+
errors: r.errors,
|
|
4989
|
+
error_rate: r.total > 0 ? (r.errors / r.total * 100).toFixed(1) + "%" : "0%"
|
|
4990
|
+
}));
|
|
4991
|
+
const grandTotal = totals.reduce((s, r) => s + r.total, 0);
|
|
4992
|
+
const grandErrors = totals.reduce((s, r) => s + r.errors, 0);
|
|
4993
|
+
return {
|
|
4994
|
+
content: [{
|
|
4995
|
+
type: "text",
|
|
4996
|
+
text: JSON.stringify({
|
|
4997
|
+
since: ts,
|
|
4998
|
+
hooks: summary,
|
|
4999
|
+
totals: { events: grandTotal, errors: grandErrors, hooks_active: totals.length }
|
|
5000
|
+
})
|
|
5001
|
+
}]
|
|
5002
|
+
};
|
|
5003
|
+
});
|
|
4440
5004
|
return server;
|
|
4441
5005
|
}
|
|
4442
5006
|
async function startSSEServer(port = MCP_PORT) {
|
|
@@ -4482,7 +5046,7 @@ var init_server = __esm(() => {
|
|
|
4482
5046
|
init_installer();
|
|
4483
5047
|
init_profiles();
|
|
4484
5048
|
__dirname3 = dirname2(fileURLToPath2(import.meta.url));
|
|
4485
|
-
pkg = JSON.parse(
|
|
5049
|
+
pkg = JSON.parse(readFileSync4(join5(__dirname3, "..", "..", "package.json"), "utf-8"));
|
|
4486
5050
|
});
|
|
4487
5051
|
|
|
4488
5052
|
// src/cli/index.tsx
|
|
@@ -4506,8 +5070,8 @@ var {
|
|
|
4506
5070
|
|
|
4507
5071
|
// src/cli/index.tsx
|
|
4508
5072
|
import chalk2 from "chalk";
|
|
4509
|
-
import { existsSync as
|
|
4510
|
-
import { join as
|
|
5073
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
5074
|
+
import { join as join6, dirname as dirname3 } from "path";
|
|
4511
5075
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4512
5076
|
|
|
4513
5077
|
// src/cli/components/App.tsx
|
|
@@ -5685,8 +6249,8 @@ init_installer();
|
|
|
5685
6249
|
init_profiles();
|
|
5686
6250
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
5687
6251
|
var __dirname4 = dirname3(fileURLToPath3(import.meta.url));
|
|
5688
|
-
var pkgPath =
|
|
5689
|
-
var pkg2 = JSON.parse(
|
|
6252
|
+
var pkgPath = existsSync6(join6(__dirname4, "..", "package.json")) ? join6(__dirname4, "..", "package.json") : join6(__dirname4, "..", "..", "package.json");
|
|
6253
|
+
var pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
5690
6254
|
var program2 = new Command;
|
|
5691
6255
|
function resolveScope(options) {
|
|
5692
6256
|
if (options.project)
|
|
@@ -5756,8 +6320,8 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
5756
6320
|
process.exit(1);
|
|
5757
6321
|
}
|
|
5758
6322
|
const hookDir = getHookPath(hook);
|
|
5759
|
-
const hookScript =
|
|
5760
|
-
if (!
|
|
6323
|
+
const hookScript = join6(hookDir, "src", "hook.ts");
|
|
6324
|
+
if (!existsSync6(hookScript)) {
|
|
5761
6325
|
console.error(JSON.stringify({ error: `Hook script not found: ${hookScript}` }));
|
|
5762
6326
|
process.exit(1);
|
|
5763
6327
|
}
|
|
@@ -6057,7 +6621,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
6057
6621
|
const settingsPath = getSettingsPath(scope);
|
|
6058
6622
|
const issues = [];
|
|
6059
6623
|
const healthy = [];
|
|
6060
|
-
const settingsExist =
|
|
6624
|
+
const settingsExist = existsSync6(settingsPath);
|
|
6061
6625
|
if (!settingsExist) {
|
|
6062
6626
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
6063
6627
|
}
|
|
@@ -6071,14 +6635,14 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
6071
6635
|
continue;
|
|
6072
6636
|
}
|
|
6073
6637
|
const hookDir = getHookPath(name);
|
|
6074
|
-
const hookScript =
|
|
6075
|
-
if (!
|
|
6638
|
+
const hookScript = join6(hookDir, "src", "hook.ts");
|
|
6639
|
+
if (!existsSync6(hookScript)) {
|
|
6076
6640
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
6077
6641
|
hookHealthy = false;
|
|
6078
6642
|
}
|
|
6079
6643
|
if (meta && settingsExist) {
|
|
6080
6644
|
try {
|
|
6081
|
-
const settings = JSON.parse(
|
|
6645
|
+
const settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
|
|
6082
6646
|
const eventHooks = settings.hooks?.[meta.event] || [];
|
|
6083
6647
|
const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
|
|
6084
6648
|
const match = h.command?.match(/^hooks run (\w+)/);
|
|
@@ -6095,7 +6659,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
6095
6659
|
}
|
|
6096
6660
|
}
|
|
6097
6661
|
if (options.json) {
|
|
6098
|
-
console.log(JSON.stringify({ healthy, issues, registered, scope }));
|
|
6662
|
+
console.log(JSON.stringify({ healthy: issues.length === 0, healthy_hooks: healthy, issues, registered, scope }));
|
|
6099
6663
|
return;
|
|
6100
6664
|
}
|
|
6101
6665
|
console.log(chalk2.bold(`
|
|
@@ -6176,10 +6740,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
6176
6740
|
return;
|
|
6177
6741
|
}
|
|
6178
6742
|
const hookPath = getHookPath(hook);
|
|
6179
|
-
const readmePath =
|
|
6743
|
+
const readmePath = join6(hookPath, "README.md");
|
|
6180
6744
|
let readme = "";
|
|
6181
|
-
if (
|
|
6182
|
-
readme =
|
|
6745
|
+
if (existsSync6(readmePath)) {
|
|
6746
|
+
readme = readFileSync5(readmePath, "utf-8");
|
|
6183
6747
|
}
|
|
6184
6748
|
if (options.json) {
|
|
6185
6749
|
console.log(JSON.stringify({ ...meta, readme }));
|
|
@@ -6361,9 +6925,9 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
6361
6925
|
if (file === "-") {
|
|
6362
6926
|
raw = await new Response(Bun.stdin.stream()).text();
|
|
6363
6927
|
} else {
|
|
6364
|
-
const { readFileSync:
|
|
6928
|
+
const { readFileSync: readFileSync6 } = await import("fs");
|
|
6365
6929
|
try {
|
|
6366
|
-
raw =
|
|
6930
|
+
raw = readFileSync6(file, "utf-8");
|
|
6367
6931
|
} catch {
|
|
6368
6932
|
if (options.json) {
|
|
6369
6933
|
console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
|
|
@@ -6394,6 +6958,154 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
6394
6958
|
console.log(chalk2.dim(` Skipped ${result.skipped} (already exist or invalid)`));
|
|
6395
6959
|
}
|
|
6396
6960
|
});
|
|
6961
|
+
var logCmd = program2.command("log").description("Query hook event logs from SQLite (~/.hooks/hooks.db)");
|
|
6962
|
+
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) => {
|
|
6963
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
6964
|
+
const db = getDb2();
|
|
6965
|
+
const limit = parseInt(options.limit) || 50;
|
|
6966
|
+
let sql = "SELECT * FROM hook_events WHERE 1=1";
|
|
6967
|
+
const params = [];
|
|
6968
|
+
if (options.hook) {
|
|
6969
|
+
sql += " AND hook_name = ?";
|
|
6970
|
+
params.push(options.hook);
|
|
6971
|
+
}
|
|
6972
|
+
if (options.session) {
|
|
6973
|
+
sql += " AND session_id LIKE ?";
|
|
6974
|
+
params.push(`${options.session}%`);
|
|
6975
|
+
}
|
|
6976
|
+
sql += " ORDER BY timestamp DESC LIMIT ?";
|
|
6977
|
+
params.push(String(limit));
|
|
6978
|
+
const rows = db.query(sql).all(...params);
|
|
6979
|
+
if (options.json) {
|
|
6980
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
6981
|
+
return;
|
|
6982
|
+
}
|
|
6983
|
+
if (rows.length === 0) {
|
|
6984
|
+
console.log(chalk2.dim("No events found."));
|
|
6985
|
+
return;
|
|
6986
|
+
}
|
|
6987
|
+
console.log(chalk2.bold(`
|
|
6988
|
+
Hook Events (${rows.length})
|
|
6989
|
+
`));
|
|
6990
|
+
for (const row of rows) {
|
|
6991
|
+
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
6992
|
+
const err = row.error ? chalk2.red(` ERR: ${row.error.slice(0, 60)}`) : "";
|
|
6993
|
+
const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
|
|
6994
|
+
console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))}${tool}${err}`);
|
|
6995
|
+
}
|
|
6996
|
+
console.log();
|
|
6997
|
+
});
|
|
6998
|
+
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) => {
|
|
6999
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
7000
|
+
const db = getDb2();
|
|
7001
|
+
const limit = parseInt(options.limit) || 50;
|
|
7002
|
+
const q = `%${text}%`;
|
|
7003
|
+
const rows = db.query("SELECT * FROM hook_events WHERE tool_input LIKE ? OR error LIKE ? ORDER BY timestamp DESC LIMIT ?").all(q, q, limit);
|
|
7004
|
+
if (options.json) {
|
|
7005
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
7006
|
+
return;
|
|
7007
|
+
}
|
|
7008
|
+
if (rows.length === 0) {
|
|
7009
|
+
console.log(chalk2.dim(`No events matching "${text}".`));
|
|
7010
|
+
return;
|
|
7011
|
+
}
|
|
7012
|
+
console.log(chalk2.bold(`
|
|
7013
|
+
Search results for "${text}" (${rows.length})
|
|
7014
|
+
`));
|
|
7015
|
+
for (const row of rows) {
|
|
7016
|
+
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
7017
|
+
const snippet = (row.tool_input || row.error || "").slice(0, 80);
|
|
7018
|
+
console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.dim(snippet)}`);
|
|
7019
|
+
}
|
|
7020
|
+
console.log();
|
|
7021
|
+
});
|
|
7022
|
+
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) => {
|
|
7023
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
7024
|
+
const db = getDb2();
|
|
7025
|
+
const limit = parseInt(options.n) || 20;
|
|
7026
|
+
const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(limit);
|
|
7027
|
+
if (options.json) {
|
|
7028
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
7029
|
+
return;
|
|
7030
|
+
}
|
|
7031
|
+
if (rows.length === 0) {
|
|
7032
|
+
console.log(chalk2.dim("No events yet."));
|
|
7033
|
+
return;
|
|
7034
|
+
}
|
|
7035
|
+
console.log(chalk2.bold(`
|
|
7036
|
+
Last ${rows.length} events
|
|
7037
|
+
`));
|
|
7038
|
+
for (const row of rows) {
|
|
7039
|
+
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
7040
|
+
const err = row.error ? chalk2.red(` \u2717 ${row.error.slice(0, 60)}`) : "";
|
|
7041
|
+
const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
|
|
7042
|
+
console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))}${tool}${err}`);
|
|
7043
|
+
}
|
|
7044
|
+
console.log();
|
|
7045
|
+
});
|
|
7046
|
+
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) => {
|
|
7047
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
7048
|
+
const db = getDb2();
|
|
7049
|
+
const limit = parseInt(options.limit) || 50;
|
|
7050
|
+
function parseDuration(s) {
|
|
7051
|
+
const m = s.match(/^(\d+)(s|m|h|d)$/);
|
|
7052
|
+
if (!m)
|
|
7053
|
+
return 86400000;
|
|
7054
|
+
const n = parseInt(m[1]);
|
|
7055
|
+
switch (m[2]) {
|
|
7056
|
+
case "s":
|
|
7057
|
+
return n * 1000;
|
|
7058
|
+
case "m":
|
|
7059
|
+
return n * 60 * 1000;
|
|
7060
|
+
case "h":
|
|
7061
|
+
return n * 60 * 60 * 1000;
|
|
7062
|
+
case "d":
|
|
7063
|
+
return n * 24 * 60 * 60 * 1000;
|
|
7064
|
+
default:
|
|
7065
|
+
return 86400000;
|
|
7066
|
+
}
|
|
7067
|
+
}
|
|
7068
|
+
const since = new Date(Date.now() - parseDuration(options.since)).toISOString();
|
|
7069
|
+
const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(since, limit);
|
|
7070
|
+
if (options.json) {
|
|
7071
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
7072
|
+
return;
|
|
7073
|
+
}
|
|
7074
|
+
if (rows.length === 0) {
|
|
7075
|
+
console.log(chalk2.dim(`No errors in the last ${options.since}.`));
|
|
7076
|
+
return;
|
|
7077
|
+
}
|
|
7078
|
+
console.log(chalk2.bold(`
|
|
7079
|
+
Errors (last ${options.since}, ${rows.length} found)
|
|
7080
|
+
`));
|
|
7081
|
+
for (const row of rows) {
|
|
7082
|
+
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
7083
|
+
console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.red(row.error.slice(0, 100))}`);
|
|
7084
|
+
}
|
|
7085
|
+
console.log();
|
|
7086
|
+
});
|
|
7087
|
+
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) => {
|
|
7088
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
7089
|
+
const db = getDb2();
|
|
7090
|
+
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();
|
|
7091
|
+
const count = countRow?.n ?? 0;
|
|
7092
|
+
if (count === 0) {
|
|
7093
|
+
console.log(chalk2.dim("Nothing to clear."));
|
|
7094
|
+
return;
|
|
7095
|
+
}
|
|
7096
|
+
if (!options.yes) {
|
|
7097
|
+
const scope = options.hook ? `hook "${options.hook}"` : "all hooks";
|
|
7098
|
+
console.log(chalk2.yellow(`About to delete ${count} event(s) for ${scope}.`));
|
|
7099
|
+
console.log(chalk2.dim("Re-run with --yes to confirm."));
|
|
7100
|
+
return;
|
|
7101
|
+
}
|
|
7102
|
+
if (options.hook) {
|
|
7103
|
+
db.run("DELETE FROM hook_events WHERE hook_name = ?", [options.hook]);
|
|
7104
|
+
} else {
|
|
7105
|
+
db.run("DELETE FROM hook_events");
|
|
7106
|
+
}
|
|
7107
|
+
console.log(chalk2.green(`\u2713 Cleared ${count} event(s).`));
|
|
7108
|
+
});
|
|
6397
7109
|
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) => {
|
|
6398
7110
|
if (options.stdio) {
|
|
6399
7111
|
const { startStdioServer: startStdioServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|