shoud-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/shoud.js +121 -0
- package/install.sh +18 -0
- package/package.json +23 -0
- package/src/auth/deviceFlow.js +40 -0
- package/src/context/checkpoint.js +23 -0
- package/src/permissions/engine.js +34 -0
- package/src/runtime/agentLoop.js +104 -0
- package/src/tools/index.js +21 -0
package/bin/shoud.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { program } = require('commander');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { login } = require('../src/auth/deviceFlow');
|
|
5
|
+
const { executeTask } = require('../src/runtime/agentLoop');
|
|
6
|
+
|
|
7
|
+
// ─── Banner Art ───────────────────────────────────────────────
|
|
8
|
+
const BANNER_ART = [
|
|
9
|
+
" ███████╗██╗ ██╗ ██████╗ ██████╗ ███████╗██╗ ██╗██╗ ██╗██████╗",
|
|
10
|
+
" ██╔════╝██║ ██║██╔═══██╗██╔══██╗██╔════╝██║ ██║██║ ██║██╔══██╗",
|
|
11
|
+
" ███████╗███████║██║ ██║██║ ██║███████╗██║ ██║██║ ██║██████╔╝",
|
|
12
|
+
" ╚════██║██╔══██║██║ ██║██║ ██║╚════██║██║ ██║██║ ██║██╔══██╗",
|
|
13
|
+
" ███████║██║ ██║╚██████╔╝██████╔╝███████║╚██████╔╝╚██████╔╝██║ ██║",
|
|
14
|
+
" ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝"
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const SLEEP = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
+
|
|
19
|
+
// ─── Animated Banner ──────────────────────────────────────────
|
|
20
|
+
async function renderAnimatedBanner() {
|
|
21
|
+
if (!process.stdout.isTTY) {
|
|
22
|
+
console.log(chalk.hex('#B5F96C')(BANNER_ART.join('\n')));
|
|
23
|
+
console.log(chalk.hex('#B5F96C').bold(' Give your computer a job.\n'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const colorGradients = [
|
|
28
|
+
['#364B1D', '#5E8232', '#86B947', '#B5F96C'],
|
|
29
|
+
['#5E8232', '#86B947', '#B5F96C', '#DDFFA8'],
|
|
30
|
+
['#86B947', '#B5F96C', '#FFFFFF', '#B5F96C'],
|
|
31
|
+
['#B5F96C', '#B5F96C', '#B5F96C', '#9EEB49']
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
for (const colors of colorGradients) {
|
|
35
|
+
process.stdout.write('\x1B[?25l');
|
|
36
|
+
process.stdout.write('\r\x1B[K');
|
|
37
|
+
console.clear();
|
|
38
|
+
console.log();
|
|
39
|
+
|
|
40
|
+
BANNER_ART.forEach((line, index) => {
|
|
41
|
+
const color = colors[index % colors.length];
|
|
42
|
+
console.log(chalk.hex(color).bold(line));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
console.log(
|
|
46
|
+
chalk.hex('#3B472E')(
|
|
47
|
+
' ┌──────────────────────────────────────────────────────┐\n' +
|
|
48
|
+
' │'
|
|
49
|
+
) +
|
|
50
|
+
chalk.hex('#B5F96C').bold(' Give your computer a job. ') +
|
|
51
|
+
chalk.hex('#3B472E')(
|
|
52
|
+
'│\n' +
|
|
53
|
+
' └──────────────────────────────────────────────────────┘\n'
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
await SLEEP(75);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
process.stdout.write('\x1B[?25h');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Custom Help ──────────────────────────────────────────────
|
|
64
|
+
async function customHelp() {
|
|
65
|
+
console.log(chalk.hex('#B5F96C').bold('\n SHOUD Terminal Agent - Command Reference\n'));
|
|
66
|
+
console.log(chalk.gray(' Usage: shoud [command] OR shoud "[prompt]"\n'));
|
|
67
|
+
|
|
68
|
+
console.log(chalk.white.bold(' Commands:'));
|
|
69
|
+
console.log(` ${chalk.hex('#B5F96C')('shoud login')} Authenticate this device with your Google account.`);
|
|
70
|
+
console.log(` ${chalk.hex('#B5F96C')('shoud help')} Display this beautiful help menu.`);
|
|
71
|
+
console.log(` ${chalk.hex('#B5F96C')('shoud status')} Check your current credit balance and active plan.\n`);
|
|
72
|
+
|
|
73
|
+
console.log(chalk.white.bold(' Autonomous Execution:'));
|
|
74
|
+
console.log(chalk.gray(' Wrap your instructions in quotes to trigger the agent loop.'));
|
|
75
|
+
console.log(` ${chalk.hex('#B5F96C')('>')} shoud "Refactor the auth middleware to use JWTs"`);
|
|
76
|
+
console.log(` ${chalk.hex('#B5F96C')('>')} shoud "Run npm test and fix any failing test cases"\n`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Main ─────────────────────────────────────────────────────
|
|
80
|
+
async function main() {
|
|
81
|
+
const args = process.argv.slice(2);
|
|
82
|
+
|
|
83
|
+
// Intercept help commands and empty input
|
|
84
|
+
if (args.length === 0 || args[0] === 'help' || args[0] === '--help') {
|
|
85
|
+
await renderAnimatedBanner();
|
|
86
|
+
await customHelp();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
program
|
|
91
|
+
.version('1.0.0')
|
|
92
|
+
.description('SHOUD Terminal Agent');
|
|
93
|
+
|
|
94
|
+
program
|
|
95
|
+
.command('login')
|
|
96
|
+
.description('Authenticate this device with your SHOUD account')
|
|
97
|
+
.action(login);
|
|
98
|
+
|
|
99
|
+
// Placeholder for status – you can replace with real implementation
|
|
100
|
+
program
|
|
101
|
+
.command('status')
|
|
102
|
+
.description('Check your current credit balance and active plan')
|
|
103
|
+
.action(() => {
|
|
104
|
+
console.log(chalk.yellow(' Status command not yet implemented.'));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
program
|
|
108
|
+
.argument('[prompt...]', 'The task you want SHOUD to execute')
|
|
109
|
+
.action(async (promptArr) => {
|
|
110
|
+
if (!promptArr || promptArr.length === 0) {
|
|
111
|
+
console.log(chalk.gray(' Usage: shoud "fix the build errors"'));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const taskPrompt = promptArr.join(' ');
|
|
115
|
+
await executeTask(taskPrompt);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
program.parse(process.argv);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
main();
|
package/install.sh
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
echo "Installing SHOUD Agent..."
|
|
5
|
+
echo "Give your computer a job."
|
|
6
|
+
|
|
7
|
+
# Check if npm is installed
|
|
8
|
+
if ! command -v npm &> /dev/null
|
|
9
|
+
then
|
|
10
|
+
echo "Error: npm could not be found. Please install Node.js and npm first."
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
npm install -g shoud-cli
|
|
15
|
+
|
|
16
|
+
echo ""
|
|
17
|
+
echo "✓ Installation complete."
|
|
18
|
+
echo "Run 'shoud login' to authenticate your device."
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shoud-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SHOUD Terminal Agent: Give your computer a job.",
|
|
5
|
+
"main": "bin/shoud.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"shoud": "./bin/shoud.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node ./bin/shoud.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"axios": "^1.6.8",
|
|
14
|
+
"chalk": "^4.1.2",
|
|
15
|
+
"commander": "^11.0.0",
|
|
16
|
+
"inquirer": "^8.2.6",
|
|
17
|
+
"open": "^8.4.2",
|
|
18
|
+
"ora": "^5.4.1"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const open = require('open');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const SESSION_FILE = path.join(os.homedir(), '.shoud_session');
|
|
9
|
+
|
|
10
|
+
async function login() {
|
|
11
|
+
console.log(chalk.cyan('Opening browser to sign in via shoud.online...'));
|
|
12
|
+
await open('https://shoud.online/login');
|
|
13
|
+
|
|
14
|
+
console.log(chalk.gray('\nOnce signed in, copy your session token from the browser.'));
|
|
15
|
+
|
|
16
|
+
const { token } = await inquirer.prompt([
|
|
17
|
+
{
|
|
18
|
+
type: 'password',
|
|
19
|
+
name: 'token',
|
|
20
|
+
message: 'Paste your session token:',
|
|
21
|
+
mask: '*'
|
|
22
|
+
}
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
if (token) {
|
|
26
|
+
fs.writeFileSync(SESSION_FILE, token, { encoding: 'utf-8', mode: 0o600 });
|
|
27
|
+
console.log(chalk.green('\n✓ Signed in successfully. Device connected.'));
|
|
28
|
+
} else {
|
|
29
|
+
console.log(chalk.red('\nLogin failed: No token provided.'));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getToken() {
|
|
34
|
+
if (!fs.existsSync(SESSION_FILE)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return fs.readFileSync(SESSION_FILE, 'utf-8').trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { login, getToken };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const CHECKPOINT_FILE = path.join(process.cwd(), '.shoud_checkpoint.json');
|
|
5
|
+
|
|
6
|
+
function saveCheckpoint(state) {
|
|
7
|
+
fs.writeFileSync(CHECKPOINT_FILE, JSON.stringify(state, null, 2));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function loadCheckpoint() {
|
|
11
|
+
if (fs.existsSync(CHECKPOINT_FILE)) {
|
|
12
|
+
return JSON.parse(fs.readFileSync(CHECKPOINT_FILE, 'utf-8'));
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function clearCheckpoint() {
|
|
18
|
+
if (fs.existsSync(CHECKPOINT_FILE)) {
|
|
19
|
+
fs.unlinkSync(CHECKPOINT_FILE);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { saveCheckpoint, loadCheckpoint, clearCheckpoint };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const inquirer = require('inquirer');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
|
|
4
|
+
// In a full implementation, this could read from a .shoudignore or local config
|
|
5
|
+
const alwaysAllowedCommands = new Set(['ls', 'pwd', 'git status']);
|
|
6
|
+
|
|
7
|
+
async function verifyPermission(toolName, input) {
|
|
8
|
+
if (toolName === 'read_file') return true; // Safe operation
|
|
9
|
+
|
|
10
|
+
const commandContext = input.command || input.path || 'unknown';
|
|
11
|
+
|
|
12
|
+
if (toolName === 'execute_shell' && alwaysAllowedCommands.has(commandContext)) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
console.log(chalk.yellow(`\n⚠ SHOUD requests permission to run: ${chalk.bold(toolName)}`));
|
|
17
|
+
console.log(chalk.gray(`Target: ${commandContext}`));
|
|
18
|
+
|
|
19
|
+
const { permission } = await inquirer.prompt([
|
|
20
|
+
{
|
|
21
|
+
type: 'list',
|
|
22
|
+
name: 'permission',
|
|
23
|
+
message: 'Allow this operation?',
|
|
24
|
+
choices: [
|
|
25
|
+
{ name: 'Allow once', value: 'once' },
|
|
26
|
+
{ name: 'Deny', value: 'deny' }
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
return permission === 'once';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { verifyPermission };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const ora = require('ora');
|
|
4
|
+
const open = require('open');
|
|
5
|
+
const { getToken } = require('../auth/deviceFlow');
|
|
6
|
+
const { verifyPermission } = require('../permissions/engine');
|
|
7
|
+
const { executeTool } = require('../tools/index');
|
|
8
|
+
const { saveCheckpoint, loadCheckpoint, clearCheckpoint } = require('../context/checkpoint');
|
|
9
|
+
|
|
10
|
+
const API_URL = process.env.SHOUD_API_URL || 'https://shoud.online/api';
|
|
11
|
+
|
|
12
|
+
async function executeTask(prompt) {
|
|
13
|
+
const token = getToken();
|
|
14
|
+
if (!token) {
|
|
15
|
+
console.log(chalk.red('Not signed in. Run `shoud login` first.'));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let messages = [];
|
|
20
|
+
const existingState = loadCheckpoint();
|
|
21
|
+
|
|
22
|
+
if (existingState) {
|
|
23
|
+
console.log(chalk.cyan('● Restoring task context from checkpoint...'));
|
|
24
|
+
messages = existingState.messages;
|
|
25
|
+
} else {
|
|
26
|
+
messages.push({ role: 'user', content: prompt });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let taskComplete = false;
|
|
30
|
+
const spinner = ora('SHOUD is thinking...').start();
|
|
31
|
+
|
|
32
|
+
while (!taskComplete) {
|
|
33
|
+
try {
|
|
34
|
+
const response = await axios.post(`${API_URL}/agent/infer`, {
|
|
35
|
+
messages,
|
|
36
|
+
estimatedBudget: 0.10 // 10 cents max budget per loop iteration
|
|
37
|
+
}, {
|
|
38
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const aiMessage = response.data;
|
|
42
|
+
messages.push({ role: 'assistant', content: aiMessage.content });
|
|
43
|
+
spinner.stop();
|
|
44
|
+
|
|
45
|
+
if (aiMessage.stop_reason === 'tool_use') {
|
|
46
|
+
for (const block of aiMessage.content) {
|
|
47
|
+
if (block.type === 'tool_use') {
|
|
48
|
+
console.log(chalk.blue(`\n● Agent requested: ${block.name}`));
|
|
49
|
+
|
|
50
|
+
const isAllowed = await verifyPermission(block.name, block.input);
|
|
51
|
+
let toolResult = '';
|
|
52
|
+
|
|
53
|
+
if (isAllowed) {
|
|
54
|
+
spinner.start(`Executing ${block.name}...`);
|
|
55
|
+
toolResult = executeTool(block.name, block.input);
|
|
56
|
+
spinner.stop();
|
|
57
|
+
console.log(chalk.green(`✓ Tool executed.`));
|
|
58
|
+
} else {
|
|
59
|
+
toolResult = "User denied permission to execute this tool.";
|
|
60
|
+
console.log(chalk.red(`✕ Permission denied.`));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
messages.push({
|
|
64
|
+
role: 'user',
|
|
65
|
+
content: [{ type: 'tool_result', tool_use_id: block.id, content: toolResult }]
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Save state after tool execution
|
|
69
|
+
saveCheckpoint({ messages });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
spinner.start('Analyzing tool results...');
|
|
73
|
+
} else {
|
|
74
|
+
// Task completed normally
|
|
75
|
+
taskComplete = true;
|
|
76
|
+
clearCheckpoint();
|
|
77
|
+
|
|
78
|
+
// Print the final text block from Claude
|
|
79
|
+
const finalResponse = aiMessage.content.find(c => c.type === 'text')?.text || 'Done.';
|
|
80
|
+
console.log(chalk.green('\n✓ Task Complete.\n'));
|
|
81
|
+
console.log(chalk.white(finalResponse));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
} catch (error) {
|
|
85
|
+
spinner.stop();
|
|
86
|
+
if (error.response && error.response.status === 402) {
|
|
87
|
+
console.log(chalk.red('\n⏸ Task paused. Insufficient SHOUD Credits.'));
|
|
88
|
+
console.log(chalk.gray(`Balance: $${error.response.data.balance.toFixed(4)}`));
|
|
89
|
+
saveCheckpoint({ messages });
|
|
90
|
+
|
|
91
|
+
console.log(chalk.cyan('Opening browser to add credits...'));
|
|
92
|
+
await open('https://shoud.vantyrixtek.online/?amount=10');
|
|
93
|
+
console.log(chalk.gray('Run your command again once credits are added to resume.'));
|
|
94
|
+
return;
|
|
95
|
+
} else {
|
|
96
|
+
console.log(chalk.red(`\nError: ${error.response?.data?.error || error.message}`));
|
|
97
|
+
saveCheckpoint({ messages });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { executeTask };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
function executeTool(toolName, input) {
|
|
5
|
+
try {
|
|
6
|
+
if (toolName === 'execute_shell') {
|
|
7
|
+
const output = execSync(input.command, { encoding: 'utf-8', stdio: 'pipe' });
|
|
8
|
+
return output.trim() || 'Command executed successfully with no output.';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (toolName === 'read_file') {
|
|
12
|
+
return fs.readFileSync(input.path, 'utf-8');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
throw new Error(`Tool ${toolName} is not supported locally.`);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return `Error executing tool: ${error.message}`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { executeTool };
|