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 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
- console.log(chalk.yellow('Resetting learnings for:'), options.reset);
540
- // Implementation would go here
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
- console.log(chalk.yellow('Deleting learning:'), options.delete);
543
- // Implementation would go here
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> <value>', 'Set config value')
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
- if (args.length >= 2) {
659
- await setSkillConfig(skill, fm, args[0], args[1]);
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> <value>', 'Set configuration value')
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 && options.value) {
789
- fm.config.set(options.set, options.value);
790
- await fm.config.save();
791
- console.log(chalk.green('✓ Configuration updated'));
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
- const { unmount, waitUntilExit } = render(React.createElement(App, { flowmind: fm }));
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
 
@@ -5,6 +5,7 @@
5
5
 
6
6
  const fs = require('fs-extra');
7
7
  const path = require('path');
8
+ const os = require('os');
8
9
 
9
10
  class ConfigManager {
10
11
  constructor(configPath = null) {
@@ -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().substr(0, 8)}`,
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().substr(0, 8)}`,
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().substr(0, 8)}`,
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
- let scenes = { version: '1.0', mappings: [] };
314
- if (await fs.pathExists(scenesPath)) {
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
- await fs.writeJson(scenesPath, scenes, { spaces: 2 });
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
- let prefs = {};
336
- if (await fs.pathExists(prefsPath)) {
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
- await fs.ensureDir(path.dirname(prefsPath));
344
- await fs.writeJson(prefsPath, prefs, { spaces: 2 });
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
- const bindingsPath = path.join(this.expandPath(this.learningPath), 'skill-bindings.json');
391
- this.skillBindings.lastUpdated = new Date().toISOString();
392
- await fs.writeJson(bindingsPath, this.skillBindings, { spaces: 2 });
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.totalRecords++;
418
- this.stats.byType[type] = (this.stats.byType[type] || 0) + 1;
419
- this.stats.bySkill[skill] = (this.stats.bySkill[skill] || 0) + 1;
420
- this.stats.lastLearning = new Date().toISOString();
421
-
422
- const statsPath = path.join(this.expandPath(this.learningPath), 'stats.json');
423
- await fs.writeJson(statsPath, this.stats, { spaces: 2 });
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: '1.0.1'
280
+ version: version
280
281
  }
281
282
  }
282
283
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowmind",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "The AI Agent That Learns How You Work - Stop repeating yourself, FlowMind learns your workflows and applies them automatically.",
5
5
  "main": "core/index.js",
6
6
  "bin": {
@@ -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
+ }