@howlil/ez-agents 3.4.2 → 3.5.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/README.md +77 -2
- package/agents/ez-observer-agent.md +260 -0
- package/agents/ez-release-agent.md +333 -0
- package/agents/ez-requirements-agent.md +377 -0
- package/agents/ez-scrum-master-agent.md +242 -0
- package/agents/ez-tech-lead-agent.md +267 -0
- package/bin/install.js +3221 -3272
- package/commands/ez/arch-review.md +102 -0
- package/commands/ez/execute-phase.md +11 -0
- package/commands/ez/export-session.md +79 -0
- package/commands/ez/gather-requirements.md +117 -0
- package/commands/ez/git-workflow.md +72 -0
- package/commands/ez/hotfix.md +120 -0
- package/commands/ez/import-session.md +82 -0
- package/commands/ez/list-sessions.md +96 -0
- package/commands/ez/package-manager.md +316 -0
- package/commands/ez/plan-phase.md +9 -1
- package/commands/ez/preflight.md +79 -0
- package/commands/ez/progress.md +13 -1
- package/commands/ez/release.md +153 -0
- package/commands/ez/resume.md +107 -0
- package/commands/ez/standup.md +85 -0
- package/ez-agents/bin/ez-tools.cjs +1095 -716
- package/ez-agents/bin/lib/bdd-validator.cjs +622 -0
- package/ez-agents/bin/lib/content-scanner.cjs +238 -0
- package/ez-agents/bin/lib/context-cache.cjs +154 -0
- package/ez-agents/bin/lib/context-errors.cjs +71 -0
- package/ez-agents/bin/lib/context-manager.cjs +220 -0
- package/ez-agents/bin/lib/discussion-synthesizer.cjs +458 -0
- package/ez-agents/bin/lib/file-access.cjs +207 -0
- package/ez-agents/bin/lib/git-errors.cjs +83 -0
- package/ez-agents/bin/lib/git-utils.cjs +321 -203
- package/ez-agents/bin/lib/git-workflow-engine.cjs +1157 -0
- package/ez-agents/bin/lib/index.cjs +46 -2
- package/ez-agents/bin/lib/lockfile-validator.cjs +227 -0
- package/ez-agents/bin/lib/logger.cjs +124 -154
- package/ez-agents/bin/lib/memory-compression.cjs +256 -0
- package/ez-agents/bin/lib/metrics-tracker.cjs +406 -0
- package/ez-agents/bin/lib/package-manager-detector.cjs +203 -0
- package/ez-agents/bin/lib/package-manager-executor.cjs +385 -0
- package/ez-agents/bin/lib/package-manager-service.cjs +216 -0
- package/ez-agents/bin/lib/release-validator.cjs +614 -0
- package/ez-agents/bin/lib/safe-exec.cjs +128 -214
- package/ez-agents/bin/lib/session-chain.cjs +304 -0
- package/ez-agents/bin/lib/session-errors.cjs +81 -0
- package/ez-agents/bin/lib/session-export.cjs +251 -0
- package/ez-agents/bin/lib/session-import.cjs +262 -0
- package/ez-agents/bin/lib/session-manager.cjs +280 -0
- package/ez-agents/bin/lib/tier-manager.cjs +428 -0
- package/ez-agents/bin/lib/url-fetch.cjs +170 -0
- package/ez-agents/references/metrics-schema.md +118 -0
- package/ez-agents/references/planning-config.md +140 -0
- package/ez-agents/references/tier-strategy.md +103 -0
- package/ez-agents/templates/bdd-feature.md +173 -0
- package/ez-agents/templates/discussion.md +68 -0
- package/ez-agents/templates/incident-runbook.md +205 -0
- package/ez-agents/templates/release-checklist.md +133 -0
- package/ez-agents/templates/rollback-plan.md +201 -0
- package/ez-agents/workflows/arch-review.md +54 -0
- package/ez-agents/workflows/autonomous.md +844 -743
- package/ez-agents/workflows/execute-phase.md +45 -0
- package/ez-agents/workflows/export-session.md +255 -0
- package/ez-agents/workflows/gather-requirements.md +206 -0
- package/ez-agents/workflows/help.md +92 -0
- package/ez-agents/workflows/hotfix.md +291 -0
- package/ez-agents/workflows/import-session.md +303 -0
- package/ez-agents/workflows/new-milestone.md +713 -384
- package/ez-agents/workflows/new-project.md +1107 -1113
- package/ez-agents/workflows/plan-phase.md +22 -0
- package/ez-agents/workflows/progress.md +15 -25
- package/ez-agents/workflows/release.md +253 -0
- package/ez-agents/workflows/resume-session.md +215 -0
- package/ez-agents/workflows/standup.md +64 -0
- package/package.json +9 -2
|
@@ -1,214 +1,128 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* EZ Safe Exec — Secure command execution with allowlist and validation
|
|
5
|
-
*
|
|
6
|
-
* Prevents command injection by:
|
|
7
|
-
* - Using execFile instead of execSync with string concatenation
|
|
8
|
-
* - Validating commands against allowlist
|
|
9
|
-
* - Blocking dangerous shell metacharacters in arguments
|
|
10
|
-
* - Logging all commands for audit
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const {
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
'
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Validate arguments don't contain injection patterns
|
|
133
|
-
* @param {string[]} args - Arguments to validate
|
|
134
|
-
* @throws {Error} If dangerous pattern found
|
|
135
|
-
*/
|
|
136
|
-
function validateArgs(args) {
|
|
137
|
-
for (const arg of args) {
|
|
138
|
-
if (DANGEROUS_PATTERN.test(arg)) {
|
|
139
|
-
throw new Error(`Dangerous argument rejected: ${arg}`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Execute command safely with validation and logging
|
|
146
|
-
* @param {string} cmd - Command to execute
|
|
147
|
-
* @param {string[]} args - Command arguments
|
|
148
|
-
* @param {Object} options - Execution options
|
|
149
|
-
* @returns {Promise<string>} - Command stdout
|
|
150
|
-
*/
|
|
151
|
-
async function safeExec(cmd, args = [], options = {}) {
|
|
152
|
-
const { timeout = 30000, log = true } = options;
|
|
153
|
-
|
|
154
|
-
// Validate command and arguments
|
|
155
|
-
validateCommand(cmd);
|
|
156
|
-
validateArgs(args);
|
|
157
|
-
|
|
158
|
-
const startTime = Date.now();
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
if (log) {
|
|
162
|
-
logger.info('Executing command', {
|
|
163
|
-
cmd,
|
|
164
|
-
args,
|
|
165
|
-
timestamp: new Date().toISOString()
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const result = await execFileAsync(cmd, args, {
|
|
170
|
-
timeout,
|
|
171
|
-
maxBuffer: 10 * 1024 * 1024 // 10MB buffer
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
const duration = Date.now() - startTime;
|
|
175
|
-
if (log) {
|
|
176
|
-
logger.debug('Command completed', {
|
|
177
|
-
cmd,
|
|
178
|
-
duration,
|
|
179
|
-
stdout_length: result.stdout?.length || 0
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return result.stdout.trim();
|
|
184
|
-
} catch (err) {
|
|
185
|
-
const duration = Date.now() - startTime;
|
|
186
|
-
logger.error('Command failed', {
|
|
187
|
-
cmd,
|
|
188
|
-
args,
|
|
189
|
-
error: err.message,
|
|
190
|
-
duration,
|
|
191
|
-
code: err.code,
|
|
192
|
-
signal: err.signal
|
|
193
|
-
});
|
|
194
|
-
throw err;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Execute command and return JSON parsed output
|
|
200
|
-
* @param {string} cmd - Command to execute
|
|
201
|
-
* @param {string[]} args - Command arguments
|
|
202
|
-
* @returns {Promise<Object>} - Parsed JSON output
|
|
203
|
-
*/
|
|
204
|
-
async function safeExecJSON(cmd, args = []) {
|
|
205
|
-
const output = await safeExec(cmd, args);
|
|
206
|
-
try {
|
|
207
|
-
return JSON.parse(output);
|
|
208
|
-
} catch (err) {
|
|
209
|
-
logger.error('Failed to parse JSON output', { cmd, output });
|
|
210
|
-
throw new Error(`Invalid JSON from ${cmd}: ${err.message}`);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
module.exports = { safeExec, safeExecJSON, ALLOWED_COMMANDS };
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EZ Safe Exec — Secure command execution with allowlist and validation
|
|
5
|
+
*
|
|
6
|
+
* Prevents command injection by:
|
|
7
|
+
* - Using execFile instead of execSync with string concatenation
|
|
8
|
+
* - Validating commands against allowlist
|
|
9
|
+
* - Blocking dangerous shell metacharacters in arguments
|
|
10
|
+
* - Logging all commands for audit
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* const { safeExec, safeExecJSON } = require('./safe-exec.cjs');
|
|
14
|
+
* const result = await safeExec('git', ['status']);
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { execFile } = require('child_process');
|
|
18
|
+
const { promisify } = require('util');
|
|
19
|
+
const execFileAsync = promisify(execFile);
|
|
20
|
+
const Logger = require('./logger.cjs');
|
|
21
|
+
const logger = new Logger();
|
|
22
|
+
|
|
23
|
+
// Allowlist of safe commands
|
|
24
|
+
const ALLOWED_COMMANDS = new Set([
|
|
25
|
+
'git', 'node', 'npm', 'npx', 'find', 'grep', 'head', 'tail', 'wc',
|
|
26
|
+
'mkdir', 'cp', 'mv', 'rm', 'cat', 'echo', 'test', 'ls', 'dir',
|
|
27
|
+
'pwd', 'cd', 'type', 'where', 'which', 'chmod', 'touch'
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
// Dangerous shell metacharacters that could enable injection
|
|
31
|
+
const DANGEROUS_PATTERN = /[;&|`$(){}\\<>]/;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validate command is in allowlist
|
|
35
|
+
* @param {string} cmd - Command to validate
|
|
36
|
+
* @throws {Error} If command not allowed
|
|
37
|
+
*/
|
|
38
|
+
function validateCommand(cmd) {
|
|
39
|
+
const baseCmd = cmd.split(' ')[0].toLowerCase();
|
|
40
|
+
if (!ALLOWED_COMMANDS.has(baseCmd)) {
|
|
41
|
+
throw new Error(`Command not allowed: ${cmd}. Allowed: ${Array.from(ALLOWED_COMMANDS).join(', ')}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validate arguments don't contain injection patterns
|
|
47
|
+
* @param {string[]} args - Arguments to validate
|
|
48
|
+
* @throws {Error} If dangerous pattern found
|
|
49
|
+
*/
|
|
50
|
+
function validateArgs(args) {
|
|
51
|
+
for (const arg of args) {
|
|
52
|
+
if (DANGEROUS_PATTERN.test(arg)) {
|
|
53
|
+
throw new Error(`Dangerous argument rejected: ${arg}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Execute command safely with validation and logging
|
|
60
|
+
* @param {string} cmd - Command to execute
|
|
61
|
+
* @param {string[]} args - Command arguments
|
|
62
|
+
* @param {Object} options - Execution options
|
|
63
|
+
* @returns {Promise<string>} - Command stdout
|
|
64
|
+
*/
|
|
65
|
+
async function safeExec(cmd, args = [], options = {}) {
|
|
66
|
+
const { timeout = 30000, log = true } = options;
|
|
67
|
+
|
|
68
|
+
// Validate command and arguments
|
|
69
|
+
validateCommand(cmd);
|
|
70
|
+
validateArgs(args);
|
|
71
|
+
|
|
72
|
+
const startTime = Date.now();
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
if (log) {
|
|
76
|
+
logger.info('Executing command', {
|
|
77
|
+
cmd,
|
|
78
|
+
args,
|
|
79
|
+
timestamp: new Date().toISOString()
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const result = await execFileAsync(cmd, args, {
|
|
84
|
+
timeout,
|
|
85
|
+
maxBuffer: 10 * 1024 * 1024 // 10MB buffer
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const duration = Date.now() - startTime;
|
|
89
|
+
if (log) {
|
|
90
|
+
logger.debug('Command completed', {
|
|
91
|
+
cmd,
|
|
92
|
+
duration,
|
|
93
|
+
stdout_length: result.stdout?.length || 0
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return result.stdout.trim();
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const duration = Date.now() - startTime;
|
|
100
|
+
logger.error('Command failed', {
|
|
101
|
+
cmd,
|
|
102
|
+
args,
|
|
103
|
+
error: err.message,
|
|
104
|
+
duration,
|
|
105
|
+
code: err.code,
|
|
106
|
+
signal: err.signal
|
|
107
|
+
});
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Execute command and return JSON parsed output
|
|
114
|
+
* @param {string} cmd - Command to execute
|
|
115
|
+
* @param {string[]} args - Command arguments
|
|
116
|
+
* @returns {Promise<Object>} - Parsed JSON output
|
|
117
|
+
*/
|
|
118
|
+
async function safeExecJSON(cmd, args = []) {
|
|
119
|
+
const output = await safeExec(cmd, args);
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(output);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
logger.error('Failed to parse JSON output', { cmd, output });
|
|
124
|
+
throw new Error(`Invalid JSON from ${cmd}: ${err.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { safeExec, safeExecJSON, ALLOWED_COMMANDS };
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Session Chain — Navigate linked sessions
|
|
5
|
+
*
|
|
6
|
+
* Provides chain navigation, visualization, and repair capabilities
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { SessionChainError } = require('./session-errors.cjs');
|
|
12
|
+
const { defaultLogger: logger } = require('./logger.cjs');
|
|
13
|
+
|
|
14
|
+
class SessionChain {
|
|
15
|
+
/**
|
|
16
|
+
* Create a SessionChain instance
|
|
17
|
+
* @param {Object} sessionManager - SessionManager instance
|
|
18
|
+
*/
|
|
19
|
+
constructor(sessionManager) {
|
|
20
|
+
this.sessionManager = sessionManager;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Navigate to adjacent session in chain
|
|
25
|
+
* @param {string} sessionId - Current session ID
|
|
26
|
+
* @param {string} direction - Navigation direction ('previous' or 'next')
|
|
27
|
+
* @returns {Object|null} Adjacent session or null
|
|
28
|
+
*/
|
|
29
|
+
navigate(sessionId, direction) {
|
|
30
|
+
const session = this.sessionManager.loadSession(sessionId);
|
|
31
|
+
if (!session) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const chain = session.metadata?.session_chain || [];
|
|
36
|
+
const currentIndex = chain.indexOf(sessionId);
|
|
37
|
+
|
|
38
|
+
if (currentIndex === -1) {
|
|
39
|
+
// Session not in chain, check if it's the last one
|
|
40
|
+
logger.warn('Session not in chain', { sessionId });
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (direction === 'previous') {
|
|
45
|
+
if (currentIndex > 0) {
|
|
46
|
+
const previousId = chain[currentIndex - 1];
|
|
47
|
+
return this.sessionManager.loadSession(previousId);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (direction === 'next') {
|
|
53
|
+
if (currentIndex < chain.length - 1) {
|
|
54
|
+
const nextId = chain[currentIndex + 1];
|
|
55
|
+
return this.sessionManager.loadSession(nextId);
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new SessionChainError(`Invalid direction: ${direction}`, chain);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get full chain as array of session objects
|
|
65
|
+
* @param {string} sessionId - Session ID in the chain
|
|
66
|
+
* @returns {Array} Array of session objects
|
|
67
|
+
*/
|
|
68
|
+
getChain(sessionId) {
|
|
69
|
+
const session = this.sessionManager.loadSession(sessionId);
|
|
70
|
+
if (!session) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const chain = session.metadata?.session_chain || [];
|
|
75
|
+
const chainSessions = [];
|
|
76
|
+
|
|
77
|
+
for (const id of chain) {
|
|
78
|
+
const chainSession = this.sessionManager.loadSession(id);
|
|
79
|
+
if (chainSession) {
|
|
80
|
+
chainSessions.push(chainSession);
|
|
81
|
+
} else {
|
|
82
|
+
logger.warn('Missing session in chain', { id });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Include current session if not already in chain
|
|
87
|
+
if (!chain.includes(sessionId)) {
|
|
88
|
+
chainSessions.push(session);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return chainSessions;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get chain visualization string
|
|
96
|
+
* @param {string} sessionId - Session ID
|
|
97
|
+
* @returns {string} Formatted chain visualization
|
|
98
|
+
*/
|
|
99
|
+
getChainVisualization(sessionId) {
|
|
100
|
+
const session = this.sessionManager.loadSession(sessionId);
|
|
101
|
+
if (!session) {
|
|
102
|
+
return `Session not found: ${sessionId}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const chain = session.metadata?.session_chain || [];
|
|
106
|
+
const currentIndex = chain.indexOf(sessionId);
|
|
107
|
+
|
|
108
|
+
let viz = `Session Chain for ${sessionId}:\n\n`;
|
|
109
|
+
|
|
110
|
+
chain.forEach((id, index) => {
|
|
111
|
+
const chainSession = this.sessionManager.loadSession(id);
|
|
112
|
+
const startedAt = chainSession?.metadata?.started_at || 'Unknown';
|
|
113
|
+
const status = chainSession?.metadata?.status || 'unknown';
|
|
114
|
+
const marker = index === currentIndex ? ' <-- Current' : '';
|
|
115
|
+
viz += `[${index + 1}] ${id} (${startedAt}) - ${status}${marker}\n`;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!chain.includes(sessionId)) {
|
|
119
|
+
const startedAt = session.metadata?.started_at || 'Unknown';
|
|
120
|
+
const status = session.metadata?.status || 'unknown';
|
|
121
|
+
viz += `[${chain.length + 1}] ${sessionId} (${startedAt}) - ${status} <-- Current\n`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
viz += `\nNavigation:\n`;
|
|
125
|
+
if (currentIndex > 0) {
|
|
126
|
+
viz += `- Previous: ${chain[currentIndex - 1]}\n`;
|
|
127
|
+
} else {
|
|
128
|
+
viz += `- Previous: none\n`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (currentIndex < chain.length - 1) {
|
|
132
|
+
viz += `- Next: ${chain[currentIndex + 1]}\n`;
|
|
133
|
+
} else {
|
|
134
|
+
viz += `- Next: none\n`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return viz;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Repair broken chain links
|
|
142
|
+
* @param {string} sessionId - Session ID
|
|
143
|
+
* @returns {Object} Repair result with warnings
|
|
144
|
+
*/
|
|
145
|
+
repairChain(sessionId) {
|
|
146
|
+
const session = this.sessionManager.loadSession(sessionId);
|
|
147
|
+
if (!session) {
|
|
148
|
+
throw new SessionChainError(`Session not found: ${sessionId}`, []);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const chain = session.metadata?.session_chain || [];
|
|
152
|
+
const warnings = [];
|
|
153
|
+
const repaired = [];
|
|
154
|
+
|
|
155
|
+
// Get all available sessions
|
|
156
|
+
const allSessions = this.sessionManager.listSessions();
|
|
157
|
+
const availableIds = new Set(allSessions.map(s => s.session_id));
|
|
158
|
+
|
|
159
|
+
// Find missing links
|
|
160
|
+
const missingLinks = [];
|
|
161
|
+
for (const id of chain) {
|
|
162
|
+
if (!availableIds.has(id)) {
|
|
163
|
+
missingLinks.push(id);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (missingLinks.length === 0) {
|
|
168
|
+
return { repaired: false, warnings: ['Chain is intact'] };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Attempt to repair by finding closest timestamp match
|
|
172
|
+
for (const missingId of missingLinks) {
|
|
173
|
+
const match = this._findClosestSessionMatch(missingId, allSessions);
|
|
174
|
+
if (match) {
|
|
175
|
+
logger.info('Auto-repaired chain link', { missing: missingId, found: match.session_id });
|
|
176
|
+
repaired.push({ missing: missingId, found: match.session_id });
|
|
177
|
+
} else {
|
|
178
|
+
warnings.push(`Unrecoverable link: ${missingId}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Update chain with repaired links
|
|
183
|
+
if (repaired.length > 0) {
|
|
184
|
+
const newChain = chain.map(id => {
|
|
185
|
+
const repair = repaired.find(r => r.missing === id);
|
|
186
|
+
return repair ? repair.found : id;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
this.sessionManager.updateSession(sessionId, {
|
|
190
|
+
metadata: { session_chain: newChain }
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
repaired: repaired.length > 0,
|
|
196
|
+
repairs: repaired,
|
|
197
|
+
warnings
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Find closest session match by timestamp
|
|
203
|
+
* @private
|
|
204
|
+
*/
|
|
205
|
+
_findClosestSessionMatch(missingId, allSessions) {
|
|
206
|
+
// Extract timestamp from missing ID
|
|
207
|
+
const timestampMatch = missingId.match(/session-(.+)/);
|
|
208
|
+
if (!timestampMatch) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const missingTimestamp = timestampMatch[1];
|
|
213
|
+
|
|
214
|
+
// Find session with closest timestamp
|
|
215
|
+
let closestMatch = null;
|
|
216
|
+
let minDiff = Infinity;
|
|
217
|
+
|
|
218
|
+
for (const session of allSessions) {
|
|
219
|
+
const sessionTimestamp = session.session_id.replace('session-', '');
|
|
220
|
+
const diff = this._compareTimestamps(missingTimestamp, sessionTimestamp);
|
|
221
|
+
|
|
222
|
+
if (diff < minDiff) {
|
|
223
|
+
minDiff = diff;
|
|
224
|
+
closestMatch = session;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Only return match if within reasonable threshold (1 hour)
|
|
229
|
+
if (minDiff < 3600000) {
|
|
230
|
+
return closestMatch;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Compare timestamps and return difference in ms
|
|
238
|
+
* @private
|
|
239
|
+
*/
|
|
240
|
+
_compareTimestamps(ts1, ts2) {
|
|
241
|
+
try {
|
|
242
|
+
const date1 = new Date(ts1.replace(/-/g, ':').replace('T', ' '));
|
|
243
|
+
const date2 = new Date(ts2.replace(/-/g, ':').replace('T', ' '));
|
|
244
|
+
return Math.abs(date1 - date2);
|
|
245
|
+
} catch {
|
|
246
|
+
return Infinity;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Add session to chain
|
|
252
|
+
* @param {string} sessionId - Session ID to add to
|
|
253
|
+
* @param {string} linkedSessionId - Session ID to link
|
|
254
|
+
* @param {string} position - Position ('before' or 'after')
|
|
255
|
+
* @returns {boolean} Success
|
|
256
|
+
*/
|
|
257
|
+
addToChain(sessionId, linkedSessionId, position = 'after') {
|
|
258
|
+
const session = this.sessionManager.loadSession(sessionId);
|
|
259
|
+
if (!session) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const linkedSession = this.sessionManager.loadSession(linkedSessionId);
|
|
264
|
+
if (!linkedSession) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let chain = session.metadata?.session_chain || [];
|
|
269
|
+
|
|
270
|
+
// Ensure current session is in chain
|
|
271
|
+
if (!chain.includes(sessionId)) {
|
|
272
|
+
chain.push(sessionId);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const currentIndex = chain.indexOf(sessionId);
|
|
276
|
+
|
|
277
|
+
if (position === 'after') {
|
|
278
|
+
// Insert after current
|
|
279
|
+
chain.splice(currentIndex + 1, 0, linkedSessionId);
|
|
280
|
+
} else if (position === 'before') {
|
|
281
|
+
// Insert before current
|
|
282
|
+
chain.splice(currentIndex, 0, linkedSessionId);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Update both sessions
|
|
286
|
+
this.sessionManager.updateSession(sessionId, {
|
|
287
|
+
metadata: { session_chain: chain }
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Also update linked session's chain
|
|
291
|
+
const linkedChain = linkedSession.metadata?.session_chain || [];
|
|
292
|
+
if (!linkedChain.includes(sessionId)) {
|
|
293
|
+
linkedChain.push(sessionId);
|
|
294
|
+
this.sessionManager.updateSession(linkedSessionId, {
|
|
295
|
+
metadata: { session_chain: linkedChain }
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
logger.info('Session added to chain', { sessionId, linkedSessionId, position });
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
module.exports = SessionChain;
|