@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.
Files changed (74) hide show
  1. package/README.md +77 -2
  2. package/agents/ez-observer-agent.md +260 -0
  3. package/agents/ez-release-agent.md +333 -0
  4. package/agents/ez-requirements-agent.md +377 -0
  5. package/agents/ez-scrum-master-agent.md +242 -0
  6. package/agents/ez-tech-lead-agent.md +267 -0
  7. package/bin/install.js +3221 -3272
  8. package/commands/ez/arch-review.md +102 -0
  9. package/commands/ez/execute-phase.md +11 -0
  10. package/commands/ez/export-session.md +79 -0
  11. package/commands/ez/gather-requirements.md +117 -0
  12. package/commands/ez/git-workflow.md +72 -0
  13. package/commands/ez/hotfix.md +120 -0
  14. package/commands/ez/import-session.md +82 -0
  15. package/commands/ez/list-sessions.md +96 -0
  16. package/commands/ez/package-manager.md +316 -0
  17. package/commands/ez/plan-phase.md +9 -1
  18. package/commands/ez/preflight.md +79 -0
  19. package/commands/ez/progress.md +13 -1
  20. package/commands/ez/release.md +153 -0
  21. package/commands/ez/resume.md +107 -0
  22. package/commands/ez/standup.md +85 -0
  23. package/ez-agents/bin/ez-tools.cjs +1095 -716
  24. package/ez-agents/bin/lib/bdd-validator.cjs +622 -0
  25. package/ez-agents/bin/lib/content-scanner.cjs +238 -0
  26. package/ez-agents/bin/lib/context-cache.cjs +154 -0
  27. package/ez-agents/bin/lib/context-errors.cjs +71 -0
  28. package/ez-agents/bin/lib/context-manager.cjs +220 -0
  29. package/ez-agents/bin/lib/discussion-synthesizer.cjs +458 -0
  30. package/ez-agents/bin/lib/file-access.cjs +207 -0
  31. package/ez-agents/bin/lib/git-errors.cjs +83 -0
  32. package/ez-agents/bin/lib/git-utils.cjs +321 -203
  33. package/ez-agents/bin/lib/git-workflow-engine.cjs +1157 -0
  34. package/ez-agents/bin/lib/index.cjs +46 -2
  35. package/ez-agents/bin/lib/lockfile-validator.cjs +227 -0
  36. package/ez-agents/bin/lib/logger.cjs +124 -154
  37. package/ez-agents/bin/lib/memory-compression.cjs +256 -0
  38. package/ez-agents/bin/lib/metrics-tracker.cjs +406 -0
  39. package/ez-agents/bin/lib/package-manager-detector.cjs +203 -0
  40. package/ez-agents/bin/lib/package-manager-executor.cjs +385 -0
  41. package/ez-agents/bin/lib/package-manager-service.cjs +216 -0
  42. package/ez-agents/bin/lib/release-validator.cjs +614 -0
  43. package/ez-agents/bin/lib/safe-exec.cjs +128 -214
  44. package/ez-agents/bin/lib/session-chain.cjs +304 -0
  45. package/ez-agents/bin/lib/session-errors.cjs +81 -0
  46. package/ez-agents/bin/lib/session-export.cjs +251 -0
  47. package/ez-agents/bin/lib/session-import.cjs +262 -0
  48. package/ez-agents/bin/lib/session-manager.cjs +280 -0
  49. package/ez-agents/bin/lib/tier-manager.cjs +428 -0
  50. package/ez-agents/bin/lib/url-fetch.cjs +170 -0
  51. package/ez-agents/references/metrics-schema.md +118 -0
  52. package/ez-agents/references/planning-config.md +140 -0
  53. package/ez-agents/references/tier-strategy.md +103 -0
  54. package/ez-agents/templates/bdd-feature.md +173 -0
  55. package/ez-agents/templates/discussion.md +68 -0
  56. package/ez-agents/templates/incident-runbook.md +205 -0
  57. package/ez-agents/templates/release-checklist.md +133 -0
  58. package/ez-agents/templates/rollback-plan.md +201 -0
  59. package/ez-agents/workflows/arch-review.md +54 -0
  60. package/ez-agents/workflows/autonomous.md +844 -743
  61. package/ez-agents/workflows/execute-phase.md +45 -0
  62. package/ez-agents/workflows/export-session.md +255 -0
  63. package/ez-agents/workflows/gather-requirements.md +206 -0
  64. package/ez-agents/workflows/help.md +92 -0
  65. package/ez-agents/workflows/hotfix.md +291 -0
  66. package/ez-agents/workflows/import-session.md +303 -0
  67. package/ez-agents/workflows/new-milestone.md +713 -384
  68. package/ez-agents/workflows/new-project.md +1107 -1113
  69. package/ez-agents/workflows/plan-phase.md +22 -0
  70. package/ez-agents/workflows/progress.md +15 -25
  71. package/ez-agents/workflows/release.md +253 -0
  72. package/ez-agents/workflows/resume-session.md +215 -0
  73. package/ez-agents/workflows/standup.md +64 -0
  74. 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
- * - Cross-platform shell detection (Windows: powershell/cmd, Unix: bash)
12
- *
13
- * Usage:
14
- * const { safeExec, safeExecJSON, getShellConfig } = require('./safe-exec.cjs');
15
- * const result = await safeExec('git', ['status']);
16
- */
17
-
18
- const { execFile, spawn } = require('child_process');
19
- const { promisify } = require('util');
20
- const os = require('os');
21
- const execFileAsync = promisify(execFile);
22
- const Logger = require('./logger.cjs');
23
- const logger = new Logger();
24
-
25
- // Allowlist of safe commands
26
- const ALLOWED_COMMANDS = new Set([
27
- 'git', 'node', 'npm', 'npx', 'find', 'grep', 'head', 'tail', 'wc',
28
- 'mkdir', 'cp', 'mv', 'rm', 'cat', 'echo', 'test', 'ls', 'dir',
29
- 'pwd', 'cd', 'type', 'where', 'which', 'chmod', 'touch'
30
- ]);
31
-
32
- // Dangerous shell metacharacters that could enable injection
33
- const DANGEROUS_PATTERN = /[;&|`$(){}\\<>]/;
34
-
35
- /**
36
- * Get shell configuration for current platform
37
- * Returns shell executable and any platform-specific flags
38
- * @returns {{shell: string, shellFlag: string}} - Shell config object
39
- */
40
- function getShellConfig() {
41
- const platform = process.platform;
42
-
43
- if (platform === 'win32') {
44
- // Windows: prefer PowerShell if available, fallback to cmd.exe
45
- // Check if PowerShell is available
46
- try {
47
- // PowerShell Core (pwsh) or Windows PowerShell (powershell)
48
- const pwshPath = require('path').join(
49
- process.env.ProgramFiles || 'C:\\Program Files',
50
- 'PowerShell', '7', 'pwsh.exe'
51
- );
52
- if (require('fs').existsSync(pwshPath)) {
53
- return { shell: 'pwsh', shellFlag: '-Command' };
54
- }
55
- return { shell: 'powershell', shellFlag: '-Command' };
56
- } catch {
57
- return { shell: 'cmd', shellFlag: '/C' };
58
- }
59
- }
60
-
61
- // Unix-like platforms (macOS, Linux): use bash or sh
62
- return { shell: 'bash', shellFlag: '-c' };
63
- }
64
-
65
- /**
66
- * Execute a shell command with platform-appropriate shell
67
- * @param {string} command - Full command string to execute
68
- * @param {Object} options - Execution options
69
- * @returns {Promise<string>} - Command stdout
70
- */
71
- async function safeShellExec(command, options = {}) {
72
- const { timeout = 30000, log = true } = options;
73
- const { shell, shellFlag } = getShellConfig();
74
-
75
- const startTime = Date.now();
76
-
77
- try {
78
- if (log) {
79
- logger.info('Executing shell command', {
80
- command,
81
- shell,
82
- platform: process.platform,
83
- timestamp: new Date().toISOString()
84
- });
85
- }
86
-
87
- const result = await execFileAsync(shell, [shellFlag, command], {
88
- timeout,
89
- maxBuffer: 10 * 1024 * 1024, // 10MB buffer
90
- windowsHide: true // Prevent console flash on Windows
91
- });
92
-
93
- const duration = Date.now() - startTime;
94
- if (log) {
95
- logger.debug('Shell command completed', {
96
- command,
97
- shell,
98
- duration,
99
- stdout_length: result.stdout?.length || 0
100
- });
101
- }
102
-
103
- return result.stdout.trim();
104
- } catch (err) {
105
- const duration = Date.now() - startTime;
106
- logger.error('Shell command failed', {
107
- command,
108
- shell,
109
- error: err.message,
110
- duration,
111
- code: err.code,
112
- signal: err.signal,
113
- platform: process.platform
114
- });
115
- throw err;
116
- }
117
- }
118
-
119
- /**
120
- * Validate command is in allowlist
121
- * @param {string} cmd - Command to validate
122
- * @throws {Error} If command not allowed
123
- */
124
- function validateCommand(cmd) {
125
- const baseCmd = cmd.split(' ')[0].toLowerCase();
126
- if (!ALLOWED_COMMANDS.has(baseCmd)) {
127
- throw new Error(`Command not allowed: ${cmd}. Allowed: ${Array.from(ALLOWED_COMMANDS).join(', ')}`);
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;