@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,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: upgrade
|
|
3
|
+
description: "Upgrade maxy-lite to the latest published version by re-running the lite installer when the owner asks."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Upgrade
|
|
7
|
+
|
|
8
|
+
The owner wants to upgrade maxy-lite. Re-run the lite installer; it is idempotent and brings the install to the latest published version.
|
|
9
|
+
|
|
10
|
+
## Run the upgrade
|
|
11
|
+
|
|
12
|
+
maxy-lite ships as one installer package (it has no brand variants), so the upgrade is a single command:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx -y @rubytech/create-maxy-lite@latest
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Stream the installer's stdout to the owner as it runs. The installer prints its own progress; pass it through, do not narrate around it.
|
|
19
|
+
|
|
20
|
+
If the lite install records its own installer package name in a config file, read that field with `Read` and use it instead of the literal above. Never invent a different default.
|
|
21
|
+
|
|
22
|
+
## After the upgrade
|
|
23
|
+
|
|
24
|
+
The lite runtime is the `claude` process in the Termux session, not a brand systemd service. The upgrade applies to the files on disk; it takes effect on the next `claude` session. Tell the owner to start a fresh session to pick up the new version.
|
|
25
|
+
|
|
26
|
+
## Surface failures verbatim
|
|
27
|
+
|
|
28
|
+
- `npx` fetch from `registry.npmjs.org` fails (network, DNS, registry outage): show the error to the owner.
|
|
29
|
+
- The installer exits non-zero: show the tail of stdout and stop.
|
|
30
|
+
- `Permission denied` on `npx`: the Bash environment is misconfigured for this install; tell the owner.
|
|
31
|
+
|
|
32
|
+
Never claim success on a failed run.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: browser
|
|
3
|
+
description: >
|
|
4
|
+
Drive the device's Chromium for the things plain HTTP cannot do: render a
|
|
5
|
+
JavaScript-heavy page, turn an HTML file into a PDF, or capture a screenshot.
|
|
6
|
+
Use when url-get returns an empty page, when a deck or brochure or booking
|
|
7
|
+
page needs to become a PDF, or when the owner wants a picture of a page.
|
|
8
|
+
Trigger phrases: "this page is empty", "render it in a browser", "save it as
|
|
9
|
+
a PDF", "print to PDF", "take a screenshot of", "grab an image of this page".
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# browser
|
|
13
|
+
|
|
14
|
+
Three one-shot operations against the device Chromium over the Chrome DevTools Protocol: render, pdf, screenshot. Each call connects to the already-running Chromium, does one thing, and disconnects. Nothing stays open between calls.
|
|
15
|
+
|
|
16
|
+
## When to use
|
|
17
|
+
|
|
18
|
+
- **render**: a page came back empty or as a shell from `[[url-get]]`. It is client-rendered, so its content only exists after the browser runs its JavaScript. This script runs the page and returns the rendered text and HTML.
|
|
19
|
+
- **pdf**: an HTML file (a deck, a brochure, a booking page) needs to become a PDF. Serve or open the HTML, then print it. This honours print CSS and prints backgrounds.
|
|
20
|
+
- **screenshot**: the owner wants a picture of a page, or a section captured as an image.
|
|
21
|
+
|
|
22
|
+
For static, server-rendered pages prefer `[[url-get]]`, which is lighter and returns verbatim markdown. Reach for `browser` only when the page needs a real browser.
|
|
23
|
+
|
|
24
|
+
## How to run
|
|
25
|
+
|
|
26
|
+
All three read the Chromium debug port from `CDP_PORT` (the lite runtime sets it). They attach to that browser and never launch their own.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Render a JS page and read its rendered DOM
|
|
30
|
+
node ~/.maxy-lite/skills/browser/scripts/render.mjs "https://app.example.com/dashboard"
|
|
31
|
+
|
|
32
|
+
# Turn an HTML page into a PDF (add --landscape for landscape)
|
|
33
|
+
node ~/.maxy-lite/skills/browser/scripts/pdf.mjs "http://localhost:8080/deck.html" /path/out.pdf
|
|
34
|
+
|
|
35
|
+
# Capture a screenshot (optional --width/--height set the viewport first)
|
|
36
|
+
node ~/.maxy-lite/skills/browser/scripts/screenshot.mjs "https://example.com" /path/out.png --width=1200 --height=800
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`render` prints the visible text then the rendered HTML to stdout. `pdf` and `screenshot` write the file and print its path.
|
|
40
|
+
|
|
41
|
+
## Reading the result
|
|
42
|
+
|
|
43
|
+
- **Output on stdout, exit 0**: the operation succeeded.
|
|
44
|
+
- **Non-zero exit**: an error on stderr as `[browser] error=<code>`:
|
|
45
|
+
- `cdp-port`: `CDP_PORT` is unset or invalid. The browser is not configured.
|
|
46
|
+
- `cdp-unreachable`: the Chromium debug port did not answer. The browser is not running.
|
|
47
|
+
- `render` / `pdf` / `screenshot`: the navigation or operation failed (a bad URL, a load timeout). The detail says which.
|
|
48
|
+
|
|
49
|
+
Surface the error plainly. Do not invent the page contents or claim a file was written when it was not.
|
|
50
|
+
|
|
51
|
+
## Form decision (recorded)
|
|
52
|
+
|
|
53
|
+
Lite ships `browser` as a skill with per-call scripts, not as a persistent MCP server. The reason: lite's real browser needs are one-shot (render a page, print a PDF, take a screenshot), and a fresh script per call adds no long-lived process. This matches lite's zero-process default.
|
|
54
|
+
|
|
55
|
+
Multi-call interactive automation (navigate, then click, then fill, then submit against one live page, with dialog arming and console buffering) genuinely needs a persistent listener and is not built here. If a real need for it appears, it is promoted to a lite browser MCP at that point.
|
|
56
|
+
|
|
57
|
+
## Boundaries
|
|
58
|
+
|
|
59
|
+
- One operation per call. There is no click, fill, type, or form-submit here. Those need a persistent page and are out of scope for the script form.
|
|
60
|
+
- Attaches to the device Chromium; it does not start or manage the browser.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Minimal Chrome DevTools Protocol client for the lite browser scripts.
|
|
2
|
+
//
|
|
3
|
+
// Attaches to the device Chromium over CDP (CDP_PORT on 127.0.0.1) and never
|
|
4
|
+
// launches its own browser. Each lite browser script connects, does one
|
|
5
|
+
// operation against a fresh target, and disconnects. There is no persistent
|
|
6
|
+
// session: the page lives only for the call. Ported in spirit from the
|
|
7
|
+
// maxy-code browser plugin's cdp-session/cdp-render, trimmed to the one-shot
|
|
8
|
+
// operations lite needs (render, pdf, screenshot).
|
|
9
|
+
const HOST = "127.0.0.1";
|
|
10
|
+
|
|
11
|
+
const fail = (code, msg) => {
|
|
12
|
+
console.error(`[browser] error=${code} detail=${JSON.stringify(msg)}`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Resolve and validate CDP_PORT. Exits with error=cdp-port if unset/invalid. */
|
|
17
|
+
export function cdpPort() {
|
|
18
|
+
const p = Number.parseInt(process.env.CDP_PORT ?? "", 10);
|
|
19
|
+
if (!p || Number.isNaN(p)) fail("cdp-port", "CDP_PORT not set or invalid");
|
|
20
|
+
return p;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Create a blank page target via the CDP HTTP surface. */
|
|
24
|
+
export async function createTarget(port, timeoutMs = 15_000) {
|
|
25
|
+
let r;
|
|
26
|
+
try {
|
|
27
|
+
r = await fetch(`http://${HOST}:${port}/json/new`, { method: "PUT", signal: AbortSignal.timeout(timeoutMs) });
|
|
28
|
+
} catch (err) {
|
|
29
|
+
fail("cdp-unreachable", err instanceof Error ? err.message : String(err));
|
|
30
|
+
}
|
|
31
|
+
if (!r.ok) fail("cdp", `json/new returned ${r.status}`);
|
|
32
|
+
const t = await r.json();
|
|
33
|
+
if (!t.id || !t.webSocketDebuggerUrl) fail("cdp", "json/new response missing id/webSocketDebuggerUrl");
|
|
34
|
+
return t;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Best-effort target close. Failure is ignored. */
|
|
38
|
+
export async function closeTarget(port, id) {
|
|
39
|
+
try {
|
|
40
|
+
await fetch(`http://${HOST}:${port}/json/close/${encodeURIComponent(id)}`, {
|
|
41
|
+
method: "PUT",
|
|
42
|
+
signal: AbortSignal.timeout(5_000),
|
|
43
|
+
});
|
|
44
|
+
} catch {
|
|
45
|
+
/* best effort */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A CDP command/event channel over one page websocket. */
|
|
50
|
+
export class Cdp {
|
|
51
|
+
constructor(ws) {
|
|
52
|
+
this.ws = ws;
|
|
53
|
+
this.seq = 0;
|
|
54
|
+
this.pending = new Map();
|
|
55
|
+
this.events = new Map();
|
|
56
|
+
ws.addEventListener("message", (ev) => {
|
|
57
|
+
const raw = typeof ev.data === "string" ? ev.data : ev.data.toString();
|
|
58
|
+
const m = JSON.parse(raw);
|
|
59
|
+
if (m.id && this.pending.has(m.id)) {
|
|
60
|
+
const { resolve, reject } = this.pending.get(m.id);
|
|
61
|
+
this.pending.delete(m.id);
|
|
62
|
+
m.error ? reject(new Error(m.error.message)) : resolve(m.result);
|
|
63
|
+
} else if (m.method && this.events.has(m.method)) {
|
|
64
|
+
const fn = this.events.get(m.method);
|
|
65
|
+
this.events.delete(m.method);
|
|
66
|
+
fn();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
cmd(method, params = {}, timeoutMs = 15_000) {
|
|
72
|
+
const id = ++this.seq;
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const to = setTimeout(() => {
|
|
75
|
+
this.pending.delete(id);
|
|
76
|
+
reject(new Error(`command-timeout ${method}`));
|
|
77
|
+
}, timeoutMs);
|
|
78
|
+
this.pending.set(id, {
|
|
79
|
+
resolve: (v) => {
|
|
80
|
+
clearTimeout(to);
|
|
81
|
+
resolve(v);
|
|
82
|
+
},
|
|
83
|
+
reject: (e) => {
|
|
84
|
+
clearTimeout(to);
|
|
85
|
+
reject(e);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
this.ws.send(JSON.stringify({ id, method, params }));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
waitFor(event, timeoutMs = 30_000) {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const to = setTimeout(() => {
|
|
95
|
+
this.events.delete(event);
|
|
96
|
+
reject(new Error(`load-timeout ${event}`));
|
|
97
|
+
}, timeoutMs);
|
|
98
|
+
this.events.set(event, () => {
|
|
99
|
+
clearTimeout(to);
|
|
100
|
+
resolve();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Open a websocket to a target and return a Cdp channel. On a connect failure
|
|
107
|
+
* the half-open socket is closed before rethrowing, so a failed openSession
|
|
108
|
+
* never leaves a dangling handle that would keep the process alive. */
|
|
109
|
+
export async function openSession(target) {
|
|
110
|
+
if (typeof WebSocket === "undefined") fail("websocket-unavailable", "global WebSocket unavailable (Node < 22.4)");
|
|
111
|
+
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
|
112
|
+
try {
|
|
113
|
+
await new Promise((resolve, reject) => {
|
|
114
|
+
ws.addEventListener("open", () => resolve());
|
|
115
|
+
ws.addEventListener("error", (e) => reject(new Error(`websocket: ${String(e?.message ?? e)}`)));
|
|
116
|
+
});
|
|
117
|
+
} catch (err) {
|
|
118
|
+
try {
|
|
119
|
+
ws.close();
|
|
120
|
+
} catch {
|
|
121
|
+
/* already closed */
|
|
122
|
+
}
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
return new Cdp(ws);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Shared navigate helper: enable Page, navigate, await load. */
|
|
129
|
+
export async function navigate(session, url, loadTimeoutMs = 30_000) {
|
|
130
|
+
await session.cmd("Page.enable");
|
|
131
|
+
const load = session.waitFor("Page.loadEventFired", loadTimeoutMs);
|
|
132
|
+
await session.cmd("Page.navigate", { url });
|
|
133
|
+
await load;
|
|
134
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// browser pdf — render a page to a PDF file using Chromium's print pipeline
|
|
3
|
+
// (honours @page/@media print CSS, prints backgrounds). One-shot: creates a
|
|
4
|
+
// target, navigates, prints, closes. The on-device path for deck/brochure/
|
|
5
|
+
// calendar-site HTML to PDF.
|
|
6
|
+
import { writeFileSync } from "node:fs";
|
|
7
|
+
import { cdpPort, createTarget, closeTarget, openSession, navigate } from "./cdp.mjs";
|
|
8
|
+
|
|
9
|
+
const url = process.argv[2];
|
|
10
|
+
const out = process.argv[3];
|
|
11
|
+
const landscape = process.argv.includes("--landscape");
|
|
12
|
+
if (!url || !out) {
|
|
13
|
+
console.error(`[browser] error=usage detail="usage: pdf.mjs <url> <out.pdf> [--landscape]"`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const port = cdpPort();
|
|
18
|
+
const target = await createTarget(port);
|
|
19
|
+
let session;
|
|
20
|
+
try {
|
|
21
|
+
session = await openSession(target);
|
|
22
|
+
await navigate(session, url);
|
|
23
|
+
const res = await session.cmd("Page.printToPDF", { landscape, printBackground: true });
|
|
24
|
+
const buf = Buffer.from(res.data, "base64");
|
|
25
|
+
writeFileSync(out, buf);
|
|
26
|
+
console.error(`[browser-pdf] url=${url} out=${out} bytes=${buf.length}`);
|
|
27
|
+
process.stdout.write(out + "\n");
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error(`[browser] error=pdf detail=${JSON.stringify(String(err?.message ?? err))}`);
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
} finally {
|
|
32
|
+
try {
|
|
33
|
+
session?.ws?.close();
|
|
34
|
+
} catch {
|
|
35
|
+
/* already closed */
|
|
36
|
+
}
|
|
37
|
+
await closeTarget(port, target.id);
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// browser render — render a JavaScript-heavy page in the device Chromium and
|
|
3
|
+
// print its fully rendered DOM text and HTML. Use for pages url-get returns
|
|
4
|
+
// empty. One-shot: creates a target, navigates, reads the DOM, closes.
|
|
5
|
+
import { cdpPort, createTarget, closeTarget, openSession, navigate } from "./cdp.mjs";
|
|
6
|
+
|
|
7
|
+
const url = process.argv[2];
|
|
8
|
+
if (!url) {
|
|
9
|
+
console.error(`[browser] error=usage detail="usage: render.mjs <url>"`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const port = cdpPort();
|
|
14
|
+
const target = await createTarget(port);
|
|
15
|
+
let session;
|
|
16
|
+
try {
|
|
17
|
+
session = await openSession(target);
|
|
18
|
+
await navigate(session, url);
|
|
19
|
+
const htmlRes = await session.cmd("Runtime.evaluate", {
|
|
20
|
+
expression: "document.documentElement.outerHTML",
|
|
21
|
+
returnByValue: true,
|
|
22
|
+
});
|
|
23
|
+
const textRes = await session.cmd("Runtime.evaluate", {
|
|
24
|
+
expression: "document.body && document.body.innerText || ''",
|
|
25
|
+
returnByValue: true,
|
|
26
|
+
});
|
|
27
|
+
const html = typeof htmlRes?.result?.value === "string" ? htmlRes.result.value : "";
|
|
28
|
+
const text = typeof textRes?.result?.value === "string" ? textRes.result.value : "";
|
|
29
|
+
console.error(`[browser-render] url=${url} domBytes=${Buffer.byteLength(html)}`);
|
|
30
|
+
process.stdout.write(
|
|
31
|
+
`--- VISIBLE TEXT ---\n${text.slice(0, 200_000)}\n\n--- RENDERED HTML ---\n${html.slice(0, 800_000)}\n`,
|
|
32
|
+
);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error(`[browser] error=render detail=${JSON.stringify(String(err?.message ?? err))}`);
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
} finally {
|
|
37
|
+
try {
|
|
38
|
+
session?.ws?.close();
|
|
39
|
+
} catch {
|
|
40
|
+
/* already closed */
|
|
41
|
+
}
|
|
42
|
+
await closeTarget(port, target.id);
|
|
43
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// browser screenshot — capture a PNG of a page to a file. One-shot: creates a
|
|
3
|
+
// target, navigates, captures, closes. Optional viewport width/height before
|
|
4
|
+
// capture to control the rendered size.
|
|
5
|
+
import { writeFileSync } from "node:fs";
|
|
6
|
+
import { cdpPort, createTarget, closeTarget, openSession, navigate } from "./cdp.mjs";
|
|
7
|
+
|
|
8
|
+
const url = process.argv[2];
|
|
9
|
+
const out = process.argv[3];
|
|
10
|
+
const args = Object.fromEntries(
|
|
11
|
+
process.argv.slice(4).map((a) => {
|
|
12
|
+
const m = a.match(/^--([^=]+)=(.*)$/);
|
|
13
|
+
return m ? [m[1], m[2]] : [a, true];
|
|
14
|
+
}),
|
|
15
|
+
);
|
|
16
|
+
if (!url || !out) {
|
|
17
|
+
console.error(`[browser] error=usage detail="usage: screenshot.mjs <url> <out.png> [--width=] [--height=]"`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const port = cdpPort();
|
|
22
|
+
const target = await createTarget(port);
|
|
23
|
+
let session;
|
|
24
|
+
try {
|
|
25
|
+
session = await openSession(target);
|
|
26
|
+
// Set the viewport before navigating so responsive layout and any
|
|
27
|
+
// viewport-dependent resources load at the requested size, not the default.
|
|
28
|
+
if (args.width && args.height) {
|
|
29
|
+
await session.cmd("Emulation.setDeviceMetricsOverride", {
|
|
30
|
+
width: Number(args.width),
|
|
31
|
+
height: Number(args.height),
|
|
32
|
+
deviceScaleFactor: 1,
|
|
33
|
+
mobile: false,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
await navigate(session, url);
|
|
37
|
+
const res = await session.cmd("Page.captureScreenshot", { format: "png" });
|
|
38
|
+
const buf = Buffer.from(res.data, "base64");
|
|
39
|
+
writeFileSync(out, buf);
|
|
40
|
+
console.error(`[browser-screenshot] url=${url} out=${out} bytes=${buf.length}`);
|
|
41
|
+
process.stdout.write(out + "\n");
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error(`[browser] error=screenshot detail=${JSON.stringify(String(err?.message ?? err))}`);
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
} finally {
|
|
46
|
+
try {
|
|
47
|
+
session?.ws?.close();
|
|
48
|
+
} catch {
|
|
49
|
+
/* already closed */
|
|
50
|
+
}
|
|
51
|
+
await closeTarget(port, target.id);
|
|
52
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: business-assistant
|
|
3
|
+
description: Handle a customer enquiry end to end for a small business: triage it, find or create the customer, book the appointment, send the confirmation, format a quote, and record every step in the vault. Use when a customer message comes in, when the user asks to book someone in, chase a quote, or see what is outstanding, or when running the day to day customer side of a small business. Composes the contacts, scheduling, and email skills; it adds no tools of its own.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Business assistant
|
|
7
|
+
|
|
8
|
+
You run the customer side of a small business so the owner can get on with the work: enquiries, bookings, quotes, and the admin around them. You are the owner's assistant, not the owner. Be professional and warm, keep messages short, and always be honest that you are an AI when asked. Never impersonate the owner, never pretend to be human, and never commit to a price without the owner's approval.
|
|
9
|
+
|
|
10
|
+
This skill does not introduce new tools. It is a workflow over skills that already exist: `contacts` for the customer record, `scheduling` for the appointment, `email-composition` for correspondence, and `work`/`memory` for follow ups and context. Every record it writes is checked by the validator. The entity field lists are in [`SCHEMA.md`](../../schema/SCHEMA.md).
|
|
11
|
+
|
|
12
|
+
## Read the vault first
|
|
13
|
+
|
|
14
|
+
Before answering a customer, read what the vault already knows. Grep `people/` for the sender, read their Person file and any notes linked to them, and check `calendar/` for anything booked. Answer from what you find, not from assumption. After anything meaningful happens, write it back, so the next message starts from a fuller picture.
|
|
15
|
+
|
|
16
|
+
## Triage
|
|
17
|
+
|
|
18
|
+
Sort every incoming message into one of four levels and act accordingly.
|
|
19
|
+
|
|
20
|
+
- **Red, urgent.** A safety risk or escalating damage (a leak, no heat for a vulnerable person in the cold). Give immediate safety advice and tell the owner now, in this conversation, before waiting on anything else.
|
|
21
|
+
- **Amber, same or next day.** Something is broken but not dangerous. Gather the details needed to book or to quote, then handle it or surface it to the owner.
|
|
22
|
+
- **Green, routine.** Planned work, a maintenance job, a quote request, a booking. Gather details and book the next suitable slot.
|
|
23
|
+
- **Quick.** Confirming an appointment, chasing a quote, changing a booking, saying thanks. Answer straight away with minimal back and forth.
|
|
24
|
+
|
|
25
|
+
## The enquiry workflow
|
|
26
|
+
|
|
27
|
+
A new enquiry runs through five steps. Each one writes a vault record, and each write is validated before you move on.
|
|
28
|
+
|
|
29
|
+
1. **Find or create the customer.** Look the sender up in `people/` (the `contacts` skill). If they are not there and this is a real enquiry, create the Person file first, with the name and whatever contact details you have. Everything else links to this file, so it has to exist before the links will resolve.
|
|
30
|
+
2. **Record the enquiry.** Write a Note in `activities/` capturing what they want, linked to the customer with `about` and, if there is a job, to a Project with `project`. The Note body is the detail (what they asked for, when they are free, anything notable).
|
|
31
|
+
3. **Book the slot.** When the enquiry needs an appointment, create the Event with the `scheduling` skill: an Event file in `calendar/` with the customer as an attendee. The vault Event is the source of truth; `scheduling` handles the calendar sync.
|
|
32
|
+
4. **Send the confirmation.** Confirm the booking to the customer with the `email-composition` skill, or as a reply on whatever channel they used. The send is recorded as a vault Email linked to the customer, exactly as `email-composition` describes.
|
|
33
|
+
5. **Leave a clean record.** After the steps above, the vault holds the customer, the enquiry Note, the booked Event, and the confirmation Email, all linked and all passing the validator. Run `maxy-lite-validate "$HOME/maxy"` and fix any named field until it exits 0. An enquiry that leaves no conformant record set is the failure this workflow exists to prevent.
|
|
34
|
+
|
|
35
|
+
A worked example: the customer, the job, the enquiry, and the booked slot, the four files passing the validator together (the confirmation Email is the one the `email-composition` skill records). Every link resolves within the set, so the project links need the Project file present:
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
# people/Jane Doe.md
|
|
39
|
+
---
|
|
40
|
+
type: person
|
|
41
|
+
name: Jane Doe
|
|
42
|
+
emails:
|
|
43
|
+
- jane@acme.example
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
# projects/Bathroom refit.md
|
|
47
|
+
---
|
|
48
|
+
type: project
|
|
49
|
+
name: Bathroom refit
|
|
50
|
+
area: work
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
# activities/Enquiry - Jane.md
|
|
54
|
+
---
|
|
55
|
+
type: note
|
|
56
|
+
title: Enquiry - bathroom refit
|
|
57
|
+
about: "[[Jane Doe]]"
|
|
58
|
+
project: "[[Bathroom refit]]"
|
|
59
|
+
area: work
|
|
60
|
+
---
|
|
61
|
+
Jane enquired about a full bathroom refit. Wants a survey then a quote. Mornings.
|
|
62
|
+
|
|
63
|
+
# calendar/Bathroom survey - Jane.md
|
|
64
|
+
---
|
|
65
|
+
type: event
|
|
66
|
+
summary: Bathroom survey - Jane Doe
|
|
67
|
+
start: 2026-06-25T09:30:00
|
|
68
|
+
end: 2026-06-25T10:00:00
|
|
69
|
+
location: 42 Oak Lane, Stansted
|
|
70
|
+
attendees:
|
|
71
|
+
- "[[Jane Doe]]"
|
|
72
|
+
project: "[[Bathroom refit]]"
|
|
73
|
+
status: CONFIRMED
|
|
74
|
+
area: work
|
|
75
|
+
---
|
|
76
|
+
On-site survey ahead of quoting the refit.
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Quotes
|
|
80
|
+
|
|
81
|
+
A quote is a number, and a number needs the owner's approval before it goes to a customer. Gather the scope, work out what you would quote, and put it to the owner. Once they approve, send the figure to the customer with the `email-composition` skill, and record what was quoted as a Note in `activities/`, linked to the customer with `about`, so the quote is part of the record. Do not generate an invoice or a signed document here; producing those is a separate skill.
|
|
82
|
+
|
|
83
|
+
## Channels are reply only
|
|
84
|
+
|
|
85
|
+
When a customer reaches you on a channel, reply on that channel; never send a cold first message to someone who has not contacted you. Cold outreach is not this skill's job and it puts the business's accounts at risk.
|
|
86
|
+
|
|
87
|
+
## Escalating to the owner
|
|
88
|
+
|
|
89
|
+
In a small business there is one owner, and that owner is the person you are talking to in this session. Escalating means surfacing the matter to them here, clearly and briefly, not routing it anywhere. There is no broadcast across channels.
|
|
90
|
+
|
|
91
|
+
Always escalate, rather than deciding alone:
|
|
92
|
+
|
|
93
|
+
- A price or any money decision the owner has not already approved.
|
|
94
|
+
- A complaint or an unhappy customer.
|
|
95
|
+
- A request outside the normal services, or a suspicious one.
|
|
96
|
+
- An emergency that needs the owner's judgement.
|
|
97
|
+
- A customer asking to speak to the owner directly. Never block that. Take their number and a good time, capture the context, and hand it over.
|
|
98
|
+
|
|
99
|
+
Keep the escalation short. The owner is busy and reading on a phone.
|
|
100
|
+
|
|
101
|
+
## Out of scope
|
|
102
|
+
|
|
103
|
+
This skill stays inside the enquiry, booking, and quote workflow. It does not do:
|
|
104
|
+
|
|
105
|
+
- **Sales pipeline objects** (deals, stages, a funnel). The store does not carry them yet; that is a separate, deferred part of the data model.
|
|
106
|
+
- **Invoices, payments, and signed documents.** Invoice and payment records and document generation belong to other skills, not here.
|
|
107
|
+
- **Industry specific scripts and overlays, voice or phone menus.** Out of scope.
|
|
108
|
+
- **The public booking site and its captured leads.** Standing up a booking page and pulling its submissions into the vault is the `calendar-site` skill's job; this skill consumes the Events that land, it does not build the site.
|
|
109
|
+
- **New tools.** Everything here is the contacts, scheduling, and email skills plus vault files.
|
|
110
|
+
</content>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: calendar-site
|
|
3
|
+
description: Stand up a public "book time with me" page on a custom domain, and pull the bookings it captures into the vault as events. This is the skill behind "give me a booking link", "put up a Calendly-style page", "let people book a call with me", "deploy the booking site", and "reconcile new bookings". It writes a static availability config, assembles the booking page plus its D1 capture, deploys via the site-deploy skill, and on demand reads new submissions from D1 and writes each as a vault Event (then pushes it to Google Calendar). The vault Event is the source of truth.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Stand up a booking site, and reconcile its bookings into the vault
|
|
7
|
+
|
|
8
|
+
Two halves. The first deploys a public page that captures bookings to Cloudflare D1, so a booking is captured even while the phone is offline or asleep. The second is a pull-on-demand reconcile: when run, it reads new submissions from D1 and turns each into a vault Event. There is no always-on process. The vault is authoritative, so a captured row only becomes real once it is written as an Event and validated.
|
|
9
|
+
|
|
10
|
+
The deploy mechanics (build, custom-domain attach, token discipline, the live done-gate) belong to the `site-deploy` skill. Read it before deploying. This skill adds the two things a booking site needs on top of a plain page: the static availability config the page reads to compute open slots, and the reconcile that brings captured rows into the vault.
|
|
11
|
+
|
|
12
|
+
## The template
|
|
13
|
+
|
|
14
|
+
The booking page ships in `template/`:
|
|
15
|
+
|
|
16
|
+
- `availability.json` is the static slot source: `timezone`, `durationMins`, `bufferMins`, and a `weekly` window per day (`{ "mon": [["09:00","17:00"]], ..., "sat": [], "sun": [] }`, with an empty array for closed days).
|
|
17
|
+
- `public/` is the page. It fetches `availability.json` and computes open slots client-side as the weekly window minus the buffer for the next two weeks. There is no live free/busy merge, so the page never reads any event detail.
|
|
18
|
+
- `functions/api/book.ts` captures a submission to D1. `schema.sql` is the `bookings` table. `wrangler.toml` carries the Pages and D1 config with placeholders.
|
|
19
|
+
|
|
20
|
+
## Deploy the page
|
|
21
|
+
|
|
22
|
+
1. Collect the availability from the operator (timezone, meeting duration, buffer, weekly window) and write `availability.json`. Re-running with new numbers is how availability changes later; there is no separate editor.
|
|
23
|
+
2. Assemble the template into the canonical Pages source path the `site-deploy` skill expects, and fill `wrangler.toml`'s `__PROJECT_NAME__`, `__D1_DATABASE_NAME__`, and `__D1_DATABASE_ID__`. Leave no `__...__` placeholder in the assembled tree.
|
|
24
|
+
3. Provision the D1 `bookings` table by applying `schema.sql` to the remote database, using the Cloudflare token discipline the `site-deploy` skill and the Cloudflare connector own. Never write or echo a token.
|
|
25
|
+
4. Hand the assembled tree to the `site-deploy` skill for the Pages deploy and custom-domain attach. Everything in its auth, domain, and DNS guidance applies unchanged.
|
|
26
|
+
|
|
27
|
+
Done for this half is the `site-deploy` live gate plus a test submission that lands in D1:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
curl -sS -X POST -H 'content-type: application/json' \
|
|
31
|
+
-d '{"slotStart":"2026-01-01T09:00:00","slotEnd":"2026-01-01T09:30:00","name":"verify","email":"verify@example.com"}' \
|
|
32
|
+
"https://<booking-domain>/api/book" -i | head -1
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Reconcile bookings into the vault
|
|
36
|
+
|
|
37
|
+
This is the bridge from a captured D1 row to a vault Event. Run it when checking for new bookings (next session, or when the operator asks). It has no background process; nothing happens to a booking until this runs.
|
|
38
|
+
|
|
39
|
+
1. Read the accepted, unswept rows:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
wrangler d1 execute <db-name> --remote --command \
|
|
43
|
+
"SELECT bookingId, slotStart, slotEnd, name, email, note FROM bookings WHERE status = 'accepted' AND swept = 0;"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
2. For each row, before writing anything, run the **overlap guard**: scan `calendar/` for an existing Event whose time range overlaps `[slotStart, slotEnd)`. If one overlaps, do not write the Event and do not mark the row swept. Tell the operator there is a clash for that slot so they can decline or rebook it; leaving the row unswept lets a later run retry it once the clash is resolved. This guard is why a static page is safe: the page can offer a slot that filled after the page loaded, and the clash is caught here rather than silently double-booking.
|
|
47
|
+
|
|
48
|
+
3. For a non-overlapping row, write the Event exactly as the `scheduling` skill prescribes: a `calendar/<summary>.md` file with `type: event`, `summary` (for example `Booking: <name>`), `start` = `slotStart`, `end` = `slotEnd`, and the booker's name, email, and note in the markdown body. Then run the validator to exit 0:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
maxy-lite-validate <vault>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
A non-zero exit means the Event drifted from the SCHEMA; fix the file before going on. Do not mark the row swept while its Event is non-conformant.
|
|
55
|
+
|
|
56
|
+
4. Once the Event validates, push it to Google Calendar through the connector (the same step the `scheduling` skill uses), then mark the row swept so it is not reconciled again:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
wrangler d1 execute <db-name> --remote --command \
|
|
60
|
+
"UPDATE bookings SET swept = 1 WHERE bookingId = '<bookingId>';"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Done for this half is a test submission that, after a reconcile run, exists as a validator-passing vault Event.
|
|
64
|
+
|
|
65
|
+
## Known limitation
|
|
66
|
+
|
|
67
|
+
The page computes open slots from the static availability window with no live busy-merge, so it can offer a slot that has already filled. The reconcile overlap guard catches the clash when the booking is pulled into the vault, but there is a window where a visitor can submit a slot that turns out to be taken. That clash surfaces at reconcile time, not at booking time. Live busy-merge and two-way sync are not part of this skill; the static window plus the reconcile guard are the whole mechanism here.
|
|
68
|
+
|
|
69
|
+
## Tool discipline
|
|
70
|
+
|
|
71
|
+
The permitted surface is `wrangler`, `curl`, the Cloudflare API via the Cloudflare connector, and native vault file writes. Follow the Cloudflare references for token discipline: reuse one stable per-scope token, never mint one per deploy, never drive the dashboard with a browser, never write or echo a token. When a step fails, report the exact output with secrets redacted, name the failing URL or command, and stop.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"timezone": "Europe/London",
|
|
3
|
+
"durationMins": 30,
|
|
4
|
+
"bufferMins": 15,
|
|
5
|
+
"weekly": {
|
|
6
|
+
"mon": [["09:00", "17:00"]],
|
|
7
|
+
"tue": [["09:00", "17:00"]],
|
|
8
|
+
"wed": [["09:00", "17:00"]],
|
|
9
|
+
"thu": [["09:00", "17:00"]],
|
|
10
|
+
"fri": [["09:00", "17:00"]],
|
|
11
|
+
"sat": [],
|
|
12
|
+
"sun": []
|
|
13
|
+
}
|
|
14
|
+
}
|