glitool 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +115 -48
  2. package/dist/agent.js +232 -37
  3. package/dist/agents/coder.js +46 -34
  4. package/dist/agents/debugger.js +111 -0
  5. package/dist/agents/explainer.js +2 -5
  6. package/dist/agents/git-agent.js +90 -0
  7. package/dist/agents/graph.js +214 -23
  8. package/dist/agents/judge.js +61 -0
  9. package/dist/agents/planner.js +31 -12
  10. package/dist/agents/planningAgent.js +41 -0
  11. package/dist/agents/refactorer.js +97 -0
  12. package/dist/agents/reviewer-agent.js +87 -0
  13. package/dist/agents/reviewer.js +6 -9
  14. package/dist/agents/types.js +1 -0
  15. package/dist/agents/validator.js +93 -0
  16. package/dist/agents/workflow.js +45 -0
  17. package/dist/auth.js +87 -0
  18. package/dist/commands/version.js +1 -0
  19. package/dist/config.js +4 -1
  20. package/dist/confirmHandler.js +4 -2
  21. package/dist/index.js +12 -25
  22. package/dist/llm/classifier.js +61 -0
  23. package/dist/llm/factory.js +50 -0
  24. package/dist/llm/router.js +191 -22
  25. package/dist/logger.js +25 -0
  26. package/dist/processEvents.js +1 -0
  27. package/dist/tools/bashTool.js +90 -0
  28. package/dist/tools/editFileTool.js +14 -3
  29. package/dist/tools/index.js +3 -1
  30. package/dist/tools/listFilesTool.js +19 -21
  31. package/dist/tools/processRegistry.js +36 -0
  32. package/dist/tools/readBackgroundOutput.js +29 -0
  33. package/dist/tools/readFileTool.js +64 -9
  34. package/dist/tools/searchCodeTool.js +14 -4
  35. package/dist/tools/webFetchTool.js +45 -0
  36. package/dist/tools/writeFileTool.js +9 -5
  37. package/dist/trust/riskScorer.js +29 -2
  38. package/dist/ui/App.js +384 -47
  39. package/dist/ui/AuthFlow.js +76 -0
  40. package/dist/ui/ConfirmCard.js +53 -0
  41. package/dist/ui/EscalationCard.js +22 -0
  42. package/dist/ui/ExplainCard.js +5 -0
  43. package/dist/ui/Pipeline.js +37 -0
  44. package/dist/ui/ProcessTrace.js +79 -0
  45. package/dist/ui/RoleRow.js +16 -0
  46. package/dist/ui/RoleRow.test.js +8 -0
  47. package/dist/ui/SlashPalette.js +32 -0
  48. package/dist/ui/StatusBar.js +44 -0
  49. package/dist/ui/ToolLog.js +62 -0
  50. package/dist/ui/Welcome.js +11 -0
  51. package/dist/ui/renderMarkdown.js +41 -0
  52. package/dist/ui/symbols.js +19 -0
  53. package/dist/ui/tokens.js +13 -0
  54. package/dist/version.js +1 -0
  55. package/package.json +56 -54
@@ -1,15 +1,26 @@
1
+ import { classifyWithLlm } from './classifier.js';
2
+ import { loadConfig } from '../config.js';
1
3
  const MODEL_BY_TIER = {
2
- quick: 'gpt-4o-mini',
3
- standard: 'gpt-4o-mini',
4
- complex: 'gpt-4o-mini'
4
+ quick: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
5
+ standard: 'Qwen/Qwen2.5-Coder-72B-Instruct',
6
+ complex: 'deepseek-ai/DeepSeek-V3',
5
7
  };
8
+ const ANAPHORA_PATTERNS = [
9
+ /\b(this|that|it|those|these)\b/i,
10
+ /\b(again|instead|previous|prior)\b/i,
11
+ ];
12
+ const VAGUE_IMPERATIVES = [
13
+ /^(do it|fix it|redo|undo|keep going|continue|proceed|go ahead)\b/i,
14
+ ];
6
15
  const CHAT_PATTERNS = [
7
- /^(hi|hello|hey|thanks|thank you|ok|sure|yes|no|great|awesome)\b/i,
16
+ /^(hi+|hello|hey|thanks|thank you|ok|sure|yes|no|great|awesome)\b/i,
8
17
  /^\/\w+/,
9
18
  ];
10
19
  const EXPLANATION_PATTERNS = [
11
20
  /^(explain|describe|what is|what are|tell me about|how does|why does|what does)\s/i,
12
21
  /^(what|who|when|where|how|why)\s.{0,60}\?$/i,
22
+ /^(read|show|open|cat|view|display|print)\s+\S+\.\w+/i,
23
+ /^(read|show|open|view)\s+(this|that|the)?\s*\S+/i,
13
24
  ];
14
25
  const CODING_PATTERNS = [
15
26
  /refactor|rewrite|redesign/i,
@@ -24,16 +35,123 @@ const PLANNING_PATTERNS = [
24
35
  /plan|roadmap|strategy|approach/i,
25
36
  /how should (i|we)|what.s the best way/i,
26
37
  ];
38
+ const EXPLICIT_ROUTES = {
39
+ '/plan': { domain: 'planning', tier: 'complex' },
40
+ '/coder': { domain: 'coding', tier: 'standard' },
41
+ '/quick': { domain: 'chat', tier: 'quick' },
42
+ '/explain': { domain: 'explanation', tier: 'quick' },
43
+ '/debug': { domain: 'debugging', tier: 'standard' },
44
+ '/refactor': { domain: 'refactoring', tier: 'complex' },
45
+ '/review': { domain: 'review', tier: 'standard' },
46
+ '/git': { domain: 'git', tier: 'quick' },
47
+ };
48
+ const DEBUGGING_PATTERNS = [
49
+ /\berror:/i,
50
+ /\b(crash|crashes|crashed|crashing)\b/i,
51
+ /\b(fail|fails|failed|failing)\b/i,
52
+ /\b(broken|broke|breaks|breaking)\b/i,
53
+ /\bfix this\b/i,
54
+ /\b(exception|traceback|stack trace)\b/i,
55
+ /\bnot working\b/i,
56
+ /\bwhy (is|does|isn't|doesn't|did|won't)\b/i,
57
+ ];
58
+ const REFACTORING_PATTERNS = [
59
+ /\brefactor(ed|ing|s)?\b/i,
60
+ /\bclean (this|it|up)\b/i,
61
+ /\bsimplif(y|ied|ying|ies)\b/i,
62
+ /\brestructur(e|ed|ing|es)\b/i,
63
+ /\brenam(e|ed|ing|es)\b/i,
64
+ /\bextract(ed|ing|s)?\b/i,
65
+ /\breduc(e|ed|ing|es) duplication\b/i,
66
+ /\bmake (this|it) more readable\b/i,
67
+ ];
68
+ const REVIEW_PATTERNS = [
69
+ /\breview(ed|ing|s)?\b/i,
70
+ /\baudit(ed|ing|s)?\b/i,
71
+ /\bcheck for (issues|bugs|problems|errors)\b/i,
72
+ /\bis this (good|correct|right|ok|fine)\b/i,
73
+ /\bsecurity (check|review|audit)\b/i,
74
+ /\bcode review\b/i,
75
+ /\blook (over|at) (this|my)\b/i,
76
+ ];
77
+ const GIT_PATTERNS = [
78
+ /\b(commit|commits|committed|committing)\b/i,
79
+ /\bgit\b/i,
80
+ /\bwhat (changed|was changed|got changed)\b/i,
81
+ /\bstage[ds]?\b/i,
82
+ /\bstaging\b/i,
83
+ /\bpush(ed|ing|es)?\b/i,
84
+ /\bpull(ed|ing|s)?\b/i,
85
+ /\bbranch(es|ing|ed)?\b/i,
86
+ /\bmerg(e|ed|ing|es)\b/i,
87
+ /\brebas(e|ed|ing|es)\b/i,
88
+ /\bcheckout\b/i,
89
+ /\bdiff\b/i,
90
+ /\bcommit message\b/i,
91
+ /\b(repo|repository)\b/i,
92
+ ];
93
+ function hasAnaphora(prompt) {
94
+ return ANAPHORA_PATTERNS.some(p => p.test(prompt))
95
+ || VAGUE_IMPERATIVES.some(p => p.test(prompt));
96
+ }
97
+ export function parseExplicitRoute(prompt) {
98
+ const trimmed = prompt.trim();
99
+ for (const [cmd, route] of Object.entries(EXPLICIT_ROUTES)) {
100
+ if (trimmed.startsWith(cmd + ' ') || trimmed === cmd) {
101
+ return {
102
+ tier: route.tier,
103
+ domain: route.domain,
104
+ complexityScore: 0,
105
+ recommendedModel: getModel(route.tier),
106
+ reason: `explicit: ${cmd}`,
107
+ source: 'explicit',
108
+ confidence: 'high', // ← ADD
109
+ };
110
+ }
111
+ }
112
+ return null;
113
+ }
114
+ export function stripExplicitPrefix(prompt) {
115
+ const trimmed = prompt.trim();
116
+ for (const cmd of Object.keys(EXPLICIT_ROUTES)) {
117
+ if (trimmed.startsWith(cmd + ' ')) {
118
+ return trimmed.slice(cmd.length + 1).trim();
119
+ }
120
+ if (trimmed === cmd) {
121
+ return '';
122
+ }
123
+ }
124
+ return prompt;
125
+ }
126
+ function getModel(tier) {
127
+ const userPref = loadConfig().preferredModel;
128
+ // User preference applies ONLY to the quick tier (chat/explain).
129
+ // Coding, planning, refactoring always use the tier-appropriate strong model.
130
+ if (userPref && tier === 'quick')
131
+ return userPref;
132
+ return MODEL_BY_TIER[tier];
133
+ }
134
+ export function getModelForTier(tier) {
135
+ return MODEL_BY_TIER[tier];
136
+ }
27
137
  function detectDomain(prompt) {
28
138
  if (CHAT_PATTERNS.some(p => p.test(prompt)))
29
- return 'chat';
139
+ return { domain: 'chat', matched: true };
140
+ if (GIT_PATTERNS.some(p => p.test(prompt)))
141
+ return { domain: 'git', matched: true };
142
+ if (REVIEW_PATTERNS.some(p => p.test(prompt)))
143
+ return { domain: 'review', matched: true };
144
+ if (REFACTORING_PATTERNS.some(p => p.test(prompt)))
145
+ return { domain: 'refactoring', matched: true };
146
+ if (DEBUGGING_PATTERNS.some(p => p.test(prompt)))
147
+ return { domain: 'debugging', matched: true };
30
148
  if (EXPLANATION_PATTERNS.some(p => p.test(prompt)))
31
- return 'explanation';
149
+ return { domain: 'explanation', matched: true };
32
150
  if (CODING_PATTERNS.some(p => p.test(prompt)))
33
- return 'coding';
151
+ return { domain: 'coding', matched: true };
34
152
  if (PLANNING_PATTERNS.some(p => p.test(prompt)))
35
- return 'planning';
36
- return 'chat';
153
+ return { domain: 'planning', matched: true };
154
+ return { domain: 'chat', matched: false };
37
155
  }
38
156
  function scoreComplexity(prompt) {
39
157
  let score = 0;
@@ -50,28 +168,79 @@ function scoreComplexity(prompt) {
50
168
  return score;
51
169
  }
52
170
  function selectTier(domain, score) {
53
- if (domain === 'chat')
171
+ if (domain === 'chat' || domain === 'git')
54
172
  return 'quick';
55
173
  if (domain === 'explanation' && score <= 1)
56
174
  return 'quick';
57
- if (score >= 3 || domain === 'planning')
175
+ if (domain === 'planning' || domain === 'refactoring')
176
+ return 'complex';
177
+ if (score >= 3)
58
178
  return 'complex';
59
179
  return 'standard';
60
180
  }
61
- export function route(prompt) {
181
+ function computeConfidence(matched, promptLength, anaphora) {
182
+ // Anaphora needs context — never high confidence without LLM lookup
183
+ if (anaphora)
184
+ return 'low';
185
+ // Very short prompts are usually clear: hi, ok, thanks
186
+ if (promptLength < 20)
187
+ return 'high';
188
+ // Pattern matched → we know what this is
189
+ if (matched)
190
+ return 'high';
191
+ // Long prompt, no pattern, no anaphora → we're guessing
192
+ return 'low';
193
+ }
194
+ export async function route(prompt, recentMessages = []) {
195
+ const start = Date.now();
196
+ const explicit = parseExplicitRoute(prompt);
197
+ if (explicit)
198
+ return { ...explicit, latency_ms: Date.now() - start };
62
199
  const trimmed = prompt.trim();
63
- const domain = detectDomain(trimmed);
200
+ const { domain: regexDomain, matched } = detectDomain(trimmed);
64
201
  const complexityScore = scoreComplexity(trimmed);
65
- const tier = selectTier(domain, complexityScore);
66
- return {
202
+ const tier = selectTier(regexDomain, complexityScore);
203
+ const anaphora = hasAnaphora(trimmed);
204
+ const confidence = computeConfidence(matched, trimmed.length, anaphora);
205
+ const regexDecision = {
67
206
  tier,
68
- domain,
207
+ domain: regexDomain,
69
208
  complexityScore,
70
- recommendedModel: MODEL_BY_TIER[tier],
71
- reason: `domain=${domain} score=${complexityScore} tier=${tier}`
209
+ recommendedModel: getModel(tier),
210
+ reason: `domain=${regexDomain} score=${complexityScore} tier=${tier} matched=${matched} anaphora=${anaphora}`,
211
+ source: 'regex',
212
+ confidence,
213
+ regex_said: regexDomain,
214
+ latency_ms: Date.now() - start,
72
215
  };
73
- }
74
- export function detectComplexity(message) {
75
- const { domain, tier } = route(message);
76
- return (domain === 'coding' || tier === 'complex') ? 'complex' : 'simple';
216
+ // Only escalate to LLM on low confidence
217
+ const config = loadConfig();
218
+ if (confidence === 'low' && config.routing.useLlmClassifier) {
219
+ const result = await classifyWithLlm(trimmed, recentMessages);
220
+ if (result) {
221
+ const llmTier = selectTier(result.domain, complexityScore);
222
+ return {
223
+ tier: llmTier,
224
+ domain: result.domain,
225
+ complexityScore,
226
+ recommendedModel: getModel(llmTier),
227
+ reason: result.reason,
228
+ source: 'llm',
229
+ confidence: result.confidence,
230
+ regex_said: regexDomain,
231
+ llm_said: result.domain,
232
+ escalation_reason: anaphora ? 'anaphora detected' : 'low regex confidence',
233
+ latency_ms: Date.now() - start,
234
+ classifier_tokens: result.tokens,
235
+ };
236
+ }
237
+ // LLM failed — fall back to regex result
238
+ return {
239
+ ...regexDecision,
240
+ source: 'regex-fallback',
241
+ escalation_reason: 'llm classifier failed',
242
+ latency_ms: Date.now() - start,
243
+ };
244
+ }
245
+ return regexDecision;
77
246
  }
package/dist/logger.js ADDED
@@ -0,0 +1,25 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ const LOG_PATH = path.join(os.homedir(), '.glitool', 'debug.log');
5
+ try {
6
+ fs.mkdirSync(path.dirname(LOG_PATH), { recursive: true });
7
+ }
8
+ catch { }
9
+ export function log(event, data) {
10
+ const line = JSON.stringify({
11
+ t: new Date().toISOString().slice(11, 23),
12
+ event,
13
+ ...data,
14
+ });
15
+ try {
16
+ fs.appendFileSync(LOG_PATH, line + '\n');
17
+ }
18
+ catch { }
19
+ }
20
+ export function clearLog() {
21
+ try {
22
+ fs.writeFileSync(LOG_PATH, '');
23
+ }
24
+ catch { }
25
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,90 @@
1
+ import { tool } from "@langchain/core/tools";
2
+ import { spawn } from "child_process";
3
+ import { z } from "zod";
4
+ import { requestConfirm } from "../confirmHandler.js";
5
+ import { scoreShellRisk } from "../trust/riskScorer.js";
6
+ import { registerProcess } from "./processRegistry.js";
7
+ const MAX_OUTPUT = 10_000;
8
+ const DEFAULT_TIMEOUT_MS = 30_000;
9
+ export const bashTool = tool(async ({ command, timeout, runInBackground }) => {
10
+ // Risk check BEFORE spawn — block dangerous, confirm sensitive
11
+ const risk = scoreShellRisk(command);
12
+ if (risk === 'block') {
13
+ return `BLOCKED: This shell command is too dangerous to run: \`${command}\`. Try a safer alternative or ask the user to run it manually.`;
14
+ }
15
+ if (risk === 'confirm') {
16
+ const ok = await requestConfirm({
17
+ type: 'write',
18
+ filePath: `[shell] ${command}`,
19
+ content: command,
20
+ risk: 'medium',
21
+ });
22
+ if (!ok) {
23
+ return 'USER_CANCELLED: The user rejected this shell command. Do NOT retry.';
24
+ }
25
+ }
26
+ const proc = spawn(command, {
27
+ shell: true,
28
+ cwd: process.cwd(),
29
+ ...(runInBackground ? {} : { timeout: timeout ?? DEFAULT_TIMEOUT_MS }),
30
+ });
31
+ if (runInBackground) {
32
+ const handle = registerProcess(proc, command);
33
+ return `Background process started. Handle: ${handle}. Use the read_background_output with this handle to see output.`;
34
+ }
35
+ // Run the command
36
+ return new Promise((resolve) => {
37
+ let stdout = '';
38
+ let stderr = '';
39
+ let truncated = false;
40
+ // const proc = spawn(command, {
41
+ // shell: true,
42
+ // cwd: process.cwd(),
43
+ // timeout: timeout ?? DEFAULT_TIMEOUT_MS,
44
+ // });
45
+ proc.stdout?.on('data', (data) => {
46
+ if (stdout.length >= MAX_OUTPUT) {
47
+ truncated = true;
48
+ return;
49
+ }
50
+ stdout += data.toString();
51
+ if (stdout.length > MAX_OUTPUT) {
52
+ stdout = stdout.slice(0, MAX_OUTPUT);
53
+ truncated = true;
54
+ }
55
+ });
56
+ proc.stderr?.on('data', (data) => {
57
+ if (stderr.length >= MAX_OUTPUT) {
58
+ truncated = true;
59
+ return;
60
+ }
61
+ stderr += data.toString();
62
+ if (stderr.length > MAX_OUTPUT) {
63
+ stderr = stderr.slice(0, MAX_OUTPUT);
64
+ truncated = true;
65
+ }
66
+ });
67
+ proc.on('close', (code) => {
68
+ const parts = [];
69
+ if (stdout)
70
+ parts.push(`stdout:\n${stdout}`);
71
+ if (stderr)
72
+ parts.push(`stderr:\n${stderr}`);
73
+ parts.push(`exit code: ${code}`);
74
+ if (truncated)
75
+ parts.push('(output truncated at 10KB)');
76
+ resolve(parts.join('\n\n'));
77
+ });
78
+ proc.on('error', (err) => {
79
+ resolve(`Error running command: ${err.message}`);
80
+ });
81
+ });
82
+ }, {
83
+ name: 'bash',
84
+ description: 'Run a shell command. Use runInBackground:true for dev servers and long-running tests — returns a handle immediately. Use read_background_output to read accumulating output. Dangerous commands (rm -rf, sudo, fork bombs) are blocked. Sensitive commands (git push, npm install/publish) require user confirmation.',
85
+ schema: z.object({
86
+ command: z.string().describe('Shell command to run'),
87
+ timeout: z.number().optional().describe('Timeout in ms for foreground commands (default 30000)'),
88
+ runInBackground: z.boolean().optional().describe('If true, run without waiting and return a handle'),
89
+ }),
90
+ });
@@ -1,7 +1,8 @@
1
- import { tool } from "@langchain/core/tools";
1
+ import { tool } from '@langchain/core/tools';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { z } from 'zod';
5
+ import { requestConfirm } from '../confirmHandler.js';
5
6
  export const editFileTool = tool(async ({ filePath, oldString, newString }) => {
6
7
  const fullPath = path.resolve(process.cwd(), filePath);
7
8
  if (!fullPath.startsWith(process.cwd())) {
@@ -14,8 +15,18 @@ export const editFileTool = tool(async ({ filePath, oldString, newString }) => {
14
15
  if (!content.includes(oldString)) {
15
16
  throw new Error(`String not found in file. Make sure oldString matches exactly, including whitespace and case.`);
16
17
  }
17
- const updated = content.replace(oldString, newString);
18
- fs.writeFileSync(fullPath, updated, 'utf-8');
18
+ const ok = await requestConfirm({
19
+ type: 'edit',
20
+ filePath,
21
+ oldString,
22
+ newString,
23
+ risk: 'low',
24
+ });
25
+ if (!ok) {
26
+ return 'USER_CANCELLED: The user explicitly rejected this file edit. Do NOT retry. Inform the user the edit was cancelled.';
27
+ }
28
+ const update = content.split(oldString).join(newString);
29
+ fs.writeFileSync(fullPath, update, 'utf-8');
19
30
  return `Successfully edited ${filePath}`;
20
31
  }, {
21
32
  name: 'editFile',
@@ -1,7 +1,9 @@
1
1
  export { readProjectTool } from './readProject.js';
2
2
  export { writeFileTool } from './writeFileTool.js';
3
- export { analyzeProjectTool } from './analyzeProject.js';
4
3
  export { listFilesTool } from './listFilesTool.js';
5
4
  export { readFileTool } from './readFileTool.js';
6
5
  export { searchCodeTool } from './searchCodeTool.js';
7
6
  export { editFileTool } from './editFileTool.js';
7
+ export { bashTool } from './bashTool.js';
8
+ export { readBackgroundOutputTool } from './readBackgroundOutput.js';
9
+ export { webFetchTool } from './webFetchTool.js';
@@ -2,26 +2,24 @@ import { tool } from '@langchain/core/tools';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { z } from 'zod';
5
- const SKIP_DIRS = ['node_modules', '.git', 'dist', 'build', '.next'];
6
- function buildTree(dir, prefix = '') {
7
- const entries = fs.readdirSync(dir, { withFileTypes: true });
8
- const lines = [];
9
- for (const entry of entries) {
10
- if (SKIP_DIRS.includes(entry.name))
11
- continue;
12
- if (entry.isSymbolicLink())
13
- continue;
14
- lines.push(`${prefix}${entry.isDirectory() ? '📁' : '📄'} ${entry.name}`);
15
- if (entry.isDirectory()) {
16
- const sub = buildTree(path.join(dir, entry.name), prefix + ' ');
17
- if (sub)
18
- lines.push(sub);
19
- }
20
- }
21
- return lines.join('\n');
22
- }
23
- export const listFilesTool = tool(async () => buildTree(process.cwd()), {
5
+ import fg from 'fast-glob';
6
+ const DEFAULT_IGNORE = ['node_modules/**', 'dist/**', 'build/**', '.git/**', '.next/**'];
7
+ export const listFilesTool = tool(async ({ pattern, ignore }) => {
8
+ const files = await fg(pattern ?? '**/*', {
9
+ cwd: process.cwd(),
10
+ ignore: [...DEFAULT_IGNORE, ...(ignore ?? [])],
11
+ dot: false,
12
+ onlyFiles: false,
13
+ markDirectories: true
14
+ });
15
+ if (files.length === 0)
16
+ return 'No files matched.';
17
+ return files.join('\n');
18
+ }, {
24
19
  name: 'listFiles',
25
- description: 'Lists files and directories in the current working directory. It returns a tree-like structure with folders and files.',
26
- schema: z.object({})
20
+ description: 'List files in the current project. Accepts a glob pattern (e.g. "src/**/*.ts") to filter results. Returns matching file paths, one per line. Directories end with /.',
21
+ schema: z.object({
22
+ pattern: z.string().optional().describe('Glob pattern to filter files (default: **/* — everything). Examples: "src/**/*.ts", "**/*.test.ts", "*.json"'),
23
+ ignore: z.array(z.string()).optional().describe('Additional glob patterns to ignore on top of the defaults (node_modules, dist, .git)')
24
+ })
27
25
  });
@@ -0,0 +1,36 @@
1
+ const registry = new Map();
2
+ let counter = 0;
3
+ export function registerProcess(proc, command) {
4
+ const handle = `bg_${++counter}`;
5
+ const entry = {
6
+ proc, command, stdout: '', stderr: '', exitCode: null, startedAt: new Date()
7
+ };
8
+ proc.stdout?.on(`data`, (d) => { entry.stdout += d.toString(); });
9
+ proc.stderr?.on(`data`, (d) => { entry.stderr += d.toString(); });
10
+ proc.on(`close`, (code) => { entry.exitCode = code; });
11
+ registry.set(handle, entry);
12
+ return handle;
13
+ }
14
+ export function getProcess(handle) {
15
+ return registry.get(handle);
16
+ }
17
+ export function killProcess(handle) {
18
+ const entry = registry.get(handle);
19
+ if (!entry)
20
+ return false;
21
+ try {
22
+ entry.proc.kill();
23
+ }
24
+ catch { }
25
+ registry.delete(handle);
26
+ return true;
27
+ }
28
+ export function cleanupAll() {
29
+ for (const [, entry] of registry) {
30
+ try {
31
+ entry.proc.kill();
32
+ }
33
+ catch { }
34
+ }
35
+ registry.clear();
36
+ }
@@ -0,0 +1,29 @@
1
+ import { tool } from "@langchain/core/tools";
2
+ import { z } from "zod";
3
+ import { getProcess, killProcess } from "./processRegistry.js";
4
+ export const readBackgroundOutputTool = tool(async ({ handle, kill }) => {
5
+ const entry = getProcess(handle);
6
+ if (!entry)
7
+ return `No background process with handle: ${handle}`;
8
+ const parts = [
9
+ `Command: ${entry.command}`,
10
+ `Started: ${entry.startedAt.toISOString()}`,
11
+ `Status: ${entry.exitCode !== null ? `exited (exit code ${entry.exitCode})` : `still runing`}`
12
+ ];
13
+ if (entry.stdout)
14
+ parts.push(`stdout:\n${entry.stdout}`);
15
+ if (entry.stderr)
16
+ parts.push(`stderr:\n${entry.stderr}`);
17
+ if (kill) {
18
+ killProcess(handle);
19
+ parts.push('process killed.');
20
+ }
21
+ return parts.join('\n\n');
22
+ }, {
23
+ name: 'read_background_output',
24
+ description: 'Read accumulated stdout/stderr from a background process started with bash runInBackground:true. Use the handle returned at start time. Optionally kill the process.',
25
+ schema: z.object({
26
+ handle: z.string().describe('Handle returned when the process was started, e.g. "bg_1"'),
27
+ kill: z.boolean().optional().describe('If true, kill the process after reading its output')
28
+ })
29
+ });
@@ -2,19 +2,74 @@ import { tool } from '@langchain/core/tools';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { z } from 'zod';
5
+ import fg from 'fast-glob';
6
+ const SEARCH_IGNORE = [
7
+ 'node_modules/**',
8
+ 'dist/**',
9
+ 'build/**',
10
+ '.git/**',
11
+ '.next/**',
12
+ 'coverage/**',
13
+ ];
14
+ const MAX_BYTES = 40_000;
15
+ async function resolveFilePath(filePath) {
16
+ const cwd = process.cwd();
17
+ // 1. Try the exact path the user gave us (against cwd).
18
+ const direct = path.resolve(cwd, filePath);
19
+ if (direct.startsWith(cwd) && fs.existsSync(direct) && fs.statSync(direct).isFile()) {
20
+ return { kind: 'ok', absPath: direct };
21
+ }
22
+ // 2. If the user passed a path with slashes, don't search — they meant a specific location.
23
+ if (filePath.includes('/') || filePath.includes('\\')) {
24
+ return { kind: 'not_found' };
25
+ }
26
+ // 3. Bare filename — search for it anywhere in the project.
27
+ const matches = await fg(`**/${filePath}`, {
28
+ cwd,
29
+ ignore: SEARCH_IGNORE,
30
+ dot: false,
31
+ onlyFiles: true,
32
+ suppressErrors: true,
33
+ });
34
+ if (matches.length === 0)
35
+ return { kind: 'not_found' };
36
+ if (matches.length === 1) {
37
+ return {
38
+ kind: 'ok',
39
+ absPath: path.resolve(cwd, matches[0]),
40
+ resolvedFrom: matches[0],
41
+ };
42
+ }
43
+ return { kind: 'ambiguous', matches };
44
+ }
5
45
  export const readFileTool = tool(async ({ filePath }) => {
6
- const fullPath = path.resolve(process.cwd(), filePath);
7
- if (!fullPath.startsWith(process.cwd())) {
8
- throw new Error('Access denied: outside of current working directory');
46
+ const resolved = await resolveFilePath(filePath);
47
+ if (resolved.kind === 'not_found') {
48
+ return `File not found: ${filePath}
49
+ Current working directory: ${process.cwd()}
50
+ Hint: try a bare filename (e.g. "agent.ts") to search the whole project, or use an absolute path.`;
51
+ }
52
+ if (resolved.kind === 'ambiguous') {
53
+ const list = resolved.matches.slice(0, 10).map(m => ` - ${m}`).join('\n');
54
+ const more = resolved.matches.length > 10
55
+ ? `\n ...and ${resolved.matches.length - 10} more`
56
+ : '';
57
+ return `Multiple files match "${filePath}":\n${list}${more}\n\nCall readFile again with one of these specific paths.`;
58
+ }
59
+ const stat = fs.statSync(resolved.absPath);
60
+ if (stat.size > MAX_BYTES) {
61
+ return `File too large (${stat.size.toLocaleString()} bytes). Use searchCode to find specific symbols, or read smaller files.`;
9
62
  }
10
- if (!fs.existsSync(fullPath)) {
11
- throw new Error(`File not found: ${filePath}`);
63
+ const content = fs.readFileSync(resolved.absPath, 'utf-8');
64
+ // If we resolved a bare filename, tell the agent which path we used so future calls can be precise.
65
+ if (resolved.resolvedFrom) {
66
+ return `[resolved "${filePath}" → ${resolved.resolvedFrom}]\n\n${content}`;
12
67
  }
13
- return fs.readFileSync(fullPath, 'utf-8');
68
+ return content;
14
69
  }, {
15
70
  name: 'readFile',
16
- description: 'Reads the contents of a specific file. Use this tool to read the contents of a file in the current working directory. Provide the relative path to the file you want to read.',
71
+ description: 'Read the contents of a file. Accepts either a relative path from the project root (e.g. "src/foo.ts") or a bare filename (e.g. "foo.ts") — if a bare name is given, it searches the whole project. If multiple files match, the tool returns the list and you should call again with the specific path.',
17
72
  schema: z.object({
18
- filePath: z.string().describe('Relative path to the file from the project root')
19
- })
73
+ filePath: z.string().describe('Relative path from project root (e.g. "CLI/src/agent.ts") OR a bare filename (e.g. "agent.ts") to search for project-wide.'),
74
+ }),
20
75
  });
@@ -1,10 +1,20 @@
1
1
  import { tool } from "@langchain/core/tools";
2
- import { execSync } from "child_process";
2
+ import { execFileSync } from "child_process";
3
3
  import { z } from "zod";
4
4
  export const searchCodeTool = tool(async ({ keyword }) => {
5
5
  try {
6
- const result = execSync(`grep -rn "${keyword}" --include="*.ts" --include="*.js" --include="*.json" --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=dist --exclude-dir=.git`, { cwd: process.cwd(), timeout: 10000, stdio: 'pipe' }).toString();
7
- return result || 'No Matches found.';
6
+ const result = execFileSync('grep', [
7
+ '-rn',
8
+ keyword,
9
+ '--include=*.ts',
10
+ '--include=*.js',
11
+ '--include=*.json',
12
+ '--exclude-dir=node_modules',
13
+ '--exclude-dir=dist',
14
+ '--exclude-dir=.git',
15
+ '.'
16
+ ], { cwd: process.cwd(), timeout: 10000, stdio: 'pipe' }).toString();
17
+ return result || 'No matches found.';
8
18
  }
9
19
  catch {
10
20
  return 'No matches found.';
@@ -13,6 +23,6 @@ export const searchCodeTool = tool(async ({ keyword }) => {
13
23
  name: 'searchCode',
14
24
  description: 'Search for a keyword or function name across all project files. Returns file paths and line numbers where the keyword is found. Use this tool to quickly locate where a specific function or variable is used in the codebase.',
15
25
  schema: z.object({
16
- keyword: z.string().describe('The Keyword, funtion name, or pattern to search for in the codebase')
26
+ keyword: z.string().describe('The keyword, function name, or pattern to search for in the codebase')
17
27
  })
18
28
  });