daily-focus 1.2.0-dev.10 → 1.2.0-dev.11

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.
@@ -3,10 +3,13 @@ import {
3
3
  Assistant,
4
4
  AutoAsk,
5
5
  CodexGate,
6
+ codexLatestModel,
6
7
  codexVersionWarning,
8
+ computerUseOff,
7
9
  countBoard,
10
+ refusalFor,
8
11
  resolveBoard
9
- } from "./chunk-ODJGQCM3.js";
12
+ } from "./chunk-EV44BC2F.js";
10
13
  import {
11
14
  FOCUS_TEMPLATE,
12
15
  sourcesTemplate
@@ -14,7 +17,7 @@ import {
14
17
  import {
15
18
  TimeZoneKeeper,
16
19
  validZone
17
- } from "./chunk-Z4SFMMRQ.js";
20
+ } from "./chunk-GVLNDQVQ.js";
18
21
  import {
19
22
  describeLink,
20
23
  linkStore,
@@ -27,7 +30,7 @@ import {
27
30
  foldActionLog,
28
31
  tempPathFor,
29
32
  writeJsonAtomic
30
- } from "./chunk-RZNQJ6UG.js";
33
+ } from "./chunk-CPJ6LOVC.js";
31
34
  import {
32
35
  CLI_NAMES,
33
36
  LAUNCH_SETTINGS,
@@ -35,6 +38,7 @@ import {
35
38
  briefWritingTools,
36
39
  describeTime,
37
40
  envSources,
41
+ expandHome,
38
42
  laterISO,
39
43
  layerEnv,
40
44
  loadConfig,
@@ -42,7 +46,7 @@ import {
42
46
  readDotEnv,
43
47
  resolveSchedule,
44
48
  scheduledRunDue
45
- } from "./chunk-J7ZYVUG6.js";
49
+ } from "./chunk-4DZEBUPU.js";
46
50
  import {
47
51
  PACKAGED,
48
52
  command,
@@ -54,19 +58,259 @@ import {
54
58
  import { createServer } from "node:http";
55
59
  import { createReadStream } from "node:fs";
56
60
  import { stat as stat4 } from "node:fs/promises";
57
- import { basename, extname, join as join4, normalize, resolve, sep } from "node:path";
61
+ import { basename, extname, join as join6, normalize, resolve, sep } from "node:path";
58
62
  import { fileURLToPath } from "node:url";
59
63
 
64
+ // src/clis.ts
65
+ import { execFile as execFile2 } from "node:child_process";
66
+ import { homedir } from "node:os";
67
+ import { join as join2 } from "node:path";
68
+
69
+ // src/setup.ts
70
+ import { execFile } from "node:child_process";
71
+ import { access, constants, readdir, readFile, stat } from "node:fs/promises";
72
+ import { delimiter, isAbsolute, join } from "node:path";
73
+ import { promisify } from "node:util";
74
+ var run = promisify(execFile);
75
+ var PLACEHOLDERS = [
76
+ "Your Name",
77
+ "you@example.com",
78
+ "you@personal.example",
79
+ "your-github-login",
80
+ "Some Shared Calendar",
81
+ "Some Noisy Calendar",
82
+ "<where your colleagues are>",
83
+ "<where you are>",
84
+ "<where you live>",
85
+ "<document id>"
86
+ ];
87
+ function placeholdersIn(text) {
88
+ return PLACEHOLDERS.filter((placeholder) => text.includes(placeholder)).length;
89
+ }
90
+ async function findExecutable(name, path = process.env.PATH ?? "") {
91
+ const candidates = isAbsolute(name) ? [name] : path.split(delimiter).filter(Boolean).map((dir) => join(dir, name));
92
+ for (const candidate of candidates) {
93
+ try {
94
+ await access(candidate, constants.X_OK);
95
+ if ((await stat(candidate)).isFile()) return candidate;
96
+ } catch {
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+ async function findClis(config) {
102
+ const found = {};
103
+ for (const cli2 of CLI_NAMES) {
104
+ const candidates = [
105
+ config.agent.cli === cli2 ? config.agent.binPath : null,
106
+ config.assistant.agent === cli2 ? config.assistant.binPath : null,
107
+ cli2
108
+ ].filter((candidate) => candidate !== null);
109
+ found[cli2] = null;
110
+ for (const candidate of candidates) {
111
+ found[cli2] = await findExecutable(candidate);
112
+ if (found[cli2]) break;
113
+ }
114
+ }
115
+ return found;
116
+ }
117
+ async function hasArchive(config) {
118
+ try {
119
+ return (await readdir(config.archiveDir)).some((name) => name.endsWith(".json"));
120
+ } catch {
121
+ return false;
122
+ }
123
+ }
124
+ async function exists(path) {
125
+ try {
126
+ await stat(path);
127
+ return true;
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+ async function firstBriefPending(config) {
133
+ return !await exists(config.itemsFile) && !await hasArchive(config);
134
+ }
135
+ async function setupState(config) {
136
+ let sources = null;
137
+ try {
138
+ sources = await readFile(config.sourcesFile, "utf8");
139
+ } catch {
140
+ sources = null;
141
+ }
142
+ const supported = process.platform === "darwin";
143
+ return {
144
+ // The file, not whether it parsed: a brief that is there but broken is a
145
+ // problem for its banner to report, not a store to set up from scratch. The
146
+ // agent's clock asks the same question.
147
+ needed: await firstBriefPending(config),
148
+ profile: config.profile,
149
+ sources: { exists: sources !== null, placeholders: sources === null ? 0 : placeholdersIn(sources) },
150
+ clis: await findClis(config),
151
+ calendar: {
152
+ supported,
153
+ built: supported && await exists(config.calendar.appPath),
154
+ chosen: config.calendar.names.length,
155
+ buildCommand: command("build-calendar")
156
+ }
157
+ };
158
+ }
159
+ var identity = null;
160
+ function ghLogins(json) {
161
+ try {
162
+ const hosts = JSON.parse(json).hosts ?? {};
163
+ const accounts = Array.isArray(hosts["github.com"]) ? hosts["github.com"] : [];
164
+ return accounts.filter((account) => typeof account.login === "string" && account.state !== "error").sort((a, b) => Number(b.active === true) - Number(a.active === true)).map((account) => account.login);
165
+ } catch {
166
+ return [];
167
+ }
168
+ }
169
+ function detectIdentity(config) {
170
+ identity ??= (async () => {
171
+ const [user, logins, email] = await Promise.all([
172
+ run(config.github.ghPath, ["api", "user", "--jq", '[.login, .name // ""] | join("\\n")'], { timeout: 5e3 }).then(
173
+ ({ stdout }) => stdout.split("\n"),
174
+ () => []
175
+ ),
176
+ run(config.github.ghPath, ["auth", "status", "--json", "hosts"], { timeout: 5e3 }).then(
177
+ ({ stdout }) => ghLogins(stdout),
178
+ () => []
179
+ ),
180
+ run("git", ["config", "--global", "user.email"], { timeout: 2e3 }).then(
181
+ ({ stdout }) => stdout.trim() || null,
182
+ () => null
183
+ )
184
+ ]);
185
+ const login = config.github.accounts[0] ?? (user[0]?.trim() || null);
186
+ return {
187
+ login,
188
+ logins: logins.length ? logins : login ? [login] : [],
189
+ name: user[1]?.trim() || null,
190
+ email
191
+ };
192
+ })();
193
+ return identity;
194
+ }
195
+ function fillIdentity(template, who) {
196
+ let text = template;
197
+ if (who.name) text = text.replaceAll("`Your Name`", `\`${who.name}\``);
198
+ if (who.email) text = text.replaceAll("`you@example.com`", `\`${who.email}\``).replaceAll("`you@personal.example`", `\`${who.email}\``);
199
+ if (who.login) text = text.replaceAll("`your-github-login`", `\`${who.login}\``);
200
+ return text;
201
+ }
202
+
203
+ // src/clis.ts
204
+ var CLI_PREFERENCE = ["codex", "claude"];
205
+ var PROBE_TIMEOUT_MS = 1e4;
206
+ var defaultProbe = (file, args) => new Promise((resolve2, reject) => {
207
+ execFile2(file, [...args], { timeout: PROBE_TIMEOUT_MS, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
208
+ const code = err ? typeof err.code === "number" ? err.code : null : 0;
209
+ if (err && err.code === "ENOENT") return reject(err);
210
+ resolve2({ code, out: `${stdout}
211
+ ${stderr}`.trim() });
212
+ });
213
+ });
214
+ var CLI_LABEL = { claude: "Claude Code", codex: "Codex" };
215
+ function knownLocations(cli2, home = homedir()) {
216
+ const common = [join2(home, ".local", "bin", cli2), `/opt/homebrew/bin/${cli2}`, `/usr/local/bin/${cli2}`];
217
+ return cli2 === "codex" ? [join2(home, ".codex", "packages", "standalone", "current", "bin", "codex"), ...common] : [...common, join2(home, ".claude", "local", "claude")];
218
+ }
219
+ var VERSION = /(\d+\.\d+\.\d+)/;
220
+ function identify(output) {
221
+ if (/claude code/i.test(output)) return "claude";
222
+ if (/\bcodex(-cli)?\b/i.test(output)) return "codex";
223
+ return null;
224
+ }
225
+ function loggedIn(cli2, result) {
226
+ if (cli2 === "claude") {
227
+ const json = /\{[\s\S]*\}/.exec(result.out)?.[0];
228
+ try {
229
+ const status = json ? JSON.parse(json) : null;
230
+ return typeof status?.loggedIn === "boolean" ? status.loggedIn : null;
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
235
+ if (/not logged in|logged out/i.test(result.out)) return false;
236
+ if (/logged in/i.test(result.out)) return true;
237
+ return result.code !== null && result.code !== 0 ? false : null;
238
+ }
239
+ var LOGIN = {
240
+ claude: { args: ["auth", "status"], fix: "claude auth login" },
241
+ codex: { args: ["login", "status"], fix: "codex login" }
242
+ };
243
+ async function probeCli(path, expected, onPath, probe = defaultProbe) {
244
+ const missing = (problem) => ({
245
+ cli: expected,
246
+ found: { path, version: null, onPath, ready: false, problem, fix: null }
247
+ });
248
+ let version;
249
+ try {
250
+ version = await probe(path, ["--version"]);
251
+ } catch {
252
+ return missing(`Nothing runs at ${path}`);
253
+ }
254
+ const cli2 = identify(version.out) ?? expected;
255
+ if (version.code !== 0 || !cli2) {
256
+ const said = version.out.split("\n")[0]?.slice(0, 120) || "nothing";
257
+ return missing(cli2 ? `${path} didn't say its version` : `${path} isn't Codex or Claude Code: it says \u201C${said}\u201D`);
258
+ }
259
+ const number = VERSION.exec(version.out)?.[1] ?? null;
260
+ const tooOld = cli2 === "codex" ? refusalFor(number) : null;
261
+ if (tooOld) {
262
+ return { cli: cli2, found: { path, version: number, onPath, ready: false, problem: tooOld, fix: "codex update" } };
263
+ }
264
+ let login = null;
265
+ try {
266
+ login = loggedIn(cli2, await probe(path, LOGIN[cli2].args));
267
+ } catch {
268
+ login = null;
269
+ }
270
+ return {
271
+ cli: cli2,
272
+ found: login === false ? { path, version: number, onPath, ready: false, problem: `${CLI_LABEL[cli2]} is installed but not logged in`, fix: LOGIN[cli2].fix } : { path, version: number, onPath, ready: true, problem: null, fix: null }
273
+ };
274
+ }
275
+ async function detectClis(configured = {}, opts = {}) {
276
+ const report = {};
277
+ await Promise.all(
278
+ CLI_NAMES.map(async (cli2) => {
279
+ const onPath = await findExecutable(cli2, opts.path);
280
+ const candidates = [
281
+ ...configured[cli2] ? [await findExecutable(configured[cli2], opts.path)] : [],
282
+ onPath,
283
+ ...await Promise.all((opts.locations ?? knownLocations)(cli2).map((location) => findExecutable(location)))
284
+ ];
285
+ const seen = /* @__PURE__ */ new Set();
286
+ for (const candidate of candidates) {
287
+ if (!candidate || seen.has(candidate)) continue;
288
+ seen.add(candidate);
289
+ const { found } = await probeCli(candidate, cli2, candidate === onPath, opts.probe);
290
+ if (found.version !== null || found.ready) {
291
+ report[cli2] = found;
292
+ return;
293
+ }
294
+ }
295
+ report[cli2] = { path: null, version: null, onPath: false, ready: false, problem: `${CLI_LABEL[cli2]} isn't installed`, fix: null };
296
+ })
297
+ );
298
+ return report;
299
+ }
300
+ function preferredCli(report) {
301
+ return CLI_PREFERENCE.find((cli2) => report[cli2].ready) ?? null;
302
+ }
303
+
60
304
  // src/editable.ts
61
305
  import { createHash } from "node:crypto";
62
- import { readFile, rename, writeFile } from "node:fs/promises";
306
+ import { readFile as readFile2, rename, writeFile } from "node:fs/promises";
63
307
  var ABSENT = "absent";
64
308
  function versionOf(text) {
65
309
  return text === null ? ABSENT : createHash("sha256").update(text).digest("hex").slice(0, 16);
66
310
  }
67
311
  async function readEditable(path) {
68
312
  try {
69
- const text = await readFile(path, "utf8");
313
+ const text = await readFile2(path, "utf8");
70
314
  return { text, version: versionOf(text) };
71
315
  } catch (error) {
72
316
  if (error.code === "ENOENT") return { text: null, version: ABSENT };
@@ -127,7 +371,7 @@ ${lines.join("\n")}
127
371
  }
128
372
 
129
373
  // src/board.ts
130
- import { readFile as readFile2 } from "node:fs/promises";
374
+ import { readFile as readFile3 } from "node:fs/promises";
131
375
 
132
376
  // src/failure.ts
133
377
  var MAX_DETAIL = 4e3;
@@ -221,9 +465,9 @@ function reasonFor(code) {
221
465
  }
222
466
 
223
467
  // src/github.ts
224
- import { execFile } from "node:child_process";
225
- import { promisify } from "node:util";
226
- var run = promisify(execFile);
468
+ import { execFile as execFile3 } from "node:child_process";
469
+ import { promisify as promisify2 } from "node:util";
470
+ var run2 = promisify2(execFile3);
227
471
  var GH_TIMEOUT_MS = 15e3;
228
472
  var FETCH_TIMEOUT_MS = 3e4;
229
473
  var GRAPHQL_URL = "https://api.github.com/graphql";
@@ -246,7 +490,7 @@ var GhAuthError = class extends Error {
246
490
  };
247
491
  async function gh(ghPath, args) {
248
492
  try {
249
- const { stdout, stderr } = await run(ghPath, args, { env: GH_ENV, timeout: GH_TIMEOUT_MS, maxBuffer: 1 << 20 });
493
+ const { stdout, stderr } = await run2(ghPath, args, { env: GH_ENV, timeout: GH_TIMEOUT_MS, maxBuffer: 1 << 20 });
250
494
  return { stdout, stderr, code: 0 };
251
495
  } catch (err) {
252
496
  const failure = err;
@@ -1178,7 +1422,7 @@ ${details[i] || "(nothing more)"}`).join("\n\n");
1178
1422
  let lowestRemaining = null;
1179
1423
  const results = await Promise.allSettled(
1180
1424
  identities.map(
1181
- (identity2) => this.#deps.fetch(identity2.token, this.#config.github.scope, identity2.account.login, this.#abort.signal)
1425
+ (identity3) => this.#deps.fetch(identity3.token, this.#config.github.scope, identity3.account.login, this.#abort.signal)
1182
1426
  )
1183
1427
  );
1184
1428
  results.forEach((result, i) => {
@@ -1323,7 +1567,7 @@ function normalizeStoredPull(raw) {
1323
1567
  async function readPullsFile(path) {
1324
1568
  let text;
1325
1569
  try {
1326
- text = await readFile2(path, "utf8");
1570
+ text = await readFile3(path, "utf8");
1327
1571
  } catch {
1328
1572
  return null;
1329
1573
  }
@@ -1347,12 +1591,12 @@ async function readPullsFile(path) {
1347
1591
  }
1348
1592
 
1349
1593
  // src/calendar.ts
1350
- import { execFile as execFile2 } from "node:child_process";
1351
- import { readFile as readFile3, rm, stat } from "node:fs/promises";
1594
+ import { execFile as execFile4 } from "node:child_process";
1595
+ import { readFile as readFile4, rm, stat as stat2 } from "node:fs/promises";
1352
1596
  import { tmpdir } from "node:os";
1353
- import { join } from "node:path";
1354
- import { promisify as promisify2 } from "node:util";
1355
- var run2 = promisify2(execFile2);
1597
+ import { join as join3 } from "node:path";
1598
+ import { promisify as promisify3 } from "node:util";
1599
+ var run3 = promisify3(execFile4);
1356
1600
  var HELPER_TIMEOUT_MS = 2e4;
1357
1601
  var POLL_MS = 150;
1358
1602
  var DECLINED = 3;
@@ -1375,17 +1619,17 @@ function parseCalendarFacts(raw) {
1375
1619
  if (typeof raw !== "object" || raw === null) return null;
1376
1620
  const obj = raw;
1377
1621
  if (str2(obj.error)) return null;
1378
- const calendars = [];
1622
+ const calendars2 = [];
1379
1623
  if (Array.isArray(obj.calendars)) {
1380
1624
  for (const entry of obj.calendars) {
1381
1625
  const id = str2(entry?.id);
1382
1626
  const title = str2(entry?.title);
1383
- if (id && title) calendars.push({ id, title, source: str2(entry?.source) ?? "" });
1627
+ if (id && title) calendars2.push({ id, title, source: str2(entry?.source) ?? "" });
1384
1628
  }
1385
1629
  }
1386
1630
  return {
1387
1631
  generatedAt: str2(obj.generatedAt) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1388
- calendars,
1632
+ calendars: calendars2,
1389
1633
  events: Array.isArray(obj.events) ? obj.events : []
1390
1634
  };
1391
1635
  }
@@ -1442,17 +1686,17 @@ var CalendarHelperError = class extends Error {
1442
1686
  };
1443
1687
  async function runHelper(appPath, selfAddresses) {
1444
1688
  try {
1445
- await stat(appPath);
1689
+ await stat2(appPath);
1446
1690
  } catch {
1447
1691
  throw new CalendarHelperError(
1448
1692
  `the calendar helper isn't built at ${appPath} \u2014 run ${command("build-calendar")}, or point DAILY_FOCUS_CALENDAR_APP at an existing build`
1449
1693
  );
1450
1694
  }
1451
- const out = join(tmpdir(), `daily-focus-calendar-${process.pid}-${Date.now()}.json`);
1695
+ const out = join3(tmpdir(), `daily-focus-calendar-${process.pid}-${Date.now()}.json`);
1452
1696
  try {
1453
1697
  try {
1454
1698
  const zone = process.env.TZ ? ["--env", `TZ=${process.env.TZ}`] : [];
1455
- await run2("/usr/bin/open", ["-n", "-a", appPath, ...zone, "--args", out, selfAddresses.join(",")], {
1699
+ await run3("/usr/bin/open", ["-n", "-a", appPath, ...zone, "--args", out, selfAddresses.join(",")], {
1456
1700
  timeout: HELPER_TIMEOUT_MS
1457
1701
  });
1458
1702
  } catch (err) {
@@ -1463,7 +1707,7 @@ async function runHelper(appPath, selfAddresses) {
1463
1707
  for (; ; ) {
1464
1708
  let text = null;
1465
1709
  try {
1466
- text = await readFile3(out, "utf8");
1710
+ text = await readFile4(out, "utf8");
1467
1711
  } catch {
1468
1712
  text = null;
1469
1713
  }
@@ -1487,7 +1731,7 @@ async function runHelper(appPath, selfAddresses) {
1487
1731
  }
1488
1732
 
1489
1733
  // src/calendarboard.ts
1490
- import { readFile as readFile4 } from "node:fs/promises";
1734
+ import { readFile as readFile5 } from "node:fs/promises";
1491
1735
  var MAX_BACKOFF_MS2 = 15 * 6e4;
1492
1736
  var realDeps2 = { run: runHelper };
1493
1737
  var CalendarBoard = class {
@@ -1639,18 +1883,18 @@ var CalendarBoard = class {
1639
1883
  };
1640
1884
  async function readCalendarFile(path) {
1641
1885
  try {
1642
- return parseCalendarFacts(JSON.parse(await readFile4(path, "utf8")));
1886
+ return parseCalendarFacts(JSON.parse(await readFile5(path, "utf8")));
1643
1887
  } catch {
1644
1888
  return null;
1645
1889
  }
1646
1890
  }
1647
1891
 
1648
1892
  // src/ticketboard.ts
1649
- import { readFile as readFile5 } from "node:fs/promises";
1893
+ import { readFile as readFile6 } from "node:fs/promises";
1650
1894
 
1651
1895
  // src/jira.ts
1652
- import { execFile as execFile3 } from "node:child_process";
1653
- import { promisify as promisify3 } from "node:util";
1896
+ import { execFile as execFile5 } from "node:child_process";
1897
+ import { promisify as promisify4 } from "node:util";
1654
1898
 
1655
1899
  // src/linked.ts
1656
1900
  var CLOSED_LOOKBACK_DAYS = 60;
@@ -1732,7 +1976,7 @@ function reconcile(ticket, linked, jiraCount) {
1732
1976
  }
1733
1977
 
1734
1978
  // src/jira.ts
1735
- var run3 = promisify3(execFile3);
1979
+ var run4 = promisify4(execFile5);
1736
1980
  var ACLI_TIMEOUT_MS = 45e3;
1737
1981
  var PAGE_LIMIT = 200;
1738
1982
  var ACLI_ENV = { ...process.env, NO_COLOR: "1" };
@@ -1768,7 +2012,7 @@ var JiraTransitionError = class extends Error {
1768
2012
  };
1769
2013
  async function acli(acliPath, args) {
1770
2014
  try {
1771
- const { stdout, stderr } = await run3(acliPath, args, {
2015
+ const { stdout, stderr } = await run4(acliPath, args, {
1772
2016
  env: ACLI_ENV,
1773
2017
  timeout: ACLI_TIMEOUT_MS,
1774
2018
  // A hundred tickets of raw Jira issue JSON measured 135 KB; this is room
@@ -1914,13 +2158,13 @@ function jsonObjectAt(text, from) {
1914
2158
  }
1915
2159
  return null;
1916
2160
  }
1917
- function readDevPullRequests(field) {
1918
- if (typeof field !== "string") return null;
1919
- const marker = field.indexOf("json=");
2161
+ function readDevPullRequests(field2) {
2162
+ if (typeof field2 !== "string") return null;
2163
+ const marker = field2.indexOf("json=");
1920
2164
  if (marker === -1) return null;
1921
- const opens = field.indexOf("{", marker);
2165
+ const opens = field2.indexOf("{", marker);
1922
2166
  if (opens === -1) return null;
1923
- const body = jsonObjectAt(field, opens);
2167
+ const body = jsonObjectAt(field2, opens);
1924
2168
  if (body === null) return null;
1925
2169
  let parsed;
1926
2170
  try {
@@ -2050,19 +2294,19 @@ async function fetchTickets(acliPath, opts) {
2050
2294
  const undecided = new Set(reconciled.filter((ticket) => !ticket.hasOpenPr && !ticket.allPrsClosed).map((ticket) => ticket.key));
2051
2295
  const unansweredNow = unanswered.filter((key) => undecided.has(key));
2052
2296
  const unaccountedNow = unaccounted.filter((key) => undecided.has(key));
2053
- const listed = (keys2) => keys2.map((key) => {
2297
+ const listed2 = (keys2) => keys2.map((key) => {
2054
2298
  const url = browseUrl(opts.site, key);
2055
2299
  return url === null ? key : `[${key}](${url})`;
2056
2300
  }).join(", ");
2057
2301
  const heldOut = (keys2) => `${keys2.length === 1 ? "it is" : "they are"} held out of "no open pull requests left" rather than guessed at`;
2058
2302
  if (unansweredNow.length > 0) {
2059
2303
  warnings.push(
2060
- `Could not read Jira's development panel for ${listed(unansweredNow)}, so ${heldOut(unansweredNow)}. The next read may well manage it.`
2304
+ `Could not read Jira's development panel for ${listed2(unansweredNow)}, so ${heldOut(unansweredNow)}. The next read may well manage it.`
2061
2305
  );
2062
2306
  }
2063
2307
  if (unaccountedNow.length > 0) {
2064
2308
  warnings.push(
2065
- `Jira's search says ${listed(unaccountedNow)} ${unaccountedNow.length === 1 ? "has" : "have"} pull requests, but its own development panel names none, so ${heldOut(unaccountedNow)}. Jira is disagreeing with itself; the row comes back when one of its caches catches up.`
2309
+ `Jira's search says ${listed2(unaccountedNow)} ${unaccountedNow.length === 1 ? "has" : "have"} pull requests, but its own development panel names none, so ${heldOut(unaccountedNow)}. Jira is disagreeing with itself; the row comes back when one of its caches catches up.`
2066
2310
  );
2067
2311
  }
2068
2312
  if (!opts.site) {
@@ -2158,7 +2402,7 @@ function ticketCourtOrder(court) {
2158
2402
  function inStatus(ticket, statuses) {
2159
2403
  const status = ticket.workflowStatus.trim().toLowerCase();
2160
2404
  if (status === "") return false;
2161
- return statuses.some((listed) => listed.trim().toLowerCase() === status);
2405
+ return statuses.some((listed2) => listed2.trim().toLowerCase() === status);
2162
2406
  }
2163
2407
  function judge(ticket, holdStatuses = []) {
2164
2408
  if (ticket.statusCategory === "done") return null;
@@ -2349,15 +2593,15 @@ var TicketBoard = class {
2349
2593
  }
2350
2594
  async #read() {
2351
2595
  const { acliPath, projects, site: configuredSite } = this.#config.jira;
2352
- let identity2;
2596
+ let identity3;
2353
2597
  try {
2354
- identity2 = await this.#deps.identity(acliPath);
2598
+ identity3 = await this.#deps.identity(acliPath);
2355
2599
  } catch (err) {
2356
2600
  this.#reason = { message: `${err.message}, or set DAILY_FOCUS_JIRA=off`, detail: errorDetail(err) };
2357
2601
  this.#streak = extend(this.#streak);
2358
2602
  return;
2359
2603
  }
2360
- const site = configuredSite ?? identity2.site;
2604
+ const site = configuredSite ?? identity3.site;
2361
2605
  const githubWarnings = [];
2362
2606
  const linked = this.#config.github.enabled ? this.#deps.linked(this.#config.github, /* @__PURE__ */ new Date(), AbortSignal.any([this.#abort.signal, AbortSignal.timeout(GITHUB_DEADLINE_MS)])).then(
2363
2607
  (read) => {
@@ -2382,7 +2626,7 @@ var TicketBoard = class {
2382
2626
  this.#file = {
2383
2627
  version: 1,
2384
2628
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
2385
- account: identity2.account,
2629
+ account: identity3.account,
2386
2630
  projects: [...projects],
2387
2631
  warnings,
2388
2632
  tickets,
@@ -2451,7 +2695,7 @@ function storedPulls(value) {
2451
2695
  async function readTicketsFile(path) {
2452
2696
  let text;
2453
2697
  try {
2454
- text = await readFile5(path, "utf8");
2698
+ text = await readFile6(path, "utf8");
2455
2699
  } catch {
2456
2700
  return null;
2457
2701
  }
@@ -2484,8 +2728,8 @@ async function readTicketsFile(path) {
2484
2728
  // src/agent.ts
2485
2729
  import { spawn } from "node:child_process";
2486
2730
  import { randomUUID } from "node:crypto";
2487
- import { appendFile, mkdir, readFile as readFile6 } from "node:fs/promises";
2488
- import { homedir } from "node:os";
2731
+ import { appendFile, mkdir, readFile as readFile7 } from "node:fs/promises";
2732
+ import { homedir as homedir2 } from "node:os";
2489
2733
  import { createInterface } from "node:readline";
2490
2734
  var RUN_TIMEOUT_MS = 60 * 6e4;
2491
2735
  var TURN_TIMEOUT_MS = 10 * 6e4;
@@ -2515,7 +2759,7 @@ function followUpPrompt(question) {
2515
2759
  question.trim()
2516
2760
  ].join("\n");
2517
2761
  }
2518
- function displayPath(path, home = homedir()) {
2762
+ function displayPath(path, home = homedir2()) {
2519
2763
  return path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
2520
2764
  }
2521
2765
  function foldAgentLog(text) {
@@ -2622,7 +2866,7 @@ var AgentRunner = class {
2622
2866
  this.loaded = true;
2623
2867
  let text = "";
2624
2868
  try {
2625
- text = await readFile6(this.config.agentLogFile, "utf8");
2869
+ text = await readFile7(this.config.agentLogFile, "utf8");
2626
2870
  } catch {
2627
2871
  return;
2628
2872
  }
@@ -2700,8 +2944,8 @@ var AgentRunner = class {
2700
2944
  * decided on.
2701
2945
  */
2702
2946
  async run(trigger = "hand", now = /* @__PURE__ */ new Date()) {
2703
- const cli = this.config.agent.cli;
2704
- if (!cli) throw new Error("running the morning agent is off; set DAILY_FOCUS_AGENT to codex or claude");
2947
+ const cli2 = this.config.agent.cli;
2948
+ if (!cli2) throw new Error("running the morning agent is off; set DAILY_FOCUS_AGENT to codex or claude");
2705
2949
  if (this.running) {
2706
2950
  throw new Error(
2707
2951
  this.running.turn ? "the morning agent is still answering a question" : "the morning agent is already running"
@@ -2710,7 +2954,7 @@ var AgentRunner = class {
2710
2954
  await this.start();
2711
2955
  const run5 = {
2712
2956
  id: randomUUID(),
2713
- cli,
2957
+ cli: cli2,
2714
2958
  trigger,
2715
2959
  sessionId: null,
2716
2960
  startedAt: now.toISOString(),
@@ -2723,8 +2967,8 @@ var AgentRunner = class {
2723
2967
  resumable: false
2724
2968
  };
2725
2969
  this.runs.push(run5);
2726
- await this.append({ event: "started", run: run5.id, cli, trigger, at: run5.startedAt });
2727
- const adapter = ADAPTERS[cli];
2970
+ await this.append({ event: "started", run: run5.id, cli: cli2, trigger, at: run5.startedAt });
2971
+ const adapter = ADAPTERS[cli2];
2728
2972
  const args = adapter.args({
2729
2973
  prompt: agentPrompt(displayPath(this.config.promptFile)),
2730
2974
  sessionId: null,
@@ -2743,8 +2987,8 @@ var AgentRunner = class {
2743
2987
  * is running; the answer arrives through `onChange` and the next state.
2744
2988
  */
2745
2989
  async ask(runId, question) {
2746
- const cli = this.config.agent.cli;
2747
- if (!cli) throw new Error("running the morning agent is off; set DAILY_FOCUS_AGENT to codex or claude");
2990
+ const cli2 = this.config.agent.cli;
2991
+ if (!cli2) throw new Error("running the morning agent is off; set DAILY_FOCUS_AGENT to codex or claude");
2748
2992
  await this.start();
2749
2993
  const run5 = this.runs.find((candidate) => candidate.id === runId);
2750
2994
  if (!run5) throw new Error("no such run");
@@ -2755,7 +2999,7 @@ var AgentRunner = class {
2755
2999
  }
2756
3000
  if (!this.resumable(run5)) {
2757
3001
  throw new Error(
2758
- run5.sessionId === null ? "this run left no session to continue" : `this run was made by ${run5.cli}, and the agent is ${cli} now`
3002
+ run5.sessionId === null ? "this run left no session to continue" : `this run was made by ${run5.cli}, and the agent is ${cli2} now`
2759
3003
  );
2760
3004
  }
2761
3005
  const text = question.trim();
@@ -2771,7 +3015,7 @@ var AgentRunner = class {
2771
3015
  };
2772
3016
  run5.turns.push(turn);
2773
3017
  await this.append({ event: "asked", run: run5.id, turn: turn.id, question: text, at: turn.startedAt });
2774
- const adapter = ADAPTERS[cli];
3018
+ const adapter = ADAPTERS[cli2];
2775
3019
  const args = adapter.args({
2776
3020
  prompt: followUpPrompt(text),
2777
3021
  sessionId: run5.sessionId,
@@ -2792,10 +3036,10 @@ var AgentRunner = class {
2792
3036
  * session under the directory it was started in, and looks for it there.
2793
3037
  */
2794
3038
  async spawnInto(run5, turn, args, timeoutMs, timeoutReason) {
2795
- const cli = run5.cli;
2796
- const adapter = ADAPTERS[cli];
3039
+ const cli2 = run5.cli;
3040
+ const adapter = ADAPTERS[cli2];
2797
3041
  await mkdir(this.config.dataDir, { recursive: true });
2798
- const refusal2 = cli === "codex" ? await this.codexGate.refusal() : null;
3042
+ const refusal2 = cli2 === "codex" ? await this.codexGate.refusal() : null;
2799
3043
  if (refusal2) {
2800
3044
  await this.finish(run5, turn, { ok: false, error: refusal2 });
2801
3045
  return;
@@ -2851,7 +3095,7 @@ ${event.text}` : event.text;
2851
3095
  this.running = null;
2852
3096
  const outcome = running.abortReason ? { ok: false, error: running.abortReason } : done ?? {
2853
3097
  ok: false,
2854
- error: `${cli} exited with code ${code}${stderr.length ? `: ${stderr.join("").trim().split("\n").slice(-3).join(" ")}` : ""}`
3098
+ error: `${cli2} exited with code ${code}${stderr.length ? `: ${stderr.join("").trim().split("\n").slice(-3).join(" ")}` : ""}`
2855
3099
  };
2856
3100
  void this.finish(run5, turn, outcome, running.abortReason !== null);
2857
3101
  });
@@ -2954,23 +3198,23 @@ function watchDataDir(dir, onChange, { debounceMs = 120, ignore = [] } = {}) {
2954
3198
  }
2955
3199
 
2956
3200
  // src/assets.ts
2957
- import { readdir, stat as stat2 } from "node:fs/promises";
2958
- import { join as join2 } from "node:path";
3201
+ import { readdir as readdir2, stat as stat3 } from "node:fs/promises";
3202
+ import { join as join4 } from "node:path";
2959
3203
  async function computeAssetVersion(dir) {
2960
3204
  const parts = [];
2961
3205
  async function walk(current) {
2962
3206
  let entries;
2963
3207
  try {
2964
- entries = await readdir(current, { withFileTypes: true });
3208
+ entries = await readdir2(current, { withFileTypes: true });
2965
3209
  } catch {
2966
3210
  return;
2967
3211
  }
2968
3212
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
2969
- const path = join2(current, entry.name);
3213
+ const path = join4(current, entry.name);
2970
3214
  if (entry.isDirectory()) {
2971
3215
  await walk(path);
2972
3216
  } else {
2973
- const info = await stat2(path);
3217
+ const info = await stat3(path);
2974
3218
  parts.push(`${entry.name}:${info.size}:${info.mtimeMs}`);
2975
3219
  }
2976
3220
  }
@@ -2989,8 +3233,8 @@ var FAVICONS = {
2989
3233
  work: `<path d="M34 30V20a8 8 0 0 1 8-8h16a8 8 0 0 1 8 8v10" fill="none" stroke="#2a78d6" stroke-width="9"/><path fill="#2a78d6" fill-rule="evenodd" d="M16 30h68a10 10 0 0 1 10 10v38a10 10 0 0 1-10 10H16A10 10 0 0 1 6 78V40a10 10 0 0 1 10-10ZM40 46v14h20V46Z"/>`,
2990
3234
  personal: `<path fill="#1baf7a" fill-rule="evenodd" d="M50 8 96 48H84v44H16V48H4ZM41 92V66h18v26Z"/>`
2991
3235
  };
2992
- function faviconSvg(profile) {
2993
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">${FAVICONS[profile]}</svg>`;
3236
+ function faviconSvg(profile2) {
3237
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">${FAVICONS[profile2]}</svg>`;
2994
3238
  }
2995
3239
 
2996
3240
  // src/guard.ts
@@ -3022,6 +3266,542 @@ function refusal(req, listenHost) {
3022
3266
  return null;
3023
3267
  }
3024
3268
 
3269
+ // src/onboarding.ts
3270
+ import { spawn as spawn2 } from "node:child_process";
3271
+ import { mkdir as mkdir2, mkdtemp, readFile as readFile8, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
3272
+ import { tmpdir as tmpdir2 } from "node:os";
3273
+ import { join as join5 } from "node:path";
3274
+ import { createInterface as createInterface2 } from "node:readline";
3275
+
3276
+ // src/questions.ts
3277
+ var INSTALL = {
3278
+ codex: { label: "Install Codex", url: "https://developers.openai.com/codex/cli" },
3279
+ claude: { label: "Install Claude Code", url: "https://code.claude.com/docs" }
3280
+ };
3281
+ function option(value, label, detail = null, group = null) {
3282
+ return { value, label, detail, group };
3283
+ }
3284
+ function choice(id, options, selected, extra = {}) {
3285
+ return { id, label: null, multiple: false, options, selected, custom: null, ...extra };
3286
+ }
3287
+ function field(id, label, value, extra = {}) {
3288
+ return { id, label, value, placeholder: null, help: null, ...extra };
3289
+ }
3290
+ function card(parts) {
3291
+ return { why: null, choices: [], fields: [], problem: null, ...parts };
3292
+ }
3293
+ function one(answer, id) {
3294
+ return answer.choices[id]?.[0] ?? null;
3295
+ }
3296
+ function str3(value) {
3297
+ return typeof value === "string" ? value.trim() : "";
3298
+ }
3299
+ function proposalOf(ctx, id) {
3300
+ const proposal = ctx.proposals[id];
3301
+ return typeof proposal === "object" && proposal !== null && !Array.isArray(proposal) ? proposal : null;
3302
+ }
3303
+ function listed(names) {
3304
+ if (names.length <= 1) return names.join("");
3305
+ return `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
3306
+ }
3307
+ function ticked(name) {
3308
+ return `\`${name.replaceAll("`", "'")}\``;
3309
+ }
3310
+ function binFor(ctx, cli2) {
3311
+ const found = cli2 ? ctx.clis?.[cli2] : null;
3312
+ return found?.path && !found.onPath ? found.path : null;
3313
+ }
3314
+ var cli = {
3315
+ id: "cli",
3316
+ title: () => "Which agent should write your brief?",
3317
+ topic: "your coding agent",
3318
+ keys: ["DAILY_FOCUS_AGENT"],
3319
+ later: { label: "Decide later", summary: "No agent yet: the boards work, and there is no brief until you choose one." },
3320
+ card(ctx) {
3321
+ if (!ctx.clis) return card({});
3322
+ const report = ctx.clis;
3323
+ const ready = CLI_PREFERENCE.filter((name) => report[name].ready);
3324
+ if (ready.length === 0) {
3325
+ const lines = CLI_PREFERENCE.map((name) => `${report[name].problem ?? `${CLI_LABEL[name]} can't be used`}.`);
3326
+ const fix = CLI_PREFERENCE.map((name) => report[name].fix).find((command3) => command3) ?? null;
3327
+ return card({
3328
+ problem: {
3329
+ message: `${lines.join(" ")} ${fix ? "Run this in a terminal, then check again." : "Install one, log in once in a terminal, then check again."}`,
3330
+ command: fix,
3331
+ blocking: true,
3332
+ links: CLI_PREFERENCE.filter((name) => !report[name].path).map((name) => INSTALL[name])
3333
+ },
3334
+ fields: [
3335
+ field("path", "Installed somewhere setup didn\u2019t look? Its path", "", {
3336
+ placeholder: "/opt/homebrew/bin/codex",
3337
+ help: "The program itself, codex or claude. Setup checks it before using it."
3338
+ })
3339
+ ]
3340
+ });
3341
+ }
3342
+ const set = ctx.current.DAILY_FOCUS_AGENT;
3343
+ const others = CLI_PREFERENCE.filter((name) => !report[name].ready && report[name].path).map((name) => `${report[name].problem}.`);
3344
+ return card({
3345
+ why: [
3346
+ ready.length > 1 ? `Both are ready on this ${ctx.machine}, so it uses Codex unless you say otherwise.` : `${CLI_LABEL[ready[0]]} is the one ready on this ${ctx.machine}.`,
3347
+ ...others
3348
+ ].join(" "),
3349
+ choices: [
3350
+ choice(
3351
+ "cli",
3352
+ ready.map((name) => option(name, CLI_LABEL[name], [report[name].version, report[name].path].filter(Boolean).join(", at "))),
3353
+ [set && ready.includes(set) ? set : ready[0]]
3354
+ )
3355
+ ]
3356
+ });
3357
+ },
3358
+ summary: (answer) => {
3359
+ const chosen = CLI_LABEL[one(answer, "cli")];
3360
+ return chosen ? `${chosen} writes your brief.` : "No agent writes your brief yet.";
3361
+ },
3362
+ // The assistant comes with it, on the same CLI, unless it has been set: it
3363
+ // only reads and drafts, and it is what the Ask button on every row needs.
3364
+ settings: (answer, ctx) => {
3365
+ const chosen = one(answer, "cli");
3366
+ const bin = binFor(ctx, chosen);
3367
+ const assistant = chosen && !ctx.current.DAILY_FOCUS_ASSISTANT;
3368
+ return {
3369
+ DAILY_FOCUS_AGENT: chosen,
3370
+ ...bin ? { DAILY_FOCUS_AGENT_BIN: bin } : {},
3371
+ ...assistant ? { DAILY_FOCUS_ASSISTANT: chosen, ...bin ? { DAILY_FOCUS_ASSISTANT_BIN: bin } : {} } : {}
3372
+ };
3373
+ }
3374
+ };
3375
+ var profile = {
3376
+ id: "profile",
3377
+ topic: "what it covers",
3378
+ title: () => "What should your brief cover?",
3379
+ keys: ["DAILY_FOCUS_PROFILE"],
3380
+ later: null,
3381
+ card: (ctx) => card({
3382
+ choices: [
3383
+ choice(
3384
+ "profile",
3385
+ [
3386
+ option("work", "Your work", "Work mail and calendars, pull requests, tickets and meeting notes."),
3387
+ option("personal", "Your personal life", "Personal mail, family calendars, reminders and messages.")
3388
+ ],
3389
+ [ctx.current.DAILY_FOCUS_PROFILE === "personal" ? "personal" : "work"]
3390
+ )
3391
+ ],
3392
+ why: "One dashboard briefs one of them. For both, run a second dashboard with its own store."
3393
+ }),
3394
+ summary: (answer) => one(answer, "profile") === "personal" ? "It briefs your personal life." : "It briefs your work.",
3395
+ settings: (answer) => ({ DAILY_FOCUS_PROFILE: one(answer, "profile") })
3396
+ };
3397
+ var TIMES = ["06:00", "06:30", "07:00", "07:30", "08:00"];
3398
+ var DAYS = { "1-5": "Weekdays", "0-6": "Every day" };
3399
+ var schedule = {
3400
+ id: "schedule",
3401
+ topic: "when it runs",
3402
+ title: () => "When should your brief be ready?",
3403
+ applies: (ctx) => ctx.cli !== null,
3404
+ keys: ["DAILY_FOCUS_AGENT_AT", "DAILY_FOCUS_AGENT_DAYS"],
3405
+ later: null,
3406
+ card(ctx) {
3407
+ const at = ctx.current.DAILY_FOCUS_AGENT_AT || "07:00";
3408
+ const days = ctx.current.DAILY_FOCUS_AGENT_DAYS || "1-5";
3409
+ const times = [.../* @__PURE__ */ new Set([...TIMES, ...at === "off" ? [] : [at]])].sort();
3410
+ return card({
3411
+ why: `It starts then and takes a few minutes. If the ${ctx.machine} is asleep or the dashboard isn't running then, it runs as soon as it is, that day.`,
3412
+ choices: [
3413
+ choice("at", [...times.map((time) => option(time, time)), option("off", "Only when I ask")], [at], {
3414
+ label: "Time",
3415
+ custom: "Another time, such as 05:45"
3416
+ }),
3417
+ choice("days", Object.entries({ ...DAYS, ...DAYS[days] ? {} : { [days]: days } }).map(([value, label]) => option(value, label)), [days], {
3418
+ label: "Days"
3419
+ })
3420
+ ]
3421
+ });
3422
+ },
3423
+ summary(answer) {
3424
+ const at = one(answer, "at");
3425
+ if (!at || at === "off") return "It runs only when you ask.";
3426
+ const days = one(answer, "days") ?? "1-5";
3427
+ return `It runs at ${at} ${days === "1-5" ? "on weekdays" : days === "0-6" ? "every day" : `on days ${days}`}.`;
3428
+ },
3429
+ settings: (answer) => ({ DAILY_FOCUS_AGENT_AT: one(answer, "at"), DAILY_FOCUS_AGENT_DAYS: one(answer, "days") })
3430
+ };
3431
+ function proposedAccounts(proposal) {
3432
+ const raw = Array.isArray(proposal?.githubAccounts) ? proposal.githubAccounts : [];
3433
+ return raw.flatMap((entry) => {
3434
+ const account = typeof entry === "object" && entry !== null ? entry : {};
3435
+ const login = str3(account.login);
3436
+ return login ? [{ login, note: str3(account.note) }] : [];
3437
+ });
3438
+ }
3439
+ function githubAccounts(ctx) {
3440
+ const accounts = /* @__PURE__ */ new Map();
3441
+ for (const login of ctx.identity.logins) accounts.set(login.toLowerCase(), { login, note: "" });
3442
+ for (const { login, note } of proposedAccounts(proposalOf(ctx, "identity"))) {
3443
+ accounts.set(login.toLowerCase(), { login: accounts.get(login.toLowerCase())?.login ?? login, note });
3444
+ }
3445
+ return [...accounts.values()];
3446
+ }
3447
+ var identity2 = {
3448
+ id: "identity",
3449
+ topic: "who you are",
3450
+ title: () => "Is this you?",
3451
+ keys: [],
3452
+ // Clearing three fields and unticking the accounts is a chore; one button, named for what it does, isn't.
3453
+ later: { label: "Leave this out", summary: "Who you are is left out, so the brief can\u2019t tell your to-dos from other people\u2019s." },
3454
+ card(ctx) {
3455
+ const proposal = proposalOf(ctx, "identity");
3456
+ const names = Array.isArray(proposal?.names) ? proposal.names.map(str3).filter(Boolean) : [];
3457
+ const name = names.length ? names.join(", ") : ctx.identity.name ?? "";
3458
+ const email = str3(proposal?.email) || (ctx.identity.email ?? "");
3459
+ const accounts = githubAccounts(ctx);
3460
+ return card({
3461
+ why: str3(proposal?.why) || "The brief tells your to-dos from everyone else\u2019s by these.",
3462
+ fields: [
3463
+ field("name", "Your name, as meeting notes write it", name, {
3464
+ placeholder: "Robin Lark",
3465
+ help: "Add short forms after a comma, such as Robin L."
3466
+ }),
3467
+ field("email", ctx.profile === "personal" ? "Your email" : "Your work email", email, { placeholder: "robin@example.com" })
3468
+ ],
3469
+ choices: [
3470
+ choice(
3471
+ "github",
3472
+ accounts.map((account) => option(account.login, account.login, account.note || null)),
3473
+ accounts.map((account) => account.login),
3474
+ { label: "Your GitHub accounts", multiple: true, custom: "Add a GitHub login" }
3475
+ )
3476
+ ]
3477
+ });
3478
+ },
3479
+ summary(answer) {
3480
+ const name = answer.fields.name?.split(",")[0]?.trim();
3481
+ const logins = answer.choices.github ?? [];
3482
+ const rest = [answer.fields.email?.trim() ?? "", logins.length ? `${listed(logins)} on GitHub` : ""].filter(Boolean);
3483
+ if (!name) return rest.length ? `You're ${listed(rest)}.` : "No details about you.";
3484
+ return `You're ${name}${rest.length ? `, ${rest.join(", ")}` : ""}.`;
3485
+ },
3486
+ settings(answer, ctx) {
3487
+ const values = {};
3488
+ if (answer.fields.email?.trim()) values.DAILY_FOCUS_CALENDAR_ADDRESSES = answer.fields.email.trim();
3489
+ const logins = answer.choices.github ?? [];
3490
+ if (logins.length > 1 || logins.length === 1 && logins[0] !== ctx.identity.logins[0]) {
3491
+ values.DAILY_FOCUS_GITHUB_ACCOUNTS = logins.join(", ");
3492
+ }
3493
+ return values;
3494
+ },
3495
+ sources(answer, ctx) {
3496
+ const names = (answer.fields.name ?? "").split(",").map((n) => n.trim()).filter(Boolean);
3497
+ const email = answer.fields.email?.trim();
3498
+ const notes = new Map(githubAccounts(ctx).map((account) => [account.login, account.note]));
3499
+ const logins = answer.choices.github ?? [];
3500
+ const lines = [
3501
+ names.length ? `- Name, as meeting notes and Doc assignments write it: ${ticked(names[0])}${names.length > 1 ? `, also ${listed(names.slice(1).map(ticked))}` : ""}` : null,
3502
+ email ? `- ${ctx.profile === "personal" ? "Personal" : "Work"} email: ${ticked(email)}` : null,
3503
+ logins.length ? [
3504
+ `- GitHub ${logins.length > 1 ? "logins" : "login"}, which review requests and authorship are judged against:`,
3505
+ ...logins.map((login) => ` - ${ticked(login)}${notes.get(login) ? ` \u2014 ${notes.get(login)}` : ""}`)
3506
+ ].join("\n") : null
3507
+ ].filter((line) => line !== null);
3508
+ return lines.length ? [{ heading: "Identity", body: lines.join("\n") }] : [];
3509
+ },
3510
+ agent: {
3511
+ brief: (ctx) => "Who the user is, as their morning brief needs to know them: their name as meeting notes and document assignments write it, with any short forms colleagues use, their " + (ctx.profile === "personal" ? "personal" : "work") + " email address, and every GitHub account they work as: gh may hold several, such as one for an employer\u2019s organisation. Give each account a few words on what it is for when you can tell. Start from what the dashboard found, below. Check the address they send mail from, how others address them, and `gh auth status`.",
3512
+ facts: (ctx) => [
3513
+ ctx.identity.name ? `- gh says their name is ${ctx.identity.name}` : null,
3514
+ ctx.identity.logins.length ? `- gh is logged in to GitHub as ${listed(ctx.identity.logins)}; the first is active` : null,
3515
+ ctx.identity.email ? `- git's global email is ${ctx.identity.email}` : null
3516
+ ].filter(Boolean).join("\n") || null,
3517
+ schema: {
3518
+ type: "object",
3519
+ properties: {
3520
+ why: { type: "string", description: "One or two sentences on where these came from." },
3521
+ names: { type: "array", items: { type: "string" }, description: "Full name first, then short forms." },
3522
+ email: { type: "string" },
3523
+ githubAccounts: {
3524
+ type: "array",
3525
+ items: {
3526
+ type: "object",
3527
+ properties: {
3528
+ login: { type: "string" },
3529
+ note: { type: "string", description: "What the account is for, in a few words, or empty." }
3530
+ },
3531
+ required: ["login", "note"],
3532
+ additionalProperties: false
3533
+ }
3534
+ }
3535
+ },
3536
+ required: ["why", "names", "email", "githubAccounts"],
3537
+ additionalProperties: false
3538
+ }
3539
+ }
3540
+ };
3541
+ function healthText(board, via) {
3542
+ if (board.reason) return board.reason;
3543
+ if (board.fetchedAt) return `Connected through ${via}.`;
3544
+ if (board.enabled) return `Checking ${via}\u2026`;
3545
+ return null;
3546
+ }
3547
+ var boards = {
3548
+ id: "boards",
3549
+ topic: "the boards",
3550
+ title: () => "What else should the dashboard show?",
3551
+ keys: ["DAILY_FOCUS_GITHUB", "DAILY_FOCUS_JIRA"],
3552
+ later: null,
3553
+ card(ctx) {
3554
+ const { github, jira } = ctx.boards;
3555
+ const selected = [
3556
+ github.enabled && !github.reason ? "github" : null,
3557
+ jira.enabled && !jira.reason && ctx.profile === "work" ? "jira" : null
3558
+ ].filter((value) => value !== null);
3559
+ return card({
3560
+ why: "Both are read live, beside the brief, so a merge or a status change shows up within minutes.",
3561
+ choices: [
3562
+ choice(
3563
+ "boards",
3564
+ [
3565
+ option("github", "Your pull requests", healthText(github, "gh") ?? "Through the GitHub CLI, gh."),
3566
+ option("jira", "Your Jira tickets", healthText(jira, "acli") ?? "Through the Atlassian CLI, acli.")
3567
+ ],
3568
+ selected,
3569
+ { multiple: true }
3570
+ )
3571
+ ]
3572
+ });
3573
+ },
3574
+ summary(answer) {
3575
+ const on = answer.choices.boards ?? [];
3576
+ const names = [on.includes("github") ? "your pull requests" : null, on.includes("jira") ? "your Jira tickets" : null].filter(
3577
+ (name) => name !== null
3578
+ );
3579
+ return names.length ? `It shows ${listed(names)}.` : "No boards beside the brief.";
3580
+ },
3581
+ settings(answer) {
3582
+ const on = answer.choices.boards ?? [];
3583
+ return { DAILY_FOCUS_GITHUB: on.includes("github") ? "on" : "off", DAILY_FOCUS_JIRA: on.includes("jira") ? "on" : "off" };
3584
+ }
3585
+ };
3586
+ var HOLIDAYS = "Holidays";
3587
+ function calendarEntries(ctx) {
3588
+ const proposal = proposalOf(ctx, "calendars");
3589
+ const verdicts = Array.isArray(proposal?.calendars) ? proposal.calendars.flatMap((raw) => {
3590
+ const entry = typeof raw === "object" && raw !== null ? raw : {};
3591
+ const title = str3(entry.title);
3592
+ return title ? [{ title, account: str3(entry.account), include: entry.include === true, holiday: entry.holiday === true, briefName: str3(entry.briefName), note: str3(entry.note) }] : [];
3593
+ }) : [];
3594
+ const configured = new Set((ctx.current.DAILY_FOCUS_CALENDARS ?? "").split(",").map((name) => name.trim().toLowerCase()).filter(Boolean));
3595
+ const used = /* @__PURE__ */ new Set();
3596
+ const entries = [];
3597
+ for (const calendar of Array.isArray(ctx.calendars) ? ctx.calendars : []) {
3598
+ const key = calendar.title.toLowerCase();
3599
+ const verdict = verdicts.find((v) => !used.has(v) && v.title.toLowerCase() === key && v.account.toLowerCase() === calendar.source.toLowerCase()) ?? verdicts.find((v) => !used.has(v) && v.title.toLowerCase() === key);
3600
+ if (verdict) used.add(verdict);
3601
+ entries.push({
3602
+ value: `live:${calendar.source}:${calendar.title}`,
3603
+ title: calendar.title,
3604
+ group: verdict?.holiday ? HOLIDAYS : calendar.source || "Calendar.app",
3605
+ live: true,
3606
+ holiday: verdict?.holiday ?? false,
3607
+ briefName: verdict?.briefName ?? "",
3608
+ note: verdict?.note ?? "",
3609
+ include: verdict ? verdict.include : configured.has(key)
3610
+ });
3611
+ }
3612
+ for (const verdict of verdicts) {
3613
+ if (used.has(verdict)) continue;
3614
+ entries.push({
3615
+ value: `brief:${verdict.account}:${verdict.title}`,
3616
+ title: verdict.title,
3617
+ group: verdict.holiday ? HOLIDAYS : verdict.account || "Your calendar account",
3618
+ live: false,
3619
+ holiday: verdict.holiday,
3620
+ briefName: verdict.briefName,
3621
+ note: verdict.note,
3622
+ include: verdict.include
3623
+ });
3624
+ }
3625
+ return entries.sort((a, b) => Number(a.group === HOLIDAYS) - Number(b.group === HOLIDAYS));
3626
+ }
3627
+ function chosenCalendars(answer, ctx) {
3628
+ const all = new Map(calendarEntries(ctx).map((entry) => [entry.value, entry]));
3629
+ const values = answer.choices.calendars ?? [];
3630
+ return {
3631
+ entries: values.flatMap((value) => all.get(value) ?? []),
3632
+ typed: values.filter((value) => !all.has(value)).map((value) => value.trim()).filter(Boolean)
3633
+ };
3634
+ }
3635
+ var calendars = {
3636
+ id: "calendars",
3637
+ topic: "your calendars",
3638
+ title: (ctx) => ctx.profile === "personal" ? "Which calendars belong in your brief?" : "Which calendars belong in your work brief?",
3639
+ keys: ["DAILY_FOCUS_CALENDARS"],
3640
+ later: null,
3641
+ card(ctx) {
3642
+ const entries = calendarEntries(ctx);
3643
+ const proposal = proposalOf(ctx, "calendars");
3644
+ const problem = ctx.calendar.supported && !ctx.calendar.built ? {
3645
+ message: "The live agenda reads Calendar.app through a small helper, built once with this command. Until then it uses the brief\u2019s events, so this list is from your calendar account.",
3646
+ command: ctx.calendar.buildCommand,
3647
+ blocking: false
3648
+ } : typeof ctx.calendars === "string" ? { message: `Calendar.app couldn't be read: ${ctx.calendars}`, command: null, blocking: false } : null;
3649
+ return card({
3650
+ why: str3(proposal?.why) || null,
3651
+ problem,
3652
+ choices: [
3653
+ choice(
3654
+ "calendars",
3655
+ entries.map((entry) => option(entry.value, entry.title, entry.note || null, entry.group)),
3656
+ entries.filter((entry) => entry.include).map((entry) => entry.value),
3657
+ { multiple: true, custom: "Add a calendar by name" }
3658
+ )
3659
+ ]
3660
+ });
3661
+ },
3662
+ summary(answer, ctx) {
3663
+ const { entries, typed } = chosenCalendars(answer, ctx);
3664
+ const names = [...entries.map((entry) => entry.title), ...typed];
3665
+ if (names.length === 0) return "No calendars.";
3666
+ if (names.length <= 3) return `It reads ${listed(names)}.`;
3667
+ return `It reads ${names.slice(0, 2).join(", ")} and ${names.length - 2} more calendars.`;
3668
+ },
3669
+ settings(answer, ctx) {
3670
+ const { entries, typed } = chosenCalendars(answer, ctx);
3671
+ const live = [.../* @__PURE__ */ new Set([...entries.filter((entry) => entry.live).map((entry) => entry.title), ...typed])];
3672
+ return { DAILY_FOCUS_CALENDARS: live.filter((name) => !name.includes(",")).join(", ") || null };
3673
+ },
3674
+ sources(answer, ctx) {
3675
+ const { entries, typed } = chosenCalendars(answer, ctx);
3676
+ const line = (entry) => `- ${ticked(entry.briefName || entry.title)}${entry.group !== HOLIDAYS && entry.group ? ` (${entry.group})` : ""}`;
3677
+ const own = [...entries.filter((entry) => !entry.holiday).map(line), ...typed.map((name) => `- ${ticked(name)}`)];
3678
+ const holidays = entries.filter((entry) => entry.holiday).map(line);
3679
+ return [
3680
+ ...own.length ? [{ heading: "Calendars", body: `Query these, and treat their events as blocking time:
3681
+
3682
+ ${own.join("\n")}` }] : [],
3683
+ ...holidays.length ? [{ heading: "Holiday calendars", body: `Call out a holiday today or within the next 14 days:
3684
+
3685
+ ${holidays.join("\n")}` }] : []
3686
+ ];
3687
+ },
3688
+ agent: {
3689
+ brief: (ctx) => `Which calendars the user's ${ctx.profile} brief should read. ` + (Array.isArray(ctx.calendars) ? "Below is every calendar Calendar.app on this Mac has, with the account it is on and how many events it has today. " : "List the calendars their calendar connector reaches. ") + "Include " + (ctx.profile === "personal" ? "their own personal and family calendars, " : "their own work calendar, team calendars whose events block their time, ") + "and the holiday calendars for where they and the people they work with are. Leave out " + (ctx.profile === "personal" ? "work calendars, " : "personal and family calendars, ") + "birthdays, and shared calendars that are mostly other people's events. If you can reach their calendar through a connector, look at the coming week for evidence, and give `briefName`, the name the connector uses where it differs from Calendar.app\u2019s. " + (Array.isArray(ctx.calendars) ? "List every calendar from the list below, and any your connector has that Calendar.app does not. " : "") + '`note` is a few words shown under the name, such as "Your primary calendar" or "12 of your meetings this week", or empty.',
3690
+ facts(ctx) {
3691
+ if (typeof ctx.calendars === "string") return `Calendar.app could not be read (${ctx.calendars}), so work from your calendar connector alone.`;
3692
+ if (!Array.isArray(ctx.calendars)) return "There is no list from Calendar.app here, so work from your calendar connector alone.";
3693
+ if (ctx.calendars.length === 0) return "Calendar.app has no calendars.";
3694
+ return [
3695
+ "Calendar.app has these, as title (account): events today.",
3696
+ "",
3697
+ ...ctx.calendars.map((calendar) => `- ${calendar.title} (${calendar.source || "no account"}): ${calendar.eventsToday}`)
3698
+ ].join("\n");
3699
+ },
3700
+ schema: {
3701
+ type: "object",
3702
+ properties: {
3703
+ why: { type: "string", description: "One or two sentences on which you ticked and why." },
3704
+ calendars: {
3705
+ type: "array",
3706
+ items: {
3707
+ type: "object",
3708
+ properties: {
3709
+ title: { type: "string", description: "Exactly as Calendar.app spells it, or the connector's name for one Calendar.app lacks." },
3710
+ account: { type: "string" },
3711
+ include: { type: "boolean" },
3712
+ holiday: { type: "boolean" },
3713
+ briefName: { type: "string" },
3714
+ note: { type: "string" }
3715
+ },
3716
+ required: ["title", "account", "include", "holiday", "briefName", "note"],
3717
+ additionalProperties: false
3718
+ }
3719
+ }
3720
+ },
3721
+ required: ["why", "calendars"],
3722
+ additionalProperties: false
3723
+ }
3724
+ }
3725
+ };
3726
+ function suggestedObjectives(proposal) {
3727
+ const raw = Array.isArray(proposal?.suggestions) ? proposal.suggestions : [];
3728
+ return raw.flatMap((entry) => {
3729
+ const suggestion = typeof entry === "object" && entry !== null ? entry : {};
3730
+ const text = str3(suggestion.objective);
3731
+ return text ? [{ text, detail: str3(suggestion.evidence) || null }] : [];
3732
+ });
3733
+ }
3734
+ var objective = {
3735
+ id: "objective",
3736
+ topic: "your objective",
3737
+ title: () => "What are you trying to get done right now?",
3738
+ keys: [],
3739
+ // Left empty, Continue says so; see `empty` on its card.
3740
+ later: null,
3741
+ card(ctx) {
3742
+ const proposal = proposalOf(ctx, "objective");
3743
+ return card({
3744
+ reply: "Tell it what you\u2019re working on, however roughly",
3745
+ empty: "Continue without one",
3746
+ why: str3(proposal?.why) || "The brief ranks the day against it. A next move works better than a project name. Tell it what you\u2019re working on, however roughly, and it will write it up.",
3747
+ fields: [
3748
+ field("objective", "Your objective", str3(proposal?.objective), {
3749
+ placeholder: "Get the staging deploy working again",
3750
+ suggestions: suggestedObjectives(proposal)
3751
+ }),
3752
+ field("blocker", "What\u2019s in the way, if anything", str3(proposal?.blocker), { placeholder: "Nobody can reproduce the failure" })
3753
+ ]
3754
+ });
3755
+ },
3756
+ summary: (answer) => {
3757
+ const text = answer.fields.objective?.trim();
3758
+ const blocker = answer.fields.blocker?.trim();
3759
+ if (!text) return "No objective for now.";
3760
+ return `Your objective: ${text}${blocker ? `. In the way: ${blocker}` : ""}`;
3761
+ },
3762
+ objective: (answer) => answer.fields.objective?.trim() ? { objective: answer.fields.objective.trim(), blocker: answer.fields.blocker?.trim() || null } : null,
3763
+ agent: {
3764
+ brief: (ctx) => `The user's current objective: the one thing they are trying to get done right now, which their ${ctx.profile} brief ranks the day against. Look at what they have been doing lately: their own recent and open pull requests, tickets assigned to them and in progress, and anything on the calendar that keeps coming back. Suggest up to three objectives, each only where there is a strong signal, with the evidence in a few words, such as "your 4 open pull requests on checkout". If nothing stands out, suggest none and say so. Leave \`objective\` and \`blocker\` empty until the user tells you what they are working on. When they do, write it up as one objective: a next move rather than a project name, in their own terms, under 12 words. Put what is in the way, if they mention anything, in \`blocker\`, as briefly.`,
3765
+ schema: {
3766
+ type: "object",
3767
+ properties: {
3768
+ why: { type: "string", description: "One or two sentences: what you looked at, or how you wrote up what they said." },
3769
+ objective: { type: "string" },
3770
+ blocker: { type: "string" },
3771
+ suggestions: {
3772
+ type: "array",
3773
+ items: {
3774
+ type: "object",
3775
+ properties: {
3776
+ objective: { type: "string" },
3777
+ evidence: { type: "string", description: "What it was drawn from, in a few words." }
3778
+ },
3779
+ required: ["objective", "evidence"],
3780
+ additionalProperties: false
3781
+ }
3782
+ }
3783
+ },
3784
+ required: ["why", "objective", "blocker", "suggestions"],
3785
+ additionalProperties: false
3786
+ }
3787
+ }
3788
+ };
3789
+ var QUESTIONS = [cli, profile, schedule, identity2, boards, calendars, objective];
3790
+ function questionById(id) {
3791
+ return QUESTIONS.find((question) => question.id === id);
3792
+ }
3793
+ function composeSources(profile2, sections, today) {
3794
+ const intro = [
3795
+ "# Sources",
3796
+ "",
3797
+ `Who you are and where to look, for your ${profile2 === "personal" ? "personal" : "work"} morning brief. Written by setup on ${today}; change it freely. The prompt next to this file (\`prompt.md\`) says *what* to gather and how to judge it; this file says *who* and *where*. A section that is missing is fine: the agent falls back to your primary calendar and connected accounts, and says what it missed.`
3798
+ ];
3799
+ return [...intro, ...sections.flatMap((section) => ["", `## ${section.heading}`, "", section.body])].join("\n") + "\n";
3800
+ }
3801
+ function machineName(platform = process.platform) {
3802
+ return platform === "darwin" ? "Mac" : platform === "win32" ? "PC" : "computer";
3803
+ }
3804
+
3025
3805
  // src/settings.ts
3026
3806
  var SETTING_GROUPS = [
3027
3807
  { id: "instance", title: "This dashboard" },
@@ -3244,7 +4024,7 @@ var SETTINGS = [
3244
4024
  label: "Jira ticket board",
3245
4025
  help: "Flags tickets whose status disagrees with their pull requests, through the Atlassian CLI. Sign in with acli jira auth login.",
3246
4026
  kind: "flag",
3247
- fallback: (profile) => profile === "personal" ? "Off" : "On"
4027
+ fallback: (profile2) => profile2 === "personal" ? "Off" : "On"
3248
4028
  },
3249
4029
  {
3250
4030
  key: "DAILY_FOCUS_JIRA_PROJECTS",
@@ -3345,7 +4125,7 @@ var SETTINGS = [
3345
4125
  label: "Show free time",
3346
4126
  help: "Find focus windows between meetings in your working hours. Off shows events only.",
3347
4127
  kind: "flag",
3348
- fallback: (profile) => profile === "personal" ? "Off" : "On"
4128
+ fallback: (profile2) => profile2 === "personal" ? "Off" : "On"
3349
4129
  },
3350
4130
  {
3351
4131
  key: "DAILY_FOCUS_WORK_START",
@@ -3382,7 +4162,7 @@ function profileOf(sources, settings = sources.settings) {
3382
4162
  return PROFILES.includes(raw) ? raw : "work";
3383
4163
  }
3384
4164
  function describeSettings(sources) {
3385
- const profile = profileOf(sources);
4165
+ const profile2 = profileOf(sources);
3386
4166
  const settings = SETTINGS.map((def) => {
3387
4167
  const fromEnv = present(sources.process, def.key);
3388
4168
  const fromSettings = present(sources.settings, def.key);
@@ -3398,7 +4178,7 @@ function describeSettings(sources) {
3398
4178
  advanced: def.advanced === true,
3399
4179
  value,
3400
4180
  source,
3401
- fallback: def.fallback(profile)
4181
+ fallback: def.fallback(profile2)
3402
4182
  };
3403
4183
  });
3404
4184
  return { groups: SETTING_GROUPS, settings, error: sources.settingsError };
@@ -3431,134 +4211,758 @@ async function saveSettings(path, settings) {
3431
4211
  await writeJsonAtomic(path, sorted);
3432
4212
  }
3433
4213
 
3434
- // src/setup.ts
3435
- import { execFile as execFile4 } from "node:child_process";
3436
- import { access, constants, readdir as readdir2, readFile as readFile7, stat as stat3 } from "node:fs/promises";
3437
- import { delimiter, isAbsolute, join as join3 } from "node:path";
3438
- import { promisify as promisify4 } from "node:util";
3439
- var run4 = promisify4(execFile4);
3440
- var PLACEHOLDERS = [
3441
- "Your Name",
3442
- "you@example.com",
3443
- "you@personal.example",
3444
- "your-github-login",
3445
- "Some Shared Calendar",
3446
- "Some Noisy Calendar",
3447
- "<where your colleagues are>",
3448
- "<where you are>",
3449
- "<where you live>",
3450
- "<document id>"
4214
+ // src/onboarding.ts
4215
+ var TURN_TIMEOUT_MS2 = 4 * 6e4;
4216
+ var KILL_AFTER_MS = 5e3;
4217
+ var STEPS_KEPT = 40;
4218
+ var SESSION_MODEL = {
4219
+ claude: { model: "opus", effort: "low" },
4220
+ codex: { model: null, effort: "low" }
4221
+ };
4222
+ var ONBOARDING_TOOLS = [
4223
+ "Bash(gh *)",
4224
+ "Bash(acli *)",
4225
+ "Bash(git config *)",
4226
+ "mcp__claude_ai_Gmail__search_threads",
4227
+ "mcp__claude_ai_Gmail__get_thread",
4228
+ "mcp__claude_ai_Gmail__get_message",
4229
+ "mcp__claude_ai_Gmail__list_labels",
4230
+ "mcp__claude_ai_Google_Calendar__list_calendars",
4231
+ "mcp__claude_ai_Google_Calendar__list_events",
4232
+ "mcp__claude_ai_Google_Calendar__search_events",
4233
+ "mcp__claude_ai_Google_Calendar__get_event",
4234
+ "mcp__claude_ai_Google_Drive__search_files",
4235
+ "mcp__claude_ai_Google_Drive__list_recent_files",
4236
+ "mcp__claude_ai_Google_Drive__get_file_metadata",
4237
+ "mcp__claude_ai_Google_Drive__read_file_content"
3451
4238
  ];
3452
- function placeholdersIn(text) {
3453
- return PLACEHOLDERS.filter((placeholder) => text.includes(placeholder)).length;
4239
+ var RESTARTED = "the dashboard restarted while this was running";
4240
+ function blankFile() {
4241
+ return { startedAt: (/* @__PURE__ */ new Date()).toISOString(), finishedAt: null, briefRequested: false, answers: {}, proposals: {} };
3454
4242
  }
3455
- async function findExecutable(name, path = process.env.PATH ?? "") {
3456
- const candidates = isAbsolute(name) ? [name] : path.split(delimiter).filter(Boolean).map((dir) => join3(dir, name));
3457
- for (const candidate of candidates) {
3458
- try {
3459
- await access(candidate, constants.X_OK);
3460
- if ((await stat3(candidate)).isFile()) return candidate;
3461
- } catch {
4243
+ function parseFile(text) {
4244
+ const file = blankFile();
4245
+ let raw;
4246
+ try {
4247
+ raw = JSON.parse(text);
4248
+ } catch {
4249
+ return file;
4250
+ }
4251
+ if (typeof raw !== "object" || raw === null) return file;
4252
+ const obj = raw;
4253
+ if (typeof obj.startedAt === "string") file.startedAt = obj.startedAt;
4254
+ if (typeof obj.finishedAt === "string") file.finishedAt = obj.finishedAt;
4255
+ file.briefRequested = obj.briefRequested === true;
4256
+ if (typeof obj.answers === "object" && obj.answers !== null) {
4257
+ for (const [id, answer] of Object.entries(obj.answers)) {
4258
+ const clean = cleanAnswer(answer);
4259
+ if (clean) file.answers[id] = clean;
3462
4260
  }
3463
4261
  }
3464
- return null;
3465
- }
3466
- async function findClis(config) {
3467
- const found = {};
3468
- for (const cli of CLI_NAMES) {
3469
- const candidates = [
3470
- config.agent.cli === cli ? config.agent.binPath : null,
3471
- config.assistant.agent === cli ? config.assistant.binPath : null,
3472
- cli
3473
- ].filter((candidate) => candidate !== null);
3474
- found[cli] = null;
3475
- for (const candidate of candidates) {
3476
- found[cli] = await findExecutable(candidate);
3477
- if (found[cli]) break;
4262
+ if (typeof obj.proposals === "object" && obj.proposals !== null) {
4263
+ for (const [id, value] of Object.entries(obj.proposals)) {
4264
+ const record = value;
4265
+ if (!record || !CLI_NAMES.includes(record.cli) || !PROFILES.includes(record.profile)) continue;
4266
+ file.proposals[id] = {
4267
+ cli: record.cli,
4268
+ profile: record.profile,
4269
+ sessionId: typeof record.sessionId === "string" ? record.sessionId : null,
4270
+ status: record.status === "ready" || record.status === "failed" ? record.status : "working",
4271
+ result: record.result ?? null,
4272
+ error: typeof record.error === "string" ? record.error : null,
4273
+ version: typeof record.version === "number" ? record.version : 0,
4274
+ conversation: Array.isArray(record.conversation) ? record.conversation.filter(
4275
+ (line) => typeof line === "object" && line !== null && (line.from === "you" || line.from === "it") && typeof line.text === "string"
4276
+ ) : []
4277
+ };
3478
4278
  }
3479
4279
  }
3480
- return found;
3481
- }
3482
- async function hasArchive(config) {
3483
- try {
3484
- return (await readdir2(config.archiveDir)).some((name) => name.endsWith(".json"));
3485
- } catch {
3486
- return false;
4280
+ return file;
4281
+ }
4282
+ function cleanAnswer(value) {
4283
+ if (typeof value !== "object" || value === null) return null;
4284
+ const obj = value;
4285
+ if (obj.later === true) return { later: true };
4286
+ const choices = {};
4287
+ const fields = {};
4288
+ if (typeof obj.choices === "object" && obj.choices !== null) {
4289
+ for (const [id, values] of Object.entries(obj.choices)) {
4290
+ if (Array.isArray(values)) choices[id] = [...new Set(values.filter((v) => typeof v === "string" && v.trim() !== ""))];
4291
+ }
3487
4292
  }
3488
- }
3489
- async function exists(path) {
3490
- try {
3491
- await stat3(path);
3492
- return true;
3493
- } catch {
3494
- return false;
4293
+ if (typeof obj.fields === "object" && obj.fields !== null) {
4294
+ for (const [id, text] of Object.entries(obj.fields)) {
4295
+ if (typeof text === "string") fields[id] = text;
4296
+ }
3495
4297
  }
4298
+ return { choices, fields };
3496
4299
  }
3497
- async function firstBriefPending(config) {
3498
- return !await exists(config.itemsFile) && !await hasArchive(config);
4300
+ function given(answer) {
4301
+ return answer && answer.later !== true ? answer : null;
3499
4302
  }
3500
- async function setupState(config, opts) {
3501
- let sources = null;
3502
- try {
3503
- sources = await readFile7(config.sourcesFile, "utf8");
3504
- } catch {
3505
- sources = null;
3506
- }
3507
- const supported = process.platform === "darwin";
4303
+ function answerFromCard(card2) {
3508
4304
  return {
3509
- // The file, not whether it parsed: a brief that is there but broken is a
3510
- // problem for its banner to report, not a store to set up from scratch. The
3511
- // agent's clock asks the same question.
3512
- needed: await firstBriefPending(config),
3513
- profile: config.profile,
3514
- profileChosen: opts.profileChosen,
3515
- sources: { exists: sources !== null, placeholders: sources === null ? 0 : placeholdersIn(sources) },
3516
- clis: await findClis(config),
3517
- calendar: {
3518
- supported,
3519
- built: supported && await exists(config.calendar.appPath),
3520
- chosen: config.calendar.names.length,
3521
- buildCommand: command("build-calendar")
3522
- }
4305
+ choices: Object.fromEntries(card2.choices.map((choice2) => [choice2.id, choice2.selected])),
4306
+ fields: Object.fromEntries(card2.fields.map((field2) => [field2.id, field2.value]))
3523
4307
  };
3524
4308
  }
3525
- var identity = null;
3526
- function detectIdentity(config) {
3527
- identity ??= (async () => {
3528
- const [user, email] = await Promise.all([
3529
- run4(config.github.ghPath, ["api", "user", "--jq", '[.login, .name // ""] | join("\\n")'], { timeout: 5e3 }).then(
3530
- ({ stdout }) => stdout.split("\n"),
3531
- () => []
3532
- ),
3533
- run4("git", ["config", "--global", "user.email"], { timeout: 2e3 }).then(
3534
- ({ stdout }) => stdout.trim() || null,
3535
- () => null
3536
- )
3537
- ]);
4309
+ var Onboarding = class {
4310
+ file = null;
4311
+ loaded = false;
4312
+ running = /* @__PURE__ */ new Map();
4313
+ steps = /* @__PURE__ */ new Map();
4314
+ /** Calendar.app's calendars being listed, which only starting the sessions waits for. */
4315
+ calendars = null;
4316
+ /** What listing them found, once it has: the calendars, or why they couldn't be listed. */
4317
+ listed = null;
4318
+ finishError = null;
4319
+ config;
4320
+ deps;
4321
+ spawnFn;
4322
+ gates = /* @__PURE__ */ new Map();
4323
+ codexOff = null;
4324
+ codexModel = null;
4325
+ /** What looking for the CLIs found, once the screen has asked. Not kept: what is installed changes. */
4326
+ detected = null;
4327
+ detecting = null;
4328
+ /** The pass starting sessions, which the next one waits for; see `startProposals`. */
4329
+ starting = Promise.resolve();
4330
+ /** The write in progress, which the next one waits for. */
4331
+ writes = Promise.resolve();
4332
+ constructor(config, deps) {
4333
+ this.config = config;
4334
+ this.deps = deps;
4335
+ this.spawnFn = deps.spawnFn ?? spawn2;
4336
+ }
4337
+ /** Whether a session has a process going. A restart waits for none. */
4338
+ get busy() {
4339
+ return this.running.size > 0;
4340
+ }
4341
+ /**
4342
+ * Read the file, and start again any session the last process left going.
4343
+ * Only while setup is still to do: `needed` is the caller's `firstBriefPending`.
4344
+ */
4345
+ async start(needed) {
4346
+ await this.load();
4347
+ if (!needed || this.file?.finishedAt) return;
4348
+ for (const record of Object.values(this.file?.proposals ?? {})) {
4349
+ if (record.status === "working") {
4350
+ record.status = "failed";
4351
+ record.error = RESTARTED;
4352
+ }
4353
+ }
4354
+ this.startProposals();
4355
+ }
4356
+ /** The screen, while setup is to do: the store has never had a brief, and setup hasn't finished. */
4357
+ async view(setup) {
4358
+ await this.load();
4359
+ if (!setup.needed || this.file?.finishedAt) return null;
4360
+ const file = this.file ?? blankFile();
4361
+ const ctx = await this.context({ setup });
4362
+ const sources = this.deps.readSources();
4363
+ const answers = this.effective(ctx, file.answers);
4364
+ const steps = [];
4365
+ for (const question of this.questions(ctx)) {
4366
+ const locked = this.locked(question, sources);
4367
+ const drawn = question.card(ctx);
4368
+ const record = file.proposals[question.id];
4369
+ const answer = locked ? question.id === "cli" ? { choices: { cli: ctx.cli ? [ctx.cli] : [] }, fields: {} } : answerFromCard(drawn) : answers[question.id] ?? null;
4370
+ const chosen = given(answer);
4371
+ const looking = question.id === "cli" && !answer && !this.detected;
4372
+ steps.push({
4373
+ id: question.id,
4374
+ title: question.title(ctx),
4375
+ topic: question.topic,
4376
+ status: answer ? chosen ? "answered" : "later" : "open",
4377
+ summary: chosen ? question.summary(chosen, ctx) : answer ? question.later?.summary ?? null : null,
4378
+ locked,
4379
+ card: { ...drawn, version: record?.version ?? 0, revisable: Boolean(question.agent && record), later: question.later?.label ?? null },
4380
+ proposal: looking ? {
4381
+ status: "working",
4382
+ conversation: [],
4383
+ steps: [{ kind: "tool", text: "Look for Codex and Claude Code", detail: null, state: "running" }],
4384
+ error: null
4385
+ } : record ? {
4386
+ status: record.status,
4387
+ conversation: record.conversation,
4388
+ steps: this.steps.get(question.id) ?? [],
4389
+ error: record.status === "failed" ? record.error : null
4390
+ } : null,
4391
+ answer
4392
+ });
4393
+ }
3538
4394
  return {
3539
- login: config.github.accounts[0] ?? (user[0]?.trim() || null),
3540
- name: user[1]?.trim() || null,
3541
- email
4395
+ detected: this.detected !== null,
4396
+ steps,
4397
+ current: steps.find((step) => step.status === "open")?.id ?? null,
4398
+ error: this.finishError
3542
4399
  };
3543
- })();
3544
- return identity;
3545
- }
3546
- function fillIdentity(template, who) {
3547
- let text = template;
3548
- if (who.name) text = text.replaceAll("`Your Name`", `\`${who.name}\``);
3549
- if (who.email) text = text.replaceAll("`you@example.com`", `\`${who.email}\``).replaceAll("`you@personal.example`", `\`${who.email}\``);
3550
- if (who.login) text = text.replaceAll("`your-github-login`", `\`${who.login}\``);
3551
- return text;
4400
+ }
4401
+ /**
4402
+ * Look for the CLIs, which the screen asks for when it opens: with one ready,
4403
+ * the first question answers itself. Asked again by Check again.
4404
+ */
4405
+ async detect() {
4406
+ await this.load();
4407
+ await this.lookForClis(true);
4408
+ this.startProposals();
4409
+ this.deps.onChange();
4410
+ }
4411
+ /**
4412
+ * Record an answer, or that the user would rather decide later. One whose
4413
+ * settings the dashboard would refuse is refused here, while the card that
4414
+ * can fix it is still open.
4415
+ */
4416
+ async answer(id, raw) {
4417
+ await this.load();
4418
+ const file = this.file ??= blankFile();
4419
+ if (file.finishedAt) return "setup has finished";
4420
+ const question = questionById(id);
4421
+ let answer = cleanAnswer(raw);
4422
+ if (!question || !answer) return `there is no question ${JSON.stringify(id)}, or that is not an answer to it`;
4423
+ if (!given(answer) && !question.later) return `"${question.topic}" needs an answer`;
4424
+ if (id === "cli" && given(answer)) {
4425
+ const chosen = await this.chooseCli(given(answer));
4426
+ if (typeof chosen === "string") return chosen;
4427
+ answer = chosen;
4428
+ }
4429
+ const ctx = await this.context();
4430
+ const before = { cli: ctx.cli, profile: ctx.profile };
4431
+ if (given(answer) && question.settings) {
4432
+ const refused = this.deps.checkSettings(this.settingsOf({ ...file.answers, [id]: answer }, await this.context({ answers: { ...file.answers, [id]: answer } })));
4433
+ if (refused) return refused;
4434
+ }
4435
+ file.answers[id] = answer;
4436
+ this.finishError = null;
4437
+ await this.stop(id, "you continued before it finished");
4438
+ const after = await this.context();
4439
+ if (after.profile !== before.profile) {
4440
+ for (const other of QUESTIONS) {
4441
+ if (!other.agent) continue;
4442
+ delete file.answers[other.id];
4443
+ await this.drop(other.id);
4444
+ }
4445
+ } else if (after.cli !== before.cli) {
4446
+ for (const other of QUESTIONS) if (other.agent && !file.answers[other.id]) await this.drop(other.id);
4447
+ }
4448
+ await this.save();
4449
+ this.startProposals();
4450
+ this.deps.onChange();
4451
+ return null;
4452
+ }
4453
+ /** Tell a question's session what to change. `draft` is what the card holds now, which it should start from. */
4454
+ async revise(id, text, draft) {
4455
+ await this.load();
4456
+ const question = questionById(id);
4457
+ const record = this.file?.proposals[id];
4458
+ if (!question?.agent || !record) return "nothing is looking into that question";
4459
+ if (this.running.has(id)) return "it is still working on that question";
4460
+ const ctx = await this.context();
4461
+ const current = cleanAnswer(draft);
4462
+ const request = [
4463
+ `The user says: ${text.trim()}`,
4464
+ ...given(current) ? ["", "What the card holds now, which they may have changed:", "", "```json", JSON.stringify(current), "```"] : [],
4465
+ "",
4466
+ "Revise your proposal, and answer with the whole of it again."
4467
+ ].join("\n");
4468
+ const resume = record.sessionId && record.cli === ctx.cli ? record.sessionId : null;
4469
+ const said = [...record.conversation, { from: "you", text: text.trim() }];
4470
+ await this.ask(question, ctx, resume ? request : `${await this.firstMessage(question, ctx)}
4471
+
4472
+ ${request}`, resume, said);
4473
+ return null;
4474
+ }
4475
+ /**
4476
+ * Look again: ask a question's session again from the start, after it failed,
4477
+ * and list Calendar.app's calendars again for the question that shows them,
4478
+ * since what failed may have been the access the user has since granted.
4479
+ */
4480
+ async retry(id) {
4481
+ await this.load();
4482
+ const question = questionById(id);
4483
+ if (!question) return `there is no question ${JSON.stringify(id)}`;
4484
+ if (this.running.has(id)) return "it is still working on that question";
4485
+ if (id === "calendars") {
4486
+ this.calendars = null;
4487
+ this.listed = null;
4488
+ }
4489
+ if (id === "cli") await this.lookForClis(true);
4490
+ if (question.agent) await this.drop(id);
4491
+ this.startProposals();
4492
+ this.deps.onChange();
4493
+ return null;
4494
+ }
4495
+ /**
4496
+ * Save everything, and ask for the first brief. Files first, then the
4497
+ * settings, whose save restarts the dashboard: the start after it begins the
4498
+ * run. Resolves with why not, or null.
4499
+ */
4500
+ async finish() {
4501
+ await this.load();
4502
+ const file = this.file ??= blankFile();
4503
+ if (file.finishedAt) return { error: "setup has already finished" };
4504
+ if (!file.answers.cli) await this.lookForClis(false);
4505
+ const ctx = await this.context();
4506
+ const sources = this.deps.readSources();
4507
+ const answers = this.effective(ctx, file.answers);
4508
+ const sections = [];
4509
+ let objective2 = null;
4510
+ for (const question of this.questions(ctx)) {
4511
+ const answer = answers[question.id];
4512
+ if (!answer && !this.locked(question, sources)) return this.refuse(`answer "${question.title(ctx)}", or decide it later`);
4513
+ const chosen = this.locked(question, sources) ? null : given(answer);
4514
+ if (!chosen) continue;
4515
+ sections.push(...question.sources?.(chosen, ctx) ?? []);
4516
+ objective2 = question.objective?.(chosen) ?? objective2;
4517
+ }
4518
+ const values = this.settingsOf(file.answers, ctx);
4519
+ const refused = this.deps.checkSettings(values);
4520
+ if (refused) return this.refuse(refused);
4521
+ await this.starting;
4522
+ for (const id of [...this.running.keys()]) await this.stop(id, "setup finished");
4523
+ const existing = await readEditable(this.config.sourcesFile);
4524
+ if (existing.text === null && sections.length > 0) {
4525
+ await mkdir2(this.config.dataDir, { recursive: true });
4526
+ await saveEditable(this.config.sourcesFile, composeSources(ctx.profile, sections, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)), ABSENT);
4527
+ }
4528
+ if (objective2) {
4529
+ const focus = await readEditable(this.config.focusFile);
4530
+ const saved = await saveEditable(this.config.focusFile, setFocusFields(focus.text, objective2, FOCUS_TEMPLATE), focus.version);
4531
+ if (!saved.saved) return this.refuse("focus.md changed on disk while saving; finish again");
4532
+ }
4533
+ file.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
4534
+ file.briefRequested = ctx.cli !== null;
4535
+ await this.save();
4536
+ const before = JSON.stringify(Object.entries(sources.settings).sort());
4537
+ const saveRefused = await this.deps.saveSettings(values);
4538
+ if (saveRefused) {
4539
+ file.finishedAt = null;
4540
+ file.briefRequested = false;
4541
+ await this.save();
4542
+ return this.refuse(saveRefused);
4543
+ }
4544
+ this.finishError = null;
4545
+ this.deps.onChange();
4546
+ return { restart: JSON.stringify(Object.entries(this.deps.readSources().settings).sort()) !== before };
4547
+ }
4548
+ /**
4549
+ * Whether the finish asked for a first brief that hasn't been started, and
4550
+ * forget it: asked once, so a run that fails is retried by the user, not by
4551
+ * every start.
4552
+ */
4553
+ async takeBriefRequest() {
4554
+ await this.load();
4555
+ if (!this.file?.briefRequested) return false;
4556
+ this.file.briefRequested = false;
4557
+ await this.save();
4558
+ return true;
4559
+ }
4560
+ /** What the finish would write as the source list, or null when the store already has one it would keep. */
4561
+ async previewSources() {
4562
+ await this.load();
4563
+ const existing = await readEditable(this.config.sourcesFile);
4564
+ if (existing.text !== null) return { text: null, kept: true };
4565
+ const ctx = await this.context();
4566
+ const sections = [];
4567
+ for (const question of this.questions(ctx)) {
4568
+ const chosen = given(this.file?.answers[question.id]);
4569
+ if (chosen) sections.push(...question.sources?.(chosen, ctx) ?? []);
4570
+ }
4571
+ return { text: sections.length ? composeSources(ctx.profile, sections, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) : null, kept: false };
4572
+ }
4573
+ /** Stop every session, for shutdown, and wait until each has exited and been written down. */
4574
+ async close() {
4575
+ await this.starting;
4576
+ const over = [...this.running.values()].map((run5) => run5.over);
4577
+ for (const id of [...this.running.keys()]) await this.stop(id, RESTARTED);
4578
+ await Promise.all(over);
4579
+ }
4580
+ /* ---------- the sessions ---------- */
4581
+ /**
4582
+ * Start whatever sessions the answers so far call for, without holding up the
4583
+ * reply that asked: starting one under Codex runs a few checks first, and a
4584
+ * screen waiting on them has every button greyed out with nothing to say why.
4585
+ * One pass at a time, so two quick answers can't both start the same question.
4586
+ */
4587
+ startProposals() {
4588
+ this.starting = this.starting.then(() => this.ensureProposals()).catch((err) => console.error(`[daily-focus] setup could not start looking: ${err.message}`));
4589
+ }
4590
+ /** Resolves once the sessions asked for so far have started: for tests, and for anything that must not race them. */
4591
+ started() {
4592
+ return this.starting;
4593
+ }
4594
+ /** Start a session for each question that has one and needs it: the CLI and the profile are known, and nobody has asked yet. */
4595
+ async ensureProposals() {
4596
+ const file = this.file;
4597
+ if (!file || file.finishedAt) return;
4598
+ if (Object.keys(file.answers).length === 0) return;
4599
+ const set = (this.deps.readSources().process.DAILY_FOCUS_AGENT ?? "").trim();
4600
+ if (file.answers.cli && !given(file.answers.cli) || set && !CLI_NAMES.includes(set)) return;
4601
+ await this.lookForClis(false);
4602
+ const ctx = await this.context({ listCalendars: true });
4603
+ const sources = this.deps.readSources();
4604
+ const profileKnown = given(file.answers.profile) !== null || (sources.process.DAILY_FOCUS_PROFILE ?? "").trim() !== "";
4605
+ if (!ctx.cli || !profileKnown) return;
4606
+ for (const question of this.questions(ctx)) {
4607
+ if (!question.agent || file.answers[question.id] || this.running.has(question.id)) continue;
4608
+ const record = file.proposals[question.id];
4609
+ if (record && record.cli === ctx.cli && record.profile === ctx.profile && record.status !== "failed") continue;
4610
+ if (record?.status === "failed" && record.error !== RESTARTED && record.cli === ctx.cli && record.profile === ctx.profile) continue;
4611
+ await this.ask(question, ctx, await this.firstMessage(question, ctx), null);
4612
+ }
4613
+ }
4614
+ /** The first message of a question's session: the instructions, what is known, and the question. */
4615
+ async firstMessage(question, ctx) {
4616
+ const instructions = await readFile8(this.config.onboardingPromptSource, "utf8").catch(
4617
+ () => "You are helping someone set up Daily Focus. Only read; never send, post or edit anything. Answer with the JSON the schema asks for."
4618
+ );
4619
+ const agent = question.agent;
4620
+ const facts = agent.facts?.(ctx) ?? null;
4621
+ return [
4622
+ instructions.trim(),
4623
+ "",
4624
+ "## This setup",
4625
+ "",
4626
+ `- Profile: ${ctx.profile}`,
4627
+ `- Today: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`,
4628
+ "",
4629
+ `## The question: ${question.title(ctx)}`,
4630
+ "",
4631
+ agent.brief(ctx),
4632
+ ...facts ? ["", "### What the dashboard already knows", "", facts] : []
4633
+ ].join("\n");
4634
+ }
4635
+ async ask(question, ctx, prompt, resume, conversation = []) {
4636
+ const file = this.file ??= blankFile();
4637
+ const cli2 = ctx.cli;
4638
+ const previous = file.proposals[question.id];
4639
+ const record = {
4640
+ cli: cli2,
4641
+ profile: ctx.profile,
4642
+ sessionId: resume,
4643
+ status: "working",
4644
+ result: previous?.cli === cli2 && previous.profile === ctx.profile ? previous.result : null,
4645
+ error: null,
4646
+ version: previous?.version ?? 0,
4647
+ conversation
4648
+ };
4649
+ file.proposals[question.id] = record;
4650
+ this.steps.set(question.id, []);
4651
+ await this.save();
4652
+ this.deps.onChange();
4653
+ const bin = ctx.clis?.[cli2]?.path ?? null;
4654
+ if (!bin) return this.settle(question.id, record, { ok: false, error: `could not find ${cli2} on this machine` });
4655
+ if (cli2 === "codex") {
4656
+ const gate = this.gates.get(bin) ?? new CodexGate(bin, this.deps.execFn);
4657
+ this.gates.set(bin, gate);
4658
+ const refusal2 = await gate.refusal();
4659
+ if (refusal2) return this.settle(question.id, record, { ok: false, error: refusal2 });
4660
+ }
4661
+ let codexConfig = [];
4662
+ if (cli2 === "codex") {
4663
+ try {
4664
+ this.codexOff ??= computerUseOff(bin, this.deps.execFn);
4665
+ codexConfig = await this.codexOff;
4666
+ } catch (err) {
4667
+ this.codexOff = null;
4668
+ return this.settle(question.id, record, { ok: false, error: `Codex could not say which plugins to switch off: ${err.message}` });
4669
+ }
4670
+ }
4671
+ const schemaDir = await mkdtemp(join5(tmpdir2(), "daily-focus-setup-"));
4672
+ const schemaFile = join5(schemaDir, "schema.json");
4673
+ const json = JSON.stringify(question.agent.schema);
4674
+ await writeFile2(schemaFile, json, "utf8");
4675
+ await mkdir2(this.config.onboardingDir, { recursive: true });
4676
+ const adapter = ADAPTERS[cli2];
4677
+ const session = SESSION_MODEL[cli2];
4678
+ const model = cli2 === "codex" ? await (this.codexModel ??= codexLatestModel(bin, this.deps.execFn)) : session.model;
4679
+ const args = adapter.args({
4680
+ prompt,
4681
+ sessionId: resume,
4682
+ model,
4683
+ effort: session.effort,
4684
+ tools: ONBOARDING_TOOLS,
4685
+ denyEdits: true,
4686
+ drafts: false,
4687
+ codexConfig,
4688
+ schema: { json, file: schemaFile }
4689
+ });
4690
+ let child;
4691
+ try {
4692
+ child = this.spawnFn(bin, args, { cwd: this.config.onboardingDir, stdio: ["ignore", "pipe", "pipe"], env: process.env });
4693
+ } catch (err) {
4694
+ await rm2(schemaDir, { recursive: true, force: true });
4695
+ return this.settle(question.id, record, { ok: false, error: `could not run ${bin}: ${err.message}` });
4696
+ }
4697
+ const timer = setTimeout(() => void this.stop(question.id, "gave up after four minutes"), TURN_TIMEOUT_MS2);
4698
+ timer.unref();
4699
+ let ended;
4700
+ const run5 = { child, timer, abortReason: null, over: new Promise((resolve2) => ended = resolve2) };
4701
+ this.running.set(question.id, run5);
4702
+ let done;
4703
+ let said = "";
4704
+ const stderr = [];
4705
+ child.stderr?.on("data", (chunk) => {
4706
+ stderr.push(chunk.toString("utf8"));
4707
+ if (stderr.length > 50) stderr.shift();
4708
+ });
4709
+ const calls = /* @__PURE__ */ new Map();
4710
+ const steps = this.steps.get(question.id);
4711
+ const add = (step) => {
4712
+ steps.push(step);
4713
+ if (steps.length > STEPS_KEPT) steps.shift();
4714
+ };
4715
+ if (child.stdout) {
4716
+ createInterface2({ input: child.stdout }).on("line", (line) => {
4717
+ const event = adapter.parse(line);
4718
+ if (!event) return;
4719
+ if (event.sessionId) record.sessionId = event.sessionId;
4720
+ if (event.text) said = event.text;
4721
+ for (const parsed of event.steps ?? []) {
4722
+ const known = calls.get(parsed.id);
4723
+ if (known) {
4724
+ if (parsed.text) known.text = parsed.text;
4725
+ if (parsed.detail) known.detail = parsed.detail;
4726
+ known.state = parsed.state;
4727
+ } else if (parsed.text) {
4728
+ const step = { kind: "tool", text: parsed.text, detail: parsed.detail ?? null, state: parsed.state };
4729
+ calls.set(parsed.id, step);
4730
+ add(step);
4731
+ }
4732
+ }
4733
+ if (event.steps) this.deps.onChange();
4734
+ if (event.done) done = event.done;
4735
+ });
4736
+ }
4737
+ child.on("error", (err) => {
4738
+ done = { ok: false, error: `could not run ${bin}: ${err.message}` };
4739
+ });
4740
+ child.on("exit", () => {
4741
+ if (run5.abortReason === null) return;
4742
+ child.stdout?.destroy();
4743
+ child.stderr?.destroy();
4744
+ });
4745
+ child.on("close", (code) => {
4746
+ clearTimeout(timer);
4747
+ this.running.delete(question.id);
4748
+ void rm2(schemaDir, { recursive: true, force: true });
4749
+ const outcome = run5.abortReason ? { ok: false, error: run5.abortReason } : done ?? {
4750
+ ok: false,
4751
+ error: `${cli2} exited with code ${code}${stderr.length ? `: ${stderr.join("").trim().split("\n").slice(-3).join(" ")}` : ""}`
4752
+ };
4753
+ void this.settle(question.id, record, outcome.ok ? { ok: true, reply: outcome.reply ?? said } : outcome).catch((err) => console.error(`[daily-focus] setup could not keep what it found: ${err.message}`)).finally(ended);
4754
+ });
4755
+ }
4756
+ /** A session is over: keep its answer when it is JSON, or why it isn't. */
4757
+ async settle(id, record, outcome) {
4758
+ if (this.file?.proposals[id] !== record) return;
4759
+ for (const step of this.steps.get(id) ?? []) if (step.state === "running") step.state = outcome.ok ? "done" : "failed";
4760
+ if (outcome.ok) {
4761
+ try {
4762
+ record.result = JSON.parse(stripFence(outcome.reply ?? ""));
4763
+ record.status = "ready";
4764
+ record.error = null;
4765
+ record.version += 1;
4766
+ const why = record.result?.why;
4767
+ if (typeof why === "string" && why.trim()) record.conversation.push({ from: "it", text: why.trim() });
4768
+ } catch {
4769
+ record.status = "failed";
4770
+ record.error = "it answered, but not in the shape asked for";
4771
+ }
4772
+ } else {
4773
+ record.status = "failed";
4774
+ record.error = outcome.error ?? "failed";
4775
+ }
4776
+ await this.save();
4777
+ this.deps.onChange();
4778
+ }
4779
+ /** Stop a session: asked to, then made to if it hasn't gone within a few seconds. */
4780
+ async stop(id, reason) {
4781
+ const run5 = this.running.get(id);
4782
+ if (!run5 || run5.abortReason !== null) return;
4783
+ run5.abortReason = reason;
4784
+ run5.child.kill("SIGTERM");
4785
+ const kill = setTimeout(() => {
4786
+ if (run5.child.exitCode === null && run5.child.signalCode === null) run5.child.kill("SIGKILL");
4787
+ }, KILL_AFTER_MS);
4788
+ kill.unref();
4789
+ void run5.over.then(() => clearTimeout(kill));
4790
+ }
4791
+ /** Forget a question's proposal, stopping its session first. */
4792
+ async drop(id) {
4793
+ await this.stop(id, "no longer needed");
4794
+ if (this.file) delete this.file.proposals[id];
4795
+ this.steps.delete(id);
4796
+ }
4797
+ /* ---------- what a question may read ---------- */
4798
+ /**
4799
+ * Look for the CLIs, once per process unless `again`. Only from a click or
4800
+ * from setup already under way: `--version` and a login check are programs
4801
+ * run on the user's behalf, and a new store's state is read before anyone is
4802
+ * at the screen.
4803
+ */
4804
+ async lookForClis(again) {
4805
+ if (this.detected && !again) return;
4806
+ this.detecting ??= this.deps.detectClis().finally(() => {
4807
+ this.detecting = null;
4808
+ });
4809
+ this.detected = await this.detecting;
4810
+ }
4811
+ /**
4812
+ * The CLI answer to keep: one of the ready ones, or a path the user typed,
4813
+ * which is used only once it has said what it is and that it is ready.
4814
+ * Otherwise why not, in a sentence the card shows.
4815
+ */
4816
+ async chooseCli(answer) {
4817
+ const typed = answer.fields.path?.trim();
4818
+ if (!typed) {
4819
+ const picked = answer.choices.cli?.[0];
4820
+ await this.lookForClis(false);
4821
+ if (picked && this.detected?.[picked]?.ready) return { choices: { cli: [picked] }, fields: {} };
4822
+ return "Enter the path to codex or claude, or decide this later";
4823
+ }
4824
+ const { cli: cli2, found } = await this.deps.probeCli(expandHome(typed));
4825
+ if (!cli2 || !found.ready) {
4826
+ return `${found.problem ?? "That can't be used"}${found.fix ? `: run ${found.fix} in a terminal, then try again.` : "."}`;
4827
+ }
4828
+ await this.lookForClis(false);
4829
+ this.detected = { ...this.detected, [cli2]: found };
4830
+ return { choices: { cli: [cli2] }, fields: { path: found.path } };
4831
+ }
4832
+ /**
4833
+ * The answers as they stand, with the CLI setup picked without asking when
4834
+ * the user hasn't picked one: the first ready, or the one already set.
4835
+ */
4836
+ effective(ctx, answers) {
4837
+ if (answers.cli || !ctx.cli) return { ...answers };
4838
+ return { ...answers, cli: { choices: { cli: [ctx.cli] }, fields: {} } };
4839
+ }
4840
+ /** Set in the environment, which wins over setup: shown as it stands, never asked. */
4841
+ locked(question, sources) {
4842
+ return question.keys.length > 0 && question.keys.every((key) => (sources.process[key] ?? "").trim() !== "");
4843
+ }
4844
+ /**
4845
+ * The settings these answers add up to, in question order, less what the
4846
+ * environment sets: that can't be saved over, and wins anyway.
4847
+ */
4848
+ settingsOf(answers, ctx) {
4849
+ const sources = this.deps.readSources();
4850
+ const values = {};
4851
+ const effective = this.effective(ctx, answers);
4852
+ for (const question of this.questions(ctx)) {
4853
+ const chosen = this.locked(question, sources) ? null : given(effective[question.id]);
4854
+ if (chosen) Object.assign(values, question.settings?.(chosen, ctx) ?? {});
4855
+ }
4856
+ for (const key of Object.keys(values)) if ((sources.process[key] ?? "").trim() !== "") delete values[key];
4857
+ return values;
4858
+ }
4859
+ questions(ctx) {
4860
+ return QUESTIONS.filter((question) => !question.applies || question.applies(ctx));
4861
+ }
4862
+ /**
4863
+ * Everything a question may read. The CLI and the profile are the answers
4864
+ * when given, else what is in effect, else the card's own pick, so the
4865
+ * questions that follow from them are counted from the start.
4866
+ *
4867
+ * Calendar.app's calendars only as already listed, unless `listCalendars`:
4868
+ * the helper may ask for calendar access, so it is launched only from a
4869
+ * click, never from a state read, which any GET makes.
4870
+ */
4871
+ async context(opts = {}) {
4872
+ const sources = this.deps.readSources();
4873
+ const env = layerEnv(sources);
4874
+ const current = {};
4875
+ for (const def of SETTINGS) {
4876
+ const value = (env[def.key] ?? "").trim();
4877
+ if (value) current[def.key] = value;
4878
+ }
4879
+ const setup = opts.setup ?? await this.deps.setup();
4880
+ const answers = opts.answers ?? this.file?.answers ?? {};
4881
+ const profileAnswer = given(answers.profile)?.choices.profile?.[0];
4882
+ const profile2 = PROFILES.includes(profileAnswer) ? profileAnswer : current.DAILY_FOCUS_PROFILE === "personal" ? "personal" : "work";
4883
+ const cliAnswer = given(answers.cli);
4884
+ const typed = cliAnswer?.fields.path?.trim();
4885
+ const answered = cliAnswer?.choices.cli?.[0];
4886
+ let clis = this.detected;
4887
+ if (clis && typed && answered && CLI_NAMES.includes(answered) && clis[answered].path !== typed) {
4888
+ clis = { ...clis, [answered]: { ...clis[answered], path: typed, onPath: false, ready: true, problem: null, fix: null } };
4889
+ }
4890
+ const set = current.DAILY_FOCUS_AGENT;
4891
+ const cliLocked = (sources.process.DAILY_FOCUS_AGENT ?? "").trim() !== "";
4892
+ const cli2 = answers.cli ? answered ?? null : cliLocked ? set && CLI_NAMES.includes(set) ? set : null : set && clis?.[set]?.ready ? set : clis ? preferredCli(clis) : null;
4893
+ const started = Object.keys(answers).length > 0;
4894
+ const profileKnown = started && (given(answers.profile) !== null || (sources.process.DAILY_FOCUS_PROFILE ?? "").trim() !== "");
4895
+ let calendars2 = null;
4896
+ const liveAgenda = !["off", "false", "0", "no"].includes((current.DAILY_FOCUS_CALENDAR ?? "").toLowerCase());
4897
+ if (setup.calendar.supported && setup.calendar.built && liveAgenda) {
4898
+ if (opts.listCalendars === true && profileKnown) {
4899
+ this.calendars ??= this.deps.listCalendars().catch((err) => err instanceof Error ? err.message : String(err)).then((listed2) => {
4900
+ this.listed = listed2;
4901
+ });
4902
+ await this.calendars;
4903
+ }
4904
+ calendars2 = this.listed;
4905
+ }
4906
+ const proposals = {};
4907
+ for (const [id, record] of Object.entries(this.file?.proposals ?? {})) {
4908
+ if (record.cli === cli2 && record.profile === profile2 && record.result !== null) proposals[id] = record.result;
4909
+ }
4910
+ return {
4911
+ machine: machineName(),
4912
+ profile: profile2,
4913
+ cli: cli2 && CLI_NAMES.includes(cli2) ? cli2 : null,
4914
+ clis,
4915
+ identity: started ? await this.deps.identity() : { name: null, login: null, logins: [], email: null },
4916
+ calendars: calendars2,
4917
+ calendar: setup.calendar,
4918
+ boards: await this.deps.boards(),
4919
+ current,
4920
+ proposals
4921
+ };
4922
+ }
4923
+ /* ---------- the file ---------- */
4924
+ async load() {
4925
+ if (this.loaded) return;
4926
+ this.loaded = true;
4927
+ try {
4928
+ this.file = parseFile(await readFile8(this.config.onboardingFile, "utf8"));
4929
+ } catch {
4930
+ this.file = null;
4931
+ }
4932
+ }
4933
+ /**
4934
+ * Write the file as it is now. One write at a time: sessions settle side by
4935
+ * side, and two renames of the same temporary file would trip over each other.
4936
+ */
4937
+ save() {
4938
+ const write = this.writes.then(async () => {
4939
+ if (!this.file) return;
4940
+ await mkdir2(this.config.dataDir, { recursive: true });
4941
+ await writeJsonAtomic(this.config.onboardingFile, this.file);
4942
+ });
4943
+ this.writes = write.catch(() => {
4944
+ });
4945
+ return write;
4946
+ }
4947
+ refuse(error) {
4948
+ this.finishError = error;
4949
+ this.deps.onChange();
4950
+ return { error };
4951
+ }
4952
+ };
4953
+ function stripFence(text) {
4954
+ const fenced = /^```(?:json)?\s*\n([\s\S]*?)\n```\s*$/.exec(text.trim());
4955
+ return fenced ? fenced[1] : text.trim();
3552
4956
  }
3553
4957
 
3554
4958
  // src/shellpath.ts
3555
- import { spawn as spawn2 } from "node:child_process";
4959
+ import { spawn as spawn3 } from "node:child_process";
3556
4960
  import { userInfo } from "node:os";
3557
4961
  var START = "__DAILY_FOCUS_PATH_START__";
3558
4962
  var END = "__DAILY_FOCUS_PATH_END__";
3559
4963
  var SHELL_TIMEOUT_MS = 5e3;
3560
4964
  var defaultExec = (file, args, { timeout }) => new Promise((resolve2, reject) => {
3561
- const child = spawn2(file, args, { detached: true, stdio: ["ignore", "pipe", "ignore"] });
4965
+ const child = spawn3(file, args, { detached: true, stdio: ["ignore", "pipe", "ignore"] });
3562
4966
  let output = "";
3563
4967
  let settled = false;
3564
4968
  const finish = (error) => {
@@ -3650,7 +5054,7 @@ async function hydratePath(env = process.env, platform = process.platform, exec
3650
5054
 
3651
5055
  // src/server.ts
3652
5056
  var PUBLIC_DIR = resolve(fileURLToPath(new URL("../public", import.meta.url)));
3653
- var VERSION = packageVersion();
5057
+ var VERSION2 = packageVersion();
3654
5058
  var MIME = {
3655
5059
  ".html": "text/html; charset=utf-8",
3656
5060
  ".js": "text/javascript; charset=utf-8",
@@ -3693,7 +5097,7 @@ async function readBody(req) {
3693
5097
  }
3694
5098
  async function serveStatic(res, urlPath) {
3695
5099
  const relative = normalize(decodeURIComponent(urlPath === "/" ? "/index.html" : urlPath));
3696
- const filePath = join4(PUBLIC_DIR, relative);
5100
+ const filePath = join6(PUBLIC_DIR, relative);
3697
5101
  if (filePath !== PUBLIC_DIR && !filePath.startsWith(PUBLIC_DIR + sep)) {
3698
5102
  res.writeHead(403).end("Forbidden");
3699
5103
  return;
@@ -3743,7 +5147,7 @@ function configure(sources) {
3743
5147
  function statusOf(state, config, busy) {
3744
5148
  const last = state.agentRun.last;
3745
5149
  return {
3746
- version: VERSION,
5150
+ version: VERSION2,
3747
5151
  dataDir: config.dataDir,
3748
5152
  profile: config.profile,
3749
5153
  setupNeeded: state.setup.needed,
@@ -3782,7 +5186,7 @@ async function startServer(env, options = {}) {
3782
5186
  let restart = null;
3783
5187
  let restarting = false;
3784
5188
  let settingsWrites = Promise.resolve();
3785
- await stat4(join4(PUBLIC_DIR, "app.js")).catch(() => {
5189
+ await stat4(join6(PUBLIC_DIR, "app.js")).catch(() => {
3786
5190
  console.warn(
3787
5191
  PACKAGED ? "[daily-focus] public/app.js is missing from the package: reinstall it" : "[daily-focus] public/app.js is missing: run `npm run build` (npm start does so on its own)"
3788
5192
  );
@@ -3822,6 +5226,46 @@ async function startServer(env, options = {}) {
3822
5226
  restartWhenIdle();
3823
5227
  }
3824
5228
  });
5229
+ const onboarding = new Onboarding(config, {
5230
+ onChange: () => {
5231
+ void broadcast();
5232
+ restartWhenIdle();
5233
+ },
5234
+ // A path already set for either CLI is looked at before PATH.
5235
+ detectClis: () => {
5236
+ const configured = {};
5237
+ if (config.agent.cli && config.agent.binPath !== config.agent.cli) configured[config.agent.cli] = config.agent.binPath;
5238
+ if (config.assistant.agent && config.assistant.binPath !== config.assistant.agent) configured[config.assistant.agent] = config.assistant.binPath;
5239
+ return detectClis(configured);
5240
+ },
5241
+ probeCli: (path) => probeCli(path, null, false),
5242
+ readSources,
5243
+ setup: () => setupState(config),
5244
+ identity: () => detectIdentity(config),
5245
+ boards: async () => {
5246
+ const actions = await store.readActions();
5247
+ const now = /* @__PURE__ */ new Date();
5248
+ const health = (view) => ({
5249
+ enabled: view.enabled,
5250
+ fetchedAt: view.fetchedAt,
5251
+ reason: view.reason?.message ?? null
5252
+ });
5253
+ return { github: health(board.view(actions, now)), jira: health(tickets.view(actions, now)) };
5254
+ },
5255
+ listCalendars: async () => {
5256
+ const facts = await runHelper(config.calendar.appPath, []);
5257
+ const today = /* @__PURE__ */ new Map();
5258
+ for (const event of facts.events) {
5259
+ if (typeof event.calendarId === "string") today.set(event.calendarId, (today.get(event.calendarId) ?? 0) + 1);
5260
+ }
5261
+ return facts.calendars.map(({ id, title, source }) => ({ title, source, eventsToday: today.get(id) ?? 0 }));
5262
+ },
5263
+ checkSettings: (values) => {
5264
+ const applied = applySettingsChange(readSources(), values);
5265
+ return "error" in applied ? applied.error : null;
5266
+ },
5267
+ saveSettings: (values) => saveSettingsValues(values)
5268
+ });
3825
5269
  let codexWarnings = [];
3826
5270
  const codexBins = /* @__PURE__ */ new Set();
3827
5271
  if (config.agent.cli === "codex") codexBins.add(config.agent.binPath);
@@ -3831,12 +5275,34 @@ async function startServer(env, options = {}) {
3831
5275
  for (const warning of codexWarnings) console.warn(`[daily-focus] ${warning}`);
3832
5276
  if (codexWarnings.length > 0) void broadcast();
3833
5277
  });
5278
+ async function startRequestedBrief() {
5279
+ if (!agent.enabled || !await firstBriefPending(config) || !await onboarding.takeBriefRequest()) return;
5280
+ await agent.run("hand").catch((err) => {
5281
+ console.error(`[daily-focus] could not start the first brief: ${err.message}`);
5282
+ });
5283
+ }
5284
+ function saveSettingsValues(values) {
5285
+ const saved = settingsWrites.then(async () => {
5286
+ const sources = readSources();
5287
+ const applied = applySettingsChange(sources, values);
5288
+ if ("error" in applied) return applied.error;
5289
+ const changed = JSON.stringify(Object.entries(applied.settings).sort()) !== JSON.stringify(Object.entries(sources.settings).sort());
5290
+ if (changed) {
5291
+ await saveSettings(sources.settingsPath, applied.settings);
5292
+ requestRestart();
5293
+ }
5294
+ return null;
5295
+ });
5296
+ settingsWrites = saved.catch(() => {
5297
+ });
5298
+ return saved;
5299
+ }
3834
5300
  function requestRestart() {
3835
5301
  restart = options.onRestart ? "waiting" : "manual";
3836
5302
  restartWhenIdle();
3837
5303
  }
3838
5304
  function restartWhenIdle() {
3839
- if (restart !== "waiting" || restarting || agent.busy || assistant.busy) return;
5305
+ if (restart !== "waiting" || restarting || agent.busy || assistant.busy || onboarding.busy) return;
3840
5306
  restarting = true;
3841
5307
  setImmediate(() => options.onRestart?.());
3842
5308
  }
@@ -3848,11 +5314,9 @@ async function startServer(env, options = {}) {
3848
5314
  // A link that can't be read is no reason to withhold the page.
3849
5315
  storeLinkWarning(config).catch(() => null)
3850
5316
  ]);
3851
- const sources = readSources();
3852
- const setup = await setupState(config, {
3853
- profileChosen: Boolean(layerEnv(sources).DAILY_FOCUS_PROFILE?.trim())
3854
- });
5317
+ const setup = await setupState(config);
3855
5318
  const pulls = board.view(actions, now);
5319
+ const onboardingState = await onboarding.view(setup);
3856
5320
  return {
3857
5321
  ...state,
3858
5322
  board: pulls,
@@ -3862,6 +5326,7 @@ async function startServer(env, options = {}) {
3862
5326
  setupWarnings: [...configWarnings, ...codexWarnings, ...linkWarning ? [linkWarning] : []],
3863
5327
  restart,
3864
5328
  setup,
5329
+ onboarding: onboardingState,
3865
5330
  assetVersion,
3866
5331
  storeId
3867
5332
  };
@@ -3907,7 +5372,10 @@ data: ${payload}
3907
5372
  basename(tempPathFor(config.timeZoneFile)),
3908
5373
  // Appended by this process, which broadcasts on its own.
3909
5374
  basename(config.assistantLogFile),
3910
- basename(config.agentLogFile)
5375
+ basename(config.agentLogFile),
5376
+ // Rewritten by the setup screen, which broadcasts on its own.
5377
+ basename(config.onboardingFile),
5378
+ basename(tempPathFor(config.onboardingFile))
3911
5379
  ]
3912
5380
  }
3913
5381
  );
@@ -3920,6 +5388,8 @@ data: ${payload}
3920
5388
  });
3921
5389
  await assistant.start();
3922
5390
  await agent.start();
5391
+ await onboarding.start(await firstBriefPending(config));
5392
+ await startRequestedBrief();
3923
5393
  let ticks = Promise.resolve();
3924
5394
  function clockTick() {
3925
5395
  ticks = ticks.then(tickOnce).catch((err) => {
@@ -4218,6 +5688,40 @@ data: ${JSON.stringify(await buildState())}
4218
5688
  void broadcast(state);
4219
5689
  return;
4220
5690
  }
5691
+ if (path.startsWith("/api/onboarding/") && req.method === "POST") {
5692
+ const body = await readJsonObject(req, res);
5693
+ if (!body) return;
5694
+ const step = typeof body.step === "string" ? body.step : "";
5695
+ let refused2 = null;
5696
+ if (path === "/api/onboarding/answer") {
5697
+ refused2 = await onboarding.answer(step, body.answer);
5698
+ } else if (path === "/api/onboarding/revise") {
5699
+ refused2 = typeof body.text === "string" && body.text.trim() !== "" ? await onboarding.revise(step, body.text, body.draft) : '"text" is required: what to change';
5700
+ } else if (path === "/api/onboarding/retry") {
5701
+ refused2 = await onboarding.retry(step);
5702
+ } else if (path === "/api/onboarding/detect") {
5703
+ await onboarding.detect();
5704
+ } else if (path === "/api/onboarding/finish") {
5705
+ const finished = await onboarding.finish();
5706
+ if ("error" in finished) refused2 = finished.error;
5707
+ else if (!finished.restart) await startRequestedBrief();
5708
+ } else {
5709
+ sendJSON(res, 404, { error: `no such setup action: ${path}` });
5710
+ return;
5711
+ }
5712
+ if (refused2) {
5713
+ sendJSON(res, 409, { error: refused2 });
5714
+ return;
5715
+ }
5716
+ const state = await buildState();
5717
+ sendState(res, state);
5718
+ void broadcast(state);
5719
+ return;
5720
+ }
5721
+ if (path === "/api/onboarding/sources" && req.method === "GET") {
5722
+ sendJSON(res, 200, await onboarding.previewSources());
5723
+ return;
5724
+ }
4221
5725
  if (path === "/api/settings" && req.method === "GET") {
4222
5726
  sendJSON(res, 200, { ...describeSettings(readSources()), restart });
4223
5727
  return;
@@ -4230,20 +5734,7 @@ data: ${JSON.stringify(await buildState())}
4230
5734
  sendJSON(res, 400, { error: '"values" must be an object of settings' });
4231
5735
  return;
4232
5736
  }
4233
- const saved = settingsWrites.then(async () => {
4234
- const sources = readSources();
4235
- const applied = applySettingsChange(sources, values);
4236
- if ("error" in applied) return applied.error;
4237
- const changed = JSON.stringify(Object.entries(applied.settings).sort()) !== JSON.stringify(Object.entries(sources.settings).sort());
4238
- if (changed) {
4239
- await saveSettings(sources.settingsPath, applied.settings);
4240
- requestRestart();
4241
- }
4242
- return null;
4243
- });
4244
- settingsWrites = saved.catch(() => {
4245
- });
4246
- const refused2 = await saved;
5737
+ const refused2 = await saveSettingsValues(values);
4247
5738
  if (refused2) {
4248
5739
  sendJSON(res, 400, { error: refused2 });
4249
5740
  return;
@@ -4283,8 +5774,8 @@ data: ${JSON.stringify(await buildState())}
4283
5774
  if (path === "/api/focus/objective" && req.method === "POST") {
4284
5775
  const body = await readJsonObject(req, res);
4285
5776
  if (!body) return;
4286
- const { objective, blocker } = body;
4287
- for (const [name, value] of [["objective", objective], ["blocker", blocker]]) {
5777
+ const { objective: objective2, blocker } = body;
5778
+ for (const [name, value] of [["objective", objective2], ["blocker", blocker]]) {
4288
5779
  if (value !== void 0 && value !== null && typeof value !== "string") {
4289
5780
  sendJSON(res, 400, { error: `"${name}" must be text, or null to clear it` });
4290
5781
  return;
@@ -4293,7 +5784,7 @@ data: ${JSON.stringify(await buildState())}
4293
5784
  const current = await readEditable(config.focusFile);
4294
5785
  const next = setFocusFields(
4295
5786
  current.text,
4296
- { objective: objective ?? null, blocker: blocker ?? null },
5787
+ { objective: objective2 ?? null, blocker: blocker ?? null },
4297
5788
  FOCUS_TEMPLATE
4298
5789
  );
4299
5790
  const result = await saveEditable(config.focusFile, next, current.version);
@@ -4310,10 +5801,10 @@ data: ${JSON.stringify(await buildState())}
4310
5801
  try {
4311
5802
  const facts = await runHelper(config.calendar.appPath, []);
4312
5803
  const seen = /* @__PURE__ */ new Set();
4313
- const calendars = facts.calendars.filter((entry) => !seen.has(`${entry.title}
5804
+ const calendars2 = facts.calendars.filter((entry) => !seen.has(`${entry.title}
4314
5805
  ${entry.source}`) && seen.add(`${entry.title}
4315
5806
  ${entry.source}`)).map(({ title, source }) => ({ title, source })).sort((a, b) => a.title.localeCompare(b.title));
4316
- sendJSON(res, 200, { calendars });
5807
+ sendJSON(res, 200, { calendars: calendars2 });
4317
5808
  } catch (err) {
4318
5809
  sendJSON(res, 409, { error: err.message });
4319
5810
  }
@@ -4352,6 +5843,7 @@ ${entry.source}`)).map(({ title, source }) => ({ title, source })).sort((a, b) =
4352
5843
  tickets.stop();
4353
5844
  await assistant.close();
4354
5845
  await agent.close();
5846
+ await onboarding.close();
4355
5847
  stopWatching();
4356
5848
  stopWatchingAssets();
4357
5849
  for (const res of subscribers) res.end();