chainlesschain 0.37.8 → 0.37.9

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.
@@ -0,0 +1,479 @@
1
+ /**
2
+ * Skill management and execution commands
3
+ * chainlesschain skill list|info|run|search|categories
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.
7
+ */
8
+
9
+ import chalk from "chalk";
10
+ import ora from "ora";
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+ import { logger } from "../lib/logger.js";
15
+
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+
18
+ /**
19
+ * Find the bundled skills directory
20
+ */
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;
39
+ }
40
+
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
+ }
96
+
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
+ }
108
+
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, "");
115
+
116
+ data[camelKey] = value;
117
+
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
+ }
130
+
131
+ /**
132
+ * Load all skill metadata from the bundled directory
133
+ */
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
+
143
+ const skillMd = path.join(skillsDir, dir.name, "SKILL.md");
144
+ if (!fs.existsSync(skillMd)) continue;
145
+
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
+ }
174
+
175
+ return skills;
176
+ }
177
+
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
+ }
185
+
186
+ export function registerSkillCommand(program) {
187
+ const skill = program
188
+ .command("skill")
189
+ .description("Manage and run built-in AI skills (138 available)");
190
+
191
+ // skill list
192
+ skill
193
+ .command("list")
194
+ .description("List all available skills")
195
+ .option("--category <category>", "Filter by category")
196
+ .option("--tag <tag>", "Filter by tag")
197
+ .option("--runnable", "Only show skills that can run headless")
198
+ .option("--json", "Output as JSON")
199
+ .action(async (options) => {
200
+ const skillsDir = findSkillsDir();
201
+ if (!skillsDir) {
202
+ logger.error(
203
+ "Skills directory not found. Make sure you're in the ChainlessChain project root.",
204
+ );
205
+ process.exit(1);
206
+ }
207
+
208
+ const spinner = ora("Loading skills...").start();
209
+ let skills = loadSkillMetadata(skillsDir);
210
+ spinner.stop();
211
+
212
+ // Filter
213
+ if (options.category) {
214
+ skills = skills.filter(
215
+ (s) => s.category.toLowerCase() === options.category.toLowerCase(),
216
+ );
217
+ }
218
+ if (options.tag) {
219
+ skills = skills.filter((s) =>
220
+ s.tags.some((t) =>
221
+ t.toLowerCase().includes(options.tag.toLowerCase()),
222
+ ),
223
+ );
224
+ }
225
+ if (options.runnable) {
226
+ skills = skills.filter((s) => s.hasHandler && canRunOnPlatform(s));
227
+ }
228
+
229
+ if (options.json) {
230
+ console.log(
231
+ JSON.stringify(
232
+ skills.map(({ body, ...rest }) => rest),
233
+ null,
234
+ 2,
235
+ ),
236
+ );
237
+ return;
238
+ }
239
+
240
+ // Group by category
241
+ const byCategory = {};
242
+ for (const s of skills) {
243
+ const cat = s.category || "uncategorized";
244
+ if (!byCategory[cat]) byCategory[cat] = [];
245
+ byCategory[cat].push(s);
246
+ }
247
+
248
+ logger.log(chalk.bold(`\nSkills (${skills.length}):\n`));
249
+
250
+ for (const [cat, catSkills] of Object.entries(byCategory).sort()) {
251
+ logger.log(chalk.yellow(` ${cat} (${catSkills.length})`));
252
+ for (const s of catSkills) {
253
+ const handler = s.hasHandler ? chalk.green("●") : chalk.gray("○");
254
+ const name = chalk.cyan(s.id.padEnd(30));
255
+ const desc = chalk.gray((s.description || "").substring(0, 50));
256
+ logger.log(` ${handler} ${name} ${desc}`);
257
+ }
258
+ logger.log("");
259
+ }
260
+
261
+ logger.log(
262
+ chalk.gray("● = has handler (runnable) ○ = documentation only\n"),
263
+ );
264
+ });
265
+
266
+ // skill categories
267
+ skill
268
+ .command("categories")
269
+ .description("List skill categories")
270
+ .action(async () => {
271
+ const skillsDir = findSkillsDir();
272
+ if (!skillsDir) {
273
+ logger.error("Skills directory not found.");
274
+ process.exit(1);
275
+ }
276
+
277
+ const skills = loadSkillMetadata(skillsDir);
278
+ const cats = {};
279
+ for (const s of skills) {
280
+ const cat = s.category || "uncategorized";
281
+ cats[cat] = (cats[cat] || 0) + 1;
282
+ }
283
+
284
+ logger.log(chalk.bold("\nSkill Categories:\n"));
285
+ for (const [cat, count] of Object.entries(cats).sort()) {
286
+ logger.log(` ${chalk.cyan(cat.padEnd(25))} ${count} skills`);
287
+ }
288
+ logger.log("");
289
+ });
290
+
291
+ // skill info
292
+ skill
293
+ .command("info")
294
+ .description("Show detailed info about a skill")
295
+ .argument("<name>", "Skill name")
296
+ .option("--json", "Output as JSON")
297
+ .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);
305
+ const s = skills.find(
306
+ (s) => s.id === name || s.dirName === name || s.id.includes(name),
307
+ );
308
+
309
+ if (!s) {
310
+ logger.error(`Skill not found: ${name}`);
311
+ const close = skills
312
+ .filter((s) => s.id.includes(name) || s.dirName.includes(name))
313
+ .slice(0, 5);
314
+ if (close.length > 0) {
315
+ logger.info(`Did you mean: ${close.map((s) => s.id).join(", ")}?`);
316
+ }
317
+ process.exit(1);
318
+ }
319
+
320
+ if (options.json) {
321
+ const { body, ...rest } = s;
322
+ console.log(JSON.stringify(rest, null, 2));
323
+ return;
324
+ }
325
+
326
+ logger.log(chalk.bold(`\n${s.displayName}`));
327
+ logger.log(chalk.gray(`ID: ${s.id} v${s.version}`));
328
+ logger.log(chalk.gray(`Category: ${s.category}`));
329
+ if (s.tags.length > 0) {
330
+ logger.log(chalk.gray(`Tags: ${s.tags.join(", ")}`));
331
+ }
332
+ logger.log("");
333
+ logger.log(s.description || "No description");
334
+ logger.log("");
335
+ logger.log(
336
+ `Handler: ${s.hasHandler ? chalk.green("Available") : chalk.gray("None")}`,
337
+ );
338
+ logger.log(
339
+ `Platform: ${canRunOnPlatform(s) ? chalk.green("Compatible") : chalk.red("Not supported")}`,
340
+ );
341
+
342
+ if (s.body) {
343
+ logger.log(chalk.bold("\n--- Documentation ---\n"));
344
+ logger.log(s.body.substring(0, 2000));
345
+ }
346
+ });
347
+
348
+ // skill search
349
+ skill
350
+ .command("search")
351
+ .description("Search skills by keyword")
352
+ .argument("<query>", "Search query")
353
+ .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);
361
+ const q = query.toLowerCase();
362
+ const matches = skills.filter(
363
+ (s) =>
364
+ s.id.includes(q) ||
365
+ s.displayName.toLowerCase().includes(q) ||
366
+ s.description.toLowerCase().includes(q) ||
367
+ s.tags.some((t) => t.toLowerCase().includes(q)),
368
+ );
369
+
370
+ if (matches.length === 0) {
371
+ logger.info(`No skills matching "${query}"`);
372
+ return;
373
+ }
374
+
375
+ logger.log(
376
+ chalk.bold(`\nSearch results for "${query}" (${matches.length}):\n`),
377
+ );
378
+ for (const s of matches) {
379
+ const handler = s.hasHandler ? chalk.green("●") : chalk.gray("○");
380
+ logger.log(
381
+ ` ${handler} ${chalk.cyan(s.id.padEnd(30))} ${chalk.gray(s.description.substring(0, 50))}`,
382
+ );
383
+ }
384
+ logger.log("");
385
+ });
386
+
387
+ // skill run
388
+ skill
389
+ .command("run")
390
+ .description("Execute a skill")
391
+ .argument("<name>", "Skill name")
392
+ .argument("[input...]", "Input for the skill")
393
+ .option("--json", "Output as JSON")
394
+ .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);
402
+ const s = skills.find((sk) => sk.id === name || sk.dirName === name);
403
+
404
+ if (!s) {
405
+ logger.error(`Skill not found: ${name}`);
406
+ process.exit(1);
407
+ }
408
+
409
+ if (!s.hasHandler) {
410
+ logger.error(
411
+ `Skill "${s.id}" has no handler (documentation only). Cannot execute.`,
412
+ );
413
+ process.exit(1);
414
+ }
415
+
416
+ if (!canRunOnPlatform(s)) {
417
+ logger.error(`Skill "${s.id}" is not supported on ${process.platform}`);
418
+ process.exit(1);
419
+ }
420
+
421
+ const spinner = ora(`Running ${s.displayName}...`).start();
422
+ const input = inputParts.join(" ");
423
+
424
+ try {
425
+ // Load the handler
426
+ const handlerPath = path.join(skillsDir, s.dirName, "handler.js");
427
+ const imported = await import(
428
+ `file://${handlerPath.replace(/\\/g, "/")}`
429
+ );
430
+ const handler = imported.default || imported;
431
+
432
+ // Initialize if needed
433
+ if (handler.init) {
434
+ await handler.init(s);
435
+ }
436
+
437
+ // Execute
438
+ const task = {
439
+ params: { input },
440
+ input,
441
+ action: input,
442
+ };
443
+ const context = {
444
+ projectRoot: process.cwd(),
445
+ workspacePath: process.cwd(),
446
+ workspaceRoot: process.cwd(),
447
+ };
448
+
449
+ const result = await handler.execute(task, context, s);
450
+ spinner.stop();
451
+
452
+ if (options.json) {
453
+ console.log(JSON.stringify(result, null, 2));
454
+ } else if (result.success) {
455
+ logger.success(result.message || "Done");
456
+ if (result.result && typeof result.result === "object") {
457
+ // Pretty print result
458
+ for (const [key, val] of Object.entries(result.result)) {
459
+ if (typeof val === "string" && val.length > 200) {
460
+ logger.log(` ${chalk.cyan(key)}: ${val.substring(0, 200)}...`);
461
+ } else {
462
+ logger.log(` ${chalk.cyan(key)}: ${JSON.stringify(val)}`);
463
+ }
464
+ }
465
+ }
466
+ } else {
467
+ logger.error(
468
+ result.message || result.error || "Skill execution failed",
469
+ );
470
+ }
471
+ } catch (err) {
472
+ spinner.fail(`Skill execution failed: ${err.message}`);
473
+ if (program.opts().verbose) {
474
+ console.error(err.stack);
475
+ }
476
+ process.exit(1);
477
+ }
478
+ });
479
+ }
package/src/index.js CHANGED
@@ -8,6 +8,13 @@ import { registerServicesCommand } from "./commands/services.js";
8
8
  import { registerConfigCommand } from "./commands/config.js";
9
9
  import { registerUpdateCommand } from "./commands/update.js";
10
10
  import { registerDoctorCommand } from "./commands/doctor.js";
11
+ import { registerDbCommand } from "./commands/db.js";
12
+ import { registerNoteCommand } from "./commands/note.js";
13
+ import { registerChatCommand } from "./commands/chat.js";
14
+ import { registerAskCommand } from "./commands/ask.js";
15
+ import { registerLlmCommand } from "./commands/llm.js";
16
+ import { registerAgentCommand } from "./commands/agent.js";
17
+ import { registerSkillCommand } from "./commands/skill.js";
11
18
 
12
19
  export function createProgram() {
13
20
  const program = new Command();
@@ -21,6 +28,7 @@ export function createProgram() {
21
28
  .option("--verbose", "Enable verbose output")
22
29
  .option("--quiet", "Suppress non-essential output");
23
30
 
31
+ // Existing commands
24
32
  registerSetupCommand(program);
25
33
  registerStartCommand(program);
26
34
  registerStopCommand(program);
@@ -30,5 +38,14 @@ export function createProgram() {
30
38
  registerUpdateCommand(program);
31
39
  registerDoctorCommand(program);
32
40
 
41
+ // New headless commands
42
+ registerDbCommand(program);
43
+ registerNoteCommand(program);
44
+ registerChatCommand(program);
45
+ registerAskCommand(program);
46
+ registerLlmCommand(program);
47
+ registerAgentCommand(program);
48
+ registerSkillCommand(program);
49
+
33
50
  return program;
34
51
  }
@@ -22,3 +22,18 @@ export function isMac() {
22
22
  export function isLinux() {
23
23
  return getPlatform() === "linux";
24
24
  }
25
+
26
+ export function getBinaryName(version) {
27
+ const p = getPlatform();
28
+ const a = getArch();
29
+ const ext = getBinaryExtension();
30
+ return `chainlesschain-${version}-${p}-${a}${ext}`;
31
+ }
32
+
33
+ export function getBinaryExtension() {
34
+ const p = getPlatform();
35
+ if (p === "win32") return ".exe";
36
+ if (p === "darwin") return ".dmg";
37
+ if (p === "linux") return ".deb";
38
+ return "";
39
+ }