lampson 0.2.6 → 0.2.8
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/README.md +16 -10
- package/bin/lampson.js +13 -7
- package/chat.syn +36 -31
- package/lampson.ps1 +11 -1
- package/lampson.sh +7 -0
- package/lib/agents.syn +10 -10
- package/lib/fs.syn +226 -0
- package/lib/loop.syn +9 -8
- package/lib/permission.syn +40 -14
- package/lib/plugins.syn +408 -0
- package/lib/prompt.syn +3 -2
- package/lib/sched_run.syn +10 -10
- package/lib/schedule.syn +36 -21
- package/lib/tools/fetch.syn +573 -0
- package/lib/tools/memo.syn +27 -5
- package/lib/tools/url.syn +220 -0
- package/lib/tools.syn +32 -30
- package/lib/workspaces.syn +34 -9
- package/package.json +12 -4
- package/{lamps/example-hello/lamp.json → plugins/example-hello/plugin.json} +2 -2
- package/plugins/example-hello/plugin.syn +19 -0
- package/public/css/panel.css +3 -3
- package/public/css/sidebar.css +21 -0
- package/public/hub.html +1 -1
- package/public/index.html +3 -3
- package/public/js/app.js +1 -1
- package/public/js/chat.js +4 -4
- package/public/js/core.js +1 -1
- package/public/js/memory.js +2 -1
- package/public/js/{lamps.js → plugins.js} +43 -41
- package/public/js/schedules.js +9 -9
- package/public/js/tree.js +212 -10
- package/public/js/workspaces.js +1 -1
- package/skills/lampson/SKILL.md +26 -11
- package/web.syn +32 -17
- package/lamps/example-hello/lamp.syn +0 -19
- package/lib/lamps.syn +0 -386
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
-- lib/tools/url.syn — URLs para la tool fetch y para permission.syn (puro, sin capacidades)
|
|
2
|
+
--
|
|
3
|
+
-- parse(url) → {ok, error?, url, scheme, host, port, path, query, origin, dir}
|
|
4
|
+
-- host_class(host) → "public" | "private" | "blocked"
|
|
5
|
+
-- sensitive(url) → motivo (texto) si la URL lleva una credencial, o nothing
|
|
6
|
+
-- resolve(base, href) → URL absoluta de un href (relativo o no), o nothing si no es navegable (#, javascript:)
|
|
7
|
+
--
|
|
8
|
+
-- Política de hosts (tomada de hermes url_safety.py, sin resolver DNS: el runtime no expone un resolver):
|
|
9
|
+
-- * "blocked": endpoints de metadata de la nube (169.254.169.254 y familia, metadata.google.internal).
|
|
10
|
+
-- Nunca son un destino legítimo: se deniegan SIEMPRE, incluso en yolo.
|
|
11
|
+
-- * "private": loopback, redes privadas (10/8, 172.16/12, 192.168/16, CGNAT 100.64/10), link-local,
|
|
12
|
+
-- localhost y sufijos internos. Legítimo para mirar el propio dev server → PIDE aprobación (yolo permite,
|
|
13
|
+
-- strict deniega). Un host numérico ambiguo (entero, hex, octal) cae aquí también.
|
|
14
|
+
-- * "public": el resto. Límite conocido: sin DNS no se detecta un nombre público que resuelve a una IP
|
|
15
|
+
-- privada (DNS rebinding); hermes lo cubre con un resolver, aquí queda documentado.
|
|
16
|
+
|
|
17
|
+
let SENSITIVE_PARAMS be ["access_token", "api_key", "apikey", "auth_token", "authorization", "awsaccesskeyid", "client_secret", "credential", "credentials", "jwt", "password", "passwd", "secret", "session_id", "signature", "token", "private_key", "x_amz_security_token", "x_amz_signature", "x-amz-security-token", "x-amz-signature"]
|
|
18
|
+
let SECRET_RE be "(?i)(\\bsk-(ant-|proj-)?[a-z0-9_-]{24,}|\\bghp_[a-z0-9]{20,}|\\bgithub_pat_[a-z0-9_]{20,}|\\bgho_[a-z0-9]{20,}|\\bglpat-[a-z0-9_-]{16,}|\\bxox[abprs]-[a-z0-9-]{10,}|\\bakia[0-9a-z]{16}\\b|\\baiza[0-9a-z_-]{30,}|-----begin [a-z ]*private key|\\beyj[a-z0-9_-]{20,}\\.eyj[a-z0-9_-]{20,})"
|
|
19
|
+
|
|
20
|
+
task fail(msg)
|
|
21
|
+
give {"ok": false, "error": msg}
|
|
22
|
+
|
|
23
|
+
export task parse(raw)
|
|
24
|
+
when raw == nothing
|
|
25
|
+
give fail("missing URL")
|
|
26
|
+
let u be trim(text(raw))
|
|
27
|
+
-- "https:// host" — el modelo a veces mete un espacio tras el esquema (hermes normalize_url_for_request)
|
|
28
|
+
set u to replace_re(u, "^([A-Za-z][A-Za-z0-9+.-]*://)\\s+", "\\1")
|
|
29
|
+
when u == ""
|
|
30
|
+
give fail("missing URL")
|
|
31
|
+
when length(u) > 2048
|
|
32
|
+
give fail("URL longer than 2048 chars")
|
|
33
|
+
when contains(u, " ") or contains(u, "\n") or contains(u, "\t")
|
|
34
|
+
give fail("URL contains whitespace: " + u)
|
|
35
|
+
let sp be split(u, "://")
|
|
36
|
+
when length(sp) < 2
|
|
37
|
+
give fail("not an absolute http(s) URL: " + u)
|
|
38
|
+
let scheme be lower(sp[0])
|
|
39
|
+
when scheme != "http" and scheme != "https"
|
|
40
|
+
give fail("unsupported scheme '" + scheme + "' (only http and https)")
|
|
41
|
+
let rest be join(slice(sp, 1, length(sp)), "://")
|
|
42
|
+
-- fragmento y query
|
|
43
|
+
let frag_sp be split(rest, "#")
|
|
44
|
+
set rest to frag_sp[0]
|
|
45
|
+
let q_sp be split(rest, "?")
|
|
46
|
+
let query be when length(q_sp) > 1 then join(slice(q_sp, 1, length(q_sp)), "?") otherwise ""
|
|
47
|
+
set rest to q_sp[0]
|
|
48
|
+
-- autoridad y path
|
|
49
|
+
let slash_sp be split(rest, "/")
|
|
50
|
+
let authority be slash_sp[0]
|
|
51
|
+
let path be when length(slash_sp) > 1 then "/" + join(slice(slash_sp, 1, length(slash_sp)), "/") otherwise "/"
|
|
52
|
+
when contains(authority, "@")
|
|
53
|
+
give fail("credentials in the URL are not allowed")
|
|
54
|
+
when authority == ""
|
|
55
|
+
give fail("missing host: " + u)
|
|
56
|
+
let host be lower(authority)
|
|
57
|
+
let port be ""
|
|
58
|
+
when starts_with(host, "[")
|
|
59
|
+
-- IPv6 literal: [::1]:8080
|
|
60
|
+
let close be split(host, "]")
|
|
61
|
+
set host to slice(close[0], 1, length(close[0]))
|
|
62
|
+
when length(close) > 1 and starts_with(close[1], ":")
|
|
63
|
+
set port to slice(close[1], 1, length(close[1]))
|
|
64
|
+
otherwise
|
|
65
|
+
let hp be split(host, ":")
|
|
66
|
+
when length(hp) > 2
|
|
67
|
+
give fail("malformed host: " + authority)
|
|
68
|
+
set host to hp[0]
|
|
69
|
+
when length(hp) == 2
|
|
70
|
+
set port to hp[1]
|
|
71
|
+
when port != "" and not matches(port, "[0-9]{1,5}")
|
|
72
|
+
give fail("malformed port: " + port)
|
|
73
|
+
-- "example.com." (FQDN con punto final) ≡ "example.com"
|
|
74
|
+
while ends_with(host, ".")
|
|
75
|
+
set host to slice(host, 0, length(host) - 1)
|
|
76
|
+
when host == ""
|
|
77
|
+
give fail("missing host: " + u)
|
|
78
|
+
let origin be scheme + "://" + host + (when port != "" then ":" + port otherwise "")
|
|
79
|
+
let dir_parts be split(path, "/")
|
|
80
|
+
let dir be join(slice(dir_parts, 0, length(dir_parts) - 1), "/") + "/"
|
|
81
|
+
give {"ok": true, "url": origin + path + (when query != "" then "?" + query otherwise ""), "scheme": scheme, "host": host, "port": port, "path": path, "query": query, "origin": origin, "dir": dir}
|
|
82
|
+
|
|
83
|
+
-- octetos de una IPv4 en notación decimal con puntos, o nothing
|
|
84
|
+
task ipv4_octets(host)
|
|
85
|
+
when not matches(host, "[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}")
|
|
86
|
+
give nothing
|
|
87
|
+
let out be []
|
|
88
|
+
each p in split(host, ".")
|
|
89
|
+
let n be floor(number(p))
|
|
90
|
+
when n > 255
|
|
91
|
+
give nothing
|
|
92
|
+
set out to append(out, n)
|
|
93
|
+
give out
|
|
94
|
+
|
|
95
|
+
task ipv4_class(o)
|
|
96
|
+
let a be o[0]
|
|
97
|
+
let b be o[1]
|
|
98
|
+
when a == 169 and b == 254
|
|
99
|
+
give "blocked"
|
|
100
|
+
when a == 100 and b == 100 and o[2] == 100 and o[3] == 200
|
|
101
|
+
give "blocked"
|
|
102
|
+
when a == 127 or a == 10 or a == 0
|
|
103
|
+
give "private"
|
|
104
|
+
when a == 192 and b == 168
|
|
105
|
+
give "private"
|
|
106
|
+
when a == 172 and b >= 16 and b <= 31
|
|
107
|
+
give "private"
|
|
108
|
+
when a == 100 and b >= 64 and b <= 127
|
|
109
|
+
give "private"
|
|
110
|
+
when a >= 224
|
|
111
|
+
give "private"
|
|
112
|
+
give "public"
|
|
113
|
+
|
|
114
|
+
export task host_class(host)
|
|
115
|
+
let h be lower(text(host))
|
|
116
|
+
when h == "metadata.google.internal" or h == "metadata.goog" or h == "fd00:ec2::254"
|
|
117
|
+
give "blocked"
|
|
118
|
+
let o be ipv4_octets(h)
|
|
119
|
+
when o != nothing
|
|
120
|
+
give ipv4_class(o)
|
|
121
|
+
-- IPv6: loopback, unspecified, ULA fc00::/7, link-local fe80::/10, IPv4 mapeadas ::ffff:a.b.c.d
|
|
122
|
+
when contains(h, ":")
|
|
123
|
+
when h == "::1" or h == "::"
|
|
124
|
+
give "private"
|
|
125
|
+
when starts_with(h, "::ffff:")
|
|
126
|
+
let inner be ipv4_octets(slice(h, 7, length(h)))
|
|
127
|
+
give when inner != nothing then ipv4_class(inner) otherwise "private"
|
|
128
|
+
when starts_with(h, "fc") or starts_with(h, "fd") or starts_with(h, "fe8") or starts_with(h, "fe9") or starts_with(h, "fea") or starts_with(h, "feb")
|
|
129
|
+
give "private"
|
|
130
|
+
give "public"
|
|
131
|
+
when h == "localhost" or ends_with(h, ".localhost") or ends_with(h, ".internal") or ends_with(h, ".local") or ends_with(h, ".home.arpa") or ends_with(h, ".lan")
|
|
132
|
+
give "private"
|
|
133
|
+
-- formas numéricas ambiguas (2130706433, 0x7f000001, 0177.0.0.1): clásicos de bypass → tratar como privado
|
|
134
|
+
when matches(h, "[0-9]+") or matches(h, "0x[0-9a-f]+") or matches(h, "[0-9x.]+")
|
|
135
|
+
give "private"
|
|
136
|
+
give "public"
|
|
137
|
+
|
|
138
|
+
-- %XX → carácter (para inspeccionar una URL con secretos codificados)
|
|
139
|
+
task percent_decode(s)
|
|
140
|
+
let out be s
|
|
141
|
+
each m in find_all(s, "%[0-9A-Fa-f]{2}")
|
|
142
|
+
let ch be nothing
|
|
143
|
+
try
|
|
144
|
+
set ch to json_decode("\"\\u00" + slice(m, 1, 3) + "\"")
|
|
145
|
+
recover e
|
|
146
|
+
set ch to nothing
|
|
147
|
+
when ch != nothing
|
|
148
|
+
set out to replace_text(out, m, ch)
|
|
149
|
+
give out
|
|
150
|
+
|
|
151
|
+
export task sensitive(raw)
|
|
152
|
+
when raw == nothing
|
|
153
|
+
give nothing
|
|
154
|
+
let u be text(raw)
|
|
155
|
+
let decoded be percent_decode(u)
|
|
156
|
+
when length(find_all(u, SECRET_RE)) > 0 or length(find_all(decoded, SECRET_RE)) > 0
|
|
157
|
+
give "an API key or token"
|
|
158
|
+
let p be parse(u)
|
|
159
|
+
when not p["ok"]
|
|
160
|
+
give nothing
|
|
161
|
+
when p["query"] == ""
|
|
162
|
+
give nothing
|
|
163
|
+
each pair in split(p["query"], "&")
|
|
164
|
+
let kv be split(pair, "=")
|
|
165
|
+
let k be lower(trim(percent_decode(kv[0])))
|
|
166
|
+
let v be when length(kv) > 1 then join(slice(kv, 1, length(kv)), "=") otherwise ""
|
|
167
|
+
when v != "" and contains(SENSITIVE_PARAMS, k)
|
|
168
|
+
give "a credential-like query parameter (" + kv[0] + ")"
|
|
169
|
+
give nothing
|
|
170
|
+
|
|
171
|
+
-- "/a/b/../c/./d" → "/a/c/d"
|
|
172
|
+
task normalize_path(path)
|
|
173
|
+
let out be []
|
|
174
|
+
each seg in split(path, "/")
|
|
175
|
+
when seg == ".."
|
|
176
|
+
when length(out) > 0
|
|
177
|
+
set out to slice(out, 0, length(out) - 1)
|
|
178
|
+
otherwise when seg != "." and seg != ""
|
|
179
|
+
set out to append(out, seg)
|
|
180
|
+
give "/" + join(out, "/") + (when ends_with(path, "/") and length(out) > 0 then "/" otherwise "")
|
|
181
|
+
|
|
182
|
+
export task resolve(base, href)
|
|
183
|
+
when href == nothing
|
|
184
|
+
give nothing
|
|
185
|
+
let h be trim(text(href))
|
|
186
|
+
let lh be lower(h)
|
|
187
|
+
when h == "" or starts_with(h, "#") or starts_with(lh, "javascript:") or starts_with(lh, "data:") or starts_with(lh, "mailto:") or starts_with(lh, "tel:")
|
|
188
|
+
give nothing
|
|
189
|
+
when matches(h, "[A-Za-z][A-Za-z0-9+.-]*:.*")
|
|
190
|
+
give h
|
|
191
|
+
let b be parse(base)
|
|
192
|
+
when not b["ok"]
|
|
193
|
+
give h
|
|
194
|
+
when starts_with(h, "//")
|
|
195
|
+
give b["scheme"] + ":" + h
|
|
196
|
+
-- separar query/fragmento del path relativo para normalizar solo el path
|
|
197
|
+
let tail be ""
|
|
198
|
+
let core be h
|
|
199
|
+
let qi be split(h, "?")
|
|
200
|
+
let fi be split(h, "#")
|
|
201
|
+
when length(qi) > 1
|
|
202
|
+
set core to qi[0]
|
|
203
|
+
set tail to "?" + join(slice(qi, 1, length(qi)), "?")
|
|
204
|
+
otherwise when length(fi) > 1
|
|
205
|
+
set core to fi[0]
|
|
206
|
+
set tail to "#" + join(slice(fi, 1, length(fi)), "#")
|
|
207
|
+
when core == ""
|
|
208
|
+
give b["origin"] + b["path"] + tail
|
|
209
|
+
when starts_with(core, "/")
|
|
210
|
+
give b["origin"] + normalize_path(core) + tail
|
|
211
|
+
give b["origin"] + normalize_path(b["dir"] + core) + tail
|
|
212
|
+
|
|
213
|
+
-- nombre de archivo estable para la caché/spill de una URL: fetch-<host>-<digest>
|
|
214
|
+
export task slug(host)
|
|
215
|
+
let s be replace_re(lower(text(host)), "[^a-z0-9._-]", "-")
|
|
216
|
+
when length(s) > 60
|
|
217
|
+
set s to slice(s, 0, 60)
|
|
218
|
+
when s == ""
|
|
219
|
+
give "page"
|
|
220
|
+
give s
|
package/lib/tools.syn
CHANGED
|
@@ -19,13 +19,14 @@ use "./tools/ls.syn" as t_ls
|
|
|
19
19
|
use "./tools/find.syn" as t_find
|
|
20
20
|
use "./tools/grep.syn" as t_grep
|
|
21
21
|
use "./tools/bash.syn" as t_bash
|
|
22
|
+
use "./tools/fetch.syn" as t_fetch
|
|
22
23
|
use "./tools/skill.syn" as t_skill
|
|
23
24
|
use "./tools/process.syn" as t_process
|
|
24
25
|
use "./tools/memo.syn" as t_memo
|
|
25
26
|
use "./tools/todo.syn" as t_todo
|
|
26
27
|
use "./skills.syn" as skills
|
|
27
28
|
use "./mcp.syn" as mcp
|
|
28
|
-
use "./
|
|
29
|
+
use "./plugins.syn" as plugins
|
|
29
30
|
use "./lsp.syn" as lsp
|
|
30
31
|
use "./schedule.syn" as schedule
|
|
31
32
|
|
|
@@ -86,40 +87,40 @@ let MCP_SPEC be {
|
|
|
86
87
|
}, "required": ["action"]}
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
-- la task de la tool
|
|
90
|
+
-- la task de la tool plugin (plugins.syn vive un nivel arriba de tools/, como mcp). enable/disable piden humano
|
|
90
91
|
-- SIEMPRE (permission.syn): encender = autorizar código a correr con las capacidades de su manifiesto.
|
|
91
|
-
task
|
|
92
|
+
task plugin_tool(action, name, manifest, files)
|
|
92
93
|
require exec
|
|
93
94
|
require time
|
|
94
95
|
require env("LAMPSON_*")
|
|
95
96
|
require file(".lampson")
|
|
96
97
|
require file(".lampson/*")
|
|
97
|
-
require file.read("
|
|
98
|
-
require file.read("
|
|
98
|
+
require file.read("plugins")
|
|
99
|
+
require file.read("plugins/*")
|
|
99
100
|
require file("workspace")
|
|
100
101
|
require file("workspace/*")
|
|
101
102
|
when action == "create"
|
|
102
|
-
give
|
|
103
|
+
give plugins.create(name, manifest, files)
|
|
103
104
|
when action == "enable"
|
|
104
|
-
give
|
|
105
|
+
give plugins.set_enabled(name, true)
|
|
105
106
|
when action == "disable"
|
|
106
|
-
give
|
|
107
|
-
let sm be
|
|
107
|
+
give plugins.set_enabled(name, false)
|
|
108
|
+
let sm be plugins.summary()
|
|
108
109
|
when length(sm) == 0
|
|
109
|
-
give "no
|
|
110
|
+
give "no plugins found. A plugin is a folder with a plugin.json manifest: " + plugins.GLOBAL_DIR + "/<name>/ (global) or " + plugins.PROJECT_DIR + "/<name>/ (this project; you can create it with write). See the lampson skill for the manifest format."
|
|
110
111
|
let lines be []
|
|
111
112
|
each l in sm
|
|
112
|
-
set lines to append(lines, l["name"] + " (" + l["scope"] + ", " + l["kind"] + ", " + (when l["enabled"] then "ON" otherwise "off") + "): " + (when l["error"] != nothing then "BROKEN — " + text(l["error"]) otherwise text(length(l["tools"])) + " tools [" + join(l["tools"], ", ") + "]" + (when l["description"] != "" then " — " + l["description"] otherwise "")))
|
|
113
|
+
set lines to append(lines, l["name"] + " (" + l["scope"] + ", " + l["kind"] + ", " + (when l["enabled"] then "ON" otherwise "off") + "): " + (when l["error"] != nothing then "BROKEN — " + text(l["error"]) otherwise text(length(l["tools"])) + " tools [" + join(l["tools"], ", ") + "]" + (when l["description"] != "" then " — " + l["description"] otherwise "")) + (when l["legacy"] then " [" + plugins.LEGACY_HINT + "]" otherwise ""))
|
|
113
114
|
give join(lines, "\n")
|
|
114
115
|
|
|
115
|
-
let
|
|
116
|
-
"name": "
|
|
117
|
-
"description": "
|
|
116
|
+
let PLUGIN_SPEC be {
|
|
117
|
+
"name": "plugin",
|
|
118
|
+
"description": "Plugins are tools you can build for this project (they were called 'lamps' until 0.2.6; the user may still say lamp/lámpara): a folder with a plugin.json manifest plus code — a Synsema program (kind=syn) run under a capability ceiling, or any executable (kind=exec, any language, no ceiling). Consider one when a task needs a reusable custom tool (a project-specific query, generator, checker) that plain bash would repeat clumsily. action=list (default): every plugin found, on/off, its tools. action=create: write a PROJECT plugin (name + manifest + files) — it validates the manifest and runs `synsema check` on a syn entry, but does NOT run or enable it. action=enable / disable: turn one on or off — ALWAYS asks the user (enabling authorizes its code to run with the manifest's capabilities). An enabled plugin's tools appear in your catalog as plugin_<plugin>_<tool> from the NEXT turn. Manifest: {\"description\", \"kind\": \"syn\"|\"exec\", \"entry\": \"plugin.syn\" (syn) | \"command\": \"python plugin.py\" (exec), \"caps\": \"file.read=workspace/*\" (syn, optional extra ceiling over stdout,time,env=PLUGIN_*), \"timeout\": 60, \"tools\": [{\"name\", \"description\", \"parameters\": JSON Schema, \"readonly\": bool}]}. Inside the code read PLUGIN_TOOL and PLUGIN_ARGS (JSON) from env and print the result to stdout (a .syn needs `require env(\"PLUGIN_*\")`). Plugin names: letters, digits, - (no _). Not the same as a lamp from lamps.sh (a portable, ceiling-enforced capability unit for any MCP agent): those are installed with `lamp add` and reach you as MCP tools via `lamp mcp`.",
|
|
118
119
|
"parameters": {"type": "object", "properties": {
|
|
119
120
|
"action": {"type": "string", "enum": ["list", "create", "enable", "disable"], "description": "Default: list"},
|
|
120
|
-
"name": {"type": "string", "description": "create/enable/disable:
|
|
121
|
-
"manifest": {"type": "object", "description": "create only: the
|
|
122
|
-
"files": {"type": "object", "description": "create only: {\"
|
|
121
|
+
"name": {"type": "string", "description": "create/enable/disable: plugin name"},
|
|
122
|
+
"manifest": {"type": "object", "description": "create only: the plugin.json content (name is filled in)"},
|
|
123
|
+
"files": {"type": "object", "description": "create only: {\"plugin.syn\": \"<code>\", …} — files written into the plugin folder", "additionalProperties": {"type": "string"}}
|
|
123
124
|
}, "required": ["action"]}
|
|
124
125
|
}
|
|
125
126
|
|
|
@@ -158,8 +159,8 @@ let LSP_SPEC be {
|
|
|
158
159
|
|
|
159
160
|
-- la tool schedule (lib/schedule.syn): tareas programadas. add/remove/enable/run piden humano SIEMPRE (permission.syn):
|
|
160
161
|
-- crear una tarea = autorizar de una vez todo lo que va a hacer sin nadie mirando. Las corridas las hace el daemon
|
|
161
|
-
-- (web.syn); desde acá solo se corren en el acto las
|
|
162
|
-
task schedule_tool(action, id, name, at, kind,
|
|
162
|
+
-- (web.syn); desde acá solo se corren en el acto las plugin/bash (una prompt anidaría un loop dentro del turno).
|
|
163
|
+
task schedule_tool(action, id, name, at, kind, plugin, tool, args, command, prompt, agent, permission, approval_timeout, notify)
|
|
163
164
|
require exec
|
|
164
165
|
require time
|
|
165
166
|
require net
|
|
@@ -167,15 +168,15 @@ task schedule_tool(action, id, name, at, kind, lamp, tool, args, command, prompt
|
|
|
167
168
|
require env("OS")
|
|
168
169
|
require file(".lampson")
|
|
169
170
|
require file(".lampson/*")
|
|
170
|
-
require file.read("
|
|
171
|
-
require file.read("
|
|
171
|
+
require file.read("plugins")
|
|
172
|
+
require file.read("plugins/*")
|
|
172
173
|
require file("workspace")
|
|
173
174
|
require file("workspace/*")
|
|
174
175
|
let act be when action == nothing then "list" otherwise action
|
|
175
176
|
when act == "add"
|
|
176
177
|
let a be {"type": kind}
|
|
177
|
-
when kind == "
|
|
178
|
-
set a to {"type": "
|
|
178
|
+
when kind == "plugin"
|
|
179
|
+
set a to {"type": "plugin", "plugin": plugin, "tool": tool, "args": when args == nothing then {} otherwise args}
|
|
179
180
|
otherwise when kind == "bash"
|
|
180
181
|
set a to {"type": "bash", "command": command}
|
|
181
182
|
otherwise when kind == "prompt"
|
|
@@ -227,16 +228,16 @@ task daemon_note()
|
|
|
227
228
|
|
|
228
229
|
let SCHEDULE_SPEC be {
|
|
229
230
|
"name": "schedule",
|
|
230
|
-
"description": "Scheduled tasks: run something on a schedule with nobody watching — a
|
|
231
|
+
"description": "Scheduled tasks: run something on a schedule with nobody watching — a plugin tool, a fixed shell command, or a full agent run from a prompt (kind=prompt: the agent works unattended with the chosen profile and writes a report; its session appears as ⏰ <name>). `at` formats — recurring: 'every 6h' | 'every 30m' | 'daily 09:00' | 'mon,wed 08:30' | 'weekdays 09:00'; ONE-TIME (runs once, then turns itself off): 'today 15:14' | 'tomorrow 09:00' | 'once 2026-08-29 15:14' | 'in 2h'. Times are the USER'S LOCAL time (the machine's timezone): write the hour exactly as the user says it — NEVER convert to UTC. Tasks belong to the current workspace. permission = what a prompt run may do without asking: strict (dangerous actions denied), ask (default: a dangerous action sends the user an approval link/notification and waits up to approval_timeout seconds, denied if unanswered), yolo (allowed). notify = optional webhook URL that receives the result as JSON (for 'search and send me' tasks). action=add ALWAYS asks the user (it authorizes future unattended runs); remove/enable/disable/run also ask; list and log do not. The tasks are executed by Lampson's resident process (`lampson --daemon start` or the open web UI) — say so if the list shows no scheduler running. Use it when the user says 'every day at', 'each N hours', 'on Mondays', 'periodically', 'remind me', 'send me'.",
|
|
231
232
|
"parameters": {"type": "object", "properties": {
|
|
232
233
|
"action": {"type": "string", "enum": ["list", "add", "remove", "enable", "disable", "run", "log"], "description": "Default: list"},
|
|
233
234
|
"id": {"type": "string", "description": "remove/enable/disable/run/log: the task id (from list)"},
|
|
234
235
|
"name": {"type": "string", "description": "add: short human name"},
|
|
235
236
|
"at": {"type": "string", "description": "add: the schedule — 'every 6h' | 'daily 09:00' | 'mon,wed 08:30' | 'weekdays 09:00'"},
|
|
236
|
-
"kind": {"type": "string", "enum": ["
|
|
237
|
-
"
|
|
238
|
-
"tool": {"type": "string", "description": "add kind=
|
|
239
|
-
"args": {"type": "object", "description": "add kind=
|
|
237
|
+
"kind": {"type": "string", "enum": ["plugin", "bash", "prompt"], "description": "add: what runs"},
|
|
238
|
+
"plugin": {"type": "string", "description": "add kind=plugin: plugin name (must be ON)"},
|
|
239
|
+
"tool": {"type": "string", "description": "add kind=plugin: tool of that plugin"},
|
|
240
|
+
"args": {"type": "object", "description": "add kind=plugin: arguments for the tool"},
|
|
240
241
|
"command": {"type": "string", "description": "add kind=bash: the shell command (must finish on its own; no servers)"},
|
|
241
242
|
"prompt": {"type": "string", "description": "add kind=prompt: self-contained instructions for the unattended agent run (what to do, how to verify, what to report)"},
|
|
242
243
|
"agent": {"type": "string", "enum": ["build", "plan", "review", "explore"], "description": "add kind=prompt: profile (default build)"},
|
|
@@ -256,17 +257,18 @@ export task registry()
|
|
|
256
257
|
"find": t_find.tool,
|
|
257
258
|
"grep": t_grep.tool,
|
|
258
259
|
"bash": t_bash.tool,
|
|
260
|
+
"fetch": t_fetch.tool,
|
|
259
261
|
"process": t_process.tool,
|
|
260
262
|
"memory": t_memo.tool,
|
|
261
263
|
"todo": t_todo.tool,
|
|
262
264
|
"skill": skill_tool,
|
|
263
265
|
"mcp": mcp_tool,
|
|
264
|
-
"
|
|
266
|
+
"plugin": plugin_tool,
|
|
265
267
|
"lsp": lsp_tool,
|
|
266
268
|
"schedule": schedule_tool
|
|
267
269
|
}
|
|
268
270
|
|
|
269
|
-
export let CATALOG be [t_read.SPEC, t_write.SPEC, t_edit.SPEC, t_ls.SPEC, t_find.SPEC, t_grep.SPEC, LSP_SPEC, t_bash.SPEC, t_process.SPEC, t_memo.SPEC, t_todo.SPEC, t_skill.SPEC, MCP_SPEC,
|
|
271
|
+
export let CATALOG be [t_read.SPEC, t_write.SPEC, t_edit.SPEC, t_ls.SPEC, t_find.SPEC, t_grep.SPEC, LSP_SPEC, t_fetch.SPEC, t_bash.SPEC, t_process.SPEC, t_memo.SPEC, t_todo.SPEC, t_skill.SPEC, MCP_SPEC, PLUGIN_SPEC, SCHEDULE_SPEC]
|
|
270
272
|
|
|
271
273
|
-- Subconjuntos (para perfiles de agente): registry/catálogo filtrados por nombre.
|
|
272
274
|
export task registry_subset(names)
|
package/lib/workspaces.syn
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
-- Por qué así (ver ../SPEC-WORKSPACES.md): en Synsema las capabilities de archivo Y los `use "./lib/…"` se resuelven
|
|
4
4
|
-- relativos al CWD del proceso, y `proxy to` es estático (se evalúa al arrancar y anexa el path entero). Entonces:
|
|
5
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
|
|
6
|
+
-- workspace → junction al proyecto lib public skills plugins memory → junctions a la instalación
|
|
7
7
|
-- web.syn chat.syn → copias (refrescadas al arrancar) .lampson/ → estado propio (sesiones, tareas…)
|
|
8
8
|
-- .lampson/global → junction a <home>/.lampson (config.json con keys, mcp/lsp globales, skills-*)
|
|
9
9
|
-- con eso `file("workspace/*")`, `use "./lib/x.syn"` y todo el código de siempre funcionan SIN cambios.
|
|
@@ -26,7 +26,7 @@ export let HUB_FILE be "hub.syn"
|
|
|
26
26
|
export let HUB_TEMPLATE be "hub.tpl.syn"
|
|
27
27
|
-- rango alto para no chocar con lo que usan las apps (3000, 5173, 8000, 8080-8090…): solo el hub queda en 8080
|
|
28
28
|
let FIRST_PORT be 47101
|
|
29
|
-
let LINKS be ["lib", "public", "skills", "
|
|
29
|
+
let LINKS be ["lib", "public", "skills", "plugins", "memory"]
|
|
30
30
|
let COPIES be ["web.syn", "chat.syn"]
|
|
31
31
|
|
|
32
32
|
task is_win()
|
|
@@ -182,6 +182,31 @@ task unlink(where_)
|
|
|
182
182
|
task is_link_dir(p)
|
|
183
183
|
give file_exists(p)
|
|
184
184
|
|
|
185
|
+
-- crea una carpeta de la instalación (ruta absoluta: fuera del alcance de write_file, así que por exec)
|
|
186
|
+
task ensure_dir(p)
|
|
187
|
+
when is_win()
|
|
188
|
+
-- argumentos separados, como mklink: una línea entera con comillas dentro de `cmd /c` se rompe al citarla.
|
|
189
|
+
-- mkdir de cmd crea los intermedios y falla (sin daño) si ya existe.
|
|
190
|
+
run("cmd", ["/c", "mkdir", replace_text(p, "/", "\\")], 10)
|
|
191
|
+
otherwise
|
|
192
|
+
run("mkdir", ["-p", p], 10)
|
|
193
|
+
give true
|
|
194
|
+
|
|
195
|
+
-- un link a la instalación (lib, public, skills, plugins, memory): se crea si falta y se REHACE si quedó colgante.
|
|
196
|
+
-- file_exists da false tanto si no hay nada como si el link apunta a un destino que no existe (npm no trae
|
|
197
|
+
-- memory/ ni plugins/; un `ln -s` a una carpeta inexistente nace roto y después memory(write) muere con
|
|
198
|
+
-- "No such file or directory" — visto en un VPS el 2026-09-02). Por eso: destino primero, link después, y si
|
|
199
|
+
-- sigue sin resolverse es un error del workspace, no un silencio.
|
|
200
|
+
export task ensure_link(where_, target)
|
|
201
|
+
when file_exists(where_)
|
|
202
|
+
give true
|
|
203
|
+
unlink(where_)
|
|
204
|
+
ensure_dir(target)
|
|
205
|
+
link(where_, target)
|
|
206
|
+
when not file_exists(where_)
|
|
207
|
+
raise("could not link " + where_ + " → " + target + " (the workspace needs it: memory, plugins and the code live there)")
|
|
208
|
+
give true
|
|
209
|
+
|
|
185
210
|
-- crea/repara .lampson/ws/<slug>: junctions al proyecto y a la instalación, copias de los entries
|
|
186
211
|
export task prepare(w)
|
|
187
212
|
require exec
|
|
@@ -209,16 +234,14 @@ export task prepare(w)
|
|
|
209
234
|
when not link(ws_link, w["path"])
|
|
210
235
|
raise("could not link " + ws_link + " → " + w["path"])
|
|
211
236
|
each n in LINKS
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
when not file_exists(d + "/.lampson/global")
|
|
215
|
-
link(d + "/.lampson/global", h + sep + ".lampson")
|
|
237
|
+
ensure_link(d + "/" + n, h + sep + n)
|
|
238
|
+
ensure_link(d + "/.lampson/global", h + sep + ".lampson")
|
|
216
239
|
each f in COPIES
|
|
217
240
|
write_file(d + "/" + f, read_file(f))
|
|
218
241
|
give d
|
|
219
242
|
|
|
220
243
|
-- estado guardado ANTES de los workspaces (en <home>/.lampson, separado por slug en cada registro): sesiones y trazas
|
|
221
|
-
-- del proyecto, sus tareas programadas y el encendido de
|
|
244
|
+
-- del proyecto, sus tareas programadas y el encendido de plugins se mueven a <ws>/.lampson la primera vez
|
|
222
245
|
task migrate(w, d)
|
|
223
246
|
let slug be w["slug"]
|
|
224
247
|
let moved be 0
|
|
@@ -248,8 +271,10 @@ task migrate(w, d)
|
|
|
248
271
|
recover err
|
|
249
272
|
set moved to moved
|
|
250
273
|
try
|
|
251
|
-
when file_exists(".lampson/
|
|
252
|
-
write_file(d + "/.lampson/
|
|
274
|
+
when file_exists(".lampson/plugins.json")
|
|
275
|
+
write_file(d + "/.lampson/plugins.json", read_file(".lampson/plugins.json"))
|
|
276
|
+
otherwise when file_exists(".lampson/lamps.json")
|
|
277
|
+
write_file(d + "/.lampson/plugins.json", read_file(".lampson/lamps.json"))
|
|
253
278
|
recover err
|
|
254
279
|
set moved to moved
|
|
255
280
|
give moved
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lampson",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "A coding agent written in Synsema: terminal + web, tools confined to the mounted project,
|
|
3
|
+
"version": "0.2.8",
|
|
4
|
+
"description": "A coding agent written in Synsema: terminal + web, tools confined to the mounted project, plugins (your own tools, any language), LSP, MCP, sub-agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -9,7 +9,15 @@
|
|
|
9
9
|
},
|
|
10
10
|
"homepage": "https://github.com/kitecosmic/lampson#readme",
|
|
11
11
|
"bugs": "https://github.com/kitecosmic/lampson/issues",
|
|
12
|
-
"keywords": [
|
|
12
|
+
"keywords": [
|
|
13
|
+
"agent",
|
|
14
|
+
"coding-agent",
|
|
15
|
+
"synsema",
|
|
16
|
+
"llm",
|
|
17
|
+
"cli",
|
|
18
|
+
"lsp",
|
|
19
|
+
"mcp"
|
|
20
|
+
],
|
|
13
21
|
"bin": {
|
|
14
22
|
"lampson": "bin/lampson.js"
|
|
15
23
|
},
|
|
@@ -18,7 +26,7 @@
|
|
|
18
26
|
"lib/",
|
|
19
27
|
"public/",
|
|
20
28
|
"skills/",
|
|
21
|
-
"
|
|
29
|
+
"plugins/",
|
|
22
30
|
"chat.syn",
|
|
23
31
|
"web.syn",
|
|
24
32
|
"hub.tpl.syn",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "example-hello",
|
|
3
|
-
"description": "Example
|
|
3
|
+
"description": "Example plugin: a Synsema program run under a capability ceiling. Copy this folder to make your own.",
|
|
4
4
|
"kind": "syn",
|
|
5
|
-
"entry": "
|
|
5
|
+
"entry": "plugin.syn",
|
|
6
6
|
"caps": "",
|
|
7
7
|
"timeout": 20,
|
|
8
8
|
"tools": [
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- plugins/example-hello/plugin.syn — el plugin de ejemplo
|
|
2
|
+
--
|
|
3
|
+
-- Lampson lo corre así por cada llamada: synsema run --cap-set stdout,time,env=PLUGIN_*,env=LAMP_* plugin.syn
|
|
4
|
+
-- El techo (--cap-set) sale del manifiesto (plugin.json → "caps") que el humano aprobó al encenderlo;
|
|
5
|
+
-- pedir más acá (p. ej. `require net`) falla con "above the host ceiling".
|
|
6
|
+
-- Entrada por env: PLUGIN_TOOL (qué tool), PLUGIN_ARGS (sus args en JSON), PLUGIN_DIR, PLUGIN_WORKSPACE.
|
|
7
|
+
-- Salida: lo que imprimas por stdout vuelve al modelo como resultado de la tool.
|
|
8
|
+
intent: "example plugin for lampson: greet"
|
|
9
|
+
|
|
10
|
+
require env("PLUGIN_*")
|
|
11
|
+
|
|
12
|
+
let tool be env("PLUGIN_TOOL", "")
|
|
13
|
+
let args be json_decode(env("PLUGIN_ARGS", "{}"))
|
|
14
|
+
|
|
15
|
+
when tool == "greet"
|
|
16
|
+
let who be when contains(args, "who") then text(args["who"]) otherwise "world"
|
|
17
|
+
print("hello, " + who + "! (from the example-hello plugin, running under a capability ceiling)")
|
|
18
|
+
otherwise
|
|
19
|
+
print("ERROR: unknown tool '" + tool + "'")
|
package/public/css/panel.css
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* panel: EL componente modal de la app (js/panel.js). Un cascarón (overlay, cabecera, ✕/Esc/clic afuera) y
|
|
2
2
|
tres layouts: browse (buscador + lista + detalle), tabs (pestañas con formularios) y form (un formulario).
|
|
3
|
-
Cada vista (sesiones,
|
|
3
|
+
Cada vista (sesiones, plugins, configuración, programar…) solo aporta su contenido; nada de esto se repite. */
|
|
4
4
|
.modal { position:fixed; inset:0; background:rgba(0,0,0,.45); display:flex; align-items:center; justify-content:center; z-index:50; }
|
|
5
5
|
.panel { background:var(--paper); border:1px solid var(--rule-2); border-radius:var(--r); box-shadow:0 10px 30px rgba(0,0,0,.35); display:flex; flex-direction:column; min-height:0; max-height:92vh; }
|
|
6
6
|
.panel.lg { width:min(1100px, 94vw); height:min(760px, 90vh); padding:14px 18px 12px; }
|
|
@@ -94,11 +94,11 @@
|
|
|
94
94
|
.panel .dfoot .del.ask .yes { color:var(--rubric); cursor:pointer; font-weight:600; }
|
|
95
95
|
.panel .dfoot .del.ask .no { cursor:pointer; }
|
|
96
96
|
.panel .none { color:var(--ink-3); font:400 13px/1.6 var(--serif); max-width:520px; }
|
|
97
|
-
/* switch encendido/apagado (
|
|
97
|
+
/* switch encendido/apagado (plugins) */
|
|
98
98
|
.panel label.sw { display:inline-flex; align-items:center; margin:0 0 0 auto; gap:6px; cursor:pointer; user-select:none; font:400 11.5px/1 var(--mono); text-transform:none; letter-spacing:0; color:var(--ink-3); }
|
|
99
99
|
.panel label.sw input { flex:none; width:16px; height:16px; padding:0; margin:0; accent-color:var(--accent); }
|
|
100
100
|
.panel label.sw.on { color:var(--accent); }
|
|
101
|
-
/* formulario generado desde un JSON Schema (tools de
|
|
101
|
+
/* formulario generado desde un JSON Schema (tools de un plugin) */
|
|
102
102
|
.panel .tool { border:1px solid var(--rule); border-radius:var(--r); padding:10px 12px; margin-bottom:10px; }
|
|
103
103
|
.panel .tool .th { display:grid; grid-template-columns:auto 1fr auto; gap:8px 10px; align-items:baseline; }
|
|
104
104
|
.panel .tool .th code { font-weight:600; color:var(--ink); }
|
package/public/css/sidebar.css
CHANGED
|
@@ -82,3 +82,24 @@
|
|
|
82
82
|
/* puertos ocupados por lampson: visibles pero sin link ni ✕ */
|
|
83
83
|
.p .port.own { color:var(--ink-3); font-weight:400; }
|
|
84
84
|
.p.own .cm { color:var(--ink-3); }
|
|
85
|
+
|
|
86
|
+
/* explorador de archivos: selección, portapapeles, arrastre, renombrar en línea y menú contextual */
|
|
87
|
+
#tree:focus { outline:none; }
|
|
88
|
+
.row.sel { background:var(--paper-2); }
|
|
89
|
+
.row.cut { opacity:.55; }
|
|
90
|
+
.row.dragging { opacity:.4; }
|
|
91
|
+
.row.drop, #tree.drop { background:var(--accent-bg); border-left-color:var(--accent); }
|
|
92
|
+
.row.flash { background:var(--accent-bg); transition:background .7s; }
|
|
93
|
+
.row input.rn { flex:1; min-width:0; font:inherit; color:var(--ink); background:var(--paper-2); border:1px solid var(--accent); border-radius:var(--r); padding:0 4px; outline:none; }
|
|
94
|
+
.row.new { color:var(--ink); }
|
|
95
|
+
.ctx { position:fixed; z-index:70; min-width:220px; background:var(--paper); border:1px solid var(--rule-2); border-radius:var(--r); box-shadow:0 10px 30px rgba(0,0,0,.35); padding:4px 0; font:400 12.5px/1.5 var(--mono); user-select:none; }
|
|
96
|
+
.ctx .it { display:flex; align-items:center; gap:14px; padding:4px 12px; cursor:pointer; color:var(--ink-2); white-space:nowrap; }
|
|
97
|
+
.ctx .it:hover { background:var(--paper-2); color:var(--ink); }
|
|
98
|
+
.ctx .it .k { margin-left:auto; color:var(--ink-3); font-size:10.5px; }
|
|
99
|
+
.ctx .it.dis { opacity:.4; pointer-events:none; }
|
|
100
|
+
.ctx .it.danger:hover { color:var(--rubric); }
|
|
101
|
+
.ctx .it.ask { color:var(--ink-2); gap:6px; }
|
|
102
|
+
.ctx .it.ask .yes { color:var(--rubric); font-weight:600; cursor:pointer; }
|
|
103
|
+
.ctx .it.ask .no { cursor:pointer; }
|
|
104
|
+
.ctx .it.ask .yes:hover, .ctx .it.ask .no:hover { text-decoration:underline; text-underline-offset:.18em; }
|
|
105
|
+
.ctx .sep { height:1px; background:var(--rule); margin:4px 0; }
|
package/public/hub.html
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
<div class="empty">
|
|
26
26
|
<p class="eyebrow">workspaces</p>
|
|
27
27
|
<h1>¿En qué proyecto trabajamos?</h1>
|
|
28
|
-
<p class="lead">Cada workspace es una carpeta de tu disco con su propio agente: sesiones, tareas programadas,
|
|
28
|
+
<p class="lead">Cada workspace es una carpeta de tu disco con su propio agente: sesiones, tareas programadas, plugins, MCP. Corren en procesos separados, así podés tener varios abiertos a la vez. Los que tienen tareas encendidas siguen vivos aunque cierres todo.</p>
|
|
29
29
|
<div class="wsgrid" id="wsGrid"></div>
|
|
30
30
|
<p class="lead" style="margin-top:22px">Desde una terminal: <code>cd mi-proyecto && lampson</code> (terminal) o <code>lampson --web</code> (esta web, en ese workspace).</p>
|
|
31
31
|
</div>
|
package/public/index.html
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
<select id="agent" title="perfil de agente: qué tools puede usar"></select>
|
|
34
34
|
<select id="perm" title="qué pasa con un comando peligroso (rm -rf, git push --force, sudo…)"><option value="ask">permisos: preguntar</option><option value="yolo">permisos: permitir todo</option><option value="strict">permisos: denegar</option></select>
|
|
35
35
|
<span class="pill on" id="wsPill" style="display:none" title="cambiar de workspace">workspace ▾</span>
|
|
36
|
-
<span class="pill" id="
|
|
36
|
+
<span class="pill" id="plugins" title="plugins: tools propias que vos encendés · clic para ver y activar">plugins</span>
|
|
37
37
|
<span class="pill" id="model" title="proveedor y modelo · clic para cambiarlos o cargar una API key">…</span>
|
|
38
38
|
</div>
|
|
39
39
|
</header>
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
<div class="body" id="apprBox"></div>
|
|
78
78
|
</section>
|
|
79
79
|
<section class="sec" data-sec="sched">
|
|
80
|
-
<h2 title="tareas programadas (cada 6 h, todos los días a las 9, lunes 8:30…):
|
|
80
|
+
<h2 title="tareas programadas (cada 6 h, todos los días a las 9, lunes 8:30…): un plugin, un comando o una corrida del agente. Corren mientras lampson esté abierto, o con lampson --daemon start"><span class="caret">▸</span>Programadas<span class="cnt" id="schedCount"></span><button class="h2act" id="schedAddBtn" title="programar una tarea">+</button></h2>
|
|
81
81
|
<div class="body" id="schedBox"></div>
|
|
82
82
|
</section>
|
|
83
83
|
</aside>
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
<script src="/js/todo.js"></script>
|
|
120
120
|
<script src="/js/mcp.js"></script>
|
|
121
121
|
<script src="/js/lsp.js"></script>
|
|
122
|
-
<script src="/js/
|
|
122
|
+
<script src="/js/plugins.js"></script>
|
|
123
123
|
<script src="/js/schedules.js"></script>
|
|
124
124
|
<script src="/js/approvals.js"></script>
|
|
125
125
|
<script src="/js/config.js"></script>
|
package/public/js/app.js
CHANGED
|
@@ -14,7 +14,7 @@ async function loadCfg() {
|
|
|
14
14
|
sel.onchange = () => { localStorage.setItem('lampson.agent', sel.value); hint(); }; hint();
|
|
15
15
|
if (!log.children.length) empty();
|
|
16
16
|
}
|
|
17
|
-
loadCfg().then(() => { loadTree(); loadSessions(); loadProcs(); loadMemory(); loadAgents(); loadMcp(); loadLsp();
|
|
17
|
+
loadCfg().then(() => { loadTree(); loadSessions(); loadProcs(); loadMemory(); loadAgents(); loadMcp(); loadLsp(); loadPlugins(); loadTodo(); loadSched(); loadApprovals(); checkUpdate(); const pq = new URLSearchParams(location.search).get('proc'); if (pq) openProc(pq); else if (session) openSession(session); });
|
|
18
18
|
// respaldos lentos por si el stream de eventos se cae
|
|
19
19
|
setInterval(loadProcs, events ? 60000 : 10000);
|
|
20
20
|
setInterval(() => { loadSched(); if (!events) loadApprovals(); }, events ? 60000 : 15000);
|