@axplusb/kepler 0.0.1 → 1.0.0
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/README.md +82 -0
- package/package.json +36 -4
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +52 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +142 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +159 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +52 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +182 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/claude-reader.ts +594 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- 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 +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- 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 +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +89 -0
- package/src/agents/parser.mjs +98 -0
- package/src/agents/teams.mjs +123 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +277 -0
- package/src/config/cli-args.mjs +173 -0
- package/src/config/env.mjs +263 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +270 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/core/agent-loop.mjs +480 -0
- package/src/core/approval.mjs +273 -0
- package/src/core/backend-url.mjs +57 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +149 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/headless.mjs +168 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +351 -0
- package/src/core/local-agent.mjs +429 -0
- package/src/core/local-store.mjs +325 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +98 -0
- package/src/core/pricing.mjs +314 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/safety.mjs +200 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +317 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +367 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +135 -0
- package/src/core/tool-executor.mjs +725 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +370 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +573 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/loader.mjs +147 -0
- package/src/skills/runner.mjs +55 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +421 -0
- package/src/terminal/main.mjs +150 -0
- package/src/terminal/repl.mjs +1484 -0
- package/src/terminal/tool-display.mjs +58 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +148 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +132 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/banner.mjs +237 -0
- package/src/ui/commands.mjs +499 -0
- package/src/ui/formatter.mjs +379 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/slash-commands.mjs +258 -0
- package/index.js +0 -1
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Executor Bridge — maps Tarang backend tool names to OCC tool calls.
|
|
3
|
+
*
|
|
4
|
+
* The Tarang backend sends tool_request events with its own tool names and arg shapes.
|
|
5
|
+
* This bridge translates those into OCC tool calls and wraps the results.
|
|
6
|
+
*
|
|
7
|
+
* Safety guardrails integrated — prevents destructive operations on source code.
|
|
8
|
+
* 14 tools mapped: 7 OCC-bridged + 7 Tarang-specific.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createToolRegistry } from '../tools/registry.mjs';
|
|
12
|
+
import { filterOutput } from './output-filter.mjs';
|
|
13
|
+
import { validatePath, validateDelete, validateShellCommand, validateWrite } from './safety.mjs';
|
|
14
|
+
import { classifyCommand, isExitCodeError } from '../permissions/command-classifier.mjs';
|
|
15
|
+
import { ContextRetriever } from '../context/retriever.mjs';
|
|
16
|
+
import { analyzeCode } from '../context/ast-parser.mjs';
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import { execSync } from 'node:child_process';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a tool executor that bridges Tarang tool names to OCC tools.
|
|
23
|
+
* @param {Object} [options]
|
|
24
|
+
* @param {ContextRetriever} [options.retriever] - BM25 retriever for search_code
|
|
25
|
+
* @returns {{ execute(name, args): Promise<Object>, listTools(): string[] }}
|
|
26
|
+
*/
|
|
27
|
+
export function createToolExecutor({ retriever } = {}) {
|
|
28
|
+
const occRegistry = createToolRegistry();
|
|
29
|
+
let _searchCodeUsed = false; // tracks if search_code was called (for read_file nudge)
|
|
30
|
+
|
|
31
|
+
// Capture CWD at creation time — OCC Bash tool mutates process.cwd() after shell commands
|
|
32
|
+
const PROJECT_CWD = process.cwd();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a path relative to project CWD, with traversal protection.
|
|
36
|
+
*/
|
|
37
|
+
function resolvePath(p) {
|
|
38
|
+
if (!p) return PROJECT_CWD;
|
|
39
|
+
const resolved = path.resolve(PROJECT_CWD, p);
|
|
40
|
+
// Prevent path traversal outside CWD's parent
|
|
41
|
+
const cwd = process.cwd();
|
|
42
|
+
const cwdParent = path.dirname(cwd);
|
|
43
|
+
if (!resolved.startsWith(cwdParent)) {
|
|
44
|
+
throw new Error(`Path traversal blocked: ${p}`);
|
|
45
|
+
}
|
|
46
|
+
return resolved;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Detect if an OCC tool result string indicates an error.
|
|
51
|
+
*/
|
|
52
|
+
function isError(result) {
|
|
53
|
+
if (typeof result !== 'string') return false;
|
|
54
|
+
return result.startsWith('Error:') || result.startsWith('Error -') ||
|
|
55
|
+
result.includes('Exit code:') && !result.includes('Exit code: 0');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Wrap an OCC string result into Tarang's { success, output } format.
|
|
60
|
+
*/
|
|
61
|
+
function wrapResult(result, toolName) {
|
|
62
|
+
if (typeof result === 'object' && result !== null && 'success' in result) {
|
|
63
|
+
result._tool = toolName;
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
const output = typeof result === 'string' ? result : JSON.stringify(result);
|
|
67
|
+
return {
|
|
68
|
+
success: !isError(output),
|
|
69
|
+
output,
|
|
70
|
+
_tool: toolName,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Auto-lint after file writes ────────────────────────────
|
|
75
|
+
|
|
76
|
+
const LINT_COMMANDS = {
|
|
77
|
+
'.py': (file) => `python3 -m py_compile "${file}" 2>&1`,
|
|
78
|
+
'.js': (file) => `npx eslint --no-eslintrc --rule '{}' "${file}" 2>&1 || true`,
|
|
79
|
+
'.ts': (file) => `npx tsc --noEmit --pretty "${file}" 2>&1 || true`,
|
|
80
|
+
'.tsx': (file) => `npx tsc --noEmit --pretty "${file}" 2>&1 || true`,
|
|
81
|
+
'.go': (file) => `go vet "${file}" 2>&1`,
|
|
82
|
+
'.rs': (file) => `rustfmt --check "${file}" 2>&1`,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function autoLint(filePath) {
|
|
86
|
+
const ext = path.extname(filePath);
|
|
87
|
+
const cmdFn = LINT_COMMANDS[ext];
|
|
88
|
+
if (!cmdFn) return null;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const output = execSync(cmdFn(filePath), {
|
|
92
|
+
encoding: 'utf-8',
|
|
93
|
+
timeout: 15_000,
|
|
94
|
+
cwd: process.cwd(),
|
|
95
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
96
|
+
});
|
|
97
|
+
const trimmed = output.trim();
|
|
98
|
+
if (!trimmed) return null;
|
|
99
|
+
return trimmed;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
// Non-zero exit means lint errors found
|
|
102
|
+
const output = (err.stderr || err.stdout || '').trim();
|
|
103
|
+
if (!output) return null;
|
|
104
|
+
return output;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Tool mapping table ──────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
const toolMap = {
|
|
111
|
+
// 1. shell → Bash + classification + smart output filtering
|
|
112
|
+
shell: async (args) => {
|
|
113
|
+
// Phase 1: legacy safety check (kept for backward compat)
|
|
114
|
+
const shellCheck = validateShellCommand(args.command);
|
|
115
|
+
if (!shellCheck.safe) {
|
|
116
|
+
return {
|
|
117
|
+
success: false,
|
|
118
|
+
output: `BLOCKED: ${shellCheck.reason}. Your current working directory is ${process.cwd()} — search within it, not from filesystem root.`,
|
|
119
|
+
_tool: 'shell', _blocked: true,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Phase 2: command classifier (PRD-050)
|
|
124
|
+
const classification = classifyCommand(args.command);
|
|
125
|
+
if (classification.classification === 'blocked') {
|
|
126
|
+
return {
|
|
127
|
+
success: false,
|
|
128
|
+
output: `BLOCKED: ${classification.reason}`,
|
|
129
|
+
_tool: 'shell', _blocked: true,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Tag for approval/sandbox routing
|
|
134
|
+
if (classification.highRisk || shellCheck.highRisk) {
|
|
135
|
+
args._highRisk = true;
|
|
136
|
+
args._riskReason = classification.reason || shellCheck.reason;
|
|
137
|
+
}
|
|
138
|
+
args._classification = classification.classification; // 'safe' or 'contained'
|
|
139
|
+
|
|
140
|
+
// Pre-check: if command is rm/unlink, verify targets exist first
|
|
141
|
+
const rmMatch = (args.command || '').match(/^rm\s+(?:-\w+\s+)*(.+)$/);
|
|
142
|
+
if (rmMatch) {
|
|
143
|
+
const targets = rmMatch[1].split(/\s+/).filter(t => !t.startsWith('-'));
|
|
144
|
+
const missing = targets.filter(t => {
|
|
145
|
+
try { return !fs.existsSync(path.resolve(process.cwd(), t)); } catch { return true; }
|
|
146
|
+
});
|
|
147
|
+
if (missing.length > 0 && missing.length === targets.length) {
|
|
148
|
+
return {
|
|
149
|
+
success: true,
|
|
150
|
+
output: `No action needed: ${missing.join(', ')} — file(s) do not exist. Do not retry.`,
|
|
151
|
+
exit_code: 0,
|
|
152
|
+
_tool: 'shell',
|
|
153
|
+
_skipped: true,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const result = await occRegistry.call('Bash', {
|
|
159
|
+
command: args.command,
|
|
160
|
+
timeout: args.timeout,
|
|
161
|
+
description: args.description || `Run: ${(args.command || '').slice(0, 50)}`,
|
|
162
|
+
});
|
|
163
|
+
const rawOutput = typeof result === 'string' ? result : String(result);
|
|
164
|
+
const exitMatch = rawOutput.match(/Exit code: (\d+)/);
|
|
165
|
+
const exitCode = exitMatch ? parseInt(exitMatch[1]) : 0;
|
|
166
|
+
// Semantic exit code: grep returns 1 for "no matches" (not an error)
|
|
167
|
+
const success = !isExitCodeError(args.command, exitCode);
|
|
168
|
+
|
|
169
|
+
// Apply smart filtering based on command type
|
|
170
|
+
const filtered = filterOutput(rawOutput, args.command, success);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
success,
|
|
174
|
+
output: filtered.output,
|
|
175
|
+
exit_code: exitCode,
|
|
176
|
+
_tool: 'shell',
|
|
177
|
+
_classification: args._classification,
|
|
178
|
+
_commandType: filtered.commandType,
|
|
179
|
+
_filtered: filtered.truncated || filtered.originalLines !== filtered.filteredLines,
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
// 2. read_file → Read (with smart truncation for large files)
|
|
184
|
+
read_file: async (args) => {
|
|
185
|
+
const filePath = resolvePath(args.file_path || args.path);
|
|
186
|
+
const hasLineRange = args.start_line || args.end_line || args.offset || args.limit;
|
|
187
|
+
|
|
188
|
+
// Nudge: if reading shallow overview files, remind agent to search deeper
|
|
189
|
+
const basename = path.basename(filePath).toLowerCase();
|
|
190
|
+
const isShallowFile = ['readme.md', 'package.json', 'pyproject.toml', 'cargo.toml', 'go.mod'].includes(basename);
|
|
191
|
+
const nudge = isShallowFile && !_searchCodeUsed
|
|
192
|
+
? '\n\nNOTE: You read a top-level overview file. Use search_code(query) to find actual implementations before drawing conclusions. READMEs and package.json do NOT show what features exist in the codebase.'
|
|
193
|
+
: '';
|
|
194
|
+
|
|
195
|
+
// If no line range specified, auto-truncate and return AST summary
|
|
196
|
+
if (!hasLineRange) {
|
|
197
|
+
try {
|
|
198
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
199
|
+
const lines = content.split('\n').length;
|
|
200
|
+
|
|
201
|
+
if (lines > 50) {
|
|
202
|
+
// File >50 lines: return AST summary with line numbers
|
|
203
|
+
// Model must use start_line/end_line to read specific sections
|
|
204
|
+
const analysis = analyzeCode(filePath);
|
|
205
|
+
const firstLines = content.split('\n').slice(0, 20).join('\n');
|
|
206
|
+
return {
|
|
207
|
+
success: true,
|
|
208
|
+
output: `${analysis.summary}\n\n` +
|
|
209
|
+
`## First 20 lines\n${firstLines}${nudge}`,
|
|
210
|
+
_tool: 'read_file',
|
|
211
|
+
_truncated: true,
|
|
212
|
+
_total_lines: lines,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
// Small file (<50 lines): return full content
|
|
216
|
+
} catch { /* let Read handle the error */ }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Convert start_line/end_line to offset/limit
|
|
220
|
+
const offset = args.start_line ? args.start_line - 1 : args.offset;
|
|
221
|
+
const limit = (args.start_line && args.end_line)
|
|
222
|
+
? (args.end_line - args.start_line + 1)
|
|
223
|
+
: args.limit;
|
|
224
|
+
|
|
225
|
+
const result = await occRegistry.call('Read', {
|
|
226
|
+
file_path: filePath,
|
|
227
|
+
offset,
|
|
228
|
+
limit,
|
|
229
|
+
});
|
|
230
|
+
const output = typeof result === 'string' ? result : String(result);
|
|
231
|
+
const content = output.replace(/^\s*\d+[→\t]/gm, '');
|
|
232
|
+
return {
|
|
233
|
+
success: !isError(output),
|
|
234
|
+
content,
|
|
235
|
+
output: output + nudge,
|
|
236
|
+
_tool: 'read_file',
|
|
237
|
+
_output_type: 'file_content',
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// 3. write_file → Write + auto-lint + safety check
|
|
242
|
+
write_file: async (args) => {
|
|
243
|
+
const rawPath = args.file_path || args.path;
|
|
244
|
+
if (!rawPath || rawPath === 'file' || rawPath.length < 3) {
|
|
245
|
+
return { success: false, output: `Error: Invalid file path "${rawPath || ''}". Use an ABSOLUTE path like "${process.cwd()}/src/main.py"`, _tool: 'write_file' };
|
|
246
|
+
}
|
|
247
|
+
const filePath = resolvePath(rawPath);
|
|
248
|
+
const writeCheck = validateWrite(filePath, args.content);
|
|
249
|
+
if (!writeCheck.safe) {
|
|
250
|
+
return { success: false, output: `🛡️ BLOCKED: ${writeCheck.reason}`, _tool: 'write_file', _blocked: true };
|
|
251
|
+
}
|
|
252
|
+
// OCC Write requires Read first for existing files — handle gracefully
|
|
253
|
+
try {
|
|
254
|
+
if (fs.existsSync(filePath)) {
|
|
255
|
+
await occRegistry.call('Read', { file_path: filePath, limit: 1 });
|
|
256
|
+
}
|
|
257
|
+
} catch { /* file may not exist yet */ }
|
|
258
|
+
const result = await occRegistry.call('Write', {
|
|
259
|
+
file_path: filePath,
|
|
260
|
+
content: args.content,
|
|
261
|
+
});
|
|
262
|
+
const wrapped = wrapResult(result, 'write_file');
|
|
263
|
+
|
|
264
|
+
// Auto-lint the written file
|
|
265
|
+
const lintOutput = autoLint(filePath);
|
|
266
|
+
if (lintOutput) {
|
|
267
|
+
wrapped.output += `\n\n--- Lint result ---\n${lintOutput}`;
|
|
268
|
+
wrapped.lint = lintOutput;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return wrapped;
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
// 3b. write_project → Batch write multiple files at once
|
|
275
|
+
write_project: async (args) => {
|
|
276
|
+
const files = args.files || [];
|
|
277
|
+
if (!files.length) {
|
|
278
|
+
return { success: false, output: 'Error: No files provided', _tool: 'write_project' };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const results = [];
|
|
282
|
+
const errors = [];
|
|
283
|
+
|
|
284
|
+
for (const file of files) {
|
|
285
|
+
const rawPath = file.path || file.file_path;
|
|
286
|
+
if (!rawPath) {
|
|
287
|
+
errors.push('Missing path in file entry');
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const filePath = resolvePath(rawPath);
|
|
291
|
+
const content = file.content || '';
|
|
292
|
+
|
|
293
|
+
const writeCheck = validateWrite(filePath, content);
|
|
294
|
+
if (!writeCheck.safe) {
|
|
295
|
+
errors.push(`${rawPath}: BLOCKED — ${writeCheck.reason}`);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
// Ensure parent directory exists
|
|
301
|
+
const dir = path.dirname(filePath);
|
|
302
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
303
|
+
|
|
304
|
+
// Read first if exists (OCC Write requirement)
|
|
305
|
+
try {
|
|
306
|
+
if (fs.existsSync(filePath)) {
|
|
307
|
+
await occRegistry.call('Read', { file_path: filePath, limit: 1 });
|
|
308
|
+
}
|
|
309
|
+
} catch { /* file may not exist yet */ }
|
|
310
|
+
|
|
311
|
+
await occRegistry.call('Write', { file_path: filePath, content });
|
|
312
|
+
results.push(rawPath);
|
|
313
|
+
} catch (err) {
|
|
314
|
+
errors.push(`${rawPath}: ${err.message}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const output = results.length > 0
|
|
319
|
+
? `Created ${results.length} file(s):\n${results.map(f => ` ✓ ${f}`).join('\n')}`
|
|
320
|
+
: 'No files written';
|
|
321
|
+
|
|
322
|
+
if (errors.length > 0) {
|
|
323
|
+
return {
|
|
324
|
+
success: results.length > 0,
|
|
325
|
+
output: `${output}\n\nErrors:\n${errors.map(e => ` ✗ ${e}`).join('\n')}`,
|
|
326
|
+
files_written: results,
|
|
327
|
+
files_failed: errors,
|
|
328
|
+
_tool: 'write_project',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return { success: true, output, files_written: results, _tool: 'write_project' };
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
// 4. edit_file → Edit + auto-lint + auto-fallback to sed
|
|
336
|
+
edit_file: async (args) => {
|
|
337
|
+
const rawPath = args.file_path || args.path;
|
|
338
|
+
const filePath = resolvePath(rawPath);
|
|
339
|
+
// OCC Edit requires Read first
|
|
340
|
+
try {
|
|
341
|
+
await occRegistry.call('Read', { file_path: filePath, limit: 1 });
|
|
342
|
+
} catch { /* best effort */ }
|
|
343
|
+
|
|
344
|
+
let result;
|
|
345
|
+
try {
|
|
346
|
+
result = await occRegistry.call('Edit', {
|
|
347
|
+
file_path: filePath,
|
|
348
|
+
old_string: args.search,
|
|
349
|
+
new_string: args.replace,
|
|
350
|
+
replace_all: args.replace_all || false,
|
|
351
|
+
});
|
|
352
|
+
} catch (editErr) {
|
|
353
|
+
// OCC Edit failed (string not found) — fallback to Python replacement
|
|
354
|
+
try {
|
|
355
|
+
const search = args.search.replace(/'/g, "\\'").replace(/\n/g, "\\n");
|
|
356
|
+
const replace = args.replace.replace(/'/g, "\\'").replace(/\n/g, "\\n");
|
|
357
|
+
const pyCmd = `python3 -c "
|
|
358
|
+
import sys
|
|
359
|
+
with open('${filePath}', 'r') as f: content = f.read()
|
|
360
|
+
old = '''${args.search}'''
|
|
361
|
+
new = '''${args.replace}'''
|
|
362
|
+
if old not in content:
|
|
363
|
+
print('ERROR: search string not found in file', file=sys.stderr)
|
|
364
|
+
sys.exit(1)
|
|
365
|
+
content = content.replace(old, new, 1)
|
|
366
|
+
with open('${filePath}', 'w') as f: f.write(content)
|
|
367
|
+
print('OK: replaced')
|
|
368
|
+
"`;
|
|
369
|
+
const fallbackResult = execSync(pyCmd, { encoding: 'utf-8', timeout: 5000, cwd: PROJECT_CWD });
|
|
370
|
+
result = `Edited ${filePath} (via fallback): ${fallbackResult.trim()}`;
|
|
371
|
+
} catch (sedErr) {
|
|
372
|
+
return { success: false, output: `edit_file failed: ${editErr?.message || 'unknown'}. Fallback also failed: ${sedErr?.message || 'unknown'}. Try shell(sed) manually.`, _tool: 'edit_file' };
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const wrapped = wrapResult(result, 'edit_file');
|
|
377
|
+
|
|
378
|
+
// Auto-lint the edited file
|
|
379
|
+
const lintOutput = autoLint(filePath);
|
|
380
|
+
if (lintOutput) {
|
|
381
|
+
wrapped.output += `\n\n--- Lint result ---\n${lintOutput}`;
|
|
382
|
+
wrapped.lint = lintOutput;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return wrapped;
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
// 5. list_files → Glob
|
|
389
|
+
list_files: async (args) => {
|
|
390
|
+
const result = await occRegistry.call('Glob', {
|
|
391
|
+
pattern: args.pattern || '**/*',
|
|
392
|
+
path: args.path ? resolvePath(args.path) : undefined,
|
|
393
|
+
});
|
|
394
|
+
const output = typeof result === 'string' ? result : String(result);
|
|
395
|
+
const files = output.split('\n').filter(Boolean);
|
|
396
|
+
return {
|
|
397
|
+
success: true,
|
|
398
|
+
files,
|
|
399
|
+
output,
|
|
400
|
+
_tool: 'list_files',
|
|
401
|
+
};
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
// 6. search_code → combined rg + BM25 for best results
|
|
405
|
+
search_code: async (args) => {
|
|
406
|
+
_searchCodeUsed = true;
|
|
407
|
+
const query = args.query || args.pattern;
|
|
408
|
+
if (!query) return { success: false, output: 'query required', _tool: 'search_code' };
|
|
409
|
+
|
|
410
|
+
const searchPath = args.path ? resolvePath(args.path) : PROJECT_CWD;
|
|
411
|
+
const parts = [];
|
|
412
|
+
|
|
413
|
+
// Layer 1: ripgrep — exact text matches with context
|
|
414
|
+
try {
|
|
415
|
+
const cmd = `rg -n -C 1 --max-count 5 --max-filesize 500K -e ${JSON.stringify(query)} ${JSON.stringify(searchPath)} 2>/dev/null | head -60`;
|
|
416
|
+
const rgOutput = execSync(cmd, { encoding: 'utf-8', timeout: 15000, cwd: searchPath }).trim();
|
|
417
|
+
if (rgOutput) {
|
|
418
|
+
parts.push(`## Exact matches (rg)\n${rgOutput}`);
|
|
419
|
+
}
|
|
420
|
+
} catch { /* rg not found or no results */ }
|
|
421
|
+
|
|
422
|
+
// Layer 2: BM25 — semantic relevance (finds related code even without exact match)
|
|
423
|
+
if (retriever) {
|
|
424
|
+
if (!retriever.index) retriever.loadIndex();
|
|
425
|
+
const chunks = retriever.retrieve(query, 5);
|
|
426
|
+
if (chunks.length > 0) {
|
|
427
|
+
const bm25Output = chunks.map(c => {
|
|
428
|
+
const score = c.score?.toFixed(2) || '?';
|
|
429
|
+
return `── ${c.id} (score: ${score}) ──\n${c.text}`;
|
|
430
|
+
}).join('\n\n');
|
|
431
|
+
parts.push(`## Related code (BM25)\n${bm25Output}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Return combined results
|
|
436
|
+
if (parts.length > 0) {
|
|
437
|
+
return {
|
|
438
|
+
success: true,
|
|
439
|
+
output: parts.join('\n\n'),
|
|
440
|
+
_tool: 'search_code',
|
|
441
|
+
_method: parts.length > 1 ? 'rg+bm25' : (parts[0].startsWith('## Exact') ? 'rg' : 'bm25'),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Nothing found — actionable hint
|
|
446
|
+
const firstWord = query.split(/\s+/)[0];
|
|
447
|
+
return {
|
|
448
|
+
success: true,
|
|
449
|
+
output: `No results for "${query}" in ${searchPath}.\n` +
|
|
450
|
+
`Try: shell(grep -rn "${firstWord}" . --include="*.py" | head -20)`,
|
|
451
|
+
_tool: 'search_code',
|
|
452
|
+
_method: 'none',
|
|
453
|
+
};
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
// 7. search_files → Grep with line numbers + context (like grep -n -C 3)
|
|
457
|
+
search_files: async (args) => {
|
|
458
|
+
const query = args.query || args.pattern || '*';
|
|
459
|
+
|
|
460
|
+
// If it looks like a glob pattern, use Glob
|
|
461
|
+
if (query.includes('*') || query.includes('?')) {
|
|
462
|
+
const result = await occRegistry.call('Glob', {
|
|
463
|
+
pattern: query,
|
|
464
|
+
path: args.path ? resolvePath(args.path) : undefined,
|
|
465
|
+
});
|
|
466
|
+
const output = typeof result === 'string' ? result : String(result);
|
|
467
|
+
return {
|
|
468
|
+
success: true,
|
|
469
|
+
files: output.split('\n').filter(Boolean),
|
|
470
|
+
output,
|
|
471
|
+
_tool: 'search_files',
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// For text patterns: grep with context lines (like grep -n -C 3)
|
|
476
|
+
const result = await occRegistry.call('Grep', {
|
|
477
|
+
pattern: query,
|
|
478
|
+
path: args.path ? resolvePath(args.path) : undefined,
|
|
479
|
+
output_mode: 'content',
|
|
480
|
+
'-n': true,
|
|
481
|
+
'-C': 3,
|
|
482
|
+
head_limit: 50,
|
|
483
|
+
});
|
|
484
|
+
const output = typeof result === 'string' ? result : String(result);
|
|
485
|
+
return {
|
|
486
|
+
success: true,
|
|
487
|
+
files: output.split('\n').filter(Boolean),
|
|
488
|
+
output,
|
|
489
|
+
_tool: 'search_files',
|
|
490
|
+
};
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
// 7b. grep → dedicated ripgrep tool (fast text/regex search)
|
|
494
|
+
grep: async (args) => {
|
|
495
|
+
const pattern = args.pattern;
|
|
496
|
+
if (!pattern) return { success: false, output: 'pattern required', _tool: 'grep' };
|
|
497
|
+
|
|
498
|
+
const searchPath = args.path ? resolvePath(args.path) : PROJECT_CWD;
|
|
499
|
+
const includeFlag = args.include ? `--glob "${args.include}"` : '';
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const cmd = `rg -n -C 2 --max-count 10 --max-filesize 500K ${includeFlag} -e ${JSON.stringify(pattern)} ${JSON.stringify(searchPath)} 2>/dev/null | head -80`;
|
|
503
|
+
const output = execSync(cmd, { encoding: 'utf-8', timeout: 15000, cwd: PROJECT_CWD }).trim();
|
|
504
|
+
if (output) {
|
|
505
|
+
return { success: true, output, _tool: 'grep' };
|
|
506
|
+
}
|
|
507
|
+
} catch { /* no results or rg not found */ }
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
success: true,
|
|
511
|
+
output: `No matches for "${pattern}" in ${searchPath}`,
|
|
512
|
+
_tool: 'grep',
|
|
513
|
+
};
|
|
514
|
+
},
|
|
515
|
+
|
|
516
|
+
// ── Tarang-specific tools (no OCC bridge) ──────────────
|
|
517
|
+
|
|
518
|
+
// 8. read_files → batch Read (with AST truncation for large files)
|
|
519
|
+
read_files: async (args) => {
|
|
520
|
+
const paths = args.file_paths || args.paths || [];
|
|
521
|
+
const results = [];
|
|
522
|
+
for (const p of paths) {
|
|
523
|
+
try {
|
|
524
|
+
const filePath = resolvePath(p);
|
|
525
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
526
|
+
const lines = content.split('\n').length;
|
|
527
|
+
|
|
528
|
+
if (lines > 50) {
|
|
529
|
+
// Large file: return AST summary instead of full content
|
|
530
|
+
const analysis = analyzeCode(filePath);
|
|
531
|
+
results.push({
|
|
532
|
+
path: p, lines,
|
|
533
|
+
content: analysis.summary,
|
|
534
|
+
_truncated: true,
|
|
535
|
+
success: true,
|
|
536
|
+
});
|
|
537
|
+
} else {
|
|
538
|
+
results.push({ path: p, content, success: true });
|
|
539
|
+
}
|
|
540
|
+
} catch (err) {
|
|
541
|
+
results.push({ path: p, error: err.message, success: false });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return { success: true, files: results, _tool: 'read_files' };
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
// 9. delete_file + safety check
|
|
548
|
+
delete_file: async (args) => {
|
|
549
|
+
try {
|
|
550
|
+
const filePath = resolvePath(args.file_path || args.path);
|
|
551
|
+
const delCheck = validateDelete(filePath);
|
|
552
|
+
if (!delCheck.safe) {
|
|
553
|
+
return { success: false, output: `🛡️ BLOCKED: ${delCheck.reason}`, _tool: 'delete_file', _blocked: true };
|
|
554
|
+
}
|
|
555
|
+
fs.unlinkSync(filePath);
|
|
556
|
+
return { success: true, message: `Deleted ${args.path}`, _tool: 'delete_file' };
|
|
557
|
+
} catch (err) {
|
|
558
|
+
return { success: false, output: `Error: ${err.message}`, _tool: 'delete_file' };
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
// 10. get_file_info
|
|
563
|
+
get_file_info: async (args) => {
|
|
564
|
+
try {
|
|
565
|
+
const filePath = resolvePath(args.file_path || args.path);
|
|
566
|
+
const stat = fs.statSync(filePath);
|
|
567
|
+
return {
|
|
568
|
+
success: true,
|
|
569
|
+
size: stat.size,
|
|
570
|
+
mtime: stat.mtime.toISOString(),
|
|
571
|
+
type: stat.isDirectory() ? 'directory' : 'file',
|
|
572
|
+
mode: stat.mode.toString(8),
|
|
573
|
+
_tool: 'get_file_info',
|
|
574
|
+
};
|
|
575
|
+
} catch (err) {
|
|
576
|
+
return { success: false, output: `Error: ${err.message}`, _tool: 'get_file_info' };
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
|
|
580
|
+
// 11. validate_file (syntax check)
|
|
581
|
+
validate_file: async (args) => {
|
|
582
|
+
try {
|
|
583
|
+
const filePath = resolvePath(args.path);
|
|
584
|
+
const ext = path.extname(filePath);
|
|
585
|
+
let cmd;
|
|
586
|
+
if (ext === '.py') cmd = `python3 -m py_compile "${filePath}"`;
|
|
587
|
+
else if (ext === '.js' || ext === '.mjs') cmd = `node --check "${filePath}"`;
|
|
588
|
+
else return { success: true, valid: true, message: 'No validator for this file type', _tool: 'validate_file' };
|
|
589
|
+
|
|
590
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
591
|
+
return { success: true, valid: true, _tool: 'validate_file' };
|
|
592
|
+
} catch (err) {
|
|
593
|
+
return { success: true, valid: false, errors: err.stderr?.toString() || err.message, _tool: 'validate_file' };
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
|
|
597
|
+
// 12. validate_build
|
|
598
|
+
validate_build: async (args) => {
|
|
599
|
+
try {
|
|
600
|
+
let cmd = args.command;
|
|
601
|
+
if (!cmd) {
|
|
602
|
+
if (fs.existsSync('package.json')) cmd = 'npm run build';
|
|
603
|
+
else if (fs.existsSync('Makefile')) cmd = 'make';
|
|
604
|
+
else if (fs.existsSync('Cargo.toml')) cmd = 'cargo build';
|
|
605
|
+
else return { success: false, output: 'No build system detected', _tool: 'validate_build' };
|
|
606
|
+
}
|
|
607
|
+
const output = execSync(cmd, { stdio: 'pipe', timeout: 120_000 }).toString();
|
|
608
|
+
return { success: true, output, _tool: 'validate_build' };
|
|
609
|
+
} catch (err) {
|
|
610
|
+
return { success: false, output: err.stderr?.toString() || err.message, _tool: 'validate_build' };
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
|
|
614
|
+
// 13. validate_structure
|
|
615
|
+
validate_structure: async (args) => {
|
|
616
|
+
const expected = args.expected || [];
|
|
617
|
+
const missing = expected.filter(f => !fs.existsSync(resolvePath(f)));
|
|
618
|
+
return {
|
|
619
|
+
success: missing.length === 0,
|
|
620
|
+
missing,
|
|
621
|
+
checked: expected.length,
|
|
622
|
+
_tool: 'validate_structure',
|
|
623
|
+
};
|
|
624
|
+
},
|
|
625
|
+
|
|
626
|
+
// 14. lint_check
|
|
627
|
+
lint_check: async (args) => {
|
|
628
|
+
try {
|
|
629
|
+
const filePath = resolvePath(args.file_path || args.path);
|
|
630
|
+
const ext = path.extname(filePath);
|
|
631
|
+
let cmd;
|
|
632
|
+
if (ext === '.py') cmd = `python3 -m ruff check "${filePath}" 2>&1 || true`;
|
|
633
|
+
else if (['.js', '.mjs', '.ts', '.tsx'].includes(ext)) cmd = `npx eslint "${filePath}" 2>&1 || true`;
|
|
634
|
+
else return { success: true, issues: [], message: 'No linter for this file type', _tool: 'lint_check' };
|
|
635
|
+
|
|
636
|
+
const output = execSync(cmd, { stdio: 'pipe', timeout: 30_000 }).toString();
|
|
637
|
+
return { success: true, output, issues: output.split('\n').filter(Boolean), _tool: 'lint_check' };
|
|
638
|
+
} catch (err) {
|
|
639
|
+
return { success: false, output: err.message, _tool: 'lint_check' };
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
// 15. run_tests
|
|
644
|
+
run_tests: async (args) => {
|
|
645
|
+
try {
|
|
646
|
+
const cmd = args.command || 'npm test';
|
|
647
|
+
const output = execSync(cmd, {
|
|
648
|
+
stdio: 'pipe', timeout: 120_000, cwd: process.cwd(),
|
|
649
|
+
encoding: 'utf-8',
|
|
650
|
+
}).toString();
|
|
651
|
+
return { success: true, output: output.slice(-3000), _tool: 'run_tests' };
|
|
652
|
+
} catch (err) {
|
|
653
|
+
const output = (err.stdout || '') + (err.stderr || '');
|
|
654
|
+
return { success: false, output: output.slice(-3000), exit_code: err.status, _tool: 'run_tests' };
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
|
|
658
|
+
// 16. git_diff
|
|
659
|
+
git_diff: async (args) => {
|
|
660
|
+
try {
|
|
661
|
+
const filePath = args.file_path ? `-- "${args.file_path}"` : '';
|
|
662
|
+
const output = execSync(`git diff ${filePath}`, {
|
|
663
|
+
stdio: 'pipe', timeout: 10_000, cwd: process.cwd(), encoding: 'utf-8',
|
|
664
|
+
}).toString();
|
|
665
|
+
return { success: true, output: output.slice(-5000) || '(no changes)', _tool: 'git_diff' };
|
|
666
|
+
} catch (err) {
|
|
667
|
+
return { success: false, output: err.message, _tool: 'git_diff' };
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
|
|
671
|
+
// 17. git_status
|
|
672
|
+
git_status: async (args) => {
|
|
673
|
+
try {
|
|
674
|
+
const output = execSync('git status --short', {
|
|
675
|
+
stdio: 'pipe', timeout: 10_000, cwd: process.cwd(), encoding: 'utf-8',
|
|
676
|
+
}).toString();
|
|
677
|
+
return { success: true, output: output || '(clean)', _tool: 'git_status' };
|
|
678
|
+
} catch (err) {
|
|
679
|
+
return { success: false, output: err.message, _tool: 'git_status' };
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
|
|
683
|
+
// 18. analyze_code — AST-based structured code analysis
|
|
684
|
+
// Returns function signatures, classes, imports instead of raw file contents
|
|
685
|
+
// 10x more token-efficient than read_file
|
|
686
|
+
analyze_code: async (args) => {
|
|
687
|
+
const filePath = resolvePath(args.file_path || args.path);
|
|
688
|
+
const result = analyzeCode(filePath, {
|
|
689
|
+
startLine: args.start_line,
|
|
690
|
+
endLine: args.end_line,
|
|
691
|
+
});
|
|
692
|
+
return {
|
|
693
|
+
success: result.success,
|
|
694
|
+
output: result.summary,
|
|
695
|
+
structure: result.structure,
|
|
696
|
+
_tool: 'analyze_code',
|
|
697
|
+
};
|
|
698
|
+
},
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
/**
|
|
703
|
+
* Execute a Tarang tool by name.
|
|
704
|
+
* @param {string} name - Tarang tool name
|
|
705
|
+
* @param {Object} args - Tool arguments
|
|
706
|
+
* @returns {Promise<Object>} - { success, output, ... }
|
|
707
|
+
*/
|
|
708
|
+
async execute(name, args) {
|
|
709
|
+
const handler = toolMap[name];
|
|
710
|
+
if (!handler) {
|
|
711
|
+
return { success: false, output: `Unknown tool: ${name}`, _tool: name };
|
|
712
|
+
}
|
|
713
|
+
try {
|
|
714
|
+
return await handler(args);
|
|
715
|
+
} catch (err) {
|
|
716
|
+
return { success: false, output: `Tool error (${name}): ${err.message}`, _tool: name };
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
|
|
720
|
+
/** List all available tool names. */
|
|
721
|
+
listTools() {
|
|
722
|
+
return Object.keys(toolMap);
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
}
|