@chenpengfei/daily-brief 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/src/agent/model-runtime-config.js +4 -76
- package/dist/src/cli.js +68 -90
- package/dist/src/config/credential-store.js +2 -14
- package/dist/src/config/model-config.js +8 -8
- package/dist/src/discord/delivery.js +1 -4
- package/dist/src/workflow/status.js +260 -21
- package/docs/operations.md +3 -7
- package/docs/user-manual.md +3 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
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.3 - 2026-06-06
|
|
6
|
+
|
|
7
|
+
Patch release for runtime configuration hardening and richer local status output.
|
|
8
|
+
|
|
9
|
+
### User-visible Changes
|
|
10
|
+
|
|
11
|
+
- Expands `daily-brief status` into a local readiness report with setup checks, today's run state, active paths, system timezone, and the next suggested action.
|
|
12
|
+
- Keeps model and Discord secrets in `auth.json` referenced by `config.yaml`, removing runtime reliance on `env:NAME`, `.env`, and `DISCORD_WEBHOOK_URL` credential paths for installed usage.
|
|
13
|
+
- Reports Discord delivery as explicitly skipped when delivery is disabled or the stored webhook credential is missing.
|
|
14
|
+
- Keeps `daily-brief run-once --date YYYY-MM-DD` available for manual backfills while making `daily-brief status` a no-flag inspection command.
|
|
15
|
+
|
|
16
|
+
### Installation and Upgrade Notes
|
|
17
|
+
|
|
18
|
+
- Upgrade with `npm install -g @chenpengfei/daily-brief@latest`.
|
|
19
|
+
- Run `daily-brief setup` after upgrading if your previous model or Discord configuration used environment-backed credential references.
|
|
20
|
+
- Scripted environments should write `config.yaml`, `sources.yaml`, and `auth.json` directly under `DAILY_BRIEF_HOME`; use `DAILY_BRIEF_DATA_HOME` when generated data should live elsewhere.
|
|
21
|
+
|
|
22
|
+
### Known Limitations
|
|
23
|
+
|
|
24
|
+
- `daily-brief status` is read-only and local: it does not collect Sources, call an LLM, refresh credentials, generate a brief, or send Discord notifications.
|
|
25
|
+
- Environment variables remain limited to path overrides for installed usage: `DAILY_BRIEF_HOME` and `DAILY_BRIEF_DATA_HOME`.
|
|
26
|
+
|
|
5
27
|
## 0.1.2 - 2026-06-04
|
|
6
28
|
|
|
7
29
|
Patch release for the simplified public CLI setup workflow.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { getOAuthApiKey, loginOpenAICodex } from "@earendil-works/pi-ai/oauth";
|
|
3
|
-
import {
|
|
3
|
+
import { getCredential, putCredential, readCredentialStore, readModelConfig, removeCredential, writeCredentialStore } from "../config/index.js";
|
|
4
4
|
import { resolveDailyBriefPaths } from "../config/paths.js";
|
|
5
5
|
export const defaultOAuthHelpers = {
|
|
6
6
|
getOAuthApiKey,
|
|
@@ -10,7 +10,7 @@ export function readModelRuntimeConfig(env = process.env, options = {}) {
|
|
|
10
10
|
const paths = resolveDailyBriefPaths(env);
|
|
11
11
|
const configPath = options.configPath ?? paths.configPath;
|
|
12
12
|
const authPath = options.authPath ?? paths.authPath;
|
|
13
|
-
const config =
|
|
13
|
+
const config = existsSync(configPath) ? readModelConfig(configPath) : undefined;
|
|
14
14
|
if (!config) {
|
|
15
15
|
return {
|
|
16
16
|
provider: "openai-codex",
|
|
@@ -20,7 +20,7 @@ export function readModelRuntimeConfig(env = process.env, options = {}) {
|
|
|
20
20
|
issues: [`Model config not found: ${configPath}. Run daily-brief setup or create config.yaml with model settings.`]
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
-
const issues = credentialIssues(config.provider, config.credentialRef,
|
|
23
|
+
const issues = credentialIssues(config.provider, config.credentialRef, authPath);
|
|
24
24
|
return {
|
|
25
25
|
provider: config.provider,
|
|
26
26
|
model: config.model,
|
|
@@ -38,10 +38,6 @@ export async function resolveModelApiKey(config, env = process.env, options = {}
|
|
|
38
38
|
if (!credentialRef) {
|
|
39
39
|
throw new Error(`credentialRef is required for ${config.provider}`);
|
|
40
40
|
}
|
|
41
|
-
if (isEnvCredentialRef(credentialRef)) {
|
|
42
|
-
const envName = envNameFromCredentialRef(credentialRef);
|
|
43
|
-
return env[envName]?.trim();
|
|
44
|
-
}
|
|
45
41
|
const authPath = options.authPath ?? resolveDailyBriefPaths(env).authPath;
|
|
46
42
|
const credential = getCredential(credentialRef, authPath);
|
|
47
43
|
if (!credential) {
|
|
@@ -108,17 +104,13 @@ export function statusModelCredentials(env = process.env, options = {}) {
|
|
|
108
104
|
secret: "<redacted>"
|
|
109
105
|
}));
|
|
110
106
|
}
|
|
111
|
-
function credentialIssues(provider, credentialRef,
|
|
107
|
+
function credentialIssues(provider, credentialRef, authPath) {
|
|
112
108
|
if (provider === "faux") {
|
|
113
109
|
return [];
|
|
114
110
|
}
|
|
115
111
|
if (!credentialRef) {
|
|
116
112
|
return [`credentialRef is required for ${provider}`];
|
|
117
113
|
}
|
|
118
|
-
if (isEnvCredentialRef(credentialRef)) {
|
|
119
|
-
const envName = envNameFromCredentialRef(credentialRef);
|
|
120
|
-
return env[envName]?.trim() ? [] : [`${envName} is required for credentialRef ${credentialRef}`];
|
|
121
|
-
}
|
|
122
114
|
const credential = getCredential(credentialRef, authPath);
|
|
123
115
|
if (!credential) {
|
|
124
116
|
return [`Credential not found: ${credentialRef}`];
|
|
@@ -128,65 +120,6 @@ function credentialIssues(provider, credentialRef, env, authPath) {
|
|
|
128
120
|
}
|
|
129
121
|
return [];
|
|
130
122
|
}
|
|
131
|
-
function readEnvModelConfig(env) {
|
|
132
|
-
const provider = env.DAILY_BRIEF_MODEL_PROVIDER?.trim();
|
|
133
|
-
if (!provider) {
|
|
134
|
-
return undefined;
|
|
135
|
-
}
|
|
136
|
-
const normalizedProvider = normalizeProvider(provider, env);
|
|
137
|
-
return {
|
|
138
|
-
provider: normalizedProvider,
|
|
139
|
-
model: env.DAILY_BRIEF_MODEL?.trim() || defaultModelForProvider(normalizedProvider),
|
|
140
|
-
...(env.DAILY_BRIEF_MODEL_BASE_URL?.trim() ? { baseUrl: env.DAILY_BRIEF_MODEL_BASE_URL.trim() } : {}),
|
|
141
|
-
...readEnvCredentialRef(env, normalizedProvider)
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
function normalizeProvider(value, env) {
|
|
145
|
-
const provider = value.trim().toLowerCase();
|
|
146
|
-
if (provider === "codex" || provider === "hermes") {
|
|
147
|
-
return "openai-codex";
|
|
148
|
-
}
|
|
149
|
-
if (provider === "faux") {
|
|
150
|
-
if (env.DAILY_BRIEF_ALLOW_FAUX_PROVIDER === "true") {
|
|
151
|
-
return provider;
|
|
152
|
-
}
|
|
153
|
-
throw new Error("DAILY_BRIEF_MODEL_PROVIDER=faux is only allowed when DAILY_BRIEF_ALLOW_FAUX_PROVIDER=true");
|
|
154
|
-
}
|
|
155
|
-
if (provider === "openai-codex" || provider === "openai" || provider === "deepseek" || provider === "openai-compatible") {
|
|
156
|
-
return provider;
|
|
157
|
-
}
|
|
158
|
-
throw new Error(`Unsupported DAILY_BRIEF_MODEL_PROVIDER: ${value}`);
|
|
159
|
-
}
|
|
160
|
-
function defaultModelForProvider(provider) {
|
|
161
|
-
if (provider === "openai-codex") {
|
|
162
|
-
return "gpt-5.5";
|
|
163
|
-
}
|
|
164
|
-
if (provider === "openai") {
|
|
165
|
-
return "gpt-4.1-mini";
|
|
166
|
-
}
|
|
167
|
-
if (provider === "deepseek") {
|
|
168
|
-
return "deepseek-chat";
|
|
169
|
-
}
|
|
170
|
-
if (provider === "openai-compatible") {
|
|
171
|
-
return "openai-compatible-model";
|
|
172
|
-
}
|
|
173
|
-
return "faux-daily-brief-renderer";
|
|
174
|
-
}
|
|
175
|
-
function defaultCredentialRefForProvider(provider) {
|
|
176
|
-
if (provider === "faux") {
|
|
177
|
-
return undefined;
|
|
178
|
-
}
|
|
179
|
-
if (provider === "openai") {
|
|
180
|
-
return "env:OPENAI_API_KEY";
|
|
181
|
-
}
|
|
182
|
-
if (provider === "deepseek") {
|
|
183
|
-
return "env:DEEPSEEK_API_KEY";
|
|
184
|
-
}
|
|
185
|
-
if (provider === "openai-compatible") {
|
|
186
|
-
return "env:OPENAI_API_KEY";
|
|
187
|
-
}
|
|
188
|
-
return "openai-codex.default";
|
|
189
|
-
}
|
|
190
123
|
function persistRefreshedOAuthCredential(ref, credential, credentials, authPath) {
|
|
191
124
|
const store = readCredentialStore(authPath);
|
|
192
125
|
const previous = store.credentials[ref];
|
|
@@ -198,11 +131,6 @@ function persistRefreshedOAuthCredential(ref, credential, credentials, authPath)
|
|
|
198
131
|
};
|
|
199
132
|
writeCredentialStore(store, authPath);
|
|
200
133
|
}
|
|
201
|
-
function readEnvCredentialRef(env, provider) {
|
|
202
|
-
const credentialRef = env.DAILY_BRIEF_MODEL_CREDENTIAL_REF?.trim() ||
|
|
203
|
-
defaultCredentialRefForProvider(provider);
|
|
204
|
-
return credentialRef ? { credentialRef } : {};
|
|
205
|
-
}
|
|
206
134
|
function promptForOAuth(io, message) {
|
|
207
135
|
if (!io.prompt) {
|
|
208
136
|
throw new Error(`OAuth login requires interactive input: ${message}`);
|
package/dist/src/cli.js
CHANGED
|
@@ -6,8 +6,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
import { stringify } from "yaml";
|
|
8
8
|
import { loginModelCredential, readModelRuntimeConfig, runOnce, toApiKeyCredential } from "./agent/index.js";
|
|
9
|
-
import {
|
|
10
|
-
import { defaultModelConfig, dateFromDateKey, formatDateKey, formatSourceRegistry, isEnvCredentialRef, loadSourceRegistry, putCredential, readDeliveryConfig, readDailyBriefConfig, readConfiguredTimezone, getCredential, resolveDailyBriefPaths, setSourceEnabled, validateSourceRegistry, writeDeliveryConfig, writeCredentialStore, writeModelConfig } from "./config/index.js";
|
|
9
|
+
import { defaultModelConfig, dateFromDateKey, formatDateKey, formatSourceRegistry, loadSourceRegistry, putCredential, readDeliveryConfig, readDailyBriefConfig, getCredential, resolveDailyBriefPaths, setSourceEnabled, validateSourceRegistry, writeDeliveryConfig, writeCredentialStore, writeModelConfig } from "./config/index.js";
|
|
11
10
|
import { getOperationalStatus } from "./workflow/index.js";
|
|
12
11
|
const consoleIo = {
|
|
13
12
|
interactive: Boolean(processStdin.isTTY && processStdout.isTTY),
|
|
@@ -32,11 +31,6 @@ const consoleIo = {
|
|
|
32
31
|
};
|
|
33
32
|
export async function runCli(args, io = consoleIo, env = process.env) {
|
|
34
33
|
const [command, subcommand, value] = args;
|
|
35
|
-
const workflowFlags = parseFlags(args.slice(1));
|
|
36
|
-
const options = {
|
|
37
|
-
...optionsFromEnv(env),
|
|
38
|
-
...readWorkflowDateOption(workflowFlags, env)
|
|
39
|
-
};
|
|
40
34
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
41
35
|
printHelp(io);
|
|
42
36
|
return;
|
|
@@ -46,6 +40,11 @@ export async function runCli(args, io = consoleIo, env = process.env) {
|
|
|
46
40
|
return;
|
|
47
41
|
}
|
|
48
42
|
if (command === "run-once") {
|
|
43
|
+
const workflowFlags = parseFlags(args.slice(1));
|
|
44
|
+
const options = {
|
|
45
|
+
...optionsFromEnv(env),
|
|
46
|
+
...readWorkflowDateOption(workflowFlags)
|
|
47
|
+
};
|
|
49
48
|
await assertWorkflowConfigured(options.sourceRegistryPath);
|
|
50
49
|
io.stdout("Daily Brief run started");
|
|
51
50
|
io.stdout(`Date: ${options.dateKey}`);
|
|
@@ -74,14 +73,16 @@ export async function runCli(args, io = consoleIo, env = process.env) {
|
|
|
74
73
|
return;
|
|
75
74
|
}
|
|
76
75
|
if (command === "status") {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
for (const failure of status.materialPartialFailures) {
|
|
80
|
-
io.stdout(`- ${failure}`);
|
|
76
|
+
if (args.length > 1) {
|
|
77
|
+
throw new Error("daily-brief status does not accept flags.");
|
|
81
78
|
}
|
|
79
|
+
const options = optionsFromEnv(env);
|
|
80
|
+
const status = await getOperationalStatus(options);
|
|
81
|
+
printOperationalStatus(status, io);
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
84
|
if (command === "sources") {
|
|
85
|
+
const options = optionsFromEnv(env);
|
|
85
86
|
await handleSourcesCommand(subcommand, value, io, options.sourceRegistryPath);
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
@@ -116,7 +117,52 @@ function countAgentStageEvents(events) {
|
|
|
116
117
|
function formatDeliveryStatus(delivery) {
|
|
117
118
|
return `${delivery.status}${delivery.reason ? ` (${delivery.reason})` : ""}`;
|
|
118
119
|
}
|
|
119
|
-
function
|
|
120
|
+
function printOperationalStatus(status, io) {
|
|
121
|
+
io.stdout("Daily Brief status");
|
|
122
|
+
io.stdout(`Health: ${status.health} - ${status.message}`);
|
|
123
|
+
io.stdout(`Date: ${status.dateKey}`);
|
|
124
|
+
io.stdout(`System timezone: ${status.systemTimezone}`);
|
|
125
|
+
io.stdout(`Home: ${status.paths.home}`);
|
|
126
|
+
io.stdout(`Data: ${status.paths.dataHome}`);
|
|
127
|
+
io.stdout("");
|
|
128
|
+
io.stdout("Setup readiness");
|
|
129
|
+
io.stdout(formatStatusCheck("Config", status.setup.config));
|
|
130
|
+
io.stdout(formatSourceRegistryCheck(status.setup.sourceRegistry));
|
|
131
|
+
io.stdout(formatModelCheck(status.setup.model));
|
|
132
|
+
io.stdout(formatStatusCheck("Delivery", status.setup.delivery));
|
|
133
|
+
io.stdout(formatStatusCheck("Data", status.setup.data));
|
|
134
|
+
io.stdout("");
|
|
135
|
+
io.stdout("Today run state");
|
|
136
|
+
io.stdout(formatStatusCheck("Source Items", status.today.sourceItems));
|
|
137
|
+
io.stdout(formatStatusCheck("Brief Archive", status.today.briefArchive));
|
|
138
|
+
io.stdout(formatStatusCheck("Agent Run Artifacts", status.today.agentRunArtifacts));
|
|
139
|
+
io.stdout("");
|
|
140
|
+
io.stdout(`Next: ${status.nextAction}`);
|
|
141
|
+
for (const failure of status.materialPartialFailures) {
|
|
142
|
+
io.stdout(`- ${failure}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function formatStatusCheck(label, check) {
|
|
146
|
+
const detail = check.detail ? ` - ${check.detail}` : "";
|
|
147
|
+
const path = check.path ? ` (${check.path})` : "";
|
|
148
|
+
return `- ${label}: ${check.state} - ${check.label}${detail}${path}`;
|
|
149
|
+
}
|
|
150
|
+
function formatSourceRegistryCheck(check) {
|
|
151
|
+
const counts = typeof check.enabledCount === "number" && typeof check.totalCount === "number"
|
|
152
|
+
? ` - ${check.enabledCount}/${check.totalCount} enabled`
|
|
153
|
+
: "";
|
|
154
|
+
const detail = check.detail && !counts.includes(check.detail) ? ` - ${check.detail}` : "";
|
|
155
|
+
const path = check.path ? ` (${check.path})` : "";
|
|
156
|
+
return `- Source Registry: ${check.state} - ${check.label}${counts}${detail}${path}`;
|
|
157
|
+
}
|
|
158
|
+
function formatModelCheck(check) {
|
|
159
|
+
const selected = check.provider && check.model ? ` - ${check.provider}/${check.model}` : "";
|
|
160
|
+
const credential = check.credentialRef ? ` - credential ${check.credentialRef}` : "";
|
|
161
|
+
const detail = check.detail ? ` - ${check.detail}` : "";
|
|
162
|
+
const path = check.path ? ` (${check.path})` : "";
|
|
163
|
+
return `- Model: ${check.state} - ${check.label}${selected}${credential}${detail}${path}`;
|
|
164
|
+
}
|
|
165
|
+
function readWorkflowDateOption(flags) {
|
|
120
166
|
if (flags.date) {
|
|
121
167
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(flags.date)) {
|
|
122
168
|
throw new Error("--date must use YYYY-MM-DD");
|
|
@@ -124,8 +170,7 @@ function readWorkflowDateOption(flags, env) {
|
|
|
124
170
|
return { date: dateFromDateKey(flags.date), dateKey: flags.date };
|
|
125
171
|
}
|
|
126
172
|
const now = new Date();
|
|
127
|
-
const
|
|
128
|
-
const timezone = readConfiguredTimezone(paths.configPath) ?? env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
173
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
129
174
|
return { date: now, dateKey: formatDateKey(now, timezone) };
|
|
130
175
|
}
|
|
131
176
|
async function assertWorkflowConfigured(sourceRegistryPath) {
|
|
@@ -208,14 +253,12 @@ async function handleSetupCommand(args, io, env) {
|
|
|
208
253
|
requireInteractiveSetup(io);
|
|
209
254
|
const paths = resolveDailyBriefPaths(env);
|
|
210
255
|
const existingConfig = readDailyBriefConfig(paths.configPath);
|
|
211
|
-
const timezone = await promptWithDefault(io, "Timezone", readConfiguredTimezone(paths.configPath) ?? env.TZ?.trim() ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC");
|
|
212
256
|
await mkdir(paths.home, { recursive: true });
|
|
213
257
|
await mkdir(paths.sourceItemRoot, { recursive: true });
|
|
214
258
|
await mkdir(paths.agentRunRoot, { recursive: true });
|
|
215
259
|
await mkdir(paths.briefArchiveRoot, { recursive: true });
|
|
216
260
|
await writeSetupBaseConfig(paths.configPath, {
|
|
217
261
|
...existingConfig,
|
|
218
|
-
timezone,
|
|
219
262
|
brief: normalizeBriefConfig(existingConfig.brief)
|
|
220
263
|
});
|
|
221
264
|
if (!(await exists(paths.authPath))) {
|
|
@@ -254,7 +297,6 @@ async function handleSetupCommand(args, io, env) {
|
|
|
254
297
|
const delivery = readDeliveryConfig(paths.configPath);
|
|
255
298
|
io.stdout(`Daily Brief home: ${paths.home}`);
|
|
256
299
|
io.stdout(`Daily Brief data: ${paths.dataHome}`);
|
|
257
|
-
io.stdout(`Timezone: ${timezone}`);
|
|
258
300
|
io.stdout(`Source Registry: ${paths.sourceRegistryPath}`);
|
|
259
301
|
io.stdout(`Model: ${readiness.provider}/${readiness.model}`);
|
|
260
302
|
io.stdout(`Model credential: ${readiness.ready ? "configured" : "missing"}`);
|
|
@@ -292,7 +334,7 @@ async function configureModelThroughSetup(io, env) {
|
|
|
292
334
|
const current = readDailyBriefConfig(paths.configPath).model ?? defaultModelConfig();
|
|
293
335
|
const provider = readProviderFlag(await promptWithDefault(io, "LLM Provider (openai-codex/openai/deepseek/openai-compatible)", current.provider));
|
|
294
336
|
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
|
|
337
|
+
io.stdout("Credential name identifies where Daily Brief finds the model secret in auth.json. Use the default unless you manage multiple credentials.");
|
|
296
338
|
const credentialRef = await promptWithDefault(io, "Model credential name", current.provider === provider ? current.credentialRef ?? defaultCredentialRef(provider) ?? "" : defaultCredentialRef(provider) ?? "");
|
|
297
339
|
const baseUrl = provider === "openai-compatible"
|
|
298
340
|
? await promptWithDefault(io, "Base URL", current.provider === provider ? current.baseUrl ?? "" : "")
|
|
@@ -335,25 +377,6 @@ async function maybeConfigureModelCredential(input) {
|
|
|
335
377
|
}
|
|
336
378
|
return;
|
|
337
379
|
}
|
|
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;
|
|
343
|
-
}
|
|
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}`);
|
|
354
|
-
}
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
380
|
if (getCredential(input.credentialRef, paths.authPath)) {
|
|
358
381
|
input.io.stdout("Model credential: configured");
|
|
359
382
|
return;
|
|
@@ -499,13 +522,13 @@ function defaultCredentialRef(provider) {
|
|
|
499
522
|
return undefined;
|
|
500
523
|
}
|
|
501
524
|
if (provider === "openai") {
|
|
502
|
-
return "
|
|
525
|
+
return "openai.default";
|
|
503
526
|
}
|
|
504
527
|
if (provider === "deepseek") {
|
|
505
|
-
return "
|
|
528
|
+
return "deepseek.default";
|
|
506
529
|
}
|
|
507
530
|
if (provider === "openai-compatible") {
|
|
508
|
-
return "
|
|
531
|
+
return "openai-compatible.default";
|
|
509
532
|
}
|
|
510
533
|
return defaultModelConfig().credentialRef;
|
|
511
534
|
}
|
|
@@ -518,63 +541,19 @@ async function promptWithDefault(io, label, defaultValue) {
|
|
|
518
541
|
}
|
|
519
542
|
function optionsFromEnv(env) {
|
|
520
543
|
const paths = resolveDailyBriefPaths(env);
|
|
521
|
-
const discordWebhookUrl = resolveConfiguredWebhookUrl(env);
|
|
522
544
|
return {
|
|
545
|
+
env,
|
|
546
|
+
dataHome: paths.dataHome,
|
|
547
|
+
configPath: paths.configPath,
|
|
548
|
+
authPath: paths.authPath,
|
|
523
549
|
sourceRegistryPath: paths.sourceRegistryPath,
|
|
524
550
|
sourceItemRoot: paths.sourceItemRoot,
|
|
525
551
|
agentRunRoot: paths.agentRunRoot,
|
|
526
552
|
archiveRoot: paths.briefArchiveRoot,
|
|
527
553
|
modelRuntimeEnv: env,
|
|
528
|
-
discordEnv: env
|
|
529
|
-
...(discordWebhookUrl ? { discordWebhookUrl } : {}),
|
|
530
|
-
...(env.DAILY_BRIEF_DISCORD_TEMPLATE_PATH ? { discordTemplatePath: env.DAILY_BRIEF_DISCORD_TEMPLATE_PATH } : {})
|
|
554
|
+
discordEnv: env
|
|
531
555
|
};
|
|
532
556
|
}
|
|
533
|
-
export async function loadDotenvFile(path = ".env", env = process.env) {
|
|
534
|
-
let contents;
|
|
535
|
-
try {
|
|
536
|
-
contents = await readFile(path, "utf8");
|
|
537
|
-
}
|
|
538
|
-
catch (error) {
|
|
539
|
-
if (isNodeError(error) && error.code === "ENOENT") {
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
throw error;
|
|
543
|
-
}
|
|
544
|
-
for (const line of contents.split("\n")) {
|
|
545
|
-
const entry = parseDotenvLine(line);
|
|
546
|
-
if (!entry || env[entry.key] !== undefined) {
|
|
547
|
-
continue;
|
|
548
|
-
}
|
|
549
|
-
env[entry.key] = entry.value;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
function parseDotenvLine(line) {
|
|
553
|
-
const trimmed = line.trim();
|
|
554
|
-
if (trimmed.length === 0 || trimmed.startsWith("#")) {
|
|
555
|
-
return undefined;
|
|
556
|
-
}
|
|
557
|
-
const equalsIndex = trimmed.indexOf("=");
|
|
558
|
-
if (equalsIndex <= 0) {
|
|
559
|
-
return undefined;
|
|
560
|
-
}
|
|
561
|
-
const key = trimmed.slice(0, equalsIndex).trim();
|
|
562
|
-
const rawValue = trimmed.slice(equalsIndex + 1).trim();
|
|
563
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
564
|
-
return undefined;
|
|
565
|
-
}
|
|
566
|
-
return { key, value: unquoteDotenvValue(rawValue) };
|
|
567
|
-
}
|
|
568
|
-
function unquoteDotenvValue(value) {
|
|
569
|
-
if (value.length >= 2) {
|
|
570
|
-
const quote = value[0];
|
|
571
|
-
const last = value[value.length - 1];
|
|
572
|
-
if ((quote === "\"" || quote === "'") && last === quote) {
|
|
573
|
-
return value.slice(1, -1);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
return value;
|
|
577
|
-
}
|
|
578
557
|
function isNodeError(error) {
|
|
579
558
|
return error instanceof Error && "code" in error;
|
|
580
559
|
}
|
|
@@ -601,7 +580,6 @@ isCliEntrypoint(process.argv[1])
|
|
|
601
580
|
if (!isEntrypoint) {
|
|
602
581
|
return false;
|
|
603
582
|
}
|
|
604
|
-
await loadDotenvFile();
|
|
605
583
|
await runCli(process.argv.slice(2));
|
|
606
584
|
return true;
|
|
607
585
|
})
|
|
@@ -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 (
|
|
68
|
-
throw new Error(`Credential reference ${ref} is
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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}`);
|
|
@@ -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: "
|
|
25
|
+
return { status: "skipped", reason: "Discord delivery webhook is not configured" };
|
|
26
26
|
}
|
|
27
27
|
try {
|
|
28
28
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
@@ -67,9 +67,6 @@ export function resolveConfiguredWebhookUrl(env = process.env) {
|
|
|
67
67
|
if (config?.enabled === false) {
|
|
68
68
|
return undefined;
|
|
69
69
|
}
|
|
70
|
-
if (env.DISCORD_WEBHOOK_URL) {
|
|
71
|
-
return env.DISCORD_WEBHOOK_URL;
|
|
72
|
-
}
|
|
73
70
|
if (!config?.enabled || !config.webhookRef) {
|
|
74
71
|
return undefined;
|
|
75
72
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
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:
|
|
271
|
+
message: setup.sourceRegistry.detail ?? `${setup.sourceRegistry.label}: ${setup.sourceRegistry.path}`
|
|
63
272
|
}
|
|
64
273
|
});
|
|
65
274
|
}
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
286
|
+
if (today.briefArchive.state !== "ok") {
|
|
71
287
|
return {
|
|
72
288
|
health: "partial-failure",
|
|
73
|
-
message: `No Daily Brief archived for ${
|
|
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 ${
|
|
295
|
+
message: `Daily Brief archive exists for ${dateKey}.`,
|
|
80
296
|
materialPartialFailures: []
|
|
81
297
|
};
|
|
82
298
|
}
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
328
|
+
catch {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function isNodeError(error) {
|
|
333
|
+
return error instanceof Error && "code" in error;
|
|
95
334
|
}
|
package/docs/operations.md
CHANGED
|
@@ -36,20 +36,16 @@ npm run cli -- run-once
|
|
|
36
36
|
npm run cli -- status
|
|
37
37
|
```
|
|
38
38
|
|
|
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
|
|
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.
|
|
40
40
|
|
|
41
|
-
Discord Delivery uses the configured credential reference
|
|
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.
|
|
42
42
|
|
|
43
43
|
## Runtime configuration
|
|
44
44
|
|
|
45
|
-
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.
|
|
46
|
-
|
|
47
45
|
Installed operational paths can be adjusted through environment variables:
|
|
48
46
|
|
|
49
47
|
- `DAILY_BRIEF_HOME`: user config directory. Defaults to `~/.daily-brief`.
|
|
50
48
|
- `DAILY_BRIEF_DATA_HOME`: generated data directory. Defaults to `~/.daily-brief/data`.
|
|
51
|
-
- `DAILY_BRIEF_DISCORD_TEMPLATE_PATH`: optional Discord notification template override. Defaults to the packaged Discord notification template.
|
|
52
|
-
- `DISCORD_WEBHOOK_URL`: Discord webhook URL. If unset, Discord Delivery is skipped with an explicit reason.
|
|
53
49
|
|
|
54
50
|
Model/provider and delivery configuration should normally be managed with:
|
|
55
51
|
|
|
@@ -57,7 +53,7 @@ Model/provider and delivery configuration should normally be managed with:
|
|
|
57
53
|
daily-brief setup
|
|
58
54
|
```
|
|
59
55
|
|
|
60
|
-
Secrets live in `~/.daily-brief/auth.json
|
|
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.
|
|
61
57
|
|
|
62
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.
|
|
63
59
|
|
package/docs/user-manual.md
CHANGED
|
@@ -61,7 +61,7 @@ Agent Stages require an LLM Provider Configuration before real Daily Brief gener
|
|
|
61
61
|
daily-brief setup
|
|
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
|
|
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.
|
|
65
65
|
|
|
66
66
|
## Configure Delivery
|
|
67
67
|
|
|
@@ -102,7 +102,7 @@ Use status after setup, after manual runs, or when diagnosing failures:
|
|
|
102
102
|
daily-brief status
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
Status output
|
|
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.
|
|
106
106
|
|
|
107
107
|
## Paths and Environment
|
|
108
108
|
|
|
@@ -110,10 +110,8 @@ The installed CLI uses user-home paths by default:
|
|
|
110
110
|
|
|
111
111
|
- `DAILY_BRIEF_HOME`: configuration directory, default `~/.daily-brief`.
|
|
112
112
|
- `DAILY_BRIEF_DATA_HOME`: generated data directory, default `~/.daily-brief/data`.
|
|
113
|
-
- `DAILY_BRIEF_DISCORD_TEMPLATE_PATH`: optional Discord notification template override.
|
|
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.
|
|
115
113
|
|
|
116
|
-
|
|
114
|
+
Model access, Discord delivery, Source choices, and secrets are configured through `config.yaml`, `sources.yaml`, and `auth.json`, not environment variables.
|
|
117
115
|
|
|
118
116
|
## Upgrade
|
|
119
117
|
|