@bahulam/code 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +6 -6
  3. package/README.md +11 -18
  4. package/package.json +11 -11
  5. package/src/agents/loader.mjs +1 -1
  6. package/src/agents/workflow_loader.mjs +2 -2
  7. package/src/auth/{tarang-auth.mjs → bahulam-auth.mjs} +4 -3
  8. package/src/commands/agent.mjs +3 -3
  9. package/src/commands/device.mjs +2 -2
  10. package/src/commands/pair.mjs +2 -2
  11. package/src/commands/remote.mjs +3 -3
  12. package/src/commands/workflow.mjs +5 -5
  13. package/src/config/env.mjs +9 -2
  14. package/src/config/memory-loader.mjs +1 -1
  15. package/src/config/model-catalog-default.json +4 -3
  16. package/src/config/model-catalog.mjs +2 -0
  17. package/src/config/settings.mjs +2 -2
  18. package/src/context/prose-chunker.mjs +162 -5
  19. package/src/core/agent-loop.mjs +17 -0
  20. package/src/core/attachments.mjs +66 -4
  21. package/src/core/backend-url.mjs +11 -12
  22. package/src/core/bundled-runtime.mjs +1 -1
  23. package/src/core/callback-client.mjs +1 -1
  24. package/src/core/headless.mjs +18 -4
  25. package/src/core/jsonl-writer.mjs +18 -18
  26. package/src/core/lint-resolver.mjs +472 -0
  27. package/src/core/local-agent.mjs +40 -8
  28. package/src/core/local-store.mjs +5 -1
  29. package/src/core/mode-selector.mjs +1 -1
  30. package/src/core/output-filter.mjs +22 -26
  31. package/src/core/pricing.mjs +10 -3
  32. package/src/core/project-artifacts.mjs +3 -3
  33. package/src/core/project-context-loader.mjs +9 -9
  34. package/src/core/settings-sync.mjs +1 -1
  35. package/src/core/stagnation.mjs +61 -1
  36. package/src/core/stream-client.mjs +13 -8
  37. package/src/core/system-prompt.mjs +2 -2
  38. package/src/core/tool-executor.mjs +178 -84
  39. package/src/daemon/http-dashboard.mjs +400 -0
  40. package/src/daemon/session-core.mjs +1 -0
  41. package/src/local-service/agent-relay.mjs +868 -0
  42. package/src/local-service/approval-bridge.mjs +222 -0
  43. package/src/local-service/browser.mjs +24 -0
  44. package/src/local-service/command.mjs +155 -0
  45. package/src/local-service/file-access.mjs +393 -0
  46. package/src/local-service/machine.mjs +86 -0
  47. package/src/local-service/paths.mjs +29 -0
  48. package/src/local-service/preview-converters.mjs +260 -0
  49. package/src/local-service/server.mjs +2644 -0
  50. package/src/local-service/session-store.mjs +221 -0
  51. package/src/mcp/client.mjs +4 -4
  52. package/src/onboarding/preflight.mjs +2 -2
  53. package/src/terminal/agents.mjs +2 -2
  54. package/src/terminal/analytics.mjs +2 -6
  55. package/src/terminal/ansi.mjs +11 -3
  56. package/src/terminal/main.mjs +43 -17
  57. package/src/terminal/model-catalog-display.mjs +111 -0
  58. package/src/terminal/repl-format.mjs +4 -1
  59. package/src/terminal/repl-model-form.mjs +81 -14
  60. package/src/terminal/repl-render.mjs +37 -7
  61. package/src/terminal/repl-resume.mjs +2 -2
  62. package/src/terminal/repl-state.mjs +12 -0
  63. package/src/terminal/repl.mjs +248 -70
  64. package/src/terminal/tool-display.mjs +32 -0
  65. package/src/terminal/watch-state.mjs +118 -0
  66. package/src/tools/agent.mjs +2 -2
  67. package/src/tools/analyze-image.mjs +158 -0
  68. package/src/tools/bash.mjs +11 -9
  69. package/src/tools/generate-image.mjs +411 -0
  70. package/src/tools/lint.mjs +24 -43
  71. package/src/tools/project-overview.mjs +12 -12
  72. package/src/tools/registry.mjs +4 -0
  73. package/src/ui/formatter.mjs +2 -2
  74. package/src/ui/icons.mjs +2 -0
  75. package/src/ui/input-dock.mjs +7 -7
  76. package/src/ui/palette.mjs +1 -1
  77. package/src/ui/slash-commands.mjs +4 -6
  78. package/src/ui/spinner.mjs +1 -1
  79. package/src/ui/term.mjs +2 -6
  80. package/src/ui/tool-card.mjs +81 -6
  81. package/pulse/app/activity/page.tsx +0 -190
  82. package/pulse/app/api/activity/route.ts +0 -138
  83. package/pulse/app/api/benchmark/route.ts +0 -113
  84. package/pulse/app/api/benchmarks/route.ts +0 -195
  85. package/pulse/app/api/costs/route.ts +0 -88
  86. package/pulse/app/api/export/route.ts +0 -77
  87. package/pulse/app/api/history/route.ts +0 -11
  88. package/pulse/app/api/import/route.ts +0 -31
  89. package/pulse/app/api/memory/route.ts +0 -50
  90. package/pulse/app/api/plans/route.ts +0 -9
  91. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  92. package/pulse/app/api/projects/route.ts +0 -121
  93. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  94. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  95. package/pulse/app/api/sessions/route.ts +0 -112
  96. package/pulse/app/api/settings/route.ts +0 -14
  97. package/pulse/app/api/stats/route.ts +0 -143
  98. package/pulse/app/api/todos/route.ts +0 -9
  99. package/pulse/app/api/tools/route.ts +0 -160
  100. package/pulse/app/benchmarks/page.tsx +0 -224
  101. package/pulse/app/costs/page.tsx +0 -179
  102. package/pulse/app/export/page.tsx +0 -465
  103. package/pulse/app/favicon.ico +0 -0
  104. package/pulse/app/globals.css +0 -263
  105. package/pulse/app/help/page.tsx +0 -143
  106. package/pulse/app/history/page.tsx +0 -157
  107. package/pulse/app/layout.tsx +0 -46
  108. package/pulse/app/memory/page.tsx +0 -365
  109. package/pulse/app/overview-client.tsx +0 -393
  110. package/pulse/app/page.tsx +0 -14
  111. package/pulse/app/plans/page.tsx +0 -308
  112. package/pulse/app/projects/[slug]/page.tsx +0 -390
  113. package/pulse/app/projects/page.tsx +0 -110
  114. package/pulse/app/sessions/[id]/page.tsx +0 -243
  115. package/pulse/app/sessions/page.tsx +0 -39
  116. package/pulse/app/settings/page.tsx +0 -188
  117. package/pulse/app/todos/page.tsx +0 -211
  118. package/pulse/app/tools/page.tsx +0 -249
  119. package/pulse/cli.js +0 -164
  120. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  121. package/pulse/components/activity/streak-card.tsx +0 -36
  122. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  123. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  124. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  125. package/pulse/components/costs/model-token-table.tsx +0 -60
  126. package/pulse/components/global-search.tsx +0 -193
  127. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  128. package/pulse/components/layout/bottom-nav.tsx +0 -53
  129. package/pulse/components/layout/client-layout.tsx +0 -31
  130. package/pulse/components/layout/sidebar-context.tsx +0 -50
  131. package/pulse/components/layout/sidebar.tsx +0 -183
  132. package/pulse/components/layout/top-bar.tsx +0 -121
  133. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  134. package/pulse/components/overview/conversation-table.tsx +0 -148
  135. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  136. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  137. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  138. package/pulse/components/overview/stat-card.tsx +0 -102
  139. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  140. package/pulse/components/projects/project-card.tsx +0 -175
  141. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  142. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  143. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  144. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  145. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  146. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  147. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  148. package/pulse/components/sessions/session-badges.tsx +0 -49
  149. package/pulse/components/sessions/session-table.tsx +0 -299
  150. package/pulse/components/theme-provider.tsx +0 -44
  151. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  152. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  153. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  154. package/pulse/components/tools/version-history-table.tsx +0 -32
  155. package/pulse/components/ui/alert.tsx +0 -66
  156. package/pulse/components/ui/badge.tsx +0 -48
  157. package/pulse/components/ui/breadcrumb.tsx +0 -109
  158. package/pulse/components/ui/button.tsx +0 -64
  159. package/pulse/components/ui/calendar.tsx +0 -220
  160. package/pulse/components/ui/card.tsx +0 -92
  161. package/pulse/components/ui/command.tsx +0 -158
  162. package/pulse/components/ui/dialog.tsx +0 -158
  163. package/pulse/components/ui/input.tsx +0 -21
  164. package/pulse/components/ui/popover.tsx +0 -89
  165. package/pulse/components/ui/progress.tsx +0 -31
  166. package/pulse/components/ui/select.tsx +0 -190
  167. package/pulse/components/ui/separator.tsx +0 -28
  168. package/pulse/components/ui/sheet.tsx +0 -143
  169. package/pulse/components/ui/skeleton.tsx +0 -13
  170. package/pulse/components/ui/table.tsx +0 -116
  171. package/pulse/components/ui/tabs.tsx +0 -91
  172. package/pulse/components/ui/tooltip.tsx +0 -57
  173. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  174. package/pulse/components.json +0 -23
  175. package/pulse/eslint.config.mjs +0 -18
  176. package/pulse/lib/bahulam-paths.ts +0 -23
  177. package/pulse/lib/claude-reader.ts +0 -592
  178. package/pulse/lib/decode.ts +0 -129
  179. package/pulse/lib/pricing.ts +0 -102
  180. package/pulse/lib/replay-parser.ts +0 -165
  181. package/pulse/lib/tool-categories.ts +0 -140
  182. package/pulse/lib/utils.ts +0 -6
  183. package/pulse/next-env.d.ts +0 -6
  184. package/pulse/next.config.ts +0 -16
  185. package/pulse/package.json +0 -45
  186. package/pulse/postcss.config.mjs +0 -7
  187. package/pulse/public/activity.png +0 -0
  188. package/pulse/public/cc-lens.png +0 -0
  189. package/pulse/public/command-k.png +0 -0
  190. package/pulse/public/costs.png +0 -0
  191. package/pulse/public/dashboard-dark.png +0 -0
  192. package/pulse/public/dashboard-white.png +0 -0
  193. package/pulse/public/export.png +0 -0
  194. package/pulse/public/file.svg +0 -1
  195. package/pulse/public/globe.svg +0 -1
  196. package/pulse/public/next.svg +0 -1
  197. package/pulse/public/projects.png +0 -0
  198. package/pulse/public/session-chat.png +0 -0
  199. package/pulse/public/todos.png +0 -0
  200. package/pulse/public/tools.png +0 -0
  201. package/pulse/public/vercel.svg +0 -1
  202. package/pulse/public/window.svg +0 -1
  203. package/pulse/tsconfig.json +0 -34
  204. package/pulse/types/claude.ts +0 -294
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Tool Executor Bridge — maps Tarang backend tool names to OCC tool calls.
2
+ * Tool Executor Bridge — maps Bahulam backend tool names to OCC tool calls.
3
3
  *
4
- * The Tarang backend sends tool_request events with its own tool names and arg shapes.
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 { TarangAuth } from '../auth/tarang-auth.mjs';
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 Tarang tool names to OCC tools.
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 Tarang's { success, output } format.
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 autoLint(filePath) {
506
- const ext = path.extname(filePath);
507
- const cmdFn = LINT_COMMANDS[ext];
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: process.cwd(),
515
- stdio: ['pipe', 'pipe', 'pipe'],
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
- const trimmed = stripAnsi(output).trim();
519
- if (!trimmed) return null;
520
- return trimmed;
521
- } catch (err) {
522
- // Non-zero exit means lint errors found
523
- const output = stripAnsi(err.stderr || err.stdout || '').trim();
524
- if (!output) return null;
525
- return output;
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: filtered.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, args.replace, projectRootFor(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: args.search,
1204
- replace: args.replace,
1205
- replace_all: args.replace_all || false,
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 Python replacement
1277
+ // OCC Edit failed (string not found) — fallback to direct text replacement.
1209
1278
  try {
1210
- const search = args.search.replace(/'/g, "\\'").replace(/\n/g, "\\n");
1211
- const replace = args.replace.replace(/'/g, "\\'").replace(/\n/g, "\\n");
1212
- const pyCmd = `python3 -c "
1213
- import sys
1214
- with open('${filePath}', 'r') as f: content = f.read()
1215
- old = '''${args.search}'''
1216
- new = '''${args.replace}'''
1217
- if old not in content:
1218
- print('ERROR: search string not found in file', file=sys.stderr)
1219
- sys.exit(1)
1220
- content = content.replace(old, new, 1)
1221
- with open('${filePath}', 'w') as f: f.write(content)
1222
- print('OK: replaced')
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
- // ── Tarang-specific tools (no OCC bridge) ──────────────
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 ext = path.extname(filePath);
1612
- let cmd;
1613
- if (ext === '.py') cmd = `python3 -m ruff check "${filePath}" 2>&1 || true`;
1614
- else if (['.js', '.mjs', '.ts', '.tsx'].includes(ext)) cmd = `npx eslint "${filePath}" 2>&1 || true`;
1615
- else return { success: true, issues: [], message: 'No linter for this file type', _tool: 'lint_check' };
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 output = await occRegistry.call('shell', {
1618
- command: cmd,
1700
+ const shellResult = await executeToolWithHooks('shell', {
1701
+ command: lint.command,
1619
1702
  timeout: 30_000,
1620
- description: `Lint: ${path.basename(filePath)}`,
1621
- cwd: projectRootFor(filePath),
1622
- signal: options.signal,
1623
- });
1624
- const rawOutput = typeof output === 'string' ? output : String(output);
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
- return { success: true, output: rawOutput, issues: rawOutput.split('\n').filter(Boolean), _tool: 'lint_check' };
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 TarangAuth().loadCredentials();
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 TarangAuth().loadCredentials();
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 TarangAuth().loadCredentials();
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 TarangAuth().loadCredentials();
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 Tarang tool by name.
2201
- * @param {string} name - Tarang tool 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
  */