@chenpengfei/daily-brief 0.1.1 → 0.1.2
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 +21 -0
- package/README.md +1 -2
- package/dist/src/agent/daily-brief-agent.js +14 -0
- package/dist/src/agent/model-runtime-config.js +1 -1
- package/dist/src/cli.js +269 -184
- package/dist/src/config/source-registry.js +30 -7
- package/dist/src/discord/delivery.js +5 -2
- package/docs/operations.md +6 -12
- package/docs/user-manual.md +23 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes for Formal Releases are recorded here. GitHub Release notes should be derived from the matching version entry.
|
|
4
4
|
|
|
5
|
+
## 0.1.2 - 2026-06-04
|
|
6
|
+
|
|
7
|
+
Patch release for the simplified public CLI setup workflow.
|
|
8
|
+
|
|
9
|
+
### User-visible Changes
|
|
10
|
+
|
|
11
|
+
- Simplifies the public CLI around `setup`, `run-once`, `status`, `sources`, `version`, and help.
|
|
12
|
+
- Reworks `daily-brief setup` into an interactive wizard that preserves existing files by default and guides model credential and optional Discord delivery configuration.
|
|
13
|
+
- Adds human-readable `run-once` progress output and clearer Source listing/edit guidance.
|
|
14
|
+
- Makes `delivery.enabled: false` take precedence over `DISCORD_WEBHOOK_URL`, preventing accidental Discord sends when delivery is disabled in config.
|
|
15
|
+
|
|
16
|
+
### Installation and Upgrade Notes
|
|
17
|
+
|
|
18
|
+
- Upgrade with `npm install -g @chenpengfei/daily-brief@latest`.
|
|
19
|
+
- Run `daily-brief setup` again after upgrading to review the simplified setup flow.
|
|
20
|
+
- Non-interactive setup now fails before writing configuration files; scripted environments should write `config.yaml`, `sources.yaml`, and `auth.json` directly.
|
|
21
|
+
|
|
22
|
+
### Known Limitations
|
|
23
|
+
|
|
24
|
+
- The simplified public CLI removes direct public `collect`, `generate`, `deliver`, `model`, and `delivery` command entry points. Use `setup`, `run-once`, `status`, and `sources` for normal installed usage.
|
|
25
|
+
|
|
5
26
|
## 0.1.1 - 2026-06-01
|
|
6
27
|
|
|
7
28
|
Patch release for first-install command discovery.
|
package/README.md
CHANGED
|
@@ -25,8 +25,7 @@ Daily Brief requires Node.js 22 or newer.
|
|
|
25
25
|
daily-brief run-once
|
|
26
26
|
daily-brief status
|
|
27
27
|
daily-brief sources list
|
|
28
|
-
daily-brief
|
|
29
|
-
daily-brief delivery status
|
|
28
|
+
daily-brief version
|
|
30
29
|
```
|
|
31
30
|
|
|
32
31
|
For installation, setup, configuration, upgrade, and troubleshooting, see `docs/user-manual.md`.
|
|
@@ -17,6 +17,7 @@ export async function runOnce(options = {}) {
|
|
|
17
17
|
const dateKey = options.dateKey;
|
|
18
18
|
let collection;
|
|
19
19
|
try {
|
|
20
|
+
options.onProgress?.("1/5 Collecting Source Items");
|
|
20
21
|
collection = await collectSources({
|
|
21
22
|
date,
|
|
22
23
|
...(dateKey ? { dateKey } : {}),
|
|
@@ -24,6 +25,9 @@ export async function runOnce(options = {}) {
|
|
|
24
25
|
...(options.sourceRegistryPath ? { sourceRegistryPath: options.sourceRegistryPath } : {}),
|
|
25
26
|
...(options.sourceItemRoot ? { sourceItemRoot: options.sourceItemRoot } : {})
|
|
26
27
|
});
|
|
28
|
+
for (const source of collection.sources) {
|
|
29
|
+
options.onProgress?.(`- ${source.sourceId}: ${source.status}, items ${source.itemCount}, written ${source.writtenCount}, duplicates ${source.skippedDuplicateCount}${source.reason ? `, ${source.reason}` : ""}`);
|
|
30
|
+
}
|
|
27
31
|
}
|
|
28
32
|
catch (error) {
|
|
29
33
|
const coreFailure = {
|
|
@@ -32,6 +36,7 @@ export async function runOnce(options = {}) {
|
|
|
32
36
|
};
|
|
33
37
|
const delivery = await deliverCoreFailureNotification(coreFailure, {
|
|
34
38
|
...(options.discordWebhookUrl ? { webhookUrl: options.discordWebhookUrl } : {}),
|
|
39
|
+
...(options.discordEnv ? { env: options.discordEnv } : {}),
|
|
35
40
|
...(options.discordFetchImpl ? { fetchImpl: options.discordFetchImpl } : {})
|
|
36
41
|
});
|
|
37
42
|
return {
|
|
@@ -50,6 +55,7 @@ export async function runOnce(options = {}) {
|
|
|
50
55
|
if (collectionFailure) {
|
|
51
56
|
const delivery = await deliverCoreFailureNotification(collectionFailure, {
|
|
52
57
|
...(options.discordWebhookUrl ? { webhookUrl: options.discordWebhookUrl } : {}),
|
|
58
|
+
...(options.discordEnv ? { env: options.discordEnv } : {}),
|
|
53
59
|
...(options.discordFetchImpl ? { fetchImpl: options.discordFetchImpl } : {})
|
|
54
60
|
});
|
|
55
61
|
return {
|
|
@@ -64,6 +70,7 @@ export async function runOnce(options = {}) {
|
|
|
64
70
|
};
|
|
65
71
|
}
|
|
66
72
|
const collectionFailures = collectionFailureRefs(collection);
|
|
73
|
+
options.onProgress?.("2/5 Generating Daily Brief");
|
|
67
74
|
const generated = await generateOnce({
|
|
68
75
|
...options,
|
|
69
76
|
...(dateKey ? { dateKey } : {}),
|
|
@@ -75,6 +82,7 @@ export async function runOnce(options = {}) {
|
|
|
75
82
|
}
|
|
76
83
|
: {})
|
|
77
84
|
});
|
|
85
|
+
options.onProgress?.("4/5 Delivering Notification");
|
|
78
86
|
const delivery = await deliverOnce({
|
|
79
87
|
...options,
|
|
80
88
|
date,
|
|
@@ -124,6 +132,7 @@ export async function generateOnce(options = {}) {
|
|
|
124
132
|
const collectionInputRefs = options.collectionFailures
|
|
125
133
|
? { collectionFailures: options.collectionFailures }
|
|
126
134
|
: undefined;
|
|
135
|
+
options.onProgress?.("- Understanding Source Items");
|
|
127
136
|
const understanding = await runSourceItemUnderstandingStage({
|
|
128
137
|
sourceItems,
|
|
129
138
|
modelRuntimeConfig,
|
|
@@ -131,6 +140,7 @@ export async function generateOnce(options = {}) {
|
|
|
131
140
|
...(collectionInputRefs ? { inputRefs: collectionInputRefs } : {}),
|
|
132
141
|
...(options.modelRuntimeEnv ? { modelRuntimeEnv: options.modelRuntimeEnv } : {})
|
|
133
142
|
});
|
|
143
|
+
options.onProgress?.("- Selecting and Ranking Signals");
|
|
134
144
|
const selected = await runSignalSelectionAndRankingStages({
|
|
135
145
|
sourceItems,
|
|
136
146
|
annotations: understanding.annotations,
|
|
@@ -147,6 +157,7 @@ export async function generateOnce(options = {}) {
|
|
|
147
157
|
signals: selected.signals
|
|
148
158
|
};
|
|
149
159
|
const narrativeEvents = [];
|
|
160
|
+
options.onProgress?.("- Writing Narrative");
|
|
150
161
|
const narrative = await enrichDailyBriefNarrativeWithAgent({
|
|
151
162
|
brief: selectedBrief,
|
|
152
163
|
sourceItems,
|
|
@@ -161,6 +172,7 @@ export async function generateOnce(options = {}) {
|
|
|
161
172
|
brief: narrative.brief,
|
|
162
173
|
...(options.collectionFailures ? { collectionFailures: options.collectionFailures } : {})
|
|
163
174
|
});
|
|
175
|
+
options.onProgress?.("- Checking Source Grounding");
|
|
164
176
|
const audited = await auditBriefWithSingleRepairAttempt({
|
|
165
177
|
narrativeBrief: narrative.brief,
|
|
166
178
|
sourceItems,
|
|
@@ -175,6 +187,7 @@ export async function generateOnce(options = {}) {
|
|
|
175
187
|
const writtenArtifact = options.agentRunRoot ? await writeAgentRunArtifact(artifact, date, options.agentRunRoot, dateKey) : undefined;
|
|
176
188
|
const markdown = renderDailyBriefMarkdown(audited.brief);
|
|
177
189
|
const piResult = await renderBriefThroughPiRuntime(markdown);
|
|
190
|
+
options.onProgress?.("3/5 Archiving Daily Brief");
|
|
178
191
|
const archived = await writeBriefArchive(piResult.markdown, date, options.archiveRoot, dateKey);
|
|
179
192
|
return {
|
|
180
193
|
archivePath: archived.path,
|
|
@@ -309,6 +322,7 @@ export async function deliverOnce(options = {}) {
|
|
|
309
322
|
...(options.discordTemplatePath ? { templatePath: options.discordTemplatePath } : {})
|
|
310
323
|
}, {
|
|
311
324
|
...(options.discordWebhookUrl ? { webhookUrl: options.discordWebhookUrl } : {}),
|
|
325
|
+
...(options.discordEnv ? { env: options.discordEnv } : {}),
|
|
312
326
|
...(options.discordFetchImpl ? { fetchImpl: options.discordFetchImpl } : {})
|
|
313
327
|
});
|
|
314
328
|
}
|
|
@@ -17,7 +17,7 @@ export function readModelRuntimeConfig(env = process.env, options = {}) {
|
|
|
17
17
|
model: "gpt-5.5",
|
|
18
18
|
credentialRef: "openai-codex.default",
|
|
19
19
|
ready: false,
|
|
20
|
-
issues: [`Model config not found: ${configPath}. Run daily-brief setup
|
|
20
|
+
issues: [`Model config not found: ${configPath}. Run daily-brief setup or create config.yaml with model settings.`]
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
const issues = credentialIssues(config.provider, config.credentialRef, env, authPath);
|
package/dist/src/cli.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
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 {
|
|
7
|
+
import { stringify } from "yaml";
|
|
8
|
+
import { loginModelCredential, readModelRuntimeConfig, runOnce, toApiKeyCredential } from "./agent/index.js";
|
|
8
9
|
import { resolveConfiguredWebhookUrl } from "./discord/index.js";
|
|
9
|
-
import { defaultModelConfig, dateFromDateKey, formatDateKey, formatSourceRegistry, loadSourceRegistry, putCredential, readDeliveryConfig, readConfiguredTimezone, getCredential, resolveDailyBriefPaths, setSourceEnabled, validateSourceRegistry, writeDeliveryConfig, writeCredentialStore, writeModelConfig } from "./config/index.js";
|
|
10
|
+
import { defaultModelConfig, dateFromDateKey, formatDateKey, formatSourceRegistry, isEnvCredentialRef, loadSourceRegistry, putCredential, readDeliveryConfig, readDailyBriefConfig, readConfiguredTimezone, getCredential, resolveDailyBriefPaths, setSourceEnabled, validateSourceRegistry, writeDeliveryConfig, writeCredentialStore, writeModelConfig } from "./config/index.js";
|
|
10
11
|
import { getOperationalStatus } from "./workflow/index.js";
|
|
11
12
|
const consoleIo = {
|
|
13
|
+
interactive: Boolean(processStdin.isTTY && processStdout.isTTY),
|
|
12
14
|
stdout(line) {
|
|
13
15
|
console.log(line);
|
|
14
16
|
},
|
|
@@ -16,7 +18,10 @@ const consoleIo = {
|
|
|
16
18
|
console.error(line);
|
|
17
19
|
},
|
|
18
20
|
async prompt(message) {
|
|
19
|
-
|
|
21
|
+
if (!processStdin.isTTY || !processStdout.isTTY) {
|
|
22
|
+
throw new Error(nonInteractiveSetupMessage());
|
|
23
|
+
}
|
|
24
|
+
const reader = createInterface({ input: processStdin, output: processStdout, terminal: false });
|
|
20
25
|
try {
|
|
21
26
|
return await reader.question(`${message} `);
|
|
22
27
|
}
|
|
@@ -36,43 +41,38 @@ export async function runCli(args, io = consoleIo, env = process.env) {
|
|
|
36
41
|
printHelp(io);
|
|
37
42
|
return;
|
|
38
43
|
}
|
|
44
|
+
if (command === "version" || command === "--version" || command === "-v") {
|
|
45
|
+
io.stdout(await readPackageVersion());
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
39
48
|
if (command === "run-once") {
|
|
40
49
|
await assertWorkflowConfigured(options.sourceRegistryPath);
|
|
41
|
-
|
|
50
|
+
io.stdout("Daily Brief run started");
|
|
51
|
+
io.stdout(`Date: ${options.dateKey}`);
|
|
52
|
+
io.stdout(`Home: ${resolveDailyBriefPaths(env).home}`);
|
|
53
|
+
io.stdout("");
|
|
54
|
+
const result = await runOnce({
|
|
55
|
+
...options,
|
|
56
|
+
onProgress(line) {
|
|
57
|
+
io.stdout(line);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
42
60
|
if (result.coreFailure) {
|
|
43
61
|
throw new Error(`Core Workflow Failure: ${result.coreFailure.kind}\n${result.coreFailure.message}`);
|
|
44
62
|
}
|
|
63
|
+
io.stdout("");
|
|
45
64
|
io.stdout(`Daily Brief archived: ${result.archivePath}`);
|
|
46
65
|
io.stdout(`Sources read: ${result.sourceCount}`);
|
|
47
66
|
io.stdout(`Source Items read: ${result.sourceItemCount}`);
|
|
48
|
-
io.stdout(`
|
|
49
|
-
io.stdout(`
|
|
67
|
+
io.stdout(`Agent stages completed: ${countAgentStageEvents(result.piEvents)}/5`);
|
|
68
|
+
io.stdout(`Discord delivery: ${formatDeliveryStatus(result.delivery)}`);
|
|
69
|
+
io.stdout("5/5 Run completed");
|
|
50
70
|
return;
|
|
51
71
|
}
|
|
52
72
|
if (command === "setup") {
|
|
53
73
|
await handleSetupCommand(args.slice(1), io, env);
|
|
54
74
|
return;
|
|
55
75
|
}
|
|
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
76
|
if (command === "status") {
|
|
77
77
|
const status = await getOperationalStatus(options);
|
|
78
78
|
io.stdout(`${status.health}: ${status.message}`);
|
|
@@ -85,16 +85,37 @@ export async function runCli(args, io = consoleIo, env = process.env) {
|
|
|
85
85
|
await handleSourcesCommand(subcommand, value, io, options.sourceRegistryPath);
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
88
|
-
if (command === "model") {
|
|
89
|
-
await handleModelCommand(args.slice(1), io, env);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
if (command === "delivery") {
|
|
93
|
-
await handleDeliveryCommand(args.slice(1), io, env);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
88
|
throw new Error(`Unknown command: ${command}`);
|
|
97
89
|
}
|
|
90
|
+
async function readPackageVersion() {
|
|
91
|
+
let directory = dirname(fileURLToPath(import.meta.url));
|
|
92
|
+
for (let index = 0; index < 8; index += 1) {
|
|
93
|
+
const path = join(directory, "package.json");
|
|
94
|
+
try {
|
|
95
|
+
const packageJson = JSON.parse(await readFile(path, "utf8"));
|
|
96
|
+
if (isRecord(packageJson) && typeof packageJson.version === "string") {
|
|
97
|
+
return `daily-brief ${packageJson.version}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (!(isNodeError(error) && error.code === "ENOENT")) {
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const parent = dirname(directory);
|
|
106
|
+
if (parent === directory) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
directory = parent;
|
|
110
|
+
}
|
|
111
|
+
return "daily-brief unknown";
|
|
112
|
+
}
|
|
113
|
+
function countAgentStageEvents(events) {
|
|
114
|
+
return events.length > 0 ? 5 : 0;
|
|
115
|
+
}
|
|
116
|
+
function formatDeliveryStatus(delivery) {
|
|
117
|
+
return `${delivery.status}${delivery.reason ? ` (${delivery.reason})` : ""}`;
|
|
118
|
+
}
|
|
98
119
|
function readWorkflowDateOption(flags, env) {
|
|
99
120
|
if (flags.date) {
|
|
100
121
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(flags.date)) {
|
|
@@ -119,8 +140,15 @@ async function handleSourcesCommand(subcommand, value, io, sourceRegistryPath) {
|
|
|
119
140
|
}
|
|
120
141
|
if (subcommand === "edit") {
|
|
121
142
|
const path = sourceRegistryPath ?? resolveDailyBriefPaths().sourceRegistryPath;
|
|
122
|
-
io.stdout(
|
|
123
|
-
io.stdout(
|
|
143
|
+
io.stdout("Source Registry:");
|
|
144
|
+
io.stdout(` ${path}`);
|
|
145
|
+
io.stdout("");
|
|
146
|
+
io.stdout("Edit this YAML file to add, remove, or update Sources.");
|
|
147
|
+
io.stdout("Use the id field as SOURCE ID for enable/disable commands.");
|
|
148
|
+
io.stdout("");
|
|
149
|
+
io.stdout("After editing:");
|
|
150
|
+
io.stdout(" daily-brief sources validate");
|
|
151
|
+
io.stdout(" daily-brief sources list");
|
|
124
152
|
return;
|
|
125
153
|
}
|
|
126
154
|
if (subcommand === "validate") {
|
|
@@ -138,10 +166,19 @@ async function handleSourcesCommand(subcommand, value, io, sourceRegistryPath) {
|
|
|
138
166
|
}
|
|
139
167
|
if (subcommand === "enable" || subcommand === "disable") {
|
|
140
168
|
if (!value) {
|
|
141
|
-
throw new Error(`sources ${subcommand} requires a
|
|
169
|
+
throw new Error(`sources ${subcommand} requires a SOURCE ID. Run daily-brief sources list to see available SOURCE ID values.`);
|
|
142
170
|
}
|
|
143
171
|
const enabled = subcommand === "enable";
|
|
144
|
-
|
|
172
|
+
try {
|
|
173
|
+
await setSourceEnabled(value, enabled, sourceRegistryPath);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
+
if (message === `Source not found: ${value}`) {
|
|
178
|
+
throw new Error(`Source not found: ${value}. Run daily-brief sources list to see available SOURCE ID values.`);
|
|
179
|
+
}
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
145
182
|
io.stdout(`${enabled ? "Enabled" : "Disabled"} Source: ${value}`);
|
|
146
183
|
return;
|
|
147
184
|
}
|
|
@@ -152,117 +189,229 @@ function printHelp(io) {
|
|
|
152
189
|
"Daily Brief Operational CLI",
|
|
153
190
|
"",
|
|
154
191
|
"Usage:",
|
|
155
|
-
" daily-brief setup
|
|
156
|
-
" daily-brief run-once",
|
|
157
|
-
" daily-brief collect",
|
|
158
|
-
" daily-brief generate",
|
|
159
|
-
" daily-brief deliver",
|
|
192
|
+
" daily-brief setup",
|
|
193
|
+
" daily-brief run-once [--date YYYY-MM-DD]",
|
|
160
194
|
" 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
195
|
" daily-brief sources list",
|
|
169
196
|
" daily-brief sources edit",
|
|
170
197
|
" daily-brief sources validate",
|
|
171
198
|
" daily-brief sources enable <source-id>",
|
|
172
|
-
" daily-brief sources disable <source-id>"
|
|
199
|
+
" daily-brief sources disable <source-id>",
|
|
200
|
+
" daily-brief version"
|
|
173
201
|
].join("\n"));
|
|
174
202
|
}
|
|
175
203
|
async function handleSetupCommand(args, io, env) {
|
|
176
204
|
const flags = parseFlags(args);
|
|
177
|
-
|
|
205
|
+
if (Object.keys(flags).length > 0 || args.some((arg) => !arg.startsWith("--"))) {
|
|
206
|
+
throw new Error("daily-brief setup does not accept flags. Re-run setup and choose what to preserve or update interactively.");
|
|
207
|
+
}
|
|
208
|
+
requireInteractiveSetup(io);
|
|
178
209
|
const paths = resolveDailyBriefPaths(env);
|
|
179
|
-
const
|
|
210
|
+
const existingConfig = readDailyBriefConfig(paths.configPath);
|
|
211
|
+
const timezone = await promptWithDefault(io, "Timezone", readConfiguredTimezone(paths.configPath) ?? env.TZ?.trim() ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC");
|
|
180
212
|
await mkdir(paths.home, { recursive: true });
|
|
181
213
|
await mkdir(paths.sourceItemRoot, { recursive: true });
|
|
182
214
|
await mkdir(paths.agentRunRoot, { recursive: true });
|
|
183
215
|
await mkdir(paths.briefArchiveRoot, { recursive: true });
|
|
184
|
-
await
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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))) {
|
|
216
|
+
await writeSetupBaseConfig(paths.configPath, {
|
|
217
|
+
...existingConfig,
|
|
218
|
+
timezone,
|
|
219
|
+
brief: normalizeBriefConfig(existingConfig.brief)
|
|
220
|
+
});
|
|
221
|
+
if (!(await exists(paths.authPath))) {
|
|
199
222
|
writeCredentialStore({ credentials: {} }, paths.authPath);
|
|
200
223
|
}
|
|
224
|
+
if (await exists(paths.sourceRegistryPath)) {
|
|
225
|
+
io.stdout(`Source Registry: ${paths.sourceRegistryPath}`);
|
|
226
|
+
try {
|
|
227
|
+
io.stdout(formatSourceRegistry(await loadSourceRegistry(paths.sourceRegistryPath)));
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
231
|
+
io.stdout(`Source Registry invalid:\n${message}`);
|
|
232
|
+
}
|
|
233
|
+
const reinitialize = await promptYesNo(io, "Reinitialize example Sources? Existing sources.yaml will be overwritten.", false);
|
|
234
|
+
if (reinitialize) {
|
|
235
|
+
await writeFile(paths.sourceRegistryPath, defaultSourceRegistryExample(), "utf8");
|
|
236
|
+
io.stdout("Source Registry reinitialized from the packaged example.");
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
io.stdout("Source Registry preserved.");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
await writeFile(paths.sourceRegistryPath, defaultSourceRegistryExample(), "utf8");
|
|
244
|
+
io.stdout(`Source Registry initialized: ${paths.sourceRegistryPath}`);
|
|
245
|
+
io.stdout(formatSourceRegistry(await loadSourceRegistry(paths.sourceRegistryPath)));
|
|
246
|
+
}
|
|
247
|
+
io.stdout("To edit Sources:");
|
|
248
|
+
io.stdout(" daily-brief sources edit");
|
|
249
|
+
io.stdout(" daily-brief sources validate");
|
|
250
|
+
io.stdout(" daily-brief sources list");
|
|
251
|
+
await configureModelThroughSetup(io, env);
|
|
252
|
+
await configureDeliveryThroughSetup(io, env);
|
|
253
|
+
const readiness = readModelRuntimeConfig(env);
|
|
254
|
+
const delivery = readDeliveryConfig(paths.configPath);
|
|
201
255
|
io.stdout(`Daily Brief home: ${paths.home}`);
|
|
202
256
|
io.stdout(`Daily Brief data: ${paths.dataHome}`);
|
|
203
257
|
io.stdout(`Timezone: ${timezone}`);
|
|
204
258
|
io.stdout(`Source Registry: ${paths.sourceRegistryPath}`);
|
|
205
|
-
io.stdout(
|
|
206
|
-
io.stdout(
|
|
207
|
-
io.stdout(
|
|
208
|
-
io.stdout("
|
|
259
|
+
io.stdout(`Model: ${readiness.provider}/${readiness.model}`);
|
|
260
|
+
io.stdout(`Model credential: ${readiness.ready ? "configured" : "missing"}`);
|
|
261
|
+
io.stdout(`Discord delivery: ${delivery?.enabled ? "enabled" : "disabled"}`);
|
|
262
|
+
io.stdout("Ready to run:");
|
|
263
|
+
io.stdout(" daily-brief run-once");
|
|
264
|
+
}
|
|
265
|
+
function requireInteractiveSetup(io) {
|
|
266
|
+
if (!io.prompt || io.interactive === false) {
|
|
267
|
+
throw new Error(nonInteractiveSetupMessage());
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function nonInteractiveSetupMessage() {
|
|
271
|
+
return [
|
|
272
|
+
"daily-brief setup requires an interactive terminal.",
|
|
273
|
+
"For CI or scripted setup, create ~/.daily-brief/config.yaml, ~/.daily-brief/sources.yaml, and ~/.daily-brief/auth.json directly.",
|
|
274
|
+
"Use DAILY_BRIEF_HOME to choose a configuration root and DAILY_BRIEF_DATA_HOME to choose a generated-data root.",
|
|
275
|
+
"Run daily-brief sources validate after writing sources.yaml."
|
|
276
|
+
].join("\n");
|
|
277
|
+
}
|
|
278
|
+
async function writeSetupBaseConfig(path, config) {
|
|
279
|
+
await mkdir(dirname(path), { recursive: true });
|
|
280
|
+
await writeFile(path, stringify(config), "utf8");
|
|
209
281
|
}
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
282
|
+
function normalizeBriefConfig(value) {
|
|
283
|
+
const current = isRecord(value) ? value : {};
|
|
284
|
+
return {
|
|
285
|
+
...current,
|
|
286
|
+
language: "zh",
|
|
287
|
+
maxSignals: typeof current.maxSignals === "number" ? current.maxSignals : 5
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
async function configureModelThroughSetup(io, env) {
|
|
213
291
|
const paths = resolveDailyBriefPaths(env);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
292
|
+
const current = readDailyBriefConfig(paths.configPath).model ?? defaultModelConfig();
|
|
293
|
+
const provider = readProviderFlag(await promptWithDefault(io, "LLM Provider (openai-codex/openai/deepseek/openai-compatible)", current.provider));
|
|
294
|
+
const model = await promptWithDefault(io, "Model", current.provider === provider ? current.model : defaultModelForProvider(provider));
|
|
295
|
+
io.stdout("Credential name identifies where Daily Brief finds the model secret. Use the default unless you manage multiple credentials; env:NAME reads an environment variable.");
|
|
296
|
+
const credentialRef = await promptWithDefault(io, "Model credential name", current.provider === provider ? current.credentialRef ?? defaultCredentialRef(provider) ?? "" : defaultCredentialRef(provider) ?? "");
|
|
297
|
+
const baseUrl = provider === "openai-compatible"
|
|
298
|
+
? await promptWithDefault(io, "Base URL", current.provider === provider ? current.baseUrl ?? "" : "")
|
|
299
|
+
: undefined;
|
|
300
|
+
const config = {
|
|
301
|
+
provider,
|
|
302
|
+
model,
|
|
303
|
+
...(credentialRef ? { credentialRef } : {}),
|
|
304
|
+
...(baseUrl ? { baseUrl } : {})
|
|
305
|
+
};
|
|
306
|
+
writeModelConfig(config, paths.configPath);
|
|
307
|
+
io.stdout(`Model configured: ${provider}/${model}`);
|
|
308
|
+
if (credentialRef) {
|
|
309
|
+
io.stdout(`Model credential name: ${credentialRef}`);
|
|
310
|
+
}
|
|
311
|
+
await maybeConfigureModelCredential({ provider, credentialRef, io, env });
|
|
312
|
+
}
|
|
313
|
+
async function maybeConfigureModelCredential(input) {
|
|
314
|
+
if (!input.credentialRef) {
|
|
224
315
|
return;
|
|
225
316
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
317
|
+
const paths = resolveDailyBriefPaths(input.env);
|
|
318
|
+
if (input.provider === "openai-codex") {
|
|
319
|
+
const credential = getCredential(input.credentialRef, paths.authPath);
|
|
320
|
+
if (credential) {
|
|
321
|
+
input.io.stdout("Model credential: configured");
|
|
230
322
|
return;
|
|
231
323
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
324
|
+
if (await promptYesNo(input.io, "Login to openai-codex now?", true)) {
|
|
325
|
+
await loginModelCredential({
|
|
326
|
+
provider: input.provider,
|
|
327
|
+
credentialRef: input.credentialRef,
|
|
328
|
+
io: input.io,
|
|
329
|
+
env: input.env
|
|
330
|
+
});
|
|
331
|
+
input.io.stdout(`Logged in credential: ${input.credentialRef}`);
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
input.io.stdout("Model credential: missing");
|
|
335
|
+
}
|
|
236
336
|
return;
|
|
237
337
|
}
|
|
238
|
-
if (
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
338
|
+
if (isEnvCredentialRef(input.credentialRef)) {
|
|
339
|
+
const hasEnvValue = Boolean(input.env[input.credentialRef.slice("env:".length)]?.trim());
|
|
340
|
+
if (hasEnvValue) {
|
|
341
|
+
input.io.stdout("Model credential: configured from environment");
|
|
342
|
+
return;
|
|
243
343
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
|
|
344
|
+
if (!(await promptYesNo(input.io, "Store API key in credential store instead of using the environment?", false))) {
|
|
345
|
+
input.io.stdout("Model credential: missing until the environment variable is set");
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const storedRef = await promptWithDefault(input.io, "Stored credential name", `${input.provider}.default`);
|
|
349
|
+
const apiKey = await promptWithDefault(input.io, "API key input may be visible in this terminal. API key", "");
|
|
350
|
+
if (apiKey) {
|
|
351
|
+
putCredential(storedRef, toApiKeyCredential(input.provider, apiKey), paths.authPath);
|
|
352
|
+
writeModelConfig({ ...readDailyBriefConfig(paths.configPath).model, credentialRef: storedRef }, paths.configPath);
|
|
353
|
+
input.io.stdout(`Stored credential: ${storedRef}`);
|
|
251
354
|
}
|
|
252
|
-
io.stdout("Discord delivery test: sent");
|
|
253
355
|
return;
|
|
254
356
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
357
|
+
if (getCredential(input.credentialRef, paths.authPath)) {
|
|
358
|
+
input.io.stdout("Model credential: configured");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (await promptYesNo(input.io, "Store API key in credential store? API key input may be visible in this terminal.", false)) {
|
|
362
|
+
const apiKey = await promptWithDefault(input.io, "API key", "");
|
|
363
|
+
if (apiKey) {
|
|
364
|
+
putCredential(input.credentialRef, toApiKeyCredential(input.provider, apiKey), paths.authPath);
|
|
365
|
+
input.io.stdout(`Stored credential: ${input.credentialRef}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
input.io.stdout("Model credential: missing");
|
|
370
|
+
}
|
|
260
371
|
}
|
|
261
|
-
async function
|
|
262
|
-
|
|
372
|
+
async function configureDeliveryThroughSetup(io, env) {
|
|
373
|
+
const paths = resolveDailyBriefPaths(env);
|
|
374
|
+
const current = readDeliveryConfig(paths.configPath);
|
|
375
|
+
const enabled = await promptYesNo(io, "Enable Discord delivery?", current?.enabled ?? false);
|
|
376
|
+
if (!enabled) {
|
|
377
|
+
writeDeliveryConfig({ enabled: false }, paths.configPath);
|
|
378
|
+
io.stdout("Discord delivery: disabled");
|
|
263
379
|
return;
|
|
264
380
|
}
|
|
265
|
-
await
|
|
381
|
+
const webhookRef = await promptWithDefault(io, "Discord webhook credential name", current?.webhookRef ?? "discord.default");
|
|
382
|
+
writeDeliveryConfig({ enabled: true, webhookRef }, paths.configPath);
|
|
383
|
+
const credential = getCredential(webhookRef, paths.authPath);
|
|
384
|
+
if (credential) {
|
|
385
|
+
io.stdout("Discord webhook: configured");
|
|
386
|
+
if (!(await promptYesNo(io, "Replace Discord webhook URL?", false))) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const webhookUrl = await promptWithDefault(io, "Discord webhook URL", "");
|
|
391
|
+
if (webhookUrl) {
|
|
392
|
+
putCredential(webhookRef, { type: "webhook", provider: "discord", webhookUrl }, paths.authPath);
|
|
393
|
+
io.stdout(`Discord webhook stored: ${webhookRef}`);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
io.stdout("Discord webhook: missing");
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async function promptYesNo(io, label, defaultValue) {
|
|
400
|
+
if (!io.prompt) {
|
|
401
|
+
throw new Error(nonInteractiveSetupMessage());
|
|
402
|
+
}
|
|
403
|
+
const suffix = defaultValue ? "[Y/n]" : "[y/N]";
|
|
404
|
+
const answer = (await io.prompt(`${label} ${suffix}:`)).trim().toLowerCase();
|
|
405
|
+
if (!answer) {
|
|
406
|
+
return defaultValue;
|
|
407
|
+
}
|
|
408
|
+
if (answer === "y" || answer === "yes" || answer === "true") {
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
if (answer === "n" || answer === "no" || answer === "false") {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
throw new Error(`${label} requires y/yes or n/no`);
|
|
266
415
|
}
|
|
267
416
|
async function exists(path) {
|
|
268
417
|
try {
|
|
@@ -293,71 +442,6 @@ function defaultSourceRegistryExample() {
|
|
|
293
442
|
""
|
|
294
443
|
].join("\n");
|
|
295
444
|
}
|
|
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
445
|
function parseFlags(args) {
|
|
362
446
|
const flags = {};
|
|
363
447
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -427,14 +511,11 @@ function defaultCredentialRef(provider) {
|
|
|
427
511
|
}
|
|
428
512
|
async function promptWithDefault(io, label, defaultValue) {
|
|
429
513
|
if (!io.prompt) {
|
|
430
|
-
throw new Error(
|
|
514
|
+
throw new Error(nonInteractiveSetupMessage());
|
|
431
515
|
}
|
|
432
516
|
const answer = await io.prompt(`${label}${defaultValue ? ` (${defaultValue})` : ""}:`);
|
|
433
517
|
return answer.trim() || defaultValue;
|
|
434
518
|
}
|
|
435
|
-
function missingFlag(name) {
|
|
436
|
-
throw new Error(`model configure requires --${name}`);
|
|
437
|
-
}
|
|
438
519
|
function optionsFromEnv(env) {
|
|
439
520
|
const paths = resolveDailyBriefPaths(env);
|
|
440
521
|
const discordWebhookUrl = resolveConfiguredWebhookUrl(env);
|
|
@@ -444,6 +525,7 @@ function optionsFromEnv(env) {
|
|
|
444
525
|
agentRunRoot: paths.agentRunRoot,
|
|
445
526
|
archiveRoot: paths.briefArchiveRoot,
|
|
446
527
|
modelRuntimeEnv: env,
|
|
528
|
+
discordEnv: env,
|
|
447
529
|
...(discordWebhookUrl ? { discordWebhookUrl } : {}),
|
|
448
530
|
...(env.DAILY_BRIEF_DISCORD_TEMPLATE_PATH ? { discordTemplatePath: env.DAILY_BRIEF_DISCORD_TEMPLATE_PATH } : {})
|
|
449
531
|
};
|
|
@@ -496,6 +578,9 @@ function unquoteDotenvValue(value) {
|
|
|
496
578
|
function isNodeError(error) {
|
|
497
579
|
return error instanceof Error && "code" in error;
|
|
498
580
|
}
|
|
581
|
+
function isRecord(value) {
|
|
582
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
583
|
+
}
|
|
499
584
|
export async function isCliEntrypoint(argvPath, moduleUrl = import.meta.url) {
|
|
500
585
|
if (!argvPath) {
|
|
501
586
|
return false;
|
|
@@ -37,12 +37,35 @@ export async function setSourceEnabled(id, enabled, path = resolveDailyBriefPath
|
|
|
37
37
|
}
|
|
38
38
|
export function formatSourceRegistry(registry) {
|
|
39
39
|
if (registry.sources.length === 0) {
|
|
40
|
-
return
|
|
40
|
+
return [
|
|
41
|
+
"No Sources configured.",
|
|
42
|
+
"",
|
|
43
|
+
"Add Sources by editing the Source Registry, then run:",
|
|
44
|
+
" daily-brief sources edit",
|
|
45
|
+
" daily-brief sources validate"
|
|
46
|
+
].join("\n");
|
|
41
47
|
}
|
|
42
|
-
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.
|
|
48
|
+
const rows = registry.sources.map((source) => ({
|
|
49
|
+
sourceId: source.id,
|
|
50
|
+
status: source.enabled ? "enabled" : "disabled",
|
|
51
|
+
platform: source.platform,
|
|
52
|
+
adapter: source.adapter,
|
|
53
|
+
target: source.target
|
|
54
|
+
}));
|
|
55
|
+
const widths = {
|
|
56
|
+
sourceId: Math.max("SOURCE ID".length, ...rows.map((row) => row.sourceId.length)),
|
|
57
|
+
status: Math.max("STATUS".length, ...rows.map((row) => row.status.length)),
|
|
58
|
+
platform: Math.max("PLATFORM".length, ...rows.map((row) => row.platform.length)),
|
|
59
|
+
adapter: Math.max("ADAPTER".length, ...rows.map((row) => row.adapter.length))
|
|
60
|
+
};
|
|
61
|
+
const firstSourceId = rows[0]?.sourceId ?? "<source-id>";
|
|
62
|
+
const lines = [
|
|
63
|
+
`${"SOURCE ID".padEnd(widths.sourceId)} ${"STATUS".padEnd(widths.status)} ${"PLATFORM".padEnd(widths.platform)} ${"ADAPTER".padEnd(widths.adapter)} TARGET`,
|
|
64
|
+
...rows.map((row) => `${row.sourceId.padEnd(widths.sourceId)} ${row.status.padEnd(widths.status)} ${row.platform.padEnd(widths.platform)} ${row.adapter.padEnd(widths.adapter)} ${row.target}`),
|
|
65
|
+
"",
|
|
66
|
+
"Use SOURCE ID with:",
|
|
67
|
+
` daily-brief sources enable ${firstSourceId}`,
|
|
68
|
+
` daily-brief sources disable ${firstSourceId}`
|
|
69
|
+
];
|
|
70
|
+
return lines.join("\n");
|
|
48
71
|
}
|
|
@@ -62,11 +62,14 @@ async function readDiscordTemplate(templatePath) {
|
|
|
62
62
|
throw lastError instanceof Error ? lastError : new Error("Discord notification template not found");
|
|
63
63
|
}
|
|
64
64
|
export function resolveConfiguredWebhookUrl(env = process.env) {
|
|
65
|
+
const paths = resolveDailyBriefPaths(env);
|
|
66
|
+
const config = readDeliveryConfig(paths.configPath);
|
|
67
|
+
if (config?.enabled === false) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
65
70
|
if (env.DISCORD_WEBHOOK_URL) {
|
|
66
71
|
return env.DISCORD_WEBHOOK_URL;
|
|
67
72
|
}
|
|
68
|
-
const paths = resolveDailyBriefPaths(env);
|
|
69
|
-
const config = readDeliveryConfig(paths.configPath);
|
|
70
73
|
if (!config?.enabled || !config.webhookRef) {
|
|
71
74
|
return undefined;
|
|
72
75
|
}
|
package/docs/operations.md
CHANGED
|
@@ -13,21 +13,18 @@ daily-brief status
|
|
|
13
13
|
Development usage from a repository checkout uses `npm run cli --`:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npm run cli -- collect
|
|
17
|
-
npm run cli -- generate
|
|
18
|
-
npm run cli -- deliver
|
|
19
16
|
npm run cli -- status
|
|
20
17
|
npm run cli -- run-once
|
|
18
|
+
npm run cli -- --version
|
|
21
19
|
```
|
|
22
20
|
|
|
23
21
|
Expected local cadence:
|
|
24
22
|
|
|
25
|
-
-
|
|
26
|
-
- 07:00 local time: run `run-once` or `generate` followed by `deliver`.
|
|
23
|
+
- Run `daily-brief run-once` once per Daily Brief Cadence window.
|
|
27
24
|
|
|
28
25
|
Scheduler integration is intentionally deployment-neutral. A local cron, launchd job, systemd timer, GitHub Actions workflow, or another scheduler can call the commands above; the repository does not bind the MVP to a specific host.
|
|
29
26
|
|
|
30
|
-
`run-once` executes collection, brief generation/archive, and Discord delivery in order. Source Item writes are deduplicated by Source Item id and content hash within the daily JSONL store, and Daily Brief generation merges repeated mentions into one multi-citation Signal, so rerunning the workflow for the same Collection Window does not duplicate equivalent Signals.
|
|
27
|
+
`run-once` executes collection, brief generation/archive, and Discord delivery in order. It prints human-readable progress for Source collection, Agent Stage execution, archiving, and delivery while the run is active. Source Item writes are deduplicated by Source Item id and content hash within the daily JSONL store, and Daily Brief generation merges repeated mentions into one multi-citation Signal, so rerunning the workflow for the same Collection Window does not duplicate equivalent Signals.
|
|
31
28
|
|
|
32
29
|
## Manual run
|
|
33
30
|
|
|
@@ -41,7 +38,7 @@ npm run cli -- status
|
|
|
41
38
|
|
|
42
39
|
`sources list` confirms which Sources are enabled, including the default `github-trending-daily` Source. `run-once` performs collection, brief generation, archive writing, and Discord Delivery once. `status` reports operational health after the run.
|
|
43
40
|
|
|
44
|
-
Discord Delivery uses `DISCORD_WEBHOOK_URL` from the shell environment or local `.env`; `.env` is loaded automatically by the Operational CLI and does not need to be sourced manually.
|
|
41
|
+
Discord Delivery uses the configured credential reference, or `DISCORD_WEBHOOK_URL` from the shell environment or local `.env`; `.env` is loaded automatically by the Operational CLI and does not need to be sourced manually. An explicit `delivery.enabled: false` in `config.yaml` disables delivery and takes precedence over `DISCORD_WEBHOOK_URL`.
|
|
45
42
|
|
|
46
43
|
## Runtime configuration
|
|
47
44
|
|
|
@@ -57,13 +54,10 @@ Installed operational paths can be adjusted through environment variables:
|
|
|
57
54
|
Model/provider and delivery configuration should normally be managed with:
|
|
58
55
|
|
|
59
56
|
```bash
|
|
60
|
-
daily-brief
|
|
61
|
-
daily-brief model status
|
|
62
|
-
daily-brief delivery configure --enabled true --webhook-url <url>
|
|
63
|
-
daily-brief delivery status
|
|
57
|
+
daily-brief setup
|
|
64
58
|
```
|
|
65
59
|
|
|
66
|
-
Secrets live in `~/.daily-brief/auth.json` or environment variables, never in `sources.yaml` or committed project files. Installed CLI configuration does not accept the faux provider; faux is reserved for tests through an explicit test-only runtime gate.
|
|
60
|
+
Secrets live in `~/.daily-brief/auth.json` or environment variables, never in `sources.yaml` or committed project files. `daily-brief setup` requires interactive input; CI and scripted environments should create the user configuration files directly under `DAILY_BRIEF_HOME`. Installed CLI configuration does not accept the faux provider; faux is reserved for tests through an explicit test-only runtime gate.
|
|
67
61
|
|
|
68
62
|
`run-once` does not archive a normal Daily Brief when every enabled Source fails or when no Source Items exist for the requested date. It reports a Core Workflow Failure instead, because a false low-signal brief would hide collection failure.
|
|
69
63
|
|
package/docs/user-manual.md
CHANGED
|
@@ -31,10 +31,14 @@ Run setup after installing the package:
|
|
|
31
31
|
|
|
32
32
|
If npm's global bin directory is in `PATH`, `daily-brief setup` is equivalent.
|
|
33
33
|
|
|
34
|
-
Setup creates user configuration and generated-data directories, initializes the Source Registry from the packaged example, prepares credential storage, and reports readiness. Setup does not collect Sources, call an LLM, generate a Daily Brief, or send a delivery notification.
|
|
34
|
+
Setup is an interactive wizard. It creates user configuration and generated-data directories, initializes the Source Registry from the packaged example, prepares credential storage, guides LLM Provider setup, offers optional Discord Delivery setup, and reports readiness. Setup does not collect Sources, call an LLM, generate a Daily Brief, or send a delivery notification.
|
|
35
|
+
|
|
36
|
+
Yes/no prompts show the default in brackets: `[y/N]` means pressing Enter chooses no, and `[Y/n]` means pressing Enter chooses yes. You can type `y`, `yes`, `n`, or `no`.
|
|
35
37
|
|
|
36
38
|
By default, configuration files live under `~/.daily-brief/`, and generated data lives under `~/.daily-brief/data/`.
|
|
37
39
|
|
|
40
|
+
`daily-brief setup` requires interactive input. For CI or scripted setup, write `config.yaml`, `sources.yaml`, and `auth.json` directly under `DAILY_BRIEF_HOME`, and use `DAILY_BRIEF_DATA_HOME` when generated data should live elsewhere.
|
|
41
|
+
|
|
38
42
|
## Configure Sources
|
|
39
43
|
|
|
40
44
|
Sources are manually controlled. The agent processes configured Sources, but it does not autonomously add or remove them.
|
|
@@ -54,25 +58,20 @@ Use `sources validate` after editing the Source Registry.
|
|
|
54
58
|
Agent Stages require an LLM Provider Configuration before real Daily Brief generation.
|
|
55
59
|
|
|
56
60
|
```bash
|
|
57
|
-
daily-brief
|
|
58
|
-
daily-brief model status
|
|
59
|
-
daily-brief model login
|
|
60
|
-
daily-brief model logout
|
|
61
|
+
daily-brief setup
|
|
61
62
|
```
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
Use the setup wizard to choose the provider, model, credential name, and optional login/API key storage path. The credential name is a stable label, such as `openai.default` or `env:OPENAI_API_KEY`, that lets `config.yaml` find the secret in `auth.json` or in the shell environment. Do not put secrets in `sources.yaml` or committed project files.
|
|
64
65
|
|
|
65
66
|
## Configure Delivery
|
|
66
67
|
|
|
67
68
|
Discord Delivery is optional. Configure it when you want generated Daily Brief notifications pushed to Discord.
|
|
68
69
|
|
|
69
70
|
```bash
|
|
70
|
-
daily-brief
|
|
71
|
-
daily-brief delivery status
|
|
72
|
-
daily-brief delivery test
|
|
71
|
+
daily-brief setup
|
|
73
72
|
```
|
|
74
73
|
|
|
75
|
-
If Discord Delivery is disabled or no webhook is configured, generation can still run and will report skipped delivery explicitly.
|
|
74
|
+
Setup asks whether to enable Discord Delivery. If Discord Delivery is disabled or no webhook is configured, generation can still run and will report skipped delivery explicitly.
|
|
76
75
|
|
|
77
76
|
## Run Daily Brief
|
|
78
77
|
|
|
@@ -82,17 +81,19 @@ For a full manual run:
|
|
|
82
81
|
daily-brief run-once
|
|
83
82
|
```
|
|
84
83
|
|
|
85
|
-
|
|
84
|
+
`run-once` performs collection, Agent Stage generation, archive writing, and delivery once. It prints human-readable progress while the run is active so you can see whether it is collecting Sources, waiting on Agent Stages, archiving, or delivering.
|
|
85
|
+
|
|
86
|
+
The expected local cadence is a single daily run around the intended delivery time. Daily Brief does not include a built-in scheduler; use an external scheduler to invoke `daily-brief run-once`.
|
|
87
|
+
|
|
88
|
+
## Version
|
|
89
|
+
|
|
90
|
+
To report the installed CLI version:
|
|
86
91
|
|
|
87
92
|
```bash
|
|
88
|
-
daily-brief
|
|
89
|
-
daily-brief
|
|
90
|
-
daily-brief deliver
|
|
91
|
-
daily-brief status
|
|
93
|
+
daily-brief version
|
|
94
|
+
daily-brief --version
|
|
92
95
|
```
|
|
93
96
|
|
|
94
|
-
The expected local cadence is collection at 06:00 and generation or delivery at 07:00 local time. Daily Brief does not include a built-in scheduler; use an external scheduler to invoke the CLI.
|
|
95
|
-
|
|
96
97
|
## Inspect Status
|
|
97
98
|
|
|
98
99
|
Use status after setup, after manual runs, or when diagnosing failures:
|
|
@@ -110,9 +111,9 @@ The installed CLI uses user-home paths by default:
|
|
|
110
111
|
- `DAILY_BRIEF_HOME`: configuration directory, default `~/.daily-brief`.
|
|
111
112
|
- `DAILY_BRIEF_DATA_HOME`: generated data directory, default `~/.daily-brief/data`.
|
|
112
113
|
- `DAILY_BRIEF_DISCORD_TEMPLATE_PATH`: optional Discord notification template override.
|
|
113
|
-
- `DISCORD_WEBHOOK_URL`: optional Discord webhook URL.
|
|
114
|
+
- `DISCORD_WEBHOOK_URL`: optional Discord webhook URL. If `config.yaml` explicitly sets `delivery.enabled: false`, delivery stays disabled even when this environment variable is present.
|
|
114
115
|
|
|
115
|
-
Repository checkouts may use a local `.env`; installed usage should normally rely on user configuration and credential
|
|
116
|
+
Repository checkouts may use a local `.env`; installed usage should normally rely on setup, user configuration files, and environment-backed credential references.
|
|
116
117
|
|
|
117
118
|
## Upgrade
|
|
118
119
|
|
|
@@ -123,7 +124,7 @@ npm install -g @chenpengfei/daily-brief@latest
|
|
|
123
124
|
"$(npm prefix -g)/bin/daily-brief" status
|
|
124
125
|
```
|
|
125
126
|
|
|
126
|
-
Run `daily-brief setup` again when release notes or status output indicate that configuration needs to be refreshed. Setup preserves existing files
|
|
127
|
+
Run `daily-brief setup` again when release notes or status output indicate that configuration needs to be refreshed. Setup preserves existing files by default and asks before replacing non-secret configuration. It does not accept a force-overwrite flag and never deletes generated data.
|
|
127
128
|
|
|
128
129
|
## Troubleshooting
|
|
129
130
|
|
|
@@ -149,14 +150,13 @@ daily-brief sources list
|
|
|
149
150
|
If model access fails, run:
|
|
150
151
|
|
|
151
152
|
```bash
|
|
152
|
-
daily-brief
|
|
153
|
+
daily-brief setup
|
|
153
154
|
```
|
|
154
155
|
|
|
155
156
|
If Discord delivery fails or is skipped, run:
|
|
156
157
|
|
|
157
158
|
```bash
|
|
158
|
-
daily-brief
|
|
159
|
-
daily-brief delivery test
|
|
159
|
+
daily-brief setup
|
|
160
160
|
```
|
|
161
161
|
|
|
162
162
|
If a run cannot honestly produce a Daily Brief, the CLI reports a Core Workflow Failure rather than archiving a false normal Daily Brief.
|