@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,641 @@
1
+ import * as crypto from 'node:crypto';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { ContextRetriever } from '../context/retriever.mjs';
7
+ import { buildProjectSkeleton } from '../context/skeleton.mjs';
8
+ import { indexDir as getIndexDir, projectConfigDir, bahulamHome } from '../core/paths.mjs';
9
+
10
+ const RESOURCE_FILE = 'project-resource.json';
11
+
12
+ /**
13
+ * Expand "~" and trim surrounding quotes/whitespace. Does NOT unescape shell
14
+ * meta characters — that is a separate, last-resort step done only if the
15
+ * literal path does not resolve.
16
+ */
17
+ function normalizePathInput(p) {
18
+ let s = String(p || '').trim();
19
+ // Trim balanced surrounding quotes.
20
+ if ((s.startsWith('"') && s.endsWith('"')) ||
21
+ (s.startsWith("'") && s.endsWith("'"))) {
22
+ s = s.slice(1, -1);
23
+ }
24
+ // Tilde expansion (~ or ~/...).
25
+ if (s === '~' || s.startsWith('~/')) {
26
+ s = path.join(os.homedir(), s.slice(1));
27
+ }
28
+ return s;
29
+ }
30
+
31
+ /**
32
+ * Replace common shell escape sequences with their literal characters. Used
33
+ * as a fallback when the literal path does not resolve — the agent may have
34
+ * pasted a copy of what they would type at a shell prompt.
35
+ */
36
+ function unescapeShellPath(p) {
37
+ return String(p || '').replace(/\\([ \t()&$;'"])/g, '$1');
38
+ }
39
+
40
+ const LANGUAGE_EXTENSIONS = new Map([
41
+ ['.py', 'Python'],
42
+ ['.js', 'JavaScript'],
43
+ ['.mjs', 'JavaScript'],
44
+ ['.ts', 'TypeScript'],
45
+ ['.tsx', 'TypeScript'],
46
+ ['.go', 'Go'],
47
+ ['.rs', 'Rust'],
48
+ ['.java', 'Java'],
49
+ ['.rb', 'Ruby'],
50
+ ['.c', 'C'],
51
+ ['.cpp', 'C++'],
52
+ ]);
53
+ const IGNORED_DIRS = new Set([
54
+ '.git', '.bahulam', '.next', '.venv', '__pycache__',
55
+ 'build', 'dist', 'node_modules', 'venv',
56
+ ]);
57
+
58
+ function projectId(canonicalPath) {
59
+ return crypto.createHash('sha256').update(canonicalPath).digest('hex').slice(0, 12);
60
+ }
61
+
62
+ function isWithin(root, candidate) {
63
+ const relative = path.relative(root, candidate);
64
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
65
+ }
66
+
67
+ function uniqueValues(values) {
68
+ return [...new Set(values.filter(Boolean))];
69
+ }
70
+
71
+ function canonicalRoot(rootPath) {
72
+ const resolved = path.resolve(normalizePathInput(rootPath));
73
+ try {
74
+ return fs.realpathSync(resolved);
75
+ } catch {
76
+ return resolved;
77
+ }
78
+ }
79
+
80
+ function canonicalizeCandidate(candidate) {
81
+ if (fs.existsSync(candidate)) return fs.realpathSync(candidate);
82
+
83
+ const missing = [];
84
+ let parent = candidate;
85
+ while (!fs.existsSync(parent)) {
86
+ const next = path.dirname(parent);
87
+ if (next === parent) break;
88
+ missing.unshift(path.basename(parent));
89
+ parent = next;
90
+ }
91
+ return path.join(fs.realpathSync(parent), ...missing);
92
+ }
93
+
94
+ function projectFingerprint(projectDir) {
95
+ const hash = crypto.createHash('sha256');
96
+ const queue = [projectDir];
97
+
98
+ while (queue.length > 0) {
99
+ const dir = queue.shift();
100
+ let entries;
101
+ try {
102
+ entries = fs.readdirSync(dir, { withFileTypes: true })
103
+ .sort((a, b) => a.name.localeCompare(b.name));
104
+ } catch {
105
+ continue;
106
+ }
107
+ for (const entry of entries) {
108
+ if (entry.name.startsWith('.') || IGNORED_DIRS.has(entry.name)) continue;
109
+ const fullPath = path.join(dir, entry.name);
110
+ if (entry.isDirectory()) {
111
+ queue.push(fullPath);
112
+ continue;
113
+ }
114
+ if (!entry.isFile()) continue;
115
+ try {
116
+ const stat = fs.statSync(fullPath);
117
+ hash.update(
118
+ `${path.relative(projectDir, fullPath)}:${stat.size}:${Math.trunc(stat.mtimeMs)}\n`
119
+ );
120
+ } catch { /* file changed during scan */ }
121
+ }
122
+ }
123
+ return hash.digest('hex').slice(0, 16);
124
+ }
125
+
126
+ function detectLanguages(projectDir) {
127
+ const counts = new Map();
128
+ const queue = [projectDir];
129
+ let scanned = 0;
130
+
131
+ while (queue.length > 0 && scanned < 500) {
132
+ const dir = queue.shift();
133
+ let entries;
134
+ try {
135
+ entries = fs.readdirSync(dir, { withFileTypes: true });
136
+ } catch {
137
+ continue;
138
+ }
139
+ for (const entry of entries) {
140
+ if (scanned >= 500) break;
141
+ if (entry.name.startsWith('.') || IGNORED_DIRS.has(entry.name)) continue;
142
+ const fullPath = path.join(dir, entry.name);
143
+ if (entry.isDirectory()) {
144
+ queue.push(fullPath);
145
+ } else if (entry.isFile()) {
146
+ scanned++;
147
+ const language = LANGUAGE_EXTENSIONS.get(path.extname(entry.name));
148
+ if (language) counts.set(language, (counts.get(language) || 0) + 1);
149
+ }
150
+ }
151
+ }
152
+
153
+ return [...counts.entries()]
154
+ .sort((a, b) => b[1] - a[1])
155
+ .slice(0, 4)
156
+ .map(([language]) => language);
157
+ }
158
+
159
+ function detectCommands(projectDir) {
160
+ const commands = {};
161
+ try {
162
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf-8'));
163
+ if (pkg.scripts?.test) commands.test = 'npm test';
164
+ if (pkg.scripts?.build) commands.build = 'npm run build';
165
+ if (pkg.scripts?.lint) commands.lint = 'npm run lint';
166
+ } catch { /* no package.json */ }
167
+
168
+ if (
169
+ fs.existsSync(path.join(projectDir, 'pyproject.toml')) ||
170
+ fs.existsSync(path.join(projectDir, 'setup.py'))
171
+ ) {
172
+ if (!commands.test) commands.test = 'python -m pytest';
173
+ }
174
+ if (fs.existsSync(path.join(projectDir, 'Makefile')) && !commands.build) {
175
+ commands.build = 'make';
176
+ }
177
+ return commands;
178
+ }
179
+
180
+ function commandVersion(command, args = ['--version']) {
181
+ try {
182
+ const result = spawnSync(command, args, {
183
+ encoding: 'utf-8',
184
+ timeout: 2000,
185
+ windowsHide: true,
186
+ });
187
+ if (result.error || result.status !== 0) return '';
188
+ return `${result.stdout || result.stderr || ''}`.trim().split('\n')[0].slice(0, 120);
189
+ } catch {
190
+ return '';
191
+ }
192
+ }
193
+
194
+ function detectEnvironment() {
195
+ const candidates = [
196
+ ['python', 'python3'],
197
+ ['node', 'node'],
198
+ ['git', 'git'],
199
+ ['npm', 'npm'],
200
+ ['uv', 'uv'],
201
+ ['pytest', 'pytest'],
202
+ ['docker', 'docker'],
203
+ ];
204
+ const tools = {};
205
+ for (const [name, command] of candidates) {
206
+ const version = commandVersion(command);
207
+ if (version) tools[name] = version;
208
+ }
209
+ return {
210
+ platform: os.platform(),
211
+ release: os.release(),
212
+ architecture: os.arch(),
213
+ shell: process.env.SHELL || process.env.ComSpec || '',
214
+ node: process.version,
215
+ tools,
216
+ };
217
+ }
218
+
219
+ function formatResource(resource) {
220
+ const lines = [
221
+ `Project registered: ${resource.name} (project_id=${resource.project_id})`,
222
+ `Root: ${resource.root}`,
223
+ `Languages: ${resource.languages.join(', ') || 'unknown'}`,
224
+ `Index: ${resource.index_status} (${resource.index_version})`,
225
+ ];
226
+ if (resource.environment) {
227
+ const env = resource.environment;
228
+ lines.push(
229
+ `Environment: ${env.platform || 'unknown'} ${env.release || ''} ` +
230
+ `(${env.architecture || 'unknown'}), shell=${env.shell || 'unknown'}, node=${env.node || 'unknown'}`
231
+ );
232
+ const toolVersions = Object.entries(env.tools || {});
233
+ if (toolVersions.length > 0) {
234
+ lines.push(`Available tools: ${toolVersions.map(([name, version]) =>
235
+ `${name}=${version}`).join(', ')}`);
236
+ }
237
+ }
238
+ if (Object.keys(resource.commands).length > 0) {
239
+ lines.push(`Commands: ${Object.entries(resource.commands)
240
+ .map(([name, command]) => `${name}="${command}"`).join(', ')}`);
241
+ }
242
+ if (resource.skills_index && resource.skills_index.length > 0) {
243
+ lines.push(`Skills: ${resource.skills_index.map(s => s.name).join(', ')}`);
244
+ }
245
+ lines.push('', resource.overview);
246
+ if (resource.project_context) {
247
+ lines.push('', '--- Project Context ---', resource.project_context);
248
+ }
249
+ if (resource.style) {
250
+ lines.push('', '--- Project Style ---', resource.style);
251
+ }
252
+ if (resource.goal) {
253
+ lines.push('', '--- Current Goal ---', resource.goal);
254
+ }
255
+ if (resource.plan) {
256
+ lines.push('', '--- Current Plan ---', resource.plan);
257
+ }
258
+ return lines.join('\n');
259
+ }
260
+
261
+ function _readIfExists(dir, filename, maxChars = 8000) {
262
+ try {
263
+ const filePath = path.join(dir, filename);
264
+ if (!fs.existsSync(filePath)) return '';
265
+ const content = fs.readFileSync(filePath, 'utf-8');
266
+ if (content.length > maxChars) {
267
+ // 70/20 head/tail truncation
268
+ const head = Math.floor(maxChars * 0.7);
269
+ const tail = Math.floor(maxChars * 0.2);
270
+ return content.slice(0, head) + '\n\n[...truncated...]\n\n' + content.slice(-tail);
271
+ }
272
+ return content;
273
+ } catch { return ''; }
274
+ }
275
+
276
+ function _scanSkills(keplerDir) {
277
+ const skillsDir = path.join(keplerDir, 'skills');
278
+ if (!fs.existsSync(skillsDir)) return [];
279
+ try {
280
+ return fs.readdirSync(skillsDir, { withFileTypes: true })
281
+ .map(entry => {
282
+ const file = entry.isDirectory()
283
+ ? path.join(skillsDir, entry.name, 'SKILL.md')
284
+ : path.join(skillsDir, entry.name);
285
+ if (!fs.existsSync(file) || !file.endsWith('.md')) return null;
286
+ const content = fs.readFileSync(file, 'utf-8');
287
+ const descMatch = content.match(/^#\s+.*\n+(.+)/);
288
+ return {
289
+ name: entry.isDirectory() ? entry.name : entry.name.replace('.md', ''),
290
+ description: content.match(/^description:\s*(.+)$/mi)?.[1]?.trim()
291
+ || (descMatch ? descMatch[1].slice(0, 100) : entry.name.replace('.md', '')),
292
+ };
293
+ })
294
+ .filter(Boolean);
295
+ } catch { return []; }
296
+ }
297
+
298
+ function defaultScratchRoots() {
299
+ return uniqueValues([
300
+ '/tmp',
301
+ '/private/tmp',
302
+ os.tmpdir(),
303
+ process.env.TMPDIR,
304
+ ...(process.env.KEPLER_SCRATCH_ROOTS || '')
305
+ .split(path.delimiter)
306
+ .map(s => s.trim())
307
+ .filter(Boolean),
308
+ ]).map(canonicalRoot);
309
+ }
310
+
311
+ export class ProjectRegistry {
312
+ constructor() {
313
+ this.projects = new Map();
314
+ this.scratchRoots = new Set(defaultScratchRoots());
315
+ this._globalIdentity = null;
316
+ this._globalPreferences = null;
317
+ this._globalSkills = null;
318
+ }
319
+
320
+ addScratchRoot(rawPath) {
321
+ if (!rawPath) return null;
322
+ const root = canonicalRoot(rawPath);
323
+ this.scratchRoots.add(root);
324
+ return root;
325
+ }
326
+
327
+ /**
328
+ * Load global context from ~/.bahulam/ (once per session).
329
+ */
330
+ loadGlobalContext() {
331
+ if (this._globalIdentity !== null) return;
332
+ // Resolver — prefers ~/.bahulam, falls back to ~/.kepler for legacy installs.
333
+ const globalDir = bahulamHome();
334
+ this._globalIdentity = _readIfExists(globalDir, 'identity.md', 4000);
335
+ this._globalPreferences = _readIfExists(globalDir, 'preferences.md', 2000);
336
+ this._globalSkills = _scanSkills(globalDir);
337
+ }
338
+
339
+ /**
340
+ * Get the global agent context (identity, preferences, skills).
341
+ */
342
+ getGlobalContext() {
343
+ this.loadGlobalContext();
344
+ return {
345
+ identity: this._globalIdentity || '',
346
+ preferences: this._globalPreferences || '',
347
+ skills: this._globalSkills || [],
348
+ };
349
+ }
350
+
351
+ // PRD-69 project context is live metadata, not index cache. Re-read it on
352
+ // every registration attempt so repeated get_project_overview calls pick up
353
+ // .bahulam/KEPLER.md, goal/plan/style, skills, AGENTS.md, etc. changes.
354
+ _attachLiveContext(resource, root) {
355
+ // Resolver — prefers .bahulam/, falls back to .kepler/ for legacy projects.
356
+ const keplerDir = projectConfigDir(root);
357
+ resource.environment = detectEnvironment();
358
+ resource.project_context = _readIfExists(keplerDir, 'KEPLER.md', 10000) ||
359
+ _readIfExists(root, 'KEPLER.md', 10000) ||
360
+ _readIfExists(keplerDir, 'project.md', 8000);
361
+ resource.style = _readIfExists(keplerDir, 'style.md', 4000);
362
+ resource.goal = _readIfExists(keplerDir, 'goal.md', 2000);
363
+ resource.plan = _readIfExists(keplerDir, 'plan.md', 6000);
364
+ resource.skills_index = _scanSkills(keplerDir);
365
+
366
+ if (!resource.project_context) {
367
+ for (const name of ['.bahulam.md', 'AGENTS.md', 'CLAUDE.md']) {
368
+ const content = _readIfExists(root, name, 8000);
369
+ if (content) { resource.project_context = content; break; }
370
+ }
371
+ }
372
+ return resource;
373
+ }
374
+
375
+ async register(rawPath, { forceRefresh = false, force_refresh = false } = {}) {
376
+ if (!rawPath) {
377
+ throw new Error('get_project_overview requires a project path');
378
+ }
379
+
380
+ // LLM sometimes passes shell-escaped paths ("Tarang\ Orca") or paths
381
+ // beginning with "~". Normalize defensively so the tool does not bounce
382
+ // back a "not found" error on a path that's correct apart from quoting.
383
+ rawPath = normalizePathInput(rawPath);
384
+
385
+ if (!path.isAbsolute(rawPath)) {
386
+ rawPath = path.resolve(process.cwd(), rawPath);
387
+ }
388
+
389
+ let root;
390
+ try {
391
+ root = fs.realpathSync(rawPath);
392
+ } catch {
393
+ // Try the unescaped variant explicitly so the error message can
394
+ // tell the agent what it actually attempted.
395
+ const unescaped = unescapeShellPath(rawPath);
396
+ if (unescaped !== rawPath) {
397
+ try { root = fs.realpathSync(unescaped); }
398
+ catch { throw new Error(`Project path not found: ${rawPath} (also tried ${unescaped})`); }
399
+ } else {
400
+ throw new Error(`Project path not found: ${rawPath}`);
401
+ }
402
+ }
403
+ if (!fs.statSync(root).isDirectory()) {
404
+ throw new Error(`Project path is not a directory: ${root}`);
405
+ }
406
+ if (root === path.parse(root).root || root === os.homedir()) {
407
+ throw new Error(
408
+ `Refusing to index ${root} — too broad. Pass the project directory itself.`
409
+ );
410
+ }
411
+
412
+ const id = projectId(root);
413
+ const fingerprint = projectFingerprint(root);
414
+ const existing = this.projects.get(id);
415
+ const shouldForceRefresh = Boolean(forceRefresh || force_refresh);
416
+ if (existing && !shouldForceRefresh && existing.resource.index_version === fingerprint) {
417
+ this._attachLiveContext(existing.resource, root);
418
+ return {
419
+ already_registered: true,
420
+ refreshed: false,
421
+ resource: existing.resource,
422
+ output:
423
+ `Project already registered as project_id=${id}. ` +
424
+ `Use project_id=${id} with search_code and use absolute paths for file tools.`,
425
+ };
426
+ }
427
+
428
+ const retriever = new ContextRetriever(root);
429
+ const resourcePath = path.join(getIndexDir(root), RESOURCE_FILE);
430
+ let resource = null;
431
+
432
+ try {
433
+ const persisted = JSON.parse(fs.readFileSync(resourcePath, 'utf-8'));
434
+ if (!shouldForceRefresh && persisted.index_version === fingerprint && retriever.loadIndex()) {
435
+ resource = persisted;
436
+ }
437
+ } catch { /* missing or stale index */ }
438
+
439
+ if (!resource) {
440
+ await retriever.buildIndex();
441
+ resource = {
442
+ project_id: id,
443
+ root,
444
+ name: path.basename(root),
445
+ languages: detectLanguages(root),
446
+ commands: detectCommands(root),
447
+ overview: buildProjectSkeleton(root, { maxFiles: 150, maxChars: 2500 }) ||
448
+ `Project at ${root}`,
449
+ index_status: 'ready',
450
+ index_version: fingerprint,
451
+ };
452
+ fs.writeFileSync(resourcePath, JSON.stringify(resource));
453
+ }
454
+
455
+ this._attachLiveContext(resource, root);
456
+ this.projects.set(id, { resource, retriever });
457
+ const refreshed = Boolean(existing);
458
+ return {
459
+ already_registered: refreshed,
460
+ refreshed,
461
+ resource,
462
+ output: refreshed
463
+ ? `Project refreshed: ${resource.name} (project_id=${id})\nRoot: ${resource.root}`
464
+ : formatResource(resource),
465
+ };
466
+ }
467
+
468
+ resources() {
469
+ return [...this.projects.values()].map(({ resource }) => resource);
470
+ }
471
+
472
+ get(projectIdValue) {
473
+ return this.projects.get(projectIdValue) || null;
474
+ }
475
+
476
+ projectScratchRoots() {
477
+ return this.resources().map(resource => path.join(projectConfigDir(resource.root), 'tmp'));
478
+ }
479
+
480
+ allowedScratchRoots() {
481
+ return uniqueValues([
482
+ ...this.scratchRoots,
483
+ ...this.projectScratchRoots(),
484
+ ]).map(canonicalRoot);
485
+ }
486
+
487
+ isAllowedScratchPath(filePath) {
488
+ const normalized = normalizePathInput(filePath);
489
+ const candidate = canonicalizeCandidate(path.resolve(normalized));
490
+ return this.allowedScratchRoots().some(root => isWithin(root, candidate));
491
+ }
492
+
493
+ async registerFileRead(candidate) {
494
+ if (!candidate || !fs.existsSync(candidate)) return null;
495
+ let stat;
496
+ try {
497
+ stat = fs.statSync(candidate);
498
+ } catch {
499
+ return null;
500
+ }
501
+ if (!stat.isFile()) return null;
502
+
503
+ const filePath = fs.realpathSync(candidate);
504
+ const dir = path.dirname(filePath);
505
+ if (dir === path.parse(dir).root || dir === os.homedir()) return null;
506
+
507
+ const registered = await this.register(dir);
508
+ const owner = this.projects.get(registered.resource.project_id);
509
+ if (!owner) return null;
510
+
511
+ const files = Array.isArray(owner.resource.files_read)
512
+ ? owner.resource.files_read
513
+ : [];
514
+ if (!files.includes(filePath)) {
515
+ owner.resource.files_read = [...files, filePath];
516
+ }
517
+ return { filePath, project: owner, registered };
518
+ }
519
+
520
+ async resolvePath(rawPath, projectIdValue, { allowMissing = false, allowExternalFileRead = false } = {}) {
521
+ let root = null;
522
+ if (projectIdValue) {
523
+ root = this.get(projectIdValue)?.resource.root || null;
524
+ if (!root) throw new Error(`Unknown project_id: ${projectIdValue}`);
525
+ }
526
+
527
+ if (!rawPath) {
528
+ if (root) return root;
529
+ if (this.projects.size === 1) return this.resources()[0].root;
530
+ // Fall back to the first registered project when the model omits
531
+ // both path and project_id. Beats throwing on an inferable case.
532
+ const first = this.resources()[0];
533
+ if (first) return first.root;
534
+ throw new Error('No projects registered. Call get_project_overview first.');
535
+ }
536
+
537
+ // LLM frequently passes shell-quoted paths copied from a terminal,
538
+ // e.g. "Tarang\ Orca/src/app/\(kepler\)/page.tsx". Normalize here so
539
+ // every tool benefits, not just get_project_overview.
540
+ rawPath = normalizePathInput(rawPath);
541
+
542
+ const buildCandidate = (input) => {
543
+ if (path.isAbsolute(input)) {
544
+ return canonicalizeCandidate(path.resolve(input));
545
+ }
546
+ if (!root) {
547
+ if (this.projects.size === 1) {
548
+ return canonicalizeCandidate(path.resolve(this.resources()[0].root, input));
549
+ }
550
+ if (this.projects.size > 1) {
551
+ throw new Error('Relative path requires project_id when multiple projects are registered. Pass project_id or use an absolute path.');
552
+ }
553
+ throw new Error('No projects registered. Call get_project_overview first.');
554
+ }
555
+ return canonicalizeCandidate(path.resolve(root, input));
556
+ };
557
+
558
+ let candidate = buildCandidate(rawPath);
559
+
560
+ const findContaining = (cand) => [...this.projects.values()].find(({ resource }) =>
561
+ isWithin(resource.root, cand)
562
+ );
563
+ const findScratchRoot = (cand) => this.allowedScratchRoots().find(scratchRoot =>
564
+ isWithin(scratchRoot, cand)
565
+ );
566
+
567
+ let containingProject = findContaining(candidate);
568
+ let containingScratchRoot = containingProject ? null : findScratchRoot(candidate);
569
+
570
+ // Two reasons to try the unescaped variant:
571
+ // (1) candidate is outside every project root (literal "Tarang\ Orca"
572
+ // does not contain a real project), or
573
+ // (2) candidate is inside a root but does not exist on disk because
574
+ // a path segment like "\(kepler\)" only resolves once unescaped.
575
+ // We retry once on the unescaped form before raising.
576
+ const needsRetry = !containingProject ||
577
+ (!allowMissing && !fs.existsSync(candidate));
578
+ if (needsRetry) {
579
+ const unescaped = unescapeShellPath(rawPath);
580
+ if (unescaped !== rawPath) {
581
+ try {
582
+ const altCandidate = buildCandidate(unescaped);
583
+ const altProject = findContaining(altCandidate);
584
+ if (altProject && (allowMissing || fs.existsSync(altCandidate))) {
585
+ candidate = altCandidate;
586
+ containingProject = altProject;
587
+ containingScratchRoot = null;
588
+ } else {
589
+ const altScratchRoot = findScratchRoot(altCandidate);
590
+ if (altScratchRoot && (allowMissing || fs.existsSync(altCandidate))) {
591
+ candidate = altCandidate;
592
+ containingScratchRoot = altScratchRoot;
593
+ }
594
+ }
595
+ } catch { /* fall through to the original error */ }
596
+ }
597
+ }
598
+
599
+ if (!containingProject && !containingScratchRoot) {
600
+ if (allowExternalFileRead) {
601
+ let external = await this.registerFileRead(candidate);
602
+ if (!external) {
603
+ const unescaped = unescapeShellPath(rawPath);
604
+ if (unescaped !== rawPath) {
605
+ try {
606
+ external = await this.registerFileRead(buildCandidate(unescaped));
607
+ } catch { /* keep original outside-root error */ }
608
+ }
609
+ }
610
+ if (external) return external.filePath;
611
+ }
612
+ throw new Error(`Path is outside registered project roots: ${rawPath}`);
613
+ }
614
+ if (!allowMissing && !fs.existsSync(candidate)) {
615
+ throw new Error(`Path not found: ${rawPath}`);
616
+ }
617
+ return candidate;
618
+ }
619
+
620
+ projectForPath(filePath) {
621
+ const normalized = normalizePathInput(filePath);
622
+ const candidate = canonicalizeCandidate(path.resolve(normalized));
623
+ const direct = [...this.projects.values()].find(({ resource }) =>
624
+ isWithin(resource.root, candidate)
625
+ );
626
+ if (direct) return direct;
627
+ // Same unescape fallback used in resolvePath.
628
+ const unescaped = unescapeShellPath(normalized);
629
+ if (unescaped !== normalized) {
630
+ const altCandidate = canonicalizeCandidate(path.resolve(unescaped));
631
+ return [...this.projects.values()].find(({ resource }) =>
632
+ isWithin(resource.root, altCandidate)
633
+ ) || null;
634
+ }
635
+ return null;
636
+ }
637
+
638
+ reset() {
639
+ this.projects.clear();
640
+ }
641
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * ReadMcpResource Tool — read a resource from an MCP server.
3
+ *
4
+ * MCP resources are identified by URI and can contain text, JSON,
5
+ * or binary data. This tool reads text/JSON resources.
6
+ */
7
+
8
+ export const ReadMcpResourceTool = {
9
+ name: 'ReadMcpResource',
10
+ description: 'Read a resource from an MCP server by URI.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ uri: {
15
+ type: 'string',
16
+ description: 'Resource URI (e.g., "file:///path" or "mcp://server/resource")',
17
+ },
18
+ server: {
19
+ type: 'string',
20
+ description: 'MCP server name (if URI does not specify)',
21
+ },
22
+ },
23
+ required: ['uri'],
24
+ },
25
+
26
+ // Set by the MCP integration layer
27
+ _mcpClients: null,
28
+
29
+ validateInput(input) {
30
+ return input.uri ? [] : ['uri is required'];
31
+ },
32
+
33
+ async call(input) {
34
+ if (!this._mcpClients || this._mcpClients.length === 0) {
35
+ return 'No MCP servers connected. Configure MCP servers in settings.';
36
+ }
37
+
38
+ for (const client of this._mcpClients) {
39
+ try {
40
+ const result = await client.readResource(input.uri);
41
+ if (result) {
42
+ if (typeof result === 'string') return result;
43
+ if (result.contents && Array.isArray(result.contents)) {
44
+ return result.contents
45
+ .map(c => c.text || JSON.stringify(c))
46
+ .join('\n');
47
+ }
48
+ return JSON.stringify(result, null, 2);
49
+ }
50
+ } catch {
51
+ // Try next client
52
+ }
53
+ }
54
+
55
+ return `Resource not found: ${input.uri}`;
56
+ },
57
+ };