erosolar-cli 1.7.406 → 1.7.408

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.
@@ -13,6 +13,7 @@ import { getLearningSummary, getRecentLearning, commitLearning, exportAllLearnin
13
13
  import { buildEnabledToolSet, evaluateToolPermissions, getToolToggleOptions, } from '../capabilities/toolRegistry.js';
14
14
  import { detectApiKeyError } from '../core/errors/apiKeyErrors.js';
15
15
  import { buildWorkspaceContext } from '../workspace.js';
16
+ import { detectBuildCommand, detectTestCommand } from '../tools/detectCommands.js';
16
17
  import { buildInteractiveSystemPrompt } from './systemPrompt.js';
17
18
  import { getTaskCompletionDetector, resetTaskCompletionDetector, } from './taskCompletionDetector.js';
18
19
  import { discoverAllModels, quickCheckProviders, getCachedDiscoveredModels, sortModelsByPriority } from '../core/modelDiscovery.js';
@@ -43,6 +44,7 @@ import { enterStreamingMode, exitStreamingMode } from '../ui/globalWriteLock.js'
43
44
  import { setGlobalAIEnhancer } from '../tools/localExplore.js';
44
45
  import { createProvider } from '../providers/providerFactory.js';
45
46
  import { getParallelAgentManager } from '../subagents/parallelAgentManager.js';
47
+ import { formatThinkingContent } from '../ui/textHighlighter.js';
46
48
  const execAsync = promisify(exec);
47
49
  const DROPDOWN_COLORS = [
48
50
  theme.primary,
@@ -147,6 +149,7 @@ export class InteractiveShell {
147
149
  customCommandMap;
148
150
  sessionRestoreConfig;
149
151
  _enabledPlugins;
152
+ autoVerificationInFlight = false;
150
153
  // Auto-test tracking
151
154
  autoTestInFlight = false;
152
155
  // AlphaZero learning tracking
@@ -254,6 +257,11 @@ export class InteractiveShell {
254
257
  description: 'Show available and loaded plugins',
255
258
  category: 'configuration',
256
259
  });
260
+ this.slashCommands.push({
261
+ command: '/contextlog',
262
+ description: 'Show recent context compaction summaries',
263
+ category: 'context',
264
+ });
257
265
  this.slashCommands.push({
258
266
  command: '/offsec',
259
267
  description: 'AlphaZero offensive security run (start/status/next)',
@@ -1430,7 +1438,9 @@ export class InteractiveShell {
1430
1438
  // Advance buffer past the tag
1431
1439
  this.assistantStreamBuffer = this.assistantStreamBuffer.slice(tagIndex + match[0].length);
1432
1440
  const isClosing = match[1] === '/';
1433
- const tagType = (match[2] ?? '').toLowerCase();
1441
+ const rawTagType = (match[2] ?? '').toLowerCase();
1442
+ // Map 'thinking' to 'thought' for AssistantBlockType compatibility
1443
+ const tagType = (rawTagType === 'thinking' ? 'thought' : rawTagType);
1434
1444
  if (isClosing) {
1435
1445
  if (this.assistantStreamPhase === tagType) {
1436
1446
  this.assistantStreamPhase = null;
@@ -1455,7 +1465,11 @@ export class InteractiveShell {
1455
1465
  if (this.assistantStreamPhase) {
1456
1466
  this.renderAssistantStreamHeader(this.assistantStreamPhase);
1457
1467
  }
1458
- this.enqueueAssistantStream(content);
1468
+ let output = content;
1469
+ if (this.assistantStreamPhase === 'thought') {
1470
+ output = this.formatThoughtDisplay(content, { streaming: true });
1471
+ }
1472
+ this.enqueueAssistantStream(output);
1459
1473
  }
1460
1474
  setAssistantStreamPhase(phase) {
1461
1475
  if (this.assistantStreamPhase === phase) {
@@ -1534,11 +1548,12 @@ export class InteractiveShell {
1534
1548
  if (!normalized) {
1535
1549
  return;
1536
1550
  }
1551
+ const formatted = type === 'thought' ? this.formatThoughtDisplay(normalized) : normalized;
1537
1552
  if (this.assistantBlocksEnabled && this.assistantBlockRenderer) {
1538
- this.renderAssistantBlock(type, normalized, metadata);
1553
+ this.renderAssistantBlock(type, formatted, metadata);
1539
1554
  return;
1540
1555
  }
1541
- this.renderAssistantFallback(type, normalized, metadata);
1556
+ this.renderAssistantFallback(type, formatted, metadata);
1542
1557
  }
1543
1558
  renderAssistantFallback(type, content, metadata) {
1544
1559
  const header = this.formatAssistantHeader(type, metadata);
@@ -1546,6 +1561,21 @@ export class InteractiveShell {
1546
1561
  const block = `\n${header}\n${compact}\n\n`;
1547
1562
  this.enqueueAssistantStream(block);
1548
1563
  }
1564
+ formatThoughtDisplay(content, options = {}) {
1565
+ const normalized = content.replace(/\r\n/g, '\n');
1566
+ if (options.streaming) {
1567
+ return formatThinkingContent(normalized);
1568
+ }
1569
+ const lines = normalized.split('\n');
1570
+ return lines
1571
+ .map((line, index) => {
1572
+ const prefix = index === 0 ? theme.info(icons.action) : theme.ui.muted(icons.subaction);
1573
+ const trimmed = line.trim();
1574
+ const body = trimmed ? formatThinkingContent(trimmed) : '';
1575
+ return body ? `${prefix} ${body}` : `${prefix}`;
1576
+ })
1577
+ .join('\n');
1578
+ }
1549
1579
  formatAssistantHeader(type, metadata) {
1550
1580
  if (this.assistantBlocksEnabled && this.assistantBlockRenderer) {
1551
1581
  return this.assistantBlockRenderer.formatHeader(type, metadata);
@@ -2245,6 +2275,9 @@ export class InteractiveShell {
2245
2275
  case '/context':
2246
2276
  await this.refreshWorkspaceContextCommand(input);
2247
2277
  break;
2278
+ case '/contextlog':
2279
+ this.showContextSummaryLog(input);
2280
+ break;
2248
2281
  case '/agents':
2249
2282
  this.showAgentsMenu();
2250
2283
  break;
@@ -2543,6 +2576,66 @@ export class InteractiveShell {
2543
2576
  display.showWarning('Workspace snapshot refreshed, but the agent failed to rebuild. Run /doctor for details.');
2544
2577
  }
2545
2578
  }
2579
+ showContextSummaryLog(input) {
2580
+ const agent = this.agent;
2581
+ if (!agent) {
2582
+ display.showWarning('No active agent session. Start one to inspect context summaries.');
2583
+ return;
2584
+ }
2585
+ const contextManager = agent.getContextManager();
2586
+ if (!contextManager?.getSummaryLog) {
2587
+ display.showWarning('Context summary logging is unavailable in this session.');
2588
+ return;
2589
+ }
2590
+ const tokens = input.trim().split(/\s+/).slice(1);
2591
+ let limit = 5;
2592
+ for (const token of tokens) {
2593
+ if (!token)
2594
+ continue;
2595
+ const normalized = token.toLowerCase();
2596
+ if (/^\d+$/.test(token)) {
2597
+ limit = Math.min(Math.max(parseInt(token, 10), 1), 20);
2598
+ }
2599
+ else if (normalized.startsWith('limit=')) {
2600
+ const value = parseInt(normalized.split('=')[1] ?? '', 10);
2601
+ if (!Number.isNaN(value)) {
2602
+ limit = Math.min(Math.max(value, 1), 20);
2603
+ }
2604
+ }
2605
+ }
2606
+ const entries = contextManager.getSummaryLog(limit);
2607
+ if (!entries.length) {
2608
+ display.showInfo('No context summaries captured yet.');
2609
+ return;
2610
+ }
2611
+ const lines = [];
2612
+ lines.push(`${theme.primary('Context summaries')} ${theme.ui.muted(`(latest ${entries.length})`)}`);
2613
+ for (const entry of entries) {
2614
+ const timestamp = new Date(entry.timestamp).toLocaleString();
2615
+ const headerParts = [
2616
+ theme.ui.muted(timestamp),
2617
+ theme.info(entry.method),
2618
+ `removed ${theme.error(String(entry.removed))}`,
2619
+ `kept ${theme.success(String(entry.preserved))}`,
2620
+ ];
2621
+ if (entry.reason) {
2622
+ headerParts.push(theme.ui.muted(entry.reason));
2623
+ }
2624
+ if (entry.model) {
2625
+ headerParts.push(theme.ui.muted(`model=${entry.model}`));
2626
+ }
2627
+ lines.push(headerParts.join(' · '));
2628
+ if (entry.summary) {
2629
+ const trimmed = entry.summary.length > 600 ? `${entry.summary.slice(0, 600)}…` : entry.summary;
2630
+ lines.push(trimmed);
2631
+ }
2632
+ else {
2633
+ lines.push(theme.ui.muted('(no summary text — simple prune)'));
2634
+ }
2635
+ lines.push(''); // spacer
2636
+ }
2637
+ display.showSystemMessage(lines.join('\n'));
2638
+ }
2546
2639
  parseContextOverrideTokens(input) {
2547
2640
  const overrides = {};
2548
2641
  let hasOverride = false;
@@ -5819,7 +5912,25 @@ What's the next action?`;
5819
5912
  }
5820
5913
  if (name === 'bash' || name === 'execute_bash') {
5821
5914
  const command = String(entry.args['command'] ?? '').toLowerCase();
5822
- return command.includes('npm test') || command.includes('yarn test') || command.includes('pnpm test');
5915
+ const patterns = [
5916
+ 'npm test',
5917
+ 'yarn test',
5918
+ 'pnpm test',
5919
+ 'bun test',
5920
+ 'go test',
5921
+ 'cargo test',
5922
+ 'pytest',
5923
+ 'python -m pytest',
5924
+ 'tox',
5925
+ 'mvn test',
5926
+ './mvnw test',
5927
+ 'gradle test',
5928
+ './gradlew test',
5929
+ 'dotnet test',
5930
+ 'make test',
5931
+ 'swift test',
5932
+ ];
5933
+ return patterns.some((pattern) => command.includes(pattern));
5823
5934
  }
5824
5935
  return false;
5825
5936
  }
@@ -5837,24 +5948,28 @@ What's the next action?`;
5837
5948
  const parts = [error?.stdout, error?.stderr, error?.message].filter((part) => typeof part === 'string' && part.trim());
5838
5949
  return parts.join('\n').trim();
5839
5950
  }
5840
- async enforceAutoTests(trigger) {
5841
- if (this.autoTestInFlight) {
5842
- return;
5843
- }
5844
- if (!this.verificationEnabled) {
5845
- return;
5951
+ async enforceAutoTests(trigger, commandInfo) {
5952
+ if (this.autoTestInFlight || !this.verificationEnabled) {
5953
+ return 'skipped';
5846
5954
  }
5847
5955
  const latestChange = this.getLatestFileChangeTimestamp();
5848
5956
  if (!latestChange) {
5849
- return;
5957
+ return 'skipped';
5850
5958
  }
5851
5959
  const latestTest = this.getLatestTestTimestamp();
5852
5960
  if (latestTest && latestChange <= latestTest) {
5853
- return;
5961
+ return 'skipped';
5962
+ }
5963
+ const detected = commandInfo ?? detectTestCommand(this.workingDir);
5964
+ if (!detected) {
5965
+ this.lastAutoTestRun = Date.now();
5966
+ display.showSystemMessage('ℹ️ Skipping auto-tests: no test command detected for this repo.');
5967
+ return 'skipped';
5854
5968
  }
5855
5969
  this.autoTestInFlight = true;
5856
- const command = 'npm test -- --runInBand';
5857
- display.showSystemMessage(`🧪 Auto-testing recent changes (${trigger}) with "${command}"...`);
5970
+ const command = detected.command;
5971
+ const reasonSuffix = detected.reason ? ` [${detected.reason}]` : '';
5972
+ display.showSystemMessage(`🧪 Auto-testing recent changes (${trigger}) with "${command}"${reasonSuffix}...`);
5858
5973
  this.updateStatusMessage('Running tests automatically...');
5859
5974
  try {
5860
5975
  const { stdout, stderr } = await execAsync(command, {
@@ -5869,6 +5984,7 @@ What's the next action?`;
5869
5984
  this.writeLocked(`${outputText}\n`);
5870
5985
  }
5871
5986
  this.statusTracker.clearOverride('tests');
5987
+ return 'success';
5872
5988
  }
5873
5989
  catch (error) {
5874
5990
  this.lastAutoTestRun = Date.now();
@@ -5878,9 +5994,10 @@ What's the next action?`;
5878
5994
  this.writeLocked(`${message}\n`);
5879
5995
  }
5880
5996
  this.statusTracker.pushOverride('tests', 'Tests failing', {
5881
- detail: 'Auto-run npm test failed',
5997
+ detail: `Auto-run ${command} failed`,
5882
5998
  tone: 'danger',
5883
5999
  });
6000
+ return 'failed';
5884
6001
  }
5885
6002
  finally {
5886
6003
  this.updateStatusMessage(null);
@@ -5896,10 +6013,31 @@ What's the next action?`;
5896
6013
  }
5897
6014
  if (name === 'bash' || name === 'execute_bash') {
5898
6015
  const command = String(entry.args['command'] ?? '').toLowerCase();
5899
- return (command.includes('npm run build') ||
5900
- command.includes('yarn build') ||
5901
- command.includes('pnpm build') ||
5902
- command.includes('tsc'));
6016
+ const makeBuild = command.startsWith('make') && !command.includes('make test') && !command.includes('make lint');
6017
+ const patterns = [
6018
+ 'npm run build',
6019
+ 'yarn build',
6020
+ 'pnpm build',
6021
+ 'bun run build',
6022
+ 'bun build',
6023
+ 'tsc',
6024
+ 'cargo build',
6025
+ 'go build',
6026
+ 'python -m build',
6027
+ 'mvn -b -dskiptests package',
6028
+ 'mvn package',
6029
+ 'mvn install',
6030
+ './mvnw -b -dskiptests package',
6031
+ './mvnw package',
6032
+ './mvnw install',
6033
+ 'gradle build',
6034
+ 'gradle assemble',
6035
+ './gradlew build',
6036
+ './gradlew assemble',
6037
+ 'dotnet build',
6038
+ 'swift build',
6039
+ ];
6040
+ return makeBuild || patterns.some((pattern) => command.includes(pattern));
5903
6041
  }
5904
6042
  return false;
5905
6043
  }
@@ -5915,26 +6053,30 @@ What's the next action?`;
5915
6053
  }
5916
6054
  /**
5917
6055
  * Auto-build verification after file edits.
5918
- * Runs `npm run build` to catch TypeScript errors and feeds failures back to the agent.
6056
+ * Detects the build command for the current repo and feeds failures back to the agent.
5919
6057
  */
5920
- async enforceAutoBuild(trigger) {
5921
- if (this.autoBuildInFlight) {
5922
- return;
5923
- }
5924
- if (!this.verificationEnabled) {
5925
- return;
6058
+ async enforceAutoBuild(trigger, commandInfo) {
6059
+ if (this.autoBuildInFlight || !this.verificationEnabled) {
6060
+ return 'skipped';
5926
6061
  }
5927
6062
  const latestChange = this.getLatestFileChangeTimestamp();
5928
6063
  if (!latestChange) {
5929
- return;
6064
+ return 'skipped';
5930
6065
  }
5931
6066
  const latestBuild = this.getLatestBuildTimestamp();
5932
6067
  if (latestBuild && latestChange <= latestBuild) {
5933
- return;
6068
+ return 'skipped';
6069
+ }
6070
+ const detected = commandInfo ?? detectBuildCommand(this.workingDir);
6071
+ if (!detected) {
6072
+ this.lastAutoBuildRun = Date.now();
6073
+ display.showSystemMessage('ℹ️ Skipping auto-build: no build command detected for this repo.');
6074
+ return 'skipped';
5934
6075
  }
5935
6076
  this.autoBuildInFlight = true;
5936
- const command = 'npm run build';
5937
- display.showSystemMessage(`🔨 Auto-building to verify changes (${trigger})...`);
6077
+ const command = detected.command;
6078
+ const reasonSuffix = detected.reason ? ` [${detected.reason}]` : '';
6079
+ display.showSystemMessage(`🔨 Auto-building to verify changes (${trigger}) with "${command}"${reasonSuffix}...`);
5938
6080
  this.updateStatusMessage('Running build automatically...');
5939
6081
  try {
5940
6082
  const { stdout, stderr } = await execAsync(command, {
@@ -5949,6 +6091,7 @@ What's the next action?`;
5949
6091
  this.writeLocked(`${outputText}\n`);
5950
6092
  }
5951
6093
  this.statusTracker.clearOverride('build');
6094
+ return 'success';
5952
6095
  }
5953
6096
  catch (error) {
5954
6097
  this.lastAutoBuildRun = Date.now();
@@ -5958,11 +6101,12 @@ What's the next action?`;
5958
6101
  this.writeLocked(`${errorOutput}\n`);
5959
6102
  }
5960
6103
  this.statusTracker.pushOverride('build', 'Build failing', {
5961
- detail: 'Auto-run npm run build failed',
6104
+ detail: `Auto-run ${command} failed`,
5962
6105
  tone: 'danger',
5963
6106
  });
5964
6107
  // Feed build errors back to the agent so it can fix them
5965
6108
  await this.feedBuildErrorsToAgent(errorOutput);
6109
+ return 'failed';
5966
6110
  }
5967
6111
  finally {
5968
6112
  this.updateStatusMessage(null);
@@ -6004,6 +6148,34 @@ What's the next action?`;
6004
6148
  display.showWarning('Agent could not automatically fix build errors. Please review manually.');
6005
6149
  }
6006
6150
  }
6151
+ async enforceAutoVerification(trigger) {
6152
+ if (this.autoVerificationInFlight || !this.verificationEnabled) {
6153
+ return;
6154
+ }
6155
+ const latestChange = this.getLatestFileChangeTimestamp();
6156
+ if (!latestChange) {
6157
+ return;
6158
+ }
6159
+ this.autoVerificationInFlight = true;
6160
+ try {
6161
+ const buildInfo = detectBuildCommand(this.workingDir);
6162
+ const testInfo = detectTestCommand(this.workingDir);
6163
+ const buildResult = await this.enforceAutoBuild(trigger, buildInfo);
6164
+ if (buildResult === 'failed') {
6165
+ return;
6166
+ }
6167
+ if (testInfo) {
6168
+ await this.enforceAutoTests(trigger, testInfo);
6169
+ }
6170
+ else {
6171
+ this.lastAutoTestRun = Date.now();
6172
+ display.showSystemMessage('ℹ️ Skipping auto-tests: no test command detected for this repo.');
6173
+ }
6174
+ }
6175
+ finally {
6176
+ this.autoVerificationInFlight = false;
6177
+ }
6178
+ }
6007
6179
  rebuildAgent() {
6008
6180
  const previousHistory = this.agent ? this.agent.getHistory() : this.cachedHistory;
6009
6181
  try {
@@ -6059,8 +6231,7 @@ What's the next action?`;
6059
6231
  }
6060
6232
  }
6061
6233
  // Auto-verify changes: build first (catches type errors), then tests
6062
- void this.enforceAutoBuild('final-response');
6063
- void this.enforceAutoTests('final-response');
6234
+ void this.enforceAutoVerification('final-response');
6064
6235
  }
6065
6236
  else {
6066
6237
  // Non-final message = narrative text before tool calls (Claude Code style)
@@ -6104,6 +6275,7 @@ What's the next action?`;
6104
6275
  // Show notification that context was pruned
6105
6276
  const method = stats['method'];
6106
6277
  const percentage = stats['percentage'];
6278
+ const summarized = stats['summarized'] === true;
6107
6279
  if (method === 'emergency-recovery') {
6108
6280
  display.stream(`\n✅ Context recovery complete. Removed ${removedCount} messages. Context now at ${percentage ?? 'unknown'}%\n`);
6109
6281
  }
@@ -6111,6 +6283,9 @@ What's the next action?`;
6111
6283
  if (typeof percentage === 'number') {
6112
6284
  this.updateContextUsage(percentage);
6113
6285
  }
6286
+ if (summarized) {
6287
+ display.showSystemMessage('Context summary captured. View the latest summaries with /contextlog.');
6288
+ }
6114
6289
  // Ensure prompt remains visible at bottom after context messages
6115
6290
  this.renderPromptArea();
6116
6291
  },
@@ -6136,8 +6311,7 @@ What's the next action?`;
6136
6311
  this.renderPromptArea();
6137
6312
  },
6138
6313
  onVerificationNeeded: () => {
6139
- void this.enforceAutoBuild('verification');
6140
- void this.enforceAutoTests('verification');
6314
+ void this.enforceAutoVerification('verification');
6141
6315
  },
6142
6316
  });
6143
6317
  // Register global AI enhancer for explore tool - uses active model by default
@@ -6248,8 +6422,8 @@ What's the next action?`;
6248
6422
  ].join('\n');
6249
6423
  case 'balanced':
6250
6424
  default:
6251
- // Balanced is the default; avoid injecting extra prompt text to keep context lean.
6252
- return null;
6425
+ // Balanced mode: show thinking for complex reasoning, planning, or multi-step tasks
6426
+ return 'When reasoning through complex tasks, planning approaches, or making decisions, wrap your thought process in <thinking> tags before your response. Keep thinking concise and focused on the decision-making process.';
6253
6427
  }
6254
6428
  }
6255
6429
  buildDisplayMetadata(metadata) {