@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/dist/src/cli.js CHANGED
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
3
4
  import { stdin as processStdin, stdout as processStdout } from "node:process";
4
5
  import { createInterface } from "node:readline/promises";
5
6
  import { fileURLToPath, pathToFileURL } from "node:url";
6
- import { deliverOnce, generateOnce, loginModelCredential, logoutModelCredential, readModelRuntimeConfig, runOnce, statusModelCredentials, toApiKeyCredential } from "./agent/index.js";
7
- import { collectSources } from "./collection/index.js";
8
- import { resolveConfiguredWebhookUrl } from "./discord/index.js";
9
- import { defaultModelConfig, dateFromDateKey, formatDateKey, formatSourceRegistry, loadSourceRegistry, putCredential, readDeliveryConfig, readConfiguredTimezone, getCredential, resolveDailyBriefPaths, setSourceEnabled, validateSourceRegistry, writeDeliveryConfig, writeCredentialStore, writeModelConfig } from "./config/index.js";
7
+ import { stringify } from "yaml";
8
+ import { loginModelCredential, readModelRuntimeConfig, runOnce, toApiKeyCredential } from "./agent/index.js";
9
+ import { defaultModelConfig, dateFromDateKey, formatDateKey, formatSourceRegistry, loadSourceRegistry, putCredential, readDeliveryConfig, readDailyBriefConfig, getCredential, resolveDailyBriefPaths, setSourceEnabled, validateSourceRegistry, writeDeliveryConfig, writeCredentialStore, writeModelConfig } from "./config/index.js";
10
10
  import { getOperationalStatus } from "./workflow/index.js";
11
11
  const consoleIo = {
12
+ interactive: Boolean(processStdin.isTTY && processStdout.isTTY),
12
13
  stdout(line) {
13
14
  console.log(line);
14
15
  },
@@ -16,7 +17,10 @@ const consoleIo = {
16
17
  console.error(line);
17
18
  },
18
19
  async prompt(message) {
19
- const reader = createInterface({ input: processStdin, output: processStdout });
20
+ if (!processStdin.isTTY || !processStdout.isTTY) {
21
+ throw new Error(nonInteractiveSetupMessage());
22
+ }
23
+ const reader = createInterface({ input: processStdin, output: processStdout, terminal: false });
20
24
  try {
21
25
  return await reader.question(`${message} `);
22
26
  }
@@ -27,75 +31,138 @@ const consoleIo = {
27
31
  };
28
32
  export async function runCli(args, io = consoleIo, env = process.env) {
29
33
  const [command, subcommand, value] = args;
30
- const workflowFlags = parseFlags(args.slice(1));
31
- const options = {
32
- ...optionsFromEnv(env),
33
- ...readWorkflowDateOption(workflowFlags, env)
34
- };
35
34
  if (!command || command === "help" || command === "--help" || command === "-h") {
36
35
  printHelp(io);
37
36
  return;
38
37
  }
38
+ if (command === "version" || command === "--version" || command === "-v") {
39
+ io.stdout(await readPackageVersion());
40
+ return;
41
+ }
39
42
  if (command === "run-once") {
43
+ const workflowFlags = parseFlags(args.slice(1));
44
+ const options = {
45
+ ...optionsFromEnv(env),
46
+ ...readWorkflowDateOption(workflowFlags)
47
+ };
40
48
  await assertWorkflowConfigured(options.sourceRegistryPath);
41
- const result = await runOnce(options);
49
+ io.stdout("Daily Brief run started");
50
+ io.stdout(`Date: ${options.dateKey}`);
51
+ io.stdout(`Home: ${resolveDailyBriefPaths(env).home}`);
52
+ io.stdout("");
53
+ const result = await runOnce({
54
+ ...options,
55
+ onProgress(line) {
56
+ io.stdout(line);
57
+ }
58
+ });
42
59
  if (result.coreFailure) {
43
60
  throw new Error(`Core Workflow Failure: ${result.coreFailure.kind}\n${result.coreFailure.message}`);
44
61
  }
62
+ io.stdout("");
45
63
  io.stdout(`Daily Brief archived: ${result.archivePath}`);
46
64
  io.stdout(`Sources read: ${result.sourceCount}`);
47
65
  io.stdout(`Source Items read: ${result.sourceItemCount}`);
48
- io.stdout(`Discord delivery: ${result.delivery.status}`);
49
- io.stdout(`Pi events: ${result.piEvents.join(", ")}`);
66
+ io.stdout(`Agent stages completed: ${countAgentStageEvents(result.piEvents)}/5`);
67
+ io.stdout(`Discord delivery: ${formatDeliveryStatus(result.delivery)}`);
68
+ io.stdout("5/5 Run completed");
50
69
  return;
51
70
  }
52
71
  if (command === "setup") {
53
72
  await handleSetupCommand(args.slice(1), io, env);
54
73
  return;
55
74
  }
56
- if (command === "collect") {
57
- await assertWorkflowConfigured(options.sourceRegistryPath);
58
- const result = await collectSources(options);
59
- io.stdout(`Source Item Store: ${result.storePath || "(no items written)"}`);
60
- for (const source of result.sources) {
61
- io.stdout(`${source.status.padEnd(7)} ${source.sourceId} items=${source.itemCount} written=${source.writtenCount} duplicates=${source.skippedDuplicateCount}${source.reason ? ` reason=${source.reason}` : ""}`);
62
- }
63
- return;
64
- }
65
- if (command === "generate") {
66
- const result = await generateOnce(options);
67
- io.stdout(`Daily Brief archived: ${result.archivePath}`);
68
- io.stdout(`Source Items read: ${result.sourceItemCount}`);
69
- return;
70
- }
71
- if (command === "deliver") {
72
- const result = await deliverOnce(options);
73
- io.stdout(`Discord delivery: ${result.status}${"reason" in result ? ` (${result.reason})` : ""}`);
74
- return;
75
- }
76
75
  if (command === "status") {
77
- const status = await getOperationalStatus(options);
78
- io.stdout(`${status.health}: ${status.message}`);
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
  }
88
- if (command === "model") {
89
- await handleModelCommand(args.slice(1), io, env);
90
- return;
89
+ throw new Error(`Unknown command: ${command}`);
90
+ }
91
+ async function readPackageVersion() {
92
+ let directory = dirname(fileURLToPath(import.meta.url));
93
+ for (let index = 0; index < 8; index += 1) {
94
+ const path = join(directory, "package.json");
95
+ try {
96
+ const packageJson = JSON.parse(await readFile(path, "utf8"));
97
+ if (isRecord(packageJson) && typeof packageJson.version === "string") {
98
+ return `daily-brief ${packageJson.version}`;
99
+ }
100
+ }
101
+ catch (error) {
102
+ if (!(isNodeError(error) && error.code === "ENOENT")) {
103
+ throw error;
104
+ }
105
+ }
106
+ const parent = dirname(directory);
107
+ if (parent === directory) {
108
+ break;
109
+ }
110
+ directory = parent;
91
111
  }
92
- if (command === "delivery") {
93
- await handleDeliveryCommand(args.slice(1), io, env);
94
- return;
112
+ return "daily-brief unknown";
113
+ }
114
+ function countAgentStageEvents(events) {
115
+ return events.length > 0 ? 5 : 0;
116
+ }
117
+ function formatDeliveryStatus(delivery) {
118
+ return `${delivery.status}${delivery.reason ? ` (${delivery.reason})` : ""}`;
119
+ }
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}`);
95
143
  }
96
- throw new Error(`Unknown command: ${command}`);
97
144
  }
98
- function readWorkflowDateOption(flags, env) {
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) {
99
166
  if (flags.date) {
100
167
  if (!/^\d{4}-\d{2}-\d{2}$/.test(flags.date)) {
101
168
  throw new Error("--date must use YYYY-MM-DD");
@@ -103,8 +170,7 @@ function readWorkflowDateOption(flags, env) {
103
170
  return { date: dateFromDateKey(flags.date), dateKey: flags.date };
104
171
  }
105
172
  const now = new Date();
106
- const paths = resolveDailyBriefPaths(env);
107
- const timezone = readConfiguredTimezone(paths.configPath) ?? env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
173
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
108
174
  return { date: now, dateKey: formatDateKey(now, timezone) };
109
175
  }
110
176
  async function assertWorkflowConfigured(sourceRegistryPath) {
@@ -119,8 +185,15 @@ async function handleSourcesCommand(subcommand, value, io, sourceRegistryPath) {
119
185
  }
120
186
  if (subcommand === "edit") {
121
187
  const path = sourceRegistryPath ?? resolveDailyBriefPaths().sourceRegistryPath;
122
- io.stdout(`Source Registry: ${path}`);
123
- io.stdout("Edit this YAML file, then run: daily-brief sources validate");
188
+ io.stdout("Source Registry:");
189
+ io.stdout(` ${path}`);
190
+ io.stdout("");
191
+ io.stdout("Edit this YAML file to add, remove, or update Sources.");
192
+ io.stdout("Use the id field as SOURCE ID for enable/disable commands.");
193
+ io.stdout("");
194
+ io.stdout("After editing:");
195
+ io.stdout(" daily-brief sources validate");
196
+ io.stdout(" daily-brief sources list");
124
197
  return;
125
198
  }
126
199
  if (subcommand === "validate") {
@@ -138,10 +211,19 @@ async function handleSourcesCommand(subcommand, value, io, sourceRegistryPath) {
138
211
  }
139
212
  if (subcommand === "enable" || subcommand === "disable") {
140
213
  if (!value) {
141
- throw new Error(`sources ${subcommand} requires a Source id`);
214
+ throw new Error(`sources ${subcommand} requires a SOURCE ID. Run daily-brief sources list to see available SOURCE ID values.`);
142
215
  }
143
216
  const enabled = subcommand === "enable";
144
- await setSourceEnabled(value, enabled, sourceRegistryPath);
217
+ try {
218
+ await setSourceEnabled(value, enabled, sourceRegistryPath);
219
+ }
220
+ catch (error) {
221
+ const message = error instanceof Error ? error.message : String(error);
222
+ if (message === `Source not found: ${value}`) {
223
+ throw new Error(`Source not found: ${value}. Run daily-brief sources list to see available SOURCE ID values.`);
224
+ }
225
+ throw error;
226
+ }
145
227
  io.stdout(`${enabled ? "Enabled" : "Disabled"} Source: ${value}`);
146
228
  return;
147
229
  }
@@ -152,117 +234,207 @@ function printHelp(io) {
152
234
  "Daily Brief Operational CLI",
153
235
  "",
154
236
  "Usage:",
155
- " daily-brief setup [--force]",
156
- " daily-brief run-once",
157
- " daily-brief collect",
158
- " daily-brief generate",
159
- " daily-brief deliver",
237
+ " daily-brief setup",
238
+ " daily-brief run-once [--date YYYY-MM-DD]",
160
239
  " daily-brief status",
161
- " daily-brief model configure [--provider <provider> --model <model> --credential-ref <ref>]",
162
- " daily-brief model login [--provider openai-codex --credential-ref <ref>]",
163
- " daily-brief model logout [--credential-ref <ref>]",
164
- " daily-brief model status",
165
- " daily-brief delivery configure --enabled true|false [--webhook-ref <ref> --webhook-url <url>]",
166
- " daily-brief delivery status",
167
- " daily-brief delivery test",
168
240
  " daily-brief sources list",
169
241
  " daily-brief sources edit",
170
242
  " daily-brief sources validate",
171
243
  " daily-brief sources enable <source-id>",
172
- " daily-brief sources disable <source-id>"
244
+ " daily-brief sources disable <source-id>",
245
+ " daily-brief version"
173
246
  ].join("\n"));
174
247
  }
175
248
  async function handleSetupCommand(args, io, env) {
176
249
  const flags = parseFlags(args);
177
- const force = flags.force === "true";
250
+ if (Object.keys(flags).length > 0 || args.some((arg) => !arg.startsWith("--"))) {
251
+ throw new Error("daily-brief setup does not accept flags. Re-run setup and choose what to preserve or update interactively.");
252
+ }
253
+ requireInteractiveSetup(io);
178
254
  const paths = resolveDailyBriefPaths(env);
179
- const timezone = env.TZ?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
255
+ const existingConfig = readDailyBriefConfig(paths.configPath);
180
256
  await mkdir(paths.home, { recursive: true });
181
257
  await mkdir(paths.sourceItemRoot, { recursive: true });
182
258
  await mkdir(paths.agentRunRoot, { recursive: true });
183
259
  await mkdir(paths.briefArchiveRoot, { recursive: true });
184
- await writeIfNeeded(paths.configPath, [
185
- `timezone: ${timezone}`,
186
- "brief:",
187
- " language: zh",
188
- " maxSignals: 5",
189
- "model:",
190
- " provider: openai-codex",
191
- " model: gpt-5.5",
192
- " credentialRef: openai-codex.default",
193
- "delivery:",
194
- " enabled: false",
195
- ""
196
- ].join("\n"), force);
197
- await writeIfNeeded(paths.sourceRegistryPath, defaultSourceRegistryExample(), force);
198
- if (force || !(await exists(paths.authPath))) {
260
+ await writeSetupBaseConfig(paths.configPath, {
261
+ ...existingConfig,
262
+ brief: normalizeBriefConfig(existingConfig.brief)
263
+ });
264
+ if (!(await exists(paths.authPath))) {
199
265
  writeCredentialStore({ credentials: {} }, paths.authPath);
200
266
  }
267
+ if (await exists(paths.sourceRegistryPath)) {
268
+ io.stdout(`Source Registry: ${paths.sourceRegistryPath}`);
269
+ try {
270
+ io.stdout(formatSourceRegistry(await loadSourceRegistry(paths.sourceRegistryPath)));
271
+ }
272
+ catch (error) {
273
+ const message = error instanceof Error ? error.message : String(error);
274
+ io.stdout(`Source Registry invalid:\n${message}`);
275
+ }
276
+ const reinitialize = await promptYesNo(io, "Reinitialize example Sources? Existing sources.yaml will be overwritten.", false);
277
+ if (reinitialize) {
278
+ await writeFile(paths.sourceRegistryPath, defaultSourceRegistryExample(), "utf8");
279
+ io.stdout("Source Registry reinitialized from the packaged example.");
280
+ }
281
+ else {
282
+ io.stdout("Source Registry preserved.");
283
+ }
284
+ }
285
+ else {
286
+ await writeFile(paths.sourceRegistryPath, defaultSourceRegistryExample(), "utf8");
287
+ io.stdout(`Source Registry initialized: ${paths.sourceRegistryPath}`);
288
+ io.stdout(formatSourceRegistry(await loadSourceRegistry(paths.sourceRegistryPath)));
289
+ }
290
+ io.stdout("To edit Sources:");
291
+ io.stdout(" daily-brief sources edit");
292
+ io.stdout(" daily-brief sources validate");
293
+ io.stdout(" daily-brief sources list");
294
+ await configureModelThroughSetup(io, env);
295
+ await configureDeliveryThroughSetup(io, env);
296
+ const readiness = readModelRuntimeConfig(env);
297
+ const delivery = readDeliveryConfig(paths.configPath);
201
298
  io.stdout(`Daily Brief home: ${paths.home}`);
202
299
  io.stdout(`Daily Brief data: ${paths.dataHome}`);
203
- io.stdout(`Timezone: ${timezone}`);
204
300
  io.stdout(`Source Registry: ${paths.sourceRegistryPath}`);
205
- io.stdout("Next: daily-brief sources validate");
206
- io.stdout("Next: daily-brief model configure");
207
- io.stdout("Optional: daily-brief delivery configure --enabled true --webhook-url <url>");
208
- io.stdout("Readiness: config files present, Source Registry initialized, data directories writable, delivery disabled");
209
- }
210
- async function handleDeliveryCommand(args, io, env) {
211
- const [subcommand, ...rest] = args;
212
- const flags = parseFlags(rest);
301
+ io.stdout(`Model: ${readiness.provider}/${readiness.model}`);
302
+ io.stdout(`Model credential: ${readiness.ready ? "configured" : "missing"}`);
303
+ io.stdout(`Discord delivery: ${delivery?.enabled ? "enabled" : "disabled"}`);
304
+ io.stdout("Ready to run:");
305
+ io.stdout(" daily-brief run-once");
306
+ }
307
+ function requireInteractiveSetup(io) {
308
+ if (!io.prompt || io.interactive === false) {
309
+ throw new Error(nonInteractiveSetupMessage());
310
+ }
311
+ }
312
+ function nonInteractiveSetupMessage() {
313
+ return [
314
+ "daily-brief setup requires an interactive terminal.",
315
+ "For CI or scripted setup, create ~/.daily-brief/config.yaml, ~/.daily-brief/sources.yaml, and ~/.daily-brief/auth.json directly.",
316
+ "Use DAILY_BRIEF_HOME to choose a configuration root and DAILY_BRIEF_DATA_HOME to choose a generated-data root.",
317
+ "Run daily-brief sources validate after writing sources.yaml."
318
+ ].join("\n");
319
+ }
320
+ async function writeSetupBaseConfig(path, config) {
321
+ await mkdir(dirname(path), { recursive: true });
322
+ await writeFile(path, stringify(config), "utf8");
323
+ }
324
+ function normalizeBriefConfig(value) {
325
+ const current = isRecord(value) ? value : {};
326
+ return {
327
+ ...current,
328
+ language: "zh",
329
+ maxSignals: typeof current.maxSignals === "number" ? current.maxSignals : 5
330
+ };
331
+ }
332
+ async function configureModelThroughSetup(io, env) {
213
333
  const paths = resolveDailyBriefPaths(env);
214
- if (subcommand === "configure") {
215
- const enabled = flags.enabled === "true" || flags.enabled === "yes";
216
- const webhookRef = flags["webhook-ref"] ?? "discord.default";
217
- writeDeliveryConfig({ enabled, ...(enabled ? { webhookRef } : {}) }, paths.configPath);
218
- if (enabled && flags["webhook-url"]) {
219
- putCredential(webhookRef, { type: "webhook", provider: "discord", webhookUrl: flags["webhook-url"] }, paths.authPath);
220
- }
221
- io.stdout(`Discord delivery: ${enabled ? "enabled" : "disabled"}`);
222
- if (enabled)
223
- io.stdout(`Webhook Ref: ${webhookRef}`);
334
+ const current = readDailyBriefConfig(paths.configPath).model ?? defaultModelConfig();
335
+ const provider = readProviderFlag(await promptWithDefault(io, "LLM Provider (openai-codex/openai/deepseek/openai-compatible)", current.provider));
336
+ const model = await promptWithDefault(io, "Model", current.provider === provider ? current.model : defaultModelForProvider(provider));
337
+ io.stdout("Credential name identifies where Daily Brief finds the model secret in auth.json. Use the default unless you manage multiple credentials.");
338
+ const credentialRef = await promptWithDefault(io, "Model credential name", current.provider === provider ? current.credentialRef ?? defaultCredentialRef(provider) ?? "" : defaultCredentialRef(provider) ?? "");
339
+ const baseUrl = provider === "openai-compatible"
340
+ ? await promptWithDefault(io, "Base URL", current.provider === provider ? current.baseUrl ?? "" : "")
341
+ : undefined;
342
+ const config = {
343
+ provider,
344
+ model,
345
+ ...(credentialRef ? { credentialRef } : {}),
346
+ ...(baseUrl ? { baseUrl } : {})
347
+ };
348
+ writeModelConfig(config, paths.configPath);
349
+ io.stdout(`Model configured: ${provider}/${model}`);
350
+ if (credentialRef) {
351
+ io.stdout(`Model credential name: ${credentialRef}`);
352
+ }
353
+ await maybeConfigureModelCredential({ provider, credentialRef, io, env });
354
+ }
355
+ async function maybeConfigureModelCredential(input) {
356
+ if (!input.credentialRef) {
224
357
  return;
225
358
  }
226
- if (subcommand === "status") {
227
- const config = readDeliveryConfig(paths.configPath);
228
- if (!config?.enabled) {
229
- io.stdout("Discord delivery: disabled");
359
+ const paths = resolveDailyBriefPaths(input.env);
360
+ if (input.provider === "openai-codex") {
361
+ const credential = getCredential(input.credentialRef, paths.authPath);
362
+ if (credential) {
363
+ input.io.stdout("Model credential: configured");
230
364
  return;
231
365
  }
232
- const credential = config.webhookRef ? getDeliveryWebhookCredential(config.webhookRef, paths.authPath) : undefined;
233
- io.stdout("Discord delivery: enabled");
234
- io.stdout(`Webhook Ref: ${config.webhookRef ?? "(missing)"}`);
235
- io.stdout(`Webhook: ${credential ? "<redacted>" : "(missing)"}`);
236
- return;
237
- }
238
- if (subcommand === "test") {
239
- const config = readDeliveryConfig(paths.configPath);
240
- const credential = config?.webhookRef ? getDeliveryWebhookCredential(config.webhookRef, paths.authPath) : undefined;
241
- if (!config?.enabled || !credential) {
242
- throw new Error("delivery test requires enabled Discord delivery with a webhook credential");
366
+ if (await promptYesNo(input.io, "Login to openai-codex now?", true)) {
367
+ await loginModelCredential({
368
+ provider: input.provider,
369
+ credentialRef: input.credentialRef,
370
+ io: input.io,
371
+ env: input.env
372
+ });
373
+ input.io.stdout(`Logged in credential: ${input.credentialRef}`);
243
374
  }
244
- const response = await (io.fetchImpl ?? fetch)(credential.webhookUrl, {
245
- method: "POST",
246
- headers: { "content-type": "application/json" },
247
- body: JSON.stringify({ content: "Daily Brief delivery test" })
248
- });
249
- if (!response.ok) {
250
- throw new Error(`Discord delivery test failed with status ${response.status}`);
375
+ else {
376
+ input.io.stdout("Model credential: missing");
251
377
  }
252
- io.stdout("Discord delivery test: sent");
253
378
  return;
254
379
  }
255
- throw new Error(`Unknown delivery command: ${subcommand ?? "(missing)"}`);
256
- }
257
- function getDeliveryWebhookCredential(ref, authPath) {
258
- const credential = getCredential(ref, authPath);
259
- return credential?.type === "webhook" && credential.provider === "discord" ? credential : undefined;
380
+ if (getCredential(input.credentialRef, paths.authPath)) {
381
+ input.io.stdout("Model credential: configured");
382
+ return;
383
+ }
384
+ if (await promptYesNo(input.io, "Store API key in credential store? API key input may be visible in this terminal.", false)) {
385
+ const apiKey = await promptWithDefault(input.io, "API key", "");
386
+ if (apiKey) {
387
+ putCredential(input.credentialRef, toApiKeyCredential(input.provider, apiKey), paths.authPath);
388
+ input.io.stdout(`Stored credential: ${input.credentialRef}`);
389
+ }
390
+ }
391
+ else {
392
+ input.io.stdout("Model credential: missing");
393
+ }
260
394
  }
261
- async function writeIfNeeded(path, contents, force) {
262
- if (!force && (await exists(path))) {
395
+ async function configureDeliveryThroughSetup(io, env) {
396
+ const paths = resolveDailyBriefPaths(env);
397
+ const current = readDeliveryConfig(paths.configPath);
398
+ const enabled = await promptYesNo(io, "Enable Discord delivery?", current?.enabled ?? false);
399
+ if (!enabled) {
400
+ writeDeliveryConfig({ enabled: false }, paths.configPath);
401
+ io.stdout("Discord delivery: disabled");
263
402
  return;
264
403
  }
265
- await writeFile(path, contents, "utf8");
404
+ const webhookRef = await promptWithDefault(io, "Discord webhook credential name", current?.webhookRef ?? "discord.default");
405
+ writeDeliveryConfig({ enabled: true, webhookRef }, paths.configPath);
406
+ const credential = getCredential(webhookRef, paths.authPath);
407
+ if (credential) {
408
+ io.stdout("Discord webhook: configured");
409
+ if (!(await promptYesNo(io, "Replace Discord webhook URL?", false))) {
410
+ return;
411
+ }
412
+ }
413
+ const webhookUrl = await promptWithDefault(io, "Discord webhook URL", "");
414
+ if (webhookUrl) {
415
+ putCredential(webhookRef, { type: "webhook", provider: "discord", webhookUrl }, paths.authPath);
416
+ io.stdout(`Discord webhook stored: ${webhookRef}`);
417
+ }
418
+ else {
419
+ io.stdout("Discord webhook: missing");
420
+ }
421
+ }
422
+ async function promptYesNo(io, label, defaultValue) {
423
+ if (!io.prompt) {
424
+ throw new Error(nonInteractiveSetupMessage());
425
+ }
426
+ const suffix = defaultValue ? "[Y/n]" : "[y/N]";
427
+ const answer = (await io.prompt(`${label} ${suffix}:`)).trim().toLowerCase();
428
+ if (!answer) {
429
+ return defaultValue;
430
+ }
431
+ if (answer === "y" || answer === "yes" || answer === "true") {
432
+ return true;
433
+ }
434
+ if (answer === "n" || answer === "no" || answer === "false") {
435
+ return false;
436
+ }
437
+ throw new Error(`${label} requires y/yes or n/no`);
266
438
  }
267
439
  async function exists(path) {
268
440
  try {
@@ -293,71 +465,6 @@ function defaultSourceRegistryExample() {
293
465
  ""
294
466
  ].join("\n");
295
467
  }
296
- async function handleModelCommand(args, io, env) {
297
- const [subcommand, ...rest] = args;
298
- const flags = parseFlags(rest);
299
- if (subcommand === "configure") {
300
- await configureModel(flags, io, env);
301
- return;
302
- }
303
- if (subcommand === "status") {
304
- const config = readModelRuntimeConfig(env);
305
- io.stdout(`Provider: ${config.provider}`);
306
- io.stdout(`Model: ${config.model}`);
307
- io.stdout(`Credential Ref: ${config.credentialRef ?? "(none)"}`);
308
- io.stdout(`Ready: ${config.ready ? "yes" : "no"}`);
309
- for (const issue of config.issues) {
310
- io.stdout(`- ${issue}`);
311
- }
312
- for (const credential of statusModelCredentials(env)) {
313
- io.stdout(`Credential: ${credential.ref} provider=${credential.provider} type=${credential.type} secret=${credential.secret}`);
314
- }
315
- return;
316
- }
317
- if (subcommand === "login") {
318
- const config = readModelRuntimeConfig(env);
319
- const provider = readProviderFlag(flags.provider ?? config.provider ?? defaultModelConfig().provider);
320
- const credentialRef = flags["credential-ref"] ?? config.credentialRef ?? defaultCredentialRef(provider);
321
- if (!credentialRef) {
322
- throw new Error("model login requires --credential-ref");
323
- }
324
- await loginModelCredential({ provider, credentialRef, io, env });
325
- io.stdout(`Logged in credential: ${credentialRef}`);
326
- return;
327
- }
328
- if (subcommand === "logout") {
329
- const config = readModelRuntimeConfig(env);
330
- const credentialRef = flags["credential-ref"] ?? rest[0] ?? config.credentialRef;
331
- if (!credentialRef) {
332
- throw new Error("model logout requires --credential-ref");
333
- }
334
- logoutModelCredential(credentialRef, env);
335
- io.stdout(`Logged out credential: ${credentialRef}`);
336
- return;
337
- }
338
- throw new Error(`Unknown model command: ${subcommand ?? "(missing)"}`);
339
- }
340
- async function configureModel(flags, io, env) {
341
- const interactive = Object.keys(flags).length === 0;
342
- const provider = readProviderFlag(flags.provider ??
343
- (interactive ? await promptWithDefault(io, "Provider", defaultModelConfig().provider) : missingFlag("provider")));
344
- const model = flags.model ?? (interactive ? await promptWithDefault(io, "Model", defaultModelForProvider(provider)) : missingFlag("model"));
345
- const credentialRef = flags["credential-ref"] ??
346
- (interactive ? await promptWithDefault(io, "Credential ref", defaultCredentialRef(provider) ?? "") : missingFlag("credential-ref"));
347
- const baseUrl = flags["base-url"] ?? (provider === "openai-compatible" && interactive ? await promptWithDefault(io, "Base URL", "") : undefined);
348
- const config = {
349
- provider,
350
- model,
351
- credentialRef,
352
- ...(baseUrl ? { baseUrl } : {})
353
- };
354
- writeModelConfig(config, resolveDailyBriefPaths(env).configPath);
355
- if (flags["api-key"]) {
356
- putCredential(credentialRef, toApiKeyCredential(provider, flags["api-key"]), resolveDailyBriefPaths(env).authPath);
357
- }
358
- io.stdout(`Model configured: ${provider}/${model}`);
359
- io.stdout(`Credential Ref: ${credentialRef}`);
360
- }
361
468
  function parseFlags(args) {
362
469
  const flags = {};
363
470
  for (let index = 0; index < args.length; index += 1) {
@@ -415,87 +522,44 @@ function defaultCredentialRef(provider) {
415
522
  return undefined;
416
523
  }
417
524
  if (provider === "openai") {
418
- return "env:OPENAI_API_KEY";
525
+ return "openai.default";
419
526
  }
420
527
  if (provider === "deepseek") {
421
- return "env:DEEPSEEK_API_KEY";
528
+ return "deepseek.default";
422
529
  }
423
530
  if (provider === "openai-compatible") {
424
- return "env:OPENAI_API_KEY";
531
+ return "openai-compatible.default";
425
532
  }
426
533
  return defaultModelConfig().credentialRef;
427
534
  }
428
535
  async function promptWithDefault(io, label, defaultValue) {
429
536
  if (!io.prompt) {
430
- throw new Error(`model configure requires --${label.toLowerCase().replaceAll(" ", "-")}`);
537
+ throw new Error(nonInteractiveSetupMessage());
431
538
  }
432
539
  const answer = await io.prompt(`${label}${defaultValue ? ` (${defaultValue})` : ""}:`);
433
540
  return answer.trim() || defaultValue;
434
541
  }
435
- function missingFlag(name) {
436
- throw new Error(`model configure requires --${name}`);
437
- }
438
542
  function optionsFromEnv(env) {
439
543
  const paths = resolveDailyBriefPaths(env);
440
- const discordWebhookUrl = resolveConfiguredWebhookUrl(env);
441
544
  return {
545
+ env,
546
+ dataHome: paths.dataHome,
547
+ configPath: paths.configPath,
548
+ authPath: paths.authPath,
442
549
  sourceRegistryPath: paths.sourceRegistryPath,
443
550
  sourceItemRoot: paths.sourceItemRoot,
444
551
  agentRunRoot: paths.agentRunRoot,
445
552
  archiveRoot: paths.briefArchiveRoot,
446
553
  modelRuntimeEnv: env,
447
- ...(discordWebhookUrl ? { discordWebhookUrl } : {}),
448
- ...(env.DAILY_BRIEF_DISCORD_TEMPLATE_PATH ? { discordTemplatePath: env.DAILY_BRIEF_DISCORD_TEMPLATE_PATH } : {})
554
+ discordEnv: env
449
555
  };
450
556
  }
451
- export async function loadDotenvFile(path = ".env", env = process.env) {
452
- let contents;
453
- try {
454
- contents = await readFile(path, "utf8");
455
- }
456
- catch (error) {
457
- if (isNodeError(error) && error.code === "ENOENT") {
458
- return;
459
- }
460
- throw error;
461
- }
462
- for (const line of contents.split("\n")) {
463
- const entry = parseDotenvLine(line);
464
- if (!entry || env[entry.key] !== undefined) {
465
- continue;
466
- }
467
- env[entry.key] = entry.value;
468
- }
469
- }
470
- function parseDotenvLine(line) {
471
- const trimmed = line.trim();
472
- if (trimmed.length === 0 || trimmed.startsWith("#")) {
473
- return undefined;
474
- }
475
- const equalsIndex = trimmed.indexOf("=");
476
- if (equalsIndex <= 0) {
477
- return undefined;
478
- }
479
- const key = trimmed.slice(0, equalsIndex).trim();
480
- const rawValue = trimmed.slice(equalsIndex + 1).trim();
481
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
482
- return undefined;
483
- }
484
- return { key, value: unquoteDotenvValue(rawValue) };
485
- }
486
- function unquoteDotenvValue(value) {
487
- if (value.length >= 2) {
488
- const quote = value[0];
489
- const last = value[value.length - 1];
490
- if ((quote === "\"" || quote === "'") && last === quote) {
491
- return value.slice(1, -1);
492
- }
493
- }
494
- return value;
495
- }
496
557
  function isNodeError(error) {
497
558
  return error instanceof Error && "code" in error;
498
559
  }
560
+ function isRecord(value) {
561
+ return typeof value === "object" && value !== null && !Array.isArray(value);
562
+ }
499
563
  export async function isCliEntrypoint(argvPath, moduleUrl = import.meta.url) {
500
564
  if (!argvPath) {
501
565
  return false;
@@ -516,7 +580,6 @@ isCliEntrypoint(process.argv[1])
516
580
  if (!isEntrypoint) {
517
581
  return false;
518
582
  }
519
- await loadDotenvFile();
520
583
  await runCli(process.argv.slice(2));
521
584
  return true;
522
585
  })