daily-focus 1.1.1-dev.2 → 1.2.0-dev.4
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 -5
- package/SETUP.md +73 -15
- package/dist/{audit-W3O6CLOF.js → audit-ITRXIX6T.js} +7 -2
- package/dist/{chunk-UM2EPNWR.js → chunk-26JG3D5C.js} +1 -1
- package/dist/chunk-AIQO2YAI.js +107 -0
- package/dist/{chunk-OPASVYLR.js → chunk-RNE27FQY.js} +3 -1
- package/dist/{chunk-GFCWPYEI.js → chunk-SX5FPS3A.js} +153 -6
- package/dist/{chunk-Q5OKXJXT.js → chunk-UY6HQLGE.js} +1 -1
- package/dist/cli.js +6 -6
- package/dist/{demo-OQLT3QWP.js → demo-KVFAVGO7.js} +3 -3
- package/dist/{init-DEVCAQZZ.js → init-57WHZZNG.js} +1 -1
- package/dist/{seed-SMKNFYTC.js → seed-VALN2TCQ.js} +3 -3
- package/dist/{server-5O3LQVEY.js → server-SOCS3SZJ.js} +86 -13
- package/dist/{service-6ZFOHPWN.js → service-NAAAV67B.js} +1 -1
- package/package.json +1 -1
- package/public/app.js +96 -20
- package/public/style.css +6 -0
package/README.md
CHANGED
|
@@ -131,11 +131,13 @@ notes live too; the reply streams in there, and you can follow up in the same
|
|
|
131
131
|
conversation. The panel stays on the item it was opened for while you move through
|
|
132
132
|
the list.
|
|
133
133
|
|
|
134
|
-
It
|
|
135
|
-
the thread, so it is waiting there when you open the mail; a reply to a
|
|
136
|
-
handed back as text for you to post. It never
|
|
137
|
-
has no checkout: fixing conflicts or a build is work for a real
|
|
138
|
-
this is for the questions you would otherwise answer by opening
|
|
134
|
+
It is told to read and draft, and nothing more. An email reply is created as a Gmail
|
|
135
|
+
draft on the thread, so it is waiting there when you open the mail; a reply to a
|
|
136
|
+
reviewer is handed back as text for you to post. It is told never to send, post, or
|
|
137
|
+
edit a file, and it has no checkout: fixing conflicts or a build is work for a real
|
|
138
|
+
coding session, and this is for the questions you would otherwise answer by opening
|
|
139
|
+
six tabs. How much of that the CLI itself enforces differs between Claude Code and
|
|
140
|
+
Codex; see [What the assistant can do](SETUP.md#what-the-assistant-can-do).
|
|
139
141
|
|
|
140
142
|
## The morning agent
|
|
141
143
|
|
package/SETUP.md
CHANGED
|
@@ -98,7 +98,7 @@ under **Morning agent** in the settings. In `.env` or the environment that is:
|
|
|
98
98
|
DAILY_FOCUS_AGENT=codex
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
Each scheduled morning at 07:00
|
|
101
|
+
Each scheduled morning at 07:00 your time the dashboard starts the agent through
|
|
102
102
|
that CLI, headless, in the store, and the brief appears when it finishes. Set
|
|
103
103
|
`DAILY_FOCUS_AGENT_AT` to another time, such as `06:30`. The dashboard has to be
|
|
104
104
|
running at the time; if it wasn't, it runs the agent as soon as it is started that
|
|
@@ -110,6 +110,15 @@ otherwise; see [Match your working week](#match-your-working-week).
|
|
|
110
110
|
refresh icon still does. A new store's first brief always comes from the setup
|
|
111
111
|
steps' button, so the clock never runs the agent before you've told it where to look.
|
|
112
112
|
|
|
113
|
+
"Your time" is the time zone of the browser you last looked at the dashboard
|
|
114
|
+
in. The page tells the dashboard when it loads, reconnects or comes back into view,
|
|
115
|
+
and when the browser's own zone changes, so a dashboard on a Mac at home follows
|
|
116
|
+
you abroad as soon as you open it there. The dashboard also follows the Mac it runs
|
|
117
|
+
on when that Mac changes time zones, which covers the morning after a flight before
|
|
118
|
+
you have opened it. Whichever changed last wins, and the dashboard remembers it
|
|
119
|
+
across restarts. Every date and time on the page is shown in that zone; a browser
|
|
120
|
+
somewhere else says so under the page title.
|
|
121
|
+
|
|
113
122
|
Whichever way it starts, every run lands in the same place: the report it gives when
|
|
114
123
|
it finishes, and a chat where you can ask it why it did what it did. See
|
|
115
124
|
[The morning agent's runs](#the-morning-agents-runs).
|
|
@@ -117,14 +126,23 @@ it finishes, and a chat where you can ask it why it did what it did. See
|
|
|
117
126
|
The agent reaches your sources the way the CLI does:
|
|
118
127
|
|
|
119
128
|
- **Codex** runs in its workspace-write sandbox with network access, confined to the
|
|
120
|
-
store. Connectors from the Codex app are found through its plugin catalogue
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
store. Connectors from the Codex app are found through its plugin catalogue, and
|
|
130
|
+
every write through one is refused: a connector tool counts as a write unless it
|
|
131
|
+
is marked read-only. A command that needs to leave the sandbox, such as `gh`
|
|
132
|
+
switching accounts, is decided by your own Codex approval settings, as it would be
|
|
133
|
+
in your Codex. With nobody set to approve, the agent reports that restriction; with
|
|
134
|
+
Codex's automatic reviewer, the reviewer decides. The dashboard won't start a run
|
|
135
|
+
on a Codex older than 0.144.0, which would ignore the refusal of connector writes;
|
|
136
|
+
update Codex if a run fails saying so.
|
|
123
137
|
- **Claude Code** may write `items.json` and nothing else in the store, and reach
|
|
124
138
|
its sources with only what `DAILY_FOCUS_AGENT_TOOLS` allows. The default covers
|
|
125
139
|
GitHub, Jira, web pages and the claude.ai Gmail connector. Add your calendar, chat
|
|
126
140
|
and document connectors to the list by name, or the brief will report them as
|
|
127
|
-
unreachable.
|
|
141
|
+
unreachable. The default allows every Gmail tool, sending included, and `gh` and
|
|
142
|
+
`acli` can write as well as read; the agent's instructions forbid changing
|
|
143
|
+
anything. To narrow Gmail to reading, name its tools instead of the connector, as
|
|
144
|
+
[What the assistant can do](#what-the-assistant-can-do) shows, without the two
|
|
145
|
+
draft tools.
|
|
128
146
|
|
|
129
147
|
Set `DAILY_FOCUS_AGENT_MODEL` and `DAILY_FOCUS_AGENT_EFFORT` if the CLI's own
|
|
130
148
|
defaults are not what you want for a brief; a brief is worth a high effort. If the
|
|
@@ -268,14 +286,53 @@ the CLI is configured with; `DAILY_FOCUS_ASSISTANT_MODEL` and
|
|
|
268
286
|
`DAILY_FOCUS_ASSISTANT_EFFORT` override them. If the server can't find the CLI,
|
|
269
287
|
set `DAILY_FOCUS_ASSISTANT_BIN` to its path.
|
|
270
288
|
|
|
271
|
-
The assistant
|
|
272
|
-
no checkout to work in. That is deliberate. Fixing merge conflicts or a
|
|
273
|
-
belongs in a real coding session; this panel is for the questions you'd
|
|
274
|
-
answer by opening six tabs. The Gmail draft is the one thing it creates,
|
|
275
|
-
Gmail tool the CLI has: with Claude Code that
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
the assistant hands you the text
|
|
289
|
+
The assistant is told to read and draft, and never to send, post or edit anything,
|
|
290
|
+
and it has no checkout to work in. That is deliberate. Fixing merge conflicts or a
|
|
291
|
+
failing build belongs in a real coding session; this panel is for the questions you'd
|
|
292
|
+
otherwise answer by opening six tabs. The Gmail draft is the one thing it creates,
|
|
293
|
+
and revises when you ask, through the Gmail tool the CLI has: with Claude Code that
|
|
294
|
+
is the claude.ai Gmail connector, which `DAILY_FOCUS_ASSISTANT_TOOLS` allows by
|
|
295
|
+
default; with Codex it is the Gmail app linked to your ChatGPT account, which the CLI
|
|
296
|
+
finds through its plugin catalogue. Without either, the assistant hands you the text
|
|
297
|
+
instead and says so.
|
|
298
|
+
|
|
299
|
+
### What the assistant can do
|
|
300
|
+
|
|
301
|
+
The assistant runs the CLI you already use, with the access that CLI already has.
|
|
302
|
+
The dashboard narrows some of that, and leaves the rest to the assistant's
|
|
303
|
+
instructions. What the CLI itself enforces:
|
|
304
|
+
|
|
305
|
+
- **Claude Code** may use only the tools in `DAILY_FOCUS_ASSISTANT_TOOLS`, and never
|
|
306
|
+
its file-editing tools, whatever the list says; anything else is refused rather
|
|
307
|
+
than asked about. Rules in your own Claude Code settings that allow a tool still
|
|
308
|
+
apply. The default list is broader than the assistant needs:
|
|
309
|
+
`mcp__claude_ai_Gmail` allows every tool on the Gmail connector, sending and
|
|
310
|
+
deleting included, and `gh` and `acli` can comment, merge and move tickets as well
|
|
311
|
+
as read. Only the instructions keep it from doing those. To hold it to reading
|
|
312
|
+
mail and drafting replies, name the Gmail tools instead of the connector:
|
|
313
|
+
|
|
314
|
+
```dotenv
|
|
315
|
+
DAILY_FOCUS_ASSISTANT_TOOLS=Bash(gh *),Bash(acli *),WebFetch,mcp__claude_ai_Gmail__search_threads,mcp__claude_ai_Gmail__get_thread,mcp__claude_ai_Gmail__get_message,mcp__claude_ai_Gmail__list_labels,mcp__claude_ai_Gmail__list_drafts,mcp__claude_ai_Gmail__get_draft,mcp__claude_ai_Gmail__create_draft,mcp__claude_ai_Gmail__update_draft
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
A tool left off the list is refused, so if the connector renames one, the
|
|
319
|
+
assistant reports that it can't reach your mail rather than working around it.
|
|
320
|
+
- **Codex** refuses every write through a connector (Gmail, Calendar, Drive, GitHub
|
|
321
|
+
and the other apps your ChatGPT account links) except creating and revising a
|
|
322
|
+
Gmail draft. A connector tool counts as a write unless it is marked read-only. A
|
|
323
|
+
tool you have told Codex to always allow, or an app given approval settings of its
|
|
324
|
+
own in your Codex config, keeps them. Computer use, the plugins that act in your
|
|
325
|
+
apps and browser with your sign-ins, is off unless you turn on
|
|
326
|
+
`DAILY_FOCUS_ASSISTANT_COMPUTER_USE`; the dashboard checks with Codex that it is
|
|
327
|
+
off before each session, and refuses to start one it can't confirm. Shell
|
|
328
|
+
commands follow your own Codex config: the sandbox keeps writes in the
|
|
329
|
+
assistant's working directory, and a command that needs to leave it is decided
|
|
330
|
+
by your Codex approval settings, as it would be in your own Codex. That keeps
|
|
331
|
+
`gh` working, and means it can write. All of this needs Codex 0.149.0 or
|
|
332
|
+
newer, and the page warns when yours is older. Before 0.144.0 Codex silently
|
|
333
|
+
ignores the refusal of connector writes, so the dashboard won't start the
|
|
334
|
+
assistant or the morning agent on it at all; before 0.149.0 it can't switch
|
|
335
|
+
computer use off, so the assistant won't answer while that setting is off.
|
|
279
336
|
|
|
280
337
|
Its instructions live in `prompts/assistant.md`, linked into the store as
|
|
281
338
|
`assistant.md` each time the dashboard starts. What it says stays in the chat in the panel: it
|
|
@@ -484,13 +541,14 @@ matters when `DAILY_FOCUS_FREE_WINDOWS` is off.
|
|
|
484
541
|
| `DAILY_FOCUS_ASSISTANT_BIN` | *the CLI's name* | Path to the assistant's CLI, for when the server's PATH lacks it. `~` is expanded |
|
|
485
542
|
| `DAILY_FOCUS_ASSISTANT_MODEL` | *the CLI's own* | Model passed to the CLI untouched: an alias like `opus`, or a full id. Unset passes no flag |
|
|
486
543
|
| `DAILY_FOCUS_ASSISTANT_EFFORT` | *the CLI's own* | Effort passed to the CLI untouched: `low`, `medium`, `high`, `xhigh` or `max`. Unset passes no flag |
|
|
487
|
-
| `DAILY_FOCUS_ASSISTANT_TOOLS` | `Bash(gh *),Bash(acli *),WebFetch,mcp__claude_ai_Gmail` | What Claude Code may use without asking, in its permission syntax, comma-separated. Editing tools are denied regardless. Ignored by Codex |
|
|
544
|
+
| `DAILY_FOCUS_ASSISTANT_TOOLS` | `Bash(gh *),Bash(acli *),WebFetch,mcp__claude_ai_Gmail` | What Claude Code may use without asking, in its permission syntax, comma-separated. Editing tools are denied regardless. The default allows every Gmail tool, sending included; see [What the assistant can do](#what-the-assistant-can-do) to narrow it. Ignored by Codex |
|
|
545
|
+
| `DAILY_FOCUS_ASSISTANT_COMPUTER_USE` | `off` | `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. Ignored by Claude Code, which uses only the tools in its list |
|
|
488
546
|
| `DAILY_FOCUS_AGENT` | `off` | `codex` or `claude` lets the dashboard run the morning agent through that CLI: each scheduled morning, and from the refresh icon beside the brief's age. `off` hides it |
|
|
489
547
|
| `DAILY_FOCUS_AGENT_AT` | `07:00` | Local time, `HH:MM`, at which the dashboard starts the agent on each scheduled day, or as soon as it is running after that. `off` means it never starts one on its own; the refresh icon still does. Needs `DAILY_FOCUS_AGENT` |
|
|
490
548
|
| `DAILY_FOCUS_AGENT_BIN` | *the CLI's name* | Path to that CLI, for when the server's PATH lacks it. `~` is expanded |
|
|
491
549
|
| `DAILY_FOCUS_AGENT_MODEL` | *the CLI's own* | Model passed to the CLI untouched. Unset passes no flag |
|
|
492
550
|
| `DAILY_FOCUS_AGENT_EFFORT` | *the CLI's own* | Effort passed to the CLI untouched. Unset passes no flag |
|
|
493
|
-
| `DAILY_FOCUS_AGENT_TOOLS` | `Bash(gh *),Bash(acli *),WebFetch,mcp__claude_ai_Gmail` | What Claude Code may use to reach your sources, in its permission syntax, comma-separated. Writing `items.json`, and nothing else in the store, is allowed regardless. Ignored by Codex |
|
|
551
|
+
| `DAILY_FOCUS_AGENT_TOOLS` | `Bash(gh *),Bash(acli *),WebFetch,mcp__claude_ai_Gmail` | What Claude Code may use to reach your sources, in its permission syntax, comma-separated. Writing `items.json`, and nothing else in the store, is allowed regardless. The default allows every Gmail tool, sending included. Ignored by Codex |
|
|
494
552
|
|
|
495
553
|
## If something looks wrong
|
|
496
554
|
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readSystemZone,
|
|
3
|
+
startingZone
|
|
4
|
+
} from "./chunk-AIQO2YAI.js";
|
|
1
5
|
import {
|
|
2
6
|
linkState
|
|
3
7
|
} from "./chunk-QKT2FAB5.js";
|
|
@@ -8,7 +12,7 @@ import {
|
|
|
8
12
|
readArchiveIndex,
|
|
9
13
|
resolveItems,
|
|
10
14
|
runContractChecks
|
|
11
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-26JG3D5C.js";
|
|
12
16
|
import {
|
|
13
17
|
BRIEF_REFRESH_GRACE_HOURS,
|
|
14
18
|
describeSchedule,
|
|
@@ -18,7 +22,7 @@ import {
|
|
|
18
22
|
resolveSchedule,
|
|
19
23
|
runsOn,
|
|
20
24
|
workingMsBetween
|
|
21
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-RNE27FQY.js";
|
|
22
26
|
import {
|
|
23
27
|
command
|
|
24
28
|
} from "./chunk-BC2W7NND.js";
|
|
@@ -27,6 +31,7 @@ import {
|
|
|
27
31
|
import { readFile, readdir } from "node:fs/promises";
|
|
28
32
|
import { resolve } from "node:path";
|
|
29
33
|
var config = loadConfig();
|
|
34
|
+
process.env.TZ = (await startingZone(config.timeZoneFile, await readSystemZone())).timeZone;
|
|
30
35
|
var store = new Store(config);
|
|
31
36
|
var failures = 0;
|
|
32
37
|
var warnings = 0;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
writeJsonAtomic
|
|
3
|
+
} from "./chunk-26JG3D5C.js";
|
|
4
|
+
import {
|
|
5
|
+
localTimeZone
|
|
6
|
+
} from "./chunk-RNE27FQY.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
|
+
};
|
|
@@ -353,7 +353,8 @@ function envAssistant(env) {
|
|
|
353
353
|
binPath: expandHome((env.DAILY_FOCUS_ASSISTANT_BIN ?? "").trim() || (agent ?? "claude")),
|
|
354
354
|
model: envString("DAILY_FOCUS_ASSISTANT_MODEL", "", env) || null,
|
|
355
355
|
effort: envString("DAILY_FOCUS_ASSISTANT_EFFORT", "", env) || null,
|
|
356
|
-
tools: tools.length > 0 ? tools : DEFAULT_ASSISTANT_TOOLS
|
|
356
|
+
tools: tools.length > 0 ? tools : DEFAULT_ASSISTANT_TOOLS,
|
|
357
|
+
computerUse: envFlag("DAILY_FOCUS_ASSISTANT_COMPUTER_USE", false, env)
|
|
357
358
|
};
|
|
358
359
|
}
|
|
359
360
|
var DEFAULT_ASSISTANT_TOOLS = ["Bash(gh *)", "Bash(acli *)", "WebFetch", "mcp__claude_ai_Gmail"];
|
|
@@ -444,6 +445,7 @@ function loadConfig(env = loadEnv()) {
|
|
|
444
445
|
pullsFile: resolve2(dataDir, "prs.json"),
|
|
445
446
|
calendarFile: resolve2(dataDir, "calendar.json"),
|
|
446
447
|
ticketsFile: resolve2(dataDir, "tickets.json"),
|
|
448
|
+
timeZoneFile: resolve2(dataDir, "timezone.json"),
|
|
447
449
|
// The same 1 to 120 the session endpoint holds a start to.
|
|
448
450
|
sessionMinutes: envBounded("DAILY_FOCUS_SESSION_MINUTES", 25, 1, 120, env),
|
|
449
451
|
awayAfterMinutes: envBounded("DAILY_FOCUS_AWAY_AFTER", profile === "personal" ? 0 : 10, 0, 24 * 60, env),
|
|
@@ -1,3 +1,105 @@
|
|
|
1
|
+
// src/codex.ts
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
var GMAIL_CONNECTOR = "connector_2128aebfecb84f64a069897515042a44";
|
|
5
|
+
var CONNECTOR_POLICY = [
|
|
6
|
+
'apps._default.default_tools_approval_mode="writes"',
|
|
7
|
+
'apps._default.approvals_reviewer="user"'
|
|
8
|
+
];
|
|
9
|
+
var GMAIL_DRAFTS = ["create_draft", "update_draft"].map(
|
|
10
|
+
(tool) => `apps.${GMAIL_CONNECTOR}.tools.${tool}.approval_mode="approve"`
|
|
11
|
+
);
|
|
12
|
+
var COMPUTER_USE_PLUGINS = [
|
|
13
|
+
"unified-computer-use@openai-bundled",
|
|
14
|
+
"computer-use@openai-bundled",
|
|
15
|
+
"browser@openai-bundled",
|
|
16
|
+
"chrome@openai-bundled"
|
|
17
|
+
];
|
|
18
|
+
var COMPUTER_USE_SERVERS = ["node_repl", "cua_repl", "computer-use"];
|
|
19
|
+
var LIST_TIMEOUT_MS = 3e4;
|
|
20
|
+
var exec = promisify(execFile);
|
|
21
|
+
var defaultExec = async (file, args, { timeout }) => {
|
|
22
|
+
try {
|
|
23
|
+
const { stdout } = await exec(file, args, { timeout, maxBuffer: 16 * 1024 * 1024 });
|
|
24
|
+
return stdout;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
const stderr = String(err.stderr ?? "").trim();
|
|
27
|
+
if (!stderr) throw err;
|
|
28
|
+
throw new Error(stderr.split("\n").slice(0, 2).join(" "));
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
function configArgs(overrides) {
|
|
32
|
+
return overrides.flatMap((override) => ["-c", override]);
|
|
33
|
+
}
|
|
34
|
+
async function enabledServers(bin, overrides, run) {
|
|
35
|
+
const output = await run(bin, ["mcp", "list", "--json", ...configArgs(overrides)], { timeout: LIST_TIMEOUT_MS });
|
|
36
|
+
let servers;
|
|
37
|
+
try {
|
|
38
|
+
servers = JSON.parse(output);
|
|
39
|
+
} catch {
|
|
40
|
+
throw new Error("codex mcp list --json printed something other than JSON");
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(servers)) throw new Error("codex mcp list --json printed something other than a list");
|
|
43
|
+
return servers.filter((server) => typeof server?.name === "string").filter((server) => server.enabled !== false).map((server) => server.name);
|
|
44
|
+
}
|
|
45
|
+
async function computerUseOff(bin, run = defaultExec) {
|
|
46
|
+
const overrides = COMPUTER_USE_PLUGINS.map((plugin) => `plugins.${plugin}.enabled=false`);
|
|
47
|
+
const left = (await enabledServers(bin, overrides, run)).filter((name) => COMPUTER_USE_SERVERS.includes(name));
|
|
48
|
+
if (left.length === 0) return overrides;
|
|
49
|
+
overrides.push(...left.map((name) => `mcp_servers.${name}.enabled=false`));
|
|
50
|
+
const still = (await enabledServers(bin, overrides, run)).filter((name) => COMPUTER_USE_SERVERS.includes(name));
|
|
51
|
+
if (still.length > 0) throw new Error(`Codex still starts ${still.join(", ")}`);
|
|
52
|
+
return overrides;
|
|
53
|
+
}
|
|
54
|
+
var CODEX_CONNECTOR_POLICY = "0.144.0";
|
|
55
|
+
var CODEX_MINIMUM = "0.149.0";
|
|
56
|
+
function versionParts(text) {
|
|
57
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(text);
|
|
58
|
+
return match ? match.slice(1, 4).map(Number) : null;
|
|
59
|
+
}
|
|
60
|
+
function olderThan(version, than) {
|
|
61
|
+
const a = versionParts(version);
|
|
62
|
+
const b = versionParts(than);
|
|
63
|
+
if (!a || !b) return false;
|
|
64
|
+
for (let i = 0; i < 3; i++) {
|
|
65
|
+
if (a[i] !== b[i]) return a[i] < b[i];
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
async function codexVersion(bin, run = defaultExec) {
|
|
70
|
+
try {
|
|
71
|
+
return versionParts(await run(bin, ["--version"], { timeout: LIST_TIMEOUT_MS }))?.join(".") ?? null;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function refusalFor(version) {
|
|
77
|
+
if (!version || !olderThan(version, CODEX_CONNECTOR_POLICY)) return null;
|
|
78
|
+
return `Codex ${version} is older than ${CODEX_CONNECTOR_POLICY}, which ignores the dashboard's refusal of connector writes without saying so, so the dashboard won't run it. Update Codex`;
|
|
79
|
+
}
|
|
80
|
+
var CodexGate = class {
|
|
81
|
+
#bin;
|
|
82
|
+
#run;
|
|
83
|
+
#cleared = false;
|
|
84
|
+
constructor(bin, run = defaultExec) {
|
|
85
|
+
this.#bin = bin;
|
|
86
|
+
this.#run = run;
|
|
87
|
+
}
|
|
88
|
+
async refusal() {
|
|
89
|
+
if (this.#cleared) return null;
|
|
90
|
+
const version = await codexVersion(this.#bin, this.#run);
|
|
91
|
+
const refusal = refusalFor(version);
|
|
92
|
+
if (version && !refusal) this.#cleared = true;
|
|
93
|
+
return refusal;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
async function codexVersionWarning(bin, run = defaultExec) {
|
|
97
|
+
const version = await codexVersion(bin, run);
|
|
98
|
+
if (!version || !olderThan(version, CODEX_MINIMUM)) return null;
|
|
99
|
+
const consequence = olderThan(version, CODEX_CONNECTOR_POLICY) ? `Before ${CODEX_CONNECTOR_POLICY} Codex ignores the dashboard's refusal of connector writes without saying so, so the dashboard won't start the agent or the assistant on it.` : `Before ${CODEX_MINIMUM} Codex can't switch computer use off, so the assistant won't answer while computer use is off in its settings.`;
|
|
100
|
+
return `Codex ${version} is older than ${CODEX_MINIMUM}, the oldest the dashboard's limits on Codex work with. ${consequence} Update Codex.`;
|
|
101
|
+
}
|
|
102
|
+
|
|
1
103
|
// src/assistant.ts
|
|
2
104
|
import { spawn } from "node:child_process";
|
|
3
105
|
import { randomUUID } from "node:crypto";
|
|
@@ -166,13 +268,20 @@ var claudeAdapter = {
|
|
|
166
268
|
}
|
|
167
269
|
};
|
|
168
270
|
var codexAdapter = {
|
|
169
|
-
args({ prompt, sessionId, model, effort }) {
|
|
271
|
+
args({ prompt, sessionId, model, effort, drafts, codexConfig }) {
|
|
170
272
|
const args = ["exec"];
|
|
171
273
|
if (sessionId) args.push("resume");
|
|
172
274
|
args.push("--json", "--skip-git-repo-check");
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
275
|
+
const config = [
|
|
276
|
+
'sandbox_mode="workspace-write"',
|
|
277
|
+
"sandbox_workspace_write.network_access=true",
|
|
278
|
+
...CONNECTOR_POLICY,
|
|
279
|
+
...drafts ? GMAIL_DRAFTS : [],
|
|
280
|
+
...codexConfig
|
|
281
|
+
];
|
|
282
|
+
if (model) config.push(`model=${JSON.stringify(model)}`);
|
|
283
|
+
if (effort) config.push(`model_reasoning_effort=${JSON.stringify(effort)}`);
|
|
284
|
+
args.push(...configArgs(config));
|
|
176
285
|
if (sessionId) args.push(sessionId);
|
|
177
286
|
args.push(prompt);
|
|
178
287
|
return args;
|
|
@@ -219,10 +328,17 @@ var Assistant = class _Assistant {
|
|
|
219
328
|
config;
|
|
220
329
|
hooks;
|
|
221
330
|
spawnFn;
|
|
222
|
-
|
|
331
|
+
execFn;
|
|
332
|
+
/** Refuses a Codex too old to hold to the connector rules; see `codex.ts`. */
|
|
333
|
+
codexGate;
|
|
334
|
+
/** What switches computer use off, once Codex has said; see `computerUseConfig`. */
|
|
335
|
+
codexOff = null;
|
|
336
|
+
constructor(config, hooks, spawnFn = spawn, execFn) {
|
|
223
337
|
this.config = config;
|
|
224
338
|
this.hooks = hooks;
|
|
225
339
|
this.spawnFn = spawnFn;
|
|
340
|
+
this.execFn = execFn;
|
|
341
|
+
this.codexGate = new CodexGate(config.assistant.binPath, execFn);
|
|
226
342
|
}
|
|
227
343
|
get enabled() {
|
|
228
344
|
return this.config.assistant.agent !== null;
|
|
@@ -329,6 +445,19 @@ ${typed}` : action.request : typed;
|
|
|
329
445
|
});
|
|
330
446
|
const prompt = previous ? resolved.request : composePrompt(await this.instructions(), context, resolved.request, this.config.profile);
|
|
331
447
|
await mkdir(this.config.assistantDir, { recursive: true });
|
|
448
|
+
const refusal = agent === "codex" ? await this.codexGate.refusal() : null;
|
|
449
|
+
if (refusal) {
|
|
450
|
+
await this.finish(turn, { ok: false, error: refusal });
|
|
451
|
+
return turn;
|
|
452
|
+
}
|
|
453
|
+
let codexConfig;
|
|
454
|
+
try {
|
|
455
|
+
codexConfig = await this.computerUseConfig();
|
|
456
|
+
} catch (err) {
|
|
457
|
+
const error = err.code === "ENOENT" ? this.spawnError(err) : `computer use is off in the settings, and Codex could not confirm it: ${err.message}. Update Codex, or turn computer use on for the assistant`;
|
|
458
|
+
await this.finish(turn, { ok: false, error });
|
|
459
|
+
return turn;
|
|
460
|
+
}
|
|
332
461
|
const adapter = ADAPTERS[agent];
|
|
333
462
|
const args = adapter.args({
|
|
334
463
|
prompt,
|
|
@@ -336,7 +465,9 @@ ${typed}` : action.request : typed;
|
|
|
336
465
|
model: this.config.assistant.model,
|
|
337
466
|
effort: this.config.assistant.effort,
|
|
338
467
|
tools: this.config.assistant.tools,
|
|
339
|
-
denyEdits: true
|
|
468
|
+
denyEdits: true,
|
|
469
|
+
drafts: true,
|
|
470
|
+
codexConfig
|
|
340
471
|
});
|
|
341
472
|
let child;
|
|
342
473
|
try {
|
|
@@ -421,6 +552,20 @@ ${event.text}` : event.text;
|
|
|
421
552
|
}
|
|
422
553
|
this.hooks.onChange();
|
|
423
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* What switches computer use off, when the assistant runs Codex and the user
|
|
557
|
+
* hasn't turned it on. Asked of Codex once and kept for the life of the
|
|
558
|
+
* process; settings that change it restart the dashboard anyway. A failure
|
|
559
|
+
* isn't kept, so the next turn asks again.
|
|
560
|
+
*/
|
|
561
|
+
computerUseConfig() {
|
|
562
|
+
if (this.config.assistant.agent !== "codex" || this.config.assistant.computerUse) return Promise.resolve([]);
|
|
563
|
+
this.codexOff ??= computerUseOff(this.config.assistant.binPath, this.execFn).catch((err) => {
|
|
564
|
+
this.codexOff = null;
|
|
565
|
+
throw err;
|
|
566
|
+
});
|
|
567
|
+
return this.codexOff;
|
|
568
|
+
}
|
|
424
569
|
spawnError(err) {
|
|
425
570
|
const code = err.code;
|
|
426
571
|
const bin = this.config.assistant.binPath;
|
|
@@ -450,6 +595,8 @@ ${event.text}` : event.text;
|
|
|
450
595
|
};
|
|
451
596
|
|
|
452
597
|
export {
|
|
598
|
+
CodexGate,
|
|
599
|
+
codexVersionWarning,
|
|
453
600
|
QUICK_ACTIONS,
|
|
454
601
|
ADAPTERS,
|
|
455
602
|
Assistant
|
package/dist/cli.js
CHANGED
|
@@ -49,7 +49,7 @@ 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-KVFAVGO7.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 }));
|
|
@@ -65,7 +65,7 @@ async function demo(open, profile) {
|
|
|
65
65
|
);
|
|
66
66
|
}
|
|
67
67
|
async function start(open, onStarted, options = {}) {
|
|
68
|
-
const { runServer } = await import("./server-
|
|
68
|
+
const { runServer } = await import("./server-SOCS3SZJ.js");
|
|
69
69
|
await runServer({
|
|
70
70
|
...options,
|
|
71
71
|
onStarted: (url) => {
|
|
@@ -162,20 +162,20 @@ async function main(argv) {
|
|
|
162
162
|
}
|
|
163
163
|
return start(open);
|
|
164
164
|
case "service": {
|
|
165
|
-
const { main: service } = await import("./service-
|
|
165
|
+
const { main: service } = await import("./service-NAAAV67B.js");
|
|
166
166
|
process.exitCode = await service(values.remove ? ["--remove"] : []);
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
169
169
|
// These three are scripts that do their work as they load, and read their
|
|
170
170
|
// own flags from the command line.
|
|
171
171
|
case "init":
|
|
172
|
-
await import("./init-
|
|
172
|
+
await import("./init-57WHZZNG.js");
|
|
173
173
|
return;
|
|
174
174
|
case "audit":
|
|
175
|
-
await import("./audit-
|
|
175
|
+
await import("./audit-ITRXIX6T.js");
|
|
176
176
|
return;
|
|
177
177
|
case "seed":
|
|
178
|
-
await import("./seed-
|
|
178
|
+
await import("./seed-VALN2TCQ.js");
|
|
179
179
|
return;
|
|
180
180
|
case "build-calendar":
|
|
181
181
|
return buildCalendar();
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
QUICK_ACTIONS
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-SX5FPS3A.js";
|
|
4
4
|
import {
|
|
5
5
|
dateFromNow,
|
|
6
6
|
sampleBrief,
|
|
7
7
|
sampleGeneratedAt
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-UY6HQLGE.js";
|
|
9
9
|
import {
|
|
10
10
|
PROFILES,
|
|
11
11
|
localDateKey
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-RNE27FQY.js";
|
|
13
13
|
import "./chunk-BC2W7NND.js";
|
|
14
14
|
|
|
15
15
|
// src/demo.ts
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
sampleBrief
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-UY6HQLGE.js";
|
|
4
4
|
import {
|
|
5
5
|
Store
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-26JG3D5C.js";
|
|
7
7
|
import {
|
|
8
8
|
loadConfig
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-RNE27FQY.js";
|
|
10
10
|
import {
|
|
11
11
|
command
|
|
12
12
|
} from "./chunk-BC2W7NND.js";
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ADAPTERS,
|
|
3
|
-
Assistant
|
|
4
|
-
|
|
3
|
+
Assistant,
|
|
4
|
+
CodexGate,
|
|
5
|
+
codexVersionWarning
|
|
6
|
+
} from "./chunk-SX5FPS3A.js";
|
|
5
7
|
import {
|
|
6
8
|
FOCUS_TEMPLATE,
|
|
7
9
|
sourcesTemplate
|
|
8
10
|
} from "./chunk-V2UN3JYL.js";
|
|
11
|
+
import {
|
|
12
|
+
TimeZoneKeeper,
|
|
13
|
+
validZone
|
|
14
|
+
} from "./chunk-AIQO2YAI.js";
|
|
9
15
|
import {
|
|
10
16
|
describeLink,
|
|
11
17
|
linkStore,
|
|
@@ -20,7 +26,7 @@ import {
|
|
|
20
26
|
stopSession,
|
|
21
27
|
tempPathFor,
|
|
22
28
|
writeJsonAtomic
|
|
23
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-26JG3D5C.js";
|
|
24
30
|
import {
|
|
25
31
|
CLI_NAMES,
|
|
26
32
|
LAUNCH_SETTINGS,
|
|
@@ -36,7 +42,7 @@ import {
|
|
|
36
42
|
readDotEnv,
|
|
37
43
|
resolveSchedule,
|
|
38
44
|
scheduledRunDue
|
|
39
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-RNE27FQY.js";
|
|
40
46
|
import {
|
|
41
47
|
PACKAGED,
|
|
42
48
|
command,
|
|
@@ -1488,7 +1494,8 @@ async function runHelper(appPath, selfAddresses) {
|
|
|
1488
1494
|
const out = join(tmpdir(), `daily-focus-calendar-${process.pid}-${Date.now()}.json`);
|
|
1489
1495
|
try {
|
|
1490
1496
|
try {
|
|
1491
|
-
|
|
1497
|
+
const zone = process.env.TZ ? ["--env", `TZ=${process.env.TZ}`] : [];
|
|
1498
|
+
await run2("/usr/bin/open", ["-n", "-a", appPath, ...zone, "--args", out, selfAddresses.join(",")], {
|
|
1492
1499
|
timeout: HELPER_TIMEOUT_MS
|
|
1493
1500
|
});
|
|
1494
1501
|
} catch (err) {
|
|
@@ -2501,10 +2508,13 @@ var AgentRunner = class {
|
|
|
2501
2508
|
config;
|
|
2502
2509
|
hooks;
|
|
2503
2510
|
spawnFn;
|
|
2504
|
-
|
|
2511
|
+
/** Refuses a Codex too old to hold to the connector rules; see `codex.ts`. */
|
|
2512
|
+
codexGate;
|
|
2513
|
+
constructor(config, hooks, spawnFn = spawn, execFn) {
|
|
2505
2514
|
this.config = config;
|
|
2506
2515
|
this.hooks = hooks;
|
|
2507
2516
|
this.spawnFn = spawnFn;
|
|
2517
|
+
this.codexGate = new CodexGate(config.agent.binPath, execFn);
|
|
2508
2518
|
}
|
|
2509
2519
|
get enabled() {
|
|
2510
2520
|
return this.config.agent.cli !== null;
|
|
@@ -2627,7 +2637,9 @@ var AgentRunner = class {
|
|
|
2627
2637
|
model: this.config.agent.model,
|
|
2628
2638
|
effort: this.config.agent.effort,
|
|
2629
2639
|
tools: [...briefWritingTools(), ...this.config.agent.tools],
|
|
2630
|
-
denyEdits: false
|
|
2640
|
+
denyEdits: false,
|
|
2641
|
+
drafts: false,
|
|
2642
|
+
codexConfig: []
|
|
2631
2643
|
});
|
|
2632
2644
|
await this.spawnInto(run6, null, args, RUN_TIMEOUT_MS, "gave up after an hour");
|
|
2633
2645
|
return run6;
|
|
@@ -2673,7 +2685,9 @@ var AgentRunner = class {
|
|
|
2673
2685
|
effort: this.config.agent.effort,
|
|
2674
2686
|
// The sources, and not the brief: a chat is given nothing to write.
|
|
2675
2687
|
tools: this.config.agent.tools,
|
|
2676
|
-
denyEdits: true
|
|
2688
|
+
denyEdits: true,
|
|
2689
|
+
drafts: false,
|
|
2690
|
+
codexConfig: []
|
|
2677
2691
|
});
|
|
2678
2692
|
await this.spawnInto(run6, turn, args, TURN_TIMEOUT_MS, "gave up after ten minutes");
|
|
2679
2693
|
return turn;
|
|
@@ -2687,6 +2701,11 @@ var AgentRunner = class {
|
|
|
2687
2701
|
const cli = run6.cli;
|
|
2688
2702
|
const adapter = ADAPTERS[cli];
|
|
2689
2703
|
await mkdir(this.config.dataDir, { recursive: true });
|
|
2704
|
+
const refusal2 = cli === "codex" ? await this.codexGate.refusal() : null;
|
|
2705
|
+
if (refusal2) {
|
|
2706
|
+
await this.finish(run6, turn, { ok: false, error: refusal2 });
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2690
2709
|
let child;
|
|
2691
2710
|
try {
|
|
2692
2711
|
child = this.spawnFn(this.config.agent.binPath, args, {
|
|
@@ -3010,7 +3029,7 @@ var SETTINGS = [
|
|
|
3010
3029
|
key: "DAILY_FOCUS_AGENT_TOOLS",
|
|
3011
3030
|
group: "agent",
|
|
3012
3031
|
label: "Tools Claude Code may use",
|
|
3013
|
-
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.",
|
|
3032
|
+
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.",
|
|
3014
3033
|
kind: "list",
|
|
3015
3034
|
fallback: always("Bash(gh *), Bash(acli *), WebFetch, mcp__claude_ai_Gmail"),
|
|
3016
3035
|
advanced: true
|
|
@@ -3037,7 +3056,7 @@ var SETTINGS = [
|
|
|
3037
3056
|
key: "DAILY_FOCUS_ASSISTANT",
|
|
3038
3057
|
group: "assistant",
|
|
3039
3058
|
label: "Answer questions with",
|
|
3040
|
-
help: "Puts an assistant in every item's panel
|
|
3059
|
+
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.",
|
|
3041
3060
|
kind: "choice",
|
|
3042
3061
|
choices: OFF_OR_CLI,
|
|
3043
3062
|
fallback: always("Off")
|
|
@@ -3063,11 +3082,19 @@ var SETTINGS = [
|
|
|
3063
3082
|
key: "DAILY_FOCUS_ASSISTANT_TOOLS",
|
|
3064
3083
|
group: "assistant",
|
|
3065
3084
|
label: "Tools Claude Code may use",
|
|
3066
|
-
help: "In Claude Code's permission syntax, comma-separated. Editing tools are denied whatever this says. Ignored by Codex.",
|
|
3085
|
+
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.",
|
|
3067
3086
|
kind: "list",
|
|
3068
3087
|
fallback: always("Bash(gh *), Bash(acli *), WebFetch, mcp__claude_ai_Gmail"),
|
|
3069
3088
|
advanced: true
|
|
3070
3089
|
},
|
|
3090
|
+
{
|
|
3091
|
+
key: "DAILY_FOCUS_ASSISTANT_COMPUTER_USE",
|
|
3092
|
+
group: "assistant",
|
|
3093
|
+
label: "Computer use",
|
|
3094
|
+
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.",
|
|
3095
|
+
kind: "flag",
|
|
3096
|
+
fallback: always("Off")
|
|
3097
|
+
},
|
|
3071
3098
|
{
|
|
3072
3099
|
key: "DAILY_FOCUS_ASSISTANT_BIN",
|
|
3073
3100
|
group: "assistant",
|
|
@@ -3686,6 +3713,8 @@ async function startServer(env, options = {}) {
|
|
|
3686
3713
|
const store = new Store(config);
|
|
3687
3714
|
await store.ensureDataDir();
|
|
3688
3715
|
const storeId = await store.fingerprint();
|
|
3716
|
+
const zone = new TimeZoneKeeper(config.timeZoneFile, options.readSystemZone);
|
|
3717
|
+
await zone.start();
|
|
3689
3718
|
for (const outcome of await linkStore(config)) {
|
|
3690
3719
|
if (outcome.result === "linked") console.log(`[daily-focus] linked ${describeLink(config, outcome)}`);
|
|
3691
3720
|
if (outcome.result === "failed") console.warn(`[daily-focus] could not link ${outcome.name}: ${outcome.detail}`);
|
|
@@ -3715,6 +3744,15 @@ async function startServer(env, options = {}) {
|
|
|
3715
3744
|
restartWhenIdle();
|
|
3716
3745
|
}
|
|
3717
3746
|
});
|
|
3747
|
+
let codexWarnings = [];
|
|
3748
|
+
const codexBins = /* @__PURE__ */ new Set();
|
|
3749
|
+
if (config.agent.cli === "codex") codexBins.add(config.agent.binPath);
|
|
3750
|
+
if (config.assistant.agent === "codex") codexBins.add(config.assistant.binPath);
|
|
3751
|
+
void Promise.all([...codexBins].map((bin) => codexVersionWarning(bin))).then((found) => {
|
|
3752
|
+
codexWarnings = [...new Set(found.filter((warning) => warning !== null))];
|
|
3753
|
+
for (const warning of codexWarnings) console.warn(`[daily-focus] ${warning}`);
|
|
3754
|
+
if (codexWarnings.length > 0) void broadcast();
|
|
3755
|
+
});
|
|
3718
3756
|
function requestRestart() {
|
|
3719
3757
|
restart = options.onRestart ? "waiting" : "manual";
|
|
3720
3758
|
restartWhenIdle();
|
|
@@ -3742,7 +3780,7 @@ async function startServer(env, options = {}) {
|
|
|
3742
3780
|
tickets: tickets.view(actions, now),
|
|
3743
3781
|
assistant: assistant.view(),
|
|
3744
3782
|
agentRun: agent.view(now),
|
|
3745
|
-
setupWarnings: [...configWarnings, ...linkWarning ? [linkWarning] : []],
|
|
3783
|
+
setupWarnings: [...configWarnings, ...codexWarnings, ...linkWarning ? [linkWarning] : []],
|
|
3746
3784
|
restart,
|
|
3747
3785
|
setup,
|
|
3748
3786
|
assetVersion,
|
|
@@ -3781,6 +3819,8 @@ data: ${payload}
|
|
|
3781
3819
|
basename(tempPathFor(config.pullsFile)),
|
|
3782
3820
|
basename(config.ticketsFile),
|
|
3783
3821
|
basename(tempPathFor(config.ticketsFile)),
|
|
3822
|
+
basename(config.timeZoneFile),
|
|
3823
|
+
basename(tempPathFor(config.timeZoneFile)),
|
|
3784
3824
|
// Appended by this process, which broadcasts on its own.
|
|
3785
3825
|
basename(config.assistantLogFile),
|
|
3786
3826
|
basename(config.agentLogFile)
|
|
@@ -3822,10 +3862,23 @@ data: ${payload}
|
|
|
3822
3862
|
await checkPresence();
|
|
3823
3863
|
await assistant.start();
|
|
3824
3864
|
await agent.start();
|
|
3825
|
-
|
|
3865
|
+
let ticks = Promise.resolve();
|
|
3866
|
+
function clockTick() {
|
|
3867
|
+
ticks = ticks.then(tickOnce).catch((err) => {
|
|
3868
|
+
console.error(`[daily-focus] the morning clock failed: ${err.message}`);
|
|
3869
|
+
});
|
|
3870
|
+
return ticks;
|
|
3871
|
+
}
|
|
3872
|
+
async function tickOnce() {
|
|
3873
|
+
if (await zone.checkSystem()) zoneMoved();
|
|
3826
3874
|
if (restart === "waiting" || await firstBriefPending(config)) return;
|
|
3827
3875
|
await agent.tick();
|
|
3828
3876
|
}
|
|
3877
|
+
function zoneMoved() {
|
|
3878
|
+
console.log(`[daily-focus] time zone is now ${zone.current}`);
|
|
3879
|
+
void calendar.refresh();
|
|
3880
|
+
void broadcast();
|
|
3881
|
+
}
|
|
3829
3882
|
void clockTick();
|
|
3830
3883
|
const agentClock = setInterval(() => {
|
|
3831
3884
|
void clockTick();
|
|
@@ -3909,6 +3962,26 @@ data: ${JSON.stringify(await buildState())}
|
|
|
3909
3962
|
sendState(res, await buildState());
|
|
3910
3963
|
return;
|
|
3911
3964
|
}
|
|
3965
|
+
if (path === "/api/timezone" && req.method === "POST") {
|
|
3966
|
+
let body;
|
|
3967
|
+
try {
|
|
3968
|
+
body = JSON.parse(await readBody(req));
|
|
3969
|
+
} catch (err) {
|
|
3970
|
+
sendJSON(res, 400, { error: `invalid JSON body: ${err.message}` });
|
|
3971
|
+
return;
|
|
3972
|
+
}
|
|
3973
|
+
const timeZone = validZone(body?.timeZone);
|
|
3974
|
+
if (!timeZone) {
|
|
3975
|
+
sendJSON(res, 400, { error: '"timeZone" must be an IANA time zone, such as Europe/Copenhagen' });
|
|
3976
|
+
return;
|
|
3977
|
+
}
|
|
3978
|
+
if (await zone.report(timeZone)) {
|
|
3979
|
+
zoneMoved();
|
|
3980
|
+
await clockTick();
|
|
3981
|
+
}
|
|
3982
|
+
sendState(res, await buildState());
|
|
3983
|
+
return;
|
|
3984
|
+
}
|
|
3912
3985
|
if (path === "/api/calendar/refresh" && req.method === "POST") {
|
|
3913
3986
|
await calendar.refresh();
|
|
3914
3987
|
sendState(res, await buildState());
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -1010,6 +1010,9 @@ function subscribe(onState, onError) {
|
|
|
1010
1010
|
source.addEventListener("open", () => onError(null));
|
|
1011
1011
|
return () => source.close();
|
|
1012
1012
|
}
|
|
1013
|
+
function postTimeZone(timeZone) {
|
|
1014
|
+
return post("/api/timezone", { timeZone });
|
|
1015
|
+
}
|
|
1013
1016
|
function postSession(body) {
|
|
1014
1017
|
return post("/api/session", body);
|
|
1015
1018
|
}
|
|
@@ -1293,15 +1296,28 @@ function ThemeToggle({ ui: ui2, handlers: handlers2 }) {
|
|
|
1293
1296
|
}
|
|
1294
1297
|
|
|
1295
1298
|
// client/format.ts
|
|
1296
|
-
var
|
|
1297
|
-
var dayFmt = new Intl.DateTimeFormat(void 0, {
|
|
1299
|
+
var DAY_FORMAT = {
|
|
1298
1300
|
weekday: "long",
|
|
1299
1301
|
day: "numeric",
|
|
1300
1302
|
month: "long"
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1303
|
+
};
|
|
1304
|
+
function formatters(timeZone) {
|
|
1305
|
+
return {
|
|
1306
|
+
time: new Intl.DateTimeFormat(void 0, { timeZone, hour: "2-digit", minute: "2-digit" }),
|
|
1307
|
+
day: new Intl.DateTimeFormat(void 0, { timeZone, ...DAY_FORMAT }),
|
|
1308
|
+
shortDay: new Intl.DateTimeFormat(void 0, { timeZone, day: "numeric", month: "short" }),
|
|
1309
|
+
shortDayYear: new Intl.DateTimeFormat(void 0, { timeZone, day: "numeric", month: "short", year: "numeric" }),
|
|
1310
|
+
weekday: new Intl.DateTimeFormat(void 0, { timeZone, weekday: "long" })
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
var clockFormats = formatters();
|
|
1314
|
+
var dateFormats = formatters("UTC");
|
|
1315
|
+
var DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
1316
|
+
function formatted(value, kind) {
|
|
1317
|
+
const bare = typeof value === "string" && DATE_ONLY.test(value);
|
|
1318
|
+
const d4 = bare ? /* @__PURE__ */ new Date(`${value}T00:00:00Z`) : parseDate(value);
|
|
1319
|
+
return d4 && !Number.isNaN(d4.getTime()) ? (bare ? dateFormats : clockFormats)[kind].format(d4) : "";
|
|
1320
|
+
}
|
|
1305
1321
|
function parseDate(value) {
|
|
1306
1322
|
if (typeof value !== "string" || value === "") return null;
|
|
1307
1323
|
const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
@@ -1310,20 +1326,16 @@ function parseDate(value) {
|
|
|
1310
1326
|
return Number.isNaN(d4.getTime()) ? null : d4;
|
|
1311
1327
|
}
|
|
1312
1328
|
function formatTime(value) {
|
|
1313
|
-
|
|
1314
|
-
return d4 ? timeFmt.format(d4) : "";
|
|
1329
|
+
return formatted(value, "time");
|
|
1315
1330
|
}
|
|
1316
1331
|
function formatDay(value) {
|
|
1317
|
-
|
|
1318
|
-
return d4 ? dayFmt.format(d4) : "";
|
|
1332
|
+
return formatted(value, "day");
|
|
1319
1333
|
}
|
|
1320
1334
|
function formatShortDay(value) {
|
|
1321
|
-
|
|
1322
|
-
return d4 ? shortDayFmt.format(d4) : "";
|
|
1335
|
+
return formatted(value, "shortDay");
|
|
1323
1336
|
}
|
|
1324
1337
|
function formatWeekday(value) {
|
|
1325
|
-
|
|
1326
|
-
return d4 ? weekdayFmt.format(d4) : "";
|
|
1338
|
+
return formatted(value, "weekday");
|
|
1327
1339
|
}
|
|
1328
1340
|
var dayKeyFmt = null;
|
|
1329
1341
|
var calendarZone;
|
|
@@ -1331,9 +1343,11 @@ function setCalendarZone(timeZone) {
|
|
|
1331
1343
|
if (timeZone === calendarZone) return;
|
|
1332
1344
|
calendarZone = timeZone;
|
|
1333
1345
|
try {
|
|
1346
|
+
clockFormats = formatters(timeZone);
|
|
1334
1347
|
dayKeyFmt = timeZone ? new Intl.DateTimeFormat("en-US", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }) : null;
|
|
1335
1348
|
} catch {
|
|
1336
1349
|
dayKeyFmt = null;
|
|
1350
|
+
clockFormats = formatters();
|
|
1337
1351
|
}
|
|
1338
1352
|
}
|
|
1339
1353
|
function dayKey(d4) {
|
|
@@ -1370,8 +1384,8 @@ function daysFromToday(value, now = /* @__PURE__ */ new Date()) {
|
|
|
1370
1384
|
}
|
|
1371
1385
|
const target = parseDate(value);
|
|
1372
1386
|
if (!target) return null;
|
|
1373
|
-
const a4 =
|
|
1374
|
-
const b4 =
|
|
1387
|
+
const a4 = Date.UTC(...splitKey(dayKey(now)));
|
|
1388
|
+
const b4 = Date.UTC(...splitKey(dayKey(target)));
|
|
1375
1389
|
return Math.round((b4 - a4) / 864e5);
|
|
1376
1390
|
}
|
|
1377
1391
|
function relativeDay(value, now = /* @__PURE__ */ new Date()) {
|
|
@@ -1402,7 +1416,7 @@ function relativeTime(value, now = /* @__PURE__ */ new Date()) {
|
|
|
1402
1416
|
if (hours < 48) return `${hours} h ago`;
|
|
1403
1417
|
const days = Math.round(hours / 24);
|
|
1404
1418
|
if (days <= 30) return `${days} days ago`;
|
|
1405
|
-
return d4.
|
|
1419
|
+
return dayKey(d4).slice(0, 4) === dayKey(now).slice(0, 4) ? formatShortDay(value) : formatted(value, "shortDayYear");
|
|
1406
1420
|
}
|
|
1407
1421
|
function firstLine(text) {
|
|
1408
1422
|
const line = String(text).split("\n").find((l5) => l5.trim() !== "") ?? "";
|
|
@@ -3458,6 +3472,46 @@ function parkButton(row, ui2, handlers2, menuKey) {
|
|
|
3458
3472
|
);
|
|
3459
3473
|
}
|
|
3460
3474
|
|
|
3475
|
+
// client/zone.ts
|
|
3476
|
+
var ZONE_CHECK_MS = 6e4;
|
|
3477
|
+
function browserZone() {
|
|
3478
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
3479
|
+
}
|
|
3480
|
+
function sameZone(a4, b4) {
|
|
3481
|
+
if (a4 === b4) return true;
|
|
3482
|
+
try {
|
|
3483
|
+
const canonical = (zone2) => new Intl.DateTimeFormat("en", { timeZone: zone2 }).resolvedOptions().timeZone;
|
|
3484
|
+
return canonical(a4) === canonical(b4);
|
|
3485
|
+
} catch {
|
|
3486
|
+
return false;
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
function watchZone(deps) {
|
|
3490
|
+
let seen = deps.browserZone();
|
|
3491
|
+
let sending = null;
|
|
3492
|
+
function tellIfElsewhere() {
|
|
3493
|
+
const here = deps.browserZone();
|
|
3494
|
+
const there = deps.dashboardZone();
|
|
3495
|
+
if (there === void 0 || sameZone(here, there) || sending === here) return;
|
|
3496
|
+
sending = here;
|
|
3497
|
+
deps.send(here).catch((err) => console.warn(`[daily-focus] could not tell the dashboard this browser is in ${here}:`, err)).finally(() => {
|
|
3498
|
+
sending = null;
|
|
3499
|
+
});
|
|
3500
|
+
}
|
|
3501
|
+
return {
|
|
3502
|
+
look() {
|
|
3503
|
+
seen = deps.browserZone();
|
|
3504
|
+
if (deps.visible()) tellIfElsewhere();
|
|
3505
|
+
},
|
|
3506
|
+
check() {
|
|
3507
|
+
const here = deps.browserZone();
|
|
3508
|
+
if (here === seen) return;
|
|
3509
|
+
seen = here;
|
|
3510
|
+
tellIfElsewhere();
|
|
3511
|
+
}
|
|
3512
|
+
};
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3461
3515
|
// client/chrome.ts
|
|
3462
3516
|
function Header({ state: state2, ui: ui2, handlers: handlers2 }) {
|
|
3463
3517
|
const error = ui2.connectionError.value;
|
|
@@ -3468,7 +3522,8 @@ function Header({ state: state2, ui: ui2, handlers: handlers2 }) {
|
|
|
3468
3522
|
"div",
|
|
3469
3523
|
{ class: "page-header__titles" },
|
|
3470
3524
|
el("h1", null, "Daily Focus"),
|
|
3471
|
-
el("p", { class: "page-header__date", id: "header-date" }, state2 ? headerDate(state2) : "")
|
|
3525
|
+
el("p", { class: "page-header__date", id: "header-date" }, state2 ? headerDate(state2) : ""),
|
|
3526
|
+
state2 ? zoneLine(state2) : null
|
|
3472
3527
|
),
|
|
3473
3528
|
el(
|
|
3474
3529
|
"div",
|
|
@@ -3562,6 +3617,11 @@ function headerDate(state2) {
|
|
|
3562
3617
|
const today = dayKey(new Date(state2.now));
|
|
3563
3618
|
return formatDay(state2.brief.date ?? today) || formatDay(today);
|
|
3564
3619
|
}
|
|
3620
|
+
function zoneLine(state2) {
|
|
3621
|
+
if (sameZone(browserZone(), state2.timeZone)) return null;
|
|
3622
|
+
const at = state2.agentRun?.schedule ? ` \xB7 Morning agent at ${state2.agentRun.schedule.at}` : "";
|
|
3623
|
+
return el("p", { class: "page-header__zone", id: "header-zone" }, `Times in ${state2.timeZone}${at}`);
|
|
3624
|
+
}
|
|
3565
3625
|
function freshness(state2, handlers2) {
|
|
3566
3626
|
let text;
|
|
3567
3627
|
let stale;
|
|
@@ -3717,7 +3777,7 @@ function HelpDialog(_props) {
|
|
|
3717
3777
|
el(
|
|
3718
3778
|
"p",
|
|
3719
3779
|
{ class: "help__note" },
|
|
3720
|
-
`Done, snoozed and dismissed all tell the morning agent to leave an item out of tomorrow's brief. A note is sent along with it as free text. On both boards only snooze and note apply: a pull request stays until it's merged or closed, and a ticket whose status looks wrong is put right in Jira rather than here. Click a card to open its panel, where its notes and the assistant live; the panel stays on the item it was opened for. The assistant is a CLI run on your machine
|
|
3780
|
+
`Done, snoozed and dismissed all tell the morning agent to leave an item out of tomorrow's brief. A note is sent along with it as free text. On both boards only snooze and note apply: a pull request stays until it's merged or closed, and a ticket whose status looks wrong is put right in Jira rather than here. Click a card to open its panel, where its notes and the assistant live; the panel stays on the item it was opened for. The assistant is a CLI run on your machine, told to read and draft, never to send or post. Click "updated \u2026 ago" in the header to open the morning agent's latest run: its report, earlier runs, and a place to ask it why it did what it did.`
|
|
3721
3781
|
),
|
|
3722
3782
|
el("form", { method: "dialog" }, el("button", { type: "submit", class: "button" }, "Close"))
|
|
3723
3783
|
);
|
|
@@ -5164,15 +5224,31 @@ j3(() => {
|
|
|
5164
5224
|
installKeyboard(handlers);
|
|
5165
5225
|
await refresh();
|
|
5166
5226
|
startLocalTick();
|
|
5227
|
+
var zone = watchZone({
|
|
5228
|
+
browserZone,
|
|
5229
|
+
dashboardZone: () => state.value?.timeZone,
|
|
5230
|
+
visible: () => document.visibilityState === "visible",
|
|
5231
|
+
send: async (timeZone) => adoptState(await postTimeZone(timeZone))
|
|
5232
|
+
});
|
|
5233
|
+
zone.look();
|
|
5234
|
+
document.addEventListener("visibilitychange", () => zone.look());
|
|
5235
|
+
window.addEventListener("focus", () => zone.look());
|
|
5236
|
+
setInterval(() => zone.check(), ZONE_CHECK_MS);
|
|
5167
5237
|
followLocation();
|
|
5168
5238
|
window.addEventListener("popstate", followLocation);
|
|
5169
5239
|
window.addEventListener("hashchange", followLocation);
|
|
5240
|
+
var reconnected = false;
|
|
5170
5241
|
subscribe(
|
|
5171
5242
|
// A server push is authoritative, but must not yank a row out from under an
|
|
5172
5243
|
// in-flight click; adoptState keeps anything pending at its optimistic status.
|
|
5173
|
-
(next) =>
|
|
5244
|
+
(next) => {
|
|
5245
|
+
adoptState(next);
|
|
5246
|
+
if (reconnected) zone.look();
|
|
5247
|
+
reconnected = false;
|
|
5248
|
+
},
|
|
5174
5249
|
(err) => {
|
|
5175
5250
|
ui.connectionError.value = err;
|
|
5251
|
+
if (err === null) reconnected = true;
|
|
5176
5252
|
}
|
|
5177
5253
|
);
|
|
5178
5254
|
//# sourceMappingURL=app.js.map
|