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
package/lib/tools/memo.syn
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "./
|
|
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
|
|
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
|
|
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("
|
|
98
|
-
require file.read("
|
|
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
|
|
102
|
+
give plugins.create(name, manifest, files)
|
|
103
103
|
when action == "enable"
|
|
104
|
-
give
|
|
104
|
+
give plugins.set_enabled(name, true)
|
|
105
105
|
when action == "disable"
|
|
106
|
-
give
|
|
107
|
-
let sm be
|
|
106
|
+
give plugins.set_enabled(name, false)
|
|
107
|
+
let sm be plugins.summary()
|
|
108
108
|
when length(sm) == 0
|
|
109
|
-
give "no
|
|
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
|
|
116
|
-
"name": "
|
|
117
|
-
"description": "
|
|
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:
|
|
121
|
-
"manifest": {"type": "object", "description": "create only: the
|
|
122
|
-
"files": {"type": "object", "description": "create only: {\"
|
|
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
|
|
162
|
-
task schedule_tool(action, id, name, at, kind,
|
|
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("
|
|
171
|
-
require file.read("
|
|
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 == "
|
|
178
|
-
set a to {"type": "
|
|
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
|
|
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": ["
|
|
237
|
-
"
|
|
238
|
-
"tool": {"type": "string", "description": "add kind=
|
|
239
|
-
"args": {"type": "object", "description": "add kind=
|
|
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
|
-
"
|
|
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,
|
|
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)
|
package/lib/workspaces.syn
CHANGED
|
@@ -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
|
|
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", "
|
|
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
|
-
|
|
213
|
-
|
|
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
|
|
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/
|
|
252
|
-
write_file(d + "/.lampson/
|
|
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.
|
|
4
|
-
"description": "A coding agent written in Synsema: terminal + web, tools confined to the mounted project,
|
|
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
|
-
"
|
|
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
|
|
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": "
|
|
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 + "'")
|
package/public/css/panel.css
CHANGED
|
@@ -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,
|
|
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 (
|
|
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
|
|
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,
|
|
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 && 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="
|
|
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…):
|
|
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/
|
|
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();
|
|
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 === '
|
|
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|
|
|
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
|
-
//
|
|
78
|
-
if (d.call && d.call.name === '
|
|
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>'); }
|
package/public/js/memory.js
CHANGED
|
@@ -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
|
}
|