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.
Files changed (79) hide show
  1. package/.env.example +10 -1
  2. package/README.md +9 -3
  3. package/bin/lampson.js +1 -1
  4. package/chat.syn +112 -8
  5. package/cli.syn +143 -7
  6. package/edge.syn +26 -0
  7. package/hub.tpl.syn +127 -6
  8. package/lampson.ps1 +43 -2
  9. package/lampson.sh +40 -2
  10. package/lib/agents.syn +30 -9
  11. package/lib/approvals.syn +2 -2
  12. package/lib/auth.syn +274 -0
  13. package/lib/computer.syn +2000 -0
  14. package/lib/computer_judge.syn +410 -0
  15. package/lib/fs.syn +242 -0
  16. package/lib/git.syn +2 -2
  17. package/lib/judge.syn +78 -0
  18. package/lib/line.syn +2 -2
  19. package/lib/loop.syn +41 -8
  20. package/lib/lsp.syn +4 -4
  21. package/lib/mcp.syn +20 -4
  22. package/lib/md.syn +11 -11
  23. package/lib/origin.syn +67 -0
  24. package/lib/permission.syn +47 -2
  25. package/lib/prompt.syn +10 -1
  26. package/lib/provider.syn +70 -20
  27. package/lib/schedule.syn +12 -12
  28. package/lib/session.syn +5 -5
  29. package/lib/settings.syn +65 -1
  30. package/lib/skills.syn +22 -6
  31. package/lib/tools/bash.syn +1 -1
  32. package/lib/tools/common.syn +1 -1
  33. package/lib/tools/fetch.syn +600 -0
  34. package/lib/tools/grep.syn +1 -1
  35. package/lib/tools/memo.syn +3 -3
  36. package/lib/tools/proc.syn +38 -20
  37. package/lib/tools/qr.js +26 -0
  38. package/lib/tools/url.syn +220 -0
  39. package/lib/tools.syn +25 -2
  40. package/lib/trace.syn +2 -2
  41. package/lib/tree.syn +29 -13
  42. package/lib/update.syn +1 -1
  43. package/lib/workspaces.syn +202 -36
  44. package/package.json +12 -3
  45. package/public/apple-touch-icon.png +0 -0
  46. package/public/badge-96.png +0 -0
  47. package/public/css/code.css +48 -0
  48. package/public/css/hub.css +9 -0
  49. package/public/css/login.css +16 -0
  50. package/public/css/panel.css +30 -0
  51. package/public/css/sidebar.css +21 -0
  52. package/public/css/tokens.css +1 -1
  53. package/public/hub.html +11 -1
  54. package/public/icon-192.png +0 -0
  55. package/public/icon-512.png +0 -0
  56. package/public/icon-maskable-512.png +0 -0
  57. package/public/icons-src/badge.svg +12 -0
  58. package/public/icons-src/icon-maskable.svg +15 -0
  59. package/public/icons-src/icon.svg +13 -0
  60. package/public/index.html +14 -3
  61. package/public/js/agents.js +1 -1
  62. package/public/js/chat.js +1 -1
  63. package/public/js/config.js +145 -2
  64. package/public/js/core.js +5 -3
  65. package/public/js/devices.js +80 -0
  66. package/public/js/editor.js +232 -0
  67. package/public/js/login.js +78 -0
  68. package/public/js/procs.js +1 -1
  69. package/public/js/pwa.js +5 -0
  70. package/public/js/tree.js +224 -14
  71. package/public/login.html +43 -0
  72. package/public/manifest.webmanifest +16 -0
  73. package/public/offline.html +24 -0
  74. package/public/sw.js +63 -0
  75. package/public/vendor/highlight.min.js +1343 -0
  76. package/public/vendor/qrcode.js +2297 -0
  77. package/skills/computer-use/SKILL.md +178 -0
  78. package/skills/lampson/SKILL.md +13 -0
  79. 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): API compatible con OpenAI. Coding Plan: LAMPSON_BASE_URL=https://api.z.ai/api/coding/paas/v4
41
- "glm": {"wire": "openai", "base_url": "https://api.z.ai/api/paas/v4", "model": "glm-5.3"}
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
- task key_var(name)
48
- give "LAMPSON_API_KEY_" + upper(name)
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 settings.raw_key(name) != ""
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
- when env(key_var(n), "") != ""
169
- set key to secret(key_var(n), "ollama")
170
- when settings.raw_key(n) != ""
171
- set key to as_secret(settings.raw_key(n), "api_key_" + n)
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
- task to_openai_messages(messages)
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
- when p == "glm"
262
- give contains(m, "vision") or ends_with(m, "v") or contains(m, "v-")
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 body be {"model": cfg["model"], "messages": to_openai_messages(messages), "max_tokens": cfg["max_tokens"]}
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 to {"model": cfg["model"], "messages": to_openai_messages(messages), "max_tokens": cfg["max_tokens"], "tools": to_openai_tools(catalog)}
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 replace_text(trim(raw), ":", "")
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(capture(t, "[0-9]+(?:\.[0-9]+)?"))
130
- let unit be replace_re(t, "^[0-9.]+", "")
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 replace_text(replace_text(replace_text(s, " a las ", " "), " at ", " "), "todos los días", "daily")
143
- set s to replace_text(replace_text(s, "todos los dias", "daily"), "every day", "daily")
144
- let toks be where(split(replace_text(s, ",", " "), " "), (x) => x != "")
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(replace_text(trim(p), "\\", "/"))
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(replace_re(text(name), "[^A-Za-z0-9]+", "-"))
373
- set s to replace_re(replace_re(s, "^-+", ""), "-+$", "")
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 replace_text(action["prompt"], "\n", " ")
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(replace_text(replace_text(text(s), "\r", ""), "\n", " ⏎ "))
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(replace_text(read_file(LOG_DIR + "/" + id + ".log"), "\r", ""), "\n")
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 replace_text(e["name"], ".json", "")
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(replace_text(replace_text(s["id"], "-", ""), "test", "")))
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 "") + replace_text(replace_text(slice(c, a, b), "\r", ""), "\n", " ") + (when b < length(c) 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", replace_text(f, "/", "\\")], 10)
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 replace_text(m["content"], "\n", " ")
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
- export let KEYS be ["tz", "public_url", "webhook_url", "webhook_secret", "idle_hours"]
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
- let out be {"name": "", "description": "", "body": md}
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
- task scan_root(root, acc)
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
- set out[name] to {"name": name, "description": fm["description"], "path": path, "source": root["source"]}
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(replace_text(s, "\r", ""))
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
@@ -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(replace_text(command, "\n", " ")))
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)
@@ -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 replace_text(text(path), "\\", "/")
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, "./")