@bahulam/code 0.1.5 → 0.1.7

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 (179) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +6 -6
  3. package/README.md +10 -17
  4. package/package.json +9 -9
  5. package/src/agents/loader.mjs +1 -1
  6. package/src/agents/workflow_loader.mjs +2 -2
  7. package/src/auth/tarang-auth.mjs +3 -2
  8. package/src/config/model-catalog-default.json +4 -3
  9. package/src/config/model-catalog.mjs +2 -0
  10. package/src/context/prose-chunker.mjs +162 -5
  11. package/src/core/attachments.mjs +65 -3
  12. package/src/core/bundled-runtime.mjs +1 -1
  13. package/src/core/headless.mjs +14 -0
  14. package/src/core/jsonl-writer.mjs +3 -3
  15. package/src/core/lint-resolver.mjs +472 -0
  16. package/src/core/local-agent.mjs +15 -4
  17. package/src/core/local-store.mjs +4 -0
  18. package/src/core/output-filter.mjs +21 -25
  19. package/src/core/pricing.mjs +10 -3
  20. package/src/core/stream-client.mjs +112 -16
  21. package/src/core/tool-executor.mjs +123 -31
  22. package/src/daemon/http-dashboard.mjs +400 -0
  23. package/src/daemon/session-core.mjs +1 -0
  24. package/src/local-service/agent-relay.mjs +868 -0
  25. package/src/local-service/approval-bridge.mjs +222 -0
  26. package/src/local-service/browser.mjs +24 -0
  27. package/src/local-service/command.mjs +155 -0
  28. package/src/local-service/file-access.mjs +393 -0
  29. package/src/local-service/machine.mjs +86 -0
  30. package/src/local-service/paths.mjs +29 -0
  31. package/src/local-service/preview-converters.mjs +260 -0
  32. package/src/local-service/server.mjs +2644 -0
  33. package/src/local-service/session-store.mjs +221 -0
  34. package/src/onboarding/preflight.mjs +1 -1
  35. package/src/terminal/analytics.mjs +2 -6
  36. package/src/terminal/ansi.mjs +11 -3
  37. package/src/terminal/main.mjs +37 -11
  38. package/src/terminal/model-catalog-display.mjs +111 -0
  39. package/src/terminal/repl-format.mjs +4 -1
  40. package/src/terminal/repl-model-form.mjs +81 -14
  41. package/src/terminal/repl-render.mjs +35 -5
  42. package/src/terminal/repl-state.mjs +12 -0
  43. package/src/terminal/repl.mjs +255 -55
  44. package/src/terminal/tool-display.mjs +32 -0
  45. package/src/terminal/watch-state.mjs +118 -0
  46. package/src/tools/analyze-image.mjs +158 -0
  47. package/src/tools/bash.mjs +11 -9
  48. package/src/tools/generate-image.mjs +411 -0
  49. package/src/tools/lint.mjs +24 -43
  50. package/src/tools/project-overview.mjs +5 -2
  51. package/src/tools/registry.mjs +4 -0
  52. package/src/ui/icons.mjs +2 -0
  53. package/src/ui/palette.mjs +1 -1
  54. package/src/ui/slash-commands.mjs +4 -6
  55. package/src/ui/tool-card.mjs +7 -3
  56. package/pulse/app/activity/page.tsx +0 -190
  57. package/pulse/app/api/activity/route.ts +0 -138
  58. package/pulse/app/api/benchmark/route.ts +0 -113
  59. package/pulse/app/api/benchmarks/route.ts +0 -195
  60. package/pulse/app/api/costs/route.ts +0 -88
  61. package/pulse/app/api/export/route.ts +0 -77
  62. package/pulse/app/api/history/route.ts +0 -11
  63. package/pulse/app/api/import/route.ts +0 -31
  64. package/pulse/app/api/memory/route.ts +0 -50
  65. package/pulse/app/api/plans/route.ts +0 -9
  66. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  67. package/pulse/app/api/projects/route.ts +0 -121
  68. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  69. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  70. package/pulse/app/api/sessions/route.ts +0 -112
  71. package/pulse/app/api/settings/route.ts +0 -14
  72. package/pulse/app/api/stats/route.ts +0 -143
  73. package/pulse/app/api/todos/route.ts +0 -9
  74. package/pulse/app/api/tools/route.ts +0 -160
  75. package/pulse/app/benchmarks/page.tsx +0 -224
  76. package/pulse/app/costs/page.tsx +0 -179
  77. package/pulse/app/export/page.tsx +0 -465
  78. package/pulse/app/favicon.ico +0 -0
  79. package/pulse/app/globals.css +0 -263
  80. package/pulse/app/help/page.tsx +0 -143
  81. package/pulse/app/history/page.tsx +0 -157
  82. package/pulse/app/layout.tsx +0 -46
  83. package/pulse/app/memory/page.tsx +0 -365
  84. package/pulse/app/overview-client.tsx +0 -393
  85. package/pulse/app/page.tsx +0 -14
  86. package/pulse/app/plans/page.tsx +0 -308
  87. package/pulse/app/projects/[slug]/page.tsx +0 -390
  88. package/pulse/app/projects/page.tsx +0 -110
  89. package/pulse/app/sessions/[id]/page.tsx +0 -243
  90. package/pulse/app/sessions/page.tsx +0 -39
  91. package/pulse/app/settings/page.tsx +0 -188
  92. package/pulse/app/todos/page.tsx +0 -211
  93. package/pulse/app/tools/page.tsx +0 -249
  94. package/pulse/cli.js +0 -164
  95. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  96. package/pulse/components/activity/streak-card.tsx +0 -36
  97. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  98. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  99. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  100. package/pulse/components/costs/model-token-table.tsx +0 -60
  101. package/pulse/components/global-search.tsx +0 -193
  102. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  103. package/pulse/components/layout/bottom-nav.tsx +0 -53
  104. package/pulse/components/layout/client-layout.tsx +0 -31
  105. package/pulse/components/layout/sidebar-context.tsx +0 -50
  106. package/pulse/components/layout/sidebar.tsx +0 -183
  107. package/pulse/components/layout/top-bar.tsx +0 -121
  108. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  109. package/pulse/components/overview/conversation-table.tsx +0 -148
  110. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  111. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  112. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  113. package/pulse/components/overview/stat-card.tsx +0 -102
  114. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  115. package/pulse/components/projects/project-card.tsx +0 -175
  116. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  117. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  118. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  119. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  120. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  121. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  122. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  123. package/pulse/components/sessions/session-badges.tsx +0 -49
  124. package/pulse/components/sessions/session-table.tsx +0 -299
  125. package/pulse/components/theme-provider.tsx +0 -44
  126. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  127. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  128. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  129. package/pulse/components/tools/version-history-table.tsx +0 -32
  130. package/pulse/components/ui/alert.tsx +0 -66
  131. package/pulse/components/ui/badge.tsx +0 -48
  132. package/pulse/components/ui/breadcrumb.tsx +0 -109
  133. package/pulse/components/ui/button.tsx +0 -64
  134. package/pulse/components/ui/calendar.tsx +0 -220
  135. package/pulse/components/ui/card.tsx +0 -92
  136. package/pulse/components/ui/command.tsx +0 -158
  137. package/pulse/components/ui/dialog.tsx +0 -158
  138. package/pulse/components/ui/input.tsx +0 -21
  139. package/pulse/components/ui/popover.tsx +0 -89
  140. package/pulse/components/ui/progress.tsx +0 -31
  141. package/pulse/components/ui/select.tsx +0 -190
  142. package/pulse/components/ui/separator.tsx +0 -28
  143. package/pulse/components/ui/sheet.tsx +0 -143
  144. package/pulse/components/ui/skeleton.tsx +0 -13
  145. package/pulse/components/ui/table.tsx +0 -116
  146. package/pulse/components/ui/tabs.tsx +0 -91
  147. package/pulse/components/ui/tooltip.tsx +0 -57
  148. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  149. package/pulse/components.json +0 -23
  150. package/pulse/eslint.config.mjs +0 -18
  151. package/pulse/lib/bahulam-paths.ts +0 -23
  152. package/pulse/lib/claude-reader.ts +0 -592
  153. package/pulse/lib/decode.ts +0 -129
  154. package/pulse/lib/pricing.ts +0 -102
  155. package/pulse/lib/replay-parser.ts +0 -165
  156. package/pulse/lib/tool-categories.ts +0 -140
  157. package/pulse/lib/utils.ts +0 -6
  158. package/pulse/next-env.d.ts +0 -6
  159. package/pulse/next.config.ts +0 -16
  160. package/pulse/package.json +0 -45
  161. package/pulse/postcss.config.mjs +0 -7
  162. package/pulse/public/activity.png +0 -0
  163. package/pulse/public/cc-lens.png +0 -0
  164. package/pulse/public/command-k.png +0 -0
  165. package/pulse/public/costs.png +0 -0
  166. package/pulse/public/dashboard-dark.png +0 -0
  167. package/pulse/public/dashboard-white.png +0 -0
  168. package/pulse/public/export.png +0 -0
  169. package/pulse/public/file.svg +0 -1
  170. package/pulse/public/globe.svg +0 -1
  171. package/pulse/public/next.svg +0 -1
  172. package/pulse/public/projects.png +0 -0
  173. package/pulse/public/session-chat.png +0 -0
  174. package/pulse/public/todos.png +0 -0
  175. package/pulse/public/tools.png +0 -0
  176. package/pulse/public/vercel.svg +0 -1
  177. package/pulse/public/window.svg +0 -1
  178. package/pulse/tsconfig.json +0 -34
  179. package/pulse/types/claude.ts +0 -294
@@ -2,7 +2,7 @@
2
2
  * JSONL Writer — writes cc-lens compatible session transcripts to ~/.bahulam/.
3
3
  *
4
4
  * Format mirrors Claude Code's ~/.claude/ JSONL structure so that
5
- * cc-lens (CLAUDE_CONFIG_DIR=~/.bahulam npx cc-lens) can read Kepler sessions.
5
+ * cc-lens (CLAUDE_CONFIG_DIR=~/.bahulam npx cc-lens) can read Bahulam sessions.
6
6
  *
7
7
  * Design:
8
8
  * - Non-blocking: buffered writes, flushed every 500ms or on turn end
@@ -143,8 +143,8 @@ export class JsonlWriter {
143
143
  }
144
144
 
145
145
  /**
146
- * Write a Kepler UI/SSE event for exact-ish terminal replay on /resume.
147
- * This is a Kepler extension; cc-lens readers should ignore unknown types.
146
+ * Write a Bahulam UI/SSE event for exact-ish terminal replay on /resume.
147
+ * This is a Bahulam extension; cc-lens readers should ignore unknown types.
148
148
  */
149
149
  writeKeplerEvent(event) {
150
150
  if (!event || !event.type) return;
@@ -0,0 +1,472 @@
1
+ /**
2
+ * Shared lint command resolver.
3
+ *
4
+ * Explicit linting may run a project's declared lint script. Automatic
5
+ * post-edit linting must stay generated and bounded, so it does not run
6
+ * arbitrary package scripts without a visible tool call.
7
+ */
8
+
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+
12
+ const PYTHON_EXTS = new Set(['.py']);
13
+ const JAVASCRIPT_EXTS = new Set(['.js', '.jsx', '.mjs', '.cjs']);
14
+ const TYPESCRIPT_EXTS = new Set(['.ts', '.tsx', '.mts', '.cts']);
15
+ const MDX_EXTS = new Set(['.mdx']);
16
+ const GO_EXTS = new Set(['.go']);
17
+ const RUST_EXTS = new Set(['.rs']);
18
+
19
+ const IGNORED_DIRS = new Set([
20
+ '.git', 'node_modules', '.next', '.turbo', 'dist', 'build', 'coverage',
21
+ '.venv', 'venv', '__pycache__', 'target',
22
+ ]);
23
+
24
+ const ESLINT_CONFIGS = [
25
+ 'eslint.config.js',
26
+ 'eslint.config.mjs',
27
+ 'eslint.config.cjs',
28
+ '.eslintrc',
29
+ '.eslintrc.js',
30
+ '.eslintrc.cjs',
31
+ '.eslintrc.json',
32
+ '.eslintrc.yaml',
33
+ '.eslintrc.yml',
34
+ ];
35
+
36
+ const BIOME_CONFIGS = ['biome.json', 'biome.jsonc'];
37
+ const RUFF_CONFIGS = ['ruff.toml', '.ruff.toml'];
38
+
39
+ export function shellQuote(value) {
40
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
41
+ }
42
+
43
+ export function resolveLintCommand(targetPath, {
44
+ projectRoot = null,
45
+ projectCommands = {},
46
+ allowProjectScript = true,
47
+ } = {}) {
48
+ const target = path.resolve(targetPath || '.');
49
+ const stat = safeStat(target);
50
+ const isDirectory = Boolean(stat?.isDirectory());
51
+ const baseDir = isDirectory ? target : path.dirname(target);
52
+ const root = path.resolve(projectRoot || inferProjectRoot(baseDir) || baseDir);
53
+ const language = detectTargetLanguage(target, { stat, root });
54
+
55
+ if (allowProjectScript && isPackageLintEligible(language)) {
56
+ const script = resolvePackageLintScript(baseDir, root, projectCommands);
57
+ if (script) {
58
+ return {
59
+ ...script,
60
+ language,
61
+ target,
62
+ scope: 'project',
63
+ };
64
+ }
65
+ }
66
+
67
+ if (language === 'python') {
68
+ return resolvePythonLint(target, { stat, root });
69
+ }
70
+ if (language === 'typescript') {
71
+ return resolveJavaScriptLint(target, { stat, root, typescript: true });
72
+ }
73
+ if (language === 'javascript') {
74
+ return resolveJavaScriptLint(target, { stat, root, typescript: false });
75
+ }
76
+ if (language === 'mdx') {
77
+ return resolveMdxLint(target, { stat, root });
78
+ }
79
+ if (language === 'go') {
80
+ return resolveGoLint(target, { stat, root });
81
+ }
82
+ if (language === 'rust') {
83
+ return resolveRustLint(target, { stat, root });
84
+ }
85
+
86
+ return null;
87
+ }
88
+
89
+ function safeStat(filePath) {
90
+ try {
91
+ return fs.statSync(filePath);
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ function inferProjectRoot(startDir) {
98
+ return findUp(startDir, path.parse(startDir).root, (dir) => (
99
+ fs.existsSync(path.join(dir, 'package.json')) ||
100
+ fs.existsSync(path.join(dir, 'pyproject.toml')) ||
101
+ fs.existsSync(path.join(dir, 'setup.py')) ||
102
+ fs.existsSync(path.join(dir, 'go.mod')) ||
103
+ fs.existsSync(path.join(dir, 'Cargo.toml')) ||
104
+ fs.existsSync(path.join(dir, '.git'))
105
+ ));
106
+ }
107
+
108
+ function isWithin(root, candidate) {
109
+ const rel = path.relative(root, candidate);
110
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
111
+ }
112
+
113
+ function findUp(startDir, stopDir, predicate) {
114
+ let dir = path.resolve(startDir || '.');
115
+ const stop = path.resolve(stopDir || path.parse(dir).root);
116
+ while (isWithin(stop, dir)) {
117
+ if (predicate(dir)) return dir;
118
+ const parent = path.dirname(dir);
119
+ if (parent === dir) break;
120
+ dir = parent;
121
+ }
122
+ return null;
123
+ }
124
+
125
+ function findUpFile(startDir, stopDir, names) {
126
+ const dir = findUp(startDir, stopDir, (candidate) =>
127
+ names.some(name => fs.existsSync(path.join(candidate, name)))
128
+ );
129
+ if (!dir) return null;
130
+ return names.map(name => path.join(dir, name)).find(filePath => fs.existsSync(filePath)) || null;
131
+ }
132
+
133
+ function readJson(filePath) {
134
+ try {
135
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ function readPackageJson(dir) {
142
+ return readJson(path.join(dir, 'package.json'));
143
+ }
144
+
145
+ function packageDirs(baseDir, root) {
146
+ const dirs = [];
147
+ const nearest = findUp(baseDir, root, dir => fs.existsSync(path.join(dir, 'package.json')));
148
+ if (nearest) dirs.push(nearest);
149
+ if (fs.existsSync(path.join(root, 'package.json'))) dirs.push(root);
150
+ return [...new Set(dirs.map(dir => path.resolve(dir)))];
151
+ }
152
+
153
+ function packageDeclares(pkg, names) {
154
+ if (!pkg) return false;
155
+ const buckets = [
156
+ pkg.dependencies,
157
+ pkg.devDependencies,
158
+ pkg.peerDependencies,
159
+ pkg.optionalDependencies,
160
+ ];
161
+ return buckets.some(bucket => names.some(name => bucket?.[name]));
162
+ }
163
+
164
+ function hasNodeTool(baseDir, root, bin, packages) {
165
+ return packageDirs(baseDir, root).some((dir) => {
166
+ const pkg = readPackageJson(dir);
167
+ return fs.existsSync(path.join(dir, 'node_modules', '.bin', bin)) ||
168
+ packageDeclares(pkg, packages);
169
+ });
170
+ }
171
+
172
+ function hasConfig(baseDir, root, names) {
173
+ return Boolean(findUpFile(baseDir, root, names));
174
+ }
175
+
176
+ function resolvePackageLintScript(baseDir, root, projectCommands = {}) {
177
+ for (const dir of packageDirs(baseDir, root)) {
178
+ const pkg = readPackageJson(dir);
179
+ if (pkg?.scripts?.lint) {
180
+ return {
181
+ command: 'npm run lint',
182
+ cwd: dir,
183
+ source: 'package-script',
184
+ reason: `package.json lint script in ${path.relative(root, dir) || '.'}`,
185
+ };
186
+ }
187
+ }
188
+ if (projectCommands?.lint) {
189
+ return {
190
+ command: projectCommands.lint,
191
+ cwd: root,
192
+ source: 'project-command',
193
+ reason: 'registered project lint command',
194
+ };
195
+ }
196
+ return null;
197
+ }
198
+
199
+ function isPackageLintEligible(language) {
200
+ return language === 'javascript' || language === 'typescript' ||
201
+ language === 'mdx' || language === 'mixed-js' || language === 'unknown';
202
+ }
203
+
204
+ function detectTargetLanguage(target, { stat, root }) {
205
+ if (stat?.isDirectory()) {
206
+ return detectDirectoryLanguage(target, root);
207
+ }
208
+ const ext = path.extname(target).toLowerCase();
209
+ if (PYTHON_EXTS.has(ext)) return 'python';
210
+ if (TYPESCRIPT_EXTS.has(ext)) return 'typescript';
211
+ if (JAVASCRIPT_EXTS.has(ext)) return 'javascript';
212
+ if (MDX_EXTS.has(ext)) return 'mdx';
213
+ if (GO_EXTS.has(ext)) return 'go';
214
+ if (RUST_EXTS.has(ext)) return 'rust';
215
+ return 'unknown';
216
+ }
217
+
218
+ function detectDirectoryLanguage(dir, root) {
219
+ if (hasConfig(dir, root, ['tsconfig.json'])) return 'typescript';
220
+ if (fs.existsSync(path.join(dir, 'package.json')) || hasConfig(dir, root, ESLINT_CONFIGS) || hasConfig(dir, root, BIOME_CONFIGS)) {
221
+ return 'javascript';
222
+ }
223
+ if (
224
+ fs.existsSync(path.join(dir, 'pyproject.toml')) ||
225
+ fs.existsSync(path.join(dir, 'setup.py')) ||
226
+ fs.existsSync(path.join(dir, 'requirements.txt'))
227
+ ) {
228
+ return 'python';
229
+ }
230
+ if (hasConfig(dir, root, ['go.mod'])) return 'go';
231
+ if (hasConfig(dir, root, ['Cargo.toml'])) return 'rust';
232
+
233
+ const counts = { python: 0, typescript: 0, javascript: 0, mdx: 0, go: 0, rust: 0 };
234
+ scanDirectory(dir, (filePath) => {
235
+ const ext = path.extname(filePath).toLowerCase();
236
+ if (PYTHON_EXTS.has(ext)) counts.python++;
237
+ else if (TYPESCRIPT_EXTS.has(ext)) counts.typescript++;
238
+ else if (JAVASCRIPT_EXTS.has(ext)) counts.javascript++;
239
+ else if (MDX_EXTS.has(ext)) counts.mdx++;
240
+ else if (GO_EXTS.has(ext)) counts.go++;
241
+ else if (RUST_EXTS.has(ext)) counts.rust++;
242
+ });
243
+ const ranked = Object.entries(counts).sort((a, b) => b[1] - a[1]);
244
+ return ranked[0]?.[1] > 0 ? ranked[0][0] : 'unknown';
245
+ }
246
+
247
+ function scanDirectory(root, visit) {
248
+ const queue = [root];
249
+ let scanned = 0;
250
+ while (queue.length > 0 && scanned < 300) {
251
+ const dir = queue.shift();
252
+ let entries;
253
+ try {
254
+ entries = fs.readdirSync(dir, { withFileTypes: true });
255
+ } catch {
256
+ continue;
257
+ }
258
+ for (const entry of entries) {
259
+ if (entry.name.startsWith('.') || IGNORED_DIRS.has(entry.name)) continue;
260
+ const fullPath = path.join(dir, entry.name);
261
+ if (entry.isDirectory()) {
262
+ queue.push(fullPath);
263
+ } else if (entry.isFile()) {
264
+ scanned++;
265
+ visit(fullPath);
266
+ }
267
+ if (scanned >= 300) break;
268
+ }
269
+ }
270
+ }
271
+
272
+ function shellPathArg(target, cwd) {
273
+ const rel = path.relative(cwd, target);
274
+ const value = rel && !rel.startsWith('..') && !path.isAbsolute(rel) ? rel : target;
275
+ return shellQuote(value || '.');
276
+ }
277
+
278
+ function captured(command) {
279
+ return `${command} 2>&1 || true`;
280
+ }
281
+
282
+ function resolveJavaScriptLint(target, { stat, root, typescript }) {
283
+ const baseDir = stat?.isDirectory() ? target : path.dirname(target);
284
+ const eslintReady = hasConfig(baseDir, root, ESLINT_CONFIGS) ||
285
+ hasNodeTool(baseDir, root, 'eslint', ['eslint']);
286
+ if (eslintReady) {
287
+ const cwd = packageDirs(baseDir, root)[0] || root;
288
+ return {
289
+ command: captured(`npx --no-install eslint ${shellPathArg(target, cwd)}`),
290
+ cwd,
291
+ language: typescript ? 'typescript' : 'javascript',
292
+ target,
293
+ scope: 'file',
294
+ source: 'eslint',
295
+ reason: 'eslint project config or dependency',
296
+ };
297
+ }
298
+
299
+ const biomeReady = hasConfig(baseDir, root, BIOME_CONFIGS) ||
300
+ hasNodeTool(baseDir, root, 'biome', ['@biomejs/biome', 'biome']);
301
+ if (biomeReady) {
302
+ const cwd = packageDirs(baseDir, root)[0] || root;
303
+ return {
304
+ command: captured(`npx --no-install biome check ${shellPathArg(target, cwd)}`),
305
+ cwd,
306
+ language: typescript ? 'typescript' : 'javascript',
307
+ target,
308
+ scope: 'file',
309
+ source: 'biome',
310
+ reason: 'biome project config or dependency',
311
+ };
312
+ }
313
+
314
+ if (typescript) {
315
+ const tsconfig = findUpFile(baseDir, root, ['tsconfig.json']);
316
+ if (!tsconfig) return null;
317
+ const cwd = path.dirname(tsconfig);
318
+ const hasTypescript = hasNodeTool(baseDir, root, 'tsc', ['typescript']);
319
+ if (!hasTypescript) return null;
320
+ return {
321
+ command: captured(`npx --no-install tsc --noEmit --pretty false -p ${shellPathArg(tsconfig, cwd)}`),
322
+ cwd,
323
+ language: 'typescript',
324
+ target,
325
+ scope: 'project',
326
+ source: 'typescript',
327
+ reason: 'tsconfig project check',
328
+ };
329
+ }
330
+
331
+ const ext = path.extname(target).toLowerCase();
332
+ if (!stat?.isDirectory() && ['.js', '.mjs', '.cjs'].includes(ext)) {
333
+ return {
334
+ command: captured(`node --check ${shellPathArg(target, root)}`),
335
+ cwd: root,
336
+ language: 'javascript',
337
+ target,
338
+ scope: 'file',
339
+ source: 'node-check',
340
+ reason: 'javascript syntax check fallback',
341
+ };
342
+ }
343
+
344
+ return null;
345
+ }
346
+
347
+ function resolveMdxLint(target, { stat, root }) {
348
+ const baseDir = stat?.isDirectory() ? target : path.dirname(target);
349
+ const eslintReady = hasConfig(baseDir, root, ESLINT_CONFIGS) ||
350
+ hasNodeTool(baseDir, root, 'eslint', ['eslint', 'eslint-plugin-mdx', 'eslint-mdx', '@mdx-js/eslint-mdx']);
351
+ if (eslintReady) {
352
+ const cwd = packageDirs(baseDir, root)[0] || root;
353
+ return {
354
+ command: captured(`npx --no-install eslint ${shellPathArg(target, cwd)}`),
355
+ cwd,
356
+ language: 'mdx',
357
+ target,
358
+ scope: stat?.isDirectory() ? 'project' : 'file',
359
+ source: 'eslint',
360
+ reason: 'eslint/MDX project config or dependency',
361
+ };
362
+ }
363
+
364
+ const biomeReady = hasConfig(baseDir, root, BIOME_CONFIGS) ||
365
+ hasNodeTool(baseDir, root, 'biome', ['@biomejs/biome', 'biome']);
366
+ if (biomeReady) {
367
+ const cwd = packageDirs(baseDir, root)[0] || root;
368
+ return {
369
+ command: captured(`npx --no-install biome check ${shellPathArg(target, cwd)}`),
370
+ cwd,
371
+ language: 'mdx',
372
+ target,
373
+ scope: stat?.isDirectory() ? 'project' : 'file',
374
+ source: 'biome',
375
+ reason: 'biome project config or dependency',
376
+ };
377
+ }
378
+
379
+ return null;
380
+ }
381
+
382
+ function resolvePythonLint(target, { stat, root }) {
383
+ const baseDir = stat?.isDirectory() ? target : path.dirname(target);
384
+ const pyproject = findUpFile(baseDir, root, ['pyproject.toml']);
385
+ const hasRuffConfig = hasConfig(baseDir, root, RUFF_CONFIGS) ||
386
+ (pyproject && /\[tool\.ruff\b|ruff\b/i.test(readText(pyproject, 12000))) ||
387
+ fileMentions(baseDir, root, ['requirements.txt', 'requirements-dev.txt'], /^ruff(?:[<=>~\s]|$)/im);
388
+
389
+ if (hasRuffConfig) {
390
+ return {
391
+ command: captured(`python3 -m ruff check ${shellPathArg(target, root)}`),
392
+ cwd: root,
393
+ language: 'python',
394
+ target,
395
+ scope: stat?.isDirectory() ? 'project' : 'file',
396
+ source: 'ruff',
397
+ reason: 'ruff project config or dependency',
398
+ };
399
+ }
400
+
401
+ if (!stat?.isDirectory()) {
402
+ return {
403
+ command: captured(`python3 -m py_compile ${shellPathArg(target, root)}`),
404
+ cwd: root,
405
+ language: 'python',
406
+ target,
407
+ scope: 'file',
408
+ source: 'py-compile',
409
+ reason: 'python syntax check fallback',
410
+ };
411
+ }
412
+
413
+ return null;
414
+ }
415
+
416
+ function readText(filePath, maxChars = 8000) {
417
+ try {
418
+ return fs.readFileSync(filePath, 'utf-8').slice(0, maxChars);
419
+ } catch {
420
+ return '';
421
+ }
422
+ }
423
+
424
+ function fileMentions(baseDir, root, names, re) {
425
+ const filePath = findUpFile(baseDir, root, names);
426
+ return filePath ? re.test(readText(filePath, 12000)) : false;
427
+ }
428
+
429
+ function resolveGoLint(target, { stat, root }) {
430
+ const baseDir = stat?.isDirectory() ? target : path.dirname(target);
431
+ const goMod = findUpFile(baseDir, root, ['go.mod']);
432
+ const cwd = goMod ? path.dirname(goMod) : root;
433
+ const arg = stat?.isDirectory() && goMod ? './...' : shellPathArg(target, cwd);
434
+ return {
435
+ command: captured(`go vet ${arg}`),
436
+ cwd,
437
+ language: 'go',
438
+ target,
439
+ scope: stat?.isDirectory() ? 'project' : 'file',
440
+ source: 'go-vet',
441
+ reason: 'go vet',
442
+ };
443
+ }
444
+
445
+ function resolveRustLint(target, { stat, root }) {
446
+ const baseDir = stat?.isDirectory() ? target : path.dirname(target);
447
+ const cargoToml = findUpFile(baseDir, root, ['Cargo.toml']);
448
+ if (cargoToml && stat?.isDirectory()) {
449
+ const cwd = path.dirname(cargoToml);
450
+ return {
451
+ command: captured('cargo clippy --quiet'),
452
+ cwd,
453
+ language: 'rust',
454
+ target,
455
+ scope: 'project',
456
+ source: 'cargo-clippy',
457
+ reason: 'cargo clippy',
458
+ };
459
+ }
460
+ if (!stat?.isDirectory()) {
461
+ return {
462
+ command: captured(`rustfmt --check ${shellPathArg(target, root)}`),
463
+ cwd: root,
464
+ language: 'rust',
465
+ target,
466
+ scope: 'file',
467
+ source: 'rustfmt',
468
+ reason: 'rustfmt check',
469
+ };
470
+ }
471
+ return null;
472
+ }
@@ -167,11 +167,11 @@ const TOOL_SCHEMAS = [
167
167
  },
168
168
  {
169
169
  name: 'lint_check',
170
- description: 'Run linter on a file. Uses ruff for Python, eslint for JS/TS.',
170
+ description: 'Run the project-aware linter or syntax checker for a file or directory.',
171
171
  input_schema: {
172
172
  type: 'object',
173
173
  properties: {
174
- file_path: { type: 'string', description: 'Path to the file to lint' },
174
+ file_path: { type: 'string', description: 'Path to the file or directory to lint' },
175
175
  },
176
176
  required: ['file_path'],
177
177
  },
@@ -303,13 +303,24 @@ export class LocalAgent {
303
303
 
304
304
  // Execute locally
305
305
  let result;
306
+ const toolStart = Date.now();
306
307
  try {
307
308
  result = await this.toolExecutor.execute(name, input || {});
308
309
  } catch (err) {
309
310
  result = { success: false, output: `Error: ${err.message}` };
310
311
  }
311
-
312
- yield { type: 'tool_done', data: { tool: name, duration_ms: 0 } };
312
+ const durationMs = Date.now() - toolStart;
313
+
314
+ yield {
315
+ type: 'tool_done',
316
+ data: {
317
+ ...result,
318
+ call_id: id,
319
+ tool: name,
320
+ args: input || {},
321
+ duration_ms: durationMs,
322
+ },
323
+ };
313
324
 
314
325
  assistantContent.push(block);
315
326
  messages.push({ role: 'assistant', content: assistantContent.slice() });
@@ -191,6 +191,7 @@ async function parseSessionMeta(filePath) {
191
191
  toolCalls: [], // [{name, count}]
192
192
  models: [], // [model strings]
193
193
  modelLimits: {}, // role -> {model, context_length, max_output, source}
194
+ subAgentModels: {}, // role -> model from backend session_info
194
195
  startTime: null,
195
196
  endTime: null,
196
197
  gitBranch: null,
@@ -244,6 +245,9 @@ async function parseSessionMeta(filePath) {
244
245
  if (info.model_limits && typeof info.model_limits === 'object') {
245
246
  meta.modelLimits = info.model_limits;
246
247
  }
248
+ if (info.sub_agent_models && typeof info.sub_agent_models === 'object') {
249
+ meta.subAgentModels = info.sub_agent_models;
250
+ }
247
251
  if (info.models && typeof info.models === 'object') {
248
252
  for (const model of Object.values(info.models)) {
249
253
  if (typeof model === 'string' && model) modelSet.add(model);
@@ -3,9 +3,9 @@
3
3
  * Ported from tarang-cli (Python) ws/executor.py with enhanced patterns.
4
4
  */
5
5
 
6
- import * as path from 'node:path';
7
6
  import * as fs from 'node:fs';
8
7
  import { execSync } from 'node:child_process';
8
+ import { resolveLintCommand } from './lint-resolver.mjs';
9
9
 
10
10
  // ── Command Classification ──────────────────────────────────
11
11
 
@@ -54,6 +54,13 @@ const COMMAND_PROFILES = {
54
54
  ],
55
55
  keepPatterns: [/error/i, /warning/i],
56
56
  },
57
+ git: {
58
+ patterns: [/^\s*git\s+(diff|show|status)\b/i],
59
+ successLimit: 50000,
60
+ failureLimit: 12000,
61
+ noisePatterns: [/^\s*$/],
62
+ keepPatterns: [],
63
+ },
57
64
  run: {
58
65
  patterns: [/python\s/i, /node\s/i, /go run/i, /cargo run/i, /npm start/i, /npm run dev/i],
59
66
  successLimit: 4000,
@@ -142,33 +149,22 @@ export function filterOutput(output, command, success = true) {
142
149
  // ── Auto-Lint ───────────────────────────────────────────────
143
150
 
144
151
  /** Auto-lint a file after write/edit. Returns lint output or null. */
145
- export function autoLint(filePath) {
152
+ export function autoLint(filePath, options = {}) {
146
153
  if (!filePath || !fs.existsSync(filePath)) return null;
147
- const ext = path.extname(filePath);
148
- let cmd;
154
+ const lint = resolveLintCommand(filePath, {
155
+ projectRoot: options.projectRoot || process.cwd(),
156
+ projectCommands: options.projectCommands || {},
157
+ allowProjectScript: false,
158
+ });
159
+ if (!lint?.command) return null;
149
160
 
150
161
  try {
151
- switch (ext) {
152
- case '.py':
153
- cmd = `python3 -m py_compile "${filePath}" 2>&1`;
154
- break;
155
- case '.js': case '.mjs': case '.cjs':
156
- cmd = `node --check "${filePath}" 2>&1`;
157
- break;
158
- case '.ts': case '.tsx':
159
- if (fs.existsSync('node_modules/.bin/tsc'))
160
- cmd = `npx tsc --noEmit --pretty "${filePath}" 2>&1`;
161
- break;
162
- case '.go':
163
- cmd = `go vet "${filePath}" 2>&1`;
164
- break;
165
- case '.rs':
166
- cmd = `rustfmt --check "${filePath}" 2>&1`;
167
- break;
168
- }
169
-
170
- if (!cmd) return null;
171
- const output = execSync(cmd, { stdio: 'pipe', timeout: 15_000, encoding: 'utf-8' });
162
+ const output = execSync(lint.command, {
163
+ cwd: lint.cwd,
164
+ stdio: 'pipe',
165
+ timeout: 15_000,
166
+ encoding: 'utf-8',
167
+ });
172
168
  return output.trim() || null;
173
169
  } catch (err) {
174
170
  const output = (err.stderr || err.stdout || '').toString().trim();
@@ -282,7 +282,7 @@ export function formatCostValue(cost) {
282
282
  }
283
283
 
284
284
  /**
285
- * Convert provider cost to Kepler credits.
285
+ * Convert provider cost to Bahulam credits.
286
286
  * 100 credits = $1 of retail model usage, 2x multiplier.
287
287
  * @param {number} costUsd
288
288
  * @returns {number}
@@ -304,14 +304,21 @@ export function formatCredits(credits) {
304
304
  }
305
305
 
306
306
  /**
307
- * Format a token count for display (e.g. 42100 → '42.1k').
307
+ * Format a token count for display.
308
+ * 42 → '42'
309
+ * 42100 → '42.1k'
310
+ * 1_500_000 → '1.5M'
311
+ * Session-cumulative counts easily cross millions on tool-heavy runs;
312
+ * without the M suffix they render as multi-thousand-'k' strings like
313
+ * '53148.2k' that read as broken.
308
314
  * @param {number} tokens
309
315
  * @returns {string}
310
316
  */
311
317
  export function formatTokens(tokens) {
312
318
  if (tokens === 0) return '0';
313
319
  if (tokens < 1000) return String(tokens);
314
- return `${(tokens / 1000).toFixed(1)}k`;
320
+ if (tokens < 1_000_000) return `${(tokens / 1000).toFixed(1)}k`;
321
+ return `${(tokens / 1_000_000).toFixed(1)}M`;
315
322
  }
316
323
 
317
324
  /**