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 CHANGED
@@ -1,3 +1,125 @@
1
- # Temporary Holding Version
1
+ # dayleaf — Headless Dayleaf
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }