@bahulam/code 2.6.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 (273) hide show
  1. package/README.md +80 -0
  2. package/package.json +49 -0
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/benchmark/route.ts +113 -0
  6. package/pulse/app/api/benchmarks/route.ts +195 -0
  7. package/pulse/app/api/costs/route.ts +88 -0
  8. package/pulse/app/api/export/route.ts +77 -0
  9. package/pulse/app/api/history/route.ts +11 -0
  10. package/pulse/app/api/import/route.ts +31 -0
  11. package/pulse/app/api/memory/route.ts +50 -0
  12. package/pulse/app/api/plans/route.ts +9 -0
  13. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  14. package/pulse/app/api/projects/route.ts +121 -0
  15. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  16. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  17. package/pulse/app/api/sessions/route.ts +112 -0
  18. package/pulse/app/api/settings/route.ts +14 -0
  19. package/pulse/app/api/stats/route.ts +143 -0
  20. package/pulse/app/api/todos/route.ts +9 -0
  21. package/pulse/app/api/tools/route.ts +160 -0
  22. package/pulse/app/benchmarks/page.tsx +224 -0
  23. package/pulse/app/costs/page.tsx +179 -0
  24. package/pulse/app/export/page.tsx +465 -0
  25. package/pulse/app/favicon.ico +0 -0
  26. package/pulse/app/globals.css +263 -0
  27. package/pulse/app/help/page.tsx +143 -0
  28. package/pulse/app/history/page.tsx +157 -0
  29. package/pulse/app/layout.tsx +46 -0
  30. package/pulse/app/memory/page.tsx +365 -0
  31. package/pulse/app/overview-client.tsx +393 -0
  32. package/pulse/app/page.tsx +14 -0
  33. package/pulse/app/plans/page.tsx +308 -0
  34. package/pulse/app/projects/[slug]/page.tsx +390 -0
  35. package/pulse/app/projects/page.tsx +110 -0
  36. package/pulse/app/sessions/[id]/page.tsx +243 -0
  37. package/pulse/app/sessions/page.tsx +39 -0
  38. package/pulse/app/settings/page.tsx +188 -0
  39. package/pulse/app/todos/page.tsx +211 -0
  40. package/pulse/app/tools/page.tsx +249 -0
  41. package/pulse/cli.js +164 -0
  42. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  43. package/pulse/components/activity/streak-card.tsx +36 -0
  44. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  45. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  46. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  47. package/pulse/components/costs/model-token-table.tsx +60 -0
  48. package/pulse/components/global-search.tsx +193 -0
  49. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  50. package/pulse/components/layout/bottom-nav.tsx +53 -0
  51. package/pulse/components/layout/client-layout.tsx +31 -0
  52. package/pulse/components/layout/sidebar-context.tsx +50 -0
  53. package/pulse/components/layout/sidebar.tsx +183 -0
  54. package/pulse/components/layout/top-bar.tsx +121 -0
  55. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  56. package/pulse/components/overview/conversation-table.tsx +148 -0
  57. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  58. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  59. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  60. package/pulse/components/overview/stat-card.tsx +102 -0
  61. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  62. package/pulse/components/projects/project-card.tsx +175 -0
  63. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  64. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  65. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  66. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  67. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  68. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  69. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  70. package/pulse/components/sessions/session-badges.tsx +49 -0
  71. package/pulse/components/sessions/session-table.tsx +299 -0
  72. package/pulse/components/theme-provider.tsx +44 -0
  73. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  74. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  75. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  76. package/pulse/components/tools/version-history-table.tsx +32 -0
  77. package/pulse/components/ui/alert.tsx +66 -0
  78. package/pulse/components/ui/badge.tsx +48 -0
  79. package/pulse/components/ui/breadcrumb.tsx +109 -0
  80. package/pulse/components/ui/button.tsx +64 -0
  81. package/pulse/components/ui/calendar.tsx +220 -0
  82. package/pulse/components/ui/card.tsx +92 -0
  83. package/pulse/components/ui/command.tsx +158 -0
  84. package/pulse/components/ui/dialog.tsx +158 -0
  85. package/pulse/components/ui/input.tsx +21 -0
  86. package/pulse/components/ui/popover.tsx +89 -0
  87. package/pulse/components/ui/progress.tsx +31 -0
  88. package/pulse/components/ui/select.tsx +190 -0
  89. package/pulse/components/ui/separator.tsx +28 -0
  90. package/pulse/components/ui/sheet.tsx +143 -0
  91. package/pulse/components/ui/skeleton.tsx +13 -0
  92. package/pulse/components/ui/table.tsx +116 -0
  93. package/pulse/components/ui/tabs.tsx +91 -0
  94. package/pulse/components/ui/tooltip.tsx +57 -0
  95. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  96. package/pulse/components.json +23 -0
  97. package/pulse/eslint.config.mjs +18 -0
  98. package/pulse/lib/bahulam-paths.ts +23 -0
  99. package/pulse/lib/claude-reader.ts +592 -0
  100. package/pulse/lib/decode.ts +129 -0
  101. package/pulse/lib/pricing.ts +102 -0
  102. package/pulse/lib/replay-parser.ts +165 -0
  103. package/pulse/lib/tool-categories.ts +127 -0
  104. package/pulse/lib/utils.ts +6 -0
  105. package/pulse/next-env.d.ts +6 -0
  106. package/pulse/next.config.ts +16 -0
  107. package/pulse/package.json +45 -0
  108. package/pulse/postcss.config.mjs +7 -0
  109. package/pulse/public/activity.png +0 -0
  110. package/pulse/public/cc-lens.png +0 -0
  111. package/pulse/public/command-k.png +0 -0
  112. package/pulse/public/costs.png +0 -0
  113. package/pulse/public/dashboard-dark.png +0 -0
  114. package/pulse/public/dashboard-white.png +0 -0
  115. package/pulse/public/export.png +0 -0
  116. package/pulse/public/file.svg +1 -0
  117. package/pulse/public/globe.svg +1 -0
  118. package/pulse/public/next.svg +1 -0
  119. package/pulse/public/projects.png +0 -0
  120. package/pulse/public/session-chat.png +0 -0
  121. package/pulse/public/todos.png +0 -0
  122. package/pulse/public/tools.png +0 -0
  123. package/pulse/public/vercel.svg +1 -0
  124. package/pulse/public/window.svg +1 -0
  125. package/pulse/tsconfig.json +34 -0
  126. package/pulse/types/claude.ts +294 -0
  127. package/src/agents/loader.mjs +94 -0
  128. package/src/agents/multi_workflow_loader.mjs +330 -0
  129. package/src/agents/parser.mjs +205 -0
  130. package/src/agents/scaffold.mjs +222 -0
  131. package/src/agents/teams.mjs +123 -0
  132. package/src/agents/workflow_loader.mjs +122 -0
  133. package/src/agents/workflow_scaffold.mjs +249 -0
  134. package/src/auth/oauth.mjs +220 -0
  135. package/src/auth/tarang-auth.mjs +306 -0
  136. package/src/commands/agent.mjs +220 -0
  137. package/src/commands/workflow.mjs +581 -0
  138. package/src/config/cli-args.mjs +200 -0
  139. package/src/config/env.mjs +263 -0
  140. package/src/config/hook-runner.mjs +100 -0
  141. package/src/config/memory-loader.mjs +32 -0
  142. package/src/config/settings-loader.mjs +45 -0
  143. package/src/config/settings.mjs +132 -0
  144. package/src/context/ast-parser.mjs +298 -0
  145. package/src/context/bm25.mjs +85 -0
  146. package/src/context/retriever.mjs +308 -0
  147. package/src/context/skeleton.mjs +134 -0
  148. package/src/context/symbol-indexer.mjs +375 -0
  149. package/src/core/agent-history.mjs +111 -0
  150. package/src/core/agent-loop.mjs +486 -0
  151. package/src/core/approval-log.mjs +104 -0
  152. package/src/core/approval.mjs +476 -0
  153. package/src/core/attachments.mjs +380 -0
  154. package/src/core/backend-url.mjs +55 -0
  155. package/src/core/cache-control.mjs +92 -0
  156. package/src/core/cache.mjs +105 -0
  157. package/src/core/callback-client.mjs +180 -0
  158. package/src/core/checkpoints.mjs +142 -0
  159. package/src/core/compact-history.mjs +127 -0
  160. package/src/core/context-envelope.mjs +54 -0
  161. package/src/core/context-manager.mjs +198 -0
  162. package/src/core/error-guidance.mjs +311 -0
  163. package/src/core/file-diff.mjs +217 -0
  164. package/src/core/headless.mjs +448 -0
  165. package/src/core/hooks-manager.mjs +87 -0
  166. package/src/core/jsonl-writer.mjs +449 -0
  167. package/src/core/local-agent.mjs +537 -0
  168. package/src/core/local-store.mjs +836 -0
  169. package/src/core/mode-selector.mjs +51 -0
  170. package/src/core/output-filter.mjs +177 -0
  171. package/src/core/paths.mjs +190 -0
  172. package/src/core/policy-resolver.mjs +156 -0
  173. package/src/core/pricing.mjs +336 -0
  174. package/src/core/project-artifacts.mjs +39 -0
  175. package/src/core/project-context-loader.mjs +139 -0
  176. package/src/core/providers.mjs +219 -0
  177. package/src/core/rate-limit-display.mjs +121 -0
  178. package/src/core/rate-limiter.mjs +119 -0
  179. package/src/core/resume-mode.mjs +192 -0
  180. package/src/core/risk-tier.mjs +337 -0
  181. package/src/core/safety.mjs +203 -0
  182. package/src/core/scheduler.mjs +173 -0
  183. package/src/core/session-manager.mjs +360 -0
  184. package/src/core/session.mjs +143 -0
  185. package/src/core/settings-sync.mjs +85 -0
  186. package/src/core/stagnation.mjs +57 -0
  187. package/src/core/stream-client.mjs +829 -0
  188. package/src/core/streaming.mjs +182 -0
  189. package/src/core/system-prompt.mjs +140 -0
  190. package/src/core/tasks.mjs +196 -0
  191. package/src/core/tool-executor.mjs +1950 -0
  192. package/src/core/trust.mjs +158 -0
  193. package/src/core/work-scope.mjs +248 -0
  194. package/src/hooks/engine.mjs +162 -0
  195. package/src/index.mjs +426 -0
  196. package/src/mcp/client.mjs +253 -0
  197. package/src/mcp/transport-shttp.mjs +130 -0
  198. package/src/mcp/transport-sse.mjs +131 -0
  199. package/src/mcp/transport-ws.mjs +134 -0
  200. package/src/onboarding/preflight.mjs +360 -0
  201. package/src/permissions/checker.mjs +57 -0
  202. package/src/permissions/command-classifier.mjs +652 -0
  203. package/src/permissions/injection-check.mjs +60 -0
  204. package/src/permissions/path-check.mjs +102 -0
  205. package/src/permissions/prompt.mjs +73 -0
  206. package/src/permissions/sandbox.mjs +112 -0
  207. package/src/plugins/loader.mjs +138 -0
  208. package/src/skills/installer.mjs +188 -0
  209. package/src/skills/loader.mjs +252 -0
  210. package/src/skills/runner.mjs +55 -0
  211. package/src/state/orbit.mjs +263 -0
  212. package/src/state/verbosity.mjs +99 -0
  213. package/src/telemetry/index.mjs +96 -0
  214. package/src/terminal/agents.mjs +177 -0
  215. package/src/terminal/analytics.mjs +292 -0
  216. package/src/terminal/ansi.mjs +695 -0
  217. package/src/terminal/init.mjs +145 -0
  218. package/src/terminal/main.mjs +269 -0
  219. package/src/terminal/repl-explore.mjs +35 -0
  220. package/src/terminal/repl-format.mjs +257 -0
  221. package/src/terminal/repl-render.mjs +561 -0
  222. package/src/terminal/repl-resume.mjs +625 -0
  223. package/src/terminal/repl-state.mjs +103 -0
  224. package/src/terminal/repl-utils.mjs +34 -0
  225. package/src/terminal/repl.mjs +3832 -0
  226. package/src/terminal/skills.mjs +54 -0
  227. package/src/terminal/tool-display.mjs +240 -0
  228. package/src/tools/agent.mjs +137 -0
  229. package/src/tools/ask-user.mjs +61 -0
  230. package/src/tools/bash.mjs +231 -0
  231. package/src/tools/cron-create.mjs +120 -0
  232. package/src/tools/cron-delete.mjs +49 -0
  233. package/src/tools/cron-list.mjs +37 -0
  234. package/src/tools/edit.mjs +82 -0
  235. package/src/tools/enter-worktree.mjs +69 -0
  236. package/src/tools/exit-worktree.mjs +57 -0
  237. package/src/tools/glob.mjs +117 -0
  238. package/src/tools/grep.mjs +129 -0
  239. package/src/tools/lint.mjs +71 -0
  240. package/src/tools/ls.mjs +58 -0
  241. package/src/tools/lsp.mjs +115 -0
  242. package/src/tools/multi-edit.mjs +94 -0
  243. package/src/tools/notebook-edit.mjs +96 -0
  244. package/src/tools/project-overview.mjs +641 -0
  245. package/src/tools/read-mcp-resource.mjs +57 -0
  246. package/src/tools/read.mjs +138 -0
  247. package/src/tools/registry.mjs +116 -0
  248. package/src/tools/remote-trigger.mjs +84 -0
  249. package/src/tools/send-message.mjs +64 -0
  250. package/src/tools/skill.mjs +52 -0
  251. package/src/tools/test-runner.mjs +49 -0
  252. package/src/tools/todo-write.mjs +68 -0
  253. package/src/tools/tool-search.mjs +77 -0
  254. package/src/tools/web-fetch.mjs +65 -0
  255. package/src/tools/web-search.mjs +89 -0
  256. package/src/tools/write.mjs +55 -0
  257. package/src/ui/approval.mjs +263 -0
  258. package/src/ui/banner.mjs +235 -0
  259. package/src/ui/commands.mjs +537 -0
  260. package/src/ui/formatter.mjs +409 -0
  261. package/src/ui/icons.mjs +164 -0
  262. package/src/ui/input-dock.mjs +444 -0
  263. package/src/ui/markdown.mjs +278 -0
  264. package/src/ui/mission-report.mjs +296 -0
  265. package/src/ui/palette.mjs +189 -0
  266. package/src/ui/slash-commands.mjs +245 -0
  267. package/src/ui/spinner.mjs +116 -0
  268. package/src/ui/sub-agent.mjs +152 -0
  269. package/src/ui/term.mjs +159 -0
  270. package/src/ui/text-layout.mjs +127 -0
  271. package/src/ui/tool-card.mjs +463 -0
  272. package/src/ui/tool-details.mjs +312 -0
  273. package/src/ui/transcript-block.mjs +21 -0
package/src/index.mjs ADDED
@@ -0,0 +1,426 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @bahulamai/code — Bahulam Code CLI (Bahulam's coding agent).
4
+ *
5
+ * Phase 3: Hybrid local/remote/auto + advanced features.
6
+ */
7
+
8
+ // Load .env file from cwd or ~/.bahulam/.env
9
+ import { readFileSync, existsSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { homedir } from 'node:os';
12
+
13
+ for (const envPath of [join(process.cwd(), '.env'), join(homedir(), '.bahulam', '.env')]) {
14
+ if (existsSync(envPath)) {
15
+ for (const line of readFileSync(envPath, 'utf-8').split('\n')) {
16
+ const match = line.match(/^\s*([\w]+)\s*=\s*(.+?)\s*$/);
17
+ if (match && !process.env[match[1]]) {
18
+ process.env[match[1]] = match[2];
19
+ }
20
+ }
21
+ break;
22
+ }
23
+ }
24
+
25
+ import { TarangStreamClient, EVENT_TYPES } from './core/stream-client.mjs';
26
+ import { LocalAgent } from './core/local-agent.mjs';
27
+ import { createToolExecutor } from './core/tool-executor.mjs';
28
+ import { TarangAuth } from './auth/tarang-auth.mjs';
29
+ import { ApprovalManager } from './core/approval.mjs';
30
+ import { SessionManager } from './core/session-manager.mjs';
31
+ import { EventFormatter } from './ui/formatter.mjs';
32
+ import { COMMANDS } from './ui/slash-commands.mjs';
33
+ import { selectMode } from './core/mode-selector.mjs';
34
+ import { printBanner, printProjectInfo, printHints, printAuthStatus, printStyledConfig, printGoodbye } from './ui/banner.mjs';
35
+ import { ContextRetriever } from './context/retriever.mjs';
36
+ import { loadSettings } from './config/settings.mjs';
37
+
38
+ const VERSION = '1.0.1';
39
+
40
+ // ── Arg Parsing (consolidated from index.mjs + cli-args.mjs) ──
41
+
42
+ function parseArgs(argv) {
43
+ const args = {
44
+ // Commands
45
+ command: null, instruction: null,
46
+ // Tarang mode flags
47
+ verbose: false, yes: false, plan: false, strict: false,
48
+ local: false, remote: false, debug: false,
49
+ version: false, help: false,
50
+ // Config subcommand flags
51
+ showConfig: false, openRouterKey: null, anthropicKey: null,
52
+ backendUrl: null, mode: null,
53
+ // Extended flags (from cli-args.mjs)
54
+ permissionMode: null,
55
+ outputFormat: null,
56
+ systemPrompt: null,
57
+ addDirs: [],
58
+ maxTurns: null,
59
+ allowedTools: null,
60
+ disallowedTools: null,
61
+ };
62
+ let i = 0;
63
+ while (i < argv.length) {
64
+ const arg = argv[i];
65
+ switch (arg) {
66
+ // Version / help
67
+ case '--version': case '-V': args.version = true; break;
68
+ case '--help': case '-h': args.help = true; break;
69
+ // Behavior flags
70
+ case '--verbose': case '-v': args.verbose = true; break;
71
+ case '--debug': case '-d': args.debug = true; args.verbose = true; break;
72
+ case '--yes': case '-y': args.yes = true; break;
73
+ case '--plan': args.plan = true; break;
74
+ case '--strict': args.strict = true; break;
75
+ // Mode flags
76
+ case '--local': args.local = true; break;
77
+ case '--remote': args.remote = true; break;
78
+ case '--mode': args.mode = argv[++i]; break;
79
+ // Commands
80
+ case 'login': args.command = 'login'; break;
81
+ case 'resume': args.command = 'resume'; break;
82
+ case 'config': args.command = 'config'; break;
83
+ case 'configure': args.command = 'configure'; break;
84
+ case 'sync':
85
+ if (args.command === 'workflow' && !args.workflowSubcommand) args.workflowSubcommand = 'sync';
86
+ else if (args.command === 'agent' && !args.agentSubcommand) args.agentSubcommand = 'sync';
87
+ else if (!args.command) args.command = 'sync';
88
+ break;
89
+ case 'workflow': args.command = 'workflow'; break;
90
+ case 'agent': args.command = 'agent'; break;
91
+ // Config flags
92
+ case '--show': args.showConfig = true; break;
93
+ case '--openrouter-key': case '-k': args.openRouterKey = argv[++i]; break;
94
+ case '--anthropic-key': args.anthropicKey = argv[++i]; break;
95
+ case '--openai-key': args.openaiKey = argv[++i]; break;
96
+ case '--google-key': args.googleKey = argv[++i]; break;
97
+ case '--gateway': args.gateway = argv[++i]; break;
98
+ // --backend-url removed: use TARANG_ENV
99
+ // --model removed: use tarang configure (web settings)
100
+ // Extended flags
101
+ case '--permission-mode': args.permissionMode = argv[++i]; break;
102
+ case '--print': case '-p': case '--instruction': case '--input': args.instruction = argv[++i]; break;
103
+ case '--output-format': args.outputFormat = argv[++i]; break;
104
+ case '--system-prompt': args.systemPrompt = argv[++i]; break;
105
+ case '--add-dir': args.addDirs.push(argv[++i]); break;
106
+ case '--max-turns': args.maxTurns = parseInt(argv[++i], 10); break;
107
+ case '--allowedTools': args.allowedTools = argv[++i]?.split(',').map(s => s.trim()); break;
108
+ case '--disallowedTools': args.disallowedTools = argv[++i]?.split(',').map(s => s.trim()); break;
109
+ // Workflow flags
110
+ case '--file': case '-f': args.workflowFile = argv[++i]; break;
111
+ case '--dir': args.workflowDir = argv[++i]; break;
112
+ case '--agent-dir': args.agentDir = argv[++i]; break;
113
+ case '--pattern': args.pattern = argv[++i]; break;
114
+ default:
115
+ if (!arg.startsWith('-')) {
116
+ if (args.command === 'workflow' && !args.workflowSubcommand) {
117
+ // First positional after 'workflow' is the subcommand
118
+ const subcommands = new Set(['create', 'create-multi', 'run', 'run-multi', 'list', 'get', 'delete', 'sync']);
119
+ if (subcommands.has(arg)) {
120
+ args.workflowSubcommand = arg;
121
+ } else {
122
+ // Second positional is the workflow name/slug
123
+ args.workflowSlug = arg;
124
+ }
125
+ } else if (args.command === 'workflow' && args.workflowSubcommand && !args.workflowSlug) {
126
+ args.workflowSlug = arg;
127
+ } else if (args.command === 'agent' && !args.agentSubcommand) {
128
+ const agentSubs = new Set(['list', 'get', 'sync']);
129
+ if (agentSubs.has(arg)) {
130
+ args.agentSubcommand = arg;
131
+ } else {
132
+ args.agentSlug = arg;
133
+ }
134
+ } else if (args.command === 'agent' && args.agentSubcommand && !args.agentSlug) {
135
+ args.agentSlug = arg;
136
+ } else if (!args.command && !args.instruction) {
137
+ args.instruction = arg;
138
+ }
139
+ }
140
+ break;
141
+ }
142
+ i++;
143
+ }
144
+ if (!args.verbose && process.env.TARANG_VERBOSE === '1') args.verbose = true;
145
+ if (!args.yes && process.env.TARANG_YES === '1') args.yes = true;
146
+ return args;
147
+ }
148
+
149
+ function printUsage() {
150
+ printBanner(VERSION);
151
+
152
+ const B = '\x1b[1m', C = '\x1b[36m', D = '\x1b[2m', G = '\x1b[32m', R = '\x1b[0m';
153
+
154
+ process.stderr.write(`${B}USAGE${R}\n`);
155
+ process.stderr.write(` ${C}bahulam-code "instruction"${R} Execute instruction\n`);
156
+ process.stderr.write(` ${C}bahulam-code${R} Interactive mode (REPL)\n`);
157
+ process.stderr.write(` ${C}bahulam-code login${R} Authenticate via GitHub OAuth\n`);
158
+ process.stderr.write(` ${C}bahulam-code configure${R} Open settings in browser\n`);
159
+ process.stderr.write(` ${C}bahulam-code config --show${R} Display local configuration\n`);
160
+ process.stderr.write(` ${C}bahulam-code resume${R} Resume a paused session\n`);
161
+ process.stderr.write(` ${C}bahulam-code workflow create --file <path>${R} Create workflow from YAML\n`);
162
+ process.stderr.write(` ${C}bahulam-code workflow run <name>${R} Run a workflow\n`);
163
+ process.stderr.write(` ${C}bahulam-code workflow list${R} List workflows\n`);
164
+ process.stderr.write(` ${C}bahulam-code workflow get <name>${R} Show workflow details\n`);
165
+ process.stderr.write(` ${C}bahulam-code workflow delete <name>${R} Delete a workflow\n`);
166
+ process.stderr.write(` ${C}bahulam-code workflow sync${R} Sync workflow YAML files\n`);
167
+ process.stderr.write(` ${C}bahulam-code agent list${R} List user-defined agents\n`);
168
+ process.stderr.write(` ${C}bahulam-code agent get <slug>${R} Show agent details\n`);
169
+ process.stderr.write(` ${C}bahulam-code agent sync${R} Sync agent YAML files\n`);
170
+ process.stderr.write('\n');
171
+ process.stderr.write(`${B}MODE FLAGS${R}\n`);
172
+ process.stderr.write(` ${G}--local${R} Direct LLM API ${D}(<100ms, offline)${R}\n`);
173
+ process.stderr.write(` ${G}--remote${R} SSE backend ${D}(multi-agent orchestration)${R}\n`);
174
+ process.stderr.write(` ${G}--mode <auto|local|remote>${R} Set mode explicitly\n`);
175
+ process.stderr.write(` ${D}(default: auto-select based on task complexity)${R}\n`);
176
+ process.stderr.write('\n');
177
+ process.stderr.write(`${B}MODEL FLAGS${R}\n`);
178
+ process.stderr.write(` ${G}--system-prompt <text>${R} Override system prompt\n`);
179
+ process.stderr.write(` ${G}--max-turns <n>${R} Maximum conversation turns\n`);
180
+ process.stderr.write(` ${D}Models are configured via: bahulam-code configure${R}\n`);
181
+ process.stderr.write('\n');
182
+ process.stderr.write(`${B}PERMISSION FLAGS${R}\n`);
183
+ process.stderr.write(` ${G}--yes, -y${R} Auto-approve all operations\n`);
184
+ process.stderr.write(` ${G}--plan${R} Read-only mode (block all writes)\n`);
185
+ process.stderr.write(` ${G}--strict${R} Deny tools not in allowed list\n`);
186
+ process.stderr.write(` ${G}--permission-mode <mode>${R} Permission mode ${D}(auto, plan, strict)${R}\n`);
187
+ process.stderr.write(` ${G}--allowedTools <tools>${R} Comma-separated allowed tools\n`);
188
+ process.stderr.write(` ${G}--disallowedTools <tools>${R} Comma-separated denied tools\n`);
189
+ process.stderr.write('\n');
190
+ process.stderr.write(`${B}OUTPUT FLAGS${R}\n`);
191
+ process.stderr.write(` ${G}--print, -p <prompt>${R} Non-interactive: run prompt and exit\n`);
192
+ process.stderr.write(` ${G}--output-format <fmt>${R} Output format: text, json, stream-json\n`);
193
+ process.stderr.write(` ${G}--verbose, -v${R} Show tool details and thinking\n`);
194
+ process.stderr.write(` ${G}--debug, -d${R} Debug mode ${D}(implies verbose)${R}\n`);
195
+ process.stderr.write('\n');
196
+ process.stderr.write(`${B}OTHER FLAGS${R}\n`);
197
+ process.stderr.write(` ${G}--version, -V${R} Show version\n`);
198
+ process.stderr.write(` ${G}--help, -h${R} Show this help\n`);
199
+ process.stderr.write(` ${G}--add-dir <dir>${R} Additional CLAUDE.md directory\n`);
200
+ process.stderr.write('\n');
201
+ process.stderr.write(`${B}SLASH COMMANDS${R} ${D}(interactive mode)${R}\n`);
202
+ for (const [k, v] of Object.entries(COMMANDS)) {
203
+ process.stderr.write(` ${C}${k.padEnd(14)}${R} ${v}\n`);
204
+ }
205
+ process.stderr.write('\n');
206
+ }
207
+
208
+ // ── Execute ─────────────────────────────────────────────────
209
+
210
+ async function executeInstruction(executor, instruction, formatter, sessionMgr) {
211
+ sessionMgr.start(instruction);
212
+ for await (const event of executor) {
213
+ formatter.render(event);
214
+ if (event.type === 'session_info') sessionMgr.setSessionInfo(event.data);
215
+ if (event.type === 'tool_call' || event.type === 'tool_request') sessionMgr.recordToolCall(event.data?.tool);
216
+ if (event.type === 'complete') sessionMgr.complete(event.data?.summary);
217
+ if (event.type === 'error' && event.data?.fatal) sessionMgr.fail(event.data?.message);
218
+ if (event.type === 'cancelled') sessionMgr.cancel();
219
+ if (event.type === 'paused') sessionMgr.pause();
220
+ }
221
+ }
222
+
223
+ // ── REPL ────────────────────────────────────────────────────
224
+
225
+ // Inline REPL removed — now delegates to startTerminalRepl() from terminal/repl.mjs
226
+ // which has full markdown rendering, turn summaries, cost display, and ANSI UI.
227
+
228
+ import { startTerminalRepl } from './terminal/repl.mjs';
229
+
230
+ // ── Main ────────────────────────────────────────────────────
231
+
232
+ async function main() {
233
+ const args = parseArgs(process.argv.slice(2));
234
+ if (args.version) { console.log(`@bahulamai/code ${VERSION}`); process.exit(0); }
235
+ if (args.help) { printUsage(); process.exit(0); }
236
+
237
+ const auth = new TarangAuth();
238
+
239
+ if (args.command === 'login') {
240
+ printBanner(VERSION);
241
+ process.stderr.write('\x1b[1mAuthentication\x1b[0m\n\n');
242
+ await auth.login();
243
+ process.stderr.write('\n\x1b[32m✓ Login successful!\x1b[0m\n');
244
+ // Sync settings from web after login
245
+ try {
246
+ process.stderr.write('\x1b[2mSyncing settings from server...\x1b[0m\n');
247
+ const remote = await auth.syncSettings();
248
+ process.stderr.write(`\x1b[32m✓ Settings synced\x1b[0m ${remote.gateway_type ? `(gateway: ${remote.gateway_type})` : ''}\n`);
249
+ } catch {
250
+ process.stderr.write('\x1b[2mSettings sync skipped — configure at bahulam.ai/dashboard/settings\x1b[0m\n');
251
+ }
252
+ process.stderr.write('\n');
253
+ // Fall through to REPL — user starts working right away
254
+ }
255
+
256
+ if (args.command === 'sync') {
257
+ printBanner(VERSION);
258
+ process.stderr.write('\x1b[1mSyncing settings...\x1b[0m\n\n');
259
+ try {
260
+ const remote = await auth.syncSettings();
261
+ process.stderr.write(`\x1b[32m✓ Gateway:\x1b[0m ${remote.gateway_type}\n`);
262
+ if (remote.models?.orchestrator) process.stderr.write(`\x1b[32m✓ Orchestrator:\x1b[0m ${remote.models.orchestrator}\n`);
263
+ if (remote.models?.reasoning) process.stderr.write(`\x1b[32m✓ Coding:\x1b[0m ${remote.models.reasoning}\n`);
264
+ if (remote.models?.local) process.stderr.write(`\x1b[32m✓ Local:\x1b[0m ${remote.models.local}\n`);
265
+ if (remote.configured_providers?.length) process.stderr.write(`\x1b[32m✓ Providers:\x1b[0m ${remote.configured_providers.join(', ')}\n`);
266
+ process.stderr.write('\n\x1b[32m✓ Settings saved to ~/.bahulam/config.json\x1b[0m\n');
267
+ } catch (err) {
268
+ process.stderr.write(`\x1b[31m✗ ${err.message}\x1b[0m\n`);
269
+ }
270
+ process.exit(0);
271
+ }
272
+
273
+ if (args.command === 'configure') {
274
+ printBanner(VERSION);
275
+ const { resolveWebUrl } = await import('./core/backend-url.mjs');
276
+ const webUrl = resolveWebUrl();
277
+ const settingsUrl = `${webUrl}/dashboard/settings?tab=providers&source=cli`;
278
+ process.stderr.write('\x1b[36mOpening settings in browser...\x1b[0m\n');
279
+ process.stderr.write(`\x1b[2m${settingsUrl}\x1b[0m\n`);
280
+ const openCmd = process.platform === 'darwin' ? 'open' :
281
+ process.platform === 'win32' ? 'start' : 'xdg-open';
282
+ const { exec } = await import('node:child_process');
283
+ exec(`${openCmd} "${settingsUrl}"`, () => {});
284
+ process.stderr.write('\n\x1b[2mConfigure your provider, models, and CLI preferences in the browser.\x1b[0m\n');
285
+ process.stderr.write('\x1b[2mChanges sync automatically to the backend.\x1b[0m\n');
286
+ process.exit(0);
287
+ }
288
+
289
+ if (args.command === 'config') {
290
+ let changed = false;
291
+ if (args.openRouterKey) { auth.saveOpenRouterKey(args.openRouterKey); process.stderr.write('\x1b[32m✓ OpenRouter key saved.\x1b[0m\n'); changed = true; }
292
+ if (args.anthropicKey) { auth.saveAnthropicKey(args.anthropicKey); process.stderr.write('\x1b[32m✓ Anthropic key saved.\x1b[0m\n'); changed = true; }
293
+ if (args.openaiKey) { auth.saveOpenAIKey(args.openaiKey); process.stderr.write('\x1b[32m✓ OpenAI key saved.\x1b[0m\n'); changed = true; }
294
+ if (args.googleKey) { auth.saveGoogleKey(args.googleKey); process.stderr.write('\x1b[32m✓ Google AI key saved.\x1b[0m\n'); changed = true; }
295
+ if (args.gateway) { auth.saveCredentials({ gateway_type: args.gateway }); process.stderr.write(`\x1b[32m✓ Gateway set to ${args.gateway}\x1b[0m\n`); changed = true; }
296
+ if (args.mode) { auth.setMode(args.mode); process.stderr.write(`\x1b[32m✓ Mode set to ${args.mode}\x1b[0m\n`); changed = true; }
297
+ if (args.showConfig || !changed) {
298
+ auth.printConfig();
299
+ }
300
+ process.exit(0);
301
+ }
302
+
303
+ if (args.command === 'workflow') {
304
+ const { handleWorkflowCommand } = await import('./commands/workflow.mjs');
305
+ await handleWorkflowCommand(args);
306
+ process.exit(0);
307
+ }
308
+
309
+ if (args.command === 'agent') {
310
+ const { handleAgentCommand } = await import('./commands/agent.mjs');
311
+ await handleAgentCommand(args);
312
+ process.exit(0);
313
+ }
314
+
315
+ // Load settings (user ~/.claude/settings.json + project .claude/settings.json + local)
316
+ const settings = await loadSettings();
317
+
318
+ /** Re-read credentials from disk (so /login updates take effect). */
319
+ function freshCreds() {
320
+ const c = auth.loadCredentials();
321
+ return {
322
+ token: process.env.TARANG_TOKEN || c.token,
323
+ openRouterKey: process.env.OPENROUTER_API_KEY || c.openRouterKey,
324
+ anthropicKey: process.env.ANTHROPIC_API_KEY || c.anthropicKey,
325
+ openaiKey: process.env.OPENAI_API_KEY || c.openaiKey,
326
+ googleKey: process.env.GOOGLE_API_KEY || c.googleKey,
327
+ backendUrl: c.backendUrl,
328
+ gatewayType: c.gatewayType,
329
+ models: c.models,
330
+ };
331
+ }
332
+
333
+ // Initial load — used for settings and startup checks
334
+ const initCreds = freshCreds();
335
+
336
+ // Apply settings as defaults (CLI flags override settings)
337
+ if (!args.verbose && settings.debugMode) args.verbose = true;
338
+ if (!args.permissionMode && settings.permissions?.defaultMode !== 'default') {
339
+ args.permissionMode = settings.permissions.defaultMode;
340
+ }
341
+
342
+ const toolExecutor = createToolExecutor();
343
+ const approval = new ApprovalManager({ autoApprove: args.yes, planMode: args.plan });
344
+ const formatter = new EventFormatter({ verbose: args.verbose });
345
+ const sessionMgr = new SessionManager();
346
+ const contextRetriever = new ContextRetriever(process.cwd());
347
+
348
+ /** Create an executor (local or remote) for a given instruction. */
349
+ async function createExecutor(instruction, messages = null) {
350
+ // Always re-read credentials so /login changes are picked up
351
+ const creds = freshCreds();
352
+ const mode = await selectMode(instruction, args, { ...creds, backendUrl: creds.backendUrl });
353
+
354
+ if (mode === 'local') {
355
+ // Model priority: CLI flag > synced web setting > settings.json > default
356
+ const localModel = creds.models?.local || settings.model || 'anthropic/claude-sonnet-4-6';
357
+ if (args.verbose) process.stderr.write(`\x1b[2m[mode] local (${localModel})\x1b[0m\n`);
358
+
359
+ // Pick the right API key based on the model/gateway
360
+ let apiKey = creds.anthropicKey;
361
+ let orKey = creds.openRouterKey;
362
+ if (creds.gatewayType === 'openai') apiKey = creds.openaiKey;
363
+ if (creds.gatewayType === 'googleai') apiKey = creds.googleKey;
364
+
365
+ return new LocalAgent({
366
+ apiKey,
367
+ openRouterKey: orKey,
368
+ model: localModel,
369
+ toolExecutor,
370
+ verbose: args.verbose,
371
+ cwd: process.cwd(),
372
+ systemPromptOverride: args.systemPrompt,
373
+ maxTurns: args.maxTurns,
374
+ stagnationDetection: settings.stagnationDetection,
375
+ stagnationThreshold: settings.stagnationThreshold,
376
+ }).execute(instruction, { cwd: process.cwd() });
377
+ } else {
378
+ if (args.verbose) process.stderr.write('\x1b[2m[mode] remote\x1b[0m\n');
379
+
380
+ // Retrieve BM25 context to send to backend
381
+ let indexedContext = {};
382
+ try {
383
+ const chunks = contextRetriever.retrieve(instruction, 8);
384
+ if (chunks.length > 0) {
385
+ indexedContext = {
386
+ indexed: chunks.map(c => ({ id: c.id, score: c.score, text: c.text })),
387
+ };
388
+ if (args.verbose) process.stderr.write(`\x1b[2m[context] ${chunks.length} chunks from BM25 index\x1b[0m\n`);
389
+ }
390
+ } catch {
391
+ // No index available — send without context
392
+ }
393
+
394
+ const client = new TarangStreamClient({
395
+ baseUrl: creds.backendUrl, token: creds.token, toolExecutor,
396
+ verbose: args.verbose, approvalManager: approval,
397
+ });
398
+ // Cancel on SIGINT but don't exit (REPL handles exit)
399
+ const sigHandler = async () => { await client.cancel().catch(() => {}); };
400
+ process.once('SIGINT', sigHandler);
401
+ return client.execute(instruction, { cwd: process.cwd(), ...indexedContext }, messages);
402
+ }
403
+ }
404
+
405
+ if (args.command === 'resume') {
406
+ const state = sessionMgr.loadState();
407
+ if (!state || state.status === 'completed') { process.stderr.write('No resumable session.\n'); process.exit(1); }
408
+ const resumeCreds = freshCreds();
409
+ const client = new TarangStreamClient({ baseUrl: resumeCreds.backendUrl, token: resumeCreds.token, toolExecutor, verbose: args.verbose, approvalManager: approval });
410
+ client.currentTaskId = state.task_id;
411
+ await client.resume();
412
+ for await (const event of client.execute(state.instruction)) formatter.render(event);
413
+ process.exit(0);
414
+ }
415
+
416
+ if (args.instruction) {
417
+ const exec = await createExecutor(args.instruction);
418
+ await executeInstruction(exec, args.instruction, formatter, sessionMgr);
419
+ process.stdout.write('\n');
420
+ process.exit(0);
421
+ }
422
+
423
+ await startTerminalRepl();
424
+ }
425
+
426
+ main().catch(err => { process.stderr.write(`\x1b[31mFatal: ${err.message}\x1b[0m\n`); process.exit(1); });
@@ -0,0 +1,253 @@
1
+ /**
2
+ * MCP Client — multi-transport Model Context Protocol client.
3
+ *
4
+ * Supports four transports:
5
+ * - stdio: spawn child process, communicate via stdin/stdout
6
+ * - sse: Server-Sent Events over HTTP
7
+ * - websocket: bidirectional WebSocket
8
+ * - streamable-http: POST with SSE response (new MCP transport)
9
+ *
10
+ * Auto-detects transport from server config.
11
+ */
12
+
13
+ import { spawn } from 'child_process';
14
+
15
+ const MCP_PROTOCOL_VERSION = '2024-11-05';
16
+
17
+ export class McpClient {
18
+ /**
19
+ * @param {object} serverConfig - { command, args, env, url, transport }
20
+ */
21
+ constructor(serverConfig) {
22
+ this.config = serverConfig;
23
+ this.process = null;
24
+ this.transport = null;
25
+ this.requestId = 0;
26
+ this.pending = new Map();
27
+ this.buffer = '';
28
+ this.tools = [];
29
+ this.resources = [];
30
+ this.serverInfo = null;
31
+ this.connected = false;
32
+ }
33
+
34
+ _detectTransport() {
35
+ if (this.config.transport) return this.config.transport;
36
+ if (this.config.command) return 'stdio';
37
+ if (this.config.url) {
38
+ if (this.config.url.startsWith('ws://') || this.config.url.startsWith('wss://')) return 'websocket';
39
+ if (this.config.url.includes('/sse')) return 'sse';
40
+ return 'streamable-http';
41
+ }
42
+ return 'stdio';
43
+ }
44
+
45
+ async connect() {
46
+ const transportType = this._detectTransport();
47
+
48
+ switch (transportType) {
49
+ case 'stdio': return this._connectStdio();
50
+ case 'sse': return this._connectSSE();
51
+ case 'websocket': return this._connectWebSocket();
52
+ case 'streamable-http': return this._connectStreamableHttp();
53
+ default: throw new Error(`Unknown MCP transport: ${transportType}`);
54
+ }
55
+ }
56
+
57
+ async _connectStdio() {
58
+ this.process = spawn(this.config.command, this.config.args || [], {
59
+ stdio: ['pipe', 'pipe', 'pipe'],
60
+ env: { ...process.env, ...this.config.env },
61
+ });
62
+
63
+ this.process.stdout.on('data', (data) => this._onData(data));
64
+ this.process.stderr.on('data', (data) => {
65
+ if (process.env.MCP_DEBUG) {
66
+ process.stderr.write(`[mcp:${this.config.command}] ${data}`);
67
+ }
68
+ });
69
+
70
+ this.process.on('exit', (code) => {
71
+ this.connected = false;
72
+ for (const [, { reject }] of this.pending) {
73
+ reject(new Error(`MCP server exited with code ${code}`));
74
+ }
75
+ this.pending.clear();
76
+ });
77
+
78
+ const initResult = await this._request('initialize', {
79
+ protocolVersion: MCP_PROTOCOL_VERSION,
80
+ capabilities: {},
81
+ clientInfo: { name: 'open-claude-code', version: '2.0.0' },
82
+ });
83
+
84
+ this.serverInfo = initResult;
85
+ this.connected = true;
86
+ this._notify('notifications/initialized', {});
87
+ return this.serverInfo;
88
+ }
89
+
90
+ async _connectSSE() {
91
+ const { SseTransport } = await import('./transport-sse.mjs');
92
+ this.transport = new SseTransport(this.config.url, {
93
+ headers: this.config.headers || {},
94
+ });
95
+ await this.transport.connect();
96
+ this.transport.onMessage((msg) => {
97
+ if (msg.data?.id && this.pending.has(msg.data.id)) {
98
+ const { resolve, reject } = this.pending.get(msg.data.id);
99
+ this.pending.delete(msg.data.id);
100
+ if (msg.data.error) reject(new Error(msg.data.error.message));
101
+ else resolve(msg.data.result);
102
+ }
103
+ });
104
+ this.connected = true;
105
+ return this._initRemote();
106
+ }
107
+
108
+ async _connectWebSocket() {
109
+ const { WebSocketTransport } = await import('./transport-ws.mjs');
110
+ this.transport = new WebSocketTransport(this.config.url, {
111
+ headers: this.config.headers || {},
112
+ });
113
+ await this.transport.connect();
114
+ this.connected = true;
115
+ this.serverInfo = await this.transport.request('initialize', {
116
+ protocolVersion: MCP_PROTOCOL_VERSION,
117
+ capabilities: {},
118
+ clientInfo: { name: 'open-claude-code', version: '2.0.0' },
119
+ });
120
+ return this.serverInfo;
121
+ }
122
+
123
+ async _connectStreamableHttp() {
124
+ const { StreamableHttpTransport } = await import('./transport-shttp.mjs');
125
+ this.transport = new StreamableHttpTransport(this.config.url, {
126
+ headers: this.config.headers || {},
127
+ });
128
+ await this.transport.connect();
129
+ this.connected = true;
130
+ this.serverInfo = await this.transport.request('initialize', {
131
+ protocolVersion: MCP_PROTOCOL_VERSION,
132
+ capabilities: {},
133
+ clientInfo: { name: 'open-claude-code', version: '2.0.0' },
134
+ });
135
+ return this.serverInfo;
136
+ }
137
+
138
+ async _initRemote() {
139
+ const result = await this._transportRequest('initialize', {
140
+ protocolVersion: MCP_PROTOCOL_VERSION,
141
+ capabilities: {},
142
+ clientInfo: { name: 'open-claude-code', version: '2.0.0' },
143
+ });
144
+ this.serverInfo = result;
145
+ return result;
146
+ }
147
+
148
+ async _transportRequest(method, params) {
149
+ return new Promise((resolve, reject) => {
150
+ const id = ++this.requestId;
151
+ this.pending.set(id, { resolve, reject });
152
+ this.transport.send({ jsonrpc: '2.0', id, method, params });
153
+ });
154
+ }
155
+
156
+ async listTools() {
157
+ let result;
158
+ if (this.transport?.request) {
159
+ result = await this.transport.request('tools/list', {});
160
+ } else if (this.transport) {
161
+ result = await this._transportRequest('tools/list', {});
162
+ } else {
163
+ result = await this._request('tools/list', {});
164
+ }
165
+ this.tools = result?.tools || [];
166
+ return this.tools;
167
+ }
168
+
169
+ async callTool(name, args) {
170
+ const params = { name, arguments: args };
171
+ let result;
172
+ if (this.transport?.request) {
173
+ result = await this.transport.request('tools/call', params);
174
+ } else if (this.transport) {
175
+ result = await this._transportRequest('tools/call', params);
176
+ } else {
177
+ result = await this._request('tools/call', params);
178
+ }
179
+ if (result?.content && Array.isArray(result.content)) {
180
+ return result.content.filter(c => c.type === 'text').map(c => c.text).join('\n');
181
+ }
182
+ return result;
183
+ }
184
+
185
+ async readResource(uri) {
186
+ const params = { uri };
187
+ if (this.transport?.request) return this.transport.request('resources/read', params);
188
+ if (this.transport) return this._transportRequest('resources/read', params);
189
+ return this._request('resources/read', params);
190
+ }
191
+
192
+ async listResources() {
193
+ let result;
194
+ if (this.transport?.request) result = await this.transport.request('resources/list', {});
195
+ else if (this.transport) result = await this._transportRequest('resources/list', {});
196
+ else result = await this._request('resources/list', {});
197
+ this.resources = result?.resources || [];
198
+ return this.resources;
199
+ }
200
+
201
+ async disconnect() {
202
+ this.connected = false;
203
+ if (this.transport) {
204
+ await this.transport.disconnect();
205
+ this.transport = null;
206
+ return;
207
+ }
208
+ if (!this.process) return;
209
+ try {
210
+ await this._request('shutdown', {});
211
+ this._notify('exit', {});
212
+ } catch { /* best effort */ }
213
+
214
+ await new Promise(resolve => {
215
+ const timeout = setTimeout(() => { this.process?.kill('SIGKILL'); resolve(); }, 2000);
216
+ this.process.on('exit', () => { clearTimeout(timeout); resolve(); });
217
+ this.process.kill('SIGTERM');
218
+ });
219
+ this.process = null;
220
+ }
221
+
222
+ _request(method, params) {
223
+ return new Promise((resolve, reject) => {
224
+ const id = ++this.requestId;
225
+ this.pending.set(id, { resolve, reject });
226
+ const msg = JSON.stringify({ jsonrpc: '2.0', id, method, params });
227
+ this.process.stdin.write(msg + '\n');
228
+ });
229
+ }
230
+
231
+ _notify(method, params) {
232
+ const msg = JSON.stringify({ jsonrpc: '2.0', method, params });
233
+ this.process?.stdin.write(msg + '\n');
234
+ }
235
+
236
+ _onData(data) {
237
+ this.buffer += data.toString();
238
+ const lines = this.buffer.split('\n');
239
+ this.buffer = lines.pop();
240
+ for (const line of lines) {
241
+ if (!line.trim()) continue;
242
+ try {
243
+ const msg = JSON.parse(line);
244
+ if (msg.id && this.pending.has(msg.id)) {
245
+ const { resolve, reject } = this.pending.get(msg.id);
246
+ this.pending.delete(msg.id);
247
+ if (msg.error) reject(new Error(`MCP error: ${msg.error.message || JSON.stringify(msg.error)}`));
248
+ else resolve(msg.result);
249
+ }
250
+ } catch { /* malformed */ }
251
+ }
252
+ }
253
+ }