lampson 0.2.6 → 0.2.7
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 +14 -9
- package/bin/lampson.js +13 -7
- package/chat.syn +35 -30
- package/lampson.ps1 +11 -1
- package/lampson.sh +7 -0
- package/lib/agents.syn +6 -6
- package/lib/loop.syn +4 -4
- package/lib/permission.syn +14 -14
- package/lib/plugins.syn +408 -0
- package/lib/prompt.syn +2 -2
- package/lib/sched_run.syn +10 -10
- package/lib/schedule.syn +36 -21
- package/lib/tools/memo.syn +27 -5
- package/lib/tools.syn +30 -30
- package/lib/workspaces.syn +34 -9
- package/package.json +3 -3
- package/{lamps/example-hello/lamp.json → plugins/example-hello/plugin.json} +2 -2
- package/plugins/example-hello/plugin.syn +19 -0
- package/public/css/panel.css +3 -3
- package/public/hub.html +1 -1
- package/public/index.html +3 -3
- package/public/js/app.js +1 -1
- package/public/js/chat.js +4 -4
- package/public/js/memory.js +2 -1
- package/public/js/{lamps.js → plugins.js} +43 -41
- package/public/js/schedules.js +9 -9
- package/public/js/workspaces.js +1 -1
- package/skills/lampson/SKILL.md +18 -11
- package/web.syn +17 -16
- package/lamps/example-hello/lamp.syn +0 -19
- package/lib/lamps.syn +0 -386
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
//
|
|
1
|
+
// plugins.js — plugins (tools propias): pill en la cabecera + vista browse del Panel (lista, detalle con
|
|
2
2
|
// switch, tools con formulario generado del schema, correr, borrar) y la ayuda «?».
|
|
3
|
-
let
|
|
4
|
-
async function
|
|
5
|
-
let r; try { r = await (await fetch(BASE + '/api/
|
|
6
|
-
|
|
7
|
-
const on =
|
|
8
|
-
const pill = $('#
|
|
9
|
-
return
|
|
3
|
+
let pluginsData = [], pluginsMeta = { global: 'plugins/', project: '.lampson/plugins/' };
|
|
4
|
+
async function fetchPlugins() {
|
|
5
|
+
let r; try { r = await (await fetch(BASE + '/api/plugins')).json(); } catch (e) { return pluginsData; }
|
|
6
|
+
pluginsData = r.plugins || []; pluginsMeta = { global: r.global || 'plugins/', project: r.project || '.lampson/plugins/' };
|
|
7
|
+
const on = pluginsData.filter(l => l.enabled).length;
|
|
8
|
+
const pill = $('#plugins'); pill.textContent = pluginsData.length ? `plugins ${on}/${pluginsData.length}` : 'plugins'; pill.classList.toggle('on', on > 0);
|
|
9
|
+
return pluginsData;
|
|
10
10
|
}
|
|
11
|
-
async function
|
|
12
|
-
function
|
|
11
|
+
async function loadPlugins() { await fetchPlugins(); if (Panel.is('plugins')) Panel.refresh(); }
|
|
12
|
+
function pluginMatches(l, q) {
|
|
13
13
|
if (!q) return true;
|
|
14
14
|
const hay = [l.name, l.description || '', l.scope, l.kind, ...(l.tools || [])].join(' ').toLowerCase();
|
|
15
15
|
return q.toLowerCase().split(/\s+/).every(t => hay.includes(t));
|
|
16
16
|
}
|
|
17
|
-
function
|
|
17
|
+
function pluginField(k, p, required) {
|
|
18
18
|
const def = p.default !== undefined ? p.default : ''; const req = required.includes(k) ? ' <span class="req">(obligatorio)</span>' : '';
|
|
19
19
|
let input;
|
|
20
20
|
if (Array.isArray(p.enum)) input = `<select data-k="${esc(k)}">${p.enum.map(v => `<option value="${esc(String(v))}" ${String(v) === String(def) ? 'selected' : ''}>${esc(String(v))}</option>`).join('')}</select>`;
|
|
@@ -24,7 +24,7 @@ function lampField(k, p, required) {
|
|
|
24
24
|
return `<label class="lf"><span class="lk">${esc(k)}${req}</span>${input}${p.description ? `<span class="ds">${esc(p.description)}</span>` : ''}</label>`;
|
|
25
25
|
}
|
|
26
26
|
// lee los campos de una tool → args (o {error})
|
|
27
|
-
function
|
|
27
|
+
function pluginArgs(tdiv, props, required) {
|
|
28
28
|
const args = {};
|
|
29
29
|
for (const el of tdiv.querySelectorAll('[data-k]')) {
|
|
30
30
|
const k = el.dataset.k, p = props[k] || {}; let v = el.value;
|
|
@@ -36,10 +36,10 @@ function lampArgs(tdiv, props, required) {
|
|
|
36
36
|
}
|
|
37
37
|
return { args };
|
|
38
38
|
}
|
|
39
|
-
const
|
|
40
|
-
<p>
|
|
41
|
-
<p><b>
|
|
42
|
-
<p><b>
|
|
39
|
+
const PLUGINS_HELP = `<h4>Herramientas hechas a medida para este proyecto</h4>
|
|
40
|
+
<p>Un plugin es una tool que el agente (o vos) escribe para <em>este</em> repo: una carpeta con <code>plugin.json</code> y el código — en Synsema (corre bajo un techo de capacidades que no puede superar) o en cualquier lenguaje (<code>python</code>, <code>node</code>, un binario). A diferencia de un script suelto, el modelo lo ve en su catálogo con parámetros tipados, vos lo podés correr desde acá, y queda en el repo para la próxima sesión.</p>
|
|
41
|
+
<p><b>Pedilo en el chat</b> — <i>«hacé un plugin que liste los endpoints del backend»</i>. El agente lo escribe en <code data-project></code>, lo valida y te pide encenderlo acá.</p>
|
|
42
|
+
<p><b>Encendido = existe</b> — apagado, el agente ni lo ve. Al encenderlo, sus tools entran al catálogo como <code>plugin_<plugin>_<tool></code> desde el próximo turno. Los globales viven en <code data-global></code> y valen para todos los proyectos.</p>
|
|
43
43
|
<p><b>Ejemplos</b></p>
|
|
44
44
|
<ul>
|
|
45
45
|
<li><b>Consultas del proyecto</b> — <code>endpoints()</code> que parsea tus rutas y devuelve método + path + handler; <code>env_keys()</code> usadas vs. definidas en <code>.env.example</code>.</li>
|
|
@@ -48,51 +48,53 @@ const LAMPS_HELP = `<h4>Herramientas hechas a medida para este proyecto</h4>
|
|
|
48
48
|
<li><b>Datos locales</b> — <code>db_schema()</code> / <code>query(sql)</code> contra tu SQLite con techo <code>db=./dev.db</code>.</li>
|
|
49
49
|
<li><b>Integraciones sin MCP</b> — <code>deploy_status()</code> contra tu API con techo <code>net=api.x.com</code>.</li>
|
|
50
50
|
</ul>
|
|
51
|
-
<p
|
|
52
|
-
|
|
51
|
+
<p>¿Querés herramientas con techo real, versionadas y compartidas entre agentes (Claude Code, Cursor, lampson…)? Eso son las <a href="https://lamps.sh" target="_blank" rel="noopener">lámparas de lamps.sh</a>: se instalan con <code>lamp add</code> y entran acá como servidor MCP (<code>lamp mcp</code>).</p>
|
|
52
|
+
<p><button class="primary" data-back>← volver a los plugins</button></p>`;
|
|
53
|
+
function openPlugins(selectName) {
|
|
53
54
|
let first = selectName || null;
|
|
54
55
|
Panel.open({
|
|
55
|
-
id: '
|
|
56
|
-
headActions: [{ label: '?', title: '¿qué es
|
|
56
|
+
id: 'plugins', eyebrow: 'plugins', title: 'Plugins', layout: 'browse',
|
|
57
|
+
headActions: [{ label: '?', title: '¿qué es un plugin?', onClick: (b) => { Panel.help(true); const h = b.querySelector('.phelp'); h.querySelector('[data-project]').textContent = pluginsMeta.project; h.querySelector('[data-global]').textContent = pluginsMeta.global; h.querySelector('[data-back]').onclick = () => Panel.help(false); } }],
|
|
57
58
|
browse: {
|
|
58
|
-
placeholder: 'buscar
|
|
59
|
+
placeholder: 'buscar plugin o tool…', listWidth: '260px', help: PLUGINS_HELP,
|
|
59
60
|
key: l => l.name,
|
|
60
|
-
load: async (q) => { const all = await
|
|
61
|
-
render: (l) => `<span class="dot">${l.error ? '!' : (l.enabled ? '●' : '○')}</span><div><div class="nm">${esc(l.name)}</div><div class="meta">${esc(l.scope)} · ${esc(l.kind)} · ${(l.tools || []).length} tool${(l.tools || []).length === 1 ? '' : 's'}${l.error ? ' ·
|
|
62
|
-
count: (rows, q) => q ? `${rows.length} de ${
|
|
63
|
-
emptyHtml: '
|
|
64
|
-
emptyDetail: `Todavía no hay
|
|
61
|
+
load: async (q) => { const all = await fetchPlugins(); const list = all.filter(l => pluginMatches(l, q)); if (first) { const i = list.findIndex(l => l.name === first); first = null; if (i > 0) list.unshift(list.splice(i, 1)[0]); } return list; },
|
|
62
|
+
render: (l) => `<span class="dot">${l.error ? '!' : (l.enabled ? '●' : '○')}</span><div><div class="nm">${esc(l.name)}</div><div class="meta">${esc(l.scope)} · ${esc(l.kind)} · ${(l.tools || []).length} tool${(l.tools || []).length === 1 ? '' : 's'}${l.error ? ' · roto' : ''}${l.legacy ? ' · carpeta vieja' : ''}</div></div>`,
|
|
63
|
+
count: (rows, q) => q ? `${rows.length} de ${pluginsData.length}` : `${pluginsData.length} plugin${pluginsData.length === 1 ? '' : 's'}`,
|
|
64
|
+
emptyHtml: 'ninguno todavía',
|
|
65
|
+
emptyDetail: `Todavía no hay plugins.<br><br>Pedile uno al agente en el chat — <i>«hacé un plugin que liste los endpoints del backend»</i> — o creá una carpeta con <code>plugin.json</code> en <code>${esc(pluginsMeta.project)}</code> (proyecto) o <code>${esc(pluginsMeta.global)}</code> (global). El botón <b>?</b> de arriba explica qué son y para qué sirven.`,
|
|
65
66
|
detail: (l) => {
|
|
66
67
|
const specs = l.tool_specs || [];
|
|
67
|
-
const cap = l.error ? `<div class="dcap err">
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
+ (l.
|
|
68
|
+
const cap = l.error ? `<div class="dcap err">roto: ${esc(l.error)}</div>` : (l.kind === 'syn' ? `<div class="dcap">techo de capacidades: ${esc(l.caps)}</div>` : `<div class="dcap"><b>sin techo</b> (exec) · $ ${esc(l.command)}</div>`);
|
|
69
|
+
const legacy = l.legacy ? `<div class="dnote">Carpeta vieja: vive en <code>.lampson/lamps/</code>. Sigue funcionando, pero renombrala a <code>.lampson/plugins/</code> (y <code>lamp.json</code> a <code>plugin.json</code>); las variables <code>LAMP_*</code> siguen llegando por ahora.</div>` : '';
|
|
70
|
+
return `<div class="dhead"><span class="nm">${esc(l.name)}</span><span class="meta">${esc(l.scope)} · ${esc(l.kind)}</span><label class="sw ${l.enabled ? 'on' : ''}" title="${l.error ? 'no se puede encender: está roto' : (l.enabled ? 'apagar' : 'encender')}"><input type="checkbox" ${l.enabled ? 'checked' : ''} ${l.error ? 'disabled' : ''}>${l.enabled ? 'encendido' : 'apagado'}</label></div>`
|
|
71
|
+
+ (l.description ? `<div class="ddesc">${esc(l.description)}</div>` : '') + cap + legacy
|
|
72
|
+
+ (l.enabled || l.error ? '' : `<div class="dnote">Apagado: el agente no lo ve y no se puede correr. Encendelo con el switch — el código va a poder ejecutarse${l.kind === 'exec' ? ' <b>sin techo de capacidades</b> (es un ejecutable)' : ' bajo el techo de arriba'}.</div>`)
|
|
71
73
|
+ specs.map((t, i) => {
|
|
72
74
|
const props = (t.parameters && t.parameters.properties) || {}; const required = (t.parameters && t.parameters.required) || [];
|
|
73
|
-
return `<div class="tool" data-i="${i}"><div class="th"><code>${esc(t.name)}</code>${t.description ? `<span class="ds">${esc(t.description)}</span>` : '<span></span>'}<span class="ds">
|
|
74
|
-
+ (Object.keys(props).length ? Object.keys(props).map(k =>
|
|
75
|
+
return `<div class="tool" data-i="${i}"><div class="th"><code>${esc(t.name)}</code>${t.description ? `<span class="ds">${esc(t.description)}</span>` : '<span></span>'}<span class="ds">plugin_${esc(l.name)}_${esc(t.name)}</span></div>`
|
|
76
|
+
+ (Object.keys(props).length ? Object.keys(props).map(k => pluginField(k, props[k], required)).join('') : '<div class="ds" style="margin-top:6px;color:var(--ink-3)">no toma parámetros</div>')
|
|
75
77
|
+ (l.enabled ? `<div class="go"><button class="primary run">▶ correr</button><span class="st"></span></div><pre class="out" style="display:none"></pre>` : '')
|
|
76
78
|
+ `</div>`;
|
|
77
79
|
}).join('')
|
|
78
|
-
+ `<div class="dfoot"><span>${esc(l.dir || '')}</span>${l.scope === 'project' ? '<span class="del">eliminar</span>' : '<span title="
|
|
80
|
+
+ `<div class="dfoot"><span>${esc(l.dir || '')}</span>${l.scope === 'project' ? '<span class="del">eliminar</span>' : '<span title="los globales se borran a mano">global · se borra a mano</span>'}</div><div class="err" data-err></div>`;
|
|
79
81
|
},
|
|
80
82
|
wire: (l, box) => {
|
|
81
83
|
const errEl = box.querySelector('[data-err]');
|
|
82
84
|
box.querySelector('.sw input').onchange = async (ev) => {
|
|
83
85
|
errEl.textContent = '';
|
|
84
|
-
const r = await api(BASE + '/api/
|
|
86
|
+
const r = await api(BASE + '/api/plugins/toggle', { name: l.name, enabled: ev.target.checked });
|
|
85
87
|
if (!r.ok) { errEl.textContent = r.data.error || ('error ' + r.status); ev.target.checked = !ev.target.checked; return; }
|
|
86
|
-
add('meta', '☼ ' + esc(r.data.result || ''));
|
|
88
|
+
add('meta', '☼ ' + esc(r.data.result || '')); loadPlugins(); loadSched();
|
|
87
89
|
};
|
|
88
90
|
for (const tdiv of box.querySelectorAll('.tool')) {
|
|
89
91
|
const t = (l.tool_specs || [])[Number(tdiv.dataset.i)]; const props = (t.parameters && t.parameters.properties) || {}; const required = (t.parameters && t.parameters.required) || [];
|
|
90
92
|
const b = tdiv.querySelector('button.run'); if (!b) continue;
|
|
91
93
|
b.onclick = async () => {
|
|
92
|
-
const st = tdiv.querySelector('.st'), out = tdiv.querySelector('.out'); const got =
|
|
94
|
+
const st = tdiv.querySelector('.st'), out = tdiv.querySelector('.out'); const got = pluginArgs(tdiv, props, required);
|
|
93
95
|
if (got.error) { st.textContent = got.error; return; }
|
|
94
96
|
st.textContent = 'corriendo…'; out.style.display = 'none'; b.disabled = true;
|
|
95
|
-
const r = await api(BASE + '/api/
|
|
97
|
+
const r = await api(BASE + '/api/plugins/run', { tool: 'plugin_' + l.name + '_' + t.name, args: got.args });
|
|
96
98
|
st.textContent = r.ok ? '' : (r.data.error || 'error ' + r.status);
|
|
97
99
|
if (r.ok) { out.textContent = r.data.output; out.style.display = ''; }
|
|
98
100
|
b.disabled = false;
|
|
@@ -100,13 +102,13 @@ function openLamps(selectName) {
|
|
|
100
102
|
}
|
|
101
103
|
const del = box.querySelector('.dfoot .del');
|
|
102
104
|
if (del) del.onclick = () => inlineConfirm(del, `¿borrar ${l.name} del disco?`, async () => {
|
|
103
|
-
const r = await api(BASE + '/api/
|
|
105
|
+
const r = await api(BASE + '/api/plugins/remove', { name: l.name });
|
|
104
106
|
if (!r.ok) { errEl.textContent = r.data.error || ('error ' + r.status); return; }
|
|
105
|
-
add('meta', '☼ ' + esc(r.data.result || ''));
|
|
107
|
+
add('meta', '☼ ' + esc(r.data.result || '')); loadPlugins();
|
|
106
108
|
});
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
111
|
});
|
|
110
112
|
}
|
|
111
|
-
$('#
|
|
112
|
-
if (location.hash === '#lamps') setTimeout(() =>
|
|
113
|
+
$('#plugins').onclick = () => openPlugins();
|
|
114
|
+
if (location.hash === '#plugins' || location.hash === '#lamps') setTimeout(() => openPlugins(), 400); // deep link: abre el panel al cargar
|
package/public/js/schedules.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// (tareas a la izquierda; a la derecha: qué hace, corridas, log, traza en vivo mientras corre, ▶ / ⏻ / quitar;
|
|
3
3
|
// «+ programar» es una fila más cuyo detalle es el formulario). Corren en cualquier lampson abierto (web o
|
|
4
4
|
// terminal, tick cada 30 s) o con `lampson --daemon start` con todo cerrado.
|
|
5
|
-
let schedData = [],
|
|
5
|
+
let schedData = [], schedPlugins = [], schedTick = 30, schedMountOk = true, schedLiveTimer = null;
|
|
6
6
|
async function fetchSched() {
|
|
7
7
|
let r; try { r = await (await fetch(BASE + '/api/schedules')).json(); } catch (e) { return schedData; }
|
|
8
|
-
schedData = r.tasks || [];
|
|
8
|
+
schedData = r.tasks || []; schedPlugins = r.plugins || []; schedTick = r.tick || 30; schedMountOk = r.mount_ok !== false;
|
|
9
9
|
return schedData;
|
|
10
10
|
}
|
|
11
11
|
async function loadSched() {
|
|
@@ -28,7 +28,7 @@ async function loadSched() {
|
|
|
28
28
|
box.appendChild(d);
|
|
29
29
|
}
|
|
30
30
|
const addRow = document.createElement('div'); addRow.className = 'p'; addRow.innerHTML = `<span class="nm" style="color:var(--accent)">+ programar una tarea…</span>`;
|
|
31
|
-
addRow.title = '
|
|
31
|
+
addRow.title = 'un plugin, un comando o el agente con instrucciones · también se lo podés pedir al agente en el chat';
|
|
32
32
|
addRow.onclick = () => openSched('__new');
|
|
33
33
|
box.appendChild(addRow);
|
|
34
34
|
if (Panel.is('schedules')) Panel.refresh();
|
|
@@ -42,7 +42,7 @@ function openSched(selectId) {
|
|
|
42
42
|
browse: {
|
|
43
43
|
placeholder: 'buscar tarea…', listWidth: '300px', key: t => t.id,
|
|
44
44
|
load: async (q) => { const all = await fetchSched(); const list = all.filter(t => !q || (t.name + ' ' + t.plan + ' ' + t.action_text).toLowerCase().includes(q.toLowerCase())); return q ? list : [...list, SCHED_NEW]; },
|
|
45
|
-
render: (t) => t === SCHED_NEW ? `<span class="dot">+</span><div><div class="nm" style="color:var(--accent);font-weight:400">programar una tarea…</div><div class="meta">
|
|
45
|
+
render: (t) => t === SCHED_NEW ? `<span class="dot">+</span><div><div class="nm" style="color:var(--accent);font-weight:400">programar una tarea…</div><div class="meta">plugin, comando o el agente</div></div>` : `<span class="dot">${schedStatusDot(t)}</span><div><div class="nm">${esc(t.name)}</div><div class="meta">${esc(t.plan)} · ${t.running ? 'corriendo' : (t.enabled ? 'próxima ' + esc(fmtWhen(t.next_run)) : 'apagada')}</div></div>`,
|
|
46
46
|
count: (rows) => { const n = rows.filter(r => r !== SCHED_NEW).length; return `${n} tarea${n === 1 ? '' : 's'}`; },
|
|
47
47
|
emptyHtml: 'nada coincide',
|
|
48
48
|
detail: async (t) => {
|
|
@@ -78,12 +78,12 @@ function openSched(selectId) {
|
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
80
|
function schedForm() {
|
|
81
|
-
const
|
|
81
|
+
const pluginOpts = schedPlugins.filter(l => l.enabled).flatMap(l => (l.tools || []).map(tl => `<option value="${esc(l.name + '/' + tl)}">${esc(l.name)} · ${esc(tl)}</option>`)).join('') || '<option value="">ningún plugin encendido</option>';
|
|
82
82
|
return `<div class="dhead"><span class="nm serif">Programar una tarea</span></div><div class="dform">
|
|
83
83
|
<p class="lead">Corre sola, a la hora que digas, mientras lampson esté abierto (esta web o la terminal); con todo cerrado, <code>lampson --daemon start</code>. También podés pedírsela al agente en el chat: <i>«todos los días a las 9 corré los tests y avisame»</i>.</p>
|
|
84
84
|
<label>nombre <input name="name" spellcheck="false" autocomplete="off" placeholder="tests nocturnos"></label>
|
|
85
85
|
<label>cuándo <input name="when" spellcheck="false" autocomplete="off" placeholder="daily 09:00 · every 6h · mon,wed 08:30 · today 15:14 · once 2026-09-01 10:00 · in 2h"></label>
|
|
86
|
-
<label>qué corre <select name="kind"><option value="prompt">el agente, con estas instrucciones</option><option value="bash">un comando de shell</option><option value="
|
|
86
|
+
<label>qué corre <select name="kind"><option value="prompt">el agente, con estas instrucciones</option><option value="bash">un comando de shell</option><option value="plugin">una tool de un plugin encendido</option></select></label>
|
|
87
87
|
<div data-kind="prompt">
|
|
88
88
|
<label class="col">instrucciones <textarea name="prompt" rows="4" spellcheck="false" placeholder="Corré la suite de tests. Si algo falla, buscá la causa y proponé el fix en el informe (no lo apliques). Terminá con un resumen corto."></textarea></label>
|
|
89
89
|
<label>perfil <select name="agent"><option value="build">build — puede editar y correr comandos</option><option value="review">review — corre tests, no edita</option><option value="plan">plan — solo lee y propone</option><option value="explore">explore — solo busca</option></select></label>
|
|
@@ -91,8 +91,8 @@ function schedForm() {
|
|
|
91
91
|
<p class="lead">Sobre de permisos de una corrida sin nadie mirando. En «preguntar», la aprobación aparece acá y, con URL pública + webhook (⚙), como link en tu canal; sin respuesta en 2 h se deniega. Lo destructivo del sistema se bloquea siempre.</p>
|
|
92
92
|
</div>
|
|
93
93
|
<div data-kind="bash" style="display:none"><label>comando <input name="command" spellcheck="false" autocomplete="off" placeholder="npm test"></label></div>
|
|
94
|
-
<div data-kind="
|
|
95
|
-
<label>
|
|
94
|
+
<div data-kind="plugin" style="display:none">
|
|
95
|
+
<label>plugin · tool <select name="plugintool">${pluginOpts}</select></label>
|
|
96
96
|
<label>args (JSON) <input name="args" spellcheck="false" autocomplete="off" placeholder='{"who": "cron"}'></label>
|
|
97
97
|
</div>
|
|
98
98
|
<label>avisar a <input name="notify" spellcheck="false" autocomplete="off" placeholder="opcional: URL de webhook que recibe el resultado (JSON)"></label>
|
|
@@ -106,7 +106,7 @@ function schedFormWire(box, err) {
|
|
|
106
106
|
const k = f('kind').value; let action;
|
|
107
107
|
if (k === 'prompt') action = { type: 'prompt', prompt: f('prompt').value.trim(), agent: f('agent').value };
|
|
108
108
|
else if (k === 'bash') action = { type: 'bash', command: f('command').value.trim() };
|
|
109
|
-
else { const v = f('
|
|
109
|
+
else { const v = f('plugintool').value; if (!v) { err('encendé un plugin primero'); return; } const [plugin, tool] = v.split('/'); let args = {}; if (f('args').value.trim()) { try { args = JSON.parse(f('args').value); } catch (e) { err('args: JSON inválido'); return; } } action = { type: 'plugin', plugin, tool, args }; }
|
|
110
110
|
const body = { name: f('name').value.trim(), when: f('when').value.trim(), action, permission: f('permission').value, notify: f('notify').value.trim() };
|
|
111
111
|
if (!body.when) { err('falta cuándo'); return; }
|
|
112
112
|
err('programando…');
|
package/public/js/workspaces.js
CHANGED
|
@@ -50,7 +50,7 @@ function openWorkspaces(selectSlug) {
|
|
|
50
50
|
}
|
|
51
51
|
function wsNewForm() {
|
|
52
52
|
return `<div class="dhead"><span class="nm serif">Nuevo workspace</span></div><div class="dform">
|
|
53
|
-
<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
|
|
53
|
+
<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 plugins.</p>
|
|
54
54
|
<div class="pickrow"><button class="primary" data-pick>Elegir carpeta…</button><span class="ds">se abre el diálogo de carpetas de tu sistema · <a href="#" data-browse>o explorá desde acá</a></span></div>
|
|
55
55
|
<div class="chosen" data-chosen style="display:none"><span class="lk">carpeta</span><code data-chosen-path></code><span class="del" data-clear title="elegir otra">✕</span></div>
|
|
56
56
|
<div data-browser class="wsbrowser" style="display:none">
|
package/skills/lampson/SKILL.md
CHANGED
|
@@ -43,7 +43,7 @@ description: How this harness works — tools, workspace mount, permissions, age
|
|
|
43
43
|
## Scheduled tasks (`schedule` tool)
|
|
44
44
|
- When the user says "every day at 9", "each 6 hours", "on Mondays", "periodically", "send me", propose
|
|
45
45
|
`schedule(action=add, name, at, kind, …)`: `at` = `every 6h` | `daily 09:00` | `mon,wed 08:30` |
|
|
46
|
-
`weekdays 09:00`; one-time: `today 15:14` | `tomorrow 09:00` | `once 2026-08-29 15:14` | `in 2h` (it turns itself off after running) — the USER'S LOCAL time: write the hour as they say it, never convert to UTC (the tool result shows the next run with its offset). Tasks belong to the current workspace. `kind=
|
|
46
|
+
`weekdays 09:00`; one-time: `today 15:14` | `tomorrow 09:00` | `once 2026-08-29 15:14` | `in 2h` (it turns itself off after running) — the USER'S LOCAL time: write the hour as they say it, never convert to UTC (the tool result shows the next run with its offset). Tasks belong to the current workspace. `kind=plugin` (a plugin that is ON: plugin + tool + args), `kind=bash` (one
|
|
47
47
|
command that finishes on its own), `kind=prompt` (an unattended agent run: write a self-contained prompt —
|
|
48
48
|
what to do, how to verify, what to report — and pick `agent` build/review/plan/explore).
|
|
49
49
|
- `permission` is the envelope of a `prompt` run with nobody watching: `strict` (dangerous → denied),
|
|
@@ -95,6 +95,10 @@ the terminal button and run `npm run dev`), then tell me the URL".
|
|
|
95
95
|
## Project memory (persistent notes)
|
|
96
96
|
- `memory(write, name, content)` saves a Markdown note about THIS project in Lampson's `memory/<project>/`
|
|
97
97
|
folder (outside the repo). The system prompt lists your notes; `memory(read, name)` loads one.
|
|
98
|
+
- That folder is OUTSIDE the workspace and outside the reach of read/ls/grep/bash (they are confined to the
|
|
99
|
+
project): the `memory` tool is the only door. Never look for it with `ls`, never write notes with bash, and
|
|
100
|
+
never store notes inside the repo (no `.lampson/notes/`). If the tool says the folder is unreachable, tell
|
|
101
|
+
the user to run `lampson` again in the project (it repairs the link) — do not improvise.
|
|
98
102
|
- Save what you would otherwise rediscover: how to run/test, env quirks, decisions, where things live,
|
|
99
103
|
root causes of bugs. Update notes instead of contradicting them. The user can read and edit them.
|
|
100
104
|
|
|
@@ -124,15 +128,18 @@ the terminal button and run `npm run dev`), then tell me the URL".
|
|
|
124
128
|
(`workspace/.agents/skills`, `workspace/.claude/skills`, and the global `~/.agents/skills` / `~/.claude/skills`
|
|
125
129
|
mounted as `.lampson/skills-global` / `.lampson/skills-claude`). Frontmatter `name:` + `description:`.
|
|
126
130
|
`skill(action=install, source=owner/repo, name=x, scope=global|project)` installs one (always asks the user).
|
|
127
|
-
- **
|
|
128
|
-
|
|
131
|
+
- **Plugins** = tools you can create for this project without touching the harness (they were called
|
|
132
|
+
"lamps" until 0.2.6 — the user may still say "lámpara"; a `.lampson/lamps/` folder still works):
|
|
133
|
+
`plugin(action=create, name, manifest, files={"plugin.syn": "…"})` writes `workspace/.lampson/plugins/<name>/`,
|
|
129
134
|
validates the manifest and runs `synsema check` on a syn entry (it does not run or enable anything —
|
|
130
|
-
like dsh's cordis_define). Then `
|
|
135
|
+
like dsh's cordis_define). Then `plugin(action=enable, name)` — the user must approve (a plugin is off until
|
|
131
136
|
a human turns it on). Build one when a task needs a reusable project-specific tool. Manifest:
|
|
132
|
-
`{"name", "description", "kind": "syn"|"exec", "entry": "
|
|
133
|
-
"caps": "file.read=workspace/*" (syn, optional extra ceiling over stdout,time,env=
|
|
134
|
-
"tools": [{"name", "description", "parameters": {JSON Schema}, "readonly": bool}]}`.
|
|
135
|
-
digits, `-` (no `_`). Each call runs the
|
|
136
|
-
for `syn`, the command for `exec`. Inside, read `
|
|
137
|
-
(`require env("
|
|
138
|
-
manifest's `caps`; ask only for what the tool needs. Once on, its tools are `
|
|
137
|
+
`{"name", "description", "kind": "syn"|"exec", "entry": "plugin.syn" (syn) | "command": "python plugin.py" (exec),
|
|
138
|
+
"caps": "file.read=workspace/*" (syn, optional extra ceiling over stdout,time,env=PLUGIN_*), "timeout": 60,
|
|
139
|
+
"tools": [{"name", "description", "parameters": {JSON Schema}, "readonly": bool}]}`. Plugin names: letters,
|
|
140
|
+
digits, `-` (no `_`). Each call runs the plugin as ONE child process: `synsema run --cap-set <ceiling> entry`
|
|
141
|
+
for `syn`, the command for `exec`. Inside, read `PLUGIN_TOOL` and `PLUGIN_ARGS` (JSON) from env
|
|
142
|
+
(`require env("PLUGIN_*")` in a .syn) and print the result to stdout. A syn plugin cannot use more than its
|
|
143
|
+
manifest's `caps`; ask only for what the tool needs. Once on, its tools are `plugin_<name>_<tool>`.
|
|
144
|
+
Not the same thing as a **lamp** from lamps.sh (a portable, ceiling-enforced capability unit for any MCP
|
|
145
|
+
agent): if the user wants one of those, it is `lamp add <ref>` + `lamp mcp` as an MCP server, not a plugin.
|
package/web.syn
CHANGED
|
@@ -34,8 +34,8 @@ require file("workspace")
|
|
|
34
34
|
require file("workspace/*")
|
|
35
35
|
require file.read("skills")
|
|
36
36
|
require file.read("skills/*")
|
|
37
|
-
require file.read("
|
|
38
|
-
require file.read("
|
|
37
|
+
require file.read("plugins")
|
|
38
|
+
require file.read("plugins/*")
|
|
39
39
|
require file("memory")
|
|
40
40
|
require file("memory/*")
|
|
41
41
|
require file(".lampson")
|
|
@@ -57,7 +57,7 @@ use "./lib/update.syn" as update
|
|
|
57
57
|
use "./lib/settings.syn" as settings
|
|
58
58
|
use "./lib/trace.syn" as trace
|
|
59
59
|
use "./lib/mcp.syn" as mcp
|
|
60
|
-
use "./lib/
|
|
60
|
+
use "./lib/plugins.syn" as plugins
|
|
61
61
|
use "./lib/lsp.syn" as lsp
|
|
62
62
|
use "./lib/tools/todo.syn" as todo
|
|
63
63
|
use "./lib/schedule.syn" as schedule
|
|
@@ -105,7 +105,7 @@ task lampson_subagent(spec_json)
|
|
|
105
105
|
-- TAREAS PROGRAMADAS (lib/schedule.syn): este proceso es el residente de lampson — con `lampson --daemon start`
|
|
106
106
|
-- (synsema daemon) queda en background y ejecuta lo programado aunque no haya ninguna UI abierta. Un tick cada
|
|
107
107
|
-- 30 s (cron_every de Synsema: intervalo puro; la hora de reloj la resuelve schedule.syn) corre lo vencido:
|
|
108
|
-
--
|
|
108
|
+
-- plugins, comandos y corridas del agente con su sobre de permisos; una aprobación en una corrida desatendida
|
|
109
109
|
-- sale por lib/approvals.syn (panel «Aprobaciones» de la UI, webhook firmado, link de un solo uso).
|
|
110
110
|
task sched_tick()
|
|
111
111
|
give sched_run.tick()
|
|
@@ -261,7 +261,8 @@ serve on 8080
|
|
|
261
261
|
give git.status()
|
|
262
262
|
|
|
263
263
|
route "GET /w/:slug/api/memory" requires auth
|
|
264
|
-
|
|
264
|
+
-- ok=false: el link memory/ del workspace está roto (la UI lo dice; el agente ya lo sabe por el prompt)
|
|
265
|
+
give {"dir": memo.dir(), "ok": memo.available(), "notes": memo.list()}
|
|
265
266
|
|
|
266
267
|
route "GET /w/:slug/api/memory/note" requires auth
|
|
267
268
|
when not contains(query, "name")
|
|
@@ -334,34 +335,34 @@ serve on 8080
|
|
|
334
335
|
recover err
|
|
335
336
|
give fail(400, text(err))
|
|
336
337
|
|
|
337
|
-
--
|
|
338
|
-
route "GET /w/:slug/api/
|
|
339
|
-
give {"
|
|
338
|
+
-- plugins (tools propias): listar y encender/apagar desde la barra superior de la UI
|
|
339
|
+
route "GET /w/:slug/api/plugins" requires auth
|
|
340
|
+
give {"plugins": plugins.summary(), "global": plugins.GLOBAL_DIR, "project": plugins.PROJECT_DIR}
|
|
340
341
|
|
|
341
|
-
-- el usuario corre una tool de
|
|
342
|
-
route "POST /w/:slug/api/
|
|
342
|
+
-- el usuario corre una tool de un plugin encendido él mismo (sin pasar por el modelo)
|
|
343
|
+
route "POST /w/:slug/api/plugins/run" requires auth
|
|
343
344
|
expect body {tool: text}
|
|
344
345
|
let b be json of request
|
|
345
346
|
let args be when contains(b, "args") then b["args"] otherwise {}
|
|
346
347
|
try
|
|
347
|
-
give {"ok": true, "output":
|
|
348
|
+
give {"ok": true, "output": plugins.call(text(b["tool"]), args)}
|
|
348
349
|
recover err
|
|
349
350
|
give fail(400, text(err))
|
|
350
351
|
|
|
351
|
-
route "POST /w/:slug/api/
|
|
352
|
+
route "POST /w/:slug/api/plugins/remove" requires auth
|
|
352
353
|
expect body {name: text}
|
|
353
354
|
let b be json of request
|
|
354
355
|
try
|
|
355
|
-
give {"ok": true, "result":
|
|
356
|
+
give {"ok": true, "result": plugins.remove(text(b["name"])), "plugins": plugins.summary()}
|
|
356
357
|
recover err
|
|
357
358
|
give fail(400, text(err))
|
|
358
359
|
|
|
359
|
-
route "POST /w/:slug/api/
|
|
360
|
+
route "POST /w/:slug/api/plugins/toggle" requires auth
|
|
360
361
|
expect body {name: text}
|
|
361
362
|
let b be json of request
|
|
362
363
|
let on be contains(b, "enabled") and b["enabled"] == true
|
|
363
364
|
try
|
|
364
|
-
give {"ok": true, "result":
|
|
365
|
+
give {"ok": true, "result": plugins.set_enabled(text(b["name"]), on), "plugins": plugins.summary()}
|
|
365
366
|
recover err
|
|
366
367
|
give fail(400, text(err))
|
|
367
368
|
|
|
@@ -478,7 +479,7 @@ serve on 8080
|
|
|
478
479
|
route "GET /w/:slug/api/schedules" requires auth
|
|
479
480
|
let age be schedule.daemon_age()
|
|
480
481
|
-- este proceso tiene su propio tick (cron_every): las tareas corren mientras la web esté abierta
|
|
481
|
-
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(), "
|
|
482
|
+
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(), "plugins": plugins.summary()}
|
|
482
483
|
|
|
483
484
|
route "POST /w/:slug/api/schedules/add" requires auth
|
|
484
485
|
let b be json of request
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
-- lamps/example-hello/lamp.syn — la lámpara de ejemplo
|
|
2
|
-
--
|
|
3
|
-
-- Lampson la corre así por cada llamada: synsema run --cap-set stdout,time,env=LAMP_* lamp.syn
|
|
4
|
-
-- El techo (--cap-set) sale del manifiesto (lamp.json → "caps") que el humano aprobó al encenderla;
|
|
5
|
-
-- pedir más acá (p. ej. `require net`) falla con "above the host ceiling".
|
|
6
|
-
-- Entrada por env: LAMP_TOOL (qué tool), LAMP_ARGS (sus args en JSON), LAMP_DIR, LAMP_WORKSPACE.
|
|
7
|
-
-- Salida: lo que imprimas por stdout vuelve al modelo como resultado de la tool.
|
|
8
|
-
intent: "example lamp for lampson: greet"
|
|
9
|
-
|
|
10
|
-
require env("LAMP_*")
|
|
11
|
-
|
|
12
|
-
let tool be env("LAMP_TOOL", "")
|
|
13
|
-
let args be json_decode(env("LAMP_ARGS", "{}"))
|
|
14
|
-
|
|
15
|
-
when tool == "greet"
|
|
16
|
-
let who be when contains(args, "who") then text(args["who"]) otherwise "world"
|
|
17
|
-
print("hello, " + who + "! (from the example-hello lamp, running under a capability ceiling)")
|
|
18
|
-
otherwise
|
|
19
|
-
print("ERROR: unknown tool '" + tool + "'")
|