lampson 0.1.3 → 0.2.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.
- package/.env.example +9 -0
- package/README.md +87 -4
- package/bin/lampson.js +1 -1
- package/chat.syn +133 -0
- package/cli.syn +68 -0
- package/hub.tpl.syn +127 -0
- package/lampson.ps1 +77 -51
- package/lampson.sh +68 -22
- package/lib/agents.syn +1 -1
- package/lib/approvals.syn +179 -0
- package/lib/lsp.syn +10 -1
- package/lib/mcp.syn +10 -1
- package/lib/permission.syn +18 -0
- package/lib/sched_run.syn +161 -0
- package/lib/schedule.syn +660 -0
- package/lib/session.syn +38 -1
- package/lib/settings.syn +66 -3
- package/lib/skills.syn +2 -0
- package/lib/tools.syn +94 -2
- package/lib/workspaces.syn +555 -0
- package/package.json +3 -1
- package/public/css/chat.css +56 -0
- package/public/css/hub.css +12 -0
- package/public/css/layout.css +97 -0
- package/public/css/panel.css +125 -0
- package/public/css/sidebar.css +80 -0
- package/public/css/tokens.css +57 -0
- package/public/hub.html +36 -0
- package/public/index.html +51 -1197
- package/public/js/agents.js +31 -0
- package/public/js/app.js +21 -0
- package/public/js/approvals.js +26 -0
- package/public/js/chat.js +92 -0
- package/public/js/config.js +100 -0
- package/public/js/core.js +91 -0
- package/public/js/events.js +31 -0
- package/public/js/hub.js +40 -0
- package/public/js/lamps.js +112 -0
- package/public/js/lsp.js +81 -0
- package/public/js/mcp.js +74 -0
- package/public/js/memory.js +17 -0
- package/public/js/panel.js +101 -0
- package/public/js/procs.js +45 -0
- package/public/js/schedules.js +118 -0
- package/public/js/sessions.js +69 -0
- package/public/js/sidebar.js +33 -0
- package/public/js/terminal.js +49 -0
- package/public/js/theme.js +6 -0
- package/public/js/todo.js +10 -0
- package/public/js/tree.js +53 -0
- package/public/js/update.js +14 -0
- package/public/js/workspaces.js +83 -0
- package/skills/lampson/SKILL.md +21 -0
- package/skills/synsema/SKILL.md +4 -1
- package/web.syn +165 -55
package/.env.example
CHANGED
|
@@ -27,3 +27,12 @@ LAMPSON_API_KEY=sk-...
|
|
|
27
27
|
|
|
28
28
|
# Reanudar una sesión en chat.syn
|
|
29
29
|
# LAMPSON_SESSION=20260826-233000
|
|
30
|
+
|
|
31
|
+
# Tareas programadas y aprobaciones a distancia (proceso residente: lampson --daemon start)
|
|
32
|
+
# LAMPSON_TZ=-03:00 # zona horaria para "daily 09:00" (default: la del sistema, `date +%z`)
|
|
33
|
+
# LAMPSON_PUBLIC_URL=https://lampson.midominio.com # URL por la que se llega a este lampson desde afuera (túnel, VPS,
|
|
34
|
+
# # edge Synsema con TLS): con esto cada aprobación pendiente trae links de un solo uso
|
|
35
|
+
# # (GET /approve/<id>/<token>?d=yes|no) para decidir desde el móvil
|
|
36
|
+
# LAMPSON_WEBHOOK_URL=https://hooks.example.com/lampson # POST JSON por cada aprobación pendiente (id, mensaje, links);
|
|
37
|
+
# # reenvialo a Telegram/Slack/mail con n8n, un bot o un .syn de 6 líneas
|
|
38
|
+
# LAMPSON_WEBHOOK_SECRET=… # firma HMAC-SHA256 del body (X-Lampson-Signature: sha256=<hex>)
|
package/README.md
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
reads, searches, edits and runs commands through tools that are confined to that project by the
|
|
5
5
|
language itself — and every step is visible, in the terminal or in a web UI.
|
|
6
6
|
|
|
7
|
-
- **
|
|
7
|
+
- **Workspaces, always on**: every project folder is a *workspace* with its own process (sessions, scheduled tasks,
|
|
8
|
+
MCP, lamps, dev servers), all behind one hub at http://127.0.0.1:8080 — open several at once and switch without
|
|
9
|
+
killing the others. `lampson` (terminal) and `lampson --web` (browser) open the workspace of the current folder;
|
|
10
|
+
`lampson --install` keeps the hub alive across logins.
|
|
8
11
|
- **Any OpenAI- or Anthropic-compatible API** over raw HTTP: DeepSeek, Kimi, Groq, Grok, OpenRouter,
|
|
9
12
|
Ollama, Anthropic, MiniMax… one `.env` line to switch.
|
|
10
13
|
- **Least-privilege by construction**: each tool declares its capabilities; the runtime enforces
|
|
@@ -29,6 +32,14 @@ language itself — and every step is visible, in the terminal or in a web UI.
|
|
|
29
32
|
under a capability ceiling, or any executable (js, py, sh…). Global in `lampson/lamps/<name>/`, per
|
|
30
33
|
project in `.lampson/lamps/<name>/` (the agent can write those). **Off by default**: you turn them on
|
|
31
34
|
from the top bar of the web UI or `/lamps on <name>`; their tools join the catalog as `lamp_<lamp>_<tool>`.
|
|
35
|
+
- **Scheduled tasks**: "every 6h", "daily 09:00", "mon,wed 08:30" — a lamp tool, a fixed shell command, or a
|
|
36
|
+
full unattended agent run from a prompt with a permission envelope fixed when you create it (`strict` /
|
|
37
|
+
`ask` / `yolo`). They run inside Lampson's resident process (`lampson --daemon start`, or the open web UI),
|
|
38
|
+
the run's session shows up as `⏰ name`, and a webhook can receive each result (for "search and send me" tasks).
|
|
39
|
+
- **Approvals from anywhere**: an unattended run that hits something dangerous waits for you — in the web UI
|
|
40
|
+
(«Aprobaciones»), and, with `LAMPSON_PUBLIC_URL` + `LAMPSON_WEBHOOK_URL`, through a signed webhook carrying
|
|
41
|
+
one-time **decision links** you can open from your phone (Telegram, mail, Slack…). Never auto-approved:
|
|
42
|
+
no answer in time = denied.
|
|
32
43
|
- **Project memory**: the agent keeps its own notes per project (`memory/<project>/*.md`, outside
|
|
33
44
|
the repo) — how to run it, gotchas, decisions — and rereads them in the next session. You can read
|
|
34
45
|
and edit them (web panel, `/memory`).
|
|
@@ -116,6 +127,68 @@ does the same with persistent sessions and memory.
|
|
|
116
127
|
> Status: developed and tested on Windows 11; the Docker image (Ubuntu 24.04) is built by CI. The Linux/macOS
|
|
117
128
|
> installer is written but not yet exercised on a real machine — issues welcome.
|
|
118
129
|
|
|
130
|
+
## Workspaces and the hub (how it runs)
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
lampson # in a project folder: registers it as a workspace (first time), makes sure the hub and
|
|
134
|
+
# the workspace's process are up, opens the terminal on it
|
|
135
|
+
lampson --web # same, then opens http://127.0.0.1:8080/w/<slug> in the browser
|
|
136
|
+
http://127.0.0.1:8080 # the workspaces screen: open one, create one (native folder dialog; a server-side
|
|
137
|
+
# browser on a VPS), turn them on/off, life policy
|
|
138
|
+
lampson --hub start|stop|status|logs|restart
|
|
139
|
+
lampson --install # Windows: Scheduled Task at logon · Linux: systemd --user unit (Restart=always)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Why processes: in Synsema a tool's file capability is a literal path relative to the process cwd, so a workspace is
|
|
143
|
+
a directory `~/lampson/.lampson/ws/<slug>/` with a junction `workspace` → your project (plus junctions to the
|
|
144
|
+
install's `lib/`, `public/`, `skills/`, `lamps/`, `memory/`), and its own `.lampson/` state. Its process
|
|
145
|
+
(`synsema serve web.syn --port 808N --bind 127.0.0.1`) only ever sees that folder. The hub (`hub.syn`, generated
|
|
146
|
+
from `hub.tpl.syn`, one proxy route per workspace) is the only listener you use: it serves the UI and forwards
|
|
147
|
+
`/w/<slug>/api/…` — SSE and the WebSocket terminal included (Synsema ≥ 0.6.12). Workspace processes are detached
|
|
148
|
+
from the hub, so a hub restart never kills them. The hub is also the supervisor: every 15 s it starts what should
|
|
149
|
+
be alive and stops what is idle (⚙ → hours of inactivity, `0` = never; a workspace with enabled scheduled tasks or
|
|
150
|
+
policy «siempre vivo» stays up).
|
|
151
|
+
|
|
152
|
+
On a VPS put the hub behind your domain (`synsema serve hub.syn --port 443 --domain … --tls-auto …` or your
|
|
153
|
+
edge); workspace ports never leave loopback. `LAMPSON_PUBLIC_URL` = `https://host` and approval links carry
|
|
154
|
+
`/w/<slug>` automatically.
|
|
155
|
+
|
|
156
|
+
## Scheduled tasks and the resident process
|
|
157
|
+
|
|
158
|
+
Lampson can run things with nobody at the keyboard. Scheduled tasks run inside the workspace's process (the hub
|
|
159
|
+
keeps it alive while it has enabled tasks) — or inside the terminal REPL when no process is up (a background thread
|
|
160
|
+
ticks every 30 s). `lampson --install` makes the hub survive reboots.
|
|
161
|
+
|
|
162
|
+
Timezone, public URL and webhook are set from the ⚙ button of the web header (tabs: General · Aprobaciones a
|
|
163
|
+
distancia · Proveedor; stored in `lampson/.lampson/config.json`, `.env` wins) or from `.env`.
|
|
164
|
+
|
|
165
|
+
Create a task from the chat («todos los días a las 9 corré los tests y avisame» — the agent calls the
|
|
166
|
+
`schedule` tool and asks you to approve the task, showing exactly what will run and with which permissions),
|
|
167
|
+
from the web sidebar («Programadas» → +), or from the terminal (`/schedule add <json>`). Three kinds:
|
|
168
|
+
|
|
169
|
+
| kind | what runs | authorization |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `lamp` | a tool of a lamp that is ON | turning the lamp on |
|
|
172
|
+
| `bash` | one fixed command (must finish on its own) | approved once, at creation |
|
|
173
|
+
| `prompt` | a full agent turn with your instructions and a profile (`build` / `review` / `plan` / `explore`) | the permission envelope: `strict` (dangerous → denied), `ask` (dangerous → approval request, denied if unanswered within `approval_timeout`, 2 h by default), `yolo` |
|
|
174
|
+
|
|
175
|
+
Schedules: `every 6h` · `every 30m` · `daily 09:00` · `mon,wed 08:30` · `weekdays 09:00` (local time; `LAMPSON_TZ`
|
|
176
|
+
overrides). Synsema's `cron_every` is a pure interval, so a 30 s tick in `web.syn` translates it to wall-clock time;
|
|
177
|
+
a run missed while the daemon was down executes when it comes back and is marked as late. State lives in
|
|
178
|
+
`.lampson/schedules.json`, logs in `.lampson/schedules/<id>.log`; a `prompt` run also leaves a normal session (`⏰ name`)
|
|
179
|
+
with its trace. `notify` = a webhook URL that receives each result as JSON.
|
|
180
|
+
|
|
181
|
+
**Approving from your phone.** Set `LAMPSON_PUBLIC_URL` (how this Lampson is reached from outside — a tunnel, a VPS,
|
|
182
|
+
or a Synsema edge with TLS in front) and `LAMPSON_WEBHOOK_URL` (+ `LAMPSON_WEBHOOK_SECRET`, HMAC-SHA256 in
|
|
183
|
+
`X-Lampson-Signature`). Every pending approval — from a scheduled run or from the chat — POSTs `{id, message, why,
|
|
184
|
+
expires_at, respond_link_yes, respond_link_no}`; forward the links wherever you read (n8n, a bot, a 6-line `.syn`).
|
|
185
|
+
`GET /approve/<id>/<token>?d=yes|no` is public on purpose: the 32-byte one-time token is the authorization, and it
|
|
186
|
+
dies with the deadline. `GET /api/approvals` (loopback) lists what is pending, never the tokens.
|
|
187
|
+
|
|
188
|
+
For a real server use systemd (`synsema serve web.syn`, `Restart=always`) instead of `synsema daemon`, which has no
|
|
189
|
+
boot start / crash restart. One box, several projects: one `web.syn` per project on its own port, behind a Synsema
|
|
190
|
+
edge that terminates TLS and routes by host (see the Synsema deploy docs).
|
|
191
|
+
|
|
119
192
|
## Use
|
|
120
193
|
|
|
121
194
|
```bash
|
|
@@ -182,10 +255,20 @@ run Lampson in a container.
|
|
|
182
255
|
## Architecture
|
|
183
256
|
|
|
184
257
|
```
|
|
185
|
-
lampson.ps1 / .sh launcher:
|
|
258
|
+
lampson.ps1 / .sh launcher: resolves the workspace (cli.syn), ensures hub + process, opens terminal or browser
|
|
259
|
+
cli.syn what launchers ask Synsema: ensure/list/hub-start/hub-stop/hub-restart (last line = JSON)
|
|
260
|
+
hub.tpl.syn template of the hub (:8080): workspaces screen + API, supervisor tick, one proxy route per workspace
|
|
261
|
+
lib/workspaces.syn registry (.lampson/workspaces.json), ws/<slug> dirs + junctions, detached processes, health,
|
|
262
|
+
life policy, folder picker (native dialog / server-side browser)
|
|
186
263
|
chat.syn terminal REPL (colors, approvals via Synsema's native `approve`)
|
|
187
|
-
web.syn
|
|
188
|
-
public/
|
|
264
|
+
web.syn one process per workspace: /w/:slug/api/… → SSE chat, sessions, tree, processes, schedules…
|
|
265
|
+
public/ web UI (no build step, no dependencies; classic scripts served by `static`)
|
|
266
|
+
index.html markup only: header, the two side panels, the chat; loads css/ and js/ in order
|
|
267
|
+
css/ tokens (fonts, palette, base) · layout (grid, header, panels, chat, composer) · sidebar · chat · panel
|
|
268
|
+
js/core.js shared state + helpers ($, esc, md, add, api, showPane/showText, inlineConfirm, debounce, empty)
|
|
269
|
+
js/panel.js THE modal component: one shell, three layouts (browse = search + list + detail, tabs, form)
|
|
270
|
+
js/<view>.js one file per thing on screen: sessions, chat, tree, terminal, procs, agents, memory, todo,
|
|
271
|
+
mcp, lsp, lamps, schedules, approvals, config (+ provider), update, events (SSE), app (boot)
|
|
189
272
|
lib/
|
|
190
273
|
provider.syn config from .env · chat(cfg, messages, catalog) · retry with backoff
|
|
191
274
|
loop.syn run_turn(): LLM → tool calls → permissions → call_tool → results → repeat; doom-loop guard; compaction
|
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', 'lampson.ps1', 'lampson.sh', 'lampson.cmd', '.env.example', 'README.md', 'LICENSE'];
|
|
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'];
|
|
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
|
@@ -14,6 +14,7 @@ require net
|
|
|
14
14
|
require time
|
|
15
15
|
require stdin
|
|
16
16
|
require exec
|
|
17
|
+
require random
|
|
17
18
|
require env("LAMPSON_*")
|
|
18
19
|
require env("OS")
|
|
19
20
|
require secret("LAMPSON_*")
|
|
@@ -51,6 +52,9 @@ use "./lib/mcp.syn" as mcp
|
|
|
51
52
|
use "./lib/lamps.syn" as lamps
|
|
52
53
|
use "./lib/lsp.syn" as lsp
|
|
53
54
|
use "./lib/tools/todo.syn" as todo
|
|
55
|
+
use "./lib/schedule.syn" as schedule
|
|
56
|
+
use "./lib/sched_run.syn" as sched_run
|
|
57
|
+
use "./lib/approvals.syn" as approvals
|
|
54
58
|
|
|
55
59
|
-- ---------- UI de terminal ----------
|
|
56
60
|
-- Colores ANSI (Windows Terminal, PowerShell 7, cualquier terminal moderna). LAMPSON_NO_COLOR=1 los apaga.
|
|
@@ -344,6 +348,8 @@ let COMMANDS be [
|
|
|
344
348
|
["/mcp", "[add <nombre> <comando…> [--project] | remove <nombre>]", "servers MCP: listar, conectar o quitar (global: lampson/.lampson/mcp.json · proyecto: .lampson/mcp.json)"],
|
|
345
349
|
["/lamps", "[on <nombre> | off <nombre> | run <lámpara> <tool> [json] | remove <nombre>]", "lámparas (plugins de tools): listar, encender o apagar (global: lampson/lamps/ · proyecto: .lampson/lamps/)"],
|
|
346
350
|
["/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"],
|
|
351
|
+
["/schedule", "[add <json> | run <id> | on <id> | off <id> | remove <id> | log <id>]", "tareas programadas (cada 6h, todos los días a las 9…): lámpara, comando o corrida del agente; corren mientras lampson esté abierto (o con lampson --daemon start, sin nada abierto)"],
|
|
352
|
+
["/approve", "<id> yes|no", "responder una aprobación pendiente de una tarea programada que corre en background"],
|
|
347
353
|
["/out", "[n]", "resultado completo de la última tool del turno (n = contar hacia atrás: /out 2 es la anteúltima)"],
|
|
348
354
|
["/verbose", "", "alternar: mostrar SIEMPRE el output completo de cada tool (queda guardado en config.json)"],
|
|
349
355
|
["/trace", "[n]", "traza legible de esta sesión (pasos, tools, tiempos, tokens, errores): .lampson/trace/<sesión>.log"],
|
|
@@ -403,6 +409,16 @@ task complete_args(cmd, head, last)
|
|
|
403
409
|
when trim(head) == "add"
|
|
404
410
|
give sort_by(keys(lsp.PRESETS), (x) => x)
|
|
405
411
|
give ["--project", "--ext"]
|
|
412
|
+
when cmd == "/approve"
|
|
413
|
+
when first
|
|
414
|
+
give apply((a) => a["id"], approvals.pending())
|
|
415
|
+
give ["yes", "no"]
|
|
416
|
+
when cmd == "/schedule"
|
|
417
|
+
when first
|
|
418
|
+
give ["add", "run", "on", "off", "remove", "log"]
|
|
419
|
+
when starts_with(trim(head), "add")
|
|
420
|
+
give []
|
|
421
|
+
give apply((s) => s["id"], schedule.summary())
|
|
406
422
|
when cmd == "/agent"
|
|
407
423
|
give ["build", "plan", "review", "explore", "worker"]
|
|
408
424
|
when cmd == "/model"
|
|
@@ -672,6 +688,57 @@ task opts_for(p, mode)
|
|
|
672
688
|
task lampson_subagent(spec_json)
|
|
673
689
|
give agents.run_child_json(spec_json)
|
|
674
690
|
|
|
691
|
+
-- Tareas programadas TAMBIÉN en la terminal: `cron_every` no corre mientras el hilo principal está en el teclado,
|
|
692
|
+
-- pero un `agent` sí (hilo real, como el Spinner). Cada 30 s llama al tick (heartbeat + corridas vencidas); una
|
|
693
|
+
-- corrida `prompt` transcurre al lado del chat sin tocar su sesión (blackboard lampson:sched:session) y si pide
|
|
694
|
+
-- permiso lo dice por consola: /approve <id> yes|no (o link/webhook si están configurados).
|
|
695
|
+
task lampson_sched_tick()
|
|
696
|
+
give sched_run.tick()
|
|
697
|
+
|
|
698
|
+
agent Sched
|
|
699
|
+
require net
|
|
700
|
+
require time
|
|
701
|
+
require exec
|
|
702
|
+
require random
|
|
703
|
+
require env("LAMPSON_*")
|
|
704
|
+
require env("OS")
|
|
705
|
+
require secret("LAMPSON_*")
|
|
706
|
+
require file("workspace")
|
|
707
|
+
require file("workspace/*")
|
|
708
|
+
require file.read("skills")
|
|
709
|
+
require file.read("skills/*")
|
|
710
|
+
require file.read("lamps")
|
|
711
|
+
require file.read("lamps/*")
|
|
712
|
+
require file("memory")
|
|
713
|
+
require file("memory/*")
|
|
714
|
+
require file(".lampson")
|
|
715
|
+
require file(".lampson/*")
|
|
716
|
+
let alive be true
|
|
717
|
+
let waited be 25
|
|
718
|
+
while alive
|
|
719
|
+
observe "lampson:spinner:stop" as st
|
|
720
|
+
when st == true
|
|
721
|
+
set alive to false
|
|
722
|
+
otherwise when waited >= 30
|
|
723
|
+
set waited to 0
|
|
724
|
+
try
|
|
725
|
+
-- bajo el hub el proceso del workspace (LAMPSON_PORT) ya hace tick: no duplicar corridas
|
|
726
|
+
let port be env("LAMPSON_PORT", "")
|
|
727
|
+
let other be false
|
|
728
|
+
when port != ""
|
|
729
|
+
try
|
|
730
|
+
let hr be http_get("http://127.0.0.1:" + port + "/w/" + env("LAMPSON_WS", "x") + "/api/health", {"timeout": 2})
|
|
731
|
+
set other to hr["ok"]
|
|
732
|
+
recover e2
|
|
733
|
+
set other to false
|
|
734
|
+
when not other
|
|
735
|
+
lampson_sched_tick()
|
|
736
|
+
recover err
|
|
737
|
+
print("[lampson] scheduler: " + text(err))
|
|
738
|
+
otherwise
|
|
739
|
+
sleep(1)
|
|
740
|
+
set waited to waited + 1
|
|
741
|
+
|
|
675
742
|
-- !comando del usuario en un pseudo-terminal: la salida se ve EN VIVO y, si el proceso se queda esperando
|
|
676
743
|
-- (prompt y/N, contraseña, REPL, `npm init`…), lo que escribas se le manda como teclas + Enter.
|
|
677
744
|
-- Enter vacío = seguir esperando · ^C = cortar. Devuelve la salida (sin ANSI) para el contexto del agente.
|
|
@@ -765,6 +832,7 @@ banner(env_info["cwd"], cfg, profile, opts["permission_mode"], sid)
|
|
|
765
832
|
share nothing as "lampson:busy"
|
|
766
833
|
share false as "lampson:spinner:stop"
|
|
767
834
|
spawn Spinner with esc = ESC, color = COLOR
|
|
835
|
+
spawn Sched
|
|
768
836
|
flush()
|
|
769
837
|
|
|
770
838
|
let total_usage be {"input": 0, "output": 0}
|
|
@@ -1005,6 +1073,71 @@ while running
|
|
|
1005
1073
|
each s in ss
|
|
1006
1074
|
print(" " + (when s["status"] == "ready" then green("● ") otherwise (when s["status"] == "error" or s["status"] == "exited" then red("○ ") otherwise dim("○ "))) + pad(s["name"], 12) + dim(pad(s["scope"], 8) + pad(s["status"], 8) + join(s["extensions"], " ") + " $ " + s["command"]) + (when s["error"] != nothing then red(" " + text(s["error"])) otherwise ""))
|
|
1007
1075
|
print(" " + dim("idle = arranca en la primera consulta del agente"))
|
|
1076
|
+
otherwise when input == "/schedule" or starts_with(input, "/schedule ")
|
|
1077
|
+
-- tareas programadas (lib/schedule.syn). Crear: JSON, o pedírselo al agente ("todos los días a las 9 corré…").
|
|
1078
|
+
-- Las corre el daemon (web.syn con `lampson --daemon start`); `/schedule run` las corre acá mismo, con
|
|
1079
|
+
-- el menú de aprobación de la terminal si la tarea es `ask`.
|
|
1080
|
+
let srest be trim(slice(input, 9, length(input)))
|
|
1081
|
+
let stoks be where(split(srest, " "), (x) => x != "")
|
|
1082
|
+
let sact be when length(stoks) > 0 then stoks[0] otherwise ""
|
|
1083
|
+
let sid_arg be when length(stoks) > 1 then stoks[1] otherwise ""
|
|
1084
|
+
when sact == "add"
|
|
1085
|
+
let sjson be trim(slice(srest, 3, length(srest)))
|
|
1086
|
+
when sjson == ""
|
|
1087
|
+
print(" uso: /schedule add {\"name\": \"…\", \"when\": \"daily 09:00\", \"action\": {\"type\": \"bash\", \"command\": \"npm test\"}, \"permission\": \"ask\"}")
|
|
1088
|
+
print(" " + dim("when: every 6h · daily 09:00 · mon,wed 08:30 · weekdays 09:00 action.type: lamp {lamp, tool, args} · bash {command} · prompt {prompt, agent}"))
|
|
1089
|
+
print(" " + dim("o pedíselo al agente en lenguaje natural: «todos los días a las 9 revisá los tests y avisame»"))
|
|
1090
|
+
otherwise
|
|
1091
|
+
try
|
|
1092
|
+
let st be schedule.add(json_decode(sjson))
|
|
1093
|
+
print(" " + green("⏰ programada «" + st["name"] + "» (" + st["id"] + "): " + schedule.describe_plan(st["plan"]) + " · próxima " + schedule.fmt_local(st["next_run"])))
|
|
1094
|
+
recover err
|
|
1095
|
+
print(" " + red(text(err)))
|
|
1096
|
+
otherwise when sact == "run" and sid_arg != ""
|
|
1097
|
+
let st be schedule.get(sid_arg)
|
|
1098
|
+
when st == nothing
|
|
1099
|
+
print(" " + red("no existe la tarea " + sid_arg))
|
|
1100
|
+
otherwise
|
|
1101
|
+
print(" " + yellow("⏰ corriendo «" + st["name"] + "» · " + schedule.describe_action(st["action"])))
|
|
1102
|
+
flush()
|
|
1103
|
+
let sr be sched_run.run(st, ask_user, on_event)
|
|
1104
|
+
print(" " + (when sr["last_status"] == "ok" then green("✓ ") otherwise red("✗ ")) + sr["last_status"] + " · " + first_line(sr["last_summary"], 160))
|
|
1105
|
+
print(" " + dim("log: " + schedule.LOG_DIR + "/" + st["id"] + ".log"))
|
|
1106
|
+
otherwise when (sact == "on" or sact == "off") and sid_arg != ""
|
|
1107
|
+
try
|
|
1108
|
+
print(" " + schedule.set_enabled(sid_arg, sact == "on"))
|
|
1109
|
+
recover err
|
|
1110
|
+
print(" " + red(text(err)))
|
|
1111
|
+
otherwise when sact == "remove" and sid_arg != ""
|
|
1112
|
+
try
|
|
1113
|
+
print(" " + schedule.remove(sid_arg))
|
|
1114
|
+
recover err
|
|
1115
|
+
print(" " + red(text(err)))
|
|
1116
|
+
otherwise when sact == "log" and sid_arg != ""
|
|
1117
|
+
let tl be schedule.log_tail(sid_arg, 60)
|
|
1118
|
+
print(when tl == "" then " sin corridas todavía" otherwise tl)
|
|
1119
|
+
otherwise
|
|
1120
|
+
let ss be schedule.summary()
|
|
1121
|
+
let age be schedule.daemon_age()
|
|
1122
|
+
when length(ss) == 0
|
|
1123
|
+
print(" sin tareas programadas. Pedísela al agente («cada 6 horas corré los tests y avisame») o: " + cyan("/schedule add <json>"))
|
|
1124
|
+
each s in ss
|
|
1125
|
+
print(" " + (when s["running"] then yellow("● ") otherwise (when s["enabled"] then green("● ") otherwise dim("○ "))) + pad(s["id"], 22) + dim(pad(s["plan"], 30)) + s["action_text"])
|
|
1126
|
+
print(" " + dim("permisos " + s["permission"] + " · próxima " + s["next_local"] + (when s["last_run"] != nothing then " · última " + s["last_status"] + ": " + first_line(s["last_summary"], 80) otherwise "")))
|
|
1127
|
+
let pend be approvals.pending()
|
|
1128
|
+
each ap in pend
|
|
1129
|
+
print(" " + yellow("⚠ aprobación pendiente " + ap["id"] + ": " + first_line(ap["message"], 100)) + dim(" → /approve " + ap["id"] + " yes|no"))
|
|
1130
|
+
print(" " + dim("corren solas mientras lampson esté abierto (tick cada " + text(schedule.TICK_SECONDS) + " s) · con todo cerrado: lampson --daemon start · run/on/off/remove/log <id>"))
|
|
1131
|
+
otherwise when starts_with(input, "/approve ")
|
|
1132
|
+
let atoks be where(split(trim(slice(input, 9, length(input))), " "), (x) => x != "")
|
|
1133
|
+
when length(atoks) < 2 or not contains(["yes", "no", "si", "sí", "y", "n"], lower(atoks[1]))
|
|
1134
|
+
print(" uso: /approve <id> yes|no (pendientes: /schedule)")
|
|
1135
|
+
otherwise
|
|
1136
|
+
let yes be contains(["yes", "si", "sí", "y"], lower(atoks[1]))
|
|
1137
|
+
when approvals.answer(atoks[0], yes)
|
|
1138
|
+
print(" " + (when yes then green("✓ permitido ") otherwise red("✗ denegado ")) + atoks[0])
|
|
1139
|
+
otherwise
|
|
1140
|
+
print(" " + red("no hay una aprobación pendiente con id " + atoks[0]))
|
|
1008
1141
|
otherwise when input == "/lamps" or starts_with(input, "/lamps ")
|
|
1009
1142
|
let lrest be trim(slice(input, 6, length(input)))
|
|
1010
1143
|
when starts_with(lrest, "run ")
|
package/cli.syn
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
-- cli.syn — lo que los launchers (lampson.ps1 / lampson.sh) le piden a Synsema. Corre con cwd = instalación.
|
|
2
|
+
--
|
|
3
|
+
-- LAMPSON_CMD=ensure LAMPSON_WORKSPACE=<ruta> synsema run cli.syn
|
|
4
|
+
-- registra/prepara el workspace, asegura el hub y su proceso → imprime UNA línea JSON {slug, port, dir, url, alive}
|
|
5
|
+
-- LAMPSON_CMD=hub-status → {alive}
|
|
6
|
+
-- LAMPSON_CMD=hub-start | hub-stop
|
|
7
|
+
-- LAMPSON_CMD=list
|
|
8
|
+
-- La salida útil va en la ÚLTIMA línea (JSON) para que el launcher la parsee.
|
|
9
|
+
|
|
10
|
+
require exec
|
|
11
|
+
require time
|
|
12
|
+
require net("127.0.0.1")
|
|
13
|
+
require env("LAMPSON_*")
|
|
14
|
+
require env("OS")
|
|
15
|
+
require env("HOME")
|
|
16
|
+
require env("USERPROFILE")
|
|
17
|
+
require file(".lampson")
|
|
18
|
+
require file(".lampson/*")
|
|
19
|
+
require file.read("web.syn")
|
|
20
|
+
require file.read(".env")
|
|
21
|
+
require file.read("chat.syn")
|
|
22
|
+
require file.read("hub.tpl.syn")
|
|
23
|
+
require file("hub.syn")
|
|
24
|
+
|
|
25
|
+
use "./lib/workspaces.syn" as ws
|
|
26
|
+
|
|
27
|
+
let cmd be env("LAMPSON_CMD", "ensure")
|
|
28
|
+
|
|
29
|
+
when cmd == "ensure"
|
|
30
|
+
let path be env("LAMPSON_WORKSPACE", "")
|
|
31
|
+
when path == ""
|
|
32
|
+
raise("LAMPSON_WORKSPACE required")
|
|
33
|
+
let w be ws.ensure(path)
|
|
34
|
+
when not ws.hub_alive()
|
|
35
|
+
ws.start_hub()
|
|
36
|
+
let hc be ws.health(w)
|
|
37
|
+
when hc == nothing
|
|
38
|
+
ws.start_ws(w)
|
|
39
|
+
ws.wait_ready(w, 10)
|
|
40
|
+
set hc to ws.health(w)
|
|
41
|
+
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()}))
|
|
43
|
+
otherwise when cmd == "hub-status"
|
|
44
|
+
print(json_encode({"alive": ws.hub_alive(), "workspaces": ws.summary()}))
|
|
45
|
+
otherwise when cmd == "hub-start"
|
|
46
|
+
when not ws.hub_alive()
|
|
47
|
+
ws.start_hub()
|
|
48
|
+
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}))
|
|
54
|
+
otherwise when cmd == "hub-restart"
|
|
55
|
+
-- lanzado desacoplado por workspaces.restart_hub_later: espera a que el hub termine lo que estaba haciendo,
|
|
56
|
+
-- lo para y lo vuelve a arrancar con el hub.syn regenerado (bajo systemd, Restart=always ya lo levanta solo)
|
|
57
|
+
sleep(1.5)
|
|
58
|
+
ws.stop_hub()
|
|
59
|
+
sleep(2)
|
|
60
|
+
when not ws.hub_alive()
|
|
61
|
+
ws.start_hub()
|
|
62
|
+
print(json_encode({"restarted": true}))
|
|
63
|
+
otherwise when cmd == "list"
|
|
64
|
+
each w in ws.summary()
|
|
65
|
+
print((when w["alive"] then "● " otherwise "○ ") + w["slug"] + " " + w["path"] + " :" + text(w["port"]) + " " + w["policy"])
|
|
66
|
+
print(json_encode({"count": length(ws.all())}))
|
|
67
|
+
otherwise
|
|
68
|
+
raise("unknown LAMPSON_CMD " + cmd)
|
package/hub.tpl.syn
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
-- hub.tpl.syn — PLANTILLA del hub. lib/workspaces.syn genera hub.syn (gitignored) reemplazando `-- @workspaces` por una
|
|
2
|
+
-- ruta proxy por workspace; el hub corre con `synsema daemon start hub.syn --watch` y se reinicia solo al cambiar.
|
|
3
|
+
--
|
|
4
|
+
-- El hub es lo único que escucha hacia afuera (:8080; en un VPS, con --domain/--tls-auto):
|
|
5
|
+
-- GET / pantalla de workspaces (public/hub.html)
|
|
6
|
+
-- GET /w/<slug> la UI de ese workspace (public/index.html; los assets /css /js los sirve el hub)
|
|
7
|
+
-- * /w/<slug>/api/… proxy al proceso del workspace (127.0.0.1:808N) — SSE y WebSocket pasan (Synsema ≥ 0.6.12)
|
|
8
|
+
-- GET /w/<slug>/approve/… links de aprobación de ese workspace
|
|
9
|
+
-- /api/workspaces… registro, crear (diálogo nativo o explorador), start/stop/quitar, política
|
|
10
|
+
-- Supervisor: cada 15 s arranca los workspaces que deben vivir y apaga los inactivos (política en ⚙ → idle_hours).
|
|
11
|
+
-- Solo loopback puede usar la API del hub (misma regla que web.syn); LAMPSON_WEB_TOKEN para scripts remotos.
|
|
12
|
+
|
|
13
|
+
intent: "Lampson hub: workspaces registry, supervisor and reverse proxy to one process per workspace"
|
|
14
|
+
|
|
15
|
+
require serve(8080)
|
|
16
|
+
require net("127.0.0.1")
|
|
17
|
+
require time
|
|
18
|
+
require exec
|
|
19
|
+
require env("LAMPSON_*")
|
|
20
|
+
require env("OS")
|
|
21
|
+
require env("HOME")
|
|
22
|
+
require env("USERPROFILE")
|
|
23
|
+
require file(".lampson")
|
|
24
|
+
require file(".lampson/*")
|
|
25
|
+
require file.read("web.syn")
|
|
26
|
+
require file.read(".env")
|
|
27
|
+
require file.read("chat.syn")
|
|
28
|
+
require file.read("hub.tpl.syn")
|
|
29
|
+
require file("hub.syn")
|
|
30
|
+
require file.read("public")
|
|
31
|
+
require file.read("public/*")
|
|
32
|
+
|
|
33
|
+
use "./lib/workspaces.syn" as ws
|
|
34
|
+
use "./lib/settings.syn" as settings
|
|
35
|
+
|
|
36
|
+
task check_client(token, request)
|
|
37
|
+
let ip be text(ip of request)
|
|
38
|
+
when ip == "127.0.0.1" or ip == "::1" or ip == "::ffff:127.0.0.1"
|
|
39
|
+
give {"local": true}
|
|
40
|
+
let want be env("LAMPSON_WEB_TOKEN", "")
|
|
41
|
+
when want != "" and token == want
|
|
42
|
+
give {"local": false}
|
|
43
|
+
give nothing
|
|
44
|
+
|
|
45
|
+
task hub_tick()
|
|
46
|
+
give ws.tick()
|
|
47
|
+
cron_every(15, hub_tick)
|
|
48
|
+
|
|
49
|
+
serve on 8080
|
|
50
|
+
max_streams 200
|
|
51
|
+
auth with check_client
|
|
52
|
+
static "./public"
|
|
53
|
+
|
|
54
|
+
route "GET /"
|
|
55
|
+
give html(read_file("public/hub.html"))
|
|
56
|
+
|
|
57
|
+
-- la UI de un workspace: el mismo index.html; el frontend deduce /w/<slug> de la URL (BASE en js/core.js).
|
|
58
|
+
-- Sin barra final: el runtime normaliza "/w/x/" → "/w/x" (301), así que la ruta canónica es esta
|
|
59
|
+
route "GET /w/:slug"
|
|
60
|
+
give html(read_file("public/index.html"))
|
|
61
|
+
|
|
62
|
+
route "GET /api/hub" requires auth
|
|
63
|
+
give {"ok": true, "port": ws.HUB_PORT, "idle_hours": ws.idle_hours(), "home": ws.home()}
|
|
64
|
+
|
|
65
|
+
route "GET /api/workspaces" requires auth
|
|
66
|
+
give {"workspaces": ws.summary(), "idle_hours": ws.idle_hours()}
|
|
67
|
+
|
|
68
|
+
-- registrar una carpeta como workspace (y arrancarla)
|
|
69
|
+
route "POST /api/workspaces" requires auth
|
|
70
|
+
expect body {path: text}
|
|
71
|
+
let b be json of request
|
|
72
|
+
try
|
|
73
|
+
let w be ws.ensure(text(b["path"]))
|
|
74
|
+
ws.start_ws(w)
|
|
75
|
+
ws.wait_ready(w, 8)
|
|
76
|
+
give {"ok": true, "workspace": w, "url": "/w/" + w["slug"] + "/"}
|
|
77
|
+
recover err
|
|
78
|
+
give fail(400, text(err))
|
|
79
|
+
|
|
80
|
+
route "POST /api/workspaces/start" requires auth
|
|
81
|
+
expect body {slug: text}
|
|
82
|
+
let b be json of request
|
|
83
|
+
let w be ws.get(text(b["slug"]))
|
|
84
|
+
when w == nothing
|
|
85
|
+
give not_found("no such workspace")
|
|
86
|
+
ws.start_ws(w)
|
|
87
|
+
give {"ok": ws.wait_ready(w, 8)}
|
|
88
|
+
|
|
89
|
+
route "POST /api/workspaces/stop" requires auth
|
|
90
|
+
expect body {slug: text}
|
|
91
|
+
let b be json of request
|
|
92
|
+
let w be ws.get(text(b["slug"]))
|
|
93
|
+
when w == nothing
|
|
94
|
+
give not_found("no such workspace")
|
|
95
|
+
give {"ok": ws.stop_ws(w)}
|
|
96
|
+
|
|
97
|
+
route "POST /api/workspaces/remove" requires auth
|
|
98
|
+
expect body {slug: text}
|
|
99
|
+
let b be json of request
|
|
100
|
+
try
|
|
101
|
+
give {"ok": true, "result": ws.remove(text(b["slug"]))}
|
|
102
|
+
recover err
|
|
103
|
+
give fail(400, text(err))
|
|
104
|
+
|
|
105
|
+
-- política por workspace: auto | always | off
|
|
106
|
+
route "POST /api/workspaces/policy" requires auth
|
|
107
|
+
expect body {slug: text, policy: text}
|
|
108
|
+
let b be json of request
|
|
109
|
+
let w be ws.get(text(b["slug"]))
|
|
110
|
+
when w == nothing
|
|
111
|
+
give not_found("no such workspace")
|
|
112
|
+
when not contains(["auto", "always", "off"], text(b["policy"]))
|
|
113
|
+
give fail(400, "policy must be auto | always | off")
|
|
114
|
+
set w["policy"] to text(b["policy"])
|
|
115
|
+
ws.put(w)
|
|
116
|
+
give {"ok": true}
|
|
117
|
+
|
|
118
|
+
-- diálogo nativo del SO (PC); nothing → la UI usa el explorador propio
|
|
119
|
+
route "POST /api/workspaces/pick" requires auth
|
|
120
|
+
let p be ws.pick_folder()
|
|
121
|
+
give {"path": p, "native": p != nothing}
|
|
122
|
+
|
|
123
|
+
route "GET /api/workspaces/browse" requires auth
|
|
124
|
+
give ws.browse(when contains(query, "path") then text(query.path) otherwise "")
|
|
125
|
+
|
|
126
|
+
-- proxies generados (una por workspace registrado):
|
|
127
|
+
-- @workspaces
|