agenticros 0.5.3 → 0.5.5

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 (52) hide show
  1. package/README.md +5 -2
  2. package/dist/__tests__/eyes.test.d.ts +2 -0
  3. package/dist/__tests__/eyes.test.d.ts.map +1 -0
  4. package/dist/__tests__/eyes.test.js +30 -0
  5. package/dist/__tests__/eyes.test.js.map +1 -0
  6. package/dist/commands/doctor.d.ts.map +1 -1
  7. package/dist/commands/doctor.js +53 -0
  8. package/dist/commands/doctor.js.map +1 -1
  9. package/dist/commands/down.d.ts +3 -3
  10. package/dist/commands/down.js +4 -4
  11. package/dist/commands/down.js.map +1 -1
  12. package/dist/commands/eyes.d.ts +16 -0
  13. package/dist/commands/eyes.d.ts.map +1 -0
  14. package/dist/commands/eyes.js +113 -0
  15. package/dist/commands/eyes.js.map +1 -0
  16. package/dist/commands/logs.d.ts +1 -0
  17. package/dist/commands/logs.d.ts.map +1 -1
  18. package/dist/commands/logs.js +2 -1
  19. package/dist/commands/logs.js.map +1 -1
  20. package/dist/commands/status.js +1 -1
  21. package/dist/commands/status.js.map +1 -1
  22. package/dist/commands/up.d.ts +6 -0
  23. package/dist/commands/up.d.ts.map +1 -1
  24. package/dist/commands/up.js +8 -0
  25. package/dist/commands/up.js.map +1 -1
  26. package/dist/index.js +28 -3
  27. package/dist/index.js.map +1 -1
  28. package/dist/menu.d.ts.map +1 -1
  29. package/dist/menu.js +5 -0
  30. package/dist/menu.js.map +1 -1
  31. package/dist/util/eyes.d.ts +23 -0
  32. package/dist/util/eyes.d.ts.map +1 -0
  33. package/dist/util/eyes.js +53 -0
  34. package/dist/util/eyes.js.map +1 -0
  35. package/dist/util/pidfile.d.ts +1 -1
  36. package/dist/util/pidfile.d.ts.map +1 -1
  37. package/dist/util/pidfile.js.map +1 -1
  38. package/package.json +1 -1
  39. package/runtime/BUNDLE.json +1 -1
  40. package/runtime/README.md +4 -2
  41. package/runtime/docs/cli.md +15 -5
  42. package/runtime/docs/eyes.md +83 -0
  43. package/runtime/packages/robot-eyes/README.md +25 -0
  44. package/runtime/packages/robot-eyes/package.json +49 -0
  45. package/runtime/packages/robot-eyes/public/eyes.js +246 -0
  46. package/runtime/packages/robot-eyes/public/index.html +14 -0
  47. package/runtime/packages/robot-eyes/public/style.css +22 -0
  48. package/runtime/packages/robot-eyes/public/teleop.js +102 -0
  49. package/runtime/packages/robot-eyes/src/index.js +410 -0
  50. package/runtime/pnpm-lock.yaml +30 -1
  51. package/runtime/ros2_ws/src/agenticros_sim/package.xml +1 -1
  52. package/runtime/scripts/pack-runtime.mjs +4 -3
@@ -0,0 +1,246 @@
1
+ const canvas = document.getElementById('eyes');
2
+ const ctx = canvas.getContext('2d');
3
+
4
+ const state = {
5
+ // Target from ROS (+/-1); idle offsets applied on top when not driving
6
+ rosGazeX: 0,
7
+ driving: false,
8
+ // Smoothed pupil position in [-1, 1]
9
+ gazeX: 0,
10
+ gazeY: 0,
11
+ // Idle wander target
12
+ idleX: 0,
13
+ idleY: 0,
14
+ nextIdleAt: 0,
15
+ idleHoldUntil: 0,
16
+ // Blink: 0 open → 1 closed
17
+ blink: 0,
18
+ blinkPhase: 'open', // open | closing | closed | opening
19
+ nextBlinkAt: 0,
20
+ blinkHoldUntil: 0,
21
+ };
22
+
23
+ const BLINK_CLOSE_MS = 90;
24
+ const BLINK_OPEN_MS = 110;
25
+ const BLINK_HOLD_MS = 40;
26
+ const GAZE_LERP = 0.12;
27
+ const IDLE_LERP = 0.04;
28
+
29
+ function rand(min, max) {
30
+ return min + Math.random() * (max - min);
31
+ }
32
+
33
+ function scheduleBlink(now) {
34
+ state.nextBlinkAt = now + rand(2200, 5200);
35
+ }
36
+
37
+ function scheduleIdle(now) {
38
+ state.nextIdleAt = now + rand(2500, 6000);
39
+ }
40
+
41
+ scheduleBlink(performance.now());
42
+ scheduleIdle(performance.now());
43
+
44
+ function connectWs() {
45
+ const proto = location.protocol === 'https:' ? 'wss' : 'ws';
46
+ const ws = new WebSocket(`${proto}://${location.host}`);
47
+
48
+ ws.addEventListener('message', (ev) => {
49
+ try {
50
+ const msg = JSON.parse(ev.data);
51
+ if (msg.type !== 'gaze') return;
52
+ state.rosGazeX = Number(msg.gazeX) || 0;
53
+ state.driving = Boolean(msg.driving);
54
+ if (state.driving) {
55
+ state.idleX = 0;
56
+ state.idleY = 0;
57
+ }
58
+ } catch {
59
+ // ignore malformed
60
+ }
61
+ });
62
+
63
+ ws.addEventListener('close', () => {
64
+ setTimeout(connectWs, 1000);
65
+ });
66
+
67
+ ws.addEventListener('error', () => {
68
+ ws.close();
69
+ });
70
+ }
71
+
72
+ async function enterFullscreen() {
73
+ const el = document.documentElement;
74
+ if (document.fullscreenElement) return;
75
+ try {
76
+ if (el.requestFullscreen) await el.requestFullscreen();
77
+ } catch {
78
+ // Kiosk browsers often already cover the screen
79
+ }
80
+ }
81
+
82
+ document.addEventListener('click', () => {
83
+ enterFullscreen();
84
+ });
85
+
86
+ document.addEventListener('keydown', (e) => {
87
+ if (e.key === 'Escape') {
88
+ // Allow escape while developing; kiosk may ignore it
89
+ window.close();
90
+ }
91
+ if (e.key === 'f' || e.key === 'F') {
92
+ enterFullscreen();
93
+ }
94
+ });
95
+
96
+ function resize() {
97
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
98
+ canvas.width = Math.floor(window.innerWidth * dpr);
99
+ canvas.height = Math.floor(window.innerHeight * dpr);
100
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
101
+ }
102
+
103
+ window.addEventListener('resize', resize);
104
+ resize();
105
+
106
+ function updateBlink(now, dt) {
107
+ if (state.blinkPhase === 'open') {
108
+ if (now >= state.nextBlinkAt) {
109
+ state.blinkPhase = 'closing';
110
+ }
111
+ return;
112
+ }
113
+
114
+ if (state.blinkPhase === 'closing') {
115
+ state.blink = Math.min(1, state.blink + dt / BLINK_CLOSE_MS);
116
+ if (state.blink >= 1) {
117
+ state.blink = 1;
118
+ state.blinkPhase = 'closed';
119
+ state.blinkHoldUntil = now + BLINK_HOLD_MS;
120
+ }
121
+ return;
122
+ }
123
+
124
+ if (state.blinkPhase === 'closed') {
125
+ if (now >= state.blinkHoldUntil) {
126
+ state.blinkPhase = 'opening';
127
+ }
128
+ return;
129
+ }
130
+
131
+ if (state.blinkPhase === 'opening') {
132
+ state.blink = Math.max(0, state.blink - dt / BLINK_OPEN_MS);
133
+ if (state.blink <= 0) {
134
+ state.blink = 0;
135
+ state.blinkPhase = 'open';
136
+ scheduleBlink(now);
137
+ }
138
+ }
139
+ }
140
+
141
+ function updateIdle(now) {
142
+ if (state.driving) {
143
+ state.idleX = 0;
144
+ state.idleY = 0;
145
+ return;
146
+ }
147
+
148
+ if (now >= state.nextIdleAt && now >= state.idleHoldUntil) {
149
+ // Sometimes glance around, sometimes return to center
150
+ if (Math.random() < 0.35) {
151
+ state.idleX = 0;
152
+ state.idleY = 0;
153
+ } else {
154
+ state.idleX = rand(-0.45, 0.45);
155
+ state.idleY = rand(-0.25, 0.25);
156
+ }
157
+ state.idleHoldUntil = now + rand(600, 1800);
158
+ scheduleIdle(now);
159
+ }
160
+ }
161
+
162
+ function updateGaze() {
163
+ const targetX = state.driving ? state.rosGazeX : state.idleX;
164
+ const targetY = state.driving ? 0 : state.idleY;
165
+ const lerp = state.driving ? GAZE_LERP : IDLE_LERP;
166
+ state.gazeX += (targetX - state.gazeX) * lerp;
167
+ state.gazeY += (targetY - state.gazeY) * lerp;
168
+ }
169
+
170
+ function drawEye(cx, cy, eyeW, eyeH, gazeX, gazeY, blink) {
171
+ const rx = eyeW / 2;
172
+ const ry = eyeH / 2;
173
+
174
+ // sclera
175
+ ctx.beginPath();
176
+ ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
177
+ ctx.fillStyle = '#f5f5f5';
178
+ ctx.fill();
179
+
180
+ // iris + pupil
181
+ const maxOffsetX = rx * 0.32;
182
+ const maxOffsetY = ry * 0.28;
183
+ const px = cx + gazeX * maxOffsetX;
184
+ const py = cy + gazeY * maxOffsetY;
185
+ const irisR = Math.min(rx, ry) * 0.42;
186
+ const pupilR = irisR * 0.55;
187
+
188
+ ctx.beginPath();
189
+ ctx.arc(px, py, irisR, 0, Math.PI * 2);
190
+ ctx.fillStyle = '#1a1a1a';
191
+ ctx.fill();
192
+
193
+ ctx.beginPath();
194
+ ctx.arc(px, py, pupilR, 0, Math.PI * 2);
195
+ ctx.fillStyle = '#000';
196
+ ctx.fill();
197
+
198
+ // highlight
199
+ ctx.beginPath();
200
+ ctx.arc(px - irisR * 0.35, py - irisR * 0.35, pupilR * 0.35, 0, Math.PI * 2);
201
+ ctx.fillStyle = 'rgba(255,255,255,0.85)';
202
+ ctx.fill();
203
+
204
+ // eyelids (cover from top and bottom)
205
+ if (blink > 0) {
206
+ const cover = ry * blink + 2;
207
+ ctx.fillStyle = '#000';
208
+ // clip to eye bounds roughly with rects over the ellipse region
209
+ ctx.fillRect(cx - rx - 2, cy - ry - 2, eyeW + 4, cover + 2);
210
+ ctx.fillRect(cx - rx - 2, cy + ry - cover, eyeW + 4, cover + 2);
211
+ }
212
+ }
213
+
214
+ function draw() {
215
+ const w = window.innerWidth;
216
+ const h = window.innerHeight;
217
+
218
+ ctx.fillStyle = '#000';
219
+ ctx.fillRect(0, 0, w, h);
220
+
221
+ const eyeW = Math.min(w * 0.28, h * 0.55);
222
+ const eyeH = eyeW * 0.62;
223
+ const gap = w * 0.14;
224
+ const cy = h * 0.5;
225
+ const leftCx = w * 0.5 - gap / 2 - eyeW / 2;
226
+ const rightCx = w * 0.5 + gap / 2 + eyeW / 2;
227
+
228
+ drawEye(leftCx, cy, eyeW, eyeH, state.gazeX, state.gazeY, state.blink);
229
+ drawEye(rightCx, cy, eyeW, eyeH, state.gazeX, state.gazeY, state.blink);
230
+ }
231
+
232
+ let last = performance.now();
233
+
234
+ function frame(now) {
235
+ const dt = Math.min(50, now - last);
236
+ last = now;
237
+ updateBlink(now, dt);
238
+ updateIdle(now);
239
+ updateGaze();
240
+ draw();
241
+ requestAnimationFrame(frame);
242
+ }
243
+
244
+ connectWs();
245
+ enterFullscreen();
246
+ requestAnimationFrame(frame);
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
6
+ <title>Robot Eyes</title>
7
+ <link rel="stylesheet" href="/style.css" />
8
+ </head>
9
+ <body>
10
+ <canvas id="eyes"></canvas>
11
+ <script type="module" src="/eyes.js"></script>
12
+ <script type="module" src="/teleop.js"></script>
13
+ </body>
14
+ </html>
@@ -0,0 +1,22 @@
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ html,
8
+ body {
9
+ width: 100%;
10
+ height: 100%;
11
+ overflow: hidden;
12
+ background: #000;
13
+ cursor: none;
14
+ touch-action: none;
15
+ }
16
+
17
+ canvas#eyes {
18
+ display: block;
19
+ width: 100vw;
20
+ height: 100vh;
21
+ background: #000;
22
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Invisible keyboard teleop — no on-screen UI.
3
+ * WASD drive, Q faster, Z slower. Messages go to the server over WebSocket.
4
+ */
5
+
6
+ const MOVE_KEYS = new Set(['w', 'a', 's', 'd']);
7
+
8
+ /** @type {WebSocket | null} */
9
+ let socket = null;
10
+ let reconnectTimer = 0;
11
+
12
+ const pressed = {
13
+ w: false,
14
+ a: false,
15
+ s: false,
16
+ d: false,
17
+ };
18
+
19
+ function send(payload) {
20
+ if (socket && socket.readyState === WebSocket.OPEN) {
21
+ socket.send(JSON.stringify(payload));
22
+ }
23
+ }
24
+
25
+ function sendKeys() {
26
+ send({
27
+ type: 'keys',
28
+ keys: { ...pressed },
29
+ });
30
+ }
31
+
32
+ function connect() {
33
+ const proto = location.protocol === 'https:' ? 'wss' : 'ws';
34
+ const ws = new WebSocket(`${proto}://${location.host}`);
35
+ socket = ws;
36
+
37
+ ws.addEventListener('open', () => {
38
+ sendKeys();
39
+ });
40
+
41
+ ws.addEventListener('close', () => {
42
+ socket = null;
43
+ clearTimeout(reconnectTimer);
44
+ reconnectTimer = setTimeout(connect, 1000);
45
+ });
46
+
47
+ ws.addEventListener('error', () => {
48
+ ws.close();
49
+ });
50
+ }
51
+
52
+ function normalizeKey(e) {
53
+ return e.key.length === 1 ? e.key.toLowerCase() : e.key.toLowerCase();
54
+ }
55
+
56
+ window.addEventListener('keydown', (e) => {
57
+ const key = normalizeKey(e);
58
+
59
+ if (MOVE_KEYS.has(key)) {
60
+ e.preventDefault();
61
+ if (!pressed[key]) {
62
+ pressed[key] = true;
63
+ sendKeys();
64
+ }
65
+ return;
66
+ }
67
+
68
+ if (key === 'q' && !e.repeat) {
69
+ e.preventDefault();
70
+ send({ type: 'speed', delta: 1 });
71
+ return;
72
+ }
73
+
74
+ if (key === 'z' && !e.repeat) {
75
+ e.preventDefault();
76
+ send({ type: 'speed', delta: -1 });
77
+ }
78
+ });
79
+
80
+ window.addEventListener('keyup', (e) => {
81
+ const key = normalizeKey(e);
82
+ if (!MOVE_KEYS.has(key)) return;
83
+ e.preventDefault();
84
+ if (pressed[key]) {
85
+ pressed[key] = false;
86
+ sendKeys();
87
+ }
88
+ });
89
+
90
+ // Release all keys if the window loses focus (safety)
91
+ window.addEventListener('blur', () => {
92
+ let changed = false;
93
+ for (const k of MOVE_KEYS) {
94
+ if (pressed[k]) {
95
+ pressed[k] = false;
96
+ changed = true;
97
+ }
98
+ }
99
+ if (changed) sendKeys();
100
+ });
101
+
102
+ connect();