lampson 0.1.0

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.
Files changed (62) hide show
  1. package/.env.example +29 -0
  2. package/LICENSE +21 -0
  3. package/README.md +382 -0
  4. package/bin/lampson.js +81 -0
  5. package/chat.syn +799 -0
  6. package/lamps/example-hello/lamp.json +16 -0
  7. package/lamps/example-hello/lamp.syn +19 -0
  8. package/lampson.cmd +4 -0
  9. package/lampson.ps1 +88 -0
  10. package/lampson.sh +42 -0
  11. package/lib/agents.syn +471 -0
  12. package/lib/git.syn +58 -0
  13. package/lib/lamps.syn +386 -0
  14. package/lib/loop.syn +455 -0
  15. package/lib/lsp.syn +503 -0
  16. package/lib/mcp.syn +403 -0
  17. package/lib/permission.syn +154 -0
  18. package/lib/prompt.syn +75 -0
  19. package/lib/provider.syn +522 -0
  20. package/lib/session.syn +111 -0
  21. package/lib/settings.syn +70 -0
  22. package/lib/skills.syn +179 -0
  23. package/lib/tools/bash.syn +105 -0
  24. package/lib/tools/common.sh +49 -0
  25. package/lib/tools/common.syn +91 -0
  26. package/lib/tools/edit.syn +32 -0
  27. package/lib/tools/find.syn +35 -0
  28. package/lib/tools/grep.syn +31 -0
  29. package/lib/tools/img.ps1 +36 -0
  30. package/lib/tools/img.sh +22 -0
  31. package/lib/tools/ls.syn +18 -0
  32. package/lib/tools/memo.syn +148 -0
  33. package/lib/tools/proc.sh +42 -0
  34. package/lib/tools/proc.syn +314 -0
  35. package/lib/tools/process.syn +46 -0
  36. package/lib/tools/read.syn +25 -0
  37. package/lib/tools/skill.syn +14 -0
  38. package/lib/tools/todo.syn +97 -0
  39. package/lib/tools/write.syn +22 -0
  40. package/lib/tools.syn +198 -0
  41. package/lib/trace.syn +116 -0
  42. package/lib/tree.syn +59 -0
  43. package/lib/update.syn +57 -0
  44. package/package.json +40 -0
  45. package/public/fonts/plex-mono-400-latin-ext.woff2 +0 -0
  46. package/public/fonts/plex-mono-400-latin.woff2 +0 -0
  47. package/public/fonts/plex-mono-600-latin-ext.woff2 +0 -0
  48. package/public/fonts/plex-mono-600-latin.woff2 +0 -0
  49. package/public/fonts/plex-serif-400-latin-ext.woff2 +0 -0
  50. package/public/fonts/plex-serif-400-latin.woff2 +0 -0
  51. package/public/fonts/plex-serif-400i-latin-ext.woff2 +0 -0
  52. package/public/fonts/plex-serif-400i-latin.woff2 +0 -0
  53. package/public/fonts/plex-serif-600-latin-ext.woff2 +0 -0
  54. package/public/fonts/plex-serif-600-latin.woff2 +0 -0
  55. package/public/index.html +1268 -0
  56. package/public/vendor/xterm-addon-fit.js +2 -0
  57. package/public/vendor/xterm.css +218 -0
  58. package/public/vendor/xterm.js +2 -0
  59. package/skills/debugging/SKILL.md +33 -0
  60. package/skills/lampson/SKILL.md +117 -0
  61. package/skills/synsema/SKILL.md +75 -0
  62. package/web.syn +468 -0
package/lib/git.syn ADDED
@@ -0,0 +1,58 @@
1
+ -- lib/git.syn — estado de git del workspace (rama + archivos cambiados) para la UI
2
+ use "./tools/common.syn" as c
3
+
4
+ let G be "git"
5
+
6
+ task run_git(args)
7
+ try
8
+ let r be run(G, args, 15, {"cwd": c.ROOT})
9
+ when r["exit_code"] != 0
10
+ give nothing
11
+ give r["stdout"]
12
+ recover err
13
+ give nothing
14
+
15
+ -- {repo: bool, branch, changes: {path: "M"|"A"|"D"|"??"|"R"|…}, counts: {modified, added, deleted, untracked}}
16
+ export task status()
17
+ require exec("git")
18
+ let out be run_git(["status", "--porcelain=v1", "-b", "--untracked-files=all"])
19
+ when out == nothing
20
+ give {"repo": false, "branch": "", "changes": {}, "counts": {"modified": 0, "added": 0, "deleted": 0, "untracked": 0}}
21
+ let branch be ""
22
+ let changes be {}
23
+ let counts be {"modified": 0, "added": 0, "deleted": 0, "untracked": 0}
24
+ each line in split(replace_text(out, "\r", ""), "\n")
25
+ when starts_with(line, "## ")
26
+ set branch to split(split(slice(line, 3, length(line)), "...")[0], " ")[0]
27
+ otherwise when length(line) > 3
28
+ let xy be slice(line, 0, 2)
29
+ let path be replace_text(trim(slice(line, 3, length(line))), "\"", "")
30
+ when contains(path, " -> ")
31
+ set path to split(path, " -> ")[1]
32
+ let code be trim(xy)
33
+ when xy == "??"
34
+ set code to "??"
35
+ set counts["untracked"] to counts["untracked"] + 1
36
+ otherwise when contains(xy, "D")
37
+ set code to "D"
38
+ set counts["deleted"] to counts["deleted"] + 1
39
+ otherwise when contains(xy, "A")
40
+ set code to "A"
41
+ set counts["added"] to counts["added"] + 1
42
+ otherwise
43
+ set code to "M"
44
+ set counts["modified"] to counts["modified"] + 1
45
+ set changes[path] to code
46
+ give {"repo": true, "branch": branch, "changes": changes, "counts": counts}
47
+
48
+ -- una línea para el banner de terminal
49
+ export task summary()
50
+ require exec("git")
51
+ let s be status()
52
+ when not s["repo"]
53
+ give "sin git"
54
+ let k be s["counts"]
55
+ let n be k["modified"] + k["added"] + k["deleted"] + k["untracked"]
56
+ when n == 0
57
+ give "git " + s["branch"] + " · limpio"
58
+ give "git " + s["branch"] + " · " + text(k["modified"]) + " modificados · " + text(k["untracked"]) + " nuevos · " + text(k["deleted"]) + " borrados"
package/lib/lamps.syn ADDED
@@ -0,0 +1,386 @@
1
+ -- lib/lamps.syn — "lámparas": plugins de tools que el usuario (o el agente) escribe y el humano ENCIENDE
2
+ --
3
+ -- Qué es una lámpara: una carpeta con un manifiesto `lamp.json` y el código que implementa sus tools.
4
+ -- lamps/<nombre>/ GLOBAL (lampson se instala una vez → vale para todos los proyectos)
5
+ -- workspace/.lampson/lamps/<nombre>/ del PROYECTO (el agente puede crearlas con write: es su workspace)
6
+ -- lamp.json = {"name", "description", "kind": "syn" | "exec",
7
+ -- "entry": "lamp.syn" (kind=syn: un programa Synsema)
8
+ -- "command": "python lamp.py" (kind=exec: cualquier ejecutable — js, py, sh…)
9
+ -- "caps": "file.read=workspace/*" (kind=syn, opcional: techo EXTRA sobre stdout,time,env=LAMP_*)
10
+ -- "timeout": 60,
11
+ -- "tools": [{"name", "description", "parameters": {JSON Schema}, "readonly": false}]}
12
+ --
13
+ -- Cómo corre (patrón "safe runner" de la doc 22-sandbox): cada llamada es UN proceso hijo que termina.
14
+ -- kind=syn → `synsema run --cap-set stdout,time,env=LAMP_*[,caps] <entry>`: el techo lo fija el
15
+ -- manifiesto que el humano aprobó al encenderla; un `require` de más en el código de la
16
+ -- lámpara falla con "above the host ceiling" — no hay forma de escalar desde adentro.
17
+ -- kind=exec → el comando tal cual (sin techo de lenguaje: por eso encender es SIEMPRE humano).
18
+ -- La tool y sus args viajan por env: LAMP_TOOL, LAMP_ARGS (JSON), LAMP_DIR (carpeta de la lámpara,
19
+ -- relativa a la raíz de lampson = cwd del hijo si kind=syn; si kind=exec el cwd es el workspace),
20
+ -- LAMP_WORKSPACE (ruta real del proyecto). La lámpara imprime el resultado (texto o JSON) por stdout.
21
+ --
22
+ -- Encendido: `.lampson/lamps.json` {"enabled": {"<nombre>": true}}. APAGADA por defecto: descubrir una
23
+ -- carpeta nueva no la activa; la enciende el humano (UI: barra superior; terminal: /lamps on <nombre>) o el
24
+ -- agente con la tool `lamp` (action=enable → ask SIEMPRE, como skill install / mcp add). Crear = write;
25
+ -- encender = permiso. Sin `use` dinámico (Synsema no lo tiene, a propósito): nada de esto entra al proceso
26
+ -- de lampson, solo corre como hijo.
27
+ --
28
+ -- Las tools encendidas entran al catálogo como `lamp_<lámpara>_<tool>`; loop.execute las despacha acá
29
+ -- (registry las marca con "lamp", como "mcp"). permission.syn: ask por defecto, yolo permite, strict deniega;
30
+ -- las readonly también en plan/review/explore.
31
+
32
+ use "./tools/common.syn" as c
33
+
34
+ export let GLOBAL_DIR be "lamps"
35
+ export let PROJECT_DIR be "workspace/.lampson/lamps"
36
+ export let STATE_FILE be ".lampson/lamps.json"
37
+ let DEFAULT_TIMEOUT be 60
38
+ let BASE_CAPS be "stdout,time,env=LAMP_*"
39
+
40
+ -- nombre de lámpara: SIN "_" (el separador de lamp_<lámpara>_<tool>); las tools sí pueden llevarlo
41
+ task valid_name(name)
42
+ when name == nothing or name == ""
43
+ give false
44
+ give matches(text(name), "[a-zA-Z0-9-]{1,32}")
45
+
46
+ task valid_tool_name(name)
47
+ when name == nothing or name == ""
48
+ give false
49
+ give matches(text(name), "[a-zA-Z0-9_-]{1,40}")
50
+
51
+ -- lee y valida un manifiesto; devuelve {ok, lamp} o {ok: false, error}
52
+ task read_manifest(dir, name, scope)
53
+ let path be dir + "/" + name + "/lamp.json"
54
+ let doc be nothing
55
+ try
56
+ set doc to json_decode(read_file(path))
57
+ recover err
58
+ give {"ok": false, "name": name, "scope": scope, "error": "no lamp.json: " + text(err)}
59
+ let kind be when contains(doc, "kind") then lower(text(doc["kind"])) otherwise "syn"
60
+ when kind != "syn" and kind != "exec"
61
+ give {"ok": false, "name": name, "scope": scope, "error": "kind must be syn or exec"}
62
+ when kind == "syn" and not contains(doc, "entry")
63
+ give {"ok": false, "name": name, "scope": scope, "error": "kind=syn needs \"entry\" (the .syn file)"}
64
+ when kind == "exec" and not contains(doc, "command")
65
+ give {"ok": false, "name": name, "scope": scope, "error": "kind=exec needs \"command\""}
66
+ when not contains(doc, "tools") or length(doc["tools"]) == 0
67
+ give {"ok": false, "name": name, "scope": scope, "error": "no tools declared"}
68
+ let tools be []
69
+ each t in doc["tools"]
70
+ when not contains(t, "name") or not valid_tool_name(t["name"])
71
+ give {"ok": false, "name": name, "scope": scope, "error": "a tool has no valid name"}
72
+ set tools to append(tools, {"name": text(t["name"]), "description": when contains(t, "description") then text(t["description"]) otherwise "", "parameters": when contains(t, "parameters") then t["parameters"] otherwise {"type": "object", "properties": {}}, "readonly": contains(t, "readonly") and t["readonly"] == true})
73
+ give {"ok": true, "name": name, "scope": scope, "lamp": {"name": name, "scope": scope, "dir": dir + "/" + name, "kind": kind, "description": when contains(doc, "description") then text(doc["description"]) otherwise "", "entry": when contains(doc, "entry") then text(doc["entry"]) otherwise "", "command": when contains(doc, "command") then text(doc["command"]) otherwise "", "caps": when contains(doc, "caps") then text(doc["caps"]) otherwise "", "timeout": when contains(doc, "timeout") then floor(number(doc["timeout"])) otherwise DEFAULT_TIMEOUT, "tools": tools}}
74
+
75
+ task scan(dir, scope)
76
+ let out be []
77
+ try
78
+ each e in list_dir(dir)
79
+ when e["is_dir"] and valid_name(e["name"])
80
+ set out to append(out, read_manifest(dir, e["name"], scope))
81
+ recover err
82
+ give out
83
+ give out
84
+
85
+ task load_state()
86
+ try
87
+ let doc be json_decode(read_file(STATE_FILE))
88
+ give when contains(doc, "enabled") then doc["enabled"] otherwise {}
89
+ recover err
90
+ give {}
91
+
92
+ -- todas las lámparas descubiertas (proyecto pisa a global por nombre), con enabled y error.
93
+ -- LAMPSON_LAMPS_DIR = una carpeta extra (tests).
94
+ export task all()
95
+ require file(".lampson")
96
+ require file(".lampson/*")
97
+ require file.read("lamps")
98
+ require file.read("lamps/*")
99
+ require file("workspace")
100
+ require file("workspace/*")
101
+ require env("LAMPSON_*")
102
+ let by_name be {}
103
+ each r in scan(GLOBAL_DIR, "global")
104
+ set by_name[r["name"]] to r
105
+ each r in scan(PROJECT_DIR, "project")
106
+ set by_name[r["name"]] to r
107
+ when env("LAMPSON_LAMPS_DIR", "") != ""
108
+ each r in scan(env("LAMPSON_LAMPS_DIR", ""), "extra")
109
+ set by_name[r["name"]] to r
110
+ let st be load_state()
111
+ let out be []
112
+ each n in sort_by(keys(by_name), (x) => x)
113
+ let r be by_name[n]
114
+ let on be contains(st, n) and st[n] == true
115
+ when r["ok"]
116
+ let l be r["lamp"]
117
+ set l["enabled"] to on
118
+ set l["error"] to nothing
119
+ set out to append(out, l)
120
+ otherwise
121
+ set out to append(out, {"name": n, "scope": r["scope"], "dir": "", "kind": "?", "description": "", "entry": "", "command": "", "caps": "", "timeout": 0, "tools": [], "enabled": false, "error": r["error"]})
122
+ give out
123
+
124
+ export task enabled()
125
+ require file(".lampson")
126
+ require file(".lampson/*")
127
+ require file.read("lamps")
128
+ require file.read("lamps/*")
129
+ require file("workspace")
130
+ require file("workspace/*")
131
+ require env("LAMPSON_*")
132
+ give where(all(), (l) => l["enabled"] and l["error"] == nothing)
133
+
134
+ -- encender/apagar (el humano, o el agente tras aprobación). Encender una lámpara rota es error.
135
+ export task set_enabled(name, on)
136
+ require file(".lampson")
137
+ require file(".lampson/*")
138
+ require file.read("lamps")
139
+ require file.read("lamps/*")
140
+ require file("workspace")
141
+ require file("workspace/*")
142
+ require env("LAMPSON_*")
143
+ when not valid_name(name)
144
+ raise("invalid lamp name")
145
+ let found be where(all(), (l) => l["name"] == name)
146
+ when length(found) == 0
147
+ raise("no lamp named '" + name + "' (global: " + GLOBAL_DIR + "/<name>/lamp.json · project: " + PROJECT_DIR + "/<name>/lamp.json)")
148
+ when on and found[0]["error"] != nothing
149
+ raise("lamp '" + name + "' is broken: " + text(found[0]["error"]))
150
+ let st be load_state()
151
+ set st[name] to on == true
152
+ write_file(STATE_FILE, json_encode({"enabled": st}))
153
+ let l be found[0]
154
+ when on
155
+ give "lamp '" + name + "' ON (" + l["kind"] + ", " + text(length(l["tools"])) + " tools" + (when l["kind"] == "syn" then ", ceiling: " + ceiling_of(l) otherwise ", NO capability ceiling: exec") + ") — its tools are in the catalog from the next turn as lamp_" + name + "_<tool>"
156
+ give "lamp '" + name + "' OFF"
157
+
158
+ task ceiling_of(l)
159
+ give when l["caps"] == "" then BASE_CAPS otherwise BASE_CAPS + "," + l["caps"]
160
+
161
+ -- ---------- catálogo ----------
162
+ export task tool_name(lamp, tool)
163
+ give "lamp_" + lamp + "_" + tool
164
+
165
+ task split_name(full)
166
+ let segs be split(slice(full, 5, length(full)), "_")
167
+ when length(segs) < 2
168
+ raise("bad lamp tool name '" + full + "'")
169
+ give {"lamp": segs[0], "tool": join(slice(segs, 1, length(segs)), "_")}
170
+
171
+ export task catalog(readonly_only)
172
+ require file(".lampson")
173
+ require file(".lampson/*")
174
+ require file.read("lamps")
175
+ require file.read("lamps/*")
176
+ require file("workspace")
177
+ require file("workspace/*")
178
+ require env("LAMPSON_*")
179
+ let out be []
180
+ each l in enabled()
181
+ each t in l["tools"]
182
+ when not readonly_only or t["readonly"]
183
+ set out to append(out, {"name": tool_name(l["name"], t["name"]), "description": "[lamp " + l["name"] + "] " + t["description"], "parameters": t["parameters"]})
184
+ give out
185
+
186
+ export task names(readonly_only)
187
+ require file(".lampson")
188
+ require file(".lampson/*")
189
+ require file.read("lamps")
190
+ require file.read("lamps/*")
191
+ require file("workspace")
192
+ require file("workspace/*")
193
+ require env("LAMPSON_*")
194
+ give apply(catalog(readonly_only), (s) => s["name"])
195
+
196
+ -- ---------- llamada: un proceso hijo por invocación ----------
197
+ task run_lamp(exe, argv, cwd, envs, t)
198
+ let p be proc_spawn(exe, argv, {"cwd": cwd, "env": envs, "stderr": "merge", "on_full": "drop_oldest"})
199
+ proc_close_stdin(p)
200
+ let deadline be now() + t
201
+ let lines be []
202
+ let code be nothing
203
+ let timed_out be false
204
+ while code == nothing and not timed_out
205
+ let left be deadline - now()
206
+ when left <= 0
207
+ set timed_out to true
208
+ otherwise
209
+ let ev be proc_recv(p, when left < 0.2 then 0.2 otherwise left)
210
+ when ev == nothing
211
+ set timed_out to now() >= deadline
212
+ otherwise when ev["type"] == "exit"
213
+ set code to ev["data"]["exit_code"]
214
+ otherwise
215
+ set lines to append(lines, ev["data"])
216
+ proc_close(p)
217
+ let out be join(lines, "\n")
218
+ when timed_out
219
+ give "ERROR: lamp timed out after " + text(t) + "s (killed). Output so far:\n" + out
220
+ when code != 0
221
+ give "ERROR: lamp exited with code " + text(code) + "\n" + out
222
+ give when trim(out) == "" then "(no output)" otherwise out
223
+
224
+ -- separa "python lamp.py --x" en exe + args (comillas simples/dobles respetadas)
225
+ export task split_command(line)
226
+ let toks be []
227
+ let cur be ""
228
+ let q be ""
229
+ let has be false
230
+ let i be 0
231
+ while i < length(line)
232
+ let ch be slice(line, i, i + 1)
233
+ set i to i + 1
234
+ when q != ""
235
+ when ch == q
236
+ set q to ""
237
+ otherwise
238
+ set cur to cur + ch
239
+ otherwise when ch == "\"" or ch == "'"
240
+ set q to ch
241
+ set has to true
242
+ otherwise when ch == " " or ch == "\t"
243
+ when cur != "" or has
244
+ set toks to append(toks, cur)
245
+ set cur to ""
246
+ set has to false
247
+ otherwise
248
+ set cur to cur + ch
249
+ when cur != "" or has
250
+ set toks to append(toks, cur)
251
+ give toks
252
+
253
+ export task call(full_name, args)
254
+ require exec
255
+ require time
256
+ require file(".lampson")
257
+ require file(".lampson/*")
258
+ require file.read("lamps")
259
+ require file.read("lamps/*")
260
+ require file("workspace")
261
+ require file("workspace/*")
262
+ require env("LAMPSON_*")
263
+ let parts be split_name(full_name)
264
+ let found be where(enabled(), (l) => l["name"] == parts["lamp"])
265
+ when length(found) == 0
266
+ give "ERROR: lamp '" + parts["lamp"] + "' is not enabled (the user turns lamps on)"
267
+ let l be found[0]
268
+ let tl be where(l["tools"], (t) => t["name"] == parts["tool"])
269
+ when length(tl) == 0
270
+ give "ERROR: lamp '" + l["name"] + "' has no tool '" + parts["tool"] + "'"
271
+ -- LAMP_DIR relativa a la raíz de lampson (cwd del hijo para kind=syn); LAMP_WORKSPACE = la ruta real del
272
+ -- proyecto (lampson.ps1 la exporta; sin ella, la junction)
273
+ let envs be {"LAMP_TOOL": parts["tool"], "LAMP_ARGS": json_encode(when args == nothing then {} otherwise args), "LAMP_DIR": l["dir"], "LAMP_WORKSPACE": env("LAMPSON_WORKSPACE", c.ROOT)}
274
+ when l["kind"] == "syn"
275
+ -- techo del manifiesto: el código de la lámpara no puede pedir más de lo que el humano aprobó
276
+ give c.truncate(run_lamp(env("LAMPSON_SYNSEMA", "synsema"), ["run", "--cap-set", ceiling_of(l), l["dir"] + "/" + l["entry"]], ".", envs, l["timeout"]), c.MAX_OUTPUT)
277
+ let argv be split_command(l["command"])
278
+ when length(argv) == 0
279
+ give "ERROR: lamp '" + l["name"] + "' has an empty command"
280
+ -- el comando corre desde la RAÍZ DEL WORKSPACE (para que "python lamp.py" vea el proyecto), así que un
281
+ -- token que nombra un archivo de la carpeta de la lámpara se resuelve solo (2026-08-28: "python lamp.py"
282
+ -- salía con code 2 porque lamp.py no estaba en el cwd)
283
+ let rel_dir be when starts_with(l["dir"], c.ROOT + "/") then slice(l["dir"], length(c.ROOT) + 1, length(l["dir"])) otherwise "../" + l["dir"]
284
+ let resolved be []
285
+ each tok in argv
286
+ when not contains(tok, "/") and not contains(tok, "\\") and file_exists(l["dir"] + "/" + tok)
287
+ set resolved to append(resolved, rel_dir + "/" + tok)
288
+ otherwise
289
+ set resolved to append(resolved, tok)
290
+ give c.truncate(run_lamp(resolved[0], slice(resolved, 1, length(resolved)), c.ROOT, envs, l["timeout"]), c.MAX_OUTPUT)
291
+
292
+ -- crear una lámpara DEL PROYECTO (workspace/.lampson/lamps/<name>/): escribe lamp.json + el código, revalida el
293
+ -- manifiesto y, si es syn, corre `synsema check` sobre el entry. Como cordis_define en dsh: define y valida,
294
+ -- NO ejecuta ni enciende — encender sigue siendo del humano (set_enabled tras ask).
295
+ -- `files` = {"lamp.syn": "…", "helper.py": "…"}: nombres simples (sin / ni ..) dentro de la carpeta.
296
+ export task create(name, manifest, files)
297
+ require exec
298
+ require time
299
+ require file(".lampson")
300
+ require file(".lampson/*")
301
+ require file.read("lamps")
302
+ require file.read("lamps/*")
303
+ require file("workspace")
304
+ require file("workspace/*")
305
+ require env("LAMPSON_*")
306
+ when not valid_name(name)
307
+ raise("invalid lamp name '" + text(name) + "': letters, digits and - only (no _), max 32")
308
+ when manifest == nothing or not contains(manifest, "tools")
309
+ raise("manifest needs at least \"tools\" (and \"entry\" for kind=syn or \"command\" for kind=exec)")
310
+ let dir be PROJECT_DIR + "/" + name
311
+ let m be manifest
312
+ set m["name"] to name
313
+ when not contains(m, "kind")
314
+ set m["kind"] to "syn"
315
+ write_file(dir + "/lamp.json", json_encode(m))
316
+ let written be ["lamp.json"]
317
+ when files != nothing
318
+ each fname in keys(files)
319
+ when not matches(fname, "[A-Za-z0-9_.-]{1,64}") or starts_with(fname, ".")
320
+ raise("bad file name '" + fname + "' (simple names inside the lamp folder only)")
321
+ write_file(dir + "/" + fname, text(files[fname]))
322
+ set written to append(written, fname)
323
+ let found be where(all(), (l) => l["name"] == name)
324
+ when length(found) == 0
325
+ raise("lamp written but not discovered (?)")
326
+ let l be found[0]
327
+ when l["error"] != nothing
328
+ give "lamp '" + name + "' written to " + dir + " but its manifest is INVALID: " + text(l["error"]) + " — fix it with edit and call lamp(action=list) to re-check"
329
+ let note be ""
330
+ when l["kind"] == "syn"
331
+ let chk be run_lamp(env("LAMPSON_SYNSEMA", "synsema"), ["check", l["dir"] + "/" + l["entry"]], ".", {}, 30)
332
+ when starts_with(chk, "ERROR")
333
+ give "lamp '" + name + "' written to " + dir + " but `synsema check " + l["entry"] + "` FAILED:\n" + chk + "\nFix the code with edit; it stays off until it checks and the user turns it on."
334
+ set note to " · synsema check OK · ceiling when on: " + ceiling_of(l)
335
+ when l["kind"] == "exec"
336
+ set note to " · exec (no capability ceiling: the user will see that when approving)"
337
+ give "lamp '" + name + "' created (project, " + l["kind"] + ", " + text(length(l["tools"])) + " tools: " + join(apply(l["tools"], (t) => t["name"]), ", ") + ") in " + dir + " [" + join(written, ", ") + "]" + note + (when l["kind"] == "exec" then ". The command runs from the workspace root; a file of the lamp folder named in the command is resolved automatically" otherwise "") + ". It is OFF until the user turns it on: offer to do it (lamp action=enable asks them), or point them to the lamp switch of their UI. Its tools will be lamp_" + name + "_<tool>."
338
+
339
+ -- eliminar una lámpara DEL PROYECTO: borra su carpeta (no hay delete_file en el runtime → rm -rf por el shell
340
+ -- de bash.syn) y la saca del estado. Las globales (lamps/) se borran a mano: son de la instalación, no del repo.
341
+ export task remove(name)
342
+ require exec
343
+ require time
344
+ require env("LAMPSON_*")
345
+ require env("OS")
346
+ require file(".lampson")
347
+ require file(".lampson/*")
348
+ require file.read("lamps")
349
+ require file.read("lamps/*")
350
+ require file("workspace")
351
+ require file("workspace/*")
352
+ when not valid_name(name)
353
+ raise("invalid lamp name")
354
+ let found be where(all(), (l) => l["name"] == name)
355
+ when length(found) == 0
356
+ raise("no lamp named '" + name + "'")
357
+ let l be found[0]
358
+ when l["scope"] != "project"
359
+ raise("'" + name + "' is a " + l["scope"] + " lamp (" + GLOBAL_DIR + "/" + name + "): delete that folder by hand")
360
+ let dir be PROJECT_DIR + "/" + name
361
+ let is_win be env("OS", "") == "Windows_NT"
362
+ let sh be env("LAMPSON_SHELL", when is_win then "C:\\Program Files\\Git\\bin\\bash.exe" otherwise "bash")
363
+ let r be run(sh, ["-c", "rm -rf '" + dir + "'"], 30, {"cwd": "."})
364
+ when file_exists(dir + "/lamp.json")
365
+ raise("could not delete " + dir + ": " + text(r["stderr"]))
366
+ let st be load_state()
367
+ let clean be {}
368
+ each k in keys(st)
369
+ when k != name
370
+ set clean[k] to st[k]
371
+ write_file(STATE_FILE, json_encode({"enabled": clean}))
372
+ give "lamp '" + name + "' deleted (" + dir + ")"
373
+
374
+ -- resumen para UI/terminal/tool
375
+ export task summary()
376
+ require file(".lampson")
377
+ require file(".lampson/*")
378
+ require file.read("lamps")
379
+ require file.read("lamps/*")
380
+ require file("workspace")
381
+ require file("workspace/*")
382
+ require env("LAMPSON_*")
383
+ let out be []
384
+ each l in all()
385
+ set out to append(out, {"name": l["name"], "scope": l["scope"], "kind": l["kind"], "description": l["description"], "enabled": l["enabled"], "error": l["error"], "caps": when l["kind"] == "syn" and l["error"] == nothing then ceiling_of(l) otherwise "", "command": when l["kind"] == "exec" then l["command"] otherwise "", "tools": apply(l["tools"], (t) => t["name"]), "tool_specs": l["tools"], "dir": l["dir"]})
386
+ give out