@mauribadnights/clooks 0.1.0 → 0.2.2
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 +215 -68
- package/dist/auth.d.ts +4 -0
- package/dist/auth.js +27 -0
- package/dist/cli.js +23 -4
- package/dist/constants.d.ts +8 -0
- package/dist/constants.js +10 -1
- package/dist/doctor.js +57 -1
- package/dist/filter.d.ts +11 -0
- package/dist/filter.js +42 -0
- package/dist/handlers.d.ts +8 -2
- package/dist/handlers.js +112 -35
- package/dist/index.d.ts +8 -3
- package/dist/index.js +22 -1
- package/dist/llm.d.ts +19 -0
- package/dist/llm.js +225 -0
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +38 -9
- package/dist/metrics.d.ts +29 -2
- package/dist/metrics.js +135 -6
- package/dist/migrate.js +6 -2
- package/dist/prefetch.d.ts +11 -0
- package/dist/prefetch.js +71 -0
- package/dist/server.d.ts +8 -2
- package/dist/server.js +79 -10
- package/dist/types.d.ts +78 -16
- package/dist/watcher.d.ts +18 -0
- package/dist/watcher.js +120 -0
- package/package.json +12 -2
package/dist/handlers.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.resetHandlerStates = resetHandlerStates;
|
|
5
5
|
exports.getHandlerStates = getHandlerStates;
|
|
6
|
+
exports.resetSessionIsolatedHandlers = resetSessionIsolatedHandlers;
|
|
6
7
|
exports.executeHandlers = executeHandlers;
|
|
7
8
|
exports.executeScriptHandler = executeScriptHandler;
|
|
8
9
|
exports.executeInlineHandler = executeInlineHandler;
|
|
@@ -10,6 +11,8 @@ const child_process_1 = require("child_process");
|
|
|
10
11
|
const url_1 = require("url");
|
|
11
12
|
const path_1 = require("path");
|
|
12
13
|
const constants_js_1 = require("./constants.js");
|
|
14
|
+
const filter_js_1 = require("./filter.js");
|
|
15
|
+
const llm_js_1 = require("./llm.js");
|
|
13
16
|
/** Runtime state per handler ID */
|
|
14
17
|
const handlerStates = new Map();
|
|
15
18
|
function getState(id) {
|
|
@@ -28,53 +31,96 @@ function resetHandlerStates() {
|
|
|
28
31
|
function getHandlerStates() {
|
|
29
32
|
return new Map(handlerStates);
|
|
30
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Reset handler states for handlers that have sessionIsolation: true.
|
|
36
|
+
* Called on SessionStart events.
|
|
37
|
+
*/
|
|
38
|
+
function resetSessionIsolatedHandlers(handlers) {
|
|
39
|
+
for (const handler of handlers) {
|
|
40
|
+
if (handler.sessionIsolation) {
|
|
41
|
+
const state = handlerStates.get(handler.id);
|
|
42
|
+
if (state) {
|
|
43
|
+
state.consecutiveFailures = 0;
|
|
44
|
+
state.disabled = false;
|
|
45
|
+
state.totalFires = 0;
|
|
46
|
+
state.totalErrors = 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
31
51
|
/**
|
|
32
52
|
* Execute all handlers for an event in parallel.
|
|
33
53
|
* Returns merged results array.
|
|
54
|
+
* Optionally accepts pre-fetched context for LLM prompt rendering.
|
|
34
55
|
*/
|
|
35
|
-
async function executeHandlers(_event, input, handlers) {
|
|
36
|
-
|
|
56
|
+
async function executeHandlers(_event, input, handlers, context) {
|
|
57
|
+
// Separate LLM handlers from script/inline, applying shared pre-checks
|
|
58
|
+
const llmHandlers = [];
|
|
59
|
+
const otherPromises = [];
|
|
60
|
+
const skippedResults = [];
|
|
61
|
+
for (const handler of handlers) {
|
|
37
62
|
// Skip disabled handlers (both manifest-disabled and auto-disabled)
|
|
38
63
|
if (handler.enabled === false) {
|
|
39
|
-
|
|
64
|
+
skippedResults.push({ id: handler.id, ok: true, output: undefined, duration_ms: 0 });
|
|
65
|
+
continue;
|
|
40
66
|
}
|
|
41
67
|
const state = getState(handler.id);
|
|
42
68
|
if (state.disabled) {
|
|
43
|
-
|
|
69
|
+
skippedResults.push({
|
|
44
70
|
id: handler.id,
|
|
45
71
|
ok: false,
|
|
46
72
|
error: `Auto-disabled after ${constants_js_1.MAX_CONSECUTIVE_FAILURES} consecutive failures`,
|
|
47
73
|
duration_ms: 0,
|
|
48
|
-
};
|
|
74
|
+
});
|
|
75
|
+
continue;
|
|
49
76
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
result = await executeScriptHandler(handler, input);
|
|
56
|
-
}
|
|
57
|
-
else if (handler.type === 'inline') {
|
|
58
|
-
result = await executeInlineHandler(handler, input);
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
result = {
|
|
77
|
+
// Evaluate keyword filter before execution
|
|
78
|
+
if (handler.filter) {
|
|
79
|
+
const inputStr = JSON.stringify(input);
|
|
80
|
+
if (!(0, filter_js_1.evaluateFilter)(handler.filter, inputStr)) {
|
|
81
|
+
skippedResults.push({
|
|
62
82
|
id: handler.id,
|
|
63
|
-
ok:
|
|
64
|
-
|
|
83
|
+
ok: true,
|
|
84
|
+
output: undefined,
|
|
65
85
|
duration_ms: 0,
|
|
66
|
-
|
|
86
|
+
filtered: true,
|
|
87
|
+
});
|
|
88
|
+
continue;
|
|
67
89
|
}
|
|
68
90
|
}
|
|
91
|
+
state.totalFires++;
|
|
92
|
+
if (handler.type === 'llm') {
|
|
93
|
+
llmHandlers.push(handler);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// Execute script/inline handlers in parallel
|
|
97
|
+
otherPromises.push(executeOtherHandler(handler, input));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Execute script/inline handlers in parallel
|
|
101
|
+
const otherResults = otherPromises.length > 0
|
|
102
|
+
? await Promise.all(otherPromises)
|
|
103
|
+
: [];
|
|
104
|
+
// Execute LLM handlers with batching (graceful — never crashes)
|
|
105
|
+
let llmResults = [];
|
|
106
|
+
if (llmHandlers.length > 0) {
|
|
107
|
+
try {
|
|
108
|
+
llmResults = await (0, llm_js_1.executeLLMHandlersBatched)(llmHandlers, input, context ?? {});
|
|
109
|
+
}
|
|
69
110
|
catch (err) {
|
|
70
|
-
|
|
71
|
-
|
|
111
|
+
// Graceful degradation: if LLM execution entirely fails, return error results
|
|
112
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
113
|
+
llmResults = llmHandlers.map(h => ({
|
|
114
|
+
id: h.id,
|
|
72
115
|
ok: false,
|
|
73
|
-
error:
|
|
74
|
-
duration_ms:
|
|
75
|
-
};
|
|
116
|
+
error: `LLM execution failed: ${errorMsg}`,
|
|
117
|
+
duration_ms: 0,
|
|
118
|
+
}));
|
|
76
119
|
}
|
|
77
|
-
|
|
120
|
+
}
|
|
121
|
+
// Update failure tracking for all executed results
|
|
122
|
+
for (const result of [...otherResults, ...llmResults]) {
|
|
123
|
+
const state = getState(result.id);
|
|
78
124
|
if (result.ok) {
|
|
79
125
|
state.consecutiveFailures = 0;
|
|
80
126
|
}
|
|
@@ -85,19 +131,49 @@ async function executeHandlers(_event, input, handlers) {
|
|
|
85
131
|
state.disabled = true;
|
|
86
132
|
}
|
|
87
133
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
134
|
+
}
|
|
135
|
+
return [...skippedResults, ...otherResults, ...llmResults];
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Execute a single script or inline handler with error handling.
|
|
139
|
+
*/
|
|
140
|
+
async function executeOtherHandler(handler, input) {
|
|
141
|
+
const start = performance.now();
|
|
142
|
+
try {
|
|
143
|
+
if (handler.type === 'script') {
|
|
144
|
+
return await executeScriptHandler(handler, input);
|
|
145
|
+
}
|
|
146
|
+
else if (handler.type === 'inline') {
|
|
147
|
+
return await executeInlineHandler(handler, input);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
return {
|
|
151
|
+
id: handler.id,
|
|
152
|
+
ok: false,
|
|
153
|
+
error: `Unknown handler type: ${handler.type}`,
|
|
154
|
+
duration_ms: 0,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
return {
|
|
160
|
+
id: handler.id,
|
|
161
|
+
ok: false,
|
|
162
|
+
error: err instanceof Error ? err.message : String(err),
|
|
163
|
+
duration_ms: performance.now() - start,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
91
166
|
}
|
|
92
167
|
/**
|
|
93
168
|
* Execute a script handler: spawn a child process, pipe input JSON to stdin,
|
|
94
169
|
* read stdout as JSON response.
|
|
95
170
|
*/
|
|
96
171
|
function executeScriptHandler(handler, input) {
|
|
97
|
-
const
|
|
172
|
+
const h = handler;
|
|
173
|
+
const timeout = h.timeout ?? constants_js_1.DEFAULT_HANDLER_TIMEOUT;
|
|
98
174
|
return new Promise((resolve) => {
|
|
99
175
|
const start = performance.now();
|
|
100
|
-
const child = (0, child_process_1.spawn)('sh', ['-c',
|
|
176
|
+
const child = (0, child_process_1.spawn)('sh', ['-c', h.command], {
|
|
101
177
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
102
178
|
timeout,
|
|
103
179
|
});
|
|
@@ -155,17 +231,18 @@ function executeScriptHandler(handler, input) {
|
|
|
155
231
|
* Execute an inline handler: dynamically import a JS module and call its default export.
|
|
156
232
|
*/
|
|
157
233
|
async function executeInlineHandler(handler, input) {
|
|
158
|
-
const
|
|
234
|
+
const h = handler;
|
|
235
|
+
const timeout = h.timeout ?? constants_js_1.DEFAULT_HANDLER_TIMEOUT;
|
|
159
236
|
const start = performance.now();
|
|
160
237
|
try {
|
|
161
|
-
const modulePath = (0, path_1.resolve)(
|
|
238
|
+
const modulePath = (0, path_1.resolve)(h.module);
|
|
162
239
|
const moduleUrl = (0, url_1.pathToFileURL)(modulePath).href;
|
|
163
240
|
const mod = await import(moduleUrl);
|
|
164
241
|
if (typeof mod.default !== 'function') {
|
|
165
242
|
return {
|
|
166
|
-
id:
|
|
243
|
+
id: h.id,
|
|
167
244
|
ok: false,
|
|
168
|
-
error: `Module "${
|
|
245
|
+
error: `Module "${h.module}" does not export a default function`,
|
|
169
246
|
duration_ms: performance.now() - start,
|
|
170
247
|
};
|
|
171
248
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,11 @@ export { MetricsCollector } from './metrics.js';
|
|
|
4
4
|
export { migrate, restore, getSettingsPath } from './migrate.js';
|
|
5
5
|
export type { MigratePathOptions } from './migrate.js';
|
|
6
6
|
export { runDoctor } from './doctor.js';
|
|
7
|
-
export { executeHandlers } from './handlers.js';
|
|
8
|
-
export {
|
|
9
|
-
export
|
|
7
|
+
export { executeHandlers, resetSessionIsolatedHandlers } from './handlers.js';
|
|
8
|
+
export { startWatcher, stopWatcher } from './watcher.js';
|
|
9
|
+
export { generateAuthToken, validateAuth } from './auth.js';
|
|
10
|
+
export { evaluateFilter } from './filter.js';
|
|
11
|
+
export { executeLLMHandler, executeLLMHandlersBatched, calculateCost, resetClient } from './llm.js';
|
|
12
|
+
export { prefetchContext, renderPromptTemplate } from './prefetch.js';
|
|
13
|
+
export { DEFAULT_PORT, CONFIG_DIR, MANIFEST_PATH, PID_FILE, METRICS_FILE, LOG_FILE, COSTS_FILE, DEFAULT_LLM_TIMEOUT, DEFAULT_LLM_MAX_TOKENS, LLM_PRICING } from './constants.js';
|
|
14
|
+
export type { HookEvent, HookInput, HandlerType, HandlerConfig, ScriptHandlerConfig, InlineHandlerConfig, LLMHandlerConfig, LLMModel, Manifest, HandlerResult, MetricEntry, HandlerState, DiagnosticResult, PrefetchKey, PrefetchContext, TokenUsage, CostEntry, } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// clooks — public API exports
|
|
3
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;
|
|
4
|
+
exports.LLM_PRICING = exports.DEFAULT_LLM_MAX_TOKENS = exports.DEFAULT_LLM_TIMEOUT = exports.COSTS_FILE = exports.LOG_FILE = exports.METRICS_FILE = exports.PID_FILE = exports.MANIFEST_PATH = exports.CONFIG_DIR = exports.DEFAULT_PORT = exports.renderPromptTemplate = exports.prefetchContext = exports.resetClient = exports.calculateCost = exports.executeLLMHandlersBatched = exports.executeLLMHandler = exports.evaluateFilter = exports.validateAuth = exports.generateAuthToken = exports.stopWatcher = exports.startWatcher = exports.resetSessionIsolatedHandlers = 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
5
|
var server_js_1 = require("./server.js");
|
|
6
6
|
Object.defineProperty(exports, "createServer", { enumerable: true, get: function () { return server_js_1.createServer; } });
|
|
7
7
|
Object.defineProperty(exports, "startDaemon", { enumerable: true, get: function () { return server_js_1.startDaemon; } });
|
|
@@ -21,6 +21,23 @@ var doctor_js_1 = require("./doctor.js");
|
|
|
21
21
|
Object.defineProperty(exports, "runDoctor", { enumerable: true, get: function () { return doctor_js_1.runDoctor; } });
|
|
22
22
|
var handlers_js_1 = require("./handlers.js");
|
|
23
23
|
Object.defineProperty(exports, "executeHandlers", { enumerable: true, get: function () { return handlers_js_1.executeHandlers; } });
|
|
24
|
+
Object.defineProperty(exports, "resetSessionIsolatedHandlers", { enumerable: true, get: function () { return handlers_js_1.resetSessionIsolatedHandlers; } });
|
|
25
|
+
var watcher_js_1 = require("./watcher.js");
|
|
26
|
+
Object.defineProperty(exports, "startWatcher", { enumerable: true, get: function () { return watcher_js_1.startWatcher; } });
|
|
27
|
+
Object.defineProperty(exports, "stopWatcher", { enumerable: true, get: function () { return watcher_js_1.stopWatcher; } });
|
|
28
|
+
var auth_js_1 = require("./auth.js");
|
|
29
|
+
Object.defineProperty(exports, "generateAuthToken", { enumerable: true, get: function () { return auth_js_1.generateAuthToken; } });
|
|
30
|
+
Object.defineProperty(exports, "validateAuth", { enumerable: true, get: function () { return auth_js_1.validateAuth; } });
|
|
31
|
+
var filter_js_1 = require("./filter.js");
|
|
32
|
+
Object.defineProperty(exports, "evaluateFilter", { enumerable: true, get: function () { return filter_js_1.evaluateFilter; } });
|
|
33
|
+
var llm_js_1 = require("./llm.js");
|
|
34
|
+
Object.defineProperty(exports, "executeLLMHandler", { enumerable: true, get: function () { return llm_js_1.executeLLMHandler; } });
|
|
35
|
+
Object.defineProperty(exports, "executeLLMHandlersBatched", { enumerable: true, get: function () { return llm_js_1.executeLLMHandlersBatched; } });
|
|
36
|
+
Object.defineProperty(exports, "calculateCost", { enumerable: true, get: function () { return llm_js_1.calculateCost; } });
|
|
37
|
+
Object.defineProperty(exports, "resetClient", { enumerable: true, get: function () { return llm_js_1.resetClient; } });
|
|
38
|
+
var prefetch_js_1 = require("./prefetch.js");
|
|
39
|
+
Object.defineProperty(exports, "prefetchContext", { enumerable: true, get: function () { return prefetch_js_1.prefetchContext; } });
|
|
40
|
+
Object.defineProperty(exports, "renderPromptTemplate", { enumerable: true, get: function () { return prefetch_js_1.renderPromptTemplate; } });
|
|
24
41
|
var constants_js_1 = require("./constants.js");
|
|
25
42
|
Object.defineProperty(exports, "DEFAULT_PORT", { enumerable: true, get: function () { return constants_js_1.DEFAULT_PORT; } });
|
|
26
43
|
Object.defineProperty(exports, "CONFIG_DIR", { enumerable: true, get: function () { return constants_js_1.CONFIG_DIR; } });
|
|
@@ -28,3 +45,7 @@ Object.defineProperty(exports, "MANIFEST_PATH", { enumerable: true, get: functio
|
|
|
28
45
|
Object.defineProperty(exports, "PID_FILE", { enumerable: true, get: function () { return constants_js_1.PID_FILE; } });
|
|
29
46
|
Object.defineProperty(exports, "METRICS_FILE", { enumerable: true, get: function () { return constants_js_1.METRICS_FILE; } });
|
|
30
47
|
Object.defineProperty(exports, "LOG_FILE", { enumerable: true, get: function () { return constants_js_1.LOG_FILE; } });
|
|
48
|
+
Object.defineProperty(exports, "COSTS_FILE", { enumerable: true, get: function () { return constants_js_1.COSTS_FILE; } });
|
|
49
|
+
Object.defineProperty(exports, "DEFAULT_LLM_TIMEOUT", { enumerable: true, get: function () { return constants_js_1.DEFAULT_LLM_TIMEOUT; } });
|
|
50
|
+
Object.defineProperty(exports, "DEFAULT_LLM_MAX_TOKENS", { enumerable: true, get: function () { return constants_js_1.DEFAULT_LLM_MAX_TOKENS; } });
|
|
51
|
+
Object.defineProperty(exports, "LLM_PRICING", { enumerable: true, get: function () { return constants_js_1.LLM_PRICING; } });
|
package/dist/llm.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { LLMHandlerConfig, HandlerResult, HookInput, PrefetchContext, TokenUsage } from './types.js';
|
|
2
|
+
/** Reset client (for testing) */
|
|
3
|
+
export declare function resetClient(): void;
|
|
4
|
+
/**
|
|
5
|
+
* Calculate cost in USD from token usage and model.
|
|
6
|
+
*/
|
|
7
|
+
export declare function calculateCost(model: string, usage: TokenUsage): number;
|
|
8
|
+
/**
|
|
9
|
+
* Execute a single LLM handler: render prompt, call Messages API, return result.
|
|
10
|
+
*/
|
|
11
|
+
export declare function executeLLMHandler(handler: LLMHandlerConfig, input: HookInput, context: PrefetchContext): Promise<HandlerResult>;
|
|
12
|
+
/**
|
|
13
|
+
* Execute multiple LLM handlers, batching those with the same batchGroup.
|
|
14
|
+
*
|
|
15
|
+
* Strategy: handlers with the same batchGroup get their prompts combined into
|
|
16
|
+
* a single API call with a structured multi-task prompt. Handlers without a
|
|
17
|
+
* batchGroup are executed individually.
|
|
18
|
+
*/
|
|
19
|
+
export declare function executeLLMHandlersBatched(handlers: LLMHandlerConfig[], input: HookInput, context: PrefetchContext): Promise<HandlerResult[]>;
|
package/dist/llm.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// clooks LLM handler execution — Anthropic Messages API with batching
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.resetClient = resetClient;
|
|
5
|
+
exports.calculateCost = calculateCost;
|
|
6
|
+
exports.executeLLMHandler = executeLLMHandler;
|
|
7
|
+
exports.executeLLMHandlersBatched = executeLLMHandlersBatched;
|
|
8
|
+
const prefetch_js_1 = require("./prefetch.js");
|
|
9
|
+
const constants_js_1 = require("./constants.js");
|
|
10
|
+
/** Lazy-loaded Anthropic SDK client */
|
|
11
|
+
let anthropicClient = null;
|
|
12
|
+
async function getClient() {
|
|
13
|
+
if (!anthropicClient) {
|
|
14
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
15
|
+
throw new Error('ANTHROPIC_API_KEY environment variable is not set. ' +
|
|
16
|
+
'LLM handlers require a valid API key.');
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
// Dynamic import with variable to avoid TypeScript resolving the module at compile time
|
|
20
|
+
const sdkModule = '@anthropic-ai/sdk';
|
|
21
|
+
const { default: Anthropic } = await import(/* webpackIgnore: true */ sdkModule);
|
|
22
|
+
anthropicClient = new Anthropic();
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
throw new Error('Anthropic SDK not installed. Run: npm install @anthropic-ai/sdk\n' +
|
|
26
|
+
'Then set ANTHROPIC_API_KEY environment variable.');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return anthropicClient;
|
|
30
|
+
}
|
|
31
|
+
/** Reset client (for testing) */
|
|
32
|
+
function resetClient() {
|
|
33
|
+
anthropicClient = null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Calculate cost in USD from token usage and model.
|
|
37
|
+
*/
|
|
38
|
+
function calculateCost(model, usage) {
|
|
39
|
+
const pricing = constants_js_1.LLM_PRICING[model];
|
|
40
|
+
if (!pricing)
|
|
41
|
+
return 0;
|
|
42
|
+
const inputCost = (usage.input_tokens / 1_000_000) * pricing.input;
|
|
43
|
+
const outputCost = (usage.output_tokens / 1_000_000) * pricing.output;
|
|
44
|
+
return inputCost + outputCost;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Execute a single LLM handler: render prompt, call Messages API, return result.
|
|
48
|
+
*/
|
|
49
|
+
async function executeLLMHandler(handler, input, context) {
|
|
50
|
+
const start = performance.now();
|
|
51
|
+
const timeout = handler.timeout ?? constants_js_1.DEFAULT_LLM_TIMEOUT;
|
|
52
|
+
const maxTokens = handler.maxTokens ?? constants_js_1.DEFAULT_LLM_MAX_TOKENS;
|
|
53
|
+
try {
|
|
54
|
+
const client = await getClient();
|
|
55
|
+
const prompt = (0, prefetch_js_1.renderPromptTemplate)(handler.prompt, input, context);
|
|
56
|
+
const apiCall = client.messages.create({
|
|
57
|
+
model: handler.model,
|
|
58
|
+
max_tokens: maxTokens,
|
|
59
|
+
messages: [{ role: 'user', content: prompt }],
|
|
60
|
+
});
|
|
61
|
+
const timeoutPromise = new Promise((_resolve, reject) => setTimeout(() => reject(new Error(`LLM handler timed out after ${timeout}ms`)), timeout));
|
|
62
|
+
const response = await Promise.race([apiCall, timeoutPromise]);
|
|
63
|
+
const text = response.content?.[0]?.text ?? '';
|
|
64
|
+
const usage = {
|
|
65
|
+
input_tokens: response.usage?.input_tokens ?? 0,
|
|
66
|
+
output_tokens: response.usage?.output_tokens ?? 0,
|
|
67
|
+
};
|
|
68
|
+
const cost_usd = calculateCost(handler.model, usage);
|
|
69
|
+
return {
|
|
70
|
+
id: handler.id,
|
|
71
|
+
ok: true,
|
|
72
|
+
output: { additionalContext: text },
|
|
73
|
+
duration_ms: performance.now() - start,
|
|
74
|
+
usage,
|
|
75
|
+
cost_usd,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
return {
|
|
80
|
+
id: handler.id,
|
|
81
|
+
ok: false,
|
|
82
|
+
error: err instanceof Error ? err.message : String(err),
|
|
83
|
+
duration_ms: performance.now() - start,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Execute a batched group of LLM handlers: combine prompts into a single
|
|
89
|
+
* multi-task API call, parse JSON response back into individual results.
|
|
90
|
+
*/
|
|
91
|
+
async function executeBatchGroup(handlers, input, context) {
|
|
92
|
+
const start = performance.now();
|
|
93
|
+
// Use model from first handler; warn if others differ
|
|
94
|
+
const model = handlers[0].model;
|
|
95
|
+
for (let i = 1; i < handlers.length; i++) {
|
|
96
|
+
if (handlers[i].model !== model) {
|
|
97
|
+
console.warn(`[clooks] Batch group "${handlers[0].batchGroup}": handler "${handlers[i].id}" ` +
|
|
98
|
+
`uses model "${handlers[i].model}" but batch uses "${model}". Using "${model}".`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Use highest maxTokens and timeout among group members
|
|
102
|
+
const maxTokens = Math.max(...handlers.map(h => h.maxTokens ?? constants_js_1.DEFAULT_LLM_MAX_TOKENS));
|
|
103
|
+
const timeout = Math.max(...handlers.map(h => h.timeout ?? constants_js_1.DEFAULT_LLM_TIMEOUT));
|
|
104
|
+
// Build combined prompt
|
|
105
|
+
const taskSections = handlers.map((h, i) => {
|
|
106
|
+
const rendered = (0, prefetch_js_1.renderPromptTemplate)(h.prompt, input, context);
|
|
107
|
+
return `TASK "${h.id}":\n${rendered}`;
|
|
108
|
+
});
|
|
109
|
+
const combinedPrompt = 'You must complete multiple analysis tasks. Respond with a JSON object where each key is the task ID and the value is your analysis for that task.\n\n' +
|
|
110
|
+
taskSections.join('\n\n') +
|
|
111
|
+
'\n\nRespond ONLY with valid JSON in this format:\n' +
|
|
112
|
+
'{' + handlers.map(h => `"${h.id}": <your analysis>`).join(', ') + '}';
|
|
113
|
+
try {
|
|
114
|
+
const client = await getClient();
|
|
115
|
+
const apiCall = client.messages.create({
|
|
116
|
+
model,
|
|
117
|
+
max_tokens: maxTokens,
|
|
118
|
+
messages: [{ role: 'user', content: combinedPrompt }],
|
|
119
|
+
});
|
|
120
|
+
const timeoutPromise = new Promise((_resolve, reject) => setTimeout(() => reject(new Error(`Batched LLM call timed out after ${timeout}ms`)), timeout));
|
|
121
|
+
const response = await Promise.race([apiCall, timeoutPromise]);
|
|
122
|
+
const text = response.content?.[0]?.text ?? '';
|
|
123
|
+
const totalUsage = {
|
|
124
|
+
input_tokens: response.usage?.input_tokens ?? 0,
|
|
125
|
+
output_tokens: response.usage?.output_tokens ?? 0,
|
|
126
|
+
};
|
|
127
|
+
// Try to parse as JSON and split results
|
|
128
|
+
let parsed = {};
|
|
129
|
+
try {
|
|
130
|
+
// Strip markdown code fences if present
|
|
131
|
+
const cleaned = text.replace(/^```(?:json)?\s*\n?/m, '').replace(/\n?```\s*$/m, '').trim();
|
|
132
|
+
parsed = JSON.parse(cleaned);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// If JSON parsing fails, give all handlers the raw text
|
|
136
|
+
const duration = performance.now() - start;
|
|
137
|
+
return handlers.map(h => ({
|
|
138
|
+
id: h.id,
|
|
139
|
+
ok: true,
|
|
140
|
+
output: { additionalContext: text },
|
|
141
|
+
duration_ms: duration,
|
|
142
|
+
usage: splitUsage(totalUsage, handlers.length),
|
|
143
|
+
cost_usd: calculateCost(model, splitUsage(totalUsage, handlers.length)),
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
// Distribute results to each handler
|
|
147
|
+
const duration = performance.now() - start;
|
|
148
|
+
const perHandlerUsage = splitUsage(totalUsage, handlers.length);
|
|
149
|
+
return handlers.map(h => {
|
|
150
|
+
const handlerResult = parsed[h.id];
|
|
151
|
+
const resultText = typeof handlerResult === 'string'
|
|
152
|
+
? handlerResult
|
|
153
|
+
: JSON.stringify(handlerResult ?? '');
|
|
154
|
+
return {
|
|
155
|
+
id: h.id,
|
|
156
|
+
ok: true,
|
|
157
|
+
output: { additionalContext: resultText },
|
|
158
|
+
duration_ms: duration,
|
|
159
|
+
usage: perHandlerUsage,
|
|
160
|
+
cost_usd: calculateCost(model, perHandlerUsage),
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
const duration = performance.now() - start;
|
|
166
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
167
|
+
return handlers.map(h => ({
|
|
168
|
+
id: h.id,
|
|
169
|
+
ok: false,
|
|
170
|
+
error: errorMsg,
|
|
171
|
+
duration_ms: duration,
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Split total token usage evenly across N handlers (for cost attribution in batches).
|
|
177
|
+
*/
|
|
178
|
+
function splitUsage(total, count) {
|
|
179
|
+
if (count <= 0)
|
|
180
|
+
return { input_tokens: 0, output_tokens: 0 };
|
|
181
|
+
return {
|
|
182
|
+
input_tokens: Math.ceil(total.input_tokens / count),
|
|
183
|
+
output_tokens: Math.ceil(total.output_tokens / count),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Execute multiple LLM handlers, batching those with the same batchGroup.
|
|
188
|
+
*
|
|
189
|
+
* Strategy: handlers with the same batchGroup get their prompts combined into
|
|
190
|
+
* a single API call with a structured multi-task prompt. Handlers without a
|
|
191
|
+
* batchGroup are executed individually.
|
|
192
|
+
*/
|
|
193
|
+
async function executeLLMHandlersBatched(handlers, input, context) {
|
|
194
|
+
// Group by batchGroup
|
|
195
|
+
const grouped = new Map();
|
|
196
|
+
const ungrouped = [];
|
|
197
|
+
for (const handler of handlers) {
|
|
198
|
+
if (handler.batchGroup) {
|
|
199
|
+
const existing = grouped.get(handler.batchGroup) ?? [];
|
|
200
|
+
existing.push(handler);
|
|
201
|
+
grouped.set(handler.batchGroup, existing);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
ungrouped.push(handler);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Execute all in parallel: individual calls + batch groups
|
|
208
|
+
const promises = [];
|
|
209
|
+
// Individual (ungrouped) handlers
|
|
210
|
+
for (const handler of ungrouped) {
|
|
211
|
+
promises.push(executeLLMHandler(handler, input, context).then(r => [r]));
|
|
212
|
+
}
|
|
213
|
+
// Batch groups
|
|
214
|
+
for (const [_groupId, groupHandlers] of grouped) {
|
|
215
|
+
if (groupHandlers.length === 1) {
|
|
216
|
+
// Single handler in group — no point batching
|
|
217
|
+
promises.push(executeLLMHandler(groupHandlers[0], input, context).then(r => [r]));
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
promises.push(executeBatchGroup(groupHandlers, input, context));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const resultArrays = await Promise.all(promises);
|
|
224
|
+
return resultArrays.flat();
|
|
225
|
+
}
|
package/dist/manifest.d.ts
CHANGED
|
@@ -12,4 +12,4 @@ export declare function validateManifest(manifest: Manifest): void;
|
|
|
12
12
|
/**
|
|
13
13
|
* Create a default commented example manifest.yaml in CONFIG_DIR.
|
|
14
14
|
*/
|
|
15
|
-
export declare function createDefaultManifest(): string;
|
|
15
|
+
export declare function createDefaultManifest(authToken?: string): string;
|
package/dist/manifest.js
CHANGED
|
@@ -48,15 +48,40 @@ function validateManifest(manifest) {
|
|
|
48
48
|
throw new Error(`Duplicate handler id: "${handler.id}"`);
|
|
49
49
|
}
|
|
50
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 "
|
|
51
|
+
if (!handler.type || !['script', 'inline', 'llm'].includes(handler.type)) {
|
|
52
|
+
throw new Error(`Handler "${handler.id}" must have type "script", "inline", or "llm"`);
|
|
53
53
|
}
|
|
54
|
-
if (handler.type === 'script' && !handler.command) {
|
|
54
|
+
if (handler.type === 'script' && !('command' in handler && handler.command)) {
|
|
55
55
|
throw new Error(`Script handler "${handler.id}" must have a "command" field`);
|
|
56
56
|
}
|
|
57
|
-
if (handler.type === 'inline' && !handler.module) {
|
|
57
|
+
if (handler.type === 'inline' && !('module' in handler && handler.module)) {
|
|
58
58
|
throw new Error(`Inline handler "${handler.id}" must have a "module" field`);
|
|
59
59
|
}
|
|
60
|
+
if (handler.type === 'llm') {
|
|
61
|
+
const llm = handler;
|
|
62
|
+
if (!llm.model) {
|
|
63
|
+
throw new Error(`LLM handler "${handler.id}" must have a "model" field`);
|
|
64
|
+
}
|
|
65
|
+
if (!llm.prompt) {
|
|
66
|
+
throw new Error(`LLM handler "${handler.id}" must have a "prompt" field`);
|
|
67
|
+
}
|
|
68
|
+
const validModels = ['claude-haiku-4-5', 'claude-sonnet-4-6', 'claude-opus-4-6'];
|
|
69
|
+
if (!validModels.includes(llm.model)) {
|
|
70
|
+
throw new Error(`LLM handler "${handler.id}" model must be one of: ${validModels.join(', ')}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Validate prefetch if present
|
|
76
|
+
if (manifest.prefetch !== undefined) {
|
|
77
|
+
if (!Array.isArray(manifest.prefetch)) {
|
|
78
|
+
throw new Error('prefetch must be an array');
|
|
79
|
+
}
|
|
80
|
+
const validKeys = ['transcript', 'git_status', 'git_diff'];
|
|
81
|
+
for (const key of manifest.prefetch) {
|
|
82
|
+
if (!validKeys.includes(key)) {
|
|
83
|
+
throw new Error(`Invalid prefetch key: "${key}". Valid keys: ${validKeys.join(', ')}`);
|
|
84
|
+
}
|
|
60
85
|
}
|
|
61
86
|
}
|
|
62
87
|
// Validate settings if present
|
|
@@ -77,10 +102,17 @@ function validateManifest(manifest) {
|
|
|
77
102
|
/**
|
|
78
103
|
* Create a default commented example manifest.yaml in CONFIG_DIR.
|
|
79
104
|
*/
|
|
80
|
-
function createDefaultManifest() {
|
|
105
|
+
function createDefaultManifest(authToken) {
|
|
81
106
|
if (!(0, fs_1.existsSync)(constants_js_1.CONFIG_DIR)) {
|
|
82
107
|
(0, fs_1.mkdirSync)(constants_js_1.CONFIG_DIR, { recursive: true });
|
|
83
108
|
}
|
|
109
|
+
const settings = {
|
|
110
|
+
port: 7890,
|
|
111
|
+
logLevel: 'info',
|
|
112
|
+
};
|
|
113
|
+
if (authToken) {
|
|
114
|
+
settings.authToken = authToken;
|
|
115
|
+
}
|
|
84
116
|
const example = {
|
|
85
117
|
handlers: {
|
|
86
118
|
PreToolUse: [
|
|
@@ -93,10 +125,7 @@ function createDefaultManifest() {
|
|
|
93
125
|
},
|
|
94
126
|
],
|
|
95
127
|
},
|
|
96
|
-
settings
|
|
97
|
-
port: 7890,
|
|
98
|
-
logLevel: 'info',
|
|
99
|
-
},
|
|
128
|
+
settings,
|
|
100
129
|
};
|
|
101
130
|
const yamlStr = '# clooks manifest — define your hook handlers here\n' +
|
|
102
131
|
'# Docs: https://github.com/mauribadnights/clooks\n' +
|
package/dist/metrics.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MetricEntry } from './types.js';
|
|
1
|
+
import type { MetricEntry, CostEntry } from './types.js';
|
|
2
2
|
interface AggregatedStats {
|
|
3
3
|
event: string;
|
|
4
4
|
fires: number;
|
|
@@ -8,8 +8,15 @@ interface AggregatedStats {
|
|
|
8
8
|
maxDuration: number;
|
|
9
9
|
}
|
|
10
10
|
export declare class MetricsCollector {
|
|
11
|
+
private static readonly MAX_ENTRIES;
|
|
11
12
|
private entries;
|
|
12
|
-
|
|
13
|
+
private ringIndex;
|
|
14
|
+
private totalRecorded;
|
|
15
|
+
private static readonly METRICS_MAX_BYTES;
|
|
16
|
+
private static readonly COSTS_MAX_BYTES;
|
|
17
|
+
/** Rotate a log file if it exceeds maxBytes. Keeps one backup (.1). */
|
|
18
|
+
private rotateIfNeeded;
|
|
19
|
+
/** Record a metric entry in memory (ring buffer) and append to disk. */
|
|
13
20
|
record(entry: MetricEntry): void;
|
|
14
21
|
/** Get aggregated stats per event type. */
|
|
15
22
|
getStats(): AggregatedStats[];
|
|
@@ -21,6 +28,26 @@ export declare class MetricsCollector {
|
|
|
21
28
|
formatStatsTable(): string;
|
|
22
29
|
/** Estimate how many process spawns were saved. */
|
|
23
30
|
estimateSpawnsSaved(): number;
|
|
31
|
+
/** Track a cost entry — appends to costs.jsonl. */
|
|
32
|
+
trackCost(entry: CostEntry): void;
|
|
33
|
+
/** Get cost statistics from persisted cost entries. */
|
|
34
|
+
getCostStats(): {
|
|
35
|
+
totalCost: number;
|
|
36
|
+
totalTokens: number;
|
|
37
|
+
byModel: Record<string, {
|
|
38
|
+
cost: number;
|
|
39
|
+
tokens: number;
|
|
40
|
+
}>;
|
|
41
|
+
byHandler: Record<string, {
|
|
42
|
+
cost: number;
|
|
43
|
+
tokens: number;
|
|
44
|
+
calls: number;
|
|
45
|
+
}>;
|
|
46
|
+
};
|
|
47
|
+
/** Format cost data as a CLI-friendly table. */
|
|
48
|
+
formatCostTable(): string;
|
|
49
|
+
/** Load cost entries from disk. */
|
|
50
|
+
private loadCosts;
|
|
24
51
|
/** Load all entries from disk + memory (deduped by combining disk file). */
|
|
25
52
|
private loadAll;
|
|
26
53
|
}
|