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
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
-- lib/workspaces.syn — varios workspaces vivos a la vez: registro, un directorio por workspace, procesos y supervisor
|
|
2
|
+
--
|
|
3
|
+
-- Por qué así (ver ../SPEC-WORKSPACES.md): en Synsema las capabilities de archivo Y los `use "./lib/…"` se resuelven
|
|
4
|
+
-- relativos al CWD del proceso, y `proxy to` es estático (se evalúa al arrancar y anexa el path entero). Entonces:
|
|
5
|
+
-- * cada workspace tiene su propio directorio .lampson/ws/<slug>/ que ES el cwd de su proceso:
|
|
6
|
+
-- workspace → junction al proyecto lib public skills lamps memory → junctions a la instalación
|
|
7
|
+
-- web.syn chat.syn → copias (refrescadas al arrancar) .lampson/ → estado propio (sesiones, tareas…)
|
|
8
|
+
-- .lampson/global → junction a <home>/.lampson (config.json con keys, mcp/lsp globales, skills-*)
|
|
9
|
+
-- con eso `file("workspace/*")`, `use "./lib/x.syn"` y todo el código de siempre funcionan SIN cambios.
|
|
10
|
+
-- * cada workspace corre `synsema serve web.syn --port 808N --bind 127.0.0.1` DESACOPLADO del hub (start /b,
|
|
11
|
+
-- process_group:false): sobrevive a reinicios del hub y a cerrar terminales. Solo loopback.
|
|
12
|
+
-- * el hub (hub.syn, :8080) se GENERA desde hub.tpl.syn con una ruta proxy por workspace y corre con
|
|
13
|
+
-- `synsema serve hub.syn` desacoplado: al registrar/quitar un workspace se reescribe y un helper lo reinicia.
|
|
14
|
+
-- * el hub es supervisor: cada 15 s arranca lo que debe estar vivo (política) y apaga lo inactivo.
|
|
15
|
+
--
|
|
16
|
+
-- Este módulo corre SIEMPRE con cwd = instalación (hub.syn y cli.syn); los procesos de workspace no lo usan.
|
|
17
|
+
|
|
18
|
+
use "./tools/bash.syn" as t_bash
|
|
19
|
+
use "./tools/proc.syn" as proc
|
|
20
|
+
use "./settings.syn" as settings
|
|
21
|
+
|
|
22
|
+
export let ROOT be ".lampson/ws"
|
|
23
|
+
export let REGISTRY be ".lampson/workspaces.json"
|
|
24
|
+
export let HUB_PORT be 8080
|
|
25
|
+
export let HUB_FILE be "hub.syn"
|
|
26
|
+
export let HUB_TEMPLATE be "hub.tpl.syn"
|
|
27
|
+
let FIRST_PORT be 8081
|
|
28
|
+
let LINKS be ["lib", "public", "skills", "lamps", "memory"]
|
|
29
|
+
let COPIES be ["web.syn", "chat.syn"]
|
|
30
|
+
|
|
31
|
+
task is_win()
|
|
32
|
+
give env("OS", "") == "Windows_NT"
|
|
33
|
+
|
|
34
|
+
task sh(cmd, t)
|
|
35
|
+
let sc be t_bash.shell_config()
|
|
36
|
+
give run(sc["shell"], [sc["flag"], cmd], t)
|
|
37
|
+
|
|
38
|
+
-- ruta absoluta de la instalación (cwd), en forma Windows si corresponde
|
|
39
|
+
export task home()
|
|
40
|
+
require exec
|
|
41
|
+
require env("LAMPSON_*")
|
|
42
|
+
require env("OS")
|
|
43
|
+
let h be env("LAMPSON_HOME", "")
|
|
44
|
+
when h != ""
|
|
45
|
+
give h
|
|
46
|
+
let r be sh("pwd -W 2>/dev/null || pwd -P", 10)
|
|
47
|
+
let p be trim(text(r["stdout"]))
|
|
48
|
+
when is_win()
|
|
49
|
+
give replace_text(p, "/", "\\")
|
|
50
|
+
give p
|
|
51
|
+
|
|
52
|
+
export task norm(p)
|
|
53
|
+
let s be lower(replace_text(trim(text(p)), "\\", "/"))
|
|
54
|
+
when matches(s, "/[a-z]/.*")
|
|
55
|
+
set s to slice(s, 1, 2) + ":" + slice(s, 2, length(s))
|
|
56
|
+
while length(s) > 1 and slice(s, length(s) - 1, length(s)) == "/"
|
|
57
|
+
set s to slice(s, 0, length(s) - 1)
|
|
58
|
+
give s
|
|
59
|
+
|
|
60
|
+
-- el mismo slug que memo.slug(): nombre limpio + 8 hex del hash de la ruta normalizada → las sesiones y la memoria
|
|
61
|
+
-- guardadas hasta hoy siguen asociadas al mismo workspace
|
|
62
|
+
export task slug_of(path)
|
|
63
|
+
let n be norm(path)
|
|
64
|
+
let parts be where(split(n, "/"), (x) => x != "")
|
|
65
|
+
let base be when length(parts) > 0 then parts[length(parts) - 1] otherwise "workspace"
|
|
66
|
+
let runs be find_all(base, "[a-z0-9]+")
|
|
67
|
+
let clean be when length(runs) == 0 then "project" otherwise join(runs, "-")
|
|
68
|
+
give clean + "-" + slice(decode(sha256(n), "hex"), 0, 8)
|
|
69
|
+
|
|
70
|
+
-- ---------- registro ----------
|
|
71
|
+
task load_reg()
|
|
72
|
+
try
|
|
73
|
+
let doc be json_decode(read_file(REGISTRY))
|
|
74
|
+
when not contains(doc, "workspaces")
|
|
75
|
+
set doc["workspaces"] to {}
|
|
76
|
+
when not contains(doc, "next_port")
|
|
77
|
+
set doc["next_port"] to FIRST_PORT
|
|
78
|
+
give doc
|
|
79
|
+
recover err
|
|
80
|
+
give {"workspaces": {}, "next_port": FIRST_PORT}
|
|
81
|
+
|
|
82
|
+
task save_reg(doc)
|
|
83
|
+
write_file(REGISTRY, json_encode(doc))
|
|
84
|
+
|
|
85
|
+
export task all()
|
|
86
|
+
require file(".lampson")
|
|
87
|
+
require file(".lampson/*")
|
|
88
|
+
let doc be load_reg()
|
|
89
|
+
let out be []
|
|
90
|
+
each k in sort_by(keys(doc["workspaces"]), (x) => 0 - doc["workspaces"][x]["last_used"])
|
|
91
|
+
set out to append(out, doc["workspaces"][k])
|
|
92
|
+
give out
|
|
93
|
+
|
|
94
|
+
export task get(slug)
|
|
95
|
+
require file(".lampson")
|
|
96
|
+
require file(".lampson/*")
|
|
97
|
+
let doc be load_reg()
|
|
98
|
+
when contains(doc["workspaces"], slug)
|
|
99
|
+
give doc["workspaces"][slug]
|
|
100
|
+
give nothing
|
|
101
|
+
|
|
102
|
+
export task by_path(path)
|
|
103
|
+
require file(".lampson")
|
|
104
|
+
require file(".lampson/*")
|
|
105
|
+
let n be norm(path)
|
|
106
|
+
each w in all()
|
|
107
|
+
when norm(w["path"]) == n
|
|
108
|
+
give w
|
|
109
|
+
give nothing
|
|
110
|
+
|
|
111
|
+
export task put(w)
|
|
112
|
+
require file(".lampson")
|
|
113
|
+
require file(".lampson/*")
|
|
114
|
+
require time
|
|
115
|
+
let doc be load_reg()
|
|
116
|
+
set doc["workspaces"][w["slug"]] to w
|
|
117
|
+
save_reg(doc)
|
|
118
|
+
give w
|
|
119
|
+
|
|
120
|
+
export task touch(slug)
|
|
121
|
+
require file(".lampson")
|
|
122
|
+
require file(".lampson/*")
|
|
123
|
+
require time
|
|
124
|
+
let w be get(slug)
|
|
125
|
+
when w == nothing
|
|
126
|
+
give false
|
|
127
|
+
set w["last_used"] to now()
|
|
128
|
+
put(w)
|
|
129
|
+
give true
|
|
130
|
+
|
|
131
|
+
-- ---------- directorio del workspace ----------
|
|
132
|
+
export task dir_of(slug)
|
|
133
|
+
give ROOT + "/" + slug
|
|
134
|
+
|
|
135
|
+
task link(where_, target)
|
|
136
|
+
when is_win()
|
|
137
|
+
let r be run("cmd", ["/c", "mklink", "/J", replace_text(where_, "/", "\\"), replace_text(target, "/", "\\")], 15)
|
|
138
|
+
give r["exit_code"] == 0
|
|
139
|
+
let r be run("ln", ["-s", target, where_], 10)
|
|
140
|
+
give r["exit_code"] == 0
|
|
141
|
+
|
|
142
|
+
task unlink(where_)
|
|
143
|
+
when is_win()
|
|
144
|
+
run("cmd", ["/c", "rmdir", replace_text(where_, "/", "\\")], 10)
|
|
145
|
+
otherwise
|
|
146
|
+
run("rm", ["-f", where_], 10)
|
|
147
|
+
give true
|
|
148
|
+
|
|
149
|
+
task is_link_dir(p)
|
|
150
|
+
give file_exists(p)
|
|
151
|
+
|
|
152
|
+
-- crea/repara .lampson/ws/<slug>: junctions al proyecto y a la instalación, copias de los entries
|
|
153
|
+
export task prepare(w)
|
|
154
|
+
require exec
|
|
155
|
+
require time
|
|
156
|
+
require env("LAMPSON_*")
|
|
157
|
+
require env("OS")
|
|
158
|
+
require file(".lampson")
|
|
159
|
+
require file(".lampson/*")
|
|
160
|
+
require file.read("web.syn")
|
|
161
|
+
require file.read("chat.syn")
|
|
162
|
+
let d be dir_of(w["slug"])
|
|
163
|
+
let h be home()
|
|
164
|
+
let sep be when is_win() then "\\" otherwise "/"
|
|
165
|
+
let fresh be not file_exists(d + "/.lampson/.keep")
|
|
166
|
+
write_file(d + "/.lampson/.keep", "")
|
|
167
|
+
when fresh
|
|
168
|
+
migrate(w, d)
|
|
169
|
+
-- el proyecto (si la junction apunta a otro lado — carpeta movida — se rehace)
|
|
170
|
+
let ws_link be d + "/workspace"
|
|
171
|
+
when file_exists(ws_link)
|
|
172
|
+
let target be current_target(ws_link)
|
|
173
|
+
when target != "" and norm(target) != norm(w["path"])
|
|
174
|
+
unlink(ws_link)
|
|
175
|
+
when not file_exists(ws_link)
|
|
176
|
+
when not link(ws_link, w["path"])
|
|
177
|
+
raise("could not link " + ws_link + " → " + w["path"])
|
|
178
|
+
each n in LINKS
|
|
179
|
+
when not file_exists(d + "/" + n)
|
|
180
|
+
link(d + "/" + n, h + sep + n)
|
|
181
|
+
when not file_exists(d + "/.lampson/global")
|
|
182
|
+
link(d + "/.lampson/global", h + sep + ".lampson")
|
|
183
|
+
each f in COPIES
|
|
184
|
+
write_file(d + "/" + f, read_file(f))
|
|
185
|
+
give d
|
|
186
|
+
|
|
187
|
+
-- estado guardado ANTES de los workspaces (en <home>/.lampson, separado por slug en cada registro): sesiones y trazas
|
|
188
|
+
-- del proyecto, sus tareas programadas y el encendido de lámparas se mueven a <ws>/.lampson la primera vez
|
|
189
|
+
task migrate(w, d)
|
|
190
|
+
let slug be w["slug"]
|
|
191
|
+
let moved be 0
|
|
192
|
+
try
|
|
193
|
+
each e in list_dir(".lampson/sessions")
|
|
194
|
+
when not e["is_dir"]
|
|
195
|
+
let doc be nothing
|
|
196
|
+
try
|
|
197
|
+
set doc to json_decode(read_file(".lampson/sessions/" + e["name"]))
|
|
198
|
+
recover err
|
|
199
|
+
set doc to nothing
|
|
200
|
+
when doc != nothing and contains(doc, "meta") and contains(doc["meta"], "project") and doc["meta"]["project"] == slug
|
|
201
|
+
let id be replace_text(e["name"], ".json", "")
|
|
202
|
+
sh("mkdir -p '" + d + "/.lampson/sessions' '" + d + "/.lampson/trace' && mv -f '.lampson/sessions/" + e["name"] + "' '" + d + "/.lampson/sessions/' && mv -f .lampson/trace/" + id + ".* '" + d + "/.lampson/trace/' 2>/dev/null; true", 20)
|
|
203
|
+
set moved to moved + 1
|
|
204
|
+
recover err
|
|
205
|
+
set moved to moved
|
|
206
|
+
try
|
|
207
|
+
let sdoc be json_decode(read_file(".lampson/schedules.json"))
|
|
208
|
+
let mine be where(sdoc["tasks"], (t) => contains(t, "project") and t["project"] == slug)
|
|
209
|
+
when length(mine) > 0
|
|
210
|
+
write_file(d + "/.lampson/schedules.json", json_encode({"tasks": mine, "seq": when contains(sdoc, "seq") then sdoc["seq"] otherwise length(mine)}))
|
|
211
|
+
set sdoc["tasks"] to where(sdoc["tasks"], (t) => not (contains(t, "project") and t["project"] == slug))
|
|
212
|
+
write_file(".lampson/schedules.json", json_encode(sdoc))
|
|
213
|
+
each t in mine
|
|
214
|
+
sh("mkdir -p '" + d + "/.lampson/schedules' && mv -f '.lampson/schedules/" + t["id"] + ".log' '" + d + "/.lampson/schedules/' 2>/dev/null; true", 10)
|
|
215
|
+
recover err
|
|
216
|
+
set moved to moved
|
|
217
|
+
try
|
|
218
|
+
when file_exists(".lampson/lamps.json")
|
|
219
|
+
write_file(d + "/.lampson/lamps.json", read_file(".lampson/lamps.json"))
|
|
220
|
+
recover err
|
|
221
|
+
set moved to moved
|
|
222
|
+
give moved
|
|
223
|
+
|
|
224
|
+
-- destino real de una junction/symlink (Git Bash: pwd -W dentro)
|
|
225
|
+
task current_target(p)
|
|
226
|
+
try
|
|
227
|
+
let r be sh("cd '" + p + "' && (pwd -W 2>/dev/null || pwd -P)", 10)
|
|
228
|
+
give trim(text(r["stdout"]))
|
|
229
|
+
recover err
|
|
230
|
+
give ""
|
|
231
|
+
|
|
232
|
+
-- registra (si hace falta) y prepara el workspace de una carpeta → la entrada del registro
|
|
233
|
+
export task ensure(path)
|
|
234
|
+
require exec
|
|
235
|
+
require time
|
|
236
|
+
require env("LAMPSON_*")
|
|
237
|
+
require env("OS")
|
|
238
|
+
require file(".lampson")
|
|
239
|
+
require file(".lampson/*")
|
|
240
|
+
require file.read("web.syn")
|
|
241
|
+
require file.read("chat.syn")
|
|
242
|
+
require file.read("hub.tpl.syn")
|
|
243
|
+
require file("hub.syn")
|
|
244
|
+
let existing be by_path(path)
|
|
245
|
+
let w be existing
|
|
246
|
+
when w == nothing
|
|
247
|
+
let doc be load_reg()
|
|
248
|
+
let slug be slug_of(path)
|
|
249
|
+
when contains(doc["workspaces"], slug)
|
|
250
|
+
-- mismo slug, otra ruta (mayúsculas/minúsculas, junction): reasociar
|
|
251
|
+
set w to doc["workspaces"][slug]
|
|
252
|
+
set w["path"] to text(path)
|
|
253
|
+
otherwise
|
|
254
|
+
let port be doc["next_port"]
|
|
255
|
+
set doc["next_port"] to port + 1
|
|
256
|
+
save_reg(doc)
|
|
257
|
+
let parts be where(split(replace_text(text(path), "\\", "/"), "/"), (x) => x != "")
|
|
258
|
+
set w to {"slug": slug, "name": parts[length(parts) - 1], "path": text(path), "port": port, "policy": "auto", "created": now(), "last_used": now()}
|
|
259
|
+
put(w)
|
|
260
|
+
when regen_hub() and hub_alive()
|
|
261
|
+
restart_hub_later()
|
|
262
|
+
prepare(w)
|
|
263
|
+
give w
|
|
264
|
+
|
|
265
|
+
export task remove(slug)
|
|
266
|
+
require exec
|
|
267
|
+
require time
|
|
268
|
+
require env("LAMPSON_*")
|
|
269
|
+
require env("OS")
|
|
270
|
+
require file(".lampson")
|
|
271
|
+
require file(".lampson/*")
|
|
272
|
+
require file.read("hub.tpl.syn")
|
|
273
|
+
require file("hub.syn")
|
|
274
|
+
require net("127.0.0.1")
|
|
275
|
+
let w be get(slug)
|
|
276
|
+
when w == nothing
|
|
277
|
+
raise("no workspace '" + text(slug) + "'")
|
|
278
|
+
stop_ws(w)
|
|
279
|
+
let doc be load_reg()
|
|
280
|
+
let keep be {}
|
|
281
|
+
each k in keys(doc["workspaces"])
|
|
282
|
+
when k != slug
|
|
283
|
+
set keep[k] to doc["workspaces"][k]
|
|
284
|
+
set doc["workspaces"] to keep
|
|
285
|
+
save_reg(doc)
|
|
286
|
+
when regen_hub() and hub_alive()
|
|
287
|
+
restart_hub_later()
|
|
288
|
+
give "workspace '" + slug + "' quitado del registro (la carpeta " + dir_of(slug) + " y el proyecto quedan en disco)"
|
|
289
|
+
|
|
290
|
+
-- ---------- hub generado ----------
|
|
291
|
+
task hub_routes(w)
|
|
292
|
+
let base be "http://127.0.0.1:" + text(w["port"])
|
|
293
|
+
let lines be []
|
|
294
|
+
each m in ["GET", "POST"]
|
|
295
|
+
set lines to append(lines, " route \"" + m + " /w/" + w["slug"] + "/api/*path\"\n proxy to \"" + base + "\"")
|
|
296
|
+
set lines to append(lines, " route \"GET /w/" + w["slug"] + "/approve/*path\"\n proxy to \"" + base + "\"")
|
|
297
|
+
give join(lines, "\n")
|
|
298
|
+
|
|
299
|
+
-- reescribe hub.syn desde la plantilla si cambió (bajo --watch, el hub se reinicia solo)
|
|
300
|
+
export task regen_hub()
|
|
301
|
+
require file(".lampson")
|
|
302
|
+
require file(".lampson/*")
|
|
303
|
+
require file.read("hub.tpl.syn")
|
|
304
|
+
require file("hub.syn")
|
|
305
|
+
let tpl be read_file(HUB_TEMPLATE)
|
|
306
|
+
let blocks be []
|
|
307
|
+
each w in all()
|
|
308
|
+
set blocks to append(blocks, hub_routes(w))
|
|
309
|
+
let body be replace_text(tpl, " -- @workspaces", when length(blocks) == 0 then " -- (sin workspaces registrados todavía)" otherwise join(blocks, "\n"))
|
|
310
|
+
let cur be ""
|
|
311
|
+
try
|
|
312
|
+
set cur to read_file(HUB_FILE)
|
|
313
|
+
recover err
|
|
314
|
+
set cur to ""
|
|
315
|
+
when cur != body
|
|
316
|
+
write_file(HUB_FILE, body)
|
|
317
|
+
give true
|
|
318
|
+
give false
|
|
319
|
+
|
|
320
|
+
-- ---------- procesos ----------
|
|
321
|
+
-- lanzar DESACOPLADO (sobrevive al request y al proceso que lo lanzó); log en <ws>/.lampson/web.log
|
|
322
|
+
-- Windows: `start /b` hereda los handles de stdout → quien capture la salida del lanzador (PowerShell, un pipe)
|
|
323
|
+
-- se queda esperando al nieto. Start-Process con redirección crea handles nuevos: el nieto queda suelto de verdad.
|
|
324
|
+
task spawn_detached(cwd, cmdline, envs)
|
|
325
|
+
when is_win()
|
|
326
|
+
-- WorkingDirectory absoluto: el relativo se resolvería contra el cwd del PowerShell (que ya es `cwd`)
|
|
327
|
+
let h be home()
|
|
328
|
+
let abs be when cwd == "." then h otherwise h + "\\" + replace_text(cwd, "/", "\\")
|
|
329
|
+
let ps be "Start-Process -FilePath cmd -ArgumentList '/c', '" + replace_text(cmdline, "'", "''") + "' -WorkingDirectory '" + replace_text(abs, "'", "''") + "' -WindowStyle Hidden"
|
|
330
|
+
let p be proc_spawn("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], {"cwd": cwd, "env": envs, "process_group": false})
|
|
331
|
+
proc_wait(p, 15)
|
|
332
|
+
proc_close(p)
|
|
333
|
+
otherwise
|
|
334
|
+
let p be proc_spawn("sh", ["-c", "nohup " + cmdline + " >/dev/null 2>&1 &"], {"cwd": cwd, "env": envs, "process_group": false})
|
|
335
|
+
proc_wait(p, 5)
|
|
336
|
+
proc_close(p)
|
|
337
|
+
give true
|
|
338
|
+
|
|
339
|
+
export task start_ws(w)
|
|
340
|
+
require exec
|
|
341
|
+
require time
|
|
342
|
+
require env("LAMPSON_*")
|
|
343
|
+
require env("OS")
|
|
344
|
+
require file(".lampson")
|
|
345
|
+
require file(".lampson/*")
|
|
346
|
+
require file.read("web.syn")
|
|
347
|
+
require file.read("chat.syn")
|
|
348
|
+
require file.read(".env")
|
|
349
|
+
let d be prepare(w)
|
|
350
|
+
let h be home()
|
|
351
|
+
let sep be when is_win() then "\\" otherwise "/"
|
|
352
|
+
let envf be h + sep + ".env"
|
|
353
|
+
let envs be {"LAMPSON_WS": w["slug"], "LAMPSON_WORKSPACE": w["path"], "LAMPSON_PORT": text(w["port"]), "LAMPSON_HOME": h}
|
|
354
|
+
let exe be env("LAMPSON_SYNSEMA", "synsema")
|
|
355
|
+
-- el .env de la instalación (cwd = home; la capability es relativa, la ruta que se pasa al hijo es absoluta)
|
|
356
|
+
let cmd be exe + " serve web.syn --port " + text(w["port"]) + " --bind 127.0.0.1" + (when file_exists(".env") then " --env-file \"" + envf + "\"" otherwise "") + " > .lampson/web.log 2>&1"
|
|
357
|
+
spawn_detached(d, cmd, envs)
|
|
358
|
+
give true
|
|
359
|
+
|
|
360
|
+
-- ¿responde? → {alive, last_used, schedules_on, version} | nothing
|
|
361
|
+
export task health(w)
|
|
362
|
+
require net("127.0.0.1")
|
|
363
|
+
require time
|
|
364
|
+
try
|
|
365
|
+
let r be http_get("http://127.0.0.1:" + text(w["port"]) + "/w/" + w["slug"] + "/api/health", {"timeout": 2})
|
|
366
|
+
when r["ok"]
|
|
367
|
+
give json_decode(body of r)
|
|
368
|
+
give nothing
|
|
369
|
+
recover err
|
|
370
|
+
give nothing
|
|
371
|
+
|
|
372
|
+
-- esperar hasta que responda (arranque)
|
|
373
|
+
export task wait_ready(w, secs)
|
|
374
|
+
require net("127.0.0.1")
|
|
375
|
+
require time
|
|
376
|
+
let deadline be now() + secs
|
|
377
|
+
while now() < deadline
|
|
378
|
+
when health(w) != nothing
|
|
379
|
+
give true
|
|
380
|
+
sleep(0.4)
|
|
381
|
+
give false
|
|
382
|
+
|
|
383
|
+
export task stop_ws(w)
|
|
384
|
+
require exec
|
|
385
|
+
require time
|
|
386
|
+
require env("LAMPSON_*")
|
|
387
|
+
require env("OS")
|
|
388
|
+
require net("127.0.0.1")
|
|
389
|
+
let victims be where(proc.listeners(), (l) => l["port"] == w["port"])
|
|
390
|
+
each l in victims
|
|
391
|
+
proc.kill_pid(l["pid"])
|
|
392
|
+
give length(victims) > 0
|
|
393
|
+
|
|
394
|
+
-- ---------- hub ----------
|
|
395
|
+
export task hub_alive()
|
|
396
|
+
require net("127.0.0.1")
|
|
397
|
+
require time
|
|
398
|
+
try
|
|
399
|
+
let r be http_get("http://127.0.0.1:" + text(HUB_PORT) + "/api/hub", {"timeout": 2})
|
|
400
|
+
give r["ok"]
|
|
401
|
+
recover err
|
|
402
|
+
give false
|
|
403
|
+
|
|
404
|
+
export task start_hub()
|
|
405
|
+
require exec
|
|
406
|
+
require time
|
|
407
|
+
require env("LAMPSON_*")
|
|
408
|
+
require env("OS")
|
|
409
|
+
require file(".lampson")
|
|
410
|
+
require file(".lampson/*")
|
|
411
|
+
require file.read("hub.tpl.syn")
|
|
412
|
+
require file("hub.syn")
|
|
413
|
+
regen_hub()
|
|
414
|
+
-- desacoplado y con cwd = instalación (synsema daemon arranca el serve con otro cwd y los `use "./lib/…"`,
|
|
415
|
+
-- que son relativos al cwd, no se encuentran). Sin --watch: en esta carpeta el watcher recorre las junctions de
|
|
416
|
+
-- los workspaces (proyectos enteros) y el server nunca llega a escuchar; el reinicio lo hace restart_hub_later.
|
|
417
|
+
let exe be env("LAMPSON_SYNSEMA", "synsema")
|
|
418
|
+
spawn_detached(".", exe + " serve " + HUB_FILE + " > .lampson/hub.log 2>&1", {"LAMPSON_HOME": home()})
|
|
419
|
+
give true
|
|
420
|
+
|
|
421
|
+
-- hub.syn cambió (workspace nuevo/quitado): un helper DESACOPLADO (cli.syn hub-restart) para el hub y lo vuelve a
|
|
422
|
+
-- arrancar — puede llamarse desde el propio hub (que va a morir en el medio) o desde cli.syn
|
|
423
|
+
export task restart_hub_later()
|
|
424
|
+
require exec
|
|
425
|
+
require time
|
|
426
|
+
require env("LAMPSON_*")
|
|
427
|
+
require env("OS")
|
|
428
|
+
let exe be env("LAMPSON_SYNSEMA", "synsema")
|
|
429
|
+
spawn_detached(".", exe + " run cli.syn > .lampson/hub-restart.log 2>&1", {"LAMPSON_HOME": home(), "LAMPSON_CMD": "hub-restart"})
|
|
430
|
+
give true
|
|
431
|
+
|
|
432
|
+
export task stop_hub()
|
|
433
|
+
require exec
|
|
434
|
+
require time
|
|
435
|
+
require env("LAMPSON_*")
|
|
436
|
+
require env("OS")
|
|
437
|
+
let victims be where(proc.listeners(), (l) => l["port"] == HUB_PORT)
|
|
438
|
+
each l in victims
|
|
439
|
+
proc.kill_pid(l["pid"])
|
|
440
|
+
give length(victims) > 0
|
|
441
|
+
|
|
442
|
+
-- ---------- política y supervisor (tick del hub) ----------
|
|
443
|
+
export task idle_hours()
|
|
444
|
+
require file.read(".lampson")
|
|
445
|
+
require file.read(".lampson/*")
|
|
446
|
+
require env("LAMPSON_*")
|
|
447
|
+
let doc be settings.load()
|
|
448
|
+
when contains(doc, "idle_hours")
|
|
449
|
+
let v be number(text(doc["idle_hours"]))
|
|
450
|
+
give when v < 0 then 0 otherwise v
|
|
451
|
+
give 4
|
|
452
|
+
|
|
453
|
+
-- ¿debe estar vivo? always | auto (usado hace < idle_hours, o con tareas encendidas) | off
|
|
454
|
+
export task should_live(w, hc)
|
|
455
|
+
require file.read(".lampson")
|
|
456
|
+
require file.read(".lampson/*")
|
|
457
|
+
require env("LAMPSON_*")
|
|
458
|
+
require time
|
|
459
|
+
let pol be when contains(w, "policy") then w["policy"] otherwise "auto"
|
|
460
|
+
when pol == "always"
|
|
461
|
+
give true
|
|
462
|
+
when pol == "off"
|
|
463
|
+
give false
|
|
464
|
+
when hc != nothing and contains(hc, "schedules_on") and hc["schedules_on"] > 0
|
|
465
|
+
give true
|
|
466
|
+
when schedules_on_disk(w) > 0
|
|
467
|
+
give true
|
|
468
|
+
let ih be idle_hours()
|
|
469
|
+
when ih == 0
|
|
470
|
+
give true
|
|
471
|
+
let last be when hc != nothing and contains(hc, "last_used") and hc["last_used"] != nothing then hc["last_used"] otherwise w["last_used"]
|
|
472
|
+
give now() - last < ih * 3600
|
|
473
|
+
|
|
474
|
+
-- tareas encendidas según el json del workspace (sirve aunque el proceso esté apagado)
|
|
475
|
+
task schedules_on_disk(w)
|
|
476
|
+
try
|
|
477
|
+
let doc be json_decode(read_file(dir_of(w["slug"]) + "/.lampson/schedules.json"))
|
|
478
|
+
give length(where(doc["tasks"], (t) => t["enabled"] == true))
|
|
479
|
+
recover err
|
|
480
|
+
give 0
|
|
481
|
+
|
|
482
|
+
-- un tick: arranca lo que debe vivir y no responde; apaga lo que no debe y responde
|
|
483
|
+
export task tick()
|
|
484
|
+
require exec
|
|
485
|
+
require time
|
|
486
|
+
require env("LAMPSON_*")
|
|
487
|
+
require env("OS")
|
|
488
|
+
require net("127.0.0.1")
|
|
489
|
+
require file(".lampson")
|
|
490
|
+
require file(".lampson/*")
|
|
491
|
+
require file.read("web.syn")
|
|
492
|
+
require file.read("chat.syn")
|
|
493
|
+
let started be []
|
|
494
|
+
let stopped be []
|
|
495
|
+
each w in all()
|
|
496
|
+
let hc be health(w)
|
|
497
|
+
let want be should_live(w, hc)
|
|
498
|
+
when want and hc == nothing
|
|
499
|
+
start_ws(w)
|
|
500
|
+
set started to append(started, w["slug"])
|
|
501
|
+
otherwise when not want and hc != nothing
|
|
502
|
+
stop_ws(w)
|
|
503
|
+
set stopped to append(stopped, w["slug"])
|
|
504
|
+
give {"started": started, "stopped": stopped}
|
|
505
|
+
|
|
506
|
+
-- resumen para la UI del hub
|
|
507
|
+
export task summary()
|
|
508
|
+
require exec
|
|
509
|
+
require time
|
|
510
|
+
require env("LAMPSON_*")
|
|
511
|
+
require env("OS")
|
|
512
|
+
require net("127.0.0.1")
|
|
513
|
+
require file(".lampson")
|
|
514
|
+
require file(".lampson/*")
|
|
515
|
+
let out be []
|
|
516
|
+
each w in all()
|
|
517
|
+
let hc be health(w)
|
|
518
|
+
set out to append(out, {"slug": w["slug"], "name": w["name"], "path": w["path"], "port": w["port"], "policy": when contains(w, "policy") then w["policy"] otherwise "auto", "created": w["created"], "last_used": when hc != nothing and contains(hc, "last_used") and hc["last_used"] != nothing then hc["last_used"] otherwise w["last_used"], "alive": hc != nothing, "schedules_on": when hc != nothing and contains(hc, "schedules_on") then hc["schedules_on"] otherwise schedules_on_disk(w), "url": "/w/" + w["slug"]})
|
|
519
|
+
give out
|
|
520
|
+
|
|
521
|
+
-- ---------- explorador de carpetas ----------
|
|
522
|
+
-- diálogo nativo del SO (PC con escritorio). Devuelve la ruta elegida, "" si canceló, nothing si no hay diálogo.
|
|
523
|
+
export task pick_folder()
|
|
524
|
+
require exec
|
|
525
|
+
require env("LAMPSON_*")
|
|
526
|
+
require env("OS")
|
|
527
|
+
try
|
|
528
|
+
when is_win()
|
|
529
|
+
let ps be "Add-Type -AssemblyName System.Windows.Forms; $d = New-Object System.Windows.Forms.FolderBrowserDialog; $d.Description = 'Elegí la carpeta del proyecto (workspace de Lampson)'; $d.ShowNewFolderButton = $true; $f = New-Object System.Windows.Forms.Form; $f.TopMost = $true; if ($d.ShowDialog($f) -eq 'OK') { Write-Output $d.SelectedPath }"
|
|
530
|
+
let r be run("powershell", ["-NoProfile", "-STA", "-Command", ps], 300)
|
|
531
|
+
give trim(text(r["stdout"]))
|
|
532
|
+
let r be run("sh", ["-c", "if command -v osascript >/dev/null; then osascript -e 'POSIX path of (choose folder with prompt \"Carpeta del proyecto\")'; elif command -v zenity >/dev/null; then zenity --file-selection --directory --title='Carpeta del proyecto'; else exit 3; fi"], 300)
|
|
533
|
+
when r["exit_code"] == 3
|
|
534
|
+
give nothing
|
|
535
|
+
give trim(text(r["stdout"]))
|
|
536
|
+
recover err
|
|
537
|
+
give nothing
|
|
538
|
+
|
|
539
|
+
-- listado de subcarpetas por shell (VPS sin escritorio): sin capabilities de archivo sobre el disco
|
|
540
|
+
export task browse(path)
|
|
541
|
+
require exec
|
|
542
|
+
require env("LAMPSON_*")
|
|
543
|
+
require env("OS")
|
|
544
|
+
let p be when path == nothing or trim(text(path)) == "" then env("HOME", env("USERPROFILE", "/")) otherwise text(path)
|
|
545
|
+
let r be sh("cd \"" + replace_text(p, "\"", "") + "\" 2>/dev/null && (pwd -W 2>/dev/null || pwd -P) && ls -1 -d -- */ 2>/dev/null", 10)
|
|
546
|
+
let lines be where(split(replace_text(text(r["stdout"]), "\r", ""), "\n"), (x) => trim(x) != "")
|
|
547
|
+
when length(lines) == 0
|
|
548
|
+
give {"path": p, "dirs": [], "error": "no existe o no se puede leer"}
|
|
549
|
+
let here be lines[0]
|
|
550
|
+
when is_win()
|
|
551
|
+
set here to replace_text(here, "/", "\\")
|
|
552
|
+
let dirs be apply(slice(lines, 1, length(lines)), (d) => replace_text(d, "/", ""))
|
|
553
|
+
let parts be where(split(replace_text(here, "\\", "/"), "/"), (x) => x != "")
|
|
554
|
+
let parent be when length(parts) > 1 then join(slice(parts, 0, length(parts) - 1), when is_win() then "\\" otherwise "/") otherwise (when is_win() then parts[0] + "\\" otherwise "/")
|
|
555
|
+
give {"path": here, "parent": parent, "dirs": where(dirs, (d) => not starts_with(d, "."))}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lampson",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A coding agent written in Synsema: terminal + web, tools confined to the mounted project, lamps (your own tool plugins), LSP, MCP, sub-agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"lamps/",
|
|
22
22
|
"chat.syn",
|
|
23
23
|
"web.syn",
|
|
24
|
+
"hub.tpl.syn",
|
|
25
|
+
"cli.syn",
|
|
24
26
|
"lampson.ps1",
|
|
25
27
|
"lampson.sh",
|
|
26
28
|
"lampson.cmd",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/* chat: mensajes, pasos del agente, tarjeta de permiso, portada (hero) */
|
|
2
|
+
/* mensajes: el del usuario es una nota al margen (como el blockquote de la doc), el del agente es prosa */
|
|
3
|
+
.msg { margin:0 0 16px; }
|
|
4
|
+
.user { margin:26px 0 16px; padding:.85em 1.1em; background:var(--paper-2); border-left:2px solid var(--accent); white-space:pre-wrap; font-size:15px; }
|
|
5
|
+
.user::before { content:"vos"; display:block; font:400 10px/1 var(--mono); letter-spacing:.12em; text-transform:uppercase; color:var(--accent); margin-bottom:6px; }
|
|
6
|
+
.assistant { font-size:15px; line-height:1.62; }
|
|
7
|
+
.assistant p { margin:0 0 .9em; }
|
|
8
|
+
.assistant code { font:400 .86em/1.5 var(--mono); background:var(--paper-2); border-radius:2px; padding:.08em .3em; color:var(--ink); }
|
|
9
|
+
.assistant pre { background:var(--paper-2); border:1px solid var(--rule); border-radius:var(--r); padding:14px 16px; overflow-x:auto; margin:1em 0; }
|
|
10
|
+
.assistant pre code { background:none; padding:0; font-size:13px; line-height:1.62; }
|
|
11
|
+
.assistant ul, .assistant ol { margin:0 0 .9em; padding-left:1.25em; }
|
|
12
|
+
.assistant li { margin:.3em 0; }
|
|
13
|
+
.assistant li::marker { color:var(--ink-3); }
|
|
14
|
+
.assistant h1, .assistant h2, .assistant h3 { font:600 1.06rem/1.4 var(--serif); margin:1.4em 0 .5em; }
|
|
15
|
+
.assistant b, .assistant strong { font-weight:600; }
|
|
16
|
+
|
|
17
|
+
/* pasos del agente: maquinaria, en mono */
|
|
18
|
+
.step { display:grid; grid-template-columns:16px 1fr; column-gap:8px; margin:0 0 7px 2px; font:400 12.5px/1.5 var(--mono); color:var(--ink-2); }
|
|
19
|
+
.step .ic { text-align:center; color:var(--ink-3); }
|
|
20
|
+
.step .ok { color:var(--str); }
|
|
21
|
+
.step .bad { color:var(--rubric); }
|
|
22
|
+
.step .name { color:var(--ink); }
|
|
23
|
+
.step .cmd { white-space:pre-wrap; word-break:break-word; max-height:4.6em; overflow:hidden; }
|
|
24
|
+
.step .cmd.open { max-height:none; }
|
|
25
|
+
.step .more { color:var(--accent); cursor:pointer; font-size:11px; }
|
|
26
|
+
.step details { grid-column:2; min-width:0; }
|
|
27
|
+
.step summary { cursor:pointer; list-style:none; color:var(--ink-3); white-space:pre-wrap; word-break:break-word; max-height:3.2em; overflow:hidden; }
|
|
28
|
+
.step summary::before { content:"→ "; }
|
|
29
|
+
.step summary.bad { color:var(--rubric); }
|
|
30
|
+
.step pre { margin:6px 0 4px; padding:10px 12px; background:var(--paper-2); border:1px solid var(--rule); border-radius:var(--r); white-space:pre-wrap; max-height:340px; overflow:auto; font-size:12px; color:var(--ink-2); }
|
|
31
|
+
.denied { color:var(--rubric); font:400 12.5px/1.5 var(--mono); }
|
|
32
|
+
.meta { color:var(--ink-3); font:400 11px/1 var(--mono); letter-spacing:.03em; margin:4px 0 20px 2px; }
|
|
33
|
+
.working { display:flex; align-items:center; gap:8px; color:var(--ink-3); font:400 11.5px/1 var(--mono); margin:4px 0 14px 2px; }
|
|
34
|
+
.working .dot { width:7px; height:7px; border-radius:50%; background:var(--accent); animation:pulse 1s infinite; }
|
|
35
|
+
@keyframes pulse { 0%,100% { opacity:.25 } 50% { opacity:1 } }
|
|
36
|
+
|
|
37
|
+
/* permisos: lo único en rubric */
|
|
38
|
+
.approval .card { border:1px solid var(--rubric); border-radius:var(--r); padding:12px 14px; max-width:720px; }
|
|
39
|
+
.approval .why { color:var(--rubric); font:600 12px/1.5 var(--mono); margin-bottom:6px; }
|
|
40
|
+
.approval code { font:400 12.5px/1.5 var(--mono); display:block; background:var(--paper-2); padding:8px 10px; border-radius:2px; margin-bottom:10px; white-space:pre-wrap; }
|
|
41
|
+
.approval .btns { display:flex; gap:8px; }
|
|
42
|
+
|
|
43
|
+
/* ---- hero: el primer golpe de vista ---- */
|
|
44
|
+
.empty { width:100%; max-width:1180px; margin:0 auto; color:var(--ink-2); }
|
|
45
|
+
.empty .eyebrow { font:400 10.5px/1 var(--mono); text-transform:uppercase; letter-spacing:.12em; color:var(--rubric); margin:0 0 12px; }
|
|
46
|
+
.empty h1 { font:600 2.1rem/1.15 var(--serif); letter-spacing:-.022em; color:var(--ink); margin:0 0 .4em; }
|
|
47
|
+
.empty .lead { font-size:1.06rem; margin:0 0 26px; max-width:720px; }
|
|
48
|
+
.empty .grid { display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); gap:14px; }
|
|
49
|
+
.empty .card { border:1px solid var(--rule); border-radius:var(--r); padding:14px 16px 12px; min-width:0; }
|
|
50
|
+
.empty .card h3 { font:400 10.5px/1 var(--mono); text-transform:uppercase; letter-spacing:.1em; color:var(--ink-3); margin:0 0 12px; }
|
|
51
|
+
.empty .card p { margin:0 0 8px; font-size:13.5px; line-height:1.5; }
|
|
52
|
+
.empty code { font:400 .88em/1.5 var(--mono); background:var(--paper-2); padding:.08em .3em; border-radius:2px; color:var(--ink); white-space:nowrap; }
|
|
53
|
+
.empty .k { font:600 12px/1 var(--mono); color:var(--ink); margin-right:.3em; }
|
|
54
|
+
.empty .try { cursor:pointer; color:var(--accent); }
|
|
55
|
+
.empty .try:hover { text-decoration:underline; text-underline-offset:.18em; }
|
|
56
|
+
@media (max-width:1400px) { .empty .grid { grid-template-columns:repeat(2, minmax(0,1fr)); } }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* hub: la pantalla de workspaces (public/hub.html) — solo cabecera y zona central, sin paneles laterales */
|
|
2
|
+
body.hub { grid-template-columns:1fr; grid-template-areas:"top" "main"; }
|
|
3
|
+
body.hub main { grid-template-rows:1fr; }
|
|
4
|
+
body.hub #stage { display:flex; align-items:center; padding:20px 40px; }
|
|
5
|
+
.wsgrid { display:grid; grid-template-columns:repeat(auto-fill, minmax(260px, 1fr)); gap:14px; margin-top:8px; }
|
|
6
|
+
.card.ws { display:block; text-decoration:none; color:inherit; cursor:pointer; transition:border-color .12s; }
|
|
7
|
+
.card.ws:hover { border-color:var(--ink-3); }
|
|
8
|
+
.card.ws.on { border-color:var(--accent); }
|
|
9
|
+
.card.ws.new { border-style:dashed; }
|
|
10
|
+
.card.ws .nm { font:600 16px/1.3 var(--serif); color:var(--ink); margin:0 0 4px; }
|
|
11
|
+
.card.ws .pth { font:400 11px/1.4 var(--mono); color:var(--ink-3); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin:0 0 6px; }
|
|
12
|
+
.card.ws .meta { font:400 11px/1.4 var(--mono); color:var(--ink-3); margin:0; }
|