@dashclaw/cli 0.3.1 → 0.3.2
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 +76 -1
- package/bin/dashclaw.js +482 -364
- 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 +138 -77
- package/lib/env.js +69 -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() {
|
|
@@ -81,6 +98,10 @@ ${bold('Usage:')}
|
|
|
81
98
|
--yes Overwrite existing files when manifest says so
|
|
82
99
|
--allow-redactions Write files that contain redacted secret patterns
|
|
83
100
|
--overwrite Clobber existing .NEW side-by-side files
|
|
101
|
+
dashclaw install claude [--trial] Provision DashClaw governance into Claude Code
|
|
102
|
+
--endpoint <url> Your DashClaw instance URL (or DASHCLAW_BASE_URL)
|
|
103
|
+
--key <key> API key (or DASHCLAW_API_KEY; --trial prompts via browser signup)
|
|
104
|
+
--agent-id <id> Agent id for governed actions (default: claude-code)
|
|
84
105
|
dashclaw install codex Provision DashClaw governance into Codex CLI
|
|
85
106
|
--project <path> Project to receive AGENTS.md (default: cwd)
|
|
86
107
|
--approval-policy <p> Codex approval_policy (default: on-request)
|
|
@@ -106,11 +127,17 @@ ${bold('Usage:')}
|
|
|
106
127
|
dashclaw behavior status Behavior Learning sample status (local recorder)
|
|
107
128
|
dashclaw behavior suggestions Evidence-backed policy suggestions per agent
|
|
108
129
|
--agent-id <id> Filter to one agent
|
|
130
|
+
dashclaw env [--agent <id>] -- <cmd...> Run <cmd> with managed secrets injected (memory-only)
|
|
131
|
+
Without -- <cmd>: list secret NAMES + count (never values)
|
|
132
|
+
dashclaw cost Spend readback from /api/finops/spend
|
|
133
|
+
--lens fleet|claude-code Which rollup (default: claude-code)
|
|
134
|
+
--period 7d|30d|90d Window (default: 7d)
|
|
109
135
|
dashclaw posture Governance posture score + remediation queue
|
|
110
136
|
dashclaw posture resolve <key> Draft a fix (inactive) | --snooze | --accept-risk
|
|
111
137
|
--note "..." Attach a note to the resolution
|
|
112
138
|
dashclaw next The single top open governance gap + its fix
|
|
113
139
|
dashclaw logout Remove saved config (~/.dashclaw/config.json)
|
|
140
|
+
dashclaw version Print the CLI version
|
|
114
141
|
dashclaw help Show this help
|
|
115
142
|
|
|
116
143
|
${bold('Config:')}
|
|
@@ -120,6 +147,12 @@ ${bold('Config:')}
|
|
|
120
147
|
`);
|
|
121
148
|
}
|
|
122
149
|
|
|
150
|
+
async function cmdVersion() {
|
|
151
|
+
const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
152
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
153
|
+
console.log(pkg.version);
|
|
154
|
+
}
|
|
155
|
+
|
|
123
156
|
async function cmdLogout() {
|
|
124
157
|
const removed = clearConfigFile();
|
|
125
158
|
if (removed) {
|
|
@@ -181,211 +214,192 @@ async function cmdDeny() {
|
|
|
181
214
|
}
|
|
182
215
|
}
|
|
183
216
|
|
|
184
|
-
async function
|
|
185
|
-
|
|
217
|
+
async function fetchPendingApprovals(state) {
|
|
218
|
+
try {
|
|
219
|
+
const result = await state.claw.getPendingApprovals(50);
|
|
220
|
+
state.items = result.actions || [];
|
|
221
|
+
} catch (err) {
|
|
222
|
+
console.error(`Error fetching approvals: ${err.message}`);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
186
226
|
|
|
187
|
-
|
|
188
|
-
|
|
227
|
+
function approvalId(action) {
|
|
228
|
+
return action.action_id || action.id;
|
|
229
|
+
}
|
|
189
230
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
231
|
+
function renderApprovalRow(action, index, selected) {
|
|
232
|
+
const type = action.action_type || '-';
|
|
233
|
+
const agent = action.agent_id || '-';
|
|
234
|
+
const goal = (action.declared_goal || '-').slice(0, 60);
|
|
235
|
+
const risk = action.risk_score != null ? colorByRisk(action.risk_score) : dim('-');
|
|
236
|
+
const line = ` [${index + 1}] ${type} | ${agent} | ${goal} | risk: ${risk}`;
|
|
237
|
+
process.stdout.write((index === selected ? inverse(line) : line) + '\n');
|
|
238
|
+
}
|
|
199
239
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
240
|
+
function renderApprovals(state) {
|
|
241
|
+
clearScreen();
|
|
242
|
+
moveCursor(1, 1);
|
|
243
|
+
process.stdout.write(bold('DashClaw Approval Inbox') + '\n\n');
|
|
204
244
|
|
|
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
|
-
}
|
|
245
|
+
if (state.items.length === 0) {
|
|
246
|
+
process.stdout.write(dim(' No pending approvals.\n'));
|
|
247
|
+
process.stdout.write(dim(' Press R to refresh, Q to quit.\n'));
|
|
248
|
+
} else {
|
|
249
|
+
state.items.forEach((action, index) => renderApprovalRow(action, index, state.selected));
|
|
250
|
+
}
|
|
221
251
|
|
|
222
|
-
|
|
252
|
+
process.stdout.write('\n' + dim(' [A] Approve [D] Deny [R] Refresh [O] Open Replay [Q] Quit') + '\n');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function replayUrlIsSafe(url) {
|
|
256
|
+
let parsed;
|
|
257
|
+
try {
|
|
258
|
+
parsed = new URL(url);
|
|
259
|
+
} catch {
|
|
260
|
+
return false;
|
|
223
261
|
}
|
|
262
|
+
const protocolOk = parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
263
|
+
return protocolOk && parsed.hostname && !/\s/.test(url) && !/[&|><^"'`]/.test(url);
|
|
264
|
+
}
|
|
224
265
|
|
|
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`);
|
|
266
|
+
function openReplay(actionId) {
|
|
267
|
+
const url = `${baseUrl}/replay/${actionId}`;
|
|
268
|
+
if (!replayUrlIsSafe(url)) {
|
|
269
|
+
process.stdout.write(`\n Invalid URL, cannot open browser.\n`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
const platform = process.platform;
|
|
274
|
+
if (platform === 'darwin') {
|
|
275
|
+
execFileSync('open', [url]);
|
|
276
|
+
} else if (platform === 'win32') {
|
|
277
|
+
// Use PowerShell Start-Process instead of relying on cmd.exe parsing
|
|
278
|
+
execFileSync('powershell', ['-NoProfile', '-Command', 'Start-Process', url]);
|
|
279
|
+
} else {
|
|
280
|
+
execFileSync('xdg-open', [url]);
|
|
264
281
|
}
|
|
282
|
+
} catch (_) {
|
|
283
|
+
process.stdout.write(`\n Could not open browser. URL: ${url}\n`);
|
|
265
284
|
}
|
|
285
|
+
}
|
|
266
286
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// Open SSE stream for live push of new approval requests
|
|
270
|
-
let stream = null;
|
|
287
|
+
function startApprovalStream(state) {
|
|
271
288
|
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
|
-
})
|
|
289
|
+
state.stream = state.claw.events()
|
|
290
|
+
.on('guard.decision.created', (data) => handleApprovalPush(state, data))
|
|
280
291
|
.on('error', () => {
|
|
281
|
-
moveCursor(items.length + 6, 1);
|
|
292
|
+
moveCursor(state.items.length + 6, 1);
|
|
282
293
|
process.stdout.write(dim(' SSE stream error — live push unavailable, use R to refresh') + '\n');
|
|
283
294
|
});
|
|
284
295
|
} catch (_) {
|
|
285
296
|
// SSE unavailable — inbox still works via manual refresh
|
|
286
297
|
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function handleApprovalPush(state, data) {
|
|
301
|
+
if (data.decision !== 'require_approval') return;
|
|
302
|
+
const exists = state.items.some((it) => approvalId(it) === data.action_id);
|
|
303
|
+
if (exists) return;
|
|
304
|
+
state.items.push(data);
|
|
305
|
+
renderApprovals(state);
|
|
306
|
+
}
|
|
287
307
|
|
|
288
|
-
|
|
308
|
+
function ensureApprovalTty() {
|
|
289
309
|
if (!process.stdin.isTTY) {
|
|
290
310
|
console.error('Error: Interactive mode requires a TTY. Use dashclaw approve/deny for non-interactive use.');
|
|
291
311
|
process.exit(1);
|
|
292
312
|
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function selectedWithinItems(state) {
|
|
316
|
+
state.selected = Math.min(state.selected, Math.max(0, state.items.length - 1));
|
|
317
|
+
}
|
|
293
318
|
|
|
319
|
+
function setupApprovalTerminal(state) {
|
|
320
|
+
ensureApprovalTty();
|
|
294
321
|
process.stdin.setRawMode(true);
|
|
295
322
|
process.stdin.resume();
|
|
296
323
|
process.stdin.setEncoding('utf8');
|
|
297
324
|
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);
|
|
325
|
+
process.on('exit', () => cleanupApprovalInbox(state));
|
|
307
326
|
process.on('SIGINT', () => process.exit(0));
|
|
327
|
+
}
|
|
308
328
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
329
|
+
function cleanupApprovalInbox(state) {
|
|
330
|
+
if (state.stream) state.stream.close();
|
|
331
|
+
showCursor();
|
|
332
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
333
|
+
process.stdout.write('\n');
|
|
334
|
+
}
|
|
312
335
|
|
|
313
|
-
|
|
314
|
-
|
|
336
|
+
async function withApprovalBusy(state, fn) {
|
|
337
|
+
state.busy = true;
|
|
338
|
+
try {
|
|
339
|
+
await fn();
|
|
340
|
+
} finally {
|
|
341
|
+
state.busy = false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
315
344
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
345
|
+
async function refreshApprovals(state) {
|
|
346
|
+
await fetchPendingApprovals(state);
|
|
347
|
+
selectedWithinItems(state);
|
|
348
|
+
renderApprovals(state);
|
|
349
|
+
}
|
|
320
350
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
351
|
+
async function decideApproval(state, decision) {
|
|
352
|
+
const actionId = approvalId(state.items[state.selected]);
|
|
353
|
+
try {
|
|
354
|
+
await state.claw.approveAction(actionId, decision);
|
|
355
|
+
state.items.splice(state.selected, 1);
|
|
356
|
+
selectedWithinItems(state);
|
|
357
|
+
} catch (err) {
|
|
358
|
+
moveCursor(state.items.length + 5, 1);
|
|
359
|
+
process.stdout.write(red(` Error: ${err.message}`) + '\n');
|
|
360
|
+
}
|
|
361
|
+
renderApprovals(state);
|
|
362
|
+
}
|
|
334
363
|
|
|
335
|
-
|
|
364
|
+
function moveApprovalSelection(state, delta) {
|
|
365
|
+
const next = state.selected + delta;
|
|
366
|
+
state.selected = Math.max(0, Math.min(next, state.items.length - 1));
|
|
367
|
+
renderApprovals(state);
|
|
368
|
+
}
|
|
336
369
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
370
|
+
const EMPTY_APPROVAL_KEY_ACTIONS = {
|
|
371
|
+
q: () => process.exit(0),
|
|
372
|
+
r: (state) => withApprovalBusy(state, () => refreshApprovals(state)),
|
|
373
|
+
};
|
|
340
374
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
375
|
+
const APPROVAL_KEY_ACTIONS = {
|
|
376
|
+
...EMPTY_APPROVAL_KEY_ACTIONS,
|
|
377
|
+
a: (state) => withApprovalBusy(state, () => decideApproval(state, 'allow')),
|
|
378
|
+
d: (state) => withApprovalBusy(state, () => decideApproval(state, 'deny')),
|
|
379
|
+
o: (state) => openReplay(approvalId(state.items[state.selected])),
|
|
380
|
+
};
|
|
349
381
|
|
|
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
|
-
}
|
|
382
|
+
function approvalActionFor(state, key) {
|
|
383
|
+
if (key === '\x03') return () => process.exit(0);
|
|
384
|
+
if (key === '\x1b[A') return () => moveApprovalSelection(state, -1);
|
|
385
|
+
if (key === '\x1b[B') return () => moveApprovalSelection(state, 1);
|
|
386
|
+
const actions = state.items.length === 0 ? EMPTY_APPROVAL_KEY_ACTIONS : APPROVAL_KEY_ACTIONS;
|
|
387
|
+
return actions[key.toLowerCase()];
|
|
388
|
+
}
|
|
368
389
|
|
|
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
|
-
}
|
|
390
|
+
async function handleApprovalKey(state, key) {
|
|
391
|
+
if (state.busy) return;
|
|
392
|
+
const action = approvalActionFor(state, key);
|
|
393
|
+
if (action) return action(state);
|
|
394
|
+
}
|
|
383
395
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
396
|
+
async function cmdApprovals() {
|
|
397
|
+
const state = { claw: createClient(), items: [], selected: 0, busy: false, stream: null };
|
|
398
|
+
await fetchPendingApprovals(state);
|
|
399
|
+
startApprovalStream(state);
|
|
400
|
+
setupApprovalTerminal(state);
|
|
401
|
+
renderApprovals(state);
|
|
402
|
+
process.stdin.on('data', (key) => handleApprovalKey(state, key));
|
|
389
403
|
}
|
|
390
404
|
|
|
391
405
|
// -- install subcommand group ------------------------------------------------
|
|
@@ -423,15 +437,63 @@ async function cmdInstallCodex() {
|
|
|
423
437
|
}
|
|
424
438
|
}
|
|
425
439
|
|
|
440
|
+
async function cmdCost() {
|
|
441
|
+
const lens = getFlag('--lens') || 'claude-code';
|
|
442
|
+
const period = getFlag('--period') || '7d';
|
|
443
|
+
|
|
444
|
+
if (!baseUrl || !apiKey) {
|
|
445
|
+
console.error('Not configured. Run `dashclaw install claude` to set up,');
|
|
446
|
+
console.error('or set DASHCLAW_BASE_URL and DASHCLAW_API_KEY.');
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
console.log(await runCost({ baseUrl, apiKey }, { lens, period }));
|
|
452
|
+
} catch (err) {
|
|
453
|
+
if (err.usage) {
|
|
454
|
+
console.error(err.message);
|
|
455
|
+
} else if (err.status === 401 || err.status === 403) {
|
|
456
|
+
console.error('API key was rejected (401/403). Re-run `dashclaw install claude` or check DASHCLAW_API_KEY.');
|
|
457
|
+
} else {
|
|
458
|
+
console.error(`Could not fetch spend from ${baseUrl}: ${err.message}`);
|
|
459
|
+
console.error('Check that the instance is running and reachable.');
|
|
460
|
+
}
|
|
461
|
+
// exitCode (not process.exit) so node drains the failed fetch socket —
|
|
462
|
+
// a hard exit here can trip a libuv teardown assert on Windows.
|
|
463
|
+
process.exitCode = 1;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function cmdInstallClaude() {
|
|
468
|
+
try {
|
|
469
|
+
const result = await installClaude({
|
|
470
|
+
endpoint: getFlag('--endpoint') || baseUrl,
|
|
471
|
+
apiKey: getFlag('--key') || apiKey,
|
|
472
|
+
agentId: getFlag('--agent-id') || undefined,
|
|
473
|
+
trial: args.includes('--trial'),
|
|
474
|
+
prompt: ask,
|
|
475
|
+
promptSecret: askSecret,
|
|
476
|
+
logger: console,
|
|
477
|
+
});
|
|
478
|
+
console.log();
|
|
479
|
+
console.log(` ${green('Done.')} Claude Code governance installed (mode: ${result.hookMode}).`);
|
|
480
|
+
} catch (err) {
|
|
481
|
+
console.error(red(`Error: ${err.message}`));
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
426
486
|
async function cmdInstall() {
|
|
427
487
|
const target = args[1];
|
|
428
488
|
switch (target) {
|
|
429
489
|
case 'codex':
|
|
430
490
|
return cmdInstallCodex();
|
|
491
|
+
case 'claude':
|
|
492
|
+
return cmdInstallClaude();
|
|
431
493
|
default:
|
|
432
494
|
console.error(`Unknown install target: dashclaw install ${target || '(missing)'}\n` +
|
|
433
|
-
'Try: dashclaw install
|
|
434
|
-
process.
|
|
495
|
+
'Try: dashclaw install claude [--trial] | dashclaw install codex [--project <path>]');
|
|
496
|
+
process.exitCode = 1;
|
|
435
497
|
}
|
|
436
498
|
}
|
|
437
499
|
|
|
@@ -445,26 +507,26 @@ async function cmdCodexNotify() {
|
|
|
445
507
|
// Skip the leading 'codex' and 'notify' tokens — runCodexNotify reads the
|
|
446
508
|
// JSON payload from the LAST argv slot (per Codex's notify contract).
|
|
447
509
|
const notifyArgv = args.slice(1); // includes 'notify' and the payload
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
510
|
+
try {
|
|
511
|
+
await runCodexNotify({
|
|
512
|
+
argv: notifyArgv,
|
|
513
|
+
baseUrl,
|
|
514
|
+
apiKey,
|
|
515
|
+
agentId: agentId || 'codex',
|
|
516
|
+
logger: console,
|
|
517
|
+
});
|
|
518
|
+
} catch {
|
|
519
|
+
// Best-effort by contract: Codex must never see a failure from its
|
|
520
|
+
// notify hook, so errors are swallowed and we still exit 0.
|
|
521
|
+
}
|
|
455
522
|
process.exit(0);
|
|
456
523
|
}
|
|
457
524
|
|
|
458
525
|
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
|
-
}
|
|
526
|
+
return runSubcommand({
|
|
527
|
+
notify: cmdCodexNotify,
|
|
528
|
+
}, args[1], (sub) => `Unknown subcommand: dashclaw codex ${sub || '(missing)'}\n` +
|
|
529
|
+
'Try: dashclaw codex notify \'<json>\' (called by Codex notify config)');
|
|
468
530
|
}
|
|
469
531
|
|
|
470
532
|
// -- code subcommand group ---------------------------------------------------
|
|
@@ -487,24 +549,27 @@ async function cmdCodeIngestCodex() {
|
|
|
487
549
|
console.log('No sessions to ingest.');
|
|
488
550
|
return;
|
|
489
551
|
}
|
|
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
|
-
}
|
|
552
|
+
const counts = tallyCodexIngestResults(results);
|
|
501
553
|
console.log();
|
|
502
|
-
console.log(`Done. Written: ${
|
|
503
|
-
if (!endpoint &&
|
|
554
|
+
console.log(`Done. Written: ${counts.written_local} Ingested: ${counts.ingested} Dry-run: ${counts.dry_run} Skipped: ${counts.skipped} Errors: ${counts.error}`);
|
|
555
|
+
if (!endpoint && counts.written_local > 0) {
|
|
504
556
|
console.log(dim(` Local sessions saved under ${outDir}.`));
|
|
505
557
|
console.log(dim(` Server-side codex ingest will be wired in a follow-up phase.`));
|
|
506
558
|
}
|
|
507
|
-
if (
|
|
559
|
+
if (counts.error > 0) process.exit(2);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function tallyCodexIngestResults(results) {
|
|
563
|
+
const counts = { written_local: 0, ingested: 0, dry_run: 0, skipped: 0, error: 0 };
|
|
564
|
+
for (const r of results) {
|
|
565
|
+
incrementCount(counts, r.status);
|
|
566
|
+
if (r.status === 'error') console.error(formatCodexIngestError(r));
|
|
567
|
+
}
|
|
568
|
+
return counts;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function formatCodexIngestError(result) {
|
|
572
|
+
return ` ${red('error')} ${result.file}: ${result.reason}${result.detail ? ' — ' + result.detail : ''}`;
|
|
508
573
|
}
|
|
509
574
|
|
|
510
575
|
async function cmdCodeIngest() {
|
|
@@ -518,17 +583,17 @@ async function cmdCodeIngest() {
|
|
|
518
583
|
dryRun,
|
|
519
584
|
});
|
|
520
585
|
if (!results.length) return;
|
|
521
|
-
|
|
522
|
-
let skipped = 0;
|
|
523
|
-
let errors = 0;
|
|
586
|
+
const counts = { ingested: 0, skipped: 0, error: 0 };
|
|
524
587
|
for (const r of results) {
|
|
525
|
-
if (r.status === '
|
|
526
|
-
|
|
527
|
-
else
|
|
588
|
+
if (r.status === 'skipped_unchanged' || r.status === 'dry_run') {
|
|
589
|
+
incrementCount(counts, 'skipped');
|
|
590
|
+
} else {
|
|
591
|
+
incrementCount(counts, r.status);
|
|
592
|
+
}
|
|
528
593
|
}
|
|
529
594
|
console.log();
|
|
530
|
-
console.log(`Done. Ingested: ${ingested} Skipped: ${skipped} Errors: ${
|
|
531
|
-
if (
|
|
595
|
+
console.log(`Done. Ingested: ${counts.ingested} Skipped: ${counts.skipped} Errors: ${counts.error}`);
|
|
596
|
+
if (counts.error > 0) process.exit(2);
|
|
532
597
|
}
|
|
533
598
|
|
|
534
599
|
async function cmdCodeMemo() {
|
|
@@ -578,24 +643,16 @@ async function cmdCodeApply() {
|
|
|
578
643
|
}
|
|
579
644
|
|
|
580
645
|
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
|
-
}
|
|
646
|
+
return runSubcommand({
|
|
647
|
+
ingest: cmdCodeIngest,
|
|
648
|
+
'ingest-codex': cmdCodeIngestCodex,
|
|
649
|
+
memo: cmdCodeMemo,
|
|
650
|
+
apply: cmdCodeApply,
|
|
651
|
+
}, args[1], (sub) => `Unknown subcommand: dashclaw code ${sub || '(missing)'}\n` +
|
|
652
|
+
'Try: dashclaw code ingest [--dry-run]\n' +
|
|
653
|
+
' dashclaw code ingest-codex [--dry-run] [--out <dir>] [--endpoint <url>]\n' +
|
|
654
|
+
' dashclaw code memo --project=<slug> [--save]\n' +
|
|
655
|
+
' dashclaw code apply <manifestId> --dest=<dir> [--yes]');
|
|
599
656
|
}
|
|
600
657
|
|
|
601
658
|
// -- prompts subcommand group ------------------------------------------------
|
|
@@ -839,36 +896,42 @@ function inboxClient() {
|
|
|
839
896
|
}
|
|
840
897
|
|
|
841
898
|
async function cmdInboxList() {
|
|
842
|
-
const unread = args.includes('--unread');
|
|
843
|
-
const limitFlag = getFlag('--limit');
|
|
844
899
|
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}`));
|
|
900
|
+
const data = await apiRequest(inboxClient(), 'GET', '/api/messages', { query: inboxListQuery() });
|
|
901
|
+
renderInboxList(data);
|
|
866
902
|
} catch (err) {
|
|
867
903
|
console.error(`Error: ${err.message}`);
|
|
868
904
|
process.exit(1);
|
|
869
905
|
}
|
|
870
906
|
}
|
|
871
907
|
|
|
908
|
+
function inboxListQuery() {
|
|
909
|
+
return {
|
|
910
|
+
agent_id: agentId,
|
|
911
|
+
direction: 'inbox',
|
|
912
|
+
unread: args.includes('--unread') ? 'true' : undefined,
|
|
913
|
+
limit: getFlag('--limit'),
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function renderInboxList(data) {
|
|
918
|
+
const messages = data.messages || [];
|
|
919
|
+
if (messages.length === 0) {
|
|
920
|
+
console.log(dim(' No messages.'));
|
|
921
|
+
} else {
|
|
922
|
+
messages.forEach(renderInboxMessage);
|
|
923
|
+
}
|
|
924
|
+
console.log();
|
|
925
|
+
console.log(dim(` unread: ${data.unread_count ?? 0}`));
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function renderInboxMessage(message) {
|
|
929
|
+
const readMark = message.is_read ? dim('read') : green('unread');
|
|
930
|
+
console.log(
|
|
931
|
+
` ${bold(message.id)} ${dim('from')} ${message.from_agent_id || '-'} ${message.subject || dim('(no subject)')} [${readMark}]`,
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
|
|
872
935
|
async function cmdInboxUpdate(action) {
|
|
873
936
|
const ids = args.slice(2).filter((a) => !a.startsWith('--'));
|
|
874
937
|
if (ids.length === 0) {
|
|
@@ -887,21 +950,14 @@ async function cmdInboxUpdate(action) {
|
|
|
887
950
|
}
|
|
888
951
|
|
|
889
952
|
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
|
-
}
|
|
953
|
+
return runSubcommand({
|
|
954
|
+
list: cmdInboxList,
|
|
955
|
+
read: () => cmdInboxUpdate('read'),
|
|
956
|
+
archive: () => cmdInboxUpdate('archive'),
|
|
957
|
+
}, args[1], (sub) => `Unknown subcommand: dashclaw inbox ${sub || '(missing)'}\n` +
|
|
958
|
+
'Try: dashclaw inbox list [--unread] [--limit N]\n' +
|
|
959
|
+
' dashclaw inbox read <id> [<id> ...]\n' +
|
|
960
|
+
' dashclaw inbox archive <id> [<id> ...]');
|
|
905
961
|
}
|
|
906
962
|
|
|
907
963
|
// -- behavior subcommand group -----------------------------------------------
|
|
@@ -918,20 +974,28 @@ function behaviorClient() {
|
|
|
918
974
|
async function cmdBehaviorStatus() {
|
|
919
975
|
try {
|
|
920
976
|
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
|
-
}
|
|
977
|
+
renderBehaviorStatus(data);
|
|
929
978
|
} catch (err) {
|
|
930
979
|
console.error(`Error: ${err.message}`);
|
|
931
980
|
process.exit(1);
|
|
932
981
|
}
|
|
933
982
|
}
|
|
934
983
|
|
|
984
|
+
function behaviorReadyLabel(data) {
|
|
985
|
+
return data.ready ? green('yes') : dim('no — need ' + (data.min_samples ?? 8) + '+ samples for an agent');
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function renderBehaviorStatus(data) {
|
|
989
|
+
console.log(` Recorder: ${data.recorder_enabled ? green('on') : dim('off')}`);
|
|
990
|
+
console.log(` Directory: ${dim(data.dir || '-')}`);
|
|
991
|
+
console.log(` Samples: ${bold(String(data.sample_count ?? 0))} ${dim('across')} ${data.agent_count ?? 0} ${dim('agent(s)')}`);
|
|
992
|
+
console.log(` Window: ${dim((data.oldest_ts || '-') + ' → ' + (data.newest_ts || '-'))}`);
|
|
993
|
+
console.log(` Ready: ${behaviorReadyLabel(data)}`);
|
|
994
|
+
for (const a of data.agents || []) {
|
|
995
|
+
console.log(` ${a.agent_id} ${dim(a.count + ' samples')}`);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
935
999
|
async function cmdBehaviorSuggestions() {
|
|
936
1000
|
const agent = getFlag('--agent-id') || agentId;
|
|
937
1001
|
try {
|
|
@@ -958,18 +1022,12 @@ async function cmdBehaviorSuggestions() {
|
|
|
958
1022
|
}
|
|
959
1023
|
|
|
960
1024
|
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
|
-
}
|
|
1025
|
+
return runSubcommand({
|
|
1026
|
+
status: cmdBehaviorStatus,
|
|
1027
|
+
suggestions: cmdBehaviorSuggestions,
|
|
1028
|
+
}, args[1], (sub) => `Unknown subcommand: dashclaw behavior ${sub || '(missing)'}\n` +
|
|
1029
|
+
'Try: dashclaw behavior status\n' +
|
|
1030
|
+
' dashclaw behavior suggestions [--agent-id X]');
|
|
973
1031
|
}
|
|
974
1032
|
|
|
975
1033
|
// -- posture subcommand group ------------------------------------------------
|
|
@@ -993,31 +1051,47 @@ function printFinding(f, indent = ' ') {
|
|
|
993
1051
|
console.log(dim(`${indent} ${f.key}`));
|
|
994
1052
|
}
|
|
995
1053
|
|
|
1054
|
+
function postureStatusLabel(status) {
|
|
1055
|
+
if (status === 'healthy') return green(status);
|
|
1056
|
+
if (status === 'at_risk') return red(status);
|
|
1057
|
+
return status;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function renderPostureHeader(data) {
|
|
1061
|
+
console.log();
|
|
1062
|
+
console.log(` ${bold('Governance posture')} ${bold(String(data.score))}${dim('/100')} ${postureStatusLabel(data.status)}` +
|
|
1063
|
+
`${data.cappedBy ? ' ' + red('[capped: incident]') : ''}`);
|
|
1064
|
+
console.log(dim(` ${data.summary?.openFindings ?? 0} open · +${Math.round(data.summary?.pointsRecoverable ?? 0)} points recoverable`));
|
|
1065
|
+
console.log();
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function renderPostureDimensions(dimensions) {
|
|
1069
|
+
for (const d of dimensions || []) {
|
|
1070
|
+
const label = POSTURE_DIM_LABEL[d.dimension] || d.dimension;
|
|
1071
|
+
const mark = d.score < 70 ? red('!') : ' ';
|
|
1072
|
+
console.log(` ${mark} ${label.padEnd(12)} ${String(d.score).padStart(3)}`);
|
|
1073
|
+
}
|
|
1074
|
+
console.log();
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function renderPostureQueue(findings) {
|
|
1078
|
+
console.log(dim(` Next — ${findings.length} open`));
|
|
1079
|
+
if (findings.length === 0) {
|
|
1080
|
+
console.log(green(' Queue is clear — no open coverage gaps.'));
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
for (const f of findings.slice(0, 5)) printFinding(f);
|
|
1084
|
+
if (findings.length > 5) console.log(dim(` … ${findings.length - 5} more`));
|
|
1085
|
+
}
|
|
1086
|
+
|
|
996
1087
|
async function cmdPostureShow() {
|
|
997
1088
|
try {
|
|
998
1089
|
const client = postureClient();
|
|
999
1090
|
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
1091
|
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
|
-
}
|
|
1092
|
+
renderPostureHeader(data);
|
|
1093
|
+
renderPostureDimensions(data.dimensions);
|
|
1094
|
+
renderPostureQueue(findings);
|
|
1021
1095
|
console.log();
|
|
1022
1096
|
} catch (err) {
|
|
1023
1097
|
console.error(`Error: ${err.message}`);
|
|
@@ -1077,18 +1151,79 @@ async function cmdNext() {
|
|
|
1077
1151
|
}
|
|
1078
1152
|
}
|
|
1079
1153
|
|
|
1154
|
+
// -- env command ---------------------------------------------------------------
|
|
1155
|
+
//
|
|
1156
|
+
// `dashclaw env [--agent <id>] -- <command...>` — managed-secrets delivery.
|
|
1157
|
+
// Fetches the delivery-enabled bundle (GET /api/secrets/env) and spawns the
|
|
1158
|
+
// child command with the values injected into its environment MEMORY-ONLY.
|
|
1159
|
+
// Secret VALUES are never printed, never written to disk, never echoed in
|
|
1160
|
+
// error paths; only NAMES are ever shown. Fail-closed: if the fetch fails,
|
|
1161
|
+
// the child is NOT run. Uses process.exitCode (not process.exit) so node
|
|
1162
|
+
// drains sockets — a hard exit can trip a libuv teardown assert on Windows.
|
|
1163
|
+
|
|
1164
|
+
async function cmdEnv() {
|
|
1165
|
+
const { flags, command: childArgv } = splitEnvArgv(args.slice(1));
|
|
1166
|
+
|
|
1167
|
+
if (flags.includes('--print')) {
|
|
1168
|
+
console.error('Error: --print is not supported. Secret values are memory-only by design —');
|
|
1169
|
+
console.error('they are injected into a child process environment, never printed or written to disk.');
|
|
1170
|
+
console.error('Use `dashclaw env` to list secret NAMES, or `dashclaw env -- <command>` to run with them.');
|
|
1171
|
+
process.exitCode = 1;
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
let agent = agentId;
|
|
1176
|
+
for (let i = 0; i < flags.length; i++) {
|
|
1177
|
+
if (flags[i] === '--agent') agent = flags[i + 1];
|
|
1178
|
+
else if (flags[i].startsWith('--agent=')) agent = flags[i].slice('--agent='.length);
|
|
1179
|
+
}
|
|
1180
|
+
if (!agent) {
|
|
1181
|
+
console.error('Error: no agent id. Pass --agent <id> or set DASHCLAW_AGENT_ID.');
|
|
1182
|
+
process.exitCode = 1;
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
let bundle;
|
|
1187
|
+
try {
|
|
1188
|
+
bundle = await fetchAgentEnv({ baseUrl, apiKey }, agent);
|
|
1189
|
+
} catch (err) {
|
|
1190
|
+
console.error(`Error: could not fetch the managed-secrets bundle: ${err.message}`);
|
|
1191
|
+
if (childArgv.length > 0) {
|
|
1192
|
+
console.error('Fail-closed: the command was NOT run without the requested env.');
|
|
1193
|
+
}
|
|
1194
|
+
process.exitCode = 1;
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if (childArgv.length === 0) {
|
|
1199
|
+
console.log(formatEnvNames(bundle));
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
process.exitCode = await runWithEnv(bundle, childArgv);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1080
1206
|
// -- Router -------------------------------------------------------------------
|
|
1081
1207
|
|
|
1082
|
-
const COMMANDS_NEEDING_CONFIG = new Set(['approvals', 'approve', 'deny', 'doctor', 'code', 'prompts', 'inbox', 'behavior', 'posture', 'next']);
|
|
1208
|
+
const COMMANDS_NEEDING_CONFIG = new Set(['approvals', 'approve', 'deny', 'doctor', 'code', 'prompts', 'inbox', 'behavior', 'posture', 'next', 'env']);
|
|
1083
1209
|
// `install` deliberately omitted: provisioning hooks and AGENTS.md shouldn't
|
|
1084
1210
|
// require the user to have already configured API keys. If config happens to
|
|
1085
1211
|
// be present, install will pick up baseUrl for the AGENTS.md instance link.
|
|
1086
1212
|
// `codex notify` also opt-in: if no config, the notify fail-softs to skipped
|
|
1087
1213
|
// rather than erroring (Codex never sees the error anyway — it spawns with
|
|
1088
1214
|
// stdio nulled).
|
|
1089
|
-
|
|
1215
|
+
// `cost` resolves config non-interactively and prints its own setup hint when
|
|
1216
|
+
// missing (the generic interactive prompt would be the wrong UX for a
|
|
1217
|
+
// read-only readback command).
|
|
1218
|
+
const COMMANDS_OPTIONAL_CONFIG = new Set(['install', 'codex', 'cost']);
|
|
1219
|
+
|
|
1220
|
+
function applyConfig(config) {
|
|
1221
|
+
baseUrl = config.baseUrl;
|
|
1222
|
+
apiKey = config.apiKey;
|
|
1223
|
+
agentId = config.agentId;
|
|
1224
|
+
}
|
|
1090
1225
|
|
|
1091
|
-
async function
|
|
1226
|
+
async function loadCommandConfig() {
|
|
1092
1227
|
if (COMMANDS_NEEDING_CONFIG.has(command)) {
|
|
1093
1228
|
const config = await resolveConfig();
|
|
1094
1229
|
if (!config) {
|
|
@@ -1096,68 +1231,51 @@ async function main() {
|
|
|
1096
1231
|
console.error('Set them as env vars, save with an interactive first run, or use a .env file.');
|
|
1097
1232
|
process.exit(1);
|
|
1098
1233
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1234
|
+
applyConfig(config);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
if (COMMANDS_OPTIONAL_CONFIG.has(command)) {
|
|
1103
1238
|
const config = await resolveConfig({ interactive: false }).catch(() => null);
|
|
1104
1239
|
if (config) {
|
|
1105
|
-
|
|
1106
|
-
apiKey = config.apiKey;
|
|
1107
|
-
agentId = config.agentId;
|
|
1240
|
+
applyConfig(config);
|
|
1108
1241
|
}
|
|
1109
1242
|
}
|
|
1243
|
+
}
|
|
1110
1244
|
|
|
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);
|
|
1245
|
+
const COMMAND_HANDLERS = {
|
|
1246
|
+
approvals: cmdApprovals,
|
|
1247
|
+
approve: cmdApprove,
|
|
1248
|
+
deny: cmdDeny,
|
|
1249
|
+
doctor: cmdDoctor,
|
|
1250
|
+
logout: cmdLogout,
|
|
1251
|
+
code: cmdCode,
|
|
1252
|
+
install: cmdInstall,
|
|
1253
|
+
codex: cmdCodex,
|
|
1254
|
+
prompts: cmdPrompts,
|
|
1255
|
+
inbox: cmdInbox,
|
|
1256
|
+
behavior: cmdBehavior,
|
|
1257
|
+
posture: cmdPosture,
|
|
1258
|
+
cost: cmdCost,
|
|
1259
|
+
next: cmdNext,
|
|
1260
|
+
env: cmdEnv,
|
|
1261
|
+
help: cmdHelp,
|
|
1262
|
+
'--help': cmdHelp,
|
|
1263
|
+
'-h': cmdHelp,
|
|
1264
|
+
version: cmdVersion,
|
|
1265
|
+
'--version': cmdVersion,
|
|
1266
|
+
'-v': cmdVersion,
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
async function main() {
|
|
1270
|
+
await loadCommandConfig();
|
|
1271
|
+
const handler = COMMAND_HANDLERS[command];
|
|
1272
|
+
if (handler) {
|
|
1273
|
+
await handler();
|
|
1274
|
+
return;
|
|
1160
1275
|
}
|
|
1276
|
+
console.error(`Unknown command: ${command}`);
|
|
1277
|
+
await cmdHelp();
|
|
1278
|
+
process.exitCode = 1;
|
|
1161
1279
|
}
|
|
1162
1280
|
|
|
1163
1281
|
main();
|