@mauribadnights/clooks 0.4.1 → 0.5.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.
- package/dist/cli.js +61 -1
- package/dist/import-plugins.d.ts +50 -0
- package/dist/import-plugins.js +196 -0
- package/dist/migrate.js +9 -0
- package/dist/sync.js +15 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -15,13 +15,14 @@ const service_js_1 = require("./service.js");
|
|
|
15
15
|
const constants_js_1 = require("./constants.js");
|
|
16
16
|
const tui_js_1 = require("./tui.js");
|
|
17
17
|
const agent_js_1 = require("./agent.js");
|
|
18
|
+
const import_plugins_js_1 = require("./import-plugins.js");
|
|
18
19
|
const fs_1 = require("fs");
|
|
19
20
|
const path_1 = require("path");
|
|
20
21
|
const program = new commander_1.Command();
|
|
21
22
|
program
|
|
22
23
|
.name('clooks')
|
|
23
24
|
.description('Persistent hook runtime for Claude Code')
|
|
24
|
-
.version('0.
|
|
25
|
+
.version('0.5.0');
|
|
25
26
|
// --- start ---
|
|
26
27
|
program
|
|
27
28
|
.command('start')
|
|
@@ -247,6 +248,65 @@ program
|
|
|
247
248
|
}
|
|
248
249
|
}
|
|
249
250
|
});
|
|
251
|
+
// --- import-plugins ---
|
|
252
|
+
program
|
|
253
|
+
.command('import-plugins')
|
|
254
|
+
.description('Import hooks from installed Claude Code plugins')
|
|
255
|
+
.action(async () => {
|
|
256
|
+
try {
|
|
257
|
+
const { plugins, handlers } = (0, import_plugins_js_1.importPlugins)();
|
|
258
|
+
if (plugins.length === 0) {
|
|
259
|
+
console.log('No Claude Code plugins with hooks found.');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// Load current manifest
|
|
263
|
+
const manifest = (0, manifest_js_1.loadCompositeManifest)();
|
|
264
|
+
// Remove previously imported plugin handlers (those with "/" matching discovered plugin names)
|
|
265
|
+
const pluginNames = new Set(plugins.map(p => p.name));
|
|
266
|
+
for (const [event, eventHandlers] of Object.entries(manifest.handlers)) {
|
|
267
|
+
if (!eventHandlers)
|
|
268
|
+
continue;
|
|
269
|
+
manifest.handlers[event] = eventHandlers.filter(h => {
|
|
270
|
+
const slashIdx = h.id.indexOf('/');
|
|
271
|
+
if (slashIdx === -1)
|
|
272
|
+
return true; // user handler, keep
|
|
273
|
+
const prefix = h.id.substring(0, slashIdx);
|
|
274
|
+
return !pluginNames.has(prefix); // remove if prefix matches a discovered plugin
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
// Add new imported handlers
|
|
278
|
+
for (const [event, eventHandlers] of Object.entries(handlers)) {
|
|
279
|
+
const hookEvent = event;
|
|
280
|
+
if (!manifest.handlers[hookEvent])
|
|
281
|
+
manifest.handlers[hookEvent] = [];
|
|
282
|
+
manifest.handlers[hookEvent].push(...eventHandlers);
|
|
283
|
+
}
|
|
284
|
+
// Write updated manifest
|
|
285
|
+
const { stringify: stringifyYaml } = await import('yaml');
|
|
286
|
+
const { writeFileSync } = await import('fs');
|
|
287
|
+
const yamlStr = '# clooks manifest — updated by import-plugins\n' +
|
|
288
|
+
`# Date: ${new Date().toISOString()}\n\n` +
|
|
289
|
+
stringifyYaml(manifest);
|
|
290
|
+
writeFileSync(constants_js_1.MANIFEST_PATH, yamlStr, 'utf-8');
|
|
291
|
+
// Sync settings.json
|
|
292
|
+
const syncAdded = (0, sync_js_1.syncSettings)();
|
|
293
|
+
// Report
|
|
294
|
+
const totalHandlers = Object.values(handlers).reduce((sum, arr) => sum + arr.length, 0);
|
|
295
|
+
console.log(`Imported ${totalHandlers} handler(s) from ${plugins.length} CC plugin(s):`);
|
|
296
|
+
for (const p of plugins) {
|
|
297
|
+
const count = Object.values(p.hooks).reduce((sum, arr) => sum + arr.filter(h => h.type === 'command').length, 0);
|
|
298
|
+
const enhanced = p.clooksEnhancements ? ' (with clooks.yaml)' : '';
|
|
299
|
+
console.log(` ${p.name} v${p.version}: ${count} handler(s)${enhanced}`);
|
|
300
|
+
}
|
|
301
|
+
if (syncAdded.length > 0) {
|
|
302
|
+
console.log(`Synced HTTP hooks for: ${syncAdded.join(', ')}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
console.error('Import failed:', err instanceof Error ? err.message : err);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
250
310
|
// --- ensure-running ---
|
|
251
311
|
program
|
|
252
312
|
.command('ensure-running')
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { HandlerConfig, HookEvent } from './types.js';
|
|
2
|
+
/** A discovered Claude Code plugin */
|
|
3
|
+
export interface CCPlugin {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
path: string;
|
|
7
|
+
hooks: Partial<Record<HookEvent, CCHookEntry[]>>;
|
|
8
|
+
clooksEnhancements?: Partial<Record<string, ClooksEnhancement>>;
|
|
9
|
+
}
|
|
10
|
+
interface CCHookEntry {
|
|
11
|
+
type: 'command' | 'http' | 'prompt' | 'agent';
|
|
12
|
+
command?: string;
|
|
13
|
+
url?: string;
|
|
14
|
+
matcher?: string;
|
|
15
|
+
timeout?: number;
|
|
16
|
+
}
|
|
17
|
+
/** clooks.yaml enhancement overlay per handler */
|
|
18
|
+
interface ClooksEnhancement {
|
|
19
|
+
filter?: string;
|
|
20
|
+
project?: string;
|
|
21
|
+
agent?: string;
|
|
22
|
+
async?: boolean;
|
|
23
|
+
depends?: string[];
|
|
24
|
+
sessionIsolation?: boolean;
|
|
25
|
+
batchGroup?: string;
|
|
26
|
+
type?: 'llm';
|
|
27
|
+
model?: string;
|
|
28
|
+
prompt?: string;
|
|
29
|
+
maxTokens?: number;
|
|
30
|
+
temperature?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Discover all installed Claude Code plugins that have hooks.
|
|
34
|
+
*/
|
|
35
|
+
export declare function discoverCCPlugins(ccPluginsDir?: string): CCPlugin[];
|
|
36
|
+
/**
|
|
37
|
+
* Convert discovered CC plugins into clooks manifest handler entries.
|
|
38
|
+
* Applies clooks.yaml enhancements if present.
|
|
39
|
+
* Handler IDs namespaced as "pluginName/derivedId".
|
|
40
|
+
*/
|
|
41
|
+
export declare function convertPluginsToHandlers(plugins: CCPlugin[]): Partial<Record<HookEvent, HandlerConfig[]>>;
|
|
42
|
+
/**
|
|
43
|
+
* Import all Claude Code plugins into clooks.
|
|
44
|
+
* Returns the discovered plugins and generated handlers.
|
|
45
|
+
*/
|
|
46
|
+
export declare function importPlugins(ccPluginsDir?: string): {
|
|
47
|
+
plugins: CCPlugin[];
|
|
48
|
+
handlers: Partial<Record<HookEvent, HandlerConfig[]>>;
|
|
49
|
+
};
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.discoverCCPlugins = discoverCCPlugins;
|
|
4
|
+
exports.convertPluginsToHandlers = convertPluginsToHandlers;
|
|
5
|
+
exports.importPlugins = importPlugins;
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
const os_1 = require("os");
|
|
9
|
+
const yaml_1 = require("yaml");
|
|
10
|
+
const constants_js_1 = require("./constants.js");
|
|
11
|
+
/** Path to Claude Code's plugin cache */
|
|
12
|
+
const CC_PLUGINS_DIR = (0, path_1.join)((0, os_1.homedir)(), '.claude', 'plugins', 'cache');
|
|
13
|
+
/**
|
|
14
|
+
* Discover all installed Claude Code plugins that have hooks.
|
|
15
|
+
*/
|
|
16
|
+
function discoverCCPlugins(ccPluginsDir) {
|
|
17
|
+
const dir = ccPluginsDir ?? CC_PLUGINS_DIR;
|
|
18
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
19
|
+
return [];
|
|
20
|
+
const results = [];
|
|
21
|
+
for (const entry of (0, fs_1.readdirSync)(dir, { withFileTypes: true })) {
|
|
22
|
+
if (!entry.isDirectory())
|
|
23
|
+
continue;
|
|
24
|
+
const pluginDir = (0, path_1.join)(dir, entry.name);
|
|
25
|
+
// Read plugin.json
|
|
26
|
+
const pluginJsonPath = (0, path_1.join)(pluginDir, '.claude-plugin', 'plugin.json');
|
|
27
|
+
if (!(0, fs_1.existsSync)(pluginJsonPath))
|
|
28
|
+
continue;
|
|
29
|
+
let pluginJson;
|
|
30
|
+
try {
|
|
31
|
+
pluginJson = JSON.parse((0, fs_1.readFileSync)(pluginJsonPath, 'utf-8'));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// Read hooks
|
|
37
|
+
const hooksJsonPath = (0, path_1.join)(pluginDir, 'hooks', 'hooks.json');
|
|
38
|
+
let hooks = {};
|
|
39
|
+
if ((0, fs_1.existsSync)(hooksJsonPath)) {
|
|
40
|
+
try {
|
|
41
|
+
hooks = JSON.parse((0, fs_1.readFileSync)(hooksJsonPath, 'utf-8'));
|
|
42
|
+
}
|
|
43
|
+
catch { /* skip */ }
|
|
44
|
+
}
|
|
45
|
+
// Also check if hooks are inline in plugin.json
|
|
46
|
+
if (pluginJson.hooks && typeof pluginJson.hooks === 'string') {
|
|
47
|
+
const inlineHooksPath = (0, path_1.join)(pluginDir, pluginJson.hooks);
|
|
48
|
+
if ((0, fs_1.existsSync)(inlineHooksPath)) {
|
|
49
|
+
try {
|
|
50
|
+
hooks = JSON.parse((0, fs_1.readFileSync)(inlineHooksPath, 'utf-8'));
|
|
51
|
+
}
|
|
52
|
+
catch { /* skip */ }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!hooks || Object.keys(hooks).length === 0)
|
|
56
|
+
continue;
|
|
57
|
+
// Read optional clooks.yaml enhancement
|
|
58
|
+
let enhancements = undefined;
|
|
59
|
+
const clooksYamlPath = (0, path_1.join)(pluginDir, 'clooks.yaml');
|
|
60
|
+
if ((0, fs_1.existsSync)(clooksYamlPath)) {
|
|
61
|
+
try {
|
|
62
|
+
enhancements = (0, yaml_1.parse)((0, fs_1.readFileSync)(clooksYamlPath, 'utf-8'));
|
|
63
|
+
}
|
|
64
|
+
catch { /* skip */ }
|
|
65
|
+
}
|
|
66
|
+
results.push({
|
|
67
|
+
name: pluginJson.name || entry.name,
|
|
68
|
+
version: pluginJson.version || '0.0.0',
|
|
69
|
+
path: pluginDir,
|
|
70
|
+
hooks: parseHooks(hooks),
|
|
71
|
+
clooksEnhancements: enhancements?.handlers,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Parse Claude Code hooks.json format into flat event->hooks map.
|
|
78
|
+
* CC format: { "PostToolUse": [{ "matcher": "...", "hooks": [{ "type": "command", ... }] }] }
|
|
79
|
+
*/
|
|
80
|
+
function parseHooks(hooks) {
|
|
81
|
+
const result = {};
|
|
82
|
+
for (const [event, ruleGroups] of Object.entries(hooks)) {
|
|
83
|
+
if (!constants_js_1.HOOK_EVENTS.includes(event) || !Array.isArray(ruleGroups))
|
|
84
|
+
continue;
|
|
85
|
+
const entries = [];
|
|
86
|
+
for (const rule of ruleGroups) {
|
|
87
|
+
if (Array.isArray(rule.hooks)) {
|
|
88
|
+
for (const hook of rule.hooks) {
|
|
89
|
+
entries.push({ ...hook, matcher: rule.matcher });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (entries.length > 0) {
|
|
94
|
+
result[event] = entries;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Convert discovered CC plugins into clooks manifest handler entries.
|
|
101
|
+
* Applies clooks.yaml enhancements if present.
|
|
102
|
+
* Handler IDs namespaced as "pluginName/derivedId".
|
|
103
|
+
*/
|
|
104
|
+
function convertPluginsToHandlers(plugins) {
|
|
105
|
+
const handlers = {};
|
|
106
|
+
for (const plugin of plugins) {
|
|
107
|
+
for (const [event, hookEntries] of Object.entries(plugin.hooks)) {
|
|
108
|
+
const hookEvent = event;
|
|
109
|
+
if (!handlers[hookEvent])
|
|
110
|
+
handlers[hookEvent] = [];
|
|
111
|
+
for (let i = 0; i < hookEntries.length; i++) {
|
|
112
|
+
const hook = hookEntries[i];
|
|
113
|
+
if (hook.type !== 'command' || !hook.command)
|
|
114
|
+
continue; // Only import command hooks
|
|
115
|
+
// Derive handler ID from command
|
|
116
|
+
const baseId = deriveId(hook.command, hookEvent, i);
|
|
117
|
+
const id = `${plugin.name}/${baseId}`;
|
|
118
|
+
// Build base handler config
|
|
119
|
+
const handler = {
|
|
120
|
+
id,
|
|
121
|
+
type: 'script',
|
|
122
|
+
command: resolvePluginVars(hook.command, plugin.path),
|
|
123
|
+
timeout: hook.timeout ? hook.timeout * 1000 : 5000,
|
|
124
|
+
enabled: true,
|
|
125
|
+
};
|
|
126
|
+
// Apply clooks.yaml enhancements if present
|
|
127
|
+
const enhancement = plugin.clooksEnhancements?.[baseId];
|
|
128
|
+
if (enhancement) {
|
|
129
|
+
applyEnhancement(handler, enhancement, plugin.name);
|
|
130
|
+
}
|
|
131
|
+
handlers[hookEvent].push(handler);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return handlers;
|
|
136
|
+
}
|
|
137
|
+
/** Apply clooks.yaml enhancement to a handler */
|
|
138
|
+
function applyEnhancement(handler, enhancement, pluginName) {
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- dynamic property assignment from enhancement overlay
|
|
140
|
+
const h = handler;
|
|
141
|
+
if (enhancement.filter)
|
|
142
|
+
h.filter = enhancement.filter;
|
|
143
|
+
if (enhancement.project)
|
|
144
|
+
h.project = enhancement.project;
|
|
145
|
+
if (enhancement.agent)
|
|
146
|
+
h.agent = enhancement.agent;
|
|
147
|
+
if (enhancement.async !== undefined)
|
|
148
|
+
h.async = enhancement.async;
|
|
149
|
+
if (enhancement.depends) {
|
|
150
|
+
h.depends = enhancement.depends.map(d => d.includes('/') ? d : `${pluginName}/${d}`);
|
|
151
|
+
}
|
|
152
|
+
if (enhancement.sessionIsolation)
|
|
153
|
+
h.sessionIsolation = enhancement.sessionIsolation;
|
|
154
|
+
// Allow converting to LLM handler
|
|
155
|
+
if (enhancement.type === 'llm' && enhancement.model && enhancement.prompt) {
|
|
156
|
+
h.type = 'llm';
|
|
157
|
+
h.model = enhancement.model;
|
|
158
|
+
h.prompt = enhancement.prompt;
|
|
159
|
+
delete h.command;
|
|
160
|
+
if (enhancement.batchGroup)
|
|
161
|
+
h.batchGroup = enhancement.batchGroup;
|
|
162
|
+
if (enhancement.maxTokens)
|
|
163
|
+
h.maxTokens = enhancement.maxTokens;
|
|
164
|
+
if (enhancement.temperature)
|
|
165
|
+
h.temperature = enhancement.temperature;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Resolve ${CLAUDE_PLUGIN_ROOT} and similar vars in commands */
|
|
169
|
+
function resolvePluginVars(command, pluginPath) {
|
|
170
|
+
return command
|
|
171
|
+
.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, pluginPath)
|
|
172
|
+
.replace(/\$CLAUDE_PLUGIN_ROOT/g, pluginPath);
|
|
173
|
+
}
|
|
174
|
+
/** Derive a handler ID from command */
|
|
175
|
+
function deriveId(command, event, index) {
|
|
176
|
+
// Extract script filename
|
|
177
|
+
const jsMatch = command.match(/[\\/]([^\\/]+)\.(?:js|ts|py|sh)(?:\s|"|'|$)/);
|
|
178
|
+
if (jsMatch) {
|
|
179
|
+
return jsMatch[1].toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
180
|
+
}
|
|
181
|
+
const moduleMatch = command.match(/python3?\s+-m\s+([\w.]+)/);
|
|
182
|
+
if (moduleMatch) {
|
|
183
|
+
const parts = moduleMatch[1].split('.');
|
|
184
|
+
return parts.slice(-2).join('-').toLowerCase();
|
|
185
|
+
}
|
|
186
|
+
return `${event.toLowerCase()}-${index}`;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Import all Claude Code plugins into clooks.
|
|
190
|
+
* Returns the discovered plugins and generated handlers.
|
|
191
|
+
*/
|
|
192
|
+
function importPlugins(ccPluginsDir) {
|
|
193
|
+
const plugins = discoverCCPlugins(ccPluginsDir);
|
|
194
|
+
const handlers = convertPluginsToHandlers(plugins);
|
|
195
|
+
return { plugins, handlers };
|
|
196
|
+
}
|
package/dist/migrate.js
CHANGED
|
@@ -10,6 +10,7 @@ const path_1 = require("path");
|
|
|
10
10
|
const os_1 = require("os");
|
|
11
11
|
const constants_js_1 = require("./constants.js");
|
|
12
12
|
const builtin_hooks_js_1 = require("./builtin-hooks.js");
|
|
13
|
+
const import_plugins_js_1 = require("./import-plugins.js");
|
|
13
14
|
const yaml_1 = require("yaml");
|
|
14
15
|
/**
|
|
15
16
|
* Find the Claude Code settings.json path.
|
|
@@ -154,6 +155,14 @@ function migrate(options) {
|
|
|
154
155
|
timeout: 6000,
|
|
155
156
|
enabled: true,
|
|
156
157
|
});
|
|
158
|
+
// Import Claude Code plugin hooks
|
|
159
|
+
const { handlers: ccHandlers } = (0, import_plugins_js_1.importPlugins)();
|
|
160
|
+
for (const [event, eventHandlers] of Object.entries(ccHandlers)) {
|
|
161
|
+
const hookEvent = event;
|
|
162
|
+
if (!manifestHandlers[hookEvent])
|
|
163
|
+
manifestHandlers[hookEvent] = [];
|
|
164
|
+
manifestHandlers[hookEvent].push(...eventHandlers);
|
|
165
|
+
}
|
|
157
166
|
// Write manifest.yaml
|
|
158
167
|
const manifest = {
|
|
159
168
|
handlers: manifestHandlers,
|
package/dist/sync.js
CHANGED
|
@@ -6,6 +6,7 @@ const fs_1 = require("fs");
|
|
|
6
6
|
const path_1 = require("path");
|
|
7
7
|
const os_1 = require("os");
|
|
8
8
|
const manifest_js_1 = require("./manifest.js");
|
|
9
|
+
const import_plugins_js_1 = require("./import-plugins.js");
|
|
9
10
|
const constants_js_1 = require("./constants.js");
|
|
10
11
|
/**
|
|
11
12
|
* Find the Claude Code settings.json path.
|
|
@@ -42,6 +43,20 @@ function syncSettings(options) {
|
|
|
42
43
|
}
|
|
43
44
|
else {
|
|
44
45
|
manifest = (0, manifest_js_1.loadCompositeManifest)();
|
|
46
|
+
// Also merge in Claude Code plugin hooks
|
|
47
|
+
const { handlers: ccHandlers } = (0, import_plugins_js_1.importPlugins)();
|
|
48
|
+
for (const [event, eventHandlers] of Object.entries(ccHandlers)) {
|
|
49
|
+
const hookEvent = event;
|
|
50
|
+
if (!manifest.handlers[hookEvent])
|
|
51
|
+
manifest.handlers[hookEvent] = [];
|
|
52
|
+
// Avoid duplicates: only add handlers whose IDs are not already present
|
|
53
|
+
const existingIds = new Set(manifest.handlers[hookEvent].map(h => h.id));
|
|
54
|
+
for (const handler of eventHandlers) {
|
|
55
|
+
if (!existingIds.has(handler.id)) {
|
|
56
|
+
manifest.handlers[hookEvent].push(handler);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
45
60
|
}
|
|
46
61
|
const port = manifest.settings?.port ?? constants_js_1.DEFAULT_PORT;
|
|
47
62
|
const authToken = manifest.settings?.authToken;
|
package/package.json
CHANGED