@rubytech/create-maxy-lite 0.1.5 → 0.1.7
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 +15 -14
- package/lib/orchestrate.mjs +2 -2
- package/lib/paths.mjs +40 -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/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/replicate/SKILL.md +63 -0
- package/payload/skills/replicate/scripts/replicate-image.mjs +131 -0
- package/payload/skills/url-get/SKILL.md +48 -0
- package/payload/skills/url-get/scripts/url-get.mjs +93 -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
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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. */
|
package/lib/orchestrate.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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,41 @@ 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
|
+
// Reproduce the spike's exact working command (`npm install node-pty` in the
|
|
70
|
+
// Ubuntu layer, scripts on) verbatim — that built a loadable pty.node. `npm
|
|
71
|
+
// install node-pty` also resolves the rest of package.json (ws), so it covers
|
|
72
|
+
// the webchat's deps too. The env prefix forces scripts on to match the spike's
|
|
73
|
+
// environment regardless of any ambient ignore-scripts in the Ubuntu image.
|
|
74
|
+
return `cd ${webchatDir} && npm_config_ignore_scripts=false npm install node-pty`
|
|
75
|
+
}
|
|
76
|
+
|
|
37
77
|
// The shipped skills, as bundled into the app dir, and the path the on-device
|
|
38
78
|
// `claude` reads as its personal-skills source. `claude` runs with HOME=/root in
|
|
39
79
|
// 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.
|
|
3
|
+
"version": "0.1.7",
|
|
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": {
|
package/payload/package.json
CHANGED
|
@@ -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
|
+
}
|