@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,48 @@
1
+ ---
2
+ name: url-get
3
+ description: >
4
+ Fetch a web page faithfully and read its content as markdown. Use when the
5
+ owner gives a URL and wants the page read, quoted, or summarised, or when you
6
+ need the verbatim text of an article, doc, or listing. Trigger phrases:
7
+ "fetch this page", "get the content of", "read this article", "what does this
8
+ link say", "pull the text from", "open this URL and tell me". For
9
+ JavaScript-heavy pages that come back empty, fall to the browser skill.
10
+ ---
11
+
12
+ # url-get
13
+
14
+ Retrieve a web page exactly as the server sent it and read it as markdown. There is no model in the fetch path, so nothing is summarised or inferred on the way in. The script prints the cleaned markdown and writes the full verbatim copy to a scratch file.
15
+
16
+ ## When to use
17
+
18
+ - The owner gives a URL and wants it read, quoted, fact-checked, or summarised.
19
+ - You need the real text of a page rather than a recollection of it.
20
+ - A research step (see `[[deep-research]]`) needs a source fetched.
21
+
22
+ Use this for server-rendered HTML, which is most of the web. For a page that runs its content in the browser (a client-rendered app), this returns little or nothing; switch to the `[[browser]]` render script.
23
+
24
+ ## How to run
25
+
26
+ ```bash
27
+ node ~/.maxy-lite/skills/url-get/scripts/url-get.mjs "https://example.com/article"
28
+ ```
29
+
30
+ The cleaned markdown prints to stdout. The full copy is written to `$LITE_SCRATCH/url-get/<hash>.md` (default `~/.maxy-lite/scratch/url-get/`) so a long page does not have to be re-fetched.
31
+
32
+ ## Reading the result
33
+
34
+ - **Markdown on stdout, exit 0**: the page text. Read it, then answer.
35
+ - **Empty stdout, exit 0**: the page is HTTP 200 but has no extractable text. This is the client-rendered-shell signal. Re-fetch with the `[[browser]]` render script, which runs the page's JavaScript first.
36
+ - **Non-zero exit**: a fetch error printed to stderr as `[url-get] error=<code>`:
37
+ - `fetch`: network failure or timeout.
38
+ - `http`: the server returned a non-2xx status.
39
+ - `non-html`: the URL is a PDF, image, or other non-HTML type this script does not handle.
40
+ - `challenge`: the response was a bot-challenge interstitial, not content. Try the `[[browser]]` render script, which presents a real browser.
41
+
42
+ Surface the error to the owner plainly. Do not invent the page contents.
43
+
44
+ ## Boundaries
45
+
46
+ - Reads one URL per call. For several pages, call it once per URL.
47
+ - Does not run page JavaScript, submit forms, or follow in-page actions. Those are the `[[browser]]` skill.
48
+ - Does not fetch PDFs or images. A PDF link needs a different tool.
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ // url-get — faithful HTTP page fetch into markdown. No model in the path.
3
+ //
4
+ // Ported from the maxy-code url-get MCP tool to a single lite CLI script.
5
+ // One mechanism: fetch the URL, refuse non-HTML and bot-challenge pages, convert
6
+ // the HTML to markdown verbatim, write the full copy to a scratch reference file,
7
+ // and print the same (capped) markdown to stdout. Nothing is summarised — a
8
+ // caller that needs a summary reads the text and summarises in its own context.
9
+ //
10
+ // FETCH -> HTTP STATUS -> CONTENT-TYPE -> CHALLENGE -> HTML->MD -> WRITE REF -> PRINT
11
+ //
12
+ // Errors go to stderr as [url-get] error=<code> and exit non-zero. Codes:
13
+ // fetch network failure / timeout
14
+ // http non-2xx response
15
+ // non-html content-type is not HTML or text/plain
16
+ // challenge response is a bot-challenge interstitial, not content
17
+ import { createHash } from "node:crypto";
18
+ import { mkdir, writeFile } from "node:fs/promises";
19
+ import { homedir } from "node:os";
20
+ import { join } from "node:path";
21
+ import { NodeHtmlMarkdown } from "node-html-markdown";
22
+
23
+ const FETCH_TIMEOUT_MS = 30_000;
24
+ const TEXT_CAP = 100_000;
25
+ const UA =
26
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
27
+ "(KHTML, like Gecko) Chrome/124.0 Safari/537.36";
28
+
29
+ // Strong, low-false-positive markers of a bot-challenge interstitial. These do
30
+ // not occur in legitimate article bodies; a match means the response is a
31
+ // challenge page, not the requested content.
32
+ const CHALLENGE = [
33
+ "/cdn-cgi/challenge-platform/",
34
+ "cf-browser-verification",
35
+ "enable javascript and cookies to continue",
36
+ "checking your browser before accessing",
37
+ "attention required! | cloudflare",
38
+ "<title>just a moment",
39
+ ];
40
+
41
+ const fail = (code, msg) => {
42
+ console.error(`[url-get] error=${code} detail=${JSON.stringify(msg)}`);
43
+ process.exit(1);
44
+ };
45
+
46
+ const url = process.argv[2];
47
+ if (!url) fail("usage", "usage: url-get.mjs <url>");
48
+
49
+ let res;
50
+ try {
51
+ res = await fetch(url, {
52
+ redirect: "follow",
53
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
54
+ headers: { "User-Agent": UA, Accept: "text/html,application/xhtml+xml" },
55
+ });
56
+ } catch (err) {
57
+ fail("fetch", err instanceof Error ? err.message : String(err));
58
+ }
59
+
60
+ if (!res.ok) fail("http", `HTTP ${res.status}`);
61
+
62
+ const ct = (res.headers.get("content-type") ?? "").toLowerCase();
63
+ if (ct && !ct.includes("html") && !ct.includes("text/plain")) {
64
+ fail("non-html", `unsupported content-type: ${ct} (url-get handles server-rendered HTML only)`);
65
+ }
66
+
67
+ const html = await res.text();
68
+ if (CHALLENGE.some((sig) => html.slice(0, 8000).toLowerCase().includes(sig))) {
69
+ fail("challenge", "page returned a bot-challenge interstitial, not content");
70
+ }
71
+
72
+ const markdown = NodeHtmlMarkdown.translate(html).trim();
73
+
74
+ // The full verbatim markdown is the ground truth on disk; stdout is the same
75
+ // content capped so a large page does not flood the caller's context. An empty
76
+ // markdown body (textLength=0) on an HTTP 200 is the JavaScript-rendered-shell
77
+ // signal: retry with the browser render script.
78
+ const scratch = process.env.LITE_SCRATCH || join(homedir(), ".maxy-lite", "scratch");
79
+ const dir = join(scratch, "url-get");
80
+ const hash = createHash("sha256").update(url).digest("hex").slice(0, 32);
81
+ const refPath = join(dir, `${hash}.md`);
82
+ const fetchedAt = new Date().toISOString();
83
+ await mkdir(dir, { recursive: true });
84
+ await writeFile(
85
+ refPath,
86
+ `<!-- url-get source=${url} fetchedAt=${fetchedAt} httpStatus=${res.status} -->\n\n${markdown}\n`,
87
+ "utf-8",
88
+ );
89
+
90
+ console.error(
91
+ `[url-get] url=${url} httpStatus=${res.status} textLength=${markdown.length} refPath=${refPath}`,
92
+ );
93
+ process.stdout.write(markdown.slice(0, TEXT_CAP));
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: work
3
+ description: Capture, find, and update to-dos in the vault. Use when the user asks to remember a task, add a to-do, note something they need to do, mark a task done, change a due date, assign work to someone, or asks what is on their list or what is outstanding. Owns the Task entity of the maxy-lite SCHEMA.
4
+ ---
5
+
6
+ # Work
7
+
8
+ A to-do is a **Task** file in `activities/`, one task per file. It is YAML frontmatter plus a markdown body for the detail (context, sub-steps, notes). The authoritative field list is [`SCHEMA.md`](../../schema/SCHEMA.md) under *Activities*, the *Task* entity.
9
+
10
+ ## Task (`activities/<Title>.md`)
11
+
12
+ Required: `type: task` and `title`. Optional: `due` (`YYYY-MM-DD`), `priority`, `status`, `taskType`, `assignee` (a single link to a Person), `about` (a single link to anything the task concerns), `project` (a single link to a Project), `area`. Set only what you know.
13
+
14
+ ```yaml
15
+ ---
16
+ type: task
17
+ title: Call the surveyor
18
+ due: 2026-07-01
19
+ status: not started
20
+ priority: high
21
+ assignee: "[[Jane Doe]]"
22
+ project: "[[House Move]]"
23
+ area: home
24
+ ---
25
+ Surveyor quoted last spring. Ask about the loft conversion before booking.
26
+ ```
27
+
28
+ `status` and `priority` are free text the assistant keeps consistent (e.g. `not started` / `in progress` / `done`); `area` must be one of the controlled areas in the SCHEMA. The links resolve to files that must already exist: `assignee` to a Person in `people/`, `project` to a Project in `projects/`, `about` to any entity. Create the target file first if it is not in the vault yet.
29
+
30
+ ## Finding what is outstanding
31
+
32
+ Grep the frontmatter rather than recalling from memory:
33
+
34
+ - open tasks: `grep -rL "status: done" activities/ | xargs grep -l "type: task"`
35
+ - tasks for one project: `grep -rl "\[\[House Move\]\]" activities/`
36
+ - tasks assigned to someone: `grep -rl "assignee:.*Jane Doe" activities/`
37
+ - due dates: `grep -rh "^due:" activities/`
38
+
39
+ Read the matched files for the detail. To mark a task done, edit its `status` field; do not delete the file.
40
+
41
+ ## After every write, validate
42
+
43
+ After creating or editing any task file, run the deterministic validator over the vault:
44
+
45
+ ```sh
46
+ maxy-lite-validate "$HOME/maxy"
47
+ ```
48
+
49
+ It exits 0 only when every file conforms. If a line names the task you just wrote with `ok=false`, the bracketed error names the violation: `title:missing` (the required title is absent), `area:area` (an area outside the controlled vocabulary), `due:type` (a malformed date), `assignee:dangling` or `project:dangling` (the linked file does not exist), `assignee: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 task you wrote in a failing state.
@@ -0,0 +1,11 @@
1
+ // Turn a message into the byte sequence written into the claude pty.
2
+ //
3
+ // The TUI submits on a carriage return, so an embedded newline would submit
4
+ // early and truncate the message. Collapse every newline run to a single space,
5
+ // then append the one trailing CR that submits the whole line. Shared by the
6
+ // browser send path and the channel `/inject` path so both are protected.
7
+
8
+ /** @param {string} text @returns {string} */
9
+ export function ptyLine(text) {
10
+ return text.replace(/[\r\n]+/g, ' ') + '\r'
11
+ }
@@ -8,7 +8,8 @@
8
8
  "maxy-lite-webchat": "server.mjs"
9
9
  },
10
10
  "scripts": {
11
- "start": "node server.mjs"
11
+ "start": "node server.mjs",
12
+ "test": "node --test"
12
13
  },
13
14
  "dependencies": {
14
15
  "node-pty": "^1.0.0",
@@ -0,0 +1,62 @@
1
+ // HTTP request router for the webchat relay.
2
+ //
3
+ // Two surfaces:
4
+ // - GET /, /app.js, /style.css → static files from the public dir.
5
+ // - POST /inject {text} → deliver an inbound message to the agent.
6
+ //
7
+ // The `/inject` route is how the on-device channel servers (Telegram, WhatsApp)
8
+ // reach the one shared agent: they POST a context-tagged line here and it is
9
+ // written into the same pty a browser send uses. `onInject` is injected so the
10
+ // router stays testable without a live pty.
11
+
12
+ import fs from 'node:fs'
13
+ import path from 'node:path'
14
+
15
+ const CONTENT_TYPES = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css' }
16
+ const STATIC = { '/': 'index.html', '/app.js': 'app.js', '/style.css': 'style.css' }
17
+
18
+ /**
19
+ * @param {{ publicDir: string, onInject: (text: string) => void }} deps
20
+ * @returns {(req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => void}
21
+ */
22
+ export function makeRequestHandler({ publicDir, onInject }) {
23
+ return (req, res) => {
24
+ if (req.method === 'POST' && req.url === '/inject') {
25
+ let body = ''
26
+ req.on('data', (c) => (body += c))
27
+ req.on('end', () => {
28
+ let text
29
+ try {
30
+ text = JSON.parse(body).text
31
+ } catch {
32
+ text = undefined
33
+ }
34
+ if (typeof text !== 'string' || text === '') {
35
+ res.writeHead(400, { 'content-type': 'application/json' })
36
+ res.end('{"ok":false,"error":"text required"}')
37
+ return
38
+ }
39
+ onInject(text)
40
+ res.writeHead(200, { 'content-type': 'application/json' })
41
+ res.end('{"ok":true}')
42
+ })
43
+ return
44
+ }
45
+
46
+ const file = STATIC[req.url]
47
+ if (!file) {
48
+ res.writeHead(404)
49
+ res.end('not found')
50
+ return
51
+ }
52
+ fs.readFile(path.join(publicDir, file), (err, data) => {
53
+ if (err) {
54
+ res.writeHead(404)
55
+ res.end('not found')
56
+ return
57
+ }
58
+ res.writeHead(200, { 'content-type': CONTENT_TYPES[path.extname(file)] || 'text/plain' })
59
+ res.end(data)
60
+ })
61
+ }
62
+ }
@@ -22,6 +22,8 @@ import { fileURLToPath } from 'node:url'
22
22
  import pty from 'node-pty'
23
23
  import { WebSocketServer } from 'ws'
24
24
  import { parseTranscript } from './parse-transcript.mjs'
25
+ import { ptyLine } from './inject-line.mjs'
26
+ import { makeRequestHandler } from './request-handler.mjs'
25
27
 
26
28
  const HERE = path.dirname(fileURLToPath(import.meta.url))
27
29
  const PUBLIC_DIR = path.join(HERE, 'public')
@@ -166,28 +168,34 @@ setInterval(() => {
166
168
  }
167
169
  }, 500)
168
170
 
169
- // ---- transport: HTTP static + WebSocket -------------------------------------
171
+ // ---- deliver a message to the agent -----------------------------------------
172
+
173
+ // Start a turn and write the message into the pty. The single source of truth
174
+ // for both the browser send path and the channel `/inject` path, so a channel
175
+ // inbound is delivered byte-for-byte the same way a browser message is. An
176
+ // embedded newline is collapsed by ptyLine so the TUI is not submitted early.
177
+ function submitToAgent(text) {
178
+ turn += 1
179
+ turnActive = true
180
+ turnSawGrowth = false
181
+ turnStart = Date.now()
182
+ lastActivity = Date.now()
183
+ log('inbound', { turn, chars: text.length })
184
+ child.write(ptyLine(text))
185
+ log('spawn', { turn, pty: true, pid: child.pid })
186
+ }
170
187
 
171
- const CONTENT_TYPES = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css' }
172
- const STATIC = { '/': 'index.html', '/app.js': 'app.js', '/style.css': 'style.css' }
188
+ // ---- transport: HTTP static + /inject + WebSocket ---------------------------
173
189
 
174
- const server = http.createServer((req, res) => {
175
- const file = STATIC[req.url]
176
- if (!file) {
177
- res.writeHead(404)
178
- res.end('not found')
179
- return
180
- }
181
- fs.readFile(path.join(PUBLIC_DIR, file), (err, data) => {
182
- if (err) {
183
- res.writeHead(404)
184
- res.end('not found')
185
- return
186
- }
187
- res.writeHead(200, { 'content-type': CONTENT_TYPES[path.extname(file)] || 'text/plain' })
188
- res.end(data)
189
- })
190
- })
190
+ const server = http.createServer(
191
+ makeRequestHandler({
192
+ publicDir: PUBLIC_DIR,
193
+ onInject: (text) => {
194
+ log('inject', { chars: text.length })
195
+ submitToAgent(text)
196
+ },
197
+ }),
198
+ )
191
199
 
192
200
  const wss = new WebSocketServer({ server, path: '/ws' })
193
201
 
@@ -209,18 +217,10 @@ wss.on('connection', (ws) => {
209
217
  return
210
218
  }
211
219
  if (m.type !== 'send' || typeof m.text !== 'string' || m.text === '') return
212
- turn += 1
213
- turnActive = true
214
- turnSawGrowth = false
215
- turnStart = Date.now()
216
- lastActivity = Date.now()
217
- log('inbound', { turn, chars: m.text.length })
218
220
  // Deliver to claude by writing into the pty, then submit with a carriage
219
- // return. This is the TUI typing model — the same input path ttyd uses. An
220
- // embedded newline would submit the TUI early and truncate the message, so
221
- // collapse any newline to a space: v0 chat is single-line per send.
222
- child.write(m.text.replace(/[\r\n]+/g, ' ') + '\r')
223
- log('spawn', { turn, pty: true, pid: child.pid })
221
+ // return. This is the TUI typing model — the same input path ttyd uses, and
222
+ // the same path the channel `/inject` route uses via submitToAgent.
223
+ submitToAgent(m.text)
224
224
  })
225
225
  })
226
226