@mauribadnights/clooks 0.1.0

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.
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ // clooks hook handlers — execution engine
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.resetHandlerStates = resetHandlerStates;
5
+ exports.getHandlerStates = getHandlerStates;
6
+ exports.executeHandlers = executeHandlers;
7
+ exports.executeScriptHandler = executeScriptHandler;
8
+ exports.executeInlineHandler = executeInlineHandler;
9
+ const child_process_1 = require("child_process");
10
+ const url_1 = require("url");
11
+ const path_1 = require("path");
12
+ const constants_js_1 = require("./constants.js");
13
+ /** Runtime state per handler ID */
14
+ const handlerStates = new Map();
15
+ function getState(id) {
16
+ let state = handlerStates.get(id);
17
+ if (!state) {
18
+ state = { consecutiveFailures: 0, disabled: false, totalFires: 0, totalErrors: 0 };
19
+ handlerStates.set(id, state);
20
+ }
21
+ return state;
22
+ }
23
+ /** Reset all handler states (useful for testing) */
24
+ function resetHandlerStates() {
25
+ handlerStates.clear();
26
+ }
27
+ /** Get a copy of the handler states map */
28
+ function getHandlerStates() {
29
+ return new Map(handlerStates);
30
+ }
31
+ /**
32
+ * Execute all handlers for an event in parallel.
33
+ * Returns merged results array.
34
+ */
35
+ async function executeHandlers(_event, input, handlers) {
36
+ const promises = handlers.map(async (handler) => {
37
+ // Skip disabled handlers (both manifest-disabled and auto-disabled)
38
+ if (handler.enabled === false) {
39
+ return { id: handler.id, ok: true, output: undefined, duration_ms: 0 };
40
+ }
41
+ const state = getState(handler.id);
42
+ if (state.disabled) {
43
+ return {
44
+ id: handler.id,
45
+ ok: false,
46
+ error: `Auto-disabled after ${constants_js_1.MAX_CONSECUTIVE_FAILURES} consecutive failures`,
47
+ duration_ms: 0,
48
+ };
49
+ }
50
+ state.totalFires++;
51
+ const start = performance.now();
52
+ let result;
53
+ try {
54
+ if (handler.type === 'script') {
55
+ result = await executeScriptHandler(handler, input);
56
+ }
57
+ else if (handler.type === 'inline') {
58
+ result = await executeInlineHandler(handler, input);
59
+ }
60
+ else {
61
+ result = {
62
+ id: handler.id,
63
+ ok: false,
64
+ error: `Unknown handler type: ${handler.type}`,
65
+ duration_ms: 0,
66
+ };
67
+ }
68
+ }
69
+ catch (err) {
70
+ result = {
71
+ id: handler.id,
72
+ ok: false,
73
+ error: err instanceof Error ? err.message : String(err),
74
+ duration_ms: performance.now() - start,
75
+ };
76
+ }
77
+ // Update failure tracking
78
+ if (result.ok) {
79
+ state.consecutiveFailures = 0;
80
+ }
81
+ else {
82
+ state.consecutiveFailures++;
83
+ state.totalErrors++;
84
+ if (state.consecutiveFailures >= constants_js_1.MAX_CONSECUTIVE_FAILURES) {
85
+ state.disabled = true;
86
+ }
87
+ }
88
+ return result;
89
+ });
90
+ return Promise.all(promises);
91
+ }
92
+ /**
93
+ * Execute a script handler: spawn a child process, pipe input JSON to stdin,
94
+ * read stdout as JSON response.
95
+ */
96
+ function executeScriptHandler(handler, input) {
97
+ const timeout = handler.timeout ?? constants_js_1.DEFAULT_HANDLER_TIMEOUT;
98
+ return new Promise((resolve) => {
99
+ const start = performance.now();
100
+ const child = (0, child_process_1.spawn)('sh', ['-c', handler.command], {
101
+ stdio: ['pipe', 'pipe', 'pipe'],
102
+ timeout,
103
+ });
104
+ let stdout = '';
105
+ let stderr = '';
106
+ child.stdout.on('data', (data) => {
107
+ stdout += data.toString();
108
+ });
109
+ child.stderr.on('data', (data) => {
110
+ stderr += data.toString();
111
+ });
112
+ // Write input JSON to stdin and close it
113
+ child.stdin.write(JSON.stringify(input));
114
+ child.stdin.end();
115
+ const timer = setTimeout(() => {
116
+ child.kill('SIGTERM');
117
+ }, timeout);
118
+ child.on('close', (code) => {
119
+ clearTimeout(timer);
120
+ const duration_ms = performance.now() - start;
121
+ if (code !== 0) {
122
+ resolve({
123
+ id: handler.id,
124
+ ok: false,
125
+ error: `Exit code ${code}${stderr ? ': ' + stderr.trim() : ''}`,
126
+ duration_ms,
127
+ });
128
+ return;
129
+ }
130
+ // Try to parse stdout as JSON
131
+ let output = undefined;
132
+ if (stdout.trim()) {
133
+ try {
134
+ output = JSON.parse(stdout.trim());
135
+ }
136
+ catch {
137
+ // If stdout isn't valid JSON, wrap it as additionalContext
138
+ output = { additionalContext: stdout.trim() };
139
+ }
140
+ }
141
+ resolve({ id: handler.id, ok: true, output, duration_ms });
142
+ });
143
+ child.on('error', (err) => {
144
+ clearTimeout(timer);
145
+ resolve({
146
+ id: handler.id,
147
+ ok: false,
148
+ error: `Spawn error: ${err.message}`,
149
+ duration_ms: performance.now() - start,
150
+ });
151
+ });
152
+ });
153
+ }
154
+ /**
155
+ * Execute an inline handler: dynamically import a JS module and call its default export.
156
+ */
157
+ async function executeInlineHandler(handler, input) {
158
+ const timeout = handler.timeout ?? constants_js_1.DEFAULT_HANDLER_TIMEOUT;
159
+ const start = performance.now();
160
+ try {
161
+ const modulePath = (0, path_1.resolve)(handler.module);
162
+ const moduleUrl = (0, url_1.pathToFileURL)(modulePath).href;
163
+ const mod = await import(moduleUrl);
164
+ if (typeof mod.default !== 'function') {
165
+ return {
166
+ id: handler.id,
167
+ ok: false,
168
+ error: `Module "${handler.module}" does not export a default function`,
169
+ duration_ms: performance.now() - start,
170
+ };
171
+ }
172
+ // Run with timeout
173
+ const result = await Promise.race([
174
+ mod.default(input),
175
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Inline handler timed out after ${timeout}ms`)), timeout)),
176
+ ]);
177
+ return {
178
+ id: handler.id,
179
+ ok: true,
180
+ output: result,
181
+ duration_ms: performance.now() - start,
182
+ };
183
+ }
184
+ catch (err) {
185
+ return {
186
+ id: handler.id,
187
+ ok: false,
188
+ error: err instanceof Error ? err.message : String(err),
189
+ duration_ms: performance.now() - start,
190
+ };
191
+ }
192
+ }
@@ -0,0 +1,9 @@
1
+ export { createServer, startDaemon, stopDaemon, isDaemonRunning } from './server.js';
2
+ export { loadManifest, validateManifest, createDefaultManifest } from './manifest.js';
3
+ export { MetricsCollector } from './metrics.js';
4
+ export { migrate, restore, getSettingsPath } from './migrate.js';
5
+ export type { MigratePathOptions } from './migrate.js';
6
+ export { runDoctor } from './doctor.js';
7
+ export { executeHandlers } from './handlers.js';
8
+ export { DEFAULT_PORT, CONFIG_DIR, MANIFEST_PATH, PID_FILE, METRICS_FILE, LOG_FILE } from './constants.js';
9
+ export type { HookEvent, HookInput, HandlerType, HandlerConfig, Manifest, HandlerResult, MetricEntry, HandlerState, DiagnosticResult, } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ // clooks — public API exports
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.LOG_FILE = exports.METRICS_FILE = exports.PID_FILE = exports.MANIFEST_PATH = exports.CONFIG_DIR = exports.DEFAULT_PORT = exports.executeHandlers = exports.runDoctor = exports.getSettingsPath = exports.restore = exports.migrate = exports.MetricsCollector = exports.createDefaultManifest = exports.validateManifest = exports.loadManifest = exports.isDaemonRunning = exports.stopDaemon = exports.startDaemon = exports.createServer = void 0;
5
+ var server_js_1 = require("./server.js");
6
+ Object.defineProperty(exports, "createServer", { enumerable: true, get: function () { return server_js_1.createServer; } });
7
+ Object.defineProperty(exports, "startDaemon", { enumerable: true, get: function () { return server_js_1.startDaemon; } });
8
+ Object.defineProperty(exports, "stopDaemon", { enumerable: true, get: function () { return server_js_1.stopDaemon; } });
9
+ Object.defineProperty(exports, "isDaemonRunning", { enumerable: true, get: function () { return server_js_1.isDaemonRunning; } });
10
+ var manifest_js_1 = require("./manifest.js");
11
+ Object.defineProperty(exports, "loadManifest", { enumerable: true, get: function () { return manifest_js_1.loadManifest; } });
12
+ Object.defineProperty(exports, "validateManifest", { enumerable: true, get: function () { return manifest_js_1.validateManifest; } });
13
+ Object.defineProperty(exports, "createDefaultManifest", { enumerable: true, get: function () { return manifest_js_1.createDefaultManifest; } });
14
+ var metrics_js_1 = require("./metrics.js");
15
+ Object.defineProperty(exports, "MetricsCollector", { enumerable: true, get: function () { return metrics_js_1.MetricsCollector; } });
16
+ var migrate_js_1 = require("./migrate.js");
17
+ Object.defineProperty(exports, "migrate", { enumerable: true, get: function () { return migrate_js_1.migrate; } });
18
+ Object.defineProperty(exports, "restore", { enumerable: true, get: function () { return migrate_js_1.restore; } });
19
+ Object.defineProperty(exports, "getSettingsPath", { enumerable: true, get: function () { return migrate_js_1.getSettingsPath; } });
20
+ var doctor_js_1 = require("./doctor.js");
21
+ Object.defineProperty(exports, "runDoctor", { enumerable: true, get: function () { return doctor_js_1.runDoctor; } });
22
+ var handlers_js_1 = require("./handlers.js");
23
+ Object.defineProperty(exports, "executeHandlers", { enumerable: true, get: function () { return handlers_js_1.executeHandlers; } });
24
+ var constants_js_1 = require("./constants.js");
25
+ Object.defineProperty(exports, "DEFAULT_PORT", { enumerable: true, get: function () { return constants_js_1.DEFAULT_PORT; } });
26
+ Object.defineProperty(exports, "CONFIG_DIR", { enumerable: true, get: function () { return constants_js_1.CONFIG_DIR; } });
27
+ Object.defineProperty(exports, "MANIFEST_PATH", { enumerable: true, get: function () { return constants_js_1.MANIFEST_PATH; } });
28
+ Object.defineProperty(exports, "PID_FILE", { enumerable: true, get: function () { return constants_js_1.PID_FILE; } });
29
+ Object.defineProperty(exports, "METRICS_FILE", { enumerable: true, get: function () { return constants_js_1.METRICS_FILE; } });
30
+ Object.defineProperty(exports, "LOG_FILE", { enumerable: true, get: function () { return constants_js_1.LOG_FILE; } });
@@ -0,0 +1,15 @@
1
+ import type { Manifest } from './types.js';
2
+ /**
3
+ * Load and validate the manifest from disk.
4
+ * Returns a Manifest with empty handlers if the file doesn't exist.
5
+ */
6
+ export declare function loadManifest(): Manifest;
7
+ /**
8
+ * Validate a parsed manifest object.
9
+ * Throws on invalid structure.
10
+ */
11
+ export declare function validateManifest(manifest: Manifest): void;
12
+ /**
13
+ * Create a default commented example manifest.yaml in CONFIG_DIR.
14
+ */
15
+ export declare function createDefaultManifest(): string;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ // clooks manifest parser (YAML hook definitions)
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.loadManifest = loadManifest;
5
+ exports.validateManifest = validateManifest;
6
+ exports.createDefaultManifest = createDefaultManifest;
7
+ const fs_1 = require("fs");
8
+ const yaml_1 = require("yaml");
9
+ const constants_js_1 = require("./constants.js");
10
+ /**
11
+ * Load and validate the manifest from disk.
12
+ * Returns a Manifest with empty handlers if the file doesn't exist.
13
+ */
14
+ function loadManifest() {
15
+ if (!(0, fs_1.existsSync)(constants_js_1.MANIFEST_PATH)) {
16
+ return { handlers: {} };
17
+ }
18
+ const raw = (0, fs_1.readFileSync)(constants_js_1.MANIFEST_PATH, 'utf-8');
19
+ const parsed = (0, yaml_1.parse)(raw);
20
+ if (!parsed || typeof parsed !== 'object') {
21
+ return { handlers: {} };
22
+ }
23
+ const manifest = parsed;
24
+ validateManifest(manifest);
25
+ return manifest;
26
+ }
27
+ /**
28
+ * Validate a parsed manifest object.
29
+ * Throws on invalid structure.
30
+ */
31
+ function validateManifest(manifest) {
32
+ if (!manifest.handlers || typeof manifest.handlers !== 'object') {
33
+ throw new Error('Manifest must have a "handlers" object');
34
+ }
35
+ const seenIds = new Set();
36
+ for (const [eventName, handlers] of Object.entries(manifest.handlers)) {
37
+ if (!constants_js_1.HOOK_EVENTS.includes(eventName)) {
38
+ throw new Error(`Unknown hook event: "${eventName}". Valid events: ${constants_js_1.HOOK_EVENTS.join(', ')}`);
39
+ }
40
+ if (!Array.isArray(handlers)) {
41
+ throw new Error(`Handlers for "${eventName}" must be an array`);
42
+ }
43
+ for (const handler of handlers) {
44
+ if (!handler.id || typeof handler.id !== 'string') {
45
+ throw new Error(`Each handler must have a string "id" (event: ${eventName})`);
46
+ }
47
+ if (seenIds.has(handler.id)) {
48
+ throw new Error(`Duplicate handler id: "${handler.id}"`);
49
+ }
50
+ seenIds.add(handler.id);
51
+ if (!handler.type || !['script', 'inline'].includes(handler.type)) {
52
+ throw new Error(`Handler "${handler.id}" must have type "script" or "inline"`);
53
+ }
54
+ if (handler.type === 'script' && !handler.command) {
55
+ throw new Error(`Script handler "${handler.id}" must have a "command" field`);
56
+ }
57
+ if (handler.type === 'inline' && !handler.module) {
58
+ throw new Error(`Inline handler "${handler.id}" must have a "module" field`);
59
+ }
60
+ }
61
+ }
62
+ // Validate settings if present
63
+ if (manifest.settings) {
64
+ if (manifest.settings.port !== undefined) {
65
+ if (typeof manifest.settings.port !== 'number' || manifest.settings.port < 1 || manifest.settings.port > 65535) {
66
+ throw new Error('settings.port must be a number between 1 and 65535');
67
+ }
68
+ }
69
+ if (manifest.settings.logLevel !== undefined) {
70
+ const validLevels = ['debug', 'info', 'warn', 'error'];
71
+ if (!validLevels.includes(manifest.settings.logLevel)) {
72
+ throw new Error(`settings.logLevel must be one of: ${validLevels.join(', ')}`);
73
+ }
74
+ }
75
+ }
76
+ }
77
+ /**
78
+ * Create a default commented example manifest.yaml in CONFIG_DIR.
79
+ */
80
+ function createDefaultManifest() {
81
+ if (!(0, fs_1.existsSync)(constants_js_1.CONFIG_DIR)) {
82
+ (0, fs_1.mkdirSync)(constants_js_1.CONFIG_DIR, { recursive: true });
83
+ }
84
+ const example = {
85
+ handlers: {
86
+ PreToolUse: [
87
+ {
88
+ id: 'example-guard',
89
+ type: 'script',
90
+ command: 'echo \'{"additionalContext":"checked by clooks"}\'',
91
+ timeout: 3000,
92
+ enabled: true,
93
+ },
94
+ ],
95
+ },
96
+ settings: {
97
+ port: 7890,
98
+ logLevel: 'info',
99
+ },
100
+ };
101
+ const yamlStr = '# clooks manifest — define your hook handlers here\n' +
102
+ '# Docs: https://github.com/mauribadnights/clooks\n' +
103
+ '#\n' +
104
+ '# Handler types:\n' +
105
+ '# script — runs a shell command, pipes hook JSON to stdin, reads stdout\n' +
106
+ '# inline — imports a JS/TS module and calls its default export\n' +
107
+ '#\n' +
108
+ '# Available events:\n' +
109
+ '# SessionStart, UserPromptSubmit, PreToolUse, PostToolUse,\n' +
110
+ '# Stop, SubagentStart, SubagentStop, Notification, ConfigChange\n' +
111
+ '#\n\n' +
112
+ (0, yaml_1.stringify)(example);
113
+ (0, fs_1.writeFileSync)(constants_js_1.MANIFEST_PATH, yamlStr, 'utf-8');
114
+ return constants_js_1.MANIFEST_PATH;
115
+ }
@@ -0,0 +1,27 @@
1
+ import type { MetricEntry } from './types.js';
2
+ interface AggregatedStats {
3
+ event: string;
4
+ fires: number;
5
+ errors: number;
6
+ avgDuration: number;
7
+ minDuration: number;
8
+ maxDuration: number;
9
+ }
10
+ export declare class MetricsCollector {
11
+ private entries;
12
+ /** Record a metric entry in memory and append to disk. */
13
+ record(entry: MetricEntry): void;
14
+ /** Get aggregated stats per event type. */
15
+ getStats(): AggregatedStats[];
16
+ /** Get stats for a specific session. */
17
+ getSessionStats(sessionId: string): AggregatedStats[];
18
+ /** Flush is a no-op since we append on every record, but provided for API completeness. */
19
+ flush(): void;
20
+ /** Format stats as a CLI-friendly table. */
21
+ formatStatsTable(): string;
22
+ /** Estimate how many process spawns were saved. */
23
+ estimateSpawnsSaved(): number;
24
+ /** Load all entries from disk + memory (deduped by combining disk file). */
25
+ private loadAll;
26
+ }
27
+ export {};
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ // clooks metrics and observability
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.MetricsCollector = void 0;
5
+ const fs_1 = require("fs");
6
+ const path_1 = require("path");
7
+ const constants_js_1 = require("./constants.js");
8
+ class MetricsCollector {
9
+ entries = [];
10
+ /** Record a metric entry in memory and append to disk. */
11
+ record(entry) {
12
+ this.entries.push(entry);
13
+ try {
14
+ const dir = (0, path_1.dirname)(constants_js_1.METRICS_FILE);
15
+ if (!(0, fs_1.existsSync)(dir)) {
16
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
17
+ }
18
+ (0, fs_1.appendFileSync)(constants_js_1.METRICS_FILE, JSON.stringify(entry) + '\n', 'utf-8');
19
+ }
20
+ catch {
21
+ // Non-critical — metrics should not crash the daemon
22
+ }
23
+ }
24
+ /** Get aggregated stats per event type. */
25
+ getStats() {
26
+ const all = this.loadAll();
27
+ const byEvent = new Map();
28
+ for (const entry of all) {
29
+ const existing = byEvent.get(entry.event) ?? [];
30
+ existing.push(entry);
31
+ byEvent.set(entry.event, existing);
32
+ }
33
+ const stats = [];
34
+ for (const [event, entries] of byEvent) {
35
+ const durations = entries.map((e) => e.duration_ms);
36
+ stats.push({
37
+ event,
38
+ fires: entries.length,
39
+ errors: entries.filter((e) => !e.ok).length,
40
+ avgDuration: durations.reduce((a, b) => a + b, 0) / durations.length,
41
+ minDuration: Math.min(...durations),
42
+ maxDuration: Math.max(...durations),
43
+ });
44
+ }
45
+ return stats.sort((a, b) => b.fires - a.fires);
46
+ }
47
+ /** Get stats for a specific session. */
48
+ getSessionStats(sessionId) {
49
+ const all = this.loadAll().filter((e) => {
50
+ // MetricEntry doesn't have session_id, but we stored it in the entry if available
51
+ return e.session_id === sessionId;
52
+ });
53
+ const byEvent = new Map();
54
+ for (const entry of all) {
55
+ const existing = byEvent.get(entry.event) ?? [];
56
+ existing.push(entry);
57
+ byEvent.set(entry.event, existing);
58
+ }
59
+ const stats = [];
60
+ for (const [event, entries] of byEvent) {
61
+ const durations = entries.map((e) => e.duration_ms);
62
+ stats.push({
63
+ event,
64
+ fires: entries.length,
65
+ errors: entries.filter((e) => !e.ok).length,
66
+ avgDuration: durations.reduce((a, b) => a + b, 0) / durations.length,
67
+ minDuration: Math.min(...durations),
68
+ maxDuration: Math.max(...durations),
69
+ });
70
+ }
71
+ return stats.sort((a, b) => b.fires - a.fires);
72
+ }
73
+ /** Flush is a no-op since we append on every record, but provided for API completeness. */
74
+ flush() {
75
+ // Already written on each record()
76
+ }
77
+ /** Format stats as a CLI-friendly table. */
78
+ formatStatsTable() {
79
+ const stats = this.getStats();
80
+ if (stats.length === 0) {
81
+ return 'No metrics recorded yet.';
82
+ }
83
+ const header = padRow(['Event', 'Fires', 'Errors', 'Avg (ms)', 'Min (ms)', 'Max (ms)']);
84
+ const separator = '-'.repeat(header.length);
85
+ const rows = stats.map((s) => padRow([
86
+ s.event,
87
+ String(s.fires),
88
+ String(s.errors),
89
+ s.avgDuration.toFixed(1),
90
+ s.minDuration.toFixed(1),
91
+ s.maxDuration.toFixed(1),
92
+ ]));
93
+ const totalFires = stats.reduce((sum, s) => sum + s.fires, 0);
94
+ const totalErrors = stats.reduce((sum, s) => sum + s.errors, 0);
95
+ const footer = `\nTotal fires: ${totalFires} | Total errors: ${totalErrors} | Spawns saved: ~${totalFires}`;
96
+ return [header, separator, ...rows, footer].join('\n');
97
+ }
98
+ /** Estimate how many process spawns were saved. */
99
+ estimateSpawnsSaved() {
100
+ const all = this.loadAll();
101
+ return all.length;
102
+ }
103
+ /** Load all entries from disk + memory (deduped by combining disk file). */
104
+ loadAll() {
105
+ if (!(0, fs_1.existsSync)(constants_js_1.METRICS_FILE)) {
106
+ return [...this.entries];
107
+ }
108
+ try {
109
+ const raw = (0, fs_1.readFileSync)(constants_js_1.METRICS_FILE, 'utf-8');
110
+ const lines = raw.trim().split('\n').filter(Boolean);
111
+ return lines.map((line) => JSON.parse(line));
112
+ }
113
+ catch {
114
+ return [...this.entries];
115
+ }
116
+ }
117
+ }
118
+ exports.MetricsCollector = MetricsCollector;
119
+ function padRow(cols) {
120
+ const widths = [20, 8, 8, 10, 10, 10];
121
+ return cols.map((col, i) => col.padEnd(widths[i])).join(' ');
122
+ }
@@ -0,0 +1,31 @@
1
+ /** Options for overriding default paths (used by tests to avoid touching real filesystem). */
2
+ export interface MigratePathOptions {
3
+ /** Override the home directory used to locate settings.json */
4
+ homeDir?: string;
5
+ /** Override the config directory (~/.clooks) */
6
+ configDir?: string;
7
+ /** Override the settings backup path */
8
+ settingsBackup?: string;
9
+ }
10
+ /**
11
+ * Find the Claude Code settings.json path.
12
+ */
13
+ export declare function getSettingsPath(options?: MigratePathOptions): string | null;
14
+ /**
15
+ * Migrate Claude Code settings.json command hooks to clooks HTTP hooks.
16
+ *
17
+ * 1. Read settings.json
18
+ * 2. Extract command hooks → generate manifest.yaml handler entries
19
+ * 3. Back up original settings.json
20
+ * 4. Rewrite settings with HTTP hooks pointing to localhost:7890
21
+ * 5. Keep SessionStart with ensure-running command hook + HTTP hook
22
+ */
23
+ export declare function migrate(options?: MigratePathOptions): {
24
+ manifestPath: string;
25
+ settingsPath: string;
26
+ handlersCreated: number;
27
+ };
28
+ /**
29
+ * Restore original settings.json from backup.
30
+ */
31
+ export declare function restore(options?: MigratePathOptions): string;