vigthoria-cli 1.6.15 → 1.6.17
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/dist/commands/chat.d.ts +4 -0
- package/dist/commands/chat.js +163 -19
- package/dist/utils/api.d.ts +1 -0
- package/dist/utils/api.js +81 -2
- package/dist/utils/tools.d.ts +22 -0
- package/dist/utils/tools.js +246 -24
- package/package.json +1 -1
package/dist/commands/chat.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export declare class ChatCommand {
|
|
|
32
32
|
private currentModel;
|
|
33
33
|
private modelExplicitlySelected;
|
|
34
34
|
private autoApprove;
|
|
35
|
+
private agentToolEvidence;
|
|
35
36
|
private operatorMode;
|
|
36
37
|
private workflowTarget;
|
|
37
38
|
private savePlanToVigFlow;
|
|
@@ -50,6 +51,9 @@ export declare class ChatCommand {
|
|
|
50
51
|
private buildTaskShapingInstructions;
|
|
51
52
|
private buildExecutionPrompt;
|
|
52
53
|
private getPromptRuntimeContext;
|
|
54
|
+
private v3IterationCount;
|
|
55
|
+
private v3ToolCallCount;
|
|
56
|
+
private v3LastActivity;
|
|
53
57
|
private describeV3AgentTool;
|
|
54
58
|
private updateV3AgentSpinner;
|
|
55
59
|
private updateOperatorSpinner;
|
package/dist/commands/chat.js
CHANGED
|
@@ -47,9 +47,9 @@ const api_js_1 = require("../utils/api.js");
|
|
|
47
47
|
const tools_js_1 = require("../utils/tools.js");
|
|
48
48
|
const session_js_1 = require("../utils/session.js");
|
|
49
49
|
const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
50
|
-
const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS || '
|
|
50
|
+
const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS || '3900000';
|
|
51
51
|
const parsed = Number.parseInt(rawValue, 10);
|
|
52
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed :
|
|
52
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 3900000;
|
|
53
53
|
})();
|
|
54
54
|
class ChatCommand {
|
|
55
55
|
config;
|
|
@@ -67,6 +67,8 @@ class ChatCommand {
|
|
|
67
67
|
currentModel = 'code';
|
|
68
68
|
modelExplicitlySelected = false;
|
|
69
69
|
autoApprove = false;
|
|
70
|
+
// Phase 5: Agent quality gate — track tool usage for evidence thresholds
|
|
71
|
+
agentToolEvidence = { discovery: 0, mutation: 0, searchFailed: 0 };
|
|
70
72
|
operatorMode = false;
|
|
71
73
|
workflowTarget = null;
|
|
72
74
|
savePlanToVigFlow = false;
|
|
@@ -195,6 +197,10 @@ class ChatCommand {
|
|
|
195
197
|
}
|
|
196
198
|
buildTaskShapingInstructions(prompt) {
|
|
197
199
|
const instructions = [];
|
|
200
|
+
// Platform-aware routing hints
|
|
201
|
+
if (os.platform() === 'win32') {
|
|
202
|
+
instructions.push('Platform: Windows. Use list_dir, glob, read_file, and the grep tool for searching.', 'The grep tool handles Windows automatically — do not use bash to call grep, findstr, or Select-String manually.', 'Do not use bash for Unix commands (cat, head, tail, awk, sed, wc).', 'Use read_file to inspect file contents instead of shell commands.', 'All file paths use forward slashes internally.');
|
|
203
|
+
}
|
|
198
204
|
if (this.isDiagnosticPrompt(prompt)) {
|
|
199
205
|
instructions.push('Diagnostic mode is active.', 'Treat this as a debugging task, not a generic code review or feature request.', 'Start with concrete evidence: logs, runtime errors, config, launch files, and exact symbol references.', 'If log files exist, inspect them before proposing fixes.', 'Do not claim a file, definition, asset, or symbol is missing until you verify that with tools.', 'If a prior diagnosis mentioned a missing symbol or YAML entry, re-check the actual files before repeating it.', 'Prefer grep plus read_file around the exact references involved in the failure.', 'Separate your reasoning into: Evidence, Confirmed Cause, and Remaining Hypotheses.', 'Do not suggest speculative fixes when the current evidence contradicts them.');
|
|
200
206
|
}
|
|
@@ -227,37 +233,87 @@ class ChatCommand {
|
|
|
227
233
|
devtoolsBridgeEndpoint: bridgeStatus.endpoint,
|
|
228
234
|
};
|
|
229
235
|
}
|
|
236
|
+
v3IterationCount = 0;
|
|
237
|
+
v3ToolCallCount = 0;
|
|
238
|
+
v3LastActivity = Date.now();
|
|
230
239
|
describeV3AgentTool(toolName) {
|
|
231
240
|
const normalized = String(toolName || '').toLowerCase();
|
|
232
241
|
if (/read|grep|search|list|find|glob/.test(normalized)) {
|
|
233
|
-
return '
|
|
242
|
+
return 'Reading project files';
|
|
234
243
|
}
|
|
235
244
|
if (/write|edit|patch|create|delete|replace|rename/.test(normalized)) {
|
|
236
|
-
return '
|
|
245
|
+
return 'Writing changes';
|
|
237
246
|
}
|
|
238
247
|
if (/test|lint|build|run|exec|terminal/.test(normalized)) {
|
|
239
|
-
return '
|
|
248
|
+
return 'Running verification';
|
|
249
|
+
}
|
|
250
|
+
if (/hyperloop/.test(normalized)) {
|
|
251
|
+
return 'Hyperloop processing';
|
|
240
252
|
}
|
|
241
|
-
return 'Working
|
|
253
|
+
return 'Working';
|
|
242
254
|
}
|
|
243
255
|
updateV3AgentSpinner(spinner, event) {
|
|
244
256
|
if (!event || typeof event !== 'object') {
|
|
245
257
|
return;
|
|
246
258
|
}
|
|
259
|
+
this.v3LastActivity = Date.now();
|
|
247
260
|
if (event.type === 'tool_call') {
|
|
248
|
-
|
|
261
|
+
this.v3ToolCallCount += 1;
|
|
262
|
+
const toolDesc = this.describeV3AgentTool(event.tool || event.name || event.tool_name);
|
|
263
|
+
const toolTarget = event.arguments?.path || event.arguments?.file_path || event.arguments?.pattern || '';
|
|
264
|
+
const shortTarget = toolTarget ? ` → ${String(toolTarget).split('/').slice(-2).join('/')}` : '';
|
|
265
|
+
spinner.text = chalk_1.default.cyan(`[${this.v3IterationCount}/${this.v3ToolCallCount}] `) + `${toolDesc}${shortTarget}`;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (event.type === 'tool_result') {
|
|
269
|
+
const success = event.success !== false;
|
|
270
|
+
const toolName = event.name || event.tool || '';
|
|
271
|
+
const indicator = success ? chalk_1.default.green('✓') : chalk_1.default.red('✗');
|
|
272
|
+
spinner.text = `${indicator} ${toolName} complete — next step...`;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (event.type === 'thinking') {
|
|
276
|
+
this.v3IterationCount += 1;
|
|
277
|
+
const iterText = event.content || '';
|
|
278
|
+
const iterMatch = iterText.match(/Iteration (\d+)/i);
|
|
279
|
+
const iterNum = iterMatch ? iterMatch[1] : String(this.v3IterationCount);
|
|
280
|
+
spinner.text = chalk_1.default.cyan(`[Iteration ${iterNum}] `) + 'Analyzing...';
|
|
249
281
|
return;
|
|
250
282
|
}
|
|
251
283
|
if (event.type === 'message') {
|
|
252
|
-
|
|
284
|
+
const preview = String(event.content || '').slice(0, 80).replace(/\n/g, ' ');
|
|
285
|
+
spinner.text = chalk_1.default.cyan('[Response] ') + (preview || 'Writing response...');
|
|
253
286
|
return;
|
|
254
287
|
}
|
|
255
288
|
if (event.type === 'complete') {
|
|
256
|
-
|
|
289
|
+
const elapsed = event.elapsed || '';
|
|
290
|
+
const iters = event.iterations || this.v3IterationCount;
|
|
291
|
+
const tools = event.tool_calls || this.v3ToolCallCount;
|
|
292
|
+
spinner.text = chalk_1.default.green('✓ ') + `Complete — ${iters} iterations, ${tools} tool calls${elapsed ? `, ${elapsed}` : ''}`;
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (event.type === 'plan') {
|
|
296
|
+
const planKind = event.plan?.task_kind || event.task_kind || '';
|
|
297
|
+
spinner.text = chalk_1.default.cyan('[Plan] ') + `Task: ${planKind || 'analyzing'}...`;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (event.type === 'error') {
|
|
301
|
+
if (event.checkpointed) {
|
|
302
|
+
spinner.text = chalk_1.default.yellow('[Checkpoint] ') + 'Budget reached — auto-continuing...';
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
spinner.text = chalk_1.default.red('[Error] ') + (event.message || 'Agent error');
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (event.type === 'context') {
|
|
310
|
+
spinner.text = chalk_1.default.cyan('[Context] ') + 'Workspace bound...';
|
|
257
311
|
return;
|
|
258
312
|
}
|
|
259
|
-
if (event.type === '
|
|
260
|
-
|
|
313
|
+
if (event.type === 'start') {
|
|
314
|
+
this.v3IterationCount = 0;
|
|
315
|
+
this.v3ToolCallCount = 0;
|
|
316
|
+
spinner.text = chalk_1.default.cyan('[Start] ') + 'Agent initialized...';
|
|
261
317
|
}
|
|
262
318
|
}
|
|
263
319
|
updateOperatorSpinner(spinner, event) {
|
|
@@ -607,10 +663,11 @@ class ChatCommand {
|
|
|
607
663
|
async runSimplePrompt(prompt) {
|
|
608
664
|
this.lastActionableUserInput = prompt;
|
|
609
665
|
this.messages.push({ role: 'user', content: this.buildExecutionPrompt(prompt) });
|
|
610
|
-
const spinner = (0, ora_1.default)({ text: 'Thinking...', spinner: 'clock' }).start();
|
|
666
|
+
const spinner = this.jsonOutput ? null : (0, ora_1.default)({ text: 'Thinking...', spinner: 'clock' }).start();
|
|
611
667
|
try {
|
|
612
668
|
const response = await this.api.chat(this.getMessagesForModel(), this.currentModel);
|
|
613
|
-
spinner
|
|
669
|
+
if (spinner)
|
|
670
|
+
spinner.stop();
|
|
614
671
|
const finalText = response.message.trim();
|
|
615
672
|
if (finalText) {
|
|
616
673
|
console.log(finalText);
|
|
@@ -619,7 +676,8 @@ class ChatCommand {
|
|
|
619
676
|
this.saveSession();
|
|
620
677
|
}
|
|
621
678
|
catch (error) {
|
|
622
|
-
spinner
|
|
679
|
+
if (spinner)
|
|
680
|
+
spinner.fail('Failed to get response');
|
|
623
681
|
this.logger.error(error.message);
|
|
624
682
|
}
|
|
625
683
|
}
|
|
@@ -657,21 +715,37 @@ class ChatCommand {
|
|
|
657
715
|
}
|
|
658
716
|
this.lastActionableUserInput = prompt;
|
|
659
717
|
this.directToolContinuationCount = 0;
|
|
718
|
+
this.agentToolEvidence = { discovery: 0, mutation: 0, searchFailed: 0 };
|
|
660
719
|
this.tools.clearSessionApprovals();
|
|
661
720
|
this.ensureAgentSystemPrompt();
|
|
662
721
|
this.messages.push({ role: 'user', content: this.buildScopedUserPrompt(prompt) });
|
|
663
722
|
this.saveSession();
|
|
664
723
|
const maxTurns = 10;
|
|
665
724
|
for (let turn = 0; turn < maxTurns; turn += 1) {
|
|
666
|
-
const spinner = (0, ora_1.default)({ text: turn === 0 ? 'Planning...' : 'Continuing...', spinner: 'clock' }).start();
|
|
725
|
+
const spinner = this.jsonOutput ? null : (0, ora_1.default)({ text: turn === 0 ? 'Planning...' : 'Continuing...', spinner: 'clock' }).start();
|
|
667
726
|
try {
|
|
668
727
|
const response = await this.api.chat(this.getMessagesForModel(), this.currentModel);
|
|
669
|
-
spinner
|
|
728
|
+
if (spinner)
|
|
729
|
+
spinner.stop();
|
|
670
730
|
const assistantMessage = response.message || '';
|
|
671
731
|
this.messages.push({ role: 'assistant', content: assistantMessage });
|
|
672
732
|
const toolCalls = this.extractToolCalls(assistantMessage);
|
|
673
733
|
const visibleText = this.stripToolPayloads(assistantMessage).trim();
|
|
674
734
|
if (toolCalls.length === 0) {
|
|
735
|
+
// Phase 5: Quality gate — if the agent tries to conclude on the first
|
|
736
|
+
// turn without any discovery, push it to gather evidence first.
|
|
737
|
+
if (turn === 0 && this.agentToolEvidence.discovery === 0 && this.isDiagnosticPrompt(prompt)) {
|
|
738
|
+
this.messages.push({
|
|
739
|
+
role: 'system',
|
|
740
|
+
content: [
|
|
741
|
+
'Quality gate: you concluded without using any discovery tools (list_dir, glob, read_file, grep).',
|
|
742
|
+
'Before answering a diagnostic or audit question, you MUST inspect the project with tools.',
|
|
743
|
+
'Use list_dir and read_file to gather concrete evidence, then provide your answer.',
|
|
744
|
+
].join('\n'),
|
|
745
|
+
});
|
|
746
|
+
this.directToolContinuationCount += 1;
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
675
749
|
const finalContent = this.resolveDirectModeCompletion(prompt, visibleText);
|
|
676
750
|
if (this.jsonOutput) {
|
|
677
751
|
console.log(JSON.stringify({
|
|
@@ -700,7 +774,8 @@ class ChatCommand {
|
|
|
700
774
|
this.saveSession();
|
|
701
775
|
}
|
|
702
776
|
catch (error) {
|
|
703
|
-
spinner
|
|
777
|
+
if (spinner)
|
|
778
|
+
spinner.fail('Agent request failed');
|
|
704
779
|
if (this.jsonOutput) {
|
|
705
780
|
process.exitCode = 1;
|
|
706
781
|
console.log(JSON.stringify({
|
|
@@ -856,6 +931,10 @@ class ChatCommand {
|
|
|
856
931
|
const runtimeContext = await this.getPromptRuntimeContext(prompt);
|
|
857
932
|
const routingPolicy = this.resolveAgentExecutionPolicy(prompt);
|
|
858
933
|
const rescueEligible = this.isSaaSRescuePrompt(prompt);
|
|
934
|
+
// Reset streaming counters for new workflow
|
|
935
|
+
this.v3IterationCount = 0;
|
|
936
|
+
this.v3ToolCallCount = 0;
|
|
937
|
+
this.v3LastActivity = Date.now();
|
|
859
938
|
const spinner = this.jsonOutput ? null : (0, ora_1.default)({
|
|
860
939
|
text: routingPolicy.cloudSelected ? 'Routing heavy task to Vigthoria Cloud...' : 'Routing to V3 Agent...',
|
|
861
940
|
spinner: 'clock',
|
|
@@ -917,6 +996,18 @@ class ChatCommand {
|
|
|
917
996
|
}
|
|
918
997
|
this.logger.error(errorMessage);
|
|
919
998
|
this.messages.push({ role: 'assistant', content: errorMessage });
|
|
999
|
+
if (this.jsonOutput) {
|
|
1000
|
+
process.exitCode = 1;
|
|
1001
|
+
console.log(JSON.stringify({
|
|
1002
|
+
success: false,
|
|
1003
|
+
mode: 'agent',
|
|
1004
|
+
model: routingPolicy.selectedModel,
|
|
1005
|
+
partial: false,
|
|
1006
|
+
content: '',
|
|
1007
|
+
error: errorMessage,
|
|
1008
|
+
metadata: { executionPath: 'v3-agent', previewGate },
|
|
1009
|
+
}, null, 2));
|
|
1010
|
+
}
|
|
920
1011
|
return true;
|
|
921
1012
|
}
|
|
922
1013
|
if (!this.jsonOutput && previewGate?.required && previewGate?.passed !== true && workspaceHasOutput) {
|
|
@@ -974,6 +1065,18 @@ class ChatCommand {
|
|
|
974
1065
|
const errorMessage = `Agent mode requires the V3 workflow and will not fall back to the legacy CLI loop. ${error.message}`;
|
|
975
1066
|
this.logger.error(errorMessage);
|
|
976
1067
|
this.messages.push({ role: 'assistant', content: errorMessage });
|
|
1068
|
+
if (this.jsonOutput) {
|
|
1069
|
+
process.exitCode = 1;
|
|
1070
|
+
console.log(JSON.stringify({
|
|
1071
|
+
success: false,
|
|
1072
|
+
mode: 'agent',
|
|
1073
|
+
model: routingPolicy.selectedModel,
|
|
1074
|
+
partial: false,
|
|
1075
|
+
content: '',
|
|
1076
|
+
error: errorMessage,
|
|
1077
|
+
metadata: { executionPath: 'v3-agent' },
|
|
1078
|
+
}, null, 2));
|
|
1079
|
+
}
|
|
977
1080
|
return true;
|
|
978
1081
|
}
|
|
979
1082
|
}
|
|
@@ -1383,10 +1486,20 @@ class ChatCommand {
|
|
|
1383
1486
|
}
|
|
1384
1487
|
buildContinuationPrompt() {
|
|
1385
1488
|
const diagnosticMode = this.isDiagnosticPrompt(this.lastActionableUserInput);
|
|
1489
|
+
const { discovery, mutation, searchFailed } = this.agentToolEvidence;
|
|
1490
|
+
const evidenceLines = [];
|
|
1491
|
+
if (discovery < 2) {
|
|
1492
|
+
evidenceLines.push(`Quality gate: only ${discovery} discovery tool(s) used so far (list_dir, glob, read_file, grep). Use at least 2 before concluding.`);
|
|
1493
|
+
}
|
|
1494
|
+
if (searchFailed > 0) {
|
|
1495
|
+
evidenceLines.push(`Warning: ${searchFailed} search tool call(s) failed. Do not treat failed searches as evidence that something is missing.`);
|
|
1496
|
+
}
|
|
1386
1497
|
return [
|
|
1387
1498
|
`Tool results received for direct mode step ${this.directToolContinuationCount + 1}.`,
|
|
1388
1499
|
`Original user request: ${this.lastActionableUserInput}`,
|
|
1389
1500
|
`Project root boundary: ${this.currentProjectPath}`,
|
|
1501
|
+
`Evidence collected: ${discovery} discovery, ${mutation} mutation, ${searchFailed} search failures.`,
|
|
1502
|
+
...evidenceLines,
|
|
1390
1503
|
'Do not declare success until the exact user question has been answered with tool-backed evidence.',
|
|
1391
1504
|
'If a user is asking which file is correct or most recent, keep inspecting until you can justify the answer from actual results.',
|
|
1392
1505
|
diagnosticMode ? 'Because this is a debugging task, prefer logs, runtime evidence, and exact symbol references over generic fixes.' : 'Keep working from concrete tool results.',
|
|
@@ -1550,16 +1663,47 @@ class ChatCommand {
|
|
|
1550
1663
|
if (!this.jsonOutput) {
|
|
1551
1664
|
console.log(chalk_1.default.cyan(`⚙ Executing: ${call.tool}`));
|
|
1552
1665
|
}
|
|
1553
|
-
|
|
1666
|
+
let result = await this.tools.execute(call);
|
|
1667
|
+
// Phase 2: If a search tool failed (search_failed), retry with alternate approach
|
|
1668
|
+
const searchStatus = result.metadata?.searchStatus;
|
|
1669
|
+
if (call.tool === 'grep' && searchStatus === 'search_failed') {
|
|
1670
|
+
if (!this.jsonOutput) {
|
|
1671
|
+
console.log(chalk_1.default.yellow(`⚠ Search backend failed, retrying with alternate method...`));
|
|
1672
|
+
}
|
|
1673
|
+
// Force Node-native fallback by re-executing with a note
|
|
1674
|
+
const fallbackResult = await this.tools.execute({
|
|
1675
|
+
tool: 'grep',
|
|
1676
|
+
args: { ...call.args, _fallback: 'node-native' },
|
|
1677
|
+
});
|
|
1678
|
+
if (fallbackResult.success || fallbackResult.metadata?.searchStatus !== 'search_failed') {
|
|
1679
|
+
result = fallbackResult;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1554
1682
|
const summary = this.formatToolResult(call, result);
|
|
1555
1683
|
if (!this.jsonOutput) {
|
|
1556
1684
|
console.log(result.success ? chalk_1.default.gray(summary) : chalk_1.default.red(summary));
|
|
1557
1685
|
}
|
|
1558
1686
|
this.messages.push({ role: 'system', content: summary });
|
|
1687
|
+
// Phase 5: Track tool evidence for quality gates
|
|
1688
|
+
const finalStatus = result.metadata?.searchStatus;
|
|
1689
|
+
if (finalStatus === 'search_failed') {
|
|
1690
|
+
this.agentToolEvidence.searchFailed += 1;
|
|
1691
|
+
}
|
|
1692
|
+
else if (/^(read_file|list_dir|glob|grep|git|repo|fetch_url)$/.test(call.tool)) {
|
|
1693
|
+
this.agentToolEvidence.discovery += 1;
|
|
1694
|
+
}
|
|
1695
|
+
else if (/^(write_file|edit_file|bash|ssh_exec)$/.test(call.tool)) {
|
|
1696
|
+
this.agentToolEvidence.mutation += 1;
|
|
1697
|
+
}
|
|
1559
1698
|
}
|
|
1560
1699
|
}
|
|
1561
1700
|
formatToolResult(call, result) {
|
|
1562
|
-
const parts = [`Tool ${call.tool} ${result.success ? 'succeeded' : '
|
|
1701
|
+
const parts = [`Tool ${call.tool} ${result.success ? 'succeeded' : 'FAILED'}.`];
|
|
1702
|
+
// Include search status classification for the agent to reason about
|
|
1703
|
+
const searchStatus = result.metadata?.searchStatus;
|
|
1704
|
+
if (searchStatus) {
|
|
1705
|
+
parts.push(`Search status: ${searchStatus}`);
|
|
1706
|
+
}
|
|
1563
1707
|
if (result.output) {
|
|
1564
1708
|
parts.push(`Output:\n${this.truncateText(result.output)}`);
|
|
1565
1709
|
}
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -188,6 +188,7 @@ export declare class APIClient {
|
|
|
188
188
|
private getAccessToken;
|
|
189
189
|
getV3AgentBaseUrls(preferLocal?: boolean): string[];
|
|
190
190
|
getV3AgentRunUrl(baseUrl: string): string;
|
|
191
|
+
getV3AgentContinueUrl(baseUrl: string): string;
|
|
191
192
|
getOperatorBaseUrls(): string[];
|
|
192
193
|
getOperatorStreamUrl(baseUrl: string): string;
|
|
193
194
|
getMcpBaseUrls(): string[];
|
package/dist/utils/api.js
CHANGED
|
@@ -26,9 +26,9 @@ const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
|
|
|
26
26
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 90000;
|
|
27
27
|
})();
|
|
28
28
|
const DEFAULT_OPERATOR_TIMEOUT_MS = (() => {
|
|
29
|
-
const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS || '
|
|
29
|
+
const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS || '3900000';
|
|
30
30
|
const parsed = Number.parseInt(rawValue, 10);
|
|
31
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed :
|
|
31
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 3900000;
|
|
32
32
|
})();
|
|
33
33
|
class APIClient {
|
|
34
34
|
client;
|
|
@@ -276,6 +276,12 @@ class APIClient {
|
|
|
276
276
|
}
|
|
277
277
|
return `${baseUrl}/api/v3-agent/run`;
|
|
278
278
|
}
|
|
279
|
+
getV3AgentContinueUrl(baseUrl) {
|
|
280
|
+
if (/127\.0\.0\.1:8030|localhost:8030/.test(baseUrl)) {
|
|
281
|
+
return `${baseUrl}/api/agent/continue`;
|
|
282
|
+
}
|
|
283
|
+
return `${baseUrl}/api/v3-agent/continue`;
|
|
284
|
+
}
|
|
279
285
|
getOperatorBaseUrls() {
|
|
280
286
|
const configuredModelsApiUrl = String(this.config.get('modelsApiUrl') || 'https://api.vigthoria.io').replace(/\/$/, '');
|
|
281
287
|
const urls = [
|
|
@@ -2234,6 +2240,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2234
2240
|
}
|
|
2235
2241
|
}
|
|
2236
2242
|
if (event.type === 'error') {
|
|
2243
|
+
if (event.checkpointed && event.task_id) {
|
|
2244
|
+
// Agent checkpointed — return data so caller can auto-continue
|
|
2245
|
+
return {
|
|
2246
|
+
task_id: event.task_id,
|
|
2247
|
+
context_id: contextId,
|
|
2248
|
+
result: final || event,
|
|
2249
|
+
events,
|
|
2250
|
+
files: streamedFiles,
|
|
2251
|
+
partial: true,
|
|
2252
|
+
checkpointed: true,
|
|
2253
|
+
checkpointed_task_id: event.task_id,
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2237
2256
|
if (this.hasAgentWorkspaceOutput(context)) {
|
|
2238
2257
|
return {
|
|
2239
2258
|
task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
|
|
@@ -2312,6 +2331,66 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2312
2331
|
throw new Error(`V3 agent ${response.status}: ${errorText.slice(0, 200)}`);
|
|
2313
2332
|
}
|
|
2314
2333
|
const data = await this.collectV3AgentStream(response, requestExecutionContext);
|
|
2334
|
+
// Auto-continuation: if the agent checkpointed (budget exceeded), continue automatically
|
|
2335
|
+
if (data.checkpointed && data.checkpointed_task_id) {
|
|
2336
|
+
const maxContinuations = 10;
|
|
2337
|
+
let continuationData = data;
|
|
2338
|
+
let continuations = 0;
|
|
2339
|
+
while (continuationData.checkpointed && continuationData.checkpointed_task_id && continuations < maxContinuations) {
|
|
2340
|
+
continuations++;
|
|
2341
|
+
if (typeof requestExecutionContext.onStreamEvent === 'function') {
|
|
2342
|
+
try {
|
|
2343
|
+
requestExecutionContext.onStreamEvent({
|
|
2344
|
+
type: 'message',
|
|
2345
|
+
content: `Auto-continuing task (phase ${continuations + 1})...`,
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
catch { /* ignore */ }
|
|
2349
|
+
}
|
|
2350
|
+
const continueBody = {
|
|
2351
|
+
task_id: continuationData.checkpointed_task_id,
|
|
2352
|
+
context: requestBody.context,
|
|
2353
|
+
context_id: requestBody.context_id,
|
|
2354
|
+
mcp_context_id: requestBody.mcp_context_id,
|
|
2355
|
+
stream: true,
|
|
2356
|
+
};
|
|
2357
|
+
const continueController = new AbortController();
|
|
2358
|
+
const continueTimeoutId = setTimeout(() => continueController.abort(), timeoutMs);
|
|
2359
|
+
try {
|
|
2360
|
+
const continueHeaders = await this.getV3AgentHeaders();
|
|
2361
|
+
const continueResponse = await fetch(this.getV3AgentContinueUrl(baseUrl), {
|
|
2362
|
+
method: 'POST',
|
|
2363
|
+
headers: { ...continueHeaders, 'Content-Type': 'application/json' },
|
|
2364
|
+
body: JSON.stringify(continueBody),
|
|
2365
|
+
signal: continueController.signal,
|
|
2366
|
+
});
|
|
2367
|
+
if (!continueResponse.ok) {
|
|
2368
|
+
break; // Fall through to normal completion with partial data
|
|
2369
|
+
}
|
|
2370
|
+
continuationData = await this.collectV3AgentStream(continueResponse, requestExecutionContext);
|
|
2371
|
+
}
|
|
2372
|
+
catch {
|
|
2373
|
+
break; // Fall through to normal completion with partial data
|
|
2374
|
+
}
|
|
2375
|
+
finally {
|
|
2376
|
+
clearTimeout(continueTimeoutId);
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
// Use the final continuation data for workspace recovery
|
|
2380
|
+
this.recoverAgentWorkspaceFiles(executionContext, continuationData.files || {}, expectedFiles);
|
|
2381
|
+
await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
|
|
2382
|
+
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
2383
|
+
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
2384
|
+
const finalContextId = continuationData.context_id || data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
|
|
2385
|
+
return {
|
|
2386
|
+
content: this.formatV3AgentResponse(continuationData) || this.formatV3AgentResponse(data),
|
|
2387
|
+
taskId: continuationData.task_id || data.task_id || null,
|
|
2388
|
+
contextId: finalContextId,
|
|
2389
|
+
backendUrl: baseUrl,
|
|
2390
|
+
partial: continuationData.checkpointed === true,
|
|
2391
|
+
metadata: { source: 'v3-agent', mode: 'agent', contextId: finalContextId, continuations, previewGate },
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2315
2394
|
const contextId = data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
|
|
2316
2395
|
const mcpContextId = response.headers.get('x-mcp-context-id') || requestExecutionContext.mcpContextId || null;
|
|
2317
2396
|
this.recoverAgentWorkspaceFiles(executionContext, data.files || {}, expectedFiles);
|
package/dist/utils/tools.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { Logger } from './logger.js';
|
|
17
17
|
export type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
|
|
18
|
+
export type SearchStatus = 'search_matches_found' | 'search_no_matches' | 'search_failed';
|
|
18
19
|
export interface ToolResult {
|
|
19
20
|
success: boolean;
|
|
20
21
|
output?: string;
|
|
@@ -23,6 +24,7 @@ export interface ToolResult {
|
|
|
23
24
|
canRetry?: boolean;
|
|
24
25
|
undoable?: boolean;
|
|
25
26
|
metadata?: {
|
|
27
|
+
searchStatus?: SearchStatus;
|
|
26
28
|
[key: string]: any;
|
|
27
29
|
};
|
|
28
30
|
}
|
|
@@ -145,6 +147,26 @@ export declare class AgenticTools {
|
|
|
145
147
|
*/
|
|
146
148
|
private bash;
|
|
147
149
|
private grep;
|
|
150
|
+
/**
|
|
151
|
+
* Windows grep: rg > Select-String > Node-native
|
|
152
|
+
*/
|
|
153
|
+
private grepWindows;
|
|
154
|
+
/**
|
|
155
|
+
* Unix grep: rg > system grep (BSD/GNU)
|
|
156
|
+
*/
|
|
157
|
+
private grepUnix;
|
|
158
|
+
/**
|
|
159
|
+
* Search using ripgrep (rg) — works on all platforms
|
|
160
|
+
*/
|
|
161
|
+
private grepWithRg;
|
|
162
|
+
/**
|
|
163
|
+
* Search using PowerShell Select-String (Windows)
|
|
164
|
+
*/
|
|
165
|
+
private grepWithSelectString;
|
|
166
|
+
/**
|
|
167
|
+
* Pure Node.js recursive file search — reliable on all platforms, no external deps
|
|
168
|
+
*/
|
|
169
|
+
private grepNodeNative;
|
|
148
170
|
private listDir;
|
|
149
171
|
private glob;
|
|
150
172
|
private git;
|
package/dist/utils/tools.js
CHANGED
|
@@ -995,48 +995,270 @@ class AgenticTools {
|
|
|
995
995
|
}
|
|
996
996
|
grep(args) {
|
|
997
997
|
const searchPath = args.path ? this.resolvePath(args.path) : this.cwd;
|
|
998
|
-
const
|
|
999
|
-
const
|
|
1000
|
-
//
|
|
1001
|
-
|
|
998
|
+
const osModule = require('os');
|
|
999
|
+
const platform = osModule.platform();
|
|
1000
|
+
// If forced fallback, go directly to Node-native
|
|
1001
|
+
if (args._fallback === 'node-native') {
|
|
1002
|
+
return this.grepNodeNative(args, searchPath);
|
|
1003
|
+
}
|
|
1004
|
+
// Try external search tools first (rg > grep/Select-String), fall back to Node-native
|
|
1005
|
+
if (platform === 'win32') {
|
|
1006
|
+
return this.grepWindows(args, searchPath);
|
|
1007
|
+
}
|
|
1008
|
+
return this.grepUnix(args, searchPath, platform);
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Windows grep: rg > Select-String > Node-native
|
|
1012
|
+
*/
|
|
1013
|
+
grepWindows(args, searchPath) {
|
|
1014
|
+
// 1. Try ripgrep (rg) first — best cross-platform search tool
|
|
1015
|
+
try {
|
|
1016
|
+
(0, child_process_1.execSync)('rg --version', { encoding: 'utf-8', timeout: 5000, stdio: 'pipe' });
|
|
1017
|
+
return this.grepWithRg(args, searchPath);
|
|
1018
|
+
}
|
|
1019
|
+
catch { /* rg not available */ }
|
|
1020
|
+
// 2. Try PowerShell Select-String
|
|
1021
|
+
try {
|
|
1022
|
+
return this.grepWithSelectString(args, searchPath);
|
|
1023
|
+
}
|
|
1024
|
+
catch { /* Select-String failed */ }
|
|
1025
|
+
// 3. Fall back to Node-native recursive file scanner
|
|
1026
|
+
return this.grepNodeNative(args, searchPath);
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Unix grep: rg > system grep (BSD/GNU)
|
|
1030
|
+
*/
|
|
1031
|
+
grepUnix(args, searchPath, platform) {
|
|
1032
|
+
// Try ripgrep first if available
|
|
1033
|
+
try {
|
|
1034
|
+
(0, child_process_1.execSync)('rg --version', { encoding: 'utf-8', timeout: 5000, stdio: 'pipe' });
|
|
1035
|
+
return this.grepWithRg(args, searchPath);
|
|
1036
|
+
}
|
|
1037
|
+
catch { /* rg not available, fall through to system grep */ }
|
|
1038
|
+
const isMac = platform === 'darwin';
|
|
1002
1039
|
let cmd;
|
|
1003
1040
|
if (isMac) {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
cmd = `grep -rn --include="${args.include}" "${args.pattern}" "${searchPath}"`;
|
|
1008
|
-
}
|
|
1009
|
-
else {
|
|
1010
|
-
cmd = `grep -rn "${args.pattern}" "${searchPath}"`;
|
|
1011
|
-
}
|
|
1041
|
+
cmd = args.include
|
|
1042
|
+
? `grep -rn --include="${args.include}" "${args.pattern}" "${searchPath}"`
|
|
1043
|
+
: `grep -rn "${args.pattern}" "${searchPath}"`;
|
|
1012
1044
|
}
|
|
1013
1045
|
else {
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1046
|
+
cmd = args.include
|
|
1047
|
+
? `grep -rn --color=never --include="${args.include}" "${args.pattern}" "${searchPath}"`
|
|
1048
|
+
: `grep -rn --color=never "${args.pattern}" "${searchPath}"`;
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
const output = (0, child_process_1.execSync)(cmd, {
|
|
1052
|
+
cwd: this.cwd,
|
|
1053
|
+
encoding: 'utf-8',
|
|
1054
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
1055
|
+
timeout: 30000,
|
|
1056
|
+
env: { ...process.env, GREP_OPTIONS: '' },
|
|
1057
|
+
});
|
|
1058
|
+
return {
|
|
1059
|
+
success: true,
|
|
1060
|
+
output: output.trim(),
|
|
1061
|
+
metadata: { searchStatus: 'search_matches_found' },
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
catch (error) {
|
|
1065
|
+
// Distinguish real "no matches" (exit 1 + no stderr) from command failure
|
|
1066
|
+
const stderr = (error.stderr || '').toString();
|
|
1067
|
+
if (error.status === 1 && !stderr.trim()) {
|
|
1068
|
+
return {
|
|
1069
|
+
success: true,
|
|
1070
|
+
output: 'No matches found',
|
|
1071
|
+
metadata: { searchStatus: 'search_no_matches' },
|
|
1072
|
+
};
|
|
1020
1073
|
}
|
|
1074
|
+
// Real failure: command not found, permission denied, etc.
|
|
1075
|
+
return {
|
|
1076
|
+
success: false,
|
|
1077
|
+
error: stderr || error.message,
|
|
1078
|
+
suggestion: 'The grep command failed. Install ripgrep (rg) for best cross-platform search.',
|
|
1079
|
+
canRetry: true,
|
|
1080
|
+
metadata: { searchStatus: 'search_failed' },
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Search using ripgrep (rg) — works on all platforms
|
|
1086
|
+
*/
|
|
1087
|
+
grepWithRg(args, searchPath) {
|
|
1088
|
+
let cmd = `rg -n --no-heading`;
|
|
1089
|
+
if (args.include) {
|
|
1090
|
+
cmd += ` -g "${args.include}"`;
|
|
1021
1091
|
}
|
|
1092
|
+
cmd += ` "${args.pattern}" "${searchPath}"`;
|
|
1022
1093
|
try {
|
|
1023
1094
|
const output = (0, child_process_1.execSync)(cmd, {
|
|
1024
1095
|
cwd: this.cwd,
|
|
1025
1096
|
encoding: 'utf-8',
|
|
1026
1097
|
maxBuffer: 5 * 1024 * 1024,
|
|
1027
1098
|
timeout: 30000,
|
|
1028
|
-
env: { ...process.env, GREP_OPTIONS: '' }, // Prevent user's grep options from interfering
|
|
1029
1099
|
});
|
|
1030
|
-
return {
|
|
1100
|
+
return {
|
|
1101
|
+
success: true,
|
|
1102
|
+
output: output.trim(),
|
|
1103
|
+
metadata: { searchStatus: 'search_matches_found', backend: 'ripgrep' },
|
|
1104
|
+
};
|
|
1031
1105
|
}
|
|
1032
1106
|
catch (error) {
|
|
1033
|
-
if (error.status === 1) {
|
|
1034
|
-
|
|
1035
|
-
|
|
1107
|
+
if (error.status === 1 && !(error.stderr || '').toString().trim()) {
|
|
1108
|
+
return {
|
|
1109
|
+
success: true,
|
|
1110
|
+
output: 'No matches found',
|
|
1111
|
+
metadata: { searchStatus: 'search_no_matches', backend: 'ripgrep' },
|
|
1112
|
+
};
|
|
1036
1113
|
}
|
|
1037
|
-
|
|
1114
|
+
throw error; // Let caller try next backend
|
|
1038
1115
|
}
|
|
1039
1116
|
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Search using PowerShell Select-String (Windows)
|
|
1119
|
+
*/
|
|
1120
|
+
grepWithSelectString(args, searchPath) {
|
|
1121
|
+
// Normalize path for PowerShell
|
|
1122
|
+
const psPath = searchPath.replace(/\//g, '\\');
|
|
1123
|
+
const includeFilter = args.include
|
|
1124
|
+
? `-Include "${args.include}"`
|
|
1125
|
+
: `-Include *`;
|
|
1126
|
+
const escapedPattern = args.pattern.replace(/'/g, "''");
|
|
1127
|
+
const cmd = `powershell -NoProfile -Command "Get-ChildItem -Path '${psPath}' -Recurse -File ${includeFilter} | Select-String -Pattern '${escapedPattern}' | ForEach-Object { $_.Path + ':' + $_.LineNumber + ':' + $_.Line }"`;
|
|
1128
|
+
try {
|
|
1129
|
+
const output = (0, child_process_1.execSync)(cmd, {
|
|
1130
|
+
cwd: this.cwd,
|
|
1131
|
+
encoding: 'utf-8',
|
|
1132
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
1133
|
+
timeout: 60000,
|
|
1134
|
+
});
|
|
1135
|
+
const trimmed = output.trim();
|
|
1136
|
+
if (!trimmed) {
|
|
1137
|
+
return {
|
|
1138
|
+
success: true,
|
|
1139
|
+
output: 'No matches found',
|
|
1140
|
+
metadata: { searchStatus: 'search_no_matches', backend: 'select-string' },
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
return {
|
|
1144
|
+
success: true,
|
|
1145
|
+
output: trimmed,
|
|
1146
|
+
metadata: { searchStatus: 'search_matches_found', backend: 'select-string' },
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
catch (error) {
|
|
1150
|
+
const stderr = (error.stderr || '').toString();
|
|
1151
|
+
// Select-String returns exit code 1 for no matches when used in pipeline
|
|
1152
|
+
if (error.status === 1 && !stderr.trim()) {
|
|
1153
|
+
return {
|
|
1154
|
+
success: true,
|
|
1155
|
+
output: 'No matches found',
|
|
1156
|
+
metadata: { searchStatus: 'search_no_matches', backend: 'select-string' },
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
throw error; // Let caller try next backend
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Pure Node.js recursive file search — reliable on all platforms, no external deps
|
|
1164
|
+
*/
|
|
1165
|
+
grepNodeNative(args, searchPath) {
|
|
1166
|
+
const { minimatch } = (() => {
|
|
1167
|
+
try {
|
|
1168
|
+
return require('minimatch');
|
|
1169
|
+
}
|
|
1170
|
+
catch {
|
|
1171
|
+
return { minimatch: null };
|
|
1172
|
+
}
|
|
1173
|
+
})();
|
|
1174
|
+
const results = [];
|
|
1175
|
+
let regex;
|
|
1176
|
+
try {
|
|
1177
|
+
regex = new RegExp(args.pattern, 'i');
|
|
1178
|
+
}
|
|
1179
|
+
catch {
|
|
1180
|
+
// If pattern is not valid regex, treat as literal
|
|
1181
|
+
regex = new RegExp(args.pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
|
|
1182
|
+
}
|
|
1183
|
+
const includeGlob = args.include || null;
|
|
1184
|
+
const maxResults = 500;
|
|
1185
|
+
const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', '__pycache__', '.next', 'vendor', '.venv']);
|
|
1186
|
+
const walk = (dir) => {
|
|
1187
|
+
if (results.length >= maxResults)
|
|
1188
|
+
return;
|
|
1189
|
+
let entries;
|
|
1190
|
+
try {
|
|
1191
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1192
|
+
}
|
|
1193
|
+
catch {
|
|
1194
|
+
return; // Permission denied or inaccessible
|
|
1195
|
+
}
|
|
1196
|
+
for (const entry of entries) {
|
|
1197
|
+
if (results.length >= maxResults)
|
|
1198
|
+
break;
|
|
1199
|
+
const fullPath = path.join(dir, entry.name);
|
|
1200
|
+
if (entry.isDirectory()) {
|
|
1201
|
+
if (!skipDirs.has(entry.name)) {
|
|
1202
|
+
walk(fullPath);
|
|
1203
|
+
}
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
if (!entry.isFile())
|
|
1207
|
+
continue;
|
|
1208
|
+
// Check include pattern
|
|
1209
|
+
if (includeGlob) {
|
|
1210
|
+
const matches = minimatch
|
|
1211
|
+
? minimatch(entry.name, includeGlob)
|
|
1212
|
+
: entry.name.endsWith(includeGlob.replace('*', ''));
|
|
1213
|
+
if (!matches)
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
// Skip binary files by extension
|
|
1217
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
1218
|
+
const binaryExts = new Set(['.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.zip', '.tar', '.gz', '.exe', '.dll', '.so', '.dylib', '.pdf', '.mp3', '.mp4', '.wav', '.avi', '.mov']);
|
|
1219
|
+
if (binaryExts.has(ext))
|
|
1220
|
+
continue;
|
|
1221
|
+
try {
|
|
1222
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
1223
|
+
const lines = content.split('\n');
|
|
1224
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1225
|
+
if (regex.test(lines[i])) {
|
|
1226
|
+
const relativePath = path.relative(this.cwd, fullPath).replace(/\\/g, '/');
|
|
1227
|
+
results.push(`${relativePath}:${i + 1}:${lines[i]}`);
|
|
1228
|
+
if (results.length >= maxResults)
|
|
1229
|
+
break;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
catch {
|
|
1234
|
+
// Skip files that can't be read (binary, encoding issues, etc.)
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
try {
|
|
1239
|
+
walk(searchPath);
|
|
1240
|
+
}
|
|
1241
|
+
catch (error) {
|
|
1242
|
+
return {
|
|
1243
|
+
success: false,
|
|
1244
|
+
error: `File search failed: ${error.message}`,
|
|
1245
|
+
metadata: { searchStatus: 'search_failed', backend: 'node-native' },
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
if (results.length === 0) {
|
|
1249
|
+
return {
|
|
1250
|
+
success: true,
|
|
1251
|
+
output: 'No matches found',
|
|
1252
|
+
metadata: { searchStatus: 'search_no_matches', backend: 'node-native' },
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
const truncationNote = results.length >= maxResults ? `\n...[truncated at ${maxResults} matches]` : '';
|
|
1256
|
+
return {
|
|
1257
|
+
success: true,
|
|
1258
|
+
output: results.join('\n') + truncationNote,
|
|
1259
|
+
metadata: { searchStatus: 'search_matches_found', backend: 'node-native' },
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1040
1262
|
listDir(args) {
|
|
1041
1263
|
const dirPath = this.resolvePath(args.path);
|
|
1042
1264
|
if (!fs.existsSync(dirPath)) {
|