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.
@@ -7,9 +7,22 @@
7
7
  -- El system prompt lleva SOLO el índice (nombre + primera línea de cada nota); el contenido entra al
8
8
  -- contexto cuando el agente llama `memory(read)`. Son archivos: el humano los lee/edita a mano y la
9
9
  -- UI web los muestra.
10
+ --
11
+ -- Cómo llega el proceso hasta ahí: el cwd del workspace es .lampson/ws/<slug>/, y `memory` es un link
12
+ -- (junction/symlink) a <home>/memory. La capability file("memory/*") es léxica sobre ese link, así que ESTA
13
+ -- tool es la única puerta: read/ls/grep/bash están confinadas a workspace/ y no ven la carpeta. Si el link
14
+ -- está roto (destino inexistente), write_file falla con "No such file or directory": `available()` lo
15
+ -- detecta, el prompt lo dice, y los errores de escritura explican qué hacer (correr `lampson` de nuevo
16
+ -- repara los links — workspaces.ensure_link) en vez de dejar que el modelo improvise notas con bash.
10
17
  use "./common.syn" as c
11
18
 
12
19
  export let ROOT be "memory"
20
+ let HOW_TO_FIX be "The memory folder is a link to Lampson's install (outside the project) and it is not reachable from this workspace — probably a broken link. Ask the user to run `lampson` again in this project (it repairs the links). Do NOT write notes with bash or inside the repo instead."
21
+
22
+ -- ¿se puede llegar a la carpeta de memoria? (false = link roto o inexistente)
23
+ export task available()
24
+ require file.read("memory")
25
+ give file_exists(ROOT)
13
26
 
14
27
  task valid_name(name)
15
28
  when name == nothing or name == ""
@@ -75,9 +88,16 @@ export task note_write(name, content)
75
88
  require file("memory/*")
76
89
  when not valid_name(name)
77
90
  raise("invalid note name '" + text(name) + "' (letters, digits, - or _)")
78
- write_file(path(name), content)
91
+ save(name, content)
79
92
  give "saved memory/" + slug() + "/" + name + ".md (" + text(length(content)) + " chars)"
80
93
 
94
+ -- escribe una nota; un fallo del sistema de archivos se explica (link roto) en vez de salir crudo
95
+ task save(name, content)
96
+ try
97
+ write_file(path(name), content)
98
+ recover err
99
+ raise("cannot write " + path(name) + ": " + text(err) + ". " + HOW_TO_FIX)
100
+
81
101
  export task note_append(name, content)
82
102
  require env("LAMPSON_*")
83
103
  require file("memory")
@@ -90,7 +110,7 @@ export task note_append(name, content)
90
110
  recover err
91
111
  set prev to ""
92
112
  let joined be when prev == "" then content otherwise prev + "\n\n" + content
93
- write_file(path(name), joined)
113
+ save(name, joined)
94
114
  give "appended to " + name + ".md (" + text(length(joined)) + " chars total)"
95
115
 
96
116
  export task note_clear(name)
@@ -99,7 +119,7 @@ export task note_clear(name)
99
119
  require file("memory/*")
100
120
  when not valid_name(name)
101
121
  raise("invalid note name")
102
- write_file(path(name), "")
122
+ save(name, "")
103
123
  give "cleared " + name + ".md (the file stays empty; the user can delete it)"
104
124
 
105
125
  -- sección del system prompt
@@ -107,8 +127,10 @@ export task prompt_section()
107
127
  require env("LAMPSON_*")
108
128
  require file.read("memory")
109
129
  require file.read("memory/*")
130
+ let head be "\n\n# Project memory (your own notes about THIS project, in memory/" + slug() + "/ — a folder OUTSIDE the project, reachable ONLY through the memory tool: read/ls/grep/bash are confined to the workspace and will not find it. Read with memory(read), keep notes current with memory(write|append).)"
131
+ when not available()
132
+ give head + "\n(UNAVAILABLE right now: the memory folder is not reachable from this workspace — a broken link in Lampson's install. Do not look for it with ls/bash and do not write notes inside the repo; tell the user to run `lampson` again in this project to repair it.)"
110
133
  let items be list()
111
- let head be "\n\n# Project memory (your own notes about THIS project, in memory/" + slug() + "/ — read with memory(read), keep them current with memory(write|append))"
112
134
  when length(items) == 0
113
135
  give head + "\n(empty — when you discover something non-obvious about this project: how to run/test it, gotchas, decisions, where things live — save it with memory(write). Keep notes short and factual.)"
114
136
  let lines be [head]
@@ -139,7 +161,7 @@ export task tool(action, name, content)
139
161
 
140
162
  export let SPEC be {
141
163
  "name": "memory",
142
- "description": "Your persistent notes about THIS project, kept across sessions (Markdown files in Lampson's memory folder, never inside the repo). Use `write` to save non-obvious facts you discovered and will need again: how to run/build/test, environment quirks, architecture decisions, where things live, bugs and their causes, what the user prefers. `append` adds to an existing note, `read` loads one, `list` shows them, `delete` clears one. The system prompt shows the index of notes — read the relevant ones before repeating an investigation. Keep notes short, factual and current: update a note instead of writing a contradictory one.",
164
+ "description": "Your persistent notes about THIS project, kept across sessions (Markdown files in Lampson's memory folder: OUTSIDE the project and outside the reach of your other tools — read/ls/grep/bash are confined to the workspace, so this tool is the ONLY way to those notes; never keep notes with bash or inside the repo). Use `write` to save non-obvious facts you discovered and will need again: how to run/build/test, environment quirks, architecture decisions, where things live, bugs and their causes, what the user prefers. `append` adds to an existing note, `read` loads one, `list` shows them, `delete` clears one. The system prompt shows the index of notes — read the relevant ones before repeating an investigation. Keep notes short, factual and current: update a note instead of writing a contradictory one.",
143
165
  "parameters": {"type": "object", "properties": {
144
166
  "action": {"type": "string", "enum": ["list", "read", "write", "append", "delete"]},
145
167
  "name": {"type": "string", "description": "Note id: letters, digits, - or _ (e.g. how-to-run, db-schema, gotchas)"},
package/lib/tools.syn CHANGED
@@ -25,7 +25,7 @@ use "./tools/memo.syn" as t_memo
25
25
  use "./tools/todo.syn" as t_todo
26
26
  use "./skills.syn" as skills
27
27
  use "./mcp.syn" as mcp
28
- use "./lamps.syn" as lamps
28
+ use "./plugins.syn" as plugins
29
29
  use "./lsp.syn" as lsp
30
30
  use "./schedule.syn" as schedule
31
31
 
@@ -86,40 +86,40 @@ let MCP_SPEC be {
86
86
  }, "required": ["action"]}
87
87
  }
88
88
 
89
- -- la task de la tool lamp (lamps.syn vive un nivel arriba de tools/, como mcp). enable/disable piden humano
89
+ -- la task de la tool plugin (plugins.syn vive un nivel arriba de tools/, como mcp). enable/disable piden humano
90
90
  -- SIEMPRE (permission.syn): encender = autorizar código a correr con las capacidades de su manifiesto.
91
- task lamp_tool(action, name, manifest, files)
91
+ task plugin_tool(action, name, manifest, files)
92
92
  require exec
93
93
  require time
94
94
  require env("LAMPSON_*")
95
95
  require file(".lampson")
96
96
  require file(".lampson/*")
97
- require file.read("lamps")
98
- require file.read("lamps/*")
97
+ require file.read("plugins")
98
+ require file.read("plugins/*")
99
99
  require file("workspace")
100
100
  require file("workspace/*")
101
101
  when action == "create"
102
- give lamps.create(name, manifest, files)
102
+ give plugins.create(name, manifest, files)
103
103
  when action == "enable"
104
- give lamps.set_enabled(name, true)
104
+ give plugins.set_enabled(name, true)
105
105
  when action == "disable"
106
- give lamps.set_enabled(name, false)
107
- let sm be lamps.summary()
106
+ give plugins.set_enabled(name, false)
107
+ let sm be plugins.summary()
108
108
  when length(sm) == 0
109
- give "no lamps found. A lamp is a folder with a lamp.json manifest: " + lamps.GLOBAL_DIR + "/<name>/ (global) or " + lamps.PROJECT_DIR + "/<name>/ (this project; you can create it with write). See the lampson skill for the manifest format."
109
+ give "no plugins found. A plugin is a folder with a plugin.json manifest: " + plugins.GLOBAL_DIR + "/<name>/ (global) or " + plugins.PROJECT_DIR + "/<name>/ (this project; you can create it with write). See the lampson skill for the manifest format."
110
110
  let lines be []
111
111
  each l in sm
112
- set lines to append(lines, l["name"] + " (" + l["scope"] + ", " + l["kind"] + ", " + (when l["enabled"] then "ON" otherwise "off") + "): " + (when l["error"] != nothing then "BROKEN — " + text(l["error"]) otherwise text(length(l["tools"])) + " tools [" + join(l["tools"], ", ") + "]" + (when l["description"] != "" then " — " + l["description"] otherwise "")))
112
+ set lines to append(lines, l["name"] + " (" + l["scope"] + ", " + l["kind"] + ", " + (when l["enabled"] then "ON" otherwise "off") + "): " + (when l["error"] != nothing then "BROKEN — " + text(l["error"]) otherwise text(length(l["tools"])) + " tools [" + join(l["tools"], ", ") + "]" + (when l["description"] != "" then " — " + l["description"] otherwise "")) + (when l["legacy"] then " [" + plugins.LEGACY_HINT + "]" otherwise ""))
113
113
  give join(lines, "\n")
114
114
 
115
- let LAMP_SPEC be {
116
- "name": "lamp",
117
- "description": "Lamps are tool plugins you can build for this project: a folder with a lamp.json manifest plus code — a Synsema program (kind=syn) run under a capability ceiling, or any executable (kind=exec). Consider one when a task needs a reusable custom tool (a project-specific query, generator, checker) that plain bash would repeat clumsily. action=list (default): every lamp found, on/off, its tools. action=create: write a PROJECT lamp (name + manifest + files) — it validates the manifest and runs `synsema check` on a syn entry, but does NOT run or enable it. action=enable / disable: turn one on or off — ALWAYS asks the user (enabling authorizes its code to run with the manifest's capabilities). An enabled lamp's tools appear in your catalog as lamp_<lamp>_<tool> from the NEXT turn. Manifest: {\"description\", \"kind\": \"syn\"|\"exec\", \"entry\": \"lamp.syn\" (syn) | \"command\": \"python lamp.py\" (exec), \"caps\": \"file.read=workspace/*\" (syn, optional extra ceiling over stdout,time,env=LAMP_*), \"timeout\": 60, \"tools\": [{\"name\", \"description\", \"parameters\": JSON Schema, \"readonly\": bool}]}. Inside the code read LAMP_TOOL and LAMP_ARGS (JSON) from env and print the result to stdout (a .syn needs `require env(\"LAMP_*\")`). Lamp names: letters, digits, - (no _).",
115
+ let PLUGIN_SPEC be {
116
+ "name": "plugin",
117
+ "description": "Plugins are tools you can build for this project (they were called 'lamps' until 0.2.6; the user may still say lamp/lámpara): a folder with a plugin.json manifest plus code — a Synsema program (kind=syn) run under a capability ceiling, or any executable (kind=exec, any language, no ceiling). Consider one when a task needs a reusable custom tool (a project-specific query, generator, checker) that plain bash would repeat clumsily. action=list (default): every plugin found, on/off, its tools. action=create: write a PROJECT plugin (name + manifest + files) — it validates the manifest and runs `synsema check` on a syn entry, but does NOT run or enable it. action=enable / disable: turn one on or off — ALWAYS asks the user (enabling authorizes its code to run with the manifest's capabilities). An enabled plugin's tools appear in your catalog as plugin_<plugin>_<tool> from the NEXT turn. Manifest: {\"description\", \"kind\": \"syn\"|\"exec\", \"entry\": \"plugin.syn\" (syn) | \"command\": \"python plugin.py\" (exec), \"caps\": \"file.read=workspace/*\" (syn, optional extra ceiling over stdout,time,env=PLUGIN_*), \"timeout\": 60, \"tools\": [{\"name\", \"description\", \"parameters\": JSON Schema, \"readonly\": bool}]}. Inside the code read PLUGIN_TOOL and PLUGIN_ARGS (JSON) from env and print the result to stdout (a .syn needs `require env(\"PLUGIN_*\")`). Plugin names: letters, digits, - (no _). Not the same as a lamp from lamps.sh (a portable, ceiling-enforced capability unit for any MCP agent): those are installed with `lamp add` and reach you as MCP tools via `lamp mcp`.",
118
118
  "parameters": {"type": "object", "properties": {
119
119
  "action": {"type": "string", "enum": ["list", "create", "enable", "disable"], "description": "Default: list"},
120
- "name": {"type": "string", "description": "create/enable/disable: lamp name"},
121
- "manifest": {"type": "object", "description": "create only: the lamp.json content (name is filled in)"},
122
- "files": {"type": "object", "description": "create only: {\"lamp.syn\": \"<code>\", …} — files written into the lamp folder", "additionalProperties": {"type": "string"}}
120
+ "name": {"type": "string", "description": "create/enable/disable: plugin name"},
121
+ "manifest": {"type": "object", "description": "create only: the plugin.json content (name is filled in)"},
122
+ "files": {"type": "object", "description": "create only: {\"plugin.syn\": \"<code>\", …} — files written into the plugin folder", "additionalProperties": {"type": "string"}}
123
123
  }, "required": ["action"]}
124
124
  }
125
125
 
@@ -158,8 +158,8 @@ let LSP_SPEC be {
158
158
 
159
159
  -- la tool schedule (lib/schedule.syn): tareas programadas. add/remove/enable/run piden humano SIEMPRE (permission.syn):
160
160
  -- crear una tarea = autorizar de una vez todo lo que va a hacer sin nadie mirando. Las corridas las hace el daemon
161
- -- (web.syn); desde acá solo se corren en el acto las lamp/bash (una prompt anidaría un loop dentro del turno).
162
- task schedule_tool(action, id, name, at, kind, lamp, tool, args, command, prompt, agent, permission, approval_timeout, notify)
161
+ -- (web.syn); desde acá solo se corren en el acto las plugin/bash (una prompt anidaría un loop dentro del turno).
162
+ task schedule_tool(action, id, name, at, kind, plugin, tool, args, command, prompt, agent, permission, approval_timeout, notify)
163
163
  require exec
164
164
  require time
165
165
  require net
@@ -167,15 +167,15 @@ task schedule_tool(action, id, name, at, kind, lamp, tool, args, command, prompt
167
167
  require env("OS")
168
168
  require file(".lampson")
169
169
  require file(".lampson/*")
170
- require file.read("lamps")
171
- require file.read("lamps/*")
170
+ require file.read("plugins")
171
+ require file.read("plugins/*")
172
172
  require file("workspace")
173
173
  require file("workspace/*")
174
174
  let act be when action == nothing then "list" otherwise action
175
175
  when act == "add"
176
176
  let a be {"type": kind}
177
- when kind == "lamp"
178
- set a to {"type": "lamp", "lamp": lamp, "tool": tool, "args": when args == nothing then {} otherwise args}
177
+ when kind == "plugin"
178
+ set a to {"type": "plugin", "plugin": plugin, "tool": tool, "args": when args == nothing then {} otherwise args}
179
179
  otherwise when kind == "bash"
180
180
  set a to {"type": "bash", "command": command}
181
181
  otherwise when kind == "prompt"
@@ -227,16 +227,16 @@ task daemon_note()
227
227
 
228
228
  let SCHEDULE_SPEC be {
229
229
  "name": "schedule",
230
- "description": "Scheduled tasks: run something on a schedule with nobody watching — a lamp tool, a fixed shell command, or a full agent run from a prompt (kind=prompt: the agent works unattended with the chosen profile and writes a report; its session appears as ⏰ <name>). `at` formats — recurring: 'every 6h' | 'every 30m' | 'daily 09:00' | 'mon,wed 08:30' | 'weekdays 09:00'; ONE-TIME (runs once, then turns itself off): 'today 15:14' | 'tomorrow 09:00' | 'once 2026-08-29 15:14' | 'in 2h'. Times are the USER'S LOCAL time (the machine's timezone): write the hour exactly as the user says it — NEVER convert to UTC. Tasks belong to the current workspace. permission = what a prompt run may do without asking: strict (dangerous actions denied), ask (default: a dangerous action sends the user an approval link/notification and waits up to approval_timeout seconds, denied if unanswered), yolo (allowed). notify = optional webhook URL that receives the result as JSON (for 'search and send me' tasks). action=add ALWAYS asks the user (it authorizes future unattended runs); remove/enable/disable/run also ask; list and log do not. The tasks are executed by Lampson's resident process (`lampson --daemon start` or the open web UI) — say so if the list shows no scheduler running. Use it when the user says 'every day at', 'each N hours', 'on Mondays', 'periodically', 'remind me', 'send me'.",
230
+ "description": "Scheduled tasks: run something on a schedule with nobody watching — a plugin tool, a fixed shell command, or a full agent run from a prompt (kind=prompt: the agent works unattended with the chosen profile and writes a report; its session appears as ⏰ <name>). `at` formats — recurring: 'every 6h' | 'every 30m' | 'daily 09:00' | 'mon,wed 08:30' | 'weekdays 09:00'; ONE-TIME (runs once, then turns itself off): 'today 15:14' | 'tomorrow 09:00' | 'once 2026-08-29 15:14' | 'in 2h'. Times are the USER'S LOCAL time (the machine's timezone): write the hour exactly as the user says it — NEVER convert to UTC. Tasks belong to the current workspace. permission = what a prompt run may do without asking: strict (dangerous actions denied), ask (default: a dangerous action sends the user an approval link/notification and waits up to approval_timeout seconds, denied if unanswered), yolo (allowed). notify = optional webhook URL that receives the result as JSON (for 'search and send me' tasks). action=add ALWAYS asks the user (it authorizes future unattended runs); remove/enable/disable/run also ask; list and log do not. The tasks are executed by Lampson's resident process (`lampson --daemon start` or the open web UI) — say so if the list shows no scheduler running. Use it when the user says 'every day at', 'each N hours', 'on Mondays', 'periodically', 'remind me', 'send me'.",
231
231
  "parameters": {"type": "object", "properties": {
232
232
  "action": {"type": "string", "enum": ["list", "add", "remove", "enable", "disable", "run", "log"], "description": "Default: list"},
233
233
  "id": {"type": "string", "description": "remove/enable/disable/run/log: the task id (from list)"},
234
234
  "name": {"type": "string", "description": "add: short human name"},
235
235
  "at": {"type": "string", "description": "add: the schedule — 'every 6h' | 'daily 09:00' | 'mon,wed 08:30' | 'weekdays 09:00'"},
236
- "kind": {"type": "string", "enum": ["lamp", "bash", "prompt"], "description": "add: what runs"},
237
- "lamp": {"type": "string", "description": "add kind=lamp: lamp name (must be ON)"},
238
- "tool": {"type": "string", "description": "add kind=lamp: tool of that lamp"},
239
- "args": {"type": "object", "description": "add kind=lamp: arguments for the tool"},
236
+ "kind": {"type": "string", "enum": ["plugin", "bash", "prompt"], "description": "add: what runs"},
237
+ "plugin": {"type": "string", "description": "add kind=plugin: plugin name (must be ON)"},
238
+ "tool": {"type": "string", "description": "add kind=plugin: tool of that plugin"},
239
+ "args": {"type": "object", "description": "add kind=plugin: arguments for the tool"},
240
240
  "command": {"type": "string", "description": "add kind=bash: the shell command (must finish on its own; no servers)"},
241
241
  "prompt": {"type": "string", "description": "add kind=prompt: self-contained instructions for the unattended agent run (what to do, how to verify, what to report)"},
242
242
  "agent": {"type": "string", "enum": ["build", "plan", "review", "explore"], "description": "add kind=prompt: profile (default build)"},
@@ -261,12 +261,12 @@ export task registry()
261
261
  "todo": t_todo.tool,
262
262
  "skill": skill_tool,
263
263
  "mcp": mcp_tool,
264
- "lamp": lamp_tool,
264
+ "plugin": plugin_tool,
265
265
  "lsp": lsp_tool,
266
266
  "schedule": schedule_tool
267
267
  }
268
268
 
269
- export let CATALOG be [t_read.SPEC, t_write.SPEC, t_edit.SPEC, t_ls.SPEC, t_find.SPEC, t_grep.SPEC, LSP_SPEC, t_bash.SPEC, t_process.SPEC, t_memo.SPEC, t_todo.SPEC, t_skill.SPEC, MCP_SPEC, LAMP_SPEC, SCHEDULE_SPEC]
269
+ export let CATALOG be [t_read.SPEC, t_write.SPEC, t_edit.SPEC, t_ls.SPEC, t_find.SPEC, t_grep.SPEC, LSP_SPEC, t_bash.SPEC, t_process.SPEC, t_memo.SPEC, t_todo.SPEC, t_skill.SPEC, MCP_SPEC, PLUGIN_SPEC, SCHEDULE_SPEC]
270
270
 
271
271
  -- Subconjuntos (para perfiles de agente): registry/catálogo filtrados por nombre.
272
272
  export task registry_subset(names)
@@ -3,7 +3,7 @@
3
3
  -- Por qué así (ver ../SPEC-WORKSPACES.md): en Synsema las capabilities de archivo Y los `use "./lib/…"` se resuelven
4
4
  -- relativos al CWD del proceso, y `proxy to` es estático (se evalúa al arrancar y anexa el path entero). Entonces:
5
5
  -- * cada workspace tiene su propio directorio .lampson/ws/<slug>/ que ES el cwd de su proceso:
6
- -- workspace → junction al proyecto lib public skills lamps memory → junctions a la instalación
6
+ -- workspace → junction al proyecto lib public skills plugins memory → junctions a la instalación
7
7
  -- web.syn chat.syn → copias (refrescadas al arrancar) .lampson/ → estado propio (sesiones, tareas…)
8
8
  -- .lampson/global → junction a <home>/.lampson (config.json con keys, mcp/lsp globales, skills-*)
9
9
  -- con eso `file("workspace/*")`, `use "./lib/x.syn"` y todo el código de siempre funcionan SIN cambios.
@@ -26,7 +26,7 @@ export let HUB_FILE be "hub.syn"
26
26
  export let HUB_TEMPLATE be "hub.tpl.syn"
27
27
  -- rango alto para no chocar con lo que usan las apps (3000, 5173, 8000, 8080-8090…): solo el hub queda en 8080
28
28
  let FIRST_PORT be 47101
29
- let LINKS be ["lib", "public", "skills", "lamps", "memory"]
29
+ let LINKS be ["lib", "public", "skills", "plugins", "memory"]
30
30
  let COPIES be ["web.syn", "chat.syn"]
31
31
 
32
32
  task is_win()
@@ -182,6 +182,31 @@ task unlink(where_)
182
182
  task is_link_dir(p)
183
183
  give file_exists(p)
184
184
 
185
+ -- crea una carpeta de la instalación (ruta absoluta: fuera del alcance de write_file, así que por exec)
186
+ task ensure_dir(p)
187
+ when is_win()
188
+ -- argumentos separados, como mklink: una línea entera con comillas dentro de `cmd /c` se rompe al citarla.
189
+ -- mkdir de cmd crea los intermedios y falla (sin daño) si ya existe.
190
+ run("cmd", ["/c", "mkdir", replace_text(p, "/", "\\")], 10)
191
+ otherwise
192
+ run("mkdir", ["-p", p], 10)
193
+ give true
194
+
195
+ -- un link a la instalación (lib, public, skills, plugins, memory): se crea si falta y se REHACE si quedó colgante.
196
+ -- file_exists da false tanto si no hay nada como si el link apunta a un destino que no existe (npm no trae
197
+ -- memory/ ni plugins/; un `ln -s` a una carpeta inexistente nace roto y después memory(write) muere con
198
+ -- "No such file or directory" — visto en un VPS el 2026-09-02). Por eso: destino primero, link después, y si
199
+ -- sigue sin resolverse es un error del workspace, no un silencio.
200
+ export task ensure_link(where_, target)
201
+ when file_exists(where_)
202
+ give true
203
+ unlink(where_)
204
+ ensure_dir(target)
205
+ link(where_, target)
206
+ when not file_exists(where_)
207
+ raise("could not link " + where_ + " → " + target + " (the workspace needs it: memory, plugins and the code live there)")
208
+ give true
209
+
185
210
  -- crea/repara .lampson/ws/<slug>: junctions al proyecto y a la instalación, copias de los entries
186
211
  export task prepare(w)
187
212
  require exec
@@ -209,16 +234,14 @@ export task prepare(w)
209
234
  when not link(ws_link, w["path"])
210
235
  raise("could not link " + ws_link + " → " + w["path"])
211
236
  each n in LINKS
212
- when not file_exists(d + "/" + n)
213
- link(d + "/" + n, h + sep + n)
214
- when not file_exists(d + "/.lampson/global")
215
- link(d + "/.lampson/global", h + sep + ".lampson")
237
+ ensure_link(d + "/" + n, h + sep + n)
238
+ ensure_link(d + "/.lampson/global", h + sep + ".lampson")
216
239
  each f in COPIES
217
240
  write_file(d + "/" + f, read_file(f))
218
241
  give d
219
242
 
220
243
  -- estado guardado ANTES de los workspaces (en <home>/.lampson, separado por slug en cada registro): sesiones y trazas
221
- -- del proyecto, sus tareas programadas y el encendido de lámparas se mueven a <ws>/.lampson la primera vez
244
+ -- del proyecto, sus tareas programadas y el encendido de plugins se mueven a <ws>/.lampson la primera vez
222
245
  task migrate(w, d)
223
246
  let slug be w["slug"]
224
247
  let moved be 0
@@ -248,8 +271,10 @@ task migrate(w, d)
248
271
  recover err
249
272
  set moved to moved
250
273
  try
251
- when file_exists(".lampson/lamps.json")
252
- write_file(d + "/.lampson/lamps.json", read_file(".lampson/lamps.json"))
274
+ when file_exists(".lampson/plugins.json")
275
+ write_file(d + "/.lampson/plugins.json", read_file(".lampson/plugins.json"))
276
+ otherwise when file_exists(".lampson/lamps.json")
277
+ write_file(d + "/.lampson/plugins.json", read_file(".lampson/lamps.json"))
253
278
  recover err
254
279
  set moved to moved
255
280
  give moved
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lampson",
3
- "version": "0.2.6",
4
- "description": "A coding agent written in Synsema: terminal + web, tools confined to the mounted project, lamps (your own tool plugins), LSP, MCP, sub-agents.",
3
+ "version": "0.2.7",
4
+ "description": "A coding agent written in Synsema: terminal + web, tools confined to the mounted project, plugins (your own tools, any language), LSP, MCP, sub-agents.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -18,7 +18,7 @@
18
18
  "lib/",
19
19
  "public/",
20
20
  "skills/",
21
- "lamps/",
21
+ "plugins/",
22
22
  "chat.syn",
23
23
  "web.syn",
24
24
  "hub.tpl.syn",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "example-hello",
3
- "description": "Example lamp: a Synsema program run under a capability ceiling. Copy this folder to make your own.",
3
+ "description": "Example plugin: a Synsema program run under a capability ceiling. Copy this folder to make your own.",
4
4
  "kind": "syn",
5
- "entry": "lamp.syn",
5
+ "entry": "plugin.syn",
6
6
  "caps": "",
7
7
  "timeout": 20,
8
8
  "tools": [
@@ -0,0 +1,19 @@
1
+ -- plugins/example-hello/plugin.syn — el plugin de ejemplo
2
+ --
3
+ -- Lampson lo corre así por cada llamada: synsema run --cap-set stdout,time,env=PLUGIN_*,env=LAMP_* plugin.syn
4
+ -- El techo (--cap-set) sale del manifiesto (plugin.json → "caps") que el humano aprobó al encenderlo;
5
+ -- pedir más acá (p. ej. `require net`) falla con "above the host ceiling".
6
+ -- Entrada por env: PLUGIN_TOOL (qué tool), PLUGIN_ARGS (sus args en JSON), PLUGIN_DIR, PLUGIN_WORKSPACE.
7
+ -- Salida: lo que imprimas por stdout vuelve al modelo como resultado de la tool.
8
+ intent: "example plugin for lampson: greet"
9
+
10
+ require env("PLUGIN_*")
11
+
12
+ let tool be env("PLUGIN_TOOL", "")
13
+ let args be json_decode(env("PLUGIN_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 plugin, running under a capability ceiling)")
18
+ otherwise
19
+ print("ERROR: unknown tool '" + tool + "'")
@@ -1,6 +1,6 @@
1
1
  /* panel: EL componente modal de la app (js/panel.js). Un cascarón (overlay, cabecera, ✕/Esc/clic afuera) y
2
2
  tres layouts: browse (buscador + lista + detalle), tabs (pestañas con formularios) y form (un formulario).
3
- Cada vista (sesiones, lámparas, configuración, programar…) solo aporta su contenido; nada de esto se repite. */
3
+ Cada vista (sesiones, plugins, configuración, programar…) solo aporta su contenido; nada de esto se repite. */
4
4
  .modal { position:fixed; inset:0; background:rgba(0,0,0,.45); display:flex; align-items:center; justify-content:center; z-index:50; }
5
5
  .panel { background:var(--paper); border:1px solid var(--rule-2); border-radius:var(--r); box-shadow:0 10px 30px rgba(0,0,0,.35); display:flex; flex-direction:column; min-height:0; max-height:92vh; }
6
6
  .panel.lg { width:min(1100px, 94vw); height:min(760px, 90vh); padding:14px 18px 12px; }
@@ -94,11 +94,11 @@
94
94
  .panel .dfoot .del.ask .yes { color:var(--rubric); cursor:pointer; font-weight:600; }
95
95
  .panel .dfoot .del.ask .no { cursor:pointer; }
96
96
  .panel .none { color:var(--ink-3); font:400 13px/1.6 var(--serif); max-width:520px; }
97
- /* switch encendido/apagado (lámparas) */
97
+ /* switch encendido/apagado (plugins) */
98
98
  .panel label.sw { display:inline-flex; align-items:center; margin:0 0 0 auto; gap:6px; cursor:pointer; user-select:none; font:400 11.5px/1 var(--mono); text-transform:none; letter-spacing:0; color:var(--ink-3); }
99
99
  .panel label.sw input { flex:none; width:16px; height:16px; padding:0; margin:0; accent-color:var(--accent); }
100
100
  .panel label.sw.on { color:var(--accent); }
101
- /* formulario generado desde un JSON Schema (tools de una lámpara) */
101
+ /* formulario generado desde un JSON Schema (tools de un plugin) */
102
102
  .panel .tool { border:1px solid var(--rule); border-radius:var(--r); padding:10px 12px; margin-bottom:10px; }
103
103
  .panel .tool .th { display:grid; grid-template-columns:auto 1fr auto; gap:8px 10px; align-items:baseline; }
104
104
  .panel .tool .th code { font-weight:600; color:var(--ink); }
package/public/hub.html CHANGED
@@ -25,7 +25,7 @@
25
25
  <div class="empty">
26
26
  <p class="eyebrow">workspaces</p>
27
27
  <h1>¿En qué proyecto trabajamos?</h1>
28
- <p class="lead">Cada workspace es una carpeta de tu disco con su propio agente: sesiones, tareas programadas, lámparas, MCP. Corren en procesos separados, así podés tener varios abiertos a la vez. Los que tienen tareas encendidas siguen vivos aunque cierres todo.</p>
28
+ <p class="lead">Cada workspace es una carpeta de tu disco con su propio agente: sesiones, tareas programadas, plugins, MCP. Corren en procesos separados, así podés tener varios abiertos a la vez. Los que tienen tareas encendidas siguen vivos aunque cierres todo.</p>
29
29
  <div class="wsgrid" id="wsGrid"></div>
30
30
  <p class="lead" style="margin-top:22px">Desde una terminal: <code>cd mi-proyecto &amp;&amp; lampson</code> (terminal) o <code>lampson --web</code> (esta web, en ese workspace).</p>
31
31
  </div>
package/public/index.html CHANGED
@@ -33,7 +33,7 @@
33
33
  <select id="agent" title="perfil de agente: qué tools puede usar"></select>
34
34
  <select id="perm" title="qué pasa con un comando peligroso (rm -rf, git push --force, sudo…)"><option value="ask">permisos: preguntar</option><option value="yolo">permisos: permitir todo</option><option value="strict">permisos: denegar</option></select>
35
35
  <span class="pill on" id="wsPill" style="display:none" title="cambiar de workspace">workspace ▾</span>
36
- <span class="pill" id="lamps" title="lámparas: plugins de tools que vos encendés · clic para ver y activar">lámparas</span>
36
+ <span class="pill" id="plugins" title="plugins: tools propias que vos encendés · clic para ver y activar">plugins</span>
37
37
  <span class="pill" id="model" title="proveedor y modelo · clic para cambiarlos o cargar una API key">…</span>
38
38
  </div>
39
39
  </header>
@@ -77,7 +77,7 @@
77
77
  <div class="body" id="apprBox"></div>
78
78
  </section>
79
79
  <section class="sec" data-sec="sched">
80
- <h2 title="tareas programadas (cada 6 h, todos los días a las 9, lunes 8:30…): una lámpara, un comando o una corrida del agente. Corren mientras lampson esté abierto, o con lampson --daemon start"><span class="caret">▸</span>Programadas<span class="cnt" id="schedCount"></span><button class="h2act" id="schedAddBtn" title="programar una tarea">+</button></h2>
80
+ <h2 title="tareas programadas (cada 6 h, todos los días a las 9, lunes 8:30…): un plugin, un comando o una corrida del agente. Corren mientras lampson esté abierto, o con lampson --daemon start"><span class="caret">▸</span>Programadas<span class="cnt" id="schedCount"></span><button class="h2act" id="schedAddBtn" title="programar una tarea">+</button></h2>
81
81
  <div class="body" id="schedBox"></div>
82
82
  </section>
83
83
  </aside>
@@ -119,7 +119,7 @@
119
119
  <script src="/js/todo.js"></script>
120
120
  <script src="/js/mcp.js"></script>
121
121
  <script src="/js/lsp.js"></script>
122
- <script src="/js/lamps.js"></script>
122
+ <script src="/js/plugins.js"></script>
123
123
  <script src="/js/schedules.js"></script>
124
124
  <script src="/js/approvals.js"></script>
125
125
  <script src="/js/config.js"></script>
package/public/js/app.js CHANGED
@@ -14,7 +14,7 @@ async function loadCfg() {
14
14
  sel.onchange = () => { localStorage.setItem('lampson.agent', sel.value); hint(); }; hint();
15
15
  if (!log.children.length) empty();
16
16
  }
17
- loadCfg().then(() => { loadTree(); loadSessions(); loadProcs(); loadMemory(); loadAgents(); loadMcp(); loadLsp(); loadLamps(); loadTodo(); loadSched(); loadApprovals(); checkUpdate(); const pq = new URLSearchParams(location.search).get('proc'); if (pq) openProc(pq); else if (session) openSession(session); });
17
+ loadCfg().then(() => { loadTree(); loadSessions(); loadProcs(); loadMemory(); loadAgents(); loadMcp(); loadLsp(); loadPlugins(); loadTodo(); loadSched(); loadApprovals(); checkUpdate(); const pq = new URLSearchParams(location.search).get('proc'); if (pq) openProc(pq); else if (session) openSession(session); });
18
18
  // respaldos lentos por si el stream de eventos se cae
19
19
  setInterval(loadProcs, events ? 60000 : 10000);
20
20
  setInterval(() => { loadSched(); if (!events) loadApprovals(); }, events ? 60000 : 15000);
package/public/js/chat.js CHANGED
@@ -69,13 +69,13 @@ function handle(chunk, thinking) {
69
69
  const k = ev.kind, d = ev.data;
70
70
  if (k === 'session') { const changed = d !== session; session = d; localStorage.setItem('lampson.session', d); if (changed) loadTodo(); }
71
71
  else if (k === 'assistant') add('assistant', md(d));
72
- else if (k === 'tool_call') { thinking.querySelector('span:last-child').textContent = 'ejecutando ' + esc(d.name) + '…'; pending = add('step', `<span class="ic">⚙</span>${cmdHtml(describe(d))}`); wireMore(pending); if (d.name === 'process') setTimeout(loadProcs, 2500); if (d.name === 'delegate') { setTimeout(loadAgents, 800); setTimeout(loadAgents, 4000); } if (d.name === 'todo') setTimeout(loadTodo, 300); if (d.name === 'lamp') setTimeout(loadLamps, 300); if (d.name === 'lsp') setTimeout(loadLsp, 1500); }
72
+ else if (k === 'tool_call') { thinking.querySelector('span:last-child').textContent = 'ejecutando ' + esc(d.name) + '…'; pending = add('step', `<span class="ic">⚙</span>${cmdHtml(describe(d))}`); wireMore(pending); if (d.name === 'process') setTimeout(loadProcs, 2500); if (d.name === 'delegate') { setTimeout(loadAgents, 800); setTimeout(loadAgents, 4000); } if (d.name === 'todo') setTimeout(loadTodo, 300); if (d.name === 'plugin') setTimeout(loadPlugins, 300); if (d.name === 'lsp') setTimeout(loadLsp, 1500); }
73
73
  else if (k === 'inbox') { add('meta', '✉ ' + esc(String(d).split('\n')[0].slice(0, 160))); loadAgents(); }
74
74
  else if (k === 'tool_result') { const out = String(d.output); const bad = /^(ERROR|DENIED)/.test(out);
75
- if (!bad && d.call && /^(write|edit|bash|process|delegate|lamp|skill)$/.test(d.call.name)) treeChanged();
75
+ if (!bad && d.call && /^(write|edit|bash|process|delegate|plugin|skill)$/.test(d.call.name)) treeChanged();
76
76
  if (d.call && d.call.name === 'schedule') { setTimeout(loadSched, 300); if (/^scheduled '/.test(out)) setSec('sched', true); }
77
- // lámpara recién creada: avisar en el chat con un acceso directo al switch (el modelo no sabe cómo se enciende en esta UI)
78
- if (d.call && d.call.name === 'lamp' && /^lamp '([^']+)' created/.test(out)) { const nm = out.match(/^lamp '([^']+)' created/)[1]; const m = add('meta', `☼ lámpara <b>${esc(nm)}</b> creada — está apagada: <a href="#" class="lampgo">encenderla en «lámparas»</a>`); m.querySelector('.lampgo').onclick = (ev) => { ev.preventDefault(); openLamps(nm); }; $('#lamps').classList.add('new'); setTimeout(() => $('#lamps').classList.remove('new'), 4000); }
77
+ // plugin recién creado: avisar en el chat con un acceso directo al switch (el modelo no sabe cómo se enciende en esta UI)
78
+ if (d.call && d.call.name === 'plugin' && /^plugin '([^']+)' created/.test(out)) { const nm = out.match(/^plugin '([^']+)' created/)[1]; const m = add('meta', `☼ plugin <b>${esc(nm)}</b> creado — está apagado: <a href="#" class="plugingo">encenderlo en «plugins»</a>`); m.querySelector('.plugingo').onclick = (ev) => { ev.preventDefault(); openPlugins(nm); }; $('#plugins').classList.add('new'); setTimeout(() => $('#plugins').classList.remove('new'), 4000); }
79
79
  const el = pending || add('step', '<span class="ic">→</span>'); pending = null; thinking.querySelector('span:last-child').textContent = 'pensando…'; const ic = el.querySelector('.ic'); if (ic) { ic.textContent = bad ? '✗' : '✓'; ic.className = 'ic ' + (bad ? 'bad' : 'ok'); } el.insertAdjacentHTML('beforeend', resultHtml(out, bad)); }
80
80
  else if (k === 'approval_request') { const el = add('approval', `<div class="card"><div class="why">⚠ ${esc(d.why)}</div><code>${esc(describe({name: d.name, args: d.args}))}</code><div class="btns"><button class="primary" data-ok="1">Permitir</button><button data-ok="0">Denegar</button></div></div>`); el.querySelectorAll('button').forEach(b => b.onclick = async () => { el.querySelectorAll('button').forEach(x => x.disabled = true); await api(BASE + '/api/approve', { id: d.id, decision: b.dataset.ok === '1' }); }); el.dataset.id = d.id; }
81
81
  else if (k === 'approval_result') { const el = [...log.querySelectorAll('.approval')].find(x => x.dataset.id === d.id); if (el) el.querySelector('.btns').innerHTML = d.approved ? '<span style="color:var(--str)">✓ permitido</span>' : (d.timeout ? '<span class="denied">✗ sin respuesta, denegado</span>' : '<span class="denied">✗ denegado</span>'); }
@@ -4,7 +4,8 @@ async function loadMemory() {
4
4
  let r; try { r = await (await fetch(BASE + '/api/memory')).json(); } catch (e) { return; }
5
5
  const box = $('#memory'); box.innerHTML = '';
6
6
  const notes = r.notes || [];
7
- $('#memCount').textContent = notes.length || ''; autoSec('memory', notes.length > 0);
7
+ $('#memCount').textContent = notes.length || ''; autoSec('memory', notes.length > 0 || r.ok === false);
8
+ if (r.ok === false) { box.innerHTML = '<div class="m none" title="el enlace memory/ del workspace no resuelve: volvé a correr lampson en este proyecto para repararlo">⚠ carpeta de memoria no accesible — corré <code>lampson</code> de nuevo en este proyecto</div>'; return; }
8
9
  if (!notes.length) { box.innerHTML = '<div class="m none">sin notas todavía</div>'; return; }
9
10
  for (const n of notes) { const d = document.createElement('div'); d.className = 'm' + (memOpen === n.name ? ' active' : ''); d.title = n.title; d.innerHTML = `<b>${esc(n.name)}</b><span class="t">${esc(n.title)}</span>`; d.onclick = () => openMemory(n.name); box.appendChild(d); }
10
11
  }