@chenpengfei/daily-brief 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,9 +38,6 @@ export function removeCredential(ref, path = resolveDailyBriefPaths().authPath)
38
38
  return store;
39
39
  }
40
40
  export function getCredential(ref, path = resolveDailyBriefPaths().authPath) {
41
- if (isEnvCredentialRef(ref)) {
42
- return undefined;
43
- }
44
41
  assertStoredCredentialRef(ref);
45
42
  return readCredentialStore(path).credentials[ref];
46
43
  }
@@ -54,18 +51,9 @@ export function redactCredentialStore(store) {
54
51
  }
55
52
  ]));
56
53
  }
57
- export function isEnvCredentialRef(ref) {
58
- return ref.startsWith("env:") && ref.slice("env:".length).trim().length > 0;
59
- }
60
- export function envNameFromCredentialRef(ref) {
61
- if (!isEnvCredentialRef(ref)) {
62
- throw new Error(`Credential reference is not an env ref: ${ref}`);
63
- }
64
- return ref.slice("env:".length).trim();
65
- }
66
54
  export function assertStoredCredentialRef(ref) {
67
- if (isEnvCredentialRef(ref)) {
68
- throw new Error(`Credential reference ${ref} is environment-backed and cannot be stored in auth.json`);
55
+ if (ref.startsWith("env:")) {
56
+ throw new Error(`Credential reference ${ref} is not supported; use a stored credential name in auth.json`);
69
57
  }
70
58
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(ref)) {
71
59
  throw new Error(`Invalid credentialRef: ${ref}`);
@@ -16,10 +16,6 @@ export function readDailyBriefConfig(path = resolveDailyBriefPaths().configPath)
16
16
  export function readModelConfig(path = resolveDailyBriefPaths().configPath) {
17
17
  return readDailyBriefConfig(path).model;
18
18
  }
19
- export function readConfiguredTimezone(path = resolveDailyBriefPaths().configPath) {
20
- const timezone = readDailyBriefConfig(path).timezone;
21
- return typeof timezone === "string" && timezone.trim().length > 0 ? timezone.trim() : undefined;
22
- }
23
19
  export function writeModelConfig(config, path = resolveDailyBriefPaths().configPath) {
24
20
  const current = readDailyBriefConfig(path);
25
21
  const next = {
@@ -72,6 +68,9 @@ export function parseModelConfig(value) {
72
68
  if ("apiKey" in value || "secret" in value || "accessToken" in value || "refreshToken" in value) {
73
69
  throw new Error("config.model must not contain secrets; use auth.json via credentialRef");
74
70
  }
71
+ if (credentialRef?.startsWith("env:")) {
72
+ throw new Error("config.model.credentialRef must be a stored credential name, not env:NAME");
73
+ }
75
74
  if (provider === "openai-compatible" && !baseUrl) {
76
75
  throw new Error("config.model.baseUrl is required when provider is openai-compatible");
77
76
  }
@@ -94,10 +93,11 @@ function readProvider(value) {
94
93
  if (provider === "codex" || provider === "hermes") {
95
94
  return "openai-codex";
96
95
  }
97
- if (provider === "faux") {
98
- throw new Error("config.model.provider must not be faux; faux is only available through test-only runtime injection");
99
- }
100
- if (provider === "openai-codex" || provider === "openai" || provider === "deepseek" || provider === "openai-compatible") {
96
+ if (provider === "faux" ||
97
+ provider === "openai-codex" ||
98
+ provider === "openai" ||
99
+ provider === "deepseek" ||
100
+ provider === "openai-compatible") {
101
101
  return provider;
102
102
  }
103
103
  throw new Error(`Unsupported model provider: ${provider}`);
@@ -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
  }
@@ -22,7 +22,7 @@ export async function deliverCoreFailureNotification(failure, options = {}) {
22
22
  async function sendDiscordContent(content, options) {
23
23
  const webhookUrl = options.webhookUrl ?? resolveConfiguredWebhookUrl(options.env ?? process.env);
24
24
  if (!webhookUrl) {
25
- return { status: "skipped", reason: "DISCORD_WEBHOOK_URL is not configured" };
25
+ return { status: "skipped", reason: "Discord delivery webhook is not configured" };
26
26
  }
27
27
  try {
28
28
  const fetchImpl = options.fetchImpl ?? fetch;
@@ -62,11 +62,11 @@ 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
- if (env.DISCORD_WEBHOOK_URL) {
66
- return env.DISCORD_WEBHOOK_URL;
67
- }
68
65
  const paths = resolveDailyBriefPaths(env);
69
66
  const config = readDeliveryConfig(paths.configPath);
67
+ if (config?.enabled === false) {
68
+ return undefined;
69
+ }
70
70
  if (!config?.enabled || !config.webhookRef) {
71
71
  return undefined;
72
72
  }
@@ -1,6 +1,9 @@
1
- import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { loadSourceRegistry, resolveDailyBriefPaths } from "../config/index.js";
1
+ import { constants } from "node:fs";
2
+ import { access, readdir } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { readModelRuntimeConfig } from "../agent/model-runtime-config.js";
5
+ import { formatDateKey, getCredential, loadSourceRegistry, readDeliveryConfig, resolveDailyBriefPaths } from "../config/index.js";
6
+ import { agentRunArtifactPath, readSourceItems, sourceItemStorePath } from "../storage/index.js";
4
7
  export function evaluateWorkflowStatus(input) {
5
8
  if (input.coreFailure) {
6
9
  return {
@@ -50,46 +53,282 @@ export function createCoreWorkflowFailureNotification(failure) {
50
53
  }
51
54
  export async function getOperationalStatus(options = {}) {
52
55
  const date = options.date ?? new Date();
56
+ const systemTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
57
+ const dateKey = options.dateKey ?? formatDateKey(date, systemTimezone);
58
+ const resolvedPaths = resolveDailyBriefPaths(options.env);
59
+ const paths = {
60
+ ...resolvedPaths,
61
+ ...(options.dataHome ? { dataHome: options.dataHome } : {}),
62
+ ...(options.configPath ? { configPath: options.configPath } : {}),
63
+ ...(options.authPath ? { authPath: options.authPath } : {}),
64
+ ...(options.sourceRegistryPath ? { sourceRegistryPath: options.sourceRegistryPath } : {}),
65
+ ...(options.sourceItemRoot ? { sourceItemRoot: options.sourceItemRoot } : {}),
66
+ ...(options.agentRunRoot ? { agentRunRoot: options.agentRunRoot } : {}),
67
+ ...(options.archiveRoot ? { briefArchiveRoot: options.archiveRoot } : {})
68
+ };
69
+ const archivePath = briefArchivePath(date, paths.briefArchiveRoot, dateKey);
70
+ const sourceItemPath = sourceItemStorePath(date, paths.sourceItemRoot, dateKey);
71
+ const agentRunDirectory = dirname(agentRunArtifactPath(date, "status-probe", paths.agentRunRoot, dateKey));
72
+ const setup = {
73
+ config: await inspectConfig(paths.configPath),
74
+ sourceRegistry: await inspectSourceRegistry(paths.sourceRegistryPath),
75
+ model: inspectModel(paths.configPath, paths.authPath, options.env),
76
+ delivery: inspectDelivery(paths.configPath, paths.authPath),
77
+ data: await inspectDataDirectory(paths.dataHome)
78
+ };
79
+ const today = {
80
+ sourceItems: await inspectSourceItems(date, paths.sourceItemRoot, dateKey, sourceItemPath),
81
+ briefArchive: await inspectBriefArchive(archivePath),
82
+ agentRunArtifacts: await inspectAgentRunArtifacts(agentRunDirectory)
83
+ };
84
+ const nextAction = chooseNextAction(setup, today);
85
+ const workflow = summarizeOperationalStatus(dateKey, setup, today);
86
+ return {
87
+ ...workflow,
88
+ dateKey,
89
+ systemTimezone,
90
+ paths: {
91
+ home: paths.home,
92
+ dataHome: paths.dataHome,
93
+ configPath: paths.configPath,
94
+ sourceRegistryPath: paths.sourceRegistryPath,
95
+ authPath: paths.authPath,
96
+ sourceItemRoot: paths.sourceItemRoot,
97
+ agentRunRoot: paths.agentRunRoot,
98
+ archiveRoot: paths.briefArchiveRoot
99
+ },
100
+ setup,
101
+ today,
102
+ nextAction
103
+ };
104
+ }
105
+ function isMaterialPartialFailure(message) {
106
+ const normalized = message.toLowerCase();
107
+ const nonMaterialPatterns = ["missing transcript", "transcript missing", "rate limit", "parse failure"];
108
+ return !nonMaterialPatterns.some((pattern) => normalized.includes(pattern));
109
+ }
110
+ function briefArchivePath(date, root, dateKey) {
111
+ const datePart = dateKey ?? date.toISOString().slice(0, 10);
112
+ const [year, month] = datePart.split("-");
113
+ if (!year || !month) {
114
+ throw new Error(`Invalid archive date: ${datePart}`);
115
+ }
116
+ return join(root, year, month, `${datePart}.md`);
117
+ }
118
+ async function inspectConfig(path) {
119
+ return (await canRead(path))
120
+ ? { state: "ok", label: "Config file found", path }
121
+ : { state: "missing", label: "Config file missing", path };
122
+ }
123
+ async function inspectSourceRegistry(path) {
124
+ try {
125
+ const registry = await loadSourceRegistry(path);
126
+ const enabledCount = registry.sources.filter((source) => source.enabled).length;
127
+ return {
128
+ state: "ok",
129
+ label: "Source Registry valid",
130
+ path,
131
+ enabledCount,
132
+ totalCount: registry.sources.length,
133
+ detail: `${enabledCount}/${registry.sources.length} enabled`
134
+ };
135
+ }
136
+ catch (error) {
137
+ return {
138
+ state: (await canRead(path)) ? "invalid" : "missing",
139
+ label: (await canRead(path)) ? "Source Registry invalid" : "Source Registry missing",
140
+ path,
141
+ detail: error instanceof Error ? error.message : String(error)
142
+ };
143
+ }
144
+ }
145
+ function inspectModel(configPath, authPath, env) {
146
+ try {
147
+ const model = readModelRuntimeConfig(env, { configPath, authPath });
148
+ return {
149
+ state: model.ready ? "ok" : "not-ready",
150
+ label: model.ready ? "Model ready" : "Model not ready",
151
+ path: configPath,
152
+ provider: model.provider,
153
+ model: model.model,
154
+ ...(model.credentialRef ? { credentialRef: model.credentialRef } : {}),
155
+ ...(model.issues.length > 0 ? { detail: model.issues.join("; ") } : {})
156
+ };
157
+ }
158
+ catch (error) {
159
+ return {
160
+ state: "invalid",
161
+ label: "Model config invalid",
162
+ path: configPath,
163
+ detail: error instanceof Error ? error.message : String(error)
164
+ };
165
+ }
166
+ }
167
+ function inspectDelivery(configPath, authPath) {
168
+ try {
169
+ const delivery = readDeliveryConfig(configPath);
170
+ if (!delivery?.enabled) {
171
+ return { state: "disabled", label: "Discord delivery disabled", path: configPath };
172
+ }
173
+ if (!delivery.webhookRef) {
174
+ return { state: "not-ready", label: "Discord delivery missing webhook credential name", path: configPath };
175
+ }
176
+ const credential = getCredential(delivery.webhookRef, authPath);
177
+ if (!credential) {
178
+ return {
179
+ state: "not-ready",
180
+ label: "Discord delivery webhook credential missing",
181
+ path: authPath,
182
+ detail: delivery.webhookRef
183
+ };
184
+ }
185
+ if (credential.type !== "webhook" || credential.provider !== "discord") {
186
+ return {
187
+ state: "invalid",
188
+ label: "Discord delivery credential is not a webhook",
189
+ path: authPath,
190
+ detail: delivery.webhookRef
191
+ };
192
+ }
193
+ return { state: "ok", label: "Discord delivery ready", path: authPath, detail: delivery.webhookRef };
194
+ }
195
+ catch (error) {
196
+ return {
197
+ state: "invalid",
198
+ label: "Discord delivery config invalid",
199
+ path: configPath,
200
+ detail: error instanceof Error ? error.message : String(error)
201
+ };
202
+ }
203
+ }
204
+ async function inspectDataDirectory(path) {
205
+ try {
206
+ await access(path, constants.W_OK);
207
+ return { state: "ok", label: "Data directory writable", path };
208
+ }
209
+ catch (error) {
210
+ return {
211
+ state: (await canRead(path)) ? "not-ready" : "missing",
212
+ label: (await canRead(path)) ? "Data directory is not writable" : "Data directory missing",
213
+ path,
214
+ detail: error instanceof Error ? error.message : String(error)
215
+ };
216
+ }
217
+ }
218
+ async function inspectSourceItems(date, root, dateKey, path) {
53
219
  try {
54
- await loadSourceRegistry(options.sourceRegistryPath);
220
+ const items = await readSourceItems(date, root, dateKey);
221
+ return items.length > 0
222
+ ? { state: "ok", label: "Source Items found", path, itemCount: items.length, detail: `${items.length} item(s)` }
223
+ : { state: "missing", label: "No Source Items for today", path, itemCount: 0 };
55
224
  }
56
225
  catch (error) {
226
+ return {
227
+ state: "invalid",
228
+ label: "Source Items invalid",
229
+ path,
230
+ detail: error instanceof Error ? error.message : String(error)
231
+ };
232
+ }
233
+ }
234
+ async function inspectBriefArchive(path) {
235
+ return (await canRead(path))
236
+ ? { state: "ok", label: "Daily Brief archive found", path }
237
+ : { state: "missing", label: "No Daily Brief archive for today", path };
238
+ }
239
+ async function inspectAgentRunArtifacts(directory) {
240
+ try {
241
+ const files = (await readdir(directory)).filter((file) => file.endsWith(".json"));
242
+ return files.length > 0
243
+ ? {
244
+ state: "ok",
245
+ label: "Agent Run Artifacts found",
246
+ path: join(directory, files[files.length - 1] ?? ""),
247
+ fileCount: files.length,
248
+ detail: `${files.length} artifact(s)`
249
+ }
250
+ : { state: "missing", label: "No Agent Run Artifacts for today", path: directory, fileCount: 0 };
251
+ }
252
+ catch (error) {
253
+ if (isNodeError(error) && error.code === "ENOENT") {
254
+ return { state: "missing", label: "No Agent Run Artifacts for today", path: directory, fileCount: 0 };
255
+ }
256
+ return {
257
+ state: "invalid",
258
+ label: "Agent Run Artifacts unreadable",
259
+ path: directory,
260
+ detail: error instanceof Error ? error.message : String(error)
261
+ };
262
+ }
263
+ }
264
+ function summarizeOperationalStatus(dateKey, setup, today) {
265
+ if (setup.sourceRegistry.state === "missing" || setup.sourceRegistry.state === "invalid") {
57
266
  return evaluateWorkflowStatus({
58
267
  collectionResults: [],
59
268
  briefGenerated: false,
60
269
  coreFailure: {
61
270
  kind: "unreadable-source-registry",
62
- message: error instanceof Error ? error.message : String(error)
271
+ message: setup.sourceRegistry.detail ?? `${setup.sourceRegistry.label}: ${setup.sourceRegistry.path}`
63
272
  }
64
273
  });
65
274
  }
66
- const archivePath = briefArchivePath(date, options.archiveRoot ?? resolveDailyBriefPaths().briefArchiveRoot, options.dateKey);
67
- try {
68
- await readFile(archivePath, "utf8");
275
+ const setupReady = setup.config.state === "ok" &&
276
+ setup.model.state === "ok" &&
277
+ setup.data.state === "ok" &&
278
+ (setup.delivery.state === "ok" || setup.delivery.state === "disabled");
279
+ if (!setupReady) {
280
+ return {
281
+ health: "partial-failure",
282
+ message: "Daily Brief setup is incomplete.",
283
+ materialPartialFailures: []
284
+ };
69
285
  }
70
- catch {
286
+ if (today.briefArchive.state !== "ok") {
71
287
  return {
72
288
  health: "partial-failure",
73
- message: `No Daily Brief archived for ${options.dateKey ?? date.toISOString().slice(0, 10)} yet.`,
289
+ message: `No Daily Brief archived for ${dateKey} yet.`,
74
290
  materialPartialFailures: []
75
291
  };
76
292
  }
77
293
  return {
78
294
  health: "success",
79
- message: `Daily Brief archive exists for ${options.dateKey ?? date.toISOString().slice(0, 10)}.`,
295
+ message: `Daily Brief archive exists for ${dateKey}.`,
80
296
  materialPartialFailures: []
81
297
  };
82
298
  }
83
- function isMaterialPartialFailure(message) {
84
- const normalized = message.toLowerCase();
85
- const nonMaterialPatterns = ["missing transcript", "transcript missing", "rate limit", "parse failure"];
86
- return !nonMaterialPatterns.some((pattern) => normalized.includes(pattern));
299
+ function chooseNextAction(setup, today) {
300
+ if (setup.config.state !== "ok") {
301
+ return "daily-brief setup";
302
+ }
303
+ if (setup.sourceRegistry.state !== "ok") {
304
+ return setup.sourceRegistry.state === "missing" ? "daily-brief setup" : "daily-brief sources validate";
305
+ }
306
+ if (setup.sourceRegistry.enabledCount === 0) {
307
+ return "daily-brief sources list, then daily-brief sources enable <source-id>";
308
+ }
309
+ if (setup.model.state !== "ok") {
310
+ return "daily-brief setup";
311
+ }
312
+ if (setup.data.state !== "ok") {
313
+ return `Check data directory permissions: ${setup.data.path}`;
314
+ }
315
+ if (setup.delivery.state === "not-ready" || setup.delivery.state === "invalid") {
316
+ return "daily-brief setup";
317
+ }
318
+ if (today.briefArchive.state !== "ok") {
319
+ return "daily-brief run-once";
320
+ }
321
+ return "No action needed";
87
322
  }
88
- function briefArchivePath(date, root, dateKey) {
89
- const datePart = dateKey ?? date.toISOString().slice(0, 10);
90
- const [year, month] = datePart.split("-");
91
- if (!year || !month) {
92
- throw new Error(`Invalid archive date: ${datePart}`);
323
+ async function canRead(path) {
324
+ try {
325
+ await access(path, constants.R_OK);
326
+ return true;
93
327
  }
94
- return join(root, year, month, `${datePart}.md`);
328
+ catch {
329
+ return false;
330
+ }
331
+ }
332
+ function isNodeError(error) {
333
+ return error instanceof Error && "code" in error;
95
334
  }
@@ -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
 
@@ -39,31 +36,24 @@ npm run cli -- run-once
39
36
  npm run cli -- status
40
37
  ```
41
38
 
42
- `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.
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 setup readiness, today's run state, active paths, and the next suggested action.
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 in `config.yaml` and `auth.json`. If Discord Delivery is disabled or its webhook credential is missing, delivery is skipped with an explicit reason.
45
42
 
46
43
  ## Runtime configuration
47
44
 
48
- When the Operational CLI starts, it loads local `.env` values from the repository root if the file exists. Existing shell environment variables take precedence over `.env` values. The `.env` file is ignored by Git; use `.env.example` as the non-secret template.
49
-
50
45
  Installed operational paths can be adjusted through environment variables:
51
46
 
52
47
  - `DAILY_BRIEF_HOME`: user config directory. Defaults to `~/.daily-brief`.
53
48
  - `DAILY_BRIEF_DATA_HOME`: generated data directory. Defaults to `~/.daily-brief/data`.
54
- - `DAILY_BRIEF_DISCORD_TEMPLATE_PATH`: optional Discord notification template override. Defaults to the packaged Discord notification template.
55
- - `DISCORD_WEBHOOK_URL`: Discord webhook URL. If unset, Discord Delivery is skipped with an explicit reason.
56
49
 
57
50
  Model/provider and delivery configuration should normally be managed with:
58
51
 
59
52
  ```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
53
+ daily-brief setup
64
54
  ```
65
55
 
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.
56
+ Secrets live in `~/.daily-brief/auth.json`, never in environment variables, `sources.yaml`, `config.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`. Faux provider coverage belongs in test configuration files, not runtime environment variables.
67
57
 
68
58
  `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
59
 
@@ -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`, that lets `config.yaml` find the secret in `auth.json`. Do not put secrets in `sources.yaml`, `config.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:
@@ -101,7 +102,7 @@ Use status after setup, after manual runs, or when diagnosing failures:
101
102
  daily-brief status
102
103
  ```
103
104
 
104
- Status output is the operational surface for collection, analysis, archive, and delivery health.
105
+ Status output reports setup readiness, today's run state, active paths, system timezone, and the next suggested action. It is a local inspection command: it does not refresh OAuth, call an LLM, collect Sources, or send Discord notifications.
105
106
 
106
107
  ## Paths and Environment
107
108
 
@@ -109,10 +110,8 @@ The installed CLI uses user-home paths by default:
109
110
 
110
111
  - `DAILY_BRIEF_HOME`: configuration directory, default `~/.daily-brief`.
111
112
  - `DAILY_BRIEF_DATA_HOME`: generated data directory, default `~/.daily-brief/data`.
112
- - `DAILY_BRIEF_DISCORD_TEMPLATE_PATH`: optional Discord notification template override.
113
- - `DISCORD_WEBHOOK_URL`: optional Discord webhook URL.
114
113
 
115
- Repository checkouts may use a local `.env`; installed usage should normally rely on user configuration and credential commands.
114
+ Model access, Discord delivery, Source choices, and secrets are configured through `config.yaml`, `sources.yaml`, and `auth.json`, not environment variables.
116
115
 
117
116
  ## Upgrade
118
117
 
@@ -123,7 +122,7 @@ npm install -g @chenpengfei/daily-brief@latest
123
122
  "$(npm prefix -g)/bin/daily-brief" status
124
123
  ```
125
124
 
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.
125
+ 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
126
 
128
127
  ## Troubleshooting
129
128
 
@@ -149,14 +148,13 @@ daily-brief sources list
149
148
  If model access fails, run:
150
149
 
151
150
  ```bash
152
- daily-brief model status
151
+ daily-brief setup
153
152
  ```
154
153
 
155
154
  If Discord delivery fails or is skipped, run:
156
155
 
157
156
  ```bash
158
- daily-brief delivery status
159
- daily-brief delivery test
157
+ daily-brief setup
160
158
  ```
161
159
 
162
160
  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.3",
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",