@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
package/index.mjs CHANGED
@@ -23,9 +23,9 @@ 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, launcherPath, launcherScript, payloadLayCommand, 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
- import { claudeOk, validatorOk, vaultOk, webchatOk, healthcheck } from './lib/healthcheck.mjs'
28
+ import { claudeOk, validatorOk, vaultOk, webchatOk, skillsOk, healthcheck } from './lib/healthcheck.mjs'
29
29
 
30
30
  const HERE = path.dirname(fileURLToPath(import.meta.url))
31
31
  const PAYLOAD = path.join(HERE, 'payload')
@@ -107,30 +107,39 @@ const layPayload = () => {
107
107
  if (r.code !== 0) throw new Error(`payload lay failed: ${r.stderr.replace(/\s+/g, ' ').trim()}`)
108
108
  }
109
109
 
110
+ /** Symlink the bundled skills into the guest's personal-skills source so `claude` discovers them. */
111
+ // Runs guest-side (the paths are inside the rootfs); idempotent (`ln -sfn`).
112
+ const linkSkills = () => {
113
+ const r = runIn(skillsLinkCommand())
114
+ if (r.code !== 0) throw new Error(`skills link failed: ${r.stderr.replace(/\s+/g, ' ').trim()}`)
115
+ }
116
+
110
117
  /** Install the app deps (validator's js-yaml) and the webchat deps (node-pty, ws). */
111
118
  // Streamed: the webchat deps build node-pty natively, which is slow and verbose;
112
119
  // a failure carries the child's stderr so the cause is in the log.
113
- const PTY_NODE = `${WEBCHAT_DIR}/node_modules/node-pty/build/Release/pty.node`
114
120
  const installDeps = async () => {
115
121
  const app = await runInStream(`cd ${PATHS.appDir} && npm install --omit=dev`)
116
122
  if (app.code !== 0) throw new Error(`app deps install failed: ${app.stderr.replace(/\s+/g, ' ').trim()}`)
117
- 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())
118
128
  if (wc.code !== 0) throw new Error(`webchat deps install failed: ${wc.stderr.replace(/\s+/g, ' ').trim()}`)
119
- // node-pty ships no linux-arm64 + Node 20 prebuild, so the install above can
120
- // leave pty.node unbuilt when npm has ignore-scripts set. `npm rebuild` re-runs
121
- // node-pty's node-gyp build; the env var forces scripts on regardless of ambient
122
- // config, and --build-from-source keeps it a source compile should a future
123
- // node-pty gain a prebuild fetcher. The toolchain step guarantees python3/make/g++.
124
- const rb = await runInStream(
125
- `cd ${WEBCHAT_DIR} && npm_config_ignore_scripts=false npm rebuild node-pty --build-from-source --foreground-scripts`,
126
- )
127
- if (rb.code !== 0) throw new Error(`node-pty native build failed: ${rb.stderr.replace(/\s+/g, ' ').trim()}`)
128
129
  // Prove the built module actually loads under the guest's Node — this is exactly
129
130
  // what the relay does at launch, so a pass here means no `Failed to load native
130
131
  // module: pty.node` crash. A bare file-presence test would miss an unloadable
131
- // binary. Fail at install time, not at relay launch.
132
- if (runIn(`cd ${WEBCHAT_DIR} && node -e 'require("node-pty")'`).code !== 0)
133
- throw new Error(`node-pty native module did not load after build: ${PTY_NODE}`)
132
+ // binary. The same ptyLoadCommand backs the npm-app skip-guard, so the guard
133
+ // admits exactly what this asserts. Fail at install time, not at relay launch.
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
+ }
134
143
  }
135
144
 
136
145
  /** Write the `maxy-lite` launcher into the Termux bin dir. */
@@ -160,7 +169,8 @@ const runHealthcheck = () =>
160
169
  // bound in and the launcher env set — then poll its port for up to 10s, record
161
170
  // whether it bound, and kill it. Run from bare Termux (`run`) because
162
171
  // vaultBindLogin already enters the distro; `runIn` would nest a second login
163
- // with no bind and test the wrong process.
172
+ // with no bind and test the wrong process. Return the full run result so a
173
+ // failed bind carries the relay's stderr (e.g. node-pty load crash) into err=.
164
174
  checkWebchat: () =>
165
175
  webchatOk(async () => {
166
176
  const inner =
@@ -169,18 +179,28 @@ const runHealthcheck = () =>
169
179
  `curl -sf -o /dev/null http://localhost:${PATHS.webchatPort} && break; done; ` +
170
180
  `curl -sf -o /dev/null http://localhost:${PATHS.webchatPort}; RC=$?; ` +
171
181
  `kill $SVPID 2>/dev/null; exit $RC`
172
- return run(vaultBindLogin(inner)).code === 0
182
+ return run(vaultBindLogin(inner))
173
183
  }),
174
- // Validator must exit 0 on an empty vault.
184
+ // Validator must exit 0 on an empty vault. Pass the full result so a non-zero
185
+ // exit carries the validator's stderr into err=.
175
186
  checkValidator: () => {
176
187
  const empty = `${PATHS.appDir}/.hc-empty-vault`
177
188
  runIn(`mkdir -p ${empty}`)
178
- return validatorOk((vault) => runIn(`node ${VALIDATOR_CLI} ${vault}`).code, empty)
189
+ return validatorOk((vault) => runIn(`node ${VALIDATOR_CLI} ${vault}`), empty)
179
190
  },
180
191
  // The vault path only exists inside the distro when the login carries the
181
192
  // launcher's `--bind`. Reproduce that bind, then check the guest path — exactly
182
- // the launcher condition, not a session where the vault was never mounted.
183
- checkVault: () => vaultOk(() => run(vaultBindLogin(`test -d ${PATHS.vaultGuest}`)).code === 0),
193
+ // the launcher condition, not a session where the vault was never mounted. The
194
+ // full result carries the bind/login stderr into err= when the path is absent.
195
+ checkVault: () => vaultOk(() => run(vaultBindLogin(`test -d ${PATHS.vaultGuest}`))),
196
+ // The shipped skills are discoverable only when the personal-skills source
197
+ // carries their SKILL.md files. With nullglob off (the default here), a glob
198
+ // that matches nothing is passed to `ls` literally and `ls` exits non-zero
199
+ // because that path does not exist; a match lists real files and exits 0. So
200
+ // a 0 exit asserts at least one SKILL.md is linked (resolving through the
201
+ // symlinks) — a dangling or empty link dir fails. The full result carries the
202
+ // `ls` stderr into err= when nothing matched.
203
+ checkSkills: () => skillsOk(() => runIn(`ls ${SKILLS_HOME}/*/SKILL.md`)),
184
204
  })
185
205
 
186
206
  // ---- dry run ----------------------------------------------------------------
@@ -237,6 +257,7 @@ async function main() {
237
257
  interactive,
238
258
  ensureStorageGrant,
239
259
  layPayload,
260
+ linkSkills,
240
261
  installDeps,
241
262
  writeLauncher,
242
263
  readVersions,
@@ -1,41 +1,82 @@
1
1
  // Post-install health check — the measured post-conditions, not "ran the step".
2
- // Each probe answers a binary question about the running system:
2
+ // Each probe answers a binary question about the running system AND carries the
3
+ // cause when it fails, so a red probe is diagnosable from the install log alone:
3
4
  // claude — does `claude --version` answer with a version?
4
- // webchat — does the relay port listen?
5
+ // webchat — does the relay port listen? (its crash output is the relay stderr)
5
6
  // validator — does the validator exit 0 on an empty vault?
6
7
  // vault — does the bind-mount path exist inside Ubuntu?
7
- // The probes take injected primitives so the decision rules are unit-testable;
8
- // index.mjs wires the real ones (run-in-Ubuntu, transient port probe, fs stat).
8
+ // skills — does the personal-skills source carry the shipped SKILL.md files?
9
+ // Each probe returns `{ ok, err }`: `ok` is the binary verdict, `err` is the tail
10
+ // of the probe's stderr when it failed (empty when it passed). The probes take
11
+ // injected primitives so the decision rules are unit-testable; index.mjs wires the
12
+ // real ones (run-in-Ubuntu, transient port probe, fs stat), each yielding a
13
+ // `{ code, stdout, stderr }` result so the cause survives into the log.
9
14
 
10
15
  const VERSION_RE = /\d+\.\d+\.\d+/
11
16
 
12
- /** True when `claude --version` exits 0 and prints a version. `runIn(cmd)` → {code, stdout}. */
17
+ /**
18
+ * The diagnostic tail of a probe's output: the last few non-empty lines, with
19
+ * whitespace collapsed and length bounded. The actual failure (e.g. node-pty's
20
+ * `Failed to load native module`) is at the END of a crash's stderr, so the tail
21
+ * carries the cause; the cap keeps one `op=healthcheck` line readable.
22
+ */
23
+ function tail(text, lines = 5, cap = 300) {
24
+ return String(text || '')
25
+ .split('\n')
26
+ .filter((l) => l.trim())
27
+ .slice(-lines)
28
+ .join(' ')
29
+ .replace(/\s+/g, ' ')
30
+ .trim()
31
+ .slice(0, cap)
32
+ }
33
+
34
+ /** `{ ok, err }` from a `{ code, stdout, stderr }` result: err is the stderr tail on failure. */
35
+ function verdict(ok, r) {
36
+ return { ok, err: ok ? '' : tail(r && (r.stderr || r.stdout)) }
37
+ }
38
+
39
+ /** True when `claude --version` exits 0 and prints a version. `runVersion()` → {code, stdout, stderr}. */
13
40
  export function claudeOk(runVersion) {
14
41
  const r = runVersion()
15
- return r.code === 0 && VERSION_RE.test(r.stdout || '')
42
+ return verdict(r.code === 0 && VERSION_RE.test(r.stdout || ''), r)
16
43
  }
17
44
 
18
- /** True when the validator exits 0 on the given (empty) vault. `runValidator(vault)` → exit code. */
45
+ /** True when the validator exits 0 on the given (empty) vault. `runValidator(vault)` → {code, stderr}. */
19
46
  export function validatorOk(runValidator, vaultPath) {
20
- return runValidator(vaultPath) === 0
47
+ const r = runValidator(vaultPath)
48
+ return verdict(r.code === 0, r)
21
49
  }
22
50
 
23
- /** True when the bind-mount path exists. `exists()` → bool. */
24
- export function vaultOk(exists) {
25
- return exists() === true
51
+ /** True when the bind-mount path exists. `check()` → {code, stderr}. */
52
+ export function vaultOk(check) {
53
+ const r = check()
54
+ return verdict(r.code === 0, r)
26
55
  }
27
56
 
28
- /** True when the relay port responded. `getStatus()` → Promise<bool>. */
57
+ /** True when the relay port responded. `getStatus()` → Promise<{code, stderr}>. */
29
58
  export async function webchatOk(getStatus) {
30
- return (await getStatus()) === true
59
+ const r = await getStatus()
60
+ return verdict(r.code === 0, r)
61
+ }
62
+
63
+ /** True when the shipped skills are present in the personal-skills source. `listSkills()` → {code, stderr} of an `ls` of the per-skill SKILL.md glob under SKILLS_HOME; err carries the ls stderr (e.g. "No such file") when nothing matched. */
64
+ export function skillsOk(listSkills) {
65
+ const r = listSkills()
66
+ return verdict(r.code === 0, r)
31
67
  }
32
68
 
33
- /** Run all four probes and return `{ claude, webchat, validator, vault }`. */
34
- export async function healthcheck({ checkClaude, checkWebchat, checkValidator, checkVault }) {
69
+ /**
70
+ * Run all five probes and return `{ claude, webchat, validator, vault, skills }`,
71
+ * each a `{ ok, err }` record. orchestrate flattens this into the `op=healthcheck`
72
+ * line: `<probe>=<ok>` for every probe, plus `<probe>Err=<cause>` for each failure.
73
+ */
74
+ export async function healthcheck({ checkClaude, checkWebchat, checkValidator, checkVault, checkSkills }) {
35
75
  return {
36
- claude: checkClaude() === true,
37
- webchat: (await checkWebchat()) === true,
38
- validator: checkValidator() === true,
39
- vault: checkVault() === true,
76
+ claude: checkClaude(),
77
+ webchat: await checkWebchat(),
78
+ validator: checkValidator(),
79
+ vault: checkVault(),
80
+ skills: checkSkills(),
40
81
  }
41
82
  }
@@ -15,7 +15,7 @@
15
15
  // All external effects go through injected ctx primitives so the sequence,
16
16
  // guards and idempotency are unit-testable; index.mjs wires the real ones.
17
17
 
18
- import { PATHS, WEBCHAT_DIR, launcherPath } from './paths.mjs'
18
+ import { PATHS, launcherPath, ptyLoadCommand } from './paths.mjs'
19
19
 
20
20
  export const STEP_NAMES = ['termux-deps', 'proot', 'ubuntu', 'node', 'toolchain', 'vault-bind', 'npm-app']
21
21
 
@@ -109,13 +109,16 @@ const STEPS = [
109
109
  },
110
110
  {
111
111
  name: 'npm-app',
112
- // Converged only when node-pty's native module is built, claude is present,
113
- // AND the launcher is written. The deps probe is the built `pty.node`, not the
114
- // node_modules dir: node-pty ships no linux-arm64 prebuild, so a dir-present
115
- // install can still lack the native module and crash the relay at launch.
116
- // Guarding on pty.node makes a re-run rebuild it instead of skipping ok=true.
112
+ // Converged only when node-pty's native module LOADS, claude is present, AND
113
+ // the launcher is written. The deps probe is `require("node-pty")`, not a
114
+ // file-presence test: a present-but-unloadable `pty.node` (stale/wrong-ABI
115
+ // from a prior build) would pass `test -f` yet crash the relay at launch, so
116
+ // the guard would skip the very install that repairs it. The load-check is the
117
+ // same condition `installDeps` asserts post-build (shared ptyLoadCommand), so
118
+ // the guard admits exactly what the build proves; a non-loadable module fails
119
+ // the guard and the step re-installs.
117
120
  done: (c) =>
118
- c.runIn(`test -f ${WEBCHAT_DIR}/node_modules/node-pty/build/Release/pty.node`).code === 0 &&
121
+ c.runIn(ptyLoadCommand()).code === 0 &&
119
122
  c.runIn('command -v claude').code === 0 &&
120
123
  c.existsHost(launcherPath()),
121
124
  run: async (c) => {
@@ -127,6 +130,9 @@ const STEPS = [
127
130
  ),
128
131
  )
129
132
  c.layPayload()
133
+ // The payload now carries the skills tree into the app dir; link each skill
134
+ // into the on-device personal-skills source so `claude` discovers them.
135
+ c.linkSkills()
130
136
  await c.installDeps()
131
137
  c.writeLauncher()
132
138
  },
@@ -168,10 +174,25 @@ async function runStep(step, ctx) {
168
174
  return { name: step.name, ok, skipped, ms }
169
175
  }
170
176
 
177
+ /**
178
+ * Flatten the healthcheck record (`{ probe: { ok, err } }`) into one line's
179
+ * fields: `<probe>=<ok>` for every probe, in order, plus `<probe>Err=<cause>`
180
+ * immediately after any probe that failed. A healthy run carries no err fields.
181
+ */
182
+ function healthcheckFields(hc) {
183
+ const fields = {}
184
+ for (const [name, p] of Object.entries(hc)) {
185
+ fields[name] = p.ok
186
+ if (!p.ok && p.err) fields[`${name}Err`] = p.err
187
+ }
188
+ return fields
189
+ }
190
+
171
191
  /**
172
192
  * Run the full install. Stops at the first failed step (logs `done ok=false`,
173
- * no healthcheck). On success: emits the resolved versions, the four-probe
174
- * healthcheck, and `done ok=<all probes true>`.
193
+ * no healthcheck). On success: emits the resolved versions, the five-probe
194
+ * healthcheck (each probe's ok plus its captured err when false), and
195
+ * `done ok=<all probes true>`.
175
196
  */
176
197
  export async function orchestrate(ctx) {
177
198
  const t0 = ctx.now()
@@ -187,8 +208,8 @@ export async function orchestrate(ctx) {
187
208
  const v = ctx.readVersions()
188
209
  ctx.log('versions', { node: v.node, claude: v.claude, ttyd: v.ttyd })
189
210
  const hc = await ctx.runHealthcheck()
190
- ctx.log('healthcheck', hc)
191
- const ok = Object.values(hc).every(Boolean)
211
+ ctx.log('healthcheck', healthcheckFields(hc))
212
+ const ok = Object.values(hc).every((p) => p.ok)
192
213
  ctx.log('done', { ok, ranMs: ctx.now() - t0 })
193
214
  return { ok, results, healthcheck: hc }
194
215
  }
package/lib/paths.mjs CHANGED
@@ -21,6 +21,79 @@ 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
+
29
+ /**
30
+ * The guest-side command that proves node-pty's native module actually loads
31
+ * under the guest's Node — `require("node-pty")` exits 0 only when `pty.node` is
32
+ * both present and loadable (right ABI). This is the single construction of the
33
+ * load-check, shared by the `npm-app` skip-guard and `installDeps`' post-build
34
+ * assertion so the two can never drift: the guard admits exactly the modules the
35
+ * build asserts. A bare `test -f pty.node` would pass on a present-but-unloadable
36
+ * binary (stale/wrong-ABI from a prior build) and wrongly skip the rebuild.
37
+ */
38
+ export function ptyLoadCommand({ webchatDir = WEBCHAT_DIR } = {}) {
39
+ return `cd ${webchatDir} && node -e 'require("node-pty")'`
40
+ }
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
+
72
+ // The shipped skills, as bundled into the app dir, and the path the on-device
73
+ // `claude` reads as its personal-skills source. `claude` runs with HOME=/root in
74
+ // the proot Ubuntu layer, so `~/.claude/skills/*/SKILL.md` resolves under /root —
75
+ // discovered regardless of `claude`'s working directory (the vault). Skills are
76
+ // shipped app content, so they live in the rootfs app dir, not in the shared
77
+ // Obsidian vault; the install links them into SKILLS_HOME from this one source.
78
+ export const SKILLS_SRC = `${PATHS.appDir}/skills`
79
+ export const SKILLS_HOME = '/root/.claude/skills'
80
+
81
+ /**
82
+ * The guest-side command that makes the bundled skills discoverable: ensure the
83
+ * personal-skills dir exists, then symlink each shipped skill dir into it. One
84
+ * source of truth (the app dir); `ln -sfn` is idempotent and leaves any sibling
85
+ * personal skills untouched. `shopt -s nullglob` makes an empty or absent skills
86
+ * source expand to zero iterations instead of bash's default unmatched-glob
87
+ * behaviour, which would otherwise pass the literal pattern through, create a
88
+ * dangling symlink named `*`, and still exit 0. So the empty case is a clean
89
+ * no-op that the `skills` health probe then reports false, rather than a silent
90
+ * false success. Run inside the distro (the paths are guest paths). `src` and
91
+ * `home` are pinned constants, never user input, so there is no injection surface.
92
+ */
93
+ export function skillsLinkCommand({ src = SKILLS_SRC, home = SKILLS_HOME } = {}) {
94
+ return `shopt -s nullglob; mkdir -p ${home} && for d in ${src}/*/; do ln -sfn "$d" "${home}/$(basename "$d")"; done`
95
+ }
96
+
24
97
  // Where the host payload is bind-mounted inside the guest while it is copied into
25
98
  // place. A throwaway path under the rootfs's own /tmp, distinct from the app dir.
26
99
  export const PAYLOAD_STAGE = '/tmp/maxy-lite-payload'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-lite",
3
- "version": "0.1.4",
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.