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.
@@ -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
- let FIRST_PORT be 8081
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lampson",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "A coding agent written in Synsema: terminal + web, tools confined to the mounted project, lamps (your own tool plugins), LSP, MCP, sub-agents.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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) open(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);
@@ -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 y los workspaces :808N) no son «servidores del proyecto»
7
- const list = (r.ports || []).filter(p => !(p.name === 'synsema.exe' || p.name === 'synsema') || !/\b(web|hub)\.syn\b/.test(p.command || '') && p.port !== 8080);
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);
@@ -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(); open(a.dataset.session); });
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;
@@ -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 = () => open(s.id);
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(); open(s.id); };
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) => open(s.id),
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 open(id) {
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();
@@ -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.url; else Panel.close(); };
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
- // standalone (abierto por el puerto del workspace, no por el hub): la pill lleva al hub
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
- -- standalone (sin hub: docker, `synsema serve web.syn`): la UI vive igual bajo /w/<slug> (BASE en js/core.js)
142
- route "GET /"
143
- give redirect("/w/" + (when env("LAMPSON_WS", "") == "" then "local" otherwise env("LAMPSON_WS", "")))
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"