pinokiod 3.180.0 → 3.182.0

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 (51) hide show
  1. package/kernel/favicon.js +91 -34
  2. package/kernel/peer.js +73 -0
  3. package/kernel/util.js +28 -4
  4. package/package.json +1 -1
  5. package/server/index.js +237 -35
  6. package/server/public/common.js +677 -240
  7. package/server/public/files-app/app.css +64 -0
  8. package/server/public/files-app/app.js +87 -0
  9. package/server/public/install.js +8 -1
  10. package/server/public/layout.js +124 -0
  11. package/server/public/nav.js +227 -64
  12. package/server/public/sound/beep.mp3 +0 -0
  13. package/server/public/sound/bell.mp3 +0 -0
  14. package/server/public/sound/bright-ring.mp3 +0 -0
  15. package/server/public/sound/clap.mp3 +0 -0
  16. package/server/public/sound/deep-ring.mp3 +0 -0
  17. package/server/public/sound/gasp.mp3 +0 -0
  18. package/server/public/sound/hehe.mp3 +0 -0
  19. package/server/public/sound/levelup.mp3 +0 -0
  20. package/server/public/sound/light-pop.mp3 +0 -0
  21. package/server/public/sound/light-ring.mp3 +0 -0
  22. package/server/public/sound/meow.mp3 +0 -0
  23. package/server/public/sound/piano.mp3 +0 -0
  24. package/server/public/sound/pop.mp3 +0 -0
  25. package/server/public/sound/uhoh.mp3 +0 -0
  26. package/server/public/sound/whistle.mp3 +0 -0
  27. package/server/public/style.css +195 -4
  28. package/server/public/tab-idle-notifier.js +700 -4
  29. package/server/public/terminal-settings.js +1131 -0
  30. package/server/public/urldropdown.css +28 -1
  31. package/server/socket.js +71 -4
  32. package/server/views/{terminals.ejs → agents.ejs} +108 -32
  33. package/server/views/app.ejs +321 -104
  34. package/server/views/bootstrap.ejs +8 -0
  35. package/server/views/connect.ejs +10 -1
  36. package/server/views/d.ejs +172 -18
  37. package/server/views/editor.ejs +8 -0
  38. package/server/views/file_browser.ejs +4 -0
  39. package/server/views/index.ejs +10 -1
  40. package/server/views/init/index.ejs +18 -3
  41. package/server/views/install.ejs +8 -0
  42. package/server/views/layout.ejs +2 -0
  43. package/server/views/net.ejs +10 -1
  44. package/server/views/network.ejs +10 -1
  45. package/server/views/pro.ejs +8 -0
  46. package/server/views/prototype/index.ejs +8 -0
  47. package/server/views/screenshots.ejs +10 -2
  48. package/server/views/settings.ejs +10 -2
  49. package/server/views/shell.ejs +8 -0
  50. package/server/views/terminal.ejs +8 -0
  51. package/server/views/tools.ejs +10 -2
@@ -415,9 +415,30 @@ body.dark .create-launcher-modal-tools-title {
415
415
  color: rgba(203, 213, 225, 0.7);
416
416
  }
417
417
  .create-launcher-modal-tools-options {
418
+ display: grid;
419
+ grid-template-columns: repeat(2, minmax(0, 1fr));
420
+ gap: 12px;
421
+ align-items: flex-start;
422
+ }
423
+ .create-launcher-modal-tools-group {
418
424
  display: flex;
425
+ flex-direction: column;
426
+ gap: 8px;
427
+ }
428
+ .create-launcher-modal-tools-group-title {
429
+ font-size: 11px;
430
+ font-weight: 600;
431
+ letter-spacing: 0.08em;
432
+ text-transform: uppercase;
433
+ color: rgba(71, 85, 105, 0.75);
434
+ }
435
+ body.dark .create-launcher-modal-tools-group-title {
436
+ color: rgba(203, 213, 225, 0.65);
437
+ }
438
+ .create-launcher-modal-tools-group-options {
439
+ display: flex;
440
+ flex-direction: column;
419
441
  gap: 10px;
420
- flex-wrap: wrap;
421
442
  }
422
443
  .create-launcher-modal-tool {
423
444
  position: relative;
@@ -462,6 +483,12 @@ body.dark .create-launcher-modal-tool.selected {
462
483
  background: rgba(127, 91, 243, 0.28);
463
484
  box-shadow: 0 14px 40px rgba(5, 9, 25, 0.55);
464
485
  }
486
+
487
+ @media (max-width: 720px) {
488
+ .create-launcher-modal-tools-options {
489
+ grid-template-columns: 1fr;
490
+ }
491
+ }
465
492
  .create-launcher-modal-tool input[type="radio"] {
466
493
  position: absolute;
467
494
  opacity: 0;
package/server/socket.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const querystring = require("querystring");
2
2
  const WebSocket = require('ws');
3
3
  const path = require('path')
4
+ const os = require('os')
4
5
  const Util = require("../kernel/util")
5
6
  const Environment = require("../kernel/environment")
6
7
  const NOTIFICATION_CHANNEL = 'kernel.notifications'
@@ -15,6 +16,20 @@ class Socket {
15
16
  this.server = parent.server
16
17
  // this.kernel = parent.kernel
17
18
  const wss = new WebSocket.Server({ server: this.parent.server })
19
+ this.localDeviceIds = new Set()
20
+ this.localAddresses = new Set()
21
+ try {
22
+ const ifaces = os.networkInterfaces() || {}
23
+ Object.values(ifaces).forEach((arr) => {
24
+ (arr || []).forEach((info) => {
25
+ if (info && info.address) {
26
+ this.localAddresses.add(info.address)
27
+ }
28
+ })
29
+ })
30
+ this.localAddresses.add('127.0.0.1')
31
+ this.localAddresses.add('::1')
32
+ } catch (_) {}
18
33
  this.subscriptions = new Map(); // Initialize a Map to store the WebSocket connections interested in each event
19
34
  this.notificationChannel = NOTIFICATION_CHANNEL
20
35
  this.notificationBridgeDispose = null
@@ -36,6 +51,12 @@ class Socket {
36
51
  this.subscriptions.delete(eventName);
37
52
  }
38
53
  });
54
+ // Cleanup device tracking
55
+ try {
56
+ if (ws._isLocalClient && ws._deviceId) {
57
+ this.localDeviceIds.delete(ws._deviceId)
58
+ }
59
+ } catch (_) {}
39
60
  this.checkNotificationBridge();
40
61
  });
41
62
  ws.on('message', async (message, isBinary) => {
@@ -171,6 +192,25 @@ class Socket {
171
192
  this.parent.kernel.api.process(req)
172
193
  }
173
194
  } else {
195
+ if (req.method === this.notificationChannel) {
196
+ if (typeof req.device_id === 'string' && req.device_id.trim()) {
197
+ ws._deviceId = req.device_id.trim()
198
+ }
199
+ // Mark local client sockets by IP matching any local address
200
+ try {
201
+ const ip = ws._ip || ''
202
+ const isLocal = (addr) => {
203
+ if (!addr || typeof addr !== 'string') return false
204
+ if (this.localAddresses.has(addr)) return true
205
+ const v = addr.trim().toLowerCase()
206
+ return v.startsWith('::ffff:127.') || v.startsWith('127.')
207
+ }
208
+ ws._isLocalClient = isLocal(ip)
209
+ if (ws._isLocalClient && ws._deviceId) {
210
+ this.localDeviceIds.add(ws._deviceId)
211
+ }
212
+ } catch (_) {}
213
+ }
174
214
  this.subscribe(ws, req.method)
175
215
  if (req.mode !== "listen") {
176
216
  this.parent.kernel.api.process(req)
@@ -350,11 +390,38 @@ class Socket {
350
390
  data: payload,
351
391
  }
352
392
  const frame = JSON.stringify(envelope)
353
- subscribers.forEach((subscriber) => {
354
- if (subscriber.readyState === WebSocket.OPEN) {
355
- subscriber.send(frame)
393
+ const targetId = (payload && typeof payload.device_id === 'string' && payload.device_id.trim()) ? payload.device_id.trim() : null
394
+ const audience = (payload && typeof payload.audience === 'string' && payload.audience.trim()) ? payload.audience.trim() : null
395
+ if (audience === 'device' && targetId) {
396
+ let delivered = false
397
+ subscribers.forEach((subscriber) => {
398
+ if (subscriber.readyState !== WebSocket.OPEN) {
399
+ return
400
+ }
401
+ if (subscriber._deviceId && subscriber._deviceId === targetId) {
402
+ try { subscriber.send(frame); delivered = true } catch (_) {}
403
+ }
404
+ })
405
+ if (!delivered) {
406
+ // Fallback: broadcast if no matching device subscriber is available
407
+ subscribers.forEach((subscriber) => {
408
+ if (subscriber.readyState === WebSocket.OPEN) {
409
+ try { subscriber.send(frame) } catch (_) {}
410
+ }
411
+ })
356
412
  }
357
- })
413
+ } else {
414
+ subscribers.forEach((subscriber) => {
415
+ if (subscriber.readyState === WebSocket.OPEN) {
416
+ try { subscriber.send(frame) } catch (_) {}
417
+ }
418
+ })
419
+ }
420
+ }
421
+
422
+ isLocalDevice(deviceId) {
423
+ if (!deviceId || typeof deviceId !== 'string') return false
424
+ return this.localDeviceIds.has(deviceId)
358
425
  }
359
426
 
360
427
  ensureNotificationBridge() {
@@ -1,6 +1,6 @@
1
1
  <html>
2
2
  <head>
3
- <title>Pinokio Terminals</title>
3
+ <title>Pinokio Agents</title>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
6
6
  <link href="/xterm.min.css" rel="stylesheet" />
@@ -53,7 +53,6 @@ body.plugin-page .btn-tab {
53
53
  display: flex;
54
54
  align-items: center;
55
55
  gap: 10px;
56
- margin-bottom: 15px;
57
56
  }
58
57
  body.plugin-page .btn-tab .btn {
59
58
  display: flex;
@@ -76,6 +75,32 @@ body.plugin-page .btn-tab .btn {
76
75
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
77
76
  gap: 14px;
78
77
  }
78
+ .plugin-category-layout {
79
+ display: grid;
80
+ grid-template-columns: repeat(2, minmax(0, 1fr));
81
+ gap: 24px;
82
+ }
83
+ .plugin-category {
84
+ display: flex;
85
+ flex-direction: column;
86
+ }
87
+ .plugin-category-header {
88
+ display: flex;
89
+ flex-direction: column;
90
+ gap: 4px;
91
+ margin-bottom: 12px;
92
+ }
93
+ .plugin-category-title {
94
+ font-size: 30px;
95
+ font-weight: 700;
96
+ margin: 0;
97
+ }
98
+ .plugin-category-subtitle {
99
+ font-size: 12px;
100
+ opacity: 0.6;
101
+ text-transform: uppercase;
102
+ letter-spacing: 0.08em;
103
+ }
79
104
  .plugin-card {
80
105
  display: flex;
81
106
  align-items: center;
@@ -195,6 +220,9 @@ body.dark .plugin-card:focus-visible .disclosure-indicator {
195
220
  background: rgba(0, 0, 0, 0.02);
196
221
  text-align: center;
197
222
  }
223
+ .plugin-empty.plugin-empty-category {
224
+ padding: 20px;
225
+ }
198
226
  body.dark .plugin-empty {
199
227
  border-color: rgba(255, 255, 255, 0.16);
200
228
  background: rgba(255, 255, 255, 0.04);
@@ -305,6 +333,11 @@ body.dark .plugin-option:hover {
305
333
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
306
334
  }
307
335
  }
336
+ @media (max-width: 900px) {
337
+ .plugin-category-layout {
338
+ grid-template-columns: 1fr;
339
+ }
340
+ }
308
341
  @media (max-width: 600px) {
309
342
  body.plugin-page main {
310
343
  flex-direction: column;
@@ -394,37 +427,71 @@ body.dark .plugin-option:hover {
394
427
  </header>
395
428
  <main class='plugin-main'>
396
429
  <div class='plugin-container'>
397
- <h1><i class="fa-solid fa-desktop"></i>Terminals</h1>
430
+ <h1><i class="fa-solid fa-robot"></i>Agents</h1>
431
+ <% const pluginCategories = pluginMenu.reduce((acc, pluginItem, index) => {
432
+ const runs = Array.isArray(pluginItem.run) ? pluginItem.run : []
433
+ const hasExec = runs.some((step) => step && step.method === 'exec')
434
+ const hasShellRun = runs.some((step) => step && step.method === 'shell.run')
435
+ if (hasExec) {
436
+ acc.ide.push({ pluginItem, index })
437
+ } else if (hasShellRun) {
438
+ acc.cli.push({ pluginItem, index })
439
+ } else {
440
+ acc.cli.push({ pluginItem, index })
441
+ }
442
+ return acc
443
+ }, { ide: [], cli: [] }) %>
398
444
  <% if (pluginMenu.length === 0) { %>
399
445
  <div class='plugin-empty'>
400
- <p>No terminals found. Clone or add terminal definitions under <code>~/pinokio/plugin</code> to see them listed here.</p>
401
- <a class='btn' href="/home?mode=download"><i class="fa-solid fa-download"></i> Download Terminals</a>
446
+ <p>No agents found. Clone or add agent definitions under <code>~/pinokio/plugin</code> to see them listed here.</p>
447
+ <a class='btn' href="/home?mode=download"><i class="fa-solid fa-download"></i> Download Agents</a>
402
448
  </div>
403
449
  <% } else { %>
404
- <div class='plugin-grid'>
405
- <% pluginMenu.forEach((pluginItem, index) => { %>
406
- <div class='plugin-card' role="button" tabindex="0" data-plugin-index="<%=index%>">
407
- <% if (pluginItem.image) { %>
408
- <img src="<%=pluginItem.image%>" alt="<%=pluginItem.title || pluginItem.text || 'Plugin'%> icon">
409
- <% } else if (pluginItem.icon) { %>
410
- <div class='plugin-icon'><i class="<%=pluginItem.icon%>"></i></div>
411
- <% } else { %>
412
- <div class='plugin-icon'><i class="fa-solid fa-desktop"></i></div>
413
- <% } %>
414
- <div class='plugin-details'>
415
- <h2><%=pluginItem.title || pluginItem.text || pluginItem.name || 'Plugin'%></h2>
416
- <% if (pluginItem.description) { %>
417
- <div class='subtitle'><%=pluginItem.description%></div>
418
- <% } %>
450
+ <div class='plugin-category-layout'>
451
+ <% [
452
+ { key: 'ide', title: 'IDE', subtitle: 'Launch externally' },
453
+ { key: 'cli', title: 'CLI', subtitle: 'Launch in Pinokio' }
454
+ ].forEach(({ key, title, subtitle }) => {
455
+ const items = pluginCategories[key] || []
456
+ %>
457
+ <section class='plugin-category' aria-label="<%=title%> agents">
458
+ <div class='plugin-category-header'>
459
+ <h2 class='plugin-category-title'><%=title%></h2>
460
+ <div class='plugin-category-subtitle'><%=subtitle%></div>
419
461
  </div>
420
- <div class='flexible'></div>
421
- <% if (pluginItem.link) { %>
422
- <button type='button' class='plugin-info' data-link="<%=pluginItem.link.replace(/"/g, '&quot;')%>" aria-label="Open info"><i class="fa-solid fa-circle-info"></i></button>
462
+ <% if (items.length) { %>
463
+ <div class='plugin-grid'>
464
+ <% items.forEach(({ pluginItem, index }) => { %>
465
+ <div class='plugin-card' role="button" tabindex="0" data-plugin-index="<%=index%>">
466
+ <% if (pluginItem.image) { %>
467
+ <img src="<%=pluginItem.image%>" alt="<%=pluginItem.title || pluginItem.text || 'Plugin'%> icon">
468
+ <% } else if (pluginItem.icon) { %>
469
+ <div class='plugin-icon'><i class="<%=pluginItem.icon%>"></i></div>
470
+ <% } else { %>
471
+ <div class='plugin-icon'><i class="fa-solid fa-robot"></i></div>
472
+ <% } %>
473
+ <div class='plugin-details'>
474
+ <h2><%=pluginItem.title || pluginItem.text || pluginItem.name || 'Plugin'%></h2>
475
+ <% if (pluginItem.description) { %>
476
+ <div class='subtitle'><%=pluginItem.description%></div>
477
+ <% } %>
478
+ </div>
479
+ <div class='flexible'></div>
480
+ <% if (pluginItem.link) { %>
481
+ <button type='button' class='plugin-info' data-link="<%=pluginItem.link.replace(/"/g, '&quot;')%>" aria-label="Open info"><i class="fa-solid fa-circle-info"></i></button>
482
+ <% } %>
483
+ <div class='disclosure-indicator' aria-hidden="true">
484
+ <i class="fa-solid fa-chevron-right"></i>
485
+ </div>
486
+ </div>
487
+ <% }) %>
488
+ </div>
489
+ <% } else { %>
490
+ <div class='plugin-empty plugin-empty-category'>
491
+ <p>No <%=title%> agents available.</p>
492
+ </div>
423
493
  <% } %>
424
- <div class='disclosure-indicator' aria-hidden="true">
425
- <i class="fa-solid fa-chevron-right"></i>
426
- </div>
427
- </div>
494
+ </section>
428
495
  <% }) %>
429
496
  </div>
430
497
  <% } %>
@@ -447,9 +514,18 @@ body.dark .plugin-option:hover {
447
514
  <a class='tab' id='genlog'><i class="fa-solid fa-laptop-code"></i><div class='caption'>Logs</div></a>
448
515
  <a id='downloadlogs' download class='hidden btn2' href="/pinokio/logs.zip"><i class="fa-solid fa-download"></i><div class='caption'>Download logs</div></a>
449
516
  <a class='tab' href="/screenshots"><i class="fa-solid fa-camera"></i><div class='caption'>Screenshots</div></a>
450
- <a class='tab' href="/tools"><i class="fa-solid fa-toolbox"></i><div class='caption'>Installed Tools</div></a>
451
- <a class='tab selected' href="/terminals"><i class="fa-solid fa-desktop"></i><div class='caption'>Terminals</div></a>
452
- <a class='tab' href="/home?mode=settings"><i class="fa-solid fa-gear"></i><div class='caption'>Settings</div></a>
517
+ <a class='tab' href="/tools"><i class="fa-solid fa-toolbox"></i><div class='caption'>Installed Tools</div></a>
518
+ <a class='tab selected' href="/agents"><i class="fa-solid fa-robot"></i><div class='caption'>Agents</div></a>
519
+ <a class='tab' href="/home?mode=settings"><i class="fa-solid fa-gear"></i><div class='caption'>Settings</div></a>
520
+ <% if (typeof peer_qr !== 'undefined' && peer_qr) { %>
521
+ <div class='qr' style='padding:12px 10px; text-align:center;'>
522
+ <a href="<%=peer_url%>" target="_blank" style="text-decoration:none; color:inherit; display:block;">
523
+ <img src="<%=peer_qr%>" alt="Open <%=peer_url%>" style="width:128px; height:128px; image-rendering: pixelated;"/>
524
+ <div class='caption'>Scan to open</div>
525
+ <div class='caption' style='font-size:10px; opacity:0.7;'><%=peer_url%></div>
526
+ </a>
527
+ </div>
528
+ <% } %>
453
529
  </aside>
454
530
  </main>
455
531
  <script type="application/json" id="plugin-data"><%- JSON.stringify(serializedPlugins) %></script>
@@ -495,7 +571,7 @@ body.dark .plugin-option:hover {
495
571
  <div class="url-modal-content" role="dialog" aria-modal="true" aria-labelledby="plugin-modal-title" aria-describedby="plugin-modal-description">
496
572
  <button type="button" class="url-modal-close" aria-label="Close">&times;</button>
497
573
  <h3 id="plugin-modal-title">Run Plugin</h3>
498
- <p class="url-modal-description" id="plugin-modal-description">Choose a project to open the terminal from.</p>
574
+ <p class="url-modal-description" id="plugin-modal-description">Choose a project to open the agent from.</p>
499
575
  <input type="search" class="url-modal-input" placeholder="Filter projects" autocomplete="off" />
500
576
  <div class="url-dropdown plugin-modal-dropdown"></div>
501
577
  <div class="url-modal-actions single">
@@ -730,7 +806,7 @@ body.dark .plugin-option:hover {
730
806
  if (state.apps.length === 0) {
731
807
  descriptionEl.textContent = 'No projects found under ~/pinokio/api. Create or download a project to continue.'
732
808
  } else {
733
- descriptionEl.textContent = 'Choose a project to open the terminal from.'
809
+ descriptionEl.textContent = 'Choose a project to open the agent from.'
734
810
  }
735
811
  renderList('')
736
812
  dropdownEl.style.display = 'block'