daily-focus 1.1.1-dev.3 → 1.2.0-dev.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -23
- package/SETUP.md +153 -38
- package/dist/{audit-B5GKGH2P.js → audit-F26VGPOO.js} +6 -5
- package/dist/{chunk-NWYOCDUU.js → chunk-4DZEBUPU.js} +9 -7
- package/dist/{chunk-C6FXWHGT.js → chunk-CPJ6LOVC.js} +123 -282
- package/dist/chunk-EV44BC2F.js +1020 -0
- package/dist/{chunk-AUBJ2IDQ.js → chunk-GVLNDQVQ.js} +2 -2
- package/dist/{chunk-YWHADSKR.js → chunk-HVCCIZGA.js} +1 -1
- package/dist/cli.js +9 -8
- package/dist/{demo-AMK3Y4JZ.js → demo-UMGG3VKY.js} +135 -32
- package/dist/{init-YGOPPELJ.js → init-MFPXASX3.js} +1 -9
- package/dist/{seed-57RJGU4Y.js → seed-S32JX5MW.js} +3 -3
- package/dist/{server-5HCA62YY.js → server-EDPYEZCP.js} +2088 -564
- package/dist/{service-63U4WBUH.js → service-XPFMYIHR.js} +1 -1
- package/package.json +2 -2
- package/prompts/README.md +8 -7
- package/prompts/assistant.md +14 -3
- package/prompts/morning-brief-personal.md +1 -25
- package/prompts/morning-brief-work.md +8 -33
- package/prompts/onboarding.md +41 -0
- package/public/app.js +1084 -747
- package/public/style.css +770 -261
- package/dist/chunk-GFCWPYEI.js +0 -456
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ADAPTERS,
|
|
3
|
-
Assistant
|
|
4
|
-
|
|
3
|
+
Assistant,
|
|
4
|
+
AutoAsk,
|
|
5
|
+
CodexGate,
|
|
6
|
+
codexLatestModel,
|
|
7
|
+
codexVersionWarning,
|
|
8
|
+
computerUseOff,
|
|
9
|
+
countBoard,
|
|
10
|
+
refusalFor,
|
|
11
|
+
resolveBoard
|
|
12
|
+
} from "./chunk-EV44BC2F.js";
|
|
5
13
|
import {
|
|
6
14
|
FOCUS_TEMPLATE,
|
|
7
15
|
sourcesTemplate
|
|
@@ -9,22 +17,20 @@ import {
|
|
|
9
17
|
import {
|
|
10
18
|
TimeZoneKeeper,
|
|
11
19
|
validZone
|
|
12
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-GVLNDQVQ.js";
|
|
13
21
|
import {
|
|
14
22
|
describeLink,
|
|
15
23
|
linkStore,
|
|
16
24
|
storeLinkWarning
|
|
17
25
|
} from "./chunk-QKT2FAB5.js";
|
|
18
26
|
import {
|
|
27
|
+
ACTION_TYPES,
|
|
19
28
|
Store,
|
|
20
29
|
canonicalId,
|
|
21
30
|
foldActionLog,
|
|
22
|
-
reconcileSession,
|
|
23
|
-
startSession,
|
|
24
|
-
stopSession,
|
|
25
31
|
tempPathFor,
|
|
26
32
|
writeJsonAtomic
|
|
27
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-CPJ6LOVC.js";
|
|
28
34
|
import {
|
|
29
35
|
CLI_NAMES,
|
|
30
36
|
LAUNCH_SETTINGS,
|
|
@@ -32,15 +38,15 @@ import {
|
|
|
32
38
|
briefWritingTools,
|
|
33
39
|
describeTime,
|
|
34
40
|
envSources,
|
|
41
|
+
expandHome,
|
|
35
42
|
laterISO,
|
|
36
43
|
layerEnv,
|
|
37
44
|
loadConfig,
|
|
38
45
|
nextScheduledRun,
|
|
39
|
-
parseISO,
|
|
40
46
|
readDotEnv,
|
|
41
47
|
resolveSchedule,
|
|
42
48
|
scheduledRunDue
|
|
43
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-4DZEBUPU.js";
|
|
44
50
|
import {
|
|
45
51
|
PACKAGED,
|
|
46
52
|
command,
|
|
@@ -52,19 +58,259 @@ import {
|
|
|
52
58
|
import { createServer } from "node:http";
|
|
53
59
|
import { createReadStream } from "node:fs";
|
|
54
60
|
import { stat as stat4 } from "node:fs/promises";
|
|
55
|
-
import { basename, extname, join as
|
|
61
|
+
import { basename, extname, join as join6, normalize, resolve, sep } from "node:path";
|
|
56
62
|
import { fileURLToPath } from "node:url";
|
|
57
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
|
+
|
|
58
304
|
// src/editable.ts
|
|
59
305
|
import { createHash } from "node:crypto";
|
|
60
|
-
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
306
|
+
import { readFile as readFile2, rename, writeFile } from "node:fs/promises";
|
|
61
307
|
var ABSENT = "absent";
|
|
62
308
|
function versionOf(text) {
|
|
63
309
|
return text === null ? ABSENT : createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
64
310
|
}
|
|
65
311
|
async function readEditable(path) {
|
|
66
312
|
try {
|
|
67
|
-
const text = await
|
|
313
|
+
const text = await readFile2(path, "utf8");
|
|
68
314
|
return { text, version: versionOf(text) };
|
|
69
315
|
} catch (error) {
|
|
70
316
|
if (error.code === "ENOENT") return { text: null, version: ABSENT };
|
|
@@ -125,7 +371,7 @@ ${lines.join("\n")}
|
|
|
125
371
|
}
|
|
126
372
|
|
|
127
373
|
// src/board.ts
|
|
128
|
-
import { readFile as
|
|
374
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
129
375
|
|
|
130
376
|
// src/failure.ts
|
|
131
377
|
var MAX_DETAIL = 4e3;
|
|
@@ -219,9 +465,9 @@ function reasonFor(code) {
|
|
|
219
465
|
}
|
|
220
466
|
|
|
221
467
|
// src/github.ts
|
|
222
|
-
import { execFile } from "node:child_process";
|
|
223
|
-
import { promisify } from "node:util";
|
|
224
|
-
var
|
|
468
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
469
|
+
import { promisify as promisify2 } from "node:util";
|
|
470
|
+
var run2 = promisify2(execFile3);
|
|
225
471
|
var GH_TIMEOUT_MS = 15e3;
|
|
226
472
|
var FETCH_TIMEOUT_MS = 3e4;
|
|
227
473
|
var GRAPHQL_URL = "https://api.github.com/graphql";
|
|
@@ -244,7 +490,7 @@ var GhAuthError = class extends Error {
|
|
|
244
490
|
};
|
|
245
491
|
async function gh(ghPath, args) {
|
|
246
492
|
try {
|
|
247
|
-
const { stdout, stderr } = await
|
|
493
|
+
const { stdout, stderr } = await run2(ghPath, args, { env: GH_ENV, timeout: GH_TIMEOUT_MS, maxBuffer: 1 << 20 });
|
|
248
494
|
return { stdout, stderr, code: 0 };
|
|
249
495
|
} catch (err) {
|
|
250
496
|
const failure = err;
|
|
@@ -497,9 +743,9 @@ function checkSlot(node) {
|
|
|
497
743
|
return `check\0${node.name ?? ""}`;
|
|
498
744
|
}
|
|
499
745
|
function checkGeneration(node) {
|
|
500
|
-
const
|
|
501
|
-
const sequence =
|
|
502
|
-
if (typeof sequence === "number") return [sequence,
|
|
746
|
+
const run5 = node.checkSuite?.workflowRun;
|
|
747
|
+
const sequence = run5?.runNumber ?? run5?.databaseId;
|
|
748
|
+
if (typeof sequence === "number") return [sequence, run5?.runAttempt ?? 0];
|
|
503
749
|
const at = node.completedAt ?? node.startedAt ?? node.createdAt ?? null;
|
|
504
750
|
const time = at ? Date.parse(at) : Number.NaN;
|
|
505
751
|
return [Number.isFinite(time) ? time : 0, 0];
|
|
@@ -815,6 +1061,7 @@ async function fetchPulls(token, scope, expectedLogin, signal) {
|
|
|
815
1061
|
for (const [nodeId, head] of checks.heads) {
|
|
816
1062
|
const pull = found.get(nodeId);
|
|
817
1063
|
if (!pull) continue;
|
|
1064
|
+
pull.headSha = head.oid;
|
|
818
1065
|
applyChecks(pull, head.rollup);
|
|
819
1066
|
if (head.rollup && rollupTruncated(head.rollup)) {
|
|
820
1067
|
partial.push({ id: nodeId, pull, oid: head.oid, rollup: head.rollup });
|
|
@@ -869,131 +1116,84 @@ async function fetchPulls(token, scope, expectedLogin, signal) {
|
|
|
869
1116
|
}
|
|
870
1117
|
return { login, pulls, warnings, orgs, rateLimitRemaining };
|
|
871
1118
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
var
|
|
875
|
-
var
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1119
|
+
var TICKET_PAGE_SIZE = 25;
|
|
1120
|
+
var TICKET_MAX_PAGES = 12;
|
|
1121
|
+
var TICKET_COMMITS = 100;
|
|
1122
|
+
var TICKET_SEARCH_QUERY = `
|
|
1123
|
+
query($q: String!, $first: Int!, $after: String) {
|
|
1124
|
+
search(query: $q, type: ISSUE, first: $first, after: $after) {
|
|
1125
|
+
pageInfo { hasNextPage endCursor }
|
|
1126
|
+
nodes {
|
|
1127
|
+
... on PullRequest {
|
|
1128
|
+
number
|
|
1129
|
+
title
|
|
1130
|
+
url
|
|
1131
|
+
state
|
|
1132
|
+
isDraft
|
|
1133
|
+
closedAt
|
|
1134
|
+
mergedAt
|
|
1135
|
+
headRefName
|
|
1136
|
+
body
|
|
1137
|
+
repository { nameWithOwner }
|
|
1138
|
+
commits(first: ${TICKET_COMMITS}) { nodes { commit { message } } }
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}`;
|
|
1143
|
+
function buildTicketSearchQueries(scope, closedSince) {
|
|
1144
|
+
return [
|
|
1145
|
+
["is:pr", "author:@me", "is:open", ...scope].join(" "),
|
|
1146
|
+
["is:pr", "author:@me", "is:closed", `closed:>=${closedSince}`, ...scope].join(" ")
|
|
1147
|
+
];
|
|
883
1148
|
}
|
|
884
|
-
function
|
|
885
|
-
if (
|
|
886
|
-
if (
|
|
887
|
-
if (
|
|
1149
|
+
function toLinkedState(state, isDraft) {
|
|
1150
|
+
if (state === "MERGED") return "merged";
|
|
1151
|
+
if (state === "CLOSED") return "closed";
|
|
1152
|
+
if (state === "OPEN") return isDraft === true ? "draft" : "open";
|
|
888
1153
|
return null;
|
|
889
1154
|
}
|
|
890
|
-
function
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
const
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
const theirMove = others !== null && (you === null || others.at > you);
|
|
916
|
-
const approval = newest(pr, "APPROVED");
|
|
917
|
-
const afterApproval = others !== null && approval !== void 0 && others.at > approval.at;
|
|
918
|
-
const approved = decision === "APPROVED" && !afterApproval;
|
|
919
|
-
if (theirMove && others && !approved) {
|
|
920
|
-
reasons.push({ kind: "activity", login: others.login, at: others.at, activity: others.kind });
|
|
921
|
-
}
|
|
922
|
-
if (reasons.length > 0) {
|
|
923
|
-
const since = theirMove && others ? others.at : pr.updatedAt;
|
|
924
|
-
return { court: "you", decision, reasons, stale: false, nudge: false, since };
|
|
925
|
-
}
|
|
926
|
-
const waitingSince = laterISO(pr.readyAt, you) ?? pr.createdAt;
|
|
927
|
-
const settled = (court, reason) => ({
|
|
928
|
-
court,
|
|
929
|
-
decision,
|
|
930
|
-
reasons: [reason],
|
|
931
|
-
stale: false,
|
|
932
|
-
nudge: false,
|
|
933
|
-
since: waitingSince
|
|
934
|
-
});
|
|
935
|
-
if (decision !== "REVIEW_REQUIRED") {
|
|
936
|
-
const gate = matchMergeGates(pr, gates);
|
|
937
|
-
if (gate.length > 0) return settled("gate", { kind: "merge-gate", checks: gate });
|
|
938
|
-
const pending = pr.pendingChecks ?? [];
|
|
939
|
-
const running = { kind: "checks-pending" };
|
|
940
|
-
if (pending.length > 0) running.checks = pending;
|
|
941
|
-
if (mergeState === "BLOCKED" && pending.length === 0) {
|
|
942
|
-
return settled("blocked", { kind: "merge-blocked" });
|
|
943
|
-
}
|
|
944
|
-
if (mergeState === "BLOCKED" || mergeState === "UNSTABLE") return settled("checks", running);
|
|
945
|
-
if (mergeState === "UNKNOWN") return settled("checks", { kind: "mergeability-unknown" });
|
|
946
|
-
if (approved) {
|
|
947
|
-
if (mergeState === "CLEAN" || mergeState === "HAS_HOOKS") {
|
|
948
|
-
return { court: "ready", decision, reasons: [], stale: false, nudge: false, since: approval?.at ?? pr.updatedAt };
|
|
1155
|
+
function normalizeTicketPull(raw) {
|
|
1156
|
+
const repo = raw.repository?.nameWithOwner;
|
|
1157
|
+
const url = safeUrl(raw.url);
|
|
1158
|
+
const state = toLinkedState(raw.state, raw.isDraft);
|
|
1159
|
+
if (typeof raw.number !== "number" || typeof repo !== "string" || typeof raw.title !== "string" || !url || !state) {
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
const closedAt = state === "merged" ? raw.mergedAt ?? raw.closedAt ?? null : state === "closed" ? raw.closedAt ?? null : null;
|
|
1163
|
+
const text = [raw.title, raw.headRefName ?? "", raw.body ?? ""];
|
|
1164
|
+
for (const node of raw.commits?.nodes ?? []) {
|
|
1165
|
+
const message = node?.commit?.message;
|
|
1166
|
+
if (typeof message === "string") text.push(message);
|
|
1167
|
+
}
|
|
1168
|
+
return { pull: { repo, number: raw.number, title: raw.title, url, state, closedAt }, text };
|
|
1169
|
+
}
|
|
1170
|
+
async function fetchTicketPulls(token, scope, closedSince, signal) {
|
|
1171
|
+
const pulls = [];
|
|
1172
|
+
let complete = true;
|
|
1173
|
+
for (const q of buildTicketSearchQueries(scope, closedSince)) {
|
|
1174
|
+
let after = null;
|
|
1175
|
+
for (let page = 0; ; page++) {
|
|
1176
|
+
const result = await graphql(token, TICKET_SEARCH_QUERY, { q, first: TICKET_PAGE_SIZE, after }, signal);
|
|
1177
|
+
const fatal = result.errors.filter((error) => !isSamlError(error));
|
|
1178
|
+
if (result.data?.search === void 0 && fatal.length > 0) {
|
|
1179
|
+
throw new GitHubRequestError(`GitHub search failed: ${fatal.map((error) => error.message).join("; ")}`);
|
|
949
1180
|
}
|
|
950
|
-
if (
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1181
|
+
if (result.errors.length > 0) complete = false;
|
|
1182
|
+
for (const node of result.data?.search?.nodes ?? []) {
|
|
1183
|
+
const found = node ? normalizeTicketPull(node) : null;
|
|
1184
|
+
if (found) pulls.push(found);
|
|
1185
|
+
else complete = false;
|
|
1186
|
+
}
|
|
1187
|
+
const pageInfo = result.data?.search?.pageInfo;
|
|
1188
|
+
if (!pageInfo?.hasNextPage || !pageInfo.endCursor) break;
|
|
1189
|
+
if (page === TICKET_MAX_PAGES - 1) {
|
|
1190
|
+
complete = false;
|
|
1191
|
+
break;
|
|
956
1192
|
}
|
|
1193
|
+
after = pageInfo.endCursor;
|
|
957
1194
|
}
|
|
958
1195
|
}
|
|
959
|
-
|
|
960
|
-
const waited = now.getTime() - (ms(quietSince) ?? now.getTime());
|
|
961
|
-
return { court: "reviewers", decision, reasons, stale: false, nudge: waited >= NUDGE_AFTER_MS, since: waitingSince };
|
|
962
|
-
}
|
|
963
|
-
function resolveBoard(pulls, actions, now, gates = []) {
|
|
964
|
-
const folded = foldActionLog(actions, now);
|
|
965
|
-
const rows = pulls.map((pr) => {
|
|
966
|
-
const log = folded.get(canonicalId(pr.id));
|
|
967
|
-
const notes = log?.notes ?? [];
|
|
968
|
-
const lastNoteAt = notes.length > 0 ? notes[notes.length - 1].at : null;
|
|
969
|
-
const verdict = judge(pr, now, lastNoteAt, gates);
|
|
970
|
-
const row = { ...pr, ...verdict, status: log?.status === "snoozed" ? "snoozed" : "open", notes };
|
|
971
|
-
if (row.status === "snoozed" && log?.snoozedUntil) row.snoozedUntil = log.snoozedUntil;
|
|
972
|
-
return row;
|
|
973
|
-
});
|
|
974
|
-
return rows.sort((a, b) => {
|
|
975
|
-
const court = courtOrder(a.court) - courtOrder(b.court);
|
|
976
|
-
if (court !== 0) return court;
|
|
977
|
-
if (a.since !== b.since) return a.since < b.since ? -1 : 1;
|
|
978
|
-
return a.id < b.id ? -1 : 1;
|
|
979
|
-
});
|
|
980
|
-
}
|
|
981
|
-
function countBoard(rows) {
|
|
982
|
-
const counts = {
|
|
983
|
-
you: 0,
|
|
984
|
-
ready: 0,
|
|
985
|
-
reviewers: 0,
|
|
986
|
-
gate: 0,
|
|
987
|
-
blocked: 0,
|
|
988
|
-
checks: 0,
|
|
989
|
-
draft: 0,
|
|
990
|
-
parked: 0
|
|
991
|
-
};
|
|
992
|
-
for (const row of rows) {
|
|
993
|
-
if (row.status === "snoozed") counts.parked++;
|
|
994
|
-
else counts[row.court]++;
|
|
995
|
-
}
|
|
996
|
-
return counts;
|
|
1196
|
+
return { pulls, complete };
|
|
997
1197
|
}
|
|
998
1198
|
|
|
999
1199
|
// src/board.ts
|
|
@@ -1222,7 +1422,7 @@ ${details[i] || "(nothing more)"}`).join("\n\n");
|
|
|
1222
1422
|
let lowestRemaining = null;
|
|
1223
1423
|
const results = await Promise.allSettled(
|
|
1224
1424
|
identities.map(
|
|
1225
|
-
(
|
|
1425
|
+
(identity3) => this.#deps.fetch(identity3.token, this.#config.github.scope, identity3.account.login, this.#abort.signal)
|
|
1226
1426
|
)
|
|
1227
1427
|
);
|
|
1228
1428
|
results.forEach((result, i) => {
|
|
@@ -1357,6 +1557,9 @@ function normalizeStoredPull(raw) {
|
|
|
1357
1557
|
autoMerge: raw.autoMerge === true,
|
|
1358
1558
|
requestedReviewers: strings(raw.requestedReviewers),
|
|
1359
1559
|
reviews,
|
|
1560
|
+
// Kept, or a restart would hand the assistant's answers ahead about failing
|
|
1561
|
+
// checks a key the live read doesn't, and they would be asked again.
|
|
1562
|
+
headSha: typeof raw.headSha === "string" ? raw.headSha : null,
|
|
1360
1563
|
lastActivityByYou: typeof raw.lastActivityByYou === "string" ? raw.lastActivityByYou : null,
|
|
1361
1564
|
lastActivityByOthers
|
|
1362
1565
|
};
|
|
@@ -1364,7 +1567,7 @@ function normalizeStoredPull(raw) {
|
|
|
1364
1567
|
async function readPullsFile(path) {
|
|
1365
1568
|
let text;
|
|
1366
1569
|
try {
|
|
1367
|
-
text = await
|
|
1570
|
+
text = await readFile3(path, "utf8");
|
|
1368
1571
|
} catch {
|
|
1369
1572
|
return null;
|
|
1370
1573
|
}
|
|
@@ -1388,12 +1591,12 @@ async function readPullsFile(path) {
|
|
|
1388
1591
|
}
|
|
1389
1592
|
|
|
1390
1593
|
// src/calendar.ts
|
|
1391
|
-
import { execFile as
|
|
1392
|
-
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";
|
|
1393
1596
|
import { tmpdir } from "node:os";
|
|
1394
|
-
import { join } from "node:path";
|
|
1395
|
-
import { promisify as
|
|
1396
|
-
var
|
|
1597
|
+
import { join as join3 } from "node:path";
|
|
1598
|
+
import { promisify as promisify3 } from "node:util";
|
|
1599
|
+
var run3 = promisify3(execFile4);
|
|
1397
1600
|
var HELPER_TIMEOUT_MS = 2e4;
|
|
1398
1601
|
var POLL_MS = 150;
|
|
1399
1602
|
var DECLINED = 3;
|
|
@@ -1416,17 +1619,17 @@ function parseCalendarFacts(raw) {
|
|
|
1416
1619
|
if (typeof raw !== "object" || raw === null) return null;
|
|
1417
1620
|
const obj = raw;
|
|
1418
1621
|
if (str2(obj.error)) return null;
|
|
1419
|
-
const
|
|
1622
|
+
const calendars2 = [];
|
|
1420
1623
|
if (Array.isArray(obj.calendars)) {
|
|
1421
1624
|
for (const entry of obj.calendars) {
|
|
1422
1625
|
const id = str2(entry?.id);
|
|
1423
1626
|
const title = str2(entry?.title);
|
|
1424
|
-
if (id && title)
|
|
1627
|
+
if (id && title) calendars2.push({ id, title, source: str2(entry?.source) ?? "" });
|
|
1425
1628
|
}
|
|
1426
1629
|
}
|
|
1427
1630
|
return {
|
|
1428
1631
|
generatedAt: str2(obj.generatedAt) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1429
|
-
calendars,
|
|
1632
|
+
calendars: calendars2,
|
|
1430
1633
|
events: Array.isArray(obj.events) ? obj.events : []
|
|
1431
1634
|
};
|
|
1432
1635
|
}
|
|
@@ -1483,17 +1686,17 @@ var CalendarHelperError = class extends Error {
|
|
|
1483
1686
|
};
|
|
1484
1687
|
async function runHelper(appPath, selfAddresses) {
|
|
1485
1688
|
try {
|
|
1486
|
-
await
|
|
1689
|
+
await stat2(appPath);
|
|
1487
1690
|
} catch {
|
|
1488
1691
|
throw new CalendarHelperError(
|
|
1489
1692
|
`the calendar helper isn't built at ${appPath} \u2014 run ${command("build-calendar")}, or point DAILY_FOCUS_CALENDAR_APP at an existing build`
|
|
1490
1693
|
);
|
|
1491
1694
|
}
|
|
1492
|
-
const out =
|
|
1695
|
+
const out = join3(tmpdir(), `daily-focus-calendar-${process.pid}-${Date.now()}.json`);
|
|
1493
1696
|
try {
|
|
1494
1697
|
try {
|
|
1495
1698
|
const zone = process.env.TZ ? ["--env", `TZ=${process.env.TZ}`] : [];
|
|
1496
|
-
await
|
|
1699
|
+
await run3("/usr/bin/open", ["-n", "-a", appPath, ...zone, "--args", out, selfAddresses.join(",")], {
|
|
1497
1700
|
timeout: HELPER_TIMEOUT_MS
|
|
1498
1701
|
});
|
|
1499
1702
|
} catch (err) {
|
|
@@ -1504,7 +1707,7 @@ async function runHelper(appPath, selfAddresses) {
|
|
|
1504
1707
|
for (; ; ) {
|
|
1505
1708
|
let text = null;
|
|
1506
1709
|
try {
|
|
1507
|
-
text = await
|
|
1710
|
+
text = await readFile4(out, "utf8");
|
|
1508
1711
|
} catch {
|
|
1509
1712
|
text = null;
|
|
1510
1713
|
}
|
|
@@ -1528,7 +1731,7 @@ async function runHelper(appPath, selfAddresses) {
|
|
|
1528
1731
|
}
|
|
1529
1732
|
|
|
1530
1733
|
// src/calendarboard.ts
|
|
1531
|
-
import { readFile as
|
|
1734
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
1532
1735
|
var MAX_BACKOFF_MS2 = 15 * 6e4;
|
|
1533
1736
|
var realDeps2 = { run: runHelper };
|
|
1534
1737
|
var CalendarBoard = class {
|
|
@@ -1680,19 +1883,100 @@ var CalendarBoard = class {
|
|
|
1680
1883
|
};
|
|
1681
1884
|
async function readCalendarFile(path) {
|
|
1682
1885
|
try {
|
|
1683
|
-
return parseCalendarFacts(JSON.parse(await
|
|
1886
|
+
return parseCalendarFacts(JSON.parse(await readFile5(path, "utf8")));
|
|
1684
1887
|
} catch {
|
|
1685
1888
|
return null;
|
|
1686
1889
|
}
|
|
1687
1890
|
}
|
|
1688
1891
|
|
|
1689
1892
|
// src/ticketboard.ts
|
|
1690
|
-
import { readFile as
|
|
1893
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
1691
1894
|
|
|
1692
1895
|
// src/jira.ts
|
|
1693
|
-
import { execFile as
|
|
1694
|
-
import { promisify as
|
|
1695
|
-
|
|
1896
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
1897
|
+
import { promisify as promisify4 } from "node:util";
|
|
1898
|
+
|
|
1899
|
+
// src/linked.ts
|
|
1900
|
+
var CLOSED_LOOKBACK_DAYS = 60;
|
|
1901
|
+
var KEYISH = /(?<![A-Za-z0-9])([A-Za-z][A-Za-z0-9_]*)-(\d+)/g;
|
|
1902
|
+
function issueKeysIn(text) {
|
|
1903
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1904
|
+
for (const match of text.matchAll(KEYISH)) {
|
|
1905
|
+
const parts = match[1].split("_");
|
|
1906
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1907
|
+
const project = parts.slice(i).join("_");
|
|
1908
|
+
if (/^[A-Za-z]/.test(project)) keys.add(`${project.toUpperCase()}-${match[2]}`);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
return keys;
|
|
1912
|
+
}
|
|
1913
|
+
var realDeps3 = { token: ghToken, search: fetchTicketPulls };
|
|
1914
|
+
var CONSEQUENCE = "so a merged pull request counts only once Jira has it";
|
|
1915
|
+
async function readLinkedPulls(github, now, signal, deps = realDeps3) {
|
|
1916
|
+
const closedSince = new Date(now.getTime() - CLOSED_LOOKBACK_DAYS * 864e5).toISOString().slice(0, 10);
|
|
1917
|
+
const logins = github.accounts.length > 0 ? [...github.accounts] : [null];
|
|
1918
|
+
const results = await Promise.allSettled(
|
|
1919
|
+
logins.map(async (login) => deps.search(await deps.token(github.ghPath, login), github.scope, closedSince, signal))
|
|
1920
|
+
);
|
|
1921
|
+
if (results.every((result) => result.status === "rejected")) {
|
|
1922
|
+
throw results[0].reason;
|
|
1923
|
+
}
|
|
1924
|
+
const warnings = [];
|
|
1925
|
+
let complete = true;
|
|
1926
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1927
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1928
|
+
results.forEach((result, i) => {
|
|
1929
|
+
const who = logins[i] ? ` as ${logins[i]}` : "";
|
|
1930
|
+
if (result.status === "rejected") {
|
|
1931
|
+
complete = false;
|
|
1932
|
+
warnings.push(`Could not ask GitHub${who} about these tickets' pull requests (${result.reason.message}), ${CONSEQUENCE}.`);
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
if (!result.value.complete) {
|
|
1936
|
+
complete = false;
|
|
1937
|
+
warnings.push(`GitHub did not list every pull request${who}, ${CONSEQUENCE}.`);
|
|
1938
|
+
}
|
|
1939
|
+
for (const { pull, text } of result.value.pulls) {
|
|
1940
|
+
if (seen.has(pull.url)) continue;
|
|
1941
|
+
seen.add(pull.url);
|
|
1942
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1943
|
+
for (const piece of text) for (const key of issueKeysIn(piece)) keys.add(key);
|
|
1944
|
+
for (const key of keys) byKey.set(key, [...byKey.get(key) ?? [], pull]);
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
return { byKey, complete, warnings };
|
|
1948
|
+
}
|
|
1949
|
+
function isOpen(pull) {
|
|
1950
|
+
return pull.state === "open" || pull.state === "draft";
|
|
1951
|
+
}
|
|
1952
|
+
function byRecency(a, b) {
|
|
1953
|
+
if (isOpen(a) !== isOpen(b)) return isOpen(a) ? -1 : 1;
|
|
1954
|
+
return (b.closedAt ?? "").localeCompare(a.closedAt ?? "");
|
|
1955
|
+
}
|
|
1956
|
+
function linkedTo(ticket, linked) {
|
|
1957
|
+
return linked?.byKey.get(ticket.key.toUpperCase()) ?? [];
|
|
1958
|
+
}
|
|
1959
|
+
function needsJiraCount(ticket, linked) {
|
|
1960
|
+
const found = linkedTo(ticket, linked);
|
|
1961
|
+
return Boolean(linked?.complete) && found.length > 0 && !found.some(isOpen) && ticket.hasAnyPr && !ticket.allPrsClosed;
|
|
1962
|
+
}
|
|
1963
|
+
function reconcile(ticket, linked, jiraCount) {
|
|
1964
|
+
const found = [...linkedTo(ticket, linked)].sort(byRecency);
|
|
1965
|
+
if (found.length === 0) return ticket;
|
|
1966
|
+
const jira = { hasAnyPr: ticket.hasAnyPr, hasOpenPr: ticket.hasOpenPr, allPrsClosed: ticket.allPrsClosed };
|
|
1967
|
+
let reading = jira;
|
|
1968
|
+
if (found.some(isOpen)) {
|
|
1969
|
+
reading = { hasAnyPr: true, hasOpenPr: true, allPrsClosed: false };
|
|
1970
|
+
} else if (linked?.complete && !ticket.allPrsClosed) {
|
|
1971
|
+
const known = ticket.hasAnyPr ? jiraCount : 0;
|
|
1972
|
+
if (known !== null && found.length >= known) reading = { hasAnyPr: true, hasOpenPr: false, allPrsClosed: true };
|
|
1973
|
+
}
|
|
1974
|
+
const changed = reading.hasAnyPr !== jira.hasAnyPr || reading.hasOpenPr !== jira.hasOpenPr || reading.allPrsClosed !== jira.allPrsClosed;
|
|
1975
|
+
return { ...ticket, ...reading, pulls: found, ...changed ? { fromGitHub: true } : {} };
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// src/jira.ts
|
|
1979
|
+
var run4 = promisify4(execFile5);
|
|
1696
1980
|
var ACLI_TIMEOUT_MS = 45e3;
|
|
1697
1981
|
var PAGE_LIMIT = 200;
|
|
1698
1982
|
var ACLI_ENV = { ...process.env, NO_COLOR: "1" };
|
|
@@ -1728,7 +2012,7 @@ var JiraTransitionError = class extends Error {
|
|
|
1728
2012
|
};
|
|
1729
2013
|
async function acli(acliPath, args) {
|
|
1730
2014
|
try {
|
|
1731
|
-
const { stdout, stderr } = await
|
|
2015
|
+
const { stdout, stderr } = await run4(acliPath, args, {
|
|
1732
2016
|
env: ACLI_ENV,
|
|
1733
2017
|
timeout: ACLI_TIMEOUT_MS,
|
|
1734
2018
|
// A hundred tickets of raw Jira issue JSON measured 135 KB; this is room
|
|
@@ -1874,13 +2158,13 @@ function jsonObjectAt(text, from) {
|
|
|
1874
2158
|
}
|
|
1875
2159
|
return null;
|
|
1876
2160
|
}
|
|
1877
|
-
function readDevPullRequests(
|
|
1878
|
-
if (typeof
|
|
1879
|
-
const marker =
|
|
2161
|
+
function readDevPullRequests(field2) {
|
|
2162
|
+
if (typeof field2 !== "string") return null;
|
|
2163
|
+
const marker = field2.indexOf("json=");
|
|
1880
2164
|
if (marker === -1) return null;
|
|
1881
|
-
const opens =
|
|
2165
|
+
const opens = field2.indexOf("{", marker);
|
|
1882
2166
|
if (opens === -1) return null;
|
|
1883
|
-
const body = jsonObjectAt(
|
|
2167
|
+
const body = jsonObjectAt(field2, opens);
|
|
1884
2168
|
if (body === null) return null;
|
|
1885
2169
|
let parsed;
|
|
1886
2170
|
try {
|
|
@@ -1975,22 +2259,23 @@ async function fetchTickets(acliPath, opts) {
|
|
|
1975
2259
|
if (unreadable > 0) {
|
|
1976
2260
|
warnings.push(`Skipped ${unreadable} work ${unreadable === 1 ? "item" : "items"} Jira described in a way this board could not read.`);
|
|
1977
2261
|
}
|
|
1978
|
-
const
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
DEV_CONCURRENCY,
|
|
1982
|
-
(ticket) => fetchDevPullRequests(acliPath, ticket.key).then(
|
|
1983
|
-
(dev) => ({ asked: true, dev }),
|
|
1984
|
-
() => ({ asked: false, dev: null })
|
|
1985
|
-
)
|
|
2262
|
+
const readPanel = (ticket) => fetchDevPullRequests(acliPath, ticket.key).then(
|
|
2263
|
+
(dev) => ({ asked: true, dev }),
|
|
2264
|
+
() => ({ asked: false, dev: null })
|
|
1986
2265
|
);
|
|
2266
|
+
const candidates = tickets.filter((ticket) => ticket.hasAnyPr && !ticket.hasOpenPr);
|
|
2267
|
+
const reading = mapLimit(candidates, DEV_CONCURRENCY, readPanel);
|
|
2268
|
+
const linked = await opts.linked ?? null;
|
|
2269
|
+
const panels = await reading;
|
|
2270
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1987
2271
|
const unanswered = [];
|
|
1988
2272
|
const unaccounted = [];
|
|
1989
2273
|
candidates.forEach((ticket, i) => {
|
|
1990
|
-
const { asked, dev } = panels[i];
|
|
2274
|
+
const { asked: asked2, dev } = panels[i];
|
|
2275
|
+
if (dev) counts.set(ticket.key, dev.count);
|
|
1991
2276
|
if (allPullRequestsClosed(dev)) {
|
|
1992
2277
|
ticket.allPrsClosed = true;
|
|
1993
|
-
} else if (!
|
|
2278
|
+
} else if (!asked2) {
|
|
1994
2279
|
unanswered.push(ticket.key);
|
|
1995
2280
|
} else if (dev === null) {
|
|
1996
2281
|
unaccounted.push(ticket.key);
|
|
@@ -1998,19 +2283,30 @@ async function fetchTickets(acliPath, opts) {
|
|
|
1998
2283
|
ticket.hasOpenPr = true;
|
|
1999
2284
|
}
|
|
2000
2285
|
});
|
|
2001
|
-
const
|
|
2286
|
+
const asked = new Set(candidates);
|
|
2287
|
+
const behind = tickets.filter((ticket) => !asked.has(ticket) && needsJiraCount(ticket, linked));
|
|
2288
|
+
const extra = await mapLimit(behind, DEV_CONCURRENCY, readPanel);
|
|
2289
|
+
behind.forEach((ticket, i) => {
|
|
2290
|
+
const dev = extra[i].dev;
|
|
2291
|
+
if (dev) counts.set(ticket.key, dev.count);
|
|
2292
|
+
});
|
|
2293
|
+
const reconciled = tickets.map((ticket) => reconcile(ticket, linked, counts.get(ticket.key) ?? null));
|
|
2294
|
+
const undecided = new Set(reconciled.filter((ticket) => !ticket.hasOpenPr && !ticket.allPrsClosed).map((ticket) => ticket.key));
|
|
2295
|
+
const unansweredNow = unanswered.filter((key) => undecided.has(key));
|
|
2296
|
+
const unaccountedNow = unaccounted.filter((key) => undecided.has(key));
|
|
2297
|
+
const listed2 = (keys2) => keys2.map((key) => {
|
|
2002
2298
|
const url = browseUrl(opts.site, key);
|
|
2003
2299
|
return url === null ? key : `[${key}](${url})`;
|
|
2004
2300
|
}).join(", ");
|
|
2005
2301
|
const heldOut = (keys2) => `${keys2.length === 1 ? "it is" : "they are"} held out of "no open pull requests left" rather than guessed at`;
|
|
2006
|
-
if (
|
|
2302
|
+
if (unansweredNow.length > 0) {
|
|
2007
2303
|
warnings.push(
|
|
2008
|
-
`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.`
|
|
2009
2305
|
);
|
|
2010
2306
|
}
|
|
2011
|
-
if (
|
|
2307
|
+
if (unaccountedNow.length > 0) {
|
|
2012
2308
|
warnings.push(
|
|
2013
|
-
`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.`
|
|
2014
2310
|
);
|
|
2015
2311
|
}
|
|
2016
2312
|
if (!opts.site) {
|
|
@@ -2027,7 +2323,7 @@ async function fetchTickets(acliPath, opts) {
|
|
|
2027
2323
|
);
|
|
2028
2324
|
}
|
|
2029
2325
|
return {
|
|
2030
|
-
tickets,
|
|
2326
|
+
tickets: reconciled,
|
|
2031
2327
|
statuses: statusesByProject(base, ...finished.map((batch) => batch ?? [])),
|
|
2032
2328
|
warnings
|
|
2033
2329
|
};
|
|
@@ -2099,16 +2395,16 @@ function readTransitionReport(stdout, key, status) {
|
|
|
2099
2395
|
}
|
|
2100
2396
|
|
|
2101
2397
|
// src/tickets.ts
|
|
2102
|
-
var
|
|
2398
|
+
var COURT_ORDER = ["settled", "started", "idle"];
|
|
2103
2399
|
function ticketCourtOrder(court) {
|
|
2104
|
-
return
|
|
2400
|
+
return COURT_ORDER.indexOf(court);
|
|
2105
2401
|
}
|
|
2106
2402
|
function inStatus(ticket, statuses) {
|
|
2107
2403
|
const status = ticket.workflowStatus.trim().toLowerCase();
|
|
2108
2404
|
if (status === "") return false;
|
|
2109
|
-
return statuses.some((
|
|
2405
|
+
return statuses.some((listed2) => listed2.trim().toLowerCase() === status);
|
|
2110
2406
|
}
|
|
2111
|
-
function
|
|
2407
|
+
function judge(ticket, holdStatuses = []) {
|
|
2112
2408
|
if (ticket.statusCategory === "done") return null;
|
|
2113
2409
|
if (ticket.hasAnyPr && !ticket.hasOpenPr && ticket.allPrsClosed) return "settled";
|
|
2114
2410
|
if (ticket.statusCategory === "new" && ticket.hasOpenPr) return "started";
|
|
@@ -2119,7 +2415,7 @@ function resolveTickets(tickets, actions, now, holdStatuses = []) {
|
|
|
2119
2415
|
const folded = foldActionLog(actions, now);
|
|
2120
2416
|
const rows = [];
|
|
2121
2417
|
for (const ticket of tickets) {
|
|
2122
|
-
const court =
|
|
2418
|
+
const court = judge(ticket, holdStatuses);
|
|
2123
2419
|
if (!court) continue;
|
|
2124
2420
|
const log = folded.get(canonicalId(ticket.id));
|
|
2125
2421
|
const row = {
|
|
@@ -2148,7 +2444,13 @@ function countTickets(rows) {
|
|
|
2148
2444
|
|
|
2149
2445
|
// src/ticketboard.ts
|
|
2150
2446
|
var MAX_BACKOFF_MS3 = 30 * 6e4;
|
|
2151
|
-
var
|
|
2447
|
+
var GITHUB_DEADLINE_MS = 9e4;
|
|
2448
|
+
var realDeps4 = {
|
|
2449
|
+
identity: acliIdentity,
|
|
2450
|
+
fetch: fetchTickets,
|
|
2451
|
+
transition: transitionTicket,
|
|
2452
|
+
linked: (github, now, signal) => readLinkedPulls(github, now, signal)
|
|
2453
|
+
};
|
|
2152
2454
|
var TicketBoard = class {
|
|
2153
2455
|
#config;
|
|
2154
2456
|
#deps;
|
|
@@ -2165,7 +2467,8 @@ var TicketBoard = class {
|
|
|
2165
2467
|
#audience = 0;
|
|
2166
2468
|
#stopped = false;
|
|
2167
2469
|
#lastReadAt = 0;
|
|
2168
|
-
|
|
2470
|
+
#abort = new AbortController();
|
|
2471
|
+
constructor(config, onChange, deps = realDeps4) {
|
|
2169
2472
|
this.#config = config;
|
|
2170
2473
|
this.#deps = deps;
|
|
2171
2474
|
this.#onChange = onChange;
|
|
@@ -2179,10 +2482,12 @@ var TicketBoard = class {
|
|
|
2179
2482
|
this.#file = await readTicketsFile(this.#config.ticketsFile);
|
|
2180
2483
|
await this.refresh();
|
|
2181
2484
|
}
|
|
2485
|
+
/** Stop reading, and cancel a GitHub request in flight so nothing lands after close. */
|
|
2182
2486
|
stop() {
|
|
2183
2487
|
this.#stopped = true;
|
|
2184
2488
|
if (this.#timer) clearTimeout(this.#timer);
|
|
2185
2489
|
this.#timer = null;
|
|
2490
|
+
this.#abort.abort();
|
|
2186
2491
|
}
|
|
2187
2492
|
/** How many browsers are watching. Polling pauses at zero. */
|
|
2188
2493
|
setAudience(count) {
|
|
@@ -2288,24 +2593,40 @@ var TicketBoard = class {
|
|
|
2288
2593
|
}
|
|
2289
2594
|
async #read() {
|
|
2290
2595
|
const { acliPath, projects, site: configuredSite } = this.#config.jira;
|
|
2291
|
-
let
|
|
2596
|
+
let identity3;
|
|
2292
2597
|
try {
|
|
2293
|
-
|
|
2598
|
+
identity3 = await this.#deps.identity(acliPath);
|
|
2294
2599
|
} catch (err) {
|
|
2295
2600
|
this.#reason = { message: `${err.message}, or set DAILY_FOCUS_JIRA=off`, detail: errorDetail(err) };
|
|
2296
2601
|
this.#streak = extend(this.#streak);
|
|
2297
2602
|
return;
|
|
2298
2603
|
}
|
|
2299
|
-
const site = configuredSite ??
|
|
2604
|
+
const site = configuredSite ?? identity3.site;
|
|
2605
|
+
const githubWarnings = [];
|
|
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(
|
|
2607
|
+
(read) => {
|
|
2608
|
+
githubWarnings.push(...read.warnings);
|
|
2609
|
+
return read;
|
|
2610
|
+
},
|
|
2611
|
+
(err) => {
|
|
2612
|
+
githubWarnings.push(
|
|
2613
|
+
`Could not ask GitHub about these tickets' pull requests (${err.message}), so they are as Jira has them, which can be days behind.`
|
|
2614
|
+
);
|
|
2615
|
+
return null;
|
|
2616
|
+
}
|
|
2617
|
+
) : Promise.resolve(null);
|
|
2300
2618
|
try {
|
|
2301
|
-
const
|
|
2619
|
+
const fetched = await this.#deps.fetch(acliPath, { projects, site, linked });
|
|
2620
|
+
await linked;
|
|
2621
|
+
const { tickets, statuses } = fetched;
|
|
2622
|
+
const warnings = [...fetched.warnings, ...githubWarnings];
|
|
2302
2623
|
this.#reason = null;
|
|
2303
2624
|
this.#warnings = warnings;
|
|
2304
2625
|
this.#streak = null;
|
|
2305
2626
|
this.#file = {
|
|
2306
2627
|
version: 1,
|
|
2307
2628
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2308
|
-
account:
|
|
2629
|
+
account: identity3.account,
|
|
2309
2630
|
projects: [...projects],
|
|
2310
2631
|
warnings,
|
|
2311
2632
|
tickets,
|
|
@@ -2334,7 +2655,7 @@ function normalizeStoredTicket(raw) {
|
|
|
2334
2655
|
const { key, url, workflowStatus, statusCategory, issueType, summary } = raw;
|
|
2335
2656
|
if (typeof key !== "string" || key === "") return null;
|
|
2336
2657
|
const site = typeof url === "string" ? /^https:\/\/([^/]+)\/browse\//.exec(url)?.[1] ?? null : null;
|
|
2337
|
-
|
|
2658
|
+
const ticket = toTicket(
|
|
2338
2659
|
{
|
|
2339
2660
|
key,
|
|
2340
2661
|
fields: {
|
|
@@ -2351,11 +2672,30 @@ function normalizeStoredTicket(raw) {
|
|
|
2351
2672
|
// files are exactly the ones whose settled court counted drafts as merges.
|
|
2352
2673
|
raw.allPrsClosed === true
|
|
2353
2674
|
);
|
|
2675
|
+
if (!ticket) return null;
|
|
2676
|
+
const pulls = storedPulls(raw.pulls);
|
|
2677
|
+
if (pulls.length > 0) {
|
|
2678
|
+
ticket.pulls = pulls;
|
|
2679
|
+
if (raw.fromGitHub === true) ticket.fromGitHub = true;
|
|
2680
|
+
}
|
|
2681
|
+
return ticket;
|
|
2682
|
+
}
|
|
2683
|
+
var PULL_STATES = ["open", "draft", "merged", "closed"];
|
|
2684
|
+
function storedPulls(value) {
|
|
2685
|
+
if (!Array.isArray(value)) return [];
|
|
2686
|
+
return value.flatMap((entry) => {
|
|
2687
|
+
if (!isRecord3(entry)) return [];
|
|
2688
|
+
const { repo, number, title, url, state, closedAt } = entry;
|
|
2689
|
+
if (typeof repo !== "string" || typeof number !== "number" || typeof title !== "string") return [];
|
|
2690
|
+
if (typeof url !== "string" || !/^https?:\/\//i.test(url)) return [];
|
|
2691
|
+
if (typeof state !== "string" || !PULL_STATES.includes(state)) return [];
|
|
2692
|
+
return [{ repo, number, title, url, state, closedAt: typeof closedAt === "string" ? closedAt : null }];
|
|
2693
|
+
});
|
|
2354
2694
|
}
|
|
2355
2695
|
async function readTicketsFile(path) {
|
|
2356
2696
|
let text;
|
|
2357
2697
|
try {
|
|
2358
|
-
text = await
|
|
2698
|
+
text = await readFile6(path, "utf8");
|
|
2359
2699
|
} catch {
|
|
2360
2700
|
return null;
|
|
2361
2701
|
}
|
|
@@ -2388,8 +2728,8 @@ async function readTicketsFile(path) {
|
|
|
2388
2728
|
// src/agent.ts
|
|
2389
2729
|
import { spawn } from "node:child_process";
|
|
2390
2730
|
import { randomUUID } from "node:crypto";
|
|
2391
|
-
import { appendFile, mkdir, readFile as
|
|
2392
|
-
import { homedir } from "node:os";
|
|
2731
|
+
import { appendFile, mkdir, readFile as readFile7 } from "node:fs/promises";
|
|
2732
|
+
import { homedir as homedir2 } from "node:os";
|
|
2393
2733
|
import { createInterface } from "node:readline";
|
|
2394
2734
|
var RUN_TIMEOUT_MS = 60 * 6e4;
|
|
2395
2735
|
var TURN_TIMEOUT_MS = 10 * 6e4;
|
|
@@ -2419,7 +2759,7 @@ function followUpPrompt(question) {
|
|
|
2419
2759
|
question.trim()
|
|
2420
2760
|
].join("\n");
|
|
2421
2761
|
}
|
|
2422
|
-
function displayPath(path, home =
|
|
2762
|
+
function displayPath(path, home = homedir2()) {
|
|
2423
2763
|
return path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
|
|
2424
2764
|
}
|
|
2425
2765
|
function foldAgentLog(text) {
|
|
@@ -2451,11 +2791,11 @@ function foldAgentLog(text) {
|
|
|
2451
2791
|
});
|
|
2452
2792
|
continue;
|
|
2453
2793
|
}
|
|
2454
|
-
const
|
|
2455
|
-
if (!
|
|
2794
|
+
const run5 = runs.get(record.run);
|
|
2795
|
+
if (!run5) continue;
|
|
2456
2796
|
if (record.event === "asked") {
|
|
2457
2797
|
if (typeof record.turn !== "string" || typeof record.question !== "string") continue;
|
|
2458
|
-
|
|
2798
|
+
run5.turns.push({
|
|
2459
2799
|
id: record.turn,
|
|
2460
2800
|
question: record.question,
|
|
2461
2801
|
startedAt: record.at,
|
|
@@ -2467,7 +2807,7 @@ function foldAgentLog(text) {
|
|
|
2467
2807
|
continue;
|
|
2468
2808
|
}
|
|
2469
2809
|
if (record.event === "replied" || record.event === "turn-failed" || record.event === "turn-aborted") {
|
|
2470
|
-
const turn =
|
|
2810
|
+
const turn = run5.turns.find((candidate) => candidate.id === record.turn);
|
|
2471
2811
|
if (!turn) continue;
|
|
2472
2812
|
turn.reply = typeof record.reply === "string" ? record.reply : turn.reply;
|
|
2473
2813
|
turn.endedAt = record.at;
|
|
@@ -2482,18 +2822,18 @@ function foldAgentLog(text) {
|
|
|
2482
2822
|
}
|
|
2483
2823
|
continue;
|
|
2484
2824
|
}
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2825
|
+
run5.sessionId = typeof record.sessionId === "string" ? record.sessionId : run5.sessionId;
|
|
2826
|
+
run5.report = typeof record.report === "string" ? record.report : run5.report;
|
|
2827
|
+
run5.messages = Array.isArray(record.messages) ? record.messages.filter((message) => typeof message === "string") : run5.report ? [run5.report] : [];
|
|
2828
|
+
run5.endedAt = record.at;
|
|
2489
2829
|
if (record.event === "finished") {
|
|
2490
|
-
|
|
2830
|
+
run5.status = "done";
|
|
2491
2831
|
} else if (record.event === "failed") {
|
|
2492
|
-
|
|
2493
|
-
|
|
2832
|
+
run5.status = "failed";
|
|
2833
|
+
run5.error = record.error;
|
|
2494
2834
|
} else if (record.event === "aborted") {
|
|
2495
|
-
|
|
2496
|
-
|
|
2835
|
+
run5.status = "aborted";
|
|
2836
|
+
run5.error = record.reason;
|
|
2497
2837
|
}
|
|
2498
2838
|
}
|
|
2499
2839
|
return [...runs.values()].sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
@@ -2506,10 +2846,13 @@ var AgentRunner = class {
|
|
|
2506
2846
|
config;
|
|
2507
2847
|
hooks;
|
|
2508
2848
|
spawnFn;
|
|
2509
|
-
|
|
2849
|
+
/** Refuses a Codex too old to hold to the connector rules; see `codex.ts`. */
|
|
2850
|
+
codexGate;
|
|
2851
|
+
constructor(config, hooks, spawnFn = spawn, execFn) {
|
|
2510
2852
|
this.config = config;
|
|
2511
2853
|
this.hooks = hooks;
|
|
2512
2854
|
this.spawnFn = spawnFn;
|
|
2855
|
+
this.codexGate = new CodexGate(config.agent.binPath, execFn);
|
|
2513
2856
|
}
|
|
2514
2857
|
get enabled() {
|
|
2515
2858
|
return this.config.agent.cli !== null;
|
|
@@ -2523,26 +2866,26 @@ var AgentRunner = class {
|
|
|
2523
2866
|
this.loaded = true;
|
|
2524
2867
|
let text = "";
|
|
2525
2868
|
try {
|
|
2526
|
-
text = await
|
|
2869
|
+
text = await readFile7(this.config.agentLogFile, "utf8");
|
|
2527
2870
|
} catch {
|
|
2528
2871
|
return;
|
|
2529
2872
|
}
|
|
2530
2873
|
this.runs = foldAgentLog(text);
|
|
2531
2874
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2532
2875
|
const reason = "the dashboard restarted while this was running";
|
|
2533
|
-
for (const
|
|
2534
|
-
if (
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
await this.append({ event: "aborted", run:
|
|
2876
|
+
for (const run5 of this.runs) {
|
|
2877
|
+
if (run5.status === "running") {
|
|
2878
|
+
run5.status = "aborted";
|
|
2879
|
+
run5.error = reason;
|
|
2880
|
+
run5.endedAt = at;
|
|
2881
|
+
await this.append({ event: "aborted", run: run5.id, sessionId: run5.sessionId, reason, report: run5.report, messages: run5.messages, at });
|
|
2539
2882
|
}
|
|
2540
|
-
for (const turn of
|
|
2883
|
+
for (const turn of run5.turns) {
|
|
2541
2884
|
if (turn.status !== "running") continue;
|
|
2542
2885
|
turn.status = "aborted";
|
|
2543
2886
|
turn.error = reason;
|
|
2544
2887
|
turn.endedAt = at;
|
|
2545
|
-
await this.append({ event: "turn-aborted", run:
|
|
2888
|
+
await this.append({ event: "turn-aborted", run: run5.id, turn: turn.id, reason, reply: turn.reply, at });
|
|
2546
2889
|
}
|
|
2547
2890
|
}
|
|
2548
2891
|
}
|
|
@@ -2555,7 +2898,7 @@ var AgentRunner = class {
|
|
|
2555
2898
|
return this.runs.at(-1) ?? null;
|
|
2556
2899
|
}
|
|
2557
2900
|
view(now = /* @__PURE__ */ new Date()) {
|
|
2558
|
-
const runs = this.runs.slice(-RUNS_SHOWN).reverse().map((
|
|
2901
|
+
const runs = this.runs.slice(-RUNS_SHOWN).reverse().map((run5) => ({ ...run5, resumable: this.resumable(run5) }));
|
|
2559
2902
|
return {
|
|
2560
2903
|
enabled: this.enabled,
|
|
2561
2904
|
cli: this.config.agent.cli,
|
|
@@ -2601,17 +2944,17 @@ var AgentRunner = class {
|
|
|
2601
2944
|
* decided on.
|
|
2602
2945
|
*/
|
|
2603
2946
|
async run(trigger = "hand", now = /* @__PURE__ */ new Date()) {
|
|
2604
|
-
const
|
|
2605
|
-
if (!
|
|
2947
|
+
const cli2 = this.config.agent.cli;
|
|
2948
|
+
if (!cli2) throw new Error("running the morning agent is off; set DAILY_FOCUS_AGENT to codex or claude");
|
|
2606
2949
|
if (this.running) {
|
|
2607
2950
|
throw new Error(
|
|
2608
2951
|
this.running.turn ? "the morning agent is still answering a question" : "the morning agent is already running"
|
|
2609
2952
|
);
|
|
2610
2953
|
}
|
|
2611
2954
|
await this.start();
|
|
2612
|
-
const
|
|
2955
|
+
const run5 = {
|
|
2613
2956
|
id: randomUUID(),
|
|
2614
|
-
cli,
|
|
2957
|
+
cli: cli2,
|
|
2615
2958
|
trigger,
|
|
2616
2959
|
sessionId: null,
|
|
2617
2960
|
startedAt: now.toISOString(),
|
|
@@ -2623,38 +2966,40 @@ var AgentRunner = class {
|
|
|
2623
2966
|
turns: [],
|
|
2624
2967
|
resumable: false
|
|
2625
2968
|
};
|
|
2626
|
-
this.runs.push(
|
|
2627
|
-
await this.append({ event: "started", run:
|
|
2628
|
-
const adapter = ADAPTERS[
|
|
2969
|
+
this.runs.push(run5);
|
|
2970
|
+
await this.append({ event: "started", run: run5.id, cli: cli2, trigger, at: run5.startedAt });
|
|
2971
|
+
const adapter = ADAPTERS[cli2];
|
|
2629
2972
|
const args = adapter.args({
|
|
2630
2973
|
prompt: agentPrompt(displayPath(this.config.promptFile)),
|
|
2631
2974
|
sessionId: null,
|
|
2632
2975
|
model: this.config.agent.model,
|
|
2633
2976
|
effort: this.config.agent.effort,
|
|
2634
2977
|
tools: [...briefWritingTools(), ...this.config.agent.tools],
|
|
2635
|
-
denyEdits: false
|
|
2978
|
+
denyEdits: false,
|
|
2979
|
+
drafts: false,
|
|
2980
|
+
codexConfig: []
|
|
2636
2981
|
});
|
|
2637
|
-
await this.spawnInto(
|
|
2638
|
-
return
|
|
2982
|
+
await this.spawnInto(run5, null, args, RUN_TIMEOUT_MS, "gave up after an hour");
|
|
2983
|
+
return run5;
|
|
2639
2984
|
}
|
|
2640
2985
|
/**
|
|
2641
2986
|
* Ask a finished run a question, in its own session. Returns once the process
|
|
2642
2987
|
* is running; the answer arrives through `onChange` and the next state.
|
|
2643
2988
|
*/
|
|
2644
2989
|
async ask(runId, question) {
|
|
2645
|
-
const
|
|
2646
|
-
if (!
|
|
2990
|
+
const cli2 = this.config.agent.cli;
|
|
2991
|
+
if (!cli2) throw new Error("running the morning agent is off; set DAILY_FOCUS_AGENT to codex or claude");
|
|
2647
2992
|
await this.start();
|
|
2648
|
-
const
|
|
2649
|
-
if (!
|
|
2993
|
+
const run5 = this.runs.find((candidate) => candidate.id === runId);
|
|
2994
|
+
if (!run5) throw new Error("no such run");
|
|
2650
2995
|
if (this.running) {
|
|
2651
2996
|
throw new Error(
|
|
2652
2997
|
this.running.turn ? "the morning agent is still answering a question" : "the morning agent is still running"
|
|
2653
2998
|
);
|
|
2654
2999
|
}
|
|
2655
|
-
if (!this.resumable(
|
|
3000
|
+
if (!this.resumable(run5)) {
|
|
2656
3001
|
throw new Error(
|
|
2657
|
-
|
|
3002
|
+
run5.sessionId === null ? "this run left no session to continue" : `this run was made by ${run5.cli}, and the agent is ${cli2} now`
|
|
2658
3003
|
);
|
|
2659
3004
|
}
|
|
2660
3005
|
const text = question.trim();
|
|
@@ -2668,19 +3013,21 @@ var AgentRunner = class {
|
|
|
2668
3013
|
reply: "",
|
|
2669
3014
|
error: null
|
|
2670
3015
|
};
|
|
2671
|
-
|
|
2672
|
-
await this.append({ event: "asked", run:
|
|
2673
|
-
const adapter = ADAPTERS[
|
|
3016
|
+
run5.turns.push(turn);
|
|
3017
|
+
await this.append({ event: "asked", run: run5.id, turn: turn.id, question: text, at: turn.startedAt });
|
|
3018
|
+
const adapter = ADAPTERS[cli2];
|
|
2674
3019
|
const args = adapter.args({
|
|
2675
3020
|
prompt: followUpPrompt(text),
|
|
2676
|
-
sessionId:
|
|
3021
|
+
sessionId: run5.sessionId,
|
|
2677
3022
|
model: this.config.agent.model,
|
|
2678
3023
|
effort: this.config.agent.effort,
|
|
2679
3024
|
// The sources, and not the brief: a chat is given nothing to write.
|
|
2680
3025
|
tools: this.config.agent.tools,
|
|
2681
|
-
denyEdits: true
|
|
3026
|
+
denyEdits: true,
|
|
3027
|
+
drafts: false,
|
|
3028
|
+
codexConfig: []
|
|
2682
3029
|
});
|
|
2683
|
-
await this.spawnInto(
|
|
3030
|
+
await this.spawnInto(run5, turn, args, TURN_TIMEOUT_MS, "gave up after ten minutes");
|
|
2684
3031
|
return turn;
|
|
2685
3032
|
}
|
|
2686
3033
|
/**
|
|
@@ -2688,10 +3035,15 @@ var AgentRunner = class {
|
|
|
2688
3035
|
* run when there is none. The store in both cases: Claude Code files a
|
|
2689
3036
|
* session under the directory it was started in, and looks for it there.
|
|
2690
3037
|
*/
|
|
2691
|
-
async spawnInto(
|
|
2692
|
-
const
|
|
2693
|
-
const adapter = ADAPTERS[
|
|
3038
|
+
async spawnInto(run5, turn, args, timeoutMs, timeoutReason) {
|
|
3039
|
+
const cli2 = run5.cli;
|
|
3040
|
+
const adapter = ADAPTERS[cli2];
|
|
2694
3041
|
await mkdir(this.config.dataDir, { recursive: true });
|
|
3042
|
+
const refusal2 = cli2 === "codex" ? await this.codexGate.refusal() : null;
|
|
3043
|
+
if (refusal2) {
|
|
3044
|
+
await this.finish(run5, turn, { ok: false, error: refusal2 });
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
2695
3047
|
let child;
|
|
2696
3048
|
try {
|
|
2697
3049
|
child = this.spawnFn(this.config.agent.binPath, args, {
|
|
@@ -2701,12 +3053,12 @@ var AgentRunner = class {
|
|
|
2701
3053
|
env: process.env
|
|
2702
3054
|
});
|
|
2703
3055
|
} catch (err) {
|
|
2704
|
-
await this.finish(
|
|
3056
|
+
await this.finish(run5, turn, { ok: false, error: this.spawnError(err) });
|
|
2705
3057
|
return;
|
|
2706
3058
|
}
|
|
2707
3059
|
const timer = setTimeout(() => void this.stop(timeoutReason), timeoutMs);
|
|
2708
3060
|
timer.unref();
|
|
2709
|
-
const running = { run:
|
|
3061
|
+
const running = { run: run5, turn, child, timer, abortReason: null };
|
|
2710
3062
|
this.running = running;
|
|
2711
3063
|
this.hooks.onChange();
|
|
2712
3064
|
let done;
|
|
@@ -2720,15 +3072,15 @@ var AgentRunner = class {
|
|
|
2720
3072
|
lines.on("line", (line) => {
|
|
2721
3073
|
const event = adapter.parse(line);
|
|
2722
3074
|
if (!event) return;
|
|
2723
|
-
if (event.sessionId && !turn)
|
|
3075
|
+
if (event.sessionId && !turn) run5.sessionId = event.sessionId;
|
|
2724
3076
|
if (event.text) {
|
|
2725
3077
|
if (turn) {
|
|
2726
3078
|
turn.reply = turn.reply ? `${turn.reply}
|
|
2727
3079
|
|
|
2728
3080
|
${event.text}` : event.text;
|
|
2729
3081
|
} else {
|
|
2730
|
-
|
|
2731
|
-
|
|
3082
|
+
run5.messages.push(event.text);
|
|
3083
|
+
run5.report = event.text;
|
|
2732
3084
|
}
|
|
2733
3085
|
this.hooks.onChange();
|
|
2734
3086
|
}
|
|
@@ -2743,9 +3095,9 @@ ${event.text}` : event.text;
|
|
|
2743
3095
|
this.running = null;
|
|
2744
3096
|
const outcome = running.abortReason ? { ok: false, error: running.abortReason } : done ?? {
|
|
2745
3097
|
ok: false,
|
|
2746
|
-
error: `${
|
|
3098
|
+
error: `${cli2} exited with code ${code}${stderr.length ? `: ${stderr.join("").trim().split("\n").slice(-3).join(" ")}` : ""}`
|
|
2747
3099
|
};
|
|
2748
|
-
void this.finish(
|
|
3100
|
+
void this.finish(run5, turn, outcome, running.abortReason !== null);
|
|
2749
3101
|
});
|
|
2750
3102
|
}
|
|
2751
3103
|
/**
|
|
@@ -2762,45 +3114,45 @@ ${event.text}` : event.text;
|
|
|
2762
3114
|
async close() {
|
|
2763
3115
|
await this.stop("the dashboard shut down");
|
|
2764
3116
|
}
|
|
2765
|
-
resumable(
|
|
2766
|
-
return
|
|
3117
|
+
resumable(run5) {
|
|
3118
|
+
return run5.status !== "running" && run5.sessionId !== null && run5.cli === this.config.agent.cli;
|
|
2767
3119
|
}
|
|
2768
|
-
async finish(
|
|
3120
|
+
async finish(run5, turn, outcome, aborted = false) {
|
|
2769
3121
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2770
3122
|
if (turn) {
|
|
2771
3123
|
const reply = outcome.reply ?? turn.reply;
|
|
2772
3124
|
turn.reply = reply;
|
|
2773
3125
|
turn.endedAt = at;
|
|
2774
3126
|
if (outcome.ok) {
|
|
2775
|
-
await this.append({ event: "replied", run:
|
|
3127
|
+
await this.append({ event: "replied", run: run5.id, turn: turn.id, reply, at });
|
|
2776
3128
|
turn.status = "done";
|
|
2777
3129
|
} else if (aborted) {
|
|
2778
3130
|
turn.error = outcome.error ?? "stopped";
|
|
2779
|
-
await this.append({ event: "turn-aborted", run:
|
|
3131
|
+
await this.append({ event: "turn-aborted", run: run5.id, turn: turn.id, reason: turn.error, reply, at });
|
|
2780
3132
|
turn.status = "aborted";
|
|
2781
3133
|
} else {
|
|
2782
3134
|
turn.error = outcome.error ?? "failed";
|
|
2783
|
-
await this.append({ event: "turn-failed", run:
|
|
3135
|
+
await this.append({ event: "turn-failed", run: run5.id, turn: turn.id, error: turn.error, reply, at });
|
|
2784
3136
|
turn.status = "failed";
|
|
2785
3137
|
}
|
|
2786
3138
|
this.hooks.onChange();
|
|
2787
3139
|
return;
|
|
2788
3140
|
}
|
|
2789
|
-
const report = outcome.reply ??
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
const messages =
|
|
3141
|
+
const report = outcome.reply ?? run5.report;
|
|
3142
|
+
run5.report = report;
|
|
3143
|
+
run5.endedAt = at;
|
|
3144
|
+
const messages = run5.messages;
|
|
2793
3145
|
if (outcome.ok) {
|
|
2794
|
-
await this.append({ event: "finished", run:
|
|
2795
|
-
|
|
3146
|
+
await this.append({ event: "finished", run: run5.id, sessionId: run5.sessionId, report, messages, at });
|
|
3147
|
+
run5.status = "done";
|
|
2796
3148
|
} else if (aborted) {
|
|
2797
|
-
|
|
2798
|
-
await this.append({ event: "aborted", run:
|
|
2799
|
-
|
|
3149
|
+
run5.error = outcome.error ?? "stopped";
|
|
3150
|
+
await this.append({ event: "aborted", run: run5.id, sessionId: run5.sessionId, reason: run5.error, report, messages, at });
|
|
3151
|
+
run5.status = "aborted";
|
|
2800
3152
|
} else {
|
|
2801
|
-
|
|
2802
|
-
await this.append({ event: "failed", run:
|
|
2803
|
-
|
|
3153
|
+
run5.error = outcome.error ?? "failed";
|
|
3154
|
+
await this.append({ event: "failed", run: run5.id, sessionId: run5.sessionId, error: run5.error, report, messages, at });
|
|
3155
|
+
run5.status = "failed";
|
|
2804
3156
|
}
|
|
2805
3157
|
this.hooks.onChange();
|
|
2806
3158
|
}
|
|
@@ -2846,23 +3198,23 @@ function watchDataDir(dir, onChange, { debounceMs = 120, ignore = [] } = {}) {
|
|
|
2846
3198
|
}
|
|
2847
3199
|
|
|
2848
3200
|
// src/assets.ts
|
|
2849
|
-
import { readdir, stat as
|
|
2850
|
-
import { join as
|
|
3201
|
+
import { readdir as readdir2, stat as stat3 } from "node:fs/promises";
|
|
3202
|
+
import { join as join4 } from "node:path";
|
|
2851
3203
|
async function computeAssetVersion(dir) {
|
|
2852
3204
|
const parts = [];
|
|
2853
3205
|
async function walk(current) {
|
|
2854
3206
|
let entries;
|
|
2855
3207
|
try {
|
|
2856
|
-
entries = await
|
|
3208
|
+
entries = await readdir2(current, { withFileTypes: true });
|
|
2857
3209
|
} catch {
|
|
2858
3210
|
return;
|
|
2859
3211
|
}
|
|
2860
3212
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2861
|
-
const path =
|
|
3213
|
+
const path = join4(current, entry.name);
|
|
2862
3214
|
if (entry.isDirectory()) {
|
|
2863
3215
|
await walk(path);
|
|
2864
3216
|
} else {
|
|
2865
|
-
const info = await
|
|
3217
|
+
const info = await stat3(path);
|
|
2866
3218
|
parts.push(`${entry.name}:${info.size}:${info.mtimeMs}`);
|
|
2867
3219
|
}
|
|
2868
3220
|
}
|
|
@@ -2881,8 +3233,8 @@ var FAVICONS = {
|
|
|
2881
3233
|
work: `<path d="M34 30V20a8 8 0 0 1 8-8h16a8 8 0 0 1 8 8v10" fill="none" stroke="#2a78d6" stroke-width="9"/><path fill="#2a78d6" fill-rule="evenodd" d="M16 30h68a10 10 0 0 1 10 10v38a10 10 0 0 1-10 10H16A10 10 0 0 1 6 78V40a10 10 0 0 1 10-10ZM40 46v14h20V46Z"/>`,
|
|
2882
3234
|
personal: `<path fill="#1baf7a" fill-rule="evenodd" d="M50 8 96 48H84v44H16V48H4ZM41 92V66h18v26Z"/>`
|
|
2883
3235
|
};
|
|
2884
|
-
function faviconSvg(
|
|
2885
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">${FAVICONS[
|
|
3236
|
+
function faviconSvg(profile2) {
|
|
3237
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">${FAVICONS[profile2]}</svg>`;
|
|
2886
3238
|
}
|
|
2887
3239
|
|
|
2888
3240
|
// src/guard.ts
|
|
@@ -2914,31 +3266,540 @@ function refusal(req, listenHost) {
|
|
|
2914
3266
|
return null;
|
|
2915
3267
|
}
|
|
2916
3268
|
|
|
2917
|
-
// src/
|
|
2918
|
-
import {
|
|
2919
|
-
import {
|
|
2920
|
-
import {
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
3269
|
+
// src/onboarding.ts
|
|
3270
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
3271
|
+
import { mkdir as mkdir2, mkdtemp, readFile as readFile8, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
3272
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
3273
|
+
import { join as join5 } from "node:path";
|
|
3274
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
3275
|
+
|
|
3276
|
+
// src/questions.ts
|
|
3277
|
+
var INSTALL = {
|
|
3278
|
+
codex: { label: "Install Codex", url: "https://developers.openai.com/codex/cli" },
|
|
3279
|
+
claude: { label: "Install Claude Code", url: "https://code.claude.com/docs" }
|
|
3280
|
+
};
|
|
3281
|
+
function option(value, label, detail = null, group = null) {
|
|
3282
|
+
return { value, label, detail, group };
|
|
3283
|
+
}
|
|
3284
|
+
function choice(id, options, selected, extra = {}) {
|
|
3285
|
+
return { id, label: null, multiple: false, options, selected, custom: null, ...extra };
|
|
3286
|
+
}
|
|
3287
|
+
function field(id, label, value, extra = {}) {
|
|
3288
|
+
return { id, label, value, placeholder: null, help: null, ...extra };
|
|
3289
|
+
}
|
|
3290
|
+
function card(parts) {
|
|
3291
|
+
return { why: null, choices: [], fields: [], problem: null, ...parts };
|
|
3292
|
+
}
|
|
3293
|
+
function one(answer, id) {
|
|
3294
|
+
return answer.choices[id]?.[0] ?? null;
|
|
3295
|
+
}
|
|
3296
|
+
function str3(value) {
|
|
3297
|
+
return typeof value === "string" ? value.trim() : "";
|
|
3298
|
+
}
|
|
3299
|
+
function proposalOf(ctx, id) {
|
|
3300
|
+
const proposal = ctx.proposals[id];
|
|
3301
|
+
return typeof proposal === "object" && proposal !== null && !Array.isArray(proposal) ? proposal : null;
|
|
3302
|
+
}
|
|
3303
|
+
function listed(names) {
|
|
3304
|
+
if (names.length <= 1) return names.join("");
|
|
3305
|
+
return `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
|
|
3306
|
+
}
|
|
3307
|
+
function ticked(name) {
|
|
3308
|
+
return `\`${name.replaceAll("`", "'")}\``;
|
|
3309
|
+
}
|
|
3310
|
+
function binFor(ctx, cli2) {
|
|
3311
|
+
const found = cli2 ? ctx.clis?.[cli2] : null;
|
|
3312
|
+
return found?.path && !found.onPath ? found.path : null;
|
|
3313
|
+
}
|
|
3314
|
+
var cli = {
|
|
3315
|
+
id: "cli",
|
|
3316
|
+
title: () => "Which agent should write your brief?",
|
|
3317
|
+
topic: "your coding agent",
|
|
3318
|
+
keys: ["DAILY_FOCUS_AGENT"],
|
|
3319
|
+
later: { label: "Decide later", summary: "No agent yet: the boards work, and there is no brief until you choose one." },
|
|
3320
|
+
card(ctx) {
|
|
3321
|
+
if (!ctx.clis) return card({});
|
|
3322
|
+
const report = ctx.clis;
|
|
3323
|
+
const ready = CLI_PREFERENCE.filter((name) => report[name].ready);
|
|
3324
|
+
if (ready.length === 0) {
|
|
3325
|
+
const lines = CLI_PREFERENCE.map((name) => `${report[name].problem ?? `${CLI_LABEL[name]} can't be used`}.`);
|
|
3326
|
+
const fix = CLI_PREFERENCE.map((name) => report[name].fix).find((command3) => command3) ?? null;
|
|
3327
|
+
return card({
|
|
3328
|
+
problem: {
|
|
3329
|
+
message: `${lines.join(" ")} ${fix ? "Run this in a terminal, then check again." : "Install one, log in once in a terminal, then check again."}`,
|
|
3330
|
+
command: fix,
|
|
3331
|
+
blocking: true,
|
|
3332
|
+
links: CLI_PREFERENCE.filter((name) => !report[name].path).map((name) => INSTALL[name])
|
|
3333
|
+
},
|
|
3334
|
+
fields: [
|
|
3335
|
+
field("path", "Installed somewhere setup didn\u2019t look? Its path", "", {
|
|
3336
|
+
placeholder: "/opt/homebrew/bin/codex",
|
|
3337
|
+
help: "The program itself, codex or claude. Setup checks it before using it."
|
|
3338
|
+
})
|
|
3339
|
+
]
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3342
|
+
const set = ctx.current.DAILY_FOCUS_AGENT;
|
|
3343
|
+
const others = CLI_PREFERENCE.filter((name) => !report[name].ready && report[name].path).map((name) => `${report[name].problem}.`);
|
|
3344
|
+
return card({
|
|
3345
|
+
why: [
|
|
3346
|
+
ready.length > 1 ? `Both are ready on this ${ctx.machine}, so it uses Codex unless you say otherwise.` : `${CLI_LABEL[ready[0]]} is the one ready on this ${ctx.machine}.`,
|
|
3347
|
+
...others
|
|
3348
|
+
].join(" "),
|
|
3349
|
+
choices: [
|
|
3350
|
+
choice(
|
|
3351
|
+
"cli",
|
|
3352
|
+
ready.map((name) => option(name, CLI_LABEL[name], [report[name].version, report[name].path].filter(Boolean).join(", at "))),
|
|
3353
|
+
[set && ready.includes(set) ? set : ready[0]]
|
|
3354
|
+
)
|
|
3355
|
+
]
|
|
2930
3356
|
});
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
3357
|
+
},
|
|
3358
|
+
summary: (answer) => {
|
|
3359
|
+
const chosen = CLI_LABEL[one(answer, "cli")];
|
|
3360
|
+
return chosen ? `${chosen} writes your brief.` : "No agent writes your brief yet.";
|
|
3361
|
+
},
|
|
3362
|
+
// The assistant comes with it, on the same CLI, unless it has been set: it
|
|
3363
|
+
// only reads and drafts, and it is what the Ask button on every row needs.
|
|
3364
|
+
settings: (answer, ctx) => {
|
|
3365
|
+
const chosen = one(answer, "cli");
|
|
3366
|
+
const bin = binFor(ctx, chosen);
|
|
3367
|
+
const assistant = chosen && !ctx.current.DAILY_FOCUS_ASSISTANT;
|
|
3368
|
+
return {
|
|
3369
|
+
DAILY_FOCUS_AGENT: chosen,
|
|
3370
|
+
...bin ? { DAILY_FOCUS_AGENT_BIN: bin } : {},
|
|
3371
|
+
...assistant ? { DAILY_FOCUS_ASSISTANT: chosen, ...bin ? { DAILY_FOCUS_ASSISTANT_BIN: bin } : {} } : {}
|
|
3372
|
+
};
|
|
2934
3373
|
}
|
|
2935
3374
|
};
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
3375
|
+
var profile = {
|
|
3376
|
+
id: "profile",
|
|
3377
|
+
topic: "what it covers",
|
|
3378
|
+
title: () => "What should your brief cover?",
|
|
3379
|
+
keys: ["DAILY_FOCUS_PROFILE"],
|
|
3380
|
+
later: null,
|
|
3381
|
+
card: (ctx) => card({
|
|
3382
|
+
choices: [
|
|
3383
|
+
choice(
|
|
3384
|
+
"profile",
|
|
3385
|
+
[
|
|
3386
|
+
option("work", "Your work", "Work mail and calendars, pull requests, tickets and meeting notes."),
|
|
3387
|
+
option("personal", "Your personal life", "Personal mail, family calendars, reminders and messages.")
|
|
3388
|
+
],
|
|
3389
|
+
[ctx.current.DAILY_FOCUS_PROFILE === "personal" ? "personal" : "work"]
|
|
3390
|
+
)
|
|
3391
|
+
],
|
|
3392
|
+
why: "One dashboard briefs one of them. For both, run a second dashboard with its own store."
|
|
3393
|
+
}),
|
|
3394
|
+
summary: (answer) => one(answer, "profile") === "personal" ? "It briefs your personal life." : "It briefs your work.",
|
|
3395
|
+
settings: (answer) => ({ DAILY_FOCUS_PROFILE: one(answer, "profile") })
|
|
3396
|
+
};
|
|
3397
|
+
var TIMES = ["06:00", "06:30", "07:00", "07:30", "08:00"];
|
|
3398
|
+
var DAYS = { "1-5": "Weekdays", "0-6": "Every day" };
|
|
3399
|
+
var schedule = {
|
|
3400
|
+
id: "schedule",
|
|
3401
|
+
topic: "when it runs",
|
|
3402
|
+
title: () => "When should your brief be ready?",
|
|
3403
|
+
applies: (ctx) => ctx.cli !== null,
|
|
3404
|
+
keys: ["DAILY_FOCUS_AGENT_AT", "DAILY_FOCUS_AGENT_DAYS"],
|
|
3405
|
+
later: null,
|
|
3406
|
+
card(ctx) {
|
|
3407
|
+
const at = ctx.current.DAILY_FOCUS_AGENT_AT || "07:00";
|
|
3408
|
+
const days = ctx.current.DAILY_FOCUS_AGENT_DAYS || "1-5";
|
|
3409
|
+
const times = [.../* @__PURE__ */ new Set([...TIMES, ...at === "off" ? [] : [at]])].sort();
|
|
3410
|
+
return card({
|
|
3411
|
+
why: `It starts then and takes a few minutes. If the ${ctx.machine} is asleep or the dashboard isn't running then, it runs as soon as it is, that day.`,
|
|
3412
|
+
choices: [
|
|
3413
|
+
choice("at", [...times.map((time) => option(time, time)), option("off", "Only when I ask")], [at], {
|
|
3414
|
+
label: "Time",
|
|
3415
|
+
custom: "Another time, such as 05:45"
|
|
3416
|
+
}),
|
|
3417
|
+
choice("days", Object.entries({ ...DAYS, ...DAYS[days] ? {} : { [days]: days } }).map(([value, label]) => option(value, label)), [days], {
|
|
3418
|
+
label: "Days"
|
|
3419
|
+
})
|
|
3420
|
+
]
|
|
3421
|
+
});
|
|
3422
|
+
},
|
|
3423
|
+
summary(answer) {
|
|
3424
|
+
const at = one(answer, "at");
|
|
3425
|
+
if (!at || at === "off") return "It runs only when you ask.";
|
|
3426
|
+
const days = one(answer, "days") ?? "1-5";
|
|
3427
|
+
return `It runs at ${at} ${days === "1-5" ? "on weekdays" : days === "0-6" ? "every day" : `on days ${days}`}.`;
|
|
3428
|
+
},
|
|
3429
|
+
settings: (answer) => ({ DAILY_FOCUS_AGENT_AT: one(answer, "at"), DAILY_FOCUS_AGENT_DAYS: one(answer, "days") })
|
|
3430
|
+
};
|
|
3431
|
+
function proposedAccounts(proposal) {
|
|
3432
|
+
const raw = Array.isArray(proposal?.githubAccounts) ? proposal.githubAccounts : [];
|
|
3433
|
+
return raw.flatMap((entry) => {
|
|
3434
|
+
const account = typeof entry === "object" && entry !== null ? entry : {};
|
|
3435
|
+
const login = str3(account.login);
|
|
3436
|
+
return login ? [{ login, note: str3(account.note) }] : [];
|
|
3437
|
+
});
|
|
3438
|
+
}
|
|
3439
|
+
function githubAccounts(ctx) {
|
|
3440
|
+
const accounts = /* @__PURE__ */ new Map();
|
|
3441
|
+
for (const login of ctx.identity.logins) accounts.set(login.toLowerCase(), { login, note: "" });
|
|
3442
|
+
for (const { login, note } of proposedAccounts(proposalOf(ctx, "identity"))) {
|
|
3443
|
+
accounts.set(login.toLowerCase(), { login: accounts.get(login.toLowerCase())?.login ?? login, note });
|
|
3444
|
+
}
|
|
3445
|
+
return [...accounts.values()];
|
|
3446
|
+
}
|
|
3447
|
+
var identity2 = {
|
|
3448
|
+
id: "identity",
|
|
3449
|
+
topic: "who you are",
|
|
3450
|
+
title: () => "Is this you?",
|
|
3451
|
+
keys: [],
|
|
3452
|
+
// Clearing three fields and unticking the accounts is a chore; one button, named for what it does, isn't.
|
|
3453
|
+
later: { label: "Leave this out", summary: "Who you are is left out, so the brief can\u2019t tell your to-dos from other people\u2019s." },
|
|
3454
|
+
card(ctx) {
|
|
3455
|
+
const proposal = proposalOf(ctx, "identity");
|
|
3456
|
+
const names = Array.isArray(proposal?.names) ? proposal.names.map(str3).filter(Boolean) : [];
|
|
3457
|
+
const name = names.length ? names.join(", ") : ctx.identity.name ?? "";
|
|
3458
|
+
const email = str3(proposal?.email) || (ctx.identity.email ?? "");
|
|
3459
|
+
const accounts = githubAccounts(ctx);
|
|
3460
|
+
return card({
|
|
3461
|
+
why: str3(proposal?.why) || "The brief tells your to-dos from everyone else\u2019s by these.",
|
|
3462
|
+
fields: [
|
|
3463
|
+
field("name", "Your name, as meeting notes write it", name, {
|
|
3464
|
+
placeholder: "Robin Lark",
|
|
3465
|
+
help: "Add short forms after a comma, such as Robin L."
|
|
3466
|
+
}),
|
|
3467
|
+
field("email", ctx.profile === "personal" ? "Your email" : "Your work email", email, { placeholder: "robin@example.com" })
|
|
3468
|
+
],
|
|
3469
|
+
choices: [
|
|
3470
|
+
choice(
|
|
3471
|
+
"github",
|
|
3472
|
+
accounts.map((account) => option(account.login, account.login, account.note || null)),
|
|
3473
|
+
accounts.map((account) => account.login),
|
|
3474
|
+
{ label: "Your GitHub accounts", multiple: true, custom: "Add a GitHub login" }
|
|
3475
|
+
)
|
|
3476
|
+
]
|
|
3477
|
+
});
|
|
3478
|
+
},
|
|
3479
|
+
summary(answer) {
|
|
3480
|
+
const name = answer.fields.name?.split(",")[0]?.trim();
|
|
3481
|
+
const logins = answer.choices.github ?? [];
|
|
3482
|
+
const rest = [answer.fields.email?.trim() ?? "", logins.length ? `${listed(logins)} on GitHub` : ""].filter(Boolean);
|
|
3483
|
+
if (!name) return rest.length ? `You're ${listed(rest)}.` : "No details about you.";
|
|
3484
|
+
return `You're ${name}${rest.length ? `, ${rest.join(", ")}` : ""}.`;
|
|
3485
|
+
},
|
|
3486
|
+
settings(answer, ctx) {
|
|
3487
|
+
const values = {};
|
|
3488
|
+
if (answer.fields.email?.trim()) values.DAILY_FOCUS_CALENDAR_ADDRESSES = answer.fields.email.trim();
|
|
3489
|
+
const logins = answer.choices.github ?? [];
|
|
3490
|
+
if (logins.length > 1 || logins.length === 1 && logins[0] !== ctx.identity.logins[0]) {
|
|
3491
|
+
values.DAILY_FOCUS_GITHUB_ACCOUNTS = logins.join(", ");
|
|
3492
|
+
}
|
|
3493
|
+
return values;
|
|
3494
|
+
},
|
|
3495
|
+
sources(answer, ctx) {
|
|
3496
|
+
const names = (answer.fields.name ?? "").split(",").map((n) => n.trim()).filter(Boolean);
|
|
3497
|
+
const email = answer.fields.email?.trim();
|
|
3498
|
+
const notes = new Map(githubAccounts(ctx).map((account) => [account.login, account.note]));
|
|
3499
|
+
const logins = answer.choices.github ?? [];
|
|
3500
|
+
const lines = [
|
|
3501
|
+
names.length ? `- Name, as meeting notes and Doc assignments write it: ${ticked(names[0])}${names.length > 1 ? `, also ${listed(names.slice(1).map(ticked))}` : ""}` : null,
|
|
3502
|
+
email ? `- ${ctx.profile === "personal" ? "Personal" : "Work"} email: ${ticked(email)}` : null,
|
|
3503
|
+
logins.length ? [
|
|
3504
|
+
`- GitHub ${logins.length > 1 ? "logins" : "login"}, which review requests and authorship are judged against:`,
|
|
3505
|
+
...logins.map((login) => ` - ${ticked(login)}${notes.get(login) ? ` \u2014 ${notes.get(login)}` : ""}`)
|
|
3506
|
+
].join("\n") : null
|
|
3507
|
+
].filter((line) => line !== null);
|
|
3508
|
+
return lines.length ? [{ heading: "Identity", body: lines.join("\n") }] : [];
|
|
3509
|
+
},
|
|
3510
|
+
agent: {
|
|
3511
|
+
brief: (ctx) => "Who the user is, as their morning brief needs to know them: their name as meeting notes and document assignments write it, with any short forms colleagues use, their " + (ctx.profile === "personal" ? "personal" : "work") + " email address, and every GitHub account they work as: gh may hold several, such as one for an employer\u2019s organisation. Give each account a few words on what it is for when you can tell. Start from what the dashboard found, below. Check the address they send mail from, how others address them, and `gh auth status`.",
|
|
3512
|
+
facts: (ctx) => [
|
|
3513
|
+
ctx.identity.name ? `- gh says their name is ${ctx.identity.name}` : null,
|
|
3514
|
+
ctx.identity.logins.length ? `- gh is logged in to GitHub as ${listed(ctx.identity.logins)}; the first is active` : null,
|
|
3515
|
+
ctx.identity.email ? `- git's global email is ${ctx.identity.email}` : null
|
|
3516
|
+
].filter(Boolean).join("\n") || null,
|
|
3517
|
+
schema: {
|
|
3518
|
+
type: "object",
|
|
3519
|
+
properties: {
|
|
3520
|
+
why: { type: "string", description: "One or two sentences on where these came from." },
|
|
3521
|
+
names: { type: "array", items: { type: "string" }, description: "Full name first, then short forms." },
|
|
3522
|
+
email: { type: "string" },
|
|
3523
|
+
githubAccounts: {
|
|
3524
|
+
type: "array",
|
|
3525
|
+
items: {
|
|
3526
|
+
type: "object",
|
|
3527
|
+
properties: {
|
|
3528
|
+
login: { type: "string" },
|
|
3529
|
+
note: { type: "string", description: "What the account is for, in a few words, or empty." }
|
|
3530
|
+
},
|
|
3531
|
+
required: ["login", "note"],
|
|
3532
|
+
additionalProperties: false
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
},
|
|
3536
|
+
required: ["why", "names", "email", "githubAccounts"],
|
|
3537
|
+
additionalProperties: false
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
};
|
|
3541
|
+
function healthText(board, via) {
|
|
3542
|
+
if (board.reason) return board.reason;
|
|
3543
|
+
if (board.fetchedAt) return `Connected through ${via}.`;
|
|
3544
|
+
if (board.enabled) return `Checking ${via}\u2026`;
|
|
3545
|
+
return null;
|
|
3546
|
+
}
|
|
3547
|
+
var boards = {
|
|
3548
|
+
id: "boards",
|
|
3549
|
+
topic: "the boards",
|
|
3550
|
+
title: () => "What else should the dashboard show?",
|
|
3551
|
+
keys: ["DAILY_FOCUS_GITHUB", "DAILY_FOCUS_JIRA"],
|
|
3552
|
+
later: null,
|
|
3553
|
+
card(ctx) {
|
|
3554
|
+
const { github, jira } = ctx.boards;
|
|
3555
|
+
const selected = [
|
|
3556
|
+
github.enabled && !github.reason ? "github" : null,
|
|
3557
|
+
jira.enabled && !jira.reason && ctx.profile === "work" ? "jira" : null
|
|
3558
|
+
].filter((value) => value !== null);
|
|
3559
|
+
return card({
|
|
3560
|
+
why: "Both are read live, beside the brief, so a merge or a status change shows up within minutes.",
|
|
3561
|
+
choices: [
|
|
3562
|
+
choice(
|
|
3563
|
+
"boards",
|
|
3564
|
+
[
|
|
3565
|
+
option("github", "Your pull requests", healthText(github, "gh") ?? "Through the GitHub CLI, gh."),
|
|
3566
|
+
option("jira", "Your Jira tickets", healthText(jira, "acli") ?? "Through the Atlassian CLI, acli.")
|
|
3567
|
+
],
|
|
3568
|
+
selected,
|
|
3569
|
+
{ multiple: true }
|
|
3570
|
+
)
|
|
3571
|
+
]
|
|
3572
|
+
});
|
|
3573
|
+
},
|
|
3574
|
+
summary(answer) {
|
|
3575
|
+
const on = answer.choices.boards ?? [];
|
|
3576
|
+
const names = [on.includes("github") ? "your pull requests" : null, on.includes("jira") ? "your Jira tickets" : null].filter(
|
|
3577
|
+
(name) => name !== null
|
|
3578
|
+
);
|
|
3579
|
+
return names.length ? `It shows ${listed(names)}.` : "No boards beside the brief.";
|
|
3580
|
+
},
|
|
3581
|
+
settings(answer) {
|
|
3582
|
+
const on = answer.choices.boards ?? [];
|
|
3583
|
+
return { DAILY_FOCUS_GITHUB: on.includes("github") ? "on" : "off", DAILY_FOCUS_JIRA: on.includes("jira") ? "on" : "off" };
|
|
3584
|
+
}
|
|
3585
|
+
};
|
|
3586
|
+
var HOLIDAYS = "Holidays";
|
|
3587
|
+
function calendarEntries(ctx) {
|
|
3588
|
+
const proposal = proposalOf(ctx, "calendars");
|
|
3589
|
+
const verdicts = Array.isArray(proposal?.calendars) ? proposal.calendars.flatMap((raw) => {
|
|
3590
|
+
const entry = typeof raw === "object" && raw !== null ? raw : {};
|
|
3591
|
+
const title = str3(entry.title);
|
|
3592
|
+
return title ? [{ title, account: str3(entry.account), include: entry.include === true, holiday: entry.holiday === true, briefName: str3(entry.briefName), note: str3(entry.note) }] : [];
|
|
3593
|
+
}) : [];
|
|
3594
|
+
const configured = new Set((ctx.current.DAILY_FOCUS_CALENDARS ?? "").split(",").map((name) => name.trim().toLowerCase()).filter(Boolean));
|
|
3595
|
+
const used = /* @__PURE__ */ new Set();
|
|
3596
|
+
const entries = [];
|
|
3597
|
+
for (const calendar of Array.isArray(ctx.calendars) ? ctx.calendars : []) {
|
|
3598
|
+
const key = calendar.title.toLowerCase();
|
|
3599
|
+
const verdict = verdicts.find((v) => !used.has(v) && v.title.toLowerCase() === key && v.account.toLowerCase() === calendar.source.toLowerCase()) ?? verdicts.find((v) => !used.has(v) && v.title.toLowerCase() === key);
|
|
3600
|
+
if (verdict) used.add(verdict);
|
|
3601
|
+
entries.push({
|
|
3602
|
+
value: `live:${calendar.source}:${calendar.title}`,
|
|
3603
|
+
title: calendar.title,
|
|
3604
|
+
group: verdict?.holiday ? HOLIDAYS : calendar.source || "Calendar.app",
|
|
3605
|
+
live: true,
|
|
3606
|
+
holiday: verdict?.holiday ?? false,
|
|
3607
|
+
briefName: verdict?.briefName ?? "",
|
|
3608
|
+
note: verdict?.note ?? "",
|
|
3609
|
+
include: verdict ? verdict.include : configured.has(key)
|
|
3610
|
+
});
|
|
3611
|
+
}
|
|
3612
|
+
for (const verdict of verdicts) {
|
|
3613
|
+
if (used.has(verdict)) continue;
|
|
3614
|
+
entries.push({
|
|
3615
|
+
value: `brief:${verdict.account}:${verdict.title}`,
|
|
3616
|
+
title: verdict.title,
|
|
3617
|
+
group: verdict.holiday ? HOLIDAYS : verdict.account || "Your calendar account",
|
|
3618
|
+
live: false,
|
|
3619
|
+
holiday: verdict.holiday,
|
|
3620
|
+
briefName: verdict.briefName,
|
|
3621
|
+
note: verdict.note,
|
|
3622
|
+
include: verdict.include
|
|
3623
|
+
});
|
|
3624
|
+
}
|
|
3625
|
+
return entries.sort((a, b) => Number(a.group === HOLIDAYS) - Number(b.group === HOLIDAYS));
|
|
3626
|
+
}
|
|
3627
|
+
function chosenCalendars(answer, ctx) {
|
|
3628
|
+
const all = new Map(calendarEntries(ctx).map((entry) => [entry.value, entry]));
|
|
3629
|
+
const values = answer.choices.calendars ?? [];
|
|
3630
|
+
return {
|
|
3631
|
+
entries: values.flatMap((value) => all.get(value) ?? []),
|
|
3632
|
+
typed: values.filter((value) => !all.has(value)).map((value) => value.trim()).filter(Boolean)
|
|
3633
|
+
};
|
|
3634
|
+
}
|
|
3635
|
+
var calendars = {
|
|
3636
|
+
id: "calendars",
|
|
3637
|
+
topic: "your calendars",
|
|
3638
|
+
title: (ctx) => ctx.profile === "personal" ? "Which calendars belong in your brief?" : "Which calendars belong in your work brief?",
|
|
3639
|
+
keys: ["DAILY_FOCUS_CALENDARS"],
|
|
3640
|
+
later: null,
|
|
3641
|
+
card(ctx) {
|
|
3642
|
+
const entries = calendarEntries(ctx);
|
|
3643
|
+
const proposal = proposalOf(ctx, "calendars");
|
|
3644
|
+
const problem = ctx.calendar.supported && !ctx.calendar.built ? {
|
|
3645
|
+
message: "The live agenda reads Calendar.app through a small helper, built once with this command. Until then it uses the brief\u2019s events, so this list is from your calendar account.",
|
|
3646
|
+
command: ctx.calendar.buildCommand,
|
|
3647
|
+
blocking: false
|
|
3648
|
+
} : typeof ctx.calendars === "string" ? { message: `Calendar.app couldn't be read: ${ctx.calendars}`, command: null, blocking: false } : null;
|
|
3649
|
+
return card({
|
|
3650
|
+
why: str3(proposal?.why) || null,
|
|
3651
|
+
problem,
|
|
3652
|
+
choices: [
|
|
3653
|
+
choice(
|
|
3654
|
+
"calendars",
|
|
3655
|
+
entries.map((entry) => option(entry.value, entry.title, entry.note || null, entry.group)),
|
|
3656
|
+
entries.filter((entry) => entry.include).map((entry) => entry.value),
|
|
3657
|
+
{ multiple: true, custom: "Add a calendar by name" }
|
|
3658
|
+
)
|
|
3659
|
+
]
|
|
3660
|
+
});
|
|
3661
|
+
},
|
|
3662
|
+
summary(answer, ctx) {
|
|
3663
|
+
const { entries, typed } = chosenCalendars(answer, ctx);
|
|
3664
|
+
const names = [...entries.map((entry) => entry.title), ...typed];
|
|
3665
|
+
if (names.length === 0) return "No calendars.";
|
|
3666
|
+
if (names.length <= 3) return `It reads ${listed(names)}.`;
|
|
3667
|
+
return `It reads ${names.slice(0, 2).join(", ")} and ${names.length - 2} more calendars.`;
|
|
3668
|
+
},
|
|
3669
|
+
settings(answer, ctx) {
|
|
3670
|
+
const { entries, typed } = chosenCalendars(answer, ctx);
|
|
3671
|
+
const live = [.../* @__PURE__ */ new Set([...entries.filter((entry) => entry.live).map((entry) => entry.title), ...typed])];
|
|
3672
|
+
return { DAILY_FOCUS_CALENDARS: live.filter((name) => !name.includes(",")).join(", ") || null };
|
|
3673
|
+
},
|
|
3674
|
+
sources(answer, ctx) {
|
|
3675
|
+
const { entries, typed } = chosenCalendars(answer, ctx);
|
|
3676
|
+
const line = (entry) => `- ${ticked(entry.briefName || entry.title)}${entry.group !== HOLIDAYS && entry.group ? ` (${entry.group})` : ""}`;
|
|
3677
|
+
const own = [...entries.filter((entry) => !entry.holiday).map(line), ...typed.map((name) => `- ${ticked(name)}`)];
|
|
3678
|
+
const holidays = entries.filter((entry) => entry.holiday).map(line);
|
|
3679
|
+
return [
|
|
3680
|
+
...own.length ? [{ heading: "Calendars", body: `Query these, and treat their events as blocking time:
|
|
3681
|
+
|
|
3682
|
+
${own.join("\n")}` }] : [],
|
|
3683
|
+
...holidays.length ? [{ heading: "Holiday calendars", body: `Call out a holiday today or within the next 14 days:
|
|
3684
|
+
|
|
3685
|
+
${holidays.join("\n")}` }] : []
|
|
3686
|
+
];
|
|
3687
|
+
},
|
|
3688
|
+
agent: {
|
|
3689
|
+
brief: (ctx) => `Which calendars the user's ${ctx.profile} brief should read. ` + (Array.isArray(ctx.calendars) ? "Below is every calendar Calendar.app on this Mac has, with the account it is on and how many events it has today. " : "List the calendars their calendar connector reaches. ") + "Include " + (ctx.profile === "personal" ? "their own personal and family calendars, " : "their own work calendar, team calendars whose events block their time, ") + "and the holiday calendars for where they and the people they work with are. Leave out " + (ctx.profile === "personal" ? "work calendars, " : "personal and family calendars, ") + "birthdays, and shared calendars that are mostly other people's events. If you can reach their calendar through a connector, look at the coming week for evidence, and give `briefName`, the name the connector uses where it differs from Calendar.app\u2019s. " + (Array.isArray(ctx.calendars) ? "List every calendar from the list below, and any your connector has that Calendar.app does not. " : "") + '`note` is a few words shown under the name, such as "Your primary calendar" or "12 of your meetings this week", or empty.',
|
|
3690
|
+
facts(ctx) {
|
|
3691
|
+
if (typeof ctx.calendars === "string") return `Calendar.app could not be read (${ctx.calendars}), so work from your calendar connector alone.`;
|
|
3692
|
+
if (!Array.isArray(ctx.calendars)) return "There is no list from Calendar.app here, so work from your calendar connector alone.";
|
|
3693
|
+
if (ctx.calendars.length === 0) return "Calendar.app has no calendars.";
|
|
3694
|
+
return [
|
|
3695
|
+
"Calendar.app has these, as title (account): events today.",
|
|
3696
|
+
"",
|
|
3697
|
+
...ctx.calendars.map((calendar) => `- ${calendar.title} (${calendar.source || "no account"}): ${calendar.eventsToday}`)
|
|
3698
|
+
].join("\n");
|
|
3699
|
+
},
|
|
3700
|
+
schema: {
|
|
3701
|
+
type: "object",
|
|
3702
|
+
properties: {
|
|
3703
|
+
why: { type: "string", description: "One or two sentences on which you ticked and why." },
|
|
3704
|
+
calendars: {
|
|
3705
|
+
type: "array",
|
|
3706
|
+
items: {
|
|
3707
|
+
type: "object",
|
|
3708
|
+
properties: {
|
|
3709
|
+
title: { type: "string", description: "Exactly as Calendar.app spells it, or the connector's name for one Calendar.app lacks." },
|
|
3710
|
+
account: { type: "string" },
|
|
3711
|
+
include: { type: "boolean" },
|
|
3712
|
+
holiday: { type: "boolean" },
|
|
3713
|
+
briefName: { type: "string" },
|
|
3714
|
+
note: { type: "string" }
|
|
3715
|
+
},
|
|
3716
|
+
required: ["title", "account", "include", "holiday", "briefName", "note"],
|
|
3717
|
+
additionalProperties: false
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
},
|
|
3721
|
+
required: ["why", "calendars"],
|
|
3722
|
+
additionalProperties: false
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
};
|
|
3726
|
+
function suggestedObjectives(proposal) {
|
|
3727
|
+
const raw = Array.isArray(proposal?.suggestions) ? proposal.suggestions : [];
|
|
3728
|
+
return raw.flatMap((entry) => {
|
|
3729
|
+
const suggestion = typeof entry === "object" && entry !== null ? entry : {};
|
|
3730
|
+
const text = str3(suggestion.objective);
|
|
3731
|
+
return text ? [{ text, detail: str3(suggestion.evidence) || null }] : [];
|
|
3732
|
+
});
|
|
3733
|
+
}
|
|
3734
|
+
var objective = {
|
|
3735
|
+
id: "objective",
|
|
3736
|
+
topic: "your objective",
|
|
3737
|
+
title: () => "What are you trying to get done right now?",
|
|
3738
|
+
keys: [],
|
|
3739
|
+
// Left empty, Continue says so; see `empty` on its card.
|
|
3740
|
+
later: null,
|
|
3741
|
+
card(ctx) {
|
|
3742
|
+
const proposal = proposalOf(ctx, "objective");
|
|
3743
|
+
return card({
|
|
3744
|
+
reply: "Tell it what you\u2019re working on, however roughly",
|
|
3745
|
+
empty: "Continue without one",
|
|
3746
|
+
why: str3(proposal?.why) || "The brief ranks the day against it. A next move works better than a project name. Tell it what you\u2019re working on, however roughly, and it will write it up.",
|
|
3747
|
+
fields: [
|
|
3748
|
+
field("objective", "Your objective", str3(proposal?.objective), {
|
|
3749
|
+
placeholder: "Get the staging deploy working again",
|
|
3750
|
+
suggestions: suggestedObjectives(proposal)
|
|
3751
|
+
}),
|
|
3752
|
+
field("blocker", "What\u2019s in the way, if anything", str3(proposal?.blocker), { placeholder: "Nobody can reproduce the failure" })
|
|
3753
|
+
]
|
|
3754
|
+
});
|
|
3755
|
+
},
|
|
3756
|
+
summary: (answer) => {
|
|
3757
|
+
const text = answer.fields.objective?.trim();
|
|
3758
|
+
const blocker = answer.fields.blocker?.trim();
|
|
3759
|
+
if (!text) return "No objective for now.";
|
|
3760
|
+
return `Your objective: ${text}${blocker ? `. In the way: ${blocker}` : ""}`;
|
|
3761
|
+
},
|
|
3762
|
+
objective: (answer) => answer.fields.objective?.trim() ? { objective: answer.fields.objective.trim(), blocker: answer.fields.blocker?.trim() || null } : null,
|
|
3763
|
+
agent: {
|
|
3764
|
+
brief: (ctx) => `The user's current objective: the one thing they are trying to get done right now, which their ${ctx.profile} brief ranks the day against. Look at what they have been doing lately: their own recent and open pull requests, tickets assigned to them and in progress, and anything on the calendar that keeps coming back. Suggest up to three objectives, each only where there is a strong signal, with the evidence in a few words, such as "your 4 open pull requests on checkout". If nothing stands out, suggest none and say so. Leave \`objective\` and \`blocker\` empty until the user tells you what they are working on. When they do, write it up as one objective: a next move rather than a project name, in their own terms, under 12 words. Put what is in the way, if they mention anything, in \`blocker\`, as briefly.`,
|
|
3765
|
+
schema: {
|
|
3766
|
+
type: "object",
|
|
3767
|
+
properties: {
|
|
3768
|
+
why: { type: "string", description: "One or two sentences: what you looked at, or how you wrote up what they said." },
|
|
3769
|
+
objective: { type: "string" },
|
|
3770
|
+
blocker: { type: "string" },
|
|
3771
|
+
suggestions: {
|
|
3772
|
+
type: "array",
|
|
3773
|
+
items: {
|
|
3774
|
+
type: "object",
|
|
3775
|
+
properties: {
|
|
3776
|
+
objective: { type: "string" },
|
|
3777
|
+
evidence: { type: "string", description: "What it was drawn from, in a few words." }
|
|
3778
|
+
},
|
|
3779
|
+
required: ["objective", "evidence"],
|
|
3780
|
+
additionalProperties: false
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
},
|
|
3784
|
+
required: ["why", "objective", "blocker", "suggestions"],
|
|
3785
|
+
additionalProperties: false
|
|
3786
|
+
}
|
|
3787
|
+
}
|
|
3788
|
+
};
|
|
3789
|
+
var QUESTIONS = [cli, profile, schedule, identity2, boards, calendars, objective];
|
|
3790
|
+
function questionById(id) {
|
|
3791
|
+
return QUESTIONS.find((question) => question.id === id);
|
|
3792
|
+
}
|
|
3793
|
+
function composeSources(profile2, sections, today) {
|
|
3794
|
+
const intro = [
|
|
3795
|
+
"# Sources",
|
|
3796
|
+
"",
|
|
3797
|
+
`Who you are and where to look, for your ${profile2 === "personal" ? "personal" : "work"} morning brief. Written by setup on ${today}; change it freely. The prompt next to this file (\`prompt.md\`) says *what* to gather and how to judge it; this file says *who* and *where*. A section that is missing is fine: the agent falls back to your primary calendar and connected accounts, and says what it missed.`
|
|
3798
|
+
];
|
|
3799
|
+
return [...intro, ...sections.flatMap((section) => ["", `## ${section.heading}`, "", section.body])].join("\n") + "\n";
|
|
3800
|
+
}
|
|
3801
|
+
function machineName(platform = process.platform) {
|
|
3802
|
+
return platform === "darwin" ? "Mac" : platform === "win32" ? "PC" : "computer";
|
|
2942
3803
|
}
|
|
2943
3804
|
|
|
2944
3805
|
// src/settings.ts
|
|
@@ -2949,8 +3810,7 @@ var SETTING_GROUPS = [
|
|
|
2949
3810
|
{ id: "github", title: "Pull requests" },
|
|
2950
3811
|
{ id: "jira", title: "Jira tickets" },
|
|
2951
3812
|
{ id: "calendar", title: "Calendar" },
|
|
2952
|
-
{ id: "day", title: "Working day" }
|
|
2953
|
-
{ id: "sessions", title: "Focus sessions" }
|
|
3813
|
+
{ id: "day", title: "Working day" }
|
|
2954
3814
|
];
|
|
2955
3815
|
var OFF_OR_CLI = [
|
|
2956
3816
|
{ value: "off", label: "Off" },
|
|
@@ -2964,7 +3824,7 @@ var SETTINGS = [
|
|
|
2964
3824
|
key: "DAILY_FOCUS_PROFILE",
|
|
2965
3825
|
group: "instance",
|
|
2966
3826
|
label: "What this dashboard briefs",
|
|
2967
|
-
help: "Work or home. Picks the morning prompt and the defaults below: the personal profile switches off Jira
|
|
3827
|
+
help: "Work or home. Picks the morning prompt and the defaults below: the personal profile switches off Jira and free time.",
|
|
2968
3828
|
kind: "choice",
|
|
2969
3829
|
choices: PROFILES.map((value) => ({ value, label: value === "work" ? "Work" : "Personal life" })),
|
|
2970
3830
|
fallback: always("Work")
|
|
@@ -3015,7 +3875,7 @@ var SETTINGS = [
|
|
|
3015
3875
|
key: "DAILY_FOCUS_AGENT_TOOLS",
|
|
3016
3876
|
group: "agent",
|
|
3017
3877
|
label: "Tools Claude Code may use",
|
|
3018
|
-
help: "In Claude Code's permission syntax, comma-separated. Add your calendar, chat and document connectors by name, or the brief reports them as unreachable. Ignored by Codex.",
|
|
3878
|
+
help: "In Claude Code's permission syntax, comma-separated. Add your calendar, chat and document connectors by name, or the brief reports them as unreachable. The default allows every Gmail tool, sending included; name the tools you want instead to narrow it. Ignored by Codex.",
|
|
3019
3879
|
kind: "list",
|
|
3020
3880
|
fallback: always("Bash(gh *), Bash(acli *), WebFetch, mcp__claude_ai_Gmail"),
|
|
3021
3881
|
advanced: true
|
|
@@ -3042,7 +3902,7 @@ var SETTINGS = [
|
|
|
3042
3902
|
key: "DAILY_FOCUS_ASSISTANT",
|
|
3043
3903
|
group: "assistant",
|
|
3044
3904
|
label: "Answer questions with",
|
|
3045
|
-
help: "Puts an assistant in every item's panel
|
|
3905
|
+
help: "Puts an assistant in every item's panel, told to read and draft and never to send, post or edit. Under Codex, every connector write but a Gmail draft is refused.",
|
|
3046
3906
|
kind: "choice",
|
|
3047
3907
|
choices: OFF_OR_CLI,
|
|
3048
3908
|
fallback: always("Off")
|
|
@@ -3068,11 +3928,36 @@ var SETTINGS = [
|
|
|
3068
3928
|
key: "DAILY_FOCUS_ASSISTANT_TOOLS",
|
|
3069
3929
|
group: "assistant",
|
|
3070
3930
|
label: "Tools Claude Code may use",
|
|
3071
|
-
help: "In Claude Code's permission syntax, comma-separated. Editing tools are denied whatever this says. Ignored by Codex.",
|
|
3931
|
+
help: "In Claude Code's permission syntax, comma-separated. Editing tools are denied whatever this says. The default allows every Gmail tool, sending included; name the tools you want instead to narrow it. Ignored by Codex.",
|
|
3072
3932
|
kind: "list",
|
|
3073
3933
|
fallback: always("Bash(gh *), Bash(acli *), WebFetch, mcp__claude_ai_Gmail"),
|
|
3074
3934
|
advanced: true
|
|
3075
3935
|
},
|
|
3936
|
+
{
|
|
3937
|
+
key: "DAILY_FOCUS_ASSISTANT_COMPUTER_USE",
|
|
3938
|
+
group: "assistant",
|
|
3939
|
+
label: "Computer use",
|
|
3940
|
+
help: "Codex only. On lets the assistant use Codex's computer-use and browser plugins, which act in your apps and browser with your sign-ins. Off switches them off for its runs. Claude Code uses only the tools in its list.",
|
|
3941
|
+
kind: "flag",
|
|
3942
|
+
fallback: always("Off")
|
|
3943
|
+
},
|
|
3944
|
+
{
|
|
3945
|
+
key: "DAILY_FOCUS_ASSISTANT_AUTO",
|
|
3946
|
+
group: "assistant",
|
|
3947
|
+
label: "Answer ahead on pull requests",
|
|
3948
|
+
help: "When a pull request starts waiting on you because of a review or a failing check, asks the assistant about it before you do, so the answer is on the row when you look. Only reads; it never drafts. It spends the CLI\u2019s usage without you pressing anything.",
|
|
3949
|
+
kind: "flag",
|
|
3950
|
+
fallback: always("Off")
|
|
3951
|
+
},
|
|
3952
|
+
{
|
|
3953
|
+
key: "DAILY_FOCUS_ASSISTANT_AUTO_LIMIT",
|
|
3954
|
+
group: "assistant",
|
|
3955
|
+
label: "Answer ahead at most",
|
|
3956
|
+
help: "Times a day the assistant may start on its own, counted on the dashboard\u2019s calendar.",
|
|
3957
|
+
kind: "number",
|
|
3958
|
+
fallback: always("10 a day"),
|
|
3959
|
+
advanced: true
|
|
3960
|
+
},
|
|
3076
3961
|
{
|
|
3077
3962
|
key: "DAILY_FOCUS_ASSISTANT_BIN",
|
|
3078
3963
|
group: "assistant",
|
|
@@ -3086,7 +3971,7 @@ var SETTINGS = [
|
|
|
3086
3971
|
key: "DAILY_FOCUS_GITHUB",
|
|
3087
3972
|
group: "github",
|
|
3088
3973
|
label: "Pull request board",
|
|
3089
|
-
help: "Reads your open pull requests through the GitHub CLI. Sign in with gh auth login.",
|
|
3974
|
+
help: "Reads your open pull requests through the GitHub CLI. Sign in with gh auth login. The Jira ticket board uses it too, to catch merges Jira hasn't heard about yet.",
|
|
3090
3975
|
kind: "flag",
|
|
3091
3976
|
fallback: always("On")
|
|
3092
3977
|
},
|
|
@@ -3139,7 +4024,7 @@ var SETTINGS = [
|
|
|
3139
4024
|
label: "Jira ticket board",
|
|
3140
4025
|
help: "Flags tickets whose status disagrees with their pull requests, through the Atlassian CLI. Sign in with acli jira auth login.",
|
|
3141
4026
|
kind: "flag",
|
|
3142
|
-
fallback: (
|
|
4027
|
+
fallback: (profile2) => profile2 === "personal" ? "Off" : "On"
|
|
3143
4028
|
},
|
|
3144
4029
|
{
|
|
3145
4030
|
key: "DAILY_FOCUS_JIRA_PROJECTS",
|
|
@@ -3240,7 +4125,7 @@ var SETTINGS = [
|
|
|
3240
4125
|
label: "Show free time",
|
|
3241
4126
|
help: "Find focus windows between meetings in your working hours. Off shows events only.",
|
|
3242
4127
|
kind: "flag",
|
|
3243
|
-
fallback: (
|
|
4128
|
+
fallback: (profile2) => profile2 === "personal" ? "Off" : "On"
|
|
3244
4129
|
},
|
|
3245
4130
|
{
|
|
3246
4131
|
key: "DAILY_FOCUS_WORK_START",
|
|
@@ -3265,22 +4150,6 @@ var SETTINGS = [
|
|
|
3265
4150
|
help: "Minutes a gap has to last to count as time for focused work.",
|
|
3266
4151
|
kind: "number",
|
|
3267
4152
|
fallback: always("45 minutes")
|
|
3268
|
-
},
|
|
3269
|
-
{
|
|
3270
|
-
key: "DAILY_FOCUS_SESSION_MINUTES",
|
|
3271
|
-
group: "sessions",
|
|
3272
|
-
label: "Session length",
|
|
3273
|
-
help: "Minutes a focus session is set for when you start one.",
|
|
3274
|
-
kind: "number",
|
|
3275
|
-
fallback: always("25 minutes")
|
|
3276
|
-
},
|
|
3277
|
-
{
|
|
3278
|
-
key: "DAILY_FOCUS_AWAY_AFTER",
|
|
3279
|
-
group: "sessions",
|
|
3280
|
-
label: "End a session when away for",
|
|
3281
|
-
help: "Minutes without touching the Mac before a session is closed where you left. 0 turns it off.",
|
|
3282
|
-
kind: "number",
|
|
3283
|
-
fallback: (profile) => profile === "personal" ? "0, off" : "10 minutes"
|
|
3284
4153
|
}
|
|
3285
4154
|
];
|
|
3286
4155
|
var BY_KEY = new Map(SETTINGS.map((def) => [def.key, def]));
|
|
@@ -3293,7 +4162,7 @@ function profileOf(sources, settings = sources.settings) {
|
|
|
3293
4162
|
return PROFILES.includes(raw) ? raw : "work";
|
|
3294
4163
|
}
|
|
3295
4164
|
function describeSettings(sources) {
|
|
3296
|
-
const
|
|
4165
|
+
const profile2 = profileOf(sources);
|
|
3297
4166
|
const settings = SETTINGS.map((def) => {
|
|
3298
4167
|
const fromEnv = present(sources.process, def.key);
|
|
3299
4168
|
const fromSettings = present(sources.settings, def.key);
|
|
@@ -3309,7 +4178,7 @@ function describeSettings(sources) {
|
|
|
3309
4178
|
advanced: def.advanced === true,
|
|
3310
4179
|
value,
|
|
3311
4180
|
source,
|
|
3312
|
-
fallback: def.fallback(
|
|
4181
|
+
fallback: def.fallback(profile2)
|
|
3313
4182
|
};
|
|
3314
4183
|
});
|
|
3315
4184
|
return { groups: SETTING_GROUPS, settings, error: sources.settingsError };
|
|
@@ -3342,134 +4211,758 @@ async function saveSettings(path, settings) {
|
|
|
3342
4211
|
await writeJsonAtomic(path, sorted);
|
|
3343
4212
|
}
|
|
3344
4213
|
|
|
3345
|
-
// src/
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
"
|
|
3355
|
-
"
|
|
3356
|
-
"
|
|
3357
|
-
"
|
|
3358
|
-
"
|
|
3359
|
-
"
|
|
3360
|
-
"
|
|
3361
|
-
"
|
|
4214
|
+
// src/onboarding.ts
|
|
4215
|
+
var TURN_TIMEOUT_MS2 = 4 * 6e4;
|
|
4216
|
+
var KILL_AFTER_MS = 5e3;
|
|
4217
|
+
var STEPS_KEPT = 40;
|
|
4218
|
+
var SESSION_MODEL = {
|
|
4219
|
+
claude: { model: "opus", effort: "low" },
|
|
4220
|
+
codex: { model: null, effort: "low" }
|
|
4221
|
+
};
|
|
4222
|
+
var ONBOARDING_TOOLS = [
|
|
4223
|
+
"Bash(gh *)",
|
|
4224
|
+
"Bash(acli *)",
|
|
4225
|
+
"Bash(git config *)",
|
|
4226
|
+
"mcp__claude_ai_Gmail__search_threads",
|
|
4227
|
+
"mcp__claude_ai_Gmail__get_thread",
|
|
4228
|
+
"mcp__claude_ai_Gmail__get_message",
|
|
4229
|
+
"mcp__claude_ai_Gmail__list_labels",
|
|
4230
|
+
"mcp__claude_ai_Google_Calendar__list_calendars",
|
|
4231
|
+
"mcp__claude_ai_Google_Calendar__list_events",
|
|
4232
|
+
"mcp__claude_ai_Google_Calendar__search_events",
|
|
4233
|
+
"mcp__claude_ai_Google_Calendar__get_event",
|
|
4234
|
+
"mcp__claude_ai_Google_Drive__search_files",
|
|
4235
|
+
"mcp__claude_ai_Google_Drive__list_recent_files",
|
|
4236
|
+
"mcp__claude_ai_Google_Drive__get_file_metadata",
|
|
4237
|
+
"mcp__claude_ai_Google_Drive__read_file_content"
|
|
3362
4238
|
];
|
|
3363
|
-
|
|
3364
|
-
|
|
4239
|
+
var RESTARTED = "the dashboard restarted while this was running";
|
|
4240
|
+
function blankFile() {
|
|
4241
|
+
return { startedAt: (/* @__PURE__ */ new Date()).toISOString(), finishedAt: null, briefRequested: false, answers: {}, proposals: {} };
|
|
3365
4242
|
}
|
|
3366
|
-
|
|
3367
|
-
const
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
4243
|
+
function parseFile(text) {
|
|
4244
|
+
const file = blankFile();
|
|
4245
|
+
let raw;
|
|
4246
|
+
try {
|
|
4247
|
+
raw = JSON.parse(text);
|
|
4248
|
+
} catch {
|
|
4249
|
+
return file;
|
|
4250
|
+
}
|
|
4251
|
+
if (typeof raw !== "object" || raw === null) return file;
|
|
4252
|
+
const obj = raw;
|
|
4253
|
+
if (typeof obj.startedAt === "string") file.startedAt = obj.startedAt;
|
|
4254
|
+
if (typeof obj.finishedAt === "string") file.finishedAt = obj.finishedAt;
|
|
4255
|
+
file.briefRequested = obj.briefRequested === true;
|
|
4256
|
+
if (typeof obj.answers === "object" && obj.answers !== null) {
|
|
4257
|
+
for (const [id, answer] of Object.entries(obj.answers)) {
|
|
4258
|
+
const clean = cleanAnswer(answer);
|
|
4259
|
+
if (clean) file.answers[id] = clean;
|
|
3373
4260
|
}
|
|
3374
4261
|
}
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
4262
|
+
if (typeof obj.proposals === "object" && obj.proposals !== null) {
|
|
4263
|
+
for (const [id, value] of Object.entries(obj.proposals)) {
|
|
4264
|
+
const record = value;
|
|
4265
|
+
if (!record || !CLI_NAMES.includes(record.cli) || !PROFILES.includes(record.profile)) continue;
|
|
4266
|
+
file.proposals[id] = {
|
|
4267
|
+
cli: record.cli,
|
|
4268
|
+
profile: record.profile,
|
|
4269
|
+
sessionId: typeof record.sessionId === "string" ? record.sessionId : null,
|
|
4270
|
+
status: record.status === "ready" || record.status === "failed" ? record.status : "working",
|
|
4271
|
+
result: record.result ?? null,
|
|
4272
|
+
error: typeof record.error === "string" ? record.error : null,
|
|
4273
|
+
version: typeof record.version === "number" ? record.version : 0,
|
|
4274
|
+
conversation: Array.isArray(record.conversation) ? record.conversation.filter(
|
|
4275
|
+
(line) => typeof line === "object" && line !== null && (line.from === "you" || line.from === "it") && typeof line.text === "string"
|
|
4276
|
+
) : []
|
|
4277
|
+
};
|
|
3389
4278
|
}
|
|
3390
4279
|
}
|
|
3391
|
-
return
|
|
3392
|
-
}
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
4280
|
+
return file;
|
|
4281
|
+
}
|
|
4282
|
+
function cleanAnswer(value) {
|
|
4283
|
+
if (typeof value !== "object" || value === null) return null;
|
|
4284
|
+
const obj = value;
|
|
4285
|
+
if (obj.later === true) return { later: true };
|
|
4286
|
+
const choices = {};
|
|
4287
|
+
const fields = {};
|
|
4288
|
+
if (typeof obj.choices === "object" && obj.choices !== null) {
|
|
4289
|
+
for (const [id, values] of Object.entries(obj.choices)) {
|
|
4290
|
+
if (Array.isArray(values)) choices[id] = [...new Set(values.filter((v) => typeof v === "string" && v.trim() !== ""))];
|
|
4291
|
+
}
|
|
3398
4292
|
}
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
return true;
|
|
3404
|
-
} catch {
|
|
3405
|
-
return false;
|
|
4293
|
+
if (typeof obj.fields === "object" && obj.fields !== null) {
|
|
4294
|
+
for (const [id, text] of Object.entries(obj.fields)) {
|
|
4295
|
+
if (typeof text === "string") fields[id] = text;
|
|
4296
|
+
}
|
|
3406
4297
|
}
|
|
4298
|
+
return { choices, fields };
|
|
3407
4299
|
}
|
|
3408
|
-
|
|
3409
|
-
return
|
|
4300
|
+
function given(answer) {
|
|
4301
|
+
return answer && answer.later !== true ? answer : null;
|
|
3410
4302
|
}
|
|
3411
|
-
|
|
3412
|
-
let sources = null;
|
|
3413
|
-
try {
|
|
3414
|
-
sources = await readFile7(config.sourcesFile, "utf8");
|
|
3415
|
-
} catch {
|
|
3416
|
-
sources = null;
|
|
3417
|
-
}
|
|
3418
|
-
const supported = process.platform === "darwin";
|
|
4303
|
+
function answerFromCard(card2) {
|
|
3419
4304
|
return {
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
// agent's clock asks the same question.
|
|
3423
|
-
needed: await firstBriefPending(config),
|
|
3424
|
-
profile: config.profile,
|
|
3425
|
-
profileChosen: opts.profileChosen,
|
|
3426
|
-
sources: { exists: sources !== null, placeholders: sources === null ? 0 : placeholdersIn(sources) },
|
|
3427
|
-
clis: await findClis(config),
|
|
3428
|
-
calendar: {
|
|
3429
|
-
supported,
|
|
3430
|
-
built: supported && await exists(config.calendar.appPath),
|
|
3431
|
-
chosen: config.calendar.names.length,
|
|
3432
|
-
buildCommand: command("build-calendar")
|
|
3433
|
-
}
|
|
4305
|
+
choices: Object.fromEntries(card2.choices.map((choice2) => [choice2.id, choice2.selected])),
|
|
4306
|
+
fields: Object.fromEntries(card2.fields.map((field2) => [field2.id, field2.value]))
|
|
3434
4307
|
};
|
|
3435
4308
|
}
|
|
3436
|
-
var
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
4309
|
+
var Onboarding = class {
|
|
4310
|
+
file = null;
|
|
4311
|
+
loaded = false;
|
|
4312
|
+
running = /* @__PURE__ */ new Map();
|
|
4313
|
+
steps = /* @__PURE__ */ new Map();
|
|
4314
|
+
/** Calendar.app's calendars being listed, which only starting the sessions waits for. */
|
|
4315
|
+
calendars = null;
|
|
4316
|
+
/** What listing them found, once it has: the calendars, or why they couldn't be listed. */
|
|
4317
|
+
listed = null;
|
|
4318
|
+
finishError = null;
|
|
4319
|
+
config;
|
|
4320
|
+
deps;
|
|
4321
|
+
spawnFn;
|
|
4322
|
+
gates = /* @__PURE__ */ new Map();
|
|
4323
|
+
codexOff = null;
|
|
4324
|
+
codexModel = null;
|
|
4325
|
+
/** What looking for the CLIs found, once the screen has asked. Not kept: what is installed changes. */
|
|
4326
|
+
detected = null;
|
|
4327
|
+
detecting = null;
|
|
4328
|
+
/** The pass starting sessions, which the next one waits for; see `startProposals`. */
|
|
4329
|
+
starting = Promise.resolve();
|
|
4330
|
+
/** The write in progress, which the next one waits for. */
|
|
4331
|
+
writes = Promise.resolve();
|
|
4332
|
+
constructor(config, deps) {
|
|
4333
|
+
this.config = config;
|
|
4334
|
+
this.deps = deps;
|
|
4335
|
+
this.spawnFn = deps.spawnFn ?? spawn2;
|
|
4336
|
+
}
|
|
4337
|
+
/** Whether a session has a process going. A restart waits for none. */
|
|
4338
|
+
get busy() {
|
|
4339
|
+
return this.running.size > 0;
|
|
4340
|
+
}
|
|
4341
|
+
/**
|
|
4342
|
+
* Read the file, and start again any session the last process left going.
|
|
4343
|
+
* Only while setup is still to do: `needed` is the caller's `firstBriefPending`.
|
|
4344
|
+
*/
|
|
4345
|
+
async start(needed) {
|
|
4346
|
+
await this.load();
|
|
4347
|
+
if (!needed || this.file?.finishedAt) return;
|
|
4348
|
+
for (const record of Object.values(this.file?.proposals ?? {})) {
|
|
4349
|
+
if (record.status === "working") {
|
|
4350
|
+
record.status = "failed";
|
|
4351
|
+
record.error = RESTARTED;
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
this.startProposals();
|
|
4355
|
+
}
|
|
4356
|
+
/** The screen, while setup is to do: the store has never had a brief, and setup hasn't finished. */
|
|
4357
|
+
async view(setup) {
|
|
4358
|
+
await this.load();
|
|
4359
|
+
if (!setup.needed || this.file?.finishedAt) return null;
|
|
4360
|
+
const file = this.file ?? blankFile();
|
|
4361
|
+
const ctx = await this.context({ setup });
|
|
4362
|
+
const sources = this.deps.readSources();
|
|
4363
|
+
const answers = this.effective(ctx, file.answers);
|
|
4364
|
+
const steps = [];
|
|
4365
|
+
for (const question of this.questions(ctx)) {
|
|
4366
|
+
const locked = this.locked(question, sources);
|
|
4367
|
+
const drawn = question.card(ctx);
|
|
4368
|
+
const record = file.proposals[question.id];
|
|
4369
|
+
const answer = locked ? question.id === "cli" ? { choices: { cli: ctx.cli ? [ctx.cli] : [] }, fields: {} } : answerFromCard(drawn) : answers[question.id] ?? null;
|
|
4370
|
+
const chosen = given(answer);
|
|
4371
|
+
const looking = question.id === "cli" && !answer && !this.detected;
|
|
4372
|
+
steps.push({
|
|
4373
|
+
id: question.id,
|
|
4374
|
+
title: question.title(ctx),
|
|
4375
|
+
topic: question.topic,
|
|
4376
|
+
status: answer ? chosen ? "answered" : "later" : "open",
|
|
4377
|
+
summary: chosen ? question.summary(chosen, ctx) : answer ? question.later?.summary ?? null : null,
|
|
4378
|
+
locked,
|
|
4379
|
+
card: { ...drawn, version: record?.version ?? 0, revisable: Boolean(question.agent && record), later: question.later?.label ?? null },
|
|
4380
|
+
proposal: looking ? {
|
|
4381
|
+
status: "working",
|
|
4382
|
+
conversation: [],
|
|
4383
|
+
steps: [{ kind: "tool", text: "Look for Codex and Claude Code", detail: null, state: "running" }],
|
|
4384
|
+
error: null
|
|
4385
|
+
} : record ? {
|
|
4386
|
+
status: record.status,
|
|
4387
|
+
conversation: record.conversation,
|
|
4388
|
+
steps: this.steps.get(question.id) ?? [],
|
|
4389
|
+
error: record.status === "failed" ? record.error : null
|
|
4390
|
+
} : null,
|
|
4391
|
+
answer
|
|
4392
|
+
});
|
|
4393
|
+
}
|
|
3449
4394
|
return {
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
4395
|
+
detected: this.detected !== null,
|
|
4396
|
+
steps,
|
|
4397
|
+
current: steps.find((step) => step.status === "open")?.id ?? null,
|
|
4398
|
+
error: this.finishError
|
|
3453
4399
|
};
|
|
3454
|
-
}
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
4400
|
+
}
|
|
4401
|
+
/**
|
|
4402
|
+
* Look for the CLIs, which the screen asks for when it opens: with one ready,
|
|
4403
|
+
* the first question answers itself. Asked again by Check again.
|
|
4404
|
+
*/
|
|
4405
|
+
async detect() {
|
|
4406
|
+
await this.load();
|
|
4407
|
+
await this.lookForClis(true);
|
|
4408
|
+
this.startProposals();
|
|
4409
|
+
this.deps.onChange();
|
|
4410
|
+
}
|
|
4411
|
+
/**
|
|
4412
|
+
* Record an answer, or that the user would rather decide later. One whose
|
|
4413
|
+
* settings the dashboard would refuse is refused here, while the card that
|
|
4414
|
+
* can fix it is still open.
|
|
4415
|
+
*/
|
|
4416
|
+
async answer(id, raw) {
|
|
4417
|
+
await this.load();
|
|
4418
|
+
const file = this.file ??= blankFile();
|
|
4419
|
+
if (file.finishedAt) return "setup has finished";
|
|
4420
|
+
const question = questionById(id);
|
|
4421
|
+
let answer = cleanAnswer(raw);
|
|
4422
|
+
if (!question || !answer) return `there is no question ${JSON.stringify(id)}, or that is not an answer to it`;
|
|
4423
|
+
if (!given(answer) && !question.later) return `"${question.topic}" needs an answer`;
|
|
4424
|
+
if (id === "cli" && given(answer)) {
|
|
4425
|
+
const chosen = await this.chooseCli(given(answer));
|
|
4426
|
+
if (typeof chosen === "string") return chosen;
|
|
4427
|
+
answer = chosen;
|
|
4428
|
+
}
|
|
4429
|
+
const ctx = await this.context();
|
|
4430
|
+
const before = { cli: ctx.cli, profile: ctx.profile };
|
|
4431
|
+
if (given(answer) && question.settings) {
|
|
4432
|
+
const refused = this.deps.checkSettings(this.settingsOf({ ...file.answers, [id]: answer }, await this.context({ answers: { ...file.answers, [id]: answer } })));
|
|
4433
|
+
if (refused) return refused;
|
|
4434
|
+
}
|
|
4435
|
+
file.answers[id] = answer;
|
|
4436
|
+
this.finishError = null;
|
|
4437
|
+
await this.stop(id, "you continued before it finished");
|
|
4438
|
+
const after = await this.context();
|
|
4439
|
+
if (after.profile !== before.profile) {
|
|
4440
|
+
for (const other of QUESTIONS) {
|
|
4441
|
+
if (!other.agent) continue;
|
|
4442
|
+
delete file.answers[other.id];
|
|
4443
|
+
await this.drop(other.id);
|
|
4444
|
+
}
|
|
4445
|
+
} else if (after.cli !== before.cli) {
|
|
4446
|
+
for (const other of QUESTIONS) if (other.agent && !file.answers[other.id]) await this.drop(other.id);
|
|
4447
|
+
}
|
|
4448
|
+
await this.save();
|
|
4449
|
+
this.startProposals();
|
|
4450
|
+
this.deps.onChange();
|
|
4451
|
+
return null;
|
|
4452
|
+
}
|
|
4453
|
+
/** Tell a question's session what to change. `draft` is what the card holds now, which it should start from. */
|
|
4454
|
+
async revise(id, text, draft) {
|
|
4455
|
+
await this.load();
|
|
4456
|
+
const question = questionById(id);
|
|
4457
|
+
const record = this.file?.proposals[id];
|
|
4458
|
+
if (!question?.agent || !record) return "nothing is looking into that question";
|
|
4459
|
+
if (this.running.has(id)) return "it is still working on that question";
|
|
4460
|
+
const ctx = await this.context();
|
|
4461
|
+
const current = cleanAnswer(draft);
|
|
4462
|
+
const request = [
|
|
4463
|
+
`The user says: ${text.trim()}`,
|
|
4464
|
+
...given(current) ? ["", "What the card holds now, which they may have changed:", "", "```json", JSON.stringify(current), "```"] : [],
|
|
4465
|
+
"",
|
|
4466
|
+
"Revise your proposal, and answer with the whole of it again."
|
|
4467
|
+
].join("\n");
|
|
4468
|
+
const resume = record.sessionId && record.cli === ctx.cli ? record.sessionId : null;
|
|
4469
|
+
const said = [...record.conversation, { from: "you", text: text.trim() }];
|
|
4470
|
+
await this.ask(question, ctx, resume ? request : `${await this.firstMessage(question, ctx)}
|
|
4471
|
+
|
|
4472
|
+
${request}`, resume, said);
|
|
4473
|
+
return null;
|
|
4474
|
+
}
|
|
4475
|
+
/**
|
|
4476
|
+
* Look again: ask a question's session again from the start, after it failed,
|
|
4477
|
+
* and list Calendar.app's calendars again for the question that shows them,
|
|
4478
|
+
* since what failed may have been the access the user has since granted.
|
|
4479
|
+
*/
|
|
4480
|
+
async retry(id) {
|
|
4481
|
+
await this.load();
|
|
4482
|
+
const question = questionById(id);
|
|
4483
|
+
if (!question) return `there is no question ${JSON.stringify(id)}`;
|
|
4484
|
+
if (this.running.has(id)) return "it is still working on that question";
|
|
4485
|
+
if (id === "calendars") {
|
|
4486
|
+
this.calendars = null;
|
|
4487
|
+
this.listed = null;
|
|
4488
|
+
}
|
|
4489
|
+
if (id === "cli") await this.lookForClis(true);
|
|
4490
|
+
if (question.agent) await this.drop(id);
|
|
4491
|
+
this.startProposals();
|
|
4492
|
+
this.deps.onChange();
|
|
4493
|
+
return null;
|
|
4494
|
+
}
|
|
4495
|
+
/**
|
|
4496
|
+
* Save everything, and ask for the first brief. Files first, then the
|
|
4497
|
+
* settings, whose save restarts the dashboard: the start after it begins the
|
|
4498
|
+
* run. Resolves with why not, or null.
|
|
4499
|
+
*/
|
|
4500
|
+
async finish() {
|
|
4501
|
+
await this.load();
|
|
4502
|
+
const file = this.file ??= blankFile();
|
|
4503
|
+
if (file.finishedAt) return { error: "setup has already finished" };
|
|
4504
|
+
if (!file.answers.cli) await this.lookForClis(false);
|
|
4505
|
+
const ctx = await this.context();
|
|
4506
|
+
const sources = this.deps.readSources();
|
|
4507
|
+
const answers = this.effective(ctx, file.answers);
|
|
4508
|
+
const sections = [];
|
|
4509
|
+
let objective2 = null;
|
|
4510
|
+
for (const question of this.questions(ctx)) {
|
|
4511
|
+
const answer = answers[question.id];
|
|
4512
|
+
if (!answer && !this.locked(question, sources)) return this.refuse(`answer "${question.title(ctx)}", or decide it later`);
|
|
4513
|
+
const chosen = this.locked(question, sources) ? null : given(answer);
|
|
4514
|
+
if (!chosen) continue;
|
|
4515
|
+
sections.push(...question.sources?.(chosen, ctx) ?? []);
|
|
4516
|
+
objective2 = question.objective?.(chosen) ?? objective2;
|
|
4517
|
+
}
|
|
4518
|
+
const values = this.settingsOf(file.answers, ctx);
|
|
4519
|
+
const refused = this.deps.checkSettings(values);
|
|
4520
|
+
if (refused) return this.refuse(refused);
|
|
4521
|
+
await this.starting;
|
|
4522
|
+
for (const id of [...this.running.keys()]) await this.stop(id, "setup finished");
|
|
4523
|
+
const existing = await readEditable(this.config.sourcesFile);
|
|
4524
|
+
if (existing.text === null && sections.length > 0) {
|
|
4525
|
+
await mkdir2(this.config.dataDir, { recursive: true });
|
|
4526
|
+
await saveEditable(this.config.sourcesFile, composeSources(ctx.profile, sections, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)), ABSENT);
|
|
4527
|
+
}
|
|
4528
|
+
if (objective2) {
|
|
4529
|
+
const focus = await readEditable(this.config.focusFile);
|
|
4530
|
+
const saved = await saveEditable(this.config.focusFile, setFocusFields(focus.text, objective2, FOCUS_TEMPLATE), focus.version);
|
|
4531
|
+
if (!saved.saved) return this.refuse("focus.md changed on disk while saving; finish again");
|
|
4532
|
+
}
|
|
4533
|
+
file.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4534
|
+
file.briefRequested = ctx.cli !== null;
|
|
4535
|
+
await this.save();
|
|
4536
|
+
const before = JSON.stringify(Object.entries(sources.settings).sort());
|
|
4537
|
+
const saveRefused = await this.deps.saveSettings(values);
|
|
4538
|
+
if (saveRefused) {
|
|
4539
|
+
file.finishedAt = null;
|
|
4540
|
+
file.briefRequested = false;
|
|
4541
|
+
await this.save();
|
|
4542
|
+
return this.refuse(saveRefused);
|
|
4543
|
+
}
|
|
4544
|
+
this.finishError = null;
|
|
4545
|
+
this.deps.onChange();
|
|
4546
|
+
return { restart: JSON.stringify(Object.entries(this.deps.readSources().settings).sort()) !== before };
|
|
4547
|
+
}
|
|
4548
|
+
/**
|
|
4549
|
+
* Whether the finish asked for a first brief that hasn't been started, and
|
|
4550
|
+
* forget it: asked once, so a run that fails is retried by the user, not by
|
|
4551
|
+
* every start.
|
|
4552
|
+
*/
|
|
4553
|
+
async takeBriefRequest() {
|
|
4554
|
+
await this.load();
|
|
4555
|
+
if (!this.file?.briefRequested) return false;
|
|
4556
|
+
this.file.briefRequested = false;
|
|
4557
|
+
await this.save();
|
|
4558
|
+
return true;
|
|
4559
|
+
}
|
|
4560
|
+
/** What the finish would write as the source list, or null when the store already has one it would keep. */
|
|
4561
|
+
async previewSources() {
|
|
4562
|
+
await this.load();
|
|
4563
|
+
const existing = await readEditable(this.config.sourcesFile);
|
|
4564
|
+
if (existing.text !== null) return { text: null, kept: true };
|
|
4565
|
+
const ctx = await this.context();
|
|
4566
|
+
const sections = [];
|
|
4567
|
+
for (const question of this.questions(ctx)) {
|
|
4568
|
+
const chosen = given(this.file?.answers[question.id]);
|
|
4569
|
+
if (chosen) sections.push(...question.sources?.(chosen, ctx) ?? []);
|
|
4570
|
+
}
|
|
4571
|
+
return { text: sections.length ? composeSources(ctx.profile, sections, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) : null, kept: false };
|
|
4572
|
+
}
|
|
4573
|
+
/** Stop every session, for shutdown, and wait until each has exited and been written down. */
|
|
4574
|
+
async close() {
|
|
4575
|
+
await this.starting;
|
|
4576
|
+
const over = [...this.running.values()].map((run5) => run5.over);
|
|
4577
|
+
for (const id of [...this.running.keys()]) await this.stop(id, RESTARTED);
|
|
4578
|
+
await Promise.all(over);
|
|
4579
|
+
}
|
|
4580
|
+
/* ---------- the sessions ---------- */
|
|
4581
|
+
/**
|
|
4582
|
+
* Start whatever sessions the answers so far call for, without holding up the
|
|
4583
|
+
* reply that asked: starting one under Codex runs a few checks first, and a
|
|
4584
|
+
* screen waiting on them has every button greyed out with nothing to say why.
|
|
4585
|
+
* One pass at a time, so two quick answers can't both start the same question.
|
|
4586
|
+
*/
|
|
4587
|
+
startProposals() {
|
|
4588
|
+
this.starting = this.starting.then(() => this.ensureProposals()).catch((err) => console.error(`[daily-focus] setup could not start looking: ${err.message}`));
|
|
4589
|
+
}
|
|
4590
|
+
/** Resolves once the sessions asked for so far have started: for tests, and for anything that must not race them. */
|
|
4591
|
+
started() {
|
|
4592
|
+
return this.starting;
|
|
4593
|
+
}
|
|
4594
|
+
/** Start a session for each question that has one and needs it: the CLI and the profile are known, and nobody has asked yet. */
|
|
4595
|
+
async ensureProposals() {
|
|
4596
|
+
const file = this.file;
|
|
4597
|
+
if (!file || file.finishedAt) return;
|
|
4598
|
+
if (Object.keys(file.answers).length === 0) return;
|
|
4599
|
+
const set = (this.deps.readSources().process.DAILY_FOCUS_AGENT ?? "").trim();
|
|
4600
|
+
if (file.answers.cli && !given(file.answers.cli) || set && !CLI_NAMES.includes(set)) return;
|
|
4601
|
+
await this.lookForClis(false);
|
|
4602
|
+
const ctx = await this.context({ listCalendars: true });
|
|
4603
|
+
const sources = this.deps.readSources();
|
|
4604
|
+
const profileKnown = given(file.answers.profile) !== null || (sources.process.DAILY_FOCUS_PROFILE ?? "").trim() !== "";
|
|
4605
|
+
if (!ctx.cli || !profileKnown) return;
|
|
4606
|
+
for (const question of this.questions(ctx)) {
|
|
4607
|
+
if (!question.agent || file.answers[question.id] || this.running.has(question.id)) continue;
|
|
4608
|
+
const record = file.proposals[question.id];
|
|
4609
|
+
if (record && record.cli === ctx.cli && record.profile === ctx.profile && record.status !== "failed") continue;
|
|
4610
|
+
if (record?.status === "failed" && record.error !== RESTARTED && record.cli === ctx.cli && record.profile === ctx.profile) continue;
|
|
4611
|
+
await this.ask(question, ctx, await this.firstMessage(question, ctx), null);
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
/** The first message of a question's session: the instructions, what is known, and the question. */
|
|
4615
|
+
async firstMessage(question, ctx) {
|
|
4616
|
+
const instructions = await readFile8(this.config.onboardingPromptSource, "utf8").catch(
|
|
4617
|
+
() => "You are helping someone set up Daily Focus. Only read; never send, post or edit anything. Answer with the JSON the schema asks for."
|
|
4618
|
+
);
|
|
4619
|
+
const agent = question.agent;
|
|
4620
|
+
const facts = agent.facts?.(ctx) ?? null;
|
|
4621
|
+
return [
|
|
4622
|
+
instructions.trim(),
|
|
4623
|
+
"",
|
|
4624
|
+
"## This setup",
|
|
4625
|
+
"",
|
|
4626
|
+
`- Profile: ${ctx.profile}`,
|
|
4627
|
+
`- Today: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`,
|
|
4628
|
+
"",
|
|
4629
|
+
`## The question: ${question.title(ctx)}`,
|
|
4630
|
+
"",
|
|
4631
|
+
agent.brief(ctx),
|
|
4632
|
+
...facts ? ["", "### What the dashboard already knows", "", facts] : []
|
|
4633
|
+
].join("\n");
|
|
4634
|
+
}
|
|
4635
|
+
async ask(question, ctx, prompt, resume, conversation = []) {
|
|
4636
|
+
const file = this.file ??= blankFile();
|
|
4637
|
+
const cli2 = ctx.cli;
|
|
4638
|
+
const previous = file.proposals[question.id];
|
|
4639
|
+
const record = {
|
|
4640
|
+
cli: cli2,
|
|
4641
|
+
profile: ctx.profile,
|
|
4642
|
+
sessionId: resume,
|
|
4643
|
+
status: "working",
|
|
4644
|
+
result: previous?.cli === cli2 && previous.profile === ctx.profile ? previous.result : null,
|
|
4645
|
+
error: null,
|
|
4646
|
+
version: previous?.version ?? 0,
|
|
4647
|
+
conversation
|
|
4648
|
+
};
|
|
4649
|
+
file.proposals[question.id] = record;
|
|
4650
|
+
this.steps.set(question.id, []);
|
|
4651
|
+
await this.save();
|
|
4652
|
+
this.deps.onChange();
|
|
4653
|
+
const bin = ctx.clis?.[cli2]?.path ?? null;
|
|
4654
|
+
if (!bin) return this.settle(question.id, record, { ok: false, error: `could not find ${cli2} on this machine` });
|
|
4655
|
+
if (cli2 === "codex") {
|
|
4656
|
+
const gate = this.gates.get(bin) ?? new CodexGate(bin, this.deps.execFn);
|
|
4657
|
+
this.gates.set(bin, gate);
|
|
4658
|
+
const refusal2 = await gate.refusal();
|
|
4659
|
+
if (refusal2) return this.settle(question.id, record, { ok: false, error: refusal2 });
|
|
4660
|
+
}
|
|
4661
|
+
let codexConfig = [];
|
|
4662
|
+
if (cli2 === "codex") {
|
|
4663
|
+
try {
|
|
4664
|
+
this.codexOff ??= computerUseOff(bin, this.deps.execFn);
|
|
4665
|
+
codexConfig = await this.codexOff;
|
|
4666
|
+
} catch (err) {
|
|
4667
|
+
this.codexOff = null;
|
|
4668
|
+
return this.settle(question.id, record, { ok: false, error: `Codex could not say which plugins to switch off: ${err.message}` });
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4671
|
+
const schemaDir = await mkdtemp(join5(tmpdir2(), "daily-focus-setup-"));
|
|
4672
|
+
const schemaFile = join5(schemaDir, "schema.json");
|
|
4673
|
+
const json = JSON.stringify(question.agent.schema);
|
|
4674
|
+
await writeFile2(schemaFile, json, "utf8");
|
|
4675
|
+
await mkdir2(this.config.onboardingDir, { recursive: true });
|
|
4676
|
+
const adapter = ADAPTERS[cli2];
|
|
4677
|
+
const session = SESSION_MODEL[cli2];
|
|
4678
|
+
const model = cli2 === "codex" ? await (this.codexModel ??= codexLatestModel(bin, this.deps.execFn)) : session.model;
|
|
4679
|
+
const args = adapter.args({
|
|
4680
|
+
prompt,
|
|
4681
|
+
sessionId: resume,
|
|
4682
|
+
model,
|
|
4683
|
+
effort: session.effort,
|
|
4684
|
+
tools: ONBOARDING_TOOLS,
|
|
4685
|
+
denyEdits: true,
|
|
4686
|
+
drafts: false,
|
|
4687
|
+
codexConfig,
|
|
4688
|
+
schema: { json, file: schemaFile }
|
|
4689
|
+
});
|
|
4690
|
+
let child;
|
|
4691
|
+
try {
|
|
4692
|
+
child = this.spawnFn(bin, args, { cwd: this.config.onboardingDir, stdio: ["ignore", "pipe", "pipe"], env: process.env });
|
|
4693
|
+
} catch (err) {
|
|
4694
|
+
await rm2(schemaDir, { recursive: true, force: true });
|
|
4695
|
+
return this.settle(question.id, record, { ok: false, error: `could not run ${bin}: ${err.message}` });
|
|
4696
|
+
}
|
|
4697
|
+
const timer = setTimeout(() => void this.stop(question.id, "gave up after four minutes"), TURN_TIMEOUT_MS2);
|
|
4698
|
+
timer.unref();
|
|
4699
|
+
let ended;
|
|
4700
|
+
const run5 = { child, timer, abortReason: null, over: new Promise((resolve2) => ended = resolve2) };
|
|
4701
|
+
this.running.set(question.id, run5);
|
|
4702
|
+
let done;
|
|
4703
|
+
let said = "";
|
|
4704
|
+
const stderr = [];
|
|
4705
|
+
child.stderr?.on("data", (chunk) => {
|
|
4706
|
+
stderr.push(chunk.toString("utf8"));
|
|
4707
|
+
if (stderr.length > 50) stderr.shift();
|
|
4708
|
+
});
|
|
4709
|
+
const calls = /* @__PURE__ */ new Map();
|
|
4710
|
+
const steps = this.steps.get(question.id);
|
|
4711
|
+
const add = (step) => {
|
|
4712
|
+
steps.push(step);
|
|
4713
|
+
if (steps.length > STEPS_KEPT) steps.shift();
|
|
4714
|
+
};
|
|
4715
|
+
if (child.stdout) {
|
|
4716
|
+
createInterface2({ input: child.stdout }).on("line", (line) => {
|
|
4717
|
+
const event = adapter.parse(line);
|
|
4718
|
+
if (!event) return;
|
|
4719
|
+
if (event.sessionId) record.sessionId = event.sessionId;
|
|
4720
|
+
if (event.text) said = event.text;
|
|
4721
|
+
for (const parsed of event.steps ?? []) {
|
|
4722
|
+
const known = calls.get(parsed.id);
|
|
4723
|
+
if (known) {
|
|
4724
|
+
if (parsed.text) known.text = parsed.text;
|
|
4725
|
+
if (parsed.detail) known.detail = parsed.detail;
|
|
4726
|
+
known.state = parsed.state;
|
|
4727
|
+
} else if (parsed.text) {
|
|
4728
|
+
const step = { kind: "tool", text: parsed.text, detail: parsed.detail ?? null, state: parsed.state };
|
|
4729
|
+
calls.set(parsed.id, step);
|
|
4730
|
+
add(step);
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4733
|
+
if (event.steps) this.deps.onChange();
|
|
4734
|
+
if (event.done) done = event.done;
|
|
4735
|
+
});
|
|
4736
|
+
}
|
|
4737
|
+
child.on("error", (err) => {
|
|
4738
|
+
done = { ok: false, error: `could not run ${bin}: ${err.message}` };
|
|
4739
|
+
});
|
|
4740
|
+
child.on("exit", () => {
|
|
4741
|
+
if (run5.abortReason === null) return;
|
|
4742
|
+
child.stdout?.destroy();
|
|
4743
|
+
child.stderr?.destroy();
|
|
4744
|
+
});
|
|
4745
|
+
child.on("close", (code) => {
|
|
4746
|
+
clearTimeout(timer);
|
|
4747
|
+
this.running.delete(question.id);
|
|
4748
|
+
void rm2(schemaDir, { recursive: true, force: true });
|
|
4749
|
+
const outcome = run5.abortReason ? { ok: false, error: run5.abortReason } : done ?? {
|
|
4750
|
+
ok: false,
|
|
4751
|
+
error: `${cli2} exited with code ${code}${stderr.length ? `: ${stderr.join("").trim().split("\n").slice(-3).join(" ")}` : ""}`
|
|
4752
|
+
};
|
|
4753
|
+
void this.settle(question.id, record, outcome.ok ? { ok: true, reply: outcome.reply ?? said } : outcome).catch((err) => console.error(`[daily-focus] setup could not keep what it found: ${err.message}`)).finally(ended);
|
|
4754
|
+
});
|
|
4755
|
+
}
|
|
4756
|
+
/** A session is over: keep its answer when it is JSON, or why it isn't. */
|
|
4757
|
+
async settle(id, record, outcome) {
|
|
4758
|
+
if (this.file?.proposals[id] !== record) return;
|
|
4759
|
+
for (const step of this.steps.get(id) ?? []) if (step.state === "running") step.state = outcome.ok ? "done" : "failed";
|
|
4760
|
+
if (outcome.ok) {
|
|
4761
|
+
try {
|
|
4762
|
+
record.result = JSON.parse(stripFence(outcome.reply ?? ""));
|
|
4763
|
+
record.status = "ready";
|
|
4764
|
+
record.error = null;
|
|
4765
|
+
record.version += 1;
|
|
4766
|
+
const why = record.result?.why;
|
|
4767
|
+
if (typeof why === "string" && why.trim()) record.conversation.push({ from: "it", text: why.trim() });
|
|
4768
|
+
} catch {
|
|
4769
|
+
record.status = "failed";
|
|
4770
|
+
record.error = "it answered, but not in the shape asked for";
|
|
4771
|
+
}
|
|
4772
|
+
} else {
|
|
4773
|
+
record.status = "failed";
|
|
4774
|
+
record.error = outcome.error ?? "failed";
|
|
4775
|
+
}
|
|
4776
|
+
await this.save();
|
|
4777
|
+
this.deps.onChange();
|
|
4778
|
+
}
|
|
4779
|
+
/** Stop a session: asked to, then made to if it hasn't gone within a few seconds. */
|
|
4780
|
+
async stop(id, reason) {
|
|
4781
|
+
const run5 = this.running.get(id);
|
|
4782
|
+
if (!run5 || run5.abortReason !== null) return;
|
|
4783
|
+
run5.abortReason = reason;
|
|
4784
|
+
run5.child.kill("SIGTERM");
|
|
4785
|
+
const kill = setTimeout(() => {
|
|
4786
|
+
if (run5.child.exitCode === null && run5.child.signalCode === null) run5.child.kill("SIGKILL");
|
|
4787
|
+
}, KILL_AFTER_MS);
|
|
4788
|
+
kill.unref();
|
|
4789
|
+
void run5.over.then(() => clearTimeout(kill));
|
|
4790
|
+
}
|
|
4791
|
+
/** Forget a question's proposal, stopping its session first. */
|
|
4792
|
+
async drop(id) {
|
|
4793
|
+
await this.stop(id, "no longer needed");
|
|
4794
|
+
if (this.file) delete this.file.proposals[id];
|
|
4795
|
+
this.steps.delete(id);
|
|
4796
|
+
}
|
|
4797
|
+
/* ---------- what a question may read ---------- */
|
|
4798
|
+
/**
|
|
4799
|
+
* Look for the CLIs, once per process unless `again`. Only from a click or
|
|
4800
|
+
* from setup already under way: `--version` and a login check are programs
|
|
4801
|
+
* run on the user's behalf, and a new store's state is read before anyone is
|
|
4802
|
+
* at the screen.
|
|
4803
|
+
*/
|
|
4804
|
+
async lookForClis(again) {
|
|
4805
|
+
if (this.detected && !again) return;
|
|
4806
|
+
this.detecting ??= this.deps.detectClis().finally(() => {
|
|
4807
|
+
this.detecting = null;
|
|
4808
|
+
});
|
|
4809
|
+
this.detected = await this.detecting;
|
|
4810
|
+
}
|
|
4811
|
+
/**
|
|
4812
|
+
* The CLI answer to keep: one of the ready ones, or a path the user typed,
|
|
4813
|
+
* which is used only once it has said what it is and that it is ready.
|
|
4814
|
+
* Otherwise why not, in a sentence the card shows.
|
|
4815
|
+
*/
|
|
4816
|
+
async chooseCli(answer) {
|
|
4817
|
+
const typed = answer.fields.path?.trim();
|
|
4818
|
+
if (!typed) {
|
|
4819
|
+
const picked = answer.choices.cli?.[0];
|
|
4820
|
+
await this.lookForClis(false);
|
|
4821
|
+
if (picked && this.detected?.[picked]?.ready) return { choices: { cli: [picked] }, fields: {} };
|
|
4822
|
+
return "Enter the path to codex or claude, or decide this later";
|
|
4823
|
+
}
|
|
4824
|
+
const { cli: cli2, found } = await this.deps.probeCli(expandHome(typed));
|
|
4825
|
+
if (!cli2 || !found.ready) {
|
|
4826
|
+
return `${found.problem ?? "That can't be used"}${found.fix ? `: run ${found.fix} in a terminal, then try again.` : "."}`;
|
|
4827
|
+
}
|
|
4828
|
+
await this.lookForClis(false);
|
|
4829
|
+
this.detected = { ...this.detected, [cli2]: found };
|
|
4830
|
+
return { choices: { cli: [cli2] }, fields: { path: found.path } };
|
|
4831
|
+
}
|
|
4832
|
+
/**
|
|
4833
|
+
* The answers as they stand, with the CLI setup picked without asking when
|
|
4834
|
+
* the user hasn't picked one: the first ready, or the one already set.
|
|
4835
|
+
*/
|
|
4836
|
+
effective(ctx, answers) {
|
|
4837
|
+
if (answers.cli || !ctx.cli) return { ...answers };
|
|
4838
|
+
return { ...answers, cli: { choices: { cli: [ctx.cli] }, fields: {} } };
|
|
4839
|
+
}
|
|
4840
|
+
/** Set in the environment, which wins over setup: shown as it stands, never asked. */
|
|
4841
|
+
locked(question, sources) {
|
|
4842
|
+
return question.keys.length > 0 && question.keys.every((key) => (sources.process[key] ?? "").trim() !== "");
|
|
4843
|
+
}
|
|
4844
|
+
/**
|
|
4845
|
+
* The settings these answers add up to, in question order, less what the
|
|
4846
|
+
* environment sets: that can't be saved over, and wins anyway.
|
|
4847
|
+
*/
|
|
4848
|
+
settingsOf(answers, ctx) {
|
|
4849
|
+
const sources = this.deps.readSources();
|
|
4850
|
+
const values = {};
|
|
4851
|
+
const effective = this.effective(ctx, answers);
|
|
4852
|
+
for (const question of this.questions(ctx)) {
|
|
4853
|
+
const chosen = this.locked(question, sources) ? null : given(effective[question.id]);
|
|
4854
|
+
if (chosen) Object.assign(values, question.settings?.(chosen, ctx) ?? {});
|
|
4855
|
+
}
|
|
4856
|
+
for (const key of Object.keys(values)) if ((sources.process[key] ?? "").trim() !== "") delete values[key];
|
|
4857
|
+
return values;
|
|
4858
|
+
}
|
|
4859
|
+
questions(ctx) {
|
|
4860
|
+
return QUESTIONS.filter((question) => !question.applies || question.applies(ctx));
|
|
4861
|
+
}
|
|
4862
|
+
/**
|
|
4863
|
+
* Everything a question may read. The CLI and the profile are the answers
|
|
4864
|
+
* when given, else what is in effect, else the card's own pick, so the
|
|
4865
|
+
* questions that follow from them are counted from the start.
|
|
4866
|
+
*
|
|
4867
|
+
* Calendar.app's calendars only as already listed, unless `listCalendars`:
|
|
4868
|
+
* the helper may ask for calendar access, so it is launched only from a
|
|
4869
|
+
* click, never from a state read, which any GET makes.
|
|
4870
|
+
*/
|
|
4871
|
+
async context(opts = {}) {
|
|
4872
|
+
const sources = this.deps.readSources();
|
|
4873
|
+
const env = layerEnv(sources);
|
|
4874
|
+
const current = {};
|
|
4875
|
+
for (const def of SETTINGS) {
|
|
4876
|
+
const value = (env[def.key] ?? "").trim();
|
|
4877
|
+
if (value) current[def.key] = value;
|
|
4878
|
+
}
|
|
4879
|
+
const setup = opts.setup ?? await this.deps.setup();
|
|
4880
|
+
const answers = opts.answers ?? this.file?.answers ?? {};
|
|
4881
|
+
const profileAnswer = given(answers.profile)?.choices.profile?.[0];
|
|
4882
|
+
const profile2 = PROFILES.includes(profileAnswer) ? profileAnswer : current.DAILY_FOCUS_PROFILE === "personal" ? "personal" : "work";
|
|
4883
|
+
const cliAnswer = given(answers.cli);
|
|
4884
|
+
const typed = cliAnswer?.fields.path?.trim();
|
|
4885
|
+
const answered = cliAnswer?.choices.cli?.[0];
|
|
4886
|
+
let clis = this.detected;
|
|
4887
|
+
if (clis && typed && answered && CLI_NAMES.includes(answered) && clis[answered].path !== typed) {
|
|
4888
|
+
clis = { ...clis, [answered]: { ...clis[answered], path: typed, onPath: false, ready: true, problem: null, fix: null } };
|
|
4889
|
+
}
|
|
4890
|
+
const set = current.DAILY_FOCUS_AGENT;
|
|
4891
|
+
const cliLocked = (sources.process.DAILY_FOCUS_AGENT ?? "").trim() !== "";
|
|
4892
|
+
const cli2 = answers.cli ? answered ?? null : cliLocked ? set && CLI_NAMES.includes(set) ? set : null : set && clis?.[set]?.ready ? set : clis ? preferredCli(clis) : null;
|
|
4893
|
+
const started = Object.keys(answers).length > 0;
|
|
4894
|
+
const profileKnown = started && (given(answers.profile) !== null || (sources.process.DAILY_FOCUS_PROFILE ?? "").trim() !== "");
|
|
4895
|
+
let calendars2 = null;
|
|
4896
|
+
const liveAgenda = !["off", "false", "0", "no"].includes((current.DAILY_FOCUS_CALENDAR ?? "").toLowerCase());
|
|
4897
|
+
if (setup.calendar.supported && setup.calendar.built && liveAgenda) {
|
|
4898
|
+
if (opts.listCalendars === true && profileKnown) {
|
|
4899
|
+
this.calendars ??= this.deps.listCalendars().catch((err) => err instanceof Error ? err.message : String(err)).then((listed2) => {
|
|
4900
|
+
this.listed = listed2;
|
|
4901
|
+
});
|
|
4902
|
+
await this.calendars;
|
|
4903
|
+
}
|
|
4904
|
+
calendars2 = this.listed;
|
|
4905
|
+
}
|
|
4906
|
+
const proposals = {};
|
|
4907
|
+
for (const [id, record] of Object.entries(this.file?.proposals ?? {})) {
|
|
4908
|
+
if (record.cli === cli2 && record.profile === profile2 && record.result !== null) proposals[id] = record.result;
|
|
4909
|
+
}
|
|
4910
|
+
return {
|
|
4911
|
+
machine: machineName(),
|
|
4912
|
+
profile: profile2,
|
|
4913
|
+
cli: cli2 && CLI_NAMES.includes(cli2) ? cli2 : null,
|
|
4914
|
+
clis,
|
|
4915
|
+
identity: started ? await this.deps.identity() : { name: null, login: null, logins: [], email: null },
|
|
4916
|
+
calendars: calendars2,
|
|
4917
|
+
calendar: setup.calendar,
|
|
4918
|
+
boards: await this.deps.boards(),
|
|
4919
|
+
current,
|
|
4920
|
+
proposals
|
|
4921
|
+
};
|
|
4922
|
+
}
|
|
4923
|
+
/* ---------- the file ---------- */
|
|
4924
|
+
async load() {
|
|
4925
|
+
if (this.loaded) return;
|
|
4926
|
+
this.loaded = true;
|
|
4927
|
+
try {
|
|
4928
|
+
this.file = parseFile(await readFile8(this.config.onboardingFile, "utf8"));
|
|
4929
|
+
} catch {
|
|
4930
|
+
this.file = null;
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4933
|
+
/**
|
|
4934
|
+
* Write the file as it is now. One write at a time: sessions settle side by
|
|
4935
|
+
* side, and two renames of the same temporary file would trip over each other.
|
|
4936
|
+
*/
|
|
4937
|
+
save() {
|
|
4938
|
+
const write = this.writes.then(async () => {
|
|
4939
|
+
if (!this.file) return;
|
|
4940
|
+
await mkdir2(this.config.dataDir, { recursive: true });
|
|
4941
|
+
await writeJsonAtomic(this.config.onboardingFile, this.file);
|
|
4942
|
+
});
|
|
4943
|
+
this.writes = write.catch(() => {
|
|
4944
|
+
});
|
|
4945
|
+
return write;
|
|
4946
|
+
}
|
|
4947
|
+
refuse(error) {
|
|
4948
|
+
this.finishError = error;
|
|
4949
|
+
this.deps.onChange();
|
|
4950
|
+
return { error };
|
|
4951
|
+
}
|
|
4952
|
+
};
|
|
4953
|
+
function stripFence(text) {
|
|
4954
|
+
const fenced = /^```(?:json)?\s*\n([\s\S]*?)\n```\s*$/.exec(text.trim());
|
|
4955
|
+
return fenced ? fenced[1] : text.trim();
|
|
3463
4956
|
}
|
|
3464
4957
|
|
|
3465
4958
|
// src/shellpath.ts
|
|
3466
|
-
import { spawn as
|
|
4959
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
3467
4960
|
import { userInfo } from "node:os";
|
|
3468
4961
|
var START = "__DAILY_FOCUS_PATH_START__";
|
|
3469
4962
|
var END = "__DAILY_FOCUS_PATH_END__";
|
|
3470
4963
|
var SHELL_TIMEOUT_MS = 5e3;
|
|
3471
4964
|
var defaultExec = (file, args, { timeout }) => new Promise((resolve2, reject) => {
|
|
3472
|
-
const child =
|
|
4965
|
+
const child = spawn3(file, args, { detached: true, stdio: ["ignore", "pipe", "ignore"] });
|
|
3473
4966
|
let output = "";
|
|
3474
4967
|
let settled = false;
|
|
3475
4968
|
const finish = (error) => {
|
|
@@ -3490,8 +4983,8 @@ var defaultExec = (file, args, { timeout }) => new Promise((resolve2, reject) =>
|
|
|
3490
4983
|
child.on("error", (error) => finish(error));
|
|
3491
4984
|
child.on("close", () => finish(null));
|
|
3492
4985
|
});
|
|
3493
|
-
function shellCandidates(envShell, loginShell,
|
|
3494
|
-
const fallback =
|
|
4986
|
+
function shellCandidates(envShell, loginShell, platform) {
|
|
4987
|
+
const fallback = platform === "darwin" ? "/bin/zsh" : platform === "linux" ? "/bin/bash" : void 0;
|
|
3495
4988
|
const shells = [];
|
|
3496
4989
|
for (const shell of [envShell, loginShell, fallback]) {
|
|
3497
4990
|
const trimmed = shell?.trim();
|
|
@@ -3536,11 +5029,11 @@ function currentLoginShell() {
|
|
|
3536
5029
|
return void 0;
|
|
3537
5030
|
}
|
|
3538
5031
|
}
|
|
3539
|
-
async function hydratePath(env = process.env,
|
|
3540
|
-
if (
|
|
5032
|
+
async function hydratePath(env = process.env, platform = process.platform, exec = defaultExec, loginShell = currentLoginShell()) {
|
|
5033
|
+
if (platform === "win32") return null;
|
|
3541
5034
|
let found = null;
|
|
3542
5035
|
let from = null;
|
|
3543
|
-
for (const shell of shellCandidates(env.SHELL, loginShell,
|
|
5036
|
+
for (const shell of shellCandidates(env.SHELL, loginShell, platform)) {
|
|
3544
5037
|
try {
|
|
3545
5038
|
found = await readLoginShellPath(shell, exec);
|
|
3546
5039
|
} catch {
|
|
@@ -3550,7 +5043,7 @@ async function hydratePath(env = process.env, platform2 = process.platform, exec
|
|
|
3550
5043
|
break;
|
|
3551
5044
|
}
|
|
3552
5045
|
}
|
|
3553
|
-
if (!found &&
|
|
5046
|
+
if (!found && platform === "darwin") {
|
|
3554
5047
|
found = await readLaunchctlPath(exec);
|
|
3555
5048
|
if (found) from = "launchctl";
|
|
3556
5049
|
}
|
|
@@ -3561,7 +5054,7 @@ async function hydratePath(env = process.env, platform2 = process.platform, exec
|
|
|
3561
5054
|
|
|
3562
5055
|
// src/server.ts
|
|
3563
5056
|
var PUBLIC_DIR = resolve(fileURLToPath(new URL("../public", import.meta.url)));
|
|
3564
|
-
var
|
|
5057
|
+
var VERSION2 = packageVersion();
|
|
3565
5058
|
var MIME = {
|
|
3566
5059
|
".html": "text/html; charset=utf-8",
|
|
3567
5060
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -3576,15 +5069,8 @@ var TEXT_FILES = {
|
|
|
3576
5069
|
"/api/text/focus": { file: "focusFile", name: "focus.md" },
|
|
3577
5070
|
"/api/text/sources": { file: "sourcesFile", name: "sources.md" }
|
|
3578
5071
|
};
|
|
3579
|
-
var VALID_ACTIONS =
|
|
3580
|
-
"done",
|
|
3581
|
-
"snooze",
|
|
3582
|
-
"dismiss",
|
|
3583
|
-
"reopen",
|
|
3584
|
-
"note"
|
|
3585
|
-
]);
|
|
5072
|
+
var VALID_ACTIONS = new Set(ACTION_TYPES);
|
|
3586
5073
|
var MAX_BODY_BYTES = 64 * 1024;
|
|
3587
|
-
var PRESENCE_POLL_MS = 3e4;
|
|
3588
5074
|
var AGENT_CLOCK_MS = 3e4;
|
|
3589
5075
|
function sendJSON(res, status, body) {
|
|
3590
5076
|
const payload = JSON.stringify(body);
|
|
@@ -3611,7 +5097,7 @@ async function readBody(req) {
|
|
|
3611
5097
|
}
|
|
3612
5098
|
async function serveStatic(res, urlPath) {
|
|
3613
5099
|
const relative = normalize(decodeURIComponent(urlPath === "/" ? "/index.html" : urlPath));
|
|
3614
|
-
const filePath =
|
|
5100
|
+
const filePath = join6(PUBLIC_DIR, relative);
|
|
3615
5101
|
if (filePath !== PUBLIC_DIR && !filePath.startsWith(PUBLIC_DIR + sep)) {
|
|
3616
5102
|
res.writeHead(403).end("Forbidden");
|
|
3617
5103
|
return;
|
|
@@ -3661,7 +5147,7 @@ function configure(sources) {
|
|
|
3661
5147
|
function statusOf(state, config, busy) {
|
|
3662
5148
|
const last = state.agentRun.last;
|
|
3663
5149
|
return {
|
|
3664
|
-
version:
|
|
5150
|
+
version: VERSION2,
|
|
3665
5151
|
dataDir: config.dataDir,
|
|
3666
5152
|
profile: config.profile,
|
|
3667
5153
|
setupNeeded: state.setup.needed,
|
|
@@ -3700,34 +5186,123 @@ async function startServer(env, options = {}) {
|
|
|
3700
5186
|
let restart = null;
|
|
3701
5187
|
let restarting = false;
|
|
3702
5188
|
let settingsWrites = Promise.resolve();
|
|
3703
|
-
await stat4(
|
|
5189
|
+
await stat4(join6(PUBLIC_DIR, "app.js")).catch(() => {
|
|
3704
5190
|
console.warn(
|
|
3705
5191
|
PACKAGED ? "[daily-focus] public/app.js is missing from the package: reinstall it" : "[daily-focus] public/app.js is missing: run `npm run build` (npm start does so on its own)"
|
|
3706
5192
|
);
|
|
3707
5193
|
});
|
|
3708
5194
|
const subscribers = /* @__PURE__ */ new Set();
|
|
3709
5195
|
let assetVersion = await computeAssetVersion(PUBLIC_DIR);
|
|
3710
|
-
|
|
5196
|
+
let autoAsk = null;
|
|
5197
|
+
const board = new Board(
|
|
5198
|
+
config,
|
|
5199
|
+
() => {
|
|
5200
|
+
void broadcast();
|
|
5201
|
+
autoAsk?.poke();
|
|
5202
|
+
},
|
|
5203
|
+
options.integrations?.github
|
|
5204
|
+
);
|
|
3711
5205
|
const calendar = new CalendarBoard(config, () => void broadcast());
|
|
3712
5206
|
const tickets = new TicketBoard(config, () => void broadcast(), options.integrations?.jira);
|
|
3713
5207
|
const assistant = new Assistant(config, {
|
|
3714
5208
|
onChange: () => {
|
|
3715
5209
|
void broadcast();
|
|
3716
5210
|
restartWhenIdle();
|
|
5211
|
+
autoAsk?.poke();
|
|
3717
5212
|
}
|
|
3718
5213
|
});
|
|
5214
|
+
autoAsk = new AutoAsk({
|
|
5215
|
+
config,
|
|
5216
|
+
assistant,
|
|
5217
|
+
rows: async () => board.view(await store.readActions(), /* @__PURE__ */ new Date()).rows,
|
|
5218
|
+
context: async (id) => contextFor(id, await buildState()),
|
|
5219
|
+
// Nothing new starts while saved settings wait on the restart that applies them.
|
|
5220
|
+
canStart: () => restart !== "waiting"
|
|
5221
|
+
});
|
|
5222
|
+
const ahead = autoAsk;
|
|
3719
5223
|
const agent = new AgentRunner(config, {
|
|
3720
5224
|
onChange: () => {
|
|
3721
5225
|
void broadcast();
|
|
3722
5226
|
restartWhenIdle();
|
|
3723
5227
|
}
|
|
3724
5228
|
});
|
|
5229
|
+
const onboarding = new Onboarding(config, {
|
|
5230
|
+
onChange: () => {
|
|
5231
|
+
void broadcast();
|
|
5232
|
+
restartWhenIdle();
|
|
5233
|
+
},
|
|
5234
|
+
// A path already set for either CLI is looked at before PATH.
|
|
5235
|
+
detectClis: () => {
|
|
5236
|
+
const configured = {};
|
|
5237
|
+
if (config.agent.cli && config.agent.binPath !== config.agent.cli) configured[config.agent.cli] = config.agent.binPath;
|
|
5238
|
+
if (config.assistant.agent && config.assistant.binPath !== config.assistant.agent) configured[config.assistant.agent] = config.assistant.binPath;
|
|
5239
|
+
return detectClis(configured);
|
|
5240
|
+
},
|
|
5241
|
+
probeCli: (path) => probeCli(path, null, false),
|
|
5242
|
+
readSources,
|
|
5243
|
+
setup: () => setupState(config),
|
|
5244
|
+
identity: () => detectIdentity(config),
|
|
5245
|
+
boards: async () => {
|
|
5246
|
+
const actions = await store.readActions();
|
|
5247
|
+
const now = /* @__PURE__ */ new Date();
|
|
5248
|
+
const health = (view) => ({
|
|
5249
|
+
enabled: view.enabled,
|
|
5250
|
+
fetchedAt: view.fetchedAt,
|
|
5251
|
+
reason: view.reason?.message ?? null
|
|
5252
|
+
});
|
|
5253
|
+
return { github: health(board.view(actions, now)), jira: health(tickets.view(actions, now)) };
|
|
5254
|
+
},
|
|
5255
|
+
listCalendars: async () => {
|
|
5256
|
+
const facts = await runHelper(config.calendar.appPath, []);
|
|
5257
|
+
const today = /* @__PURE__ */ new Map();
|
|
5258
|
+
for (const event of facts.events) {
|
|
5259
|
+
if (typeof event.calendarId === "string") today.set(event.calendarId, (today.get(event.calendarId) ?? 0) + 1);
|
|
5260
|
+
}
|
|
5261
|
+
return facts.calendars.map(({ id, title, source }) => ({ title, source, eventsToday: today.get(id) ?? 0 }));
|
|
5262
|
+
},
|
|
5263
|
+
checkSettings: (values) => {
|
|
5264
|
+
const applied = applySettingsChange(readSources(), values);
|
|
5265
|
+
return "error" in applied ? applied.error : null;
|
|
5266
|
+
},
|
|
5267
|
+
saveSettings: (values) => saveSettingsValues(values)
|
|
5268
|
+
});
|
|
5269
|
+
let codexWarnings = [];
|
|
5270
|
+
const codexBins = /* @__PURE__ */ new Set();
|
|
5271
|
+
if (config.agent.cli === "codex") codexBins.add(config.agent.binPath);
|
|
5272
|
+
if (config.assistant.agent === "codex") codexBins.add(config.assistant.binPath);
|
|
5273
|
+
void Promise.all([...codexBins].map((bin) => codexVersionWarning(bin))).then((found) => {
|
|
5274
|
+
codexWarnings = [...new Set(found.filter((warning) => warning !== null))];
|
|
5275
|
+
for (const warning of codexWarnings) console.warn(`[daily-focus] ${warning}`);
|
|
5276
|
+
if (codexWarnings.length > 0) void broadcast();
|
|
5277
|
+
});
|
|
5278
|
+
async function startRequestedBrief() {
|
|
5279
|
+
if (!agent.enabled || !await firstBriefPending(config) || !await onboarding.takeBriefRequest()) return;
|
|
5280
|
+
await agent.run("hand").catch((err) => {
|
|
5281
|
+
console.error(`[daily-focus] could not start the first brief: ${err.message}`);
|
|
5282
|
+
});
|
|
5283
|
+
}
|
|
5284
|
+
function saveSettingsValues(values) {
|
|
5285
|
+
const saved = settingsWrites.then(async () => {
|
|
5286
|
+
const sources = readSources();
|
|
5287
|
+
const applied = applySettingsChange(sources, values);
|
|
5288
|
+
if ("error" in applied) return applied.error;
|
|
5289
|
+
const changed = JSON.stringify(Object.entries(applied.settings).sort()) !== JSON.stringify(Object.entries(sources.settings).sort());
|
|
5290
|
+
if (changed) {
|
|
5291
|
+
await saveSettings(sources.settingsPath, applied.settings);
|
|
5292
|
+
requestRestart();
|
|
5293
|
+
}
|
|
5294
|
+
return null;
|
|
5295
|
+
});
|
|
5296
|
+
settingsWrites = saved.catch(() => {
|
|
5297
|
+
});
|
|
5298
|
+
return saved;
|
|
5299
|
+
}
|
|
3725
5300
|
function requestRestart() {
|
|
3726
5301
|
restart = options.onRestart ? "waiting" : "manual";
|
|
3727
5302
|
restartWhenIdle();
|
|
3728
5303
|
}
|
|
3729
5304
|
function restartWhenIdle() {
|
|
3730
|
-
if (restart !== "waiting" || restarting || agent.busy || assistant.busy) return;
|
|
5305
|
+
if (restart !== "waiting" || restarting || agent.busy || assistant.busy || onboarding.busy) return;
|
|
3731
5306
|
restarting = true;
|
|
3732
5307
|
setImmediate(() => options.onRestart?.());
|
|
3733
5308
|
}
|
|
@@ -3739,23 +5314,30 @@ async function startServer(env, options = {}) {
|
|
|
3739
5314
|
// A link that can't be read is no reason to withhold the page.
|
|
3740
5315
|
storeLinkWarning(config).catch(() => null)
|
|
3741
5316
|
]);
|
|
3742
|
-
const
|
|
3743
|
-
const
|
|
3744
|
-
|
|
3745
|
-
});
|
|
5317
|
+
const setup = await setupState(config);
|
|
5318
|
+
const pulls = board.view(actions, now);
|
|
5319
|
+
const onboardingState = await onboarding.view(setup);
|
|
3746
5320
|
return {
|
|
3747
5321
|
...state,
|
|
3748
|
-
board:
|
|
5322
|
+
board: pulls,
|
|
3749
5323
|
tickets: tickets.view(actions, now),
|
|
3750
|
-
assistant: assistant.view(),
|
|
5324
|
+
assistant: { ...assistant.view(), auto: ahead.view(pulls.rows) },
|
|
3751
5325
|
agentRun: agent.view(now),
|
|
3752
|
-
setupWarnings: [...configWarnings, ...linkWarning ? [linkWarning] : []],
|
|
5326
|
+
setupWarnings: [...configWarnings, ...codexWarnings, ...linkWarning ? [linkWarning] : []],
|
|
3753
5327
|
restart,
|
|
3754
5328
|
setup,
|
|
5329
|
+
onboarding: onboardingState,
|
|
3755
5330
|
assetVersion,
|
|
3756
5331
|
storeId
|
|
3757
5332
|
};
|
|
3758
5333
|
}
|
|
5334
|
+
function contextFor(id, current) {
|
|
5335
|
+
return {
|
|
5336
|
+
item: current.items.find((item) => item.id === id),
|
|
5337
|
+
pull: current.board.rows.find((row) => row.id === id),
|
|
5338
|
+
ticket: current.tickets.rows.find((row) => row.id === id) ?? current.tickets.inProgress.find((row) => row.id === id)
|
|
5339
|
+
};
|
|
5340
|
+
}
|
|
3759
5341
|
async function broadcast(prebuilt) {
|
|
3760
5342
|
if (subscribers.size === 0) return;
|
|
3761
5343
|
let payload;
|
|
@@ -3777,13 +5359,11 @@ data: ${payload}
|
|
|
3777
5359
|
() => {
|
|
3778
5360
|
void store.archiveCurrentBrief().then(() => broadcast());
|
|
3779
5361
|
},
|
|
3780
|
-
// The
|
|
3781
|
-
//
|
|
3782
|
-
//
|
|
5362
|
+
// The two boards rewrite prs.json and tickets.json every fetch; that's this
|
|
5363
|
+
// process talking to itself, and both already broadcast when something
|
|
5364
|
+
// actually changed.
|
|
3783
5365
|
{
|
|
3784
5366
|
ignore: [
|
|
3785
|
-
basename(config.sessionFile),
|
|
3786
|
-
basename(tempPathFor(config.sessionFile)),
|
|
3787
5367
|
basename(config.pullsFile),
|
|
3788
5368
|
basename(tempPathFor(config.pullsFile)),
|
|
3789
5369
|
basename(config.ticketsFile),
|
|
@@ -3792,7 +5372,10 @@ data: ${payload}
|
|
|
3792
5372
|
basename(tempPathFor(config.timeZoneFile)),
|
|
3793
5373
|
// Appended by this process, which broadcasts on its own.
|
|
3794
5374
|
basename(config.assistantLogFile),
|
|
3795
|
-
basename(config.agentLogFile)
|
|
5375
|
+
basename(config.agentLogFile),
|
|
5376
|
+
// Rewritten by the setup screen, which broadcasts on its own.
|
|
5377
|
+
basename(config.onboardingFile),
|
|
5378
|
+
basename(tempPathFor(config.onboardingFile))
|
|
3796
5379
|
]
|
|
3797
5380
|
}
|
|
3798
5381
|
);
|
|
@@ -3803,34 +5386,10 @@ data: ${payload}
|
|
|
3803
5386
|
void broadcast();
|
|
3804
5387
|
});
|
|
3805
5388
|
});
|
|
3806
|
-
let sessionTick = null;
|
|
3807
|
-
function scheduleSessionTick(endsAt) {
|
|
3808
|
-
if (sessionTick) clearTimeout(sessionTick);
|
|
3809
|
-
sessionTick = null;
|
|
3810
|
-
if (!endsAt) return;
|
|
3811
|
-
const delay = new Date(endsAt).getTime() - Date.now();
|
|
3812
|
-
if (delay <= 0 || delay > 36e5) return;
|
|
3813
|
-
sessionTick = setTimeout(() => void broadcast(), delay);
|
|
3814
|
-
sessionTick.unref();
|
|
3815
|
-
}
|
|
3816
|
-
async function checkPresence() {
|
|
3817
|
-
let closed;
|
|
3818
|
-
try {
|
|
3819
|
-
closed = await reconcileSession(config, readIdleSeconds);
|
|
3820
|
-
} catch (err) {
|
|
3821
|
-
console.error(`[daily-focus] presence check failed: ${err.message}`);
|
|
3822
|
-
return;
|
|
3823
|
-
}
|
|
3824
|
-
if (!closed) return;
|
|
3825
|
-
console.log(
|
|
3826
|
-
`[daily-focus] stopped "${closed.title}" at ${closed.endedAt} after ${closed.actualMinutes} min \u2014 machine untouched since then`
|
|
3827
|
-
);
|
|
3828
|
-
scheduleSessionTick(null);
|
|
3829
|
-
await broadcast();
|
|
3830
|
-
}
|
|
3831
|
-
await checkPresence();
|
|
3832
5389
|
await assistant.start();
|
|
3833
5390
|
await agent.start();
|
|
5391
|
+
await onboarding.start(await firstBriefPending(config));
|
|
5392
|
+
await startRequestedBrief();
|
|
3834
5393
|
let ticks = Promise.resolve();
|
|
3835
5394
|
function clockTick() {
|
|
3836
5395
|
ticks = ticks.then(tickOnce).catch((err) => {
|
|
@@ -3853,10 +5412,10 @@ data: ${payload}
|
|
|
3853
5412
|
void clockTick();
|
|
3854
5413
|
}, AGENT_CLOCK_MS);
|
|
3855
5414
|
agentClock.unref();
|
|
3856
|
-
scheduleSessionTick((await buildState()).session.active?.endsAt ?? null);
|
|
3857
5415
|
void board.start().catch((err) => {
|
|
3858
5416
|
console.error(`[daily-focus] pull request board failed to start: ${err.message}`);
|
|
3859
5417
|
});
|
|
5418
|
+
ahead.poke();
|
|
3860
5419
|
void calendar.start().catch((err) => {
|
|
3861
5420
|
console.error(`[daily-focus] live agenda failed to start: ${err.message}`);
|
|
3862
5421
|
});
|
|
@@ -3867,10 +5426,6 @@ data: ${payload}
|
|
|
3867
5426
|
void broadcast();
|
|
3868
5427
|
}, 6e4);
|
|
3869
5428
|
heartbeat.unref();
|
|
3870
|
-
const presencePoll = setInterval(() => {
|
|
3871
|
-
void checkPresence();
|
|
3872
|
-
}, PRESENCE_POLL_MS);
|
|
3873
|
-
presencePoll.unref();
|
|
3874
5429
|
const server = createServer((req, res) => {
|
|
3875
5430
|
void handle(req, res).catch((err) => {
|
|
3876
5431
|
console.error(`[daily-focus] ${req.method} ${req.url} failed:`, err);
|
|
@@ -4017,12 +5572,7 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4017
5572
|
sendJSON(res, 409, { error: "the assistant is off; set DAILY_FOCUS_ASSISTANT to claude or codex" });
|
|
4018
5573
|
return;
|
|
4019
5574
|
}
|
|
4020
|
-
const
|
|
4021
|
-
const context = {
|
|
4022
|
-
item: current.items.find((item) => item.id === id),
|
|
4023
|
-
pull: current.board.rows.find((row) => row.id === id),
|
|
4024
|
-
ticket: current.tickets.rows.find((row) => row.id === id) ?? current.tickets.inProgress.find((row) => row.id === id)
|
|
4025
|
-
};
|
|
5575
|
+
const context = contextFor(id, await buildState());
|
|
4026
5576
|
if (!context.item && !context.pull && !context.ticket) {
|
|
4027
5577
|
sendJSON(res, 404, { error: `no item with id ${id}` });
|
|
4028
5578
|
return;
|
|
@@ -4058,8 +5608,8 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4058
5608
|
sendJSON(res, 400, { error: `invalid JSON body: ${err.message}` });
|
|
4059
5609
|
return;
|
|
4060
5610
|
}
|
|
4061
|
-
const { run:
|
|
4062
|
-
if (typeof
|
|
5611
|
+
const { run: run5, text } = typeof body === "object" && body !== null ? body : {};
|
|
5612
|
+
if (typeof run5 !== "string" || run5 === "") {
|
|
4063
5613
|
sendJSON(res, 400, { error: "run must be a run id" });
|
|
4064
5614
|
return;
|
|
4065
5615
|
}
|
|
@@ -4068,7 +5618,7 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4068
5618
|
return;
|
|
4069
5619
|
}
|
|
4070
5620
|
try {
|
|
4071
|
-
await agent.ask(
|
|
5621
|
+
await agent.ask(run5, text);
|
|
4072
5622
|
} catch (err) {
|
|
4073
5623
|
sendJSON(res, 409, { error: err.message });
|
|
4074
5624
|
return;
|
|
@@ -4138,53 +5688,40 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4138
5688
|
void broadcast(state);
|
|
4139
5689
|
return;
|
|
4140
5690
|
}
|
|
4141
|
-
if (path
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
}
|
|
4149
|
-
|
|
4150
|
-
|
|
5691
|
+
if (path.startsWith("/api/onboarding/") && req.method === "POST") {
|
|
5692
|
+
const body = await readJsonObject(req, res);
|
|
5693
|
+
if (!body) return;
|
|
5694
|
+
const step = typeof body.step === "string" ? body.step : "";
|
|
5695
|
+
let refused2 = null;
|
|
5696
|
+
if (path === "/api/onboarding/answer") {
|
|
5697
|
+
refused2 = await onboarding.answer(step, body.answer);
|
|
5698
|
+
} else if (path === "/api/onboarding/revise") {
|
|
5699
|
+
refused2 = typeof body.text === "string" && body.text.trim() !== "" ? await onboarding.revise(step, body.text, body.draft) : '"text" is required: what to change';
|
|
5700
|
+
} else if (path === "/api/onboarding/retry") {
|
|
5701
|
+
refused2 = await onboarding.retry(step);
|
|
5702
|
+
} else if (path === "/api/onboarding/detect") {
|
|
5703
|
+
await onboarding.detect();
|
|
5704
|
+
} else if (path === "/api/onboarding/finish") {
|
|
5705
|
+
const finished = await onboarding.finish();
|
|
5706
|
+
if ("error" in finished) refused2 = finished.error;
|
|
5707
|
+
else if (!finished.restart) await startRequestedBrief();
|
|
5708
|
+
} else {
|
|
5709
|
+
sendJSON(res, 404, { error: `no such setup action: ${path}` });
|
|
4151
5710
|
return;
|
|
4152
5711
|
}
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
await stopSession(config);
|
|
4156
|
-
} else if (action === "start") {
|
|
4157
|
-
if (typeof id !== "string" || id === "") {
|
|
4158
|
-
sendJSON(res, 400, { error: '"id" is required to start a session' });
|
|
4159
|
-
return;
|
|
4160
|
-
}
|
|
4161
|
-
const { brief } = await store.readBrief();
|
|
4162
|
-
const item = brief?.items.find((candidate) => candidate.id === id);
|
|
4163
|
-
if (!item) {
|
|
4164
|
-
sendJSON(res, 404, { error: `no item with id ${id}` });
|
|
4165
|
-
return;
|
|
4166
|
-
}
|
|
4167
|
-
const requested = typeof minutes === "number" && Number.isFinite(minutes) ? Math.round(minutes) : config.sessionMinutes;
|
|
4168
|
-
if (requested < 1 || requested > 120) {
|
|
4169
|
-
sendJSON(res, 400, { error: '"minutes" must be between 1 and 120' });
|
|
4170
|
-
return;
|
|
4171
|
-
}
|
|
4172
|
-
await startSession(config, {
|
|
4173
|
-
id,
|
|
4174
|
-
title: item.title,
|
|
4175
|
-
minutes: requested,
|
|
4176
|
-
advancesObjective: item.advancesObjective === true
|
|
4177
|
-
});
|
|
4178
|
-
} else {
|
|
4179
|
-
sendJSON(res, 400, { error: '"action" must be "start" or "stop"' });
|
|
5712
|
+
if (refused2) {
|
|
5713
|
+
sendJSON(res, 409, { error: refused2 });
|
|
4180
5714
|
return;
|
|
4181
5715
|
}
|
|
4182
5716
|
const state = await buildState();
|
|
4183
5717
|
sendState(res, state);
|
|
4184
|
-
scheduleSessionTick(state.session.active?.endsAt ?? null);
|
|
4185
5718
|
void broadcast(state);
|
|
4186
5719
|
return;
|
|
4187
5720
|
}
|
|
5721
|
+
if (path === "/api/onboarding/sources" && req.method === "GET") {
|
|
5722
|
+
sendJSON(res, 200, await onboarding.previewSources());
|
|
5723
|
+
return;
|
|
5724
|
+
}
|
|
4188
5725
|
if (path === "/api/settings" && req.method === "GET") {
|
|
4189
5726
|
sendJSON(res, 200, { ...describeSettings(readSources()), restart });
|
|
4190
5727
|
return;
|
|
@@ -4197,20 +5734,7 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4197
5734
|
sendJSON(res, 400, { error: '"values" must be an object of settings' });
|
|
4198
5735
|
return;
|
|
4199
5736
|
}
|
|
4200
|
-
const
|
|
4201
|
-
const sources = readSources();
|
|
4202
|
-
const applied = applySettingsChange(sources, values);
|
|
4203
|
-
if ("error" in applied) return applied.error;
|
|
4204
|
-
const changed = JSON.stringify(Object.entries(applied.settings).sort()) !== JSON.stringify(Object.entries(sources.settings).sort());
|
|
4205
|
-
if (changed) {
|
|
4206
|
-
await saveSettings(sources.settingsPath, applied.settings);
|
|
4207
|
-
requestRestart();
|
|
4208
|
-
}
|
|
4209
|
-
return null;
|
|
4210
|
-
});
|
|
4211
|
-
settingsWrites = saved.catch(() => {
|
|
4212
|
-
});
|
|
4213
|
-
const refused2 = await saved;
|
|
5737
|
+
const refused2 = await saveSettingsValues(values);
|
|
4214
5738
|
if (refused2) {
|
|
4215
5739
|
sendJSON(res, 400, { error: refused2 });
|
|
4216
5740
|
return;
|
|
@@ -4250,8 +5774,8 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4250
5774
|
if (path === "/api/focus/objective" && req.method === "POST") {
|
|
4251
5775
|
const body = await readJsonObject(req, res);
|
|
4252
5776
|
if (!body) return;
|
|
4253
|
-
const { objective, blocker } = body;
|
|
4254
|
-
for (const [name, value] of [["objective",
|
|
5777
|
+
const { objective: objective2, blocker } = body;
|
|
5778
|
+
for (const [name, value] of [["objective", objective2], ["blocker", blocker]]) {
|
|
4255
5779
|
if (value !== void 0 && value !== null && typeof value !== "string") {
|
|
4256
5780
|
sendJSON(res, 400, { error: `"${name}" must be text, or null to clear it` });
|
|
4257
5781
|
return;
|
|
@@ -4260,7 +5784,7 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4260
5784
|
const current = await readEditable(config.focusFile);
|
|
4261
5785
|
const next = setFocusFields(
|
|
4262
5786
|
current.text,
|
|
4263
|
-
{ objective:
|
|
5787
|
+
{ objective: objective2 ?? null, blocker: blocker ?? null },
|
|
4264
5788
|
FOCUS_TEMPLATE
|
|
4265
5789
|
);
|
|
4266
5790
|
const result = await saveEditable(config.focusFile, next, current.version);
|
|
@@ -4277,10 +5801,10 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4277
5801
|
try {
|
|
4278
5802
|
const facts = await runHelper(config.calendar.appPath, []);
|
|
4279
5803
|
const seen = /* @__PURE__ */ new Set();
|
|
4280
|
-
const
|
|
5804
|
+
const calendars2 = facts.calendars.filter((entry) => !seen.has(`${entry.title}
|
|
4281
5805
|
${entry.source}`) && seen.add(`${entry.title}
|
|
4282
5806
|
${entry.source}`)).map(({ title, source }) => ({ title, source })).sort((a, b) => a.title.localeCompare(b.title));
|
|
4283
|
-
sendJSON(res, 200, { calendars });
|
|
5807
|
+
sendJSON(res, 200, { calendars: calendars2 });
|
|
4284
5808
|
} catch (err) {
|
|
4285
5809
|
sendJSON(res, 409, { error: err.message });
|
|
4286
5810
|
}
|
|
@@ -4312,14 +5836,14 @@ ${entry.source}`)).map(({ title, source }) => ({ title, source })).sort((a, b) =
|
|
|
4312
5836
|
}
|
|
4313
5837
|
async function stopEverything() {
|
|
4314
5838
|
clearInterval(heartbeat);
|
|
4315
|
-
clearInterval(presencePoll);
|
|
4316
5839
|
clearInterval(agentClock);
|
|
4317
|
-
|
|
5840
|
+
ahead.close();
|
|
4318
5841
|
board.stop();
|
|
4319
5842
|
calendar.stop();
|
|
4320
5843
|
tickets.stop();
|
|
4321
5844
|
await assistant.close();
|
|
4322
5845
|
await agent.close();
|
|
5846
|
+
await onboarding.close();
|
|
4323
5847
|
stopWatching();
|
|
4324
5848
|
stopWatchingAssets();
|
|
4325
5849
|
for (const res of subscribers) res.end();
|