vigthoria-cli 1.13.18 → 1.13.22
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/completions/_vigthoria +49 -0
- package/completions/vigthoria.bash +6 -0
- package/completions/vigthoria.fish +41 -0
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +84 -135
- package/dist/commands/background.d.ts +0 -2
- package/dist/commands/background.js +26 -34
- package/dist/commands/bridge.js +45 -41
- package/dist/commands/cancel.d.ts +1 -0
- package/dist/commands/cancel.js +22 -16
- package/dist/commands/chat.d.ts +5 -42
- package/dist/commands/chat.js +462 -1122
- package/dist/commands/coding-registration.d.ts +4 -0
- package/dist/commands/coding-registration.js +24 -0
- package/dist/commands/config.js +32 -42
- package/dist/commands/conversation-registration.d.ts +4 -0
- package/dist/commands/conversation-registration.js +122 -0
- package/dist/commands/deploy.d.ts +8 -2
- package/dist/commands/deploy.js +92 -90
- package/dist/commands/device.d.ts +1 -2
- package/dist/commands/device.js +37 -21
- package/dist/commands/doctor-registration.d.ts +4 -0
- package/dist/commands/doctor-registration.js +200 -0
- package/dist/commands/edit.js +22 -33
- package/dist/commands/explain.js +7 -10
- package/dist/commands/fork.d.ts +1 -2
- package/dist/commands/fork.js +72 -22
- package/dist/commands/game.d.ts +1 -1
- package/dist/commands/game.js +17 -6
- package/dist/commands/generate.js +7 -9
- package/dist/commands/history.js +11 -7
- package/dist/commands/hub.d.ts +2 -2
- package/dist/commands/hub.js +34 -35
- package/dist/commands/legion.d.ts +0 -1
- package/dist/commands/legion.js +62 -79
- package/dist/commands/music.d.ts +1 -2
- package/dist/commands/music.js +62 -27
- package/dist/commands/platform-registration.d.ts +4 -0
- package/dist/commands/platform-registration.js +45 -0
- package/dist/commands/preview.d.ts +0 -1
- package/dist/commands/preview.js +82 -35
- package/dist/commands/product-run-registration.d.ts +4 -0
- package/dist/commands/product-run-registration.js +490 -0
- package/dist/commands/replay.d.ts +1 -2
- package/dist/commands/replay.js +15 -11
- package/dist/commands/repo.d.ts +14 -7
- package/dist/commands/repo.js +245 -196
- package/dist/commands/review.js +9 -12
- package/dist/commands/security.d.ts +1 -2
- package/dist/commands/security.js +12 -6
- package/dist/commands/update-registration.d.ts +3 -0
- package/dist/commands/update-registration.js +338 -0
- package/dist/commands/v4-registration.d.ts +4 -0
- package/dist/commands/v4-registration.js +24 -0
- package/dist/commands/v4.d.ts +1 -1
- package/dist/commands/v4.js +46 -8
- package/dist/commands/wallet.d.ts +1 -3
- package/dist/commands/wallet.js +18 -45
- package/dist/commands/workflow.d.ts +2 -0
- package/dist/commands/workflow.js +52 -39
- package/dist/index.js +122 -1498
- package/dist/utils/agent-stream-state.d.ts +27 -0
- package/dist/utils/agent-stream-state.js +93 -0
- package/dist/utils/agentRoute.d.ts +2 -1
- package/dist/utils/agentRoute.js +22 -9
- package/dist/utils/agentRunOutcome.d.ts +16 -0
- package/dist/utils/agentRunOutcome.js +37 -2
- package/dist/utils/api-client-factory.d.ts +8 -0
- package/dist/utils/api-client-factory.js +8 -0
- package/dist/utils/api.d.ts +45 -269
- package/dist/utils/api.js +664 -3210
- package/dist/utils/auth-session.d.ts +41 -0
- package/dist/utils/auth-session.js +224 -0
- package/dist/utils/brain-hub-client.d.ts +0 -4
- package/dist/utils/brain-hub-client.js +6 -3
- package/dist/utils/bridge-client.js +2 -0
- package/dist/utils/capability-health-service.d.ts +48 -0
- package/dist/utils/capability-health-service.js +64 -0
- package/dist/utils/chat-prompt-policy.d.ts +18 -0
- package/dist/utils/chat-prompt-policy.js +61 -0
- package/dist/utils/chat-tool-call-parser.d.ts +11 -0
- package/dist/utils/chat-tool-call-parser.js +71 -0
- package/dist/utils/cli-state.d.ts +22 -4
- package/dist/utils/cli-state.js +105 -47
- package/dist/utils/code-operations-service.d.ts +104 -0
- package/dist/utils/code-operations-service.js +837 -0
- package/dist/utils/codebase-indexer.d.ts +1 -0
- package/dist/utils/codebase-indexer.js +28 -8
- package/dist/utils/command-contract.d.ts +61 -0
- package/dist/utils/command-contract.js +149 -0
- package/dist/utils/command-policy.d.ts +10 -0
- package/dist/utils/command-policy.js +54 -0
- package/dist/utils/config.d.ts +13 -5
- package/dist/utils/config.js +227 -99
- package/dist/utils/context-ranker.js +11 -2
- package/dist/utils/context-state.d.ts +39 -0
- package/dist/utils/context-state.js +52 -0
- package/dist/utils/deckEvents.d.ts +0 -5
- package/dist/utils/deckEvents.js +3 -2
- package/dist/utils/desktop-bridge-client.js +2 -0
- package/dist/utils/direct-output-policy.d.ts +3 -0
- package/dist/utils/direct-output-policy.js +76 -0
- package/dist/utils/durable-json.d.ts +57 -0
- package/dist/utils/durable-json.js +319 -0
- package/dist/utils/fastAgentRouter.d.ts +6 -7
- package/dist/utils/fastAgentRouter.js +21 -30
- package/dist/utils/files.js +21 -13
- package/dist/utils/frontend-preview-service.d.ts +76 -0
- package/dist/utils/frontend-preview-service.js +482 -0
- package/dist/utils/goaEvents.d.ts +0 -4
- package/dist/utils/goaEvents.js +5 -2
- package/dist/utils/logger.d.ts +2 -1
- package/dist/utils/logger.js +27 -20
- package/dist/utils/mcp-context-client.d.ts +24 -0
- package/dist/utils/mcp-context-client.js +58 -0
- package/dist/utils/model-governance.d.ts +18 -0
- package/dist/utils/model-governance.js +75 -0
- package/dist/utils/model-transport-service.d.ts +44 -0
- package/dist/utils/model-transport-service.js +111 -0
- package/dist/utils/mutation-journal.d.ts +42 -0
- package/dist/utils/mutation-journal.js +387 -0
- package/dist/utils/network-policy.d.ts +24 -0
- package/dist/utils/network-policy.js +202 -0
- package/dist/utils/operator-client.d.ts +45 -0
- package/dist/utils/operator-client.js +159 -0
- package/dist/utils/post-write-validator.js +7 -9
- package/dist/utils/preview-screenshot-adapter.d.ts +13 -0
- package/dist/utils/preview-screenshot-adapter.js +36 -0
- package/dist/utils/process-policy.d.ts +23 -0
- package/dist/utils/process-policy.js +284 -0
- package/dist/utils/project-memory.d.ts +13 -3
- package/dist/utils/project-memory.js +70 -17
- package/dist/utils/prompt-workspace-resolver.d.ts +14 -0
- package/dist/utils/prompt-workspace-resolver.js +64 -0
- package/dist/utils/release-install.d.ts +11 -0
- package/dist/utils/release-install.js +155 -0
- package/dist/utils/release-policy.d.ts +64 -0
- package/dist/utils/release-policy.js +293 -0
- package/dist/utils/requestIntent.js +22 -3
- package/dist/utils/runtime-capability.d.ts +8 -0
- package/dist/utils/runtime-capability.js +16 -0
- package/dist/utils/secret-policy.d.ts +20 -0
- package/dist/utils/secret-policy.js +160 -0
- package/dist/utils/session.d.ts +34 -2
- package/dist/utils/session.js +354 -110
- package/dist/utils/tool-approval-service.d.ts +17 -0
- package/dist/utils/tool-approval-service.js +78 -0
- package/dist/utils/tool-capability-providers.d.ts +57 -0
- package/dist/utils/tool-capability-providers.js +57 -0
- package/dist/utils/tools.d.ts +2 -23
- package/dist/utils/tools.js +216 -508
- package/dist/utils/update-policy.d.ts +18 -0
- package/dist/utils/update-policy.js +137 -0
- package/dist/utils/v3-agent-client.d.ts +40 -0
- package/dist/utils/v3-agent-client.js +165 -0
- package/dist/utils/v3-workspace-service.d.ts +28 -0
- package/dist/utils/v3-workspace-service.js +279 -0
- package/dist/utils/vigflow-client.d.ts +95 -0
- package/dist/utils/vigflow-client.js +105 -0
- package/dist/utils/workspace-boundary.d.ts +45 -0
- package/dist/utils/workspace-boundary.js +377 -0
- package/dist/utils/workspace-brain-service.d.ts +2 -0
- package/dist/utils/workspace-brain-service.js +11 -1
- package/dist/utils/workspace-cache.js +37 -15
- package/dist/utils/workspace-stream.js +35 -10
- package/install.ps1 +86 -143
- package/install.sh +76 -81
- package/package.json +44 -9
- package/release-policy.json +43 -0
- package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +1 -1
- package/scripts/release/generate-completions.mjs +24 -0
- package/scripts/release/install-release.mjs +10 -0
- package/scripts/release/publish-cli-release.mjs +116 -0
- package/scripts/release/publish-cli-release.sh +2 -70
- package/scripts/release/resolve-release-manifest.mjs +26 -0
- package/scripts/release/test-balanced-model-live.sh +42 -0
- package/scripts/release/validate-live-service-gates.sh +90 -0
- package/scripts/release/validate-no-go-gates.sh +135 -172
- package/scripts/release/verify-runtime-consistency.mjs +4 -1
- package/dist/commands/index.d.ts +0 -12
- package/dist/commands/index.js +0 -194
- package/dist/commands/update.d.ts +0 -9
- package/dist/commands/update.js +0 -301
- package/dist/utils/templateInstantPath.d.ts +0 -18
- package/dist/utils/templateInstantPath.js +0 -228
package/dist/utils/tools.js
CHANGED
|
@@ -15,18 +15,22 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import * as fs from 'fs';
|
|
17
17
|
import * as path from 'path';
|
|
18
|
-
import {
|
|
18
|
+
import { execFileSync, spawn } from 'child_process';
|
|
19
19
|
import { createRequire } from 'node:module';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
20
21
|
import chalk from 'chalk';
|
|
21
22
|
// ESM shim — re-create CommonJS-style `require()` so the inline
|
|
22
23
|
// `require('os')` / `require('minimatch')` / `require('glob')` calls
|
|
23
24
|
// inside lazy code paths continue to work unchanged in ESM mode.
|
|
24
25
|
const require = createRequire(import.meta.url);
|
|
25
26
|
import { CH } from './logger.js';
|
|
26
|
-
import {
|
|
27
|
+
import { assertContainedAbsolutePath } from './workspace-boundary.js';
|
|
28
|
+
import { assertProcessNetworkAllowed, buildApprovalScope, hardenGitArguments, parseGitArguments, parseSupportedProcess, safeGitEnvironment, } from './process-policy.js';
|
|
29
|
+
import { containsHighConfidenceSecret, isSensitivePath, safeChildProcessEnv } from './secret-policy.js';
|
|
30
|
+
import { isOfflineNetworkMode } from './network-policy.js';
|
|
31
|
+
import { ToolApprovalService } from './tool-approval-service.js';
|
|
32
|
+
import { DelegationToolProvider, FilesystemToolProvider, NetworkBrowserToolProvider, ProcessToolProvider, RepositoryToolProvider, SearchToolProvider, SshToolProvider, ToolCapabilityRegistry, } from './tool-capability-providers.js';
|
|
27
33
|
const STREAM_RESPONSE_MAX_YIELD_CHARS = 32 * 1024;
|
|
28
|
-
const POWERSHELL_SAFE_PATH_PATTERN = /^[A-Za-z0-9_:\\/.\-\s]+$/;
|
|
29
|
-
const POWERSHELL_SAFE_INCLUDE_PATTERN = /^[A-Za-z0-9_*?.\-]+$/;
|
|
30
34
|
const SSH_SAFE_HOST_PATTERN = /^[A-Za-z0-9.-]+$/;
|
|
31
35
|
function getSshAllowedHosts() {
|
|
32
36
|
const configured = String(process.env.VIGTHORIA_SSH_ALLOWED_HOSTS || '')
|
|
@@ -58,6 +62,9 @@ function resolveWindowsInstallerPath() {
|
|
|
58
62
|
export async function installUpdateWindows() {
|
|
59
63
|
const installerPath = resolveWindowsInstallerPath();
|
|
60
64
|
try {
|
|
65
|
+
if (isOfflineNetworkMode()) {
|
|
66
|
+
return { success: false, platform: 'windows', error: 'Offline mode prohibits launching the update installer' };
|
|
67
|
+
}
|
|
61
68
|
if (!fs.existsSync(installerPath)) {
|
|
62
69
|
return { success: false, platform: 'windows', error: 'ENOENT' };
|
|
63
70
|
}
|
|
@@ -68,9 +75,18 @@ export async function installUpdateWindows() {
|
|
|
68
75
|
detached: true,
|
|
69
76
|
stdio: 'ignore',
|
|
70
77
|
windowsHide: false,
|
|
78
|
+
env: safeChildProcessEnv(),
|
|
79
|
+
});
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
child.kill();
|
|
82
|
+
reject(new Error('Windows update installer did not start within 10 seconds'));
|
|
83
|
+
}, 10_000);
|
|
84
|
+
child.once('error', (error) => {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
reject(error);
|
|
71
87
|
});
|
|
72
|
-
child.once('error', reject);
|
|
73
88
|
child.once('spawn', () => {
|
|
89
|
+
clearTimeout(timer);
|
|
74
90
|
child.unref();
|
|
75
91
|
resolve();
|
|
76
92
|
});
|
|
@@ -89,7 +105,6 @@ export async function* robustifyStreamResponse(res) {
|
|
|
89
105
|
const MAX_YIELD_CHARS = STREAM_RESPONSE_MAX_YIELD_CHARS;
|
|
90
106
|
const MAX_BUFFER_CHARS = 256 * 1024;
|
|
91
107
|
const body = res?.body ?? res;
|
|
92
|
-
const decoder = new TextDecoder('utf-8');
|
|
93
108
|
let reader = null;
|
|
94
109
|
let lineBuffer = '';
|
|
95
110
|
const emitContent = async function* (type, content) {
|
|
@@ -330,6 +345,8 @@ export class AgenticTools {
|
|
|
330
345
|
cwd;
|
|
331
346
|
permissionCallback;
|
|
332
347
|
autoApprove;
|
|
348
|
+
approvals;
|
|
349
|
+
capabilityRegistry;
|
|
333
350
|
undoStack = [];
|
|
334
351
|
maxUndoStack = 50;
|
|
335
352
|
retryConfig = {
|
|
@@ -353,17 +370,6 @@ export class AgenticTools {
|
|
|
353
370
|
}
|
|
354
371
|
return `External tool call failed in ${toolName} during ${operation}: ${String(error)}`;
|
|
355
372
|
}
|
|
356
|
-
externalToolFailure(toolName, operation, error, suggestion) {
|
|
357
|
-
const message = this.formatExternalToolError(toolName, operation, error);
|
|
358
|
-
this.logger.debug(message);
|
|
359
|
-
this.logger.warn(message);
|
|
360
|
-
return {
|
|
361
|
-
success: false,
|
|
362
|
-
error: message,
|
|
363
|
-
suggestion: suggestion || 'Review the command, arguments, permissions, and environment, then retry.',
|
|
364
|
-
canRetry: true,
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
373
|
cleanupAfterToolError(toolName, operation, cleanup) {
|
|
368
374
|
try {
|
|
369
375
|
cleanup();
|
|
@@ -374,29 +380,6 @@ export class AgenticTools {
|
|
|
374
380
|
this.logger.warn(message);
|
|
375
381
|
}
|
|
376
382
|
}
|
|
377
|
-
runExternalCommand(toolName, operation, command, options, cleanup) {
|
|
378
|
-
if (!this.isNonEmptyString(toolName)) {
|
|
379
|
-
throw new Error('Invalid tool execution: toolName must be a non-empty string.');
|
|
380
|
-
}
|
|
381
|
-
if (!this.isNonEmptyString(operation)) {
|
|
382
|
-
throw new Error(`Invalid external command for ${toolName}: operation must be a non-empty string.`);
|
|
383
|
-
}
|
|
384
|
-
if (!this.isNonEmptyString(command)) {
|
|
385
|
-
throw new Error(`Invalid external command for ${toolName} during ${operation}: command must be a non-empty string.`);
|
|
386
|
-
}
|
|
387
|
-
try {
|
|
388
|
-
return execSync(command, options);
|
|
389
|
-
}
|
|
390
|
-
catch (error) {
|
|
391
|
-
if (cleanup) {
|
|
392
|
-
this.cleanupAfterToolError(toolName, operation, cleanup);
|
|
393
|
-
}
|
|
394
|
-
const message = this.formatExternalToolError(toolName, operation, error);
|
|
395
|
-
const wrapped = new Error(message);
|
|
396
|
-
wrapped.cause = error;
|
|
397
|
-
throw wrapped;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
383
|
isNonEmptyString(value) {
|
|
401
384
|
return typeof value === 'string' && value.trim().length > 0;
|
|
402
385
|
}
|
|
@@ -422,19 +405,6 @@ export class AgenticTools {
|
|
|
422
405
|
}
|
|
423
406
|
}
|
|
424
407
|
}
|
|
425
|
-
requireNonEmptyString(value, fieldName, toolName) {
|
|
426
|
-
if (!this.isNonEmptyString(value)) {
|
|
427
|
-
throw new Error(`Invalid arguments for ${toolName}: ${fieldName} is required and must be a non-empty string.`);
|
|
428
|
-
}
|
|
429
|
-
return value;
|
|
430
|
-
}
|
|
431
|
-
requireArgsObject(args, toolName) {
|
|
432
|
-
this.assertStringRecord(args, `Invalid arguments for ${toolName}: args`);
|
|
433
|
-
}
|
|
434
|
-
// Session-based tool approvals - remembers which tools user approved for this turn
|
|
435
|
-
sessionApprovedTools = new Set();
|
|
436
|
-
// Persistent permissions - tool allowlists per project
|
|
437
|
-
static permissionsFile = path.join(process.env.HOME || process.env.USERPROFILE || '~', '.vigthoria', 'permissions.json');
|
|
438
408
|
constructor(logger, cwd, permissionCallback, autoApprove = false) {
|
|
439
409
|
if (!logger) {
|
|
440
410
|
throw new Error('AgenticTools initialization failed: logger is required.');
|
|
@@ -449,9 +419,23 @@ export class AgenticTools {
|
|
|
449
419
|
throw new Error('AgenticTools initialization failed: autoApprove must be a boolean.');
|
|
450
420
|
}
|
|
451
421
|
this.logger = logger;
|
|
452
|
-
this.cwd = cwd;
|
|
422
|
+
this.cwd = fs.realpathSync(path.resolve(cwd));
|
|
453
423
|
this.permissionCallback = permissionCallback;
|
|
454
424
|
this.autoApprove = autoApprove;
|
|
425
|
+
this.approvals = new ToolApprovalService(this.cwd, logger);
|
|
426
|
+
this.capabilityRegistry = new ToolCapabilityRegistry([
|
|
427
|
+
new FilesystemToolProvider({
|
|
428
|
+
read_file: (args) => this.readFile(args), write_file: (args) => this.writeFile(args),
|
|
429
|
+
edit_file: (args) => this.editFile(args), list_dir: (args) => this.listDir(args),
|
|
430
|
+
glob: (args) => this.glob(args), multi_edit: (args) => this.multiEdit(args),
|
|
431
|
+
}),
|
|
432
|
+
new ProcessToolProvider({ bash: (args) => this.bash(args) }),
|
|
433
|
+
new SearchToolProvider({ grep: (args) => this.grep(args), codebase_search: (args) => this.codebaseSearch(args) }),
|
|
434
|
+
new RepositoryToolProvider({ git: (args) => this.git(args), repo: (args) => this.repo(args) }),
|
|
435
|
+
new NetworkBrowserToolProvider({ fetch_url: (args) => this.fetchUrl(args), browser: (args) => this.browserTool(args) }),
|
|
436
|
+
new SshToolProvider({ ssh_exec: (args) => this.sshExec(args) }),
|
|
437
|
+
new DelegationToolProvider({ task: (args) => this.task(args) }),
|
|
438
|
+
]);
|
|
455
439
|
}
|
|
456
440
|
getErrorMessage(error) {
|
|
457
441
|
if (error instanceof Error) {
|
|
@@ -467,85 +451,17 @@ export class AgenticTools {
|
|
|
467
451
|
return String(error);
|
|
468
452
|
}
|
|
469
453
|
}
|
|
470
|
-
assertToolCall(call) {
|
|
471
|
-
if (!call || typeof call !== 'object') {
|
|
472
|
-
throw new Error('Invalid tool call: call must be an object.');
|
|
473
|
-
}
|
|
474
|
-
if (typeof call.tool !== 'string' || call.tool.trim().length === 0) {
|
|
475
|
-
throw new Error('Invalid tool call: tool name must be a non-empty string.');
|
|
476
|
-
}
|
|
477
|
-
if (!call.args || typeof call.args !== 'object' || Array.isArray(call.args)) {
|
|
478
|
-
throw new Error(`Invalid tool call for ${call.tool}: args must be an object.`);
|
|
479
|
-
}
|
|
480
|
-
for (const [key, value] of Object.entries(call.args)) {
|
|
481
|
-
if (typeof key !== 'string' || key.trim().length === 0) {
|
|
482
|
-
throw new Error(`Invalid tool call for ${call.tool}: argument names must be non-empty strings.`);
|
|
483
|
-
}
|
|
484
|
-
if (typeof value !== 'string') {
|
|
485
|
-
throw new Error(`Invalid argument "${key}" for ${call.tool}: expected string, received ${typeof value}.`);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
/**
|
|
490
|
-
* Load persistent permissions for the current project
|
|
491
|
-
*/
|
|
492
|
-
loadPersistentPermissions() {
|
|
493
|
-
try {
|
|
494
|
-
if (fs.existsSync(AgenticTools.permissionsFile)) {
|
|
495
|
-
return JSON.parse(fs.readFileSync(AgenticTools.permissionsFile, 'utf-8'));
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
catch (error) {
|
|
499
|
-
const message = this.formatExternalToolError('permissions', `read ${AgenticTools.permissionsFile}`, error);
|
|
500
|
-
this.logger.debug(message);
|
|
501
|
-
this.logger.warn(message);
|
|
502
|
-
}
|
|
503
|
-
return {};
|
|
504
|
-
}
|
|
505
|
-
/**
|
|
506
|
-
* Save a persistent permission for a tool in the current project
|
|
507
|
-
*/
|
|
508
|
-
savePersistentPermission(toolName) {
|
|
509
|
-
const permissions = this.loadPersistentPermissions();
|
|
510
|
-
const projectKey = this.cwd;
|
|
511
|
-
if (!permissions[projectKey]) {
|
|
512
|
-
permissions[projectKey] = { tools: [], updatedAt: new Date().toISOString() };
|
|
513
|
-
}
|
|
514
|
-
if (!permissions[projectKey].tools.includes(toolName)) {
|
|
515
|
-
permissions[projectKey].tools.push(toolName);
|
|
516
|
-
permissions[projectKey].updatedAt = new Date().toISOString();
|
|
517
|
-
}
|
|
518
|
-
try {
|
|
519
|
-
const dir = path.dirname(AgenticTools.permissionsFile);
|
|
520
|
-
if (!fs.existsSync(dir))
|
|
521
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
522
|
-
fs.writeFileSync(AgenticTools.permissionsFile, JSON.stringify(permissions, null, 2), 'utf-8');
|
|
523
|
-
}
|
|
524
|
-
catch (error) {
|
|
525
|
-
const message = this.formatExternalToolError('permissions', `write ${AgenticTools.permissionsFile}`, error);
|
|
526
|
-
this.logger.debug(message);
|
|
527
|
-
this.logger.warn(message);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
/**
|
|
531
|
-
* Check if a tool has persistent permission for the current project
|
|
532
|
-
*/
|
|
533
|
-
hasPersistentPermission(toolName) {
|
|
534
|
-
const permissions = this.loadPersistentPermissions();
|
|
535
|
-
const projectKey = this.cwd;
|
|
536
|
-
return permissions[projectKey]?.tools?.includes(toolName) || false;
|
|
537
|
-
}
|
|
538
454
|
/**
|
|
539
455
|
* Clear session-approved tools (call this at the start of each new AI turn)
|
|
540
456
|
*/
|
|
541
457
|
clearSessionApprovals() {
|
|
542
|
-
this.
|
|
458
|
+
this.approvals.clearSession();
|
|
543
459
|
}
|
|
544
460
|
/**
|
|
545
461
|
* Get currently approved tools for this session
|
|
546
462
|
*/
|
|
547
463
|
getSessionApprovedTools() {
|
|
548
|
-
return
|
|
464
|
+
return this.approvals.sessionKeys();
|
|
549
465
|
}
|
|
550
466
|
/**
|
|
551
467
|
* Get the undo stack for inspection
|
|
@@ -647,7 +563,7 @@ export class AgenticTools {
|
|
|
647
563
|
},
|
|
648
564
|
{
|
|
649
565
|
name: 'bash',
|
|
650
|
-
description: 'Run a shell
|
|
566
|
+
description: 'Run one supported process using a parsed argument vector (no shell syntax)',
|
|
651
567
|
parameters: [
|
|
652
568
|
{ name: 'command', description: 'Shell command to execute', required: true },
|
|
653
569
|
{ name: 'cwd', description: 'Working directory', required: false },
|
|
@@ -835,11 +751,17 @@ export class AgenticTools {
|
|
|
835
751
|
}
|
|
836
752
|
// Check permission for dangerous/modifying actions
|
|
837
753
|
if (tool.requiresPermission && !this.autoApprove) {
|
|
838
|
-
|
|
839
|
-
|
|
754
|
+
let approvalScope;
|
|
755
|
+
try {
|
|
756
|
+
approvalScope = buildApprovalScope(normalizedCall.tool, normalizedCall.args, this.cwd);
|
|
757
|
+
}
|
|
758
|
+
catch (error) {
|
|
759
|
+
return this.createErrorResult(ToolErrorType.INVALID_ARGS, this.getErrorMessage(error));
|
|
760
|
+
}
|
|
761
|
+
if (this.approvals.hasPersistent(approvalScope)) {
|
|
840
762
|
this.logger.info(`${call.tool}: Auto-approved (persistent)`);
|
|
841
763
|
}
|
|
842
|
-
else if (this.
|
|
764
|
+
else if (this.approvals.hasSession(approvalScope)) {
|
|
843
765
|
// Already approved - skip permission prompt
|
|
844
766
|
this.logger.info(`${call.tool}: Auto-approved (batch)`);
|
|
845
767
|
}
|
|
@@ -856,17 +778,16 @@ export class AgenticTools {
|
|
|
856
778
|
// 'persist' (typed 'p') = approve permanently for this project
|
|
857
779
|
// 'y' or 'yes' = approve this single request only
|
|
858
780
|
if (approved === 'batch') {
|
|
859
|
-
this.
|
|
781
|
+
this.approvals.recordSession(approvalScope);
|
|
860
782
|
}
|
|
861
783
|
else if (approved === 'persist') {
|
|
862
|
-
this.
|
|
863
|
-
this.
|
|
864
|
-
this.logger.info(`${normalizedCall.tool}: Saved as persistent permission`);
|
|
784
|
+
this.approvals.recordPersistent(approvalScope);
|
|
785
|
+
this.logger.info(`${normalizedCall.tool}: Saved exact operation as persistent permission`);
|
|
865
786
|
}
|
|
866
787
|
}
|
|
867
788
|
}
|
|
868
789
|
// Execute with retry logic for applicable operations
|
|
869
|
-
return this.executeWithRetry(normalizedCall
|
|
790
|
+
return this.executeWithRetry(normalizedCall);
|
|
870
791
|
}
|
|
871
792
|
normalizeToolCall(call) {
|
|
872
793
|
const tool = String(call.tool || '').trim();
|
|
@@ -912,7 +833,7 @@ export class AgenticTools {
|
|
|
912
833
|
/**
|
|
913
834
|
* Execute tool with automatic retry for transient failures
|
|
914
835
|
*/
|
|
915
|
-
async executeWithRetry(call
|
|
836
|
+
async executeWithRetry(call) {
|
|
916
837
|
let lastError = null;
|
|
917
838
|
for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
|
|
918
839
|
try {
|
|
@@ -980,40 +901,7 @@ export class AgenticTools {
|
|
|
980
901
|
message,
|
|
981
902
|
};
|
|
982
903
|
}
|
|
983
|
-
|
|
984
|
-
case 'read_file':
|
|
985
|
-
return this.readFile(call.args);
|
|
986
|
-
case 'write_file':
|
|
987
|
-
return this.writeFile(call.args);
|
|
988
|
-
case 'edit_file':
|
|
989
|
-
return this.editFile(call.args);
|
|
990
|
-
case 'bash':
|
|
991
|
-
return this.bash(call.args);
|
|
992
|
-
case 'grep':
|
|
993
|
-
return this.grep(call.args);
|
|
994
|
-
case 'list_dir':
|
|
995
|
-
return this.listDir(call.args);
|
|
996
|
-
case 'glob':
|
|
997
|
-
return this.glob(call.args);
|
|
998
|
-
case 'git':
|
|
999
|
-
return this.git(call.args);
|
|
1000
|
-
case 'repo':
|
|
1001
|
-
return this.repo(call.args);
|
|
1002
|
-
case 'fetch_url':
|
|
1003
|
-
return this.fetchUrl(call.args);
|
|
1004
|
-
case 'browser':
|
|
1005
|
-
return this.browserTool(call.args);
|
|
1006
|
-
case 'ssh_exec':
|
|
1007
|
-
return this.sshExec(call.args);
|
|
1008
|
-
case 'task':
|
|
1009
|
-
return this.task(call.args);
|
|
1010
|
-
case 'multi_edit':
|
|
1011
|
-
return this.multiEdit(call.args);
|
|
1012
|
-
case 'codebase_search':
|
|
1013
|
-
return this.codebaseSearch(call.args);
|
|
1014
|
-
default:
|
|
1015
|
-
return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Tool not implemented: ${call.tool}`);
|
|
1016
|
-
}
|
|
904
|
+
return await this.capabilityRegistry.execute(call);
|
|
1017
905
|
}
|
|
1018
906
|
catch (error) {
|
|
1019
907
|
const context = `Tool '${call.tool}' failed while executing with args ${this.safeStringifyArgs(call.args)}`;
|
|
@@ -1216,7 +1104,7 @@ export class AgenticTools {
|
|
|
1216
1104
|
msg += chalk.red(' The AI is requesting to execute commands on your system.\n');
|
|
1217
1105
|
}
|
|
1218
1106
|
// Add batch approval hint
|
|
1219
|
-
msg += `\n${chalk.gray('Respond:')} ${chalk.white('[y]es')} ${chalk.gray('/')} ${chalk.white('[n]o')} ${chalk.gray('/')} ${chalk.cyan('[a]pprove
|
|
1107
|
+
msg += `\n${chalk.gray('Respond:')} ${chalk.white('[y]es')} ${chalk.gray('/')} ${chalk.white('[n]o')} ${chalk.gray('/')} ${chalk.cyan('[a]pprove this exact operation')} ${chalk.gray('for this turn')}\n`;
|
|
1220
1108
|
return msg;
|
|
1221
1109
|
}
|
|
1222
1110
|
sleep(ms) {
|
|
@@ -1227,6 +1115,9 @@ export class AgenticTools {
|
|
|
1227
1115
|
* Read file with enhanced error handling and suggestions
|
|
1228
1116
|
*/
|
|
1229
1117
|
readFile(args) {
|
|
1118
|
+
if (isSensitivePath(args.path)) {
|
|
1119
|
+
return this.createErrorResult(ToolErrorType.PERMISSION_DENIED, `Sensitive file is excluded from agent context: ${path.basename(args.path)}`);
|
|
1120
|
+
}
|
|
1230
1121
|
const filePath = this.resolvePath(args.path);
|
|
1231
1122
|
if (!fs.existsSync(filePath)) {
|
|
1232
1123
|
// Try to find similar files
|
|
@@ -1322,7 +1213,7 @@ export class AgenticTools {
|
|
|
1322
1213
|
};
|
|
1323
1214
|
this.undoStack.push(undoOp);
|
|
1324
1215
|
// Limit undo stack size
|
|
1325
|
-
if (this.undoStack.length >
|
|
1216
|
+
if (this.undoStack.length > this.maxUndoStack) {
|
|
1326
1217
|
this.undoStack.shift();
|
|
1327
1218
|
}
|
|
1328
1219
|
const isNew = originalContent === null;
|
|
@@ -1395,7 +1286,7 @@ export class AgenticTools {
|
|
|
1395
1286
|
};
|
|
1396
1287
|
this.undoStack.push(undoOp);
|
|
1397
1288
|
// Limit undo stack size
|
|
1398
|
-
if (this.undoStack.length >
|
|
1289
|
+
if (this.undoStack.length > this.maxUndoStack) {
|
|
1399
1290
|
this.undoStack.shift();
|
|
1400
1291
|
}
|
|
1401
1292
|
// Show syntax-highlighted diff
|
|
@@ -1459,7 +1350,7 @@ export class AgenticTools {
|
|
|
1459
1350
|
return this.formatExternalToolError('syntax_check', `write temporary file ${tempFile}`, writeError);
|
|
1460
1351
|
}
|
|
1461
1352
|
try {
|
|
1462
|
-
|
|
1353
|
+
execFileSync(process.execPath, ['--check', tempFile], { stdio: 'pipe', timeout: 10_000, windowsHide: true, env: safeChildProcessEnv() });
|
|
1463
1354
|
return null;
|
|
1464
1355
|
}
|
|
1465
1356
|
catch (error) {
|
|
@@ -1615,30 +1506,21 @@ export class AgenticTools {
|
|
|
1615
1506
|
}
|
|
1616
1507
|
try {
|
|
1617
1508
|
const startTime = Date.now();
|
|
1618
|
-
|
|
1509
|
+
const processRequest = parseSupportedProcess(args.command);
|
|
1619
1510
|
const execOptions = {
|
|
1620
1511
|
cwd,
|
|
1621
1512
|
timeout,
|
|
1622
|
-
encoding: '
|
|
1513
|
+
encoding: 'utf8',
|
|
1623
1514
|
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
1624
1515
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1625
1516
|
// Ensure consistent environment
|
|
1626
|
-
env: {
|
|
1627
|
-
...process.env,
|
|
1628
|
-
// Prevent locale issues
|
|
1629
|
-
LC_ALL: 'C',
|
|
1630
|
-
LANG: 'C',
|
|
1631
|
-
},
|
|
1517
|
+
env: safeChildProcessEnv(process.env, { LC_ALL: 'C', LANG: 'C' }),
|
|
1632
1518
|
};
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
// Use /bin/sh on Unix-like systems (more portable than bash)
|
|
1639
|
-
execOptions.shell = '/bin/sh';
|
|
1640
|
-
}
|
|
1641
|
-
const output = this.runExternalCommand('bash', args.command, args.command, execOptions);
|
|
1519
|
+
const output = execFileSync(processRequest.executable, processRequest.args, {
|
|
1520
|
+
...execOptions,
|
|
1521
|
+
encoding: 'utf8',
|
|
1522
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1523
|
+
});
|
|
1642
1524
|
const duration = Date.now() - startTime;
|
|
1643
1525
|
return {
|
|
1644
1526
|
success: true,
|
|
@@ -1651,8 +1533,23 @@ export class AgenticTools {
|
|
|
1651
1533
|
}
|
|
1652
1534
|
catch (error) {
|
|
1653
1535
|
// Command failed but may have output
|
|
1654
|
-
|
|
1655
|
-
|
|
1536
|
+
// runExternalCommand deliberately wraps child_process failures so every
|
|
1537
|
+
// external tool gets a consistent message. Preserve the underlying
|
|
1538
|
+
// ExecException here; otherwise stdout/stderr/exit status disappear and
|
|
1539
|
+
// remote executors receive only the unhelpful "Command failed" wrapper.
|
|
1540
|
+
const processError = error?.cause && typeof error.cause === 'object'
|
|
1541
|
+
? error.cause
|
|
1542
|
+
: error;
|
|
1543
|
+
const decodeProcessText = (value) => {
|
|
1544
|
+
if (Buffer.isBuffer(value))
|
|
1545
|
+
return value.toString('utf8');
|
|
1546
|
+
return typeof value === 'string' ? value : '';
|
|
1547
|
+
};
|
|
1548
|
+
const output = decodeProcessText(processError?.stdout);
|
|
1549
|
+
const stderr = decodeProcessText(processError?.stderr);
|
|
1550
|
+
const exitStatus = Number.isInteger(processError?.status)
|
|
1551
|
+
? ` (exit ${processError.status})`
|
|
1552
|
+
: '';
|
|
1656
1553
|
// Provide helpful error messages for common cases
|
|
1657
1554
|
if (error.killed) {
|
|
1658
1555
|
return this.createErrorResult(ToolErrorType.TIMEOUT, `Command timed out after ${timeout / 1000}s`, 'Try increasing the timeout or breaking the command into smaller parts.');
|
|
@@ -1667,9 +1564,9 @@ export class AgenticTools {
|
|
|
1667
1564
|
return {
|
|
1668
1565
|
success: false,
|
|
1669
1566
|
output,
|
|
1670
|
-
error: stderr || error.message
|
|
1567
|
+
error: stderr || `${error.message}${exitStatus}`,
|
|
1671
1568
|
suggestion: 'Check command syntax and try again.',
|
|
1672
|
-
canRetry: !
|
|
1569
|
+
canRetry: !processError?.killed,
|
|
1673
1570
|
};
|
|
1674
1571
|
}
|
|
1675
1572
|
}
|
|
@@ -1685,7 +1582,7 @@ export class AgenticTools {
|
|
|
1685
1582
|
if (platform === 'win32') {
|
|
1686
1583
|
return this.grepWindows(args, searchPath);
|
|
1687
1584
|
}
|
|
1688
|
-
return this.grepUnix(args, searchPath
|
|
1585
|
+
return this.grepUnix(args, searchPath);
|
|
1689
1586
|
}
|
|
1690
1587
|
/**
|
|
1691
1588
|
* Windows grep: rg > Select-String > Node-native
|
|
@@ -1693,96 +1590,51 @@ export class AgenticTools {
|
|
|
1693
1590
|
grepWindows(args, searchPath) {
|
|
1694
1591
|
// 1. Try ripgrep (rg) first — best cross-platform search tool
|
|
1695
1592
|
try {
|
|
1696
|
-
|
|
1593
|
+
execFileSync('rg', ['--version'], { encoding: 'utf-8', timeout: 5000, stdio: 'pipe', windowsHide: true, env: safeChildProcessEnv() });
|
|
1697
1594
|
return this.grepWithRg(args, searchPath);
|
|
1698
1595
|
}
|
|
1699
1596
|
catch (error) {
|
|
1700
1597
|
this.logger.debug(this.formatExternalToolError('grep', 'checking ripgrep availability on Windows', error));
|
|
1701
1598
|
}
|
|
1702
|
-
//
|
|
1703
|
-
|
|
1704
|
-
return this.grepWithSelectString(args, searchPath);
|
|
1705
|
-
}
|
|
1706
|
-
catch (error) {
|
|
1707
|
-
this.logger.debug(this.formatExternalToolError('grep', 'running PowerShell Select-String fallback', error));
|
|
1708
|
-
}
|
|
1709
|
-
// 3. Fall back to Node-native recursive file scanner
|
|
1599
|
+
// Fall back to the Node-native scanner. Avoid constructing a PowerShell
|
|
1600
|
+
// program from model-controlled patterns.
|
|
1710
1601
|
return this.grepNodeNative(args, searchPath);
|
|
1711
1602
|
}
|
|
1712
1603
|
/**
|
|
1713
1604
|
* Unix grep: rg > system grep (BSD/GNU)
|
|
1714
1605
|
*/
|
|
1715
|
-
grepUnix(args, searchPath
|
|
1606
|
+
grepUnix(args, searchPath) {
|
|
1716
1607
|
// Try ripgrep first if available
|
|
1717
1608
|
try {
|
|
1718
|
-
|
|
1609
|
+
execFileSync('rg', ['--version'], { encoding: 'utf-8', timeout: 5000, stdio: 'pipe', windowsHide: true, env: safeChildProcessEnv() });
|
|
1719
1610
|
return this.grepWithRg(args, searchPath);
|
|
1720
1611
|
}
|
|
1721
1612
|
catch (error) {
|
|
1722
1613
|
this.logger.debug(this.formatExternalToolError('grep', 'checking ripgrep availability on Unix', error));
|
|
1723
1614
|
// rg not available, fall through to system grep
|
|
1724
1615
|
}
|
|
1725
|
-
|
|
1726
|
-
let cmd;
|
|
1727
|
-
if (isMac) {
|
|
1728
|
-
cmd = args.include
|
|
1729
|
-
? `grep -rn --include="${args.include}" "${args.pattern}" "${searchPath}"`
|
|
1730
|
-
: `grep -rn "${args.pattern}" "${searchPath}"`;
|
|
1731
|
-
}
|
|
1732
|
-
else {
|
|
1733
|
-
cmd = args.include
|
|
1734
|
-
? `grep -rn --color=never --include="${args.include}" "${args.pattern}" "${searchPath}"`
|
|
1735
|
-
: `grep -rn --color=never "${args.pattern}" "${searchPath}"`;
|
|
1736
|
-
}
|
|
1737
|
-
try {
|
|
1738
|
-
const output = this.runExternalCommand('grep', 'system grep search', cmd, {
|
|
1739
|
-
cwd: this.cwd,
|
|
1740
|
-
encoding: 'utf-8',
|
|
1741
|
-
maxBuffer: 5 * 1024 * 1024,
|
|
1742
|
-
timeout: 30000,
|
|
1743
|
-
env: { ...process.env, GREP_OPTIONS: '' },
|
|
1744
|
-
});
|
|
1745
|
-
return {
|
|
1746
|
-
success: true,
|
|
1747
|
-
output: output.trim(),
|
|
1748
|
-
metadata: { searchStatus: 'search_matches_found' },
|
|
1749
|
-
};
|
|
1750
|
-
}
|
|
1751
|
-
catch (error) {
|
|
1752
|
-
// Distinguish real "no matches" (exit 1 + no stderr) from command failure
|
|
1753
|
-
const stderr = (error.stderr || '').toString();
|
|
1754
|
-
if (error.status === 1 && !stderr.trim()) {
|
|
1755
|
-
return {
|
|
1756
|
-
success: true,
|
|
1757
|
-
output: 'No matches found',
|
|
1758
|
-
metadata: { searchStatus: 'search_no_matches' },
|
|
1759
|
-
};
|
|
1760
|
-
}
|
|
1761
|
-
// Real failure: command not found, permission denied, etc.
|
|
1762
|
-
return {
|
|
1763
|
-
success: false,
|
|
1764
|
-
error: stderr || error.message,
|
|
1765
|
-
suggestion: 'The grep command failed. Install ripgrep (rg) for best cross-platform search.',
|
|
1766
|
-
canRetry: true,
|
|
1767
|
-
metadata: { searchStatus: 'search_failed' },
|
|
1768
|
-
};
|
|
1769
|
-
}
|
|
1616
|
+
return this.grepNodeNative(args, searchPath);
|
|
1770
1617
|
}
|
|
1771
1618
|
/**
|
|
1772
1619
|
* Search using ripgrep (rg) — works on all platforms
|
|
1773
1620
|
*/
|
|
1774
1621
|
grepWithRg(args, searchPath) {
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1622
|
+
const rgArgs = [
|
|
1623
|
+
'-n', '--no-heading',
|
|
1624
|
+
'-g', '!.git/**', '-g', '!.vigthoria/**', '-g', '!.env', '-g', '!.env.*',
|
|
1625
|
+
'-g', '!**/credentials*', '-g', '!**/secrets*', '-g', '!**/*.pem', '-g', '!**/*.key',
|
|
1626
|
+
];
|
|
1627
|
+
if (args.include)
|
|
1628
|
+
rgArgs.push('-g', args.include);
|
|
1629
|
+
rgArgs.push('--', args.pattern, searchPath);
|
|
1780
1630
|
try {
|
|
1781
|
-
const output =
|
|
1631
|
+
const output = execFileSync('rg', rgArgs, {
|
|
1782
1632
|
cwd: this.cwd,
|
|
1783
|
-
encoding: '
|
|
1633
|
+
encoding: 'utf8',
|
|
1784
1634
|
maxBuffer: 5 * 1024 * 1024,
|
|
1785
1635
|
timeout: 30000,
|
|
1636
|
+
windowsHide: true,
|
|
1637
|
+
env: safeChildProcessEnv(),
|
|
1786
1638
|
});
|
|
1787
1639
|
return {
|
|
1788
1640
|
success: true,
|
|
@@ -1801,56 +1653,6 @@ export class AgenticTools {
|
|
|
1801
1653
|
throw error; // Let caller try next backend
|
|
1802
1654
|
}
|
|
1803
1655
|
}
|
|
1804
|
-
/**
|
|
1805
|
-
* Search using PowerShell Select-String (Windows)
|
|
1806
|
-
*/
|
|
1807
|
-
grepWithSelectString(args, searchPath) {
|
|
1808
|
-
// Normalize path for PowerShell
|
|
1809
|
-
const psPath = searchPath.replace(/\//g, '\\');
|
|
1810
|
-
if (!POWERSHELL_SAFE_PATH_PATTERN.test(psPath)) {
|
|
1811
|
-
return this.createErrorResult(ToolErrorType.INVALID_ARGS, 'Unsafe search path for PowerShell backend', 'Use a normal workspace path containing only letters, numbers, separators, dot, dash, and spaces.');
|
|
1812
|
-
}
|
|
1813
|
-
const includeArg = args.include ? String(args.include) : '*';
|
|
1814
|
-
if (!POWERSHELL_SAFE_INCLUDE_PATTERN.test(includeArg)) {
|
|
1815
|
-
return this.createErrorResult(ToolErrorType.INVALID_ARGS, 'Unsafe include filter for PowerShell backend', 'Use simple wildcard filters like *.ts or *.py.');
|
|
1816
|
-
}
|
|
1817
|
-
const includeFilter = `-Include "${includeArg}"`;
|
|
1818
|
-
const escapedPattern = args.pattern.replace(/'/g, "''");
|
|
1819
|
-
const cmd = `powershell -NoProfile -Command "Get-ChildItem -Path '${psPath}' -Recurse -File ${includeFilter} | Select-String -Pattern '${escapedPattern}' | ForEach-Object { $_.Path + ':' + $_.LineNumber + ':' + $_.Line }"`;
|
|
1820
|
-
try {
|
|
1821
|
-
const output = execSync(cmd, {
|
|
1822
|
-
cwd: this.cwd,
|
|
1823
|
-
encoding: 'utf-8',
|
|
1824
|
-
maxBuffer: 5 * 1024 * 1024,
|
|
1825
|
-
timeout: 60000,
|
|
1826
|
-
});
|
|
1827
|
-
const trimmed = output.trim();
|
|
1828
|
-
if (!trimmed) {
|
|
1829
|
-
return {
|
|
1830
|
-
success: true,
|
|
1831
|
-
output: 'No matches found',
|
|
1832
|
-
metadata: { searchStatus: 'search_no_matches', backend: 'select-string' },
|
|
1833
|
-
};
|
|
1834
|
-
}
|
|
1835
|
-
return {
|
|
1836
|
-
success: true,
|
|
1837
|
-
output: trimmed,
|
|
1838
|
-
metadata: { searchStatus: 'search_matches_found', backend: 'select-string' },
|
|
1839
|
-
};
|
|
1840
|
-
}
|
|
1841
|
-
catch (error) {
|
|
1842
|
-
const stderr = (error.stderr || '').toString();
|
|
1843
|
-
// Select-String returns exit code 1 for no matches when used in pipeline
|
|
1844
|
-
if (error.status === 1 && !stderr.trim()) {
|
|
1845
|
-
return {
|
|
1846
|
-
success: true,
|
|
1847
|
-
output: 'No matches found',
|
|
1848
|
-
metadata: { searchStatus: 'search_no_matches', backend: 'select-string' },
|
|
1849
|
-
};
|
|
1850
|
-
}
|
|
1851
|
-
throw error; // Let caller try next backend
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
1656
|
/**
|
|
1855
1657
|
* Pure Node.js recursive file search — reliable on all platforms, no external deps
|
|
1856
1658
|
*/
|
|
@@ -1897,6 +1699,9 @@ export class AgenticTools {
|
|
|
1897
1699
|
}
|
|
1898
1700
|
if (!entry.isFile())
|
|
1899
1701
|
continue;
|
|
1702
|
+
const relativePath = path.relative(this.cwd, fullPath).replace(/\\/g, '/');
|
|
1703
|
+
if (isSensitivePath(relativePath))
|
|
1704
|
+
continue;
|
|
1900
1705
|
// Check include pattern
|
|
1901
1706
|
if (includeGlob) {
|
|
1902
1707
|
const matches = minimatch
|
|
@@ -1915,7 +1720,6 @@ export class AgenticTools {
|
|
|
1915
1720
|
const lines = content.split('\n');
|
|
1916
1721
|
for (let i = 0; i < lines.length; i++) {
|
|
1917
1722
|
if (regex.test(lines[i])) {
|
|
1918
|
-
const relativePath = path.relative(this.cwd, fullPath).replace(/\\/g, '/');
|
|
1919
1723
|
results.push(`${relativePath}:${i + 1}:${lines[i]}`);
|
|
1920
1724
|
if (results.length >= maxResults)
|
|
1921
1725
|
break;
|
|
@@ -1957,7 +1761,7 @@ export class AgenticTools {
|
|
|
1957
1761
|
return { success: false, error: `Directory not found: ${args.path}` };
|
|
1958
1762
|
}
|
|
1959
1763
|
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
1960
|
-
const output = entries.map(e => {
|
|
1764
|
+
const output = entries.filter((entry) => !isSensitivePath(path.join(args.path || '.', entry.name))).map(e => {
|
|
1961
1765
|
const suffix = e.isDirectory() ? '/' : '';
|
|
1962
1766
|
return e.name + suffix;
|
|
1963
1767
|
}).join('\n');
|
|
@@ -1966,8 +1770,17 @@ export class AgenticTools {
|
|
|
1966
1770
|
glob(args) {
|
|
1967
1771
|
const { globSync } = require('glob');
|
|
1968
1772
|
try {
|
|
1969
|
-
const files = globSync(args.pattern, { cwd: this.cwd });
|
|
1970
|
-
|
|
1773
|
+
const files = globSync(args.pattern, { cwd: this.cwd, follow: false, nodir: true });
|
|
1774
|
+
const contained = files.filter((file) => {
|
|
1775
|
+
try {
|
|
1776
|
+
this.resolvePath(file);
|
|
1777
|
+
return !isSensitivePath(file);
|
|
1778
|
+
}
|
|
1779
|
+
catch {
|
|
1780
|
+
return false;
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
return { success: true, output: contained.join('\n') };
|
|
1971
1784
|
}
|
|
1972
1785
|
catch (error) {
|
|
1973
1786
|
return { success: false, error: error.message };
|
|
@@ -1975,10 +1788,13 @@ export class AgenticTools {
|
|
|
1975
1788
|
}
|
|
1976
1789
|
git(args) {
|
|
1977
1790
|
try {
|
|
1978
|
-
const
|
|
1791
|
+
const gitArgs = parseGitArguments(args.args);
|
|
1792
|
+
const output = execFileSync('git', hardenGitArguments(gitArgs), {
|
|
1979
1793
|
cwd: this.cwd,
|
|
1980
1794
|
encoding: 'utf-8',
|
|
1981
1795
|
timeout: 60000,
|
|
1796
|
+
windowsHide: true,
|
|
1797
|
+
env: safeChildProcessEnv(safeGitEnvironment()),
|
|
1982
1798
|
});
|
|
1983
1799
|
return { success: true, output: output.trim() };
|
|
1984
1800
|
}
|
|
@@ -1997,158 +1813,39 @@ export class AgenticTools {
|
|
|
1997
1813
|
const targetPath = args.path || this.cwd;
|
|
1998
1814
|
const description = args.description || '';
|
|
1999
1815
|
try {
|
|
2000
|
-
//
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
return { success: false, error: 'No auth token found. Run vigthoria login first.' };
|
|
2019
|
-
}
|
|
2020
|
-
// Build files array
|
|
2021
|
-
let filesArray = [];
|
|
2022
|
-
if (args.files) {
|
|
2023
|
-
// AI passed files directly
|
|
2024
|
-
let filesInput;
|
|
2025
|
-
if (typeof args.files === 'string') {
|
|
2026
|
-
try {
|
|
2027
|
-
filesInput = JSON.parse(args.files);
|
|
2028
|
-
}
|
|
2029
|
-
catch {
|
|
2030
|
-
filesInput = args.files;
|
|
2031
|
-
}
|
|
2032
|
-
}
|
|
2033
|
-
else {
|
|
2034
|
-
filesInput = args.files;
|
|
2035
|
-
}
|
|
2036
|
-
if (Array.isArray(filesInput)) {
|
|
2037
|
-
// Already in [{path, content}] format
|
|
2038
|
-
filesArray = filesInput.map((f) => ({ path: f.path || f.name || 'file', content: String(f.content || '') }));
|
|
2039
|
-
}
|
|
2040
|
-
else if (typeof filesInput === 'object' && filesInput !== null) {
|
|
2041
|
-
// {filename: content} format - convert
|
|
2042
|
-
for (const [filePath, content] of Object.entries(filesInput)) {
|
|
2043
|
-
filesArray.push({ path: filePath, content: String(content) });
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
// If no files from AI, read from disk
|
|
2048
|
-
if (filesArray.length === 0 && fs.existsSync(targetPath)) {
|
|
2049
|
-
const readDir = (dir, base) => {
|
|
2050
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
2051
|
-
for (const entry of entries) {
|
|
2052
|
-
const fullPath = path.join(dir, entry.name);
|
|
2053
|
-
const relPath = path.relative(base, fullPath);
|
|
2054
|
-
// Skip hidden dirs, node_modules, common non-essential dirs
|
|
2055
|
-
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '__pycache__')
|
|
2056
|
-
continue;
|
|
2057
|
-
if (entry.isDirectory()) {
|
|
2058
|
-
readDir(fullPath, base);
|
|
2059
|
-
}
|
|
2060
|
-
else if (entry.isFile()) {
|
|
2061
|
-
try {
|
|
2062
|
-
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
2063
|
-
filesArray.push({ path: relPath, content });
|
|
2064
|
-
}
|
|
2065
|
-
catch { /* skip binary/unreadable files */ }
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2068
|
-
};
|
|
2069
|
-
const stat = fs.statSync(targetPath);
|
|
2070
|
-
if (stat.isDirectory()) {
|
|
2071
|
-
readDir(targetPath, targetPath);
|
|
2072
|
-
}
|
|
2073
|
-
else {
|
|
2074
|
-
filesArray.push({ path: path.basename(targetPath), content: fs.readFileSync(targetPath, 'utf-8') });
|
|
1816
|
+
// Every repository action invokes the canonical CLI route through an
|
|
1817
|
+
// argument vector. This prevents the agent tool from developing a second
|
|
1818
|
+
// credential, containment, mutation-retry, and payload policy.
|
|
1819
|
+
// Project,
|
|
1820
|
+
// user, permission, and destination values never enter a shell string.
|
|
1821
|
+
let cliArgs;
|
|
1822
|
+
const validRepoIdentifier = (value) => /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
|
|
1823
|
+
if (project && !validRepoIdentifier(project)) {
|
|
1824
|
+
return { success: false, error: 'Project name contains unsupported characters' };
|
|
1825
|
+
}
|
|
1826
|
+
switch (action) {
|
|
1827
|
+
case 'push':
|
|
1828
|
+
if (!project) {
|
|
1829
|
+
return {
|
|
1830
|
+
success: false,
|
|
1831
|
+
error: 'Project name is required for push',
|
|
1832
|
+
suggestion: 'Provide a project name, e.g., repo action=push project=my-project',
|
|
1833
|
+
};
|
|
2075
1834
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
projectName: project,
|
|
2083
|
-
description: description || `${project} - pushed via Vigthoria AI`,
|
|
2084
|
-
visibility,
|
|
2085
|
-
files: filesArray,
|
|
2086
|
-
commitMessage: `Push from Vigthoria AI: ${filesArray.length} file(s)`
|
|
2087
|
-
});
|
|
2088
|
-
const headers = {
|
|
2089
|
-
'Authorization': `Bearer ${authToken}`,
|
|
2090
|
-
'Content-Type': 'application/json'
|
|
2091
|
-
};
|
|
2092
|
-
// Try proxy first; direct Community server is only reachable when running on the
|
|
2093
|
-
// Vigthoria backend itself (isServerRuntime), never expose this fallback to remote users.
|
|
2094
|
-
const pushUrls = [`${apiBase}/api/community-repo/push`];
|
|
2095
|
-
if (isServerRuntime()) {
|
|
2096
|
-
const directHost = ['local', 'host'].join('');
|
|
2097
|
-
pushUrls.push(`http://${directHost}:9000/api/repo/push`);
|
|
2098
|
-
}
|
|
2099
|
-
let result;
|
|
2100
|
-
let lastError = '';
|
|
2101
|
-
for (const pushUrl of pushUrls) {
|
|
2102
|
-
try {
|
|
2103
|
-
const response = await fetch(pushUrl, { method: 'POST', headers, body });
|
|
2104
|
-
result = await response.json();
|
|
2105
|
-
if (response.ok && result.success)
|
|
2106
|
-
break;
|
|
2107
|
-
if (result.code === 'REPOSITORY_SECURITY_REVIEW') {
|
|
2108
|
-
return {
|
|
2109
|
-
success: false,
|
|
2110
|
-
error: result.error || 'Repository security review required',
|
|
2111
|
-
metadata: {
|
|
2112
|
-
code: result.code,
|
|
2113
|
-
review: {
|
|
2114
|
-
reviewId: result.reviewId,
|
|
2115
|
-
status: result.status,
|
|
2116
|
-
riskScore: result.riskScore,
|
|
2117
|
-
findings: result.findings || [],
|
|
2118
|
-
actionItems: result.actionItems || [],
|
|
2119
|
-
retryCommand: result.retryCommand,
|
|
2120
|
-
},
|
|
2121
|
-
},
|
|
2122
|
-
suggestion: `Inspect with "vigthoria repo review ${result.reviewId}" and retry corrected content with "${result.retryCommand || `vigthoria repo push --retry ${result.reviewId}`}".`,
|
|
2123
|
-
};
|
|
2124
|
-
}
|
|
2125
|
-
lastError = result.error || result.message || `Status ${response.status}`;
|
|
2126
|
-
result = null;
|
|
1835
|
+
if (args.files) {
|
|
1836
|
+
return {
|
|
1837
|
+
success: false,
|
|
1838
|
+
error: 'Inline repository payloads are prohibited. Write approved files inside the workspace before pushing.',
|
|
1839
|
+
suggestion: 'Use write_file for each approved workspace file, then call repo action=push with path.',
|
|
1840
|
+
};
|
|
2127
1841
|
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
result = null;
|
|
1842
|
+
if (!['private', 'restricted', 'public'].includes(visibility)) {
|
|
1843
|
+
return { success: false, error: 'Repository visibility is invalid' };
|
|
2131
1844
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
error: lastError || 'Push failed on all endpoints',
|
|
2137
|
-
suggestion: 'Check your authentication and try again. Run vigthoria login to refresh your token.'
|
|
2138
|
-
};
|
|
2139
|
-
}
|
|
2140
|
-
return {
|
|
2141
|
-
success: true,
|
|
2142
|
-
output: `Successfully pushed "${project}" to Vigthoria Community!\n` +
|
|
2143
|
-
`Action: ${result.action || 'created'}\n` +
|
|
2144
|
-
`Files: ${result.filesWritten || filesArray.length}\n` +
|
|
2145
|
-
`URL: ${result.url || `https://community.vigthoria.io/showcase/${result.projectId}`}`,
|
|
2146
|
-
metadata: { action: 'push', project, filesWritten: result.filesWritten || filesArray.length, url: result.url }
|
|
2147
|
-
};
|
|
2148
|
-
}
|
|
2149
|
-
// All other actions use CLI commands
|
|
2150
|
-
let command;
|
|
2151
|
-
switch (action) {
|
|
1845
|
+
cliArgs = ['repo', 'push', this.resolvePath(targetPath), '--name', project, '--visibility', visibility, '--yes'];
|
|
1846
|
+
if (description)
|
|
1847
|
+
cliArgs.push('--description', description);
|
|
1848
|
+
break;
|
|
2152
1849
|
case 'pull':
|
|
2153
1850
|
if (!project) {
|
|
2154
1851
|
return {
|
|
@@ -2157,10 +1854,10 @@ export class AgenticTools {
|
|
|
2157
1854
|
suggestion: 'Provide a project name, e.g., repo action=pull project=my-project'
|
|
2158
1855
|
};
|
|
2159
1856
|
}
|
|
2160
|
-
|
|
1857
|
+
cliArgs = ['repo', 'pull', project];
|
|
2161
1858
|
break;
|
|
2162
1859
|
case 'list':
|
|
2163
|
-
|
|
1860
|
+
cliArgs = ['repo', 'list'];
|
|
2164
1861
|
break;
|
|
2165
1862
|
case 'status':
|
|
2166
1863
|
if (!project) {
|
|
@@ -2170,7 +1867,7 @@ export class AgenticTools {
|
|
|
2170
1867
|
suggestion: 'Provide a project name, e.g., repo action=status project=my-project'
|
|
2171
1868
|
};
|
|
2172
1869
|
}
|
|
2173
|
-
|
|
1870
|
+
cliArgs = ['repo', 'status', project];
|
|
2174
1871
|
break;
|
|
2175
1872
|
case 'share':
|
|
2176
1873
|
if (!project || !args.username) {
|
|
@@ -2181,7 +1878,10 @@ export class AgenticTools {
|
|
|
2181
1878
|
};
|
|
2182
1879
|
}
|
|
2183
1880
|
const permission = args.permission || 'read';
|
|
2184
|
-
|
|
1881
|
+
if (!validRepoIdentifier(args.username) || !['read', 'write', 'admin'].includes(permission)) {
|
|
1882
|
+
return { success: false, error: 'Repository username or permission is invalid' };
|
|
1883
|
+
}
|
|
1884
|
+
cliArgs = ['repo', 'share', project, args.username, '--permission', permission];
|
|
2185
1885
|
break;
|
|
2186
1886
|
case 'delete':
|
|
2187
1887
|
if (!project) {
|
|
@@ -2191,7 +1891,7 @@ export class AgenticTools {
|
|
|
2191
1891
|
suggestion: 'Provide a project name, e.g., repo action=delete project=my-project'
|
|
2192
1892
|
};
|
|
2193
1893
|
}
|
|
2194
|
-
|
|
1894
|
+
cliArgs = ['repo', 'delete', project, '--force'];
|
|
2195
1895
|
break;
|
|
2196
1896
|
case 'clone':
|
|
2197
1897
|
if (!project) {
|
|
@@ -2201,7 +1901,7 @@ export class AgenticTools {
|
|
|
2201
1901
|
suggestion: 'Provide a project name, e.g., repo action=clone project=my-project path=/path/to/target'
|
|
2202
1902
|
};
|
|
2203
1903
|
}
|
|
2204
|
-
|
|
1904
|
+
cliArgs = ['repo', 'clone', project, this.resolvePath(targetPath)];
|
|
2205
1905
|
break;
|
|
2206
1906
|
default:
|
|
2207
1907
|
return {
|
|
@@ -2210,11 +1910,12 @@ export class AgenticTools {
|
|
|
2210
1910
|
suggestion: 'Available actions: push, pull, list, status, share, delete, clone'
|
|
2211
1911
|
};
|
|
2212
1912
|
}
|
|
2213
|
-
const
|
|
1913
|
+
const cliEntry = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'index.js');
|
|
1914
|
+
const output = execFileSync(process.execPath, [cliEntry, ...cliArgs], {
|
|
2214
1915
|
cwd: this.cwd,
|
|
2215
1916
|
encoding: 'utf-8',
|
|
2216
1917
|
timeout: 120000,
|
|
2217
|
-
env:
|
|
1918
|
+
env: safeChildProcessEnv(process.env, { FORCE_COLOR: '0' })
|
|
2218
1919
|
});
|
|
2219
1920
|
return {
|
|
2220
1921
|
success: true,
|
|
@@ -2508,25 +2209,26 @@ export class AgenticTools {
|
|
|
2508
2209
|
}
|
|
2509
2210
|
}
|
|
2510
2211
|
try {
|
|
2511
|
-
const
|
|
2512
|
-
const
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2212
|
+
const processRequest = parseSupportedProcess(command);
|
|
2213
|
+
const remoteExecutable = path.basename(processRequest.executable);
|
|
2214
|
+
const sshArgs = [
|
|
2215
|
+
'-o', 'BatchMode=yes',
|
|
2216
|
+
'-o', 'StrictHostKeyChecking=accept-new',
|
|
2217
|
+
'-o', 'ConnectTimeout=10',
|
|
2218
|
+
normalizedHost,
|
|
2219
|
+
'--',
|
|
2220
|
+
remoteExecutable,
|
|
2221
|
+
...processRequest.args,
|
|
2222
|
+
];
|
|
2223
|
+
assertProcessNetworkAllowed('ssh', sshArgs);
|
|
2224
|
+
const execOptions = {
|
|
2225
|
+
encoding: 'utf8',
|
|
2516
2226
|
timeout,
|
|
2517
2227
|
maxBuffer: 10 * 1024 * 1024,
|
|
2228
|
+
windowsHide: true,
|
|
2229
|
+
env: safeChildProcessEnv(),
|
|
2518
2230
|
};
|
|
2519
|
-
|
|
2520
|
-
// On Windows, use the ssh command from OpenSSH
|
|
2521
|
-
sshCommand = `ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 ${host} "${command.replace(/"/g, '\\"')}"`;
|
|
2522
|
-
execOptions.shell = true;
|
|
2523
|
-
}
|
|
2524
|
-
else {
|
|
2525
|
-
// On Unix-like systems
|
|
2526
|
-
sshCommand = `ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 ${host} '${command.replace(/'/g, "'\\''")}'`;
|
|
2527
|
-
execOptions.shell = '/bin/sh';
|
|
2528
|
-
}
|
|
2529
|
-
const output = execSync(sshCommand, execOptions);
|
|
2231
|
+
const output = execFileSync('ssh', sshArgs, execOptions);
|
|
2530
2232
|
return {
|
|
2531
2233
|
success: true,
|
|
2532
2234
|
output: output.trim(),
|
|
@@ -2610,23 +2312,25 @@ export class AgenticTools {
|
|
|
2610
2312
|
else {
|
|
2611
2313
|
resolvedPath = path.normalize(path.join(this.cwd, safeInput));
|
|
2612
2314
|
}
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
// Return the sanitized relative path within workspace
|
|
2618
|
-
const basename = path.basename(stripWorkspacePrefix(normalizedInput) || normalizedInput);
|
|
2619
|
-
return path.join(this.cwd, basename);
|
|
2620
|
-
}
|
|
2621
|
-
return resolvedPath;
|
|
2315
|
+
return assertContainedAbsolutePath(workspaceRoot, resolvedPath, {
|
|
2316
|
+
allowMissing: true,
|
|
2317
|
+
allowRoot: true,
|
|
2318
|
+
});
|
|
2622
2319
|
}
|
|
2623
2320
|
/**
|
|
2624
2321
|
* Check if a path is within the allowed workspace
|
|
2625
2322
|
*/
|
|
2626
2323
|
isPathWithinWorkspace(p) {
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2324
|
+
try {
|
|
2325
|
+
assertContainedAbsolutePath(this.cwd, path.isAbsolute(p) ? p : path.join(this.cwd, p), {
|
|
2326
|
+
allowMissing: true,
|
|
2327
|
+
allowRoot: true,
|
|
2328
|
+
});
|
|
2329
|
+
return true;
|
|
2330
|
+
}
|
|
2331
|
+
catch {
|
|
2332
|
+
return false;
|
|
2333
|
+
}
|
|
2630
2334
|
}
|
|
2631
2335
|
// ═══════════════════════════════════════════════════════════════
|
|
2632
2336
|
// TASK (Sub-Agent) Tool
|
|
@@ -2660,10 +2364,10 @@ export class AgenticTools {
|
|
|
2660
2364
|
try {
|
|
2661
2365
|
for (let turn = 0; turn < maxSubTurns; turn++) {
|
|
2662
2366
|
// Import api dynamically to avoid circular dependency
|
|
2663
|
-
const {
|
|
2367
|
+
const { createAPIClient } = await import('./api-client-factory.js');
|
|
2664
2368
|
const { Config } = await import('./config.js');
|
|
2665
2369
|
const config = new Config();
|
|
2666
|
-
const api =
|
|
2370
|
+
const api = createAPIClient(config, this.logger);
|
|
2667
2371
|
const response = await api.chat(messages, 'code');
|
|
2668
2372
|
const assistantMessage = response.message || '';
|
|
2669
2373
|
messages.push({ role: 'assistant', content: assistantMessage });
|
|
@@ -2822,6 +2526,9 @@ export class AgenticTools {
|
|
|
2822
2526
|
if (ignorePatterns.includes(entry.name) || entry.name.startsWith('.'))
|
|
2823
2527
|
continue;
|
|
2824
2528
|
const fullPath = path.join(currentDir, entry.name);
|
|
2529
|
+
const relativePath = path.relative(this.cwd, fullPath).replace(/\\/g, '/');
|
|
2530
|
+
if (isSensitivePath(relativePath))
|
|
2531
|
+
continue;
|
|
2825
2532
|
if (entry.isDirectory()) {
|
|
2826
2533
|
walk(fullPath, depth + 1);
|
|
2827
2534
|
}
|
|
@@ -2830,13 +2537,15 @@ export class AgenticTools {
|
|
|
2830
2537
|
const basename = entry.name;
|
|
2831
2538
|
// Simple glob matching for extension patterns like *.ts, *.js
|
|
2832
2539
|
const globPattern = pattern.replace(/\*\*\//g, '(.*/)?').replace(/\*/g, '[^/]*').replace(/\?/g, '.');
|
|
2833
|
-
if (new RegExp(globPattern, 'i').test(basename)
|
|
2834
|
-
|
|
2835
|
-
}
|
|
2540
|
+
if (!new RegExp(globPattern, 'i').test(basename) && !fullPath.includes(pattern.replace(/\*/g, '')))
|
|
2541
|
+
continue;
|
|
2836
2542
|
}
|
|
2837
|
-
|
|
2838
|
-
|
|
2543
|
+
try {
|
|
2544
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
2545
|
+
if (!containsHighConfidenceSecret(content))
|
|
2546
|
+
files.push(fullPath);
|
|
2839
2547
|
}
|
|
2548
|
+
catch { /* unreadable files are excluded from agent context */ }
|
|
2840
2549
|
}
|
|
2841
2550
|
}
|
|
2842
2551
|
};
|
|
@@ -2913,12 +2622,12 @@ export class AgenticTools {
|
|
|
2913
2622
|
if (includePattern)
|
|
2914
2623
|
rgArgs.push('-g', includePattern);
|
|
2915
2624
|
rgArgs.push('--', query, this.cwd);
|
|
2916
|
-
const
|
|
2917
|
-
const quote = (s) => isWin ? `"${s}"` : `'${s}'`;
|
|
2918
|
-
const rgOutput = execSync(`rg ${rgArgs.map(a => quote(a)).join(' ')}`, {
|
|
2625
|
+
const rgOutput = execFileSync('rg', rgArgs, {
|
|
2919
2626
|
encoding: 'utf-8',
|
|
2920
2627
|
timeout: 15000,
|
|
2921
2628
|
maxBuffer: 5 * 1024 * 1024,
|
|
2629
|
+
windowsHide: true,
|
|
2630
|
+
env: safeChildProcessEnv(),
|
|
2922
2631
|
}).trim();
|
|
2923
2632
|
for (const line of rgOutput.split('\n').slice(0, maxResults - results.length)) {
|
|
2924
2633
|
if (!line.trim())
|
|
@@ -3032,7 +2741,6 @@ export class AgenticTools {
|
|
|
3032
2741
|
.replace(/\t/g, '\\t'); // Escape tabs
|
|
3033
2742
|
// Escape literal newlines inside strings (but not \n which is already escaped)
|
|
3034
2743
|
// We need to be careful - only escape newlines that are inside quoted strings
|
|
3035
|
-
const parts = [];
|
|
3036
2744
|
let inString = false;
|
|
3037
2745
|
let currentPart = '';
|
|
3038
2746
|
for (let i = 0; i < fixed.length; i++) {
|