lampson 0.2.8 → 0.2.9

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 (78) 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/panel.css +30 -0
  51. package/public/css/tokens.css +1 -1
  52. package/public/hub.html +11 -1
  53. package/public/icon-192.png +0 -0
  54. package/public/icon-512.png +0 -0
  55. package/public/icon-maskable-512.png +0 -0
  56. package/public/icons-src/badge.svg +12 -0
  57. package/public/icons-src/icon-maskable.svg +15 -0
  58. package/public/icons-src/icon.svg +13 -0
  59. package/public/index.html +14 -3
  60. package/public/js/agents.js +1 -1
  61. package/public/js/chat.js +1 -1
  62. package/public/js/config.js +145 -2
  63. package/public/js/core.js +4 -2
  64. package/public/js/devices.js +80 -0
  65. package/public/js/editor.js +232 -0
  66. package/public/js/login.js +78 -0
  67. package/public/js/procs.js +1 -1
  68. package/public/js/pwa.js +5 -0
  69. package/public/js/tree.js +12 -4
  70. package/public/login.html +43 -0
  71. package/public/manifest.webmanifest +16 -0
  72. package/public/offline.html +24 -0
  73. package/public/sw.js +63 -0
  74. package/public/vendor/highlight.min.js +1343 -0
  75. package/public/vendor/qrcode.js +2297 -0
  76. package/skills/computer-use/SKILL.md +178 -0
  77. package/skills/lampson/SKILL.md +5 -0
  78. package/web.syn +67 -6
package/.env.example CHANGED
@@ -1,6 +1,7 @@
1
1
  # Lampson — copia a .env (gitignored). Las claves nunca entran al proceso del programa: secret() es opaco.
2
2
 
3
- # Proveedor: openai | deepseek | kimi | groq | grok | openrouter | ollama | anthropic | minimax | glm
3
+ # Proveedor: openai | deepseek | kimi | groq | grok | openrouter | ollama | anthropic | minimax | glm | glm-coding
4
+ # glm = Z.ai pago por uso · glm-coding = GLM Coding Plan (suscripción): misma key, endpoint /coding/
4
5
  LAMPSON_PROVIDER=deepseek
5
6
  LAMPSON_API_KEY=sk-...
6
7
 
@@ -36,3 +37,11 @@ LAMPSON_API_KEY=sk-...
36
37
  # LAMPSON_WEBHOOK_URL=https://hooks.example.com/lampson # POST JSON por cada aprobación pendiente (id, mensaje, links);
37
38
  # # reenvialo a Telegram/Slack/mail con n8n, un bot o un .syn de 6 líneas
38
39
  # LAMPSON_WEBHOOK_SECRET=… # firma HMAC-SHA256 del body (X-Lampson-Signature: sha256=<hex>)
40
+
41
+ # Computer use (el agente maneja tu escritorio y navegador en segundo plano; apagado por defecto).
42
+ # Se enciende en la web (⚙ → Computer use) o con /computer-use on; estas variables lo fuerzan desde .env.
43
+ # Necesita un driver aparte que Lampson NO instala (la UI y /computer-use muestran el comando si falta).
44
+ # LAMPSON_COMPUTER_USE=1
45
+ # LAMPSON_COMPUTER_MAX_TABS=5 # pestañas distintas por corrida (los atajos que abren pestañas se rechazan siempre)
46
+ # LAMPSON_COMPUTER_GRANT_PROFILE=1 # permitir atarse a tu navegador YA ABIERTO (sesión iniciada); default: navegador aparte
47
+ # LAMPSON_COMPUTER_DRIVER=C:\ruta\cua-driver.exe # solo si el driver no está en PATH ni en la carpeta del instalador
package/README.md CHANGED
@@ -23,7 +23,7 @@ The form now shows “Wrong email or password” when the API returns 401. Tests
23
23
  npm i -g lampson
24
24
  ```
25
25
 
26
- No Node.js? One line does it all (installs what's missing, including [Synsema](https://synsema.com)):
26
+ No Node.js? One line does it all (installs what's missing, including [Synsema](https://synsema.org)):
27
27
 
28
28
  ```powershell
29
29
  # Windows (PowerShell)
@@ -43,7 +43,7 @@ lampson # in the terminal
43
43
  lampson --web # in the browser → http://127.0.0.1:8080
44
44
  ```
45
45
 
46
- The first time it asks which provider you use — DeepSeek, Anthropic, OpenAI, GLM (Z.ai), Kimi, Groq, OpenRouter,
46
+ The first time it asks which provider you use — DeepSeek, Anthropic, OpenAI, GLM (Z.ai, pay-as-you-go or Coding Plan), Kimi, Groq, OpenRouter,
47
47
  Ollama (runs on your machine, no key)… — and for your API key. That's it. Change it any time with `/model`.
48
48
 
49
49
  More ways (Docker, from source, updating): **[lampson.org/install](https://lampson.org/install)**.
@@ -55,6 +55,9 @@ More ways (Docker, from source, updating): **[lampson.org/install](https://lamps
55
55
  - **Review** — `/agent review` runs the tests and reads the diff, but never edits.
56
56
  - **Run things while you're away** — "every weekday at 9, run the tests and tell me if something broke." If a
57
57
  scheduled run hits something risky, you get a link on your phone to approve or deny it.
58
+ - **From your phone** — install it as an app, sign in with a passkey (fingerprint or face) and work on your PC or
59
+ your server from anywhere. A tunnel for a home PC, or `lampson --domain` for HTTPS on a VPS:
60
+ **[lampson.org/docs/phone](https://lampson.org/docs/phone)**.
58
61
 
59
62
  It can only touch the folder you opened it in — not your home directory, not the project next door. That isn't
60
63
  a setting: it's how the language it's written in works. Reading and editing just happen; deleting, installing,
@@ -96,7 +99,9 @@ plugins were called *lamps*; old `.lampson/lamps/` folders and `LAMP_*` variable
96
99
  Sub-agents that work in parallel · skills (`SKILL.md` procedures, anything on [skills.sh](https://skills.sh)
97
100
  works) · MCP servers with the JSON you already have · language servers for real go-to-definition · project
98
101
  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.
102
+ Markdown (a tenth of the tokens of raw HTML) · paste a screenshot and ask · computer use, opt-in: the agent
103
+ drives your desktop and browser in the background (one tab, no destructive keys) on a driver you install apart,
104
+ with an optional decision judge (Jev or local Laya) that picks the clicks in under a second.
100
105
 
101
106
  ## Learn more
102
107
 
package/bin/lampson.js CHANGED
@@ -22,7 +22,7 @@ const home = process.env.LAMPSON_HOME || path.join(os.homedir(), 'lampson');
22
22
  const marker = path.join(home, '.npm-installed');
23
23
 
24
24
  // what gets copied into the home: the runtime, never state. Keep in sync with "files" in package.json.
25
- const CODE = ['lib', 'public', 'skills', 'chat.syn', 'web.syn', 'hub.tpl.syn', 'cli.syn', 'lampson.ps1', 'lampson.sh', 'lampson.cmd', '.env.example', 'README.md', 'LICENSE'];
25
+ const CODE = ['lib', 'public', 'skills', 'chat.syn', 'web.syn', 'hub.tpl.syn', 'edge.syn', 'cli.syn', 'lampson.ps1', 'lampson.sh', 'lampson.cmd', '.env.example', 'README.md', 'LICENSE'];
26
26
 
27
27
  // copia un archivo; en unix, los scripts de shell y los .syn salen con LF aunque el paquete se haya armado
28
28
  // en Windows (un CRLF en lampson.sh rompe bash: "set: pipefail\r: invalid option")
package/chat.syn CHANGED
@@ -18,6 +18,7 @@ require random
18
18
  require env("LAMPSON_*")
19
19
  require env("OS")
20
20
  require secret("LAMPSON_*")
21
+ require judge
21
22
  require file("workspace")
22
23
  require file("workspace/*")
23
24
  require file.read("skills")
@@ -55,6 +56,8 @@ use "./lib/tools/todo.syn" as todo
55
56
  use "./lib/schedule.syn" as schedule
56
57
  use "./lib/sched_run.syn" as sched_run
57
58
  use "./lib/approvals.syn" as approvals
59
+ use "./lib/computer.syn" as computer
60
+ use "./lib/judge.syn" as jdg
58
61
 
59
62
  -- ---------- UI de terminal ----------
60
63
  -- Colores ANSI (Windows Terminal, PowerShell 7, cualquier terminal moderna). LAMPSON_NO_COLOR=1 los apaga.
@@ -350,6 +353,8 @@ let COMMANDS be [
350
353
  ["/mcp", "[add <nombre> <comando…> [--project] | remove <nombre>]", "servers MCP: listar, conectar o quitar (global: lampson/.lampson/mcp.json · proyecto: .lampson/mcp.json)"],
351
354
  ["/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
355
  ["/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"],
356
+ ["/computer-use", "[on|off]", "computer use: el agente maneja tu escritorio y navegador en segundo plano (driver aparte, apagado por defecto); sin argumento muestra el estado y, si falta el driver, cómo instalarlo"],
357
+ ["/judge", "[off|jev|laya]", "juez de decisión: un modelo chico que decide con probabilidades (hoy lo usa computer use para elegir los clics) — jev = TypeSafe en la nube (pide la API key), laya = local (pide la carpeta del checkpoint); sin argumento muestra el estado"],
353
358
  ["/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
359
  ["/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
360
  ["/out", "[n]", "resultado completo de la última tool del turno (n = contar hacia atrás: /out 2 es la anteúltima)"],
@@ -411,6 +416,14 @@ task complete_args(cmd, head, last)
411
416
  when trim(head) == "add"
412
417
  give sort_by(keys(lsp.PRESETS), (x) => x)
413
418
  give ["--project", "--ext"]
419
+ when cmd == "/computer-use"
420
+ when first
421
+ give ["on", "off"]
422
+ give []
423
+ when cmd == "/judge"
424
+ when first
425
+ give ["off", "jev", "laya"]
426
+ give []
414
427
  when cmd == "/approve"
415
428
  when first
416
429
  give apply((a) => a["id"], approvals.pending())
@@ -536,12 +549,81 @@ task show_config(cfg, ws, profile, mode, opts)
536
549
  print("")
537
550
  print(" proveedor " + cfg["provider"] + " (" + cfg["wire"] + " → " + cfg["base_url"] + ")")
538
551
  print(" modelo " + cfg["model"] + " max_tokens " + text(cfg["max_tokens"]) + " timeout " + text(floor(cfg["timeout"])) + "s")
539
- print(" agente " + profile + " tools: " + join(agents.profile(profile)["tools"], ", "))
552
+ print(" agente " + profile + " tools: " + join(agents.tool_names(agents.profile(profile)), ", "))
540
553
  print(" permisos " + mode)
541
554
  print(" límites " + text(opts["max_steps"]) + " pasos/turno · " + text(floor(opts["budget_tokens"])) + " tokens/turno · compacta a " + text(floor(opts["compact_at_tokens"])))
542
555
  print(" workspace " + ws)
556
+ print(" computer " + (when computer.enabled() then (when computer.driver() != "" then "encendido" otherwise "encendido · driver NO instalado (/computer-use)") otherwise "apagado (/computer-use on)"))
543
557
  print("")
544
558
 
559
+ -- /computer-use: estado del driver externo y cómo instalarlo (lib/computer.syn); on|off queda en config.json
560
+ task computer_lines()
561
+ let s be computer.status()
562
+ let out be []
563
+ when s["enabled"]
564
+ set out to append(out, green("computer use: encendido") + dim(" — el agente maneja tu escritorio y navegador en segundo plano"))
565
+ otherwise
566
+ set out to append(out, dim("computer use: apagado") + " " + cyan("/computer-use on") + dim(" para encenderlo — el agente maneja tu escritorio y navegador en segundo plano"))
567
+ when not s["installed"]
568
+ set out to append(out, (when s["enabled"] then red("○ driver no instalado") otherwise dim("○ driver no instalado")) + dim(" — es un paquete aparte (" + s["install"]["docs"] + "); instalalo vos en una terminal:"))
569
+ set out to append(out, " " + cyan(s["install"]["command"]))
570
+ set out to append(out, " " + cyan(s["install"]["then"]))
571
+ otherwise
572
+ let bad be where(s["checks"], (k) => k["status"] != "ok" and k["status"] != "skip")
573
+ set out to append(out, (when s["ok"] then green("● driver " + s["version"]) otherwise yellow("▲ driver " + s["version"] + " con problemas")) + dim(" " + s["path"]))
574
+ each k in bad
575
+ set out to append(out, " " + yellow(k["label"] + ": " + k["message"]))
576
+ when s["error"] != nothing
577
+ set out to append(out, " " + red(text(s["error"])))
578
+ when s["driver_error"] != nothing
579
+ set out to append(out, " " + red(text(s["driver_error"])))
580
+ set out to append(out, dim(" " + (when s["running"] then "conectado" otherwise "arranca con la primera acción del agente") + " · pestañas máx " + text(s["max_tabs"]) + " · navegador abierto del usuario: " + (when s["grant_profile"] then "permitido" otherwise "no (abre uno aparte)") + " · también en la web: ⚙ → Computer use"))
581
+ -- el juez de decisión (lib/judge.syn) lo usa la acción act; se configura aparte con /judge
582
+ set out to append(out, dim(" juez de decisión (acción act): ") + judge_short(s["judge"]))
583
+ give out
584
+
585
+ -- una línea del estado del juez (para /computer-use y /judge)
586
+ task judge_short(j)
587
+ when j["mode"] == "off"
588
+ give dim("apagado — ") + cyan("/judge jev") + dim(" o ") + cyan("/judge laya")
589
+ when j["live"] and not j["restart"]
590
+ give green("● " + j["mode"] + " activo") + dim(when j["provider_model"] != "" then " · " + j["provider_model"] otherwise "")
591
+ give yellow("▲ " + j["mode"] + " configurado, esta sesión no lo tiene") + dim(" — /exit y volvé a entrar" + (when j["mode"] == "jev" and not j["has_key"] then " · falta la API key" otherwise "") + (when j["mode"] == "laya" and j["model_setting"] == "" then " · falta la carpeta del checkpoint" otherwise ""))
592
+
593
+ -- /judge [off|jev|laya [carpeta]]: el juez de decisión, una credencial de Lampson (no de computer use). jev pide
594
+ -- la API key de TypeSafe si falta; laya, la carpeta del checkpoint. El runtime lo lee al arrancar: /exit y volver.
595
+ task judge_command(rest)
596
+ let args be where(split(rest, " "), (x) => x != "")
597
+ when length(args) > 0
598
+ let jm be lower(args[0])
599
+ when jm != "off" and jm != "jev" and jm != "laya"
600
+ print(" uso: /judge [off|jev|laya] — laya acepta la carpeta del checkpoint: /judge laya C:\\models\\laya")
601
+ give false
602
+ let st be jdg.status()
603
+ let model be when length(args) > 1 then join(slice(args, 1, length(args)), " ") otherwise nothing
604
+ when jm == "laya" and model == nothing and st["model_setting"] == ""
605
+ let ans be read_line(" carpeta del checkpoint de Laya (model.safetensors, encoder/, tokenizer/): ")
606
+ when ans == nothing or trim(ans) == ""
607
+ print(" (sin cambios: Laya necesita la carpeta)")
608
+ give false
609
+ set model to trim(ans)
610
+ let key be nothing
611
+ when jm == "jev" and not st["has_key"]
612
+ print(" " + dim("la key se guarda en lampson/.lampson/config.json (solo en esta máquina) · console.typesafe.ai"))
613
+ let k be read_line(" API key de TypeSafe: ")
614
+ when k == nothing or trim(k) == ""
615
+ print(" (sin cambios: Jev necesita la API key)")
616
+ give false
617
+ set key to trim(k)
618
+ let r be jdg.configure(jm, model, key)
619
+ when not r["ok"]
620
+ print(" " + red(r["error"]))
621
+ give false
622
+ print(" " + green("juez de decisión: " + jm) + dim(" — se aplica al volver a entrar: /exit y abrí lampson de nuevo (la web lo aplica sola al guardar en ⚙)"))
623
+ print(" juez de decisión: " + judge_short(jdg.status()))
624
+ print(dim(" un modelo chico que decide con probabilidades · hoy lo usa computer use (acción act: elige los clics sin gastar turnos del modelo)"))
625
+ give true
626
+
545
627
  -- sugerencias para un comando desconocido: por prefijo
546
628
  task suggest(input)
547
629
  let word be split(input, " ")[0]
@@ -578,8 +660,10 @@ task banner(ws, cfg, profile, mode, sid)
578
660
  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
661
  when length(ss) > 0
580
662
  set ext to append(ext, dim("lsp " + text(length(ss))))
663
+ when computer.enabled()
664
+ set ext to append(ext, when computer.driver() != "" then green("computer use") otherwise red("computer use: driver no instalado"))
581
665
  when length(ext) > 0
582
- print(" extras " + join(ext, dim(" · ")) + dim(" (/plugins /mcp /lsp)"))
666
+ print(" extras " + join(ext, dim(" · ")) + dim(" (/plugins /mcp /lsp /computer-use)"))
583
667
  let upd be update.line()
584
668
  when upd != ""
585
669
  print(" " + yellow("⬆ " + upd) + dim(" (o /update acá)"))
@@ -746,7 +830,7 @@ task grab_image(mode, path)
746
830
  set r to run("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "lib/tools/img.ps1", mode, path], 60)
747
831
  otherwise
748
832
  set r to run("bash", ["lib/tools/img.sh", mode, path], 60)
749
- let line be trim(replace_text(r["stdout"], "\r", ""))
833
+ let line be trim(replace(r["stdout"], "\r", ""))
750
834
  when line == "" or starts_with(line, "ERROR|")
751
835
  give {"error": when line == "" then trim(r["stderr"]) otherwise slice(line, 6, length(line))}
752
836
  let parts be split(line, "|")
@@ -1028,7 +1112,7 @@ while running
1028
1112
  print(trace.tail(sid, lines))
1029
1113
  otherwise when starts_with(input, "/model") or starts_with(input, "/provider") or starts_with(input, "/setup")
1030
1114
  -- un solo flujo (ver setup_wizard): proveedor → modelo (lista de la API) → key si falta; se guarda en config.json
1031
- let rest be trim(replace_re(input, "^/(model|provider|setup)", ""))
1115
+ let rest be trim(regex_replace(input, "^/(model|provider|setup)", ""))
1032
1116
  when setup_from_args(rest, cfg["provider"], cfg["model"])
1033
1117
  set cfg to provider.config()
1034
1118
  otherwise when input == "/paste"
@@ -1129,13 +1213,13 @@ while running
1129
1213
  print(memo.note_read(nm))
1130
1214
  otherwise when input == "/todo"
1131
1215
  let items be todo.load(sid)
1132
- print(when length(items) == 0 then " sin tareas (el agente las crea con la tool todo)" otherwise " " + replace_text(todo.format(items), "\n", "\n "))
1216
+ print(when length(items) == 0 then " sin tareas (el agente las crea con la tool todo)" otherwise " " + replace(todo.format(items), "\n", "\n "))
1133
1217
  otherwise when input == "/lsp" or starts_with(input, "/lsp ")
1134
1218
  let srest be trim(slice(input, 4, length(input)))
1135
1219
  when starts_with(srest, "add ")
1136
1220
  -- /lsp add typescript [--project] · /lsp add mylang cmd args… --ext .x=lang [--ext .y=lang2]
1137
1221
  let sscope be when contains(srest, "--project") then "project" otherwise "global"
1138
- let sline be trim(replace_text(srest, "--project", ""))
1222
+ let sline be trim(replace(srest, "--project", ""))
1139
1223
  let stoks be where(split(slice(sline, 4, length(sline)), " "), (x) => x != "")
1140
1224
  when length(stoks) == 0
1141
1225
  print(" uso: /lsp add <" + join(keys(lsp.PRESETS), "|") + "> [--project] · /lsp add <nombre> <comando…> --ext .ext=languageId")
@@ -1298,7 +1382,7 @@ while running
1298
1382
  when starts_with(mrest, "add ")
1299
1383
  -- /mcp add <nombre> <comando…> [--project] (env/keys: editá el mcp.json a mano, no van por terminal)
1300
1384
  let mscope be when contains(mrest, "--project") then "project" otherwise "global"
1301
- let mline be trim(replace_text(mrest, "--project", ""))
1385
+ let mline be trim(replace(mrest, "--project", ""))
1302
1386
  let mtoks be where(split(slice(mline, 4, length(mline)), " "), (x) => x != "")
1303
1387
  when length(mtoks) < 2
1304
1388
  print(" uso: /mcp add <nombre> <comando…> [--project] p.ej.: /mcp add github npx -y @modelcontextprotocol/server-github")
@@ -1342,6 +1426,26 @@ while running
1342
1426
  set mode to slice(input, 1, length(input))
1343
1427
  set opts to opts_for(profile, mode)
1344
1428
  print("permisos=" + opts["permission_mode"])
1429
+ otherwise when input == "/judge" or starts_with(input, "/judge ")
1430
+ judge_command(trim(slice(input, 6, length(input))))
1431
+ otherwise when input == "/computer-use" or starts_with(input, "/computer-use ")
1432
+ let crest be lower(trim(slice(input, 13, length(input))))
1433
+ when crest == "on" or crest == "true" or crest == "off" or crest == "false"
1434
+ let con be crest == "on" or crest == "true"
1435
+ settings.set_value("computer_use", when con then "1" otherwise "")
1436
+ computer.reset_state()
1437
+ when not con
1438
+ computer.stop_driver()
1439
+ -- el catálogo y el system prompt cambian (la tool y su skill entran o salen)
1440
+ set system_msg to system_for(profile)
1441
+ set messages to [system_msg] + slice(messages, 1, length(messages))
1442
+ set opts to opts_for(profile, mode)
1443
+ otherwise when starts_with(crest, "judge")
1444
+ print(" " + yellow("el juez ahora es una configuración propia: ") + cyan("/judge off|jev|laya"))
1445
+ otherwise when crest != ""
1446
+ print(" uso: /computer-use [on|off]")
1447
+ each l in computer_lines()
1448
+ print(" " + l)
1345
1449
  otherwise when starts_with(input, "/agent")
1346
1450
  let want be trim(slice(input, 6, length(input)))
1347
1451
  when want == ""
package/cli.syn CHANGED
@@ -3,13 +3,17 @@
3
3
  -- LAMPSON_CMD=ensure LAMPSON_WORKSPACE=<ruta> synsema run cli.syn
4
4
  -- registra/prepara el workspace, asegura el hub y su proceso → imprime UNA línea JSON {slug, port, dir, url, alive}
5
5
  -- LAMPSON_CMD=hub-status → {alive}
6
- -- LAMPSON_CMD=hub-start | hub-stop
6
+ -- LAMPSON_CMD=hub-start | hub-stop | hub-reboot (stop + start: hub y workspaces; lo lanza ⚙ → Apagar o reiniciar)
7
7
  -- LAMPSON_CMD=list
8
+ -- LAMPSON_CMD=pair [LAMPSON_PAIR_URL=https://…] código para emparejar un celular (lampson --pair dibuja el QR)
9
+ -- LAMPSON_CMD=domain LAMPSON_DOMAIN=<dominio>|off|"" [LAMPSON_EMAIL=<email>] dominio propio con HTTPS (opcional)
8
10
  -- La salida útil va en la ÚLTIMA línea (JSON) para que el launcher la parsee.
9
11
 
10
12
  require exec
11
13
  require time
12
14
  require net("127.0.0.1")
15
+ -- lampson --pair comprueba que la dirección pública (túnel o dominio) responda antes de dibujar un QR inútil
16
+ require net
13
17
  require env("LAMPSON_*")
14
18
  require env("OS")
15
19
  require env("HOME")
@@ -23,6 +27,7 @@ require file.read("hub.tpl.syn")
23
27
  require file("hub.syn")
24
28
 
25
29
  use "./lib/workspaces.syn" as ws
30
+ use "./lib/settings.syn" as settings
26
31
 
27
32
  let cmd be env("LAMPSON_CMD", "ensure")
28
33
 
@@ -39,18 +44,84 @@ when cmd == "ensure"
39
44
  ws.wait_ready(w, 10)
40
45
  set hc to ws.health(w)
41
46
  ws.touch(w["slug"])
42
- print(json_encode({"slug": w["slug"], "port": w["port"], "dir": ws.dir_of(w["slug"]), "url": "http://127.0.0.1:" + text(ws.HUB_PORT) + "/w/" + w["slug"], "alive": hc != nothing, "hub": ws.hub_alive()}))
47
+ -- judge_*: el juez de computer use para la terminal (lampson.ps1|sh lo exportan antes de chat.syn; ver settings.judge_env)
48
+ let je be settings.judge_env()
49
+ print(json_encode({"slug": w["slug"], "port": w["port"], "dir": ws.dir_of(w["slug"]), "url": "http://127.0.0.1:" + text(ws.HUB_PORT) + "/w/" + w["slug"], "alive": hc != nothing, "hub": ws.hub_alive(), "judge_provider": when contains(je, "SYNSEMA_JUDGE_PROVIDER") then je["SYNSEMA_JUDGE_PROVIDER"] otherwise "", "judge_model": when contains(je, "SYNSEMA_JUDGE_MODEL") then je["SYNSEMA_JUDGE_MODEL"] otherwise "", "judge_key": when contains(je, "TYPESAFE_API_KEY") then je["TYPESAFE_API_KEY"] otherwise ""}))
43
50
  otherwise when cmd == "hub-status"
44
51
  print(json_encode({"alive": ws.hub_alive(), "workspaces": ws.summary()}))
45
52
  otherwise when cmd == "hub-start"
46
53
  when not ws.hub_alive()
47
54
  ws.start_hub()
55
+ when ws.domain_cfg() != nothing and not ws.edge_alive()
56
+ ws.start_edge()
48
57
  print(json_encode({"alive": true}))
49
- otherwise when cmd == "hub-stop"
50
- each w in ws.all()
51
- ws.stop_ws(w)
52
- ws.stop_hub()
53
- print(json_encode({"alive": false}))
58
+ otherwise when cmd == "hub-stop" or cmd == "hub-reboot"
59
+ -- desde ⚙ llega desacoplado (workspaces.cli_later): darle al hub tiempo de contestar antes de pararlo
60
+ when env("LAMPSON_DETACHED", "") != ""
61
+ sleep(1.5)
62
+ ws.stop_all()
63
+ when cmd == "hub-reboot"
64
+ sleep(2)
65
+ ws.start_hub()
66
+ when ws.domain_cfg() != nothing
67
+ ws.start_edge()
68
+ print(json_encode({"alive": cmd == "hub-reboot"}))
69
+ otherwise when cmd == "domain"
70
+ let arg be trim(env("LAMPSON_DOMAIN", ""))
71
+ when arg == ""
72
+ -- estado
73
+ let cfg be ws.domain_cfg()
74
+ when cfg == nothing
75
+ print("sin dominio propio: lampson se usa en http://127.0.0.1:" + text(ws.HUB_PORT) + " (o por túnel).")
76
+ print("para usar el tuyo: lampson --domain lampson.midominio.com --email vos@midominio.com")
77
+ otherwise
78
+ print("dominio: https://" + cfg["domain"] + " (" + (when ws.edge_alive() then "● HTTPS activo" otherwise "○ el edge no está escuchando — mirá .lampson/edge.log") + ")")
79
+ print("certificado de Let's Encrypt a nombre de " + cfg["email"] + " · quitar: lampson --domain off")
80
+ print(json_encode({"domain": when cfg == nothing then nothing otherwise cfg["domain"], "edge": ws.edge_alive()}))
81
+ otherwise when lower(arg) == "off"
82
+ let had be ws.clear_domain()
83
+ ws.stop_edge()
84
+ ws.regen_hub()
85
+ when ws.hub_alive()
86
+ ws.stop_hub()
87
+ sleep(2)
88
+ ws.start_hub()
89
+ print(when had then "dominio quitado: el hub vuelve a escuchar en :" + text(ws.HUB_PORT) + " como antes." otherwise "no había dominio configurado.")
90
+ print(json_encode({"domain": nothing}))
91
+ otherwise
92
+ let cfg be nothing
93
+ try
94
+ set cfg to ws.set_domain(arg, env("LAMPSON_EMAIL", ""))
95
+ recover err
96
+ print("✗ " + regex_replace(text(err), "^.*?:\d+:\d+: ", ""))
97
+ print(json_encode({"error": text(err)}))
98
+ when cfg != nothing
99
+ -- el hub pasa a 127.0.0.1 (regen) y el edge arranca con el dominio nuevo
100
+ ws.regen_hub()
101
+ ws.stop_edge()
102
+ when ws.hub_alive()
103
+ ws.stop_hub()
104
+ sleep(2)
105
+ ws.start_hub()
106
+ ws.start_edge()
107
+ print("dominio: https://" + cfg["domain"] + " — pidiendo el certificado a Let's Encrypt…")
108
+ -- esperar a que el edge escuche (el certificado se pide al arrancar; tarda unos segundos)
109
+ let deadline be now() + 45
110
+ while now() < deadline and not ws.edge_alive()
111
+ sleep(1)
112
+ let up be ws.edge_alive()
113
+ let tail be ws.edge_log(12)
114
+ when up
115
+ print("● HTTPS activo en https://" + cfg["domain"] + " — emparejá el celular desde «dispositivos» (el QR ya usa esta URL).")
116
+ otherwise
117
+ print("○ el edge no quedó escuchando. Últimas líneas de .lampson/edge.log:")
118
+ each l in tail
119
+ print(" " + l)
120
+ print("revisá: 1) el DNS de " + cfg["domain"] + " apunta a la IP de este servidor (registro A/AAAA);")
121
+ print(" 2) los puertos 80 y 443 están abiertos en el firewall;")
122
+ print(" 3) en Linux sin root, el binario puede usar puertos bajos: sudo setcap cap_net_bind_service=+ep $(command -v synsema)")
123
+ print("el hub reintenta solo cada 10 min; `lampson --domain off` vuelve a como estaba.")
124
+ print(json_encode({"domain": cfg["domain"], "edge": up}))
54
125
  otherwise when cmd == "hub-restart"
55
126
  -- lanzado desacoplado por workspaces.restart_hub_later: espera a que el hub termine lo que estaba haciendo,
56
127
  -- lo para y lo vuelve a arrancar con el hub.syn regenerado (bajo systemd, Restart=always ya lo levanta solo)
@@ -60,6 +131,71 @@ otherwise when cmd == "hub-restart"
60
131
  when not ws.hub_alive()
61
132
  ws.start_hub()
62
133
  print(json_encode({"restarted": true}))
134
+ otherwise when cmd == "ws-restart"
135
+ -- lanzado desacoplado por workspaces.restart_ws_later desde el propio workspace: espera a que conteste el
136
+ -- request que lo pidió, lo para y lo arranca de nuevo (con el entorno del juez al día)
137
+ let w be ws.get(env("LAMPSON_WS", ""))
138
+ when w == nothing
139
+ raise("unknown workspace " + env("LAMPSON_WS", ""))
140
+ sleep(1.5)
141
+ ws.stop_ws(w)
142
+ let deadline be now() + 8
143
+ while now() < deadline and ws.health(w) != nothing
144
+ sleep(0.4)
145
+ ws.start_ws(w)
146
+ ws.wait_ready(w, 15)
147
+ print(json_encode({"restarted": w["slug"], "alive": ws.health(w) != nothing}))
148
+ otherwise when cmd == "pair"
149
+ -- lampson --pair [URL]: emparejar un celular desde la terminal — en un VPS por SSH no hay navegador en «esta
150
+ -- PC». Se le pide el código al hub por 127.0.0.1 (el emparejamiento solo se inicia desde local); el lanzador
151
+ -- dibuja el QR con lib/tools/qr.js. Con URL, primero la guarda como pública (la del túnel o el dominio).
152
+ when not ws.hub_alive()
153
+ ws.start_hub()
154
+ let deadline be now() + 15
155
+ while now() < deadline and not ws.hub_alive()
156
+ sleep(0.5)
157
+ let base be "http://127.0.0.1:" + text(ws.HUB_PORT)
158
+ let hdr be {"Content-Type": "application/json"}
159
+ let bad be nothing
160
+ let want be trim(env("LAMPSON_PAIR_URL", ""))
161
+ when want != ""
162
+ let ru be http_post(base + "/api/auth/public-url", json_encode({"url": want}), hdr, 15)
163
+ when contains(ru, "error")
164
+ set bad to "el hub no responde en " + base
165
+ otherwise when status of ru != 200
166
+ set bad to "no pude guardar la URL: " + slice(body of ru, 0, 300)
167
+ -- ¿la dirección pública responde? Un túnel rápido de cloudflare (*.trycloudflare.com) muere al cerrar
168
+ -- cloudflared y la dirección guardada queda vieja: el QR llevaría a «sin conexión» (2026-09-28, en el celular)
169
+ when bad == nothing
170
+ let cur be http_get(base + "/api/auth/public-url", {"timeout": 10})
171
+ let pub0 be ""
172
+ when not contains(cur, "error")
173
+ when status of cur == 200
174
+ set pub0 to text(json_decode(body of cur)["public_url"])
175
+ when pub0 != "" and pub0 != "nothing"
176
+ let probe be http_get(pub0 + "/api/auth/me", {"timeout": 8})
177
+ let alive be false
178
+ when not contains(probe, "error")
179
+ set alive to status of probe == 200 and contains(body of probe, "authenticated")
180
+ when not alive
181
+ let tunnel be contains(pub0, "trycloudflare.com")
182
+ set bad to "la dirección pública " + pub0 + " no responde" + (when tunnel then ": ese túnel de cloudflare ya no existe (mueren al cerrar cloudflared y cada uno trae una dirección nueva). Levantá uno con «cloudflared tunnel --url http://localhost:8080» y corré lampson --pair <la https nueva que imprime>" otherwise " — ¿está el dominio apuntando a este servidor y el edge HTTPS arriba? (lampson --domain muestra el estado)")
183
+ when bad == nothing
184
+ let r be http_post(base + "/api/auth/pair", "{}", hdr, 15)
185
+ when contains(r, "error")
186
+ set bad to "el hub no responde en " + base + " — lampson --hub start"
187
+ otherwise when status of r != 200
188
+ set bad to "el hub rechazó el pedido (" + text(status of r) + "): " + slice(body of r, 0, 300)
189
+ otherwise
190
+ let j be json_decode(body of r)
191
+ let pub be text(j["public_url"])
192
+ when pub == "" or pub == "nothing"
193
+ set bad to "falta la dirección pública por la que entra el celular: lampson --pair https://tu-dominio (o configurá el dominio con lampson --domain)"
194
+ otherwise
195
+ print(json_encode({"link": pub + "/pair#" + text(j["code"]), "expires_in": j["expires_in"], "public_url": pub}))
196
+ when bad != nothing
197
+ print("✗ " + bad)
198
+ print(json_encode({"error": bad}))
63
199
  otherwise when cmd == "list"
64
200
  each w in ws.summary()
65
201
  print((when w["alive"] then "● " otherwise "○ ") + w["slug"] + " " + w["path"] + " :" + text(w["port"]) + " " + w["policy"])
package/edge.syn ADDED
@@ -0,0 +1,26 @@
1
+ -- edge.syn — HTTPS con TU dominio delante del hub. OPCIONAL: solo corre si configuraste uno (`lampson --domain …`).
2
+ --
3
+ -- Synsema termina TLS acá (certificado gratis de Let's Encrypt, renovación automática, :80 → :443) y pasa todo al hub
4
+ -- en 127.0.0.1:8080, que sigue igual que siempre. El edge agrega X-Forwarded-For/-Proto/-Host y el hub, que confía
5
+ -- en loopback (`trust proxy`), los ve: para él es una visita REMOTA con el dominio público, así que pide la passkey
6
+ -- de un dispositivo emparejado (lib/origin.syn, lib/auth.syn). El edge NO confía en nadie: lo que un cliente mande
7
+ -- como X-Forwarded-* lo borra el motor. SSE y WebSocket (chat, terminal) cruzan el edge.
8
+ --
9
+ -- Lo lanza el supervisor del hub (lib/workspaces.syn start_edge) con:
10
+ -- synsema serve edge.syn --port 443 --domain <dominio> --tls-auto <email>
11
+ -- El archivo no cambia entre máquinas: el dominio y el email son flags.
12
+
13
+ intent: "Lampson edge: TLS for the user's own domain in front of the local hub"
14
+
15
+ require serve(443)
16
+ require net("127.0.0.1")
17
+
18
+ serve on 443
19
+ max_streams 200
20
+ -- "/*path" no matchea "/": la raíz va aparte, por método
21
+ route "GET /"
22
+ proxy to "http://127.0.0.1:8080"
23
+ route "GET /*path"
24
+ proxy to "http://127.0.0.1:8080"
25
+ route "POST /*path"
26
+ proxy to "http://127.0.0.1:8080"