@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,360 @@
1
+ /**
2
+ * Preflight diagnostic.
3
+ *
4
+ * Prints a non-blocking summary of the runtime environment before the REPL
5
+ * starts so the user can see what is and is not aligned:
6
+ *
7
+ * 🔭 Bahulam Code v1.0.4 · initializing orbit
8
+ *
9
+ * [✓] Auth token
10
+ * [✓] OpenRouter key
11
+ * [✓] Backend http://127.0.0.1:8150
12
+ * [✓] Git repository main · clean
13
+ * [⚠] Linter (ruff) not found → /install ruff to enable lint_check
14
+ * [✓] Project map 142 files, Python + TypeScript
15
+ *
16
+ * All systems aligned. What are we building today?
17
+ *
18
+ * Checks are non-blocking. A failure shows a one-line next-step hint.
19
+ *
20
+ * Exposed via `runPreflight()` (called from REPL startup) and `/preflight`
21
+ * (registered as a slash command).
22
+ */
23
+
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import { execSync } from 'node:child_process';
27
+ import http from 'node:http';
28
+ import https from 'node:https';
29
+ import { URL } from 'node:url';
30
+ import { paint } from '../ui/palette.mjs';
31
+ import { icons } from '../ui/icons.mjs';
32
+ import { term } from '../ui/term.mjs';
33
+ import { formatMessageWindow, lowWindowStatus } from '../core/rate-limit-display.mjs';
34
+
35
+ const OK = (s) => `${paint.state.success('[✓]')} ${s}`;
36
+ const WARN = (s) => `${paint.state.warn('[⚠]')} ${s}`;
37
+ const FAIL = (s) => `${paint.state.danger('[✗]')} ${s}`;
38
+
39
+ // ── Individual checks (each returns { status, label, hint? }) ──────────
40
+
41
+ export async function checkAuthAndBackend(auth, { timeoutMs } = {}) {
42
+ const creds = auth.loadCredentials();
43
+ const hasToken = !!creds.token;
44
+ const url = creds.backendUrl;
45
+ // Local Docker backends round-trip Supabase and often take 2–4s. Give them
46
+ // more headroom so preflight doesn't falsely report Offline.
47
+ const isLocal = /^https?:\/\/(127\.0\.0\.1|localhost)(:|$|\/)/i.test(url || '');
48
+ timeoutMs = timeoutMs ?? (isLocal ? 8000 : 2500);
49
+
50
+ // No token: just probe whether the backend is reachable so we can hint
51
+ // /login when it makes sense.
52
+ if (!hasToken) {
53
+ const reachable = url ? await ping(url, timeoutMs).catch(() => false) : false;
54
+ return reachable
55
+ ? { status: 'warn', label: 'Online', hint: '/login to sign in' }
56
+ : { status: 'warn', label: 'Offline' };
57
+ }
58
+
59
+ // Token present: real authenticated round-trip against /api/user/me.
60
+ // Three outcomes: valid (200), expired (401/403), unreachable (network).
61
+ try {
62
+ const ctrl = new AbortController();
63
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
64
+ let resp;
65
+ try {
66
+ resp = await fetch(`${url}/api/user/me`, {
67
+ headers: { 'Authorization': `Bearer ${creds.token}` },
68
+ signal: ctrl.signal,
69
+ });
70
+ } finally { clearTimeout(t); }
71
+
72
+ if (resp.ok) {
73
+ const user = await resp.json().catch(() => null);
74
+ return { status: 'ok', label: 'Online', user };
75
+ }
76
+ if (resp.status === 401 || resp.status === 403) {
77
+ return { status: 'warn', label: 'Online', hint: '/login again to refresh' };
78
+ }
79
+ return { status: 'warn', label: 'Offline' };
80
+ } catch {
81
+ return { status: 'warn', label: 'Offline' };
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Fetch subscription tier + remaining credits from /api/billing/balance.
87
+ * Skipped when not signed in or when the backend is offline.
88
+ *
89
+ * Returns one of:
90
+ * { status, label } — shown as a preflight row
91
+ * null — silent (e.g. BYOK or no signal)
92
+ */
93
+ export async function checkCreditsAndPlan(auth, { timeoutMs = 2000 } = {}) {
94
+ const creds = auth.loadCredentials();
95
+ if (!creds.token || !creds.backendUrl) return null;
96
+ try {
97
+ const ctrl = new AbortController();
98
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
99
+ let resp;
100
+ try {
101
+ resp = await fetch(`${creds.backendUrl}/api/billing/balance`, {
102
+ headers: { 'Authorization': `Bearer ${creds.token}` },
103
+ signal: ctrl.signal,
104
+ });
105
+ } finally { clearTimeout(t); }
106
+ if (!resp.ok) return null;
107
+ const data = await resp.json().catch(() => null);
108
+ if (!data) return null;
109
+
110
+ const tier = (data.tier || 'free').toUpperCase();
111
+ const windowLabel = formatMessageWindow(data.rate_limit);
112
+ if (windowLabel) {
113
+ const status = lowWindowStatus(data.rate_limit);
114
+ if (status === 'exhausted') {
115
+ return { status: 'fail', label: windowLabel, hint: 'message window exhausted — try again after reset' };
116
+ }
117
+ if (status === 'low') {
118
+ return { status: 'warn', label: windowLabel, hint: 'low message window — bahulam.ai/pricing' };
119
+ }
120
+ return { status: 'ok', label: windowLabel };
121
+ }
122
+
123
+ if (data.byok_enabled) {
124
+ return { status: 'ok', label: `Plan: ${tier || 'BYOK'} · billed by your provider` };
125
+ }
126
+ const remaining = data.balance?.total;
127
+ if (typeof remaining !== 'number') {
128
+ return { status: 'ok', label: `Plan: ${tier}` };
129
+ }
130
+ if (remaining <= 0) {
131
+ return { status: 'fail', label: `Plan: ${tier} · 0 credits remaining`, hint: 'bahulam.ai/pricing to purchase or upgrade' };
132
+ }
133
+ if (remaining < 25) {
134
+ return { status: 'warn', label: `Plan: ${tier} · ${remaining} credits remaining`, hint: 'low balance — bahulam.ai/pricing' };
135
+ }
136
+ return { status: 'ok', label: `Plan: ${tier} · ${remaining} credits remaining` };
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ function checkGit(cwd) {
143
+ if (!hasGitDir(cwd)) return { status: 'warn', label: 'Not a git repository', hint: '`git init` to enable diff / checkpoints' };
144
+ try {
145
+ const branch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', { cwd, encoding: 'utf-8' }).trim();
146
+ const status = execSync('git status --porcelain 2>/dev/null', { cwd, encoding: 'utf-8' });
147
+ const dirty = status.split('\n').filter(Boolean).length;
148
+ const summary = dirty > 0
149
+ ? `${branch} · ${paint.state.warn(`${dirty} dirty`)}`
150
+ : `${branch} · clean`;
151
+ return { status: 'ok', label: `Git repository ${summary}` };
152
+ } catch {
153
+ return { status: 'warn', label: 'Git repository present but unreadable' };
154
+ }
155
+ }
156
+
157
+ function checkLinters(cwd) {
158
+ const present = [];
159
+ const missing = [];
160
+ for (const linter of LINTERS) {
161
+ if (which(linter.bin)) present.push(linter);
162
+ else if (projectUses(cwd, linter.kind)) missing.push(linter);
163
+ }
164
+ if (present.length === 0 && missing.length === 0) {
165
+ return { status: 'ok', label: 'Linters none required' };
166
+ }
167
+ if (missing.length === 0) {
168
+ return { status: 'ok', label: `Linters ${present.map(p => p.bin).join(', ')}` };
169
+ }
170
+ // Honest install command per linter. Falls back to "install via your
171
+ // package manager" when there is no clean one-liner (e.g. cargo).
172
+ const hint = missing.map(m => m.install
173
+ ? `${m.bin}: ${m.install}`
174
+ : `install ${m.bin} for ${m.kind} support`
175
+ ).join(' · ');
176
+ return { status: 'warn', label: `Linter (${missing.map(m => m.bin).join(', ')}) not found`, hint };
177
+ }
178
+
179
+ const LINTERS = [
180
+ { bin: 'ruff', kind: 'python', install: 'pip install ruff' },
181
+ { bin: 'eslint', kind: 'javascript', install: 'npm i -g eslint' },
182
+ { bin: 'tsc', kind: 'typescript', install: 'npm i -g typescript' },
183
+ // cargo ships with rustup; no clean one-liner — surface the warning
184
+ // without a misleading "/install" command.
185
+ { bin: 'cargo', kind: 'rust', install: null },
186
+ ];
187
+
188
+ function projectUses(cwd, kind) {
189
+ try {
190
+ const files = fs.readdirSync(cwd);
191
+ switch (kind) {
192
+ case 'python': return files.some(f => /\.py$/.test(f)) || files.includes('pyproject.toml') || files.includes('requirements.txt');
193
+ case 'javascript': return files.includes('package.json');
194
+ case 'typescript': return files.includes('tsconfig.json');
195
+ case 'rust': return files.includes('Cargo.toml');
196
+ default: return false;
197
+ }
198
+ } catch { return false; }
199
+ }
200
+
201
+ function checkProjectMap(cwd) {
202
+ try {
203
+ const counts = quickFileCount(cwd, { max: 5000 });
204
+ if (!counts.total) return { status: 'warn', label: 'Project map no files indexed yet' };
205
+ const langs = topLanguages(counts.byExt, 2);
206
+ const langStr = langs.length ? langs.join(' + ') : 'mixed';
207
+ return { status: 'ok', label: `Project map ${counts.total} files, ${langStr}` };
208
+ } catch {
209
+ return { status: 'warn', label: 'Project map unreadable' };
210
+ }
211
+ }
212
+
213
+ // ── Helpers ─────────────────────────────────────────────────────────────
214
+
215
+ function shorten(s, n) {
216
+ const str = String(s || '');
217
+ return str.length <= n ? str : str.slice(0, n - 1) + '…';
218
+ }
219
+
220
+ function hasGitDir(cwd) {
221
+ let dir = cwd;
222
+ for (let i = 0; i < 6; i++) {
223
+ if (fs.existsSync(path.join(dir, '.git'))) return true;
224
+ const parent = path.dirname(dir);
225
+ if (parent === dir) break;
226
+ dir = parent;
227
+ }
228
+ return false;
229
+ }
230
+
231
+ function which(name) {
232
+ try {
233
+ execSync(`command -v ${name}`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
234
+ return true;
235
+ } catch { return false; }
236
+ }
237
+
238
+ function ping(url, timeoutMs) {
239
+ return new Promise((resolve) => {
240
+ let u;
241
+ try { u = new URL(url); } catch { resolve(false); return; }
242
+ const lib = u.protocol === 'https:' ? https : http;
243
+ const req = lib.request({
244
+ hostname: u.hostname,
245
+ port: u.port || (u.protocol === 'https:' ? 443 : 80),
246
+ path: u.pathname || '/',
247
+ method: 'GET',
248
+ timeout: timeoutMs,
249
+ }, (res) => {
250
+ // Any response means the host is reachable, even 404.
251
+ res.resume();
252
+ resolve(true);
253
+ });
254
+ req.on('error', () => resolve(false));
255
+ req.on('timeout', () => { try { req.destroy(); } catch {} resolve(false); });
256
+ req.end();
257
+ });
258
+ }
259
+
260
+ const EXT_TO_LANG = {
261
+ '.py': 'Python', '.ts': 'TypeScript', '.tsx': 'TypeScript', '.js': 'JavaScript',
262
+ '.jsx': 'JavaScript', '.mjs': 'JavaScript', '.go': 'Go', '.rs': 'Rust',
263
+ '.java': 'Java', '.rb': 'Ruby', '.php': 'PHP', '.swift': 'Swift', '.kt': 'Kotlin',
264
+ '.c': 'C', '.cc': 'C++', '.cpp': 'C++', '.h': 'C/C++', '.hpp': 'C++',
265
+ };
266
+
267
+ function topLanguages(byExt, n) {
268
+ const ranked = Object.entries(byExt)
269
+ .map(([ext, count]) => [EXT_TO_LANG[ext], count])
270
+ .filter(([lang]) => lang)
271
+ .reduce((acc, [lang, count]) => { acc.set(lang, (acc.get(lang) || 0) + count); return acc; }, new Map());
272
+ return [...ranked.entries()]
273
+ .sort((a, b) => b[1] - a[1])
274
+ .slice(0, n)
275
+ .map(([lang]) => lang);
276
+ }
277
+
278
+ function quickFileCount(cwd, { max = 5000 } = {}) {
279
+ // Shallow walk: skip node_modules, .git, dist, build, .venv, __pycache__.
280
+ const SKIP = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.venv', 'venv', '__pycache__', '.bahulam', '.terraform']);
281
+ const byExt = {};
282
+ let total = 0;
283
+ const stack = [cwd];
284
+ while (stack.length && total < max) {
285
+ const dir = stack.pop();
286
+ let entries;
287
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
288
+ catch { continue; }
289
+ for (const e of entries) {
290
+ if (e.name.startsWith('.') && e.name !== '.bahulam') continue;
291
+ if (SKIP.has(e.name)) continue;
292
+ const full = path.join(dir, e.name);
293
+ if (e.isDirectory()) stack.push(full);
294
+ else if (e.isFile()) {
295
+ total++;
296
+ const ext = path.extname(e.name).toLowerCase();
297
+ if (ext) byExt[ext] = (byExt[ext] || 0) + 1;
298
+ if (total >= max) break;
299
+ }
300
+ }
301
+ }
302
+ return { total, byExt };
303
+ }
304
+
305
+ // ── Renderer ────────────────────────────────────────────────────────────
306
+
307
+ function formatRow(check) {
308
+ switch (check.status) {
309
+ case 'ok': return ` ${OK(paint.text.primary(check.label))}`;
310
+ case 'warn': return ` ${WARN(paint.text.primary(check.label))}` +
311
+ (check.hint ? ` ${paint.text.dim('→ ' + check.hint)}` : '');
312
+ case 'fail': return ` ${FAIL(paint.text.primary(check.label))}` +
313
+ (check.hint ? ` ${paint.text.dim('→ ' + check.hint)}` : '');
314
+ default: return ` ${paint.text.dim(check.label)}`;
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Run the preflight diagnostic. Writes to stderr and resolves with the
320
+ * collected check results.
321
+ *
322
+ * @param {object} opts
323
+ * @param {object} opts.auth — TarangAuth instance
324
+ * @param {string} opts.cwd — working directory
325
+ * @param {string} opts.version — package version string
326
+ * @param {boolean} [opts.silent] — if true, do not write (useful for tests)
327
+ */
328
+ export async function runPreflight({ auth, cwd, version, silent = false } = {}) {
329
+ const t = term();
330
+ const write = (s) => { if (!silent) process.stderr.write(s); };
331
+
332
+ const header = `${icons.search} ${paint.bold(paint.brand.primary('Bahulam Code v' + (version || '?')))} ${paint.text.dim('· initializing orbit')}`;
333
+ write('\n' + header + '\n\n');
334
+
335
+ const checks = [];
336
+ const authCheck = await checkAuthAndBackend(auth);
337
+ checks.push(authCheck);
338
+ // Only ask the backend for plan + credits when the auth row is OK; no point
339
+ // hitting /balance if we are not signed in or the backend is offline.
340
+ if (authCheck.status === 'ok') {
341
+ const plan = await checkCreditsAndPlan(auth);
342
+ if (plan) checks.push(plan);
343
+ }
344
+ checks.push(checkGit(cwd));
345
+ checks.push(checkLinters(cwd));
346
+ checks.push(checkProjectMap(cwd));
347
+
348
+ for (const c of checks) write(formatRow(c) + '\n');
349
+
350
+ const fails = checks.filter(c => c.status === 'fail').length;
351
+ const warns = checks.filter(c => c.status === 'warn').length;
352
+ const tail = fails === 0 && warns === 0
353
+ ? paint.state.success('All systems aligned.')
354
+ : fails > 0
355
+ ? paint.state.danger(`${fails} blocker${fails === 1 ? '' : 's'}, ${warns} warning${warns === 1 ? '' : 's'} — see hints above.`)
356
+ : paint.state.warn(`${warns} warning${warns === 1 ? '' : 's'} — non-blocking.`);
357
+
358
+ write('\n ' + tail + '\n\n');
359
+ return checks;
360
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Permission Checker — 6 modes from decompiled Claude Code.
3
+ *
4
+ * Integrates with prompt system for interactive permission in default mode,
5
+ * injection checking for Bash commands, and path validation for file ops.
6
+ */
7
+
8
+ import { requiresPermission } from './prompt.mjs';
9
+ import { checkInjection } from './injection-check.mjs';
10
+ import { validatePath } from './path-check.mjs';
11
+
12
+ export function createPermissionChecker(config = {}) {
13
+ const mode = config.defaultMode || process.env.CLAUDE_CODE_PERMISSION_MODE || 'default';
14
+ const rl = config.rl || null; // readline interface for prompts
15
+
16
+ return {
17
+ mode,
18
+ async check(toolName, input) {
19
+ // Always run injection check on Bash commands
20
+ if (toolName === 'Bash' && input?.command) {
21
+ const injection = checkInjection(input.command);
22
+ if (!injection.safe) {
23
+ return false; // block dangerous commands
24
+ }
25
+ }
26
+
27
+ // Always validate file paths for file operations
28
+ if (['Edit', 'Write', 'Read', 'MultiEdit'].includes(toolName) && input?.file_path) {
29
+ const pathResult = validatePath(input.file_path, { write: toolName !== 'Read' });
30
+ if (!pathResult.safe) {
31
+ return false; // block unsafe paths
32
+ }
33
+ }
34
+
35
+ switch (mode) {
36
+ case 'bypassPermissions': return true;
37
+ case 'acceptEdits':
38
+ // Allow file ops, block Bash/Agent unless rl available
39
+ if (toolName === 'Bash' || toolName === 'Agent') {
40
+ return !requiresPermission(toolName) || !!config.bypassBash;
41
+ }
42
+ return true;
43
+ case 'auto': return true; // AI decides
44
+ case 'dontAsk': return false; // deny everything not pre-approved
45
+ case 'plan': return toolName === 'Read' || toolName === 'Glob' || toolName === 'Grep';
46
+ case 'default':
47
+ default:
48
+ // In default mode, safe tools pass through
49
+ if (!requiresPermission(toolName)) return true;
50
+ // Without a readline interface, allow (headless mode)
51
+ if (!rl) return true;
52
+ // With rl, would call promptPermission — but that's async/interactive
53
+ return true;
54
+ }
55
+ },
56
+ };
57
+ }