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.
- package/.env.example +10 -1
- package/README.md +8 -3
- package/bin/lampson.js +1 -1
- package/chat.syn +111 -7
- package/cli.syn +143 -7
- package/edge.syn +26 -0
- package/hub.tpl.syn +127 -6
- package/lampson.ps1 +43 -2
- package/lampson.sh +40 -2
- package/lib/agents.syn +27 -6
- package/lib/approvals.syn +2 -2
- package/lib/auth.syn +274 -0
- package/lib/computer.syn +2000 -0
- package/lib/computer_judge.syn +410 -0
- package/lib/fs.syn +19 -3
- package/lib/git.syn +2 -2
- package/lib/judge.syn +78 -0
- package/lib/line.syn +2 -2
- package/lib/loop.syn +36 -4
- package/lib/lsp.syn +4 -4
- package/lib/mcp.syn +20 -4
- package/lib/md.syn +11 -11
- package/lib/origin.syn +67 -0
- package/lib/permission.syn +21 -2
- package/lib/prompt.syn +9 -1
- package/lib/provider.syn +70 -20
- package/lib/schedule.syn +12 -12
- package/lib/session.syn +5 -5
- package/lib/settings.syn +65 -1
- package/lib/skills.syn +22 -6
- package/lib/tools/bash.syn +1 -1
- package/lib/tools/common.syn +1 -1
- package/lib/tools/fetch.syn +98 -71
- package/lib/tools/grep.syn +1 -1
- package/lib/tools/memo.syn +3 -3
- package/lib/tools/proc.syn +38 -20
- package/lib/tools/qr.js +26 -0
- package/lib/tools/url.syn +5 -5
- package/lib/tools.syn +23 -2
- package/lib/trace.syn +2 -2
- package/lib/tree.syn +29 -13
- package/lib/update.syn +1 -1
- package/lib/workspaces.syn +202 -36
- package/package.json +3 -2
- package/public/apple-touch-icon.png +0 -0
- package/public/badge-96.png +0 -0
- package/public/css/code.css +48 -0
- package/public/css/hub.css +9 -0
- package/public/css/login.css +16 -0
- package/public/css/mobile.css +95 -0
- package/public/css/panel.css +30 -0
- package/public/css/tokens.css +1 -1
- package/public/hub.html +11 -1
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon-maskable-512.png +0 -0
- package/public/icons-src/badge.svg +12 -0
- package/public/icons-src/icon-maskable.svg +15 -0
- package/public/icons-src/icon.svg +13 -0
- package/public/index.html +16 -3
- package/public/js/agents.js +1 -1
- package/public/js/chat.js +1 -1
- package/public/js/config.js +145 -2
- package/public/js/core.js +4 -2
- package/public/js/devices.js +80 -0
- package/public/js/editor.js +232 -0
- package/public/js/login.js +78 -0
- package/public/js/mobile.js +122 -0
- package/public/js/procs.js +1 -1
- package/public/js/pwa.js +5 -0
- package/public/js/terminal.js +1 -1
- package/public/js/tree.js +12 -4
- package/public/login.html +43 -0
- package/public/manifest.webmanifest +16 -0
- package/public/offline.html +24 -0
- package/public/sw.js +63 -0
- package/public/vendor/highlight.min.js +1343 -0
- package/public/vendor/qrcode.js +2297 -0
- package/skills/computer-use/SKILL.md +178 -0
- package/skills/lampson/SKILL.md +5 -0
- 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.
|
|
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)
|
|
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(
|
|
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(
|
|
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 " " +
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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"
|