@hasna/hooks 0.6.3 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/bin/index.js +2124 -1405
- package/bin/serve.js +34 -4
- package/dist/db/migrations/005_subagent_start_event.d.ts +10 -0
- package/dist/db/schema.d.ts +2 -2
- package/dist/index.d.ts +9 -5
- package/dist/index.js +647 -194
- package/dist/lib/db-writer.d.ts +40 -0
- package/dist/lib/installer.d.ts +37 -0
- package/dist/lib/resolve.d.ts +2 -0
- package/dist/lib/run.d.ts +8 -0
- package/dist/lib/store.d.ts +15 -0
- package/dist/lib/sync.d.ts +19 -0
- package/dist/storage.js +34 -2
- package/package.json +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared hook DB writer — single write path for all observability hooks.
|
|
3
|
+
* Never throws: errors are written to stderr only.
|
|
4
|
+
*/
|
|
5
|
+
import type { HookEventRow } from "../db/schema";
|
|
6
|
+
export type HookEventInput = Omit<HookEventRow, "id" | "timestamp"> & {
|
|
7
|
+
timestamp?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function writeHookEvent(event: HookEventInput): void;
|
|
10
|
+
/** Normalize a hook_event_name from hook input to a value the schema accepts. */
|
|
11
|
+
export declare function normalizeEventType(value: unknown): string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Pick the event type for a run record: the hook input's hook_event_name when
|
|
14
|
+
* it is valid, else the hook's declared event. Guarantees a row lands for
|
|
15
|
+
* every execution even when the agent passes an unknown event name.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveEventType(inputEvent: unknown, fallbackEvent: string | null | undefined): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Record one hook execution in hook_events — the row `hooks log` reads.
|
|
20
|
+
* Written by every run path (CLI run, SDK runHook, MCP run tools) so a real
|
|
21
|
+
* fire is always observable (bug ef58dcb7: 0 rows after real fires).
|
|
22
|
+
*
|
|
23
|
+
* Never throws: observability must not break execution. An invalid event type
|
|
24
|
+
* or a missing DB is reported on stderr and skipped.
|
|
25
|
+
*/
|
|
26
|
+
export declare function recordHookRun(record: {
|
|
27
|
+
hookName: string;
|
|
28
|
+
eventType: string | null;
|
|
29
|
+
version?: string | null;
|
|
30
|
+
sha256?: string | null;
|
|
31
|
+
sessionId?: string | null;
|
|
32
|
+
toolName?: string | null;
|
|
33
|
+
toolInput?: unknown;
|
|
34
|
+
result?: "continue" | "block" | null;
|
|
35
|
+
error?: string | null;
|
|
36
|
+
exitCode: number;
|
|
37
|
+
durationMs: number;
|
|
38
|
+
projectDir?: string | null;
|
|
39
|
+
metadata?: Record<string, unknown>;
|
|
40
|
+
}): void;
|
package/dist/lib/installer.d.ts
CHANGED
|
@@ -54,4 +54,41 @@ export declare function getRegisteredHooks(scope?: Scope): string[];
|
|
|
54
54
|
/** @deprecated Use getRegisteredHooks instead */
|
|
55
55
|
export declare const getInstalledHooks: typeof getRegisteredHooks;
|
|
56
56
|
export declare function removeHook(name: string, scope?: Scope, target?: Target): boolean;
|
|
57
|
+
export interface UninstallResult {
|
|
58
|
+
name: string;
|
|
59
|
+
removed: boolean;
|
|
60
|
+
source: "custom" | "bundled" | "registered-only" | null;
|
|
61
|
+
settingsScopes: Scope[];
|
|
62
|
+
storeDirRemoved: boolean;
|
|
63
|
+
pinRemoved: boolean;
|
|
64
|
+
dbRecordRemoved: boolean;
|
|
65
|
+
/** Targets whose config still registers the hook after removal (e.g. codewith TOML the caller must edit itself). */
|
|
66
|
+
registrationsRemaining: string[];
|
|
67
|
+
error?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Lossless removal of a `hooks run <name>` entry from a Codewith config.toml.
|
|
71
|
+
*
|
|
72
|
+
* Works on sections split by blank lines — the shape buildCodewithTomlFragment
|
|
73
|
+
* writes: a `[[hooks.EVENT]]` header section (with optional matcher), then one
|
|
74
|
+
* `[[hooks.EVENT.hooks]]` entry section per hook containing
|
|
75
|
+
* `command = "hooks run <name>"`. Only sections positively identified as this
|
|
76
|
+
* hook's entries are removed; the enclosing EVENT header is dropped only when
|
|
77
|
+
* every one of its entries belonged to this hook. Anything ambiguous is
|
|
78
|
+
* preserved verbatim.
|
|
79
|
+
*/
|
|
80
|
+
export declare function removeCodewithHookEntry(configText: string, name: string): {
|
|
81
|
+
text: string;
|
|
82
|
+
removed: boolean;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Full uninstall — the settings registration, the store directory (custom
|
|
86
|
+
* hooks), the lock pin and the DB record are all removed. Bundled hooks keep
|
|
87
|
+
* their package files (they belong to the package, not the store).
|
|
88
|
+
*
|
|
89
|
+
* Resolves custom and registry-synced hooks, which live in the custom store
|
|
90
|
+
* dir, as well as bundled ones (QA-1 BUG-A / QA-4: remove was bundled-only
|
|
91
|
+
* and never cleaned the store/lock/DB).
|
|
92
|
+
*/
|
|
93
|
+
export declare function uninstallHook(name: string, scope?: Scope, target?: Target): UninstallResult;
|
|
57
94
|
export {};
|
package/dist/lib/resolve.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface ResolvedHook {
|
|
|
12
12
|
description: string;
|
|
13
13
|
scriptPath: string;
|
|
14
14
|
meta: HookMeta;
|
|
15
|
+
/** The hook's own declared timeout (manifest timeout_ms), or null. */
|
|
16
|
+
timeoutMs: number | null;
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* Locate the bundled hooks directory at runtime from the executing module's
|
package/dist/lib/run.d.ts
CHANGED
|
@@ -34,4 +34,12 @@ export interface VerifiedRunResult {
|
|
|
34
34
|
stderr: string;
|
|
35
35
|
exitCode: number;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Raised when a verified script exceeds its timeout. The whole process group
|
|
39
|
+
* was killed, so no descendant survives (see executeVerifiedScript).
|
|
40
|
+
*/
|
|
41
|
+
export declare class HookTimeoutError extends Error {
|
|
42
|
+
readonly timeoutMs: number;
|
|
43
|
+
constructor(timeoutMs: number);
|
|
44
|
+
}
|
|
37
45
|
export declare function executeVerifiedScript(options: VerifiedRunOptions): Promise<VerifiedRunResult>;
|
package/dist/lib/store.d.ts
CHANGED
|
@@ -44,6 +44,21 @@ export declare function writeLock(lock: LockFile): string;
|
|
|
44
44
|
export declare function setPinnedHook(name: string, entry: LockEntry): string;
|
|
45
45
|
export declare function getPinnedHook(name: string): LockEntry | undefined;
|
|
46
46
|
export declare function removePinnedHook(name: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Pin a hook at install time — the ACTUAL installed version and sha, so the
|
|
49
|
+
* first run is trusted with real provenance instead of a 0.0.0 placeholder
|
|
50
|
+
* (QA-1 P3: install pinned 0.0.0 until trust).
|
|
51
|
+
*/
|
|
52
|
+
export declare function pinInstalledHook(name: string, version: string, sha256: string, source: string, sourceRef?: string | null): void;
|
|
53
|
+
/**
|
|
54
|
+
* Remove every store-side record of a hook: the lock pin and the DB row.
|
|
55
|
+
* Does not touch hook files on disk — callers decide whether the hook lives
|
|
56
|
+
* in the package (bundled, keep) or the custom store dir (remove).
|
|
57
|
+
*/
|
|
58
|
+
export declare function removeHookFromStore(name: string): {
|
|
59
|
+
removedPin: boolean;
|
|
60
|
+
removedRecord: boolean;
|
|
61
|
+
};
|
|
47
62
|
export interface TrustCheck {
|
|
48
63
|
ok: boolean;
|
|
49
64
|
pinned: boolean;
|
package/dist/lib/sync.d.ts
CHANGED
|
@@ -33,3 +33,22 @@ export declare function planSync(): Promise<SyncPlan>;
|
|
|
33
33
|
export declare function syncHooks(options?: {
|
|
34
34
|
dryRun?: boolean;
|
|
35
35
|
}): Promise<SyncPlan>;
|
|
36
|
+
export interface PinnedHookInstall {
|
|
37
|
+
name: string;
|
|
38
|
+
version: string;
|
|
39
|
+
sha256: string;
|
|
40
|
+
source: string;
|
|
41
|
+
source_ref: string;
|
|
42
|
+
artifact: ArtifactResponse;
|
|
43
|
+
scriptPath: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Fetch one exact hook version from the remote registry, verify its sha
|
|
47
|
+
* against the remote lock, write it to the custom store, and pin it.
|
|
48
|
+
* Powers `hooks install <name>@<version>` / `hooks update <name>@<version>`
|
|
49
|
+
* (QA-2 finding: pinned-version install/update was unsupported).
|
|
50
|
+
*
|
|
51
|
+
* Requires an api_url (remote registry). The sha check is against the remote
|
|
52
|
+
* lock — the exact version named by the user, never a mutable latest.
|
|
53
|
+
*/
|
|
54
|
+
export declare function fetchPinnedHook(name: string, version: string, apiUrl: string): Promise<PinnedHookInstall>;
|
package/dist/storage.js
CHANGED
|
@@ -22,7 +22,7 @@ var CREATE_HOOK_EVENTS_TABLE = `
|
|
|
22
22
|
timestamp TEXT NOT NULL,
|
|
23
23
|
session_id TEXT NOT NULL,
|
|
24
24
|
hook_name TEXT NOT NULL,
|
|
25
|
-
event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit')),
|
|
25
|
+
event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart')),
|
|
26
26
|
tool_name TEXT,
|
|
27
27
|
tool_input TEXT,
|
|
28
28
|
result TEXT CHECK (result IN ('continue', 'block', NULL)),
|
|
@@ -137,6 +137,35 @@ var init_004_hooks_table = __esm(() => {
|
|
|
137
137
|
];
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
// src/db/migrations/005_subagent_start_event.ts
|
|
141
|
+
function up5(db) {
|
|
142
|
+
const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("hook_events");
|
|
143
|
+
if (!row?.sql)
|
|
144
|
+
return;
|
|
145
|
+
if (row.sql.includes("SubagentStart"))
|
|
146
|
+
return;
|
|
147
|
+
db.exec("BEGIN");
|
|
148
|
+
try {
|
|
149
|
+
db.exec("ALTER TABLE hook_events RENAME TO hook_events_old");
|
|
150
|
+
db.exec(CREATE_HOOK_EVENTS_TABLE);
|
|
151
|
+
db.exec(`INSERT INTO hook_events
|
|
152
|
+
(id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
|
|
153
|
+
SELECT id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata
|
|
154
|
+
FROM hook_events_old`);
|
|
155
|
+
db.exec("DROP TABLE hook_events_old");
|
|
156
|
+
for (const idx of CREATE_INDEXES) {
|
|
157
|
+
db.exec(idx);
|
|
158
|
+
}
|
|
159
|
+
db.exec("COMMIT");
|
|
160
|
+
} catch (err) {
|
|
161
|
+
db.exec("ROLLBACK");
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
var init_005_subagent_start_event = __esm(() => {
|
|
166
|
+
init_schema();
|
|
167
|
+
});
|
|
168
|
+
|
|
140
169
|
// src/db/migrations/index.ts
|
|
141
170
|
function ensureMigrationsTable(db) {
|
|
142
171
|
db.exec(`
|
|
@@ -169,11 +198,13 @@ var init_migrations = __esm(() => {
|
|
|
169
198
|
init_002_session_events();
|
|
170
199
|
init_003_user_prompt_submit_event();
|
|
171
200
|
init_004_hooks_table();
|
|
201
|
+
init_005_subagent_start_event();
|
|
172
202
|
MIGRATIONS = [
|
|
173
203
|
{ version: "001_initial", up },
|
|
174
204
|
{ version: "002_session_events", up: up2 },
|
|
175
205
|
{ version: "003_user_prompt_submit_event", up: up3 },
|
|
176
|
-
{ version: "004_hooks_table", up: up4 }
|
|
206
|
+
{ version: "004_hooks_table", up: up4 },
|
|
207
|
+
{ version: "005_subagent_start_event", up: up5 }
|
|
177
208
|
];
|
|
178
209
|
});
|
|
179
210
|
|
|
@@ -343,6 +374,7 @@ function getDb() {
|
|
|
343
374
|
const isNew = dbPath === ":memory:" || !existsSync2(dbPath);
|
|
344
375
|
ensureDir(dbPath);
|
|
345
376
|
instance = new Database(dbPath);
|
|
377
|
+
instance.exec("PRAGMA busy_timeout=5000");
|
|
346
378
|
instance.exec("PRAGMA journal_mode=WAL");
|
|
347
379
|
instance.exec("PRAGMA foreign_keys=ON");
|
|
348
380
|
runMigrations(instance);
|
package/package.json
CHANGED