@zhijiewang/openharness 2.1.0 → 2.3.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 (233) hide show
  1. package/README.md +4 -4
  2. package/dist/DeferredTool.js +3 -1
  3. package/dist/Tool.d.ts +1 -1
  4. package/dist/agents/roles.js +58 -62
  5. package/dist/commands/cybergotchi.d.ts +1 -1
  6. package/dist/commands/cybergotchi.js +30 -30
  7. package/dist/commands/index.js +288 -132
  8. package/dist/components/App.d.ts +1 -1
  9. package/dist/components/App.js +6 -6
  10. package/dist/components/CompanionFooter.d.ts +1 -1
  11. package/dist/components/CompanionFooter.js +6 -8
  12. package/dist/components/CybergotchiBubble.js +5 -5
  13. package/dist/components/CybergotchiPanel.d.ts +1 -1
  14. package/dist/components/CybergotchiPanel.js +7 -7
  15. package/dist/components/CybergotchiPanelConnected.js +2 -2
  16. package/dist/components/CybergotchiSetup.js +26 -24
  17. package/dist/components/CybergotchiSprite.d.ts +1 -1
  18. package/dist/components/CybergotchiSprite.js +8 -12
  19. package/dist/components/DiffView.d.ts +1 -1
  20. package/dist/components/DiffView.js +10 -10
  21. package/dist/components/ErrorBoundary.d.ts +1 -1
  22. package/dist/components/ErrorBoundary.js +1 -1
  23. package/dist/components/InitWizard.js +65 -33
  24. package/dist/components/Markdown.js +2 -4
  25. package/dist/components/Messages.js +4 -4
  26. package/dist/components/PermissionPrompt.d.ts +1 -1
  27. package/dist/components/PermissionPrompt.js +15 -17
  28. package/dist/components/REPL.d.ts +1 -1
  29. package/dist/components/REPL.js +74 -49
  30. package/dist/components/Spinner.js +2 -2
  31. package/dist/components/TextInput.js +35 -29
  32. package/dist/components/ToolCallDisplay.js +3 -5
  33. package/dist/cybergotchi/bones.d.ts +1 -1
  34. package/dist/cybergotchi/bones.js +8 -8
  35. package/dist/cybergotchi/config.d.ts +2 -2
  36. package/dist/cybergotchi/config.js +13 -13
  37. package/dist/cybergotchi/events.d.ts +5 -5
  38. package/dist/cybergotchi/events.js +7 -7
  39. package/dist/cybergotchi/needs.d.ts +2 -2
  40. package/dist/cybergotchi/needs.js +7 -9
  41. package/dist/cybergotchi/personality.d.ts +2 -2
  42. package/dist/cybergotchi/personality.js +2 -2
  43. package/dist/cybergotchi/species.d.ts +1 -1
  44. package/dist/cybergotchi/species.js +145 -217
  45. package/dist/cybergotchi/speech.d.ts +2 -2
  46. package/dist/cybergotchi/speech.js +43 -43
  47. package/dist/cybergotchi/types.d.ts +4 -4
  48. package/dist/cybergotchi/types.js +26 -26
  49. package/dist/cybergotchi/useCybergotchi.d.ts +1 -1
  50. package/dist/cybergotchi/useCybergotchi.js +29 -25
  51. package/dist/git/index.js +11 -9
  52. package/dist/harness/checkpoints.js +29 -21
  53. package/dist/harness/config.d.ts +3 -3
  54. package/dist/harness/config.js +15 -9
  55. package/dist/harness/context-warning.d.ts +1 -1
  56. package/dist/harness/context-warning.js +1 -1
  57. package/dist/harness/cost.js +1 -1
  58. package/dist/harness/credentials.js +13 -13
  59. package/dist/harness/hooks.js +7 -5
  60. package/dist/harness/keybindings.js +20 -18
  61. package/dist/harness/marketplace.d.ts +3 -3
  62. package/dist/harness/marketplace.js +55 -42
  63. package/dist/harness/memory.d.ts +23 -5
  64. package/dist/harness/memory.js +142 -41
  65. package/dist/harness/onboarding.js +30 -10
  66. package/dist/harness/plugins.d.ts +9 -1
  67. package/dist/harness/plugins.js +54 -30
  68. package/dist/harness/rules.js +12 -7
  69. package/dist/harness/sandbox.js +15 -15
  70. package/dist/harness/session-db.d.ts +55 -0
  71. package/dist/harness/session-db.js +165 -0
  72. package/dist/harness/session.d.ts +1 -1
  73. package/dist/harness/session.js +34 -15
  74. package/dist/harness/store.d.ts +3 -3
  75. package/dist/harness/store.js +6 -4
  76. package/dist/harness/submit-handler.d.ts +4 -4
  77. package/dist/harness/submit-handler.js +25 -23
  78. package/dist/harness/telemetry.d.ts +1 -1
  79. package/dist/harness/telemetry.js +23 -19
  80. package/dist/harness/traces.d.ts +2 -2
  81. package/dist/harness/traces.js +39 -33
  82. package/dist/harness/verification.d.ts +1 -1
  83. package/dist/harness/verification.js +50 -44
  84. package/dist/lsp/client.js +44 -40
  85. package/dist/main.js +98 -59
  86. package/dist/mcp/DeferredMcpTool.d.ts +4 -4
  87. package/dist/mcp/DeferredMcpTool.js +9 -5
  88. package/dist/mcp/McpTool.d.ts +4 -4
  89. package/dist/mcp/McpTool.js +8 -4
  90. package/dist/mcp/client.d.ts +2 -2
  91. package/dist/mcp/client.js +21 -21
  92. package/dist/mcp/loader.d.ts +1 -1
  93. package/dist/mcp/loader.js +17 -12
  94. package/dist/mcp/registry.d.ts +3 -3
  95. package/dist/mcp/registry.js +97 -97
  96. package/dist/mcp/schema.d.ts +1 -1
  97. package/dist/mcp/schema.js +16 -16
  98. package/dist/mcp/server.d.ts +1 -1
  99. package/dist/mcp/server.js +21 -21
  100. package/dist/mcp/types.d.ts +3 -3
  101. package/dist/providers/anthropic.d.ts +2 -2
  102. package/dist/providers/anthropic.js +10 -9
  103. package/dist/providers/base.d.ts +1 -1
  104. package/dist/providers/index.js +10 -3
  105. package/dist/providers/llamacpp.d.ts +2 -2
  106. package/dist/providers/llamacpp.js +1 -3
  107. package/dist/providers/ollama.d.ts +2 -2
  108. package/dist/providers/ollama.js +3 -4
  109. package/dist/providers/openai.d.ts +2 -2
  110. package/dist/providers/openai.js +3 -5
  111. package/dist/providers/openrouter.d.ts +2 -2
  112. package/dist/providers/router.d.ts +1 -1
  113. package/dist/providers/router.js +7 -7
  114. package/dist/query/compress.d.ts +2 -2
  115. package/dist/query/compress.js +22 -21
  116. package/dist/query/context-manager.d.ts +1 -1
  117. package/dist/query/context-manager.js +5 -5
  118. package/dist/query/errors.js +1 -1
  119. package/dist/query/index.d.ts +1 -1
  120. package/dist/query/index.js +30 -22
  121. package/dist/query/tools.js +15 -12
  122. package/dist/query/types.d.ts +1 -1
  123. package/dist/query.d.ts +1 -1
  124. package/dist/query.js +1 -1
  125. package/dist/remote/auth.d.ts +2 -2
  126. package/dist/remote/auth.js +8 -8
  127. package/dist/remote/server.d.ts +3 -3
  128. package/dist/remote/server.js +60 -60
  129. package/dist/renderer/cells.js +9 -9
  130. package/dist/renderer/colors.js +24 -6
  131. package/dist/renderer/diff.d.ts +2 -2
  132. package/dist/renderer/diff.js +27 -19
  133. package/dist/renderer/differ.d.ts +1 -1
  134. package/dist/renderer/differ.js +9 -9
  135. package/dist/renderer/image.js +19 -19
  136. package/dist/renderer/index.d.ts +6 -6
  137. package/dist/renderer/index.js +163 -93
  138. package/dist/renderer/input.js +66 -48
  139. package/dist/renderer/layout.d.ts +6 -6
  140. package/dist/renderer/layout.js +163 -124
  141. package/dist/renderer/markdown.d.ts +2 -2
  142. package/dist/renderer/markdown.js +173 -54
  143. package/dist/renderer/session-browser.d.ts +2 -2
  144. package/dist/renderer/session-browser.js +19 -21
  145. package/dist/repl.d.ts +5 -5
  146. package/dist/repl.js +300 -198
  147. package/dist/sdk/index.d.ts +5 -5
  148. package/dist/sdk/index.js +32 -26
  149. package/dist/services/AgentDispatcher.d.ts +3 -3
  150. package/dist/services/AgentDispatcher.js +33 -29
  151. package/dist/services/CronExecutor.d.ts +4 -4
  152. package/dist/services/CronExecutor.js +12 -8
  153. package/dist/services/EvaluatorLoop.d.ts +3 -3
  154. package/dist/services/EvaluatorLoop.js +29 -21
  155. package/dist/services/MetaHarness.d.ts +1 -1
  156. package/dist/services/MetaHarness.js +34 -32
  157. package/dist/services/PipelineExecutor.d.ts +1 -1
  158. package/dist/services/PipelineExecutor.js +23 -25
  159. package/dist/services/SkillExtractor.d.ts +43 -0
  160. package/dist/services/SkillExtractor.js +143 -0
  161. package/dist/services/StreamingToolExecutor.d.ts +2 -2
  162. package/dist/services/StreamingToolExecutor.js +11 -7
  163. package/dist/services/a2a.d.ts +8 -8
  164. package/dist/services/a2a.js +44 -34
  165. package/dist/services/agent-messaging.d.ts +33 -15
  166. package/dist/services/agent-messaging.js +65 -13
  167. package/dist/services/cron.js +16 -16
  168. package/dist/tools/AgentTool/index.d.ts +5 -2
  169. package/dist/tools/AgentTool/index.js +35 -15
  170. package/dist/tools/AskUserTool/index.js +1 -1
  171. package/dist/tools/BashTool/index.d.ts +2 -2
  172. package/dist/tools/BashTool/index.js +18 -10
  173. package/dist/tools/CronTool/index.d.ts +2 -2
  174. package/dist/tools/CronTool/index.js +30 -12
  175. package/dist/tools/DiagnosticsTool/index.js +28 -22
  176. package/dist/tools/EnterPlanModeTool/index.js +93 -14
  177. package/dist/tools/EnterWorktreeTool/index.js +7 -3
  178. package/dist/tools/ExitPlanModeTool/index.d.ts +22 -1
  179. package/dist/tools/ExitPlanModeTool/index.js +20 -5
  180. package/dist/tools/ExitWorktreeTool/index.js +11 -4
  181. package/dist/tools/FileEditTool/index.js +3 -5
  182. package/dist/tools/FileReadTool/index.js +16 -10
  183. package/dist/tools/FileWriteTool/index.js +2 -2
  184. package/dist/tools/GlobTool/index.js +5 -9
  185. package/dist/tools/GrepTool/index.d.ts +2 -2
  186. package/dist/tools/GrepTool/index.js +14 -9
  187. package/dist/tools/ImageReadTool/index.js +2 -2
  188. package/dist/tools/KillProcessTool/index.js +11 -7
  189. package/dist/tools/LSTool/index.js +3 -3
  190. package/dist/tools/MemoryTool/index.d.ts +11 -11
  191. package/dist/tools/MemoryTool/index.js +28 -14
  192. package/dist/tools/MonitorTool/index.js +24 -19
  193. package/dist/tools/MultiEditTool/index.js +9 -5
  194. package/dist/tools/NotebookEditTool/index.js +3 -3
  195. package/dist/tools/ParallelAgentTool/index.d.ts +4 -4
  196. package/dist/tools/ParallelAgentTool/index.js +12 -6
  197. package/dist/tools/PipelineTool/index.d.ts +4 -4
  198. package/dist/tools/PipelineTool/index.js +3 -3
  199. package/dist/tools/PowerShellTool/index.js +10 -6
  200. package/dist/tools/RemoteTriggerTool/index.js +8 -4
  201. package/dist/tools/ScheduleWakeupTool/index.d.ts +42 -0
  202. package/dist/tools/ScheduleWakeupTool/index.js +115 -0
  203. package/dist/tools/SendMessageTool/index.js +25 -7
  204. package/dist/tools/SessionSearchTool/index.d.ts +15 -0
  205. package/dist/tools/SessionSearchTool/index.js +36 -0
  206. package/dist/tools/SkillTool/index.d.ts +3 -0
  207. package/dist/tools/SkillTool/index.js +39 -9
  208. package/dist/tools/TaskCreateTool/index.d.ts +2 -2
  209. package/dist/tools/TaskCreateTool/index.js +2 -2
  210. package/dist/tools/TaskGetTool/index.js +2 -2
  211. package/dist/tools/TaskListTool/index.js +3 -5
  212. package/dist/tools/TaskOutputTool/index.js +2 -2
  213. package/dist/tools/TaskStopTool/index.js +3 -3
  214. package/dist/tools/TaskUpdateTool/index.d.ts +4 -4
  215. package/dist/tools/TaskUpdateTool/index.js +2 -2
  216. package/dist/tools/ToolSearchTool/index.js +9 -6
  217. package/dist/tools/WebFetchTool/index.js +1 -1
  218. package/dist/tools/WebSearchTool/index.js +2 -6
  219. package/dist/tools.js +31 -30
  220. package/dist/types/permissions.js +15 -9
  221. package/dist/utils/bash-safety.d.ts +1 -1
  222. package/dist/utils/bash-safety.js +64 -54
  223. package/dist/utils/diff-algorithm.d.ts +3 -3
  224. package/dist/utils/diff-algorithm.js +7 -7
  225. package/dist/utils/fs.js +3 -3
  226. package/dist/utils/safe-env.js +1 -1
  227. package/dist/utils/theme-data.d.ts +1 -1
  228. package/dist/utils/theme-data.js +1 -1
  229. package/dist/utils/theme.d.ts +1 -1
  230. package/dist/utils/theme.js +1 -1
  231. package/dist/utils/tool-summary.d.ts +1 -1
  232. package/dist/utils/tool-summary.js +27 -9
  233. package/package.json +10 -3
package/dist/repl.js CHANGED
@@ -2,68 +2,68 @@
2
2
  * Imperative REPL — extracted business logic from React REPL.tsx.
3
3
  * Uses TerminalRenderer for display instead of Ink.
4
4
  */
5
- import { createAssistantMessage, createMessage, createInfoMessage } from './types/message.js';
6
- import { query } from './query/index.js';
7
- import { createStore } from './harness/store.js';
8
- import { createSession, saveSession, loadSession } from './harness/session.js';
9
- import { CostTracker, estimateCost, getContextWindow } from './harness/cost.js';
10
- import { autoCommitAIEdits, isGitRepo } from './git/index.js';
11
- import { cybergotchiEvents } from './cybergotchi/events.js';
12
- import { loadCompanionConfig, saveCompanionConfig } from './cybergotchi/config.js';
13
- import { readOhConfig, writeOhConfig } from './harness/config.js';
14
- import { roll } from './cybergotchi/bones.js';
15
- import { getSpecies } from './cybergotchi/species.js';
16
- import { EYE_STYLES, RARITY_COLORS, RARITY_STARS } from './cybergotchi/types.js';
17
- import { TerminalRenderer } from './renderer/index.js';
18
- import { formatTokenCount } from './utils/format.js';
19
- import { formatToolArgs, summarizeToolOutput } from './utils/tool-summary.js';
20
- import { getCommandEntries } from './commands/index.js';
21
- import { handleUserInput } from './harness/submit-handler.js';
22
- import { estimateMessageTokens, getContextWarning } from './harness/context-warning.js';
23
- import { setActiveTheme } from './utils/theme-data.js';
24
- import { resetStyleCache } from './renderer/layout.js';
25
- import { resetMdStyleCache } from './renderer/markdown.js';
26
- import { resetDiffStyleCache } from './renderer/diff.js';
27
- import { homedir } from 'node:os';
5
+ import { homedir } from "node:os";
6
+ import { getCommandEntries } from "./commands/index.js";
7
+ import { roll } from "./cybergotchi/bones.js";
8
+ import { loadCompanionConfig, saveCompanionConfig } from "./cybergotchi/config.js";
9
+ import { cybergotchiEvents } from "./cybergotchi/events.js";
10
+ import { getSpecies } from "./cybergotchi/species.js";
11
+ import { EYE_STYLES, RARITY_COLORS, RARITY_STARS } from "./cybergotchi/types.js";
12
+ import { autoCommitAIEdits, isGitRepo } from "./git/index.js";
13
+ import { readOhConfig, writeOhConfig } from "./harness/config.js";
14
+ import { estimateMessageTokens, getContextWarning } from "./harness/context-warning.js";
15
+ import { CostTracker, estimateCost, getContextWindow } from "./harness/cost.js";
16
+ import { createSession, loadSession, saveSession } from "./harness/session.js";
17
+ import { createStore } from "./harness/store.js";
18
+ import { handleUserInput } from "./harness/submit-handler.js";
19
+ import { query } from "./query/index.js";
20
+ import { resetDiffStyleCache } from "./renderer/diff.js";
21
+ import { TerminalRenderer } from "./renderer/index.js";
22
+ import { resetStyleCache } from "./renderer/layout.js";
23
+ import { resetMdStyleCache } from "./renderer/markdown.js";
24
+ import { createAssistantMessage, createInfoMessage, createMessage } from "./types/message.js";
25
+ import { formatTokenCount } from "./utils/format.js";
26
+ import { setActiveTheme } from "./utils/theme-data.js";
27
+ import { formatToolArgs, summarizeToolOutput } from "./utils/tool-summary.js";
28
28
  export async function startREPL(config) {
29
29
  if (config.theme)
30
30
  setActiveTheme(config.theme);
31
31
  const renderer = new TerminalRenderer();
32
32
  // Set banner in live area (avoids the empty gap between scrollback banner and bottom-anchored input)
33
33
  if (config.welcomeText) {
34
- renderer.setBannerLines(config.welcomeText.split('\n'));
34
+ renderer.setBannerLines(config.welcomeText.split("\n"));
35
35
  }
36
36
  // Session
37
37
  let session;
38
38
  const sessionExtras = {
39
39
  workingDir: process.cwd(),
40
- gitBranch: isGitRepo() ? (await import('./git/index.js')).gitBranch() : undefined,
41
- tools: config.tools.map(t => t.name),
40
+ gitBranch: isGitRepo() ? (await import("./git/index.js")).gitBranch() : undefined,
41
+ tools: config.tools.map((t) => t.name),
42
42
  };
43
43
  try {
44
44
  session = config.resumeSessionId
45
45
  ? loadSession(config.resumeSessionId)
46
- : createSession(config.provider.name, config.model ?? '', sessionExtras);
46
+ : createSession(config.provider.name, config.model ?? "", sessionExtras);
47
47
  }
48
48
  catch {
49
- session = createSession(config.provider.name, config.model ?? '', sessionExtras);
49
+ session = createSession(config.provider.name, config.model ?? "", sessionExtras);
50
50
  }
51
51
  // Wake context: inject session summary when resuming
52
52
  if (config.resumeSessionId && session.hibernate) {
53
- const { buildWakeContext } = await import('./harness/session.js');
53
+ const { buildWakeContext } = await import("./harness/session.js");
54
54
  const wakeMsg = buildWakeContext(session);
55
- const { createInfoMessage } = await import('./types/message.js');
55
+ const { createInfoMessage } = await import("./types/message.js");
56
56
  session.messages.push(createInfoMessage(wakeMsg));
57
57
  }
58
58
  // Initialize checkpoints for file rewind
59
- const { initCheckpoints } = await import('./harness/checkpoints.js');
59
+ const { initCheckpoints } = await import("./harness/checkpoints.js");
60
60
  initCheckpoints(session.id);
61
61
  // Start background cron executor
62
- const { CronExecutor } = await import('./services/CronExecutor.js');
62
+ const { CronExecutor } = await import("./services/CronExecutor.js");
63
63
  const cronExecutor = new CronExecutor(config.provider, config.tools, config.systemPrompt, config.permissionMode, config.model);
64
64
  cronExecutor.start();
65
65
  // A2A: publish agent card for cross-process discovery
66
- const { createSessionCard, publishCard, unpublishCard } = await import('./services/a2a.js');
66
+ const { createSessionCard, publishCard, unpublishCard } = await import("./services/a2a.js");
67
67
  const agentCard = createSessionCard(session.id, {
68
68
  provider: config.provider.name,
69
69
  model: config.model,
@@ -75,8 +75,8 @@ export async function startREPL(config) {
75
75
  const store = createStore({
76
76
  messages: config.resumeSessionId ? session.messages : (config.initialMessages ?? []),
77
77
  loading: false,
78
- currentModel: config.model ?? '',
79
- inputText: '',
78
+ currentModel: config.model ?? "",
79
+ inputText: "",
80
80
  inputCursor: 0,
81
81
  inputHistory: [],
82
82
  historyIndex: -1,
@@ -127,47 +127,53 @@ export async function startREPL(config) {
127
127
  });
128
128
  function updateAutocomplete() {
129
129
  acIsPath = false;
130
- if (inputText.startsWith('/') && inputText.length > 1 && !inputText.includes(' ')) {
130
+ if (inputText.startsWith("/") && inputText.length > 1 && !inputText.includes(" ")) {
131
131
  // Slash command autocomplete
132
132
  const prefix = inputText.slice(1).toLowerCase();
133
- const entries = getCommandEntries().filter(e => e.name.startsWith(prefix)).slice(0, 5);
134
- acSuggestions = entries.map(e => e.name);
135
- acDescriptions = entries.map(e => e.description);
133
+ const entries = getCommandEntries()
134
+ .filter((e) => e.name.startsWith(prefix))
135
+ .slice(0, 5);
136
+ acSuggestions = entries.map((e) => e.name);
137
+ acDescriptions = entries.map((e) => e.description);
136
138
  acTokenStart = 0;
137
139
  acIndex = -1;
138
140
  }
139
- else if (inputText.length > 0 && !inputText.startsWith('/')) {
141
+ else if (inputText.length > 0 && !inputText.startsWith("/")) {
140
142
  // File path autocomplete: extract token under cursor
141
143
  const beforeCursor = inputText.slice(0, inputCursor);
142
144
  const tokenMatch = beforeCursor.match(/(\S+)$/);
143
- if (tokenMatch && (tokenMatch[1].includes('/') || tokenMatch[1].includes('\\') || tokenMatch[1].startsWith('.') || tokenMatch[1].startsWith('~'))) {
145
+ if (tokenMatch &&
146
+ (tokenMatch[1].includes("/") ||
147
+ tokenMatch[1].includes("\\") ||
148
+ tokenMatch[1].startsWith(".") ||
149
+ tokenMatch[1].startsWith("~"))) {
144
150
  const token = tokenMatch[1];
145
151
  acTokenStart = inputCursor - token.length;
146
- const expanded = token.startsWith('~') ? token.replace('~', homedir()) : token;
147
- const lastSep = Math.max(expanded.lastIndexOf('/'), expanded.lastIndexOf('\\'));
148
- const dir = lastSep >= 0 ? expanded.slice(0, lastSep + 1) : '.';
152
+ const expanded = token.startsWith("~") ? token.replace("~", homedir()) : token;
153
+ const lastSep = Math.max(expanded.lastIndexOf("/"), expanded.lastIndexOf("\\"));
154
+ const dir = lastSep >= 0 ? expanded.slice(0, lastSep + 1) : ".";
149
155
  const prefix = lastSep >= 0 ? expanded.slice(lastSep + 1) : expanded;
150
156
  try {
151
- const { readdirSync, statSync } = require('node:fs');
157
+ const { readdirSync, statSync } = require("node:fs");
152
158
  const entries = readdirSync(dir)
153
159
  .filter((name) => name.toLowerCase().startsWith(prefix.toLowerCase()))
154
160
  .slice(0, 10);
155
161
  acSuggestions = entries.map((name) => {
156
- const full = dir === '.' ? name : dir + name;
162
+ const full = dir === "." ? name : dir + name;
157
163
  try {
158
- return statSync(full).isDirectory() ? full + '/' : full;
164
+ return statSync(full).isDirectory() ? `${full}/` : full;
159
165
  }
160
166
  catch {
161
167
  return full;
162
168
  }
163
169
  });
164
170
  acDescriptions = entries.map((name) => {
165
- const full = dir === '.' ? name : dir + name;
171
+ const full = dir === "." ? name : dir + name;
166
172
  try {
167
- return statSync(full).isDirectory() ? '[dir]' : '[file]';
173
+ return statSync(full).isDirectory() ? "[dir]" : "[file]";
168
174
  }
169
175
  catch {
170
- return '';
176
+ return "";
171
177
  }
172
178
  });
173
179
  acIsPath = acSuggestions.length > 0;
@@ -199,19 +205,19 @@ export async function startREPL(config) {
199
205
  saveCompanionConfig(companionConfig);
200
206
  const bones = roll(companionConfig.seed);
201
207
  const species = getSpecies(bones.species);
202
- const eyes = EYE_STYLES[bones.eyeStyle % EYE_STYLES.length] ?? 'o o';
208
+ const eyes = EYE_STYLES[bones.eyeStyle % EYE_STYLES.length] ?? "o o";
203
209
  const idleFrames = species.frames.idle;
204
210
  const color = RARITY_COLORS[bones.rarity];
205
211
  const nameLine = `${companionConfig.soul.name} ${RARITY_STARS[bones.rarity]}`;
206
212
  // Render initial frame
207
- const frame0 = (idleFrames[0] ?? []).map((l) => l.replace('{E}', eyes));
213
+ const frame0 = (idleFrames[0] ?? []).map((l) => l.replace("{E}", eyes));
208
214
  renderer.setCompanion([...frame0, nameLine], color);
209
215
  // Animate on timer
210
216
  renderer.onAnimation((frameIdx) => {
211
217
  if (!companionVisible)
212
218
  return;
213
219
  const f = idleFrames[frameIdx % idleFrames.length] ?? idleFrames[0] ?? [];
214
- const lines = f.map((l) => l.replace('{E}', eyes));
220
+ const lines = f.map((l) => l.replace("{E}", eyes));
215
221
  renderer.setCompanion([...lines, nameLine], color);
216
222
  });
217
223
  }
@@ -219,42 +225,54 @@ export async function startREPL(config) {
219
225
  /** Sync local aliases back to the centralized store */
220
226
  function syncStore() {
221
227
  store.setState({
222
- messages, loading, currentModel, inputText, inputCursor,
223
- inputHistory, historyIndex, vimMode, fastMode,
224
- acSuggestions, acDescriptions, acIndex, acTokenStart, acIsPath,
228
+ messages,
229
+ loading,
230
+ currentModel,
231
+ inputText,
232
+ inputCursor,
233
+ inputHistory,
234
+ historyIndex,
235
+ vimMode,
236
+ fastMode,
237
+ acSuggestions,
238
+ acDescriptions,
239
+ acIndex,
240
+ acTokenStart,
241
+ acIsPath,
225
242
  });
226
243
  }
227
244
  function syncRenderer() {
228
245
  syncStore();
229
246
  renderer.setMessages(messages);
230
247
  renderer.setLoading(loading);
231
- const hints = `exit to quit${loading ? ' | Ctrl+C stop | Ctrl+O thinking' : ' | Tab expand tools | Ctrl+O transcript'}${companionConfig?.soul?.name ? ` | @${companionConfig.soul.name}` : ''}`;
248
+ const hints = `exit to quit${loading ? " | Ctrl+C stop | Ctrl+O thinking" : " | Tab expand tools | Ctrl+O transcript"}${companionConfig?.soul?.name ? ` | @${companionConfig.soul.name}` : ""}`;
232
249
  renderer.setStatusHints(hints);
233
250
  // Status line: model | tokens | cost | ctx
234
251
  const inTok = cost.totalInputTokens;
235
252
  const outTok = cost.totalOutputTokens;
236
253
  const totalCostVal = cost.totalCost;
237
- const tokensStr = (inTok > 0 || outTok > 0) ? `${formatTokenCount(inTok)}↑ ${formatTokenCount(outTok)}↓` : '';
238
- const costStr = totalCostVal > 0 ? `$${totalCostVal.toFixed(4)}` : '';
239
- let ctxStr = '';
254
+ const tokensStr = inTok > 0 || outTok > 0 ? `${formatTokenCount(inTok)}↑ ${formatTokenCount(outTok)}↓` : "";
255
+ const costStr = totalCostVal > 0 ? `$${totalCostVal.toFixed(4)}` : "";
256
+ let ctxStr = "";
240
257
  const ctxWindow = getContextWindow(currentModel);
241
258
  if (ctxWindow > 0 && estimatedTokenCount > 0) {
242
259
  const usage = Math.min(1, estimatedTokenCount / ctxWindow);
243
260
  const barWidth = 10;
244
261
  const filled = Math.max(1, Math.round(usage * barWidth));
245
- const bar = ''.repeat(filled) + ''.repeat(barWidth - filled);
262
+ const bar = "".repeat(filled) + "".repeat(barWidth - filled);
246
263
  const pct = Math.max(1, Math.ceil(usage * 100));
247
264
  ctxStr = `ctx [${bar}] ${pct}%`;
248
265
  }
249
266
  // Use template if configured, otherwise default format
250
267
  if (cachedConfig?.statusLineFormat) {
251
268
  const line = cachedConfig.statusLineFormat
252
- .replace('{model}', currentModel || '')
253
- .replace('{tokens}', tokensStr)
254
- .replace('{cost}', costStr)
255
- .replace('{ctx}', ctxStr)
256
- .replace(/\s*│\s*│/g, '') // collapse empty sections
257
- .replace(/^│\s*/, '').replace(/\s*│$/, ''); // trim leading/trailing separators
269
+ .replace("{model}", currentModel || "")
270
+ .replace("{tokens}", tokensStr)
271
+ .replace("{cost}", costStr)
272
+ .replace("{ctx}", ctxStr)
273
+ .replace(/\s*│\s*│/g, "") // collapse empty sections
274
+ .replace(/^│\s*/, "")
275
+ .replace(/\s*│$/, ""); // trim leading/trailing separators
258
276
  renderer.setStatusLine(line);
259
277
  }
260
278
  else {
@@ -267,7 +285,7 @@ export async function startREPL(config) {
267
285
  parts.push(costStr);
268
286
  if (ctxStr)
269
287
  parts.push(ctxStr);
270
- renderer.setStatusLine(parts.join(''));
288
+ renderer.setStatusLine(parts.join(""));
271
289
  }
272
290
  // Context warning
273
291
  updateContextWarning();
@@ -284,7 +302,7 @@ export async function startREPL(config) {
284
302
  // Input handling
285
303
  renderer.onKeypress((key) => {
286
304
  // Ctrl+C: abort or exit
287
- if (key.ctrl && key.char === 'c') {
305
+ if (key.ctrl && key.char === "c") {
288
306
  if (loading && abortController) {
289
307
  abortController.abort();
290
308
  }
@@ -297,84 +315,84 @@ export async function startREPL(config) {
297
315
  // Search: use terminal's native search (Ctrl+Shift+F in VS Code)
298
316
  // Vim mode
299
317
  if (vimMode !== null) {
300
- if (key.name === 'escape') {
301
- vimMode = 'normal';
318
+ if (key.name === "escape") {
319
+ vimMode = "normal";
302
320
  renderer.setVimMode(vimMode);
303
321
  return;
304
322
  }
305
- if (vimMode === 'normal') {
323
+ if (vimMode === "normal") {
306
324
  // -- Mode transitions --
307
- if (key.char === 'i') {
308
- vimMode = 'insert';
325
+ if (key.char === "i") {
326
+ vimMode = "insert";
309
327
  renderer.setVimMode(vimMode);
310
328
  return;
311
329
  }
312
- if (key.char === 'a') {
313
- vimMode = 'insert';
330
+ if (key.char === "a") {
331
+ vimMode = "insert";
314
332
  renderer.setVimMode(vimMode);
315
333
  if (inputCursor < inputText.length)
316
334
  inputCursor++;
317
335
  renderer.setInputCursor(inputCursor);
318
336
  return;
319
337
  }
320
- if (key.char === 'I') {
321
- vimMode = 'insert';
338
+ if (key.char === "I") {
339
+ vimMode = "insert";
322
340
  renderer.setVimMode(vimMode);
323
341
  inputCursor = 0;
324
342
  renderer.setInputCursor(inputCursor);
325
343
  return;
326
344
  }
327
- if (key.char === 'A') {
328
- vimMode = 'insert';
345
+ if (key.char === "A") {
346
+ vimMode = "insert";
329
347
  renderer.setVimMode(vimMode);
330
348
  inputCursor = inputText.length;
331
349
  renderer.setInputCursor(inputCursor);
332
350
  return;
333
351
  }
334
- if (key.char === 'o') {
335
- vimMode = 'insert';
352
+ if (key.char === "o") {
353
+ vimMode = "insert";
336
354
  renderer.setVimMode(vimMode);
337
- inputText = inputText + '\n';
355
+ inputText = `${inputText}\n`;
338
356
  inputCursor = inputText.length;
339
357
  renderer.setInputText(inputText);
340
358
  renderer.setInputCursor(inputCursor);
341
359
  return;
342
360
  }
343
361
  // -- Movement --
344
- if (key.char === 'h' || key.name === 'left') {
362
+ if (key.char === "h" || key.name === "left") {
345
363
  if (inputCursor > 0) {
346
364
  inputCursor--;
347
365
  renderer.setInputCursor(inputCursor);
348
366
  }
349
367
  return;
350
368
  }
351
- if (key.char === 'l' || key.name === 'right') {
369
+ if (key.char === "l" || key.name === "right") {
352
370
  if (inputCursor < inputText.length) {
353
371
  inputCursor++;
354
372
  renderer.setInputCursor(inputCursor);
355
373
  }
356
374
  return;
357
375
  }
358
- if (key.char === 'j' || key.name === 'down') {
376
+ if (key.char === "j" || key.name === "down") {
359
377
  navigateHistory(1);
360
378
  return;
361
379
  }
362
- if (key.char === 'k' || key.name === 'up') {
380
+ if (key.char === "k" || key.name === "up") {
363
381
  navigateHistory(-1);
364
382
  return;
365
383
  }
366
- if (key.char === '0') {
384
+ if (key.char === "0") {
367
385
  inputCursor = 0;
368
386
  renderer.setInputCursor(inputCursor);
369
387
  return;
370
388
  }
371
- if (key.char === '$') {
389
+ if (key.char === "$") {
372
390
  inputCursor = inputText.length;
373
391
  renderer.setInputCursor(inputCursor);
374
392
  return;
375
393
  }
376
394
  // Word forward (w)
377
- if (key.char === 'w') {
395
+ if (key.char === "w") {
378
396
  const rest = inputText.slice(inputCursor);
379
397
  const m = rest.match(/^\S*\s+/);
380
398
  inputCursor = m ? Math.min(inputCursor + m[0].length, inputText.length) : inputText.length;
@@ -382,7 +400,7 @@ export async function startREPL(config) {
382
400
  return;
383
401
  }
384
402
  // Word backward (b)
385
- if (key.char === 'b') {
403
+ if (key.char === "b") {
386
404
  const before = inputText.slice(0, inputCursor);
387
405
  const m = before.match(/\S+\s*$/);
388
406
  inputCursor = m ? inputCursor - m[0].length : 0;
@@ -390,7 +408,7 @@ export async function startREPL(config) {
390
408
  return;
391
409
  }
392
410
  // End of word (e)
393
- if (key.char === 'e') {
411
+ if (key.char === "e") {
394
412
  const rest = inputText.slice(inputCursor + 1);
395
413
  const m = rest.match(/^\s*\S*/);
396
414
  inputCursor = m ? Math.min(inputCursor + 1 + m[0].length, inputText.length) : inputText.length;
@@ -398,7 +416,7 @@ export async function startREPL(config) {
398
416
  return;
399
417
  }
400
418
  // -- Editing --
401
- if (key.char === 'x') {
419
+ if (key.char === "x") {
402
420
  if (inputCursor < inputText.length) {
403
421
  inputText = inputText.slice(0, inputCursor) + inputText.slice(inputCursor + 1);
404
422
  if (inputCursor >= inputText.length && inputCursor > 0)
@@ -409,21 +427,21 @@ export async function startREPL(config) {
409
427
  return;
410
428
  }
411
429
  // dd — delete entire line
412
- if (key.char === 'd') {
430
+ if (key.char === "d") {
413
431
  // Simple: clear entire input (like dd in single-line mode)
414
- inputText = '';
432
+ inputText = "";
415
433
  inputCursor = 0;
416
434
  renderer.setInputText(inputText);
417
435
  renderer.setInputCursor(inputCursor);
418
436
  return;
419
437
  }
420
438
  // Submit with Enter even in normal mode
421
- if (key.name === 'return') {
439
+ if (key.name === "return") {
422
440
  if (inputText.trim() && !loading) {
423
441
  handleSubmit(inputText.trim());
424
442
  inputHistory.unshift(inputText);
425
443
  historyIndex = -1;
426
- inputText = '';
444
+ inputText = "";
427
445
  inputCursor = 0;
428
446
  acSuggestions = [];
429
447
  acDescriptions = [];
@@ -438,25 +456,25 @@ export async function startREPL(config) {
438
456
  }
439
457
  // Session browser navigation
440
458
  if (renderer.isSessionBrowserOpen()) {
441
- if (key.name === 'up') {
459
+ if (key.name === "up") {
442
460
  renderer.sessionBrowserUp();
443
461
  return;
444
462
  }
445
- if (key.name === 'down') {
463
+ if (key.name === "down") {
446
464
  renderer.sessionBrowserDown();
447
465
  return;
448
466
  }
449
- if (key.name === 'return') {
467
+ if (key.name === "return") {
450
468
  const id = renderer.sessionBrowserSelect();
451
469
  if (id)
452
470
  handleSubmit(`/resume ${id}`);
453
471
  return;
454
472
  }
455
- if (key.name === 'escape') {
473
+ if (key.name === "escape") {
456
474
  renderer.closeSessionBrowser();
457
475
  return;
458
476
  }
459
- if (key.name === 'backspace') {
477
+ if (key.name === "backspace") {
460
478
  renderer.sessionBrowserBackspace();
461
479
  return;
462
480
  }
@@ -467,12 +485,12 @@ export async function startREPL(config) {
467
485
  return; // swallow other keys during browser
468
486
  }
469
487
  // Ctrl+K: toggle code block expansion
470
- if (key.ctrl && key.char === 'k' && !loading) {
488
+ if (key.ctrl && key.char === "k" && !loading) {
471
489
  renderer.toggleCodeBlockExpansion();
472
490
  return;
473
491
  }
474
492
  // Ctrl+O: cycle through views — thinking toggle → transcript (flush all to scrollback)
475
- if (key.ctrl && key.char === 'o') {
493
+ if (key.ctrl && key.char === "o") {
476
494
  if (loading) {
477
495
  // During streaming: toggle thinking expansion
478
496
  renderer.toggleThinkingExpanded();
@@ -483,23 +501,23 @@ export async function startREPL(config) {
483
501
  renderer.clearLiveArea();
484
502
  renderer.setMessages(messages);
485
503
  renderer.flushMessages();
486
- renderer.notify('Transcript written to scrollback (scroll up to review)');
504
+ renderer.notify("Transcript written to scrollback (scroll up to review)");
487
505
  }
488
506
  return;
489
507
  }
490
508
  // Scroll wheel: adjust manual scroll offset
491
- if (key.name === 'scrollup') {
509
+ if (key.name === "scrollup") {
492
510
  renderer.scrollBy(3);
493
511
  return;
494
512
  }
495
- if (key.name === 'scrolldown') {
513
+ if (key.name === "scrolldown") {
496
514
  renderer.scrollBy(-3);
497
515
  return;
498
516
  }
499
- if (key.name === 'pageup' || key.name === 'pagedown' || key.name === 'mouse')
517
+ if (key.name === "pageup" || key.name === "pagedown" || key.name === "mouse")
500
518
  return;
501
519
  // Tab: autocomplete slash commands or file paths, or cycle tool call expansion
502
- if (key.name === 'tab' && !loading) {
520
+ if (key.name === "tab" && !loading) {
503
521
  if (acSuggestions.length > 0) {
504
522
  acIndex = (acIndex + 1) % acSuggestions.length;
505
523
  if (acIsPath) {
@@ -522,20 +540,20 @@ export async function startREPL(config) {
522
540
  return;
523
541
  }
524
542
  // Alt+Enter or paste newline: insert newline at cursor
525
- if (key.name === 'newline') {
526
- inputText = inputText.slice(0, inputCursor) + '\n' + inputText.slice(inputCursor);
543
+ if (key.name === "newline") {
544
+ inputText = `${inputText.slice(0, inputCursor)}\n${inputText.slice(inputCursor)}`;
527
545
  inputCursor++;
528
546
  renderer.setInputText(inputText);
529
547
  renderer.setInputCursor(inputCursor);
530
548
  return;
531
549
  }
532
550
  // Enter: submit
533
- if (key.name === 'return') {
551
+ if (key.name === "return") {
534
552
  if (inputText.trim() && !loading) {
535
553
  handleSubmit(inputText.trim());
536
554
  inputHistory.unshift(inputText);
537
555
  historyIndex = -1;
538
- inputText = '';
556
+ inputText = "";
539
557
  inputCursor = 0;
540
558
  acSuggestions = [];
541
559
  acIndex = -1;
@@ -546,36 +564,36 @@ export async function startREPL(config) {
546
564
  return;
547
565
  }
548
566
  // History
549
- if (key.name === 'up') {
567
+ if (key.name === "up") {
550
568
  navigateHistory(-1);
551
569
  return;
552
570
  }
553
- if (key.name === 'down') {
571
+ if (key.name === "down") {
554
572
  navigateHistory(1);
555
573
  return;
556
574
  }
557
575
  // Editing
558
- if (key.name === 'backspace') {
576
+ if (key.name === "backspace") {
559
577
  if (inputCursor > 0) {
560
578
  inputText = inputText.slice(0, inputCursor - 1) + inputText.slice(inputCursor);
561
579
  inputCursor--;
562
580
  }
563
581
  }
564
- else if (key.name === 'delete') {
582
+ else if (key.name === "delete") {
565
583
  inputText = inputText.slice(0, inputCursor) + inputText.slice(inputCursor + 1);
566
584
  }
567
- else if (key.name === 'left') {
585
+ else if (key.name === "left") {
568
586
  if (inputCursor > 0)
569
587
  inputCursor--;
570
588
  }
571
- else if (key.name === 'right') {
589
+ else if (key.name === "right") {
572
590
  if (inputCursor < inputText.length)
573
591
  inputCursor++;
574
592
  }
575
- else if (key.ctrl && key.char === 'a') {
593
+ else if (key.ctrl && key.char === "a") {
576
594
  inputCursor = 0;
577
595
  }
578
- else if (key.ctrl && key.char === 'e') {
596
+ else if (key.ctrl && key.char === "e") {
579
597
  inputCursor = inputText.length;
580
598
  }
581
599
  else if (key.char && key.char.length === 1 && !key.ctrl && !key.meta) {
@@ -587,9 +605,20 @@ export async function startREPL(config) {
587
605
  updateAutocomplete();
588
606
  // Sync local aliases back to store after each keypress
589
607
  store.setState({
590
- messages, loading, currentModel, inputText, inputCursor,
591
- inputHistory, historyIndex, vimMode, fastMode,
592
- acSuggestions, acDescriptions, acIndex, acTokenStart, acIsPath,
608
+ messages,
609
+ loading,
610
+ currentModel,
611
+ inputText,
612
+ inputCursor,
613
+ inputHistory,
614
+ historyIndex,
615
+ vimMode,
616
+ fastMode,
617
+ acSuggestions,
618
+ acDescriptions,
619
+ acIndex,
620
+ acTokenStart,
621
+ acIsPath,
593
622
  });
594
623
  });
595
624
  function navigateHistory(dir) {
@@ -600,7 +629,7 @@ export async function startREPL(config) {
600
629
  else if (dir > 0) {
601
630
  if (historyIndex <= 0) {
602
631
  historyIndex = -1;
603
- inputText = '';
632
+ inputText = "";
604
633
  }
605
634
  else {
606
635
  historyIndex--;
@@ -615,29 +644,65 @@ export async function startREPL(config) {
615
644
  // Clear any previous errors on new input
616
645
  renderer.setError(null);
617
646
  // Exit
618
- if (input === 'exit' || input === 'quit' || input === '/exit' || input === '/quit' || input === '/q') {
647
+ if (input === "exit" || input === "quit" || input === "/exit" || input === "/quit" || input === "/q") {
619
648
  // Hibernate: save session state for potential wake-up resume
620
649
  try {
621
- const { buildHibernateState } = await import('./harness/session.js');
650
+ const { buildHibernateState } = await import("./harness/session.js");
622
651
  session.hibernate = buildHibernateState(messages);
623
652
  }
624
- catch { /* ignore */ }
653
+ catch {
654
+ /* ignore */
655
+ }
625
656
  // Dream consolidation: prune stale memories before exit
626
657
  try {
627
- const { consolidateMemories } = await import('./harness/memory.js');
628
- const { readOhConfig } = await import('./harness/config.js');
658
+ const { consolidateMemories } = await import("./harness/memory.js");
659
+ const { readOhConfig } = await import("./harness/config.js");
629
660
  const ohCfg = readOhConfig();
630
661
  if (ohCfg?.memory?.consolidateOnExit !== false) {
631
662
  consolidateMemories();
632
663
  }
633
664
  }
634
- catch { /* ignore */ }
665
+ catch {
666
+ /* ignore */
667
+ }
668
+ // Post-session learning: extract skills + update user profile
669
+ try {
670
+ const { runExtraction } = await import("./services/SkillExtractor.js");
671
+ const { updateUserProfile, loadUserProfile, detectMemories } = await import("./harness/memory.js");
672
+ // Skill extraction (async, may take a few seconds)
673
+ const extracted = await runExtraction(config.provider, messages, session.id, currentModel);
674
+ if (extracted.length > 0) {
675
+ console.log(`[learn] Extracted ${extracted.length} skill(s) from this session.`);
676
+ }
677
+ // User profile update
678
+ if (messages.length >= 6) {
679
+ const detected = await detectMemories(config.provider, messages, currentModel);
680
+ const profileUpdates = detected.filter((d) => d.type === "user");
681
+ if (profileUpdates.length > 0) {
682
+ const currentProfile = loadUserProfile();
683
+ const newObservations = profileUpdates.map((d) => d.content).join("\n");
684
+ const merged = currentProfile
685
+ ? `${currentProfile}\n\n## Recent Observations\n${newObservations}`
686
+ : newObservations;
687
+ updateUserProfile(merged);
688
+ }
689
+ }
690
+ }
691
+ catch {
692
+ /* learning is optional — don't block exit */
693
+ }
635
694
  // Emit sessionEnd hook
636
695
  try {
637
- const { emitHookAsync } = await import('./harness/hooks.js');
638
- await emitHookAsync('sessionEnd', {});
696
+ const { emitHookAsync } = await import("./harness/hooks.js");
697
+ await emitHookAsync("sessionEnd", {
698
+ sessionId: session.id,
699
+ model: currentModel,
700
+ provider: config.provider.name,
701
+ });
702
+ }
703
+ catch {
704
+ /* ignore */
639
705
  }
640
- catch { /* ignore */ }
641
706
  cleanup();
642
707
  process.exit(0);
643
708
  }
@@ -653,14 +718,14 @@ export async function startREPL(config) {
653
718
  messages = result.messages;
654
719
  // Check for special commands
655
720
  const lastMsg = messages[messages.length - 1];
656
- if (lastMsg?.content === '__OPEN_SESSION_BROWSER__') {
721
+ if (lastMsg?.content === "__OPEN_SESSION_BROWSER__") {
657
722
  messages = messages.slice(0, -1);
658
723
  renderer.openSessionBrowser();
659
724
  syncRenderer();
660
725
  return;
661
726
  }
662
- if (lastMsg?.content?.startsWith('__SWITCH_THEME__:')) {
663
- const themeName = lastMsg.content.split(':')[1];
727
+ if (lastMsg?.content?.startsWith("__SWITCH_THEME__:")) {
728
+ const themeName = lastMsg.content.split(":")[1];
664
729
  messages = messages.slice(0, -1);
665
730
  setActiveTheme(themeName);
666
731
  resetStyleCache();
@@ -668,35 +733,41 @@ export async function startREPL(config) {
668
733
  resetDiffStyleCache();
669
734
  // Persist theme to config
670
735
  try {
671
- const cfg = cachedConfig ?? { provider: config.provider.name, model: currentModel, permissionMode: config.permissionMode };
736
+ const cfg = cachedConfig ?? {
737
+ provider: config.provider.name,
738
+ model: currentModel,
739
+ permissionMode: config.permissionMode,
740
+ };
672
741
  cfg.theme = themeName;
673
742
  writeOhConfig(cfg);
674
743
  cachedConfig = cfg;
675
744
  }
676
- catch { /* ignore */ }
745
+ catch {
746
+ /* ignore */
747
+ }
677
748
  messages = [...messages, createInfoMessage(`Theme switched to ${themeName}`)];
678
749
  syncRenderer();
679
750
  return;
680
751
  }
681
- if (lastMsg?.content === '__COMPANION_OFF__' || lastMsg?.content === '__COMPANION_ON__') {
682
- companionVisible = lastMsg.content === '__COMPANION_ON__';
752
+ if (lastMsg?.content === "__COMPANION_OFF__" || lastMsg?.content === "__COMPANION_ON__") {
753
+ companionVisible = lastMsg.content === "__COMPANION_ON__";
683
754
  messages = messages.slice(0, -1);
684
755
  if (!companionVisible)
685
- renderer.setCompanion(null, 'cyan');
686
- messages = [...messages, createInfoMessage(`Companion ${companionVisible ? 'shown' : 'hidden'}`)];
756
+ renderer.setCompanion(null, "cyan");
757
+ messages = [...messages, createInfoMessage(`Companion ${companionVisible ? "shown" : "hidden"}`)];
687
758
  syncRenderer();
688
759
  return;
689
760
  }
690
761
  if (result.newModel)
691
762
  currentModel = result.newModel;
692
763
  if (result.vimToggled) {
693
- vimMode = vimMode === null ? 'normal' : null;
694
- messages = [...messages, createInfoMessage(vimMode ? 'Vim mode ON' : 'Vim mode OFF')];
764
+ vimMode = vimMode === null ? "normal" : null;
765
+ messages = [...messages, createInfoMessage(vimMode ? "Vim mode ON" : "Vim mode OFF")];
695
766
  renderer.setVimMode(vimMode);
696
767
  }
697
768
  if (result.fastModeToggled) {
698
769
  fastMode = !fastMode;
699
- messages = [...messages, createInfoMessage(fastMode ? 'Fast mode ON — optimized for speed' : 'Fast mode OFF')];
770
+ messages = [...messages, createInfoMessage(fastMode ? "Fast mode ON — optimized for speed" : "Fast mode OFF")];
700
771
  }
701
772
  // Clear old live area BEFORE syncRenderer when a query will follow.
702
773
  // syncRenderer → scheduleRender → queueMicrotask(render). The microtask fires
@@ -721,16 +792,17 @@ export async function startREPL(config) {
721
792
  renderer.setError(null);
722
793
  renderer.clearToolCalls();
723
794
  abortController = new AbortController();
724
- let accumulated = '';
795
+ let accumulated = "";
725
796
  const callIdToToolName = new Map();
726
797
  const askUser = (toolName, description, riskLevel) => {
727
- return renderer.askPermission(toolName, description, riskLevel ?? 'medium');
798
+ return renderer.askPermission(toolName, description, riskLevel ?? "medium");
728
799
  };
729
800
  const askUserQuestion = (question, options) => {
730
801
  return renderer.askQuestion(question, options);
731
802
  };
732
803
  const effectiveSystemPrompt = fastMode
733
- ? config.systemPrompt + "\n\nIMPORTANT: Fast mode is active. Be extremely concise. Skip explanations. Go straight to the answer or action."
804
+ ? config.systemPrompt +
805
+ "\n\nIMPORTANT: Fast mode is active. Be extremely concise. Skip explanations. Go straight to the answer or action."
734
806
  : config.systemPrompt;
735
807
  const queryConfig = {
736
808
  provider: config.provider,
@@ -745,116 +817,135 @@ export async function startREPL(config) {
745
817
  try {
746
818
  for await (const event of query(prompt, queryConfig, messages)) {
747
819
  switch (event.type) {
748
- case 'text_delta':
820
+ case "text_delta": {
749
821
  // Content auto-scrolls via terminal native scrollback
750
822
  accumulated += event.content;
751
823
  // Move completed lines to messages, keep partial in streaming
752
- const lines = accumulated.split('\n');
824
+ const lines = accumulated.split("\n");
753
825
  if (lines.length > 1) {
754
- const completedText = lines.slice(0, -1).join('\n');
826
+ const completedText = lines.slice(0, -1).join("\n");
755
827
  const last = messages[messages.length - 1];
756
828
  if (last?.meta?.isStreaming) {
757
- messages = [...messages.slice(0, -1), { ...last, content: last.content + completedText + '\n' }];
829
+ messages = [...messages.slice(0, -1), { ...last, content: `${last.content + completedText}\n` }];
758
830
  }
759
831
  else {
760
- messages = [...messages, createMessage('assistant', completedText + '\n', { meta: { isStreaming: true } })];
832
+ messages = [
833
+ ...messages,
834
+ createMessage("assistant", `${completedText}\n`, { meta: { isStreaming: true } }),
835
+ ];
761
836
  }
762
837
  accumulated = lines[lines.length - 1];
763
838
  }
764
839
  renderer.setMessages(messages);
765
840
  renderer.setStreamingText(accumulated);
766
841
  break;
767
- case 'thinking_delta':
842
+ }
843
+ case "thinking_delta":
768
844
  if (!renderer.getThinkingStartedAt())
769
845
  renderer.setThinkingStartedAt(Date.now());
770
846
  renderer.setThinkingText(event.content);
771
847
  break;
772
- case 'tool_call_start': {
848
+ case "tool_call_start": {
773
849
  callIdToToolName.set(event.callId, event.toolName);
774
- const isAgentTool = event.toolName === 'Agent' || event.toolName === 'ParallelAgents';
775
- renderer.setToolCall(event.callId, { toolName: event.toolName, status: 'running', startedAt: Date.now(), isAgent: isAgentTool });
850
+ const isAgentTool = event.toolName === "Agent" || event.toolName === "ParallelAgents";
851
+ renderer.setToolCall(event.callId, {
852
+ toolName: event.toolName,
853
+ status: "running",
854
+ startedAt: Date.now(),
855
+ isAgent: isAgentTool,
856
+ });
776
857
  break;
777
858
  }
778
- case 'tool_call_complete': {
779
- const tcToolName = callIdToToolName.get(event.callId) ?? '';
859
+ case "tool_call_complete": {
860
+ const tcToolName = callIdToToolName.get(event.callId) ?? "";
780
861
  const existingTc = renderer.getToolCall(event.callId);
781
- const isAgentCall = tcToolName === 'Agent' || tcToolName === 'ParallelAgents';
782
- const agentDesc = isAgentCall ? event.arguments.description : undefined;
862
+ const isAgentCall = tcToolName === "Agent" || tcToolName === "ParallelAgents";
863
+ const agentDesc = isAgentCall
864
+ ? event.arguments.description
865
+ : undefined;
783
866
  renderer.setToolCall(event.callId, {
784
867
  ...existingTc,
785
868
  toolName: tcToolName,
786
- status: 'running',
869
+ status: "running",
787
870
  args: formatToolArgs(tcToolName, event.arguments),
788
871
  agentDescription: agentDesc ?? existingTc?.agentDescription,
789
872
  });
790
873
  break;
791
874
  }
792
- case 'tool_output_delta': {
875
+ case "tool_output_delta": {
793
876
  // Accumulate streaming output lines
794
877
  const existing = renderer.getToolCall(event.callId) ?? {
795
- toolName: callIdToToolName.get(event.callId) ?? 'unknown',
796
- status: 'running',
878
+ toolName: callIdToToolName.get(event.callId) ?? "unknown",
879
+ status: "running",
797
880
  };
798
881
  const lines = existing.liveOutput ?? [];
799
- const chunks = event.chunk.split('\n');
882
+ const chunks = event.chunk.split("\n");
800
883
  const merged = [...lines];
801
- if (merged.length > 0 && !event.chunk.startsWith('\n')) {
802
- merged[merged.length - 1] = (merged[merged.length - 1] ?? '') + chunks[0];
803
- merged.push(...chunks.slice(1).filter((c) => c !== ''));
884
+ if (merged.length > 0 && !event.chunk.startsWith("\n")) {
885
+ merged[merged.length - 1] = (merged[merged.length - 1] ?? "") + chunks[0];
886
+ merged.push(...chunks.slice(1).filter((c) => c !== ""));
804
887
  }
805
888
  else {
806
- merged.push(...chunks.filter((c) => c !== ''));
889
+ merged.push(...chunks.filter((c) => c !== ""));
807
890
  }
808
891
  renderer.setToolCall(event.callId, { ...existing, liveOutput: merged });
809
892
  break;
810
893
  }
811
- case 'tool_call_end': {
894
+ case "tool_call_end": {
812
895
  const toolName = callIdToToolName.get(event.callId) ?? event.callId;
813
896
  const prevTc = renderer.getToolCall(event.callId);
814
- const elapsed = prevTc?.startedAt ? Math.floor((Date.now() - prevTc.startedAt) / 1000) : 0;
897
+ const _elapsed = prevTc?.startedAt ? Math.floor((Date.now() - prevTc.startedAt) / 1000) : 0;
815
898
  renderer.setToolCall(event.callId, {
816
899
  toolName,
817
- status: event.isError ? 'error' : 'done',
900
+ status: event.isError ? "error" : "done",
818
901
  output: event.output?.slice(0, 500),
819
902
  args: prevTc?.args,
820
903
  resultSummary: event.output ? summarizeToolOutput(event.output) : undefined,
821
904
  startedAt: prevTc?.startedAt,
822
905
  });
823
- cybergotchiEvents.emit('cybergotchi', { type: event.isError ? 'toolError' : 'toolSuccess', toolName });
906
+ cybergotchiEvents.emit("cybergotchi", { type: event.isError ? "toolError" : "toolSuccess", toolName });
824
907
  // Auto-commit with file list
825
908
  if (!event.isError && isGitRepo()) {
826
- const rawArgs = prevTc?.args ?? '';
827
- const filePath = rawArgs.startsWith('$') ? null : rawArgs;
909
+ const rawArgs = prevTc?.args ?? "";
910
+ const filePath = rawArgs.startsWith("$") ? null : rawArgs;
828
911
  const hash = autoCommitAIEdits(toolName, filePath ? [filePath] : [], process.cwd());
829
912
  if (hash) {
830
913
  // Show changed files in commit message
831
914
  let commitMsg = `git: committed ${hash}`;
832
915
  try {
833
- const { execSync } = await import('node:child_process');
834
- const files = execSync(`git diff-tree --no-commit-id --name-only -r ${hash}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
916
+ const { execSync } = await import("node:child_process");
917
+ const files = execSync(`git diff-tree --no-commit-id --name-only -r ${hash}`, {
918
+ encoding: "utf-8",
919
+ stdio: ["pipe", "pipe", "pipe"],
920
+ }).trim();
835
921
  if (files)
836
- commitMsg += `\n${files.split('\n').map(f => ` ${f}`).join('\n')}`;
922
+ commitMsg += `\n${files
923
+ .split("\n")
924
+ .map((f) => ` ${f}`)
925
+ .join("\n")}`;
926
+ }
927
+ catch {
928
+ /* ignore */
837
929
  }
838
- catch { /* ignore */ }
839
930
  messages = [...messages, createInfoMessage(commitMsg)];
840
- cybergotchiEvents.emit('cybergotchi', { type: 'commit' });
931
+ cybergotchiEvents.emit("cybergotchi", { type: "commit" });
841
932
  }
842
933
  }
843
934
  break;
844
935
  }
845
- case 'cost_update':
936
+ case "cost_update":
846
937
  currentModel = event.model;
847
- cost.record('provider', event.model, event.inputTokens, event.outputTokens, event.cost || estimateCost(event.model, event.inputTokens, event.outputTokens));
938
+ cost.record("provider", event.model, event.inputTokens, event.outputTokens, event.cost || estimateCost(event.model, event.inputTokens, event.outputTokens));
848
939
  renderer.setTokenCount(cost.totalOutputTokens);
849
940
  syncRenderer();
850
941
  break;
851
- case 'rate_limited':
942
+ case "rate_limited":
852
943
  renderer.setError(`⏳ Rate limited — retrying in ${event.retryIn}s (attempt ${event.attempt}/3)`);
853
944
  break;
854
- case 'error':
945
+ case "error":
855
946
  renderer.setError(event.message);
856
947
  break;
857
- case 'turn_complete': {
948
+ case "turn_complete": {
858
949
  // Save thinking summary before clearing
859
950
  const thinkElapsed = renderer.getThinkingStartedAt()
860
951
  ? Math.floor((Date.now() - renderer.getThinkingStartedAt()) / 1000)
@@ -865,7 +956,7 @@ export async function startREPL(config) {
865
956
  else {
866
957
  renderer.setLastThinkingSummary(null);
867
958
  }
868
- renderer.setThinkingText('');
959
+ renderer.setThinkingText("");
869
960
  renderer.setThinkingStartedAt(null);
870
961
  // Finalize streaming message
871
962
  if (accumulated) {
@@ -876,7 +967,7 @@ export async function startREPL(config) {
876
967
  else {
877
968
  messages = [...messages, createAssistantMessage(accumulated)];
878
969
  }
879
- accumulated = '';
970
+ accumulated = "";
880
971
  }
881
972
  else {
882
973
  const last = messages[messages.length - 1];
@@ -884,7 +975,7 @@ export async function startREPL(config) {
884
975
  messages = [...messages.slice(0, -1), { ...last, meta: {} }];
885
976
  }
886
977
  }
887
- renderer.setStreamingText('');
978
+ renderer.setStreamingText("");
888
979
  // Collapse all tool calls from this turn (clean up visual noise)
889
980
  renderer.collapseAllToolCalls();
890
981
  // Save session
@@ -893,7 +984,9 @@ export async function startREPL(config) {
893
984
  try {
894
985
  saveSession(session);
895
986
  }
896
- catch { /* ignore */ }
987
+ catch {
988
+ /* ignore */
989
+ }
897
990
  break;
898
991
  }
899
992
  }
@@ -912,14 +1005,14 @@ export async function startREPL(config) {
912
1005
  messages = [...messages.slice(0, -1), { ...last, content: last.content + accumulated, meta: {} }];
913
1006
  }
914
1007
  else {
915
- messages = [...messages, createAssistantMessage(accumulated + '\n\n[interrupted]')];
1008
+ messages = [...messages, createAssistantMessage(`${accumulated}\n\n[interrupted]`)];
916
1009
  }
917
- accumulated = '';
1010
+ accumulated = "";
918
1011
  }
919
1012
  loading = false;
920
1013
  abortController = null;
921
1014
  renderer.setLoading(false);
922
- renderer.setStreamingText('');
1015
+ renderer.setStreamingText("");
923
1016
  // Content auto-scrolls via terminal native scrollback
924
1017
  syncRenderer();
925
1018
  }
@@ -938,12 +1031,21 @@ export async function startREPL(config) {
938
1031
  try {
939
1032
  saveSession(session);
940
1033
  }
941
- catch { /* ignore */ }
1034
+ catch {
1035
+ /* ignore */
1036
+ }
942
1037
  }
943
1038
  // Ensure terminal restoration on unexpected exit
944
- process.on('exit', cleanup);
945
- process.on('SIGTERM', () => { cleanup(); process.exit(143); });
946
- process.on('uncaughtException', (err) => { cleanup(); console.error('Fatal:', err); process.exit(1); });
1039
+ process.on("exit", cleanup);
1040
+ process.on("SIGTERM", () => {
1041
+ cleanup();
1042
+ process.exit(143);
1043
+ });
1044
+ process.on("uncaughtException", (err) => {
1045
+ cleanup();
1046
+ console.error("Fatal:", err);
1047
+ process.exit(1);
1048
+ });
947
1049
  // Start
948
1050
  renderer.start();
949
1051
  // Banner is already printed to stdout by main.tsx (visible in terminal scrollback)