@bahulam/code 0.1.6 → 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 (178) 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 +9 -4
  21. package/src/core/tool-executor.mjs +105 -29
  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 +231 -53
  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/registry.mjs +4 -0
  51. package/src/ui/icons.mjs +2 -0
  52. package/src/ui/palette.mjs +1 -1
  53. package/src/ui/slash-commands.mjs +4 -6
  54. package/src/ui/tool-card.mjs +7 -3
  55. package/pulse/app/activity/page.tsx +0 -190
  56. package/pulse/app/api/activity/route.ts +0 -138
  57. package/pulse/app/api/benchmark/route.ts +0 -113
  58. package/pulse/app/api/benchmarks/route.ts +0 -195
  59. package/pulse/app/api/costs/route.ts +0 -88
  60. package/pulse/app/api/export/route.ts +0 -77
  61. package/pulse/app/api/history/route.ts +0 -11
  62. package/pulse/app/api/import/route.ts +0 -31
  63. package/pulse/app/api/memory/route.ts +0 -50
  64. package/pulse/app/api/plans/route.ts +0 -9
  65. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  66. package/pulse/app/api/projects/route.ts +0 -121
  67. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  68. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  69. package/pulse/app/api/sessions/route.ts +0 -112
  70. package/pulse/app/api/settings/route.ts +0 -14
  71. package/pulse/app/api/stats/route.ts +0 -143
  72. package/pulse/app/api/todos/route.ts +0 -9
  73. package/pulse/app/api/tools/route.ts +0 -160
  74. package/pulse/app/benchmarks/page.tsx +0 -224
  75. package/pulse/app/costs/page.tsx +0 -179
  76. package/pulse/app/export/page.tsx +0 -465
  77. package/pulse/app/favicon.ico +0 -0
  78. package/pulse/app/globals.css +0 -263
  79. package/pulse/app/help/page.tsx +0 -143
  80. package/pulse/app/history/page.tsx +0 -157
  81. package/pulse/app/layout.tsx +0 -46
  82. package/pulse/app/memory/page.tsx +0 -365
  83. package/pulse/app/overview-client.tsx +0 -393
  84. package/pulse/app/page.tsx +0 -14
  85. package/pulse/app/plans/page.tsx +0 -308
  86. package/pulse/app/projects/[slug]/page.tsx +0 -390
  87. package/pulse/app/projects/page.tsx +0 -110
  88. package/pulse/app/sessions/[id]/page.tsx +0 -243
  89. package/pulse/app/sessions/page.tsx +0 -39
  90. package/pulse/app/settings/page.tsx +0 -188
  91. package/pulse/app/todos/page.tsx +0 -211
  92. package/pulse/app/tools/page.tsx +0 -249
  93. package/pulse/cli.js +0 -164
  94. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  95. package/pulse/components/activity/streak-card.tsx +0 -36
  96. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  97. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  98. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  99. package/pulse/components/costs/model-token-table.tsx +0 -60
  100. package/pulse/components/global-search.tsx +0 -193
  101. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  102. package/pulse/components/layout/bottom-nav.tsx +0 -53
  103. package/pulse/components/layout/client-layout.tsx +0 -31
  104. package/pulse/components/layout/sidebar-context.tsx +0 -50
  105. package/pulse/components/layout/sidebar.tsx +0 -183
  106. package/pulse/components/layout/top-bar.tsx +0 -121
  107. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  108. package/pulse/components/overview/conversation-table.tsx +0 -148
  109. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  110. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  111. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  112. package/pulse/components/overview/stat-card.tsx +0 -102
  113. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  114. package/pulse/components/projects/project-card.tsx +0 -175
  115. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  116. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  117. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  118. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  119. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  120. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  121. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  122. package/pulse/components/sessions/session-badges.tsx +0 -49
  123. package/pulse/components/sessions/session-table.tsx +0 -299
  124. package/pulse/components/theme-provider.tsx +0 -44
  125. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  126. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  127. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  128. package/pulse/components/tools/version-history-table.tsx +0 -32
  129. package/pulse/components/ui/alert.tsx +0 -66
  130. package/pulse/components/ui/badge.tsx +0 -48
  131. package/pulse/components/ui/breadcrumb.tsx +0 -109
  132. package/pulse/components/ui/button.tsx +0 -64
  133. package/pulse/components/ui/calendar.tsx +0 -220
  134. package/pulse/components/ui/card.tsx +0 -92
  135. package/pulse/components/ui/command.tsx +0 -158
  136. package/pulse/components/ui/dialog.tsx +0 -158
  137. package/pulse/components/ui/input.tsx +0 -21
  138. package/pulse/components/ui/popover.tsx +0 -89
  139. package/pulse/components/ui/progress.tsx +0 -31
  140. package/pulse/components/ui/select.tsx +0 -190
  141. package/pulse/components/ui/separator.tsx +0 -28
  142. package/pulse/components/ui/sheet.tsx +0 -143
  143. package/pulse/components/ui/skeleton.tsx +0 -13
  144. package/pulse/components/ui/table.tsx +0 -116
  145. package/pulse/components/ui/tabs.tsx +0 -91
  146. package/pulse/components/ui/tooltip.tsx +0 -57
  147. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  148. package/pulse/components.json +0 -23
  149. package/pulse/eslint.config.mjs +0 -18
  150. package/pulse/lib/bahulam-paths.ts +0 -23
  151. package/pulse/lib/claude-reader.ts +0 -592
  152. package/pulse/lib/decode.ts +0 -129
  153. package/pulse/lib/pricing.ts +0 -102
  154. package/pulse/lib/replay-parser.ts +0 -165
  155. package/pulse/lib/tool-categories.ts +0 -140
  156. package/pulse/lib/utils.ts +0 -6
  157. package/pulse/next-env.d.ts +0 -6
  158. package/pulse/next.config.ts +0 -16
  159. package/pulse/package.json +0 -45
  160. package/pulse/postcss.config.mjs +0 -7
  161. package/pulse/public/activity.png +0 -0
  162. package/pulse/public/cc-lens.png +0 -0
  163. package/pulse/public/command-k.png +0 -0
  164. package/pulse/public/costs.png +0 -0
  165. package/pulse/public/dashboard-dark.png +0 -0
  166. package/pulse/public/dashboard-white.png +0 -0
  167. package/pulse/public/export.png +0 -0
  168. package/pulse/public/file.svg +0 -1
  169. package/pulse/public/globe.svg +0 -1
  170. package/pulse/public/next.svg +0 -1
  171. package/pulse/public/projects.png +0 -0
  172. package/pulse/public/session-chat.png +0 -0
  173. package/pulse/public/todos.png +0 -0
  174. package/pulse/public/tools.png +0 -0
  175. package/pulse/public/vercel.svg +0 -1
  176. package/pulse/public/window.svg +0 -1
  177. package/pulse/tsconfig.json +0 -34
  178. 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
  /**