@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.
- package/LICENSE +1 -1
- package/NOTICE +6 -6
- package/README.md +11 -18
- package/package.json +11 -11
- package/src/agents/loader.mjs +1 -1
- package/src/agents/workflow_loader.mjs +2 -2
- package/src/auth/{tarang-auth.mjs → bahulam-auth.mjs} +4 -3
- package/src/commands/agent.mjs +3 -3
- package/src/commands/device.mjs +2 -2
- package/src/commands/pair.mjs +2 -2
- package/src/commands/remote.mjs +3 -3
- package/src/commands/workflow.mjs +5 -5
- package/src/config/env.mjs +9 -2
- package/src/config/memory-loader.mjs +1 -1
- package/src/config/model-catalog-default.json +4 -3
- package/src/config/model-catalog.mjs +2 -0
- package/src/config/settings.mjs +2 -2
- package/src/context/prose-chunker.mjs +162 -5
- package/src/core/agent-loop.mjs +17 -0
- package/src/core/attachments.mjs +66 -4
- package/src/core/backend-url.mjs +11 -12
- package/src/core/bundled-runtime.mjs +1 -1
- package/src/core/callback-client.mjs +1 -1
- package/src/core/headless.mjs +18 -4
- package/src/core/jsonl-writer.mjs +18 -18
- package/src/core/lint-resolver.mjs +472 -0
- package/src/core/local-agent.mjs +40 -8
- package/src/core/local-store.mjs +5 -1
- package/src/core/mode-selector.mjs +1 -1
- package/src/core/output-filter.mjs +22 -26
- package/src/core/pricing.mjs +10 -3
- package/src/core/project-artifacts.mjs +3 -3
- package/src/core/project-context-loader.mjs +9 -9
- package/src/core/settings-sync.mjs +1 -1
- package/src/core/stagnation.mjs +61 -1
- package/src/core/stream-client.mjs +13 -8
- package/src/core/system-prompt.mjs +2 -2
- package/src/core/tool-executor.mjs +178 -84
- package/src/daemon/http-dashboard.mjs +400 -0
- package/src/daemon/session-core.mjs +1 -0
- package/src/local-service/agent-relay.mjs +868 -0
- package/src/local-service/approval-bridge.mjs +222 -0
- package/src/local-service/browser.mjs +24 -0
- package/src/local-service/command.mjs +155 -0
- package/src/local-service/file-access.mjs +393 -0
- package/src/local-service/machine.mjs +86 -0
- package/src/local-service/paths.mjs +29 -0
- package/src/local-service/preview-converters.mjs +260 -0
- package/src/local-service/server.mjs +2644 -0
- package/src/local-service/session-store.mjs +221 -0
- package/src/mcp/client.mjs +4 -4
- package/src/onboarding/preflight.mjs +2 -2
- package/src/terminal/agents.mjs +2 -2
- package/src/terminal/analytics.mjs +2 -6
- package/src/terminal/ansi.mjs +11 -3
- package/src/terminal/main.mjs +43 -17
- package/src/terminal/model-catalog-display.mjs +111 -0
- package/src/terminal/repl-format.mjs +4 -1
- package/src/terminal/repl-model-form.mjs +81 -14
- package/src/terminal/repl-render.mjs +37 -7
- package/src/terminal/repl-resume.mjs +2 -2
- package/src/terminal/repl-state.mjs +12 -0
- package/src/terminal/repl.mjs +248 -70
- package/src/terminal/tool-display.mjs +32 -0
- package/src/terminal/watch-state.mjs +118 -0
- package/src/tools/agent.mjs +2 -2
- package/src/tools/analyze-image.mjs +158 -0
- package/src/tools/bash.mjs +11 -9
- package/src/tools/generate-image.mjs +411 -0
- package/src/tools/lint.mjs +24 -43
- package/src/tools/project-overview.mjs +12 -12
- package/src/tools/registry.mjs +4 -0
- package/src/ui/formatter.mjs +2 -2
- package/src/ui/icons.mjs +2 -0
- package/src/ui/input-dock.mjs +7 -7
- package/src/ui/palette.mjs +1 -1
- package/src/ui/slash-commands.mjs +4 -6
- package/src/ui/spinner.mjs +1 -1
- package/src/ui/term.mjs +2 -6
- package/src/ui/tool-card.mjs +81 -6
- package/pulse/app/activity/page.tsx +0 -190
- package/pulse/app/api/activity/route.ts +0 -138
- package/pulse/app/api/benchmark/route.ts +0 -113
- package/pulse/app/api/benchmarks/route.ts +0 -195
- package/pulse/app/api/costs/route.ts +0 -88
- package/pulse/app/api/export/route.ts +0 -77
- package/pulse/app/api/history/route.ts +0 -11
- package/pulse/app/api/import/route.ts +0 -31
- package/pulse/app/api/memory/route.ts +0 -50
- package/pulse/app/api/plans/route.ts +0 -9
- package/pulse/app/api/projects/[slug]/route.ts +0 -96
- package/pulse/app/api/projects/route.ts +0 -121
- package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
- package/pulse/app/api/sessions/[id]/route.ts +0 -31
- package/pulse/app/api/sessions/route.ts +0 -112
- package/pulse/app/api/settings/route.ts +0 -14
- package/pulse/app/api/stats/route.ts +0 -143
- package/pulse/app/api/todos/route.ts +0 -9
- package/pulse/app/api/tools/route.ts +0 -160
- package/pulse/app/benchmarks/page.tsx +0 -224
- package/pulse/app/costs/page.tsx +0 -179
- package/pulse/app/export/page.tsx +0 -465
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +0 -263
- package/pulse/app/help/page.tsx +0 -143
- package/pulse/app/history/page.tsx +0 -157
- package/pulse/app/layout.tsx +0 -46
- package/pulse/app/memory/page.tsx +0 -365
- package/pulse/app/overview-client.tsx +0 -393
- package/pulse/app/page.tsx +0 -14
- package/pulse/app/plans/page.tsx +0 -308
- package/pulse/app/projects/[slug]/page.tsx +0 -390
- package/pulse/app/projects/page.tsx +0 -110
- package/pulse/app/sessions/[id]/page.tsx +0 -243
- package/pulse/app/sessions/page.tsx +0 -39
- package/pulse/app/settings/page.tsx +0 -188
- package/pulse/app/todos/page.tsx +0 -211
- package/pulse/app/tools/page.tsx +0 -249
- package/pulse/cli.js +0 -164
- package/pulse/components/activity/day-of-week-chart.tsx +0 -35
- package/pulse/components/activity/streak-card.tsx +0 -36
- package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
- package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
- package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
- package/pulse/components/costs/model-token-table.tsx +0 -60
- package/pulse/components/global-search.tsx +0 -193
- package/pulse/components/keyboard-nav-provider.tsx +0 -23
- package/pulse/components/layout/bottom-nav.tsx +0 -53
- package/pulse/components/layout/client-layout.tsx +0 -31
- package/pulse/components/layout/sidebar-context.tsx +0 -50
- package/pulse/components/layout/sidebar.tsx +0 -183
- package/pulse/components/layout/top-bar.tsx +0 -121
- package/pulse/components/overview/activity-heatmap.tsx +0 -107
- package/pulse/components/overview/conversation-table.tsx +0 -148
- package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
- package/pulse/components/overview/peak-hours-chart.tsx +0 -87
- package/pulse/components/overview/project-activity-donut.tsx +0 -96
- package/pulse/components/overview/stat-card.tsx +0 -102
- package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
- package/pulse/components/projects/project-card.tsx +0 -175
- package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
- package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
- package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
- package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
- package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
- package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
- package/pulse/components/sessions/session-badges.tsx +0 -49
- package/pulse/components/sessions/session-table.tsx +0 -299
- package/pulse/components/theme-provider.tsx +0 -44
- package/pulse/components/tools/feature-adoption-table.tsx +0 -58
- package/pulse/components/tools/mcp-server-panel.tsx +0 -45
- package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
- package/pulse/components/tools/version-history-table.tsx +0 -32
- package/pulse/components/ui/alert.tsx +0 -66
- package/pulse/components/ui/badge.tsx +0 -48
- package/pulse/components/ui/breadcrumb.tsx +0 -109
- package/pulse/components/ui/button.tsx +0 -64
- package/pulse/components/ui/calendar.tsx +0 -220
- package/pulse/components/ui/card.tsx +0 -92
- package/pulse/components/ui/command.tsx +0 -158
- package/pulse/components/ui/dialog.tsx +0 -158
- package/pulse/components/ui/input.tsx +0 -21
- package/pulse/components/ui/popover.tsx +0 -89
- package/pulse/components/ui/progress.tsx +0 -31
- package/pulse/components/ui/select.tsx +0 -190
- package/pulse/components/ui/separator.tsx +0 -28
- package/pulse/components/ui/sheet.tsx +0 -143
- package/pulse/components/ui/skeleton.tsx +0 -13
- package/pulse/components/ui/table.tsx +0 -116
- package/pulse/components/ui/tabs.tsx +0 -91
- package/pulse/components/ui/tooltip.tsx +0 -57
- package/pulse/components/use-global-keyboard-nav.ts +0 -79
- package/pulse/components.json +0 -23
- package/pulse/eslint.config.mjs +0 -18
- package/pulse/lib/bahulam-paths.ts +0 -23
- package/pulse/lib/claude-reader.ts +0 -592
- package/pulse/lib/decode.ts +0 -129
- package/pulse/lib/pricing.ts +0 -102
- package/pulse/lib/replay-parser.ts +0 -165
- package/pulse/lib/tool-categories.ts +0 -140
- package/pulse/lib/utils.ts +0 -6
- package/pulse/next-env.d.ts +0 -6
- package/pulse/next.config.ts +0 -16
- package/pulse/package.json +0 -45
- package/pulse/postcss.config.mjs +0 -7
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +0 -1
- package/pulse/public/globe.svg +0 -1
- package/pulse/public/next.svg +0 -1
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +0 -1
- package/pulse/public/window.svg +0 -1
- package/pulse/tsconfig.json +0 -34
- 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
|
+
}
|
package/src/core/local-agent.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
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', '
|
|
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
|
|
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 {
|
|
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() });
|
package/src/core/local-store.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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();
|