@trenchwork/erosolar 1.1.63 → 1.2.1

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 (185) hide show
  1. package/dist/core/agent.d.ts.map +1 -1
  2. package/dist/core/agent.js +4 -48
  3. package/dist/core/agent.js.map +1 -1
  4. package/dist/core/contextManager.d.ts.map +1 -1
  5. package/dist/core/contextManager.js +5 -2
  6. package/dist/core/contextManager.js.map +1 -1
  7. package/dist/core/errorClassification.d.ts +44 -0
  8. package/dist/core/errorClassification.d.ts.map +1 -0
  9. package/dist/core/errorClassification.js +333 -0
  10. package/dist/core/errorClassification.js.map +1 -0
  11. package/dist/core/hitl.d.ts.map +1 -1
  12. package/dist/core/hitl.js +8 -0
  13. package/dist/core/hitl.js.map +1 -1
  14. package/dist/core/hostedAuth.d.ts +33 -0
  15. package/dist/core/hostedAuth.d.ts.map +1 -1
  16. package/dist/core/hostedAuth.js +85 -0
  17. package/dist/core/hostedAuth.js.map +1 -1
  18. package/dist/core/inputProtection.js +1 -1
  19. package/dist/core/inputProtection.js.map +1 -1
  20. package/dist/core/quota.d.ts +61 -0
  21. package/dist/core/quota.d.ts.map +1 -0
  22. package/dist/core/quota.js +104 -0
  23. package/dist/core/quota.js.map +1 -0
  24. package/dist/core/quotaErrors.d.ts.map +1 -1
  25. package/dist/core/quotaErrors.js +3 -5
  26. package/dist/core/quotaErrors.js.map +1 -1
  27. package/dist/core/resultVerification.d.ts +3 -2
  28. package/dist/core/resultVerification.d.ts.map +1 -1
  29. package/dist/core/resultVerification.js +3 -2
  30. package/dist/core/resultVerification.js.map +1 -1
  31. package/dist/core/slashCommands.d.ts.map +1 -1
  32. package/dist/core/slashCommands.js +3 -0
  33. package/dist/core/slashCommands.js.map +1 -1
  34. package/dist/core/updateChecker.d.ts.map +1 -1
  35. package/dist/core/updateChecker.js +5 -1
  36. package/dist/core/updateChecker.js.map +1 -1
  37. package/dist/core/usage.d.ts +28 -0
  38. package/dist/core/usage.d.ts.map +1 -0
  39. package/dist/core/usage.js +77 -0
  40. package/dist/core/usage.js.map +1 -0
  41. package/dist/headless/interactiveShell.d.ts +2 -0
  42. package/dist/headless/interactiveShell.d.ts.map +1 -1
  43. package/dist/headless/interactiveShell.js +99 -9
  44. package/dist/headless/interactiveShell.js.map +1 -1
  45. package/dist/plugins/providers/deepseek/index.d.ts.map +1 -1
  46. package/dist/plugins/providers/deepseek/index.js +8 -5
  47. package/dist/plugins/providers/deepseek/index.js.map +1 -1
  48. package/dist/providers/baseProvider.d.ts +5 -13
  49. package/dist/providers/baseProvider.d.ts.map +1 -1
  50. package/dist/providers/baseProvider.js +12 -66
  51. package/dist/providers/baseProvider.js.map +1 -1
  52. package/dist/providers/openaiChatCompletionsProvider.d.ts.map +1 -1
  53. package/dist/providers/openaiChatCompletionsProvider.js +27 -76
  54. package/dist/providers/openaiChatCompletionsProvider.js.map +1 -1
  55. package/dist/providers/resilientProvider.d.ts +2 -9
  56. package/dist/providers/resilientProvider.d.ts.map +1 -1
  57. package/dist/providers/resilientProvider.js +13 -199
  58. package/dist/providers/resilientProvider.js.map +1 -1
  59. package/dist/shell/toolPresentation.d.ts.map +1 -1
  60. package/dist/shell/toolPresentation.js +27 -3
  61. package/dist/shell/toolPresentation.js.map +1 -1
  62. package/dist/tools/bashTools.d.ts.map +1 -1
  63. package/dist/tools/bashTools.js +9 -3
  64. package/dist/tools/bashTools.js.map +1 -1
  65. package/dist/tools/grepTools.d.ts.map +1 -1
  66. package/dist/tools/grepTools.js +10 -1
  67. package/dist/tools/grepTools.js.map +1 -1
  68. package/dist/tools/searchTools.d.ts.map +1 -1
  69. package/dist/tools/searchTools.js +5 -4
  70. package/dist/tools/searchTools.js.map +1 -1
  71. package/dist/tools/webTools.d.ts.map +1 -1
  72. package/dist/tools/webTools.js +3 -1
  73. package/dist/tools/webTools.js.map +1 -1
  74. package/dist/ui/ink/ChatStatic.d.ts.map +1 -1
  75. package/dist/ui/ink/ChatStatic.js +21 -5
  76. package/dist/ui/ink/ChatStatic.js.map +1 -1
  77. package/dist/ui/ink/InkPromptController.d.ts +3 -0
  78. package/dist/ui/ink/InkPromptController.d.ts.map +1 -1
  79. package/dist/ui/ink/InkPromptController.js +12 -6
  80. package/dist/ui/ink/InkPromptController.js.map +1 -1
  81. package/dist/ui/ink/Prompt.d.ts +4 -0
  82. package/dist/ui/ink/Prompt.d.ts.map +1 -1
  83. package/dist/ui/ink/Prompt.js +62 -10
  84. package/dist/ui/ink/Prompt.js.map +1 -1
  85. package/dist/ui/ink/pasteBuffer.d.ts +44 -0
  86. package/dist/ui/ink/pasteBuffer.d.ts.map +1 -0
  87. package/dist/ui/ink/pasteBuffer.js +73 -0
  88. package/dist/ui/ink/pasteBuffer.js.map +1 -0
  89. package/package.json +1 -1
  90. package/dist/core/index.d.ts +0 -7
  91. package/dist/core/index.d.ts.map +0 -1
  92. package/dist/core/index.js +0 -7
  93. package/dist/core/index.js.map +0 -1
  94. package/dist/core/providerKeys.d.ts +0 -20
  95. package/dist/core/providerKeys.d.ts.map +0 -1
  96. package/dist/core/providerKeys.js +0 -40
  97. package/dist/core/providerKeys.js.map +0 -1
  98. package/dist/plugins/index.d.ts +0 -49
  99. package/dist/plugins/index.d.ts.map +0 -1
  100. package/dist/plugins/index.js +0 -104
  101. package/dist/plugins/index.js.map +0 -1
  102. package/dist/plugins/tools/agentSpawning/agentSpawningPlugin.d.ts +0 -10
  103. package/dist/plugins/tools/agentSpawning/agentSpawningPlugin.d.ts.map +0 -1
  104. package/dist/plugins/tools/agentSpawning/agentSpawningPlugin.js +0 -110
  105. package/dist/plugins/tools/agentSpawning/agentSpawningPlugin.js.map +0 -1
  106. package/dist/plugins/tools/bash/localBashPlugin.d.ts +0 -3
  107. package/dist/plugins/tools/bash/localBashPlugin.d.ts.map +0 -1
  108. package/dist/plugins/tools/bash/localBashPlugin.js +0 -14
  109. package/dist/plugins/tools/bash/localBashPlugin.js.map +0 -1
  110. package/dist/plugins/tools/edit/editPlugin.d.ts +0 -9
  111. package/dist/plugins/tools/edit/editPlugin.d.ts.map +0 -1
  112. package/dist/plugins/tools/edit/editPlugin.js +0 -15
  113. package/dist/plugins/tools/edit/editPlugin.js.map +0 -1
  114. package/dist/plugins/tools/enhancedGit/enhancedGitPlugin.d.ts +0 -3
  115. package/dist/plugins/tools/enhancedGit/enhancedGitPlugin.d.ts.map +0 -1
  116. package/dist/plugins/tools/enhancedGit/enhancedGitPlugin.js +0 -9
  117. package/dist/plugins/tools/enhancedGit/enhancedGitPlugin.js.map +0 -1
  118. package/dist/plugins/tools/filesystem/localFilesystemPlugin.d.ts +0 -3
  119. package/dist/plugins/tools/filesystem/localFilesystemPlugin.d.ts.map +0 -1
  120. package/dist/plugins/tools/filesystem/localFilesystemPlugin.js +0 -14
  121. package/dist/plugins/tools/filesystem/localFilesystemPlugin.js.map +0 -1
  122. package/dist/plugins/tools/gitHistory/gitHistoryPlugin.d.ts +0 -3
  123. package/dist/plugins/tools/gitHistory/gitHistoryPlugin.d.ts.map +0 -1
  124. package/dist/plugins/tools/gitHistory/gitHistoryPlugin.js +0 -9
  125. package/dist/plugins/tools/gitHistory/gitHistoryPlugin.js.map +0 -1
  126. package/dist/plugins/tools/index.d.ts +0 -3
  127. package/dist/plugins/tools/index.d.ts.map +0 -1
  128. package/dist/plugins/tools/index.js +0 -3
  129. package/dist/plugins/tools/index.js.map +0 -1
  130. package/dist/plugins/tools/integrity/integrityPlugin.d.ts +0 -3
  131. package/dist/plugins/tools/integrity/integrityPlugin.d.ts.map +0 -1
  132. package/dist/plugins/tools/integrity/integrityPlugin.js +0 -31
  133. package/dist/plugins/tools/integrity/integrityPlugin.js.map +0 -1
  134. package/dist/plugins/tools/mcp/mcpPlugin.d.ts +0 -3
  135. package/dist/plugins/tools/mcp/mcpPlugin.d.ts.map +0 -1
  136. package/dist/plugins/tools/mcp/mcpPlugin.js +0 -27
  137. package/dist/plugins/tools/mcp/mcpPlugin.js.map +0 -1
  138. package/dist/plugins/tools/nodeDefaults.d.ts +0 -13
  139. package/dist/plugins/tools/nodeDefaults.d.ts.map +0 -1
  140. package/dist/plugins/tools/nodeDefaults.js +0 -33
  141. package/dist/plugins/tools/nodeDefaults.js.map +0 -1
  142. package/dist/plugins/tools/orchestration/orchestrationPlugin.d.ts +0 -3
  143. package/dist/plugins/tools/orchestration/orchestrationPlugin.d.ts.map +0 -1
  144. package/dist/plugins/tools/orchestration/orchestrationPlugin.js +0 -340
  145. package/dist/plugins/tools/orchestration/orchestrationPlugin.js.map +0 -1
  146. package/dist/plugins/tools/registry.d.ts +0 -22
  147. package/dist/plugins/tools/registry.d.ts.map +0 -1
  148. package/dist/plugins/tools/registry.js +0 -58
  149. package/dist/plugins/tools/registry.js.map +0 -1
  150. package/dist/plugins/tools/search/localSearchPlugin.d.ts +0 -3
  151. package/dist/plugins/tools/search/localSearchPlugin.d.ts.map +0 -1
  152. package/dist/plugins/tools/search/localSearchPlugin.js +0 -14
  153. package/dist/plugins/tools/search/localSearchPlugin.js.map +0 -1
  154. package/dist/plugins/tools/skills/skillPlugin.d.ts +0 -3
  155. package/dist/plugins/tools/skills/skillPlugin.d.ts.map +0 -1
  156. package/dist/plugins/tools/skills/skillPlugin.js +0 -27
  157. package/dist/plugins/tools/skills/skillPlugin.js.map +0 -1
  158. package/dist/plugins/tools/todo/todoPlugin.d.ts +0 -3
  159. package/dist/plugins/tools/todo/todoPlugin.d.ts.map +0 -1
  160. package/dist/plugins/tools/todo/todoPlugin.js +0 -10
  161. package/dist/plugins/tools/todo/todoPlugin.js.map +0 -1
  162. package/dist/runtime/agentWorkerPool.d.ts +0 -167
  163. package/dist/runtime/agentWorkerPool.d.ts.map +0 -1
  164. package/dist/runtime/agentWorkerPool.js +0 -435
  165. package/dist/runtime/agentWorkerPool.js.map +0 -1
  166. package/dist/shell/autoExecutor.d.ts +0 -70
  167. package/dist/shell/autoExecutor.d.ts.map +0 -1
  168. package/dist/shell/autoExecutor.js +0 -320
  169. package/dist/shell/autoExecutor.js.map +0 -1
  170. package/dist/shell/commandRegistry.d.ts +0 -122
  171. package/dist/shell/commandRegistry.d.ts.map +0 -1
  172. package/dist/shell/commandRegistry.js +0 -355
  173. package/dist/shell/commandRegistry.js.map +0 -1
  174. package/dist/shell/composableMessage.d.ts +0 -178
  175. package/dist/shell/composableMessage.d.ts.map +0 -1
  176. package/dist/shell/composableMessage.js +0 -384
  177. package/dist/shell/composableMessage.js.map +0 -1
  178. package/dist/shell/vimMode.d.ts +0 -66
  179. package/dist/shell/vimMode.d.ts.map +0 -1
  180. package/dist/shell/vimMode.js +0 -435
  181. package/dist/shell/vimMode.js.map +0 -1
  182. package/dist/tools/localExplore.d.ts +0 -38
  183. package/dist/tools/localExplore.d.ts.map +0 -1
  184. package/dist/tools/localExplore.js +0 -30
  185. package/dist/tools/localExplore.js.map +0 -1
@@ -26,6 +26,8 @@ export interface WelcomeLineInput {
26
26
  keyMode?: 'hosted' | 'own' | 'none';
27
27
  /** Dim status line for the active key source (hosted/own). */
28
28
  keyModeLine?: string | null;
29
+ /** Display version (e.g. "v1.1.63"), shown in the welcome title. */
30
+ version?: string;
29
31
  }
30
32
  /**
31
33
  * Compose the lines shown when the interactive shell opens. Deliberately NOT a
@@ -1 +1 @@
1
- {"version":3,"file":"interactiveShell.d.ts","sourceRoot":"","sources":["../../src/headless/interactiveShell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAmKH,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,OAAO,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IACpC,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAgDD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,EAAE,CAErE;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmDzF"}
1
+ {"version":3,"file":"interactiveShell.d.ts","sourceRoot":"","sources":["../../src/headless/interactiveShell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAoKH,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,OAAO,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IACpC,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAmDD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,EAAE,CAErE;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmDzF"}
@@ -32,8 +32,9 @@ import { createAgentController } from '../runtime/agentController.js';
32
32
  import { expandFileMentions, listWorkspaceFiles } from '../core/fileMentions.js';
33
33
  import { resolveWorkspaceCaptureOptions, buildWorkspaceContext } from '../workspace.js';
34
34
  import { loadAllSecrets, listSecretDefinitions, setSecretValue, getSecretValue, getSecretDefinition, classifyKeyEntry } from '../core/secretStore.js';
35
- import { resolveKeyMode, keyModeLine, setPreferOwnKeys, clearHostedSession } from '../core/hostedAuth.js';
35
+ import { resolveKeyMode, keyModeLine, setPreferOwnKeys, clearHostedSession, loginViaLoopback } from '../core/hostedAuth.js';
36
36
  import { appendMemoryNote } from '../tools/memoryTools.js';
37
+ import { recordDeepSeekUsage, getUsage, TAVILY_MONTHLY_FREE, TAVILY_ONE_TIME_BONUS } from '../core/usage.js';
37
38
  import { listSessions, loadSessionById, saveSessionSnapshot } from '../core/sessionStore.js';
38
39
  import { relativeTime } from '../core/relativeTime.js';
39
40
  import { getModelContextInfo } from '../core/contextWindow.js';
@@ -148,7 +149,8 @@ function getVersion() {
148
149
  }
149
150
  /** Inner content of the welcome box (plain, no border/colour). */
150
151
  function welcomeBodyLines(input) {
151
- const body = ['✻ Welcome to Erosolar Coder', ''];
152
+ const title = input.version ? `✻ Welcome to Erosolar Coder ${input.version}` : '✻ Welcome to Erosolar Coder';
153
+ const body = [title, ''];
152
154
  const mode = input.keyMode ?? (input.hasApiKey ? 'own' : 'none');
153
155
  if (mode === 'hosted') {
154
156
  // Signed in — running on hosted keys. The mode line names the account so
@@ -159,7 +161,7 @@ function welcomeBodyLines(input) {
159
161
  body.push(`${input.model} · ${input.provider}`, `Key: ${input.maskedKey} · /help for commands`);
160
162
  }
161
163
  else {
162
- body.push('⚠ No DeepSeek API key configured', '', 'Bring your own keys:', ' /key sk-… DeepSeek (required) · platform.deepseek.com', ' /key tvly-… Tavily web search (optional) · tavily.com');
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');
163
165
  }
164
166
  if (input.cwd)
165
167
  body.push(`cwd: ${input.cwd}`);
@@ -283,6 +285,12 @@ class InteractiveShell {
283
285
  };
284
286
  pendingModelSwitch = null;
285
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 = '';
286
294
  // Store original prompt for auto-continuation
287
295
  originalPromptForAutoContinue = null;
288
296
  // (Pinned prompt removed per request — field intentionally absent.)
@@ -368,6 +376,7 @@ class InteractiveShell {
368
376
  // the spinner's "esc to interrupt" is real. Ctrl+C still works too.
369
377
  onEscape: () => this.handleInterrupt(),
370
378
  onShowShortcuts: () => this.showKeyboardShortcuts(),
379
+ onDismissPanel: () => this.dismissInlinePanel(),
371
380
  });
372
381
  // Register cleanup callback for graceful shutdown
373
382
  onShutdown(() => {
@@ -524,6 +533,7 @@ class InteractiveShell {
524
533
  cwd: this.workingDir,
525
534
  keyMode: keyStatus.mode,
526
535
  keyModeLine: keyModeLine(keyStatus),
536
+ version: `v${version}`,
527
537
  });
528
538
  const boxed = roundedBox(body, (cell) => cell.replace('✻', flare('✻')), (s) => wire(s));
529
539
  const welcomeContent = ['', ...updateLines, ...boxed, ''].join('\n');
@@ -857,6 +867,11 @@ class InteractiveShell {
857
867
  r?.addEvent('system', this.accountStatusText(resolveKeyMode()));
858
868
  return true;
859
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
+ }
860
875
  // /logout — drop the hosted session (back to your own keys, or none).
861
876
  if (lower === '/logout' || lower === '/signout') {
862
877
  clearHostedSession();
@@ -907,6 +922,13 @@ class InteractiveShell {
907
922
  this.showContext();
908
923
  return true;
909
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
+ }
910
932
  // /diff — review the files the agent changed this run, as colored diffs.
911
933
  if (lower === '/diff' || lower === '/changes') {
912
934
  this.showDiff();
@@ -1447,6 +1469,29 @@ class InteractiveShell {
1447
1469
  this.promptController.setInlinePanel(lines);
1448
1470
  this.scheduleInlinePanelDismiss();
1449
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
+ }
1450
1495
  /**
1451
1496
  * /diff — review every file the agent changed this run as a colored diff,
1452
1497
  * in a dismissable panel. Reads each file's original content from the change
@@ -1538,10 +1583,45 @@ class InteractiveShell {
1538
1583
  }
1539
1584
  if (s.mode === 'own') {
1540
1585
  return chalk.green(`Your own keys · DeepSeek${s.ownTavily ? ' + Tavily' : ''}.`) +
1541
- chalk.dim(s.signedIn ? ` /account hosted to use hosted keys.` : ` Sign-in for hosted keys is coming.`);
1586
+ chalk.dim(s.signedIn ? ` /account hosted to use hosted keys.` : ` /login to use hosted keys.`);
1542
1587
  }
1543
1588
  return chalk.yellow('No keys configured.') +
1544
- chalk.dim(' Set your own: /key sk-… (and /key tvly-…). Hosted sign-in is coming.');
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}`));
1545
1625
  }
1546
1626
  showHelp() {
1547
1627
  if (!this.promptController?.supportsInlinePanel()) {
@@ -1557,18 +1637,20 @@ class InteractiveShell {
1557
1637
  const lines = [
1558
1638
  chalk.bold.hex('#ece6da')('Erosolar Coder') + dim(' (press any key to dismiss)'),
1559
1639
  '',
1640
+ cmd('/login') + dim(' Sign in with Google (ero.solar) to use hosted keys'),
1560
1641
  cmd('/key sk-…') + dim(' Set your DeepSeek API key (required)'),
1561
1642
  cmd('/key tvly-…') + dim(' Set your Tavily key for web search (optional)'),
1562
1643
  cmd('/account') + dim(' Show / switch key source (hosted vs your own)'),
1563
1644
  cmd('/update') + dim(' Check npm and upgrade to the latest version'),
1564
1645
  cmd('/resume') + dim(' Restore a previous conversation'),
1565
1646
  cmd('/context') + dim(' Show context-window usage'),
1647
+ cmd('/cost') + dim(' DeepSeek tokens + Tavily searches consumed'),
1566
1648
  cmd('/diff') + dim(' Review changes made this run'),
1567
1649
  cmd('/rewind') + dim(' Undo this run\'s file changes'),
1568
1650
  '',
1569
1651
  dim('Prefixes: ') + cmd('@file') + dim(' attach · ') + cmd('!cmd') + dim(' run shell · ') + cmd('#note') + dim(' save to memory'),
1570
1652
  '',
1571
- dim('Everything else runs automatically for max performance —'),
1653
+ dim('Everything else runs automatically —'),
1572
1654
  dim('deepseek-v4-pro · max thought · ultracode · adversarial verifier, all on.'),
1573
1655
  dim('Shift+Tab cycles permission mode · Ctrl+D exits · ? for shortcuts'),
1574
1656
  ];
@@ -1726,6 +1808,7 @@ class InteractiveShell {
1726
1808
  enterCriticalSection();
1727
1809
  this.isProcessing = true;
1728
1810
  this.currentResponseBuffer = '';
1811
+ this.finalResponseText = '';
1729
1812
  this.promptController?.setStreaming(true);
1730
1813
  this.promptController?.setStatusMessage('Analyzing request…');
1731
1814
  const renderer = this.promptController?.getRenderer();
@@ -1794,6 +1877,7 @@ class InteractiveShell {
1794
1877
  case 'message.start':
1795
1878
  // AI has started processing - update status to show activity
1796
1879
  this.currentResponseBuffer = '';
1880
+ this.finalResponseText = '';
1797
1881
  reasoningBuffer = '';
1798
1882
  reasoningOnlyStartTime = null; // Reset on new message
1799
1883
  this.promptController?.setStatusMessage('Thinking...');
@@ -1801,6 +1885,7 @@ class InteractiveShell {
1801
1885
  case 'message.delta':
1802
1886
  // Stream content as it arrives
1803
1887
  this.currentResponseBuffer += event.content ?? '';
1888
+ this.finalResponseText += event.content ?? '';
1804
1889
  if (renderer) {
1805
1890
  renderer.addEvent('stream', event.content);
1806
1891
  }
@@ -1865,6 +1950,9 @@ class InteractiveShell {
1865
1950
  }
1866
1951
  }
1867
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;
1868
1956
  }
1869
1957
  this.currentResponseBuffer = '';
1870
1958
  break;
@@ -1943,6 +2031,8 @@ class InteractiveShell {
1943
2031
  }
1944
2032
  break;
1945
2033
  case 'usage': {
2034
+ // Meter cumulative DeepSeek consumption for /usage + the portal.
2035
+ recordDeepSeekUsage(event.inputTokens, event.outputTokens);
1946
2036
  // inputTokens = exactly what occupies the context window this turn.
1947
2037
  // The real model window (not a hardcoded guess) is the denominator
1948
2038
  // so "% context left" reflects the actual model.
@@ -2098,7 +2188,7 @@ class InteractiveShell {
2098
2188
  // model declines the request, the request is *done* — auto-continue
2099
2189
  // would just resubmit "continue" and start a new spinner cycle, which
2100
2190
  // is what produced the stuck "Thinking… (4m N s)" timer the user saw.
2101
- const refusedTurn = isSafetyRefusal(this.currentResponseBuffer);
2191
+ const refusedTurn = isSafetyRefusal(this.finalResponseText);
2102
2192
  this.isProcessing = false;
2103
2193
  this.promptController?.setStreaming(false);
2104
2194
  this.promptController?.setStatusMessage(null);
@@ -2122,7 +2212,7 @@ class InteractiveShell {
2122
2212
  // Snapshot this turn's full output (tool results + narration) BEFORE the
2123
2213
  // buffer is cleared — the auto-continue governor + failure registry need
2124
2214
  // the real error text, which the reset below would otherwise wipe.
2125
- const combinedTurnOutput = (turnToolOutput + '\n' + this.currentResponseBuffer).slice(-16000);
2215
+ const combinedTurnOutput = (turnToolOutput + '\n' + this.finalResponseText).slice(-16000);
2126
2216
  this.currentResponseBuffer = '';
2127
2217
  // Autosave the conversation so /resume has something to restore. Each
2128
2218
  // turn updates the same snapshot in place (keyed by this.sessionId).
@@ -2154,7 +2244,7 @@ class InteractiveShell {
2154
2244
  if (autoMode !== 'off') {
2155
2245
  // Check if original user prompt is fully completed
2156
2246
  const detector = getTaskCompletionDetector();
2157
- const analysis = detector.analyzeCompletion(this.currentResponseBuffer, toolsUsed);
2247
+ const analysis = detector.analyzeCompletion(this.finalResponseText, toolsUsed);
2158
2248
  // Record this turn with the governor (bounds the loop + detects a
2159
2249
  // stall: the same tools/files/failure repeating with no new progress)
2160
2250
  // and the failure registry (catches the same error recurring across