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.
- package/README.md +39 -3
- package/STIGMERGY.md +3 -0
- package/config/builtin-skills.json +43 -0
- package/config/enhanced-cli-config.json +438 -0
- package/docs/CLI_TOOLS_AGENT_SKILL_ANALYSIS.md +463 -0
- package/docs/DESIGN_CLI_HELP_ANALYZER_REFACTOR.md +726 -0
- package/docs/ENHANCED_CLI_AGENT_SKILL_CONFIG.md +285 -0
- package/docs/IMPLEMENTATION_CHECKLIST_CLI_HELP_ANALYZER_REFACTOR.md +1268 -0
- package/docs/INSTALLER_ARCHITECTURE.md +257 -0
- package/docs/LESSONS_LEARNED.md +252 -0
- package/docs/SPECS_CLI_HELP_ANALYZER_REFACTOR.md +287 -0
- package/docs/SUDO_PROBLEM_AND_SOLUTION.md +529 -0
- package/docs/correct-skillsio-implementation.md +368 -0
- package/docs/development_guidelines.md +276 -0
- package/docs/independent-resume-implementation.md +198 -0
- package/docs/resumesession-final-implementation.md +195 -0
- package/docs/resumesession-usage.md +87 -0
- package/package.json +146 -136
- package/scripts/analyze-router.js +168 -0
- package/scripts/run-comprehensive-tests.js +230 -0
- package/scripts/run-quick-tests.js +90 -0
- package/scripts/test-runner.js +344 -0
- package/skills/resumesession/INDEPENDENT_SKILL.md +403 -0
- package/skills/resumesession/README.md +381 -0
- package/skills/resumesession/SKILL.md +211 -0
- package/skills/resumesession/__init__.py +33 -0
- package/skills/resumesession/implementations/simple-resume.js +13 -0
- package/skills/resumesession/independent-resume.js +750 -0
- package/skills/resumesession/package.json +1 -0
- package/skills/resumesession/skill.json +1 -0
- package/src/adapters/claude/install_claude_integration.js +9 -1
- package/src/adapters/codebuddy/install_codebuddy_integration.js +3 -1
- package/src/adapters/codex/install_codex_integration.js +15 -5
- package/src/adapters/gemini/install_gemini_integration.js +3 -1
- package/src/adapters/qwen/install_qwen_integration.js +3 -1
- package/src/cli/commands/autoinstall.js +65 -0
- package/src/cli/commands/errors.js +190 -0
- package/src/cli/commands/independent-resume.js +395 -0
- package/src/cli/commands/install.js +179 -0
- package/src/cli/commands/permissions.js +108 -0
- package/src/cli/commands/project.js +485 -0
- package/src/cli/commands/scan.js +97 -0
- package/src/cli/commands/simple-resume.js +377 -0
- package/src/cli/commands/skills.js +158 -0
- package/src/cli/commands/status.js +113 -0
- package/src/cli/commands/stigmergy-resume.js +775 -0
- package/src/cli/commands/system.js +301 -0
- package/src/cli/commands/universal-resume.js +394 -0
- package/src/cli/router-beta.js +471 -0
- package/src/cli/utils/environment.js +75 -0
- package/src/cli/utils/formatters.js +47 -0
- package/src/cli/utils/skills_cache.js +92 -0
- package/src/core/cache_cleaner.js +1 -0
- package/src/core/cli_adapters.js +345 -0
- package/src/core/cli_help_analyzer.js +1236 -680
- package/src/core/cli_path_detector.js +702 -709
- package/src/core/cli_tools.js +515 -160
- package/src/core/coordination/nodejs/CLIIntegrationManager.js +18 -0
- package/src/core/coordination/nodejs/HookDeploymentManager.js +242 -412
- package/src/core/coordination/nodejs/HookDeploymentManager.refactored.js +323 -0
- package/src/core/coordination/nodejs/generators/CLIAdapterGenerator.js +363 -0
- package/src/core/coordination/nodejs/generators/ResumeSessionGenerator.js +932 -0
- package/src/core/coordination/nodejs/generators/SkillsIntegrationGenerator.js +1395 -0
- package/src/core/coordination/nodejs/generators/index.js +12 -0
- package/src/core/enhanced_cli_installer.js +1208 -608
- package/src/core/enhanced_cli_parameter_handler.js +402 -0
- package/src/core/execution_mode_detector.js +222 -0
- package/src/core/installer.js +151 -106
- package/src/core/local_skill_scanner.js +732 -0
- package/src/core/multilingual/language-pattern-manager.js +1 -1
- package/src/core/skills/BuiltinSkillsDeployer.js +188 -0
- package/src/core/skills/StigmergySkillManager.js +123 -16
- package/src/core/skills/embedded-openskills/SkillParser.js +7 -3
- package/src/core/smart_router.js +550 -261
- package/src/index.js +10 -4
- package/src/utils.js +66 -7
- package/test/cli-integration.test.js +304 -0
- package/test/direct_smart_router_test.js +88 -0
- package/test/enhanced-cli-agent-skill-test.js +485 -0
- package/test/simple_test.js +82 -0
- package/test/smart_router_test_runner.js +123 -0
- package/test/smart_routing_edge_cases.test.js +284 -0
- package/test/smart_routing_simple_verification.js +139 -0
- package/test/smart_routing_verification.test.js +346 -0
- package/test/specific-cli-agent-skill-analysis.js +385 -0
- package/test/unit/smart_router.test.js +295 -0
- package/test/very_simple_test.js +54 -0
- 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
|
+
}
|