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.
@@ -1,20 +1,20 @@
1
- // lamps.js — lámparas (plugins de tools): pill en la cabecera + vista browse del Panel (lista, detalle con
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 lampsData = [], lampsMeta = { global: 'lamps/', project: '.lampson/lamps/' };
4
- async function fetchLamps() {
5
- let r; try { r = await (await fetch(BASE + '/api/lamps')).json(); } catch (e) { return lampsData; }
6
- lampsData = r.lamps || []; lampsMeta = { global: r.global || 'lamps/', project: r.project || '.lampson/lamps/' };
7
- const on = lampsData.filter(l => l.enabled).length;
8
- const pill = $('#lamps'); pill.textContent = lampsData.length ? `lámparas ${on}/${lampsData.length}` : 'lámparas'; pill.classList.toggle('on', on > 0);
9
- return lampsData;
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 loadLamps() { await fetchLamps(); if (Panel.is('lamps')) Panel.refresh(); }
12
- function lampMatches(l, q) {
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 lampField(k, p, required) {
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 lampArgs(tdiv, props, required) {
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 LAMPS_HELP = `<h4>Herramientas hechas a medida para este proyecto</h4>
40
- <p>Una lámpara es una tool que el agente (o vos) escribe para <em>este</em> repo: una carpeta con <code>lamp.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 la ve en su catálogo con parámetros tipados, vos la podés correr desde acá, y queda en el repo para la próxima sesión.</p>
41
- <p><b>Pedila en el chat</b> — <i>«hacé una lámpara que liste los endpoints del backend»</i>. El agente la escribe en <code data-project></code>, la valida y te pide encenderla acá.</p>
42
- <p><b>Encendida = existe</b> — apagada, el agente ni la ve. Al encenderla, sus tools entran al catálogo como <code>lamp_&lt;lámpara&gt;_&lt;tool&gt;</code> desde el próximo turno. Las globales viven en <code data-global></code> y valen para todos los proyectos.</p>
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_&lt;plugin&gt;_&lt;tool&gt;</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><button class="primary" data-back>← volver a las lámparas</button></p>`;
52
- function openLamps(selectName) {
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: 'lamps', eyebrow: 'lámparas', title: 'Lámparas', layout: 'browse',
56
- headActions: [{ label: '?', title: '¿qué es una lámpara?', onClick: (b) => { Panel.help(true); const h = b.querySelector('.phelp'); h.querySelector('[data-project]').textContent = lampsMeta.project; h.querySelector('[data-global]').textContent = lampsMeta.global; h.querySelector('[data-back]').onclick = () => Panel.help(false); } }],
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 lámpara o tool…', listWidth: '260px', help: LAMPS_HELP,
59
+ placeholder: 'buscar plugin o tool…', listWidth: '260px', help: PLUGINS_HELP,
59
60
  key: l => l.name,
60
- load: async (q) => { const all = await fetchLamps(); const list = all.filter(l => lampMatches(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; },
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 ? ' · rota' : ''}</div></div>`,
62
- count: (rows, q) => q ? `${rows.length} de ${lampsData.length}` : `${lampsData.length} lámpara${lampsData.length === 1 ? '' : 's'}`,
63
- emptyHtml: 'ninguna todavía',
64
- emptyDetail: `Todavía no hay lámparas.<br><br>Pedile una al agente en el chat — <i>«hacé una lámpara que liste los endpoints del backend»</i> — o creá una carpeta con <code>lamp.json</code> en <code>${esc(lampsMeta.project)}</code> (proyecto) o <code>${esc(lampsMeta.global)}</code> (global). El botón <b>?</b> de arriba explica qué son y para qué sirven.`,
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">rota: ${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>`);
68
- 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á rota' : (l.enabled ? 'apagar' : 'encender')}"><input type="checkbox" ${l.enabled ? 'checked' : ''} ${l.error ? 'disabled' : ''}>${l.enabled ? 'encendida' : 'apagada'}</label></div>`
69
- + (l.description ? `<div class="ddesc">${esc(l.description)}</div>` : '') + cap
70
- + (l.enabled || l.error ? '' : `<div class="dnote">Apagada: el agente no la ve y no se puede correr. Encendela 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>`)
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">lamp_${esc(l.name)}_${esc(t.name)}</span></div>`
74
- + (Object.keys(props).length ? Object.keys(props).map(k => lampField(k, props[k], required)).join('') : '<div class="ds" style="margin-top:6px;color:var(--ink-3)">no toma parámetros</div>')
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="las globales se borran a mano">global · se borra a mano</span>'}</div><div class="err" data-err></div>`;
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/lamps/toggle', { name: l.name, enabled: ev.target.checked });
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 || '')); loadLamps(); loadSched();
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 = lampArgs(tdiv, props, required);
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/lamps/run', { tool: 'lamp_' + l.name + '_' + t.name, args: got.args });
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/lamps/remove', { name: l.name });
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 || '')); loadLamps();
107
+ add('meta', '☼ ' + esc(r.data.result || '')); loadPlugins();
106
108
  });
107
109
  }
108
110
  }
109
111
  });
110
112
  }
111
- $('#lamps').onclick = () => openLamps();
112
- if (location.hash === '#lamps') setTimeout(() => openLamps(), 400); // deep link: abre el panel al cargar
113
+ $('#plugins').onclick = () => openPlugins();
114
+ if (location.hash === '#plugins' || location.hash === '#lamps') setTimeout(() => openPlugins(), 400); // deep link: abre el panel al cargar
@@ -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 = [], schedLamps = [], schedTick = 30, schedMountOk = true, schedLiveTimer = null;
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 || []; schedLamps = r.lamps || []; schedTick = r.tick || 30; schedMountOk = r.mount_ok !== false;
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 = 'una lámpara, un comando o el agente con instrucciones · también se lo podés pedir al agente en el chat';
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">lámpara, 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>`,
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 lampOpts = schedLamps.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="">ninguna lámpara encendida</option>';
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="lamp">una tool de una lámpara encendida</option></select></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="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="lamp" style="display:none">
95
- <label>lámpara · tool <select name="lamptool">${lampOpts}</select></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('lamptool').value; if (!v) { err('encendé una lámpara primero'); return; } const [lamp, 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: 'lamp', lamp, tool, args }; }
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…');
@@ -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 lámparas.</p>
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">
@@ -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=lamp` (a lamp that is ON: lamp + tool + args), `kind=bash` (one
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
- - **Lamps** = tool plugins you can create for this project without touching the harness:
128
- `lamp(action=create, name, manifest, files={"lamp.syn": ""})` writes `workspace/.lampson/lamps/<name>/`,
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 `lamp(action=enable, name)` — the user must approve (a lamp is off until
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": "lamp.syn" (syn) | "command": "python lamp.py" (exec),
133
- "caps": "file.read=workspace/*" (syn, optional extra ceiling over stdout,time,env=LAMP_*), "timeout": 60,
134
- "tools": [{"name", "description", "parameters": {JSON Schema}, "readonly": bool}]}`. Lamp names: letters,
135
- digits, `-` (no `_`). Each call runs the lamp as ONE child process: `synsema run --cap-set <ceiling> entry`
136
- for `syn`, the command for `exec`. Inside, read `LAMP_TOOL` and `LAMP_ARGS` (JSON) from env
137
- (`require env("LAMP_*")` in a .syn) and print the result to stdout. A syn lamp cannot use more than its
138
- manifest's `caps`; ask only for what the tool needs. Once on, its tools are `lamp_<name>_<tool>`.
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("lamps")
38
- require file.read("lamps/*")
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/lamps.syn" as lamps
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
- -- lámparas, comandos y corridas del agente con su sobre de permisos; una aprobación en una corrida desatendida
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
- give {"dir": memo.dir(), "notes": memo.list()}
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
- -- lámparas (plugins de tools): listar y encender/apagar desde la barra superior de la UI
338
- route "GET /w/:slug/api/lamps" requires auth
339
- give {"lamps": lamps.summary(), "global": lamps.GLOBAL_DIR, "project": lamps.PROJECT_DIR}
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 una lámpara encendida él mismo (sin pasar por el modelo)
342
- route "POST /w/:slug/api/lamps/run" requires auth
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": lamps.call(text(b["tool"]), args)}
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/lamps/remove" requires auth
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": lamps.remove(text(b["name"])), "lamps": lamps.summary()}
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/lamps/toggle" requires auth
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": lamps.set_enabled(text(b["name"]), on), "lamps": lamps.summary()}
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(), "lamps": lamps.summary()}
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 + "'")