chainlesschain 0.43.2 → 0.44.0

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,630 @@
1
+ /**
2
+ * CLI Command Skill Pack Generator
3
+ *
4
+ * Auto-generates SKILL.md + handler.js for each CLI domain pack.
5
+ * Output goes to <userData>/skills/ (managed layer, globally available).
6
+ *
7
+ * Usage:
8
+ * import { generateCliPacks } from './generator.js';
9
+ * const result = await generateCliPacks({ force: false, dryRun: false });
10
+ */
11
+
12
+ import fs from "fs";
13
+ import path from "path";
14
+ import crypto from "crypto";
15
+ import { fileURLToPath } from "url";
16
+ import {
17
+ CLI_PACK_DOMAINS,
18
+ EXECUTION_MODE_DESCRIPTIONS,
19
+ AGENT_MODE_COMMANDS,
20
+ PACK_SCHEMA_VERSION,
21
+ } from "./schema.js";
22
+ import { getElectronUserDataDir } from "../paths.js";
23
+
24
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
+
26
+ // ── Hash utilities ─────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Compute a version hash for a domain pack.
30
+ * Hash changes when: schema version, CLI version, or command list changes.
31
+ */
32
+ export function computePackHash(domainKey, domainDef, cliVersion) {
33
+ const commands = Object.keys(domainDef.commands).sort().join(",");
34
+ const raw = `${PACK_SCHEMA_VERSION}|${cliVersion}|${domainKey}|${commands}`;
35
+ return crypto.createHash("sha256").update(raw).digest("hex").slice(0, 16);
36
+ }
37
+
38
+ /**
39
+ * Read the current CLI package version
40
+ */
41
+ function getCliVersion() {
42
+ try {
43
+ const pkgPath = path.resolve(__dirname, "../../../package.json");
44
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
45
+ return pkg.version || "0.0.0";
46
+ } catch {
47
+ return "0.0.0";
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Read the version hash stored in an existing SKILL.md
53
+ */
54
+ function readExistingHash(skillMdPath) {
55
+ try {
56
+ if (!fs.existsSync(skillMdPath)) return null;
57
+ const content = fs.readFileSync(skillMdPath, "utf-8");
58
+ const match = content.match(/cli-version-hash:\s*["']?([a-f0-9]+)["']?/);
59
+ return match ? match[1] : null;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ // ── SKILL.md generator ─────────────────────────────────────────────
66
+
67
+ /**
68
+ * Generate SKILL.md content for a domain pack
69
+ */
70
+ export function generateSkillMd(domainKey, domainDef, hash) {
71
+ const commandEntries = Object.entries(domainDef.commands);
72
+ const allTags = domainDef.tags.join(", ");
73
+
74
+ // Build commands documentation section
75
+ const commandDocs = commandEntries
76
+ .map(([cmd, info]) => {
77
+ const subCmds =
78
+ info.subcommands && info.subcommands.length > 0
79
+ ? `\n 子命令: \`${info.subcommands.join("`、`")}\``
80
+ : "";
81
+ const example = info.example
82
+ ? `\n 示例: \`chainlesschain ${info.example}\``
83
+ : "";
84
+ const agentNote =
85
+ info.isAgentMode || AGENT_MODE_COMMANDS.has(cmd)
86
+ ? "\n ⚠️ 此指令需要在终端中直接运行"
87
+ : "";
88
+ return `### \`${cmd}\`\n${info.description}${subCmds}${example}${agentNote}`;
89
+ })
90
+ .join("\n\n");
91
+
92
+ // Build input format examples
93
+ const inputExamples = commandEntries
94
+ .filter((_, i) => i < 3)
95
+ .map(([, info]) => info.example || "")
96
+ .filter(Boolean)
97
+ .map((ex) => `- "${ex}"`)
98
+ .join("\n");
99
+
100
+ const modeDesc = EXECUTION_MODE_DESCRIPTIONS[domainDef.executionMode] || "";
101
+ const agentWarning =
102
+ domainDef.executionMode === "agent"
103
+ ? "\n\n> **注意**: 此技能包中的指令需要交互式终端,handler不会直接执行指令,而是返回完整的使用说明供Agent决策。"
104
+ : "";
105
+
106
+ return `---
107
+ name: ${domainKey}
108
+ display-name: ${domainDef.displayName}
109
+ description: ${domainDef.description}
110
+ version: 1.0.0
111
+ category: ${domainDef.category}
112
+ execution-mode: ${domainDef.executionMode}
113
+ cli-domain: ${domainKey.replace("cli-", "").replace("-pack", "")}
114
+ cli-version-hash: "${hash}"
115
+ tags: [${allTags}]
116
+ user-invocable: true
117
+ handler: handler.js
118
+ ---
119
+
120
+ # ${domainDef.displayName}
121
+
122
+ ${domainDef.description}${agentWarning}
123
+
124
+ ## 执行模式
125
+
126
+ **${domainDef.executionMode}** — ${modeDesc}
127
+
128
+ ## 调用方式
129
+
130
+ \`\`\`
131
+ chainlesschain skill run ${domainKey} "<command> [subcommand] [args] [--options]"
132
+ \`\`\`
133
+
134
+ **输入格式示例**:
135
+ ${inputExamples}
136
+
137
+ ## 包含指令
138
+
139
+ ${commandDocs}
140
+
141
+ ## 全局选项
142
+
143
+ 所有指令支持以下全局选项:
144
+ - \`--verbose\` 详细日志输出
145
+ - \`--quiet\` 静默模式
146
+ - \`--json\` JSON格式输出(部分指令支持)
147
+
148
+ ## 提供商配置(适用于AI相关指令)
149
+
150
+ - \`--provider <name>\` 指定LLM提供商 (ollama/openai/anthropic/deepseek等)
151
+ - \`--model <name>\` 指定模型名称
152
+ - \`--api-key <key>\` API密钥
153
+ `;
154
+ }
155
+
156
+ // ── Handler generator ──────────────────────────────────────────────
157
+
158
+ /**
159
+ * Generate handler.js content for a direct-execution pack
160
+ */
161
+ function generateDirectHandler(domainKey, domainDef) {
162
+ const commandList = Object.keys(domainDef.commands).join('", "');
163
+ const commandGuide = Object.entries(domainDef.commands)
164
+ .map(
165
+ ([cmd, info]) =>
166
+ ` ${cmd}: '${info.example ? `chainlesschain ${info.example}` : `chainlesschain ${cmd}`}'`,
167
+ )
168
+ .join(",\n");
169
+
170
+ return `/**
171
+ * ${domainDef.displayName} — 直接执行处理器
172
+ *
173
+ * 执行模式: ${domainDef.executionMode}
174
+ * 包含指令: ${Object.keys(domainDef.commands).join(", ")}
175
+ *
176
+ * 自动生成 — 请勿手动修改,运行 \`chainlesschain skill sync-cli\` 重新生成
177
+ */
178
+
179
+ const { spawnSync } = require("child_process");
180
+
181
+ /** 解析输入字符串为指令+参数数组 */
182
+ function parseInput(input) {
183
+ if (!input || !input.trim()) return { args: [], raw: "" };
184
+ // Shell-style tokenizer (handles quoted strings)
185
+ const args = [];
186
+ let current = "";
187
+ let inSingle = false;
188
+ let inDouble = false;
189
+ for (const ch of input.trim()) {
190
+ if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; }
191
+ if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; }
192
+ if (ch === " " && !inSingle && !inDouble) {
193
+ if (current) { args.push(current); current = ""; }
194
+ continue;
195
+ }
196
+ current += ch;
197
+ }
198
+ if (current) args.push(current);
199
+ return { args, raw: input.trim() };
200
+ }
201
+
202
+ /** 检测是否支持 --json 输出的指令 */
203
+ const JSON_SUPPORTED_COMMANDS = new Set([
204
+ "note", "search", "did", "wallet", "org", "p2p",
205
+ "session", "tokens", "llm", "skill", "mcp", "plugin",
206
+ "bi", "lowcode", "compliance", "siem", "hook", "workflow", "a2a", "hmemory"
207
+ ]);
208
+
209
+ const VALID_COMMANDS = new Set(["${commandList}"]);
210
+
211
+ const handler = {
212
+ async init(_skill) {
213
+ // No initialization needed for direct execution
214
+ },
215
+
216
+ async execute(task, context, _skill) {
217
+ const input = task.input || task.params?.input || "";
218
+ const { args, raw } = parseInput(input);
219
+
220
+ if (!args.length) {
221
+ return {
222
+ success: false,
223
+ error: "请提供要执行的指令。示例:\\n" + JSON.stringify({
224
+ ${commandGuide}
225
+ }, null, 2),
226
+ };
227
+ }
228
+
229
+ const command = args[0];
230
+
231
+ // Validate command belongs to this pack
232
+ if (!VALID_COMMANDS.has(command)) {
233
+ return {
234
+ success: false,
235
+ error: \`指令 "\${command}" 不在此技能包中。可用指令: \${[...VALID_COMMANDS].join(", ")}\`,
236
+ };
237
+ }
238
+
239
+ // Build CLI args — add --json for structured output when supported
240
+ const cliArgs = [...args];
241
+ const useJson = JSON_SUPPORTED_COMMANDS.has(command) && !cliArgs.includes("--json");
242
+ if (useJson) cliArgs.push("--json");
243
+
244
+ // Execute via child process
245
+ const result = spawnSync("chainlesschain", cliArgs, {
246
+ encoding: "utf-8",
247
+ shell: true,
248
+ cwd: context.projectRoot || process.cwd(),
249
+ timeout: 30000,
250
+ env: { ...process.env },
251
+ });
252
+
253
+ if (result.error) {
254
+ return {
255
+ success: false,
256
+ error: \`执行失败: \${result.error.message}\`,
257
+ command: \`chainlesschain \${raw}\`,
258
+ };
259
+ }
260
+
261
+ const stdout = (result.stdout || "").trim();
262
+ const stderr = (result.stderr || "").trim();
263
+ const exitCode = result.status ?? -1;
264
+
265
+ if (exitCode !== 0) {
266
+ return {
267
+ success: false,
268
+ error: stderr || \`指令退出码: \${exitCode}\`,
269
+ stdout,
270
+ command: \`chainlesschain \${raw}\`,
271
+ };
272
+ }
273
+
274
+ // Try to parse JSON output for structured result
275
+ let parsed = null;
276
+ if (useJson && stdout) {
277
+ try { parsed = JSON.parse(stdout); } catch { /* plain text output */ }
278
+ }
279
+
280
+ return {
281
+ success: true,
282
+ message: \`chainlesschain \${cliArgs.slice(0, 2).join(" ")} 执行成功\`,
283
+ result: parsed || stdout || "(无输出)",
284
+ command: \`chainlesschain \${raw}\`,
285
+ };
286
+ },
287
+ };
288
+
289
+ module.exports = handler;
290
+ `;
291
+ }
292
+
293
+ /**
294
+ * Generate handler.js content for agent-mode pack
295
+ */
296
+ function generateAgentHandler(domainKey, domainDef) {
297
+ const usageGuide = Object.entries(domainDef.commands)
298
+ .map(([cmd, info]) => {
299
+ const ex = info.example
300
+ ? `chainlesschain ${info.example}`
301
+ : `chainlesschain ${cmd}`;
302
+ return ` { command: '${cmd}', description: '${info.description}', example: '${ex}' }`;
303
+ })
304
+ .join(",\n");
305
+
306
+ return `/**
307
+ * ${domainDef.displayName} — Agent模式处理器
308
+ *
309
+ * 执行模式: ${domainDef.executionMode}
310
+ * ⚠️ 此技能包中的指令需要交互式终端,不能通过子进程直接调用。
311
+ * handler返回使用说明,由上层Agent决策如何通知用户执行。
312
+ *
313
+ * 自动生成 — 请勿手动修改,运行 \`chainlesschain skill sync-cli\` 重新生成
314
+ */
315
+
316
+ const COMMANDS = [
317
+ ${usageGuide}
318
+ ];
319
+
320
+ const handler = {
321
+ async init(_skill) {},
322
+
323
+ async execute(task, _context, _skill) {
324
+ const input = (task.input || task.params?.input || "").trim();
325
+
326
+ // Match requested command
327
+ const requestedCmd = input.split(/\\s+/)[0] || "";
328
+ const matched = requestedCmd
329
+ ? COMMANDS.find(c => c.command === requestedCmd)
330
+ : null;
331
+
332
+ if (matched) {
333
+ return {
334
+ success: true,
335
+ executionMode: "agent",
336
+ message: \`\${matched.command} 需要在终端中交互式运行\`,
337
+ result: {
338
+ description: matched.description,
339
+ howToRun: \`在终端中执行: \${matched.example}\`,
340
+ note: "此指令需要交互式REPL,请直接在终端运行上方命令",
341
+ options: {
342
+ "--provider <name>": "LLM提供商 (ollama/openai/anthropic/...)",
343
+ "--model <name>": "模型名称",
344
+ "--session <id>": "恢复上次会话",
345
+ },
346
+ },
347
+ };
348
+ }
349
+
350
+ // Return full command guide
351
+ return {
352
+ success: true,
353
+ executionMode: "agent",
354
+ message: "以下指令需要在终端中直接运行",
355
+ result: {
356
+ availableCommands: COMMANDS.map(c => ({
357
+ command: c.command,
358
+ description: c.description,
359
+ example: c.example,
360
+ })),
361
+ note: "这些指令需要交互式终端 (REPL),请直接在终端中运行对应命令",
362
+ },
363
+ };
364
+ },
365
+ };
366
+
367
+ module.exports = handler;
368
+ `;
369
+ }
370
+
371
+ /**
372
+ * Generate handler.js content for hybrid-mode pack
373
+ */
374
+ function generateHybridHandler(domainKey, domainDef) {
375
+ const agentCmds = Object.entries(domainDef.commands)
376
+ .filter(([cmd, info]) => info.isAgentMode || AGENT_MODE_COMMANDS.has(cmd))
377
+ .map(([cmd]) => `"${cmd}"`)
378
+ .join(", ");
379
+
380
+ const commandList = Object.keys(domainDef.commands).join('", "');
381
+ const commandGuide = Object.entries(domainDef.commands)
382
+ .filter(([cmd, info]) => !info.isAgentMode && !AGENT_MODE_COMMANDS.has(cmd))
383
+ .map(
384
+ ([cmd, info]) =>
385
+ ` ${cmd}: '${info.example ? `chainlesschain ${info.example}` : `chainlesschain ${cmd}`}'`,
386
+ )
387
+ .join(",\n");
388
+
389
+ return `/**
390
+ * ${domainDef.displayName} — 混合执行处理器
391
+ *
392
+ * 执行模式: ${domainDef.executionMode}
393
+ * Agent模式指令 (需终端): [${agentCmds || "无"}]
394
+ * 其余指令通过子进程直接执行
395
+ *
396
+ * 自动生成 — 请勿手动修改,运行 \`chainlesschain skill sync-cli\` 重新生成
397
+ */
398
+
399
+ const { spawnSync } = require("child_process");
400
+
401
+ const AGENT_ONLY_COMMANDS = new Set([${agentCmds}]);
402
+ const VALID_COMMANDS = new Set(["${commandList}"]);
403
+
404
+ function parseInput(input) {
405
+ if (!input || !input.trim()) return { args: [], raw: "" };
406
+ const args = [];
407
+ let current = "";
408
+ let inSingle = false;
409
+ let inDouble = false;
410
+ for (const ch of input.trim()) {
411
+ if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; }
412
+ if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; }
413
+ if (ch === " " && !inSingle && !inDouble) {
414
+ if (current) { args.push(current); current = ""; }
415
+ continue;
416
+ }
417
+ current += ch;
418
+ }
419
+ if (current) args.push(current);
420
+ return { args, raw: input.trim() };
421
+ }
422
+
423
+ const handler = {
424
+ async init(_skill) {},
425
+
426
+ async execute(task, context, _skill) {
427
+ const input = task.input || task.params?.input || "";
428
+ const { args, raw } = parseInput(input);
429
+
430
+ if (!args.length) {
431
+ return {
432
+ success: false,
433
+ error: "请提供要执行的指令。可用指令: " + [...VALID_COMMANDS].join(", "),
434
+ };
435
+ }
436
+
437
+ const command = args[0];
438
+
439
+ if (!VALID_COMMANDS.has(command)) {
440
+ return {
441
+ success: false,
442
+ error: \`指令 "\${command}" 不在此技能包中。可用指令: \${[...VALID_COMMANDS].join(", ")}\`,
443
+ };
444
+ }
445
+
446
+ // Agent-only commands: return usage guide
447
+ if (AGENT_ONLY_COMMANDS.has(command)) {
448
+ return {
449
+ success: true,
450
+ executionMode: "agent",
451
+ message: \`\${command} 需要在终端中交互式运行\`,
452
+ result: {
453
+ howToRun: \`chainlesschain \${raw}\`,
454
+ note: "此指令需要交互式终端,请直接在终端中运行",
455
+ },
456
+ };
457
+ }
458
+
459
+ // Direct execution for other commands
460
+ const cliArgs = [...args, "--quiet"];
461
+ const result = spawnSync("chainlesschain", cliArgs, {
462
+ encoding: "utf-8",
463
+ shell: true,
464
+ cwd: context.projectRoot || process.cwd(),
465
+ timeout: 30000,
466
+ env: { ...process.env },
467
+ });
468
+
469
+ if (result.error) {
470
+ return { success: false, error: \`执行失败: \${result.error.message}\` };
471
+ }
472
+
473
+ const exitCode = result.status ?? -1;
474
+ if (exitCode !== 0) {
475
+ return {
476
+ success: false,
477
+ error: (result.stderr || "").trim() || \`退出码: \${exitCode}\`,
478
+ stdout: (result.stdout || "").trim(),
479
+ };
480
+ }
481
+
482
+ let parsed = null;
483
+ const stdout = (result.stdout || "").trim();
484
+ try { parsed = JSON.parse(stdout); } catch { /* plain text */ }
485
+
486
+ return {
487
+ success: true,
488
+ message: \`chainlesschain \${command} 执行成功\`,
489
+ result: parsed || stdout || "(无输出)",
490
+ };
491
+ },
492
+ };
493
+
494
+ module.exports = handler;
495
+ `;
496
+ }
497
+
498
+ // ── Main generator ─────────────────────────────────────────────────
499
+
500
+ /**
501
+ * Generate all CLI pack skills
502
+ *
503
+ * @param {object} options
504
+ * @param {boolean} [options.force=false] - Force regenerate even if unchanged
505
+ * @param {boolean} [options.dryRun=false] - Preview changes without writing
506
+ * @param {string} [options.outputDir] - Override output directory (default: managed layer)
507
+ * @returns {Promise<GeneratorResult>}
508
+ */
509
+ export async function generateCliPacks(options = {}) {
510
+ const { force = false, dryRun = false } = options;
511
+
512
+ const cliVersion = getCliVersion();
513
+ const outputDir =
514
+ options.outputDir || path.join(getElectronUserDataDir(), "skills");
515
+
516
+ const results = {
517
+ generated: [],
518
+ skipped: [],
519
+ errors: [],
520
+ outputDir,
521
+ cliVersion,
522
+ };
523
+
524
+ for (const [domainKey, domainDef] of Object.entries(CLI_PACK_DOMAINS)) {
525
+ const packDir = path.join(outputDir, domainKey);
526
+ const skillMdPath = path.join(packDir, "SKILL.md");
527
+ const handlerPath = path.join(packDir, "handler.js");
528
+
529
+ const newHash = computePackHash(domainKey, domainDef, cliVersion);
530
+ const existingHash = readExistingHash(skillMdPath);
531
+
532
+ // Skip if unchanged (unless forced)
533
+ if (!force && existingHash === newHash) {
534
+ results.skipped.push({ domain: domainKey, reason: "unchanged" });
535
+ continue;
536
+ }
537
+
538
+ const changeReason =
539
+ existingHash === null ? "new" : force ? "forced" : "hash-changed";
540
+
541
+ if (dryRun) {
542
+ results.generated.push({ domain: domainKey, dryRun: true, changeReason });
543
+ continue;
544
+ }
545
+
546
+ try {
547
+ // Create directory
548
+ fs.mkdirSync(packDir, { recursive: true });
549
+
550
+ // Generate SKILL.md
551
+ const skillMd = generateSkillMd(domainKey, domainDef, newHash);
552
+ fs.writeFileSync(skillMdPath, skillMd, "utf-8");
553
+
554
+ // Generate handler.js based on execution mode
555
+ let handlerJs;
556
+ if (domainDef.executionMode === "agent") {
557
+ handlerJs = generateAgentHandler(domainKey, domainDef);
558
+ } else if (domainDef.executionMode === "hybrid") {
559
+ handlerJs = generateHybridHandler(domainKey, domainDef);
560
+ } else {
561
+ // direct or llm-query — both use spawnSync
562
+ handlerJs = generateDirectHandler(domainKey, domainDef);
563
+ }
564
+ fs.writeFileSync(handlerPath, handlerJs, "utf-8");
565
+
566
+ results.generated.push({
567
+ domain: domainKey,
568
+ displayName: domainDef.displayName,
569
+ executionMode: domainDef.executionMode,
570
+ commandCount: Object.keys(domainDef.commands).length,
571
+ packDir,
572
+ changeReason,
573
+ hash: newHash,
574
+ });
575
+ } catch (err) {
576
+ results.errors.push({ domain: domainKey, error: err.message });
577
+ }
578
+ }
579
+
580
+ return results;
581
+ }
582
+
583
+ /**
584
+ * Check which packs need updating (without generating)
585
+ * @param {string} [outputDir]
586
+ * @returns {object[]} Array of packs that need updating
587
+ */
588
+ export function checkForUpdates(outputDir) {
589
+ const dir = outputDir || path.join(getElectronUserDataDir(), "skills");
590
+ const cliVersion = getCliVersion();
591
+ const updates = [];
592
+
593
+ for (const [domainKey, domainDef] of Object.entries(CLI_PACK_DOMAINS)) {
594
+ const skillMdPath = path.join(dir, domainKey, "SKILL.md");
595
+ const existingHash = readExistingHash(skillMdPath);
596
+ const newHash = computePackHash(domainKey, domainDef, cliVersion);
597
+
598
+ if (existingHash !== newHash) {
599
+ updates.push({
600
+ domain: domainKey,
601
+ displayName: domainDef.displayName,
602
+ exists: existingHash !== null,
603
+ changeReason: existingHash === null ? "new" : "hash-changed",
604
+ existingHash,
605
+ newHash,
606
+ });
607
+ }
608
+ }
609
+
610
+ return updates;
611
+ }
612
+
613
+ /**
614
+ * Remove all generated CLI packs
615
+ * @param {string} [outputDir]
616
+ */
617
+ export function removeCliPacks(outputDir) {
618
+ const dir = outputDir || path.join(getElectronUserDataDir(), "skills");
619
+ const removed = [];
620
+
621
+ for (const domainKey of Object.keys(CLI_PACK_DOMAINS)) {
622
+ const packDir = path.join(dir, domainKey);
623
+ if (fs.existsSync(packDir)) {
624
+ fs.rmSync(packDir, { recursive: true, force: true });
625
+ removed.push(domainKey);
626
+ }
627
+ }
628
+
629
+ return removed;
630
+ }