daily-focus 1.1.1-dev.2 → 1.2.0-dev.10
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 +34 -20
- package/SETUP.md +103 -21
- package/dist/{audit-W3O6CLOF.js → audit-7ZCLKF46.js} +10 -4
- package/dist/{chunk-Q5OKXJXT.js → chunk-64XB7UDN.js} +1 -1
- package/dist/{chunk-OPASVYLR.js → chunk-J7ZYVUG6.js} +5 -6
- package/dist/chunk-ODJGQCM3.js +1003 -0
- package/dist/{chunk-UM2EPNWR.js → chunk-RZNQJ6UG.js} +123 -282
- package/dist/chunk-Z4SFMMRQ.js +107 -0
- package/dist/cli.js +9 -8
- package/dist/{demo-OQLT3QWP.js → demo-BYLLMKSQ.js} +135 -32
- package/dist/{init-DEVCAQZZ.js → init-EMZDG2I7.js} +1 -9
- package/dist/{seed-SMKNFYTC.js → seed-JU4LQ33D.js} +3 -3
- package/dist/{server-5O3LQVEY.js → server-WQYUOKB3.js} +459 -385
- package/dist/{service-6ZFOHPWN.js → service-PUJ3YDH5.js} +1 -1
- package/package.json +2 -2
- package/prompts/README.md +2 -3
- package/prompts/assistant.md +14 -3
- package/prompts/morning-brief-personal.md +1 -25
- package/prompts/morning-brief-work.md +8 -33
- package/public/app.js +506 -431
- package/public/style.css +198 -181
- package/dist/chunk-GFCWPYEI.js +0 -456
|
@@ -14,7 +14,15 @@ import {
|
|
|
14
14
|
startOfLocalDay,
|
|
15
15
|
workingDaysSince,
|
|
16
16
|
workingMsBetween
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-J7ZYVUG6.js";
|
|
18
|
+
|
|
19
|
+
// src/ids.ts
|
|
20
|
+
function canonicalId(id) {
|
|
21
|
+
return id.trim().toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
function fingerprintId(id) {
|
|
24
|
+
return id.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
25
|
+
}
|
|
18
26
|
|
|
19
27
|
// src/fs.ts
|
|
20
28
|
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
@@ -30,109 +38,6 @@ function tempPathFor(path) {
|
|
|
30
38
|
return `${path}.tmp`;
|
|
31
39
|
}
|
|
32
40
|
|
|
33
|
-
// src/ids.ts
|
|
34
|
-
function canonicalId(id) {
|
|
35
|
-
return id.trim().toLowerCase();
|
|
36
|
-
}
|
|
37
|
-
function fingerprintId(id) {
|
|
38
|
-
return id.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// src/archive.ts
|
|
42
|
-
import { readFile, readdir } from "node:fs/promises";
|
|
43
|
-
import { resolve } from "node:path";
|
|
44
|
-
var ARCHIVE_FILE = /^items-(\d{4}-\d{2}-\d{2})\.json$/;
|
|
45
|
-
var MAX_ARCHIVE_FILES = 130;
|
|
46
|
-
var RECENT_WINDOW_DAYS = 14;
|
|
47
|
-
function archivePath(config, dateKey) {
|
|
48
|
-
return resolve(config.archiveDir, `items-${dateKey}.json`);
|
|
49
|
-
}
|
|
50
|
-
function briefDateKey(brief) {
|
|
51
|
-
if (brief.date && /^\d{4}-\d{2}-\d{2}$/.test(brief.date)) return brief.date;
|
|
52
|
-
const generated = parseISO(brief.generatedAt);
|
|
53
|
-
return localDateKey(generated ?? /* @__PURE__ */ new Date());
|
|
54
|
-
}
|
|
55
|
-
async function archiveBrief(config, brief) {
|
|
56
|
-
const dateKey = briefDateKey(brief);
|
|
57
|
-
const target = archivePath(config, dateKey);
|
|
58
|
-
try {
|
|
59
|
-
const existing = JSON.parse(await readFile(target, "utf8"));
|
|
60
|
-
const existingAt = parseISO(existing.generatedAt)?.getTime() ?? 0;
|
|
61
|
-
const incomingAt = parseISO(brief.generatedAt)?.getTime() ?? 0;
|
|
62
|
-
if (existingAt >= incomingAt) return false;
|
|
63
|
-
} catch {
|
|
64
|
-
}
|
|
65
|
-
await writeJsonAtomic(target, brief);
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
async function listArchivedDates(config) {
|
|
69
|
-
let names;
|
|
70
|
-
try {
|
|
71
|
-
names = await readdir(config.archiveDir);
|
|
72
|
-
} catch {
|
|
73
|
-
return [];
|
|
74
|
-
}
|
|
75
|
-
return names.map((name) => ARCHIVE_FILE.exec(name)?.[1]).filter((date) => date !== void 0).sort();
|
|
76
|
-
}
|
|
77
|
-
async function readArchiveIndex(config) {
|
|
78
|
-
const index = /* @__PURE__ */ new Map();
|
|
79
|
-
const dated = (await listArchivedDates(config)).reverse().slice(0, MAX_ARCHIVE_FILES);
|
|
80
|
-
for (const date of dated) {
|
|
81
|
-
let brief;
|
|
82
|
-
try {
|
|
83
|
-
brief = JSON.parse(await readFile(archivePath(config, date), "utf8"));
|
|
84
|
-
} catch {
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
for (const item of brief.items ?? []) {
|
|
88
|
-
if (!item?.id) continue;
|
|
89
|
-
const existing = index.get(item.id);
|
|
90
|
-
if (!existing) {
|
|
91
|
-
index.set(item.id, {
|
|
92
|
-
title: item.title ?? item.id,
|
|
93
|
-
advancesObjective: item.advancesObjective === true,
|
|
94
|
-
firstSeen: item.firstSeen
|
|
95
|
-
});
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (item.firstSeen && (!existing.firstSeen || item.firstSeen < existing.firstSeen)) {
|
|
99
|
-
existing.firstSeen = item.firstSeen;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return index;
|
|
104
|
-
}
|
|
105
|
-
function computeObjectiveProgress(index, actions, now, counts = isWorkingDay) {
|
|
106
|
-
const lastAction = /* @__PURE__ */ new Map();
|
|
107
|
-
for (const action of actions) {
|
|
108
|
-
if (action.action === "note") continue;
|
|
109
|
-
lastAction.set(canonicalId(action.id), action);
|
|
110
|
-
}
|
|
111
|
-
const byCanonical = /* @__PURE__ */ new Map();
|
|
112
|
-
for (const [id, entry] of index) byCanonical.set(canonicalId(id), entry);
|
|
113
|
-
let lastAt = null;
|
|
114
|
-
let lastTitle = null;
|
|
115
|
-
let recentCount = 0;
|
|
116
|
-
for (const [id, action] of lastAction) {
|
|
117
|
-
if (action.action !== "done") continue;
|
|
118
|
-
if (!byCanonical.get(id)?.advancesObjective) continue;
|
|
119
|
-
const at = parseISO(action.at);
|
|
120
|
-
if (!at) continue;
|
|
121
|
-
if (workingDaysSince(at, now, counts) <= RECENT_WINDOW_DAYS) recentCount++;
|
|
122
|
-
if (!lastAt || at.getTime() > lastAt.getTime()) {
|
|
123
|
-
lastAt = at;
|
|
124
|
-
lastTitle = byCanonical.get(id)?.title ?? null;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return {
|
|
128
|
-
workingDaysSince: lastAt ? workingDaysSince(lastAt, now, counts) : null,
|
|
129
|
-
lastAt: lastAt ? lastAt.toISOString() : null,
|
|
130
|
-
lastTitle,
|
|
131
|
-
recentCount,
|
|
132
|
-
windowDays: RECENT_WINDOW_DAYS
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
41
|
// src/validate.ts
|
|
137
42
|
var SOURCES = /* @__PURE__ */ new Set([
|
|
138
43
|
"github",
|
|
@@ -300,6 +205,10 @@ function parseBrief(text) {
|
|
|
300
205
|
if (raw.dayEnd !== void 0 && !dayEnd) warnings.push('items.json has an unreadable "dayEnd"; using the default');
|
|
301
206
|
return { brief, error: null, warnings };
|
|
302
207
|
}
|
|
208
|
+
var ACTION_TYPES = ["done", "snooze", "dismiss", "reopen", "note", "nudge"];
|
|
209
|
+
function setsStatus(action) {
|
|
210
|
+
return action.action !== "note" && action.action !== "nudge";
|
|
211
|
+
}
|
|
303
212
|
function parseActionLine(line) {
|
|
304
213
|
const trimmed = line.trim();
|
|
305
214
|
if (trimmed === "") return null;
|
|
@@ -313,7 +222,7 @@ function parseActionLine(line) {
|
|
|
313
222
|
const id = optionalString(raw.id);
|
|
314
223
|
const action = optionalString(raw.action);
|
|
315
224
|
if (!id || !action) return null;
|
|
316
|
-
if (!
|
|
225
|
+
if (!ACTION_TYPES.includes(action)) return null;
|
|
317
226
|
const result = {
|
|
318
227
|
id,
|
|
319
228
|
action,
|
|
@@ -326,6 +235,101 @@ function parseActionLine(line) {
|
|
|
326
235
|
return result;
|
|
327
236
|
}
|
|
328
237
|
|
|
238
|
+
// src/archive.ts
|
|
239
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
240
|
+
import { resolve } from "node:path";
|
|
241
|
+
var ARCHIVE_FILE = /^items-(\d{4}-\d{2}-\d{2})\.json$/;
|
|
242
|
+
var MAX_ARCHIVE_FILES = 130;
|
|
243
|
+
var RECENT_WINDOW_DAYS = 14;
|
|
244
|
+
function archivePath(config, dateKey) {
|
|
245
|
+
return resolve(config.archiveDir, `items-${dateKey}.json`);
|
|
246
|
+
}
|
|
247
|
+
function briefDateKey(brief) {
|
|
248
|
+
if (brief.date && /^\d{4}-\d{2}-\d{2}$/.test(brief.date)) return brief.date;
|
|
249
|
+
const generated = parseISO(brief.generatedAt);
|
|
250
|
+
return localDateKey(generated ?? /* @__PURE__ */ new Date());
|
|
251
|
+
}
|
|
252
|
+
async function archiveBrief(config, brief) {
|
|
253
|
+
const dateKey = briefDateKey(brief);
|
|
254
|
+
const target = archivePath(config, dateKey);
|
|
255
|
+
try {
|
|
256
|
+
const existing = JSON.parse(await readFile(target, "utf8"));
|
|
257
|
+
const existingAt = parseISO(existing.generatedAt)?.getTime() ?? 0;
|
|
258
|
+
const incomingAt = parseISO(brief.generatedAt)?.getTime() ?? 0;
|
|
259
|
+
if (existingAt >= incomingAt) return false;
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
await writeJsonAtomic(target, brief);
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
async function listArchivedDates(config) {
|
|
266
|
+
let names;
|
|
267
|
+
try {
|
|
268
|
+
names = await readdir(config.archiveDir);
|
|
269
|
+
} catch {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
return names.map((name) => ARCHIVE_FILE.exec(name)?.[1]).filter((date) => date !== void 0).sort();
|
|
273
|
+
}
|
|
274
|
+
async function readArchiveIndex(config) {
|
|
275
|
+
const index = /* @__PURE__ */ new Map();
|
|
276
|
+
const dated = (await listArchivedDates(config)).reverse().slice(0, MAX_ARCHIVE_FILES);
|
|
277
|
+
for (const date of dated) {
|
|
278
|
+
let brief;
|
|
279
|
+
try {
|
|
280
|
+
brief = JSON.parse(await readFile(archivePath(config, date), "utf8"));
|
|
281
|
+
} catch {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
for (const item of brief.items ?? []) {
|
|
285
|
+
if (!item?.id) continue;
|
|
286
|
+
const existing = index.get(item.id);
|
|
287
|
+
if (!existing) {
|
|
288
|
+
index.set(item.id, {
|
|
289
|
+
title: item.title ?? item.id,
|
|
290
|
+
advancesObjective: item.advancesObjective === true,
|
|
291
|
+
firstSeen: item.firstSeen
|
|
292
|
+
});
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (item.firstSeen && (!existing.firstSeen || item.firstSeen < existing.firstSeen)) {
|
|
296
|
+
existing.firstSeen = item.firstSeen;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return index;
|
|
301
|
+
}
|
|
302
|
+
function computeObjectiveProgress(index, actions, now, counts = isWorkingDay) {
|
|
303
|
+
const lastAction = /* @__PURE__ */ new Map();
|
|
304
|
+
for (const action of actions) {
|
|
305
|
+
if (!setsStatus(action)) continue;
|
|
306
|
+
lastAction.set(canonicalId(action.id), action);
|
|
307
|
+
}
|
|
308
|
+
const byCanonical = /* @__PURE__ */ new Map();
|
|
309
|
+
for (const [id, entry] of index) byCanonical.set(canonicalId(id), entry);
|
|
310
|
+
let lastAt = null;
|
|
311
|
+
let lastTitle = null;
|
|
312
|
+
let recentCount = 0;
|
|
313
|
+
for (const [id, action] of lastAction) {
|
|
314
|
+
if (action.action !== "done") continue;
|
|
315
|
+
if (!byCanonical.get(id)?.advancesObjective) continue;
|
|
316
|
+
const at = parseISO(action.at);
|
|
317
|
+
if (!at) continue;
|
|
318
|
+
if (workingDaysSince(at, now, counts) <= RECENT_WINDOW_DAYS) recentCount++;
|
|
319
|
+
if (!lastAt || at.getTime() > lastAt.getTime()) {
|
|
320
|
+
lastAt = at;
|
|
321
|
+
lastTitle = byCanonical.get(id)?.title ?? null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
workingDaysSince: lastAt ? workingDaysSince(lastAt, now, counts) : null,
|
|
326
|
+
lastAt: lastAt ? lastAt.toISOString() : null,
|
|
327
|
+
lastTitle,
|
|
328
|
+
recentCount,
|
|
329
|
+
windowDays: RECENT_WINDOW_DAYS
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
329
333
|
// src/checks.ts
|
|
330
334
|
var GENEROUS_FLAG_THRESHOLD = 5;
|
|
331
335
|
function runContractChecks({ brief, items, focus, history }) {
|
|
@@ -386,172 +390,9 @@ function findLeakedPhrase(brief, focus) {
|
|
|
386
390
|
return null;
|
|
387
391
|
}
|
|
388
392
|
|
|
389
|
-
// src/sessions.ts
|
|
390
|
-
import { appendFile, mkdir as mkdir2, readFile as readFile2, unlink } from "node:fs/promises";
|
|
391
|
-
var MAX_SESSION_MINUTES = 120;
|
|
392
|
-
var MIN_LOGGABLE_MINUTES = 1;
|
|
393
|
-
async function readActive(config) {
|
|
394
|
-
try {
|
|
395
|
-
return JSON.parse(await readFile2(config.sessionFile, "utf8"));
|
|
396
|
-
} catch {
|
|
397
|
-
return null;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
async function writeActive(config, session) {
|
|
401
|
-
await writeJsonAtomic(config.sessionFile, session);
|
|
402
|
-
}
|
|
403
|
-
async function clearActive(config) {
|
|
404
|
-
try {
|
|
405
|
-
await unlink(config.sessionFile);
|
|
406
|
-
} catch {
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
async function logSession(config, session, endedAt, endedBy) {
|
|
410
|
-
const startedAt = parseISO(session.startedAt);
|
|
411
|
-
if (!startedAt) return null;
|
|
412
|
-
const actualMinutes = Math.round((endedAt.getTime() - startedAt.getTime()) / 6e4);
|
|
413
|
-
if (actualMinutes < MIN_LOGGABLE_MINUTES) return null;
|
|
414
|
-
const entry = {
|
|
415
|
-
id: session.id,
|
|
416
|
-
title: session.title,
|
|
417
|
-
startedAt: session.startedAt,
|
|
418
|
-
endedAt: endedAt.toISOString(),
|
|
419
|
-
plannedMinutes: session.minutes,
|
|
420
|
-
actualMinutes,
|
|
421
|
-
reachedTarget: actualMinutes >= session.minutes,
|
|
422
|
-
advancesObjective: session.advancesObjective,
|
|
423
|
-
endedBy
|
|
424
|
-
};
|
|
425
|
-
await mkdir2(config.dataDir, { recursive: true });
|
|
426
|
-
await appendFile(config.sessionsLogFile, `${JSON.stringify(entry)}
|
|
427
|
-
`, "utf8");
|
|
428
|
-
return entry;
|
|
429
|
-
}
|
|
430
|
-
async function startSession(config, opts) {
|
|
431
|
-
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
432
|
-
await stopSession(config, now);
|
|
433
|
-
await writeActive(config, {
|
|
434
|
-
id: opts.id,
|
|
435
|
-
title: opts.title,
|
|
436
|
-
startedAt: now.toISOString(),
|
|
437
|
-
minutes: opts.minutes,
|
|
438
|
-
advancesObjective: opts.advancesObjective,
|
|
439
|
-
// The click that got here is itself the first evidence of a person.
|
|
440
|
-
lastActiveAt: now.toISOString()
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
async function stopSession(config, now = /* @__PURE__ */ new Date(), endedBy = "user") {
|
|
444
|
-
const active = await readActive(config);
|
|
445
|
-
if (!active) return null;
|
|
446
|
-
await logSession(config, active, now, endedBy);
|
|
447
|
-
await clearActive(config);
|
|
448
|
-
return active;
|
|
449
|
-
}
|
|
450
|
-
async function reconcileSession(config, probe, now = /* @__PURE__ */ new Date()) {
|
|
451
|
-
if (config.awayAfterMinutes <= 0) return null;
|
|
452
|
-
const active = await readActive(config);
|
|
453
|
-
if (!active) return null;
|
|
454
|
-
const startedAt = parseISO(active.startedAt);
|
|
455
|
-
if (!startedAt) {
|
|
456
|
-
await clearActive(config);
|
|
457
|
-
return null;
|
|
458
|
-
}
|
|
459
|
-
const awayAfterMs = config.awayAfterMinutes * 6e4;
|
|
460
|
-
const previous = parseISO(active.lastActiveAt) ?? startedAt;
|
|
461
|
-
if (now.getTime() - previous.getTime() > awayAfterMs) {
|
|
462
|
-
const logged = await logSession(config, active, previous, "away");
|
|
463
|
-
await clearActive(config);
|
|
464
|
-
return logged;
|
|
465
|
-
}
|
|
466
|
-
const idleSeconds = await probe();
|
|
467
|
-
const lastInput = idleSeconds === null ? now : new Date(now.getTime() - Math.max(0, idleSeconds) * 1e3);
|
|
468
|
-
const confirmed = lastInput.getTime() > previous.getTime() ? lastInput : previous;
|
|
469
|
-
if (confirmed.toISOString() !== active.lastActiveAt) {
|
|
470
|
-
await writeActive(config, { ...active, lastActiveAt: confirmed.toISOString() });
|
|
471
|
-
}
|
|
472
|
-
return null;
|
|
473
|
-
}
|
|
474
|
-
async function closeIfPastBackstop(config, active, now) {
|
|
475
|
-
if (!active) return null;
|
|
476
|
-
const startedAt = parseISO(active.startedAt);
|
|
477
|
-
const elapsed = startedAt ? (now.getTime() - startedAt.getTime()) / 6e4 : Infinity;
|
|
478
|
-
if (elapsed <= MAX_SESSION_MINUTES) return active;
|
|
479
|
-
const ceiling = new Date((startedAt?.getTime() ?? now.getTime()) + MAX_SESSION_MINUTES * 6e4);
|
|
480
|
-
await stopSession(config, ceiling, "limit");
|
|
481
|
-
return null;
|
|
482
|
-
}
|
|
483
|
-
function lastUnattendedClose(todayLog) {
|
|
484
|
-
const last = todayLog.at(-1);
|
|
485
|
-
if (!last || last.endedBy === void 0 || last.endedBy === "user") return null;
|
|
486
|
-
return {
|
|
487
|
-
id: last.id,
|
|
488
|
-
title: last.title,
|
|
489
|
-
endedAt: last.endedAt,
|
|
490
|
-
actualMinutes: last.actualMinutes,
|
|
491
|
-
reason: last.endedBy
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
async function readSessionState(config, agenda, now = /* @__PURE__ */ new Date()) {
|
|
495
|
-
const active = await closeIfPastBackstop(config, await readActive(config), now);
|
|
496
|
-
const todayLog = await readTodaysSessions(config, now);
|
|
497
|
-
const completedToday = todayLog.length;
|
|
498
|
-
const minutesToday = todayLog.reduce((sum, entry) => sum + entry.actualMinutes, 0);
|
|
499
|
-
const unattendedClose = active ? null : lastUnattendedClose(todayLog);
|
|
500
|
-
if (!active) {
|
|
501
|
-
return { active: null, completedToday, minutesToday, unattendedClose };
|
|
502
|
-
}
|
|
503
|
-
const startedAt = parseISO(active.startedAt);
|
|
504
|
-
const endsAt = new Date(startedAt.getTime() + active.minutes * 6e4);
|
|
505
|
-
const remainingSeconds = Math.round((endsAt.getTime() - now.getTime()) / 1e3);
|
|
506
|
-
const nextEvent = agenda.events.map((event) => parseISO(event.start)).filter((start) => start !== null && start.getTime() > now.getTime()).sort((a, b) => a.getTime() - b.getTime())[0];
|
|
507
|
-
return {
|
|
508
|
-
active: {
|
|
509
|
-
id: active.id,
|
|
510
|
-
title: active.title,
|
|
511
|
-
startedAt: active.startedAt,
|
|
512
|
-
advancesObjective: active.advancesObjective,
|
|
513
|
-
lastActiveAt: active.lastActiveAt,
|
|
514
|
-
endsAt: endsAt.toISOString(),
|
|
515
|
-
minutes: active.minutes,
|
|
516
|
-
remainingSeconds,
|
|
517
|
-
overrun: remainingSeconds < 0,
|
|
518
|
-
collidesWithNextEvent: nextEvent ? nextEvent.getTime() < endsAt.getTime() : false,
|
|
519
|
-
nextEventAt: nextEvent ? nextEvent.toISOString() : null
|
|
520
|
-
},
|
|
521
|
-
completedToday,
|
|
522
|
-
minutesToday,
|
|
523
|
-
unattendedClose
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
|
-
async function readSessions(config) {
|
|
527
|
-
let text;
|
|
528
|
-
try {
|
|
529
|
-
text = await readFile2(config.sessionsLogFile, "utf8");
|
|
530
|
-
} catch {
|
|
531
|
-
return [];
|
|
532
|
-
}
|
|
533
|
-
const sessions = [];
|
|
534
|
-
for (const line of text.split("\n")) {
|
|
535
|
-
if (line.trim() === "") continue;
|
|
536
|
-
try {
|
|
537
|
-
const entry = JSON.parse(line);
|
|
538
|
-
if (entry?.id && entry.startedAt) sessions.push(entry);
|
|
539
|
-
} catch {
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
return sessions;
|
|
543
|
-
}
|
|
544
|
-
async function readTodaysSessions(config, now) {
|
|
545
|
-
const all = await readSessions(config);
|
|
546
|
-
return all.filter((entry) => {
|
|
547
|
-
const started = parseISO(entry.startedAt);
|
|
548
|
-
return started !== null && started.getFullYear() === now.getFullYear() && started.getMonth() === now.getMonth() && started.getDate() === now.getDate();
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
|
|
552
393
|
// src/store.ts
|
|
553
394
|
import { createHash } from "node:crypto";
|
|
554
|
-
import { appendFile
|
|
395
|
+
import { appendFile, mkdir as mkdir2, readFile as readFile2, stat } from "node:fs/promises";
|
|
555
396
|
import { setTimeout as delay } from "node:timers/promises";
|
|
556
397
|
|
|
557
398
|
// src/agenda.ts
|
|
@@ -708,7 +549,7 @@ var Store = class {
|
|
|
708
549
|
this.config = config;
|
|
709
550
|
}
|
|
710
551
|
async ensureDataDir() {
|
|
711
|
-
await
|
|
552
|
+
await mkdir2(this.config.dataDir, { recursive: true });
|
|
712
553
|
}
|
|
713
554
|
/**
|
|
714
555
|
* Which store this is, as an opaque fingerprint: its path, and the directory's
|
|
@@ -736,7 +577,7 @@ var Store = class {
|
|
|
736
577
|
for (let attempt = 0; attempt < READ_RETRIES; attempt++) {
|
|
737
578
|
let text;
|
|
738
579
|
try {
|
|
739
|
-
text = await
|
|
580
|
+
text = await readFile2(this.config.itemsFile, "utf8");
|
|
740
581
|
} catch (err) {
|
|
741
582
|
if (err.code === "ENOENT") {
|
|
742
583
|
return {
|
|
@@ -757,7 +598,7 @@ var Store = class {
|
|
|
757
598
|
async readActions() {
|
|
758
599
|
let text;
|
|
759
600
|
try {
|
|
760
|
-
text = await
|
|
601
|
+
text = await readFile2(this.config.actionsFile, "utf8");
|
|
761
602
|
} catch (err) {
|
|
762
603
|
if (err.code === "ENOENT") return [];
|
|
763
604
|
throw err;
|
|
@@ -789,7 +630,7 @@ var Store = class {
|
|
|
789
630
|
/** Read the standing objective. Returns null when focus.md hasn't been written. */
|
|
790
631
|
async readFocus() {
|
|
791
632
|
try {
|
|
792
|
-
return parseFocus(await
|
|
633
|
+
return parseFocus(await readFile2(this.config.focusFile, "utf8"));
|
|
793
634
|
} catch (err) {
|
|
794
635
|
if (err.code === "ENOENT") return null;
|
|
795
636
|
console.warn(`[daily-focus] could not read focus.md: ${err.message}`);
|
|
@@ -802,7 +643,7 @@ var Store = class {
|
|
|
802
643
|
`;
|
|
803
644
|
const write = this.#writeQueue.then(async () => {
|
|
804
645
|
await this.ensureDataDir();
|
|
805
|
-
await
|
|
646
|
+
await appendFile(this.config.actionsFile, line, "utf8");
|
|
806
647
|
});
|
|
807
648
|
this.#writeQueue = write.catch(() => {
|
|
808
649
|
});
|
|
@@ -870,12 +711,10 @@ var Store = class {
|
|
|
870
711
|
}
|
|
871
712
|
objectiveProgress = computeObjectiveProgress(withToday, actions, now, isRunDay);
|
|
872
713
|
}
|
|
873
|
-
const session = await readSessionState(this.config, agenda, now);
|
|
874
714
|
return {
|
|
875
715
|
// toPublicFocus, not focus — the agent-only section must never be serialised.
|
|
876
716
|
focus: focus ? toPublicFocus(focus) : null,
|
|
877
717
|
objectiveProgress,
|
|
878
|
-
session,
|
|
879
718
|
schedule: {
|
|
880
719
|
days: [...schedule.days],
|
|
881
720
|
source: schedule.source,
|
|
@@ -931,6 +770,9 @@ function foldActionLog(actions, now) {
|
|
|
931
770
|
case "note":
|
|
932
771
|
if (action.text) entry.notes.push({ text: action.text, at: action.at });
|
|
933
772
|
break;
|
|
773
|
+
case "nudge":
|
|
774
|
+
entry.nudgedAt = action.at;
|
|
775
|
+
break;
|
|
934
776
|
case "done":
|
|
935
777
|
entry.status = "done";
|
|
936
778
|
delete entry.snoozedUntil;
|
|
@@ -982,16 +824,15 @@ function resolveItems(items, actions, now) {
|
|
|
982
824
|
}
|
|
983
825
|
|
|
984
826
|
export {
|
|
985
|
-
writeJsonAtomic,
|
|
986
|
-
tempPathFor,
|
|
987
827
|
canonicalId,
|
|
988
828
|
fingerprintId,
|
|
989
|
-
|
|
829
|
+
writeJsonAtomic,
|
|
830
|
+
tempPathFor,
|
|
990
831
|
parseBrief,
|
|
832
|
+
ACTION_TYPES,
|
|
833
|
+
setsStatus,
|
|
834
|
+
readArchiveIndex,
|
|
991
835
|
runContractChecks,
|
|
992
|
-
startSession,
|
|
993
|
-
stopSession,
|
|
994
|
-
reconcileSession,
|
|
995
836
|
Store,
|
|
996
837
|
foldActionLog,
|
|
997
838
|
resolveItems
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
writeJsonAtomic
|
|
3
|
+
} from "./chunk-RZNQJ6UG.js";
|
|
4
|
+
import {
|
|
5
|
+
localTimeZone
|
|
6
|
+
} from "./chunk-J7ZYVUG6.js";
|
|
7
|
+
|
|
8
|
+
// src/zone.ts
|
|
9
|
+
import { readFile, readlink } from "node:fs/promises";
|
|
10
|
+
function validZone(raw) {
|
|
11
|
+
if (typeof raw !== "string" || !/^[A-Za-z][A-Za-z0-9_+\-/]*$/.test(raw)) return null;
|
|
12
|
+
try {
|
|
13
|
+
new Intl.DateTimeFormat("en", { timeZone: raw });
|
|
14
|
+
return raw;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function readSystemZone(path = "/etc/localtime") {
|
|
20
|
+
try {
|
|
21
|
+
const target = await readlink(path);
|
|
22
|
+
const name = /zoneinfo\/(?:posix\/|right\/)?(.+)$/.exec(target)?.[1];
|
|
23
|
+
return validZone(name);
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function startingZone(file, system) {
|
|
29
|
+
const saved = await loadZoneFile(file);
|
|
30
|
+
if (!saved) return { timeZone: localTimeZone(), moved: false };
|
|
31
|
+
if (system && saved.systemTimeZone && system !== saved.systemTimeZone) return { timeZone: system, moved: true };
|
|
32
|
+
return { timeZone: saved.timeZone, moved: false };
|
|
33
|
+
}
|
|
34
|
+
async function loadZoneFile(file) {
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(await readFile(file, "utf8"));
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const record = parsed ?? {};
|
|
42
|
+
const timeZone = validZone(record.timeZone);
|
|
43
|
+
if (!timeZone) return null;
|
|
44
|
+
return { timeZone, systemTimeZone: validZone(record.systemTimeZone) };
|
|
45
|
+
}
|
|
46
|
+
var TimeZoneKeeper = class {
|
|
47
|
+
#file;
|
|
48
|
+
#readSystem;
|
|
49
|
+
#current = localTimeZone();
|
|
50
|
+
/** The Mac's zone as last read, so `checkSystem` acts on a change rather than a difference. */
|
|
51
|
+
#system = null;
|
|
52
|
+
constructor(file, readSystem = readSystemZone) {
|
|
53
|
+
this.#file = file;
|
|
54
|
+
this.#readSystem = readSystem;
|
|
55
|
+
}
|
|
56
|
+
get current() {
|
|
57
|
+
return this.#current;
|
|
58
|
+
}
|
|
59
|
+
/** Decide the zone at startup; see `startingZone`. */
|
|
60
|
+
async start() {
|
|
61
|
+
this.#system = await this.#readSystem();
|
|
62
|
+
const { timeZone, moved } = await startingZone(this.#file, this.#system);
|
|
63
|
+
if (moved) await this.#adopt(timeZone);
|
|
64
|
+
else this.#apply(timeZone);
|
|
65
|
+
}
|
|
66
|
+
/** A browser says it is in `zone`. True if that changed the dashboard's. */
|
|
67
|
+
async report(zone) {
|
|
68
|
+
if (zone === this.#current) return false;
|
|
69
|
+
await this.#adopt(zone);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
/** Follow the Mac if its zone has changed since last read. True if that changed the dashboard's. */
|
|
73
|
+
async checkSystem() {
|
|
74
|
+
const system = await this.#readSystem();
|
|
75
|
+
if (!system || system === this.#system) return false;
|
|
76
|
+
this.#system = system;
|
|
77
|
+
if (system === this.#current) {
|
|
78
|
+
await this.#save();
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
await this.#adopt(system);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
async #adopt(zone) {
|
|
85
|
+
this.#apply(zone);
|
|
86
|
+
await this.#save();
|
|
87
|
+
}
|
|
88
|
+
#apply(zone) {
|
|
89
|
+
this.#current = zone;
|
|
90
|
+
process.env.TZ = zone;
|
|
91
|
+
}
|
|
92
|
+
async #save() {
|
|
93
|
+
const file = { timeZone: this.#current, systemTimeZone: this.#system };
|
|
94
|
+
try {
|
|
95
|
+
await writeJsonAtomic(this.#file, file);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.warn(`[daily-focus] could not save the time zone: ${err.message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export {
|
|
103
|
+
validZone,
|
|
104
|
+
readSystemZone,
|
|
105
|
+
startingZone,
|
|
106
|
+
TimeZoneKeeper
|
|
107
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -49,11 +49,12 @@ function shouldOpen(flag) {
|
|
|
49
49
|
return flag && process.stdout.isTTY === true && !process.env.CI;
|
|
50
50
|
}
|
|
51
51
|
async function demo(open, profile) {
|
|
52
|
-
const { demoEnv, demoIntegrations, demoProfile, writeDemoStore } = await import("./demo-
|
|
52
|
+
const { demoEnv, demoIntegrations, demoProfile, writeDemoStore } = await import("./demo-BYLLMKSQ.js");
|
|
53
53
|
const chosen = demoProfile(profile);
|
|
54
54
|
const store = await mkdtemp(join(tmpdir(), "daily-focus-demo-"));
|
|
55
55
|
process.on("exit", () => rmSync(store, { recursive: true, force: true }));
|
|
56
|
-
|
|
56
|
+
const now = /* @__PURE__ */ new Date();
|
|
57
|
+
await writeDemoStore(store, chosen, now);
|
|
57
58
|
await start(
|
|
58
59
|
open,
|
|
59
60
|
(url) => {
|
|
@@ -61,11 +62,11 @@ async function demo(open, profile) {
|
|
|
61
62
|
The demo is at ${url}, with sample data in a throwaway store (the ${chosen} profile).`);
|
|
62
63
|
console.log(" Nothing is connected and nothing runs. Stop it with Ctrl+C; the store goes with it.\n");
|
|
63
64
|
},
|
|
64
|
-
{ env: demoEnv(process.env, store, chosen), integrations: demoIntegrations(chosen) }
|
|
65
|
+
{ env: demoEnv(process.env, store, chosen), integrations: demoIntegrations(chosen, now) }
|
|
65
66
|
);
|
|
66
67
|
}
|
|
67
68
|
async function start(open, onStarted, options = {}) {
|
|
68
|
-
const { runServer } = await import("./server-
|
|
69
|
+
const { runServer } = await import("./server-WQYUOKB3.js");
|
|
69
70
|
await runServer({
|
|
70
71
|
...options,
|
|
71
72
|
onStarted: (url) => {
|
|
@@ -162,20 +163,20 @@ async function main(argv) {
|
|
|
162
163
|
}
|
|
163
164
|
return start(open);
|
|
164
165
|
case "service": {
|
|
165
|
-
const { main: service } = await import("./service-
|
|
166
|
+
const { main: service } = await import("./service-PUJ3YDH5.js");
|
|
166
167
|
process.exitCode = await service(values.remove ? ["--remove"] : []);
|
|
167
168
|
return;
|
|
168
169
|
}
|
|
169
170
|
// These three are scripts that do their work as they load, and read their
|
|
170
171
|
// own flags from the command line.
|
|
171
172
|
case "init":
|
|
172
|
-
await import("./init-
|
|
173
|
+
await import("./init-EMZDG2I7.js");
|
|
173
174
|
return;
|
|
174
175
|
case "audit":
|
|
175
|
-
await import("./audit-
|
|
176
|
+
await import("./audit-7ZCLKF46.js");
|
|
176
177
|
return;
|
|
177
178
|
case "seed":
|
|
178
|
-
await import("./seed-
|
|
179
|
+
await import("./seed-JU4LQ33D.js");
|
|
179
180
|
return;
|
|
180
181
|
case "build-calendar":
|
|
181
182
|
return buildCalendar();
|