@rubytech/create-maxy-lite 0.1.4 → 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,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.
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: site-deploy
3
+ description: Deploy a folder of static HTML and assets to Cloudflare Pages and return a live URL that stays up with the phone offline. Use when a site or page is built and ready to go live, or when a live site is stale and needs redeploying. Runs `wrangler pages deploy` over Bash; the page is edge-hosted, not served from the device. This is the implementer behind `publish-site`.
4
+ ---
5
+
6
+ # Deploy a site to Cloudflare Pages
7
+
8
+ You take one folder of static files (an [`slides`](../slides/SKILL.md) deck, a landing page, any HTML tree) and put it live on Cloudflare's edge. Cloudflare hosts the build, so the page stays reachable when the phone is off. The live URL is the deliverable.
9
+
10
+ The mechanism is the `wrangler` CLI, run over Bash. It is already available through Node; invoke it with `npx wrangler` if it is not on the path. Nothing here uses an MCP connector: the Cloudflare connector cannot deploy Pages, and its login is not reachable from the shell.
11
+
12
+ ## Auth, once
13
+
14
+ `wrangler` needs to be signed in to the Cloudflare account. Two ways, in order of preference:
15
+
16
+ - `npx wrangler login` opens the Cloudflare OAuth page in the browser and persists the session. Do this once; later deploys reuse it. This is the on-device default.
17
+ - Or set `CLOUDFLARE_API_TOKEN` (a token with **Account · Cloudflare Pages · Edit**) and `CLOUDFLARE_ACCOUNT_ID` in the environment. Never write the token into the project folder, never commit it, never print it.
18
+
19
+ If neither is present, `wrangler` reports it is not authenticated. Stop and tell the user to run `wrangler login`; do not guess a token.
20
+
21
+ ## Deploy
22
+
23
+ Pick a project slug (lowercase, hyphenated, derived from the site's name). The first deploy of a new slug creates the project.
24
+
25
+ ```bash
26
+ npx wrangler pages deploy <output-dir> --project-name <slug> --branch=main
27
+ ```
28
+
29
+ `--branch=main` deploys to the production alias `https://<slug>.pages.dev`; any other branch name produces a preview URL instead. `wrangler` prints the deployment URL. Surface the operation as verb plus target ("deploying Pages project `<slug>`"); the token value never appears in chat or stdout.
30
+
31
+ ## Custom domain (optional)
32
+
33
+ By default the site answers on `<slug>.pages.dev`, which is enough for most uses. To serve it on the user's own domain, attach the domain to the Pages project in the Cloudflare dashboard (**Workers & Pages → the project → Custom domains → Set up a domain**). When the zone is on the same Cloudflare account, Cloudflare provisions the certificate and the DNS record itself. A custom domain is optional polish; do not block the deploy on it.
34
+
35
+ ## Done is a live 200, not a clean exit
36
+
37
+ A successful `wrangler` run is not the finish line. The finish line is the live URL serving the new content. Check it, cache-busted, and read the first line:
38
+
39
+ ```bash
40
+ curl -s "https://<slug>.pages.dev/?cb=$(date +%s)" -D - -o /tmp/site.html | head -1
41
+ ```
42
+
43
+ Done holds only when all are true:
44
+
45
+ - the status line is `HTTP/2 200` (or `HTTP/1.1 200 OK`); a `522` is the certificate window on a fresh custom domain, so wait and re-curl rather than calling it done,
46
+ - the body carries a content marker you can name in advance (a headline or title from this site), not a leftover from whatever the folder was copied from.
47
+
48
+ Any other status, or a missing or stale marker, blocks done and is reported with the exact failing URL. Record the live URL and this check in the result so it can be re-run later to confirm the site is still up without reading the source.
49
+
50
+ ## Out of scope
51
+
52
+ Building the HTML (that is [`slides`](../slides/SKILL.md) or whatever produced the folder), form-to-database capture, and serving from the device over a tunnel. The lite default is Pages, which survives the phone going offline; device-origin hosting is not part of lite.
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: slides
3
+ description: Build a slide deck or a single web page as one self-contained HTML file from vault content. Use when the user asks for a deck, slides, a presentation, a pitch, a talk, a one-pager, or a landing page, or wants to turn notes, a project, or a meeting into something they can present or publish. Produces one HTML file with inline CSS and JS, no build step. Pair with `publish-site` / `site-deploy` to put it online.
4
+ ---
5
+
6
+ # Slides and pages
7
+
8
+ You produce **one self-contained HTML file**: inline CSS and JS, no build step, no dependencies beyond the Inter web font. It opens straight in a browser, navigates with arrow keys, space, or swipe, and exports to PDF with `P`. To put it online, hand the file to [`publish-site`](../publish-site/SKILL.md).
9
+
10
+ The starting point is [`deck.html`](deck.html) in this folder. Copy it, then change only the content. It carries the design tokens, the component library (21 components, indexed in a comment at the top of the file), the headline pattern, and the keyboard/swipe/PDF behaviour. Stay on its tokens; do not invent a new colour scheme or font.
11
+
12
+ ## Use what the user gave you
13
+
14
+ The user's message and the vault are the brief. Pull the content from where it already lives: a Project file and its linked Tasks, a set of Notes, a meeting, a person or organization. Read those files, do not re-ask for what the vault already holds. Only ask once, in one short question, if you genuinely have no idea what the deck is about.
15
+
16
+ ## Pick the shape, then outline
17
+
18
+ Most decks fit the six-beat story arc. Choose the timing to the format; the proportions hold.
19
+
20
+ | Beat | ~Share | Purpose |
21
+ |------|--------|---------|
22
+ | Open | 10% | Hook the room with a confession, a contradiction, or a surprising fact |
23
+ | The world before | 15% | The status quo the audience already lives in, to build recognition |
24
+ | The turn | 15% | Name the one thing that changed, cleanly |
25
+ | The evidence | 40% | Three to five concrete proof points: pain → action → result |
26
+ | The honest part | 15% | The caveat, the cost, the open question; this earns trust |
27
+ | Close | 5% | Land one line |
28
+
29
+ For a pitch lean into problem → market → product → traction → ask; for a one-pager or landing page drop the navigation idea and write a single long scrolling section. The arc is a default, not a cage.
30
+
31
+ Write the slide-by-slide outline first (number, component, headline, which beat) and show it to the user before you write HTML. Get a nod, then build.
32
+
33
+ ## Write the HTML
34
+
35
+ Copy the matching component from `deck.html` and swap the text. The headline pattern is the visual identity: a bold anchor phrase plus a dim extension that fades.
36
+
37
+ ```html
38
+ <h1>Anchor. <span class="dim">Extension that fades.</span></h1>
39
+ ```
40
+
41
+ One dark slide per deck at most; it loses its weight if every slide is dark. Keep claims true to the vault; do not invent numbers to fill a stat grid. Drop images into a `media/` folder beside the HTML and reference them with relative paths.
42
+
43
+ ## When the user wants it online
44
+
45
+ A deck or page is a static HTML file, so it deploys to Cloudflare Pages like any site. When the user asks to publish, host, or share a live link, hand the file (and its `media/` folder, if any) to [`publish-site`](../publish-site/SKILL.md), which routes to [`site-deploy`](../site-deploy/SKILL.md). The result is an edge-hosted URL that stays live with the phone offline.