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.
- package/README.md +166 -10
- package/package.json +1 -1
- package/src/commands/a2a.js +374 -0
- package/src/commands/bi.js +240 -0
- package/src/commands/cowork.js +317 -0
- package/src/commands/economy.js +375 -0
- package/src/commands/evolution.js +398 -0
- package/src/commands/hmemory.js +273 -0
- package/src/commands/hook.js +260 -0
- package/src/commands/init.js +184 -0
- package/src/commands/lowcode.js +320 -0
- package/src/commands/plugin.js +55 -2
- package/src/commands/sandbox.js +366 -0
- package/src/commands/skill.js +254 -201
- package/src/commands/workflow.js +359 -0
- package/src/commands/zkp.js +277 -0
- package/src/index.js +44 -0
- package/src/lib/a2a-protocol.js +371 -0
- package/src/lib/agent-coordinator.js +273 -0
- package/src/lib/agent-economy.js +369 -0
- package/src/lib/app-builder.js +377 -0
- package/src/lib/bi-engine.js +299 -0
- package/src/lib/cowork/ab-comparator-cli.js +180 -0
- package/src/lib/cowork/code-knowledge-graph-cli.js +232 -0
- package/src/lib/cowork/debate-review-cli.js +144 -0
- package/src/lib/cowork/decision-kb-cli.js +153 -0
- package/src/lib/cowork/project-style-analyzer-cli.js +168 -0
- package/src/lib/cowork-adapter.js +106 -0
- package/src/lib/evolution-system.js +508 -0
- package/src/lib/hierarchical-memory.js +471 -0
- package/src/lib/hook-manager.js +387 -0
- package/src/lib/plugin-manager.js +118 -0
- package/src/lib/project-detector.js +53 -0
- package/src/lib/sandbox-v2.js +503 -0
- package/src/lib/service-container.js +183 -0
- package/src/lib/skill-loader.js +274 -0
- package/src/lib/workflow-engine.js +503 -0
- package/src/lib/zkp-engine.js +241 -0
- 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
|
+
}
|