@trenchwork/erosolar 1.1.62 → 1.2.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.
Files changed (225) hide show
  1. package/dist/capabilities/todoCapability.js +2 -2
  2. package/dist/capabilities/todoCapability.js.map +1 -1
  3. package/dist/config.js +1 -1
  4. package/dist/contracts/v1/agent.d.ts +12 -2
  5. package/dist/contracts/v1/agent.d.ts.map +1 -1
  6. package/dist/core/adversarialCorrection.d.ts +22 -0
  7. package/dist/core/adversarialCorrection.d.ts.map +1 -0
  8. package/dist/core/adversarialCorrection.js +25 -0
  9. package/dist/core/adversarialCorrection.js.map +1 -0
  10. package/dist/core/agent.d.ts +2 -0
  11. package/dist/core/agent.d.ts.map +1 -1
  12. package/dist/core/agent.js +11 -45
  13. package/dist/core/agent.js.map +1 -1
  14. package/dist/core/contextManager.d.ts.map +1 -1
  15. package/dist/core/contextManager.js +5 -2
  16. package/dist/core/contextManager.js.map +1 -1
  17. package/dist/core/errorClassification.d.ts +44 -0
  18. package/dist/core/errorClassification.d.ts.map +1 -0
  19. package/dist/core/errorClassification.js +333 -0
  20. package/dist/core/errorClassification.js.map +1 -0
  21. package/dist/core/failureRegistry.d.ts +30 -0
  22. package/dist/core/failureRegistry.d.ts.map +1 -0
  23. package/dist/core/failureRegistry.js +74 -0
  24. package/dist/core/failureRegistry.js.map +1 -0
  25. package/dist/core/hitl.d.ts.map +1 -1
  26. package/dist/core/hitl.js +8 -0
  27. package/dist/core/hitl.js.map +1 -1
  28. package/dist/core/hostedAuth.d.ts +88 -0
  29. package/dist/core/hostedAuth.d.ts.map +1 -0
  30. package/dist/core/hostedAuth.js +219 -0
  31. package/dist/core/hostedAuth.js.map +1 -0
  32. package/dist/core/quota.d.ts +61 -0
  33. package/dist/core/quota.d.ts.map +1 -0
  34. package/dist/core/quota.js +104 -0
  35. package/dist/core/quota.js.map +1 -0
  36. package/dist/core/quotaErrors.d.ts.map +1 -1
  37. package/dist/core/quotaErrors.js +3 -5
  38. package/dist/core/quotaErrors.js.map +1 -1
  39. package/dist/core/resultVerification.d.ts +3 -2
  40. package/dist/core/resultVerification.d.ts.map +1 -1
  41. package/dist/core/resultVerification.js +3 -2
  42. package/dist/core/resultVerification.js.map +1 -1
  43. package/dist/core/secretStore.d.ts +11 -0
  44. package/dist/core/secretStore.d.ts.map +1 -1
  45. package/dist/core/secretStore.js +25 -0
  46. package/dist/core/secretStore.js.map +1 -1
  47. package/dist/core/slashCommands.d.ts.map +1 -1
  48. package/dist/core/slashCommands.js +4 -0
  49. package/dist/core/slashCommands.js.map +1 -1
  50. package/dist/core/thinkingVerbs.d.ts +31 -0
  51. package/dist/core/thinkingVerbs.d.ts.map +1 -0
  52. package/dist/core/thinkingVerbs.js +58 -0
  53. package/dist/core/thinkingVerbs.js.map +1 -0
  54. package/dist/core/turnGovernor.d.ts +63 -0
  55. package/dist/core/turnGovernor.d.ts.map +1 -0
  56. package/dist/core/turnGovernor.js +94 -0
  57. package/dist/core/turnGovernor.js.map +1 -0
  58. package/dist/core/updateChecker.d.ts.map +1 -1
  59. package/dist/core/updateChecker.js +5 -1
  60. package/dist/core/updateChecker.js.map +1 -1
  61. package/dist/core/usage.d.ts +28 -0
  62. package/dist/core/usage.d.ts.map +1 -0
  63. package/dist/core/usage.js +77 -0
  64. package/dist/core/usage.js.map +1 -0
  65. package/dist/headless/interactiveShell.d.ts +6 -0
  66. package/dist/headless/interactiveShell.d.ts.map +1 -1
  67. package/dist/headless/interactiveShell.js +262 -42
  68. package/dist/headless/interactiveShell.js.map +1 -1
  69. package/dist/plugins/providers/deepseek/index.d.ts.map +1 -1
  70. package/dist/plugins/providers/deepseek/index.js +8 -5
  71. package/dist/plugins/providers/deepseek/index.js.map +1 -1
  72. package/dist/providers/baseProvider.d.ts +5 -13
  73. package/dist/providers/baseProvider.d.ts.map +1 -1
  74. package/dist/providers/baseProvider.js +12 -66
  75. package/dist/providers/baseProvider.js.map +1 -1
  76. package/dist/providers/openaiChatCompletionsProvider.d.ts.map +1 -1
  77. package/dist/providers/openaiChatCompletionsProvider.js +27 -76
  78. package/dist/providers/openaiChatCompletionsProvider.js.map +1 -1
  79. package/dist/providers/resilientProvider.d.ts +2 -9
  80. package/dist/providers/resilientProvider.d.ts.map +1 -1
  81. package/dist/providers/resilientProvider.js +13 -199
  82. package/dist/providers/resilientProvider.js.map +1 -1
  83. package/dist/runtime/agentController.d.ts.map +1 -1
  84. package/dist/runtime/agentController.js +7 -0
  85. package/dist/runtime/agentController.js.map +1 -1
  86. package/dist/shell/toolPresentation.d.ts +7 -0
  87. package/dist/shell/toolPresentation.d.ts.map +1 -1
  88. package/dist/shell/toolPresentation.js +78 -4
  89. package/dist/shell/toolPresentation.js.map +1 -1
  90. package/dist/tools/bashTools.d.ts.map +1 -1
  91. package/dist/tools/bashTools.js +9 -3
  92. package/dist/tools/bashTools.js.map +1 -1
  93. package/dist/tools/grepTools.d.ts.map +1 -1
  94. package/dist/tools/grepTools.js +10 -1
  95. package/dist/tools/grepTools.js.map +1 -1
  96. package/dist/tools/memoryTools.d.ts +7 -0
  97. package/dist/tools/memoryTools.d.ts.map +1 -1
  98. package/dist/tools/memoryTools.js +17 -0
  99. package/dist/tools/memoryTools.js.map +1 -1
  100. package/dist/tools/searchTools.d.ts.map +1 -1
  101. package/dist/tools/searchTools.js +5 -4
  102. package/dist/tools/searchTools.js.map +1 -1
  103. package/dist/tools/todoTools.d.ts +3 -4
  104. package/dist/tools/todoTools.d.ts.map +1 -1
  105. package/dist/tools/todoTools.js +23 -4
  106. package/dist/tools/todoTools.js.map +1 -1
  107. package/dist/tools/webTools.d.ts.map +1 -1
  108. package/dist/tools/webTools.js +3 -1
  109. package/dist/tools/webTools.js.map +1 -1
  110. package/dist/ui/ink/ChatStatic.d.ts.map +1 -1
  111. package/dist/ui/ink/ChatStatic.js +21 -5
  112. package/dist/ui/ink/ChatStatic.js.map +1 -1
  113. package/dist/ui/ink/InkPromptController.d.ts +5 -0
  114. package/dist/ui/ink/InkPromptController.d.ts.map +1 -1
  115. package/dist/ui/ink/InkPromptController.js +16 -6
  116. package/dist/ui/ink/InkPromptController.js.map +1 -1
  117. package/dist/ui/ink/Prompt.d.ts +6 -0
  118. package/dist/ui/ink/Prompt.d.ts.map +1 -1
  119. package/dist/ui/ink/Prompt.js +69 -10
  120. package/dist/ui/ink/Prompt.js.map +1 -1
  121. package/dist/ui/ink/StatusLine.d.ts +6 -0
  122. package/dist/ui/ink/StatusLine.d.ts.map +1 -1
  123. package/dist/ui/ink/StatusLine.js +17 -3
  124. package/dist/ui/ink/StatusLine.js.map +1 -1
  125. package/dist/ui/ink/pasteBuffer.d.ts +44 -0
  126. package/dist/ui/ink/pasteBuffer.d.ts.map +1 -0
  127. package/dist/ui/ink/pasteBuffer.js +73 -0
  128. package/dist/ui/ink/pasteBuffer.js.map +1 -0
  129. package/package.json +1 -1
  130. package/dist/core/index.d.ts +0 -7
  131. package/dist/core/index.d.ts.map +0 -1
  132. package/dist/core/index.js +0 -7
  133. package/dist/core/index.js.map +0 -1
  134. package/dist/core/providerKeys.d.ts +0 -20
  135. package/dist/core/providerKeys.d.ts.map +0 -1
  136. package/dist/core/providerKeys.js +0 -40
  137. package/dist/core/providerKeys.js.map +0 -1
  138. package/dist/plugins/index.d.ts +0 -49
  139. package/dist/plugins/index.d.ts.map +0 -1
  140. package/dist/plugins/index.js +0 -104
  141. package/dist/plugins/index.js.map +0 -1
  142. package/dist/plugins/tools/agentSpawning/agentSpawningPlugin.d.ts +0 -10
  143. package/dist/plugins/tools/agentSpawning/agentSpawningPlugin.d.ts.map +0 -1
  144. package/dist/plugins/tools/agentSpawning/agentSpawningPlugin.js +0 -110
  145. package/dist/plugins/tools/agentSpawning/agentSpawningPlugin.js.map +0 -1
  146. package/dist/plugins/tools/bash/localBashPlugin.d.ts +0 -3
  147. package/dist/plugins/tools/bash/localBashPlugin.d.ts.map +0 -1
  148. package/dist/plugins/tools/bash/localBashPlugin.js +0 -14
  149. package/dist/plugins/tools/bash/localBashPlugin.js.map +0 -1
  150. package/dist/plugins/tools/edit/editPlugin.d.ts +0 -9
  151. package/dist/plugins/tools/edit/editPlugin.d.ts.map +0 -1
  152. package/dist/plugins/tools/edit/editPlugin.js +0 -15
  153. package/dist/plugins/tools/edit/editPlugin.js.map +0 -1
  154. package/dist/plugins/tools/enhancedGit/enhancedGitPlugin.d.ts +0 -3
  155. package/dist/plugins/tools/enhancedGit/enhancedGitPlugin.d.ts.map +0 -1
  156. package/dist/plugins/tools/enhancedGit/enhancedGitPlugin.js +0 -9
  157. package/dist/plugins/tools/enhancedGit/enhancedGitPlugin.js.map +0 -1
  158. package/dist/plugins/tools/filesystem/localFilesystemPlugin.d.ts +0 -3
  159. package/dist/plugins/tools/filesystem/localFilesystemPlugin.d.ts.map +0 -1
  160. package/dist/plugins/tools/filesystem/localFilesystemPlugin.js +0 -14
  161. package/dist/plugins/tools/filesystem/localFilesystemPlugin.js.map +0 -1
  162. package/dist/plugins/tools/gitHistory/gitHistoryPlugin.d.ts +0 -3
  163. package/dist/plugins/tools/gitHistory/gitHistoryPlugin.d.ts.map +0 -1
  164. package/dist/plugins/tools/gitHistory/gitHistoryPlugin.js +0 -9
  165. package/dist/plugins/tools/gitHistory/gitHistoryPlugin.js.map +0 -1
  166. package/dist/plugins/tools/index.d.ts +0 -3
  167. package/dist/plugins/tools/index.d.ts.map +0 -1
  168. package/dist/plugins/tools/index.js +0 -3
  169. package/dist/plugins/tools/index.js.map +0 -1
  170. package/dist/plugins/tools/integrity/integrityPlugin.d.ts +0 -3
  171. package/dist/plugins/tools/integrity/integrityPlugin.d.ts.map +0 -1
  172. package/dist/plugins/tools/integrity/integrityPlugin.js +0 -31
  173. package/dist/plugins/tools/integrity/integrityPlugin.js.map +0 -1
  174. package/dist/plugins/tools/mcp/mcpPlugin.d.ts +0 -3
  175. package/dist/plugins/tools/mcp/mcpPlugin.d.ts.map +0 -1
  176. package/dist/plugins/tools/mcp/mcpPlugin.js +0 -27
  177. package/dist/plugins/tools/mcp/mcpPlugin.js.map +0 -1
  178. package/dist/plugins/tools/nodeDefaults.d.ts +0 -13
  179. package/dist/plugins/tools/nodeDefaults.d.ts.map +0 -1
  180. package/dist/plugins/tools/nodeDefaults.js +0 -33
  181. package/dist/plugins/tools/nodeDefaults.js.map +0 -1
  182. package/dist/plugins/tools/orchestration/orchestrationPlugin.d.ts +0 -3
  183. package/dist/plugins/tools/orchestration/orchestrationPlugin.d.ts.map +0 -1
  184. package/dist/plugins/tools/orchestration/orchestrationPlugin.js +0 -340
  185. package/dist/plugins/tools/orchestration/orchestrationPlugin.js.map +0 -1
  186. package/dist/plugins/tools/registry.d.ts +0 -22
  187. package/dist/plugins/tools/registry.d.ts.map +0 -1
  188. package/dist/plugins/tools/registry.js +0 -58
  189. package/dist/plugins/tools/registry.js.map +0 -1
  190. package/dist/plugins/tools/search/localSearchPlugin.d.ts +0 -3
  191. package/dist/plugins/tools/search/localSearchPlugin.d.ts.map +0 -1
  192. package/dist/plugins/tools/search/localSearchPlugin.js +0 -14
  193. package/dist/plugins/tools/search/localSearchPlugin.js.map +0 -1
  194. package/dist/plugins/tools/skills/skillPlugin.d.ts +0 -3
  195. package/dist/plugins/tools/skills/skillPlugin.d.ts.map +0 -1
  196. package/dist/plugins/tools/skills/skillPlugin.js +0 -27
  197. package/dist/plugins/tools/skills/skillPlugin.js.map +0 -1
  198. package/dist/plugins/tools/todo/todoPlugin.d.ts +0 -3
  199. package/dist/plugins/tools/todo/todoPlugin.d.ts.map +0 -1
  200. package/dist/plugins/tools/todo/todoPlugin.js +0 -10
  201. package/dist/plugins/tools/todo/todoPlugin.js.map +0 -1
  202. package/dist/runtime/agentWorkerPool.d.ts +0 -167
  203. package/dist/runtime/agentWorkerPool.d.ts.map +0 -1
  204. package/dist/runtime/agentWorkerPool.js +0 -435
  205. package/dist/runtime/agentWorkerPool.js.map +0 -1
  206. package/dist/shell/autoExecutor.d.ts +0 -70
  207. package/dist/shell/autoExecutor.d.ts.map +0 -1
  208. package/dist/shell/autoExecutor.js +0 -320
  209. package/dist/shell/autoExecutor.js.map +0 -1
  210. package/dist/shell/commandRegistry.d.ts +0 -122
  211. package/dist/shell/commandRegistry.d.ts.map +0 -1
  212. package/dist/shell/commandRegistry.js +0 -355
  213. package/dist/shell/commandRegistry.js.map +0 -1
  214. package/dist/shell/composableMessage.d.ts +0 -178
  215. package/dist/shell/composableMessage.d.ts.map +0 -1
  216. package/dist/shell/composableMessage.js +0 -384
  217. package/dist/shell/composableMessage.js.map +0 -1
  218. package/dist/shell/vimMode.d.ts +0 -66
  219. package/dist/shell/vimMode.d.ts.map +0 -1
  220. package/dist/shell/vimMode.js +0 -435
  221. package/dist/shell/vimMode.js.map +0 -1
  222. package/dist/tools/localExplore.d.ts +0 -38
  223. package/dist/tools/localExplore.d.ts.map +0 -1
  224. package/dist/tools/localExplore.js +0 -30
  225. package/dist/tools/localExplore.js.map +0 -1
@@ -13,9 +13,8 @@
13
13
  * - Ctrl+C to interrupt
14
14
  */
15
15
  import { stdin, stdout, exit } from 'node:process';
16
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
17
- import { resolve, dirname, join, relative } from 'node:path';
18
- import { homedir } from 'node:os';
16
+ import { readFileSync } from 'node:fs';
17
+ import { resolve, dirname, relative } from 'node:path';
19
18
  import { fileURLToPath } from 'node:url';
20
19
  import { exec as childExec } from 'node:child_process';
21
20
  import { promisify } from 'node:util';
@@ -32,7 +31,10 @@ import { resolveProfileConfig } from '../config.js';
32
31
  import { createAgentController } from '../runtime/agentController.js';
33
32
  import { expandFileMentions, listWorkspaceFiles } from '../core/fileMentions.js';
34
33
  import { resolveWorkspaceCaptureOptions, buildWorkspaceContext } from '../workspace.js';
35
- import { loadAllSecrets, listSecretDefinitions, setSecretValue, getSecretValue } from '../core/secretStore.js';
34
+ import { loadAllSecrets, listSecretDefinitions, setSecretValue, getSecretValue, getSecretDefinition, classifyKeyEntry } from '../core/secretStore.js';
35
+ import { resolveKeyMode, keyModeLine, setPreferOwnKeys, clearHostedSession, loginViaLoopback } from '../core/hostedAuth.js';
36
+ import { appendMemoryNote } from '../tools/memoryTools.js';
37
+ import { recordDeepSeekUsage, getUsage, TAVILY_MONTHLY_FREE, TAVILY_ONE_TIME_BONUS } from '../core/usage.js';
36
38
  import { listSessions, loadSessionById, saveSessionSnapshot } from '../core/sessionStore.js';
37
39
  import { relativeTime } from '../core/relativeTime.js';
38
40
  import { getModelContextInfo } from '../core/contextWindow.js';
@@ -48,6 +50,10 @@ import { setDebugMode, debugSnippet } from '../utils/debugLogger.js';
48
50
  const exec = promisify(childExec);
49
51
  import { ensureNextSteps } from '../core/finalResponseFormatter.js';
50
52
  import { getTaskCompletionDetector, detectFailingTestOrBuild } from '../core/taskCompletionDetector.js';
53
+ import { TurnGovernor, pendingTodos, nextTodoPrompt } from '../core/turnGovernor.js';
54
+ import { FailureRegistry } from '../core/failureRegistry.js';
55
+ import { buildAdversarialCorrectionPrompt, MAX_ADVERSARIAL_CORRECTIONS } from '../core/adversarialCorrection.js';
56
+ import { getCurrentTodos } from '../tools/todoTools.js';
51
57
  import { checkForUpdates, performBackgroundUpdate } from '../core/updateChecker.js';
52
58
  import { startNewRun } from '../tools/fileChangeTracker.js';
53
59
  import { onSudoPasswordNeeded, offSudoPasswordNeeded, provideSudoPassword } from '../core/sudoPasswordManager.js';
@@ -143,13 +149,20 @@ function getVersion() {
143
149
  }
144
150
  /** Inner content of the welcome box (plain, no border/colour). */
145
151
  function welcomeBodyLines(input) {
146
- const body = ['✻ Welcome to Erosolar Coder', ''];
147
- if (!input.hasApiKey) {
148
- body.push('⚠ No API key configured', '', 'Get your key: https://platform.deepseek.com/', 'Set your key: /key YOUR_API_KEY');
152
+ const title = input.version ? `✻ Welcome to Erosolar Coder ${input.version}` : '✻ Welcome to Erosolar Coder';
153
+ const body = [title, ''];
154
+ const mode = input.keyMode ?? (input.hasApiKey ? 'own' : 'none');
155
+ if (mode === 'hosted') {
156
+ // Signed in — running on hosted keys. The mode line names the account so
157
+ // it's unmistakable this is NOT the user's own key.
158
+ body.push(input.keyModeLine ?? 'Signed in · using hosted keys');
149
159
  }
150
- else {
160
+ else if (mode === 'own') {
151
161
  body.push(`${input.model} · ${input.provider}`, `Key: ${input.maskedKey} · /help for commands`);
152
162
  }
163
+ else {
164
+ body.push('⚠ No DeepSeek API key configured', '', '/login Sign in with Google for hosted keys', '', 'Or bring your own:', ' /key sk-… DeepSeek (required) · platform.deepseek.com', ' /key tvly-… Tavily web search (optional) · tavily.com');
165
+ }
153
166
  if (input.cwd)
154
167
  body.push(`cwd: ${input.cwd}`);
155
168
  return body;
@@ -272,9 +285,26 @@ class InteractiveShell {
272
285
  };
273
286
  pendingModelSwitch = null;
274
287
  currentResponseBuffer = '';
288
+ // The turn's final assistant text, captured BEFORE currentResponseBuffer is
289
+ // cleared on message.complete. The auto-continue refusal/completion/governor
290
+ // reads run in the `finally`, AFTER that clear, so reading the buffer there saw
291
+ // '' and blinded them (completion detection + safety-refusal both need the
292
+ // text). This mirrors the buffer's content but is never cleared mid-turn.
293
+ finalResponseText = '';
275
294
  // Store original prompt for auto-continuation
276
295
  originalPromptForAutoContinue = null;
277
296
  // (Pinned prompt removed per request — field intentionally absent.)
297
+ // Bounds + stall-detects the auto-continue loop per user request, and drives
298
+ // continuation from the live TODO plan (see src/core/turnGovernor.ts). Reset
299
+ // when a fresh user prompt arrives.
300
+ autoGovernor = new TurnGovernor();
301
+ // Remembers recurring error signatures across auto-continue turns so the
302
+ // agent stops re-trying the same dead end (see src/core/failureRegistry.ts).
303
+ failureRegistry = new FailureRegistry();
304
+ // Adversarial auto-correction: how many bounded re-fixes the reviewer has
305
+ // triggered for the CURRENT user request (capped). Reset on a fresh prompt;
306
+ // the findings themselves are a per-turn local in processPrompt.
307
+ adversarialCorrectionCount = 0;
278
308
  constructor(controller, profile, profileConfig, workingDir) {
279
309
  this.controller = controller;
280
310
  this.profile = profile;
@@ -345,6 +375,8 @@ class InteractiveShell {
345
375
  // Esc interrupts a running turn (handleInterrupt no-ops when idle), so
346
376
  // the spinner's "esc to interrupt" is real. Ctrl+C still works too.
347
377
  onEscape: () => this.handleInterrupt(),
378
+ onShowShortcuts: () => this.showKeyboardShortcuts(),
379
+ onDismissPanel: () => this.dismissInlinePanel(),
348
380
  });
349
381
  // Register cleanup callback for graceful shutdown
350
382
  onShutdown(() => {
@@ -492,12 +524,16 @@ class InteractiveShell {
492
524
  // WHICH lines appear; here we draw the same box with brand colour.
493
525
  const flare = chalk.hex('#ff6a1f');
494
526
  const wire = chalk.hex('#3a362e');
527
+ const keyStatus = resolveKeyMode();
495
528
  const body = welcomeBodyLines({
496
529
  hasApiKey,
497
530
  maskedKey: hasApiKey ? maskApiKey(apiKey) : '',
498
531
  model: this.profileConfig.model,
499
532
  provider: this.profileConfig.provider,
500
533
  cwd: this.workingDir,
534
+ keyMode: keyStatus.mode,
535
+ keyModeLine: keyModeLine(keyStatus),
536
+ version: `v${version}`,
501
537
  });
502
538
  const boxed = roundedBox(body, (cell) => cell.replace('✻', flare('✻')), (s) => wire(s));
503
539
  const welcomeContent = ['', ...updateLines, ...boxed, ''].join('\n');
@@ -791,26 +827,19 @@ class InteractiveShell {
791
827
  const lower = trimmed.toLowerCase();
792
828
  // /model and /secrets were removed: Erosolar is locked to deepseek-v4-pro
793
829
  // on max thought (no model switching), and /key is the one key you set.
794
- // Handle /key - shortcut to set DEEPSEEK_API_KEY
830
+ // Handle /key set your own DeepSeek OR Tavily API key. Routed by prefix:
831
+ // `sk-…` → DeepSeek (the model), `tvly-…` → Tavily (web search). Explicit
832
+ // `/key tavily <k>` / `/key deepseek <k>` also work. Bring-your-own-key is
833
+ // the model; both are stored in the OS-permission secret store.
795
834
  if (lower === '/key' || lower.startsWith('/key ')) {
796
- const parts = trimmed.split(/\s+/);
797
- const keyValue = parts[1];
798
835
  const renderer = this.promptController?.getRenderer();
799
- if (keyValue) {
800
- // Direct file write - most reliable method
836
+ const arg = trimmed.slice('/key'.length).trim();
837
+ const entry = classifyKeyEntry(arg);
838
+ if (entry) {
801
839
  try {
802
- const secretDir = join(homedir(), '.erosolar');
803
- const secretFile = join(secretDir, 'secrets.json');
804
- mkdirSync(secretDir, { recursive: true });
805
- const existing = existsSync(secretFile)
806
- ? JSON.parse(readFileSync(secretFile, 'utf-8'))
807
- : {};
808
- existing['DEEPSEEK_API_KEY'] = keyValue;
809
- writeFileSync(secretFile, JSON.stringify(existing, null, 2) + '\n');
810
- // Also set in process.env for immediate use
811
- process.env['DEEPSEEK_API_KEY'] = keyValue;
812
- // Show confirmation via renderer
813
- renderer?.addEvent('system', chalk.green('✓ DEEPSEEK_API_KEY saved'));
840
+ setSecretValue(entry.id, entry.value);
841
+ const label = getSecretDefinition(entry.id)?.label ?? entry.id;
842
+ renderer?.addEvent('system', chalk.green(`✓ ${label} saved`));
814
843
  }
815
844
  catch (error) {
816
845
  const msg = error instanceof Error ? error.message : String(error);
@@ -818,11 +847,38 @@ class InteractiveShell {
818
847
  }
819
848
  }
820
849
  else {
821
- // Show usage hint
822
- renderer?.addEvent('system', chalk.yellow('Usage: /key YOUR_API_KEY'));
850
+ renderer?.addEvent('system', chalk.yellow('Usage: /key sk-… (DeepSeek) or /key tvly-… (Tavily web search)'));
823
851
  }
824
852
  return true;
825
853
  }
854
+ // /account — show the active key source (hosted vs your own) and switch
855
+ // between them. `/account own` forces your own keys even while signed in;
856
+ // `/account hosted` returns to hosted. Hosted keys come from sign-in
857
+ // (server-side, never baked into this client) — see core/hostedAuth.ts.
858
+ if (lower === '/account' || lower.startsWith('/account ')) {
859
+ const r = this.promptController?.getRenderer();
860
+ const arg = trimmed.slice('/account'.length).trim().toLowerCase();
861
+ if (arg === 'own')
862
+ setPreferOwnKeys(true);
863
+ else if (arg === 'hosted')
864
+ setPreferOwnKeys(false);
865
+ if (arg === 'own' || arg === 'hosted')
866
+ void this.showWelcome(); // banner reflects the switch
867
+ r?.addEvent('system', this.accountStatusText(resolveKeyMode()));
868
+ return true;
869
+ }
870
+ // /login — Google sign-in via ero.solar (loopback OAuth) to unlock hosted keys.
871
+ if (lower === '/login' || lower === '/signin') {
872
+ void this.handleLogin();
873
+ return true;
874
+ }
875
+ // /logout — drop the hosted session (back to your own keys, or none).
876
+ if (lower === '/logout' || lower === '/signout') {
877
+ clearHostedSession();
878
+ this.promptController?.getRenderer()?.addEvent('system', chalk.green('✓ Signed out — using your own keys.'));
879
+ void this.showWelcome();
880
+ return true;
881
+ }
826
882
  // /update — check npm for a newer version and upgrade in-shell.
827
883
  if (lower === '/update' || lower === '/upgrade') {
828
884
  void this.handleUpdateCommand();
@@ -866,6 +922,13 @@ class InteractiveShell {
866
922
  this.showContext();
867
923
  return true;
868
924
  }
925
+ // /cost — DeepSeek tokens + Tavily searches consumed (this session + all
926
+ // time), and the hosted free-pool reference. Account-wide remaining is a
927
+ // backend number shown in the ero.solar portal.
928
+ if (lower === '/cost' || lower === '/spend') {
929
+ this.showUsage();
930
+ return true;
931
+ }
869
932
  // /diff — review the files the agent changed this run, as colored diffs.
870
933
  if (lower === '/diff' || lower === '/changes') {
871
934
  this.showDiff();
@@ -1406,6 +1469,29 @@ class InteractiveShell {
1406
1469
  this.promptController.setInlinePanel(lines);
1407
1470
  this.scheduleInlinePanelDismiss();
1408
1471
  }
1472
+ /** /cost — DeepSeek tokens + Tavily searches consumed (this install). */
1473
+ showUsage() {
1474
+ if (!this.promptController?.supportsInlinePanel()) {
1475
+ this.promptController?.setStatusMessage('Use /cost in interactive mode');
1476
+ setTimeout(() => this.promptController?.setStatusMessage(null), 3000);
1477
+ return;
1478
+ }
1479
+ const { session, cumulative } = getUsage();
1480
+ const label = (s) => chalk.hex('#ffb142')(s.padEnd(9));
1481
+ const dim = (s) => chalk.dim(s);
1482
+ const ds = (u) => `${formatTokenCount(u.deepseekInputTokens)} in · ${formatTokenCount(u.deepseekOutputTokens)} out`;
1483
+ const lines = [
1484
+ chalk.bold.hex('#ece6da')('Usage') + dim(' (press any key to dismiss)'),
1485
+ '',
1486
+ label('DeepSeek') + dim(`${ds(cumulative)} · this session ${ds(session)}`),
1487
+ label('Tavily') + dim(`${cumulative.tavilySearches} searches · this session ${session.tavilySearches}`),
1488
+ '',
1489
+ dim(`Hosted free pool: Tavily ${TAVILY_MONTHLY_FREE.toLocaleString('en-US')}/mo + ${TAVILY_ONE_TIME_BONUS.toLocaleString('en-US')} one-time bonus.`),
1490
+ dim('Account-wide totals + remaining show in the ero.solar portal after sign-in.'),
1491
+ ];
1492
+ this.promptController.setInlinePanel(lines);
1493
+ this.scheduleInlinePanelDismiss();
1494
+ }
1409
1495
  /**
1410
1496
  * /diff — review every file the agent changed this run as a colored diff,
1411
1497
  * in a dismissable panel. Reads each file's original content from the change
@@ -1489,6 +1575,54 @@ class InteractiveShell {
1489
1575
  revertAllChanges(this.workingDir); // restores/deletes on disk + clears tracking
1490
1576
  renderer?.addEvent('system', chalk.green('✓ ' + rewindResultLine(restored, deleted)));
1491
1577
  }
1578
+ /** One-line summary of the active key source for /account. */
1579
+ accountStatusText(s) {
1580
+ if (s.mode === 'hosted') {
1581
+ return chalk.green(`Hosted keys · signed in as ${s.email}.`) +
1582
+ chalk.dim(` /account own to use your own · /logout to sign out.`);
1583
+ }
1584
+ if (s.mode === 'own') {
1585
+ return chalk.green(`Your own keys · DeepSeek${s.ownTavily ? ' + Tavily' : ''}.`) +
1586
+ chalk.dim(s.signedIn ? ` /account hosted to use hosted keys.` : ` /login to use hosted keys.`);
1587
+ }
1588
+ return chalk.yellow('No keys configured.') +
1589
+ chalk.dim(' /login for hosted keys, or set your own: /key sk-… (and /key tvly-…).');
1590
+ }
1591
+ /**
1592
+ * /login — Google sign-in via ero.solar. Opens the browser to the SSO URL and
1593
+ * runs a one-shot 127.0.0.1 loopback server that captures the redirect with
1594
+ * the short-lived token (see core/hostedAuth.ts). On success the CLI is on
1595
+ * hosted keys; no key ever touches this client.
1596
+ */
1597
+ async handleLogin() {
1598
+ const r = this.promptController?.getRenderer();
1599
+ const status = resolveKeyMode();
1600
+ if (status.signedIn) {
1601
+ r?.addEvent('system', chalk.green(`Already signed in as ${status.email}.`) +
1602
+ chalk.dim(' /logout to sign out · /account to switch key source.'));
1603
+ return;
1604
+ }
1605
+ r?.addEvent('system', chalk.dim('Opening ero.solar sign-in in your browser — finish there, then return here…'));
1606
+ const result = await loginViaLoopback({ open: (url) => this.openInBrowser(url) });
1607
+ if (result.ok && result.session) {
1608
+ r?.addEvent('system', chalk.green(`✓ Signed in as ${result.session.email} — using hosted keys.`));
1609
+ void this.showWelcome();
1610
+ }
1611
+ else {
1612
+ r?.addEvent('system', chalk.yellow(`Sign-in didn't complete: ${result.error ?? 'unknown error'}.`) +
1613
+ chalk.dim(' Retry /login, or use /key sk-… for your own key.'));
1614
+ }
1615
+ }
1616
+ /** Best-effort open a URL in the OS browser; also prints it as a fallback. */
1617
+ openInBrowser(url) {
1618
+ const opener = process.platform === 'darwin' ? 'open'
1619
+ : process.platform === 'win32' ? 'start ""'
1620
+ : 'xdg-open';
1621
+ // url is built by loginViaLoopback (no user input) and JSON-quoted, so the
1622
+ // `&` in the query string can't break out of the argument.
1623
+ childExec(`${opener} ${JSON.stringify(url)}`, () => { });
1624
+ this.promptController?.getRenderer()?.addEvent('system', chalk.dim(`If the browser didn't open: ${url}`));
1625
+ }
1492
1626
  showHelp() {
1493
1627
  if (!this.promptController?.supportsInlinePanel()) {
1494
1628
  this.promptController?.setStatusMessage('Help: /key sk-… (everything else is automatic)');
@@ -1503,14 +1637,20 @@ class InteractiveShell {
1503
1637
  const lines = [
1504
1638
  chalk.bold.hex('#ece6da')('Erosolar Coder') + dim(' (press any key to dismiss)'),
1505
1639
  '',
1506
- cmd('/key sk-…') + dim(' Set your DeepSeek API key'),
1640
+ cmd('/login') + dim(' Sign in with Google (ero.solar) to use hosted keys'),
1641
+ cmd('/key sk-…') + dim(' Set your DeepSeek API key (required)'),
1642
+ cmd('/key tvly-…') + dim(' Set your Tavily key for web search (optional)'),
1643
+ cmd('/account') + dim(' Show / switch key source (hosted vs your own)'),
1507
1644
  cmd('/update') + dim(' Check npm and upgrade to the latest version'),
1508
1645
  cmd('/resume') + dim(' Restore a previous conversation'),
1509
1646
  cmd('/context') + dim(' Show context-window usage'),
1647
+ cmd('/cost') + dim(' DeepSeek tokens + Tavily searches consumed'),
1510
1648
  cmd('/diff') + dim(' Review changes made this run'),
1511
1649
  cmd('/rewind') + dim(' Undo this run\'s file changes'),
1512
1650
  '',
1513
- dim('Everything else runs automatically for max performance '),
1651
+ dim('Prefixes: ') + cmd('@file') + dim(' attach · ') + cmd('!cmd') + dim(' run shell · ') + cmd('#note') + dim(' save to memory'),
1652
+ '',
1653
+ dim('Everything else runs automatically —'),
1514
1654
  dim('deepseek-v4-pro · max thought · ultracode · adversarial verifier, all on.'),
1515
1655
  dim('Shift+Tab cycles permission mode · Ctrl+D exits · ? for shortcuts'),
1516
1656
  ];
@@ -1609,6 +1749,19 @@ class InteractiveShell {
1609
1749
  void this.runLocalCommand(trimmed.slice(1).trim());
1610
1750
  return;
1611
1751
  }
1752
+ // `#note` — quick-capture a note to persistent project memory (Claude Code
1753
+ // parity), no model round-trip. Lands in .erosolar/memory/ where the agent
1754
+ // reads it on later sessions.
1755
+ if (trimmed.startsWith('#')) {
1756
+ this.dismissInlinePanel();
1757
+ const note = trimmed.slice(1).trim();
1758
+ const r = this.promptController?.getRenderer();
1759
+ if (appendMemoryNote(this.workingDir, note))
1760
+ r?.addEvent('system', chalk.green('✓ Saved to memory'));
1761
+ else
1762
+ r?.addEvent('system', chalk.yellow('Usage: #<note to remember>'));
1763
+ return;
1764
+ }
1612
1765
  // Dismiss inline panel for regular user prompts
1613
1766
  this.dismissInlinePanel();
1614
1767
  // Live follow-up queue (Claude Code parity): a prompt typed while the agent
@@ -1645,18 +1798,29 @@ class InteractiveShell {
1645
1798
  // A fresh user prompt clears any prior interrupt state — this is new
1646
1799
  // work the user actually wants done.
1647
1800
  this.userInterruptedRun = false;
1801
+ // Fresh user request → start a new auto-continue turn budget + failure log.
1802
+ this.autoGovernor.reset();
1803
+ this.failureRegistry.reset();
1804
+ this.adversarialCorrectionCount = 0;
1648
1805
  // Pinned-prompt persistence removed per request — no longer
1649
1806
  // displayed above the chat box.
1650
1807
  }
1651
1808
  enterCriticalSection();
1652
1809
  this.isProcessing = true;
1653
1810
  this.currentResponseBuffer = '';
1811
+ this.finalResponseText = '';
1654
1812
  this.promptController?.setStreaming(true);
1655
1813
  this.promptController?.setStatusMessage('Analyzing request…');
1656
1814
  const renderer = this.promptController?.getRenderer();
1657
1815
  let episodeSuccess = false;
1658
1816
  const toolsUsed = [];
1659
1817
  const filesModified = [];
1818
+ // Tail of this turn's tool outputs (where TS/test/build errors land), so the
1819
+ // failure registry + governor see real error text, not just the narration.
1820
+ let turnToolOutput = '';
1821
+ // Reviewer findings from THIS turn (set by the adversarial.findings event),
1822
+ // used in the finally to drive a bounded auto-correction.
1823
+ let turnAdversarialFindings = null;
1660
1824
  // Track reasoning content for fallback when response is empty
1661
1825
  let reasoningBuffer = '';
1662
1826
  // Track reasoning-only time to prevent models from reasoning forever without action
@@ -1713,6 +1877,7 @@ class InteractiveShell {
1713
1877
  case 'message.start':
1714
1878
  // AI has started processing - update status to show activity
1715
1879
  this.currentResponseBuffer = '';
1880
+ this.finalResponseText = '';
1716
1881
  reasoningBuffer = '';
1717
1882
  reasoningOnlyStartTime = null; // Reset on new message
1718
1883
  this.promptController?.setStatusMessage('Thinking...');
@@ -1720,6 +1885,7 @@ class InteractiveShell {
1720
1885
  case 'message.delta':
1721
1886
  // Stream content as it arrives
1722
1887
  this.currentResponseBuffer += event.content ?? '';
1888
+ this.finalResponseText += event.content ?? '';
1723
1889
  if (renderer) {
1724
1890
  renderer.addEvent('stream', event.content);
1725
1891
  }
@@ -1784,6 +1950,9 @@ class InteractiveShell {
1784
1950
  }
1785
1951
  }
1786
1952
  renderer.addEvent('response', '\n');
1953
+ // Capture the authoritative final text BEFORE the buffer is cleared
1954
+ // (the finally's auto-continue reads run after this clear).
1955
+ this.finalResponseText = sourceText || this.finalResponseText;
1787
1956
  }
1788
1957
  this.currentResponseBuffer = '';
1789
1958
  break;
@@ -1820,6 +1989,11 @@ class InteractiveShell {
1820
1989
  if (isHitlToolName(event.toolName)) {
1821
1990
  hitlDepth = Math.max(0, hitlDepth - 1);
1822
1991
  }
1992
+ // Keep the tail of tool output for the failure registry / governor
1993
+ // (errors land here, not in the assistant narration).
1994
+ if (typeof event.result === 'string' && event.result) {
1995
+ turnToolOutput = (turnToolOutput + '\n' + event.result).slice(-16000);
1996
+ }
1823
1997
  // Clear the activity label; the agent is thinking again.
1824
1998
  this.promptController?.setStatusMessage('Thinking…');
1825
1999
  // Reset reasoning timer after tool completes
@@ -1857,6 +2031,8 @@ class InteractiveShell {
1857
2031
  }
1858
2032
  break;
1859
2033
  case 'usage': {
2034
+ // Meter cumulative DeepSeek consumption for /usage + the portal.
2035
+ recordDeepSeekUsage(event.inputTokens, event.outputTokens);
1860
2036
  // inputTokens = exactly what occupies the context window this turn.
1861
2037
  // The real model window (not a hardcoded guess) is the denominator
1862
2038
  // so "% context left" reflects the actual model.
@@ -1883,6 +2059,11 @@ class InteractiveShell {
1883
2059
  elapsedMs: event.elapsedMs,
1884
2060
  })));
1885
2061
  break;
2062
+ case 'adversarial.findings':
2063
+ // The reviewer refuted this turn's draft — remember it so the
2064
+ // auto-continue loop can run a bounded re-fix (handled in finally).
2065
+ turnAdversarialFindings = event.findings;
2066
+ break;
1886
2067
  case 'context.compacted': {
1887
2068
  // The conversation was auto-compacted to stay within the window —
1888
2069
  // surface it as a dim note (Claude Code parity) instead of silently.
@@ -2007,7 +2188,7 @@ class InteractiveShell {
2007
2188
  // model declines the request, the request is *done* — auto-continue
2008
2189
  // would just resubmit "continue" and start a new spinner cycle, which
2009
2190
  // is what produced the stuck "Thinking… (4m N s)" timer the user saw.
2010
- const refusedTurn = isSafetyRefusal(this.currentResponseBuffer);
2191
+ const refusedTurn = isSafetyRefusal(this.finalResponseText);
2011
2192
  this.isProcessing = false;
2012
2193
  this.promptController?.setStreaming(false);
2013
2194
  this.promptController?.setStatusMessage(null);
@@ -2028,6 +2209,10 @@ class InteractiveShell {
2028
2209
  r?.setQueuedPrompts([]);
2029
2210
  // Note: pendingPrompts may still have items if a drain just started
2030
2211
  // a new processPrompt; the new run will manage the list.
2212
+ // Snapshot this turn's full output (tool results + narration) BEFORE the
2213
+ // buffer is cleared — the auto-continue governor + failure registry need
2214
+ // the real error text, which the reset below would otherwise wipe.
2215
+ const combinedTurnOutput = (turnToolOutput + '\n' + this.finalResponseText).slice(-16000);
2031
2216
  this.currentResponseBuffer = '';
2032
2217
  // Autosave the conversation so /resume has something to restore. Each
2033
2218
  // turn updates the same snapshot in place (keyed by this.sessionId).
@@ -2059,20 +2244,55 @@ class InteractiveShell {
2059
2244
  if (autoMode !== 'off') {
2060
2245
  // Check if original user prompt is fully completed
2061
2246
  const detector = getTaskCompletionDetector();
2062
- const analysis = detector.analyzeCompletion(this.currentResponseBuffer, toolsUsed);
2063
- // Continue until task is complete
2064
- if (!analysis.isComplete) {
2247
+ const analysis = detector.analyzeCompletion(this.finalResponseText, toolsUsed);
2248
+ // Record this turn with the governor (bounds the loop + detects a
2249
+ // stall: the same tools/files/failure repeating with no new progress)
2250
+ // and the failure registry (catches the same error recurring across
2251
+ // NON-consecutive turns — a thrash the stall check would miss).
2252
+ this.autoGovernor.recordTurn({
2253
+ toolsUsed,
2254
+ filesModified,
2255
+ failingSignal: detectFailingTestOrBuild(combinedTurnOutput),
2256
+ });
2257
+ this.failureRegistry.trackTurn(combinedTurnOutput);
2258
+ const gov = this.autoGovernor.check();
2259
+ const failureNudge = this.failureRegistry.nudge();
2260
+ const todos = getCurrentTodos();
2261
+ const pending = pendingTodos(todos);
2262
+ if (gov.stop) {
2263
+ // Yield to the user WITH state instead of thrashing forever.
2264
+ const note = gov.reason === 'limit'
2265
+ ? `Paused after ${gov.turn} auto-continue turns (turn limit).${pending.length ? ` ${pending.length} task${pending.length === 1 ? '' : 's'} still pending` : ''} — say "continue" to keep going.`
2266
+ : `Paused: no new progress over the last few turns (same actions repeating).${pending.length ? ` ${pending.length} task${pending.length === 1 ? '' : 's'} pending` : ''} — tell me how to proceed.`;
2267
+ this.promptController?.getRenderer()?.addEvent('system', chalk.dim(note));
2268
+ this.promptController?.setStatusMessage(null);
2269
+ this.originalPromptForAutoContinue = null;
2270
+ }
2271
+ else if (turnAdversarialFindings && this.adversarialCorrectionCount < MAX_ADVERSARIAL_CORRECTIONS) {
2272
+ // The reviewer refuted this turn's draft — re-run the FULL tool loop
2273
+ // to actually fix the findings (not just show the caveat), bounded
2274
+ // by the governor + this per-request cap.
2275
+ this.adversarialCorrectionCount += 1;
2276
+ this.promptController?.setStatusMessage('Addressing reviewer findings…');
2277
+ await new Promise(resolve => setTimeout(resolve, 300));
2278
+ await this.processPrompt(buildAdversarialCorrectionPrompt(turnAdversarialFindings));
2279
+ }
2280
+ else if (!analysis.isComplete || pending.length > 0) {
2281
+ // Continue — but only stop when the LIVE PLAN is also clear: pending
2282
+ // todos force a continue even if the response sounded "done".
2065
2283
  this.promptController?.setStatusMessage('Continuing...');
2066
2284
  await new Promise(resolve => setTimeout(resolve, 500));
2067
- // Generate auto-continue prompt using stored original prompt
2068
- const autoPrompt = this.generateAutoContinuePrompt(this.originalPromptForAutoContinue || '', this.currentResponseBuffer, toolsUsed);
2069
- if (autoPrompt) {
2070
- await this.processPrompt(autoPrompt);
2071
- }
2072
- else {
2073
- // Default continue if no specific auto-prompt generated
2074
- await this.processPrompt('continue');
2075
- }
2285
+ // Prefer the plan's next task; fall back to the response heuristic.
2286
+ const base = nextTodoPrompt(todos)
2287
+ ?? this.generateAutoContinuePrompt(this.originalPromptForAutoContinue || '', combinedTurnOutput, toolsUsed)
2288
+ ?? 'continue';
2289
+ // When a failure keeps recurring, lead with the change-approach nudge.
2290
+ // Keep an IMPORTANT: prefix so this counts as an auto-continue (not a
2291
+ // fresh user prompt, which would reset the governor).
2292
+ const autoPrompt = failureNudge
2293
+ ? `IMPORTANT: ${failureNudge}\n\n${base.replace(/^IMPORTANT:\s*/, '')}`
2294
+ : base;
2295
+ await this.processPrompt(autoPrompt);
2076
2296
  }
2077
2297
  else {
2078
2298
  this.promptController?.setStatusMessage('Task complete');