dravoice 0.1.2 → 0.1.3
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/LICENSE +21 -21
- package/README.md +102 -36
- package/bin/dravoice.js +11 -10
- package/package.json +47 -45
- package/src/index.js +874 -197
- package/src/v2/analyzers/discourse.js +63 -63
- package/src/v2/analyzers/evidence.js +82 -82
- package/src/v2/analyzers/lexical.js +114 -114
- package/src/v2/analyzers/register.js +46 -34
- package/src/v2/analyzers/rhetorical-shape.js +59 -59
- package/src/v2/analyzers/rhythm.js +39 -47
- package/src/v2/analyzers/structure.js +24 -24
- package/src/v2/benchmark.js +574 -568
- package/src/v2/brief.js +154 -146
- package/src/v2/config.js +78 -0
- package/src/v2/document-model.js +351 -260
- package/src/v2/inspect.js +67 -67
- package/src/v2/io-utils.js +51 -0
- package/src/v2/profile.js +227 -203
- package/src/v2/prompt.js +65 -64
- package/src/v2/review.js +177 -173
- package/src/v2/revise-plan.js +437 -433
- package/src/v2/stylometry.js +342 -332
- package/src/v2/text-utils.js +123 -123
package/src/index.js
CHANGED
|
@@ -1,226 +1,903 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import {
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import readline from "node:readline";
|
|
4
|
+
import {
|
|
5
|
+
prepareVoiceBenchmark,
|
|
6
|
+
renderBenchmarkReport,
|
|
7
|
+
scoreVoiceBenchmark,
|
|
8
|
+
} from "./v2/benchmark.js";
|
|
9
|
+
import { renderVoiceBriefV2, voiceArticleBriefV2 } from "./v2/brief.js";
|
|
10
|
+
import { renderInspectV2 } from "./v2/inspect.js";
|
|
10
11
|
import { learnVoicePackV2, loadVoicePackV2 } from "./v2/profile.js";
|
|
11
12
|
import { voicePromptPackV2 } from "./v2/prompt.js";
|
|
12
13
|
import { renderVoiceReviewV2, reviewVoiceDraftV2 } from "./v2/review.js";
|
|
13
14
|
import { renderRevisePlanV2, revisePlanDraftV2 } from "./v2/revise-plan.js";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
import { VOICE_EXTENSIONS } from "./v2/document-model.js";
|
|
16
|
+
import { writeUtf8FileSafely } from "./v2/io-utils.js";
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_EXAMPLES_DIR,
|
|
19
|
+
DEFAULT_PROMPT_FORMAT,
|
|
20
|
+
DEFAULT_VOICE_DIR,
|
|
21
|
+
displayPath,
|
|
22
|
+
loadProjectConfig,
|
|
23
|
+
resolveConfiguredPath,
|
|
24
|
+
writeProjectConfig,
|
|
25
|
+
} from "./v2/config.js";
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
learnVoicePackV2 as learnVoicePack,
|
|
17
29
|
loadVoicePackV2 as loadVoicePack,
|
|
18
30
|
revisePlanDraftV2 as revisePlanDraft,
|
|
19
31
|
reviewVoiceDraftV2 as reviewVoiceDraft,
|
|
20
32
|
voicePromptPackV2 as voicePromptPack,
|
|
21
33
|
};
|
|
22
34
|
export { renderInspectV2, renderRevisePlanV2 as renderRevisePlan, renderVoiceReviewV2 as renderVoiceReview };
|
|
23
|
-
export { renderVoiceBriefV2 as renderVoiceBrief, voiceArticleBriefV2 as voiceArticleBrief };
|
|
24
|
-
export { prepareVoiceBenchmark, renderBenchmarkReport, scoreVoiceBenchmark };
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
export { renderVoiceBriefV2 as renderVoiceBrief, voiceArticleBriefV2 as voiceArticleBrief };
|
|
36
|
+
export { prepareVoiceBenchmark, renderBenchmarkReport, scoreVoiceBenchmark };
|
|
37
|
+
|
|
38
|
+
const INIT_DISCOVERY_DIRS = [
|
|
39
|
+
"./articles",
|
|
40
|
+
"./content",
|
|
41
|
+
"./posts",
|
|
42
|
+
"./blog",
|
|
43
|
+
"./essays",
|
|
44
|
+
"./writing",
|
|
45
|
+
"./drafts",
|
|
46
|
+
"./docs",
|
|
47
|
+
];
|
|
48
|
+
const SKIP_SOURCE_DIRS = new Set([".git", "node_modules", "dist", "build", "__pycache__", "prompts", "voice-pack", "dravoice-voice"]);
|
|
49
|
+
|
|
50
|
+
export async function runCli(args, io) {
|
|
51
|
+
try {
|
|
52
|
+
const [command, ...rest] = args;
|
|
53
|
+
if (!command || command === "--help" || command === "-h") {
|
|
54
|
+
io.stdout.write(helpText());
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (command === "help") {
|
|
59
|
+
if (rest.length === 0 || isHelpFlag(rest[0])) {
|
|
60
|
+
io.stdout.write(helpText());
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
io.stdout.write(helpForTopic(rest.join(" ")));
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (isHelpFlag(rest[0])) {
|
|
68
|
+
io.stdout.write(helpForTopic(command));
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (command === "init") {
|
|
73
|
+
const { options, positional } = parseArgs(rest, ["examples", "out", "no-interactive"], "init", ["no-interactive"]);
|
|
74
|
+
rejectPositionals(positional, "init");
|
|
75
|
+
const config = loadProjectConfig(io.cwd);
|
|
76
|
+
const examples = options.examples ?? config.examples ?? DEFAULT_EXAMPLES_DIR;
|
|
77
|
+
const out = options.out ?? config.voice ?? DEFAULT_VOICE_DIR;
|
|
78
|
+
const interactive = shouldRunInteractiveInit(io, options);
|
|
79
|
+
if (interactive) {
|
|
80
|
+
const guided = await runInitWizard({ io, examples, out });
|
|
81
|
+
if (guided.code !== undefined) {
|
|
82
|
+
return guided.code;
|
|
83
|
+
}
|
|
84
|
+
const profile = learnWithUsageExample({
|
|
85
|
+
examplesDir: guided.resolvedExamples,
|
|
86
|
+
outDir: resolvePath(io.cwd, out),
|
|
87
|
+
}, "init");
|
|
88
|
+
writeProjectConfig(io.cwd, { voice: displayPath(out), examples: displayPath(guided.examples) });
|
|
89
|
+
writeProfileSuccess(io, {
|
|
90
|
+
action: "Initialized Dravoice voice profile",
|
|
91
|
+
profile,
|
|
92
|
+
examples: guided.examples,
|
|
93
|
+
out,
|
|
94
|
+
});
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
const profile = learnWithUsageExample({
|
|
98
|
+
examplesDir: resolvePath(io.cwd, examples),
|
|
99
|
+
outDir: resolvePath(io.cwd, out),
|
|
100
|
+
}, "init");
|
|
101
|
+
writeProjectConfig(io.cwd, { voice: displayPath(out), examples: displayPath(examples) });
|
|
102
|
+
writeProfileSuccess(io, {
|
|
103
|
+
action: "Initialized Dravoice voice profile",
|
|
104
|
+
profile,
|
|
105
|
+
examples,
|
|
106
|
+
out,
|
|
107
|
+
});
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (command === "learn") {
|
|
112
|
+
const { options, positional } = parseArgs(rest, ["examples", "out"], "learn");
|
|
113
|
+
rejectPositionals(positional, "learn");
|
|
114
|
+
const config = loadProjectConfig(io.cwd);
|
|
115
|
+
const examples = options.examples ?? config.examples ?? DEFAULT_EXAMPLES_DIR;
|
|
116
|
+
const out = options.out ?? config.voice ?? DEFAULT_VOICE_DIR;
|
|
117
|
+
const profile = learnWithUsageExample({
|
|
118
|
+
examplesDir: resolvePath(io.cwd, examples),
|
|
119
|
+
outDir: resolvePath(io.cwd, out),
|
|
120
|
+
}, "learn");
|
|
121
|
+
writeProjectConfig(io.cwd, { voice: displayPath(out), examples: displayPath(examples) });
|
|
122
|
+
writeProfileSuccess(io, {
|
|
123
|
+
action: "Generated V2 voice pack",
|
|
124
|
+
profile,
|
|
125
|
+
examples,
|
|
126
|
+
out,
|
|
127
|
+
});
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
44
131
|
if (command === "review") {
|
|
45
|
-
const { options, positional } = parseArgs(rest, ["voice", "mode", "format"]);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
file
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
132
|
+
const { options, positional } = parseArgs(rest, ["voice", "mode", "format"], "review");
|
|
133
|
+
rejectUnexpectedPositionals(positional, 1, "review positional");
|
|
134
|
+
const format = formatOption(options.format, ["text", "json"], "review");
|
|
135
|
+
const mode = modeOption(options.mode, "review");
|
|
136
|
+
const file = positional[0];
|
|
137
|
+
if (!file) {
|
|
138
|
+
throw usageError("Missing draft file path for review", "review");
|
|
139
|
+
}
|
|
140
|
+
const result = reviewVoiceDraftV2({
|
|
141
|
+
file: resolvePath(io.cwd, file),
|
|
142
|
+
voice: loadVoicePackV2(resolveVoicePath(io.cwd, options.voice)),
|
|
143
|
+
cwd: io.cwd,
|
|
144
|
+
mode,
|
|
145
|
+
});
|
|
146
|
+
if (format === "json") {
|
|
147
|
+
io.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
148
|
+
} else {
|
|
149
|
+
io.stdout.write(renderVoiceReviewV2(result));
|
|
150
|
+
io.stdout.write("Next: revise only the findings that fit the article's intent.\n");
|
|
151
|
+
}
|
|
61
152
|
return result.exitCode;
|
|
62
153
|
}
|
|
63
154
|
|
|
64
155
|
if (command === "revise-plan") {
|
|
65
|
-
const { options, positional } = parseArgs(rest, ["voice", "format"]);
|
|
156
|
+
const { options, positional } = parseArgs(rest, ["voice", "format"], "revise-plan");
|
|
157
|
+
rejectUnexpectedPositionals(positional, 1, "revise-plan");
|
|
158
|
+
const format = formatOption(options.format, ["text", "json"], "revise-plan");
|
|
66
159
|
const file = positional[0];
|
|
67
160
|
if (!file) {
|
|
68
|
-
throw
|
|
161
|
+
throw usageError("Missing draft file path for revise-plan", "revise-plan");
|
|
69
162
|
}
|
|
70
163
|
const result = revisePlanDraftV2({
|
|
71
164
|
file: resolvePath(io.cwd, file),
|
|
72
|
-
voice: loadVoicePackV2(
|
|
165
|
+
voice: loadVoicePackV2(resolveVoicePath(io.cwd, options.voice)),
|
|
73
166
|
cwd: io.cwd,
|
|
74
167
|
});
|
|
75
|
-
if (
|
|
168
|
+
if (format === "json") {
|
|
76
169
|
io.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
77
170
|
} else {
|
|
78
171
|
io.stdout.write(renderRevisePlanV2(result));
|
|
172
|
+
io.stdout.write(`Next: drav review ${file}\n`);
|
|
79
173
|
}
|
|
80
174
|
return 0;
|
|
81
175
|
}
|
|
82
176
|
|
|
83
|
-
if (command === "prompt") {
|
|
84
|
-
const { options } = parseArgs(rest, ["voice", "format", "out"]);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
io.
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
});
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
177
|
+
if (command === "prompt") {
|
|
178
|
+
const { options, positional } = parseArgs(rest, ["voice", "format", "out"], "prompt");
|
|
179
|
+
rejectPositionals(positional, "prompt");
|
|
180
|
+
const promptOptions = resolvePromptOptions(io.cwd, options);
|
|
181
|
+
const format = formatOption(promptOptions.format, ["agents", "claude", "system"], "prompt");
|
|
182
|
+
const rendered = voicePromptPackV2({
|
|
183
|
+
voice: loadVoicePackV2(resolveVoicePath(io.cwd, options.voice)),
|
|
184
|
+
format,
|
|
185
|
+
outPath: promptOptions.out ? resolvePath(io.cwd, promptOptions.out) : undefined,
|
|
186
|
+
});
|
|
187
|
+
if (!promptOptions.out) {
|
|
188
|
+
io.stdout.write(rendered);
|
|
189
|
+
} else {
|
|
190
|
+
io.stdout.write(`Wrote prompt guidance to ${promptOptions.out}\n`);
|
|
191
|
+
io.stdout.write("Next: drav brief \"New topic\" --evidence notes.md --out brief.md\n");
|
|
192
|
+
}
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (command === "brief") {
|
|
197
|
+
const { options, positional } = parseArgs(rest, ["voice", "topic", "evidence", "format", "out"], "brief");
|
|
198
|
+
const format = formatOption(options.format, ["text", "json"], "brief");
|
|
199
|
+
const result = voiceArticleBriefV2({
|
|
200
|
+
voice: loadVoicePackV2(resolveVoicePath(io.cwd, options.voice)),
|
|
201
|
+
topic: topicOption(options, positional, "brief"),
|
|
202
|
+
evidence: options.evidence ? resolvePath(io.cwd, options.evidence) : undefined,
|
|
203
|
+
cwd: io.cwd,
|
|
204
|
+
});
|
|
205
|
+
const rendered = format === "json" ? `${JSON.stringify(result, null, 2)}\n` : renderVoiceBriefV2(result);
|
|
206
|
+
if (options.out) {
|
|
207
|
+
const outPath = resolvePath(io.cwd, options.out);
|
|
208
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
209
|
+
writeUtf8FileSafely(outPath, rendered);
|
|
210
|
+
if (format !== "json") {
|
|
211
|
+
io.stdout.write(`Wrote article brief to ${options.out}\n`);
|
|
212
|
+
io.stdout.write("Next: draft from the brief, then run drav revise-plan draft.md\n");
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
io.stdout.write(rendered);
|
|
216
|
+
}
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (command === "inspect") {
|
|
221
|
+
const { options, positional } = parseArgs(rest, ["voice"], "inspect");
|
|
222
|
+
rejectPositionals(positional, "inspect");
|
|
223
|
+
const profile = loadVoicePackV2(resolveVoicePath(io.cwd, options.voice));
|
|
224
|
+
io.stdout.write(renderInspectV2(profile));
|
|
225
|
+
io.stdout.write("Next: drav prompt --out AGENTS.md\n");
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (command === "benchmark") {
|
|
230
|
+
return runBenchmarkCli(rest, io);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
io.stderr.write(`${usageErrorMessage(`Unknown command: ${command}`, "help")}\n`);
|
|
234
|
+
return 2;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
io.stderr.write(`${error.message}\n`);
|
|
237
|
+
return 2;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function runBenchmarkCli(args, io) {
|
|
242
|
+
const [benchmarkCommand, ...rest] = args;
|
|
243
|
+
if (!benchmarkCommand || isHelpFlag(benchmarkCommand)) {
|
|
244
|
+
io.stdout.write(helpForTopic("benchmark"));
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (isHelpFlag(rest[0])) {
|
|
249
|
+
io.stdout.write(helpForTopic(`benchmark ${benchmarkCommand}`));
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (benchmarkCommand === "prepare") {
|
|
254
|
+
const { options, positional } = parseArgs(rest, ["examples", "topic", "out", "seed"], "benchmark prepare");
|
|
255
|
+
rejectPositionals(positional, "benchmark prepare");
|
|
256
|
+
const benchmark = prepareVoiceBenchmark({
|
|
257
|
+
examplesDir: resolvePath(io.cwd, requiredOption(options, "examples", "benchmark prepare")),
|
|
258
|
+
topic: requiredOption(options, "topic", "benchmark prepare"),
|
|
259
|
+
outDir: resolvePath(io.cwd, requiredOption(options, "out", "benchmark prepare")),
|
|
260
|
+
seed: options.seed ?? "1",
|
|
261
|
+
});
|
|
262
|
+
io.stdout.write(`Prepared benchmark at ${resolvePath(io.cwd, requiredOption(options, "out", "benchmark prepare"))} with ${benchmark.corpus.fileCount} source article(s).\n`);
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (benchmarkCommand === "score") {
|
|
267
|
+
const { options, positional } = parseArgs(rest, ["run", "judge", "format"], "benchmark score");
|
|
268
|
+
rejectPositionals(positional, "benchmark score");
|
|
269
|
+
const format = formatOption(options.format, ["text", "json"], "benchmark score");
|
|
270
|
+
const result = scoreVoiceBenchmark({
|
|
271
|
+
runDir: resolvePath(io.cwd, requiredOption(options, "run", "benchmark score")),
|
|
272
|
+
judgePath: options.judge ? resolvePath(io.cwd, options.judge) : undefined,
|
|
273
|
+
});
|
|
274
|
+
if (format === "json") {
|
|
275
|
+
io.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
276
|
+
} else {
|
|
277
|
+
io.stdout.write(renderBenchmarkReport(result));
|
|
278
|
+
}
|
|
279
|
+
return result.exitCode;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
throw usageError(`Unknown benchmark command: ${benchmarkCommand ?? ""}`.trim(), "benchmark");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function shouldRunInteractiveInit(io, options) {
|
|
286
|
+
return !options["no-interactive"] && !options.examples && !options.out && Boolean(io.stdin?.isTTY);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function runInitWizard({ io, examples, out }) {
|
|
290
|
+
const defaultExamples = examples ?? DEFAULT_EXAMPLES_DIR;
|
|
291
|
+
const defaultDisplay = displayPath(defaultExamples);
|
|
292
|
+
const defaultResolved = resolvePath(io.cwd, defaultExamples);
|
|
293
|
+
io.stdout.write([
|
|
294
|
+
"Dravoice needs your own writing before it can build a voice profile.",
|
|
295
|
+
`Current directory: ${io.cwd}`,
|
|
296
|
+
"`./articles` means an `articles` directory inside this folder.",
|
|
297
|
+
`Dravoice will write the learned profile to ${displayChildPath(out, "profile.json")} after it finds source writing.`,
|
|
298
|
+
"",
|
|
299
|
+
].join("\n"));
|
|
300
|
+
|
|
301
|
+
const rl = readline.createInterface({
|
|
302
|
+
input: io.stdin,
|
|
303
|
+
terminal: Boolean(io.stdin?.isTTY),
|
|
304
|
+
});
|
|
305
|
+
try {
|
|
306
|
+
const defaultFiles = voiceFilesInDirectory(defaultResolved);
|
|
307
|
+
if (defaultFiles.length > 0) {
|
|
308
|
+
return await confirmSourceFiles({ io, rl, examples: defaultExamples, resolvedExamples: defaultResolved, files: defaultFiles });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const candidates = discoverLikelySourceDirectories(io.cwd, defaultExamples);
|
|
312
|
+
if (candidates.length > 0) {
|
|
313
|
+
io.stdout.write("\nFound possible writing folders:\n");
|
|
314
|
+
candidates.slice(0, 5).forEach((candidate, index) => {
|
|
315
|
+
io.stdout.write(`${index + 1}. ${displayPath(candidate.examples)} (${candidate.files.length} supported files)\n`);
|
|
316
|
+
});
|
|
317
|
+
const first = candidates[0];
|
|
318
|
+
const useCandidate = await askLine(rl, io, `Use ${displayPath(first.examples)}? [Y/n] `);
|
|
319
|
+
if (!/^n/i.test(useCandidate.trim())) {
|
|
320
|
+
return await confirmSourceFiles({
|
|
321
|
+
io,
|
|
322
|
+
rl,
|
|
323
|
+
examples: first.examples,
|
|
324
|
+
resolvedExamples: first.resolvedExamples,
|
|
325
|
+
files: first.files,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
io.stdout.write("\n");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const answer = await askLine(rl, io, `Where is your existing writing? [${defaultDisplay}] `);
|
|
332
|
+
const selectedExamples = answer.trim() || defaultExamples;
|
|
333
|
+
const resolvedExamples = resolvePath(io.cwd, selectedExamples);
|
|
334
|
+
const selectedFiles = voiceFilesInDirectory(resolvedExamples);
|
|
335
|
+
|
|
336
|
+
if (selectedFiles.length > 0) {
|
|
337
|
+
return await confirmSourceFiles({ io, rl, examples: selectedExamples, resolvedExamples, files: selectedFiles });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!fs.existsSync(resolvedExamples)) {
|
|
341
|
+
const createAnswer = await askLine(rl, io, `Create ${displayPath(selectedExamples)} now? [Y/n] `);
|
|
342
|
+
if (!/^n/i.test(createAnswer.trim())) {
|
|
343
|
+
fs.mkdirSync(resolvedExamples, { recursive: true });
|
|
344
|
+
io.stdout.write(`\nCreated ${displayPath(selectedExamples)}\n`);
|
|
345
|
+
writeEmptyCorpusNextSteps(io, selectedExamples, "drav init");
|
|
346
|
+
return { code: 2 };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (directoryHasAnyFiles(resolvedExamples)) {
|
|
351
|
+
io.stdout.write(`\n${unsupportedExamplesMessage(resolvedExamples)}\n`);
|
|
352
|
+
writeUnsupportedCorpusNextSteps(io, selectedExamples, "drav init");
|
|
353
|
+
return { code: 2 };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
io.stdout.write(`\nNo Markdown, MDX, or text examples found at ${displayPath(selectedExamples)}\n`);
|
|
357
|
+
writeEmptyCorpusNextSteps(io, selectedExamples, "drav init");
|
|
358
|
+
return { code: 2 };
|
|
359
|
+
} finally {
|
|
360
|
+
rl.close();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function confirmSourceFiles({ io, rl, examples, resolvedExamples, files }) {
|
|
365
|
+
io.stdout.write(`\nFound ${files.length} supported source file(s) in ${displayPath(examples)}:\n`);
|
|
366
|
+
for (const file of files.slice(0, 8)) {
|
|
367
|
+
io.stdout.write(` - ${displayPath(path.relative(resolvedExamples, file))}\n`);
|
|
368
|
+
}
|
|
369
|
+
if (files.length > 8) {
|
|
370
|
+
io.stdout.write(` - ...and ${files.length - 8} more\n`);
|
|
371
|
+
}
|
|
372
|
+
const confirm = await askLine(rl, io, "Build the profile from these files? [Y/n] ");
|
|
373
|
+
if (/^n/i.test(confirm.trim())) {
|
|
374
|
+
io.stdout.write(`\nNo profile written. Next: drav init --examples <directory>\n`);
|
|
375
|
+
return { code: 2 };
|
|
376
|
+
}
|
|
377
|
+
io.stdout.write("\n");
|
|
378
|
+
return { examples, resolvedExamples };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function askLine(rl, io, prompt) {
|
|
382
|
+
io.stdout.write(prompt);
|
|
383
|
+
return new Promise((resolve) => {
|
|
384
|
+
rl.question("", resolve);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function discoverLikelySourceDirectories(cwd, defaultExamples) {
|
|
389
|
+
const defaultResolved = resolvePath(cwd, defaultExamples);
|
|
390
|
+
const seen = new Set([path.resolve(defaultResolved)]);
|
|
391
|
+
const candidates = [];
|
|
392
|
+
for (const candidate of INIT_DISCOVERY_DIRS) {
|
|
393
|
+
const resolvedExamples = resolvePath(cwd, candidate);
|
|
394
|
+
const normalized = path.resolve(resolvedExamples);
|
|
395
|
+
if (seen.has(normalized)) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
seen.add(normalized);
|
|
399
|
+
const files = voiceFilesInDirectory(resolvedExamples);
|
|
400
|
+
if (files.length > 0) {
|
|
401
|
+
candidates.push({ examples: candidate, resolvedExamples, files });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return candidates;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function voiceFilesInDirectory(rootDir) {
|
|
408
|
+
if (!fs.existsSync(rootDir)) {
|
|
409
|
+
return [];
|
|
410
|
+
}
|
|
411
|
+
const rootStats = fs.statSync(rootDir);
|
|
412
|
+
if (!rootStats.isDirectory()) {
|
|
413
|
+
return [];
|
|
414
|
+
}
|
|
415
|
+
const result = [];
|
|
416
|
+
const stack = [rootDir];
|
|
417
|
+
while (stack.length) {
|
|
418
|
+
const current = stack.pop();
|
|
419
|
+
const entries = fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
420
|
+
for (const entry of entries) {
|
|
421
|
+
const fullPath = path.join(current, entry.name);
|
|
422
|
+
if (entry.isDirectory()) {
|
|
423
|
+
if (!SKIP_SOURCE_DIRS.has(entry.name)) {
|
|
424
|
+
stack.push(fullPath);
|
|
425
|
+
}
|
|
426
|
+
} else if (entry.isFile() && VOICE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {
|
|
427
|
+
result.push(fullPath);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return result.sort((left, right) => left.localeCompare(right));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function directoryHasAnyFiles(rootDir) {
|
|
435
|
+
if (!fs.existsSync(rootDir)) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
const rootStats = fs.statSync(rootDir);
|
|
439
|
+
if (!rootStats.isDirectory()) {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
const stack = [rootDir];
|
|
443
|
+
while (stack.length) {
|
|
444
|
+
const current = stack.pop();
|
|
445
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
446
|
+
for (const entry of entries) {
|
|
447
|
+
const fullPath = path.join(current, entry.name);
|
|
448
|
+
if (entry.isDirectory()) {
|
|
449
|
+
if (!SKIP_SOURCE_DIRS.has(entry.name)) {
|
|
450
|
+
stack.push(fullPath);
|
|
451
|
+
}
|
|
452
|
+
} else if (entry.isFile()) {
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function writeEmptyCorpusNextSteps(io, examples, command) {
|
|
461
|
+
const target = displayPath(examples);
|
|
462
|
+
io.stdout.write([
|
|
463
|
+
"Dravoice did not learn a profile yet.",
|
|
464
|
+
`Add at least 3 representative long-form .md, .mdx, or .txt pieces to ${target}.`,
|
|
465
|
+
`For example, copy your existing writing into \`${copyDirectoryLabel(examples)}\`.`,
|
|
466
|
+
"Five to ten pieces is better when you have them.",
|
|
467
|
+
`Next: ${command}`,
|
|
468
|
+
"",
|
|
469
|
+
].join("\n"));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function writeUnsupportedCorpusNextSteps(io, examples, command) {
|
|
473
|
+
io.stdout.write([
|
|
474
|
+
"Dravoice did not learn a profile yet.",
|
|
475
|
+
"Supported extensions: .md, .mdx, .txt.",
|
|
476
|
+
`Convert or copy your writing into Markdown, MDX, or plain text in ${displayPath(examples)}.`,
|
|
477
|
+
`Next: ${command}`,
|
|
478
|
+
"",
|
|
479
|
+
].join("\n"));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function writeProfileSuccess(io, { action, profile, examples, out }) {
|
|
483
|
+
io.stdout.write(`${action}.\n`);
|
|
484
|
+
io.stdout.write(`Source: ${displayPath(examples)}\n`);
|
|
485
|
+
io.stdout.write(`Profile: ${displayChildPath(out, "profile.json")}\n`);
|
|
486
|
+
io.stdout.write("Config: .dravoice.yml\n");
|
|
487
|
+
io.stdout.write(`Documents: ${profile.source.documentCount}\n`);
|
|
488
|
+
io.stdout.write(`Words: ${profile.source.wordCount}\n`);
|
|
489
|
+
io.stdout.write(`Confidence: ${profile.source.confidence.band} - ${profile.source.confidence.message}\n`);
|
|
490
|
+
if (profile.source.documentCount < 3) {
|
|
491
|
+
const remaining = 3 - profile.source.documentCount;
|
|
492
|
+
io.stdout.write(`Warning: add at least ${remaining} more representative long-form piece(s) when you can.\n`);
|
|
493
|
+
}
|
|
494
|
+
io.stdout.write("Next: drav inspect\n");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function displayChildPath(base, child) {
|
|
498
|
+
const normalized = displayPath(base).replace(/\/+$/, "");
|
|
499
|
+
return `${normalized}/${child}`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function copyDirectoryLabel(value) {
|
|
503
|
+
const normalized = displayPath(value).replace(/\/+$/, "");
|
|
504
|
+
if (normalized === "./articles" || normalized === "articles") {
|
|
505
|
+
return "articles/";
|
|
506
|
+
}
|
|
507
|
+
return `${normalized}/`;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function parseArgs(args, allowedOptionNames, topic, booleanOptionNames = []) {
|
|
511
|
+
const allowed = new Set(allowedOptionNames);
|
|
512
|
+
const booleans = new Set(booleanOptionNames);
|
|
513
|
+
const options = {};
|
|
514
|
+
const positional = [];
|
|
515
|
+
let parseOptions = true;
|
|
516
|
+
|
|
517
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
518
|
+
const arg = args[index];
|
|
519
|
+
|
|
520
|
+
if (parseOptions && arg === "--") {
|
|
521
|
+
parseOptions = false;
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (!parseOptions || !arg.startsWith("--")) {
|
|
526
|
+
positional.push(arg);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const equalsIndex = arg.indexOf("=");
|
|
531
|
+
const key = equalsIndex === -1 ? arg.slice(2) : arg.slice(2, equalsIndex);
|
|
532
|
+
if (!allowed.has(key)) {
|
|
533
|
+
throw usageError(`Unknown option --${key}`, topic);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const inlineValue = equalsIndex === -1 ? null : arg.slice(equalsIndex + 1);
|
|
537
|
+
if (booleans.has(key)) {
|
|
538
|
+
if (inlineValue !== null) {
|
|
539
|
+
throw usageError(`Option --${key} does not take a value`, topic);
|
|
540
|
+
}
|
|
541
|
+
options[key] = true;
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const value = inlineValue ?? args[index + 1];
|
|
546
|
+
if (value === undefined || value === "" || (inlineValue === null && value.startsWith("--"))) {
|
|
547
|
+
throw usageError(`Missing value for --${key}`, topic);
|
|
548
|
+
}
|
|
549
|
+
options[key] = value;
|
|
550
|
+
if (inlineValue === null) {
|
|
551
|
+
index += 1;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return { options, positional };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function requiredOption(options, name, topic) {
|
|
558
|
+
if (!options[name]) {
|
|
559
|
+
throw usageError(`Missing required option --${name}`, topic);
|
|
560
|
+
}
|
|
561
|
+
return options[name];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function learnWithUsageExample(settings, topic) {
|
|
565
|
+
try {
|
|
566
|
+
return learnVoicePackV2(settings);
|
|
567
|
+
} catch (error) {
|
|
568
|
+
throw usageError(actionableCorpusError(error.message, topic, settings.examplesDir), topic);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function actionableCorpusError(message, topic, examplesDir) {
|
|
573
|
+
if (!/^No Markdown, MDX, or text examples found/.test(message)) {
|
|
574
|
+
return message;
|
|
575
|
+
}
|
|
576
|
+
const command = topic === "learn" ? "drav learn" : "drav init";
|
|
577
|
+
const elsewhereCommand = topic === "learn"
|
|
578
|
+
? "drav learn --examples ~/path/to/writing --out ./dravoice-voice"
|
|
579
|
+
: "drav init --examples ~/path/to/writing";
|
|
580
|
+
if (directoryHasAnyFiles(examplesDir)) {
|
|
581
|
+
return [
|
|
582
|
+
unsupportedExamplesMessage(examplesDir),
|
|
583
|
+
"",
|
|
584
|
+
"Supported extensions: .md, .mdx, .txt.",
|
|
585
|
+
"Convert or copy your writing into Markdown, MDX, or plain text.",
|
|
586
|
+
"",
|
|
587
|
+
"If your writing already lives somewhere else:",
|
|
588
|
+
` ${elsewhereCommand}`,
|
|
589
|
+
"",
|
|
590
|
+
`Next: ${command}`,
|
|
591
|
+
].join("\n");
|
|
592
|
+
}
|
|
593
|
+
return [
|
|
594
|
+
message,
|
|
595
|
+
"",
|
|
596
|
+
"Start here:",
|
|
597
|
+
" mkdir -p articles",
|
|
598
|
+
" # copy your existing writing into `articles/`",
|
|
599
|
+
` ${command}`,
|
|
600
|
+
"",
|
|
601
|
+
"If your writing already lives somewhere else:",
|
|
602
|
+
` ${elsewhereCommand}`,
|
|
603
|
+
"",
|
|
604
|
+
`Next: ${command}`,
|
|
605
|
+
].join("\n");
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function unsupportedExamplesMessage(examplesDir) {
|
|
609
|
+
return `Found files in ${displayPath(examplesDir)}, but none are supported voice examples.`;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function topicOption(options, positional, topic) {
|
|
613
|
+
if (positional.length > 1) {
|
|
614
|
+
throw usageError(`Unexpected positional argument: ${positional[1]}`, topic);
|
|
615
|
+
}
|
|
616
|
+
if (options.topic && positional[0]) {
|
|
617
|
+
throw usageError("Use either positional topic or --topic, not both", topic);
|
|
618
|
+
}
|
|
619
|
+
return options.topic ?? positional[0] ?? requiredOption(options, "topic", topic);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function formatOption(value, allowed, topic) {
|
|
623
|
+
const format = value ?? "text";
|
|
624
|
+
if (!allowed.includes(format)) {
|
|
625
|
+
throw usageError(`Unsupported --format ${format}. Expected ${allowed.join(" or ")}.`, topic);
|
|
626
|
+
}
|
|
627
|
+
return format;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function modeOption(value, topic) {
|
|
631
|
+
const mode = value ?? "balanced";
|
|
632
|
+
if (!["loose", "balanced", "strict"].includes(mode)) {
|
|
633
|
+
throw usageError(`Unsupported --mode ${mode}. Expected loose, balanced, or strict.`, topic);
|
|
634
|
+
}
|
|
635
|
+
return mode;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function rejectUnexpectedPositionals(positional, count, topic) {
|
|
639
|
+
if (positional.length > count) {
|
|
640
|
+
throw usageError(`Unexpected positional argument: ${positional[count]}`, topic);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function rejectPositionals(positional, topic) {
|
|
645
|
+
rejectUnexpectedPositionals(positional, 0, topic);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function resolvePath(cwd, value) {
|
|
649
|
+
return path.isAbsolute(value) ? value : path.join(cwd, value);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function resolveVoicePath(cwd, optionValue) {
|
|
653
|
+
if (optionValue) {
|
|
654
|
+
return resolvePath(cwd, optionValue);
|
|
655
|
+
}
|
|
656
|
+
const config = loadProjectConfig(cwd);
|
|
657
|
+
const configured = resolveConfiguredPath(cwd, config.voice);
|
|
658
|
+
if (configured) {
|
|
659
|
+
return configured;
|
|
660
|
+
}
|
|
661
|
+
const conventional = path.join(cwd, DEFAULT_VOICE_DIR);
|
|
662
|
+
if (fs.existsSync(path.join(conventional, "profile.json"))) {
|
|
663
|
+
return conventional;
|
|
664
|
+
}
|
|
665
|
+
return cwd;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function resolvePromptOptions(cwd, options) {
|
|
669
|
+
const config = loadProjectConfig(cwd);
|
|
670
|
+
return {
|
|
671
|
+
format: options.format ?? config.promptFormat ?? DEFAULT_PROMPT_FORMAT,
|
|
672
|
+
out: options.out ?? (options.voice ? undefined : config.promptOut) ?? undefined,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function isHelpFlag(value) {
|
|
677
|
+
return value === "--help" || value === "-h";
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function usageError(message, topic) {
|
|
681
|
+
return new Error(usageErrorMessage(message, topic));
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function usageErrorMessage(message, topic) {
|
|
685
|
+
const example = {
|
|
686
|
+
init: "drav init --examples ./articles --out ./dravoice-voice",
|
|
687
|
+
"review positional": "drav review draft.md",
|
|
688
|
+
}[topic] ?? EXAMPLES[topic];
|
|
689
|
+
return example ? `${message}\nExample: ${example}` : message;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function helpText() {
|
|
693
|
+
return [
|
|
694
|
+
"Dravoice - local-first voice guidance for writers",
|
|
695
|
+
"",
|
|
696
|
+
"First run:",
|
|
697
|
+
"1. Initialize your voice profile",
|
|
698
|
+
" drav init",
|
|
699
|
+
"2. Inspect the profile before trusting it",
|
|
700
|
+
" drav inspect",
|
|
701
|
+
"3. Generate reusable drafting guidance",
|
|
702
|
+
" drav prompt --out AGENTS.md",
|
|
703
|
+
"4. Plan a grounded draft from evidence",
|
|
704
|
+
" drav brief \"New topic\" --evidence notes.md --out brief.md",
|
|
705
|
+
"5. Revise, then review",
|
|
706
|
+
" drav revise-plan draft.md",
|
|
707
|
+
" drav review draft.md",
|
|
708
|
+
"",
|
|
709
|
+
"Commands:",
|
|
710
|
+
" init Learn a profile and save project defaults in one first-run command.",
|
|
711
|
+
" learn Build a local voice profile from Markdown, MDX, or text examples.",
|
|
712
|
+
" inspect Show the learned profile in plain language.",
|
|
713
|
+
" prompt Render reusable LLM drafting guidance.",
|
|
714
|
+
" brief Create an evidence-first article brief.",
|
|
715
|
+
" revise-plan Rank human-editable revision actions.",
|
|
716
|
+
" review Compare a draft against the voice profile.",
|
|
717
|
+
"",
|
|
718
|
+
"Advanced:",
|
|
719
|
+
" drav review draft.md --mode strict --format json",
|
|
720
|
+
" drav benchmark prepare --examples ./articles --topic \"New topic\" --out ./bench-run --seed 42",
|
|
721
|
+
"",
|
|
722
|
+
"Run `drav help init` for command-specific help.",
|
|
723
|
+
"",
|
|
724
|
+
].join("\n");
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function helpForTopic(topic) {
|
|
728
|
+
const normalized = String(topic ?? "").trim();
|
|
729
|
+
const text = HELP_TOPICS[normalized];
|
|
730
|
+
if (!text) {
|
|
731
|
+
throw usageError(`Unknown command: ${normalized}`, "help");
|
|
732
|
+
}
|
|
733
|
+
return text;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const EXAMPLES = {
|
|
737
|
+
help: "drav help init",
|
|
738
|
+
init: "drav init",
|
|
739
|
+
learn: "drav learn --examples ./articles --out ./dravoice-voice",
|
|
740
|
+
inspect: "drav inspect",
|
|
741
|
+
prompt: "drav prompt --out AGENTS.md",
|
|
742
|
+
brief: "drav brief \"New topic\" --evidence notes.md --out brief.md",
|
|
743
|
+
"revise-plan": "drav revise-plan draft.md",
|
|
744
|
+
review: "drav review draft.md --format json",
|
|
745
|
+
benchmark: "drav benchmark prepare --examples ./articles --topic \"New topic\" --out ./bench-run --seed 42",
|
|
746
|
+
"benchmark prepare": "drav benchmark prepare --examples ./articles --topic \"New topic\" --out ./bench-run --seed 42",
|
|
747
|
+
"benchmark score": "drav benchmark score --run ./bench-run --judge ./bench-run/judge/judgment.json",
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
const HELP_TOPICS = {
|
|
751
|
+
init: [
|
|
752
|
+
"Usage: drav init [--examples ./articles] [--out ./dravoice-voice] [--no-interactive]",
|
|
753
|
+
"",
|
|
754
|
+
"What it does:",
|
|
755
|
+
"Guides first-time setup, builds a voice profile, and writes .dravoice.yml project defaults.",
|
|
756
|
+
"",
|
|
757
|
+
"Options:",
|
|
758
|
+
" --examples <dir> Directory with representative long-form pieces. Defaults to ./articles.",
|
|
759
|
+
" --out <dir> Directory where Dravoice writes profile.json and metadata. Defaults to ./dravoice-voice.",
|
|
760
|
+
" --no-interactive Do not prompt when ./articles is missing.",
|
|
761
|
+
"",
|
|
762
|
+
"First run:",
|
|
763
|
+
" mkdir -p articles",
|
|
764
|
+
" # copy at least 3 .md, .mdx, or .txt pieces into articles/",
|
|
765
|
+
" drav init",
|
|
766
|
+
"",
|
|
767
|
+
"Interactive init previews supported files before learning. If ./articles",
|
|
768
|
+
"is missing, it can suggest ./content, ./posts, ./blog, ./essays, and other common folders.",
|
|
769
|
+
"",
|
|
770
|
+
`Example: ${EXAMPLES.init}`,
|
|
771
|
+
"Next: drav inspect",
|
|
772
|
+
"",
|
|
773
|
+
].join("\n"),
|
|
774
|
+
learn: [
|
|
775
|
+
"Usage: drav learn [--examples ./articles] [--out ./dravoice-voice]",
|
|
776
|
+
"",
|
|
777
|
+
"What it does:",
|
|
778
|
+
"Builds a local schemaVersion 2 voice profile from Markdown, MDX, and plain-text examples.",
|
|
779
|
+
"",
|
|
780
|
+
"Options:",
|
|
781
|
+
" --examples <dir> Directory with at least 3 representative long-form pieces.",
|
|
782
|
+
" --out <dir> Directory where Dravoice writes profile.json and metadata.",
|
|
783
|
+
"",
|
|
784
|
+
`Example: ${EXAMPLES.learn}`,
|
|
785
|
+
"Next: drav inspect",
|
|
786
|
+
"",
|
|
787
|
+
].join("\n"),
|
|
788
|
+
inspect: [
|
|
789
|
+
"Usage: drav inspect [--voice ./dravoice-voice]",
|
|
790
|
+
"",
|
|
791
|
+
"What it does:",
|
|
792
|
+
"Shows corpus confidence, feature families, revision handles, and drafting guidance in plain language.",
|
|
793
|
+
"",
|
|
794
|
+
"Options:",
|
|
795
|
+
" --voice <dir> Voice profile directory. Defaults to .dravoice.yml,",
|
|
796
|
+
" ./dravoice-voice, then current directory.",
|
|
797
|
+
"",
|
|
798
|
+
`Example: ${EXAMPLES.inspect}`,
|
|
799
|
+
"Next: drav prompt --out AGENTS.md",
|
|
800
|
+
"",
|
|
801
|
+
].join("\n"),
|
|
802
|
+
prompt: [
|
|
803
|
+
"Usage: drav prompt [--voice ./dravoice-voice] [--format agents] [--out AGENTS.md]",
|
|
804
|
+
"",
|
|
805
|
+
"What it does:",
|
|
806
|
+
"Turns high-confidence profile observations into reusable drafting guidance for an LLM or writing agent.",
|
|
807
|
+
"",
|
|
808
|
+
"Options:",
|
|
809
|
+
" --voice <dir> Voice profile directory. Defaults to .dravoice.yml,",
|
|
810
|
+
" ./dravoice-voice, then current directory.",
|
|
811
|
+
" --format <format> agents, claude, or system. Defaults to agents.",
|
|
812
|
+
" --out <file> Write guidance to a file instead of stdout.",
|
|
813
|
+
"",
|
|
814
|
+
`Example: ${EXAMPLES.prompt}`,
|
|
815
|
+
"Next: drav brief \"New topic\" --evidence notes.md --out brief.md",
|
|
816
|
+
"",
|
|
817
|
+
].join("\n"),
|
|
818
|
+
brief: [
|
|
819
|
+
"Usage: drav brief \"New topic\" [--voice ./dravoice-voice] [--evidence notes.md] [--out brief.md]",
|
|
820
|
+
"",
|
|
821
|
+
"What it does:",
|
|
822
|
+
"Creates an evidence-first article brief that keeps broad claims close to supplied notes, dates, quotes, and examples.",
|
|
823
|
+
"",
|
|
824
|
+
"Required input:",
|
|
825
|
+
" topic Target article topic, passed positionally or with --topic.",
|
|
826
|
+
"",
|
|
827
|
+
"Options:",
|
|
828
|
+
" --voice <dir> Voice profile directory. Defaults to .dravoice.yml,",
|
|
829
|
+
" ./dravoice-voice, then current directory.",
|
|
830
|
+
" --topic <text> Alternative to positional topic.",
|
|
831
|
+
" --evidence <file> Notes file used to extract concrete evidence anchors.",
|
|
832
|
+
" --format <format> text or json. Defaults to text.",
|
|
833
|
+
" --out <file> Write the brief to a file instead of stdout.",
|
|
834
|
+
"",
|
|
835
|
+
`Example: ${EXAMPLES.brief}`,
|
|
836
|
+
"Next: draft from the brief, then run drav revise-plan draft.md",
|
|
837
|
+
"",
|
|
838
|
+
].join("\n"),
|
|
839
|
+
"revise-plan": [
|
|
840
|
+
"Usage: drav revise-plan draft.md [--voice ./dravoice-voice]",
|
|
841
|
+
"",
|
|
842
|
+
"What it does:",
|
|
843
|
+
"Ranks calibrated, human-editable revision actions. It does not rewrite the draft or claim AI detection.",
|
|
844
|
+
"",
|
|
845
|
+
"Options:",
|
|
846
|
+
" --voice <dir> Voice profile directory. Defaults to .dravoice.yml,",
|
|
847
|
+
" ./dravoice-voice, then current directory.",
|
|
848
|
+
" --format <format> text or json. Defaults to text.",
|
|
849
|
+
"",
|
|
850
|
+
`Example: ${EXAMPLES["revise-plan"]}`,
|
|
851
|
+
"Next: drav review draft.md",
|
|
852
|
+
"",
|
|
853
|
+
].join("\n"),
|
|
854
|
+
review: [
|
|
855
|
+
"Usage: drav review draft.md [--voice ./dravoice-voice]",
|
|
856
|
+
"",
|
|
857
|
+
"What it does:",
|
|
858
|
+
"Compares a draft with the profile and reports family-level drift. It is revision guidance, not AI detection.",
|
|
859
|
+
"",
|
|
860
|
+
"Options:",
|
|
861
|
+
" --voice <dir> Voice profile directory. Defaults to .dravoice.yml,",
|
|
862
|
+
" ./dravoice-voice, then current directory.",
|
|
863
|
+
" --mode <mode> loose, balanced, or strict. Defaults to balanced.",
|
|
864
|
+
" --format <format> text or json. Defaults to text.",
|
|
865
|
+
"",
|
|
866
|
+
"Automation example:",
|
|
867
|
+
" drav review draft.md --voice ./dravoice-voice --mode strict --format json",
|
|
868
|
+
"",
|
|
869
|
+
`Example: ${EXAMPLES.review}`,
|
|
870
|
+
"Next: revise only the findings that fit the article's intent.",
|
|
871
|
+
"",
|
|
872
|
+
].join("\n"),
|
|
873
|
+
benchmark: [
|
|
874
|
+
"Usage: drav benchmark <prepare|score> ...",
|
|
875
|
+
"",
|
|
876
|
+
"What it does:",
|
|
877
|
+
"Runs validation workflows for Dravoice development. Most writers do not need this for first use.",
|
|
878
|
+
"",
|
|
879
|
+
"Examples:",
|
|
880
|
+
` ${EXAMPLES["benchmark prepare"]}`,
|
|
881
|
+
` ${EXAMPLES["benchmark score"]}`,
|
|
882
|
+
"",
|
|
883
|
+
].join("\n"),
|
|
884
|
+
"benchmark prepare": [
|
|
885
|
+
"Usage: drav benchmark prepare --examples ./articles --topic \"New topic\" --out ./bench-run --seed 42",
|
|
886
|
+
"",
|
|
887
|
+
"What it does:",
|
|
888
|
+
"Creates a local benchmark run directory from a source corpus and topic.",
|
|
889
|
+
"",
|
|
890
|
+
`Example: ${EXAMPLES["benchmark prepare"]}`,
|
|
891
|
+
"Next: fill benchmark drafts, then run drav benchmark score --run ./bench-run --judge ./bench-run/judge/judgment.json",
|
|
892
|
+
"",
|
|
893
|
+
].join("\n"),
|
|
894
|
+
"benchmark score": [
|
|
895
|
+
"Usage: drav benchmark score --run ./bench-run --judge ./bench-run/judge/judgment.json",
|
|
896
|
+
"",
|
|
897
|
+
"What it does:",
|
|
898
|
+
"Scores a prepared benchmark run using the V2 profile and review diagnostics.",
|
|
899
|
+
"",
|
|
900
|
+
`Example: ${EXAMPLES["benchmark score"]}`,
|
|
901
|
+
"",
|
|
902
|
+
].join("\n"),
|
|
903
|
+
};
|