ccjk 12.2.1 → 12.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.
|
@@ -1,87 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { h as hookRegistry } from '../shared/ccjk.CCcQfbni.mjs';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { c as commonjsGlobal } from '../shared/ccjk.BAGoDD49.mjs';
|
|
4
4
|
import require$$0 from 'crypto';
|
|
5
|
+
import { l as logger } from '../shared/ccjk.DG_o24cZ.mjs';
|
|
5
6
|
import { existsSync, readFileSync } from 'fs';
|
|
6
7
|
import { homedir } from 'os';
|
|
7
8
|
import { join } from 'path';
|
|
8
9
|
import './index2.mjs';
|
|
9
10
|
|
|
10
|
-
class HookRegistry {
|
|
11
|
-
hooks = /* @__PURE__ */ new Map();
|
|
12
|
-
/**
|
|
13
|
-
* Register a hook for an event
|
|
14
|
-
*/
|
|
15
|
-
register(event, hook) {
|
|
16
|
-
const hooks = this.hooks.get(event) || [];
|
|
17
|
-
hooks.push(hook);
|
|
18
|
-
hooks.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
19
|
-
this.hooks.set(event, hooks);
|
|
20
|
-
logger.debug(`Registered hook "${hook.name}" for event "${event}"`);
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Unregister a hook
|
|
24
|
-
*/
|
|
25
|
-
unregister(event, hookName) {
|
|
26
|
-
const hooks = this.hooks.get(event);
|
|
27
|
-
if (!hooks) return;
|
|
28
|
-
const filtered = hooks.filter((h) => h.name !== hookName);
|
|
29
|
-
this.hooks.set(event, filtered);
|
|
30
|
-
logger.debug(`Unregistered hook "${hookName}" for event "${event}"`);
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Execute all hooks for an event
|
|
34
|
-
* Returns false if any hook returns continue: false
|
|
35
|
-
*/
|
|
36
|
-
async execute(context) {
|
|
37
|
-
const hooks = this.hooks.get(context.event) || [];
|
|
38
|
-
const enabledHooks = hooks.filter((h) => h.enabled !== false);
|
|
39
|
-
if (enabledHooks.length === 0) {
|
|
40
|
-
return { continue: true };
|
|
41
|
-
}
|
|
42
|
-
let currentContext = context;
|
|
43
|
-
let aggregatedData = {};
|
|
44
|
-
for (const hook of enabledHooks) {
|
|
45
|
-
try {
|
|
46
|
-
const result = await hook.fn(currentContext);
|
|
47
|
-
if (result.data) {
|
|
48
|
-
aggregatedData = { ...aggregatedData, ...result.data };
|
|
49
|
-
}
|
|
50
|
-
if (!result.continue) {
|
|
51
|
-
return {
|
|
52
|
-
continue: false,
|
|
53
|
-
data: aggregatedData,
|
|
54
|
-
error: result.error
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
currentContext = {
|
|
58
|
-
...currentContext,
|
|
59
|
-
data: { ...currentContext.data, ...aggregatedData }
|
|
60
|
-
};
|
|
61
|
-
} catch (error) {
|
|
62
|
-
logger.error(`Hook "${hook.name}" failed:`, error);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return {
|
|
66
|
-
continue: true,
|
|
67
|
-
data: aggregatedData
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Get all registered hooks for an event
|
|
72
|
-
*/
|
|
73
|
-
getHooks(event) {
|
|
74
|
-
return this.hooks.get(event) || [];
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Clear all hooks
|
|
78
|
-
*/
|
|
79
|
-
clear() {
|
|
80
|
-
this.hooks.clear();
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
const hookRegistry = new HookRegistry();
|
|
84
|
-
|
|
85
11
|
var cuid2 = {};
|
|
86
12
|
|
|
87
13
|
var src = {};
|
package/dist/chunks/cli-hook.mjs
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { contextLoader } from './context-loader.mjs';
|
|
3
|
+
import { l as logger } from '../shared/ccjk.DG_o24cZ.mjs';
|
|
4
|
+
import { h as hookRegistry } from '../shared/ccjk.CCcQfbni.mjs';
|
|
5
|
+
import { Buffer } from 'node:buffer';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { d as dirname, j as join } from '../shared/ccjk.bQ7Dh1g4.mjs';
|
|
2
9
|
import { a as getGlobalStateManager, g as getGlobalConvoyManager, S as SessionIntelligence } from './convoy-manager.mjs';
|
|
3
10
|
import { n as nanoid } from '../shared/ccjk.BoApaI4j.mjs';
|
|
4
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
-
import { j as join } from '../shared/ccjk.bQ7Dh1g4.mjs';
|
|
6
11
|
import process__default from 'node:process';
|
|
12
|
+
import 'tinyglobby';
|
|
13
|
+
import '../shared/ccjk.CePkJq2S.mjs';
|
|
14
|
+
import './index2.mjs';
|
|
15
|
+
import '../shared/ccjk.BAGoDD49.mjs';
|
|
16
|
+
import '../shared/ccjk.BxSmJ8B7.mjs';
|
|
7
17
|
import 'node:child_process';
|
|
8
18
|
import 'node:fs/promises';
|
|
9
19
|
import 'node:os';
|
|
@@ -12,7 +22,918 @@ import './main.mjs';
|
|
|
12
22
|
import 'module';
|
|
13
23
|
import 'node:stream';
|
|
14
24
|
import 'node:readline';
|
|
15
|
-
|
|
25
|
+
|
|
26
|
+
async function emitCommandHookEvent(event, data, sessionId) {
|
|
27
|
+
try {
|
|
28
|
+
const context = {
|
|
29
|
+
event,
|
|
30
|
+
sessionId,
|
|
31
|
+
data,
|
|
32
|
+
timestamp: Date.now(),
|
|
33
|
+
metadata: {
|
|
34
|
+
source: "brain-router"
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
await hookRegistry.execute(context);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
logger.debug(`Command hook bridge skipped for "${event}": ${String(error)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const DEFAULT_CONFIG = {
|
|
44
|
+
enablePersistence: false,
|
|
45
|
+
maxHistorySize: 1e3,
|
|
46
|
+
messageRetentionTime: 24 * 60 * 60 * 1e3,
|
|
47
|
+
// 24 hours
|
|
48
|
+
enableLogging: true,
|
|
49
|
+
logLevel: "info",
|
|
50
|
+
enableValidation: true,
|
|
51
|
+
maxMessageSize: 1024 * 1024,
|
|
52
|
+
// 1MB
|
|
53
|
+
enableDeadLetterQueue: true
|
|
54
|
+
};
|
|
55
|
+
class FileMessageStorage {
|
|
56
|
+
filePath;
|
|
57
|
+
constructor(filePath) {
|
|
58
|
+
this.filePath = filePath;
|
|
59
|
+
this.ensureDirectory();
|
|
60
|
+
}
|
|
61
|
+
ensureDirectory() {
|
|
62
|
+
const dir = dirname(this.filePath);
|
|
63
|
+
if (!existsSync(dir)) {
|
|
64
|
+
mkdirSync(dir, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async save(message) {
|
|
68
|
+
try {
|
|
69
|
+
const messages = await this.load();
|
|
70
|
+
messages.push(message);
|
|
71
|
+
writeFileSync(this.filePath, JSON.stringify(messages, null, 2), "utf-8");
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error("Failed to save message:", error);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async load(filter) {
|
|
78
|
+
try {
|
|
79
|
+
if (!existsSync(this.filePath)) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
const content = readFileSync(this.filePath, "utf-8");
|
|
83
|
+
const messages = JSON.parse(content);
|
|
84
|
+
if (filter) {
|
|
85
|
+
return messages.filter(filter);
|
|
86
|
+
}
|
|
87
|
+
return messages;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error("Failed to load messages:", error);
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async delete(messageId) {
|
|
94
|
+
try {
|
|
95
|
+
const messages = await this.load();
|
|
96
|
+
const filtered = messages.filter((msg) => msg.id !== messageId);
|
|
97
|
+
writeFileSync(this.filePath, JSON.stringify(filtered, null, 2), "utf-8");
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error("Failed to delete message:", error);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async clear() {
|
|
104
|
+
try {
|
|
105
|
+
writeFileSync(this.filePath, JSON.stringify([], null, 2), "utf-8");
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error("Failed to clear messages:", error);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async getStats() {
|
|
112
|
+
try {
|
|
113
|
+
if (!existsSync(this.filePath)) {
|
|
114
|
+
return { count: 0, size: 0 };
|
|
115
|
+
}
|
|
116
|
+
const messages = await this.load();
|
|
117
|
+
const content = readFileSync(this.filePath, "utf-8");
|
|
118
|
+
return {
|
|
119
|
+
count: messages.length,
|
|
120
|
+
size: Buffer.byteLength(content, "utf-8")
|
|
121
|
+
};
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error("Failed to get storage stats:", error);
|
|
124
|
+
return { count: 0, size: 0 };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
class MessageBus {
|
|
129
|
+
config;
|
|
130
|
+
subscriptions;
|
|
131
|
+
messageHistory;
|
|
132
|
+
deadLetterQueue;
|
|
133
|
+
storage;
|
|
134
|
+
stats;
|
|
135
|
+
constructor(config = {}) {
|
|
136
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
137
|
+
this.subscriptions = /* @__PURE__ */ new Map();
|
|
138
|
+
this.messageHistory = [];
|
|
139
|
+
this.deadLetterQueue = [];
|
|
140
|
+
if (this.config.enablePersistence && this.config.persistencePath) {
|
|
141
|
+
this.storage = new FileMessageStorage(this.config.persistencePath);
|
|
142
|
+
this.loadPersistedMessages();
|
|
143
|
+
}
|
|
144
|
+
this.stats = {
|
|
145
|
+
totalMessages: 0,
|
|
146
|
+
messagesByType: {},
|
|
147
|
+
messagesByStatus: {},
|
|
148
|
+
activeSubscriptions: 0,
|
|
149
|
+
historySize: 0,
|
|
150
|
+
deadLetterQueueSize: 0,
|
|
151
|
+
avgProcessingTime: 0
|
|
152
|
+
};
|
|
153
|
+
this.startCleanupInterval();
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Publish a message to the bus
|
|
157
|
+
*/
|
|
158
|
+
async publish(type, from, to, subject, payload, options = {}) {
|
|
159
|
+
const message = {
|
|
160
|
+
id: randomUUID(),
|
|
161
|
+
type,
|
|
162
|
+
from,
|
|
163
|
+
to,
|
|
164
|
+
subject,
|
|
165
|
+
payload,
|
|
166
|
+
priority: options.priority || "normal",
|
|
167
|
+
status: "pending",
|
|
168
|
+
timestamp: Date.now(),
|
|
169
|
+
correlationId: options.correlationId,
|
|
170
|
+
replyTo: options.replyTo,
|
|
171
|
+
metadata: options.metadata
|
|
172
|
+
};
|
|
173
|
+
if (this.config.enableValidation) {
|
|
174
|
+
this.validateMessage(message);
|
|
175
|
+
}
|
|
176
|
+
this.logMessage("publish", message);
|
|
177
|
+
this.updateStats(message);
|
|
178
|
+
this.addToHistory(message);
|
|
179
|
+
if (this.storage) {
|
|
180
|
+
await this.storage.save(message);
|
|
181
|
+
}
|
|
182
|
+
await this.routeMessage(message);
|
|
183
|
+
return message.id;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Subscribe to messages
|
|
187
|
+
*/
|
|
188
|
+
subscribe(subscriber, handler, options = {}) {
|
|
189
|
+
const subscription = {
|
|
190
|
+
id: randomUUID(),
|
|
191
|
+
subscriber,
|
|
192
|
+
options,
|
|
193
|
+
handler,
|
|
194
|
+
createdAt: Date.now(),
|
|
195
|
+
unsubscribe: () => this.unsubscribe(subscription.id)
|
|
196
|
+
};
|
|
197
|
+
this.subscriptions.set(subscription.id, subscription);
|
|
198
|
+
this.stats.activeSubscriptions = this.subscriptions.size;
|
|
199
|
+
this.log("info", `Agent ${subscriber} subscribed with ID ${subscription.id}`);
|
|
200
|
+
return subscription;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Unsubscribe from messages
|
|
204
|
+
*/
|
|
205
|
+
unsubscribe(subscriptionId) {
|
|
206
|
+
const subscription = this.subscriptions.get(subscriptionId);
|
|
207
|
+
if (subscription) {
|
|
208
|
+
this.subscriptions.delete(subscriptionId);
|
|
209
|
+
this.stats.activeSubscriptions = this.subscriptions.size;
|
|
210
|
+
this.log("info", `Subscription ${subscriptionId} removed`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Get message by ID
|
|
215
|
+
*/
|
|
216
|
+
getMessage(messageId) {
|
|
217
|
+
return this.messageHistory.find((msg) => msg.id === messageId);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Get messages by filter
|
|
221
|
+
*/
|
|
222
|
+
getMessages(filter) {
|
|
223
|
+
return this.messageHistory.filter(filter);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Update message status
|
|
227
|
+
*/
|
|
228
|
+
async updateMessageStatus(messageId, status, error) {
|
|
229
|
+
const message = this.getMessage(messageId);
|
|
230
|
+
if (message) {
|
|
231
|
+
message.status = status;
|
|
232
|
+
if (error) {
|
|
233
|
+
message.error = error;
|
|
234
|
+
}
|
|
235
|
+
if (status === "failed" && this.config.enableDeadLetterQueue) {
|
|
236
|
+
this.deadLetterQueue.push(message);
|
|
237
|
+
this.stats.deadLetterQueueSize = this.deadLetterQueue.length;
|
|
238
|
+
}
|
|
239
|
+
this.stats.messagesByStatus[status] = (this.stats.messagesByStatus[status] || 0) + 1;
|
|
240
|
+
if (this.storage) {
|
|
241
|
+
await this.storage.save(message);
|
|
242
|
+
}
|
|
243
|
+
this.log("debug", `Message ${messageId} status updated to ${status}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Get message bus statistics
|
|
248
|
+
*/
|
|
249
|
+
getStats() {
|
|
250
|
+
return { ...this.stats };
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Clear message history
|
|
254
|
+
*/
|
|
255
|
+
async clearHistory() {
|
|
256
|
+
this.messageHistory = [];
|
|
257
|
+
this.stats.historySize = 0;
|
|
258
|
+
if (this.storage) {
|
|
259
|
+
await this.storage.clear();
|
|
260
|
+
}
|
|
261
|
+
this.log("info", "Message history cleared");
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Clear dead letter queue
|
|
265
|
+
*/
|
|
266
|
+
clearDeadLetterQueue() {
|
|
267
|
+
this.deadLetterQueue = [];
|
|
268
|
+
this.stats.deadLetterQueueSize = 0;
|
|
269
|
+
this.log("info", "Dead letter queue cleared");
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Get dead letter queue messages
|
|
273
|
+
*/
|
|
274
|
+
getDeadLetterQueue() {
|
|
275
|
+
return [...this.deadLetterQueue];
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Shutdown message bus
|
|
279
|
+
*/
|
|
280
|
+
async shutdown() {
|
|
281
|
+
this.log("info", "Shutting down message bus");
|
|
282
|
+
this.subscriptions.clear();
|
|
283
|
+
this.stats.activeSubscriptions = 0;
|
|
284
|
+
if (this.storage && this.messageHistory.length > 0) {
|
|
285
|
+
for (const message of this.messageHistory) {
|
|
286
|
+
await this.storage.save(message);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
this.log("info", "Message bus shutdown complete");
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Route message to subscribers
|
|
293
|
+
*/
|
|
294
|
+
async routeMessage(message) {
|
|
295
|
+
const startTime = Date.now();
|
|
296
|
+
const matchingSubscriptions = this.findMatchingSubscriptions(message);
|
|
297
|
+
this.log("debug", `Routing message ${message.id} to ${matchingSubscriptions.length} subscribers`);
|
|
298
|
+
for (const subscription of matchingSubscriptions) {
|
|
299
|
+
try {
|
|
300
|
+
message.status = "processing";
|
|
301
|
+
if (subscription.options.async) {
|
|
302
|
+
await subscription.handler(message);
|
|
303
|
+
} else {
|
|
304
|
+
subscription.handler(message);
|
|
305
|
+
}
|
|
306
|
+
message.status = "completed";
|
|
307
|
+
} catch (error) {
|
|
308
|
+
this.log("error", `Error handling message ${message.id} in subscription ${subscription.id}:`, error);
|
|
309
|
+
await this.updateMessageStatus(message.id, "failed", {
|
|
310
|
+
code: "HANDLER_ERROR",
|
|
311
|
+
message: error instanceof Error ? error.message : String(error),
|
|
312
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const processingTime = Date.now() - startTime;
|
|
317
|
+
this.stats.avgProcessingTime = (this.stats.avgProcessingTime + processingTime) / 2;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Find subscriptions matching the message
|
|
321
|
+
*/
|
|
322
|
+
findMatchingSubscriptions(message) {
|
|
323
|
+
const matching = [];
|
|
324
|
+
const subscriptions = Array.from(this.subscriptions.values());
|
|
325
|
+
for (const subscription of subscriptions) {
|
|
326
|
+
const isAddressed = message.to === "all" || message.to === subscription.subscriber || Array.isArray(message.to) && message.to.includes(subscription.subscriber);
|
|
327
|
+
if (!isAddressed) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (!this.matchesSubscriptionFilters(message, subscription.options)) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
matching.push(subscription);
|
|
334
|
+
}
|
|
335
|
+
return matching;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Check if message matches subscription filters
|
|
339
|
+
*/
|
|
340
|
+
matchesSubscriptionFilters(message, options) {
|
|
341
|
+
if (options.type) {
|
|
342
|
+
const types = Array.isArray(options.type) ? options.type : [options.type];
|
|
343
|
+
if (!types.includes(message.type)) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (options.from) {
|
|
348
|
+
const senders = Array.isArray(options.from) ? options.from : [options.from];
|
|
349
|
+
if (!senders.includes(message.from)) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (options.priority) {
|
|
354
|
+
const priorities = Array.isArray(options.priority) ? options.priority : [options.priority];
|
|
355
|
+
if (!priorities.includes(message.priority)) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (options.filter && !options.filter(message)) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Validate message
|
|
366
|
+
*/
|
|
367
|
+
validateMessage(message) {
|
|
368
|
+
if (!message.id) {
|
|
369
|
+
throw new Error("Message ID is required");
|
|
370
|
+
}
|
|
371
|
+
if (!message.type) {
|
|
372
|
+
throw new Error("Message type is required");
|
|
373
|
+
}
|
|
374
|
+
if (!message.from) {
|
|
375
|
+
throw new Error("Message sender is required");
|
|
376
|
+
}
|
|
377
|
+
if (!message.to) {
|
|
378
|
+
throw new Error("Message recipient is required");
|
|
379
|
+
}
|
|
380
|
+
if (!message.subject) {
|
|
381
|
+
throw new Error("Message subject is required");
|
|
382
|
+
}
|
|
383
|
+
if (this.config.maxMessageSize) {
|
|
384
|
+
const messageSize = Buffer.byteLength(JSON.stringify(message), "utf-8");
|
|
385
|
+
if (messageSize > this.config.maxMessageSize) {
|
|
386
|
+
throw new Error(`Message size ${messageSize} exceeds maximum ${this.config.maxMessageSize}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Add message to history
|
|
392
|
+
*/
|
|
393
|
+
addToHistory(message) {
|
|
394
|
+
this.messageHistory.push(message);
|
|
395
|
+
this.stats.historySize = this.messageHistory.length;
|
|
396
|
+
const maxHistorySize = this.config.maxHistorySize ?? 1e3;
|
|
397
|
+
if (this.messageHistory.length > maxHistorySize) {
|
|
398
|
+
this.messageHistory.shift();
|
|
399
|
+
this.stats.historySize = this.messageHistory.length;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Update statistics
|
|
404
|
+
*/
|
|
405
|
+
updateStats(message) {
|
|
406
|
+
this.stats.totalMessages++;
|
|
407
|
+
this.stats.messagesByType[message.type] = (this.stats.messagesByType[message.type] || 0) + 1;
|
|
408
|
+
this.stats.messagesByStatus[message.status] = (this.stats.messagesByStatus[message.status] || 0) + 1;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Load persisted messages
|
|
412
|
+
*/
|
|
413
|
+
async loadPersistedMessages() {
|
|
414
|
+
if (!this.storage) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
const messages = await this.storage.load();
|
|
419
|
+
this.messageHistory = messages;
|
|
420
|
+
this.stats.historySize = messages.length;
|
|
421
|
+
this.log("info", `Loaded ${messages.length} persisted messages`);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
this.log("error", "Failed to load persisted messages:", error);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Start cleanup interval for old messages
|
|
428
|
+
*/
|
|
429
|
+
startCleanupInterval() {
|
|
430
|
+
setInterval(() => {
|
|
431
|
+
const now = Date.now();
|
|
432
|
+
const retentionTime = this.config.messageRetentionTime ?? 864e5;
|
|
433
|
+
this.messageHistory = this.messageHistory.filter(
|
|
434
|
+
(msg) => now - msg.timestamp < retentionTime
|
|
435
|
+
);
|
|
436
|
+
this.stats.historySize = this.messageHistory.length;
|
|
437
|
+
this.deadLetterQueue = this.deadLetterQueue.filter(
|
|
438
|
+
(msg) => now - msg.timestamp < retentionTime
|
|
439
|
+
);
|
|
440
|
+
this.stats.deadLetterQueueSize = this.deadLetterQueue.length;
|
|
441
|
+
this.log("debug", "Cleanup completed");
|
|
442
|
+
}, 60 * 60 * 1e3);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Log message
|
|
446
|
+
*/
|
|
447
|
+
logMessage(action, message) {
|
|
448
|
+
if (!this.config.enableLogging) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const logLevel = this.config.logLevel;
|
|
452
|
+
const shouldLog = logLevel === "debug" || logLevel === "info" && ["publish", "subscribe"].includes(action) || logLevel === "warn" && message.priority === "high" || logLevel === "error" && message.status === "failed";
|
|
453
|
+
if (shouldLog) {
|
|
454
|
+
this.log("debug", `[${action}] ${message.type} from ${message.from} to ${message.to}: ${message.subject}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Internal logging
|
|
459
|
+
*/
|
|
460
|
+
log(level, message, ...args) {
|
|
461
|
+
if (!this.config.enableLogging) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const levels = ["debug", "info", "warn", "error"];
|
|
465
|
+
const configLevel = levels.indexOf(this.config.logLevel ?? "info");
|
|
466
|
+
const messageLevel = levels.indexOf(level);
|
|
467
|
+
if (messageLevel >= configLevel) {
|
|
468
|
+
console[level](`[MessageBus] ${message}`, ...args);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
let globalMessageBus = null;
|
|
473
|
+
function getMessageBus(config) {
|
|
474
|
+
if (!globalMessageBus) {
|
|
475
|
+
globalMessageBus = new MessageBus(config);
|
|
476
|
+
}
|
|
477
|
+
return globalMessageBus;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
class SkillRegistry extends EventEmitter {
|
|
481
|
+
skills;
|
|
482
|
+
filePathIndex;
|
|
483
|
+
triggerIndex;
|
|
484
|
+
messageBus = getMessageBus();
|
|
485
|
+
constructor() {
|
|
486
|
+
super();
|
|
487
|
+
this.skills = /* @__PURE__ */ new Map();
|
|
488
|
+
this.filePathIndex = /* @__PURE__ */ new Map();
|
|
489
|
+
this.triggerIndex = /* @__PURE__ */ new Map();
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Register a skill
|
|
493
|
+
*
|
|
494
|
+
* @param skill - Parsed skill file
|
|
495
|
+
* @param source - Skill source type
|
|
496
|
+
* @returns Registered entry
|
|
497
|
+
*/
|
|
498
|
+
register(skill, source = "user") {
|
|
499
|
+
const id = skill.metadata.name;
|
|
500
|
+
const existing = this.skills.get(id);
|
|
501
|
+
const dependencies = this.extractDependencies(skill.metadata);
|
|
502
|
+
const entry = {
|
|
503
|
+
id,
|
|
504
|
+
metadata: skill.metadata,
|
|
505
|
+
content: skill.content,
|
|
506
|
+
filePath: skill.filePath,
|
|
507
|
+
enabled: existing?.enabled ?? true,
|
|
508
|
+
source,
|
|
509
|
+
registeredAt: existing?.registeredAt ?? Date.now(),
|
|
510
|
+
modifiedAt: skill.modifiedAt?.getTime() ?? Date.now(),
|
|
511
|
+
estimatedTokens: this.estimateTokens(skill),
|
|
512
|
+
dependencies,
|
|
513
|
+
dependents: existing?.dependents || /* @__PURE__ */ new Set()
|
|
514
|
+
};
|
|
515
|
+
this.skills.set(id, entry);
|
|
516
|
+
this.filePathIndex.set(skill.filePath, id);
|
|
517
|
+
this.updateTriggerIndex(id, entry.metadata.triggers);
|
|
518
|
+
this.updateDependencyGraph(id, dependencies);
|
|
519
|
+
if (existing) {
|
|
520
|
+
this.emit("skill:updated", existing, entry);
|
|
521
|
+
this.publishMessage("skill:updated", { oldSkill: existing, newSkill: entry });
|
|
522
|
+
} else {
|
|
523
|
+
this.emit("skill:registered", entry);
|
|
524
|
+
this.publishMessage("skill:registered", entry);
|
|
525
|
+
}
|
|
526
|
+
return entry;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Unregister a skill by ID
|
|
530
|
+
*
|
|
531
|
+
* @param id - Skill ID
|
|
532
|
+
* @returns True if skill was unregistered
|
|
533
|
+
*/
|
|
534
|
+
unregister(id) {
|
|
535
|
+
const entry = this.skills.get(id);
|
|
536
|
+
if (!entry)
|
|
537
|
+
return false;
|
|
538
|
+
if (entry.dependents.size > 0) {
|
|
539
|
+
const dependentList = Array.from(entry.dependents).join(", ");
|
|
540
|
+
throw new Error(`Cannot unregister skill "${id}": depended upon by: ${dependentList}`);
|
|
541
|
+
}
|
|
542
|
+
this.skills.delete(id);
|
|
543
|
+
this.filePathIndex.delete(entry.filePath);
|
|
544
|
+
for (const trigger of entry.metadata.triggers) {
|
|
545
|
+
const skills = this.triggerIndex.get(trigger);
|
|
546
|
+
if (skills) {
|
|
547
|
+
skills.delete(id);
|
|
548
|
+
if (skills.size === 0)
|
|
549
|
+
this.triggerIndex.delete(trigger);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
for (const dep of entry.dependencies) {
|
|
553
|
+
const depEntry = this.skills.get(dep);
|
|
554
|
+
if (depEntry)
|
|
555
|
+
depEntry.dependents.delete(id);
|
|
556
|
+
}
|
|
557
|
+
this.emit("skill:unregistered", entry);
|
|
558
|
+
this.publishMessage("skill:unregistered", entry);
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Unregister a skill by file path
|
|
563
|
+
*
|
|
564
|
+
* @param filePath - File path
|
|
565
|
+
* @returns True if skill was unregistered
|
|
566
|
+
*/
|
|
567
|
+
unregisterByPath(filePath) {
|
|
568
|
+
const id = this.filePathIndex.get(filePath);
|
|
569
|
+
return id ? this.unregister(id) : false;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Get a skill by ID
|
|
573
|
+
*
|
|
574
|
+
* @param id - Skill ID
|
|
575
|
+
* @returns Skill entry or undefined
|
|
576
|
+
*/
|
|
577
|
+
getById(id) {
|
|
578
|
+
return this.skills.get(id);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Get a skill by file path
|
|
582
|
+
*
|
|
583
|
+
* @param filePath - File path
|
|
584
|
+
* @returns Skill entry or undefined
|
|
585
|
+
*/
|
|
586
|
+
getByPath(filePath) {
|
|
587
|
+
const id = this.filePathIndex.get(filePath);
|
|
588
|
+
return id ? this.skills.get(id) : void 0;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Get skills by trigger
|
|
592
|
+
*
|
|
593
|
+
* @param trigger - Trigger string (e.g., '/commit')
|
|
594
|
+
* @returns Array of matching skills
|
|
595
|
+
*/
|
|
596
|
+
getByTrigger(trigger) {
|
|
597
|
+
const ids = this.triggerIndex.get(trigger);
|
|
598
|
+
if (!ids)
|
|
599
|
+
return [];
|
|
600
|
+
return Array.from(ids).map((id) => this.skills.get(id)).filter((e) => e !== void 0 && e.enabled);
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Lookup skills with filters
|
|
604
|
+
*
|
|
605
|
+
* @param options - Lookup options
|
|
606
|
+
* @returns Array of matching skills
|
|
607
|
+
*/
|
|
608
|
+
lookup(options = {}) {
|
|
609
|
+
let results = Array.from(this.skills.values());
|
|
610
|
+
if (options.enabled !== void 0) {
|
|
611
|
+
results = results.filter((e) => e.enabled === options.enabled);
|
|
612
|
+
}
|
|
613
|
+
if (options.category) {
|
|
614
|
+
results = results.filter((e) => e.metadata.category === options.category);
|
|
615
|
+
}
|
|
616
|
+
if (options.source) {
|
|
617
|
+
results = results.filter((e) => e.source === options.source);
|
|
618
|
+
}
|
|
619
|
+
if (options.userInvocable !== void 0) {
|
|
620
|
+
results = results.filter(
|
|
621
|
+
(e) => (e.metadata.user_invocable ?? true) === options.userInvocable
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
if (options.autoActivate !== void 0) {
|
|
625
|
+
results = results.filter(
|
|
626
|
+
(e) => (e.metadata.auto_activate ?? false) === options.autoActivate
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
if (options.agent) {
|
|
630
|
+
results = results.filter(
|
|
631
|
+
(e) => e.metadata.agent === options.agent || e.metadata.agents?.includes(options.agent)
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
if (options.search) {
|
|
635
|
+
const query = options.search.toLowerCase();
|
|
636
|
+
results = results.filter(
|
|
637
|
+
(e) => e.id.toLowerCase().includes(query) || e.metadata.description.toLowerCase().includes(query) || e.metadata.tags?.some((t) => t.toLowerCase().includes(query))
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
if (options.sortBy) {
|
|
641
|
+
const dir = options.sortDir === "desc" ? -1 : 1;
|
|
642
|
+
results.sort((a, b) => {
|
|
643
|
+
switch (options.sortBy) {
|
|
644
|
+
case "name":
|
|
645
|
+
return a.id.localeCompare(b.id) * dir;
|
|
646
|
+
case "priority":
|
|
647
|
+
return ((a.metadata.priority ?? 5) - (b.metadata.priority ?? 5)) * dir;
|
|
648
|
+
case "registeredAt":
|
|
649
|
+
return (a.registeredAt - b.registeredAt) * dir;
|
|
650
|
+
case "modifiedAt":
|
|
651
|
+
return (a.modifiedAt - b.modifiedAt) * dir;
|
|
652
|
+
default:
|
|
653
|
+
return 0;
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
if (options.limit) {
|
|
658
|
+
results = results.slice(0, options.limit);
|
|
659
|
+
}
|
|
660
|
+
return results;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Enable a skill
|
|
664
|
+
*
|
|
665
|
+
* @param id - Skill ID
|
|
666
|
+
* @returns True if enabled
|
|
667
|
+
*/
|
|
668
|
+
enable(id) {
|
|
669
|
+
const entry = this.skills.get(id);
|
|
670
|
+
if (!entry || entry.enabled)
|
|
671
|
+
return false;
|
|
672
|
+
entry.enabled = true;
|
|
673
|
+
this.emit("skill:enabled", entry);
|
|
674
|
+
this.publishMessage("skill:enabled", entry);
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Disable a skill
|
|
679
|
+
*
|
|
680
|
+
* @param id - Skill ID
|
|
681
|
+
* @returns True if disabled
|
|
682
|
+
*/
|
|
683
|
+
disable(id) {
|
|
684
|
+
const entry = this.skills.get(id);
|
|
685
|
+
if (!entry || !entry.enabled)
|
|
686
|
+
return false;
|
|
687
|
+
entry.enabled = false;
|
|
688
|
+
this.emit("skill:disabled", entry);
|
|
689
|
+
this.publishMessage("skill:disabled", entry);
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Toggle skill enabled state
|
|
694
|
+
*
|
|
695
|
+
* @param id - Skill ID
|
|
696
|
+
* @returns New enabled state
|
|
697
|
+
*/
|
|
698
|
+
toggle(id) {
|
|
699
|
+
const entry = this.skills.get(id);
|
|
700
|
+
if (!entry)
|
|
701
|
+
return false;
|
|
702
|
+
entry.enabled = !entry.enabled;
|
|
703
|
+
const event = entry.enabled ? "skill:enabled" : "skill:disabled";
|
|
704
|
+
this.emit(event, entry);
|
|
705
|
+
this.publishMessage(event, entry);
|
|
706
|
+
return entry.enabled;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Check if a skill exists
|
|
710
|
+
*
|
|
711
|
+
* @param id - Skill ID
|
|
712
|
+
* @returns True if skill exists
|
|
713
|
+
*/
|
|
714
|
+
has(id) {
|
|
715
|
+
return this.skills.has(id);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Check if a skill is enabled
|
|
719
|
+
*
|
|
720
|
+
* @param id - Skill ID
|
|
721
|
+
* @returns True if enabled, false if disabled or not found
|
|
722
|
+
*/
|
|
723
|
+
isEnabled(id) {
|
|
724
|
+
return this.skills.get(id)?.enabled ?? false;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Get all skill IDs
|
|
728
|
+
*
|
|
729
|
+
* @returns Array of skill IDs
|
|
730
|
+
*/
|
|
731
|
+
getIds() {
|
|
732
|
+
return Array.from(this.skills.keys());
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Get all entries
|
|
736
|
+
*
|
|
737
|
+
* @returns Array of all entries
|
|
738
|
+
*/
|
|
739
|
+
getAll() {
|
|
740
|
+
return Array.from(this.skills.values());
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Get enabled entries
|
|
744
|
+
*
|
|
745
|
+
* @returns Array of enabled entries
|
|
746
|
+
*/
|
|
747
|
+
getEnabled() {
|
|
748
|
+
return this.lookup({ enabled: true });
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Get registry statistics
|
|
752
|
+
*
|
|
753
|
+
* @returns Registry statistics
|
|
754
|
+
*/
|
|
755
|
+
getStats() {
|
|
756
|
+
const all = this.getAll();
|
|
757
|
+
const enabled = this.getEnabled();
|
|
758
|
+
const byCategory = {
|
|
759
|
+
dev: 0,
|
|
760
|
+
git: 0,
|
|
761
|
+
review: 0,
|
|
762
|
+
testing: 0,
|
|
763
|
+
docs: 0,
|
|
764
|
+
devops: 0,
|
|
765
|
+
planning: 0,
|
|
766
|
+
debugging: 0,
|
|
767
|
+
custom: 0
|
|
768
|
+
};
|
|
769
|
+
const bySource = { builtin: 0, user: 0, marketplace: 0 };
|
|
770
|
+
let totalTokens = 0;
|
|
771
|
+
let lastRegistered = 0;
|
|
772
|
+
let lastModified = 0;
|
|
773
|
+
for (const entry of all) {
|
|
774
|
+
byCategory[entry.metadata.category]++;
|
|
775
|
+
bySource[entry.source]++;
|
|
776
|
+
totalTokens += entry.estimatedTokens;
|
|
777
|
+
if (entry.registeredAt > lastRegistered)
|
|
778
|
+
lastRegistered = entry.registeredAt;
|
|
779
|
+
if (entry.modifiedAt > lastModified)
|
|
780
|
+
lastModified = entry.modifiedAt;
|
|
781
|
+
}
|
|
782
|
+
return {
|
|
783
|
+
totalSkills: all.length,
|
|
784
|
+
enabledSkills: enabled.length,
|
|
785
|
+
disabledSkills: all.length - enabled.length,
|
|
786
|
+
byCategory,
|
|
787
|
+
bySource,
|
|
788
|
+
totalTokens,
|
|
789
|
+
lastRegistered: lastRegistered > 0 ? this.getById(this.getEntriesSortedBy("registeredAt")[0]?.id || "")?.id : void 0,
|
|
790
|
+
lastModified: lastModified > 0 ? this.getById(this.getEntriesSortedBy("modifiedAt")[0]?.id || "")?.id : void 0
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Get dependent skills
|
|
795
|
+
*
|
|
796
|
+
* @param id - Skill ID
|
|
797
|
+
* @returns Array of dependent skill IDs
|
|
798
|
+
*/
|
|
799
|
+
getDependents(id) {
|
|
800
|
+
return Array.from(this.skills.get(id)?.dependents || []);
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Get skill dependencies
|
|
804
|
+
*
|
|
805
|
+
* @param id - Skill ID
|
|
806
|
+
* @returns Array of dependency IDs
|
|
807
|
+
*/
|
|
808
|
+
getDependencies(id) {
|
|
809
|
+
return this.skills.get(id)?.dependencies || [];
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Check for missing dependencies
|
|
813
|
+
*
|
|
814
|
+
* @param id - Skill ID
|
|
815
|
+
* @returns Array of missing dependency IDs
|
|
816
|
+
*/
|
|
817
|
+
getMissingDependencies(id) {
|
|
818
|
+
const deps = this.getDependencies(id);
|
|
819
|
+
return deps.filter((dep) => !this.has(dep));
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Validate all dependencies
|
|
823
|
+
*
|
|
824
|
+
* @returns Map of skill ID to missing dependencies
|
|
825
|
+
*/
|
|
826
|
+
validateDependencies() {
|
|
827
|
+
const missing = /* @__PURE__ */ new Map();
|
|
828
|
+
Array.from(this.skills.entries()).forEach(([id, entry]) => {
|
|
829
|
+
const missingDeps = entry.dependencies.filter((dep) => !this.has(dep));
|
|
830
|
+
if (missingDeps.length > 0) {
|
|
831
|
+
missing.set(id, missingDeps);
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
return missing;
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Clear all skills from registry
|
|
838
|
+
*/
|
|
839
|
+
clear() {
|
|
840
|
+
this.skills.clear();
|
|
841
|
+
this.filePathIndex.clear();
|
|
842
|
+
this.triggerIndex.clear();
|
|
843
|
+
this.emit("registry:cleared");
|
|
844
|
+
this.publishMessage("registry:cleared", {});
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Get the size of the registry
|
|
848
|
+
*
|
|
849
|
+
* @returns Number of registered skills
|
|
850
|
+
*/
|
|
851
|
+
size() {
|
|
852
|
+
return this.skills.size;
|
|
853
|
+
}
|
|
854
|
+
// ==========================================================================
|
|
855
|
+
// Private Methods
|
|
856
|
+
// ==========================================================================
|
|
857
|
+
/**
|
|
858
|
+
* Extract dependencies from skill metadata
|
|
859
|
+
*/
|
|
860
|
+
extractDependencies(metadata) {
|
|
861
|
+
const deps = [];
|
|
862
|
+
if (metadata.agents) {
|
|
863
|
+
deps.push(...metadata.agents);
|
|
864
|
+
}
|
|
865
|
+
if (metadata.related_skills) {
|
|
866
|
+
deps.push(...metadata.related_skills);
|
|
867
|
+
}
|
|
868
|
+
return deps;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Update trigger index
|
|
872
|
+
*/
|
|
873
|
+
updateTriggerIndex(id, triggers) {
|
|
874
|
+
Array.from(this.triggerIndex.entries()).forEach(([trigger, skillIds]) => {
|
|
875
|
+
skillIds.delete(id);
|
|
876
|
+
if (skillIds.size === 0)
|
|
877
|
+
this.triggerIndex.delete(trigger);
|
|
878
|
+
});
|
|
879
|
+
for (const trigger of triggers) {
|
|
880
|
+
if (!this.triggerIndex.has(trigger)) {
|
|
881
|
+
this.triggerIndex.set(trigger, /* @__PURE__ */ new Set());
|
|
882
|
+
}
|
|
883
|
+
this.triggerIndex.get(trigger).add(id);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Update dependency graph
|
|
888
|
+
*/
|
|
889
|
+
updateDependencyGraph(id, dependencies) {
|
|
890
|
+
Array.from(this.skills.entries()).forEach(([_depId, entry]) => {
|
|
891
|
+
if (entry.dependencies.includes(id)) {
|
|
892
|
+
entry.dependents.delete(id);
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
for (const dep of dependencies) {
|
|
896
|
+
const depEntry = this.skills.get(dep);
|
|
897
|
+
if (depEntry) {
|
|
898
|
+
depEntry.dependents.add(id);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Estimate token count for a skill
|
|
904
|
+
*/
|
|
905
|
+
estimateTokens(skill) {
|
|
906
|
+
const contentTokens = Math.ceil(skill.content.length / 4);
|
|
907
|
+
const metadataTokens = Math.ceil(JSON.stringify(skill.metadata).length / 4);
|
|
908
|
+
return contentTokens + metadataTokens;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Get entries sorted by a field
|
|
912
|
+
*/
|
|
913
|
+
getEntriesSortedBy(field) {
|
|
914
|
+
return Array.from(this.skills.values()).sort((a, b) => b[field] - a[field]);
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Publish message to event bus
|
|
918
|
+
*/
|
|
919
|
+
publishMessage(type, payload) {
|
|
920
|
+
this.messageBus.publish(
|
|
921
|
+
type,
|
|
922
|
+
"coordinator",
|
|
923
|
+
"all",
|
|
924
|
+
`Skill registry event: ${type}`,
|
|
925
|
+
payload,
|
|
926
|
+
{ priority: "normal" }
|
|
927
|
+
).catch(console.error);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
let registryInstance = null;
|
|
931
|
+
function getSkillRegistry() {
|
|
932
|
+
if (!registryInstance) {
|
|
933
|
+
registryInstance = new SkillRegistry();
|
|
934
|
+
}
|
|
935
|
+
return registryInstance;
|
|
936
|
+
}
|
|
16
937
|
|
|
17
938
|
class ProgressTracker extends EventEmitter {
|
|
18
939
|
config;
|
|
@@ -1879,6 +2800,11 @@ class AutoExecutor extends EventEmitter {
|
|
|
1879
2800
|
const executionId = `exec-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1880
2801
|
const startedAt = Date.now();
|
|
1881
2802
|
this.emit("execution:started", { input: userInput });
|
|
2803
|
+
this.emitCommandHook("command-start", {
|
|
2804
|
+
executionId,
|
|
2805
|
+
inputLength: userInput.length,
|
|
2806
|
+
source: "auto-executor"
|
|
2807
|
+
});
|
|
1882
2808
|
this.config.telemetry.record({
|
|
1883
2809
|
executionId,
|
|
1884
2810
|
phase: "execution",
|
|
@@ -1904,6 +2830,13 @@ class AutoExecutor extends EventEmitter {
|
|
|
1904
2830
|
complexity: routeResult.intent.complexity
|
|
1905
2831
|
}
|
|
1906
2832
|
});
|
|
2833
|
+
this.emitCommandHook("command-telemetry", {
|
|
2834
|
+
executionId,
|
|
2835
|
+
phase: "intent",
|
|
2836
|
+
action: "route",
|
|
2837
|
+
route: routeResult.route,
|
|
2838
|
+
confidence: routeResult.intent.confidence
|
|
2839
|
+
});
|
|
1907
2840
|
const elicitationResult = await this.resolveRouteWithElicitation(
|
|
1908
2841
|
routeResult.route,
|
|
1909
2842
|
routeResult.intent,
|
|
@@ -2039,6 +2972,12 @@ class AutoExecutor extends EventEmitter {
|
|
|
2039
2972
|
complexity: intent.complexity
|
|
2040
2973
|
}
|
|
2041
2974
|
});
|
|
2975
|
+
this.emitCommandHook("command-telemetry", {
|
|
2976
|
+
executionId,
|
|
2977
|
+
phase: "route",
|
|
2978
|
+
action: route,
|
|
2979
|
+
durationMs: routeDuration
|
|
2980
|
+
});
|
|
2042
2981
|
const totalDuration = Date.now() - startedAt;
|
|
2043
2982
|
this.metricsCollector.recordResponseTime("auto-executor", totalDuration);
|
|
2044
2983
|
this.metricsCollector.recordTaskCompletion("auto-executor", true, totalDuration);
|
|
@@ -2055,6 +2994,13 @@ class AutoExecutor extends EventEmitter {
|
|
|
2055
2994
|
mcpToolsUsed: mcpToolsUsed.length
|
|
2056
2995
|
}
|
|
2057
2996
|
});
|
|
2997
|
+
this.emitCommandHook("command-complete", {
|
|
2998
|
+
executionId,
|
|
2999
|
+
success: true,
|
|
3000
|
+
route,
|
|
3001
|
+
durationMs: totalDuration,
|
|
3002
|
+
mcpToolsUsed: mcpToolsUsed.length
|
|
3003
|
+
});
|
|
2058
3004
|
result.insights = this.buildExecutionInsights({
|
|
2059
3005
|
initialRoute: routeResult.route,
|
|
2060
3006
|
resolvedRoute: route,
|
|
@@ -2082,6 +3028,12 @@ class AutoExecutor extends EventEmitter {
|
|
|
2082
3028
|
error: errorMessage
|
|
2083
3029
|
}
|
|
2084
3030
|
});
|
|
3031
|
+
this.emitCommandHook("command-complete", {
|
|
3032
|
+
executionId,
|
|
3033
|
+
success: false,
|
|
3034
|
+
durationMs: totalDuration,
|
|
3035
|
+
error: errorMessage
|
|
3036
|
+
});
|
|
2085
3037
|
this.emit("execution:failed", { error, input: userInput });
|
|
2086
3038
|
throw error;
|
|
2087
3039
|
}
|
|
@@ -2552,6 +3504,9 @@ class AutoExecutor extends EventEmitter {
|
|
|
2552
3504
|
getTelemetryEvents(limit = 50) {
|
|
2553
3505
|
return this.config.telemetry.getRecent(limit);
|
|
2554
3506
|
}
|
|
3507
|
+
clearTelemetry() {
|
|
3508
|
+
this.config.telemetry.clear();
|
|
3509
|
+
}
|
|
2555
3510
|
getErrorMessage(error) {
|
|
2556
3511
|
if (error instanceof Error) {
|
|
2557
3512
|
return error.message;
|
|
@@ -2566,6 +3521,9 @@ class AutoExecutor extends EventEmitter {
|
|
|
2566
3521
|
console.log(`[AutoExecutor] ${message}`);
|
|
2567
3522
|
}
|
|
2568
3523
|
}
|
|
3524
|
+
emitCommandHook(event, data) {
|
|
3525
|
+
void emitCommandHookEvent(event, data);
|
|
3526
|
+
}
|
|
2569
3527
|
}
|
|
2570
3528
|
let globalExecutor = null;
|
|
2571
3529
|
function getGlobalAutoExecutor(config) {
|
|
@@ -2674,6 +3632,7 @@ class CliInterceptor extends EventEmitter {
|
|
|
2674
3632
|
autoExecute: config.autoExecute !== void 0 ? config.autoExecute : true,
|
|
2675
3633
|
showIntent: config.showIntent !== void 0 ? config.showIntent : true,
|
|
2676
3634
|
bypassKeywords: config.bypassKeywords || [],
|
|
3635
|
+
ccjkOwnedSlashPrefixes: config.ccjkOwnedSlashPrefixes || ["/ccjk", "/ccjk:", "/plugin", "/plugins", "/skill"],
|
|
2677
3636
|
verbose: config.verbose !== void 0 ? config.verbose : false
|
|
2678
3637
|
};
|
|
2679
3638
|
}
|
|
@@ -2690,6 +3649,7 @@ class CliInterceptor extends EventEmitter {
|
|
|
2690
3649
|
this.emit("intercept:started", { input: userInput });
|
|
2691
3650
|
const bypassCheck = this.shouldBypass(userInput);
|
|
2692
3651
|
if (bypassCheck.bypass) {
|
|
3652
|
+
await this.handleBypassedCommand(userInput, bypassCheck.reason);
|
|
2693
3653
|
this.emit("intercept:bypassed", { input: userInput, reason: bypassCheck.reason });
|
|
2694
3654
|
return {
|
|
2695
3655
|
intercepted: false,
|
|
@@ -2720,6 +3680,12 @@ class CliInterceptor extends EventEmitter {
|
|
|
2720
3680
|
*/
|
|
2721
3681
|
shouldBypass(input) {
|
|
2722
3682
|
const normalized = input.trim().toLowerCase();
|
|
3683
|
+
if (normalized.startsWith("/")) {
|
|
3684
|
+
const isCcjkOwned = this.config.ccjkOwnedSlashPrefixes.some((prefix) => normalized.startsWith(prefix));
|
|
3685
|
+
if (!isCcjkOwned) {
|
|
3686
|
+
return { bypass: true, reason: "Native slash command passthrough" };
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
2723
3689
|
if (this.systemCommands.some((cmd) => normalized.startsWith(cmd))) {
|
|
2724
3690
|
return { bypass: true, reason: "System command" };
|
|
2725
3691
|
}
|
|
@@ -2742,6 +3708,31 @@ class CliInterceptor extends EventEmitter {
|
|
|
2742
3708
|
console.log(" System will automatically handle: skills, agents, MCP tools\n");
|
|
2743
3709
|
console.log(" Smart mode: ambiguity checks + capability-ranked tool selection + telemetry\n");
|
|
2744
3710
|
}
|
|
3711
|
+
async handleBypassedCommand(input, reason) {
|
|
3712
|
+
const normalized = input.trim().toLowerCase();
|
|
3713
|
+
const command = this.extractCommandName(normalized);
|
|
3714
|
+
if (normalized.startsWith("/clear")) {
|
|
3715
|
+
this.autoExecutor.clearTelemetry();
|
|
3716
|
+
contextLoader.clearCache();
|
|
3717
|
+
getSkillRegistry().clear();
|
|
3718
|
+
await emitCommandHookEvent("command-clear", {
|
|
3719
|
+
command,
|
|
3720
|
+
cleared: ["telemetry", "context-cache", "skill-registry"]
|
|
3721
|
+
});
|
|
3722
|
+
}
|
|
3723
|
+
if (normalized.startsWith("/")) {
|
|
3724
|
+
await emitCommandHookEvent("command-bypass", {
|
|
3725
|
+
command,
|
|
3726
|
+
reason
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
extractCommandName(input) {
|
|
3731
|
+
if (!input.startsWith("/")) {
|
|
3732
|
+
return "";
|
|
3733
|
+
}
|
|
3734
|
+
return input.split(/\s+/)[0];
|
|
3735
|
+
}
|
|
2745
3736
|
/**
|
|
2746
3737
|
* Enable interceptor
|
|
2747
3738
|
*/
|
package/dist/chunks/package.mjs
CHANGED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { l as logger } from './ccjk.DG_o24cZ.mjs';
|
|
2
|
+
|
|
3
|
+
class HookRegistry {
|
|
4
|
+
hooks = /* @__PURE__ */ new Map();
|
|
5
|
+
/**
|
|
6
|
+
* Register a hook for an event
|
|
7
|
+
*/
|
|
8
|
+
register(event, hook) {
|
|
9
|
+
const hooks = this.hooks.get(event) || [];
|
|
10
|
+
hooks.push(hook);
|
|
11
|
+
hooks.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
12
|
+
this.hooks.set(event, hooks);
|
|
13
|
+
logger.debug(`Registered hook "${hook.name}" for event "${event}"`);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Unregister a hook
|
|
17
|
+
*/
|
|
18
|
+
unregister(event, hookName) {
|
|
19
|
+
const hooks = this.hooks.get(event);
|
|
20
|
+
if (!hooks) return;
|
|
21
|
+
const filtered = hooks.filter((h) => h.name !== hookName);
|
|
22
|
+
this.hooks.set(event, filtered);
|
|
23
|
+
logger.debug(`Unregistered hook "${hookName}" for event "${event}"`);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Execute all hooks for an event
|
|
27
|
+
* Returns false if any hook returns continue: false
|
|
28
|
+
*/
|
|
29
|
+
async execute(context) {
|
|
30
|
+
const hooks = this.hooks.get(context.event) || [];
|
|
31
|
+
const enabledHooks = hooks.filter((h) => h.enabled !== false);
|
|
32
|
+
if (enabledHooks.length === 0) {
|
|
33
|
+
return { continue: true };
|
|
34
|
+
}
|
|
35
|
+
let currentContext = context;
|
|
36
|
+
let aggregatedData = {};
|
|
37
|
+
for (const hook of enabledHooks) {
|
|
38
|
+
try {
|
|
39
|
+
const result = await hook.fn(currentContext);
|
|
40
|
+
if (result.data) {
|
|
41
|
+
aggregatedData = { ...aggregatedData, ...result.data };
|
|
42
|
+
}
|
|
43
|
+
if (!result.continue) {
|
|
44
|
+
return {
|
|
45
|
+
continue: false,
|
|
46
|
+
data: aggregatedData,
|
|
47
|
+
error: result.error
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
currentContext = {
|
|
51
|
+
...currentContext,
|
|
52
|
+
data: { ...currentContext.data, ...aggregatedData }
|
|
53
|
+
};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
logger.error(`Hook "${hook.name}" failed:`, error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
continue: true,
|
|
60
|
+
data: aggregatedData
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get all registered hooks for an event
|
|
65
|
+
*/
|
|
66
|
+
getHooks(event) {
|
|
67
|
+
return this.hooks.get(event) || [];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Clear all hooks
|
|
71
|
+
*/
|
|
72
|
+
clear() {
|
|
73
|
+
this.hooks.clear();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const hookRegistry = new HookRegistry();
|
|
77
|
+
|
|
78
|
+
export { hookRegistry as h };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccjk",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "12.2.
|
|
4
|
+
"version": "12.2.2",
|
|
5
5
|
"packageManager": "pnpm@10.17.1",
|
|
6
6
|
"description": "CLI toolkit for Claude Code and Codex setup. Simplifies MCP service installation, API configuration, workflow management, and multi-provider support with guided interactive setup.",
|
|
7
7
|
"author": {
|