@dashclaw/cli 0.1.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.
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from 'child_process';
4
+ import { DashClaw } from 'dashclaw';
5
+ import {
6
+ bold, dim, inverse, colorByRisk, clearScreen,
7
+ moveCursor, hideCursor, showCursor,
8
+ green, red,
9
+ } from '../lib/render.js';
10
+
11
+ process.on('unhandledRejection', (reason) => {
12
+ console.error('Unhandled Rejection:', reason);
13
+ process.exit(1);
14
+ });
15
+
16
+ // -- Config -------------------------------------------------------------------
17
+
18
+ const baseUrl = process.env.DASHCLAW_BASE_URL;
19
+ const apiKey = process.env.DASHCLAW_API_KEY;
20
+ const agentId = process.env.DASHCLAW_AGENT_ID || 'cli-operator';
21
+
22
+ function requireEnv() {
23
+ const missing = [];
24
+ if (!baseUrl) missing.push('DASHCLAW_BASE_URL');
25
+ if (!apiKey) missing.push('DASHCLAW_API_KEY');
26
+ if (missing.length) {
27
+ console.error(`Error: Missing required environment variable(s): ${missing.join(', ')}`);
28
+ console.error('Set them in your shell or a .env file.');
29
+ process.exit(1);
30
+ }
31
+ }
32
+
33
+ function createClient() {
34
+ requireEnv();
35
+ return new DashClaw({ baseUrl, apiKey, agentId });
36
+ }
37
+
38
+ // -- Argv Parsing -------------------------------------------------------------
39
+
40
+ const args = process.argv.slice(2);
41
+ const command = args[0] || 'help';
42
+
43
+ function getFlag(name) {
44
+ const idx = args.indexOf(name);
45
+ if (idx === -1 || idx + 1 >= args.length) return undefined;
46
+ return args[idx + 1];
47
+ }
48
+
49
+ // -- Commands -----------------------------------------------------------------
50
+
51
+ async function cmdHelp() {
52
+ console.log(`
53
+ ${bold('DashClaw CLI')} — terminal approval client
54
+
55
+ ${bold('Usage:')}
56
+ dashclaw approvals Interactive approval inbox
57
+ dashclaw approve <actionId> [--reason] Approve an action
58
+ dashclaw deny <actionId> [--reason] Deny an action
59
+ dashclaw help Show this help
60
+
61
+ ${bold('Environment:')}
62
+ DASHCLAW_BASE_URL (required) DashClaw instance URL
63
+ DASHCLAW_API_KEY (required) API key for authentication
64
+ DASHCLAW_AGENT_ID (optional) Operator identity (default: cli-operator)
65
+ `);
66
+ }
67
+
68
+ async function cmdApprove() {
69
+ const actionId = args[1];
70
+ if (!actionId) {
71
+ console.error('Error: Missing action ID. Usage: dashclaw approve <actionId>');
72
+ process.exit(1);
73
+ }
74
+ const reason = getFlag('--reason');
75
+ const claw = createClient();
76
+
77
+ try {
78
+ await claw.approveAction(actionId, 'allow', reason);
79
+ console.log(`\n ${green('Approved:')} ${actionId}`);
80
+ console.log(` Replay: ${baseUrl}/replay/${actionId}\n`);
81
+ } catch (err) {
82
+ console.error(`Error: ${err.message}`);
83
+ process.exit(1);
84
+ }
85
+ }
86
+
87
+ async function cmdDeny() {
88
+ const actionId = args[1];
89
+ if (!actionId) {
90
+ console.error('Error: Missing action ID. Usage: dashclaw deny <actionId>');
91
+ process.exit(1);
92
+ }
93
+ const reason = getFlag('--reason');
94
+ const claw = createClient();
95
+
96
+ try {
97
+ await claw.approveAction(actionId, 'deny', reason);
98
+ console.log(`\n ${red('Denied:')} ${actionId}`);
99
+ console.log(` Replay: ${baseUrl}/replay/${actionId}\n`);
100
+ } catch (err) {
101
+ console.error(`Error: ${err.message}`);
102
+ process.exit(1);
103
+ }
104
+ }
105
+
106
+ async function cmdApprovals() {
107
+ const claw = createClient();
108
+
109
+ let items = [];
110
+ let selected = 0;
111
+
112
+ async function fetchPending() {
113
+ try {
114
+ const result = await claw.getPendingApprovals(50);
115
+ items = result.actions || [];
116
+ } catch (err) {
117
+ console.error(`Error fetching approvals: ${err.message}`);
118
+ process.exit(1);
119
+ }
120
+ }
121
+
122
+ function render() {
123
+ clearScreen();
124
+ moveCursor(1, 1);
125
+ process.stdout.write(bold('DashClaw Approval Inbox') + '\n\n');
126
+
127
+ if (items.length === 0) {
128
+ process.stdout.write(dim(' No pending approvals.\n'));
129
+ process.stdout.write(dim(' Press R to refresh, Q to quit.\n'));
130
+ } else {
131
+ for (let i = 0; i < items.length; i++) {
132
+ const a = items[i];
133
+ const id = a.action_id || a.id || '?';
134
+ const type = a.action_type || '-';
135
+ const agent = a.agent_id || '-';
136
+ const goal = (a.declared_goal || '-').slice(0, 60);
137
+ const risk = a.risk_score != null ? colorByRisk(a.risk_score) : dim('-');
138
+
139
+ const line = ` [${i + 1}] ${type} | ${agent} | ${goal} | risk: ${risk}`;
140
+ process.stdout.write((i === selected ? inverse(line) : line) + '\n');
141
+ }
142
+ }
143
+
144
+ process.stdout.write('\n' + dim(' [A] Approve [D] Deny [R] Refresh [O] Open Replay [Q] Quit') + '\n');
145
+ }
146
+
147
+ function openReplay(actionId) {
148
+ const url = `${baseUrl}/replay/${actionId}`;
149
+ try {
150
+ const platform = process.platform;
151
+ if (platform === 'darwin') execFileSync('open', [url]);
152
+ else if (platform === 'win32') execFileSync('cmd', ['/c', 'start', '', url]);
153
+ else execFileSync('xdg-open', [url]);
154
+ } catch (_) {
155
+ process.stdout.write(`\n Could not open browser. URL: ${url}\n`);
156
+ }
157
+ }
158
+
159
+ await fetchPending();
160
+
161
+ // Set up raw mode for interactive input
162
+ if (!process.stdin.isTTY) {
163
+ console.error('Error: Interactive mode requires a TTY. Use dashclaw approve/deny for non-interactive use.');
164
+ process.exit(1);
165
+ }
166
+
167
+ process.stdin.setRawMode(true);
168
+ process.stdin.resume();
169
+ process.stdin.setEncoding('utf8');
170
+ hideCursor();
171
+
172
+ // Ensure cleanup on exit
173
+ function cleanup() {
174
+ showCursor();
175
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
176
+ process.stdout.write('\n');
177
+ }
178
+ process.on('exit', cleanup);
179
+ process.on('SIGINT', () => process.exit(0));
180
+
181
+ render();
182
+
183
+ let busy = false;
184
+
185
+ process.stdin.on('data', async (key) => {
186
+ if (busy) return;
187
+
188
+ // Ctrl+C
189
+ if (key === '\x03') {
190
+ process.exit(0);
191
+ }
192
+
193
+ // Arrow keys: escape sequences
194
+ if (key === '\x1b[A') {
195
+ // Up
196
+ if (selected > 0) selected--;
197
+ render();
198
+ return;
199
+ }
200
+ if (key === '\x1b[B') {
201
+ // Down
202
+ if (selected < items.length - 1) selected++;
203
+ render();
204
+ return;
205
+ }
206
+
207
+ const ch = key.toLowerCase();
208
+
209
+ if (ch === 'q') {
210
+ process.exit(0);
211
+ }
212
+
213
+ if (ch === 'r') {
214
+ busy = true;
215
+ await fetchPending();
216
+ selected = Math.min(selected, Math.max(0, items.length - 1));
217
+ render();
218
+ busy = false;
219
+ return;
220
+ }
221
+
222
+ if (items.length === 0) return;
223
+ const current = items[selected];
224
+ const actionId = current.action_id || current.id;
225
+
226
+ if (ch === 'a') {
227
+ busy = true;
228
+ try {
229
+ await claw.approveAction(actionId, 'allow');
230
+ items.splice(selected, 1);
231
+ selected = Math.min(selected, Math.max(0, items.length - 1));
232
+ } catch (err) {
233
+ moveCursor(items.length + 5, 1);
234
+ process.stdout.write(red(` Error: ${err.message}`) + '\n');
235
+ }
236
+ render();
237
+ busy = false;
238
+ return;
239
+ }
240
+
241
+ if (ch === 'd') {
242
+ busy = true;
243
+ try {
244
+ await claw.approveAction(actionId, 'deny');
245
+ items.splice(selected, 1);
246
+ selected = Math.min(selected, Math.max(0, items.length - 1));
247
+ } catch (err) {
248
+ moveCursor(items.length + 5, 1);
249
+ process.stdout.write(red(` Error: ${err.message}`) + '\n');
250
+ }
251
+ render();
252
+ busy = false;
253
+ return;
254
+ }
255
+
256
+ if (ch === 'o') {
257
+ openReplay(actionId);
258
+ return;
259
+ }
260
+ });
261
+ }
262
+
263
+ // -- Router -------------------------------------------------------------------
264
+
265
+ switch (command) {
266
+ case 'approvals':
267
+ cmdApprovals();
268
+ break;
269
+ case 'approve':
270
+ cmdApprove();
271
+ break;
272
+ case 'deny':
273
+ cmdDeny();
274
+ break;
275
+ case 'help':
276
+ case '--help':
277
+ case '-h':
278
+ cmdHelp();
279
+ break;
280
+ default:
281
+ console.error(`Unknown command: ${command}`);
282
+ cmdHelp();
283
+ process.exit(1);
284
+ }
package/lib/render.js ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Terminal rendering helpers for DashClaw CLI.
3
+ * Uses only ANSI escape codes — no external dependencies.
4
+ */
5
+
6
+ const ESC = '\x1b[';
7
+
8
+ export function bold(str) {
9
+ return `${ESC}1m${str}${ESC}0m`;
10
+ }
11
+
12
+ export function dim(str) {
13
+ return `${ESC}2m${str}${ESC}0m`;
14
+ }
15
+
16
+ export function inverse(str) {
17
+ return `${ESC}7m${str}${ESC}0m`;
18
+ }
19
+
20
+ export function green(str) {
21
+ return `${ESC}32m${str}${ESC}0m`;
22
+ }
23
+
24
+ export function yellow(str) {
25
+ return `${ESC}33m${str}${ESC}0m`;
26
+ }
27
+
28
+ export function red(str) {
29
+ return `${ESC}31m${str}${ESC}0m`;
30
+ }
31
+
32
+ export function colorByRisk(score) {
33
+ if (score >= 70) return red(String(score));
34
+ if (score >= 40) return yellow(String(score));
35
+ return green(String(score));
36
+ }
37
+
38
+ export function clearScreen() {
39
+ process.stdout.write(`${ESC}2J${ESC}H`);
40
+ }
41
+
42
+ export function moveCursor(row, col) {
43
+ process.stdout.write(`${ESC}${row};${col}H`);
44
+ }
45
+
46
+ export function hideCursor() {
47
+ process.stdout.write(`${ESC}?25l`);
48
+ }
49
+
50
+ export function showCursor() {
51
+ process.stdout.write(`${ESC}?25h`);
52
+ }
53
+
54
+ export function printApprovalBlock(action, baseUrl) {
55
+ const actionId = action.action_id || action.id || 'unknown';
56
+ const actionType = action.action_type || 'unknown';
57
+ const riskScore = action.risk_score != null ? String(action.risk_score) : '-';
58
+ const goal = action.declared_goal || '-';
59
+ const agentId = action.agent_id || '-';
60
+ const replayUrl = `${baseUrl}/replay/${actionId}`;
61
+
62
+ const lines = [
63
+ '\u2554\u2550\u2550 DashClaw Approval Required \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557',
64
+ ` Action ID: ${actionId}`,
65
+ ` Agent: ${agentId}`,
66
+ ` Action: ${actionType}`,
67
+ ' Policy: require_approval',
68
+ ` Risk Score: ${riskScore}`,
69
+ ` Goal: ${goal}`,
70
+ '',
71
+ ` Replay: ${replayUrl}`,
72
+ '',
73
+ ' Waiting for approval... (Ctrl+C to abort)',
74
+ '\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d',
75
+ ];
76
+ process.stdout.write('\n' + lines.join('\n') + '\n\n');
77
+ }
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@dashclaw/cli",
3
+ "version": "0.1.0",
4
+ "description": "DashClaw terminal approval client",
5
+ "type": "module",
6
+ "bin": {
7
+ "dashclaw": "./bin/dashclaw.js"
8
+ },
9
+ "files": ["bin/", "lib/"],
10
+ "engines": { "node": ">=18.0.0" },
11
+ "dependencies": {
12
+ "dashclaw": "*"
13
+ },
14
+ "license": "MIT",
15
+ "publishConfig": {
16
+ "access": "public"
17
+ }
18
+ }