@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tool Executor Bridge — maps
|
|
2
|
+
* Tool Executor Bridge — maps Bahulam backend tool names to OCC tool calls.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
4
|
+
* The Bahulam backend sends tool_request events with its own tool names and arg shapes.
|
|
5
5
|
* This bridge translates those into OCC tool calls and wraps the results.
|
|
6
6
|
*
|
|
7
7
|
* Safety guardrails integrated — prevents destructive operations on source code.
|
|
@@ -18,21 +18,23 @@ import { SkillInstaller } from '../skills/installer.mjs';
|
|
|
18
18
|
import { SkillsLoader } from '../skills/loader.mjs';
|
|
19
19
|
import { createAgentFile, listLocalAgents, syncAgentsToBackend } from '../agents/scaffold.mjs';
|
|
20
20
|
import { createWorkflowFile, listLocalWorkflows, WORKFLOW_SYNC_ENDPOINT, slugifyWorkflowName } from '../agents/workflow_scaffold.mjs';
|
|
21
|
-
import {
|
|
21
|
+
import { BahulamAuth } from '../auth/bahulam-auth.mjs';
|
|
22
|
+
import { detectImageFile } from './attachments.mjs';
|
|
22
23
|
import { streamResponse } from './streaming.mjs';
|
|
23
24
|
import { sendApprovalDecision, sendCallback } from './callback-client.mjs';
|
|
24
25
|
import { HookRunner } from '../config/hook-runner.mjs';
|
|
25
26
|
import { buildFileDiff } from './file-diff.mjs';
|
|
26
27
|
import { buildWorkScope } from './work-scope.mjs';
|
|
27
28
|
import { loadDiskMemory, ensureBahulamDir, globalMemoryPath, projectMemoryPath } from './memory-disk.mjs';
|
|
29
|
+
import { resolveLintCommand } from './lint-resolver.mjs';
|
|
28
30
|
import * as fs from 'node:fs';
|
|
29
31
|
import * as os from 'node:os';
|
|
30
32
|
import * as path from 'node:path';
|
|
31
33
|
import * as crypto from 'node:crypto';
|
|
32
|
-
import { execSync } from 'node:child_process';
|
|
34
|
+
import { exec, execSync } from 'node:child_process';
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
|
-
* Create a tool executor that bridges
|
|
37
|
+
* Create a tool executor that bridges Bahulam tool names to OCC tools.
|
|
36
38
|
* @param {Object} [options]
|
|
37
39
|
* @param {ProjectRegistry} [options.projectRegistry] - session-owned project registry
|
|
38
40
|
* @returns {{ execute(name, args): Promise<Object>, listTools(): string[] }}
|
|
@@ -470,7 +472,7 @@ export function createToolExecutor({
|
|
|
470
472
|
}
|
|
471
473
|
|
|
472
474
|
/**
|
|
473
|
-
* Wrap an OCC string result into
|
|
475
|
+
* Wrap an OCC string result into Bahulam's { success, output } format.
|
|
474
476
|
*/
|
|
475
477
|
function wrapResult(result, toolName) {
|
|
476
478
|
if (typeof result === 'object' && result !== null && 'success' in result) {
|
|
@@ -487,43 +489,39 @@ export function createToolExecutor({
|
|
|
487
489
|
|
|
488
490
|
// ── Auto-lint after file writes ────────────────────────────
|
|
489
491
|
|
|
490
|
-
const LINT_COMMANDS = {
|
|
491
|
-
'.py': (file) => `python3 -m py_compile "${file}" 2>&1`,
|
|
492
|
-
'.js': (file) => `npx eslint --no-eslintrc --rule '{}' "${file}" 2>&1 || true`,
|
|
493
|
-
'.ts': (file) => `npx tsc --noEmit --pretty "${file}" 2>&1 || true`,
|
|
494
|
-
'.tsx': (file) => `npx tsc --noEmit --pretty "${file}" 2>&1 || true`,
|
|
495
|
-
'.go': (file) => `go vet "${file}" 2>&1`,
|
|
496
|
-
'.rs': (file) => `rustfmt --check "${file}" 2>&1`,
|
|
497
|
-
};
|
|
498
|
-
|
|
499
492
|
// tsc --pretty and eslint emit ANSI codes (including background-red
|
|
500
493
|
// highlights) which bleed when our renderer slices the first 80 chars.
|
|
501
494
|
// Strip color codes so the stored lint string is always plain text.
|
|
502
495
|
const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]/g;
|
|
503
496
|
function stripAnsi(s) { return String(s || '').replace(ANSI_RE, ''); }
|
|
504
497
|
|
|
505
|
-
function
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
if (!cmdFn) return null;
|
|
509
|
-
|
|
510
|
-
try {
|
|
511
|
-
const output = execSync(cmdFn(filePath), {
|
|
498
|
+
function runAutoLintCommand(lint) {
|
|
499
|
+
return new Promise((resolve) => {
|
|
500
|
+
exec(lint.command, {
|
|
512
501
|
encoding: 'utf-8',
|
|
513
502
|
timeout: 15_000,
|
|
514
|
-
cwd:
|
|
515
|
-
|
|
503
|
+
cwd: lint.cwd,
|
|
504
|
+
maxBuffer: 1_000_000,
|
|
516
505
|
env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1', TERM: 'dumb' },
|
|
506
|
+
}, (err, stdout = '', stderr = '') => {
|
|
507
|
+
const output = err
|
|
508
|
+
? stripAnsi(stderr || stdout || '').trim()
|
|
509
|
+
: stripAnsi(stdout || stderr || '').trim();
|
|
510
|
+
resolve(output || null);
|
|
517
511
|
});
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function autoLint(filePath) {
|
|
516
|
+
const project = projectRegistry.projectForPath(filePath);
|
|
517
|
+
const lint = resolveLintCommand(filePath, {
|
|
518
|
+
projectRoot: project?.resource?.root || projectRootFor(filePath),
|
|
519
|
+
projectCommands: project?.resource?.commands || {},
|
|
520
|
+
allowProjectScript: false,
|
|
521
|
+
});
|
|
522
|
+
if (!lint?.command) return null;
|
|
523
|
+
|
|
524
|
+
return runAutoLintCommand(lint);
|
|
527
525
|
}
|
|
528
526
|
|
|
529
527
|
// ── Post-edit verification hint ──────────────────────────────
|
|
@@ -695,6 +693,52 @@ export function createToolExecutor({
|
|
|
695
693
|
};
|
|
696
694
|
},
|
|
697
695
|
|
|
696
|
+
analyze_image: async (args, options = {}) => {
|
|
697
|
+
throwIfAborted(options.signal);
|
|
698
|
+
const tool = occRegistry.get('analyze_image');
|
|
699
|
+
if (!tool) {
|
|
700
|
+
return { success: false, output: 'analyze_image tool is not registered in the CLI.', _tool: 'analyze_image' };
|
|
701
|
+
}
|
|
702
|
+
try {
|
|
703
|
+
const result = await tool.call(args || {});
|
|
704
|
+
throwIfAborted(options.signal);
|
|
705
|
+
if (typeof result === 'string') {
|
|
706
|
+
return { success: !/^error\b/i.test(result), output: result, _tool: 'analyze_image' };
|
|
707
|
+
}
|
|
708
|
+
return {
|
|
709
|
+
success: result?.success !== false,
|
|
710
|
+
output: result?.output || result?.summary || JSON.stringify(result),
|
|
711
|
+
...result,
|
|
712
|
+
_tool: 'analyze_image',
|
|
713
|
+
};
|
|
714
|
+
} catch (err) {
|
|
715
|
+
return { success: false, output: String(err?.message || err), _tool: 'analyze_image' };
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
|
|
719
|
+
generate_image: async (args, options = {}) => {
|
|
720
|
+
throwIfAborted(options.signal);
|
|
721
|
+
const tool = occRegistry.get('generate_image');
|
|
722
|
+
if (!tool) {
|
|
723
|
+
return { success: false, output: 'generate_image tool is not registered in the CLI.', _tool: 'generate_image' };
|
|
724
|
+
}
|
|
725
|
+
try {
|
|
726
|
+
const result = await tool.call(args || {});
|
|
727
|
+
throwIfAborted(options.signal);
|
|
728
|
+
if (typeof result === 'string') {
|
|
729
|
+
return { success: !/^error\b/i.test(result), output: result, _tool: 'generate_image' };
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
success: result?.success !== false,
|
|
733
|
+
output: result?.output || JSON.stringify(result),
|
|
734
|
+
...result,
|
|
735
|
+
_tool: 'generate_image',
|
|
736
|
+
};
|
|
737
|
+
} catch (err) {
|
|
738
|
+
return { success: false, output: String(err?.message || err), _tool: 'generate_image' };
|
|
739
|
+
}
|
|
740
|
+
},
|
|
741
|
+
|
|
698
742
|
// 0b. read_attachment → chunked text extraction from a local document.
|
|
699
743
|
// Backend registers the schema; execution happens client-side because
|
|
700
744
|
// only the CLI has the user's filesystem. Supports the `path` mode
|
|
@@ -734,6 +778,17 @@ export function createToolExecutor({
|
|
|
734
778
|
return { success: false, output: String(err?.message || err), _tool: 'read_attachment' };
|
|
735
779
|
}
|
|
736
780
|
|
|
781
|
+
const imageInfo = detectImageFile(abs);
|
|
782
|
+
if (imageInfo) {
|
|
783
|
+
return {
|
|
784
|
+
success: false,
|
|
785
|
+
output: `This file is an image (${imageInfo.mime_type}). read_attachment only extracts text documents. Use analyze_image with a specific question. If analyze_image already failed, report that vision error directly instead of retrying this image with read_attachment.`,
|
|
786
|
+
_tool: 'read_attachment',
|
|
787
|
+
_path: abs,
|
|
788
|
+
_mime: imageInfo.mime_type,
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
737
792
|
const { extractFromPath } = await import('../context/prose-chunker.mjs');
|
|
738
793
|
let mime, chunks;
|
|
739
794
|
try {
|
|
@@ -747,7 +802,7 @@ export function createToolExecutor({
|
|
|
747
802
|
if (!chunks.length) {
|
|
748
803
|
return {
|
|
749
804
|
success: false,
|
|
750
|
-
output: `Unsupported or empty file (mime=${mime}). Supported: pdf, txt, md/mdx, csv, tsv, json, yaml, toml, html, log, rst. For CSV/Excel analysis use read_table; for images use analyze_image.`,
|
|
805
|
+
output: `Unsupported or empty file (mime=${mime}). Supported text: pdf, txt, md/mdx, ipynb, csv, tsv, json, yaml, toml, xml, html, log, rst, sql, sh, .env, .ini, Dockerfile, .gitignore — plus any file whose first 8KB is valid UTF-8 (auto-sniffed). For CSV/Excel analysis use read_table; for images use analyze_image. If analyze_image already failed for this file, report that vision error directly instead of retrying with read_attachment.`,
|
|
751
806
|
_tool: 'read_attachment',
|
|
752
807
|
};
|
|
753
808
|
}
|
|
@@ -966,10 +1021,12 @@ export function createToolExecutor({
|
|
|
966
1021
|
filteredLines: rawOutput.split('\n').length,
|
|
967
1022
|
}
|
|
968
1023
|
: filterOutput(rawOutput, args.command, success);
|
|
1024
|
+
const agentOutput = observationTimeout && timedOut ? filtered.output : rawOutput;
|
|
969
1025
|
|
|
970
1026
|
return {
|
|
971
1027
|
success,
|
|
972
|
-
output:
|
|
1028
|
+
output: agentOutput,
|
|
1029
|
+
output_preview: filtered.output,
|
|
973
1030
|
exit_code: exitCode,
|
|
974
1031
|
_tool: 'shell',
|
|
975
1032
|
_classification: args._classification,
|
|
@@ -1087,7 +1144,7 @@ export function createToolExecutor({
|
|
|
1087
1144
|
updateProjectIndex(filePath);
|
|
1088
1145
|
|
|
1089
1146
|
// Auto-lint the written file
|
|
1090
|
-
const lintOutput = autoLint(filePath);
|
|
1147
|
+
const lintOutput = await autoLint(filePath);
|
|
1091
1148
|
if (lintOutput) {
|
|
1092
1149
|
wrapped.output += `\n\n--- Lint ---\n${lintOutput}`;
|
|
1093
1150
|
wrapped.lint = lintOutput;
|
|
@@ -1180,9 +1237,21 @@ export function createToolExecutor({
|
|
|
1180
1237
|
// 4. edit_file → Edit + auto-lint + auto-fallback to sed
|
|
1181
1238
|
edit_file: async (args) => {
|
|
1182
1239
|
const rawPath = args.file_path || args.path;
|
|
1240
|
+
const searchText = args.search ?? args.old_string ?? args.oldString;
|
|
1241
|
+
const replaceText = args.replace ?? args.new_string ?? args.newString;
|
|
1242
|
+
const replaceAll = args.replace_all === true || args.replaceAll === true;
|
|
1243
|
+
if (!rawPath || rawPath === 'file' || rawPath.length < 3) {
|
|
1244
|
+
return { success: false, output: `Error: Invalid file path "${rawPath || ''}". Register the project, then use an absolute path.`, _tool: 'edit_file' };
|
|
1245
|
+
}
|
|
1246
|
+
if (typeof searchText !== 'string' || searchText.length === 0) {
|
|
1247
|
+
return { success: false, output: 'Error: edit_file requires a non-empty search string.', _tool: 'edit_file' };
|
|
1248
|
+
}
|
|
1249
|
+
if (typeof replaceText !== 'string') {
|
|
1250
|
+
return { success: false, output: 'Error: edit_file requires a replacement string.', _tool: 'edit_file' };
|
|
1251
|
+
}
|
|
1183
1252
|
const filePath = await resolvePath(rawPath, args);
|
|
1184
1253
|
const before = readTextIfExists(filePath);
|
|
1185
|
-
const writeCheck = validateWrite(filePath,
|
|
1254
|
+
const writeCheck = validateWrite(filePath, replaceText, projectRootFor(filePath));
|
|
1186
1255
|
if (!writeCheck.safe) {
|
|
1187
1256
|
return { success: false, output: `BLOCKED: ${writeCheck.reason}`, _tool: 'edit_file', _blocked: true };
|
|
1188
1257
|
}
|
|
@@ -1200,46 +1269,51 @@ export function createToolExecutor({
|
|
|
1200
1269
|
try {
|
|
1201
1270
|
result = await occRegistry.call('edit_file', {
|
|
1202
1271
|
file_path: filePath,
|
|
1203
|
-
search:
|
|
1204
|
-
replace:
|
|
1205
|
-
replace_all:
|
|
1272
|
+
search: searchText,
|
|
1273
|
+
replace: replaceText,
|
|
1274
|
+
replace_all: replaceAll,
|
|
1206
1275
|
});
|
|
1207
1276
|
} catch (editErr) {
|
|
1208
|
-
// OCC Edit failed (string not found) — fallback to
|
|
1277
|
+
// OCC Edit failed (string not found) — fallback to direct text replacement.
|
|
1209
1278
|
try {
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
if
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
"`;
|
|
1224
|
-
const fallbackResult = execSync(pyCmd, {
|
|
1225
|
-
encoding: 'utf-8',
|
|
1226
|
-
timeout: 5000,
|
|
1227
|
-
cwd: projectRootFor(filePath),
|
|
1228
|
-
});
|
|
1229
|
-
result = `Edited ${filePath} (via fallback): ${fallbackResult.trim()}`;
|
|
1230
|
-
} catch (sedErr) {
|
|
1231
|
-
return { success: false, output: `edit_file failed: ${editErr?.message || 'unknown'}. Fallback also failed: ${sedErr?.message || 'unknown'}. Try shell(sed) manually.`, _tool: 'edit_file' };
|
|
1279
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1280
|
+
if (!content.includes(searchText)) {
|
|
1281
|
+
throw new Error('search string not found in file');
|
|
1282
|
+
}
|
|
1283
|
+
const nextContent = replaceAll
|
|
1284
|
+
? content.split(searchText).join(replaceText)
|
|
1285
|
+
: content.replace(searchText, replaceText);
|
|
1286
|
+
if (nextContent !== content) {
|
|
1287
|
+
fs.writeFileSync(filePath, nextContent, 'utf-8');
|
|
1288
|
+
}
|
|
1289
|
+
result = `Edited ${filePath} (via fallback): OK: replaced`;
|
|
1290
|
+
} catch (fallbackErr) {
|
|
1291
|
+
return { success: false, output: `edit_file failed: ${editErr?.message || 'unknown'}. Fallback also failed: ${fallbackErr?.message || 'unknown'}. Re-read the target range and provide an exact search string.`, _tool: 'edit_file' };
|
|
1232
1292
|
}
|
|
1233
1293
|
}
|
|
1234
1294
|
|
|
1235
1295
|
const wrapped = wrapResult(result, 'edit_file');
|
|
1236
1296
|
const after = readTextIfExists(filePath);
|
|
1297
|
+
if (wrapped.success !== false && before === after) {
|
|
1298
|
+
const relativePath = path.relative(projectRootFor(filePath), filePath) || path.basename(filePath);
|
|
1299
|
+
return {
|
|
1300
|
+
success: false,
|
|
1301
|
+
output: `edit_file made no changes to ${relativePath}. The search string may already be replaced, the replacement may be identical, or the target content may have drifted. Re-read the target range before trying a different edit.`,
|
|
1302
|
+
_tool: 'edit_file',
|
|
1303
|
+
_no_change: true,
|
|
1304
|
+
no_change: true,
|
|
1305
|
+
file_path: filePath,
|
|
1306
|
+
relative_path: relativePath,
|
|
1307
|
+
lines_added: 0,
|
|
1308
|
+
lines_removed: 0,
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1237
1311
|
attachFileDiff(wrapped, filePath, before, after);
|
|
1238
1312
|
updateProjectIndex(filePath);
|
|
1239
1313
|
_hasEdited = true;
|
|
1240
1314
|
|
|
1241
1315
|
// Auto-lint the edited file
|
|
1242
|
-
const lintOutput = autoLint(filePath);
|
|
1316
|
+
const lintOutput = await autoLint(filePath);
|
|
1243
1317
|
if (lintOutput) {
|
|
1244
1318
|
wrapped.output += `\n\n--- Lint ---\n${lintOutput}`;
|
|
1245
1319
|
wrapped.lint = lintOutput;
|
|
@@ -1449,7 +1523,7 @@ print('OK: replaced')
|
|
|
1449
1523
|
};
|
|
1450
1524
|
},
|
|
1451
1525
|
|
|
1452
|
-
// ──
|
|
1526
|
+
// ── Bahulam-specific tools (no OCC bridge) ──────────────
|
|
1453
1527
|
|
|
1454
1528
|
// 8. read_files → batch Read (with AST truncation for large files)
|
|
1455
1529
|
read_files: async (args) => {
|
|
@@ -1608,24 +1682,44 @@ print('OK: replaced')
|
|
|
1608
1682
|
try {
|
|
1609
1683
|
throwIfAborted(options.signal);
|
|
1610
1684
|
const filePath = await resolvePath(args.file_path || args.path, args);
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1685
|
+
const project = projectRegistry.projectForPath(filePath);
|
|
1686
|
+
const lint = resolveLintCommand(filePath, {
|
|
1687
|
+
projectRoot: project?.resource?.root || projectRootFor(filePath),
|
|
1688
|
+
projectCommands: project?.resource?.commands || {},
|
|
1689
|
+
allowProjectScript: true,
|
|
1690
|
+
});
|
|
1691
|
+
if (!lint?.command) {
|
|
1692
|
+
return {
|
|
1693
|
+
success: true,
|
|
1694
|
+
issues: [],
|
|
1695
|
+
message: 'No project-aware linter for this path or file type',
|
|
1696
|
+
_tool: 'lint_check',
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1616
1699
|
|
|
1617
|
-
const
|
|
1618
|
-
command:
|
|
1700
|
+
const shellResult = await executeToolWithHooks('shell', {
|
|
1701
|
+
command: lint.command,
|
|
1619
1702
|
timeout: 30_000,
|
|
1620
|
-
description: `Lint: ${path.basename(filePath)}`,
|
|
1621
|
-
cwd:
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1703
|
+
description: `Lint: ${path.basename(filePath)} (${lint.source})`,
|
|
1704
|
+
cwd: lint.cwd,
|
|
1705
|
+
}, options);
|
|
1706
|
+
const rawOutput = typeof shellResult?.output === 'string'
|
|
1707
|
+
? shellResult.output
|
|
1708
|
+
: (typeof shellResult === 'string' ? shellResult : String(shellResult));
|
|
1625
1709
|
if (/^Error:\s*Command cancelled by user/i.test(rawOutput)) {
|
|
1626
1710
|
return { success: false, output: 'Cancelled by user', _cancelled: true, _tool: 'lint_check' };
|
|
1627
1711
|
}
|
|
1628
|
-
|
|
1712
|
+
const normalizedOutput = rawOutput === '(no output)' ? '' : rawOutput;
|
|
1713
|
+
return {
|
|
1714
|
+
success: true,
|
|
1715
|
+
output: normalizedOutput || 'No lint issues found.',
|
|
1716
|
+
command: lint.command,
|
|
1717
|
+
cwd: lint.cwd,
|
|
1718
|
+
language: lint.language,
|
|
1719
|
+
lint_source: lint.source,
|
|
1720
|
+
issues: normalizedOutput.split('\n').filter(Boolean),
|
|
1721
|
+
_tool: 'lint_check',
|
|
1722
|
+
};
|
|
1629
1723
|
} catch (err) {
|
|
1630
1724
|
if (isAbortError(err) || options.signal?.aborted) return cancelledToolResult('lint_check');
|
|
1631
1725
|
return { success: false, output: err.message, _tool: 'lint_check' };
|
|
@@ -1858,7 +1952,7 @@ print('OK: replaced')
|
|
|
1858
1952
|
const target = args.name || args.slug || '';
|
|
1859
1953
|
throw new Error(target ? `No local agent found: ${target}` : 'No local agents found in .bahulam/agents');
|
|
1860
1954
|
}
|
|
1861
|
-
const creds = new
|
|
1955
|
+
const creds = new BahulamAuth().loadCredentials();
|
|
1862
1956
|
const result = await syncAgentsToBackend({
|
|
1863
1957
|
backendUrl: creds.backendUrl,
|
|
1864
1958
|
token: creds.token,
|
|
@@ -1880,7 +1974,7 @@ print('OK: replaced')
|
|
|
1880
1974
|
const local = filterLocalWorkflows(args).map(compactWorkflowMetadata);
|
|
1881
1975
|
let backend = [];
|
|
1882
1976
|
try {
|
|
1883
|
-
const creds = new
|
|
1977
|
+
const creds = new BahulamAuth().loadCredentials();
|
|
1884
1978
|
if (creds.backendUrl && creds.token) {
|
|
1885
1979
|
const resp = await fetch(`${creds.backendUrl}${WORKFLOW_SYNC_ENDPOINT}`, {
|
|
1886
1980
|
headers: {
|
|
@@ -1946,7 +2040,7 @@ print('OK: replaced')
|
|
|
1946
2040
|
const target = args.name || args.slug || '';
|
|
1947
2041
|
throw new Error(target ? `No local workflow found: ${target}` : 'No local workflows found in .bahulam/workflows');
|
|
1948
2042
|
}
|
|
1949
|
-
const creds = new
|
|
2043
|
+
const creds = new BahulamAuth().loadCredentials();
|
|
1950
2044
|
if (!creds.backendUrl || !creds.token) {
|
|
1951
2045
|
throw new Error('Not logged in. Run bahulam login first.');
|
|
1952
2046
|
}
|
|
@@ -2018,7 +2112,7 @@ print('OK: replaced')
|
|
|
2018
2112
|
if (!target) {
|
|
2019
2113
|
throw new Error('workflow_id is required');
|
|
2020
2114
|
}
|
|
2021
|
-
const creds = new
|
|
2115
|
+
const creds = new BahulamAuth().loadCredentials();
|
|
2022
2116
|
if (!creds.backendUrl || !creds.token) {
|
|
2023
2117
|
throw new Error('Not logged in. Run bahulam login first.');
|
|
2024
2118
|
}
|
|
@@ -2197,8 +2291,8 @@ print('OK: replaced')
|
|
|
2197
2291
|
|
|
2198
2292
|
return {
|
|
2199
2293
|
/**
|
|
2200
|
-
* Execute a
|
|
2201
|
-
* @param {string} name -
|
|
2294
|
+
* Execute a Bahulam tool by name.
|
|
2295
|
+
* @param {string} name - Bahulam tool name
|
|
2202
2296
|
* @param {Object} args - Tool arguments
|
|
2203
2297
|
* @returns {Promise<Object>} - { success, output, ... }
|
|
2204
2298
|
*/
|