daily-focus 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.
@@ -0,0 +1,27 @@
1
+ // src/install.ts
2
+ import { homedir } from "node:os";
3
+ import { resolve, sep } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ var PACKAGED = import.meta.url.endsWith(".js");
6
+ var ROOT = resolve(import.meta.dirname, "..");
7
+ var IN_NPX_CACHE = ROOT.split(sep).includes("_npx");
8
+ function command(name, args = "") {
9
+ if (PACKAGED) return `daily-focus ${name}${args ? ` ${args}` : ""}`;
10
+ return `npm run ${name === "build-calendar" ? "build:calendar" : name}${args ? ` -- ${args}` : ""}`;
11
+ }
12
+ function ranDirectly(url) {
13
+ const file = fileURLToPath(url);
14
+ return file.endsWith(".ts") && process.argv[1] !== void 0 && resolve(process.argv[1]) === file;
15
+ }
16
+ var APP_CALENDAR_APP = ROOT.includes(`.app${sep}Contents${sep}Resources${sep}`) ? resolve(ROOT, "..", "..", "Helpers", "Daily Focus Calendar.app") : null;
17
+ var USER_CALENDAR_APP = resolve(homedir(), "Library", "Application Support", "Daily Focus", "Daily Focus Calendar.app");
18
+
19
+ export {
20
+ PACKAGED,
21
+ ROOT,
22
+ IN_NPX_CACHE,
23
+ command,
24
+ ranDirectly,
25
+ APP_CALENDAR_APP,
26
+ USER_CALENDAR_APP
27
+ };
@@ -0,0 +1,77 @@
1
+ // src/time.ts
2
+ var MS_PER_DAY = 864e5;
3
+ function localDateKey(d) {
4
+ const y = d.getFullYear();
5
+ const m = String(d.getMonth() + 1).padStart(2, "0");
6
+ const day = String(d.getDate()).padStart(2, "0");
7
+ return `${y}-${m}-${day}`;
8
+ }
9
+ var DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
10
+ function isDateOnly(value) {
11
+ return typeof value === "string" && DATE_ONLY.test(value);
12
+ }
13
+ function parseISO(value) {
14
+ if (typeof value !== "string" || value === "") return null;
15
+ const dateOnly = DATE_ONLY.exec(value);
16
+ if (dateOnly) {
17
+ const [, y, m, d] = dateOnly;
18
+ return new Date(Number(y), Number(m) - 1, Number(d));
19
+ }
20
+ const parsed = new Date(value);
21
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
22
+ }
23
+ function startOfLocalDay(d) {
24
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate());
25
+ }
26
+ function calendarDaysBetween(from, to) {
27
+ const a = startOfLocalDay(from).getTime();
28
+ const b = startOfLocalDay(to).getTime();
29
+ return Math.round((b - a) / MS_PER_DAY);
30
+ }
31
+ function isSameLocalDay(d, reference) {
32
+ return localDateKey(d) === localDateKey(reference);
33
+ }
34
+ function isWorkingDay(d) {
35
+ const day = d.getDay();
36
+ return day !== 0 && day !== 6;
37
+ }
38
+ function workingDaysSince(from, to, counts = isWorkingDay) {
39
+ const cursor = startOfLocalDay(from);
40
+ const end = startOfLocalDay(to);
41
+ let count = 0;
42
+ while (cursor.getTime() < end.getTime()) {
43
+ cursor.setDate(cursor.getDate() + 1);
44
+ if (counts(cursor)) count++;
45
+ }
46
+ return count;
47
+ }
48
+ function workingMsBetween(from, to, counts = isWorkingDay) {
49
+ if (to.getTime() <= from.getTime()) return 0;
50
+ let cursor = from;
51
+ let total = 0;
52
+ while (cursor.getTime() < to.getTime()) {
53
+ const nextMidnight = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() + 1);
54
+ const sliceEnd = Math.min(nextMidnight.getTime(), to.getTime());
55
+ if (counts(cursor)) total += sliceEnd - cursor.getTime();
56
+ cursor = new Date(sliceEnd);
57
+ }
58
+ return total;
59
+ }
60
+ function laterISO(a, b) {
61
+ if (!b) return a ?? null;
62
+ if (!a) return b;
63
+ return b > a ? b : a;
64
+ }
65
+
66
+ export {
67
+ localDateKey,
68
+ isDateOnly,
69
+ parseISO,
70
+ startOfLocalDay,
71
+ calendarDaysBetween,
72
+ isSameLocalDay,
73
+ isWorkingDay,
74
+ workingDaysSince,
75
+ workingMsBetween,
76
+ laterISO
77
+ };
@@ -0,0 +1,425 @@
1
+ import {
2
+ APP_CALENDAR_APP,
3
+ USER_CALENDAR_APP
4
+ } from "./chunk-6VWWJZKJ.js";
5
+ import {
6
+ isSameLocalDay,
7
+ localDateKey,
8
+ parseISO,
9
+ startOfLocalDay
10
+ } from "./chunk-EXGTFXNV.js";
11
+
12
+ // src/env.ts
13
+ import { readFileSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { isAbsolute, resolve } from "node:path";
16
+ import { parseEnv } from "node:util";
17
+ var DOTENV_PATH = resolve(import.meta.dirname, "..", ".env");
18
+ var DEFAULT_DATA_DIR = "~/.daily-focus";
19
+ var SETTINGS_FILE = "settings.json";
20
+ var LAUNCH_SETTINGS = ["DAILY_FOCUS_DATA", "DAILY_FOCUS_PORT", "DAILY_FOCUS_HOST"];
21
+ function expandHome(p) {
22
+ if (p === "~") return homedir();
23
+ if (p.startsWith("~/")) return resolve(homedir(), p.slice(2));
24
+ if (!p.includes("/")) return p;
25
+ return isAbsolute(p) ? p : resolve(process.cwd(), p);
26
+ }
27
+ function readDotEnv(path = DOTENV_PATH) {
28
+ let text;
29
+ try {
30
+ text = readFileSync(path, "utf8");
31
+ } catch (err) {
32
+ if (err.code === "ENOENT") return {};
33
+ throw new Error(`could not read ${path}: ${err.message}`);
34
+ }
35
+ const parsed = {};
36
+ for (const [key, value] of Object.entries(parseEnv(text.replace(/^/, "")))) {
37
+ if (value !== void 0) parsed[key] = value;
38
+ }
39
+ return parsed;
40
+ }
41
+ function readSettingsFile(path) {
42
+ let text;
43
+ try {
44
+ text = readFileSync(path, "utf8");
45
+ } catch (err) {
46
+ if (err.code === "ENOENT") return { values: {}, error: null };
47
+ return { values: {}, error: `could not read ${path}: ${err.message}` };
48
+ }
49
+ let parsed;
50
+ try {
51
+ parsed = JSON.parse(text);
52
+ } catch (err) {
53
+ return { values: {}, error: `${path} is not valid JSON: ${err.message}` };
54
+ }
55
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
56
+ return { values: {}, error: `${path} should hold an object of settings` };
57
+ }
58
+ const values = {};
59
+ const unreadable = [];
60
+ for (const [key, value] of Object.entries(parsed)) {
61
+ if (!key.startsWith("DAILY_FOCUS_") || LAUNCH_SETTINGS.includes(key)) continue;
62
+ if (typeof value === "string") values[key] = value;
63
+ else if (typeof value === "number" && Number.isFinite(value) || typeof value === "boolean") values[key] = String(value);
64
+ else unreadable.push(key);
65
+ }
66
+ const error = unreadable.length > 0 ? `${path} has values that aren't text, a number or true/false: ${unreadable.join(", ")}` : null;
67
+ return { values, error };
68
+ }
69
+ function prefixed(record) {
70
+ const kept = {};
71
+ for (const [key, value] of Object.entries(record)) {
72
+ if (key.startsWith("DAILY_FOCUS_") && value !== void 0) kept[key] = value;
73
+ }
74
+ return kept;
75
+ }
76
+ function mergeEnv(fromFile, processEnv = process.env) {
77
+ return { ...prefixed(fromFile), ...processEnv };
78
+ }
79
+ function envSources(processEnv = process.env, dotenv = readDotEnv()) {
80
+ const merged = mergeEnv(dotenv, processEnv);
81
+ const dataDir = expandHome((merged.DAILY_FOCUS_DATA ?? "").trim() || DEFAULT_DATA_DIR);
82
+ const settingsPath = resolve(dataDir, SETTINGS_FILE);
83
+ const { values, error } = readSettingsFile(settingsPath);
84
+ return { dotenv: prefixed(dotenv), settings: values, process: processEnv, settingsPath, settingsError: error };
85
+ }
86
+ function layerEnv(sources, settings = sources.settings) {
87
+ return { ...sources.dotenv, ...withoutBlanks(settings), ...withoutBlanks(sources.process) };
88
+ }
89
+ function withoutBlanks(record) {
90
+ const kept = {};
91
+ for (const [key, value] of Object.entries(record)) {
92
+ if (value !== void 0 && value.trim() !== "") kept[key] = value;
93
+ }
94
+ return kept;
95
+ }
96
+ function loadEnv() {
97
+ return layerEnv(envSources());
98
+ }
99
+
100
+ // src/schedule.ts
101
+ var BRIEF_REFRESH_GRACE_HOURS = 45 / 60;
102
+ var MONDAY_TO_FRIDAY = [1, 2, 3, 4, 5];
103
+ var DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
104
+ function runsOn(schedule, date) {
105
+ return schedule.days.includes(date.getDay());
106
+ }
107
+ function nextRunDate(schedule, from) {
108
+ const cursor = startOfLocalDay(from);
109
+ for (let i = 0; i < 7; i++) {
110
+ cursor.setDate(cursor.getDate() + 1);
111
+ if (runsOn(schedule, cursor)) return localDateKey(cursor);
112
+ }
113
+ return null;
114
+ }
115
+ function describeSchedule(schedule) {
116
+ const days = schedule.days;
117
+ if (days.length === 0) return "never";
118
+ const contiguous = days.every((day, i) => i === 0 || day === days[i - 1] + 1);
119
+ return contiguous && days.length > 2 ? `${DAY_NAMES[days[0]]}\u2013${DAY_NAMES[days[days.length - 1]]}` : days.map((day) => DAY_NAMES[day]).join(", ");
120
+ }
121
+ function parseWeekdays(spec) {
122
+ const days = /* @__PURE__ */ new Set();
123
+ for (const part of spec.split(",")) {
124
+ const piece = part.trim();
125
+ if (piece === "") continue;
126
+ const range = /^([0-6])\s*-\s*([0-6])$/.exec(piece);
127
+ if (range) {
128
+ const from = Number(range[1]);
129
+ const to = Number(range[2]);
130
+ if (from > to) {
131
+ throw new Error(`day range "${piece}" runs backwards; write it as a list, e.g. "6,0"`);
132
+ }
133
+ for (let day = from; day <= to; day++) days.add(day);
134
+ continue;
135
+ }
136
+ if (!/^[0-6]$/.test(piece)) {
137
+ throw new Error(`"${piece}" is not a weekday; use 0\u20136 where 0 is Sunday, e.g. "1-5"`);
138
+ }
139
+ days.add(Number(piece));
140
+ }
141
+ if (days.size === 0) throw new Error("no weekdays given");
142
+ return [...days].sort((a, b) => a - b);
143
+ }
144
+ function resolveSchedule(config) {
145
+ if (config.agentDays) return { days: config.agentDays, source: "config" };
146
+ return { days: MONDAY_TO_FRIDAY, source: "default" };
147
+ }
148
+ function atTimeOn(date, at) {
149
+ const when = startOfLocalDay(date);
150
+ when.setHours(at.hour, at.minute, 0, 0);
151
+ return when;
152
+ }
153
+ function describeTime(at) {
154
+ return `${String(at.hour).padStart(2, "0")}:${String(at.minute).padStart(2, "0")}`;
155
+ }
156
+ function scheduledRunDue({ at, schedule, lastRunStartedAt, now }) {
157
+ if (!runsOn(schedule, now)) return false;
158
+ if (now.getTime() < atTimeOn(now, at).getTime()) return false;
159
+ return !ranToday(lastRunStartedAt, now);
160
+ }
161
+ function nextScheduledRun({ at, schedule, lastRunStartedAt, now }) {
162
+ const today = atTimeOn(now, at);
163
+ if (runsOn(schedule, now) && now.getTime() < today.getTime() && !ranToday(lastRunStartedAt, now)) {
164
+ return today;
165
+ }
166
+ const next = nextRunDate(schedule, now);
167
+ if (!next) return null;
168
+ const date = parseISO(next);
169
+ return date ? atTimeOn(date, at) : null;
170
+ }
171
+ function ranToday(lastRunStartedAt, now) {
172
+ const when = parseISO(lastRunStartedAt);
173
+ return when !== null && isSameLocalDay(when, now);
174
+ }
175
+
176
+ // src/config.ts
177
+ import { existsSync } from "node:fs";
178
+ import { resolve as resolve2 } from "node:path";
179
+ var PROFILES = ["work", "personal"];
180
+ var CLI_NAMES = ["claude", "codex"];
181
+ function envInt(name, fallback, env) {
182
+ const raw = env[name];
183
+ if (raw === void 0 || raw.trim() === "") return fallback;
184
+ const n = Number(raw);
185
+ if (!Number.isFinite(n)) {
186
+ throw new Error(`${name} must be a number, got ${JSON.stringify(raw)}`);
187
+ }
188
+ return n;
189
+ }
190
+ function envBounded(name, fallback, min, max, env) {
191
+ const n = envInt(name, fallback, env);
192
+ if (!Number.isInteger(n) || n < min || n > max) {
193
+ throw new Error(`${name} must be a whole number from ${min} to ${max}, got ${JSON.stringify(env[name])}`);
194
+ }
195
+ return n;
196
+ }
197
+ function envNameList(name, env) {
198
+ const raw = env[name];
199
+ if (raw === void 0) return [];
200
+ const names = [];
201
+ for (const entry of raw.split(",")) {
202
+ const trimmed = entry.trim();
203
+ if (trimmed !== "" && !names.includes(trimmed)) names.push(trimmed);
204
+ }
205
+ return names;
206
+ }
207
+ function envList(name, env) {
208
+ const raw = env[name];
209
+ if (raw === void 0) return [];
210
+ return raw.split(/[\s,]+/).map((entry) => entry.trim()).filter((entry) => entry !== "");
211
+ }
212
+ function parseScope(entries) {
213
+ const qualifiers = [];
214
+ for (const entry of entries) {
215
+ const bare = entry.replace(/^(org|repo|user):/i, "");
216
+ if (bare === "") continue;
217
+ const qualifier = bare.includes("/") ? `repo:${bare}` : `org:${bare}`;
218
+ if (!qualifiers.includes(qualifier)) qualifiers.push(qualifier);
219
+ }
220
+ return qualifiers;
221
+ }
222
+ function envFlag(name, fallback, env) {
223
+ const flag = (env[name] ?? "").trim().toLowerCase();
224
+ if (flag === "") return fallback;
225
+ return !["off", "false", "0", "no"].includes(flag);
226
+ }
227
+ function envGitHub(env) {
228
+ const pollMinutes = envInt("DAILY_FOCUS_GITHUB_POLL_MINUTES", 5, env);
229
+ if (pollMinutes < 1) {
230
+ throw new Error(`DAILY_FOCUS_GITHUB_POLL_MINUTES must be at least 1, got ${pollMinutes}`);
231
+ }
232
+ return {
233
+ enabled: envFlag("DAILY_FOCUS_GITHUB", true, env),
234
+ accounts: envList("DAILY_FOCUS_GITHUB_ACCOUNTS", env),
235
+ scope: parseScope(envList("DAILY_FOCUS_GITHUB_SCOPE", env)),
236
+ mergeGateChecks: envNameList("DAILY_FOCUS_GITHUB_MERGE_GATE_CHECKS", env),
237
+ pollMinutes,
238
+ ghPath: expandHome((env.DAILY_FOCUS_GH ?? "").trim() || "gh")
239
+ };
240
+ }
241
+ function envCalendar(env) {
242
+ const pollMinutes = envInt("DAILY_FOCUS_CALENDAR_POLL_MINUTES", 5, env);
243
+ if (pollMinutes < 1) {
244
+ throw new Error(`DAILY_FOCUS_CALENDAR_POLL_MINUTES must be at least 1, got ${pollMinutes}`);
245
+ }
246
+ return {
247
+ enabled: envFlag("DAILY_FOCUS_CALENDAR", true, env),
248
+ // Commas only, as for merge gate checks: calendar names contain spaces.
249
+ names: envNameList("DAILY_FOCUS_CALENDARS", env),
250
+ addresses: envNameList("DAILY_FOCUS_CALENDAR_ADDRESSES", env),
251
+ pollMinutes,
252
+ // What the user set, then the helper the Mac app carries, then the one
253
+ // `daily-focus build-calendar` installs, then a checkout's own build.
254
+ appPath: expandHome(
255
+ (env.DAILY_FOCUS_CALENDAR_APP ?? "").trim() || [APP_CALENDAR_APP, USER_CALENDAR_APP].find((path) => path !== null && existsSync(path)) || resolve2(import.meta.dirname, "..", "tools/dfcal/build/Daily Focus Calendar.app")
256
+ )
257
+ };
258
+ }
259
+ function envJira(env, profile) {
260
+ const pollMinutes = envInt("DAILY_FOCUS_JIRA_POLL_MINUTES", 15, env);
261
+ if (pollMinutes < 1) {
262
+ throw new Error(`DAILY_FOCUS_JIRA_POLL_MINUTES must be at least 1, got ${pollMinutes}`);
263
+ }
264
+ return {
265
+ enabled: envFlag("DAILY_FOCUS_JIRA", profile === "work", env),
266
+ projects: envList("DAILY_FOCUS_JIRA_PROJECTS", env),
267
+ // Commas only, as for merge gate checks and calendars: status names contain
268
+ // spaces almost by default — "In Review", "Waiting for customer".
269
+ holdStatuses: envNameList("DAILY_FOCUS_JIRA_HOLD_STATUSES", env),
270
+ inProgressStatuses: envNameList("DAILY_FOCUS_JIRA_IN_PROGRESS_STATUSES", env),
271
+ pollMinutes,
272
+ // Tolerates a pasted URL, since that is what is on screen when someone goes
273
+ // looking for their site's name.
274
+ site: envString("DAILY_FOCUS_JIRA_SITE", "", env).replace(/^https?:\/\//i, "").replace(/\/.*$/, "") || null,
275
+ acliPath: expandHome((env.DAILY_FOCUS_ACLI ?? "").trim() || "acli")
276
+ };
277
+ }
278
+ function envCli(name, env) {
279
+ const raw = envString(name, "off", env).toLowerCase();
280
+ if (["off", "false", "0", "no", "none"].includes(raw)) return null;
281
+ if (!CLI_NAMES.includes(raw)) {
282
+ throw new Error(`${name} must be off or one of ${CLI_NAMES.join(", ")}, got ${JSON.stringify(raw)}`);
283
+ }
284
+ return raw;
285
+ }
286
+ function envAssistant(env) {
287
+ const agent = envCli("DAILY_FOCUS_ASSISTANT", env);
288
+ const tools = envNameList("DAILY_FOCUS_ASSISTANT_TOOLS", env);
289
+ return {
290
+ agent,
291
+ binPath: expandHome((env.DAILY_FOCUS_ASSISTANT_BIN ?? "").trim() || (agent ?? "claude")),
292
+ model: envString("DAILY_FOCUS_ASSISTANT_MODEL", "", env) || null,
293
+ effort: envString("DAILY_FOCUS_ASSISTANT_EFFORT", "", env) || null,
294
+ tools: tools.length > 0 ? tools : DEFAULT_ASSISTANT_TOOLS
295
+ };
296
+ }
297
+ var DEFAULT_ASSISTANT_TOOLS = ["Bash(gh *)", "Bash(acli *)", "WebFetch", "mcp__claude_ai_Gmail"];
298
+ var DEFAULT_AGENT_TOOLS = ["Bash(gh *)", "Bash(acli *)", "WebFetch", "mcp__claude_ai_Gmail"];
299
+ function briefWritingTools() {
300
+ const repo = resolve2(import.meta.dirname, "..");
301
+ return [
302
+ // `//` anchors a rule at the filesystem root; the path supplies one slash.
303
+ `Read(/${repo}/prompts/**)`,
304
+ `Read(/${repo}/schema/**)`,
305
+ "Write(./items.json)",
306
+ "Write(./items.json.*)",
307
+ "Edit(./items.json.*)",
308
+ "Bash(mv items.json.* items.json)"
309
+ ];
310
+ }
311
+ var DEFAULT_AGENT_AT = { hour: 7, minute: 0 };
312
+ function envAgent(env) {
313
+ const cli = envCli("DAILY_FOCUS_AGENT", env);
314
+ const at = envTimeOfDay("DAILY_FOCUS_AGENT_AT", env);
315
+ if (at && !cli && envString("DAILY_FOCUS_AGENT_AT", "", env) !== "") {
316
+ throw new Error("DAILY_FOCUS_AGENT_AT needs DAILY_FOCUS_AGENT set to codex or claude");
317
+ }
318
+ const tools = envNameList("DAILY_FOCUS_AGENT_TOOLS", env);
319
+ return {
320
+ cli,
321
+ at: cli ? at : null,
322
+ binPath: expandHome((env.DAILY_FOCUS_AGENT_BIN ?? "").trim() || (cli ?? "codex")),
323
+ model: envString("DAILY_FOCUS_AGENT_MODEL", "", env) || null,
324
+ effort: envString("DAILY_FOCUS_AGENT_EFFORT", "", env) || null,
325
+ tools: tools.length > 0 ? tools : DEFAULT_AGENT_TOOLS
326
+ };
327
+ }
328
+ function envTimeOfDay(name, env) {
329
+ const raw = envString(name, "", env).toLowerCase();
330
+ if (raw === "") return DEFAULT_AGENT_AT;
331
+ if (["off", "false", "0", "no", "none", "never"].includes(raw)) return null;
332
+ const match = /^([01]?\d|2[0-3]):([0-5]\d)$/.exec(raw);
333
+ if (!match) throw new Error(`${name} must be a 24-hour time such as 06:30, or off, got ${JSON.stringify(raw)}`);
334
+ return { hour: Number(match[1]), minute: Number(match[2]) };
335
+ }
336
+ function envWeekdays(name, env) {
337
+ const raw = env[name];
338
+ if (raw === void 0 || raw.trim() === "") return null;
339
+ try {
340
+ return parseWeekdays(raw);
341
+ } catch (error) {
342
+ throw new Error(`${name}: ${error instanceof Error ? error.message : String(error)}`);
343
+ }
344
+ }
345
+ function envString(name, fallback, env) {
346
+ const raw = env[name];
347
+ return raw === void 0 || raw.trim() === "" ? fallback : raw.trim();
348
+ }
349
+ function workingDay(env) {
350
+ const workStartHour = envBounded("DAILY_FOCUS_WORK_START", 9, 0, 23, env);
351
+ const workEndHour = envBounded("DAILY_FOCUS_WORK_END", 17, 1, 24, env);
352
+ if (workEndHour <= workStartHour) {
353
+ throw new Error(`DAILY_FOCUS_WORK_END (${workEndHour}) must be later than DAILY_FOCUS_WORK_START (${workStartHour})`);
354
+ }
355
+ return { workStartHour, workEndHour };
356
+ }
357
+ function envProfile(env) {
358
+ const raw = envString("DAILY_FOCUS_PROFILE", "work", env).toLowerCase();
359
+ if (!PROFILES.includes(raw)) {
360
+ throw new Error(`DAILY_FOCUS_PROFILE must be one of ${PROFILES.join(", ")}, got ${JSON.stringify(raw)}`);
361
+ }
362
+ return raw;
363
+ }
364
+ function loadConfig(env = loadEnv()) {
365
+ const dataDir = expandHome(envString("DAILY_FOCUS_DATA", DEFAULT_DATA_DIR, env));
366
+ const profile = envProfile(env);
367
+ return {
368
+ profile,
369
+ dataDir,
370
+ itemsFile: resolve2(dataDir, "items.json"),
371
+ actionsFile: resolve2(dataDir, "actions.jsonl"),
372
+ focusFile: resolve2(dataDir, "focus.md"),
373
+ settingsFile: resolve2(dataDir, SETTINGS_FILE),
374
+ sourcesFile: resolve2(dataDir, "sources.md"),
375
+ promptFile: resolve2(dataDir, "prompt.md"),
376
+ promptSource: resolve2(import.meta.dirname, "..", "prompts", `morning-brief-${profile}.md`),
377
+ schemaFile: resolve2(dataDir, "items.schema.json"),
378
+ schemaSource: resolve2(import.meta.dirname, "..", "schema", "items.schema.json"),
379
+ archiveDir: resolve2(dataDir, "archive"),
380
+ sessionFile: resolve2(dataDir, "session.json"),
381
+ sessionsLogFile: resolve2(dataDir, "sessions.jsonl"),
382
+ pullsFile: resolve2(dataDir, "prs.json"),
383
+ calendarFile: resolve2(dataDir, "calendar.json"),
384
+ ticketsFile: resolve2(dataDir, "tickets.json"),
385
+ // The same 1 to 120 the session endpoint holds a start to.
386
+ sessionMinutes: envBounded("DAILY_FOCUS_SESSION_MINUTES", 25, 1, 120, env),
387
+ awayAfterMinutes: envBounded("DAILY_FOCUS_AWAY_AFTER", profile === "personal" ? 0 : 10, 0, 24 * 60, env),
388
+ port: envBounded("DAILY_FOCUS_PORT", 4321, 0, 65535, env),
389
+ host: envString("DAILY_FOCUS_HOST", "127.0.0.1", env),
390
+ freeWindows: envFlag("DAILY_FOCUS_FREE_WINDOWS", profile !== "personal", env),
391
+ ...workingDay(env),
392
+ minFreeWindowMinutes: envBounded("DAILY_FOCUS_MIN_FREE_WINDOW", 45, 1, 24 * 60, env),
393
+ staleAfterHours: envBounded("DAILY_FOCUS_STALE_AFTER_HOURS", 24, 1, 24 * 30, env),
394
+ agentDays: envWeekdays("DAILY_FOCUS_AGENT_DAYS", env),
395
+ github: envGitHub(env),
396
+ calendar: envCalendar(env),
397
+ jira: envJira(env, profile),
398
+ assistant: envAssistant(env),
399
+ assistantLogFile: resolve2(dataDir, "assistant.jsonl"),
400
+ assistantDir: resolve2(dataDir, "assistant"),
401
+ assistantPromptFile: resolve2(dataDir, "assistant.md"),
402
+ assistantPromptSource: resolve2(import.meta.dirname, "..", "prompts", "assistant.md"),
403
+ agent: envAgent(env),
404
+ agentLogFile: resolve2(dataDir, "agent.jsonl")
405
+ };
406
+ }
407
+
408
+ export {
409
+ LAUNCH_SETTINGS,
410
+ readDotEnv,
411
+ envSources,
412
+ layerEnv,
413
+ BRIEF_REFRESH_GRACE_HOURS,
414
+ runsOn,
415
+ nextRunDate,
416
+ describeSchedule,
417
+ resolveSchedule,
418
+ describeTime,
419
+ scheduledRunDue,
420
+ nextScheduledRun,
421
+ PROFILES,
422
+ CLI_NAMES,
423
+ briefWritingTools,
424
+ loadConfig
425
+ };