s9n-devops-agent 2.0.18-dev.1 → 2.0.18-dev.12

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.
@@ -28,6 +28,7 @@ import { execSync } from 'child_process';
28
28
  import { fileURLToPath } from 'url';
29
29
  import { dirname } from 'path';
30
30
  import { credentialsManager } from './credentials-manager.js';
31
+ import { SessionCoordinator } from './session-coordinator.js';
31
32
  import {
32
33
  colors,
33
34
  status,
@@ -41,6 +42,7 @@ import {
41
42
  error as errorMsg,
42
43
  confirm,
43
44
  prompt as uiPrompt,
45
+ choose,
44
46
  progressStep,
45
47
  drawSection
46
48
  } from './ui-utils.js';
@@ -149,20 +151,165 @@ This structure is compatible with the DevOps Agent's automation tools.
149
151
  return missingFolders;
150
152
  }
151
153
 
152
- function checkContractsExist(projectRoot) {
153
- const contractsDir = path.join(projectRoot, 'House_Rules_Contracts');
154
- if (!fs.existsSync(contractsDir)) return false;
155
-
156
- const requiredContracts = [
157
- 'FEATURES_CONTRACT.md',
158
- 'API_CONTRACT.md',
159
- 'DATABASE_SCHEMA_CONTRACT.md',
160
- 'SQL_CONTRACT.json',
161
- 'THIRD_PARTY_INTEGRATIONS.md',
162
- 'INFRA_CONTRACT.md'
163
- ];
164
-
165
- return requiredContracts.every(file => fs.existsSync(path.join(contractsDir, file)));
154
+ async function checkContractsExist(projectRoot) {
155
+ // Search recursively for contract folders and files
156
+ try {
157
+ const requiredContracts = [
158
+ 'FEATURES_CONTRACT.md',
159
+ 'API_CONTRACT.md',
160
+ 'DATABASE_SCHEMA_CONTRACT.md',
161
+ 'SQL_CONTRACT.json',
162
+ 'THIRD_PARTY_INTEGRATIONS.md',
163
+ 'INFRA_CONTRACT.md'
164
+ ];
165
+
166
+ // Map to hold found files for each type
167
+ const contractMap = {};
168
+ requiredContracts.forEach(c => contractMap[c] = []);
169
+
170
+ // Find all files that look like contracts
171
+ // We look for files containing "CONTRACT" in the name, excluding typical ignores
172
+ // Use -iname for case-insensitive matching
173
+ const findCommand = `find "${projectRoot}" -type f \\( -iname "*CONTRACT*.md" -o -iname "*CONTRACT*.json" \\) -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/local_deploy/*"`;
174
+
175
+ let files = [];
176
+ try {
177
+ const output = execSync(findCommand, { encoding: 'utf8' }).trim();
178
+ files = output.split('\n').filter(Boolean);
179
+ } catch (e) {
180
+ // find might fail if no matches or other issues, just treat as empty
181
+ }
182
+
183
+ // Categorize found files
184
+ for (const file of files) {
185
+ const basename = path.basename(file).toUpperCase();
186
+
187
+ // Skip files in the target directory itself (House_Rules_Contracts) to avoid self-merging if we run this multiple times
188
+ // actually we SHOULD include them to see if we have them, but valid if we are merging duplicates from elsewhere
189
+
190
+ let matched = false;
191
+
192
+ if (basename.includes('FEATURE')) contractMap['FEATURES_CONTRACT.md'].push(file);
193
+ else if (basename.includes('API')) contractMap['API_CONTRACT.md'].push(file);
194
+ else if (basename.includes('DATABASE') || basename.includes('SCHEMA')) contractMap['DATABASE_SCHEMA_CONTRACT.md'].push(file);
195
+ else if (basename.includes('SQL')) contractMap['SQL_CONTRACT.json'].push(file);
196
+ else if (basename.includes('INFRA')) contractMap['INFRA_CONTRACT.md'].push(file);
197
+ else if (basename.includes('THIRD') || basename.includes('INTEGRATION')) contractMap['THIRD_PARTY_INTEGRATIONS.md'].push(file);
198
+ else {
199
+ // Fallback or ignore
200
+ }
201
+ }
202
+
203
+ const targetDir = path.join(projectRoot, 'House_Rules_Contracts');
204
+ let hasChanges = false;
205
+
206
+ // Process each contract type
207
+ let scatteredCount = 0;
208
+ let centralCount = 0;
209
+ let missingCount = 0;
210
+
211
+ // Print summary header
212
+ console.log(`\n${colors.bright}Contract Search Results:${colors.reset}`);
213
+
214
+ for (const [type, foundFiles] of Object.entries(contractMap)) {
215
+ // Filter out unique paths (resolve them)
216
+ const uniqueFiles = [...new Set(foundFiles.map(f => path.resolve(f)))];
217
+ const isCentral = fs.existsSync(path.join(targetDir, type));
218
+
219
+ let statusIcon = '';
220
+ let statusText = '';
221
+
222
+ if (isCentral) {
223
+ statusIcon = colors.green + '✓' + colors.reset;
224
+ statusText = colors.green + 'Present (Central)' + colors.reset;
225
+ centralCount++;
226
+ } else if (uniqueFiles.length > 0) {
227
+ statusIcon = colors.yellow + '⚠️' + colors.reset;
228
+ statusText = colors.yellow + `Found ${uniqueFiles.length} scattered file(s)` + colors.reset;
229
+ scatteredCount++;
230
+ } else {
231
+ statusIcon = colors.red + '✗' + colors.reset;
232
+ statusText = colors.red + 'Missing' + colors.reset;
233
+ missingCount++;
234
+ }
235
+
236
+ console.log(` ${statusIcon} ${type.padEnd(30)} : ${statusText}`);
237
+
238
+ if (uniqueFiles.length > 0 && !isCentral) {
239
+ // Show locations for scattered files
240
+ uniqueFiles.forEach(f => console.log(` ${colors.dim}- ${path.relative(projectRoot, f)}${colors.reset}`));
241
+
242
+ const shouldMerge = await confirm(` Merge/Copy ${uniqueFiles.length} file(s) to House_Rules_Contracts/${type}?`, true);
243
+
244
+ if (shouldMerge) {
245
+ ensureDirectoryExists(targetDir);
246
+ const targetPath = path.join(targetDir, type);
247
+
248
+ let mergedContent = '';
249
+ // Handle JSON vs MD
250
+ if (type.endsWith('.json')) {
251
+ // For JSON, we try to merge arrays/objects or just list them
252
+ const mergedJson = [];
253
+ for (const file of uniqueFiles) {
254
+ try {
255
+ const content = JSON.parse(fs.readFileSync(file, 'utf8'));
256
+ mergedJson.push({ source: path.relative(projectRoot, file), content });
257
+ } catch (e) {
258
+ log.warn(`Skipping invalid JSON in ${path.basename(file)}`);
259
+ }
260
+ }
261
+ mergedContent = JSON.stringify(mergedJson, null, 2);
262
+ } else {
263
+ // Markdown
264
+ mergedContent = `# Merged ${type}\n\nGenerated on ${new Date().toISOString()}\n\n`;
265
+ for (const file of uniqueFiles) {
266
+ const content = fs.readFileSync(file, 'utf8');
267
+ mergedContent += `\n<!-- SOURCE: ${path.relative(projectRoot, file)} -->\n`;
268
+ mergedContent += `## Source: ${path.basename(file)}\n(Path: ${path.relative(projectRoot, file)})\n\n`;
269
+ mergedContent += `${content}\n\n---\n`;
270
+ }
271
+ }
272
+
273
+ fs.writeFileSync(targetPath, mergedContent);
274
+ log.success(` Merged into ${path.relative(projectRoot, targetPath)}`);
275
+ hasChanges = true;
276
+ centralCount++; // Now it's central
277
+ scatteredCount--;
278
+ }
279
+ }
280
+ }
281
+
282
+ console.log(); // Spacing
283
+
284
+ // Final check logic
285
+ const result = {
286
+ missingCount,
287
+ scatteredCount,
288
+ centralCount,
289
+ valid: missingCount === 0
290
+ };
291
+
292
+ if (missingCount === 0) {
293
+ if (hasChanges) log.success('Contracts consolidated and verified.');
294
+ else log.info('All required contracts are present.');
295
+ return result;
296
+ }
297
+
298
+ if (scatteredCount > 0) {
299
+ log.warn(`${scatteredCount} contract types exist but are not centralized.`);
300
+ log.warn('We recommend merging them, but you can proceed without it.');
301
+ }
302
+
303
+ if (missingCount > 0) {
304
+ log.warn(`${missingCount} contract types are completely missing.`);
305
+ }
306
+
307
+ return result;
308
+
309
+ } catch (error) {
310
+ log.warn(`Error searching for contracts: ${error.message}`);
311
+ return { missingCount: 1, scatteredCount: 0, valid: false }; // Fallback
312
+ }
166
313
  }
167
314
 
168
315
  async function generateContracts(projectRoot) {
@@ -834,30 +981,50 @@ async function setupEnvFile(projectRoot) {
834
981
  log.info('Creating .env file');
835
982
  }
836
983
 
837
- // Check for OPENAI_API_KEY
984
+ // Check if OPENAI_API_KEY is already present in memory (from credentials.json)
985
+ const existingKey = credentialsManager.getGroqApiKey();
986
+
987
+ // Check for OPENAI_API_KEY in .env content
838
988
  if (!envContent.includes('OPENAI_API_KEY=')) {
839
- console.log();
840
- explain(`
841
- ${colors.bright}Groq API Key Setup${colors.reset}
842
- The contract automation features use Groq LLM (via OpenAI compatibility).
843
- You can enter your API key now, or set it later in the .env file.
844
- `);
845
-
846
- const apiKey = await prompt('Enter Groq API Key (leave empty to skip)');
847
-
848
- if (apiKey) {
989
+ if (existingKey) {
990
+ log.info('Found existing Groq API Key in credentials store.');
849
991
  const newLine = envContent.endsWith('\n') || envContent === '' ? '' : '\n';
850
- envContent += `${newLine}# Groq API Key for Contract Automation\nOPENAI_API_KEY=${apiKey}\n`;
992
+ envContent += `${newLine}# Groq API Key for Contract Automation\nOPENAI_API_KEY=${existingKey}\n`;
851
993
  fs.writeFileSync(envPath, envContent);
852
- log.success('Added OPENAI_API_KEY to .env');
994
+ log.success('Restored OPENAI_API_KEY to .env');
853
995
  } else {
854
- log.warn('Skipped Groq API Key. Contract automation features may not work.');
855
- if (!fs.existsSync(envPath)) {
856
- fs.writeFileSync(envPath, '# Environment Variables\n');
996
+ console.log();
997
+ explain(`
998
+ ${colors.bright}Groq API Key Setup${colors.reset}
999
+ The contract automation features use Groq LLM (via OpenAI compatibility).
1000
+ You can enter your API key now, or set it later in the .env file.
1001
+ `);
1002
+
1003
+ const apiKey = await prompt('Enter Groq API Key (leave empty to skip)');
1004
+
1005
+ if (apiKey) {
1006
+ const newLine = envContent.endsWith('\n') || envContent === '' ? '' : '\n';
1007
+ envContent += `${newLine}# Groq API Key for Contract Automation\nOPENAI_API_KEY=${apiKey}\n`;
1008
+ fs.writeFileSync(envPath, envContent);
1009
+
1010
+ // Also save to credentials manager for persistence across updates
1011
+ credentialsManager.setGroqApiKey(apiKey);
1012
+
1013
+ log.success('Added OPENAI_API_KEY to .env');
1014
+ } else {
1015
+ log.warn('Skipped Groq API Key. Contract automation features may not work.');
1016
+ if (!fs.existsSync(envPath)) {
1017
+ fs.writeFileSync(envPath, '# Environment Variables\n');
1018
+ }
857
1019
  }
858
1020
  }
859
1021
  } else {
860
1022
  log.info('OPENAI_API_KEY is already configured in .env');
1023
+ // Ensure it's backed up in credentials manager if it exists in .env
1024
+ const match = envContent.match(/OPENAI_API_KEY=(.+)/);
1025
+ if (match && match[1] && !existingKey) {
1026
+ credentialsManager.setGroqApiKey(match[1].trim());
1027
+ }
861
1028
  }
862
1029
  }
863
1030
 
@@ -950,17 +1117,15 @@ ${colors.dim}This takes about 2 minutes.${colors.reset}
950
1117
  const projectRoot = providedRoot ? path.resolve(providedRoot) : findProjectRoot();
951
1118
  log.info(`Project root: ${projectRoot}`);
952
1119
 
953
- // Ensure ScriptCS_DevOpsAgent directory exists
954
- // const scriptsDir = path.join(projectRoot, 'ScriptCS_DevOpsAgent');
955
- // if (!fs.existsSync(scriptsDir)) {
956
- // log.warn('ScriptCS_DevOpsAgent folder not found. Assuming global install or custom setup.');
957
- // }
1120
+ // Initialize coordinator to access settings
1121
+ const coordinator = new SessionCoordinator();
1122
+ const currentSettings = coordinator.loadSettings();
958
1123
 
959
1124
  // Get developer initials
960
1125
  console.log();
961
1126
  sectionTitle('Developer Identification');
962
1127
 
963
- let initials = providedInitials;
1128
+ let initials = providedInitials || currentSettings.developerInitials;
964
1129
 
965
1130
  if (!initials) {
966
1131
  explain(`
@@ -995,6 +1160,7 @@ ${colors.bright}How:${colors.reset} Creates branches like dev_abc_2025-10-31
995
1160
  sectionTitle('Primary AI Assistant');
996
1161
 
997
1162
  let agentName = providedAgent;
1163
+ const defaultAgent = currentSettings.preferences?.primaryAgent || 'Claude';
998
1164
 
999
1165
  if (!agentName) {
1000
1166
  explain(`
@@ -1003,16 +1169,52 @@ ${colors.bright}Why:${colors.reset} Customizes file names (e.g., .warp-commit-ms
1003
1169
  `);
1004
1170
 
1005
1171
  if (skipPrompts) {
1006
- agentName = 'Claude'; // Default if skipping prompts
1172
+ agentName = defaultAgent; // Default if skipping prompts
1007
1173
  } else {
1008
- agentName = await prompt('Primary AI Assistant? [Claude]');
1009
- agentName = agentName.trim() || 'Claude';
1174
+ agentName = await prompt(`Primary AI Assistant? [${defaultAgent}]`, defaultAgent);
1175
+ agentName = agentName.trim() || defaultAgent;
1010
1176
  }
1011
1177
  }
1012
1178
 
1013
1179
  success(`Using assistant: ${colors.cyan}${agentName}${colors.reset}`);
1014
1180
  console.log();
1015
1181
 
1182
+ // Ask to save settings if changed or not saved
1183
+ if (!skipPrompts && (initials !== currentSettings.developerInitials || agentName !== currentSettings.preferences?.primaryAgent)) {
1184
+ console.log();
1185
+ sectionTitle('Save Preferences');
1186
+ explain(`
1187
+ ${colors.bright}Save these settings for future sessions?${colors.reset}
1188
+ • ${colors.bright}Global:${colors.reset} Saves to ~/.devops-agent/settings.json (applies to all projects)
1189
+ • ${colors.bright}Project:${colors.reset} Saves to local_deploy/project-settings.json (this project only)
1190
+ `);
1191
+
1192
+ const saveChoice = await choose('Where should we save these settings?', [
1193
+ 'Global (User Profile)',
1194
+ 'Project (Local Only)',
1195
+ 'Do not save'
1196
+ ], { defaultChoice: '1' });
1197
+
1198
+ if (saveChoice === 0) {
1199
+ // Global
1200
+ const globalSettings = coordinator.loadGlobalSettings();
1201
+ globalSettings.developerInitials = initials;
1202
+ globalSettings.preferences = globalSettings.preferences || {};
1203
+ globalSettings.preferences.primaryAgent = agentName;
1204
+ globalSettings.configured = true;
1205
+ coordinator.saveGlobalSettings(globalSettings);
1206
+ log.success('Settings saved globally.');
1207
+ } else if (saveChoice === 1) {
1208
+ // Project
1209
+ const projectSettings = coordinator.loadProjectSettings();
1210
+ projectSettings.preferences = projectSettings.preferences || {};
1211
+ projectSettings.developerInitials = initials; // Allow project overrides
1212
+ projectSettings.preferences.primaryAgent = agentName;
1213
+ coordinator.saveProjectSettings(projectSettings);
1214
+ log.success('Settings saved to project.');
1215
+ }
1216
+ }
1217
+
1016
1218
  // Groq API Key Setup
1017
1219
  sectionTitle('Groq API Key (Contract Automation)');
1018
1220
 
@@ -1093,16 +1295,23 @@ ${colors.bright}Security:${colors.reset} Stored locally in ${colors.yellow}local
1093
1295
  }
1094
1296
 
1095
1297
  // Check for contracts
1096
- if (!checkContractsExist(projectRoot)) {
1298
+ const contractStatus = await checkContractsExist(projectRoot);
1299
+
1300
+ if (contractStatus.missingCount > 0) {
1097
1301
  log.header();
1098
1302
  log.title('📜 Contract Files Missing');
1099
1303
 
1304
+ console.log(`${colors.red}Found ${contractStatus.missingCount} missing contract types.${colors.reset}`);
1305
+ if (contractStatus.scatteredCount > 0) {
1306
+ console.log(`${colors.yellow}Note: ${contractStatus.scatteredCount} contract types were found but are scattered/unmerged.${colors.reset}`);
1307
+ }
1308
+
1100
1309
  if (!skipPrompts) {
1101
1310
  explain(`
1102
1311
  ${colors.bright}Contract System:${colors.reset}
1103
1312
  This project uses a Contract System to coordinate multiple AI agents.
1104
- It seems like this is a fresh setup or contracts are missing.
1105
- We can scan your codebase and generate them now.
1313
+ Since required contracts are missing, we should generate them to ensure
1314
+ all agents understand the project structure and rules.
1106
1315
  `);
1107
1316
  }
1108
1317
 
@@ -1110,6 +1319,10 @@ We can scan your codebase and generate them now.
1110
1319
  if (shouldGenerate) {
1111
1320
  await generateContracts(projectRoot);
1112
1321
  }
1322
+ } else if (contractStatus.scatteredCount > 0) {
1323
+ // All present, but some scattered. The checkContractsExist function already asked to merge.
1324
+ // We don't need to do anything else here.
1325
+ log.info('All contracts present (some were found in scattered locations).');
1113
1326
  }
1114
1327
 
1115
1328
  // Run setup steps
@@ -1130,6 +1343,49 @@ We can scan your codebase and generate them now.
1130
1343
  log.success('Created .env file');
1131
1344
  }
1132
1345
  }
1346
+
1347
+ // Initialize SessionCoordinator for versioning and house rules setup
1348
+ // const coordinator = new SessionCoordinator(); // Already initialized at top
1349
+
1350
+ // Ensure House Rules are set up
1351
+ if (!skipPrompts) {
1352
+ log.header();
1353
+ log.title('🏠 House Rules Setup');
1354
+ await coordinator.ensureHouseRulesSetup();
1355
+ } else {
1356
+ // In non-interactive mode, ensure defaults if missing
1357
+ // ensureHouseRulesSetup has internal logic, but we might want to skip the prompt
1358
+ // Since we can't easily force defaults without modifying coordinator, we'll skip for now
1359
+ // or we could check if it exists and warn
1360
+ if (!fs.existsSync(path.join(projectRoot, 'houserules.md'))) {
1361
+ log.warn('House rules missing. Run setup interactively to configure folder structure.');
1362
+ }
1363
+ }
1364
+
1365
+ // Check/Setup versioning strategy
1366
+ if (!skipPrompts) {
1367
+ const settings = coordinator.loadProjectSettings();
1368
+ if (!settings.versioningStrategy?.configured) {
1369
+ log.header();
1370
+ log.title('📅 Project Versioning Strategy');
1371
+ await coordinator.ensureProjectSetup();
1372
+ } else {
1373
+ // Optional reconfigure
1374
+ log.info('Versioning strategy is already configured.');
1375
+ const reconfigure = await confirm('Do you want to reconfigure versioning?', false);
1376
+ if (reconfigure) {
1377
+ await coordinator.ensureProjectSetup({ force: true });
1378
+ }
1379
+ }
1380
+ } else {
1381
+ // In non-interactive mode, we only ensure if missing (and hope it doesn't block or has defaults)
1382
+ // Actually promptForStartingVersion is interactive-only, so we skip if missing in non-interactive
1383
+ // or we could force defaults. For now, we skip to avoid hanging.
1384
+ const settings = coordinator.loadProjectSettings();
1385
+ if (!settings.versioningStrategy?.configured) {
1386
+ log.warn('Skipping versioning setup (interactive-only). Run setup without --yes to configure.');
1387
+ }
1388
+ }
1133
1389
 
1134
1390
  // Clean up DevOpsAgent files to avoid duplicates
1135
1391
  cleanupDevOpsAgentFiles(projectRoot, agentName);
package/src/ui-utils.js CHANGED
@@ -478,7 +478,7 @@ export function showWelcome(version = '2.0.0') {
478
478
  */
479
479
  export function showCopyright() {
480
480
  console.log();
481
- console.log(`${colors.dim}Copyright © 2024 SecondBrain Labs. All rights reserved.${colors.reset}`);
481
+ console.log(`${colors.dim}Copyright © 2025 SeKondBrain AI Labs Limited. All rights reserved.${colors.reset}`);
482
482
  console.log(`${colors.dim}Licensed under the MIT License${colors.reset}`);
483
483
  console.log();
484
484
  }
@@ -50,7 +50,7 @@ show_copyright() {
50
50
  echo " CS_DevOpsAgent - Intelligent Git Automation System"
51
51
  echo " Version 1.7.2 | Build 20251010.03"
52
52
  echo " "
53
- echo " Copyright (c) 2024 SecondBrain Labs"
53
+ echo " Copyright (c) 2025 SeKondBrain AI Labs Limited"
54
54
  echo " Author: Sachin Dev Duggal"
55
55
  echo " "
56
56
  echo " Licensed under the MIT License"
@@ -394,32 +394,9 @@ main() {
394
394
  echo -e "${DIM}💡 New to DevOps Agent? Run: s9n-devops-agent tutorial${NC}"
395
395
  echo
396
396
 
397
- # Main selection loop
398
- while true; do
399
- if select_session; then
400
- # After agent exits, ask if they want to continue or exit
401
- echo
402
- echo -e "${YELLOW}═══════════════════════════════════════════════════════════${NC}"
403
- echo -e "${BOLD}Agent has stopped.${NC}"
404
- echo -e "${YELLOW}═══════════════════════════════════════════════════════════${NC}"
405
- echo
406
- echo -e "Would you like to:"
407
- echo -e " ${BOLD}1)${NC} Select another session"
408
- echo -e " ${BOLD}2)${NC} Exit the session manager"
409
- echo
410
- echo -n "Your choice [1/2]: "
411
- read continue_choice
412
-
413
- if [[ "$continue_choice" == "2" ]]; then
414
- echo
415
- echo -e "${GREEN}Goodbye! Thank you for using DevOps Session Manager.${NC}"
416
- exit 0
417
- fi
418
- echo
419
- echo -e "${BLUE}Returning to session selection...${NC}"
420
- echo
421
- fi
422
- done
397
+ # Run Kora (Smart Assistant)
398
+ # This replaces the old menu system with the unified chat interface
399
+ node "$SRC_DIR/agent-chat.js" "$@"
423
400
  }
424
401
 
425
402
  # Handle Ctrl+C gracefully