@bahulam/code 2.6.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 +80 -0
- package/package.json +49 -0
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -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 +50 -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/benchmarks/page.tsx +224 -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 +143 -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 +164 -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 +53 -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 +183 -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/bahulam-paths.ts +23 -0
- package/pulse/lib/claude-reader.ts +592 -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 +94 -0
- package/src/agents/multi_workflow_loader.mjs +330 -0
- package/src/agents/parser.mjs +205 -0
- package/src/agents/scaffold.mjs +222 -0
- package/src/agents/teams.mjs +123 -0
- package/src/agents/workflow_loader.mjs +122 -0
- package/src/agents/workflow_scaffold.mjs +249 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +306 -0
- package/src/commands/agent.mjs +220 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +200 -0
- package/src/config/env.mjs +263 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -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 +308 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/agent-history.mjs +111 -0
- package/src/core/agent-loop.mjs +486 -0
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +476 -0
- package/src/core/attachments.mjs +380 -0
- package/src/core/backend-url.mjs +55 -0
- package/src/core/cache-control.mjs +92 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +180 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/error-guidance.mjs +311 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +448 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +537 -0
- package/src/core/local-store.mjs +836 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +190 -0
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/pricing.mjs +336 -0
- package/src/core/project-artifacts.mjs +39 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limit-display.mjs +121 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/resume-mode.mjs +192 -0
- package/src/core/risk-tier.mjs +337 -0
- package/src/core/safety.mjs +203 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +360 -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 +829 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +140 -0
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +1950 -0
- package/src/core/trust.mjs +158 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +426 -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/onboarding/preflight.mjs +360 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +652 -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/installer.mjs +188 -0
- package/src/skills/loader.mjs +252 -0
- package/src/skills/runner.mjs +55 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -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 +695 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +269 -0
- package/src/terminal/repl-explore.mjs +35 -0
- package/src/terminal/repl-format.mjs +257 -0
- package/src/terminal/repl-render.mjs +561 -0
- package/src/terminal/repl-resume.mjs +625 -0
- package/src/terminal/repl-state.mjs +103 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +3832 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +240 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +231 -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/project-overview.mjs +641 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +116 -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/approval.mjs +263 -0
- package/src/ui/banner.mjs +235 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/input-dock.mjs +444 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/mission-report.mjs +296 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/slash-commands.mjs +245 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +463 -0
- package/src/ui/tool-details.mjs +312 -0
- package/src/ui/transcript-block.mjs +21 -0
|
@@ -0,0 +1,1950 @@
|
|
|
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
|
+
* Tools are mapped across file, search, shell, validation, and Git operations.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createToolRegistry } from '../tools/registry.mjs';
|
|
12
|
+
import { detectCommandType, 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 { analyzeCode } from '../context/ast-parser.mjs';
|
|
16
|
+
import { ProjectRegistry } from '../tools/project-overview.mjs';
|
|
17
|
+
import { SkillInstaller } from '../skills/installer.mjs';
|
|
18
|
+
import { SkillsLoader } from '../skills/loader.mjs';
|
|
19
|
+
import { createAgentFile, listLocalAgents, syncAgentsToBackend } from '../agents/scaffold.mjs';
|
|
20
|
+
import { createWorkflowFile, listLocalWorkflows, WORKFLOW_SYNC_ENDPOINT, slugifyWorkflowName } from '../agents/workflow_scaffold.mjs';
|
|
21
|
+
import { TarangAuth } from '../auth/tarang-auth.mjs';
|
|
22
|
+
import { streamResponse } from './streaming.mjs';
|
|
23
|
+
import { sendApprovalDecision, sendCallback } from './callback-client.mjs';
|
|
24
|
+
import { HookRunner } from '../config/hook-runner.mjs';
|
|
25
|
+
import { buildFileDiff } from './file-diff.mjs';
|
|
26
|
+
import { buildWorkScope } from './work-scope.mjs';
|
|
27
|
+
import * as fs from 'node:fs';
|
|
28
|
+
import * as os from 'node:os';
|
|
29
|
+
import * as path from 'node:path';
|
|
30
|
+
import { execSync } from 'node:child_process';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a tool executor that bridges Tarang tool names to OCC tools.
|
|
34
|
+
* @param {Object} [options]
|
|
35
|
+
* @param {ProjectRegistry} [options.projectRegistry] - session-owned project registry
|
|
36
|
+
* @returns {{ execute(name, args): Promise<Object>, listTools(): string[] }}
|
|
37
|
+
*/
|
|
38
|
+
export function createToolExecutor({
|
|
39
|
+
projectRegistry = new ProjectRegistry(),
|
|
40
|
+
skillsLoader = new SkillsLoader().load(process.cwd()),
|
|
41
|
+
skillInstaller = null,
|
|
42
|
+
checkpoints = null,
|
|
43
|
+
hookRunner = null,
|
|
44
|
+
} = {}) {
|
|
45
|
+
const occRegistry = createToolRegistry();
|
|
46
|
+
const skillTool = occRegistry.get('Skill');
|
|
47
|
+
if (skillTool) skillTool._skillsLoader = skillsLoader;
|
|
48
|
+
const installer = skillInstaller || new SkillInstaller({
|
|
49
|
+
cwd: process.cwd(),
|
|
50
|
+
homeDir: skillsLoader.homeDir || os.homedir(),
|
|
51
|
+
});
|
|
52
|
+
let _searchCodeUsed = false; // tracks if search_code was called (for read_file nudge)
|
|
53
|
+
let _readOnlyCacheGeneration = 0;
|
|
54
|
+
const readOnlyResultCache = new Map();
|
|
55
|
+
|
|
56
|
+
function resolvePath(p, args = {}, options = {}) {
|
|
57
|
+
return projectRegistry.resolvePath(p, args.project_id, options);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function projectRootFor(filePath) {
|
|
61
|
+
const project = projectRegistry.projectForPath(filePath);
|
|
62
|
+
if (!project) throw new Error(`No registered project contains path: ${filePath}`);
|
|
63
|
+
return project.resource.root;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function commandCwd(args = {}) {
|
|
67
|
+
return await resolvePath(args.cwd || null, args);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function shellTargetPath(cwd, target) {
|
|
71
|
+
const value = String(target || '');
|
|
72
|
+
if (value === '~') return os.homedir();
|
|
73
|
+
if (value.startsWith('~/')) return path.join(os.homedir(), value.slice(2));
|
|
74
|
+
return path.resolve(cwd, value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function longRunningObservationTimeoutMs() {
|
|
78
|
+
const configured = Number(process.env.KEPLER_LONG_RUNNING_TIMEOUT_MS);
|
|
79
|
+
return Number.isFinite(configured) && configured > 0 ? configured : 15_000;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isLikelyLongRunningCommand(command) {
|
|
83
|
+
const cmd = String(command || '').trim();
|
|
84
|
+
if (!cmd) return false;
|
|
85
|
+
if (/^(?:timeout|gtimeout)\s+\S+\s+/i.test(cmd)) return false;
|
|
86
|
+
if (/(?:^|[;&|]\s*)(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|preview)\b/i.test(cmd)) return true;
|
|
87
|
+
if (/(?:^|[;&|]\s*)(?:vite|next|nuxt|astro|webpack-dev-server)\b/i.test(cmd)) return true;
|
|
88
|
+
if (/\b(?:uvicorn|gunicorn|flask\s+run|rails\s+server|bin\/rails\s+server|django-admin\s+runserver|manage\.py\s+runserver)\b/i.test(cmd)) return true;
|
|
89
|
+
if (/\b(?:python|python3)\s+-m\s+http\.server\b/i.test(cmd)) return true;
|
|
90
|
+
if (/\bnode\b[\s\S]*(?:setInterval|\.listen\s*\(|createServer\s*\()/i.test(cmd)) return true;
|
|
91
|
+
if (/\b(?:docker\s+compose|docker-compose)\s+up\b(?![\s\S]*\s-d\b)/i.test(cmd)) return true;
|
|
92
|
+
if (/\btail\s+-f\b/i.test(cmd)) return true;
|
|
93
|
+
if (/\b(?:--watch|watch)\b/i.test(cmd)) return true;
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function limitTail(text, maxChars = 8000) {
|
|
98
|
+
const value = String(text || '');
|
|
99
|
+
if (value.length <= maxChars) return value;
|
|
100
|
+
return `... (tail truncated)\n${value.slice(value.length - maxChars)}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isAbortError(err) {
|
|
104
|
+
return err?.name === 'AbortError' || err?.code === 'ABORT_ERR';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function throwIfAborted(signal) {
|
|
108
|
+
if (!signal?.aborted) return;
|
|
109
|
+
const err = new Error('Cancelled by user');
|
|
110
|
+
err.name = 'AbortError';
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function cancelledToolResult(name) {
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
output: 'Cancelled by user',
|
|
118
|
+
_tool: name,
|
|
119
|
+
_cancelled: true,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function skillScope(args = {}) {
|
|
124
|
+
const scope = String(args.scope || '').trim();
|
|
125
|
+
if (scope !== 'project' && scope !== 'global') {
|
|
126
|
+
throw new Error('scope must be "project" or "global"');
|
|
127
|
+
}
|
|
128
|
+
return scope;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function reloadSkillCatalog() {
|
|
132
|
+
skillsLoader.load(installer.cwd || process.cwd());
|
|
133
|
+
return skillsLoader.list();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeAgentTools(tools) {
|
|
137
|
+
if (Array.isArray(tools) && tools.length > 0) {
|
|
138
|
+
return tools.map(tool => String(tool).trim()).filter(Boolean);
|
|
139
|
+
}
|
|
140
|
+
if (typeof tools === 'string' && tools.trim()) {
|
|
141
|
+
return tools.split(',').map(tool => tool.trim()).filter(Boolean);
|
|
142
|
+
}
|
|
143
|
+
return ['read_file', 'search_code', 'list_files'];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function agentMatches(agent, query) {
|
|
147
|
+
const needle = String(query || '').trim().toLowerCase();
|
|
148
|
+
if (!needle) return true;
|
|
149
|
+
return [
|
|
150
|
+
agent.slug,
|
|
151
|
+
agent.name,
|
|
152
|
+
agent.description,
|
|
153
|
+
agent.role,
|
|
154
|
+
agent.model,
|
|
155
|
+
...(Array.isArray(agent.tools) ? agent.tools : []),
|
|
156
|
+
...(Array.isArray(agent.capabilities) ? agent.capabilities : []),
|
|
157
|
+
...(Array.isArray(agent.domains) ? agent.domains : []),
|
|
158
|
+
].some(value => String(value || '').toLowerCase().includes(needle));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function compactAgentMetadata(agent) {
|
|
162
|
+
return {
|
|
163
|
+
slug: agent.slug,
|
|
164
|
+
name: agent.name,
|
|
165
|
+
description: agent.description || '',
|
|
166
|
+
role: agent.role || 'specialist',
|
|
167
|
+
model: agent.model || null,
|
|
168
|
+
models: agent.models && Object.keys(agent.models).length ? agent.models : undefined,
|
|
169
|
+
tools: Array.isArray(agent.tools) ? agent.tools : [],
|
|
170
|
+
capabilities: Array.isArray(agent.capabilities) ? agent.capabilities : [],
|
|
171
|
+
domains: Array.isArray(agent.domains) ? agent.domains : [],
|
|
172
|
+
source_scope: agent.source_scope || 'unknown',
|
|
173
|
+
source: agent.source || '',
|
|
174
|
+
content_hash: agent.content_hash || '',
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function filterLocalAgents(args = {}) {
|
|
179
|
+
const scope = String(args.scope || '').trim();
|
|
180
|
+
if (scope && scope !== 'project' && scope !== 'global') {
|
|
181
|
+
throw new Error('scope must be "project" or "global"');
|
|
182
|
+
}
|
|
183
|
+
return listLocalAgents(process.cwd())
|
|
184
|
+
.filter(agent => !scope || agent.source_scope === scope)
|
|
185
|
+
.filter(agent => agentMatches(agent, args.query || args.name || ''));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function selectAgentsForSync(args = {}) {
|
|
189
|
+
const target = String(args.name || args.slug || '').trim();
|
|
190
|
+
const agents = listLocalAgents(process.cwd());
|
|
191
|
+
if (!target) return agents;
|
|
192
|
+
return agents.filter(agent => (
|
|
193
|
+
agent.slug === target ||
|
|
194
|
+
String(agent.name || '').toLowerCase() === target.toLowerCase()
|
|
195
|
+
));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function compactWorkflowMetadata(workflow) {
|
|
199
|
+
return {
|
|
200
|
+
slug: workflow.slug,
|
|
201
|
+
name: workflow.name,
|
|
202
|
+
description: workflow.description || '',
|
|
203
|
+
pattern: workflow.pattern || workflow.orchestration_pattern || 'sequential',
|
|
204
|
+
agent_count: workflow.agent_count || (workflow.graph?.nodes || []).filter(node => node.type === 'agent').length,
|
|
205
|
+
edge_count: workflow.edge_count || (workflow.graph?.edges || []).length,
|
|
206
|
+
source: workflow.filePath || workflow.source || '',
|
|
207
|
+
source_scope: workflow.source_scope || 'project',
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function filterLocalWorkflows(args = {}) {
|
|
212
|
+
const query = String(args.query || args.name || args.slug || '').trim().toLowerCase();
|
|
213
|
+
return listLocalWorkflows(process.cwd())
|
|
214
|
+
.filter(workflow => {
|
|
215
|
+
if (!query) return true;
|
|
216
|
+
return [
|
|
217
|
+
workflow.slug,
|
|
218
|
+
workflow.name,
|
|
219
|
+
workflow.description,
|
|
220
|
+
workflow.pattern,
|
|
221
|
+
...(Array.isArray(workflow.agents) ? workflow.agents.map(a => a.slug || a.label || a.name) : []),
|
|
222
|
+
].some(value => String(value || '').toLowerCase().includes(query));
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function selectWorkflowsForSync(args = {}) {
|
|
227
|
+
const target = String(args.name || args.slug || '').trim();
|
|
228
|
+
const workflows = listLocalWorkflows(process.cwd());
|
|
229
|
+
if (!target) return workflows;
|
|
230
|
+
return workflows.filter(workflow => (
|
|
231
|
+
workflow.slug === target ||
|
|
232
|
+
String(workflow.name || '').toLowerCase() === target.toLowerCase()
|
|
233
|
+
));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function workflowTargetMatches(workflow, target) {
|
|
237
|
+
const needle = String(target || '').trim().toLowerCase();
|
|
238
|
+
if (!needle) return false;
|
|
239
|
+
return [
|
|
240
|
+
workflow.id,
|
|
241
|
+
workflow.slug,
|
|
242
|
+
workflow.name,
|
|
243
|
+
].some(value => String(value || '').toLowerCase() === needle);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function resolveWorkflowId(creds, target) {
|
|
247
|
+
const trimmed = String(target || '').trim();
|
|
248
|
+
if (!trimmed) return null;
|
|
249
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(trimmed)) {
|
|
250
|
+
return trimmed;
|
|
251
|
+
}
|
|
252
|
+
if (!creds.backendUrl || !creds.token) return null;
|
|
253
|
+
const resp = await fetch(`${creds.backendUrl}${WORKFLOW_SYNC_ENDPOINT}`, {
|
|
254
|
+
headers: {
|
|
255
|
+
Authorization: `Bearer ${creds.token}`,
|
|
256
|
+
Accept: 'application/json',
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
if (!resp.ok) return null;
|
|
260
|
+
const payload = await resp.json().catch(() => ({}));
|
|
261
|
+
const workflows = Array.isArray(payload.workflows) ? payload.workflows : [];
|
|
262
|
+
const match = workflows.find(workflow => workflowTargetMatches(workflow, trimmed));
|
|
263
|
+
return match?.id || null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function formatObservationTimeoutOutput(rawOutput, timeoutMs) {
|
|
267
|
+
const tail = String(rawOutput || '')
|
|
268
|
+
.replace(/^Error:\s*Command timed out after \d+ms\s*/i, '')
|
|
269
|
+
.trim();
|
|
270
|
+
const body = tail || '(no output captured before timeout)';
|
|
271
|
+
return limitTail(
|
|
272
|
+
`Observation timeout after ${timeoutMs}ms for a likely long-running command. ` +
|
|
273
|
+
`The process was stopped after collecting the output tail.\n${body}`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function updateProjectIndex(filePath) {
|
|
278
|
+
try {
|
|
279
|
+
projectRegistry.projectForPath(filePath)?.retriever.updateFile(filePath);
|
|
280
|
+
} catch { /* best effort */ }
|
|
281
|
+
_readOnlyCacheGeneration++;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function readTextIfExists(filePath) {
|
|
285
|
+
try {
|
|
286
|
+
if (!fs.existsSync(filePath)) return '';
|
|
287
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
288
|
+
} catch {
|
|
289
|
+
return '';
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function stable(value) {
|
|
294
|
+
if (Array.isArray(value)) return value.map(stable);
|
|
295
|
+
if (value && typeof value === 'object') {
|
|
296
|
+
return Object.fromEntries(
|
|
297
|
+
Object.entries(value)
|
|
298
|
+
.filter(([, v]) => v !== undefined)
|
|
299
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
300
|
+
.map(([k, v]) => [k, stable(v)]),
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return value;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function clonePlain(value) {
|
|
307
|
+
return JSON.parse(JSON.stringify(value));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function fileFingerprint(filePath) {
|
|
311
|
+
try {
|
|
312
|
+
const stat = fs.statSync(filePath);
|
|
313
|
+
return {
|
|
314
|
+
filePath,
|
|
315
|
+
size: stat.size,
|
|
316
|
+
mtimeMs: Math.round(stat.mtimeMs),
|
|
317
|
+
};
|
|
318
|
+
} catch {
|
|
319
|
+
return { filePath, missing: true };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function cacheKey(kind, args, fingerprint) {
|
|
324
|
+
return JSON.stringify(stable({
|
|
325
|
+
kind,
|
|
326
|
+
args,
|
|
327
|
+
fingerprint,
|
|
328
|
+
generation: _readOnlyCacheGeneration,
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function compactCachedResult(kind, cached) {
|
|
333
|
+
const result = clonePlain(cached.result);
|
|
334
|
+
const output = String(result.output || result.content || result.message || '').trim();
|
|
335
|
+
const excerpt = output.length > 1200 ? `${output.slice(0, 1200)}\n[... cached output truncated ...]` : output;
|
|
336
|
+
result.output = `[Bahulam Code reused prior ${kind} result; source unchanged.]${excerpt ? `\n\n${excerpt}` : ''}`;
|
|
337
|
+
if (typeof result.content === 'string') {
|
|
338
|
+
result.content = result.content.length > 1200
|
|
339
|
+
? `${result.content.slice(0, 1200)}\n[... cached content truncated ...]`
|
|
340
|
+
: result.content;
|
|
341
|
+
}
|
|
342
|
+
result._cache_reused = true;
|
|
343
|
+
result._cache_source_call = cached.callId;
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function withReadOnlyCache(kind, args, fingerprint, compute) {
|
|
348
|
+
const key = cacheKey(kind, args, fingerprint);
|
|
349
|
+
const cached = readOnlyResultCache.get(key);
|
|
350
|
+
if (cached) return compactCachedResult(kind, cached);
|
|
351
|
+
const result = await compute();
|
|
352
|
+
if (result?.success !== false) {
|
|
353
|
+
readOnlyResultCache.set(key, {
|
|
354
|
+
result: clonePlain(result),
|
|
355
|
+
callId: `${kind}-${readOnlyResultCache.size + 1}`,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function attachFileDiff(result, filePath, before, after) {
|
|
362
|
+
try {
|
|
363
|
+
const diff = buildFileDiff({
|
|
364
|
+
filePath,
|
|
365
|
+
before,
|
|
366
|
+
after,
|
|
367
|
+
cwd: projectRootFor(filePath),
|
|
368
|
+
});
|
|
369
|
+
result.file_diff = diff;
|
|
370
|
+
result.diff = diff.unified;
|
|
371
|
+
result.lines_added = diff.lines_added;
|
|
372
|
+
result.lines_removed = diff.lines_removed;
|
|
373
|
+
} catch { /* best effort */ }
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Detect if an OCC tool result string indicates an error.
|
|
379
|
+
*/
|
|
380
|
+
function isError(result) {
|
|
381
|
+
if (typeof result !== 'string') return false;
|
|
382
|
+
return result.startsWith('Error:') || result.startsWith('Error -') ||
|
|
383
|
+
result.includes('Exit code:') && !result.includes('Exit code: 0');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Wrap an OCC string result into Tarang's { success, output } format.
|
|
388
|
+
*/
|
|
389
|
+
function wrapResult(result, toolName) {
|
|
390
|
+
if (typeof result === 'object' && result !== null && 'success' in result) {
|
|
391
|
+
result._tool = toolName;
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
const output = typeof result === 'string' ? result : JSON.stringify(result);
|
|
395
|
+
return {
|
|
396
|
+
success: !isError(output),
|
|
397
|
+
output,
|
|
398
|
+
_tool: toolName,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ── Auto-lint after file writes ────────────────────────────
|
|
403
|
+
|
|
404
|
+
const LINT_COMMANDS = {
|
|
405
|
+
'.py': (file) => `python3 -m py_compile "${file}" 2>&1`,
|
|
406
|
+
'.js': (file) => `npx eslint --no-eslintrc --rule '{}' "${file}" 2>&1 || true`,
|
|
407
|
+
'.ts': (file) => `npx tsc --noEmit --pretty "${file}" 2>&1 || true`,
|
|
408
|
+
'.tsx': (file) => `npx tsc --noEmit --pretty "${file}" 2>&1 || true`,
|
|
409
|
+
'.go': (file) => `go vet "${file}" 2>&1`,
|
|
410
|
+
'.rs': (file) => `rustfmt --check "${file}" 2>&1`,
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// tsc --pretty and eslint emit ANSI codes (including background-red
|
|
414
|
+
// highlights) which bleed when our renderer slices the first 80 chars.
|
|
415
|
+
// Strip color codes so the stored lint string is always plain text.
|
|
416
|
+
const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]/g;
|
|
417
|
+
function stripAnsi(s) { return String(s || '').replace(ANSI_RE, ''); }
|
|
418
|
+
|
|
419
|
+
function autoLint(filePath) {
|
|
420
|
+
const ext = path.extname(filePath);
|
|
421
|
+
const cmdFn = LINT_COMMANDS[ext];
|
|
422
|
+
if (!cmdFn) return null;
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const output = execSync(cmdFn(filePath), {
|
|
426
|
+
encoding: 'utf-8',
|
|
427
|
+
timeout: 15_000,
|
|
428
|
+
cwd: process.cwd(),
|
|
429
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
430
|
+
env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1', TERM: 'dumb' },
|
|
431
|
+
});
|
|
432
|
+
const trimmed = stripAnsi(output).trim();
|
|
433
|
+
if (!trimmed) return null;
|
|
434
|
+
return trimmed;
|
|
435
|
+
} catch (err) {
|
|
436
|
+
// Non-zero exit means lint errors found
|
|
437
|
+
const output = stripAnsi(err.stderr || err.stdout || '').trim();
|
|
438
|
+
if (!output) return null;
|
|
439
|
+
return output;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ── Post-edit verification hint ──────────────────────────────
|
|
444
|
+
// Appended to edit_file/write_file results so the model knows
|
|
445
|
+
// exactly how to verify. Uses detected project commands.
|
|
446
|
+
|
|
447
|
+
function verificationHint(filePath) {
|
|
448
|
+
const project = projectRegistry.projectForPath(filePath);
|
|
449
|
+
const commands = project?.resource?.commands || {};
|
|
450
|
+
const parts = [];
|
|
451
|
+
if (commands.test) {
|
|
452
|
+
parts.push(`Run tests: ${commands.test}`);
|
|
453
|
+
}
|
|
454
|
+
if (parts.length === 0) {
|
|
455
|
+
const ext = path.extname(filePath);
|
|
456
|
+
if (ext === '.py') parts.push('Run tests: python -m pytest');
|
|
457
|
+
else if (['.js', '.ts', '.tsx', '.mjs'].includes(ext)) parts.push('Run tests: npm test');
|
|
458
|
+
}
|
|
459
|
+
return parts.length ? `\n--- Verify ---\n${parts.join('\n')}` : '';
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ── Solution nudge after exploration ───────────────────────
|
|
463
|
+
// After the agent has read enough code, nudge it to formulate
|
|
464
|
+
// a solution based on the goal — not to blindly edit, but to
|
|
465
|
+
// synthesize what it learned into a fix approach.
|
|
466
|
+
let _codeReadsCount = 0;
|
|
467
|
+
let _hasEdited = false;
|
|
468
|
+
|
|
469
|
+
function solutionNudge(filePath) {
|
|
470
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
471
|
+
const isCode = ['.py', '.js', '.ts', '.tsx', '.mjs', '.go', '.rs', '.java', '.rb'].includes(ext);
|
|
472
|
+
if (!isCode || _hasEdited) return '';
|
|
473
|
+
|
|
474
|
+
_codeReadsCount++;
|
|
475
|
+
if (_codeReadsCount < 4) return '';
|
|
476
|
+
|
|
477
|
+
// Only nudge once at threshold, not every read after
|
|
478
|
+
if (_codeReadsCount === 4) {
|
|
479
|
+
return '\n\n--- You have explored enough code to formulate a solution. ' +
|
|
480
|
+
'Based on what you have read, determine the fix and apply it. ' +
|
|
481
|
+
'If the approach is unclear, call plan() with your findings. ---';
|
|
482
|
+
}
|
|
483
|
+
return '';
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function buildDirectoryTree(rootPath, { maxDepth = 2, maxEntries = 200 } = {}) {
|
|
487
|
+
const ignored = new Set(['.git', 'node_modules', '.next', '.turbo', 'dist', 'build', 'coverage']);
|
|
488
|
+
const rootName = path.basename(rootPath) || rootPath;
|
|
489
|
+
const lines = [`${rootName}/`];
|
|
490
|
+
const files = [];
|
|
491
|
+
const directories = [rootPath];
|
|
492
|
+
let entriesSeen = 0;
|
|
493
|
+
let truncated = false;
|
|
494
|
+
|
|
495
|
+
function walk(dir, depth, prefix) {
|
|
496
|
+
if (depth >= maxDepth || truncated) return;
|
|
497
|
+
let entries;
|
|
498
|
+
try {
|
|
499
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
500
|
+
.filter(entry => !ignored.has(entry.name))
|
|
501
|
+
.sort((a, b) => {
|
|
502
|
+
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
|
|
503
|
+
return a.name.localeCompare(b.name);
|
|
504
|
+
});
|
|
505
|
+
} catch (err) {
|
|
506
|
+
lines.push(`${prefix}[error: ${err.message}]`);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
for (let i = 0; i < entries.length; i++) {
|
|
511
|
+
if (entriesSeen >= maxEntries) {
|
|
512
|
+
truncated = true;
|
|
513
|
+
lines.push(`${prefix}... [truncated after ${maxEntries} entries]`);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const entry = entries[i];
|
|
517
|
+
const fullPath = path.join(dir, entry.name);
|
|
518
|
+
const isLast = i === entries.length - 1;
|
|
519
|
+
const connector = isLast ? '`-- ' : '|-- ';
|
|
520
|
+
entriesSeen++;
|
|
521
|
+
|
|
522
|
+
if (entry.isDirectory()) {
|
|
523
|
+
directories.push(fullPath);
|
|
524
|
+
lines.push(`${prefix}${connector}${entry.name}/`);
|
|
525
|
+
walk(fullPath, depth + 1, `${prefix}${isLast ? ' ' : '| '}`);
|
|
526
|
+
} else {
|
|
527
|
+
files.push(fullPath);
|
|
528
|
+
lines.push(`${prefix}${connector}${entry.name}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
walk(rootPath, 0, '');
|
|
534
|
+
return { output: lines.join('\n'), files, directories, truncated };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function executeToolWithHooks(name, args, options = {}) {
|
|
538
|
+
const handler = toolMap[name];
|
|
539
|
+
if (!handler) {
|
|
540
|
+
return { success: false, output: `Unknown tool: ${name}`, _tool: name };
|
|
541
|
+
}
|
|
542
|
+
const hooks = hookRunner || new HookRunner({ cwd: process.cwd() });
|
|
543
|
+
try {
|
|
544
|
+
throwIfAborted(options.signal);
|
|
545
|
+
const pre = await hooks.run('PreToolUse', { toolName: name, input: args || {} });
|
|
546
|
+
throwIfAborted(options.signal);
|
|
547
|
+
if (pre.blocked) {
|
|
548
|
+
return { success: false, output: `BLOCKED by hook: ${pre.message}`, _tool: name, _blocked: true };
|
|
549
|
+
}
|
|
550
|
+
let result = await handler(args || {}, options);
|
|
551
|
+
if (result?._cancelled) return result;
|
|
552
|
+
throwIfAborted(options.signal);
|
|
553
|
+
const post = await hooks.run('PostToolUse', { toolName: name, input: args || {}, result });
|
|
554
|
+
throwIfAborted(options.signal);
|
|
555
|
+
for (const item of post.results || []) {
|
|
556
|
+
if (item.parsed?.modifiedResult !== undefined) result = item.parsed.modifiedResult;
|
|
557
|
+
if (item.parsed?.feedback && result && typeof result === 'object') {
|
|
558
|
+
result.output = `${result.output || ''}\n\n--- Hook Feedback ---\n${item.parsed.feedback}`.trim();
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return result;
|
|
562
|
+
} catch (err) {
|
|
563
|
+
if (isAbortError(err) || options.signal?.aborted) {
|
|
564
|
+
return cancelledToolResult(name);
|
|
565
|
+
}
|
|
566
|
+
return { success: false, output: `Tool error (${name}): ${err.message}`, _tool: name };
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ── Tool mapping table ──────────────────────────────────────
|
|
571
|
+
|
|
572
|
+
const toolMap = {
|
|
573
|
+
// 1. shell → Bash + classification + smart output filtering
|
|
574
|
+
shell: async (args, options = {}) => {
|
|
575
|
+
throwIfAborted(options.signal);
|
|
576
|
+
// Phase 1: legacy safety check (kept for backward compat)
|
|
577
|
+
const shellCheck = validateShellCommand(args.command);
|
|
578
|
+
if (!shellCheck.safe) {
|
|
579
|
+
return {
|
|
580
|
+
success: false,
|
|
581
|
+
output: `BLOCKED: ${shellCheck.reason}. Work only inside a registered project root.`,
|
|
582
|
+
_tool: 'shell', _blocked: true,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Phase 2: command classifier (PRD-050)
|
|
587
|
+
const classification = classifyCommand(args.command);
|
|
588
|
+
if (classification.classification === 'blocked') {
|
|
589
|
+
return {
|
|
590
|
+
success: false,
|
|
591
|
+
output: `BLOCKED: ${classification.reason}`,
|
|
592
|
+
_tool: 'shell', _blocked: true,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Tag for approval/sandbox routing
|
|
597
|
+
if (classification.highRisk || shellCheck.highRisk) {
|
|
598
|
+
args._highRisk = true;
|
|
599
|
+
args._riskReason = classification.reason || shellCheck.reason;
|
|
600
|
+
}
|
|
601
|
+
args._classification = classification.classification; // 'safe' or 'contained'
|
|
602
|
+
const cwd = await commandCwd(args);
|
|
603
|
+
|
|
604
|
+
// Pre-check: if command is rm/unlink, verify targets exist first
|
|
605
|
+
const rmMatch = (args.command || '').match(/^rm\s+(?:-\w+\s+)*(.+)$/);
|
|
606
|
+
if (rmMatch) {
|
|
607
|
+
const targets = rmMatch[1].split(/\s+/).filter(t => !t.startsWith('-'));
|
|
608
|
+
const missing = targets.filter(t => {
|
|
609
|
+
try { return !fs.existsSync(shellTargetPath(cwd, t)); } catch { return true; }
|
|
610
|
+
});
|
|
611
|
+
if (missing.length > 0 && missing.length === targets.length) {
|
|
612
|
+
return {
|
|
613
|
+
success: true,
|
|
614
|
+
output: `No action needed: ${missing.join(', ')} — file(s) do not exist. Do not retry.`,
|
|
615
|
+
exit_code: 0,
|
|
616
|
+
_tool: 'shell',
|
|
617
|
+
_skipped: true,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const observationTimeout = args.timeout == null && isLikelyLongRunningCommand(args.command);
|
|
623
|
+
const effectiveTimeout = observationTimeout ? longRunningObservationTimeoutMs() : args.timeout;
|
|
624
|
+
const result = await occRegistry.call('Bash', {
|
|
625
|
+
command: args.command,
|
|
626
|
+
timeout: effectiveTimeout,
|
|
627
|
+
description: args.description || `Run: ${(args.command || '').slice(0, 50)}`,
|
|
628
|
+
cwd,
|
|
629
|
+
signal: options.signal,
|
|
630
|
+
});
|
|
631
|
+
const rawOutput = typeof result === 'string' ? result : String(result);
|
|
632
|
+
const cancelled = /^Error:\s*Command cancelled by user/i.test(rawOutput);
|
|
633
|
+
const timedOut = /^Error:\s*Command timed out after \d+ms/i.test(rawOutput);
|
|
634
|
+
const exitMatch = rawOutput.match(/Exit code: (\d+)/);
|
|
635
|
+
const exitCode = cancelled ? 130 : (timedOut ? 124 : (exitMatch ? parseInt(exitMatch[1]) : 0));
|
|
636
|
+
// Semantic exit code: grep returns 1 for "no matches" (not an error)
|
|
637
|
+
const success = cancelled ? false : (observationTimeout && timedOut ? true : (!timedOut && !isExitCodeError(args.command, exitCode)));
|
|
638
|
+
|
|
639
|
+
// Apply smart filtering based on command type
|
|
640
|
+
const filtered = observationTimeout && timedOut
|
|
641
|
+
? {
|
|
642
|
+
output: formatObservationTimeoutOutput(rawOutput, effectiveTimeout),
|
|
643
|
+
commandType: detectCommandType(args.command),
|
|
644
|
+
truncated: false,
|
|
645
|
+
originalLines: rawOutput.split('\n').length,
|
|
646
|
+
filteredLines: rawOutput.split('\n').length,
|
|
647
|
+
}
|
|
648
|
+
: filterOutput(rawOutput, args.command, success);
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
success,
|
|
652
|
+
output: filtered.output,
|
|
653
|
+
exit_code: exitCode,
|
|
654
|
+
_tool: 'shell',
|
|
655
|
+
_classification: args._classification,
|
|
656
|
+
_commandType: filtered.commandType,
|
|
657
|
+
_filtered: filtered.truncated || filtered.originalLines !== filtered.filteredLines,
|
|
658
|
+
_timed_out: timedOut,
|
|
659
|
+
_cancelled: cancelled,
|
|
660
|
+
_observation_timeout: observationTimeout && timedOut,
|
|
661
|
+
_observation_timeout_ms: observationTimeout && timedOut ? effectiveTimeout : undefined,
|
|
662
|
+
};
|
|
663
|
+
},
|
|
664
|
+
|
|
665
|
+
// 2. read_file → Read (with smart truncation for large files)
|
|
666
|
+
read_file: async (args) => {
|
|
667
|
+
const filePath = await resolvePath(args.file_path || args.path, args, { allowExternalFileRead: true });
|
|
668
|
+
const hasLineRange = args.start_line || args.end_line || args.offset || args.limit;
|
|
669
|
+
const offset = args.start_line ? args.start_line - 1 : args.offset;
|
|
670
|
+
const limit = (args.start_line && args.end_line)
|
|
671
|
+
? (args.end_line - args.start_line + 1)
|
|
672
|
+
: args.limit;
|
|
673
|
+
|
|
674
|
+
return await withReadOnlyCache(
|
|
675
|
+
'read_file',
|
|
676
|
+
{ filePath, offset, limit },
|
|
677
|
+
fileFingerprint(filePath),
|
|
678
|
+
async () => {
|
|
679
|
+
|
|
680
|
+
// Nudge: if reading shallow overview files, remind agent to search deeper
|
|
681
|
+
const basename = path.basename(filePath).toLowerCase();
|
|
682
|
+
const isShallowFile = ['readme.md', 'package.json', 'pyproject.toml', 'cargo.toml', 'go.mod'].includes(basename);
|
|
683
|
+
const nudge = isShallowFile && !_searchCodeUsed
|
|
684
|
+
? '\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.'
|
|
685
|
+
: '';
|
|
686
|
+
|
|
687
|
+
// If no line range specified, auto-truncate and return AST summary
|
|
688
|
+
if (!hasLineRange) {
|
|
689
|
+
try {
|
|
690
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
691
|
+
const lines = content.split('\n').length;
|
|
692
|
+
|
|
693
|
+
if (lines > 50) {
|
|
694
|
+
// File >50 lines: return AST summary with line numbers
|
|
695
|
+
// Model must use start_line/end_line to read specific sections
|
|
696
|
+
const analysis = analyzeCode(filePath);
|
|
697
|
+
const firstLines = content.split('\n').slice(0, 20).join('\n');
|
|
698
|
+
return {
|
|
699
|
+
success: true,
|
|
700
|
+
output: `${analysis.summary}\n\n` +
|
|
701
|
+
`## First 20 lines\n${firstLines}${nudge}`,
|
|
702
|
+
_tool: 'read_file',
|
|
703
|
+
_truncated: true,
|
|
704
|
+
_total_lines: lines,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
// Small file (<50 lines): return full content
|
|
708
|
+
} catch { /* let Read handle the error */ }
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const result = await occRegistry.call('Read', {
|
|
712
|
+
file_path: filePath,
|
|
713
|
+
offset,
|
|
714
|
+
limit,
|
|
715
|
+
});
|
|
716
|
+
const output = typeof result === 'string' ? result : String(result);
|
|
717
|
+
const content = output.replace(/^\s*\d+[→\t]/gm, '');
|
|
718
|
+
const actNudge = solutionNudge(filePath);
|
|
719
|
+
return {
|
|
720
|
+
success: !isError(output),
|
|
721
|
+
content,
|
|
722
|
+
output: output + nudge + actNudge,
|
|
723
|
+
_tool: 'read_file',
|
|
724
|
+
_output_type: 'file_content',
|
|
725
|
+
};
|
|
726
|
+
},
|
|
727
|
+
);
|
|
728
|
+
},
|
|
729
|
+
|
|
730
|
+
// 3. write_file → Write + auto-lint + safety check
|
|
731
|
+
write_file: async (args) => {
|
|
732
|
+
const rawPath = args.file_path || args.path;
|
|
733
|
+
if (!rawPath || rawPath === 'file' || rawPath.length < 3) {
|
|
734
|
+
return { success: false, output: `Error: Invalid file path "${rawPath || ''}". Register the project, then use an absolute path.`, _tool: 'write_file' };
|
|
735
|
+
}
|
|
736
|
+
const filePath = await resolvePath(rawPath, args, { allowMissing: true });
|
|
737
|
+
const before = readTextIfExists(filePath);
|
|
738
|
+
const writeCheck = validateWrite(filePath, args.content, projectRootFor(filePath));
|
|
739
|
+
if (!writeCheck.safe) {
|
|
740
|
+
return { success: false, output: `🛡️ BLOCKED: ${writeCheck.reason}`, _tool: 'write_file', _blocked: true };
|
|
741
|
+
}
|
|
742
|
+
// OCC Write requires Read first for existing files — handle gracefully
|
|
743
|
+
try {
|
|
744
|
+
if (fs.existsSync(filePath)) {
|
|
745
|
+
await occRegistry.call('Read', { file_path: filePath, limit: 1 });
|
|
746
|
+
}
|
|
747
|
+
} catch { /* file may not exist yet */ }
|
|
748
|
+
// Checkpoint before overwrite so /undo can restore the previous content.
|
|
749
|
+
if (checkpoints && fs.existsSync(filePath)) {
|
|
750
|
+
try { checkpoints.save(filePath); } catch { /* best effort */ }
|
|
751
|
+
}
|
|
752
|
+
const result = await occRegistry.call('Write', {
|
|
753
|
+
file_path: filePath,
|
|
754
|
+
content: args.content,
|
|
755
|
+
});
|
|
756
|
+
const wrapped = wrapResult(result, 'write_file');
|
|
757
|
+
const after = readTextIfExists(filePath);
|
|
758
|
+
attachFileDiff(wrapped, filePath, before, after);
|
|
759
|
+
updateProjectIndex(filePath);
|
|
760
|
+
|
|
761
|
+
// Auto-lint the written file
|
|
762
|
+
const lintOutput = autoLint(filePath);
|
|
763
|
+
if (lintOutput) {
|
|
764
|
+
wrapped.output += `\n\n--- Lint ---\n${lintOutput}`;
|
|
765
|
+
wrapped.lint = lintOutput;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Nudge: tell the model how to verify
|
|
769
|
+
const hint = verificationHint(filePath);
|
|
770
|
+
if (hint) wrapped.output += hint;
|
|
771
|
+
|
|
772
|
+
return wrapped;
|
|
773
|
+
},
|
|
774
|
+
|
|
775
|
+
// 3b. write_project → Batch write multiple files at once
|
|
776
|
+
write_project: async (args) => {
|
|
777
|
+
const files = args.files || [];
|
|
778
|
+
if (!files.length) {
|
|
779
|
+
return { success: false, output: 'Error: No files provided', _tool: 'write_project' };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const results = [];
|
|
783
|
+
const errors = [];
|
|
784
|
+
const diffs = [];
|
|
785
|
+
|
|
786
|
+
for (const file of files) {
|
|
787
|
+
const rawPath = file.path || file.file_path;
|
|
788
|
+
if (!rawPath) {
|
|
789
|
+
errors.push('Missing path in file entry');
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
const filePath = await resolvePath(rawPath, file, { allowMissing: true });
|
|
793
|
+
const content = file.content || '';
|
|
794
|
+
|
|
795
|
+
const writeCheck = validateWrite(filePath, content, projectRootFor(filePath));
|
|
796
|
+
if (!writeCheck.safe) {
|
|
797
|
+
errors.push(`${rawPath}: BLOCKED — ${writeCheck.reason}`);
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
try {
|
|
802
|
+
// Ensure parent directory exists
|
|
803
|
+
const dir = path.dirname(filePath);
|
|
804
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
805
|
+
const before = readTextIfExists(filePath);
|
|
806
|
+
|
|
807
|
+
// Read first if exists (OCC Write requirement)
|
|
808
|
+
try {
|
|
809
|
+
if (fs.existsSync(filePath)) {
|
|
810
|
+
await occRegistry.call('Read', { file_path: filePath, limit: 1 });
|
|
811
|
+
}
|
|
812
|
+
} catch { /* file may not exist yet */ }
|
|
813
|
+
|
|
814
|
+
await occRegistry.call('Write', { file_path: filePath, content });
|
|
815
|
+
const after = readTextIfExists(filePath);
|
|
816
|
+
diffs.push(buildFileDiff({
|
|
817
|
+
filePath,
|
|
818
|
+
before,
|
|
819
|
+
after,
|
|
820
|
+
cwd: projectRootFor(filePath),
|
|
821
|
+
}));
|
|
822
|
+
updateProjectIndex(filePath);
|
|
823
|
+
results.push(rawPath);
|
|
824
|
+
} catch (err) {
|
|
825
|
+
errors.push(`${rawPath}: ${err.message}`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const output = results.length > 0
|
|
830
|
+
? `Created ${results.length} file(s):\n${results.map(f => ` ✓ ${f}`).join('\n')}`
|
|
831
|
+
: 'No files written';
|
|
832
|
+
|
|
833
|
+
if (errors.length > 0) {
|
|
834
|
+
return {
|
|
835
|
+
success: results.length > 0,
|
|
836
|
+
output: `${output}\n\nErrors:\n${errors.map(e => ` ✗ ${e}`).join('\n')}`,
|
|
837
|
+
files_written: results,
|
|
838
|
+
files_failed: errors,
|
|
839
|
+
file_diffs: diffs,
|
|
840
|
+
lines_added: diffs.reduce((sum, diff) => sum + (diff.lines_added || 0), 0),
|
|
841
|
+
lines_removed: diffs.reduce((sum, diff) => sum + (diff.lines_removed || 0), 0),
|
|
842
|
+
_tool: 'write_project',
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return {
|
|
847
|
+
success: true,
|
|
848
|
+
output,
|
|
849
|
+
files_written: results,
|
|
850
|
+
file_diffs: diffs,
|
|
851
|
+
lines_added: diffs.reduce((sum, diff) => sum + (diff.lines_added || 0), 0),
|
|
852
|
+
lines_removed: diffs.reduce((sum, diff) => sum + (diff.lines_removed || 0), 0),
|
|
853
|
+
_tool: 'write_project',
|
|
854
|
+
};
|
|
855
|
+
},
|
|
856
|
+
|
|
857
|
+
// 4. edit_file → Edit + auto-lint + auto-fallback to sed
|
|
858
|
+
edit_file: async (args) => {
|
|
859
|
+
const rawPath = args.file_path || args.path;
|
|
860
|
+
const filePath = await resolvePath(rawPath, args);
|
|
861
|
+
const before = readTextIfExists(filePath);
|
|
862
|
+
const writeCheck = validateWrite(filePath, args.replace, projectRootFor(filePath));
|
|
863
|
+
if (!writeCheck.safe) {
|
|
864
|
+
return { success: false, output: `BLOCKED: ${writeCheck.reason}`, _tool: 'edit_file', _blocked: true };
|
|
865
|
+
}
|
|
866
|
+
// OCC Edit requires Read first
|
|
867
|
+
try {
|
|
868
|
+
await occRegistry.call('Read', { file_path: filePath, limit: 1 });
|
|
869
|
+
} catch { /* best effort */ }
|
|
870
|
+
|
|
871
|
+
// Checkpoint before edit so /undo can restore the previous content.
|
|
872
|
+
if (checkpoints) {
|
|
873
|
+
try { checkpoints.save(filePath); } catch { /* best effort */ }
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
let result;
|
|
877
|
+
try {
|
|
878
|
+
result = await occRegistry.call('Edit', {
|
|
879
|
+
file_path: filePath,
|
|
880
|
+
old_string: args.search,
|
|
881
|
+
new_string: args.replace,
|
|
882
|
+
replace_all: args.replace_all || false,
|
|
883
|
+
});
|
|
884
|
+
} catch (editErr) {
|
|
885
|
+
// OCC Edit failed (string not found) — fallback to Python replacement
|
|
886
|
+
try {
|
|
887
|
+
const search = args.search.replace(/'/g, "\\'").replace(/\n/g, "\\n");
|
|
888
|
+
const replace = args.replace.replace(/'/g, "\\'").replace(/\n/g, "\\n");
|
|
889
|
+
const pyCmd = `python3 -c "
|
|
890
|
+
import sys
|
|
891
|
+
with open('${filePath}', 'r') as f: content = f.read()
|
|
892
|
+
old = '''${args.search}'''
|
|
893
|
+
new = '''${args.replace}'''
|
|
894
|
+
if old not in content:
|
|
895
|
+
print('ERROR: search string not found in file', file=sys.stderr)
|
|
896
|
+
sys.exit(1)
|
|
897
|
+
content = content.replace(old, new, 1)
|
|
898
|
+
with open('${filePath}', 'w') as f: f.write(content)
|
|
899
|
+
print('OK: replaced')
|
|
900
|
+
"`;
|
|
901
|
+
const fallbackResult = execSync(pyCmd, {
|
|
902
|
+
encoding: 'utf-8',
|
|
903
|
+
timeout: 5000,
|
|
904
|
+
cwd: projectRootFor(filePath),
|
|
905
|
+
});
|
|
906
|
+
result = `Edited ${filePath} (via fallback): ${fallbackResult.trim()}`;
|
|
907
|
+
} catch (sedErr) {
|
|
908
|
+
return { success: false, output: `edit_file failed: ${editErr?.message || 'unknown'}. Fallback also failed: ${sedErr?.message || 'unknown'}. Try shell(sed) manually.`, _tool: 'edit_file' };
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const wrapped = wrapResult(result, 'edit_file');
|
|
913
|
+
const after = readTextIfExists(filePath);
|
|
914
|
+
attachFileDiff(wrapped, filePath, before, after);
|
|
915
|
+
updateProjectIndex(filePath);
|
|
916
|
+
_hasEdited = true;
|
|
917
|
+
|
|
918
|
+
// Auto-lint the edited file
|
|
919
|
+
const lintOutput = autoLint(filePath);
|
|
920
|
+
if (lintOutput) {
|
|
921
|
+
wrapped.output += `\n\n--- Lint ---\n${lintOutput}`;
|
|
922
|
+
wrapped.lint = lintOutput;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Nudge: tell the model how to verify
|
|
926
|
+
const hint = verificationHint(filePath);
|
|
927
|
+
if (hint) wrapped.output += hint;
|
|
928
|
+
|
|
929
|
+
return wrapped;
|
|
930
|
+
},
|
|
931
|
+
|
|
932
|
+
// 5. list_files → Glob
|
|
933
|
+
list_files: async (args) => {
|
|
934
|
+
const searchPath = await resolvePath(args.path || null, args);
|
|
935
|
+
return await withReadOnlyCache(
|
|
936
|
+
'list_files',
|
|
937
|
+
{
|
|
938
|
+
pattern: args.pattern || '**/*',
|
|
939
|
+
path: searchPath,
|
|
940
|
+
format: args.format || (args.tree === true ? 'tree' : 'glob'),
|
|
941
|
+
max_depth: args.max_depth ?? args.maxDepth ?? null,
|
|
942
|
+
},
|
|
943
|
+
{ generation: _readOnlyCacheGeneration },
|
|
944
|
+
async () => {
|
|
945
|
+
if (args.format === 'tree' || args.tree === true) {
|
|
946
|
+
const requestedDepth = Number(args.max_depth ?? args.maxDepth ?? 2);
|
|
947
|
+
const maxDepth = Number.isFinite(requestedDepth)
|
|
948
|
+
? Math.max(1, Math.min(6, Math.trunc(requestedDepth)))
|
|
949
|
+
: 2;
|
|
950
|
+
const tree = buildDirectoryTree(searchPath, { maxDepth });
|
|
951
|
+
return {
|
|
952
|
+
success: true,
|
|
953
|
+
output: tree.output,
|
|
954
|
+
tree: tree.output,
|
|
955
|
+
files: tree.files,
|
|
956
|
+
directories: tree.directories,
|
|
957
|
+
truncated: tree.truncated,
|
|
958
|
+
_tool: 'list_files',
|
|
959
|
+
_format: 'tree',
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
const result = await occRegistry.call('Glob', {
|
|
963
|
+
pattern: args.pattern || '**/*',
|
|
964
|
+
path: searchPath,
|
|
965
|
+
});
|
|
966
|
+
const output = typeof result === 'string' ? result : String(result);
|
|
967
|
+
const files = output.split('\n').filter(Boolean);
|
|
968
|
+
return {
|
|
969
|
+
success: true,
|
|
970
|
+
files,
|
|
971
|
+
output,
|
|
972
|
+
_tool: 'list_files',
|
|
973
|
+
};
|
|
974
|
+
},
|
|
975
|
+
);
|
|
976
|
+
},
|
|
977
|
+
|
|
978
|
+
// 6. search_code → combined rg + BM25 for best results
|
|
979
|
+
search_code: async (args) => {
|
|
980
|
+
_searchCodeUsed = true;
|
|
981
|
+
const query = args.query || args.pattern;
|
|
982
|
+
if (!query) return { success: false, output: 'query required', _tool: 'search_code' };
|
|
983
|
+
|
|
984
|
+
let project;
|
|
985
|
+
if (args.project_id) {
|
|
986
|
+
project = projectRegistry.get(args.project_id);
|
|
987
|
+
if (!project) {
|
|
988
|
+
return { success: false, output: `Unknown project_id: ${args.project_id}`, _tool: 'search_code' };
|
|
989
|
+
}
|
|
990
|
+
} else if (args.path) {
|
|
991
|
+
project = projectRegistry.projectForPath(await resolvePath(args.path, args));
|
|
992
|
+
} else if (projectRegistry.resources().length === 1) {
|
|
993
|
+
project = projectRegistry.get(projectRegistry.resources()[0].project_id);
|
|
994
|
+
} else {
|
|
995
|
+
return {
|
|
996
|
+
success: false,
|
|
997
|
+
output: 'search_code requires project_id when multiple or no projects are registered',
|
|
998
|
+
_tool: 'search_code',
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
const searchPath = args.path ? await resolvePath(args.path, args) : project.resource.root;
|
|
1002
|
+
const parts = [];
|
|
1003
|
+
|
|
1004
|
+
// Layer 1: ripgrep — exact text matches with context
|
|
1005
|
+
try {
|
|
1006
|
+
const cmd = `rg -n -C 1 --max-count 5 --max-filesize 500K -e ${JSON.stringify(query)} ${JSON.stringify(searchPath)} 2>/dev/null | head -60`;
|
|
1007
|
+
const rgOutput = execSync(cmd, { encoding: 'utf-8', timeout: 15000, cwd: searchPath }).trim();
|
|
1008
|
+
if (rgOutput) {
|
|
1009
|
+
parts.push(`## Exact matches (rg)\n${rgOutput}`);
|
|
1010
|
+
}
|
|
1011
|
+
} catch { /* rg not found or no results */ }
|
|
1012
|
+
|
|
1013
|
+
// Layer 2: Symbol search — AST-extracted functions/classes with signatures
|
|
1014
|
+
if (project?.retriever) {
|
|
1015
|
+
if (!project.retriever.index) project.retriever.loadIndex();
|
|
1016
|
+
const symbols = project.retriever.searchSymbols(query, 5);
|
|
1017
|
+
if (symbols.length > 0) {
|
|
1018
|
+
const symOutput = project.retriever.formatSymbolResults(symbols);
|
|
1019
|
+
parts.push(`## Symbols (functions/classes)\n${symOutput}`);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Layer 3: BM25 chunks — broader context when symbols aren't enough
|
|
1023
|
+
const chunks = project.retriever.retrieve(query, 5);
|
|
1024
|
+
if (chunks.length > 0) {
|
|
1025
|
+
const bm25Output = chunks.map(c => {
|
|
1026
|
+
const score = c.score?.toFixed(2) || '?';
|
|
1027
|
+
return `── ${c.id} (score: ${score}) ──\n${c.text}`;
|
|
1028
|
+
}).join('\n\n');
|
|
1029
|
+
parts.push(`## Related code (BM25)\n${bm25Output}`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// Return combined results
|
|
1034
|
+
if (parts.length > 0) {
|
|
1035
|
+
return {
|
|
1036
|
+
success: true,
|
|
1037
|
+
output: parts.join('\n\n'),
|
|
1038
|
+
_tool: 'search_code',
|
|
1039
|
+
_method: parts.length > 1 ? 'rg+bm25' : (parts[0].startsWith('## Exact') ? 'rg' : 'bm25'),
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Nothing found — actionable hint
|
|
1044
|
+
const firstWord = query.split(/\s+/)[0];
|
|
1045
|
+
return {
|
|
1046
|
+
success: true,
|
|
1047
|
+
output: `No results for "${query}" in ${searchPath}.\n` +
|
|
1048
|
+
`Try: shell(grep -rn "${firstWord}" . --include="*.py" | head -20)`,
|
|
1049
|
+
_tool: 'search_code',
|
|
1050
|
+
_method: 'none',
|
|
1051
|
+
};
|
|
1052
|
+
},
|
|
1053
|
+
|
|
1054
|
+
// 7. search_files → Grep with line numbers + context (like grep -n -C 3)
|
|
1055
|
+
search_files: async (args) => {
|
|
1056
|
+
const query = args.query || args.pattern || '*';
|
|
1057
|
+
const searchPath = await resolvePath(args.path || null, args);
|
|
1058
|
+
|
|
1059
|
+
// If it looks like a glob pattern, use Glob
|
|
1060
|
+
if (query.includes('*') || query.includes('?')) {
|
|
1061
|
+
return await withReadOnlyCache(
|
|
1062
|
+
'search_files',
|
|
1063
|
+
{ query, path: searchPath, mode: 'glob' },
|
|
1064
|
+
{ generation: _readOnlyCacheGeneration },
|
|
1065
|
+
async () => {
|
|
1066
|
+
const result = await occRegistry.call('Glob', {
|
|
1067
|
+
pattern: query,
|
|
1068
|
+
path: searchPath,
|
|
1069
|
+
});
|
|
1070
|
+
const output = typeof result === 'string' ? result : String(result);
|
|
1071
|
+
return {
|
|
1072
|
+
success: true,
|
|
1073
|
+
files: output.split('\n').filter(Boolean),
|
|
1074
|
+
output,
|
|
1075
|
+
_tool: 'search_files',
|
|
1076
|
+
};
|
|
1077
|
+
},
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// For text patterns: grep with context lines (like grep -n -C 3)
|
|
1082
|
+
return await withReadOnlyCache(
|
|
1083
|
+
'search_files',
|
|
1084
|
+
{ query, path: searchPath, mode: 'grep' },
|
|
1085
|
+
{ generation: _readOnlyCacheGeneration },
|
|
1086
|
+
async () => {
|
|
1087
|
+
const result = await occRegistry.call('Grep', {
|
|
1088
|
+
pattern: query,
|
|
1089
|
+
path: searchPath,
|
|
1090
|
+
output_mode: 'content',
|
|
1091
|
+
'-n': true,
|
|
1092
|
+
'-C': 3,
|
|
1093
|
+
head_limit: 50,
|
|
1094
|
+
});
|
|
1095
|
+
const output = typeof result === 'string' ? result : String(result);
|
|
1096
|
+
return {
|
|
1097
|
+
success: true,
|
|
1098
|
+
files: output.split('\n').filter(Boolean),
|
|
1099
|
+
output,
|
|
1100
|
+
_tool: 'search_files',
|
|
1101
|
+
};
|
|
1102
|
+
},
|
|
1103
|
+
);
|
|
1104
|
+
},
|
|
1105
|
+
|
|
1106
|
+
// 7b. grep → dedicated ripgrep tool (fast text/regex search)
|
|
1107
|
+
grep: async (args) => {
|
|
1108
|
+
const pattern = args.pattern;
|
|
1109
|
+
if (!pattern) return { success: false, output: 'pattern required', _tool: 'grep' };
|
|
1110
|
+
|
|
1111
|
+
const searchPath = await resolvePath(args.path || null, args);
|
|
1112
|
+
const includeFlag = args.include ? `--glob "${args.include}"` : '';
|
|
1113
|
+
|
|
1114
|
+
try {
|
|
1115
|
+
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`;
|
|
1116
|
+
const output = execSync(cmd, { encoding: 'utf-8', timeout: 15000, cwd: searchPath }).trim();
|
|
1117
|
+
if (output) {
|
|
1118
|
+
return { success: true, output, _tool: 'grep' };
|
|
1119
|
+
}
|
|
1120
|
+
} catch { /* no results or rg not found */ }
|
|
1121
|
+
|
|
1122
|
+
return {
|
|
1123
|
+
success: true,
|
|
1124
|
+
output: `No matches for "${pattern}" in ${searchPath}`,
|
|
1125
|
+
_tool: 'grep',
|
|
1126
|
+
};
|
|
1127
|
+
},
|
|
1128
|
+
|
|
1129
|
+
// ── Tarang-specific tools (no OCC bridge) ──────────────
|
|
1130
|
+
|
|
1131
|
+
// 8. read_files → batch Read (with AST truncation for large files)
|
|
1132
|
+
read_files: async (args) => {
|
|
1133
|
+
const rawItems = args.items || args.files || args.file_paths || args.paths || [];
|
|
1134
|
+
const items = (Array.isArray(rawItems) ? rawItems : [])
|
|
1135
|
+
.map(item => typeof item === 'string' ? { file_path: item } : item)
|
|
1136
|
+
.filter(Boolean);
|
|
1137
|
+
const results = [];
|
|
1138
|
+
for (const item of items) {
|
|
1139
|
+
const p = item.file_path || item.path;
|
|
1140
|
+
try {
|
|
1141
|
+
const result = await toolMap.read_file({
|
|
1142
|
+
...args,
|
|
1143
|
+
...item,
|
|
1144
|
+
file_path: p,
|
|
1145
|
+
});
|
|
1146
|
+
results.push({
|
|
1147
|
+
path: p,
|
|
1148
|
+
success: result.success !== false,
|
|
1149
|
+
content: result.content,
|
|
1150
|
+
output: result.output,
|
|
1151
|
+
lines: result._total_lines,
|
|
1152
|
+
cached: Boolean(result._cache_reused),
|
|
1153
|
+
truncated: Boolean(result._truncated),
|
|
1154
|
+
});
|
|
1155
|
+
} catch (err) {
|
|
1156
|
+
results.push({ path: p, error: err.message, success: false });
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return {
|
|
1160
|
+
success: results.every(item => item.success !== false),
|
|
1161
|
+
files: results,
|
|
1162
|
+
output: results.map(item => {
|
|
1163
|
+
const status = item.success === false ? 'ERROR' : item.cached ? 'CACHED' : 'OK';
|
|
1164
|
+
return `## ${item.path} [${status}]\n${item.output || item.content || item.error || ''}`;
|
|
1165
|
+
}).join('\n\n'),
|
|
1166
|
+
_tool: 'read_files',
|
|
1167
|
+
};
|
|
1168
|
+
},
|
|
1169
|
+
|
|
1170
|
+
read_batch: async (args) => {
|
|
1171
|
+
const result = await toolMap.read_files({
|
|
1172
|
+
...args,
|
|
1173
|
+
items: args.items || args.files || args.file_paths || args.paths || [],
|
|
1174
|
+
});
|
|
1175
|
+
return { ...result, _tool: 'read_batch' };
|
|
1176
|
+
},
|
|
1177
|
+
|
|
1178
|
+
// 9. delete_file + safety check + checkpoint for undo
|
|
1179
|
+
delete_file: async (args) => {
|
|
1180
|
+
try {
|
|
1181
|
+
const filePath = await resolvePath(args.file_path || args.path, args);
|
|
1182
|
+
const delCheck = validateDelete(filePath, projectRootFor(filePath));
|
|
1183
|
+
if (!delCheck.safe) {
|
|
1184
|
+
return { success: false, output: `🛡️ BLOCKED: ${delCheck.reason}`, _tool: 'delete_file', _blocked: true };
|
|
1185
|
+
}
|
|
1186
|
+
if (checkpoints) {
|
|
1187
|
+
try { checkpoints.save(filePath); } catch { /* best effort */ }
|
|
1188
|
+
}
|
|
1189
|
+
fs.unlinkSync(filePath);
|
|
1190
|
+
updateProjectIndex(filePath);
|
|
1191
|
+
return { success: true, message: `Deleted ${args.path}`, _tool: 'delete_file' };
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
return { success: false, output: `Error: ${err.message}`, _tool: 'delete_file' };
|
|
1194
|
+
}
|
|
1195
|
+
},
|
|
1196
|
+
|
|
1197
|
+
// 10. get_file_info
|
|
1198
|
+
get_file_info: async (args) => {
|
|
1199
|
+
try {
|
|
1200
|
+
const filePath = await resolvePath(args.file_path || args.path, args);
|
|
1201
|
+
const stat = fs.statSync(filePath);
|
|
1202
|
+
return {
|
|
1203
|
+
success: true,
|
|
1204
|
+
size: stat.size,
|
|
1205
|
+
mtime: stat.mtime.toISOString(),
|
|
1206
|
+
type: stat.isDirectory() ? 'directory' : 'file',
|
|
1207
|
+
mode: stat.mode.toString(8),
|
|
1208
|
+
_tool: 'get_file_info',
|
|
1209
|
+
};
|
|
1210
|
+
} catch (err) {
|
|
1211
|
+
return { success: false, output: `Error: ${err.message}`, _tool: 'get_file_info' };
|
|
1212
|
+
}
|
|
1213
|
+
},
|
|
1214
|
+
|
|
1215
|
+
// 11. validate_file (syntax check)
|
|
1216
|
+
validate_file: async (args) => {
|
|
1217
|
+
try {
|
|
1218
|
+
const filePath = await resolvePath(args.path, args);
|
|
1219
|
+
const ext = path.extname(filePath);
|
|
1220
|
+
let cmd;
|
|
1221
|
+
if (ext === '.py') cmd = `python3 -m py_compile "${filePath}"`;
|
|
1222
|
+
else if (ext === '.js' || ext === '.mjs') cmd = `node --check "${filePath}"`;
|
|
1223
|
+
else return { success: true, valid: true, message: 'No validator for this file type', _tool: 'validate_file' };
|
|
1224
|
+
|
|
1225
|
+
execSync(cmd, { stdio: 'pipe', cwd: projectRootFor(filePath) });
|
|
1226
|
+
return { success: true, valid: true, _tool: 'validate_file' };
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
return { success: true, valid: false, errors: err.stderr?.toString() || err.message, _tool: 'validate_file' };
|
|
1229
|
+
}
|
|
1230
|
+
},
|
|
1231
|
+
|
|
1232
|
+
// 12. validate_build
|
|
1233
|
+
validate_build: async (args, options = {}) => {
|
|
1234
|
+
try {
|
|
1235
|
+
throwIfAborted(options.signal);
|
|
1236
|
+
let cmd = args.command;
|
|
1237
|
+
const cwd = await commandCwd(args);
|
|
1238
|
+
if (!cmd) {
|
|
1239
|
+
if (fs.existsSync(path.join(cwd, 'package.json'))) cmd = 'npm run build';
|
|
1240
|
+
else if (fs.existsSync(path.join(cwd, 'Makefile'))) cmd = 'make';
|
|
1241
|
+
else if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) cmd = 'cargo build';
|
|
1242
|
+
else return { success: false, output: 'No build system detected', _tool: 'validate_build' };
|
|
1243
|
+
}
|
|
1244
|
+
const output = await occRegistry.call('Bash', {
|
|
1245
|
+
command: cmd,
|
|
1246
|
+
timeout: Math.min(args.timeout || 120_000, 600_000),
|
|
1247
|
+
description: `Validate build: ${cmd.slice(0, 80)}`,
|
|
1248
|
+
cwd,
|
|
1249
|
+
signal: options.signal,
|
|
1250
|
+
});
|
|
1251
|
+
const rawOutput = typeof output === 'string' ? output : String(output);
|
|
1252
|
+
if (/^Error:\s*Command cancelled by user/i.test(rawOutput)) {
|
|
1253
|
+
return { success: false, output: 'Cancelled by user', exit_code: 130, _cancelled: true, _tool: 'validate_build' };
|
|
1254
|
+
}
|
|
1255
|
+
const exitMatch = rawOutput.match(/Exit code: (\d+)/);
|
|
1256
|
+
if (exitMatch || /^Error:\s*Command timed out/i.test(rawOutput)) {
|
|
1257
|
+
return { success: false, output: rawOutput, exit_code: exitMatch ? Number(exitMatch[1]) : 124, _tool: 'validate_build' };
|
|
1258
|
+
}
|
|
1259
|
+
return { success: true, output: rawOutput, _tool: 'validate_build' };
|
|
1260
|
+
} catch (err) {
|
|
1261
|
+
if (isAbortError(err) || options.signal?.aborted) return cancelledToolResult('validate_build');
|
|
1262
|
+
return { success: false, output: err.stderr?.toString() || err.message, _tool: 'validate_build' };
|
|
1263
|
+
}
|
|
1264
|
+
},
|
|
1265
|
+
|
|
1266
|
+
// 13. validate_structure
|
|
1267
|
+
validate_structure: async (args) => {
|
|
1268
|
+
const expected = args.expected || [];
|
|
1269
|
+
const missing = [];
|
|
1270
|
+
for (const f of expected) {
|
|
1271
|
+
if (!fs.existsSync(await resolvePath(f, args, { allowMissing: true }))) {
|
|
1272
|
+
missing.push(f);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
return {
|
|
1276
|
+
success: missing.length === 0,
|
|
1277
|
+
missing,
|
|
1278
|
+
checked: expected.length,
|
|
1279
|
+
_tool: 'validate_structure',
|
|
1280
|
+
};
|
|
1281
|
+
},
|
|
1282
|
+
|
|
1283
|
+
// 14. lint_check
|
|
1284
|
+
lint_check: async (args, options = {}) => {
|
|
1285
|
+
try {
|
|
1286
|
+
throwIfAborted(options.signal);
|
|
1287
|
+
const filePath = await resolvePath(args.file_path || args.path, args);
|
|
1288
|
+
const ext = path.extname(filePath);
|
|
1289
|
+
let cmd;
|
|
1290
|
+
if (ext === '.py') cmd = `python3 -m ruff check "${filePath}" 2>&1 || true`;
|
|
1291
|
+
else if (['.js', '.mjs', '.ts', '.tsx'].includes(ext)) cmd = `npx eslint "${filePath}" 2>&1 || true`;
|
|
1292
|
+
else return { success: true, issues: [], message: 'No linter for this file type', _tool: 'lint_check' };
|
|
1293
|
+
|
|
1294
|
+
const output = await occRegistry.call('Bash', {
|
|
1295
|
+
command: cmd,
|
|
1296
|
+
timeout: 30_000,
|
|
1297
|
+
description: `Lint: ${path.basename(filePath)}`,
|
|
1298
|
+
cwd: projectRootFor(filePath),
|
|
1299
|
+
signal: options.signal,
|
|
1300
|
+
});
|
|
1301
|
+
const rawOutput = typeof output === 'string' ? output : String(output);
|
|
1302
|
+
if (/^Error:\s*Command cancelled by user/i.test(rawOutput)) {
|
|
1303
|
+
return { success: false, output: 'Cancelled by user', _cancelled: true, _tool: 'lint_check' };
|
|
1304
|
+
}
|
|
1305
|
+
return { success: true, output: rawOutput, issues: rawOutput.split('\n').filter(Boolean), _tool: 'lint_check' };
|
|
1306
|
+
} catch (err) {
|
|
1307
|
+
if (isAbortError(err) || options.signal?.aborted) return cancelledToolResult('lint_check');
|
|
1308
|
+
return { success: false, output: err.message, _tool: 'lint_check' };
|
|
1309
|
+
}
|
|
1310
|
+
},
|
|
1311
|
+
|
|
1312
|
+
// 15. run_tests
|
|
1313
|
+
run_tests: async (args, options = {}) => {
|
|
1314
|
+
try {
|
|
1315
|
+
throwIfAborted(options.signal);
|
|
1316
|
+
const cmd = args.command || 'npm test';
|
|
1317
|
+
const cwd = await commandCwd(args);
|
|
1318
|
+
const output = await occRegistry.call('Bash', {
|
|
1319
|
+
command: cmd,
|
|
1320
|
+
timeout: Math.min(args.timeout || 120_000, 600_000),
|
|
1321
|
+
description: `Run tests: ${cmd.slice(0, 80)}`,
|
|
1322
|
+
cwd,
|
|
1323
|
+
signal: options.signal,
|
|
1324
|
+
});
|
|
1325
|
+
const rawOutput = typeof output === 'string' ? output : String(output);
|
|
1326
|
+
if (/^Error:\s*Command cancelled by user/i.test(rawOutput)) {
|
|
1327
|
+
return { success: false, output: 'Cancelled by user', exit_code: 130, _cancelled: true, _tool: 'run_tests' };
|
|
1328
|
+
}
|
|
1329
|
+
const exitMatch = rawOutput.match(/Exit code: (\d+)/);
|
|
1330
|
+
const timedOut = /^Error:\s*Command timed out/i.test(rawOutput);
|
|
1331
|
+
return {
|
|
1332
|
+
success: !exitMatch && !timedOut,
|
|
1333
|
+
output: rawOutput.slice(-3000),
|
|
1334
|
+
exit_code: exitMatch ? Number(exitMatch[1]) : (timedOut ? 124 : 0),
|
|
1335
|
+
_tool: 'run_tests',
|
|
1336
|
+
};
|
|
1337
|
+
} catch (err) {
|
|
1338
|
+
if (isAbortError(err) || options.signal?.aborted) return cancelledToolResult('run_tests');
|
|
1339
|
+
const output = (err.stdout || '') + (err.stderr || '');
|
|
1340
|
+
return { success: false, output: output.slice(-3000), exit_code: err.status, _tool: 'run_tests' };
|
|
1341
|
+
}
|
|
1342
|
+
},
|
|
1343
|
+
|
|
1344
|
+
// 16. git_diff
|
|
1345
|
+
git_diff: async (args) => {
|
|
1346
|
+
try {
|
|
1347
|
+
const filePath = args.file_path ? `-- "${args.file_path}"` : '';
|
|
1348
|
+
const cwd = await commandCwd(args);
|
|
1349
|
+
const output = execSync(`git diff ${filePath}`, {
|
|
1350
|
+
stdio: 'pipe', timeout: 10_000, cwd, encoding: 'utf-8',
|
|
1351
|
+
}).toString();
|
|
1352
|
+
return { success: true, output: output.slice(-5000) || '(no changes)', _tool: 'git_diff' };
|
|
1353
|
+
} catch (err) {
|
|
1354
|
+
return { success: false, output: err.message, _tool: 'git_diff' };
|
|
1355
|
+
}
|
|
1356
|
+
},
|
|
1357
|
+
|
|
1358
|
+
// 17. git_status
|
|
1359
|
+
git_status: async (args) => {
|
|
1360
|
+
try {
|
|
1361
|
+
const cwd = await commandCwd(args);
|
|
1362
|
+
const output = execSync('git status --short', {
|
|
1363
|
+
stdio: 'pipe', timeout: 10_000, cwd, encoding: 'utf-8',
|
|
1364
|
+
}).toString();
|
|
1365
|
+
return { success: true, output: output || '(clean)', _tool: 'git_status' };
|
|
1366
|
+
} catch (err) {
|
|
1367
|
+
return { success: false, output: err.message, _tool: 'git_status' };
|
|
1368
|
+
}
|
|
1369
|
+
},
|
|
1370
|
+
|
|
1371
|
+
// 18. analyze_code — AST-based structured code analysis
|
|
1372
|
+
// Returns function signatures, classes, imports instead of raw file contents
|
|
1373
|
+
// 10x more token-efficient than read_file
|
|
1374
|
+
analyze_code: async (args) => {
|
|
1375
|
+
const filePath = await resolvePath(args.file_path || args.path, args);
|
|
1376
|
+
let stat;
|
|
1377
|
+
try {
|
|
1378
|
+
stat = fs.statSync(filePath);
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
return { success: false, output: `Error: ${err.message}`, structure: {}, _tool: 'analyze_code' };
|
|
1381
|
+
}
|
|
1382
|
+
if (stat.isDirectory()) {
|
|
1383
|
+
return {
|
|
1384
|
+
success: false,
|
|
1385
|
+
output: `Error: analyze_code expects a file, but got directory: ${filePath}. Use list_files/search_code first, then pass a specific source file.`,
|
|
1386
|
+
structure: {},
|
|
1387
|
+
_tool: 'analyze_code',
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
const result = analyzeCode(filePath, {
|
|
1391
|
+
startLine: args.start_line,
|
|
1392
|
+
endLine: args.end_line,
|
|
1393
|
+
});
|
|
1394
|
+
return {
|
|
1395
|
+
success: result.success,
|
|
1396
|
+
output: result.summary,
|
|
1397
|
+
structure: result.structure,
|
|
1398
|
+
_tool: 'analyze_code',
|
|
1399
|
+
};
|
|
1400
|
+
},
|
|
1401
|
+
|
|
1402
|
+
// Project overview — on-demand index + skeleton
|
|
1403
|
+
get_project_overview: async (args) => {
|
|
1404
|
+
const projectPath = args.path || args.project_path;
|
|
1405
|
+
const result = await projectRegistry.register(projectPath, {
|
|
1406
|
+
forceRefresh: Boolean(args.force_refresh || args.forceRefresh),
|
|
1407
|
+
});
|
|
1408
|
+
return {
|
|
1409
|
+
success: true,
|
|
1410
|
+
output: result.output,
|
|
1411
|
+
project_resource: result.resource,
|
|
1412
|
+
already_registered: result.already_registered,
|
|
1413
|
+
refreshed: result.refreshed,
|
|
1414
|
+
_tool: 'get_project_overview',
|
|
1415
|
+
};
|
|
1416
|
+
},
|
|
1417
|
+
|
|
1418
|
+
// Portable skills — metadata first, full content only on demand.
|
|
1419
|
+
skills_list: async (args) => ({
|
|
1420
|
+
success: true,
|
|
1421
|
+
output: JSON.stringify(skillsLoader.list({
|
|
1422
|
+
query: args.query || '',
|
|
1423
|
+
source: args.source || '',
|
|
1424
|
+
scope: args.scope || '',
|
|
1425
|
+
}), null, 2),
|
|
1426
|
+
skills: skillsLoader.list({
|
|
1427
|
+
query: args.query || '',
|
|
1428
|
+
source: args.source || '',
|
|
1429
|
+
scope: args.scope || '',
|
|
1430
|
+
}),
|
|
1431
|
+
_tool: 'skills_list',
|
|
1432
|
+
}),
|
|
1433
|
+
|
|
1434
|
+
skill_view: async (args) => {
|
|
1435
|
+
const skill = skillsLoader.view(
|
|
1436
|
+
args.name,
|
|
1437
|
+
args.path || null,
|
|
1438
|
+
{ sourceId: args.source_id || null },
|
|
1439
|
+
);
|
|
1440
|
+
return {
|
|
1441
|
+
success: true,
|
|
1442
|
+
output: JSON.stringify(skill, null, 2),
|
|
1443
|
+
skill,
|
|
1444
|
+
_tool: 'skill_view',
|
|
1445
|
+
};
|
|
1446
|
+
},
|
|
1447
|
+
|
|
1448
|
+
skill_install: async (args) => {
|
|
1449
|
+
const result = installer.install(args.source, {
|
|
1450
|
+
scope: skillScope(args),
|
|
1451
|
+
force: Boolean(args.force),
|
|
1452
|
+
});
|
|
1453
|
+
const skills = reloadSkillCatalog();
|
|
1454
|
+
const payload = { ...result, skills };
|
|
1455
|
+
return {
|
|
1456
|
+
success: true,
|
|
1457
|
+
output: JSON.stringify(payload, null, 2),
|
|
1458
|
+
...payload,
|
|
1459
|
+
_tool: 'skill_install',
|
|
1460
|
+
};
|
|
1461
|
+
},
|
|
1462
|
+
|
|
1463
|
+
skill_update: async (args) => {
|
|
1464
|
+
const result = installer.update(args.name, { scope: skillScope(args) });
|
|
1465
|
+
const skills = reloadSkillCatalog();
|
|
1466
|
+
const payload = { ...result, skills };
|
|
1467
|
+
return {
|
|
1468
|
+
success: true,
|
|
1469
|
+
output: JSON.stringify(payload, null, 2),
|
|
1470
|
+
...payload,
|
|
1471
|
+
_tool: 'skill_update',
|
|
1472
|
+
};
|
|
1473
|
+
},
|
|
1474
|
+
|
|
1475
|
+
skill_remove: async (args) => {
|
|
1476
|
+
const result = installer.remove(args.name, { scope: skillScope(args) });
|
|
1477
|
+
const skills = reloadSkillCatalog();
|
|
1478
|
+
const payload = { ...result, skills };
|
|
1479
|
+
return {
|
|
1480
|
+
success: true,
|
|
1481
|
+
output: JSON.stringify(payload, null, 2),
|
|
1482
|
+
...payload,
|
|
1483
|
+
_tool: 'skill_remove',
|
|
1484
|
+
};
|
|
1485
|
+
},
|
|
1486
|
+
|
|
1487
|
+
// User-defined agents — metadata first, project YAML + backend sync on demand.
|
|
1488
|
+
agents_list: async (args = {}) => {
|
|
1489
|
+
const agents = filterLocalAgents(args).map(compactAgentMetadata);
|
|
1490
|
+
const payload = { agents, count: agents.length };
|
|
1491
|
+
return {
|
|
1492
|
+
success: true,
|
|
1493
|
+
output: JSON.stringify(payload, null, 2),
|
|
1494
|
+
...payload,
|
|
1495
|
+
_tool: 'agents_list',
|
|
1496
|
+
};
|
|
1497
|
+
},
|
|
1498
|
+
|
|
1499
|
+
agent_create: async (args = {}) => {
|
|
1500
|
+
if (!args.name || !String(args.name).trim()) {
|
|
1501
|
+
throw new Error('name is required');
|
|
1502
|
+
}
|
|
1503
|
+
const result = createAgentFile({
|
|
1504
|
+
cwd: process.cwd(),
|
|
1505
|
+
name: args.name,
|
|
1506
|
+
description: args.description || '',
|
|
1507
|
+
role: args.role || 'specialist',
|
|
1508
|
+
model: args.model || '',
|
|
1509
|
+
tools: normalizeAgentTools(args.tools),
|
|
1510
|
+
prompt: args.system_prompt || args.prompt || '',
|
|
1511
|
+
force: Boolean(args.force),
|
|
1512
|
+
});
|
|
1513
|
+
const created = listLocalAgents(process.cwd()).find(agent => agent.slug === result.slug);
|
|
1514
|
+
const payload = {
|
|
1515
|
+
...result,
|
|
1516
|
+
agent: created ? compactAgentMetadata(created) : null,
|
|
1517
|
+
next_actions: [
|
|
1518
|
+
`Edit ${result.filePath}`,
|
|
1519
|
+
`Run /agents sync ${result.slug} when ready`,
|
|
1520
|
+
],
|
|
1521
|
+
};
|
|
1522
|
+
return {
|
|
1523
|
+
success: true,
|
|
1524
|
+
output: JSON.stringify(payload, null, 2),
|
|
1525
|
+
...payload,
|
|
1526
|
+
_tool: 'agent_create',
|
|
1527
|
+
};
|
|
1528
|
+
},
|
|
1529
|
+
|
|
1530
|
+
agent_sync: async (args = {}) => {
|
|
1531
|
+
const selected = selectAgentsForSync(args);
|
|
1532
|
+
if (!selected.length) {
|
|
1533
|
+
const target = args.name || args.slug || '';
|
|
1534
|
+
throw new Error(target ? `No local agent found: ${target}` : 'No local agents found in .bahulam/agents');
|
|
1535
|
+
}
|
|
1536
|
+
const creds = new TarangAuth().loadCredentials();
|
|
1537
|
+
const result = await syncAgentsToBackend({
|
|
1538
|
+
backendUrl: creds.backendUrl,
|
|
1539
|
+
token: creds.token,
|
|
1540
|
+
agents: selected,
|
|
1541
|
+
});
|
|
1542
|
+
const payload = {
|
|
1543
|
+
...result,
|
|
1544
|
+
agents: selected.map(compactAgentMetadata),
|
|
1545
|
+
};
|
|
1546
|
+
return {
|
|
1547
|
+
success: true,
|
|
1548
|
+
output: JSON.stringify(payload, null, 2),
|
|
1549
|
+
...payload,
|
|
1550
|
+
_tool: 'agent_sync',
|
|
1551
|
+
};
|
|
1552
|
+
},
|
|
1553
|
+
|
|
1554
|
+
workflow_list: async (args = {}) => {
|
|
1555
|
+
const local = filterLocalWorkflows(args).map(compactWorkflowMetadata);
|
|
1556
|
+
let backend = [];
|
|
1557
|
+
try {
|
|
1558
|
+
const creds = new TarangAuth().loadCredentials();
|
|
1559
|
+
if (creds.backendUrl && creds.token) {
|
|
1560
|
+
const resp = await fetch(`${creds.backendUrl}${WORKFLOW_SYNC_ENDPOINT}`, {
|
|
1561
|
+
headers: {
|
|
1562
|
+
Authorization: `Bearer ${creds.token}`,
|
|
1563
|
+
Accept: 'application/json',
|
|
1564
|
+
},
|
|
1565
|
+
});
|
|
1566
|
+
if (resp.ok) {
|
|
1567
|
+
const payload = await resp.json().catch(() => ({}));
|
|
1568
|
+
backend = Array.isArray(payload.workflows) ? payload.workflows : [];
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
} catch {
|
|
1572
|
+
// best effort
|
|
1573
|
+
}
|
|
1574
|
+
const payload = {
|
|
1575
|
+
local_workflows: local,
|
|
1576
|
+
backend_workflows: backend,
|
|
1577
|
+
count: local.length + backend.length,
|
|
1578
|
+
};
|
|
1579
|
+
return {
|
|
1580
|
+
success: true,
|
|
1581
|
+
output: JSON.stringify(payload, null, 2),
|
|
1582
|
+
...payload,
|
|
1583
|
+
_tool: 'workflow_list',
|
|
1584
|
+
};
|
|
1585
|
+
},
|
|
1586
|
+
|
|
1587
|
+
workflow_create_multi: async (args = {}) => {
|
|
1588
|
+
if (!args.name || !String(args.name).trim()) {
|
|
1589
|
+
throw new Error('name is required');
|
|
1590
|
+
}
|
|
1591
|
+
const result = createWorkflowFile({
|
|
1592
|
+
cwd: process.cwd(),
|
|
1593
|
+
name: args.name,
|
|
1594
|
+
description: args.description || '',
|
|
1595
|
+
pattern: args.pattern || args.orchestration_pattern || 'sequential',
|
|
1596
|
+
agents: args.agents || [],
|
|
1597
|
+
edges: args.edges || [],
|
|
1598
|
+
globalParams: args.global_params || args.globalParams || {},
|
|
1599
|
+
force: Boolean(args.force),
|
|
1600
|
+
});
|
|
1601
|
+
const workflow = listLocalWorkflows(process.cwd()).find(item => item.slug === result.slug);
|
|
1602
|
+
const payload = {
|
|
1603
|
+
...result,
|
|
1604
|
+
workflow: workflow ? compactWorkflowMetadata(workflow) : null,
|
|
1605
|
+
next_actions: [
|
|
1606
|
+
`Edit ${result.filePath}`,
|
|
1607
|
+
`Run workflow sync for ${result.slug} when ready`,
|
|
1608
|
+
],
|
|
1609
|
+
};
|
|
1610
|
+
return {
|
|
1611
|
+
success: true,
|
|
1612
|
+
output: JSON.stringify(payload, null, 2),
|
|
1613
|
+
...payload,
|
|
1614
|
+
_tool: 'workflow_create_multi',
|
|
1615
|
+
};
|
|
1616
|
+
},
|
|
1617
|
+
|
|
1618
|
+
workflow_sync_multi: async (args = {}) => {
|
|
1619
|
+
const selected = selectWorkflowsForSync(args);
|
|
1620
|
+
if (!selected.length) {
|
|
1621
|
+
const target = args.name || args.slug || '';
|
|
1622
|
+
throw new Error(target ? `No local workflow found: ${target}` : 'No local workflows found in .bahulam/workflows');
|
|
1623
|
+
}
|
|
1624
|
+
const creds = new TarangAuth().loadCredentials();
|
|
1625
|
+
if (!creds.backendUrl || !creds.token) {
|
|
1626
|
+
throw new Error('Not logged in. Run bahulam-code login first.');
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
const headers = {
|
|
1630
|
+
Authorization: `Bearer ${creds.token}`,
|
|
1631
|
+
'Content-Type': 'application/json',
|
|
1632
|
+
};
|
|
1633
|
+
const listResp = await fetch(`${creds.backendUrl}${WORKFLOW_SYNC_ENDPOINT}`, {
|
|
1634
|
+
headers: { Authorization: `Bearer ${creds.token}`, Accept: 'application/json' },
|
|
1635
|
+
});
|
|
1636
|
+
const backendPayload = listResp.ok ? await listResp.json().catch(() => ({})) : {};
|
|
1637
|
+
const existing = Array.isArray(backendPayload.workflows) ? backendPayload.workflows : [];
|
|
1638
|
+
const existingByName = new Map(existing.map(item => [String(item.name || '').toLowerCase(), item]));
|
|
1639
|
+
|
|
1640
|
+
const results = [];
|
|
1641
|
+
for (const workflow of selected) {
|
|
1642
|
+
const payload = {
|
|
1643
|
+
name: workflow.name,
|
|
1644
|
+
description: workflow.description || '',
|
|
1645
|
+
graph: workflow.graph,
|
|
1646
|
+
global_params: workflow.global_params || {},
|
|
1647
|
+
orchestration_pattern: workflow.pattern || workflow.orchestration_pattern || 'sequential',
|
|
1648
|
+
pattern: workflow.pattern || workflow.orchestration_pattern || 'sequential',
|
|
1649
|
+
};
|
|
1650
|
+
const existingWorkflow = existingByName.get(String(workflow.name || '').toLowerCase());
|
|
1651
|
+
const endpoint = existingWorkflow
|
|
1652
|
+
? `${creds.backendUrl}${WORKFLOW_SYNC_ENDPOINT}/${encodeURIComponent(existingWorkflow.id)}`
|
|
1653
|
+
: `${creds.backendUrl}${WORKFLOW_SYNC_ENDPOINT}`;
|
|
1654
|
+
const method = existingWorkflow ? 'PATCH' : 'POST';
|
|
1655
|
+
const resp = await fetch(endpoint, {
|
|
1656
|
+
method,
|
|
1657
|
+
headers,
|
|
1658
|
+
body: JSON.stringify(payload),
|
|
1659
|
+
});
|
|
1660
|
+
if (!resp.ok) {
|
|
1661
|
+
let detail = '';
|
|
1662
|
+
try {
|
|
1663
|
+
const data = await resp.json();
|
|
1664
|
+
detail = data.detail || data.error || JSON.stringify(data);
|
|
1665
|
+
} catch {
|
|
1666
|
+
detail = await resp.text().catch(() => '');
|
|
1667
|
+
}
|
|
1668
|
+
throw new Error(`Workflow sync failed (${resp.status})${detail ? `: ${detail}` : ''}`);
|
|
1669
|
+
}
|
|
1670
|
+
const data = await resp.json().catch(() => ({}));
|
|
1671
|
+
results.push({
|
|
1672
|
+
workflow: compactWorkflowMetadata(workflow),
|
|
1673
|
+
action: existingWorkflow ? 'updated' : 'created',
|
|
1674
|
+
id: data?.workflow?.id || data?.id || existingWorkflow?.id || null,
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
const payload = {
|
|
1679
|
+
workflows: results,
|
|
1680
|
+
created: results.filter(item => item.action === 'created').length,
|
|
1681
|
+
updated: results.filter(item => item.action === 'updated').length,
|
|
1682
|
+
};
|
|
1683
|
+
return {
|
|
1684
|
+
success: true,
|
|
1685
|
+
output: JSON.stringify(payload, null, 2),
|
|
1686
|
+
...payload,
|
|
1687
|
+
_tool: 'workflow_sync_multi',
|
|
1688
|
+
};
|
|
1689
|
+
},
|
|
1690
|
+
|
|
1691
|
+
workflow_run_multi: async (args = {}, options = {}) => {
|
|
1692
|
+
const target = String(args.workflow_id || args.workflowId || args.id || args.name || args.slug || '').trim();
|
|
1693
|
+
if (!target) {
|
|
1694
|
+
throw new Error('workflow_id is required');
|
|
1695
|
+
}
|
|
1696
|
+
const creds = new TarangAuth().loadCredentials();
|
|
1697
|
+
if (!creds.backendUrl || !creds.token) {
|
|
1698
|
+
throw new Error('Not logged in. Run bahulam-code login first.');
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
const workflowId = await resolveWorkflowId(creds, target);
|
|
1702
|
+
if (!workflowId) {
|
|
1703
|
+
const localMatch = listLocalWorkflows(process.cwd()).find(workflow => workflowTargetMatches(workflow, target));
|
|
1704
|
+
if (localMatch) {
|
|
1705
|
+
throw new Error(`Workflow '${target}' exists locally but is not synced yet. Run workflow_sync_multi before workflow_run_multi.`);
|
|
1706
|
+
}
|
|
1707
|
+
throw new Error(`Workflow not found: ${target}`);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
const url = `${creds.backendUrl}/api/workflows/${encodeURIComponent(workflowId)}/run-multi`;
|
|
1711
|
+
const approvalAllowedTools = [
|
|
1712
|
+
'get_project_overview',
|
|
1713
|
+
'write_file',
|
|
1714
|
+
'write_project',
|
|
1715
|
+
'edit_file',
|
|
1716
|
+
'shell',
|
|
1717
|
+
'lint_check',
|
|
1718
|
+
'validate_build',
|
|
1719
|
+
'run_tests',
|
|
1720
|
+
'agents_list',
|
|
1721
|
+
'agent_create',
|
|
1722
|
+
'agent_sync',
|
|
1723
|
+
'workflow_list',
|
|
1724
|
+
'workflow_create_multi',
|
|
1725
|
+
'workflow_sync_multi',
|
|
1726
|
+
'workflow_run_multi',
|
|
1727
|
+
];
|
|
1728
|
+
const instruction = args.instruction || '';
|
|
1729
|
+
const projectResources = projectRegistry.resources();
|
|
1730
|
+
const workflowScope = buildWorkScope({
|
|
1731
|
+
instruction,
|
|
1732
|
+
cwd: process.cwd(),
|
|
1733
|
+
projectResources,
|
|
1734
|
+
});
|
|
1735
|
+
const suppliedGlobalParams = args.global_params || args.globalParams || {};
|
|
1736
|
+
const globalParams = {
|
|
1737
|
+
instruction,
|
|
1738
|
+
cwd: process.cwd(),
|
|
1739
|
+
project_root: process.cwd(),
|
|
1740
|
+
project_resources: projectResources,
|
|
1741
|
+
work_scope: workflowScope,
|
|
1742
|
+
...suppliedGlobalParams,
|
|
1743
|
+
};
|
|
1744
|
+
const body = {
|
|
1745
|
+
trigger_input: {
|
|
1746
|
+
instruction,
|
|
1747
|
+
cwd: process.cwd(),
|
|
1748
|
+
project_root: process.cwd(),
|
|
1749
|
+
work_scope: globalParams.work_scope,
|
|
1750
|
+
},
|
|
1751
|
+
global_params: globalParams,
|
|
1752
|
+
orchestration_pattern: args.pattern || args.orchestration_pattern || 'sequential',
|
|
1753
|
+
pattern: args.pattern || args.orchestration_pattern || 'sequential',
|
|
1754
|
+
// Multi-agent workflow runs are SSE-only today. Do not forward
|
|
1755
|
+
// a model-supplied "sync" mode into the backend 400 path.
|
|
1756
|
+
mode: 'stream',
|
|
1757
|
+
approval_scope: {
|
|
1758
|
+
approved: true,
|
|
1759
|
+
source: 'cli_hitl',
|
|
1760
|
+
scope: 'workflow_run',
|
|
1761
|
+
workflow_id: workflowId,
|
|
1762
|
+
target,
|
|
1763
|
+
allowed_tools: approvalAllowedTools,
|
|
1764
|
+
allow_destructive: false,
|
|
1765
|
+
reason: 'User approved workflow execution',
|
|
1766
|
+
},
|
|
1767
|
+
};
|
|
1768
|
+
|
|
1769
|
+
const resp = await fetch(url, {
|
|
1770
|
+
method: 'POST',
|
|
1771
|
+
headers: {
|
|
1772
|
+
Authorization: `Bearer ${creds.token}`,
|
|
1773
|
+
Accept: 'text/event-stream',
|
|
1774
|
+
'Content-Type': 'application/json',
|
|
1775
|
+
},
|
|
1776
|
+
body: JSON.stringify(body),
|
|
1777
|
+
signal: options.signal,
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
if (!resp.ok) {
|
|
1781
|
+
let detail = '';
|
|
1782
|
+
try {
|
|
1783
|
+
const data = await resp.json();
|
|
1784
|
+
detail = data.detail || data.error || JSON.stringify(data);
|
|
1785
|
+
} catch {
|
|
1786
|
+
detail = await resp.text().catch(() => '');
|
|
1787
|
+
}
|
|
1788
|
+
throw new Error(`Workflow run failed (${resp.status})${detail ? `: ${detail}` : ''}`);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
const events = [];
|
|
1792
|
+
let final = null;
|
|
1793
|
+
let nestedToolCalls = 0;
|
|
1794
|
+
let callbacksPosted = 0;
|
|
1795
|
+
const workflowTaskId = resp.headers.get('X-Task-ID') || resp.headers.get('X-Workflow-Run-ID');
|
|
1796
|
+
for await (const event of streamResponse(resp)) {
|
|
1797
|
+
events.push(event.type);
|
|
1798
|
+
if (event.type === 'tool_call' || event.type === 'tool_request') {
|
|
1799
|
+
if (event.server_side || event.data?.server_side) continue;
|
|
1800
|
+
const toolName = event.tool || event.name || event.data?.tool || event.data?.name;
|
|
1801
|
+
const callId = event.call_id || event.id || event.data?.call_id || event.data?.id;
|
|
1802
|
+
const toolArgs = event.args || event.input || event.data?.args || event.data?.input || {};
|
|
1803
|
+
if (!workflowTaskId || !callId || !toolName) {
|
|
1804
|
+
throw new Error(`Workflow tool call missing callback metadata: ${JSON.stringify(event).slice(0, 300)}`);
|
|
1805
|
+
}
|
|
1806
|
+
if (toolName === 'workflow_run_multi') {
|
|
1807
|
+
const nestedResult = {
|
|
1808
|
+
success: false,
|
|
1809
|
+
output: 'Nested workflow_run_multi is not allowed inside a workflow run.',
|
|
1810
|
+
};
|
|
1811
|
+
await sendCallback(creds.backendUrl, creds.token, workflowTaskId, callId, nestedResult);
|
|
1812
|
+
throw new Error(nestedResult.output);
|
|
1813
|
+
}
|
|
1814
|
+
nestedToolCalls++;
|
|
1815
|
+
const result = await executeToolWithHooks(toolName, toolArgs, {
|
|
1816
|
+
...options,
|
|
1817
|
+
workflowRun: true,
|
|
1818
|
+
workflowId,
|
|
1819
|
+
workflowTaskId,
|
|
1820
|
+
});
|
|
1821
|
+
const posted = await sendCallback(creds.backendUrl, creds.token, workflowTaskId, callId, result);
|
|
1822
|
+
if (!posted) {
|
|
1823
|
+
throw new Error(`Workflow tool callback failed for ${toolName}`);
|
|
1824
|
+
}
|
|
1825
|
+
callbacksPosted++;
|
|
1826
|
+
} else if (event.type === 'approval_required') {
|
|
1827
|
+
const toolId = event.tool_id || event.id || event.data?.tool_id || event.data?.id;
|
|
1828
|
+
const toolName = event.tool || event.data?.tool || 'tool';
|
|
1829
|
+
if (workflowTaskId && toolId) {
|
|
1830
|
+
await sendApprovalDecision(
|
|
1831
|
+
creds.backendUrl,
|
|
1832
|
+
creds.token,
|
|
1833
|
+
workflowTaskId,
|
|
1834
|
+
toolId,
|
|
1835
|
+
'deny',
|
|
1836
|
+
'once',
|
|
1837
|
+
'Workflow run approval scope did not include this operation',
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
throw new Error(`Workflow requested additional approval for ${toolName}; the upfront workflow-run approval scope did not cover it.`);
|
|
1841
|
+
} else if (event.type === 'orchestration_complete') {
|
|
1842
|
+
final = event;
|
|
1843
|
+
} else if (event.type === 'run_error') {
|
|
1844
|
+
const detail = event.error || event.data?.error || event.message || 'Workflow run failed';
|
|
1845
|
+
throw new Error(detail);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
const payload = {
|
|
1850
|
+
workflow_id: workflowId,
|
|
1851
|
+
target,
|
|
1852
|
+
pattern: body.pattern,
|
|
1853
|
+
run_id: final?.run_id || null,
|
|
1854
|
+
result: final?.result || final?.data?.result || '',
|
|
1855
|
+
total_tokens: final?.total_tokens || 0,
|
|
1856
|
+
total_cost: final?.total_cost || 0,
|
|
1857
|
+
duration_s: final?.duration_s || 0,
|
|
1858
|
+
agent_count: final?.agent_count || 0,
|
|
1859
|
+
events_seen: events.length,
|
|
1860
|
+
nested_tool_calls: nestedToolCalls,
|
|
1861
|
+
callbacks_posted: callbacksPosted,
|
|
1862
|
+
};
|
|
1863
|
+
|
|
1864
|
+
return {
|
|
1865
|
+
success: true,
|
|
1866
|
+
output: JSON.stringify(payload, null, 2),
|
|
1867
|
+
...payload,
|
|
1868
|
+
_tool: 'workflow_run_multi',
|
|
1869
|
+
};
|
|
1870
|
+
},
|
|
1871
|
+
};
|
|
1872
|
+
|
|
1873
|
+
return {
|
|
1874
|
+
/**
|
|
1875
|
+
* Execute a Tarang tool by name.
|
|
1876
|
+
* @param {string} name - Tarang tool name
|
|
1877
|
+
* @param {Object} args - Tool arguments
|
|
1878
|
+
* @returns {Promise<Object>} - { success, output, ... }
|
|
1879
|
+
*/
|
|
1880
|
+
async execute(name, args, options = {}) {
|
|
1881
|
+
return executeToolWithHooks(name, args, options);
|
|
1882
|
+
},
|
|
1883
|
+
|
|
1884
|
+
/** List all available tool names. */
|
|
1885
|
+
listTools() {
|
|
1886
|
+
return Object.keys(toolMap);
|
|
1887
|
+
},
|
|
1888
|
+
|
|
1889
|
+
getProjectResources() {
|
|
1890
|
+
return projectRegistry.resources();
|
|
1891
|
+
},
|
|
1892
|
+
|
|
1893
|
+
async registerProjectRoots(roots, { forceRefresh = false } = {}) {
|
|
1894
|
+
const results = [];
|
|
1895
|
+
const seen = new Set();
|
|
1896
|
+
for (const root of Array.isArray(roots) ? roots : []) {
|
|
1897
|
+
if (!root || seen.has(root)) continue;
|
|
1898
|
+
seen.add(root);
|
|
1899
|
+
try {
|
|
1900
|
+
const result = await projectRegistry.register(root, { forceRefresh });
|
|
1901
|
+
results.push({ success: true, root: result.resource.root, ...result });
|
|
1902
|
+
} catch (err) {
|
|
1903
|
+
results.push({ success: false, root, error: err.message });
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
return results;
|
|
1907
|
+
},
|
|
1908
|
+
|
|
1909
|
+
getAgentContext() {
|
|
1910
|
+
const global = projectRegistry.getGlobalContext();
|
|
1911
|
+
return {
|
|
1912
|
+
identity: global.identity,
|
|
1913
|
+
preferences: global.preferences,
|
|
1914
|
+
global_skills: skillsLoader.list(),
|
|
1915
|
+
available_agents: listLocalAgents(process.cwd()).map(agent => ({
|
|
1916
|
+
slug: agent.slug,
|
|
1917
|
+
name: agent.name,
|
|
1918
|
+
description: agent.description,
|
|
1919
|
+
role: agent.role,
|
|
1920
|
+
model: agent.model,
|
|
1921
|
+
models: agent.models,
|
|
1922
|
+
tools: agent.tools,
|
|
1923
|
+
capabilities: agent.capabilities,
|
|
1924
|
+
domains: agent.domains,
|
|
1925
|
+
source_scope: agent.source_scope,
|
|
1926
|
+
spec: agent.spec,
|
|
1927
|
+
})),
|
|
1928
|
+
available_workflows: listLocalWorkflows(process.cwd()).map(workflow => ({
|
|
1929
|
+
slug: workflow.slug,
|
|
1930
|
+
name: workflow.name,
|
|
1931
|
+
description: workflow.description || '',
|
|
1932
|
+
pattern: workflow.pattern || workflow.orchestration_pattern || 'sequential',
|
|
1933
|
+
agent_count: workflow.agent_count || 0,
|
|
1934
|
+
edge_count: workflow.edge_count || 0,
|
|
1935
|
+
source_scope: 'project',
|
|
1936
|
+
})),
|
|
1937
|
+
source: 'cli',
|
|
1938
|
+
};
|
|
1939
|
+
},
|
|
1940
|
+
|
|
1941
|
+
reloadSkills(cwd = process.cwd()) {
|
|
1942
|
+
skillsLoader.load(cwd);
|
|
1943
|
+
return skillsLoader.list();
|
|
1944
|
+
},
|
|
1945
|
+
|
|
1946
|
+
resetProjects() {
|
|
1947
|
+
projectRegistry.reset();
|
|
1948
|
+
},
|
|
1949
|
+
};
|
|
1950
|
+
}
|