@hasna/hooks 0.2.6 → 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 +545 -41
- 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);
|
|
@@ -4263,7 +4521,7 @@ function createHooksServer() {
|
|
|
4263
4521
|
const settingsPath = getSettingsPath(scope);
|
|
4264
4522
|
const issues = [];
|
|
4265
4523
|
const healthy = [];
|
|
4266
|
-
const settingsExist =
|
|
4524
|
+
const settingsExist = existsSync5(settingsPath);
|
|
4267
4525
|
if (!settingsExist) {
|
|
4268
4526
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
4269
4527
|
}
|
|
@@ -4276,13 +4534,13 @@ function createHooksServer() {
|
|
|
4276
4534
|
continue;
|
|
4277
4535
|
}
|
|
4278
4536
|
const hookDir = getHookPath(name);
|
|
4279
|
-
if (!
|
|
4537
|
+
if (!existsSync5(join5(hookDir, "src", "hook.ts"))) {
|
|
4280
4538
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
4281
4539
|
hookHealthy = false;
|
|
4282
4540
|
}
|
|
4283
4541
|
if (meta && settingsExist) {
|
|
4284
4542
|
try {
|
|
4285
|
-
const settings = JSON.parse(
|
|
4543
|
+
const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
|
|
4286
4544
|
const eventHooks = settings.hooks?.[meta.event] || [];
|
|
4287
4545
|
const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
|
|
4288
4546
|
const match = h.command?.match(/^hooks run (\w+)/);
|
|
@@ -4313,10 +4571,10 @@ function createHooksServer() {
|
|
|
4313
4571
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
4314
4572
|
}
|
|
4315
4573
|
const hookPath = getHookPath(name);
|
|
4316
|
-
const readmePath =
|
|
4574
|
+
const readmePath = join5(hookPath, "README.md");
|
|
4317
4575
|
let readme = "";
|
|
4318
|
-
if (
|
|
4319
|
-
readme =
|
|
4576
|
+
if (existsSync5(readmePath)) {
|
|
4577
|
+
readme = readFileSync4(readmePath, "utf-8");
|
|
4320
4578
|
}
|
|
4321
4579
|
return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
|
|
4322
4580
|
}
|
|
@@ -4365,8 +4623,8 @@ function createHooksServer() {
|
|
|
4365
4623
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
4366
4624
|
}
|
|
4367
4625
|
const hookDir = getHookPath(name);
|
|
4368
|
-
const hookScript =
|
|
4369
|
-
if (!
|
|
4626
|
+
const hookScript = join5(hookDir, "src", "hook.ts");
|
|
4627
|
+
if (!existsSync5(hookScript)) {
|
|
4370
4628
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook script not found: ${hookScript}` }) }] };
|
|
4371
4629
|
}
|
|
4372
4630
|
let hookInput = { ...input };
|
|
@@ -4457,7 +4715,7 @@ function createHooksServer() {
|
|
|
4457
4715
|
const ctx = {
|
|
4458
4716
|
scope,
|
|
4459
4717
|
settings_path: settingsPath,
|
|
4460
|
-
settings_exists:
|
|
4718
|
+
settings_exists: existsSync5(settingsPath),
|
|
4461
4719
|
registered_hooks: hooks,
|
|
4462
4720
|
hook_count: hooks.length,
|
|
4463
4721
|
healthy,
|
|
@@ -4495,8 +4753,8 @@ function createHooksServer() {
|
|
|
4495
4753
|
const input = { tool_name, tool_input };
|
|
4496
4754
|
const results = await Promise.all(matchingHooks.map(async (name) => {
|
|
4497
4755
|
const hookDir = getHookPath(name);
|
|
4498
|
-
const hookScript =
|
|
4499
|
-
if (!
|
|
4756
|
+
const hookScript = join5(hookDir, "src", "hook.ts");
|
|
4757
|
+
if (!existsSync5(hookScript))
|
|
4500
4758
|
return { name, decision: "approve", error: "script not found" };
|
|
4501
4759
|
const proc = Bun.spawn(["bun", "run", hookScript], {
|
|
4502
4760
|
stdin: new Response(JSON.stringify(input)),
|
|
@@ -4564,8 +4822,8 @@ function createHooksServer() {
|
|
|
4564
4822
|
const meta = getHook(name);
|
|
4565
4823
|
if (!meta)
|
|
4566
4824
|
return { name, error: `Hook '${name}' not found` };
|
|
4567
|
-
const hookScript =
|
|
4568
|
-
if (!
|
|
4825
|
+
const hookScript = join5(getHookPath(name), "src", "hook.ts");
|
|
4826
|
+
if (!existsSync5(hookScript))
|
|
4569
4827
|
return { name, error: "script not found" };
|
|
4570
4828
|
const proc = Bun.spawn(["bun", "run", hookScript], {
|
|
4571
4829
|
stdin: new Response(JSON.stringify(input)),
|
|
@@ -4598,8 +4856,8 @@ function createHooksServer() {
|
|
|
4598
4856
|
const settingsPath = getSettingsPath(scope);
|
|
4599
4857
|
let settings = {};
|
|
4600
4858
|
try {
|
|
4601
|
-
if (
|
|
4602
|
-
settings = JSON.parse(
|
|
4859
|
+
if (existsSync5(settingsPath))
|
|
4860
|
+
settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
|
|
4603
4861
|
} catch {}
|
|
4604
4862
|
if (!settings.hooks)
|
|
4605
4863
|
settings.hooks = {};
|
|
@@ -4607,9 +4865,9 @@ function createHooksServer() {
|
|
|
4607
4865
|
if (!disabled.includes(name))
|
|
4608
4866
|
disabled.push(name);
|
|
4609
4867
|
settings.hooks.__disabled = disabled;
|
|
4610
|
-
const { writeFileSync: writeFileSync3, mkdirSync:
|
|
4868
|
+
const { writeFileSync: writeFileSync3, mkdirSync: mkdirSync4 } = await import("fs");
|
|
4611
4869
|
const { dirname: dirname3 } = await import("path");
|
|
4612
|
-
|
|
4870
|
+
mkdirSync4(dirname3(settingsPath), { recursive: true });
|
|
4613
4871
|
writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
4614
4872
|
`);
|
|
4615
4873
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
|
|
@@ -4621,8 +4879,8 @@ function createHooksServer() {
|
|
|
4621
4879
|
const settingsPath = getSettingsPath(scope);
|
|
4622
4880
|
let settings = {};
|
|
4623
4881
|
try {
|
|
4624
|
-
if (
|
|
4625
|
-
settings = JSON.parse(
|
|
4882
|
+
if (existsSync5(settingsPath))
|
|
4883
|
+
settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
|
|
4626
4884
|
} catch {}
|
|
4627
4885
|
if (settings.hooks?.__disabled) {
|
|
4628
4886
|
settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
|
|
@@ -4645,6 +4903,104 @@ function createHooksServer() {
|
|
|
4645
4903
|
const profiles = listProfiles();
|
|
4646
4904
|
return { content: [{ type: "text", text: JSON.stringify(profiles) }] };
|
|
4647
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
|
+
});
|
|
4648
5004
|
return server;
|
|
4649
5005
|
}
|
|
4650
5006
|
async function startSSEServer(port = MCP_PORT) {
|
|
@@ -4690,7 +5046,7 @@ var init_server = __esm(() => {
|
|
|
4690
5046
|
init_installer();
|
|
4691
5047
|
init_profiles();
|
|
4692
5048
|
__dirname3 = dirname2(fileURLToPath2(import.meta.url));
|
|
4693
|
-
pkg = JSON.parse(
|
|
5049
|
+
pkg = JSON.parse(readFileSync4(join5(__dirname3, "..", "..", "package.json"), "utf-8"));
|
|
4694
5050
|
});
|
|
4695
5051
|
|
|
4696
5052
|
// src/cli/index.tsx
|
|
@@ -4714,8 +5070,8 @@ var {
|
|
|
4714
5070
|
|
|
4715
5071
|
// src/cli/index.tsx
|
|
4716
5072
|
import chalk2 from "chalk";
|
|
4717
|
-
import { existsSync as
|
|
4718
|
-
import { join as
|
|
5073
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
5074
|
+
import { join as join6, dirname as dirname3 } from "path";
|
|
4719
5075
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4720
5076
|
|
|
4721
5077
|
// src/cli/components/App.tsx
|
|
@@ -5893,8 +6249,8 @@ init_installer();
|
|
|
5893
6249
|
init_profiles();
|
|
5894
6250
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
5895
6251
|
var __dirname4 = dirname3(fileURLToPath3(import.meta.url));
|
|
5896
|
-
var pkgPath =
|
|
5897
|
-
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"));
|
|
5898
6254
|
var program2 = new Command;
|
|
5899
6255
|
function resolveScope(options) {
|
|
5900
6256
|
if (options.project)
|
|
@@ -5964,8 +6320,8 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
5964
6320
|
process.exit(1);
|
|
5965
6321
|
}
|
|
5966
6322
|
const hookDir = getHookPath(hook);
|
|
5967
|
-
const hookScript =
|
|
5968
|
-
if (!
|
|
6323
|
+
const hookScript = join6(hookDir, "src", "hook.ts");
|
|
6324
|
+
if (!existsSync6(hookScript)) {
|
|
5969
6325
|
console.error(JSON.stringify({ error: `Hook script not found: ${hookScript}` }));
|
|
5970
6326
|
process.exit(1);
|
|
5971
6327
|
}
|
|
@@ -6265,7 +6621,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
6265
6621
|
const settingsPath = getSettingsPath(scope);
|
|
6266
6622
|
const issues = [];
|
|
6267
6623
|
const healthy = [];
|
|
6268
|
-
const settingsExist =
|
|
6624
|
+
const settingsExist = existsSync6(settingsPath);
|
|
6269
6625
|
if (!settingsExist) {
|
|
6270
6626
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
6271
6627
|
}
|
|
@@ -6279,14 +6635,14 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
6279
6635
|
continue;
|
|
6280
6636
|
}
|
|
6281
6637
|
const hookDir = getHookPath(name);
|
|
6282
|
-
const hookScript =
|
|
6283
|
-
if (!
|
|
6638
|
+
const hookScript = join6(hookDir, "src", "hook.ts");
|
|
6639
|
+
if (!existsSync6(hookScript)) {
|
|
6284
6640
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
6285
6641
|
hookHealthy = false;
|
|
6286
6642
|
}
|
|
6287
6643
|
if (meta && settingsExist) {
|
|
6288
6644
|
try {
|
|
6289
|
-
const settings = JSON.parse(
|
|
6645
|
+
const settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
|
|
6290
6646
|
const eventHooks = settings.hooks?.[meta.event] || [];
|
|
6291
6647
|
const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
|
|
6292
6648
|
const match = h.command?.match(/^hooks run (\w+)/);
|
|
@@ -6384,10 +6740,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
6384
6740
|
return;
|
|
6385
6741
|
}
|
|
6386
6742
|
const hookPath = getHookPath(hook);
|
|
6387
|
-
const readmePath =
|
|
6743
|
+
const readmePath = join6(hookPath, "README.md");
|
|
6388
6744
|
let readme = "";
|
|
6389
|
-
if (
|
|
6390
|
-
readme =
|
|
6745
|
+
if (existsSync6(readmePath)) {
|
|
6746
|
+
readme = readFileSync5(readmePath, "utf-8");
|
|
6391
6747
|
}
|
|
6392
6748
|
if (options.json) {
|
|
6393
6749
|
console.log(JSON.stringify({ ...meta, readme }));
|
|
@@ -6569,9 +6925,9 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
6569
6925
|
if (file === "-") {
|
|
6570
6926
|
raw = await new Response(Bun.stdin.stream()).text();
|
|
6571
6927
|
} else {
|
|
6572
|
-
const { readFileSync:
|
|
6928
|
+
const { readFileSync: readFileSync6 } = await import("fs");
|
|
6573
6929
|
try {
|
|
6574
|
-
raw =
|
|
6930
|
+
raw = readFileSync6(file, "utf-8");
|
|
6575
6931
|
} catch {
|
|
6576
6932
|
if (options.json) {
|
|
6577
6933
|
console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
|
|
@@ -6602,6 +6958,154 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
6602
6958
|
console.log(chalk2.dim(` Skipped ${result.skipped} (already exist or invalid)`));
|
|
6603
6959
|
}
|
|
6604
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
|
+
});
|
|
6605
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) => {
|
|
6606
7110
|
if (options.stdio) {
|
|
6607
7111
|
const { startStdioServer: startStdioServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|