@dashclaw/cli 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -1
- package/bin/dashclaw.js +482 -364
- package/lib/claude/install.js +386 -0
- package/lib/code/ingest.js +17 -1
- package/lib/config.js +2 -2
- package/lib/cost.js +108 -0
- package/lib/doctor.js +138 -77
- package/lib/env.js +69 -0
- package/package.json +34 -21
package/lib/doctor.js
CHANGED
|
@@ -61,27 +61,36 @@ function nextStepFor(check, { noFix }) {
|
|
|
61
61
|
return GUIDANCE[check.id] || null;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
* Run doctor via the API and render results.
|
|
66
|
-
* @param {{ baseUrl: string, apiKey: string, json?: boolean, noFix?: boolean, category?: string }} options
|
|
67
|
-
*/
|
|
68
|
-
export async function runDoctor({ baseUrl, apiKey, json, noFix, category }) {
|
|
69
|
-
const base = baseUrl.replace(/\/+$/, '');
|
|
70
|
-
const headers = { 'Content-Type': 'application/json', 'x-api-key': apiKey };
|
|
71
|
-
|
|
64
|
+
function doctorUrl(base, category) {
|
|
72
65
|
let url = `${base}/api/doctor?include_fixes=true`;
|
|
73
66
|
if (category) url += `&category=${encodeURIComponent(category)}`;
|
|
67
|
+
return url;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function doctorHeaders(apiKey) {
|
|
71
|
+
return { 'Content-Type': 'application/json', 'x-api-key': apiKey };
|
|
72
|
+
}
|
|
74
73
|
|
|
74
|
+
function exitFetchError(base, err) {
|
|
75
|
+
console.error(red(`\nError: Could not reach DashClaw at ${base}`));
|
|
76
|
+
console.error(dim(` ${err.cause?.code || err.message}`));
|
|
77
|
+
console.error(dim(` Check DASHCLAW_BASE_URL and confirm your instance is running.\n`));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function fetchDoctorResult({ base, apiKey, category }) {
|
|
82
|
+
const headers = doctorHeaders(apiKey);
|
|
75
83
|
let res;
|
|
76
84
|
try {
|
|
77
|
-
res = await fetch(
|
|
85
|
+
res = await fetch(doctorUrl(base, category), { headers });
|
|
78
86
|
} catch (err) {
|
|
79
|
-
|
|
80
|
-
console.error(dim(` ${err.cause?.code || err.message}`));
|
|
81
|
-
console.error(dim(` Check DASHCLAW_BASE_URL and confirm your instance is running.\n`));
|
|
82
|
-
process.exit(1);
|
|
87
|
+
exitFetchError(base, err);
|
|
83
88
|
}
|
|
89
|
+
await handleDoctorResponse(base, res);
|
|
90
|
+
return { result: await res.json(), headers };
|
|
91
|
+
}
|
|
84
92
|
|
|
93
|
+
async function handleDoctorResponse(base, res) {
|
|
85
94
|
if (res.status === 401 || res.status === 403) {
|
|
86
95
|
console.error(red(`\nError: API key rejected by ${base} (${res.status}).`));
|
|
87
96
|
console.error(dim(` Check DASHCLAW_API_KEY matches the key on your instance.\n`));
|
|
@@ -93,82 +102,100 @@ export async function runDoctor({ baseUrl, apiKey, json, noFix, category }) {
|
|
|
93
102
|
console.error(red(`Doctor check failed (${res.status}): ${errText}`));
|
|
94
103
|
process.exit(1);
|
|
95
104
|
}
|
|
105
|
+
}
|
|
96
106
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
process.exit(result.status === 'healthy' ? 0 : 1);
|
|
102
|
-
}
|
|
107
|
+
function exitJson(result) {
|
|
108
|
+
console.log(JSON.stringify(result, null, 2));
|
|
109
|
+
process.exit(result.status === 'healthy' ? 0 : 1);
|
|
110
|
+
}
|
|
103
111
|
|
|
104
|
-
|
|
112
|
+
function renderHeader(base) {
|
|
105
113
|
const hostLabel = base.replace(/^https?:\/\//, '');
|
|
106
114
|
console.log();
|
|
107
115
|
console.log(` ${BRAND('[DashClaw]')} ${bold('Doctor')} ${dim(hostLabel)}`);
|
|
108
116
|
console.log();
|
|
117
|
+
}
|
|
109
118
|
|
|
110
|
-
|
|
119
|
+
function groupChecks(checks) {
|
|
111
120
|
const grouped = {};
|
|
112
|
-
for (const check of
|
|
121
|
+
for (const check of checks) {
|
|
113
122
|
if (!grouped[check.category]) grouped[check.category] = [];
|
|
114
123
|
grouped[check.category].push(check);
|
|
115
124
|
}
|
|
125
|
+
return grouped;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function renderCheck(check, noFix) {
|
|
129
|
+
const icon = ICONS[check.status] || '?';
|
|
130
|
+
const titleStyled = check.status === 'pass' ? check.title : bold(check.title);
|
|
131
|
+
console.log(` ${icon} ${titleStyled}`);
|
|
132
|
+
|
|
133
|
+
if (check.status !== 'pass') {
|
|
134
|
+
console.log(` ${dim(check.message)}`);
|
|
135
|
+
const tip = nextStepFor(check, { noFix });
|
|
136
|
+
if (tip) {
|
|
137
|
+
console.log(` ${dim('\u2192')} ${tip}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
116
141
|
|
|
142
|
+
function renderGroupedChecks(checks, noFix) {
|
|
143
|
+
const grouped = groupChecks(checks);
|
|
117
144
|
for (const cat of CATEGORY_ORDER) {
|
|
118
|
-
const
|
|
119
|
-
if (!
|
|
145
|
+
const categoryChecks = grouped[cat];
|
|
146
|
+
if (!categoryChecks || categoryChecks.length === 0) continue;
|
|
120
147
|
|
|
121
148
|
console.log(` ${bold(CATEGORY_LABELS[cat] || cat)}`);
|
|
122
|
-
for (const check of
|
|
123
|
-
|
|
124
|
-
const titleStyled = check.status === 'pass' ? check.title : bold(check.title);
|
|
125
|
-
console.log(` ${icon} ${titleStyled}`);
|
|
126
|
-
|
|
127
|
-
if (check.status !== 'pass') {
|
|
128
|
-
console.log(` ${dim(check.message)}`);
|
|
129
|
-
const tip = nextStepFor(check, { noFix });
|
|
130
|
-
if (tip) {
|
|
131
|
-
console.log(` ${dim('\u2192')} ${tip}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
149
|
+
for (const check of categoryChecks) {
|
|
150
|
+
renderCheck(check, noFix);
|
|
134
151
|
}
|
|
135
152
|
console.log();
|
|
136
153
|
}
|
|
154
|
+
}
|
|
137
155
|
|
|
138
|
-
|
|
156
|
+
function autoFixableChecks(result) {
|
|
157
|
+
return result.checks.filter((c) => c.status === 'fail' && c.fix?.type === 'auto');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function applyDoctorFix({ base, headers, check }) {
|
|
161
|
+
try {
|
|
162
|
+
const fixRes = await fetch(`${base}/api/doctor/fix`, {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers,
|
|
165
|
+
body: JSON.stringify({ action: check.fix.action }),
|
|
166
|
+
});
|
|
167
|
+
const fixResult = await fixRes.json();
|
|
168
|
+
if (fixResult.applied) {
|
|
169
|
+
console.log(` ${green('\u2192')} Fixed: ${fixResult.description}`);
|
|
170
|
+
return { fixed: 1, recheck: fixResult.recheck || null };
|
|
171
|
+
}
|
|
172
|
+
console.log(` ${dim('\u2192')} Skipped: ${fixResult.description}`);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.log(` ${red('\u2717')} Fix "${check.fix.action}" failed: ${err.cause?.code || err.message}`);
|
|
175
|
+
}
|
|
176
|
+
return { fixed: 0, recheck: null };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function applyAutoFixes({ base, headers, result, noFix }) {
|
|
139
180
|
let fixCount = 0;
|
|
140
181
|
let latestRecheck = null;
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
});
|
|
152
|
-
const fixResult = await fixRes.json();
|
|
153
|
-
if (fixResult.applied) {
|
|
154
|
-
console.log(` ${green('\u2192')} Fixed: ${fixResult.description}`);
|
|
155
|
-
fixCount++;
|
|
156
|
-
if (fixResult.recheck) latestRecheck = fixResult.recheck;
|
|
157
|
-
} else {
|
|
158
|
-
console.log(` ${dim('\u2192')} Skipped: ${fixResult.description}`);
|
|
159
|
-
}
|
|
160
|
-
} catch (err) {
|
|
161
|
-
console.log(` ${red('\u2717')} Fix "${check.fix.action}" failed: ${err.cause?.code || err.message}`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
console.log();
|
|
165
|
-
}
|
|
182
|
+
if (noFix) return { fixCount, latestRecheck };
|
|
183
|
+
|
|
184
|
+
const fixable = autoFixableChecks(result);
|
|
185
|
+
if (fixable.length === 0) return { fixCount, latestRecheck };
|
|
186
|
+
|
|
187
|
+
console.log(` ${bold('Applying auto-fixes...')}`);
|
|
188
|
+
for (const check of fixable) {
|
|
189
|
+
const outcome = await applyDoctorFix({ base, headers, check });
|
|
190
|
+
fixCount += outcome.fixed;
|
|
191
|
+
latestRecheck = outcome.recheck || latestRecheck;
|
|
166
192
|
}
|
|
193
|
+
console.log();
|
|
194
|
+
return { fixCount, latestRecheck };
|
|
195
|
+
}
|
|
167
196
|
|
|
168
|
-
|
|
169
|
-
const reporting = latestRecheck || result;
|
|
197
|
+
function renderSummary(reporting) {
|
|
170
198
|
const { pass, warn, fail } = reporting.summary;
|
|
171
|
-
|
|
172
199
|
console.log(hr());
|
|
173
200
|
console.log();
|
|
174
201
|
|
|
@@ -179,31 +206,65 @@ export async function runDoctor({ baseUrl, apiKey, json, noFix, category }) {
|
|
|
179
206
|
];
|
|
180
207
|
console.log(` ${segments.join(' ' + dim('\u00b7') + ' ')}`);
|
|
181
208
|
console.log();
|
|
209
|
+
}
|
|
182
210
|
|
|
183
|
-
|
|
184
|
-
const healthy = reporting.status === 'healthy';
|
|
185
|
-
|
|
211
|
+
function renderFixCount(fixCount) {
|
|
186
212
|
if (fixCount > 0) {
|
|
187
213
|
console.log(` ${green('\u2713')} ${fixCount} issue${fixCount !== 1 ? 's' : ''} auto-fixed this run.`);
|
|
188
214
|
}
|
|
215
|
+
}
|
|
189
216
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
).
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
)
|
|
198
|
-
|
|
217
|
+
function renderNoFixHint(result, noFix) {
|
|
218
|
+
if (!noFix) return;
|
|
219
|
+
const autoFixable = result.checks.filter(
|
|
220
|
+
(c) => c.status !== 'pass' && c.fix?.type === 'auto',
|
|
221
|
+
).length;
|
|
222
|
+
if (autoFixable > 0) {
|
|
223
|
+
console.log(
|
|
224
|
+
` ${BRAND('\u2192')} ${autoFixable} issue${autoFixable !== 1 ? 's' : ''} can be auto-fixed. Run: ${bold('dashclaw doctor')}`,
|
|
225
|
+
);
|
|
199
226
|
}
|
|
227
|
+
}
|
|
200
228
|
|
|
229
|
+
function renderHealthFooter(base, healthy) {
|
|
201
230
|
if (!healthy) {
|
|
202
231
|
console.log(` ${dim('Docs:')} ${base}/setup`);
|
|
203
232
|
} else {
|
|
204
233
|
console.log(` ${green('\u2713')} ${bold('All systems healthy')} — ${dim('ready to govern actions')}`);
|
|
205
234
|
}
|
|
206
235
|
console.log();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Run doctor via the API and render results.
|
|
240
|
+
* @param {{ baseUrl: string, apiKey: string, json?: boolean, noFix?: boolean, category?: string }} options
|
|
241
|
+
*/
|
|
242
|
+
export async function runDoctor({ baseUrl, apiKey, json, noFix, category }) {
|
|
243
|
+
const base = baseUrl.replace(/\/+$/, '');
|
|
244
|
+
const { result, headers } = await fetchDoctorResult({ base, apiKey, category });
|
|
245
|
+
|
|
246
|
+
if (json) {
|
|
247
|
+
exitJson(result);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// --- Header ----------------------------------------------------------------
|
|
251
|
+
renderHeader(base);
|
|
252
|
+
|
|
253
|
+
// --- Grouped checks --------------------------------------------------------
|
|
254
|
+
renderGroupedChecks(result.checks, noFix);
|
|
255
|
+
|
|
256
|
+
// --- Auto-fix (remote fixes only; local-only fixes blocked by API) --------
|
|
257
|
+
const { fixCount, latestRecheck } = await applyAutoFixes({ base, headers, result, noFix });
|
|
258
|
+
|
|
259
|
+
// --- Summary ---------------------------------------------------------------
|
|
260
|
+
const reporting = latestRecheck || result;
|
|
261
|
+
renderSummary(reporting);
|
|
262
|
+
|
|
263
|
+
// --- Contextual footer -----------------------------------------------------
|
|
264
|
+
const healthy = reporting.status === 'healthy';
|
|
265
|
+
renderFixCount(fixCount);
|
|
266
|
+
renderNoFixHint(result, noFix);
|
|
267
|
+
renderHealthFooter(base, healthy);
|
|
207
268
|
|
|
208
269
|
process.exit(healthy ? 0 : 1);
|
|
209
270
|
}
|
package/lib/env.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// cli/lib/env.js
|
|
2
|
+
//
|
|
3
|
+
// `dashclaw env [--agent <id>] [-- <command...>]` — fetch the delivery-enabled
|
|
4
|
+
// managed-secret bundle from GET /api/secrets/env and inject it into a child
|
|
5
|
+
// process environment MEMORY-ONLY. Secret VALUES are never written to disk,
|
|
6
|
+
// never printed, and never echoed in error paths — only NAMES are ever shown.
|
|
7
|
+
// Fail-closed: if the bundle fetch fails, the child command is NOT run.
|
|
8
|
+
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { apiRequest } from './api.js';
|
|
11
|
+
import { dim } from './render.js';
|
|
12
|
+
|
|
13
|
+
/** GET /api/secrets/env for one agent. Returns { env, count, delivered }. */
|
|
14
|
+
export async function fetchAgentEnv(config, agentId) {
|
|
15
|
+
return apiRequest(config, 'GET', '/api/secrets/env', { query: { agent_id: agentId } });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Split the CLI argv at the `--` separator.
|
|
20
|
+
* Tokens before `--` are flags for `dashclaw env`; tokens after are the
|
|
21
|
+
* child command + its args (left untouched, including any `--print`).
|
|
22
|
+
*/
|
|
23
|
+
export function splitEnvArgv(argv) {
|
|
24
|
+
const sep = argv.indexOf('--');
|
|
25
|
+
if (sep === -1) return { flags: argv, command: [] };
|
|
26
|
+
return { flags: argv.slice(0, sep), command: argv.slice(sep + 1) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Names-only listing — never values. */
|
|
30
|
+
export function formatEnvNames(bundle) {
|
|
31
|
+
const names = Array.isArray(bundle.delivered) ? bundle.delivered : Object.keys(bundle.env || {});
|
|
32
|
+
const lines = [];
|
|
33
|
+
if (names.length === 0) {
|
|
34
|
+
lines.push(dim(' No delivery-enabled secrets for this agent.'));
|
|
35
|
+
} else {
|
|
36
|
+
for (const name of names) lines.push(` ${name}`);
|
|
37
|
+
}
|
|
38
|
+
lines.push('');
|
|
39
|
+
lines.push(dim(` ${names.length} secret(s). Values are never printed — run: dashclaw env -- <command>`));
|
|
40
|
+
return lines.join('\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Spawn the child command with the secret bundle merged into its environment.
|
|
45
|
+
* Memory-only: the merged env object lives only in this process and the
|
|
46
|
+
* child's process table — nothing is written anywhere. Resolves with the
|
|
47
|
+
* exit code to assign to process.exitCode (never calls process.exit — a hard
|
|
48
|
+
* exit can trip a libuv teardown assert on Windows).
|
|
49
|
+
*/
|
|
50
|
+
export function runWithEnv(bundle, commandArgv) {
|
|
51
|
+
const [cmd, ...cmdArgs] = commandArgv;
|
|
52
|
+
// No shell: args pass through verbatim (shell:true concatenates them
|
|
53
|
+
// unescaped — mangles quoted args and is deprecated, DEP0190).
|
|
54
|
+
const child = spawn(cmd, cmdArgs, {
|
|
55
|
+
stdio: 'inherit',
|
|
56
|
+
env: { ...process.env, ...(bundle.env || {}) },
|
|
57
|
+
});
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
child.on('error', (err) => {
|
|
60
|
+
// err.message is a spawn error (ENOENT/EINVAL etc.) — never contains values.
|
|
61
|
+
console.error(`Error: could not start "${cmd}": ${err.message}`);
|
|
62
|
+
if (process.platform === 'win32' && (err.code === 'EINVAL' || err.code === 'ENOENT')) {
|
|
63
|
+
console.error(`Hint: Windows .cmd shims (npm, npx) cannot be spawned directly — try: dashclaw env -- cmd /c ${cmd} ...`);
|
|
64
|
+
}
|
|
65
|
+
resolve(1);
|
|
66
|
+
});
|
|
67
|
+
child.on('exit', (code, signal) => resolve(signal ? 1 : code ?? 1));
|
|
68
|
+
});
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,21 +1,34 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@dashclaw/cli",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "DashClaw terminal client — approve agent actions and diagnose your instance",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"
|
|
7
|
-
"dashclaw"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@dashclaw/cli",
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "DashClaw terminal client — approve agent actions and diagnose your instance",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"dashclaw",
|
|
8
|
+
"cli",
|
|
9
|
+
"claude-code",
|
|
10
|
+
"ai-governance",
|
|
11
|
+
"agent-governance"
|
|
12
|
+
],
|
|
13
|
+
"bin": {
|
|
14
|
+
"dashclaw": "./bin/dashclaw.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin/",
|
|
18
|
+
"lib/",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18.0.0"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "node --test test/**/*.test.js"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"dashclaw": "^2.2.1"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
}
|
|
34
|
+
}
|