@chenpengfei/daily-brief 0.1.1 → 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/CHANGELOG.md +43 -0
- package/README.md +1 -2
- package/dist/src/agent/daily-brief-agent.js +14 -0
- package/dist/src/agent/model-runtime-config.js +5 -77
- package/dist/src/cli.js +315 -252
- package/dist/src/config/credential-store.js +2 -14
- package/dist/src/config/model-config.js +8 -8
- package/dist/src/config/source-registry.js +30 -7
- package/dist/src/discord/delivery.js +4 -4
- package/dist/src/workflow/status.js +260 -21
- package/docs/operations.md +7 -17
- package/docs/user-manual.md +23 -25
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
3
4
|
import { stdin as processStdin, stdout as processStdout } from "node:process";
|
|
4
5
|
import { createInterface } from "node:readline/promises";
|
|
5
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { defaultModelConfig, dateFromDateKey, formatDateKey, formatSourceRegistry, loadSourceRegistry, putCredential, readDeliveryConfig, readConfiguredTimezone, getCredential, resolveDailyBriefPaths, setSourceEnabled, validateSourceRegistry, writeDeliveryConfig, writeCredentialStore, writeModelConfig } from "./config/index.js";
|
|
7
|
+
import { stringify } from "yaml";
|
|
8
|
+
import { loginModelCredential, readModelRuntimeConfig, runOnce, toApiKeyCredential } from "./agent/index.js";
|
|
9
|
+
import { defaultModelConfig, dateFromDateKey, formatDateKey, formatSourceRegistry, loadSourceRegistry, putCredential, readDeliveryConfig, readDailyBriefConfig, getCredential, resolveDailyBriefPaths, setSourceEnabled, validateSourceRegistry, writeDeliveryConfig, writeCredentialStore, writeModelConfig } from "./config/index.js";
|
|
10
10
|
import { getOperationalStatus } from "./workflow/index.js";
|
|
11
11
|
const consoleIo = {
|
|
12
|
+
interactive: Boolean(processStdin.isTTY && processStdout.isTTY),
|
|
12
13
|
stdout(line) {
|
|
13
14
|
console.log(line);
|
|
14
15
|
},
|
|
@@ -16,7 +17,10 @@ const consoleIo = {
|
|
|
16
17
|
console.error(line);
|
|
17
18
|
},
|
|
18
19
|
async prompt(message) {
|
|
19
|
-
|
|
20
|
+
if (!processStdin.isTTY || !processStdout.isTTY) {
|
|
21
|
+
throw new Error(nonInteractiveSetupMessage());
|
|
22
|
+
}
|
|
23
|
+
const reader = createInterface({ input: processStdin, output: processStdout, terminal: false });
|
|
20
24
|
try {
|
|
21
25
|
return await reader.question(`${message} `);
|
|
22
26
|
}
|
|
@@ -27,75 +31,138 @@ const consoleIo = {
|
|
|
27
31
|
};
|
|
28
32
|
export async function runCli(args, io = consoleIo, env = process.env) {
|
|
29
33
|
const [command, subcommand, value] = args;
|
|
30
|
-
const workflowFlags = parseFlags(args.slice(1));
|
|
31
|
-
const options = {
|
|
32
|
-
...optionsFromEnv(env),
|
|
33
|
-
...readWorkflowDateOption(workflowFlags, env)
|
|
34
|
-
};
|
|
35
34
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
36
35
|
printHelp(io);
|
|
37
36
|
return;
|
|
38
37
|
}
|
|
38
|
+
if (command === "version" || command === "--version" || command === "-v") {
|
|
39
|
+
io.stdout(await readPackageVersion());
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
39
42
|
if (command === "run-once") {
|
|
43
|
+
const workflowFlags = parseFlags(args.slice(1));
|
|
44
|
+
const options = {
|
|
45
|
+
...optionsFromEnv(env),
|
|
46
|
+
...readWorkflowDateOption(workflowFlags)
|
|
47
|
+
};
|
|
40
48
|
await assertWorkflowConfigured(options.sourceRegistryPath);
|
|
41
|
-
|
|
49
|
+
io.stdout("Daily Brief run started");
|
|
50
|
+
io.stdout(`Date: ${options.dateKey}`);
|
|
51
|
+
io.stdout(`Home: ${resolveDailyBriefPaths(env).home}`);
|
|
52
|
+
io.stdout("");
|
|
53
|
+
const result = await runOnce({
|
|
54
|
+
...options,
|
|
55
|
+
onProgress(line) {
|
|
56
|
+
io.stdout(line);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
42
59
|
if (result.coreFailure) {
|
|
43
60
|
throw new Error(`Core Workflow Failure: ${result.coreFailure.kind}\n${result.coreFailure.message}`);
|
|
44
61
|
}
|
|
62
|
+
io.stdout("");
|
|
45
63
|
io.stdout(`Daily Brief archived: ${result.archivePath}`);
|
|
46
64
|
io.stdout(`Sources read: ${result.sourceCount}`);
|
|
47
65
|
io.stdout(`Source Items read: ${result.sourceItemCount}`);
|
|
48
|
-
io.stdout(`
|
|
49
|
-
io.stdout(`
|
|
66
|
+
io.stdout(`Agent stages completed: ${countAgentStageEvents(result.piEvents)}/5`);
|
|
67
|
+
io.stdout(`Discord delivery: ${formatDeliveryStatus(result.delivery)}`);
|
|
68
|
+
io.stdout("5/5 Run completed");
|
|
50
69
|
return;
|
|
51
70
|
}
|
|
52
71
|
if (command === "setup") {
|
|
53
72
|
await handleSetupCommand(args.slice(1), io, env);
|
|
54
73
|
return;
|
|
55
74
|
}
|
|
56
|
-
if (command === "collect") {
|
|
57
|
-
await assertWorkflowConfigured(options.sourceRegistryPath);
|
|
58
|
-
const result = await collectSources(options);
|
|
59
|
-
io.stdout(`Source Item Store: ${result.storePath || "(no items written)"}`);
|
|
60
|
-
for (const source of result.sources) {
|
|
61
|
-
io.stdout(`${source.status.padEnd(7)} ${source.sourceId} items=${source.itemCount} written=${source.writtenCount} duplicates=${source.skippedDuplicateCount}${source.reason ? ` reason=${source.reason}` : ""}`);
|
|
62
|
-
}
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
if (command === "generate") {
|
|
66
|
-
const result = await generateOnce(options);
|
|
67
|
-
io.stdout(`Daily Brief archived: ${result.archivePath}`);
|
|
68
|
-
io.stdout(`Source Items read: ${result.sourceItemCount}`);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
if (command === "deliver") {
|
|
72
|
-
const result = await deliverOnce(options);
|
|
73
|
-
io.stdout(`Discord delivery: ${result.status}${"reason" in result ? ` (${result.reason})` : ""}`);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
75
|
if (command === "status") {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
for (const failure of status.materialPartialFailures) {
|
|
80
|
-
io.stdout(`- ${failure}`);
|
|
76
|
+
if (args.length > 1) {
|
|
77
|
+
throw new Error("daily-brief status does not accept flags.");
|
|
81
78
|
}
|
|
79
|
+
const options = optionsFromEnv(env);
|
|
80
|
+
const status = await getOperationalStatus(options);
|
|
81
|
+
printOperationalStatus(status, io);
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
84
|
if (command === "sources") {
|
|
85
|
+
const options = optionsFromEnv(env);
|
|
85
86
|
await handleSourcesCommand(subcommand, value, io, options.sourceRegistryPath);
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
throw new Error(`Unknown command: ${command}`);
|
|
90
|
+
}
|
|
91
|
+
async function readPackageVersion() {
|
|
92
|
+
let directory = dirname(fileURLToPath(import.meta.url));
|
|
93
|
+
for (let index = 0; index < 8; index += 1) {
|
|
94
|
+
const path = join(directory, "package.json");
|
|
95
|
+
try {
|
|
96
|
+
const packageJson = JSON.parse(await readFile(path, "utf8"));
|
|
97
|
+
if (isRecord(packageJson) && typeof packageJson.version === "string") {
|
|
98
|
+
return `daily-brief ${packageJson.version}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (!(isNodeError(error) && error.code === "ENOENT")) {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const parent = dirname(directory);
|
|
107
|
+
if (parent === directory) {
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
directory = parent;
|
|
91
111
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
112
|
+
return "daily-brief unknown";
|
|
113
|
+
}
|
|
114
|
+
function countAgentStageEvents(events) {
|
|
115
|
+
return events.length > 0 ? 5 : 0;
|
|
116
|
+
}
|
|
117
|
+
function formatDeliveryStatus(delivery) {
|
|
118
|
+
return `${delivery.status}${delivery.reason ? ` (${delivery.reason})` : ""}`;
|
|
119
|
+
}
|
|
120
|
+
function printOperationalStatus(status, io) {
|
|
121
|
+
io.stdout("Daily Brief status");
|
|
122
|
+
io.stdout(`Health: ${status.health} - ${status.message}`);
|
|
123
|
+
io.stdout(`Date: ${status.dateKey}`);
|
|
124
|
+
io.stdout(`System timezone: ${status.systemTimezone}`);
|
|
125
|
+
io.stdout(`Home: ${status.paths.home}`);
|
|
126
|
+
io.stdout(`Data: ${status.paths.dataHome}`);
|
|
127
|
+
io.stdout("");
|
|
128
|
+
io.stdout("Setup readiness");
|
|
129
|
+
io.stdout(formatStatusCheck("Config", status.setup.config));
|
|
130
|
+
io.stdout(formatSourceRegistryCheck(status.setup.sourceRegistry));
|
|
131
|
+
io.stdout(formatModelCheck(status.setup.model));
|
|
132
|
+
io.stdout(formatStatusCheck("Delivery", status.setup.delivery));
|
|
133
|
+
io.stdout(formatStatusCheck("Data", status.setup.data));
|
|
134
|
+
io.stdout("");
|
|
135
|
+
io.stdout("Today run state");
|
|
136
|
+
io.stdout(formatStatusCheck("Source Items", status.today.sourceItems));
|
|
137
|
+
io.stdout(formatStatusCheck("Brief Archive", status.today.briefArchive));
|
|
138
|
+
io.stdout(formatStatusCheck("Agent Run Artifacts", status.today.agentRunArtifacts));
|
|
139
|
+
io.stdout("");
|
|
140
|
+
io.stdout(`Next: ${status.nextAction}`);
|
|
141
|
+
for (const failure of status.materialPartialFailures) {
|
|
142
|
+
io.stdout(`- ${failure}`);
|
|
95
143
|
}
|
|
96
|
-
throw new Error(`Unknown command: ${command}`);
|
|
97
144
|
}
|
|
98
|
-
function
|
|
145
|
+
function formatStatusCheck(label, check) {
|
|
146
|
+
const detail = check.detail ? ` - ${check.detail}` : "";
|
|
147
|
+
const path = check.path ? ` (${check.path})` : "";
|
|
148
|
+
return `- ${label}: ${check.state} - ${check.label}${detail}${path}`;
|
|
149
|
+
}
|
|
150
|
+
function formatSourceRegistryCheck(check) {
|
|
151
|
+
const counts = typeof check.enabledCount === "number" && typeof check.totalCount === "number"
|
|
152
|
+
? ` - ${check.enabledCount}/${check.totalCount} enabled`
|
|
153
|
+
: "";
|
|
154
|
+
const detail = check.detail && !counts.includes(check.detail) ? ` - ${check.detail}` : "";
|
|
155
|
+
const path = check.path ? ` (${check.path})` : "";
|
|
156
|
+
return `- Source Registry: ${check.state} - ${check.label}${counts}${detail}${path}`;
|
|
157
|
+
}
|
|
158
|
+
function formatModelCheck(check) {
|
|
159
|
+
const selected = check.provider && check.model ? ` - ${check.provider}/${check.model}` : "";
|
|
160
|
+
const credential = check.credentialRef ? ` - credential ${check.credentialRef}` : "";
|
|
161
|
+
const detail = check.detail ? ` - ${check.detail}` : "";
|
|
162
|
+
const path = check.path ? ` (${check.path})` : "";
|
|
163
|
+
return `- Model: ${check.state} - ${check.label}${selected}${credential}${detail}${path}`;
|
|
164
|
+
}
|
|
165
|
+
function readWorkflowDateOption(flags) {
|
|
99
166
|
if (flags.date) {
|
|
100
167
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(flags.date)) {
|
|
101
168
|
throw new Error("--date must use YYYY-MM-DD");
|
|
@@ -103,8 +170,7 @@ function readWorkflowDateOption(flags, env) {
|
|
|
103
170
|
return { date: dateFromDateKey(flags.date), dateKey: flags.date };
|
|
104
171
|
}
|
|
105
172
|
const now = new Date();
|
|
106
|
-
const
|
|
107
|
-
const timezone = readConfiguredTimezone(paths.configPath) ?? env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
173
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
108
174
|
return { date: now, dateKey: formatDateKey(now, timezone) };
|
|
109
175
|
}
|
|
110
176
|
async function assertWorkflowConfigured(sourceRegistryPath) {
|
|
@@ -119,8 +185,15 @@ async function handleSourcesCommand(subcommand, value, io, sourceRegistryPath) {
|
|
|
119
185
|
}
|
|
120
186
|
if (subcommand === "edit") {
|
|
121
187
|
const path = sourceRegistryPath ?? resolveDailyBriefPaths().sourceRegistryPath;
|
|
122
|
-
io.stdout(
|
|
123
|
-
io.stdout(
|
|
188
|
+
io.stdout("Source Registry:");
|
|
189
|
+
io.stdout(` ${path}`);
|
|
190
|
+
io.stdout("");
|
|
191
|
+
io.stdout("Edit this YAML file to add, remove, or update Sources.");
|
|
192
|
+
io.stdout("Use the id field as SOURCE ID for enable/disable commands.");
|
|
193
|
+
io.stdout("");
|
|
194
|
+
io.stdout("After editing:");
|
|
195
|
+
io.stdout(" daily-brief sources validate");
|
|
196
|
+
io.stdout(" daily-brief sources list");
|
|
124
197
|
return;
|
|
125
198
|
}
|
|
126
199
|
if (subcommand === "validate") {
|
|
@@ -138,10 +211,19 @@ async function handleSourcesCommand(subcommand, value, io, sourceRegistryPath) {
|
|
|
138
211
|
}
|
|
139
212
|
if (subcommand === "enable" || subcommand === "disable") {
|
|
140
213
|
if (!value) {
|
|
141
|
-
throw new Error(`sources ${subcommand} requires a
|
|
214
|
+
throw new Error(`sources ${subcommand} requires a SOURCE ID. Run daily-brief sources list to see available SOURCE ID values.`);
|
|
142
215
|
}
|
|
143
216
|
const enabled = subcommand === "enable";
|
|
144
|
-
|
|
217
|
+
try {
|
|
218
|
+
await setSourceEnabled(value, enabled, sourceRegistryPath);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
222
|
+
if (message === `Source not found: ${value}`) {
|
|
223
|
+
throw new Error(`Source not found: ${value}. Run daily-brief sources list to see available SOURCE ID values.`);
|
|
224
|
+
}
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
145
227
|
io.stdout(`${enabled ? "Enabled" : "Disabled"} Source: ${value}`);
|
|
146
228
|
return;
|
|
147
229
|
}
|
|
@@ -152,117 +234,207 @@ function printHelp(io) {
|
|
|
152
234
|
"Daily Brief Operational CLI",
|
|
153
235
|
"",
|
|
154
236
|
"Usage:",
|
|
155
|
-
" daily-brief setup
|
|
156
|
-
" daily-brief run-once",
|
|
157
|
-
" daily-brief collect",
|
|
158
|
-
" daily-brief generate",
|
|
159
|
-
" daily-brief deliver",
|
|
237
|
+
" daily-brief setup",
|
|
238
|
+
" daily-brief run-once [--date YYYY-MM-DD]",
|
|
160
239
|
" daily-brief status",
|
|
161
|
-
" daily-brief model configure [--provider <provider> --model <model> --credential-ref <ref>]",
|
|
162
|
-
" daily-brief model login [--provider openai-codex --credential-ref <ref>]",
|
|
163
|
-
" daily-brief model logout [--credential-ref <ref>]",
|
|
164
|
-
" daily-brief model status",
|
|
165
|
-
" daily-brief delivery configure --enabled true|false [--webhook-ref <ref> --webhook-url <url>]",
|
|
166
|
-
" daily-brief delivery status",
|
|
167
|
-
" daily-brief delivery test",
|
|
168
240
|
" daily-brief sources list",
|
|
169
241
|
" daily-brief sources edit",
|
|
170
242
|
" daily-brief sources validate",
|
|
171
243
|
" daily-brief sources enable <source-id>",
|
|
172
|
-
" daily-brief sources disable <source-id>"
|
|
244
|
+
" daily-brief sources disable <source-id>",
|
|
245
|
+
" daily-brief version"
|
|
173
246
|
].join("\n"));
|
|
174
247
|
}
|
|
175
248
|
async function handleSetupCommand(args, io, env) {
|
|
176
249
|
const flags = parseFlags(args);
|
|
177
|
-
|
|
250
|
+
if (Object.keys(flags).length > 0 || args.some((arg) => !arg.startsWith("--"))) {
|
|
251
|
+
throw new Error("daily-brief setup does not accept flags. Re-run setup and choose what to preserve or update interactively.");
|
|
252
|
+
}
|
|
253
|
+
requireInteractiveSetup(io);
|
|
178
254
|
const paths = resolveDailyBriefPaths(env);
|
|
179
|
-
const
|
|
255
|
+
const existingConfig = readDailyBriefConfig(paths.configPath);
|
|
180
256
|
await mkdir(paths.home, { recursive: true });
|
|
181
257
|
await mkdir(paths.sourceItemRoot, { recursive: true });
|
|
182
258
|
await mkdir(paths.agentRunRoot, { recursive: true });
|
|
183
259
|
await mkdir(paths.briefArchiveRoot, { recursive: true });
|
|
184
|
-
await
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
"model:",
|
|
190
|
-
" provider: openai-codex",
|
|
191
|
-
" model: gpt-5.5",
|
|
192
|
-
" credentialRef: openai-codex.default",
|
|
193
|
-
"delivery:",
|
|
194
|
-
" enabled: false",
|
|
195
|
-
""
|
|
196
|
-
].join("\n"), force);
|
|
197
|
-
await writeIfNeeded(paths.sourceRegistryPath, defaultSourceRegistryExample(), force);
|
|
198
|
-
if (force || !(await exists(paths.authPath))) {
|
|
260
|
+
await writeSetupBaseConfig(paths.configPath, {
|
|
261
|
+
...existingConfig,
|
|
262
|
+
brief: normalizeBriefConfig(existingConfig.brief)
|
|
263
|
+
});
|
|
264
|
+
if (!(await exists(paths.authPath))) {
|
|
199
265
|
writeCredentialStore({ credentials: {} }, paths.authPath);
|
|
200
266
|
}
|
|
267
|
+
if (await exists(paths.sourceRegistryPath)) {
|
|
268
|
+
io.stdout(`Source Registry: ${paths.sourceRegistryPath}`);
|
|
269
|
+
try {
|
|
270
|
+
io.stdout(formatSourceRegistry(await loadSourceRegistry(paths.sourceRegistryPath)));
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
274
|
+
io.stdout(`Source Registry invalid:\n${message}`);
|
|
275
|
+
}
|
|
276
|
+
const reinitialize = await promptYesNo(io, "Reinitialize example Sources? Existing sources.yaml will be overwritten.", false);
|
|
277
|
+
if (reinitialize) {
|
|
278
|
+
await writeFile(paths.sourceRegistryPath, defaultSourceRegistryExample(), "utf8");
|
|
279
|
+
io.stdout("Source Registry reinitialized from the packaged example.");
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
io.stdout("Source Registry preserved.");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
await writeFile(paths.sourceRegistryPath, defaultSourceRegistryExample(), "utf8");
|
|
287
|
+
io.stdout(`Source Registry initialized: ${paths.sourceRegistryPath}`);
|
|
288
|
+
io.stdout(formatSourceRegistry(await loadSourceRegistry(paths.sourceRegistryPath)));
|
|
289
|
+
}
|
|
290
|
+
io.stdout("To edit Sources:");
|
|
291
|
+
io.stdout(" daily-brief sources edit");
|
|
292
|
+
io.stdout(" daily-brief sources validate");
|
|
293
|
+
io.stdout(" daily-brief sources list");
|
|
294
|
+
await configureModelThroughSetup(io, env);
|
|
295
|
+
await configureDeliveryThroughSetup(io, env);
|
|
296
|
+
const readiness = readModelRuntimeConfig(env);
|
|
297
|
+
const delivery = readDeliveryConfig(paths.configPath);
|
|
201
298
|
io.stdout(`Daily Brief home: ${paths.home}`);
|
|
202
299
|
io.stdout(`Daily Brief data: ${paths.dataHome}`);
|
|
203
|
-
io.stdout(`Timezone: ${timezone}`);
|
|
204
300
|
io.stdout(`Source Registry: ${paths.sourceRegistryPath}`);
|
|
205
|
-
io.stdout(
|
|
206
|
-
io.stdout(
|
|
207
|
-
io.stdout(
|
|
208
|
-
io.stdout("
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
301
|
+
io.stdout(`Model: ${readiness.provider}/${readiness.model}`);
|
|
302
|
+
io.stdout(`Model credential: ${readiness.ready ? "configured" : "missing"}`);
|
|
303
|
+
io.stdout(`Discord delivery: ${delivery?.enabled ? "enabled" : "disabled"}`);
|
|
304
|
+
io.stdout("Ready to run:");
|
|
305
|
+
io.stdout(" daily-brief run-once");
|
|
306
|
+
}
|
|
307
|
+
function requireInteractiveSetup(io) {
|
|
308
|
+
if (!io.prompt || io.interactive === false) {
|
|
309
|
+
throw new Error(nonInteractiveSetupMessage());
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function nonInteractiveSetupMessage() {
|
|
313
|
+
return [
|
|
314
|
+
"daily-brief setup requires an interactive terminal.",
|
|
315
|
+
"For CI or scripted setup, create ~/.daily-brief/config.yaml, ~/.daily-brief/sources.yaml, and ~/.daily-brief/auth.json directly.",
|
|
316
|
+
"Use DAILY_BRIEF_HOME to choose a configuration root and DAILY_BRIEF_DATA_HOME to choose a generated-data root.",
|
|
317
|
+
"Run daily-brief sources validate after writing sources.yaml."
|
|
318
|
+
].join("\n");
|
|
319
|
+
}
|
|
320
|
+
async function writeSetupBaseConfig(path, config) {
|
|
321
|
+
await mkdir(dirname(path), { recursive: true });
|
|
322
|
+
await writeFile(path, stringify(config), "utf8");
|
|
323
|
+
}
|
|
324
|
+
function normalizeBriefConfig(value) {
|
|
325
|
+
const current = isRecord(value) ? value : {};
|
|
326
|
+
return {
|
|
327
|
+
...current,
|
|
328
|
+
language: "zh",
|
|
329
|
+
maxSignals: typeof current.maxSignals === "number" ? current.maxSignals : 5
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
async function configureModelThroughSetup(io, env) {
|
|
213
333
|
const paths = resolveDailyBriefPaths(env);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
334
|
+
const current = readDailyBriefConfig(paths.configPath).model ?? defaultModelConfig();
|
|
335
|
+
const provider = readProviderFlag(await promptWithDefault(io, "LLM Provider (openai-codex/openai/deepseek/openai-compatible)", current.provider));
|
|
336
|
+
const model = await promptWithDefault(io, "Model", current.provider === provider ? current.model : defaultModelForProvider(provider));
|
|
337
|
+
io.stdout("Credential name identifies where Daily Brief finds the model secret in auth.json. Use the default unless you manage multiple credentials.");
|
|
338
|
+
const credentialRef = await promptWithDefault(io, "Model credential name", current.provider === provider ? current.credentialRef ?? defaultCredentialRef(provider) ?? "" : defaultCredentialRef(provider) ?? "");
|
|
339
|
+
const baseUrl = provider === "openai-compatible"
|
|
340
|
+
? await promptWithDefault(io, "Base URL", current.provider === provider ? current.baseUrl ?? "" : "")
|
|
341
|
+
: undefined;
|
|
342
|
+
const config = {
|
|
343
|
+
provider,
|
|
344
|
+
model,
|
|
345
|
+
...(credentialRef ? { credentialRef } : {}),
|
|
346
|
+
...(baseUrl ? { baseUrl } : {})
|
|
347
|
+
};
|
|
348
|
+
writeModelConfig(config, paths.configPath);
|
|
349
|
+
io.stdout(`Model configured: ${provider}/${model}`);
|
|
350
|
+
if (credentialRef) {
|
|
351
|
+
io.stdout(`Model credential name: ${credentialRef}`);
|
|
352
|
+
}
|
|
353
|
+
await maybeConfigureModelCredential({ provider, credentialRef, io, env });
|
|
354
|
+
}
|
|
355
|
+
async function maybeConfigureModelCredential(input) {
|
|
356
|
+
if (!input.credentialRef) {
|
|
224
357
|
return;
|
|
225
358
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
359
|
+
const paths = resolveDailyBriefPaths(input.env);
|
|
360
|
+
if (input.provider === "openai-codex") {
|
|
361
|
+
const credential = getCredential(input.credentialRef, paths.authPath);
|
|
362
|
+
if (credential) {
|
|
363
|
+
input.io.stdout("Model credential: configured");
|
|
230
364
|
return;
|
|
231
365
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const credential = config?.webhookRef ? getDeliveryWebhookCredential(config.webhookRef, paths.authPath) : undefined;
|
|
241
|
-
if (!config?.enabled || !credential) {
|
|
242
|
-
throw new Error("delivery test requires enabled Discord delivery with a webhook credential");
|
|
366
|
+
if (await promptYesNo(input.io, "Login to openai-codex now?", true)) {
|
|
367
|
+
await loginModelCredential({
|
|
368
|
+
provider: input.provider,
|
|
369
|
+
credentialRef: input.credentialRef,
|
|
370
|
+
io: input.io,
|
|
371
|
+
env: input.env
|
|
372
|
+
});
|
|
373
|
+
input.io.stdout(`Logged in credential: ${input.credentialRef}`);
|
|
243
374
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
headers: { "content-type": "application/json" },
|
|
247
|
-
body: JSON.stringify({ content: "Daily Brief delivery test" })
|
|
248
|
-
});
|
|
249
|
-
if (!response.ok) {
|
|
250
|
-
throw new Error(`Discord delivery test failed with status ${response.status}`);
|
|
375
|
+
else {
|
|
376
|
+
input.io.stdout("Model credential: missing");
|
|
251
377
|
}
|
|
252
|
-
io.stdout("Discord delivery test: sent");
|
|
253
378
|
return;
|
|
254
379
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
380
|
+
if (getCredential(input.credentialRef, paths.authPath)) {
|
|
381
|
+
input.io.stdout("Model credential: configured");
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (await promptYesNo(input.io, "Store API key in credential store? API key input may be visible in this terminal.", false)) {
|
|
385
|
+
const apiKey = await promptWithDefault(input.io, "API key", "");
|
|
386
|
+
if (apiKey) {
|
|
387
|
+
putCredential(input.credentialRef, toApiKeyCredential(input.provider, apiKey), paths.authPath);
|
|
388
|
+
input.io.stdout(`Stored credential: ${input.credentialRef}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
input.io.stdout("Model credential: missing");
|
|
393
|
+
}
|
|
260
394
|
}
|
|
261
|
-
async function
|
|
262
|
-
|
|
395
|
+
async function configureDeliveryThroughSetup(io, env) {
|
|
396
|
+
const paths = resolveDailyBriefPaths(env);
|
|
397
|
+
const current = readDeliveryConfig(paths.configPath);
|
|
398
|
+
const enabled = await promptYesNo(io, "Enable Discord delivery?", current?.enabled ?? false);
|
|
399
|
+
if (!enabled) {
|
|
400
|
+
writeDeliveryConfig({ enabled: false }, paths.configPath);
|
|
401
|
+
io.stdout("Discord delivery: disabled");
|
|
263
402
|
return;
|
|
264
403
|
}
|
|
265
|
-
await
|
|
404
|
+
const webhookRef = await promptWithDefault(io, "Discord webhook credential name", current?.webhookRef ?? "discord.default");
|
|
405
|
+
writeDeliveryConfig({ enabled: true, webhookRef }, paths.configPath);
|
|
406
|
+
const credential = getCredential(webhookRef, paths.authPath);
|
|
407
|
+
if (credential) {
|
|
408
|
+
io.stdout("Discord webhook: configured");
|
|
409
|
+
if (!(await promptYesNo(io, "Replace Discord webhook URL?", false))) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const webhookUrl = await promptWithDefault(io, "Discord webhook URL", "");
|
|
414
|
+
if (webhookUrl) {
|
|
415
|
+
putCredential(webhookRef, { type: "webhook", provider: "discord", webhookUrl }, paths.authPath);
|
|
416
|
+
io.stdout(`Discord webhook stored: ${webhookRef}`);
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
io.stdout("Discord webhook: missing");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async function promptYesNo(io, label, defaultValue) {
|
|
423
|
+
if (!io.prompt) {
|
|
424
|
+
throw new Error(nonInteractiveSetupMessage());
|
|
425
|
+
}
|
|
426
|
+
const suffix = defaultValue ? "[Y/n]" : "[y/N]";
|
|
427
|
+
const answer = (await io.prompt(`${label} ${suffix}:`)).trim().toLowerCase();
|
|
428
|
+
if (!answer) {
|
|
429
|
+
return defaultValue;
|
|
430
|
+
}
|
|
431
|
+
if (answer === "y" || answer === "yes" || answer === "true") {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
if (answer === "n" || answer === "no" || answer === "false") {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
throw new Error(`${label} requires y/yes or n/no`);
|
|
266
438
|
}
|
|
267
439
|
async function exists(path) {
|
|
268
440
|
try {
|
|
@@ -293,71 +465,6 @@ function defaultSourceRegistryExample() {
|
|
|
293
465
|
""
|
|
294
466
|
].join("\n");
|
|
295
467
|
}
|
|
296
|
-
async function handleModelCommand(args, io, env) {
|
|
297
|
-
const [subcommand, ...rest] = args;
|
|
298
|
-
const flags = parseFlags(rest);
|
|
299
|
-
if (subcommand === "configure") {
|
|
300
|
-
await configureModel(flags, io, env);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
if (subcommand === "status") {
|
|
304
|
-
const config = readModelRuntimeConfig(env);
|
|
305
|
-
io.stdout(`Provider: ${config.provider}`);
|
|
306
|
-
io.stdout(`Model: ${config.model}`);
|
|
307
|
-
io.stdout(`Credential Ref: ${config.credentialRef ?? "(none)"}`);
|
|
308
|
-
io.stdout(`Ready: ${config.ready ? "yes" : "no"}`);
|
|
309
|
-
for (const issue of config.issues) {
|
|
310
|
-
io.stdout(`- ${issue}`);
|
|
311
|
-
}
|
|
312
|
-
for (const credential of statusModelCredentials(env)) {
|
|
313
|
-
io.stdout(`Credential: ${credential.ref} provider=${credential.provider} type=${credential.type} secret=${credential.secret}`);
|
|
314
|
-
}
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
if (subcommand === "login") {
|
|
318
|
-
const config = readModelRuntimeConfig(env);
|
|
319
|
-
const provider = readProviderFlag(flags.provider ?? config.provider ?? defaultModelConfig().provider);
|
|
320
|
-
const credentialRef = flags["credential-ref"] ?? config.credentialRef ?? defaultCredentialRef(provider);
|
|
321
|
-
if (!credentialRef) {
|
|
322
|
-
throw new Error("model login requires --credential-ref");
|
|
323
|
-
}
|
|
324
|
-
await loginModelCredential({ provider, credentialRef, io, env });
|
|
325
|
-
io.stdout(`Logged in credential: ${credentialRef}`);
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
if (subcommand === "logout") {
|
|
329
|
-
const config = readModelRuntimeConfig(env);
|
|
330
|
-
const credentialRef = flags["credential-ref"] ?? rest[0] ?? config.credentialRef;
|
|
331
|
-
if (!credentialRef) {
|
|
332
|
-
throw new Error("model logout requires --credential-ref");
|
|
333
|
-
}
|
|
334
|
-
logoutModelCredential(credentialRef, env);
|
|
335
|
-
io.stdout(`Logged out credential: ${credentialRef}`);
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
throw new Error(`Unknown model command: ${subcommand ?? "(missing)"}`);
|
|
339
|
-
}
|
|
340
|
-
async function configureModel(flags, io, env) {
|
|
341
|
-
const interactive = Object.keys(flags).length === 0;
|
|
342
|
-
const provider = readProviderFlag(flags.provider ??
|
|
343
|
-
(interactive ? await promptWithDefault(io, "Provider", defaultModelConfig().provider) : missingFlag("provider")));
|
|
344
|
-
const model = flags.model ?? (interactive ? await promptWithDefault(io, "Model", defaultModelForProvider(provider)) : missingFlag("model"));
|
|
345
|
-
const credentialRef = flags["credential-ref"] ??
|
|
346
|
-
(interactive ? await promptWithDefault(io, "Credential ref", defaultCredentialRef(provider) ?? "") : missingFlag("credential-ref"));
|
|
347
|
-
const baseUrl = flags["base-url"] ?? (provider === "openai-compatible" && interactive ? await promptWithDefault(io, "Base URL", "") : undefined);
|
|
348
|
-
const config = {
|
|
349
|
-
provider,
|
|
350
|
-
model,
|
|
351
|
-
credentialRef,
|
|
352
|
-
...(baseUrl ? { baseUrl } : {})
|
|
353
|
-
};
|
|
354
|
-
writeModelConfig(config, resolveDailyBriefPaths(env).configPath);
|
|
355
|
-
if (flags["api-key"]) {
|
|
356
|
-
putCredential(credentialRef, toApiKeyCredential(provider, flags["api-key"]), resolveDailyBriefPaths(env).authPath);
|
|
357
|
-
}
|
|
358
|
-
io.stdout(`Model configured: ${provider}/${model}`);
|
|
359
|
-
io.stdout(`Credential Ref: ${credentialRef}`);
|
|
360
|
-
}
|
|
361
468
|
function parseFlags(args) {
|
|
362
469
|
const flags = {};
|
|
363
470
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -415,87 +522,44 @@ function defaultCredentialRef(provider) {
|
|
|
415
522
|
return undefined;
|
|
416
523
|
}
|
|
417
524
|
if (provider === "openai") {
|
|
418
|
-
return "
|
|
525
|
+
return "openai.default";
|
|
419
526
|
}
|
|
420
527
|
if (provider === "deepseek") {
|
|
421
|
-
return "
|
|
528
|
+
return "deepseek.default";
|
|
422
529
|
}
|
|
423
530
|
if (provider === "openai-compatible") {
|
|
424
|
-
return "
|
|
531
|
+
return "openai-compatible.default";
|
|
425
532
|
}
|
|
426
533
|
return defaultModelConfig().credentialRef;
|
|
427
534
|
}
|
|
428
535
|
async function promptWithDefault(io, label, defaultValue) {
|
|
429
536
|
if (!io.prompt) {
|
|
430
|
-
throw new Error(
|
|
537
|
+
throw new Error(nonInteractiveSetupMessage());
|
|
431
538
|
}
|
|
432
539
|
const answer = await io.prompt(`${label}${defaultValue ? ` (${defaultValue})` : ""}:`);
|
|
433
540
|
return answer.trim() || defaultValue;
|
|
434
541
|
}
|
|
435
|
-
function missingFlag(name) {
|
|
436
|
-
throw new Error(`model configure requires --${name}`);
|
|
437
|
-
}
|
|
438
542
|
function optionsFromEnv(env) {
|
|
439
543
|
const paths = resolveDailyBriefPaths(env);
|
|
440
|
-
const discordWebhookUrl = resolveConfiguredWebhookUrl(env);
|
|
441
544
|
return {
|
|
545
|
+
env,
|
|
546
|
+
dataHome: paths.dataHome,
|
|
547
|
+
configPath: paths.configPath,
|
|
548
|
+
authPath: paths.authPath,
|
|
442
549
|
sourceRegistryPath: paths.sourceRegistryPath,
|
|
443
550
|
sourceItemRoot: paths.sourceItemRoot,
|
|
444
551
|
agentRunRoot: paths.agentRunRoot,
|
|
445
552
|
archiveRoot: paths.briefArchiveRoot,
|
|
446
553
|
modelRuntimeEnv: env,
|
|
447
|
-
|
|
448
|
-
...(env.DAILY_BRIEF_DISCORD_TEMPLATE_PATH ? { discordTemplatePath: env.DAILY_BRIEF_DISCORD_TEMPLATE_PATH } : {})
|
|
554
|
+
discordEnv: env
|
|
449
555
|
};
|
|
450
556
|
}
|
|
451
|
-
export async function loadDotenvFile(path = ".env", env = process.env) {
|
|
452
|
-
let contents;
|
|
453
|
-
try {
|
|
454
|
-
contents = await readFile(path, "utf8");
|
|
455
|
-
}
|
|
456
|
-
catch (error) {
|
|
457
|
-
if (isNodeError(error) && error.code === "ENOENT") {
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
throw error;
|
|
461
|
-
}
|
|
462
|
-
for (const line of contents.split("\n")) {
|
|
463
|
-
const entry = parseDotenvLine(line);
|
|
464
|
-
if (!entry || env[entry.key] !== undefined) {
|
|
465
|
-
continue;
|
|
466
|
-
}
|
|
467
|
-
env[entry.key] = entry.value;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
function parseDotenvLine(line) {
|
|
471
|
-
const trimmed = line.trim();
|
|
472
|
-
if (trimmed.length === 0 || trimmed.startsWith("#")) {
|
|
473
|
-
return undefined;
|
|
474
|
-
}
|
|
475
|
-
const equalsIndex = trimmed.indexOf("=");
|
|
476
|
-
if (equalsIndex <= 0) {
|
|
477
|
-
return undefined;
|
|
478
|
-
}
|
|
479
|
-
const key = trimmed.slice(0, equalsIndex).trim();
|
|
480
|
-
const rawValue = trimmed.slice(equalsIndex + 1).trim();
|
|
481
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
482
|
-
return undefined;
|
|
483
|
-
}
|
|
484
|
-
return { key, value: unquoteDotenvValue(rawValue) };
|
|
485
|
-
}
|
|
486
|
-
function unquoteDotenvValue(value) {
|
|
487
|
-
if (value.length >= 2) {
|
|
488
|
-
const quote = value[0];
|
|
489
|
-
const last = value[value.length - 1];
|
|
490
|
-
if ((quote === "\"" || quote === "'") && last === quote) {
|
|
491
|
-
return value.slice(1, -1);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
return value;
|
|
495
|
-
}
|
|
496
557
|
function isNodeError(error) {
|
|
497
558
|
return error instanceof Error && "code" in error;
|
|
498
559
|
}
|
|
560
|
+
function isRecord(value) {
|
|
561
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
562
|
+
}
|
|
499
563
|
export async function isCliEntrypoint(argvPath, moduleUrl = import.meta.url) {
|
|
500
564
|
if (!argvPath) {
|
|
501
565
|
return false;
|
|
@@ -516,7 +580,6 @@ isCliEntrypoint(process.argv[1])
|
|
|
516
580
|
if (!isEntrypoint) {
|
|
517
581
|
return false;
|
|
518
582
|
}
|
|
519
|
-
await loadDotenvFile();
|
|
520
583
|
await runCli(process.argv.slice(2));
|
|
521
584
|
return true;
|
|
522
585
|
})
|