@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.
- package/index.mjs +43 -22
- package/lib/healthcheck.mjs +60 -19
- package/lib/orchestrate.mjs +32 -11
- package/lib/paths.mjs +73 -0
- package/package.json +1 -1
- package/payload/package.json +2 -1
- package/payload/skills/README.md +26 -0
- package/payload/skills/admin/datetime/SKILL.md +147 -0
- package/payload/skills/admin/session-management/SKILL.md +39 -0
- package/payload/skills/admin/upgrade/SKILL.md +32 -0
- package/payload/skills/browser/SKILL.md +60 -0
- package/payload/skills/browser/scripts/cdp.mjs +134 -0
- package/payload/skills/browser/scripts/pdf.mjs +38 -0
- package/payload/skills/browser/scripts/render.mjs +43 -0
- package/payload/skills/browser/scripts/screenshot.mjs +52 -0
- package/payload/skills/business-assistant/SKILL.md +110 -0
- package/payload/skills/calendar-site/SKILL.md +71 -0
- package/payload/skills/calendar-site/template/availability.json +14 -0
- package/payload/skills/calendar-site/template/functions/api/book.ts +112 -0
- package/payload/skills/calendar-site/template/public/booking.css +100 -0
- package/payload/skills/calendar-site/template/public/booking.js +202 -0
- package/payload/skills/calendar-site/template/public/index.html +44 -0
- package/payload/skills/calendar-site/template/schema.sql +19 -0
- package/payload/skills/calendar-site/template/wrangler.toml +14 -0
- package/payload/skills/contacts/SKILL.md +57 -0
- package/payload/skills/deep-research/SKILL.md +70 -0
- package/payload/skills/deep-research/references/citation-styles.md +52 -0
- package/payload/skills/deep-research/references/research-modes.md +22 -0
- package/payload/skills/deep-research/references/search-strategy.md +24 -0
- package/payload/skills/docs/SKILL.md +23 -0
- package/payload/skills/docs/references/capability-map.md +25 -0
- package/payload/skills/docs/references/getting-started.md +29 -0
- package/payload/skills/docs/references/vault-model.md +40 -0
- package/payload/skills/email-composition/SKILL.md +107 -0
- package/payload/skills/memory/SKILL.md +48 -0
- package/payload/skills/projects/SKILL.md +47 -0
- package/payload/skills/publish-site/SKILL.md +21 -0
- package/payload/skills/replicate/SKILL.md +63 -0
- package/payload/skills/replicate/scripts/replicate-image.mjs +131 -0
- package/payload/skills/scheduling/SKILL.md +74 -0
- package/payload/skills/site-deploy/SKILL.md +52 -0
- package/payload/skills/slides/SKILL.md +45 -0
- package/payload/skills/slides/deck.html +1359 -0
- package/payload/skills/url-get/SKILL.md +48 -0
- package/payload/skills/url-get/scripts/url-get.mjs +93 -0
- package/payload/skills/work/SKILL.md +49 -0
- package/payload/webchat/inject-line.mjs +11 -0
- package/payload/webchat/package.json +2 -1
- package/payload/webchat/request-handler.mjs +62 -0
- 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.
|