@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,350 @@
1
+ import { Agent } from "@earendil-works/pi-agent-core";
2
+ import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai";
3
+ import { readFile } from "node:fs/promises";
4
+ import { generateDailyBrief, renderDailyBriefMarkdown } from "../brief/index.js";
5
+ import { collectSources } from "../collection/index.js";
6
+ import { deliverCoreFailureNotification, deliverDiscordNotification } from "../discord/index.js";
7
+ import { briefArchivePath, createAgentRunArtifact, readSourceItems, writeAgentRunArtifact, writeBriefArchive } from "../storage/index.js";
8
+ import { readModelRuntimeConfig } from "./model-runtime-config.js";
9
+ import { enrichDailyBriefNarrativeWithAgent } from "./signal-narrative.js";
10
+ import { runSignalSelectionAndRankingStages } from "./signal-selection-ranking.js";
11
+ import { AnalysisFailureError, runSourceGroundingAuditStage } from "./source-grounding-audit.js";
12
+ import { runSourceGroundingRepairStage } from "./source-grounding-repair.js";
13
+ import { runSourceItemUnderstandingStage } from "./source-item-understanding.js";
14
+ import { runAgentStage } from "./stage-runner.js";
15
+ export async function runOnce(options = {}) {
16
+ const date = options.date ?? new Date();
17
+ const dateKey = options.dateKey;
18
+ let collection;
19
+ try {
20
+ collection = await collectSources({
21
+ date,
22
+ ...(dateKey ? { dateKey } : {}),
23
+ fetchedAt: date,
24
+ ...(options.sourceRegistryPath ? { sourceRegistryPath: options.sourceRegistryPath } : {}),
25
+ ...(options.sourceItemRoot ? { sourceItemRoot: options.sourceItemRoot } : {})
26
+ });
27
+ }
28
+ catch (error) {
29
+ const coreFailure = {
30
+ kind: "unreadable-source-registry",
31
+ message: error instanceof Error ? error.message : String(error)
32
+ };
33
+ const delivery = await deliverCoreFailureNotification(coreFailure, {
34
+ ...(options.discordWebhookUrl ? { webhookUrl: options.discordWebhookUrl } : {}),
35
+ ...(options.discordFetchImpl ? { fetchImpl: options.discordFetchImpl } : {})
36
+ });
37
+ return {
38
+ archivePath: "",
39
+ markdown: "",
40
+ sourceCount: 0,
41
+ sourceItemCount: 0,
42
+ piEvents: [],
43
+ collection: { storePath: "", sources: [] },
44
+ delivery,
45
+ coreFailure
46
+ };
47
+ }
48
+ const collectedItems = await readSourceItems(date, options.sourceItemRoot, dateKey);
49
+ const collectionFailure = classifyCollectionCoreFailure(collection, collectedItems.length);
50
+ if (collectionFailure) {
51
+ const delivery = await deliverCoreFailureNotification(collectionFailure, {
52
+ ...(options.discordWebhookUrl ? { webhookUrl: options.discordWebhookUrl } : {}),
53
+ ...(options.discordFetchImpl ? { fetchImpl: options.discordFetchImpl } : {})
54
+ });
55
+ return {
56
+ archivePath: "",
57
+ markdown: "",
58
+ sourceCount: collection.sources.length,
59
+ sourceItemCount: collectedItems.length,
60
+ piEvents: [],
61
+ collection,
62
+ delivery,
63
+ coreFailure: collectionFailure
64
+ };
65
+ }
66
+ const collectionFailures = collectionFailureRefs(collection);
67
+ const generated = await generateOnce({
68
+ ...options,
69
+ ...(dateKey ? { dateKey } : {}),
70
+ sourceCount: collection.sources.length,
71
+ ...(collectionFailures.length > 0
72
+ ? {
73
+ partialFailures: collectionFailures.map((failure) => `${failure.sourceId}: ${failure.reason}`),
74
+ collectionFailures
75
+ }
76
+ : {})
77
+ });
78
+ const delivery = await deliverOnce({
79
+ ...options,
80
+ date,
81
+ brief: generated.brief,
82
+ archivePath: generated.archivePath
83
+ });
84
+ return {
85
+ archivePath: generated.archivePath,
86
+ markdown: generated.markdown,
87
+ sourceCount: collection.sources.length,
88
+ sourceItemCount: generated.sourceItemCount,
89
+ piEvents: generated.piEvents,
90
+ collection,
91
+ delivery,
92
+ ...(generated.agentRunArtifactPath ? { agentRunArtifactPath: generated.agentRunArtifactPath } : {})
93
+ };
94
+ }
95
+ export async function generateOnce(options = {}) {
96
+ const date = options.date ?? new Date();
97
+ const dateKey = options.dateKey;
98
+ const sourceItems = await readSourceItems(date, options.sourceItemRoot, dateKey);
99
+ const modelRuntimeConfig = readModelRuntimeConfig(options.modelRuntimeEnv);
100
+ if (sourceItems.length === 0) {
101
+ throw new Error("No usable Source Items found for generation; Daily Brief will not archive a false low-signal brief.");
102
+ }
103
+ if (!modelRuntimeConfig.ready) {
104
+ throw new Error(`Model runtime is not ready:\n${modelRuntimeConfig.issues.map((issue) => `- ${issue}`).join("\n")}`);
105
+ }
106
+ const baseBrief = generateDailyBrief({
107
+ date,
108
+ ...(dateKey ? { dateKey } : {}),
109
+ sourceItems,
110
+ ...(options.partialFailures ? { partialFailures: options.partialFailures } : {}),
111
+ ...(options.sourceCount ? { sourceCount: options.sourceCount } : {})
112
+ });
113
+ const artifact = createAgentRunArtifact({
114
+ date,
115
+ ...(dateKey ? { dateKey } : {}),
116
+ modelRuntimeConfig,
117
+ inputRefs: {
118
+ sourceItemIds: sourceItems.map((item) => item.id),
119
+ signalIds: baseBrief.signals.map((signal) => signal.id),
120
+ ...(options.collectionFailures ? { collectionFailures: options.collectionFailures } : {})
121
+ }
122
+ });
123
+ try {
124
+ const collectionInputRefs = options.collectionFailures
125
+ ? { collectionFailures: options.collectionFailures }
126
+ : undefined;
127
+ const understanding = await runSourceItemUnderstandingStage({
128
+ sourceItems,
129
+ modelRuntimeConfig,
130
+ artifact,
131
+ ...(collectionInputRefs ? { inputRefs: collectionInputRefs } : {}),
132
+ ...(options.modelRuntimeEnv ? { modelRuntimeEnv: options.modelRuntimeEnv } : {})
133
+ });
134
+ const selected = await runSignalSelectionAndRankingStages({
135
+ sourceItems,
136
+ annotations: understanding.annotations,
137
+ artifact,
138
+ modelRuntimeConfig,
139
+ ...(collectionInputRefs ? { inputRefs: collectionInputRefs } : {}),
140
+ ...(options.modelRuntimeEnv ? { modelRuntimeEnv: options.modelRuntimeEnv } : {})
141
+ });
142
+ const selectedBrief = {
143
+ ...baseBrief,
144
+ executiveSummary: selected.signals.length === 0
145
+ ? "今天是 low-signal day:Selection/Ranking Stages 没有选出足够强的 Source-grounded Signals。"
146
+ : `今天有 ${selected.signals.length} 个 Agent-selected Source-grounded Signals,均保留 Source Item citation 以便回溯。`,
147
+ signals: selected.signals
148
+ };
149
+ const narrativeEvents = [];
150
+ const narrative = await enrichDailyBriefNarrativeWithAgent({
151
+ brief: selectedBrief,
152
+ sourceItems,
153
+ modelRuntimeConfig,
154
+ ...(options.modelRuntimeEnv ? { modelRuntimeEnv: options.modelRuntimeEnv } : {})
155
+ });
156
+ narrativeEvents.push(...narrative.events);
157
+ const narrativeStage = await recordNarrativeStage({
158
+ artifact,
159
+ date,
160
+ sourceItems,
161
+ brief: narrative.brief,
162
+ ...(options.collectionFailures ? { collectionFailures: options.collectionFailures } : {})
163
+ });
164
+ const audited = await auditBriefWithSingleRepairAttempt({
165
+ narrativeBrief: narrative.brief,
166
+ sourceItems,
167
+ artifact,
168
+ date,
169
+ modelRuntimeConfig,
170
+ ...(options.modelRuntimeEnv ? { modelRuntimeEnv: options.modelRuntimeEnv } : {}),
171
+ ...(options.collectionFailures ? { collectionFailures: options.collectionFailures } : {}),
172
+ ...(collectionInputRefs ? { collectionInputRefs } : {})
173
+ });
174
+ narrativeEvents.push(...audited.narrativeEvents);
175
+ const writtenArtifact = options.agentRunRoot ? await writeAgentRunArtifact(artifact, date, options.agentRunRoot, dateKey) : undefined;
176
+ const markdown = renderDailyBriefMarkdown(audited.brief);
177
+ const piResult = await renderBriefThroughPiRuntime(markdown);
178
+ const archived = await writeBriefArchive(piResult.markdown, date, options.archiveRoot, dateKey);
179
+ return {
180
+ archivePath: archived.path,
181
+ markdown: piResult.markdown,
182
+ brief: audited.brief,
183
+ sourceItemCount: sourceItems.length,
184
+ piEvents: [...understanding.events, ...selected.events, ...narrativeEvents, ...piResult.events],
185
+ modelRuntimeConfig,
186
+ ...((writtenArtifact?.path ?? narrativeStage.artifactPath) ? { agentRunArtifactPath: writtenArtifact?.path ?? narrativeStage.artifactPath } : {})
187
+ };
188
+ }
189
+ catch (error) {
190
+ artifact.failure = {
191
+ kind: "execution",
192
+ message: error instanceof Error ? error.message : String(error)
193
+ };
194
+ if (options.agentRunRoot) {
195
+ await writeAgentRunArtifact(artifact, date, options.agentRunRoot, dateKey);
196
+ }
197
+ throw error;
198
+ }
199
+ }
200
+ async function recordNarrativeStage(input) {
201
+ const result = await runAgentStage({
202
+ stage: "narrative",
203
+ artifact: input.artifact,
204
+ date: input.date,
205
+ inputRefs: {
206
+ ...(input.collectionFailures ? { collectionFailures: input.collectionFailures } : {}),
207
+ sourceItemIds: input.sourceItems.map((item) => item.id),
208
+ signalIds: input.brief.signals.map((signal) => signal.id)
209
+ },
210
+ validationContext: {
211
+ signalIds: input.brief.signals.map((signal) => signal.id)
212
+ },
213
+ execute: async () => ({
214
+ stage: "narrative",
215
+ executiveSummary: input.brief.executiveSummary,
216
+ signalNarratives: input.brief.signals.map((signal) => ({
217
+ signalId: signal.id,
218
+ focusAreas: signal.focusAreas ?? [],
219
+ directions: signal.directions ?? [],
220
+ whatItIs: signal.summary.whatItIs,
221
+ whatItIsNot: signal.summary.whatItIsNot,
222
+ minimalExample: signal.summary.minimalExample,
223
+ whyItMatters: signal.whyItMatters
224
+ }))
225
+ })
226
+ });
227
+ return result;
228
+ }
229
+ async function auditBriefWithSingleRepairAttempt(input) {
230
+ const narrativeEvents = [];
231
+ try {
232
+ const audited = await runSourceGroundingAuditStage({
233
+ brief: input.narrativeBrief,
234
+ sourceItems: input.sourceItems,
235
+ artifact: input.artifact,
236
+ modelRuntimeConfig: input.modelRuntimeConfig,
237
+ ...(input.collectionInputRefs ? { inputRefs: input.collectionInputRefs } : {}),
238
+ ...(input.modelRuntimeEnv ? { modelRuntimeEnv: input.modelRuntimeEnv } : {})
239
+ });
240
+ return { brief: audited.brief, narrativeEvents };
241
+ }
242
+ catch (error) {
243
+ if (!(error instanceof AnalysisFailureError)) {
244
+ throw error;
245
+ }
246
+ const repaired = await runSourceGroundingRepairStage({
247
+ brief: input.narrativeBrief,
248
+ sourceItems: input.sourceItems,
249
+ auditFindings: error.findings,
250
+ artifact: input.artifact,
251
+ modelRuntimeConfig: input.modelRuntimeConfig,
252
+ ...(input.collectionInputRefs ? { inputRefs: input.collectionInputRefs } : {}),
253
+ ...(input.modelRuntimeEnv ? { modelRuntimeEnv: input.modelRuntimeEnv } : {})
254
+ });
255
+ narrativeEvents.push(...repaired.events);
256
+ const audited = await runSourceGroundingAuditStage({
257
+ brief: repaired.brief,
258
+ sourceItems: input.sourceItems,
259
+ artifact: input.artifact,
260
+ modelRuntimeConfig: input.modelRuntimeConfig,
261
+ ...(input.collectionInputRefs ? { inputRefs: input.collectionInputRefs } : {}),
262
+ ...(input.modelRuntimeEnv ? { modelRuntimeEnv: input.modelRuntimeEnv } : {})
263
+ });
264
+ return { brief: audited.brief, narrativeEvents };
265
+ }
266
+ }
267
+ function collectionFailureRefs(collection) {
268
+ return collection.sources
269
+ .filter((source) => source.status === "failed")
270
+ .map((source) => ({ sourceId: source.sourceId, reason: source.reason ?? "Unknown failure" }));
271
+ }
272
+ function classifyCollectionCoreFailure(collection, storedSourceItemCount) {
273
+ const enabledResults = collection.sources.filter((source) => source.status !== "skipped");
274
+ const failedResults = enabledResults.filter((source) => source.status === "failed");
275
+ if (enabledResults.length === 0 && storedSourceItemCount === 0) {
276
+ return {
277
+ kind: "no-usable-source-items",
278
+ message: "No enabled Sources produced Source Items; Daily Brief will not generate a false low-signal brief."
279
+ };
280
+ }
281
+ if (enabledResults.length > 0 && failedResults.length === enabledResults.length && storedSourceItemCount === 0) {
282
+ return {
283
+ kind: "no-usable-source-items",
284
+ message: `All enabled Sources failed and no Source Items exist for this date: ${failedResults
285
+ .map((source) => `${source.sourceId}: ${source.reason ?? "Unknown failure"}`)
286
+ .join("; ")}`
287
+ };
288
+ }
289
+ return undefined;
290
+ }
291
+ export async function deliverOnce(options = {}) {
292
+ const date = options.date ?? new Date();
293
+ const archivePath = options.archivePath ?? briefArchivePath(date, options.archiveRoot, options.dateKey);
294
+ try {
295
+ await readFile(archivePath, "utf8");
296
+ }
297
+ catch {
298
+ return { status: "failed", reason: `Brief Archive entry not found: ${archivePath}` };
299
+ }
300
+ const brief = options.brief ??
301
+ generateDailyBrief({
302
+ date,
303
+ ...(options.dateKey ? { dateKey: options.dateKey } : {}),
304
+ sourceItems: await readSourceItems(date, options.sourceItemRoot, options.dateKey)
305
+ });
306
+ return deliverDiscordNotification({
307
+ brief,
308
+ briefPath: archivePath,
309
+ ...(options.discordTemplatePath ? { templatePath: options.discordTemplatePath } : {})
310
+ }, {
311
+ ...(options.discordWebhookUrl ? { webhookUrl: options.discordWebhookUrl } : {}),
312
+ ...(options.discordFetchImpl ? { fetchImpl: options.discordFetchImpl } : {})
313
+ });
314
+ }
315
+ export async function renderBriefThroughPiRuntime(markdown) {
316
+ const provider = registerFauxProvider();
317
+ provider.setResponses([fauxAssistantMessage(markdown)]);
318
+ try {
319
+ const events = [];
320
+ const model = provider.getModel();
321
+ const agent = new Agent({
322
+ initialState: {
323
+ systemPrompt: "You are the Daily Brief Agent. Return only the already grounded Markdown Daily Brief you are given.",
324
+ model,
325
+ thinkingLevel: "off"
326
+ },
327
+ sessionId: "daily-brief-run-once"
328
+ });
329
+ agent.subscribe((event) => {
330
+ events.push(event.type);
331
+ });
332
+ await agent.prompt([
333
+ "Render this Source-grounded Daily Brief exactly as Markdown.",
334
+ "",
335
+ markdown
336
+ ].join("\n"));
337
+ const assistantMessage = [...agent.state.messages].reverse().find((message) => message.role === "assistant");
338
+ const rendered = assistantMessage?.content
339
+ .filter((block) => block.type === "text")
340
+ .map((block) => block.text)
341
+ .join("");
342
+ if (!rendered) {
343
+ throw new Error("Pi Agent Runtime did not return Daily Brief Markdown");
344
+ }
345
+ return { markdown: rendered, events };
346
+ }
347
+ finally {
348
+ provider.unregister();
349
+ }
350
+ }
@@ -0,0 +1,10 @@
1
+ export * from "./daily-brief-agent.js";
2
+ export * from "./model-runtime-config.js";
3
+ export * from "./model-stage-runtime.js";
4
+ export * from "./signal-narrative.js";
5
+ export * from "./signal-selection-ranking.js";
6
+ export * from "./source-item-understanding.js";
7
+ export * from "./source-grounding-audit.js";
8
+ export * from "./source-grounding-repair.js";
9
+ export * from "./stage-contracts.js";
10
+ export * from "./stage-runner.js";
@@ -0,0 +1,221 @@
1
+ import { existsSync } from "node:fs";
2
+ import { getOAuthApiKey, loginOpenAICodex } from "@earendil-works/pi-ai/oauth";
3
+ import { envNameFromCredentialRef, getCredential, isEnvCredentialRef, putCredential, readCredentialStore, readModelConfig, removeCredential, writeCredentialStore } from "../config/index.js";
4
+ import { resolveDailyBriefPaths } from "../config/paths.js";
5
+ export const defaultOAuthHelpers = {
6
+ getOAuthApiKey,
7
+ loginOpenAICodex
8
+ };
9
+ export function readModelRuntimeConfig(env = process.env, options = {}) {
10
+ const paths = resolveDailyBriefPaths(env);
11
+ const configPath = options.configPath ?? paths.configPath;
12
+ const authPath = options.authPath ?? paths.authPath;
13
+ const config = readEnvModelConfig(env) ?? (existsSync(configPath) ? readModelConfig(configPath) : undefined);
14
+ if (!config) {
15
+ return {
16
+ provider: "openai-codex",
17
+ model: "gpt-5.5",
18
+ credentialRef: "openai-codex.default",
19
+ ready: false,
20
+ issues: [`Model config not found: ${configPath}. Run daily-brief setup and daily-brief model configure/login first.`]
21
+ };
22
+ }
23
+ const issues = credentialIssues(config.provider, config.credentialRef, env, authPath);
24
+ return {
25
+ provider: config.provider,
26
+ model: config.model,
27
+ ...(config.credentialRef ? { credentialRef: config.credentialRef } : {}),
28
+ ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
29
+ ready: issues.length === 0,
30
+ issues
31
+ };
32
+ }
33
+ export async function resolveModelApiKey(config, env = process.env, options = {}) {
34
+ if (config.provider === "faux") {
35
+ return undefined;
36
+ }
37
+ const credentialRef = config.credentialRef;
38
+ if (!credentialRef) {
39
+ throw new Error(`credentialRef is required for ${config.provider}`);
40
+ }
41
+ if (isEnvCredentialRef(credentialRef)) {
42
+ const envName = envNameFromCredentialRef(credentialRef);
43
+ return env[envName]?.trim();
44
+ }
45
+ const authPath = options.authPath ?? resolveDailyBriefPaths(env).authPath;
46
+ const credential = getCredential(credentialRef, authPath);
47
+ if (!credential) {
48
+ throw new Error(`Credential not found: ${credentialRef}`);
49
+ }
50
+ if (credential.provider !== config.provider) {
51
+ throw new Error(`Credential ${credentialRef} is for ${credential.provider}, not ${config.provider}`);
52
+ }
53
+ if (credential.type === "api-key") {
54
+ return credential.apiKey;
55
+ }
56
+ const helpers = options.oauthHelpers ?? defaultOAuthHelpers;
57
+ const result = await helpers.getOAuthApiKey(credential.provider, {
58
+ [credential.provider]: credential.credentials
59
+ });
60
+ if (!result) {
61
+ throw new Error(`OAuth credential ${credentialRef} is not logged in`);
62
+ }
63
+ persistRefreshedOAuthCredential(credentialRef, credential, result.newCredentials, authPath);
64
+ return result.apiKey;
65
+ }
66
+ export async function loginModelCredential(input) {
67
+ if (input.provider !== "openai-codex") {
68
+ throw new Error(`model login currently supports OAuth provider openai-codex, received ${input.provider}`);
69
+ }
70
+ const helpers = input.oauthHelpers ?? defaultOAuthHelpers;
71
+ const credentials = await helpers.loginOpenAICodex({
72
+ onAuth: (info) => {
73
+ input.io.stdout(`Open authorization URL: ${info.url}`);
74
+ if (info.instructions) {
75
+ input.io.stdout(info.instructions);
76
+ }
77
+ },
78
+ onDeviceCode: (info) => {
79
+ input.io.stdout(`Open verification URL: ${info.verificationUri}`);
80
+ input.io.stdout(`Device code: ${info.userCode}`);
81
+ },
82
+ onPrompt: (prompt) => promptForOAuth(input.io, prompt.message),
83
+ onManualCodeInput: () => promptForOAuth(input.io, "Authorization code:"),
84
+ onProgress: (message) => input.io.stdout(message),
85
+ onSelect: async (prompt) => {
86
+ for (const option of prompt.options) {
87
+ input.io.stdout(`${option.id}: ${option.label}`);
88
+ }
89
+ const selected = await promptForOAuth(input.io, prompt.message);
90
+ return selected || undefined;
91
+ }
92
+ });
93
+ putCredential(input.credentialRef, {
94
+ type: "oauth",
95
+ provider: "openai-codex",
96
+ credentials
97
+ }, input.authPath ?? resolveDailyBriefPaths(input.env ?? process.env).authPath);
98
+ }
99
+ export function logoutModelCredential(ref, env = process.env, authPath) {
100
+ removeCredential(ref, authPath ?? resolveDailyBriefPaths(env).authPath);
101
+ }
102
+ export function statusModelCredentials(env = process.env, options = {}) {
103
+ const authPath = options.authPath ?? resolveDailyBriefPaths(env).authPath;
104
+ return Object.entries(readCredentialStore(authPath).credentials).map(([ref, credential]) => ({
105
+ ref,
106
+ type: credential.type,
107
+ provider: credential.provider,
108
+ secret: "<redacted>"
109
+ }));
110
+ }
111
+ function credentialIssues(provider, credentialRef, env, authPath) {
112
+ if (provider === "faux") {
113
+ return [];
114
+ }
115
+ if (!credentialRef) {
116
+ return [`credentialRef is required for ${provider}`];
117
+ }
118
+ if (isEnvCredentialRef(credentialRef)) {
119
+ const envName = envNameFromCredentialRef(credentialRef);
120
+ return env[envName]?.trim() ? [] : [`${envName} is required for credentialRef ${credentialRef}`];
121
+ }
122
+ const credential = getCredential(credentialRef, authPath);
123
+ if (!credential) {
124
+ return [`Credential not found: ${credentialRef}`];
125
+ }
126
+ if (credential.provider !== provider) {
127
+ return [`Credential ${credentialRef} is for ${credential.provider}, not ${provider}`];
128
+ }
129
+ return [];
130
+ }
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
+ function persistRefreshedOAuthCredential(ref, credential, credentials, authPath) {
191
+ const store = readCredentialStore(authPath);
192
+ const previous = store.credentials[ref];
193
+ store.credentials[ref] = {
194
+ ...credential,
195
+ ...((previous?.createdAt ?? credential.createdAt) ? { createdAt: previous?.createdAt ?? credential.createdAt } : {}),
196
+ updatedAt: new Date().toISOString(),
197
+ credentials
198
+ };
199
+ writeCredentialStore(store, authPath);
200
+ }
201
+ function readEnvCredentialRef(env, provider) {
202
+ const credentialRef = env.DAILY_BRIEF_MODEL_CREDENTIAL_REF?.trim() ||
203
+ defaultCredentialRefForProvider(provider);
204
+ return credentialRef ? { credentialRef } : {};
205
+ }
206
+ function promptForOAuth(io, message) {
207
+ if (!io.prompt) {
208
+ throw new Error(`OAuth login requires interactive input: ${message}`);
209
+ }
210
+ return io.prompt(message);
211
+ }
212
+ export function toApiKeyCredential(provider, apiKey) {
213
+ if (provider === "faux" || provider === "openai-codex") {
214
+ throw new Error(`${provider} does not accept API key credentials in auth.json`);
215
+ }
216
+ return {
217
+ type: "api-key",
218
+ provider,
219
+ apiKey
220
+ };
221
+ }
@@ -0,0 +1,63 @@
1
+ import { fauxAssistantMessage, getModel, registerBuiltInApiProviders, registerFauxProvider } from "@earendil-works/pi-ai";
2
+ import { resolveModelApiKey } from "./model-runtime-config.js";
3
+ export function createStageModelRuntime(input) {
4
+ const { config, env } = input;
5
+ if (config.provider === "faux") {
6
+ if (!input.fauxResponse) {
7
+ throw new Error("fauxResponse is required for faux model runtime");
8
+ }
9
+ const provider = registerFauxProvider({
10
+ models: [{ id: config.model, name: config.model }]
11
+ });
12
+ provider.setResponses([fauxAssistantMessage(input.fauxResponse)]);
13
+ return {
14
+ model: provider.getModel(config.model) ?? provider.getModel(),
15
+ thinkingLevel: "off",
16
+ unregister: provider.unregister
17
+ };
18
+ }
19
+ registerBuiltInApiProviders();
20
+ if (config.provider === "openai-codex") {
21
+ return {
22
+ model: getModel("openai-codex", config.model),
23
+ getApiKey: (provider) => (provider === "openai-codex" ? resolveModelApiKey(config, env) : undefined),
24
+ thinkingLevel: input.openAICodexThinkingLevel ?? "low"
25
+ };
26
+ }
27
+ if (config.provider === "deepseek") {
28
+ return {
29
+ model: getModel("deepseek", config.model),
30
+ getApiKey: (provider) => (provider === "deepseek" ? resolveModelApiKey(config, env) : undefined),
31
+ thinkingLevel: "off"
32
+ };
33
+ }
34
+ if (config.provider === "openai-compatible") {
35
+ return {
36
+ model: buildOpenAICompatibleModel(config),
37
+ getApiKey: (provider) => (provider === "openai-compatible" ? resolveModelApiKey(config, env) : undefined),
38
+ thinkingLevel: "off"
39
+ };
40
+ }
41
+ return {
42
+ model: getModel("openai", config.model),
43
+ getApiKey: (provider) => (provider === "openai" ? resolveModelApiKey(config, env) : undefined),
44
+ thinkingLevel: "off"
45
+ };
46
+ }
47
+ export function buildOpenAICompatibleModel(config) {
48
+ if (!config.baseUrl) {
49
+ throw new Error("baseUrl is required for openai-compatible model runtime");
50
+ }
51
+ return {
52
+ id: config.model,
53
+ name: config.model,
54
+ api: "openai-completions",
55
+ provider: "openai-compatible",
56
+ baseUrl: config.baseUrl,
57
+ reasoning: false,
58
+ input: ["text"],
59
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
60
+ contextWindow: 128000,
61
+ maxTokens: 4096
62
+ };
63
+ }