daily-focus 1.2.0-dev.10 → 1.2.0-dev.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/SETUP.md +61 -18
- package/dist/{audit-7ZCLKF46.js → audit-JVDLHD3B.js} +3 -3
- package/dist/{chunk-RZNQJ6UG.js → chunk-D3JAWEF7.js} +1 -1
- package/dist/{chunk-J7ZYVUG6.js → chunk-DPMHC72S.js} +8 -2
- package/dist/{chunk-ODJGQCM3.js → chunk-FPBY3TAW.js} +22 -5
- package/dist/{chunk-Z4SFMMRQ.js → chunk-HEP46XRX.js} +2 -2
- package/dist/{chunk-64XB7UDN.js → chunk-TUZHZ3NP.js} +1 -1
- package/dist/cli.js +6 -6
- package/dist/{demo-BYLLMKSQ.js → demo-UKILSZYF.js} +4 -4
- package/dist/{init-EMZDG2I7.js → init-BZSCAS3L.js} +1 -1
- package/dist/{seed-JU4LQ33D.js → seed-WQEXDK2L.js} +3 -3
- package/dist/{server-WQYUOKB3.js → server-RKEB6FDK.js} +1723 -209
- package/dist/{service-PUJ3YDH5.js → service-RRHZ7BJN.js} +1 -1
- package/package.json +1 -1
- package/prompts/README.md +13 -4
- package/prompts/morning-brief-personal.md +10 -0
- package/prompts/morning-brief-work.md +10 -0
- package/prompts/onboarding.md +41 -0
- package/public/app.js +611 -273
- package/public/style.css +573 -75
|
@@ -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-
|
|
12
|
+
} from "./chunk-FPBY3TAW.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-
|
|
20
|
+
} from "./chunk-HEP46XRX.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-
|
|
33
|
+
} from "./chunk-D3JAWEF7.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-
|
|
49
|
+
} from "./chunk-DPMHC72S.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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
(
|
|
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
|
|
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
|
|
1351
|
-
import { readFile as
|
|
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
|
|
1355
|
-
var
|
|
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
|
|
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)
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1893
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
1650
1894
|
|
|
1651
1895
|
// src/jira.ts
|
|
1652
|
-
import { execFile as
|
|
1653
|
-
import { promisify as
|
|
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
|
|
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
|
|
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(
|
|
1918
|
-
if (typeof
|
|
1919
|
-
const marker =
|
|
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 =
|
|
2165
|
+
const opens = field2.indexOf("{", marker);
|
|
1922
2166
|
if (opens === -1) return null;
|
|
1923
|
-
const body = jsonObjectAt(
|
|
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
|
|
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 ${
|
|
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 ${
|
|
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((
|
|
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
|
|
2596
|
+
let identity3;
|
|
2353
2597
|
try {
|
|
2354
|
-
|
|
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 ??
|
|
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:
|
|
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
|
|
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
|
|
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;
|
|
@@ -2505,6 +2749,11 @@ function agentPrompt(promptPath) {
|
|
|
2505
2749
|
"Finish by reporting back as that file asks you to."
|
|
2506
2750
|
].join("\n");
|
|
2507
2751
|
}
|
|
2752
|
+
function agentSettings(config) {
|
|
2753
|
+
return {
|
|
2754
|
+
searchTrash: config.agent.searchTrash
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2508
2757
|
function followUpPrompt(question) {
|
|
2509
2758
|
return [
|
|
2510
2759
|
"A follow-up question about the brief you wrote in this session. Answer it here.",
|
|
@@ -2515,7 +2764,7 @@ function followUpPrompt(question) {
|
|
|
2515
2764
|
question.trim()
|
|
2516
2765
|
].join("\n");
|
|
2517
2766
|
}
|
|
2518
|
-
function displayPath(path, home =
|
|
2767
|
+
function displayPath(path, home = homedir2()) {
|
|
2519
2768
|
return path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
|
|
2520
2769
|
}
|
|
2521
2770
|
function foldAgentLog(text) {
|
|
@@ -2622,7 +2871,7 @@ var AgentRunner = class {
|
|
|
2622
2871
|
this.loaded = true;
|
|
2623
2872
|
let text = "";
|
|
2624
2873
|
try {
|
|
2625
|
-
text = await
|
|
2874
|
+
text = await readFile7(this.config.agentLogFile, "utf8");
|
|
2626
2875
|
} catch {
|
|
2627
2876
|
return;
|
|
2628
2877
|
}
|
|
@@ -2700,8 +2949,8 @@ var AgentRunner = class {
|
|
|
2700
2949
|
* decided on.
|
|
2701
2950
|
*/
|
|
2702
2951
|
async run(trigger = "hand", now = /* @__PURE__ */ new Date()) {
|
|
2703
|
-
const
|
|
2704
|
-
if (!
|
|
2952
|
+
const cli2 = this.config.agent.cli;
|
|
2953
|
+
if (!cli2) throw new Error("running the morning agent is off; set DAILY_FOCUS_AGENT to codex or claude");
|
|
2705
2954
|
if (this.running) {
|
|
2706
2955
|
throw new Error(
|
|
2707
2956
|
this.running.turn ? "the morning agent is still answering a question" : "the morning agent is already running"
|
|
@@ -2710,7 +2959,7 @@ var AgentRunner = class {
|
|
|
2710
2959
|
await this.start();
|
|
2711
2960
|
const run5 = {
|
|
2712
2961
|
id: randomUUID(),
|
|
2713
|
-
cli,
|
|
2962
|
+
cli: cli2,
|
|
2714
2963
|
trigger,
|
|
2715
2964
|
sessionId: null,
|
|
2716
2965
|
startedAt: now.toISOString(),
|
|
@@ -2723,8 +2972,14 @@ var AgentRunner = class {
|
|
|
2723
2972
|
resumable: false
|
|
2724
2973
|
};
|
|
2725
2974
|
this.runs.push(run5);
|
|
2726
|
-
await this.append({ event: "started", run: run5.id, cli, trigger, at: run5.startedAt });
|
|
2727
|
-
|
|
2975
|
+
await this.append({ event: "started", run: run5.id, cli: cli2, trigger, at: run5.startedAt });
|
|
2976
|
+
try {
|
|
2977
|
+
await writeJsonAtomic(this.config.agentSettingsFile, agentSettings(this.config));
|
|
2978
|
+
} catch (err) {
|
|
2979
|
+
await this.finish(run5, null, { ok: false, error: `could not write ${displayPath(this.config.agentSettingsFile)}: ${err.message}` });
|
|
2980
|
+
return run5;
|
|
2981
|
+
}
|
|
2982
|
+
const adapter = ADAPTERS[cli2];
|
|
2728
2983
|
const args = adapter.args({
|
|
2729
2984
|
prompt: agentPrompt(displayPath(this.config.promptFile)),
|
|
2730
2985
|
sessionId: null,
|
|
@@ -2743,8 +2998,8 @@ var AgentRunner = class {
|
|
|
2743
2998
|
* is running; the answer arrives through `onChange` and the next state.
|
|
2744
2999
|
*/
|
|
2745
3000
|
async ask(runId, question) {
|
|
2746
|
-
const
|
|
2747
|
-
if (!
|
|
3001
|
+
const cli2 = this.config.agent.cli;
|
|
3002
|
+
if (!cli2) throw new Error("running the morning agent is off; set DAILY_FOCUS_AGENT to codex or claude");
|
|
2748
3003
|
await this.start();
|
|
2749
3004
|
const run5 = this.runs.find((candidate) => candidate.id === runId);
|
|
2750
3005
|
if (!run5) throw new Error("no such run");
|
|
@@ -2755,7 +3010,7 @@ var AgentRunner = class {
|
|
|
2755
3010
|
}
|
|
2756
3011
|
if (!this.resumable(run5)) {
|
|
2757
3012
|
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 ${
|
|
3013
|
+
run5.sessionId === null ? "this run left no session to continue" : `this run was made by ${run5.cli}, and the agent is ${cli2} now`
|
|
2759
3014
|
);
|
|
2760
3015
|
}
|
|
2761
3016
|
const text = question.trim();
|
|
@@ -2771,7 +3026,7 @@ var AgentRunner = class {
|
|
|
2771
3026
|
};
|
|
2772
3027
|
run5.turns.push(turn);
|
|
2773
3028
|
await this.append({ event: "asked", run: run5.id, turn: turn.id, question: text, at: turn.startedAt });
|
|
2774
|
-
const adapter = ADAPTERS[
|
|
3029
|
+
const adapter = ADAPTERS[cli2];
|
|
2775
3030
|
const args = adapter.args({
|
|
2776
3031
|
prompt: followUpPrompt(text),
|
|
2777
3032
|
sessionId: run5.sessionId,
|
|
@@ -2792,10 +3047,10 @@ var AgentRunner = class {
|
|
|
2792
3047
|
* session under the directory it was started in, and looks for it there.
|
|
2793
3048
|
*/
|
|
2794
3049
|
async spawnInto(run5, turn, args, timeoutMs, timeoutReason) {
|
|
2795
|
-
const
|
|
2796
|
-
const adapter = ADAPTERS[
|
|
3050
|
+
const cli2 = run5.cli;
|
|
3051
|
+
const adapter = ADAPTERS[cli2];
|
|
2797
3052
|
await mkdir(this.config.dataDir, { recursive: true });
|
|
2798
|
-
const refusal2 =
|
|
3053
|
+
const refusal2 = cli2 === "codex" ? await this.codexGate.refusal() : null;
|
|
2799
3054
|
if (refusal2) {
|
|
2800
3055
|
await this.finish(run5, turn, { ok: false, error: refusal2 });
|
|
2801
3056
|
return;
|
|
@@ -2851,7 +3106,7 @@ ${event.text}` : event.text;
|
|
|
2851
3106
|
this.running = null;
|
|
2852
3107
|
const outcome = running.abortReason ? { ok: false, error: running.abortReason } : done ?? {
|
|
2853
3108
|
ok: false,
|
|
2854
|
-
error: `${
|
|
3109
|
+
error: `${cli2} exited with code ${code}${stderr.length ? `: ${stderr.join("").trim().split("\n").slice(-3).join(" ")}` : ""}`
|
|
2855
3110
|
};
|
|
2856
3111
|
void this.finish(run5, turn, outcome, running.abortReason !== null);
|
|
2857
3112
|
});
|
|
@@ -2954,23 +3209,23 @@ function watchDataDir(dir, onChange, { debounceMs = 120, ignore = [] } = {}) {
|
|
|
2954
3209
|
}
|
|
2955
3210
|
|
|
2956
3211
|
// src/assets.ts
|
|
2957
|
-
import { readdir, stat as
|
|
2958
|
-
import { join as
|
|
3212
|
+
import { readdir as readdir2, stat as stat3 } from "node:fs/promises";
|
|
3213
|
+
import { join as join4 } from "node:path";
|
|
2959
3214
|
async function computeAssetVersion(dir) {
|
|
2960
3215
|
const parts = [];
|
|
2961
3216
|
async function walk(current) {
|
|
2962
3217
|
let entries;
|
|
2963
3218
|
try {
|
|
2964
|
-
entries = await
|
|
3219
|
+
entries = await readdir2(current, { withFileTypes: true });
|
|
2965
3220
|
} catch {
|
|
2966
3221
|
return;
|
|
2967
3222
|
}
|
|
2968
3223
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2969
|
-
const path =
|
|
3224
|
+
const path = join4(current, entry.name);
|
|
2970
3225
|
if (entry.isDirectory()) {
|
|
2971
3226
|
await walk(path);
|
|
2972
3227
|
} else {
|
|
2973
|
-
const info = await
|
|
3228
|
+
const info = await stat3(path);
|
|
2974
3229
|
parts.push(`${entry.name}:${info.size}:${info.mtimeMs}`);
|
|
2975
3230
|
}
|
|
2976
3231
|
}
|
|
@@ -2989,8 +3244,8 @@ var FAVICONS = {
|
|
|
2989
3244
|
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
3245
|
personal: `<path fill="#1baf7a" fill-rule="evenodd" d="M50 8 96 48H84v44H16V48H4ZM41 92V66h18v26Z"/>`
|
|
2991
3246
|
};
|
|
2992
|
-
function faviconSvg(
|
|
2993
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">${FAVICONS[
|
|
3247
|
+
function faviconSvg(profile2) {
|
|
3248
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">${FAVICONS[profile2]}</svg>`;
|
|
2994
3249
|
}
|
|
2995
3250
|
|
|
2996
3251
|
// src/guard.ts
|
|
@@ -3022,6 +3277,542 @@ function refusal(req, listenHost) {
|
|
|
3022
3277
|
return null;
|
|
3023
3278
|
}
|
|
3024
3279
|
|
|
3280
|
+
// src/onboarding.ts
|
|
3281
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
3282
|
+
import { mkdir as mkdir2, mkdtemp, readFile as readFile8, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
3283
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
3284
|
+
import { join as join5 } from "node:path";
|
|
3285
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
3286
|
+
|
|
3287
|
+
// src/questions.ts
|
|
3288
|
+
var INSTALL = {
|
|
3289
|
+
codex: { label: "Install Codex", url: "https://developers.openai.com/codex/cli" },
|
|
3290
|
+
claude: { label: "Install Claude Code", url: "https://code.claude.com/docs" }
|
|
3291
|
+
};
|
|
3292
|
+
function option(value, label, detail = null, group = null) {
|
|
3293
|
+
return { value, label, detail, group };
|
|
3294
|
+
}
|
|
3295
|
+
function choice(id, options, selected, extra = {}) {
|
|
3296
|
+
return { id, label: null, multiple: false, options, selected, custom: null, ...extra };
|
|
3297
|
+
}
|
|
3298
|
+
function field(id, label, value, extra = {}) {
|
|
3299
|
+
return { id, label, value, placeholder: null, help: null, ...extra };
|
|
3300
|
+
}
|
|
3301
|
+
function card(parts) {
|
|
3302
|
+
return { why: null, choices: [], fields: [], problem: null, ...parts };
|
|
3303
|
+
}
|
|
3304
|
+
function one(answer, id) {
|
|
3305
|
+
return answer.choices[id]?.[0] ?? null;
|
|
3306
|
+
}
|
|
3307
|
+
function str3(value) {
|
|
3308
|
+
return typeof value === "string" ? value.trim() : "";
|
|
3309
|
+
}
|
|
3310
|
+
function proposalOf(ctx, id) {
|
|
3311
|
+
const proposal = ctx.proposals[id];
|
|
3312
|
+
return typeof proposal === "object" && proposal !== null && !Array.isArray(proposal) ? proposal : null;
|
|
3313
|
+
}
|
|
3314
|
+
function listed(names) {
|
|
3315
|
+
if (names.length <= 1) return names.join("");
|
|
3316
|
+
return `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
|
|
3317
|
+
}
|
|
3318
|
+
function ticked(name) {
|
|
3319
|
+
return `\`${name.replaceAll("`", "'")}\``;
|
|
3320
|
+
}
|
|
3321
|
+
function binFor(ctx, cli2) {
|
|
3322
|
+
const found = cli2 ? ctx.clis?.[cli2] : null;
|
|
3323
|
+
return found?.path && !found.onPath ? found.path : null;
|
|
3324
|
+
}
|
|
3325
|
+
var cli = {
|
|
3326
|
+
id: "cli",
|
|
3327
|
+
title: () => "Which agent should write your brief?",
|
|
3328
|
+
topic: "your coding agent",
|
|
3329
|
+
keys: ["DAILY_FOCUS_AGENT"],
|
|
3330
|
+
later: { label: "Decide later", summary: "No agent yet: the boards work, and there is no brief until you choose one." },
|
|
3331
|
+
card(ctx) {
|
|
3332
|
+
if (!ctx.clis) return card({});
|
|
3333
|
+
const report = ctx.clis;
|
|
3334
|
+
const ready = CLI_PREFERENCE.filter((name) => report[name].ready);
|
|
3335
|
+
if (ready.length === 0) {
|
|
3336
|
+
const lines = CLI_PREFERENCE.map((name) => `${report[name].problem ?? `${CLI_LABEL[name]} can't be used`}.`);
|
|
3337
|
+
const fix = CLI_PREFERENCE.map((name) => report[name].fix).find((command3) => command3) ?? null;
|
|
3338
|
+
return card({
|
|
3339
|
+
problem: {
|
|
3340
|
+
message: `${lines.join(" ")} ${fix ? "Run this in a terminal, then check again." : "Install one, log in once in a terminal, then check again."}`,
|
|
3341
|
+
command: fix,
|
|
3342
|
+
blocking: true,
|
|
3343
|
+
links: CLI_PREFERENCE.filter((name) => !report[name].path).map((name) => INSTALL[name])
|
|
3344
|
+
},
|
|
3345
|
+
fields: [
|
|
3346
|
+
field("path", "Installed somewhere setup didn\u2019t look? Its path", "", {
|
|
3347
|
+
placeholder: "/opt/homebrew/bin/codex",
|
|
3348
|
+
help: "The program itself, codex or claude. Setup checks it before using it."
|
|
3349
|
+
})
|
|
3350
|
+
]
|
|
3351
|
+
});
|
|
3352
|
+
}
|
|
3353
|
+
const set = ctx.current.DAILY_FOCUS_AGENT;
|
|
3354
|
+
const others = CLI_PREFERENCE.filter((name) => !report[name].ready && report[name].path).map((name) => `${report[name].problem}.`);
|
|
3355
|
+
return card({
|
|
3356
|
+
why: [
|
|
3357
|
+
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}.`,
|
|
3358
|
+
...others
|
|
3359
|
+
].join(" "),
|
|
3360
|
+
choices: [
|
|
3361
|
+
choice(
|
|
3362
|
+
"cli",
|
|
3363
|
+
ready.map((name) => option(name, CLI_LABEL[name], [report[name].version, report[name].path].filter(Boolean).join(", at "))),
|
|
3364
|
+
[set && ready.includes(set) ? set : ready[0]]
|
|
3365
|
+
)
|
|
3366
|
+
]
|
|
3367
|
+
});
|
|
3368
|
+
},
|
|
3369
|
+
summary: (answer) => {
|
|
3370
|
+
const chosen = CLI_LABEL[one(answer, "cli")];
|
|
3371
|
+
return chosen ? `${chosen} writes your brief.` : "No agent writes your brief yet.";
|
|
3372
|
+
},
|
|
3373
|
+
// The assistant comes with it, on the same CLI, unless it has been set: it
|
|
3374
|
+
// only reads and drafts, and it is what the Ask button on every row needs.
|
|
3375
|
+
settings: (answer, ctx) => {
|
|
3376
|
+
const chosen = one(answer, "cli");
|
|
3377
|
+
const bin = binFor(ctx, chosen);
|
|
3378
|
+
const assistant = chosen && !ctx.current.DAILY_FOCUS_ASSISTANT;
|
|
3379
|
+
return {
|
|
3380
|
+
DAILY_FOCUS_AGENT: chosen,
|
|
3381
|
+
...bin ? { DAILY_FOCUS_AGENT_BIN: bin } : {},
|
|
3382
|
+
...assistant ? { DAILY_FOCUS_ASSISTANT: chosen, ...bin ? { DAILY_FOCUS_ASSISTANT_BIN: bin } : {} } : {}
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
3385
|
+
};
|
|
3386
|
+
var profile = {
|
|
3387
|
+
id: "profile",
|
|
3388
|
+
topic: "what it covers",
|
|
3389
|
+
title: () => "What should your brief cover?",
|
|
3390
|
+
keys: ["DAILY_FOCUS_PROFILE"],
|
|
3391
|
+
later: null,
|
|
3392
|
+
card: (ctx) => card({
|
|
3393
|
+
choices: [
|
|
3394
|
+
choice(
|
|
3395
|
+
"profile",
|
|
3396
|
+
[
|
|
3397
|
+
option("work", "Your work", "Work mail and calendars, pull requests, tickets and meeting notes."),
|
|
3398
|
+
option("personal", "Your personal life", "Personal mail, family calendars, reminders and messages.")
|
|
3399
|
+
],
|
|
3400
|
+
[ctx.current.DAILY_FOCUS_PROFILE === "personal" ? "personal" : "work"]
|
|
3401
|
+
)
|
|
3402
|
+
],
|
|
3403
|
+
why: "One dashboard briefs one of them. For both, run a second dashboard with its own store."
|
|
3404
|
+
}),
|
|
3405
|
+
summary: (answer) => one(answer, "profile") === "personal" ? "It briefs your personal life." : "It briefs your work.",
|
|
3406
|
+
settings: (answer) => ({ DAILY_FOCUS_PROFILE: one(answer, "profile") })
|
|
3407
|
+
};
|
|
3408
|
+
var TIMES = ["06:00", "06:30", "07:00", "07:30", "08:00"];
|
|
3409
|
+
var DAYS = { "1-5": "Weekdays", "0-6": "Every day" };
|
|
3410
|
+
var schedule = {
|
|
3411
|
+
id: "schedule",
|
|
3412
|
+
topic: "when it runs",
|
|
3413
|
+
title: () => "When should your brief be ready?",
|
|
3414
|
+
applies: (ctx) => ctx.cli !== null,
|
|
3415
|
+
keys: ["DAILY_FOCUS_AGENT_AT", "DAILY_FOCUS_AGENT_DAYS"],
|
|
3416
|
+
later: null,
|
|
3417
|
+
card(ctx) {
|
|
3418
|
+
const at = ctx.current.DAILY_FOCUS_AGENT_AT || "07:00";
|
|
3419
|
+
const days = ctx.current.DAILY_FOCUS_AGENT_DAYS || "1-5";
|
|
3420
|
+
const times = [.../* @__PURE__ */ new Set([...TIMES, ...at === "off" ? [] : [at]])].sort();
|
|
3421
|
+
return card({
|
|
3422
|
+
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.`,
|
|
3423
|
+
choices: [
|
|
3424
|
+
choice("at", [...times.map((time) => option(time, time)), option("off", "Only when I ask")], [at], {
|
|
3425
|
+
label: "Time",
|
|
3426
|
+
custom: "Another time, such as 05:45"
|
|
3427
|
+
}),
|
|
3428
|
+
choice("days", Object.entries({ ...DAYS, ...DAYS[days] ? {} : { [days]: days } }).map(([value, label]) => option(value, label)), [days], {
|
|
3429
|
+
label: "Days"
|
|
3430
|
+
})
|
|
3431
|
+
]
|
|
3432
|
+
});
|
|
3433
|
+
},
|
|
3434
|
+
summary(answer) {
|
|
3435
|
+
const at = one(answer, "at");
|
|
3436
|
+
if (!at || at === "off") return "It runs only when you ask.";
|
|
3437
|
+
const days = one(answer, "days") ?? "1-5";
|
|
3438
|
+
return `It runs at ${at} ${days === "1-5" ? "on weekdays" : days === "0-6" ? "every day" : `on days ${days}`}.`;
|
|
3439
|
+
},
|
|
3440
|
+
settings: (answer) => ({ DAILY_FOCUS_AGENT_AT: one(answer, "at"), DAILY_FOCUS_AGENT_DAYS: one(answer, "days") })
|
|
3441
|
+
};
|
|
3442
|
+
function proposedAccounts(proposal) {
|
|
3443
|
+
const raw = Array.isArray(proposal?.githubAccounts) ? proposal.githubAccounts : [];
|
|
3444
|
+
return raw.flatMap((entry) => {
|
|
3445
|
+
const account = typeof entry === "object" && entry !== null ? entry : {};
|
|
3446
|
+
const login = str3(account.login);
|
|
3447
|
+
return login ? [{ login, note: str3(account.note) }] : [];
|
|
3448
|
+
});
|
|
3449
|
+
}
|
|
3450
|
+
function githubAccounts(ctx) {
|
|
3451
|
+
const accounts = /* @__PURE__ */ new Map();
|
|
3452
|
+
for (const login of ctx.identity.logins) accounts.set(login.toLowerCase(), { login, note: "" });
|
|
3453
|
+
for (const { login, note } of proposedAccounts(proposalOf(ctx, "identity"))) {
|
|
3454
|
+
accounts.set(login.toLowerCase(), { login: accounts.get(login.toLowerCase())?.login ?? login, note });
|
|
3455
|
+
}
|
|
3456
|
+
return [...accounts.values()];
|
|
3457
|
+
}
|
|
3458
|
+
var identity2 = {
|
|
3459
|
+
id: "identity",
|
|
3460
|
+
topic: "who you are",
|
|
3461
|
+
title: () => "Is this you?",
|
|
3462
|
+
keys: [],
|
|
3463
|
+
// Clearing three fields and unticking the accounts is a chore; one button, named for what it does, isn't.
|
|
3464
|
+
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." },
|
|
3465
|
+
card(ctx) {
|
|
3466
|
+
const proposal = proposalOf(ctx, "identity");
|
|
3467
|
+
const names = Array.isArray(proposal?.names) ? proposal.names.map(str3).filter(Boolean) : [];
|
|
3468
|
+
const name = names.length ? names.join(", ") : ctx.identity.name ?? "";
|
|
3469
|
+
const email = str3(proposal?.email) || (ctx.identity.email ?? "");
|
|
3470
|
+
const accounts = githubAccounts(ctx);
|
|
3471
|
+
return card({
|
|
3472
|
+
why: str3(proposal?.why) || "The brief tells your to-dos from everyone else\u2019s by these.",
|
|
3473
|
+
fields: [
|
|
3474
|
+
field("name", "Your name, as meeting notes write it", name, {
|
|
3475
|
+
placeholder: "Robin Lark",
|
|
3476
|
+
help: "Add short forms after a comma, such as Robin L."
|
|
3477
|
+
}),
|
|
3478
|
+
field("email", ctx.profile === "personal" ? "Your email" : "Your work email", email, { placeholder: "robin@example.com" })
|
|
3479
|
+
],
|
|
3480
|
+
choices: [
|
|
3481
|
+
choice(
|
|
3482
|
+
"github",
|
|
3483
|
+
accounts.map((account) => option(account.login, account.login, account.note || null)),
|
|
3484
|
+
accounts.map((account) => account.login),
|
|
3485
|
+
{ label: "Your GitHub accounts", multiple: true, custom: "Add a GitHub login" }
|
|
3486
|
+
)
|
|
3487
|
+
]
|
|
3488
|
+
});
|
|
3489
|
+
},
|
|
3490
|
+
summary(answer) {
|
|
3491
|
+
const name = answer.fields.name?.split(",")[0]?.trim();
|
|
3492
|
+
const logins = answer.choices.github ?? [];
|
|
3493
|
+
const rest = [answer.fields.email?.trim() ?? "", logins.length ? `${listed(logins)} on GitHub` : ""].filter(Boolean);
|
|
3494
|
+
if (!name) return rest.length ? `You're ${listed(rest)}.` : "No details about you.";
|
|
3495
|
+
return `You're ${name}${rest.length ? `, ${rest.join(", ")}` : ""}.`;
|
|
3496
|
+
},
|
|
3497
|
+
settings(answer, ctx) {
|
|
3498
|
+
const values = {};
|
|
3499
|
+
if (answer.fields.email?.trim()) values.DAILY_FOCUS_CALENDAR_ADDRESSES = answer.fields.email.trim();
|
|
3500
|
+
const logins = answer.choices.github ?? [];
|
|
3501
|
+
if (logins.length > 1 || logins.length === 1 && logins[0] !== ctx.identity.logins[0]) {
|
|
3502
|
+
values.DAILY_FOCUS_GITHUB_ACCOUNTS = logins.join(", ");
|
|
3503
|
+
}
|
|
3504
|
+
return values;
|
|
3505
|
+
},
|
|
3506
|
+
sources(answer, ctx) {
|
|
3507
|
+
const names = (answer.fields.name ?? "").split(",").map((n) => n.trim()).filter(Boolean);
|
|
3508
|
+
const email = answer.fields.email?.trim();
|
|
3509
|
+
const notes = new Map(githubAccounts(ctx).map((account) => [account.login, account.note]));
|
|
3510
|
+
const logins = answer.choices.github ?? [];
|
|
3511
|
+
const lines = [
|
|
3512
|
+
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,
|
|
3513
|
+
email ? `- ${ctx.profile === "personal" ? "Personal" : "Work"} email: ${ticked(email)}` : null,
|
|
3514
|
+
logins.length ? [
|
|
3515
|
+
`- GitHub ${logins.length > 1 ? "logins" : "login"}, which review requests and authorship are judged against:`,
|
|
3516
|
+
...logins.map((login) => ` - ${ticked(login)}${notes.get(login) ? ` \u2014 ${notes.get(login)}` : ""}`)
|
|
3517
|
+
].join("\n") : null
|
|
3518
|
+
].filter((line) => line !== null);
|
|
3519
|
+
return lines.length ? [{ heading: "Identity", body: lines.join("\n") }] : [];
|
|
3520
|
+
},
|
|
3521
|
+
agent: {
|
|
3522
|
+
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`.",
|
|
3523
|
+
facts: (ctx) => [
|
|
3524
|
+
ctx.identity.name ? `- gh says their name is ${ctx.identity.name}` : null,
|
|
3525
|
+
ctx.identity.logins.length ? `- gh is logged in to GitHub as ${listed(ctx.identity.logins)}; the first is active` : null,
|
|
3526
|
+
ctx.identity.email ? `- git's global email is ${ctx.identity.email}` : null
|
|
3527
|
+
].filter(Boolean).join("\n") || null,
|
|
3528
|
+
schema: {
|
|
3529
|
+
type: "object",
|
|
3530
|
+
properties: {
|
|
3531
|
+
why: { type: "string", description: "One or two sentences on where these came from." },
|
|
3532
|
+
names: { type: "array", items: { type: "string" }, description: "Full name first, then short forms." },
|
|
3533
|
+
email: { type: "string" },
|
|
3534
|
+
githubAccounts: {
|
|
3535
|
+
type: "array",
|
|
3536
|
+
items: {
|
|
3537
|
+
type: "object",
|
|
3538
|
+
properties: {
|
|
3539
|
+
login: { type: "string" },
|
|
3540
|
+
note: { type: "string", description: "What the account is for, in a few words, or empty." }
|
|
3541
|
+
},
|
|
3542
|
+
required: ["login", "note"],
|
|
3543
|
+
additionalProperties: false
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
},
|
|
3547
|
+
required: ["why", "names", "email", "githubAccounts"],
|
|
3548
|
+
additionalProperties: false
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
};
|
|
3552
|
+
function healthText(board, via) {
|
|
3553
|
+
if (board.reason) return board.reason;
|
|
3554
|
+
if (board.fetchedAt) return `Connected through ${via}.`;
|
|
3555
|
+
if (board.enabled) return `Checking ${via}\u2026`;
|
|
3556
|
+
return null;
|
|
3557
|
+
}
|
|
3558
|
+
var boards = {
|
|
3559
|
+
id: "boards",
|
|
3560
|
+
topic: "the boards",
|
|
3561
|
+
title: () => "What else should the dashboard show?",
|
|
3562
|
+
keys: ["DAILY_FOCUS_GITHUB", "DAILY_FOCUS_JIRA"],
|
|
3563
|
+
later: null,
|
|
3564
|
+
card(ctx) {
|
|
3565
|
+
const { github, jira } = ctx.boards;
|
|
3566
|
+
const selected = [
|
|
3567
|
+
github.enabled && !github.reason ? "github" : null,
|
|
3568
|
+
jira.enabled && !jira.reason && ctx.profile === "work" ? "jira" : null
|
|
3569
|
+
].filter((value) => value !== null);
|
|
3570
|
+
return card({
|
|
3571
|
+
why: "Both are read live, beside the brief, so a merge or a status change shows up within minutes.",
|
|
3572
|
+
choices: [
|
|
3573
|
+
choice(
|
|
3574
|
+
"boards",
|
|
3575
|
+
[
|
|
3576
|
+
option("github", "Your pull requests", healthText(github, "gh") ?? "Through the GitHub CLI, gh."),
|
|
3577
|
+
option("jira", "Your Jira tickets", healthText(jira, "acli") ?? "Through the Atlassian CLI, acli.")
|
|
3578
|
+
],
|
|
3579
|
+
selected,
|
|
3580
|
+
{ multiple: true }
|
|
3581
|
+
)
|
|
3582
|
+
]
|
|
3583
|
+
});
|
|
3584
|
+
},
|
|
3585
|
+
summary(answer) {
|
|
3586
|
+
const on = answer.choices.boards ?? [];
|
|
3587
|
+
const names = [on.includes("github") ? "your pull requests" : null, on.includes("jira") ? "your Jira tickets" : null].filter(
|
|
3588
|
+
(name) => name !== null
|
|
3589
|
+
);
|
|
3590
|
+
return names.length ? `It shows ${listed(names)}.` : "No boards beside the brief.";
|
|
3591
|
+
},
|
|
3592
|
+
settings(answer) {
|
|
3593
|
+
const on = answer.choices.boards ?? [];
|
|
3594
|
+
return { DAILY_FOCUS_GITHUB: on.includes("github") ? "on" : "off", DAILY_FOCUS_JIRA: on.includes("jira") ? "on" : "off" };
|
|
3595
|
+
}
|
|
3596
|
+
};
|
|
3597
|
+
var HOLIDAYS = "Holidays";
|
|
3598
|
+
function calendarEntries(ctx) {
|
|
3599
|
+
const proposal = proposalOf(ctx, "calendars");
|
|
3600
|
+
const verdicts = Array.isArray(proposal?.calendars) ? proposal.calendars.flatMap((raw) => {
|
|
3601
|
+
const entry = typeof raw === "object" && raw !== null ? raw : {};
|
|
3602
|
+
const title = str3(entry.title);
|
|
3603
|
+
return title ? [{ title, account: str3(entry.account), include: entry.include === true, holiday: entry.holiday === true, briefName: str3(entry.briefName), note: str3(entry.note) }] : [];
|
|
3604
|
+
}) : [];
|
|
3605
|
+
const configured = new Set((ctx.current.DAILY_FOCUS_CALENDARS ?? "").split(",").map((name) => name.trim().toLowerCase()).filter(Boolean));
|
|
3606
|
+
const used = /* @__PURE__ */ new Set();
|
|
3607
|
+
const entries = [];
|
|
3608
|
+
for (const calendar of Array.isArray(ctx.calendars) ? ctx.calendars : []) {
|
|
3609
|
+
const key = calendar.title.toLowerCase();
|
|
3610
|
+
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);
|
|
3611
|
+
if (verdict) used.add(verdict);
|
|
3612
|
+
entries.push({
|
|
3613
|
+
value: `live:${calendar.source}:${calendar.title}`,
|
|
3614
|
+
title: calendar.title,
|
|
3615
|
+
group: verdict?.holiday ? HOLIDAYS : calendar.source || "Calendar.app",
|
|
3616
|
+
live: true,
|
|
3617
|
+
holiday: verdict?.holiday ?? false,
|
|
3618
|
+
briefName: verdict?.briefName ?? "",
|
|
3619
|
+
note: verdict?.note ?? "",
|
|
3620
|
+
include: verdict ? verdict.include : configured.has(key)
|
|
3621
|
+
});
|
|
3622
|
+
}
|
|
3623
|
+
for (const verdict of verdicts) {
|
|
3624
|
+
if (used.has(verdict)) continue;
|
|
3625
|
+
entries.push({
|
|
3626
|
+
value: `brief:${verdict.account}:${verdict.title}`,
|
|
3627
|
+
title: verdict.title,
|
|
3628
|
+
group: verdict.holiday ? HOLIDAYS : verdict.account || "Your calendar account",
|
|
3629
|
+
live: false,
|
|
3630
|
+
holiday: verdict.holiday,
|
|
3631
|
+
briefName: verdict.briefName,
|
|
3632
|
+
note: verdict.note,
|
|
3633
|
+
include: verdict.include
|
|
3634
|
+
});
|
|
3635
|
+
}
|
|
3636
|
+
return entries.sort((a, b) => Number(a.group === HOLIDAYS) - Number(b.group === HOLIDAYS));
|
|
3637
|
+
}
|
|
3638
|
+
function chosenCalendars(answer, ctx) {
|
|
3639
|
+
const all = new Map(calendarEntries(ctx).map((entry) => [entry.value, entry]));
|
|
3640
|
+
const values = answer.choices.calendars ?? [];
|
|
3641
|
+
return {
|
|
3642
|
+
entries: values.flatMap((value) => all.get(value) ?? []),
|
|
3643
|
+
typed: values.filter((value) => !all.has(value)).map((value) => value.trim()).filter(Boolean)
|
|
3644
|
+
};
|
|
3645
|
+
}
|
|
3646
|
+
var calendars = {
|
|
3647
|
+
id: "calendars",
|
|
3648
|
+
topic: "your calendars",
|
|
3649
|
+
title: (ctx) => ctx.profile === "personal" ? "Which calendars belong in your brief?" : "Which calendars belong in your work brief?",
|
|
3650
|
+
keys: ["DAILY_FOCUS_CALENDARS"],
|
|
3651
|
+
later: null,
|
|
3652
|
+
card(ctx) {
|
|
3653
|
+
const entries = calendarEntries(ctx);
|
|
3654
|
+
const proposal = proposalOf(ctx, "calendars");
|
|
3655
|
+
const problem = ctx.calendar.supported && !ctx.calendar.built ? {
|
|
3656
|
+
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.",
|
|
3657
|
+
command: ctx.calendar.buildCommand,
|
|
3658
|
+
blocking: false
|
|
3659
|
+
} : typeof ctx.calendars === "string" ? { message: `Calendar.app couldn't be read: ${ctx.calendars}`, command: null, blocking: false } : null;
|
|
3660
|
+
return card({
|
|
3661
|
+
why: str3(proposal?.why) || null,
|
|
3662
|
+
problem,
|
|
3663
|
+
choices: [
|
|
3664
|
+
choice(
|
|
3665
|
+
"calendars",
|
|
3666
|
+
entries.map((entry) => option(entry.value, entry.title, entry.note || null, entry.group)),
|
|
3667
|
+
entries.filter((entry) => entry.include).map((entry) => entry.value),
|
|
3668
|
+
{ multiple: true, custom: "Add a calendar by name" }
|
|
3669
|
+
)
|
|
3670
|
+
]
|
|
3671
|
+
});
|
|
3672
|
+
},
|
|
3673
|
+
summary(answer, ctx) {
|
|
3674
|
+
const { entries, typed } = chosenCalendars(answer, ctx);
|
|
3675
|
+
const names = [...entries.map((entry) => entry.title), ...typed];
|
|
3676
|
+
if (names.length === 0) return "No calendars.";
|
|
3677
|
+
if (names.length <= 3) return `It reads ${listed(names)}.`;
|
|
3678
|
+
return `It reads ${names.slice(0, 2).join(", ")} and ${names.length - 2} more calendars.`;
|
|
3679
|
+
},
|
|
3680
|
+
settings(answer, ctx) {
|
|
3681
|
+
const { entries, typed } = chosenCalendars(answer, ctx);
|
|
3682
|
+
const live = [.../* @__PURE__ */ new Set([...entries.filter((entry) => entry.live).map((entry) => entry.title), ...typed])];
|
|
3683
|
+
return { DAILY_FOCUS_CALENDARS: live.filter((name) => !name.includes(",")).join(", ") || null };
|
|
3684
|
+
},
|
|
3685
|
+
sources(answer, ctx) {
|
|
3686
|
+
const { entries, typed } = chosenCalendars(answer, ctx);
|
|
3687
|
+
const line = (entry) => `- ${ticked(entry.briefName || entry.title)}${entry.group !== HOLIDAYS && entry.group ? ` (${entry.group})` : ""}`;
|
|
3688
|
+
const own = [...entries.filter((entry) => !entry.holiday).map(line), ...typed.map((name) => `- ${ticked(name)}`)];
|
|
3689
|
+
const holidays = entries.filter((entry) => entry.holiday).map(line);
|
|
3690
|
+
return [
|
|
3691
|
+
...own.length ? [{ heading: "Calendars", body: `Query these, and treat their events as blocking time:
|
|
3692
|
+
|
|
3693
|
+
${own.join("\n")}` }] : [],
|
|
3694
|
+
...holidays.length ? [{ heading: "Holiday calendars", body: `Call out a holiday today or within the next 14 days:
|
|
3695
|
+
|
|
3696
|
+
${holidays.join("\n")}` }] : []
|
|
3697
|
+
];
|
|
3698
|
+
},
|
|
3699
|
+
agent: {
|
|
3700
|
+
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.',
|
|
3701
|
+
facts(ctx) {
|
|
3702
|
+
if (typeof ctx.calendars === "string") return `Calendar.app could not be read (${ctx.calendars}), so work from your calendar connector alone.`;
|
|
3703
|
+
if (!Array.isArray(ctx.calendars)) return "There is no list from Calendar.app here, so work from your calendar connector alone.";
|
|
3704
|
+
if (ctx.calendars.length === 0) return "Calendar.app has no calendars.";
|
|
3705
|
+
return [
|
|
3706
|
+
"Calendar.app has these, as title (account): events today.",
|
|
3707
|
+
"",
|
|
3708
|
+
...ctx.calendars.map((calendar) => `- ${calendar.title} (${calendar.source || "no account"}): ${calendar.eventsToday}`)
|
|
3709
|
+
].join("\n");
|
|
3710
|
+
},
|
|
3711
|
+
schema: {
|
|
3712
|
+
type: "object",
|
|
3713
|
+
properties: {
|
|
3714
|
+
why: { type: "string", description: "One or two sentences on which you ticked and why." },
|
|
3715
|
+
calendars: {
|
|
3716
|
+
type: "array",
|
|
3717
|
+
items: {
|
|
3718
|
+
type: "object",
|
|
3719
|
+
properties: {
|
|
3720
|
+
title: { type: "string", description: "Exactly as Calendar.app spells it, or the connector's name for one Calendar.app lacks." },
|
|
3721
|
+
account: { type: "string" },
|
|
3722
|
+
include: { type: "boolean" },
|
|
3723
|
+
holiday: { type: "boolean" },
|
|
3724
|
+
briefName: { type: "string" },
|
|
3725
|
+
note: { type: "string" }
|
|
3726
|
+
},
|
|
3727
|
+
required: ["title", "account", "include", "holiday", "briefName", "note"],
|
|
3728
|
+
additionalProperties: false
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
},
|
|
3732
|
+
required: ["why", "calendars"],
|
|
3733
|
+
additionalProperties: false
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
};
|
|
3737
|
+
function suggestedObjectives(proposal) {
|
|
3738
|
+
const raw = Array.isArray(proposal?.suggestions) ? proposal.suggestions : [];
|
|
3739
|
+
return raw.flatMap((entry) => {
|
|
3740
|
+
const suggestion = typeof entry === "object" && entry !== null ? entry : {};
|
|
3741
|
+
const text = str3(suggestion.objective);
|
|
3742
|
+
return text ? [{ text, detail: str3(suggestion.evidence) || null }] : [];
|
|
3743
|
+
});
|
|
3744
|
+
}
|
|
3745
|
+
var objective = {
|
|
3746
|
+
id: "objective",
|
|
3747
|
+
topic: "your objective",
|
|
3748
|
+
title: () => "What are you trying to get done right now?",
|
|
3749
|
+
keys: [],
|
|
3750
|
+
// Left empty, Continue says so; see `empty` on its card.
|
|
3751
|
+
later: null,
|
|
3752
|
+
card(ctx) {
|
|
3753
|
+
const proposal = proposalOf(ctx, "objective");
|
|
3754
|
+
return card({
|
|
3755
|
+
reply: "Tell it what you\u2019re working on, however roughly",
|
|
3756
|
+
empty: "Continue without one",
|
|
3757
|
+
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.",
|
|
3758
|
+
fields: [
|
|
3759
|
+
field("objective", "Your objective", str3(proposal?.objective), {
|
|
3760
|
+
placeholder: "Get the staging deploy working again",
|
|
3761
|
+
suggestions: suggestedObjectives(proposal)
|
|
3762
|
+
}),
|
|
3763
|
+
field("blocker", "What\u2019s in the way, if anything", str3(proposal?.blocker), { placeholder: "Nobody can reproduce the failure" })
|
|
3764
|
+
]
|
|
3765
|
+
});
|
|
3766
|
+
},
|
|
3767
|
+
summary: (answer) => {
|
|
3768
|
+
const text = answer.fields.objective?.trim();
|
|
3769
|
+
const blocker = answer.fields.blocker?.trim();
|
|
3770
|
+
if (!text) return "No objective for now.";
|
|
3771
|
+
return `Your objective: ${text}${blocker ? `. In the way: ${blocker}` : ""}`;
|
|
3772
|
+
},
|
|
3773
|
+
objective: (answer) => answer.fields.objective?.trim() ? { objective: answer.fields.objective.trim(), blocker: answer.fields.blocker?.trim() || null } : null,
|
|
3774
|
+
agent: {
|
|
3775
|
+
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.`,
|
|
3776
|
+
schema: {
|
|
3777
|
+
type: "object",
|
|
3778
|
+
properties: {
|
|
3779
|
+
why: { type: "string", description: "One or two sentences: what you looked at, or how you wrote up what they said." },
|
|
3780
|
+
objective: { type: "string" },
|
|
3781
|
+
blocker: { type: "string" },
|
|
3782
|
+
suggestions: {
|
|
3783
|
+
type: "array",
|
|
3784
|
+
items: {
|
|
3785
|
+
type: "object",
|
|
3786
|
+
properties: {
|
|
3787
|
+
objective: { type: "string" },
|
|
3788
|
+
evidence: { type: "string", description: "What it was drawn from, in a few words." }
|
|
3789
|
+
},
|
|
3790
|
+
required: ["objective", "evidence"],
|
|
3791
|
+
additionalProperties: false
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
},
|
|
3795
|
+
required: ["why", "objective", "blocker", "suggestions"],
|
|
3796
|
+
additionalProperties: false
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
};
|
|
3800
|
+
var QUESTIONS = [cli, profile, schedule, identity2, boards, calendars, objective];
|
|
3801
|
+
function questionById(id) {
|
|
3802
|
+
return QUESTIONS.find((question) => question.id === id);
|
|
3803
|
+
}
|
|
3804
|
+
function composeSources(profile2, sections, today) {
|
|
3805
|
+
const intro = [
|
|
3806
|
+
"# Sources",
|
|
3807
|
+
"",
|
|
3808
|
+
`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.`
|
|
3809
|
+
];
|
|
3810
|
+
return [...intro, ...sections.flatMap((section) => ["", `## ${section.heading}`, "", section.body])].join("\n") + "\n";
|
|
3811
|
+
}
|
|
3812
|
+
function machineName(platform = process.platform) {
|
|
3813
|
+
return platform === "darwin" ? "Mac" : platform === "win32" ? "PC" : "computer";
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3025
3816
|
// src/settings.ts
|
|
3026
3817
|
var SETTING_GROUPS = [
|
|
3027
3818
|
{ id: "instance", title: "This dashboard" },
|
|
@@ -3091,6 +3882,14 @@ var SETTINGS = [
|
|
|
3091
3882
|
choices: EFFORTS,
|
|
3092
3883
|
fallback: always("The CLI's own")
|
|
3093
3884
|
},
|
|
3885
|
+
{
|
|
3886
|
+
key: "DAILY_FOCUS_AGENT_SEARCH_TRASH",
|
|
3887
|
+
group: "agent",
|
|
3888
|
+
label: "Search deleted mail",
|
|
3889
|
+
help: "Lets the agent look in your mail\u2019s Trash as well as the inbox and archive. Off, it leaves out what you deleted.",
|
|
3890
|
+
kind: "flag",
|
|
3891
|
+
fallback: always("Off")
|
|
3892
|
+
},
|
|
3094
3893
|
{
|
|
3095
3894
|
key: "DAILY_FOCUS_AGENT_TOOLS",
|
|
3096
3895
|
group: "agent",
|
|
@@ -3244,7 +4043,7 @@ var SETTINGS = [
|
|
|
3244
4043
|
label: "Jira ticket board",
|
|
3245
4044
|
help: "Flags tickets whose status disagrees with their pull requests, through the Atlassian CLI. Sign in with acli jira auth login.",
|
|
3246
4045
|
kind: "flag",
|
|
3247
|
-
fallback: (
|
|
4046
|
+
fallback: (profile2) => profile2 === "personal" ? "Off" : "On"
|
|
3248
4047
|
},
|
|
3249
4048
|
{
|
|
3250
4049
|
key: "DAILY_FOCUS_JIRA_PROJECTS",
|
|
@@ -3345,7 +4144,7 @@ var SETTINGS = [
|
|
|
3345
4144
|
label: "Show free time",
|
|
3346
4145
|
help: "Find focus windows between meetings in your working hours. Off shows events only.",
|
|
3347
4146
|
kind: "flag",
|
|
3348
|
-
fallback: (
|
|
4147
|
+
fallback: (profile2) => profile2 === "personal" ? "Off" : "On"
|
|
3349
4148
|
},
|
|
3350
4149
|
{
|
|
3351
4150
|
key: "DAILY_FOCUS_WORK_START",
|
|
@@ -3382,7 +4181,7 @@ function profileOf(sources, settings = sources.settings) {
|
|
|
3382
4181
|
return PROFILES.includes(raw) ? raw : "work";
|
|
3383
4182
|
}
|
|
3384
4183
|
function describeSettings(sources) {
|
|
3385
|
-
const
|
|
4184
|
+
const profile2 = profileOf(sources);
|
|
3386
4185
|
const settings = SETTINGS.map((def) => {
|
|
3387
4186
|
const fromEnv = present(sources.process, def.key);
|
|
3388
4187
|
const fromSettings = present(sources.settings, def.key);
|
|
@@ -3398,7 +4197,7 @@ function describeSettings(sources) {
|
|
|
3398
4197
|
advanced: def.advanced === true,
|
|
3399
4198
|
value,
|
|
3400
4199
|
source,
|
|
3401
|
-
fallback: def.fallback(
|
|
4200
|
+
fallback: def.fallback(profile2)
|
|
3402
4201
|
};
|
|
3403
4202
|
});
|
|
3404
4203
|
return { groups: SETTING_GROUPS, settings, error: sources.settingsError };
|
|
@@ -3431,134 +4230,758 @@ async function saveSettings(path, settings) {
|
|
|
3431
4230
|
await writeJsonAtomic(path, sorted);
|
|
3432
4231
|
}
|
|
3433
4232
|
|
|
3434
|
-
// src/
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
"
|
|
3444
|
-
"
|
|
3445
|
-
"
|
|
3446
|
-
"
|
|
3447
|
-
"
|
|
3448
|
-
"
|
|
3449
|
-
"
|
|
3450
|
-
"
|
|
4233
|
+
// src/onboarding.ts
|
|
4234
|
+
var TURN_TIMEOUT_MS2 = 4 * 6e4;
|
|
4235
|
+
var KILL_AFTER_MS = 5e3;
|
|
4236
|
+
var STEPS_KEPT = 40;
|
|
4237
|
+
var SESSION_MODEL = {
|
|
4238
|
+
claude: { model: "opus", effort: "low" },
|
|
4239
|
+
codex: { model: null, effort: "low" }
|
|
4240
|
+
};
|
|
4241
|
+
var ONBOARDING_TOOLS = [
|
|
4242
|
+
"Bash(gh *)",
|
|
4243
|
+
"Bash(acli *)",
|
|
4244
|
+
"Bash(git config *)",
|
|
4245
|
+
"mcp__claude_ai_Gmail__search_threads",
|
|
4246
|
+
"mcp__claude_ai_Gmail__get_thread",
|
|
4247
|
+
"mcp__claude_ai_Gmail__get_message",
|
|
4248
|
+
"mcp__claude_ai_Gmail__list_labels",
|
|
4249
|
+
"mcp__claude_ai_Google_Calendar__list_calendars",
|
|
4250
|
+
"mcp__claude_ai_Google_Calendar__list_events",
|
|
4251
|
+
"mcp__claude_ai_Google_Calendar__search_events",
|
|
4252
|
+
"mcp__claude_ai_Google_Calendar__get_event",
|
|
4253
|
+
"mcp__claude_ai_Google_Drive__search_files",
|
|
4254
|
+
"mcp__claude_ai_Google_Drive__list_recent_files",
|
|
4255
|
+
"mcp__claude_ai_Google_Drive__get_file_metadata",
|
|
4256
|
+
"mcp__claude_ai_Google_Drive__read_file_content"
|
|
3451
4257
|
];
|
|
3452
|
-
|
|
3453
|
-
|
|
4258
|
+
var RESTARTED = "the dashboard restarted while this was running";
|
|
4259
|
+
function blankFile() {
|
|
4260
|
+
return { startedAt: (/* @__PURE__ */ new Date()).toISOString(), finishedAt: null, briefRequested: false, answers: {}, proposals: {} };
|
|
3454
4261
|
}
|
|
3455
|
-
|
|
3456
|
-
const
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
4262
|
+
function parseFile(text) {
|
|
4263
|
+
const file = blankFile();
|
|
4264
|
+
let raw;
|
|
4265
|
+
try {
|
|
4266
|
+
raw = JSON.parse(text);
|
|
4267
|
+
} catch {
|
|
4268
|
+
return file;
|
|
4269
|
+
}
|
|
4270
|
+
if (typeof raw !== "object" || raw === null) return file;
|
|
4271
|
+
const obj = raw;
|
|
4272
|
+
if (typeof obj.startedAt === "string") file.startedAt = obj.startedAt;
|
|
4273
|
+
if (typeof obj.finishedAt === "string") file.finishedAt = obj.finishedAt;
|
|
4274
|
+
file.briefRequested = obj.briefRequested === true;
|
|
4275
|
+
if (typeof obj.answers === "object" && obj.answers !== null) {
|
|
4276
|
+
for (const [id, answer] of Object.entries(obj.answers)) {
|
|
4277
|
+
const clean = cleanAnswer(answer);
|
|
4278
|
+
if (clean) file.answers[id] = clean;
|
|
3462
4279
|
}
|
|
3463
4280
|
}
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
4281
|
+
if (typeof obj.proposals === "object" && obj.proposals !== null) {
|
|
4282
|
+
for (const [id, value] of Object.entries(obj.proposals)) {
|
|
4283
|
+
const record = value;
|
|
4284
|
+
if (!record || !CLI_NAMES.includes(record.cli) || !PROFILES.includes(record.profile)) continue;
|
|
4285
|
+
file.proposals[id] = {
|
|
4286
|
+
cli: record.cli,
|
|
4287
|
+
profile: record.profile,
|
|
4288
|
+
sessionId: typeof record.sessionId === "string" ? record.sessionId : null,
|
|
4289
|
+
status: record.status === "ready" || record.status === "failed" ? record.status : "working",
|
|
4290
|
+
result: record.result ?? null,
|
|
4291
|
+
error: typeof record.error === "string" ? record.error : null,
|
|
4292
|
+
version: typeof record.version === "number" ? record.version : 0,
|
|
4293
|
+
conversation: Array.isArray(record.conversation) ? record.conversation.filter(
|
|
4294
|
+
(line) => typeof line === "object" && line !== null && (line.from === "you" || line.from === "it") && typeof line.text === "string"
|
|
4295
|
+
) : []
|
|
4296
|
+
};
|
|
3478
4297
|
}
|
|
3479
4298
|
}
|
|
3480
|
-
return
|
|
3481
|
-
}
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
4299
|
+
return file;
|
|
4300
|
+
}
|
|
4301
|
+
function cleanAnswer(value) {
|
|
4302
|
+
if (typeof value !== "object" || value === null) return null;
|
|
4303
|
+
const obj = value;
|
|
4304
|
+
if (obj.later === true) return { later: true };
|
|
4305
|
+
const choices = {};
|
|
4306
|
+
const fields = {};
|
|
4307
|
+
if (typeof obj.choices === "object" && obj.choices !== null) {
|
|
4308
|
+
for (const [id, values] of Object.entries(obj.choices)) {
|
|
4309
|
+
if (Array.isArray(values)) choices[id] = [...new Set(values.filter((v) => typeof v === "string" && v.trim() !== ""))];
|
|
4310
|
+
}
|
|
3487
4311
|
}
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
return true;
|
|
3493
|
-
} catch {
|
|
3494
|
-
return false;
|
|
4312
|
+
if (typeof obj.fields === "object" && obj.fields !== null) {
|
|
4313
|
+
for (const [id, text] of Object.entries(obj.fields)) {
|
|
4314
|
+
if (typeof text === "string") fields[id] = text;
|
|
4315
|
+
}
|
|
3495
4316
|
}
|
|
4317
|
+
return { choices, fields };
|
|
3496
4318
|
}
|
|
3497
|
-
|
|
3498
|
-
return
|
|
4319
|
+
function given(answer) {
|
|
4320
|
+
return answer && answer.later !== true ? answer : null;
|
|
3499
4321
|
}
|
|
3500
|
-
|
|
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";
|
|
4322
|
+
function answerFromCard(card2) {
|
|
3508
4323
|
return {
|
|
3509
|
-
|
|
3510
|
-
|
|
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
|
-
}
|
|
4324
|
+
choices: Object.fromEntries(card2.choices.map((choice2) => [choice2.id, choice2.selected])),
|
|
4325
|
+
fields: Object.fromEntries(card2.fields.map((field2) => [field2.id, field2.value]))
|
|
3523
4326
|
};
|
|
3524
4327
|
}
|
|
3525
|
-
var
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
4328
|
+
var Onboarding = class {
|
|
4329
|
+
file = null;
|
|
4330
|
+
loaded = false;
|
|
4331
|
+
running = /* @__PURE__ */ new Map();
|
|
4332
|
+
steps = /* @__PURE__ */ new Map();
|
|
4333
|
+
/** Calendar.app's calendars being listed, which only starting the sessions waits for. */
|
|
4334
|
+
calendars = null;
|
|
4335
|
+
/** What listing them found, once it has: the calendars, or why they couldn't be listed. */
|
|
4336
|
+
listed = null;
|
|
4337
|
+
finishError = null;
|
|
4338
|
+
config;
|
|
4339
|
+
deps;
|
|
4340
|
+
spawnFn;
|
|
4341
|
+
gates = /* @__PURE__ */ new Map();
|
|
4342
|
+
codexOff = null;
|
|
4343
|
+
codexModel = null;
|
|
4344
|
+
/** What looking for the CLIs found, once the screen has asked. Not kept: what is installed changes. */
|
|
4345
|
+
detected = null;
|
|
4346
|
+
detecting = null;
|
|
4347
|
+
/** The pass starting sessions, which the next one waits for; see `startProposals`. */
|
|
4348
|
+
starting = Promise.resolve();
|
|
4349
|
+
/** The write in progress, which the next one waits for. */
|
|
4350
|
+
writes = Promise.resolve();
|
|
4351
|
+
constructor(config, deps) {
|
|
4352
|
+
this.config = config;
|
|
4353
|
+
this.deps = deps;
|
|
4354
|
+
this.spawnFn = deps.spawnFn ?? spawn2;
|
|
4355
|
+
}
|
|
4356
|
+
/** Whether a session has a process going. A restart waits for none. */
|
|
4357
|
+
get busy() {
|
|
4358
|
+
return this.running.size > 0;
|
|
4359
|
+
}
|
|
4360
|
+
/**
|
|
4361
|
+
* Read the file, and start again any session the last process left going.
|
|
4362
|
+
* Only while setup is still to do: `needed` is the caller's `firstBriefPending`.
|
|
4363
|
+
*/
|
|
4364
|
+
async start(needed) {
|
|
4365
|
+
await this.load();
|
|
4366
|
+
if (!needed || this.file?.finishedAt) return;
|
|
4367
|
+
for (const record of Object.values(this.file?.proposals ?? {})) {
|
|
4368
|
+
if (record.status === "working") {
|
|
4369
|
+
record.status = "failed";
|
|
4370
|
+
record.error = RESTARTED;
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
this.startProposals();
|
|
4374
|
+
}
|
|
4375
|
+
/** The screen, while setup is to do: the store has never had a brief, and setup hasn't finished. */
|
|
4376
|
+
async view(setup) {
|
|
4377
|
+
await this.load();
|
|
4378
|
+
if (!setup.needed || this.file?.finishedAt) return null;
|
|
4379
|
+
const file = this.file ?? blankFile();
|
|
4380
|
+
const ctx = await this.context({ setup });
|
|
4381
|
+
const sources = this.deps.readSources();
|
|
4382
|
+
const answers = this.effective(ctx, file.answers);
|
|
4383
|
+
const steps = [];
|
|
4384
|
+
for (const question of this.questions(ctx)) {
|
|
4385
|
+
const locked = this.locked(question, sources);
|
|
4386
|
+
const drawn = question.card(ctx);
|
|
4387
|
+
const record = file.proposals[question.id];
|
|
4388
|
+
const answer = locked ? question.id === "cli" ? { choices: { cli: ctx.cli ? [ctx.cli] : [] }, fields: {} } : answerFromCard(drawn) : answers[question.id] ?? null;
|
|
4389
|
+
const chosen = given(answer);
|
|
4390
|
+
const looking = question.id === "cli" && !answer && !this.detected;
|
|
4391
|
+
steps.push({
|
|
4392
|
+
id: question.id,
|
|
4393
|
+
title: question.title(ctx),
|
|
4394
|
+
topic: question.topic,
|
|
4395
|
+
status: answer ? chosen ? "answered" : "later" : "open",
|
|
4396
|
+
summary: chosen ? question.summary(chosen, ctx) : answer ? question.later?.summary ?? null : null,
|
|
4397
|
+
locked,
|
|
4398
|
+
card: { ...drawn, version: record?.version ?? 0, revisable: Boolean(question.agent && record), later: question.later?.label ?? null },
|
|
4399
|
+
proposal: looking ? {
|
|
4400
|
+
status: "working",
|
|
4401
|
+
conversation: [],
|
|
4402
|
+
steps: [{ kind: "tool", text: "Look for Codex and Claude Code", detail: null, state: "running" }],
|
|
4403
|
+
error: null
|
|
4404
|
+
} : record ? {
|
|
4405
|
+
status: record.status,
|
|
4406
|
+
conversation: record.conversation,
|
|
4407
|
+
steps: this.steps.get(question.id) ?? [],
|
|
4408
|
+
error: record.status === "failed" ? record.error : null
|
|
4409
|
+
} : null,
|
|
4410
|
+
answer
|
|
4411
|
+
});
|
|
4412
|
+
}
|
|
3538
4413
|
return {
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
4414
|
+
detected: this.detected !== null,
|
|
4415
|
+
steps,
|
|
4416
|
+
current: steps.find((step) => step.status === "open")?.id ?? null,
|
|
4417
|
+
error: this.finishError
|
|
3542
4418
|
};
|
|
3543
|
-
}
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
4419
|
+
}
|
|
4420
|
+
/**
|
|
4421
|
+
* Look for the CLIs, which the screen asks for when it opens: with one ready,
|
|
4422
|
+
* the first question answers itself. Asked again by Check again.
|
|
4423
|
+
*/
|
|
4424
|
+
async detect() {
|
|
4425
|
+
await this.load();
|
|
4426
|
+
await this.lookForClis(true);
|
|
4427
|
+
this.startProposals();
|
|
4428
|
+
this.deps.onChange();
|
|
4429
|
+
}
|
|
4430
|
+
/**
|
|
4431
|
+
* Record an answer, or that the user would rather decide later. One whose
|
|
4432
|
+
* settings the dashboard would refuse is refused here, while the card that
|
|
4433
|
+
* can fix it is still open.
|
|
4434
|
+
*/
|
|
4435
|
+
async answer(id, raw) {
|
|
4436
|
+
await this.load();
|
|
4437
|
+
const file = this.file ??= blankFile();
|
|
4438
|
+
if (file.finishedAt) return "setup has finished";
|
|
4439
|
+
const question = questionById(id);
|
|
4440
|
+
let answer = cleanAnswer(raw);
|
|
4441
|
+
if (!question || !answer) return `there is no question ${JSON.stringify(id)}, or that is not an answer to it`;
|
|
4442
|
+
if (!given(answer) && !question.later) return `"${question.topic}" needs an answer`;
|
|
4443
|
+
if (id === "cli" && given(answer)) {
|
|
4444
|
+
const chosen = await this.chooseCli(given(answer));
|
|
4445
|
+
if (typeof chosen === "string") return chosen;
|
|
4446
|
+
answer = chosen;
|
|
4447
|
+
}
|
|
4448
|
+
const ctx = await this.context();
|
|
4449
|
+
const before = { cli: ctx.cli, profile: ctx.profile };
|
|
4450
|
+
if (given(answer) && question.settings) {
|
|
4451
|
+
const refused = this.deps.checkSettings(this.settingsOf({ ...file.answers, [id]: answer }, await this.context({ answers: { ...file.answers, [id]: answer } })));
|
|
4452
|
+
if (refused) return refused;
|
|
4453
|
+
}
|
|
4454
|
+
file.answers[id] = answer;
|
|
4455
|
+
this.finishError = null;
|
|
4456
|
+
await this.stop(id, "you continued before it finished");
|
|
4457
|
+
const after = await this.context();
|
|
4458
|
+
if (after.profile !== before.profile) {
|
|
4459
|
+
for (const other of QUESTIONS) {
|
|
4460
|
+
if (!other.agent) continue;
|
|
4461
|
+
delete file.answers[other.id];
|
|
4462
|
+
await this.drop(other.id);
|
|
4463
|
+
}
|
|
4464
|
+
} else if (after.cli !== before.cli) {
|
|
4465
|
+
for (const other of QUESTIONS) if (other.agent && !file.answers[other.id]) await this.drop(other.id);
|
|
4466
|
+
}
|
|
4467
|
+
await this.save();
|
|
4468
|
+
this.startProposals();
|
|
4469
|
+
this.deps.onChange();
|
|
4470
|
+
return null;
|
|
4471
|
+
}
|
|
4472
|
+
/** Tell a question's session what to change. `draft` is what the card holds now, which it should start from. */
|
|
4473
|
+
async revise(id, text, draft) {
|
|
4474
|
+
await this.load();
|
|
4475
|
+
const question = questionById(id);
|
|
4476
|
+
const record = this.file?.proposals[id];
|
|
4477
|
+
if (!question?.agent || !record) return "nothing is looking into that question";
|
|
4478
|
+
if (this.running.has(id)) return "it is still working on that question";
|
|
4479
|
+
const ctx = await this.context();
|
|
4480
|
+
const current = cleanAnswer(draft);
|
|
4481
|
+
const request = [
|
|
4482
|
+
`The user says: ${text.trim()}`,
|
|
4483
|
+
...given(current) ? ["", "What the card holds now, which they may have changed:", "", "```json", JSON.stringify(current), "```"] : [],
|
|
4484
|
+
"",
|
|
4485
|
+
"Revise your proposal, and answer with the whole of it again."
|
|
4486
|
+
].join("\n");
|
|
4487
|
+
const resume = record.sessionId && record.cli === ctx.cli ? record.sessionId : null;
|
|
4488
|
+
const said = [...record.conversation, { from: "you", text: text.trim() }];
|
|
4489
|
+
await this.ask(question, ctx, resume ? request : `${await this.firstMessage(question, ctx)}
|
|
4490
|
+
|
|
4491
|
+
${request}`, resume, said);
|
|
4492
|
+
return null;
|
|
4493
|
+
}
|
|
4494
|
+
/**
|
|
4495
|
+
* Look again: ask a question's session again from the start, after it failed,
|
|
4496
|
+
* and list Calendar.app's calendars again for the question that shows them,
|
|
4497
|
+
* since what failed may have been the access the user has since granted.
|
|
4498
|
+
*/
|
|
4499
|
+
async retry(id) {
|
|
4500
|
+
await this.load();
|
|
4501
|
+
const question = questionById(id);
|
|
4502
|
+
if (!question) return `there is no question ${JSON.stringify(id)}`;
|
|
4503
|
+
if (this.running.has(id)) return "it is still working on that question";
|
|
4504
|
+
if (id === "calendars") {
|
|
4505
|
+
this.calendars = null;
|
|
4506
|
+
this.listed = null;
|
|
4507
|
+
}
|
|
4508
|
+
if (id === "cli") await this.lookForClis(true);
|
|
4509
|
+
if (question.agent) await this.drop(id);
|
|
4510
|
+
this.startProposals();
|
|
4511
|
+
this.deps.onChange();
|
|
4512
|
+
return null;
|
|
4513
|
+
}
|
|
4514
|
+
/**
|
|
4515
|
+
* Save everything, and ask for the first brief. Files first, then the
|
|
4516
|
+
* settings, whose save restarts the dashboard: the start after it begins the
|
|
4517
|
+
* run. Resolves with why not, or null.
|
|
4518
|
+
*/
|
|
4519
|
+
async finish() {
|
|
4520
|
+
await this.load();
|
|
4521
|
+
const file = this.file ??= blankFile();
|
|
4522
|
+
if (file.finishedAt) return { error: "setup has already finished" };
|
|
4523
|
+
if (!file.answers.cli) await this.lookForClis(false);
|
|
4524
|
+
const ctx = await this.context();
|
|
4525
|
+
const sources = this.deps.readSources();
|
|
4526
|
+
const answers = this.effective(ctx, file.answers);
|
|
4527
|
+
const sections = [];
|
|
4528
|
+
let objective2 = null;
|
|
4529
|
+
for (const question of this.questions(ctx)) {
|
|
4530
|
+
const answer = answers[question.id];
|
|
4531
|
+
if (!answer && !this.locked(question, sources)) return this.refuse(`answer "${question.title(ctx)}", or decide it later`);
|
|
4532
|
+
const chosen = this.locked(question, sources) ? null : given(answer);
|
|
4533
|
+
if (!chosen) continue;
|
|
4534
|
+
sections.push(...question.sources?.(chosen, ctx) ?? []);
|
|
4535
|
+
objective2 = question.objective?.(chosen) ?? objective2;
|
|
4536
|
+
}
|
|
4537
|
+
const values = this.settingsOf(file.answers, ctx);
|
|
4538
|
+
const refused = this.deps.checkSettings(values);
|
|
4539
|
+
if (refused) return this.refuse(refused);
|
|
4540
|
+
await this.starting;
|
|
4541
|
+
for (const id of [...this.running.keys()]) await this.stop(id, "setup finished");
|
|
4542
|
+
const existing = await readEditable(this.config.sourcesFile);
|
|
4543
|
+
if (existing.text === null && sections.length > 0) {
|
|
4544
|
+
await mkdir2(this.config.dataDir, { recursive: true });
|
|
4545
|
+
await saveEditable(this.config.sourcesFile, composeSources(ctx.profile, sections, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)), ABSENT);
|
|
4546
|
+
}
|
|
4547
|
+
if (objective2) {
|
|
4548
|
+
const focus = await readEditable(this.config.focusFile);
|
|
4549
|
+
const saved = await saveEditable(this.config.focusFile, setFocusFields(focus.text, objective2, FOCUS_TEMPLATE), focus.version);
|
|
4550
|
+
if (!saved.saved) return this.refuse("focus.md changed on disk while saving; finish again");
|
|
4551
|
+
}
|
|
4552
|
+
file.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4553
|
+
file.briefRequested = ctx.cli !== null;
|
|
4554
|
+
await this.save();
|
|
4555
|
+
const before = JSON.stringify(Object.entries(sources.settings).sort());
|
|
4556
|
+
const saveRefused = await this.deps.saveSettings(values);
|
|
4557
|
+
if (saveRefused) {
|
|
4558
|
+
file.finishedAt = null;
|
|
4559
|
+
file.briefRequested = false;
|
|
4560
|
+
await this.save();
|
|
4561
|
+
return this.refuse(saveRefused);
|
|
4562
|
+
}
|
|
4563
|
+
this.finishError = null;
|
|
4564
|
+
this.deps.onChange();
|
|
4565
|
+
return { restart: JSON.stringify(Object.entries(this.deps.readSources().settings).sort()) !== before };
|
|
4566
|
+
}
|
|
4567
|
+
/**
|
|
4568
|
+
* Whether the finish asked for a first brief that hasn't been started, and
|
|
4569
|
+
* forget it: asked once, so a run that fails is retried by the user, not by
|
|
4570
|
+
* every start.
|
|
4571
|
+
*/
|
|
4572
|
+
async takeBriefRequest() {
|
|
4573
|
+
await this.load();
|
|
4574
|
+
if (!this.file?.briefRequested) return false;
|
|
4575
|
+
this.file.briefRequested = false;
|
|
4576
|
+
await this.save();
|
|
4577
|
+
return true;
|
|
4578
|
+
}
|
|
4579
|
+
/** What the finish would write as the source list, or null when the store already has one it would keep. */
|
|
4580
|
+
async previewSources() {
|
|
4581
|
+
await this.load();
|
|
4582
|
+
const existing = await readEditable(this.config.sourcesFile);
|
|
4583
|
+
if (existing.text !== null) return { text: null, kept: true };
|
|
4584
|
+
const ctx = await this.context();
|
|
4585
|
+
const sections = [];
|
|
4586
|
+
for (const question of this.questions(ctx)) {
|
|
4587
|
+
const chosen = given(this.file?.answers[question.id]);
|
|
4588
|
+
if (chosen) sections.push(...question.sources?.(chosen, ctx) ?? []);
|
|
4589
|
+
}
|
|
4590
|
+
return { text: sections.length ? composeSources(ctx.profile, sections, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) : null, kept: false };
|
|
4591
|
+
}
|
|
4592
|
+
/** Stop every session, for shutdown, and wait until each has exited and been written down. */
|
|
4593
|
+
async close() {
|
|
4594
|
+
await this.starting;
|
|
4595
|
+
const over = [...this.running.values()].map((run5) => run5.over);
|
|
4596
|
+
for (const id of [...this.running.keys()]) await this.stop(id, RESTARTED);
|
|
4597
|
+
await Promise.all(over);
|
|
4598
|
+
}
|
|
4599
|
+
/* ---------- the sessions ---------- */
|
|
4600
|
+
/**
|
|
4601
|
+
* Start whatever sessions the answers so far call for, without holding up the
|
|
4602
|
+
* reply that asked: starting one under Codex runs a few checks first, and a
|
|
4603
|
+
* screen waiting on them has every button greyed out with nothing to say why.
|
|
4604
|
+
* One pass at a time, so two quick answers can't both start the same question.
|
|
4605
|
+
*/
|
|
4606
|
+
startProposals() {
|
|
4607
|
+
this.starting = this.starting.then(() => this.ensureProposals()).catch((err) => console.error(`[daily-focus] setup could not start looking: ${err.message}`));
|
|
4608
|
+
}
|
|
4609
|
+
/** Resolves once the sessions asked for so far have started: for tests, and for anything that must not race them. */
|
|
4610
|
+
started() {
|
|
4611
|
+
return this.starting;
|
|
4612
|
+
}
|
|
4613
|
+
/** Start a session for each question that has one and needs it: the CLI and the profile are known, and nobody has asked yet. */
|
|
4614
|
+
async ensureProposals() {
|
|
4615
|
+
const file = this.file;
|
|
4616
|
+
if (!file || file.finishedAt) return;
|
|
4617
|
+
if (Object.keys(file.answers).length === 0) return;
|
|
4618
|
+
const set = (this.deps.readSources().process.DAILY_FOCUS_AGENT ?? "").trim();
|
|
4619
|
+
if (file.answers.cli && !given(file.answers.cli) || set && !CLI_NAMES.includes(set)) return;
|
|
4620
|
+
await this.lookForClis(false);
|
|
4621
|
+
const ctx = await this.context({ listCalendars: true });
|
|
4622
|
+
const sources = this.deps.readSources();
|
|
4623
|
+
const profileKnown = given(file.answers.profile) !== null || (sources.process.DAILY_FOCUS_PROFILE ?? "").trim() !== "";
|
|
4624
|
+
if (!ctx.cli || !profileKnown) return;
|
|
4625
|
+
for (const question of this.questions(ctx)) {
|
|
4626
|
+
if (!question.agent || file.answers[question.id] || this.running.has(question.id)) continue;
|
|
4627
|
+
const record = file.proposals[question.id];
|
|
4628
|
+
if (record && record.cli === ctx.cli && record.profile === ctx.profile && record.status !== "failed") continue;
|
|
4629
|
+
if (record?.status === "failed" && record.error !== RESTARTED && record.cli === ctx.cli && record.profile === ctx.profile) continue;
|
|
4630
|
+
await this.ask(question, ctx, await this.firstMessage(question, ctx), null);
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
/** The first message of a question's session: the instructions, what is known, and the question. */
|
|
4634
|
+
async firstMessage(question, ctx) {
|
|
4635
|
+
const instructions = await readFile8(this.config.onboardingPromptSource, "utf8").catch(
|
|
4636
|
+
() => "You are helping someone set up Daily Focus. Only read; never send, post or edit anything. Answer with the JSON the schema asks for."
|
|
4637
|
+
);
|
|
4638
|
+
const agent = question.agent;
|
|
4639
|
+
const facts = agent.facts?.(ctx) ?? null;
|
|
4640
|
+
return [
|
|
4641
|
+
instructions.trim(),
|
|
4642
|
+
"",
|
|
4643
|
+
"## This setup",
|
|
4644
|
+
"",
|
|
4645
|
+
`- Profile: ${ctx.profile}`,
|
|
4646
|
+
`- Today: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`,
|
|
4647
|
+
"",
|
|
4648
|
+
`## The question: ${question.title(ctx)}`,
|
|
4649
|
+
"",
|
|
4650
|
+
agent.brief(ctx),
|
|
4651
|
+
...facts ? ["", "### What the dashboard already knows", "", facts] : []
|
|
4652
|
+
].join("\n");
|
|
4653
|
+
}
|
|
4654
|
+
async ask(question, ctx, prompt, resume, conversation = []) {
|
|
4655
|
+
const file = this.file ??= blankFile();
|
|
4656
|
+
const cli2 = ctx.cli;
|
|
4657
|
+
const previous = file.proposals[question.id];
|
|
4658
|
+
const record = {
|
|
4659
|
+
cli: cli2,
|
|
4660
|
+
profile: ctx.profile,
|
|
4661
|
+
sessionId: resume,
|
|
4662
|
+
status: "working",
|
|
4663
|
+
result: previous?.cli === cli2 && previous.profile === ctx.profile ? previous.result : null,
|
|
4664
|
+
error: null,
|
|
4665
|
+
version: previous?.version ?? 0,
|
|
4666
|
+
conversation
|
|
4667
|
+
};
|
|
4668
|
+
file.proposals[question.id] = record;
|
|
4669
|
+
this.steps.set(question.id, []);
|
|
4670
|
+
await this.save();
|
|
4671
|
+
this.deps.onChange();
|
|
4672
|
+
const bin = ctx.clis?.[cli2]?.path ?? null;
|
|
4673
|
+
if (!bin) return this.settle(question.id, record, { ok: false, error: `could not find ${cli2} on this machine` });
|
|
4674
|
+
if (cli2 === "codex") {
|
|
4675
|
+
const gate = this.gates.get(bin) ?? new CodexGate(bin, this.deps.execFn);
|
|
4676
|
+
this.gates.set(bin, gate);
|
|
4677
|
+
const refusal2 = await gate.refusal();
|
|
4678
|
+
if (refusal2) return this.settle(question.id, record, { ok: false, error: refusal2 });
|
|
4679
|
+
}
|
|
4680
|
+
let codexConfig = [];
|
|
4681
|
+
if (cli2 === "codex") {
|
|
4682
|
+
try {
|
|
4683
|
+
this.codexOff ??= computerUseOff(bin, this.deps.execFn);
|
|
4684
|
+
codexConfig = await this.codexOff;
|
|
4685
|
+
} catch (err) {
|
|
4686
|
+
this.codexOff = null;
|
|
4687
|
+
return this.settle(question.id, record, { ok: false, error: `Codex could not say which plugins to switch off: ${err.message}` });
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
4690
|
+
const schemaDir = await mkdtemp(join5(tmpdir2(), "daily-focus-setup-"));
|
|
4691
|
+
const schemaFile = join5(schemaDir, "schema.json");
|
|
4692
|
+
const json = JSON.stringify(question.agent.schema);
|
|
4693
|
+
await writeFile2(schemaFile, json, "utf8");
|
|
4694
|
+
await mkdir2(this.config.onboardingDir, { recursive: true });
|
|
4695
|
+
const adapter = ADAPTERS[cli2];
|
|
4696
|
+
const session = SESSION_MODEL[cli2];
|
|
4697
|
+
const model = cli2 === "codex" ? await (this.codexModel ??= codexLatestModel(bin, this.deps.execFn)) : session.model;
|
|
4698
|
+
const args = adapter.args({
|
|
4699
|
+
prompt,
|
|
4700
|
+
sessionId: resume,
|
|
4701
|
+
model,
|
|
4702
|
+
effort: session.effort,
|
|
4703
|
+
tools: ONBOARDING_TOOLS,
|
|
4704
|
+
denyEdits: true,
|
|
4705
|
+
drafts: false,
|
|
4706
|
+
codexConfig,
|
|
4707
|
+
schema: { json, file: schemaFile }
|
|
4708
|
+
});
|
|
4709
|
+
let child;
|
|
4710
|
+
try {
|
|
4711
|
+
child = this.spawnFn(bin, args, { cwd: this.config.onboardingDir, stdio: ["ignore", "pipe", "pipe"], env: process.env });
|
|
4712
|
+
} catch (err) {
|
|
4713
|
+
await rm2(schemaDir, { recursive: true, force: true });
|
|
4714
|
+
return this.settle(question.id, record, { ok: false, error: `could not run ${bin}: ${err.message}` });
|
|
4715
|
+
}
|
|
4716
|
+
const timer = setTimeout(() => void this.stop(question.id, "gave up after four minutes"), TURN_TIMEOUT_MS2);
|
|
4717
|
+
timer.unref();
|
|
4718
|
+
let ended;
|
|
4719
|
+
const run5 = { child, timer, abortReason: null, over: new Promise((resolve2) => ended = resolve2) };
|
|
4720
|
+
this.running.set(question.id, run5);
|
|
4721
|
+
let done;
|
|
4722
|
+
let said = "";
|
|
4723
|
+
const stderr = [];
|
|
4724
|
+
child.stderr?.on("data", (chunk) => {
|
|
4725
|
+
stderr.push(chunk.toString("utf8"));
|
|
4726
|
+
if (stderr.length > 50) stderr.shift();
|
|
4727
|
+
});
|
|
4728
|
+
const calls = /* @__PURE__ */ new Map();
|
|
4729
|
+
const steps = this.steps.get(question.id);
|
|
4730
|
+
const add = (step) => {
|
|
4731
|
+
steps.push(step);
|
|
4732
|
+
if (steps.length > STEPS_KEPT) steps.shift();
|
|
4733
|
+
};
|
|
4734
|
+
if (child.stdout) {
|
|
4735
|
+
createInterface2({ input: child.stdout }).on("line", (line) => {
|
|
4736
|
+
const event = adapter.parse(line);
|
|
4737
|
+
if (!event) return;
|
|
4738
|
+
if (event.sessionId) record.sessionId = event.sessionId;
|
|
4739
|
+
if (event.text) said = event.text;
|
|
4740
|
+
for (const parsed of event.steps ?? []) {
|
|
4741
|
+
const known = calls.get(parsed.id);
|
|
4742
|
+
if (known) {
|
|
4743
|
+
if (parsed.text) known.text = parsed.text;
|
|
4744
|
+
if (parsed.detail) known.detail = parsed.detail;
|
|
4745
|
+
known.state = parsed.state;
|
|
4746
|
+
} else if (parsed.text) {
|
|
4747
|
+
const step = { kind: "tool", text: parsed.text, detail: parsed.detail ?? null, state: parsed.state };
|
|
4748
|
+
calls.set(parsed.id, step);
|
|
4749
|
+
add(step);
|
|
4750
|
+
}
|
|
4751
|
+
}
|
|
4752
|
+
if (event.steps) this.deps.onChange();
|
|
4753
|
+
if (event.done) done = event.done;
|
|
4754
|
+
});
|
|
4755
|
+
}
|
|
4756
|
+
child.on("error", (err) => {
|
|
4757
|
+
done = { ok: false, error: `could not run ${bin}: ${err.message}` };
|
|
4758
|
+
});
|
|
4759
|
+
child.on("exit", () => {
|
|
4760
|
+
if (run5.abortReason === null) return;
|
|
4761
|
+
child.stdout?.destroy();
|
|
4762
|
+
child.stderr?.destroy();
|
|
4763
|
+
});
|
|
4764
|
+
child.on("close", (code) => {
|
|
4765
|
+
clearTimeout(timer);
|
|
4766
|
+
this.running.delete(question.id);
|
|
4767
|
+
void rm2(schemaDir, { recursive: true, force: true });
|
|
4768
|
+
const outcome = run5.abortReason ? { ok: false, error: run5.abortReason } : done ?? {
|
|
4769
|
+
ok: false,
|
|
4770
|
+
error: `${cli2} exited with code ${code}${stderr.length ? `: ${stderr.join("").trim().split("\n").slice(-3).join(" ")}` : ""}`
|
|
4771
|
+
};
|
|
4772
|
+
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);
|
|
4773
|
+
});
|
|
4774
|
+
}
|
|
4775
|
+
/** A session is over: keep its answer when it is JSON, or why it isn't. */
|
|
4776
|
+
async settle(id, record, outcome) {
|
|
4777
|
+
if (this.file?.proposals[id] !== record) return;
|
|
4778
|
+
for (const step of this.steps.get(id) ?? []) if (step.state === "running") step.state = outcome.ok ? "done" : "failed";
|
|
4779
|
+
if (outcome.ok) {
|
|
4780
|
+
try {
|
|
4781
|
+
record.result = JSON.parse(stripFence(outcome.reply ?? ""));
|
|
4782
|
+
record.status = "ready";
|
|
4783
|
+
record.error = null;
|
|
4784
|
+
record.version += 1;
|
|
4785
|
+
const why = record.result?.why;
|
|
4786
|
+
if (typeof why === "string" && why.trim()) record.conversation.push({ from: "it", text: why.trim() });
|
|
4787
|
+
} catch {
|
|
4788
|
+
record.status = "failed";
|
|
4789
|
+
record.error = "it answered, but not in the shape asked for";
|
|
4790
|
+
}
|
|
4791
|
+
} else {
|
|
4792
|
+
record.status = "failed";
|
|
4793
|
+
record.error = outcome.error ?? "failed";
|
|
4794
|
+
}
|
|
4795
|
+
await this.save();
|
|
4796
|
+
this.deps.onChange();
|
|
4797
|
+
}
|
|
4798
|
+
/** Stop a session: asked to, then made to if it hasn't gone within a few seconds. */
|
|
4799
|
+
async stop(id, reason) {
|
|
4800
|
+
const run5 = this.running.get(id);
|
|
4801
|
+
if (!run5 || run5.abortReason !== null) return;
|
|
4802
|
+
run5.abortReason = reason;
|
|
4803
|
+
run5.child.kill("SIGTERM");
|
|
4804
|
+
const kill = setTimeout(() => {
|
|
4805
|
+
if (run5.child.exitCode === null && run5.child.signalCode === null) run5.child.kill("SIGKILL");
|
|
4806
|
+
}, KILL_AFTER_MS);
|
|
4807
|
+
kill.unref();
|
|
4808
|
+
void run5.over.then(() => clearTimeout(kill));
|
|
4809
|
+
}
|
|
4810
|
+
/** Forget a question's proposal, stopping its session first. */
|
|
4811
|
+
async drop(id) {
|
|
4812
|
+
await this.stop(id, "no longer needed");
|
|
4813
|
+
if (this.file) delete this.file.proposals[id];
|
|
4814
|
+
this.steps.delete(id);
|
|
4815
|
+
}
|
|
4816
|
+
/* ---------- what a question may read ---------- */
|
|
4817
|
+
/**
|
|
4818
|
+
* Look for the CLIs, once per process unless `again`. Only from a click or
|
|
4819
|
+
* from setup already under way: `--version` and a login check are programs
|
|
4820
|
+
* run on the user's behalf, and a new store's state is read before anyone is
|
|
4821
|
+
* at the screen.
|
|
4822
|
+
*/
|
|
4823
|
+
async lookForClis(again) {
|
|
4824
|
+
if (this.detected && !again) return;
|
|
4825
|
+
this.detecting ??= this.deps.detectClis().finally(() => {
|
|
4826
|
+
this.detecting = null;
|
|
4827
|
+
});
|
|
4828
|
+
this.detected = await this.detecting;
|
|
4829
|
+
}
|
|
4830
|
+
/**
|
|
4831
|
+
* The CLI answer to keep: one of the ready ones, or a path the user typed,
|
|
4832
|
+
* which is used only once it has said what it is and that it is ready.
|
|
4833
|
+
* Otherwise why not, in a sentence the card shows.
|
|
4834
|
+
*/
|
|
4835
|
+
async chooseCli(answer) {
|
|
4836
|
+
const typed = answer.fields.path?.trim();
|
|
4837
|
+
if (!typed) {
|
|
4838
|
+
const picked = answer.choices.cli?.[0];
|
|
4839
|
+
await this.lookForClis(false);
|
|
4840
|
+
if (picked && this.detected?.[picked]?.ready) return { choices: { cli: [picked] }, fields: {} };
|
|
4841
|
+
return "Enter the path to codex or claude, or decide this later";
|
|
4842
|
+
}
|
|
4843
|
+
const { cli: cli2, found } = await this.deps.probeCli(expandHome(typed));
|
|
4844
|
+
if (!cli2 || !found.ready) {
|
|
4845
|
+
return `${found.problem ?? "That can't be used"}${found.fix ? `: run ${found.fix} in a terminal, then try again.` : "."}`;
|
|
4846
|
+
}
|
|
4847
|
+
await this.lookForClis(false);
|
|
4848
|
+
this.detected = { ...this.detected, [cli2]: found };
|
|
4849
|
+
return { choices: { cli: [cli2] }, fields: { path: found.path } };
|
|
4850
|
+
}
|
|
4851
|
+
/**
|
|
4852
|
+
* The answers as they stand, with the CLI setup picked without asking when
|
|
4853
|
+
* the user hasn't picked one: the first ready, or the one already set.
|
|
4854
|
+
*/
|
|
4855
|
+
effective(ctx, answers) {
|
|
4856
|
+
if (answers.cli || !ctx.cli) return { ...answers };
|
|
4857
|
+
return { ...answers, cli: { choices: { cli: [ctx.cli] }, fields: {} } };
|
|
4858
|
+
}
|
|
4859
|
+
/** Set in the environment, which wins over setup: shown as it stands, never asked. */
|
|
4860
|
+
locked(question, sources) {
|
|
4861
|
+
return question.keys.length > 0 && question.keys.every((key) => (sources.process[key] ?? "").trim() !== "");
|
|
4862
|
+
}
|
|
4863
|
+
/**
|
|
4864
|
+
* The settings these answers add up to, in question order, less what the
|
|
4865
|
+
* environment sets: that can't be saved over, and wins anyway.
|
|
4866
|
+
*/
|
|
4867
|
+
settingsOf(answers, ctx) {
|
|
4868
|
+
const sources = this.deps.readSources();
|
|
4869
|
+
const values = {};
|
|
4870
|
+
const effective = this.effective(ctx, answers);
|
|
4871
|
+
for (const question of this.questions(ctx)) {
|
|
4872
|
+
const chosen = this.locked(question, sources) ? null : given(effective[question.id]);
|
|
4873
|
+
if (chosen) Object.assign(values, question.settings?.(chosen, ctx) ?? {});
|
|
4874
|
+
}
|
|
4875
|
+
for (const key of Object.keys(values)) if ((sources.process[key] ?? "").trim() !== "") delete values[key];
|
|
4876
|
+
return values;
|
|
4877
|
+
}
|
|
4878
|
+
questions(ctx) {
|
|
4879
|
+
return QUESTIONS.filter((question) => !question.applies || question.applies(ctx));
|
|
4880
|
+
}
|
|
4881
|
+
/**
|
|
4882
|
+
* Everything a question may read. The CLI and the profile are the answers
|
|
4883
|
+
* when given, else what is in effect, else the card's own pick, so the
|
|
4884
|
+
* questions that follow from them are counted from the start.
|
|
4885
|
+
*
|
|
4886
|
+
* Calendar.app's calendars only as already listed, unless `listCalendars`:
|
|
4887
|
+
* the helper may ask for calendar access, so it is launched only from a
|
|
4888
|
+
* click, never from a state read, which any GET makes.
|
|
4889
|
+
*/
|
|
4890
|
+
async context(opts = {}) {
|
|
4891
|
+
const sources = this.deps.readSources();
|
|
4892
|
+
const env = layerEnv(sources);
|
|
4893
|
+
const current = {};
|
|
4894
|
+
for (const def of SETTINGS) {
|
|
4895
|
+
const value = (env[def.key] ?? "").trim();
|
|
4896
|
+
if (value) current[def.key] = value;
|
|
4897
|
+
}
|
|
4898
|
+
const setup = opts.setup ?? await this.deps.setup();
|
|
4899
|
+
const answers = opts.answers ?? this.file?.answers ?? {};
|
|
4900
|
+
const profileAnswer = given(answers.profile)?.choices.profile?.[0];
|
|
4901
|
+
const profile2 = PROFILES.includes(profileAnswer) ? profileAnswer : current.DAILY_FOCUS_PROFILE === "personal" ? "personal" : "work";
|
|
4902
|
+
const cliAnswer = given(answers.cli);
|
|
4903
|
+
const typed = cliAnswer?.fields.path?.trim();
|
|
4904
|
+
const answered = cliAnswer?.choices.cli?.[0];
|
|
4905
|
+
let clis = this.detected;
|
|
4906
|
+
if (clis && typed && answered && CLI_NAMES.includes(answered) && clis[answered].path !== typed) {
|
|
4907
|
+
clis = { ...clis, [answered]: { ...clis[answered], path: typed, onPath: false, ready: true, problem: null, fix: null } };
|
|
4908
|
+
}
|
|
4909
|
+
const set = current.DAILY_FOCUS_AGENT;
|
|
4910
|
+
const cliLocked = (sources.process.DAILY_FOCUS_AGENT ?? "").trim() !== "";
|
|
4911
|
+
const cli2 = answers.cli ? answered ?? null : cliLocked ? set && CLI_NAMES.includes(set) ? set : null : set && clis?.[set]?.ready ? set : clis ? preferredCli(clis) : null;
|
|
4912
|
+
const started = Object.keys(answers).length > 0;
|
|
4913
|
+
const profileKnown = started && (given(answers.profile) !== null || (sources.process.DAILY_FOCUS_PROFILE ?? "").trim() !== "");
|
|
4914
|
+
let calendars2 = null;
|
|
4915
|
+
const liveAgenda = !["off", "false", "0", "no"].includes((current.DAILY_FOCUS_CALENDAR ?? "").toLowerCase());
|
|
4916
|
+
if (setup.calendar.supported && setup.calendar.built && liveAgenda) {
|
|
4917
|
+
if (opts.listCalendars === true && profileKnown) {
|
|
4918
|
+
this.calendars ??= this.deps.listCalendars().catch((err) => err instanceof Error ? err.message : String(err)).then((listed2) => {
|
|
4919
|
+
this.listed = listed2;
|
|
4920
|
+
});
|
|
4921
|
+
await this.calendars;
|
|
4922
|
+
}
|
|
4923
|
+
calendars2 = this.listed;
|
|
4924
|
+
}
|
|
4925
|
+
const proposals = {};
|
|
4926
|
+
for (const [id, record] of Object.entries(this.file?.proposals ?? {})) {
|
|
4927
|
+
if (record.cli === cli2 && record.profile === profile2 && record.result !== null) proposals[id] = record.result;
|
|
4928
|
+
}
|
|
4929
|
+
return {
|
|
4930
|
+
machine: machineName(),
|
|
4931
|
+
profile: profile2,
|
|
4932
|
+
cli: cli2 && CLI_NAMES.includes(cli2) ? cli2 : null,
|
|
4933
|
+
clis,
|
|
4934
|
+
identity: started ? await this.deps.identity() : { name: null, login: null, logins: [], email: null },
|
|
4935
|
+
calendars: calendars2,
|
|
4936
|
+
calendar: setup.calendar,
|
|
4937
|
+
boards: await this.deps.boards(),
|
|
4938
|
+
current,
|
|
4939
|
+
proposals
|
|
4940
|
+
};
|
|
4941
|
+
}
|
|
4942
|
+
/* ---------- the file ---------- */
|
|
4943
|
+
async load() {
|
|
4944
|
+
if (this.loaded) return;
|
|
4945
|
+
this.loaded = true;
|
|
4946
|
+
try {
|
|
4947
|
+
this.file = parseFile(await readFile8(this.config.onboardingFile, "utf8"));
|
|
4948
|
+
} catch {
|
|
4949
|
+
this.file = null;
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
/**
|
|
4953
|
+
* Write the file as it is now. One write at a time: sessions settle side by
|
|
4954
|
+
* side, and two renames of the same temporary file would trip over each other.
|
|
4955
|
+
*/
|
|
4956
|
+
save() {
|
|
4957
|
+
const write = this.writes.then(async () => {
|
|
4958
|
+
if (!this.file) return;
|
|
4959
|
+
await mkdir2(this.config.dataDir, { recursive: true });
|
|
4960
|
+
await writeJsonAtomic(this.config.onboardingFile, this.file);
|
|
4961
|
+
});
|
|
4962
|
+
this.writes = write.catch(() => {
|
|
4963
|
+
});
|
|
4964
|
+
return write;
|
|
4965
|
+
}
|
|
4966
|
+
refuse(error) {
|
|
4967
|
+
this.finishError = error;
|
|
4968
|
+
this.deps.onChange();
|
|
4969
|
+
return { error };
|
|
4970
|
+
}
|
|
4971
|
+
};
|
|
4972
|
+
function stripFence(text) {
|
|
4973
|
+
const fenced = /^```(?:json)?\s*\n([\s\S]*?)\n```\s*$/.exec(text.trim());
|
|
4974
|
+
return fenced ? fenced[1] : text.trim();
|
|
3552
4975
|
}
|
|
3553
4976
|
|
|
3554
4977
|
// src/shellpath.ts
|
|
3555
|
-
import { spawn as
|
|
4978
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
3556
4979
|
import { userInfo } from "node:os";
|
|
3557
4980
|
var START = "__DAILY_FOCUS_PATH_START__";
|
|
3558
4981
|
var END = "__DAILY_FOCUS_PATH_END__";
|
|
3559
4982
|
var SHELL_TIMEOUT_MS = 5e3;
|
|
3560
4983
|
var defaultExec = (file, args, { timeout }) => new Promise((resolve2, reject) => {
|
|
3561
|
-
const child =
|
|
4984
|
+
const child = spawn3(file, args, { detached: true, stdio: ["ignore", "pipe", "ignore"] });
|
|
3562
4985
|
let output = "";
|
|
3563
4986
|
let settled = false;
|
|
3564
4987
|
const finish = (error) => {
|
|
@@ -3650,7 +5073,7 @@ async function hydratePath(env = process.env, platform = process.platform, exec
|
|
|
3650
5073
|
|
|
3651
5074
|
// src/server.ts
|
|
3652
5075
|
var PUBLIC_DIR = resolve(fileURLToPath(new URL("../public", import.meta.url)));
|
|
3653
|
-
var
|
|
5076
|
+
var VERSION2 = packageVersion();
|
|
3654
5077
|
var MIME = {
|
|
3655
5078
|
".html": "text/html; charset=utf-8",
|
|
3656
5079
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -3693,7 +5116,7 @@ async function readBody(req) {
|
|
|
3693
5116
|
}
|
|
3694
5117
|
async function serveStatic(res, urlPath) {
|
|
3695
5118
|
const relative = normalize(decodeURIComponent(urlPath === "/" ? "/index.html" : urlPath));
|
|
3696
|
-
const filePath =
|
|
5119
|
+
const filePath = join6(PUBLIC_DIR, relative);
|
|
3697
5120
|
if (filePath !== PUBLIC_DIR && !filePath.startsWith(PUBLIC_DIR + sep)) {
|
|
3698
5121
|
res.writeHead(403).end("Forbidden");
|
|
3699
5122
|
return;
|
|
@@ -3743,7 +5166,7 @@ function configure(sources) {
|
|
|
3743
5166
|
function statusOf(state, config, busy) {
|
|
3744
5167
|
const last = state.agentRun.last;
|
|
3745
5168
|
return {
|
|
3746
|
-
version:
|
|
5169
|
+
version: VERSION2,
|
|
3747
5170
|
dataDir: config.dataDir,
|
|
3748
5171
|
profile: config.profile,
|
|
3749
5172
|
setupNeeded: state.setup.needed,
|
|
@@ -3782,7 +5205,7 @@ async function startServer(env, options = {}) {
|
|
|
3782
5205
|
let restart = null;
|
|
3783
5206
|
let restarting = false;
|
|
3784
5207
|
let settingsWrites = Promise.resolve();
|
|
3785
|
-
await stat4(
|
|
5208
|
+
await stat4(join6(PUBLIC_DIR, "app.js")).catch(() => {
|
|
3786
5209
|
console.warn(
|
|
3787
5210
|
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
5211
|
);
|
|
@@ -3822,6 +5245,46 @@ async function startServer(env, options = {}) {
|
|
|
3822
5245
|
restartWhenIdle();
|
|
3823
5246
|
}
|
|
3824
5247
|
});
|
|
5248
|
+
const onboarding = new Onboarding(config, {
|
|
5249
|
+
onChange: () => {
|
|
5250
|
+
void broadcast();
|
|
5251
|
+
restartWhenIdle();
|
|
5252
|
+
},
|
|
5253
|
+
// A path already set for either CLI is looked at before PATH.
|
|
5254
|
+
detectClis: () => {
|
|
5255
|
+
const configured = {};
|
|
5256
|
+
if (config.agent.cli && config.agent.binPath !== config.agent.cli) configured[config.agent.cli] = config.agent.binPath;
|
|
5257
|
+
if (config.assistant.agent && config.assistant.binPath !== config.assistant.agent) configured[config.assistant.agent] = config.assistant.binPath;
|
|
5258
|
+
return detectClis(configured);
|
|
5259
|
+
},
|
|
5260
|
+
probeCli: (path) => probeCli(path, null, false),
|
|
5261
|
+
readSources,
|
|
5262
|
+
setup: () => setupState(config),
|
|
5263
|
+
identity: () => detectIdentity(config),
|
|
5264
|
+
boards: async () => {
|
|
5265
|
+
const actions = await store.readActions();
|
|
5266
|
+
const now = /* @__PURE__ */ new Date();
|
|
5267
|
+
const health = (view) => ({
|
|
5268
|
+
enabled: view.enabled,
|
|
5269
|
+
fetchedAt: view.fetchedAt,
|
|
5270
|
+
reason: view.reason?.message ?? null
|
|
5271
|
+
});
|
|
5272
|
+
return { github: health(board.view(actions, now)), jira: health(tickets.view(actions, now)) };
|
|
5273
|
+
},
|
|
5274
|
+
listCalendars: async () => {
|
|
5275
|
+
const facts = await runHelper(config.calendar.appPath, []);
|
|
5276
|
+
const today = /* @__PURE__ */ new Map();
|
|
5277
|
+
for (const event of facts.events) {
|
|
5278
|
+
if (typeof event.calendarId === "string") today.set(event.calendarId, (today.get(event.calendarId) ?? 0) + 1);
|
|
5279
|
+
}
|
|
5280
|
+
return facts.calendars.map(({ id, title, source }) => ({ title, source, eventsToday: today.get(id) ?? 0 }));
|
|
5281
|
+
},
|
|
5282
|
+
checkSettings: (values) => {
|
|
5283
|
+
const applied = applySettingsChange(readSources(), values);
|
|
5284
|
+
return "error" in applied ? applied.error : null;
|
|
5285
|
+
},
|
|
5286
|
+
saveSettings: (values) => saveSettingsValues(values)
|
|
5287
|
+
});
|
|
3825
5288
|
let codexWarnings = [];
|
|
3826
5289
|
const codexBins = /* @__PURE__ */ new Set();
|
|
3827
5290
|
if (config.agent.cli === "codex") codexBins.add(config.agent.binPath);
|
|
@@ -3831,12 +5294,34 @@ async function startServer(env, options = {}) {
|
|
|
3831
5294
|
for (const warning of codexWarnings) console.warn(`[daily-focus] ${warning}`);
|
|
3832
5295
|
if (codexWarnings.length > 0) void broadcast();
|
|
3833
5296
|
});
|
|
5297
|
+
async function startRequestedBrief() {
|
|
5298
|
+
if (!agent.enabled || !await firstBriefPending(config) || !await onboarding.takeBriefRequest()) return;
|
|
5299
|
+
await agent.run("hand").catch((err) => {
|
|
5300
|
+
console.error(`[daily-focus] could not start the first brief: ${err.message}`);
|
|
5301
|
+
});
|
|
5302
|
+
}
|
|
5303
|
+
function saveSettingsValues(values) {
|
|
5304
|
+
const saved = settingsWrites.then(async () => {
|
|
5305
|
+
const sources = readSources();
|
|
5306
|
+
const applied = applySettingsChange(sources, values);
|
|
5307
|
+
if ("error" in applied) return applied.error;
|
|
5308
|
+
const changed = JSON.stringify(Object.entries(applied.settings).sort()) !== JSON.stringify(Object.entries(sources.settings).sort());
|
|
5309
|
+
if (changed) {
|
|
5310
|
+
await saveSettings(sources.settingsPath, applied.settings);
|
|
5311
|
+
requestRestart();
|
|
5312
|
+
}
|
|
5313
|
+
return null;
|
|
5314
|
+
});
|
|
5315
|
+
settingsWrites = saved.catch(() => {
|
|
5316
|
+
});
|
|
5317
|
+
return saved;
|
|
5318
|
+
}
|
|
3834
5319
|
function requestRestart() {
|
|
3835
5320
|
restart = options.onRestart ? "waiting" : "manual";
|
|
3836
5321
|
restartWhenIdle();
|
|
3837
5322
|
}
|
|
3838
5323
|
function restartWhenIdle() {
|
|
3839
|
-
if (restart !== "waiting" || restarting || agent.busy || assistant.busy) return;
|
|
5324
|
+
if (restart !== "waiting" || restarting || agent.busy || assistant.busy || onboarding.busy) return;
|
|
3840
5325
|
restarting = true;
|
|
3841
5326
|
setImmediate(() => options.onRestart?.());
|
|
3842
5327
|
}
|
|
@@ -3848,11 +5333,9 @@ async function startServer(env, options = {}) {
|
|
|
3848
5333
|
// A link that can't be read is no reason to withhold the page.
|
|
3849
5334
|
storeLinkWarning(config).catch(() => null)
|
|
3850
5335
|
]);
|
|
3851
|
-
const
|
|
3852
|
-
const setup = await setupState(config, {
|
|
3853
|
-
profileChosen: Boolean(layerEnv(sources).DAILY_FOCUS_PROFILE?.trim())
|
|
3854
|
-
});
|
|
5336
|
+
const setup = await setupState(config);
|
|
3855
5337
|
const pulls = board.view(actions, now);
|
|
5338
|
+
const onboardingState = await onboarding.view(setup);
|
|
3856
5339
|
return {
|
|
3857
5340
|
...state,
|
|
3858
5341
|
board: pulls,
|
|
@@ -3862,6 +5345,7 @@ async function startServer(env, options = {}) {
|
|
|
3862
5345
|
setupWarnings: [...configWarnings, ...codexWarnings, ...linkWarning ? [linkWarning] : []],
|
|
3863
5346
|
restart,
|
|
3864
5347
|
setup,
|
|
5348
|
+
onboarding: onboardingState,
|
|
3865
5349
|
assetVersion,
|
|
3866
5350
|
storeId
|
|
3867
5351
|
};
|
|
@@ -3907,7 +5391,13 @@ data: ${payload}
|
|
|
3907
5391
|
basename(tempPathFor(config.timeZoneFile)),
|
|
3908
5392
|
// Appended by this process, which broadcasts on its own.
|
|
3909
5393
|
basename(config.assistantLogFile),
|
|
3910
|
-
basename(config.agentLogFile)
|
|
5394
|
+
basename(config.agentLogFile),
|
|
5395
|
+
// Rewritten by the runner before every run; nothing on the page shows it.
|
|
5396
|
+
basename(config.agentSettingsFile),
|
|
5397
|
+
basename(tempPathFor(config.agentSettingsFile)),
|
|
5398
|
+
// Rewritten by the setup screen, which broadcasts on its own.
|
|
5399
|
+
basename(config.onboardingFile),
|
|
5400
|
+
basename(tempPathFor(config.onboardingFile))
|
|
3911
5401
|
]
|
|
3912
5402
|
}
|
|
3913
5403
|
);
|
|
@@ -3920,6 +5410,8 @@ data: ${payload}
|
|
|
3920
5410
|
});
|
|
3921
5411
|
await assistant.start();
|
|
3922
5412
|
await agent.start();
|
|
5413
|
+
await onboarding.start(await firstBriefPending(config));
|
|
5414
|
+
await startRequestedBrief();
|
|
3923
5415
|
let ticks = Promise.resolve();
|
|
3924
5416
|
function clockTick() {
|
|
3925
5417
|
ticks = ticks.then(tickOnce).catch((err) => {
|
|
@@ -4218,6 +5710,40 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4218
5710
|
void broadcast(state);
|
|
4219
5711
|
return;
|
|
4220
5712
|
}
|
|
5713
|
+
if (path.startsWith("/api/onboarding/") && req.method === "POST") {
|
|
5714
|
+
const body = await readJsonObject(req, res);
|
|
5715
|
+
if (!body) return;
|
|
5716
|
+
const step = typeof body.step === "string" ? body.step : "";
|
|
5717
|
+
let refused2 = null;
|
|
5718
|
+
if (path === "/api/onboarding/answer") {
|
|
5719
|
+
refused2 = await onboarding.answer(step, body.answer);
|
|
5720
|
+
} else if (path === "/api/onboarding/revise") {
|
|
5721
|
+
refused2 = typeof body.text === "string" && body.text.trim() !== "" ? await onboarding.revise(step, body.text, body.draft) : '"text" is required: what to change';
|
|
5722
|
+
} else if (path === "/api/onboarding/retry") {
|
|
5723
|
+
refused2 = await onboarding.retry(step);
|
|
5724
|
+
} else if (path === "/api/onboarding/detect") {
|
|
5725
|
+
await onboarding.detect();
|
|
5726
|
+
} else if (path === "/api/onboarding/finish") {
|
|
5727
|
+
const finished = await onboarding.finish();
|
|
5728
|
+
if ("error" in finished) refused2 = finished.error;
|
|
5729
|
+
else if (!finished.restart) await startRequestedBrief();
|
|
5730
|
+
} else {
|
|
5731
|
+
sendJSON(res, 404, { error: `no such setup action: ${path}` });
|
|
5732
|
+
return;
|
|
5733
|
+
}
|
|
5734
|
+
if (refused2) {
|
|
5735
|
+
sendJSON(res, 409, { error: refused2 });
|
|
5736
|
+
return;
|
|
5737
|
+
}
|
|
5738
|
+
const state = await buildState();
|
|
5739
|
+
sendState(res, state);
|
|
5740
|
+
void broadcast(state);
|
|
5741
|
+
return;
|
|
5742
|
+
}
|
|
5743
|
+
if (path === "/api/onboarding/sources" && req.method === "GET") {
|
|
5744
|
+
sendJSON(res, 200, await onboarding.previewSources());
|
|
5745
|
+
return;
|
|
5746
|
+
}
|
|
4221
5747
|
if (path === "/api/settings" && req.method === "GET") {
|
|
4222
5748
|
sendJSON(res, 200, { ...describeSettings(readSources()), restart });
|
|
4223
5749
|
return;
|
|
@@ -4230,20 +5756,7 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4230
5756
|
sendJSON(res, 400, { error: '"values" must be an object of settings' });
|
|
4231
5757
|
return;
|
|
4232
5758
|
}
|
|
4233
|
-
const
|
|
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;
|
|
5759
|
+
const refused2 = await saveSettingsValues(values);
|
|
4247
5760
|
if (refused2) {
|
|
4248
5761
|
sendJSON(res, 400, { error: refused2 });
|
|
4249
5762
|
return;
|
|
@@ -4283,8 +5796,8 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4283
5796
|
if (path === "/api/focus/objective" && req.method === "POST") {
|
|
4284
5797
|
const body = await readJsonObject(req, res);
|
|
4285
5798
|
if (!body) return;
|
|
4286
|
-
const { objective, blocker } = body;
|
|
4287
|
-
for (const [name, value] of [["objective",
|
|
5799
|
+
const { objective: objective2, blocker } = body;
|
|
5800
|
+
for (const [name, value] of [["objective", objective2], ["blocker", blocker]]) {
|
|
4288
5801
|
if (value !== void 0 && value !== null && typeof value !== "string") {
|
|
4289
5802
|
sendJSON(res, 400, { error: `"${name}" must be text, or null to clear it` });
|
|
4290
5803
|
return;
|
|
@@ -4293,7 +5806,7 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4293
5806
|
const current = await readEditable(config.focusFile);
|
|
4294
5807
|
const next = setFocusFields(
|
|
4295
5808
|
current.text,
|
|
4296
|
-
{ objective:
|
|
5809
|
+
{ objective: objective2 ?? null, blocker: blocker ?? null },
|
|
4297
5810
|
FOCUS_TEMPLATE
|
|
4298
5811
|
);
|
|
4299
5812
|
const result = await saveEditable(config.focusFile, next, current.version);
|
|
@@ -4310,10 +5823,10 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4310
5823
|
try {
|
|
4311
5824
|
const facts = await runHelper(config.calendar.appPath, []);
|
|
4312
5825
|
const seen = /* @__PURE__ */ new Set();
|
|
4313
|
-
const
|
|
5826
|
+
const calendars2 = facts.calendars.filter((entry) => !seen.has(`${entry.title}
|
|
4314
5827
|
${entry.source}`) && seen.add(`${entry.title}
|
|
4315
5828
|
${entry.source}`)).map(({ title, source }) => ({ title, source })).sort((a, b) => a.title.localeCompare(b.title));
|
|
4316
|
-
sendJSON(res, 200, { calendars });
|
|
5829
|
+
sendJSON(res, 200, { calendars: calendars2 });
|
|
4317
5830
|
} catch (err) {
|
|
4318
5831
|
sendJSON(res, 409, { error: err.message });
|
|
4319
5832
|
}
|
|
@@ -4352,6 +5865,7 @@ ${entry.source}`)).map(({ title, source }) => ({ title, source })).sort((a, b) =
|
|
|
4352
5865
|
tickets.stop();
|
|
4353
5866
|
await assistant.close();
|
|
4354
5867
|
await agent.close();
|
|
5868
|
+
await onboarding.close();
|
|
4355
5869
|
stopWatching();
|
|
4356
5870
|
stopWatchingAssets();
|
|
4357
5871
|
for (const res of subscribers) res.end();
|