lampson 0.2.6 → 0.2.8

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 CHANGED
@@ -60,15 +60,15 @@ It can only touch the folder you opened it in — not your home directory, not t
60
60
  a setting: it's how the language it's written in works. Reading and editing just happen; deleting, installing,
61
61
  `git reset`, `sudo` stop and wait for your *yes*; truly destructive commands are refused in every mode.
62
62
 
63
- ## Lamps
63
+ ## Plugins
64
64
 
65
- A **lamp** is a small folder that gives Lampson a new tool: query your database, call your company's API,
65
+ A **plugin** is a small folder that gives Lampson a new tool: query your database, call your company's API,
66
66
  deploy, send a message. Any language. **Off by default** — you turn each one on, and only then can the agent
67
67
  use it.
68
68
 
69
69
  ```
70
- ~/lampson/lamps/postgres/
71
- lamp.json ← what it's called, what it does, which tools it offers
70
+ ~/lampson/plugins/postgres/
71
+ plugin.json ← what it's called, what it does, which tools it offers
72
72
  query.py ← the code
73
73
  ```
74
74
 
@@ -79,22 +79,28 @@ use it.
79
79
  "parameters": {"type": "object", "properties": {"sql": {"type": "string"}}}, "readonly": true}]}
80
80
  ```
81
81
 
82
- The script gets the call in environment variables (`LAMP_TOOL`, `LAMP_ARGS` as JSON) and prints the result.
83
- Turn it on with `/lamps on postgres` or the **lámparas** pill in the web UI. Keep lamps global (every project)
84
- or inside a project (`.lampson/lamps/` — commit it and your team has it). The agent can *write* a lamp for you;
82
+ The script gets the call in environment variables (`PLUGIN_TOOL`, `PLUGIN_ARGS` as JSON) and prints the result.
83
+ Turn it on with `/plugins on postgres` or the **plugins** pill in the web UI. Keep plugins global (every project)
84
+ or inside a project (`.lampson/plugins/` — commit it and your team has it). The agent can *write* a plugin for you;
85
85
  turning it on is always yours.
86
86
 
87
- → [How lamps work](https://lampson.org/docs/lamps) · the example lamp ships in `lamps/example-hello/`.
87
+ → [How plugins work](https://lampson.org/docs/plugins) · the example ships in `plugins/example-hello/`.
88
+
89
+ Plugins are yours and local: any language, no sandbox unless you write them in Synsema. For tools with an
90
+ **enforced capability ceiling**, versioned and shared with any agent (Claude Code, Cursor, Lampson…), use
91
+ [lamps.sh](https://lamps.sh): `lamp add <ref>`, then `lamp mcp` as an MCP server in Lampson. (Until 0.2.6
92
+ plugins were called *lamps*; old `.lampson/lamps/` folders and `LAMP_*` variables still work for now.)
88
93
 
89
94
  ## Also in the box
90
95
 
91
96
  Sub-agents that work in parallel · skills (`SKILL.md` procedures, anything on [skills.sh](https://skills.sh)
92
97
  works) · MCP servers with the JSON you already have · language servers for real go-to-definition · project
93
- memory it reads back next session · sessions with a readable trace of every step · paste a screenshot and ask.
98
+ memory it reads back next session · sessions with a readable trace of every step · web pages fetched as
99
+ Markdown (a tenth of the tokens of raw HTML) · paste a screenshot and ask.
94
100
 
95
101
  ## Learn more
96
102
 
97
- - **[lampson.org/docs](https://lampson.org/docs)** — quickstart, permissions, providers, lamps, schedules…
103
+ - **[lampson.org/docs](https://lampson.org/docs)** — quickstart, permissions, providers, plugins, schedules…
98
104
  - **[guide.md](guide.md)** — the long version: how it runs, architecture, every knob, runtime notes.
99
105
 
100
106
  ## License
package/bin/lampson.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // bin/lampson.js — entry point of the npm package (`npm i -g lampson`).
3
3
  //
4
4
  // The code ships inside node_modules, but Lampson keeps STATE next to its code: the mounted `workspace`
5
- // junction, `.lampson/` (config, sessions, traces, spill), `memory/` and global `lamps/`. Living inside
5
+ // junction, `.lampson/` (config, sessions, traces, spill), `memory/` and global `plugins/`. Living inside
6
6
  // node_modules would lose all of that on every `npm i -g lampson@latest`. So this launcher keeps a stable
7
7
  // home (LAMPSON_HOME, default ~/lampson), syncs the package's code files into it when the version changes,
8
8
  // and runs the same launcher the git install uses (lampson.ps1 / lampson.sh). Everything else — synsema on
@@ -45,6 +45,9 @@ function sync() {
45
45
  try { installed = fs.readFileSync(marker, 'utf8').trim(); } catch (e) { /* first run */ }
46
46
  if (installed === pkg.version && fs.existsSync(path.join(home, 'chat.syn'))) return 'ok';
47
47
  fs.mkdirSync(home, { recursive: true });
48
+ // the folders every workspace links to (lib/workspaces.syn LINKS): if `memory/` does not exist when the
49
+ // link is made, the link is born dangling and memory(write) fails with "No such file or directory"
50
+ for (const d of ['memory', 'plugins']) fs.mkdirSync(path.join(home, d), { recursive: true });
48
51
  for (const item of CODE) {
49
52
  const src = path.join(pkgDir, item);
50
53
  if (!fs.existsSync(src)) continue;
@@ -52,11 +55,14 @@ function sync() {
52
55
  if (fs.statSync(src).isDirectory()) { fs.rmSync(dst, { recursive: true, force: true }); copyDir(src, dst); }
53
56
  else copyFile(src, dst);
54
57
  }
55
- // global lamps are user content: seed the example once, never overwrite what the user put there
56
- const lampsSrc = path.join(pkgDir, 'lamps'), lampsDst = path.join(home, 'lamps');
57
- if (fs.existsSync(lampsSrc)) {
58
- for (const e of fs.readdirSync(lampsSrc, { withFileTypes: true })) {
59
- if (e.isDirectory() && !fs.existsSync(path.join(lampsDst, e.name))) copyDir(path.join(lampsSrc, e.name), path.join(lampsDst, e.name));
58
+ // global plugins are user content: seed the example once, never overwrite what the user put there.
59
+ // Until 2026-08 the folder was `lamps/` (they were "lamps"): an existing one is renamed once, same content.
60
+ const legacyDst = path.join(home, 'lamps'), pluginsDst = path.join(home, 'plugins');
61
+ if (fs.existsSync(legacyDst) && !fs.existsSync(pluginsDst)) { fs.renameSync(legacyDst, pluginsDst); console.log('lampson: renamed ' + legacyDst + ' -> ' + pluginsDst + ' (lamps are now called plugins)'); }
62
+ const pluginsSrc = path.join(pkgDir, 'plugins');
63
+ if (fs.existsSync(pluginsSrc)) {
64
+ for (const e of fs.readdirSync(pluginsSrc, { withFileTypes: true })) {
65
+ if (e.isDirectory() && !fs.existsSync(path.join(pluginsDst, e.name))) copyDir(path.join(pluginsSrc, e.name), path.join(pluginsDst, e.name));
60
66
  }
61
67
  }
62
68
  fs.writeFileSync(marker, pkg.version + '\n');
@@ -68,7 +74,7 @@ if (args.some(a => /^--?(version|v)$/i.test(a))) { console.log('lampson ' + pkg.
68
74
 
69
75
  let state;
70
76
  try { state = sync(); } catch (e) { console.error('lampson: could not prepare ' + home + ': ' + e.message); process.exit(1); }
71
- if (state === 'installed') console.log('lampson ' + pkg.version + ' → ' + home + ' (your config, sessions and lamps live there)');
77
+ if (state === 'installed') console.log('lampson ' + pkg.version + ' → ' + home + ' (your config, sessions and plugins live there)');
72
78
  if (state === 'updated') console.log('lampson updated to ' + pkg.version + ' in ' + home);
73
79
 
74
80
  // --update under npm: the code comes from the registry, not from git
package/chat.syn CHANGED
@@ -22,8 +22,8 @@ require file("workspace")
22
22
  require file("workspace/*")
23
23
  require file.read("skills")
24
24
  require file.read("skills/*")
25
- require file.read("lamps")
26
- require file.read("lamps/*")
25
+ require file.read("plugins")
26
+ require file.read("plugins/*")
27
27
  require file("memory")
28
28
  require file("memory/*")
29
29
  require file(".lampson")
@@ -49,7 +49,7 @@ use "./lib/diff.syn" as diff
49
49
  use "./lib/line.syn" as ed
50
50
  use "./lib/ui.syn" as ui
51
51
  use "./lib/mcp.syn" as mcp
52
- use "./lib/lamps.syn" as lamps
52
+ use "./lib/plugins.syn" as plugins
53
53
  use "./lib/lsp.syn" as lsp
54
54
  use "./lib/tools/todo.syn" as todo
55
55
  use "./lib/schedule.syn" as schedule
@@ -165,7 +165,7 @@ task print_diff_with(lead, path, d)
165
165
 
166
166
  -- tools "instantáneas": la llamada se imprime junto con su resultado, en una sola línea
167
167
  task is_instant(name)
168
- give name == "read" or name == "ls" or name == "find" or name == "grep" or name == "memory" or name == "edit" or name == "write" or name == "todo" or name == "lsp"
168
+ give name == "read" or name == "ls" or name == "find" or name == "grep" or name == "memory" or name == "edit" or name == "write" or name == "todo" or name == "lsp" or name == "fetch"
169
169
 
170
170
  let pending_call be ""
171
171
 
@@ -348,9 +348,9 @@ let COMMANDS be [
348
348
  ["/agents", "", "subagentes lanzados con delegate: estado, pasos y log de cada uno"],
349
349
  ["/todo", "", "lista de tareas del agente en esta sesión (la mantiene con la tool todo)"],
350
350
  ["/mcp", "[add <nombre> <comando…> [--project] | remove <nombre>]", "servers MCP: listar, conectar o quitar (global: lampson/.lampson/mcp.json · proyecto: .lampson/mcp.json)"],
351
- ["/lamps", "[on <nombre> | off <nombre> | run <lámpara> <tool> [json] | remove <nombre>]", "lámparas (plugins de tools): listar, encender o apagar (global: lampson/lamps/ · proyecto: .lampson/lamps/)"],
351
+ ["/plugins", "[on <nombre> | off <nombre> | run <plugin> <tool> [json] | remove <nombre>]", "plugins (tools propias, en cualquier lenguaje): listar, encender o apagar (global: lampson/plugins/ · proyecto: .lampson/plugins/)"],
352
352
  ["/lsp", "[add <typescript|python|rust|go|css|html> [--project] | add <nombre> <comando…> --ext .x=lang | remove <nombre>]", "language servers (navegación semántica: symbols/definition/references/hover); arrancan en la primera consulta"],
353
- ["/schedule", "[add <json> | run <id> | on <id> | off <id> | remove <id> | log <id>]", "tareas programadas (cada 6h, todos los días a las 9…): lámpara, comando o corrida del agente; corren mientras lampson esté abierto (o con lampson --daemon start, sin nada abierto)"],
353
+ ["/schedule", "[add <json> | run <id> | on <id> | off <id> | remove <id> | log <id>]", "tareas programadas (cada 6h, todos los días a las 9…): plugin, comando o corrida del agente; corren mientras lampson esté abierto (o con lampson --daemon start, sin nada abierto)"],
354
354
  ["/approve", "<id> [yes|no]", "responder una aprobación pendiente de una tarea programada que corre en background (sin yes|no la pregunta con el menú ↑↓)"],
355
355
  ["/out", "[n]", "resultado completo de la última tool del turno (n = contar hacia atrás: /out 2 es la anteúltima)"],
356
356
  ["/verbose", "", "alternar: mostrar SIEMPRE el output completo de cada tool (queda guardado en config.json)"],
@@ -393,10 +393,10 @@ task complete_args(cmd, head, last)
393
393
  give []
394
394
  when cmd == "/memory"
395
395
  give apply((m) => m["name"], memo.list())
396
- when cmd == "/lamps"
396
+ when cmd == "/plugins"
397
397
  when first
398
398
  give ["on", "off", "run", "remove"]
399
- give apply((l) => l["name"], lamps.summary())
399
+ give apply((l) => l["name"], plugins.summary())
400
400
  when cmd == "/mcp"
401
401
  when first
402
402
  give ["add", "remove"]
@@ -565,21 +565,21 @@ task banner(ws, cfg, profile, mode, sid)
565
565
  print(" workspace " + ws)
566
566
  print(" agente " + profile + " permisos " + mode + " modelo " + cfg["model"])
567
567
  print(" sesión " + sid + " " + git.summary())
568
- -- extensiones en una línea (detalle con /lamps, /mcp, /lsp): encendidas/total
569
- let ls be lamps.summary()
570
- let lamps_on be length(where(ls, (l) => l["enabled"]))
568
+ -- extensiones en una línea (detalle con /plugins, /mcp, /lsp): encendidas/total
569
+ let ls be plugins.summary()
570
+ let plugins_on be length(where(ls, (l) => l["enabled"]))
571
571
  let ms be mcp.summary()
572
572
  let mcp_on be length(where(ms, (m) => m["status"] == "ready"))
573
573
  let ss be lsp.summary()
574
574
  let ext be []
575
575
  when length(ls) > 0
576
- set ext to append(ext, (when lamps_on > 0 then green("lamps " + text(lamps_on) + "/" + text(length(ls))) otherwise dim("lamps 0/" + text(length(ls)))))
576
+ set ext to append(ext, (when plugins_on > 0 then green("plugins " + text(plugins_on) + "/" + text(length(ls))) otherwise dim("plugins 0/" + text(length(ls)))))
577
577
  when length(ms) > 0
578
578
  set ext to append(ext, (when mcp_on == length(ms) then green("mcp " + text(mcp_on) + "/" + text(length(ms))) otherwise red("mcp " + text(mcp_on) + "/" + text(length(ms)))))
579
579
  when length(ss) > 0
580
580
  set ext to append(ext, dim("lsp " + text(length(ss))))
581
581
  when length(ext) > 0
582
- print(" extras " + join(ext, dim(" · ")) + dim(" (/lamps /mcp /lsp)"))
582
+ print(" extras " + join(ext, dim(" · ")) + dim(" (/plugins /mcp /lsp)"))
583
583
  let upd be update.line()
584
584
  when upd != ""
585
585
  print(" " + yellow("⬆ " + upd) + dim(" (o /update acá)"))
@@ -815,8 +815,8 @@ agent Sched
815
815
  require file("workspace/*")
816
816
  require file.read("skills")
817
817
  require file.read("skills/*")
818
- require file.read("lamps")
819
- require file.read("lamps/*")
818
+ require file.read("plugins")
819
+ require file.read("plugins/*")
820
820
  require file("memory")
821
821
  require file("memory/*")
822
822
  require file(".lampson")
@@ -902,13 +902,13 @@ let system_msg be system_for(profile)
902
902
  -- servers MCP (globales en .lampson/mcp.json, del proyecto en workspace/.lampson/mcp.json): arrancan antes
903
903
  -- de armar el catálogo de tools; hasta 8 s de espera a que estén listos
904
904
  let mcp_servers be mcp.start_all(8)
905
- -- mcp / lsp / lámparas van resumidos en el banner ("extras"); acá solo se avisa lo que está ROTO
905
+ -- mcp / lsp / plugins van resumidos en el banner ("extras"); acá solo se avisa lo que está ROTO
906
906
  each ms in mcp.summary()
907
907
  when ms["status"] != "ready"
908
908
  print(" " + red("○ mcp " + ms["name"] + " " + ms["status"]) + dim(when ms["error"] != nothing then " · " + text(ms["error"]) otherwise ""))
909
- each l in lamps.summary()
909
+ each l in plugins.summary()
910
910
  when l["error"] != nothing
911
- print(" " + red("○ lamp " + l["name"] + " rota") + dim(" · " + text(l["error"])))
911
+ print(" " + red("○ plugin " + l["name"] + " roto") + dim(" · " + text(l["error"])))
912
912
  let opts be opts_for(profile, lower(env("LAMPSON_PERMISSION", "ask")))
913
913
 
914
914
  -- marca de corrida (blackboard): session.save la estampa en meta.run; reanudar una sesión guardada por
@@ -1181,7 +1181,7 @@ while running
1181
1181
  let sjson be trim(slice(srest, 3, length(srest)))
1182
1182
  when sjson == ""
1183
1183
  print(" uso: /schedule add {\"name\": \"…\", \"when\": \"daily 09:00\", \"action\": {\"type\": \"bash\", \"command\": \"npm test\"}, \"permission\": \"ask\"}")
1184
- print(" " + dim("when: every 6h · daily 09:00 · mon,wed 08:30 · weekdays 09:00 action.type: lamp {lamp, tool, args} · bash {command} · prompt {prompt, agent}"))
1184
+ print(" " + dim("when: every 6h · daily 09:00 · mon,wed 08:30 · weekdays 09:00 action.type: plugin {plugin, tool, args} · bash {command} · prompt {prompt, agent}"))
1185
1185
  print(" " + dim("o pedíselo al agente en lenguaje natural: «todos los días a las 9 revisá los tests y avisame»"))
1186
1186
  otherwise
1187
1187
  try
@@ -1249,13 +1249,17 @@ while running
1249
1249
  print(" " + (when yes then green("✓ permitido ") otherwise red("✗ denegado ")) + atoks[0])
1250
1250
  otherwise
1251
1251
  print(" " + red("no hay una aprobación pendiente con id " + atoks[0]))
1252
- otherwise when input == "/lamps" or starts_with(input, "/lamps ")
1253
- let lrest be trim(slice(input, 6, length(input)))
1252
+ otherwise when input == "/plugins" or starts_with(input, "/plugins ") or input == "/lamps" or starts_with(input, "/lamps ")
1253
+ -- /lamps = el nombre viejo (hasta 2026-09): sigue andando, con aviso
1254
+ let is_old be starts_with(input, "/lamps")
1255
+ when is_old
1256
+ print(" " + dim("/lamps ahora es /plugins (las lámparas de lampson pasaron a llamarse plugins)"))
1257
+ let lrest be trim(slice(input, when is_old then 6 otherwise 8, length(input)))
1254
1258
  when starts_with(lrest, "run ")
1255
- -- /lamps run <lámpara> <tool> [json de args] → la corre el usuario, sin el modelo
1259
+ -- /plugins run <plugin> <tool> [json de args] → la corre el usuario, sin el modelo
1256
1260
  let rt be where(split(trim(slice(lrest, 4, length(lrest))), " "), (x) => x != "")
1257
1261
  when length(rt) < 2
1258
- print(" uso: /lamps run <lámpara> <tool> [{\"arg\": \"valor\"}]")
1262
+ print(" uso: /plugins run <plugin> <tool> [{\"arg\": \"valor\"}]")
1259
1263
  otherwise
1260
1264
  let rargs be {}
1261
1265
  when length(rt) > 2
@@ -1264,30 +1268,31 @@ while running
1264
1268
  recover err
1265
1269
  print(" " + red("args: JSON inválido"))
1266
1270
  try
1267
- print(lamps.call(lamps.tool_name(rt[0], rt[1]), rargs))
1271
+ print(plugins.call(plugins.tool_name(rt[0], rt[1]), rargs))
1268
1272
  recover err
1269
1273
  print(" " + red(text(err)))
1270
1274
  otherwise when starts_with(lrest, "remove ")
1271
1275
  try
1272
- print(" " + lamps.remove(trim(slice(lrest, 7, length(lrest)))))
1276
+ print(" " + plugins.remove(trim(slice(lrest, 7, length(lrest)))))
1273
1277
  recover err
1274
1278
  print(" " + red(text(err)))
1275
1279
  set opts to opts_for(profile, mode)
1276
1280
  otherwise when starts_with(lrest, "on ") or starts_with(lrest, "off ")
1277
1281
  let lon be starts_with(lrest, "on ")
1278
1282
  try
1279
- print(" " + lamps.set_enabled(trim(slice(lrest, when lon then 3 otherwise 4, length(lrest))), lon))
1283
+ print(" " + plugins.set_enabled(trim(slice(lrest, when lon then 3 otherwise 4, length(lrest))), lon))
1280
1284
  recover err
1281
1285
  print(" " + red(text(err)))
1282
1286
  set opts to opts_for(profile, mode)
1283
1287
  otherwise
1284
- let ls be lamps.summary()
1288
+ let ls be plugins.summary()
1285
1289
  when length(ls) == 0
1286
- print(" sin lámparas. Una lámpara es una carpeta con lamp.json: " + dim("lampson\\lamps\\<nombre>\\ (global) · .lampson\\lamps\\<nombre>\\ del repo (proyecto)"))
1287
- print(" " + dim("el agente también puede crearlas (write en .lampson/lamps/) y pedirte encenderlas"))
1290
+ print(" sin plugins. Un plugin es una carpeta con plugin.json: " + dim("lampson\\plugins\\<nombre>\\ (global) · .lampson\\plugins\\<nombre>\\ del repo (proyecto)"))
1291
+ print(" " + dim("el agente también puede crearlos (write en .lampson/plugins/) y pedirte encenderlos"))
1288
1292
  each l in ls
1289
- print(" " + (when l["enabled"] then green("● ") otherwise dim("○ ")) + pad(l["name"], 14) + dim(pad(l["scope"], 8) + pad(l["kind"], 5)) + (when l["error"] != nothing then red("rota: " + text(l["error"])) otherwise dim(text(length(l["tools"])) + " tools [" + join(l["tools"], ", ") + "]" + (when l["kind"] == "syn" then " techo: " + l["caps"] otherwise " $ " + l["command"]))))
1290
- print(" " + dim("encender/apagar: /lamps on <nombre> · /lamps off <nombre> · correr una vos: /lamps run <lámpara> <tool> [json]"))
1293
+ print(" " + (when l["enabled"] then green("● ") otherwise dim("○ ")) + pad(l["name"], 14) + dim(pad(l["scope"], 8) + pad(l["kind"], 5)) + (when l["error"] != nothing then red("roto: " + text(l["error"])) otherwise dim(text(length(l["tools"])) + " tools [" + join(l["tools"], ", ") + "]" + (when l["kind"] == "syn" then " techo: " + l["caps"] otherwise " $ " + l["command"]))) + (when l["legacy"] then yellow(" carpeta vieja .lampson/lamps/ → renombrala a .lampson/plugins/") otherwise ""))
1294
+ print(" " + dim("encender/apagar: /plugins on <nombre> · /plugins off <nombre> · correr uno vos: /plugins run <plugin> <tool> [json]"))
1295
+ print(" " + dim("¿herramientas con techo real, versionadas y para cualquier agente? eso son las lámparas de lamps.sh: lamp add <ref> y entran por /mcp add lamps lamp mcp"))
1291
1296
  otherwise when input == "/mcp" or starts_with(input, "/mcp ")
1292
1297
  let mrest be trim(slice(input, 4, length(input)))
1293
1298
  when starts_with(mrest, "add ")
package/lampson.ps1 CHANGED
@@ -8,7 +8,7 @@
8
8
  # lampson --agent plan · --yolo|--strict|--ask · --update · --help
9
9
  #
10
10
  # Cómo funciona (ver SPEC-WORKSPACES.md): .lampson\ws\<slug>\ es el cwd del proceso del workspace, con una junction
11
- # `workspace` al proyecto y junctions a lib/public/skills/lamps/memory de esta instalación. El registro y los procesos
11
+ # `workspace` al proyecto y junctions a lib/public/skills/plugins/memory de esta instalación. El registro y los procesos
12
12
  # los maneja lib/workspaces.syn vía cli.syn; este script solo resuelve la carpeta, llama a cli.syn y abre lo pedido.
13
13
  $ErrorActionPreference = "Stop"
14
14
  $here = Split-Path -Parent $MyInvocation.MyCommand.Path
@@ -45,6 +45,16 @@ foreach ($k in $skillMounts.Keys) {
45
45
  $link = Join-Path $here ".lampson\$k"; $src = $skillMounts[$k]
46
46
  if (-not (Test-Path -LiteralPath $link) -and (Test-Path -LiteralPath $src -PathType Container)) { New-Item -ItemType Junction -Path $link -Target $src | Out-Null }
47
47
  }
48
+ # 2026-09: las "lámparas" globales pasaron a llamarse plugins (plugins\). git renombra las del repo; las carpetas que
49
+ # vos pusiste en lamps\ se mueven una vez, y la carpeta vieja se borra solo si quedó vacía.
50
+ $oldLamps = Join-Path $here "lamps"
51
+ if (Test-Path -LiteralPath $oldLamps -PathType Container) {
52
+ $newPlugins = Join-Path $here "plugins"; New-Item -ItemType Directory -Force $newPlugins | Out-Null
53
+ foreach ($d in Get-ChildItem -LiteralPath $oldLamps -Directory -Force) {
54
+ if (-not (Test-Path -LiteralPath (Join-Path $newPlugins $d.Name))) { Move-Item -LiteralPath $d.FullName -Destination $newPlugins; Write-Host "lampson: lamps\$($d.Name) → plugins\$($d.Name) (las lámparas ahora son plugins)" }
55
+ }
56
+ if (-not (Get-ChildItem -LiteralPath $oldLamps -Force)) { Remove-Item -LiteralPath $oldLamps -Force }
57
+ }
48
58
 
49
59
  function Invoke-Cli([string]$cmd, [string]$ws) {
50
60
  # cli.syn imprime la respuesta como la ÚLTIMA línea (JSON)
package/lampson.sh CHANGED
@@ -32,6 +32,13 @@ for pair in "skills-global:$HOME/.agents/skills" "skills-claude:$HOME/.claude/sk
32
32
  link="$here/.lampson/${pair%%:*}"; src="${pair#*:}"
33
33
  [ -e "$link" ] || { [ -d "$src" ] && ln -s "$src" "$link"; } || true
34
34
  done
35
+ # 2026-09: las "lámparas" globales pasaron a llamarse plugins (plugins/). git renombra las del repo; las carpetas
36
+ # que vos pusiste en lamps/ se mueven una vez, y la carpeta vieja se borra solo si quedó vacía.
37
+ if [ -d "$here/lamps" ]; then
38
+ mkdir -p "$here/plugins"
39
+ for d in "$here"/lamps/*/; do [ -d "$d" ] || continue; n="$(basename "$d")"; [ -e "$here/plugins/$n" ] || { mv "$d" "$here/plugins/$n" && echo "lampson: lamps/$n → plugins/$n (las lámparas ahora son plugins)"; }; done
40
+ rmdir "$here/lamps" 2>/dev/null || true
41
+ fi
35
42
  cli() { # última línea = JSON
36
43
  local out; out="$(cd "$here" && LAMPSON_CMD="$1" LAMPSON_WORKSPACE="${2:-}" synsema run cli.syn)" || { echo "$out" >&2; exit 1; }
37
44
  echo "$out" | sed '$d' >&2 || true
package/lib/agents.syn CHANGED
@@ -32,7 +32,7 @@ use "./loop.syn" as loop
32
32
  use "./provider.syn" as provider
33
33
  use "./prompt.syn" as prompt
34
34
  use "./mcp.syn" as mcp
35
- use "./lamps.syn" as lamps
35
+ use "./plugins.syn" as plugins
36
36
 
37
37
  export let DIR be ".lampson/agents"
38
38
  let MAX_TASKS be 6
@@ -40,27 +40,27 @@ let PARALLEL be 4
40
40
 
41
41
  export let PROFILES be {
42
42
  "build": {
43
- "tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "bash", "process", "memory", "todo", "skill", "mcp", "lamp", "schedule", "delegate"],
43
+ "tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "fetch", "bash", "process", "memory", "todo", "skill", "mcp", "plugin", "schedule", "delegate"],
44
44
  "steps": 40,
45
45
  "addendum": "You are in BUILD mode: you may read, edit, create files and run commands. Delegate exploration of large codebases to the `explore` agent and independent code review to the `review` agent when it saves context; several independent questions can go in one delegate call (they run in parallel)."
46
46
  },
47
47
  "plan": {
48
- "tools": ["read", "ls", "find", "grep", "lsp", "memory", "todo", "skill", "delegate"],
48
+ "tools": ["read", "ls", "find", "grep", "lsp", "fetch", "memory", "todo", "skill", "delegate"],
49
49
  "steps": 30,
50
50
  "addendum": "You are in PLAN mode: READ-ONLY. You cannot edit files or run commands (those tools are not available). Investigate the codebase, then answer with a concrete, numbered implementation plan: files to touch, what changes in each, risks, and how to verify. Do not write code beyond short illustrative snippets."
51
51
  },
52
52
  "review": {
53
- "tools": ["read", "ls", "find", "grep", "lsp", "bash", "process", "memory", "skill"],
53
+ "tools": ["read", "ls", "find", "grep", "lsp", "fetch", "bash", "process", "memory", "skill"],
54
54
  "steps": 25,
55
55
  "addendum": "You are a REVIEWER: read-only on files (no write/edit), but you may run commands (tests, linters, git diff). Find real bugs, missing error handling, security issues and violations of the project's conventions. Report findings as a list with file:line, severity and a suggested fix. Do not modify files."
56
56
  },
57
57
  "explore": {
58
- "tools": ["read", "ls", "find", "grep", "lsp", "memory", "skill"],
58
+ "tools": ["read", "ls", "find", "grep", "lsp", "fetch", "memory", "skill"],
59
59
  "steps": 20,
60
60
  "addendum": "You are an EXPLORER: read-only. Locate the code relevant to the question (files, functions, line numbers) and report it concisely with paths and short excerpts. Do not propose changes."
61
61
  },
62
62
  "worker": {
63
- "tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "bash", "process", "memory", "todo", "skill"],
63
+ "tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "fetch", "bash", "process", "memory", "todo", "skill"],
64
64
  "steps": 40,
65
65
  "addendum": "You are a WORKER sub-agent: implement exactly the scoped task you were given (read, edit, run tests), then report what you changed (files) and how you verified it. Stay inside the scope; if something outside it is needed, report it instead of doing it."
66
66
  }
@@ -105,8 +105,8 @@ export task registry_for(name)
105
105
  set reg["delegate"] to delegate
106
106
  each m in mcp.names(mcp_readonly_only(name))
107
107
  set reg[m] to "mcp"
108
- each m in lamps.names(mcp_readonly_only(name))
109
- set reg[m] to "lamp"
108
+ each m in plugins.names(mcp_readonly_only(name))
109
+ set reg[m] to "plugin"
110
110
  give reg
111
111
 
112
112
  export task catalog_for(name)
@@ -119,7 +119,7 @@ export task catalog_for(name)
119
119
  let cat be tools.catalog_subset(p["tools"])
120
120
  when contains(p["tools"], "delegate")
121
121
  set cat to append(cat, DELEGATE_SPEC)
122
- give cat + mcp.catalog(mcp_readonly_only(name)) + lamps.catalog(mcp_readonly_only(name))
122
+ give cat + mcp.catalog(mcp_readonly_only(name)) + plugins.catalog(mcp_readonly_only(name))
123
123
 
124
124
  -- Info de entorno para el system prompt (compartida por chat.syn / web.syn / delegate)
125
125
  export task env_info(cfg)
@@ -128,7 +128,7 @@ export task env_info(cfg)
128
128
  require time
129
129
  let sc be tools.shell_config()
130
130
  -- desde dónde habla el usuario (blackboard "lampson:ui", lo comparte el entry): el modelo NO debe decirle
131
- -- "/lamps" a alguien que está en la web (2026-08-28)
131
+ -- "/plugins" a alguien que está en la web (2026-08-28)
132
132
  observe "lampson:ui" as ui
133
133
  give {
134
134
  "ui": when ui == nothing then "terminal" otherwise text(ui["kind"]),
package/lib/fs.syn ADDED
@@ -0,0 +1,226 @@
1
+ -- lib/fs.syn — operaciones de archivos del explorador web: crear, renombrar, mover, copiar, borrar
2
+ --
3
+ -- El runtime tiene write_file/list_dir/file_info pero NO rename/delete/copy/mkdir (v0.6.14), así que mover,
4
+ -- copiar y borrar corren por el shell POSIX (Git Bash en Windows), igual que plugins.remove. Todo path viene
5
+ -- del navegador y pasa por common.ws(): relativo al workspace, sin `..` ni absolutos (raise instructivo).
6
+ -- Reglas al estilo VS Code: nunca pisar un destino existente; duplicar = "<nombre> copy<.ext>", luego
7
+ -- "<nombre> copy 2<.ext>"…; una carpeta no se mueve/copia dentro de sí misma; la raíz no se toca.
8
+ use "./tools/common.syn" as c
9
+
10
+ task sh_quote(s)
11
+ give "'" + replace_text(s, "'", "'\\''") + "'"
12
+
13
+ -- shell POSIX aunque LAMPSON_SHELL apunte a cmd: mv/cp/rm/mkdir son de Git Bash
14
+ task shell()
15
+ require env("LAMPSON_*")
16
+ require env("OS")
17
+ let is_win be env("OS", "") == "Windows_NT"
18
+ let sh be env("LAMPSON_SHELL", when is_win then "C:\\Program Files\\Git\\bin\\bash.exe" otherwise "bash")
19
+ when is_win and not c.ends_with(lower(sh), "bash.exe")
20
+ set sh to "C:\\Program Files\\Git\\bin\\bash.exe"
21
+ give sh
22
+
23
+ -- corre un comando con cwd = workspace; error legible si falla
24
+ task sh(cmd)
25
+ require exec
26
+ require env("LAMPSON_*")
27
+ require env("OS")
28
+ let r be run(shell(), ["-c", cmd], 30, {"cwd": c.ROOT})
29
+ when r["exit_code"] != 0
30
+ let err be trim(text(r["stderr"]))
31
+ raise(when err == "" then "command failed: " + cmd otherwise err)
32
+ give true
33
+
34
+ -- path del navegador → {rel (como lo ve el modelo, "." = raíz), real ("workspace/…"), info}
35
+ task target(path)
36
+ require file.read("workspace")
37
+ require file.read("workspace/*")
38
+ let real be c.ws(path)
39
+ give {"rel": c.unws(real), "real": real, "info": file_info(real)}
40
+
41
+ task parent_of(rel)
42
+ when not contains(rel, "/")
43
+ give "."
44
+ let parts be split(rel, "/")
45
+ give join(slice(parts, 0, length(parts) - 1), "/")
46
+
47
+ task base_of(rel)
48
+ let parts be split(rel, "/")
49
+ give parts[length(parts) - 1]
50
+
51
+ task join_rel(dir, name)
52
+ give when dir == "." or dir == "" then name otherwise dir + "/" + name
53
+
54
+ task check_name(name)
55
+ let n be trim(text(name))
56
+ when n == "" or n == "." or n == ".." or contains(n, "/") or contains(n, "\\")
57
+ raise("invalid name \"" + text(name) + "\": one path segment, without / or \\")
58
+ when length(find_all(n, "[<>:\"|?*]")) > 0
59
+ raise("invalid name \"" + n + "\": < > : \" | ? * are not allowed in file names")
60
+ give n
61
+
62
+ task must_exist(t)
63
+ when not t["info"]["exists"]
64
+ raise("\"" + t["rel"] + "\" does not exist")
65
+
66
+ task must_not_exist(rel)
67
+ require file.read("workspace")
68
+ require file.read("workspace/*")
69
+ when file_exists(c.ROOT + "/" + rel)
70
+ raise("\"" + rel + "\" already exists")
71
+
72
+ -- ¿`path` es `dir` o está dentro de `dir`?
73
+ task inside(dir, path)
74
+ when dir == "."
75
+ give true
76
+ give path == dir or starts_with(path, dir + "/")
77
+
78
+ task must_be_dir(rel, info)
79
+ when not info["exists"]
80
+ raise("\"" + rel + "\" does not exist")
81
+ when not info["is_dir"]
82
+ raise("\"" + rel + "\" is not a folder")
83
+
84
+ export task mkdir(dir, name)
85
+ require exec
86
+ require env("LAMPSON_*")
87
+ require env("OS")
88
+ require file("workspace")
89
+ require file("workspace/*")
90
+ let t be target(dir)
91
+ let rel be join_rel(t["rel"], check_name(name))
92
+ must_not_exist(rel)
93
+ sh("mkdir -p " + sh_quote(rel))
94
+ give {"path": rel, "is_dir": true}
95
+
96
+ export task create(dir, name, content)
97
+ require file("workspace")
98
+ require file("workspace/*")
99
+ let t be target(dir)
100
+ let rel be join_rel(t["rel"], check_name(name))
101
+ must_not_exist(rel)
102
+ write_file(c.ROOT + "/" + rel, when content == nothing then "" otherwise text(content))
103
+ give {"path": rel, "is_dir": false}
104
+
105
+ export task rename(path, name)
106
+ require exec
107
+ require env("LAMPSON_*")
108
+ require env("OS")
109
+ require file("workspace")
110
+ require file("workspace/*")
111
+ let t be target(path)
112
+ must_exist(t)
113
+ when t["rel"] == "."
114
+ raise("the workspace root cannot be renamed")
115
+ let rel be join_rel(parent_of(t["rel"]), check_name(name))
116
+ when rel == t["rel"]
117
+ give {"path": rel, "is_dir": t["info"]["is_dir"]}
118
+ -- solo cambian mayúsculas: en un FS case-insensitive el destino "existe" → pasar por un temporal
119
+ when lower(rel) == lower(t["rel"])
120
+ let tmp be rel + ".lampson-rename-tmp"
121
+ sh("mv " + sh_quote(t["rel"]) + " " + sh_quote(tmp) + " && mv " + sh_quote(tmp) + " " + sh_quote(rel))
122
+ give {"path": rel, "is_dir": t["info"]["is_dir"]}
123
+ must_not_exist(rel)
124
+ sh("mv " + sh_quote(t["rel"]) + " " + sh_quote(rel))
125
+ give {"path": rel, "is_dir": t["info"]["is_dir"]}
126
+
127
+ export task move(path, to_dir)
128
+ require exec
129
+ require env("LAMPSON_*")
130
+ require env("OS")
131
+ require file("workspace")
132
+ require file("workspace/*")
133
+ let t be target(path)
134
+ must_exist(t)
135
+ when t["rel"] == "."
136
+ raise("the workspace root cannot be moved")
137
+ let d be target(to_dir)
138
+ must_be_dir(d["rel"], d["info"])
139
+ when t["info"]["is_dir"] and inside(t["rel"], d["rel"])
140
+ raise("cannot move a folder into itself")
141
+ let rel be join_rel(d["rel"], base_of(t["rel"]))
142
+ when rel == t["rel"]
143
+ give {"path": rel, "is_dir": t["info"]["is_dir"]}
144
+ must_not_exist(rel)
145
+ sh("mv " + sh_quote(t["rel"]) + " " + sh_quote(rel))
146
+ give {"path": rel, "is_dir": t["info"]["is_dir"]}
147
+
148
+ -- "b.ts" en `dir` → "b copy.ts", "b copy 2.ts"…; carpetas y dotfiles: "sub copy", ".env copy"
149
+ task copy_name(dir, base)
150
+ require file.read("workspace")
151
+ require file.read("workspace/*")
152
+ let stem be base
153
+ let ext be ""
154
+ let parts be split(base, ".")
155
+ when length(parts) > 1 and parts[0] != ""
156
+ set ext to "." + parts[length(parts) - 1]
157
+ set stem to slice(base, 0, length(base) - length(ext))
158
+ let n be 1
159
+ let cand be stem + " copy" + ext
160
+ while file_exists(c.ROOT + "/" + join_rel(dir, cand)) and n < 100
161
+ set n to n + 1
162
+ set cand to stem + " copy " + text(n) + ext
163
+ give cand
164
+
165
+ -- to_dir = nothing → duplicar al lado; otra carpeta → copiar ahí (mismo nombre, o "copy" si ya hay uno)
166
+ export task copy(path, to_dir)
167
+ require exec
168
+ require env("LAMPSON_*")
169
+ require env("OS")
170
+ require file("workspace")
171
+ require file("workspace/*")
172
+ let t be target(path)
173
+ must_exist(t)
174
+ when t["rel"] == "."
175
+ raise("the workspace root cannot be copied")
176
+ let dir be when to_dir == nothing then parent_of(t["rel"]) otherwise target(to_dir)["rel"]
177
+ must_be_dir(dir, file_info(c.ws(dir)))
178
+ when t["info"]["is_dir"] and inside(t["rel"], dir)
179
+ raise("cannot copy a folder into itself")
180
+ let base be base_of(t["rel"])
181
+ let rel be join_rel(dir, base)
182
+ when rel == t["rel"] or file_exists(c.ROOT + "/" + rel)
183
+ set rel to join_rel(dir, copy_name(dir, base))
184
+ sh("cp -r " + sh_quote(t["rel"]) + " " + sh_quote(rel))
185
+ give {"path": rel, "is_dir": t["info"]["is_dir"]}
186
+
187
+ export task remove(path)
188
+ require exec
189
+ require env("LAMPSON_*")
190
+ require env("OS")
191
+ require file("workspace")
192
+ require file("workspace/*")
193
+ let t be target(path)
194
+ must_exist(t)
195
+ when t["rel"] == "."
196
+ raise("the workspace root cannot be deleted")
197
+ when t["rel"] == ".git" or t["rel"] == ".lampson"
198
+ raise("\"" + t["rel"] + "\" is not deleted from the explorer")
199
+ sh("rm -rf " + sh_quote(t["rel"]))
200
+ give {"path": t["rel"], "is_dir": t["info"]["is_dir"]}
201
+
202
+ -- POST /api/fs: {op, path, name?, to?, content?} → {path, is_dir}
203
+ export task apply(b)
204
+ require exec
205
+ require env("LAMPSON_*")
206
+ require env("OS")
207
+ require file("workspace")
208
+ require file("workspace/*")
209
+ let op be when contains(b, "op") then lower(text(b["op"])) otherwise ""
210
+ let path be when contains(b, "path") then text(b["path"]) otherwise "."
211
+ let name be when contains(b, "name") then b["name"] otherwise nothing
212
+ -- (`to` es palabra reservada: `set x to`)
213
+ let dest be when contains(b, "to") then text(b["to"]) otherwise nothing
214
+ when op == "mkdir"
215
+ give mkdir(path, name)
216
+ when op == "create"
217
+ give create(path, name, when contains(b, "content") then b["content"] otherwise "")
218
+ when op == "rename"
219
+ give rename(path, name)
220
+ when op == "move"
221
+ give move(path, when dest == nothing then "." otherwise dest)
222
+ when op == "copy"
223
+ give copy(path, dest)
224
+ when op == "delete"
225
+ give remove(path)
226
+ raise("unknown op '" + op + "' (mkdir, create, rename, move, copy, delete)")