matex-cli 1.2.81 → 1.2.84
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/chaos.js +3 -8
- package/dist/prompts/chaos-prompts.js +26 -21
- package/dist/session/agent-session.js +38 -9
- package/dist/utils/agent-orchestrator.js +7 -0
- package/dist/utils/mcp-server.js +6 -0
- package/dist/utils/tui.js +84 -7
- package/package.json +7 -1
- package/.agents/skills/mcp-server-dev/SKILL.md +0 -60
- package/.agents/skills/mcp-server-dev/examples/basic-ts-server/package.json +0 -20
- package/.agents/skills/mcp-server-dev/examples/basic-ts-server/src/index.ts +0 -66
- package/.agents/skills/mcp-server-dev/resources/best_practices.md +0 -20
- package/.agents/skills/pptx-presentation-builder/SKILL.md +0 -338
- package/.agents/workflows/deploy.md +0 -27
- package/dist/api/client.d.ts +0 -40
- package/dist/api/client.d.ts.map +0 -1
- package/dist/api/client.js.map +0 -1
- package/dist/commands/ask.d.ts +0 -3
- package/dist/commands/ask.d.ts.map +0 -1
- package/dist/commands/ask.js.map +0 -1
- package/dist/commands/augov.d.ts +0 -3
- package/dist/commands/augov.d.ts.map +0 -1
- package/dist/commands/augov.js.map +0 -1
- package/dist/commands/bro.d.ts +0 -4
- package/dist/commands/bro.d.ts.map +0 -1
- package/dist/commands/bro.js.map +0 -1
- package/dist/commands/chaos.d.ts +0 -3
- package/dist/commands/chaos.d.ts.map +0 -1
- package/dist/commands/chaos.js.map +0 -1
- package/dist/commands/chat.d.ts +0 -3
- package/dist/commands/chat.d.ts.map +0 -1
- package/dist/commands/chat.js.map +0 -1
- package/dist/commands/code.d.ts +0 -3
- package/dist/commands/code.d.ts.map +0 -1
- package/dist/commands/code.js.map +0 -1
- package/dist/commands/config.d.ts +0 -3
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/dev.d.ts +0 -3
- package/dist/commands/dev.d.ts.map +0 -1
- package/dist/commands/dev.js.map +0 -1
- package/dist/commands/help.d.ts +0 -3
- package/dist/commands/help.d.ts.map +0 -1
- package/dist/commands/help.js.map +0 -1
- package/dist/commands/login.d.ts +0 -3
- package/dist/commands/login.d.ts.map +0 -1
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/models.d.ts +0 -3
- package/dist/commands/models.d.ts.map +0 -1
- package/dist/commands/models.js.map +0 -1
- package/dist/commands/student.d.ts +0 -3
- package/dist/commands/student.d.ts.map +0 -1
- package/dist/commands/student.js.map +0 -1
- package/dist/commands/study.d.ts +0 -3
- package/dist/commands/study.d.ts.map +0 -1
- package/dist/commands/study.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/prompts/banter-augov.d.ts +0 -2
- package/dist/prompts/banter-augov.d.ts.map +0 -1
- package/dist/prompts/banter-augov.js.map +0 -1
- package/dist/prompts/banter.d.ts +0 -6
- package/dist/prompts/banter.d.ts.map +0 -1
- package/dist/prompts/banter.js.map +0 -1
- package/dist/prompts/chaos-prompts.d.ts +0 -3
- package/dist/prompts/chaos-prompts.d.ts.map +0 -1
- package/dist/prompts/chaos-prompts.js.map +0 -1
- package/dist/prompts/system-prompts.d.ts +0 -4
- package/dist/prompts/system-prompts.d.ts.map +0 -1
- package/dist/prompts/system-prompts.js.map +0 -1
- package/dist/session/agent-session.d.ts +0 -41
- package/dist/session/agent-session.d.ts.map +0 -1
- package/dist/session/agent-session.js.map +0 -1
- package/dist/utils/agent-orchestrator.d.ts +0 -44
- package/dist/utils/agent-orchestrator.d.ts.map +0 -1
- package/dist/utils/agent-orchestrator.js.map +0 -1
- package/dist/utils/augov-logger.d.ts +0 -11
- package/dist/utils/augov-logger.d.ts.map +0 -1
- package/dist/utils/augov-logger.js.map +0 -1
- package/dist/utils/augov-scrubber.d.ts +0 -23
- package/dist/utils/augov-scrubber.d.ts.map +0 -1
- package/dist/utils/augov-scrubber.js.map +0 -1
- package/dist/utils/command-executor.d.ts +0 -56
- package/dist/utils/command-executor.d.ts.map +0 -1
- package/dist/utils/command-executor.js.map +0 -1
- package/dist/utils/config.d.ts +0 -56
- package/dist/utils/config.d.ts.map +0 -1
- package/dist/utils/config.js.map +0 -1
- package/dist/utils/mcp-server.d.ts +0 -77
- package/dist/utils/mcp-server.d.ts.map +0 -1
- package/dist/utils/mcp-server.js.map +0 -1
- package/dist/utils/patcher.d.ts +0 -45
- package/dist/utils/patcher.d.ts.map +0 -1
- package/dist/utils/patcher.js.map +0 -1
- package/dist/utils/repo-mapper.d.ts +0 -32
- package/dist/utils/repo-mapper.d.ts.map +0 -1
- package/dist/utils/repo-mapper.js.map +0 -1
- package/dist/utils/spinner.d.ts +0 -15
- package/dist/utils/spinner.d.ts.map +0 -1
- package/dist/utils/spinner.js.map +0 -1
- package/dist/utils/tui.d.ts +0 -134
- package/dist/utils/tui.d.ts.map +0 -1
- package/dist/utils/tui.js.map +0 -1
- package/fix-npm-permissions.sh +0 -23
- package/publish-local.sh +0 -16
- package/skills-lock.json +0 -10
- package/src/api/client.ts +0 -197
- package/src/commands/ask.ts +0 -62
- package/src/commands/augov.ts +0 -301
- package/src/commands/bro.ts +0 -336
- package/src/commands/chaos.ts +0 -67
- package/src/commands/chat.ts +0 -61
- package/src/commands/code.ts +0 -99
- package/src/commands/config.ts +0 -78
- package/src/commands/dev.ts +0 -68
- package/src/commands/help.ts +0 -67
- package/src/commands/login.ts +0 -47
- package/src/commands/models.ts +0 -81
- package/src/commands/student.ts +0 -23
- package/src/commands/study.ts +0 -75
- package/src/index.ts +0 -284
- package/src/prompts/banter-augov.ts +0 -17
- package/src/prompts/banter.ts +0 -101
- package/src/prompts/chaos-prompts.ts +0 -48
- package/src/prompts/system-prompts.ts +0 -145
- package/src/session/agent-session.ts +0 -428
- package/src/utils/agent-orchestrator.ts +0 -264
- package/src/utils/augov-logger.ts +0 -34
- package/src/utils/augov-scrubber.ts +0 -67
- package/src/utils/command-executor.ts +0 -529
- package/src/utils/config.ts +0 -124
- package/src/utils/mcp-server.ts +0 -388
- package/src/utils/patcher.ts +0 -229
- package/src/utils/repo-mapper.ts +0 -198
- package/src/utils/spinner.ts +0 -66
- package/src/utils/tui.ts +0 -749
- package/test-chaos-container.js +0 -16
- package/test-chaos-fix.js +0 -18
- package/test-config.ts +0 -2
- package/test-ui-output.js +0 -16
- package/tsconfig.json +0 -27
- package/vertex_ai_agent.py +0 -52
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* IRAP-Compliant Audit Logger
|
|
6
|
-
* The Australian Government requires strict logging of AI tool usage.
|
|
7
|
-
* This logger records prompts and actions locally so security teams can audit them.
|
|
8
|
-
*/
|
|
9
|
-
export class GovAuditLogger {
|
|
10
|
-
private logFile: string;
|
|
11
|
-
|
|
12
|
-
constructor(baseDir: string = process.cwd()) {
|
|
13
|
-
const logDir = path.join(baseDir, '.matex_audit');
|
|
14
|
-
if (!fs.existsSync(logDir)) {
|
|
15
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Create an auditable log per day
|
|
19
|
-
const today = new Date().toISOString().split('T')[0];
|
|
20
|
-
this.logFile = path.join(logDir, `augov_audit_${today}.log`);
|
|
21
|
-
|
|
22
|
-
if (!fs.existsSync(this.logFile)) {
|
|
23
|
-
fs.writeFileSync(this.logFile, 'TIMESTAMP | ROLE | ACTION | CONTENT\n');
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
public logInteraction(role: 'USER' | 'AI_SWARM' | 'SYSTEM', action: string, content: string) {
|
|
28
|
-
const timestamp = new Date().toISOString();
|
|
29
|
-
const cleanContent = content.replace(/\n/g, ' ').substring(0, 500); // truncate for CSV safety, full logs can be large
|
|
30
|
-
const logEntry = `${timestamp} | ${role} | ${action} | ${cleanContent}\n`;
|
|
31
|
-
|
|
32
|
-
fs.appendFileSync(this.logFile, logEntry);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Citizen Privacy Shield
|
|
5
|
-
* A local redaction engine that scrubs Australian PII (Personally Identifiable Information)
|
|
6
|
-
* BEFORE it ever leaves the user's computer to hit an AI API.
|
|
7
|
-
*/
|
|
8
|
-
export class GovPrivacyScrubber {
|
|
9
|
-
|
|
10
|
-
// Regex patterns for Australian specific sensitive data
|
|
11
|
-
private static patterns = [
|
|
12
|
-
// Australian Tax File Number (TFN) - 9 digits
|
|
13
|
-
{ regex: /\b\d{3}[- ]?\d{3}[- ]?\d{3}\b/g, replacement: '[REDACTED_TFN]' },
|
|
14
|
-
// Medicare Number - 10 digits
|
|
15
|
-
{ regex: /\b[2-6]\d{9}\b/g, replacement: '[REDACTED_MEDICARE]' },
|
|
16
|
-
// Australian Phone Numbers (Mobile & Landline)
|
|
17
|
-
{ regex: /(?:\+?61|0)[2-478](?:[ -]?[0-9]){8}\b/g, replacement: '[REDACTED_PHONE]' },
|
|
18
|
-
// Email Addresses
|
|
19
|
-
{ regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, replacement: '[REDACTED_EMAIL]' },
|
|
20
|
-
// Credit Card Numbers
|
|
21
|
-
{ regex: /\b(?:\d[ -]*?){13,16}\b/g, replacement: '[REDACTED_CREDIT_CARD]' },
|
|
22
|
-
|
|
23
|
-
// --- FOI (Freedom of Information) Specific Redactions ---
|
|
24
|
-
|
|
25
|
-
// Cabinet in Confidence Markers
|
|
26
|
-
{ regex: /Cabinet in Confidence/ig, replacement: '[REDACTED_CABINET_MATERIAL]' },
|
|
27
|
-
{ regex: /National Cabinet/ig, replacement: '[REDACTED_NATIONAL_CABINET_MATERIAL]' },
|
|
28
|
-
// Highly Classified Clearance Words
|
|
29
|
-
{ regex: /TOP SECRET/ig, replacement: '[REDACTED_CLASSIFICATION]' },
|
|
30
|
-
// Military Base Locations (Example subset for ADF)
|
|
31
|
-
{ regex: /HMAS Stirling|RAAF Base Tindal|Pine Gap|Russell Offices/ig, replacement: '[REDACTED_DEFENCE_ASSET]' },
|
|
32
|
-
// Diplomatic Identifiers
|
|
33
|
-
{ regex: /Ambassador [A-Z][a-z]+/ig, replacement: '[REDACTED_DIPLOMAT]' },
|
|
34
|
-
{ regex: /Five Eyes|AUKUS/ig, replacement: '[REDACTED_INTELLIGENCE_ALLIANCE]' }
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Scrubs sensitive data from a string.
|
|
39
|
-
* @param input Raw string from user or file
|
|
40
|
-
* @returns Scrubbed string safe for AI processing
|
|
41
|
-
*/
|
|
42
|
-
public static scrub(input: string): string {
|
|
43
|
-
let scrubbed = input;
|
|
44
|
-
for (const rule of this.patterns) {
|
|
45
|
-
scrubbed = scrubbed.replace(rule.regex, rule.replacement);
|
|
46
|
-
}
|
|
47
|
-
return scrubbed;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Specialized FOI Document Scrubber
|
|
52
|
-
* Used exclusively for the `matex augov redact` command
|
|
53
|
-
*/
|
|
54
|
-
public static async redactDocumentContent(content: string): Promise<{ scrubbed: string, redactionCount: number }> {
|
|
55
|
-
let scrubbed = content;
|
|
56
|
-
let redactionCount = 0;
|
|
57
|
-
|
|
58
|
-
for (const rule of this.patterns) {
|
|
59
|
-
const matches = content.match(rule.regex);
|
|
60
|
-
if (matches) {
|
|
61
|
-
redactionCount += matches.length;
|
|
62
|
-
scrubbed = scrubbed.replace(rule.regex, chalk.bgBlack.white(rule.replacement));
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return { scrubbed, redactionCount };
|
|
66
|
-
}
|
|
67
|
-
}
|
|
@@ -1,529 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import { exec, spawn } from 'child_process';
|
|
5
|
-
import { promisify } from 'util';
|
|
6
|
-
import inquirer from 'inquirer';
|
|
7
|
-
import { TUI } from './tui';
|
|
8
|
-
import { AgentOrchestrator } from './agent-orchestrator';
|
|
9
|
-
import { Patcher } from './patcher';
|
|
10
|
-
|
|
11
|
-
const execAsync = promisify(exec);
|
|
12
|
-
|
|
13
|
-
export interface CommandBlock {
|
|
14
|
-
language: string;
|
|
15
|
-
code: string;
|
|
16
|
-
dangerous: boolean;
|
|
17
|
-
description?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Extract executable commands from AI response
|
|
22
|
-
*/
|
|
23
|
-
export function extractCommands(response: string): CommandBlock[] {
|
|
24
|
-
const commands: CommandBlock[] = [];
|
|
25
|
-
|
|
26
|
-
// Match code blocks with language tags (case-insensitive)
|
|
27
|
-
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/gi;
|
|
28
|
-
let match;
|
|
29
|
-
|
|
30
|
-
while ((match = codeBlockRegex.exec(response)) !== null) {
|
|
31
|
-
const language = match[1] || 'bash';
|
|
32
|
-
const code = match[2].trim();
|
|
33
|
-
|
|
34
|
-
// 🛡️ HEURISTIC: Reject directory trees formatted as "commands"
|
|
35
|
-
const isTree = /├──|└──|│\s+├──/.test(code);
|
|
36
|
-
if (isTree) continue;
|
|
37
|
-
|
|
38
|
-
// Only extract shell commands
|
|
39
|
-
if (['bash', 'sh', 'zsh', 'shell', 'powershell', 'cmd'].includes(language.toLowerCase())) {
|
|
40
|
-
// 🛡️ DIALOGUE FILTER: Strip lines that look like agent banter [Ajay Vai]: etc.
|
|
41
|
-
const lines = code.split('\n');
|
|
42
|
-
const filteredLines = lines.filter(line => {
|
|
43
|
-
const trimmed = line.trim();
|
|
44
|
-
// If it starts with [Agent Name] or typical agent markers, it's NOT a command
|
|
45
|
-
const isAgent = /^(?:\[\**\s*|\b)(Ajay|Sunil|Sandip|Bishal|Narayan|Big Bro)\s*(?:Vai|Dai)?\s*\**\]?[:\s]*/i.test(trimmed);
|
|
46
|
-
return !isAgent;
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const filteredCode = filteredLines.join('\n').trim();
|
|
50
|
-
if (filteredCode) {
|
|
51
|
-
commands.push({
|
|
52
|
-
language,
|
|
53
|
-
code: filteredCode,
|
|
54
|
-
dangerous: isDangerousCommand(filteredCode)
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return commands;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Check if command is potentially dangerous
|
|
65
|
-
*/
|
|
66
|
-
function isDangerousCommand(command: string): boolean {
|
|
67
|
-
const dangerousPatterns = [
|
|
68
|
-
/rm\s+-rf\s+\//, // rm -rf /
|
|
69
|
-
/rm\s+-rf\s+~/, // rm -rf ~
|
|
70
|
-
/rm\s+-rf\s+\*/, // rm -rf *
|
|
71
|
-
/:\(\)\{.*\}/, // Fork bomb
|
|
72
|
-
/dd\s+if=.*of=\/dev/, // Disk operations
|
|
73
|
-
/mkfs/, // Format filesystem
|
|
74
|
-
/fdisk/, // Partition operations
|
|
75
|
-
/>\s*\/dev\/sd/, // Write to disk
|
|
76
|
-
/chmod\s+-R\s+777/, // Dangerous permissions
|
|
77
|
-
/curl.*\|\s*bash/, // Pipe to bash
|
|
78
|
-
/wget.*\|\s*sh/, // Pipe to shell
|
|
79
|
-
/sudo\s+rm/, // Sudo remove
|
|
80
|
-
/format\s+[A-Z]:/, // Windows format
|
|
81
|
-
/del\s+\/[FS]/, // Windows delete
|
|
82
|
-
];
|
|
83
|
-
|
|
84
|
-
return dangerousPatterns.some(pattern => pattern.test(command));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Check if command is safe to auto-execute
|
|
89
|
-
*/
|
|
90
|
-
export function isSafeAutoCommand(command: string): boolean {
|
|
91
|
-
const safePrefixes = [
|
|
92
|
-
// File Discovery & Read
|
|
93
|
-
'cat ', 'ls ', 'grep ', 'find ', 'pwd', 'echo ', 'read ', 'type ', 'dir',
|
|
94
|
-
// File Operations
|
|
95
|
-
'mkdir ', 'touch ', 'cp ', 'mv ', 'rm ', 'del ', 'copy ', 'move ',
|
|
96
|
-
// Dev Tools
|
|
97
|
-
'npm ', 'npx ', 'git ', 'node ', 'python ', 'python3 ', 'pip ', 'pip3 ',
|
|
98
|
-
// Cloud & Mobile
|
|
99
|
-
'firebase ', 'gcloud ', 'docker ', 'flutter ', 'react-native ',
|
|
100
|
-
// Build & Package
|
|
101
|
-
'cd ', 'tsc ', 'vite ', 'next ', 'cargo ', 'go ', 'swift '
|
|
102
|
-
];
|
|
103
|
-
const trimmed = command.trim();
|
|
104
|
-
return safePrefixes.some(prefix => trimmed.startsWith(prefix));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Ask user for permission to execute command
|
|
109
|
-
*/
|
|
110
|
-
export async function askPermission(command: CommandBlock): Promise<boolean> {
|
|
111
|
-
// Clean Codex-style Prompt with Multi-Agent Iconography
|
|
112
|
-
console.log();
|
|
113
|
-
|
|
114
|
-
// Remove auto-execution logic to prevent infinite "run forever" loops
|
|
115
|
-
// The user should always be prompted or have the option to safely step through.
|
|
116
|
-
|
|
117
|
-
console.log(chalk.yellow('🛡️ SyntaxGuard ') + chalk.white('Analysis:'));
|
|
118
|
-
console.log(chalk.gray('> ') + chalk.white(command.code));
|
|
119
|
-
console.log();
|
|
120
|
-
|
|
121
|
-
if (command.dangerous) {
|
|
122
|
-
console.log(chalk.red('🚨 WARNING: Dangerous command'));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const { execute } = await inquirer.prompt([{
|
|
126
|
-
type: 'confirm',
|
|
127
|
-
name: 'execute',
|
|
128
|
-
message: command.dangerous ?
|
|
129
|
-
chalk.red('Execute?') :
|
|
130
|
-
chalk.gray('Execute?'),
|
|
131
|
-
default: true // Default to true for seamless flow (Codex style)
|
|
132
|
-
}]);
|
|
133
|
-
|
|
134
|
-
return execute;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Execute a shell command with real-time streaming and interruption support
|
|
139
|
-
*/
|
|
140
|
-
export async function executeCommand(command: string, shell?: string, cwd?: string): Promise<{ stdout: string; stderr: string; aborted?: boolean }> {
|
|
141
|
-
return new Promise((resolve, reject) => {
|
|
142
|
-
const shellPath = shell || process.env.SHELL || '/bin/bash';
|
|
143
|
-
const child = spawn(shellPath, ['-c', command], {
|
|
144
|
-
cwd: cwd || process.cwd(),
|
|
145
|
-
env: { ...process.env, FORCE_COLOR: 'true' }
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
let stdout = '';
|
|
149
|
-
let stderr = '';
|
|
150
|
-
let isAborted = false;
|
|
151
|
-
const commandStartTime = Date.now();
|
|
152
|
-
|
|
153
|
-
// Listen for "Enter" or "Escape" to kill the command
|
|
154
|
-
const onData = (data: Buffer) => {
|
|
155
|
-
// Check for Enter (13), Newline (10), Escape (27), or Ctrl+C (3)
|
|
156
|
-
// 🏁 GRACE PERIOD: Ignore aborts in the first 200ms to prevent stray newlines
|
|
157
|
-
if (Date.now() - commandStartTime < 200) return;
|
|
158
|
-
|
|
159
|
-
if (data[0] === 13 || data[0] === 10 || data[0] === 27 || data[0] === 3) {
|
|
160
|
-
isAborted = true;
|
|
161
|
-
child.kill('SIGINT'); // Send SIGINT to gracefully terminate
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const isRaw = process.stdin.isRaw;
|
|
166
|
-
process.stdin.resume();
|
|
167
|
-
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
168
|
-
process.stdin.on('data', onData);
|
|
169
|
-
|
|
170
|
-
child.stdout.on('data', (data: Buffer) => {
|
|
171
|
-
const chunk = data.toString();
|
|
172
|
-
stdout += chunk;
|
|
173
|
-
// No direct process.stdout.write here to prevent leaks
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
child.stderr.on('data', (data: Buffer) => {
|
|
177
|
-
const chunk = data.toString();
|
|
178
|
-
stderr += chunk;
|
|
179
|
-
// No direct process.stderr.write here to prevent leaks
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
child.on('close', (code: number | null) => {
|
|
183
|
-
// Restore terminal state
|
|
184
|
-
process.stdin.removeListener('data', onData);
|
|
185
|
-
if (process.stdin.setRawMode) process.stdin.setRawMode(isRaw);
|
|
186
|
-
process.stdin.pause();
|
|
187
|
-
|
|
188
|
-
if (isAborted) {
|
|
189
|
-
resolve({ stdout, stderr, aborted: true });
|
|
190
|
-
} else if (code === 0) {
|
|
191
|
-
resolve({ stdout, stderr });
|
|
192
|
-
} else {
|
|
193
|
-
resolve({ stdout, stderr }); // Resolve instead of reject to keep agent loop alive
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
child.on('error', (err: Error) => {
|
|
198
|
-
process.stdin.removeListener('data', onData);
|
|
199
|
-
if (process.stdin.setRawMode) process.stdin.setRawMode(isRaw);
|
|
200
|
-
process.stdin.pause();
|
|
201
|
-
reject(err);
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Execute commands with user permission
|
|
208
|
-
*/
|
|
209
|
-
export interface ExecutionResult {
|
|
210
|
-
success: boolean;
|
|
211
|
-
executed: boolean;
|
|
212
|
-
output?: string;
|
|
213
|
-
error?: string;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Execute commands and surgical patches with user permission
|
|
218
|
-
*/
|
|
219
|
-
export async function executeWithPermission(response: string, currentSessionCwd?: string): Promise<ExecutionResult & { newCwd?: string }> {
|
|
220
|
-
const commands = extractCommands(response);
|
|
221
|
-
const patches = Patcher.parseEditBlocks(response);
|
|
222
|
-
const files = Patcher.parseFileBlocks(response);
|
|
223
|
-
|
|
224
|
-
let activeCwd = currentSessionCwd || process.cwd();
|
|
225
|
-
let accumulatedOutput = '';
|
|
226
|
-
let accumulatedError = '';
|
|
227
|
-
|
|
228
|
-
if (commands.length === 0 && patches.length === 0 && files.length === 0) {
|
|
229
|
-
return { success: true, executed: false, newCwd: activeCwd };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// 1. Handle New File Creation
|
|
233
|
-
for (const file of files) {
|
|
234
|
-
// Ensure creation happens relative to activeCwd if path is relative
|
|
235
|
-
const originalPath = file.filePath;
|
|
236
|
-
if (!path.isAbsolute(file.filePath)) {
|
|
237
|
-
file.filePath = path.join(activeCwd, file.filePath);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
let showFull = false;
|
|
241
|
-
let decided = false;
|
|
242
|
-
|
|
243
|
-
while (!decided) {
|
|
244
|
-
const isTruncated = Patcher.showDiff(file, showFull);
|
|
245
|
-
|
|
246
|
-
const choices = [
|
|
247
|
-
{ name: '✅ Create this new file brother', value: 'create' },
|
|
248
|
-
{ name: '⏭️ Skip', value: 'skip' }
|
|
249
|
-
];
|
|
250
|
-
|
|
251
|
-
if (isTruncated && !showFull) {
|
|
252
|
-
choices.splice(1, 0, { name: '🔍 View Full Code', value: 'full' });
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const { action } = await inquirer.prompt([{
|
|
256
|
-
type: 'list',
|
|
257
|
-
name: 'action',
|
|
258
|
-
message: chalk.cyan(`Create ${originalPath}?`),
|
|
259
|
-
choices: choices
|
|
260
|
-
}]);
|
|
261
|
-
|
|
262
|
-
if (action === 'full') {
|
|
263
|
-
showFull = true;
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (action === 'create') {
|
|
268
|
-
const result = Patcher.createFile(file);
|
|
269
|
-
if (!result.success) return { success: false, executed: true, error: result.error, newCwd: activeCwd };
|
|
270
|
-
}
|
|
271
|
-
decided = true;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// 2. Handle Shell Commands
|
|
276
|
-
for (let i = 0; i < commands.length; i++) {
|
|
277
|
-
const command = commands[i];
|
|
278
|
-
|
|
279
|
-
// 🛡️ PRE-EXECUTION HALLUCINATION GUARD: Check for fake directories
|
|
280
|
-
const commandLines = command.code.split('\n');
|
|
281
|
-
let skipThisCommand = false;
|
|
282
|
-
let sanitizedCode = command.code;
|
|
283
|
-
|
|
284
|
-
for (const line of commandLines) {
|
|
285
|
-
const trimmed = line.trim();
|
|
286
|
-
// Check standalone cd commands
|
|
287
|
-
if (trimmed.startsWith('cd ')) {
|
|
288
|
-
let targetDir = trimmed.substring(3).replace(/['"]/g, '').trim();
|
|
289
|
-
const potentialCwd = path.isAbsolute(targetDir) ? targetDir : path.resolve(activeCwd, targetDir);
|
|
290
|
-
|
|
291
|
-
if (!fs.existsSync(potentialCwd) || !fs.statSync(potentialCwd).isDirectory()) {
|
|
292
|
-
console.log(chalk.yellow(`\n ⚠️ Skipped hallucinated directory: "${targetDir}"`));
|
|
293
|
-
console.log(chalk.gray(` (Does not exist at: ${potentialCwd})`));
|
|
294
|
-
console.log(chalk.gray(` Current CWD stays: ${activeCwd}\n`));
|
|
295
|
-
skipThisCommand = true;
|
|
296
|
-
break;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
// Check cd inside combined commands like: cd "fake" && npm install
|
|
300
|
-
const cdChainMatch = trimmed.match(/^cd\s+["']?([^"'&]+)["']?\s*&&/);
|
|
301
|
-
if (cdChainMatch) {
|
|
302
|
-
let targetDir = cdChainMatch[1].trim();
|
|
303
|
-
const potentialCwd = path.isAbsolute(targetDir) ? targetDir : path.resolve(activeCwd, targetDir);
|
|
304
|
-
|
|
305
|
-
if (!fs.existsSync(potentialCwd) || !fs.statSync(potentialCwd).isDirectory()) {
|
|
306
|
-
console.log(chalk.yellow(`\n ⚠️ Stripping hallucinated cd from command: "${targetDir}"`));
|
|
307
|
-
// Remove the cd part and execute the rest
|
|
308
|
-
sanitizedCode = trimmed.replace(/^cd\s+["']?[^"'&]+["']?\s*&&\s*/, '');
|
|
309
|
-
console.log(chalk.gray(` Running remaining: ${sanitizedCode}\n`));
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (skipThisCommand) continue;
|
|
315
|
-
|
|
316
|
-
// Update command code with sanitized version
|
|
317
|
-
command.code = sanitizedCode;
|
|
318
|
-
|
|
319
|
-
// 🚀 MCP INTERCEPTION: If the command matches an MCP tool directly, use MCPServer
|
|
320
|
-
const mcpMatch = command.code.trim().match(/^(web_search|read_file|write_file|list_directory|search_files|run_command|fetch_url|git_status)\s+(.*)$/);
|
|
321
|
-
|
|
322
|
-
if (mcpMatch) {
|
|
323
|
-
const toolName = mcpMatch[1];
|
|
324
|
-
const rawArgs = mcpMatch[2].trim();
|
|
325
|
-
console.log(chalk.cyan(`⚡ Intercepted MCP Tool: `) + chalk.white(`${toolName}(${rawArgs})`));
|
|
326
|
-
|
|
327
|
-
try {
|
|
328
|
-
const { MCPServer } = await import('./mcp-server');
|
|
329
|
-
const mcp = new MCPServer(activeCwd);
|
|
330
|
-
|
|
331
|
-
// Heuristic: If it's just a string, wrap it in a query/path object
|
|
332
|
-
let args: any = {};
|
|
333
|
-
if (toolName === 'web_search') args = { query: rawArgs.replace(/^['"]|['"]$/g, '') };
|
|
334
|
-
else if (toolName === 'fetch_url') args = { url: rawArgs.replace(/^['"]|['"]$/g, '') };
|
|
335
|
-
else if (['read_file', 'list_directory', 'git_status'].includes(toolName)) args = { path: rawArgs.replace(/^['"]|['"]$/g, '') };
|
|
336
|
-
else args = { command: rawArgs }; // Default fallback
|
|
337
|
-
|
|
338
|
-
const result = await mcp.execute(toolName, args);
|
|
339
|
-
|
|
340
|
-
if (result.success) {
|
|
341
|
-
accumulatedOutput += `[MCP ${toolName} Output]:\n${result.output}\n\n`;
|
|
342
|
-
AgentOrchestrator.terminal(command.code, result.output);
|
|
343
|
-
} else {
|
|
344
|
-
accumulatedError += `[MCP ${toolName} Error]: ${result.error}\n`;
|
|
345
|
-
AgentOrchestrator.terminal(command.code, undefined, result.error);
|
|
346
|
-
}
|
|
347
|
-
continue; // Move to next command
|
|
348
|
-
} catch (e: any) {
|
|
349
|
-
console.log(chalk.red(` ❌ MCP Execution Error: ${e.message}`));
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const shouldExecute = await askPermission(command);
|
|
354
|
-
|
|
355
|
-
if (shouldExecute) {
|
|
356
|
-
try {
|
|
357
|
-
// Persistent CWD logic: Trace 'cd' commands
|
|
358
|
-
const lines = command.code.split('\n');
|
|
359
|
-
for (const line of lines) {
|
|
360
|
-
const trimmed = line.trim();
|
|
361
|
-
if (trimmed.startsWith('cd ')) {
|
|
362
|
-
let targetDir = trimmed.substring(3).replace(/['"]/g, '').trim();
|
|
363
|
-
// Resolve relative to activeCwd
|
|
364
|
-
const potentialCwd = path.isAbsolute(targetDir) ? targetDir : path.resolve(activeCwd, targetDir);
|
|
365
|
-
|
|
366
|
-
if (fs.existsSync(potentialCwd) && fs.statSync(potentialCwd).isDirectory()) {
|
|
367
|
-
activeCwd = potentialCwd;
|
|
368
|
-
} else {
|
|
369
|
-
// Directory doesn't exist - skip silently instead of crashing
|
|
370
|
-
console.log(chalk.yellow(` ⚠️ Directory not found: ${targetDir} — staying in ${activeCwd}`));
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
TUI.drawLiveTerminalStart(command.code);
|
|
377
|
-
|
|
378
|
-
const shellPath = process.env.SHELL || '/bin/bash';
|
|
379
|
-
const child = spawn(shellPath, ['-c', command.code], {
|
|
380
|
-
cwd: activeCwd,
|
|
381
|
-
env: { ...process.env, FORCE_COLOR: 'true' }
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
let stdout = '';
|
|
385
|
-
let stderr = '';
|
|
386
|
-
let isAborted = false;
|
|
387
|
-
const commandStartTime = Date.now();
|
|
388
|
-
|
|
389
|
-
const onData = (data: Buffer) => {
|
|
390
|
-
if (Date.now() - commandStartTime < 500) return; // Hardened 500ms grace period
|
|
391
|
-
if (data[0] === 13 || data[0] === 10 || data[0] === 27 || data[0] === 3) {
|
|
392
|
-
isAborted = true;
|
|
393
|
-
child.kill('SIGINT');
|
|
394
|
-
}
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
const isRaw = process.stdin.isRaw;
|
|
398
|
-
process.stdin.resume();
|
|
399
|
-
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
400
|
-
process.stdin.on('data', onData);
|
|
401
|
-
|
|
402
|
-
child.stdout.on('data', (data: Buffer) => {
|
|
403
|
-
const chunk = data.toString();
|
|
404
|
-
stdout += chunk;
|
|
405
|
-
TUI.drawLiveTerminalLine(chunk);
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
child.stderr.on('data', (data: Buffer) => {
|
|
409
|
-
const chunk = data.toString();
|
|
410
|
-
stderr += chunk;
|
|
411
|
-
TUI.drawLiveTerminalLine(chunk, true);
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
const { code } = await new Promise<{ code: number | null }>(resolve => {
|
|
415
|
-
child.on('close', (code) => {
|
|
416
|
-
process.stdin.removeListener('data', onData);
|
|
417
|
-
if (process.stdin.setRawMode) process.stdin.setRawMode(isRaw);
|
|
418
|
-
process.stdin.pause();
|
|
419
|
-
TUI.drawLiveTerminalEnd();
|
|
420
|
-
resolve({ code });
|
|
421
|
-
});
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
accumulatedOutput += stdout;
|
|
425
|
-
accumulatedError += stderr;
|
|
426
|
-
|
|
427
|
-
if (isAborted) {
|
|
428
|
-
console.log(chalk.yellow(' [🛑] Command aborted by brother.\n'));
|
|
429
|
-
AgentOrchestrator.terminal(command.code, stdout, stderr + '\n(Aborted)');
|
|
430
|
-
} else if (code === 0) {
|
|
431
|
-
if (stdout || stderr) {
|
|
432
|
-
AgentOrchestrator.terminal(command.code, stdout, stderr);
|
|
433
|
-
} else {
|
|
434
|
-
AgentOrchestrator.terminal(command.code, '✓ Success (No output)');
|
|
435
|
-
}
|
|
436
|
-
} else {
|
|
437
|
-
AgentOrchestrator.terminal(command.code, stdout, stderr || `Failed with code ${code}`);
|
|
438
|
-
}
|
|
439
|
-
} catch (error: any) {
|
|
440
|
-
accumulatedError += error.message;
|
|
441
|
-
AgentOrchestrator.terminal(command.code, undefined, error.message);
|
|
442
|
-
return { success: false, executed: true, error: accumulatedError, output: accumulatedOutput, newCwd: activeCwd };
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// 3. Handle Surgical Patches
|
|
448
|
-
for (const surgicalPatch of patches) {
|
|
449
|
-
// Ensure patch happens relative to activeCwd if path is relative
|
|
450
|
-
const originalPath = surgicalPatch.filePath;
|
|
451
|
-
if (!path.isAbsolute(surgicalPatch.filePath)) {
|
|
452
|
-
surgicalPatch.filePath = path.join(activeCwd, surgicalPatch.filePath);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
let showFull = false;
|
|
456
|
-
let decided = false;
|
|
457
|
-
|
|
458
|
-
while (!decided) {
|
|
459
|
-
const isTruncated = Patcher.showDiff(surgicalPatch, showFull);
|
|
460
|
-
|
|
461
|
-
const choices = [
|
|
462
|
-
{ name: '✅ Apply this surgical patch brother', value: 'apply' },
|
|
463
|
-
{ name: '⏭️ Skip', value: 'skip' }
|
|
464
|
-
];
|
|
465
|
-
|
|
466
|
-
if (isTruncated && !showFull) {
|
|
467
|
-
choices.splice(1, 0, { name: '🔍 View Full Diff', value: 'full' });
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
const { action } = await inquirer.prompt([{
|
|
471
|
-
type: 'list',
|
|
472
|
-
name: 'action',
|
|
473
|
-
message: chalk.cyan(`Patch ${originalPath}?`),
|
|
474
|
-
choices: choices
|
|
475
|
-
}]);
|
|
476
|
-
|
|
477
|
-
if (action === 'full') {
|
|
478
|
-
showFull = true;
|
|
479
|
-
continue;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (action === 'apply') {
|
|
483
|
-
const result = Patcher.applyPatch(surgicalPatch);
|
|
484
|
-
if (result.success) {
|
|
485
|
-
console.log(chalk.green(` ✅ Applied surgical patch to ${originalPath}\n`));
|
|
486
|
-
} else {
|
|
487
|
-
console.log(chalk.red(` ❌ Error applying patch: ${result.error}\n`));
|
|
488
|
-
return { success: false, executed: true, error: result.error, newCwd: activeCwd };
|
|
489
|
-
}
|
|
490
|
-
} else {
|
|
491
|
-
console.log(chalk.gray(' Skipped.\n'));
|
|
492
|
-
}
|
|
493
|
-
decided = true;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
return {
|
|
498
|
-
success: true,
|
|
499
|
-
executed: true,
|
|
500
|
-
output: accumulatedOutput.trim(),
|
|
501
|
-
error: accumulatedError.trim(),
|
|
502
|
-
newCwd: activeCwd
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* Command history for undo/rollback
|
|
508
|
-
*/
|
|
509
|
-
export class CommandHistory {
|
|
510
|
-
private history: Array<{ command: string; timestamp: Date; success: boolean }> = [];
|
|
511
|
-
|
|
512
|
-
add(command: string, success: boolean) {
|
|
513
|
-
this.history.push({
|
|
514
|
-
command,
|
|
515
|
-
timestamp: new Date(),
|
|
516
|
-
success
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
getHistory() {
|
|
521
|
-
return this.history;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
clear() {
|
|
525
|
-
this.history = [];
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
export const commandHistory = new CommandHistory();
|