@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 +3 -3
- package/bin/dashclaw.js +2 -1
- package/lib/claude/install.js +23 -10
- package/lib/codex/install.js +27 -6
- package/lib/config.js +22 -2
- package/lib/up/db.js +6 -3
- package/lib/up/fetch-app.js +22 -2
- package/package.json +1 -1
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
|
|
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
|
|
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`.
|
|
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
|
package/lib/claude/install.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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');
|
package/lib/codex/install.js
CHANGED
|
@@ -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
|
|
7
|
-
// vendored `dashclaw_agent_intel` module into
|
|
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
|
|
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
|
-
|
|
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:
|
|
126
|
-
password:
|
|
127
|
-
port:
|
|
128
|
+
user: localDb.username,
|
|
129
|
+
password: localDb.password,
|
|
130
|
+
port: Number(localDb.port),
|
|
128
131
|
persistent: true,
|
|
129
132
|
});
|
|
130
133
|
try {
|
package/lib/up/fetch-app.js
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|