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
@@ -1,192 +1,89 @@
1
1
  /**
2
2
  * Skill management and execution commands
3
- * chainlesschain skill list|info|run|search|categories
3
+ * chainlesschain skill list|info|run|search|categories|add|remove|sources
4
4
  *
5
- * Loads built-in skills directly from the desktop app's bundled skill definitions.
6
- * Most skills (110+/138) are pure JS and run headless without Electron.
5
+ * Uses multi-layer skill loader:
6
+ * bundled < marketplace < managed (global) < workspace (project)
7
7
  */
8
8
 
9
9
  import chalk from "chalk";
10
10
  import ora from "ora";
11
11
  import fs from "fs";
12
12
  import path from "path";
13
- import { fileURLToPath } from "url";
14
13
  import { logger } from "../lib/logger.js";
14
+ import { CLISkillLoader, LAYER_NAMES } from "../lib/skill-loader.js";
15
+ import { getElectronUserDataDir } from "../lib/paths.js";
16
+ import { findProjectRoot } from "../lib/project-detector.js";
15
17
 
16
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+ const LAYER_LABELS = {
19
+ bundled: chalk.blue("[bundled]"),
20
+ marketplace: chalk.magenta("[marketplace]"),
21
+ managed: chalk.yellow("[global]"),
22
+ workspace: chalk.green("[project]"),
23
+ };
17
24
 
18
25
  /**
19
- * Find the bundled skills directory
26
+ * Check if a skill can run on current platform
20
27
  */
21
- function findSkillsDir() {
22
- // Walk up from CLI package to find desktop-app-vue
23
- const candidates = [
24
- path.resolve(
25
- __dirname,
26
- "../../../../desktop-app-vue/src/main/ai-engine/cowork/skills/builtin",
27
- ),
28
- path.resolve(
29
- process.cwd(),
30
- "desktop-app-vue/src/main/ai-engine/cowork/skills/builtin",
31
- ),
32
- ];
33
-
34
- for (const dir of candidates) {
35
- if (fs.existsSync(dir)) return dir;
36
- }
37
-
38
- return null;
28
+ function canRunOnPlatform(skill) {
29
+ if (!skill.os || skill.os.length === 0) return true;
30
+ return skill.os.includes(process.platform);
39
31
  }
40
32
 
41
- /**
42
- * Simple YAML frontmatter parser (no dependencies)
43
- */
44
- function parseSkillMd(content) {
45
- const lines = content.split("\n");
46
- if (lines[0].trim() !== "---") return { data: {}, body: content };
47
-
48
- let endIndex = -1;
49
- for (let i = 1; i < lines.length; i++) {
50
- if (lines[i].trim() === "---") {
51
- endIndex = i;
52
- break;
53
- }
54
- }
55
-
56
- if (endIndex === -1) return { data: {}, body: content };
57
-
58
- const yamlLines = lines.slice(1, endIndex);
59
- const body = lines
60
- .slice(endIndex + 1)
61
- .join("\n")
62
- .trim();
63
- const data = {};
64
-
65
- let currentKey = null;
66
- let currentArray = null;
67
-
68
- for (const line of yamlLines) {
69
- if (!line.trim() || line.trim().startsWith("#")) continue;
70
-
71
- const trimmed = line.trim();
72
-
73
- if (trimmed.startsWith("- ")) {
74
- const value = trimmed
75
- .slice(2)
76
- .trim()
77
- .replace(/^['"]|['"]$/g, "");
78
- if (currentArray) currentArray.push(value);
79
- continue;
80
- }
81
-
82
- const colonIndex = trimmed.indexOf(":");
83
- if (colonIndex > 0) {
84
- const key = trimmed.slice(0, colonIndex).trim();
85
- let value = trimmed.slice(colonIndex + 1).trim();
86
-
87
- // Convert kebab-case to camelCase
88
- const camelKey = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
89
-
90
- if (value === "") {
91
- // Could be start of nested object or array
92
- currentKey = camelKey;
93
- currentArray = null;
94
- continue;
95
- }
33
+ const SKILL_TEMPLATE_MD = (name) => `---
34
+ name: ${name}
35
+ display-name: ${name}
36
+ description: Custom skill — edit this description
37
+ version: 1.0.0
38
+ category: custom
39
+ tags: [custom]
40
+ user-invocable: true
41
+ handler: handler.js
42
+ ---
96
43
 
97
- // Handle inline arrays [a, b, c]
98
- if (value.startsWith("[") && value.endsWith("]")) {
99
- data[camelKey] = value
100
- .slice(1, -1)
101
- .split(",")
102
- .map((v) => v.trim().replace(/^['"]|['"]$/g, ""))
103
- .filter(Boolean);
104
- currentArray = null;
105
- currentKey = null;
106
- continue;
107
- }
44
+ # ${name}
108
45
 
109
- // Handle booleans and numbers
110
- if (value === "true") value = true;
111
- else if (value === "false") value = false;
112
- else if (value === "null") value = null;
113
- else if (/^\d+(\.\d+)?$/.test(value)) value = parseFloat(value);
114
- else value = value.replace(/^['"]|['"]$/g, "");
46
+ Describe what this skill does and how to use it.
115
47
 
116
- data[camelKey] = value;
48
+ ## Usage
117
49
 
118
- // If next lines might be array items for this key
119
- if (Array.isArray(data[camelKey])) {
120
- currentArray = data[camelKey];
121
- } else {
122
- currentArray = null;
123
- }
124
- currentKey = camelKey;
125
- }
126
- }
127
-
128
- return { data, body };
129
- }
50
+ \`\`\`
51
+ chainlesschain skill run ${name} "your input"
52
+ \`\`\`
53
+ `;
130
54
 
131
- /**
132
- * Load all skill metadata from the bundled directory
55
+ const SKILL_TEMPLATE_HANDLER = (name) => `/**
56
+ * Handler for ${name} skill
133
57
  */
134
- function loadSkillMetadata(skillsDir) {
135
- const skills = [];
136
-
137
- try {
138
- const dirs = fs.readdirSync(skillsDir, { withFileTypes: true });
139
-
140
- for (const dir of dirs) {
141
- if (!dir.isDirectory()) continue;
142
58
 
143
- const skillMd = path.join(skillsDir, dir.name, "SKILL.md");
144
- if (!fs.existsSync(skillMd)) continue;
59
+ const handler = {
60
+ async init(skill) {
61
+ // Optional initialization
62
+ },
145
63
 
146
- try {
147
- const content = fs.readFileSync(skillMd, "utf8");
148
- const { data, body } = parseSkillMd(content);
149
-
150
- skills.push({
151
- id: data.name || dir.name,
152
- displayName: data.displayName || dir.name,
153
- description: data.description || "",
154
- version: data.version || "1.0.0",
155
- category: data.category || "uncategorized",
156
- tags: data.tags || [],
157
- userInvocable: data.userInvocable !== false,
158
- handler: data.handler || null,
159
- capabilities: data.capabilities || [],
160
- os: data.os || [],
161
- dirName: dir.name,
162
- hasHandler: fs.existsSync(
163
- path.join(skillsDir, dir.name, "handler.js"),
164
- ),
165
- body,
166
- });
167
- } catch {
168
- // Skip malformed skill files
169
- }
170
- }
171
- } catch (err) {
172
- logger.error(`Failed to read skills directory: ${err.message}`);
173
- }
64
+ async execute(task, context, skill) {
65
+ const input = task.input || task.params?.input || "";
174
66
 
175
- return skills;
176
- }
67
+ // TODO: Implement skill logic
68
+ return {
69
+ success: true,
70
+ message: \`${name} executed successfully\`,
71
+ result: { input },
72
+ };
73
+ },
74
+ };
177
75
 
178
- /**
179
- * Check if a skill can run on current platform
180
- */
181
- function canRunOnPlatform(skill) {
182
- if (!skill.os || skill.os.length === 0) return true;
183
- return skill.os.includes(process.platform);
184
- }
76
+ export default handler;
77
+ `;
185
78
 
186
79
  export function registerSkillCommand(program) {
187
80
  const skill = program
188
81
  .command("skill")
189
- .description("Manage and run built-in AI skills (138 available)");
82
+ .description(
83
+ "Manage and run AI skills (multi-layer: bundled + global + project)",
84
+ );
85
+
86
+ const loader = new CLISkillLoader();
190
87
 
191
88
  // skill list
192
89
  skill
@@ -195,20 +92,23 @@ export function registerSkillCommand(program) {
195
92
  .option("--category <category>", "Filter by category")
196
93
  .option("--tag <tag>", "Filter by tag")
197
94
  .option("--runnable", "Only show skills that can run headless")
95
+ .option(
96
+ "--source <layer>",
97
+ "Filter by source layer (bundled, marketplace, managed, workspace)",
98
+ )
198
99
  .option("--json", "Output as JSON")
199
100
  .action(async (options) => {
200
- const skillsDir = findSkillsDir();
201
- if (!skillsDir) {
101
+ const spinner = ora("Loading skills...").start();
102
+ let skills = loader.loadAll();
103
+ spinner.stop();
104
+
105
+ if (skills.length === 0) {
202
106
  logger.error(
203
- "Skills directory not found. Make sure you're in the ChainlessChain project root.",
107
+ "No skills found. Make sure you're in the ChainlessChain project root or have skills installed.",
204
108
  );
205
109
  process.exit(1);
206
110
  }
207
111
 
208
- const spinner = ora("Loading skills...").start();
209
- let skills = loadSkillMetadata(skillsDir);
210
- spinner.stop();
211
-
212
112
  // Filter
213
113
  if (options.category) {
214
114
  skills = skills.filter(
@@ -225,11 +125,14 @@ export function registerSkillCommand(program) {
225
125
  if (options.runnable) {
226
126
  skills = skills.filter((s) => s.hasHandler && canRunOnPlatform(s));
227
127
  }
128
+ if (options.source) {
129
+ skills = skills.filter((s) => s.source === options.source);
130
+ }
228
131
 
229
132
  if (options.json) {
230
133
  console.log(
231
134
  JSON.stringify(
232
- skills.map(({ body, ...rest }) => rest),
135
+ skills.map(({ body, skillDir, ...rest }) => rest),
233
136
  null,
234
137
  2,
235
138
  ),
@@ -252,8 +155,9 @@ export function registerSkillCommand(program) {
252
155
  for (const s of catSkills) {
253
156
  const handler = s.hasHandler ? chalk.green("●") : chalk.gray("○");
254
157
  const name = chalk.cyan(s.id.padEnd(30));
255
- const desc = chalk.gray((s.description || "").substring(0, 50));
256
- logger.log(` ${handler} ${name} ${desc}`);
158
+ const desc = chalk.gray((s.description || "").substring(0, 40));
159
+ const label = LAYER_LABELS[s.source] || chalk.gray(`[${s.source}]`);
160
+ logger.log(` ${handler} ${name} ${desc} ${label}`);
257
161
  }
258
162
  logger.log("");
259
163
  }
@@ -268,13 +172,12 @@ export function registerSkillCommand(program) {
268
172
  .command("categories")
269
173
  .description("List skill categories")
270
174
  .action(async () => {
271
- const skillsDir = findSkillsDir();
272
- if (!skillsDir) {
273
- logger.error("Skills directory not found.");
175
+ const skills = loader.loadAll();
176
+ if (skills.length === 0) {
177
+ logger.error("No skills found.");
274
178
  process.exit(1);
275
179
  }
276
180
 
277
- const skills = loadSkillMetadata(skillsDir);
278
181
  const cats = {};
279
182
  for (const s of skills) {
280
183
  const cat = s.category || "uncategorized";
@@ -295,13 +198,7 @@ export function registerSkillCommand(program) {
295
198
  .argument("<name>", "Skill name")
296
199
  .option("--json", "Output as JSON")
297
200
  .action(async (name, options) => {
298
- const skillsDir = findSkillsDir();
299
- if (!skillsDir) {
300
- logger.error("Skills directory not found.");
301
- process.exit(1);
302
- }
303
-
304
- const skills = loadSkillMetadata(skillsDir);
201
+ const skills = loader.loadAll();
305
202
  const s = skills.find(
306
203
  (s) => s.id === name || s.dirName === name || s.id.includes(name),
307
204
  );
@@ -318,7 +215,7 @@ export function registerSkillCommand(program) {
318
215
  }
319
216
 
320
217
  if (options.json) {
321
- const { body, ...rest } = s;
218
+ const { body, skillDir, ...rest } = s;
322
219
  console.log(JSON.stringify(rest, null, 2));
323
220
  return;
324
221
  }
@@ -326,6 +223,7 @@ export function registerSkillCommand(program) {
326
223
  logger.log(chalk.bold(`\n${s.displayName}`));
327
224
  logger.log(chalk.gray(`ID: ${s.id} v${s.version}`));
328
225
  logger.log(chalk.gray(`Category: ${s.category}`));
226
+ logger.log(chalk.gray(`Source: ${s.source}`));
329
227
  if (s.tags.length > 0) {
330
228
  logger.log(chalk.gray(`Tags: ${s.tags.join(", ")}`));
331
229
  }
@@ -351,13 +249,7 @@ export function registerSkillCommand(program) {
351
249
  .description("Search skills by keyword")
352
250
  .argument("<query>", "Search query")
353
251
  .action(async (query) => {
354
- const skillsDir = findSkillsDir();
355
- if (!skillsDir) {
356
- logger.error("Skills directory not found.");
357
- process.exit(1);
358
- }
359
-
360
- const skills = loadSkillMetadata(skillsDir);
252
+ const skills = loader.loadAll();
361
253
  const q = query.toLowerCase();
362
254
  const matches = skills.filter(
363
255
  (s) =>
@@ -377,8 +269,9 @@ export function registerSkillCommand(program) {
377
269
  );
378
270
  for (const s of matches) {
379
271
  const handler = s.hasHandler ? chalk.green("●") : chalk.gray("○");
272
+ const label = LAYER_LABELS[s.source] || "";
380
273
  logger.log(
381
- ` ${handler} ${chalk.cyan(s.id.padEnd(30))} ${chalk.gray(s.description.substring(0, 50))}`,
274
+ ` ${handler} ${chalk.cyan(s.id.padEnd(30))} ${chalk.gray(s.description.substring(0, 40))} ${label}`,
382
275
  );
383
276
  }
384
277
  logger.log("");
@@ -392,13 +285,7 @@ export function registerSkillCommand(program) {
392
285
  .argument("[input...]", "Input for the skill")
393
286
  .option("--json", "Output as JSON")
394
287
  .action(async (name, inputParts, options) => {
395
- const skillsDir = findSkillsDir();
396
- if (!skillsDir) {
397
- logger.error("Skills directory not found.");
398
- process.exit(1);
399
- }
400
-
401
- const skills = loadSkillMetadata(skillsDir);
288
+ const skills = loader.loadAll();
402
289
  const s = skills.find((sk) => sk.id === name || sk.dirName === name);
403
290
 
404
291
  if (!s) {
@@ -422,19 +309,16 @@ export function registerSkillCommand(program) {
422
309
  const input = inputParts.join(" ");
423
310
 
424
311
  try {
425
- // Load the handler
426
- const handlerPath = path.join(skillsDir, s.dirName, "handler.js");
312
+ const handlerPath = path.join(s.skillDir, "handler.js");
427
313
  const imported = await import(
428
314
  `file://${handlerPath.replace(/\\/g, "/")}`
429
315
  );
430
316
  const handler = imported.default || imported;
431
317
 
432
- // Initialize if needed
433
318
  if (handler.init) {
434
319
  await handler.init(s);
435
320
  }
436
321
 
437
- // Execute
438
322
  const task = {
439
323
  params: { input },
440
324
  input,
@@ -454,7 +338,6 @@ export function registerSkillCommand(program) {
454
338
  } else if (result.success) {
455
339
  logger.success(result.message || "Done");
456
340
  if (result.result && typeof result.result === "object") {
457
- // Pretty print result
458
341
  for (const [key, val] of Object.entries(result.result)) {
459
342
  if (typeof val === "string" && val.length > 200) {
460
343
  logger.log(` ${chalk.cyan(key)}: ${val.substring(0, 200)}...`);
@@ -476,4 +359,174 @@ export function registerSkillCommand(program) {
476
359
  process.exit(1);
477
360
  }
478
361
  });
362
+
363
+ // skill add — create a custom skill scaffold
364
+ skill
365
+ .command("add")
366
+ .description("Create a custom skill")
367
+ .argument("<name>", "Skill name")
368
+ .option(
369
+ "--global",
370
+ "Create as a global (managed) skill instead of project-level",
371
+ )
372
+ .action(async (name, options) => {
373
+ let targetDir;
374
+
375
+ if (options.global) {
376
+ const userData = getElectronUserDataDir();
377
+ targetDir = path.join(userData, "skills", name);
378
+ } else {
379
+ const projectRoot = findProjectRoot();
380
+ if (!projectRoot) {
381
+ logger.error(
382
+ 'Not inside a ChainlessChain project. Run "chainlesschain init" first, or use --global.',
383
+ );
384
+ process.exit(1);
385
+ }
386
+ targetDir = path.join(projectRoot, ".chainlesschain", "skills", name);
387
+ }
388
+
389
+ if (fs.existsSync(targetDir)) {
390
+ logger.error(`Skill already exists: ${targetDir}`);
391
+ process.exit(1);
392
+ }
393
+
394
+ try {
395
+ fs.mkdirSync(targetDir, { recursive: true });
396
+ fs.writeFileSync(
397
+ path.join(targetDir, "SKILL.md"),
398
+ SKILL_TEMPLATE_MD(name),
399
+ "utf-8",
400
+ );
401
+ fs.writeFileSync(
402
+ path.join(targetDir, "handler.js"),
403
+ SKILL_TEMPLATE_HANDLER(name),
404
+ "utf-8",
405
+ );
406
+
407
+ const scope = options.global ? "global" : "project";
408
+ logger.success(`Created ${scope} skill: ${chalk.cyan(name)}`);
409
+ logger.log(` ${chalk.gray(targetDir)}`);
410
+ logger.log("");
411
+ logger.log(chalk.bold("Files created:"));
412
+ logger.log(
413
+ ` ${chalk.gray("SKILL.md")} — Skill metadata and documentation`,
414
+ );
415
+ logger.log(` ${chalk.gray("handler.js")} — Skill execution logic`);
416
+ logger.log("");
417
+ logger.log(
418
+ `Edit these files, then run: ${chalk.cyan(`chainlesschain skill run ${name} "test input"`)}`,
419
+ );
420
+ } catch (err) {
421
+ logger.error(`Failed to create skill: ${err.message}`);
422
+ process.exit(1);
423
+ }
424
+ });
425
+
426
+ // skill remove — delete a custom skill
427
+ skill
428
+ .command("remove")
429
+ .description("Remove a custom skill (project or global)")
430
+ .argument("<name>", "Skill name")
431
+ .option("--global", "Remove from global (managed) skills")
432
+ .option("--force", "Skip confirmation")
433
+ .action(async (name, options) => {
434
+ let targetDir;
435
+
436
+ if (options.global) {
437
+ const userData = getElectronUserDataDir();
438
+ targetDir = path.join(userData, "skills", name);
439
+ } else {
440
+ const projectRoot = findProjectRoot();
441
+ if (!projectRoot) {
442
+ logger.error("Not inside a ChainlessChain project.");
443
+ process.exit(1);
444
+ }
445
+ targetDir = path.join(projectRoot, ".chainlesschain", "skills", name);
446
+ }
447
+
448
+ if (!fs.existsSync(targetDir)) {
449
+ logger.error(`Skill not found: ${name}`);
450
+ process.exit(1);
451
+ }
452
+
453
+ if (!options.force) {
454
+ try {
455
+ const { confirm } = await import("@inquirer/prompts");
456
+ const ok = await confirm({
457
+ message: `Remove skill "${name}" from ${targetDir}?`,
458
+ });
459
+ if (!ok) {
460
+ logger.info("Cancelled");
461
+ return;
462
+ }
463
+ } catch {
464
+ return;
465
+ }
466
+ }
467
+
468
+ try {
469
+ fs.rmSync(targetDir, { recursive: true, force: true });
470
+ logger.success(`Removed skill: ${name}`);
471
+ } catch (err) {
472
+ logger.error(`Failed to remove skill: ${err.message}`);
473
+ process.exit(1);
474
+ }
475
+ });
476
+
477
+ // skill sources — show layer paths and counts
478
+ skill
479
+ .command("sources")
480
+ .description("Show skill source layers, paths, and counts")
481
+ .option("--json", "Output as JSON")
482
+ .action(async (options) => {
483
+ const layers = loader.getLayerPaths();
484
+
485
+ // Count skills per layer
486
+ const layerCounts = {};
487
+ const allSkills = loader.loadAll();
488
+ for (const s of allSkills) {
489
+ layerCounts[s.source] = (layerCounts[s.source] || 0) + 1;
490
+ }
491
+
492
+ if (options.json) {
493
+ console.log(
494
+ JSON.stringify(
495
+ layers.map((l) => ({
496
+ ...l,
497
+ count: layerCounts[l.layer] || 0,
498
+ })),
499
+ null,
500
+ 2,
501
+ ),
502
+ );
503
+ return;
504
+ }
505
+
506
+ logger.log(chalk.bold("\nSkill Source Layers:\n"));
507
+ logger.log(
508
+ chalk.gray(
509
+ " Priority: workspace (highest) > managed > marketplace > bundled (lowest)\n",
510
+ ),
511
+ );
512
+
513
+ for (let i = layers.length - 1; i >= 0; i--) {
514
+ const l = layers[i];
515
+ const count = layerCounts[l.layer] || 0;
516
+ const status = l.exists
517
+ ? chalk.green("active")
518
+ : chalk.gray("not found");
519
+ const label = LAYER_LABELS[l.layer] || l.layer;
520
+ const priority = `(priority ${LAYER_NAMES.indexOf(l.layer)})`;
521
+
522
+ logger.log(` ${label} ${chalk.gray(priority)}`);
523
+ logger.log(` Path: ${chalk.gray(l.path || "(none)")}`);
524
+ logger.log(` Status: ${status} Skills: ${count}`);
525
+ logger.log("");
526
+ }
527
+
528
+ logger.log(
529
+ ` ${chalk.bold("Total:")} ${allSkills.length} skills resolved\n`,
530
+ );
531
+ });
479
532
  }