@dashclaw/cli 0.7.6 → 0.8.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.
@@ -59,12 +59,10 @@ const HOOK_FILES = [
59
59
  'dashclaw_pretool.py',
60
60
  'dashclaw_posttool.py',
61
61
  'dashclaw_stop.py',
62
- // dashclaw_stop.py imports this for Code Sessions ingest; without it the
63
- // import fails inside a try/except and ingest silently no-ops (v2.7 fix).
64
- 'dashclaw_code_session_reporter.py',
65
- // SessionStart digest (v3.7 item 6) — wired once codex-cli 0.139.0's
66
- // SessionStart hook event was confirmed to fire (see PLUGIN_PARITY.md).
67
- 'dashclaw_session_digest.py',
62
+ // SessionStart enforcement-liveness probe (v8.2) the SessionStart hook
63
+ // (wired once codex-cli 0.139.0's SessionStart event was confirmed to fire;
64
+ // see PLUGIN_PARITY.md). Replaced the retired session-digest hook.
65
+ 'enforcement_liveness_probe.py',
68
66
  ];
69
67
  const HOOK_INTEL_DIR = 'dashclaw_agent_intel';
70
68
 
@@ -192,7 +190,7 @@ export function buildConfigTomlBlock({
192
190
  const pre = join(hooksDir, 'dashclaw_pretool.py');
193
191
  const post = join(hooksDir, 'dashclaw_posttool.py');
194
192
  const stop = join(hooksDir, 'dashclaw_stop.py');
195
- const sessionStart = join(hooksDir, 'dashclaw_session_digest.py');
193
+ const sessionStart = join(hooksDir, 'enforcement_liveness_probe.py');
196
194
 
197
195
  const lines = [
198
196
  MANAGED_START,
@@ -244,11 +242,14 @@ export function buildConfigTomlBlock({
244
242
  '',
245
243
  // Codex CLI 0.139.0's hook-event enum contains SessionStart alongside
246
244
  // Pre/Post/Stop; wired once that lifecycle was verified to fire
247
- // (roadmap v3.7 item 6 — see PLUGIN_PARITY.md).
245
+ // (roadmap v3.7 item 6 — see PLUGIN_PARITY.md). Runs the enforcement-
246
+ // liveness probe (v8.2): `--source session-start` throttles it to once/12h
247
+ // and detaches, so session start is never delayed. No --agent-id — the
248
+ // probe forces its own synthetic identity internally.
248
249
  '[[hooks.SessionStart]]',
249
250
  '[[hooks.SessionStart.hooks]]',
250
251
  'type = "command"',
251
- `command = ${tomlString(`${py} ${sessionStart} --agent-id codex`)}`,
252
+ `command = ${tomlString(`${py} ${sessionStart} --source session-start`)}`,
252
253
  MANAGED_END,
253
254
  );
254
255
 
package/lib/up/index.js CHANGED
@@ -13,6 +13,7 @@
13
13
  // without touching the network, Docker, or a real Next server.
14
14
 
15
15
  import { spawnSync } from 'node:child_process';
16
+ import { randomBytes } from 'node:crypto';
16
17
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
17
18
  import { homedir } from 'node:os';
18
19
  import { join } from 'node:path';
@@ -111,6 +112,32 @@ export function rewriteEnvDatabaseUrl(appDir, databaseUrl, logger = console) {
111
112
  }
112
113
  }
113
114
 
115
+ /**
116
+ * Mint a one-time browser sign-in token into the app's .env.local, BEFORE the
117
+ * server starts (Next reads .env.local at boot). The server consumes it on
118
+ * first use (app/api/auth/local); the 15-minute expiry bounds replay. This is
119
+ * what lets `dashclaw up` open a browser that lands already signed in instead
120
+ * of a login form asking for a password the user never saw.
121
+ * Best-effort: returns null (→ fall back to /setup) when .env.local is absent.
122
+ */
123
+ export function mintLoginToken(appDir, logger = console) {
124
+ try {
125
+ const envPath = join(appDir, '.env.local');
126
+ if (!existsSync(envPath)) return null;
127
+ const token = randomBytes(24).toString('base64url');
128
+ const line = `DASHCLAW_LOGIN_OTT=${token}.${Date.now() + 15 * 60_000}`;
129
+ const src = readFileSync(envPath, 'utf8');
130
+ const out = /^DASHCLAW_LOGIN_OTT=.*$/m.test(src)
131
+ ? src.replace(/^DASHCLAW_LOGIN_OTT=.*$/m, line)
132
+ : `${src}${src.endsWith('\n') || src === '' ? '' : '\n'}${line}\n`;
133
+ writeFileSync(envPath, out);
134
+ return token;
135
+ } catch (e) {
136
+ logger.error(`[warn] Could not mint a browser sign-in link: ${e.message} — sign in with the admin password instead.`);
137
+ return null;
138
+ }
139
+ }
140
+
114
141
  /** Default process-liveness probe: signal 0 succeeds iff the pid exists. */
115
142
  export function defaultProcessAlive(pid) {
116
143
  try { process.kill(pid, 0); return true; } catch { return false; }
@@ -125,6 +152,7 @@ export function realDeps() {
125
152
  chooseDbMode,
126
153
  provisionDatabase,
127
154
  runSetupScript: runSetupScriptReal,
155
+ mintLoginToken,
128
156
  buildApp,
129
157
  startServer,
130
158
  waitForHealth,
@@ -210,10 +238,11 @@ export async function runUp({ args, baseDir = join(homedir(), '.dashclaw'), deps
210
238
  if (out.ok === false) throw new Error(out.error || 'Setup failed.');
211
239
  apiKey = out.apiKey;
212
240
  inst = saveInstance(baseDir, { apiKey });
213
- // setup.mjs already prints the password once (to stderr) and writes it to
214
- // .env.local; we deliberately do NOT echo it again here no secrets twice.
241
+ // setup.mjs prints the password to ITS stderr, but runSetupScript pipes
242
+ // (and on success discards) that stream so this line is the one place
243
+ // the operator ever sees it. It is also saved to <appDir>/.env.local.
215
244
  if (out.adminPassword) {
216
- logger.error('[ok] First admin created credentials written to .env.local (printed once).');
245
+ logger.log(`[ok] Dashboard admin password: ${out.adminPassword} (also saved to ${join(appDir, '.env.local')})`);
217
246
  }
218
247
  inst = checkpoint(baseDir, 'setup_done');
219
248
  }
@@ -242,7 +271,11 @@ export async function runUp({ args, baseDir = join(homedir(), '.dashclaw'), deps
242
271
  // Pid alive but health check failed — fall through to a fresh start.
243
272
  }
244
273
  }
274
+ // One-time browser sign-in: only mintable for a server WE are about to start
275
+ // (Next reads .env.local at boot; a reused server would never see the token).
276
+ let loginToken = null;
245
277
  if (!reusedServer) {
278
+ loginToken = deps.mintLoginToken?.(appDir, logger) ?? null;
246
279
  child = deps.startServer({ appDir, port, logger });
247
280
  inst = saveInstance(baseDir, { pid: child.pid });
248
281
  try {
@@ -271,8 +304,16 @@ export async function runUp({ args, baseDir = join(homedir(), '.dashclaw'), deps
271
304
  }
272
305
 
273
306
  // 8. open -----------------------------------------------------------------
307
+ // With a fresh token the browser opens /login?ott=... and lands signed in
308
+ // (redirecting on to /setup); otherwise it opens /setup directly and any
309
+ // protected page will ask for the admin password.
310
+ const signInUrl = loginToken
311
+ ? `${baseUrl}/login?ott=${loginToken}&next=${encodeURIComponent('/setup')}`
312
+ : `${baseUrl}/setup`;
274
313
  if (!args.noBrowser) {
275
- deps.openBrowser(`${baseUrl}/setup`, logger);
314
+ deps.openBrowser(signInUrl, logger);
315
+ } else if (loginToken) {
316
+ logger.log(`Sign in (link valid ~15 min, single use): ${signInUrl}`);
276
317
  }
277
318
  logger.log(`Done. First steps: ${baseUrl}/connect`);
278
319
 
package/package.json CHANGED
@@ -1,37 +1,37 @@
1
- {
2
- "name": "@dashclaw/cli",
3
- "version": "0.7.6",
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
- "embedded-postgres": "18.4.0-beta.17",
30
- "pg": "^8.22.0",
31
- "tar": "^7.4.0"
32
- },
33
- "license": "MIT",
34
- "publishConfig": {
35
- "access": "public"
36
- }
37
- }
1
+ {
2
+ "name": "@dashclaw/cli",
3
+ "version": "0.8.1",
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
+ "embedded-postgres": "18.4.0-beta.17",
30
+ "pg": "^8.22.0",
31
+ "tar": "^7.4.0"
32
+ },
33
+ "license": "MIT",
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }
package/lib/code/apply.js DELETED
@@ -1,164 +0,0 @@
1
- // `dashclaw code apply <manifestId>` — fetch a write plan from the server
2
- // and apply it locally. The server emits the manifest (Phase 6) with a
3
- // 24h TTL. This module is the only place the CLI writes to disk for the
4
- // Optimal Files feature.
5
-
6
- import fs from 'node:fs';
7
- import path from 'node:path';
8
- import { _ensureInsideProject, applyMerge } from './vendored.js';
9
-
10
- const SECRET_PATTERNS = [
11
- { name: 'stripe_test', re: /sk_test_[A-Za-z0-9]{8,}/g },
12
- { name: 'stripe_live', re: /sk_live_[A-Za-z0-9]{8,}/g },
13
- { name: 'stripe_webhook', re: /whsec_[A-Za-z0-9]{8,}/g },
14
- { name: 'openai_key', re: /sk-[A-Za-z0-9]{20,}/g },
15
- { name: 'anthropic_key', re: /sk-ant-[A-Za-z0-9_\-]{20,}/g },
16
- { name: 'github_pat', re: /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g },
17
- { name: 'aws_access', re: /AKIA[0-9A-Z]{16}/g },
18
- { name: 'jwt', re: /eyJ[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}/g },
19
- { name: 'private_key', re: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
20
- { name: 'env_assign', re: /\b([A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASS|PWD|CRED|AUTH))\s*=\s*['"]?[^\s'"\n]+/g },
21
- ];
22
-
23
- function scanForSecrets(content) {
24
- let redactions = 0;
25
- let out = String(content == null ? '' : content);
26
- for (const p of SECRET_PATTERNS) {
27
- out = out.replace(p.re, (_match, captured) => {
28
- redactions++;
29
- if (p.name === 'env_assign' && captured) return `${captured}=<REDACTED:${p.name}>`;
30
- return `<REDACTED:${p.name}>`;
31
- });
32
- }
33
- return { status: redactions === 0 ? 'passed' : 'redacted', redactions, redacted: out };
34
- }
35
-
36
- async function fetchManifest(baseUrl, apiKey, manifestId, { fetchImpl = fetch }) {
37
- const url = baseUrl.replace(/\/+$/, '') + '/api/code-sessions/manifests/' + encodeURIComponent(manifestId);
38
- const res = await fetchImpl(url, {
39
- headers: { 'x-api-key': apiKey },
40
- });
41
- let body = null;
42
- try { body = await res.json(); } catch { /* null */ }
43
- return { status: res.status, ok: res.ok, body };
44
- }
45
-
46
- export async function runApply({
47
- baseUrl,
48
- apiKey,
49
- manifestId,
50
- dest,
51
- yes = false,
52
- allowRedactions = false,
53
- allowOverwriteSideBySide = false,
54
- fetchImpl = fetch,
55
- logger = console,
56
- }) {
57
- if (!manifestId) throw new Error('runApply: manifestId is required');
58
- if (!dest) throw new Error('runApply: --dest=<dir> is required');
59
- if (!baseUrl || !apiKey) throw new Error('runApply: baseUrl and apiKey are required');
60
-
61
- const destAbs = path.resolve(dest);
62
- try { fs.mkdirSync(destAbs, { recursive: true }); }
63
- catch (err) { throw new Error('Could not create destination ' + destAbs + ': ' + err.message); }
64
-
65
- const { status, ok, body } = await fetchManifest(baseUrl, apiKey, manifestId, { fetchImpl });
66
- if (!ok) throw new Error('Failed to load manifest: HTTP ' + status + (body?.error ? ' — ' + body.error : ''));
67
- const plan = Array.isArray(body?.plan) ? body.plan : Array.isArray(body?.plan?.results) ? body.plan.results : null;
68
- if (!plan) throw new Error('Manifest payload missing plan');
69
-
70
- const results = [];
71
- for (const entry of plan) {
72
- const target = entry.path;
73
- if (!target) {
74
- results.push({ path: '(unknown)', status: 'invalid_entry' });
75
- continue;
76
- }
77
- const abs = _ensureInsideProject(destAbs, target);
78
- if (!abs) {
79
- results.push({ path: target, status: 'unsafe_path' });
80
- logger.warn(` ${target} -> unsafe_path (refused)`);
81
- continue;
82
- }
83
- const content = entry.content;
84
- if (typeof content !== 'string') {
85
- results.push({ path: target, status: 'no_content' });
86
- continue;
87
- }
88
- const scan = scanForSecrets(content);
89
- if (scan.status === 'redacted' && !allowRedactions) {
90
- results.push({ path: target, status: 'redacted', redactions: scan.redactions });
91
- logger.warn(` ${target} -> ${scan.redactions} secret pattern(s) detected, refused (use --allow-redactions)`);
92
- continue;
93
- }
94
- const mode = entry.mode || 'create';
95
-
96
- try {
97
- if (mode === 'merge') {
98
- if (!fs.existsSync(abs)) {
99
- fs.mkdirSync(path.dirname(abs), { recursive: true });
100
- fs.writeFileSync(abs, scan.redacted, 'utf8');
101
- results.push({ path: target, status: 'created', bytes: Buffer.byteLength(scan.redacted, 'utf8') });
102
- logger.info(` ${target} -> created`);
103
- continue;
104
- }
105
- const existing = fs.readFileSync(abs, 'utf8');
106
- const merged = applyMerge(existing, scan.redacted, {
107
- acceptedHeadings: entry.acceptedHeadings || [],
108
- acceptedBullets: entry.acceptedBullets || [],
109
- });
110
- fs.writeFileSync(abs, merged.merged, 'utf8');
111
- results.push({ path: target, status: 'merged', additions: merged.additions });
112
- logger.info(` ${target} -> merged`);
113
- } else if (mode === 'side_by_side') {
114
- const sideAbs = entry.absolutePath
115
- ? path.resolve(entry.absolutePath)
116
- : sideBySidePath(abs);
117
- const guardedSide = _ensureInsideProject(destAbs, path.relative(destAbs, sideAbs));
118
- if (!guardedSide) {
119
- results.push({ path: target, status: 'unsafe_path' });
120
- continue;
121
- }
122
- fs.mkdirSync(path.dirname(guardedSide), { recursive: true });
123
- if (fs.existsSync(guardedSide) && !allowOverwriteSideBySide) {
124
- results.push({ path: target, status: 'side_by_side_conflict' });
125
- logger.warn(` ${target} -> side_by_side_conflict (pass --overwrite to replace)`);
126
- continue;
127
- }
128
- fs.writeFileSync(guardedSide, scan.redacted, 'utf8');
129
- results.push({ path: target, status: 'side_by_side', bytes: Buffer.byteLength(scan.redacted, 'utf8') });
130
- logger.info(` ${target} -> side_by_side`);
131
- } else if (mode === 'skip') {
132
- results.push({ path: target, status: 'skipped' });
133
- } else {
134
- // 'create' or 'overwrite'
135
- fs.mkdirSync(path.dirname(abs), { recursive: true });
136
- const existed = fs.existsSync(abs);
137
- if (existed && mode !== 'overwrite' && !yes) {
138
- results.push({ path: target, status: 'refused_overwrite_without_yes' });
139
- logger.warn(` ${target} -> exists; pass --yes or rebuild manifest with mode='overwrite'`);
140
- continue;
141
- }
142
- fs.writeFileSync(abs, scan.redacted, 'utf8');
143
- results.push({ path: target, status: existed ? 'overwritten' : 'created' });
144
- logger.info(` ${target} -> ${existed ? 'overwritten' : 'created'}`);
145
- }
146
- } catch (err) {
147
- results.push({ path: target, status: 'error', error: err.message });
148
- logger.warn(` ${target} -> error: ${err.message}`);
149
- }
150
- }
151
- return results;
152
- }
153
-
154
- function sideBySidePath(abs) {
155
- const dir = path.dirname(abs);
156
- const base = path.basename(abs);
157
- const ext = path.extname(base);
158
- if (!ext) return path.join(dir, base + '.NEW');
159
- return path.join(dir, base.slice(0, -ext.length) + '.NEW' + ext);
160
- }
161
-
162
- // Exported for the sync script + tests that want to assert the scan layer
163
- // stays in lock-step with app/lib/claude-code/optimal-files/secret-scan.js.
164
- export { scanForSecrets };