@dashclaw/cli 0.3.1 → 0.4.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/README.md +86 -7
- package/bin/dashclaw.js +495 -367
- package/lib/claude/install.js +386 -0
- package/lib/code/ingest.js +17 -1
- package/lib/config.js +2 -2
- package/lib/cost.js +108 -0
- package/lib/doctor.js +255 -93
- package/lib/env.js +69 -0
- package/lib/local-doctor.js +532 -0
- package/package.json +34 -21
package/bin/dashclaw.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { execFileSync } from 'child_process';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
4
5
|
import { dirname, resolve } from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { DashClaw } from 'dashclaw';
|
|
@@ -10,7 +11,7 @@ import {
|
|
|
10
11
|
green, red,
|
|
11
12
|
} from '../lib/render.js';
|
|
12
13
|
import { runDoctor as runDoctorCommand } from '../lib/doctor.js';
|
|
13
|
-
import { resolveConfig, clearConfigFile, configPath } from '../lib/config.js';
|
|
14
|
+
import { resolveConfig, clearConfigFile, configPath, ask, askSecret } from '../lib/config.js';
|
|
14
15
|
import { runIngest, defaultClaudeProjectsDir } from '../lib/code/ingest.js';
|
|
15
16
|
import { runMemo } from '../lib/code/memo.js';
|
|
16
17
|
import { runApply } from '../lib/code/apply.js';
|
|
@@ -20,13 +21,18 @@ import {
|
|
|
20
21
|
defaultCodexOutDir,
|
|
21
22
|
} from '../lib/code/ingest-codex.js';
|
|
22
23
|
import { installCodex, codexConfigPath, codexHooksDir } from '../lib/codex/install.js';
|
|
24
|
+
import { installClaude } from '../lib/claude/install.js';
|
|
25
|
+
import { runCost } from '../lib/cost.js';
|
|
23
26
|
import { runCodexNotify } from '../lib/codex/notify.js';
|
|
24
27
|
import { apiRequest } from '../lib/api.js';
|
|
25
28
|
import { fetchPosture, fetchFindings, fetchNext, resolveFinding } from '../lib/posture.js';
|
|
29
|
+
import { fetchAgentEnv, splitEnvArgv, formatEnvNames, runWithEnv } from '../lib/env.js';
|
|
26
30
|
|
|
27
31
|
process.on('unhandledRejection', (reason) => {
|
|
28
32
|
console.error('Unhandled Rejection:', reason);
|
|
29
|
-
process.exit
|
|
33
|
+
// exitCode (not process.exit) — a hard exit during in-flight I/O trips a
|
|
34
|
+
// libuv teardown assert on Windows.
|
|
35
|
+
process.exitCode = 1;
|
|
30
36
|
});
|
|
31
37
|
|
|
32
38
|
// -- Config -------------------------------------------------------------------
|
|
@@ -54,6 +60,17 @@ function getFlag(name) {
|
|
|
54
60
|
return undefined;
|
|
55
61
|
}
|
|
56
62
|
|
|
63
|
+
function incrementCount(counts, key) {
|
|
64
|
+
counts[key] = (counts[key] || 0) + 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function runSubcommand(table, sub, errorMessage) {
|
|
68
|
+
const handler = table[sub];
|
|
69
|
+
if (handler) return handler();
|
|
70
|
+
console.error(errorMessage(sub));
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
57
74
|
// -- Commands -----------------------------------------------------------------
|
|
58
75
|
|
|
59
76
|
async function cmdHelp() {
|
|
@@ -64,10 +81,12 @@ ${bold('Usage:')}
|
|
|
64
81
|
dashclaw approvals Interactive approval inbox
|
|
65
82
|
dashclaw approve <actionId> [--reason] Approve an action
|
|
66
83
|
dashclaw deny <actionId> [--reason] Deny an action
|
|
67
|
-
dashclaw doctor Diagnose
|
|
84
|
+
dashclaw doctor Diagnose your instance + this machine (report-only)
|
|
85
|
+
--fix Apply safe auto-fixes, then re-check and report
|
|
68
86
|
--json Output as JSON (for CI/scripts)
|
|
69
|
-
--no-fix
|
|
70
|
-
--category <list> Filter checks (e.g., database,config)
|
|
87
|
+
--no-fix Accepted no-op alias (report-only is the default)
|
|
88
|
+
--category <list> Filter remote checks (e.g., database,config)
|
|
89
|
+
--repo <path> Treat <path> as the DashClaw checkout for repo checks
|
|
71
90
|
dashclaw code ingest [--dry-run] Backfill Claude Code transcripts from ~/.claude/projects
|
|
72
91
|
--projects-dir <path> Override the default scan directory
|
|
73
92
|
dashclaw code ingest-codex [--dry-run] Backfill Codex transcripts from ~/.codex/sessions
|
|
@@ -81,6 +100,10 @@ ${bold('Usage:')}
|
|
|
81
100
|
--yes Overwrite existing files when manifest says so
|
|
82
101
|
--allow-redactions Write files that contain redacted secret patterns
|
|
83
102
|
--overwrite Clobber existing .NEW side-by-side files
|
|
103
|
+
dashclaw install claude [--trial] Provision DashClaw governance into Claude Code
|
|
104
|
+
--endpoint <url> Your DashClaw instance URL (or DASHCLAW_BASE_URL)
|
|
105
|
+
--key <key> API key (or DASHCLAW_API_KEY; --trial prompts via browser signup)
|
|
106
|
+
--agent-id <id> Agent id for governed actions (default: claude-code)
|
|
84
107
|
dashclaw install codex Provision DashClaw governance into Codex CLI
|
|
85
108
|
--project <path> Project to receive AGENTS.md (default: cwd)
|
|
86
109
|
--approval-policy <p> Codex approval_policy (default: on-request)
|
|
@@ -106,11 +129,17 @@ ${bold('Usage:')}
|
|
|
106
129
|
dashclaw behavior status Behavior Learning sample status (local recorder)
|
|
107
130
|
dashclaw behavior suggestions Evidence-backed policy suggestions per agent
|
|
108
131
|
--agent-id <id> Filter to one agent
|
|
132
|
+
dashclaw env [--agent <id>] -- <cmd...> Run <cmd> with managed secrets injected (memory-only)
|
|
133
|
+
Without -- <cmd>: list secret NAMES + count (never values)
|
|
134
|
+
dashclaw cost Spend readback from /api/finops/spend
|
|
135
|
+
--lens fleet|claude-code Which rollup (default: claude-code)
|
|
136
|
+
--period 7d|30d|90d Window (default: 7d)
|
|
109
137
|
dashclaw posture Governance posture score + remediation queue
|
|
110
138
|
dashclaw posture resolve <key> Draft a fix (inactive) | --snooze | --accept-risk
|
|
111
139
|
--note "..." Attach a note to the resolution
|
|
112
140
|
dashclaw next The single top open governance gap + its fix
|
|
113
141
|
dashclaw logout Remove saved config (~/.dashclaw/config.json)
|
|
142
|
+
dashclaw version Print the CLI version
|
|
114
143
|
dashclaw help Show this help
|
|
115
144
|
|
|
116
145
|
${bold('Config:')}
|
|
@@ -120,6 +149,12 @@ ${bold('Config:')}
|
|
|
120
149
|
`);
|
|
121
150
|
}
|
|
122
151
|
|
|
152
|
+
async function cmdVersion() {
|
|
153
|
+
const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
154
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
155
|
+
console.log(pkg.version);
|
|
156
|
+
}
|
|
157
|
+
|
|
123
158
|
async function cmdLogout() {
|
|
124
159
|
const removed = clearConfigFile();
|
|
125
160
|
if (removed) {
|
|
@@ -131,15 +166,23 @@ async function cmdLogout() {
|
|
|
131
166
|
|
|
132
167
|
async function cmdDoctor() {
|
|
133
168
|
const jsonFlag = args.includes('--json');
|
|
169
|
+
const fixFlag = args.includes('--fix');
|
|
134
170
|
const noFixFlag = args.includes('--no-fix');
|
|
135
171
|
const catIdx = args.indexOf('--category');
|
|
136
172
|
const catValue = catIdx !== -1 ? args[catIdx + 1] : undefined;
|
|
173
|
+
const repoIdx = args.indexOf('--repo');
|
|
174
|
+
const repoValue = repoIdx !== -1 ? args[repoIdx + 1] : undefined;
|
|
175
|
+
const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
176
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
137
177
|
await runDoctorCommand({
|
|
138
178
|
baseUrl,
|
|
139
179
|
apiKey,
|
|
140
180
|
json: jsonFlag,
|
|
181
|
+
fix: fixFlag,
|
|
141
182
|
noFix: noFixFlag,
|
|
142
183
|
category: catValue,
|
|
184
|
+
repo: repoValue,
|
|
185
|
+
cliVersion: pkg.version,
|
|
143
186
|
});
|
|
144
187
|
}
|
|
145
188
|
|
|
@@ -181,211 +224,192 @@ async function cmdDeny() {
|
|
|
181
224
|
}
|
|
182
225
|
}
|
|
183
226
|
|
|
184
|
-
async function
|
|
185
|
-
|
|
227
|
+
async function fetchPendingApprovals(state) {
|
|
228
|
+
try {
|
|
229
|
+
const result = await state.claw.getPendingApprovals(50);
|
|
230
|
+
state.items = result.actions || [];
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.error(`Error fetching approvals: ${err.message}`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
186
236
|
|
|
187
|
-
|
|
188
|
-
|
|
237
|
+
function approvalId(action) {
|
|
238
|
+
return action.action_id || action.id;
|
|
239
|
+
}
|
|
189
240
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
241
|
+
function renderApprovalRow(action, index, selected) {
|
|
242
|
+
const type = action.action_type || '-';
|
|
243
|
+
const agent = action.agent_id || '-';
|
|
244
|
+
const goal = (action.declared_goal || '-').slice(0, 60);
|
|
245
|
+
const risk = action.risk_score != null ? colorByRisk(action.risk_score) : dim('-');
|
|
246
|
+
const line = ` [${index + 1}] ${type} | ${agent} | ${goal} | risk: ${risk}`;
|
|
247
|
+
process.stdout.write((index === selected ? inverse(line) : line) + '\n');
|
|
248
|
+
}
|
|
199
249
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
250
|
+
function renderApprovals(state) {
|
|
251
|
+
clearScreen();
|
|
252
|
+
moveCursor(1, 1);
|
|
253
|
+
process.stdout.write(bold('DashClaw Approval Inbox') + '\n\n');
|
|
204
254
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const id = a.action_id || a.id || '?';
|
|
212
|
-
const type = a.action_type || '-';
|
|
213
|
-
const agent = a.agent_id || '-';
|
|
214
|
-
const goal = (a.declared_goal || '-').slice(0, 60);
|
|
215
|
-
const risk = a.risk_score != null ? colorByRisk(a.risk_score) : dim('-');
|
|
216
|
-
|
|
217
|
-
const line = ` [${i + 1}] ${type} | ${agent} | ${goal} | risk: ${risk}`;
|
|
218
|
-
process.stdout.write((i === selected ? inverse(line) : line) + '\n');
|
|
219
|
-
}
|
|
220
|
-
}
|
|
255
|
+
if (state.items.length === 0) {
|
|
256
|
+
process.stdout.write(dim(' No pending approvals.\n'));
|
|
257
|
+
process.stdout.write(dim(' Press R to refresh, Q to quit.\n'));
|
|
258
|
+
} else {
|
|
259
|
+
state.items.forEach((action, index) => renderApprovalRow(action, index, state.selected));
|
|
260
|
+
}
|
|
221
261
|
|
|
222
|
-
|
|
262
|
+
process.stdout.write('\n' + dim(' [A] Approve [D] Deny [R] Refresh [O] Open Replay [Q] Quit') + '\n');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function replayUrlIsSafe(url) {
|
|
266
|
+
let parsed;
|
|
267
|
+
try {
|
|
268
|
+
parsed = new URL(url);
|
|
269
|
+
} catch {
|
|
270
|
+
return false;
|
|
223
271
|
}
|
|
272
|
+
const protocolOk = parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
273
|
+
return protocolOk && parsed.hostname && !/\s/.test(url) && !/[&|><^"'`]/.test(url);
|
|
274
|
+
}
|
|
224
275
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
process.stdout.write(`\n Invalid URL, cannot open browser.\n`);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
if (/\s/.test(url)) {
|
|
244
|
-
process.stdout.write(`\n Invalid URL, cannot open browser.\n`);
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
// Disallow characters that are dangerous when passed through a shell
|
|
248
|
-
if (/[&|><^"'`]/.test(url)) {
|
|
249
|
-
process.stdout.write(`\n Invalid URL, cannot open browser.\n`);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
try {
|
|
253
|
-
const platform = process.platform;
|
|
254
|
-
if (platform === 'darwin') {
|
|
255
|
-
execFileSync('open', [url]);
|
|
256
|
-
} else if (platform === 'win32') {
|
|
257
|
-
// Use PowerShell Start-Process instead of relying on cmd.exe parsing
|
|
258
|
-
execFileSync('powershell', ['-NoProfile', '-Command', 'Start-Process', url]);
|
|
259
|
-
} else {
|
|
260
|
-
execFileSync('xdg-open', [url]);
|
|
261
|
-
}
|
|
262
|
-
} catch (_) {
|
|
263
|
-
process.stdout.write(`\n Could not open browser. URL: ${url}\n`);
|
|
276
|
+
function openReplay(actionId) {
|
|
277
|
+
const url = `${baseUrl}/replay/${actionId}`;
|
|
278
|
+
if (!replayUrlIsSafe(url)) {
|
|
279
|
+
process.stdout.write(`\n Invalid URL, cannot open browser.\n`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const platform = process.platform;
|
|
284
|
+
if (platform === 'darwin') {
|
|
285
|
+
execFileSync('open', [url]);
|
|
286
|
+
} else if (platform === 'win32') {
|
|
287
|
+
// Use PowerShell Start-Process instead of relying on cmd.exe parsing
|
|
288
|
+
execFileSync('powershell', ['-NoProfile', '-Command', 'Start-Process', url]);
|
|
289
|
+
} else {
|
|
290
|
+
execFileSync('xdg-open', [url]);
|
|
264
291
|
}
|
|
292
|
+
} catch (_) {
|
|
293
|
+
process.stdout.write(`\n Could not open browser. URL: ${url}\n`);
|
|
265
294
|
}
|
|
295
|
+
}
|
|
266
296
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// Open SSE stream for live push of new approval requests
|
|
270
|
-
let stream = null;
|
|
297
|
+
function startApprovalStream(state) {
|
|
271
298
|
try {
|
|
272
|
-
stream = claw.events()
|
|
273
|
-
.on('guard.decision.created', (data) =>
|
|
274
|
-
if (data.decision !== 'require_approval') return;
|
|
275
|
-
const exists = items.some((it) => (it.action_id || it.id) === data.action_id);
|
|
276
|
-
if (exists) return;
|
|
277
|
-
items.push(data);
|
|
278
|
-
render();
|
|
279
|
-
})
|
|
299
|
+
state.stream = state.claw.events()
|
|
300
|
+
.on('guard.decision.created', (data) => handleApprovalPush(state, data))
|
|
280
301
|
.on('error', () => {
|
|
281
|
-
moveCursor(items.length + 6, 1);
|
|
302
|
+
moveCursor(state.items.length + 6, 1);
|
|
282
303
|
process.stdout.write(dim(' SSE stream error — live push unavailable, use R to refresh') + '\n');
|
|
283
304
|
});
|
|
284
305
|
} catch (_) {
|
|
285
306
|
// SSE unavailable — inbox still works via manual refresh
|
|
286
307
|
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function handleApprovalPush(state, data) {
|
|
311
|
+
if (data.decision !== 'require_approval') return;
|
|
312
|
+
const exists = state.items.some((it) => approvalId(it) === data.action_id);
|
|
313
|
+
if (exists) return;
|
|
314
|
+
state.items.push(data);
|
|
315
|
+
renderApprovals(state);
|
|
316
|
+
}
|
|
287
317
|
|
|
288
|
-
|
|
318
|
+
function ensureApprovalTty() {
|
|
289
319
|
if (!process.stdin.isTTY) {
|
|
290
320
|
console.error('Error: Interactive mode requires a TTY. Use dashclaw approve/deny for non-interactive use.');
|
|
291
321
|
process.exit(1);
|
|
292
322
|
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function selectedWithinItems(state) {
|
|
326
|
+
state.selected = Math.min(state.selected, Math.max(0, state.items.length - 1));
|
|
327
|
+
}
|
|
293
328
|
|
|
329
|
+
function setupApprovalTerminal(state) {
|
|
330
|
+
ensureApprovalTty();
|
|
294
331
|
process.stdin.setRawMode(true);
|
|
295
332
|
process.stdin.resume();
|
|
296
333
|
process.stdin.setEncoding('utf8');
|
|
297
334
|
hideCursor();
|
|
298
|
-
|
|
299
|
-
// Ensure cleanup on exit
|
|
300
|
-
function cleanup() {
|
|
301
|
-
if (stream) stream.close();
|
|
302
|
-
showCursor();
|
|
303
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
304
|
-
process.stdout.write('\n');
|
|
305
|
-
}
|
|
306
|
-
process.on('exit', cleanup);
|
|
335
|
+
process.on('exit', () => cleanupApprovalInbox(state));
|
|
307
336
|
process.on('SIGINT', () => process.exit(0));
|
|
337
|
+
}
|
|
308
338
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
339
|
+
function cleanupApprovalInbox(state) {
|
|
340
|
+
if (state.stream) state.stream.close();
|
|
341
|
+
showCursor();
|
|
342
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
343
|
+
process.stdout.write('\n');
|
|
344
|
+
}
|
|
312
345
|
|
|
313
|
-
|
|
314
|
-
|
|
346
|
+
async function withApprovalBusy(state, fn) {
|
|
347
|
+
state.busy = true;
|
|
348
|
+
try {
|
|
349
|
+
await fn();
|
|
350
|
+
} finally {
|
|
351
|
+
state.busy = false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
315
354
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
355
|
+
async function refreshApprovals(state) {
|
|
356
|
+
await fetchPendingApprovals(state);
|
|
357
|
+
selectedWithinItems(state);
|
|
358
|
+
renderApprovals(state);
|
|
359
|
+
}
|
|
320
360
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
361
|
+
async function decideApproval(state, decision) {
|
|
362
|
+
const actionId = approvalId(state.items[state.selected]);
|
|
363
|
+
try {
|
|
364
|
+
await state.claw.approveAction(actionId, decision);
|
|
365
|
+
state.items.splice(state.selected, 1);
|
|
366
|
+
selectedWithinItems(state);
|
|
367
|
+
} catch (err) {
|
|
368
|
+
moveCursor(state.items.length + 5, 1);
|
|
369
|
+
process.stdout.write(red(` Error: ${err.message}`) + '\n');
|
|
370
|
+
}
|
|
371
|
+
renderApprovals(state);
|
|
372
|
+
}
|
|
334
373
|
|
|
335
|
-
|
|
374
|
+
function moveApprovalSelection(state, delta) {
|
|
375
|
+
const next = state.selected + delta;
|
|
376
|
+
state.selected = Math.max(0, Math.min(next, state.items.length - 1));
|
|
377
|
+
renderApprovals(state);
|
|
378
|
+
}
|
|
336
379
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
380
|
+
const EMPTY_APPROVAL_KEY_ACTIONS = {
|
|
381
|
+
q: () => process.exit(0),
|
|
382
|
+
r: (state) => withApprovalBusy(state, () => refreshApprovals(state)),
|
|
383
|
+
};
|
|
340
384
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
385
|
+
const APPROVAL_KEY_ACTIONS = {
|
|
386
|
+
...EMPTY_APPROVAL_KEY_ACTIONS,
|
|
387
|
+
a: (state) => withApprovalBusy(state, () => decideApproval(state, 'allow')),
|
|
388
|
+
d: (state) => withApprovalBusy(state, () => decideApproval(state, 'deny')),
|
|
389
|
+
o: (state) => openReplay(approvalId(state.items[state.selected])),
|
|
390
|
+
};
|
|
349
391
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
await claw.approveAction(actionId, 'allow');
|
|
358
|
-
items.splice(selected, 1);
|
|
359
|
-
selected = Math.min(selected, Math.max(0, items.length - 1));
|
|
360
|
-
} catch (err) {
|
|
361
|
-
moveCursor(items.length + 5, 1);
|
|
362
|
-
process.stdout.write(red(` Error: ${err.message}`) + '\n');
|
|
363
|
-
}
|
|
364
|
-
render();
|
|
365
|
-
busy = false;
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
392
|
+
function approvalActionFor(state, key) {
|
|
393
|
+
if (key === '\x03') return () => process.exit(0);
|
|
394
|
+
if (key === '\x1b[A') return () => moveApprovalSelection(state, -1);
|
|
395
|
+
if (key === '\x1b[B') return () => moveApprovalSelection(state, 1);
|
|
396
|
+
const actions = state.items.length === 0 ? EMPTY_APPROVAL_KEY_ACTIONS : APPROVAL_KEY_ACTIONS;
|
|
397
|
+
return actions[key.toLowerCase()];
|
|
398
|
+
}
|
|
368
399
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
selected = Math.min(selected, Math.max(0, items.length - 1));
|
|
375
|
-
} catch (err) {
|
|
376
|
-
moveCursor(items.length + 5, 1);
|
|
377
|
-
process.stdout.write(red(` Error: ${err.message}`) + '\n');
|
|
378
|
-
}
|
|
379
|
-
render();
|
|
380
|
-
busy = false;
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
400
|
+
async function handleApprovalKey(state, key) {
|
|
401
|
+
if (state.busy) return;
|
|
402
|
+
const action = approvalActionFor(state, key);
|
|
403
|
+
if (action) return action(state);
|
|
404
|
+
}
|
|
383
405
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
406
|
+
async function cmdApprovals() {
|
|
407
|
+
const state = { claw: createClient(), items: [], selected: 0, busy: false, stream: null };
|
|
408
|
+
await fetchPendingApprovals(state);
|
|
409
|
+
startApprovalStream(state);
|
|
410
|
+
setupApprovalTerminal(state);
|
|
411
|
+
renderApprovals(state);
|
|
412
|
+
process.stdin.on('data', (key) => handleApprovalKey(state, key));
|
|
389
413
|
}
|
|
390
414
|
|
|
391
415
|
// -- install subcommand group ------------------------------------------------
|
|
@@ -423,15 +447,63 @@ async function cmdInstallCodex() {
|
|
|
423
447
|
}
|
|
424
448
|
}
|
|
425
449
|
|
|
450
|
+
async function cmdCost() {
|
|
451
|
+
const lens = getFlag('--lens') || 'claude-code';
|
|
452
|
+
const period = getFlag('--period') || '7d';
|
|
453
|
+
|
|
454
|
+
if (!baseUrl || !apiKey) {
|
|
455
|
+
console.error('Not configured. Run `dashclaw install claude` to set up,');
|
|
456
|
+
console.error('or set DASHCLAW_BASE_URL and DASHCLAW_API_KEY.');
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
console.log(await runCost({ baseUrl, apiKey }, { lens, period }));
|
|
462
|
+
} catch (err) {
|
|
463
|
+
if (err.usage) {
|
|
464
|
+
console.error(err.message);
|
|
465
|
+
} else if (err.status === 401 || err.status === 403) {
|
|
466
|
+
console.error('API key was rejected (401/403). Re-run `dashclaw install claude` or check DASHCLAW_API_KEY.');
|
|
467
|
+
} else {
|
|
468
|
+
console.error(`Could not fetch spend from ${baseUrl}: ${err.message}`);
|
|
469
|
+
console.error('Check that the instance is running and reachable.');
|
|
470
|
+
}
|
|
471
|
+
// exitCode (not process.exit) so node drains the failed fetch socket —
|
|
472
|
+
// a hard exit here can trip a libuv teardown assert on Windows.
|
|
473
|
+
process.exitCode = 1;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function cmdInstallClaude() {
|
|
478
|
+
try {
|
|
479
|
+
const result = await installClaude({
|
|
480
|
+
endpoint: getFlag('--endpoint') || baseUrl,
|
|
481
|
+
apiKey: getFlag('--key') || apiKey,
|
|
482
|
+
agentId: getFlag('--agent-id') || undefined,
|
|
483
|
+
trial: args.includes('--trial'),
|
|
484
|
+
prompt: ask,
|
|
485
|
+
promptSecret: askSecret,
|
|
486
|
+
logger: console,
|
|
487
|
+
});
|
|
488
|
+
console.log();
|
|
489
|
+
console.log(` ${green('Done.')} Claude Code governance installed (mode: ${result.hookMode}).`);
|
|
490
|
+
} catch (err) {
|
|
491
|
+
console.error(red(`Error: ${err.message}`));
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
426
496
|
async function cmdInstall() {
|
|
427
497
|
const target = args[1];
|
|
428
498
|
switch (target) {
|
|
429
499
|
case 'codex':
|
|
430
500
|
return cmdInstallCodex();
|
|
501
|
+
case 'claude':
|
|
502
|
+
return cmdInstallClaude();
|
|
431
503
|
default:
|
|
432
504
|
console.error(`Unknown install target: dashclaw install ${target || '(missing)'}\n` +
|
|
433
|
-
'Try: dashclaw install
|
|
434
|
-
process.
|
|
505
|
+
'Try: dashclaw install claude [--trial] | dashclaw install codex [--project <path>]');
|
|
506
|
+
process.exitCode = 1;
|
|
435
507
|
}
|
|
436
508
|
}
|
|
437
509
|
|
|
@@ -445,26 +517,26 @@ async function cmdCodexNotify() {
|
|
|
445
517
|
// Skip the leading 'codex' and 'notify' tokens — runCodexNotify reads the
|
|
446
518
|
// JSON payload from the LAST argv slot (per Codex's notify contract).
|
|
447
519
|
const notifyArgv = args.slice(1); // includes 'notify' and the payload
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
520
|
+
try {
|
|
521
|
+
await runCodexNotify({
|
|
522
|
+
argv: notifyArgv,
|
|
523
|
+
baseUrl,
|
|
524
|
+
apiKey,
|
|
525
|
+
agentId: agentId || 'codex',
|
|
526
|
+
logger: console,
|
|
527
|
+
});
|
|
528
|
+
} catch {
|
|
529
|
+
// Best-effort by contract: Codex must never see a failure from its
|
|
530
|
+
// notify hook, so errors are swallowed and we still exit 0.
|
|
531
|
+
}
|
|
455
532
|
process.exit(0);
|
|
456
533
|
}
|
|
457
534
|
|
|
458
535
|
async function cmdCodex() {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
default:
|
|
464
|
-
console.error(`Unknown subcommand: dashclaw codex ${sub || '(missing)'}\n` +
|
|
465
|
-
'Try: dashclaw codex notify \'<json>\' (called by Codex notify config)');
|
|
466
|
-
process.exit(1);
|
|
467
|
-
}
|
|
536
|
+
return runSubcommand({
|
|
537
|
+
notify: cmdCodexNotify,
|
|
538
|
+
}, args[1], (sub) => `Unknown subcommand: dashclaw codex ${sub || '(missing)'}\n` +
|
|
539
|
+
'Try: dashclaw codex notify \'<json>\' (called by Codex notify config)');
|
|
468
540
|
}
|
|
469
541
|
|
|
470
542
|
// -- code subcommand group ---------------------------------------------------
|
|
@@ -487,24 +559,27 @@ async function cmdCodeIngestCodex() {
|
|
|
487
559
|
console.log('No sessions to ingest.');
|
|
488
560
|
return;
|
|
489
561
|
}
|
|
490
|
-
|
|
491
|
-
for (const r of results) {
|
|
492
|
-
if (r.status === 'written_local') written++;
|
|
493
|
-
else if (r.status === 'ingested') ingested++;
|
|
494
|
-
else if (r.status === 'dry_run') dryRunCount++;
|
|
495
|
-
else if (r.status === 'skipped') skipped++;
|
|
496
|
-
else if (r.status === 'error') {
|
|
497
|
-
errors++;
|
|
498
|
-
console.error(` ${red('error')} ${r.file}: ${r.reason}${r.detail ? ' — ' + r.detail : ''}`);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
562
|
+
const counts = tallyCodexIngestResults(results);
|
|
501
563
|
console.log();
|
|
502
|
-
console.log(`Done. Written: ${
|
|
503
|
-
if (!endpoint &&
|
|
564
|
+
console.log(`Done. Written: ${counts.written_local} Ingested: ${counts.ingested} Dry-run: ${counts.dry_run} Skipped: ${counts.skipped} Errors: ${counts.error}`);
|
|
565
|
+
if (!endpoint && counts.written_local > 0) {
|
|
504
566
|
console.log(dim(` Local sessions saved under ${outDir}.`));
|
|
505
567
|
console.log(dim(` Server-side codex ingest will be wired in a follow-up phase.`));
|
|
506
568
|
}
|
|
507
|
-
if (
|
|
569
|
+
if (counts.error > 0) process.exit(2);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function tallyCodexIngestResults(results) {
|
|
573
|
+
const counts = { written_local: 0, ingested: 0, dry_run: 0, skipped: 0, error: 0 };
|
|
574
|
+
for (const r of results) {
|
|
575
|
+
incrementCount(counts, r.status);
|
|
576
|
+
if (r.status === 'error') console.error(formatCodexIngestError(r));
|
|
577
|
+
}
|
|
578
|
+
return counts;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function formatCodexIngestError(result) {
|
|
582
|
+
return ` ${red('error')} ${result.file}: ${result.reason}${result.detail ? ' — ' + result.detail : ''}`;
|
|
508
583
|
}
|
|
509
584
|
|
|
510
585
|
async function cmdCodeIngest() {
|
|
@@ -518,17 +593,17 @@ async function cmdCodeIngest() {
|
|
|
518
593
|
dryRun,
|
|
519
594
|
});
|
|
520
595
|
if (!results.length) return;
|
|
521
|
-
|
|
522
|
-
let skipped = 0;
|
|
523
|
-
let errors = 0;
|
|
596
|
+
const counts = { ingested: 0, skipped: 0, error: 0 };
|
|
524
597
|
for (const r of results) {
|
|
525
|
-
if (r.status === '
|
|
526
|
-
|
|
527
|
-
else
|
|
598
|
+
if (r.status === 'skipped_unchanged' || r.status === 'dry_run') {
|
|
599
|
+
incrementCount(counts, 'skipped');
|
|
600
|
+
} else {
|
|
601
|
+
incrementCount(counts, r.status);
|
|
602
|
+
}
|
|
528
603
|
}
|
|
529
604
|
console.log();
|
|
530
|
-
console.log(`Done. Ingested: ${ingested} Skipped: ${skipped} Errors: ${
|
|
531
|
-
if (
|
|
605
|
+
console.log(`Done. Ingested: ${counts.ingested} Skipped: ${counts.skipped} Errors: ${counts.error}`);
|
|
606
|
+
if (counts.error > 0) process.exit(2);
|
|
532
607
|
}
|
|
533
608
|
|
|
534
609
|
async function cmdCodeMemo() {
|
|
@@ -578,24 +653,16 @@ async function cmdCodeApply() {
|
|
|
578
653
|
}
|
|
579
654
|
|
|
580
655
|
async function cmdCode() {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
default:
|
|
592
|
-
console.error(`Unknown subcommand: dashclaw code ${sub || '(missing)'}\n` +
|
|
593
|
-
'Try: dashclaw code ingest [--dry-run]\n' +
|
|
594
|
-
' dashclaw code ingest-codex [--dry-run] [--out <dir>] [--endpoint <url>]\n' +
|
|
595
|
-
' dashclaw code memo --project=<slug> [--save]\n' +
|
|
596
|
-
' dashclaw code apply <manifestId> --dest=<dir> [--yes]');
|
|
597
|
-
process.exit(1);
|
|
598
|
-
}
|
|
656
|
+
return runSubcommand({
|
|
657
|
+
ingest: cmdCodeIngest,
|
|
658
|
+
'ingest-codex': cmdCodeIngestCodex,
|
|
659
|
+
memo: cmdCodeMemo,
|
|
660
|
+
apply: cmdCodeApply,
|
|
661
|
+
}, args[1], (sub) => `Unknown subcommand: dashclaw code ${sub || '(missing)'}\n` +
|
|
662
|
+
'Try: dashclaw code ingest [--dry-run]\n' +
|
|
663
|
+
' dashclaw code ingest-codex [--dry-run] [--out <dir>] [--endpoint <url>]\n' +
|
|
664
|
+
' dashclaw code memo --project=<slug> [--save]\n' +
|
|
665
|
+
' dashclaw code apply <manifestId> --dest=<dir> [--yes]');
|
|
599
666
|
}
|
|
600
667
|
|
|
601
668
|
// -- prompts subcommand group ------------------------------------------------
|
|
@@ -839,36 +906,42 @@ function inboxClient() {
|
|
|
839
906
|
}
|
|
840
907
|
|
|
841
908
|
async function cmdInboxList() {
|
|
842
|
-
const unread = args.includes('--unread');
|
|
843
|
-
const limitFlag = getFlag('--limit');
|
|
844
909
|
try {
|
|
845
|
-
const data = await apiRequest(inboxClient(), 'GET', '/api/messages', {
|
|
846
|
-
|
|
847
|
-
agent_id: agentId,
|
|
848
|
-
direction: 'inbox',
|
|
849
|
-
unread: unread ? 'true' : undefined,
|
|
850
|
-
limit: limitFlag,
|
|
851
|
-
},
|
|
852
|
-
});
|
|
853
|
-
const messages = data.messages || [];
|
|
854
|
-
if (messages.length === 0) {
|
|
855
|
-
console.log(dim(' No messages.'));
|
|
856
|
-
} else {
|
|
857
|
-
for (const m of messages) {
|
|
858
|
-
const readMark = m.is_read ? dim('read') : green('unread');
|
|
859
|
-
console.log(
|
|
860
|
-
` ${bold(m.id)} ${dim('from')} ${m.from_agent_id || '-'} ${m.subject || dim('(no subject)')} [${readMark}]`,
|
|
861
|
-
);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
console.log();
|
|
865
|
-
console.log(dim(` unread: ${data.unread_count ?? 0}`));
|
|
910
|
+
const data = await apiRequest(inboxClient(), 'GET', '/api/messages', { query: inboxListQuery() });
|
|
911
|
+
renderInboxList(data);
|
|
866
912
|
} catch (err) {
|
|
867
913
|
console.error(`Error: ${err.message}`);
|
|
868
914
|
process.exit(1);
|
|
869
915
|
}
|
|
870
916
|
}
|
|
871
917
|
|
|
918
|
+
function inboxListQuery() {
|
|
919
|
+
return {
|
|
920
|
+
agent_id: agentId,
|
|
921
|
+
direction: 'inbox',
|
|
922
|
+
unread: args.includes('--unread') ? 'true' : undefined,
|
|
923
|
+
limit: getFlag('--limit'),
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function renderInboxList(data) {
|
|
928
|
+
const messages = data.messages || [];
|
|
929
|
+
if (messages.length === 0) {
|
|
930
|
+
console.log(dim(' No messages.'));
|
|
931
|
+
} else {
|
|
932
|
+
messages.forEach(renderInboxMessage);
|
|
933
|
+
}
|
|
934
|
+
console.log();
|
|
935
|
+
console.log(dim(` unread: ${data.unread_count ?? 0}`));
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function renderInboxMessage(message) {
|
|
939
|
+
const readMark = message.is_read ? dim('read') : green('unread');
|
|
940
|
+
console.log(
|
|
941
|
+
` ${bold(message.id)} ${dim('from')} ${message.from_agent_id || '-'} ${message.subject || dim('(no subject)')} [${readMark}]`,
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
|
|
872
945
|
async function cmdInboxUpdate(action) {
|
|
873
946
|
const ids = args.slice(2).filter((a) => !a.startsWith('--'));
|
|
874
947
|
if (ids.length === 0) {
|
|
@@ -887,21 +960,14 @@ async function cmdInboxUpdate(action) {
|
|
|
887
960
|
}
|
|
888
961
|
|
|
889
962
|
async function cmdInbox() {
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
default:
|
|
899
|
-
console.error(`Unknown subcommand: dashclaw inbox ${sub || '(missing)'}\n` +
|
|
900
|
-
'Try: dashclaw inbox list [--unread] [--limit N]\n' +
|
|
901
|
-
' dashclaw inbox read <id> [<id> ...]\n' +
|
|
902
|
-
' dashclaw inbox archive <id> [<id> ...]');
|
|
903
|
-
process.exit(1);
|
|
904
|
-
}
|
|
963
|
+
return runSubcommand({
|
|
964
|
+
list: cmdInboxList,
|
|
965
|
+
read: () => cmdInboxUpdate('read'),
|
|
966
|
+
archive: () => cmdInboxUpdate('archive'),
|
|
967
|
+
}, args[1], (sub) => `Unknown subcommand: dashclaw inbox ${sub || '(missing)'}\n` +
|
|
968
|
+
'Try: dashclaw inbox list [--unread] [--limit N]\n' +
|
|
969
|
+
' dashclaw inbox read <id> [<id> ...]\n' +
|
|
970
|
+
' dashclaw inbox archive <id> [<id> ...]');
|
|
905
971
|
}
|
|
906
972
|
|
|
907
973
|
// -- behavior subcommand group -----------------------------------------------
|
|
@@ -918,20 +984,28 @@ function behaviorClient() {
|
|
|
918
984
|
async function cmdBehaviorStatus() {
|
|
919
985
|
try {
|
|
920
986
|
const data = await apiRequest(behaviorClient(), 'GET', '/api/behavior/samples');
|
|
921
|
-
|
|
922
|
-
console.log(` Directory: ${dim(data.dir || '-')}`);
|
|
923
|
-
console.log(` Samples: ${bold(String(data.sample_count ?? 0))} ${dim('across')} ${data.agent_count ?? 0} ${dim('agent(s)')}`);
|
|
924
|
-
console.log(` Window: ${dim((data.oldest_ts || '-') + ' → ' + (data.newest_ts || '-'))}`);
|
|
925
|
-
console.log(` Ready: ${data.ready ? green('yes') : dim('no — need ' + (data.min_samples ?? 8) + '+ samples for an agent')}`);
|
|
926
|
-
for (const a of data.agents || []) {
|
|
927
|
-
console.log(` ${a.agent_id} ${dim(a.count + ' samples')}`);
|
|
928
|
-
}
|
|
987
|
+
renderBehaviorStatus(data);
|
|
929
988
|
} catch (err) {
|
|
930
989
|
console.error(`Error: ${err.message}`);
|
|
931
990
|
process.exit(1);
|
|
932
991
|
}
|
|
933
992
|
}
|
|
934
993
|
|
|
994
|
+
function behaviorReadyLabel(data) {
|
|
995
|
+
return data.ready ? green('yes') : dim('no — need ' + (data.min_samples ?? 8) + '+ samples for an agent');
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function renderBehaviorStatus(data) {
|
|
999
|
+
console.log(` Recorder: ${data.recorder_enabled ? green('on') : dim('off')}`);
|
|
1000
|
+
console.log(` Directory: ${dim(data.dir || '-')}`);
|
|
1001
|
+
console.log(` Samples: ${bold(String(data.sample_count ?? 0))} ${dim('across')} ${data.agent_count ?? 0} ${dim('agent(s)')}`);
|
|
1002
|
+
console.log(` Window: ${dim((data.oldest_ts || '-') + ' → ' + (data.newest_ts || '-'))}`);
|
|
1003
|
+
console.log(` Ready: ${behaviorReadyLabel(data)}`);
|
|
1004
|
+
for (const a of data.agents || []) {
|
|
1005
|
+
console.log(` ${a.agent_id} ${dim(a.count + ' samples')}`);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
935
1009
|
async function cmdBehaviorSuggestions() {
|
|
936
1010
|
const agent = getFlag('--agent-id') || agentId;
|
|
937
1011
|
try {
|
|
@@ -958,18 +1032,12 @@ async function cmdBehaviorSuggestions() {
|
|
|
958
1032
|
}
|
|
959
1033
|
|
|
960
1034
|
async function cmdBehavior() {
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
default:
|
|
968
|
-
console.error(`Unknown subcommand: dashclaw behavior ${sub || '(missing)'}\n` +
|
|
969
|
-
'Try: dashclaw behavior status\n' +
|
|
970
|
-
' dashclaw behavior suggestions [--agent-id X]');
|
|
971
|
-
process.exit(1);
|
|
972
|
-
}
|
|
1035
|
+
return runSubcommand({
|
|
1036
|
+
status: cmdBehaviorStatus,
|
|
1037
|
+
suggestions: cmdBehaviorSuggestions,
|
|
1038
|
+
}, args[1], (sub) => `Unknown subcommand: dashclaw behavior ${sub || '(missing)'}\n` +
|
|
1039
|
+
'Try: dashclaw behavior status\n' +
|
|
1040
|
+
' dashclaw behavior suggestions [--agent-id X]');
|
|
973
1041
|
}
|
|
974
1042
|
|
|
975
1043
|
// -- posture subcommand group ------------------------------------------------
|
|
@@ -993,31 +1061,47 @@ function printFinding(f, indent = ' ') {
|
|
|
993
1061
|
console.log(dim(`${indent} ${f.key}`));
|
|
994
1062
|
}
|
|
995
1063
|
|
|
1064
|
+
function postureStatusLabel(status) {
|
|
1065
|
+
if (status === 'healthy') return green(status);
|
|
1066
|
+
if (status === 'at_risk') return red(status);
|
|
1067
|
+
return status;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function renderPostureHeader(data) {
|
|
1071
|
+
console.log();
|
|
1072
|
+
console.log(` ${bold('Governance posture')} ${bold(String(data.score))}${dim('/100')} ${postureStatusLabel(data.status)}` +
|
|
1073
|
+
`${data.cappedBy ? ' ' + red('[capped: incident]') : ''}`);
|
|
1074
|
+
console.log(dim(` ${data.summary?.openFindings ?? 0} open · +${Math.round(data.summary?.pointsRecoverable ?? 0)} points recoverable`));
|
|
1075
|
+
console.log();
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function renderPostureDimensions(dimensions) {
|
|
1079
|
+
for (const d of dimensions || []) {
|
|
1080
|
+
const label = POSTURE_DIM_LABEL[d.dimension] || d.dimension;
|
|
1081
|
+
const mark = d.score < 70 ? red('!') : ' ';
|
|
1082
|
+
console.log(` ${mark} ${label.padEnd(12)} ${String(d.score).padStart(3)}`);
|
|
1083
|
+
}
|
|
1084
|
+
console.log();
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function renderPostureQueue(findings) {
|
|
1088
|
+
console.log(dim(` Next — ${findings.length} open`));
|
|
1089
|
+
if (findings.length === 0) {
|
|
1090
|
+
console.log(green(' Queue is clear — no open coverage gaps.'));
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
for (const f of findings.slice(0, 5)) printFinding(f);
|
|
1094
|
+
if (findings.length > 5) console.log(dim(` … ${findings.length - 5} more`));
|
|
1095
|
+
}
|
|
1096
|
+
|
|
996
1097
|
async function cmdPostureShow() {
|
|
997
1098
|
try {
|
|
998
1099
|
const client = postureClient();
|
|
999
1100
|
const [data, queue] = await Promise.all([fetchPosture(client), fetchFindings(client)]);
|
|
1000
|
-
const status = data.status === 'healthy' ? green(data.status)
|
|
1001
|
-
: data.status === 'at_risk' ? red(data.status) : data.status;
|
|
1002
|
-
console.log();
|
|
1003
|
-
console.log(` ${bold('Governance posture')} ${bold(String(data.score))}${dim('/100')} ${status}` +
|
|
1004
|
-
`${data.cappedBy ? ' ' + red('[capped: incident]') : ''}`);
|
|
1005
|
-
console.log(dim(` ${data.summary?.openFindings ?? 0} open · +${Math.round(data.summary?.pointsRecoverable ?? 0)} points recoverable`));
|
|
1006
|
-
console.log();
|
|
1007
|
-
for (const d of data.dimensions || []) {
|
|
1008
|
-
const label = POSTURE_DIM_LABEL[d.dimension] || d.dimension;
|
|
1009
|
-
const mark = d.score < 70 ? red('!') : ' ';
|
|
1010
|
-
console.log(` ${mark} ${label.padEnd(12)} ${String(d.score).padStart(3)}`);
|
|
1011
|
-
}
|
|
1012
|
-
console.log();
|
|
1013
1101
|
const findings = queue.findings || [];
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
} else {
|
|
1018
|
-
for (const f of findings.slice(0, 5)) printFinding(f);
|
|
1019
|
-
if (findings.length > 5) console.log(dim(` … ${findings.length - 5} more`));
|
|
1020
|
-
}
|
|
1102
|
+
renderPostureHeader(data);
|
|
1103
|
+
renderPostureDimensions(data.dimensions);
|
|
1104
|
+
renderPostureQueue(findings);
|
|
1021
1105
|
console.log();
|
|
1022
1106
|
} catch (err) {
|
|
1023
1107
|
console.error(`Error: ${err.message}`);
|
|
@@ -1077,18 +1161,79 @@ async function cmdNext() {
|
|
|
1077
1161
|
}
|
|
1078
1162
|
}
|
|
1079
1163
|
|
|
1164
|
+
// -- env command ---------------------------------------------------------------
|
|
1165
|
+
//
|
|
1166
|
+
// `dashclaw env [--agent <id>] -- <command...>` — managed-secrets delivery.
|
|
1167
|
+
// Fetches the delivery-enabled bundle (GET /api/secrets/env) and spawns the
|
|
1168
|
+
// child command with the values injected into its environment MEMORY-ONLY.
|
|
1169
|
+
// Secret VALUES are never printed, never written to disk, never echoed in
|
|
1170
|
+
// error paths; only NAMES are ever shown. Fail-closed: if the fetch fails,
|
|
1171
|
+
// the child is NOT run. Uses process.exitCode (not process.exit) so node
|
|
1172
|
+
// drains sockets — a hard exit can trip a libuv teardown assert on Windows.
|
|
1173
|
+
|
|
1174
|
+
async function cmdEnv() {
|
|
1175
|
+
const { flags, command: childArgv } = splitEnvArgv(args.slice(1));
|
|
1176
|
+
|
|
1177
|
+
if (flags.includes('--print')) {
|
|
1178
|
+
console.error('Error: --print is not supported. Secret values are memory-only by design —');
|
|
1179
|
+
console.error('they are injected into a child process environment, never printed or written to disk.');
|
|
1180
|
+
console.error('Use `dashclaw env` to list secret NAMES, or `dashclaw env -- <command>` to run with them.');
|
|
1181
|
+
process.exitCode = 1;
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
let agent = agentId;
|
|
1186
|
+
for (let i = 0; i < flags.length; i++) {
|
|
1187
|
+
if (flags[i] === '--agent') agent = flags[i + 1];
|
|
1188
|
+
else if (flags[i].startsWith('--agent=')) agent = flags[i].slice('--agent='.length);
|
|
1189
|
+
}
|
|
1190
|
+
if (!agent) {
|
|
1191
|
+
console.error('Error: no agent id. Pass --agent <id> or set DASHCLAW_AGENT_ID.');
|
|
1192
|
+
process.exitCode = 1;
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
let bundle;
|
|
1197
|
+
try {
|
|
1198
|
+
bundle = await fetchAgentEnv({ baseUrl, apiKey }, agent);
|
|
1199
|
+
} catch (err) {
|
|
1200
|
+
console.error(`Error: could not fetch the managed-secrets bundle: ${err.message}`);
|
|
1201
|
+
if (childArgv.length > 0) {
|
|
1202
|
+
console.error('Fail-closed: the command was NOT run without the requested env.');
|
|
1203
|
+
}
|
|
1204
|
+
process.exitCode = 1;
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (childArgv.length === 0) {
|
|
1209
|
+
console.log(formatEnvNames(bundle));
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
process.exitCode = await runWithEnv(bundle, childArgv);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1080
1216
|
// -- Router -------------------------------------------------------------------
|
|
1081
1217
|
|
|
1082
|
-
const COMMANDS_NEEDING_CONFIG = new Set(['approvals', 'approve', 'deny', 'doctor', 'code', 'prompts', 'inbox', 'behavior', 'posture', 'next']);
|
|
1218
|
+
const COMMANDS_NEEDING_CONFIG = new Set(['approvals', 'approve', 'deny', 'doctor', 'code', 'prompts', 'inbox', 'behavior', 'posture', 'next', 'env']);
|
|
1083
1219
|
// `install` deliberately omitted: provisioning hooks and AGENTS.md shouldn't
|
|
1084
1220
|
// require the user to have already configured API keys. If config happens to
|
|
1085
1221
|
// be present, install will pick up baseUrl for the AGENTS.md instance link.
|
|
1086
1222
|
// `codex notify` also opt-in: if no config, the notify fail-softs to skipped
|
|
1087
1223
|
// rather than erroring (Codex never sees the error anyway — it spawns with
|
|
1088
1224
|
// stdio nulled).
|
|
1089
|
-
|
|
1225
|
+
// `cost` resolves config non-interactively and prints its own setup hint when
|
|
1226
|
+
// missing (the generic interactive prompt would be the wrong UX for a
|
|
1227
|
+
// read-only readback command).
|
|
1228
|
+
const COMMANDS_OPTIONAL_CONFIG = new Set(['install', 'codex', 'cost']);
|
|
1229
|
+
|
|
1230
|
+
function applyConfig(config) {
|
|
1231
|
+
baseUrl = config.baseUrl;
|
|
1232
|
+
apiKey = config.apiKey;
|
|
1233
|
+
agentId = config.agentId;
|
|
1234
|
+
}
|
|
1090
1235
|
|
|
1091
|
-
async function
|
|
1236
|
+
async function loadCommandConfig() {
|
|
1092
1237
|
if (COMMANDS_NEEDING_CONFIG.has(command)) {
|
|
1093
1238
|
const config = await resolveConfig();
|
|
1094
1239
|
if (!config) {
|
|
@@ -1096,68 +1241,51 @@ async function main() {
|
|
|
1096
1241
|
console.error('Set them as env vars, save with an interactive first run, or use a .env file.');
|
|
1097
1242
|
process.exit(1);
|
|
1098
1243
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1244
|
+
applyConfig(config);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
if (COMMANDS_OPTIONAL_CONFIG.has(command)) {
|
|
1103
1248
|
const config = await resolveConfig({ interactive: false }).catch(() => null);
|
|
1104
1249
|
if (config) {
|
|
1105
|
-
|
|
1106
|
-
apiKey = config.apiKey;
|
|
1107
|
-
agentId = config.agentId;
|
|
1250
|
+
applyConfig(config);
|
|
1108
1251
|
}
|
|
1109
1252
|
}
|
|
1253
|
+
}
|
|
1110
1254
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
break;
|
|
1142
|
-
case 'behavior':
|
|
1143
|
-
await cmdBehavior();
|
|
1144
|
-
break;
|
|
1145
|
-
case 'posture':
|
|
1146
|
-
await cmdPosture();
|
|
1147
|
-
break;
|
|
1148
|
-
case 'next':
|
|
1149
|
-
await cmdNext();
|
|
1150
|
-
break;
|
|
1151
|
-
case 'help':
|
|
1152
|
-
case '--help':
|
|
1153
|
-
case '-h':
|
|
1154
|
-
await cmdHelp();
|
|
1155
|
-
break;
|
|
1156
|
-
default:
|
|
1157
|
-
console.error(`Unknown command: ${command}`);
|
|
1158
|
-
await cmdHelp();
|
|
1159
|
-
process.exit(1);
|
|
1255
|
+
const COMMAND_HANDLERS = {
|
|
1256
|
+
approvals: cmdApprovals,
|
|
1257
|
+
approve: cmdApprove,
|
|
1258
|
+
deny: cmdDeny,
|
|
1259
|
+
doctor: cmdDoctor,
|
|
1260
|
+
logout: cmdLogout,
|
|
1261
|
+
code: cmdCode,
|
|
1262
|
+
install: cmdInstall,
|
|
1263
|
+
codex: cmdCodex,
|
|
1264
|
+
prompts: cmdPrompts,
|
|
1265
|
+
inbox: cmdInbox,
|
|
1266
|
+
behavior: cmdBehavior,
|
|
1267
|
+
posture: cmdPosture,
|
|
1268
|
+
cost: cmdCost,
|
|
1269
|
+
next: cmdNext,
|
|
1270
|
+
env: cmdEnv,
|
|
1271
|
+
help: cmdHelp,
|
|
1272
|
+
'--help': cmdHelp,
|
|
1273
|
+
'-h': cmdHelp,
|
|
1274
|
+
version: cmdVersion,
|
|
1275
|
+
'--version': cmdVersion,
|
|
1276
|
+
'-v': cmdVersion,
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
async function main() {
|
|
1280
|
+
await loadCommandConfig();
|
|
1281
|
+
const handler = COMMAND_HANDLERS[command];
|
|
1282
|
+
if (handler) {
|
|
1283
|
+
await handler();
|
|
1284
|
+
return;
|
|
1160
1285
|
}
|
|
1286
|
+
console.error(`Unknown command: ${command}`);
|
|
1287
|
+
await cmdHelp();
|
|
1288
|
+
process.exitCode = 1;
|
|
1161
1289
|
}
|
|
1162
1290
|
|
|
1163
1291
|
main();
|