@dashclaw/cli 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/dashclaw.js CHANGED
@@ -1,12 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execFileSync } from 'child_process';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
4
6
  import { DashClaw } from 'dashclaw';
5
7
  import {
6
8
  bold, dim, inverse, colorByRisk, clearScreen,
7
9
  moveCursor, hideCursor, showCursor,
8
10
  green, red,
9
11
  } from '../lib/render.js';
12
+ import { runDoctor as runDoctorCommand } from '../lib/doctor.js';
13
+ import { resolveConfig, clearConfigFile, configPath } from '../lib/config.js';
14
+ import { runIngest, defaultClaudeProjectsDir } from '../lib/code/ingest.js';
15
+ import { runMemo } from '../lib/code/memo.js';
16
+ import { runApply } from '../lib/code/apply.js';
17
+ import {
18
+ runCodexIngest,
19
+ defaultCodexSessionsDir,
20
+ defaultCodexOutDir,
21
+ } from '../lib/code/ingest-codex.js';
22
+ import { installCodex, codexConfigPath, codexHooksDir } from '../lib/codex/install.js';
23
+ import { runCodexNotify } from '../lib/codex/notify.js';
24
+ import { apiRequest } from '../lib/api.js';
25
+ import { fetchPosture, fetchFindings, fetchNext, resolveFinding } from '../lib/posture.js';
10
26
 
11
27
  process.on('unhandledRejection', (reason) => {
12
28
  console.error('Unhandled Rejection:', reason);
@@ -15,23 +31,12 @@ process.on('unhandledRejection', (reason) => {
15
31
 
16
32
  // -- Config -------------------------------------------------------------------
17
33
 
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
- }
34
+ // Populated by main() before any command runs.
35
+ let baseUrl;
36
+ let apiKey;
37
+ let agentId;
32
38
 
33
39
  function createClient() {
34
- requireEnv();
35
40
  return new DashClaw({ baseUrl, apiKey, agentId });
36
41
  }
37
42
 
@@ -41,9 +46,12 @@ const args = process.argv.slice(2);
41
46
  const command = args[0] || 'help';
42
47
 
43
48
  function getFlag(name) {
44
- const idx = args.indexOf(name);
45
- if (idx === -1 || idx + 1 >= args.length) return undefined;
46
- return args[idx + 1];
49
+ for (let i = 0; i < args.length; i++) {
50
+ const a = args[i];
51
+ if (a === name) return i + 1 < args.length ? args[i + 1] : undefined; // --name value
52
+ if (a.startsWith(name + '=')) return a.slice(name.length + 1); // --name=value
53
+ }
54
+ return undefined;
47
55
  }
48
56
 
49
57
  // -- Commands -----------------------------------------------------------------
@@ -56,15 +64,85 @@ ${bold('Usage:')}
56
64
  dashclaw approvals Interactive approval inbox
57
65
  dashclaw approve <actionId> [--reason] Approve an action
58
66
  dashclaw deny <actionId> [--reason] Deny an action
67
+ dashclaw doctor Diagnose and auto-fix your DashClaw instance
68
+ --json Output as JSON (for CI/scripts)
69
+ --no-fix Diagnose only, skip auto-fixes
70
+ --category <list> Filter checks (e.g., database,config)
71
+ dashclaw code ingest [--dry-run] Backfill Claude Code transcripts from ~/.claude/projects
72
+ --projects-dir <path> Override the default scan directory
73
+ dashclaw code ingest-codex [--dry-run] Backfill Codex transcripts from ~/.codex/sessions
74
+ --sessions-dir <path> Override the default scan directory
75
+ --out <dir> Local output dir (default ~/.dashclaw/codex-sessions)
76
+ --endpoint <url> POST to <url> instead of writing local (advanced)
77
+ dashclaw code memo --project=<slug> Print the latest weekly memo for a project
78
+ --save Also write to ./memos/<weekTag>-<slug>.md
79
+ dashclaw code apply <manifestId> Apply an Optimal Files manifest (Phase 6+ feature)
80
+ --dest=<dir> Target project directory (required)
81
+ --yes Overwrite existing files when manifest says so
82
+ --allow-redactions Write files that contain redacted secret patterns
83
+ --overwrite Clobber existing .NEW side-by-side files
84
+ dashclaw install codex Provision DashClaw governance into Codex CLI
85
+ --project <path> Project to receive AGENTS.md (default: cwd)
86
+ --approval-policy <p> Codex approval_policy (default: on-request)
87
+ --include-notify Also wire Codex's notify config to dashclaw codex notify
88
+ dashclaw codex notify '<json>' Record a Codex turn-complete event
89
+ (called by Codex's notify config; always exits 0)
90
+ dashclaw prompts list [--category C] List prompt templates
91
+ dashclaw prompts get <id> Show a template
92
+ dashclaw prompts versions <id> List a template's versions
93
+ dashclaw prompts render <templateId> Render a prompt
94
+ --version-id <vid> Render a specific version instead of the active one
95
+ --var <key=value> Set a variable (repeatable)
96
+ --record Record the render as a prompt run
97
+ dashclaw prompts create --name N Create a template (admin)
98
+ --description <D> --category <C>
99
+ dashclaw prompts add-version <id> --content C Add a version (admin)
100
+ --changelog <L> --model-hint <M>
101
+ dashclaw prompts activate <id> <vid> Activate a version (admin)
102
+ dashclaw prompts stats [--template-id X] Prompt run analytics
103
+ dashclaw inbox list [--unread] [--limit N] List inbox messages
104
+ dashclaw inbox read <id> [<id> ...] Mark messages read
105
+ dashclaw inbox archive <id> [<id> ...] Archive messages
106
+ dashclaw behavior status Behavior Learning sample status (local recorder)
107
+ dashclaw behavior suggestions Evidence-backed policy suggestions per agent
108
+ --agent-id <id> Filter to one agent
109
+ dashclaw posture Governance posture score + remediation queue
110
+ dashclaw posture resolve <key> Draft a fix (inactive) | --snooze | --accept-risk
111
+ --note "..." Attach a note to the resolution
112
+ dashclaw next The single top open governance gap + its fix
113
+ dashclaw logout Remove saved config (~/.dashclaw/config.json)
59
114
  dashclaw help Show this help
60
115
 
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)
116
+ ${bold('Config:')}
117
+ On first run, prompts for DASHCLAW_BASE_URL and DASHCLAW_API_KEY and offers
118
+ to save them to ~/.dashclaw/config.json (mode 600). Env vars always override
119
+ the saved values.
65
120
  `);
66
121
  }
67
122
 
123
+ async function cmdLogout() {
124
+ const removed = clearConfigFile();
125
+ if (removed) {
126
+ console.log(`${green('Removed')} ${configPath()}`);
127
+ } else {
128
+ console.log(`${dim('No saved config at')} ${configPath()}`);
129
+ }
130
+ }
131
+
132
+ async function cmdDoctor() {
133
+ const jsonFlag = args.includes('--json');
134
+ const noFixFlag = args.includes('--no-fix');
135
+ const catIdx = args.indexOf('--category');
136
+ const catValue = catIdx !== -1 ? args[catIdx + 1] : undefined;
137
+ await runDoctorCommand({
138
+ baseUrl,
139
+ apiKey,
140
+ json: jsonFlag,
141
+ noFix: noFixFlag,
142
+ category: catValue,
143
+ });
144
+ }
145
+
68
146
  async function cmdApprove() {
69
147
  const actionId = args[1];
70
148
  if (!actionId) {
@@ -146,11 +224,41 @@ async function cmdApprovals() {
146
224
 
147
225
  function openReplay(actionId) {
148
226
  const url = `${baseUrl}/replay/${actionId}`;
227
+ let parsed;
228
+ try {
229
+ parsed = new URL(url);
230
+ } catch {
231
+ process.stdout.write(`\n Invalid URL, cannot open browser.\n`);
232
+ return;
233
+ }
234
+ const protocol = parsed.protocol;
235
+ if (protocol !== 'http:' && protocol !== 'https:') {
236
+ process.stdout.write(`\n Invalid URL, cannot open browser.\n`);
237
+ return;
238
+ }
239
+ if (!parsed.hostname) {
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
+ }
149
252
  try {
150
253
  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]);
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
+ }
154
262
  } catch (_) {
155
263
  process.stdout.write(`\n Could not open browser. URL: ${url}\n`);
156
264
  }
@@ -280,25 +388,776 @@ async function cmdApprovals() {
280
388
  });
281
389
  }
282
390
 
283
- // -- Router -------------------------------------------------------------------
391
+ // -- install subcommand group ------------------------------------------------
392
+
393
+ const __dirname = dirname(fileURLToPath(import.meta.url));
394
+ // cli/bin/ -> cli/ -> repo root
395
+ const REPO_ROOT = resolve(__dirname, '..', '..');
396
+
397
+ async function cmdInstallCodex() {
398
+ const projectDir = getFlag('--project') || process.cwd();
399
+ const approvalPolicy = getFlag('--approval-policy') || 'on-request';
400
+ const includeNotify = args.includes('--include-notify');
401
+
402
+ try {
403
+ const result = await installCodex({
404
+ repoRoot: REPO_ROOT,
405
+ projectDir,
406
+ baseUrl,
407
+ approvalPolicy,
408
+ includeNotify,
409
+ logger: console,
410
+ });
411
+
412
+ console.log();
413
+ console.log(` ${green('Done.')} DashClaw governance is wired into Codex.`);
414
+ console.log(` ${dim('Hooks:')} ${result.hooks.hooksDst}`);
415
+ console.log(` ${dim('Config:')} ${result.config.path}${result.config.backup ? dim(' (backup: ' + result.config.backup + ')') : ''}`);
416
+ console.log(` ${dim('AGENTS:')} ${result.agentsMd.path}${result.agentsMd.backup ? dim(' (backup: ' + result.agentsMd.backup + ')') : ''}`);
417
+ console.log();
418
+ console.log(` Next: open a new Codex session in ${projectDir} and run a governed tool call.`);
419
+ console.log(` Codex requires you to trust new hooks; it will prompt on first use.`);
420
+ } catch (err) {
421
+ console.error(`Error: ${err.message}`);
422
+ process.exit(1);
423
+ }
424
+ }
425
+
426
+ async function cmdInstall() {
427
+ const target = args[1];
428
+ switch (target) {
429
+ case 'codex':
430
+ return cmdInstallCodex();
431
+ default:
432
+ console.error(`Unknown install target: dashclaw install ${target || '(missing)'}\n` +
433
+ 'Try: dashclaw install codex [--project <path>] [--approval-policy <p>]');
434
+ process.exit(1);
435
+ }
436
+ }
437
+
438
+ // -- codex subcommand group --------------------------------------------------
439
+ //
440
+ // `dashclaw codex notify '<json>'` is invoked by Codex's legacy notify config.
441
+ // It records a turn-complete action_record in DashClaw. ALWAYS exits 0 so
442
+ // Codex never sees an error from the spawn.
443
+
444
+ async function cmdCodexNotify() {
445
+ // Skip the leading 'codex' and 'notify' tokens — runCodexNotify reads the
446
+ // JSON payload from the LAST argv slot (per Codex's notify contract).
447
+ const notifyArgv = args.slice(1); // includes 'notify' and the payload
448
+ await runCodexNotify({
449
+ argv: notifyArgv,
450
+ baseUrl,
451
+ apiKey,
452
+ agentId: agentId || 'codex',
453
+ logger: console,
454
+ });
455
+ process.exit(0);
456
+ }
457
+
458
+ async function cmdCodex() {
459
+ const sub = args[1];
460
+ switch (sub) {
461
+ case 'notify':
462
+ return cmdCodexNotify();
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
+ }
468
+ }
469
+
470
+ // -- code subcommand group ---------------------------------------------------
471
+
472
+ async function cmdCodeIngestCodex() {
473
+ const dryRun = args.includes('--dry-run');
474
+ const sessionsDir = getFlag('--sessions-dir') || defaultCodexSessionsDir();
475
+ const outDir = getFlag('--out') || defaultCodexOutDir();
476
+ const endpoint = getFlag('--endpoint') || null;
477
+ console.log(`Scanning ${sessionsDir} ...`);
478
+ const results = await runCodexIngest({
479
+ sessionsDir,
480
+ outDir,
481
+ endpoint,
482
+ apiKey,
483
+ dryRun,
484
+ logger: console,
485
+ });
486
+ if (!results.length) {
487
+ console.log('No sessions to ingest.');
488
+ return;
489
+ }
490
+ let written = 0, ingested = 0, dryRunCount = 0, skipped = 0, errors = 0;
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
+ }
501
+ console.log();
502
+ console.log(`Done. Written: ${written} Ingested: ${ingested} Dry-run: ${dryRunCount} Skipped: ${skipped} Errors: ${errors}`);
503
+ if (!endpoint && written > 0) {
504
+ console.log(dim(` Local sessions saved under ${outDir}.`));
505
+ console.log(dim(` Server-side codex ingest will be wired in a follow-up phase.`));
506
+ }
507
+ if (errors > 0) process.exit(2);
508
+ }
509
+
510
+ async function cmdCodeIngest() {
511
+ const dryRun = args.includes('--dry-run');
512
+ const projectsDir = getFlag('--projects-dir') || defaultClaudeProjectsDir();
513
+ console.log(`Scanning ${projectsDir} ...`);
514
+ const results = await runIngest({
515
+ baseUrl,
516
+ apiKey,
517
+ projectsDir,
518
+ dryRun,
519
+ });
520
+ if (!results.length) return;
521
+ let ingested = 0;
522
+ let skipped = 0;
523
+ let errors = 0;
524
+ for (const r of results) {
525
+ if (r.status === 'ingested') ingested++;
526
+ else if (r.status === 'skipped_unchanged' || r.status === 'skipped' || r.status === 'dry_run') skipped++;
527
+ else if (r.status === 'error') errors++;
528
+ }
529
+ console.log();
530
+ console.log(`Done. Ingested: ${ingested} Skipped: ${skipped} Errors: ${errors}`);
531
+ if (errors > 0) process.exit(2);
532
+ }
533
+
534
+ async function cmdCodeMemo() {
535
+ const project = getFlag('--project');
536
+ const save = args.includes('--save');
537
+ if (!project) {
538
+ console.error('Error: --project=<slug-or-id> is required.');
539
+ process.exit(1);
540
+ }
541
+ await runMemo({ baseUrl, apiKey, project, save });
542
+ }
543
+
544
+ async function cmdCodeApply() {
545
+ const manifestId = args[2];
546
+ const dest = getFlag('--dest');
547
+ const yes = args.includes('--yes');
548
+ const allowRedactions = args.includes('--allow-redactions');
549
+ const overwrite = args.includes('--overwrite');
550
+ if (!manifestId) {
551
+ console.error('Error: usage — dashclaw code apply <manifestId> --dest=<dir> [--yes] [--allow-redactions] [--overwrite]');
552
+ process.exit(1);
553
+ }
554
+ if (!dest) {
555
+ console.error('Error: --dest=<dir> is required.');
556
+ process.exit(1);
557
+ }
558
+ try {
559
+ const results = await runApply({
560
+ baseUrl,
561
+ apiKey,
562
+ manifestId,
563
+ dest,
564
+ yes,
565
+ allowRedactions,
566
+ allowOverwriteSideBySide: overwrite,
567
+ });
568
+ const summary = results.reduce((acc, r) => {
569
+ acc[r.status] = (acc[r.status] || 0) + 1;
570
+ return acc;
571
+ }, {});
572
+ console.log();
573
+ console.log('Apply summary:', JSON.stringify(summary));
574
+ } catch (err) {
575
+ console.error('Error: ' + err.message);
576
+ process.exit(1);
577
+ }
578
+ }
579
+
580
+ async function cmdCode() {
581
+ const sub = args[1];
582
+ switch (sub) {
583
+ case 'ingest':
584
+ return cmdCodeIngest();
585
+ case 'ingest-codex':
586
+ return cmdCodeIngestCodex();
587
+ case 'memo':
588
+ return cmdCodeMemo();
589
+ case 'apply':
590
+ return cmdCodeApply();
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
+ }
599
+ }
600
+
601
+ // -- prompts subcommand group ------------------------------------------------
602
+ //
603
+ // Direct-API calls (apiRequest) rather than SDK methods: the CLI imports the
604
+ // PUBLISHED `dashclaw` package, which may lag this repo and lack newly-added
605
+ // prompt methods. fetch + x-api-key against the resolved baseUrl/apiKey is the
606
+ // durable path.
607
+
608
+ function promptsClient() {
609
+ return { baseUrl, apiKey };
610
+ }
611
+
612
+ // Collect repeated `--var key=value` flags into a { key: value } object.
613
+ function parseVars() {
614
+ const vars = {};
615
+ for (let i = 0; i < args.length; i++) {
616
+ if (args[i] === '--var' && i + 1 < args.length) {
617
+ const pair = args[i + 1];
618
+ const eq = pair.indexOf('=');
619
+ if (eq === -1) {
620
+ console.error(`Error: --var expects key=value, got "${pair}"`);
621
+ process.exit(1);
622
+ }
623
+ vars[pair.slice(0, eq)] = pair.slice(eq + 1);
624
+ }
625
+ }
626
+ return vars;
627
+ }
284
628
 
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();
629
+ async function cmdPromptsList() {
630
+ const category = getFlag('--category');
631
+ try {
632
+ const data = await apiRequest(promptsClient(), 'GET', '/api/prompts/templates', {
633
+ query: { category },
634
+ });
635
+ const templates = data.templates || [];
636
+ if (templates.length === 0) {
637
+ console.log(dim(' No templates.'));
638
+ return;
639
+ }
640
+ for (const t of templates) {
641
+ console.log(
642
+ ` ${bold(t.id)} ${t.name || '-'} ${dim('[' + (t.category || 'uncategorized') + ']')}` +
643
+ ` active v${t.active_version ?? '-'} ${dim('(' + (t.version_count ?? 0) + ' versions)')}`,
644
+ );
645
+ }
646
+ } catch (err) {
647
+ console.error(`Error: ${err.message}`);
303
648
  process.exit(1);
649
+ }
650
+ }
651
+
652
+ async function cmdPromptsGet() {
653
+ const id = args[2];
654
+ if (!id) {
655
+ console.error('Error: Missing template ID. Usage: dashclaw prompts get <id>');
656
+ process.exit(1);
657
+ }
658
+ try {
659
+ const data = await apiRequest(promptsClient(), 'GET', `/api/prompts/templates/${encodeURIComponent(id)}`);
660
+ console.log(JSON.stringify(data, null, 2));
661
+ } catch (err) {
662
+ console.error(`Error: ${err.message}`);
663
+ process.exit(1);
664
+ }
304
665
  }
666
+
667
+ async function cmdPromptsVersions() {
668
+ const id = args[2];
669
+ if (!id) {
670
+ console.error('Error: Missing template ID. Usage: dashclaw prompts versions <id>');
671
+ process.exit(1);
672
+ }
673
+ try {
674
+ const data = await apiRequest(
675
+ promptsClient(), 'GET', `/api/prompts/templates/${encodeURIComponent(id)}/versions`,
676
+ );
677
+ const versions = data.versions || [];
678
+ if (versions.length === 0) {
679
+ console.log(dim(' No versions.'));
680
+ return;
681
+ }
682
+ for (const v of versions) {
683
+ const active = v.is_active ? green(' (active)') : '';
684
+ console.log(` v${v.version} ${bold(v.id)}${active} ${dim(v.changelog || '')}`);
685
+ }
686
+ } catch (err) {
687
+ console.error(`Error: ${err.message}`);
688
+ process.exit(1);
689
+ }
690
+ }
691
+
692
+ async function cmdPromptsRender() {
693
+ const versionId = getFlag('--version-id');
694
+ const templateId = args[2] && !args[2].startsWith('--') ? args[2] : undefined;
695
+ if (!templateId && !versionId) {
696
+ console.error('Error: usage — dashclaw prompts render <templateId> [--var k=v ...] [--record]');
697
+ console.error(' or: dashclaw prompts render --version-id <vid> [--var k=v ...]');
698
+ process.exit(1);
699
+ }
700
+ const variables = parseVars();
701
+ const record = args.includes('--record');
702
+ try {
703
+ const data = await apiRequest(promptsClient(), 'POST', '/api/prompts/render', {
704
+ body: {
705
+ template_id: templateId,
706
+ version_id: versionId,
707
+ variables,
708
+ agent_id: agentId,
709
+ record,
710
+ },
711
+ });
712
+ console.log(data.rendered ?? '');
713
+ if (Array.isArray(data.parameters) && data.parameters.length > 0) {
714
+ console.log();
715
+ console.log(dim(` parameters: ${data.parameters.join(', ')}`));
716
+ }
717
+ } catch (err) {
718
+ console.error(`Error: ${err.message}`);
719
+ process.exit(1);
720
+ }
721
+ }
722
+
723
+ async function cmdPromptsCreate() {
724
+ const name = getFlag('--name');
725
+ if (!name) {
726
+ console.error('Error: --name is required. Usage: dashclaw prompts create --name N [--description D] [--category C]');
727
+ process.exit(1);
728
+ }
729
+ const description = getFlag('--description');
730
+ const category = getFlag('--category');
731
+ try {
732
+ const data = await apiRequest(promptsClient(), 'POST', '/api/prompts/templates', {
733
+ body: { name, description, category },
734
+ });
735
+ console.log(` ${green('Created')} ${bold(data.id)} ${data.name}`);
736
+ } catch (err) {
737
+ console.error(`Error: ${err.message}`);
738
+ process.exit(1);
739
+ }
740
+ }
741
+
742
+ async function cmdPromptsAddVersion() {
743
+ const templateId = args[2];
744
+ if (!templateId || templateId.startsWith('--')) {
745
+ console.error('Error: usage — dashclaw prompts add-version <templateId> --content C [--changelog L] [--model-hint M]');
746
+ process.exit(1);
747
+ }
748
+ const content = getFlag('--content');
749
+ if (!content) {
750
+ console.error('Error: --content is required.');
751
+ process.exit(1);
752
+ }
753
+ const changelog = getFlag('--changelog');
754
+ const modelHint = getFlag('--model-hint');
755
+ try {
756
+ const data = await apiRequest(
757
+ promptsClient(), 'POST', `/api/prompts/templates/${encodeURIComponent(templateId)}/versions`,
758
+ { body: { content, changelog, model_hint: modelHint } },
759
+ );
760
+ console.log(` ${green('Added')} v${data.version} ${bold(data.id)}`);
761
+ } catch (err) {
762
+ console.error(`Error: ${err.message}`);
763
+ process.exit(1);
764
+ }
765
+ }
766
+
767
+ async function cmdPromptsActivate() {
768
+ const templateId = args[2];
769
+ const versionId = args[3];
770
+ if (!templateId || !versionId) {
771
+ console.error('Error: usage — dashclaw prompts activate <templateId> <versionId>');
772
+ process.exit(1);
773
+ }
774
+ try {
775
+ await apiRequest(
776
+ promptsClient(), 'POST',
777
+ `/api/prompts/templates/${encodeURIComponent(templateId)}/versions/${encodeURIComponent(versionId)}`,
778
+ );
779
+ console.log(` ${green('Activated')} version ${bold(versionId)}`);
780
+ } catch (err) {
781
+ console.error(`Error: ${err.message}`);
782
+ process.exit(1);
783
+ }
784
+ }
785
+
786
+ async function cmdPromptsStats() {
787
+ const templateId = getFlag('--template-id');
788
+ try {
789
+ const data = await apiRequest(promptsClient(), 'GET', '/api/prompts/stats', {
790
+ query: { template_id: templateId },
791
+ });
792
+ console.log(JSON.stringify(data, null, 2));
793
+ } catch (err) {
794
+ console.error(`Error: ${err.message}`);
795
+ process.exit(1);
796
+ }
797
+ }
798
+
799
+ async function cmdPrompts() {
800
+ const sub = args[1];
801
+ switch (sub) {
802
+ case 'list':
803
+ return cmdPromptsList();
804
+ case 'get':
805
+ return cmdPromptsGet();
806
+ case 'versions':
807
+ return cmdPromptsVersions();
808
+ case 'render':
809
+ return cmdPromptsRender();
810
+ case 'create':
811
+ return cmdPromptsCreate();
812
+ case 'add-version':
813
+ return cmdPromptsAddVersion();
814
+ case 'activate':
815
+ return cmdPromptsActivate();
816
+ case 'stats':
817
+ return cmdPromptsStats();
818
+ default:
819
+ console.error(`Unknown subcommand: dashclaw prompts ${sub || '(missing)'}\n` +
820
+ 'Try: dashclaw prompts list [--category C]\n' +
821
+ ' dashclaw prompts get <id>\n' +
822
+ ' dashclaw prompts versions <id>\n' +
823
+ ' dashclaw prompts render <templateId> [--var k=v ...] [--record]\n' +
824
+ ' dashclaw prompts create --name N [--description D] [--category C]\n' +
825
+ ' dashclaw prompts add-version <templateId> --content C [--changelog L] [--model-hint M]\n' +
826
+ ' dashclaw prompts activate <templateId> <versionId>\n' +
827
+ ' dashclaw prompts stats [--template-id X]');
828
+ process.exit(1);
829
+ }
830
+ }
831
+
832
+ // -- inbox subcommand group --------------------------------------------------
833
+ //
834
+ // Direct-API calls against /api/messages (durable path, see prompts group).
835
+ // Uses the resolved `agentId` for direction=inbox filtering and PATCH attribution.
836
+
837
+ function inboxClient() {
838
+ return { baseUrl, apiKey };
839
+ }
840
+
841
+ async function cmdInboxList() {
842
+ const unread = args.includes('--unread');
843
+ const limitFlag = getFlag('--limit');
844
+ try {
845
+ const data = await apiRequest(inboxClient(), 'GET', '/api/messages', {
846
+ query: {
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}`));
866
+ } catch (err) {
867
+ console.error(`Error: ${err.message}`);
868
+ process.exit(1);
869
+ }
870
+ }
871
+
872
+ async function cmdInboxUpdate(action) {
873
+ const ids = args.slice(2).filter((a) => !a.startsWith('--'));
874
+ if (ids.length === 0) {
875
+ console.error(`Error: usage — dashclaw inbox ${action} <id> [<id> ...]`);
876
+ process.exit(1);
877
+ }
878
+ try {
879
+ const data = await apiRequest(inboxClient(), 'PATCH', '/api/messages', {
880
+ body: { message_ids: ids, action, agent_id: agentId },
881
+ });
882
+ console.log(` ${green('Updated')} ${data.updated ?? 0} message(s)`);
883
+ } catch (err) {
884
+ console.error(`Error: ${err.message}`);
885
+ process.exit(1);
886
+ }
887
+ }
888
+
889
+ async function cmdInbox() {
890
+ const sub = args[1];
891
+ switch (sub) {
892
+ case 'list':
893
+ return cmdInboxList();
894
+ case 'read':
895
+ return cmdInboxUpdate('read');
896
+ case 'archive':
897
+ return cmdInboxUpdate('archive');
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
+ }
905
+ }
906
+
907
+ // -- behavior subcommand group -----------------------------------------------
908
+ //
909
+ // Behavior Learning / Policy Coach. Read-only inspection of the locally-recorded
910
+ // behavior samples and the evidence-backed policy suggestions derived from them.
911
+ // Direct-API calls (durable path, see prompts/inbox groups). Adopt/dismiss are
912
+ // intentionally UI-only in V1 — they require simulation review.
913
+
914
+ function behaviorClient() {
915
+ return { baseUrl, apiKey };
916
+ }
917
+
918
+ async function cmdBehaviorStatus() {
919
+ try {
920
+ const data = await apiRequest(behaviorClient(), 'GET', '/api/behavior/samples');
921
+ console.log(` Recorder: ${data.recorder_enabled ? green('on') : dim('off')}`);
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
+ }
929
+ } catch (err) {
930
+ console.error(`Error: ${err.message}`);
931
+ process.exit(1);
932
+ }
933
+ }
934
+
935
+ async function cmdBehaviorSuggestions() {
936
+ const agent = getFlag('--agent-id') || agentId;
937
+ try {
938
+ const data = await apiRequest(behaviorClient(), 'GET', '/api/behavior/suggestions', {
939
+ query: { agent_id: agent },
940
+ });
941
+ const suggestions = data.suggestions || [];
942
+ if (suggestions.length === 0) {
943
+ console.log(dim(` No suggestions (${data.sample_count ?? 0} samples analyzed).`));
944
+ return;
945
+ }
946
+ for (const s of suggestions) {
947
+ const kind = s.enforceable ? green('draft') : dim('advisory');
948
+ console.log(` ${bold(s.type)} ${dim(s.agent_id)} ${s.confidence}% [${kind}] ${dim(s.severity)}`);
949
+ console.log(` ${s.expected_effect}`);
950
+ console.log(dim(` evidence: ${s.matching_sample_size}/${s.sample_size} · id ${s.id}`));
951
+ }
952
+ console.log();
953
+ console.log(dim(' Review + simulate + adopt from the Policy Coach UI (/policy-coach).'));
954
+ } catch (err) {
955
+ console.error(`Error: ${err.message}`);
956
+ process.exit(1);
957
+ }
958
+ }
959
+
960
+ async function cmdBehavior() {
961
+ const sub = args[1];
962
+ switch (sub) {
963
+ case 'status':
964
+ return cmdBehaviorStatus();
965
+ case 'suggestions':
966
+ return cmdBehaviorSuggestions();
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
+ }
973
+ }
974
+
975
+ // -- posture subcommand group ------------------------------------------------
976
+ //
977
+ // Direct-API (apiRequest via cli/lib/posture.js) — the governance posture score
978
+ // + the prioritized remediation queue. `posture resolve` is DRAFT-ONLY: it can
979
+ // create an inactive policy draft / snooze / accept risk, never activate
980
+ // enforcement (a human does that at /policies).
981
+
982
+ function postureClient() {
983
+ return { baseUrl, apiKey };
984
+ }
985
+
986
+ const POSTURE_DIM_LABEL = {
987
+ identity: 'Identity', enforcement: 'Enforcement', spend: 'Spend',
988
+ auditability: 'Audit', approval: 'Approval', data_protection: 'Data',
989
+ };
990
+
991
+ function printFinding(f, indent = ' ') {
992
+ console.log(`${indent}${bold('+' + (f.scoreDelta ?? 0))} ${dim('[' + f.severity + ']')} ${f.title}`);
993
+ console.log(dim(`${indent} ${f.key}`));
994
+ }
995
+
996
+ async function cmdPostureShow() {
997
+ try {
998
+ const client = postureClient();
999
+ 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
+ const findings = queue.findings || [];
1014
+ console.log(dim(` Next — ${findings.length} open`));
1015
+ if (findings.length === 0) {
1016
+ console.log(green(' Queue is clear — no open coverage gaps.'));
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
+ }
1021
+ console.log();
1022
+ } catch (err) {
1023
+ console.error(`Error: ${err.message}`);
1024
+ process.exit(1);
1025
+ }
1026
+ }
1027
+
1028
+ async function cmdPostureResolve() {
1029
+ const key = args[2];
1030
+ if (!key || key.startsWith('-')) {
1031
+ console.error('Error: usage — dashclaw posture resolve <key> [--snooze | --accept-risk] [--note "..."]');
1032
+ process.exit(1);
1033
+ }
1034
+ const action = args.includes('--snooze') ? 'snooze'
1035
+ : args.includes('--accept-risk') ? 'accept_risk' : 'create_draft';
1036
+ try {
1037
+ await resolveFinding(postureClient(), key, action, getFlag('--note'));
1038
+ if (action === 'create_draft') {
1039
+ console.log(green(' Draft created (inactive).') +
1040
+ dim(' Activate it at /policies and rescan — drafting does not change the score.'));
1041
+ } else {
1042
+ console.log(green(` Finding ${action === 'snooze' ? 'snoozed' : 'risk-accepted'}.`));
1043
+ }
1044
+ } catch (err) {
1045
+ console.error(`Error: ${err.message}`);
1046
+ process.exit(1);
1047
+ }
1048
+ }
1049
+
1050
+ async function cmdPosture() {
1051
+ const sub = args[1];
1052
+ if (sub === undefined) return cmdPostureShow();
1053
+ if (sub === 'resolve') return cmdPostureResolve();
1054
+ console.error(`Unknown subcommand: dashclaw posture ${sub}\n` +
1055
+ 'Try: dashclaw posture\n dashclaw posture resolve <key>');
1056
+ process.exit(1);
1057
+ }
1058
+
1059
+ async function cmdNext() {
1060
+ try {
1061
+ const f = await fetchNext(postureClient());
1062
+ if (!f) {
1063
+ console.log(green(' Queue is clear — no open coverage gaps.'));
1064
+ return;
1065
+ }
1066
+ console.log();
1067
+ printFinding(f, ' ');
1068
+ if (f.fix?.type === 'create_policy_draft') {
1069
+ console.log(dim(` Fix: draft a ${f.fix.policyType} policy → dashclaw posture resolve ${f.key}`));
1070
+ } else if (f.fix?.deepLink) {
1071
+ console.log(dim(` Fix: ${f.fix.type} → ${f.fix.deepLink}`));
1072
+ }
1073
+ console.log();
1074
+ } catch (err) {
1075
+ console.error(`Error: ${err.message}`);
1076
+ process.exit(1);
1077
+ }
1078
+ }
1079
+
1080
+ // -- Router -------------------------------------------------------------------
1081
+
1082
+ const COMMANDS_NEEDING_CONFIG = new Set(['approvals', 'approve', 'deny', 'doctor', 'code', 'prompts', 'inbox', 'behavior', 'posture', 'next']);
1083
+ // `install` deliberately omitted: provisioning hooks and AGENTS.md shouldn't
1084
+ // require the user to have already configured API keys. If config happens to
1085
+ // be present, install will pick up baseUrl for the AGENTS.md instance link.
1086
+ // `codex notify` also opt-in: if no config, the notify fail-softs to skipped
1087
+ // rather than erroring (Codex never sees the error anyway — it spawns with
1088
+ // stdio nulled).
1089
+ const COMMANDS_OPTIONAL_CONFIG = new Set(['install', 'codex']);
1090
+
1091
+ async function main() {
1092
+ if (COMMANDS_NEEDING_CONFIG.has(command)) {
1093
+ const config = await resolveConfig();
1094
+ if (!config) {
1095
+ console.error('Error: Missing required config (DASHCLAW_BASE_URL, DASHCLAW_API_KEY).');
1096
+ console.error('Set them as env vars, save with an interactive first run, or use a .env file.');
1097
+ process.exit(1);
1098
+ }
1099
+ baseUrl = config.baseUrl;
1100
+ apiKey = config.apiKey;
1101
+ agentId = config.agentId;
1102
+ } else if (COMMANDS_OPTIONAL_CONFIG.has(command)) {
1103
+ const config = await resolveConfig({ interactive: false }).catch(() => null);
1104
+ if (config) {
1105
+ baseUrl = config.baseUrl;
1106
+ apiKey = config.apiKey;
1107
+ agentId = config.agentId;
1108
+ }
1109
+ }
1110
+
1111
+ switch (command) {
1112
+ case 'approvals':
1113
+ await cmdApprovals();
1114
+ break;
1115
+ case 'approve':
1116
+ await cmdApprove();
1117
+ break;
1118
+ case 'deny':
1119
+ await cmdDeny();
1120
+ break;
1121
+ case 'doctor':
1122
+ await cmdDoctor();
1123
+ break;
1124
+ case 'logout':
1125
+ await cmdLogout();
1126
+ break;
1127
+ case 'code':
1128
+ await cmdCode();
1129
+ break;
1130
+ case 'install':
1131
+ await cmdInstall();
1132
+ break;
1133
+ case 'codex':
1134
+ await cmdCodex();
1135
+ break;
1136
+ case 'prompts':
1137
+ await cmdPrompts();
1138
+ break;
1139
+ case 'inbox':
1140
+ await cmdInbox();
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);
1160
+ }
1161
+ }
1162
+
1163
+ main();