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,167 @@
1
+ import { registerView, api, dashboardApi, escapeHtml, formatTime } from '/app.js';
2
+
3
+ let refreshTimer = null;
4
+
5
+ function mount(container) {
6
+ container.innerHTML = `
7
+ <div class="section-header">Context Browser</div>
8
+
9
+ <div class="form-row mb-1">
10
+ <div class="form-group">
11
+ <select class="input" id="ctx-session"></select>
12
+ </div>
13
+ <div class="form-group">
14
+ <input class="input" id="ctx-filter" placeholder="Filter by key prefix...">
15
+ </div>
16
+ <button class="btn btn-primary btn-sm" id="btn-ctx-refresh">Refresh</button>
17
+ </div>
18
+
19
+ <div id="ctx-table-wrap"></div>
20
+
21
+ <div class="mt-1" style="border-top:1px solid var(--border-dim);padding-top:0.75rem">
22
+ <div class="form-label mb-1">New Entry</div>
23
+ <div class="form-row">
24
+ <div class="form-group"><input class="input" id="ctx-new-key" placeholder="Key"></div>
25
+ <div class="form-group"><input class="input" id="ctx-new-ttl" type="number" placeholder="TTL (sec)" style="width:100px"></div>
26
+ </div>
27
+ <div class="form-row">
28
+ <div class="form-group"><textarea class="textarea input" id="ctx-new-value" placeholder="Value" rows="2"></textarea></div>
29
+ <button class="btn btn-success" id="btn-ctx-set">Set</button>
30
+ </div>
31
+ </div>
32
+ `;
33
+
34
+ loadSessions();
35
+ document.getElementById('btn-ctx-refresh').addEventListener('click', loadEntries);
36
+ document.getElementById('btn-ctx-set').addEventListener('click', setEntry);
37
+ document.getElementById('ctx-filter').addEventListener('input', loadEntries);
38
+ document.getElementById('ctx-session').addEventListener('change', loadEntries);
39
+
40
+ refreshTimer = setInterval(loadEntries, 15000);
41
+ }
42
+
43
+ function unmount() {
44
+ if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
45
+ }
46
+
47
+ async function loadSessions() {
48
+ try {
49
+ const { main, sessions } = await dashboardApi('/sessions');
50
+ const sel = document.getElementById('ctx-session');
51
+ const all = [];
52
+ if (main && main.status === 'running') all.push(main);
53
+ all.push(...sessions.filter(s => s.status === 'running'));
54
+ sel.innerHTML = all.map(s => `<option value="${s.port}">${escapeHtml(s.hostname)} :${s.port}</option>`).join('');
55
+ loadEntries();
56
+ } catch {}
57
+ }
58
+
59
+ async function loadEntries() {
60
+ const port = document.getElementById('ctx-session').value;
61
+ const prefix = document.getElementById('ctx-filter').value.trim();
62
+ if (!port) return;
63
+
64
+ try {
65
+ const path = prefix ? `/context?prefix=${encodeURIComponent(prefix)}` : '/context';
66
+ const data = await api(port, path);
67
+ renderTable(data.entries || []);
68
+ } catch (err) {
69
+ document.getElementById('ctx-table-wrap').innerHTML = `<div class="empty">${escapeHtml(err.message)}</div>`;
70
+ }
71
+ }
72
+
73
+ function renderTable(entries) {
74
+ if (entries.length === 0) {
75
+ document.getElementById('ctx-table-wrap').innerHTML = '<div class="empty">No context entries</div>';
76
+ return;
77
+ }
78
+
79
+ const sorted = entries.sort((a, b) => a.key.localeCompare(b.key));
80
+ document.getElementById('ctx-table-wrap').innerHTML = `
81
+ <table class="data-table">
82
+ <thead>
83
+ <tr>
84
+ <th>Key</th>
85
+ <th>Value</th>
86
+ <th class="hide-mobile">TTL</th>
87
+ <th class="hide-mobile">Updated</th>
88
+ <th></th>
89
+ </tr>
90
+ </thead>
91
+ <tbody>
92
+ ${sorted.map((e, i) => `
93
+ <tr>
94
+ <td><span class="data-key" onclick="window.__toggleCtx(${i})">${escapeHtml(e.key)}</span></td>
95
+ <td><div class="data-value" id="ctx-val-${i}">${escapeHtml(truncate(e.value, 60))}</div></td>
96
+ <td class="hide-mobile text-muted text-xs">${e.ttl ? e.ttl + 's' : '—'}</td>
97
+ <td class="hide-mobile text-muted text-xs">${formatTime(e.updatedAt)}</td>
98
+ <td><button class="btn btn-danger btn-sm" onclick="window.__deleteCtx('${escapeHtml(e.key)}')">Del</button></td>
99
+ </tr>
100
+ `).join('')}
101
+ </tbody>
102
+ </table>
103
+ `;
104
+
105
+ // Store full values for expansion
106
+ window.__ctxEntries = sorted;
107
+ }
108
+
109
+ function truncate(str, len) {
110
+ return str.length > len ? str.slice(0, len) + '...' : str;
111
+ }
112
+
113
+ window.__toggleCtx = (idx) => {
114
+ const el = document.getElementById(`ctx-val-${idx}`);
115
+ const entry = window.__ctxEntries?.[idx];
116
+ if (!el || !entry) return;
117
+
118
+ if (el.classList.contains('expanded')) {
119
+ el.classList.remove('expanded');
120
+ el.textContent = truncate(entry.value, 60);
121
+ } else {
122
+ el.classList.add('expanded');
123
+ // Try JSON pretty-print
124
+ try {
125
+ const parsed = JSON.parse(entry.value);
126
+ el.textContent = JSON.stringify(parsed, null, 2);
127
+ } catch {
128
+ el.textContent = entry.value;
129
+ }
130
+ }
131
+ };
132
+
133
+ window.__deleteCtx = async (key) => {
134
+ const port = document.getElementById('ctx-session').value;
135
+ if (!port) return;
136
+ try {
137
+ await api(port, `/context/${encodeURIComponent(key)}`, { method: 'DELETE' });
138
+ await loadEntries();
139
+ } catch (err) {
140
+ alert('Failed: ' + err.message);
141
+ }
142
+ };
143
+
144
+ async function setEntry() {
145
+ const port = document.getElementById('ctx-session').value;
146
+ const key = document.getElementById('ctx-new-key').value.trim();
147
+ const value = document.getElementById('ctx-new-value').value;
148
+ const ttlStr = document.getElementById('ctx-new-ttl').value;
149
+ const ttl = ttlStr ? parseInt(ttlStr) : undefined;
150
+
151
+ if (!port || !key || !value) return;
152
+
153
+ try {
154
+ await api(port, `/context/${encodeURIComponent(key)}`, {
155
+ method: 'PUT',
156
+ body: JSON.stringify({ value, ttl }),
157
+ });
158
+ document.getElementById('ctx-new-key').value = '';
159
+ document.getElementById('ctx-new-value').value = '';
160
+ document.getElementById('ctx-new-ttl').value = '';
161
+ await loadEntries();
162
+ } catch (err) {
163
+ alert('Failed: ' + err.message);
164
+ }
165
+ }
166
+
167
+ registerView('context', { mount, unmount });
@@ -0,0 +1,263 @@
1
+ import { registerView, api, dashboardApi, escapeHtml, formatTime } from '/app.js';
2
+
3
+ let refreshTimer = null;
4
+ let activeTab = 'inbox';
5
+ let expandedMsgIdx = null;
6
+
7
+ function mount(container) {
8
+ container.innerHTML = `
9
+ <div class="section-header">Messages</div>
10
+
11
+ <div class="form-row mb-1">
12
+ <div class="form-group">
13
+ <select class="input" id="msg-session"></select>
14
+ </div>
15
+ </div>
16
+
17
+ <div class="tabs">
18
+ <div class="tab active" data-tab="inbox">Inbox</div>
19
+ <div class="tab" data-tab="outbox">Outbox</div>
20
+ <div class="tab" data-tab="compose">Compose</div>
21
+ </div>
22
+
23
+ <div id="msg-content"></div>
24
+ `;
25
+
26
+ document.querySelectorAll('.tab').forEach(t => {
27
+ t.addEventListener('click', () => {
28
+ activeTab = t.dataset.tab;
29
+ expandedMsgIdx = null;
30
+ document.querySelectorAll('.tab').forEach(x => x.classList.toggle('active', x === t));
31
+ renderTab();
32
+ });
33
+ });
34
+
35
+ loadSessions();
36
+ refreshTimer = setInterval(loadMessages, 10000);
37
+ }
38
+
39
+ function unmount() {
40
+ if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
41
+ }
42
+
43
+ async function loadSessions() {
44
+ try {
45
+ const { main, sessions } = await dashboardApi('/sessions');
46
+ const sel = document.getElementById('msg-session');
47
+ const all = [];
48
+ if (main && main.status === 'running') all.push(main);
49
+ all.push(...sessions.filter(s => s.status === 'running'));
50
+ sel.innerHTML =
51
+ '<option value="all">All Sessions</option>' +
52
+ all.map(s => `<option value="${s.port}" data-hostname="${escapeHtml(s.hostname)}">${escapeHtml(s.hostname)} :${s.port}</option>`).join('');
53
+ sel.addEventListener('change', () => { expandedMsgIdx = null; loadMessages(); });
54
+ renderTab();
55
+ } catch {}
56
+ }
57
+
58
+ async function renderTab() {
59
+ if (activeTab === 'compose') {
60
+ renderCompose();
61
+ } else {
62
+ await loadMessages();
63
+ }
64
+ }
65
+
66
+ async function loadMessages() {
67
+ const portVal = document.getElementById('msg-session').value;
68
+ if (!portVal || activeTab === 'compose') return;
69
+
70
+ const content = document.getElementById('msg-content');
71
+ try {
72
+ let entries;
73
+ if (portVal === 'all') {
74
+ const data = await dashboardApi(`/messages/all?tab=${activeTab}`);
75
+ entries = data.entries || [];
76
+ } else {
77
+ const prefix = activeTab === 'inbox' ? 'inbox:' : 'outbox:';
78
+ const data = await api(portVal, `/context?prefix=${encodeURIComponent(prefix)}`);
79
+ entries = data.entries || [];
80
+ }
81
+
82
+ // Sort by envelope timestamp (latest first), fall back to updatedAt
83
+ entries.sort((a, b) => {
84
+ let tsA = a.updatedAt || 0, tsB = b.updatedAt || 0;
85
+ try { tsA = JSON.parse(a.value).ts || tsA; } catch {}
86
+ try { tsB = JSON.parse(b.value).ts || tsB; } catch {}
87
+ return tsB - tsA;
88
+ });
89
+
90
+ if (entries.length === 0) {
91
+ content.innerHTML = `<div class="empty">No ${activeTab} messages</div>`;
92
+ return;
93
+ }
94
+
95
+ content.innerHTML = entries.map((e, i) => {
96
+ let from = '?', body = e.value, type = '?', ts = e.updatedAt;
97
+ try {
98
+ const env = JSON.parse(e.value);
99
+ from = env.from || '?';
100
+ body = env.body || e.value;
101
+ type = env.type || '?';
102
+ ts = env.ts || e.updatedAt;
103
+ } catch {}
104
+
105
+ const badgeClass = type === 'chat' ? 'badge-cyan' : type === 'request' ? 'badge-amber' : type === 'response' ? 'badge-green' : 'badge-red';
106
+ const isExpanded = expandedMsgIdx === i;
107
+ const displayBody = isExpanded ? body : truncate(body, 80);
108
+
109
+ return `
110
+ <div class="msg-row" onclick="window.__toggleMsg(${i})">
111
+ <div class="msg-header">
112
+ <span class="msg-from">${escapeHtml(from)}</span>
113
+ <span class="badge ${badgeClass}">${escapeHtml(type)}</span>
114
+ <span class="msg-time">${formatTime(ts)}</span>
115
+ </div>
116
+ <div class="msg-body${isExpanded ? ' expanded' : ''}" id="msg-body-${i}">${escapeHtml(displayBody)}</div>
117
+ </div>
118
+ `;
119
+ }).join('');
120
+
121
+ window.__msgEntries = entries;
122
+ } catch (err) {
123
+ content.innerHTML = `<div class="empty">${escapeHtml(err.message)}</div>`;
124
+ }
125
+ }
126
+
127
+ function renderCompose() {
128
+ const content = document.getElementById('msg-content');
129
+ content.innerHTML = `
130
+ <div class="form-row">
131
+ <div class="form-group">
132
+ <div class="form-label">Send From</div>
133
+ <select class="input" id="compose-from"></select>
134
+ </div>
135
+ <div class="form-group">
136
+ <div class="form-label">Send To</div>
137
+ <select class="input" id="compose-peer">
138
+ <option value="">Loading peers...</option>
139
+ </select>
140
+ </div>
141
+ <div class="form-group">
142
+ <div class="form-label">Type</div>
143
+ <select class="input" id="compose-type">
144
+ <option value="chat">Chat</option>
145
+ <option value="request">Request</option>
146
+ <option value="response">Response</option>
147
+ <option value="broadcast">Broadcast</option>
148
+ </select>
149
+ </div>
150
+ </div>
151
+ <div class="form-row">
152
+ <div class="form-group">
153
+ <div class="form-label">Message</div>
154
+ <textarea class="textarea input" id="compose-body" rows="3" placeholder="Type your message..."></textarea>
155
+ </div>
156
+ </div>
157
+ <button class="btn btn-primary" id="btn-send">Send Message</button>
158
+ <div id="compose-status" class="mt-1 text-sm"></div>
159
+ `;
160
+
161
+ // Populate "from" dropdown with specific sessions (no "all")
162
+ const msgSel = document.getElementById('msg-session');
163
+ const fromSel = document.getElementById('compose-from');
164
+ fromSel.innerHTML = Array.from(msgSel.options)
165
+ .filter(o => o.value !== 'all')
166
+ .map(o => `<option value="${o.value}" data-hostname="${o.dataset?.hostname || ''}">${o.text}</option>`)
167
+ .join('');
168
+
169
+ loadPeersForCompose();
170
+ document.getElementById('btn-send').addEventListener('click', sendMessage);
171
+ }
172
+
173
+ async function loadPeersForCompose() {
174
+ const sel = document.getElementById('compose-peer');
175
+ if (!sel) return;
176
+ try {
177
+ const data = await dashboardApi('/peers/all');
178
+ const peers = (data.peers || []).filter(p => p.status === 'online');
179
+ if (peers.length === 0) {
180
+ sel.innerHTML = '<option value="">No online peers</option>';
181
+ } else {
182
+ sel.innerHTML = peers.map(p =>
183
+ `<option value="${escapeHtml(p.hostname)}">${escapeHtml(p.hostname)} (${escapeHtml(p.address)}:${p.port})</option>`
184
+ ).join('');
185
+ }
186
+ } catch {
187
+ sel.innerHTML = '<option value="">Failed to load peers</option>';
188
+ }
189
+ }
190
+
191
+ async function sendMessage() {
192
+ const fromPort = document.getElementById('compose-from').value;
193
+ const toHostname = document.getElementById('compose-peer').value;
194
+ const type = document.getElementById('compose-type').value;
195
+ const body = document.getElementById('compose-body').value.trim();
196
+ const statusEl = document.getElementById('compose-status');
197
+
198
+ if (!fromPort || !toHostname || !body) {
199
+ statusEl.innerHTML = '<span class="text-red">Fill in all fields</span>';
200
+ return;
201
+ }
202
+
203
+ statusEl.innerHTML = '<span class="text-muted">Sending...</span>';
204
+ try {
205
+ const result = await dashboardApi('/messages/send', {
206
+ method: 'POST',
207
+ body: JSON.stringify({ fromPort: parseInt(fromPort), toHostname, body, type }),
208
+ });
209
+ if (result.success) {
210
+ statusEl.innerHTML = `<span class="text-green">Sent! ID: ${escapeHtml(result.id)}</span>`;
211
+ document.getElementById('compose-body').value = '';
212
+ } else {
213
+ statusEl.innerHTML = `<span class="text-red">${escapeHtml(result.error || 'Unknown error')}</span>`;
214
+ }
215
+ } catch (err) {
216
+ statusEl.innerHTML = `<span class="text-red">Failed: ${escapeHtml(err.message)}</span>`;
217
+ }
218
+ }
219
+
220
+ function truncate(str, len) {
221
+ return str.length > len ? str.slice(0, len) + '...' : str;
222
+ }
223
+
224
+ window.__toggleMsg = (idx) => {
225
+ const entry = window.__msgEntries?.[idx];
226
+ if (!entry) return;
227
+
228
+ if (expandedMsgIdx === idx) {
229
+ expandedMsgIdx = null;
230
+ } else {
231
+ if (expandedMsgIdx !== null) {
232
+ const prevEl = document.getElementById(`msg-body-${expandedMsgIdx}`);
233
+ const prevEntry = window.__msgEntries?.[expandedMsgIdx];
234
+ if (prevEl && prevEntry) {
235
+ prevEl.classList.remove('expanded');
236
+ let prevBody = prevEntry.value;
237
+ try { prevBody = JSON.parse(prevEntry.value).body || prevBody; } catch {}
238
+ prevEl.textContent = truncate(prevBody, 80);
239
+ }
240
+ }
241
+ expandedMsgIdx = idx;
242
+ }
243
+
244
+ const el = document.getElementById(`msg-body-${idx}`);
245
+ if (!el) return;
246
+
247
+ if (expandedMsgIdx === idx) {
248
+ el.classList.add('expanded');
249
+ try {
250
+ const env = JSON.parse(entry.value);
251
+ el.textContent = env.body || entry.value;
252
+ } catch {
253
+ el.textContent = entry.value;
254
+ }
255
+ } else {
256
+ el.classList.remove('expanded');
257
+ let body = entry.value;
258
+ try { body = JSON.parse(entry.value).body || body; } catch {}
259
+ el.textContent = truncate(body, 80);
260
+ }
261
+ };
262
+
263
+ registerView('messages', { mount, unmount });
@@ -0,0 +1,228 @@
1
+ import { registerView, dashboardApi, formatUptime, escapeHtml } 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">Command Center</div>
18
+ <div class="stats-row" id="stats-row">
19
+ <div class="stat-card"><div class="stat-value" id="stat-sessions">—</div><div class="stat-label">Sessions</div></div>
20
+ <div class="stat-card"><div class="stat-value" id="stat-peers">—</div><div class="stat-label">Network Peers</div></div>
21
+ <div class="stat-card"><div class="stat-value" id="stat-context">—</div><div class="stat-label">Context Entries</div></div>
22
+ <div class="stat-card"><div class="stat-value" id="stat-jobs">—</div><div class="stat-label">Active Jobs</div></div>
23
+ </div>
24
+
25
+ <div class="flex items-center justify-between flex-wrap gap-sm mb-1">
26
+ <div class="section-header" style="margin-bottom:0">Sessions</div>
27
+ <div class="flex gap-sm items-center">
28
+ <input type="number" class="input" id="fleet-count" value="3" min="1" max="10" style="width:60px">
29
+ <button class="btn btn-primary" id="btn-fleet">Start Fleet</button>
30
+ <button class="btn btn-warning btn-sm" id="btn-restart-all">Restart All</button>
31
+ <button class="btn btn-danger btn-sm" id="btn-stop-all">Stop All</button>
32
+ </div>
33
+ </div>
34
+
35
+ <div class="session-grid" id="session-grid"></div>
36
+
37
+ <div class="section-header" style="margin-top:1.5rem;margin-bottom:0.75rem">Network Peers</div>
38
+ <div class="peer-grid" id="network-peer-grid"></div>
39
+ `;
40
+
41
+ document.getElementById('btn-fleet').addEventListener('click', startFleet);
42
+ document.getElementById('btn-restart-all').addEventListener('click', restartAll);
43
+ document.getElementById('btn-stop-all').addEventListener('click', stopAll);
44
+
45
+ refresh();
46
+ refreshTimer = setInterval(refresh, 5000);
47
+ }
48
+
49
+ function unmount() {
50
+ if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
51
+ }
52
+
53
+ async function refresh() {
54
+ try {
55
+ const data = await dashboardApi('/status/aggregate');
56
+ renderStats(data.sessions, data.network);
57
+ renderGrid(data.sessions);
58
+ renderNetworkPeers(data.network);
59
+ } catch (err) {
60
+ document.getElementById('session-grid').innerHTML = `<div class="empty">Failed to load: ${escapeHtml(err.message)}</div>`;
61
+ }
62
+ }
63
+
64
+ function renderStats(sessions, network) {
65
+ const running = sessions.filter(s => s.status === 'running' && !s.error);
66
+ const uniquePeers = network?.uniqueCount ?? 0;
67
+ const totalCtx = running.reduce((a, s) => a + (s.context?.entries ?? 0), 0);
68
+ const totalJobs = running.reduce((a, s) => a + (s.jobs?.active ?? 0), 0);
69
+
70
+ document.getElementById('stat-sessions').textContent = running.length;
71
+ document.getElementById('stat-peers').textContent = `${network?.onlineCount ?? 0}/${uniquePeers}`;
72
+ document.getElementById('stat-context').textContent = totalCtx;
73
+ document.getElementById('stat-jobs').textContent = totalJobs;
74
+ }
75
+
76
+ function renderGrid(sessions) {
77
+ const grid = document.getElementById('session-grid');
78
+ grid.innerHTML = sessions.map(s => {
79
+ const isRunning = s.status === 'running' && !s.error;
80
+ const dotClass = isRunning ? 'online' : s.status === 'running' ? 'unknown' : 'offline';
81
+ const cardClass = isRunning ? 'running' : 'stopped';
82
+
83
+ return `
84
+ <div class="session-card ${cardClass}">
85
+ <div class="session-card-header">
86
+ <div class="session-name">
87
+ <span class="status-dot ${dotClass}"></span>
88
+ ${escapeHtml(s.hostname || s.name)}
89
+ </div>
90
+ ${isRunning
91
+ ? s.name !== 'main'
92
+ ? `<div class="flex gap-sm">
93
+ <button class="btn btn-warning btn-sm" onclick="window.__restartSession('${escapeHtml(s.name)}')">Restart</button>
94
+ <button class="btn btn-danger btn-sm" onclick="window.__stopSession('${escapeHtml(s.name)}')">Stop</button>
95
+ </div>`
96
+ : ''
97
+ : s.name !== 'main'
98
+ ? `<button class="btn btn-success btn-sm" onclick="window.__startSession('${escapeHtml(s.name)}')">Start</button>`
99
+ : ''
100
+ }
101
+ </div>
102
+ <div class="session-meta">
103
+ <span>port</span> ${s.port || '—'}
104
+ &nbsp;&middot;&nbsp;
105
+ <span>platform</span> ${s.platform || '—'}
106
+ ${isRunning ? `
107
+ <br>
108
+ <span>peers</span> ${s.peers?.online ?? 0}/${s.peers?.total ?? 0}
109
+ &nbsp;&middot;&nbsp;
110
+ <span>jobs</span> ${s.jobs?.active ?? 0}
111
+ &nbsp;&middot;&nbsp;
112
+ <span>ctx</span> ${s.context?.entries ?? 0}
113
+ <br>
114
+ <span>uptime</span> ${formatUptime(s.uptime)}
115
+ ` : ''}
116
+ ${s.error ? `<br><span class="text-red">unreachable</span>` : ''}
117
+ </div>
118
+ </div>
119
+ `;
120
+ }).join('');
121
+ }
122
+
123
+ function renderNetworkPeers(network) {
124
+ const grid = document.getElementById('network-peer-grid');
125
+ if (!grid) return;
126
+ const peers = network?.peers ?? [];
127
+
128
+ if (peers.length === 0) {
129
+ grid.innerHTML = '<div class="empty">No peers discovered</div>';
130
+ return;
131
+ }
132
+
133
+ grid.innerHTML = peers.map(p => {
134
+ const dotClass = p.status === 'online' ? 'online' : p.status === 'offline' ? 'offline' : 'unknown';
135
+ const platformIcon = platformSvg(p.platform);
136
+
137
+ return `
138
+ <div class="peer-card">
139
+ <div class="flex items-center justify-between mb-1">
140
+ <div class="flex items-center gap-sm">
141
+ <span class="status-dot ${dotClass}"></span>
142
+ <span class="font-mono text-sm" style="font-weight:600">${escapeHtml(p.hostname)}</span>
143
+ </div>
144
+ ${platformIcon}
145
+ </div>
146
+ <div class="font-mono text-xs text-muted" style="line-height:1.7">
147
+ ${escapeHtml(p.address)}:${p.port}<br>
148
+ version ${escapeHtml(p.version || '?')}<br>
149
+ last seen ${p.lastSeen ? new Date(p.lastSeen).toLocaleTimeString() : 'never'}
150
+ </div>
151
+ </div>
152
+ `;
153
+ }).join('');
154
+ }
155
+
156
+ async function startFleet() {
157
+ const count = parseInt(document.getElementById('fleet-count').value) || 3;
158
+ const btn = document.getElementById('btn-fleet');
159
+ btn.disabled = true;
160
+ try {
161
+ await dashboardApi('/sessions', {
162
+ method: 'POST',
163
+ body: JSON.stringify({ fleet: true, count }),
164
+ });
165
+ await refresh();
166
+ } catch (err) {
167
+ alert('Failed: ' + err.message);
168
+ }
169
+ btn.disabled = false;
170
+ }
171
+
172
+ async function restartAll() {
173
+ if (!confirm('Restart all sessions?')) return;
174
+ const btn = document.getElementById('btn-restart-all');
175
+ btn.disabled = true;
176
+ try {
177
+ await dashboardApi('/sessions/restart-all', { method: 'POST' });
178
+ await new Promise(r => setTimeout(r, 1500));
179
+ await refresh();
180
+ } catch (err) {
181
+ alert('Failed: ' + err.message);
182
+ }
183
+ btn.disabled = false;
184
+ }
185
+
186
+ async function stopAll() {
187
+ if (!confirm('Stop all sessions?')) return;
188
+ const btn = document.getElementById('btn-stop-all');
189
+ btn.disabled = true;
190
+ try {
191
+ const { sessions } = await dashboardApi('/sessions');
192
+ for (const s of sessions) {
193
+ if (s.status === 'running') {
194
+ try { await dashboardApi(`/sessions/${s.name}`, { method: 'DELETE' }); } catch {}
195
+ }
196
+ }
197
+ await new Promise(r => setTimeout(r, 500));
198
+ await refresh();
199
+ } catch {}
200
+ btn.disabled = false;
201
+ }
202
+
203
+ // Global handlers for inline onclick
204
+ window.__stopSession = async (name) => {
205
+ try {
206
+ await dashboardApi(`/sessions/${name}`, { method: 'DELETE' });
207
+ await new Promise(r => setTimeout(r, 500));
208
+ await refresh();
209
+ } catch (err) { alert('Failed: ' + err.message); }
210
+ };
211
+
212
+ window.__restartSession = async (name) => {
213
+ try {
214
+ await dashboardApi(`/sessions/${name}/restart`, { method: 'POST' });
215
+ await new Promise(r => setTimeout(r, 1500));
216
+ await refresh();
217
+ } catch (err) { alert('Failed: ' + err.message); }
218
+ };
219
+
220
+ window.__startSession = async (name) => {
221
+ try {
222
+ await dashboardApi('/sessions', { method: 'POST', body: JSON.stringify({ name }) });
223
+ await new Promise(r => setTimeout(r, 500));
224
+ await refresh();
225
+ } catch (err) { alert('Failed: ' + err.message); }
226
+ };
227
+
228
+ registerView('overview', { mount, unmount });