s9n-devops-agent 2.0.18-dev.0 → 2.0.18-dev.11
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 +94 -143
- package/bin/cs-devops-agent +16 -20
- package/docs/RELEASE_NOTES.md +15 -0
- package/package.json +1 -1
- package/scripts/deploy-local.sh +100 -0
- package/src/agent-chat.js +299 -36
- package/src/credentials-manager.js +28 -6
- package/src/cs-devops-agent-worker.js +446 -87
- package/src/kora-skills.json +47 -0
- package/src/session-coordinator.js +499 -70
- package/src/setup-cs-devops-agent.js +298 -44
- package/src/ui-utils.js +1 -1
- package/start-devops-session.sh +4 -27
package/src/agent-chat.js
CHANGED
|
@@ -23,7 +23,7 @@ import readline from 'readline';
|
|
|
23
23
|
import Groq from 'groq-sdk';
|
|
24
24
|
import { fileURLToPath } from 'url';
|
|
25
25
|
import { dirname } from 'path';
|
|
26
|
-
import { spawn } from 'child_process';
|
|
26
|
+
import { spawn, execSync } from 'child_process';
|
|
27
27
|
import { credentialsManager } from './credentials-manager.js';
|
|
28
28
|
import HouseRulesManager from './house-rules-manager.js';
|
|
29
29
|
// We'll import SessionCoordinator dynamically to avoid circular deps if any
|
|
@@ -51,9 +51,14 @@ const CONFIG = {
|
|
|
51
51
|
|
|
52
52
|
class SmartAssistant {
|
|
53
53
|
constructor() {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
// Initialize Groq client lazily or with null if key is missing
|
|
55
|
+
const apiKey = process.env.GROQ_API_KEY || process.env.OPENAI_API_KEY;
|
|
56
|
+
|
|
57
|
+
if (apiKey) {
|
|
58
|
+
this.groq = new Groq({ apiKey });
|
|
59
|
+
} else {
|
|
60
|
+
this.groq = null; // Will be initialized in start()
|
|
61
|
+
}
|
|
57
62
|
|
|
58
63
|
this.history = [];
|
|
59
64
|
this.repoRoot = process.cwd();
|
|
@@ -99,10 +104,50 @@ class SmartAssistant {
|
|
|
99
104
|
description: "Check the status of active sessions and locks",
|
|
100
105
|
parameters: { type: "object", properties: {} }
|
|
101
106
|
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
type: "function",
|
|
110
|
+
function: {
|
|
111
|
+
name: "resume_session",
|
|
112
|
+
description: "Resume an existing or orphaned session",
|
|
113
|
+
parameters: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: {
|
|
116
|
+
sessionId: { type: "string", description: "The ID of the session to resume" },
|
|
117
|
+
taskName: { type: "string", description: "The task name to search for (optional)" }
|
|
118
|
+
},
|
|
119
|
+
required: ["sessionId"]
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
type: "function",
|
|
125
|
+
function: {
|
|
126
|
+
name: "recover_sessions",
|
|
127
|
+
description: "Scan for and recover orphaned sessions from existing worktrees",
|
|
128
|
+
parameters: { type: "object", properties: {} }
|
|
129
|
+
}
|
|
102
130
|
}
|
|
103
131
|
];
|
|
104
132
|
|
|
105
|
-
|
|
133
|
+
// Load skills definition
|
|
134
|
+
let skillsDef = null;
|
|
135
|
+
try {
|
|
136
|
+
const skillsPath = path.join(__dirname, 'kora-skills.json');
|
|
137
|
+
if (fs.existsSync(skillsPath)) {
|
|
138
|
+
skillsDef = JSON.parse(fs.readFileSync(skillsPath, 'utf8'));
|
|
139
|
+
}
|
|
140
|
+
} catch (e) {
|
|
141
|
+
// Fallback if file missing or invalid
|
|
142
|
+
console.error('Warning: Could not load kora-skills.json, using defaults.');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (skillsDef) {
|
|
146
|
+
// Build dynamic system prompt from skills definition
|
|
147
|
+
const allowedTopics = skillsDef.guardrails.allowed_topics.map(t => `- ${t}`).join('\n');
|
|
148
|
+
const disallowedTopics = skillsDef.guardrails.disallowed_topics.map(t => `- ${t}`).join('\n');
|
|
149
|
+
|
|
150
|
+
this.systemPrompt = `You are ${skillsDef.assistant_name}, the ${skillsDef.role}.
|
|
106
151
|
Your goal is to help developers follow the House Rules and Contract System while being helpful and efficient.
|
|
107
152
|
|
|
108
153
|
CONTEXT:
|
|
@@ -111,22 +156,81 @@ CONTEXT:
|
|
|
111
156
|
- Users need to create "Sessions" to do work.
|
|
112
157
|
- You can execute tools to help the user.
|
|
113
158
|
|
|
114
|
-
|
|
159
|
+
CAPABILITIES (ALLOWED):
|
|
160
|
+
${allowedTopics}
|
|
161
|
+
|
|
162
|
+
GUARDRAILS (STRICTLY PROHIBITED):
|
|
163
|
+
${disallowedTopics}
|
|
164
|
+
|
|
165
|
+
AVAILABLE TOOLS:
|
|
166
|
+
1. get_house_rules_summary - Read the project's rules.
|
|
167
|
+
2. list_contracts - Check what contract files exist.
|
|
168
|
+
3. check_session_status - See active work sessions.
|
|
169
|
+
4. start_session - Begin a new task.
|
|
170
|
+
5. resume_session - Resume an existing or orphaned session.
|
|
171
|
+
6. recover_sessions - Scan and restore lost session locks.
|
|
172
|
+
|
|
173
|
+
IMPORTANT INSTRUCTIONS:
|
|
174
|
+
- ONLY use the tools listed above. Do NOT invent new tools like "check_compliance" or "run_tests".
|
|
175
|
+
- If the user asks for something OUTSIDE your capabilities, you MUST reply with exactly this message:
|
|
176
|
+
"${skillsDef.guardrails.fallback_response}"
|
|
177
|
+
- Be concise but helpful.
|
|
178
|
+
- Identify yourself as "${skillsDef.assistant_name}".
|
|
179
|
+
- If the user asks about starting a task, ask for a clear task name if not provided.
|
|
180
|
+
- If the user asks about rules, summarize them from the actual files.
|
|
181
|
+
- If the user wants to resume work, use check_session_status first.
|
|
182
|
+
- If a session seems missing but worktree exists, use recover_sessions.
|
|
183
|
+
- Always prefer "Structured" organization for new code.
|
|
184
|
+
|
|
185
|
+
When you want to perform an action, use the available tools.`;
|
|
186
|
+
} else {
|
|
187
|
+
// Fallback static prompt
|
|
188
|
+
this.systemPrompt = `You are Kora, the Smart DevOps Assistant.
|
|
189
|
+
Your goal is to help developers follow the House Rules and Contract System while being helpful and efficient.
|
|
190
|
+
|
|
191
|
+
CONTEXT:
|
|
192
|
+
- You are running inside a "DevOps Agent" environment.
|
|
193
|
+
- The project follows a strict "Contract System" (API, DB, Features, etc.).
|
|
194
|
+
- Users need to create "Sessions" to do work.
|
|
195
|
+
- You can execute tools to help the user.
|
|
196
|
+
|
|
197
|
+
AVAILABLE TOOLS:
|
|
198
|
+
1. get_house_rules_summary - Read the project's rules.
|
|
199
|
+
2. list_contracts - Check what contract files exist.
|
|
200
|
+
3. check_session_status - See active work sessions.
|
|
201
|
+
4. start_session - Begin a new task.
|
|
202
|
+
5. resume_session - Resume an existing or orphaned session.
|
|
203
|
+
6. recover_sessions - Scan and restore lost session locks.
|
|
204
|
+
|
|
205
|
+
IMPORTANT INSTRUCTIONS:
|
|
206
|
+
- ONLY use the tools listed above. Do NOT invent new tools like "check_compliance" or "run_tests".
|
|
207
|
+
- If a user asks for something you can't do with a tool (like running tests), tell them you can't do it yet but they can run "npm test" themselves.
|
|
115
208
|
- Be concise but helpful.
|
|
116
209
|
- Identify yourself as "Kora".
|
|
117
210
|
- If the user asks about starting a task, ask for a clear task name if not provided.
|
|
118
211
|
- If the user asks about rules, summarize them from the actual files.
|
|
212
|
+
- If the user wants to resume work, use check_session_status first.
|
|
213
|
+
- If a session seems missing but worktree exists, use recover_sessions.
|
|
119
214
|
- Always prefer "Structured" organization for new code.
|
|
120
215
|
|
|
121
216
|
When you want to perform an action, use the available tools.`;
|
|
217
|
+
}
|
|
122
218
|
}
|
|
123
219
|
|
|
124
220
|
/**
|
|
125
221
|
* Initialize the chat session
|
|
126
222
|
*/
|
|
127
223
|
async start() {
|
|
224
|
+
// Ensure Groq client is initialized
|
|
225
|
+
if (!this.groq) {
|
|
226
|
+
const apiKey = credentialsManager.getGroqApiKey();
|
|
227
|
+
if (apiKey) {
|
|
228
|
+
this.groq = new Groq({ apiKey });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
128
232
|
// Check for Groq API Key
|
|
129
|
-
if (!credentialsManager.hasGroqApiKey()) {
|
|
233
|
+
if (!this.groq && !credentialsManager.hasGroqApiKey()) {
|
|
130
234
|
console.log('\n' + '='.repeat(60));
|
|
131
235
|
console.log(`${CONFIG.colors.yellow}⚠️ GROQ API KEY MISSING${CONFIG.colors.reset}`);
|
|
132
236
|
console.log('='.repeat(60));
|
|
@@ -171,6 +275,41 @@ When you want to perform an action, use the available tools.`;
|
|
|
171
275
|
console.log(`\n${CONFIG.colors.cyan}Hi! I'm Kora. How can I help you today?${CONFIG.colors.reset}`);
|
|
172
276
|
console.log(`${CONFIG.colors.dim}(Try: "Start a new task for login", "Explain house rules", "Check contracts")${CONFIG.colors.reset}\n`);
|
|
173
277
|
|
|
278
|
+
// Check for command line arguments
|
|
279
|
+
const args = process.argv.slice(2);
|
|
280
|
+
const taskIndex = args.indexOf('--task') !== -1 ? args.indexOf('--task') : args.indexOf('-t');
|
|
281
|
+
|
|
282
|
+
if (taskIndex !== -1 && args[taskIndex + 1]) {
|
|
283
|
+
const taskName = args[taskIndex + 1];
|
|
284
|
+
console.log(`\n${CONFIG.colors.cyan}Auto-starting session for task: ${taskName}${CONFIG.colors.reset}\n`);
|
|
285
|
+
await this.startSession({ taskName });
|
|
286
|
+
return; // Exit after session? Or continue chat? Usually session start spawns child and returns.
|
|
287
|
+
// startSession re-initializes readline after child exit, so we can continue chat.
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Check for auto-resume
|
|
291
|
+
const resumeIndex = args.indexOf('resume');
|
|
292
|
+
const sessionIdIndex = args.indexOf('--session-id');
|
|
293
|
+
|
|
294
|
+
if (resumeIndex !== -1 || sessionIdIndex !== -1) {
|
|
295
|
+
let sessionId = null;
|
|
296
|
+
let taskName = null;
|
|
297
|
+
|
|
298
|
+
if (sessionIdIndex !== -1 && args[sessionIdIndex + 1]) {
|
|
299
|
+
sessionId = args[sessionIdIndex + 1];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (taskIndex !== -1 && args[taskIndex + 1]) {
|
|
303
|
+
taskName = args[taskIndex + 1];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (sessionId || taskName) {
|
|
307
|
+
console.log(`\n${CONFIG.colors.cyan}Auto-resuming session...${CONFIG.colors.reset}\n`);
|
|
308
|
+
await this.resumeSession({ sessionId, taskName });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
174
313
|
this.startReadline();
|
|
175
314
|
}
|
|
176
315
|
|
|
@@ -292,6 +431,12 @@ When you want to perform an action, use the available tools.`;
|
|
|
292
431
|
case 'start_session':
|
|
293
432
|
result = await this.startSession(args);
|
|
294
433
|
break;
|
|
434
|
+
case 'resume_session':
|
|
435
|
+
result = await this.resumeSession(args);
|
|
436
|
+
break;
|
|
437
|
+
case 'recover_sessions':
|
|
438
|
+
result = await this.recoverSessions();
|
|
439
|
+
break;
|
|
295
440
|
default:
|
|
296
441
|
result = JSON.stringify({ error: "Unknown tool" });
|
|
297
442
|
}
|
|
@@ -351,15 +496,50 @@ When you want to perform an action, use the available tools.`;
|
|
|
351
496
|
|
|
352
497
|
async listContracts() {
|
|
353
498
|
const contractsDir = path.join(this.repoRoot, 'House_Rules_Contracts');
|
|
354
|
-
|
|
355
|
-
|
|
499
|
+
const centralExists = fs.existsSync(contractsDir);
|
|
500
|
+
|
|
501
|
+
// Recursive search for contracts (similar to setup script)
|
|
502
|
+
const findCommand = `find "${this.repoRoot}" -type f \\( -iname "*CONTRACT*.md" -o -iname "*CONTRACT*.json" \\) -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/local_deploy/*"`;
|
|
503
|
+
|
|
504
|
+
let allFiles = [];
|
|
505
|
+
try {
|
|
506
|
+
const output = execSync(findCommand, { encoding: 'utf8' }).trim();
|
|
507
|
+
allFiles = output.split('\n').filter(Boolean);
|
|
508
|
+
} catch (e) {
|
|
509
|
+
// Find failed or no files
|
|
356
510
|
}
|
|
357
511
|
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
512
|
+
const requiredTypes = [
|
|
513
|
+
'FEATURES_CONTRACT.md', 'API_CONTRACT.md', 'DATABASE_SCHEMA_CONTRACT.md',
|
|
514
|
+
'SQL_CONTRACT.json', 'THIRD_PARTY_INTEGRATIONS.md', 'INFRA_CONTRACT.md'
|
|
515
|
+
];
|
|
516
|
+
|
|
517
|
+
const status = {};
|
|
518
|
+
let scatteredCount = 0;
|
|
519
|
+
|
|
520
|
+
requiredTypes.forEach(type => {
|
|
521
|
+
// Check if in central folder
|
|
522
|
+
const isCentral = fs.existsSync(path.join(contractsDir, type));
|
|
523
|
+
|
|
524
|
+
// Check if anywhere in repo
|
|
525
|
+
const found = allFiles.filter(f => path.basename(f).toUpperCase() === type || path.basename(f).toUpperCase().includes(type.split('.')[0]));
|
|
526
|
+
|
|
527
|
+
status[type] = {
|
|
528
|
+
central: isCentral,
|
|
529
|
+
foundCount: found.length,
|
|
530
|
+
locations: found.map(f => path.relative(this.repoRoot, f))
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
if (!isCentral && found.length > 0) scatteredCount++;
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
return JSON.stringify({
|
|
537
|
+
centralFolderExists: centralExists,
|
|
538
|
+
scatteredContractsCount: scatteredCount,
|
|
539
|
+
details: status,
|
|
540
|
+
message: scatteredCount > 0
|
|
541
|
+
? "Contracts found scattered in repository. Recommend running 'npm run setup' to consolidate."
|
|
542
|
+
: (centralExists ? "Contracts found in central folder." : "No contracts found.")
|
|
363
543
|
});
|
|
364
544
|
}
|
|
365
545
|
|
|
@@ -391,55 +571,138 @@ When you want to perform an action, use the available tools.`;
|
|
|
391
571
|
}
|
|
392
572
|
|
|
393
573
|
async startSession(args) {
|
|
394
|
-
const taskName = args
|
|
574
|
+
const { taskName, description } = args;
|
|
395
575
|
|
|
396
|
-
console.log(`${CONFIG.colors.magenta}Kora > ${CONFIG.colors.reset}Starting
|
|
576
|
+
console.log(`${CONFIG.colors.magenta}Kora > ${CONFIG.colors.reset}Starting session for task: ${taskName}...`);
|
|
397
577
|
|
|
398
|
-
// Close readline interface
|
|
578
|
+
// Close readline interface
|
|
399
579
|
if (this.rl) {
|
|
400
580
|
this.rl.close();
|
|
401
581
|
this.rl = null;
|
|
402
582
|
}
|
|
403
583
|
|
|
404
|
-
// We need to run the session coordinator interactively
|
|
405
|
-
// We'll use the 'create-and-start' command to jump straight to the task
|
|
406
584
|
const scriptPath = path.join(__dirname, 'session-coordinator.js');
|
|
407
585
|
|
|
408
586
|
return new Promise((resolve, reject) => {
|
|
409
|
-
// Use 'inherit' for stdio to allow interactive input/output
|
|
410
587
|
const child = spawn('node', [scriptPath, 'create-and-start', '--task', taskName], {
|
|
411
588
|
stdio: 'inherit',
|
|
412
589
|
cwd: this.repoRoot
|
|
413
590
|
});
|
|
414
591
|
|
|
415
592
|
child.on('close', (code) => {
|
|
416
|
-
// Re-initialize readline interface after child process exits
|
|
417
593
|
this.startReadline();
|
|
594
|
+
if (code === 0) {
|
|
595
|
+
resolve(JSON.stringify({ success: true, message: "Session started successfully." }));
|
|
596
|
+
} else {
|
|
597
|
+
resolve(JSON.stringify({ success: false, message: `Session process exited with code ${code}.` }));
|
|
598
|
+
}
|
|
599
|
+
console.log(`\n${CONFIG.colors.cyan}Welcome back to Kora!${CONFIG.colors.reset}`);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
child.on('error', (err) => {
|
|
603
|
+
this.startReadline();
|
|
604
|
+
resolve(JSON.stringify({ success: false, error: err.message }));
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
}
|
|
418
608
|
|
|
609
|
+
async recoverSessions() {
|
|
610
|
+
console.log(`${CONFIG.colors.magenta}Kora > ${CONFIG.colors.reset}Scanning for orphaned sessions to recover...`);
|
|
611
|
+
|
|
612
|
+
// Close readline interface
|
|
613
|
+
if (this.rl) {
|
|
614
|
+
this.rl.close();
|
|
615
|
+
this.rl = null;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const scriptPath = path.join(__dirname, 'session-coordinator.js');
|
|
619
|
+
|
|
620
|
+
return new Promise((resolve, reject) => {
|
|
621
|
+
const child = spawn('node', [scriptPath, 'recover'], {
|
|
622
|
+
stdio: 'inherit',
|
|
623
|
+
cwd: this.repoRoot
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
child.on('close', (code) => {
|
|
627
|
+
this.startReadline();
|
|
419
628
|
if (code === 0) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
629
|
+
// Instead of generic message, suggest checking status
|
|
630
|
+
resolve(JSON.stringify({
|
|
631
|
+
success: true,
|
|
632
|
+
message: "Recovery scan complete. Please run 'check_session_status' to see recovered sessions."
|
|
423
633
|
}));
|
|
424
634
|
} else {
|
|
425
|
-
resolve(JSON.stringify({
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
635
|
+
resolve(JSON.stringify({ success: false, message: `Recovery process exited with code ${code}.` }));
|
|
636
|
+
}
|
|
637
|
+
// Don't print "Welcome back" here to keep flow cleaner
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
child.on('error', (err) => {
|
|
641
|
+
this.startReadline();
|
|
642
|
+
resolve(JSON.stringify({ success: false, error: err.message }));
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
async resumeSession(args) {
|
|
647
|
+
const { sessionId, taskName } = args;
|
|
648
|
+
|
|
649
|
+
console.log(`${CONFIG.colors.magenta}Kora > ${CONFIG.colors.reset}Resuming session: ${sessionId || taskName}...`);
|
|
650
|
+
|
|
651
|
+
// Close readline interface
|
|
652
|
+
if (this.rl) {
|
|
653
|
+
this.rl.close();
|
|
654
|
+
this.rl = null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const scriptPath = path.join(__dirname, 'session-coordinator.js');
|
|
658
|
+
|
|
659
|
+
// Construct arguments for session coordinator
|
|
660
|
+
// We use the 'resume' command if we have an ID, or create-and-start with task if we're fuzzy matching
|
|
661
|
+
// But actually session-coordinator doesn't have a direct 'resume' command exposed easily via CLI args
|
|
662
|
+
// that jumps straight to monitoring without prompts, EXCEPT via the way createSession handles existing sessions.
|
|
663
|
+
// However, createSession with --task will prompt.
|
|
664
|
+
// Let's use a new approach: pass --resume-session-id if we have it.
|
|
665
|
+
|
|
666
|
+
// Wait, session-coordinator.js CLI handling (which I can't fully see but I saw 'create' and 'list')
|
|
667
|
+
// I need to check how to invoke resume.
|
|
668
|
+
// Looking at session-coordinator.js (which I read), it has 'requestSession' and 'createSession'.
|
|
669
|
+
// It doesn't seem to have a direct CLI command for 'resume' that takes an ID.
|
|
670
|
+
// However, 'create-and-start' (implied by startSession usage) might support it?
|
|
671
|
+
// In startSession: [scriptPath, 'create-and-start', '--task', taskName, '--skip-setup', '--skip-update']
|
|
672
|
+
|
|
673
|
+
// If I use 'create-and-start' with the SAME task name, it triggers the "Found existing session" logic
|
|
674
|
+
// but that logic is interactive (prompts Y/n).
|
|
675
|
+
|
|
676
|
+
// I should probably add a CLI command to session-coordinator.js to resume by ID non-interactively,
|
|
677
|
+
// OR just use the 'worker' command directly if I know the worktree?
|
|
678
|
+
// But the coordinator handles setting up the environment.
|
|
679
|
+
|
|
680
|
+
// Let's assume for now we can use a new 'resume' command in session-coordinator.js
|
|
681
|
+
// I will need to implement that in session-coordinator.js as well.
|
|
682
|
+
// But first let's implement the caller here.
|
|
683
|
+
|
|
684
|
+
const cmdArgs = ['resume', '--session-id', sessionId];
|
|
685
|
+
if (taskName) cmdArgs.push('--task', taskName);
|
|
686
|
+
|
|
687
|
+
return new Promise((resolve, reject) => {
|
|
688
|
+
const child = spawn('node', [scriptPath, ...cmdArgs], {
|
|
689
|
+
stdio: 'inherit',
|
|
690
|
+
cwd: this.repoRoot
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
child.on('close', (code) => {
|
|
694
|
+
this.startReadline();
|
|
695
|
+
if (code === 0) {
|
|
696
|
+
resolve(JSON.stringify({ success: true, message: "Session resumed successfully." }));
|
|
697
|
+
} else {
|
|
698
|
+
resolve(JSON.stringify({ success: false, message: `Session process exited with code ${code}.` }));
|
|
429
699
|
}
|
|
430
|
-
|
|
431
|
-
// Resume the chat interface after the child process exits
|
|
432
700
|
console.log(`\n${CONFIG.colors.cyan}Welcome back to Kora!${CONFIG.colors.reset}`);
|
|
433
701
|
});
|
|
434
702
|
|
|
435
703
|
child.on('error', (err) => {
|
|
436
|
-
// Re-initialize readline interface on error
|
|
437
704
|
this.startReadline();
|
|
438
|
-
|
|
439
|
-
resolve(JSON.stringify({
|
|
440
|
-
success: false,
|
|
441
|
-
error: err.message
|
|
442
|
-
}));
|
|
705
|
+
resolve(JSON.stringify({ success: false, error: err.message }));
|
|
443
706
|
});
|
|
444
707
|
});
|
|
445
708
|
}
|
|
@@ -3,11 +3,15 @@ import path from 'path';
|
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { dirname } from 'path';
|
|
5
5
|
|
|
6
|
+
import os from 'os';
|
|
7
|
+
|
|
6
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
9
|
const __dirname = dirname(__filename);
|
|
8
|
-
const rootDir = path.join(__dirname, '..');
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
// Use home directory for persistent storage across package updates
|
|
12
|
+
const HOME_DIR = os.homedir();
|
|
13
|
+
const CONFIG_DIR = path.join(HOME_DIR, '.devops-agent');
|
|
14
|
+
const CREDENTIALS_PATH = process.env.DEVOPS_CREDENTIALS_PATH || path.join(CONFIG_DIR, 'credentials.json');
|
|
11
15
|
|
|
12
16
|
// Simple obfuscation to prevent casual shoulder surfing
|
|
13
17
|
// NOTE: This is NOT strong encryption. In a production environment with sensitive keys,
|
|
@@ -38,15 +42,33 @@ export class CredentialsManager {
|
|
|
38
42
|
console.error('Failed to load credentials:', error.message);
|
|
39
43
|
this.credentials = {};
|
|
40
44
|
}
|
|
45
|
+
} else {
|
|
46
|
+
// Migration: Check for old local_deploy location
|
|
47
|
+
const oldPath = path.join(__dirname, '..', 'local_deploy', 'credentials.json');
|
|
48
|
+
if (fs.existsSync(oldPath)) {
|
|
49
|
+
try {
|
|
50
|
+
const rawData = fs.readFileSync(oldPath, 'utf8');
|
|
51
|
+
const data = JSON.parse(rawData);
|
|
52
|
+
// Deobfuscate sensitive values
|
|
53
|
+
if (data.groqApiKey) {
|
|
54
|
+
data.groqApiKey = deobfuscate(data.groqApiKey);
|
|
55
|
+
}
|
|
56
|
+
this.credentials = data;
|
|
57
|
+
// Save to new location immediately
|
|
58
|
+
this.save();
|
|
59
|
+
} catch (e) {
|
|
60
|
+
// Ignore migration errors
|
|
61
|
+
}
|
|
62
|
+
}
|
|
41
63
|
}
|
|
42
64
|
}
|
|
43
65
|
|
|
44
66
|
save() {
|
|
45
67
|
try {
|
|
46
|
-
// Ensure
|
|
47
|
-
const
|
|
48
|
-
if (!fs.existsSync(
|
|
49
|
-
fs.mkdirSync(
|
|
68
|
+
// Ensure config dir exists
|
|
69
|
+
const configDir = path.dirname(CREDENTIALS_PATH);
|
|
70
|
+
if (!fs.existsSync(configDir)) {
|
|
71
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
50
72
|
}
|
|
51
73
|
|
|
52
74
|
// Clone and obfuscate
|