shadok-ai 0.8.26 → 0.8.28
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/context/agents-skill/pilotctl.mjs +42 -2
- package/package.json +1 -1
- package/public/index.html +74 -62
|
@@ -6,7 +6,22 @@ import fs from "node:fs";
|
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
|
|
9
|
+
// The WebSocket implementation, chosen at load: Node's BUILT-IN global first,
|
|
10
|
+
// the `ws` package only as a fallback.
|
|
11
|
+
//
|
|
12
|
+
// Why the built-in first: seeded into ~/.claude/skills so any agent can pilot,
|
|
13
|
+
// this file has no node_modules to resolve `ws` from, and the bare import broke
|
|
14
|
+
// every agent piloting outside the repo with ERR_MODULE_NOT_FOUND.
|
|
15
|
+
//
|
|
16
|
+
// Why `ws` is still needed: the global WebSocket is only unflagged from Node 22.
|
|
17
|
+
// This project supports Node >= 20 (README, and ci.yml runs the suite on 20),
|
|
18
|
+
// where `globalThis.WebSocket` is undefined — dropping the package outright
|
|
19
|
+
// traded a failure that hit agents outside the repo for one that hits EVERY
|
|
20
|
+
// Node 20 user, in-repo included. The `.catch` keeps the absent-package case
|
|
21
|
+
// (seeded copy) from throwing at import time, so the pure helpers this module
|
|
22
|
+
// exports stay usable; `makeWS` reports it instead, when someone actually
|
|
23
|
+
// connects.
|
|
24
|
+
const WS = globalThis.WebSocket ?? (await import("ws").catch(() => null))?.default;
|
|
10
25
|
|
|
11
26
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
27
|
// Only used to auto-start a server when none is reachable (a dev running pilotctl
|
|
@@ -24,6 +39,31 @@ export const wsUrl = () => `ws://localhost:${port()}/ws`;
|
|
|
24
39
|
export const authHeaders = () => (process.env.SHADOK_AUTH ? { cookie: process.env.SHADOK_AUTH } : {});
|
|
25
40
|
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
26
41
|
|
|
42
|
+
// The built-in WebSocket is WHATWG (addEventListener + event.data); the rest of
|
|
43
|
+
// this file uses the `ws` package's EventEmitter surface. Bridge them: `.on` /
|
|
44
|
+
// `.once`, handing a `message` listener the raw payload (event.data — a string
|
|
45
|
+
// for the JSON frames the server sends). Only these two methods are used here.
|
|
46
|
+
// `ws` already has them, so it is returned untouched.
|
|
47
|
+
//
|
|
48
|
+
// Both accept the non-standard `{ headers }` option, which is what carries the
|
|
49
|
+
// SHADOK_AUTH cookie past the password gate — verified against a real upgrade
|
|
50
|
+
// handler on the built-in, not assumed.
|
|
51
|
+
function makeWS(url, opts) {
|
|
52
|
+
if (!WS)
|
|
53
|
+
throw new Error(
|
|
54
|
+
"no WebSocket available: Node < 22 has no global one, and the `ws` package " +
|
|
55
|
+
"is not resolvable from here (a globally-seeded skill has no node_modules). " +
|
|
56
|
+
"Run pilotctl from the repo, or upgrade to Node 22+.",
|
|
57
|
+
);
|
|
58
|
+
const ws = new WS(url, opts);
|
|
59
|
+
if (typeof ws.on === "function") return ws; // the `ws` package: already an emitter
|
|
60
|
+
const add = (type, fn, once) =>
|
|
61
|
+
ws.addEventListener(type, type === "message" ? (e) => fn(e.data) : fn, once ? { once: true } : undefined);
|
|
62
|
+
ws.on = (type, fn) => (add(type, fn, false), ws);
|
|
63
|
+
ws.once = (type, fn) => (add(type, fn, true), ws);
|
|
64
|
+
return ws;
|
|
65
|
+
}
|
|
66
|
+
|
|
27
67
|
export function stateDir() {
|
|
28
68
|
return process.env.SHADOK_STATE_DIR ?? path.join(os.homedir(), ".shadok-ai", "pilotctl");
|
|
29
69
|
}
|
|
@@ -69,7 +109,7 @@ export function parseArgs(argv) {
|
|
|
69
109
|
|
|
70
110
|
function connect() {
|
|
71
111
|
return new Promise((resolve, reject) => {
|
|
72
|
-
const ws =
|
|
112
|
+
const ws = makeWS(wsUrl(), { headers: authHeaders() });
|
|
73
113
|
ws.once("open", () => resolve(ws));
|
|
74
114
|
ws.once("error", reject);
|
|
75
115
|
});
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<title>shadok-ai — cockpit</title>
|
|
12
12
|
<!-- The idle favicon: a resting Shadok. paint() swaps in the pumping / "!" / "?"
|
|
13
13
|
variants once the JS runs; this is only the pre-script icon. -->
|
|
14
|
-
<link rel="icon" id="favicon" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2032%2032%22%3E%3Crect%20width%3D%2232%22%20height%3D%2232%22%20rx%3D%227%22%20fill%3D%22%2314161d%22%2F%3E%
|
|
14
|
+
<link rel="icon" id="favicon" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2032%2032%22%3E%3Crect%20width%3D%2232%22%20height%3D%2232%22%20rx%3D%227%22%20fill%3D%22%2314161d%22%2F%3E%3Cline%20x1%3D%2210.8%22%20y1%3D%2223%22%20x2%3D%2210.4%22%20y2%3D%2230.6%22%20stroke%3D%22%23f0a848%22%20stroke-width%3D%221.3%22%20stroke-linecap%3D%22round%22%2F%3E%3Cline%20x1%3D%2216.400000000000002%22%20y1%3D%2223%22%20x2%3D%2216.8%22%20y2%3D%2230.6%22%20stroke%3D%22%23f0a848%22%20stroke-width%3D%221.3%22%20stroke-linecap%3D%22round%22%2F%3E%3Cline%20x1%3D%228.8%22%20y1%3D%2230.7%22%20x2%3D%2211.9%22%20y2%3D%2230.7%22%20stroke%3D%22%23f0a848%22%20stroke-width%3D%221.3%22%20stroke-linecap%3D%22round%22%2F%3E%3Cline%20x1%3D%2215.4%22%20y1%3D%2230.7%22%20x2%3D%2218.5%22%20y2%3D%2230.7%22%20stroke%3D%22%23f0a848%22%20stroke-width%3D%221.3%22%20stroke-linecap%3D%22round%22%2F%3E%3Cline%20x1%3D%2219.6%22%20y1%3D%2217.2%22%20x2%3D%2224.000000000000004%22%20y2%3D%2220.4%22%20stroke%3D%22%23f0a848%22%20stroke-width%3D%221.3%22%20stroke-linecap%3D%22round%22%2F%3E%3Cg%3E%3Cline%20x1%3D%2211.600000000000001%22%20y1%3D%225.399999999999999%22%20x2%3D%229.600000000000001%22%20y2%3D%222.8%22%20stroke%3D%22%23f0a848%22%20stroke-width%3D%220.9%22%20stroke-linecap%3D%22round%22%2F%3E%3Ccircle%20cx%3D%229.600000000000001%22%20cy%3D%222.8%22%20r%3D%221%22%20fill%3D%22%23f0a848%22%2F%3E%3Cline%20x1%3D%2216%22%20y1%3D%225.399999999999999%22%20x2%3D%2218%22%20y2%3D%222.8%22%20stroke%3D%22%23f0a848%22%20stroke-width%3D%220.9%22%20stroke-linecap%3D%22round%22%2F%3E%3Ccircle%20cx%3D%2218%22%20cy%3D%222.8%22%20r%3D%221%22%20fill%3D%22%23f0a848%22%2F%3E%3Ccircle%20cx%3D%2213.8%22%20cy%3D%2216.2%22%20r%3D%228.4%22%20fill%3D%22%23f0a848%22%2F%3E%3Cpath%20d%3D%22M12.200000000000001%2018.2%20L15.4%2018.2%20L13.8%2021.6%20Z%22%20fill%3D%22%23f0a848%22%2F%3E%3Ccircle%20cx%3D%2210.9%22%20cy%3D%227.799999999999999%22%20r%3D%222.6%22%20fill%3D%22%23fff%22%2F%3E%3Ccircle%20cx%3D%2216.7%22%20cy%3D%227.799999999999999%22%20r%3D%222.6%22%20fill%3D%22%23fff%22%2F%3E%3Ccircle%20cx%3D%2211.200000000000001%22%20cy%3D%228.1%22%20r%3D%221.3%22%20fill%3D%22%2314161d%22%2F%3E%3Ccircle%20cx%3D%2217%22%20cy%3D%228.1%22%20r%3D%221.3%22%20fill%3D%22%2314161d%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E">
|
|
15
15
|
<style>
|
|
16
16
|
:root {
|
|
17
17
|
--bg: #14161d;
|
|
@@ -1017,17 +1017,12 @@
|
|
|
1017
1017
|
}
|
|
1018
1018
|
@keyframes tw-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
|
|
1019
1019
|
/* The pumping Shadok: `color` feeds its `currentColor` fill so it follows the
|
|
1020
|
-
palette.
|
|
1020
|
+
palette. It is animated by re-drawing in JS (renderTw), not CSS — the pump's
|
|
1021
|
+
arm has to be re-linked to the lever each frame. */
|
|
1021
1022
|
#thinking .tw-shadok { display: inline-flex; color: var(--amber); }
|
|
1022
1023
|
#thinking .tw-shadok svg { width: 22px; height: 22px; overflow: visible; }
|
|
1023
|
-
/* The whole Shadok (body + arm + handle) bobs with translate — no
|
|
1024
|
-
transform-origin, which is a cross-browser trap on SVG — while the pump rod
|
|
1025
|
-
and legs stay planted, so the handle strokes up and down the rod. */
|
|
1026
|
-
#thinking .tw-shadok .sh-bob { animation: tw-bob 0.55s ease-in-out infinite; }
|
|
1027
|
-
@keyframes tw-bob { 0%, 100% { transform: translateY(-1.8px); } 50% { transform: translateY(2.2px); } }
|
|
1028
1024
|
@media (prefers-reduced-motion: reduce) {
|
|
1029
1025
|
#thinking .tw-text { animation: none; color: var(--phosphor); -webkit-text-fill-color: currentColor; }
|
|
1030
|
-
#thinking .tw-shadok .sh-bob { animation: none; }
|
|
1031
1026
|
}
|
|
1032
1027
|
|
|
1033
1028
|
/* Composer */
|
|
@@ -3241,50 +3236,53 @@
|
|
|
3241
3236
|
const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
3242
3237
|
return v || fallback;
|
|
3243
3238
|
}
|
|
3244
|
-
/** The Shadok itself — the cockpit's mascot, which famously PUMPS. Drawn
|
|
3245
|
-
*
|
|
3246
|
-
*
|
|
3247
|
-
*
|
|
3248
|
-
*
|
|
3249
|
-
*
|
|
3250
|
-
*
|
|
3251
|
-
*
|
|
3252
|
-
*
|
|
3253
|
-
*
|
|
3254
|
-
*
|
|
3255
|
-
*
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
function shadokInner(fill, { bob = 0, pump = false, anim = false, sym = null, symColor } = {}) {
|
|
3239
|
+
/** The Shadok itself — the cockpit's mascot, which famously PUMPS. Drawn like
|
|
3240
|
+
* the real thing (Rouxel): a round BALL body, two big googly eyes perched on
|
|
3241
|
+
* top, two antennae, a tiny beak, thin stick legs. One drawing serves every
|
|
3242
|
+
* state. `lever` (degrees around the fulcrum) poses the see-saw hand-pump it
|
|
3243
|
+
* works — pass a number to show the pump, null for the idle stub arm; `bob`
|
|
3244
|
+
* dips the ball with the effort. The arm is recomputed each call from the
|
|
3245
|
+
* (bobbed) shoulder to the lever end it grips, so the linkage is always right —
|
|
3246
|
+
* animating the arm with CSS instead made it swing around the pivot, backwards.
|
|
3247
|
+
* `sym` stamps a bold "!" / "?" over the ball, outlined in the ground colour so
|
|
3248
|
+
* the accent-on-accent "?" still reads (eyes stay clear above it). Legs and the
|
|
3249
|
+
* fulcrum stay planted. Eyes are always white + dark, never the accent, so they
|
|
3250
|
+
* read on any `fill` (the `currentColor` indicator included). */
|
|
3251
|
+
const PX = 23.2, PY = 24.4; // the pump lever's fulcrum
|
|
3252
|
+
function shadokInner(fill, { lever = null, bob = 0, sym = null, symColor } = {}) {
|
|
3259
3253
|
const base = themeColor("--bg", "#14161d");
|
|
3260
|
-
const
|
|
3261
|
-
const
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3254
|
+
const cx = 13.8, cy = 16.2, r = 8.4, eyeY = cy - 8.4, eyeSep = 5.8, eyeR = 2.6, pupR = 1.3;
|
|
3255
|
+
const shX = cx + r - 2.6, shY = cy + 1.8 + bob; // shoulder (moves with the bob)
|
|
3256
|
+
let fulcrum = "", leverEl = "", armEl = "";
|
|
3257
|
+
if (lever !== null) {
|
|
3258
|
+
const rad = (lever * Math.PI) / 180, nearX = PX - 6.5, farX = PX + 5;
|
|
3259
|
+
const ax = PX + (nearX - PX) * Math.cos(rad), ay = PY + (nearX - PX) * Math.sin(rad);
|
|
3260
|
+
const bx = PX + (farX - PX) * Math.cos(rad), by = PY + (farX - PX) * Math.sin(rad);
|
|
3261
|
+
fulcrum = `<path d="M${PX - 2.2} 29 L${PX + 2.2} 29 L${PX} ${PY} Z" fill="${fill}"/>`;
|
|
3262
|
+
leverEl = `<line x1="${ax}" y1="${ay}" x2="${bx}" y2="${by}" stroke="${fill}" stroke-width="1.7" stroke-linecap="round"/>`;
|
|
3263
|
+
armEl = `<line x1="${shX}" y1="${shY}" x2="${ax}" y2="${ay}" stroke="${fill}" stroke-width="1.3" stroke-linecap="round"/>`;
|
|
3264
|
+
} else {
|
|
3265
|
+
armEl = `<line x1="${shX}" y1="${cy + 1 + bob}" x2="${cx + r + 1.8}" y2="${cy + 4.2 + bob}" stroke="${fill}" stroke-width="1.3" stroke-linecap="round"/>`;
|
|
3266
|
+
}
|
|
3269
3267
|
const glyph = sym
|
|
3270
|
-
? `<text x="${
|
|
3268
|
+
? `<text x="${cx}" y="${cy + 3.4 + bob}" font-family="ui-monospace, monospace" font-size="14" font-weight="800" fill="${symColor || fill}" stroke="${base}" stroke-width="2.4" paint-order="stroke" text-anchor="middle">${sym}</text>`
|
|
3271
3269
|
: "";
|
|
3272
3270
|
return (
|
|
3273
|
-
|
|
3274
|
-
// legs + feet (planted
|
|
3275
|
-
`<
|
|
3276
|
-
`<
|
|
3277
|
-
`<
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
`<
|
|
3282
|
-
`<
|
|
3283
|
-
`<
|
|
3284
|
-
`<circle cx="${
|
|
3285
|
-
`<path d="M${
|
|
3286
|
-
`<circle cx="${
|
|
3287
|
-
`<circle cx="${
|
|
3271
|
+
fulcrum + leverEl +
|
|
3272
|
+
// thin legs + feet (planted)
|
|
3273
|
+
`<line x1="${cx - 3}" y1="${cy + r - 1.6}" x2="${cx - 3.4}" y2="30.6" stroke="${fill}" stroke-width="1.3" stroke-linecap="round"/>` +
|
|
3274
|
+
`<line x1="${cx + 2.6}" y1="${cy + r - 1.6}" x2="${cx + 3}" y2="30.6" stroke="${fill}" stroke-width="1.3" stroke-linecap="round"/>` +
|
|
3275
|
+
`<line x1="${cx - 5}" y1="30.7" x2="${cx - 1.9}" y2="30.7" stroke="${fill}" stroke-width="1.3" stroke-linecap="round"/>` +
|
|
3276
|
+
`<line x1="${cx + 1.6}" y1="30.7" x2="${cx + 4.7}" y2="30.7" stroke="${fill}" stroke-width="1.3" stroke-linecap="round"/>` +
|
|
3277
|
+
armEl +
|
|
3278
|
+
// ball body + antennae + beak + eyes — bob together with the effort
|
|
3279
|
+
`<g transform="translate(0 ${bob})">` +
|
|
3280
|
+
`<line x1="${cx - 2.2}" y1="${eyeY - 2.4}" x2="${cx - 4.2}" y2="2.8" stroke="${fill}" stroke-width="0.9" stroke-linecap="round"/><circle cx="${cx - 4.2}" cy="2.8" r="1" fill="${fill}"/>` +
|
|
3281
|
+
`<line x1="${cx + 2.2}" y1="${eyeY - 2.4}" x2="${cx + 4.2}" y2="2.8" stroke="${fill}" stroke-width="0.9" stroke-linecap="round"/><circle cx="${cx + 4.2}" cy="2.8" r="1" fill="${fill}"/>` +
|
|
3282
|
+
`<circle cx="${cx}" cy="${cy}" r="${r}" fill="${fill}"/>` +
|
|
3283
|
+
`<path d="M${cx - 1.6} ${cy + 2} L${cx + 1.6} ${cy + 2} L${cx} ${cy + 5.4} Z" fill="${fill}"/>` +
|
|
3284
|
+
`<circle cx="${cx - eyeSep / 2}" cy="${eyeY}" r="${eyeR}" fill="#fff"/><circle cx="${cx + eyeSep / 2}" cy="${eyeY}" r="${eyeR}" fill="#fff"/>` +
|
|
3285
|
+
`<circle cx="${cx - eyeSep / 2 + 0.3}" cy="${eyeY + 0.3}" r="${pupR}" fill="${base}"/><circle cx="${cx + eyeSep / 2 + 0.3}" cy="${eyeY + 0.3}" r="${pupR}" fill="${base}"/>` +
|
|
3288
3286
|
`</g>` +
|
|
3289
3287
|
glyph
|
|
3290
3288
|
);
|
|
@@ -3306,14 +3304,16 @@
|
|
|
3306
3304
|
const accent = themeColor("--amber", "#f0a848");
|
|
3307
3305
|
return faviconWrap(shadokInner(accent, symbol ? { sym: symbol, symColor } : {}));
|
|
3308
3306
|
}
|
|
3309
|
-
/** The
|
|
3310
|
-
*
|
|
3311
|
-
|
|
3307
|
+
/** The pump pose for a phase p (0..1..0): the Shadok pushes the lever DOWN (near
|
|
3308
|
+
* end down = negative angle) and the ball dips with the effort — shared by the
|
|
3309
|
+
* favicon and the in-page indicator so both pump the same way. */
|
|
3310
|
+
function pumpPose(p) { return { lever: 13 - p * 28, bob: +(p * 1.5).toFixed(2) }; }
|
|
3311
|
+
/** The "working" favicon: the Shadok pumping its lever. `phase` rides a cycle. */
|
|
3312
|
+
const PUMP_FRAMES = 12;
|
|
3312
3313
|
function faviconPumpingSVG(phase) {
|
|
3313
3314
|
const accent = themeColor("--amber", "#f0a848");
|
|
3314
3315
|
const p = (Math.sin((phase / PUMP_FRAMES) * 2 * Math.PI - Math.PI / 2) + 1) / 2; // 0..1..0
|
|
3315
|
-
|
|
3316
|
-
return faviconWrap(shadokInner(accent, { bob, pump: true }));
|
|
3316
|
+
return faviconWrap(shadokInner(accent, pumpPose(p)));
|
|
3317
3317
|
}
|
|
3318
3318
|
/** What each channel is asking for, as the pure module wants it. */
|
|
3319
3319
|
function attentionInput() {
|
|
@@ -3377,15 +3377,27 @@
|
|
|
3377
3377
|
}
|
|
3378
3378
|
}
|
|
3379
3379
|
function refreshBadge() { paint(); }
|
|
3380
|
-
// The in-page "pumping"
|
|
3381
|
-
//
|
|
3382
|
-
//
|
|
3383
|
-
//
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3380
|
+
// The in-page "Shadok is pumping" indicator uses the SAME `shadokInner`, tinted
|
|
3381
|
+
// via `currentColor` so it follows the palette. It animates by RE-DRAWING each
|
|
3382
|
+
// frame (like the favicon), not CSS: the pump's arm must be re-linked to the
|
|
3383
|
+
// lever every frame. A single heartbeat re-draws only while the indicator is
|
|
3384
|
+
// visible (it shows only during a turn), so an idle cockpit pays almost nothing.
|
|
3385
|
+
const twShadok = document.querySelector("#thinking .tw-shadok");
|
|
3386
|
+
const thinkingEl = document.getElementById("thinking");
|
|
3387
|
+
function renderTw(p) {
|
|
3388
|
+
if (!twShadok) return;
|
|
3389
|
+
const { lever, bob } = pumpPose(p);
|
|
3390
|
+
twShadok.innerHTML =
|
|
3391
|
+
`<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">${shadokInner("currentColor", { lever, bob })}</svg>`;
|
|
3392
|
+
}
|
|
3393
|
+
renderTw(0); // populate immediately so it isn't blank on first show
|
|
3394
|
+
if (!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches)) {
|
|
3395
|
+
let twPhase = 0;
|
|
3396
|
+
setInterval(() => {
|
|
3397
|
+
if (!thinkingEl || !thinkingEl.classList.contains("visible")) return;
|
|
3398
|
+
twPhase = (twPhase + 1) % PUMP_FRAMES;
|
|
3399
|
+
renderTw((Math.sin((twPhase / PUMP_FRAMES) * 2 * Math.PI - Math.PI / 2) + 1) / 2);
|
|
3400
|
+
}, 95);
|
|
3389
3401
|
}
|
|
3390
3402
|
// Coming back must stop the blink at once, not at the next tick. Both signals
|
|
3391
3403
|
// matter: switching tabs fires visibilitychange, switching application only
|