@kaydash9999/banker-plugins 0.15.2 → 0.16.1

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 (41) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +65 -0
  4. package/README.md +31 -9
  5. package/codex/manifest.json +6 -3
  6. package/codex/transform-matrix.md +2 -0
  7. package/commands/progress.md +10 -0
  8. package/commands/setup.md +1 -1
  9. package/hooks/graceful-pause.mjs +31 -71
  10. package/hooks/hooks.json +11 -1
  11. package/hooks/progress-fallback.mjs +10 -0
  12. package/hooks/progress.mjs +427 -0
  13. package/hooks/register.mjs +82 -5
  14. package/package.json +1 -1
  15. package/skills/3d-intro-build/SKILL.md +238 -122
  16. package/skills/3d-intro-build/references/NOTICE.md +2 -1
  17. package/skills/3d-intro-build/references/assemble.mjs +164 -108
  18. package/skills/3d-intro-build/references/azure-adapter.mjs +204 -44
  19. package/skills/3d-intro-build/references/curate.mjs +573 -267
  20. package/skills/3d-intro-build/references/intro-fixes.css +61 -0
  21. package/skills/3d-intro-build/references/panel-glass.css +55 -0
  22. package/skills/3d-intro-build/references/preview-lib.mjs +256 -0
  23. package/skills/3d-intro-build/references/prompts.md +32 -9
  24. package/skills/3d-intro-build/references/serve.mjs +111 -103
  25. package/skills/3d-intro-build/references/step-nav.css +59 -0
  26. package/skills/3d-intro-build/references/step-nav.js +503 -0
  27. package/skills/3d-intro-build/references/verify-intro.mjs +474 -0
  28. package/skills/3d-intro-build/references/verify.md +45 -0
  29. package/skills/3d-intro-build/references/video-pool.mjs +817 -0
  30. package/skills/3d-intro-setup/SKILL.md +120 -47
  31. package/skills/3d-intro-setup/references/azure-adapter.mjs +204 -44
  32. package/skills/3d-intro-setup/references/video-pool.mjs +817 -0
  33. package/skills/motion-graphic-make/SKILL.md +73 -28
  34. package/skills/motion-graphic-make/references/curate.mjs +607 -0
  35. package/skills/motion-graphic-make/references/preview-lib.mjs +256 -0
  36. package/skills/motion-graphic-make/references/serve.mjs +131 -0
  37. package/skills/omc-patch/SKILL.md +143 -0
  38. package/skills/omc-patch/scripts/omc-patch.mjs +577 -0
  39. package/skills/remains/SKILL.md +125 -0
  40. package/skills/remains/scripts/boxes.mjs +448 -0
  41. package/skills/trouble-shooting/SKILL.md +50 -0
@@ -1,123 +1,131 @@
1
1
  #!/usr/bin/env node
2
2
  /*
3
- * First-party static preview server for the 3d-intro-build skill.
3
+ * Local preview server for an assembled intro (index.html + scrub-engine.js + assets/), so the
4
+ * scroll-scrubbed page is checked in a real browser. The project convention is a self-contained
5
+ * local page on an unused port, never a claude.ai artifact. Shared by 3d-intro-build and
6
+ * motion-graphic-make (scripts/sync-adapter.js keeps the copies identical).
4
7
  *
5
- * Serves an assembled intro directory (index.html + scrub-engine.js + assets/) so the
6
- * scroll-scrubbed page can be previewed in a real browser — the project convention is a
7
- * self-contained local HTML on an unused port, never a claude.ai artifact.
8
- *
9
- * Why a server at all: the page loads each .mp4 with fetch()->Blob (always seekable), and
10
- * mp4 delivery is smoother with HTTP Range (206) support, which file:// cannot provide.
11
- *
12
- * Runtime: Node >=18 builtins ONLY (node:http / node:fs / node:path). No external deps,
13
- * no shell — cross-platform (path.join, path.resolve, path.sep everywhere).
8
+ * Why a server at all: the engine loads each clip with fetch() into a Blob, which file:// cannot
9
+ * serve, and video plays smoother with byte ranges (206).
14
10
  *
15
11
  * Usage:
16
- * node serve.mjs [dir] [--port N]
17
- * dir directory to serve (default: current working directory)
18
- * --port preferred port; falls back through a candidate list if busy/omitted
19
- * On listen it writes <dir>/port.txt and prints PREVIEW http://localhost:<port>/
12
+ * node serve.mjs [dir] [--port N] [--csp strict|"<policy>"] [--state file]
13
+ * node serve.mjs --stop [dir] [--state file]
14
+ * dir folder to serve (default: the current one)
15
+ * --port preferred port; when busy or absent the system picks a free one
16
+ * --csp sends Content-Security-Policy on every response: `strict` (no inline script or
17
+ * style; blob: media) or the production policy as written. Check a page for a site
18
+ * with a CSP under that same policy: a plain server hides CSP errors.
19
+ * --state state file (default: <dir>.server.json beside the folder, never inside it)
20
+ * --stop ends the server the state file records, only when it answers with that PID
21
+ * A start while the recorded server still answers with the same settings only reports its
22
+ * address; with other settings (another folder sharing the --state file, --csp, PREVIEW_HOST, a
23
+ * --port other than the one it listens on or its own start asked for) it exits 1: --stop first.
24
+ * A request whose Host is a DNS name other than localhost is refused (DNS rebinding).
25
+ * On listen it writes the state file and prints PREVIEW http://localhost:<port>/
26
+ * PREVIEW_HOST=0.0.0.0 binds every interface (on-device phone checks) and prints a warning.
27
+ *
28
+ * Runtime: Node >=18 builtins only. No shell, cross-platform.
20
29
  */
21
30
  import http from 'node:http';
22
31
  import fs from 'node:fs';
23
32
  import path from 'node:path';
33
+ import { fileURLToPath } from 'node:url';
34
+ import {
35
+ LOOPBACK, answersPid, cspFrom, decodePath, forgetSelf, hostAllowed, hostWarning, isRunning, listen, portMatches, readState,
36
+ resolveInside, send, sendFile, siblingState, stopServer, urlFor, writeState,
37
+ } from './preview-lib.mjs';
24
38
 
25
- // ---- args ----
26
- const argv = process.argv.slice(2);
27
- let dirArg = null;
28
- let portArg = null;
29
- for (let i = 0; i < argv.length; i++) {
30
- const a = argv[i];
31
- if (a === '--port') portArg = Number(argv[++i]);
32
- else if (a.startsWith('--port=')) portArg = Number(a.slice('--port='.length));
33
- else if (!a.startsWith('--')) dirArg = a;
39
+ function handler(dir, headers, host) {
40
+ return (req, res) => {
41
+ if (!hostAllowed(req.headers.host, [host])) return send(res, 403, 'host not allowed', headers);
42
+ if (req.method !== 'GET' && req.method !== 'HEAD') return send(res, 405, '405', headers);
43
+ const urlPath = decodePath(req.url);
44
+ if (urlPath === null) return send(res, 400, '400', headers);
45
+ if (answersPid(urlPath, res)) return;
46
+ const fp = resolveInside(dir, urlPath === '/' ? '/index.html' : urlPath);
47
+ if (!fp) return send(res, 403, '403', headers);
48
+ sendFile(req, res, fp, headers);
49
+ };
34
50
  }
35
51
 
36
- const DIR = path.resolve(dirArg || process.cwd());
37
- if (!fs.existsSync(DIR) || !fs.statSync(DIR).isDirectory()) {
38
- console.error(`serve: not a directory: ${DIR}`);
39
- process.exit(1);
52
+ /**
53
+ * Serves `dir` until close(). Resolves to { server, port, host, url, stateFile, close }.
54
+ * @param {{ port?: number, host?: string, csp?: string, stateFile?: string }} opts
55
+ */
56
+ export async function startServe(dir, { port, host = process.env.PREVIEW_HOST || LOOPBACK, csp, stateFile } = {}) {
57
+ const root = path.resolve(dir);
58
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) throw new Error(`serve: not a directory: ${root}`);
59
+ const policy = cspFrom(csp);
60
+ const server = http.createServer(handler(root, policy ? { 'Content-Security-Policy': policy } : {}, host));
61
+ const bound = await listen(server, { host, port });
62
+ const url = urlFor(host, bound);
63
+ const file = stateFile ? path.resolve(stateFile) : siblingState(root);
64
+ writeState(file, {
65
+ pid: process.pid, port: bound, requestedPort: port || null, host, url, dir: root, csp: policy, startedAt: new Date().toISOString(),
66
+ });
67
+ const close = () => new Promise((resolve) => { forgetSelf(file); server.close(() => resolve()); });
68
+ return { server, port: bound, host, url, stateFile: file, close };
40
69
  }
41
70
 
42
- const MIME = {
43
- '.html': 'text/html;charset=utf-8',
44
- '.css': 'text/css;charset=utf-8',
45
- '.js': 'text/javascript;charset=utf-8',
46
- '.mjs': 'text/javascript;charset=utf-8',
47
- '.json': 'application/json;charset=utf-8',
48
- '.png': 'image/png',
49
- '.jpg': 'image/jpeg',
50
- '.jpeg': 'image/jpeg',
51
- '.webp': 'image/webp',
52
- '.gif': 'image/gif',
53
- '.svg': 'image/svg+xml',
54
- '.mp4': 'video/mp4',
55
- '.webm': 'video/webm',
56
- '.mov': 'video/quicktime',
57
- };
58
-
59
- const server = http.createServer((req, res) => {
60
- let p = decodeURIComponent((req.url || '/').split('?')[0]);
61
- if (p === '/') p = '/index.html';
71
+ function parseArgs(argv) {
72
+ const o = { dir: null, port: null, csp: null, state: null, stop: false };
73
+ for (let i = 0; i < argv.length; i++) {
74
+ const a = argv[i];
75
+ const [key, inline] = a.includes('=') ? [a.slice(0, a.indexOf('=')), a.slice(a.indexOf('=') + 1)] : [a, null];
76
+ const value = () => (inline !== null ? inline : argv[++i]);
77
+ if (key === '--port') o.port = Number(value());
78
+ else if (key === '--csp') o.csp = value();
79
+ else if (key === '--state') o.state = value();
80
+ else if (key === '--stop') o.stop = true;
81
+ else if (!a.startsWith('--')) o.dir = a;
82
+ }
83
+ return o;
84
+ }
62
85
 
63
- // Resolve inside DIR and reject any path that escapes it (traversal guard).
64
- const fp = path.normalize(path.join(DIR, p));
65
- if (fp !== DIR && !fp.startsWith(DIR + path.sep)) { res.writeHead(403); res.end('403'); return; }
66
- if (!fs.existsSync(fp) || fs.statSync(fp).isDirectory()) { res.writeHead(404); res.end('404'); return; }
86
+ // Nothing left running is a success; a port held by another program, or one that stays open, is not.
87
+ async function stopMain(o, dir) {
88
+ const r = await stopServer(o.state ? path.resolve(o.state) : siblingState(dir));
89
+ console.log(`STOP ${r.reason}`);
90
+ process.exit(['stopped', 'not-running', 'no-state'].includes(r.reason) ? 0 : 1);
91
+ }
67
92
 
68
- const size = fs.statSync(fp).size;
69
- const type = MIME[path.extname(fp).toLowerCase()] || 'application/octet-stream';
70
- const isVideo = type.startsWith('video/');
71
- const range = req.headers.range;
93
+ // Whether the recorded server answers this start: this folder, the same CSP and host, and the port
94
+ // asked for. A state file shared through --state can record another folder's server.
95
+ const sameSettings = (st, o, dir) => path.resolve(String(st.dir || '')) === dir && (st.csp || null) === cspFrom(o.csp)
96
+ && (st.host || LOOPBACK) === (process.env.PREVIEW_HOST || LOOPBACK) && portMatches(st, o.port);
72
97
 
73
- // Range requests (video scrubbing benefits from 206 partial content).
74
- if (range && isVideo) {
75
- const m = /bytes=(\d+)-(\d*)/.exec(range);
76
- if (m) {
77
- const start = parseInt(m[1], 10);
78
- const end = m[2] ? parseInt(m[2], 10) : size - 1;
79
- if (start >= size || end >= size || start > end) {
80
- res.writeHead(416, { 'Content-Range': `bytes */${size}` }); res.end(); return;
81
- }
82
- res.writeHead(206, {
83
- 'Content-Type': type,
84
- 'Content-Range': `bytes ${start}-${end}/${size}`,
85
- 'Accept-Ranges': 'bytes',
86
- 'Content-Length': end - start + 1,
87
- 'Cache-Control': 'no-store',
88
- });
89
- fs.createReadStream(fp, { start, end }).pipe(res);
90
- return;
91
- }
98
+ // A server already running for this state file: the same settings only report its address; other
99
+ // settings (folder, CSP, host, port) never pass for it, so a check meant to run under a CSP cannot
100
+ // run without one, and a page meant for one folder or port is not checked on another.
101
+ async function reportRunning(recorded, o, dir) {
102
+ if (!(await isRunning(recorded))) return false;
103
+ const st = readState(recorded);
104
+ if (sameSettings(st, o, dir)) {
105
+ console.log(`PREVIEW ${st.url} (already running, pid ${st.pid})`);
106
+ return true;
92
107
  }
108
+ console.error(`serve: the state file records a server running with different settings (folder: ${st.dir}, `
109
+ + `csp: ${st.csp ? 'on' : 'off'}, host: ${st.host}, port: ${st.port}); stop it with --stop and start again`);
110
+ process.exit(1);
111
+ }
93
112
 
94
- res.writeHead(200, {
95
- 'Content-Type': type,
96
- 'Content-Length': size,
97
- 'Accept-Ranges': 'bytes',
98
- 'Cache-Control': 'no-store',
99
- });
100
- fs.createReadStream(fp).pipe(res);
101
- });
102
-
103
- // ---- pick a free port (preferred first, then a candidate list) ----
104
- const candidates = [8931, 8932, 8933, 8080, 8090, 8100, 8123, 8321];
105
- const ports = portArg ? [portArg, ...candidates.filter((p) => p !== portArg)] : candidates;
106
- // Default to loopback (matches the printed localhost URL). Set PREVIEW_HOST=0.0.0.0 for LAN / on-device mobile preview.
107
- const HOST = process.env.PREVIEW_HOST || '127.0.0.1';
108
-
109
- (function tryPort(i) {
110
- if (i >= ports.length) { console.error('serve: no free port available'); process.exit(1); }
111
- server.once('error', (e) => {
112
- if (e.code === 'EADDRINUSE') tryPort(i + 1);
113
- else { console.error(e); process.exit(1); }
114
- });
115
- server.listen(ports[i], HOST, () => {
116
- try { fs.writeFileSync(path.join(DIR, 'port.txt'), String(ports[i])); } catch { /* best effort */ }
117
- console.log(`PREVIEW http://${HOST === '127.0.0.1' ? 'localhost' : HOST}:${ports[i]}/`);
118
- console.log(`serving ${DIR}`);
119
- });
120
- })(0);
113
+ async function main() {
114
+ const o = parseArgs(process.argv.slice(2));
115
+ const dir = path.resolve(o.dir || process.cwd());
116
+ if (o.stop) return stopMain(o, dir);
117
+ if (await reportRunning(o.state ? path.resolve(o.state) : siblingState(dir), o, dir)) return;
118
+ const s = await startServe(dir, { port: o.port || undefined, csp: o.csp || undefined, stateFile: o.state || undefined });
119
+ const warn = hostWarning(s.host);
120
+ if (warn) console.warn(warn);
121
+ console.log(`PREVIEW ${s.url}`);
122
+ console.log(`serving ${dir}${o.csp ? ' (with Content-Security-Policy)' : ''} state: ${s.stateFile}`);
123
+ const quit = () => { forgetSelf(s.stateFile); s.server.close(); process.exit(0); };
124
+ process.on('SIGINT', quit);
125
+ process.on('SIGTERM', quit);
126
+ }
121
127
 
122
- process.on('SIGINT', () => { server.close(); process.exit(0); });
123
- process.on('SIGTERM', () => { server.close(); process.exit(0); });
128
+ const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
129
+ if (invokedDirectly) {
130
+ main().catch((e) => { console.error(String(e?.message || e)); process.exit(1); });
131
+ }
@@ -0,0 +1,59 @@
1
+ /* Hints, skip link and main button of step-nav.js (scene-by-scene navigation, opt-in).
2
+ step-nav.js sets on <html>: isn-on, isn-mouse or isn-touch, isn-last (at the last scene),
3
+ isn-ready (the last clip has played to its end and the page has a body to go to), isn-body.
4
+ Colours are variables: set them from the brand (a light pill on a dark site, and so on). */
5
+
6
+ :root {
7
+ --isn-pill-bg: rgba(11, 18, 32, .72);
8
+ --isn-pill-ink: #F1F5F9;
9
+ --isn-skip-bg: rgba(11, 18, 32, .6);
10
+ --isn-main-bg: #F1F5F9;
11
+ --isn-main-ink: #0B1220;
12
+ --isn-focus: #38BDF8;
13
+ }
14
+
15
+ /* Mouse users: a dark pill with a wheel and a down key, bottom centre. It replaces the engine hint. */
16
+ .isn-hint {
17
+ position: fixed; left: 50%; bottom: 26px; z-index: 31; translate: -50% 0;
18
+ display: none; align-items: center; gap: 10px; padding: 9px 16px; border-radius: 999px;
19
+ background: var(--isn-pill-bg); color: var(--isn-pill-ink); pointer-events: none;
20
+ font: 600 .78rem/1.2 system-ui, -apple-system, "Segoe UI", sans-serif; letter-spacing: .02em;
21
+ }
22
+ html.isn-mouse .isn-hint { display: flex; }
23
+ html.isn-mouse .sw-hint { display: none; }
24
+ .isn-hint__wheel { width: 14px; height: 22px; border: 2px solid currentColor; border-radius: 8px; position: relative; opacity: .9; }
25
+ .isn-hint__wheel::after { content: ""; position: absolute; left: 50%; top: 4px; width: 2px; height: 5px; border-radius: 1px; background: currentColor; translate: -50% 0; }
26
+ .isn-hint__key { width: 20px; height: 20px; border: 2px solid currentColor; border-radius: 5px; position: relative; opacity: .9; }
27
+ .isn-hint__key::after { content: ""; position: absolute; left: 50%; top: 4px; width: 6px; height: 6px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; translate: -50% 0; rotate: 45deg; }
28
+
29
+ /* Touch users: the engine's own hint stays up until the last scene (the engine fades it with an
30
+ inline opacity, which !important overrides). Bottom left at 600px and below. */
31
+ html.isn-touch .sw-hint { opacity: 1 !important; }
32
+ @media (max-width: 600px) {
33
+ html.isn-touch .sw-hint { left: 18px; transform: none; align-items: flex-start; }
34
+ }
35
+
36
+ /* A narrow and short window has no room for the mouse hint. */
37
+ @media (max-width: 600px) and (max-height: 700px) {
38
+ .isn-hint { display: none !important; }
39
+ }
40
+
41
+ /* Skip link: in every scene but the last. */
42
+ .isn-skip {
43
+ position: fixed; right: clamp(14px, 2.4vw, 30px); bottom: 22px; z-index: 32;
44
+ padding: 8px 14px; border-radius: 999px; background: var(--isn-skip-bg); color: var(--isn-pill-ink);
45
+ font: 600 .8rem/1.2 system-ui, -apple-system, "Segoe UI", sans-serif; text-decoration: none;
46
+ }
47
+ .isn-skip:focus-visible, .isn-main:focus-visible { outline: 3px solid var(--isn-focus); outline-offset: 3px; }
48
+
49
+ /* Main button: only once the last clip has played to its end. */
50
+ .isn-main {
51
+ position: fixed; left: 50%; bottom: 26px; z-index: 32; translate: -50% 0; display: none;
52
+ padding: 12px 22px; border: 0; border-radius: 999px; background: var(--isn-main-bg); color: var(--isn-main-ink); cursor: pointer;
53
+ font: 700 .95rem/1.2 system-ui, -apple-system, "Segoe UI", sans-serif;
54
+ }
55
+ html.isn-ready .isn-main { display: inline-flex; }
56
+
57
+ /* The last scene keeps the main button alone; the body shows none of these. */
58
+ html.isn-last .isn-hint, html.isn-last .sw-hint, html.isn-last .isn-skip,
59
+ html.isn-body .isn-hint, html.isn-body .sw-hint, html.isn-body .isn-skip, html.isn-body .isn-main { display: none !important; }