@dashclaw/cli 0.7.6 → 0.8.0

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.
@@ -1,219 +0,0 @@
1
- // VENDORED from app/lib/claude-code/optimal-files/merge.js and
2
- // app/lib/claude-code/optimal-files/bundle.js.
3
- //
4
- // The CLI is a sibling package and can't import from app/lib/ without
5
- // monorepo tooling, but Phase 6's `dashclaw code apply` needs the
6
- // path-traversal guard and the markdown merge applier locally. Keep this
7
- // file in sync with the canonical source via
8
- // `node scripts/sync-cli-vendored-code.mjs`.
9
- //
10
- // Source of truth:
11
- // app/lib/claude-code/optimal-files/merge.js
12
- // app/lib/claude-code/optimal-files/bundle.js#absolutize
13
- //
14
- // Only `applyMerge`, `previewMerge`, the heading/bullet helpers, and the
15
- // `_ensureInsideProject` guard are vendored. Everything else stays on the
16
- // server side.
17
-
18
- import path from 'node:path';
19
-
20
- // --- _ensureInsideProject (originally from bundle.js#absolutize) ---------
21
-
22
- export function _ensureInsideProject(projectCwd, candidatePath) {
23
- if (!projectCwd || !candidatePath) return null;
24
- let abs;
25
- if (path.isAbsolute(candidatePath)) {
26
- abs = path.normalize(candidatePath);
27
- } else {
28
- abs = path.normalize(path.join(projectCwd, candidatePath));
29
- }
30
- const cwdNorm = path.normalize(projectCwd);
31
- const ci = process.platform === 'win32';
32
- const a = ci ? abs.toLowerCase() : abs;
33
- const b = ci ? cwdNorm.toLowerCase() : cwdNorm;
34
- if (a !== b && !a.startsWith(b + path.sep)) return null;
35
- return abs;
36
- }
37
-
38
- // --- markdown merge (originally from merge.js) ---------------------------
39
-
40
- export function parseMarkdownSections(text) {
41
- const lines = String(text || '').split(/\r?\n/);
42
- const sections = [];
43
- let cur = { level: 0, heading: '__preamble__', headingRaw: '', body: [] };
44
- for (const line of lines) {
45
- const m = line.match(/^(#{1,6})\s+(.+?)\s*$/);
46
- if (m) {
47
- sections.push(cur);
48
- cur = { level: m[1].length, heading: normalizeHeading(m[2]), headingRaw: m[2], body: [] };
49
- } else {
50
- cur.body.push(line);
51
- }
52
- }
53
- sections.push(cur);
54
- return sections;
55
- }
56
-
57
- export function normalizeHeading(h) {
58
- return String(h || '')
59
- .toLowerCase()
60
- .replace(/[`*_]/g, '')
61
- .replace(/[^\w\s]+/g, ' ')
62
- .replace(/\s+/g, ' ')
63
- .trim();
64
- }
65
-
66
- function parseBullets(bodyLines) {
67
- const bullets = [];
68
- const other = [];
69
- for (const line of bodyLines) {
70
- if (/^\s*[-*]\s+/.test(line)) bullets.push({ raw: line, normalized: normalizeBullet(line) });
71
- else other.push(line);
72
- }
73
- return { bullets, other };
74
- }
75
-
76
- function normalizeBullet(line) {
77
- return String(line || '')
78
- .replace(/^\s*[-*]\s+/, '')
79
- .replace(/`[^`]*`/g, ' ')
80
- .replace(/[^a-z0-9\s]+/gi, ' ')
81
- .toLowerCase()
82
- .trim()
83
- .split(/\s+/)
84
- .slice(0, 8)
85
- .join(' ');
86
- }
87
-
88
- function bulletAlreadyPresent(generatedNorm, existingBullets) {
89
- if (!generatedNorm) return true;
90
- for (const b of existingBullets) {
91
- if (!b.normalized) continue;
92
- if (b.normalized.startsWith(generatedNorm)) return true;
93
- if (generatedNorm.startsWith(b.normalized) && b.normalized.length >= 4) return true;
94
- const aWords = generatedNorm.split(' ');
95
- const bWords = b.normalized.split(' ');
96
- let shared = 0;
97
- for (let i = 0; i < Math.min(aWords.length, bWords.length); i++) {
98
- if (aWords[i] === bWords[i]) shared++; else break;
99
- }
100
- if (shared >= 5) return true;
101
- }
102
- return false;
103
- }
104
-
105
- function isFooterSection(section) {
106
- if (!section || !Array.isArray(section.body)) return false;
107
- const joined = section.body.join('\n').toLowerCase();
108
- return (joined.includes('generated by dashclaw') || joined.includes('generated by agentlens')) && joined.includes('review before committing');
109
- }
110
-
111
- function renderSection(section) {
112
- return `## ${section.headingRaw}\n${section.body.join('\n')}`.replace(/\n+$/, '\n');
113
- }
114
-
115
- export function previewMerge(existing, generated) {
116
- const ex = parseMarkdownSections(existing);
117
- const gn = parseMarkdownSections(generated);
118
- const exByHeading = new Map();
119
- for (const s of ex) {
120
- if (s.heading === '__preamble__') continue;
121
- if (!exByHeading.has(s.heading)) exByHeading.set(s.heading, s);
122
- }
123
- const appendSections = [];
124
- const sharedSections = [];
125
- for (let i = 0; i < gn.length; i++) {
126
- const s = gn[i];
127
- if (s.heading === '__preamble__') continue;
128
- if (isFooterSection(s)) continue;
129
- if (s.level !== 2) continue;
130
- const exSection = exByHeading.get(s.heading);
131
- if (!exSection) {
132
- appendSections.push({ heading: s.heading, headingRaw: s.headingRaw, bodyText: renderSection(s) });
133
- } else {
134
- const exBullets = parseBullets(exSection.body).bullets;
135
- const genBullets = parseBullets(s.body).bullets;
136
- const candidates = [];
137
- for (const gb of genBullets) {
138
- if (gb.normalized && !bulletAlreadyPresent(gb.normalized, exBullets)) {
139
- candidates.push({ text: gb.raw.replace(/^\s*[-*]\s+/, '- ') });
140
- }
141
- }
142
- if (candidates.length) {
143
- sharedSections.push({
144
- heading: s.heading,
145
- headingRaw: s.headingRaw,
146
- existingBulletCount: exBullets.length,
147
- candidateBullets: candidates,
148
- });
149
- }
150
- }
151
- }
152
- return {
153
- appendSections,
154
- sharedSections,
155
- newSectionCount: appendSections.length,
156
- sharedSectionCount: sharedSections.length,
157
- candidateBulletCount: sharedSections.reduce((acc, s) => acc + s.candidateBullets.length, 0),
158
- };
159
- }
160
-
161
- function trimTrailingBlanks(lines) {
162
- const out = lines.slice();
163
- while (out.length && out[out.length - 1].trim() === '') out.pop();
164
- return out;
165
- }
166
-
167
- export function applyMerge(existing, generated, selection) {
168
- const preview = previewMerge(existing, generated);
169
- selection = selection || {};
170
- const acceptedHeadings = new Set((selection.acceptedHeadings || []).map(h => normalizeHeading(h)));
171
- const acceptedBulletsByHeading = new Map();
172
- for (const b of (selection.acceptedBullets || [])) {
173
- const k = normalizeHeading(b.heading);
174
- if (!acceptedBulletsByHeading.has(k)) acceptedBulletsByHeading.set(k, []);
175
- acceptedBulletsByHeading.get(k).push(b.text);
176
- }
177
- let merged = String(existing == null ? '' : existing);
178
- if (merged && !merged.endsWith('\n')) merged += '\n';
179
- if (acceptedBulletsByHeading.size) {
180
- const sections = parseMarkdownSections(merged);
181
- const rebuilt = [];
182
- for (const s of sections) {
183
- if (s.heading === '__preamble__') { rebuilt.push(s.body.join('\n')); continue; }
184
- const headingLine = `${'#'.repeat(s.level)} ${s.headingRaw}`;
185
- const accepted = acceptedBulletsByHeading.get(s.heading) || [];
186
- if (accepted.length) {
187
- const trimmedBody = trimTrailingBlanks(s.body);
188
- rebuilt.push(headingLine);
189
- rebuilt.push(trimmedBody.join('\n'));
190
- rebuilt.push('');
191
- rebuilt.push('<!-- dashclaw:merged-bullets -->');
192
- for (const t of accepted) rebuilt.push(t.startsWith('- ') ? t : `- ${t}`);
193
- rebuilt.push('<!-- /dashclaw:merged-bullets -->');
194
- rebuilt.push('');
195
- } else {
196
- rebuilt.push(headingLine);
197
- rebuilt.push(s.body.join('\n'));
198
- }
199
- }
200
- merged = rebuilt.join('\n').replace(/\n+$/, '\n');
201
- }
202
- const wholeAdditions = preview.appendSections.filter(s => acceptedHeadings.has(s.heading));
203
- if (wholeAdditions.length) {
204
- if (!merged.endsWith('\n')) merged += '\n';
205
- merged += '\n<!-- dashclaw:merged-sections -->\n\n';
206
- for (const s of wholeAdditions) {
207
- merged += s.bodyText.endsWith('\n') ? s.bodyText : s.bodyText + '\n';
208
- merged += '\n';
209
- }
210
- merged += '<!-- /dashclaw:merged-sections -->\n';
211
- }
212
- return {
213
- merged,
214
- additions: {
215
- sections: wholeAdditions.map(s => s.headingRaw),
216
- bulletCount: [...acceptedBulletsByHeading.values()].reduce((acc, l) => acc + l.length, 0),
217
- },
218
- };
219
- }
package/lib/cost.js DELETED
@@ -1,108 +0,0 @@
1
- // cli/lib/cost.js
2
- //
3
- // `dashclaw cost [--lens fleet|claude-code] [--period 7d|30d|90d]` — terminal
4
- // readback over GET /api/finops/spend (the finops rollups). Defaults:
5
- // lens=claude-code, period=7d — the wedge user's own Claude Code spend.
6
-
7
- import { apiRequest } from './api.js';
8
-
9
- export const VALID_LENSES = ['fleet', 'claude-code'];
10
- export const VALID_PERIODS = ['7d', '30d', '90d'];
11
- export const USAGE =
12
- 'Usage: dashclaw cost [--lens fleet|claude-code] [--period 7d|30d|90d]\n' +
13
- ' Defaults: --lens claude-code --period 7d';
14
-
15
- /** Validate flag values. Returns null when fine, or a usage-bearing message. */
16
- export function validateCostFlags({ lens, period }) {
17
- if (!VALID_LENSES.includes(lens)) {
18
- return `Invalid --lens "${lens}". Valid: ${VALID_LENSES.join(', ')}.\n${USAGE}`;
19
- }
20
- if (!VALID_PERIODS.includes(period)) {
21
- return `Invalid --period "${period}". Valid: ${VALID_PERIODS.join(', ')}.\n${USAGE}`;
22
- }
23
- return null;
24
- }
25
-
26
- export async function fetchSpend(config, { lens, period }) {
27
- return apiRequest(config, 'GET', '/api/finops/spend', { query: { lens, period } });
28
- }
29
-
30
- const money = (n) => '$' + Number(n || 0).toFixed(2);
31
-
32
- function table(rows) {
33
- // rows: [label, value][] — right-pad labels so values align.
34
- const width = Math.max(...rows.map(([label]) => label.length));
35
- return rows.map(([label, value]) => ` ${label.padEnd(width + 2)}${value}`).join('\n');
36
- }
37
-
38
- function formatClaudeCode(data, period) {
39
- const cs = data.code_sessions || {};
40
- const total = Number(data.code_total_usd || 0);
41
- const sessions = Number(cs.session_count || 0);
42
- if (total === 0 && sessions === 0) {
43
- return (
44
- `No Claude Code spend recorded yet for ${period}.\n` +
45
- 'Sessions are captured by the Stop hook (on by default, metadata-only;\n' +
46
- 'opt-out: DASHCLAW_CODE_SESSIONS_ENABLED=0). Run a Claude Code session and check back.'
47
- );
48
- }
49
- const lines = [
50
- `Claude Code spend — last ${period}`,
51
- '',
52
- table([
53
- ['Total', money(total)],
54
- ['Sessions', String(sessions)],
55
- ['Cache saved', money(cs.total_cache_savings_usd)],
56
- ]),
57
- ];
58
- const projects = Array.isArray(cs.by_project) ? cs.by_project.filter((p) => Number(p.cost_usd || 0) > 0) : [];
59
- if (projects.length > 0) {
60
- lines.push('', ' By project:');
61
- lines.push(table(projects.map((p) => [` ${p.project_name || p.project_id || 'unknown'}`, money(p.cost_usd)])));
62
- }
63
- lines.push('', `Summary: ${money(total)} across ${sessions} session(s) in the last ${period}.`);
64
- return lines.join('\n');
65
- }
66
-
67
- function formatFleet(data, period) {
68
- const agent = Number(data.agent?.total_cost_usd || 0);
69
- const x402 = Number(data.x402?.total_spend_usd || 0);
70
- const total = Number(data.fleet_total_usd || 0);
71
- if (total === 0) {
72
- return (
73
- `No fleet spend recorded yet for ${period}.\n` +
74
- 'Agent spend lands when governed actions report tokens_in/tokens_out + model.'
75
- );
76
- }
77
- const lines = [
78
- `Fleet spend — last ${period}`,
79
- '',
80
- table([
81
- ['Agent LLM', money(agent)],
82
- ['x402 purchases', money(x402)],
83
- ['Total', money(total)],
84
- ]),
85
- '',
86
- `Summary: ${money(total)} fleet spend in the last ${period} (LLM ${money(agent)} + x402 ${money(x402)}).`,
87
- ];
88
- return lines.join('\n');
89
- }
90
-
91
- export function formatSpend(data, { lens, period }) {
92
- return lens === 'claude-code' ? formatClaudeCode(data, period) : formatFleet(data, period);
93
- }
94
-
95
- /**
96
- * Validate, fetch, format. Throws a usage-tagged Error on bad flags so the
97
- * caller prints usage and exits non-zero without a stack trace.
98
- */
99
- export async function runCost(config, { lens = 'claude-code', period = '7d' } = {}, { fetcher = fetchSpend } = {}) {
100
- const invalid = validateCostFlags({ lens, period });
101
- if (invalid) {
102
- const err = new Error(invalid);
103
- err.usage = true;
104
- throw err;
105
- }
106
- const data = await fetcher(config, { lens, period });
107
- return formatSpend(data, { lens, period });
108
- }
package/lib/env.js DELETED
@@ -1,69 +0,0 @@
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/lib/posture.js DELETED
@@ -1,45 +0,0 @@
1
- // cli/lib/posture.js
2
- //
3
- // Direct-API helpers for the `dashclaw posture` / `dashclaw next` commands.
4
- // Like the other CLI command groups, these call the live endpoints via
5
- // apiRequest (fetch + x-api-key) rather than the published SDK, so the CLI never
6
- // depends on a possibly-stale `dashclaw` package for newly-added routes.
7
- //
8
- // Resolve is DRAFT-ONLY: the CLI can create an inactive policy draft, snooze, or
9
- // accept risk — it can NEVER activate enforcement. An operator (or agent) can
10
- // prepare a fix; only a human activates it at /policies. Mirrors the API + MCP
11
- // ceiling so agents can never self-escalate their own governance.
12
-
13
- import { apiRequest } from './api.js';
14
-
15
- /** GET /api/posture — score + dimensions + findings + summary + trend. */
16
- export async function fetchPosture(config) {
17
- return apiRequest(config, 'GET', '/api/posture');
18
- }
19
-
20
- /** GET /api/posture/findings — the prioritized queue (+ optional filters). */
21
- export async function fetchFindings(config, { status, dimension } = {}) {
22
- return apiRequest(config, 'GET', '/api/posture/findings', { query: { status, dimension } });
23
- }
24
-
25
- /** The single top open finding (the `next` gap), or null when the queue is clear. */
26
- export async function fetchNext(config) {
27
- const data = await fetchFindings(config);
28
- return (data && Array.isArray(data.findings) && data.findings[0]) || null;
29
- }
30
-
31
- const RESOLVE_ACTIONS = new Set(['create_draft', 'snooze', 'accept_risk']);
32
-
33
- /**
34
- * POST /api/posture/findings/<key>/resolve — DRAFT-ONLY actions.
35
- * `create_draft` (default) inserts an inactive policy draft; snooze/accept_risk
36
- * record state. None of these activate enforcement.
37
- */
38
- export async function resolveFinding(config, key, action = 'create_draft', note) {
39
- if (!RESOLVE_ACTIONS.has(action)) {
40
- throw new Error(`Invalid resolve action "${action}". Draft-only: ${[...RESOLVE_ACTIONS].join(', ')}.`);
41
- }
42
- return apiRequest(config, 'POST', `/api/posture/findings/${encodeURIComponent(key)}/resolve`, {
43
- body: { action, note },
44
- });
45
- }