chainlesschain 0.38.1 → 0.40.1

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,379 @@
1
+ /**
2
+ * CLI-Anything Bridge — discovers and registers CLI-Anything generated tools
3
+ * as ChainlessChain managed-layer skills.
4
+ *
5
+ * CLI-Anything (https://github.com/HKUDS/CLI-Anything) generates Agent-native
6
+ * CLI wrappers for arbitrary software. This bridge scans for those wrappers
7
+ * on the user's PATH and turns each one into a SKILL.md + handler.js pair
8
+ * that the existing 4-layer skill-loader picks up automatically.
9
+ */
10
+
11
+ import { execSync } from "child_process";
12
+ import fs from "fs";
13
+ import path from "path";
14
+ import { getElectronUserDataDir } from "./paths.js";
15
+
16
+ /* ---------- _deps injection (Vitest CJS mock pattern) ---------- */
17
+ export const _deps = { execSync, fs, path };
18
+
19
+ /* ----------------------------------------------------------------
20
+ * Database helpers
21
+ * ---------------------------------------------------------------- */
22
+
23
+ const CREATE_TABLE_SQL = `
24
+ CREATE TABLE IF NOT EXISTS cli_anything_tools (
25
+ id TEXT PRIMARY KEY,
26
+ name TEXT NOT NULL UNIQUE,
27
+ software_path TEXT,
28
+ cli_command TEXT NOT NULL,
29
+ version TEXT DEFAULT '1.0.0',
30
+ description TEXT,
31
+ subcommands TEXT,
32
+ skill_name TEXT,
33
+ status TEXT DEFAULT 'discovered',
34
+ created_at TEXT DEFAULT (datetime('now')),
35
+ updated_at TEXT DEFAULT (datetime('now'))
36
+ )`;
37
+
38
+ export function ensureCliAnythingTables(db) {
39
+ db.exec(CREATE_TABLE_SQL);
40
+ }
41
+
42
+ /* ----------------------------------------------------------------
43
+ * Python / CLI-Anything detection
44
+ * ---------------------------------------------------------------- */
45
+
46
+ /**
47
+ * Detect a usable Python interpreter.
48
+ * Returns { found: boolean, command?: string, version?: string }.
49
+ */
50
+ export function detectPython() {
51
+ const candidates =
52
+ process.platform === "win32"
53
+ ? ["python", "python3", "py"]
54
+ : ["python3", "python"];
55
+
56
+ for (const cmd of candidates) {
57
+ try {
58
+ const ver = _deps
59
+ .execSync(`${cmd} --version`, {
60
+ encoding: "utf-8",
61
+ timeout: 10000,
62
+ stdio: ["pipe", "pipe", "pipe"],
63
+ })
64
+ .trim();
65
+ const match = ver.match(/Python\s+([\d.]+)/i);
66
+ if (match) {
67
+ return { found: true, command: cmd, version: match[1] };
68
+ }
69
+ } catch (_err) {
70
+ // This candidate not available — try next
71
+ }
72
+ }
73
+ return { found: false };
74
+ }
75
+
76
+ /**
77
+ * Check whether the `cli-anything` Python package is installed.
78
+ * Returns { installed: boolean, version?: string }.
79
+ */
80
+ export function detectCliAnything() {
81
+ const py = detectPython();
82
+ if (!py.found) return { installed: false };
83
+
84
+ try {
85
+ const out = _deps.execSync(`${py.command} -m pip show cli-anything`, {
86
+ encoding: "utf-8",
87
+ timeout: 15000,
88
+ stdio: ["pipe", "pipe", "pipe"],
89
+ });
90
+ const verMatch = out.match(/Version:\s*([\d.]+)/i);
91
+ return {
92
+ installed: true,
93
+ version: verMatch ? verMatch[1] : "unknown",
94
+ pythonCommand: py.command,
95
+ };
96
+ } catch (_err) {
97
+ return { installed: false, pythonCommand: py.command };
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Install CLI-Anything via pip.
103
+ */
104
+ export function installCliAnything(pythonCmd) {
105
+ _deps.execSync(`${pythonCmd} -m pip install cli-anything`, {
106
+ encoding: "utf-8",
107
+ timeout: 120000,
108
+ stdio: "inherit",
109
+ });
110
+ }
111
+
112
+ /* ----------------------------------------------------------------
113
+ * Tool scanning
114
+ * ---------------------------------------------------------------- */
115
+
116
+ /**
117
+ * Scan PATH for executables matching `cli-anything-*`.
118
+ * Returns an array of { name, command, path }.
119
+ */
120
+ export function scanPathForTools() {
121
+ const results = [];
122
+ const seen = new Set();
123
+ const dirs = (process.env.PATH || "").split(_deps.path.delimiter);
124
+
125
+ for (const dir of dirs) {
126
+ try {
127
+ const entries = _deps.fs.readdirSync(dir);
128
+ for (const entry of entries) {
129
+ const baseName = entry.replace(/\.exe$/i, "");
130
+ if (!baseName.startsWith("cli-anything-")) continue;
131
+ if (seen.has(baseName)) continue;
132
+ seen.add(baseName);
133
+
134
+ const toolName = baseName.replace(/^cli-anything-/, "");
135
+ results.push({
136
+ name: toolName,
137
+ command: baseName,
138
+ path: _deps.path.join(dir, entry),
139
+ });
140
+ }
141
+ } catch (_err) {
142
+ // Directory not readable — skip
143
+ }
144
+ }
145
+ return results;
146
+ }
147
+
148
+ /**
149
+ * Parse `--help` output of a cli-anything generated tool.
150
+ * Returns { description, subcommands: [{ name, description }] }.
151
+ */
152
+ export function parseToolHelp(command) {
153
+ let helpText;
154
+ try {
155
+ helpText = _deps.execSync(`${command} --help`, {
156
+ encoding: "utf-8",
157
+ timeout: 15000,
158
+ stdio: ["pipe", "pipe", "pipe"],
159
+ });
160
+ } catch (err) {
161
+ const raw = err.stdout || err.stderr || "";
162
+ helpText = typeof raw === "string" ? raw : raw.toString("utf-8");
163
+ }
164
+
165
+ const lines = helpText.split("\n").map((l) => l.trimEnd());
166
+
167
+ // First non-empty, non-"usage:" line is typically the description
168
+ let description = "";
169
+ for (const line of lines) {
170
+ const trimmed = line.trim();
171
+ if (!trimmed) continue;
172
+ if (/^usage:/i.test(trimmed)) continue;
173
+ if (/^options?:/i.test(trimmed)) break;
174
+ if (/^commands?:/i.test(trimmed)) break;
175
+ description = trimmed;
176
+ break;
177
+ }
178
+
179
+ // Parse subcommands section
180
+ const subcommands = [];
181
+ let inCommands = false;
182
+ for (const line of lines) {
183
+ const trimmed = line.trim();
184
+ if (/^commands?:/i.test(trimmed) || /^subcommands?:/i.test(trimmed)) {
185
+ inCommands = true;
186
+ continue;
187
+ }
188
+ if (inCommands) {
189
+ if (!trimmed || /^options?:/i.test(trimmed)) break;
190
+ const match = trimmed.match(/^(\S+)\s+(.*)/);
191
+ if (match) {
192
+ subcommands.push({ name: match[1], description: match[2].trim() });
193
+ }
194
+ }
195
+ }
196
+
197
+ return {
198
+ description: description || `CLI-Anything tool: ${command}`,
199
+ subcommands,
200
+ };
201
+ }
202
+
203
+ /* ----------------------------------------------------------------
204
+ * Skill registration / removal
205
+ * ---------------------------------------------------------------- */
206
+
207
+ function _skillDir(toolName) {
208
+ const userData = getElectronUserDataDir();
209
+ return _deps.path.join(userData, "skills", `cli-anything-${toolName}`);
210
+ }
211
+
212
+ /**
213
+ * Generate SKILL.md content for a CLI-Anything tool.
214
+ */
215
+ export function _generateSkillMd(name, helpData) {
216
+ const subs = (helpData.subcommands || [])
217
+ .map((s) => `- **${s.name}**: ${s.description}`)
218
+ .join("\n");
219
+
220
+ return `---
221
+ name: cli-anything-${name}
222
+ display-name: CLI-Anything ${name}
223
+ description: ${helpData.description || `Agent-native CLI for ${name}`}
224
+ version: 1.0.0
225
+ category: integration
226
+ tags: [cli-anything, ${name}, external-tool]
227
+ user-invocable: true
228
+ handler: handler.js
229
+ capabilities: [shell-exec]
230
+ os: [linux, darwin, win32]
231
+ ---
232
+
233
+ # cli-anything-${name}
234
+
235
+ Auto-registered by \`chainlesschain cli-anything register ${name}\`.
236
+
237
+ ${helpData.description || ""}
238
+
239
+ ${subs ? `## Subcommands\n\n${subs}\n` : ""}
240
+ ## Usage
241
+
242
+ Pass the subcommand and arguments as plain text input:
243
+
244
+ \`\`\`
245
+ /skill cli-anything-${name} <subcommand> [args...]
246
+ \`\`\`
247
+ `;
248
+ }
249
+
250
+ /**
251
+ * Generate handler.js content for a CLI-Anything tool.
252
+ */
253
+ export function _generateHandlerJs(name, command) {
254
+ return `"use strict";
255
+ const { execSync } = require("child_process");
256
+
257
+ module.exports = {
258
+ async execute(task, context) {
259
+ const input = (task?.params?.input || task?.action || "").trim();
260
+ if (!input) return { success: false, error: "No input provided" };
261
+ try {
262
+ const output = execSync(\`${command} \${input}\`, {
263
+ encoding: "utf-8",
264
+ timeout: 60000,
265
+ cwd: context?.projectRoot || process.cwd(),
266
+ stdio: ["pipe", "pipe", "pipe"],
267
+ });
268
+ let result;
269
+ try { result = JSON.parse(output); } catch (_e) { result = { output: output.trim() }; }
270
+ return { success: true, result, message: "Completed" };
271
+ } catch (err) {
272
+ const errMsg = typeof err.stderr === "string" ? err.stderr : (err.stderr?.toString("utf-8") || "");
273
+ return { success: false, error: errMsg || err.message };
274
+ }
275
+ },
276
+ };
277
+ `;
278
+ }
279
+
280
+ /**
281
+ * Register a CLI-Anything tool as a managed-layer skill.
282
+ */
283
+ export function registerTool(db, toolName, opts = {}) {
284
+ const command = opts.command || `cli-anything-${toolName}`;
285
+ const helpData = opts.helpData || parseToolHelp(command);
286
+ const force = opts.force || false;
287
+
288
+ const dir = _skillDir(toolName);
289
+
290
+ // Check existing
291
+ const existing = db
292
+ .prepare("SELECT id FROM cli_anything_tools WHERE name = ?")
293
+ .get(toolName);
294
+ if (existing && !force) {
295
+ throw new Error(
296
+ `Tool "${toolName}" already registered. Use --force to overwrite.`,
297
+ );
298
+ }
299
+
300
+ // Write skill files
301
+ _deps.fs.mkdirSync(dir, { recursive: true });
302
+ _deps.fs.writeFileSync(
303
+ _deps.path.join(dir, "SKILL.md"),
304
+ _generateSkillMd(toolName, helpData),
305
+ "utf-8",
306
+ );
307
+ _deps.fs.writeFileSync(
308
+ _deps.path.join(dir, "handler.js"),
309
+ _generateHandlerJs(toolName, command),
310
+ "utf-8",
311
+ );
312
+
313
+ // Upsert DB record
314
+ const id = existing ? existing.id : `clia-${toolName}-${Date.now()}`;
315
+ const skillName = `cli-anything-${toolName}`;
316
+ const subcommandsJson = JSON.stringify(helpData.subcommands || []);
317
+
318
+ if (existing) {
319
+ db.prepare(
320
+ `
321
+ UPDATE cli_anything_tools
322
+ SET cli_command = ?, description = ?, subcommands = ?,
323
+ skill_name = ?, status = 'registered', updated_at = datetime('now')
324
+ WHERE name = ?
325
+ `,
326
+ ).run(command, helpData.description, subcommandsJson, skillName, toolName);
327
+ } else {
328
+ db.prepare(
329
+ `
330
+ INSERT INTO cli_anything_tools (id, name, software_path, cli_command, description, subcommands, skill_name, status)
331
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'registered')
332
+ `,
333
+ ).run(
334
+ id,
335
+ toolName,
336
+ opts.softwarePath || null,
337
+ command,
338
+ helpData.description,
339
+ subcommandsJson,
340
+ skillName,
341
+ );
342
+ }
343
+
344
+ return { id, skillName, dir, subcommands: helpData.subcommands || [] };
345
+ }
346
+
347
+ /**
348
+ * Remove a registered CLI-Anything tool.
349
+ */
350
+ export function removeTool(db, toolName) {
351
+ const row = db
352
+ .prepare("SELECT id FROM cli_anything_tools WHERE name = ?")
353
+ .get(toolName);
354
+ if (!row) {
355
+ throw new Error(`Tool "${toolName}" is not registered.`);
356
+ }
357
+
358
+ // Remove skill directory
359
+ const dir = _skillDir(toolName);
360
+ try {
361
+ _deps.fs.rmSync(dir, { recursive: true, force: true });
362
+ } catch (_err) {
363
+ // Directory may already be gone
364
+ }
365
+
366
+ db.prepare("DELETE FROM cli_anything_tools WHERE name = ?").run(toolName);
367
+ return { removed: true, toolName };
368
+ }
369
+
370
+ /**
371
+ * List all registered CLI-Anything tools.
372
+ */
373
+ export function listTools(db) {
374
+ return db
375
+ .prepare(
376
+ "SELECT id, name, cli_command, description, skill_name, status, created_at, updated_at FROM cli_anything_tools ORDER BY name",
377
+ )
378
+ .all();
379
+ }
@@ -24,7 +24,7 @@ export function gitExec(args, cwd) {
24
24
  stdio: ["pipe", "pipe", "pipe"],
25
25
  }).trim();
26
26
  } catch (err) {
27
- const stderr = err.stderr ? err.stderr.toString().trim() : "";
27
+ const stderr = err.stderr ? err.stderr.toString("utf-8").trim() : "";
28
28
  throw new Error(stderr || err.message);
29
29
  }
30
30
  }
@@ -70,6 +70,19 @@ export const BUILT_IN_PROVIDERS = {
70
70
  ],
71
71
  free: false,
72
72
  },
73
+ volcengine: {
74
+ name: "volcengine",
75
+ displayName: "Volcengine (豆包)",
76
+ baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
77
+ apiKeyEnv: "VOLCENGINE_API_KEY",
78
+ models: [
79
+ "doubao-seed-1-6-251015",
80
+ "doubao-seed-1-6-flash-250828",
81
+ "doubao-seed-1-6-lite-251015",
82
+ "doubao-seed-code",
83
+ ],
84
+ free: false,
85
+ },
73
86
  };
74
87
 
75
88
  /**
@@ -302,7 +315,7 @@ export class LLMProviderRegistry {
302
315
  return { ok: true, elapsed: Date.now() - start, response: text.trim() };
303
316
  }
304
317
 
305
- // OpenAI-compatible (openai, deepseek, dashscope, mistral)
318
+ // OpenAI-compatible (openai, deepseek, dashscope, mistral, volcengine)
306
319
  const key = this.getApiKey(name);
307
320
  if (!key) throw new Error(`${provider.apiKeyEnv} not set`);
308
321
  const res = await fetch(`${provider.baseUrl}/chat/completions`, {
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Task-based intelligent model selector for CLI
3
+ *
4
+ * Detects task type from user messages and recommends the best model
5
+ * for each LLM provider. Enables automatic model switching based on
6
+ * what the user is trying to accomplish.
7
+ */
8
+
9
+ /**
10
+ * Task types supported by the selector
11
+ */
12
+ export const TaskType = {
13
+ CHAT: "chat",
14
+ CODE: "code",
15
+ REASONING: "reasoning",
16
+ FAST: "fast",
17
+ TRANSLATE: "translate",
18
+ CREATIVE: "creative",
19
+ };
20
+
21
+ /**
22
+ * Task type → recommended model per provider
23
+ * Each provider maps to the best model for that task type.
24
+ */
25
+ const TASK_MODEL_MAP = {
26
+ [TaskType.CHAT]: {
27
+ volcengine: "doubao-seed-1-6-flash-250828",
28
+ openai: "gpt-4o-mini",
29
+ anthropic: "claude-sonnet-4-6",
30
+ deepseek: "deepseek-chat",
31
+ dashscope: "qwen-plus",
32
+ gemini: "gemini-2.0-flash",
33
+ mistral: "mistral-medium-latest",
34
+ ollama: "qwen2:7b",
35
+ },
36
+ [TaskType.CODE]: {
37
+ volcengine: "doubao-seed-code",
38
+ openai: "gpt-4o",
39
+ anthropic: "claude-sonnet-4-6",
40
+ deepseek: "deepseek-coder",
41
+ dashscope: "qwen-max",
42
+ gemini: "gemini-2.0-pro",
43
+ mistral: "mistral-large-latest",
44
+ ollama: "codellama:7b",
45
+ },
46
+ [TaskType.REASONING]: {
47
+ volcengine: "doubao-seed-1-6-251015",
48
+ openai: "o1",
49
+ anthropic: "claude-opus-4-6",
50
+ deepseek: "deepseek-reasoner",
51
+ dashscope: "qwen-max",
52
+ gemini: "gemini-2.0-pro",
53
+ mistral: "mistral-large-latest",
54
+ ollama: "qwen2:7b",
55
+ },
56
+ [TaskType.FAST]: {
57
+ volcengine: "doubao-seed-1-6-lite-251015",
58
+ openai: "gpt-4o-mini",
59
+ anthropic: "claude-haiku-4-5-20251001",
60
+ deepseek: "deepseek-chat",
61
+ dashscope: "qwen-turbo",
62
+ gemini: "gemini-2.0-flash",
63
+ mistral: "mistral-small-latest",
64
+ ollama: "qwen2:7b",
65
+ },
66
+ [TaskType.TRANSLATE]: {
67
+ volcengine: "doubao-seed-1-6-251015",
68
+ openai: "gpt-4o",
69
+ anthropic: "claude-sonnet-4-6",
70
+ deepseek: "deepseek-chat",
71
+ dashscope: "qwen-plus",
72
+ gemini: "gemini-2.0-flash",
73
+ mistral: "mistral-large-latest",
74
+ ollama: "qwen2:7b",
75
+ },
76
+ [TaskType.CREATIVE]: {
77
+ volcengine: "doubao-seed-1-6-251015",
78
+ openai: "gpt-4o",
79
+ anthropic: "claude-opus-4-6",
80
+ deepseek: "deepseek-chat",
81
+ dashscope: "qwen-max",
82
+ gemini: "gemini-2.0-pro",
83
+ mistral: "mistral-large-latest",
84
+ ollama: "qwen2:7b",
85
+ },
86
+ };
87
+
88
+ /**
89
+ * Task type display names (Chinese + English)
90
+ */
91
+ const TASK_NAMES = {
92
+ [TaskType.CHAT]: "日常对话",
93
+ [TaskType.CODE]: "代码任务",
94
+ [TaskType.REASONING]: "复杂推理",
95
+ [TaskType.FAST]: "快速响应",
96
+ [TaskType.TRANSLATE]: "翻译任务",
97
+ [TaskType.CREATIVE]: "创意写作",
98
+ };
99
+
100
+ /**
101
+ * Keyword patterns for detecting task type from user message.
102
+ * Each pattern is [regex, taskType, priority].
103
+ * Higher priority wins when multiple patterns match.
104
+ */
105
+ const TASK_PATTERNS = [
106
+ // Code patterns (priority 10) — English with word boundaries
107
+ [
108
+ /\b(code|coding|program|function|class|bug|debug|refactor|implement)\b/i,
109
+ TaskType.CODE,
110
+ 10,
111
+ ],
112
+ [
113
+ /\b(javascript|typescript|python|java|rust|go|c\+\+|sql|html|css|react|vue|node|npm|git|api|endpoint|database)\b/i,
114
+ TaskType.CODE,
115
+ 10,
116
+ ],
117
+ [/```[\s\S]*```/, TaskType.CODE, 10],
118
+ // Code patterns — Chinese (no \b, Chinese chars are not word-boundary compatible)
119
+ [
120
+ /(代码|编程|函数|调试|重构|实现|写[一个]*[代码函数方法])/,
121
+ TaskType.CODE,
122
+ 10,
123
+ ],
124
+
125
+ // Reasoning patterns (priority 8)
126
+ [
127
+ /\b(analyze|reason|explain why|prove|compare|evaluate)\b/i,
128
+ TaskType.REASONING,
129
+ 8,
130
+ ],
131
+ [/\b(step.by.step|think.*through)\b/i, TaskType.REASONING, 8],
132
+ [
133
+ /(分析|推理|解释为什么|证明|比较|评估|深度思考|逻辑|逐步|一步一步)/,
134
+ TaskType.REASONING,
135
+ 8,
136
+ ],
137
+
138
+ // Translation patterns (priority 9)
139
+ [/\b(translate|translation|translate.*to)\b/i, TaskType.TRANSLATE, 9],
140
+ [/(翻译|转换.*语言|英译中|中译英)/, TaskType.TRANSLATE, 9],
141
+
142
+ // Creative patterns (priority 7)
143
+ [
144
+ /\b(write|create|compose|story|poem|essay|blog|article)\b/i,
145
+ TaskType.CREATIVE,
146
+ 7,
147
+ ],
148
+ [/(写[一篇]*.*[故事诗歌文章博客]|创作|小说|剧本)/, TaskType.CREATIVE, 7],
149
+
150
+ // Fast patterns (priority 5)
151
+ [/\b(quick|brief|short)\b/i, TaskType.FAST, 5],
152
+ [/(简短|快速|简单回答|一句话)/, TaskType.FAST, 5],
153
+ ];
154
+
155
+ /**
156
+ * Detect the task type from a user message using keyword matching.
157
+ *
158
+ * @param {string} message - User's input message
159
+ * @returns {{ taskType: string, confidence: number, name: string }}
160
+ */
161
+ export function detectTaskType(message) {
162
+ if (!message || typeof message !== "string") {
163
+ return {
164
+ taskType: TaskType.CHAT,
165
+ confidence: 0,
166
+ name: TASK_NAMES[TaskType.CHAT],
167
+ };
168
+ }
169
+
170
+ let bestMatch = null;
171
+ let bestPriority = -1;
172
+ let matchCount = 0;
173
+
174
+ for (const [pattern, taskType, priority] of TASK_PATTERNS) {
175
+ if (pattern.test(message)) {
176
+ matchCount++;
177
+ if (priority > bestPriority) {
178
+ bestPriority = priority;
179
+ bestMatch = taskType;
180
+ }
181
+ }
182
+ }
183
+
184
+ if (!bestMatch) {
185
+ return {
186
+ taskType: TaskType.CHAT,
187
+ confidence: 0,
188
+ name: TASK_NAMES[TaskType.CHAT],
189
+ };
190
+ }
191
+
192
+ // Confidence based on match count and priority
193
+ const confidence = Math.min(1, matchCount * 0.3 + bestPriority * 0.07);
194
+
195
+ return {
196
+ taskType: bestMatch,
197
+ confidence,
198
+ name: TASK_NAMES[bestMatch],
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Select the best model for a given provider and task type.
204
+ *
205
+ * @param {string} provider - LLM provider name
206
+ * @param {string} taskType - Task type from TaskType enum
207
+ * @returns {string|null} Model ID, or null if no recommendation
208
+ */
209
+ export function selectModelForTask(provider, taskType) {
210
+ const taskMap = TASK_MODEL_MAP[taskType];
211
+ if (!taskMap) return null;
212
+ return taskMap[provider] || null;
213
+ }
214
+
215
+ /**
216
+ * Get a human-readable task name.
217
+ *
218
+ * @param {string} taskType - Task type
219
+ * @returns {string}
220
+ */
221
+ export function getTaskName(taskType) {
222
+ return TASK_NAMES[taskType] || taskType;
223
+ }
224
+
225
+ /**
226
+ * Get all supported task types.
227
+ *
228
+ * @returns {Object} TaskType enum
229
+ */
230
+ export function getTaskTypes() {
231
+ return { ...TaskType };
232
+ }