daily-focus 1.2.0-dev.7 → 1.2.0-dev.9
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 +7 -12
- package/SETUP.md +3 -5
- package/dist/{audit-7YAJYHZF.js → audit-DNTFZOHH.js} +3 -3
- package/dist/{chunk-4DCBCR4D.js → chunk-6SMUSPZG.js} +2 -2
- package/dist/{chunk-JQGYCWRX.js → chunk-GQ3NCWRT.js} +7 -175
- package/dist/{chunk-SX5FPS3A.js → chunk-KL6RU4XT.js} +141 -15
- package/dist/{chunk-RNE27FQY.js → chunk-KZNY45XW.js} +0 -5
- package/dist/{chunk-UY6HQLGE.js → chunk-P3E2B2SD.js} +1 -1
- package/dist/cli.js +6 -6
- package/dist/{demo-5I4BNHW2.js → demo-TIIXODJG.js} +38 -25
- package/dist/{init-57WHZZNG.js → init-XDMAXU2S.js} +1 -9
- package/dist/{seed-BLFBCHIA.js → seed-TWT7EDXJ.js} +3 -3
- package/dist/{server-UI6WYRTL.js → server-WT4CQ3VX.js} +88 -218
- package/dist/{service-NAAAV67B.js → service-ZUHI3VKH.js} +1 -1
- package/package.json +1 -1
- package/prompts/README.md +2 -3
- package/prompts/assistant.md +7 -2
- package/prompts/morning-brief-personal.md +0 -25
- package/prompts/morning-brief-work.md +0 -28
- package/public/app.js +361 -427
- package/public/style.css +141 -180
package/README.md
CHANGED
|
@@ -64,13 +64,6 @@ since you completed something that advanced your objective.
|
|
|
64
64
|
- Mark tasks **Done** when you've handled them.
|
|
65
65
|
- **Snooze** an item until a date, or **Dismiss** it if it doesn't need attention.
|
|
66
66
|
- Leave a **Note** for the next brief, such as "already replied on Slack".
|
|
67
|
-
- Press **f** for focus mode, which shows your top item and agenda.
|
|
68
|
-
- Press **p** on an item to start a focus timer. Its countdown appears in the tab
|
|
69
|
-
title, and the session becomes part of the next brief's context.
|
|
70
|
-
|
|
71
|
-
The timer keeps counting after its target time. On macOS, it ends a session when
|
|
72
|
-
you've been away from the machine, excluding the time you were away. On systems
|
|
73
|
-
without idle detection, a two-hour limit caps the recorded session.
|
|
74
67
|
|
|
75
68
|
You can undo item actions except notes. The page updates as new data arrives, and
|
|
76
69
|
the theme button switches between system, light, and dark mode.
|
|
@@ -137,9 +130,13 @@ why is CI red, what does this thread actually want from me. With
|
|
|
137
130
|
`DAILY_FOCUS_ASSISTANT` set to `claude` or `codex`, every item's panel gets an
|
|
138
131
|
assistant that runs that CLI headless against the item, with quick actions for the
|
|
139
132
|
common asks and a field for anything else. Click a card to open its panel, where its
|
|
140
|
-
notes live too
|
|
141
|
-
|
|
142
|
-
|
|
133
|
+
notes live too. While the assistant works, the panel shows what it is doing: each
|
|
134
|
+
command it runs and each page, thread or ticket it reads, with a clock, and a cross
|
|
135
|
+
on anything that failed, such as a repository it couldn't see. Once it answers,
|
|
136
|
+
that trail folds away under the answer. The answer has buttons to copy it, or any
|
|
137
|
+
block in it meant for pasting, and to open a draft it created. The field stays at
|
|
138
|
+
the foot of the panel, so you can follow up in the same conversation. The panel
|
|
139
|
+
stays on the item it was opened for while you move through the list.
|
|
143
140
|
|
|
144
141
|
It is told to read and draft, and nothing more. An email reply is created as a Gmail
|
|
145
142
|
draft on the thread, so it is waiting there when you open the mail; a reply to a
|
|
@@ -177,8 +174,6 @@ Press **?** in the dashboard to see the shortcuts.
|
|
|
177
174
|
| `a` | Open the panel, ready to ask the assistant |
|
|
178
175
|
| `o` | Open the source link |
|
|
179
176
|
| `u` | Undo the last action |
|
|
180
|
-
| `f` | Toggle focus mode |
|
|
181
|
-
| `p` | Start / stop a focus session |
|
|
182
177
|
| `r` | Refresh the current board |
|
|
183
178
|
| `w` | Switch Jira views |
|
|
184
179
|
| `,` | Settings |
|
package/SETUP.md
CHANGED
|
@@ -148,8 +148,8 @@ Set `DAILY_FOCUS_AGENT_MODEL` and `DAILY_FOCUS_AGENT_EFFORT` if the CLI's own
|
|
|
148
148
|
defaults are not what you want for a brief; a brief is worth a high effort. If the
|
|
149
149
|
server can't find the CLI, set `DAILY_FOCUS_AGENT_BIN`.
|
|
150
150
|
|
|
151
|
-
The agent writes `items.json`. The dashboard records your actions
|
|
152
|
-
|
|
151
|
+
The agent writes `items.json`. The dashboard records your actions for the next run
|
|
152
|
+
to read.
|
|
153
153
|
|
|
154
154
|
After the first run, read its report (click "updated … ago"): it says which sources
|
|
155
155
|
it couldn't reach. To check the brief against the payload contract, run:
|
|
@@ -461,7 +461,7 @@ DAILY_FOCUS_PROFILE=personal
|
|
|
461
461
|
The dashboard then links the personal prompt, `prompts/morning-brief-personal.md`,
|
|
462
462
|
into the store as `prompt.md`, and the source list starts from a template with
|
|
463
463
|
sections for those sources. The profile also switches off
|
|
464
|
-
the Jira board
|
|
464
|
+
the Jira board, and gives the tab a green house icon instead of the blue briefcase. The pull request
|
|
465
465
|
board stays on for side projects; point it at your personal account with
|
|
466
466
|
`DAILY_FOCUS_GITHUB_ACCOUNTS`, or set `DAILY_FOCUS_GITHUB=off`.
|
|
467
467
|
|
|
@@ -522,8 +522,6 @@ matters when `DAILY_FOCUS_FREE_WINDOWS` is off.
|
|
|
522
522
|
| `DAILY_FOCUS_MIN_FREE_WINDOW` | `45` | Minutes before a gap counts as a focus window |
|
|
523
523
|
| `DAILY_FOCUS_STALE_AFTER_HOURS` | `24` | When to expect a refresh. Shows an informational message for 45 minutes before warning that the agent may not have run. Counted only in hours a run was due, so days off never trip it |
|
|
524
524
|
| `DAILY_FOCUS_AGENT_DAYS` | `1-5` | Weekdays the dashboard runs the agent on, cron-style and cron-numbered: `1-5` for Monday to Friday, `0-4` for Sunday to Thursday, `0,6` for a weekend-only run |
|
|
525
|
-
| `DAILY_FOCUS_SESSION_MINUTES` | `25` | Default focus session length |
|
|
526
|
-
| `DAILY_FOCUS_AWAY_AFTER` | `10`; `0` for personal | Minutes of an untouched machine before a session is closed at the last sign of life. `0` turns it off |
|
|
527
525
|
| `DAILY_FOCUS_GITHUB` | `on` | `off` disables the pull request board, and the ticket board stops checking GitHub; nothing is polled |
|
|
528
526
|
| `DAILY_FOCUS_GITHUB_ACCOUNTS` | *gh's active account* | Logins to poll as, comma-separated, each resolved with `gh auth token --user` |
|
|
529
527
|
| `DAILY_FOCUS_GITHUB_SCOPE` | *everything* | Organisations or `owner/repo` entries to limit the board to, comma-separated |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
readSystemZone,
|
|
3
3
|
startingZone
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-6SMUSPZG.js";
|
|
5
5
|
import {
|
|
6
6
|
linkState
|
|
7
7
|
} from "./chunk-QKT2FAB5.js";
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
resolveItems,
|
|
14
14
|
runContractChecks,
|
|
15
15
|
setsStatus
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-GQ3NCWRT.js";
|
|
17
17
|
import {
|
|
18
18
|
BRIEF_REFRESH_GRACE_HOURS,
|
|
19
19
|
describeSchedule,
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
resolveSchedule,
|
|
24
24
|
runsOn,
|
|
25
25
|
workingMsBetween
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-KZNY45XW.js";
|
|
27
27
|
import {
|
|
28
28
|
command
|
|
29
29
|
} from "./chunk-BC2W7NND.js";
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
startOfLocalDay,
|
|
15
15
|
workingDaysSince,
|
|
16
16
|
workingMsBetween
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-KZNY45XW.js";
|
|
18
18
|
|
|
19
19
|
// src/fs.ts
|
|
20
20
|
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
@@ -390,172 +390,9 @@ function findLeakedPhrase(brief, focus) {
|
|
|
390
390
|
return null;
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
-
// src/sessions.ts
|
|
394
|
-
import { appendFile, mkdir as mkdir2, readFile as readFile2, unlink } from "node:fs/promises";
|
|
395
|
-
var MAX_SESSION_MINUTES = 120;
|
|
396
|
-
var MIN_LOGGABLE_MINUTES = 1;
|
|
397
|
-
async function readActive(config) {
|
|
398
|
-
try {
|
|
399
|
-
return JSON.parse(await readFile2(config.sessionFile, "utf8"));
|
|
400
|
-
} catch {
|
|
401
|
-
return null;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
async function writeActive(config, session) {
|
|
405
|
-
await writeJsonAtomic(config.sessionFile, session);
|
|
406
|
-
}
|
|
407
|
-
async function clearActive(config) {
|
|
408
|
-
try {
|
|
409
|
-
await unlink(config.sessionFile);
|
|
410
|
-
} catch {
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
async function logSession(config, session, endedAt, endedBy) {
|
|
414
|
-
const startedAt = parseISO(session.startedAt);
|
|
415
|
-
if (!startedAt) return null;
|
|
416
|
-
const actualMinutes = Math.round((endedAt.getTime() - startedAt.getTime()) / 6e4);
|
|
417
|
-
if (actualMinutes < MIN_LOGGABLE_MINUTES) return null;
|
|
418
|
-
const entry = {
|
|
419
|
-
id: session.id,
|
|
420
|
-
title: session.title,
|
|
421
|
-
startedAt: session.startedAt,
|
|
422
|
-
endedAt: endedAt.toISOString(),
|
|
423
|
-
plannedMinutes: session.minutes,
|
|
424
|
-
actualMinutes,
|
|
425
|
-
reachedTarget: actualMinutes >= session.minutes,
|
|
426
|
-
advancesObjective: session.advancesObjective,
|
|
427
|
-
endedBy
|
|
428
|
-
};
|
|
429
|
-
await mkdir2(config.dataDir, { recursive: true });
|
|
430
|
-
await appendFile(config.sessionsLogFile, `${JSON.stringify(entry)}
|
|
431
|
-
`, "utf8");
|
|
432
|
-
return entry;
|
|
433
|
-
}
|
|
434
|
-
async function startSession(config, opts) {
|
|
435
|
-
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
436
|
-
await stopSession(config, now);
|
|
437
|
-
await writeActive(config, {
|
|
438
|
-
id: opts.id,
|
|
439
|
-
title: opts.title,
|
|
440
|
-
startedAt: now.toISOString(),
|
|
441
|
-
minutes: opts.minutes,
|
|
442
|
-
advancesObjective: opts.advancesObjective,
|
|
443
|
-
// The click that got here is itself the first evidence of a person.
|
|
444
|
-
lastActiveAt: now.toISOString()
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
async function stopSession(config, now = /* @__PURE__ */ new Date(), endedBy = "user") {
|
|
448
|
-
const active = await readActive(config);
|
|
449
|
-
if (!active) return null;
|
|
450
|
-
await logSession(config, active, now, endedBy);
|
|
451
|
-
await clearActive(config);
|
|
452
|
-
return active;
|
|
453
|
-
}
|
|
454
|
-
async function reconcileSession(config, probe, now = /* @__PURE__ */ new Date()) {
|
|
455
|
-
if (config.awayAfterMinutes <= 0) return null;
|
|
456
|
-
const active = await readActive(config);
|
|
457
|
-
if (!active) return null;
|
|
458
|
-
const startedAt = parseISO(active.startedAt);
|
|
459
|
-
if (!startedAt) {
|
|
460
|
-
await clearActive(config);
|
|
461
|
-
return null;
|
|
462
|
-
}
|
|
463
|
-
const awayAfterMs = config.awayAfterMinutes * 6e4;
|
|
464
|
-
const previous = parseISO(active.lastActiveAt) ?? startedAt;
|
|
465
|
-
if (now.getTime() - previous.getTime() > awayAfterMs) {
|
|
466
|
-
const logged = await logSession(config, active, previous, "away");
|
|
467
|
-
await clearActive(config);
|
|
468
|
-
return logged;
|
|
469
|
-
}
|
|
470
|
-
const idleSeconds = await probe();
|
|
471
|
-
const lastInput = idleSeconds === null ? now : new Date(now.getTime() - Math.max(0, idleSeconds) * 1e3);
|
|
472
|
-
const confirmed = lastInput.getTime() > previous.getTime() ? lastInput : previous;
|
|
473
|
-
if (confirmed.toISOString() !== active.lastActiveAt) {
|
|
474
|
-
await writeActive(config, { ...active, lastActiveAt: confirmed.toISOString() });
|
|
475
|
-
}
|
|
476
|
-
return null;
|
|
477
|
-
}
|
|
478
|
-
async function closeIfPastBackstop(config, active, now) {
|
|
479
|
-
if (!active) return null;
|
|
480
|
-
const startedAt = parseISO(active.startedAt);
|
|
481
|
-
const elapsed = startedAt ? (now.getTime() - startedAt.getTime()) / 6e4 : Infinity;
|
|
482
|
-
if (elapsed <= MAX_SESSION_MINUTES) return active;
|
|
483
|
-
const ceiling = new Date((startedAt?.getTime() ?? now.getTime()) + MAX_SESSION_MINUTES * 6e4);
|
|
484
|
-
await stopSession(config, ceiling, "limit");
|
|
485
|
-
return null;
|
|
486
|
-
}
|
|
487
|
-
function lastUnattendedClose(todayLog) {
|
|
488
|
-
const last = todayLog.at(-1);
|
|
489
|
-
if (!last || last.endedBy === void 0 || last.endedBy === "user") return null;
|
|
490
|
-
return {
|
|
491
|
-
id: last.id,
|
|
492
|
-
title: last.title,
|
|
493
|
-
endedAt: last.endedAt,
|
|
494
|
-
actualMinutes: last.actualMinutes,
|
|
495
|
-
reason: last.endedBy
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
async function readSessionState(config, agenda, now = /* @__PURE__ */ new Date()) {
|
|
499
|
-
const active = await closeIfPastBackstop(config, await readActive(config), now);
|
|
500
|
-
const todayLog = await readTodaysSessions(config, now);
|
|
501
|
-
const completedToday = todayLog.length;
|
|
502
|
-
const minutesToday = todayLog.reduce((sum, entry) => sum + entry.actualMinutes, 0);
|
|
503
|
-
const unattendedClose = active ? null : lastUnattendedClose(todayLog);
|
|
504
|
-
if (!active) {
|
|
505
|
-
return { active: null, completedToday, minutesToday, unattendedClose };
|
|
506
|
-
}
|
|
507
|
-
const startedAt = parseISO(active.startedAt);
|
|
508
|
-
const endsAt = new Date(startedAt.getTime() + active.minutes * 6e4);
|
|
509
|
-
const remainingSeconds = Math.round((endsAt.getTime() - now.getTime()) / 1e3);
|
|
510
|
-
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];
|
|
511
|
-
return {
|
|
512
|
-
active: {
|
|
513
|
-
id: active.id,
|
|
514
|
-
title: active.title,
|
|
515
|
-
startedAt: active.startedAt,
|
|
516
|
-
advancesObjective: active.advancesObjective,
|
|
517
|
-
lastActiveAt: active.lastActiveAt,
|
|
518
|
-
endsAt: endsAt.toISOString(),
|
|
519
|
-
minutes: active.minutes,
|
|
520
|
-
remainingSeconds,
|
|
521
|
-
overrun: remainingSeconds < 0,
|
|
522
|
-
collidesWithNextEvent: nextEvent ? nextEvent.getTime() < endsAt.getTime() : false,
|
|
523
|
-
nextEventAt: nextEvent ? nextEvent.toISOString() : null
|
|
524
|
-
},
|
|
525
|
-
completedToday,
|
|
526
|
-
minutesToday,
|
|
527
|
-
unattendedClose
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
async function readSessions(config) {
|
|
531
|
-
let text;
|
|
532
|
-
try {
|
|
533
|
-
text = await readFile2(config.sessionsLogFile, "utf8");
|
|
534
|
-
} catch {
|
|
535
|
-
return [];
|
|
536
|
-
}
|
|
537
|
-
const sessions = [];
|
|
538
|
-
for (const line of text.split("\n")) {
|
|
539
|
-
if (line.trim() === "") continue;
|
|
540
|
-
try {
|
|
541
|
-
const entry = JSON.parse(line);
|
|
542
|
-
if (entry?.id && entry.startedAt) sessions.push(entry);
|
|
543
|
-
} catch {
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
return sessions;
|
|
547
|
-
}
|
|
548
|
-
async function readTodaysSessions(config, now) {
|
|
549
|
-
const all = await readSessions(config);
|
|
550
|
-
return all.filter((entry) => {
|
|
551
|
-
const started = parseISO(entry.startedAt);
|
|
552
|
-
return started !== null && started.getFullYear() === now.getFullYear() && started.getMonth() === now.getMonth() && started.getDate() === now.getDate();
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
|
|
556
393
|
// src/store.ts
|
|
557
394
|
import { createHash } from "node:crypto";
|
|
558
|
-
import { appendFile
|
|
395
|
+
import { appendFile, mkdir as mkdir2, readFile as readFile2, stat } from "node:fs/promises";
|
|
559
396
|
import { setTimeout as delay } from "node:timers/promises";
|
|
560
397
|
|
|
561
398
|
// src/agenda.ts
|
|
@@ -712,7 +549,7 @@ var Store = class {
|
|
|
712
549
|
this.config = config;
|
|
713
550
|
}
|
|
714
551
|
async ensureDataDir() {
|
|
715
|
-
await
|
|
552
|
+
await mkdir2(this.config.dataDir, { recursive: true });
|
|
716
553
|
}
|
|
717
554
|
/**
|
|
718
555
|
* Which store this is, as an opaque fingerprint: its path, and the directory's
|
|
@@ -740,7 +577,7 @@ var Store = class {
|
|
|
740
577
|
for (let attempt = 0; attempt < READ_RETRIES; attempt++) {
|
|
741
578
|
let text;
|
|
742
579
|
try {
|
|
743
|
-
text = await
|
|
580
|
+
text = await readFile2(this.config.itemsFile, "utf8");
|
|
744
581
|
} catch (err) {
|
|
745
582
|
if (err.code === "ENOENT") {
|
|
746
583
|
return {
|
|
@@ -761,7 +598,7 @@ var Store = class {
|
|
|
761
598
|
async readActions() {
|
|
762
599
|
let text;
|
|
763
600
|
try {
|
|
764
|
-
text = await
|
|
601
|
+
text = await readFile2(this.config.actionsFile, "utf8");
|
|
765
602
|
} catch (err) {
|
|
766
603
|
if (err.code === "ENOENT") return [];
|
|
767
604
|
throw err;
|
|
@@ -793,7 +630,7 @@ var Store = class {
|
|
|
793
630
|
/** Read the standing objective. Returns null when focus.md hasn't been written. */
|
|
794
631
|
async readFocus() {
|
|
795
632
|
try {
|
|
796
|
-
return parseFocus(await
|
|
633
|
+
return parseFocus(await readFile2(this.config.focusFile, "utf8"));
|
|
797
634
|
} catch (err) {
|
|
798
635
|
if (err.code === "ENOENT") return null;
|
|
799
636
|
console.warn(`[daily-focus] could not read focus.md: ${err.message}`);
|
|
@@ -806,7 +643,7 @@ var Store = class {
|
|
|
806
643
|
`;
|
|
807
644
|
const write = this.#writeQueue.then(async () => {
|
|
808
645
|
await this.ensureDataDir();
|
|
809
|
-
await
|
|
646
|
+
await appendFile(this.config.actionsFile, line, "utf8");
|
|
810
647
|
});
|
|
811
648
|
this.#writeQueue = write.catch(() => {
|
|
812
649
|
});
|
|
@@ -874,12 +711,10 @@ var Store = class {
|
|
|
874
711
|
}
|
|
875
712
|
objectiveProgress = computeObjectiveProgress(withToday, actions, now, isRunDay);
|
|
876
713
|
}
|
|
877
|
-
const session = await readSessionState(this.config, agenda, now);
|
|
878
714
|
return {
|
|
879
715
|
// toPublicFocus, not focus — the agent-only section must never be serialised.
|
|
880
716
|
focus: focus ? toPublicFocus(focus) : null,
|
|
881
717
|
objectiveProgress,
|
|
882
|
-
session,
|
|
883
718
|
schedule: {
|
|
884
719
|
days: [...schedule.days],
|
|
885
720
|
source: schedule.source,
|
|
@@ -998,9 +833,6 @@ export {
|
|
|
998
833
|
setsStatus,
|
|
999
834
|
readArchiveIndex,
|
|
1000
835
|
runContractChecks,
|
|
1001
|
-
startSession,
|
|
1002
|
-
stopSession,
|
|
1003
|
-
reconcileSession,
|
|
1004
836
|
Store,
|
|
1005
837
|
foldActionLog,
|
|
1006
838
|
resolveItems
|
|
@@ -107,6 +107,8 @@ import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
|
107
107
|
import { createInterface } from "node:readline";
|
|
108
108
|
var TURN_TIMEOUT_MS = 10 * 6e4;
|
|
109
109
|
var TURNS_SHOWN = 20;
|
|
110
|
+
var STEPS_KEPT = 100;
|
|
111
|
+
var DETAIL_LENGTH = 200;
|
|
110
112
|
var QUICK_ACTIONS = [
|
|
111
113
|
{
|
|
112
114
|
id: "assess-review",
|
|
@@ -193,6 +195,7 @@ function foldAssistantLog(text) {
|
|
|
193
195
|
endedAt: null,
|
|
194
196
|
status: "running",
|
|
195
197
|
reply: "",
|
|
198
|
+
steps: [],
|
|
196
199
|
error: null
|
|
197
200
|
});
|
|
198
201
|
continue;
|
|
@@ -201,6 +204,7 @@ function foldAssistantLog(text) {
|
|
|
201
204
|
if (!turn) continue;
|
|
202
205
|
turn.sessionId = typeof record.sessionId === "string" ? record.sessionId : turn.sessionId;
|
|
203
206
|
turn.reply = typeof record.reply === "string" ? record.reply : turn.reply;
|
|
207
|
+
turn.steps = readSteps(record.steps);
|
|
204
208
|
turn.endedAt = record.at;
|
|
205
209
|
if (record.event === "finished") {
|
|
206
210
|
turn.status = "done";
|
|
@@ -214,6 +218,27 @@ function foldAssistantLog(text) {
|
|
|
214
218
|
}
|
|
215
219
|
return turns;
|
|
216
220
|
}
|
|
221
|
+
function addStep(turn, step) {
|
|
222
|
+
turn.steps.push(step);
|
|
223
|
+
if (turn.steps.length > STEPS_KEPT) turn.steps.shift();
|
|
224
|
+
}
|
|
225
|
+
function sameWords(a, b) {
|
|
226
|
+
return a.replace(/\s+/g, " ").trim() === b.replace(/\s+/g, " ").trim();
|
|
227
|
+
}
|
|
228
|
+
function readSteps(value) {
|
|
229
|
+
if (!Array.isArray(value)) return [];
|
|
230
|
+
const steps = [];
|
|
231
|
+
for (const candidate of value) {
|
|
232
|
+
if (typeof candidate !== "object" || candidate === null || typeof candidate.text !== "string") continue;
|
|
233
|
+
steps.push({
|
|
234
|
+
kind: candidate.kind === "message" ? "message" : "tool",
|
|
235
|
+
text: candidate.text,
|
|
236
|
+
detail: typeof candidate.detail === "string" ? candidate.detail : null,
|
|
237
|
+
state: candidate.state === "failed" ? "failed" : "done"
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return steps;
|
|
241
|
+
}
|
|
217
242
|
function parseLine(line) {
|
|
218
243
|
const trimmed = line.trim();
|
|
219
244
|
if (!trimmed.startsWith("{")) return null;
|
|
@@ -251,11 +276,15 @@ var claudeAdapter = {
|
|
|
251
276
|
if (type === "system" && typeof event.session_id === "string") {
|
|
252
277
|
return { sessionId: event.session_id };
|
|
253
278
|
}
|
|
254
|
-
if (type === "assistant") {
|
|
279
|
+
if (type === "assistant" || type === "user") {
|
|
255
280
|
const message = event.message;
|
|
256
281
|
const content = Array.isArray(message?.content) ? message.content : [];
|
|
257
|
-
const
|
|
258
|
-
|
|
282
|
+
const parsed = {};
|
|
283
|
+
const text = type === "assistant" ? content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n\n") : "";
|
|
284
|
+
if (text) parsed.text = text;
|
|
285
|
+
const steps = content.flatMap((block) => claudeStep(block) ?? []);
|
|
286
|
+
if (steps.length) parsed.steps = steps;
|
|
287
|
+
return text || steps.length ? parsed : null;
|
|
259
288
|
}
|
|
260
289
|
if (type === "result") {
|
|
261
290
|
const ok = event.is_error !== true && event.subtype === "success";
|
|
@@ -293,10 +322,11 @@ var codexAdapter = {
|
|
|
293
322
|
if (type === "thread.started" && typeof event.thread_id === "string") {
|
|
294
323
|
return { sessionId: event.thread_id };
|
|
295
324
|
}
|
|
296
|
-
if (type === "item.completed") {
|
|
325
|
+
if (type === "item.started" || type === "item.completed") {
|
|
297
326
|
const item = event.item;
|
|
298
|
-
if (item?.type === "agent_message" && typeof item.text === "string") return { text: item.text };
|
|
299
|
-
|
|
327
|
+
if (type === "item.completed" && item?.type === "agent_message" && typeof item.text === "string") return { text: item.text };
|
|
328
|
+
const step = item ? codexStep(item, type === "item.completed") : null;
|
|
329
|
+
return step ? { steps: [step] } : null;
|
|
300
330
|
}
|
|
301
331
|
if (type === "turn.completed") return { done: { ok: true } };
|
|
302
332
|
if (type === "turn.failed" || type === "error") {
|
|
@@ -311,6 +341,87 @@ var ADAPTERS = {
|
|
|
311
341
|
claude: claudeAdapter,
|
|
312
342
|
codex: codexAdapter
|
|
313
343
|
};
|
|
344
|
+
var CLAUDE_QUIET_TOOLS = /* @__PURE__ */ new Set(["ToolSearch", "TodoWrite"]);
|
|
345
|
+
function claudeStep(block) {
|
|
346
|
+
if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
|
|
347
|
+
return { id: block.tool_use_id, state: block.is_error === true ? "failed" : "done" };
|
|
348
|
+
}
|
|
349
|
+
if (block.type !== "tool_use" || typeof block.id !== "string" || typeof block.name !== "string") return null;
|
|
350
|
+
if (CLAUDE_QUIET_TOOLS.has(block.name)) return null;
|
|
351
|
+
const input = block.input ?? {};
|
|
352
|
+
const said = (key) => typeof input[key] === "string" && input[key] !== "" ? input[key] : null;
|
|
353
|
+
const step = (text, detail) => ({ id: block.id, text, detail: shorten(detail), state: "running" });
|
|
354
|
+
switch (block.name) {
|
|
355
|
+
case "Bash":
|
|
356
|
+
return step(said("description") ?? "Run a command", said("command"));
|
|
357
|
+
case "WebFetch":
|
|
358
|
+
return step("Fetch a page", said("url"));
|
|
359
|
+
case "WebSearch":
|
|
360
|
+
return step("Search the web", said("query"));
|
|
361
|
+
case "Read":
|
|
362
|
+
return step("Read a file", said("file_path"));
|
|
363
|
+
case "Grep":
|
|
364
|
+
return step("Search files", said("pattern"));
|
|
365
|
+
case "Glob":
|
|
366
|
+
return step("List files", said("pattern"));
|
|
367
|
+
case "Task":
|
|
368
|
+
case "Agent":
|
|
369
|
+
return step("Hand part of it to a subagent", said("description"));
|
|
370
|
+
}
|
|
371
|
+
const mcp = /^mcp__(.+?)__(.+)$/.exec(block.name);
|
|
372
|
+
if (mcp) return step(connectorLabel(mcp[1], mcp[2]), compactArgs(input));
|
|
373
|
+
return step(block.name, compactArgs(input));
|
|
374
|
+
}
|
|
375
|
+
function codexStep(item, completed) {
|
|
376
|
+
if (typeof item.id !== "string") return null;
|
|
377
|
+
const failed = item.status === "failed" || item.error !== void 0 && item.error !== null;
|
|
378
|
+
const state = completed ? failed ? "failed" : "done" : "running";
|
|
379
|
+
if (item.type === "command_execution") {
|
|
380
|
+
const exited = typeof item.exit_code === "number" && item.exit_code !== 0;
|
|
381
|
+
return {
|
|
382
|
+
id: item.id,
|
|
383
|
+
text: "Run a command",
|
|
384
|
+
detail: shorten(unwrapShell(item.command ?? "")),
|
|
385
|
+
state: completed && exited ? "failed" : state
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
if (item.type === "mcp_tool_call" && typeof item.server === "string" && typeof item.tool === "string") {
|
|
389
|
+
return { id: item.id, text: connectorLabel(item.server, item.tool), detail: compactArgs(item.arguments), state };
|
|
390
|
+
}
|
|
391
|
+
if (item.type === "web_search") {
|
|
392
|
+
const action = item.action ?? {};
|
|
393
|
+
if (action.type === "open_page") return { id: item.id, text: "Open a page", detail: shorten(action.url ?? null), state };
|
|
394
|
+
if (action.type === "find_in_page") return { id: item.id, text: "Find in a page", detail: shorten(action.pattern ?? action.url ?? null), state };
|
|
395
|
+
return { id: item.id, text: "Search the web", detail: shorten(action.query || item.query || null), state };
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
function unwrapShell(command) {
|
|
400
|
+
const wrapped = /^(?:\S*\/)?(?:ba|z)?sh\s+-l?c\s+(['"])([\s\S]*)\1$/.exec(command.trim());
|
|
401
|
+
if (!wrapped) return command.trim();
|
|
402
|
+
return wrapped[1] === "'" ? wrapped[2].replaceAll(`'\\''`, `'`) : wrapped[2];
|
|
403
|
+
}
|
|
404
|
+
function connectorLabel(server, tool) {
|
|
405
|
+
const words = (name) => name.replace(/[_-]+/g, " ").trim();
|
|
406
|
+
if (server === "codex_apps") {
|
|
407
|
+
const said = words(tool);
|
|
408
|
+
return said.charAt(0).toUpperCase() + said.slice(1);
|
|
409
|
+
}
|
|
410
|
+
return `${words(server.replace(/^claude_ai_/, ""))}: ${words(tool)}`;
|
|
411
|
+
}
|
|
412
|
+
function compactArgs(input) {
|
|
413
|
+
if (input === null || input === void 0) return null;
|
|
414
|
+
if (typeof input === "object" && Object.keys(input).length === 0) return null;
|
|
415
|
+
return shorten(typeof input === "string" ? input : JSON.stringify(input));
|
|
416
|
+
}
|
|
417
|
+
function shorten(value) {
|
|
418
|
+
if (!value) return null;
|
|
419
|
+
const lines = value.trim().split("\n");
|
|
420
|
+
const first = lines[0].trim();
|
|
421
|
+
const more = lines.length > 1;
|
|
422
|
+
if (first.length > DETAIL_LENGTH) return `${first.slice(0, DETAIL_LENGTH - 1)}\u2026`;
|
|
423
|
+
return more ? `${first} \u2026` : first;
|
|
424
|
+
}
|
|
314
425
|
function composePrompt(instructions, context, request, profile) {
|
|
315
426
|
const now = /* @__PURE__ */ new Date();
|
|
316
427
|
const facts = [`- Profile: ${profile}`, `- Today: ${now.toISOString().slice(0, 10)}`];
|
|
@@ -372,6 +483,7 @@ var Assistant = class _Assistant {
|
|
|
372
483
|
sessionId: turn.sessionId,
|
|
373
484
|
reason: turn.error,
|
|
374
485
|
reply: turn.reply,
|
|
486
|
+
steps: turn.steps,
|
|
375
487
|
at: turn.endedAt
|
|
376
488
|
});
|
|
377
489
|
}
|
|
@@ -431,6 +543,7 @@ ${typed}` : action.request : typed;
|
|
|
431
543
|
endedAt: null,
|
|
432
544
|
status: "running",
|
|
433
545
|
reply: "",
|
|
546
|
+
steps: [],
|
|
434
547
|
error: null
|
|
435
548
|
};
|
|
436
549
|
this.turns.set(turn.id, turn);
|
|
@@ -492,18 +605,27 @@ ${typed}` : action.request : typed;
|
|
|
492
605
|
stderr.push(chunk.toString("utf8"));
|
|
493
606
|
if (stderr.length > 50) stderr.shift();
|
|
494
607
|
});
|
|
608
|
+
const calls = /* @__PURE__ */ new Map();
|
|
495
609
|
if (child.stdout) {
|
|
496
610
|
const lines = createInterface({ input: child.stdout });
|
|
497
611
|
lines.on("line", (line) => {
|
|
498
612
|
const event = adapter.parse(line);
|
|
499
613
|
if (!event) return;
|
|
500
614
|
if (event.sessionId) turn.sessionId = event.sessionId;
|
|
501
|
-
if (event.text) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
615
|
+
if (event.text) addStep(turn, { kind: "message", text: event.text, detail: null, state: "done" });
|
|
616
|
+
for (const parsed of event.steps ?? []) {
|
|
617
|
+
const known = calls.get(parsed.id);
|
|
618
|
+
if (known) {
|
|
619
|
+
if (parsed.text) known.text = parsed.text;
|
|
620
|
+
if (parsed.detail) known.detail = parsed.detail;
|
|
621
|
+
known.state = parsed.state;
|
|
622
|
+
} else if (parsed.text) {
|
|
623
|
+
const step = { kind: "tool", text: parsed.text, detail: parsed.detail ?? null, state: parsed.state };
|
|
624
|
+
calls.set(parsed.id, step);
|
|
625
|
+
addStep(turn, step);
|
|
626
|
+
}
|
|
506
627
|
}
|
|
628
|
+
if (event.text || event.steps) this.hooks.onChange();
|
|
507
629
|
if (event.done) done = event.done;
|
|
508
630
|
});
|
|
509
631
|
}
|
|
@@ -535,20 +657,24 @@ ${event.text}` : event.text;
|
|
|
535
657
|
}
|
|
536
658
|
async finish(turn, outcome, aborted = false) {
|
|
537
659
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
538
|
-
const
|
|
660
|
+
const last = turn.steps.findLast((step) => step.kind === "message");
|
|
661
|
+
const reply = outcome.ok ? outcome.reply ?? last?.text ?? "" : "";
|
|
662
|
+
if (outcome.ok && last && sameWords(last.text, reply)) turn.steps.splice(turn.steps.lastIndexOf(last), 1);
|
|
663
|
+
for (const step of turn.steps) if (step.state === "running") step.state = outcome.ok ? "done" : "failed";
|
|
539
664
|
turn.reply = reply;
|
|
540
665
|
turn.endedAt = at;
|
|
666
|
+
const steps = turn.steps;
|
|
541
667
|
if (outcome.ok) {
|
|
542
668
|
turn.status = "done";
|
|
543
|
-
await this.append({ event: "finished", turn: turn.id, sessionId: turn.sessionId, reply, at });
|
|
669
|
+
await this.append({ event: "finished", turn: turn.id, sessionId: turn.sessionId, reply, steps, at });
|
|
544
670
|
} else if (aborted) {
|
|
545
671
|
turn.status = "aborted";
|
|
546
672
|
turn.error = outcome.error ?? "stopped";
|
|
547
|
-
await this.append({ event: "aborted", turn: turn.id, sessionId: turn.sessionId, reason: turn.error, reply, at });
|
|
673
|
+
await this.append({ event: "aborted", turn: turn.id, sessionId: turn.sessionId, reason: turn.error, reply, steps, at });
|
|
548
674
|
} else {
|
|
549
675
|
turn.status = "failed";
|
|
550
676
|
turn.error = outcome.error ?? "failed";
|
|
551
|
-
await this.append({ event: "failed", turn: turn.id, sessionId: turn.sessionId, error: turn.error, reply, at });
|
|
677
|
+
await this.append({ event: "failed", turn: turn.id, sessionId: turn.sessionId, error: turn.error, reply, steps, at });
|
|
552
678
|
}
|
|
553
679
|
this.hooks.onChange();
|
|
554
680
|
}
|
|
@@ -440,15 +440,10 @@ function loadConfig(env = loadEnv()) {
|
|
|
440
440
|
schemaFile: resolve2(dataDir, "items.schema.json"),
|
|
441
441
|
schemaSource: resolve2(import.meta.dirname, "..", "schema", "items.schema.json"),
|
|
442
442
|
archiveDir: resolve2(dataDir, "archive"),
|
|
443
|
-
sessionFile: resolve2(dataDir, "session.json"),
|
|
444
|
-
sessionsLogFile: resolve2(dataDir, "sessions.jsonl"),
|
|
445
443
|
pullsFile: resolve2(dataDir, "prs.json"),
|
|
446
444
|
calendarFile: resolve2(dataDir, "calendar.json"),
|
|
447
445
|
ticketsFile: resolve2(dataDir, "tickets.json"),
|
|
448
446
|
timeZoneFile: resolve2(dataDir, "timezone.json"),
|
|
449
|
-
// The same 1 to 120 the session endpoint holds a start to.
|
|
450
|
-
sessionMinutes: envBounded("DAILY_FOCUS_SESSION_MINUTES", 25, 1, 120, env),
|
|
451
|
-
awayAfterMinutes: envBounded("DAILY_FOCUS_AWAY_AFTER", profile === "personal" ? 0 : 10, 0, 24 * 60, env),
|
|
452
447
|
port: envBounded("DAILY_FOCUS_PORT", 4321, 0, 65535, env),
|
|
453
448
|
host: envString("DAILY_FOCUS_HOST", "127.0.0.1", env),
|
|
454
449
|
freeWindows: envFlag("DAILY_FOCUS_FREE_WINDOWS", profile !== "personal", env),
|