flowmind 1.2.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/flowmind.js +23 -16
- package/core/config-manager.js +1 -0
- package/core/learning-engine.js +122 -30
- package/mcp/server.js +2 -1
- package/package.json +1 -1
- package/skills/api-sync/index.js +130 -0
- package/skills/archive-change/index.js +104 -0
- package/skills/auto-flow/index.js +124 -0
- package/skills/code-review/index.js +79 -0
- package/skills/code-review-audit/index.js +77 -0
- package/skills/data-logic-validation/index.js +108 -0
- package/skills/data-validation/index.js +72 -0
- package/skills/git-review/index.js +73 -0
- package/skills/learning-engine/index.js +50 -0
- package/skills/learning-feedback/index.js +83 -0
- package/skills/log-audit/index.js +88 -0
- package/skills/project-review/index.js +105 -0
- package/skills/requirement-analyst/index.js +88 -0
- package/skills/resource-bind/index.js +60 -0
- package/skills/sls-log-audit/index.js +120 -0
- package/skills/yapi-sync-interface/index.js +101 -0
- package/skills/yuque-sync-design/index.js +133 -0
package/bin/flowmind.js
CHANGED
|
@@ -536,11 +536,15 @@ program
|
|
|
536
536
|
await fm.importLearnings(data);
|
|
537
537
|
console.log(chalk.green('✓ Learnings imported successfully'));
|
|
538
538
|
} else if (options.reset) {
|
|
539
|
-
|
|
540
|
-
|
|
539
|
+
const count = await fm.learning.resetSkill(options.reset);
|
|
540
|
+
console.log(chalk.green(`✓ Reset ${count} learning(s) for skill: ${options.reset}`));
|
|
541
541
|
} else if (options.delete) {
|
|
542
|
-
|
|
543
|
-
|
|
542
|
+
const deleted = await fm.learning.deleteRecord(options.delete);
|
|
543
|
+
if (deleted) {
|
|
544
|
+
console.log(chalk.green(`✓ Deleted learning record: ${options.delete}`));
|
|
545
|
+
} else {
|
|
546
|
+
console.log(chalk.yellow(`Record not found: ${options.delete}`));
|
|
547
|
+
}
|
|
544
548
|
} else {
|
|
545
549
|
// Default to list
|
|
546
550
|
const stats = await fm.getStats();
|
|
@@ -620,7 +624,7 @@ program
|
|
|
620
624
|
.description('View or modify skill configuration')
|
|
621
625
|
.option('-i, --info', 'Show skill info (default)')
|
|
622
626
|
.option('-c, --config', 'Show/edit skill configuration')
|
|
623
|
-
.option('-s, --set <key>
|
|
627
|
+
.option('-s, --set <key>', 'Set config value (value as next argument)')
|
|
624
628
|
.option('-r, --read', 'Read SKILL.md content')
|
|
625
629
|
.option('-e, --edit', 'Open SKILL.md in editor')
|
|
626
630
|
.option('-j, --json', 'Output as JSON (for tool integration)')
|
|
@@ -650,13 +654,10 @@ program
|
|
|
650
654
|
} else if (options.config) {
|
|
651
655
|
await showSkillConfig(skill, fm, options.json);
|
|
652
656
|
} else if (options.set) {
|
|
653
|
-
// options.set is the key, need value from next arg
|
|
654
|
-
const value = options.set;
|
|
655
|
-
const key = options.set;
|
|
656
|
-
// Get key and value from command line
|
|
657
657
|
const args = process.argv.slice(3);
|
|
658
|
-
|
|
659
|
-
|
|
658
|
+
const value = args.find(a => !a.startsWith('-'));
|
|
659
|
+
if (value) {
|
|
660
|
+
await setSkillConfig(skill, fm, options.set, value);
|
|
660
661
|
} else {
|
|
661
662
|
console.error(chalk.red('Usage: flowmind skill <name> --set <key> <value>'));
|
|
662
663
|
}
|
|
@@ -775,7 +776,7 @@ program
|
|
|
775
776
|
.command('config')
|
|
776
777
|
.description('Manage configuration')
|
|
777
778
|
.option('-l, --list', 'List configuration')
|
|
778
|
-
.option('-s, --set <key>
|
|
779
|
+
.option('-s, --set <key>', 'Set configuration value (value as next argument)')
|
|
779
780
|
.option('-g, --get <key>', 'Get configuration value')
|
|
780
781
|
.action(async (options) => {
|
|
781
782
|
try {
|
|
@@ -785,10 +786,16 @@ program
|
|
|
785
786
|
const config = fm.config.getAll();
|
|
786
787
|
console.log(chalk.cyan('\nFlowMind Configuration:'));
|
|
787
788
|
console.log(JSON.stringify(config, null, 2));
|
|
788
|
-
} else if (options.set
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
789
|
+
} else if (options.set) {
|
|
790
|
+
const args = process.argv.slice(3);
|
|
791
|
+
const value = args.find(a => !a.startsWith('-'));
|
|
792
|
+
if (value) {
|
|
793
|
+
fm.config.set(options.set, value);
|
|
794
|
+
await fm.config.save();
|
|
795
|
+
console.log(chalk.green('✓ Configuration updated'));
|
|
796
|
+
} else {
|
|
797
|
+
console.error(chalk.red('Usage: flowmind config --set <key> <value>'));
|
|
798
|
+
}
|
|
792
799
|
} else if (options.get) {
|
|
793
800
|
const value = fm.config.get(options.get);
|
|
794
801
|
console.log(value);
|
package/core/config-manager.js
CHANGED
package/core/learning-engine.js
CHANGED
|
@@ -8,11 +8,31 @@ const path = require('path');
|
|
|
8
8
|
const { v4: uuidv4 } = require('uuid');
|
|
9
9
|
const eventBus = require('./event-bus');
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Per-key write queue to prevent concurrent read-modify-write races
|
|
13
|
+
*/
|
|
14
|
+
class WriteQueue {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.queues = new Map();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async run(key, fn) {
|
|
20
|
+
if (!this.queues.has(key)) {
|
|
21
|
+
this.queues.set(key, Promise.resolve());
|
|
22
|
+
}
|
|
23
|
+
const prev = this.queues.get(key);
|
|
24
|
+
const next = prev.then(fn, fn);
|
|
25
|
+
this.queues.set(key, next);
|
|
26
|
+
return next;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
class LearningEngine {
|
|
12
31
|
constructor(config, honorEngine = null) {
|
|
13
32
|
this.config = config;
|
|
14
33
|
this.honorEngine = honorEngine;
|
|
15
34
|
this.learningPath = config.get('learning.storagePath', '~/.flowmind/learning');
|
|
35
|
+
this.writeQueue = new WriteQueue();
|
|
16
36
|
this.records = {};
|
|
17
37
|
this.skillBindings = {};
|
|
18
38
|
this.stats = {};
|
|
@@ -144,7 +164,7 @@ class LearningEngine {
|
|
|
144
164
|
*/
|
|
145
165
|
async recordCorrection(correction, context) {
|
|
146
166
|
const record = {
|
|
147
|
-
id: `learn-${Date.now()}-${uuidv4().
|
|
167
|
+
id: `learn-${Date.now()}-${uuidv4().slice(0, 8)}`,
|
|
148
168
|
timestamp: new Date().toISOString(),
|
|
149
169
|
type: correction.type,
|
|
150
170
|
severity: correction.severity,
|
|
@@ -191,7 +211,7 @@ class LearningEngine {
|
|
|
191
211
|
*/
|
|
192
212
|
async recordSceneMapping(sceneMapping, context) {
|
|
193
213
|
const record = {
|
|
194
|
-
id: `scene-${Date.now()}-${uuidv4().
|
|
214
|
+
id: `scene-${Date.now()}-${uuidv4().slice(0, 8)}`,
|
|
195
215
|
timestamp: new Date().toISOString(),
|
|
196
216
|
type: 'scene_mapping',
|
|
197
217
|
input: sceneMapping.input,
|
|
@@ -232,7 +252,7 @@ class LearningEngine {
|
|
|
232
252
|
*/
|
|
233
253
|
async recordPreference(preference, context) {
|
|
234
254
|
const record = {
|
|
235
|
-
id: `pref-${Date.now()}-${uuidv4().
|
|
255
|
+
id: `pref-${Date.now()}-${uuidv4().slice(0, 8)}`,
|
|
236
256
|
timestamp: new Date().toISOString(),
|
|
237
257
|
type: 'preference',
|
|
238
258
|
preferenceType: preference.preferenceType,
|
|
@@ -309,16 +329,17 @@ class LearningEngine {
|
|
|
309
329
|
*/
|
|
310
330
|
async saveSceneMapping(record) {
|
|
311
331
|
const scenesPath = path.join(this.expandPath(this.learningPath), 'scenes.json');
|
|
332
|
+
await this.writeQueue.run('scenes.json', async () => {
|
|
333
|
+
let scenes = { version: '1.0', mappings: [] };
|
|
334
|
+
if (await fs.pathExists(scenesPath)) {
|
|
335
|
+
scenes = await fs.readJson(scenesPath);
|
|
336
|
+
}
|
|
312
337
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
scenes = await fs.readJson(scenesPath);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
scenes.mappings.push(record);
|
|
319
|
-
scenes.lastUpdated = new Date().toISOString();
|
|
338
|
+
scenes.mappings.push(record);
|
|
339
|
+
scenes.lastUpdated = new Date().toISOString();
|
|
320
340
|
|
|
321
|
-
|
|
341
|
+
await fs.writeJson(scenesPath, scenes, { spaces: 2 });
|
|
342
|
+
});
|
|
322
343
|
}
|
|
323
344
|
|
|
324
345
|
/**
|
|
@@ -331,17 +352,19 @@ class LearningEngine {
|
|
|
331
352
|
record.skill,
|
|
332
353
|
'preferences.json'
|
|
333
354
|
);
|
|
355
|
+
const queueKey = `prefs:${record.skill}`;
|
|
356
|
+
await this.writeQueue.run(queueKey, async () => {
|
|
357
|
+
let prefs = {};
|
|
358
|
+
if (await fs.pathExists(prefsPath)) {
|
|
359
|
+
prefs = await fs.readJson(prefsPath);
|
|
360
|
+
}
|
|
334
361
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
prefs = await fs.readJson(prefsPath);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
prefs[record.preferenceType] = record.value;
|
|
341
|
-
prefs.lastUpdated = new Date().toISOString();
|
|
362
|
+
prefs[record.preferenceType] = record.value;
|
|
363
|
+
prefs.lastUpdated = new Date().toISOString();
|
|
342
364
|
|
|
343
|
-
|
|
344
|
-
|
|
365
|
+
await fs.ensureDir(path.dirname(prefsPath));
|
|
366
|
+
await fs.writeJson(prefsPath, prefs, { spaces: 2 });
|
|
367
|
+
});
|
|
345
368
|
}
|
|
346
369
|
|
|
347
370
|
/**
|
|
@@ -387,9 +410,11 @@ class LearningEngine {
|
|
|
387
410
|
* Save skill bindings
|
|
388
411
|
*/
|
|
389
412
|
async saveSkillBindings() {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
413
|
+
await this.writeQueue.run('bindings', async () => {
|
|
414
|
+
const bindingsPath = path.join(this.expandPath(this.learningPath), 'skill-bindings.json');
|
|
415
|
+
this.skillBindings.lastUpdated = new Date().toISOString();
|
|
416
|
+
await fs.writeJson(bindingsPath, this.skillBindings, { spaces: 2 });
|
|
417
|
+
});
|
|
393
418
|
}
|
|
394
419
|
|
|
395
420
|
/**
|
|
@@ -414,13 +439,15 @@ class LearningEngine {
|
|
|
414
439
|
* Update stats
|
|
415
440
|
*/
|
|
416
441
|
async updateStats(type, skill) {
|
|
417
|
-
this.stats
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
442
|
+
await this.writeQueue.run('stats', async () => {
|
|
443
|
+
this.stats.totalRecords++;
|
|
444
|
+
this.stats.byType[type] = (this.stats.byType[type] || 0) + 1;
|
|
445
|
+
this.stats.bySkill[skill] = (this.stats.bySkill[skill] || 0) + 1;
|
|
446
|
+
this.stats.lastLearning = new Date().toISOString();
|
|
447
|
+
|
|
448
|
+
const statsPath = path.join(this.expandPath(this.learningPath), 'stats.json');
|
|
449
|
+
await fs.writeJson(statsPath, this.stats, { spaces: 2 });
|
|
450
|
+
});
|
|
424
451
|
|
|
425
452
|
// Award honor points for learning
|
|
426
453
|
if (this.honorEngine) {
|
|
@@ -484,6 +511,71 @@ class LearningEngine {
|
|
|
484
511
|
return { success: true, imported: data.stats.totalRecords };
|
|
485
512
|
}
|
|
486
513
|
|
|
514
|
+
/**
|
|
515
|
+
* Reset all learnings for a specific skill
|
|
516
|
+
*/
|
|
517
|
+
async resetSkill(skillName) {
|
|
518
|
+
const basePath = this.expandPath(this.learningPath);
|
|
519
|
+
|
|
520
|
+
// Delete records directory for this skill
|
|
521
|
+
const recordsDir = path.join(basePath, 'records', skillName);
|
|
522
|
+
if (await fs.pathExists(recordsDir)) {
|
|
523
|
+
await fs.remove(recordsDir);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Remove from skill bindings
|
|
527
|
+
if (this.skillBindings.bindings && this.skillBindings.bindings[skillName]) {
|
|
528
|
+
delete this.skillBindings.bindings[skillName];
|
|
529
|
+
await this.saveSkillBindings();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Update stats
|
|
533
|
+
const count = (this.records[skillName] || []).length;
|
|
534
|
+
if (count > 0 && this.stats.totalRecords) {
|
|
535
|
+
this.stats.totalRecords = Math.max(0, this.stats.totalRecords - count);
|
|
536
|
+
}
|
|
537
|
+
if (this.stats.bySkill && this.stats.bySkill[skillName]) {
|
|
538
|
+
delete this.stats.bySkill[skillName];
|
|
539
|
+
}
|
|
540
|
+
await this.saveStats();
|
|
541
|
+
delete this.records[skillName];
|
|
542
|
+
|
|
543
|
+
return count;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Delete a specific learning record by ID
|
|
548
|
+
*/
|
|
549
|
+
async deleteRecord(recordId) {
|
|
550
|
+
const basePath = path.join(this.expandPath(this.learningPath), 'records');
|
|
551
|
+
if (!(await fs.pathExists(basePath))) return false;
|
|
552
|
+
|
|
553
|
+
const skillDirs = await fs.readdir(basePath);
|
|
554
|
+
for (const skill of skillDirs) {
|
|
555
|
+
const recordPath = path.join(basePath, skill, `${recordId}.json`);
|
|
556
|
+
if (await fs.pathExists(recordPath)) {
|
|
557
|
+
await fs.remove(recordPath);
|
|
558
|
+
// Remove from memory cache
|
|
559
|
+
if (this.records[skill]) {
|
|
560
|
+
this.records[skill] = this.records[skill].filter(r => r.id !== recordId);
|
|
561
|
+
}
|
|
562
|
+
// Update stats
|
|
563
|
+
if (this.stats.totalRecords) this.stats.totalRecords--;
|
|
564
|
+
await this.saveStats();
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Save stats to disk
|
|
573
|
+
*/
|
|
574
|
+
async saveStats() {
|
|
575
|
+
const statsPath = path.join(this.expandPath(this.learningPath), 'stats.json');
|
|
576
|
+
await fs.writeJson(statsPath, this.stats, { spaces: 2 });
|
|
577
|
+
}
|
|
578
|
+
|
|
487
579
|
/**
|
|
488
580
|
* Helper methods
|
|
489
581
|
*/
|
package/mcp/server.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const FlowMind = require('../core');
|
|
9
9
|
const eventBus = require('../core/event-bus');
|
|
10
|
+
const { version } = require('../package.json');
|
|
10
11
|
|
|
11
12
|
// MCP Server 实现
|
|
12
13
|
class FlowMindMCPServer {
|
|
@@ -276,7 +277,7 @@ async function main() {
|
|
|
276
277
|
},
|
|
277
278
|
serverInfo: {
|
|
278
279
|
name: 'flowmind',
|
|
279
|
-
version:
|
|
280
|
+
version: version
|
|
280
281
|
}
|
|
281
282
|
}
|
|
282
283
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Sync Skill
|
|
3
|
+
* Sync API definitions, generate documentation, maintain API consistency
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs-extra');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
canHandle(input, context) {
|
|
11
|
+
if (!input) return false;
|
|
12
|
+
return /api.*sync|同步.*api|api.*文档|swagger.*sync|接口.*同步/i.test(input);
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
async execute(input, context) {
|
|
16
|
+
const params = parseApiSyncParams(input);
|
|
17
|
+
|
|
18
|
+
if (params.action === 'generate') {
|
|
19
|
+
return generateDocs(params, input);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (params.action === 'sync') {
|
|
23
|
+
return syncToYApi(params, input, context);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
type: 'result',
|
|
28
|
+
skill: 'api-sync',
|
|
29
|
+
message: 'API sync. Available actions: generate (from code), sync (to platform)',
|
|
30
|
+
data: {
|
|
31
|
+
actions: ['generate - Generate API docs from code', 'sync - Sync to YApi/Swagger Hub'],
|
|
32
|
+
params
|
|
33
|
+
},
|
|
34
|
+
input,
|
|
35
|
+
timestamp: new Date().toISOString()
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function parseApiSyncParams(input) {
|
|
41
|
+
const params = {};
|
|
42
|
+
if (/生成|generate|导出/i.test(input)) params.action = 'generate';
|
|
43
|
+
if (/同步|sync|上传|push/i.test(input)) params.action = 'sync';
|
|
44
|
+
|
|
45
|
+
const pathMatch = input.match(/(?:路径|path|目录|dir)\s*[:=]?\s*(\S+)/i);
|
|
46
|
+
if (pathMatch) params.path = pathMatch[1];
|
|
47
|
+
|
|
48
|
+
const platformMatch = input.match(/(?:平台|platform)\s*[:=]?\s*(yapi|swagger|postman)/i);
|
|
49
|
+
if (platformMatch) params.platform = platformMatch[1].toLowerCase();
|
|
50
|
+
|
|
51
|
+
const projectMatch = input.match(/(?:项目|project)\s*[:=]?\s*(\S+)/i);
|
|
52
|
+
if (projectMatch) params.project = projectMatch[1];
|
|
53
|
+
|
|
54
|
+
return params;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function generateDocs(params, input) {
|
|
58
|
+
const dirPath = params.path || '.';
|
|
59
|
+
if (!(await fs.pathExists(dirPath))) {
|
|
60
|
+
return {
|
|
61
|
+
type: 'error', skill: 'api-sync',
|
|
62
|
+
message: `Path not found: ${dirPath}`,
|
|
63
|
+
input, timestamp: new Date().toISOString()
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const apis = [];
|
|
68
|
+
const files = await findApiFiles(dirPath);
|
|
69
|
+
|
|
70
|
+
for (const file of files.slice(0, 20)) {
|
|
71
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
72
|
+
const endpoints = extractEndpoints(content);
|
|
73
|
+
apis.push(...endpoints.map(e => ({ ...e, file: path.relative(dirPath, file) })));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
type: 'result',
|
|
78
|
+
skill: 'api-sync',
|
|
79
|
+
message: `Found ${apis.length} API endpoint(s) in ${files.length} file(s)`,
|
|
80
|
+
data: { apis, totalEndpoints: apis.length, totalFiles: files.length },
|
|
81
|
+
input,
|
|
82
|
+
timestamp: new Date().toISOString()
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function syncToYApi(params, input, context) {
|
|
87
|
+
return {
|
|
88
|
+
type: 'result',
|
|
89
|
+
skill: 'api-sync',
|
|
90
|
+
message: 'API sync to platform (use yapi-sync-interface skill for YApi-specific operations)',
|
|
91
|
+
data: { platform: params.platform || 'yapi', hint: 'Use yapi-sync-interface skill for direct YApi integration' },
|
|
92
|
+
input,
|
|
93
|
+
timestamp: new Date().toISOString()
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function findApiFiles(dir) {
|
|
98
|
+
const results = [];
|
|
99
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
102
|
+
const fullPath = path.join(dir, entry.name);
|
|
103
|
+
if (entry.isDirectory()) {
|
|
104
|
+
results.push(...await findApiFiles(fullPath));
|
|
105
|
+
} else if (/\.(js|ts|java|py)$/.test(entry.name)) {
|
|
106
|
+
results.push(fullPath);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return results;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function extractEndpoints(content) {
|
|
113
|
+
const endpoints = [];
|
|
114
|
+
const patterns = [
|
|
115
|
+
/@(?:GET|POST|PUT|DELETE|PATCH)Mapping\s*\(\s*["']([^"']+)["']/gi,
|
|
116
|
+
/(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*["']([^"']+)["']/gi,
|
|
117
|
+
/@(?:Get|Post|Put|Delete|Patch)\s*\(\s*["']([^"']+)["']/gi,
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
for (const pattern of patterns) {
|
|
121
|
+
let match;
|
|
122
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
123
|
+
const method = match[1]?.toUpperCase() || 'GET';
|
|
124
|
+
const urlPath = match[2] || match[1];
|
|
125
|
+
endpoints.push({ method, path: urlPath });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return endpoints;
|
|
130
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Archive Change Skill
|
|
3
|
+
* Archive completed changes and maintain project history
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs-extra');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
canHandle(input, context) {
|
|
11
|
+
if (!input) return false;
|
|
12
|
+
return /归档|archive|完成.*存档|change.*archive|整理.*完成/i.test(input);
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
async execute(input, context) {
|
|
16
|
+
const params = parseArchiveParams(input);
|
|
17
|
+
|
|
18
|
+
if (params.action === 'check') {
|
|
19
|
+
const checklist = await checkArchiveReadiness(params.path || '.');
|
|
20
|
+
return {
|
|
21
|
+
type: 'result',
|
|
22
|
+
skill: 'archive-change',
|
|
23
|
+
message: `Archive readiness: ${checklist.passed}/${checklist.total} checks passed`,
|
|
24
|
+
data: { checklist },
|
|
25
|
+
input,
|
|
26
|
+
timestamp: new Date().toISOString()
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (params.action === 'archive') {
|
|
31
|
+
const archiveDir = params.archiveDir || '.archive';
|
|
32
|
+
const changeName = params.name || `change-${Date.now()}`;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
type: 'result',
|
|
36
|
+
skill: 'archive-change',
|
|
37
|
+
message: `Archiving change: ${changeName}`,
|
|
38
|
+
data: {
|
|
39
|
+
changeName,
|
|
40
|
+
archiveDir: path.join(archiveDir, changeName),
|
|
41
|
+
structure: ['SUMMARY.md', 'PROPOSAL.md', 'SPECS.md', 'DESIGN.md', 'TASKS.md', 'CHANGELOG.md', 'TESTS.md', 'artifacts/'],
|
|
42
|
+
preCheck: await checkArchiveReadiness(params.path || '.')
|
|
43
|
+
},
|
|
44
|
+
input,
|
|
45
|
+
timestamp: new Date().toISOString()
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
type: 'result',
|
|
51
|
+
skill: 'archive-change',
|
|
52
|
+
message: 'Archive change. Available actions: check, archive',
|
|
53
|
+
data: {
|
|
54
|
+
actions: ['check - Check readiness', 'archive - Archive change'],
|
|
55
|
+
archiveStructure: ['SUMMARY.md', 'PROPOSAL.md', 'SPECS.md', 'DESIGN.md', 'TASKS.md', 'CHANGELOG.md', 'TESTS.md']
|
|
56
|
+
},
|
|
57
|
+
input,
|
|
58
|
+
timestamp: new Date().toISOString()
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function parseArchiveParams(input) {
|
|
64
|
+
const params = {};
|
|
65
|
+
if (/检查|check|就绪|ready/i.test(input)) params.action = 'check';
|
|
66
|
+
if (/归档|archive|执行/i.test(input)) params.action = 'archive';
|
|
67
|
+
|
|
68
|
+
const nameMatch = input.match(/(?:名称|name)\s*[:=]?\s*(\S+)/i);
|
|
69
|
+
if (nameMatch) params.name = nameMatch[1];
|
|
70
|
+
|
|
71
|
+
const pathMatch = input.match(/(?:路径|path|目录|dir)\s*[:=]?\s*(\S+)/i);
|
|
72
|
+
if (pathMatch) params.path = pathMatch[1];
|
|
73
|
+
|
|
74
|
+
return params;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function checkArchiveReadiness(dir) {
|
|
78
|
+
const checks = [
|
|
79
|
+
{ name: 'README exists', check: () => fs.pathExists(path.join(dir, 'README.md')) },
|
|
80
|
+
{ name: 'Has git history', check: () => fs.pathExists(path.join(dir, '.git')) },
|
|
81
|
+
{ name: 'Has package.json or similar', check: async () =>
|
|
82
|
+
await fs.pathExists(path.join(dir, 'package.json')) ||
|
|
83
|
+
await fs.pathExists(path.join(dir, 'pom.xml')) ||
|
|
84
|
+
await fs.pathExists(path.join(dir, 'Cargo.toml'))
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const results = [];
|
|
89
|
+
for (const c of checks) {
|
|
90
|
+
try {
|
|
91
|
+
const passed = await c.check();
|
|
92
|
+
results.push({ name: c.name, passed });
|
|
93
|
+
} catch {
|
|
94
|
+
results.push({ name: c.name, passed: false });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
checks: results,
|
|
100
|
+
passed: results.filter(r => r.passed).length,
|
|
101
|
+
total: results.length,
|
|
102
|
+
ready: results.every(r => r.passed)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto Flow Skill
|
|
3
|
+
* Define, execute, and manage complex multi-step workflows
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs-extra');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const BUILTIN_WORKFLOWS = {
|
|
10
|
+
'dev-workflow': {
|
|
11
|
+
name: 'Development Workflow',
|
|
12
|
+
steps: [
|
|
13
|
+
{ id: 'review', skill: 'code-review', name: 'Code Review' },
|
|
14
|
+
{ id: 'test', action: 'run-command', command: 'npm test', depends_on: ['review'] },
|
|
15
|
+
{ id: 'docs', action: 'run-command', command: 'npm run docs', depends_on: ['test'] },
|
|
16
|
+
{ id: 'archive', skill: 'archive-change', name: 'Archive', depends_on: ['docs'] }
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
'deploy-workflow': {
|
|
20
|
+
name: 'Deploy Workflow',
|
|
21
|
+
steps: [
|
|
22
|
+
{ id: 'validate', action: 'run-command', command: 'npm run validate' },
|
|
23
|
+
{ id: 'build', action: 'run-command', command: 'npm run build', depends_on: ['validate'] },
|
|
24
|
+
{ id: 'deploy', action: 'deploy', depends_on: ['build'] },
|
|
25
|
+
{ id: 'verify', action: 'run-command', command: 'npm run verify', depends_on: ['deploy'] },
|
|
26
|
+
{ id: 'notify', action: 'notify', depends_on: ['verify'] }
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
canHandle(input, context) {
|
|
33
|
+
if (!input) return false;
|
|
34
|
+
return /自动流程|auto.*flow|工作流|workflow|执行流程|run.*flow/i.test(input);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async execute(input, context) {
|
|
38
|
+
const params = parseFlowParams(input);
|
|
39
|
+
|
|
40
|
+
if (params.action === 'list') {
|
|
41
|
+
return {
|
|
42
|
+
type: 'result',
|
|
43
|
+
skill: 'auto-flow',
|
|
44
|
+
message: `Available workflows: ${Object.keys(BUILTIN_WORKFLOWS).join(', ')}`,
|
|
45
|
+
data: { workflows: Object.entries(BUILTIN_WORKFLOWS).map(([k, v]) => ({ id: k, name: v.name, steps: v.steps.length })) },
|
|
46
|
+
input,
|
|
47
|
+
timestamp: new Date().toISOString()
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (params.action === 'run') {
|
|
52
|
+
const workflow = BUILTIN_WORKFLOWS[params.workflow];
|
|
53
|
+
if (!workflow) {
|
|
54
|
+
return {
|
|
55
|
+
type: 'error', skill: 'auto-flow',
|
|
56
|
+
message: `Workflow not found: ${params.workflow}. Available: ${Object.keys(BUILTIN_WORKFLOWS).join(', ')}`,
|
|
57
|
+
input, timestamp: new Date().toISOString()
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
type: 'result',
|
|
63
|
+
skill: 'auto-flow',
|
|
64
|
+
message: `Starting workflow: ${workflow.name} (${workflow.steps.length} steps)`,
|
|
65
|
+
data: {
|
|
66
|
+
workflow: params.workflow,
|
|
67
|
+
steps: workflow.steps.map(s => ({
|
|
68
|
+
id: s.id,
|
|
69
|
+
name: s.name || s.action || s.skill,
|
|
70
|
+
depends_on: s.depends_on || [],
|
|
71
|
+
status: 'pending'
|
|
72
|
+
})),
|
|
73
|
+
status: 'started'
|
|
74
|
+
},
|
|
75
|
+
input,
|
|
76
|
+
timestamp: new Date().toISOString()
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (params.action === 'define') {
|
|
81
|
+
return {
|
|
82
|
+
type: 'result',
|
|
83
|
+
skill: 'auto-flow',
|
|
84
|
+
message: 'Define a workflow in YAML format',
|
|
85
|
+
data: {
|
|
86
|
+
format: 'YAML workflow definition',
|
|
87
|
+
example: {
|
|
88
|
+
name: 'My Workflow',
|
|
89
|
+
steps: [
|
|
90
|
+
{ id: 'step1', action: 'run-command', command: 'echo hello' },
|
|
91
|
+
{ id: 'step2', skill: 'code-review', depends_on: ['step1'] }
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
input,
|
|
96
|
+
timestamp: new Date().toISOString()
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
type: 'result',
|
|
102
|
+
skill: 'auto-flow',
|
|
103
|
+
message: 'Auto Flow. Available actions: list, run, define',
|
|
104
|
+
data: {
|
|
105
|
+
actions: ['list - List workflows', 'run <name> - Run workflow', 'define - Define new workflow'],
|
|
106
|
+
builtinWorkflows: Object.keys(BUILTIN_WORKFLOWS)
|
|
107
|
+
},
|
|
108
|
+
input,
|
|
109
|
+
timestamp: new Date().toISOString()
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
function parseFlowParams(input) {
|
|
115
|
+
const params = {};
|
|
116
|
+
if (/列表|list/i.test(input)) params.action = 'list';
|
|
117
|
+
if (/执行|run|start|运行/i.test(input)) params.action = 'run';
|
|
118
|
+
if (/定义|define|创建|create/i.test(input)) params.action = 'define';
|
|
119
|
+
|
|
120
|
+
const workflowMatch = input.match(/(?:workflow|流程|工作流)\s*[:=]?\s*(\S+)/i);
|
|
121
|
+
if (workflowMatch) params.workflow = workflowMatch[1];
|
|
122
|
+
|
|
123
|
+
return params;
|
|
124
|
+
}
|