@chenpengfei/daily-brief 0.1.0

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +28 -0
  4. package/config/sources.example.yaml +20 -0
  5. package/dist/src/adapters/fixture.js +70 -0
  6. package/dist/src/adapters/github-trending.js +183 -0
  7. package/dist/src/adapters/index.js +5 -0
  8. package/dist/src/adapters/rss.js +156 -0
  9. package/dist/src/adapters/types.js +1 -0
  10. package/dist/src/adapters/x.js +115 -0
  11. package/dist/src/agent/daily-brief-agent.js +350 -0
  12. package/dist/src/agent/index.js +10 -0
  13. package/dist/src/agent/model-runtime-config.js +221 -0
  14. package/dist/src/agent/model-stage-runtime.js +63 -0
  15. package/dist/src/agent/signal-narrative.js +247 -0
  16. package/dist/src/agent/signal-selection-ranking.js +276 -0
  17. package/dist/src/agent/source-grounding-audit.js +148 -0
  18. package/dist/src/agent/source-grounding-repair.js +159 -0
  19. package/dist/src/agent/source-item-understanding.js +206 -0
  20. package/dist/src/agent/stage-contracts.js +205 -0
  21. package/dist/src/agent/stage-runner.js +66 -0
  22. package/dist/src/brief/daily-brief.js +234 -0
  23. package/dist/src/brief/index.js +1 -0
  24. package/dist/src/cli.js +531 -0
  25. package/dist/src/collection/collect.js +67 -0
  26. package/dist/src/collection/index.js +1 -0
  27. package/dist/src/config/credential-store.js +169 -0
  28. package/dist/src/config/date-key.js +25 -0
  29. package/dist/src/config/index.js +5 -0
  30. package/dist/src/config/model-config.js +123 -0
  31. package/dist/src/config/paths.js +20 -0
  32. package/dist/src/config/source-registry.js +48 -0
  33. package/dist/src/discord/delivery.js +84 -0
  34. package/dist/src/discord/index.js +1 -0
  35. package/dist/src/domain/index.js +2 -0
  36. package/dist/src/domain/source-item.js +21 -0
  37. package/dist/src/domain/source.js +93 -0
  38. package/dist/src/storage/agent-run-artifact.js +44 -0
  39. package/dist/src/storage/brief-archive.js +17 -0
  40. package/dist/src/storage/index.js +3 -0
  41. package/dist/src/storage/source-item-store.js +63 -0
  42. package/dist/src/workflow/index.js +1 -0
  43. package/dist/src/workflow/status.js +95 -0
  44. package/docs/operations.md +74 -0
  45. package/docs/release-workflow.md +220 -0
  46. package/docs/user-manual.md +146 -0
  47. package/package.json +65 -0
  48. package/templates/daily-brief.md +9 -0
  49. package/templates/discord-notification.md +7 -0
@@ -0,0 +1,169 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { resolveDailyBriefPaths } from "./paths.js";
4
+ export function readCredentialStore(path = resolveDailyBriefPaths().authPath) {
5
+ if (!existsSync(path)) {
6
+ return { credentials: {} };
7
+ }
8
+ const payload = JSON.parse(readFileSync(path, "utf8"));
9
+ if (!isRecord(payload)) {
10
+ throw new Error("auth.json must be a JSON object");
11
+ }
12
+ return parseCredentialStore(payload);
13
+ }
14
+ export function writeCredentialStore(store, path = resolveDailyBriefPaths().authPath) {
15
+ mkdirSync(dirname(path), { recursive: true });
16
+ writeFileSync(path, `${JSON.stringify(parseCredentialStore(store), null, 2)}\n`, {
17
+ mode: 0o600
18
+ });
19
+ }
20
+ export function putCredential(ref, credential, path = resolveDailyBriefPaths().authPath) {
21
+ assertStoredCredentialRef(ref);
22
+ const now = new Date().toISOString();
23
+ const store = readCredentialStore(path);
24
+ const previous = store.credentials[ref];
25
+ store.credentials[ref] = {
26
+ ...credential,
27
+ createdAt: previous?.createdAt ?? credential.createdAt ?? now,
28
+ updatedAt: now
29
+ };
30
+ writeCredentialStore(store, path);
31
+ return store;
32
+ }
33
+ export function removeCredential(ref, path = resolveDailyBriefPaths().authPath) {
34
+ assertStoredCredentialRef(ref);
35
+ const store = readCredentialStore(path);
36
+ delete store.credentials[ref];
37
+ writeCredentialStore(store, path);
38
+ return store;
39
+ }
40
+ export function getCredential(ref, path = resolveDailyBriefPaths().authPath) {
41
+ if (isEnvCredentialRef(ref)) {
42
+ return undefined;
43
+ }
44
+ assertStoredCredentialRef(ref);
45
+ return readCredentialStore(path).credentials[ref];
46
+ }
47
+ export function redactCredentialStore(store) {
48
+ return Object.fromEntries(Object.entries(store.credentials).map(([ref, credential]) => [
49
+ ref,
50
+ {
51
+ type: credential.type,
52
+ provider: credential.provider,
53
+ secret: "<redacted>"
54
+ }
55
+ ]));
56
+ }
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
+ 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`);
69
+ }
70
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(ref)) {
71
+ throw new Error(`Invalid credentialRef: ${ref}`);
72
+ }
73
+ }
74
+ function parseCredentialStore(value) {
75
+ const credentials = value.credentials;
76
+ if (credentials === undefined) {
77
+ return { credentials: {} };
78
+ }
79
+ if (!isRecord(credentials)) {
80
+ throw new Error("auth.json credentials must be a mapping");
81
+ }
82
+ return {
83
+ credentials: Object.fromEntries(Object.entries(credentials).map(([ref, credential]) => {
84
+ assertStoredCredentialRef(ref);
85
+ if (!isRecord(credential)) {
86
+ throw new Error(`Credential ${ref} must be a mapping`);
87
+ }
88
+ return [ref, parseCredentialRecord(ref, credential)];
89
+ }))
90
+ };
91
+ }
92
+ function parseCredentialRecord(ref, value) {
93
+ if (value.type === "api-key") {
94
+ const provider = readProvider(value.provider, ref);
95
+ const apiKey = readString(value.apiKey, `Credential ${ref} apiKey`);
96
+ return {
97
+ type: "api-key",
98
+ provider,
99
+ apiKey,
100
+ ...readTimestamps(value)
101
+ };
102
+ }
103
+ if (value.type === "oauth") {
104
+ const provider = readProvider(value.provider, ref);
105
+ if (provider !== "openai-codex") {
106
+ throw new Error(`Credential ${ref} OAuth provider must be openai-codex`);
107
+ }
108
+ if (!isRecord(value.credentials)) {
109
+ throw new Error(`Credential ${ref} credentials must be a mapping`);
110
+ }
111
+ const credentials = value.credentials;
112
+ const refresh = readString(credentials.refresh, `Credential ${ref} refresh`);
113
+ const access = readString(credentials.access, `Credential ${ref} access`);
114
+ const expires = typeof credentials.expires === "number" ? credentials.expires : undefined;
115
+ if (!expires) {
116
+ throw new Error(`Credential ${ref} expires must be a number`);
117
+ }
118
+ return {
119
+ type: "oauth",
120
+ provider,
121
+ credentials: {
122
+ ...credentials,
123
+ refresh,
124
+ access,
125
+ expires
126
+ },
127
+ ...readTimestamps(value)
128
+ };
129
+ }
130
+ if (value.type === "webhook") {
131
+ const provider = readString(value.provider, `Credential ${ref} provider`);
132
+ if (provider !== "discord") {
133
+ throw new Error(`Credential ${ref} webhook provider must be discord`);
134
+ }
135
+ return {
136
+ type: "webhook",
137
+ provider,
138
+ webhookUrl: readString(value.webhookUrl, `Credential ${ref} webhookUrl`),
139
+ ...readTimestamps(value)
140
+ };
141
+ }
142
+ throw new Error(`Credential ${ref} type must be api-key, oauth, or webhook`);
143
+ }
144
+ function readProvider(value, ref) {
145
+ const provider = readString(value, `Credential ${ref} provider`).toLowerCase();
146
+ if (provider === "faux" ||
147
+ provider === "openai" ||
148
+ provider === "openai-codex" ||
149
+ provider === "deepseek" ||
150
+ provider === "openai-compatible") {
151
+ return provider;
152
+ }
153
+ throw new Error(`Credential ${ref} has unsupported provider: ${provider}`);
154
+ }
155
+ function readTimestamps(value) {
156
+ return {
157
+ ...(typeof value.createdAt === "string" ? { createdAt: value.createdAt } : {}),
158
+ ...(typeof value.updatedAt === "string" ? { updatedAt: value.updatedAt } : {})
159
+ };
160
+ }
161
+ function readString(value, field) {
162
+ if (typeof value !== "string" || value.trim().length === 0) {
163
+ throw new Error(`${field} is required`);
164
+ }
165
+ return value.trim();
166
+ }
167
+ function isRecord(value) {
168
+ return typeof value === "object" && value !== null && !Array.isArray(value);
169
+ }
@@ -0,0 +1,25 @@
1
+ export function formatDateKey(date, timeZone = "UTC") {
2
+ const parts = new Intl.DateTimeFormat("en-US", {
3
+ timeZone,
4
+ year: "numeric",
5
+ month: "2-digit",
6
+ day: "2-digit"
7
+ }).formatToParts(date);
8
+ const year = readPart(parts, "year");
9
+ const month = readPart(parts, "month");
10
+ const day = readPart(parts, "day");
11
+ return `${year}-${month}-${day}`;
12
+ }
13
+ export function dateFromDateKey(dateKey) {
14
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(dateKey)) {
15
+ throw new Error(`Invalid date key: ${dateKey}`);
16
+ }
17
+ return new Date(`${dateKey}T00:00:00.000Z`);
18
+ }
19
+ function readPart(parts, type) {
20
+ const part = parts.find((entry) => entry.type === type)?.value;
21
+ if (!part) {
22
+ throw new Error(`Unable to format date part: ${type}`);
23
+ }
24
+ return part;
25
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./credential-store.js";
2
+ export * from "./date-key.js";
3
+ export * from "./model-config.js";
4
+ export * from "./paths.js";
5
+ export * from "./source-registry.js";
@@ -0,0 +1,123 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { parse, stringify } from "yaml";
4
+ import { resolveDailyBriefPaths } from "./paths.js";
5
+ export function readDailyBriefConfig(path = resolveDailyBriefPaths().configPath) {
6
+ if (!existsSync(path)) {
7
+ return {};
8
+ }
9
+ const contents = readFileSync(path, "utf8");
10
+ const parsed = parse(contents);
11
+ if (!isRecord(parsed)) {
12
+ throw new Error("Daily Brief config must be a mapping");
13
+ }
14
+ return parseDailyBriefConfig(parsed);
15
+ }
16
+ export function readModelConfig(path = resolveDailyBriefPaths().configPath) {
17
+ return readDailyBriefConfig(path).model;
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
+ export function writeModelConfig(config, path = resolveDailyBriefPaths().configPath) {
24
+ const current = readDailyBriefConfig(path);
25
+ const next = {
26
+ ...current,
27
+ model: parseModelConfig(config)
28
+ };
29
+ mkdirSync(dirname(path), { recursive: true });
30
+ writeFileSync(path, stringify(next), "utf8");
31
+ }
32
+ export function parseDailyBriefConfig(value) {
33
+ const config = { ...value };
34
+ if (value.model !== undefined) {
35
+ if (!isRecord(value.model)) {
36
+ throw new Error("config.model must be a mapping");
37
+ }
38
+ config.model = parseModelConfig(value.model);
39
+ }
40
+ if (value.delivery !== undefined) {
41
+ if (!isRecord(value.delivery)) {
42
+ throw new Error("config.delivery must be a mapping");
43
+ }
44
+ config.delivery = parseDeliveryConfig(value.delivery);
45
+ }
46
+ return config;
47
+ }
48
+ export function readDeliveryConfig(path = resolveDailyBriefPaths().configPath) {
49
+ return readDailyBriefConfig(path).delivery;
50
+ }
51
+ export function writeDeliveryConfig(config, path = resolveDailyBriefPaths().configPath) {
52
+ const current = readDailyBriefConfig(path);
53
+ const next = { ...current, delivery: parseDeliveryConfig(config) };
54
+ mkdirSync(dirname(path), { recursive: true });
55
+ writeFileSync(path, stringify(next), "utf8");
56
+ }
57
+ function parseDeliveryConfig(value) {
58
+ if (typeof value.enabled !== "boolean") {
59
+ throw new Error("config.delivery.enabled must be a boolean");
60
+ }
61
+ const webhookRef = readOptionalString(value.webhookRef, "config.delivery.webhookRef");
62
+ return {
63
+ enabled: value.enabled,
64
+ ...(webhookRef ? { webhookRef } : {})
65
+ };
66
+ }
67
+ export function parseModelConfig(value) {
68
+ const provider = readProvider(value.provider);
69
+ const model = readRequiredString(value.model, "config.model.model");
70
+ const credentialRef = readOptionalString(value.credentialRef, "config.model.credentialRef");
71
+ const baseUrl = readOptionalString(value.baseUrl, "config.model.baseUrl");
72
+ if ("apiKey" in value || "secret" in value || "accessToken" in value || "refreshToken" in value) {
73
+ throw new Error("config.model must not contain secrets; use auth.json via credentialRef");
74
+ }
75
+ if (provider === "openai-compatible" && !baseUrl) {
76
+ throw new Error("config.model.baseUrl is required when provider is openai-compatible");
77
+ }
78
+ return {
79
+ provider,
80
+ model,
81
+ ...(credentialRef ? { credentialRef } : {}),
82
+ ...(baseUrl ? { baseUrl } : {})
83
+ };
84
+ }
85
+ export function defaultModelConfig() {
86
+ return {
87
+ provider: "openai-codex",
88
+ model: "gpt-5.5",
89
+ credentialRef: "openai-codex.default"
90
+ };
91
+ }
92
+ function readProvider(value) {
93
+ const provider = readRequiredString(value, "config.model.provider").toLowerCase();
94
+ if (provider === "codex" || provider === "hermes") {
95
+ return "openai-codex";
96
+ }
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") {
101
+ return provider;
102
+ }
103
+ throw new Error(`Unsupported model provider: ${provider}`);
104
+ }
105
+ function readRequiredString(value, field) {
106
+ if (typeof value !== "string" || value.trim().length === 0) {
107
+ throw new Error(`${field} is required`);
108
+ }
109
+ return value.trim();
110
+ }
111
+ function readOptionalString(value, field) {
112
+ if (value === undefined || value === null) {
113
+ return undefined;
114
+ }
115
+ if (typeof value !== "string") {
116
+ throw new Error(`${field} must be a string`);
117
+ }
118
+ const trimmed = value.trim();
119
+ return trimmed.length > 0 ? trimmed : undefined;
120
+ }
121
+ function isRecord(value) {
122
+ return typeof value === "object" && value !== null && !Array.isArray(value);
123
+ }
@@ -0,0 +1,20 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ export function resolveDailyBriefPaths(env = process.env) {
4
+ const home = nonEmpty(env.DAILY_BRIEF_HOME) ?? join(homedir(), ".daily-brief");
5
+ const dataHome = nonEmpty(env.DAILY_BRIEF_DATA_HOME) ?? join(home, "data");
6
+ return {
7
+ home,
8
+ dataHome,
9
+ configPath: join(home, "config.yaml"),
10
+ sourceRegistryPath: join(home, "sources.yaml"),
11
+ authPath: join(home, "auth.json"),
12
+ sourceItemRoot: join(dataHome, "source-items"),
13
+ agentRunRoot: join(dataHome, "agent-runs"),
14
+ briefArchiveRoot: join(dataHome, "briefs")
15
+ };
16
+ }
17
+ function nonEmpty(value) {
18
+ const trimmed = value?.trim();
19
+ return trimmed && trimmed.length > 0 ? trimmed : undefined;
20
+ }
@@ -0,0 +1,48 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { isMap, isSeq, parse, parseDocument } from "yaml";
3
+ import { parseSourceRegistry } from "../domain/index.js";
4
+ import { resolveDailyBriefPaths } from "./paths.js";
5
+ export async function loadSourceRegistry(path = resolveDailyBriefPaths().sourceRegistryPath) {
6
+ const contents = await readFile(path, "utf8");
7
+ return parseSourceRegistry(parse(contents));
8
+ }
9
+ export async function validateSourceRegistry(path = resolveDailyBriefPaths().sourceRegistryPath) {
10
+ return loadSourceRegistry(path);
11
+ }
12
+ export async function setSourceEnabled(id, enabled, path = resolveDailyBriefPaths().sourceRegistryPath) {
13
+ const contents = await readFile(path, "utf8");
14
+ const document = parseDocument(contents);
15
+ const sources = document.get("sources", true);
16
+ if (!isSeq(sources)) {
17
+ throw new Error("Source Registry must contain a sources list");
18
+ }
19
+ let found = false;
20
+ for (const source of sources.items) {
21
+ if (!isMap(source)) {
22
+ continue;
23
+ }
24
+ if (source.get("id") === id) {
25
+ source.set("enabled", enabled);
26
+ found = true;
27
+ break;
28
+ }
29
+ }
30
+ if (!found) {
31
+ throw new Error(`Source not found: ${id}`);
32
+ }
33
+ const nextContents = String(document);
34
+ const registry = parseSourceRegistry(parse(nextContents));
35
+ await writeFile(path, nextContents, "utf8");
36
+ return registry;
37
+ }
38
+ export function formatSourceRegistry(registry) {
39
+ if (registry.sources.length === 0) {
40
+ return "No Sources configured.";
41
+ }
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
+ }
@@ -0,0 +1,84 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import { getCredential, readDeliveryConfig, resolveDailyBriefPaths } from "../config/index.js";
4
+ import { createCoreWorkflowFailureNotification } from "../workflow/index.js";
5
+ export async function renderDiscordNotification(input) {
6
+ const template = await readDiscordTemplate(input.templatePath);
7
+ const date = input.brief.date.toISOString().slice(0, 10);
8
+ const summaryBullets = buildSummaryBullets(input.brief);
9
+ return template
10
+ .replaceAll("{{date}}", date)
11
+ .replaceAll("{{summary_bullets}}", summaryBullets)
12
+ .replaceAll("{{brief_path}}", input.briefPath)
13
+ .trim();
14
+ }
15
+ export async function deliverDiscordNotification(input, options = {}) {
16
+ const content = await renderDiscordNotification(input);
17
+ return sendDiscordContent(content, options);
18
+ }
19
+ export async function deliverCoreFailureNotification(failure, options = {}) {
20
+ return sendDiscordContent(createCoreWorkflowFailureNotification(failure), options);
21
+ }
22
+ async function sendDiscordContent(content, options) {
23
+ const webhookUrl = options.webhookUrl ?? resolveConfiguredWebhookUrl(options.env ?? process.env);
24
+ if (!webhookUrl) {
25
+ return { status: "skipped", reason: "DISCORD_WEBHOOK_URL is not configured" };
26
+ }
27
+ try {
28
+ const fetchImpl = options.fetchImpl ?? fetch;
29
+ const response = await fetchImpl(webhookUrl, {
30
+ method: "POST",
31
+ headers: {
32
+ "content-type": "application/json"
33
+ },
34
+ body: JSON.stringify({ content })
35
+ });
36
+ if (!response.ok) {
37
+ return { status: "failed", reason: `Discord webhook returned ${response.status}` };
38
+ }
39
+ return { status: "sent" };
40
+ }
41
+ catch (error) {
42
+ return { status: "failed", reason: error instanceof Error ? error.message : String(error) };
43
+ }
44
+ }
45
+ async function readDiscordTemplate(templatePath) {
46
+ if (templatePath) {
47
+ return readFile(templatePath, "utf8");
48
+ }
49
+ const candidates = [
50
+ fileURLToPath(new URL("../../templates/discord-notification.md", import.meta.url)),
51
+ fileURLToPath(new URL("../../../templates/discord-notification.md", import.meta.url))
52
+ ];
53
+ let lastError;
54
+ for (const candidate of candidates) {
55
+ try {
56
+ return await readFile(candidate, "utf8");
57
+ }
58
+ catch (error) {
59
+ lastError = error;
60
+ }
61
+ }
62
+ throw lastError instanceof Error ? lastError : new Error("Discord notification template not found");
63
+ }
64
+ export function resolveConfiguredWebhookUrl(env = process.env) {
65
+ if (env.DISCORD_WEBHOOK_URL) {
66
+ return env.DISCORD_WEBHOOK_URL;
67
+ }
68
+ const paths = resolveDailyBriefPaths(env);
69
+ const config = readDeliveryConfig(paths.configPath);
70
+ if (!config?.enabled || !config.webhookRef) {
71
+ return undefined;
72
+ }
73
+ const credential = getCredential(config.webhookRef, paths.authPath);
74
+ return credential?.type === "webhook" && credential.provider === "discord" ? credential.webhookUrl : undefined;
75
+ }
76
+ function buildSummaryBullets(brief) {
77
+ if (brief.signals.length === 0) {
78
+ return "- low-signal day:暂无可引用的 Top Signals";
79
+ }
80
+ return brief.signals
81
+ .slice(0, 3)
82
+ .map((signal) => `- [${signal.type}] ${signal.title}`)
83
+ .join("\n");
84
+ }
@@ -0,0 +1 @@
1
+ export * from "./delivery.js";
@@ -0,0 +1,2 @@
1
+ export * from "./source.js";
2
+ export * from "./source-item.js";
@@ -0,0 +1,21 @@
1
+ import { createHash } from "node:crypto";
2
+ export function createSourceItem(input) {
3
+ return {
4
+ ...input,
5
+ contentHash: hashSourceItemContent(input)
6
+ };
7
+ }
8
+ export function hashSourceItemContent(input) {
9
+ return createHash("sha256")
10
+ .update(JSON.stringify({
11
+ sourceId: input.sourceId,
12
+ platform: input.platform,
13
+ url: input.url,
14
+ title: input.title,
15
+ author: input.author,
16
+ publishedAt: input.publishedAt,
17
+ analyzableText: input.analyzableText,
18
+ metadata: input.metadata
19
+ }))
20
+ .digest("hex");
21
+ }
@@ -0,0 +1,93 @@
1
+ const SOURCE_KEYS = ["id", "platform", "adapter", "target", "enabled", "notes"];
2
+ const REGISTRY_KEYS = ["sources"];
3
+ export class SourceRegistryValidationError extends Error {
4
+ issues;
5
+ constructor(issues) {
6
+ super(`Invalid Source Registry:\n${issues.map((issue) => `- ${issue}`).join("\n")}`);
7
+ this.name = "SourceRegistryValidationError";
8
+ this.issues = issues;
9
+ }
10
+ }
11
+ export function parseSourceRegistry(value) {
12
+ const issues = [];
13
+ if (!isRecord(value)) {
14
+ throw new SourceRegistryValidationError(["registry must be a mapping with a sources list"]);
15
+ }
16
+ collectUnknownKeys("registry", value, REGISTRY_KEYS, issues);
17
+ if (!Array.isArray(value.sources)) {
18
+ throw new SourceRegistryValidationError([...issues, "sources must be a list"]);
19
+ }
20
+ const sourceIds = new Set();
21
+ const sources = value.sources.flatMap((entry, index) => {
22
+ const prefix = `sources[${index}]`;
23
+ const parsed = parseSource(entry, prefix, issues);
24
+ if (!parsed) {
25
+ return [];
26
+ }
27
+ if (sourceIds.has(parsed.id)) {
28
+ issues.push(`${prefix}.id duplicates another Source id: ${parsed.id}`);
29
+ }
30
+ sourceIds.add(parsed.id);
31
+ return [parsed];
32
+ });
33
+ if (issues.length > 0) {
34
+ throw new SourceRegistryValidationError(issues);
35
+ }
36
+ return { sources };
37
+ }
38
+ function parseSource(value, prefix, issues) {
39
+ if (!isRecord(value)) {
40
+ issues.push(`${prefix} must be a mapping`);
41
+ return undefined;
42
+ }
43
+ collectUnknownKeys(prefix, value, SOURCE_KEYS, issues);
44
+ const id = readRequiredString(value, "id", prefix, issues);
45
+ const platform = readRequiredString(value, "platform", prefix, issues);
46
+ const adapter = readRequiredString(value, "adapter", prefix, issues);
47
+ const target = readRequiredString(value, "target", prefix, issues);
48
+ const enabled = readRequiredBoolean(value, "enabled", prefix, issues);
49
+ const notes = readRequiredString(value, "notes", prefix, issues);
50
+ if (!id || !platform || !adapter || !target || enabled === undefined || !notes) {
51
+ return undefined;
52
+ }
53
+ return {
54
+ id,
55
+ platform,
56
+ adapter,
57
+ target,
58
+ enabled,
59
+ notes
60
+ };
61
+ }
62
+ function readRequiredString(source, key, prefix, issues) {
63
+ const value = source[key];
64
+ if (typeof value !== "string") {
65
+ issues.push(`${prefix}.${key} must be a string`);
66
+ return undefined;
67
+ }
68
+ const trimmed = value.trim();
69
+ if (trimmed.length === 0) {
70
+ issues.push(`${prefix}.${key} must not be empty`);
71
+ return undefined;
72
+ }
73
+ return trimmed;
74
+ }
75
+ function readRequiredBoolean(source, key, prefix, issues) {
76
+ const value = source[key];
77
+ if (typeof value !== "boolean") {
78
+ issues.push(`${prefix}.${key} must be a boolean`);
79
+ return undefined;
80
+ }
81
+ return value;
82
+ }
83
+ function collectUnknownKeys(prefix, value, allowedKeys, issues) {
84
+ const allowed = new Set(allowedKeys);
85
+ for (const key of Object.keys(value)) {
86
+ if (!allowed.has(key)) {
87
+ issues.push(`${prefix}.${key} is not allowed in the Source Registry`);
88
+ }
89
+ }
90
+ }
91
+ function isRecord(value) {
92
+ return typeof value === "object" && value !== null && !Array.isArray(value);
93
+ }
@@ -0,0 +1,44 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { randomUUID } from "node:crypto";
3
+ import { dirname, join } from "node:path";
4
+ import { formatDateKey, resolveDailyBriefPaths } from "../config/index.js";
5
+ export function createAgentRunArtifact(input) {
6
+ const startedAt = input.startedAt ?? new Date();
7
+ return {
8
+ schemaVersion: 1,
9
+ runId: input.runId ?? createAgentRunId(startedAt),
10
+ date: input.dateKey ?? formatDateKey(input.date),
11
+ startedAt: startedAt.toISOString(),
12
+ model: {
13
+ provider: input.modelRuntimeConfig.provider,
14
+ model: input.modelRuntimeConfig.model,
15
+ ...(input.modelRuntimeConfig.credentialRef ? { credentialRef: input.modelRuntimeConfig.credentialRef } : {})
16
+ },
17
+ inputRefs: input.inputRefs ?? {},
18
+ stages: []
19
+ };
20
+ }
21
+ export async function writeAgentRunArtifact(artifact, date, root = resolveDailyBriefPaths().agentRunRoot, dateKey) {
22
+ const completed = {
23
+ ...artifact,
24
+ completedAt: artifact.completedAt ?? new Date().toISOString()
25
+ };
26
+ const path = agentRunArtifactPath(date, completed.runId, root, dateKey ?? completed.date);
27
+ await mkdir(dirname(path), { recursive: true });
28
+ await writeFile(path, `${JSON.stringify(completed, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
29
+ return { path, artifact: completed };
30
+ }
31
+ export async function readAgentRunArtifact(path) {
32
+ return JSON.parse(await readFile(path, "utf8"));
33
+ }
34
+ export function agentRunArtifactPath(date, runId, root = resolveDailyBriefPaths().agentRunRoot, dateKey) {
35
+ const datePart = dateKey ?? formatDateKey(date);
36
+ const [year, month] = datePart.split("-");
37
+ if (!year || !month) {
38
+ throw new Error(`Invalid Agent Run Artifact date: ${datePart}`);
39
+ }
40
+ return join(root, year, month, datePart, `${runId}.json`);
41
+ }
42
+ function createAgentRunId(date) {
43
+ return `${date.toISOString().replace(/[:.]/g, "-")}-${randomUUID()}`;
44
+ }
@@ -0,0 +1,17 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { formatDateKey, resolveDailyBriefPaths } from "../config/index.js";
4
+ export async function writeBriefArchive(markdown, date, root = resolveDailyBriefPaths().briefArchiveRoot, dateKey) {
5
+ const path = briefArchivePath(date, root, dateKey);
6
+ await mkdir(dirname(path), { recursive: true });
7
+ await writeFile(path, markdown, "utf8");
8
+ return { path };
9
+ }
10
+ export function briefArchivePath(date, root = resolveDailyBriefPaths().briefArchiveRoot, dateKey) {
11
+ const datePart = dateKey ?? formatDateKey(date);
12
+ const [year, month] = datePart.split("-");
13
+ if (!year || !month) {
14
+ throw new Error(`Invalid archive date: ${datePart}`);
15
+ }
16
+ return join(root, year, month, `${datePart}.md`);
17
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./agent-run-artifact.js";
2
+ export * from "./brief-archive.js";
3
+ export * from "./source-item-store.js";