lampson 0.2.8 → 0.2.10

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 (81) hide show
  1. package/.env.example +10 -1
  2. package/README.md +8 -3
  3. package/bin/lampson.js +1 -1
  4. package/chat.syn +111 -7
  5. package/cli.syn +143 -7
  6. package/edge.syn +26 -0
  7. package/hub.tpl.syn +127 -6
  8. package/lampson.ps1 +43 -2
  9. package/lampson.sh +40 -2
  10. package/lib/agents.syn +27 -6
  11. package/lib/approvals.syn +2 -2
  12. package/lib/auth.syn +274 -0
  13. package/lib/computer.syn +2000 -0
  14. package/lib/computer_judge.syn +410 -0
  15. package/lib/fs.syn +19 -3
  16. package/lib/git.syn +2 -2
  17. package/lib/judge.syn +78 -0
  18. package/lib/line.syn +2 -2
  19. package/lib/loop.syn +36 -4
  20. package/lib/lsp.syn +4 -4
  21. package/lib/mcp.syn +20 -4
  22. package/lib/md.syn +11 -11
  23. package/lib/origin.syn +67 -0
  24. package/lib/permission.syn +21 -2
  25. package/lib/prompt.syn +9 -1
  26. package/lib/provider.syn +70 -20
  27. package/lib/schedule.syn +12 -12
  28. package/lib/session.syn +5 -5
  29. package/lib/settings.syn +65 -1
  30. package/lib/skills.syn +22 -6
  31. package/lib/tools/bash.syn +1 -1
  32. package/lib/tools/common.syn +1 -1
  33. package/lib/tools/fetch.syn +98 -71
  34. package/lib/tools/grep.syn +1 -1
  35. package/lib/tools/memo.syn +3 -3
  36. package/lib/tools/proc.syn +38 -20
  37. package/lib/tools/qr.js +26 -0
  38. package/lib/tools/url.syn +5 -5
  39. package/lib/tools.syn +23 -2
  40. package/lib/trace.syn +2 -2
  41. package/lib/tree.syn +29 -13
  42. package/lib/update.syn +1 -1
  43. package/lib/workspaces.syn +202 -36
  44. package/package.json +3 -2
  45. package/public/apple-touch-icon.png +0 -0
  46. package/public/badge-96.png +0 -0
  47. package/public/css/code.css +48 -0
  48. package/public/css/hub.css +9 -0
  49. package/public/css/login.css +16 -0
  50. package/public/css/mobile.css +95 -0
  51. package/public/css/panel.css +30 -0
  52. package/public/css/tokens.css +1 -1
  53. package/public/hub.html +11 -1
  54. package/public/icon-192.png +0 -0
  55. package/public/icon-512.png +0 -0
  56. package/public/icon-maskable-512.png +0 -0
  57. package/public/icons-src/badge.svg +12 -0
  58. package/public/icons-src/icon-maskable.svg +15 -0
  59. package/public/icons-src/icon.svg +13 -0
  60. package/public/index.html +16 -3
  61. package/public/js/agents.js +1 -1
  62. package/public/js/chat.js +1 -1
  63. package/public/js/config.js +145 -2
  64. package/public/js/core.js +4 -2
  65. package/public/js/devices.js +80 -0
  66. package/public/js/editor.js +232 -0
  67. package/public/js/login.js +78 -0
  68. package/public/js/mobile.js +122 -0
  69. package/public/js/procs.js +1 -1
  70. package/public/js/pwa.js +5 -0
  71. package/public/js/terminal.js +1 -1
  72. package/public/js/tree.js +12 -4
  73. package/public/login.html +43 -0
  74. package/public/manifest.webmanifest +16 -0
  75. package/public/offline.html +24 -0
  76. package/public/sw.js +63 -0
  77. package/public/vendor/highlight.min.js +1343 -0
  78. package/public/vendor/qrcode.js +2297 -0
  79. package/skills/computer-use/SKILL.md +178 -0
  80. package/skills/lampson/SKILL.md +5 -0
  81. package/web.syn +67 -6
@@ -38,7 +38,7 @@ task path(name, ext)
38
38
  give DIR + "/" + name + "." + ext
39
39
 
40
40
  task tail_lines(content, n)
41
- let lines be split(replace_text(content, "\r", ""), "\n")
41
+ let lines be split(replace(content, "\r", ""), "\n")
42
42
  when length(lines) > n
43
43
  set lines to slice(lines, length(lines) - n, length(lines))
44
44
  give join(lines, "\n")
@@ -211,7 +211,7 @@ export task resume_note(sid)
211
211
  give "[harness] This session was resumed after a lampson restart. Managed processes do NOT survive a restart — anything this conversation started earlier is gone unless listed as running below. Current real state:\n" + join(lines, "\n") + "\nOld logs remain in " + DIR + "/<name>.log. Start a process again with the `process` tool only if it is needed now; do not assume any server is up."
212
212
 
213
213
  task first_line_of(s)
214
- let t be replace_text(text(s), "\r", "")
214
+ let t be replace(text(s), "\r", "")
215
215
  give split(t, "\n")[0]
216
216
 
217
217
  -- {run, session} actuales (blackboard "lampson:run" / "lampson:session"; vacíos fuera de un entry)
@@ -246,43 +246,61 @@ export task list()
246
246
  give []
247
247
  give out
248
248
 
249
- -- Puertos TCP en escucha (cualquier proceso, gestionado o no): [{port, pid, name, command}] — via proc.sh ports
250
- -- (Windows: netstat+tasklist · Linux: ss · macOS: lsof). Un servidor huérfano no aparece en list(); acá sí.
251
- export task listeners()
249
+ -- Todos los puertos TCP en escucha con su dueño, en crudo: [{port, pid, name}] — UNA llamada a proc.sh ports
250
+ -- (Windows: netstat+tasklist · Linux: ss · macOS: lsof), sin filtrar y sin la línea de comando. Para encontrar y
251
+ -- parar los procesos de Lampson por su puerto (lib/workspaces.syn): el edge de --domain escucha en :443.
252
+ export task port_owners()
252
253
  require exec
253
254
  require env("LAMPSON_*")
254
255
  require env("OS")
255
- require file(".lampson")
256
- require file(".lampson/*")
257
256
  let out be []
258
257
  try
259
258
  let r be run(shell(), ["lib/tools/proc.sh", "ports"], 20, {"cwd": "."})
260
- let seen be {}
261
- each line in split(replace_text(r["stdout"], "\r", ""), "\n")
259
+ each line in split(replace(r["stdout"], "\r", ""), "\n")
262
260
  let parts be where(split(trim(line), " "), (x) => x != "")
263
- when length(parts) >= 2
264
- let port be floor(number(parts[0]))
265
- let nm be when length(parts) >= 3 then parts[2] otherwise "?"
266
- when port >= 1024 and port < 49152 and not contains(SYSTEM_PROCS, lower(nm)) and not contains(seen, text(port))
267
- set seen[text(port)] to true
268
- set out to append(out, {"port": port, "pid": floor(number(parts[1])), "name": nm, "command": ""})
261
+ when length(parts) >= 2 and matches(parts[0], "[0-9]+") and matches(parts[1], "[0-9]+")
262
+ set out to append(out, {"port": floor(number(parts[0])), "pid": floor(number(parts[1])), "name": when length(parts) >= 3 then parts[2] otherwise "?"})
269
263
  recover err
270
264
  give out
265
+ give out
266
+
267
+ -- Puertos TCP en escucha que le interesan al usuario y al agente (panel «Puertos», tool process): sin los del
268
+ -- sistema ni los efímeros, uno por puerto, con la línea de comando: [{port, pid, name, command}]. Un servidor
269
+ -- huérfano no aparece en list(); acá sí.
270
+ export task listeners()
271
+ require exec
272
+ require env("LAMPSON_*")
273
+ require env("OS")
274
+ require file(".lampson")
275
+ require file(".lampson/*")
276
+ let out be []
277
+ let seen be {}
278
+ each l in port_owners()
279
+ when l["port"] >= 1024 and l["port"] < 49152 and not contains(SYSTEM_PROCS, lower(l["name"])) and not contains(seen, text(l["port"]))
280
+ set seen[text(l["port"])] to true
281
+ set out to append(out, {"port": l["port"], "pid": l["pid"], "name": l["name"], "command": ""})
271
282
  -- la línea de comando dice QUÉ es (next dev, un MCP server de otra herramienta, synsema serve…); un run por pid
272
283
  let cmds be {}
273
284
  let enriched be []
274
285
  each l in out
275
286
  let key be text(l["pid"])
276
287
  when not contains(cmds, key)
277
- try
278
- let r be run(shell(), ["lib/tools/proc.sh", "info", key], 15, {"cwd": "."})
279
- set cmds[key] to trim(replace_text(r["stdout"], "\n", " "))
280
- recover err
281
- set cmds[key] to ""
288
+ set cmds[key] to command_of(l["pid"])
282
289
  set l["command"] to when length(cmds[key]) > 300 then slice(cmds[key], 0, 300) + "…" otherwise cmds[key]
283
290
  set enriched to append(enriched, l)
284
291
  give sort_by(enriched, (x) => x["port"])
285
292
 
293
+ -- la línea de comando de un pid ("" si no se puede saber) — via proc.sh info
294
+ export task command_of(pid)
295
+ require exec
296
+ require env("LAMPSON_*")
297
+ require env("OS")
298
+ try
299
+ let r be run(shell(), ["lib/tools/proc.sh", "info", text(floor(pid))], 15, {"cwd": "."})
300
+ give trim(replace(r["stdout"], "\n", " "))
301
+ recover err
302
+ give ""
303
+
286
304
  -- matar un pid ajeno (huérfano de otra herramienta): taskkill /T o kill_tree, via proc.sh
287
305
  export task kill_pid(pid)
288
306
  require exec
@@ -0,0 +1,26 @@
1
+ // qr.js <texto> — dibuja un QR en la terminal (lampson --pair): medio bloque por fila de módulos (▀ ▄ █), con
2
+ // colores FORZADOS (negro sobre blanco) para que la cámara lo lea igual con tema claro u oscuro. Usa la misma
3
+ // librería que el QR de la web (public/vendor/qrcode.js).
4
+ const path = require('path');
5
+ const qrcode = require(path.join(__dirname, '..', '..', 'public', 'vendor', 'qrcode.js'));
6
+
7
+ const text = process.argv[2];
8
+ if (!text) { console.error('uso: node qr.js <texto>'); process.exit(2); }
9
+ const qr = qrcode(0, 'M');
10
+ qr.addData(text);
11
+ qr.make();
12
+
13
+ const n = qr.getModuleCount();
14
+ const quiet = 3; // margen claro alrededor (la cámara lo necesita para encontrar el código)
15
+ const dark = (r, c) => r >= 0 && c >= 0 && r < n && c < n && qr.isDark(r, c);
16
+ const ON = '\x1b[30;107m', OFF = '\x1b[0m'; // texto negro, fondo blanco brillante
17
+ const lines = [];
18
+ for (let r = -quiet; r < n + quiet; r += 2) {
19
+ let line = ' ' + ON;
20
+ for (let c = -quiet; c < n + quiet; c++) {
21
+ const top = dark(r, c), bottom = dark(r + 1, c);
22
+ line += top && bottom ? '█' : top ? '▀' : bottom ? '▄' : ' ';
23
+ }
24
+ lines.push(line + OFF);
25
+ }
26
+ process.stdout.write(lines.join('\n') + '\n');
package/lib/tools/url.syn CHANGED
@@ -25,7 +25,7 @@ export task parse(raw)
25
25
  give fail("missing URL")
26
26
  let u be trim(text(raw))
27
27
  -- "https:// host" — el modelo a veces mete un espacio tras el esquema (hermes normalize_url_for_request)
28
- set u to replace_re(u, "^([A-Za-z][A-Za-z0-9+.-]*://)\\s+", "\\1")
28
+ set u to regex_replace(u, "^([A-Za-z][A-Za-z0-9+.-]*://)\\s+", "\\1")
29
29
  when u == ""
30
30
  give fail("missing URL")
31
31
  when length(u) > 2048
@@ -138,14 +138,14 @@ export task host_class(host)
138
138
  -- %XX → carácter (para inspeccionar una URL con secretos codificados)
139
139
  task percent_decode(s)
140
140
  let out be s
141
- each m in find_all(s, "%[0-9A-Fa-f]{2}")
141
+ each m in regex_find_all(s, "%[0-9A-Fa-f]{2}")
142
142
  let ch be nothing
143
143
  try
144
144
  set ch to json_decode("\"\\u00" + slice(m, 1, 3) + "\"")
145
145
  recover e
146
146
  set ch to nothing
147
147
  when ch != nothing
148
- set out to replace_text(out, m, ch)
148
+ set out to replace(out, m, ch)
149
149
  give out
150
150
 
151
151
  export task sensitive(raw)
@@ -153,7 +153,7 @@ export task sensitive(raw)
153
153
  give nothing
154
154
  let u be text(raw)
155
155
  let decoded be percent_decode(u)
156
- when length(find_all(u, SECRET_RE)) > 0 or length(find_all(decoded, SECRET_RE)) > 0
156
+ when length(regex_find_all(u, SECRET_RE)) > 0 or length(regex_find_all(decoded, SECRET_RE)) > 0
157
157
  give "an API key or token"
158
158
  let p be parse(u)
159
159
  when not p["ok"]
@@ -212,7 +212,7 @@ export task resolve(base, href)
212
212
 
213
213
  -- nombre de archivo estable para la caché/spill de una URL: fetch-<host>-<digest>
214
214
  export task slug(host)
215
- let s be replace_re(lower(text(host)), "[^a-z0-9._-]", "-")
215
+ let s be regex_replace(lower(text(host)), "[^a-z0-9._-]", "-")
216
216
  when length(s) > 60
217
217
  set s to slice(s, 0, 60)
218
218
  when s == ""
package/lib/tools.syn CHANGED
@@ -29,6 +29,26 @@ use "./mcp.syn" as mcp
29
29
  use "./plugins.syn" as plugins
30
30
  use "./lsp.syn" as lsp
31
31
  use "./schedule.syn" as schedule
32
+ use "./computer.syn" as computer
33
+
34
+ -- la tool computer_use (lib/computer.syn): el escritorio y el navegador del usuario en segundo plano, sobre un
35
+ -- driver externo. Está en el registry siempre, pero al catálogo del modelo solo entra cuando está encendida y el
36
+ -- driver existe (agents.tool_names). Los args llegan planos del JSON del modelo y van a computer.call como map.
37
+ -- OJO: cada propiedad del SPEC tiene que ser un parámetro acá — un arg que no está da "unknown parameter" antes de
38
+ -- llegar a computer.call (2026-09-06: delivery_mode faltaba y el modelo no pudo apretar Enter en Chrome). Lo cubre
39
+ -- un test de unit.test.syn que recorre las propiedades del SPEC.
40
+ task computer_tool(action, pid, window_id, element, x, y, text, key, keys, modifiers, direction, amount, from_x, from_y, to_x, to_y, x1, y1, x2, y2, value, name, path, url, target_id, tab_id, ref, query, screenshot, max_elements, replace, delta_y, delta_x, profile, seconds, button, delivery_mode, option, app, capture_after, fields, goal, steps, enter)
41
+ require exec
42
+ require time
43
+ require env("LAMPSON_*")
44
+ require env("OS")
45
+ require file.read(".lampson")
46
+ require file.read(".lampson/*")
47
+ let a be {}
48
+ each e in [["pid", pid], ["window_id", window_id], ["element", element], ["x", x], ["y", y], ["text", text], ["key", key], ["keys", keys], ["modifiers", modifiers], ["direction", direction], ["amount", amount], ["from_x", from_x], ["from_y", from_y], ["to_x", to_x], ["to_y", to_y], ["x1", x1], ["y1", y1], ["x2", x2], ["y2", y2], ["value", value], ["name", name], ["path", path], ["url", url], ["target_id", target_id], ["tab_id", tab_id], ["ref", ref], ["query", query], ["screenshot", screenshot], ["max_elements", max_elements], ["replace", replace], ["delta_y", delta_y], ["delta_x", delta_x], ["profile", profile], ["seconds", seconds], ["button", button], ["delivery_mode", delivery_mode], ["option", option], ["app", app], ["capture_after", capture_after], ["fields", fields], ["goal", goal], ["steps", steps], ["enter", enter]]
49
+ when e[1] != nothing
50
+ set a[e[0]] to e[1]
51
+ give computer.call(action, a)
32
52
 
33
53
  -- la task de la tool skill (su SPEC está en tools/skill.syn)
34
54
  task skill_tool(name, action, source, scope)
@@ -265,10 +285,11 @@ export task registry()
265
285
  "mcp": mcp_tool,
266
286
  "plugin": plugin_tool,
267
287
  "lsp": lsp_tool,
268
- "schedule": schedule_tool
288
+ "schedule": schedule_tool,
289
+ "computer_use": computer_tool
269
290
  }
270
291
 
271
- 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_fetch.SPEC, t_bash.SPEC, t_process.SPEC, t_memo.SPEC, t_todo.SPEC, t_skill.SPEC, MCP_SPEC, PLUGIN_SPEC, SCHEDULE_SPEC]
292
+ 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_fetch.SPEC, t_bash.SPEC, t_process.SPEC, t_memo.SPEC, t_todo.SPEC, t_skill.SPEC, MCP_SPEC, PLUGIN_SPEC, SCHEDULE_SPEC, computer.SPEC]
272
293
 
273
294
  -- Subconjuntos (para perfiles de agente): registry/catálogo filtrados por nombre.
274
295
  export task registry_subset(names)
package/lib/trace.syn CHANGED
@@ -16,7 +16,7 @@ task path(sid)
16
16
  give DIR + "/" + sid + ".log"
17
17
 
18
18
  task one_line(s, max)
19
- let t be replace_text(replace_text(text(s), "\r", ""), "\n", " ⏎ ")
19
+ let t be replace(replace(text(s), "\r", ""), "\n", " ⏎ ")
20
20
  when length(t) > max
21
21
  give slice(t, 0, max) + "…"
22
22
  give t
@@ -105,7 +105,7 @@ export task tail(sid, n)
105
105
  require file(".lampson")
106
106
  require file(".lampson/*")
107
107
  try
108
- let lines be split(replace_text(read_file(path(sid)), "\r", ""), "\n")
108
+ let lines be split(replace(read_file(path(sid)), "\r", ""), "\n")
109
109
  when length(lines) > n
110
110
  set lines to slice(lines, length(lines) - n, length(lines))
111
111
  give join(lines, "\n")
package/lib/tree.syn CHANGED
@@ -3,35 +3,38 @@ use "./tools/common.syn" as c
3
3
 
4
4
  let MAX_ENTRIES be 2000
5
5
 
6
- task walk(dir, depth, max_depth, state)
6
+ -- n = entradas contadas hasta ahora en todo el árbol; da {items, n} (listas y mapas son valores: el contador vuelve con give)
7
+ task walk(dir, depth, max_depth, n)
7
8
  let out be []
8
9
  when depth > max_depth
9
- give out
10
+ give {"items": out, "n": n}
10
11
  let entries be []
11
12
  try
12
13
  set entries to list_dir(dir)
13
14
  recover err
14
- give out
15
+ give {"items": out, "n": n}
15
16
  each e in entries
16
- when state["n"] >= MAX_ENTRIES
17
- give out
17
+ when n >= MAX_ENTRIES
18
+ give {"items": out, "n": n}
18
19
  let rel be c.unws(dir + "/" + e["name"])
19
20
  when e["is_dir"]
20
21
  when not contains(c.IGNORED_DIRS, e["name"])
21
- set state["n"] to state["n"] + 1
22
- set out to append(out, {"name": e["name"], "path": rel, "is_dir": true, "children": walk(dir + "/" + e["name"], depth + 1, max_depth, state)})
22
+ set n to n + 1
23
+ let sub be walk(dir + "/" + e["name"], depth + 1, max_depth, n)
24
+ set n to sub["n"]
25
+ set out to append(out, {"name": e["name"], "path": rel, "is_dir": true, "children": sub["items"]})
23
26
  otherwise
24
- set state["n"] to state["n"] + 1
27
+ set n to n + 1
25
28
  set out to append(out, {"name": e["name"], "path": rel, "is_dir": false, "size": e["size"]})
26
29
  -- carpetas primero
27
- give where(out, (x) => x["is_dir"]) + where(out, (x) => not x["is_dir"])
30
+ give {"items": where(out, (x) => x["is_dir"]) + where(out, (x) => not x["is_dir"]), "n": n}
28
31
 
29
32
  -- árbol anidado: [{name, path, is_dir, size?, children?}]
30
33
  export task tree(max_depth)
31
34
  require file.read("workspace")
32
35
  require file.read("workspace/*")
33
- let state be {"n": 0}
34
- give {"entries": walk(c.ROOT, 0, max_depth, state), "count": state["n"], "truncated": state["n"] >= MAX_ENTRIES}
36
+ let w be walk(c.ROOT, 0, max_depth, 0)
37
+ give {"entries": w["items"], "count": w["n"], "truncated": w["n"] >= MAX_ENTRIES}
35
38
 
36
39
  -- render de texto para la terminal (2 niveles, compacto)
37
40
  export task render(max_depth, max_lines)
@@ -55,5 +58,18 @@ export task file_content(path)
55
58
  require file.read("workspace")
56
59
  require file.read("workspace/*")
57
60
  let real be c.ws(path)
58
- let content be read_file(real)
59
- give {"path": c.unws(real), "content": c.truncate(content, 200000), "lines": length(split(content, "\n"))}
61
+ let rel be c.unws(real)
62
+ let info be file_info(real)
63
+ when not info["exists"]
64
+ raise("\"" + rel + "\" does not exist")
65
+ when info["is_dir"]
66
+ raise("\"" + rel + "\" is a folder")
67
+ -- binarios (sqlite, imágenes, fuentes…): read_file no puede decodificarlos como UTF-8 y el runtime lo reporta
68
+ -- como "File not found" (engañoso; visto 2026-09-05 con un dev.db de Prisma). En vez de un error, {binary: true}
69
+ -- y el visor muestra su aviso, sin ensuciar el chat.
70
+ try
71
+ let content be read_file(real)
72
+ -- truncated: el editor de la web lo abre en solo lectura (guardar el recorte destruiría el archivo)
73
+ give {"path": rel, "content": c.truncate(content, 200000), "lines": length(split(content, "\n")), "truncated": length(content) > 200000, "binary": false, "size": info["size"]}
74
+ recover err
75
+ give {"path": rel, "content": "", "lines": 0, "truncated": false, "binary": true, "size": info["size"], "error": text(err)}
package/lib/update.syn CHANGED
@@ -12,7 +12,7 @@ task run_git(args, secs)
12
12
  let r be run("git", args, secs)
13
13
  when r["exit_code"] != 0
14
14
  give nothing
15
- give trim(replace_text(r["stdout"], "\r", ""))
15
+ give trim(replace(r["stdout"], "\r", ""))
16
16
  recover err
17
17
  give nothing
18
18