@chenpengfei/daily-brief 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +1 -2
- package/dist/src/agent/daily-brief-agent.js +14 -0
- package/dist/src/agent/model-runtime-config.js +5 -77
- package/dist/src/cli.js +315 -252
- package/dist/src/config/credential-store.js +2 -14
- package/dist/src/config/model-config.js +8 -8
- package/dist/src/config/source-registry.js +30 -7
- package/dist/src/discord/delivery.js +4 -4
- package/dist/src/workflow/status.js +260 -21
- package/docs/operations.md +7 -17
- package/docs/user-manual.md +23 -25
- package/package.json +1 -1
|
@@ -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}`);
|
|
@@ -37,12 +37,35 @@ export async function setSourceEnabled(id, enabled, path = resolveDailyBriefPath
|
|
|
37
37
|
}
|
|
38
38
|
export function formatSourceRegistry(registry) {
|
|
39
39
|
if (registry.sources.length === 0) {
|
|
40
|
-
return
|
|
40
|
+
return [
|
|
41
|
+
"No Sources configured.",
|
|
42
|
+
"",
|
|
43
|
+
"Add Sources by editing the Source Registry, then run:",
|
|
44
|
+
" daily-brief sources edit",
|
|
45
|
+
" daily-brief sources validate"
|
|
46
|
+
].join("\n");
|
|
41
47
|
}
|
|
42
|
-
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.
|
|
48
|
+
const rows = registry.sources.map((source) => ({
|
|
49
|
+
sourceId: source.id,
|
|
50
|
+
status: source.enabled ? "enabled" : "disabled",
|
|
51
|
+
platform: source.platform,
|
|
52
|
+
adapter: source.adapter,
|
|
53
|
+
target: source.target
|
|
54
|
+
}));
|
|
55
|
+
const widths = {
|
|
56
|
+
sourceId: Math.max("SOURCE ID".length, ...rows.map((row) => row.sourceId.length)),
|
|
57
|
+
status: Math.max("STATUS".length, ...rows.map((row) => row.status.length)),
|
|
58
|
+
platform: Math.max("PLATFORM".length, ...rows.map((row) => row.platform.length)),
|
|
59
|
+
adapter: Math.max("ADAPTER".length, ...rows.map((row) => row.adapter.length))
|
|
60
|
+
};
|
|
61
|
+
const firstSourceId = rows[0]?.sourceId ?? "<source-id>";
|
|
62
|
+
const lines = [
|
|
63
|
+
`${"SOURCE ID".padEnd(widths.sourceId)} ${"STATUS".padEnd(widths.status)} ${"PLATFORM".padEnd(widths.platform)} ${"ADAPTER".padEnd(widths.adapter)} TARGET`,
|
|
64
|
+
...rows.map((row) => `${row.sourceId.padEnd(widths.sourceId)} ${row.status.padEnd(widths.status)} ${row.platform.padEnd(widths.platform)} ${row.adapter.padEnd(widths.adapter)} ${row.target}`),
|
|
65
|
+
"",
|
|
66
|
+
"Use SOURCE ID with:",
|
|
67
|
+
` daily-brief sources enable ${firstSourceId}`,
|
|
68
|
+
` daily-brief sources disable ${firstSourceId}`
|
|
69
|
+
];
|
|
70
|
+
return lines.join("\n");
|
|
48
71
|
}
|
|
@@ -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;
|
|
@@ -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 {
|
|
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
|
@@ -13,21 +13,18 @@ daily-brief status
|
|
|
13
13
|
Development usage from a repository checkout uses `npm run cli --`:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npm run cli -- collect
|
|
17
|
-
npm run cli -- generate
|
|
18
|
-
npm run cli -- deliver
|
|
19
16
|
npm run cli -- status
|
|
20
17
|
npm run cli -- run-once
|
|
18
|
+
npm run cli -- --version
|
|
21
19
|
```
|
|
22
20
|
|
|
23
21
|
Expected local cadence:
|
|
24
22
|
|
|
25
|
-
-
|
|
26
|
-
- 07:00 local time: run `run-once` or `generate` followed by `deliver`.
|
|
23
|
+
- Run `daily-brief run-once` once per Daily Brief Cadence window.
|
|
27
24
|
|
|
28
25
|
Scheduler integration is intentionally deployment-neutral. A local cron, launchd job, systemd timer, GitHub Actions workflow, or another scheduler can call the commands above; the repository does not bind the MVP to a specific host.
|
|
29
26
|
|
|
30
|
-
`run-once` executes collection, brief generation/archive, and Discord delivery in order. Source Item writes are deduplicated by Source Item id and content hash within the daily JSONL store, and Daily Brief generation merges repeated mentions into one multi-citation Signal, so rerunning the workflow for the same Collection Window does not duplicate equivalent Signals.
|
|
27
|
+
`run-once` executes collection, brief generation/archive, and Discord delivery in order. It prints human-readable progress for Source collection, Agent Stage execution, archiving, and delivery while the run is active. Source Item writes are deduplicated by Source Item id and content hash within the daily JSONL store, and Daily Brief generation merges repeated mentions into one multi-citation Signal, so rerunning the workflow for the same Collection Window does not duplicate equivalent Signals.
|
|
31
28
|
|
|
32
29
|
## Manual run
|
|
33
30
|
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/docs/user-manual.md
CHANGED
|
@@ -31,10 +31,14 @@ Run setup after installing the package:
|
|
|
31
31
|
|
|
32
32
|
If npm's global bin directory is in `PATH`, `daily-brief setup` is equivalent.
|
|
33
33
|
|
|
34
|
-
Setup creates user configuration and generated-data directories, initializes the Source Registry from the packaged example, prepares credential storage, and reports readiness. Setup does not collect Sources, call an LLM, generate a Daily Brief, or send a delivery notification.
|
|
34
|
+
Setup is an interactive wizard. It creates user configuration and generated-data directories, initializes the Source Registry from the packaged example, prepares credential storage, guides LLM Provider setup, offers optional Discord Delivery setup, and reports readiness. Setup does not collect Sources, call an LLM, generate a Daily Brief, or send a delivery notification.
|
|
35
|
+
|
|
36
|
+
Yes/no prompts show the default in brackets: `[y/N]` means pressing Enter chooses no, and `[Y/n]` means pressing Enter chooses yes. You can type `y`, `yes`, `n`, or `no`.
|
|
35
37
|
|
|
36
38
|
By default, configuration files live under `~/.daily-brief/`, and generated data lives under `~/.daily-brief/data/`.
|
|
37
39
|
|
|
40
|
+
`daily-brief setup` requires interactive input. For CI or scripted setup, write `config.yaml`, `sources.yaml`, and `auth.json` directly under `DAILY_BRIEF_HOME`, and use `DAILY_BRIEF_DATA_HOME` when generated data should live elsewhere.
|
|
41
|
+
|
|
38
42
|
## Configure Sources
|
|
39
43
|
|
|
40
44
|
Sources are manually controlled. The agent processes configured Sources, but it does not autonomously add or remove them.
|
|
@@ -54,25 +58,20 @@ Use `sources validate` after editing the Source Registry.
|
|
|
54
58
|
Agent Stages require an LLM Provider Configuration before real Daily Brief generation.
|
|
55
59
|
|
|
56
60
|
```bash
|
|
57
|
-
daily-brief
|
|
58
|
-
daily-brief model status
|
|
59
|
-
daily-brief model login
|
|
60
|
-
daily-brief model logout
|
|
61
|
+
daily-brief setup
|
|
61
62
|
```
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
Use the setup wizard to choose the provider, model, credential name, and optional login/API key storage path. The credential name is a stable label, such as `openai.default`, 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
|
|
71
|
-
daily-brief delivery status
|
|
72
|
-
daily-brief delivery test
|
|
71
|
+
daily-brief setup
|
|
73
72
|
```
|
|
74
73
|
|
|
75
|
-
If Discord Delivery is disabled or no webhook is configured, generation can still run and will report skipped delivery explicitly.
|
|
74
|
+
Setup asks whether to enable Discord Delivery. If Discord Delivery is disabled or no webhook is configured, generation can still run and will report skipped delivery explicitly.
|
|
76
75
|
|
|
77
76
|
## Run Daily Brief
|
|
78
77
|
|
|
@@ -82,17 +81,19 @@ For a full manual run:
|
|
|
82
81
|
daily-brief run-once
|
|
83
82
|
```
|
|
84
83
|
|
|
85
|
-
|
|
84
|
+
`run-once` performs collection, Agent Stage generation, archive writing, and delivery once. It prints human-readable progress while the run is active so you can see whether it is collecting Sources, waiting on Agent Stages, archiving, or delivering.
|
|
85
|
+
|
|
86
|
+
The expected local cadence is a single daily run around the intended delivery time. Daily Brief does not include a built-in scheduler; use an external scheduler to invoke `daily-brief run-once`.
|
|
87
|
+
|
|
88
|
+
## Version
|
|
89
|
+
|
|
90
|
+
To report the installed CLI version:
|
|
86
91
|
|
|
87
92
|
```bash
|
|
88
|
-
daily-brief
|
|
89
|
-
daily-brief
|
|
90
|
-
daily-brief deliver
|
|
91
|
-
daily-brief status
|
|
93
|
+
daily-brief version
|
|
94
|
+
daily-brief --version
|
|
92
95
|
```
|
|
93
96
|
|
|
94
|
-
The expected local cadence is collection at 06:00 and generation or delivery at 07:00 local time. Daily Brief does not include a built-in scheduler; use an external scheduler to invoke the CLI.
|
|
95
|
-
|
|
96
97
|
## Inspect Status
|
|
97
98
|
|
|
98
99
|
Use status after setup, after manual runs, or when diagnosing failures:
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|