shoud-cli 1.0.7 → 1.0.10
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/bin/shoud.js +24 -11
- package/package.json +1 -1
- package/src/tools/index.js +96 -121
package/bin/shoud.js
CHANGED
|
@@ -5,7 +5,7 @@ const chalk = require('chalk');
|
|
|
5
5
|
const { login } = require('../src/auth/deviceFlow');
|
|
6
6
|
const { executeTask } = require('../src/runtime/agentLoop');
|
|
7
7
|
|
|
8
|
-
// ───
|
|
8
|
+
// ─── Original SHOUD Banner Art ──────────────────────────────
|
|
9
9
|
const BANNER_ART = [
|
|
10
10
|
" ███████╗██╗ ██╗ ██████╗ ██╗ ██╗██████╗ ",
|
|
11
11
|
" ██╔════╝██║ ██║██╔═══██╗██║ ██║██╔══██╗",
|
|
@@ -15,13 +15,21 @@ const BANNER_ART = [
|
|
|
15
15
|
" ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ "
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
+
// ─── Static Banner (prints at the start of commands) ──────
|
|
19
|
+
function printStaticBanner() {
|
|
20
|
+
const color = chalk.hex('#B5F96C');
|
|
21
|
+
console.log(color(BANNER_ART.join('\n')));
|
|
22
|
+
console.log(color(' ┌──────────────────────────────────────────────────────────┐'));
|
|
23
|
+
console.log(color(' │ Give your computer a job. │'));
|
|
24
|
+
console.log(color(' └──────────────────────────────────────────────────────────┘'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─── Animated Banner (for help) ─────────────────────────────
|
|
18
28
|
const SLEEP = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
29
|
|
|
20
|
-
// ─── Animated Banner ─────────────────────────────────────────────────────────
|
|
21
30
|
async function renderAnimatedBanner() {
|
|
22
31
|
if (!process.stdout.isTTY) {
|
|
23
|
-
|
|
24
|
-
console.log(chalk.hex('#B5F96C').bold(' Give your computer a job.\n'));
|
|
32
|
+
printStaticBanner();
|
|
25
33
|
return;
|
|
26
34
|
}
|
|
27
35
|
|
|
@@ -61,7 +69,7 @@ async function renderAnimatedBanner() {
|
|
|
61
69
|
process.stdout.write('\x1B[?25h');
|
|
62
70
|
}
|
|
63
71
|
|
|
64
|
-
// ─── Custom Help
|
|
72
|
+
// ─── Custom Help ──────────────────────────────────────────────
|
|
65
73
|
async function customHelp() {
|
|
66
74
|
console.log(chalk.hex('#B5F96C').bold('\n SHOUD Terminal Agent - Command Reference\n'));
|
|
67
75
|
console.log(chalk.gray(' Usage: shoud [command] OR shoud "[prompt]"\n'));
|
|
@@ -78,7 +86,7 @@ async function customHelp() {
|
|
|
78
86
|
console.log(` ${chalk.hex('#B5F96C')('>')} shoud "Run npm test and fix any failing test cases"\n`);
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
// ─── Main
|
|
89
|
+
// ─── Main ──────────────────────────────────────────────────────
|
|
82
90
|
async function main() {
|
|
83
91
|
const args = process.argv.slice(2);
|
|
84
92
|
|
|
@@ -87,8 +95,7 @@ async function main() {
|
|
|
87
95
|
args[0] === 'help' || args[0] === '--help' || args[0] === '-h' ||
|
|
88
96
|
args[0] === '--version' || args[0] === '-v') {
|
|
89
97
|
if (args[0] === '--version' || args[0] === '-v') {
|
|
90
|
-
|
|
91
|
-
program.version('1.0.0');
|
|
98
|
+
program.version('1.0.9');
|
|
92
99
|
program.parse(process.argv);
|
|
93
100
|
return;
|
|
94
101
|
}
|
|
@@ -98,15 +105,18 @@ async function main() {
|
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
program
|
|
101
|
-
.version('1.0.
|
|
108
|
+
.version('1.0.9')
|
|
102
109
|
.description('SHOUD Terminal Agent');
|
|
103
110
|
|
|
104
111
|
program
|
|
105
112
|
.command('login')
|
|
106
113
|
.description('Authenticate this device with your SHOUD account')
|
|
107
|
-
.action(
|
|
114
|
+
.action(() => {
|
|
115
|
+
// Optionally show banner before login? We'll keep it clean.
|
|
116
|
+
login();
|
|
117
|
+
});
|
|
108
118
|
|
|
109
|
-
// Placeholder for status
|
|
119
|
+
// Placeholder for status
|
|
110
120
|
program
|
|
111
121
|
.command('status')
|
|
112
122
|
.description('Check your current credit balance and active plan')
|
|
@@ -122,7 +132,10 @@ async function main() {
|
|
|
122
132
|
return;
|
|
123
133
|
}
|
|
124
134
|
const taskPrompt = promptArr.join(' ');
|
|
135
|
+
// ✅ Show the static banner at the start
|
|
136
|
+
printStaticBanner();
|
|
125
137
|
await executeTask(taskPrompt);
|
|
138
|
+
// No banner at the end (removed)
|
|
126
139
|
});
|
|
127
140
|
|
|
128
141
|
program.parse(process.argv);
|
package/package.json
CHANGED
package/src/tools/index.js
CHANGED
|
@@ -1,140 +1,115 @@
|
|
|
1
|
-
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
2
5
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const { login } = require('../src/auth/deviceFlow');
|
|
6
|
-
const { executeTask } = require('../src/runtime/agentLoop');
|
|
6
|
+
const MAX_OUTPUT_SIZE = 1024 * 1024;
|
|
7
|
+
const PROJECT_ROOT = process.cwd();
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
" ╚════██║██╔══██║██║ ██║██║ ██║██║ ██║",
|
|
14
|
-
" ███████║██║ ██║╚██████╔╝╚██████╔╝██████╔╝",
|
|
15
|
-
" ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ "
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
// ─── Static Banner (no animation) ──────────────────────────
|
|
19
|
-
function printStaticBanner() {
|
|
20
|
-
const color = chalk.hex('#B5F96C');
|
|
21
|
-
console.log(color(BANNER_ART.join('\n')));
|
|
22
|
-
console.log(color(' ┌──────────────────────────────────────────────────────────┐'));
|
|
23
|
-
console.log(color(' │ Give your computer a job. │'));
|
|
24
|
-
console.log(color(' └──────────────────────────────────────────────────────────┘'));
|
|
9
|
+
function getShell() {
|
|
10
|
+
if (os.platform() === 'win32') {
|
|
11
|
+
return 'powershell.exe';
|
|
12
|
+
}
|
|
13
|
+
return '/bin/bash';
|
|
25
14
|
}
|
|
26
15
|
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (!process.stdout.isTTY) {
|
|
32
|
-
printStaticBanner();
|
|
33
|
-
return;
|
|
16
|
+
function sanitizePath(inputPath) {
|
|
17
|
+
const resolved = path.resolve(PROJECT_ROOT, inputPath);
|
|
18
|
+
if (!resolved.startsWith(PROJECT_ROOT)) {
|
|
19
|
+
throw new Error('Path is outside the project directory.');
|
|
34
20
|
}
|
|
21
|
+
return resolved;
|
|
22
|
+
}
|
|
35
23
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Execute a shell command and return plain text output.
|
|
26
|
+
* On Windows, we use PowerShell with `Out-String` to get text.
|
|
27
|
+
*/
|
|
28
|
+
function executeShell(commandString) {
|
|
29
|
+
if (!commandString || commandString.trim().length === 0) {
|
|
30
|
+
throw new Error('Empty command.');
|
|
31
|
+
}
|
|
42
32
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
let finalCommand = commandString;
|
|
34
|
+
if (os.platform() === 'win32') {
|
|
35
|
+
// ✅ Force PowerShell to output plain text
|
|
36
|
+
// Escape double quotes in the command
|
|
37
|
+
const escaped = commandString.replace(/"/g, '`"');
|
|
38
|
+
// Wrap in a PowerShell script block and pipe to Out-String
|
|
39
|
+
finalCommand = `& { ${escaped} } | Out-String -Width 4096`;
|
|
40
|
+
}
|
|
48
41
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
try {
|
|
43
|
+
const output = execSync(finalCommand, {
|
|
44
|
+
cwd: PROJECT_ROOT,
|
|
45
|
+
encoding: 'utf-8',
|
|
46
|
+
shell: getShell(),
|
|
47
|
+
timeout: 30000,
|
|
48
|
+
maxBuffer: MAX_OUTPUT_SIZE,
|
|
49
|
+
stdio: 'pipe',
|
|
52
50
|
});
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
await SLEEP(75);
|
|
51
|
+
const trimmed = output.trim();
|
|
52
|
+
return trimmed || 'Command executed successfully with no output.';
|
|
53
|
+
} catch (err) {
|
|
54
|
+
// If the command fails, return the error message
|
|
55
|
+
if (err.stderr) {
|
|
56
|
+
return `Error: ${err.stderr.trim()}`;
|
|
57
|
+
}
|
|
58
|
+
if (err.stdout) {
|
|
59
|
+
// Some commands output to stdout even on failure (e.g., `dir`)
|
|
60
|
+
return err.stdout.trim();
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Command execution failed: ${err.message}`);
|
|
67
63
|
}
|
|
68
|
-
|
|
69
|
-
process.stdout.write('\x1B[?25h');
|
|
70
64
|
}
|
|
71
65
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
66
|
+
function readFile(filePath) {
|
|
67
|
+
const safePath = sanitizePath(filePath);
|
|
68
|
+
try {
|
|
69
|
+
const stats = fs.statSync(safePath);
|
|
70
|
+
if (!stats.isFile()) throw new Error('Path is not a file.');
|
|
71
|
+
if (stats.size > MAX_OUTPUT_SIZE) {
|
|
72
|
+
const buffer = Buffer.alloc(MAX_OUTPUT_SIZE);
|
|
73
|
+
const fd = fs.openSync(safePath, 'r');
|
|
74
|
+
fs.readSync(fd, buffer, 0, MAX_OUTPUT_SIZE, 0);
|
|
75
|
+
fs.closeSync(fd);
|
|
76
|
+
return buffer.toString('utf-8') + '\n... (file truncated)';
|
|
77
|
+
}
|
|
78
|
+
return fs.readFileSync(safePath, 'utf-8');
|
|
79
|
+
} catch (err) {
|
|
80
|
+
throw new Error(`Failed to read file: ${err.message}`);
|
|
81
|
+
}
|
|
87
82
|
}
|
|
88
83
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
args[0] === 'help' || args[0] === '--help' || args[0] === '-h' ||
|
|
96
|
-
args[0] === '--version' || args[0] === '-v') {
|
|
97
|
-
if (args[0] === '--version' || args[0] === '-v') {
|
|
98
|
-
program.version('1.0.6');
|
|
99
|
-
program.parse(process.argv);
|
|
100
|
-
return;
|
|
84
|
+
function writeFile(filePath, content) {
|
|
85
|
+
const safePath = sanitizePath(filePath);
|
|
86
|
+
try {
|
|
87
|
+
const dir = path.dirname(safePath);
|
|
88
|
+
if (!fs.existsSync(dir)) {
|
|
89
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
101
90
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
91
|
+
fs.writeFileSync(safePath, content, 'utf-8');
|
|
92
|
+
return `File written successfully: ${filePath}`;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
throw new Error(`Failed to write file: ${err.message}`);
|
|
105
95
|
}
|
|
96
|
+
}
|
|
106
97
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
program
|
|
125
|
-
.argument('[prompt...]', 'The task you want SHOUD to execute')
|
|
126
|
-
.action(async (promptArr) => {
|
|
127
|
-
if (!promptArr || promptArr.length === 0) {
|
|
128
|
-
console.log(chalk.gray(' Usage: shoud "fix the build errors"'));
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
const taskPrompt = promptArr.join(' ');
|
|
132
|
-
await executeTask(taskPrompt);
|
|
133
|
-
// ✅ After task completes, show the static banner
|
|
134
|
-
printStaticBanner();
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
program.parse(process.argv);
|
|
98
|
+
function executeTool(toolName, input) {
|
|
99
|
+
try {
|
|
100
|
+
switch (toolName) {
|
|
101
|
+
case 'execute_shell':
|
|
102
|
+
return executeShell(input.command);
|
|
103
|
+
case 'read_file':
|
|
104
|
+
return readFile(input.path);
|
|
105
|
+
case 'write_file':
|
|
106
|
+
return writeFile(input.path, input.content);
|
|
107
|
+
default:
|
|
108
|
+
throw new Error(`Tool "${toolName}" is not supported.`);
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return `Tool execution failed: ${error.message}`;
|
|
112
|
+
}
|
|
138
113
|
}
|
|
139
114
|
|
|
140
|
-
|
|
115
|
+
module.exports = { executeTool };
|