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/lib/session.syn
CHANGED
|
@@ -72,7 +72,8 @@ export task list()
|
|
|
72
72
|
try
|
|
73
73
|
let d be load(id)
|
|
74
74
|
let title be when contains(d["meta"], "title") then d["meta"]["title"] otherwise ""
|
|
75
|
-
|
|
75
|
+
-- sin marca de proyecto (anteriores al 2026-08-27) → no se listan en ningún workspace (aparecían en todos)
|
|
76
|
+
let project be when contains(d["meta"], "project") then d["meta"]["project"] otherwise ""
|
|
76
77
|
when project == mine
|
|
77
78
|
set out to append(out, {"id": id, "updated": d["updated"], "title": title})
|
|
78
79
|
recover err
|
|
@@ -81,6 +82,42 @@ export task list()
|
|
|
81
82
|
give []
|
|
82
83
|
give sort_by(out, (s) => 0 - number(replace_text(replace_text(s["id"], "-", ""), "test", "")))
|
|
83
84
|
|
|
85
|
+
-- Búsqueda de texto en las sesiones de ESTE proyecto (título y contenido de los mensajes user/assistant):
|
|
86
|
+
-- [{id, updated, title, snippet}] — snippet = el primer trozo donde aparece `q`. Vacío = todas (sin snippet).
|
|
87
|
+
-- Lineal sobre los archivos (local, decenas o cientos de sesiones): suficiente; migrar a db si crece.
|
|
88
|
+
export task search(q, limit)
|
|
89
|
+
require file.read(".lampson")
|
|
90
|
+
require file.read(".lampson/*")
|
|
91
|
+
require env("LAMPSON_*")
|
|
92
|
+
let needle be lower(trim(text(q)))
|
|
93
|
+
let out be []
|
|
94
|
+
each s in list()
|
|
95
|
+
when length(out) >= limit
|
|
96
|
+
give out
|
|
97
|
+
when needle == ""
|
|
98
|
+
set out to append(out, {"id": s["id"], "updated": s["updated"], "title": s["title"], "snippet": ""})
|
|
99
|
+
otherwise
|
|
100
|
+
let hit be nothing
|
|
101
|
+
when contains(lower(s["title"]), needle)
|
|
102
|
+
set hit to ""
|
|
103
|
+
otherwise
|
|
104
|
+
try
|
|
105
|
+
let d be load(s["id"])
|
|
106
|
+
each m in d["messages"]
|
|
107
|
+
when hit == nothing and (m["role"] == "user" or m["role"] == "assistant") and text(m["content"]) == m["content"]
|
|
108
|
+
let c be text(m["content"])
|
|
109
|
+
let lc be lower(c)
|
|
110
|
+
when contains(lc, needle)
|
|
111
|
+
let pos be length(split(lc, needle)[0])
|
|
112
|
+
let a be when pos > 60 then pos - 60 otherwise 0
|
|
113
|
+
let b be when pos + length(needle) + 90 < length(c) then pos + length(needle) + 90 otherwise length(c)
|
|
114
|
+
set hit to (when a > 0 then "…" otherwise "") + replace_text(replace_text(slice(c, a, b), "\r", ""), "\n", " ") + (when b < length(c) then "…" otherwise "")
|
|
115
|
+
recover err
|
|
116
|
+
set hit to nothing
|
|
117
|
+
when hit != nothing
|
|
118
|
+
set out to append(out, {"id": s["id"], "updated": s["updated"], "title": s["title"], "snippet": hit})
|
|
119
|
+
give out
|
|
120
|
+
|
|
84
121
|
-- Borrar una sesión (no hay delete_file en el runtime: se usa el shell del sistema sobre la ruta fija).
|
|
85
122
|
export task delete(id)
|
|
86
123
|
require exec
|
package/lib/settings.syn
CHANGED
|
@@ -7,7 +7,15 @@
|
|
|
7
7
|
--
|
|
8
8
|
-- Forma: {"provider": "deepseek", "model": "deepseek-chat", "keys": {"deepseek": "sk-…"}}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
-- bajo el hub cada workspace corre con su propio .lampson/ y una junction .lampson/global → <home>/.lampson:
|
|
11
|
+
-- la config (keys, ⚙) es una sola para todos. Sin junction (instalación directa, tests) es .lampson/config.json
|
|
12
|
+
task cfg_path()
|
|
13
|
+
try
|
|
14
|
+
when file_exists(".lampson/global/config.json") or file_exists(".lampson/global")
|
|
15
|
+
give ".lampson/global/config.json"
|
|
16
|
+
recover err
|
|
17
|
+
give ".lampson/config.json"
|
|
18
|
+
give ".lampson/config.json"
|
|
11
19
|
|
|
12
20
|
export task load()
|
|
13
21
|
require file.read(".lampson")
|
|
@@ -18,7 +26,7 @@ export task load()
|
|
|
18
26
|
when env("LAMPSON_NO_CONFIG", "") == "1"
|
|
19
27
|
give {"provider": "", "model": "", "keys": {}}
|
|
20
28
|
try
|
|
21
|
-
let doc be json_decode(read_file(
|
|
29
|
+
let doc be json_decode(read_file(cfg_path()))
|
|
22
30
|
-- claves siempre presentes: quien lee no tiene que chequear (y `and` no cortocircuita)
|
|
23
31
|
when not contains(doc, "keys")
|
|
24
32
|
set doc["keys"] to {}
|
|
@@ -33,7 +41,7 @@ export task load()
|
|
|
33
41
|
export task save(doc)
|
|
34
42
|
require file(".lampson")
|
|
35
43
|
require file(".lampson/*")
|
|
36
|
-
write_file(
|
|
44
|
+
write_file(cfg_path(), json_encode(doc))
|
|
37
45
|
give true
|
|
38
46
|
|
|
39
47
|
-- key guardada para un proveedor, como TEXTO ("" si no hay). Sellarla con as_secret antes de usarla.
|
|
@@ -68,3 +76,58 @@ export task set_default(name, model)
|
|
|
68
76
|
set doc["model"] to model
|
|
69
77
|
save(doc)
|
|
70
78
|
give true
|
|
79
|
+
|
|
80
|
+
-- valores de configuración generales (rueda de la UI): tz, public_url, webhook_url, webhook_secret…
|
|
81
|
+
-- Vacío = borrar la clave. Nunca se devuelven secretos al navegador: `values()` marca webhook_secret como has_*.
|
|
82
|
+
export let KEYS be ["tz", "public_url", "webhook_url", "webhook_secret", "idle_hours"]
|
|
83
|
+
|
|
84
|
+
-- el archivo REAL, ignorando LAMPSON_NO_CONFIG: escribir con el doc en blanco de los tests pisaría provider/keys
|
|
85
|
+
-- (pasó el 2026-08-29 probando la rueda con un web de test)
|
|
86
|
+
task load_raw()
|
|
87
|
+
try
|
|
88
|
+
let doc be json_decode(read_file(cfg_path()))
|
|
89
|
+
when not contains(doc, "keys")
|
|
90
|
+
set doc["keys"] to {}
|
|
91
|
+
when not contains(doc, "provider")
|
|
92
|
+
set doc["provider"] to ""
|
|
93
|
+
when not contains(doc, "model")
|
|
94
|
+
set doc["model"] to ""
|
|
95
|
+
give doc
|
|
96
|
+
recover err
|
|
97
|
+
give {"provider": "", "model": "", "keys": {}}
|
|
98
|
+
|
|
99
|
+
export task set_value(key, value)
|
|
100
|
+
require file(".lampson")
|
|
101
|
+
require file(".lampson/*")
|
|
102
|
+
require env("LAMPSON_*")
|
|
103
|
+
when not contains(KEYS, key)
|
|
104
|
+
raise("unknown setting '" + text(key) + "'")
|
|
105
|
+
let doc be load_raw()
|
|
106
|
+
when trim(text(value)) == ""
|
|
107
|
+
let clean be {}
|
|
108
|
+
each k in keys(doc)
|
|
109
|
+
when k != key
|
|
110
|
+
set clean[k] to doc[k]
|
|
111
|
+
set doc to clean
|
|
112
|
+
otherwise
|
|
113
|
+
set doc[key] to trim(text(value))
|
|
114
|
+
save(doc)
|
|
115
|
+
give true
|
|
116
|
+
|
|
117
|
+
export task values()
|
|
118
|
+
require file.read(".lampson")
|
|
119
|
+
require file.read(".lampson/*")
|
|
120
|
+
require env("LAMPSON_*")
|
|
121
|
+
let doc be load_raw()
|
|
122
|
+
let out be {}
|
|
123
|
+
each k in KEYS
|
|
124
|
+
when k == "webhook_secret"
|
|
125
|
+
set out["has_webhook_secret"] to (contains(doc, k) and text(doc[k]) != "") or env("LAMPSON_WEBHOOK_SECRET", "") != ""
|
|
126
|
+
otherwise
|
|
127
|
+
set out[k] to when contains(doc, k) then text(doc[k]) otherwise ""
|
|
128
|
+
-- lo fijado en .env gana y se muestra como tal
|
|
129
|
+
let e be env("LAMPSON_" + upper(k), "")
|
|
130
|
+
when e != ""
|
|
131
|
+
set out[k] to e
|
|
132
|
+
set out[k + "_from_env"] to true
|
|
133
|
+
give out
|
package/lib/skills.syn
CHANGED
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
let ROOTS be [
|
|
29
29
|
{"dir": ".lampson/skills-global", "source": "global"},
|
|
30
30
|
{"dir": ".lampson/skills-claude", "source": "global"},
|
|
31
|
+
{"dir": ".lampson/global/skills-global", "source": "global"},
|
|
32
|
+
{"dir": ".lampson/global/skills-claude", "source": "global"},
|
|
31
33
|
{"dir": "skills", "source": "harness"},
|
|
32
34
|
{"dir": "workspace/.claude/skills", "source": "project"},
|
|
33
35
|
{"dir": "workspace/.agents/skills", "source": "project"},
|
package/lib/tools.syn
CHANGED
|
@@ -27,6 +27,7 @@ use "./skills.syn" as skills
|
|
|
27
27
|
use "./mcp.syn" as mcp
|
|
28
28
|
use "./lamps.syn" as lamps
|
|
29
29
|
use "./lsp.syn" as lsp
|
|
30
|
+
use "./schedule.syn" as schedule
|
|
30
31
|
|
|
31
32
|
-- la task de la tool skill (su SPEC está en tools/skill.syn)
|
|
32
33
|
task skill_tool(name, action, source, scope)
|
|
@@ -155,6 +156,96 @@ let LSP_SPEC be {
|
|
|
155
156
|
}, "required": ["op"]}
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
-- la tool schedule (lib/schedule.syn): tareas programadas. add/remove/enable/run piden humano SIEMPRE (permission.syn):
|
|
160
|
+
-- 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 lamp/bash (una prompt anidaría un loop dentro del turno).
|
|
162
|
+
task schedule_tool(action, id, name, at, kind, lamp, tool, args, command, prompt, agent, permission, approval_timeout, notify)
|
|
163
|
+
require exec
|
|
164
|
+
require time
|
|
165
|
+
require net
|
|
166
|
+
require env("LAMPSON_*")
|
|
167
|
+
require env("OS")
|
|
168
|
+
require file(".lampson")
|
|
169
|
+
require file(".lampson/*")
|
|
170
|
+
require file.read("lamps")
|
|
171
|
+
require file.read("lamps/*")
|
|
172
|
+
require file("workspace")
|
|
173
|
+
require file("workspace/*")
|
|
174
|
+
let act be when action == nothing then "list" otherwise action
|
|
175
|
+
when act == "add"
|
|
176
|
+
let a be {"type": kind}
|
|
177
|
+
when kind == "lamp"
|
|
178
|
+
set a to {"type": "lamp", "lamp": lamp, "tool": tool, "args": when args == nothing then {} otherwise args}
|
|
179
|
+
otherwise when kind == "bash"
|
|
180
|
+
set a to {"type": "bash", "command": command}
|
|
181
|
+
otherwise when kind == "prompt"
|
|
182
|
+
set a to {"type": "prompt", "prompt": prompt, "agent": when agent == nothing then "build" otherwise agent}
|
|
183
|
+
let spec be {"name": name, "when": at, "action": a, "permission": when permission == nothing then "ask" otherwise permission}
|
|
184
|
+
when approval_timeout != nothing
|
|
185
|
+
set spec["approval_timeout"] to approval_timeout
|
|
186
|
+
when notify != nothing
|
|
187
|
+
set spec["notify"] to notify
|
|
188
|
+
let t be schedule.add(spec)
|
|
189
|
+
give "scheduled '" + t["name"] + "' (id " + t["id"] + "): " + schedule.describe_plan(t["plan"]) + " (local time) · " + schedule.describe_action(t["action"]) + " · permission " + t["permission"] + " · next run " + schedule.fmt_local(t["next_run"]) + " — tell the user this next-run time as is. The run's report lands in a NEW session named ⏰ <name> (sidebar «Sesiones» / /sessions) and, if notify is set, in that webhook — NOT in this chat: say so. " + daemon_note()
|
|
190
|
+
when act == "remove"
|
|
191
|
+
give schedule.remove(id)
|
|
192
|
+
when act == "enable"
|
|
193
|
+
give schedule.set_enabled(id, true)
|
|
194
|
+
when act == "disable"
|
|
195
|
+
give schedule.set_enabled(id, false)
|
|
196
|
+
when act == "run"
|
|
197
|
+
let t be schedule.get(id)
|
|
198
|
+
when t == nothing
|
|
199
|
+
raise("no scheduled task '" + text(id) + "'")
|
|
200
|
+
when t["action"]["type"] == "prompt"
|
|
201
|
+
schedule.request_run(id)
|
|
202
|
+
give "run requested: it runs within " + text(schedule.TICK_SECONDS) + " s in the background and its report lands in a NEW session (⏰ " + t["name"] + "), not in this chat. " + daemon_note()
|
|
203
|
+
schedule.mark_started(t)
|
|
204
|
+
let started be now()
|
|
205
|
+
let out be schedule.run_simple(t)
|
|
206
|
+
schedule.record(t, started, when starts_with(out, "ERROR") or starts_with(out, "DENIED") then "error" otherwise "ok", out, {})
|
|
207
|
+
give out
|
|
208
|
+
when act == "log"
|
|
209
|
+
let tail be schedule.log_tail(id, 80)
|
|
210
|
+
give when tail == "" then "no runs yet for " + text(id) otherwise tail
|
|
211
|
+
let sm be schedule.summary()
|
|
212
|
+
when length(sm) == 0
|
|
213
|
+
give "no scheduled tasks. " + daemon_note()
|
|
214
|
+
let lines be []
|
|
215
|
+
each s in sm
|
|
216
|
+
set lines to append(lines, s["id"] + (when s["enabled"] then " ON " otherwise " off") + " · " + s["plan"] + " · " + s["action_text"] + " · permission " + s["permission"] + " · next " + s["next_local"] + (when s["last_run"] != nothing then " · last " + s["last_status"] + ": " + s["last_summary"] otherwise ""))
|
|
217
|
+
give join(lines, "\n") + "\n" + daemon_note()
|
|
218
|
+
|
|
219
|
+
task daemon_note()
|
|
220
|
+
require time
|
|
221
|
+
require file(".lampson")
|
|
222
|
+
require file(".lampson/*")
|
|
223
|
+
let age be schedule.daemon_age()
|
|
224
|
+
when age == nothing or age > schedule.TICK_SECONDS * 3
|
|
225
|
+
give "NOTE: no scheduler is running right now (scheduled tasks run inside the web/daemon process): the user starts it with `lampson --daemon start` (or keeps `lampson --web` open)."
|
|
226
|
+
give "Scheduler alive (last tick " + text(floor(age)) + " s ago)."
|
|
227
|
+
|
|
228
|
+
let SCHEDULE_SPEC be {
|
|
229
|
+
"name": "schedule",
|
|
230
|
+
"description": "Scheduled tasks: run something on a schedule with nobody watching — a lamp 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
|
+
"parameters": {"type": "object", "properties": {
|
|
232
|
+
"action": {"type": "string", "enum": ["list", "add", "remove", "enable", "disable", "run", "log"], "description": "Default: list"},
|
|
233
|
+
"id": {"type": "string", "description": "remove/enable/disable/run/log: the task id (from list)"},
|
|
234
|
+
"name": {"type": "string", "description": "add: short human name"},
|
|
235
|
+
"at": {"type": "string", "description": "add: the schedule — 'every 6h' | 'daily 09:00' | 'mon,wed 08:30' | 'weekdays 09:00'"},
|
|
236
|
+
"kind": {"type": "string", "enum": ["lamp", "bash", "prompt"], "description": "add: what runs"},
|
|
237
|
+
"lamp": {"type": "string", "description": "add kind=lamp: lamp name (must be ON)"},
|
|
238
|
+
"tool": {"type": "string", "description": "add kind=lamp: tool of that lamp"},
|
|
239
|
+
"args": {"type": "object", "description": "add kind=lamp: arguments for the tool"},
|
|
240
|
+
"command": {"type": "string", "description": "add kind=bash: the shell command (must finish on its own; no servers)"},
|
|
241
|
+
"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
|
+
"agent": {"type": "string", "enum": ["build", "plan", "review", "explore"], "description": "add kind=prompt: profile (default build)"},
|
|
243
|
+
"permission": {"type": "string", "enum": ["strict", "ask", "yolo"], "description": "add: permission envelope for unattended runs (default ask)"},
|
|
244
|
+
"approval_timeout": {"type": "integer", "description": "add: seconds to wait for a remote approval (default 7200)"},
|
|
245
|
+
"notify": {"type": "string", "description": "add: webhook URL that receives each result as JSON"}
|
|
246
|
+
}, "required": ["action"]}
|
|
247
|
+
}
|
|
248
|
+
|
|
158
249
|
-- Allow-list nombre → task. El modelo solo devuelve NOMBRES; el loop decide si se ejecuta.
|
|
159
250
|
export task registry()
|
|
160
251
|
give {
|
|
@@ -171,10 +262,11 @@ export task registry()
|
|
|
171
262
|
"skill": skill_tool,
|
|
172
263
|
"mcp": mcp_tool,
|
|
173
264
|
"lamp": lamp_tool,
|
|
174
|
-
"lsp": lsp_tool
|
|
265
|
+
"lsp": lsp_tool,
|
|
266
|
+
"schedule": schedule_tool
|
|
175
267
|
}
|
|
176
268
|
|
|
177
|
-
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, LAMP_SPEC]
|
|
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, LAMP_SPEC, SCHEDULE_SPEC]
|
|
178
270
|
|
|
179
271
|
-- Subconjuntos (para perfiles de agente): registry/catálogo filtrados por nombre.
|
|
180
272
|
export task registry_subset(names)
|