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/web.syn
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
-- web.syn — Lampson por URL (chat web con SSE)
|
|
2
2
|
--
|
|
3
|
-
--
|
|
3
|
+
-- Un proceso POR WORKSPACE, lanzado por el hub / cli.syn con cwd = .lampson/ws/<slug> (ver lib/workspaces.syn):
|
|
4
|
+
-- synsema serve web.syn --port 808N --bind 127.0.0.1 --env-file <home>/.env
|
|
5
|
+
-- Todas las rutas van bajo /w/:slug/… porque el hub (hub.syn, :8080) proxyea sin quitar el prefijo. El :slug se
|
|
6
|
+
-- ignora (cada proceso es un workspace); LAMPSON_WS trae el propio. Standalone sigue sirviendo en :8080.
|
|
4
7
|
--
|
|
5
|
-
-- API:
|
|
6
|
-
-- GET /
|
|
8
|
+
-- API (bajo /w/<slug>):
|
|
9
|
+
-- GET /api/health {alive, last_used, schedules_on} (hub, terminal)
|
|
7
10
|
-- GET /api/sessions lista de sesiones
|
|
8
11
|
-- GET /api/sessions/:id historial de una sesión
|
|
9
12
|
-- POST /api/chat {session?, message, agent?} → SSE con eventos del loop y "done" al final
|
|
@@ -23,6 +26,7 @@ require serve(8080)
|
|
|
23
26
|
require net
|
|
24
27
|
require time
|
|
25
28
|
require exec
|
|
29
|
+
require random
|
|
26
30
|
require env("LAMPSON_*")
|
|
27
31
|
require env("OS")
|
|
28
32
|
require secret("LAMPSON_*")
|
|
@@ -36,6 +40,8 @@ require file("memory")
|
|
|
36
40
|
require file("memory/*")
|
|
37
41
|
require file(".lampson")
|
|
38
42
|
require file(".lampson/*")
|
|
43
|
+
require file.read("public")
|
|
44
|
+
require file.read("public/*")
|
|
39
45
|
|
|
40
46
|
use "./lib/provider.syn" as provider
|
|
41
47
|
use "./lib/tools.syn" as tools
|
|
@@ -54,6 +60,10 @@ use "./lib/mcp.syn" as mcp
|
|
|
54
60
|
use "./lib/lamps.syn" as lamps
|
|
55
61
|
use "./lib/lsp.syn" as lsp
|
|
56
62
|
use "./lib/tools/todo.syn" as todo
|
|
63
|
+
use "./lib/schedule.syn" as schedule
|
|
64
|
+
use "./lib/sched_run.syn" as sched_run
|
|
65
|
+
use "./lib/approvals.syn" as approvals
|
|
66
|
+
use "./lib/permission.syn" as permission
|
|
57
67
|
|
|
58
68
|
let APPROVAL_TIMEOUT be 180
|
|
59
69
|
|
|
@@ -106,6 +116,15 @@ task boot(profile, mode, ask_fn, pname, model)
|
|
|
106
116
|
task lampson_subagent(spec_json)
|
|
107
117
|
give agents.run_child_json(spec_json)
|
|
108
118
|
|
|
119
|
+
-- TAREAS PROGRAMADAS (lib/schedule.syn): este proceso es el residente de lampson — con `lampson --daemon start`
|
|
120
|
+
-- (synsema daemon) queda en background y ejecuta lo programado aunque no haya ninguna UI abierta. Un tick cada
|
|
121
|
+
-- 30 s (cron_every de Synsema: intervalo puro; la hora de reloj la resuelve schedule.syn) corre lo vencido:
|
|
122
|
+
-- lámparas, comandos y corridas del agente con su sobre de permisos; una aprobación en una corrida desatendida
|
|
123
|
+
-- sale por lib/approvals.syn (panel «Aprobaciones» de la UI, webhook firmado, link de un solo uso).
|
|
124
|
+
task sched_tick()
|
|
125
|
+
give sched_run.tick()
|
|
126
|
+
cron_every(schedule.TICK_SECONDS, sched_tick)
|
|
127
|
+
|
|
109
128
|
-- servers MCP: supervisores (agentes) lanzados al arrancar; los handlers leen su estado del blackboard
|
|
110
129
|
mcp.start_all(8)
|
|
111
130
|
|
|
@@ -119,22 +138,39 @@ serve on 8080
|
|
|
119
138
|
auth with check_client
|
|
120
139
|
static "./public"
|
|
121
140
|
|
|
122
|
-
|
|
141
|
+
-- standalone (sin hub: docker, `synsema serve web.syn`): la UI vive igual bajo /w/<slug> (BASE en js/core.js)
|
|
142
|
+
route "GET /"
|
|
143
|
+
give redirect("/w/" + (when env("LAMPSON_WS", "") == "" then "local" otherwise env("LAMPSON_WS", "")))
|
|
144
|
+
route "GET /w/:slug"
|
|
145
|
+
give html(read_file("public/index.html"))
|
|
146
|
+
|
|
147
|
+
-- salud para el hub/supervisor y la terminal (último uso = chat, terminal, eventos o abrir la UI)
|
|
148
|
+
route "GET /w/:slug/api/health"
|
|
149
|
+
give {"alive": true, "slug": env("LAMPSON_WS", ""), "last_used": state_get("lampson:last_used", nothing), "schedules_on": length(where(schedule.all(), (t) => t["enabled"] == true)), "version": env("LAMPSON_VERSION", "")}
|
|
150
|
+
|
|
151
|
+
route "GET /w/:slug/api/sessions" requires auth
|
|
152
|
+
state_set("lampson:last_used", now())
|
|
123
153
|
give {"sessions": session.list()}
|
|
124
154
|
|
|
125
|
-
|
|
155
|
+
-- buscador de sesiones (modal «ver todas»): ?q=texto busca en títulos y mensajes de este proyecto
|
|
156
|
+
route "GET /w/:slug/api/sessions/search" requires auth
|
|
157
|
+
let q be when contains(query, "q") then text(query.q) otherwise ""
|
|
158
|
+
let n be when contains(query, "limit") then floor(number(query.limit)) otherwise 200
|
|
159
|
+
give {"q": q, "sessions": session.search(q, n)}
|
|
160
|
+
|
|
161
|
+
route "GET /w/:slug/api/sessions/:id" requires auth
|
|
126
162
|
when not session.exists(params.id)
|
|
127
163
|
give not_found("session not found")
|
|
128
164
|
let doc be session.load(params.id)
|
|
129
165
|
let visible be where(doc["messages"], (m) => m["role"] != "system")
|
|
130
166
|
give {"id": doc["id"], "updated": doc["updated"], "meta": doc["meta"], "messages": visible}
|
|
131
167
|
|
|
132
|
-
route "GET /api/config" requires auth
|
|
168
|
+
route "GET /w/:slug/api/config" requires auth
|
|
133
169
|
let cfg be provider.config()
|
|
134
170
|
give {"provider": cfg["provider"], "model": cfg["model"], "wire": cfg["wire"], "vision": provider.supports_vision(cfg), "providers": provider.providers(), "configured": provider.configured(), "permission": lower(env("LAMPSON_PERMISSION", "ask")), "agents": agents.names(), "workspace": env("LAMPSON_WORKSPACE", "./workspace"), "profiles": agents.PROFILES}
|
|
135
171
|
|
|
136
172
|
-- proveedor/modelo por defecto y API keys → .lampson/config.json (local). La key nunca vuelve al cliente.
|
|
137
|
-
route "POST /api/settings" requires auth
|
|
173
|
+
route "POST /w/:slug/api/settings" requires auth
|
|
138
174
|
expect body {provider: text}
|
|
139
175
|
let b be json of request
|
|
140
176
|
let name be lower(text(b["provider"]))
|
|
@@ -147,12 +183,12 @@ serve on 8080
|
|
|
147
183
|
let ncfg be provider.config_for(name, "")
|
|
148
184
|
give {"ok": true, "providers": provider.providers(), "provider": name, "model": ncfg["model"], "vision": provider.supports_vision(ncfg), "configured": provider.configured()}
|
|
149
185
|
|
|
150
|
-
route "POST /api/sessions/delete" requires auth
|
|
186
|
+
route "POST /w/:slug/api/sessions/delete" requires auth
|
|
151
187
|
expect body {id: text}
|
|
152
188
|
let b be json of request
|
|
153
189
|
give {"result": session.delete(text(b["id"]))}
|
|
154
190
|
|
|
155
|
-
route "GET /api/tree" requires auth
|
|
191
|
+
route "GET /w/:slug/api/tree" requires auth
|
|
156
192
|
give tree.tree(6)
|
|
157
193
|
|
|
158
194
|
-- Terminal real en el navegador: un shell dentro de un pseudo-terminal (pty) por conexión WebSocket.
|
|
@@ -160,8 +196,9 @@ serve on 8080
|
|
|
160
196
|
-- Servidor → navegador: frames de texto con prefijo: "o" + salida cruda del pty (ANSI incluido, xterm.js
|
|
161
197
|
-- la dibuja) | "c" + JSON de control ({type: hello|exit}).
|
|
162
198
|
-- El shell vive lo que vive el socket: cerrar el panel lo mata (el runtime no deja huérfanos).
|
|
163
|
-
route "GET /api/term" requires auth
|
|
199
|
+
route "GET /w/:slug/api/term" requires auth
|
|
164
200
|
socket
|
|
201
|
+
state_set("lampson:last_used", now())
|
|
165
202
|
let sh be term_shell()
|
|
166
203
|
let p be proc_spawn(sh[0], sh[1], {"cwd": term_cwd(), "pty": true, "cols": 120, "rows": 32})
|
|
167
204
|
ws_send(socket, "c" + json_encode({"type": "hello", "shell": sh[0], "pid": proc_stats(p)["pid"], "cwd": term_cwd()}))
|
|
@@ -188,25 +225,25 @@ serve on 8080
|
|
|
188
225
|
ws_send(socket, "o" + as_text(ev["data"]))
|
|
189
226
|
proc_close(p)
|
|
190
227
|
|
|
191
|
-
route "GET /api/update" requires auth
|
|
228
|
+
route "GET /w/:slug/api/update" requires auth
|
|
192
229
|
give update.check()
|
|
193
230
|
|
|
194
|
-
route "POST /api/update" requires auth
|
|
231
|
+
route "POST /w/:slug/api/update" requires auth
|
|
195
232
|
give {"result": update.apply()}
|
|
196
233
|
|
|
197
|
-
route "GET /api/git" requires auth
|
|
234
|
+
route "GET /w/:slug/api/git" requires auth
|
|
198
235
|
give git.status()
|
|
199
236
|
|
|
200
|
-
route "GET /api/memory" requires auth
|
|
237
|
+
route "GET /w/:slug/api/memory" requires auth
|
|
201
238
|
give {"dir": memo.dir(), "notes": memo.list()}
|
|
202
239
|
|
|
203
|
-
route "GET /api/memory/note" requires auth
|
|
240
|
+
route "GET /w/:slug/api/memory/note" requires auth
|
|
204
241
|
when not contains(query, "name")
|
|
205
242
|
give fail(400, "name required")
|
|
206
243
|
give {"name": query.name, "content": memo.note_read(query.name)}
|
|
207
244
|
|
|
208
245
|
-- traza legible de una sesión (ver lib/trace.syn)
|
|
209
|
-
route "GET /api/trace" requires auth
|
|
246
|
+
route "GET /w/:slug/api/trace" requires auth
|
|
210
247
|
when not contains(query, "session")
|
|
211
248
|
give fail(400, "session required")
|
|
212
249
|
when not matches(query.session, "[A-Za-z0-9_-]{1,40}")
|
|
@@ -215,22 +252,22 @@ serve on 8080
|
|
|
215
252
|
give {"session": query.session, "file": trace.file_of(query.session), "trace": trace.tail(query.session, n)}
|
|
216
253
|
|
|
217
254
|
-- modelos que declara la API del proveedor (para no tipear nombres a ciegas: DeepSeek es case-sensitive)
|
|
218
|
-
route "GET /api/models" requires auth
|
|
255
|
+
route "GET /w/:slug/api/models" requires auth
|
|
219
256
|
let pname be when contains(query, "provider") then lower(text(query.provider)) otherwise ""
|
|
220
257
|
let cfg be provider.config_for(pname, "")
|
|
221
258
|
give provider.list_models(cfg)
|
|
222
259
|
|
|
223
|
-
route "GET /api/todo" requires auth
|
|
260
|
+
route "GET /w/:slug/api/todo" requires auth
|
|
224
261
|
-- ?session=<id> → la lista de esa sesión; sin query, la de la sesión en curso
|
|
225
262
|
when contains(query, "session") and matches(query.session, "[A-Za-z0-9_-]{1,40}")
|
|
226
263
|
give {"items": todo.load(query.session)}
|
|
227
264
|
give {"items": todo.load(nothing)}
|
|
228
265
|
|
|
229
|
-
route "GET /api/mcp" requires auth
|
|
266
|
+
route "GET /w/:slug/api/mcp" requires auth
|
|
230
267
|
give {"servers": mcp.summary(), "global": mcp.GLOBAL_CONFIG, "project": mcp.PROJECT_CONFIG}
|
|
231
268
|
|
|
232
269
|
-- conectar un server MCP desde la UI: se escribe en el mcp.json del scope y arranca su supervisor
|
|
233
|
-
route "POST /api/mcp/add" requires auth
|
|
270
|
+
route "POST /w/:slug/api/mcp/add" requires auth
|
|
234
271
|
expect body {name: text, command: text}
|
|
235
272
|
let b be json of request
|
|
236
273
|
let scope be when contains(b, "scope") then lower(text(b["scope"])) otherwise "global"
|
|
@@ -240,7 +277,7 @@ serve on 8080
|
|
|
240
277
|
recover err
|
|
241
278
|
give fail(400, text(err))
|
|
242
279
|
|
|
243
|
-
route "POST /api/mcp/remove" requires auth
|
|
280
|
+
route "POST /w/:slug/api/mcp/remove" requires auth
|
|
244
281
|
expect body {name: text}
|
|
245
282
|
let b be json of request
|
|
246
283
|
try
|
|
@@ -249,10 +286,10 @@ serve on 8080
|
|
|
249
286
|
give fail(400, text(err))
|
|
250
287
|
|
|
251
288
|
-- language servers (navegación semántica): listar, agregar (preset o comando propio), quitar
|
|
252
|
-
route "GET /api/lsp" requires auth
|
|
289
|
+
route "GET /w/:slug/api/lsp" requires auth
|
|
253
290
|
give {"servers": lsp.summary(), "presets": lsp.PRESETS, "global": lsp.GLOBAL_CONFIG, "project": lsp.PROJECT_CONFIG}
|
|
254
291
|
|
|
255
|
-
route "POST /api/lsp/add" requires auth
|
|
292
|
+
route "POST /w/:slug/api/lsp/add" requires auth
|
|
256
293
|
expect body {name: text}
|
|
257
294
|
let b be json of request
|
|
258
295
|
let scope be when contains(b, "scope") then lower(text(b["scope"])) otherwise "global"
|
|
@@ -263,7 +300,7 @@ serve on 8080
|
|
|
263
300
|
recover err
|
|
264
301
|
give fail(400, text(err))
|
|
265
302
|
|
|
266
|
-
route "POST /api/lsp/remove" requires auth
|
|
303
|
+
route "POST /w/:slug/api/lsp/remove" requires auth
|
|
267
304
|
expect body {name: text}
|
|
268
305
|
let b be json of request
|
|
269
306
|
try
|
|
@@ -272,11 +309,11 @@ serve on 8080
|
|
|
272
309
|
give fail(400, text(err))
|
|
273
310
|
|
|
274
311
|
-- lámparas (plugins de tools): listar y encender/apagar desde la barra superior de la UI
|
|
275
|
-
route "GET /api/lamps" requires auth
|
|
312
|
+
route "GET /w/:slug/api/lamps" requires auth
|
|
276
313
|
give {"lamps": lamps.summary(), "global": lamps.GLOBAL_DIR, "project": lamps.PROJECT_DIR}
|
|
277
314
|
|
|
278
315
|
-- el usuario corre una tool de una lámpara encendida él mismo (sin pasar por el modelo)
|
|
279
|
-
route "POST /api/lamps/run" requires auth
|
|
316
|
+
route "POST /w/:slug/api/lamps/run" requires auth
|
|
280
317
|
expect body {tool: text}
|
|
281
318
|
let b be json of request
|
|
282
319
|
let args be when contains(b, "args") then b["args"] otherwise {}
|
|
@@ -285,7 +322,7 @@ serve on 8080
|
|
|
285
322
|
recover err
|
|
286
323
|
give fail(400, text(err))
|
|
287
324
|
|
|
288
|
-
route "POST /api/lamps/remove" requires auth
|
|
325
|
+
route "POST /w/:slug/api/lamps/remove" requires auth
|
|
289
326
|
expect body {name: text}
|
|
290
327
|
let b be json of request
|
|
291
328
|
try
|
|
@@ -293,7 +330,7 @@ serve on 8080
|
|
|
293
330
|
recover err
|
|
294
331
|
give fail(400, text(err))
|
|
295
332
|
|
|
296
|
-
route "POST /api/lamps/toggle" requires auth
|
|
333
|
+
route "POST /w/:slug/api/lamps/toggle" requires auth
|
|
297
334
|
expect body {name: text}
|
|
298
335
|
let b be json of request
|
|
299
336
|
let on be contains(b, "enabled") and b["enabled"] == true
|
|
@@ -302,16 +339,17 @@ serve on 8080
|
|
|
302
339
|
recover err
|
|
303
340
|
give fail(400, text(err))
|
|
304
341
|
|
|
305
|
-
route "GET /api/agents" requires auth
|
|
342
|
+
route "GET /w/:slug/api/agents" requires auth
|
|
306
343
|
agents.prune(10, 1800)
|
|
307
344
|
give {"agents": agents.list_children()}
|
|
308
345
|
|
|
309
346
|
-- Eventos en vivo para la UI (SSE): lo que publican los supervisores de procesos (proc.<name>: líneas y
|
|
310
347
|
-- cambios de estado) y los subagentes (subagent.started|done|<id>). Un bus por programa; cada cliente
|
|
311
348
|
-- tiene su cola acotada (drop_oldest). Heartbeat cada 25 s para que el proxy/navegador no cierre.
|
|
312
|
-
route "GET /api/events" requires auth
|
|
349
|
+
route "GET /w/:slug/api/events" requires auth
|
|
313
350
|
stream
|
|
314
|
-
|
|
351
|
+
state_set("lampson:last_used", now())
|
|
352
|
+
let sub be bus_subscribe(["proc.*", "subagent.*", "mcp.*", "approval.*", "schedule.*"], {"max_queue": 512})
|
|
315
353
|
let open be true
|
|
316
354
|
while open
|
|
317
355
|
let ev be bus_recv(sub, 25)
|
|
@@ -324,7 +362,7 @@ serve on 8080
|
|
|
324
362
|
set open to false
|
|
325
363
|
bus_unsubscribe(sub)
|
|
326
364
|
|
|
327
|
-
route "GET /api/agents/log" requires auth
|
|
365
|
+
route "GET /w/:slug/api/agents/log" requires auth
|
|
328
366
|
when not contains(query, "id")
|
|
329
367
|
give fail(400, "id required")
|
|
330
368
|
when not matches(query.id, "[a-z]+-[0-9]+")
|
|
@@ -339,7 +377,7 @@ serve on 8080
|
|
|
339
377
|
recover err
|
|
340
378
|
give fail(404, "no such sub-agent")
|
|
341
379
|
|
|
342
|
-
route "POST /api/agents/stop" requires auth
|
|
380
|
+
route "POST /w/:slug/api/agents/stop" requires auth
|
|
343
381
|
expect body {id: text}
|
|
344
382
|
let b be json of request
|
|
345
383
|
when not matches(b["id"], "[a-z]+-[0-9]+")
|
|
@@ -348,40 +386,117 @@ serve on 8080
|
|
|
348
386
|
bus_publish("subagent." + b["id"], {"kind": "stop", "line": "stop requested from the UI"})
|
|
349
387
|
give {"result": "stop requested for " + b["id"]}
|
|
350
388
|
|
|
351
|
-
route "GET /api/proc" requires auth
|
|
389
|
+
route "GET /w/:slug/api/proc" requires auth
|
|
352
390
|
give {"processes": proc.list()}
|
|
353
391
|
|
|
354
|
-
route "GET /api/proc/log" requires auth
|
|
392
|
+
route "GET /w/:slug/api/proc/log" requires auth
|
|
355
393
|
when not contains(query, "name")
|
|
356
394
|
give fail(400, "name required")
|
|
357
395
|
let n be when contains(query, "tail") then number(query.tail) otherwise 200
|
|
358
396
|
give {"name": query.name, "running": proc.alive(query.name), "log": proc.logs(query.name, n)}
|
|
359
397
|
|
|
360
|
-
route "GET /api/ports" requires auth
|
|
398
|
+
route "GET /w/:slug/api/ports" requires auth
|
|
361
399
|
give {"ports": proc.listeners()}
|
|
362
400
|
|
|
363
|
-
route "POST /api/ports/kill" requires auth
|
|
401
|
+
route "POST /w/:slug/api/ports/kill" requires auth
|
|
364
402
|
expect body {pid: number}
|
|
365
403
|
let b be json of request
|
|
366
404
|
give {"result": proc.kill_pid(b["pid"])}
|
|
367
405
|
|
|
368
|
-
route "POST /api/proc/stop" requires auth
|
|
406
|
+
route "POST /w/:slug/api/proc/stop" requires auth
|
|
369
407
|
expect body {name: text}
|
|
370
408
|
let b be json of request
|
|
371
409
|
give {"result": proc.halt(b["name"])}
|
|
372
410
|
|
|
373
|
-
route "GET /api/file" requires auth
|
|
411
|
+
route "GET /w/:slug/api/file" requires auth
|
|
374
412
|
when not contains(query, "path")
|
|
375
413
|
give fail(400, "path required")
|
|
376
414
|
give tree.file_content(query.path)
|
|
377
415
|
|
|
378
|
-
|
|
416
|
+
-- configuración general (rueda de la UI): zona horaria, URL pública, webhook… → .lampson/config.json
|
|
417
|
+
route "GET /w/:slug/api/settings/values" requires auth
|
|
418
|
+
give {"values": settings.values(), "tz_detected": schedule.tz_offset()}
|
|
419
|
+
|
|
420
|
+
route "POST /w/:slug/api/settings/values" requires auth
|
|
421
|
+
let b be json of request
|
|
422
|
+
try
|
|
423
|
+
each k in keys(b)
|
|
424
|
+
settings.set_value(k, b[k])
|
|
425
|
+
-- la zona horaria cacheada en el blackboard se recalcula
|
|
426
|
+
share nothing as "lampson:tz"
|
|
427
|
+
give {"ok": true, "values": settings.values(), "tz_detected": schedule.tz_offset()}
|
|
428
|
+
recover err
|
|
429
|
+
give fail(400, text(err))
|
|
430
|
+
|
|
431
|
+
-- aprobaciones (lib/approvals.syn): las del chat y las de tareas programadas comparten la cola
|
|
432
|
+
route "POST /w/:slug/api/approve" requires auth
|
|
379
433
|
expect body {id: text, decision: bool}
|
|
380
434
|
let b be json of request
|
|
381
|
-
|
|
382
|
-
|
|
435
|
+
give {"ok": approvals.answer(text(b["id"]), b["decision"])}
|
|
436
|
+
|
|
437
|
+
route "GET /w/:slug/api/approvals" requires auth
|
|
438
|
+
give {"approvals": approvals.list(), "public_url": approvals.public_url(), "webhook": approvals.webhook_url() != ""}
|
|
439
|
+
|
|
440
|
+
-- link de decisión (PÚBLICO a propósito: lo abre el humano desde el teléfono; el token de un solo uso es la
|
|
441
|
+
-- autorización, como en el `approve` nativo de Synsema bajo serve). GET porque un link en un chat es un GET.
|
|
442
|
+
route "GET /w/:slug/approve/:id/:token"
|
|
443
|
+
let d be when contains(query, "d") then lower(text(query.d)) otherwise ""
|
|
444
|
+
when d != "yes" and d != "no"
|
|
445
|
+
give html(approvals.html_page("¿Permitir?", "<a href='?d=yes' style='display:inline-block;padding:10px 18px;border-radius:4px;background:#1d1c1a;color:#fff;text-decoration:none;margin-right:10px'>Sí, permitir</a> <a href='?d=no' style='display:inline-block;padding:10px 18px;border-radius:4px;border:1px solid #a33;color:#a33;text-decoration:none'>No, denegar</a>"))
|
|
446
|
+
let r be approvals.answer_with_token(text(params.id), text(params.token), d == "yes")
|
|
447
|
+
when not r["ok"]
|
|
448
|
+
give html(approvals.html_page("Este link ya no sirve", "La aprobación venció, ya fue respondida o el link no es válido (" + r["why"] + ")."))
|
|
449
|
+
give html(approvals.html_page(when d == "yes" then "✓ Permitido" otherwise "✗ Denegado", "Lampson sigue con la tarea. Podés cerrar esta pestaña."))
|
|
450
|
+
|
|
451
|
+
-- tareas programadas (lib/schedule.syn): listar, crear, quitar, encender/apagar, correr ahora, log
|
|
452
|
+
route "GET /w/:slug/api/schedules" requires auth
|
|
453
|
+
let age be schedule.daemon_age()
|
|
454
|
+
-- este proceso tiene su propio tick (cron_every): las tareas corren mientras la web esté abierta
|
|
455
|
+
give {"tasks": schedule.summary(), "tick": schedule.TICK_SECONDS, "alive": true, "heartbeat_age": age, "mount_ok": schedule.mount_ok(), "workspace": env("LAMPSON_WORKSPACE", ""), "tz": schedule.tz_offset(), "lamps": lamps.summary()}
|
|
456
|
+
|
|
457
|
+
route "POST /w/:slug/api/schedules/add" requires auth
|
|
458
|
+
let b be json of request
|
|
459
|
+
try
|
|
460
|
+
let t be schedule.add(b)
|
|
461
|
+
give {"ok": true, "task": t, "result": "programada «" + t["name"] + "»: " + schedule.describe_plan(t["plan"]) + " · próxima " + schedule.fmt_local(t["next_run"])}
|
|
462
|
+
recover err
|
|
463
|
+
give fail(400, text(err))
|
|
464
|
+
|
|
465
|
+
route "POST /w/:slug/api/schedules/remove" requires auth
|
|
466
|
+
expect body {id: text}
|
|
467
|
+
let b be json of request
|
|
468
|
+
try
|
|
469
|
+
give {"ok": true, "result": schedule.remove(text(b["id"]))}
|
|
470
|
+
recover err
|
|
471
|
+
give fail(400, text(err))
|
|
472
|
+
|
|
473
|
+
route "POST /w/:slug/api/schedules/toggle" requires auth
|
|
474
|
+
expect body {id: text}
|
|
475
|
+
let b be json of request
|
|
476
|
+
let on be contains(b, "enabled") and b["enabled"] == true
|
|
477
|
+
try
|
|
478
|
+
give {"ok": true, "result": schedule.set_enabled(text(b["id"]), on)}
|
|
479
|
+
recover err
|
|
480
|
+
give fail(400, text(err))
|
|
481
|
+
|
|
482
|
+
-- correr ahora: la marca run_now la levanta el próximo tick (así el request no se queda minutos abierto
|
|
483
|
+
-- y la corrida vive en el hilo del cron, no en el del request)
|
|
484
|
+
route "POST /w/:slug/api/schedules/run" requires auth
|
|
485
|
+
expect body {id: text}
|
|
486
|
+
let b be json of request
|
|
487
|
+
try
|
|
488
|
+
let t be schedule.request_run(text(b["id"]))
|
|
489
|
+
give {"ok": true, "result": "«" + t["name"] + "» corre en el próximo tick (≤ " + text(schedule.TICK_SECONDS) + " s)"}
|
|
490
|
+
recover err
|
|
491
|
+
give fail(400, text(err))
|
|
492
|
+
|
|
493
|
+
route "GET /w/:slug/api/schedules/log" requires auth
|
|
494
|
+
when not contains(query, "id")
|
|
495
|
+
give fail(400, "id required")
|
|
496
|
+
let n be when contains(query, "tail") then floor(number(query.tail)) otherwise 300
|
|
497
|
+
give {"id": query.id, "log": schedule.log_tail(query.id, n), "task": schedule.get(query.id)}
|
|
383
498
|
|
|
384
|
-
route "POST /api/chat" requires auth
|
|
499
|
+
route "POST /w/:slug/api/chat" requires auth
|
|
385
500
|
expect body {message: text}
|
|
386
501
|
let b be json of request
|
|
387
502
|
let message be b["message"]
|
|
@@ -392,24 +507,19 @@ serve on 8080
|
|
|
392
507
|
set mode to "ask"
|
|
393
508
|
let pname be when contains(b, "provider") then lower(text(b["provider"])) otherwise ""
|
|
394
509
|
let pmodel be when contains(b, "model") then text(b["model"]) otherwise ""
|
|
510
|
+
state_set("lampson:last_used", now())
|
|
395
511
|
stream
|
|
396
512
|
task emit(kind, data, tag)
|
|
397
513
|
trace.event(sid, kind, data, tag)
|
|
398
514
|
send {"kind": kind, "data": data, "tag": tag} as "event"
|
|
399
|
-
-- aprobación humana
|
|
515
|
+
-- aprobación humana: la cola de lib/approvals.syn (id + token) — la UI responde por SSE/POST /api/approve,
|
|
516
|
+
-- y si hay webhook/LAMPSON_PUBLIC_URL también llega un link al canal del usuario (aprobar desde el móvil)
|
|
400
517
|
task ask_web(name, args, why)
|
|
401
|
-
let id be
|
|
518
|
+
let id be approvals.open("chat", permission.describe_call(name, args), why, APPROVAL_TIMEOUT)
|
|
402
519
|
send {"kind": "approval_request", "data": {"id": id, "name": name, "args": args, "why": why, "timeout": APPROVAL_TIMEOUT}} as "event"
|
|
403
|
-
let
|
|
404
|
-
let
|
|
405
|
-
|
|
406
|
-
set answer to state_get("approval:" + id, nothing)
|
|
407
|
-
when answer == nothing
|
|
408
|
-
sleep(0.5)
|
|
409
|
-
set waited to waited + 0.5
|
|
410
|
-
state_delete("approval:" + id)
|
|
411
|
-
let ok be answer == "yes"
|
|
412
|
-
send {"kind": "approval_result", "data": {"id": id, "approved": ok, "timeout": answer == nothing}} as "event"
|
|
520
|
+
let t0 be now()
|
|
521
|
+
let ok be approvals.wait(id, APPROVAL_TIMEOUT)
|
|
522
|
+
send {"kind": "approval_result", "data": {"id": id, "approved": ok, "timeout": (not ok) and now() - t0 >= APPROVAL_TIMEOUT - 1}} as "event"
|
|
413
523
|
give ok
|
|
414
524
|
let ctx be boot(want, mode, ask_web, pname, pmodel)
|
|
415
525
|
let messages be [ctx["system"]]
|