loopsy 1.0.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 (262) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +425 -0
  3. package/dist/cli/commands/connect.d.ts +2 -0
  4. package/dist/cli/commands/connect.d.ts.map +1 -0
  5. package/dist/cli/commands/connect.js +120 -0
  6. package/dist/cli/commands/connect.js.map +1 -0
  7. package/dist/cli/commands/context.d.ts +2 -0
  8. package/dist/cli/commands/context.d.ts.map +1 -0
  9. package/dist/cli/commands/context.js +39 -0
  10. package/dist/cli/commands/context.js.map +1 -0
  11. package/dist/cli/commands/daemon.d.ts +4 -0
  12. package/dist/cli/commands/daemon.d.ts.map +1 -0
  13. package/dist/cli/commands/daemon.js +55 -0
  14. package/dist/cli/commands/daemon.js.map +1 -0
  15. package/dist/cli/commands/dashboard.d.ts +2 -0
  16. package/dist/cli/commands/dashboard.d.ts.map +1 -0
  17. package/dist/cli/commands/dashboard.js +24 -0
  18. package/dist/cli/commands/dashboard.js.map +1 -0
  19. package/dist/cli/commands/doctor.d.ts +2 -0
  20. package/dist/cli/commands/doctor.d.ts.map +1 -0
  21. package/dist/cli/commands/doctor.js +130 -0
  22. package/dist/cli/commands/doctor.js.map +1 -0
  23. package/dist/cli/commands/exec.d.ts +2 -0
  24. package/dist/cli/commands/exec.d.ts.map +1 -0
  25. package/dist/cli/commands/exec.js +34 -0
  26. package/dist/cli/commands/exec.js.map +1 -0
  27. package/dist/cli/commands/init.d.ts +2 -0
  28. package/dist/cli/commands/init.d.ts.map +1 -0
  29. package/dist/cli/commands/init.js +71 -0
  30. package/dist/cli/commands/init.js.map +1 -0
  31. package/dist/cli/commands/key.d.ts +2 -0
  32. package/dist/cli/commands/key.d.ts.map +1 -0
  33. package/dist/cli/commands/key.js +39 -0
  34. package/dist/cli/commands/key.js.map +1 -0
  35. package/dist/cli/commands/logs.d.ts +2 -0
  36. package/dist/cli/commands/logs.d.ts.map +1 -0
  37. package/dist/cli/commands/logs.js +26 -0
  38. package/dist/cli/commands/logs.js.map +1 -0
  39. package/dist/cli/commands/mcp.d.ts +4 -0
  40. package/dist/cli/commands/mcp.d.ts.map +1 -0
  41. package/dist/cli/commands/mcp.js +70 -0
  42. package/dist/cli/commands/mcp.js.map +1 -0
  43. package/dist/cli/commands/pair.d.ts +6 -0
  44. package/dist/cli/commands/pair.d.ts.map +1 -0
  45. package/dist/cli/commands/pair.js +208 -0
  46. package/dist/cli/commands/pair.js.map +1 -0
  47. package/dist/cli/commands/peers.d.ts +2 -0
  48. package/dist/cli/commands/peers.d.ts.map +1 -0
  49. package/dist/cli/commands/peers.js +29 -0
  50. package/dist/cli/commands/peers.js.map +1 -0
  51. package/dist/cli/commands/service/linux.d.ts +7 -0
  52. package/dist/cli/commands/service/linux.d.ts.map +1 -0
  53. package/dist/cli/commands/service/linux.js +86 -0
  54. package/dist/cli/commands/service/linux.js.map +1 -0
  55. package/dist/cli/commands/service/macos.d.ts +7 -0
  56. package/dist/cli/commands/service/macos.d.ts.map +1 -0
  57. package/dist/cli/commands/service/macos.js +83 -0
  58. package/dist/cli/commands/service/macos.js.map +1 -0
  59. package/dist/cli/commands/service/windows.d.ts +7 -0
  60. package/dist/cli/commands/service/windows.d.ts.map +1 -0
  61. package/dist/cli/commands/service/windows.js +52 -0
  62. package/dist/cli/commands/service/windows.js.map +1 -0
  63. package/dist/cli/commands/service.d.ts +4 -0
  64. package/dist/cli/commands/service.d.ts.map +1 -0
  65. package/dist/cli/commands/service.js +68 -0
  66. package/dist/cli/commands/service.js.map +1 -0
  67. package/dist/cli/commands/session.d.ts +8 -0
  68. package/dist/cli/commands/session.d.ts.map +1 -0
  69. package/dist/cli/commands/session.js +270 -0
  70. package/dist/cli/commands/session.js.map +1 -0
  71. package/dist/cli/commands/transfer.d.ts +3 -0
  72. package/dist/cli/commands/transfer.d.ts.map +1 -0
  73. package/dist/cli/commands/transfer.js +57 -0
  74. package/dist/cli/commands/transfer.js.map +1 -0
  75. package/dist/cli/index.d.ts +3 -0
  76. package/dist/cli/index.d.ts.map +1 -0
  77. package/dist/cli/index.js +89 -0
  78. package/dist/cli/index.js.map +1 -0
  79. package/dist/cli/package-root.d.ts +27 -0
  80. package/dist/cli/package-root.d.ts.map +1 -0
  81. package/dist/cli/package-root.js +54 -0
  82. package/dist/cli/package-root.js.map +1 -0
  83. package/dist/cli/utils.d.ts +11 -0
  84. package/dist/cli/utils.d.ts.map +1 -0
  85. package/dist/cli/utils.js +48 -0
  86. package/dist/cli/utils.js.map +1 -0
  87. package/dist/daemon/config.d.ts +4 -0
  88. package/dist/daemon/config.d.ts.map +1 -0
  89. package/dist/daemon/config.js +58 -0
  90. package/dist/daemon/config.js.map +1 -0
  91. package/dist/daemon/hooks/permission-hook.mjs +108 -0
  92. package/dist/daemon/index.d.ts +3 -0
  93. package/dist/daemon/index.d.ts.map +1 -0
  94. package/dist/daemon/index.js +3 -0
  95. package/dist/daemon/index.js.map +1 -0
  96. package/dist/daemon/main.d.ts +3 -0
  97. package/dist/daemon/main.d.ts.map +1 -0
  98. package/dist/daemon/main.js +28 -0
  99. package/dist/daemon/main.js.map +1 -0
  100. package/dist/daemon/middleware/auth.d.ts +3 -0
  101. package/dist/daemon/middleware/auth.d.ts.map +1 -0
  102. package/dist/daemon/middleware/auth.js +22 -0
  103. package/dist/daemon/middleware/auth.js.map +1 -0
  104. package/dist/daemon/routes/ai-tasks.d.ts +4 -0
  105. package/dist/daemon/routes/ai-tasks.d.ts.map +1 -0
  106. package/dist/daemon/routes/ai-tasks.js +146 -0
  107. package/dist/daemon/routes/ai-tasks.js.map +1 -0
  108. package/dist/daemon/routes/context.d.ts +4 -0
  109. package/dist/daemon/routes/context.d.ts.map +1 -0
  110. package/dist/daemon/routes/context.js +49 -0
  111. package/dist/daemon/routes/context.js.map +1 -0
  112. package/dist/daemon/routes/execute.d.ts +4 -0
  113. package/dist/daemon/routes/execute.d.ts.map +1 -0
  114. package/dist/daemon/routes/execute.js +74 -0
  115. package/dist/daemon/routes/execute.js.map +1 -0
  116. package/dist/daemon/routes/health.d.ts +15 -0
  117. package/dist/daemon/routes/health.d.ts.map +1 -0
  118. package/dist/daemon/routes/health.js +38 -0
  119. package/dist/daemon/routes/health.js.map +1 -0
  120. package/dist/daemon/routes/pair.d.ts +11 -0
  121. package/dist/daemon/routes/pair.d.ts.map +1 -0
  122. package/dist/daemon/routes/pair.js +149 -0
  123. package/dist/daemon/routes/pair.js.map +1 -0
  124. package/dist/daemon/routes/peers.d.ts +5 -0
  125. package/dist/daemon/routes/peers.d.ts.map +1 -0
  126. package/dist/daemon/routes/peers.js +69 -0
  127. package/dist/daemon/routes/peers.js.map +1 -0
  128. package/dist/daemon/routes/transfer.d.ts +4 -0
  129. package/dist/daemon/routes/transfer.d.ts.map +1 -0
  130. package/dist/daemon/routes/transfer.js +135 -0
  131. package/dist/daemon/routes/transfer.js.map +1 -0
  132. package/dist/daemon/server.d.ts +8 -0
  133. package/dist/daemon/server.d.ts.map +1 -0
  134. package/dist/daemon/server.js +170 -0
  135. package/dist/daemon/server.js.map +1 -0
  136. package/dist/daemon/services/ai-task-manager.d.ts +56 -0
  137. package/dist/daemon/services/ai-task-manager.d.ts.map +1 -0
  138. package/dist/daemon/services/ai-task-manager.js +491 -0
  139. package/dist/daemon/services/ai-task-manager.js.map +1 -0
  140. package/dist/daemon/services/audit-logger.d.ts +16 -0
  141. package/dist/daemon/services/audit-logger.d.ts.map +1 -0
  142. package/dist/daemon/services/audit-logger.js +23 -0
  143. package/dist/daemon/services/audit-logger.js.map +1 -0
  144. package/dist/daemon/services/context-store.d.ts +17 -0
  145. package/dist/daemon/services/context-store.d.ts.map +1 -0
  146. package/dist/daemon/services/context-store.js +97 -0
  147. package/dist/daemon/services/context-store.js.map +1 -0
  148. package/dist/daemon/services/job-manager.d.ts +19 -0
  149. package/dist/daemon/services/job-manager.d.ts.map +1 -0
  150. package/dist/daemon/services/job-manager.js +92 -0
  151. package/dist/daemon/services/job-manager.js.map +1 -0
  152. package/dist/daemon/services/tls-manager.d.ts +33 -0
  153. package/dist/daemon/services/tls-manager.d.ts.map +1 -0
  154. package/dist/daemon/services/tls-manager.js +114 -0
  155. package/dist/daemon/services/tls-manager.js.map +1 -0
  156. package/dist/daemon/utils/which.d.ts +2 -0
  157. package/dist/daemon/utils/which.d.ts.map +1 -0
  158. package/dist/daemon/utils/which.js +18 -0
  159. package/dist/daemon/utils/which.js.map +1 -0
  160. package/dist/dashboard/config.d.ts +8 -0
  161. package/dist/dashboard/config.d.ts.map +1 -0
  162. package/dist/dashboard/config.js +22 -0
  163. package/dist/dashboard/config.js.map +1 -0
  164. package/dist/dashboard/public/app.js +120 -0
  165. package/dist/dashboard/public/icon-192.png +0 -0
  166. package/dist/dashboard/public/icon-512.png +0 -0
  167. package/dist/dashboard/public/index.html +85 -0
  168. package/dist/dashboard/public/manifest.json +12 -0
  169. package/dist/dashboard/public/style.css +784 -0
  170. package/dist/dashboard/public/sw.js +31 -0
  171. package/dist/dashboard/public/views/ai-tasks.js +679 -0
  172. package/dist/dashboard/public/views/context.js +167 -0
  173. package/dist/dashboard/public/views/messages.js +263 -0
  174. package/dist/dashboard/public/views/overview.js +228 -0
  175. package/dist/dashboard/public/views/peers.js +136 -0
  176. package/dist/dashboard/public/views/terminal.js +153 -0
  177. package/dist/dashboard/routes/ai-tasks.d.ts +3 -0
  178. package/dist/dashboard/routes/ai-tasks.d.ts.map +1 -0
  179. package/dist/dashboard/routes/ai-tasks.js +193 -0
  180. package/dist/dashboard/routes/ai-tasks.js.map +1 -0
  181. package/dist/dashboard/routes/messages.d.ts +3 -0
  182. package/dist/dashboard/routes/messages.d.ts.map +1 -0
  183. package/dist/dashboard/routes/messages.js +137 -0
  184. package/dist/dashboard/routes/messages.js.map +1 -0
  185. package/dist/dashboard/routes/peer-utils.d.ts +17 -0
  186. package/dist/dashboard/routes/peer-utils.d.ts.map +1 -0
  187. package/dist/dashboard/routes/peer-utils.js +193 -0
  188. package/dist/dashboard/routes/peer-utils.js.map +1 -0
  189. package/dist/dashboard/routes/peers-all.d.ts +3 -0
  190. package/dist/dashboard/routes/peers-all.d.ts.map +1 -0
  191. package/dist/dashboard/routes/peers-all.js +8 -0
  192. package/dist/dashboard/routes/peers-all.js.map +1 -0
  193. package/dist/dashboard/routes/proxy.d.ts +3 -0
  194. package/dist/dashboard/routes/proxy.d.ts.map +1 -0
  195. package/dist/dashboard/routes/proxy.js +59 -0
  196. package/dist/dashboard/routes/proxy.js.map +1 -0
  197. package/dist/dashboard/routes/sessions.d.ts +3 -0
  198. package/dist/dashboard/routes/sessions.d.ts.map +1 -0
  199. package/dist/dashboard/routes/sessions.js +64 -0
  200. package/dist/dashboard/routes/sessions.js.map +1 -0
  201. package/dist/dashboard/routes/sse.d.ts +3 -0
  202. package/dist/dashboard/routes/sse.d.ts.map +1 -0
  203. package/dist/dashboard/routes/sse.js +49 -0
  204. package/dist/dashboard/routes/sse.js.map +1 -0
  205. package/dist/dashboard/routes/status.d.ts +3 -0
  206. package/dist/dashboard/routes/status.d.ts.map +1 -0
  207. package/dist/dashboard/routes/status.js +38 -0
  208. package/dist/dashboard/routes/status.js.map +1 -0
  209. package/dist/dashboard/server.d.ts +3 -0
  210. package/dist/dashboard/server.d.ts.map +1 -0
  211. package/dist/dashboard/server.js +77 -0
  212. package/dist/dashboard/server.js.map +1 -0
  213. package/dist/dashboard/session-manager.d.ts +17 -0
  214. package/dist/dashboard/session-manager.d.ts.map +1 -0
  215. package/dist/dashboard/session-manager.js +225 -0
  216. package/dist/dashboard/session-manager.js.map +1 -0
  217. package/dist/discovery/health-checker.d.ts +15 -0
  218. package/dist/discovery/health-checker.d.ts.map +1 -0
  219. package/dist/discovery/health-checker.js +47 -0
  220. package/dist/discovery/health-checker.js.map +1 -0
  221. package/dist/discovery/index.d.ts +4 -0
  222. package/dist/discovery/index.d.ts.map +1 -0
  223. package/dist/discovery/index.js +4 -0
  224. package/dist/discovery/index.js.map +1 -0
  225. package/dist/discovery/mdns.d.ts +21 -0
  226. package/dist/discovery/mdns.d.ts.map +1 -0
  227. package/dist/discovery/mdns.js +83 -0
  228. package/dist/discovery/mdns.js.map +1 -0
  229. package/dist/discovery/peer-registry.d.ts +18 -0
  230. package/dist/discovery/peer-registry.d.ts.map +1 -0
  231. package/dist/discovery/peer-registry.js +81 -0
  232. package/dist/discovery/peer-registry.js.map +1 -0
  233. package/dist/mcp-server/daemon-client.d.ts +69 -0
  234. package/dist/mcp-server/daemon-client.d.ts.map +1 -0
  235. package/dist/mcp-server/daemon-client.js +281 -0
  236. package/dist/mcp-server/daemon-client.js.map +1 -0
  237. package/dist/mcp-server/index.d.ts +3 -0
  238. package/dist/mcp-server/index.d.ts.map +1 -0
  239. package/dist/mcp-server/index.js +406 -0
  240. package/dist/mcp-server/index.js.map +1 -0
  241. package/dist/protocol/constants.d.ts +66 -0
  242. package/dist/protocol/constants.d.ts.map +1 -0
  243. package/dist/protocol/constants.js +66 -0
  244. package/dist/protocol/constants.js.map +1 -0
  245. package/dist/protocol/errors.d.ts +47 -0
  246. package/dist/protocol/errors.d.ts.map +1 -0
  247. package/dist/protocol/errors.js +62 -0
  248. package/dist/protocol/errors.js.map +1 -0
  249. package/dist/protocol/index.d.ts +5 -0
  250. package/dist/protocol/index.d.ts.map +1 -0
  251. package/dist/protocol/index.js +5 -0
  252. package/dist/protocol/index.js.map +1 -0
  253. package/dist/protocol/schemas.d.ts +209 -0
  254. package/dist/protocol/schemas.d.ts.map +1 -0
  255. package/dist/protocol/schemas.js +115 -0
  256. package/dist/protocol/schemas.js.map +1 -0
  257. package/dist/protocol/types.d.ts +302 -0
  258. package/dist/protocol/types.d.ts.map +1 -0
  259. package/dist/protocol/types.js +2 -0
  260. package/dist/protocol/types.js.map +1 -0
  261. package/package.json +50 -0
  262. package/scripts/postinstall.mjs +42 -0
@@ -0,0 +1,136 @@
1
+ import { registerView, api, dashboardApi, escapeHtml, formatTime } from '/app.js';
2
+
3
+ function platformSvg(platform) {
4
+ const s = (d) => `<svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="color:var(--text-secondary)">${d}</svg>`;
5
+ switch (platform) {
6
+ case 'darwin': return s('<path d="M12.5 3C11 3 10 4 9 4S7 3 5.5 3C3.5 3 2 5 2 7.5c0 4 4.5 8 7 8.5 2.5-.5 7-4.5 7-8.5C16 5 14.5 3 12.5 3z"/>');
7
+ case 'win32': return s('<rect x="2" y="2" width="6" height="6" rx="0.5"/><rect x="10" y="2" width="6" height="6" rx="0.5"/><rect x="2" y="10" width="6" height="6" rx="0.5"/><rect x="10" y="10" width="6" height="6" rx="0.5"/>');
8
+ case 'linux': return s('<circle cx="9" cy="5" r="3"/><path d="M4 16c0-3 2.5-5 5-5s5 2 5 5"/>');
9
+ default: return s('<circle cx="9" cy="9" r="6"/><path d="M8.5 12h1"/><path d="M9 6a2 2 0 011.5 3.5L9 11"/>');
10
+ }
11
+ }
12
+
13
+ let refreshTimer = null;
14
+
15
+ function mount(container) {
16
+ container.innerHTML = `
17
+ <div class="section-header">Peer Network</div>
18
+
19
+ <div class="form-row mb-1">
20
+ <div class="form-group">
21
+ <select class="input" id="peers-session"></select>
22
+ </div>
23
+ <button class="btn btn-primary btn-sm" id="btn-peers-refresh">Refresh</button>
24
+ </div>
25
+
26
+ <div class="peer-grid" id="peer-grid"></div>
27
+
28
+ <div class="mt-1" style="border-top:1px solid var(--border-dim);padding-top:0.75rem">
29
+ <div class="form-label mb-1">Add Peer</div>
30
+ <div class="form-row">
31
+ <div class="form-group"><input class="input" id="peer-address" placeholder="IP address"></div>
32
+ <div class="form-group"><input class="input" id="peer-port" type="number" placeholder="Port" value="19532" style="width:100px"></div>
33
+ <button class="btn btn-success" id="btn-add-peer">Add</button>
34
+ </div>
35
+ </div>
36
+ `;
37
+
38
+ loadSessions();
39
+ document.getElementById('btn-peers-refresh').addEventListener('click', loadPeers);
40
+ document.getElementById('btn-add-peer').addEventListener('click', addPeer);
41
+ document.getElementById('peers-session').addEventListener('change', loadPeers);
42
+
43
+ refreshTimer = setInterval(loadPeers, 15000);
44
+ }
45
+
46
+ function unmount() {
47
+ if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
48
+ }
49
+
50
+ async function loadSessions() {
51
+ try {
52
+ const { main, sessions } = await dashboardApi('/sessions');
53
+ const sel = document.getElementById('peers-session');
54
+ const all = [];
55
+ if (main && main.status === 'running') all.push(main);
56
+ all.push(...sessions.filter(s => s.status === 'running'));
57
+ sel.innerHTML =
58
+ '<option value="all">All Sessions</option>' +
59
+ all.map(s => `<option value="${s.port}">${escapeHtml(s.hostname)} :${s.port}</option>`).join('');
60
+ loadPeers();
61
+ } catch {}
62
+ }
63
+
64
+ async function loadPeers() {
65
+ const portVal = document.getElementById('peers-session').value;
66
+ const grid = document.getElementById('peer-grid');
67
+
68
+ try {
69
+ let peers;
70
+ if (portVal === 'all') {
71
+ const data = await dashboardApi('/peers/all');
72
+ peers = data.peers || [];
73
+ } else {
74
+ if (!portVal) return;
75
+ const data = await api(portVal, '/peers');
76
+ peers = data.peers || [];
77
+ }
78
+
79
+ if (peers.length === 0) {
80
+ grid.innerHTML = '<div class="empty">No peers discovered</div>';
81
+ return;
82
+ }
83
+
84
+ grid.innerHTML = peers.map(p => {
85
+ const dotClass = p.status === 'online' ? 'online' : p.status === 'offline' ? 'offline' : 'unknown';
86
+ const platformIcon = platformSvg(p.platform);
87
+
88
+ return `
89
+ <div class="peer-card">
90
+ <div class="flex items-center justify-between mb-1">
91
+ <div class="flex items-center gap-sm">
92
+ <span class="status-dot ${dotClass}"></span>
93
+ <span class="font-mono text-sm" style="font-weight:600">${escapeHtml(p.hostname)}</span>
94
+ </div>
95
+ ${platformIcon}
96
+ </div>
97
+ <div class="font-mono text-xs text-muted" style="line-height:1.7">
98
+ ${escapeHtml(p.address)}:${p.port}<br>
99
+ version ${escapeHtml(p.version || '?')}<br>
100
+ last seen ${p.lastSeen ? formatTime(p.lastSeen) : 'never'}<br>
101
+ ${p.manuallyAdded ? '<span class="badge badge-amber">manual</span>' : ''}
102
+ ${p.capabilities?.length ? p.capabilities.map(c => `<span class="badge badge-cyan">${escapeHtml(c)}</span>`).join(' ') : ''}
103
+ </div>
104
+ </div>
105
+ `;
106
+ }).join('');
107
+ } catch (err) {
108
+ grid.innerHTML = `<div class="empty">${escapeHtml(err.message)}</div>`;
109
+ }
110
+ }
111
+
112
+ async function addPeer() {
113
+ const portVal = document.getElementById('peers-session').value;
114
+ if (portVal === 'all') {
115
+ alert('Select a specific session to add a peer');
116
+ return;
117
+ }
118
+
119
+ const address = document.getElementById('peer-address').value.trim();
120
+ const peerPort = parseInt(document.getElementById('peer-port').value) || 19532;
121
+
122
+ if (!portVal || !address) return;
123
+
124
+ try {
125
+ await api(portVal, '/peers', {
126
+ method: 'POST',
127
+ body: JSON.stringify({ address, port: peerPort }),
128
+ });
129
+ document.getElementById('peer-address').value = '';
130
+ await loadPeers();
131
+ } catch (err) {
132
+ alert('Failed: ' + err.message);
133
+ }
134
+ }
135
+
136
+ registerView('peers', { mount, unmount });
@@ -0,0 +1,153 @@
1
+ import { registerView, dashboardApi, escapeHtml } from '/app.js';
2
+
3
+ let eventSource = null;
4
+ let refreshTimer = null;
5
+
6
+ function mount(container) {
7
+ container.innerHTML = `
8
+ <div class="section-header">Terminal</div>
9
+
10
+ <div class="form-row mb-1">
11
+ <div class="form-group">
12
+ <div class="form-label">Session</div>
13
+ <select class="input" id="term-session"></select>
14
+ </div>
15
+ <div class="form-group">
16
+ <div class="form-label">Working Directory</div>
17
+ <input class="input" id="term-cwd" placeholder="/ (optional)">
18
+ </div>
19
+ </div>
20
+
21
+ <div class="prompt-bar">
22
+ <span class="prompt-prefix" id="term-prompt">$</span>
23
+ <input class="prompt-input" id="term-cmd" placeholder="Enter command..." autofocus>
24
+ <button class="btn btn-primary btn-sm" id="btn-run">Run</button>
25
+ <button class="btn btn-danger btn-sm" id="btn-cancel" style="display:none">Cancel</button>
26
+ </div>
27
+
28
+ <div class="terminal" id="term-output">
29
+ <div class="terminal-line system">Ready. Select a session and enter a command.</div>
30
+ </div>
31
+ `;
32
+
33
+ loadSessions();
34
+ refreshTimer = setInterval(loadSessions, 10000);
35
+
36
+ const cmdInput = document.getElementById('term-cmd');
37
+ cmdInput.addEventListener('keydown', (e) => {
38
+ if (e.key === 'Enter') runCommand();
39
+ });
40
+ document.getElementById('btn-run').addEventListener('click', runCommand);
41
+ document.getElementById('btn-cancel').addEventListener('click', cancelCommand);
42
+ document.getElementById('term-session').addEventListener('change', updatePrompt);
43
+ }
44
+
45
+ function unmount() {
46
+ if (eventSource) { eventSource.close(); eventSource = null; }
47
+ if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
48
+ }
49
+
50
+ async function loadSessions() {
51
+ try {
52
+ const { main, sessions } = await dashboardApi('/sessions');
53
+ const sel = document.getElementById('term-session');
54
+ const currentVal = sel.value;
55
+ const allSessions = [];
56
+ if (main && main.status === 'running') allSessions.push(main);
57
+ allSessions.push(...sessions.filter(s => s.status === 'running'));
58
+
59
+ sel.innerHTML = allSessions.map(s =>
60
+ `<option value="${s.port}" ${s.port == currentVal ? 'selected' : ''}>${escapeHtml(s.hostname)} :${s.port}</option>`
61
+ ).join('');
62
+
63
+ updatePrompt();
64
+ } catch {}
65
+ }
66
+
67
+ function updatePrompt() {
68
+ const sel = document.getElementById('term-session');
69
+ const opt = sel.options[sel.selectedIndex];
70
+ const hostname = opt ? opt.textContent.split(' :')[0] : '?';
71
+ document.getElementById('term-prompt').textContent = `${hostname} $`;
72
+ }
73
+
74
+ function runCommand() {
75
+ const port = document.getElementById('term-session').value;
76
+ const cmd = document.getElementById('term-cmd').value.trim();
77
+ const cwd = document.getElementById('term-cwd').value.trim();
78
+
79
+ if (!cmd || !port) return;
80
+
81
+ const output = document.getElementById('term-output');
82
+ output.innerHTML = '';
83
+
84
+ // Show command being run
85
+ const promptText = document.getElementById('term-prompt').textContent;
86
+ appendLine(output, `${promptText} ${cmd}`, 'system');
87
+
88
+ // Disable/enable buttons
89
+ document.getElementById('btn-run').style.display = 'none';
90
+ document.getElementById('btn-cancel').style.display = '';
91
+ document.getElementById('term-cmd').disabled = true;
92
+
93
+ // Parse command: first word is command, rest are args
94
+ const parts = cmd.split(/\s+/);
95
+ const command = parts[0];
96
+ const args = parts.slice(1);
97
+
98
+ const url = `/dashboard/api/sse/execute/${port}?command=${encodeURIComponent(command)}&args=${encodeURIComponent(JSON.stringify(args))}${cwd ? '&cwd=' + encodeURIComponent(cwd) : ''}`;
99
+
100
+ eventSource = new EventSource(url);
101
+
102
+ eventSource.onmessage = (event) => {
103
+ try {
104
+ const data = JSON.parse(event.data);
105
+ if (data.type === 'stdout') {
106
+ appendLine(output, data.data, 'stdout');
107
+ } else if (data.type === 'stderr') {
108
+ appendLine(output, data.data, 'stderr');
109
+ } else if (data.type === 'exit') {
110
+ const code = parseInt(data.data);
111
+ appendLine(output, `\nProcess exited with code ${code}`, code === 0 ? 'exit-ok' : 'exit-err');
112
+ finishCommand();
113
+ } else if (data.type === 'error') {
114
+ appendLine(output, `Error: ${data.data}`, 'exit-err');
115
+ finishCommand();
116
+ }
117
+ } catch {}
118
+ };
119
+
120
+ eventSource.onerror = () => {
121
+ appendLine(output, '\nConnection closed.', 'system');
122
+ finishCommand();
123
+ };
124
+ }
125
+
126
+ function cancelCommand() {
127
+ if (eventSource) {
128
+ eventSource.close();
129
+ eventSource = null;
130
+ }
131
+ const output = document.getElementById('term-output');
132
+ appendLine(output, '\n^C Cancelled.', 'exit-err');
133
+ finishCommand();
134
+ }
135
+
136
+ function finishCommand() {
137
+ if (eventSource) { eventSource.close(); eventSource = null; }
138
+ document.getElementById('btn-run').style.display = '';
139
+ document.getElementById('btn-cancel').style.display = 'none';
140
+ document.getElementById('term-cmd').disabled = false;
141
+ document.getElementById('term-cmd').value = '';
142
+ document.getElementById('term-cmd').focus();
143
+ }
144
+
145
+ function appendLine(container, text, className) {
146
+ const div = document.createElement('div');
147
+ div.className = `terminal-line ${className}`;
148
+ div.textContent = text;
149
+ container.appendChild(div);
150
+ container.scrollTop = container.scrollHeight;
151
+ }
152
+
153
+ registerView('terminal', { mount, unmount });
@@ -0,0 +1,3 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ export declare function registerAiTaskRoutes(app: FastifyInstance, apiKey: string, allowedKeys: Record<string, string>): void;
3
+ //# sourceMappingURL=ai-tasks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-tasks.d.ts","sourceRoot":"","sources":["../../src/routes/ai-tasks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAwB/C,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,QA2MpC"}
@@ -0,0 +1,193 @@
1
+ import { listSessions } from '../session-manager.js';
2
+ import { fetchAndDeduplicatePeers } from './peer-utils.js';
3
+ function resolveHost(address) {
4
+ const addr = address?.trim() || '';
5
+ const isRemote = addr.length > 0 && addr !== '127.0.0.1' && addr !== 'localhost';
6
+ return { host: isRemote ? addr : '127.0.0.1', isRemote };
7
+ }
8
+ function resolveApiKey(host, isRemote, localApiKey, allowedKeys) {
9
+ if (!isRemote)
10
+ return localApiKey;
11
+ // All remote peers share the same allowed key set — pick the first non-local key
12
+ for (const key of Object.values(allowedKeys)) {
13
+ if (key !== localApiKey)
14
+ return key;
15
+ }
16
+ return localApiKey;
17
+ }
18
+ export function registerAiTaskRoutes(app, apiKey, allowedKeys) {
19
+ // Dispatch an AI task to a target session/peer
20
+ app.post('/dashboard/api/ai-tasks/dispatch', async (request, reply) => {
21
+ const { targetPort, targetAddress, prompt, cwd, permissionMode, model, maxBudgetUsd, allowedTools, disallowedTools, additionalArgs, resumeSessionId } = request.body;
22
+ if (!prompt) {
23
+ reply.code(400);
24
+ return { error: 'Missing prompt' };
25
+ }
26
+ const body = { prompt, cwd, permissionMode, model, maxBudgetUsd, allowedTools, disallowedTools, additionalArgs };
27
+ if (resumeSessionId)
28
+ body.resumeSessionId = resumeSessionId;
29
+ const { host, isRemote } = resolveHost(targetAddress);
30
+ const port = targetPort || 19532;
31
+ const targetApiKey = resolveApiKey(host, isRemote, apiKey, allowedKeys);
32
+ try {
33
+ const res = await fetch(`http://${host}:${port}/api/v1/ai-tasks`, {
34
+ method: 'POST',
35
+ headers: {
36
+ 'Content-Type': 'application/json',
37
+ Authorization: `Bearer ${targetApiKey}`,
38
+ },
39
+ body: JSON.stringify(body),
40
+ signal: AbortSignal.timeout(10_000),
41
+ });
42
+ const data = await res.json();
43
+ if (!res.ok) {
44
+ reply.code(res.status);
45
+ }
46
+ return { ...data, _targetPort: port, _targetAddress: host };
47
+ }
48
+ catch (err) {
49
+ reply.code(502);
50
+ return { error: `Failed to reach target: ${err.message}` };
51
+ }
52
+ });
53
+ // Aggregate tasks across all running sessions AND remote peers
54
+ app.get('/dashboard/api/ai-tasks/all', async () => {
55
+ const { main, sessions } = await listSessions();
56
+ const running = [];
57
+ // Local sessions
58
+ if (main && main.status === 'running') {
59
+ running.push({ host: '127.0.0.1', port: main.port, hostname: main.hostname, key: apiKey });
60
+ }
61
+ for (const s of sessions.filter((s) => s.status === 'running')) {
62
+ running.push({ host: '127.0.0.1', port: s.port, hostname: s.hostname, key: apiKey });
63
+ }
64
+ // Remote peers
65
+ try {
66
+ const peers = await fetchAndDeduplicatePeers(apiKey, allowedKeys);
67
+ const remotePeers = peers.filter((p) => p.address !== '127.0.0.1' && p.address !== 'localhost' && p.status === 'online');
68
+ for (const p of remotePeers) {
69
+ const remoteKey = resolveApiKey(p.address, true, apiKey, allowedKeys);
70
+ // Avoid duplicates (same address:port)
71
+ if (!running.some((r) => r.host === p.address && r.port === p.port)) {
72
+ running.push({ host: p.address, port: p.port, hostname: p.hostname, key: remoteKey });
73
+ }
74
+ }
75
+ }
76
+ catch { }
77
+ const results = await Promise.allSettled(running.map(async (s) => {
78
+ const res = await fetch(`http://${s.host}:${s.port}/api/v1/ai-tasks`, {
79
+ headers: { Authorization: `Bearer ${s.key}` },
80
+ signal: AbortSignal.timeout(3000),
81
+ });
82
+ if (!res.ok)
83
+ return [];
84
+ const data = (await res.json());
85
+ return (data.tasks || []).map((t) => ({
86
+ ...t,
87
+ _sourcePort: s.port,
88
+ _sourceAddress: s.host,
89
+ _sourceHostname: s.hostname,
90
+ }));
91
+ }));
92
+ const tasks = [];
93
+ for (const r of results) {
94
+ if (r.status === 'fulfilled')
95
+ tasks.push(...r.value);
96
+ }
97
+ // Deduplicate by taskId
98
+ const map = new Map();
99
+ for (const t of tasks) {
100
+ if (!map.has(t.taskId))
101
+ map.set(t.taskId, t);
102
+ }
103
+ const dedupedTasks = Array.from(map.values()).sort((a, b) => b.startedAt - a.startedAt);
104
+ return { tasks: dedupedTasks, timestamp: Date.now() };
105
+ });
106
+ // SSE proxy: stream task events from a specific daemon
107
+ app.get('/dashboard/api/ai-tasks/stream/:port/:taskId', async (request, reply) => {
108
+ const port = parseInt(request.params.port, 10);
109
+ const { taskId } = request.params;
110
+ const since = request.query.since || '0';
111
+ const { host, isRemote } = resolveHost(request.query.address);
112
+ const targetKey = resolveApiKey(host, isRemote, apiKey, allowedKeys);
113
+ reply.raw.writeHead(200, {
114
+ 'Content-Type': 'text/event-stream',
115
+ 'Cache-Control': 'no-cache',
116
+ Connection: 'keep-alive',
117
+ 'X-Accel-Buffering': 'no',
118
+ });
119
+ try {
120
+ const upstreamRes = await fetch(`http://${host}:${port}/api/v1/ai-tasks/${encodeURIComponent(taskId)}/stream?since=${since}`, {
121
+ headers: { Authorization: `Bearer ${targetKey}` },
122
+ });
123
+ if (!upstreamRes.ok) {
124
+ const errText = await upstreamRes.text();
125
+ reply.raw.write(`data: ${JSON.stringify({ type: 'error', taskId, timestamp: Date.now(), data: errText })}\n\n`);
126
+ reply.raw.end();
127
+ return;
128
+ }
129
+ if (upstreamRes.body) {
130
+ for await (const chunk of upstreamRes.body) {
131
+ if (reply.raw.destroyed)
132
+ break;
133
+ reply.raw.write(chunk);
134
+ }
135
+ }
136
+ }
137
+ catch (err) {
138
+ reply.raw.write(`data: ${JSON.stringify({ type: 'error', taskId, timestamp: Date.now(), data: err.message })}\n\n`);
139
+ }
140
+ reply.raw.end();
141
+ });
142
+ // Proxy approval to daemon
143
+ app.post('/dashboard/api/ai-tasks/approve/:port/:taskId', async (request, reply) => {
144
+ const port = parseInt(request.params.port, 10);
145
+ const { taskId } = request.params;
146
+ const { host, isRemote } = resolveHost(request.query.address);
147
+ const targetKey = resolveApiKey(host, isRemote, apiKey, allowedKeys);
148
+ try {
149
+ const res = await fetch(`http://${host}:${port}/api/v1/ai-tasks/${encodeURIComponent(taskId)}/approve`, {
150
+ method: 'POST',
151
+ headers: {
152
+ 'Content-Type': 'application/json',
153
+ Authorization: `Bearer ${targetKey}`,
154
+ },
155
+ body: JSON.stringify(request.body),
156
+ signal: AbortSignal.timeout(5000),
157
+ });
158
+ const data = await res.json();
159
+ if (!res.ok)
160
+ reply.code(res.status);
161
+ return data;
162
+ }
163
+ catch (err) {
164
+ reply.code(502);
165
+ return { error: err.message };
166
+ }
167
+ });
168
+ // Proxy cancel to daemon
169
+ app.delete('/dashboard/api/ai-tasks/cancel/:port/:taskId', async (request, reply) => {
170
+ const port = parseInt(request.params.port, 10);
171
+ const { taskId } = request.params;
172
+ const { host, isRemote } = resolveHost(request.query.address);
173
+ const targetKey = resolveApiKey(host, isRemote, apiKey, allowedKeys);
174
+ try {
175
+ const res = await fetch(`http://${host}:${port}/api/v1/ai-tasks/${encodeURIComponent(taskId)}`, {
176
+ method: 'DELETE',
177
+ headers: { Authorization: `Bearer ${targetKey}` },
178
+ signal: AbortSignal.timeout(5000),
179
+ });
180
+ const data = await res.json();
181
+ if (!res.ok)
182
+ reply.code(res.status);
183
+ return data;
184
+ }
185
+ catch (err) {
186
+ reply.code(502);
187
+ return { error: err.message };
188
+ }
189
+ });
190
+ }
191
+ // Import the constant
192
+ const DEFAULT_AI_TASK_TIMEOUT = 1_800_000;
193
+ //# sourceMappingURL=ai-tasks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-tasks.js","sourceRoot":"","sources":["../../src/routes/ai-tasks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ export declare function registerMessageRoutes(app: FastifyInstance, apiKey: string, allowedKeys: Record<string, string>): void;
3
+ //# sourceMappingURL=messages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/routes/messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAW/C,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,QA0JpC"}
@@ -0,0 +1,137 @@
1
+ import { listSessions } from '../session-manager.js';
2
+ import { fetchAndDeduplicatePeers } from './peer-utils.js';
3
+ const ALLOWED_PORT_MIN = 19532;
4
+ const ALLOWED_PORT_MAX = 19640;
5
+ function randomHex4() {
6
+ return Math.random().toString(16).slice(2, 6);
7
+ }
8
+ export function registerMessageRoutes(app, apiKey, allowedKeys) {
9
+ // Unified inbox/outbox across all sessions
10
+ app.get('/dashboard/api/messages/all', async (req, reply) => {
11
+ const tab = req.query.tab === 'outbox' ? 'outbox' : 'inbox';
12
+ const prefix = tab === 'inbox' ? 'inbox:' : 'outbox:';
13
+ const { main, sessions } = await listSessions();
14
+ const running = [];
15
+ if (main && main.status === 'running')
16
+ running.push(main);
17
+ running.push(...sessions.filter((s) => s.status === 'running'));
18
+ const results = await Promise.allSettled(running.map(async (s) => {
19
+ const res = await fetch(`http://127.0.0.1:${s.port}/api/v1/context?prefix=${encodeURIComponent(prefix)}`, {
20
+ headers: { Authorization: `Bearer ${apiKey}` },
21
+ signal: AbortSignal.timeout(3000),
22
+ });
23
+ if (!res.ok)
24
+ return [];
25
+ const data = (await res.json());
26
+ return (data.entries || []).map((e) => ({ ...e, _sourcePort: s.port }));
27
+ }));
28
+ const allEntries = [];
29
+ for (const r of results) {
30
+ if (r.status === 'fulfilled')
31
+ allEntries.push(...r.value);
32
+ }
33
+ // Deduplicate by key (message IDs are globally unique)
34
+ const map = new Map();
35
+ for (const e of allEntries) {
36
+ const existing = map.get(e.key);
37
+ if (!existing || (e.updatedAt && e.updatedAt > existing.updatedAt)) {
38
+ map.set(e.key, e);
39
+ }
40
+ }
41
+ const entries = Array.from(map.values()).sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
42
+ return { entries, timestamp: Date.now() };
43
+ });
44
+ // Send message to remote peer via server-side routing
45
+ app.post('/dashboard/api/messages/send', async (req, reply) => {
46
+ const { fromPort, toHostname, body, type } = req.body;
47
+ if (!fromPort || !toHostname || !body || !type) {
48
+ return reply.status(400).send({ error: 'Missing required fields: fromPort, toHostname, body, type' });
49
+ }
50
+ if (fromPort < ALLOWED_PORT_MIN || fromPort > ALLOWED_PORT_MAX) {
51
+ return reply.status(400).send({ error: 'Invalid fromPort' });
52
+ }
53
+ // Get sender hostname
54
+ let fromHostname = 'unknown';
55
+ try {
56
+ const statusRes = await fetch(`http://127.0.0.1:${fromPort}/api/v1/status`, {
57
+ headers: { Authorization: `Bearer ${apiKey}` },
58
+ signal: AbortSignal.timeout(3000),
59
+ });
60
+ if (statusRes.ok) {
61
+ const statusData = (await statusRes.json());
62
+ fromHostname = statusData.hostname || 'unknown';
63
+ }
64
+ }
65
+ catch { }
66
+ // Find target peer using deduplicated peer list (resolves 127.0.0.1 hostnames)
67
+ let peerAddress = null;
68
+ let peerPort = null;
69
+ try {
70
+ const allPeers = await fetchAndDeduplicatePeers(apiKey, allowedKeys);
71
+ const peer = allPeers.find((p) => p.hostname === toHostname);
72
+ if (peer) {
73
+ peerAddress = peer.address;
74
+ peerPort = peer.port;
75
+ }
76
+ }
77
+ catch { }
78
+ if (!peerAddress || !peerPort) {
79
+ return reply.status(404).send({ error: `Peer "${toHostname}" not found in peer registry` });
80
+ }
81
+ // Determine peer API key: check allowedKeys, fall back to own apiKey
82
+ let peerApiKey = apiKey;
83
+ for (const [, key] of Object.entries(allowedKeys)) {
84
+ // Try each allowed key — for remote peers, their key is in our allowedKeys
85
+ // For local sessions, they share our apiKey
86
+ // We can't know which key maps to which peer by name alone,
87
+ // so for remote peers we try the first non-self key
88
+ if (key !== apiKey) {
89
+ peerApiKey = key;
90
+ break;
91
+ }
92
+ }
93
+ // Generate message ID and envelope
94
+ const id = `${Date.now()}-${fromHostname}-${randomHex4()}`;
95
+ const envelope = { from: fromHostname, to: toHostname, ts: Date.now(), id, type, body };
96
+ const value = JSON.stringify(envelope);
97
+ // Store outbox on local session
98
+ try {
99
+ await fetch(`http://127.0.0.1:${fromPort}/api/v1/context/${encodeURIComponent('outbox:' + id)}`, {
100
+ method: 'PUT',
101
+ headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
102
+ body: JSON.stringify({ value, ttl: 3600 }),
103
+ signal: AbortSignal.timeout(3000),
104
+ });
105
+ }
106
+ catch (err) {
107
+ return reply.status(500).send({ error: 'Failed to store outbox', details: err.message });
108
+ }
109
+ // Put inbox on remote peer
110
+ const inboxKey = `inbox:${toHostname}:${id}`;
111
+ try {
112
+ const inboxRes = await fetch(`http://${peerAddress}:${peerPort}/api/v1/context/${encodeURIComponent(inboxKey)}`, {
113
+ method: 'PUT',
114
+ headers: { Authorization: `Bearer ${peerApiKey}`, 'Content-Type': 'application/json' },
115
+ body: JSON.stringify({ value, ttl: 3600 }),
116
+ signal: AbortSignal.timeout(5000),
117
+ });
118
+ if (!inboxRes.ok) {
119
+ const errText = await inboxRes.text().catch(() => '');
120
+ return reply.status(502).send({
121
+ error: 'Outbox written but inbox delivery failed',
122
+ details: `Remote returned ${inboxRes.status}: ${errText}`,
123
+ id,
124
+ });
125
+ }
126
+ }
127
+ catch (err) {
128
+ return reply.status(502).send({
129
+ error: 'Outbox written but inbox delivery failed',
130
+ details: err.message,
131
+ id,
132
+ });
133
+ }
134
+ return { success: true, id, outboxKey: 'outbox:' + id, inboxKey };
135
+ });
136
+ }
137
+ //# sourceMappingURL=messages.js.map