dayleaf 0.0.0-stage → 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/README.md +124 -2
- package/dist/cli.js +190 -0
- package/dist/client.js +101 -0
- package/dist/mcp.js +85 -0
- package/package.json +13 -4
package/README.md
CHANGED
|
@@ -1,3 +1,125 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dayleaf — Headless Dayleaf
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Read and write your [Dayleaf](https://getdayleaf.app) journal from the command
|
|
4
|
+
line or from any AI agent, through the local API the Dayleaf Mac app serves on
|
|
5
|
+
your computer. Nothing leaves your Mac.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx dayleaf add "Ran 5k along the river before work" --planner Health
|
|
9
|
+
npx dayleaf day today
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Setup (one minute)
|
|
13
|
+
|
|
14
|
+
1. Open **Dayleaf on your Mac** (2.5.0 or later) → Settings → **Headless Dayleaf** → switch it on.
|
|
15
|
+
2. Click **Copy API token**.
|
|
16
|
+
3. In Terminal:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install -g dayleaf # or use npx dayleaf … each time
|
|
20
|
+
dayleaf auth <paste-token>
|
|
21
|
+
dayleaf ping # → Dayleaf 2.5.0 (292) at http://127.0.0.1:7317 — authenticated
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The token is stored in `~/.dayleaf/config.json`. The app must be open for the
|
|
25
|
+
API to answer; it listens only on `127.0.0.1:7317`.
|
|
26
|
+
|
|
27
|
+
## CLI
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
dayleaf add "text" [--date today|yesterday|-N|YYYY-MM-DD] [--planner Name] [--source muse]
|
|
31
|
+
dayleaf edit <id> [--content "…"] [--date …] [--planner …]
|
|
32
|
+
dayleaf rm <id>
|
|
33
|
+
dayleaf list [--from …] [--to …] [--planner …] [--q text] [--limit N]
|
|
34
|
+
dayleaf day [date]
|
|
35
|
+
dayleaf show <id>
|
|
36
|
+
dayleaf planners · dayleaf stickers · dayleaf goals [--kind daily|weekly|monthly] [--date …]
|
|
37
|
+
dayleaf stamp <stickerId> [--date …] · dayleaf unstamp <stickerId> [--date …]
|
|
38
|
+
dayleaf goal add "text" [--kind …] [--date …] · dayleaf goal done <id> · dayleaf goal undo <id>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Add `--json` to any command for machine-readable output. A planner named in
|
|
42
|
+
`--planner` is created if it does not exist, so `--planner Weather` just works.
|
|
43
|
+
|
|
44
|
+
Entries written through the API carry a `source` (default `cli`, or
|
|
45
|
+
`--source muse`) and show up in the app with a small "via muse" label, so your
|
|
46
|
+
own words and an agent's notes never blur.
|
|
47
|
+
|
|
48
|
+
## MCP server (for agents)
|
|
49
|
+
|
|
50
|
+
`dayleaf-mcp` is a [Model Context Protocol](https://modelcontextprotocol.io)
|
|
51
|
+
server over stdio. It exposes these tools: `dayleaf_add_entry`,
|
|
52
|
+
`dayleaf_list_entries`, `dayleaf_get_day`, `dayleaf_update_entry`,
|
|
53
|
+
`dayleaf_delete_entry`, `dayleaf_list_planners`, `dayleaf_list_stickers`,
|
|
54
|
+
`dayleaf_stamp_sticker`, `dayleaf_get_goals`, `dayleaf_add_goal`,
|
|
55
|
+
`dayleaf_set_goal`.
|
|
56
|
+
|
|
57
|
+
Claude Desktop (`claude_desktop_config.json`):
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{ "mcpServers": { "dayleaf": { "command": "npx", "args": ["-y", "dayleaf-mcp"], "env": { "DAYLEAF_SOURCE": "claude" } } } }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Claude Code:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
claude mcp add dayleaf -- npx -y dayleaf-mcp
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Any other MCP-capable agent: run `npx -y dayleaf-mcp` as a stdio server and
|
|
70
|
+
set `DAYLEAF_SOURCE` to the agent's name. Then say things like "log today's
|
|
71
|
+
weather in my Weather planner" or "what did I write last week about sleep?".
|
|
72
|
+
|
|
73
|
+
## Cloud: agents that are not on your Mac (Meta Muse, hosted assistants)
|
|
74
|
+
|
|
75
|
+
Switch on **Settings → Cloud access for agents** in Dayleaf (any platform, signed in),
|
|
76
|
+
tap **Create API key**, and point the agent at the cloud endpoint:
|
|
77
|
+
|
|
78
|
+
- **MCP server URL:** `https://date-jotter-production.up.railway.app/mcp` with header `Authorization: Bearer <API key>`
|
|
79
|
+
- **REST:** same `/v1/…` routes as above at `https://date-jotter-production.up.railway.app`
|
|
80
|
+
- **This CLI:** `dayleaf auth <API key> --url https://date-jotter-production.up.railway.app`
|
|
81
|
+
|
|
82
|
+
Tell Muse: *"Build a custom integration to Dayleaf. Its MCP server URL is
|
|
83
|
+
https://date-jotter-production.up.railway.app/mcp and it needs a bearer token.
|
|
84
|
+
I want you to be able to write my daily weather and health notes into my journal
|
|
85
|
+
and read back what I wrote, from any future conversation. Connect to it, test
|
|
86
|
+
every capability end to end, show me the results, and save the integration as a
|
|
87
|
+
reusable skill."* Muse asks for the token through its secure credential flow.
|
|
88
|
+
|
|
89
|
+
The cloud keeps a **text-only** copy of your entries (never photos, audio or video)
|
|
90
|
+
and every device of the account pulls what agents write. Stickers and goals are
|
|
91
|
+
local-API only for now.
|
|
92
|
+
|
|
93
|
+
## Shell scripts and cron
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
# Every evening: today's weather into the Weather planner
|
|
97
|
+
curl -s "wttr.in/San+Jose?format=%C+%t" | xargs -I{} dayleaf add "{}" --planner Weather --source wttr
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## HTTP API
|
|
101
|
+
|
|
102
|
+
Base URL `http://127.0.0.1:7317`, header `Authorization: Bearer <token>`, JSON in and out.
|
|
103
|
+
|
|
104
|
+
| Method | Path | Body / query |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| GET | `/v1/ping` | (no auth) version, build, `authenticated` |
|
|
107
|
+
| GET | `/v1/planners` | |
|
|
108
|
+
| POST | `/v1/planners` | `{name, color?}` |
|
|
109
|
+
| GET | `/v1/entries` | `?from&to&planner&q&limit` |
|
|
110
|
+
| POST | `/v1/entries` | `{content, date?, planner?, source?}` |
|
|
111
|
+
| GET / PATCH / DELETE | `/v1/entries/:id` | PATCH `{content?, date?, planner?}` |
|
|
112
|
+
| GET | `/v1/days/:date` | entries + stickers + daily goals |
|
|
113
|
+
| GET | `/v1/stickers` | catalog + custom stickers |
|
|
114
|
+
| POST | `/v1/days/:date/stickers` | `{id}` |
|
|
115
|
+
| DELETE | `/v1/days/:date/stickers/:id` | |
|
|
116
|
+
| GET | `/v1/goals` | `?kind=daily\|weekly\|monthly&date` |
|
|
117
|
+
| POST | `/v1/goals` | `{kind, date?, text}` |
|
|
118
|
+
| PATCH | `/v1/goals/:id` | `{kind, date?, done?, text?}` |
|
|
119
|
+
|
|
120
|
+
Dates are `YYYY-MM-DD`. Planners accept an id or a name. Errors are `{error}` with 400/401/404.
|
|
121
|
+
|
|
122
|
+
## Privacy
|
|
123
|
+
|
|
124
|
+
The API is loopback-only and off by default. The token is generated on your
|
|
125
|
+
Mac and never sent anywhere; regenerate it from Settings at any time.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// dayleaf — command-line access to your Dayleaf journal (via the Mac app's
|
|
3
|
+
// local API). `dayleaf help` lists everything.
|
|
4
|
+
import { Dayleaf, DayleafError, loadConfig, saveConfig, resolveDate } from "./client.js";
|
|
5
|
+
const argv = process.argv.slice(2);
|
|
6
|
+
const flags = {};
|
|
7
|
+
const positional = [];
|
|
8
|
+
for (let i = 0; i < argv.length; i++) {
|
|
9
|
+
const a = argv[i];
|
|
10
|
+
if (a.startsWith("--")) {
|
|
11
|
+
const [k, v] = a.slice(2).split("=", 2);
|
|
12
|
+
if (v !== undefined)
|
|
13
|
+
flags[k] = v;
|
|
14
|
+
else if (i + 1 < argv.length && !argv[i + 1].startsWith("--"))
|
|
15
|
+
flags[k] = argv[++i];
|
|
16
|
+
else
|
|
17
|
+
flags[k] = true;
|
|
18
|
+
}
|
|
19
|
+
else
|
|
20
|
+
positional.push(a);
|
|
21
|
+
}
|
|
22
|
+
const cmd = positional.shift() || "help";
|
|
23
|
+
const json = flags.json === true;
|
|
24
|
+
const str = (k) => (typeof flags[k] === "string" ? flags[k] : undefined);
|
|
25
|
+
const HELP = `dayleaf — Headless Dayleaf CLI
|
|
26
|
+
|
|
27
|
+
Setup (once):
|
|
28
|
+
dayleaf auth <token> save the token from Dayleaf → Settings → Headless Dayleaf
|
|
29
|
+
dayleaf ping check the Mac app is reachable
|
|
30
|
+
|
|
31
|
+
Write:
|
|
32
|
+
dayleaf add "text" [--date today|yesterday|-N|YYYY-MM-DD] [--planner Name] [--source muse]
|
|
33
|
+
dayleaf edit <id> [--content "…"] [--date …] [--planner …]
|
|
34
|
+
dayleaf rm <id>
|
|
35
|
+
dayleaf stamp <stickerId> [--date …] dayleaf unstamp <stickerId> [--date …]
|
|
36
|
+
dayleaf goal add "text" [--kind daily|weekly|monthly] [--date …]
|
|
37
|
+
dayleaf goal done <id> [--kind …] [--date …] dayleaf goal undo <id> …
|
|
38
|
+
|
|
39
|
+
Read:
|
|
40
|
+
dayleaf list [--from …] [--to …] [--planner …] [--q text] [--limit N]
|
|
41
|
+
dayleaf day [date] entries, stickers and goals for a day
|
|
42
|
+
dayleaf show <id>
|
|
43
|
+
dayleaf planners dayleaf stickers dayleaf goals [--kind …] [--date …]
|
|
44
|
+
|
|
45
|
+
Every command accepts --json for machine-readable output, --url and --token to override config.
|
|
46
|
+
Agents: run \`dayleaf-mcp\` as an MCP server (stdio) to get the same operations as tools.`;
|
|
47
|
+
function out(data, human) { if (json)
|
|
48
|
+
console.log(JSON.stringify(data, null, 2));
|
|
49
|
+
else
|
|
50
|
+
human(); }
|
|
51
|
+
function fmtEntry(e, planners) {
|
|
52
|
+
const src = e.source ? ` (via ${e.source})` : "";
|
|
53
|
+
const first = e.content.split("\n")[0];
|
|
54
|
+
return `${e.date} ${(planners.get(e.plannerId) || e.plannerId).padEnd(12)} ${first.length > 70 ? first.slice(0, 69) + "…" : first}${src}\n id ${e.id}`;
|
|
55
|
+
}
|
|
56
|
+
async function main() {
|
|
57
|
+
if (cmd === "help" || flags.help) {
|
|
58
|
+
console.log(HELP);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (cmd === "auth") {
|
|
62
|
+
const token = positional[0] || str("token");
|
|
63
|
+
if (!token)
|
|
64
|
+
throw new DayleafError("usage: dayleaf auth <token>");
|
|
65
|
+
saveConfig({ ...loadConfig(), token, url: str("url") || loadConfig().url });
|
|
66
|
+
const api = new Dayleaf({ token, url: str("url") });
|
|
67
|
+
const p = await api.ping();
|
|
68
|
+
console.log(p.authenticated ? `Connected to Dayleaf ${p.version} (${p.build}) — token saved to ~/.dayleaf/config.json` : "Token saved, but Dayleaf rejected it. Copy it again from Settings → Headless Dayleaf.");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const api = new Dayleaf({ url: str("url"), token: str("token"), source: str("source") });
|
|
72
|
+
const plannerNames = async () => new Map((await api.planners()).planners.map(p => [p.id, p.name]));
|
|
73
|
+
switch (cmd) {
|
|
74
|
+
case "ping": {
|
|
75
|
+
const p = await api.ping();
|
|
76
|
+
out(p, () => console.log(`Dayleaf ${p.version} (${p.build}) at ${api.url} — ${p.authenticated ? "authenticated" : "NOT authenticated (run dayleaf auth <token>)"}`));
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case "add": {
|
|
80
|
+
const content = positional.join(" ") || str("content");
|
|
81
|
+
if (!content)
|
|
82
|
+
throw new DayleafError("usage: dayleaf add \"text\" [--date …] [--planner …]");
|
|
83
|
+
const r = await api.addEntry({ content, date: resolveDate(str("date")), planner: str("planner"), source: str("source") });
|
|
84
|
+
out(r.entry, () => console.log(`Added to ${r.entry.date} (${r.entry.plannerId}) — id ${r.entry.id}`));
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case "edit": {
|
|
88
|
+
const id = positional[0];
|
|
89
|
+
if (!id)
|
|
90
|
+
throw new DayleafError("usage: dayleaf edit <id> --content … | --date … | --planner …");
|
|
91
|
+
const r = await api.updateEntry(id, { content: str("content"), date: str("date") ? resolveDate(str("date")) : undefined, planner: str("planner") });
|
|
92
|
+
out(r.entry, () => console.log(`Updated ${r.entry.id}`));
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case "rm":
|
|
96
|
+
case "delete": {
|
|
97
|
+
const id = positional[0];
|
|
98
|
+
if (!id)
|
|
99
|
+
throw new DayleafError("usage: dayleaf rm <id>");
|
|
100
|
+
const r = await api.deleteEntry(id);
|
|
101
|
+
out(r, () => console.log(`Deleted ${r.deleted}`));
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case "list": {
|
|
105
|
+
const r = await api.entries({ from: str("from") ? resolveDate(str("from")) : undefined, to: str("to") ? resolveDate(str("to")) : undefined, planner: str("planner"), q: str("q"), limit: str("limit") ? Number(str("limit")) : undefined });
|
|
106
|
+
const names = await plannerNames();
|
|
107
|
+
out(r, () => { console.log(r.entries.map(e => fmtEntry(e, names)).join("\n")); console.log(`\n${r.entries.length} of ${r.total} entries`); });
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case "show": {
|
|
111
|
+
const id = positional[0];
|
|
112
|
+
if (!id)
|
|
113
|
+
throw new DayleafError("usage: dayleaf show <id>");
|
|
114
|
+
const r = await api.entry(id);
|
|
115
|
+
out(r.entry, () => console.log(`${r.entry.date} · ${r.entry.plannerId}${r.entry.source ? ` · via ${r.entry.source}` : ""}\n\n${r.entry.content}`));
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case "day": {
|
|
119
|
+
const date = resolveDate(positional[0] || str("date"));
|
|
120
|
+
const d = await api.day(date);
|
|
121
|
+
const names = await plannerNames();
|
|
122
|
+
out(d, () => {
|
|
123
|
+
console.log(`${d.date}`);
|
|
124
|
+
if (d.stickers.length)
|
|
125
|
+
console.log(`stickers: ${d.stickers.map(s => s.count > 1 ? `${s.id}×${s.count}` : s.id).join(", ")}`);
|
|
126
|
+
if (d.goals.length)
|
|
127
|
+
console.log(`goals:\n${d.goals.map(g => ` [${g.done ? "x" : " "}] ${g.text} (${g.id})`).join("\n")}`);
|
|
128
|
+
console.log(d.entries.length ? d.entries.map(e => fmtEntry(e, names)).join("\n") : "(no entries)");
|
|
129
|
+
});
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case "planners": {
|
|
133
|
+
const r = await api.planners();
|
|
134
|
+
out(r, () => console.log(r.planners.map(p => `${p.name.padEnd(16)} ${p.color} ${p.id}${p.icon ? ` ${p.icon}` : ""}`).join("\n")));
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case "stickers": {
|
|
138
|
+
const r = await api.stickers();
|
|
139
|
+
out(r, () => console.log(r.stickers.map(s => `${s.id.padEnd(14)} ${s.symbol}${s.custom ? " (custom)" : ""}`).join("\n")));
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case "stamp": {
|
|
143
|
+
const id = positional[0];
|
|
144
|
+
if (!id)
|
|
145
|
+
throw new DayleafError("usage: dayleaf stamp <stickerId> [--date …]");
|
|
146
|
+
const r = await api.stamp(resolveDate(str("date")), id);
|
|
147
|
+
out(r, () => console.log(`Stamped ${r.id} on ${r.date} (×${r.count})`));
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
case "unstamp": {
|
|
151
|
+
const id = positional[0];
|
|
152
|
+
if (!id)
|
|
153
|
+
throw new DayleafError("usage: dayleaf unstamp <stickerId> [--date …]");
|
|
154
|
+
const r = await api.unstamp(resolveDate(str("date")), id);
|
|
155
|
+
out(r, () => console.log(`Removed one ${id} from ${r.date}`));
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case "goals": {
|
|
159
|
+
const kind = (str("kind") || "daily");
|
|
160
|
+
const r = await api.goals(kind, str("date") ? resolveDate(str("date")) : undefined);
|
|
161
|
+
out(r, () => { console.log(`${r.kind} goals · ${r.period}`); console.log(r.goals.length ? r.goals.map(g => ` [${g.done ? "x" : " "}] ${g.text} (${g.id})`).join("\n") : " (none)"); if (r.note)
|
|
162
|
+
console.log(`note: ${r.note}`); });
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case "goal": {
|
|
166
|
+
const sub = positional.shift();
|
|
167
|
+
const kind = (str("kind") || "daily");
|
|
168
|
+
const date = str("date") ? resolveDate(str("date")) : undefined;
|
|
169
|
+
if (sub === "add") {
|
|
170
|
+
const text = positional.join(" ");
|
|
171
|
+
if (!text)
|
|
172
|
+
throw new DayleafError("usage: dayleaf goal add \"text\"");
|
|
173
|
+
const r = await api.addGoal(text, kind, date);
|
|
174
|
+
out(r, () => console.log(`Added ${kind} goal for ${r.period} — id ${r.goal.id}`));
|
|
175
|
+
}
|
|
176
|
+
else if (sub === "done" || sub === "undo") {
|
|
177
|
+
const id = positional[0];
|
|
178
|
+
if (!id)
|
|
179
|
+
throw new DayleafError(`usage: dayleaf goal ${sub} <id>`);
|
|
180
|
+
const r = await api.setGoal(id, { done: sub === "done" }, kind, date);
|
|
181
|
+
out(r, () => console.log(`${r.goal.done ? "Done" : "Reopened"}: ${r.goal.text}`));
|
|
182
|
+
}
|
|
183
|
+
else
|
|
184
|
+
throw new DayleafError("usage: dayleaf goal add|done|undo …");
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
default: throw new DayleafError(`Unknown command "${cmd}". Run dayleaf help.`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
main().catch((e) => { console.error(e instanceof DayleafError ? e.message : (e?.stack || String(e))); process.exit(1); });
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Thin client for the Headless Dayleaf local API served by the Mac app
|
|
2
|
+
// (Settings → Headless Dayleaf). Token and base URL come from, in order:
|
|
3
|
+
// flags, DAYLEAF_TOKEN / DAYLEAF_URL, or ~/.dayleaf/config.json.
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
export const DEFAULT_URL = "http://127.0.0.1:7317";
|
|
8
|
+
const CONFIG_DIR = join(homedir(), ".dayleaf");
|
|
9
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
10
|
+
export function loadConfig() {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function saveConfig(cfg) {
|
|
19
|
+
if (!existsSync(CONFIG_DIR))
|
|
20
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
21
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", { mode: 0o600 });
|
|
22
|
+
}
|
|
23
|
+
export class DayleafError extends Error {
|
|
24
|
+
status;
|
|
25
|
+
constructor(message, status) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.status = status;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class Dayleaf {
|
|
31
|
+
url;
|
|
32
|
+
token;
|
|
33
|
+
source;
|
|
34
|
+
constructor(opts = {}) {
|
|
35
|
+
const cfg = loadConfig();
|
|
36
|
+
this.url = (opts.url || process.env.DAYLEAF_URL || cfg.url || DEFAULT_URL).replace(/\/$/, "");
|
|
37
|
+
this.token = opts.token || process.env.DAYLEAF_TOKEN || cfg.token || "";
|
|
38
|
+
this.source = opts.source || process.env.DAYLEAF_SOURCE || "cli";
|
|
39
|
+
}
|
|
40
|
+
async request(method, path, body, query) {
|
|
41
|
+
const qs = query ? "?" + Object.entries(query).filter(([, v]) => v !== undefined && v !== "").map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&") : "";
|
|
42
|
+
let res;
|
|
43
|
+
try {
|
|
44
|
+
res = await fetch(this.url + path + (qs === "?" ? "" : qs), {
|
|
45
|
+
method,
|
|
46
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${this.token}`, "x-dayleaf-source": this.source },
|
|
47
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
throw new DayleafError(`Cannot reach Dayleaf at ${this.url}. Open Dayleaf on this Mac and switch on Settings → Headless Dayleaf. (${e?.cause?.code || e?.message})`);
|
|
52
|
+
}
|
|
53
|
+
const text = await res.text();
|
|
54
|
+
let json = {};
|
|
55
|
+
try {
|
|
56
|
+
json = text ? JSON.parse(text) : {};
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
json = { raw: text };
|
|
60
|
+
}
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const hint = res.status === 401 ? " Run `dayleaf auth <token>` with the token from Settings → Headless Dayleaf → Copy API token." : "";
|
|
63
|
+
throw new DayleafError((json.error || `HTTP ${res.status}`) + hint, res.status);
|
|
64
|
+
}
|
|
65
|
+
return json;
|
|
66
|
+
}
|
|
67
|
+
ping() { return this.request("GET", "/v1/ping"); }
|
|
68
|
+
planners() { return this.request("GET", "/v1/planners"); }
|
|
69
|
+
createPlanner(name, color) { return this.request("POST", "/v1/planners", { name, color }); }
|
|
70
|
+
entries(q = {}) { return this.request("GET", "/v1/entries", undefined, q); }
|
|
71
|
+
entry(id) { return this.request("GET", `/v1/entries/${encodeURIComponent(id)}`); }
|
|
72
|
+
addEntry(e) { return this.request("POST", "/v1/entries", { ...e, source: e.source || this.source }); }
|
|
73
|
+
updateEntry(id, patch) { return this.request("PATCH", `/v1/entries/${encodeURIComponent(id)}`, patch); }
|
|
74
|
+
deleteEntry(id) { return this.request("DELETE", `/v1/entries/${encodeURIComponent(id)}`); }
|
|
75
|
+
day(date) { return this.request("GET", `/v1/days/${date}`); }
|
|
76
|
+
stickers() { return this.request("GET", "/v1/stickers"); }
|
|
77
|
+
stamp(date, id) { return this.request("POST", `/v1/days/${date}/stickers`, { id }); }
|
|
78
|
+
unstamp(date, id) { return this.request("DELETE", `/v1/days/${date}/stickers/${encodeURIComponent(id)}`); }
|
|
79
|
+
goals(kind = "daily", date) { return this.request("GET", "/v1/goals", undefined, { kind, date }); }
|
|
80
|
+
addGoal(text, kind = "daily", date) { return this.request("POST", "/v1/goals", { kind, date, text }); }
|
|
81
|
+
setGoal(id, patch, kind = "daily", date) { return this.request("PATCH", `/v1/goals/${encodeURIComponent(id)}`, { kind, date, ...patch }); }
|
|
82
|
+
}
|
|
83
|
+
export function today() {
|
|
84
|
+
const d = new Date();
|
|
85
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
86
|
+
}
|
|
87
|
+
// "today" | "yesterday" | "-3" (days ago) | YYYY-MM-DD
|
|
88
|
+
export function resolveDate(input) {
|
|
89
|
+
if (!input || input === "today")
|
|
90
|
+
return today();
|
|
91
|
+
const d = new Date();
|
|
92
|
+
if (input === "yesterday")
|
|
93
|
+
d.setDate(d.getDate() - 1);
|
|
94
|
+
else if (/^-\d+$/.test(input))
|
|
95
|
+
d.setDate(d.getDate() + Number(input));
|
|
96
|
+
else if (/^\d{4}-\d{2}-\d{2}$/.test(input))
|
|
97
|
+
return input;
|
|
98
|
+
else
|
|
99
|
+
throw new DayleafError(`Unrecognised date "${input}" (use YYYY-MM-DD, today, yesterday or -N)`);
|
|
100
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
101
|
+
}
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// dayleaf-mcp — a Model Context Protocol server (stdio) exposing the Dayleaf
|
|
3
|
+
// journal to agents: Claude, ChatGPT, Muse, anything that speaks MCP.
|
|
4
|
+
// Configure once with `dayleaf auth <token>`; then e.g. in Claude Desktop:
|
|
5
|
+
// { "mcpServers": { "dayleaf": { "command": "npx", "args": ["-y", "dayleaf-mcp"] } } }
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { Dayleaf, DayleafError, resolveDate } from "./client.js";
|
|
10
|
+
const api = new Dayleaf({ source: process.env.DAYLEAF_SOURCE || "agent" });
|
|
11
|
+
const server = new McpServer({ name: "dayleaf", version: "0.1.0" });
|
|
12
|
+
const text = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] });
|
|
13
|
+
const fail = (e) => ({ content: [{ type: "text", text: e instanceof DayleafError ? e.message : String(e) }], isError: true });
|
|
14
|
+
const dateArg = z.string().optional().describe("YYYY-MM-DD, 'today', 'yesterday' or '-N' days ago. Default today.");
|
|
15
|
+
const kindArg = z.enum(["daily", "weekly", "monthly"]).optional().describe("Goal period. Default daily.");
|
|
16
|
+
server.tool("dayleaf_add_entry", "Write a journal entry into Dayleaf for a date. Use this to log things on the user's behalf (weather, health, screen time, a summary of the day). Entries are shown in the app with a 'via <source>' label.", { content: z.string().describe("The entry text (markdown-free plain text, newlines allowed)."), date: dateArg,
|
|
17
|
+
planner: z.string().optional().describe("Planner name or id, e.g. 'Health' or 'Weather'. Created if it doesn't exist."),
|
|
18
|
+
source: z.string().optional().describe("Who is writing, e.g. 'muse'. Defaults to the server's DAYLEAF_SOURCE.") }, async ({ content, date, planner, source }) => { try {
|
|
19
|
+
return text((await api.addEntry({ content, date: resolveDate(date), planner, source })).entry);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
return fail(e);
|
|
23
|
+
} });
|
|
24
|
+
server.tool("dayleaf_list_entries", "Read journal entries, newest first. Filter by date range, planner, or a text search.", { from: dateArg, to: dateArg, planner: z.string().optional(), q: z.string().optional().describe("Case-insensitive text match."), limit: z.number().int().min(1).max(500).optional() }, async ({ from, to, planner, q, limit }) => { try {
|
|
25
|
+
return text(await api.entries({ from: from ? resolveDate(from) : undefined, to: to ? resolveDate(to) : undefined, planner, q, limit }));
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
return fail(e);
|
|
29
|
+
} });
|
|
30
|
+
server.tool("dayleaf_get_day", "Everything Dayleaf has for one day: entries, stickers and daily goals.", { date: dateArg }, async ({ date }) => { try {
|
|
31
|
+
return text(await api.day(resolveDate(date)));
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
return fail(e);
|
|
35
|
+
} });
|
|
36
|
+
server.tool("dayleaf_update_entry", "Edit an existing entry's text, date or planner.", { id: z.string(), content: z.string().optional(), date: dateArg, planner: z.string().optional() }, async ({ id, content, date, planner }) => { try {
|
|
37
|
+
return text((await api.updateEntry(id, { content, date: date ? resolveDate(date) : undefined, planner })).entry);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
return fail(e);
|
|
41
|
+
} });
|
|
42
|
+
server.tool("dayleaf_delete_entry", "Delete an entry by id. Only delete entries the agent itself created unless the user asked.", { id: z.string() }, async ({ id }) => { try {
|
|
43
|
+
return text(await api.deleteEntry(id));
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
return fail(e);
|
|
47
|
+
} });
|
|
48
|
+
server.tool("dayleaf_list_planners", "List the user's planners (journals/notebooks) with ids and colours.", {}, async () => { try {
|
|
49
|
+
return text(await api.planners());
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
return fail(e);
|
|
53
|
+
} });
|
|
54
|
+
server.tool("dayleaf_list_stickers", "List sticker ids that can be stamped on a day (catalog + the user's custom stickers).", {}, async () => { try {
|
|
55
|
+
return text(await api.stickers());
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
return fail(e);
|
|
59
|
+
} });
|
|
60
|
+
server.tool("dayleaf_stamp_sticker", "Stamp a sticker on a day (e.g. 'workout', 'travel'). Repeat to add another copy.", { id: z.string(), date: dateArg }, async ({ id, date }) => { try {
|
|
61
|
+
return text(await api.stamp(resolveDate(date), id));
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
return fail(e);
|
|
65
|
+
} });
|
|
66
|
+
server.tool("dayleaf_get_goals", "Read daily, weekly or monthly goals for a period.", { kind: kindArg, date: dateArg }, async ({ kind, date }) => { try {
|
|
67
|
+
return text(await api.goals(kind || "daily", date ? resolveDate(date) : undefined));
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
return fail(e);
|
|
71
|
+
} });
|
|
72
|
+
server.tool("dayleaf_add_goal", "Add a goal to a daily/weekly/monthly list.", { text: z.string(), kind: kindArg, date: dateArg }, async ({ text: t, kind, date }) => { try {
|
|
73
|
+
return text(await api.addGoal(t, kind || "daily", date ? resolveDate(date) : undefined));
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
return fail(e);
|
|
77
|
+
} });
|
|
78
|
+
server.tool("dayleaf_set_goal", "Mark a goal done/undone or change its text.", { id: z.string(), kind: kindArg, date: dateArg, done: z.boolean().optional(), text: z.string().optional() }, async ({ id, kind, date, done, text: t }) => { try {
|
|
79
|
+
return text(await api.setGoal(id, { done, text: t }, kind || "daily", date ? resolveDate(date) : undefined));
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
return fail(e);
|
|
83
|
+
} });
|
|
84
|
+
const transport = new StdioServerTransport();
|
|
85
|
+
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dayleaf",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Headless Dayleaf: CLI and MCP server for reading and writing your Dayleaf journal on the Mac",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": { "dayleaf": "dist/cli.js", "dayleaf-mcp": "dist/mcp.js" },
|
|
8
|
+
"files": ["dist", "README.md"],
|
|
9
|
+
"scripts": { "build": "tsc -p tsconfig.json", "prepublishOnly": "npm run build" },
|
|
10
|
+
"engines": { "node": ">=18" },
|
|
11
|
+
"keywords": ["dayleaf", "journal", "diary", "mcp", "agent", "cli"],
|
|
12
|
+
"homepage": "https://getdayleaf.app/headless.html",
|
|
13
|
+
"dependencies": { "@modelcontextprotocol/sdk": "^1.12.0", "zod": "^3.23.8" },
|
|
14
|
+
"devDependencies": { "typescript": "^5.6.0", "@types/node": "^22.0.0" }
|
|
15
|
+
}
|