echelon-dev 1.0.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.
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Session Recorder - Captures EVERYTHING from AI pair programming sessions
3
+ *
4
+ * Saves full prompts, responses, code changes (git diffs), timing data,
5
+ * tools used, and workspace state for future model training.
6
+ *
7
+ * Output: ~/.echelon/sessions/{timestamp}-{task-slug}.json
8
+ *
9
+ * ZERO EXTERNAL DEPENDENCIES
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+ const { execSync } = require('child_process');
16
+
17
+ const SESSIONS_DIR = path.join(os.homedir(), '.echelon', 'sessions');
18
+
19
+ function ensureDir(dirPath) {
20
+ if (!fs.existsSync(dirPath)) {
21
+ fs.mkdirSync(dirPath, { recursive: true });
22
+ }
23
+ }
24
+
25
+ function slugify(text) {
26
+ return text
27
+ .toLowerCase()
28
+ .replace(/[^a-z0-9]+/g, '-')
29
+ .replace(/^-|-$/g, '')
30
+ .slice(0, 60);
31
+ }
32
+
33
+ function safeExec(command, cwd) {
34
+ try {
35
+ return execSync(command, { cwd, encoding: 'utf8', timeout: 10000 }).trim();
36
+ } catch (e) {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function getGitState(workspace) {
42
+ const isGit = fs.existsSync(path.join(workspace, '.git'));
43
+ if (!isGit) return null;
44
+
45
+ return {
46
+ branch: safeExec('git rev-parse --abbrev-ref HEAD', workspace),
47
+ commit: safeExec('git rev-parse --short HEAD', workspace),
48
+ dirty: safeExec('git status --porcelain', workspace) || '',
49
+ diff: safeExec('git diff', workspace) || '',
50
+ stagedDiff: safeExec('git diff --cached', workspace) || ''
51
+ };
52
+ }
53
+
54
+ function getFilesSnapshot(workspace) {
55
+ // Quick snapshot of file modification times for key files
56
+ try {
57
+ const output = safeExec(
58
+ 'find . -maxdepth 4 -type f \\( -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.jsx" -o -name "*.java" -o -name "*.py" -o -name "*.go" -o -name "*.rs" -o -name "*.json" -o -name "*.yaml" -o -name "*.yml" \\) -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/target/*" -not -path "*/dist/*" -not -path "*/build/*" -printf "%T@ %p\\n" 2>/dev/null | sort -rn | head -50',
59
+ workspace
60
+ );
61
+ // Fallback for macOS (no -printf)
62
+ if (!output) {
63
+ const macOutput = safeExec(
64
+ 'find . -maxdepth 4 -type f \\( -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.jsx" -o -name "*.java" -o -name "*.py" -o -name "*.go" \\) -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/target/*" | head -50',
65
+ workspace
66
+ );
67
+ return macOutput ? macOutput.split('\n').filter(Boolean) : [];
68
+ }
69
+ return output ? output.split('\n').filter(Boolean) : [];
70
+ } catch (e) {
71
+ return [];
72
+ }
73
+ }
74
+
75
+ class SessionRecorder {
76
+ constructor(options = {}) {
77
+ this.sessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
78
+ this.task = options.task || '';
79
+ this.workspace = options.workspace || process.cwd();
80
+ this.agents = options.agents || [];
81
+ this.aiProvider = options.aiProvider || 'unknown';
82
+ this.startedAt = new Date().toISOString();
83
+ this.steps = [];
84
+ this.metadata = {};
85
+
86
+ // Capture initial state
87
+ this.initialGitState = getGitState(this.workspace);
88
+ this.initialFiles = getFilesSnapshot(this.workspace);
89
+
90
+ ensureDir(SESSIONS_DIR);
91
+ }
92
+
93
+ /**
94
+ * Record a step starting
95
+ */
96
+ startStep(stepKey, agentId, instruction) {
97
+ const step = {
98
+ stepKey,
99
+ agentId,
100
+ agentName: agentId,
101
+ instruction,
102
+ prompt: null, // Full prompt sent to AI (set later)
103
+ response: null, // Full AI response (set later)
104
+ startedAt: new Date().toISOString(),
105
+ completedAt: null,
106
+ durationMs: null,
107
+ gitDiffBefore: safeExec('git diff', this.workspace) || '',
108
+ gitDiffAfter: null,
109
+ error: null
110
+ };
111
+ this.steps.push(step);
112
+ return this.steps.length - 1;
113
+ }
114
+
115
+ /**
116
+ * Record the full prompt sent to AI
117
+ */
118
+ setStepPrompt(stepIndex, prompt) {
119
+ if (this.steps[stepIndex]) {
120
+ this.steps[stepIndex].prompt = prompt;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Record the full AI response
126
+ */
127
+ completeStep(stepIndex, response, error) {
128
+ const step = this.steps[stepIndex];
129
+ if (!step) return;
130
+
131
+ step.response = response;
132
+ step.completedAt = new Date().toISOString();
133
+ step.durationMs = new Date(step.completedAt) - new Date(step.startedAt);
134
+ step.gitDiffAfter = safeExec('git diff', this.workspace) || '';
135
+ step.error = error || null;
136
+
137
+ // Compute what changed in this step
138
+ if (step.gitDiffBefore !== step.gitDiffAfter) {
139
+ step.codeChanged = true;
140
+ step.diffDelta = step.gitDiffAfter.length - step.gitDiffBefore.length;
141
+ } else {
142
+ step.codeChanged = false;
143
+ step.diffDelta = 0;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Add arbitrary metadata
149
+ */
150
+ addMetadata(key, value) {
151
+ this.metadata[key] = value;
152
+ }
153
+
154
+ /**
155
+ * Finalize and save the session recording
156
+ */
157
+ save() {
158
+ const finalGitState = getGitState(this.workspace);
159
+ const completedAt = new Date().toISOString();
160
+ const totalDurationMs = new Date(completedAt) - new Date(this.startedAt);
161
+
162
+ const record = {
163
+ // Session identity
164
+ sessionId: this.sessionId,
165
+ version: '1.0.0',
166
+
167
+ // What happened
168
+ task: this.task,
169
+ workspace: this.workspace,
170
+ agents: this.agents,
171
+ aiProvider: this.aiProvider,
172
+
173
+ // Timing
174
+ startedAt: this.startedAt,
175
+ completedAt,
176
+ totalDurationMs,
177
+
178
+ // Steps (the meat — prompts, responses, diffs)
179
+ steps: this.steps,
180
+ totalSteps: this.steps.length,
181
+ completedSteps: this.steps.filter(s => s.completedAt && !s.error).length,
182
+ failedSteps: this.steps.filter(s => s.error).length,
183
+
184
+ // Git state
185
+ git: {
186
+ initial: this.initialGitState,
187
+ final: finalGitState,
188
+ totalDiff: finalGitState
189
+ ? safeExec('git diff ' + (this.initialGitState?.commit || 'HEAD'), this.workspace)
190
+ : null
191
+ },
192
+
193
+ // Environment
194
+ environment: {
195
+ platform: os.platform(),
196
+ arch: os.arch(),
197
+ nodeVersion: process.version,
198
+ hostname: os.hostname()
199
+ },
200
+
201
+ // Extra metadata
202
+ metadata: this.metadata
203
+ };
204
+
205
+ // Write to file
206
+ const slug = slugify(this.task);
207
+ const filename = `${this.sessionId}-${slug}.json`;
208
+ const filepath = path.join(SESSIONS_DIR, filename);
209
+
210
+ fs.writeFileSync(filepath, JSON.stringify(record, null, 2), 'utf8');
211
+
212
+ // Also write a lightweight index entry
213
+ this._updateIndex(filepath, record);
214
+
215
+ return filepath;
216
+ }
217
+
218
+ /**
219
+ * Maintain a lightweight session index for quick lookups
220
+ */
221
+ _updateIndex(filepath, record) {
222
+ const indexPath = path.join(SESSIONS_DIR, 'index.jsonl');
223
+ const entry = {
224
+ sessionId: record.sessionId,
225
+ task: record.task,
226
+ workspace: record.workspace,
227
+ agents: record.agents,
228
+ startedAt: record.startedAt,
229
+ totalDurationMs: record.totalDurationMs,
230
+ totalSteps: record.totalSteps,
231
+ completedSteps: record.completedSteps,
232
+ file: path.basename(filepath)
233
+ };
234
+
235
+ try {
236
+ fs.appendFileSync(indexPath, JSON.stringify(entry) + '\n', 'utf8');
237
+ } catch (e) {
238
+ // Non-critical — index is a convenience
239
+ }
240
+ }
241
+ }
242
+
243
+ /**
244
+ * List recent sessions
245
+ */
246
+ function listSessions(limit = 20) {
247
+ const indexPath = path.join(SESSIONS_DIR, 'index.jsonl');
248
+ if (!fs.existsSync(indexPath)) return [];
249
+
250
+ try {
251
+ const lines = fs.readFileSync(indexPath, 'utf8')
252
+ .split('\n')
253
+ .filter(Boolean)
254
+ .map(line => JSON.parse(line));
255
+
256
+ return lines.slice(-limit).reverse();
257
+ } catch (e) {
258
+ return [];
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Load a full session recording
264
+ */
265
+ function loadSession(sessionId) {
266
+ ensureDir(SESSIONS_DIR);
267
+
268
+ try {
269
+ const files = fs.readdirSync(SESSIONS_DIR).filter(f => f.startsWith(sessionId));
270
+ if (files.length === 0) return null;
271
+
272
+ const filepath = path.join(SESSIONS_DIR, files[0]);
273
+ return JSON.parse(fs.readFileSync(filepath, 'utf8'));
274
+ } catch (e) {
275
+ return null;
276
+ }
277
+ }
278
+
279
+ module.exports = {
280
+ SessionRecorder,
281
+ listSessions,
282
+ loadSession,
283
+ SESSIONS_DIR
284
+ };
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "echelon-dev",
3
+ "version": "1.0.0",
4
+ "description": "Multi-agent pair programming with Knoxis, Solan, and Astrahelm - your AI dev team on qig.ai",
5
+ "bin": {
6
+ "echelon-dev": "./bin/echelon-dev.js"
7
+ },
8
+ "keywords": ["echelon", "pair-programming", "claude", "qig", "multi-agent", "knoxis", "solan", "astrahelm"],
9
+ "license": "MIT",
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "files": [
14
+ "bin/",
15
+ "lib/"
16
+ ]
17
+ }