shoud-cli 1.0.11 → 3.0.1

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 CHANGED
@@ -1,144 +1,248 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  const { program } = require('commander');
4
3
  const chalk = require('chalk');
5
- const { login } = require('../src/auth/deviceFlow');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const inquirer = require('inquirer');
7
+
8
+ const { initDirs } = require('../src/utils/config');
9
+ const { ExitCodes } = require('../src/utils/errors');
10
+ const { printStaticBanner } = require('../src/ui/banner');
11
+ const { login, getEmail } = require('../src/auth/deviceFlow');
12
+ const { createJob, JobStatus } = require('../src/job/model');
13
+ const { saveJob, loadJob, listJobs, findResumableJob } = require('../src/job/manager');
6
14
  const { executeTask } = require('../src/runtime/agentLoop');
15
+ const { undoJob, undoLatest } = require('../src/job/undo');
16
+ const { formatReceipt } = require('../src/job/receipt');
17
+ const { discoverProject, formatProject } = require('../src/project/discovery');
7
18
 
8
- // ─── Original SHOUD Banner Art ──────────────────────────────
9
- const BANNER_ART = [
10
- " ███████╗██╗ ██╗ ██████╗ ██╗ ██╗██████╗ ",
11
- " ██╔════╝██║ ██║██╔═══██╗██║ ██║██╔══██╗",
12
- " ███████╗███████║██║ ██║██║ ██║██║ ██║",
13
- " ╚════██║██╔══██║██║ ██║██║ ██║██║ ██║",
14
- " ███████║██║ ██║╚██████╔╝╚██████╔╝██████╔╝",
15
- " ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ "
16
- ];
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) ─────────────────────────────
28
- const SLEEP = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
29
-
30
- async function renderAnimatedBanner() {
31
- if (!process.stdout.isTTY) {
32
- printStaticBanner();
33
- return;
34
- }
35
-
36
- const colorGradients = [
37
- ['#364B1D', '#5E8232', '#86B947', '#B5F96C'],
38
- ['#5E8232', '#86B947', '#B5F96C', '#DDFFA8'],
39
- ['#86B947', '#B5F96C', '#FFFFFF', '#B5F96C'],
40
- ['#B5F96C', '#B5F96C', '#B5F96C', '#9EEB49']
41
- ];
42
-
43
- for (const colors of colorGradients) {
44
- process.stdout.write('\x1B[?25l');
45
- process.stdout.write('\r\x1B[K');
46
- console.clear();
47
- console.log();
48
-
49
- BANNER_ART.forEach((line, index) => {
50
- const color = colors[index % colors.length];
51
- console.log(chalk.hex(color).bold(line));
52
- });
53
-
54
- console.log(
55
- chalk.hex('#3B472E')(
56
- ' ┌──────────────────────────────────────────────────────────┐\n' +
57
- ' │'
58
- ) +
59
- chalk.hex('#B5F96C').bold(' Give your computer a job. ') +
60
- chalk.hex('#3B472E')(
61
- '│\n' +
62
- ' └──────────────────────────────────────────────────────────┘\n'
63
- )
64
- );
65
-
66
- await SLEEP(75);
67
- }
68
-
69
- process.stdout.write('\x1B[?25h');
70
- }
19
+ initDirs();
71
20
 
72
- // ─── Custom Help ──────────────────────────────────────────────
73
- async function customHelp() {
74
- console.log(chalk.hex('#B5F96C').bold('\n SHOUD Terminal Agent - Command Reference\n'));
75
- console.log(chalk.gray(' Usage: shoud [command] OR shoud "[prompt]"\n'));
76
-
77
- console.log(chalk.white.bold(' Commands:'));
78
- console.log(` ${chalk.hex('#B5F96C')('shoud login')} Authenticate this device with your Google account.`);
79
- console.log(` ${chalk.hex('#B5F96C')('shoud help')} Display this help menu.`);
80
- console.log(` ${chalk.hex('#B5F96C')('shoud status')} Check your current credit balance and active plan.`);
81
- console.log(` ${chalk.hex('#B5F96C')('shoud --version')} Show the version number.\n`);
82
-
83
- console.log(chalk.white.bold(' Autonomous Execution:'));
84
- console.log(chalk.gray(' Wrap your instructions in quotes to trigger the agent loop.'));
85
- console.log(` ${chalk.hex('#B5F96C')('>')} shoud "Refactor the auth middleware to use JWTs"`);
86
- console.log(` ${chalk.hex('#B5F96C')('>')} shoud "Run npm test and fix any failing test cases"\n`);
87
- }
88
-
89
- // ─── Main ──────────────────────────────────────────────────────
90
21
  async function main() {
91
22
  const args = process.argv.slice(2);
92
23
 
93
- // Intercept help, version, and empty input
94
- if (args.length === 0 ||
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.9');
99
- program.parse(process.argv);
100
- return;
101
- }
102
- await renderAnimatedBanner();
103
- await customHelp();
104
- return;
24
+ // Intercept bare invocation
25
+ if (args.length === 0) {
26
+ return handleBareInvocation();
105
27
  }
106
28
 
107
29
  program
108
- .version('1.0.9')
109
- .description('SHOUD Terminal Agent');
30
+ .name('shoud')
31
+ .version('2.0.0')
32
+ .description('SHOUD Terminal Agent')
33
+ .argument('[prompt...]', 'The task you want SHOUD to execute')
34
+ .option('--budget <amount>', 'Budget for this job in USD', parseFloat)
35
+ .option('--json', 'Emit JSON output (for scripting)')
36
+ .option('--no-color', 'Disable colored output')
37
+ .action(async (promptArr, opts) => {
38
+ if (!promptArr || promptArr.length === 0) {
39
+ await handleBareInvocation();
40
+ return;
41
+ }
42
+ await runJob(promptArr.join(' '), opts);
43
+ });
110
44
 
111
45
  program
112
46
  .command('login')
113
- .description('Authenticate this device with your SHOUD account')
114
- .action(() => {
115
- // Optionally show banner before login? We'll keep it clean.
116
- login();
47
+ .description('Authenticate this device')
48
+ .action(async () => {
49
+ try { await login(); process.exit(0); }
50
+ catch (e) { console.error(chalk.red(e.message)); process.exit(1); }
117
51
  });
118
52
 
119
- // Placeholder for status
120
53
  program
121
54
  .command('status')
122
- .description('Check your current credit balance and active plan')
55
+ .description('Show active job or last completed job')
56
+ .action(() => showStatus());
57
+
58
+ program
59
+ .command('jobs')
60
+ .description('List all jobs')
123
61
  .action(() => {
124
- console.log(chalk.yellow(' Status command not yet implemented.'));
62
+ const jobs = listJobs();
63
+ if (!jobs.length) { console.log('No jobs found.'); return; }
64
+ for (const j of jobs) {
65
+ console.log(` ${j.id} ${j.status.padEnd(24)} $${(j.spent || 0).toFixed(4)} ${j.prompt?.slice(0, 60)}`);
66
+ }
125
67
  });
126
68
 
127
69
  program
128
- .argument('[prompt...]', 'The task you want SHOUD to execute')
129
- .action(async (promptArr) => {
130
- if (!promptArr || promptArr.length === 0) {
131
- console.log(chalk.gray(' Usage: shoud "fix the build errors"'));
132
- return;
70
+ .command('undo [jobId]')
71
+ .description('Undo changes from a job (or the most recent completed job)')
72
+ .action(async (jobId) => {
73
+ try {
74
+ let result;
75
+ if (jobId) result = { job: { id: jobId }, result: undoJob(jobId) };
76
+ else result = undoLatest();
77
+ console.log(chalk.green(`\n✓ Reverted job ${result.job.id}.`));
78
+ if (result.result.preservedUserChanges) {
79
+ console.log(chalk.green('✓ Pre-existing user changes preserved.'));
80
+ }
81
+ process.exit(0);
82
+ } catch (e) {
83
+ console.error(chalk.red(`\n✗ Undo failed: ${e.message}`));
84
+ process.exit(1);
133
85
  }
134
- const taskPrompt = promptArr.join(' ');
135
- // ✅ Show the static banner at the start
136
- printStaticBanner();
137
- await executeTask(taskPrompt);
138
- // No banner at the end (removed)
139
86
  });
140
87
 
141
- program.parse(process.argv);
88
+ program
89
+ .command('receipt [jobId]')
90
+ .description('Show the receipt for a job')
91
+ .action((jobId) => {
92
+ const id = jobId || listJobs()[0]?.id;
93
+ if (!id) { console.log('No jobs found.'); return; }
94
+ const job = loadJob(id);
95
+ if (!job) { console.log(`Job ${id} not found.`); return; }
96
+ console.log('\n' + formatReceipt(job));
97
+ });
98
+
99
+ program
100
+ .command('project')
101
+ .description('Show detected project info')
102
+ .action(() => {
103
+ const p = discoverProject(process.cwd());
104
+ console.log(chalk.bold('\nProject info\n'));
105
+ console.log(formatProject(p));
106
+ console.log();
107
+ });
108
+
109
+ await program.parseAsync(process.argv);
110
+ }
111
+
112
+ async function handleBareInvocation() {
113
+ printStaticBanner();
114
+ const resumable = findResumableJob();
115
+
116
+ if (resumable) {
117
+ console.log(chalk.yellow(`\nInterrupted job found: ${resumable.id}`));
118
+ console.log(chalk.gray(` ${resumable.prompt}`));
119
+ console.log(chalk.gray(` Status: ${resumable.status} Spent: $${(resumable.spent || 0).toFixed(4)}`));
120
+
121
+ const { action } = await inquirer.prompt([{
122
+ type: 'list', name: 'action',
123
+ message: 'What would you like to do?',
124
+ choices: [
125
+ { name: 'Resume it', value: 'resume' },
126
+ { name: 'View receipt', value: 'receipt' },
127
+ { name: 'Undo its changes', value: 'undo' },
128
+ { name: 'Start a new job', value: 'new' },
129
+ { name: 'Quit', value: 'quit' },
130
+ ],
131
+ }]);
132
+
133
+ if (action === 'resume') {
134
+ const job = loadJob(resumable.id);
135
+ job.status = JobStatus.RUNNING;
136
+ await executeTask(job);
137
+ return;
138
+ }
139
+ if (action === 'receipt') {
140
+ const job = loadJob(resumable.id);
141
+ console.log('\n' + formatReceipt(job));
142
+ return;
143
+ }
144
+ if (action === 'undo') {
145
+ try { undoJob(resumable.id); console.log(chalk.green('\n✓ Reverted.')); }
146
+ catch (e) { console.error(chalk.red(`✗ ${e.message}`)); }
147
+ return;
148
+ }
149
+ if (action === 'quit') return;
150
+ // 'new' falls through
151
+ }
152
+
153
+ // No resumable job, no prompt → print usage
154
+ console.log(chalk.bold('\nUsage:') + ' shoud "[prompt]"');
155
+ console.log(chalk.gray(' or: shoud login (authenticate)'));
156
+ console.log(chalk.gray(' or: shoud jobs (list jobs)'));
157
+ console.log(chalk.gray(' or: shoud undo (revert last job)'));
158
+ console.log();
159
+ console.log(chalk.bold('Commands:'));
160
+ for (const c of [
161
+ ['shoud login', 'Authenticate this device'],
162
+ ['shoud status', 'Show active/last job'],
163
+ ['shoud jobs', 'List all jobs'],
164
+ ['shoud undo [id]', 'Revert changes from a job'],
165
+ ['shoud receipt [id]', 'Show a job receipt'],
166
+ ['shoud project', 'Show detected project info'],
167
+ ]) {
168
+ console.log(` ${chalk.hex('#B5F96C')(c[0].padEnd(22))} ${chalk.gray(c[1])}`);
169
+ }
170
+ console.log();
171
+ }
172
+
173
+ async function runJob(prompt, opts) {
174
+ printStaticBanner();
175
+
176
+ const email = await getEmail();
177
+ if (!email) {
178
+ console.log(chalk.red('\nNot signed in. Run `shoud login` first.'));
179
+ process.exit(ExitCodes.AUTH_REQUIRED);
180
+ }
181
+
182
+ const projectRoot = process.cwd();
183
+ const job = createJob({
184
+ prompt,
185
+ projectRoot,
186
+ budget: opts.budget || 2.0,
187
+ });
188
+
189
+ // Pre-fill verification commands from project discovery + SHOUD.md
190
+ const project = discoverProject(projectRoot);
191
+ const { loadShoudMd } = require('../src/project/shoudMd');
192
+ const shoud = loadShoudMd(projectRoot);
193
+ job.verification.commands = shoud.verification.length ? shoud.verification : (project.verification || []);
194
+
195
+ saveJob(job);
196
+
197
+ try {
198
+ const finalJob = await executeTask(job);
199
+ let exitCode = ExitCodes.VERIFIED;
200
+ switch (finalJob.status) {
201
+ case JobStatus.VERIFIED: exitCode = ExitCodes.VERIFIED; break;
202
+ case JobStatus.IMPLEMENTED_NOT_VERIFIED: exitCode = ExitCodes.VERIFIED; break;
203
+ case JobStatus.FAILED: exitCode = ExitCodes.FAILED; break;
204
+ case JobStatus.BLOCKED: exitCode = ExitCodes.BLOCKED; break;
205
+ case JobStatus.BUDGET_EXCEEDED: exitCode = ExitCodes.BUDGET_EXCEEDED; break;
206
+ case JobStatus.PERMISSION_DENIED: exitCode = ExitCodes.PERMISSION_DENIED; break;
207
+ case JobStatus.CANCELLED: exitCode = ExitCodes.CANCELLED; break;
208
+ default: exitCode = ExitCodes.FAILED;
209
+ }
210
+ if (opts.json) {
211
+ process.stdout.write(JSON.stringify({
212
+ jobId: finalJob.id,
213
+ status: finalJob.status,
214
+ spent: finalJob.spent,
215
+ changedFiles: finalJob.changedFiles,
216
+ verification: finalJob.verification.results.map(r => ({ command: r.command, ok: r.ok })),
217
+ }, null, 2) + '\n');
218
+ }
219
+ process.exit(exitCode);
220
+ } catch (err) {
221
+ if (err.code === ExitCodes.AUTH_REQUIRED) {
222
+ console.error(chalk.red('\n' + err.message));
223
+ process.exit(ExitCodes.AUTH_REQUIRED);
224
+ }
225
+ console.error(chalk.red(`\n✗ ${err.message}`));
226
+ process.exit(1);
227
+ }
228
+ }
229
+
230
+ function showStatus() {
231
+ const jobs = listJobs();
232
+ if (!jobs.length) { console.log('No jobs yet.'); return; }
233
+ const active = jobs.find(j => !['verified', 'failed', 'cancelled', 'blocked', 'budget_exceeded'].includes(j.status));
234
+ const j = active || jobs[0];
235
+ console.log(chalk.bold('\nJob ' + j.id));
236
+ console.log(` Prompt: ${j.prompt}`);
237
+ console.log(` Status: ${j.status}`);
238
+ console.log(` Spent: $${(j.spent || 0).toFixed(4)}`);
239
+ console.log(` Started: ${j.startedAt}`);
240
+ if (j.completedAt) console.log(` Ended: ${j.completedAt}`);
241
+ console.log();
142
242
  }
143
243
 
144
- main();
244
+ main().catch((err) => {
245
+ console.error(chalk.red(`\nFatal: ${err.message}`));
246
+ if (process.env.SHOUD_DEBUG) console.error(err.stack);
247
+ process.exit(1);
248
+ });
package/install.sh CHANGED
@@ -1,18 +1,23 @@
1
1
  #!/usr/bin/env bash
2
2
  set -e
3
3
 
4
- echo "Installing SHOUD Agent..."
4
+ echo "Installing SHOUD Terminal Agent v2..."
5
5
  echo "Give your computer a job."
6
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
7
+ if ! command -v node &> /dev/null; then
8
+ echo "Error: Node.js >= 18 required. Install from https://nodejs.org"
9
+ exit 1
10
+ fi
11
+
12
+ NODE_MAJOR=$(node -p "process.versions.node.split('.')[0]")
13
+ if [ "$NODE_MAJOR" -lt 18 ]; then
14
+ echo "Error: Node.js >= 18 required (found $(node -v))."
15
+ exit 1
12
16
  fi
13
17
 
14
18
  npm install -g shoud-cli
15
19
 
16
20
  echo ""
17
- echo "✓ Installation complete."
18
- echo "Run 'shoud login' to authenticate your device."
21
+ echo "✓ Installed."
22
+ echo " Authenticate with: shoud login"
23
+ echo " Run a job with: shoud \"fix the build\""
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "shoud-cli",
3
- "version": "1.0.11",
3
+ "version": "3.0.1",
4
4
  "description": "SHOUD Terminal Agent: Give your computer a job.",
5
5
  "main": "bin/shoud.js",
6
6
  "bin": {
7
7
  "shoud": "bin/shoud.js"
8
8
  },
9
9
  "scripts": {
10
- "start": "node ./bin/shoud.js"
10
+ "start": "node ./bin/shoud.js",
11
+ "test": "node --test tests/"
11
12
  },
12
13
  "dependencies": {
13
14
  "axios": "^1.6.8",
@@ -15,21 +16,19 @@
15
16
  "commander": "^11.0.0",
16
17
  "inquirer": "^8.2.6",
17
18
  "open": "^8.4.2",
18
- "ora": "^5.4.1"
19
+ "ora": "^5.4.1",
20
+ "fast-glob": "^3.3.2",
21
+ "ignore": "^5.3.0",
22
+ "diff": "^5.1.0"
19
23
  },
20
24
  "engines": {
21
- "node": "18.0.0"
25
+ "node": ">=18.0.0"
22
26
  },
23
27
  "files": [
24
28
  "bin/",
25
29
  "src/",
26
30
  "install.sh",
27
- "!**/*.test.js",
28
- "!**/*.spec.js"
31
+ "README.md"
29
32
  ],
30
- "license": "MIT",
31
- "repository": {
32
- "type": "git",
33
- "url": "https://github.com/your-username/shoud-monorepo.git"
34
- }
33
+ "license": "MIT"
35
34
  }
@@ -0,0 +1,49 @@
1
+ const fs = require('fs');
2
+ const { paths } = require('../utils/config');
3
+
4
+ /**
5
+ * Stores CLI credentials. Attempts OS keychain first, falls back to 0600 file.
6
+ * OS keychain support is via optional peer dependency `keytar` — if unavailable,
7
+ * we transparently fall back so the CLI works everywhere.
8
+ */
9
+ let keytar = null;
10
+ try { keytar = require('keytar'); } catch (_) { keytar = null; }
11
+
12
+ const SERVICE = 'shoud-cli';
13
+ const ACCOUNT = 'default';
14
+
15
+ async function saveCredentials(payload) {
16
+ const json = JSON.stringify(payload);
17
+ if (keytar) {
18
+ try {
19
+ await keytar.setPassword(SERVICE, ACCOUNT, json);
20
+ return { storage: 'keychain' };
21
+ } catch (_) { /* fall through */ }
22
+ }
23
+ fs.writeFileSync(paths.SESSION_FILE, json, { encoding: 'utf-8', mode: 0o600 });
24
+ return { storage: 'file' };
25
+ }
26
+
27
+ async function loadCredentials() {
28
+ if (keytar) {
29
+ try {
30
+ const raw = await keytar.getPassword(SERVICE, ACCOUNT);
31
+ if (raw) return JSON.parse(raw);
32
+ } catch (_) {}
33
+ }
34
+ try {
35
+ if (fs.existsSync(paths.SESSION_FILE)) {
36
+ return JSON.parse(fs.readFileSync(paths.SESSION_FILE, 'utf-8'));
37
+ }
38
+ } catch (_) {}
39
+ return null;
40
+ }
41
+
42
+ async function clearCredentials() {
43
+ if (keytar) { try { await keytar.deletePassword(SERVICE, ACCOUNT); } catch (_) {} }
44
+ if (fs.existsSync(paths.SESSION_FILE)) {
45
+ try { fs.unlinkSync(paths.SESSION_FILE); } catch (_) {}
46
+ }
47
+ }
48
+
49
+ module.exports = { saveCredentials, loadCredentials, clearCredentials };