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,286 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Echelon Dev - Multi-Agent Pair Programming CLI
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx echelon-dev # Interactive setup (first time)
|
|
8
|
+
* npx echelon-dev --backend wss://... # Connect to specific backend
|
|
9
|
+
* npx echelon-dev --status # Check connection status
|
|
10
|
+
* npx echelon-dev pair --task "add auth" # Start pair programming session
|
|
11
|
+
* npx echelon-dev sessions # List recorded sessions
|
|
12
|
+
* npx echelon-dev session <id> # View session details
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const readline = require('readline');
|
|
19
|
+
|
|
20
|
+
const CONFIG_DIR = path.join(os.homedir(), '.echelon');
|
|
21
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
22
|
+
const DEFAULT_BACKEND = 'wss://voice-app-v2.graymushroom-5a5599fe.centralus.azurecontainerapps.io';
|
|
23
|
+
|
|
24
|
+
function loadConfig() {
|
|
25
|
+
try {
|
|
26
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
27
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
28
|
+
}
|
|
29
|
+
} catch (e) {}
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function saveConfig(config) {
|
|
34
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
35
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseArgs() {
|
|
41
|
+
const args = {};
|
|
42
|
+
const positional = [];
|
|
43
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
44
|
+
const arg = process.argv[i];
|
|
45
|
+
if (arg === '--status') { args.status = true; continue; }
|
|
46
|
+
if (arg === '--reset') { args.reset = true; continue; }
|
|
47
|
+
if (arg.startsWith('--')) {
|
|
48
|
+
const key = arg.slice(2);
|
|
49
|
+
const next = process.argv[i + 1];
|
|
50
|
+
if (next && !next.startsWith('--')) {
|
|
51
|
+
args[key] = next;
|
|
52
|
+
i++;
|
|
53
|
+
} else {
|
|
54
|
+
args[key] = true;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
positional.push(arg);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
args._positional = positional;
|
|
61
|
+
return args;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function ask(rl, question) {
|
|
65
|
+
return new Promise(resolve => rl.question(question, resolve));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function firstTimeSetup(args) {
|
|
69
|
+
const config = loadConfig();
|
|
70
|
+
|
|
71
|
+
console.log('');
|
|
72
|
+
console.log(' Echelon Dev - First Time Setup');
|
|
73
|
+
console.log(' ==============================');
|
|
74
|
+
console.log('');
|
|
75
|
+
console.log(' This connects your machine to the Echelon dev team');
|
|
76
|
+
console.log(' (Knoxis, Solan, Astrahelm) for pair programming.');
|
|
77
|
+
console.log('');
|
|
78
|
+
|
|
79
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
80
|
+
|
|
81
|
+
let backend = args.backend || config.backendWsUrl;
|
|
82
|
+
if (!backend) {
|
|
83
|
+
const answer = await ask(rl, ` Backend URL [${DEFAULT_BACKEND}]: `);
|
|
84
|
+
backend = answer.trim() || DEFAULT_BACKEND;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (backend.startsWith('http://')) {
|
|
88
|
+
backend = backend.replace('http://', 'ws://');
|
|
89
|
+
} else if (backend.startsWith('https://')) {
|
|
90
|
+
backend = backend.replace('https://', 'wss://');
|
|
91
|
+
} else if (!backend.startsWith('ws://') && !backend.startsWith('wss://')) {
|
|
92
|
+
backend = 'wss://' + backend;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let userId = args.userId || args.user || config.userId;
|
|
96
|
+
if (!userId) {
|
|
97
|
+
console.log('');
|
|
98
|
+
console.log(' Your user ID is on your profile page at qig.ai/settings.');
|
|
99
|
+
const answer = await ask(rl, ' User ID: ');
|
|
100
|
+
userId = answer.trim();
|
|
101
|
+
|
|
102
|
+
if (!userId) {
|
|
103
|
+
console.error(' User ID is required. Find it at qig.ai/settings.');
|
|
104
|
+
rl.close();
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
rl.close();
|
|
110
|
+
|
|
111
|
+
config.backendWsUrl = backend;
|
|
112
|
+
config.userId = userId;
|
|
113
|
+
config.configuredAt = new Date().toISOString();
|
|
114
|
+
saveConfig(config);
|
|
115
|
+
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log(' Configuration saved to ~/.echelon/config.json');
|
|
118
|
+
console.log(` Backend: ${backend}`);
|
|
119
|
+
console.log(` User: ${userId}`);
|
|
120
|
+
console.log('');
|
|
121
|
+
|
|
122
|
+
return config;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function showSessions() {
|
|
126
|
+
const { listSessions } = require('../lib/session-recorder');
|
|
127
|
+
const sessions = listSessions(20);
|
|
128
|
+
|
|
129
|
+
if (sessions.length === 0) {
|
|
130
|
+
console.log('No recorded sessions yet.');
|
|
131
|
+
console.log('Run a pair programming session to start recording.');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log('');
|
|
136
|
+
console.log(' Recent Sessions');
|
|
137
|
+
console.log(' ===============');
|
|
138
|
+
console.log('');
|
|
139
|
+
|
|
140
|
+
for (const session of sessions) {
|
|
141
|
+
const date = new Date(session.startedAt).toLocaleString();
|
|
142
|
+
const duration = session.totalDurationMs
|
|
143
|
+
? `${Math.round(session.totalDurationMs / 1000)}s`
|
|
144
|
+
: '?';
|
|
145
|
+
const agents = session.agents ? session.agents.join(', ') : '?';
|
|
146
|
+
const steps = `${session.completedSteps || 0}/${session.totalSteps || 0} steps`;
|
|
147
|
+
|
|
148
|
+
console.log(` ${session.sessionId}`);
|
|
149
|
+
console.log(` Task: ${session.task}`);
|
|
150
|
+
console.log(` Date: ${date} Duration: ${duration} Agents: ${agents} ${steps}`);
|
|
151
|
+
console.log('');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function showSessionDetail(sessionId) {
|
|
156
|
+
const { loadSession } = require('../lib/session-recorder');
|
|
157
|
+
const session = loadSession(sessionId);
|
|
158
|
+
|
|
159
|
+
if (!session) {
|
|
160
|
+
console.error(`Session not found: ${sessionId}`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log('');
|
|
165
|
+
console.log(` Session: ${session.sessionId}`);
|
|
166
|
+
console.log(` Task: ${session.task}`);
|
|
167
|
+
console.log(` Workspace: ${session.workspace}`);
|
|
168
|
+
console.log(` Agents: ${session.agents.join(', ')}`);
|
|
169
|
+
console.log(` AI: ${session.aiProvider}`);
|
|
170
|
+
console.log(` Duration: ${Math.round(session.totalDurationMs / 1000)}s`);
|
|
171
|
+
console.log(` Steps: ${session.completedSteps}/${session.totalSteps} completed`);
|
|
172
|
+
console.log('');
|
|
173
|
+
|
|
174
|
+
for (const step of session.steps) {
|
|
175
|
+
const status = step.error ? 'FAILED' : 'OK';
|
|
176
|
+
const duration = step.durationMs ? `${Math.round(step.durationMs / 1000)}s` : '?';
|
|
177
|
+
const changed = step.codeChanged ? ' [code changed]' : '';
|
|
178
|
+
|
|
179
|
+
console.log(` [${status}] ${step.agentId} - ${step.stepKey} (${duration})${changed}`);
|
|
180
|
+
if (step.error) {
|
|
181
|
+
console.log(` Error: ${step.error}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (session.git?.final?.commit) {
|
|
186
|
+
console.log('');
|
|
187
|
+
console.log(` Git: ${session.git.initial?.commit || '?'} -> ${session.git.final.commit}`);
|
|
188
|
+
}
|
|
189
|
+
console.log('');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function main() {
|
|
193
|
+
const args = parseArgs();
|
|
194
|
+
const command = args._positional[0];
|
|
195
|
+
|
|
196
|
+
// Reset
|
|
197
|
+
if (args.reset) {
|
|
198
|
+
try { fs.unlinkSync(CONFIG_FILE); } catch (e) {}
|
|
199
|
+
console.log('Configuration cleared. Run again to set up.');
|
|
200
|
+
process.exit(0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Status
|
|
204
|
+
if (args.status) {
|
|
205
|
+
const config = loadConfig();
|
|
206
|
+
if (!config.backendWsUrl || !config.userId) {
|
|
207
|
+
console.log('Not configured. Run: npx echelon-dev');
|
|
208
|
+
} else {
|
|
209
|
+
console.log('Configured:');
|
|
210
|
+
console.log(` Backend: ${config.backendWsUrl}`);
|
|
211
|
+
console.log(` User: ${config.userId}`);
|
|
212
|
+
}
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Sessions listing
|
|
217
|
+
if (command === 'sessions') {
|
|
218
|
+
showSessions();
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Session detail
|
|
223
|
+
if (command === 'session') {
|
|
224
|
+
const sessionId = args._positional[1];
|
|
225
|
+
if (!sessionId) {
|
|
226
|
+
console.error('Usage: echelon-dev session <session-id>');
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
showSessionDetail(sessionId);
|
|
230
|
+
process.exit(0);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Pair programming (direct CLI mode)
|
|
234
|
+
if (command === 'pair') {
|
|
235
|
+
const task = args.task || args.prompt;
|
|
236
|
+
if (!task) {
|
|
237
|
+
console.error('Usage: echelon-dev pair --task "your task description" [--workspace /path] [--agents knoxis,solan]');
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Build args for the pair programming script
|
|
242
|
+
const pairArgs = ['node', path.join(__dirname, '..', 'lib', 'echelon-pair-program.js')];
|
|
243
|
+
pairArgs.push('--workspace', args.workspace || process.cwd());
|
|
244
|
+
pairArgs.push('--prompt', task);
|
|
245
|
+
if (args.agents) pairArgs.push('--agents', args.agents);
|
|
246
|
+
if (args.provider) pairArgs.push('--ai-provider', args.provider);
|
|
247
|
+
if (args['context-file']) pairArgs.push('--context-file', args['context-file']);
|
|
248
|
+
if (args['no-record']) pairArgs.push('--no-record');
|
|
249
|
+
|
|
250
|
+
// Set process.argv and require the script
|
|
251
|
+
process.argv = pairArgs;
|
|
252
|
+
require('../lib/echelon-pair-program');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Default: setup + launch local agent
|
|
257
|
+
let config = loadConfig();
|
|
258
|
+
const needsSetup = !config.backendWsUrl || !config.userId || args.backend || args.userId;
|
|
259
|
+
|
|
260
|
+
if (needsSetup) {
|
|
261
|
+
config = await firstTimeSetup(args);
|
|
262
|
+
} else {
|
|
263
|
+
console.log('');
|
|
264
|
+
console.log(` Echelon Dev - Connecting to ${config.backendWsUrl}`);
|
|
265
|
+
console.log(` User: ${config.userId}`);
|
|
266
|
+
console.log('');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Set environment variables for the local agent
|
|
270
|
+
process.env.ECHELON_BACKEND_WS_URL = config.backendWsUrl;
|
|
271
|
+
process.env.ECHELON_USER_ID = config.userId;
|
|
272
|
+
|
|
273
|
+
// Launch the local agent
|
|
274
|
+
const agentPath = path.join(__dirname, '..', 'lib', 'echelon-local-agent.js');
|
|
275
|
+
if (!fs.existsSync(agentPath)) {
|
|
276
|
+
console.error('Could not find echelon-local-agent.js at:', agentPath);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
require(agentPath);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
main().catch(err => {
|
|
284
|
+
console.error('Error:', err.message);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
});
|
package/lib/agents.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Echelon Dev Team - Agent Persona Definitions
|
|
3
|
+
*
|
|
4
|
+
* These are three senior engineers on a team. Each has a specialty,
|
|
5
|
+
* but all are competent full-stack developers who can handle anything.
|
|
6
|
+
* They don't force their specialty where it doesn't apply.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const AGENTS = {
|
|
10
|
+
knoxis: {
|
|
11
|
+
id: 'knoxis',
|
|
12
|
+
name: 'Knoxis',
|
|
13
|
+
color: '\x1b[36m', // cyan
|
|
14
|
+
specialty: 'architecture and systems logic',
|
|
15
|
+
description: 'Senior engineer. Thinks in systems — data flow, APIs, state management, infrastructure. Strong across the full stack but naturally gravitates to the structural side of things.',
|
|
16
|
+
persona: `You are Knoxis, a senior software engineer on a small dev team.
|
|
17
|
+
|
|
18
|
+
Your strengths: Systems thinking, architecture, API design, data modeling, backend logic, infrastructure.
|
|
19
|
+
But you're a well-rounded engineer — you're comfortable with frontend, DevOps, databases, whatever the task needs.
|
|
20
|
+
|
|
21
|
+
Your style:
|
|
22
|
+
- You think about how pieces fit together before writing code
|
|
23
|
+
- You prefer clean, simple solutions over clever ones
|
|
24
|
+
- You speak plainly about tradeoffs — no hand-waving
|
|
25
|
+
- If something is over-engineered, you'll say so
|
|
26
|
+
- You don't add complexity unless it earns its keep
|
|
27
|
+
|
|
28
|
+
You are NOT a "backend-only" bot. You work on whatever the task requires.
|
|
29
|
+
If the task is purely frontend, you handle it competently without deferring to someone else.
|
|
30
|
+
If there's nothing architectural to say, you just get the work done.`
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
solan: {
|
|
34
|
+
id: 'solan',
|
|
35
|
+
name: 'Solan',
|
|
36
|
+
color: '\x1b[35m', // magenta
|
|
37
|
+
specialty: 'design thinking and user experience',
|
|
38
|
+
description: 'Senior engineer with a design eye. Thinks about how things feel to use — component structure, accessibility, responsive behavior, state that touches the UI. Practical, not precious.',
|
|
39
|
+
persona: `You are Solan, a senior software engineer on a small dev team.
|
|
40
|
+
|
|
41
|
+
Your strengths: UI/UX thinking, component architecture, accessibility, responsive design, frontend state management, making things feel good to use.
|
|
42
|
+
But you're a full-stack engineer — you can write backend code, work with databases, set up infrastructure, whatever is needed.
|
|
43
|
+
|
|
44
|
+
Your style:
|
|
45
|
+
- You think about the person using the thing, not just the code
|
|
46
|
+
- You only bring up design considerations when they're actually relevant
|
|
47
|
+
- You don't redesign things that work fine — if it ain't broke, leave it alone
|
|
48
|
+
- You're practical, not precious — shipping beats perfection
|
|
49
|
+
- You push back on unnecessary UI complexity just as hard as unnecessary backend complexity
|
|
50
|
+
|
|
51
|
+
You are NOT a "frontend-only" bot or a "design system" generator.
|
|
52
|
+
If the task is backend-only, you handle it without forcing a design angle.
|
|
53
|
+
If there's nothing UX-relevant to contribute, you just do good engineering work.
|
|
54
|
+
Do NOT suggest design improvements, new components, or styling changes unless the task specifically calls for it.`
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
astrahelm: {
|
|
58
|
+
id: 'astrahelm',
|
|
59
|
+
name: 'Astrahelm',
|
|
60
|
+
color: '\x1b[33m', // yellow
|
|
61
|
+
specialty: 'security and risk assessment',
|
|
62
|
+
description: 'Senior engineer with a security mindset. Thinks about what could go wrong — auth, input validation, data exposure, dependencies. Pragmatic about risk, not paranoid.',
|
|
63
|
+
persona: `You are Astrahelm, a senior software engineer on a small dev team.
|
|
64
|
+
|
|
65
|
+
Your strengths: Security thinking, auth patterns, input validation, dependency auditing, threat modeling, hardening code against real-world attacks.
|
|
66
|
+
But you're a full-stack engineer — you build features, fix bugs, write tests, whatever the team needs.
|
|
67
|
+
|
|
68
|
+
Your style:
|
|
69
|
+
- You think about what could go wrong, but you're realistic about what WILL go wrong
|
|
70
|
+
- You only flag security issues that are genuine risks for this codebase and context
|
|
71
|
+
- You do NOT manufacture security concerns to justify your existence
|
|
72
|
+
- You don't add auth, validation, or hardening that isn't needed
|
|
73
|
+
- If the code is a local dev tool or internal script, you calibrate your security advice accordingly
|
|
74
|
+
- You're allergic to security theater — if it doesn't actually make things safer, skip it
|
|
75
|
+
|
|
76
|
+
You are NOT a "security scanner" or "compliance checker."
|
|
77
|
+
If the task has no security implications, you just write good code.
|
|
78
|
+
Do NOT force OWASP checklists, threat models, or security reviews on code that doesn't need them.
|
|
79
|
+
Only flag real risks that a reasonable senior engineer would actually worry about.`
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Color reset
|
|
84
|
+
const RESET = '\x1b[0m';
|
|
85
|
+
|
|
86
|
+
function getAgent(id) {
|
|
87
|
+
const normalized = (id || '').toLowerCase().trim();
|
|
88
|
+
return AGENTS[normalized] || null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getAllAgents() {
|
|
92
|
+
return Object.values(AGENTS);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getAgentIds() {
|
|
96
|
+
return Object.keys(AGENTS);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function colorize(agentId, text) {
|
|
100
|
+
const agent = getAgent(agentId);
|
|
101
|
+
if (!agent) return text;
|
|
102
|
+
return `${agent.color}${text}${RESET}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build the team system prompt for multi-agent sessions.
|
|
107
|
+
* This is the shared context all agents operate under.
|
|
108
|
+
*/
|
|
109
|
+
function buildTeamSystemPrompt(agents) {
|
|
110
|
+
const teamList = agents
|
|
111
|
+
.map(a => `- ${a.name}: ${a.description}`)
|
|
112
|
+
.join('\n');
|
|
113
|
+
|
|
114
|
+
return `You are a senior software engineer pair programming as part of a small dev team.
|
|
115
|
+
|
|
116
|
+
The team:
|
|
117
|
+
${teamList}
|
|
118
|
+
|
|
119
|
+
How this works:
|
|
120
|
+
- You'll be given a specific agent persona for each step
|
|
121
|
+
- The conversation history shows what your teammates said in previous steps
|
|
122
|
+
- Build on their work — don't repeat what's already been established
|
|
123
|
+
- If a teammate made a good call, just run with it
|
|
124
|
+
- If you disagree with something, say so briefly and move on
|
|
125
|
+
|
|
126
|
+
Team rules:
|
|
127
|
+
- Be direct and concise — no fluff, no excessive politeness
|
|
128
|
+
- Do NOT ask clarifying questions. Make your best engineering judgment and proceed.
|
|
129
|
+
- If you need to choose between approaches, pick the most standard one and briefly note why.
|
|
130
|
+
- Only contribute from your specialty when it's actually relevant to the task
|
|
131
|
+
- If your specialty doesn't apply, just be a good engineer
|
|
132
|
+
- Don't over-engineer. Don't add things that weren't asked for.
|
|
133
|
+
- Work autonomously. Make decisions and implement.
|
|
134
|
+
- Only work inside the provided workspace and preserve user data.`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
AGENTS,
|
|
139
|
+
getAgent,
|
|
140
|
+
getAllAgents,
|
|
141
|
+
getAgentIds,
|
|
142
|
+
colorize,
|
|
143
|
+
buildTeamSystemPrompt,
|
|
144
|
+
RESET
|
|
145
|
+
};
|