lampson 0.1.4 → 0.2.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.
- package/README.md +38 -9
- package/bin/lampson.js +1 -1
- package/chat.syn +11 -1
- package/cli.syn +68 -0
- package/hub.tpl.syn +127 -0
- package/lampson.ps1 +78 -66
- package/lampson.sh +68 -36
- package/lib/approvals.syn +5 -2
- package/lib/lsp.syn +10 -1
- package/lib/mcp.syn +10 -1
- package/lib/settings.syn +13 -5
- package/lib/skills.syn +2 -0
- package/lib/workspaces.syn +555 -0
- package/package.json +3 -1
- package/public/css/hub.css +12 -0
- package/public/hub.html +36 -0
- package/public/index.html +2 -0
- package/public/js/agents.js +3 -3
- package/public/js/app.js +1 -1
- package/public/js/approvals.js +3 -3
- package/public/js/chat.js +2 -2
- package/public/js/config.js +7 -5
- package/public/js/core.js +3 -0
- package/public/js/events.js +1 -1
- package/public/js/hub.js +40 -0
- package/public/js/lamps.js +4 -4
- package/public/js/lsp.js +3 -3
- package/public/js/mcp.js +3 -3
- package/public/js/memory.js +2 -2
- package/public/js/procs.js +6 -6
- package/public/js/schedules.js +7 -7
- package/public/js/sessions.js +6 -6
- package/public/js/terminal.js +1 -1
- package/public/js/todo.js +1 -1
- package/public/js/tree.js +3 -3
- package/public/js/update.js +2 -2
- package/public/js/workspaces.js +83 -0
- package/skills/lampson/SKILL.md +5 -0
- package/skills/synsema/SKILL.md +4 -1
- package/web.syn +70 -51
package/public/js/tree.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// tree.js — árbol de archivos (panel derecho) con estado git, y el visor de archivos
|
|
2
2
|
let gitStatus = { changes: {} };
|
|
3
3
|
async function loadGit() {
|
|
4
|
-
try { gitStatus = await (await fetch('/api/git')).json(); } catch (e) { gitStatus = { repo: false, changes: {} }; }
|
|
4
|
+
try { gitStatus = await (await fetch(BASE + '/api/git')).json(); } catch (e) { gitStatus = { repo: false, changes: {} }; }
|
|
5
5
|
const g = $('#git'); if (!gitStatus.repo) { g.style.display = 'none'; return; }
|
|
6
6
|
const k = gitStatus.counts || {}; const nuevos = (k.added||0)+(k.untracked||0);
|
|
7
7
|
const parts = []; // solo contadores distintos de cero — lo demás es ruido
|
|
@@ -20,7 +20,7 @@ function gitClass(path, isDir) {
|
|
|
20
20
|
async function loadTree() {
|
|
21
21
|
await loadGit();
|
|
22
22
|
const box = $('#tree'); box.innerHTML = '';
|
|
23
|
-
let t; try { t = await (await fetch('/api/tree')).json(); } catch (e) { box.innerHTML = '<div class="row none">sin árbol</div>'; return; }
|
|
23
|
+
let t; try { t = await (await fetch(BASE + '/api/tree')).json(); } catch (e) { box.innerHTML = '<div class="row none">sin árbol</div>'; return; }
|
|
24
24
|
const render = (entries, parent, depth) => {
|
|
25
25
|
for (const e of entries) {
|
|
26
26
|
const n = document.createElement('div'); n.className = 'node';
|
|
@@ -41,7 +41,7 @@ async function loadTree() {
|
|
|
41
41
|
}
|
|
42
42
|
async function openFile(path, row) {
|
|
43
43
|
document.querySelectorAll('.row.active').forEach(x => x.classList.remove('active')); if (row) row.classList.add('active');
|
|
44
|
-
const r = await fetch('/api/file?path=' + encodeURIComponent(path)); const d = await r.json();
|
|
44
|
+
const r = await fetch(BASE + '/api/file?path=' + encodeURIComponent(path)); const d = await r.json();
|
|
45
45
|
if (!r.ok) { add('denied', esc(d.error || 'error')); return; }
|
|
46
46
|
$('#vpath').textContent = d.path; $('#vmeta').textContent = d.lines + ' líneas';
|
|
47
47
|
$('#vbody').className = ''; $('#vbody').innerHTML = String(d.content).split('\n').map((l, i) => `<span class="ln">${i + 1}</span>${esc(l)}`).join('\n');
|
package/public/js/update.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// update.js — actualizaciones de Lampson (una vez por carga; el fetch a origin puede tardar)
|
|
2
2
|
async function checkUpdate() {
|
|
3
|
-
let u; try { u = await (await fetch('/api/update')).json(); } catch (e) { return; }
|
|
3
|
+
let u; try { u = await (await fetch(BASE + '/api/update')).json(); } catch (e) { return; }
|
|
4
4
|
const b = $('#update'); if (!u.available) { b.style.display = 'none'; return; }
|
|
5
5
|
b.textContent = `⬆ actualizar · ${u.behind} commit${u.behind === 1 ? '' : 's'}`;
|
|
6
6
|
b.title = `${u.current} → ${u.latest}\n${(u.notes || []).map(n => '· ' + n).join('\n')}\n\nen terminal: ${u.command}`;
|
|
7
7
|
b.style.display = '';
|
|
8
8
|
b.onclick = async () => {
|
|
9
9
|
b.disabled = true; b.textContent = 'actualizando…';
|
|
10
|
-
let r; try { r = await (await fetch('/api/update', { method: 'POST' })).json(); } catch (e) { r = { result: 'no se pudo actualizar: ' + e.message }; }
|
|
10
|
+
let r; try { r = await (await fetch(BASE + '/api/update', { method: 'POST' })).json(); } catch (e) { r = { result: 'no se pudo actualizar: ' + e.message }; }
|
|
11
11
|
add('meta', '⬆ ' + esc(r.result || ''));
|
|
12
12
|
b.disabled = false; b.textContent = '⬆ reiniciá el servidor'; b.onclick = null;
|
|
13
13
|
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// workspaces.js — el selector de workspace de la cabecera (dentro de un workspace) y el Panel de workspaces.
|
|
2
|
+
// La API de workspaces es del HUB (raíz, sin BASE): /api/workspaces… Sin hub (standalone) el selector se oculta.
|
|
3
|
+
let wsList = [], wsIdle = 4;
|
|
4
|
+
async function fetchWorkspaces() {
|
|
5
|
+
try { const r = await (await fetch('/api/workspaces')).json(); wsList = r.workspaces || []; wsIdle = r.idle_hours; return true; } catch (e) { wsList = []; return false; }
|
|
6
|
+
}
|
|
7
|
+
async function paintWorkspacePill() {
|
|
8
|
+
const pill = $('#wsPill'); if (!pill) return;
|
|
9
|
+
if (!BASE) { pill.style.display = 'none'; return; }
|
|
10
|
+
const ok = await fetchWorkspaces();
|
|
11
|
+
const me = wsList.find(w => w.slug === WS_SLUG);
|
|
12
|
+
pill.style.display = ok ? '' : 'none';
|
|
13
|
+
pill.textContent = (me ? me.name : WS_SLUG) + ' ▾'; pill.title = (me ? me.path + '\n' : '') + 'cambiar de workspace · los demás siguen vivos';
|
|
14
|
+
}
|
|
15
|
+
const WS_NEW = { slug: '__new' };
|
|
16
|
+
function openWorkspaces(selectSlug) {
|
|
17
|
+
Panel.open({
|
|
18
|
+
id: 'workspaces', eyebrow: 'workspaces', title: 'Workspaces', sub: 'cada uno corre en su propio proceso', layout: 'browse', select: selectSlug || WS_SLUG || null,
|
|
19
|
+
browse: {
|
|
20
|
+
placeholder: 'buscar workspace…', listWidth: '300px', key: w => w.slug,
|
|
21
|
+
load: async (q) => { await fetchWorkspaces(); const list = wsList.filter(w => !q || (w.name + ' ' + w.path).toLowerCase().includes(q.toLowerCase())); return q ? list : [...list, WS_NEW]; },
|
|
22
|
+
render: (w) => w === WS_NEW ? `<span class="dot">+</span><div><div class="nm" style="color:var(--accent);font-weight:400">nuevo workspace…</div><div class="meta">elegí una carpeta</div></div>` : `<span class="dot">${w.alive ? '●' : '○'}</span><div><div class="nm">${esc(w.name)}${w.slug === WS_SLUG ? ' <span class="meta">(este)</span>' : ''}</div><div class="meta">${esc(w.path)}</div></div>`,
|
|
23
|
+
count: (rows) => { const n = rows.filter(r => r !== WS_NEW).length, a = rows.filter(r => r !== WS_NEW && r.alive).length; return `${n} workspace${n === 1 ? '' : 's'} · ${a} vivo${a === 1 ? '' : 's'}`; },
|
|
24
|
+
emptyHtml: 'nada coincide', emptyDetail: 'Todavía no hay workspaces. Creá uno con «nuevo workspace…» o abrí <code>lampson</code> en la carpeta de un proyecto.',
|
|
25
|
+
detail: (w) => w === WS_NEW ? wsNewForm() : `<div class="dhead"><span class="nm">${esc(w.name)}</span><span class="meta">${w.alive ? '● vivo' : '○ apagado'} · ${esc(w.policy)}</span></div>
|
|
26
|
+
<div class="dcap">${esc(w.path)}</div>
|
|
27
|
+
<div class="ddesc">${w.schedules_on ? `${w.schedules_on} tarea${w.schedules_on === 1 ? '' : 's'} programada${w.schedules_on === 1 ? '' : 's'} encendida${w.schedules_on === 1 ? '' : 's'} (lo mantienen vivo). ` : ''}Último uso: ${esc(fmtWhen(w.last_used))}.</div>
|
|
28
|
+
<div class="dacts"><button class="primary" data-open>${w.slug === WS_SLUG ? 'Ya estás acá' : 'Abrir'}</button>${w.alive ? '<button data-stop>■ apagar</button>' : '<button data-start>▶ encender</button>'}</div>
|
|
29
|
+
<div class="dform" style="margin-top:14px"><label>vida <select name="policy"><option value="auto" ${w.policy === 'auto' ? 'selected' : ''}>auto — vivo mientras se use (${wsIdle === 0 ? 'nunca se apaga' : 'se apaga tras ' + wsIdle + ' h sin uso'}) o tenga tareas</option><option value="always" ${w.policy === 'always' ? 'selected' : ''}>siempre vivo</option><option value="off" ${w.policy === 'off' ? 'selected' : ''}>apagado — solo cuando lo abrís</option></select></label><span class="ds">las horas de inactividad se cambian en ⚙ → General</span></div>
|
|
30
|
+
<div class="dfoot"><span>/w/${esc(w.slug)}/ · :${w.port} (solo loopback)</span><span class="del">quitar del registro</span></div><div class="derr"></div>`,
|
|
31
|
+
wire: (w, box) => {
|
|
32
|
+
const err = (m) => { const e = box.querySelector('.derr'); if (e) e.textContent = m || ''; };
|
|
33
|
+
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(); };
|
|
35
|
+
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
|
+
const sp = box.querySelector('[data-stop]'); if (sp) sp.onclick = async () => { await api('/api/workspaces/stop', { slug: w.slug }); setTimeout(() => Panel.refresh(), 800); };
|
|
37
|
+
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
|
+
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(); });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function wsNewForm() {
|
|
45
|
+
return `<div class="dhead"><span class="nm serif">Nuevo workspace</span></div><div class="dform">
|
|
46
|
+
<p class="lead">Un workspace es una carpeta de tu disco: el agente solo puede tocar lo que hay adentro. Corre en su propio proceso, con sus sesiones, tareas programadas, MCP y lámparas.</p>
|
|
47
|
+
<div class="dacts" style="margin-top:0"><button class="primary" data-pick>Elegir carpeta…</button><span class="ds" data-pickhint>abre el diálogo de tu sistema</span></div>
|
|
48
|
+
<label style="margin-top:14px">ruta <input name="path" spellcheck="false" autocomplete="off" placeholder="C:\\proyectos\\mi-app · /home/yo/proyectos/mi-app"></label>
|
|
49
|
+
<div data-browser style="display:none"><div class="dcap" data-here></div><div class="bitems" data-dirs style="max-height:32vh;border:1px solid var(--rule);border-radius:var(--r);padding:4px"></div></div>
|
|
50
|
+
<div class="pfoot"><button class="primary" data-go>Crear y abrir</button><button data-browse>explorar en el servidor</button><span class="derr"></span></div></div>`;
|
|
51
|
+
}
|
|
52
|
+
function wsNewWire(box, err) {
|
|
53
|
+
const f = n => box.querySelector(`[name="${n}"]`);
|
|
54
|
+
const browser = box.querySelector('[data-browser]');
|
|
55
|
+
const showDir = async (p) => {
|
|
56
|
+
const r = await (await fetch('/api/workspaces/browse?path=' + encodeURIComponent(p || ''))).json();
|
|
57
|
+
browser.style.display = '';
|
|
58
|
+
box.querySelector('[data-here]').textContent = r.path + (r.error ? ' — ' + r.error : '');
|
|
59
|
+
f('path').value = r.path || '';
|
|
60
|
+
const list = box.querySelector('[data-dirs]'); list.innerHTML = '';
|
|
61
|
+
const up = document.createElement('div'); up.className = 'li nodot'; up.innerHTML = '<div><div class="nm">..</div></div>'; up.onclick = () => showDir(r.parent); list.appendChild(up);
|
|
62
|
+
for (const d of (r.dirs || [])) { const it = document.createElement('div'); it.className = 'li nodot'; it.innerHTML = `<div><div class="nm" style="font-weight:400">${esc(d)}/</div></div>`; it.onclick = () => showDir(r.path.replace(/[\\/]$/, '') + (r.path.includes('\\') ? '\\' : '/') + d); list.appendChild(it); }
|
|
63
|
+
};
|
|
64
|
+
box.querySelector('[data-browse]').onclick = () => showDir(f('path').value.trim());
|
|
65
|
+
box.querySelector('[data-pick]').onclick = async () => {
|
|
66
|
+
err('esperando el diálogo… (mirá la barra de tareas)');
|
|
67
|
+
const r = await api('/api/workspaces/pick', {});
|
|
68
|
+
if (!r.data.native) { err(''); box.querySelector('[data-pickhint]').textContent = 'no hay diálogo en este equipo (VPS): usá el explorador'; showDir(''); return; }
|
|
69
|
+
err(''); if (r.data.path) f('path').value = r.data.path;
|
|
70
|
+
};
|
|
71
|
+
box.querySelector('[data-go]').onclick = async () => {
|
|
72
|
+
const path = f('path').value.trim(); if (!path) { err('elegí una carpeta'); return; }
|
|
73
|
+
err('creando y arrancando… (el hub se reinicia un instante)');
|
|
74
|
+
const r = await api('/api/workspaces', { path });
|
|
75
|
+
if (!r.ok) { err(r.data.error || ('error ' + r.status)); return; }
|
|
76
|
+
// el hub se regenera con la ruta nueva y se reinicia (--watch): esperar a que vuelva y navegar
|
|
77
|
+
const url = r.data.url; let tries = 0;
|
|
78
|
+
const poll = async () => { tries++; try { const h = await fetch(BASE + '/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'); };
|
|
79
|
+
setTimeout(poll, 1200);
|
|
80
|
+
};
|
|
81
|
+
f('path').focus();
|
|
82
|
+
}
|
|
83
|
+
if ($('#wsPill')) { $('#wsPill').onclick = () => openWorkspaces(); paintWorkspacePill(); }
|
package/skills/lampson/SKILL.md
CHANGED
|
@@ -78,6 +78,11 @@ the terminal button and run `npm run dev`), then tell me the URL".
|
|
|
78
78
|
a vision model instead of guessing what the image shows.
|
|
79
79
|
- Sessions can be deleted (`/delete <id>`, or ✕ in the web sidebar).
|
|
80
80
|
|
|
81
|
+
## Workspaces
|
|
82
|
+
- Each project folder is a workspace with its own Lampson process; the user switches between them from the
|
|
83
|
+
header pill or http://127.0.0.1:8080 (the hub). You only ever see this workspace. If the user asks to work on
|
|
84
|
+
another project, tell them to open it as a workspace (hub screen, or `lampson` in that folder) — you cannot.
|
|
85
|
+
|
|
81
86
|
## Network exposure
|
|
82
87
|
- The web server listens on all interfaces but every `/api/*` route (and the terminal socket) only
|
|
83
88
|
accepts loopback clients; others get 401 unless they present `LAMPSON_WEB_TOKEN`. If the user asks
|
package/skills/synsema/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: synsema
|
|
|
3
3
|
description: Writing, checking, running and testing Synsema (.syn) code — syntax reflexes, capabilities, live processes / pseudo-terminals, and the runtime traps that cost hours. Load before touching any .syn file.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Synsema quick reference (v0.6.
|
|
6
|
+
# Synsema quick reference (v0.6.12)
|
|
7
7
|
|
|
8
8
|
> Curated 10 KB summary for the agent (the full reference is ~450 KB and lives in the user's editor
|
|
9
9
|
> skill). Kept in sync by hand with each `synsema update`; if `synsema --version` is newer than the
|
|
@@ -55,6 +55,9 @@ description: Writing, checking, running and testing Synsema (.syn) code — synt
|
|
|
55
55
|
`proc_spawn` in a handler is gone when the handler returns. A process that must outlive requests lives
|
|
56
56
|
inside an `agent` spawned from the handler (own lifecycle; blackboard `share/observe` + `bus_*` are shared
|
|
57
57
|
with handlers). That is how lampson's `process` tool works (`lib/tools/proc.syn`).
|
|
58
|
+
- **Reverse proxy streams (v0.6.12+)**: `proxy to "http://127.0.0.1:N"` passes SSE in real time and tunnels
|
|
59
|
+
`Upgrade: websocket`; needs `require net("<upstream host>")`; `GET /*path` does not match `/`. **Cron expressions
|
|
60
|
+
(v0.6.12+)**: `cron_every("0 9 * * mon-fri", task, {"tz": "-03:00"})` — fixed offset, no DST, no persistence.
|
|
58
61
|
- **Own terminal / raw keys (v0.6.11+)**: `let h be term_open({"ctrl_c": "exit"})` → `nothing` without a
|
|
59
62
|
TTY / under `test`/`serve` (fall back to `read_line`); `term_recv(h, secs)` → `{type: "key", key, text,
|
|
60
63
|
ctrl, alt, shift}` (`key` = `"char"|"enter"|"tab"|"backspace"|"up"|…`), `paste`, `resize`, `eof`;
|
package/web.syn
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
-- web.syn — Lampson por URL (chat web con SSE)
|
|
2
2
|
--
|
|
3
|
-
--
|
|
3
|
+
-- Un proceso POR WORKSPACE, lanzado por el hub / cli.syn con cwd = .lampson/ws/<slug> (ver lib/workspaces.syn):
|
|
4
|
+
-- synsema serve web.syn --port 808N --bind 127.0.0.1 --env-file <home>/.env
|
|
5
|
+
-- Todas las rutas van bajo /w/:slug/… porque el hub (hub.syn, :8080) proxyea sin quitar el prefijo. El :slug se
|
|
6
|
+
-- ignora (cada proceso es un workspace); LAMPSON_WS trae el propio. Standalone sigue sirviendo en :8080.
|
|
4
7
|
--
|
|
5
|
-
-- API:
|
|
6
|
-
-- GET /
|
|
8
|
+
-- API (bajo /w/<slug>):
|
|
9
|
+
-- GET /api/health {alive, last_used, schedules_on} (hub, terminal)
|
|
7
10
|
-- GET /api/sessions lista de sesiones
|
|
8
11
|
-- GET /api/sessions/:id historial de una sesión
|
|
9
12
|
-- POST /api/chat {session?, message, agent?} → SSE con eventos del loop y "done" al final
|
|
@@ -37,6 +40,8 @@ require file("memory")
|
|
|
37
40
|
require file("memory/*")
|
|
38
41
|
require file(".lampson")
|
|
39
42
|
require file(".lampson/*")
|
|
43
|
+
require file.read("public")
|
|
44
|
+
require file.read("public/*")
|
|
40
45
|
|
|
41
46
|
use "./lib/provider.syn" as provider
|
|
42
47
|
use "./lib/tools.syn" as tools
|
|
@@ -133,28 +138,39 @@ serve on 8080
|
|
|
133
138
|
auth with check_client
|
|
134
139
|
static "./public"
|
|
135
140
|
|
|
136
|
-
|
|
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", "")))
|
|
144
|
+
route "GET /w/:slug"
|
|
145
|
+
give html(read_file("public/index.html"))
|
|
146
|
+
|
|
147
|
+
-- salud para el hub/supervisor y la terminal (último uso = chat, terminal, eventos o abrir la UI)
|
|
148
|
+
route "GET /w/:slug/api/health"
|
|
149
|
+
give {"alive": true, "slug": env("LAMPSON_WS", ""), "last_used": state_get("lampson:last_used", nothing), "schedules_on": length(where(schedule.all(), (t) => t["enabled"] == true)), "version": env("LAMPSON_VERSION", "")}
|
|
150
|
+
|
|
151
|
+
route "GET /w/:slug/api/sessions" requires auth
|
|
152
|
+
state_set("lampson:last_used", now())
|
|
137
153
|
give {"sessions": session.list()}
|
|
138
154
|
|
|
139
155
|
-- buscador de sesiones (modal «ver todas»): ?q=texto busca en títulos y mensajes de este proyecto
|
|
140
|
-
route "GET /api/sessions/search" requires auth
|
|
156
|
+
route "GET /w/:slug/api/sessions/search" requires auth
|
|
141
157
|
let q be when contains(query, "q") then text(query.q) otherwise ""
|
|
142
158
|
let n be when contains(query, "limit") then floor(number(query.limit)) otherwise 200
|
|
143
159
|
give {"q": q, "sessions": session.search(q, n)}
|
|
144
160
|
|
|
145
|
-
route "GET /api/sessions/:id" requires auth
|
|
161
|
+
route "GET /w/:slug/api/sessions/:id" requires auth
|
|
146
162
|
when not session.exists(params.id)
|
|
147
163
|
give not_found("session not found")
|
|
148
164
|
let doc be session.load(params.id)
|
|
149
165
|
let visible be where(doc["messages"], (m) => m["role"] != "system")
|
|
150
166
|
give {"id": doc["id"], "updated": doc["updated"], "meta": doc["meta"], "messages": visible}
|
|
151
167
|
|
|
152
|
-
route "GET /api/config" requires auth
|
|
168
|
+
route "GET /w/:slug/api/config" requires auth
|
|
153
169
|
let cfg be provider.config()
|
|
154
170
|
give {"provider": cfg["provider"], "model": cfg["model"], "wire": cfg["wire"], "vision": provider.supports_vision(cfg), "providers": provider.providers(), "configured": provider.configured(), "permission": lower(env("LAMPSON_PERMISSION", "ask")), "agents": agents.names(), "workspace": env("LAMPSON_WORKSPACE", "./workspace"), "profiles": agents.PROFILES}
|
|
155
171
|
|
|
156
172
|
-- proveedor/modelo por defecto y API keys → .lampson/config.json (local). La key nunca vuelve al cliente.
|
|
157
|
-
route "POST /api/settings" requires auth
|
|
173
|
+
route "POST /w/:slug/api/settings" requires auth
|
|
158
174
|
expect body {provider: text}
|
|
159
175
|
let b be json of request
|
|
160
176
|
let name be lower(text(b["provider"]))
|
|
@@ -167,12 +183,12 @@ serve on 8080
|
|
|
167
183
|
let ncfg be provider.config_for(name, "")
|
|
168
184
|
give {"ok": true, "providers": provider.providers(), "provider": name, "model": ncfg["model"], "vision": provider.supports_vision(ncfg), "configured": provider.configured()}
|
|
169
185
|
|
|
170
|
-
route "POST /api/sessions/delete" requires auth
|
|
186
|
+
route "POST /w/:slug/api/sessions/delete" requires auth
|
|
171
187
|
expect body {id: text}
|
|
172
188
|
let b be json of request
|
|
173
189
|
give {"result": session.delete(text(b["id"]))}
|
|
174
190
|
|
|
175
|
-
route "GET /api/tree" requires auth
|
|
191
|
+
route "GET /w/:slug/api/tree" requires auth
|
|
176
192
|
give tree.tree(6)
|
|
177
193
|
|
|
178
194
|
-- Terminal real en el navegador: un shell dentro de un pseudo-terminal (pty) por conexión WebSocket.
|
|
@@ -180,8 +196,9 @@ serve on 8080
|
|
|
180
196
|
-- Servidor → navegador: frames de texto con prefijo: "o" + salida cruda del pty (ANSI incluido, xterm.js
|
|
181
197
|
-- la dibuja) | "c" + JSON de control ({type: hello|exit}).
|
|
182
198
|
-- El shell vive lo que vive el socket: cerrar el panel lo mata (el runtime no deja huérfanos).
|
|
183
|
-
route "GET /api/term" requires auth
|
|
199
|
+
route "GET /w/:slug/api/term" requires auth
|
|
184
200
|
socket
|
|
201
|
+
state_set("lampson:last_used", now())
|
|
185
202
|
let sh be term_shell()
|
|
186
203
|
let p be proc_spawn(sh[0], sh[1], {"cwd": term_cwd(), "pty": true, "cols": 120, "rows": 32})
|
|
187
204
|
ws_send(socket, "c" + json_encode({"type": "hello", "shell": sh[0], "pid": proc_stats(p)["pid"], "cwd": term_cwd()}))
|
|
@@ -208,25 +225,25 @@ serve on 8080
|
|
|
208
225
|
ws_send(socket, "o" + as_text(ev["data"]))
|
|
209
226
|
proc_close(p)
|
|
210
227
|
|
|
211
|
-
route "GET /api/update" requires auth
|
|
228
|
+
route "GET /w/:slug/api/update" requires auth
|
|
212
229
|
give update.check()
|
|
213
230
|
|
|
214
|
-
route "POST /api/update" requires auth
|
|
231
|
+
route "POST /w/:slug/api/update" requires auth
|
|
215
232
|
give {"result": update.apply()}
|
|
216
233
|
|
|
217
|
-
route "GET /api/git" requires auth
|
|
234
|
+
route "GET /w/:slug/api/git" requires auth
|
|
218
235
|
give git.status()
|
|
219
236
|
|
|
220
|
-
route "GET /api/memory" requires auth
|
|
237
|
+
route "GET /w/:slug/api/memory" requires auth
|
|
221
238
|
give {"dir": memo.dir(), "notes": memo.list()}
|
|
222
239
|
|
|
223
|
-
route "GET /api/memory/note" requires auth
|
|
240
|
+
route "GET /w/:slug/api/memory/note" requires auth
|
|
224
241
|
when not contains(query, "name")
|
|
225
242
|
give fail(400, "name required")
|
|
226
243
|
give {"name": query.name, "content": memo.note_read(query.name)}
|
|
227
244
|
|
|
228
245
|
-- traza legible de una sesión (ver lib/trace.syn)
|
|
229
|
-
route "GET /api/trace" requires auth
|
|
246
|
+
route "GET /w/:slug/api/trace" requires auth
|
|
230
247
|
when not contains(query, "session")
|
|
231
248
|
give fail(400, "session required")
|
|
232
249
|
when not matches(query.session, "[A-Za-z0-9_-]{1,40}")
|
|
@@ -235,22 +252,22 @@ serve on 8080
|
|
|
235
252
|
give {"session": query.session, "file": trace.file_of(query.session), "trace": trace.tail(query.session, n)}
|
|
236
253
|
|
|
237
254
|
-- modelos que declara la API del proveedor (para no tipear nombres a ciegas: DeepSeek es case-sensitive)
|
|
238
|
-
route "GET /api/models" requires auth
|
|
255
|
+
route "GET /w/:slug/api/models" requires auth
|
|
239
256
|
let pname be when contains(query, "provider") then lower(text(query.provider)) otherwise ""
|
|
240
257
|
let cfg be provider.config_for(pname, "")
|
|
241
258
|
give provider.list_models(cfg)
|
|
242
259
|
|
|
243
|
-
route "GET /api/todo" requires auth
|
|
260
|
+
route "GET /w/:slug/api/todo" requires auth
|
|
244
261
|
-- ?session=<id> → la lista de esa sesión; sin query, la de la sesión en curso
|
|
245
262
|
when contains(query, "session") and matches(query.session, "[A-Za-z0-9_-]{1,40}")
|
|
246
263
|
give {"items": todo.load(query.session)}
|
|
247
264
|
give {"items": todo.load(nothing)}
|
|
248
265
|
|
|
249
|
-
route "GET /api/mcp" requires auth
|
|
266
|
+
route "GET /w/:slug/api/mcp" requires auth
|
|
250
267
|
give {"servers": mcp.summary(), "global": mcp.GLOBAL_CONFIG, "project": mcp.PROJECT_CONFIG}
|
|
251
268
|
|
|
252
269
|
-- conectar un server MCP desde la UI: se escribe en el mcp.json del scope y arranca su supervisor
|
|
253
|
-
route "POST /api/mcp/add" requires auth
|
|
270
|
+
route "POST /w/:slug/api/mcp/add" requires auth
|
|
254
271
|
expect body {name: text, command: text}
|
|
255
272
|
let b be json of request
|
|
256
273
|
let scope be when contains(b, "scope") then lower(text(b["scope"])) otherwise "global"
|
|
@@ -260,7 +277,7 @@ serve on 8080
|
|
|
260
277
|
recover err
|
|
261
278
|
give fail(400, text(err))
|
|
262
279
|
|
|
263
|
-
route "POST /api/mcp/remove" requires auth
|
|
280
|
+
route "POST /w/:slug/api/mcp/remove" requires auth
|
|
264
281
|
expect body {name: text}
|
|
265
282
|
let b be json of request
|
|
266
283
|
try
|
|
@@ -269,10 +286,10 @@ serve on 8080
|
|
|
269
286
|
give fail(400, text(err))
|
|
270
287
|
|
|
271
288
|
-- language servers (navegación semántica): listar, agregar (preset o comando propio), quitar
|
|
272
|
-
route "GET /api/lsp" requires auth
|
|
289
|
+
route "GET /w/:slug/api/lsp" requires auth
|
|
273
290
|
give {"servers": lsp.summary(), "presets": lsp.PRESETS, "global": lsp.GLOBAL_CONFIG, "project": lsp.PROJECT_CONFIG}
|
|
274
291
|
|
|
275
|
-
route "POST /api/lsp/add" requires auth
|
|
292
|
+
route "POST /w/:slug/api/lsp/add" requires auth
|
|
276
293
|
expect body {name: text}
|
|
277
294
|
let b be json of request
|
|
278
295
|
let scope be when contains(b, "scope") then lower(text(b["scope"])) otherwise "global"
|
|
@@ -283,7 +300,7 @@ serve on 8080
|
|
|
283
300
|
recover err
|
|
284
301
|
give fail(400, text(err))
|
|
285
302
|
|
|
286
|
-
route "POST /api/lsp/remove" requires auth
|
|
303
|
+
route "POST /w/:slug/api/lsp/remove" requires auth
|
|
287
304
|
expect body {name: text}
|
|
288
305
|
let b be json of request
|
|
289
306
|
try
|
|
@@ -292,11 +309,11 @@ serve on 8080
|
|
|
292
309
|
give fail(400, text(err))
|
|
293
310
|
|
|
294
311
|
-- lámparas (plugins de tools): listar y encender/apagar desde la barra superior de la UI
|
|
295
|
-
route "GET /api/lamps" requires auth
|
|
312
|
+
route "GET /w/:slug/api/lamps" requires auth
|
|
296
313
|
give {"lamps": lamps.summary(), "global": lamps.GLOBAL_DIR, "project": lamps.PROJECT_DIR}
|
|
297
314
|
|
|
298
315
|
-- el usuario corre una tool de una lámpara encendida él mismo (sin pasar por el modelo)
|
|
299
|
-
route "POST /api/lamps/run" requires auth
|
|
316
|
+
route "POST /w/:slug/api/lamps/run" requires auth
|
|
300
317
|
expect body {tool: text}
|
|
301
318
|
let b be json of request
|
|
302
319
|
let args be when contains(b, "args") then b["args"] otherwise {}
|
|
@@ -305,7 +322,7 @@ serve on 8080
|
|
|
305
322
|
recover err
|
|
306
323
|
give fail(400, text(err))
|
|
307
324
|
|
|
308
|
-
route "POST /api/lamps/remove" requires auth
|
|
325
|
+
route "POST /w/:slug/api/lamps/remove" requires auth
|
|
309
326
|
expect body {name: text}
|
|
310
327
|
let b be json of request
|
|
311
328
|
try
|
|
@@ -313,7 +330,7 @@ serve on 8080
|
|
|
313
330
|
recover err
|
|
314
331
|
give fail(400, text(err))
|
|
315
332
|
|
|
316
|
-
route "POST /api/lamps/toggle" requires auth
|
|
333
|
+
route "POST /w/:slug/api/lamps/toggle" requires auth
|
|
317
334
|
expect body {name: text}
|
|
318
335
|
let b be json of request
|
|
319
336
|
let on be contains(b, "enabled") and b["enabled"] == true
|
|
@@ -322,15 +339,16 @@ serve on 8080
|
|
|
322
339
|
recover err
|
|
323
340
|
give fail(400, text(err))
|
|
324
341
|
|
|
325
|
-
route "GET /api/agents" requires auth
|
|
342
|
+
route "GET /w/:slug/api/agents" requires auth
|
|
326
343
|
agents.prune(10, 1800)
|
|
327
344
|
give {"agents": agents.list_children()}
|
|
328
345
|
|
|
329
346
|
-- Eventos en vivo para la UI (SSE): lo que publican los supervisores de procesos (proc.<name>: líneas y
|
|
330
347
|
-- cambios de estado) y los subagentes (subagent.started|done|<id>). Un bus por programa; cada cliente
|
|
331
348
|
-- tiene su cola acotada (drop_oldest). Heartbeat cada 25 s para que el proxy/navegador no cierre.
|
|
332
|
-
route "GET /api/events" requires auth
|
|
349
|
+
route "GET /w/:slug/api/events" requires auth
|
|
333
350
|
stream
|
|
351
|
+
state_set("lampson:last_used", now())
|
|
334
352
|
let sub be bus_subscribe(["proc.*", "subagent.*", "mcp.*", "approval.*", "schedule.*"], {"max_queue": 512})
|
|
335
353
|
let open be true
|
|
336
354
|
while open
|
|
@@ -344,7 +362,7 @@ serve on 8080
|
|
|
344
362
|
set open to false
|
|
345
363
|
bus_unsubscribe(sub)
|
|
346
364
|
|
|
347
|
-
route "GET /api/agents/log" requires auth
|
|
365
|
+
route "GET /w/:slug/api/agents/log" requires auth
|
|
348
366
|
when not contains(query, "id")
|
|
349
367
|
give fail(400, "id required")
|
|
350
368
|
when not matches(query.id, "[a-z]+-[0-9]+")
|
|
@@ -359,7 +377,7 @@ serve on 8080
|
|
|
359
377
|
recover err
|
|
360
378
|
give fail(404, "no such sub-agent")
|
|
361
379
|
|
|
362
|
-
route "POST /api/agents/stop" requires auth
|
|
380
|
+
route "POST /w/:slug/api/agents/stop" requires auth
|
|
363
381
|
expect body {id: text}
|
|
364
382
|
let b be json of request
|
|
365
383
|
when not matches(b["id"], "[a-z]+-[0-9]+")
|
|
@@ -368,38 +386,38 @@ serve on 8080
|
|
|
368
386
|
bus_publish("subagent." + b["id"], {"kind": "stop", "line": "stop requested from the UI"})
|
|
369
387
|
give {"result": "stop requested for " + b["id"]}
|
|
370
388
|
|
|
371
|
-
route "GET /api/proc" requires auth
|
|
389
|
+
route "GET /w/:slug/api/proc" requires auth
|
|
372
390
|
give {"processes": proc.list()}
|
|
373
391
|
|
|
374
|
-
route "GET /api/proc/log" requires auth
|
|
392
|
+
route "GET /w/:slug/api/proc/log" requires auth
|
|
375
393
|
when not contains(query, "name")
|
|
376
394
|
give fail(400, "name required")
|
|
377
395
|
let n be when contains(query, "tail") then number(query.tail) otherwise 200
|
|
378
396
|
give {"name": query.name, "running": proc.alive(query.name), "log": proc.logs(query.name, n)}
|
|
379
397
|
|
|
380
|
-
route "GET /api/ports" requires auth
|
|
398
|
+
route "GET /w/:slug/api/ports" requires auth
|
|
381
399
|
give {"ports": proc.listeners()}
|
|
382
400
|
|
|
383
|
-
route "POST /api/ports/kill" requires auth
|
|
401
|
+
route "POST /w/:slug/api/ports/kill" requires auth
|
|
384
402
|
expect body {pid: number}
|
|
385
403
|
let b be json of request
|
|
386
404
|
give {"result": proc.kill_pid(b["pid"])}
|
|
387
405
|
|
|
388
|
-
route "POST /api/proc/stop" requires auth
|
|
406
|
+
route "POST /w/:slug/api/proc/stop" requires auth
|
|
389
407
|
expect body {name: text}
|
|
390
408
|
let b be json of request
|
|
391
409
|
give {"result": proc.halt(b["name"])}
|
|
392
410
|
|
|
393
|
-
route "GET /api/file" requires auth
|
|
411
|
+
route "GET /w/:slug/api/file" requires auth
|
|
394
412
|
when not contains(query, "path")
|
|
395
413
|
give fail(400, "path required")
|
|
396
414
|
give tree.file_content(query.path)
|
|
397
415
|
|
|
398
416
|
-- configuración general (rueda de la UI): zona horaria, URL pública, webhook… → .lampson/config.json
|
|
399
|
-
route "GET /api/settings/values" requires auth
|
|
417
|
+
route "GET /w/:slug/api/settings/values" requires auth
|
|
400
418
|
give {"values": settings.values(), "tz_detected": schedule.tz_offset()}
|
|
401
419
|
|
|
402
|
-
route "POST /api/settings/values" requires auth
|
|
420
|
+
route "POST /w/:slug/api/settings/values" requires auth
|
|
403
421
|
let b be json of request
|
|
404
422
|
try
|
|
405
423
|
each k in keys(b)
|
|
@@ -411,17 +429,17 @@ serve on 8080
|
|
|
411
429
|
give fail(400, text(err))
|
|
412
430
|
|
|
413
431
|
-- aprobaciones (lib/approvals.syn): las del chat y las de tareas programadas comparten la cola
|
|
414
|
-
route "POST /api/approve" requires auth
|
|
432
|
+
route "POST /w/:slug/api/approve" requires auth
|
|
415
433
|
expect body {id: text, decision: bool}
|
|
416
434
|
let b be json of request
|
|
417
435
|
give {"ok": approvals.answer(text(b["id"]), b["decision"])}
|
|
418
436
|
|
|
419
|
-
route "GET /api/approvals" requires auth
|
|
437
|
+
route "GET /w/:slug/api/approvals" requires auth
|
|
420
438
|
give {"approvals": approvals.list(), "public_url": approvals.public_url(), "webhook": approvals.webhook_url() != ""}
|
|
421
439
|
|
|
422
440
|
-- link de decisión (PÚBLICO a propósito: lo abre el humano desde el teléfono; el token de un solo uso es la
|
|
423
441
|
-- autorización, como en el `approve` nativo de Synsema bajo serve). GET porque un link en un chat es un GET.
|
|
424
|
-
route "GET /approve/:id/:token"
|
|
442
|
+
route "GET /w/:slug/approve/:id/:token"
|
|
425
443
|
let d be when contains(query, "d") then lower(text(query.d)) otherwise ""
|
|
426
444
|
when d != "yes" and d != "no"
|
|
427
445
|
give html(approvals.html_page("¿Permitir?", "<a href='?d=yes' style='display:inline-block;padding:10px 18px;border-radius:4px;background:#1d1c1a;color:#fff;text-decoration:none;margin-right:10px'>Sí, permitir</a> <a href='?d=no' style='display:inline-block;padding:10px 18px;border-radius:4px;border:1px solid #a33;color:#a33;text-decoration:none'>No, denegar</a>"))
|
|
@@ -431,12 +449,12 @@ serve on 8080
|
|
|
431
449
|
give html(approvals.html_page(when d == "yes" then "✓ Permitido" otherwise "✗ Denegado", "Lampson sigue con la tarea. Podés cerrar esta pestaña."))
|
|
432
450
|
|
|
433
451
|
-- tareas programadas (lib/schedule.syn): listar, crear, quitar, encender/apagar, correr ahora, log
|
|
434
|
-
route "GET /api/schedules" requires auth
|
|
452
|
+
route "GET /w/:slug/api/schedules" requires auth
|
|
435
453
|
let age be schedule.daemon_age()
|
|
436
454
|
-- este proceso tiene su propio tick (cron_every): las tareas corren mientras la web esté abierta
|
|
437
455
|
give {"tasks": schedule.summary(), "tick": schedule.TICK_SECONDS, "alive": true, "heartbeat_age": age, "mount_ok": schedule.mount_ok(), "workspace": env("LAMPSON_WORKSPACE", ""), "tz": schedule.tz_offset(), "lamps": lamps.summary()}
|
|
438
456
|
|
|
439
|
-
route "POST /api/schedules/add" requires auth
|
|
457
|
+
route "POST /w/:slug/api/schedules/add" requires auth
|
|
440
458
|
let b be json of request
|
|
441
459
|
try
|
|
442
460
|
let t be schedule.add(b)
|
|
@@ -444,7 +462,7 @@ serve on 8080
|
|
|
444
462
|
recover err
|
|
445
463
|
give fail(400, text(err))
|
|
446
464
|
|
|
447
|
-
route "POST /api/schedules/remove" requires auth
|
|
465
|
+
route "POST /w/:slug/api/schedules/remove" requires auth
|
|
448
466
|
expect body {id: text}
|
|
449
467
|
let b be json of request
|
|
450
468
|
try
|
|
@@ -452,7 +470,7 @@ serve on 8080
|
|
|
452
470
|
recover err
|
|
453
471
|
give fail(400, text(err))
|
|
454
472
|
|
|
455
|
-
route "POST /api/schedules/toggle" requires auth
|
|
473
|
+
route "POST /w/:slug/api/schedules/toggle" requires auth
|
|
456
474
|
expect body {id: text}
|
|
457
475
|
let b be json of request
|
|
458
476
|
let on be contains(b, "enabled") and b["enabled"] == true
|
|
@@ -463,7 +481,7 @@ serve on 8080
|
|
|
463
481
|
|
|
464
482
|
-- correr ahora: la marca run_now la levanta el próximo tick (así el request no se queda minutos abierto
|
|
465
483
|
-- y la corrida vive en el hilo del cron, no en el del request)
|
|
466
|
-
route "POST /api/schedules/run" requires auth
|
|
484
|
+
route "POST /w/:slug/api/schedules/run" requires auth
|
|
467
485
|
expect body {id: text}
|
|
468
486
|
let b be json of request
|
|
469
487
|
try
|
|
@@ -472,13 +490,13 @@ serve on 8080
|
|
|
472
490
|
recover err
|
|
473
491
|
give fail(400, text(err))
|
|
474
492
|
|
|
475
|
-
route "GET /api/schedules/log" requires auth
|
|
493
|
+
route "GET /w/:slug/api/schedules/log" requires auth
|
|
476
494
|
when not contains(query, "id")
|
|
477
495
|
give fail(400, "id required")
|
|
478
496
|
let n be when contains(query, "tail") then floor(number(query.tail)) otherwise 300
|
|
479
497
|
give {"id": query.id, "log": schedule.log_tail(query.id, n), "task": schedule.get(query.id)}
|
|
480
498
|
|
|
481
|
-
route "POST /api/chat" requires auth
|
|
499
|
+
route "POST /w/:slug/api/chat" requires auth
|
|
482
500
|
expect body {message: text}
|
|
483
501
|
let b be json of request
|
|
484
502
|
let message be b["message"]
|
|
@@ -489,6 +507,7 @@ serve on 8080
|
|
|
489
507
|
set mode to "ask"
|
|
490
508
|
let pname be when contains(b, "provider") then lower(text(b["provider"])) otherwise ""
|
|
491
509
|
let pmodel be when contains(b, "model") then text(b["model"]) otherwise ""
|
|
510
|
+
state_set("lampson:last_used", now())
|
|
492
511
|
stream
|
|
493
512
|
task emit(kind, data, tag)
|
|
494
513
|
trace.event(sid, kind, data, tag)
|