stigmergy 1.2.13 → 1.3.1

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.
Files changed (88) hide show
  1. package/README.md +39 -3
  2. package/STIGMERGY.md +3 -0
  3. package/config/builtin-skills.json +43 -0
  4. package/config/enhanced-cli-config.json +438 -0
  5. package/docs/CLI_TOOLS_AGENT_SKILL_ANALYSIS.md +463 -0
  6. package/docs/DESIGN_CLI_HELP_ANALYZER_REFACTOR.md +726 -0
  7. package/docs/ENHANCED_CLI_AGENT_SKILL_CONFIG.md +285 -0
  8. package/docs/IMPLEMENTATION_CHECKLIST_CLI_HELP_ANALYZER_REFACTOR.md +1268 -0
  9. package/docs/INSTALLER_ARCHITECTURE.md +257 -0
  10. package/docs/LESSONS_LEARNED.md +252 -0
  11. package/docs/SPECS_CLI_HELP_ANALYZER_REFACTOR.md +287 -0
  12. package/docs/SUDO_PROBLEM_AND_SOLUTION.md +529 -0
  13. package/docs/correct-skillsio-implementation.md +368 -0
  14. package/docs/development_guidelines.md +276 -0
  15. package/docs/independent-resume-implementation.md +198 -0
  16. package/docs/resumesession-final-implementation.md +195 -0
  17. package/docs/resumesession-usage.md +87 -0
  18. package/package.json +146 -136
  19. package/scripts/analyze-router.js +168 -0
  20. package/scripts/run-comprehensive-tests.js +230 -0
  21. package/scripts/run-quick-tests.js +90 -0
  22. package/scripts/test-runner.js +344 -0
  23. package/skills/resumesession/INDEPENDENT_SKILL.md +403 -0
  24. package/skills/resumesession/README.md +381 -0
  25. package/skills/resumesession/SKILL.md +211 -0
  26. package/skills/resumesession/__init__.py +33 -0
  27. package/skills/resumesession/implementations/simple-resume.js +13 -0
  28. package/skills/resumesession/independent-resume.js +750 -0
  29. package/skills/resumesession/package.json +1 -0
  30. package/skills/resumesession/skill.json +1 -0
  31. package/src/adapters/claude/install_claude_integration.js +9 -1
  32. package/src/adapters/codebuddy/install_codebuddy_integration.js +3 -1
  33. package/src/adapters/codex/install_codex_integration.js +15 -5
  34. package/src/adapters/gemini/install_gemini_integration.js +3 -1
  35. package/src/adapters/qwen/install_qwen_integration.js +3 -1
  36. package/src/cli/commands/autoinstall.js +65 -0
  37. package/src/cli/commands/errors.js +190 -0
  38. package/src/cli/commands/independent-resume.js +395 -0
  39. package/src/cli/commands/install.js +179 -0
  40. package/src/cli/commands/permissions.js +108 -0
  41. package/src/cli/commands/project.js +485 -0
  42. package/src/cli/commands/scan.js +97 -0
  43. package/src/cli/commands/simple-resume.js +377 -0
  44. package/src/cli/commands/skills.js +158 -0
  45. package/src/cli/commands/status.js +113 -0
  46. package/src/cli/commands/stigmergy-resume.js +775 -0
  47. package/src/cli/commands/system.js +301 -0
  48. package/src/cli/commands/universal-resume.js +394 -0
  49. package/src/cli/router-beta.js +471 -0
  50. package/src/cli/utils/environment.js +75 -0
  51. package/src/cli/utils/formatters.js +47 -0
  52. package/src/cli/utils/skills_cache.js +92 -0
  53. package/src/core/cache_cleaner.js +1 -0
  54. package/src/core/cli_adapters.js +345 -0
  55. package/src/core/cli_help_analyzer.js +1236 -680
  56. package/src/core/cli_path_detector.js +702 -709
  57. package/src/core/cli_tools.js +515 -160
  58. package/src/core/coordination/nodejs/CLIIntegrationManager.js +18 -0
  59. package/src/core/coordination/nodejs/HookDeploymentManager.js +242 -412
  60. package/src/core/coordination/nodejs/HookDeploymentManager.refactored.js +323 -0
  61. package/src/core/coordination/nodejs/generators/CLIAdapterGenerator.js +363 -0
  62. package/src/core/coordination/nodejs/generators/ResumeSessionGenerator.js +932 -0
  63. package/src/core/coordination/nodejs/generators/SkillsIntegrationGenerator.js +1395 -0
  64. package/src/core/coordination/nodejs/generators/index.js +12 -0
  65. package/src/core/enhanced_cli_installer.js +1208 -608
  66. package/src/core/enhanced_cli_parameter_handler.js +402 -0
  67. package/src/core/execution_mode_detector.js +222 -0
  68. package/src/core/installer.js +151 -106
  69. package/src/core/local_skill_scanner.js +732 -0
  70. package/src/core/multilingual/language-pattern-manager.js +1 -1
  71. package/src/core/skills/BuiltinSkillsDeployer.js +188 -0
  72. package/src/core/skills/StigmergySkillManager.js +123 -16
  73. package/src/core/skills/embedded-openskills/SkillParser.js +7 -3
  74. package/src/core/smart_router.js +550 -261
  75. package/src/index.js +10 -4
  76. package/src/utils.js +66 -7
  77. package/test/cli-integration.test.js +304 -0
  78. package/test/direct_smart_router_test.js +88 -0
  79. package/test/enhanced-cli-agent-skill-test.js +485 -0
  80. package/test/simple_test.js +82 -0
  81. package/test/smart_router_test_runner.js +123 -0
  82. package/test/smart_routing_edge_cases.test.js +284 -0
  83. package/test/smart_routing_simple_verification.js +139 -0
  84. package/test/smart_routing_verification.test.js +346 -0
  85. package/test/specific-cli-agent-skill-analysis.js +385 -0
  86. package/test/unit/smart_router.test.js +295 -0
  87. package/test/very_simple_test.js +54 -0
  88. package/src/cli/router.js +0 -1783
@@ -0,0 +1,775 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ResumeSession Command
4
+ * Cross-CLI session recovery and history management
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+
11
+ class ResumeSessionCommand {
12
+ constructor() {
13
+ this.projectPath = process.cwd();
14
+ }
15
+
16
+ // Get all CLI session paths (with dynamic detection)
17
+ getAllCLISessionPaths() {
18
+ const homeDir = os.homedir();
19
+ const paths = {};
20
+
21
+ // Detect Claude CLI session paths
22
+ paths.claude = this.detectCLISessionPaths([
23
+ path.join(homeDir, '.claude', 'projects'),
24
+ path.join(homeDir, '.claude', 'sessions'),
25
+ path.join(homeDir, '.claude', 'history')
26
+ ]);
27
+
28
+ // Detect Gemini CLI session paths
29
+ paths.gemini = this.detectCLISessionPaths([
30
+ path.join(homeDir, '.config', 'gemini', 'tmp'),
31
+ path.join(homeDir, '.gemini', 'sessions'),
32
+ path.join(homeDir, '.gemini', 'history')
33
+ ]);
34
+
35
+ // Detect Qwen CLI session paths
36
+ paths.qwen = this.detectCLISessionPaths([
37
+ path.join(homeDir, '.qwen', 'projects'),
38
+ path.join(homeDir, '.qwen', 'sessions'),
39
+ path.join(homeDir, '.qwen', 'history')
40
+ ]);
41
+
42
+ // Detect IFlow CLI session paths
43
+ paths.iflow = this.detectCLISessionPaths([
44
+ path.join(homeDir, '.iflow', 'projects'),
45
+ path.join(homeDir, '.iflow', 'sessions'),
46
+ path.join(homeDir, '.iflow', 'history')
47
+ ]);
48
+
49
+ // Detect QoderCLI session paths
50
+ paths.qodercli = this.detectCLISessionPaths([
51
+ path.join(homeDir, '.qoder', 'projects'),
52
+ path.join(homeDir, '.qoder', 'sessions')
53
+ ]);
54
+
55
+ // Detect CodeBuddy session paths
56
+ paths.codebuddy = this.detectCLISessionPaths([
57
+ path.join(homeDir, '.codebuddy'),
58
+ path.join(homeDir, '.codebuddy', 'sessions'),
59
+ path.join(homeDir, '.codebuddy', 'history')
60
+ ]);
61
+
62
+ // Detect Codex session paths
63
+ paths.codex = this.detectCLISessionPaths([
64
+ path.join(homeDir, '.config', 'codex'),
65
+ path.join(homeDir, '.codex', 'sessions'),
66
+ path.join(homeDir, '.codex', 'history')
67
+ ]);
68
+
69
+ // Detect Kode session paths
70
+ paths.kode = this.detectCLISessionPaths([
71
+ path.join(homeDir, '.kode', 'projects'),
72
+ path.join(homeDir, '.kode', 'sessions')
73
+ ]);
74
+
75
+ return paths;
76
+ }
77
+
78
+ // Detect which session paths actually exist and contain session files
79
+ detectCLISessionPaths(candidatePaths) {
80
+ const validPaths = [];
81
+
82
+ for (const candidatePath of candidatePaths) {
83
+ if (!fs.existsSync(candidatePath)) {
84
+ continue;
85
+ }
86
+
87
+ // Check if this directory contains session files
88
+ const hasSessionFiles = this.hasSessionFiles(candidatePath);
89
+ if (hasSessionFiles) {
90
+ validPaths.push(candidatePath);
91
+ }
92
+ }
93
+
94
+ return validPaths.length > 0 ? validPaths : candidatePaths.filter(p => fs.existsSync(p));
95
+ }
96
+
97
+ // Check if a directory contains session files
98
+ hasSessionFiles(dirPath) {
99
+ try {
100
+ const files = fs.readdirSync(dirPath);
101
+ return files.some(file =>
102
+ file.endsWith('.jsonl') ||
103
+ file.endsWith('.json') ||
104
+ file.endsWith('.session')
105
+ );
106
+ } catch (error) {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ // Scan sessions for a specific CLI
112
+ scanSessions(cliType, sessionsPath, projectPath) {
113
+ const sessions = [];
114
+ if (!sessionsPath || !projectPath) return sessions;
115
+
116
+ try {
117
+ if (!fs.existsSync(sessionsPath)) return sessions;
118
+
119
+ // For IFlow, Claude, QoderCLI, Kode: scan projects subdirectories (one level)
120
+ if ((cliType === 'iflow' || cliType === 'claude' || cliType === 'qodercli' || cliType === 'kode') && sessionsPath.includes('projects')) {
121
+ const subdirs = fs.readdirSync(sessionsPath);
122
+ for (const subdir of subdirs) {
123
+ const subdirPath = path.join(sessionsPath, subdir);
124
+ try {
125
+ const stat = fs.statSync(subdirPath);
126
+ if (stat.isDirectory()) {
127
+ sessions.push(...this.scanSessionFiles(cliType, subdirPath, projectPath));
128
+ }
129
+ } catch (error) {
130
+ continue;
131
+ }
132
+ }
133
+ return sessions;
134
+ }
135
+
136
+ // For Gemini: scan tmp/<hash>/chats subdirectories (multiple levels)
137
+ if (cliType === 'gemini' && sessionsPath.includes('tmp')) {
138
+ const hashDirs = fs.readdirSync(sessionsPath);
139
+ for (const hashDir of hashDirs) {
140
+ const hashDirPath = path.join(sessionsPath, hashDir);
141
+ try {
142
+ const stat = fs.statSync(hashDirPath);
143
+ if (stat.isDirectory()) {
144
+ const chatsPath = path.join(hashDirPath, 'chats');
145
+ if (fs.existsSync(chatsPath)) {
146
+ sessions.push(...this.scanSessionFiles(cliType, chatsPath, projectPath));
147
+ }
148
+ }
149
+ } catch (error) {
150
+ continue;
151
+ }
152
+ }
153
+ return sessions;
154
+ }
155
+
156
+ // For Qwen: scan projects/<projectName>/chats subdirectories (two levels)
157
+ if (cliType === 'qwen' && sessionsPath.includes('projects')) {
158
+ const projectDirs = fs.readdirSync(sessionsPath);
159
+ for (const projectDir of projectDirs) {
160
+ const projectDirPath = path.join(sessionsPath, projectDir);
161
+ try {
162
+ const stat = fs.statSync(projectDirPath);
163
+ if (stat.isDirectory()) {
164
+ const chatsPath = path.join(projectDirPath, 'chats');
165
+ if (fs.existsSync(chatsPath)) {
166
+ sessions.push(...this.scanSessionFiles(cliType, chatsPath, projectPath));
167
+ }
168
+ }
169
+ } catch (error) {
170
+ continue;
171
+ }
172
+ }
173
+ return sessions;
174
+ }
175
+
176
+ // For CodeBuddy: scan both projects subdirectories and root history.jsonl
177
+ if (cliType === 'codebuddy') {
178
+ const projectsPath = path.join(sessionsPath, 'projects');
179
+ if (fs.existsSync(projectsPath)) {
180
+ const projectDirs = fs.readdirSync(projectsPath);
181
+ for (const projectDir of projectDirs) {
182
+ const projectDirPath = path.join(projectsPath, projectDir);
183
+ try {
184
+ const stat = fs.statSync(projectDirPath);
185
+ if (stat.isDirectory()) {
186
+ sessions.push(...this.scanSessionFiles(cliType, projectDirPath, projectPath));
187
+ }
188
+ } catch (error) {
189
+ continue;
190
+ }
191
+ }
192
+ }
193
+ sessions.push(...this.scanSessionFiles(cliType, sessionsPath, projectPath));
194
+ return sessions;
195
+ }
196
+
197
+ return this.scanSessionFiles(cliType, sessionsPath, projectPath);
198
+ } catch (error) {
199
+ console.warn(`Warning: Could not scan ${cliType} sessions:`, error.message);
200
+ }
201
+
202
+ return sessions;
203
+ }
204
+
205
+ // Scan session files in a directory
206
+ scanSessionFiles(cliType, sessionsPath, projectPath) {
207
+ const sessions = [];
208
+ try {
209
+ const files = fs.readdirSync(sessionsPath);
210
+ for (const file of files) {
211
+ if (file.endsWith('.json') || file.endsWith('.session') || file.endsWith('.jsonl')) {
212
+ try {
213
+ const filePath = path.join(sessionsPath, file);
214
+ let sessionData;
215
+
216
+ if (file.endsWith('.jsonl')) {
217
+ const content = fs.readFileSync(filePath, 'utf8');
218
+ const lines = content.trim().split('\n').filter(line => line.trim());
219
+ const messages = lines.map(line => JSON.parse(line));
220
+
221
+ if (messages.length === 0) continue;
222
+ sessionData = this.parseJSONLSession(messages, file);
223
+ } else {
224
+ const content = fs.readFileSync(filePath, 'utf8');
225
+ sessionData = JSON.parse(content);
226
+ }
227
+
228
+ if (this.isProjectSession(sessionData, projectPath)) {
229
+ sessions.push({
230
+ cliType,
231
+ sessionId: sessionData.id || sessionData.sessionId || file.replace(/\.(json|session|jsonl)$/, ''),
232
+ title: sessionData.title || sessionData.topic || 'Untitled',
233
+ content: this.extractContent(sessionData),
234
+ updatedAt: new Date(sessionData.updatedAt || sessionData.timestamp || fs.statSync(filePath).mtime),
235
+ messageCount: sessionData.messageCount || this.countMessages(sessionData),
236
+ projectPath
237
+ });
238
+ }
239
+ } catch (error) {
240
+ console.warn(`Warning: Could not parse ${file}:`, error.message);
241
+ }
242
+ }
243
+ }
244
+ } catch (error) {
245
+ console.warn('Warning: Could not scan files:', error.message);
246
+ }
247
+
248
+ return sessions;
249
+ }
250
+
251
+ // Parse JSONL session format
252
+ parseJSONLSession(messages, filename) {
253
+ const firstMsg = messages[0];
254
+ const lastMsg = messages[messages.length - 1];
255
+ const userMessages = messages.filter(m => m.type === 'user' || m.role === 'user');
256
+
257
+ let title = 'Untitled Session';
258
+ if (userMessages.length > 0) {
259
+ const firstUserMsg = userMessages[0];
260
+ let content = firstUserMsg.message?.content || firstUserMsg.content || '';
261
+ if (typeof content === 'object') {
262
+ content = JSON.stringify(content);
263
+ }
264
+ if (typeof content === 'string' && content.trim()) {
265
+ title = content.substring(0, 100) || title;
266
+ }
267
+ }
268
+
269
+ const contentParts = messages
270
+ .map(m => {
271
+ if (m.message && typeof m.message === 'object') {
272
+ return m.message.content || m.message.text || '';
273
+ }
274
+ return m.content || m.text || '';
275
+ })
276
+ .filter(text => text && typeof text === 'string' && text.trim());
277
+
278
+ return {
279
+ sessionId: firstMsg.sessionId || filename.replace('.jsonl', ''),
280
+ title: title,
281
+ content: contentParts.join(' '),
282
+ timestamp: lastMsg.timestamp || new Date().toISOString(),
283
+ projectPath: firstMsg.cwd || firstMsg.workingDirectory,
284
+ messageCount: messages.filter(m => m.type === 'user' || m.type === 'assistant' || m.role === 'user' || m.role === 'assistant').length,
285
+ messages: messages
286
+ };
287
+ }
288
+
289
+ // Scan all CLI sessions
290
+ scanAllCLISessions(projectPath) {
291
+ const allSessions = [];
292
+ const cliPathsMap = this.getAllCLISessionPaths();
293
+
294
+ for (const [cliType, sessionsPaths] of Object.entries(cliPathsMap)) {
295
+ for (const sessionsPath of sessionsPaths) {
296
+ const sessions = this.scanSessions(cliType, sessionsPath, projectPath);
297
+ allSessions.push(...sessions);
298
+ }
299
+ }
300
+
301
+ return allSessions;
302
+ }
303
+
304
+ // Check if session belongs to current project
305
+ isProjectSession(session, projectPath) {
306
+ const sessionProject = session.projectPath || session.workingDirectory || session.cwd;
307
+ // If no project path info, exclude this session (unless showing all projects)
308
+ if (!sessionProject) return false;
309
+
310
+ return sessionProject === projectPath ||
311
+ sessionProject.startsWith(projectPath) ||
312
+ projectPath.startsWith(sessionProject);
313
+ }
314
+
315
+ // Extract content from session data
316
+ extractContent(sessionData) {
317
+ if (sessionData.content && typeof sessionData.content === 'string') {
318
+ return sessionData.content;
319
+ }
320
+
321
+ if (sessionData.messages && Array.isArray(sessionData.messages)) {
322
+ return sessionData.messages
323
+ .map(msg => {
324
+ if (msg.message && typeof msg.message === 'object') {
325
+ const content = msg.message.content || msg.message.text || '';
326
+ return this.extractTextFromContent(content);
327
+ }
328
+ const content = msg.content || msg.text || '';
329
+ return this.extractTextFromContent(content);
330
+ })
331
+ .filter(text => text && typeof text === 'string' && text.trim())
332
+ .join(' ');
333
+ }
334
+
335
+ if (Array.isArray(sessionData)) {
336
+ return sessionData
337
+ .map(item => {
338
+ if (item.message && typeof item.message === 'object') {
339
+ const content = item.message.content || item.message.text || '';
340
+ return this.extractTextFromContent(content);
341
+ }
342
+ const content = item.content || item.text || '';
343
+ return this.extractTextFromContent(content);
344
+ })
345
+ .filter(text => text && typeof text === 'string' && text.trim())
346
+ .join(' ');
347
+ }
348
+
349
+ return '';
350
+ }
351
+
352
+ // Extract text from content
353
+ extractTextFromContent(content) {
354
+ if (typeof content === 'string') {
355
+ return content;
356
+ }
357
+
358
+ if (Array.isArray(content)) {
359
+ return content
360
+ .map(item => {
361
+ if (typeof item === 'string') return item;
362
+ if (item && typeof item === 'object') {
363
+ return item.text || item.content || '';
364
+ }
365
+ return '';
366
+ })
367
+ .filter(text => text && typeof text === 'string')
368
+ .join(' ');
369
+ }
370
+
371
+ if (content && typeof content === 'object') {
372
+ return content.text || content.content || JSON.stringify(content);
373
+ }
374
+
375
+ return '';
376
+ }
377
+
378
+ // Count messages in session
379
+ countMessages(sessionData) {
380
+ if (sessionData.messages) {
381
+ return Array.isArray(sessionData.messages) ? sessionData.messages.length : 0;
382
+ }
383
+
384
+ if (Array.isArray(sessionData)) {
385
+ return sessionData.length;
386
+ }
387
+
388
+ return 0;
389
+ }
390
+
391
+ // Filter sessions by CLI
392
+ filterByCLI(sessions, cliType) {
393
+ if (!cliType) return sessions;
394
+ return sessions.filter(session => session.cliType === cliType);
395
+ }
396
+
397
+ // Filter sessions by search term
398
+ filterBySearch(sessions, searchTerm) {
399
+ if (!searchTerm) return sessions;
400
+
401
+ const lowerSearch = searchTerm.toLowerCase();
402
+ return sessions.filter(session =>
403
+ session.title.toLowerCase().includes(lowerSearch) ||
404
+ session.content.toLowerCase().includes(lowerSearch)
405
+ );
406
+ }
407
+
408
+ // Filter sessions by date range
409
+ filterByDateRange(sessions, timeRange = 'all') {
410
+ if (timeRange === 'all') return sessions;
411
+
412
+ const now = new Date();
413
+ return sessions.filter(session => {
414
+ const sessionDate = new Date(session.updatedAt);
415
+
416
+ switch (timeRange) {
417
+ case 'today':
418
+ return sessionDate.toDateString() === now.toDateString();
419
+ case 'week':
420
+ const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
421
+ return sessionDate >= weekAgo;
422
+ case 'month':
423
+ const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
424
+ return sessionDate >= monthAgo;
425
+ default:
426
+ return true;
427
+ }
428
+ });
429
+ }
430
+
431
+ // Sort sessions by date
432
+ sortByDate(sessions) {
433
+ return [...sessions].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
434
+ }
435
+
436
+ // Apply all filters
437
+ applyFilters(sessions, options, projectPath) {
438
+ let filteredSessions = [...sessions];
439
+
440
+ // 如果不是 --all,则只过滤当前项目的会话
441
+ if (!options.showAll) {
442
+ filteredSessions = filteredSessions.filter(session => this.isProjectSession(session, projectPath));
443
+ }
444
+
445
+ if (options.cli) {
446
+ filteredSessions = this.filterByCLI(filteredSessions, options.cli);
447
+ }
448
+
449
+ if (options.search) {
450
+ filteredSessions = this.filterBySearch(filteredSessions, options.search);
451
+ }
452
+
453
+ if (options.timeRange) {
454
+ filteredSessions = this.filterByDateRange(filteredSessions, options.timeRange);
455
+ }
456
+
457
+ filteredSessions = this.sortByDate(filteredSessions);
458
+
459
+ // Only apply limit if not showing all projects
460
+ if (!options.showAll && options.limit && options.limit > 0) {
461
+ filteredSessions = filteredSessions.slice(0, options.limit);
462
+ }
463
+
464
+ return filteredSessions;
465
+ }
466
+
467
+ // Format sessions as summary
468
+ formatSummary(sessions) {
469
+ if (sessions.length === 0) {
470
+ return `📭 暂无会话记录\n\n💡 **提示:** 尝试: stigmergy resume --all 查看所有项目的会话`;
471
+ }
472
+
473
+ let response = `📁 ${this.projectPath} 项目会话\n\n📊 共找到 ${sessions.length} 个会话\n\n`;
474
+
475
+ const byCLI = {};
476
+ sessions.forEach(session => {
477
+ if (!byCLI[session.cliType]) byCLI[session.cliType] = [];
478
+ byCLI[session.cliType].push(session);
479
+ });
480
+
481
+ Object.entries(byCLI).forEach(([cli, cliSessions]) => {
482
+ const icon = this.getCLIIcon(cli);
483
+ response += `${icon} **${cli.toUpperCase()}** (${cliSessions.length}个)\n`;
484
+
485
+ cliSessions.forEach((session, i) => {
486
+ const date = this.formatDate(session.updatedAt);
487
+ const title = session.title.substring(0, 50);
488
+ response += ` ${i + 1}. ${title}...\n`;
489
+ response += ` 📅 ${date} • 💬 ${session.messageCount}条消息\n`;
490
+ });
491
+ response += '\n';
492
+ });
493
+
494
+ if (!this.projectPath.includes('stigmergy')) {
495
+ response += `💡 **使用方法:**\n`;
496
+ response += `• 'stigmergy resume' - 恢复最近的会话\n`;
497
+ response += `• 'stigmergy resume <数字>' - 显示指定数量的会话\n`;
498
+ response += `• 'stigmergy resume <cli>' - 查看特定CLI的会话\n`;
499
+ response += `• 'stigmergy resume <cli> <数字>' - 查看特定CLI的指定数量会话\n`;
500
+ response += `• 'stigmergy resume --all' - 查看所有项目的会话`;
501
+ }
502
+
503
+ return response;
504
+ }
505
+
506
+ // Format sessions as timeline
507
+ formatTimeline(sessions) {
508
+ if (sessions.length === 0) {
509
+ return '📭 暂无会话时间线。';
510
+ }
511
+
512
+ let response = `⏰ **时间线视图**\n\n`;
513
+
514
+ sessions.forEach((session, index) => {
515
+ const date = this.formatDate(session.updatedAt);
516
+ const cliIcon = this.getCLIIcon(session.cliType);
517
+
518
+ response += `${index + 1}. ${cliIcon} ${session.title}\n`;
519
+ response += ` 📅 ${date} • 💬 ${session.messageCount}条消息\n`;
520
+ response += ` 🔑 ${session.cliType}:${session.sessionId}\n\n`;
521
+ });
522
+
523
+ return response;
524
+ }
525
+
526
+ // Format sessions as detailed
527
+ formatDetailed(sessions) {
528
+ if (sessions.length === 0) {
529
+ return '📭 暂无详细会话信息。';
530
+ }
531
+
532
+ let response = `📋 **详细视图**\n\n`;
533
+
534
+ sessions.forEach((session, index) => {
535
+ const cliIcon = this.getCLIIcon(session.cliType);
536
+ const date = session.updatedAt.toLocaleString();
537
+
538
+ response += `${index + 1}. ${cliIcon} **${session.title}**\n`;
539
+ response += ` 📅 ${date}\n`;
540
+ response += ` 🔧 CLI: ${session.cliType}\n`;
541
+ response += ` 💬 消息数: ${session.messageCount}\n`;
542
+ response += ` 🆔 会话ID: '${session.sessionId}'\n\n`;
543
+ });
544
+
545
+ return response;
546
+ }
547
+
548
+ // Format session context
549
+ formatContext(session) {
550
+ if (!session) {
551
+ return `📭 暂无可恢复的上下文。`;
552
+ }
553
+
554
+ let response = `🔄 **上下文恢复**\n\n`;
555
+ response += `📅 会话时间: ${this.formatDate(session.updatedAt)} (${session.updatedAt.toLocaleString()})\n`;
556
+ response += `🔧 来源CLI: ${session.cliType}\n`;
557
+ response += `💬 消息数: ${session.messageCount}\n`;
558
+ response += `🆔 会话ID: ${session.sessionId}\n\n`;
559
+ response += `---\n\n`;
560
+ response += `**会话标题:**\n${session.title}\n\n`;
561
+
562
+ const content = session.content.trim();
563
+ if (content.length === 0) {
564
+ response += `⚠️ 此会话内容为空\n`;
565
+ } else {
566
+ response += `**上次讨论内容:**\n`;
567
+ response += content.substring(0, 500);
568
+ if (content.length > 500) {
569
+ response += `...`;
570
+ }
571
+ }
572
+
573
+ return response;
574
+ }
575
+
576
+ // Get CLI icon
577
+ getCLIIcon(cliType) {
578
+ const icons = {
579
+ 'claude': '🟢',
580
+ 'gemini': '🔵',
581
+ 'qwen': '🟡',
582
+ 'iflow': '🔴',
583
+ 'codebuddy': '🟣',
584
+ 'codex': '🟪',
585
+ 'qodercli': '🟠',
586
+ 'kode': '⚡'
587
+ };
588
+ return icons[cliType] || '🔹';
589
+ }
590
+
591
+ // Format date
592
+ formatDate(date) {
593
+ const now = new Date();
594
+ const diff = now.getTime() - date.getTime();
595
+ const days = Math.floor(diff / (24 * 60 * 60 * 1000));
596
+
597
+ if (days === 0) {
598
+ return date.toLocaleTimeString();
599
+ } else if (days === 1) {
600
+ return '昨天';
601
+ } else if (days < 7) {
602
+ return `${days}天前`;
603
+ } else if (days < 30) {
604
+ return `${Math.floor(days / 7)}周前`;
605
+ } else {
606
+ return `${Math.floor(days / 30)}个月前`;
607
+ }
608
+ }
609
+
610
+ // Parse command options
611
+ parseOptions(args) {
612
+ const options = {
613
+ limit: null, // null 表示显示最近的会话,数字表示显示多个
614
+ showAll: false, // --all 显示所有项目的会话
615
+ format: 'context', // 默认显示上下文格式
616
+ timeRange: 'all',
617
+ cli: null,
618
+ search: null
619
+ };
620
+
621
+ for (let i = 0; i < args.length; i++) {
622
+ const arg = args[i];
623
+
624
+ if (arg === '--all') {
625
+ options.showAll = true;
626
+ options.format = 'summary';
627
+ } else if (arg === '--cli' && i + 1 < args.length) {
628
+ options.cli = args[++i];
629
+ } else if (arg === '--search' && i + 1 < args.length) {
630
+ options.search = args[++i];
631
+ } else if (arg === '--limit' && i + 1 < args.length) {
632
+ options.limit = parseInt(args[++i]);
633
+ } else if (arg === '--format' && i + 1 < args.length) {
634
+ const format = args[++i]?.toLowerCase();
635
+ if (['summary', 'timeline', 'detailed', 'context'].includes(format)) {
636
+ options.format = format;
637
+ }
638
+ } else if (arg === '--today') {
639
+ options.timeRange = 'today';
640
+ } else if (arg === '--week') {
641
+ options.timeRange = 'week';
642
+ } else if (arg === '--month') {
643
+ options.timeRange = 'month';
644
+ } else if (!arg.startsWith('--')) {
645
+ // 检查是否是数字
646
+ const num = parseInt(arg);
647
+ if (!isNaN(num) && num > 0) {
648
+ options.limit = num;
649
+ options.format = 'summary'; // 数字参数显示列表格式
650
+ } else if (!options.search) {
651
+ options.search = arg;
652
+ }
653
+ }
654
+ }
655
+
656
+ return options;
657
+ }
658
+
659
+ // Check if session has content
660
+ hasContent(session) {
661
+ if (!session) return false;
662
+ if (!session.content) return false;
663
+ const trimmed = session.content.trim();
664
+ return trimmed.length > 0;
665
+ }
666
+
667
+ // Execute resume command
668
+ execute(args) {
669
+ try {
670
+ const options = this.parseOptions(args);
671
+ const allSessions = this.scanAllCLISessions(this.projectPath);
672
+ const filteredSessions = this.applyFilters(allSessions, options, this.projectPath);
673
+
674
+ let response;
675
+
676
+ // 默认模式:显示最近的会话上下文
677
+ if (options.format === 'context' && !options.limit && !options.showAll) {
678
+ // 找到最近的会话
679
+ let session = filteredSessions[0] || null;
680
+
681
+ // 如果最近的会话内容为空,尝试上一个会话
682
+ if (session && !this.hasContent(session) && filteredSessions.length > 1) {
683
+ for (let i = 1; i < filteredSessions.length; i++) {
684
+ if (this.hasContent(filteredSessions[i])) {
685
+ session = filteredSessions[i];
686
+ break;
687
+ }
688
+ }
689
+ }
690
+
691
+ if (!session) {
692
+ response = `📭 ${options.showAll ? '暂无会话记录' : '当前项目暂无会话记录'}\n\n💡 **提示:** 尝试: stigmergy resume --all 查看所有项目的会话`;
693
+ } else {
694
+ response = this.formatContext(session);
695
+ }
696
+ } else {
697
+ // 列表模式或其他格式
698
+ switch (options.format) {
699
+ case 'timeline':
700
+ response = this.formatTimeline(filteredSessions);
701
+ break;
702
+ case 'detailed':
703
+ response = this.formatDetailed(filteredSessions);
704
+ break;
705
+ case 'context':
706
+ response = this.formatContext(filteredSessions[0] || null);
707
+ break;
708
+ case 'summary':
709
+ default:
710
+ response = this.formatSummary(filteredSessions);
711
+ break;
712
+ }
713
+ }
714
+
715
+ console.log(response);
716
+ return 0;
717
+ } catch (error) {
718
+ console.error(`❌ 历史查询失败: ${error.message}`);
719
+ return 1;
720
+ }
721
+ }
722
+ }
723
+
724
+ // Export for use as module
725
+ module.exports = ResumeSessionCommand;
726
+
727
+ // Export handler function for router
728
+ module.exports.handleResumeCommand = async function(args, options) {
729
+ const command = new ResumeSessionCommand();
730
+ return command.execute(args);
731
+ };
732
+
733
+ // Export help function
734
+ module.exports.printResumeHelp = function() {
735
+ console.log(`
736
+ ResumeSession - Cross-CLI session recovery and history management
737
+
738
+ Usage: stigmergy resume [options] [cli] [limit]
739
+
740
+ 默认行为: 恢复当前项目最近的会话上下文
741
+
742
+ Arguments:
743
+ cli CLI tool to filter (claude, gemini, qwen, iflow, codebuddy, codex, qodercli)
744
+ limit Maximum number of sessions to show (default: show latest session context)
745
+
746
+ Options:
747
+ --all Show all projects' sessions (not just current project)
748
+ --cli <tool> Filter by specific CLI tool
749
+ --search <term> Search sessions by content
750
+ --format <format> Output format: summary, timeline, detailed, context (default: context)
751
+ --today Show today's sessions
752
+ --week Show sessions from last 7 days
753
+ --month Show sessions from last 30 days
754
+ --limit <number> Limit number of sessions
755
+ -v, --verbose Verbose output
756
+ -h, --help Show this help
757
+
758
+ Examples:
759
+ stigmergy resume # 恢复当前项目最近的会话
760
+ stigmergy resume 5 # 显示当前项目最近 5 个会话
761
+ stigmergy resume --all # 显示所有项目的会话
762
+ stigmergy resume iflow # 显示当前项目 iflow 的会话
763
+ stigmergy resume iflow 5 # 显示 iflow 的 5 个会话
764
+ stigmergy resume --search "react" # 搜索包含 "react" 的会话
765
+ stigmergy resume --today # 显示今天的会话
766
+ stigmergy resume --format timeline # 以时间线格式显示
767
+ `);
768
+ };
769
+
770
+ // Run as CLI command
771
+ if (require.main === module) {
772
+ const command = new ResumeSessionCommand();
773
+ const args = process.argv.slice(2);
774
+ process.exit(command.execute(args));
775
+ }