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.
- package/bin/echelon-dev.js +286 -0
- package/lib/agents.js +145 -0
- package/lib/echelon-local-agent.js +683 -0
- package/lib/echelon-pair-program.js +610 -0
- package/lib/session-recorder.js +284 -0
- package/package.json +17 -0
|
@@ -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
|
+
}
|