flowmind 1.2.2 → 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 +85 -18
- 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);
|
|
@@ -1343,17 +1350,45 @@ program
|
|
|
1343
1350
|
.command('tui')
|
|
1344
1351
|
.description('Launch enhanced TUI with split panels, skill browser, and dragon display')
|
|
1345
1352
|
.action(async () => {
|
|
1353
|
+
let stdinWrapper = null;
|
|
1346
1354
|
try {
|
|
1347
1355
|
// Register .jsx extension for CJS
|
|
1348
1356
|
require('module')._extensions['.jsx'] = require('module')._extensions['.js'];
|
|
1349
1357
|
|
|
1350
1358
|
const React = require('react');
|
|
1351
1359
|
const { render } = require('ink');
|
|
1360
|
+
const { PassThrough } = require('stream');
|
|
1352
1361
|
const App = require('../tui/app.jsx');
|
|
1353
1362
|
|
|
1354
1363
|
const fm = await initFlowMind();
|
|
1355
1364
|
|
|
1356
|
-
|
|
1365
|
+
// Create a stdin wrapper to handle non-TTY environments (e.g., piped stdin).
|
|
1366
|
+
// Ink v3's useInput hook calls setRawMode(true) which throws if stdin is not a TTY.
|
|
1367
|
+
const realStdin = process.stdin;
|
|
1368
|
+
stdinWrapper = new PassThrough();
|
|
1369
|
+
stdinWrapper.isTTY = true;
|
|
1370
|
+
stdinWrapper.isRaw = false;
|
|
1371
|
+
stdinWrapper.setRawMode = (mode) => {
|
|
1372
|
+
try {
|
|
1373
|
+
if (realStdin.setRawMode) {
|
|
1374
|
+
realStdin.setRawMode(mode);
|
|
1375
|
+
}
|
|
1376
|
+
} catch (e) {
|
|
1377
|
+
// Suppress raw mode errors in non-TTY environments
|
|
1378
|
+
}
|
|
1379
|
+
return stdinWrapper;
|
|
1380
|
+
};
|
|
1381
|
+
// Forward real stdin data to the wrapper
|
|
1382
|
+
if (realStdin.readable) {
|
|
1383
|
+
realStdin.on('data', (chunk) => {
|
|
1384
|
+
if (!stdinWrapper.destroyed) stdinWrapper.write(chunk);
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
const { unmount, waitUntilExit } = render(
|
|
1389
|
+
React.createElement(App, { flowmind: fm }),
|
|
1390
|
+
{ stdin: stdinWrapper }
|
|
1391
|
+
);
|
|
1357
1392
|
await waitUntilExit();
|
|
1358
1393
|
unmount();
|
|
1359
1394
|
} catch (error) {
|
|
@@ -1361,6 +1396,10 @@ program
|
|
|
1361
1396
|
if (error.message.includes('Cannot find module')) {
|
|
1362
1397
|
console.log(chalk.yellow('Try running: npm install ink@3 react ink-text-input ink-spinner'));
|
|
1363
1398
|
}
|
|
1399
|
+
} finally {
|
|
1400
|
+
if (stdinWrapper && !stdinWrapper.destroyed) {
|
|
1401
|
+
stdinWrapper.destroy();
|
|
1402
|
+
}
|
|
1364
1403
|
}
|
|
1365
1404
|
});
|
|
1366
1405
|
|
|
@@ -1369,19 +1408,43 @@ program
|
|
|
1369
1408
|
.command('dashboard')
|
|
1370
1409
|
.description('Launch real-time monitoring dashboard for MCP activity and events')
|
|
1371
1410
|
.action(async () => {
|
|
1411
|
+
let stdinWrapper = null;
|
|
1372
1412
|
try {
|
|
1373
1413
|
// Register .jsx extension for CJS
|
|
1374
1414
|
require('module')._extensions['.jsx'] = require('module')._extensions['.js'];
|
|
1375
1415
|
|
|
1376
1416
|
const React = require('react');
|
|
1377
1417
|
const { render } = require('ink');
|
|
1418
|
+
const { PassThrough } = require('stream');
|
|
1378
1419
|
const DashboardApp = require('../dashboard/app.jsx');
|
|
1379
1420
|
const eventBus = require('../core/event-bus');
|
|
1380
1421
|
|
|
1381
1422
|
const fm = await initFlowMind();
|
|
1382
1423
|
|
|
1424
|
+
// Create a stdin wrapper to handle non-TTY environments
|
|
1425
|
+
const realStdin = process.stdin;
|
|
1426
|
+
stdinWrapper = new PassThrough();
|
|
1427
|
+
stdinWrapper.isTTY = true;
|
|
1428
|
+
stdinWrapper.isRaw = false;
|
|
1429
|
+
stdinWrapper.setRawMode = (mode) => {
|
|
1430
|
+
try {
|
|
1431
|
+
if (realStdin.setRawMode) {
|
|
1432
|
+
realStdin.setRawMode(mode);
|
|
1433
|
+
}
|
|
1434
|
+
} catch (e) {
|
|
1435
|
+
// Suppress raw mode errors in non-TTY environments
|
|
1436
|
+
}
|
|
1437
|
+
return stdinWrapper;
|
|
1438
|
+
};
|
|
1439
|
+
if (realStdin.readable) {
|
|
1440
|
+
realStdin.on('data', (chunk) => {
|
|
1441
|
+
if (!stdinWrapper.destroyed) stdinWrapper.write(chunk);
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1383
1445
|
const { unmount, waitUntilExit } = render(
|
|
1384
|
-
React.createElement(DashboardApp, { flowmind: fm, eventBus })
|
|
1446
|
+
React.createElement(DashboardApp, { flowmind: fm, eventBus }),
|
|
1447
|
+
{ stdin: stdinWrapper }
|
|
1385
1448
|
);
|
|
1386
1449
|
await waitUntilExit();
|
|
1387
1450
|
unmount();
|
|
@@ -1390,6 +1453,10 @@ program
|
|
|
1390
1453
|
if (error.message.includes('Cannot find module')) {
|
|
1391
1454
|
console.log(chalk.yellow('Try running: npm install ink@3 react'));
|
|
1392
1455
|
}
|
|
1456
|
+
} finally {
|
|
1457
|
+
if (stdinWrapper && !stdinWrapper.destroyed) {
|
|
1458
|
+
stdinWrapper.destroy();
|
|
1459
|
+
}
|
|
1393
1460
|
}
|
|
1394
1461
|
});
|
|
1395
1462
|
|
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
|
+
}
|