@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/README.md +104 -0
- package/bin/dashclaw.js +903 -44
- package/lib/api.js +64 -0
- package/lib/code/apply.js +164 -0
- package/lib/code/codex-parser.vendored.js +360 -0
- package/lib/code/ingest-codex.js +244 -0
- package/lib/code/ingest.js +245 -0
- package/lib/code/memo.js +57 -0
- package/lib/code/vendored.js +219 -0
- package/lib/codex/install.js +405 -0
- package/lib/codex/notify.js +203 -0
- package/lib/config.js +160 -0
- package/lib/doctor.js +209 -0
- package/lib/posture.js +45 -0
- package/package.json +21 -18
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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('
|
|
62
|
-
DASHCLAW_BASE_URL
|
|
63
|
-
|
|
64
|
-
|
|
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')
|
|
152
|
-
|
|
153
|
-
else
|
|
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
|
-
// --
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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();
|