@dashclaw/cli 0.5.0 → 0.6.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 CHANGED
@@ -48,7 +48,7 @@ npx dashclaw up --yes # non-interactive, accept all defaults
48
48
  npx dashclaw up --no-browser # skip opening /setup in the browser
49
49
  npx dashclaw up --db docker # force Docker Postgres
50
50
  npx dashclaw up --db embedded # force embedded Postgres (~40 MB download)
51
- npx dashclaw up --db <url> # use an existing postgresql:// connection string
51
+ npx dashclaw up --db url # bring your own Postgres — prompts for a postgresql:// connection string
52
52
  npx dashclaw up --dir <path> # install to a custom directory (default: ~/.dashclaw)
53
53
  npx dashclaw up --port <n> # bind to a custom port (default: 3000)
54
54
  npx dashclaw up --source-dir <path> # use a local repo checkout instead of the published tarball
@@ -91,10 +91,10 @@ Provision DashClaw governance into Claude Code without cloning the repo.
91
91
  ```bash
92
92
  dashclaw install claude # prompts for endpoint + API key
93
93
  dashclaw install claude --endpoint <url> --key <oc_live_...>
94
- dashclaw install claude --trial # browser signup on a hosted instance, paste the key
94
+ dashclaw install claude --trial # browser signup on hosted.dashclaw.io, paste the key
95
95
  ```
96
96
 
97
- Flow: preflight (`/api/health` + an authenticated read — nothing is written until it passes), hooks downloaded from your instance's `/downloads/dashclaw-claude-code-hooks.zip` (or copied from a repo checkout), `python3`/`python` resolved automatically, managed hook entries merged into `~/.claude/settings.json` (`.dashclaw-bak` backup, replace-on-reinstall), credentials written to `~/.dashclaw/claude-hooks/.env` (mode 600 — no secret lands in settings.json). Installs in **observe mode**; flip to enforce by setting `DASHCLAW_HOOK_MODE=enforce` in that `.env`. For `--trial`, set `DASHCLAW_HOSTED_URL` (or `--endpoint`) to the hosted instance if it isn't prompted.
97
+ Flow: preflight (`/api/health` + an authenticated read — nothing is written until it passes), hooks downloaded from your instance's `/downloads/dashclaw-claude-code-hooks.zip` (or copied from a repo checkout), `python3`/`python` resolved automatically, managed hook entries merged into `~/.claude/settings.json` (`.dashclaw-bak` backup, replace-on-reinstall), credentials written to `~/.dashclaw/claude-hooks/.env` (mode 600 — no secret lands in settings.json). Installs in **observe mode**; flip to enforce by setting `DASHCLAW_HOOK_MODE=enforce` in that `.env`. `--trial` defaults to the public trial at `https://hosted.dashclaw.io`; point it at your own hosted instance with `--endpoint <url>` (or `DASHCLAW_HOSTED_URL`).
98
98
 
99
99
  ### `dashclaw cost`
100
100
 
package/bin/dashclaw.js CHANGED
@@ -110,7 +110,8 @@ ${bold('Usage:')}
110
110
  --allow-redactions Write files that contain redacted secret patterns
111
111
  --overwrite Clobber existing .NEW side-by-side files
112
112
  dashclaw install claude [--trial] Provision DashClaw governance into Claude Code
113
- --endpoint <url> Your DashClaw instance URL (or DASHCLAW_BASE_URL)
113
+ --endpoint <url> Your DashClaw instance URL (or DASHCLAW_BASE_URL;
114
+ --trial defaults to https://hosted.dashclaw.io)
114
115
  --key <key> API key (or DASHCLAW_API_KEY; --trial prompts via browser signup)
115
116
  --agent-id <id> Agent id for governed actions (default: claude-code)
116
117
  dashclaw install codex Provision DashClaw governance into Codex CLI
@@ -52,6 +52,11 @@ const HOOKS_BUNDLE_PATH = '/downloads/dashclaw-claude-code-hooks.zip';
52
52
 
53
53
  export const DEFAULT_AGENT_ID = 'claude-code';
54
54
 
55
+ // The public hosted trial instance. `--trial` falls back to it when no
56
+ // --endpoint / DASHCLAW_HOSTED_URL is given — a cold outsider following
57
+ // QUICK-START has no way to answer a "which URL?" prompt (v5.4 outsider run).
58
+ export const DEFAULT_HOSTED_TRIAL_URL = 'https://hosted.dashclaw.io';
59
+
55
60
  // ---------------------------------------------------------------------------
56
61
  // Python resolution
57
62
  // ---------------------------------------------------------------------------
@@ -182,8 +187,8 @@ async function downloadHooksBundle(endpoint, hooksDst, { fetchImpl = fetch } = {
182
187
  // ---------------------------------------------------------------------------
183
188
 
184
189
  const HOOK_EVENTS = {
185
- PreToolUse: { matcher: 'Agent|Task|Bash|Edit|Write|MultiEdit|Skill|mcp__.*', script: 'dashclaw_pretool.py', timeout: 3600000 },
186
- PostToolUse: { matcher: 'Agent|Task|Bash|Edit|Write|MultiEdit|mcp__.*', script: 'dashclaw_posttool.py' },
190
+ PreToolUse: { matcher: 'Agent|Task|Workflow|Bash|Edit|Write|MultiEdit|Skill|mcp__.*', script: 'dashclaw_pretool.py', timeout: 3600000 },
191
+ PostToolUse: { matcher: 'Agent|Task|Workflow|Bash|Edit|Write|MultiEdit|mcp__.*', script: 'dashclaw_posttool.py' },
187
192
  Stop: { script: 'dashclaw_stop.py' },
188
193
  };
189
194
 
@@ -192,12 +197,15 @@ export function isManagedHookEntry(entry) {
192
197
  }
193
198
 
194
199
  /** Build the managed hook entries pointing at <hooksDir> via <python>. */
195
- export function buildHookEntries(hooksDir, python) {
200
+ export function buildHookEntries(hooksDir, python, agentId = DEFAULT_AGENT_ID) {
196
201
  const entries = {};
197
202
  for (const [event, spec] of Object.entries(HOOK_EVENTS)) {
198
203
  const hook = {
199
204
  type: 'command',
200
- command: `${python} "${join(hooksDir, spec.script)}"`,
205
+ // --agent-id is the per-harness identity declaration (roadmap v2.2):
206
+ // argv beats the machine-ambient DASHCLAW_AGENT_ID env var, so this
207
+ // install keeps its identity even when another harness exports one.
208
+ command: `${python} "${join(hooksDir, spec.script)}" --agent-id "${agentId}"`,
201
209
  ...(spec.timeout ? { timeout: spec.timeout } : {}),
202
210
  };
203
211
  entries[event] = [{ ...(spec.matcher ? { matcher: spec.matcher } : {}), hooks: [hook] }];
@@ -209,7 +217,7 @@ export function buildHookEntries(hooksDir, python) {
209
217
  * Merge the managed hook entries into a Claude Code settings.json file:
210
218
  * previous dashclaw-managed entries are replaced, everything else preserved.
211
219
  */
212
- export function mergeClaudeSettings(settingsPath, hooksDir, python) {
220
+ export function mergeClaudeSettings(settingsPath, hooksDir, python, agentId = DEFAULT_AGENT_ID) {
213
221
  let settings = {};
214
222
  if (existsSync(settingsPath)) {
215
223
  try {
@@ -221,7 +229,7 @@ export function mergeClaudeSettings(settingsPath, hooksDir, python) {
221
229
  if (!existsSync(bak)) copyFileSync(settingsPath, bak);
222
230
  }
223
231
  settings.hooks = settings.hooks || {};
224
- const managed = buildHookEntries(hooksDir, python);
232
+ const managed = buildHookEntries(hooksDir, python, agentId);
225
233
  for (const [event, entries] of Object.entries(managed)) {
226
234
  const existing = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
227
235
  settings.hooks[event] = [...existing.filter((e) => !isManagedHookEntry(e)), ...entries];
@@ -284,8 +292,13 @@ export async function installClaude({
284
292
  apiKey = apiKey || env.DASHCLAW_API_KEY || '';
285
293
 
286
294
  if (trial && !apiKey) {
287
- const hostedBase = (endpoint || env.DASHCLAW_HOSTED_URL || '').replace(/\/+$/, '')
288
- || (await mustPrompt(prompt, 'Hosted DashClaw URL (where you signed up / will sign up): '));
295
+ let hostedBase = (endpoint || env.DASHCLAW_HOSTED_URL || '').replace(/\/+$/, '');
296
+ if (!hostedBase) {
297
+ hostedBase = DEFAULT_HOSTED_TRIAL_URL;
298
+ logger.log('');
299
+ logger.log(` Using the public hosted trial: ${DEFAULT_HOSTED_TRIAL_URL}`);
300
+ logger.log(' (Trying your own instance instead? Re-run with --endpoint <url>.)');
301
+ }
289
302
  const signupUrl = `${hostedBase.replace(/\/+$/, '')}/connect`;
290
303
  logger.log('');
291
304
  logger.log(` Opening the trial signup page: ${signupUrl}`);
@@ -321,8 +334,8 @@ export async function installClaude({
321
334
  throw new Error('No python3 or python found on PATH. Install Python 3.10+ and re-run.');
322
335
  }
323
336
  const settingsPath = join(homeDir, '.claude', 'settings.json');
324
- logger.log(` Wiring hooks into ${settingsPath} (python: ${python})`);
325
- mergeClaudeSettings(settingsPath, hooksDir, python);
337
+ logger.log(` Wiring hooks into ${settingsPath} (python: ${python}, agent: ${agentId})`);
338
+ mergeClaudeSettings(settingsPath, hooksDir, python, agentId);
326
339
 
327
340
  // 5. Credentials -------------------------------------------------------------
328
341
  const hookEnvPath = join(hooksDir, '.env');
@@ -3,11 +3,13 @@
3
3
  // `dashclaw install codex` — provisions DashClaw governance into Codex CLI.
4
4
  //
5
5
  // What it does, idempotently:
6
- // 1. Copies the Python governance hooks (pretool, posttool, stop) and the
7
- // vendored `dashclaw_agent_intel` module into ~/.codex/hooks/dashclaw/.
6
+ // 1. Copies the Python governance hooks (pretool, posttool, stop, session
7
+ // digest) and the vendored `dashclaw_agent_intel` module into
8
+ // ~/.codex/hooks/dashclaw/.
8
9
  // 2. Merges a managed block into ~/.codex/config.toml that registers:
9
10
  // - the DashClaw MCP server (stdio)
10
- // - PreToolUse / PostToolUse / Stop hooks pointing at the copied scripts
11
+ // - PreToolUse / PostToolUse / Stop / SessionStart hooks pointing at
12
+ // the copied scripts
11
13
  // - approval_policy = "on-request" so Codex surfaces require_approval
12
14
  // decisions from DashClaw guard
13
15
  // 3. Drops a managed block into <project>/AGENTS.md (or creates the file)
@@ -57,6 +59,12 @@ const HOOK_FILES = [
57
59
  'dashclaw_pretool.py',
58
60
  'dashclaw_posttool.py',
59
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',
60
68
  ];
61
69
  const HOOK_INTEL_DIR = 'dashclaw_agent_intel';
62
70
 
@@ -184,6 +192,7 @@ export function buildConfigTomlBlock({
184
192
  const pre = join(hooksDir, 'dashclaw_pretool.py');
185
193
  const post = join(hooksDir, 'dashclaw_posttool.py');
186
194
  const stop = join(hooksDir, 'dashclaw_stop.py');
195
+ const sessionStart = join(hooksDir, 'dashclaw_session_digest.py');
187
196
 
188
197
  const lines = [
189
198
  MANAGED_START,
@@ -215,19 +224,31 @@ export function buildConfigTomlBlock({
215
224
  'matcher = "Bash|Edit|Write|MultiEdit"',
216
225
  '[[hooks.PreToolUse.hooks]]',
217
226
  'type = "command"',
218
- `command = ${tomlString(`${py} ${pre}`)}`,
227
+ // --agent-id on the hook command line mirrors the MCP server args above:
228
+ // the hooks' argv identity beats the machine-ambient DASHCLAW_AGENT_ID
229
+ // env var, so Codex tool calls are never mis-attributed to another
230
+ // harness (roadmap v2.2).
231
+ `command = ${tomlString(`${py} ${pre} --agent-id codex`)}`,
219
232
  'timeoutSec = 3600',
220
233
  '',
221
234
  '[[hooks.PostToolUse]]',
222
235
  'matcher = "Bash|Edit|Write|MultiEdit"',
223
236
  '[[hooks.PostToolUse.hooks]]',
224
237
  'type = "command"',
225
- `command = ${tomlString(`${py} ${post}`)}`,
238
+ `command = ${tomlString(`${py} ${post} --agent-id codex`)}`,
226
239
  '',
227
240
  '[[hooks.Stop]]',
228
241
  '[[hooks.Stop.hooks]]',
229
242
  'type = "command"',
230
- `command = ${tomlString(`${py} ${stop}`)}`,
243
+ `command = ${tomlString(`${py} ${stop} --agent-id codex`)}`,
244
+ '',
245
+ // Codex CLI 0.139.0's hook-event enum contains SessionStart alongside
246
+ // Pre/Post/Stop; wired once that lifecycle was verified to fire
247
+ // (roadmap v3.7 item 6 — see PLUGIN_PARITY.md).
248
+ '[[hooks.SessionStart]]',
249
+ '[[hooks.SessionStart.hooks]]',
250
+ 'type = "command"',
251
+ `command = ${tomlString(`${py} ${sessionStart} --agent-id codex`)}`,
231
252
  MANAGED_END,
232
253
  );
233
254
 
package/lib/config.js CHANGED
@@ -37,17 +37,29 @@ export function clearConfigFile() {
37
37
 
38
38
  export function ask(question) {
39
39
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
40
- return new Promise((res) => {
40
+ return new Promise((res, rej) => {
41
+ let answered = false;
41
42
  rl.question(question, (answer) => {
43
+ answered = true;
42
44
  rl.close();
43
45
  res(answer.trim());
44
46
  });
47
+ // stdin ending (piped input exhausted, Ctrl+D) used to leave the promise
48
+ // pending forever — node then drained the event loop and exited 0 as if
49
+ // the install had succeeded (v5.4 outsider run). Fail loudly instead.
50
+ rl.on('close', () => {
51
+ if (!answered) rej(new Error('stdin closed before the prompt was answered.'));
52
+ });
45
53
  });
46
54
  }
47
55
 
48
56
  export function askSecret(question) {
49
- return new Promise((res) => {
57
+ return new Promise((res, rej) => {
50
58
  const stdin = process.stdin;
59
+ if (stdin.readableEnded || stdin.destroyed) {
60
+ rej(new Error('stdin closed before the prompt was answered.'));
61
+ return;
62
+ }
51
63
  process.stdout.write(question);
52
64
  const wasRaw = stdin.isRaw;
53
65
  if (stdin.setRawMode) stdin.setRawMode(true);
@@ -55,6 +67,13 @@ export function askSecret(question) {
55
67
  stdin.setEncoding('utf8');
56
68
 
57
69
  let input = '';
70
+ // Same silent-exit-0 hazard as ask(): if stdin ends before a newline,
71
+ // reject instead of leaving the promise pending while node exits clean.
72
+ const onEnd = () => {
73
+ stdin.removeListener('data', onData);
74
+ rej(new Error('stdin closed before the prompt was answered.'));
75
+ };
76
+ stdin.once('end', onEnd);
58
77
  const onData = (char) => {
59
78
  const str = String(char);
60
79
  for (const c of str) {
@@ -62,6 +81,7 @@ export function askSecret(question) {
62
81
  if (stdin.setRawMode) stdin.setRawMode(wasRaw);
63
82
  stdin.pause();
64
83
  stdin.removeListener('data', onData);
84
+ stdin.removeListener('end', onEnd);
65
85
  process.stdout.write('\n');
66
86
  res(input.trim());
67
87
  return;
package/lib/up/db.js CHANGED
@@ -120,11 +120,14 @@ export async function provisionDatabase({ mode, baseDir, promptFn, logger = cons
120
120
  // Only clean up on failure when WE created it (a pre-existing dir means
121
121
  // a working prior install whose data we must not delete).
122
122
  const dirPreExisted = existsSync(pgDir);
123
+ // Derive credentials from LOCAL_DB_URL so the local dev creds live in
124
+ // exactly one place rather than being duplicated as bare literals here.
125
+ const localDb = new URL(LOCAL_DB_URL);
123
126
  const pg = new EmbeddedPostgres({
124
127
  databaseDir: pgDir,
125
- user: 'dashclaw',
126
- password: 'dashclaw',
127
- port: 5433,
128
+ user: localDb.username,
129
+ password: localDb.password,
130
+ port: Number(localDb.port),
128
131
  persistent: true,
129
132
  });
130
133
  try {
@@ -16,17 +16,37 @@ const REPO = 'ucsandman/DashClaw';
16
16
  * Fetch the latest published platform version from the npm registry.
17
17
  * The `dashclaw` npm package version mirrors the platform version (unified versioning).
18
18
  *
19
+ * npm's version number is only trusted after verifying its git tag exists —
20
+ * a publish that shipped without cutting its tag would otherwise 404 every
21
+ * install. When the tag is missing, fall back to the latest GitHub release.
22
+ *
19
23
  * @param {typeof fetch} fetchImpl - injectable for tests
24
+ * @param {{ error: (...args: any[]) => void }} logger
20
25
  * @returns {Promise<string>} semver string e.g. '4.21.0'
21
26
  */
22
- export async function resolveAppVersion(fetchImpl = fetch) {
27
+ export async function resolveAppVersion(fetchImpl = fetch, logger = console) {
23
28
  const res = await fetchImpl('https://registry.npmjs.org/dashclaw/latest');
24
29
  if (!res.ok) {
25
30
  throw new Error(`npm registry lookup failed (${res.status}) — check your network and retry.`);
26
31
  }
27
32
  const { version } = await res.json();
28
33
  if (!version) throw new Error('npm registry returned no version for dashclaw.');
29
- return version;
34
+
35
+ const head = await fetchImpl(tarballUrl(version), { method: 'HEAD' });
36
+ if (head.ok) return version;
37
+
38
+ const rel = await fetchImpl(`https://api.github.com/repos/${REPO}/releases/latest`, {
39
+ headers: { accept: 'application/vnd.github+json' },
40
+ });
41
+ const tagName = rel.ok ? (await rel.json()).tag_name : null;
42
+ const fallback = typeof tagName === 'string' ? tagName.replace(/^v/, '') : null;
43
+ if (!fallback) {
44
+ throw new Error(
45
+ `Tag v${version} (npm latest) is missing on GitHub and no release fallback was found — report this at https://github.com/${REPO}/issues.`,
46
+ );
47
+ }
48
+ logger.error(`[warn] npm reports ${version} but tag v${version} is missing; using latest GitHub release ${fallback} instead.`);
49
+ return fallback;
30
50
  }
31
51
 
32
52
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dashclaw/cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "DashClaw terminal client — approve agent actions and diagnose your instance",
5
5
  "type": "module",
6
6
  "keywords": [