@rubytech/create-maxy-lite 0.1.5 → 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 (32) hide show
  1. package/index.mjs +15 -14
  2. package/lib/orchestrate.mjs +2 -2
  3. package/lib/paths.mjs +35 -0
  4. package/package.json +1 -1
  5. package/payload/package.json +2 -1
  6. package/payload/skills/README.md +26 -0
  7. package/payload/skills/admin/datetime/SKILL.md +147 -0
  8. package/payload/skills/admin/session-management/SKILL.md +39 -0
  9. package/payload/skills/admin/upgrade/SKILL.md +32 -0
  10. package/payload/skills/browser/SKILL.md +60 -0
  11. package/payload/skills/browser/scripts/cdp.mjs +134 -0
  12. package/payload/skills/browser/scripts/pdf.mjs +38 -0
  13. package/payload/skills/browser/scripts/render.mjs +43 -0
  14. package/payload/skills/browser/scripts/screenshot.mjs +52 -0
  15. package/payload/skills/business-assistant/SKILL.md +110 -0
  16. package/payload/skills/deep-research/SKILL.md +70 -0
  17. package/payload/skills/deep-research/references/citation-styles.md +52 -0
  18. package/payload/skills/deep-research/references/research-modes.md +22 -0
  19. package/payload/skills/deep-research/references/search-strategy.md +24 -0
  20. package/payload/skills/docs/SKILL.md +23 -0
  21. package/payload/skills/docs/references/capability-map.md +25 -0
  22. package/payload/skills/docs/references/getting-started.md +29 -0
  23. package/payload/skills/docs/references/vault-model.md +40 -0
  24. package/payload/skills/email-composition/SKILL.md +107 -0
  25. package/payload/skills/replicate/SKILL.md +63 -0
  26. package/payload/skills/replicate/scripts/replicate-image.mjs +131 -0
  27. package/payload/skills/url-get/SKILL.md +48 -0
  28. package/payload/skills/url-get/scripts/url-get.mjs +93 -0
  29. package/payload/webchat/inject-line.mjs +11 -0
  30. package/payload/webchat/package.json +2 -1
  31. package/payload/webchat/request-handler.mjs +62 -0
  32. package/payload/webchat/server.mjs +31 -31
package/index.mjs CHANGED
@@ -23,7 +23,7 @@ import { fileURLToPath } from 'node:url'
23
23
 
24
24
  import { makeLog } from './lib/log.mjs'
25
25
  import { readPins } from './lib/pins.mjs'
26
- import { PATHS, WEBCHAT_DIR, VALIDATOR_CLI, SKILLS_HOME, launcherPath, launcherScript, payloadLayCommand, ptyLoadCommand, skillsLinkCommand, vaultBindLogin, webchatRunEnv } from './lib/paths.mjs'
26
+ import { PATHS, WEBCHAT_DIR, PTY_NODE, VALIDATOR_CLI, SKILLS_HOME, launcherPath, launcherScript, payloadLayCommand, ptyLoadCommand, ptyDirectLoadCommand, webchatInstallCommand, skillsLinkCommand, vaultBindLogin, webchatRunEnv } from './lib/paths.mjs'
27
27
  import { orchestrate, STEP_NAMES } from './lib/orchestrate.mjs'
28
28
  import { claudeOk, validatorOk, vaultOk, webchatOk, skillsOk, healthcheck } from './lib/healthcheck.mjs'
29
29
 
@@ -117,28 +117,29 @@ const linkSkills = () => {
117
117
  /** Install the app deps (validator's js-yaml) and the webchat deps (node-pty, ws). */
118
118
  // Streamed: the webchat deps build node-pty natively, which is slow and verbose;
119
119
  // a failure carries the child's stderr so the cause is in the log.
120
- const PTY_NODE = `${WEBCHAT_DIR}/node_modules/node-pty/build/Release/pty.node`
121
120
  const installDeps = async () => {
122
121
  const app = await runInStream(`cd ${PATHS.appDir} && npm install --omit=dev`)
123
122
  if (app.code !== 0) throw new Error(`app deps install failed: ${app.stderr.replace(/\s+/g, ' ').trim()}`)
124
- const wc = await runInStream(`cd ${WEBCHAT_DIR} && npm install --omit=dev`)
123
+ // The webchat install runs with scripts enabled so node-pty's own install-time
124
+ // source build produces a pty.node that loads — the exact install the spike's Q2
125
+ // proved works in this proot layer. (Earlier this disabled scripts then forced an
126
+ // `npm rebuild --build-from-source`; that bespoke rebuild's artifact did not load.)
127
+ const wc = await runInStream(webchatInstallCommand())
125
128
  if (wc.code !== 0) throw new Error(`webchat deps install failed: ${wc.stderr.replace(/\s+/g, ' ').trim()}`)
126
- // node-pty ships no linux-arm64 + Node 20 prebuild, so the install above can
127
- // leave pty.node unbuilt when npm has ignore-scripts set. `npm rebuild` re-runs
128
- // node-pty's node-gyp build; the env var forces scripts on regardless of ambient
129
- // config, and --build-from-source keeps it a source compile should a future
130
- // node-pty gain a prebuild fetcher. The toolchain step guarantees python3/make/g++.
131
- const rb = await runInStream(
132
- `cd ${WEBCHAT_DIR} && npm_config_ignore_scripts=false npm rebuild node-pty --build-from-source --foreground-scripts`,
133
- )
134
- if (rb.code !== 0) throw new Error(`node-pty native build failed: ${rb.stderr.replace(/\s+/g, ' ').trim()}`)
135
129
  // Prove the built module actually loads under the guest's Node — this is exactly
136
130
  // what the relay does at launch, so a pass here means no `Failed to load native
137
131
  // module: pty.node` crash. A bare file-presence test would miss an unloadable
138
132
  // binary. The same ptyLoadCommand backs the npm-app skip-guard, so the guard
139
133
  // admits exactly what this asserts. Fail at install time, not at relay launch.
140
- if (runIn(ptyLoadCommand()).code !== 0)
141
- throw new Error(`node-pty native module did not load after build: ${PTY_NODE}`)
134
+ // ptyLoadCommand goes through node-pty's loader, which masks the real cause (it
135
+ // reports only the last-tried prebuilds path's ENOENT). So on failure, require the
136
+ // built artifact directly to recover the actual dlopen error, and carry THAT into
137
+ // err= — the true reason (ABI/NODE_MODULE_VERSION/symbol/ELF), not the swallowed miss.
138
+ if (runIn(ptyLoadCommand()).code !== 0) {
139
+ const direct = runIn(ptyDirectLoadCommand())
140
+ const cause = direct.stderr.replace(/\s+/g, ' ').trim() || `no stderr; ${PTY_NODE}`
141
+ throw new Error(`node-pty native module did not load after build: ${cause}`)
142
+ }
142
143
  }
143
144
 
144
145
  /** Write the `maxy-lite` launcher into the Termux bin dir. */
@@ -113,10 +113,10 @@ const STEPS = [
113
113
  // the launcher is written. The deps probe is `require("node-pty")`, not a
114
114
  // file-presence test: a present-but-unloadable `pty.node` (stale/wrong-ABI
115
115
  // from a prior build) would pass `test -f` yet crash the relay at launch, so
116
- // the guard would skip the very rebuild that repairs it. The load-check is the
116
+ // the guard would skip the very install that repairs it. The load-check is the
117
117
  // same condition `installDeps` asserts post-build (shared ptyLoadCommand), so
118
118
  // the guard admits exactly what the build proves; a non-loadable module fails
119
- // the guard and the step rebuilds.
119
+ // the guard and the step re-installs.
120
120
  done: (c) =>
121
121
  c.runIn(ptyLoadCommand()).code === 0 &&
122
122
  c.runIn('command -v claude').code === 0 &&
package/lib/paths.mjs CHANGED
@@ -21,6 +21,11 @@ export const PATHS = {
21
21
  export const WEBCHAT_DIR = `${PATHS.appDir}/webchat`
22
22
  export const VALIDATOR_CLI = `${PATHS.appDir}/validator/cli.mjs`
23
23
 
24
+ // The native artifact node-pty's source build emits. Absolute so it can be required
25
+ // directly (see ptyDirectLoadCommand), and so the build-output path is asserted in
26
+ // exactly one place.
27
+ export const PTY_NODE = `${WEBCHAT_DIR}/node_modules/node-pty/build/Release/pty.node`
28
+
24
29
  /**
25
30
  * The guest-side command that proves node-pty's native module actually loads
26
31
  * under the guest's Node — `require("node-pty")` exits 0 only when `pty.node` is
@@ -34,6 +39,36 @@ export function ptyLoadCommand({ webchatDir = WEBCHAT_DIR } = {}) {
34
39
  return `cd ${webchatDir} && node -e 'require("node-pty")'`
35
40
  }
36
41
 
42
+ /**
43
+ * The guest-side command that surfaces node-pty's *real* native-load error. It
44
+ * requires the built artifact (`build/Release/pty.node`) directly, so node-pty's
45
+ * `loadNativeModule` — which tries build/Release, build/Debug and prebuilds in turn
46
+ * and on total failure throws wrapping only the *last* path's ENOENT
47
+ * (`Cannot find module './prebuilds/<platform>-<arch>//pty.node'`), swallowing the
48
+ * build/Release dlopen error — is never on the path. A direct require of the
49
+ * artifact reports the actual cause (ABI/`NODE_MODULE_VERSION`/missing-symbol/ELF)
50
+ * on stderr. Diagnostic only: run when `ptyLoadCommand` (the relay's real load path,
51
+ * and the pass/fail gate) fails, to put the true reason into the step's `err=`.
52
+ */
53
+ export function ptyDirectLoadCommand({ ptyNode = PTY_NODE } = {}) {
54
+ return `node -e 'require("${ptyNode}")'`
55
+ }
56
+
57
+ /**
58
+ * The webchat dependency install — the exact incantation the spike's Q2 proved
59
+ * builds *and loads* node-pty in this proot Ubuntu layer: a plain `npm install`
60
+ * with install scripts enabled. node-pty ships no linux-arm64 prebuild, so its
61
+ * own install script compiles `pty.node` from source against the guest's Node,
62
+ * and that artifact loads. `npm_config_ignore_scripts=false` forces scripts on
63
+ * regardless of any ambient ignore-scripts in the Ubuntu image. This deliberately
64
+ * replaces the earlier disable-scripts-then-force-`npm rebuild --build-from-source`
65
+ * workaround, whose rebuilt artifact did not load — the rebuild, not the version,
66
+ * was the divergence from Q2. The toolchain step guarantees python3/make/g++.
67
+ */
68
+ export function webchatInstallCommand({ webchatDir = WEBCHAT_DIR } = {}) {
69
+ return `cd ${webchatDir} && npm_config_ignore_scripts=false npm install --omit=dev`
70
+ }
71
+
37
72
  // The shipped skills, as bundled into the app dir, and the path the on-device
38
73
  // `claude` reads as its personal-skills source. `claude` runs with HOME=/root in
39
74
  // the proot Ubuntu layer, so `~/.claude/skills/*/SKILL.md` resolves under /root —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-lite",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Install maxy-lite on an Android phone: orchestrates proot-distro Ubuntu, glibc Node, claude, the web-chat relay, the vault and its bind-mount — run via npx in bare Termux.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -3,6 +3,7 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "js-yaml": "^4.1.0"
6
+ "js-yaml": "^4.1.0",
7
+ "node-html-markdown": "^1.3.0"
7
8
  }
8
9
  }
@@ -0,0 +1,26 @@
1
+ # maxy-lite skills
2
+
3
+ The capability bundles maxy-lite carries beyond native vault file operations. Each bundle is a folder here; the lite runtime lays these into the assistant's skills directory at install time.
4
+
5
+ ## Layout
6
+
7
+ A bundle is a folder with a `SKILL.md` (the instructions the assistant reads) and, where a capability needs to run something, a `scripts/` directory of small Node ESM scripts run via Bash. Skills are files, so they add no running process. Scripts depend on Node 22 or newer for native `fetch` and `WebSocket`. Some scripts read configuration (`REPLICATE_API_TOKEN`, `CDP_PORT`, `LITE_VAULT`, `LITE_SCRATCH`) from the environment, which the runtime supplies.
8
+
9
+ ## The bundles
10
+
11
+ | Bundle | Form | What it does |
12
+ |---|---|---|
13
+ | `url-get` | skill + script | Fetch a web page faithfully as markdown. |
14
+ | `deep-research` | skill | Research a topic over live web sources, save a cited Note to the vault. |
15
+ | `replicate` | skill + script | Generate an image from a prompt via the Replicate API. |
16
+ | `browser` | skill + scripts | Render a JavaScript page, turn HTML into a PDF, take a screenshot, over the device Chromium. |
17
+ | `admin/datetime` | skill | Timezone queries and deterministic relative-date arithmetic. |
18
+ | `admin/upgrade` | skill | Upgrade the install by re-running the lite installer. |
19
+ | `admin/session-management` | skill | Start, resume, and recall native `claude` sessions. |
20
+ | `docs` | skill | Reference documentation about how lite works, loaded on demand. |
21
+
22
+ ## Decisions recorded here
23
+
24
+ **Browser form.** `browser` ships as a skill with per-call scripts, not a persistent MCP server, because lite's real browser needs are one-shot (render, print, screenshot). A fresh script per call adds no long-lived process, matching lite's zero-process default. Multi-call interactive automation (navigate, click, fill, submit against one live page) genuinely needs a persistent listener and is not built here; it is deferred to a follow-up task if a real need appears.
25
+
26
+ **`update-knowledge` dropped.** The maxy-code `update-knowledge` admin skill refreshes a public agent's `KNOWLEDGE.md` from the graph. Lite has no graph and no public agents, so there is no analogue. It is intentionally not ported.
@@ -0,0 +1,147 @@
1
+ ---
2
+ name: datetime
3
+ description: >
4
+ Timezone queries (current time, conversion, UTC offset, DST) and
5
+ relative-date arithmetic (last Tuesday, two weeks ago, Q3 last year).
6
+ Trigger phrases: "what time is it in", "convert time", "timezone",
7
+ "time difference", "DST", "last X", "N days/weeks/months ago",
8
+ "this/next/previous X", "Qn YYYY", "Qn last year".
9
+ ---
10
+
11
+ # Datetime & Timezone Queries
12
+
13
+ Answer timezone-related questions and relative-date arithmetic using Node.js. Never freelance the math. Relative dates and quarter forms must go through the deterministic one-liner in this skill, which is the single source of truth for date arithmetic in lite. Computing dates by hand drifts on weekday offsets, leap days, and quarter-start months; the one-liner does not.
14
+
15
+ ## When to Activate
16
+
17
+ - User asks for the current time in a named city or timezone
18
+ - User asks to convert a time between timezones
19
+ - User asks about UTC offset, time difference, or DST status for a location
20
+ - User asks "what time is it?" with a location qualifier
21
+ - User uses a relative date: "last Tuesday", "two weeks ago", "yesterday", "this Christmas", "Q3 last year", "Q1 2025"
22
+
23
+ For simple "what time is it?" without a location, answer directly from the `<datetime>` block in the system prompt. No tool use needed.
24
+
25
+ ## Behaviour
26
+
27
+ Use Bash to run a Node.js one-liner with `Intl.DateTimeFormat`. The IANA timezone database is built into Node.js, so no external packages are required.
28
+
29
+ ### Current time in a timezone
30
+
31
+ ```bash
32
+ node -e "console.log(new Intl.DateTimeFormat('en-GB', { weekday:'long', year:'numeric', month:'long', day:'numeric', hour:'2-digit', minute:'2-digit', second:'2-digit', timeZone:'IANA_TZ', timeZoneName:'longOffset' }).format(new Date()))"
33
+ ```
34
+
35
+ Replace `IANA_TZ` with the target timezone identifier (e.g., `America/New_York`, `Asia/Tokyo`, `Europe/Berlin`).
36
+
37
+ ### Time conversion
38
+
39
+ To convert a specific time from one zone to another, construct a `Date` object for the source time and format it in the target zone:
40
+
41
+ ```bash
42
+ node -e "
43
+ const d = new Date('ISO_DATETIME');
44
+ const fmt = (tz) => new Intl.DateTimeFormat('en-GB', { weekday:'long', year:'numeric', month:'long', day:'numeric', hour:'2-digit', minute:'2-digit', timeZone:tz, timeZoneName:'short' }).format(d);
45
+ console.log(fmt('SOURCE_TZ'));
46
+ console.log(fmt('TARGET_TZ'));
47
+ "
48
+ ```
49
+
50
+ ### DST and offset info
51
+
52
+ ```bash
53
+ node -e "
54
+ const now = new Date();
55
+ const opts = { timeZone:'IANA_TZ', timeZoneName:'longOffset' };
56
+ const parts = new Intl.DateTimeFormat('en-GB', opts).formatToParts(now);
57
+ const offset = parts.find(p => p.type === 'timeZoneName')?.value ?? 'unknown';
58
+ // Check DST by comparing Jan and Jul offsets
59
+ const jan = new Date(now.getFullYear(), 0, 1);
60
+ const jul = new Date(now.getFullYear(), 6, 1);
61
+ const janOff = new Intl.DateTimeFormat('en-GB', opts).formatToParts(jan).find(p => p.type === 'timeZoneName')?.value;
62
+ const julOff = new Intl.DateTimeFormat('en-GB', opts).formatToParts(jul).find(p => p.type === 'timeZoneName')?.value;
63
+ const dstActive = janOff !== julOff;
64
+ const inDst = dstActive && offset !== janOff;
65
+ console.log('Current offset:', offset);
66
+ console.log('DST observed:', dstActive);
67
+ console.log('Currently in DST:', inDst);
68
+ "
69
+ ```
70
+
71
+ ## Common City-to-Timezone Mapping
72
+
73
+ When the user names a city, map it to the IANA identifier:
74
+ - London `Europe/London`
75
+ - New York `America/New_York`
76
+ - Los Angeles `America/Los_Angeles`
77
+ - Tokyo `Asia/Tokyo`
78
+ - Sydney `Australia/Sydney`
79
+ - Dubai `Asia/Dubai`
80
+ - Mumbai / Delhi `Asia/Kolkata`
81
+ - Berlin / Paris `Europe/Berlin` / `Europe/Paris`
82
+ - Singapore `Asia/Singapore`
83
+ - Hong Kong `Asia/Hong_Kong`
84
+
85
+ For less common cities, use Bash to search the IANA database:
86
+ ```bash
87
+ node -e "console.log(Intl.supportedValuesOf('timeZone').filter(z => z.toLowerCase().includes('SEARCH_TERM'.toLowerCase())).join('\n'))"
88
+ ```
89
+
90
+ ## Relative date arithmetic
91
+
92
+ When the owner references a relative date ("last Tuesday", "two weeks ago", "yesterday", "this Christmas", "Q3 2024", "Q1 last year"), never compute the result by hand. Use this deterministic one-liner. The reference timestamp defaults to now; pass an ISO string as the second argument to anchor against an earlier moment (e.g. when the owner's message timestamp matters).
93
+
94
+ ```bash
95
+ node -e "
96
+ const REF = process.argv[2] ? new Date(process.argv[2]) : new Date();
97
+ const TXT = process.argv[1];
98
+ const WEEKDAYS = {sun:0,sunday:0,mon:1,monday:1,tue:2,tues:2,tuesday:2,wed:3,weds:3,wednesday:3,thu:4,thur:4,thurs:4,thursday:4,fri:5,friday:5,sat:6,saturday:6};
99
+ const NUMS = {one:1,two:2,three:3,four:4,five:5,six:6,seven:7,eight:8,nine:9,ten:10,eleven:11,twelve:12};
100
+ const sod = d => { const o=new Date(d); o.setUTCHours(0,0,0,0); return o; };
101
+ const addD = (d,n) => { const o=new Date(d); o.setUTCDate(o.getUTCDate()+n); return o; };
102
+ const addM = (d,n) => { const o=new Date(d); o.setUTCMonth(o.getUTCMonth()+n); return o; };
103
+ const addY = (d,n) => { const o=new Date(d); o.setUTCFullYear(o.getUTCFullYear()+n); return o; };
104
+ const base = sod(REF);
105
+ let m;
106
+ if (/\\bevery(\\s+other)?\\s+/i.test(TXT)) { console.log('null (recurring)'); process.exit(0); }
107
+ if (m = TXT.match(/\\b(yesterday|today|tomorrow)\\b/i)) {
108
+ const w = m[1].toLowerCase();
109
+ console.log((w==='yesterday'?addD(base,-1):w==='tomorrow'?addD(base,1):base).toISOString());
110
+ } else if (m = TXT.match(/\\b(last|next|this)\\s+([A-Za-z]+)\\b/i)) {
111
+ const q = m[1].toLowerCase(), t = WEEKDAYS[m[2].toLowerCase()];
112
+ if (t === undefined) { console.log('null'); process.exit(0); }
113
+ const dow = base.getUTCDay();
114
+ let delta;
115
+ if (q==='last') { delta = t-dow; if (delta>=0) delta-=7; }
116
+ else if (q==='next') { delta = t-dow; if (delta<=0) delta+=7; }
117
+ else { const id = dow===0?7:dow, it = t===0?7:t; delta = it-id; }
118
+ console.log(addD(base, delta).toISOString());
119
+ } else if (m = TXT.match(/\\b(a|an|\\d+|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\\s+(day|week|month|year)s?\\s+ago\\b/i)) {
120
+ const w = m[1].toLowerCase();
121
+ const n = (w==='a'||w==='an')?1:(NUMS[w]??Number(w));
122
+ const u = m[2].toLowerCase();
123
+ const d = u==='day'?addD(base,-n):u==='week'?addD(base,-7*n):u==='month'?addM(base,-n):addY(base,-n);
124
+ console.log(d.toISOString());
125
+ } else if (TXT.match(/\\b(?:this\\s+)?Christmas\\b/i)) {
126
+ console.log(new Date(Date.UTC(base.getUTCFullYear(),11,25)).toISOString());
127
+ } else if (TXT.match(/\\b(?:this\\s+)?New\\s+Year(?:'s)?(?:\\s+Day)?\\b/i)) {
128
+ console.log(new Date(Date.UTC(base.getUTCFullYear(),0,1)).toISOString());
129
+ } else if (m = TXT.match(/\\bQ([1-4])\\s+(\\d{2}|\\d{4})\\b/i)) {
130
+ let y = Number(m[2]); if (y<100) y+=2000;
131
+ console.log(new Date(Date.UTC(y,(Number(m[1])-1)*3,1)).toISOString());
132
+ } else if (m = TXT.match(/\\bQ([1-4])\\s+(last|this|next)\\s+year\\b/i)) {
133
+ const refY = base.getUTCFullYear();
134
+ const y = m[2].toLowerCase()==='last'?refY-1:m[2].toLowerCase()==='next'?refY+1:refY;
135
+ console.log(new Date(Date.UTC(y,(Number(m[1])-1)*3,1)).toISOString());
136
+ } else { console.log('null'); }
137
+ " "PHRASE" "OPTIONAL_REF_ISO"
138
+ ```
139
+
140
+ Replace `PHRASE` with the relative phrase ("last Tuesday", "Q3 2024") and optionally `OPTIONAL_REF_ISO` with the ISO reference timestamp. Recurring forms ("every other Friday") return `null` by design. Phrases that don't match return `null`.
141
+
142
+ ## Boundaries
143
+
144
+ - Answers timezone questions and relative-date arithmetic only. Not scheduling, reminders, or alarms.
145
+ - Uses the device system clock. If the clock is wrong, answers will be wrong.
146
+ - Historical timezone data (e.g., "what time was it in Tokyo on 15 March 1990?") depends on the Node.js ICU dataset and may be inaccurate for dates before 1970.
147
+ - The relative-date parser handles single-date phrases only; multi-date ranges ("from March through May") are out of scope.
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: session-management
3
+ description: "Start a new session, resume a previous one, or recall what a past session was about. Triggers when the owner says 'start a new session', 'continue the last session', 'pick up where we left off', 'what were we doing last time', or asks about a past conversation."
4
+ ---
5
+
6
+ # Session management
7
+
8
+ In maxy-lite a session is one run of the native `claude` CLI. There is no platform session machinery: no session-reset/list/resume tools, no injected previous-context block, no managed-context mode. This skill is the thin layer over what `claude` itself provides, for the moments the owner names a session intent directly.
9
+
10
+ ## What a lite session is
11
+
12
+ Each `claude` run writes its transcript to a JSONL file under `~/.claude/projects/<project-dir>/<session-id>.jsonl`. That file is the durable record of the conversation. Resuming a session means starting `claude` against that file again.
13
+
14
+ ## Starting a new session
15
+
16
+ When the owner wants to start fresh or clear the conversation: before they do, persist anything that should outlive the conversation to the vault. The new session starts with no memory of this one except what is written to files. Save open tasks as vault Task files, profile or decision changes as the relevant vault entity, then tell the owner what was saved and that they can start a new `claude` session.
17
+
18
+ ## Resuming a previous session
19
+
20
+ `claude` resumes by session id:
21
+
22
+ ```bash
23
+ claude --resume <session-id>
24
+ ```
25
+
26
+ - **The most recent session.** List the transcripts newest-first and take the top one:
27
+ ```bash
28
+ ls -t ~/.claude/projects/*/*.jsonl | head -1
29
+ ```
30
+ The basename (without `.jsonl`) is the session id. Resume it.
31
+ - **A specific past session by topic.** List several recent transcripts, read the start of each to find the one the owner means, then resume that id.
32
+
33
+ ## Recalling what a past session was about
34
+
35
+ When the owner asks "what were we doing last time", do not guess. Read the most recent transcript (the newest `.jsonl` above) and summarise it from its actual contents. The transcript is the source of truth, not memory.
36
+
37
+ ## What this skill does not do
38
+
39
+ It does not edit transcripts, delete past sessions, or compact context. Those are not lite features. If the owner asks for them, say plainly that lite keeps every session as a plain transcript file and does not manage them beyond new and resume.
@@ -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
+ }