@dzhechkov/harness-cli 0.3.138 → 0.3.142
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 +6 -1
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +414 -9
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
- package/src/cli.ts +409 -9
package/README.md
CHANGED
|
@@ -325,7 +325,7 @@ Get the whole set with `dz init --target claude-code --preset meta`, or pick one
|
|
|
325
325
|
|
|
326
326
|
> **A skill and its npx toolkit are not duplicates — they're a graduation.** Several skills (e.g. `feature-adr`, `design-thinking`) exist BOTH as a skill inside a `dz` preset AND as a standalone `npx` package. The preset's SKILL.md is **fully functional on its own** (the whole methodology — modules + references — travels with it, and it auto-activates by description), and it's the only way to compile that capability to the **non-Claude platforms** (Codex/OpenCode/Hermes/OpenClaude) via `dz`. The npx package adds **project-level runtime governance** around the same skill: a slash command, governance rules, a context shard, and (for feature-adr) reward-learning + `/harvest`. So: pick the **skill/preset** for a working capability across platforms; pick the **npx toolkit** when you want it as a governed, command-driven fixture of one project.
|
|
327
327
|
|
|
328
|
-
## All Commands (
|
|
328
|
+
## All Commands (36)
|
|
329
329
|
|
|
330
330
|
```
|
|
331
331
|
dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force]
|
|
@@ -336,6 +336,11 @@ dz teach "<pattern>" [--reward <0-1>] [--domain <name>] [--type rule
|
|
|
336
336
|
dz consolidate [--sessions-dir <dir>] [--project <dir>] [--no-mirror]
|
|
337
337
|
dz recall "<query>" [--limit <N>] [--project <dir>]
|
|
338
338
|
dz recall --books "<query>" [--book <slug>] # digitized-book KUs; --book narrows to one book
|
|
339
|
+
dz brain list [--json] # the durable cross-project knowledge brain
|
|
340
|
+
dz brain query "<q>" [--source <slug>] [--limit <N>] [--json] # cross-source lexical recall over the brain
|
|
341
|
+
dz brain add [--source <slug>] [--project <dir>] [--json] # promote this project's digitized-book KUs into the brain
|
|
342
|
+
dz brain ground "<prompt>" [--k <N>] [--source <slug>] [--text] # retrieve + emit grounding citations (hook entrypoint; silent if irrelevant)
|
|
343
|
+
dz brain init [--project <dir>] [--k <N>] # opt-in: wire the grounding UserPromptSubmit hook into settings.json
|
|
339
344
|
dz pretrain [--project <dir>]
|
|
340
345
|
dz recommend "<task description>"
|
|
341
346
|
dz compose <preset1+preset2+...> [--target <name>]
|
package/dist/cli.d.ts
CHANGED
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
export interface CliIo {
|
|
8
8
|
readonly cwd?: string;
|
|
9
9
|
readonly write?: (line: string) => void;
|
|
10
|
+
/**
|
|
11
|
+
* Pre-read STDIN content (injectable so `dz brain ground`'s hook path is testable without
|
|
12
|
+
* an actual pipe). When omitted, the CLI reads fd 0 synchronously — but only for the one
|
|
13
|
+
* command that needs it (`brain ground`), and never when stdin is a TTY (nothing piped).
|
|
14
|
+
*/
|
|
15
|
+
readonly stdin?: string;
|
|
10
16
|
}
|
|
11
17
|
export declare function runCli(argv: string[], io?: CliIo): Promise<number>;
|
|
12
18
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAsHH,2EAA2E;AAC3E,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AA6xED,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,KAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAsG5E"}
|
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import { execSync } from 'node:child_process';
|
|
|
9
9
|
import { homedir } from 'node:os';
|
|
10
10
|
import { createRequire } from 'node:module';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import { createSkill, getSkillInfo, getWorkflow, isTargetName, listSkills, runDoctor, runInit, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, checkUpstream, compareSkills, checkAllUpstream, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, TARGET_NAMES, WORKFLOW_NAMES, importEcc, recordPattern, consolidateSessions, pruneNoisePatterns, recallPatterns, queryBookKnowledge, loadStorePatternsSync, bundleSkills, brainHome, listBrain, promoteProjectToBrain, queryBrain, } from '@dzhechkov/harness-core';
|
|
12
|
+
import { createSkill, getSkillInfo, getWorkflow, isTargetName, listSkills, runDoctor, runInit, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, checkUpstream, compareSkills, checkAllUpstream, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, TARGET_NAMES, WORKFLOW_NAMES, importEcc, recordPattern, consolidateSessions, pruneNoisePatterns, recallPatterns, queryBookKnowledge, loadStorePatternsSync, bundleSkills, brainHome, listBrain, promoteProjectToBrain, queryBrain, groundPrompt, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, } from '@dzhechkov/harness-core';
|
|
13
13
|
import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
|
|
14
14
|
import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
|
|
15
15
|
const USAGE = `dz - DZ cross-platform harness CLI
|
|
@@ -35,7 +35,11 @@ Usage:
|
|
|
35
35
|
dz recall "<query>" [--limit <N>] [--books [--book <slug>]] [--project <dir>] | dz recall --all [--json]
|
|
36
36
|
dz brain list [--json] (the durable cross-project knowledge brain)
|
|
37
37
|
dz brain query "<q>" [--source <slug>] [--limit <N>] [--json] (cross-source lexical recall over the brain)
|
|
38
|
-
dz brain add [--source <slug>] [--project <dir>] [--json]
|
|
38
|
+
dz brain add [--source <slug>] [--project <dir>] [--from-slice <f>|--from-pack <p>|--from-kus <f> --slug <s>] [--kind <k>] [--license <spdx>] [--json] (grow the brain: promote this project, or import a slice/pack/KU-array)
|
|
39
|
+
dz brain primer <slug> [--json] (print a source's capability card — KU-type histogram + top decision moments)
|
|
40
|
+
dz brain export --source <slug> --out <file> (export ONE source as a portable, lexical-only books.sqlite slice)
|
|
41
|
+
dz brain ground [<prompt>] [--k <N>] [--source <slug>] [--text] (UserPromptSubmit hook: inject brain citations for a prompt; reads STDIN if no prompt)
|
|
42
|
+
dz brain init [--project <dir>] [--k <N>] (wire the grounding hook into .claude/settings.json — opt-in)
|
|
39
43
|
dz pretrain [--project <dir>]
|
|
40
44
|
dz recommend "<task description>"
|
|
41
45
|
dz compose <preset1+preset2+...> [--target <name>]
|
|
@@ -906,9 +910,60 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
906
910
|
const BRAIN_USAGE = `dz brain — the durable, cross-project knowledge brain
|
|
907
911
|
|
|
908
912
|
Usage:
|
|
909
|
-
dz brain list
|
|
910
|
-
dz brain query
|
|
911
|
-
dz brain add
|
|
913
|
+
dz brain list [--json]
|
|
914
|
+
dz brain query "<q>" [--source <slug>] [--limit <N>] [--json]
|
|
915
|
+
dz brain add [--source <slug>] [--project <dir>] [--json]
|
|
916
|
+
dz brain add --from-slice <file.sqlite> [--json]
|
|
917
|
+
dz brain add --from-pack <pkg-or-dir> [--json]
|
|
918
|
+
dz brain add --from-kus <file.json> --slug <s> [--kind repo|book|paper] [--license <spdx>] [--override] [--json]
|
|
919
|
+
dz brain primer <slug> [--json]
|
|
920
|
+
dz brain export --source <slug> --out <file>
|
|
921
|
+
dz brain ground [<prompt>] [--k <N>] [--source <slug>] [--text]
|
|
922
|
+
dz brain init [--project <dir>] [--k <N>]
|
|
923
|
+
|
|
924
|
+
add: default promotes THIS project's digitized-book KUs into the brain. The three --from-* modes
|
|
925
|
+
are mutually exclusive alternate inputs:
|
|
926
|
+
--from-slice imports a standalone per-book slice (a \`dz brain export\` output).
|
|
927
|
+
--from-pack resolves a pack (node_modules pkg or local dir) and imports every
|
|
928
|
+
\`brain/<slug>.sqlite\` slice it ships.
|
|
929
|
+
--from-kus registers a JSON array of already-shaped KUs under --slug (default --kind repo).
|
|
930
|
+
primer: prints a source's capability card (the deterministic KU-type histogram + top decision moments).
|
|
931
|
+
export: writes ONE source's KUs as a portable, lexical-only \`books.sqlite\` slice (vectors re-embed on import).
|
|
932
|
+
ground: the UserPromptSubmit hook entrypoint. Grounds a prompt (positional or STDIN) against
|
|
933
|
+
the brain and, when relevant KUs are found, prints Claude-Code-injectable JSON on
|
|
934
|
+
stdout ({"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":…}}).
|
|
935
|
+
Always exits 0 — grounding is advisory and must never fail a prompt. --text prints the
|
|
936
|
+
raw citation block instead of the JSON wrapper (for manual inspection).
|
|
937
|
+
init: wires \`brain ground\` into .claude/settings.json as an opt-in UserPromptSubmit hook.`;
|
|
938
|
+
/**
|
|
939
|
+
* Extract the user prompt from a Claude Code `UserPromptSubmit` hook STDIN payload. Tries the
|
|
940
|
+
* common JSON shapes (\`.prompt\`, \`.user_prompt\`, \`.userPrompt\`); if the payload is plain
|
|
941
|
+
* non-JSON text, the whole (trimmed) text is the prompt. Empty / unrecognized → \`''\` (the caller
|
|
942
|
+
* then emits nothing and exits 0 — grounding never blocks).
|
|
943
|
+
*/
|
|
944
|
+
function extractPromptFromStdin(raw) {
|
|
945
|
+
const text = raw.trim();
|
|
946
|
+
if (text === '')
|
|
947
|
+
return '';
|
|
948
|
+
let parsed;
|
|
949
|
+
try {
|
|
950
|
+
parsed = JSON.parse(text);
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
// Plain non-JSON text → treat the whole thing as the prompt.
|
|
954
|
+
return text;
|
|
955
|
+
}
|
|
956
|
+
if (parsed !== null && typeof parsed === 'object') {
|
|
957
|
+
const obj = parsed;
|
|
958
|
+
for (const key of ['prompt', 'user_prompt', 'userPrompt']) {
|
|
959
|
+
const v = obj[key];
|
|
960
|
+
if (typeof v === 'string' && v.trim() !== '')
|
|
961
|
+
return v;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
// Valid JSON but no recognizable prompt field (or a bare scalar) → nothing to ground.
|
|
965
|
+
return '';
|
|
966
|
+
}
|
|
912
967
|
/**
|
|
913
968
|
* Resolve the deps root — the directory whose `package.json` can resolve `better-sqlite3` **and**
|
|
914
969
|
* `agentdb`, the native driver `book-kb` + `agentdb-index` load through. Try `cwd` first (a project
|
|
@@ -933,7 +988,71 @@ function resolveDepsRoot(cwd) {
|
|
|
933
988
|
const cliDir = dirname(fileURLToPath(import.meta.url)); // .../@dzhechkov/harness-cli/dist
|
|
934
989
|
return canResolve(cliDir) ? cliDir : cwd;
|
|
935
990
|
}
|
|
936
|
-
|
|
991
|
+
/**
|
|
992
|
+
* Resolve a `--from-pack` spec to a pack directory: a local directory (absolute or cwd-relative)
|
|
993
|
+
* wins; otherwise resolve it as an installed node package (its `package.json` from `cwd` then the
|
|
994
|
+
* CLI's deps root). Returns the pack's root dir, or `undefined` if it is neither.
|
|
995
|
+
*/
|
|
996
|
+
function resolvePackDir(cwd, depsRoot, spec) {
|
|
997
|
+
const asDir = resolve(cwd, spec);
|
|
998
|
+
if (existsSync(asDir) && lstatSync(asDir).isDirectory())
|
|
999
|
+
return asDir;
|
|
1000
|
+
for (const root of [cwd, depsRoot]) {
|
|
1001
|
+
try {
|
|
1002
|
+
const req = createRequire(join(root, 'package.json'));
|
|
1003
|
+
return dirname(req.resolve(`${spec}/package.json`));
|
|
1004
|
+
}
|
|
1005
|
+
catch {
|
|
1006
|
+
/* try the next resolution base */
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return undefined;
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Validate that a parsed JSON value is an array of KU-shaped objects (the `--from-kus` backend).
|
|
1013
|
+
* Every element must carry the required BookKU string fields; `book` is optional (the caller's
|
|
1014
|
+
* `--slug` is authoritative). On the first bad element returns `{ error }` naming exactly what is
|
|
1015
|
+
* wrong; otherwise returns clean `BookKU[]` (extra fields dropped).
|
|
1016
|
+
*/
|
|
1017
|
+
function validateKuArray(parsed) {
|
|
1018
|
+
if (!Array.isArray(parsed))
|
|
1019
|
+
return { error: 'expected a JSON array of KU objects at the top level' };
|
|
1020
|
+
if (parsed.length === 0)
|
|
1021
|
+
return { error: 'the KU array is empty' };
|
|
1022
|
+
const required = ['kuId', 'corpusVersion', 'type', 'name', 'problem', 'content'];
|
|
1023
|
+
const kus = [];
|
|
1024
|
+
for (let i = 0; i < parsed.length; i += 1) {
|
|
1025
|
+
const v = parsed[i];
|
|
1026
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v)) {
|
|
1027
|
+
return { error: `KU #${i} is not an object` };
|
|
1028
|
+
}
|
|
1029
|
+
const o = v;
|
|
1030
|
+
const missing = required.filter((k) => typeof o[k] !== 'string');
|
|
1031
|
+
if (missing.length > 0) {
|
|
1032
|
+
return { error: `KU #${i} missing/invalid string field(s): ${missing.join(', ')}` };
|
|
1033
|
+
}
|
|
1034
|
+
const pages = Array.isArray(o['pages'])
|
|
1035
|
+
? o['pages'].filter((n) => typeof n === 'number')
|
|
1036
|
+
: undefined;
|
|
1037
|
+
const meta = o['metadata'] !== null && typeof o['metadata'] === 'object' && !Array.isArray(o['metadata'])
|
|
1038
|
+
? o['metadata']
|
|
1039
|
+
: undefined;
|
|
1040
|
+
kus.push({
|
|
1041
|
+
book: typeof o['book'] === 'string' ? o['book'] : '',
|
|
1042
|
+
kuId: o['kuId'],
|
|
1043
|
+
corpusVersion: o['corpusVersion'],
|
|
1044
|
+
type: o['type'],
|
|
1045
|
+
name: o['name'],
|
|
1046
|
+
problem: o['problem'],
|
|
1047
|
+
content: o['content'],
|
|
1048
|
+
...(typeof o['chapter'] === 'string' ? { chapter: o['chapter'] } : {}),
|
|
1049
|
+
...(pages !== undefined ? { pages } : {}),
|
|
1050
|
+
...(meta !== undefined ? { metadata: meta } : {}),
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
return { kus };
|
|
1054
|
+
}
|
|
1055
|
+
async function cmdBrain(options, flags, cwd, write, readStdin) {
|
|
937
1056
|
const sub = options.get('_positional_0');
|
|
938
1057
|
const asJson = flags.has('json');
|
|
939
1058
|
// ── dz brain list ──────────────────────────────────────────────────────────────────────────
|
|
@@ -989,13 +1108,189 @@ async function cmdBrain(options, flags, cwd, write) {
|
|
|
989
1108
|
}
|
|
990
1109
|
return 0;
|
|
991
1110
|
}
|
|
1111
|
+
// ── dz brain primer <slug> ───────────────────────────────────────────────────────────────────
|
|
1112
|
+
// Print a source's capability card (KU-type histogram + top decision moments) from the brain.
|
|
1113
|
+
if (sub === 'primer') {
|
|
1114
|
+
const slug = options.get('_positional_1');
|
|
1115
|
+
if (!slug) {
|
|
1116
|
+
write('dz brain primer: a source slug is required');
|
|
1117
|
+
write(' Example: dz brain primer ddia');
|
|
1118
|
+
return 1;
|
|
1119
|
+
}
|
|
1120
|
+
const { markdown, error } = await buildPrimer({ slug, depsRoot: resolveDepsRoot(cwd) });
|
|
1121
|
+
if (error !== undefined) {
|
|
1122
|
+
write(`dz brain primer: ${error}`);
|
|
1123
|
+
return 1;
|
|
1124
|
+
}
|
|
1125
|
+
if (asJson) {
|
|
1126
|
+
write(JSON.stringify({ slug, markdown }));
|
|
1127
|
+
return 0;
|
|
1128
|
+
}
|
|
1129
|
+
write(markdown);
|
|
1130
|
+
return 0;
|
|
1131
|
+
}
|
|
1132
|
+
// ── dz brain export --source <slug> --out <file> ─────────────────────────────────────────────
|
|
1133
|
+
// Export ONE source's KUs as a portable, lexical-only books.sqlite slice.
|
|
1134
|
+
if (sub === 'export') {
|
|
1135
|
+
const source = options.get('source');
|
|
1136
|
+
const out = options.get('out') ?? (flags.has('out') ? '' : undefined);
|
|
1137
|
+
if (source === undefined || source === '' || out === undefined || out === '') {
|
|
1138
|
+
write('dz brain export: --source <slug> and --out <file> are both required');
|
|
1139
|
+
write(' Example: dz brain export --source ddia --out ./ddia.sqlite');
|
|
1140
|
+
return 1;
|
|
1141
|
+
}
|
|
1142
|
+
const outPath = resolve(cwd, out);
|
|
1143
|
+
const { kuCount, error } = await exportBrainSlice({ slug: source, outPath, depsRoot: resolveDepsRoot(cwd) });
|
|
1144
|
+
if (error !== undefined) {
|
|
1145
|
+
write(`dz brain export: ${error}`);
|
|
1146
|
+
return 1;
|
|
1147
|
+
}
|
|
1148
|
+
write(`dz brain: exported ${kuCount} KU slice → ${outPath}`);
|
|
1149
|
+
return 0;
|
|
1150
|
+
}
|
|
992
1151
|
// ── dz brain add ─────────────────────────────────────────────────────────────────────────────
|
|
1152
|
+
// Default promotes THIS project's book KB. Three mutually-exclusive --from-* modes ingest a
|
|
1153
|
+
// slice, a pack's shipped slices, or a raw KU array instead.
|
|
993
1154
|
if (sub === 'add') {
|
|
1155
|
+
const depsRoot = resolveDepsRoot(cwd);
|
|
1156
|
+
const addedTs = new Date().toISOString();
|
|
1157
|
+
// A `--from-* ` given without a value parses as a FLAG (missing value); treat that as '' so the
|
|
1158
|
+
// mode still triggers its "requires a value" guard rather than silently falling through.
|
|
1159
|
+
const fromSlice = options.get('from-slice') ?? (flags.has('from-slice') ? '' : undefined);
|
|
1160
|
+
const fromPack = options.get('from-pack') ?? (flags.has('from-pack') ? '' : undefined);
|
|
1161
|
+
const fromKus = options.get('from-kus') ?? (flags.has('from-kus') ? '' : undefined);
|
|
1162
|
+
const modes = [fromSlice, fromPack, fromKus].filter((m) => m !== undefined).length;
|
|
1163
|
+
if (modes > 1) {
|
|
1164
|
+
write('dz brain add: --from-slice, --from-pack, and --from-kus are mutually exclusive');
|
|
1165
|
+
return 1;
|
|
1166
|
+
}
|
|
1167
|
+
// ── mode: --from-slice <file> → import a standalone per-book slice ──────────────────────────
|
|
1168
|
+
if (fromSlice !== undefined) {
|
|
1169
|
+
if (fromSlice === '') {
|
|
1170
|
+
write('dz brain add: --from-slice requires a slice file path');
|
|
1171
|
+
return 1;
|
|
1172
|
+
}
|
|
1173
|
+
const slicePath = resolve(cwd, fromSlice);
|
|
1174
|
+
if (!existsSync(slicePath)) {
|
|
1175
|
+
write(`dz brain add: no slice at ${slicePath}`);
|
|
1176
|
+
return 1;
|
|
1177
|
+
}
|
|
1178
|
+
const result = await importBrainSlice({ slicePath, depsRoot, addedTs });
|
|
1179
|
+
if (asJson) {
|
|
1180
|
+
write(JSON.stringify(result));
|
|
1181
|
+
return result.sources.length === 0 ? 1 : 0;
|
|
1182
|
+
}
|
|
1183
|
+
if (result.sources.length === 0) {
|
|
1184
|
+
write(`dz brain add: ${result.error ?? 'nothing to import from the slice'}`);
|
|
1185
|
+
return 1;
|
|
1186
|
+
}
|
|
1187
|
+
write(`dz brain: imported ${result.kus} KU from slice → ${result.sources.length} source(s) into ${brainHome()}: ${result.sources.join(', ')}`);
|
|
1188
|
+
if (result.error !== undefined)
|
|
1189
|
+
write(` (partial: ${result.error})`);
|
|
1190
|
+
return 0;
|
|
1191
|
+
}
|
|
1192
|
+
// ── mode: --from-pack <pkg-or-dir> → import every brain/<slug>.sqlite the pack ships ────────
|
|
1193
|
+
if (fromPack !== undefined) {
|
|
1194
|
+
if (fromPack === '') {
|
|
1195
|
+
write('dz brain add: --from-pack requires a package name or directory');
|
|
1196
|
+
return 1;
|
|
1197
|
+
}
|
|
1198
|
+
const packDir = resolvePackDir(cwd, depsRoot, fromPack);
|
|
1199
|
+
if (packDir === undefined) {
|
|
1200
|
+
write(`dz brain add: could not resolve pack '${fromPack}' (not a local directory, not an installed package)`);
|
|
1201
|
+
return 1;
|
|
1202
|
+
}
|
|
1203
|
+
const packBrainDir = join(packDir, 'brain');
|
|
1204
|
+
const slices = existsSync(packBrainDir)
|
|
1205
|
+
? readdirSync(packBrainDir).filter((f) => f.endsWith('.sqlite')).sort()
|
|
1206
|
+
: [];
|
|
1207
|
+
if (slices.length === 0) {
|
|
1208
|
+
write(`dz brain add: no brain/<slug>.sqlite slice found in pack '${fromPack}' (${packDir})`);
|
|
1209
|
+
return 1;
|
|
1210
|
+
}
|
|
1211
|
+
const imported = [];
|
|
1212
|
+
for (const f of slices) {
|
|
1213
|
+
imported.push(await importBrainSlice({ slicePath: join(packBrainDir, f), depsRoot, addedTs }));
|
|
1214
|
+
}
|
|
1215
|
+
const allSources = imported.flatMap((r) => r.sources);
|
|
1216
|
+
const totalKus = imported.reduce((s, r) => s + r.kus, 0);
|
|
1217
|
+
const firstError = imported.find((r) => r.error !== undefined)?.error;
|
|
1218
|
+
if (asJson) {
|
|
1219
|
+
write(JSON.stringify({ sources: allSources, kus: totalKus, ...(firstError !== undefined ? { error: firstError } : {}) }));
|
|
1220
|
+
return allSources.length === 0 ? 1 : 0;
|
|
1221
|
+
}
|
|
1222
|
+
if (allSources.length === 0) {
|
|
1223
|
+
write(`dz brain add: ${firstError ?? 'nothing imported'} from pack '${fromPack}'`);
|
|
1224
|
+
return 1;
|
|
1225
|
+
}
|
|
1226
|
+
write(`dz brain: imported ${totalKus} KU from ${slices.length} slice(s) in pack '${fromPack}' → ${allSources.length} source(s) into ${brainHome()}: ${allSources.join(', ')}`);
|
|
1227
|
+
if (firstError !== undefined)
|
|
1228
|
+
write(` (partial: ${firstError})`);
|
|
1229
|
+
return 0;
|
|
1230
|
+
}
|
|
1231
|
+
// ── mode: --from-kus <file.json> --slug <s> → register a raw KU array ───────────────────────
|
|
1232
|
+
if (fromKus !== undefined) {
|
|
1233
|
+
if (fromKus === '') {
|
|
1234
|
+
write('dz brain add: --from-kus requires a JSON file path');
|
|
1235
|
+
return 1;
|
|
1236
|
+
}
|
|
1237
|
+
const slug = options.get('slug');
|
|
1238
|
+
if (slug === undefined || slug === '') {
|
|
1239
|
+
write('dz brain add --from-kus: --slug <s> is required (every KU is registered under it)');
|
|
1240
|
+
return 1;
|
|
1241
|
+
}
|
|
1242
|
+
const kusPath = resolve(cwd, fromKus);
|
|
1243
|
+
if (!existsSync(kusPath)) {
|
|
1244
|
+
write(`dz brain add: no KU file at ${kusPath}`);
|
|
1245
|
+
return 1;
|
|
1246
|
+
}
|
|
1247
|
+
let parsed;
|
|
1248
|
+
try {
|
|
1249
|
+
parsed = JSON.parse(readFileSync(kusPath, 'utf8'));
|
|
1250
|
+
}
|
|
1251
|
+
catch (err) {
|
|
1252
|
+
write(`dz brain add --from-kus: invalid JSON in ${kusPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1253
|
+
return 1;
|
|
1254
|
+
}
|
|
1255
|
+
const valid = validateKuArray(parsed);
|
|
1256
|
+
if ('error' in valid) {
|
|
1257
|
+
write(`dz brain add --from-kus: bad KU shape — ${valid.error}`);
|
|
1258
|
+
return 1;
|
|
1259
|
+
}
|
|
1260
|
+
const kind = options.get('kind') ?? 'repo';
|
|
1261
|
+
if (kind !== 'repo' && kind !== 'book' && kind !== 'paper') {
|
|
1262
|
+
write(`dz brain add --from-kus: --kind must be one of repo|book|paper (got '${kind}')`);
|
|
1263
|
+
return 1;
|
|
1264
|
+
}
|
|
1265
|
+
const license = options.get('license');
|
|
1266
|
+
const result = await registerKusToBrain({
|
|
1267
|
+
kus: valid.kus,
|
|
1268
|
+
slug,
|
|
1269
|
+
kind,
|
|
1270
|
+
depsRoot,
|
|
1271
|
+
addedTs,
|
|
1272
|
+
...(license !== undefined ? { license } : {}),
|
|
1273
|
+
...(flags.has('override') ? { override: true } : {}),
|
|
1274
|
+
});
|
|
1275
|
+
if (asJson) {
|
|
1276
|
+
write(JSON.stringify({ slug, kind, ...result }));
|
|
1277
|
+
return result.kus === 0 ? 1 : 0;
|
|
1278
|
+
}
|
|
1279
|
+
if (result.kus === 0) {
|
|
1280
|
+
write(`dz brain add --from-kus: ${result.error ?? 'nothing registered'}`);
|
|
1281
|
+
return 1;
|
|
1282
|
+
}
|
|
1283
|
+
write(`dz brain: registered ${result.kus} KU under '${slug}' (kind ${kind}) into ${brainHome()}`);
|
|
1284
|
+
if (result.error !== undefined)
|
|
1285
|
+
write(` (partial: ${result.error})`);
|
|
1286
|
+
return 0;
|
|
1287
|
+
}
|
|
1288
|
+
// ── default: promote THIS project's digitized book KB ──────────────────────────────────────
|
|
994
1289
|
const source = options.get('source');
|
|
995
1290
|
const result = await promoteProjectToBrain({
|
|
996
1291
|
projectRoot: resolve(cwd, options.get('project') ?? '.'),
|
|
997
|
-
depsRoot
|
|
998
|
-
addedTs
|
|
1292
|
+
depsRoot,
|
|
1293
|
+
addedTs,
|
|
999
1294
|
...(source !== undefined ? { source } : {}),
|
|
1000
1295
|
});
|
|
1001
1296
|
if (asJson) {
|
|
@@ -1014,6 +1309,101 @@ async function cmdBrain(options, flags, cwd, write) {
|
|
|
1014
1309
|
write(` (partial: ${result.error})`);
|
|
1015
1310
|
return 0;
|
|
1016
1311
|
}
|
|
1312
|
+
// ── dz brain ground [<prompt>] ───────────────────────────────────────────────────────────────
|
|
1313
|
+
// The UserPromptSubmit hook entrypoint. ALWAYS exits 0 — grounding is advisory and must never
|
|
1314
|
+
// fail a prompt. Emits nothing (silent) unless the brain has relevant citations for the prompt.
|
|
1315
|
+
if (sub === 'ground') {
|
|
1316
|
+
const positional = options.get('_positional_1');
|
|
1317
|
+
// Positional prompt wins; else read the hook payload from STDIN. Empty stdin → emit nothing.
|
|
1318
|
+
const prompt = positional !== undefined && positional !== ''
|
|
1319
|
+
? positional
|
|
1320
|
+
: extractPromptFromStdin(readStdin());
|
|
1321
|
+
if (prompt.trim() === '')
|
|
1322
|
+
return 0;
|
|
1323
|
+
const source = options.get('source');
|
|
1324
|
+
const kRaw = options.get('k');
|
|
1325
|
+
const k = kRaw !== undefined ? Math.max(1, parseInt(kRaw, 10) || 5) : undefined;
|
|
1326
|
+
const gopts = {
|
|
1327
|
+
prompt,
|
|
1328
|
+
depsRoot: resolveDepsRoot(cwd),
|
|
1329
|
+
};
|
|
1330
|
+
if (k !== undefined)
|
|
1331
|
+
gopts.k = k;
|
|
1332
|
+
if (source !== undefined)
|
|
1333
|
+
gopts.source = source;
|
|
1334
|
+
const res = await groundPrompt(gopts);
|
|
1335
|
+
if (!res.emitted)
|
|
1336
|
+
return 0; // no relevant citations → inject nothing
|
|
1337
|
+
// --text → raw block for manual inspection; default → Claude-Code-injectable additionalContext.
|
|
1338
|
+
if (flags.has('text')) {
|
|
1339
|
+
write(res.block);
|
|
1340
|
+
return 0;
|
|
1341
|
+
}
|
|
1342
|
+
write(JSON.stringify({
|
|
1343
|
+
hookSpecificOutput: {
|
|
1344
|
+
hookEventName: 'UserPromptSubmit',
|
|
1345
|
+
additionalContext: res.block,
|
|
1346
|
+
},
|
|
1347
|
+
}));
|
|
1348
|
+
return 0;
|
|
1349
|
+
}
|
|
1350
|
+
// ── dz brain init ────────────────────────────────────────────────────────────────────────────
|
|
1351
|
+
// Opt-in: wire `dz brain ground` into .claude/settings.json as a UserPromptSubmit hook.
|
|
1352
|
+
// Idempotent read-merge-write — preserve every existing hook/key (e.g. the agentic-qe route hook).
|
|
1353
|
+
if (sub === 'init') {
|
|
1354
|
+
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
1355
|
+
const kRaw = options.get('k');
|
|
1356
|
+
const k = kRaw !== undefined ? Math.max(1, parseInt(kRaw, 10) || 5) : 5;
|
|
1357
|
+
// Invoke the SAME bin this CLI runs from: dist/bin.js sits next to dist/cli.js.
|
|
1358
|
+
const dzBin = join(dirname(fileURLToPath(import.meta.url)), 'bin.js');
|
|
1359
|
+
const groundCmd = `node ${JSON.stringify(dzBin)} brain ground --k ${k}`;
|
|
1360
|
+
const settingsPath = join(projectRoot, '.claude', 'settings.json');
|
|
1361
|
+
let settings = {};
|
|
1362
|
+
if (existsSync(settingsPath)) {
|
|
1363
|
+
try {
|
|
1364
|
+
const parsed = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
1365
|
+
if (parsed !== null && typeof parsed === 'object')
|
|
1366
|
+
settings = parsed;
|
|
1367
|
+
}
|
|
1368
|
+
catch {
|
|
1369
|
+
// Corrupt/unreadable settings → start fresh rather than crash (grounding is opt-in glue).
|
|
1370
|
+
settings = {};
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
const hooks = (settings['hooks'] !== null && typeof settings['hooks'] === 'object'
|
|
1374
|
+
? settings['hooks']
|
|
1375
|
+
: {});
|
|
1376
|
+
const ups = Array.isArray(hooks['UserPromptSubmit']) ? [...hooks['UserPromptSubmit']] : [];
|
|
1377
|
+
// The grounding hook, in Claude Code's matcher-group shape (UserPromptSubmit takes no matcher).
|
|
1378
|
+
const entry = { hooks: [{ type: 'command', command: groundCmd, timeout: 5000 }] };
|
|
1379
|
+
// Identify OUR entry by a `brain ground` command; replace in place (idempotent), else append.
|
|
1380
|
+
const isGroundEntry = (e) => {
|
|
1381
|
+
const ex = e;
|
|
1382
|
+
return Array.isArray(ex?.hooks)
|
|
1383
|
+
&& ex.hooks.some((h) => typeof h?.command === 'string' && h.command.includes('brain ground'));
|
|
1384
|
+
};
|
|
1385
|
+
const idx = ups.findIndex(isGroundEntry);
|
|
1386
|
+
const replaced = idx >= 0;
|
|
1387
|
+
if (replaced)
|
|
1388
|
+
ups[idx] = entry;
|
|
1389
|
+
else
|
|
1390
|
+
ups.push(entry);
|
|
1391
|
+
hooks['UserPromptSubmit'] = ups;
|
|
1392
|
+
settings['hooks'] = hooks;
|
|
1393
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
1394
|
+
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
1395
|
+
write(`dz brain init: ${replaced ? 're-wired' : 'wired'} the grounding hook into ${settingsPath}`);
|
|
1396
|
+
write(` UserPromptSubmit → ${groundCmd}`);
|
|
1397
|
+
write(` Grounding is now ON for this project (k=${k}). Every prompt is screened against your brain;`);
|
|
1398
|
+
write(` when ≥2 content terms co-occur in a stored KU, the retrieved citations are injected as`);
|
|
1399
|
+
write(` additionalContext. The command exits 0 on any failure (empty brain, error, empty stdin),`);
|
|
1400
|
+
write(` so a grounding miss can never block or fail a prompt (continueOnError-equivalent).`);
|
|
1401
|
+
write(` Honesty: the hook only MECHANICALLY injects the retrieved citations — actually answering`);
|
|
1402
|
+
write(` FROM them (not from model memory/drift) is agent discipline (§7.2), not something the hook`);
|
|
1403
|
+
write(` can enforce.`);
|
|
1404
|
+
write(` To turn it OFF: remove the "brain ground" entry under hooks.UserPromptSubmit in ${settingsPath}.`);
|
|
1405
|
+
return 0;
|
|
1406
|
+
}
|
|
1017
1407
|
// ── unknown / absent subcommand ──────────────────────────────────────────────────────────────
|
|
1018
1408
|
write(BRAIN_USAGE);
|
|
1019
1409
|
return sub === undefined ? 0 : 1;
|
|
@@ -1869,6 +2259,21 @@ async function cmdImportEcc(options, flags, cwd, write) {
|
|
|
1869
2259
|
export async function runCli(argv, io = {}) {
|
|
1870
2260
|
const cwd = io.cwd ?? process.cwd();
|
|
1871
2261
|
const write = io.write ?? ((line) => { console.log(line); });
|
|
2262
|
+
// Lazy STDIN reader — only `dz brain ground` reads it, and only when no positional prompt is
|
|
2263
|
+
// given. Never blocks: injected `io.stdin` wins; else read fd 0 synchronously, but bail to '' on
|
|
2264
|
+
// a TTY (nothing piped) or any read error. Grounding must never hang waiting on an empty pipe.
|
|
2265
|
+
const readStdin = () => {
|
|
2266
|
+
if (io.stdin !== undefined)
|
|
2267
|
+
return io.stdin;
|
|
2268
|
+
try {
|
|
2269
|
+
if (process.stdin.isTTY)
|
|
2270
|
+
return '';
|
|
2271
|
+
return readFileSync(0, 'utf8');
|
|
2272
|
+
}
|
|
2273
|
+
catch {
|
|
2274
|
+
return '';
|
|
2275
|
+
}
|
|
2276
|
+
};
|
|
1872
2277
|
const { command, options, flags } = parseArgs(argv);
|
|
1873
2278
|
if (command === '' || command === 'help' || flags.has('help')) {
|
|
1874
2279
|
write(USAGE);
|
|
@@ -1908,7 +2313,7 @@ export async function runCli(argv, io = {}) {
|
|
|
1908
2313
|
case 'recall':
|
|
1909
2314
|
return await cmdRecall(options, flags, cwd, write);
|
|
1910
2315
|
case 'brain':
|
|
1911
|
-
return await cmdBrain(options, flags, cwd, write);
|
|
2316
|
+
return await cmdBrain(options, flags, cwd, write, readStdin);
|
|
1912
2317
|
case 'setup':
|
|
1913
2318
|
return await cmdSetup(options, flags, cwd, write);
|
|
1914
2319
|
case 'pretrain':
|