@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 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 model status
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 and daily-brief model configure/login first.`]
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 { deliverOnce, generateOnce, loginModelCredential, logoutModelCredential, readModelRuntimeConfig, runOnce, statusModelCredentials, toApiKeyCredential } from "./agent/index.js";
7
- import { collectSources } from "./collection/index.js";
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
- const reader = createInterface({ input: processStdin, output: processStdout });
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
- const result = await runOnce(options);
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(`Discord delivery: ${result.delivery.status}`);
49
- io.stdout(`Pi events: ${result.piEvents.join(", ")}`);
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(`Source Registry: ${path}`);
123
- io.stdout("Edit this YAML file, then run: daily-brief sources validate");
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 Source id`);
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
- await setSourceEnabled(value, enabled, sourceRegistryPath);
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 [--force]",
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
- const force = flags.force === "true";
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 timezone = env.TZ?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
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 writeIfNeeded(paths.configPath, [
185
- `timezone: ${timezone}`,
186
- "brief:",
187
- " language: zh",
188
- " maxSignals: 5",
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))) {
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("Next: daily-brief sources validate");
206
- io.stdout("Next: daily-brief model configure");
207
- io.stdout("Optional: daily-brief delivery configure --enabled true --webhook-url <url>");
208
- io.stdout("Readiness: config files present, Source Registry initialized, data directories writable, delivery disabled");
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
- async function handleDeliveryCommand(args, io, env) {
211
- const [subcommand, ...rest] = args;
212
- const flags = parseFlags(rest);
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
- if (subcommand === "configure") {
215
- const enabled = flags.enabled === "true" || flags.enabled === "yes";
216
- const webhookRef = flags["webhook-ref"] ?? "discord.default";
217
- writeDeliveryConfig({ enabled, ...(enabled ? { webhookRef } : {}) }, paths.configPath);
218
- if (enabled && flags["webhook-url"]) {
219
- putCredential(webhookRef, { type: "webhook", provider: "discord", webhookUrl: flags["webhook-url"] }, paths.authPath);
220
- }
221
- io.stdout(`Discord delivery: ${enabled ? "enabled" : "disabled"}`);
222
- if (enabled)
223
- io.stdout(`Webhook Ref: ${webhookRef}`);
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
- if (subcommand === "status") {
227
- const config = readDeliveryConfig(paths.configPath);
228
- if (!config?.enabled) {
229
- io.stdout("Discord delivery: disabled");
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
- const credential = config.webhookRef ? getDeliveryWebhookCredential(config.webhookRef, paths.authPath) : undefined;
233
- io.stdout("Discord delivery: enabled");
234
- io.stdout(`Webhook Ref: ${config.webhookRef ?? "(missing)"}`);
235
- io.stdout(`Webhook: ${credential ? "<redacted>" : "(missing)"}`);
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 (subcommand === "test") {
239
- const config = readDeliveryConfig(paths.configPath);
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");
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
- const response = await (io.fetchImpl ?? fetch)(credential.webhookUrl, {
245
- method: "POST",
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}`);
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
- throw new Error(`Unknown delivery command: ${subcommand ?? "(missing)"}`);
256
- }
257
- function getDeliveryWebhookCredential(ref, authPath) {
258
- const credential = getCredential(ref, authPath);
259
- return credential?.type === "webhook" && credential.provider === "discord" ? credential : undefined;
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 writeIfNeeded(path, contents, force) {
262
- if (!force && (await exists(path))) {
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 writeFile(path, contents, "utf8");
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(`model configure requires --${label.toLowerCase().replaceAll(" ", "-")}`);
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 "No Sources configured.";
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
- return registry.sources
43
- .map((source) => {
44
- const state = source.enabled ? "enabled" : "disabled";
45
- return `${state.padEnd(8)} ${source.id} ${source.platform}/${source.adapter} ${source.target}`;
46
- })
47
- .join("\n");
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
  }
@@ -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
- - 06:00 local time: run `collect`.
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 model configure
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
 
@@ -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 model configure
58
- daily-brief model status
59
- daily-brief model login
60
- daily-brief model logout
61
+ daily-brief setup
61
62
  ```
62
63
 
63
- Credentials are stored in the Model Credential Store or environment variables. Do not put secrets in `sources.yaml` or committed project files.
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 delivery configure --enabled true --webhook-url <url>
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
- For separated operational steps:
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 collect
89
- daily-brief generate
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 commands.
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 unless an overwrite or force behavior is explicitly selected.
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 model status
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 delivery status
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chenpengfei/daily-brief",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Agent-driven daily brief CLI for manually curated Agent architecture and AI Coding sources.",
5
5
  "private": false,
6
6
  "type": "module",