@dashclaw/cli 0.2.0 → 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/bin/dashclaw.js CHANGED
@@ -1,37 +1,48 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execFileSync } from 'child_process';
4
+ import { readFileSync } from 'node:fs';
5
+ import { dirname, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
4
7
  import { DashClaw } from 'dashclaw';
5
8
  import {
6
9
  bold, dim, inverse, colorByRisk, clearScreen,
7
10
  moveCursor, hideCursor, showCursor,
8
11
  green, red,
9
12
  } from '../lib/render.js';
13
+ import { runDoctor as runDoctorCommand } from '../lib/doctor.js';
14
+ import { resolveConfig, clearConfigFile, configPath, ask, askSecret } from '../lib/config.js';
15
+ import { runIngest, defaultClaudeProjectsDir } from '../lib/code/ingest.js';
16
+ import { runMemo } from '../lib/code/memo.js';
17
+ import { runApply } from '../lib/code/apply.js';
18
+ import {
19
+ runCodexIngest,
20
+ defaultCodexSessionsDir,
21
+ defaultCodexOutDir,
22
+ } from '../lib/code/ingest-codex.js';
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';
26
+ import { runCodexNotify } from '../lib/codex/notify.js';
27
+ import { apiRequest } from '../lib/api.js';
28
+ import { fetchPosture, fetchFindings, fetchNext, resolveFinding } from '../lib/posture.js';
29
+ import { fetchAgentEnv, splitEnvArgv, formatEnvNames, runWithEnv } from '../lib/env.js';
10
30
 
11
31
  process.on('unhandledRejection', (reason) => {
12
32
  console.error('Unhandled Rejection:', reason);
13
- process.exit(1);
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;
14
36
  });
15
37
 
16
38
  // -- Config -------------------------------------------------------------------
17
39
 
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
- }
40
+ // Populated by main() before any command runs.
41
+ let baseUrl;
42
+ let apiKey;
43
+ let agentId;
32
44
 
33
45
  function createClient() {
34
- requireEnv();
35
46
  return new DashClaw({ baseUrl, apiKey, agentId });
36
47
  }
37
48
 
@@ -41,9 +52,23 @@ const args = process.argv.slice(2);
41
52
  const command = args[0] || 'help';
42
53
 
43
54
  function getFlag(name) {
44
- const idx = args.indexOf(name);
45
- if (idx === -1 || idx + 1 >= args.length) return undefined;
46
- return args[idx + 1];
55
+ for (let i = 0; i < args.length; i++) {
56
+ const a = args[i];
57
+ if (a === name) return i + 1 < args.length ? args[i + 1] : undefined; // --name value
58
+ if (a.startsWith(name + '=')) return a.slice(name.length + 1); // --name=value
59
+ }
60
+ return undefined;
61
+ }
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;
47
72
  }
48
73
 
49
74
  // -- Commands -----------------------------------------------------------------
@@ -56,15 +81,101 @@ ${bold('Usage:')}
56
81
  dashclaw approvals Interactive approval inbox
57
82
  dashclaw approve <actionId> [--reason] Approve an action
58
83
  dashclaw deny <actionId> [--reason] Deny an action
84
+ dashclaw doctor Diagnose and auto-fix your DashClaw instance
85
+ --json Output as JSON (for CI/scripts)
86
+ --no-fix Diagnose only, skip auto-fixes
87
+ --category <list> Filter checks (e.g., database,config)
88
+ dashclaw code ingest [--dry-run] Backfill Claude Code transcripts from ~/.claude/projects
89
+ --projects-dir <path> Override the default scan directory
90
+ dashclaw code ingest-codex [--dry-run] Backfill Codex transcripts from ~/.codex/sessions
91
+ --sessions-dir <path> Override the default scan directory
92
+ --out <dir> Local output dir (default ~/.dashclaw/codex-sessions)
93
+ --endpoint <url> POST to <url> instead of writing local (advanced)
94
+ dashclaw code memo --project=<slug> Print the latest weekly memo for a project
95
+ --save Also write to ./memos/<weekTag>-<slug>.md
96
+ dashclaw code apply <manifestId> Apply an Optimal Files manifest (Phase 6+ feature)
97
+ --dest=<dir> Target project directory (required)
98
+ --yes Overwrite existing files when manifest says so
99
+ --allow-redactions Write files that contain redacted secret patterns
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)
105
+ dashclaw install codex Provision DashClaw governance into Codex CLI
106
+ --project <path> Project to receive AGENTS.md (default: cwd)
107
+ --approval-policy <p> Codex approval_policy (default: on-request)
108
+ --include-notify Also wire Codex's notify config to dashclaw codex notify
109
+ dashclaw codex notify '<json>' Record a Codex turn-complete event
110
+ (called by Codex's notify config; always exits 0)
111
+ dashclaw prompts list [--category C] List prompt templates
112
+ dashclaw prompts get <id> Show a template
113
+ dashclaw prompts versions <id> List a template's versions
114
+ dashclaw prompts render <templateId> Render a prompt
115
+ --version-id <vid> Render a specific version instead of the active one
116
+ --var <key=value> Set a variable (repeatable)
117
+ --record Record the render as a prompt run
118
+ dashclaw prompts create --name N Create a template (admin)
119
+ --description <D> --category <C>
120
+ dashclaw prompts add-version <id> --content C Add a version (admin)
121
+ --changelog <L> --model-hint <M>
122
+ dashclaw prompts activate <id> <vid> Activate a version (admin)
123
+ dashclaw prompts stats [--template-id X] Prompt run analytics
124
+ dashclaw inbox list [--unread] [--limit N] List inbox messages
125
+ dashclaw inbox read <id> [<id> ...] Mark messages read
126
+ dashclaw inbox archive <id> [<id> ...] Archive messages
127
+ dashclaw behavior status Behavior Learning sample status (local recorder)
128
+ dashclaw behavior suggestions Evidence-backed policy suggestions per agent
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)
135
+ dashclaw posture Governance posture score + remediation queue
136
+ dashclaw posture resolve <key> Draft a fix (inactive) | --snooze | --accept-risk
137
+ --note "..." Attach a note to the resolution
138
+ dashclaw next The single top open governance gap + its fix
139
+ dashclaw logout Remove saved config (~/.dashclaw/config.json)
140
+ dashclaw version Print the CLI version
59
141
  dashclaw help Show this help
60
142
 
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)
143
+ ${bold('Config:')}
144
+ On first run, prompts for DASHCLAW_BASE_URL and DASHCLAW_API_KEY and offers
145
+ to save them to ~/.dashclaw/config.json (mode 600). Env vars always override
146
+ the saved values.
65
147
  `);
66
148
  }
67
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
+
156
+ async function cmdLogout() {
157
+ const removed = clearConfigFile();
158
+ if (removed) {
159
+ console.log(`${green('Removed')} ${configPath()}`);
160
+ } else {
161
+ console.log(`${dim('No saved config at')} ${configPath()}`);
162
+ }
163
+ }
164
+
165
+ async function cmdDoctor() {
166
+ const jsonFlag = args.includes('--json');
167
+ const noFixFlag = args.includes('--no-fix');
168
+ const catIdx = args.indexOf('--category');
169
+ const catValue = catIdx !== -1 ? args[catIdx + 1] : undefined;
170
+ await runDoctorCommand({
171
+ baseUrl,
172
+ apiKey,
173
+ json: jsonFlag,
174
+ noFix: noFixFlag,
175
+ category: catValue,
176
+ });
177
+ }
178
+
68
179
  async function cmdApprove() {
69
180
  const actionId = args[1];
70
181
  if (!actionId) {
@@ -103,202 +214,1068 @@ async function cmdDeny() {
103
214
  }
104
215
  }
105
216
 
106
- async function cmdApprovals() {
107
- const claw = createClient();
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
+ }
108
226
 
109
- let items = [];
110
- let selected = 0;
227
+ function approvalId(action) {
228
+ return action.action_id || action.id;
229
+ }
111
230
 
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
- }
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
+ }
121
239
 
122
- function render() {
123
- clearScreen();
124
- moveCursor(1, 1);
125
- process.stdout.write(bold('DashClaw Approval Inbox') + '\n\n');
240
+ function renderApprovals(state) {
241
+ clearScreen();
242
+ moveCursor(1, 1);
243
+ process.stdout.write(bold('DashClaw Approval Inbox') + '\n\n');
126
244
 
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
- }
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
+ }
251
+
252
+ process.stdout.write('\n' + dim(' [A] Approve [D] Deny [R] Refresh [O] Open Replay [Q] Quit') + '\n');
253
+ }
143
254
 
144
- process.stdout.write('\n' + dim(' [A] Approve [D] Deny [R] Refresh [O] Open Replay [Q] Quit') + '\n');
255
+ function replayUrlIsSafe(url) {
256
+ let parsed;
257
+ try {
258
+ parsed = new URL(url);
259
+ } catch {
260
+ return false;
145
261
  }
262
+ const protocolOk = parsed.protocol === 'http:' || parsed.protocol === 'https:';
263
+ return protocolOk && parsed.hostname && !/\s/.test(url) && !/[&|><^"'`]/.test(url);
264
+ }
146
265
 
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`);
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]);
156
281
  }
282
+ } catch (_) {
283
+ process.stdout.write(`\n Could not open browser. URL: ${url}\n`);
157
284
  }
285
+ }
158
286
 
159
- await fetchPending();
160
-
161
- // Open SSE stream for live push of new approval requests
162
- let stream = null;
287
+ function startApprovalStream(state) {
163
288
  try {
164
- stream = claw.events()
165
- .on('guard.decision.created', (data) => {
166
- if (data.decision !== 'require_approval') return;
167
- const exists = items.some((it) => (it.action_id || it.id) === data.action_id);
168
- if (exists) return;
169
- items.push(data);
170
- render();
171
- })
289
+ state.stream = state.claw.events()
290
+ .on('guard.decision.created', (data) => handleApprovalPush(state, data))
172
291
  .on('error', () => {
173
- moveCursor(items.length + 6, 1);
292
+ moveCursor(state.items.length + 6, 1);
174
293
  process.stdout.write(dim(' SSE stream error — live push unavailable, use R to refresh') + '\n');
175
294
  });
176
295
  } catch (_) {
177
296
  // SSE unavailable — inbox still works via manual refresh
178
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
+ }
179
307
 
180
- // Set up raw mode for interactive input
308
+ function ensureApprovalTty() {
181
309
  if (!process.stdin.isTTY) {
182
310
  console.error('Error: Interactive mode requires a TTY. Use dashclaw approve/deny for non-interactive use.');
183
311
  process.exit(1);
184
312
  }
313
+ }
185
314
 
315
+ function selectedWithinItems(state) {
316
+ state.selected = Math.min(state.selected, Math.max(0, state.items.length - 1));
317
+ }
318
+
319
+ function setupApprovalTerminal(state) {
320
+ ensureApprovalTty();
186
321
  process.stdin.setRawMode(true);
187
322
  process.stdin.resume();
188
323
  process.stdin.setEncoding('utf8');
189
324
  hideCursor();
325
+ process.on('exit', () => cleanupApprovalInbox(state));
326
+ process.on('SIGINT', () => process.exit(0));
327
+ }
328
+
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
+ }
190
335
 
191
- // Ensure cleanup on exit
192
- function cleanup() {
193
- if (stream) stream.close();
194
- showCursor();
195
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
196
- process.stdout.write('\n');
336
+ async function withApprovalBusy(state, fn) {
337
+ state.busy = true;
338
+ try {
339
+ await fn();
340
+ } finally {
341
+ state.busy = false;
197
342
  }
198
- process.on('exit', cleanup);
199
- process.on('SIGINT', () => process.exit(0));
343
+ }
200
344
 
201
- render();
345
+ async function refreshApprovals(state) {
346
+ await fetchPendingApprovals(state);
347
+ selectedWithinItems(state);
348
+ renderApprovals(state);
349
+ }
202
350
 
203
- let busy = false;
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
+ }
204
363
 
205
- process.stdin.on('data', async (key) => {
206
- if (busy) return;
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
+ }
207
369
 
208
- // Ctrl+C
209
- if (key === '\x03') {
210
- process.exit(0);
211
- }
370
+ const EMPTY_APPROVAL_KEY_ACTIONS = {
371
+ q: () => process.exit(0),
372
+ r: (state) => withApprovalBusy(state, () => refreshApprovals(state)),
373
+ };
212
374
 
213
- // Arrow keys: escape sequences
214
- if (key === '\x1b[A') {
215
- // Up
216
- if (selected > 0) selected--;
217
- render();
218
- return;
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
+ };
381
+
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
+ }
389
+
390
+ async function handleApprovalKey(state, key) {
391
+ if (state.busy) return;
392
+ const action = approvalActionFor(state, key);
393
+ if (action) return action(state);
394
+ }
395
+
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));
403
+ }
404
+
405
+ // -- install subcommand group ------------------------------------------------
406
+
407
+ const __dirname = dirname(fileURLToPath(import.meta.url));
408
+ // cli/bin/ -> cli/ -> repo root
409
+ const REPO_ROOT = resolve(__dirname, '..', '..');
410
+
411
+ async function cmdInstallCodex() {
412
+ const projectDir = getFlag('--project') || process.cwd();
413
+ const approvalPolicy = getFlag('--approval-policy') || 'on-request';
414
+ const includeNotify = args.includes('--include-notify');
415
+
416
+ try {
417
+ const result = await installCodex({
418
+ repoRoot: REPO_ROOT,
419
+ projectDir,
420
+ baseUrl,
421
+ approvalPolicy,
422
+ includeNotify,
423
+ logger: console,
424
+ });
425
+
426
+ console.log();
427
+ console.log(` ${green('Done.')} DashClaw governance is wired into Codex.`);
428
+ console.log(` ${dim('Hooks:')} ${result.hooks.hooksDst}`);
429
+ console.log(` ${dim('Config:')} ${result.config.path}${result.config.backup ? dim(' (backup: ' + result.config.backup + ')') : ''}`);
430
+ console.log(` ${dim('AGENTS:')} ${result.agentsMd.path}${result.agentsMd.backup ? dim(' (backup: ' + result.agentsMd.backup + ')') : ''}`);
431
+ console.log();
432
+ console.log(` Next: open a new Codex session in ${projectDir} and run a governed tool call.`);
433
+ console.log(` Codex requires you to trust new hooks; it will prompt on first use.`);
434
+ } catch (err) {
435
+ console.error(`Error: ${err.message}`);
436
+ process.exit(1);
437
+ }
438
+ }
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.');
219
460
  }
220
- if (key === '\x1b[B') {
221
- // Down
222
- if (selected < items.length - 1) selected++;
223
- render();
224
- return;
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
+
486
+ async function cmdInstall() {
487
+ const target = args[1];
488
+ switch (target) {
489
+ case 'codex':
490
+ return cmdInstallCodex();
491
+ case 'claude':
492
+ return cmdInstallClaude();
493
+ default:
494
+ console.error(`Unknown install target: dashclaw install ${target || '(missing)'}\n` +
495
+ 'Try: dashclaw install claude [--trial] | dashclaw install codex [--project <path>]');
496
+ process.exitCode = 1;
497
+ }
498
+ }
499
+
500
+ // -- codex subcommand group --------------------------------------------------
501
+ //
502
+ // `dashclaw codex notify '<json>'` is invoked by Codex's legacy notify config.
503
+ // It records a turn-complete action_record in DashClaw. ALWAYS exits 0 so
504
+ // Codex never sees an error from the spawn.
505
+
506
+ async function cmdCodexNotify() {
507
+ // Skip the leading 'codex' and 'notify' tokens — runCodexNotify reads the
508
+ // JSON payload from the LAST argv slot (per Codex's notify contract).
509
+ const notifyArgv = args.slice(1); // includes 'notify' and the payload
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
+ }
522
+ process.exit(0);
523
+ }
524
+
525
+ async function cmdCodex() {
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)');
530
+ }
531
+
532
+ // -- code subcommand group ---------------------------------------------------
533
+
534
+ async function cmdCodeIngestCodex() {
535
+ const dryRun = args.includes('--dry-run');
536
+ const sessionsDir = getFlag('--sessions-dir') || defaultCodexSessionsDir();
537
+ const outDir = getFlag('--out') || defaultCodexOutDir();
538
+ const endpoint = getFlag('--endpoint') || null;
539
+ console.log(`Scanning ${sessionsDir} ...`);
540
+ const results = await runCodexIngest({
541
+ sessionsDir,
542
+ outDir,
543
+ endpoint,
544
+ apiKey,
545
+ dryRun,
546
+ logger: console,
547
+ });
548
+ if (!results.length) {
549
+ console.log('No sessions to ingest.');
550
+ return;
551
+ }
552
+ const counts = tallyCodexIngestResults(results);
553
+ console.log();
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) {
556
+ console.log(dim(` Local sessions saved under ${outDir}.`));
557
+ console.log(dim(` Server-side codex ingest will be wired in a follow-up phase.`));
558
+ }
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 : ''}`;
573
+ }
574
+
575
+ async function cmdCodeIngest() {
576
+ const dryRun = args.includes('--dry-run');
577
+ const projectsDir = getFlag('--projects-dir') || defaultClaudeProjectsDir();
578
+ console.log(`Scanning ${projectsDir} ...`);
579
+ const results = await runIngest({
580
+ baseUrl,
581
+ apiKey,
582
+ projectsDir,
583
+ dryRun,
584
+ });
585
+ if (!results.length) return;
586
+ const counts = { ingested: 0, skipped: 0, error: 0 };
587
+ for (const r of results) {
588
+ if (r.status === 'skipped_unchanged' || r.status === 'dry_run') {
589
+ incrementCount(counts, 'skipped');
590
+ } else {
591
+ incrementCount(counts, r.status);
225
592
  }
593
+ }
594
+ console.log();
595
+ console.log(`Done. Ingested: ${counts.ingested} Skipped: ${counts.skipped} Errors: ${counts.error}`);
596
+ if (counts.error > 0) process.exit(2);
597
+ }
598
+
599
+ async function cmdCodeMemo() {
600
+ const project = getFlag('--project');
601
+ const save = args.includes('--save');
602
+ if (!project) {
603
+ console.error('Error: --project=<slug-or-id> is required.');
604
+ process.exit(1);
605
+ }
606
+ await runMemo({ baseUrl, apiKey, project, save });
607
+ }
608
+
609
+ async function cmdCodeApply() {
610
+ const manifestId = args[2];
611
+ const dest = getFlag('--dest');
612
+ const yes = args.includes('--yes');
613
+ const allowRedactions = args.includes('--allow-redactions');
614
+ const overwrite = args.includes('--overwrite');
615
+ if (!manifestId) {
616
+ console.error('Error: usage — dashclaw code apply <manifestId> --dest=<dir> [--yes] [--allow-redactions] [--overwrite]');
617
+ process.exit(1);
618
+ }
619
+ if (!dest) {
620
+ console.error('Error: --dest=<dir> is required.');
621
+ process.exit(1);
622
+ }
623
+ try {
624
+ const results = await runApply({
625
+ baseUrl,
626
+ apiKey,
627
+ manifestId,
628
+ dest,
629
+ yes,
630
+ allowRedactions,
631
+ allowOverwriteSideBySide: overwrite,
632
+ });
633
+ const summary = results.reduce((acc, r) => {
634
+ acc[r.status] = (acc[r.status] || 0) + 1;
635
+ return acc;
636
+ }, {});
637
+ console.log();
638
+ console.log('Apply summary:', JSON.stringify(summary));
639
+ } catch (err) {
640
+ console.error('Error: ' + err.message);
641
+ process.exit(1);
642
+ }
643
+ }
644
+
645
+ async function cmdCode() {
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]');
656
+ }
226
657
 
227
- const ch = key.toLowerCase();
658
+ // -- prompts subcommand group ------------------------------------------------
659
+ //
660
+ // Direct-API calls (apiRequest) rather than SDK methods: the CLI imports the
661
+ // PUBLISHED `dashclaw` package, which may lag this repo and lack newly-added
662
+ // prompt methods. fetch + x-api-key against the resolved baseUrl/apiKey is the
663
+ // durable path.
228
664
 
229
- if (ch === 'q') {
230
- process.exit(0);
665
+ function promptsClient() {
666
+ return { baseUrl, apiKey };
667
+ }
668
+
669
+ // Collect repeated `--var key=value` flags into a { key: value } object.
670
+ function parseVars() {
671
+ const vars = {};
672
+ for (let i = 0; i < args.length; i++) {
673
+ if (args[i] === '--var' && i + 1 < args.length) {
674
+ const pair = args[i + 1];
675
+ const eq = pair.indexOf('=');
676
+ if (eq === -1) {
677
+ console.error(`Error: --var expects key=value, got "${pair}"`);
678
+ process.exit(1);
679
+ }
680
+ vars[pair.slice(0, eq)] = pair.slice(eq + 1);
231
681
  }
682
+ }
683
+ return vars;
684
+ }
232
685
 
233
- if (ch === 'r') {
234
- busy = true;
235
- await fetchPending();
236
- selected = Math.min(selected, Math.max(0, items.length - 1));
237
- render();
238
- busy = false;
686
+ async function cmdPromptsList() {
687
+ const category = getFlag('--category');
688
+ try {
689
+ const data = await apiRequest(promptsClient(), 'GET', '/api/prompts/templates', {
690
+ query: { category },
691
+ });
692
+ const templates = data.templates || [];
693
+ if (templates.length === 0) {
694
+ console.log(dim(' No templates.'));
239
695
  return;
240
696
  }
697
+ for (const t of templates) {
698
+ console.log(
699
+ ` ${bold(t.id)} ${t.name || '-'} ${dim('[' + (t.category || 'uncategorized') + ']')}` +
700
+ ` active v${t.active_version ?? '-'} ${dim('(' + (t.version_count ?? 0) + ' versions)')}`,
701
+ );
702
+ }
703
+ } catch (err) {
704
+ console.error(`Error: ${err.message}`);
705
+ process.exit(1);
706
+ }
707
+ }
241
708
 
242
- if (items.length === 0) return;
243
- const current = items[selected];
244
- const actionId = current.action_id || current.id;
245
-
246
- if (ch === 'a') {
247
- busy = true;
248
- try {
249
- await claw.approveAction(actionId, 'allow');
250
- items.splice(selected, 1);
251
- selected = Math.min(selected, Math.max(0, items.length - 1));
252
- } catch (err) {
253
- moveCursor(items.length + 5, 1);
254
- process.stdout.write(red(` Error: ${err.message}`) + '\n');
255
- }
256
- render();
257
- busy = false;
709
+ async function cmdPromptsGet() {
710
+ const id = args[2];
711
+ if (!id) {
712
+ console.error('Error: Missing template ID. Usage: dashclaw prompts get <id>');
713
+ process.exit(1);
714
+ }
715
+ try {
716
+ const data = await apiRequest(promptsClient(), 'GET', `/api/prompts/templates/${encodeURIComponent(id)}`);
717
+ console.log(JSON.stringify(data, null, 2));
718
+ } catch (err) {
719
+ console.error(`Error: ${err.message}`);
720
+ process.exit(1);
721
+ }
722
+ }
723
+
724
+ async function cmdPromptsVersions() {
725
+ const id = args[2];
726
+ if (!id) {
727
+ console.error('Error: Missing template ID. Usage: dashclaw prompts versions <id>');
728
+ process.exit(1);
729
+ }
730
+ try {
731
+ const data = await apiRequest(
732
+ promptsClient(), 'GET', `/api/prompts/templates/${encodeURIComponent(id)}/versions`,
733
+ );
734
+ const versions = data.versions || [];
735
+ if (versions.length === 0) {
736
+ console.log(dim(' No versions.'));
258
737
  return;
259
738
  }
739
+ for (const v of versions) {
740
+ const active = v.is_active ? green(' (active)') : '';
741
+ console.log(` v${v.version} ${bold(v.id)}${active} ${dim(v.changelog || '')}`);
742
+ }
743
+ } catch (err) {
744
+ console.error(`Error: ${err.message}`);
745
+ process.exit(1);
746
+ }
747
+ }
260
748
 
261
- if (ch === 'd') {
262
- busy = true;
263
- try {
264
- await claw.approveAction(actionId, 'deny');
265
- items.splice(selected, 1);
266
- selected = Math.min(selected, Math.max(0, items.length - 1));
267
- } catch (err) {
268
- moveCursor(items.length + 5, 1);
269
- process.stdout.write(red(` Error: ${err.message}`) + '\n');
270
- }
271
- render();
272
- busy = false;
749
+ async function cmdPromptsRender() {
750
+ const versionId = getFlag('--version-id');
751
+ const templateId = args[2] && !args[2].startsWith('--') ? args[2] : undefined;
752
+ if (!templateId && !versionId) {
753
+ console.error('Error: usage — dashclaw prompts render <templateId> [--var k=v ...] [--record]');
754
+ console.error(' or: dashclaw prompts render --version-id <vid> [--var k=v ...]');
755
+ process.exit(1);
756
+ }
757
+ const variables = parseVars();
758
+ const record = args.includes('--record');
759
+ try {
760
+ const data = await apiRequest(promptsClient(), 'POST', '/api/prompts/render', {
761
+ body: {
762
+ template_id: templateId,
763
+ version_id: versionId,
764
+ variables,
765
+ agent_id: agentId,
766
+ record,
767
+ },
768
+ });
769
+ console.log(data.rendered ?? '');
770
+ if (Array.isArray(data.parameters) && data.parameters.length > 0) {
771
+ console.log();
772
+ console.log(dim(` parameters: ${data.parameters.join(', ')}`));
773
+ }
774
+ } catch (err) {
775
+ console.error(`Error: ${err.message}`);
776
+ process.exit(1);
777
+ }
778
+ }
779
+
780
+ async function cmdPromptsCreate() {
781
+ const name = getFlag('--name');
782
+ if (!name) {
783
+ console.error('Error: --name is required. Usage: dashclaw prompts create --name N [--description D] [--category C]');
784
+ process.exit(1);
785
+ }
786
+ const description = getFlag('--description');
787
+ const category = getFlag('--category');
788
+ try {
789
+ const data = await apiRequest(promptsClient(), 'POST', '/api/prompts/templates', {
790
+ body: { name, description, category },
791
+ });
792
+ console.log(` ${green('Created')} ${bold(data.id)} ${data.name}`);
793
+ } catch (err) {
794
+ console.error(`Error: ${err.message}`);
795
+ process.exit(1);
796
+ }
797
+ }
798
+
799
+ async function cmdPromptsAddVersion() {
800
+ const templateId = args[2];
801
+ if (!templateId || templateId.startsWith('--')) {
802
+ console.error('Error: usage — dashclaw prompts add-version <templateId> --content C [--changelog L] [--model-hint M]');
803
+ process.exit(1);
804
+ }
805
+ const content = getFlag('--content');
806
+ if (!content) {
807
+ console.error('Error: --content is required.');
808
+ process.exit(1);
809
+ }
810
+ const changelog = getFlag('--changelog');
811
+ const modelHint = getFlag('--model-hint');
812
+ try {
813
+ const data = await apiRequest(
814
+ promptsClient(), 'POST', `/api/prompts/templates/${encodeURIComponent(templateId)}/versions`,
815
+ { body: { content, changelog, model_hint: modelHint } },
816
+ );
817
+ console.log(` ${green('Added')} v${data.version} ${bold(data.id)}`);
818
+ } catch (err) {
819
+ console.error(`Error: ${err.message}`);
820
+ process.exit(1);
821
+ }
822
+ }
823
+
824
+ async function cmdPromptsActivate() {
825
+ const templateId = args[2];
826
+ const versionId = args[3];
827
+ if (!templateId || !versionId) {
828
+ console.error('Error: usage — dashclaw prompts activate <templateId> <versionId>');
829
+ process.exit(1);
830
+ }
831
+ try {
832
+ await apiRequest(
833
+ promptsClient(), 'POST',
834
+ `/api/prompts/templates/${encodeURIComponent(templateId)}/versions/${encodeURIComponent(versionId)}`,
835
+ );
836
+ console.log(` ${green('Activated')} version ${bold(versionId)}`);
837
+ } catch (err) {
838
+ console.error(`Error: ${err.message}`);
839
+ process.exit(1);
840
+ }
841
+ }
842
+
843
+ async function cmdPromptsStats() {
844
+ const templateId = getFlag('--template-id');
845
+ try {
846
+ const data = await apiRequest(promptsClient(), 'GET', '/api/prompts/stats', {
847
+ query: { template_id: templateId },
848
+ });
849
+ console.log(JSON.stringify(data, null, 2));
850
+ } catch (err) {
851
+ console.error(`Error: ${err.message}`);
852
+ process.exit(1);
853
+ }
854
+ }
855
+
856
+ async function cmdPrompts() {
857
+ const sub = args[1];
858
+ switch (sub) {
859
+ case 'list':
860
+ return cmdPromptsList();
861
+ case 'get':
862
+ return cmdPromptsGet();
863
+ case 'versions':
864
+ return cmdPromptsVersions();
865
+ case 'render':
866
+ return cmdPromptsRender();
867
+ case 'create':
868
+ return cmdPromptsCreate();
869
+ case 'add-version':
870
+ return cmdPromptsAddVersion();
871
+ case 'activate':
872
+ return cmdPromptsActivate();
873
+ case 'stats':
874
+ return cmdPromptsStats();
875
+ default:
876
+ console.error(`Unknown subcommand: dashclaw prompts ${sub || '(missing)'}\n` +
877
+ 'Try: dashclaw prompts list [--category C]\n' +
878
+ ' dashclaw prompts get <id>\n' +
879
+ ' dashclaw prompts versions <id>\n' +
880
+ ' dashclaw prompts render <templateId> [--var k=v ...] [--record]\n' +
881
+ ' dashclaw prompts create --name N [--description D] [--category C]\n' +
882
+ ' dashclaw prompts add-version <templateId> --content C [--changelog L] [--model-hint M]\n' +
883
+ ' dashclaw prompts activate <templateId> <versionId>\n' +
884
+ ' dashclaw prompts stats [--template-id X]');
885
+ process.exit(1);
886
+ }
887
+ }
888
+
889
+ // -- inbox subcommand group --------------------------------------------------
890
+ //
891
+ // Direct-API calls against /api/messages (durable path, see prompts group).
892
+ // Uses the resolved `agentId` for direction=inbox filtering and PATCH attribution.
893
+
894
+ function inboxClient() {
895
+ return { baseUrl, apiKey };
896
+ }
897
+
898
+ async function cmdInboxList() {
899
+ try {
900
+ const data = await apiRequest(inboxClient(), 'GET', '/api/messages', { query: inboxListQuery() });
901
+ renderInboxList(data);
902
+ } catch (err) {
903
+ console.error(`Error: ${err.message}`);
904
+ process.exit(1);
905
+ }
906
+ }
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
+
935
+ async function cmdInboxUpdate(action) {
936
+ const ids = args.slice(2).filter((a) => !a.startsWith('--'));
937
+ if (ids.length === 0) {
938
+ console.error(`Error: usage — dashclaw inbox ${action} <id> [<id> ...]`);
939
+ process.exit(1);
940
+ }
941
+ try {
942
+ const data = await apiRequest(inboxClient(), 'PATCH', '/api/messages', {
943
+ body: { message_ids: ids, action, agent_id: agentId },
944
+ });
945
+ console.log(` ${green('Updated')} ${data.updated ?? 0} message(s)`);
946
+ } catch (err) {
947
+ console.error(`Error: ${err.message}`);
948
+ process.exit(1);
949
+ }
950
+ }
951
+
952
+ async function cmdInbox() {
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> ...]');
961
+ }
962
+
963
+ // -- behavior subcommand group -----------------------------------------------
964
+ //
965
+ // Behavior Learning / Policy Coach. Read-only inspection of the locally-recorded
966
+ // behavior samples and the evidence-backed policy suggestions derived from them.
967
+ // Direct-API calls (durable path, see prompts/inbox groups). Adopt/dismiss are
968
+ // intentionally UI-only in V1 — they require simulation review.
969
+
970
+ function behaviorClient() {
971
+ return { baseUrl, apiKey };
972
+ }
973
+
974
+ async function cmdBehaviorStatus() {
975
+ try {
976
+ const data = await apiRequest(behaviorClient(), 'GET', '/api/behavior/samples');
977
+ renderBehaviorStatus(data);
978
+ } catch (err) {
979
+ console.error(`Error: ${err.message}`);
980
+ process.exit(1);
981
+ }
982
+ }
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
+
999
+ async function cmdBehaviorSuggestions() {
1000
+ const agent = getFlag('--agent-id') || agentId;
1001
+ try {
1002
+ const data = await apiRequest(behaviorClient(), 'GET', '/api/behavior/suggestions', {
1003
+ query: { agent_id: agent },
1004
+ });
1005
+ const suggestions = data.suggestions || [];
1006
+ if (suggestions.length === 0) {
1007
+ console.log(dim(` No suggestions (${data.sample_count ?? 0} samples analyzed).`));
273
1008
  return;
274
1009
  }
1010
+ for (const s of suggestions) {
1011
+ const kind = s.enforceable ? green('draft') : dim('advisory');
1012
+ console.log(` ${bold(s.type)} ${dim(s.agent_id)} ${s.confidence}% [${kind}] ${dim(s.severity)}`);
1013
+ console.log(` ${s.expected_effect}`);
1014
+ console.log(dim(` evidence: ${s.matching_sample_size}/${s.sample_size} · id ${s.id}`));
1015
+ }
1016
+ console.log();
1017
+ console.log(dim(' Review + simulate + adopt from the Policy Coach UI (/policy-coach).'));
1018
+ } catch (err) {
1019
+ console.error(`Error: ${err.message}`);
1020
+ process.exit(1);
1021
+ }
1022
+ }
1023
+
1024
+ async function cmdBehavior() {
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]');
1031
+ }
1032
+
1033
+ // -- posture subcommand group ------------------------------------------------
1034
+ //
1035
+ // Direct-API (apiRequest via cli/lib/posture.js) — the governance posture score
1036
+ // + the prioritized remediation queue. `posture resolve` is DRAFT-ONLY: it can
1037
+ // create an inactive policy draft / snooze / accept risk, never activate
1038
+ // enforcement (a human does that at /policies).
1039
+
1040
+ function postureClient() {
1041
+ return { baseUrl, apiKey };
1042
+ }
1043
+
1044
+ const POSTURE_DIM_LABEL = {
1045
+ identity: 'Identity', enforcement: 'Enforcement', spend: 'Spend',
1046
+ auditability: 'Audit', approval: 'Approval', data_protection: 'Data',
1047
+ };
1048
+
1049
+ function printFinding(f, indent = ' ') {
1050
+ console.log(`${indent}${bold('+' + (f.scoreDelta ?? 0))} ${dim('[' + f.severity + ']')} ${f.title}`);
1051
+ console.log(dim(`${indent} ${f.key}`));
1052
+ }
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
+
1087
+ async function cmdPostureShow() {
1088
+ try {
1089
+ const client = postureClient();
1090
+ const [data, queue] = await Promise.all([fetchPosture(client), fetchFindings(client)]);
1091
+ const findings = queue.findings || [];
1092
+ renderPostureHeader(data);
1093
+ renderPostureDimensions(data.dimensions);
1094
+ renderPostureQueue(findings);
1095
+ console.log();
1096
+ } catch (err) {
1097
+ console.error(`Error: ${err.message}`);
1098
+ process.exit(1);
1099
+ }
1100
+ }
275
1101
 
276
- if (ch === 'o') {
277
- openReplay(actionId);
1102
+ async function cmdPostureResolve() {
1103
+ const key = args[2];
1104
+ if (!key || key.startsWith('-')) {
1105
+ console.error('Error: usage — dashclaw posture resolve <key> [--snooze | --accept-risk] [--note "..."]');
1106
+ process.exit(1);
1107
+ }
1108
+ const action = args.includes('--snooze') ? 'snooze'
1109
+ : args.includes('--accept-risk') ? 'accept_risk' : 'create_draft';
1110
+ try {
1111
+ await resolveFinding(postureClient(), key, action, getFlag('--note'));
1112
+ if (action === 'create_draft') {
1113
+ console.log(green(' Draft created (inactive).') +
1114
+ dim(' Activate it at /policies and rescan — drafting does not change the score.'));
1115
+ } else {
1116
+ console.log(green(` Finding ${action === 'snooze' ? 'snoozed' : 'risk-accepted'}.`));
1117
+ }
1118
+ } catch (err) {
1119
+ console.error(`Error: ${err.message}`);
1120
+ process.exit(1);
1121
+ }
1122
+ }
1123
+
1124
+ async function cmdPosture() {
1125
+ const sub = args[1];
1126
+ if (sub === undefined) return cmdPostureShow();
1127
+ if (sub === 'resolve') return cmdPostureResolve();
1128
+ console.error(`Unknown subcommand: dashclaw posture ${sub}\n` +
1129
+ 'Try: dashclaw posture\n dashclaw posture resolve <key>');
1130
+ process.exit(1);
1131
+ }
1132
+
1133
+ async function cmdNext() {
1134
+ try {
1135
+ const f = await fetchNext(postureClient());
1136
+ if (!f) {
1137
+ console.log(green(' Queue is clear — no open coverage gaps.'));
278
1138
  return;
279
1139
  }
280
- });
1140
+ console.log();
1141
+ printFinding(f, ' ');
1142
+ if (f.fix?.type === 'create_policy_draft') {
1143
+ console.log(dim(` Fix: draft a ${f.fix.policyType} policy → dashclaw posture resolve ${f.key}`));
1144
+ } else if (f.fix?.deepLink) {
1145
+ console.log(dim(` Fix: ${f.fix.type} → ${f.fix.deepLink}`));
1146
+ }
1147
+ console.log();
1148
+ } catch (err) {
1149
+ console.error(`Error: ${err.message}`);
1150
+ process.exit(1);
1151
+ }
1152
+ }
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);
281
1204
  }
282
1205
 
283
1206
  // -- Router -------------------------------------------------------------------
284
1207
 
285
- switch (command) {
286
- case 'approvals':
287
- cmdApprovals();
288
- break;
289
- case 'approve':
290
- cmdApprove();
291
- break;
292
- case 'deny':
293
- cmdDeny();
294
- break;
295
- case 'help':
296
- case '--help':
297
- case '-h':
298
- cmdHelp();
299
- break;
300
- default:
301
- console.error(`Unknown command: ${command}`);
302
- cmdHelp();
303
- process.exit(1);
1208
+ const COMMANDS_NEEDING_CONFIG = new Set(['approvals', 'approve', 'deny', 'doctor', 'code', 'prompts', 'inbox', 'behavior', 'posture', 'next', 'env']);
1209
+ // `install` deliberately omitted: provisioning hooks and AGENTS.md shouldn't
1210
+ // require the user to have already configured API keys. If config happens to
1211
+ // be present, install will pick up baseUrl for the AGENTS.md instance link.
1212
+ // `codex notify` also opt-in: if no config, the notify fail-softs to skipped
1213
+ // rather than erroring (Codex never sees the error anyway — it spawns with
1214
+ // stdio nulled).
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;
304
1224
  }
1225
+
1226
+ async function loadCommandConfig() {
1227
+ if (COMMANDS_NEEDING_CONFIG.has(command)) {
1228
+ const config = await resolveConfig();
1229
+ if (!config) {
1230
+ console.error('Error: Missing required config (DASHCLAW_BASE_URL, DASHCLAW_API_KEY).');
1231
+ console.error('Set them as env vars, save with an interactive first run, or use a .env file.');
1232
+ process.exit(1);
1233
+ }
1234
+ applyConfig(config);
1235
+ return;
1236
+ }
1237
+ if (COMMANDS_OPTIONAL_CONFIG.has(command)) {
1238
+ const config = await resolveConfig({ interactive: false }).catch(() => null);
1239
+ if (config) {
1240
+ applyConfig(config);
1241
+ }
1242
+ }
1243
+ }
1244
+
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;
1275
+ }
1276
+ console.error(`Unknown command: ${command}`);
1277
+ await cmdHelp();
1278
+ process.exitCode = 1;
1279
+ }
1280
+
1281
+ main();