chainlesschain 0.37.10 → 0.37.12

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.
Files changed (39) hide show
  1. package/README.md +166 -10
  2. package/package.json +1 -1
  3. package/src/commands/a2a.js +374 -0
  4. package/src/commands/bi.js +240 -0
  5. package/src/commands/cowork.js +317 -0
  6. package/src/commands/economy.js +375 -0
  7. package/src/commands/evolution.js +398 -0
  8. package/src/commands/hmemory.js +273 -0
  9. package/src/commands/hook.js +260 -0
  10. package/src/commands/init.js +184 -0
  11. package/src/commands/lowcode.js +320 -0
  12. package/src/commands/plugin.js +55 -2
  13. package/src/commands/sandbox.js +366 -0
  14. package/src/commands/skill.js +254 -201
  15. package/src/commands/workflow.js +359 -0
  16. package/src/commands/zkp.js +277 -0
  17. package/src/index.js +44 -0
  18. package/src/lib/a2a-protocol.js +371 -0
  19. package/src/lib/agent-coordinator.js +273 -0
  20. package/src/lib/agent-economy.js +369 -0
  21. package/src/lib/app-builder.js +377 -0
  22. package/src/lib/bi-engine.js +299 -0
  23. package/src/lib/cowork/ab-comparator-cli.js +180 -0
  24. package/src/lib/cowork/code-knowledge-graph-cli.js +232 -0
  25. package/src/lib/cowork/debate-review-cli.js +144 -0
  26. package/src/lib/cowork/decision-kb-cli.js +153 -0
  27. package/src/lib/cowork/project-style-analyzer-cli.js +168 -0
  28. package/src/lib/cowork-adapter.js +106 -0
  29. package/src/lib/evolution-system.js +508 -0
  30. package/src/lib/hierarchical-memory.js +471 -0
  31. package/src/lib/hook-manager.js +387 -0
  32. package/src/lib/plugin-manager.js +118 -0
  33. package/src/lib/project-detector.js +53 -0
  34. package/src/lib/sandbox-v2.js +503 -0
  35. package/src/lib/service-container.js +183 -0
  36. package/src/lib/skill-loader.js +274 -0
  37. package/src/lib/workflow-engine.js +503 -0
  38. package/src/lib/zkp-engine.js +241 -0
  39. package/src/repl/agent-repl.js +117 -112
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Hook Manager — Lifecycle hook registration, execution, and statistics for CLI.
3
+ * Manages hooks that trigger on system events (IPC, tools, sessions, git, etc.).
4
+ */
5
+
6
+ import crypto from "crypto";
7
+
8
+ /**
9
+ * Hook priority levels — lower values run first.
10
+ */
11
+ export const HookPriority = {
12
+ SYSTEM: 0,
13
+ HIGH: 100,
14
+ NORMAL: 500,
15
+ LOW: 900,
16
+ MONITOR: 1000,
17
+ };
18
+
19
+ /**
20
+ * Hook execution types.
21
+ */
22
+ export const HookType = {
23
+ SYNC: "sync",
24
+ ASYNC: "async",
25
+ COMMAND: "command",
26
+ SCRIPT: "script",
27
+ };
28
+
29
+ /**
30
+ * All supported hook event names.
31
+ */
32
+ export const HookEvents = {
33
+ PreIPCCall: "PreIPCCall",
34
+ PostIPCCall: "PostIPCCall",
35
+ IPCError: "IPCError",
36
+ PreToolUse: "PreToolUse",
37
+ PostToolUse: "PostToolUse",
38
+ ToolError: "ToolError",
39
+ SessionStart: "SessionStart",
40
+ SessionEnd: "SessionEnd",
41
+ PreCompact: "PreCompact",
42
+ PostCompact: "PostCompact",
43
+ UserPromptSubmit: "UserPromptSubmit",
44
+ AssistantResponse: "AssistantResponse",
45
+ AgentStart: "AgentStart",
46
+ AgentStop: "AgentStop",
47
+ TaskAssigned: "TaskAssigned",
48
+ TaskCompleted: "TaskCompleted",
49
+ PreFileAccess: "PreFileAccess",
50
+ PostFileAccess: "PostFileAccess",
51
+ FileModified: "FileModified",
52
+ MemorySave: "MemorySave",
53
+ MemoryLoad: "MemoryLoad",
54
+ AuditLog: "AuditLog",
55
+ ComplianceCheck: "ComplianceCheck",
56
+ DataSubjectRequest: "DataSubjectRequest",
57
+ PreGitCommit: "PreGitCommit",
58
+ PostGitCommit: "PostGitCommit",
59
+ PreGitPush: "PreGitPush",
60
+ CIFailure: "CIFailure",
61
+ };
62
+
63
+ /**
64
+ * Ensure hooks table exists in the database.
65
+ */
66
+ export function ensureHookTables(db) {
67
+ db.exec(`
68
+ CREATE TABLE IF NOT EXISTS hooks (
69
+ id TEXT PRIMARY KEY,
70
+ event TEXT NOT NULL,
71
+ name TEXT NOT NULL,
72
+ type TEXT NOT NULL DEFAULT 'sync',
73
+ priority INTEGER NOT NULL DEFAULT 500,
74
+ handler TEXT,
75
+ matcher TEXT,
76
+ timeout INTEGER DEFAULT 5000,
77
+ enabled INTEGER DEFAULT 1,
78
+ description TEXT,
79
+ execution_count INTEGER DEFAULT 0,
80
+ error_count INTEGER DEFAULT 0,
81
+ total_execution_time REAL DEFAULT 0,
82
+ created_at TEXT DEFAULT (datetime('now')),
83
+ updated_at TEXT DEFAULT (datetime('now'))
84
+ )
85
+ `);
86
+ }
87
+
88
+ /**
89
+ * Register a new hook.
90
+ */
91
+ export function registerHook(db, hookConfig) {
92
+ ensureHookTables(db);
93
+
94
+ const {
95
+ event,
96
+ name,
97
+ type = HookType.SYNC,
98
+ priority = HookPriority.NORMAL,
99
+ handler,
100
+ matcher,
101
+ timeout = 5000,
102
+ enabled = true,
103
+ description,
104
+ } = hookConfig;
105
+
106
+ if (!event) {
107
+ throw new Error("Hook event is required");
108
+ }
109
+ if (!name) {
110
+ throw new Error("Hook name is required");
111
+ }
112
+
113
+ // Validate event name
114
+ if (!Object.values(HookEvents).includes(event)) {
115
+ throw new Error(
116
+ `Invalid hook event: ${event}. Use one of: ${Object.values(HookEvents).join(", ")}`,
117
+ );
118
+ }
119
+
120
+ // Validate type
121
+ if (!Object.values(HookType).includes(type)) {
122
+ throw new Error(
123
+ `Invalid hook type: ${type}. Use one of: ${Object.values(HookType).join(", ")}`,
124
+ );
125
+ }
126
+
127
+ const id = `hook-${crypto.randomBytes(8).toString("hex")}`;
128
+
129
+ db.prepare(
130
+ `INSERT INTO hooks (id, event, name, type, priority, handler, matcher, timeout, enabled, description)
131
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
132
+ ).run(
133
+ id,
134
+ event,
135
+ name,
136
+ type,
137
+ priority,
138
+ handler || null,
139
+ matcher || null,
140
+ timeout,
141
+ enabled ? 1 : 0,
142
+ description || null,
143
+ );
144
+
145
+ return {
146
+ id,
147
+ event,
148
+ name,
149
+ type,
150
+ priority,
151
+ handler,
152
+ matcher,
153
+ timeout,
154
+ enabled: !!enabled,
155
+ description,
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Unregister (remove) a hook by ID.
161
+ */
162
+ export function unregisterHook(db, hookId) {
163
+ ensureHookTables(db);
164
+ const result = db.prepare("DELETE FROM hooks WHERE id = ?").run(hookId);
165
+ return result.changes > 0;
166
+ }
167
+
168
+ /**
169
+ * List hooks with optional filters.
170
+ */
171
+ export function listHooks(db, options = {}) {
172
+ ensureHookTables(db);
173
+ const { event, enabledOnly = false } = options;
174
+
175
+ if (event && enabledOnly) {
176
+ return db
177
+ .prepare(
178
+ "SELECT * FROM hooks WHERE event = ? AND enabled = 1 ORDER BY priority ASC",
179
+ )
180
+ .all(event);
181
+ }
182
+ if (event) {
183
+ return db
184
+ .prepare("SELECT * FROM hooks WHERE event = ? ORDER BY priority ASC")
185
+ .all(event);
186
+ }
187
+ if (enabledOnly) {
188
+ return db
189
+ .prepare("SELECT * FROM hooks WHERE enabled = 1 ORDER BY priority ASC")
190
+ .all();
191
+ }
192
+ return db.prepare("SELECT * FROM hooks ORDER BY priority ASC").all();
193
+ }
194
+
195
+ /**
196
+ * Get a single hook by ID.
197
+ */
198
+ export function getHook(db, hookId) {
199
+ ensureHookTables(db);
200
+ return db.prepare("SELECT * FROM hooks WHERE id = ?").get(hookId);
201
+ }
202
+
203
+ /**
204
+ * Compile a matcher pattern into a test function.
205
+ * Supports:
206
+ * - null/undefined → matches everything
207
+ * - Pipe-separated patterns: "Edit|Write" matches "Edit" or "Write"
208
+ * - Wildcards: "*" matches any chars, "?" matches one char
209
+ * - Regex strings starting with "/": "/^Pre/" matches "PreIPCCall"
210
+ */
211
+ export function compileMatcher(pattern) {
212
+ if (!pattern) {
213
+ return () => true;
214
+ }
215
+
216
+ // Regex pattern (starts and ends with /)
217
+ if (pattern.startsWith("/") && pattern.lastIndexOf("/") > 0) {
218
+ const lastSlash = pattern.lastIndexOf("/");
219
+ const regexBody = pattern.slice(1, lastSlash);
220
+ const flags = pattern.slice(lastSlash + 1);
221
+ try {
222
+ const re = new RegExp(regexBody, flags);
223
+ return (value) => re.test(value);
224
+ } catch (_err) {
225
+ // Invalid regex — fall through to wildcard matching
226
+ }
227
+ }
228
+
229
+ // Pipe-separated patterns (e.g. "Edit|Write")
230
+ if (pattern.includes("|")) {
231
+ const parts = pattern.split("|").map((p) => p.trim());
232
+ const matchers = parts.map((p) => compileMatcher(p));
233
+ return (value) => matchers.some((m) => m(value));
234
+ }
235
+
236
+ // Wildcard pattern (* and ?)
237
+ const escaped = pattern
238
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
239
+ .replace(/\*/g, ".*")
240
+ .replace(/\?/g, ".");
241
+ const re = new RegExp(`^${escaped}$`);
242
+ return (value) => re.test(value);
243
+ }
244
+
245
+ /**
246
+ * Execute a single hook with context.
247
+ * Returns { success, result, error, executionTime }.
248
+ */
249
+ export async function executeHook(hook, context = {}) {
250
+ const start = Date.now();
251
+
252
+ try {
253
+ const type = hook.type || HookType.SYNC;
254
+
255
+ if (type === HookType.COMMAND || type === HookType.SCRIPT) {
256
+ // Command/script hooks execute a shell command
257
+ const { execSync } = await import("child_process");
258
+ const cmd = hook.handler || "";
259
+ if (!cmd) {
260
+ return {
261
+ success: false,
262
+ result: null,
263
+ error: "No handler command specified",
264
+ executionTime: 0,
265
+ };
266
+ }
267
+ const env = {
268
+ ...process.env,
269
+ HOOK_EVENT: hook.event,
270
+ HOOK_CONTEXT: JSON.stringify(context),
271
+ };
272
+ const output = execSync(cmd, {
273
+ encoding: "utf-8",
274
+ timeout: hook.timeout || 5000,
275
+ env,
276
+ });
277
+ const executionTime = Date.now() - start;
278
+ return {
279
+ success: true,
280
+ result: output.trim(),
281
+ error: null,
282
+ executionTime,
283
+ };
284
+ }
285
+
286
+ // For sync/async hooks with a handler function string
287
+ if (hook.handlerFn && typeof hook.handlerFn === "function") {
288
+ const result = await Promise.resolve(hook.handlerFn(context));
289
+ const executionTime = Date.now() - start;
290
+ return { success: true, result, error: null, executionTime };
291
+ }
292
+
293
+ // No executable handler
294
+ const executionTime = Date.now() - start;
295
+ return { success: true, result: null, error: null, executionTime };
296
+ } catch (err) {
297
+ const executionTime = Date.now() - start;
298
+ return { success: false, result: null, error: err.message, executionTime };
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Execute all hooks for a given event, in priority order.
304
+ * Returns array of { hookId, hookName, success, result, error, executionTime }.
305
+ */
306
+ export async function executeHooks(db, eventName, context = {}) {
307
+ const hooks = listHooks(db, { event: eventName, enabledOnly: true });
308
+ const results = [];
309
+
310
+ for (const hook of hooks) {
311
+ // Check matcher against context
312
+ if (hook.matcher) {
313
+ const matchFn = compileMatcher(hook.matcher);
314
+ const target =
315
+ context.target ||
316
+ context.channel ||
317
+ context.tool ||
318
+ context.file ||
319
+ eventName;
320
+ if (!matchFn(target)) {
321
+ continue;
322
+ }
323
+ }
324
+
325
+ const outcome = await executeHook(hook, context);
326
+ results.push({
327
+ hookId: hook.id,
328
+ hookName: hook.name,
329
+ ...outcome,
330
+ });
331
+
332
+ // Update stats
333
+ updateHookStats(db, hook.id, {
334
+ executionTime: outcome.executionTime,
335
+ success: outcome.success,
336
+ });
337
+ }
338
+
339
+ return results;
340
+ }
341
+
342
+ /**
343
+ * Get hook execution statistics.
344
+ */
345
+ export function getHookStats(db) {
346
+ ensureHookTables(db);
347
+ const hooks = db
348
+ .prepare(
349
+ "SELECT id, event, name, execution_count, error_count, total_execution_time FROM hooks ORDER BY execution_count DESC",
350
+ )
351
+ .all();
352
+
353
+ return hooks.map((h) => ({
354
+ id: h.id,
355
+ event: h.event,
356
+ name: h.name,
357
+ executionCount: h.execution_count || 0,
358
+ errorCount: h.error_count || 0,
359
+ avgExecutionTime:
360
+ h.execution_count > 0
361
+ ? Math.round((h.total_execution_time / h.execution_count) * 100) / 100
362
+ : 0,
363
+ totalExecutionTime: h.total_execution_time || 0,
364
+ }));
365
+ }
366
+
367
+ /**
368
+ * Update hook statistics after execution.
369
+ */
370
+ export function updateHookStats(
371
+ db,
372
+ hookId,
373
+ { executionTime = 0, success = true } = {},
374
+ ) {
375
+ ensureHookTables(db);
376
+
377
+ const hook = getHook(db, hookId);
378
+ if (!hook) return;
379
+
380
+ const newCount = (hook.execution_count || 0) + 1;
381
+ const newErrorCount = (hook.error_count || 0) + (success ? 0 : 1);
382
+ const newTotalTime = (hook.total_execution_time || 0) + executionTime;
383
+
384
+ db.prepare(
385
+ "UPDATE hooks SET execution_count = ?, error_count = ?, total_execution_time = ?, updated_at = datetime('now') WHERE id = ?",
386
+ ).run(newCount, newErrorCount, newTotalTime, hookId);
387
+ }
@@ -6,6 +6,7 @@
6
6
  import crypto from "crypto";
7
7
  import fs from "fs";
8
8
  import path from "path";
9
+ import { getElectronUserDataDir } from "./paths.js";
9
10
 
10
11
  /**
11
12
  * Ensure plugin tables exist.
@@ -36,6 +37,15 @@ export function ensurePluginTables(db) {
36
37
  value TEXT
37
38
  )
38
39
  `);
40
+ db.exec(`
41
+ CREATE TABLE IF NOT EXISTS plugin_skills (
42
+ id TEXT PRIMARY KEY,
43
+ plugin_name TEXT NOT NULL,
44
+ skill_name TEXT NOT NULL,
45
+ skill_path TEXT NOT NULL,
46
+ installed_at TEXT DEFAULT (datetime('now'))
47
+ )
48
+ `);
39
49
  db.exec(`
40
50
  CREATE TABLE IF NOT EXISTS plugin_registry (
41
51
  name TEXT PRIMARY KEY,
@@ -310,3 +320,111 @@ export function getPluginSummary(db) {
310
320
  registryCount: registry?.c || 0,
311
321
  };
312
322
  }
323
+
324
+ // ─── Plugin Skills ──────────────────────────────────────
325
+
326
+ /**
327
+ * Get the marketplace skills directory
328
+ */
329
+ function getMarketplaceSkillsDir() {
330
+ return path.join(getElectronUserDataDir(), "marketplace", "skills");
331
+ }
332
+
333
+ /**
334
+ * Install skills from a plugin manifest.
335
+ * Copies skill directories to the marketplace/skills/ layer.
336
+ *
337
+ * @param {object} db - Database instance
338
+ * @param {string} pluginName - Plugin name
339
+ * @param {string} pluginPath - Root path of the plugin package
340
+ * @param {{ name: string, path: string }[]} skills - Skills declared in manifest
341
+ * @returns {{ installed: string[] }} Names of installed skills
342
+ */
343
+ export function installPluginSkills(db, pluginName, pluginPath, skills) {
344
+ ensurePluginTables(db);
345
+ if (!skills || skills.length === 0) return { installed: [] };
346
+
347
+ const marketplaceDir = getMarketplaceSkillsDir();
348
+ const installed = [];
349
+
350
+ for (const skill of skills) {
351
+ const srcDir = path.resolve(pluginPath, skill.path);
352
+ if (!fs.existsSync(srcDir)) continue;
353
+
354
+ const destDir = path.join(marketplaceDir, skill.name);
355
+ fs.mkdirSync(destDir, { recursive: true });
356
+
357
+ // Copy skill files
358
+ _copyDirSync(srcDir, destDir);
359
+
360
+ // Record in DB
361
+ const id = `ps-${crypto.randomBytes(6).toString("hex")}`;
362
+ db.prepare(
363
+ `INSERT OR REPLACE INTO plugin_skills (id, plugin_name, skill_name, skill_path)
364
+ VALUES (?, ?, ?, ?)`,
365
+ ).run(id, pluginName, skill.name, destDir);
366
+
367
+ installed.push(skill.name);
368
+ }
369
+
370
+ return { installed };
371
+ }
372
+
373
+ /**
374
+ * Remove all skills installed by a plugin.
375
+ *
376
+ * @param {object} db - Database instance
377
+ * @param {string} pluginName - Plugin name
378
+ * @returns {{ removed: string[] }} Names of removed skills
379
+ */
380
+ export function removePluginSkills(db, pluginName) {
381
+ ensurePluginTables(db);
382
+ const rows = db
383
+ .prepare("SELECT * FROM plugin_skills WHERE plugin_name = ?")
384
+ .all(pluginName);
385
+
386
+ const removed = [];
387
+ for (const row of rows) {
388
+ // Remove the skill directory
389
+ if (fs.existsSync(row.skill_path)) {
390
+ fs.rmSync(row.skill_path, { recursive: true, force: true });
391
+ }
392
+ removed.push(row.skill_name);
393
+ }
394
+
395
+ db.prepare("DELETE FROM plugin_skills WHERE plugin_name = ?").run(pluginName);
396
+ return { removed };
397
+ }
398
+
399
+ /**
400
+ * List skills installed by a specific plugin.
401
+ *
402
+ * @param {object} db - Database instance
403
+ * @param {string} pluginName - Plugin name
404
+ * @returns {{ skill_name: string, skill_path: string }[]}
405
+ */
406
+ export function getPluginSkills(db, pluginName) {
407
+ ensurePluginTables(db);
408
+ return db
409
+ .prepare(
410
+ "SELECT skill_name, skill_path FROM plugin_skills WHERE plugin_name = ?",
411
+ )
412
+ .all(pluginName);
413
+ }
414
+
415
+ /**
416
+ * Recursively copy a directory
417
+ */
418
+ function _copyDirSync(src, dest) {
419
+ fs.mkdirSync(dest, { recursive: true });
420
+ const entries = fs.readdirSync(src, { withFileTypes: true });
421
+ for (const entry of entries) {
422
+ const srcPath = path.join(src, entry.name);
423
+ const destPath = path.join(dest, entry.name);
424
+ if (entry.isDirectory()) {
425
+ _copyDirSync(srcPath, destPath);
426
+ } else {
427
+ fs.copyFileSync(srcPath, destPath);
428
+ }
429
+ }
430
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Project root detection utility
3
+ * Finds and loads .chainlesschain/ project configuration
4
+ */
5
+
6
+ import fs from "fs";
7
+ import path from "path";
8
+
9
+ /**
10
+ * Walk up from startDir looking for .chainlesschain/config.json
11
+ * @param {string} [startDir=process.cwd()] - Directory to start searching from
12
+ * @returns {string|null} Project root directory or null
13
+ */
14
+ export function findProjectRoot(startDir) {
15
+ let dir = path.resolve(startDir || process.cwd());
16
+ const root = path.parse(dir).root;
17
+
18
+ while (dir !== root) {
19
+ const configPath = path.join(dir, ".chainlesschain", "config.json");
20
+ if (fs.existsSync(configPath)) {
21
+ return dir;
22
+ }
23
+ const parent = path.dirname(dir);
24
+ if (parent === dir) break;
25
+ dir = parent;
26
+ }
27
+
28
+ return null;
29
+ }
30
+
31
+ /**
32
+ * Load project configuration from .chainlesschain/config.json
33
+ * @param {string} projectRoot - Project root directory
34
+ * @returns {object|null} Parsed config or null on error
35
+ */
36
+ export function loadProjectConfig(projectRoot) {
37
+ const configPath = path.join(projectRoot, ".chainlesschain", "config.json");
38
+ try {
39
+ const content = fs.readFileSync(configPath, "utf-8");
40
+ return JSON.parse(content);
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Quick boolean check: are we inside a .chainlesschain project?
48
+ * @param {string} [startDir] - Directory to check from
49
+ * @returns {boolean}
50
+ */
51
+ export function isInsideProject(startDir) {
52
+ return findProjectRoot(startDir) !== null;
53
+ }