@bahulam/code 0.1.6 → 0.1.8

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 (204) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +6 -6
  3. package/README.md +11 -18
  4. package/package.json +11 -11
  5. package/src/agents/loader.mjs +1 -1
  6. package/src/agents/workflow_loader.mjs +2 -2
  7. package/src/auth/{tarang-auth.mjs → bahulam-auth.mjs} +4 -3
  8. package/src/commands/agent.mjs +3 -3
  9. package/src/commands/device.mjs +2 -2
  10. package/src/commands/pair.mjs +2 -2
  11. package/src/commands/remote.mjs +3 -3
  12. package/src/commands/workflow.mjs +5 -5
  13. package/src/config/env.mjs +9 -2
  14. package/src/config/memory-loader.mjs +1 -1
  15. package/src/config/model-catalog-default.json +4 -3
  16. package/src/config/model-catalog.mjs +2 -0
  17. package/src/config/settings.mjs +2 -2
  18. package/src/context/prose-chunker.mjs +162 -5
  19. package/src/core/agent-loop.mjs +17 -0
  20. package/src/core/attachments.mjs +66 -4
  21. package/src/core/backend-url.mjs +11 -12
  22. package/src/core/bundled-runtime.mjs +1 -1
  23. package/src/core/callback-client.mjs +1 -1
  24. package/src/core/headless.mjs +18 -4
  25. package/src/core/jsonl-writer.mjs +18 -18
  26. package/src/core/lint-resolver.mjs +472 -0
  27. package/src/core/local-agent.mjs +40 -8
  28. package/src/core/local-store.mjs +5 -1
  29. package/src/core/mode-selector.mjs +1 -1
  30. package/src/core/output-filter.mjs +22 -26
  31. package/src/core/pricing.mjs +10 -3
  32. package/src/core/project-artifacts.mjs +3 -3
  33. package/src/core/project-context-loader.mjs +9 -9
  34. package/src/core/settings-sync.mjs +1 -1
  35. package/src/core/stagnation.mjs +61 -1
  36. package/src/core/stream-client.mjs +13 -8
  37. package/src/core/system-prompt.mjs +2 -2
  38. package/src/core/tool-executor.mjs +178 -84
  39. package/src/daemon/http-dashboard.mjs +400 -0
  40. package/src/daemon/session-core.mjs +1 -0
  41. package/src/local-service/agent-relay.mjs +868 -0
  42. package/src/local-service/approval-bridge.mjs +222 -0
  43. package/src/local-service/browser.mjs +24 -0
  44. package/src/local-service/command.mjs +155 -0
  45. package/src/local-service/file-access.mjs +393 -0
  46. package/src/local-service/machine.mjs +86 -0
  47. package/src/local-service/paths.mjs +29 -0
  48. package/src/local-service/preview-converters.mjs +260 -0
  49. package/src/local-service/server.mjs +2644 -0
  50. package/src/local-service/session-store.mjs +221 -0
  51. package/src/mcp/client.mjs +4 -4
  52. package/src/onboarding/preflight.mjs +2 -2
  53. package/src/terminal/agents.mjs +2 -2
  54. package/src/terminal/analytics.mjs +2 -6
  55. package/src/terminal/ansi.mjs +11 -3
  56. package/src/terminal/main.mjs +43 -17
  57. package/src/terminal/model-catalog-display.mjs +111 -0
  58. package/src/terminal/repl-format.mjs +4 -1
  59. package/src/terminal/repl-model-form.mjs +81 -14
  60. package/src/terminal/repl-render.mjs +37 -7
  61. package/src/terminal/repl-resume.mjs +2 -2
  62. package/src/terminal/repl-state.mjs +12 -0
  63. package/src/terminal/repl.mjs +248 -70
  64. package/src/terminal/tool-display.mjs +32 -0
  65. package/src/terminal/watch-state.mjs +118 -0
  66. package/src/tools/agent.mjs +2 -2
  67. package/src/tools/analyze-image.mjs +158 -0
  68. package/src/tools/bash.mjs +11 -9
  69. package/src/tools/generate-image.mjs +411 -0
  70. package/src/tools/lint.mjs +24 -43
  71. package/src/tools/project-overview.mjs +12 -12
  72. package/src/tools/registry.mjs +4 -0
  73. package/src/ui/formatter.mjs +2 -2
  74. package/src/ui/icons.mjs +2 -0
  75. package/src/ui/input-dock.mjs +7 -7
  76. package/src/ui/palette.mjs +1 -1
  77. package/src/ui/slash-commands.mjs +4 -6
  78. package/src/ui/spinner.mjs +1 -1
  79. package/src/ui/term.mjs +2 -6
  80. package/src/ui/tool-card.mjs +81 -6
  81. package/pulse/app/activity/page.tsx +0 -190
  82. package/pulse/app/api/activity/route.ts +0 -138
  83. package/pulse/app/api/benchmark/route.ts +0 -113
  84. package/pulse/app/api/benchmarks/route.ts +0 -195
  85. package/pulse/app/api/costs/route.ts +0 -88
  86. package/pulse/app/api/export/route.ts +0 -77
  87. package/pulse/app/api/history/route.ts +0 -11
  88. package/pulse/app/api/import/route.ts +0 -31
  89. package/pulse/app/api/memory/route.ts +0 -50
  90. package/pulse/app/api/plans/route.ts +0 -9
  91. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  92. package/pulse/app/api/projects/route.ts +0 -121
  93. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  94. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  95. package/pulse/app/api/sessions/route.ts +0 -112
  96. package/pulse/app/api/settings/route.ts +0 -14
  97. package/pulse/app/api/stats/route.ts +0 -143
  98. package/pulse/app/api/todos/route.ts +0 -9
  99. package/pulse/app/api/tools/route.ts +0 -160
  100. package/pulse/app/benchmarks/page.tsx +0 -224
  101. package/pulse/app/costs/page.tsx +0 -179
  102. package/pulse/app/export/page.tsx +0 -465
  103. package/pulse/app/favicon.ico +0 -0
  104. package/pulse/app/globals.css +0 -263
  105. package/pulse/app/help/page.tsx +0 -143
  106. package/pulse/app/history/page.tsx +0 -157
  107. package/pulse/app/layout.tsx +0 -46
  108. package/pulse/app/memory/page.tsx +0 -365
  109. package/pulse/app/overview-client.tsx +0 -393
  110. package/pulse/app/page.tsx +0 -14
  111. package/pulse/app/plans/page.tsx +0 -308
  112. package/pulse/app/projects/[slug]/page.tsx +0 -390
  113. package/pulse/app/projects/page.tsx +0 -110
  114. package/pulse/app/sessions/[id]/page.tsx +0 -243
  115. package/pulse/app/sessions/page.tsx +0 -39
  116. package/pulse/app/settings/page.tsx +0 -188
  117. package/pulse/app/todos/page.tsx +0 -211
  118. package/pulse/app/tools/page.tsx +0 -249
  119. package/pulse/cli.js +0 -164
  120. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  121. package/pulse/components/activity/streak-card.tsx +0 -36
  122. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  123. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  124. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  125. package/pulse/components/costs/model-token-table.tsx +0 -60
  126. package/pulse/components/global-search.tsx +0 -193
  127. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  128. package/pulse/components/layout/bottom-nav.tsx +0 -53
  129. package/pulse/components/layout/client-layout.tsx +0 -31
  130. package/pulse/components/layout/sidebar-context.tsx +0 -50
  131. package/pulse/components/layout/sidebar.tsx +0 -183
  132. package/pulse/components/layout/top-bar.tsx +0 -121
  133. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  134. package/pulse/components/overview/conversation-table.tsx +0 -148
  135. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  136. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  137. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  138. package/pulse/components/overview/stat-card.tsx +0 -102
  139. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  140. package/pulse/components/projects/project-card.tsx +0 -175
  141. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  142. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  143. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  144. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  145. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  146. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  147. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  148. package/pulse/components/sessions/session-badges.tsx +0 -49
  149. package/pulse/components/sessions/session-table.tsx +0 -299
  150. package/pulse/components/theme-provider.tsx +0 -44
  151. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  152. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  153. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  154. package/pulse/components/tools/version-history-table.tsx +0 -32
  155. package/pulse/components/ui/alert.tsx +0 -66
  156. package/pulse/components/ui/badge.tsx +0 -48
  157. package/pulse/components/ui/breadcrumb.tsx +0 -109
  158. package/pulse/components/ui/button.tsx +0 -64
  159. package/pulse/components/ui/calendar.tsx +0 -220
  160. package/pulse/components/ui/card.tsx +0 -92
  161. package/pulse/components/ui/command.tsx +0 -158
  162. package/pulse/components/ui/dialog.tsx +0 -158
  163. package/pulse/components/ui/input.tsx +0 -21
  164. package/pulse/components/ui/popover.tsx +0 -89
  165. package/pulse/components/ui/progress.tsx +0 -31
  166. package/pulse/components/ui/select.tsx +0 -190
  167. package/pulse/components/ui/separator.tsx +0 -28
  168. package/pulse/components/ui/sheet.tsx +0 -143
  169. package/pulse/components/ui/skeleton.tsx +0 -13
  170. package/pulse/components/ui/table.tsx +0 -116
  171. package/pulse/components/ui/tabs.tsx +0 -91
  172. package/pulse/components/ui/tooltip.tsx +0 -57
  173. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  174. package/pulse/components.json +0 -23
  175. package/pulse/eslint.config.mjs +0 -18
  176. package/pulse/lib/bahulam-paths.ts +0 -23
  177. package/pulse/lib/claude-reader.ts +0 -592
  178. package/pulse/lib/decode.ts +0 -129
  179. package/pulse/lib/pricing.ts +0 -102
  180. package/pulse/lib/replay-parser.ts +0 -165
  181. package/pulse/lib/tool-categories.ts +0 -140
  182. package/pulse/lib/utils.ts +0 -6
  183. package/pulse/next-env.d.ts +0 -6
  184. package/pulse/next.config.ts +0 -16
  185. package/pulse/package.json +0 -45
  186. package/pulse/postcss.config.mjs +0 -7
  187. package/pulse/public/activity.png +0 -0
  188. package/pulse/public/cc-lens.png +0 -0
  189. package/pulse/public/command-k.png +0 -0
  190. package/pulse/public/costs.png +0 -0
  191. package/pulse/public/dashboard-dark.png +0 -0
  192. package/pulse/public/dashboard-white.png +0 -0
  193. package/pulse/public/export.png +0 -0
  194. package/pulse/public/file.svg +0 -1
  195. package/pulse/public/globe.svg +0 -1
  196. package/pulse/public/next.svg +0 -1
  197. package/pulse/public/projects.png +0 -0
  198. package/pulse/public/session-chat.png +0 -0
  199. package/pulse/public/todos.png +0 -0
  200. package/pulse/public/tools.png +0 -0
  201. package/pulse/public/vercel.svg +0 -1
  202. package/pulse/public/window.svg +0 -1
  203. package/pulse/tsconfig.json +0 -34
  204. package/pulse/types/claude.ts +0 -294
@@ -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
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Local Agent — T18: Direct LLM API calls, <100ms startup, offline.
3
3
  * Replaces the SSE backend for --local mode.
4
- * Yields events matching the same format as TarangStreamClient.
4
+ * Yields events matching the same format as BahulamStreamClient.
5
5
  */
6
6
 
7
7
  import { ContextRetriever } from '../context/retriever.mjs';
@@ -58,15 +58,16 @@ const TOOL_SCHEMAS = [
58
58
  },
59
59
  {
60
60
  name: 'edit_file',
61
- description: 'Search for a string in a file and replace it. The old_string must match exactly (including whitespace).',
61
+ description: 'Search for a string in a file and replace it. The search string must match exactly, including whitespace.',
62
62
  input_schema: {
63
63
  type: 'object',
64
64
  properties: {
65
65
  file_path: { type: 'string', description: 'Path to the file' },
66
- old_string: { type: 'string', description: 'Exact string to find in the file' },
67
- new_string: { type: 'string', description: 'Replacement string' },
66
+ search: { type: 'string', description: 'Exact string to find in the file' },
67
+ replace: { type: 'string', description: 'Replacement string' },
68
+ replace_all: { type: 'boolean', description: 'Replace every occurrence instead of only the first match' },
68
69
  },
69
- required: ['file_path', 'old_string', 'new_string'],
70
+ required: ['file_path', 'search', 'replace'],
70
71
  },
71
72
  },
72
73
  {
@@ -167,11 +168,11 @@ const TOOL_SCHEMAS = [
167
168
  },
168
169
  {
169
170
  name: 'lint_check',
170
- description: 'Run linter on a file. Uses ruff for Python, eslint for JS/TS.',
171
+ description: 'Run the project-aware linter or syntax checker for a file or directory.',
171
172
  input_schema: {
172
173
  type: 'object',
173
174
  properties: {
174
- file_path: { type: 'string', description: 'Path to the file to lint' },
175
+ file_path: { type: 'string', description: 'Path to the file or directory to lint' },
175
176
  },
176
177
  required: ['file_path'],
177
178
  },
@@ -303,13 +304,44 @@ export class LocalAgent {
303
304
 
304
305
  // Execute locally
305
306
  let result;
307
+ const toolStart = Date.now();
306
308
  try {
307
309
  result = await this.toolExecutor.execute(name, input || {});
308
310
  } catch (err) {
309
311
  result = { success: false, output: `Error: ${err.message}` };
310
312
  }
313
+ const durationMs = Date.now() - toolStart;
314
+ const resultStagnation = stagnation.recordResult(name, input || {}, result);
315
+ if (resultStagnation.detected) {
316
+ const message = stagnationMessage(name, resultStagnation.count, resultStagnation);
317
+ result = {
318
+ ...result,
319
+ success: false,
320
+ output: message,
321
+ _stagnation: true,
322
+ };
323
+ yield {
324
+ type: 'stagnation',
325
+ data: {
326
+ tool: name,
327
+ count: resultStagnation.count,
328
+ message,
329
+ kind: resultStagnation.kind,
330
+ target: resultStagnation.target,
331
+ },
332
+ };
333
+ }
311
334
 
312
- yield { type: 'tool_done', data: { tool: name, duration_ms: 0 } };
335
+ yield {
336
+ type: 'tool_done',
337
+ data: {
338
+ ...result,
339
+ call_id: id,
340
+ tool: name,
341
+ args: input || {},
342
+ duration_ms: durationMs,
343
+ },
344
+ };
313
345
 
314
346
  assistantContent.push(block);
315
347
  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);
@@ -829,7 +833,7 @@ export function getHistory(n = 50) {
829
833
 
830
834
  export function getStorePaths() {
831
835
  return {
832
- keplerDir: KEPLER_DIR,
836
+ bahulamDir: KEPLER_DIR,
833
837
  projectsDir: PROJECTS_DIR,
834
838
  historyPath: path.join(KEPLER_DIR, 'history.jsonl'),
835
839
  };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Mode Selector
3
3
  *
4
- * remote (default): All requests go to Tarang backend.
4
+ * remote (default): All requests go to Bahulam backend.
5
5
  * Backend handles orchestration, model selection, tool routing.
6
6
  * User's provider and models configured via web Settings page.
7
7
  *
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Output Filter — Smart shell output filtering + auto-lint.
3
- * Ported from tarang-cli (Python) ws/executor.py with enhanced patterns.
3
+ * Ported from tarang-cli (Python, legacy) 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();