bonecode 1.1.0 → 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 (271) hide show
  1. package/README.md +145 -9
  2. package/bin/bonecode +47 -42
  3. package/compat/opencode_adapter.ts +188 -17
  4. package/dist/bone/output/agent/src/algorithms.d.ts +1 -0
  5. package/dist/bone/output/agent/src/algorithms.js +3 -0
  6. package/dist/bone/output/agent/src/algorithms.js.map +1 -0
  7. package/dist/bone/output/agent/src/audit.d.ts +3 -0
  8. package/dist/bone/output/agent/src/audit.js +40 -0
  9. package/dist/bone/output/agent/src/audit.js.map +1 -0
  10. package/dist/bone/output/agent/src/auth.d.ts +8 -0
  11. package/dist/bone/output/agent/src/auth.js +56 -0
  12. package/dist/bone/output/agent/src/auth.js.map +1 -0
  13. package/dist/bone/output/agent/src/db.d.ts +6 -0
  14. package/dist/bone/output/agent/src/db.js +63 -0
  15. package/dist/bone/output/agent/src/db.js.map +1 -0
  16. package/dist/bone/output/agent/src/events.d.ts +25 -0
  17. package/dist/bone/output/agent/src/events.js +184 -0
  18. package/dist/bone/output/agent/src/events.js.map +1 -0
  19. package/dist/bone/output/agent/src/logger.d.ts +28 -0
  20. package/dist/bone/output/agent/src/logger.js +45 -0
  21. package/dist/bone/output/agent/src/logger.js.map +1 -0
  22. package/dist/bone/output/agent/src/metrics.d.ts +5 -0
  23. package/dist/bone/output/agent/src/metrics.js +60 -0
  24. package/dist/bone/output/agent/src/metrics.js.map +1 -0
  25. package/dist/bone/output/agent/src/routes/agent_instance.d.ts +1 -0
  26. package/dist/bone/output/agent/src/routes/agent_instance.js +253 -0
  27. package/dist/bone/output/agent/src/routes/agent_instance.js.map +1 -0
  28. package/dist/bone/output/agent/src/routes/build_step.d.ts +1 -0
  29. package/dist/bone/output/agent/src/routes/build_step.js +133 -0
  30. package/dist/bone/output/agent/src/routes/build_step.js.map +1 -0
  31. package/dist/bone/output/agent/src/routes/plan.d.ts +1 -0
  32. package/dist/bone/output/agent/src/routes/plan.js +119 -0
  33. package/dist/bone/output/agent/src/routes/plan.js.map +1 -0
  34. package/dist/bone/output/agent/src/routes/task.d.ts +1 -0
  35. package/dist/bone/output/agent/src/routes/task.js +133 -0
  36. package/dist/bone/output/agent/src/routes/task.js.map +1 -0
  37. package/dist/bone/output/agent/src/routes/tool_call.d.ts +1 -0
  38. package/dist/bone/output/agent/src/routes/tool_call.js +190 -0
  39. package/dist/bone/output/agent/src/routes/tool_call.js.map +1 -0
  40. package/dist/bone/output/agent/src/state_machines/agent_instance.d.ts +9 -0
  41. package/dist/bone/output/agent/src/state_machines/agent_instance.js +22 -0
  42. package/dist/bone/output/agent/src/state_machines/agent_instance.js.map +1 -0
  43. package/dist/bone/output/agent/src/state_machines/build_step.d.ts +9 -0
  44. package/dist/bone/output/agent/src/state_machines/build_step.js +20 -0
  45. package/dist/bone/output/agent/src/state_machines/build_step.js.map +1 -0
  46. package/dist/bone/output/agent/src/state_machines/plan.d.ts +9 -0
  47. package/dist/bone/output/agent/src/state_machines/plan.js +20 -0
  48. package/dist/bone/output/agent/src/state_machines/plan.js.map +1 -0
  49. package/dist/bone/output/agent/src/state_machines/task.d.ts +9 -0
  50. package/dist/bone/output/agent/src/state_machines/task.js +20 -0
  51. package/dist/bone/output/agent/src/state_machines/task.js.map +1 -0
  52. package/dist/bone/output/agent/src/state_machines/tool_call.d.ts +9 -0
  53. package/dist/bone/output/agent/src/state_machines/tool_call.js +20 -0
  54. package/dist/bone/output/agent/src/state_machines/tool_call.js.map +1 -0
  55. package/dist/bone/output/rag/src/algorithms.d.ts +1 -0
  56. package/dist/bone/output/rag/src/algorithms.js +3 -0
  57. package/dist/bone/output/rag/src/algorithms.js.map +1 -0
  58. package/dist/bone/output/rag/src/auth.d.ts +8 -0
  59. package/dist/bone/output/rag/src/auth.js +56 -0
  60. package/dist/bone/output/rag/src/auth.js.map +1 -0
  61. package/dist/bone/output/rag/src/db.d.ts +6 -0
  62. package/dist/bone/output/rag/src/db.js +63 -0
  63. package/dist/bone/output/rag/src/db.js.map +1 -0
  64. package/dist/bone/output/rag/src/events.d.ts +25 -0
  65. package/dist/bone/output/rag/src/events.js +184 -0
  66. package/dist/bone/output/rag/src/events.js.map +1 -0
  67. package/dist/bone/output/rag/src/extensions.d.ts +83 -0
  68. package/dist/bone/output/rag/src/extensions.js +329 -0
  69. package/dist/bone/output/rag/src/extensions.js.map +1 -0
  70. package/dist/bone/output/rag/src/flows.d.ts +24 -0
  71. package/dist/bone/output/rag/src/flows.js +236 -0
  72. package/dist/bone/output/rag/src/flows.js.map +1 -0
  73. package/dist/bone/output/rag/src/logger.d.ts +28 -0
  74. package/dist/bone/output/rag/src/logger.js +45 -0
  75. package/dist/bone/output/rag/src/logger.js.map +1 -0
  76. package/dist/bone/output/rag/src/metrics.d.ts +5 -0
  77. package/dist/bone/output/rag/src/metrics.js +60 -0
  78. package/dist/bone/output/rag/src/metrics.js.map +1 -0
  79. package/dist/bone/output/rag/src/routes/code_chunk.d.ts +1 -0
  80. package/dist/bone/output/rag/src/routes/code_chunk.js +100 -0
  81. package/dist/bone/output/rag/src/routes/code_chunk.js.map +1 -0
  82. package/dist/bone/output/rag/src/routes/code_file.d.ts +1 -0
  83. package/dist/bone/output/rag/src/routes/code_file.js +127 -0
  84. package/dist/bone/output/rag/src/routes/code_file.js.map +1 -0
  85. package/dist/bone/output/rag/src/routes/indexing_job.d.ts +1 -0
  86. package/dist/bone/output/rag/src/routes/indexing_job.js +113 -0
  87. package/dist/bone/output/rag/src/routes/indexing_job.js.map +1 -0
  88. package/dist/bone/output/rag/src/routes/knowledge_base.d.ts +1 -0
  89. package/dist/bone/output/rag/src/routes/knowledge_base.js +242 -0
  90. package/dist/bone/output/rag/src/routes/knowledge_base.js.map +1 -0
  91. package/dist/bone/output/rag/src/routes/memory_entry.d.ts +1 -0
  92. package/dist/bone/output/rag/src/routes/memory_entry.js +113 -0
  93. package/dist/bone/output/rag/src/routes/memory_entry.js.map +1 -0
  94. package/dist/bone/output/rag/src/state_machines/code_file.d.ts +9 -0
  95. package/dist/bone/output/rag/src/state_machines/code_file.js +21 -0
  96. package/dist/bone/output/rag/src/state_machines/code_file.js.map +1 -0
  97. package/dist/bone/output/rag/src/state_machines/indexing_job.d.ts +9 -0
  98. package/dist/bone/output/rag/src/state_machines/indexing_job.js +20 -0
  99. package/dist/bone/output/rag/src/state_machines/indexing_job.js.map +1 -0
  100. package/dist/bone/output/rag/src/state_machines/knowledge_base.d.ts +9 -0
  101. package/dist/bone/output/rag/src/state_machines/knowledge_base.js +21 -0
  102. package/dist/bone/output/rag/src/state_machines/knowledge_base.js.map +1 -0
  103. package/dist/bone/output/rag/src/state_machines/memory_entry.d.ts +9 -0
  104. package/dist/bone/output/rag/src/state_machines/memory_entry.js +18 -0
  105. package/dist/bone/output/rag/src/state_machines/memory_entry.js.map +1 -0
  106. package/dist/bone/output/session/src/algorithms.d.ts +1 -0
  107. package/dist/bone/output/session/src/algorithms.js +3 -0
  108. package/dist/bone/output/session/src/algorithms.js.map +1 -0
  109. package/dist/bone/output/session/src/audit.d.ts +3 -0
  110. package/dist/bone/output/session/src/audit.js +40 -0
  111. package/dist/bone/output/session/src/audit.js.map +1 -0
  112. package/dist/bone/output/session/src/auth.d.ts +8 -0
  113. package/dist/bone/output/session/src/auth.js +56 -0
  114. package/dist/bone/output/session/src/auth.js.map +1 -0
  115. package/dist/bone/output/session/src/db.d.ts +6 -0
  116. package/dist/bone/output/session/src/db.js +63 -0
  117. package/dist/bone/output/session/src/db.js.map +1 -0
  118. package/dist/bone/output/session/src/events.d.ts +26 -0
  119. package/dist/bone/output/session/src/events.js +212 -0
  120. package/dist/bone/output/session/src/events.js.map +1 -0
  121. package/dist/bone/output/session/src/extensions.d.ts +41 -0
  122. package/dist/bone/output/session/src/extensions.js +217 -0
  123. package/dist/bone/output/session/src/extensions.js.map +1 -0
  124. package/dist/bone/output/session/src/logger.d.ts +28 -0
  125. package/dist/bone/output/session/src/logger.js +44 -0
  126. package/dist/bone/output/session/src/logger.js.map +1 -0
  127. package/dist/bone/output/session/src/metrics.d.ts +5 -0
  128. package/dist/bone/output/session/src/metrics.js +60 -0
  129. package/dist/bone/output/session/src/metrics.js.map +1 -0
  130. package/dist/bone/output/session/src/routes/message.d.ts +1 -0
  131. package/dist/bone/output/session/src/routes/message.js +120 -0
  132. package/dist/bone/output/session/src/routes/message.js.map +1 -0
  133. package/dist/bone/output/session/src/routes/part.d.ts +1 -0
  134. package/dist/bone/output/session/src/routes/part.js +106 -0
  135. package/dist/bone/output/session/src/routes/part.js.map +1 -0
  136. package/dist/bone/output/session/src/routes/permission.d.ts +1 -0
  137. package/dist/bone/output/session/src/routes/permission.js +106 -0
  138. package/dist/bone/output/session/src/routes/permission.js.map +1 -0
  139. package/dist/bone/output/session/src/routes/project.d.ts +1 -0
  140. package/dist/bone/output/session/src/routes/project.js +106 -0
  141. package/dist/bone/output/session/src/routes/project.js.map +1 -0
  142. package/dist/bone/output/session/src/routes/session.d.ts +1 -0
  143. package/dist/bone/output/session/src/routes/session.js +308 -0
  144. package/dist/bone/output/session/src/routes/session.js.map +1 -0
  145. package/dist/bone/output/session/src/state_machines/session.d.ts +9 -0
  146. package/dist/bone/output/session/src/state_machines/session.js +21 -0
  147. package/dist/bone/output/session/src/state_machines/session.js.map +1 -0
  148. package/dist/bone/output/session/src/websocket.d.ts +15 -0
  149. package/dist/bone/output/session/src/websocket.js +215 -0
  150. package/dist/bone/output/session/src/websocket.js.map +1 -0
  151. package/dist/bone/output/workspace/src/algorithms.d.ts +1 -0
  152. package/dist/bone/output/workspace/src/algorithms.js +3 -0
  153. package/dist/bone/output/workspace/src/algorithms.js.map +1 -0
  154. package/dist/bone/output/workspace/src/auth.d.ts +8 -0
  155. package/dist/bone/output/workspace/src/auth.js +56 -0
  156. package/dist/bone/output/workspace/src/auth.js.map +1 -0
  157. package/dist/bone/output/workspace/src/db.d.ts +6 -0
  158. package/dist/bone/output/workspace/src/db.js +63 -0
  159. package/dist/bone/output/workspace/src/db.js.map +1 -0
  160. package/dist/bone/output/workspace/src/events.d.ts +25 -0
  161. package/dist/bone/output/workspace/src/events.js +184 -0
  162. package/dist/bone/output/workspace/src/events.js.map +1 -0
  163. package/dist/bone/output/workspace/src/logger.d.ts +28 -0
  164. package/dist/bone/output/workspace/src/logger.js +45 -0
  165. package/dist/bone/output/workspace/src/logger.js.map +1 -0
  166. package/dist/bone/output/workspace/src/metrics.d.ts +5 -0
  167. package/dist/bone/output/workspace/src/metrics.js +60 -0
  168. package/dist/bone/output/workspace/src/metrics.js.map +1 -0
  169. package/dist/bone/output/workspace/src/routes/codebase.d.ts +1 -0
  170. package/dist/bone/output/workspace/src/routes/codebase.js +113 -0
  171. package/dist/bone/output/workspace/src/routes/codebase.js.map +1 -0
  172. package/dist/bone/output/workspace/src/routes/snapshot.d.ts +1 -0
  173. package/dist/bone/output/workspace/src/routes/snapshot.js +151 -0
  174. package/dist/bone/output/workspace/src/routes/snapshot.js.map +1 -0
  175. package/dist/bone/output/workspace/src/routes/workspace.d.ts +1 -0
  176. package/dist/bone/output/workspace/src/routes/workspace.js +209 -0
  177. package/dist/bone/output/workspace/src/routes/workspace.js.map +1 -0
  178. package/dist/bone/output/workspace/src/state_machines/codebase.d.ts +9 -0
  179. package/dist/bone/output/workspace/src/state_machines/codebase.js +19 -0
  180. package/dist/bone/output/workspace/src/state_machines/codebase.js.map +1 -0
  181. package/dist/bone/output/workspace/src/state_machines/snapshot.d.ts +9 -0
  182. package/dist/bone/output/workspace/src/state_machines/snapshot.js +18 -0
  183. package/dist/bone/output/workspace/src/state_machines/snapshot.js.map +1 -0
  184. package/dist/bone/output/workspace/src/state_machines/workspace.d.ts +9 -0
  185. package/dist/bone/output/workspace/src/state_machines/workspace.js +19 -0
  186. package/dist/bone/output/workspace/src/state_machines/workspace.js.map +1 -0
  187. package/dist/compat/opencode_adapter.d.ts +25 -0
  188. package/dist/compat/opencode_adapter.js +599 -0
  189. package/dist/compat/opencode_adapter.js.map +1 -0
  190. package/dist/extensions/chunker.d.ts +24 -0
  191. package/dist/extensions/chunker.js +360 -0
  192. package/dist/extensions/chunker.js.map +1 -0
  193. package/dist/extensions/embedding_provider.d.ts +18 -0
  194. package/dist/extensions/embedding_provider.js +150 -0
  195. package/dist/extensions/embedding_provider.js.map +1 -0
  196. package/dist/extensions/llm_provider.d.ts +33 -0
  197. package/dist/extensions/llm_provider.js +338 -0
  198. package/dist/extensions/llm_provider.js.map +1 -0
  199. package/dist/extensions/mcp_bridge.d.ts +44 -0
  200. package/dist/extensions/mcp_bridge.js +151 -0
  201. package/dist/extensions/mcp_bridge.js.map +1 -0
  202. package/dist/extensions/rag_search.d.ts +38 -0
  203. package/dist/extensions/rag_search.js +242 -0
  204. package/dist/extensions/rag_search.js.map +1 -0
  205. package/dist/extensions/snapshot.d.ts +14 -0
  206. package/dist/extensions/snapshot.js +158 -0
  207. package/dist/extensions/snapshot.js.map +1 -0
  208. package/dist/extensions/tool_executor.d.ts +28 -0
  209. package/dist/extensions/tool_executor.js +268 -0
  210. package/dist/extensions/tool_executor.js.map +1 -0
  211. package/dist/src/cli.d.ts +15 -0
  212. package/dist/src/cli.js +687 -0
  213. package/dist/src/cli.js.map +1 -0
  214. package/dist/src/config.d.ts +44 -0
  215. package/dist/src/config.js +165 -0
  216. package/dist/src/config.js.map +1 -0
  217. package/dist/src/context_builder.d.ts +51 -0
  218. package/dist/src/context_builder.js +558 -0
  219. package/dist/src/context_builder.js.map +1 -0
  220. package/dist/src/db_adapter.d.ts +24 -0
  221. package/dist/src/db_adapter.js +341 -0
  222. package/dist/src/db_adapter.js.map +1 -0
  223. package/dist/src/engine/session/compaction_logic.d.ts +11 -0
  224. package/dist/src/engine/session/compaction_logic.js +113 -0
  225. package/dist/src/engine/session/compaction_logic.js.map +1 -0
  226. package/dist/src/engine/session/instruction_loader.d.ts +5 -0
  227. package/dist/src/engine/session/instruction_loader.js +78 -0
  228. package/dist/src/engine/session/instruction_loader.js.map +1 -0
  229. package/dist/src/engine/session/overflow_check.d.ts +14 -0
  230. package/dist/src/engine/session/overflow_check.js +45 -0
  231. package/dist/src/engine/session/overflow_check.js.map +1 -0
  232. package/dist/src/engine/session/prompt.d.ts +45 -0
  233. package/dist/src/engine/session/prompt.js +584 -0
  234. package/dist/src/engine/session/prompt.js.map +1 -0
  235. package/dist/src/engine/session/provider_transform.d.ts +59 -0
  236. package/dist/src/engine/session/provider_transform.js +193 -0
  237. package/dist/src/engine/session/provider_transform.js.map +1 -0
  238. package/dist/src/engine/session/retry_logic.d.ts +12 -0
  239. package/dist/src/engine/session/retry_logic.js +72 -0
  240. package/dist/src/engine/session/retry_logic.js.map +1 -0
  241. package/dist/src/engine/session/system_prompt.d.ts +9 -0
  242. package/dist/src/engine/session/system_prompt.js +96 -0
  243. package/dist/src/engine/session/system_prompt.js.map +1 -0
  244. package/dist/src/engine/session/tool_registry.d.ts +5 -0
  245. package/dist/src/engine/session/tool_registry.js +117 -0
  246. package/dist/src/engine/session/tool_registry.js.map +1 -0
  247. package/dist/src/export.d.ts +13 -0
  248. package/dist/src/export.js +103 -0
  249. package/dist/src/export.js.map +1 -0
  250. package/dist/src/mdns.d.ts +7 -0
  251. package/dist/src/mdns.js +60 -0
  252. package/dist/src/mdns.js.map +1 -0
  253. package/dist/src/rag_worker.d.ts +38 -0
  254. package/dist/src/rag_worker.js +435 -0
  255. package/dist/src/rag_worker.js.map +1 -0
  256. package/dist/src/server.d.ts +11 -0
  257. package/dist/src/server.js +214 -0
  258. package/dist/src/server.js.map +1 -0
  259. package/dist/src/stats.d.ts +45 -0
  260. package/dist/src/stats.js +233 -0
  261. package/dist/src/stats.js.map +1 -0
  262. package/dist/src/tui.d.ts +29 -0
  263. package/dist/src/tui.js +1053 -0
  264. package/dist/src/tui.js.map +1 -0
  265. package/package.json +7 -4
  266. package/src/cli.ts +247 -5
  267. package/src/export.ts +122 -0
  268. package/src/mdns.ts +53 -0
  269. package/src/server.ts +32 -0
  270. package/src/stats.ts +290 -0
  271. package/src/tui.ts +749 -248
@@ -0,0 +1,1053 @@
1
+ "use strict";
2
+ /**
3
+ * BoneCode TUI — terminal interface modeled after OpenCode
4
+ *
5
+ * Three core behaviours:
6
+ * 1. Tool activity is displayed as concise status lines
7
+ * (← Edit src/foo.ts, → Read package.json, $ npm test) — never raw code dumps.
8
+ * Assistant text is shown inline; tool calls and tool outputs are summarized.
9
+ *
10
+ * 2. Typing "/" shows an inline command menu above the prompt with
11
+ * arrow-key selection and tab/enter to insert.
12
+ *
13
+ * 3. Ctrl+C aborts the in-flight LLM stream by aborting the fetch
14
+ * AND notifying the server to cancel the agent loop.
15
+ */
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.startInteractiveTUI = exports.runTUI = void 0;
41
+ const path = __importStar(require("path"));
42
+ const fs = __importStar(require("fs"));
43
+ const readline = __importStar(require("readline"));
44
+ const http = __importStar(require("http"));
45
+ // ─── ANSI ─────────────────────────────────────────────────────────────────────
46
+ const ESC = "\x1b";
47
+ const R = `${ESC}[0m`;
48
+ const BOLD = `${ESC}[1m`;
49
+ const DIM = `${ESC}[2m`;
50
+ const CYAN = `${ESC}[96m`;
51
+ const GREEN = `${ESC}[92m`;
52
+ const YELLOW = `${ESC}[93m`;
53
+ const RED = `${ESC}[91m`;
54
+ const GRAY = `${ESC}[90m`;
55
+ const WHITE = `${ESC}[97m`;
56
+ const BLUE = `${ESC}[94m`;
57
+ // Box drawing
58
+ const VERT = "┃";
59
+ const CORN = "╹";
60
+ const SHADE = "▀";
61
+ const BLOCK = "▣";
62
+ const ARROW_R = "→";
63
+ const ARROW_L = "←";
64
+ const DOLLAR = "$";
65
+ const STAR = "✱";
66
+ const PCT = "%";
67
+ const GEAR = "⚙";
68
+ const HASH = "#";
69
+ function cols() { return process.stdout.columns || 80; }
70
+ function out(s) { process.stdout.write(s); }
71
+ function nl(s = "") { process.stdout.write(s + "\n"); }
72
+ function clearLine() { out(`\r${ESC}[2K`); }
73
+ // ─── Logo ─────────────────────────────────────────────────────────────────────
74
+ function printLogo() {
75
+ nl();
76
+ nl(`${GRAY}${BOLD}█▀▀▄ █▀▀█ █▀▀█ █▀▀▀ █▀▀▀ █▀▀█ █▀▀▄ █▀▀▀${R}`);
77
+ nl(`${GRAY}${BOLD}█▀▀▄ █ █ █ █ █▀▀ █ █ █ █ █ █▀▀ ${R}`);
78
+ nl(`${GRAY}${BOLD}▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀▀${R}`);
79
+ nl();
80
+ }
81
+ // ─── Package version ──────────────────────────────────────────────────────────
82
+ const PKG_ROOT = (() => {
83
+ const fromSrc = path.resolve(__dirname, "..");
84
+ const fromDist = path.resolve(__dirname, "..", "..");
85
+ if (fs.existsSync(path.join(fromDist, "package.json")))
86
+ return fromDist;
87
+ return fromSrc;
88
+ })();
89
+ function getVersion() {
90
+ try {
91
+ return JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8")).version;
92
+ }
93
+ catch {
94
+ return "0.0.0";
95
+ }
96
+ }
97
+ const COMMANDS = [
98
+ { name: "/new", description: "Start a new session" },
99
+ { name: "/session", description: "Show current session ID" },
100
+ { name: "/sessions", description: "List recent sessions" },
101
+ { name: "/model", description: "Switch model", args: "<provider/model>" },
102
+ { name: "/provider", description: "Switch provider", args: "<id>" },
103
+ { name: "/providers", description: "List all providers" },
104
+ { name: "/clear", description: "Clear screen" },
105
+ { name: "/history", description: "Show last 10 prompts" },
106
+ { name: "/help", description: "Show this help" },
107
+ { name: "/exit", description: "Exit BoneCode" },
108
+ ];
109
+ // ─── @file autocomplete helpers ───────────────────────────────────────────────
110
+ const IGNORED_DIRS = new Set([
111
+ "node_modules", ".git", "dist", "build", ".next", "__pycache__",
112
+ ".venv", "venv", "target", "vendor", ".cache", "coverage",
113
+ ]);
114
+ const CODE_EXTS = new Set([
115
+ ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs",
116
+ ".java", ".kt", ".cs", ".cpp", ".c", ".h", ".rb", ".php", ".swift",
117
+ ".md", ".mdx", ".json", ".yaml", ".yml", ".toml", ".env", ".sql", ".sh", ".graphql",
118
+ ]);
119
+ function listFiles(worktree, prefix) {
120
+ const results = [];
121
+ function walk(dir, depth) {
122
+ if (results.length >= 50 || depth > 4)
123
+ return;
124
+ let entries;
125
+ try {
126
+ entries = fs.readdirSync(dir, { withFileTypes: true });
127
+ }
128
+ catch {
129
+ return;
130
+ }
131
+ for (const e of entries) {
132
+ if (results.length >= 50)
133
+ break;
134
+ if (IGNORED_DIRS.has(e.name) || e.name.startsWith("."))
135
+ continue;
136
+ const rel = path.relative(worktree, path.join(dir, e.name));
137
+ if (e.isDirectory()) {
138
+ if (!prefix || rel.startsWith(prefix) || prefix.startsWith(rel)) {
139
+ results.push(rel + "/");
140
+ walk(path.join(dir, e.name), depth + 1);
141
+ }
142
+ }
143
+ else if (CODE_EXTS.has(path.extname(e.name).toLowerCase())) {
144
+ if (!prefix || rel.startsWith(prefix))
145
+ results.push(rel);
146
+ }
147
+ }
148
+ }
149
+ walk(worktree, 0);
150
+ return results.sort();
151
+ }
152
+ function buildCompleter(worktree) {
153
+ return (line) => {
154
+ if (line.startsWith("/")) {
155
+ // Tab-complete commands too (in addition to the inline menu)
156
+ const matches = COMMANDS.map(c => c.name).filter(c => c.startsWith(line));
157
+ return [matches, line];
158
+ }
159
+ const atIdx = line.lastIndexOf("@");
160
+ if (atIdx !== -1) {
161
+ const prefix = line.slice(atIdx + 1);
162
+ const completions = listFiles(worktree, prefix).map(f => line.slice(0, atIdx + 1) + f);
163
+ return [completions, line];
164
+ }
165
+ return [[], line];
166
+ };
167
+ }
168
+ // ─── Server health ────────────────────────────────────────────────────────────
169
+ async function waitForServer(port, maxMs = 30000) {
170
+ const start = Date.now();
171
+ while (Date.now() - start < maxMs) {
172
+ try {
173
+ const ok = await new Promise((resolve) => {
174
+ const req = http.get(`http://localhost:${port}/health`, (res) => resolve(res.statusCode === 200));
175
+ req.on("error", () => resolve(false));
176
+ req.setTimeout(1000, () => { req.destroy(); resolve(false); });
177
+ });
178
+ if (ok)
179
+ return true;
180
+ }
181
+ catch { }
182
+ await new Promise(r => setTimeout(r, 500));
183
+ }
184
+ return false;
185
+ }
186
+ // ─── API ──────────────────────────────────────────────────────────────────────
187
+ async function apiGet(url, token) {
188
+ const r = await fetch(url, { headers: { "Authorization": `Bearer ${token}` } });
189
+ if (!r.ok)
190
+ throw new Error(`API ${r.status}`);
191
+ return r.json();
192
+ }
193
+ async function apiPost(url, body, token) {
194
+ return fetch(url, {
195
+ method: "POST",
196
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
197
+ body: JSON.stringify(body),
198
+ });
199
+ }
200
+ async function apiDelete(url, token) {
201
+ await fetch(url, { method: "DELETE", headers: { "Authorization": `Bearer ${token}` } });
202
+ }
203
+ async function createSession(port, token, worktree, title) {
204
+ const r = await apiPost(`http://localhost:${port}/v2/session`, { title, directory: worktree }, token);
205
+ if (!r.ok)
206
+ throw new Error(`Failed to create session: ${await r.text()}`);
207
+ const sess = await r.json();
208
+ if (!sess.id)
209
+ throw new Error("No session ID returned");
210
+ return sess.id;
211
+ }
212
+ function relPath(p, worktree) {
213
+ if (!p)
214
+ return "";
215
+ try {
216
+ if (path.isAbsolute(p)) {
217
+ const rel = path.relative(worktree, p);
218
+ if (!rel.startsWith(".."))
219
+ return rel.replace(/\\/g, "/");
220
+ }
221
+ return p.replace(/\\/g, "/");
222
+ }
223
+ catch {
224
+ return p;
225
+ }
226
+ }
227
+ function describeTool(toolName, input, worktree) {
228
+ const n = (toolName || "").toLowerCase();
229
+ const inp = input || {};
230
+ // Map BoneCode's tool names AND opencode's names
231
+ const isWrite = n === "write" || n === "write_file";
232
+ const isEdit = n === "edit" || n === "edit_file";
233
+ const isRead = n === "read" || n === "read_file";
234
+ const isShell = n === "bash" || n === "shell" || n === "run_command";
235
+ const isGlob = n === "glob";
236
+ const isGrep = n === "grep" || n === "search_files";
237
+ const isList = n === "list_directory" || n === "list" || n === "ls";
238
+ const isWebfetch = n === "webfetch";
239
+ const isWebsearch = n === "websearch";
240
+ const isPatch = n === "apply_patch" || n === "patch";
241
+ const isTodo = n === "todo_write" || n === "todowrite" || n === "todo";
242
+ const isTask = n === "task" || n === "subagent";
243
+ if (isWrite) {
244
+ return { icon: ARROW_L, color: CYAN, title: "Write", detail: relPath(inp.path || inp.filePath, worktree) };
245
+ }
246
+ if (isEdit) {
247
+ return { icon: ARROW_L, color: CYAN, title: "Edit", detail: relPath(inp.path || inp.filePath, worktree) };
248
+ }
249
+ if (isPatch) {
250
+ return { icon: PCT, color: CYAN, title: "Patch", detail: inp.patch ? `${String(inp.patch).split("\n").length} lines` : "" };
251
+ }
252
+ if (isRead) {
253
+ let detail = relPath(inp.path || inp.filePath, worktree);
254
+ if (inp.start_line || inp.end_line) {
255
+ detail += ` ${GRAY}[${inp.start_line || 1}-${inp.end_line || ""}]${R}`;
256
+ }
257
+ return { icon: ARROW_R, color: GRAY, title: "Read", detail };
258
+ }
259
+ if (isShell) {
260
+ const cmd = String(inp.command || "").trim();
261
+ const short = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
262
+ return { icon: DOLLAR, color: YELLOW, title: short || "Shell" };
263
+ }
264
+ if (isGlob) {
265
+ return { icon: STAR, color: GRAY, title: "Glob", detail: inp.pattern || "" };
266
+ }
267
+ if (isGrep) {
268
+ const pattern = inp.pattern || "";
269
+ const glob = inp.glob ? ` in ${inp.glob}` : "";
270
+ return { icon: STAR, color: GRAY, title: "Grep", detail: `"${pattern}"${glob}` };
271
+ }
272
+ if (isList) {
273
+ return { icon: ARROW_R, color: GRAY, title: "List", detail: relPath(inp.path, worktree) };
274
+ }
275
+ if (isWebfetch) {
276
+ return { icon: PCT, color: BLUE, title: "WebFetch", detail: inp.url || "" };
277
+ }
278
+ if (isWebsearch) {
279
+ return { icon: PCT, color: BLUE, title: "WebSearch", detail: inp.query ? `"${inp.query}"` : "" };
280
+ }
281
+ if (isTodo) {
282
+ const todos = Array.isArray(inp.todos) ? inp.todos : [];
283
+ const done = todos.filter((t) => t.status === "completed").length;
284
+ return { icon: HASH, color: GRAY, title: "Todos", detail: `${done}/${todos.length} done` };
285
+ }
286
+ if (isTask) {
287
+ return { icon: HASH, color: CYAN, title: "Task", detail: inp.description || "" };
288
+ }
289
+ // Fallback
290
+ return { icon: GEAR, color: GRAY, title: toolName };
291
+ }
292
+ function renderToolStart(d) {
293
+ const detail = d.detail ? ` ${GRAY}${d.detail}${R}` : "";
294
+ nl(` ${d.color}${d.icon}${R} ${WHITE}${d.title}${R}${detail}`);
295
+ }
296
+ function renderToolDone(d, ms) {
297
+ const detail = d.detail ? ` ${GRAY}${d.detail}${R}` : "";
298
+ const time = ms !== undefined ? ` ${GRAY}${(ms / 1000).toFixed(1)}s${R}` : "";
299
+ // Replace the "..." with a checkmark (or just write a new line if not interactive)
300
+ nl(` ${GREEN}✓${R} ${GRAY}${d.title}${R}${detail}${time}`);
301
+ }
302
+ function renderToolError(d, err) {
303
+ const detail = d.detail ? ` ${GRAY}${d.detail}${R}` : "";
304
+ nl(` ${RED}✗${R} ${GRAY}${d.title}${R}${detail} ${RED}${err}${R}`);
305
+ }
306
+ // ─── User message rendering ───────────────────────────────────────────────────
307
+ function renderUserMessage(text) {
308
+ nl();
309
+ for (const line of text.split("\n")) {
310
+ nl(`${GRAY}${VERT}${R} ${WHITE}${line}${R}`);
311
+ }
312
+ nl();
313
+ }
314
+ function renderTurnEnd(model, elapsedMs, interrupted) {
315
+ const elapsed = (elapsedMs / 1000).toFixed(1);
316
+ const dur = interrupted ? `${YELLOW}interrupted${R}` : `${GRAY}${elapsed}s${R}`;
317
+ nl();
318
+ nl(` ${CYAN}${BLOCK}${R} ${WHITE}Build${R}${GRAY} · ${model} · ${R}${dur}`);
319
+ }
320
+ // ─── Help / providers / commands menu ─────────────────────────────────────────
321
+ function printHelp() {
322
+ nl();
323
+ nl(`${CYAN}${BOLD}BoneCode${R} ${GRAY}commands${R}`);
324
+ nl();
325
+ for (const c of COMMANDS) {
326
+ const args = c.args ? ` ${GRAY}${c.args}${R}` : "";
327
+ nl(` ${CYAN}${c.name.padEnd(12)}${R}${args.padEnd(20)} ${GRAY}${c.description}${R}`);
328
+ }
329
+ nl();
330
+ nl(` ${CYAN}Ctrl+C${R} ${GRAY}Interrupt current request${R}`);
331
+ nl(` ${CYAN}Ctrl+D${R} ${GRAY}Exit BoneCode${R}`);
332
+ nl(` ${CYAN}↑ / ↓${R} ${GRAY}Prompt history${R}`);
333
+ nl(` ${CYAN}@<path>${R} ${GRAY}Attach a file (Tab completes)${R}`);
334
+ nl();
335
+ }
336
+ async function fetchProviders(port, token) {
337
+ try {
338
+ return await apiGet(`http://localhost:${port}/v2/provider`, token);
339
+ }
340
+ catch {
341
+ return [];
342
+ }
343
+ }
344
+ function printProviders(providers, currentProvider) {
345
+ const width = cols();
346
+ nl();
347
+ nl(`${CYAN}${BOLD}Providers${R}`);
348
+ nl(`${GRAY}${"─".repeat(Math.min(width, 56))}${R}`);
349
+ for (const p of providers) {
350
+ const active = p.id === currentProvider;
351
+ const dot = active ? `${GREEN}●${R}` : `${GRAY}○${R}`;
352
+ const free = p.free ? ` ${GREEN}free${R}` : "";
353
+ const local = p.id === "local" ? ` ${CYAN}local config${R}` : "";
354
+ nl(` ${dot} ${active ? CYAN + BOLD : WHITE}${p.id}${R}${free}${local} ${GRAY}${p.name}${R}`);
355
+ if (p.freeNote)
356
+ nl(` ${GRAY}${p.freeNote}${R}`);
357
+ if (p.models?.length)
358
+ nl(` ${GRAY}${p.models.slice(0, 3).join(" ")}${R}`);
359
+ if (p.keyEnv)
360
+ nl(` ${GRAY}${p.keyEnv}${R}`);
361
+ }
362
+ nl(`${GRAY}${"─".repeat(Math.min(width, 56))}${R}`);
363
+ nl(` ${GRAY}/provider <id> /model <id>${R}`);
364
+ nl();
365
+ }
366
+ /**
367
+ * Stateful code-fence collapser.
368
+ *
369
+ * The LLM streams text token-by-token, including ```python\n...\n``` blocks.
370
+ * We don't want to dump that raw to the terminal because:
371
+ * (a) The code is going to be saved via a tool call anyway, so showing it
372
+ * twice (raw stream + saved file path) is redundant.
373
+ * (b) It can be 100s of lines long and overwhelms the TUI.
374
+ *
375
+ * Strategy: buffer everything between ``` and ```. When the closing fence
376
+ * arrives, emit a one-line marker like [code: python, 42 lines]
377
+ * instead of the buffered content.
378
+ *
379
+ * State machine:
380
+ * "text" — pass deltas through verbatim (with partial-fence detection)
381
+ * "fence" — buffer everything; emit marker on close fence
382
+ */
383
+ function makeCodeFenceCollapser() {
384
+ let mode = "text";
385
+ let lang = "";
386
+ let buffered = ""; // bytes accumulated inside a fence
387
+ let pending = ""; // partial-fence buffer (when we see backticks but don't know yet)
388
+ const lines = (s) => s.split("\n").length;
389
+ function flushPending() {
390
+ const out = pending;
391
+ pending = "";
392
+ return out;
393
+ }
394
+ return {
395
+ feed(chunk) {
396
+ let result = "";
397
+ let i = 0;
398
+ const buf = pending + chunk;
399
+ pending = "";
400
+ while (i < buf.length) {
401
+ if (mode === "text") {
402
+ // Look for ``` to enter fence mode
403
+ const fenceIdx = buf.indexOf("```", i);
404
+ if (fenceIdx === -1) {
405
+ // No fence in remaining text — emit it all, but hold the last 2 chars
406
+ // in case they're part of an upcoming fence.
407
+ const safeEnd = Math.max(i, buf.length - 2);
408
+ result += buf.slice(i, safeEnd);
409
+ pending = buf.slice(safeEnd);
410
+ i = buf.length;
411
+ break;
412
+ }
413
+ // Emit text up to the fence
414
+ result += buf.slice(i, fenceIdx);
415
+ // Read the language (everything until newline)
416
+ const nlIdx = buf.indexOf("\n", fenceIdx + 3);
417
+ if (nlIdx === -1) {
418
+ // Don't have the full opening line yet — buffer
419
+ pending = buf.slice(fenceIdx);
420
+ i = buf.length;
421
+ break;
422
+ }
423
+ lang = buf.slice(fenceIdx + 3, nlIdx).trim() || "code";
424
+ i = nlIdx + 1;
425
+ mode = "fence";
426
+ buffered = "";
427
+ // Print a leading marker (will be amended when we close)
428
+ result += `${GRAY}┃ code: ${lang}…${R}`;
429
+ continue;
430
+ }
431
+ // mode === "fence" — look for closing ```
432
+ const closeIdx = buf.indexOf("```", i);
433
+ if (closeIdx === -1) {
434
+ buffered += buf.slice(i);
435
+ // Hold last 2 chars in case they're part of an upcoming close fence
436
+ const safeEnd = Math.max(i, buf.length - 2);
437
+ buffered = buffered.slice(0, buffered.length - (buf.length - safeEnd));
438
+ pending = buf.slice(safeEnd);
439
+ i = buf.length;
440
+ break;
441
+ }
442
+ // Closing fence found
443
+ buffered += buf.slice(i, closeIdx);
444
+ const lineCount = lines(buffered.replace(/\n+$/, ""));
445
+ // Replace the placeholder we already wrote with the final marker.
446
+ // Carriage return + clear line + reprint marker.
447
+ result += `\r${ESC}[2K ${GRAY}┃ code: ${lang}, ${lineCount} line${lineCount === 1 ? "" : "s"}${R}\n`;
448
+ i = closeIdx + 3;
449
+ // Skip optional newline after closing fence
450
+ if (buf[i] === "\n")
451
+ i++;
452
+ mode = "text";
453
+ lang = "";
454
+ buffered = "";
455
+ }
456
+ return result;
457
+ },
458
+ flush() {
459
+ const tail = flushPending();
460
+ if (mode === "fence") {
461
+ // Stream ended mid-fence — close it with an approximate count
462
+ const lineCount = lines(buffered.replace(/\n+$/, ""));
463
+ mode = "text";
464
+ return tail + `\r${ESC}[2K ${GRAY}┃ code: ${lang}, ${lineCount}+ lines${R}\n`;
465
+ }
466
+ return tail;
467
+ },
468
+ };
469
+ }
470
+ async function streamPrompt(opts) {
471
+ const { port, token, sessionId, model, provider, message, worktree, abortSignal } = opts;
472
+ const t0 = Date.now();
473
+ let fullText = "";
474
+ let tokens = 0;
475
+ let inAssistantText = false;
476
+ let tools = new Map();
477
+ // Code-fence collapser — replaces ```...``` blocks with [code: lang, N lines]
478
+ // markers across delta boundaries.
479
+ const fence = makeCodeFenceCollapser();
480
+ const collapseCodeFences = (chunk) => fence.feed(chunk);
481
+ // Track which message the deltas belong to so we can detect tool boundaries
482
+ // The compat adapter emits these event types:
483
+ // part.delta — text chunk
484
+ // tool.requested — tool call started
485
+ // message.updated — message saved
486
+ // session.updated — session state changed
487
+ // error — error event
488
+ const flushTextLine = () => {
489
+ if (inAssistantText) {
490
+ // Make sure assistant text ends with a newline before the next thing
491
+ if (fullText && !fullText.endsWith("\n"))
492
+ out("\n");
493
+ inAssistantText = false;
494
+ }
495
+ };
496
+ try {
497
+ const r = await fetch(`http://localhost:${port}/v2/session/${sessionId}/prompt`, {
498
+ method: "POST",
499
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
500
+ body: JSON.stringify({ content: message, modelID: model, providerID: provider }),
501
+ signal: abortSignal,
502
+ });
503
+ if (!r.ok) {
504
+ return { text: "", tokens: 0, elapsedMs: Date.now() - t0, error: `HTTP ${r.status}: ${await r.text()}`, interrupted: false };
505
+ }
506
+ const reader = r.body.getReader();
507
+ const dec = new TextDecoder();
508
+ let buf = "";
509
+ while (true) {
510
+ const { value, done } = await reader.read();
511
+ if (done)
512
+ break;
513
+ buf += dec.decode(value, { stream: true });
514
+ const lines = buf.split("\n");
515
+ buf = lines.pop() || "";
516
+ for (const raw of lines) {
517
+ if (!raw.startsWith("data: "))
518
+ continue;
519
+ const json = raw.slice(6).trim();
520
+ if (!json || json === "[DONE]")
521
+ continue;
522
+ try {
523
+ const ev = JSON.parse(json);
524
+ // Text delta — assistant is generating prose
525
+ if (ev.type === "part.delta" && ev.delta?.type === "text") {
526
+ const text = ev.delta.text || "";
527
+ if (!text)
528
+ continue;
529
+ if (!inAssistantText) {
530
+ // Start of assistant text — print the indent prefix once
531
+ out(` `);
532
+ inAssistantText = true;
533
+ }
534
+ // Process the delta through the code-fence collapser so streaming
535
+ // code blocks appear as "[code: lang]" placeholders instead of
536
+ // dumping raw source.
537
+ const piece = collapseCodeFences(text);
538
+ // Print with leading-newline indenting (so each new line gets the 3-space prefix)
539
+ const indented = piece.replace(/\n/g, `\n `);
540
+ out(indented);
541
+ fullText += text;
542
+ continue;
543
+ }
544
+ // Tool requested — show concise activity line
545
+ if (ev.type === "tool.requested") {
546
+ flushTextLine();
547
+ const callId = ev.tool_call_id || ev.id || `${ev.tool_name}-${Date.now()}`;
548
+ const display = describeTool(ev.tool_name || "tool", ev.tool_input || {}, worktree);
549
+ tools.set(callId, { callId, display, startedAt: Date.now() });
550
+ renderToolStart(display);
551
+ continue;
552
+ }
553
+ // Tool completed
554
+ if (ev.type === "tool.completed" || ev.type === "tool.success") {
555
+ flushTextLine();
556
+ const callId = ev.tool_call_id || ev.id || "";
557
+ const tracked = tools.get(callId);
558
+ if (tracked) {
559
+ const ms = Date.now() - tracked.startedAt;
560
+ renderToolDone(tracked.display, ms);
561
+ tools.delete(callId);
562
+ }
563
+ continue;
564
+ }
565
+ // Tool failed
566
+ if (ev.type === "tool.failed" || ev.type === "tool.error") {
567
+ flushTextLine();
568
+ const callId = ev.tool_call_id || ev.id || "";
569
+ const tracked = tools.get(callId);
570
+ const err = ev.error || ev.message || "failed";
571
+ if (tracked) {
572
+ renderToolError(tracked.display, err);
573
+ tools.delete(callId);
574
+ }
575
+ else {
576
+ nl(` ${RED}✗ ${err}${R}`);
577
+ }
578
+ continue;
579
+ }
580
+ // Retry notification
581
+ if (ev.type === "session.retry") {
582
+ flushTextLine();
583
+ nl(` ${YELLOW}⟳ Retry ${ev.attempt || ""}: ${ev.message || ""}${R}`);
584
+ continue;
585
+ }
586
+ // Server-side error
587
+ if (ev.type === "error") {
588
+ flushTextLine();
589
+ return {
590
+ text: fullText,
591
+ tokens,
592
+ elapsedMs: Date.now() - t0,
593
+ error: ev.properties?.message || "error",
594
+ interrupted: false,
595
+ };
596
+ }
597
+ // Compaction
598
+ if (ev.type === "session.compacted") {
599
+ flushTextLine();
600
+ nl(` ${BLUE}⊕ Context compacted${R}`);
601
+ continue;
602
+ }
603
+ }
604
+ catch {
605
+ // Ignore malformed events
606
+ }
607
+ }
608
+ }
609
+ flushTextLine();
610
+ // Drain any tools that didn't get a completion event
611
+ for (const tracked of tools.values()) {
612
+ const ms = Date.now() - tracked.startedAt;
613
+ renderToolDone(tracked.display, ms);
614
+ }
615
+ tools.clear();
616
+ // Flush any unclosed code fence
617
+ const tail = fence.flush();
618
+ if (tail)
619
+ out(tail);
620
+ // Fetch the final message to get token totals
621
+ try {
622
+ const msgs = await apiGet(`http://localhost:${port}/v2/session/${sessionId}/message`, token);
623
+ const last = msgs.filter((m) => m.role === "assistant").slice(-1)[0];
624
+ tokens = (last?.tokens?.input || 0) + (last?.tokens?.output || 0);
625
+ // If we didn't get any text deltas but the stored message has text, render it now
626
+ if (!fullText && last?.parts) {
627
+ for (const p of last.parts) {
628
+ if (p.type === "text" && p.text) {
629
+ out(` ${p.text.replace(/\n/g, `\n `)}\n`);
630
+ fullText = p.text;
631
+ break;
632
+ }
633
+ }
634
+ }
635
+ }
636
+ catch { }
637
+ return { text: fullText, tokens, elapsedMs: Date.now() - t0, interrupted: false };
638
+ }
639
+ catch (e) {
640
+ flushTextLine();
641
+ if (e.name === "AbortError") {
642
+ return { text: fullText, tokens, elapsedMs: Date.now() - t0, interrupted: true };
643
+ }
644
+ return { text: fullText, tokens, elapsedMs: Date.now() - t0, error: e.message, interrupted: false };
645
+ }
646
+ }
647
+ function filterCommands(query) {
648
+ if (!query || query === "/")
649
+ return COMMANDS;
650
+ const q = query.toLowerCase();
651
+ return COMMANDS.filter(c => c.name.toLowerCase().startsWith(q));
652
+ }
653
+ function renderCommandMenu(state) {
654
+ if (!state.visible || state.options.length === 0)
655
+ return 0;
656
+ const max = Math.min(state.options.length, 8);
657
+ for (let i = 0; i < max; i++) {
658
+ const c = state.options[i];
659
+ const selected = i === state.selected;
660
+ const prefix = selected ? `${CYAN}▌${R} ` : ` `;
661
+ const name = selected ? `${CYAN}${BOLD}${c.name}${R}` : `${WHITE}${c.name}${R}`;
662
+ const args = c.args ? ` ${GRAY}${c.args}${R}` : "";
663
+ const desc = `${GRAY}${c.description}${R}`;
664
+ nl(`${prefix}${name}${args} ${desc}`);
665
+ }
666
+ if (state.options.length > max) {
667
+ nl(` ${GRAY}... ${state.options.length - max} more (keep typing)${R}`);
668
+ return max + 1;
669
+ }
670
+ return max;
671
+ }
672
+ function clearMenu(rows) {
673
+ if (rows <= 0)
674
+ return;
675
+ // Move up `rows` lines and clear each one
676
+ for (let i = 0; i < rows; i++) {
677
+ out(`${ESC}[1A${ESC}[2K`);
678
+ }
679
+ }
680
+ // ─── Main TUI loop ────────────────────────────────────────────────────────────
681
+ async function runTUI(opts) {
682
+ let { model, provider } = opts;
683
+ const { port, token, worktree } = opts;
684
+ let sessionId = opts.sessionId || null;
685
+ const history = [];
686
+ let abort = null;
687
+ let streaming = false;
688
+ // Initial session
689
+ if (!sessionId) {
690
+ try {
691
+ sessionId = await createSession(port, token, worktree, "BoneCode Session");
692
+ }
693
+ catch (e) {
694
+ process.stderr.write(`${RED}Failed to create session: ${e.message}${R}\n`);
695
+ process.exit(1);
696
+ }
697
+ }
698
+ // Header
699
+ printLogo();
700
+ nl(` ${GRAY}v${getVersion()} · ${model} · ${path.basename(worktree)}${R}`);
701
+ nl(` ${GRAY}session ${sessionId.slice(0, 8)}${R}`);
702
+ nl();
703
+ nl(` ${GRAY}Type ${R}${CYAN}/${R}${GRAY} to see commands · Ctrl+C interrupt · Ctrl+D exit${R}`);
704
+ nl();
705
+ // ─── Slash command menu state ──────────────────────────────────────────────
706
+ const menu = {
707
+ visible: false,
708
+ options: [],
709
+ selected: 0,
710
+ rowsRendered: 0,
711
+ };
712
+ // Set up readline AFTER setting up SIGINT so we control it
713
+ const rl = readline.createInterface({
714
+ input: process.stdin,
715
+ output: process.stdout,
716
+ terminal: true,
717
+ historySize: 200,
718
+ completer: buildCompleter(worktree),
719
+ });
720
+ const promptStr = () => `${CYAN}${BOLD}>${R} `;
721
+ // ─── Ctrl+C handling ──────────────────────────────────────────────────────
722
+ // When streaming: abort the request AND notify server
723
+ // When idle: clear menu/input or hint to use /exit
724
+ const onSigint = async () => {
725
+ if (streaming && abort) {
726
+ abort.abort();
727
+ // Also tell the server to cancel the agent loop
728
+ try {
729
+ await fetch(`http://localhost:${port}/v2/session/${sessionId}/cancel`, {
730
+ method: "POST",
731
+ headers: { "Authorization": `Bearer ${token}` },
732
+ });
733
+ }
734
+ catch { /* server may not have the endpoint, abort is enough */ }
735
+ // Don't reprompt here — the streamPrompt finally block will handle UI
736
+ return;
737
+ }
738
+ // Idle: clear menu if visible, else hint
739
+ if (menu.visible) {
740
+ clearMenu(menu.rowsRendered);
741
+ menu.visible = false;
742
+ menu.rowsRendered = 0;
743
+ menu.selected = 0;
744
+ }
745
+ out(`\n${GRAY}(Ctrl+D or /exit to quit)${R}\n`);
746
+ rl.setPrompt(promptStr());
747
+ rl.prompt();
748
+ };
749
+ // Detach readline's default SIGINT (which closes the line buffer)
750
+ // and route it to our handler.
751
+ rl.on("SIGINT", onSigint);
752
+ rl.on("close", () => {
753
+ if (streaming && abort)
754
+ abort.abort();
755
+ nl(`\n${GRAY}Goodbye.${R}`);
756
+ process.exit(0);
757
+ });
758
+ // ─── Live menu update on every keystroke ──────────────────────────────────
759
+ // We hook into the keypress events of stdin to redraw the menu.
760
+ // The menu sits ABOVE the prompt line.
761
+ const stdin = rl.input;
762
+ const updateMenu = () => {
763
+ if (streaming)
764
+ return;
765
+ const line = rl.line || "";
766
+ const startsWithSlash = line.startsWith("/");
767
+ const shouldShow = startsWithSlash && !line.includes(" ");
768
+ if (!shouldShow) {
769
+ if (menu.visible) {
770
+ // Need to clear the menu — but we need to preserve the current input line.
771
+ // Strategy: save current line content & cursor, clear menu lines above,
772
+ // then restore the prompt and content.
773
+ const cursor = rl.cursor || 0;
774
+ out(`\r${ESC}[2K`); // clear current prompt line
775
+ clearMenu(menu.rowsRendered); // clear menu lines above
776
+ menu.visible = false;
777
+ menu.rowsRendered = 0;
778
+ menu.selected = 0;
779
+ // Redraw prompt + line
780
+ out(promptStr() + line);
781
+ // Restore cursor position
782
+ const drawn = line.length;
783
+ if (cursor < drawn) {
784
+ out(`${ESC}[${drawn - cursor}D`);
785
+ }
786
+ }
787
+ return;
788
+ }
789
+ // Filter and reset selection if list changed
790
+ const newOptions = filterCommands(line);
791
+ if (newOptions.length === 0) {
792
+ // Clear menu if visible
793
+ if (menu.visible) {
794
+ const cursor = rl.cursor || 0;
795
+ out(`\r${ESC}[2K`);
796
+ clearMenu(menu.rowsRendered);
797
+ menu.visible = false;
798
+ menu.rowsRendered = 0;
799
+ out(promptStr() + line);
800
+ if (cursor < line.length)
801
+ out(`${ESC}[${line.length - cursor}D`);
802
+ }
803
+ return;
804
+ }
805
+ // Save current input line
806
+ const cursor = rl.cursor || 0;
807
+ out(`\r${ESC}[2K`); // clear prompt line
808
+ clearMenu(menu.rowsRendered); // clear old menu
809
+ menu.options = newOptions;
810
+ menu.selected = Math.min(menu.selected, newOptions.length - 1);
811
+ if (menu.selected < 0)
812
+ menu.selected = 0;
813
+ menu.visible = true;
814
+ // Render the menu (each item ends in newline)
815
+ menu.rowsRendered = renderCommandMenu(menu);
816
+ // Redraw prompt + line
817
+ out(promptStr() + line);
818
+ if (cursor < line.length) {
819
+ out(`${ESC}[${line.length - cursor}D`);
820
+ }
821
+ };
822
+ // Capture special keys for menu navigation
823
+ const onKeypress = (_chunk, key) => {
824
+ if (!key)
825
+ return;
826
+ if (streaming)
827
+ return;
828
+ if (menu.visible && menu.options.length > 0) {
829
+ // Arrow up/down navigate the menu
830
+ if (key.name === "up") {
831
+ menu.selected = (menu.selected - 1 + menu.options.length) % menu.options.length;
832
+ updateMenu();
833
+ return;
834
+ }
835
+ if (key.name === "down") {
836
+ menu.selected = (menu.selected + 1) % menu.options.length;
837
+ updateMenu();
838
+ return;
839
+ }
840
+ // Tab: replace input with the selected command
841
+ if (key.name === "tab") {
842
+ const sel = menu.options[menu.selected];
843
+ if (sel) {
844
+ // Replace the line buffer with the command + space
845
+ rl.line = sel.name + " ";
846
+ rl.cursor = sel.name.length + 1;
847
+ updateMenu();
848
+ out(`\r${ESC}[2K`);
849
+ out(promptStr() + rl.line);
850
+ }
851
+ return;
852
+ }
853
+ }
854
+ // Default: trigger menu update on the next tick (after readline updates the buffer)
855
+ setImmediate(updateMenu);
856
+ };
857
+ stdin.on("keypress", onKeypress);
858
+ rl.setPrompt(promptStr());
859
+ rl.prompt();
860
+ // ─── Main input loop ──────────────────────────────────────────────────────
861
+ for await (const rawLine of rl) {
862
+ const text = rawLine.trim();
863
+ // Clear any visible menu before processing
864
+ if (menu.visible) {
865
+ menu.visible = false;
866
+ menu.rowsRendered = 0;
867
+ menu.selected = 0;
868
+ }
869
+ if (!text) {
870
+ rl.setPrompt(promptStr());
871
+ rl.prompt();
872
+ continue;
873
+ }
874
+ history.push(text);
875
+ if (history.length > 200)
876
+ history.shift();
877
+ // ── Slash commands ──────────────────────────────────────────────────────
878
+ if (text.startsWith("/")) {
879
+ const parts = text.slice(1).trim().split(/\s+/);
880
+ const cmd = parts[0]?.toLowerCase() || "";
881
+ const args = parts.slice(1);
882
+ switch (cmd) {
883
+ case "help":
884
+ case "h":
885
+ printHelp();
886
+ break;
887
+ case "new":
888
+ try {
889
+ sessionId = await createSession(port, token, worktree, "New Session");
890
+ nl(`${GREEN}✓${R} ${GRAY}session ${sessionId.slice(0, 8)}${R}`);
891
+ }
892
+ catch (e) {
893
+ nl(`${RED}✗ ${e.message}${R}`);
894
+ }
895
+ break;
896
+ case "session":
897
+ nl(`${GRAY}${sessionId}${R}`);
898
+ break;
899
+ case "sessions": {
900
+ try {
901
+ const list = await apiGet(`http://localhost:${port}/v2/session?limit=10`, token);
902
+ if (!list.length) {
903
+ nl(`${GRAY}No sessions${R}`);
904
+ break;
905
+ }
906
+ nl();
907
+ for (const s of list) {
908
+ const active = s.id === sessionId;
909
+ const dot = active ? `${GREEN}●${R}` : `${GRAY}○${R}`;
910
+ const t = new Date(s.time?.updated || Date.now()).toLocaleString();
911
+ nl(` ${dot} ${WHITE}${(s.title || "untitled").slice(0, 50).padEnd(50)}${R} ${GRAY}${t}${R}`);
912
+ }
913
+ nl();
914
+ }
915
+ catch (e) {
916
+ nl(`${RED}✗ ${e.message}${R}`);
917
+ }
918
+ break;
919
+ }
920
+ case "model":
921
+ if (args[0]) {
922
+ if (args[0] === "local") {
923
+ provider = process.env.DEFAULT_PROVIDER || "openai_compatible";
924
+ model = process.env.DEFAULT_MODEL || "local-model";
925
+ }
926
+ else if (args[0].includes("/")) {
927
+ const i = args[0].indexOf("/");
928
+ provider = args[0].slice(0, i);
929
+ model = args[0].slice(i + 1);
930
+ }
931
+ else {
932
+ model = args[0];
933
+ }
934
+ nl(`${GREEN}✓${R} ${WHITE}${provider}/${model}${R}`);
935
+ }
936
+ else {
937
+ nl(`${GRAY}${provider}/${model}${R}`);
938
+ }
939
+ break;
940
+ case "provider":
941
+ if (args[0]) {
942
+ if (args[0] === "local") {
943
+ provider = process.env.DEFAULT_PROVIDER || "openai_compatible";
944
+ model = process.env.DEFAULT_MODEL || "local-model";
945
+ nl(`${GREEN}✓${R} ${WHITE}${provider}/${model}${R}`);
946
+ }
947
+ else {
948
+ provider = args[0];
949
+ nl(`${GREEN}✓${R} ${WHITE}${provider}${R} ${GRAY}use /model <id> to set model${R}`);
950
+ }
951
+ }
952
+ else {
953
+ nl(`${GRAY}${provider}${R}`);
954
+ }
955
+ break;
956
+ case "providers": {
957
+ const list = await fetchProviders(port, token);
958
+ if (list.length)
959
+ printProviders(list, provider);
960
+ else
961
+ nl(`${YELLOW}Could not fetch providers${R}`);
962
+ break;
963
+ }
964
+ case "clear":
965
+ process.stdout.write(`${ESC}[2J${ESC}[H`);
966
+ printLogo();
967
+ break;
968
+ case "history":
969
+ if (!history.length)
970
+ nl(`${GRAY}No history${R}`);
971
+ else
972
+ history.slice(-10).forEach((h, i) => nl(` ${GRAY}${i + 1}.${R} ${h.slice(0, 80)}`));
973
+ break;
974
+ case "exit":
975
+ case "quit":
976
+ case "q":
977
+ nl(`\n${GRAY}Goodbye.${R}`);
978
+ process.exit(0);
979
+ break;
980
+ default:
981
+ nl(`${YELLOW}Unknown: /${cmd}${R} ${GRAY}Type /help for available commands${R}`);
982
+ }
983
+ rl.setPrompt(promptStr());
984
+ rl.prompt();
985
+ continue;
986
+ }
987
+ // ── Prompt to the agent ─────────────────────────────────────────────────
988
+ if (!sessionId) {
989
+ try {
990
+ sessionId = await createSession(port, token, worktree, text.slice(0, 50));
991
+ }
992
+ catch (e) {
993
+ nl(`${RED}✗ ${e.message}${R}`);
994
+ rl.setPrompt(promptStr());
995
+ rl.prompt();
996
+ continue;
997
+ }
998
+ }
999
+ // Resolve @file mentions to absolute paths
1000
+ const resolved = text.replace(/@([\w./\-]+)/g, (match, fp) => {
1001
+ const abs = path.isAbsolute(fp) ? fp : path.resolve(worktree, fp);
1002
+ return fs.existsSync(abs) ? `@${abs}` : match;
1003
+ });
1004
+ renderUserMessage(text);
1005
+ // Show "..." spinner so the user knows something is happening
1006
+ out(` ${GRAY}thinking...${R}`);
1007
+ rl.pause();
1008
+ streaming = true;
1009
+ abort = new AbortController();
1010
+ const t0 = Date.now();
1011
+ const result = await streamPrompt({
1012
+ port, token, sessionId,
1013
+ model, provider,
1014
+ message: resolved,
1015
+ worktree,
1016
+ abortSignal: abort.signal,
1017
+ });
1018
+ streaming = false;
1019
+ abort = null;
1020
+ // Clear the thinking spinner if no output happened
1021
+ if (!result.text && !result.error) {
1022
+ clearLine();
1023
+ }
1024
+ else {
1025
+ // Make sure the next render starts on a clean line
1026
+ const cur = process.stdout.cursor;
1027
+ if (cur && cur.col !== 0)
1028
+ out("\n");
1029
+ }
1030
+ renderTurnEnd(model, result.elapsedMs, result.interrupted);
1031
+ if (result.error && !result.interrupted) {
1032
+ nl(` ${RED}✗ ${result.error}${R}`);
1033
+ }
1034
+ nl();
1035
+ rl.resume();
1036
+ rl.setPrompt(promptStr());
1037
+ rl.prompt();
1038
+ }
1039
+ stdin.removeListener("keypress", onKeypress);
1040
+ }
1041
+ exports.runTUI = runTUI;
1042
+ // ─── Entry point ──────────────────────────────────────────────────────────────
1043
+ async function startInteractiveTUI(opts) {
1044
+ const ready = await waitForServer(opts.port, 30000);
1045
+ if (!ready) {
1046
+ process.stderr.write(`${RED}BoneCode server not responding on port ${opts.port}${R}\n`);
1047
+ process.stderr.write(`${GRAY}Start with: bonecode serve${R}\n`);
1048
+ process.exit(1);
1049
+ }
1050
+ await runTUI(opts);
1051
+ }
1052
+ exports.startInteractiveTUI = startInteractiveTUI;
1053
+ //# sourceMappingURL=tui.js.map