@rubytech/create-maxy-lite 0.1.4 → 0.1.6

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.
Files changed (50) hide show
  1. package/index.mjs +43 -22
  2. package/lib/healthcheck.mjs +60 -19
  3. package/lib/orchestrate.mjs +32 -11
  4. package/lib/paths.mjs +73 -0
  5. package/package.json +1 -1
  6. package/payload/package.json +2 -1
  7. package/payload/skills/README.md +26 -0
  8. package/payload/skills/admin/datetime/SKILL.md +147 -0
  9. package/payload/skills/admin/session-management/SKILL.md +39 -0
  10. package/payload/skills/admin/upgrade/SKILL.md +32 -0
  11. package/payload/skills/browser/SKILL.md +60 -0
  12. package/payload/skills/browser/scripts/cdp.mjs +134 -0
  13. package/payload/skills/browser/scripts/pdf.mjs +38 -0
  14. package/payload/skills/browser/scripts/render.mjs +43 -0
  15. package/payload/skills/browser/scripts/screenshot.mjs +52 -0
  16. package/payload/skills/business-assistant/SKILL.md +110 -0
  17. package/payload/skills/calendar-site/SKILL.md +71 -0
  18. package/payload/skills/calendar-site/template/availability.json +14 -0
  19. package/payload/skills/calendar-site/template/functions/api/book.ts +112 -0
  20. package/payload/skills/calendar-site/template/public/booking.css +100 -0
  21. package/payload/skills/calendar-site/template/public/booking.js +202 -0
  22. package/payload/skills/calendar-site/template/public/index.html +44 -0
  23. package/payload/skills/calendar-site/template/schema.sql +19 -0
  24. package/payload/skills/calendar-site/template/wrangler.toml +14 -0
  25. package/payload/skills/contacts/SKILL.md +57 -0
  26. package/payload/skills/deep-research/SKILL.md +70 -0
  27. package/payload/skills/deep-research/references/citation-styles.md +52 -0
  28. package/payload/skills/deep-research/references/research-modes.md +22 -0
  29. package/payload/skills/deep-research/references/search-strategy.md +24 -0
  30. package/payload/skills/docs/SKILL.md +23 -0
  31. package/payload/skills/docs/references/capability-map.md +25 -0
  32. package/payload/skills/docs/references/getting-started.md +29 -0
  33. package/payload/skills/docs/references/vault-model.md +40 -0
  34. package/payload/skills/email-composition/SKILL.md +107 -0
  35. package/payload/skills/memory/SKILL.md +48 -0
  36. package/payload/skills/projects/SKILL.md +47 -0
  37. package/payload/skills/publish-site/SKILL.md +21 -0
  38. package/payload/skills/replicate/SKILL.md +63 -0
  39. package/payload/skills/replicate/scripts/replicate-image.mjs +131 -0
  40. package/payload/skills/scheduling/SKILL.md +74 -0
  41. package/payload/skills/site-deploy/SKILL.md +52 -0
  42. package/payload/skills/slides/SKILL.md +45 -0
  43. package/payload/skills/slides/deck.html +1359 -0
  44. package/payload/skills/url-get/SKILL.md +48 -0
  45. package/payload/skills/url-get/scripts/url-get.mjs +93 -0
  46. package/payload/skills/work/SKILL.md +49 -0
  47. package/payload/webchat/inject-line.mjs +11 -0
  48. package/payload/webchat/package.json +2 -1
  49. package/payload/webchat/request-handler.mjs +62 -0
  50. package/payload/webchat/server.mjs +31 -31
@@ -0,0 +1,112 @@
1
+ // Booking capture Pages Function (calendar-site skill).
2
+ //
3
+ // Served at POST /api/book on the booking domain. Writes the submission
4
+ // straight to D1 so capture survives while the device is offline or asleep —
5
+ // the calendar-site reconcile pass turns accepted rows into vault Events later.
6
+ // Emits the [calendar-booking] submit + d1-write lifeline (Cloudflare Pages
7
+ // captures console output).
8
+ //
9
+ // The handler is split so its logic is verifiable without the Pages runtime:
10
+ // processBooking takes an injected DB + logger and returns the HTTP shape.
11
+
12
+ interface D1Result {
13
+ meta?: { changes?: number }
14
+ }
15
+ interface D1PreparedStatement {
16
+ bind: (...values: unknown[]) => D1PreparedStatement
17
+ run: () => Promise<D1Result>
18
+ }
19
+ interface D1Database {
20
+ prepare: (query: string) => D1PreparedStatement
21
+ }
22
+ interface BookingEnv {
23
+ DB: D1Database
24
+ }
25
+
26
+ interface BookingBody {
27
+ slotStart?: unknown
28
+ slotEnd?: unknown
29
+ name?: unknown
30
+ email?: unknown
31
+ note?: unknown
32
+ company?: unknown // honeypot — real users never fill this
33
+ }
34
+
35
+ type Logger = (line: string) => void
36
+
37
+ function isNonEmptyString(v: unknown): v is string {
38
+ return typeof v === 'string' && v.trim().length > 0
39
+ }
40
+
41
+ // Field caps so a malicious POST cannot store an unbounded blob in D1.
42
+ const MAX = { name: 200, email: 320, note: 2000, slot: 40 }
43
+
44
+ function isIsoTimestamp(v: unknown): v is string {
45
+ return typeof v === 'string' && v.length <= MAX.slot && !Number.isNaN(Date.parse(v))
46
+ }
47
+
48
+ export async function processBooking(
49
+ body: BookingBody,
50
+ env: BookingEnv,
51
+ log: Logger,
52
+ newId: () => string,
53
+ ): Promise<{ status: number; payload: Record<string, unknown> }> {
54
+ // Honeypot: a populated `company` field means a bot. Return ok (so the bot
55
+ // sees success) but write nothing.
56
+ if (isNonEmptyString(body.company)) {
57
+ return { status: 200, payload: { ok: true } }
58
+ }
59
+
60
+ if (
61
+ !isIsoTimestamp(body.slotStart) ||
62
+ !isIsoTimestamp(body.slotEnd) ||
63
+ !isNonEmptyString(body.name) ||
64
+ !isNonEmptyString(body.email)
65
+ ) {
66
+ return { status: 400, payload: { ok: false, error: 'name, email, and ISO slotStart/slotEnd are required' } }
67
+ }
68
+ if (body.name.length > MAX.name || body.email.length > MAX.email) {
69
+ return { status: 400, payload: { ok: false, error: 'name or email too long' } }
70
+ }
71
+
72
+ const bookingId = newId()
73
+ const note = isNonEmptyString(body.note) ? body.note.slice(0, MAX.note) : ''
74
+ const createdAt = new Date().toISOString()
75
+ log(`[calendar-booking] op=submit bookingId=${bookingId} slotStart=${body.slotStart} email=${body.email}`)
76
+
77
+ const result = await env.DB.prepare(
78
+ `INSERT INTO bookings (bookingId, slotStart, slotEnd, name, email, note, status, createdAt, swept)
79
+ VALUES (?, ?, ?, ?, ?, ?, 'accepted', ?, 0)`,
80
+ )
81
+ .bind(bookingId, body.slotStart, body.slotEnd, body.name, body.email, note, createdAt)
82
+ .run()
83
+
84
+ const rowsWritten = result.meta?.changes ?? 0
85
+ log(`[calendar-booking] op=d1-write bookingId=${bookingId} rowsWritten=${rowsWritten}`)
86
+
87
+ if (rowsWritten < 1) {
88
+ return { status: 500, payload: { ok: false, error: 'capture failed' } }
89
+ }
90
+ return { status: 200, payload: { ok: true, bookingId } }
91
+ }
92
+
93
+ interface PagesContext {
94
+ request: Request
95
+ env: BookingEnv
96
+ }
97
+
98
+ export async function onRequestPost(context: PagesContext): Promise<Response> {
99
+ let body: BookingBody
100
+ try {
101
+ body = (await context.request.json()) as BookingBody
102
+ } catch {
103
+ return Response.json({ ok: false, error: 'invalid JSON' }, { status: 400 })
104
+ }
105
+ const { status, payload } = await processBooking(
106
+ body,
107
+ context.env,
108
+ (line) => console.log(line),
109
+ () => crypto.randomUUID(),
110
+ )
111
+ return Response.json(payload, { status })
112
+ }
@@ -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,70 @@
1
+ ---
2
+ name: deep-research
3
+ description: >
4
+ Use when the owner asks to research a topic, find current information, compare
5
+ options, review literature, fact-check a claim, or answer any question that
6
+ benefits from live web sources. Trigger phrases: "research", "find out",
7
+ "what's the latest on", "compare X and Y", "who is", "look into", "fact-check",
8
+ "deep dive", "what do we know about", "literature review", or any question
9
+ where stored knowledge may be stale or insufficient. Runs over native web
10
+ search and the url-get skill, and saves the result as a vault Note.
11
+ ---
12
+
13
+ # deep-research
14
+
15
+ Answer questions using live web sources. Search, read, extract, and cite. Do not guess. Synthesise across sources and present the result with full attribution, then save it to the vault.
16
+
17
+ This runs over the native `WebSearch` and `WebFetch` tools and the `[[url-get]]` skill for full-page fetches. No external API key is needed.
18
+
19
+ ## Config
20
+
21
+ ```yaml
22
+ citation_style: "inline" # inline | footnotes | cards | minimal
23
+ max_sources: 8 # sources to synthesise per query (4-12 recommended)
24
+ search_depth: "standard" # standard (3-5 searches) | deep (6-12 searches)
25
+ show_confidence: true # append a High / Medium / Low confidence note
26
+ follow_up_questions: true # suggest 3 follow-up questions
27
+ ```
28
+
29
+ ## Workflow
30
+
31
+ **Phase 1, Decompose.** Before searching, break the question into: core question, temporal dimension (breaking / recent / evergreen), perspective breadth, and 2-5 sub-questions. State the decomposition in 1-3 lines, then proceed without asking permission. Read `references/research-modes.md` and select the research mode.
32
+
33
+ **Phase 2, Search.** Map sub-questions to targeted search queries via `WebSearch`. Read `references/search-strategy.md` for query crafting and source evaluation.
34
+
35
+ **Phase 3, Fetch and extract.** For each source, fetch the full page (`WebFetch`, or `[[url-get]]` for verbatim markdown), extract only what is relevant to the sub-question, and note publication dates. See `references/search-strategy.md` for the source-quality hierarchy and staleness rules.
36
+
37
+ **Phase 4, Synthesise.** Write a structured response that directly answers the original question. Read `references/citation-styles.md` and apply the style from `config.citation_style`.
38
+
39
+ **Phase 5, Confidence.** If `show_confidence: true`, append a confidence assessment. See `references/citation-styles.md` for the High / Medium / Low criteria.
40
+
41
+ **Phase 6, Follow-ups.** If `follow_up_questions: true`, suggest 3 genuinely useful next questions. See `references/citation-styles.md`.
42
+
43
+ **Phase 7, Persist.** Save the report to the vault as a Note so it is kept and searchable. Write one markdown file under the vault with frontmatter and the report in the body:
44
+
45
+ ```markdown
46
+ ---
47
+ type: note
48
+ title: <the research question>
49
+ area: <an Areas value, if the topic fits one>
50
+ ---
51
+
52
+ <the synthesised report, with citations inline>
53
+ ```
54
+
55
+ `type` and `title` are required; `area` is optional and must be a value from the schema's Areas vocabulary. The report prose lives in the body, not in frontmatter. After writing, validate the vault and confirm it passes:
56
+
57
+ ```bash
58
+ node ~/.maxy-lite/validator/cli.mjs ~/.maxy-lite/vault
59
+ ```
60
+
61
+ Expect `invalid=0` and exit 0. If the Note fails validation, fix the frontmatter and re-run before telling the owner it is saved.
62
+
63
+ ## Hard rules
64
+
65
+ 1. **Never fabricate citations.** If you cannot find a source for a claim, say so explicitly.
66
+ 2. **Never quote more than 15 words verbatim** from any single source.
67
+ 3. **One direct quote per source maximum.** Paraphrase everything else.
68
+ 4. **Do not recite stored knowledge as search results.** If relying on prior knowledge, say "(from prior knowledge, not verified live)".
69
+ 5. **Stale is worse than honest.** If you cannot find current information, state the most recent date you found and invite the owner to check for updates.
70
+ 6. **No padding.** Every sentence must carry information. No filler, no generic caveats, no restated conclusions.
@@ -0,0 +1,52 @@
1
+ # Citation Styles, Confidence, and Follow-ups
2
+
3
+ ## Citation Styles (Phase 4)
4
+
5
+ Apply the style set in `config.citation_style`:
6
+
7
+ ### inline (default)
8
+ > The company raised $120M in Series B funding [1], led by Andreessen Horowitz [2].
9
+ > Sources: [1] techcrunch.com/... [2] a16z.com/...
10
+
11
+ ### footnotes
12
+ > Prose with no inline markers. Full source list at end:
13
+ > ---
14
+ > **Sources**
15
+ > 1. Title. domain.com/path (Date if known)
16
+ > 2. Title. domain.com/path
17
+
18
+ ### cards
19
+ > Each source rendered as a mini card:
20
+ > ┌─────────────────────────────────────────┐
21
+ > │ [1] Title of article │
22
+ > │ Source: domain.com · Date: YYYY-MM-DD │
23
+ > │ Relevant finding: one-line summary │
24
+ > └─────────────────────────────────────────┘
25
+
26
+ ### minimal
27
+ > The company raised $120M (techcrunch.com/...), led by a16z (a16z.com/...).
28
+
29
+ ## Confidence Assessment (Phase 5)
30
+
31
+ If `show_confidence: true`, append a brief confidence assessment after the main answer:
32
+
33
+ **Confidence: High / Medium / Low**
34
+ - High: multiple independent sources agree, sources are recent and authoritative
35
+ - Medium: sources partially agree, or primary sources are thin
36
+ - Low: limited sources, conflicting information, or significant recency gaps
37
+
38
+ Always flag explicitly if:
39
+ - A key sub-question could not be answered from available sources
40
+ - Important information may exist behind paywalls or login walls
41
+ - The topic is moving fast and this answer may be stale within days/weeks
42
+
43
+ ## Follow-up Questions (Phase 6)
44
+
45
+ If `follow_up_questions: true`, end with:
46
+
47
+ **You might also want to ask:**
48
+ 1. [Specific follow-up question 1]
49
+ 2. [Specific follow-up question 2]
50
+ 3. [Specific follow-up question 3]
51
+
52
+ Make these genuinely useful: the next natural step in the research, not generic variations of the original question.