@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.
- package/LICENSE +1 -1
- package/NOTICE +6 -6
- package/README.md +10 -17
- package/package.json +9 -9
- package/src/agents/loader.mjs +1 -1
- package/src/agents/workflow_loader.mjs +2 -2
- package/src/auth/tarang-auth.mjs +3 -2
- package/src/config/model-catalog-default.json +4 -3
- package/src/config/model-catalog.mjs +2 -0
- package/src/context/prose-chunker.mjs +162 -5
- package/src/core/attachments.mjs +65 -3
- package/src/core/bundled-runtime.mjs +1 -1
- package/src/core/headless.mjs +14 -0
- package/src/core/jsonl-writer.mjs +3 -3
- package/src/core/lint-resolver.mjs +472 -0
- package/src/core/local-agent.mjs +15 -4
- package/src/core/local-store.mjs +4 -0
- package/src/core/output-filter.mjs +21 -25
- package/src/core/pricing.mjs +10 -3
- package/src/core/stream-client.mjs +9 -4
- package/src/core/tool-executor.mjs +105 -29
- 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/onboarding/preflight.mjs +1 -1
- package/src/terminal/analytics.mjs +2 -6
- package/src/terminal/ansi.mjs +11 -3
- package/src/terminal/main.mjs +37 -11
- 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 +35 -5
- package/src/terminal/repl-state.mjs +12 -0
- package/src/terminal/repl.mjs +231 -53
- package/src/terminal/tool-display.mjs +32 -0
- package/src/terminal/watch-state.mjs +118 -0
- 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/registry.mjs +4 -0
- package/src/ui/icons.mjs +2 -0
- package/src/ui/palette.mjs +1 -1
- package/src/ui/slash-commands.mjs +4 -6
- package/src/ui/tool-card.mjs +7 -3
- 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
|
@@ -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
|
|
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
|
|
147
|
-
* This is a
|
|
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
|
+
}
|
package/src/core/local-agent.mjs
CHANGED
|
@@ -167,11 +167,11 @@ const TOOL_SCHEMAS = [
|
|
|
167
167
|
},
|
|
168
168
|
{
|
|
169
169
|
name: 'lint_check',
|
|
170
|
-
description: 'Run
|
|
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
|
-
|
|
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() });
|
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);
|
|
@@ -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
|
|
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();
|
package/src/core/pricing.mjs
CHANGED
|
@@ -282,7 +282,7 @@ export function formatCostValue(cost) {
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
/**
|
|
285
|
-
* Convert provider cost to
|
|
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
|
|
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
|
/**
|