daily-focus 0.1.0
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/LICENSE +21 -0
- package/README.md +170 -0
- package/SETUP.md +501 -0
- package/dist/audit-4AUZVELT.js +215 -0
- package/dist/chunk-2PLWVW5T.js +987 -0
- package/dist/chunk-6VWWJZKJ.js +27 -0
- package/dist/chunk-EXGTFXNV.js +77 -0
- package/dist/chunk-G4K24I4M.js +425 -0
- package/dist/chunk-NDWZUZJE.js +179 -0
- package/dist/chunk-QKT2FAB5.js +132 -0
- package/dist/chunk-V2UN3JYL.js +140 -0
- package/dist/cli.js +203 -0
- package/dist/init-ZJCI2723.js +122 -0
- package/dist/sample-M7WNKQOV.js +7 -0
- package/dist/seed-7DIDKXT7.js +50 -0
- package/dist/server-B4FWZHS2.js +4814 -0
- package/dist/service-ILBMIZB7.js +240 -0
- package/package.json +66 -0
- package/prompts/README.md +122 -0
- package/prompts/assistant.md +62 -0
- package/prompts/morning-brief-personal.md +442 -0
- package/prompts/morning-brief-work.md +630 -0
- package/public/app.js +5058 -0
- package/public/index.html +36 -0
- package/public/style.css +2515 -0
- package/schema/items.schema.json +99 -0
- package/tools/dfcal/Info.plist +16 -0
- package/tools/dfcal/build.sh +52 -0
- package/tools/dfcal/main.swift +133 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import {
|
|
2
|
+
localDateKey
|
|
3
|
+
} from "./chunk-EXGTFXNV.js";
|
|
4
|
+
|
|
5
|
+
// src/sample.ts
|
|
6
|
+
function sampleBrief(now = /* @__PURE__ */ new Date()) {
|
|
7
|
+
function at(hour, minute = 0) {
|
|
8
|
+
return new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute).toISOString();
|
|
9
|
+
}
|
|
10
|
+
function daysFromNow(days) {
|
|
11
|
+
const d = new Date(now.getFullYear(), now.getMonth(), now.getDate() + days);
|
|
12
|
+
return localDateKey(d);
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
version: 1,
|
|
16
|
+
generatedAt: at(6, 30),
|
|
17
|
+
generatedBy: "morning-brief-agent (sample)",
|
|
18
|
+
date: localDateKey(now),
|
|
19
|
+
headline: "Two things are genuinely time-boxed today: the **Workday compliance training** deadline and the review `webapp#3421` is blocked on. The 13:00\u201315:00 gap is your only real focus block.",
|
|
20
|
+
items: [
|
|
21
|
+
{
|
|
22
|
+
id: "workday:task:compliance-training-2026",
|
|
23
|
+
source: "workday",
|
|
24
|
+
kind: "task",
|
|
25
|
+
title: "Complete annual security & compliance training",
|
|
26
|
+
detail: "Workday has sent three reminders. Due end of day \u2014 after that it escalates to your manager.",
|
|
27
|
+
url: "https://wd5.myworkday.com/acme/d/task/compliance-2026",
|
|
28
|
+
priority: 1,
|
|
29
|
+
due: daysFromNow(0),
|
|
30
|
+
firstSeen: daysFromNow(-6),
|
|
31
|
+
tags: ["deadline"]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "github:pr:acme/webapp#3421",
|
|
35
|
+
source: "github",
|
|
36
|
+
kind: "task",
|
|
37
|
+
title: "Review requested: Fix session replay memory leak on long-lived tabs",
|
|
38
|
+
detail: "Requested you directly 2 days ago. 4 files, +180/\u221262. No other reviewer has looked yet.",
|
|
39
|
+
url: "https://github.com/acme/webapp/pull/3421",
|
|
40
|
+
priority: 2,
|
|
41
|
+
firstSeen: daysFromNow(-2),
|
|
42
|
+
people: ["@alice"],
|
|
43
|
+
tags: ["review-requested"]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "gtasks:task:MTU3NDkyODkwMjE2NDcx",
|
|
47
|
+
source: "tasks",
|
|
48
|
+
kind: "task",
|
|
49
|
+
title: "Write up the staging repro steps for the checkout breakage",
|
|
50
|
+
detail: "Your own note, and the only item here nobody else will ever chase.",
|
|
51
|
+
due: daysFromNow(1),
|
|
52
|
+
firstSeen: daysFromNow(-4),
|
|
53
|
+
advancesObjective: true
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "email:thread:18f2a9c4b7",
|
|
57
|
+
source: "email",
|
|
58
|
+
kind: "task",
|
|
59
|
+
title: "Re: Q4 headcount plan \u2014 Bob is waiting on your input",
|
|
60
|
+
detail: "They asked for the team split on Monday and followed up yesterday. Nothing sent back yet.",
|
|
61
|
+
url: "https://mail.google.com/mail/u/0/#inbox/18f2a9c4b7",
|
|
62
|
+
priority: 3,
|
|
63
|
+
firstSeen: daysFromNow(-3),
|
|
64
|
+
people: ["bob@example.com"]
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "github:pr:acme/webapp#3402",
|
|
68
|
+
source: "github",
|
|
69
|
+
kind: "task",
|
|
70
|
+
title: "CI failing on your PR: Add trace context to fetch instrumentation",
|
|
71
|
+
detail: "`unit-tests (node-20)` is red. The failure is in `tracing/fetch.spec.ts`, which your diff touches \u2014 likely yours, not flaky infra.",
|
|
72
|
+
url: "https://github.com/acme/webapp/pull/3402",
|
|
73
|
+
firstSeen: daysFromNow(-1),
|
|
74
|
+
tags: ["ci-failing"]
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "github:pr:acme/webapp#3395",
|
|
78
|
+
source: "github",
|
|
79
|
+
kind: "task",
|
|
80
|
+
title: "Open 3 days with no review: Bump chrome-launcher to 1.2.0",
|
|
81
|
+
url: "https://github.com/acme/webapp/pull/3395",
|
|
82
|
+
firstSeen: daysFromNow(-3),
|
|
83
|
+
tags: ["awaiting-review"]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "github:pr:acme/webapp#3430",
|
|
87
|
+
source: "github",
|
|
88
|
+
kind: "task",
|
|
89
|
+
title: "Draft PR is green \u2014 ready to mark for review?",
|
|
90
|
+
detail: "You opened this last night as a draft. All checks passed 40 minutes ago.",
|
|
91
|
+
url: "https://github.com/acme/webapp/pull/3430",
|
|
92
|
+
firstSeen: daysFromNow(0),
|
|
93
|
+
tags: ["draft", "ci-green"]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "jira:PROJ-8842",
|
|
97
|
+
source: "jira",
|
|
98
|
+
kind: "task",
|
|
99
|
+
title: "PROJ-8842 still open \u2014 all 2 linked PRs are merged",
|
|
100
|
+
detail: "Assigned to you. Both PRs merged last week; the ticket never moved to Done.",
|
|
101
|
+
url: "https://acme.atlassian.net/browse/PROJ-8842",
|
|
102
|
+
firstSeen: daysFromNow(-5)
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "slack:msg:C04ABCDE/1757480412.118",
|
|
106
|
+
source: "slack",
|
|
107
|
+
kind: "task",
|
|
108
|
+
title: "#webapp-dev: Chris asked whether v6 can drop IE11 shims",
|
|
109
|
+
detail: "Direct question to you, 19 hours ago, still unanswered in-thread.",
|
|
110
|
+
url: "https://acme.slack.com/archives/C04ABCDE/p1757480412118",
|
|
111
|
+
firstSeen: daysFromNow(-1),
|
|
112
|
+
people: ["@chris"]
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "email:thread:18f30bb211",
|
|
116
|
+
source: "email",
|
|
117
|
+
kind: "task",
|
|
118
|
+
title: "Travel: approve your Dublin flight option before 17:00",
|
|
119
|
+
detail: "The held fare expires this evening. Two options attached.",
|
|
120
|
+
url: "https://mail.google.com/mail/u/0/#inbox/18f30bb211",
|
|
121
|
+
due: daysFromNow(0),
|
|
122
|
+
firstSeen: daysFromNow(0),
|
|
123
|
+
tags: ["travel"]
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "calendar:event:standup",
|
|
127
|
+
source: "calendar",
|
|
128
|
+
kind: "event",
|
|
129
|
+
title: "Webapp standup",
|
|
130
|
+
start: at(9, 30),
|
|
131
|
+
end: at(9, 45),
|
|
132
|
+
url: "https://meet.google.com/abc-defg-hij"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: "calendar:event:1on1",
|
|
136
|
+
source: "calendar",
|
|
137
|
+
kind: "event",
|
|
138
|
+
title: "1:1 with Alice",
|
|
139
|
+
start: at(11, 0),
|
|
140
|
+
end: at(11, 30)
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: "calendar:event:design-review",
|
|
144
|
+
source: "calendar",
|
|
145
|
+
kind: "event",
|
|
146
|
+
title: "Design review: search indexing v3",
|
|
147
|
+
start: at(11, 15),
|
|
148
|
+
end: at(12, 0)
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "calendar:event:sync",
|
|
152
|
+
source: "calendar",
|
|
153
|
+
kind: "event",
|
|
154
|
+
title: "Platform \u2194 Infra sync",
|
|
155
|
+
start: at(15, 0),
|
|
156
|
+
end: at(16, 0)
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "calendar:holiday:us-labor-day",
|
|
160
|
+
source: "calendar",
|
|
161
|
+
kind: "info",
|
|
162
|
+
title: "US public holiday today \u2014 most American colleagues are off",
|
|
163
|
+
detail: "Expect slow replies from colleagues there.",
|
|
164
|
+
firstSeen: daysFromNow(0)
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "calendar:holiday:local-national-day",
|
|
168
|
+
source: "calendar",
|
|
169
|
+
kind: "info",
|
|
170
|
+
title: `Local public holiday in 9 days (${daysFromNow(9)}) \u2014 office closed`,
|
|
171
|
+
firstSeen: daysFromNow(0)
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export {
|
|
178
|
+
sampleBrief
|
|
179
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// src/links.ts
|
|
2
|
+
import { lstat, readlink, rename, stat, symlink, unlink } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, relative, resolve } from "node:path";
|
|
5
|
+
async function linkState(link, source) {
|
|
6
|
+
try {
|
|
7
|
+
if (!(await lstat(link)).isSymbolicLink()) return { state: "own" };
|
|
8
|
+
} catch {
|
|
9
|
+
return { state: "missing" };
|
|
10
|
+
}
|
|
11
|
+
const target = resolve(dirname(link), await readlink(link));
|
|
12
|
+
if (target === source) return { state: "linked" };
|
|
13
|
+
try {
|
|
14
|
+
await stat(target);
|
|
15
|
+
} catch {
|
|
16
|
+
return { state: "dangling", target };
|
|
17
|
+
}
|
|
18
|
+
return { state: "elsewhere", target };
|
|
19
|
+
}
|
|
20
|
+
function linksInUse(config) {
|
|
21
|
+
const links = [];
|
|
22
|
+
if (config.agent.cli) {
|
|
23
|
+
const reader = "the morning agent";
|
|
24
|
+
links.push({ name: "prompt.md", link: config.promptFile, source: config.promptSource, reader, fallsBack: false });
|
|
25
|
+
links.push({ name: "items.schema.json", link: config.schemaFile, source: config.schemaSource, reader, fallsBack: false });
|
|
26
|
+
}
|
|
27
|
+
if (config.assistant.agent) {
|
|
28
|
+
links.push({
|
|
29
|
+
name: "assistant.md",
|
|
30
|
+
link: config.assistantPromptFile,
|
|
31
|
+
source: config.assistantPromptSource,
|
|
32
|
+
reader: "the assistant",
|
|
33
|
+
fallsBack: true
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return links;
|
|
37
|
+
}
|
|
38
|
+
function shown(path, home) {
|
|
39
|
+
return path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
|
|
40
|
+
}
|
|
41
|
+
function checkoutOf(target) {
|
|
42
|
+
const folder = dirname(target);
|
|
43
|
+
return ["prompts", "schema"].includes(basename(folder)) ? dirname(folder) : target;
|
|
44
|
+
}
|
|
45
|
+
function names(list) {
|
|
46
|
+
const code = list.map((name) => `\`${name}\``);
|
|
47
|
+
return code.length <= 1 ? code.join("") : `${code.slice(0, -1).join(", ")} and ${code.at(-1)}`;
|
|
48
|
+
}
|
|
49
|
+
async function storeLinkWarning(config, home = homedir()) {
|
|
50
|
+
const elsewhere = [];
|
|
51
|
+
const dangling = [];
|
|
52
|
+
const missing = [];
|
|
53
|
+
for (const entry of linksInUse(config)) {
|
|
54
|
+
const found = await linkState(entry.link, entry.source);
|
|
55
|
+
if (found.state === "elsewhere") elsewhere.push({ name: entry.name, target: found.target, reader: entry.reader });
|
|
56
|
+
else if (entry.fallsBack) continue;
|
|
57
|
+
else if (found.state === "dangling") dangling.push({ name: entry.name, target: found.target });
|
|
58
|
+
else if (found.state === "missing") missing.push(entry.name);
|
|
59
|
+
}
|
|
60
|
+
const sentences = [];
|
|
61
|
+
if (elsewhere.length > 0) {
|
|
62
|
+
const one = elsewhere.length === 1;
|
|
63
|
+
const readers = [...new Set(elsewhere.map((e) => e.reader))];
|
|
64
|
+
sentences.push(
|
|
65
|
+
`${names(elsewhere.map((e) => e.name))} in the store ${one ? "links" : "link"} to another checkout (\`${shown(checkoutOf(elsewhere[0].target), home)}\`), so ${readers.join(" and ")} ${readers.length === 1 ? "follows" : "follow"} that copy, not this one.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (dangling.length > 0) {
|
|
69
|
+
const one = dangling.length === 1;
|
|
70
|
+
sentences.push(
|
|
71
|
+
`${names(dangling.map((d) => d.name))} in the store ${one ? "links to a file that" : "link to files that"} no longer ${one ? "exists" : "exist"} (\`${shown(dangling[0].target, home)}\`), so the morning agent can't read ${one ? "it" : "them"}.`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (missing.length > 0) {
|
|
75
|
+
const one = missing.length === 1;
|
|
76
|
+
sentences.push(`${names(missing)} ${one ? "is" : "are"} missing from the store, so the morning agent can't read ${one ? "it" : "them"}.`);
|
|
77
|
+
}
|
|
78
|
+
if (sentences.length === 0) return null;
|
|
79
|
+
const count = elsewhere.length + dangling.length + missing.length;
|
|
80
|
+
const checkout = shown(dirname(dirname(config.promptSource)), home);
|
|
81
|
+
sentences.push(`Restart the dashboard to link ${count === 1 ? "it" : "them"} back to \`${checkout}\`.`);
|
|
82
|
+
return sentences.join(" ");
|
|
83
|
+
}
|
|
84
|
+
async function linkOne(name, target, source) {
|
|
85
|
+
let before = null;
|
|
86
|
+
try {
|
|
87
|
+
const stats = await lstat(target);
|
|
88
|
+
if (!stats.isSymbolicLink()) return { name, result: "own", detail: null };
|
|
89
|
+
before = resolve(dirname(target), await readlink(target));
|
|
90
|
+
if (before === source) return { name, result: "kept", detail: null };
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error.code !== "ENOENT") {
|
|
93
|
+
return { name, result: "failed", detail: error instanceof Error ? error.message : String(error) };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const staged = `${target}.${process.pid}.link`;
|
|
97
|
+
try {
|
|
98
|
+
await unlink(staged).catch(() => {
|
|
99
|
+
});
|
|
100
|
+
await symlink(source, staged);
|
|
101
|
+
await rename(staged, target);
|
|
102
|
+
return { name, result: "linked", detail: before };
|
|
103
|
+
} catch (error) {
|
|
104
|
+
await unlink(staged).catch(() => {
|
|
105
|
+
});
|
|
106
|
+
const code = error.code;
|
|
107
|
+
const hint = code === "EPERM" || code === "EACCES" ? "; copy it there by hand instead" : "";
|
|
108
|
+
return { name, result: "failed", detail: `${error instanceof Error ? error.message : String(error)}${hint}` };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function linkStore(config) {
|
|
112
|
+
return [
|
|
113
|
+
await linkOne("prompt.md", config.promptFile, config.promptSource),
|
|
114
|
+
await linkOne("items.schema.json", config.schemaFile, config.schemaSource),
|
|
115
|
+
await linkOne("assistant.md", config.assistantPromptFile, config.assistantPromptSource)
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
function describeLink(config, outcome) {
|
|
119
|
+
const source = {
|
|
120
|
+
"prompt.md": config.promptSource,
|
|
121
|
+
"items.schema.json": config.schemaSource,
|
|
122
|
+
"assistant.md": config.assistantPromptSource
|
|
123
|
+
}[outcome.name];
|
|
124
|
+
return source ? `${outcome.name} \u2192 ${relative(dirname(dirname(config.promptSource)), source)}` : outcome.name;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export {
|
|
128
|
+
linkState,
|
|
129
|
+
storeLinkWarning,
|
|
130
|
+
linkStore,
|
|
131
|
+
describeLink
|
|
132
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// src/templates.ts
|
|
2
|
+
var FOCUS_TEMPLATE = `---
|
|
3
|
+
objective:
|
|
4
|
+
blocker:
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!--
|
|
8
|
+
Fill in the two lines above: the one thing you are actually trying to achieve right
|
|
9
|
+
now, and what is in the way today, if anything. Leaving \`objective\` blank is fine:
|
|
10
|
+
the dashboard shows a quiet reminder instead. Delete this file to turn the feature
|
|
11
|
+
off entirely.
|
|
12
|
+
|
|
13
|
+
The dashboard renders them at the top of the board, and the briefing agent ranks the
|
|
14
|
+
whole day against them \u2014 so an objective phrased as a project ("ship the thing")
|
|
15
|
+
gives it less to work with than one phrased as a next move ("get the staging path
|
|
16
|
+
working again").
|
|
17
|
+
|
|
18
|
+
Prose below the frontmatter is shown under the objective. Comments like this one are
|
|
19
|
+
not. Keep it short and keep it current. This file is the only input describing what
|
|
20
|
+
you are trying to do, as opposed to what other people sent you overnight.
|
|
21
|
+
-->
|
|
22
|
+
|
|
23
|
+
<!-- agent-only -->
|
|
24
|
+
Everything below this marker is read by the agent and never shown on the board. The
|
|
25
|
+
dashboard shows it only in the editor you open to change it.
|
|
26
|
+
|
|
27
|
+
Put context here that should steer the ranking but shouldn't be on screen during a
|
|
28
|
+
screen share: why this objective matters right now, who is watching it, what is
|
|
29
|
+
riding on it. The agent weighs it, and is told never to quote it into a title or
|
|
30
|
+
detail.
|
|
31
|
+
`;
|
|
32
|
+
var WORK_SOURCES_TEMPLATE = `# Sources
|
|
33
|
+
|
|
34
|
+
The personal half of your morning brief. The prompt next to this file
|
|
35
|
+
(\`prompt.md\`) says *what* to gather and how to judge it; this file says *who* and
|
|
36
|
+
*where*. The briefing agent reads it; the dashboard shows it only in its editor.
|
|
37
|
+
|
|
38
|
+
Replace the placeholders below and delete whatever doesn't apply. A section you leave
|
|
39
|
+
out is fine \u2014 the agent falls back to your primary calendar and connected accounts,
|
|
40
|
+
and reports the gap. A section that is *wrong* is worse than one that is missing.
|
|
41
|
+
|
|
42
|
+
## Identity
|
|
43
|
+
|
|
44
|
+
Meeting notes, Doc assignments and tickets attribute work by name rather than by
|
|
45
|
+
account, so the agent needs to know which name is yours before it can tell your action
|
|
46
|
+
items from everyone else's. Leaving this out doesn't fail loudly \u2014 it just quietly
|
|
47
|
+
stops raising anything from those sources.
|
|
48
|
+
|
|
49
|
+
- Name, as meeting notes and Doc assignments write it: \`Your Name\`
|
|
50
|
+
- Work email: \`you@example.com\`
|
|
51
|
+
- GitHub login: \`your-github-login\` \u2014 review requests and authorship are judged
|
|
52
|
+
against it
|
|
53
|
+
|
|
54
|
+
## Calendars
|
|
55
|
+
|
|
56
|
+
Query:
|
|
57
|
+
|
|
58
|
+
- the primary work calendar
|
|
59
|
+
- \`Some Shared Calendar\` \u2014 a second calendar whose events should still block time
|
|
60
|
+
|
|
61
|
+
Never query or include: \`Some Noisy Calendar\`.
|
|
62
|
+
|
|
63
|
+
## Holiday calendars
|
|
64
|
+
|
|
65
|
+
- \`Holidays in <where your colleagues are>\` \u2014 a holiday there means slow replies, and
|
|
66
|
+
it is worth saying so.
|
|
67
|
+
- \`Holidays in <where you are>\` \u2014 call one out today, or within the next 14 days.
|
|
68
|
+
|
|
69
|
+
## Recurring meeting notes
|
|
70
|
+
|
|
71
|
+
Documents to re-read on every run. Name the tab where it matters, since these tend to
|
|
72
|
+
be long and only one part is current:
|
|
73
|
+
|
|
74
|
+
- https://docs.google.com/document/d/<document id>/edit?tab=t.0 \u2014 what meeting it is
|
|
75
|
+
`;
|
|
76
|
+
var PERSONAL_SOURCES_TEMPLATE = `# Sources
|
|
77
|
+
|
|
78
|
+
The private half of your morning brief. The prompt next to this file
|
|
79
|
+
(\`prompt.md\`) says *what* to gather and how to judge it; this file says *who* and
|
|
80
|
+
*where*. The briefing agent reads it; the dashboard shows it only in its editor.
|
|
81
|
+
|
|
82
|
+
Replace the placeholders below and delete whatever doesn't apply. A section you leave
|
|
83
|
+
out is fine \u2014 the agent reports the gap. A section that is *wrong* is worse than one
|
|
84
|
+
that is missing.
|
|
85
|
+
|
|
86
|
+
## Identity
|
|
87
|
+
|
|
88
|
+
- Name: \`Your Name\`
|
|
89
|
+
- Personal email: \`you@personal.example\`
|
|
90
|
+
- GitHub login: \`your-github-login\` \u2014 review requests and authorship are judged
|
|
91
|
+
against it
|
|
92
|
+
|
|
93
|
+
## My weeks
|
|
94
|
+
|
|
95
|
+
What decides how much of a day is yours, so the agent can set the day's bounds: when
|
|
96
|
+
work ends, regular pickups, a rhythm that alternates week to week and how to tell
|
|
97
|
+
which week it is.
|
|
98
|
+
|
|
99
|
+
- Workdays end at 16:00; personal time is from then until 22:00.
|
|
100
|
+
|
|
101
|
+
## Calendars
|
|
102
|
+
|
|
103
|
+
- \`Personal\`
|
|
104
|
+
- \`Family\` \u2014 shared; its events block time too
|
|
105
|
+
|
|
106
|
+
Never query or include: \`Some Noisy Calendar\`.
|
|
107
|
+
|
|
108
|
+
## Holiday calendars
|
|
109
|
+
|
|
110
|
+
- \`Holidays in <where you live>\` \u2014 including school holidays, if there is one
|
|
111
|
+
|
|
112
|
+
## Email
|
|
113
|
+
|
|
114
|
+
Which tool reaches the account, and any senders to always raise or always skip.
|
|
115
|
+
|
|
116
|
+
## e-Boks
|
|
117
|
+
|
|
118
|
+
Which tool or CLI reaches it.
|
|
119
|
+
|
|
120
|
+
## Reminders
|
|
121
|
+
|
|
122
|
+
Which tool reaches Apple Reminders, and which lists to read. Unset means all of them.
|
|
123
|
+
|
|
124
|
+
## Messages
|
|
125
|
+
|
|
126
|
+
Which tool reaches Apple Messages, and any conversations to skip.
|
|
127
|
+
|
|
128
|
+
## GitHub
|
|
129
|
+
|
|
130
|
+
Which repositories or organisations are your own projects. Unset means everything
|
|
131
|
+
the login above has open.
|
|
132
|
+
`;
|
|
133
|
+
function sourcesTemplate(profile) {
|
|
134
|
+
return profile === "personal" ? PERSONAL_SOURCES_TEMPLATE : WORK_SOURCES_TEMPLATE;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export {
|
|
138
|
+
FOCUS_TEMPLATE,
|
|
139
|
+
sourcesTemplate
|
|
140
|
+
};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
IN_NPX_CACHE,
|
|
4
|
+
ROOT,
|
|
5
|
+
USER_CALENDAR_APP
|
|
6
|
+
} from "./chunk-6VWWJZKJ.js";
|
|
7
|
+
|
|
8
|
+
// src/cli.ts
|
|
9
|
+
import { execFile, spawn } from "node:child_process";
|
|
10
|
+
import { readFileSync, rmSync } from "node:fs";
|
|
11
|
+
import { cp, mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { parseArgs, promisify } from "node:util";
|
|
15
|
+
var run = promisify(execFile);
|
|
16
|
+
var HELP = `Daily Focus: a dashboard for deciding what to work on today.
|
|
17
|
+
|
|
18
|
+
Usage
|
|
19
|
+
daily-focus [options] start the dashboard and open it in your browser
|
|
20
|
+
daily-focus --demo look around with sample data, in a throwaway store
|
|
21
|
+
daily-focus service [--remove] keep it running in the background (macOS)
|
|
22
|
+
daily-focus init create the store with templates to edit by hand
|
|
23
|
+
daily-focus audit [--against <items.json>]
|
|
24
|
+
check the current brief against the contract
|
|
25
|
+
daily-focus seed [--force] write a sample brief to the store
|
|
26
|
+
daily-focus build-calendar build the macOS calendar helper for the live agenda
|
|
27
|
+
|
|
28
|
+
Options
|
|
29
|
+
--data <dir> the store (default ~/.daily-focus)
|
|
30
|
+
--port <port> the port to listen on (default 4321)
|
|
31
|
+
--host <host> the address to listen on (default 127.0.0.1)
|
|
32
|
+
--profile <name> work or personal, for a new store
|
|
33
|
+
--no-open don't open the browser
|
|
34
|
+
--exit-with-stdin stop when standard input closes, for an app running it as a child
|
|
35
|
+
-v, --version print the version
|
|
36
|
+
-h, --help print this
|
|
37
|
+
|
|
38
|
+
Everything else is set up in the dashboard itself.`;
|
|
39
|
+
function version() {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).version ?? "unknown";
|
|
42
|
+
} catch {
|
|
43
|
+
return "unknown";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function openInBrowser(url) {
|
|
47
|
+
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
48
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
49
|
+
child.on("error", () => {
|
|
50
|
+
});
|
|
51
|
+
child.unref();
|
|
52
|
+
}
|
|
53
|
+
function shouldOpen(flag) {
|
|
54
|
+
return flag && process.stdout.isTTY === true && !process.env.CI;
|
|
55
|
+
}
|
|
56
|
+
async function demo(open) {
|
|
57
|
+
const store = await mkdtemp(join(tmpdir(), "daily-focus-demo-"));
|
|
58
|
+
process.on("exit", () => rmSync(store, { recursive: true, force: true }));
|
|
59
|
+
Object.assign(process.env, {
|
|
60
|
+
DAILY_FOCUS_DATA: store,
|
|
61
|
+
// Any free port unless one was asked for, so the demo never collides with
|
|
62
|
+
// a dashboard already running on 4321.
|
|
63
|
+
DAILY_FOCUS_PORT: process.env.DAILY_FOCUS_PORT ?? "0",
|
|
64
|
+
DAILY_FOCUS_PROFILE: "work",
|
|
65
|
+
DAILY_FOCUS_AGENT: "off",
|
|
66
|
+
DAILY_FOCUS_ASSISTANT: "off",
|
|
67
|
+
DAILY_FOCUS_GITHUB: "off",
|
|
68
|
+
DAILY_FOCUS_JIRA: "off",
|
|
69
|
+
DAILY_FOCUS_CALENDAR: "off"
|
|
70
|
+
});
|
|
71
|
+
const { sampleBrief } = await import("./sample-M7WNKQOV.js");
|
|
72
|
+
await writeFile(join(store, "items.json"), `${JSON.stringify(sampleBrief(), null, 2)}
|
|
73
|
+
`);
|
|
74
|
+
await start(open, (url) => {
|
|
75
|
+
console.log(`
|
|
76
|
+
The demo is at ${url}, with sample data in a throwaway store.`);
|
|
77
|
+
console.log(" Nothing is connected and nothing runs. Stop it with Ctrl+C; the store goes with it.\n");
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async function start(open, onStarted) {
|
|
81
|
+
const { runServer } = await import("./server-B4FWZHS2.js");
|
|
82
|
+
await runServer({
|
|
83
|
+
onStarted: (url) => {
|
|
84
|
+
onStarted?.(url);
|
|
85
|
+
if (shouldOpen(open)) openInBrowser(url);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async function buildCalendar() {
|
|
90
|
+
if (process.platform !== "darwin") {
|
|
91
|
+
console.log("The calendar helper reads Calendar.app, so it is macOS-only. Nothing to build.");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const work = await mkdtemp(join(tmpdir(), "daily-focus-calendar-"));
|
|
95
|
+
try {
|
|
96
|
+
await cp(join(ROOT, "tools", "dfcal"), work, { recursive: true });
|
|
97
|
+
try {
|
|
98
|
+
const { stdout } = await run("/bin/sh", [join(work, "build.sh")]);
|
|
99
|
+
process.stdout.write(stdout);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error(err.stderr?.trim() || err.message);
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
await mkdir(join(USER_CALENDAR_APP, ".."), { recursive: true });
|
|
106
|
+
rmSync(USER_CALENDAR_APP, { recursive: true, force: true });
|
|
107
|
+
await run("/usr/bin/ditto", [join(work, "build", "Daily Focus Calendar.app"), USER_CALENDAR_APP]);
|
|
108
|
+
} finally {
|
|
109
|
+
rmSync(work, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
console.log(`installed ${USER_CALENDAR_APP}`);
|
|
112
|
+
console.log("Restart the dashboard, choose calendars in its settings, and allow access when macOS asks.");
|
|
113
|
+
}
|
|
114
|
+
async function main(argv) {
|
|
115
|
+
const { values, positionals } = parseArgs({
|
|
116
|
+
args: argv,
|
|
117
|
+
allowPositionals: true,
|
|
118
|
+
allowNegative: true,
|
|
119
|
+
options: {
|
|
120
|
+
data: { type: "string" },
|
|
121
|
+
port: { type: "string" },
|
|
122
|
+
host: { type: "string" },
|
|
123
|
+
profile: { type: "string" },
|
|
124
|
+
open: { type: "boolean", default: true },
|
|
125
|
+
"exit-with-stdin": { type: "boolean" },
|
|
126
|
+
demo: { type: "boolean" },
|
|
127
|
+
remove: { type: "boolean" },
|
|
128
|
+
force: { type: "boolean" },
|
|
129
|
+
against: { type: "string" },
|
|
130
|
+
version: { type: "boolean", short: "v" },
|
|
131
|
+
help: { type: "boolean", short: "h" }
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
if (values.help) {
|
|
135
|
+
console.log(HELP);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (values.version) {
|
|
139
|
+
console.log(version());
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (values.data !== void 0) process.env.DAILY_FOCUS_DATA = values.data;
|
|
143
|
+
if (values.port !== void 0) process.env.DAILY_FOCUS_PORT = values.port;
|
|
144
|
+
if (values.host !== void 0) process.env.DAILY_FOCUS_HOST = values.host;
|
|
145
|
+
if (values.profile !== void 0) process.env.DAILY_FOCUS_PROFILE = values.profile;
|
|
146
|
+
const open = values.open !== false;
|
|
147
|
+
const [command, ...rest] = positionals;
|
|
148
|
+
if (rest.length > 0) throw new Error(`unexpected ${rest[0]}; see daily-focus --help`);
|
|
149
|
+
const only = {
|
|
150
|
+
demo: ["start"],
|
|
151
|
+
"exit-with-stdin": ["start"],
|
|
152
|
+
open: ["start"],
|
|
153
|
+
remove: ["service"],
|
|
154
|
+
force: ["seed"],
|
|
155
|
+
against: ["audit"]
|
|
156
|
+
};
|
|
157
|
+
const given = command ?? "start";
|
|
158
|
+
for (const [flag, commands] of Object.entries(only)) {
|
|
159
|
+
const set = values[flag];
|
|
160
|
+
if (set === void 0 || flag === "open" && set === true) continue;
|
|
161
|
+
if (!commands.includes(given)) throw new Error(`--${flag === "open" ? "no-open" : flag} doesn't apply to ${given}; see daily-focus --help`);
|
|
162
|
+
}
|
|
163
|
+
switch (command) {
|
|
164
|
+
case void 0:
|
|
165
|
+
case "start":
|
|
166
|
+
if (values.demo) return demo(open);
|
|
167
|
+
if (IN_NPX_CACHE) {
|
|
168
|
+
console.log(" Running from npx. To keep it running in the background, install it:");
|
|
169
|
+
console.log(" npm install -g daily-focus, then daily-focus service.\n");
|
|
170
|
+
}
|
|
171
|
+
if (values["exit-with-stdin"]) {
|
|
172
|
+
const stop = () => process.kill(process.pid, "SIGTERM");
|
|
173
|
+
process.stdin.once("end", stop).once("close", stop).resume();
|
|
174
|
+
}
|
|
175
|
+
return start(open);
|
|
176
|
+
case "service": {
|
|
177
|
+
const { main: service } = await import("./service-ILBMIZB7.js");
|
|
178
|
+
process.exitCode = await service(values.remove ? ["--remove"] : []);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// These three are scripts that do their work as they load, and read their
|
|
182
|
+
// own flags from the command line.
|
|
183
|
+
case "init":
|
|
184
|
+
await import("./init-ZJCI2723.js");
|
|
185
|
+
return;
|
|
186
|
+
case "audit":
|
|
187
|
+
await import("./audit-4AUZVELT.js");
|
|
188
|
+
return;
|
|
189
|
+
case "seed":
|
|
190
|
+
await import("./seed-7DIDKXT7.js");
|
|
191
|
+
return;
|
|
192
|
+
case "build-calendar":
|
|
193
|
+
return buildCalendar();
|
|
194
|
+
default:
|
|
195
|
+
throw new Error(`unknown command ${command}; see daily-focus --help`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
await main(process.argv.slice(2));
|
|
200
|
+
} catch (err) {
|
|
201
|
+
console.error(`daily-focus: ${err.message}`);
|
|
202
|
+
process.exitCode = 1;
|
|
203
|
+
}
|