codexmate 0.0.13 → 0.0.14

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.
package/cli.js CHANGED
@@ -21,6 +21,8 @@ const {
21
21
  detectLineEnding,
22
22
  normalizeLineEnding,
23
23
  isValidProviderName,
24
+ escapeTomlBasicString,
25
+ buildModelProviderTableHeader,
24
26
  buildModelsCandidates,
25
27
  isValidHttpUrl,
26
28
  normalizeBaseUrl,
@@ -54,6 +56,10 @@ const {
54
56
  resolveMaxMessagesValue
55
57
  } = require('./lib/cli-session-utils');
56
58
  const { createMcpStdioServer } = require('./lib/mcp-stdio');
59
+ const {
60
+ validateWorkflowDefinition,
61
+ executeWorkflowDefinition
62
+ } = require('./lib/workflow-engine');
57
63
 
58
64
  const DEFAULT_WEB_PORT = 3737;
59
65
  const DEFAULT_WEB_HOST = '127.0.0.1';
@@ -78,6 +84,8 @@ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
78
84
  const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
79
85
  const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
80
86
  const RECENT_CONFIGS_FILE = path.join(CONFIG_DIR, 'recent-configs.json');
87
+ const WORKFLOW_DEFINITIONS_FILE = path.join(CONFIG_DIR, 'codexmate-workflows.json');
88
+ const WORKFLOW_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-workflow-runs.jsonl');
81
89
  const DEFAULT_CLAUDE_MODEL = 'glm-4.7';
82
90
  const CODEX_BACKUP_NAME = 'codex-config';
83
91
 
@@ -98,6 +106,15 @@ const SESSION_SCAN_FACTOR = 4;
98
106
  const SESSION_SCAN_MIN_FILES = 800;
99
107
  const MAX_SESSION_PATH_LIST_SIZE = 2000;
100
108
  const AGENTS_FILE_NAME = 'AGENTS.md';
109
+ const CODEX_SKILLS_DIR = path.join(CONFIG_DIR, 'skills');
110
+ const CLAUDE_SKILLS_DIR = path.join(CLAUDE_DIR, 'skills');
111
+ const GEMINI_SKILLS_DIR = path.join(os.homedir(), '.gemini', 'skills');
112
+ const OPENCODE_SKILLS_DIR = path.join(os.homedir(), '.opencode', 'skills');
113
+ const SKILL_IMPORT_SOURCES = Object.freeze([
114
+ { app: 'claude', label: 'Claude Code', dir: CLAUDE_SKILLS_DIR },
115
+ { app: 'gemini', label: 'Gemini CLI', dir: GEMINI_SKILLS_DIR },
116
+ { app: 'opencode', label: 'OpenCode', dir: OPENCODE_SKILLS_DIR }
117
+ ]);
101
118
  const MODELS_CACHE_TTL_MS = 60 * 1000;
102
119
  const MODELS_NEGATIVE_CACHE_TTL_MS = 5 * 1000;
103
120
  const MODELS_CACHE_MAX_ENTRIES = 50;
@@ -215,16 +232,50 @@ function ensureConfigDir() {
215
232
  }
216
233
  }
217
234
 
235
+ function createConfigLoadError(type, message, detail) {
236
+ const err = new Error(detail || message);
237
+ err.configErrorType = type || 'read';
238
+ err.configPublicReason = message || '读取 config.toml 失败';
239
+ err.configDetail = detail || message || '';
240
+ return err;
241
+ }
242
+
218
243
  function readConfig() {
219
244
  if (!fs.existsSync(CONFIG_FILE)) {
220
- throw new Error(`配置文件不存在: ${CONFIG_FILE}`);
245
+ throw createConfigLoadError(
246
+ 'missing',
247
+ '未检测到 config.toml',
248
+ `配置文件不存在: ${CONFIG_FILE}`
249
+ );
250
+ }
251
+
252
+ let content = '';
253
+ try {
254
+ content = fs.readFileSync(CONFIG_FILE, 'utf-8');
255
+ } catch (e) {
256
+ throw createConfigLoadError(
257
+ 'read',
258
+ '读取 config.toml 失败',
259
+ `读取配置文件失败: ${e && e.message ? e.message : e}`
260
+ );
221
261
  }
262
+
263
+ let parsed;
222
264
  try {
223
- const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
224
- return toml.parse(content);
265
+ parsed = toml.parse(content);
225
266
  } catch (e) {
226
- throw new Error(`配置文件解析失败: ${e.message}`);
267
+ throw createConfigLoadError(
268
+ 'parse',
269
+ 'config.toml 解析失败',
270
+ `配置文件解析失败: ${e && e.message ? e.message : e}`
271
+ );
272
+ }
273
+
274
+ if (isPlainObject(parsed) && isPlainObject(parsed.model_providers)) {
275
+ const providerHeaderSegmentKeySet = collectModelProviderHeaderSegmentKeySet(content);
276
+ parsed.model_providers = normalizeLegacyModelProviders(parsed.model_providers, providerHeaderSegmentKeySet);
227
277
  }
278
+ return parsed;
228
279
  }
229
280
 
230
281
  function writeConfig(content) {
@@ -277,6 +328,519 @@ function isPlainObject(value) {
277
328
  return !!value && typeof value === 'object' && !Array.isArray(value);
278
329
  }
279
330
 
331
+ const PROVIDER_CONFIG_KEYS = new Set([
332
+ 'name',
333
+ 'base_url',
334
+ 'wire_api',
335
+ 'requires_openai_auth',
336
+ 'preferred_auth_method',
337
+ 'request_max_retries',
338
+ 'stream_max_retries',
339
+ 'stream_idle_timeout_ms'
340
+ ]);
341
+ const RECOVERABLE_PROVIDER_SIGNAL_KEYS = [...PROVIDER_CONFIG_KEYS].filter((key) => key !== 'name' && key !== 'base_url');
342
+
343
+ function looksLikeProviderConfig(value) {
344
+ if (!isPlainObject(value)) return false;
345
+ return Object.keys(value).some((key) => PROVIDER_CONFIG_KEYS.has(key));
346
+ }
347
+
348
+ function isRecoverableNestedProviderConfig(value) {
349
+ if (!isPlainObject(value)) return false;
350
+ const hasBaseUrl = typeof value.base_url === 'string' && value.base_url.trim() !== '';
351
+ if (!hasBaseUrl) return false;
352
+ const hasName = typeof value.name === 'string' && value.name.trim() !== '';
353
+ const hasProviderSignals = RECOVERABLE_PROVIDER_SIGNAL_KEYS.some((key) => Object.prototype.hasOwnProperty.call(value, key));
354
+ return hasName || hasProviderSignals;
355
+ }
356
+
357
+ function collectNestedProviderConfigs(node, pathSegments, collector) {
358
+ if (!isPlainObject(node)) return;
359
+ const segments = Array.isArray(pathSegments) ? pathSegments : [String(pathSegments || '')];
360
+ const lastSegment = segments.length > 0 ? segments[segments.length - 1] : '';
361
+ if (segments.length > 1 && lastSegment === 'metadata') {
362
+ return;
363
+ }
364
+ if (isRecoverableNestedProviderConfig(node)) {
365
+ collector.push({
366
+ name: segments.join('.'),
367
+ segments: segments.slice(),
368
+ provider: node
369
+ });
370
+ }
371
+ for (const [childKey, childValue] of Object.entries(node)) {
372
+ if (!isPlainObject(childValue)) continue;
373
+ collectNestedProviderConfigs(childValue, [...segments, childKey], collector);
374
+ }
375
+ }
376
+
377
+ function normalizeLegacySegments(segments) {
378
+ if (!Array.isArray(segments) || segments.length === 0) return null;
379
+ return segments.map((item) => String(item));
380
+ }
381
+
382
+ function buildLegacySegmentsKey(segments) {
383
+ const normalized = normalizeLegacySegments(segments);
384
+ return normalized ? JSON.stringify(normalized) : '';
385
+ }
386
+
387
+ function appendLegacySegmentsVariant(provider, segments) {
388
+ if (!isPlainObject(provider)) return;
389
+ const normalized = normalizeLegacySegments(segments);
390
+ if (!normalized) return;
391
+
392
+ const variants = [];
393
+ const seen = new Set();
394
+ const pushVariant = (candidate) => {
395
+ const key = buildLegacySegmentsKey(candidate);
396
+ if (!key || seen.has(key)) return;
397
+ seen.add(key);
398
+ variants.push(normalizeLegacySegments(candidate));
399
+ };
400
+
401
+ if (Array.isArray(provider.__codexmate_legacy_segments)) {
402
+ pushVariant(provider.__codexmate_legacy_segments);
403
+ }
404
+ if (Array.isArray(provider.__codexmate_legacy_segment_variants)) {
405
+ for (const candidate of provider.__codexmate_legacy_segment_variants) {
406
+ pushVariant(candidate);
407
+ }
408
+ }
409
+ pushVariant(normalized);
410
+
411
+ try {
412
+ if (!Array.isArray(provider.__codexmate_legacy_segments)) {
413
+ Object.defineProperty(provider, '__codexmate_legacy_segments', {
414
+ value: normalized,
415
+ enumerable: false,
416
+ configurable: true,
417
+ writable: true
418
+ });
419
+ }
420
+ Object.defineProperty(provider, '__codexmate_legacy_segment_variants', {
421
+ value: variants,
422
+ enumerable: false,
423
+ configurable: true,
424
+ writable: true
425
+ });
426
+ } catch (e) {}
427
+ }
428
+
429
+ function setLegacySegmentsMetadata(provider, segments) {
430
+ appendLegacySegmentsVariant(provider, segments);
431
+ }
432
+
433
+ function normalizeLegacyModelProviders(modelProviders, providerHeaderSegmentKeySet = null) {
434
+ if (!isPlainObject(modelProviders)) {
435
+ return modelProviders;
436
+ }
437
+
438
+ let changed = false;
439
+ const normalized = {};
440
+ const addRecovered = (entry) => {
441
+ const name = entry && typeof entry.name === 'string' ? entry.name : '';
442
+ const segments = entry && Array.isArray(entry.segments) ? entry.segments.slice() : null;
443
+ const provider = entry ? entry.provider : null;
444
+ if (!name || !isPlainObject(provider)) return;
445
+ const segmentKey = buildLegacySegmentsKey(segments);
446
+ if (providerHeaderSegmentKeySet instanceof Set && segmentKey && !providerHeaderSegmentKeySet.has(segmentKey)) {
447
+ return;
448
+ }
449
+ const existing = Object.prototype.hasOwnProperty.call(normalized, name)
450
+ ? normalized[name]
451
+ : (Object.prototype.hasOwnProperty.call(modelProviders, name) ? modelProviders[name] : null);
452
+ if (isPlainObject(existing)) {
453
+ if (!Array.isArray(existing.__codexmate_legacy_segments)) {
454
+ setLegacySegmentsMetadata(existing, [name]);
455
+ }
456
+ appendLegacySegmentsVariant(existing, segments);
457
+ return;
458
+ }
459
+ if (Object.prototype.hasOwnProperty.call(modelProviders, name)) return;
460
+ if (Object.prototype.hasOwnProperty.call(normalized, name)) return;
461
+ setLegacySegmentsMetadata(provider, segments);
462
+ normalized[name] = provider;
463
+ changed = true;
464
+ };
465
+
466
+ for (const [name, provider] of Object.entries(modelProviders)) {
467
+ normalized[name] = provider;
468
+ if (!isPlainObject(provider)) continue;
469
+
470
+ if (looksLikeProviderConfig(provider)) {
471
+ setLegacySegmentsMetadata(provider, [name]);
472
+ for (const [childKey, childValue] of Object.entries(provider)) {
473
+ if (!isPlainObject(childValue)) continue;
474
+ const recovered = [];
475
+ collectNestedProviderConfigs(childValue, [name, childKey], recovered);
476
+ for (const recoveredEntry of recovered) {
477
+ addRecovered(recoveredEntry);
478
+ }
479
+ }
480
+ continue;
481
+ }
482
+
483
+ const recovered = [];
484
+ collectNestedProviderConfigs(provider, [name], recovered);
485
+ delete normalized[name];
486
+ changed = true;
487
+ for (const recoveredEntry of recovered) {
488
+ addRecovered(recoveredEntry);
489
+ }
490
+ }
491
+
492
+ return changed ? normalized : modelProviders;
493
+ }
494
+
495
+ function escapeRegex(value) {
496
+ return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
497
+ }
498
+
499
+ function areStringArraysEqual(a, b) {
500
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
501
+ for (let i = 0; i < a.length; i++) {
502
+ if (String(a[i]) !== String(b[i])) return false;
503
+ }
504
+ return true;
505
+ }
506
+
507
+ function parseTomlDottedKeyExpression(expression) {
508
+ const text = String(expression || '');
509
+ let index = 0;
510
+ const segments = [];
511
+ const skipWhitespace = () => {
512
+ while (index < text.length && /\s/.test(text[index])) index++;
513
+ };
514
+
515
+ while (index < text.length) {
516
+ skipWhitespace();
517
+ if (index >= text.length) break;
518
+
519
+ const ch = text[index];
520
+ if (ch === "'") {
521
+ const end = text.indexOf("'", index + 1);
522
+ if (end === -1) return null;
523
+ segments.push(text.slice(index + 1, end));
524
+ index = end + 1;
525
+ } else if (ch === '"') {
526
+ index += 1;
527
+ let value = '';
528
+ let closed = false;
529
+ while (index < text.length) {
530
+ const cur = text[index];
531
+ if (cur === '"') {
532
+ index += 1;
533
+ closed = true;
534
+ break;
535
+ }
536
+ if (cur !== '\\') {
537
+ value += cur;
538
+ index += 1;
539
+ continue;
540
+ }
541
+ if (index + 1 >= text.length) return null;
542
+ const esc = text[index + 1];
543
+ if (esc === 'u' || esc === 'U') {
544
+ const hexLen = esc === 'u' ? 4 : 8;
545
+ const hex = text.slice(index + 2, index + 2 + hexLen);
546
+ if (!/^[0-9a-fA-F]+$/.test(hex)) return null;
547
+ try {
548
+ value += String.fromCodePoint(parseInt(hex, 16));
549
+ } catch (e) {
550
+ return null;
551
+ }
552
+ index += 2 + hexLen;
553
+ continue;
554
+ }
555
+ const unescaped = {
556
+ b: '\b',
557
+ t: '\t',
558
+ n: '\n',
559
+ f: '\f',
560
+ r: '\r',
561
+ '"': '"',
562
+ '\\': '\\'
563
+ }[esc];
564
+ if (unescaped === undefined) return null;
565
+ value += unescaped;
566
+ index += 2;
567
+ }
568
+ if (!closed) return null;
569
+ segments.push(value);
570
+ } else {
571
+ const start = index;
572
+ while (index < text.length && !/\s|\./.test(text[index])) index++;
573
+ const bare = text.slice(start, index);
574
+ if (!bare) return null;
575
+ segments.push(bare);
576
+ }
577
+
578
+ skipWhitespace();
579
+ if (index >= text.length) break;
580
+ if (text[index] !== '.') return null;
581
+ index += 1;
582
+ }
583
+
584
+ return segments.length > 0 ? segments : null;
585
+ }
586
+
587
+ function collectTomlMultilineStringRanges(text) {
588
+ const source = typeof text === 'string' ? text : '';
589
+ const ranges = [];
590
+ let i = 0;
591
+ let inMultilineBasic = false;
592
+ let inMultilineLiteral = false;
593
+ let rangeStart = -1;
594
+
595
+ while (i < source.length) {
596
+ if (inMultilineBasic) {
597
+ if (source.slice(i, i + 3) === '"""') {
598
+ let slashCount = 0;
599
+ for (let j = i - 1; j >= 0 && source[j] === '\\'; j--) {
600
+ slashCount++;
601
+ }
602
+ if (slashCount % 2 === 0) {
603
+ let runEnd = i + 3;
604
+ while (runEnd < source.length && source[runEnd] === '"') runEnd++;
605
+ ranges.push({ start: rangeStart, end: runEnd });
606
+ inMultilineBasic = false;
607
+ rangeStart = -1;
608
+ i = runEnd;
609
+ continue;
610
+ }
611
+ }
612
+ i++;
613
+ continue;
614
+ }
615
+
616
+ if (inMultilineLiteral) {
617
+ if (source.slice(i, i + 3) === "'''") {
618
+ let runEnd = i + 3;
619
+ while (runEnd < source.length && source[runEnd] === '\'') runEnd++;
620
+ ranges.push({ start: rangeStart, end: runEnd });
621
+ inMultilineLiteral = false;
622
+ rangeStart = -1;
623
+ i = runEnd;
624
+ continue;
625
+ }
626
+ i++;
627
+ continue;
628
+ }
629
+
630
+ const ch = source[i];
631
+ if (ch === '#') {
632
+ while (i < source.length && source[i] !== '\n') i++;
633
+ continue;
634
+ }
635
+
636
+ if (source.slice(i, i + 3) === '"""') {
637
+ inMultilineBasic = true;
638
+ rangeStart = i;
639
+ i += 3;
640
+ continue;
641
+ }
642
+
643
+ if (source.slice(i, i + 3) === "'''") {
644
+ inMultilineLiteral = true;
645
+ rangeStart = i;
646
+ i += 3;
647
+ continue;
648
+ }
649
+
650
+ if (ch === '"') {
651
+ i++;
652
+ while (i < source.length) {
653
+ if (source[i] === '\\') {
654
+ i += 2;
655
+ continue;
656
+ }
657
+ if (source[i] === '"' || source[i] === '\n') {
658
+ i++;
659
+ break;
660
+ }
661
+ i++;
662
+ }
663
+ continue;
664
+ }
665
+
666
+ if (ch === '\'') {
667
+ i++;
668
+ while (i < source.length) {
669
+ if (source[i] === '\'' || source[i] === '\n') {
670
+ i++;
671
+ break;
672
+ }
673
+ i++;
674
+ }
675
+ continue;
676
+ }
677
+
678
+ i++;
679
+ }
680
+
681
+ if (rangeStart >= 0) {
682
+ ranges.push({ start: rangeStart, end: source.length });
683
+ }
684
+ return ranges;
685
+ }
686
+
687
+ function isIndexInRanges(index, ranges) {
688
+ for (const range of ranges) {
689
+ if (index < range.start) return false;
690
+ if (index >= range.start && index < range.end) return true;
691
+ }
692
+ return false;
693
+ }
694
+
695
+ function findProviderSectionRanges(content, providerName, exactSegments = null) {
696
+ const text = typeof content === 'string' ? content : '';
697
+ const name = typeof providerName === 'string' ? providerName.trim() : '';
698
+ const targetSegments = Array.isArray(exactSegments) ? exactSegments.map((item) => String(item)) : null;
699
+ if (!text || !name) return [];
700
+
701
+ const safeName = escapeRegex(name);
702
+ const headerPatterns = [
703
+ { priority: 0, regex: new RegExp(`^\\s*model_providers\\s*\\.\\s*"${safeName}"\\s*$`) },
704
+ { priority: 1, regex: new RegExp(`^\\s*model_providers\\s*\\.\\s*'${safeName}'\\s*$`) },
705
+ { priority: 2, regex: new RegExp(`^\\s*model_providers\\s*\\.\\s*${safeName}\\s*$`) }
706
+ ];
707
+
708
+ const allHeaders = [];
709
+ const targetPriorityByStart = new Map();
710
+ const multilineStringRanges = collectTomlMultilineStringRanges(text);
711
+ const sectionLineRegex = /^[ \t]*\[(?!\[)([^\]\n]+)\][ \t]*(?:#.*)?$/gm;
712
+ let match;
713
+ while ((match = sectionLineRegex.exec(text)) !== null) {
714
+ const start = match.index;
715
+ if (isIndexInRanges(start, multilineStringRanges)) {
716
+ continue;
717
+ }
718
+ allHeaders.push(start);
719
+ const headerExpr = String(match[1] || '').trim();
720
+
721
+ const parsedSegments = parseTomlDottedKeyExpression(headerExpr);
722
+ if (Array.isArray(parsedSegments) && parsedSegments.length >= 2 && parsedSegments[0] === 'model_providers') {
723
+ const providerSegments = parsedSegments.slice(1);
724
+ if (targetSegments && targetSegments.length > 0 && areStringArraysEqual(providerSegments, targetSegments)) {
725
+ const prev = targetPriorityByStart.get(start);
726
+ if (prev === undefined || -3 < prev) {
727
+ targetPriorityByStart.set(start, -3);
728
+ }
729
+ continue;
730
+ }
731
+ if (!targetSegments || targetSegments.length === 0) {
732
+ const parsedName = providerSegments.join('.');
733
+ if (parsedName === name) {
734
+ const prev = targetPriorityByStart.get(start);
735
+ if (prev === undefined || -2 < prev) {
736
+ targetPriorityByStart.set(start, -2);
737
+ }
738
+ continue;
739
+ }
740
+ }
741
+ }
742
+
743
+ for (const pattern of headerPatterns) {
744
+ if (pattern.regex.test(headerExpr)) {
745
+ const prev = targetPriorityByStart.get(start);
746
+ if (prev === undefined || pattern.priority < prev) {
747
+ targetPriorityByStart.set(start, pattern.priority);
748
+ }
749
+ break;
750
+ }
751
+ }
752
+ }
753
+
754
+ if (targetPriorityByStart.size === 0) {
755
+ return [];
756
+ }
757
+
758
+ const ranges = [];
759
+ for (let i = 0; i < allHeaders.length; i++) {
760
+ const start = allHeaders[i];
761
+ if (!targetPriorityByStart.has(start)) continue;
762
+ const end = i + 1 < allHeaders.length ? allHeaders[i + 1] : text.length;
763
+ ranges.push({
764
+ start,
765
+ end,
766
+ priority: targetPriorityByStart.get(start)
767
+ });
768
+ }
769
+ const exactMatches = ranges.filter((range) => range.priority === -3);
770
+ return exactMatches.length > 0 ? exactMatches : ranges;
771
+ }
772
+
773
+ function doesSegmentsStartWith(segments, prefix) {
774
+ if (!Array.isArray(segments) || !Array.isArray(prefix) || prefix.length === 0 || segments.length < prefix.length) {
775
+ return false;
776
+ }
777
+ for (let i = 0; i < prefix.length; i++) {
778
+ if (String(segments[i]) !== String(prefix[i])) return false;
779
+ }
780
+ return true;
781
+ }
782
+
783
+ function findProviderDescendantSectionRanges(content, prefixSegments) {
784
+ const text = typeof content === 'string' ? content : '';
785
+ const prefix = Array.isArray(prefixSegments) ? prefixSegments.map((item) => String(item)) : [];
786
+ if (!text || prefix.length === 0) return [];
787
+
788
+ const allHeaders = [];
789
+ const parsedProviderSegmentsByStart = new Map();
790
+ const multilineStringRanges = collectTomlMultilineStringRanges(text);
791
+ const sectionLineRegex = /^[ \t]*\[(?!\[)([^\]\n]+)\][ \t]*(?:#.*)?$/gm;
792
+ let match;
793
+ while ((match = sectionLineRegex.exec(text)) !== null) {
794
+ const start = match.index;
795
+ if (isIndexInRanges(start, multilineStringRanges)) {
796
+ continue;
797
+ }
798
+ allHeaders.push(start);
799
+ const headerExpr = String(match[1] || '').trim();
800
+ const parsedSegments = parseTomlDottedKeyExpression(headerExpr);
801
+ if (!Array.isArray(parsedSegments) || parsedSegments.length < 2 || parsedSegments[0] !== 'model_providers') {
802
+ continue;
803
+ }
804
+ parsedProviderSegmentsByStart.set(start, parsedSegments.slice(1));
805
+ }
806
+
807
+ const ranges = [];
808
+ for (let i = 0; i < allHeaders.length; i++) {
809
+ const start = allHeaders[i];
810
+ const providerSegments = parsedProviderSegmentsByStart.get(start);
811
+ if (!providerSegments) continue;
812
+ if (!doesSegmentsStartWith(providerSegments, prefix)) continue;
813
+ if (providerSegments.length <= prefix.length) continue;
814
+ const end = i + 1 < allHeaders.length ? allHeaders[i + 1] : text.length;
815
+ ranges.push({ start, end, priority: 0 });
816
+ }
817
+ return ranges;
818
+ }
819
+
820
+ function collectModelProviderHeaderSegmentKeySet(content) {
821
+ const text = typeof content === 'string' ? content : '';
822
+ const keys = new Set();
823
+ if (!text) return keys;
824
+
825
+ const multilineStringRanges = collectTomlMultilineStringRanges(text);
826
+ const sectionLineRegex = /^[ \t]*\[(?!\[)([^\]\n]+)\][ \t]*(?:#.*)?$/gm;
827
+ let match;
828
+ while ((match = sectionLineRegex.exec(text)) !== null) {
829
+ const start = match.index;
830
+ if (isIndexInRanges(start, multilineStringRanges)) {
831
+ continue;
832
+ }
833
+ const headerExpr = String(match[1] || '').trim();
834
+ const parsedSegments = parseTomlDottedKeyExpression(headerExpr);
835
+ if (!Array.isArray(parsedSegments) || parsedSegments.length < 2 || parsedSegments[0] !== 'model_providers') {
836
+ continue;
837
+ }
838
+ const key = buildLegacySegmentsKey(parsedSegments.slice(1));
839
+ if (key) keys.add(key);
840
+ }
841
+ return keys;
842
+ }
843
+
280
844
  function normalizeAuthProfileName(value) {
281
845
  const raw = typeof value === 'string' ? value.trim() : '';
282
846
  if (!raw) return '';
@@ -790,78 +1354,620 @@ function validateAgentsBaseDir(filePath) {
790
1354
  return { ok: true, dirPath };
791
1355
  }
792
1356
 
793
- function readAgentsFile(params = {}) {
794
- const filePath = resolveAgentsFilePath(params);
795
- const dirCheck = validateAgentsBaseDir(filePath);
796
- if (dirCheck.error) {
797
- return { error: dirCheck.error };
1357
+ function normalizeCodexSkillName(name) {
1358
+ const value = typeof name === 'string' ? name.trim() : '';
1359
+ if (!value) {
1360
+ return { error: '技能名称不能为空' };
798
1361
  }
799
-
800
- if (!fs.existsSync(filePath)) {
801
- return {
802
- exists: false,
803
- path: filePath,
804
- content: '',
805
- lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n'
806
- };
1362
+ if (value.includes('\0')) {
1363
+ return { error: '技能名称非法' };
807
1364
  }
808
-
809
- try {
810
- const raw = fs.readFileSync(filePath, 'utf-8');
811
- return {
812
- exists: true,
813
- path: filePath,
814
- content: stripUtf8Bom(raw),
815
- lineEnding: detectLineEnding(raw)
816
- };
817
- } catch (e) {
818
- return { error: `读取 AGENTS.md 失败: ${e.message}` };
1365
+ if (value === '.' || value === '..') {
1366
+ return { error: '技能名称非法' };
819
1367
  }
820
- }
821
-
822
- function applyAgentsFile(params = {}) {
823
- const filePath = resolveAgentsFilePath(params);
824
- const dirCheck = validateAgentsBaseDir(filePath);
825
- if (dirCheck.error) {
826
- return { error: dirCheck.error };
1368
+ if (value.includes('/') || value.includes('\\')) {
1369
+ return { error: '技能名称非法' };
827
1370
  }
1371
+ if (path.basename(value) !== value) {
1372
+ return { error: '技能名称非法' };
1373
+ }
1374
+ if (value.startsWith('.')) {
1375
+ return { error: '系统技能不可删除' };
1376
+ }
1377
+ return { name: value };
1378
+ }
828
1379
 
829
- const content = typeof params.content === 'string' ? params.content : '';
830
- const lineEnding = params.lineEnding === '\r\n' ? '\r\n' : '\n';
831
- const normalized = normalizeLineEnding(content, lineEnding);
832
- const finalContent = ensureUtf8Bom(normalized);
833
-
1380
+ function isSkillDirectoryEntry(entryName) {
1381
+ const targetPath = path.join(CODEX_SKILLS_DIR, entryName);
834
1382
  try {
835
- fs.writeFileSync(filePath, finalContent, 'utf-8');
836
- return { success: true, path: filePath };
1383
+ const stat = fs.statSync(targetPath);
1384
+ return stat.isDirectory();
837
1385
  } catch (e) {
838
- return { error: `写入 AGENTS.md 失败: ${e.message}` };
1386
+ return false;
839
1387
  }
840
1388
  }
841
1389
 
842
- function resolveOpenclawWorkspaceDir(config) {
843
- const workspace = config
844
- && config.agents
845
- && config.agents.defaults
846
- && typeof config.agents.defaults.workspace === 'string'
847
- ? config.agents.defaults.workspace
848
- : '';
849
- const resolved = resolveHomePath(workspace);
850
- if (!resolved) {
851
- return OPENCLAW_WORKSPACE_DIR;
852
- }
853
- if (path.isAbsolute(resolved)) {
854
- return resolved;
855
- }
856
- return path.join(OPENCLAW_DIR, resolved);
1390
+ function normalizeSkillImportSourceApp(app) {
1391
+ const value = typeof app === 'string' ? app.trim().toLowerCase() : '';
1392
+ return SKILL_IMPORT_SOURCES.some((item) => item.app === value) ? value : '';
857
1393
  }
858
1394
 
859
- function normalizeOpenclawWorkspaceFileName(input) {
860
- const raw = typeof input === 'string' ? input.trim() : '';
861
- if (!raw) {
862
- return { error: '文件名不能为空' };
1395
+ function getSkillImportSourceByApp(app) {
1396
+ const normalizedApp = normalizeSkillImportSourceApp(app);
1397
+ if (!normalizedApp) return null;
1398
+ return SKILL_IMPORT_SOURCES.find((item) => item.app === normalizedApp) || null;
1399
+ }
1400
+
1401
+ function parseSimpleSkillFrontmatter(content = '') {
1402
+ const normalized = String(content || '').replace(/\r\n/g, '\n');
1403
+ if (!normalized.startsWith('---\n')) {
1404
+ return {};
863
1405
  }
864
- if (raw.includes('\0')) {
1406
+ const endIndex = normalized.indexOf('\n---\n', 4);
1407
+ if (endIndex <= 4) {
1408
+ return {};
1409
+ }
1410
+ const frontmatterRaw = normalized.slice(4, endIndex);
1411
+ const result = {};
1412
+ const lines = frontmatterRaw.split('\n');
1413
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
1414
+ const line = lines[lineIndex];
1415
+ const trimmed = line.trim();
1416
+ if (!trimmed || trimmed.startsWith('#')) continue;
1417
+ const matched = trimmed.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
1418
+ if (!matched) continue;
1419
+ const key = matched[1];
1420
+ let value = matched[2] || '';
1421
+ const indicator = value.trim();
1422
+ if (/^[>|]/.test(indicator)) {
1423
+ const blockLines = [];
1424
+ let cursor = lineIndex + 1;
1425
+ while (cursor < lines.length) {
1426
+ const candidateLine = lines[cursor];
1427
+ if (!candidateLine.trim()) {
1428
+ blockLines.push('');
1429
+ cursor += 1;
1430
+ continue;
1431
+ }
1432
+ if (/^\s/.test(candidateLine)) {
1433
+ blockLines.push(candidateLine);
1434
+ cursor += 1;
1435
+ continue;
1436
+ }
1437
+ break;
1438
+ }
1439
+ lineIndex = cursor - 1;
1440
+ const indents = blockLines
1441
+ .filter((item) => item.trim())
1442
+ .map((item) => {
1443
+ const indentMatch = item.match(/^[ \t]*/);
1444
+ return indentMatch ? indentMatch[0].length : 0;
1445
+ });
1446
+ const commonIndent = indents.length ? Math.min(...indents) : 0;
1447
+ const deindented = blockLines.map((item) => {
1448
+ if (!item.trim()) return '';
1449
+ return item.slice(commonIndent);
1450
+ });
1451
+ if (indicator.startsWith('>')) {
1452
+ const paragraphs = [];
1453
+ let paragraphLines = [];
1454
+ for (const blockLine of deindented) {
1455
+ const blockTrimmed = blockLine.trim();
1456
+ if (!blockTrimmed) {
1457
+ if (paragraphLines.length) {
1458
+ paragraphs.push(paragraphLines.join(' '));
1459
+ paragraphLines = [];
1460
+ }
1461
+ continue;
1462
+ }
1463
+ paragraphLines.push(blockTrimmed);
1464
+ }
1465
+ if (paragraphLines.length) {
1466
+ paragraphs.push(paragraphLines.join(' '));
1467
+ }
1468
+ value = paragraphs.join('\n');
1469
+ } else {
1470
+ value = deindented.join('\n');
1471
+ }
1472
+ }
1473
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''))) {
1474
+ value = value.slice(1, -1);
1475
+ }
1476
+ result[key] = value.trim();
1477
+ }
1478
+ return result;
1479
+ }
1480
+
1481
+ function stripMarkdownFrontmatter(content = '') {
1482
+ const normalized = String(content || '').replace(/\r\n/g, '\n');
1483
+ if (!normalized.startsWith('---\n')) {
1484
+ return normalized;
1485
+ }
1486
+ const endIndex = normalized.indexOf('\n---\n', 4);
1487
+ if (endIndex <= 4) {
1488
+ return normalized;
1489
+ }
1490
+ return normalized.slice(endIndex + 5);
1491
+ }
1492
+
1493
+ function extractSkillDescriptionFromMarkdown(content = '') {
1494
+ const normalized = String(content || '').replace(/\r\n/g, '\n');
1495
+ const lines = normalized.split('\n');
1496
+ let inFence = false;
1497
+ for (const line of lines) {
1498
+ const trimmedStart = line.trimStart();
1499
+ if (trimmedStart.startsWith('```')) {
1500
+ inFence = !inFence;
1501
+ continue;
1502
+ }
1503
+ if (inFence) continue;
1504
+ if (/^( {4}|\t)/.test(line)) continue;
1505
+ const trimmed = line.trim();
1506
+ if (!trimmed) continue;
1507
+ if (trimmed.startsWith('#')) continue;
1508
+ if (trimmed.startsWith('---')) continue;
1509
+ if (/^([A-Za-z0-9_-]+)\s*:\s*/.test(trimmed)) continue;
1510
+ return trimmed.slice(0, 200);
1511
+ }
1512
+ return '';
1513
+ }
1514
+
1515
+ function readCodexSkillMetadata(skillPath) {
1516
+ const skillFile = path.join(skillPath, 'SKILL.md');
1517
+ if (!fs.existsSync(skillFile)) {
1518
+ return {
1519
+ hasSkillFile: false,
1520
+ displayName: '',
1521
+ description: ''
1522
+ };
1523
+ }
1524
+ try {
1525
+ const raw = fs.readFileSync(skillFile, 'utf-8');
1526
+ const content = stripUtf8Bom(raw);
1527
+ const frontmatter = parseSimpleSkillFrontmatter(content);
1528
+ const contentWithoutFrontmatter = stripMarkdownFrontmatter(content);
1529
+ const heading = contentWithoutFrontmatter.match(/^\s*#\s+(.+)$/m);
1530
+ const displayName = typeof frontmatter.name === 'string' && frontmatter.name.trim()
1531
+ ? frontmatter.name.trim()
1532
+ : (heading && heading[1] ? heading[1].trim() : '');
1533
+ const description = typeof frontmatter.description === 'string' && frontmatter.description.trim()
1534
+ ? frontmatter.description.trim().slice(0, 200)
1535
+ : extractSkillDescriptionFromMarkdown(contentWithoutFrontmatter);
1536
+ return {
1537
+ hasSkillFile: true,
1538
+ displayName,
1539
+ description
1540
+ };
1541
+ } catch (e) {
1542
+ return {
1543
+ hasSkillFile: false,
1544
+ displayName: '',
1545
+ description: ''
1546
+ };
1547
+ }
1548
+ }
1549
+
1550
+ function getCodexSkillEntryInfoByName(entryName) {
1551
+ const targetPath = path.join(CODEX_SKILLS_DIR, entryName);
1552
+ const normalized = normalizeCodexSkillName(entryName);
1553
+ if (normalized.error) {
1554
+ return null;
1555
+ }
1556
+ const relativePath = path.relative(CODEX_SKILLS_DIR, targetPath);
1557
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
1558
+ return null;
1559
+ }
1560
+
1561
+ try {
1562
+ const lstat = fs.lstatSync(targetPath);
1563
+ const isSymbolicLink = lstat.isSymbolicLink();
1564
+ if (!lstat.isDirectory() && !isSymbolicLink) {
1565
+ return null;
1566
+ }
1567
+ if (isSymbolicLink && !isSkillDirectoryEntry(entryName)) {
1568
+ return null;
1569
+ }
1570
+ const metadata = readCodexSkillMetadata(targetPath);
1571
+ return {
1572
+ name: entryName,
1573
+ path: targetPath,
1574
+ hasSkillFile: !!metadata.hasSkillFile,
1575
+ displayName: metadata.displayName || entryName,
1576
+ description: metadata.description || '',
1577
+ sourceType: isSymbolicLink ? 'symlink' : 'directory',
1578
+ updatedAt: Number.isFinite(lstat.mtimeMs) ? Math.floor(lstat.mtimeMs) : 0
1579
+ };
1580
+ } catch (e) {
1581
+ return null;
1582
+ }
1583
+ }
1584
+
1585
+ function listCodexSkills() {
1586
+ if (!fs.existsSync(CODEX_SKILLS_DIR)) {
1587
+ return {
1588
+ root: CODEX_SKILLS_DIR,
1589
+ exists: false,
1590
+ items: []
1591
+ };
1592
+ }
1593
+ try {
1594
+ const entries = fs.readdirSync(CODEX_SKILLS_DIR, { withFileTypes: true });
1595
+ const items = entries
1596
+ .map((entry) => {
1597
+ const name = entry && entry.name ? entry.name : '';
1598
+ if (!name || name.startsWith('.')) return null;
1599
+ return getCodexSkillEntryInfoByName(name);
1600
+ })
1601
+ .filter(Boolean)
1602
+ .sort((a, b) => a.displayName.localeCompare(b.displayName, 'zh-Hans-CN'));
1603
+ return {
1604
+ root: CODEX_SKILLS_DIR,
1605
+ exists: true,
1606
+ items
1607
+ };
1608
+ } catch (e) {
1609
+ return { error: `读取 skills 目录失败: ${e.message}` };
1610
+ }
1611
+ }
1612
+
1613
+ function listSkillEntriesByRoot(rootDir) {
1614
+ if (!rootDir || !fs.existsSync(rootDir)) {
1615
+ return [];
1616
+ }
1617
+ try {
1618
+ const entries = fs.readdirSync(rootDir, { withFileTypes: true });
1619
+ return entries
1620
+ .map((entry) => {
1621
+ const name = entry && entry.name ? entry.name : '';
1622
+ if (!name || name.startsWith('.')) return null;
1623
+ const normalized = normalizeCodexSkillName(name);
1624
+ if (normalized.error) return null;
1625
+ const skillPath = path.join(rootDir, name);
1626
+ const relativePath = path.relative(rootDir, skillPath);
1627
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
1628
+ return null;
1629
+ }
1630
+ try {
1631
+ const lstat = fs.lstatSync(skillPath);
1632
+ const isSymbolicLink = lstat.isSymbolicLink();
1633
+ if (!lstat.isDirectory() && !isSymbolicLink) {
1634
+ return null;
1635
+ }
1636
+ if (isSymbolicLink) {
1637
+ const realPath = fs.realpathSync(skillPath);
1638
+ const realStat = fs.statSync(realPath);
1639
+ if (!realStat.isDirectory()) {
1640
+ return null;
1641
+ }
1642
+ }
1643
+ return {
1644
+ name,
1645
+ path: skillPath,
1646
+ sourceType: isSymbolicLink ? 'symlink' : 'directory'
1647
+ };
1648
+ } catch (e) {
1649
+ return null;
1650
+ }
1651
+ })
1652
+ .filter(Boolean);
1653
+ } catch (e) {
1654
+ return [];
1655
+ }
1656
+ }
1657
+
1658
+ function scanUnmanagedCodexSkills() {
1659
+ const existing = listCodexSkills();
1660
+ if (existing.error) {
1661
+ return { error: existing.error };
1662
+ }
1663
+ const existingNames = new Set((Array.isArray(existing.items) ? existing.items : [])
1664
+ .map((item) => (item && typeof item.name === 'string' ? item.name.trim() : ''))
1665
+ .filter(Boolean));
1666
+
1667
+ const items = [];
1668
+ for (const source of SKILL_IMPORT_SOURCES) {
1669
+ const sourceEntries = listSkillEntriesByRoot(source.dir);
1670
+ for (const entry of sourceEntries) {
1671
+ if (existingNames.has(entry.name)) {
1672
+ continue;
1673
+ }
1674
+ const metadata = readCodexSkillMetadata(entry.path);
1675
+ items.push({
1676
+ key: `${source.app}:${entry.name}`,
1677
+ name: entry.name,
1678
+ displayName: metadata.displayName || entry.name,
1679
+ description: metadata.description || '',
1680
+ sourceApp: source.app,
1681
+ sourceLabel: source.label,
1682
+ sourcePath: entry.path,
1683
+ sourceType: entry.sourceType,
1684
+ hasSkillFile: !!metadata.hasSkillFile
1685
+ });
1686
+ }
1687
+ }
1688
+
1689
+ items.sort((a, b) => {
1690
+ const nameCompare = a.displayName.localeCompare(b.displayName, 'zh-Hans-CN');
1691
+ if (nameCompare !== 0) return nameCompare;
1692
+ return a.sourceLabel.localeCompare(b.sourceLabel, 'zh-Hans-CN');
1693
+ });
1694
+
1695
+ return {
1696
+ root: CODEX_SKILLS_DIR,
1697
+ items,
1698
+ sources: SKILL_IMPORT_SOURCES.map((source) => ({
1699
+ app: source.app,
1700
+ label: source.label,
1701
+ path: source.dir,
1702
+ exists: fs.existsSync(source.dir)
1703
+ }))
1704
+ };
1705
+ }
1706
+
1707
+ function importCodexSkills(params = {}) {
1708
+ const rawItems = Array.isArray(params.items) ? params.items : [];
1709
+ if (!rawItems.length) {
1710
+ return { error: '请先选择要导入的 skill' };
1711
+ }
1712
+
1713
+ ensureDir(CODEX_SKILLS_DIR);
1714
+
1715
+ const imported = [];
1716
+ const failed = [];
1717
+ const dedup = new Set();
1718
+
1719
+ for (const rawItem of rawItems) {
1720
+ const safeItem = rawItem && typeof rawItem === 'object' ? rawItem : {};
1721
+ const normalizedName = normalizeCodexSkillName(safeItem.name);
1722
+ if (normalizedName.error) {
1723
+ failed.push({
1724
+ name: safeItem && safeItem.name ? String(safeItem.name) : '',
1725
+ sourceApp: safeItem && safeItem.sourceApp ? String(safeItem.sourceApp) : '',
1726
+ error: normalizedName.error
1727
+ });
1728
+ continue;
1729
+ }
1730
+ const source = getSkillImportSourceByApp(safeItem.sourceApp);
1731
+ if (!source) {
1732
+ failed.push({
1733
+ name: normalizedName.name,
1734
+ sourceApp: safeItem && safeItem.sourceApp ? String(safeItem.sourceApp) : '',
1735
+ error: '来源应用不支持'
1736
+ });
1737
+ continue;
1738
+ }
1739
+ const dedupKey = `${source.app}:${normalizedName.name}`;
1740
+ if (dedup.has(dedupKey)) {
1741
+ continue;
1742
+ }
1743
+ dedup.add(dedupKey);
1744
+
1745
+ const sourcePath = path.join(source.dir, normalizedName.name);
1746
+ const sourceRelative = path.relative(source.dir, sourcePath);
1747
+ if (sourceRelative.startsWith('..') || path.isAbsolute(sourceRelative)) {
1748
+ failed.push({
1749
+ name: normalizedName.name,
1750
+ sourceApp: source.app,
1751
+ error: '来源路径非法'
1752
+ });
1753
+ continue;
1754
+ }
1755
+ if (!fs.existsSync(sourcePath)) {
1756
+ failed.push({
1757
+ name: normalizedName.name,
1758
+ sourceApp: source.app,
1759
+ error: '来源 skill 不存在'
1760
+ });
1761
+ continue;
1762
+ }
1763
+
1764
+ const targetPath = path.join(CODEX_SKILLS_DIR, normalizedName.name);
1765
+ const targetRelative = path.relative(CODEX_SKILLS_DIR, targetPath);
1766
+ if (targetRelative.startsWith('..') || path.isAbsolute(targetRelative)) {
1767
+ failed.push({
1768
+ name: normalizedName.name,
1769
+ sourceApp: source.app,
1770
+ error: '目标路径非法'
1771
+ });
1772
+ continue;
1773
+ }
1774
+ if (fs.existsSync(targetPath)) {
1775
+ failed.push({
1776
+ name: normalizedName.name,
1777
+ sourceApp: source.app,
1778
+ error: 'Codex 中已存在同名 skill'
1779
+ });
1780
+ continue;
1781
+ }
1782
+
1783
+ let copiedToTarget = false;
1784
+ try {
1785
+ const lstat = fs.lstatSync(sourcePath);
1786
+ if (!lstat.isDirectory() && !lstat.isSymbolicLink()) {
1787
+ failed.push({
1788
+ name: normalizedName.name,
1789
+ sourceApp: source.app,
1790
+ error: '来源不是技能目录'
1791
+ });
1792
+ continue;
1793
+ }
1794
+ const sourceDirForCopy = lstat.isSymbolicLink() ? fs.realpathSync(sourcePath) : sourcePath;
1795
+ const sourceStat = fs.statSync(sourceDirForCopy);
1796
+ if (!sourceStat.isDirectory()) {
1797
+ failed.push({
1798
+ name: normalizedName.name,
1799
+ sourceApp: source.app,
1800
+ error: '来源 skill 无法读取'
1801
+ });
1802
+ continue;
1803
+ }
1804
+ const visitedRealPaths = new Set([sourceDirForCopy]);
1805
+ copyDirRecursive(sourceDirForCopy, targetPath, {
1806
+ dereferenceSymlinks: true,
1807
+ visitedRealPaths
1808
+ });
1809
+ copiedToTarget = true;
1810
+ imported.push({
1811
+ name: normalizedName.name,
1812
+ sourceApp: source.app,
1813
+ sourceLabel: source.label,
1814
+ path: targetPath
1815
+ });
1816
+ } catch (e) {
1817
+ if (!copiedToTarget && fs.existsSync(targetPath)) {
1818
+ try {
1819
+ removeDirectoryRecursive(targetPath);
1820
+ } catch (_) {}
1821
+ }
1822
+ failed.push({
1823
+ name: normalizedName.name,
1824
+ sourceApp: source.app,
1825
+ error: e && e.message ? e.message : '导入失败'
1826
+ });
1827
+ }
1828
+ }
1829
+
1830
+ return {
1831
+ success: failed.length === 0,
1832
+ imported,
1833
+ failed,
1834
+ root: CODEX_SKILLS_DIR
1835
+ };
1836
+ }
1837
+
1838
+ function removeDirectoryRecursive(targetPath) {
1839
+ if (typeof fs.rmSync === 'function') {
1840
+ fs.rmSync(targetPath, { recursive: true, force: false });
1841
+ return;
1842
+ }
1843
+ fs.rmdirSync(targetPath, { recursive: true });
1844
+ }
1845
+
1846
+ function deleteCodexSkills(params = {}) {
1847
+ const rawList = Array.isArray(params.names) ? params.names : [];
1848
+ const uniqueNames = Array.from(new Set(rawList
1849
+ .map((item) => (typeof item === 'string' ? item.trim() : ''))
1850
+ .filter(Boolean)));
1851
+ if (!uniqueNames.length) {
1852
+ return { error: '请先选择要删除的 skill' };
1853
+ }
1854
+
1855
+ const deleted = [];
1856
+ const failed = [];
1857
+ for (const rawName of uniqueNames) {
1858
+ const normalized = normalizeCodexSkillName(rawName);
1859
+ if (normalized.error) {
1860
+ failed.push({ name: rawName, error: normalized.error });
1861
+ continue;
1862
+ }
1863
+
1864
+ const skillPath = path.join(CODEX_SKILLS_DIR, normalized.name);
1865
+ const relativePath = path.relative(CODEX_SKILLS_DIR, skillPath);
1866
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
1867
+ failed.push({ name: normalized.name, error: '技能路径非法' });
1868
+ continue;
1869
+ }
1870
+ if (!fs.existsSync(skillPath)) {
1871
+ failed.push({ name: normalized.name, error: 'skill 不存在' });
1872
+ continue;
1873
+ }
1874
+
1875
+ try {
1876
+ const stat = fs.lstatSync(skillPath);
1877
+ if (!stat.isDirectory() && !stat.isSymbolicLink()) {
1878
+ failed.push({ name: normalized.name, error: '仅支持删除技能目录' });
1879
+ continue;
1880
+ }
1881
+ removeDirectoryRecursive(skillPath);
1882
+ deleted.push(normalized.name);
1883
+ } catch (e) {
1884
+ failed.push({
1885
+ name: normalized.name,
1886
+ error: e && e.message ? e.message : '删除失败'
1887
+ });
1888
+ }
1889
+ }
1890
+
1891
+ return {
1892
+ success: failed.length === 0,
1893
+ deleted,
1894
+ failed,
1895
+ root: CODEX_SKILLS_DIR
1896
+ };
1897
+ }
1898
+
1899
+ function readAgentsFile(params = {}) {
1900
+ const filePath = resolveAgentsFilePath(params);
1901
+ const dirCheck = validateAgentsBaseDir(filePath);
1902
+ if (dirCheck.error) {
1903
+ return { error: dirCheck.error };
1904
+ }
1905
+
1906
+ if (!fs.existsSync(filePath)) {
1907
+ return {
1908
+ exists: false,
1909
+ path: filePath,
1910
+ content: '',
1911
+ lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n'
1912
+ };
1913
+ }
1914
+
1915
+ try {
1916
+ const raw = fs.readFileSync(filePath, 'utf-8');
1917
+ return {
1918
+ exists: true,
1919
+ path: filePath,
1920
+ content: stripUtf8Bom(raw),
1921
+ lineEnding: detectLineEnding(raw)
1922
+ };
1923
+ } catch (e) {
1924
+ return { error: `读取 AGENTS.md 失败: ${e.message}` };
1925
+ }
1926
+ }
1927
+
1928
+ function applyAgentsFile(params = {}) {
1929
+ const filePath = resolveAgentsFilePath(params);
1930
+ const dirCheck = validateAgentsBaseDir(filePath);
1931
+ if (dirCheck.error) {
1932
+ return { error: dirCheck.error };
1933
+ }
1934
+
1935
+ const content = typeof params.content === 'string' ? params.content : '';
1936
+ const lineEnding = params.lineEnding === '\r\n' ? '\r\n' : '\n';
1937
+ const normalized = normalizeLineEnding(content, lineEnding);
1938
+ const finalContent = ensureUtf8Bom(normalized);
1939
+
1940
+ try {
1941
+ fs.writeFileSync(filePath, finalContent, 'utf-8');
1942
+ return { success: true, path: filePath };
1943
+ } catch (e) {
1944
+ return { error: `写入 AGENTS.md 失败: ${e.message}` };
1945
+ }
1946
+ }
1947
+
1948
+ function resolveOpenclawWorkspaceDir(config) {
1949
+ const workspace = config
1950
+ && config.agents
1951
+ && config.agents.defaults
1952
+ && typeof config.agents.defaults.workspace === 'string'
1953
+ ? config.agents.defaults.workspace
1954
+ : '';
1955
+ const resolved = resolveHomePath(workspace);
1956
+ if (!resolved) {
1957
+ return OPENCLAW_WORKSPACE_DIR;
1958
+ }
1959
+ if (path.isAbsolute(resolved)) {
1960
+ return resolved;
1961
+ }
1962
+ return path.join(OPENCLAW_DIR, resolved);
1963
+ }
1964
+
1965
+ function normalizeOpenclawWorkspaceFileName(input) {
1966
+ const raw = typeof input === 'string' ? input.trim() : '';
1967
+ if (!raw) {
1968
+ return { error: '文件名不能为空' };
1969
+ }
1970
+ if (raw.includes('\0')) {
865
1971
  return { error: '文件名非法' };
866
1972
  }
867
1973
  if (raw.includes('/') || raw.includes('\\') || raw.includes('..')) {
@@ -1307,11 +2413,28 @@ async function buildConfigHealthReport(params = {}) {
1307
2413
  const config = status.config || {};
1308
2414
 
1309
2415
  if (status.isVirtual) {
2416
+ const parseFailed = status.errorType === 'parse';
2417
+ const readFailed = status.errorType === 'read';
1310
2418
  issues.push({
1311
- code: 'config-missing',
1312
- message: status.reason || '未检测到 config.toml',
1313
- suggestion: '在模板编辑器中确认应用配置,生成可用的 config.toml'
2419
+ code: parseFailed ? 'config-parse-failed' : (readFailed ? 'config-read-failed' : 'config-missing'),
2420
+ message: status.reason || (parseFailed
2421
+ ? 'config.toml 解析失败'
2422
+ : (readFailed ? '读取 config.toml 失败' : '未检测到 config.toml')),
2423
+ suggestion: parseFailed
2424
+ ? '修复 config.toml 语法错误后重试'
2425
+ : (readFailed ? '检查文件权限后重试' : '在模板编辑器中确认应用配置,生成可用的 config.toml')
1314
2426
  });
2427
+ if (parseFailed || readFailed) {
2428
+ return {
2429
+ ok: false,
2430
+ issues,
2431
+ summary: {
2432
+ currentProvider: '',
2433
+ currentModel: ''
2434
+ },
2435
+ remote: null
2436
+ };
2437
+ }
1315
2438
  }
1316
2439
 
1317
2440
  const providerName = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
@@ -1498,13 +2621,26 @@ function readConfigOrVirtualDefault() {
1498
2621
  return {
1499
2622
  config: readConfig(),
1500
2623
  isVirtual: false,
1501
- reason: ''
2624
+ reason: '',
2625
+ detail: '',
2626
+ errorType: ''
1502
2627
  };
1503
2628
  } catch (e) {
2629
+ const errorType = typeof e.configErrorType === 'string' && e.configErrorType.trim()
2630
+ ? e.configErrorType.trim()
2631
+ : 'read';
2632
+ const publicReason = typeof e.configPublicReason === 'string' && e.configPublicReason.trim()
2633
+ ? e.configPublicReason.trim()
2634
+ : (errorType === 'parse' ? 'config.toml 解析失败' : '读取 config.toml 失败');
2635
+ const detail = typeof e.configDetail === 'string' && e.configDetail.trim()
2636
+ ? e.configDetail.trim()
2637
+ : (e && e.message ? e.message : publicReason);
1504
2638
  return {
1505
- config: buildVirtualDefaultConfig(),
2639
+ config: errorType === 'missing' ? buildVirtualDefaultConfig() : {},
1506
2640
  isVirtual: true,
1507
- reason: e.message || '配置文件无效,已回退到默认模板'
2641
+ reason: publicReason,
2642
+ detail,
2643
+ errorType
1508
2644
  };
1509
2645
  }
1510
2646
  }
@@ -1512,10 +2648,31 @@ function readConfigOrVirtualDefault() {
1512
2648
  return {
1513
2649
  config: buildVirtualDefaultConfig(),
1514
2650
  isVirtual: true,
1515
- reason: `配置文件不存在: ${CONFIG_FILE}`
2651
+ reason: '未检测到 config.toml',
2652
+ detail: `配置文件不存在: ${CONFIG_FILE}`,
2653
+ errorType: 'missing'
1516
2654
  };
1517
2655
  }
1518
2656
 
2657
+ function hasConfigLoadError(result) {
2658
+ return !!(result
2659
+ && result.isVirtual
2660
+ && (result.errorType === 'parse' || result.errorType === 'read'));
2661
+ }
2662
+
2663
+ function printConfigLoadErrorAndMarkExit(result) {
2664
+ const isReadError = result && result.errorType === 'read';
2665
+ const detail = result && typeof result.detail === 'string' && result.detail.trim()
2666
+ ? result.detail.trim()
2667
+ : (isReadError ? '读取配置文件失败' : '配置文件解析失败');
2668
+ console.error(`\n错误: ${isReadError ? '读取 config.toml 失败' : '配置文件解析失败'}`);
2669
+ console.error(` 详情: ${detail}`);
2670
+ console.error(` 路径: ${CONFIG_FILE}`);
2671
+ console.error(` 建议: ${isReadError ? '检查文件权限后重试' : '修复 config.toml 语法后重试'}`);
2672
+ console.error();
2673
+ process.exitCode = 1;
2674
+ }
2675
+
1519
2676
  function normalizeTopLevelConfigWithTemplate(template, selectedProvider, selectedModel) {
1520
2677
  let content = typeof template === 'string' ? template : '';
1521
2678
  if (!content.trim()) {
@@ -1656,6 +2813,9 @@ function addProviderToConfig(params = {}) {
1656
2813
 
1657
2814
  if (!name) return { error: '名称不能为空' };
1658
2815
  if (!url) return { error: 'URL 不能为空' };
2816
+ if (!isValidProviderName(name)) {
2817
+ return { error: '名称仅支持字母/数字/._-' };
2818
+ }
1659
2819
  if (isReservedProviderNameForCreation(name)) {
1660
2820
  return { error: 'local provider 为系统保留名称,不可新增' };
1661
2821
  }
@@ -1687,24 +2847,20 @@ function addProviderToConfig(params = {}) {
1687
2847
  return { error: `config.toml 解析失败: ${e.message}` };
1688
2848
  }
1689
2849
 
1690
- if (!parsed.model_providers || typeof parsed.model_providers !== 'object') {
1691
- parsed.model_providers = {};
1692
- }
1693
-
1694
- if (parsed.model_providers[name]) {
2850
+ const providerHeaderSegmentKeySet = collectModelProviderHeaderSegmentKeySet(content);
2851
+ const normalizedProviders = isPlainObject(parsed.model_providers)
2852
+ ? normalizeLegacyModelProviders(parsed.model_providers, providerHeaderSegmentKeySet)
2853
+ : {};
2854
+ if (normalizedProviders && normalizedProviders[name]) {
1695
2855
  return { error: '提供商已存在' };
1696
2856
  }
1697
2857
 
1698
- const escapeTomlString = (value) => String(value || '')
1699
- .replace(/\\/g, '\\\\')
1700
- .replace(/"/g, '\\"');
1701
-
1702
2858
  const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
1703
- const safeName = escapeTomlString(name);
1704
- const safeUrl = escapeTomlString(url);
1705
- const safeKey = escapeTomlString(key);
2859
+ const safeName = escapeTomlBasicString(name);
2860
+ const safeUrl = escapeTomlBasicString(url);
2861
+ const safeKey = escapeTomlBasicString(key);
1706
2862
  const block = [
1707
- `[model_providers.${safeName}]`,
2863
+ buildModelProviderTableHeader(name),
1708
2864
  `name = "${safeName}"`,
1709
2865
  `base_url = "${safeUrl}"`,
1710
2866
  `wire_api = "responses"`,
@@ -1804,8 +2960,36 @@ function performProviderDeletion(name, options = {}) {
1804
2960
  const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
1805
2961
  const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
1806
2962
  const hasBom = content.charCodeAt(0) === 0xFEFF;
1807
- const safeName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1808
- const sectionRegex = new RegExp(`\\[\\s*model_providers\\s*\\.\\s*(?:"${safeName}"|'${safeName}'|${safeName})\\s*\\]`);
2963
+ const providerConfig = config.model_providers[name];
2964
+ const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)
2965
+ ? providerConfig.__codexmate_legacy_segments
2966
+ : null;
2967
+ const providerSegmentVariants = (() => {
2968
+ const variants = [];
2969
+ const seen = new Set();
2970
+ const pushVariant = (segments) => {
2971
+ const normalized = normalizeLegacySegments(segments);
2972
+ const key = buildLegacySegmentsKey(normalized);
2973
+ if (!key || seen.has(key)) return;
2974
+ seen.add(key);
2975
+ variants.push(normalized);
2976
+ };
2977
+ if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)) {
2978
+ pushVariant(providerConfig.__codexmate_legacy_segments);
2979
+ }
2980
+ if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segment_variants)) {
2981
+ for (const segments of providerConfig.__codexmate_legacy_segment_variants) {
2982
+ pushVariant(segments);
2983
+ }
2984
+ }
2985
+ if (providerSegments) {
2986
+ pushVariant(providerSegments);
2987
+ }
2988
+ if (variants.length === 0) {
2989
+ pushVariant(String(name || '').split('.').filter((item) => item));
2990
+ }
2991
+ return variants;
2992
+ })();
1809
2993
 
1810
2994
  const remainingProviders = Object.keys(config.model_providers || {}).filter(item => item !== name);
1811
2995
  if (remainingProviders.length === 0) {
@@ -1844,17 +3028,25 @@ function performProviderDeletion(name, options = {}) {
1844
3028
  };
1845
3029
 
1846
3030
  let updatedContent = null;
1847
- const match = content.match(sectionRegex);
1848
- if (match) {
1849
- const startIdx = match.index;
1850
- const rest = content.slice(startIdx + match[0].length);
1851
- const nextIdx = rest.indexOf('[');
1852
- const endIdx = nextIdx === -1 ? content.length : (startIdx + match[0].length + nextIdx);
1853
-
1854
- const removedContent = (content.slice(0, startIdx) + content.slice(endIdx))
1855
- .replace(/\n{3,}/g, lineEnding + lineEnding);
1856
-
1857
- updatedContent = removedContent;
3031
+ const combinedRanges = [];
3032
+ for (const segments of providerSegmentVariants) {
3033
+ combinedRanges.push(...findProviderSectionRanges(content, name, segments));
3034
+ combinedRanges.push(...findProviderDescendantSectionRanges(content, segments));
3035
+ }
3036
+ if (combinedRanges.length === 0) {
3037
+ combinedRanges.push(...findProviderSectionRanges(content, name, providerSegments));
3038
+ }
3039
+ if (combinedRanges.length > 0) {
3040
+ const sorted = combinedRanges.sort((a, b) => b.start - a.start || b.end - a.end);
3041
+ const seen = new Set();
3042
+ let removedContent = content;
3043
+ for (const range of sorted) {
3044
+ const rangeKey = `${range.start}:${range.end}`;
3045
+ if (seen.has(rangeKey)) continue;
3046
+ seen.add(rangeKey);
3047
+ removedContent = removedContent.slice(0, range.start) + removedContent.slice(range.end);
3048
+ }
3049
+ updatedContent = removedContent.replace(/\n{3,}/g, lineEnding + lineEnding);
1858
3050
  }
1859
3051
 
1860
3052
  if (updatedContent) {
@@ -4857,7 +6049,12 @@ async function cmdSetup() {
4857
6049
 
4858
6050
  // 显示当前状态
4859
6051
  function cmdStatus() {
4860
- const { config, isVirtual } = readConfigOrVirtualDefault();
6052
+ const configResult = readConfigOrVirtualDefault();
6053
+ if (hasConfigLoadError(configResult)) {
6054
+ printConfigLoadErrorAndMarkExit(configResult);
6055
+ return;
6056
+ }
6057
+ const { config, isVirtual } = configResult;
4861
6058
  const current = config.model_provider || '未设置';
4862
6059
  const currentModel = config.model || '未设置';
4863
6060
 
@@ -4873,7 +6070,12 @@ function cmdStatus() {
4873
6070
 
4874
6071
  // 列出所有提供商
4875
6072
  function cmdList() {
4876
- const { config, isVirtual } = readConfigOrVirtualDefault();
6073
+ const configResult = readConfigOrVirtualDefault();
6074
+ if (hasConfigLoadError(configResult)) {
6075
+ printConfigLoadErrorAndMarkExit(configResult);
6076
+ return;
6077
+ }
6078
+ const { config, isVirtual } = configResult;
4877
6079
  const providers = config.model_providers || {};
4878
6080
  const current = config.model_provider;
4879
6081
 
@@ -5028,6 +6230,10 @@ function cmdAdd(name, baseUrl, apiKey, silent = false) {
5028
6230
  }
5029
6231
  throw new Error('名称和URL必填');
5030
6232
  }
6233
+ if (!isValidProviderName(providerName)) {
6234
+ if (!silent) console.error('错误: 名称仅支持字母/数字/._-');
6235
+ throw new Error('名称仅支持字母/数字/._-');
6236
+ }
5031
6237
  if (isReservedProviderNameForCreation(providerName)) {
5032
6238
  if (!silent) console.error('错误: local provider 为系统保留名称,不可新增');
5033
6239
  throw new Error('local provider 为系统保留名称,不可新增');
@@ -5039,13 +6245,16 @@ function cmdAdd(name, baseUrl, apiKey, silent = false) {
5039
6245
  throw new Error('提供商已存在');
5040
6246
  }
5041
6247
 
6248
+ const safeName = escapeTomlBasicString(providerName);
6249
+ const safeBaseUrl = escapeTomlBasicString(providerBaseUrl);
6250
+ const safeApiKey = escapeTomlBasicString(apiKey || '');
5042
6251
  const newBlock = `
5043
- [model_providers.${providerName}]
5044
- name = "${providerName}"
5045
- base_url = "${providerBaseUrl}"
6252
+ ${buildModelProviderTableHeader(providerName)}
6253
+ name = "${safeName}"
6254
+ base_url = "${safeBaseUrl}"
5046
6255
  wire_api = "responses"
5047
6256
  requires_openai_auth = false
5048
- preferred_auth_method = "${apiKey || ''}"
6257
+ preferred_auth_method = "${safeApiKey}"
5049
6258
  request_max_retries = 4
5050
6259
  stream_max_retries = 10
5051
6260
  stream_idle_timeout_ms = 300000
@@ -5105,42 +6314,156 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
5105
6314
  }
5106
6315
 
5107
6316
  const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
5108
- const safeName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
5109
- const sectionRegex = new RegExp(`\\[\\s*model_providers\\s*\\.\\s*${safeName}\\s*\\]`);
5110
- const match = content.match(sectionRegex);
5111
- if (!match) {
6317
+ const providerConfig = config.model_providers[name];
6318
+ const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)
6319
+ ? providerConfig.__codexmate_legacy_segments
6320
+ : null;
6321
+ const ranges = findProviderSectionRanges(content, name, providerSegments);
6322
+ if (ranges.length === 0) {
5112
6323
  if (!silent) console.error('错误: 无法找到提供商配置块');
5113
6324
  throw new Error('无法找到提供商配置块');
5114
6325
  }
5115
6326
 
5116
- const startIdx = match.index;
5117
- const rest = content.slice(startIdx + match[0].length);
5118
- const nextIdx = rest.indexOf('[');
5119
- const endIdx = nextIdx === -1 ? content.length : (startIdx + match[0].length + nextIdx);
5120
-
5121
- // 提取该提供商的配置块
5122
- const providerBlock = content.slice(startIdx, endIdx);
6327
+ const replaceTomlStringField = (block, fieldName, rawValue) => {
6328
+ const safeValue = escapeTomlBasicString(rawValue);
6329
+ const escapedFieldName = escapeRegex(fieldName);
6330
+ const multilineRanges = collectTomlMultilineStringRanges(block);
6331
+ const tripleStartRegex = new RegExp(`^(\\s*${escapedFieldName}\\s*=\\s*)(\"\"\"|''')`, 'mg');
6332
+ let tripleStartMatch = null;
6333
+ let tripleCandidate;
6334
+ while ((tripleCandidate = tripleStartRegex.exec(block)) !== null) {
6335
+ if (isIndexInRanges(tripleCandidate.index, multilineRanges)) {
6336
+ continue;
6337
+ }
6338
+ tripleStartMatch = tripleCandidate;
6339
+ break;
6340
+ }
6341
+ if (tripleStartMatch) {
6342
+ const prefixStart = tripleStartMatch.index;
6343
+ const prefixEnd = prefixStart + tripleStartMatch[1].length;
6344
+ const tripleQuote = tripleStartMatch[2];
6345
+ const valueStart = prefixEnd + tripleQuote.length;
6346
+ const quoteChar = tripleQuote[0];
6347
+ let valueEnd = -1;
6348
+ let closingRunLength = 0;
6349
+ for (let i = valueStart; i < block.length; i++) {
6350
+ if (block[i] !== quoteChar) continue;
6351
+ let runEnd = i + 1;
6352
+ while (runEnd < block.length && block[runEnd] === quoteChar) {
6353
+ runEnd++;
6354
+ }
6355
+ const runLength = runEnd - i;
6356
+ if (runLength < tripleQuote.length) {
6357
+ i = runEnd - 1;
6358
+ continue;
6359
+ }
6360
+ if (tripleQuote === '"""') {
6361
+ let slashCount = 0;
6362
+ for (let j = i - 1; j >= valueStart && block[j] === '\\'; j--) {
6363
+ slashCount++;
6364
+ }
6365
+ if (slashCount % 2 !== 0) {
6366
+ continue;
6367
+ }
6368
+ }
6369
+ valueEnd = i;
6370
+ closingRunLength = runLength;
6371
+ break;
6372
+ }
6373
+ if (valueEnd === -1) {
6374
+ throw new Error(`${fieldName} 使用了未闭合的多行 TOML 字符串,无法安全更新`);
6375
+ }
6376
+ const lineEndIndex = block.indexOf('\n', valueEnd + closingRunLength);
6377
+ let tailEnd = lineEndIndex === -1 ? block.length : lineEndIndex;
6378
+ if (lineEndIndex > 0 && block[lineEndIndex - 1] === '\r') {
6379
+ tailEnd = lineEndIndex - 1;
6380
+ }
6381
+ const tail = block.slice(valueEnd + closingRunLength, tailEnd);
6382
+ const tailMatch = tail.match(/^(\s+#.*)?\s*$/);
6383
+ if (!tailMatch) {
6384
+ throw new Error(`${fieldName} 多行字符串后的语法不受支持,无法安全更新`);
6385
+ }
6386
+ const commentSuffix = tailMatch[1] || '';
6387
+ const replacementLine = `${block.slice(prefixStart, prefixEnd)}"${safeValue}"${commentSuffix}`;
6388
+ return block.slice(0, prefixStart) + replacementLine + block.slice(tailEnd);
6389
+ }
5123
6390
 
5124
- // 替换 base_url
5125
- let updatedBlock = providerBlock;
5126
- if (baseUrl) {
5127
- updatedBlock = updatedBlock.replace(
5128
- /^(base_url\s*=\s*)(["']).*?\2/m,
5129
- `$1$2${baseUrl}$2`
6391
+ const withCommentRegex = new RegExp(
6392
+ `^(\\s*${escapedFieldName}\\s*=\\s*)(?:"(?:\\\\.|[^"\\\\])*"|'[^'\\n]*')(\\s+#.*)?$`,
6393
+ 'mg'
5130
6394
  );
5131
- }
5132
-
5133
- // 替换 preferred_auth_method (API Key)
5134
- if (apiKey !== undefined) {
5135
- updatedBlock = updatedBlock.replace(
5136
- /^(preferred_auth_method\s*=\s*)(["']).*?\2/m,
5137
- `$1$2${apiKey}$2`
6395
+ let replaced = false;
6396
+ let next = block.replace(
6397
+ withCommentRegex,
6398
+ (full, prefix, suffix = '', offset) => {
6399
+ if (replaced || isIndexInRanges(offset, multilineRanges)) {
6400
+ return full;
6401
+ }
6402
+ replaced = true;
6403
+ return `${prefix}"${safeValue}"${suffix}`;
6404
+ }
5138
6405
  );
6406
+ if (!replaced) {
6407
+ const fallbackRegex = new RegExp(`^(\\s*${escapedFieldName}\\s*=\\s*)(.*?)(\\s+#.*)?$`, 'mg');
6408
+ let fallbackReplaced = false;
6409
+ const multilineRangesForNext = collectTomlMultilineStringRanges(next);
6410
+ let fallbackMatch;
6411
+ let fallbackCandidate;
6412
+ while ((fallbackCandidate = fallbackRegex.exec(next)) !== null) {
6413
+ if (isIndexInRanges(fallbackCandidate.index, multilineRangesForNext)) {
6414
+ continue;
6415
+ }
6416
+ fallbackMatch = fallbackCandidate;
6417
+ break;
6418
+ }
6419
+ if (fallbackMatch) {
6420
+ const existingValue = String(fallbackMatch[2] || '').trim();
6421
+ const looksLikeMultilineArray = existingValue.startsWith('[') && !existingValue.endsWith(']');
6422
+ const looksLikeMultilineInlineTable = existingValue.startsWith('{') && !existingValue.endsWith('}');
6423
+ if (looksLikeMultilineArray || looksLikeMultilineInlineTable) {
6424
+ throw new Error(`${fieldName} 当前值是多行 TOML 结构,无法安全更新`);
6425
+ }
6426
+ const prefix = fallbackMatch[1];
6427
+ const suffix = fallbackMatch[3] || '';
6428
+ const replacement = `${prefix}"${safeValue}"${suffix}`;
6429
+ next = `${next.slice(0, fallbackMatch.index)}${replacement}${next.slice(fallbackMatch.index + fallbackMatch[0].length)}`;
6430
+ fallbackReplaced = true;
6431
+ }
6432
+ if (!fallbackReplaced) {
6433
+ const keyIndentMatch = block.match(/^(\s*)[A-Za-z0-9_.-]+\s*=/m);
6434
+ const indent = keyIndentMatch ? keyIndentMatch[1] : '';
6435
+ const lineEnding = block.includes('\r\n') ? '\r\n' : '\n';
6436
+ const tailMatch = block.match(/(\s*)$/);
6437
+ const tail = tailMatch ? tailMatch[1] : '';
6438
+ const body = block.slice(0, block.length - tail.length);
6439
+ const separator = body.endsWith('\n') || body.endsWith('\r') ? '' : lineEnding;
6440
+ next = `${body}${separator}${indent}${fieldName} = "${safeValue}"${tail}`;
6441
+ }
6442
+ }
6443
+ return next;
6444
+ };
6445
+
6446
+ let newContent = content;
6447
+ const sorted = ranges.sort((a, b) => b.start - a.start);
6448
+ for (const range of sorted) {
6449
+ const providerBlock = newContent.slice(range.start, range.end);
6450
+ let updatedBlock = providerBlock;
6451
+ if (baseUrl) {
6452
+ updatedBlock = replaceTomlStringField(updatedBlock, 'base_url', baseUrl);
6453
+ }
6454
+ if (apiKey !== undefined) {
6455
+ updatedBlock = replaceTomlStringField(updatedBlock, 'preferred_auth_method', apiKey);
6456
+ }
6457
+ newContent = newContent.slice(0, range.start) + updatedBlock + newContent.slice(range.end);
5139
6458
  }
5140
6459
 
5141
- // 组合新的内容
5142
- const newContent = content.slice(0, startIdx) + updatedBlock + content.slice(endIdx);
5143
- writeConfig(newContent.trim());
6460
+ const finalContent = newContent.trim();
6461
+ try {
6462
+ toml.parse(finalContent);
6463
+ } catch (e) {
6464
+ throw new Error(`更新后的 config.toml 无效: ${e.message}`);
6465
+ }
6466
+ writeConfig(finalContent);
5144
6467
 
5145
6468
  // 如果更新了 API Key 且该提供商是当前激活的,同步更新 auth.json
5146
6469
  const currentProvider = config.model_provider;
@@ -5368,17 +6691,57 @@ async function prepareCodexDirDownload() {
5368
6691
  }
5369
6692
  }
5370
6693
 
5371
- function copyDirRecursive(srcDir, destDir) {
6694
+ function copyDirRecursive(srcDir, destDir, options = {}) {
6695
+ const dereferenceSymlinks = !!(options && options.dereferenceSymlinks);
6696
+ const visitedRealPaths = options && options.visitedRealPaths instanceof Set
6697
+ ? options.visitedRealPaths
6698
+ : new Set();
6699
+ const childOptions = {
6700
+ ...options,
6701
+ dereferenceSymlinks,
6702
+ visitedRealPaths
6703
+ };
5372
6704
  ensureDir(destDir);
5373
6705
  const entries = fs.readdirSync(srcDir, { withFileTypes: true });
5374
6706
  for (const entry of entries) {
5375
6707
  const srcPath = path.join(srcDir, entry.name);
5376
6708
  const destPath = path.join(destDir, entry.name);
5377
6709
  if (entry.isDirectory()) {
5378
- copyDirRecursive(srcPath, destPath);
6710
+ if (!dereferenceSymlinks) {
6711
+ copyDirRecursive(srcPath, destPath, childOptions);
6712
+ continue;
6713
+ }
6714
+ const realPath = fs.realpathSync(srcPath);
6715
+ if (visitedRealPaths.has(realPath)) {
6716
+ continue;
6717
+ }
6718
+ visitedRealPaths.add(realPath);
6719
+ try {
6720
+ copyDirRecursive(srcPath, destPath, childOptions);
6721
+ } finally {
6722
+ visitedRealPaths.delete(realPath);
6723
+ }
5379
6724
  } else if (entry.isSymbolicLink()) {
5380
- const target = fs.readlinkSync(srcPath);
5381
- fs.symlinkSync(target, destPath);
6725
+ if (dereferenceSymlinks) {
6726
+ const realPath = fs.realpathSync(srcPath);
6727
+ const realStat = fs.statSync(realPath);
6728
+ if (realStat.isDirectory()) {
6729
+ if (visitedRealPaths.has(realPath)) {
6730
+ continue;
6731
+ }
6732
+ visitedRealPaths.add(realPath);
6733
+ try {
6734
+ copyDirRecursive(realPath, destPath, childOptions);
6735
+ } finally {
6736
+ visitedRealPaths.delete(realPath);
6737
+ }
6738
+ } else {
6739
+ fs.copyFileSync(realPath, destPath);
6740
+ }
6741
+ } else {
6742
+ const target = fs.readlinkSync(srcPath);
6743
+ fs.symlinkSync(target, destPath);
6744
+ }
5382
6745
  } else {
5383
6746
  fs.copyFileSync(srcPath, destPath);
5384
6747
  }
@@ -6185,6 +7548,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
6185
7548
  serviceTier,
6186
7549
  modelReasoningEffort,
6187
7550
  configReady: !statusConfigResult.isVirtual,
7551
+ configErrorType: statusConfigResult.errorType || '',
6188
7552
  configNotice: statusConfigResult.reason || '',
6189
7553
  initNotice: consumeInitNotice()
6190
7554
  };
@@ -6199,6 +7563,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
6199
7563
  const current = listConfig.model_provider;
6200
7564
  result = {
6201
7565
  configReady: !listConfigResult.isVirtual,
7566
+ configErrorType: listConfigResult.errorType || '',
7567
+ configNotice: listConfigResult.reason || '',
6202
7568
  providers: Object.entries(providers).map(([name, p]) => ({
6203
7569
  name,
6204
7570
  url: p.base_url || '',
@@ -6273,6 +7639,18 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
6273
7639
  case 'apply-agents-file':
6274
7640
  result = applyAgentsFile(params || {});
6275
7641
  break;
7642
+ case 'list-codex-skills':
7643
+ result = listCodexSkills();
7644
+ break;
7645
+ case 'delete-codex-skills':
7646
+ result = deleteCodexSkills(params || {});
7647
+ break;
7648
+ case 'scan-unmanaged-codex-skills':
7649
+ result = scanUnmanagedCodexSkills();
7650
+ break;
7651
+ case 'import-codex-skills':
7652
+ result = importCodexSkills(params || {});
7653
+ break;
6276
7654
  case 'get-openclaw-config':
6277
7655
  result = readOpenclawConfigFile();
6278
7656
  break;
@@ -6433,6 +7811,58 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
6433
7811
  case 'proxy-apply-provider':
6434
7812
  result = applyBuiltinProxyProvider(params || {});
6435
7813
  break;
7814
+ case 'workflow-list':
7815
+ result = listWorkflowDefinitions();
7816
+ break;
7817
+ case 'workflow-get':
7818
+ {
7819
+ const id = params && typeof params.id === 'string' ? params.id.trim() : '';
7820
+ if (!id) {
7821
+ result = { error: 'workflow id is required' };
7822
+ } else {
7823
+ result = getWorkflowDefinitionById(id);
7824
+ }
7825
+ }
7826
+ break;
7827
+ case 'workflow-validate':
7828
+ {
7829
+ const id = params && typeof params.id === 'string' ? params.id.trim() : '';
7830
+ if (!id) {
7831
+ result = { ok: false, error: 'workflow id is required' };
7832
+ break;
7833
+ }
7834
+ const input = params && params.input && typeof params.input === 'object' && !Array.isArray(params.input)
7835
+ ? params.input
7836
+ : {};
7837
+ result = validateWorkflowById(id, input);
7838
+ }
7839
+ break;
7840
+ case 'workflow-run':
7841
+ {
7842
+ const id = params && typeof params.id === 'string' ? params.id.trim() : '';
7843
+ if (!id) {
7844
+ result = { error: 'workflow id is required' };
7845
+ break;
7846
+ }
7847
+ const input = params && params.input && typeof params.input === 'object' && !Array.isArray(params.input)
7848
+ ? params.input
7849
+ : {};
7850
+ result = await runWorkflowById(id, input, {
7851
+ allowWrite: !!(params && params.allowWrite),
7852
+ dryRun: !!(params && params.dryRun)
7853
+ });
7854
+ }
7855
+ break;
7856
+ case 'workflow-runs':
7857
+ {
7858
+ const rawLimit = params && Number.isFinite(params.limit) ? params.limit : parseInt(params && params.limit, 10);
7859
+ const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.floor(rawLimit)) : 20;
7860
+ result = {
7861
+ runs: listWorkflowRunRecords(limit),
7862
+ limit
7863
+ };
7864
+ }
7865
+ break;
6436
7866
  default:
6437
7867
  result = { error: '未知操作' };
6438
7868
  }
@@ -6942,13 +8372,229 @@ async function cmdProxy(args = []) {
6942
8372
  return;
6943
8373
  }
6944
8374
 
6945
- if (subcommand === 'stop') {
6946
- await stopBuiltinProxyRuntime();
6947
- console.log('✓ 内建代理已停止\n');
8375
+ if (subcommand === 'stop') {
8376
+ await stopBuiltinProxyRuntime();
8377
+ console.log('✓ 内建代理已停止\n');
8378
+ return;
8379
+ }
8380
+
8381
+ throw new Error(`未知 proxy 子命令: ${subcommand}`);
8382
+ }
8383
+
8384
+ function parseWorkflowInputArg(rawInput) {
8385
+ const raw = typeof rawInput === 'string' ? rawInput.trim() : '';
8386
+ if (!raw) {
8387
+ return {};
8388
+ }
8389
+ let content = raw;
8390
+ if (raw.startsWith('@')) {
8391
+ const filePath = path.resolve(raw.slice(1));
8392
+ if (!fs.existsSync(filePath)) {
8393
+ throw new Error(`工作流输入文件不存在: ${filePath}`);
8394
+ }
8395
+ content = fs.readFileSync(filePath, 'utf-8');
8396
+ }
8397
+ let parsed;
8398
+ try {
8399
+ parsed = JSON.parse(content);
8400
+ } catch (e) {
8401
+ throw new Error(`工作流输入 JSON 解析失败: ${e.message}`);
8402
+ }
8403
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
8404
+ throw new Error('工作流输入必须是 JSON 对象');
8405
+ }
8406
+ return parsed;
8407
+ }
8408
+
8409
+ function printWorkflowHelp() {
8410
+ console.log('\n用法: codexmate workflow <list|get|validate|run|runs> [参数]');
8411
+ console.log(' codexmate workflow list');
8412
+ console.log(' codexmate workflow get diagnose-config');
8413
+ console.log(' codexmate workflow validate safe-provider-switch --input \'{"provider":"e2e"}\'');
8414
+ console.log(' codexmate workflow run diagnose-config --input \'{}\'');
8415
+ console.log(' codexmate workflow run safe-provider-switch --input \'{"provider":"e2e","apply":true}\' --allow-write');
8416
+ console.log(' codexmate workflow runs --limit 20');
8417
+ console.log('参数:');
8418
+ console.log(' --input <JSON|@file> 传入工作流输入');
8419
+ console.log(' --allow-write 允许执行写入步骤');
8420
+ console.log(' --dry-run 跳过写入步骤,仅预演');
8421
+ console.log(' --limit <N> 读取最近执行记录数量(runs)');
8422
+ console.log(' --json 以 JSON 输出');
8423
+ console.log();
8424
+ }
8425
+
8426
+ function parseWorkflowCliOptions(args = []) {
8427
+ const options = {
8428
+ inputRaw: '',
8429
+ allowWrite: false,
8430
+ dryRun: false,
8431
+ limit: 20,
8432
+ json: false
8433
+ };
8434
+ const rest = [];
8435
+ for (let i = 0; i < args.length; i += 1) {
8436
+ const arg = args[i];
8437
+ if (arg === '--allow-write') {
8438
+ options.allowWrite = true;
8439
+ continue;
8440
+ }
8441
+ if (arg === '--dry-run') {
8442
+ options.dryRun = true;
8443
+ continue;
8444
+ }
8445
+ if (arg === '--json') {
8446
+ options.json = true;
8447
+ continue;
8448
+ }
8449
+ if (arg === '--input') {
8450
+ options.inputRaw = args[i + 1] || '';
8451
+ i += 1;
8452
+ continue;
8453
+ }
8454
+ if (arg.startsWith('--input=')) {
8455
+ options.inputRaw = arg.slice('--input='.length);
8456
+ continue;
8457
+ }
8458
+ if (arg === '--limit') {
8459
+ const raw = args[i + 1];
8460
+ i += 1;
8461
+ const value = parseInt(raw, 10);
8462
+ if (Number.isFinite(value)) {
8463
+ options.limit = value;
8464
+ }
8465
+ continue;
8466
+ }
8467
+ if (arg.startsWith('--limit=')) {
8468
+ const value = parseInt(arg.slice('--limit='.length), 10);
8469
+ if (Number.isFinite(value)) {
8470
+ options.limit = value;
8471
+ }
8472
+ continue;
8473
+ }
8474
+ rest.push(arg);
8475
+ }
8476
+ return { options, rest };
8477
+ }
8478
+
8479
+ async function cmdWorkflow(args = []) {
8480
+ const argv = Array.isArray(args) ? args : [];
8481
+ if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
8482
+ printWorkflowHelp();
8483
+ return;
8484
+ }
8485
+ const subcommand = String(argv[0] || '').trim().toLowerCase();
8486
+ const parsed = parseWorkflowCliOptions(argv.slice(1));
8487
+ const options = parsed.options;
8488
+ const rest = parsed.rest;
8489
+
8490
+ if (subcommand === 'list') {
8491
+ const result = listWorkflowDefinitions();
8492
+ if (options.json) {
8493
+ console.log(JSON.stringify(result, null, 2));
8494
+ return;
8495
+ }
8496
+ const workflows = Array.isArray(result.workflows) ? result.workflows : [];
8497
+ console.log('\n可用工作流:');
8498
+ for (const item of workflows) {
8499
+ const mode = item.readOnly ? 'read-only' : 'read-write';
8500
+ console.log(` - ${item.id} (${mode}, steps=${item.stepCount})`);
8501
+ if (item.description) {
8502
+ console.log(` ${item.description}`);
8503
+ }
8504
+ }
8505
+ if (Array.isArray(result.warnings) && result.warnings.length > 0) {
8506
+ console.log('\n警告:');
8507
+ result.warnings.forEach((msg) => console.log(` - ${msg}`));
8508
+ }
8509
+ console.log();
8510
+ return;
8511
+ }
8512
+
8513
+ if (subcommand === 'runs') {
8514
+ const limit = Number.isFinite(options.limit) ? Math.max(1, Math.floor(options.limit)) : 20;
8515
+ const runs = listWorkflowRunRecords(limit);
8516
+ if (options.json) {
8517
+ console.log(JSON.stringify({ runs, limit }, null, 2));
8518
+ return;
8519
+ }
8520
+ console.log(`\n最近执行记录(${runs.length}/${limit}):`);
8521
+ for (const item of runs) {
8522
+ const status = item && item.success ? 'OK' : 'FAIL';
8523
+ console.log(` - [${status}] ${item.workflowId || '(unknown)'} runId=${item.runId || ''} duration=${item.durationMs || 0}ms`);
8524
+ if (item && item.error) {
8525
+ console.log(` error: ${item.error}`);
8526
+ }
8527
+ }
8528
+ console.log();
8529
+ return;
8530
+ }
8531
+
8532
+ const workflowId = typeof rest[0] === 'string' ? rest[0].trim() : '';
8533
+ if (!workflowId) {
8534
+ throw new Error('workflow id is required');
8535
+ }
8536
+ const input = parseWorkflowInputArg(options.inputRaw);
8537
+
8538
+ if (subcommand === 'get') {
8539
+ const result = getWorkflowDefinitionById(workflowId);
8540
+ if (result.error) {
8541
+ throw new Error(result.error);
8542
+ }
8543
+ console.log(JSON.stringify(result, null, 2));
8544
+ return;
8545
+ }
8546
+
8547
+ if (subcommand === 'validate') {
8548
+ const result = validateWorkflowById(workflowId, input);
8549
+ if (!result.ok) {
8550
+ throw new Error(result.error || 'workflow validate failed');
8551
+ }
8552
+ if (options.json) {
8553
+ console.log(JSON.stringify(result, null, 2));
8554
+ } else {
8555
+ console.log(`✓ 工作流校验通过: ${workflowId}`);
8556
+ if (Array.isArray(result.warnings) && result.warnings.length > 0) {
8557
+ result.warnings.forEach((msg) => console.log(` - ${msg}`));
8558
+ }
8559
+ console.log();
8560
+ }
8561
+ return;
8562
+ }
8563
+
8564
+ if (subcommand === 'run') {
8565
+ const result = await runWorkflowById(workflowId, input, {
8566
+ allowWrite: options.allowWrite,
8567
+ dryRun: options.dryRun
8568
+ });
8569
+ if (options.json) {
8570
+ console.log(JSON.stringify(result, null, 2));
8571
+ } else {
8572
+ if (result.error) {
8573
+ console.error(`✗ 工作流执行失败: ${result.error}`);
8574
+ } else {
8575
+ console.log(`✓ 工作流执行完成: ${workflowId} (${result.durationMs || 0}ms)`);
8576
+ }
8577
+ const steps = Array.isArray(result.steps) ? result.steps : [];
8578
+ for (const step of steps) {
8579
+ const status = step.status || 'unknown';
8580
+ const label = step.id || step.tool || '(step)';
8581
+ console.log(` - ${label}: ${status} (${step.durationMs || 0}ms)`);
8582
+ if (step.error) {
8583
+ console.log(` error: ${step.error}`);
8584
+ }
8585
+ }
8586
+ if (result.runId) {
8587
+ console.log(` runId: ${result.runId}`);
8588
+ }
8589
+ console.log();
8590
+ }
8591
+ if (result.error) {
8592
+ throw new Error(result.error);
8593
+ }
6948
8594
  return;
6949
8595
  }
6950
8596
 
6951
- throw new Error(`未知 proxy 子命令: ${subcommand}`);
8597
+ throw new Error(`未知 workflow 子命令: ${subcommand}`);
6952
8598
  }
6953
8599
 
6954
8600
  async function runProxyCommand(displayName, binNames, args = [], installTip = '') {
@@ -7103,6 +8749,7 @@ function buildMcpStatusPayload() {
7103
8749
  serviceTier,
7104
8750
  modelReasoningEffort,
7105
8751
  configReady: !statusConfigResult.isVirtual,
8752
+ configErrorType: statusConfigResult.errorType || '',
7106
8753
  configNotice: statusConfigResult.reason || '',
7107
8754
  initNotice: consumeInitNotice()
7108
8755
  };
@@ -7115,6 +8762,8 @@ function buildMcpProviderListPayload() {
7115
8762
  const current = listConfig.model_provider;
7116
8763
  return {
7117
8764
  configReady: !listConfigResult.isVirtual,
8765
+ configErrorType: listConfigResult.errorType || '',
8766
+ configNotice: listConfigResult.reason || '',
7118
8767
  providers: Object.entries(providers).map(([name, p]) => ({
7119
8768
  name,
7120
8769
  url: p.base_url || '',
@@ -7167,6 +8816,541 @@ function normalizeMcpSource(value) {
7167
8816
  return null;
7168
8817
  }
7169
8818
 
8819
+ const BUILTIN_WORKFLOW_DEFINITIONS = Object.freeze({
8820
+ 'diagnose-config': {
8821
+ id: 'diagnose-config',
8822
+ name: 'Diagnose Config',
8823
+ description: 'Collect status/providers/proxy snapshots for troubleshooting.',
8824
+ readOnly: true,
8825
+ inputSchema: {
8826
+ type: 'object',
8827
+ properties: {},
8828
+ additionalProperties: false
8829
+ },
8830
+ steps: [
8831
+ { id: 'status', tool: 'codexmate.status.get', arguments: {} },
8832
+ { id: 'providers', tool: 'codexmate.provider.list', arguments: {} },
8833
+ { id: 'proxy', tool: 'codexmate.proxy.status', arguments: {} }
8834
+ ]
8835
+ },
8836
+ 'safe-provider-switch': {
8837
+ id: 'safe-provider-switch',
8838
+ name: 'Safe Provider Switch',
8839
+ description: 'Build template for a provider switch and optionally apply it.',
8840
+ readOnly: false,
8841
+ inputSchema: {
8842
+ type: 'object',
8843
+ properties: {
8844
+ provider: { type: 'string' },
8845
+ model: { type: 'string' },
8846
+ serviceTier: { type: 'string' },
8847
+ reasoningEffort: { type: 'string' },
8848
+ apply: { type: 'boolean' }
8849
+ },
8850
+ required: ['provider'],
8851
+ additionalProperties: false
8852
+ },
8853
+ steps: [
8854
+ { id: 'providers', tool: 'codexmate.provider.list', arguments: {} },
8855
+ {
8856
+ id: 'template',
8857
+ tool: 'codexmate.config.template.get',
8858
+ arguments: {
8859
+ provider: '{{input.provider}}',
8860
+ model: '{{input.model}}',
8861
+ serviceTier: '{{input.serviceTier}}',
8862
+ reasoningEffort: '{{input.reasoningEffort}}'
8863
+ }
8864
+ },
8865
+ {
8866
+ id: 'apply',
8867
+ tool: 'codexmate.config.template.apply',
8868
+ when: { path: 'input.apply', equals: true },
8869
+ arguments: {
8870
+ template: '{{steps.template.output.template}}'
8871
+ }
8872
+ },
8873
+ {
8874
+ id: 'statusAfter',
8875
+ tool: 'codexmate.status.get',
8876
+ when: { path: 'input.apply', equals: true },
8877
+ arguments: {}
8878
+ }
8879
+ ]
8880
+ },
8881
+ 'session-issue-pack': {
8882
+ id: 'session-issue-pack',
8883
+ name: 'Session Issue Pack',
8884
+ description: 'Collect session detail and markdown export for issue reports.',
8885
+ readOnly: true,
8886
+ inputSchema: {
8887
+ type: 'object',
8888
+ properties: {
8889
+ source: { type: 'string' },
8890
+ sessionId: { type: 'string' },
8891
+ file: { type: 'string' },
8892
+ maxMessages: { type: ['string', 'number'] }
8893
+ },
8894
+ additionalProperties: true
8895
+ },
8896
+ steps: [
8897
+ {
8898
+ id: 'detail',
8899
+ tool: 'codexmate.session.detail',
8900
+ arguments: {
8901
+ source: '{{input.source}}',
8902
+ sessionId: '{{input.sessionId}}',
8903
+ file: '{{input.file}}',
8904
+ maxMessages: '{{input.maxMessages}}'
8905
+ }
8906
+ },
8907
+ {
8908
+ id: 'export',
8909
+ tool: 'codexmate.session.export',
8910
+ arguments: {
8911
+ source: '{{input.source}}',
8912
+ sessionId: '{{input.sessionId}}',
8913
+ file: '{{input.file}}',
8914
+ maxMessages: '{{input.maxMessages}}'
8915
+ }
8916
+ }
8917
+ ]
8918
+ }
8919
+ });
8920
+
8921
+ function cloneJson(value, fallback) {
8922
+ try {
8923
+ return JSON.parse(JSON.stringify(value));
8924
+ } catch (_) {
8925
+ return fallback;
8926
+ }
8927
+ }
8928
+
8929
+ function normalizeWorkflowId(value) {
8930
+ const raw = typeof value === 'string' ? value.trim() : '';
8931
+ if (!raw) return '';
8932
+ if (!/^[a-zA-Z0-9._-]+$/.test(raw)) {
8933
+ return '';
8934
+ }
8935
+ return raw.toLowerCase();
8936
+ }
8937
+
8938
+ function normalizeWorkflowDefinition(raw, idHint = '', source = 'custom') {
8939
+ const safe = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : null;
8940
+ if (!safe) {
8941
+ return { ok: false, error: 'workflow must be an object' };
8942
+ }
8943
+ const id = normalizeWorkflowId(safe.id || idHint);
8944
+ if (!id) {
8945
+ return { ok: false, error: 'workflow id is invalid' };
8946
+ }
8947
+ const name = typeof safe.name === 'string' && safe.name.trim()
8948
+ ? safe.name.trim()
8949
+ : id;
8950
+ const description = typeof safe.description === 'string' ? safe.description.trim() : '';
8951
+ const inputSchema = safe.inputSchema && typeof safe.inputSchema === 'object'
8952
+ ? cloneJson(safe.inputSchema, { type: 'object', properties: {}, additionalProperties: true })
8953
+ : { type: 'object', properties: {}, additionalProperties: true };
8954
+ const stepsRaw = Array.isArray(safe.steps) ? safe.steps : [];
8955
+ if (stepsRaw.length === 0) {
8956
+ return { ok: false, error: 'workflow steps cannot be empty' };
8957
+ }
8958
+
8959
+ const steps = [];
8960
+ for (let i = 0; i < stepsRaw.length; i += 1) {
8961
+ const item = stepsRaw[i];
8962
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
8963
+ return { ok: false, error: `workflow step #${i + 1} must be an object` };
8964
+ }
8965
+ const stepIdRaw = typeof item.id === 'string' && item.id.trim()
8966
+ ? item.id.trim()
8967
+ : `step${i + 1}`;
8968
+ const stepId = normalizeWorkflowId(stepIdRaw);
8969
+ if (!stepId) {
8970
+ return { ok: false, error: `workflow step id invalid at #${i + 1}` };
8971
+ }
8972
+ const toolName = typeof item.tool === 'string' ? item.tool.trim() : '';
8973
+ if (!toolName) {
8974
+ return { ok: false, error: `workflow step "${stepId}" missing tool` };
8975
+ }
8976
+ const args = item.arguments && typeof item.arguments === 'object' && !Array.isArray(item.arguments)
8977
+ ? cloneJson(item.arguments, {})
8978
+ : {};
8979
+ const when = item.when && typeof item.when === 'object' && !Array.isArray(item.when)
8980
+ ? cloneJson(item.when, {})
8981
+ : null;
8982
+ steps.push({
8983
+ id: stepId,
8984
+ name: typeof item.name === 'string' ? item.name.trim() : '',
8985
+ tool: toolName,
8986
+ arguments: args,
8987
+ when,
8988
+ continueOnError: item.continueOnError === true,
8989
+ write: item.write === true
8990
+ });
8991
+ }
8992
+
8993
+ return {
8994
+ ok: true,
8995
+ data: {
8996
+ id,
8997
+ name,
8998
+ description,
8999
+ source,
9000
+ readOnly: safe.readOnly !== false,
9001
+ inputSchema,
9002
+ steps
9003
+ }
9004
+ };
9005
+ }
9006
+
9007
+ function loadBuiltinWorkflowDefinitions() {
9008
+ const items = [];
9009
+ for (const [id, raw] of Object.entries(BUILTIN_WORKFLOW_DEFINITIONS)) {
9010
+ const normalized = normalizeWorkflowDefinition(raw, id, 'builtin');
9011
+ if (!normalized.ok) {
9012
+ continue;
9013
+ }
9014
+ items.push(normalized.data);
9015
+ }
9016
+ return items;
9017
+ }
9018
+
9019
+ function loadCustomWorkflowDefinitions() {
9020
+ const parsed = readJsonObjectFromFile(WORKFLOW_DEFINITIONS_FILE, {});
9021
+ if (!parsed.ok || !parsed.exists) {
9022
+ return {
9023
+ items: [],
9024
+ warnings: parsed.ok ? [] : [parsed.error || 'workflow file parse failed']
9025
+ };
9026
+ }
9027
+ const data = parsed.data && typeof parsed.data === 'object' ? parsed.data : {};
9028
+ let list = [];
9029
+ if (Array.isArray(data.workflows)) {
9030
+ list = data.workflows;
9031
+ } else if (data.workflows && typeof data.workflows === 'object') {
9032
+ list = Object.entries(data.workflows).map(([id, item]) => ({ ...(item || {}), id }));
9033
+ } else {
9034
+ list = Object.entries(data).map(([id, item]) => ({ ...(item || {}), id }));
9035
+ }
9036
+
9037
+ const items = [];
9038
+ const warnings = [];
9039
+ for (const item of list) {
9040
+ const normalized = normalizeWorkflowDefinition(item, item && item.id ? item.id : '', 'custom');
9041
+ if (!normalized.ok) {
9042
+ warnings.push(normalized.error || 'invalid custom workflow');
9043
+ continue;
9044
+ }
9045
+ items.push(normalized.data);
9046
+ }
9047
+ return { items, warnings };
9048
+ }
9049
+
9050
+ function buildWorkflowRegistry() {
9051
+ const registry = new Map();
9052
+ const warnings = [];
9053
+ const builtin = loadBuiltinWorkflowDefinitions();
9054
+ for (const item of builtin) {
9055
+ registry.set(item.id, item);
9056
+ }
9057
+ const custom = loadCustomWorkflowDefinitions();
9058
+ for (const item of custom.items) {
9059
+ if (registry.has(item.id)) {
9060
+ warnings.push(`custom workflow id duplicated with builtin and ignored: ${item.id}`);
9061
+ continue;
9062
+ }
9063
+ registry.set(item.id, item);
9064
+ }
9065
+ warnings.push(...custom.warnings);
9066
+ return { registry, warnings };
9067
+ }
9068
+
9069
+ function listWorkflowDefinitions() {
9070
+ const { registry, warnings } = buildWorkflowRegistry();
9071
+ const workflows = Array.from(registry.values())
9072
+ .sort((a, b) => a.id.localeCompare(b.id))
9073
+ .map((item) => ({
9074
+ id: item.id,
9075
+ name: item.name,
9076
+ description: item.description,
9077
+ source: item.source,
9078
+ readOnly: item.readOnly !== false,
9079
+ stepCount: Array.isArray(item.steps) ? item.steps.length : 0
9080
+ }));
9081
+ return {
9082
+ workflows,
9083
+ warnings
9084
+ };
9085
+ }
9086
+
9087
+ function getWorkflowDefinitionById(rawId) {
9088
+ const id = normalizeWorkflowId(rawId);
9089
+ if (!id) {
9090
+ return { error: 'workflow id is required' };
9091
+ }
9092
+ const { registry, warnings } = buildWorkflowRegistry();
9093
+ const workflow = registry.get(id);
9094
+ if (!workflow) {
9095
+ return { error: `workflow not found: ${id}` };
9096
+ }
9097
+ return {
9098
+ workflow: cloneJson(workflow, {}),
9099
+ warnings
9100
+ };
9101
+ }
9102
+
9103
+ function createWorkflowToolCatalog() {
9104
+ return {
9105
+ 'codexmate.status.get': {
9106
+ readOnly: true,
9107
+ handler: async () => buildMcpStatusPayload()
9108
+ },
9109
+ 'codexmate.provider.list': {
9110
+ readOnly: true,
9111
+ handler: async () => buildMcpProviderListPayload()
9112
+ },
9113
+ 'codexmate.proxy.status': {
9114
+ readOnly: true,
9115
+ handler: async () => getBuiltinProxyStatus()
9116
+ },
9117
+ 'codexmate.session.list': {
9118
+ readOnly: true,
9119
+ handler: async (args = {}) => {
9120
+ const source = normalizeMcpSource(args.source);
9121
+ if (source === null) {
9122
+ return { error: 'Invalid source. Must be codex, claude, or all' };
9123
+ }
9124
+ return {
9125
+ source: source || 'all',
9126
+ sessions: listAllSessions({
9127
+ ...args,
9128
+ source: source || 'all'
9129
+ })
9130
+ };
9131
+ }
9132
+ },
9133
+ 'codexmate.session.detail': {
9134
+ readOnly: true,
9135
+ handler: async (args = {}) => readSessionDetail(args || {})
9136
+ },
9137
+ 'codexmate.session.export': {
9138
+ readOnly: true,
9139
+ handler: async (args = {}) => exportSessionData(args || {})
9140
+ },
9141
+ 'codexmate.config.template.get': {
9142
+ readOnly: true,
9143
+ handler: async (args = {}) => getConfigTemplate(args || {})
9144
+ },
9145
+ 'codexmate.config.template.apply': {
9146
+ readOnly: false,
9147
+ handler: async (args = {}) => applyConfigTemplate(args || {})
9148
+ }
9149
+ };
9150
+ }
9151
+
9152
+ function getWorkflowKnownToolsSet() {
9153
+ return new Set(Object.keys(createWorkflowToolCatalog()));
9154
+ }
9155
+
9156
+ function resolveWorkflowDefinitionWithToolMeta(workflow) {
9157
+ const catalog = createWorkflowToolCatalog();
9158
+ const safe = cloneJson(workflow, {});
9159
+ safe.steps = (Array.isArray(safe.steps) ? safe.steps : []).map((step) => {
9160
+ const tool = catalog[step.tool];
9161
+ return {
9162
+ ...step,
9163
+ write: step.write === true || !!(tool && tool.readOnly === false)
9164
+ };
9165
+ });
9166
+ return safe;
9167
+ }
9168
+
9169
+ function validateWorkflowInputBySchema(inputSchema, input) {
9170
+ const schema = inputSchema && typeof inputSchema === 'object' ? inputSchema : {};
9171
+ if (schema.type && schema.type !== 'object') {
9172
+ return { ok: false, error: `unsupported input schema type: ${schema.type}` };
9173
+ }
9174
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
9175
+ return { ok: false, error: 'workflow input must be an object' };
9176
+ }
9177
+ const required = Array.isArray(schema.required) ? schema.required : [];
9178
+ for (const key of required) {
9179
+ if (!Object.prototype.hasOwnProperty.call(input, key)) {
9180
+ return { ok: false, error: `missing required input field: ${key}` };
9181
+ }
9182
+ }
9183
+ const properties = schema.properties && typeof schema.properties === 'object' ? schema.properties : {};
9184
+ for (const [key, expected] of Object.entries(properties)) {
9185
+ if (!Object.prototype.hasOwnProperty.call(input, key)) continue;
9186
+ const value = input[key];
9187
+ if (!expected || typeof expected !== 'object') continue;
9188
+ const type = expected.type;
9189
+ if (!type) continue;
9190
+ const typeList = Array.isArray(type) ? type : [type];
9191
+ const actualType = value === null ? 'null' : (Array.isArray(value) ? 'array' : typeof value);
9192
+ const matched = typeList.some((candidate) => {
9193
+ if (candidate === 'number') return typeof value === 'number' && Number.isFinite(value);
9194
+ if (candidate === 'integer') return Number.isInteger(value);
9195
+ if (candidate === 'array') return Array.isArray(value);
9196
+ if (candidate === 'object') return value && typeof value === 'object' && !Array.isArray(value);
9197
+ if (candidate === 'null') return value === null;
9198
+ return actualType === candidate;
9199
+ });
9200
+ if (!matched) {
9201
+ return { ok: false, error: `input field "${key}" type mismatch` };
9202
+ }
9203
+ }
9204
+ return { ok: true };
9205
+ }
9206
+
9207
+ function appendWorkflowRunRecord(record) {
9208
+ ensureDir(path.dirname(WORKFLOW_RUNS_FILE));
9209
+ const content = `${JSON.stringify(record)}\n`;
9210
+ fs.appendFileSync(WORKFLOW_RUNS_FILE, content, { encoding: 'utf-8', mode: 0o600 });
9211
+ }
9212
+
9213
+ function listWorkflowRunRecords(limit = 20) {
9214
+ const max = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 20;
9215
+ if (!fs.existsSync(WORKFLOW_RUNS_FILE)) {
9216
+ return [];
9217
+ }
9218
+ let content = '';
9219
+ try {
9220
+ content = fs.readFileSync(WORKFLOW_RUNS_FILE, 'utf-8');
9221
+ } catch (_) {
9222
+ return [];
9223
+ }
9224
+ const rows = content
9225
+ .split(/\r?\n/g)
9226
+ .map((line) => line.trim())
9227
+ .filter(Boolean);
9228
+ const parsed = [];
9229
+ for (let i = rows.length - 1; i >= 0; i -= 1) {
9230
+ try {
9231
+ const item = JSON.parse(rows[i]);
9232
+ parsed.push(item);
9233
+ if (parsed.length >= max) {
9234
+ break;
9235
+ }
9236
+ } catch (_) {}
9237
+ }
9238
+ return parsed;
9239
+ }
9240
+
9241
+ function validateWorkflowById(workflowId, input = {}) {
9242
+ const definitionResult = getWorkflowDefinitionById(workflowId);
9243
+ if (definitionResult.error) {
9244
+ return { ok: false, error: definitionResult.error };
9245
+ }
9246
+ const workflow = resolveWorkflowDefinitionWithToolMeta(definitionResult.workflow);
9247
+ const knownTools = getWorkflowKnownToolsSet();
9248
+ const validation = validateWorkflowDefinition(workflow, { knownTools });
9249
+ if (!validation.ok) {
9250
+ return {
9251
+ ok: false,
9252
+ error: validation.error || 'workflow validation failed',
9253
+ issues: validation.issues || []
9254
+ };
9255
+ }
9256
+ const schemaValidation = validateWorkflowInputBySchema(workflow.inputSchema, input || {});
9257
+ if (!schemaValidation.ok) {
9258
+ return { ok: false, error: schemaValidation.error || 'workflow input validation failed' };
9259
+ }
9260
+ return {
9261
+ ok: true,
9262
+ workflow: {
9263
+ id: workflow.id,
9264
+ name: workflow.name,
9265
+ readOnly: workflow.readOnly !== false,
9266
+ stepCount: Array.isArray(workflow.steps) ? workflow.steps.length : 0
9267
+ },
9268
+ warnings: definitionResult.warnings || []
9269
+ };
9270
+ }
9271
+
9272
+ async function runWorkflowById(workflowId, input = {}, options = {}) {
9273
+ const definitionResult = getWorkflowDefinitionById(workflowId);
9274
+ if (definitionResult.error) {
9275
+ return { error: definitionResult.error };
9276
+ }
9277
+ const workflow = resolveWorkflowDefinitionWithToolMeta(definitionResult.workflow);
9278
+ const knownTools = getWorkflowKnownToolsSet();
9279
+ const validation = validateWorkflowDefinition(workflow, { knownTools });
9280
+ if (!validation.ok) {
9281
+ return {
9282
+ error: validation.error || 'workflow validation failed',
9283
+ issues: validation.issues || []
9284
+ };
9285
+ }
9286
+ const schemaValidation = validateWorkflowInputBySchema(workflow.inputSchema, input || {});
9287
+ if (!schemaValidation.ok) {
9288
+ return { error: schemaValidation.error || 'workflow input validation failed' };
9289
+ }
9290
+
9291
+ const catalog = createWorkflowToolCatalog();
9292
+ const allowWrite = options.allowWrite === true;
9293
+ const dryRun = options.dryRun === true;
9294
+ const runId = `wf-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
9295
+ const startedAt = toIsoTime(Date.now());
9296
+
9297
+ const execution = await executeWorkflowDefinition(workflow, input || {}, {
9298
+ allowWrite,
9299
+ dryRun,
9300
+ invokeTool: async (toolName, args = {}) => {
9301
+ const tool = catalog[toolName];
9302
+ if (!tool) {
9303
+ return { error: `workflow tool not supported: ${toolName}` };
9304
+ }
9305
+ if (!tool.readOnly && !allowWrite) {
9306
+ return { error: `workflow requires write permission for tool: ${toolName}` };
9307
+ }
9308
+ return tool.handler(args || {});
9309
+ }
9310
+ });
9311
+
9312
+ const endedAt = toIsoTime(Date.now());
9313
+ const record = {
9314
+ runId,
9315
+ workflowId: workflow.id,
9316
+ workflowName: workflow.name,
9317
+ success: execution.success === true,
9318
+ error: execution.error || '',
9319
+ allowWrite,
9320
+ dryRun,
9321
+ startedAt,
9322
+ endedAt,
9323
+ durationMs: execution.durationMs || 0,
9324
+ steps: Array.isArray(execution.steps) ? execution.steps.map((step) => ({
9325
+ id: step.id,
9326
+ tool: step.tool,
9327
+ status: step.status,
9328
+ durationMs: step.durationMs || 0,
9329
+ error: step.error || ''
9330
+ })) : [],
9331
+ input: cloneJson(input || {}, {})
9332
+ };
9333
+ try {
9334
+ appendWorkflowRunRecord(record);
9335
+ } catch (_) {}
9336
+
9337
+ return {
9338
+ success: execution.success === true,
9339
+ runId,
9340
+ workflowId: workflow.id,
9341
+ workflowName: workflow.name,
9342
+ allowWrite,
9343
+ dryRun,
9344
+ startedAt: execution.startedAt || startedAt,
9345
+ endedAt: execution.endedAt || endedAt,
9346
+ durationMs: execution.durationMs || 0,
9347
+ steps: execution.steps || [],
9348
+ output: execution.output || null,
9349
+ warnings: definitionResult.warnings || [],
9350
+ ...(execution.error ? { error: execution.error } : {})
9351
+ };
9352
+ }
9353
+
7170
9354
  function createMcpTools(options = {}) {
7171
9355
  const allowWrite = !!options.allowWrite;
7172
9356
  const tools = [];
@@ -7362,6 +9546,89 @@ function createMcpTools(options = {}) {
7362
9546
  handler: async () => getBuiltinProxyStatus()
7363
9547
  });
7364
9548
 
9549
+ pushTool({
9550
+ name: 'codexmate.workflow.list',
9551
+ description: 'List available workflows (builtin + custom).',
9552
+ readOnly: true,
9553
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
9554
+ handler: async () => listWorkflowDefinitions()
9555
+ });
9556
+
9557
+ pushTool({
9558
+ name: 'codexmate.workflow.get',
9559
+ description: 'Get one workflow definition by id.',
9560
+ readOnly: true,
9561
+ inputSchema: {
9562
+ type: 'object',
9563
+ properties: {
9564
+ id: { type: 'string' }
9565
+ },
9566
+ required: ['id'],
9567
+ additionalProperties: false
9568
+ },
9569
+ handler: async (args = {}) => {
9570
+ const id = typeof args.id === 'string' ? args.id.trim() : '';
9571
+ if (!id) {
9572
+ return { error: 'workflow id is required' };
9573
+ }
9574
+ return getWorkflowDefinitionById(id);
9575
+ }
9576
+ });
9577
+
9578
+ pushTool({
9579
+ name: 'codexmate.workflow.validate',
9580
+ description: 'Validate workflow definition and input payload.',
9581
+ readOnly: true,
9582
+ inputSchema: {
9583
+ type: 'object',
9584
+ properties: {
9585
+ id: { type: 'string' },
9586
+ input: { type: 'object' }
9587
+ },
9588
+ required: ['id'],
9589
+ additionalProperties: false
9590
+ },
9591
+ handler: async (args = {}) => {
9592
+ const id = typeof args.id === 'string' ? args.id.trim() : '';
9593
+ if (!id) {
9594
+ return { ok: false, error: 'workflow id is required' };
9595
+ }
9596
+ const input = args.input && typeof args.input === 'object' && !Array.isArray(args.input)
9597
+ ? args.input
9598
+ : {};
9599
+ return validateWorkflowById(id, input);
9600
+ }
9601
+ });
9602
+
9603
+ pushTool({
9604
+ name: 'codexmate.workflow.run',
9605
+ description: 'Run workflow by id. Write steps require allow-write mode.',
9606
+ readOnly: true,
9607
+ inputSchema: {
9608
+ type: 'object',
9609
+ properties: {
9610
+ id: { type: 'string' },
9611
+ input: { type: 'object' },
9612
+ dryRun: { type: 'boolean' }
9613
+ },
9614
+ required: ['id'],
9615
+ additionalProperties: false
9616
+ },
9617
+ handler: async (args = {}) => {
9618
+ const id = typeof args.id === 'string' ? args.id.trim() : '';
9619
+ if (!id) {
9620
+ return { error: 'workflow id is required' };
9621
+ }
9622
+ const input = args.input && typeof args.input === 'object' && !Array.isArray(args.input)
9623
+ ? args.input
9624
+ : {};
9625
+ return runWorkflowById(id, input, {
9626
+ allowWrite,
9627
+ dryRun: args.dryRun === true
9628
+ });
9629
+ }
9630
+ });
9631
+
7365
9632
  pushTool({
7366
9633
  name: 'codexmate.config.template.apply',
7367
9634
  description: 'Apply Codex TOML template and sync auth/model pointers.',
@@ -7636,6 +9903,50 @@ function createMcpResources() {
7636
9903
  }]
7637
9904
  };
7638
9905
  }
9906
+ },
9907
+ {
9908
+ uri: 'codexmate://workflows',
9909
+ name: 'Workflows',
9910
+ description: 'Workflow list resource (builtin + custom).',
9911
+ mimeType: 'application/json',
9912
+ read: async () => ({
9913
+ contents: [{
9914
+ uri: 'codexmate://workflows',
9915
+ mimeType: 'application/json',
9916
+ text: JSON.stringify(listWorkflowDefinitions(), null, 2)
9917
+ }]
9918
+ })
9919
+ },
9920
+ {
9921
+ uri: 'codexmate://workflow-runs',
9922
+ name: 'WorkflowRuns',
9923
+ description: 'Recent workflow execution records. Supports ?limit=<N>.',
9924
+ mimeType: 'application/json',
9925
+ read: async (params = {}) => {
9926
+ const uri = typeof params.uri === 'string' ? params.uri : 'codexmate://workflow-runs';
9927
+ let limit = 20;
9928
+ try {
9929
+ const parsed = new URL(uri);
9930
+ const rawLimit = parsed.searchParams.get('limit');
9931
+ if (rawLimit) {
9932
+ const parsedLimit = parseInt(rawLimit, 10);
9933
+ if (Number.isFinite(parsedLimit)) {
9934
+ limit = parsedLimit;
9935
+ }
9936
+ }
9937
+ } catch (_) {}
9938
+ const payload = {
9939
+ runs: listWorkflowRunRecords(limit),
9940
+ limit: Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 20
9941
+ };
9942
+ return {
9943
+ contents: [{
9944
+ uri,
9945
+ mimeType: 'application/json',
9946
+ text: JSON.stringify(payload, null, 2)
9947
+ }]
9948
+ };
9949
+ }
7639
9950
  }
7640
9951
  ];
7641
9952
  }
@@ -7820,6 +10131,7 @@ async function main() {
7820
10131
  console.log(' codexmate delete-model <模型> 删除模型');
7821
10132
  console.log(' codexmate auth <list|import|switch|delete|status> 认证文件管理');
7822
10133
  console.log(' codexmate proxy <status|set|apply|enable|start|stop> 内建代理');
10134
+ console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
7823
10135
  console.log(' codexmate run [--host <HOST>] 启动 Web 界面');
7824
10136
  console.log(' codexmate codex [参数...] 等同于 codex --yolo');
7825
10137
  console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
@@ -7846,6 +10158,7 @@ async function main() {
7846
10158
  case 'delete-model': cmdDeleteModel(args[1]); break;
7847
10159
  case 'auth': cmdAuth(args.slice(1)); break;
7848
10160
  case 'proxy': await cmdProxy(args.slice(1)); break;
10161
+ case 'workflow': await cmdWorkflow(args.slice(1)); break;
7849
10162
  case 'run': cmdStart(parseStartOptions(args.slice(1))); break;
7850
10163
  case 'start':
7851
10164
  console.error('错误: 命令已更名为 "run",请使用: codexmate run');