@rubytech/create-maxy-lite 0.1.3 → 0.1.5

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.
@@ -0,0 +1,100 @@
1
+ /* Public booking page (calendar-site skill). Clean single-column Calendly-style
2
+ layout. Self-contained — no shared tokens, since this ships to Cloudflare
3
+ Pages, not the admin shell. The skill may overlay brand colors. */
4
+ :root {
5
+ --bk-bg: #f6f6f4;
6
+ --bk-card: #ffffff;
7
+ --bk-ink: #1d1d1f;
8
+ --bk-muted: #6b6b70;
9
+ --bk-line: #e3e3e0;
10
+ --bk-accent: #4b6358;
11
+ --bk-accent-ink: #ffffff;
12
+ --bk-radius: 12px;
13
+ }
14
+
15
+ * { box-sizing: border-box; }
16
+
17
+ body {
18
+ margin: 0;
19
+ background: var(--bk-bg);
20
+ color: var(--bk-ink);
21
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
22
+ display: flex;
23
+ justify-content: center;
24
+ padding: 32px 16px;
25
+ }
26
+
27
+ .bk-card {
28
+ width: 100%;
29
+ max-width: 520px;
30
+ background: var(--bk-card);
31
+ border: 1px solid var(--bk-line);
32
+ border-radius: var(--bk-radius);
33
+ padding: 28px;
34
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
35
+ }
36
+
37
+ .bk-title { margin: 0 0 4px; font-size: 1.6rem; }
38
+ .bk-sub { margin: 0 0 20px; color: var(--bk-muted); }
39
+ .bk-status { margin: 8px 0; color: var(--bk-muted); }
40
+
41
+ .bk-day { margin-bottom: 20px; }
42
+ .bk-day-title { font-size: 0.95rem; margin: 0 0 8px; color: var(--bk-ink); }
43
+ .bk-day-slots { display: flex; flex-wrap: wrap; gap: 8px; }
44
+
45
+ .bk-slot {
46
+ border: 1px solid var(--bk-accent);
47
+ background: transparent;
48
+ color: var(--bk-accent);
49
+ border-radius: 8px;
50
+ padding: 8px 14px;
51
+ font-size: 0.9rem;
52
+ cursor: pointer;
53
+ }
54
+ .bk-slot:hover { background: var(--bk-accent); color: var(--bk-accent-ink); }
55
+
56
+ .bk-form { display: flex; flex-direction: column; gap: 14px; }
57
+ .bk-chosen { font-weight: 600; margin: 0 0 4px; }
58
+ .bk-label { display: flex; flex-direction: column; gap: 4px; font-size: 0.85rem; color: var(--bk-muted); }
59
+ .bk-input {
60
+ border: 1px solid var(--bk-line);
61
+ border-radius: 8px;
62
+ padding: 10px 12px;
63
+ font-size: 1rem;
64
+ color: var(--bk-ink);
65
+ font-family: inherit;
66
+ }
67
+ .bk-input:focus { outline: 2px solid var(--bk-accent); border-color: var(--bk-accent); }
68
+
69
+ /* honeypot — visually and from the layout removed, still in the DOM for bots */
70
+ .bk-hp {
71
+ position: absolute;
72
+ left: -9999px;
73
+ width: 1px;
74
+ height: 1px;
75
+ opacity: 0;
76
+ }
77
+
78
+ .bk-actions { display: flex; justify-content: space-between; gap: 12px; margin-top: 4px; }
79
+ .bk-back {
80
+ background: transparent;
81
+ border: 1px solid var(--bk-line);
82
+ border-radius: 8px;
83
+ padding: 10px 16px;
84
+ cursor: pointer;
85
+ color: var(--bk-muted);
86
+ }
87
+ .bk-submit {
88
+ background: var(--bk-accent);
89
+ color: var(--bk-accent-ink);
90
+ border: none;
91
+ border-radius: 8px;
92
+ padding: 10px 20px;
93
+ font-size: 1rem;
94
+ cursor: pointer;
95
+ }
96
+ .bk-submit:disabled { opacity: 0.6; cursor: default; }
97
+
98
+ .bk-done { text-align: center; padding: 16px 0; }
99
+ .bk-done-title { margin: 0 0 8px; font-size: 1.3rem; }
100
+ .bk-done-msg { color: var(--bk-muted); margin: 0; }
@@ -0,0 +1,202 @@
1
+ // Public booking page (calendar-site skill). Dependency-free.
2
+ // 1. Fetch the static availability config shipped with the page.
3
+ // 2. Compute open slots client-side: the weekly window minus a buffer, for the
4
+ // next two weeks. There is no live free/busy merge, so a slot can collide
5
+ // with an existing event; the device-side reconcile pass rejects a colliding
6
+ // booking when it pulls submissions into the vault.
7
+ // 3. Let the visitor pick a slot, collect name/email/note, and POST to the
8
+ // same-origin Pages Function (/api/book -> D1).
9
+
10
+ // Pure slot computation, exported for test and reused by the browser code below.
11
+ // availability: { timezone, durationMins, bufferMins, weekly: { mon: [["09:00","17:00"]], ... } }
12
+ // from: a Date marking "now". daysAhead: how many days forward to offer.
13
+ // Returns [{ start, end }] as timezone-naive ISO strings (YYYY-MM-DDTHH:MM:00).
14
+ function computeSlots(availability, from, daysAhead) {
15
+ var DAY_KEYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
16
+ var durationMins = availability.durationMins;
17
+ var stepMins = availability.durationMins + availability.bufferMins;
18
+ var weekly = availability.weekly || {};
19
+ var out = [];
20
+
21
+ function pad(n) { return (n < 10 ? '0' : '') + n; }
22
+ function toMinutes(hhmm) {
23
+ var parts = hhmm.split(':');
24
+ return parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10);
25
+ }
26
+ function naive(date, mins) {
27
+ var y = date.getFullYear();
28
+ var m = pad(date.getMonth() + 1);
29
+ var d = pad(date.getDate());
30
+ var hh = pad(Math.floor(mins / 60));
31
+ var mm = pad(mins % 60);
32
+ return y + '-' + m + '-' + d + 'T' + hh + ':' + mm + ':00';
33
+ }
34
+
35
+ for (var dayOffset = 0; dayOffset < daysAhead; dayOffset++) {
36
+ var day = new Date(from.getFullYear(), from.getMonth(), from.getDate() + dayOffset);
37
+ var windows = weekly[DAY_KEYS[day.getDay()]] || [];
38
+ for (var w = 0; w < windows.length; w++) {
39
+ var winStart = toMinutes(windows[w][0]);
40
+ var winEnd = toMinutes(windows[w][1]);
41
+ for (var t = winStart; t + durationMins <= winEnd; t += stepMins) {
42
+ var startStr = naive(day, t);
43
+ if (new Date(startStr) < from) continue; // drop past slots
44
+ out.push({ start: startStr, end: naive(day, t + durationMins) });
45
+ }
46
+ }
47
+ }
48
+ return out;
49
+ }
50
+
51
+ if (typeof module !== 'undefined' && module.exports) {
52
+ module.exports = { computeSlots };
53
+ }
54
+
55
+ (function () {
56
+ 'use strict';
57
+ if (typeof document === 'undefined') return; // running under Node for tests
58
+
59
+ var DAYS_AHEAD = 14;
60
+
61
+ var statusEl = document.getElementById('bk-status');
62
+ var slotsEl = document.getElementById('bk-slots');
63
+ var formEl = document.getElementById('bk-form');
64
+ var chosenEl = document.getElementById('bk-chosen');
65
+ var doneEl = document.getElementById('bk-done');
66
+ var doneMsg = document.getElementById('bk-done-msg');
67
+ var durationEl = document.getElementById('bk-duration');
68
+ var backBtn = document.getElementById('bk-back');
69
+
70
+ var chosenSlot = null;
71
+
72
+ function setStatus(msg) {
73
+ statusEl.textContent = msg || '';
74
+ }
75
+ function fmtDayHeading(d) {
76
+ return d.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' });
77
+ }
78
+ function fmtTime(d) {
79
+ return d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
80
+ }
81
+
82
+ function renderSlots(slots, durationMins) {
83
+ if (durationMins) {
84
+ durationEl.textContent = durationMins + ' minute meeting';
85
+ }
86
+ if (slots.length === 0) {
87
+ setStatus('No open times in the next two weeks. Please check back later.');
88
+ return;
89
+ }
90
+ setStatus('');
91
+
92
+ var byDay = {};
93
+ var order = [];
94
+ slots.forEach(function (s) {
95
+ var start = new Date(s.start);
96
+ var key = start.toDateString();
97
+ if (!byDay[key]) {
98
+ byDay[key] = [];
99
+ order.push(key);
100
+ }
101
+ byDay[key].push(s);
102
+ });
103
+
104
+ order.forEach(function (key) {
105
+ var group = document.createElement('div');
106
+ group.className = 'bk-day';
107
+ var h = document.createElement('h2');
108
+ h.className = 'bk-day-title';
109
+ h.textContent = fmtDayHeading(new Date(key));
110
+ group.appendChild(h);
111
+
112
+ var row = document.createElement('div');
113
+ row.className = 'bk-day-slots';
114
+ byDay[key].forEach(function (s) {
115
+ var btn = document.createElement('button');
116
+ btn.type = 'button';
117
+ btn.className = 'bk-slot';
118
+ btn.textContent = fmtTime(new Date(s.start));
119
+ btn.addEventListener('click', function () {
120
+ chooseSlot(s);
121
+ });
122
+ row.appendChild(btn);
123
+ });
124
+ group.appendChild(row);
125
+ slotsEl.appendChild(group);
126
+ });
127
+ }
128
+
129
+ function chooseSlot(slot) {
130
+ chosenSlot = slot;
131
+ var start = new Date(slot.start);
132
+ chosenEl.textContent = fmtDayHeading(start) + ' at ' + fmtTime(start);
133
+ slotsEl.hidden = true;
134
+ formEl.hidden = false;
135
+ }
136
+
137
+ backBtn.addEventListener('click', function () {
138
+ chosenSlot = null;
139
+ formEl.hidden = true;
140
+ slotsEl.hidden = false;
141
+ });
142
+
143
+ formEl.addEventListener('submit', function (e) {
144
+ e.preventDefault();
145
+ if (!chosenSlot) return;
146
+ var fd = new FormData(formEl);
147
+ var payload = {
148
+ slotStart: chosenSlot.start,
149
+ slotEnd: chosenSlot.end,
150
+ name: fd.get('name'),
151
+ email: fd.get('email'),
152
+ note: fd.get('note'),
153
+ company: fd.get('company'),
154
+ };
155
+ var submitBtn = formEl.querySelector('.bk-submit');
156
+ submitBtn.disabled = true;
157
+ submitBtn.textContent = 'Booking…';
158
+
159
+ fetch('/api/book', {
160
+ method: 'POST',
161
+ headers: { 'content-type': 'application/json' },
162
+ body: JSON.stringify(payload),
163
+ })
164
+ .then(function (res) {
165
+ return res.json().then(function (body) {
166
+ return { ok: res.ok, body: body };
167
+ });
168
+ })
169
+ .then(function (r) {
170
+ if (!r.ok || !r.body.ok) {
171
+ throw new Error((r.body && r.body.error) || 'Booking failed');
172
+ }
173
+ formEl.hidden = true;
174
+ doneEl.hidden = false;
175
+ var start = new Date(chosenSlot.start);
176
+ doneMsg.textContent = 'Confirmed for ' + fmtDayHeading(start) + ' at ' + fmtTime(start) + '. A confirmation will follow by email.';
177
+ })
178
+ .catch(function (err) {
179
+ submitBtn.disabled = false;
180
+ submitBtn.textContent = 'Confirm booking';
181
+ setStatus('Sorry, that did not go through: ' + err.message + '. Please try again.');
182
+ });
183
+ });
184
+
185
+ function load() {
186
+ setStatus('Loading available times…');
187
+ fetch('/availability.json')
188
+ .then(function (res) {
189
+ if (!res.ok) throw new Error('HTTP ' + res.status);
190
+ return res.json();
191
+ })
192
+ .then(function (availability) {
193
+ var slots = computeSlots(availability, new Date(), DAYS_AHEAD);
194
+ renderSlots(slots, availability.durationMins);
195
+ })
196
+ .catch(function () {
197
+ setStatus('Could not load available times right now. Please refresh in a moment.');
198
+ });
199
+ }
200
+
201
+ load();
202
+ })();
@@ -0,0 +1,44 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Book a time</title>
7
+ <link rel="stylesheet" href="/booking.css" />
8
+ </head>
9
+ <body>
10
+ <main class="bk-card">
11
+ <h1 class="bk-title">Book a time</h1>
12
+ <p class="bk-sub" id="bk-duration"></p>
13
+
14
+ <div id="bk-status" class="bk-status" role="status"></div>
15
+
16
+ <section id="bk-slots" class="bk-slots" aria-label="Available times"></section>
17
+
18
+ <form id="bk-form" class="bk-form" hidden>
19
+ <p class="bk-chosen" id="bk-chosen"></p>
20
+ <label class="bk-label">Name
21
+ <input class="bk-input" type="text" name="name" required autocomplete="name" />
22
+ </label>
23
+ <label class="bk-label">Email
24
+ <input class="bk-input" type="email" name="email" required autocomplete="email" />
25
+ </label>
26
+ <label class="bk-label">Note (optional)
27
+ <textarea class="bk-input" name="note" rows="3"></textarea>
28
+ </label>
29
+ <!-- honeypot: hidden from people, tempting to bots -->
30
+ <input class="bk-hp" type="text" name="company" tabindex="-1" autocomplete="off" aria-hidden="true" />
31
+ <div class="bk-actions">
32
+ <button class="bk-back" type="button" id="bk-back">Back</button>
33
+ <button class="bk-submit" type="submit">Confirm booking</button>
34
+ </div>
35
+ </form>
36
+
37
+ <div id="bk-done" class="bk-done" hidden>
38
+ <h2 class="bk-done-title">You are booked</h2>
39
+ <p id="bk-done-msg" class="bk-done-msg"></p>
40
+ </div>
41
+ </main>
42
+ <script src="/booking.js"></script>
43
+ </body>
44
+ </html>
@@ -0,0 +1,19 @@
1
+ -- D1 schema for the booking site (calendar-site skill).
2
+ -- One row per public booking submission. Captured at the edge so a submission
3
+ -- survives while the device is offline or asleep; the calendar-site reconcile
4
+ -- pass later reads accepted, unswept rows and writes each into the vault as an
5
+ -- Event (then marks the row swept).
6
+ CREATE TABLE IF NOT EXISTS bookings (
7
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
8
+ bookingId TEXT UNIQUE NOT NULL,
9
+ slotStart TEXT NOT NULL,
10
+ slotEnd TEXT NOT NULL,
11
+ name TEXT NOT NULL,
12
+ email TEXT NOT NULL,
13
+ note TEXT,
14
+ status TEXT NOT NULL DEFAULT 'accepted',
15
+ createdAt TEXT NOT NULL,
16
+ swept INTEGER NOT NULL DEFAULT 0
17
+ );
18
+
19
+ CREATE INDEX IF NOT EXISTS bookings_unswept ON bookings (status, swept);
@@ -0,0 +1,14 @@
1
+ # Cloudflare Pages config for the booking site (calendar-site skill).
2
+ # The skill fills the placeholders at assemble time:
3
+ # __PROJECT_NAME__ the Pages project name
4
+ # __D1_DATABASE_NAME__ the D1 database name
5
+ # __D1_DATABASE_ID__ the id printed by `wrangler d1 create`
6
+ name = "__PROJECT_NAME__"
7
+ pages_build_output_dir = "public"
8
+ compatibility_date = "2024-01-01"
9
+ compatibility_flags = ["nodejs_compat"]
10
+
11
+ [[d1_databases]]
12
+ binding = "DB"
13
+ database_name = "__D1_DATABASE_NAME__"
14
+ database_id = "__D1_DATABASE_ID__"
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: contacts
3
+ description: Create, look up, and update people and organizations in the vault. Use when the user mentions a person or company to remember, asks who someone is, wants a contact's email or phone, links someone to where they work, or asks to update a contact's details. Owns the Person and Organization entities of the maxy-lite SCHEMA.
4
+ ---
5
+
6
+ # Contacts
7
+
8
+ The address book lives in the vault as one markdown file per contact. A **Person** is a file in `people/`; an **Organization** is a file in `organizations/`. Each file is YAML frontmatter (the typed fields) plus a markdown body for free notes about the contact. The authoritative field list is [`SCHEMA.md`](../../schema/SCHEMA.md) under *Contacts*. Read it before writing fields you are unsure of; the validator parses the same tables.
9
+
10
+ ## Person (`people/<Full Name>.md`)
11
+
12
+ Required: `type: person` and `name` (the full display name). Optional, all from vCard: `firstName`, `lastName`, `nickname`, `emails` (a list), `tels` (a list), `title`, `role`, `address`, `birthday` (`YYYY-MM-DD`), `anniversary`, `urls` (a list), `categories` (a list), `area`. Set only the fields you actually know; do not invent values to fill a field.
13
+
14
+ ```yaml
15
+ ---
16
+ type: person
17
+ name: Jane Doe
18
+ firstName: Jane
19
+ lastName: Doe
20
+ emails:
21
+ - jane@acme.example
22
+ tels:
23
+ - "+44 7700 900000"
24
+ org: "[[Acme Ltd]]"
25
+ title: Head of Design
26
+ area: work
27
+ ---
28
+ Met at the London conference. Prefers email over phone.
29
+ ```
30
+
31
+ ## Organization (`organizations/<Name>.md`)
32
+
33
+ Required: `type: organization` and `name`. Optional: `domain`, `website`, `industry`, `companyType`, `employees` (number), `revenue` (number), `phone`, `city`, `state`, `country`, `area`.
34
+
35
+ ## Linking a person to where they work
36
+
37
+ A Person's `org` field is a single link to an Organization file; an Organization's `members` field is a list of links to Person files. A link is the target's basename in wikilink form, e.g. `org: "[[Acme Ltd]]"`. The Organization file must already exist, or the link dangles and fails. When you link a person to an organization that is not in the vault yet, create the Organization file first, then set `org`.
38
+
39
+ ## Looking someone up
40
+
41
+ The vault is the index. To find a contact, Grep the frontmatter, not your memory:
42
+
43
+ - by name: `grep -rl "name: Jane" people/`
44
+ - by email: `grep -rl "jane@acme.example" people/`
45
+ - everyone at a company: `grep -rl "\[\[Acme Ltd\]\]" people/` (people whose `org` points at it)
46
+
47
+ Read the matched file for the full record, and follow its `org` link to the organization. Keep each filename unique: the validator resolves links by basename, so two `Jane Doe.md` files in different folders cannot be told apart.
48
+
49
+ ## After every write, validate
50
+
51
+ The SCHEMA is enforced by a deterministic validator, not by convention. After creating or editing any contact file, run it over the vault:
52
+
53
+ ```sh
54
+ maxy-lite-validate "$HOME/maxy"
55
+ ```
56
+
57
+ It prints one `[lite-schema] op=validate ... ok=<bool>` line per file and exits 0 only when every file conforms. If a line names the file you just wrote with `ok=false`, the bracketed error names the violation: `name:missing` (a required field is absent), `field:type` (wrong value type), `area:area` (an area outside the controlled vocabulary), `org:dangling` (the linked organization file does not exist), `org:target` (the link points at a file of the wrong type). Fix the named field and re-run until that file is `ok=true`. Never leave a contact you wrote in a failing state.
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: memory
3
+ description: Remember and recall facts across sessions. Use when the user says to remember something, asks what you know about a person or topic, refers to something told to you earlier, or when you need to recover context at the start of a session. The vault is the memory; this skill is the convention for writing facts into it and reading them back.
4
+ ---
5
+
6
+ # Memory
7
+
8
+ There is no separate memory store. The vault is the memory: every fact you need to keep lives as frontmatter or body text in a vault file, and recall is reading those files back. This skill is thin on purpose. It codifies two things: where a fact goes when you learn it, and how to find a fact when you need it. The entity field lists are in [`SCHEMA.md`](../../schema/SCHEMA.md); the `contacts`, `work`, and `projects` skills own their respective writes.
9
+
10
+ ## Where a new fact goes
11
+
12
+ A fact almost always belongs to something that already has a file. Put it there, not in a parallel store:
13
+
14
+ - A fact **about a person or organization** (a preference, a relationship, a detail) goes in the body of that contact's file, or as a typed field if the SCHEMA has one for it (a new email goes in `emails`, a birthday in `birthday`). Edit the existing file.
15
+ - A fact that **is** a thing the SCHEMA already models (a task to do, an event, a document) is created with the matching skill, not written as loose memory.
16
+ - A standalone fact that belongs to **no existing entity** becomes a **Note**: a file in `activities/` with `type: note`, a `title`, and the fact in the markdown body. Link it to whatever it concerns with `about` so it is reachable by traversal.
17
+
18
+ ```yaml
19
+ ---
20
+ type: note
21
+ title: Jane wine preference
22
+ about: "[[Jane Doe]]"
23
+ area: personal
24
+ ---
25
+ Jane prefers Rioja. Mentioned at the conference dinner.
26
+ ```
27
+
28
+ Prefer enriching an existing file over creating a new note. A note is for a fact with no better home.
29
+
30
+ ## How to recall
31
+
32
+ Recall is search over the vault, in this order:
33
+
34
+ 1. **Grep the frontmatter and body** for the term: `grep -rl "Rioja" .` finds every file mentioning it; `grep -rl "name: Jane" people/` finds the contact.
35
+ 2. **Read the matched file** for the full record.
36
+ 3. **Follow the links** to pull in connected context: a note's `about`, a person's `org`, a task's `project`. Traversal is following basenames between files.
37
+
38
+ At the start of a session with no context, recall is the same move: Grep for the names or topics the user raises, read what comes back, and follow the links. Nothing is remembered that is not in a file, so if a fact is not found by Grep it was never written; write it.
39
+
40
+ ## After writing a fact, validate
41
+
42
+ When a fact is written as a new note or by editing an entity file, run the deterministic validator over the vault:
43
+
44
+ ```sh
45
+ maxy-lite-validate "$HOME/maxy"
46
+ ```
47
+
48
+ It exits 0 only when every file conforms. If a line names the note you just wrote with `ok=false`, the bracketed error names the violation: `title:missing` (a note needs a title), `area:area` (an area outside the controlled vocabulary), `about:dangling` (the linked file does not exist). Fix the named field and re-run until that file is `ok=true`. A fact left in a failing file is a fact that will not be trusted on recall.
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: projects
3
+ description: Create and track multi-step efforts in the vault and tie tasks, events, and documents to them. Use when the user describes something with several moving parts (a holiday, a house move, a renovation, a launch), asks to start a project, wants to see everything related to one, or wants to file a task or event under a project. Owns the Project entity of the maxy-lite SCHEMA.
4
+ ---
5
+
6
+ # Projects
7
+
8
+ A **Project** is a multi-step effort that ties together the tasks, events, and documents it involves. It is one file in `projects/`: YAML frontmatter plus a markdown body for the plan, status notes, and links. The authoritative field list is [`SCHEMA.md`](../../schema/SCHEMA.md) under *Projects*.
9
+
10
+ ## Project (`projects/<Name>.md`)
11
+
12
+ Required: `type: project`, `name`, and `area` (one of the controlled areas in the SCHEMA). Optional: `status` (`active` / `done` / `someday`), `startDate`, `dueDate` (`YYYY-MM-DD`), `about` (a single link to anything the project centres on). `area` is required for a project, unlike for a contact or task, so always set it.
13
+
14
+ ```yaml
15
+ ---
16
+ type: project
17
+ name: House Move
18
+ area: home
19
+ status: active
20
+ startDate: 2026-06-01
21
+ dueDate: 2026-09-30
22
+ ---
23
+ Moving from the London flat to the house in Bristol. Survey, mortgage, packers, schools.
24
+ ```
25
+
26
+ ## Tying tasks and events to a project
27
+
28
+ A project does not list its members in its own frontmatter. Instead each Task or Event carries a `project` link back to the project. To file an item under a project, edit the item and set its `project` field to the project's basename in wikilink form, e.g. in a task file `project: "[[House Move]]"`. The project file must exist first. The `work` skill owns the Task fields; this skill owns the Project file.
29
+
30
+ ## Seeing everything in a project
31
+
32
+ The links point inward, so Grep for the project's name across the activity and calendar folders:
33
+
34
+ - tasks in a project: `grep -rl "\[\[House Move\]\]" activities/`
35
+ - events in a project: `grep -rl "\[\[House Move\]\]" calendar/`
36
+
37
+ Read the matched files for the detail. To close a project, edit its `status` to `done`; do not delete the file.
38
+
39
+ ## After every write, validate
40
+
41
+ After creating or editing a project file (or after setting a `project` link on a task or event), run the deterministic validator over the vault:
42
+
43
+ ```sh
44
+ maxy-lite-validate "$HOME/maxy"
45
+ ```
46
+
47
+ It exits 0 only when every file conforms. If a line names the project you just wrote with `ok=false`, the bracketed error names the violation: `name:missing` or `area:missing` (a required field is absent), `area:area` (an area outside the controlled vocabulary), `about:dangling` (the linked file does not exist), `startDate:type` or `dueDate:type` (a malformed date). Fix the named field and re-run until that file is `ok=true`. Never leave a project you wrote in a failing state.
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: publish-site
3
+ description: Put a built site or page online and hand back a live link. Use when the user says publish, host, put this online, make it live, or share a link to a deck or page. Routes to `site-deploy`, which deploys to Cloudflare Pages so the page stays up with the phone offline.
4
+ ---
5
+
6
+ # Publish a site
7
+
8
+ This is the intent router for "make it live". When the user wants a page online, you confirm what to publish and where the files are, then hand off to [`site-deploy`](../site-deploy/SKILL.md), which does the actual Cloudflare Pages deploy and returns the URL.
9
+
10
+ ## What you need before routing
11
+
12
+ - A folder of static files ready to go: an [`slides`](../slides/SKILL.md) deck (the `.html` file plus its `media/` folder, if any), a landing page, or any HTML tree. If the site is not built yet, build it first; this skill does not generate content.
13
+ - A project slug (lowercase, hyphenated). Offer one derived from the site's name and confirm it with the user, because the slug is the live address.
14
+
15
+ ## Where it gets hosted
16
+
17
+ Cloudflare Pages is the lite default: the page is hosted on Cloudflare's edge and stays reachable when the phone is off. There is one hosting path in lite, so there is nothing to choose between; serving a page from the device itself is not part of lite. Hand the folder and the slug to `site-deploy` and relay the live URL it returns.
18
+
19
+ ## After it is live
20
+
21
+ `site-deploy` gates "done" on a live `200` with the right content and gives back the URL. Surface that URL to the user as the result. If the deploy fails on authentication, the fix is `wrangler login`, per `site-deploy`.
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: scheduling
3
+ description: Create, update, and cancel calendar events as vault files, and sync them to Google Calendar. This is the skill behind "put a meeting on my calendar", "schedule a call", "move that appointment", "cancel the meeting", and "add this to Google Calendar". The vault Event file is the source of truth; the Google Calendar connector is pushed to after the file is written and validated. A push that fails leaves a correct local Event, never lost.
4
+ ---
5
+
6
+ # Schedule, update, and cancel events
7
+
8
+ An event in the vault is one file under `calendar/`, shaped by the Event entity in the SCHEMA (`schema/SCHEMA.md`). This skill writes that file, validates it, then pushes the change to Google Calendar. The order is fixed and it matters: the vault is authoritative, so the file is written and proven conformant before anything leaves the device.
9
+
10
+ ## The Event file
11
+
12
+ Write `calendar/<summary>.md`. The frontmatter carries the typed fields; the markdown body holds the meeting notes, agenda, or call notes (free prose, never in frontmatter).
13
+
14
+ ```
15
+ ---
16
+ type: event
17
+ summary: Intro call with Acme
18
+ start: 2026-07-01T15:00:00
19
+ end: 2026-07-01T15:30:00
20
+ location: Google Meet
21
+ attendees:
22
+ - "[[Jane Doe]]"
23
+ organizer: "[[John Smith]]"
24
+ project: "[[Acme Onboarding]]"
25
+ recurrence: FREQ=WEEKLY;BYDAY=TU
26
+ status: CONFIRMED
27
+ area: work
28
+ ---
29
+
30
+ Agenda and notes live here.
31
+ ```
32
+
33
+ Field rules, taken straight from the SCHEMA so a write never drifts:
34
+
35
+ - `type` is always `event` and `summary` and `start` are required. Everything else is optional.
36
+ - `start` and `end` are an ISO date (`2026-07-01`) or date-time (`2026-07-01T15:00:00`).
37
+ - `attendees` is a list of `[[Person]]` links and `organizer` is a single `[[Person]]` link. Each link resolves by basename to a file in `people/`; create the Person first if it does not exist, so the link is not dangling.
38
+ - `project` is a single `[[Project]]` link resolving into `projects/`.
39
+ - `recurrence` is a raw iCalendar `RRULE` string (for example `FREQ=WEEKLY;BYDAY=TU`). It is stored only. This skill does not expand recurrences or fire anything on a schedule.
40
+ - `status` mirrors iCalendar: `CONFIRMED`, `TENTATIVE`, or `CANCELLED`.
41
+ - `area` is one of the SCHEMA Areas, when the event belongs to one.
42
+
43
+ ## The fixed order: write, validate, then push
44
+
45
+ Every create, update, and cancel follows the same three steps in this order.
46
+
47
+ 1. Write the Event file to `calendar/`.
48
+ 2. Run the validator over the vault and read its exit code:
49
+
50
+ ```bash
51
+ maxy-lite-validate <vault>
52
+ ```
53
+
54
+ Exit 0 means every file conforms. A non-zero exit with `ok=false` on the Event you just wrote means the file drifted from the SCHEMA (a missing required field, a bad area, a dangling `[[Person]]` link). Fix the file and re-run until it exits 0. Never leave a non-conformant Event.
55
+
56
+ 3. Only after the validator exits 0, push the event to the Google Calendar connector: create the calendar event for a new file, update it for an edited file, cancel it for a cancellation. Use whatever the Google Calendar connector exposes for that action; do not hardcode a tool name.
57
+
58
+ This order is what "vault-authoritative" means here. The local file is correct and proven before the network is touched.
59
+
60
+ ## When the push fails
61
+
62
+ A Google Calendar push can fail while the local Event is perfectly valid. That is a sync problem, not data loss. Keep the local file exactly as written, report that the calendar push did not land and why, and offer to retry the push. Do not roll back, do not delete the Event, and do not treat it as a schema problem. The two failure modes are distinct:
63
+
64
+ - Validator non-zero on the Event: schema drift. Fix the file.
65
+ - Validator 0 but the push returned an error: sync issue. The file is correct; retry the push.
66
+
67
+ ## Update and cancel
68
+
69
+ - Update: edit the Event file in place, run the validator to 0, then push the update to Google Calendar.
70
+ - Cancel: set `status: CANCELLED` in the frontmatter, run the validator to 0, then push the cancellation to Google Calendar. Keep the file. A cancelled event is a retained record with a cancelled status, not a deleted file.
71
+
72
+ ## Conflicts
73
+
74
+ The vault wins. If the same event differs between the vault and Google Calendar, the vault file is correct and the connector is brought into line with it on the next push. Richer two-way merge is out of scope for this skill.