@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
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Streaming Handler — processes Anthropic SSE events from the Messages API.
3
+ *
4
+ * Handles ALL SSE event types:
5
+ * - message_start, message_delta, message_stop
6
+ * - content_block_start, content_block_delta, content_block_stop
7
+ * - ping
8
+ * - error
9
+ *
10
+ * Parses:
11
+ * - thinking blocks (type: "thinking")
12
+ * - tool_use input streaming (type: "input_json_delta")
13
+ * - Usage tracking from message_delta.usage
14
+ */
15
+
16
+ /**
17
+ * Parse an SSE stream from an Anthropic streaming response.
18
+ * @param {Response} response - fetch Response with streaming body
19
+ * @yields {object} Parsed SSE event data
20
+ */
21
+ export async function* streamResponse(response) {
22
+ const reader = response.body.getReader();
23
+ const decoder = new TextDecoder();
24
+ let buffer = '';
25
+
26
+ try {
27
+ while (true) {
28
+ const { done, value } = await reader.read();
29
+ if (done) break;
30
+
31
+ buffer += decoder.decode(value, { stream: true });
32
+
33
+ // SSE events are separated by double newlines
34
+ while (buffer.includes('\n\n')) {
35
+ const idx = buffer.indexOf('\n\n');
36
+ const chunk = buffer.slice(0, idx);
37
+ buffer = buffer.slice(idx + 2);
38
+
39
+ const event = parseSSEChunk(chunk);
40
+ if (event) yield event;
41
+ }
42
+ }
43
+
44
+ // Handle remaining buffer
45
+ if (buffer.trim()) {
46
+ const event = parseSSEChunk(buffer.trim());
47
+ if (event) yield event;
48
+ }
49
+ } finally {
50
+ reader.releaseLock();
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Parse a single SSE chunk into an event object.
56
+ * @param {string} chunk - raw SSE text (may contain event: and data: lines)
57
+ * @returns {object|null} Parsed event or null
58
+ */
59
+ function parseSSEChunk(chunk) {
60
+ let eventType = null;
61
+ let dataLines = [];
62
+
63
+ for (const line of chunk.split('\n')) {
64
+ if (line.startsWith('event: ')) {
65
+ eventType = line.slice(7).trim();
66
+ } else if (line.startsWith('data: ')) {
67
+ dataLines.push(line.slice(6));
68
+ } else if (line.startsWith(':')) {
69
+ // SSE comment, ignore
70
+ continue;
71
+ }
72
+ }
73
+
74
+ // Handle ping events (no data)
75
+ if (eventType === 'ping') {
76
+ return { type: 'ping' };
77
+ }
78
+
79
+ if (dataLines.length === 0) return null;
80
+
81
+ const raw = dataLines.join('\n');
82
+ if (raw === '[DONE]') return { type: 'done' };
83
+
84
+ try {
85
+ const data = JSON.parse(raw);
86
+ return { type: eventType || data.type || 'unknown', ...data };
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Accumulate streaming events into a complete message response.
94
+ * Collects content blocks, thinking blocks, and usage stats.
95
+ *
96
+ * @param {AsyncIterable} events - stream of SSE events
97
+ * @returns {object} Complete message in the same shape as non-streaming API
98
+ */
99
+ export async function accumulateStream(events) {
100
+ const message = {
101
+ id: null,
102
+ role: 'assistant',
103
+ content: [],
104
+ model: null,
105
+ stop_reason: null,
106
+ usage: { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
107
+ };
108
+
109
+ let currentBlock = null;
110
+ let blockIndex = -1;
111
+
112
+ for await (const event of events) {
113
+ switch (event.type) {
114
+ case 'message_start':
115
+ if (event.message) {
116
+ message.id = event.message.id;
117
+ message.model = event.message.model;
118
+ if (event.message.usage) {
119
+ message.usage.input_tokens = event.message.usage.input_tokens || 0;
120
+ message.usage.cache_creation_input_tokens = event.message.usage.cache_creation_input_tokens || 0;
121
+ message.usage.cache_read_input_tokens = event.message.usage.cache_read_input_tokens || 0;
122
+ }
123
+ }
124
+ break;
125
+
126
+ case 'content_block_start':
127
+ blockIndex = event.index ?? message.content.length;
128
+ currentBlock = { ...event.content_block };
129
+ if (currentBlock.type === 'text') currentBlock.text = '';
130
+ if (currentBlock.type === 'thinking') currentBlock.thinking = '';
131
+ if (currentBlock.type === 'tool_use') {
132
+ currentBlock.input = '';
133
+ }
134
+ message.content[blockIndex] = currentBlock;
135
+ break;
136
+
137
+ case 'content_block_delta':
138
+ if (!currentBlock) break;
139
+ if (event.delta?.type === 'text_delta') {
140
+ currentBlock.text += event.delta.text;
141
+ } else if (event.delta?.type === 'thinking_delta') {
142
+ currentBlock.thinking += event.delta.thinking;
143
+ } else if (event.delta?.type === 'input_json_delta') {
144
+ currentBlock.input += event.delta.partial_json;
145
+ }
146
+ break;
147
+
148
+ case 'content_block_stop':
149
+ // Parse tool_use input from accumulated JSON string
150
+ if (currentBlock?.type === 'tool_use' && typeof currentBlock.input === 'string') {
151
+ try {
152
+ currentBlock.input = JSON.parse(currentBlock.input || '{}');
153
+ } catch {
154
+ currentBlock.input = {};
155
+ }
156
+ }
157
+ currentBlock = null;
158
+ break;
159
+
160
+ case 'message_delta':
161
+ if (event.delta?.stop_reason) {
162
+ message.stop_reason = event.delta.stop_reason;
163
+ }
164
+ if (event.usage) {
165
+ message.usage.output_tokens = event.usage.output_tokens || 0;
166
+ }
167
+ break;
168
+
169
+ case 'message_stop':
170
+ break;
171
+
172
+ case 'ping':
173
+ // Keepalive, ignore
174
+ break;
175
+
176
+ case 'error':
177
+ throw new Error(`Stream error: ${event.error?.message || JSON.stringify(event)}`);
178
+ }
179
+ }
180
+
181
+ return message;
182
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * System Prompt Builder — loads and merges CLAUDE.md and KEPLER.md files.
3
+ *
4
+ * Features:
5
+ * - Loads CLAUDE.md from: ~/.claude/CLAUDE.md, project root, parent dirs
6
+ * - Merges in order (global -> project -> local)
7
+ * - Splits at cache boundary (static prefix cached, dynamic suffix not)
8
+ * - Includes tool schemas in the system prompt
9
+ */
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import os from 'os';
13
+ import { loadKeplerMemory } from '../config/memory-loader.mjs';
14
+
15
+ /**
16
+ * Load all CLAUDE.md files and merge them in order.
17
+ * @param {string} [cwd] - current working directory
18
+ * @returns {string[]} Array of CLAUDE.md contents in merge order
19
+ */
20
+ export function loadClaudeMdFiles(cwd = process.cwd()) {
21
+ const files = [];
22
+
23
+ // 1. Global: ~/.claude/CLAUDE.md
24
+ const globalPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
25
+ if (fs.existsSync(globalPath)) {
26
+ try {
27
+ files.push({ source: 'global', content: fs.readFileSync(globalPath, 'utf-8') });
28
+ } catch { /* skip */ }
29
+ }
30
+
31
+ // 2. Walk from cwd up to root, collecting CLAUDE.md files
32
+ const projectFiles = [];
33
+ let dir = path.resolve(cwd);
34
+ const root = path.parse(dir).root;
35
+ while (dir !== root) {
36
+ const candidates = [
37
+ path.join(dir, 'CLAUDE.md'),
38
+ path.join(dir, '.claude', 'CLAUDE.md'),
39
+ ];
40
+ for (const f of candidates) {
41
+ if (fs.existsSync(f)) {
42
+ try {
43
+ projectFiles.push({ source: dir, content: fs.readFileSync(f, 'utf-8'), path: f });
44
+ } catch { /* skip */ }
45
+ }
46
+ }
47
+ dir = path.dirname(dir);
48
+ }
49
+
50
+ // Reverse so parent dirs come first (global -> project -> local)
51
+ projectFiles.reverse();
52
+ files.push(...projectFiles);
53
+
54
+ return files;
55
+ }
56
+
57
+ /**
58
+ * Build the full system prompt from CLAUDE.md files and tool schemas.
59
+ * @param {object} options
60
+ * @param {string} [options.cwd] - current working directory
61
+ * @param {Array} [options.tools] - tool definitions for schema inclusion
62
+ * @param {string} [options.override] - override system prompt entirely
63
+ * @param {string[]} [options.addDirs] - additional directories to search for CLAUDE.md
64
+ * @returns {{ staticPrefix: string, dynamicSuffix: string, full: string }}
65
+ */
66
+ export function buildSystemPrompt({ cwd, tools, override, addDirs } = {}) {
67
+ if (override) {
68
+ return { staticPrefix: override, dynamicSuffix: '', full: override };
69
+ }
70
+
71
+ const parts = ['You are an AI coding assistant.'];
72
+
73
+ // Load CLAUDE.md files
74
+ const mdFiles = loadClaudeMdFiles(cwd);
75
+
76
+ // Add additional directories
77
+ if (addDirs) {
78
+ for (const dir of addDirs) {
79
+ const p = path.join(dir, 'CLAUDE.md');
80
+ if (fs.existsSync(p)) {
81
+ try {
82
+ mdFiles.push({ source: dir, content: fs.readFileSync(p, 'utf-8') });
83
+ } catch { /* skip */ }
84
+ }
85
+ }
86
+ }
87
+
88
+ for (const f of mdFiles) {
89
+ parts.push(f.content);
90
+ }
91
+
92
+ for (const f of loadKeplerMemory({ cwd })) {
93
+ parts.push(f.content);
94
+ }
95
+
96
+ // The static prefix is the base prompt + CLAUDE.md content (cacheable)
97
+ const staticPrefix = parts.join('\n\n');
98
+
99
+ // Dynamic suffix includes tool schemas (changes per-request)
100
+ let dynamicSuffix = '';
101
+ if (tools && tools.length > 0) {
102
+ const toolSummary = tools.map(t =>
103
+ `- ${t.name}: ${(t.description || '').slice(0, 100)}`
104
+ ).join('\n');
105
+ dynamicSuffix = `\n\nAvailable tools:\n${toolSummary}`;
106
+ }
107
+
108
+ return {
109
+ staticPrefix,
110
+ dynamicSuffix,
111
+ full: staticPrefix + dynamicSuffix,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Convert system prompt to Anthropic cache-control format.
117
+ * @param {string} staticPrefix
118
+ * @param {string} dynamicSuffix
119
+ * @returns {Array} system blocks with cache_control
120
+ */
121
+ export function toCacheBlocks(staticPrefix, dynamicSuffix) {
122
+ const blocks = [];
123
+
124
+ if (staticPrefix) {
125
+ blocks.push({
126
+ type: 'text',
127
+ text: staticPrefix,
128
+ cache_control: { type: 'ephemeral' },
129
+ });
130
+ }
131
+
132
+ if (dynamicSuffix) {
133
+ blocks.push({
134
+ type: 'text',
135
+ text: dynamicSuffix,
136
+ });
137
+ }
138
+
139
+ return blocks;
140
+ }
@@ -0,0 +1,196 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ export const TASK_FILES = Object.freeze({
5
+ active: 'active.md',
6
+ backlog: 'backlog.md',
7
+ blocked: 'blocked.md',
8
+ done: 'done.md',
9
+ });
10
+
11
+ const DEFAULT_CONTENT = Object.freeze({
12
+ 'README.md': '# Bahulam Tasks\n\nChecklist files are read every turn.\n',
13
+ 'active.md': '# Active\n\n',
14
+ 'backlog.md': '# Backlog\n\n',
15
+ 'blocked.md': '# Blocked\n\n',
16
+ 'done.md': '# Done\n\n',
17
+ });
18
+
19
+ export function ensureTaskFiles({ cwd = process.cwd() } = {}) {
20
+ const dir = path.join(cwd, '.bahulam', 'tasks');
21
+ fs.mkdirSync(dir, { recursive: true });
22
+ const written = [];
23
+ for (const [name, content] of Object.entries(DEFAULT_CONTENT)) {
24
+ const filePath = path.join(dir, name);
25
+ if (fs.existsSync(filePath)) continue;
26
+ fs.writeFileSync(filePath, content);
27
+ written.push(filePath);
28
+ }
29
+ return { dir, written };
30
+ }
31
+
32
+ export function loadTaskBoard({ cwd = process.cwd(), create = false } = {}) {
33
+ const dir = path.join(cwd, '.bahulam', 'tasks');
34
+ if (create) ensureTaskFiles({ cwd });
35
+ const lists = {};
36
+ for (const [list, fileName] of Object.entries(TASK_FILES)) {
37
+ const filePath = path.join(dir, fileName);
38
+ const content = readText(filePath);
39
+ lists[list] = {
40
+ list,
41
+ fileName,
42
+ path: filePath,
43
+ exists: fs.existsSync(filePath),
44
+ content,
45
+ tasks: parseTaskMarkdown(content, list),
46
+ };
47
+ }
48
+
49
+ const planPath = path.join(cwd, '.bahulam', 'plan.md');
50
+ const goalPath = path.join(cwd, '.bahulam', 'goal.md');
51
+ return {
52
+ dir,
53
+ lists,
54
+ plan: { path: planPath, exists: fs.existsSync(planPath), content: readText(planPath) },
55
+ goal: { path: goalPath, exists: fs.existsSync(goalPath), content: readText(goalPath) },
56
+ };
57
+ }
58
+
59
+ export function appendTask({ cwd = process.cwd(), list = 'backlog', text }) {
60
+ const normalized = normalizeList(list);
61
+ const taskText = String(text || '').trim();
62
+ if (!taskText) throw new Error('Task text is required');
63
+ const { dir } = ensureTaskFiles({ cwd });
64
+ const filePath = path.join(dir, TASK_FILES[normalized]);
65
+ const prefix = normalized === 'done' ? '- [x]' : '- [ ]';
66
+ fs.appendFileSync(filePath, `${prefix} ${taskText}\n`);
67
+ return { list: normalized, path: filePath, text: taskText };
68
+ }
69
+
70
+ export function updateTask({ cwd = process.cwd(), list = 'active', index, text, checked } = {}) {
71
+ const normalized = normalizeList(list);
72
+ const taskIndex = normalizeTaskIndex(index);
73
+ ensureTaskFiles({ cwd });
74
+ const filePath = taskFilePath(cwd, normalized);
75
+ const content = readText(filePath);
76
+ const tasks = parseTaskMarkdown(content, normalized);
77
+ const task = tasks[taskIndex - 1];
78
+ if (!task) throw new Error(`No task ${taskIndex} in ${normalized}`);
79
+
80
+ const lines = content.split(/\r?\n/);
81
+ const nextText = String(text ?? task.text).trim();
82
+ if (!nextText) throw new Error('Task text is required');
83
+ const nextChecked = checked === undefined ? task.checked : Boolean(checked);
84
+ lines[task.line - 1] = taskLine(nextText, normalized, nextChecked);
85
+ fs.writeFileSync(filePath, lines.join('\n'));
86
+ return { list: normalized, path: filePath, index: taskIndex, previous: task, text: nextText, checked: nextChecked };
87
+ }
88
+
89
+ export function removeTask({ cwd = process.cwd(), list = 'active', index } = {}) {
90
+ const normalized = normalizeList(list);
91
+ const taskIndex = normalizeTaskIndex(index);
92
+ ensureTaskFiles({ cwd });
93
+ const filePath = taskFilePath(cwd, normalized);
94
+ const content = readText(filePath);
95
+ const tasks = parseTaskMarkdown(content, normalized);
96
+ const task = tasks[taskIndex - 1];
97
+ if (!task) throw new Error(`No task ${taskIndex} in ${normalized}`);
98
+
99
+ const lines = content.split(/\r?\n/);
100
+ lines.splice(task.line - 1, 1);
101
+ fs.writeFileSync(filePath, lines.join('\n'));
102
+ return { list: normalized, path: filePath, index: taskIndex, task };
103
+ }
104
+
105
+ export function moveTask({ cwd = process.cwd(), from = 'active', index, to = 'done', text } = {}) {
106
+ const source = normalizeList(from);
107
+ const target = normalizeList(to);
108
+ const removed = removeTask({ cwd, list: source, index });
109
+ const taskText = String(text ?? removed.task.text).trim();
110
+ const appended = appendTask({ cwd, list: target, text: taskText });
111
+ return {
112
+ from: source,
113
+ to: target,
114
+ index: removed.index,
115
+ text: taskText,
116
+ sourcePath: removed.path,
117
+ targetPath: appended.path,
118
+ };
119
+ }
120
+
121
+ export function taskCounts(board) {
122
+ const lists = board?.lists || {};
123
+ return Object.fromEntries(
124
+ Object.entries(TASK_FILES).map(([list]) => [list, lists[list]?.tasks?.length || 0]),
125
+ );
126
+ }
127
+
128
+ export function normalizeList(value) {
129
+ const key = String(value || '').toLowerCase();
130
+ if (key === 'todo' || key === 'pending') return 'backlog';
131
+ if (key === 'current' || key === 'doing') return 'active';
132
+ if (key === 'complete' || key === 'completed') return 'done';
133
+ if (key in TASK_FILES) return key;
134
+ throw new Error(`Unknown task list: ${value}`);
135
+ }
136
+
137
+ function normalizeTaskIndex(value) {
138
+ const n = Number(value);
139
+ if (!Number.isInteger(n) || n < 1) throw new Error('Task index must be a positive number');
140
+ return n;
141
+ }
142
+
143
+ function taskFilePath(cwd, list) {
144
+ return path.join(cwd, '.bahulam', 'tasks', TASK_FILES[list]);
145
+ }
146
+
147
+ function taskLine(text, list, checked = false) {
148
+ const mark = checked || list === 'done' ? 'x' : ' ';
149
+ return `- [${mark}] ${text}`;
150
+ }
151
+
152
+ function readText(filePath) {
153
+ try {
154
+ return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : '';
155
+ } catch {
156
+ return '';
157
+ }
158
+ }
159
+
160
+ export function parseTaskMarkdown(content, list = 'backlog') {
161
+ const tasks = [];
162
+ let section = '';
163
+ const lines = String(content || '').split(/\r?\n/);
164
+ for (let i = 0; i < lines.length; i++) {
165
+ const line = lines[i];
166
+ const heading = line.match(/^#{1,6}\s+(.+?)\s*$/);
167
+ if (heading) {
168
+ section = heading[1].trim();
169
+ continue;
170
+ }
171
+
172
+ const checkbox = line.match(/^\s*[-*]\s+\[([ xX!-])\]\s+(.+?)\s*$/);
173
+ if (checkbox) {
174
+ tasks.push({
175
+ list,
176
+ line: i + 1,
177
+ section,
178
+ checked: checkbox[1].toLowerCase() === 'x',
179
+ text: checkbox[2].trim(),
180
+ });
181
+ continue;
182
+ }
183
+
184
+ const bullet = line.match(/^\s*[-*]\s+(.+?)\s*$/);
185
+ if (bullet && !bullet[1].startsWith('`')) {
186
+ tasks.push({
187
+ list,
188
+ line: i + 1,
189
+ section,
190
+ checked: list === 'done',
191
+ text: bullet[1].trim(),
192
+ });
193
+ }
194
+ }
195
+ return tasks;
196
+ }