foliko 2.0.11 → 2.0.13

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.
@@ -263,6 +263,59 @@ function buildSchemaFromType(type, desc, defaultValue) {
263
263
  return schema;
264
264
  }
265
265
 
266
+ /**
267
+ * 从 options 构建 JSON Schema(供 AI SDK 直接使用,不走 Zod)
268
+ * 用于替换旧的 CLI 字符串模式,让 LLM 能用结构化参数调用
269
+ *
270
+ * type 可选值:'string' | 'number' | 'boolean' | 'array' | 'object'
271
+ * (对应 JS 原生类型 String, Boolean, Number, Array, Object)
272
+ */
273
+ function buildInputSchemaFromOptions(options) {
274
+ if (!Array.isArray(options) || options.length === 0) {
275
+ return { type: 'object', properties: {}, required: [] };
276
+ }
277
+
278
+ const properties = {};
279
+ const required = [];
280
+
281
+ for (const opt of options) {
282
+ const name = extractOptionName(opt.flags);
283
+ if (!name) continue;
284
+
285
+ const hasValue = optionHasValue(opt.flags);
286
+ const isRequired = opt.required === true && hasValue;
287
+ const desc = opt.description || '';
288
+ const defaultVal = opt.defaultValue;
289
+
290
+ let jsonType = 'string';
291
+ if (opt.schema && typeof opt.schema.parse === 'function') {
292
+ // 从 Zod 实例猜测类型
293
+ const s = opt.schema;
294
+ if (s._def.typeName === 'ZodNumber' || s._def.typeName === 'ZodInt') jsonType = 'number';
295
+ else if (s._def.typeName === 'ZodBoolean') jsonType = 'boolean';
296
+ else if (s._def.typeName === 'ZodArray') jsonType = 'array';
297
+ else if (s._def.typeName === 'ZodObject' || s._def.typeName === 'ZodRecord') jsonType = 'object';
298
+ } else if (opt.type) {
299
+ const L = opt.type.toLowerCase();
300
+ if (L === 'number' || L === 'int' || L === 'integer') jsonType = 'number';
301
+ else if (L === 'boolean' || L === 'bool') jsonType = 'boolean';
302
+ else if (L === 'array') jsonType = 'array';
303
+ else if (L === 'object') jsonType = 'object';
304
+ } else {
305
+ jsonType = hasValue ? 'string' : 'boolean';
306
+ }
307
+
308
+ const prop = { type: jsonType, description: desc };
309
+ if (defaultVal !== undefined && defaultVal !== null) {
310
+ prop.default = defaultVal;
311
+ }
312
+ properties[name] = prop;
313
+ if (isRequired) required.push(name);
314
+ }
315
+
316
+ return { type: 'object', properties, required };
317
+ }
318
+
266
319
  /**
267
320
  * 生成等效的 JSON schema(用于 _rawSchema,让 ext_skill 渲染更清晰)
268
321
  */
@@ -329,6 +382,50 @@ function buildExampleFromOptions(options) {
329
382
  return parts.join(' ');
330
383
  }
331
384
 
385
+ /**
386
+ * 解析命令行参数字符串为结构化对象(供 messaging 插件 backward compatible 使用)
387
+ * 支持:--name value, -n value, --flag, -f
388
+ * @param {string} str - 参数字符串
389
+ * @param {Array} options - 选项定义数组
390
+ * @returns {Object} 解析后的参数对象
391
+ */
392
+ function parseCommandArgs(str, options) {
393
+ if (typeof str !== 'string' || !str.trim()) return {};
394
+ const result = {};
395
+ const tokens = parseShellArgs(str);
396
+ let i = 0;
397
+ while (i < tokens.length) {
398
+ const token = tokens[i];
399
+ const optMatch = token.match(/^(-[a-zA-Z]|-{1,2}[a-zA-Z][a-zA-Z0-9-]*)$/);
400
+ if (optMatch) {
401
+ const optKey = optMatch[1];
402
+ const opt = options.find(o => {
403
+ const flags = o.flags || '';
404
+ return flags.includes(optKey) || flags.includes(optKey.replace(/^--?/, '--'));
405
+ });
406
+ if (opt) {
407
+ const name = extractOptionName(opt.flags);
408
+ const hasValue = optionHasValue(opt.flags);
409
+ if (hasValue && i + 1 < tokens.length) {
410
+ const next = tokens[i + 1];
411
+ if (!next.match(/^-/)) {
412
+ result[name] = next;
413
+ i += 2;
414
+ continue;
415
+ }
416
+ } else if (!hasValue) {
417
+ result[name] = true;
418
+ i += 1;
419
+ continue;
420
+ }
421
+ }
422
+ }
423
+ i += 1;
424
+ }
425
+ if (i < tokens.length) result._ = tokens.slice(i);
426
+ return result;
427
+ }
428
+
332
429
  /**
333
430
  * Skill 类
334
431
  */
@@ -356,6 +453,17 @@ class Skill {
356
453
  return [];
357
454
  }
358
455
 
456
+ /**
457
+ * 简单命令行字符串解析器(backward compatible,供 messaging 插件使用)
458
+ * 支持:--name value, -n value, --flag, -f, positional args
459
+ * @param {string} str - 参数字符串
460
+ * @param {Array} options - 选项定义数组
461
+ * @returns {Object} 解析后的参数对象
462
+ */
463
+ _parseCommandString(str, options) {
464
+ return parseCommandArgs(str, options);
465
+ }
466
+
359
467
  /**
360
468
  * 注册 skill 的命令到 extension 系统
361
469
  * 异步版本:用动态 import() 替代 require(),支持 ESM 包
@@ -415,87 +523,13 @@ class Skill {
415
523
  // 命令名格式: {skill_name}:{cmd_name}
416
524
  const fullName = `${skillName}:${cmd.name}`;
417
525
 
418
- // 获取参数解析器(优先使用 argumentParser,否则用 options 构建)
419
- let argumentParser = cmd.argumentParser;
420
- if (!argumentParser && Array.isArray(cmd.options) && cmd.options.length > 0) {
421
- argumentParser = (rawArgs) => {
422
- try {
423
- const program = new Command();
424
- program.exitOverride(); // 不自动退出
425
- program.configureOutput({ writeErr: () => {} }); // 隐藏错误输出
426
- for (const opt of cmd.options) {
427
- program.option(opt.flags, opt.description, opt.defaultValue);
428
- }
429
- program.arguments('[args...]');
430
- program.parse(['node', 'cmd', ...parseShellArgs(rawArgs)]);
431
- return { ...program.opts(), args: program.args };
432
- } catch (err) {
433
- log.warn(`[SkillManager] Command parse error: ${err.message}`);
434
- // Commander 解析失败时,用简单方式从 rawArgs 提取选项值
435
- const result = {};
436
- const tokens = parseShellArgs(rawArgs);
437
- let i = 0;
438
- while (i < tokens.length) {
439
- const token = tokens[i];
440
- // 匹配选项(-x 或 --xxx)
441
- const optMatch = token.match(/^(-[a-zA-Z]|-{1,2}[a-zA-Z][a-zA-Z0-9-]*)$/);
442
- if (optMatch) {
443
- const optKey = optMatch[1];
444
- // 找对应的 option
445
- const opt = cmd.options.find(o => {
446
- const flags = o.flags || '';
447
- return flags.includes(optKey) || flags.includes(optKey.replace(/^--?/, '--'));
448
- });
449
- if (opt) {
450
- const name = extractOptionName(opt.flags);
451
- // 判断是否有值(下一个 token 不是选项)
452
- const hasValue = /<[^>]+>/.test(opt.flags);
453
- if (hasValue && i + 1 < tokens.length) {
454
- const next = tokens[i + 1];
455
- if (!next.match(/^-/)) {
456
- result[name] = next;
457
- i += 2;
458
- continue;
459
- } else {
460
- // 下个 token 是另一个选项,但当前选项需要值,用默认值
461
- result[name] = opt.defaultValue ?? undefined;
462
- i += 1;
463
- continue;
464
- }
465
- } else if (!hasValue) {
466
- result[name] = true;
467
- i += 1;
468
- continue;
469
- } else if (hasValue && i + 1 >= tokens.length) {
470
- // 当前选项需要值但没有更多 token,用默认值
471
- result[name] = opt.defaultValue ?? undefined;
472
- i += 1;
473
- continue;
474
- }
475
- }
476
- }
477
- i += 1;
478
- }
479
- // 剩余的 token 当 args
480
- result.args = tokens.slice(i);
481
- return result;
482
- }
483
- };
484
- }
485
-
486
- // 直接使用 command 字符串的 schema,describe 说明命令行格式
487
- const exampleArgs = cmd.options && cmd.options.length > 0
488
- ? buildExampleFromOptions(cmd.options)
489
- : '';
526
+ // 新模式:结构化参数 schema,让 LLM 直接用 JSON 对象调用
490
527
  const toolDef = {
491
528
  name: cmd.name,
492
529
  description: cmd.description || 'Skill command',
493
- inputSchema: z.object({
494
- command: z.string().optional().describe(exampleArgs || '命令行字符串,如:-n value1 -v'),
495
- }),
530
+ // 直接用 JSON Schema,不走 Zod(让 LLM 看到清晰的结构化参数)
531
+ inputSchema: buildInputSchemaFromOptions(cmd.options),
496
532
  _options: cmd.options || null,
497
- // 把原始 options 数组也保存下来,便于 ext_skill 显示
498
- _rawSchema: buildRawJsonSchema(cmd.options),
499
533
  execute: async (toolArgs, framework) => {
500
534
  const ctx = {
501
535
  sessionId: framework?._currentSessionId || 'unknown',
@@ -503,13 +537,13 @@ class Skill {
503
537
  framework: framework,
504
538
  cwd: framework?.getCwd?.() ?? process.cwd(),
505
539
  };
506
- // 直接使用 command 字符串
507
- const commandString = toolArgs?.command || '';
508
- let parsedArgs = commandString;
509
- if (typeof argumentParser === 'function') {
510
- parsedArgs = argumentParser(commandString);
540
+ // backward compatible:如果 args.command 是字符串(messaging 插件传来),
541
+ // options 做简单解析;否则直接传递结构化参数(AI SDK / ext_call 传来)
542
+ let args = toolArgs || {};
543
+ if (typeof args.command === 'string' && Array.isArray(cmd.options) && cmd.options.length > 0) {
544
+ args = parseCommandArgs(args.command, cmd.options);
511
545
  }
512
- return await handler(parsedArgs, ctx);
546
+ return await handler(args, ctx);
513
547
  },
514
548
  };
515
549
 
@@ -1283,72 +1317,12 @@ class SkillManagerPlugin extends Plugin {
1283
1317
 
1284
1318
  const fullName = `${name}:${cmd.name}`;
1285
1319
 
1286
- // 获取参数解析器
1287
- let argumentParser = cmd.argumentParser;
1288
- if (!argumentParser && Array.isArray(cmd.options) && cmd.options.length > 0) {
1289
- argumentParser = (rawArgs) => {
1290
- try {
1291
- const program = new Command();
1292
- program.exitOverride();
1293
- program.configureOutput({ writeErr: () => {} });
1294
- for (const opt of cmd.options) {
1295
- program.option(opt.flags, opt.description, opt.defaultValue);
1296
- }
1297
- program.arguments('[args...]');
1298
- program.parse(['node', 'cmd', ...parseShellArgs(rawArgs)]);
1299
- return { ...program.opts(), args: program.args };
1300
- } catch (err) {
1301
- log.warn(`[SkillManager] Command parse error: ${err.message}`);
1302
- // Commander 解析失败时,用简单方式从 rawArgs 提取选项值
1303
- const result = {};
1304
- const tokens = parseShellArgs(rawArgs);
1305
- let i = 0;
1306
- while (i < tokens.length) {
1307
- const token = tokens[i];
1308
- const optMatch = token.match(/^(-[a-zA-Z]|-{1,2}[a-zA-Z][a-zA-Z0-9-]*)$/);
1309
- if (optMatch) {
1310
- const optKey = optMatch[1];
1311
- const opt = cmd.options.find(o => {
1312
- const flags = o.flags || '';
1313
- return flags.includes(optKey) || flags.includes(optKey.replace(/^--?/, '--'));
1314
- });
1315
- if (opt) {
1316
- const name = extractOptionName(opt.flags);
1317
- const hasValue = /<[^>]+>/.test(opt.flags);
1318
- if (hasValue && i + 1 < tokens.length) {
1319
- const next = tokens[i + 1];
1320
- if (!next.match(/^-/)) {
1321
- result[name] = next;
1322
- i += 2;
1323
- continue;
1324
- }
1325
- } else if (!hasValue) {
1326
- result[name] = true;
1327
- i += 1;
1328
- continue;
1329
- }
1330
- }
1331
- }
1332
- i += 1;
1333
- }
1334
- result.args = tokens.slice(i);
1335
- return result;
1336
- }
1337
- };
1338
- }
1339
-
1340
- // 转为 Zod schema(统一风格)
1341
- const exampleArgs2 = cmd.options && cmd.options.length > 0
1342
- ? buildExampleFromOptions(cmd.options)
1343
- : '';
1320
+ // 新模式:结构化参数 schema
1344
1321
  const toolDef = {
1345
- name: cmd.name, // 只用命令名
1322
+ name: cmd.name,
1346
1323
  description: cmd.description || 'Skill command',
1347
- inputSchema: z.object({
1348
- command: z.string().optional().describe(exampleArgs2 || '命令行字符串'),
1349
- }),
1324
+ inputSchema: buildInputSchemaFromOptions(cmd.options),
1350
1325
  _options: cmd.options || null,
1351
- _rawSchema: buildRawJsonSchema(cmd.options),
1352
1326
  execute: async (toolArgs, framework) => {
1353
1327
  const ctx = {
1354
1328
  sessionId: framework?._currentSessionId || 'unknown',
@@ -1356,12 +1330,12 @@ class SkillManagerPlugin extends Plugin {
1356
1330
  framework: framework,
1357
1331
  cwd: framework?.getCwd?.() ?? process.cwd(),
1358
1332
  };
1359
- const commandString = toolArgs?.command || '';
1360
- let parsedArgs = commandString;
1361
- if (typeof argumentParser === 'function') {
1362
- parsedArgs = argumentParser(commandString);
1333
+ // backward compatible:如果 args.command 是字符串,解析为结构化对象
1334
+ let args = toolArgs || {};
1335
+ if (typeof args.command === 'string' && Array.isArray(cmd.options) && cmd.options.length > 0) {
1336
+ args = parseCommandArgs(args.command, cmd.options);
1363
1337
  }
1364
- return await handler(parsedArgs, ctx);
1338
+ return await handler(args, ctx);
1365
1339
  },
1366
1340
  };
1367
1341