lampson 0.2.7 → 0.2.9
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 +10 -1
- package/README.md +9 -3
- package/bin/lampson.js +1 -1
- package/chat.syn +112 -8
- package/cli.syn +143 -7
- package/edge.syn +26 -0
- package/hub.tpl.syn +127 -6
- package/lampson.ps1 +43 -2
- package/lampson.sh +40 -2
- package/lib/agents.syn +30 -9
- package/lib/approvals.syn +2 -2
- package/lib/auth.syn +274 -0
- package/lib/computer.syn +2000 -0
- package/lib/computer_judge.syn +410 -0
- package/lib/fs.syn +242 -0
- package/lib/git.syn +2 -2
- package/lib/judge.syn +78 -0
- package/lib/line.syn +2 -2
- package/lib/loop.syn +41 -8
- package/lib/lsp.syn +4 -4
- package/lib/mcp.syn +20 -4
- package/lib/md.syn +11 -11
- package/lib/origin.syn +67 -0
- package/lib/permission.syn +47 -2
- package/lib/prompt.syn +10 -1
- package/lib/provider.syn +70 -20
- package/lib/schedule.syn +12 -12
- package/lib/session.syn +5 -5
- package/lib/settings.syn +65 -1
- package/lib/skills.syn +22 -6
- package/lib/tools/bash.syn +1 -1
- package/lib/tools/common.syn +1 -1
- package/lib/tools/fetch.syn +600 -0
- package/lib/tools/grep.syn +1 -1
- package/lib/tools/memo.syn +3 -3
- package/lib/tools/proc.syn +38 -20
- package/lib/tools/qr.js +26 -0
- package/lib/tools/url.syn +220 -0
- package/lib/tools.syn +25 -2
- package/lib/trace.syn +2 -2
- package/lib/tree.syn +29 -13
- package/lib/update.syn +1 -1
- package/lib/workspaces.syn +202 -36
- package/package.json +12 -3
- package/public/apple-touch-icon.png +0 -0
- package/public/badge-96.png +0 -0
- package/public/css/code.css +48 -0
- package/public/css/hub.css +9 -0
- package/public/css/login.css +16 -0
- package/public/css/panel.css +30 -0
- package/public/css/sidebar.css +21 -0
- package/public/css/tokens.css +1 -1
- package/public/hub.html +11 -1
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon-maskable-512.png +0 -0
- package/public/icons-src/badge.svg +12 -0
- package/public/icons-src/icon-maskable.svg +15 -0
- package/public/icons-src/icon.svg +13 -0
- package/public/index.html +14 -3
- package/public/js/agents.js +1 -1
- package/public/js/chat.js +1 -1
- package/public/js/config.js +145 -2
- package/public/js/core.js +5 -3
- package/public/js/devices.js +80 -0
- package/public/js/editor.js +232 -0
- package/public/js/login.js +78 -0
- package/public/js/procs.js +1 -1
- package/public/js/pwa.js +5 -0
- package/public/js/tree.js +224 -14
- package/public/login.html +43 -0
- package/public/manifest.webmanifest +16 -0
- package/public/offline.html +24 -0
- package/public/sw.js +63 -0
- package/public/vendor/highlight.min.js +1343 -0
- package/public/vendor/qrcode.js +2297 -0
- package/skills/computer-use/SKILL.md +178 -0
- package/skills/lampson/SKILL.md +13 -0
- package/web.syn +82 -7
package/lib/provider.syn
CHANGED
|
@@ -37,15 +37,39 @@ export let PRESETS be {
|
|
|
37
37
|
"ollama": {"wire": "openai", "base_url": "http://127.0.0.1:11434/v1", "model": "qwen2.5-coder"},
|
|
38
38
|
"anthropic": {"wire": "anthropic", "base_url": "https://api.anthropic.com/v1", "model": "claude-sonnet-4-6"},
|
|
39
39
|
"minimax": {"wire": "anthropic", "base_url": "https://api.minimax.io/anthropic/v1", "model": "MiniMax-M3"},
|
|
40
|
-
-- Z.ai (Zhipu)
|
|
41
|
-
|
|
40
|
+
-- Z.ai (Zhipu), API compatible con OpenAI, dos presets con la MISMA key (es la misma cuenta):
|
|
41
|
+
-- `glm` = pago por uso (saldo prepago); `glm-coding` = GLM Coding Plan (suscripción), que solo vale
|
|
42
|
+
-- en el endpoint /coding/. La key de un Coding Plan contra `glm` da 429 código 1113 "Insufficient
|
|
43
|
+
-- balance or no resource package" (visto 2026-09-05): no es la key, es el endpoint.
|
|
44
|
+
-- Modelo por defecto glm-5.3-flash (2026-09-07): el primer GLM-5 multimodal nativo (imágenes por
|
|
45
|
+
-- image_url, verificado con un PNG en data URL), 1M de contexto, 128K de salida y más rápido que
|
|
46
|
+
-- glm-5.3; ambos endpoints lo listan en GET /models. Su thinking no se puede apagar (ver chat_openai).
|
|
47
|
+
"glm": {"wire": "openai", "base_url": "https://api.z.ai/api/paas/v4", "model": "glm-5.3-flash"},
|
|
48
|
+
"glm-coding": {"wire": "openai", "base_url": "https://api.z.ai/api/coding/paas/v4", "model": "glm-5.3-flash"}
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
-- Keys: una por proveedor en .env como LAMPSON_API_KEY_<PROVIDER> (LAMPSON_API_KEY_DEEPSEEK, _ANTHROPIC…);
|
|
45
52
|
-- LAMPSON_API_KEY es la del proveedor por defecto (LAMPSON_PROVIDER). Así se cambia de proveedor en caliente
|
|
46
53
|
-- (/provider en terminal, selector en la web) sin tocar .env. Solo se comprueba SI hay key, nunca se muestra.
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
-- (`glm-coding` → LAMPSON_API_KEY_GLM_CODING: un guion no es válido en un nombre de variable)
|
|
55
|
+
export task key_var(name)
|
|
56
|
+
give "LAMPSON_API_KEY_" + upper(join(split(name, "-"), "_"))
|
|
57
|
+
|
|
58
|
+
-- Presets que comparten cuenta y key (z.ai): si uno no tiene key propia, usa la del otro.
|
|
59
|
+
let KEY_ALIASES be {"glm-coding": "glm", "glm": "glm-coding"}
|
|
60
|
+
|
|
61
|
+
-- Nombre bajo el que hay key para `n` (config.json o LAMPSON_API_KEY_<P>): él mismo, si no su alias, si no "".
|
|
62
|
+
task key_owner(n)
|
|
63
|
+
require env("LAMPSON_*")
|
|
64
|
+
require file.read(".lampson")
|
|
65
|
+
require file.read(".lampson/*")
|
|
66
|
+
when settings.raw_key(n) != "" or env(key_var(n), "") != ""
|
|
67
|
+
give n
|
|
68
|
+
when contains(KEY_ALIASES, n)
|
|
69
|
+
let a be KEY_ALIASES[n]
|
|
70
|
+
when settings.raw_key(a) != "" or env(key_var(a), "") != ""
|
|
71
|
+
give a
|
|
72
|
+
give ""
|
|
49
73
|
|
|
50
74
|
-- proveedor por defecto: config.json (onboarding / UI) > LAMPSON_PROVIDER en .env > deepseek
|
|
51
75
|
task env_default()
|
|
@@ -67,9 +91,7 @@ export task has_key(name)
|
|
|
67
91
|
require file.read(".lampson/*")
|
|
68
92
|
when name == "ollama"
|
|
69
93
|
give true
|
|
70
|
-
when
|
|
71
|
-
give true
|
|
72
|
-
when env(key_var(name), "") != ""
|
|
94
|
+
when key_owner(name) != ""
|
|
73
95
|
give true
|
|
74
96
|
give name == env_default() and env("LAMPSON_API_KEY", "") != ""
|
|
75
97
|
|
|
@@ -97,7 +119,7 @@ export task providers()
|
|
|
97
119
|
-- Nombres de modelo: DeepSeek, OpenAI, Anthropic, Groq, Kimi, Grok y OpenRouter publican ids en minúsculas
|
|
98
120
|
-- y la API rechaza otra capitalización ("deepseek-v4-pro" sí, "DeepSeek-V4-Pro" → 400 invalid_request_error,
|
|
99
121
|
-- visto 2026-08-27 con un nombre tipeado en /setup). MiniMax ("MiniMax-M3") y Ollama (tags) se dejan tal cual.
|
|
100
|
-
let LOWERCASE_MODELS be ["deepseek", "openai", "anthropic", "groq", "kimi", "grok", "openrouter", "glm"]
|
|
122
|
+
let LOWERCASE_MODELS be ["deepseek", "openai", "anthropic", "groq", "kimi", "grok", "openrouter", "glm", "glm-coding"]
|
|
101
123
|
|
|
102
124
|
export task normalize_model(name, model)
|
|
103
125
|
let m be trim(text(model))
|
|
@@ -163,12 +185,15 @@ export task config_for(name, model)
|
|
|
163
185
|
when model != nothing and model != ""
|
|
164
186
|
set m to model
|
|
165
187
|
set m to normalize_model(n, m)
|
|
166
|
-
-- key: guardada en config.json > LAMPSON_API_KEY_<P> > LAMPSON_API_KEY (solo para el proveedor de .env)
|
|
188
|
+
-- key: guardada en config.json > LAMPSON_API_KEY_<P> > LAMPSON_API_KEY (solo para el proveedor de .env).
|
|
189
|
+
-- Si el preset no tiene key propia se usa la de su alias (KEY_ALIASES: glm ↔ glm-coding).
|
|
167
190
|
let key be secret("LAMPSON_API_KEY", "ollama")
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
191
|
+
let ko be key_owner(n)
|
|
192
|
+
when ko != ""
|
|
193
|
+
when env(key_var(ko), "") != ""
|
|
194
|
+
set key to secret(key_var(ko), "ollama")
|
|
195
|
+
when settings.raw_key(ko) != ""
|
|
196
|
+
set key to as_secret(settings.raw_key(ko), "api_key_" + ko)
|
|
172
197
|
give {
|
|
173
198
|
"provider": n,
|
|
174
199
|
"wire": wire,
|
|
@@ -200,7 +225,15 @@ task err_result(msg)
|
|
|
200
225
|
|
|
201
226
|
-- ---------- OpenAI wire (chat/completions) ----------
|
|
202
227
|
|
|
203
|
-
|
|
228
|
+
-- Presets de Z.ai: comparten cuenta y key, y son los únicos que reciben `thinking` y el eco del razonamiento.
|
|
229
|
+
task is_glm(cfg)
|
|
230
|
+
when not contains(cfg, "provider")
|
|
231
|
+
give false
|
|
232
|
+
give cfg["provider"] == "glm" or cfg["provider"] == "glm-coding"
|
|
233
|
+
|
|
234
|
+
-- keep_reasoning: devolver el `reasoning` guardado en cada assistant como `reasoning_content` (solo Z.ai;
|
|
235
|
+
-- otros endpoints podrían rechazar el campo). Ver chat_openai.
|
|
236
|
+
task to_openai_messages(messages, keep_reasoning)
|
|
204
237
|
let out be []
|
|
205
238
|
each m in messages
|
|
206
239
|
when m["role"] == "assistant"
|
|
@@ -211,6 +244,10 @@ task to_openai_messages(messages)
|
|
|
211
244
|
each tc in m["tool_calls"]
|
|
212
245
|
set tcs to append(tcs, {"id": tc["id"], "type": "function", "function": {"name": tc["name"], "arguments": json_encode(tc["args"])}})
|
|
213
246
|
set entry to {"role": "assistant", "content": m["content"], "tool_calls": tcs}
|
|
247
|
+
when keep_reasoning and contains(m, "reasoning")
|
|
248
|
+
when m["reasoning"] != nothing
|
|
249
|
+
when m["reasoning"] != ""
|
|
250
|
+
set entry["reasoning_content"] to m["reasoning"]
|
|
214
251
|
set out to append(out, entry)
|
|
215
252
|
otherwise when m["role"] == "tool"
|
|
216
253
|
set out to append(out, {"role": "tool", "tool_call_id": m["tool_call_id"], "content": m["content"]})
|
|
@@ -257,9 +294,10 @@ export task supports_vision(cfg)
|
|
|
257
294
|
give contains(m, "vision") or starts_with(m, "grok-4")
|
|
258
295
|
when p == "kimi"
|
|
259
296
|
give contains(m, "vision") or contains(m, "vl")
|
|
260
|
-
-- GLM: los modelos con visión llevan sufijo v (glm-4.5v, glm-4.6v)
|
|
261
|
-
|
|
262
|
-
|
|
297
|
+
-- GLM: los modelos con visión llevan sufijo v (glm-4.5v, glm-4.6v); glm-5.3-flash es el primer GLM-5
|
|
298
|
+
-- multimodal nativo (glm-5.3 a secas NO acepta imágenes). Verificado 2026-09-07 con un PNG por data URL.
|
|
299
|
+
when p == "glm" or p == "glm-coding"
|
|
300
|
+
give contains(m, "vision") or ends_with(m, "v") or contains(m, "v-") or starts_with(m, "glm-5.3-flash")
|
|
263
301
|
give false
|
|
264
302
|
|
|
265
303
|
-- Modelo sin visión: la imagen se reemplaza por una nota que el modelo puede explicarle al usuario
|
|
@@ -311,9 +349,16 @@ task parse_args(raw)
|
|
|
311
349
|
give {"_raw": raw, "_parse_error": err}
|
|
312
350
|
|
|
313
351
|
task chat_openai(cfg, messages, catalog)
|
|
314
|
-
let
|
|
352
|
+
let glm be is_glm(cfg)
|
|
353
|
+
let body be {"model": cfg["model"], "messages": to_openai_messages(messages, glm), "max_tokens": cfg["max_tokens"]}
|
|
315
354
|
when length(catalog) > 0
|
|
316
|
-
set body
|
|
355
|
+
set body["tools"] to to_openai_tools(catalog)
|
|
356
|
+
-- Z.ai: el thinking de glm-5.3-flash no se puede apagar y la doc recomienda clear_thinking=false, que
|
|
357
|
+
-- conserva el razonamiento entre pasos de un turno SI cada assistant vuelve con su reasoning_content
|
|
358
|
+
-- (por eso se guarda `reasoning` en el mensaje y to_openai_messages lo ecoa). Solo para los presets
|
|
359
|
+
-- glm: otros endpoints OpenAI rechazan parámetros desconocidos. Verificado 2026-09-07: 200 con y sin eco.
|
|
360
|
+
when glm
|
|
361
|
+
set body["thinking"] to {"type": "enabled", "clear_thinking": false}
|
|
317
362
|
let headers be {"Authorization": bearer(cfg["key"]), "Content-Type": "application/json"}
|
|
318
363
|
let r be http_post(strip_slash(cfg["base_url"]) + "/chat/completions", json_encode(body), headers, cfg["timeout"])
|
|
319
364
|
when contains(r, "error")
|
|
@@ -331,6 +376,11 @@ task chat_openai(cfg, messages, catalog)
|
|
|
331
376
|
when msg["tool_calls"] != nothing
|
|
332
377
|
each tc in msg["tool_calls"]
|
|
333
378
|
set calls to append(calls, {"id": tc["id"], "name": tc["function"]["name"], "args": parse_args(tc["function"]["arguments"])})
|
|
379
|
+
-- razonamiento del paso (reasoning_content): se guarda solo para Z.ai, que es quien lo vuelve a pedir
|
|
380
|
+
let reasoning be ""
|
|
381
|
+
when glm and contains(msg, "reasoning_content")
|
|
382
|
+
when msg["reasoning_content"] != nothing
|
|
383
|
+
set reasoning to text(msg["reasoning_content"])
|
|
334
384
|
let usage be {"input": 0, "output": 0, "cached": 0}
|
|
335
385
|
when contains(j, "usage")
|
|
336
386
|
when j["usage"] != nothing
|
|
@@ -348,7 +398,7 @@ task chat_openai(cfg, messages, catalog)
|
|
|
348
398
|
give {"text": "", "tool_calls": [], "usage": usage, "stop": "end", "error": "empty response: the model used its whole output budget (max_tokens=" + text(cfg["max_tokens"]) + ") without emitting text or tool calls — a reasoning model can spend it all thinking. Raise LAMPSON_MAX_TOKENS (or simplify the request) and say «seguí»."}
|
|
349
399
|
set txt to txt + "\n\n[respuesta cortada: se alcanzó max_tokens=" + text(cfg["max_tokens"]) + " — decí «seguí» para que continúe]"
|
|
350
400
|
let finish be when length(calls) > 0 then "tool_calls" otherwise "end"
|
|
351
|
-
give {"text": txt, "tool_calls": calls, "usage": usage, "stop": finish, "error": nothing}
|
|
401
|
+
give {"text": txt, "tool_calls": calls, "usage": usage, "stop": finish, "error": nothing, "reasoning": reasoning}
|
|
352
402
|
|
|
353
403
|
-- ---------- Anthropic wire (messages) ----------
|
|
354
404
|
|
package/lib/schedule.syn
CHANGED
|
@@ -70,7 +70,7 @@ export task tz_offset()
|
|
|
70
70
|
|
|
71
71
|
-- "+0200" | "-03:00" | "-3" → segundos
|
|
72
72
|
export task parse_offset(raw)
|
|
73
|
-
let s be
|
|
73
|
+
let s be replace(trim(raw), ":", "")
|
|
74
74
|
when s == ""
|
|
75
75
|
give 0
|
|
76
76
|
let sign be when starts_with(s, "-") then -1 otherwise 1
|
|
@@ -126,8 +126,8 @@ task parse_span(tok)
|
|
|
126
126
|
let t be lower(tok)
|
|
127
127
|
when not matches(t, "[0-9]+(\.[0-9]+)?(s|m|h|d|min|h?ora?s?|d[ií]as?|seg(undos)?|minutos?)?")
|
|
128
128
|
give nothing
|
|
129
|
-
let num be number(
|
|
130
|
-
let unit be
|
|
129
|
+
let num be number(regex_capture(t, "[0-9]+(?:\.[0-9]+)?")[0])
|
|
130
|
+
let unit be regex_replace(t, "^[0-9.]+", "")
|
|
131
131
|
when unit == "s" or starts_with(unit, "seg")
|
|
132
132
|
give floor(num)
|
|
133
133
|
when unit == "m" or starts_with(unit, "min")
|
|
@@ -139,9 +139,9 @@ task parse_span(tok)
|
|
|
139
139
|
-- → {ok, plan: {kind: every|daily|weekly, seconds?, hh?, mm?, days?}, error}
|
|
140
140
|
export task parse_when(spec)
|
|
141
141
|
let s be lower(trim(text(spec)))
|
|
142
|
-
set s to
|
|
143
|
-
set s to
|
|
144
|
-
let toks be where(split(
|
|
142
|
+
set s to replace(replace(replace(s, " a las ", " "), " at ", " "), "todos los días", "daily")
|
|
143
|
+
set s to replace(replace(s, "todos los dias", "daily"), "every day", "daily")
|
|
144
|
+
let toks be where(split(replace(s, ",", " "), " "), (x) => x != "")
|
|
145
145
|
when length(toks) == 0
|
|
146
146
|
give {"ok": false, "error": "empty schedule"}
|
|
147
147
|
let head be toks[0]
|
|
@@ -335,7 +335,7 @@ export task mount_ok()
|
|
|
335
335
|
give true
|
|
336
336
|
|
|
337
337
|
task norm_path(p)
|
|
338
|
-
let s be lower(
|
|
338
|
+
let s be lower(replace(trim(p), "\\", "/"))
|
|
339
339
|
-- Git Bash: /c/Users/x → c:/users/x
|
|
340
340
|
when matches(s, "/[a-z]/.*")
|
|
341
341
|
set s to slice(s, 1, 2) + ":" + slice(s, 2, length(s))
|
|
@@ -369,8 +369,8 @@ task put(t)
|
|
|
369
369
|
give t
|
|
370
370
|
|
|
371
371
|
task slug(name)
|
|
372
|
-
let s be lower(
|
|
373
|
-
set s to
|
|
372
|
+
let s be lower(regex_replace(text(name), "[^A-Za-z0-9]+", "-"))
|
|
373
|
+
set s to regex_replace(regex_replace(s, "^-+", ""), "-+$", "")
|
|
374
374
|
when s == ""
|
|
375
375
|
give "task"
|
|
376
376
|
when length(s) > 24
|
|
@@ -471,7 +471,7 @@ task default_name(action)
|
|
|
471
471
|
when action["type"] == "bash"
|
|
472
472
|
let cmd be action["command"]
|
|
473
473
|
give when length(cmd) > 40 then slice(cmd, 0, 40) + "…" otherwise cmd
|
|
474
|
-
let p be
|
|
474
|
+
let p be replace(action["prompt"], "\n", " ")
|
|
475
475
|
give when length(p) > 40 then slice(p, 0, 40) + "…" otherwise p
|
|
476
476
|
|
|
477
477
|
export task remove(id)
|
|
@@ -628,7 +628,7 @@ export task record(t, started, status, output, extra)
|
|
|
628
628
|
give fresh
|
|
629
629
|
|
|
630
630
|
task one_line(s, max)
|
|
631
|
-
let t be trim(
|
|
631
|
+
let t be trim(replace(replace(text(s), "\r", ""), "\n", " ⏎ "))
|
|
632
632
|
when length(t) > max
|
|
633
633
|
give slice(t, 0, max) + "…"
|
|
634
634
|
give t
|
|
@@ -646,7 +646,7 @@ export task log_tail(id, n)
|
|
|
646
646
|
when not matches(text(id), "[a-z0-9-]{1,40}")
|
|
647
647
|
give ""
|
|
648
648
|
try
|
|
649
|
-
let lines be split(
|
|
649
|
+
let lines be split(replace(read_file(LOG_DIR + "/" + id + ".log"), "\r", ""), "\n")
|
|
650
650
|
when length(lines) > n
|
|
651
651
|
set lines to slice(lines, length(lines) - n, length(lines))
|
|
652
652
|
give join(lines, "\n")
|
package/lib/session.syn
CHANGED
|
@@ -68,7 +68,7 @@ export task list()
|
|
|
68
68
|
let entries be list_dir(DIR)
|
|
69
69
|
each e in entries
|
|
70
70
|
when not e["is_dir"]
|
|
71
|
-
let id be
|
|
71
|
+
let id be replace(e["name"], ".json", "")
|
|
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 ""
|
|
@@ -80,7 +80,7 @@ export task list()
|
|
|
80
80
|
set out to out
|
|
81
81
|
recover err
|
|
82
82
|
give []
|
|
83
|
-
give sort_by(out, (s) => 0 - number(
|
|
83
|
+
give sort_by(out, (s) => 0 - number(replace(replace(s["id"], "-", ""), "test", "")))
|
|
84
84
|
|
|
85
85
|
-- Búsqueda de texto en las sesiones de ESTE proyecto (título y contenido de los mensajes user/assistant):
|
|
86
86
|
-- [{id, updated, title, snippet}] — snippet = el primer trozo donde aparece `q`. Vacío = todas (sin snippet).
|
|
@@ -111,7 +111,7 @@ export task search(q, limit)
|
|
|
111
111
|
let pos be length(split(lc, needle)[0])
|
|
112
112
|
let a be when pos > 60 then pos - 60 otherwise 0
|
|
113
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 "") +
|
|
114
|
+
set hit to (when a > 0 then "…" otherwise "") + replace(replace(slice(c, a, b), "\r", ""), "\n", " ") + (when b < length(c) then "…" otherwise "")
|
|
115
115
|
recover err
|
|
116
116
|
set hit to nothing
|
|
117
117
|
when hit != nothing
|
|
@@ -132,7 +132,7 @@ export task delete(id)
|
|
|
132
132
|
let files be [path_of(id), ".lampson/trace/" + id + ".log", ".lampson/trace/" + id + ".last"]
|
|
133
133
|
each f in files
|
|
134
134
|
when env("OS", "") == "Windows_NT"
|
|
135
|
-
run("cmd", ["/c", "del", "/q",
|
|
135
|
+
run("cmd", ["/c", "del", "/q", replace(f, "/", "\\")], 10)
|
|
136
136
|
otherwise
|
|
137
137
|
run("rm", ["-f", f], 10)
|
|
138
138
|
give "borrada " + id
|
|
@@ -141,7 +141,7 @@ export task delete(id)
|
|
|
141
141
|
export task title_of(messages)
|
|
142
142
|
each m in messages
|
|
143
143
|
when m["role"] == "user"
|
|
144
|
-
let t be
|
|
144
|
+
let t be replace(m["content"], "\n", " ")
|
|
145
145
|
when length(t) > 60
|
|
146
146
|
give slice(t, 0, 60) + "…"
|
|
147
147
|
give t
|
package/lib/settings.syn
CHANGED
|
@@ -41,6 +41,12 @@ export task load()
|
|
|
41
41
|
export task save(doc)
|
|
42
42
|
require file(".lampson")
|
|
43
43
|
require file(".lampson/*")
|
|
44
|
+
require env("LAMPSON_*")
|
|
45
|
+
-- LAMPSON_NO_CONFIG=1 (tests): NUNCA escribir el archivo real. Como load() devuelve un doc en blanco,
|
|
46
|
+
-- cualquier save() (remember_command al tipear /exit en tty_test, /verbose, set_key, set_default…)
|
|
47
|
+
-- pisaba provider y keys del usuario (pasó el 2026-09-05: tty_test dejó config.json vacío).
|
|
48
|
+
when env("LAMPSON_NO_CONFIG", "") == "1"
|
|
49
|
+
give false
|
|
44
50
|
write_file(cfg_path(), json_encode(doc))
|
|
45
51
|
give true
|
|
46
52
|
|
|
@@ -79,7 +85,11 @@ export task set_default(name, model)
|
|
|
79
85
|
|
|
80
86
|
-- valores de configuración generales (rueda de la UI): tz, public_url, webhook_url, webhook_secret…
|
|
81
87
|
-- Vacío = borrar la clave. Nunca se devuelven secretos al navegador: `values()` marca webhook_secret como has_*.
|
|
82
|
-
|
|
88
|
+
-- computer_*: la tool computer_use (lib/computer.syn) — "1" enciende; max_tabs y grant_profile son sus knobs.
|
|
89
|
+
-- judge = off | jev | laya: el juez de decisión de Lampson (lib/judge.syn), una credencial propia que usa computer
|
|
90
|
+
-- use y puede usar cualquier otra parte; judge_model = con laya, la carpeta del checkpoint; con jev, un id para
|
|
91
|
+
-- pinear (opcional). computer_judge / computer_judge_model: los nombres del primer día (2026-09-28), solo se leen.
|
|
92
|
+
export let KEYS be ["tz", "public_url", "webhook_url", "webhook_secret", "idle_hours", "computer_use", "computer_max_tabs", "computer_grant_profile", "judge", "judge_model", "computer_judge", "computer_judge_model"]
|
|
83
93
|
|
|
84
94
|
-- el archivo REAL, ignorando LAMPSON_NO_CONFIG: escribir con el doc en blanco de los tests pisaría provider/keys
|
|
85
95
|
-- (pasó el 2026-08-29 probando la rueda con un web de test)
|
|
@@ -131,3 +141,57 @@ export task values()
|
|
|
131
141
|
set out[k] to e
|
|
132
142
|
set out[k + "_from_env"] to true
|
|
133
143
|
give out
|
|
144
|
+
|
|
145
|
+
-- el juez de computer use como ENTORNO para el runtime: Synsema lee SYNSEMA_JUDGE_* y TYPESAFE_API_KEY del proceso
|
|
146
|
+
-- (o del .env) al arrancar, y un programa no puede cambiarlos en caliente. Lo usan quienes lanzan procesos: el
|
|
147
|
+
-- workspace web (workspaces.start_ws) y la terminal (cli.syn ensure → lampson.ps1|sh). {} = no tocar nada.
|
|
148
|
+
-- La key de TypeSafe se guarda con set_key("typesafe", …) como las de los proveedores.
|
|
149
|
+
export task judge_env()
|
|
150
|
+
require file.read(".lampson")
|
|
151
|
+
require file.read(".lampson/*")
|
|
152
|
+
require env("LAMPSON_*")
|
|
153
|
+
let js be judge_setting()
|
|
154
|
+
let mode be lower(trim(text(js["mode"])))
|
|
155
|
+
let model be trim(text(js["model"]))
|
|
156
|
+
when mode == "jev" or mode == "typesafe"
|
|
157
|
+
let out be {"SYNSEMA_JUDGE_PROVIDER": "typesafe"}
|
|
158
|
+
let k be raw_key("typesafe")
|
|
159
|
+
when k != ""
|
|
160
|
+
set out["TYPESAFE_API_KEY"] to k
|
|
161
|
+
when model != ""
|
|
162
|
+
set out["SYNSEMA_JUDGE_MODEL"] to model
|
|
163
|
+
give out
|
|
164
|
+
when mode == "laya" or mode == "local"
|
|
165
|
+
let out be {"SYNSEMA_JUDGE_PROVIDER": "laya"}
|
|
166
|
+
when model != ""
|
|
167
|
+
set out["SYNSEMA_JUDGE_MODEL"] to model
|
|
168
|
+
give out
|
|
169
|
+
give {}
|
|
170
|
+
|
|
171
|
+
-- el juez configurado → {mode, model, from_env}: `judge` / `judge_model` (LAMPSON_JUDGE en .env gana), y si no, los
|
|
172
|
+
-- nombres viejos computer_judge / computer_judge_model
|
|
173
|
+
export task judge_setting()
|
|
174
|
+
require file.read(".lampson")
|
|
175
|
+
require file.read(".lampson/*")
|
|
176
|
+
require env("LAMPSON_*")
|
|
177
|
+
let v be values()
|
|
178
|
+
let m be trim(text(v["judge"]))
|
|
179
|
+
let mo be trim(text(v["judge_model"]))
|
|
180
|
+
when m == ""
|
|
181
|
+
set m to trim(text(v["computer_judge"]))
|
|
182
|
+
when mo == ""
|
|
183
|
+
set mo to trim(text(v["computer_judge_model"]))
|
|
184
|
+
give {"mode": m, "model": mo, "from_env": contains(v, "judge_from_env")}
|
|
185
|
+
|
|
186
|
+
-- guardar el juez con los nombres nuevos y borrar los viejos (una sola fuente de verdad)
|
|
187
|
+
export task set_judge(mode, model)
|
|
188
|
+
require file(".lampson")
|
|
189
|
+
require file(".lampson/*")
|
|
190
|
+
require env("LAMPSON_*")
|
|
191
|
+
set_value("judge", when mode == "off" then "" otherwise mode)
|
|
192
|
+
when model != nothing
|
|
193
|
+
set_value("judge_model", text(model))
|
|
194
|
+
set_value("computer_judge", "")
|
|
195
|
+
when model != nothing
|
|
196
|
+
set_value("computer_judge_model", "")
|
|
197
|
+
give true
|
package/lib/skills.syn
CHANGED
|
@@ -37,9 +37,12 @@ let ROOTS be [
|
|
|
37
37
|
{"dir": "workspace/.lampson/skills", "source": "local"}
|
|
38
38
|
]
|
|
39
39
|
|
|
40
|
+
use "./computer.syn" as computer
|
|
41
|
+
|
|
40
42
|
task frontmatter(md)
|
|
41
|
-
-- devuelve {name, description, body}; sin frontmatter → name vacío
|
|
42
|
-
|
|
43
|
+
-- devuelve {name, description, requires, body}; sin frontmatter → name vacío.
|
|
44
|
+
-- `requires: computer_use` (skills del harness): la skill solo entra al índice si esa tool está disponible
|
|
45
|
+
let out be {"name": "", "description": "", "requires": "", "body": md}
|
|
43
46
|
when not starts_with(md, "---")
|
|
44
47
|
give out
|
|
45
48
|
let rest be slice(md, 3, length(md))
|
|
@@ -54,10 +57,13 @@ task frontmatter(md)
|
|
|
54
57
|
set out["name"] to trim(slice(l, 5, length(l)))
|
|
55
58
|
otherwise when starts_with(l, "description:")
|
|
56
59
|
set out["description"] to trim(slice(l, 12, length(l)))
|
|
60
|
+
otherwise when starts_with(l, "requires:")
|
|
61
|
+
set out["requires"] to trim(slice(l, 9, length(l)))
|
|
57
62
|
set out["body"] to trim(body)
|
|
58
63
|
give out
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
-- hidden = tools que NO están disponibles: una skill con `requires:` de una de ellas no se lista
|
|
66
|
+
task scan_root(root, acc, hidden)
|
|
61
67
|
let out be acc
|
|
62
68
|
try
|
|
63
69
|
each e in list_dir(root["dir"])
|
|
@@ -66,7 +72,8 @@ task scan_root(root, acc)
|
|
|
66
72
|
try
|
|
67
73
|
let fm be frontmatter(read_file(path))
|
|
68
74
|
let name be when fm["name"] == "" then e["name"] otherwise fm["name"]
|
|
69
|
-
|
|
75
|
+
when not contains(hidden, fm["requires"])
|
|
76
|
+
set out[name] to {"name": name, "description": fm["description"], "path": path, "source": root["source"]}
|
|
70
77
|
recover err
|
|
71
78
|
set out to out
|
|
72
79
|
recover err
|
|
@@ -75,15 +82,20 @@ task scan_root(root, acc)
|
|
|
75
82
|
|
|
76
83
|
-- índice: map name → {name, description, path, source}
|
|
77
84
|
export task index()
|
|
85
|
+
require exec
|
|
86
|
+
require time
|
|
87
|
+
require env("LAMPSON_*")
|
|
88
|
+
require env("OS")
|
|
78
89
|
require file.read("skills")
|
|
79
90
|
require file.read("skills/*")
|
|
80
91
|
require file.read("workspace")
|
|
81
92
|
require file.read("workspace/*")
|
|
82
93
|
require file.read(".lampson")
|
|
83
94
|
require file.read(".lampson/*")
|
|
95
|
+
let hidden be when computer.available() then [] otherwise ["computer_use"]
|
|
84
96
|
let acc be {}
|
|
85
97
|
each r in ROOTS
|
|
86
|
-
set acc to scan_root(r, acc)
|
|
98
|
+
set acc to scan_root(r, acc, hidden)
|
|
87
99
|
give acc
|
|
88
100
|
|
|
89
101
|
-- sección del system prompt
|
|
@@ -98,6 +110,10 @@ export task prompt_section(idx)
|
|
|
98
110
|
give join(lines, "\n")
|
|
99
111
|
|
|
100
112
|
export task load(name)
|
|
113
|
+
require exec
|
|
114
|
+
require time
|
|
115
|
+
require env("LAMPSON_*")
|
|
116
|
+
require env("OS")
|
|
101
117
|
require file.read("skills")
|
|
102
118
|
require file.read("skills/*")
|
|
103
119
|
require file.read("workspace")
|
|
@@ -149,7 +165,7 @@ task ensure_global_mount()
|
|
|
149
165
|
run(shell_exe(), ["-c", when is_win then win otherwise unix], 30, {"cwd": "."})
|
|
150
166
|
|
|
151
167
|
task tail_out(s)
|
|
152
|
-
let t be strip_ansi(
|
|
168
|
+
let t be strip_ansi(replace(s, "\r", ""))
|
|
153
169
|
when length(t) > 3000
|
|
154
170
|
give slice(t, length(t) - 3000, length(t))
|
|
155
171
|
give t
|
package/lib/tools/bash.syn
CHANGED
|
@@ -27,7 +27,7 @@ export task shell_config()
|
|
|
27
27
|
let SERVER_RE be ".*((npm|pnpm|yarn|bun)( run)? (dev|start|serve|watch|preview)|npx (next|vite|nodemon|serve|http-server)|next (dev|start)|vite( |$)|nodemon|synsema serve|uvicorn|gunicorn|flask run|manage\\.py runserver|python3? -m http\\.server|rails s(erver)?|php -S|ng serve|cargo (run|watch)|go run|dotnet (run|watch)|tail -f).*"
|
|
28
28
|
|
|
29
29
|
export task looks_like_server(command)
|
|
30
|
-
let cmd be trim(lower(
|
|
30
|
+
let cmd be trim(lower(replace(command, "\n", " ")))
|
|
31
31
|
when matches(cmd, "timeout [0-9]+.*")
|
|
32
32
|
give false
|
|
33
33
|
give matches(cmd, SERVER_RE)
|
package/lib/tools/common.syn
CHANGED
|
@@ -20,7 +20,7 @@ task has_dotdot(p)
|
|
|
20
20
|
|
|
21
21
|
-- path del modelo → path real bajo workspace/
|
|
22
22
|
export task ws(path)
|
|
23
|
-
let p be when path == nothing then "." otherwise
|
|
23
|
+
let p be when path == nothing then "." otherwise replace(text(path), "\\", "/")
|
|
24
24
|
let stripping be true
|
|
25
25
|
while stripping
|
|
26
26
|
when starts_with(p, "./")
|