lampson 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/workspaces.syn +37 -1
- package/package.json +1 -1
- package/public/css/sidebar.css +4 -0
- package/public/js/app.js +1 -1
- package/public/js/procs.js +6 -3
- package/public/js/schedules.js +1 -1
- package/public/js/sessions.js +4 -4
- package/public/js/workspaces.js +7 -6
- package/web.syn +21 -3
package/lib/workspaces.syn
CHANGED
|
@@ -24,7 +24,8 @@ export let REGISTRY be ".lampson/workspaces.json"
|
|
|
24
24
|
export let HUB_PORT be 8080
|
|
25
25
|
export let HUB_FILE be "hub.syn"
|
|
26
26
|
export let HUB_TEMPLATE be "hub.tpl.syn"
|
|
27
|
-
|
|
27
|
+
-- rango alto para no chocar con lo que usan las apps (3000, 5173, 8000, 8080-8090…): solo el hub queda en 8080
|
|
28
|
+
let FIRST_PORT be 47101
|
|
28
29
|
let LINKS be ["lib", "public", "skills", "lamps", "memory"]
|
|
29
30
|
let COPIES be ["web.syn", "chat.syn"]
|
|
30
31
|
|
|
@@ -82,6 +83,38 @@ task load_reg()
|
|
|
82
83
|
task save_reg(doc)
|
|
83
84
|
write_file(REGISTRY, json_encode(doc))
|
|
84
85
|
|
|
86
|
+
-- registros hechos con puertos 808N (antes de 0.2.3): reasignar al rango alto; los procesos viejos se paran en
|
|
87
|
+
-- ensure/tick (por puerto) y el supervisor los levanta en el nuevo
|
|
88
|
+
export task migrate_ports()
|
|
89
|
+
require exec
|
|
90
|
+
require time
|
|
91
|
+
require env("LAMPSON_*")
|
|
92
|
+
require env("OS")
|
|
93
|
+
require file(".lampson")
|
|
94
|
+
require file(".lampson/*")
|
|
95
|
+
require file.read("hub.tpl.syn")
|
|
96
|
+
require file("hub.syn")
|
|
97
|
+
let doc be load_reg()
|
|
98
|
+
let changed be false
|
|
99
|
+
when doc["next_port"] < FIRST_PORT
|
|
100
|
+
set doc["next_port"] to FIRST_PORT
|
|
101
|
+
set changed to true
|
|
102
|
+
each k in keys(doc["workspaces"])
|
|
103
|
+
let w be doc["workspaces"][k]
|
|
104
|
+
when w["port"] < FIRST_PORT
|
|
105
|
+
let old be w["port"]
|
|
106
|
+
each l in proc.listeners()
|
|
107
|
+
when l["port"] == old
|
|
108
|
+
proc.kill_pid(l["pid"])
|
|
109
|
+
set w["port"] to doc["next_port"]
|
|
110
|
+
set doc["next_port"] to doc["next_port"] + 1
|
|
111
|
+
set doc["workspaces"][k] to w
|
|
112
|
+
set changed to true
|
|
113
|
+
when changed
|
|
114
|
+
save_reg(doc)
|
|
115
|
+
regen_hub()
|
|
116
|
+
give changed
|
|
117
|
+
|
|
85
118
|
export task all()
|
|
86
119
|
require file(".lampson")
|
|
87
120
|
require file(".lampson/*")
|
|
@@ -241,6 +274,7 @@ export task ensure(path)
|
|
|
241
274
|
require file.read("chat.syn")
|
|
242
275
|
require file.read("hub.tpl.syn")
|
|
243
276
|
require file("hub.syn")
|
|
277
|
+
migrate_ports()
|
|
244
278
|
let existing be by_path(path)
|
|
245
279
|
let w be existing
|
|
246
280
|
when w == nothing
|
|
@@ -490,6 +524,8 @@ export task tick()
|
|
|
490
524
|
require file(".lampson/*")
|
|
491
525
|
require file.read("web.syn")
|
|
492
526
|
require file.read("chat.syn")
|
|
527
|
+
when migrate_ports() and hub_alive()
|
|
528
|
+
restart_hub_later()
|
|
493
529
|
let started be []
|
|
494
530
|
let stopped be []
|
|
495
531
|
each w in all()
|
package/package.json
CHANGED
package/public/css/sidebar.css
CHANGED
|
@@ -78,3 +78,7 @@
|
|
|
78
78
|
.del.ask .yes { color:var(--rubric); cursor:pointer; font-weight:600; }
|
|
79
79
|
.del.ask .no { color:var(--ink-2); cursor:pointer; }
|
|
80
80
|
.del.ask .yes:hover, .del.ask .no:hover { text-decoration:underline; text-underline-offset:.18em; }
|
|
81
|
+
|
|
82
|
+
/* puertos ocupados por lampson: visibles pero sin link ni ✕ */
|
|
83
|
+
.p .port.own { color:var(--ink-3); font-weight:400; }
|
|
84
|
+
.p.own .cm { color:var(--ink-3); }
|
package/public/js/app.js
CHANGED
|
@@ -14,7 +14,7 @@ async function loadCfg() {
|
|
|
14
14
|
sel.onchange = () => { localStorage.setItem('lampson.agent', sel.value); hint(); }; hint();
|
|
15
15
|
if (!log.children.length) empty();
|
|
16
16
|
}
|
|
17
|
-
loadCfg().then(() => { loadTree(); loadSessions(); loadProcs(); loadMemory(); loadAgents(); loadMcp(); loadLsp(); loadLamps(); loadTodo(); loadSched(); loadApprovals(); checkUpdate(); const pq = new URLSearchParams(location.search).get('proc'); if (pq) openProc(pq); else if (session)
|
|
17
|
+
loadCfg().then(() => { loadTree(); loadSessions(); loadProcs(); loadMemory(); loadAgents(); loadMcp(); loadLsp(); loadLamps(); loadTodo(); loadSched(); loadApprovals(); checkUpdate(); const pq = new URLSearchParams(location.search).get('proc'); if (pq) openProc(pq); else if (session) openSession(session); });
|
|
18
18
|
// respaldos lentos por si el stream de eventos se cae
|
|
19
19
|
setInterval(loadProcs, events ? 60000 : 10000);
|
|
20
20
|
setInterval(() => { loadSched(); if (!events) loadApprovals(); }, events ? 60000 : 15000);
|
package/public/js/procs.js
CHANGED
|
@@ -3,13 +3,16 @@ let procOpen = null, procTimer = null;
|
|
|
3
3
|
async function loadPorts() {
|
|
4
4
|
let r; try { r = await (await fetch(BASE + '/api/ports')).json(); } catch (e) { return; }
|
|
5
5
|
const box = $('#ports'); box.innerHTML = '';
|
|
6
|
-
// los procesos de lampson (hub :8080
|
|
7
|
-
|
|
6
|
+
// los procesos de lampson (hub :8080, un workspace por puerto alto) OCUPAN el puerto: se ven, identificados,
|
|
7
|
+
// sin link (la UI se usa por el hub) y sin ✕ (un workspace se apaga desde su Panel; matarlo lo relanza el supervisor)
|
|
8
|
+
let wsByPort = {}; try { const ws = (await (await fetch('/api/workspaces')).json()).workspaces || []; for (const w of ws) wsByPort[w.port] = w; } catch (e) {}
|
|
9
|
+
const list = (r.ports || []).map(p => { const own = /\b(web|hub)\.syn\b/.test(p.command || '') || (p.port === 8080 && /synsema/.test(p.name || '')); return Object.assign({}, p, { lampson: own, wsName: own && wsByPort[p.port] ? wsByPort[p.port].name : (own && p.port === 8080 ? 'hub' : '') }); });
|
|
8
10
|
$('#portCount').textContent = list.length || ''; autoSec('ports', list.length > 0);
|
|
9
11
|
if (!list.length) { box.innerHTML = '<div class="p none">ninguno</div>'; return; }
|
|
10
12
|
for (const p of list) {
|
|
11
|
-
const d = document.createElement('div'); d.className = 'p'; d.style.cursor = 'default'; d.title = `pid ${p.pid}\n${p.command || '(línea de comando no disponible)'}`;
|
|
13
|
+
const d = document.createElement('div'); d.className = 'p' + (p.lampson ? ' own' : ''); d.style.cursor = 'default'; d.title = `pid ${p.pid}\n${p.command || '(línea de comando no disponible)'}`;
|
|
12
14
|
const what = p.command ? p.command.replace(/^"?[A-Za-z]:\\[^"]*\\([^"\\]+)"?/, '$1') : p.name;
|
|
15
|
+
if (p.lampson) { d.innerHTML = `<span class="port own" title="puerto ocupado por lampson">:${p.port}</span><span class="cm">lampson · ${esc(p.wsName ? (p.port === 8080 ? 'hub' : 'workspace ' + p.wsName) : 'proceso')}</span>`; box.appendChild(d); continue; }
|
|
13
16
|
d.innerHTML = `<a class="port" href="http://127.0.0.1:${p.port}" target="_blank" rel="noopener" title="abrir http://127.0.0.1:${p.port} en otra pestaña">:${p.port}</a><span class="cm">${esc(what)}</span><button title="matar este proceso (pid ${p.pid}; no lo gestiona lampson)">✕</button>`;
|
|
14
17
|
const kb = d.querySelector('button'); kb.onclick = () => { const holder = document.createElement('span'); holder.className = 'del'; kb.replaceWith(holder); inlineConfirm(holder, `¿matar pid ${p.pid}?`, async () => { await api(BASE + '/api/ports/kill', { pid: p.pid }); loadPorts(); }); setTimeout(() => { if (!holder.classList.contains('ask')) loadPorts(); }, 6100); };
|
|
15
18
|
box.appendChild(d);
|
package/public/js/schedules.js
CHANGED
|
@@ -68,7 +68,7 @@ function openSched(selectId) {
|
|
|
68
68
|
if (t === SCHED_NEW) { schedFormWire(box, err); return; }
|
|
69
69
|
box.querySelector('[data-run]').onclick = async () => { const r = await api(BASE + '/api/schedules/run', { id: t.id }); add('meta', '⏰ ' + esc(r.data.result || r.data.error || '')); setTimeout(loadSched, 1500); setTimeout(() => Panel.is('schedules') && Panel.detail(), 2500); };
|
|
70
70
|
box.querySelector('[data-toggle]').onclick = async () => { const r = await api(BASE + '/api/schedules/toggle', { id: t.id, enabled: !t.enabled }); if (!r.ok) { err(r.data.error || 'error'); return; } loadSched(); };
|
|
71
|
-
box.querySelectorAll('[data-session]').forEach(a => a.onclick = (e) => { e.preventDefault(); Panel.close();
|
|
71
|
+
box.querySelectorAll('[data-session]').forEach(a => a.onclick = (e) => { e.preventDefault(); Panel.close(); openSession(a.dataset.session); });
|
|
72
72
|
const del = box.querySelector('.dfoot .del');
|
|
73
73
|
del.onclick = () => inlineConfirm(del, `¿quitar ${t.name}?`, async () => { await api(BASE + '/api/schedules/remove', { id: t.id }); loadSched(); });
|
|
74
74
|
const live = box.querySelector('[data-live]'); if (live) live.scrollTop = live.scrollHeight;
|
package/public/js/sessions.js
CHANGED
|
@@ -9,7 +9,7 @@ async function loadSessions() {
|
|
|
9
9
|
if (!r.sessions.length) { box.innerHTML = '<div class="s none">todavía ninguna</div>'; return; }
|
|
10
10
|
const shown = r.sessions.slice(0, 4);
|
|
11
11
|
for (const s of shown) {
|
|
12
|
-
const d = document.createElement('div'); d.className = 's' + (s.id === session ? ' active' : ''); d.innerHTML = '<span class="id">' + esc(s.id.slice(-6)) + '</span><span class="title">' + esc(s.title || '') + '</span><span class="del tr" title="traza: qué hizo el agente en esta sesión (pasos, tools, tiempos, tokens)">≡</span><span class="del" title="borrar esta sesión">✕</span>'; d.title = s.id; d.onclick = () =>
|
|
12
|
+
const d = document.createElement('div'); d.className = 's' + (s.id === session ? ' active' : ''); d.innerHTML = '<span class="id">' + esc(s.id.slice(-6)) + '</span><span class="title">' + esc(s.title || '') + '</span><span class="del tr" title="traza: qué hizo el agente en esta sesión (pasos, tools, tiempos, tokens)">≡</span><span class="del" title="borrar esta sesión">✕</span>'; d.title = s.id; d.onclick = () => openSession(s.id);
|
|
13
13
|
d.querySelector('.tr').onclick = (ev) => { ev.stopPropagation(); openTrace(s.id); };
|
|
14
14
|
d.querySelector('.del:not(.tr)').onclick = (ev) => { ev.stopPropagation(); inlineConfirm(d.querySelector('.del:not(.tr)'), '¿borrar?', () => deleteSession(s.id)); };
|
|
15
15
|
box.appendChild(d);
|
|
@@ -35,18 +35,18 @@ function openSessionsPanel() {
|
|
|
35
35
|
+ `<div class="dfoot"><span>.lampson/sessions/${esc(s.id)}.json</span><span class="del">borrar</span></div>`;
|
|
36
36
|
},
|
|
37
37
|
wire: (s, box) => {
|
|
38
|
-
box.querySelector('[data-act="open"]').onclick = () => { Panel.close();
|
|
38
|
+
box.querySelector('[data-act="open"]').onclick = () => { Panel.close(); openSession(s.id); };
|
|
39
39
|
box.querySelector('[data-act="trace"]').onclick = () => { Panel.close(); openTrace(s.id); };
|
|
40
40
|
const del = box.querySelector('.dfoot .del');
|
|
41
41
|
del.onclick = () => inlineConfirm(del, `¿borrar ${s.id}?`, async () => { await deleteSession(s.id); Panel.refresh(); });
|
|
42
42
|
},
|
|
43
|
-
onPick: (s) =>
|
|
43
|
+
onPick: (s) => openSession(s.id),
|
|
44
44
|
count: (rows, q) => q ? `${rows.length} coincidencia${rows.length === 1 ? '' : 's'}` : `${rows.length} sesión${rows.length === 1 ? '' : 'es'}`,
|
|
45
45
|
emptyHtml: 'todavía no hay sesiones'
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
|
-
async function
|
|
49
|
+
async function openSession(id) {
|
|
50
50
|
session = id; localStorage.setItem('lampson.session', id); log.innerHTML = ''; log.classList.remove('hero'); showPane('log'); loadTodo();
|
|
51
51
|
const r = await fetch(BASE + '/api/sessions/' + id); if (!r.ok) { session = ''; empty(); return loadSessions(); }
|
|
52
52
|
const d = await r.json();
|
package/public/js/workspaces.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// workspaces.js — el selector de workspace de la cabecera (dentro de un workspace) y el Panel de workspaces.
|
|
2
2
|
// La API de workspaces es del HUB (raíz, sin BASE): /api/workspaces… Sin hub (standalone) el selector se oculta.
|
|
3
3
|
let wsList = [], wsIdle = 4;
|
|
4
|
+
// abierto por el puerto de un proceso (:808N) las URLs de otros workspaces tienen que ir al hub (:8080), no a este proceso
|
|
5
|
+
const HUB_BASE = (location.port && location.port !== '8080') ? location.protocol + '//' + location.hostname + ':8080' : '';
|
|
6
|
+
function wsUrl(w) { return HUB_BASE + w.url; }
|
|
4
7
|
async function fetchWorkspaces() {
|
|
5
8
|
try { const r = await (await fetch('/api/workspaces')).json(); wsList = r.workspaces || []; wsIdle = r.idle_hours; return true; } catch (e) { wsList = []; return false; }
|
|
6
9
|
}
|
|
@@ -31,12 +34,12 @@ function openWorkspaces(selectSlug) {
|
|
|
31
34
|
wire: (w, box) => {
|
|
32
35
|
const err = (m) => { const e = box.querySelector('.derr'); if (e) e.textContent = m || ''; };
|
|
33
36
|
if (w === WS_NEW) { wsNewWire(box, err); return; }
|
|
34
|
-
box.querySelector('[data-open]').onclick = () => { if (w.slug !== WS_SLUG) location.href = w
|
|
37
|
+
box.querySelector('[data-open]').onclick = () => { if (w.slug !== WS_SLUG) location.href = wsUrl(w); else Panel.close(); };
|
|
35
38
|
const st = box.querySelector('[data-start]'); if (st) st.onclick = async () => { err('arrancando…'); const r = await api('/api/workspaces/start', { slug: w.slug }); err(r.data.ok ? '' : 'no respondió a tiempo'); Panel.refresh(); };
|
|
36
39
|
const sp = box.querySelector('[data-stop]'); if (sp) sp.onclick = async () => { await api('/api/workspaces/stop', { slug: w.slug }); setTimeout(() => Panel.refresh(), 800); };
|
|
37
40
|
box.querySelector('[name="policy"]').onchange = async (ev) => { const r = await api('/api/workspaces/policy', { slug: w.slug, policy: ev.target.value }); if (!r.ok) err(r.data.error || 'error'); };
|
|
38
41
|
const del = box.querySelector('.dfoot .del');
|
|
39
|
-
del.onclick = () => inlineConfirm(del, `¿quitar ${w.name} del registro?`, async () => { const r = await api('/api/workspaces/remove', { slug: w.slug }); if (!r.ok) { err(r.data.error || 'error'); return; } if (w.slug === WS_SLUG) location.href = '/'; else Panel.refresh(); });
|
|
42
|
+
del.onclick = () => inlineConfirm(del, `¿quitar ${w.name} del registro?`, async () => { const r = await api('/api/workspaces/remove', { slug: w.slug }); if (!r.ok) { err(r.data.error || 'error'); return; } if (w.slug === WS_SLUG) location.href = HUB_BASE + '/'; else Panel.refresh(); });
|
|
40
43
|
}
|
|
41
44
|
}
|
|
42
45
|
});
|
|
@@ -87,11 +90,9 @@ function wsNewWire(box, err0) {
|
|
|
87
90
|
const r = await api('/api/workspaces', { path: chosen });
|
|
88
91
|
if (!r.ok) { err(r.data.error || ('error ' + r.status)); return; }
|
|
89
92
|
// el hub se regenera con la ruta nueva y se reinicia: esperar a que vuelva y navegar
|
|
90
|
-
const url = r.data.url; let tries = 0;
|
|
93
|
+
const url = HUB_BASE + r.data.url; let tries = 0;
|
|
91
94
|
const poll = async () => { tries++; try { const h = await fetch('/api/hub'); if (h.ok && tries > 2) { location.href = url; return; } } catch (e) {} if (tries < 40) setTimeout(poll, 500); else err('el hub no volvió: abrí ' + url + ' a mano'); };
|
|
92
95
|
setTimeout(poll, 1200);
|
|
93
96
|
};
|
|
94
97
|
}
|
|
95
|
-
|
|
96
|
-
const HUB_URL = (location.port && location.port !== '8080') ? location.protocol + '//' + location.hostname + ':8080/' : '';
|
|
97
|
-
if ($('#wsPill')) { if (HUB_URL) { const pill = $('#wsPill'); pill.style.display = ''; pill.textContent = 'workspaces ↗'; pill.title = 'esta pestaña es el proceso del workspace; los workspaces se gestionan en el hub'; pill.onclick = () => window.open(HUB_URL, '_blank'); } else { $('#wsPill').onclick = () => openWorkspaces(); paintWorkspacePill(); } }
|
|
98
|
+
if ($('#wsPill')) { $('#wsPill').onclick = () => openWorkspaces(); paintWorkspacePill(); }
|
package/web.syn
CHANGED
|
@@ -138,11 +138,29 @@ serve on 8080
|
|
|
138
138
|
auth with check_client
|
|
139
139
|
static "./public"
|
|
140
140
|
|
|
141
|
-
--
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
-- la UI vive bajo /w/<slug> (BASE en js/core.js); si alguien entra por el puerto del proceso, es lo mismo
|
|
142
|
+
-- Acceso DIRECTO por el puerto del proceso (sin X-Forwarded-Host, que solo agrega el hub al proxyear): la UI se
|
|
143
|
+
-- usa siempre por el hub → redirigir a http://127.0.0.1:8080/w/<slug>. Standalone sin hub (docker) no hay
|
|
144
|
+
-- LAMPSON_WS y se sirve acá mismo.
|
|
144
145
|
route "GET /w/:slug"
|
|
146
|
+
let h be headers of request
|
|
147
|
+
when env("LAMPSON_WS", "") != "" and not contains(h, "x-forwarded-host")
|
|
148
|
+
give redirect("http://127.0.0.1:8080/w/" + params.slug)
|
|
145
149
|
give html(read_file("public/index.html"))
|
|
150
|
+
route "GET /"
|
|
151
|
+
when env("LAMPSON_WS", "") == ""
|
|
152
|
+
give redirect("/w/local")
|
|
153
|
+
give redirect("http://127.0.0.1:8080/w/" + env("LAMPSON_WS", ""))
|
|
154
|
+
route "GET /api/hub"
|
|
155
|
+
proxy to "http://127.0.0.1:8080"
|
|
156
|
+
route "GET /api/workspaces"
|
|
157
|
+
proxy to "http://127.0.0.1:8080"
|
|
158
|
+
route "GET /api/workspaces/*path"
|
|
159
|
+
proxy to "http://127.0.0.1:8080"
|
|
160
|
+
route "POST /api/workspaces"
|
|
161
|
+
proxy to "http://127.0.0.1:8080"
|
|
162
|
+
route "POST /api/workspaces/*path"
|
|
163
|
+
proxy to "http://127.0.0.1:8080"
|
|
146
164
|
|
|
147
165
|
-- salud para el hub/supervisor y la terminal (último uso = chat, terminal, eventos o abrir la UI)
|
|
148
166
|
route "GET /w/:slug/api/health"
|