lampson 0.2.8 → 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 +8 -3
- package/bin/lampson.js +1 -1
- package/chat.syn +111 -7
- 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 +27 -6
- 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 +19 -3
- package/lib/git.syn +2 -2
- package/lib/judge.syn +78 -0
- package/lib/line.syn +2 -2
- package/lib/loop.syn +36 -4
- 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 +21 -2
- package/lib/prompt.syn +9 -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 +98 -71
- 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 +5 -5
- package/lib/tools.syn +23 -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 +3 -2
- 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/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 +4 -2
- 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 +12 -4
- 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 +5 -0
- package/web.syn +67 -6
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, "./")
|
package/lib/tools/fetch.syn
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
-- lib/tools/fetch.syn — traer una página web o una respuesta HTTP como Markdown/texto, sin ruido
|
|
2
2
|
--
|
|
3
3
|
-- Antes (2026-09-02) el modelo hacía `bash curl <url>` y recibía HTML crudo: head, CSS, nav, scripts —
|
|
4
|
-
-- hasta 10k chars por llamada de puro ruido (synsema.
|
|
4
|
+
-- hasta 10k chars por llamada de puro ruido (synsema.org: 26,8 KB de HTML vs 2,5 KB de Markdown).
|
|
5
5
|
-- Esta tool combina lo mejor de los tres harnesses de referencia:
|
|
6
6
|
-- * opencode webfetch.ts — negociación `Accept` con q-values por formato (markdown/text/html): un sitio
|
|
7
|
-
-- que sirve Markdown (synsema.
|
|
7
|
+
-- que sirve Markdown (synsema.org) lo entrega directo, sin convertir.
|
|
8
8
|
-- * deepseek web-fetch-http — transporte: redirects MANUALES (http_get no los sigue) hasta 5 saltos,
|
|
9
9
|
-- revalidando cada salto con la política de hosts; tope de bytes; Content-Type clasificado (html /
|
|
10
10
|
-- texto / binario); cabecera "url · HTTP status · tipo · tamaño" y pie explícito al truncar.
|
|
@@ -126,27 +126,27 @@ let NAMED be {"nbsp": " ", "lt": "<", "gt": ">", "quot": "\"", "apos": "'", "cop
|
|
|
126
126
|
export task decode_entities(s)
|
|
127
127
|
let out be s
|
|
128
128
|
-- numéricas: é é
|
|
129
|
-
each m in
|
|
129
|
+
each m in regex_find_all(out, "&#[0-9]{1,7};|&#[xX][0-9A-Fa-f]{1,6};")
|
|
130
130
|
let inner be slice(m, 2, length(m) - 1)
|
|
131
131
|
let cp be when starts_with(lower(inner), "x") then hex_to_int(slice(inner, 1, length(inner))) otherwise floor(number(inner))
|
|
132
|
-
set out to
|
|
132
|
+
set out to replace(out, m, cp_text(cp))
|
|
133
133
|
-- con nombre (las comunes; el resto se deja tal cual)
|
|
134
|
-
each m in
|
|
134
|
+
each m in regex_find_all(out, "&[A-Za-z][A-Za-z0-9]{1,8};")
|
|
135
135
|
let name be slice(m, 1, length(m) - 1)
|
|
136
136
|
when contains(NAMED, name)
|
|
137
|
-
set out to
|
|
138
|
-
give
|
|
137
|
+
set out to replace(out, m, NAMED[name])
|
|
138
|
+
give replace(out, "&", "&")
|
|
139
139
|
|
|
140
140
|
task strip_tags(s)
|
|
141
|
-
give
|
|
141
|
+
give regex_replace(s, "(?s)<[^>]*>", "")
|
|
142
142
|
|
|
143
143
|
-- texto de un fragmento inline (título, texto de un link, celda): sin tags, entidades, espacios colapsados
|
|
144
144
|
task inline_text(s)
|
|
145
|
-
give trim(
|
|
145
|
+
give trim(regex_replace(decode_entities(strip_tags(s)), "\\s+", " "))
|
|
146
146
|
|
|
147
147
|
export task title_of(html)
|
|
148
148
|
-- capture con grupos devuelve una LISTA de grupos (verificado): [0] es el título
|
|
149
|
-
let t be
|
|
149
|
+
let t be regex_capture(html, "(?is)<title[^>]*>(.*?)</title>")
|
|
150
150
|
when t == nothing
|
|
151
151
|
give ""
|
|
152
152
|
give inline_text(t[0])
|
|
@@ -157,14 +157,14 @@ export task title_of(html)
|
|
|
157
157
|
task pull_pre(h, plain)
|
|
158
158
|
let blocks be []
|
|
159
159
|
let out be h
|
|
160
|
-
each e in enumerate(
|
|
160
|
+
each e in enumerate(regex_find_all(h, "(?is)<pre\\b.*?</pre>"))
|
|
161
161
|
let blk be e["item"]
|
|
162
|
-
let langm be
|
|
162
|
+
let langm be regex_capture(blk, "(?i)language-([A-Za-z0-9_+#-]+)")
|
|
163
163
|
let lang be when langm == nothing then nothing otherwise langm[0]
|
|
164
|
-
let innerm be
|
|
164
|
+
let innerm be regex_capture(blk, "(?is)<pre\\b[^>]*>(.*?)</pre>")
|
|
165
165
|
let inner be when innerm == nothing then "" otherwise innerm[0]
|
|
166
166
|
let code be decode_entities(strip_tags(inner))
|
|
167
|
-
set code to
|
|
167
|
+
set code to replace(code, "\r\n", "\n")
|
|
168
168
|
-- quitar un salto inicial/final (el HTML suele meterlos), no la indentación
|
|
169
169
|
when starts_with(code, "\n")
|
|
170
170
|
set code to slice(code, 1, length(code))
|
|
@@ -172,47 +172,47 @@ task pull_pre(h, plain)
|
|
|
172
172
|
set code to slice(code, 0, length(code) - 1)
|
|
173
173
|
let fenced be when plain then "\n\n" + code + "\n\n" otherwise "\n\n```" + (when lang != nothing then text(lang) otherwise "") + "\n" + code + "\n```\n\n"
|
|
174
174
|
set blocks to append(blocks, fenced)
|
|
175
|
-
set out to
|
|
175
|
+
set out to replace(out, blk, "%%LPRE" + text(e["index"]) + "%%")
|
|
176
176
|
give {"html": out, "blocks": blocks}
|
|
177
177
|
|
|
178
178
|
task push_pre(s, blocks)
|
|
179
179
|
let out be s
|
|
180
180
|
each e in enumerate(blocks)
|
|
181
|
-
set out to
|
|
181
|
+
set out to replace(out, "%%LPRE" + text(e["index"]) + "%%", e["item"])
|
|
182
182
|
give out
|
|
183
183
|
|
|
184
184
|
-- <ol> → "1. …" (find_all + reemplazo literal: replace_re no puede contar)
|
|
185
185
|
task number_lists(h)
|
|
186
186
|
let out be h
|
|
187
|
-
each blk in
|
|
187
|
+
each blk in regex_find_all(h, "(?is)<ol\\b[^>]*>.*?</ol>")
|
|
188
188
|
let parts be split(blk, "<li")
|
|
189
189
|
let items be []
|
|
190
190
|
each e in enumerate(parts)
|
|
191
191
|
when e["index"] > 0
|
|
192
192
|
-- "<li" + [atributos]> contenido </li>
|
|
193
|
-
let body be
|
|
194
|
-
set body to
|
|
193
|
+
let body be regex_replace(e["item"], "(?s)^[^>]*>", "")
|
|
194
|
+
set body to regex_replace(body, "(?is)</li>.*$", "")
|
|
195
195
|
set items to append(items, text(length(items) + 1) + ". " + trim(body))
|
|
196
|
-
set out to
|
|
196
|
+
set out to replace(out, blk, "\n\n" + join(items, "\n") + "\n\n")
|
|
197
197
|
give out
|
|
198
198
|
|
|
199
199
|
-- <table> → tabla GFM (sin colspan; hermes/deepseek también los ignoran)
|
|
200
200
|
task tables(h)
|
|
201
201
|
let out be h
|
|
202
|
-
each blk in
|
|
202
|
+
each blk in regex_find_all(h, "(?is)<table\\b.*?</table>")
|
|
203
203
|
let rows be []
|
|
204
204
|
let first_is_head be false
|
|
205
205
|
each e in enumerate(split(blk, "<tr"))
|
|
206
206
|
when e["index"] > 0
|
|
207
|
-
let row be
|
|
208
|
-
set row to
|
|
207
|
+
let row be regex_replace(e["item"], "(?s)^[^>]*>", "")
|
|
208
|
+
set row to regex_replace(row, "(?is)</tr>.*$", "")
|
|
209
209
|
let cells be []
|
|
210
|
-
let row_td be
|
|
210
|
+
let row_td be regex_replace(row, "(?i)<th\\b", "<td")
|
|
211
211
|
each ce in enumerate(split(row_td, "<td"))
|
|
212
212
|
when ce["index"] > 0
|
|
213
|
-
let cell be
|
|
214
|
-
set cell to
|
|
215
|
-
set cells to append(cells,
|
|
213
|
+
let cell be regex_replace(ce["item"], "(?s)^[^>]*>", "")
|
|
214
|
+
set cell to regex_replace(cell, "(?is)</t[dh]>.*$", "")
|
|
215
|
+
set cells to append(cells, replace(inline_text(cell), "|", "\\|"))
|
|
216
216
|
when length(cells) > 0
|
|
217
217
|
when length(rows) == 0 and contains(lower(row), "<th")
|
|
218
218
|
set first_is_head to true
|
|
@@ -230,17 +230,17 @@ task tables(h)
|
|
|
230
230
|
when e["index"] > 0
|
|
231
231
|
set with_sep to append(with_sep, e["item"])
|
|
232
232
|
set rows to with_sep
|
|
233
|
-
set out to
|
|
233
|
+
set out to replace(out, blk, "\n\n" + join(rows, "\n") + "\n\n")
|
|
234
234
|
give out
|
|
235
235
|
|
|
236
236
|
-- <a href> → [texto](url absoluta); en plain, solo el texto
|
|
237
237
|
task links(h, base, plain)
|
|
238
238
|
let out be h
|
|
239
|
-
each a in
|
|
240
|
-
let inner be
|
|
239
|
+
each a in regex_find_all(h, "(?is)<a\\b[^>]*>.*?</a>")
|
|
240
|
+
let inner be regex_capture(a, "(?is)<a\\b[^>]*>(.*?)</a>")
|
|
241
241
|
let txt be when inner == nothing then "" otherwise inline_text(inner[0])
|
|
242
|
-
let href be
|
|
243
|
-
let abs be when href == nothing then nothing otherwise u.resolve(base, href[0])
|
|
242
|
+
let href be regex_capture(a, "(?is)<a\\b[^>]*\\shref\\s*=\\s*[\"']([^\"']*)[\"']")
|
|
243
|
+
let abs be when href == nothing then nothing otherwise unwrap_ddg(u.resolve(base, href[0]))
|
|
244
244
|
let repl be txt
|
|
245
245
|
-- anclas internas (#x) sin destino real: "Skip to content", el "§"/"#" de cada heading → fuera
|
|
246
246
|
when abs == nothing
|
|
@@ -248,9 +248,36 @@ task links(h, base, plain)
|
|
|
248
248
|
set repl to ""
|
|
249
249
|
when not plain and abs != nothing and txt != ""
|
|
250
250
|
set repl to when abs == txt then "<" + abs + ">" otherwise "[" + txt + "](" + abs + ")"
|
|
251
|
-
set out to
|
|
251
|
+
set out to replace(out, a, repl)
|
|
252
252
|
give out
|
|
253
253
|
|
|
254
|
+
-- DuckDuckGo (html.duckduckgo.com — buscar en la web SIN API key ni navegador) envuelve cada resultado en
|
|
255
|
+
-- //duckduckgo.com/l/?uddg=<destino codificado>&rut=…: el modelo necesita el destino real (para traerlo con fetch
|
|
256
|
+
-- o abrirlo con computer_use), no el redirect. Sin builtin de percent-decoding: se decodifica a bytes y luego UTF-8.
|
|
257
|
+
task unwrap_ddg(url)
|
|
258
|
+
when url == nothing
|
|
259
|
+
give nothing
|
|
260
|
+
when not contains(url, "duckduckgo.com/l/?")
|
|
261
|
+
give url
|
|
262
|
+
let m be regex_capture(url, "[?&]uddg=([^&]*)")
|
|
263
|
+
when m == nothing
|
|
264
|
+
give url
|
|
265
|
+
give pct_decode(m[0])
|
|
266
|
+
|
|
267
|
+
task pct_decode(s)
|
|
268
|
+
let out be bytes("")
|
|
269
|
+
let i be 0
|
|
270
|
+
let n be length(s)
|
|
271
|
+
while i < n
|
|
272
|
+
let ch be slice(s, i, i + 1)
|
|
273
|
+
when ch == "%" and i + 3 <= n and matches(slice(s, i + 1, i + 3), "[0-9A-Fa-f]{2}")
|
|
274
|
+
set out to out + bytes(slice(s, i + 1, i + 3), "hex")
|
|
275
|
+
set i to i + 3
|
|
276
|
+
otherwise
|
|
277
|
+
set out to out + bytes(when ch == "+" then " " otherwise ch)
|
|
278
|
+
set i to i + 1
|
|
279
|
+
give decode(out, "utf8_lossy")
|
|
280
|
+
|
|
254
281
|
-- blockquote: marcadores y luego prefijo "> " línea por línea
|
|
255
282
|
task quote_lines(s)
|
|
256
283
|
when not contains(s, "%%LBQ%%")
|
|
@@ -261,7 +288,7 @@ task quote_lines(s)
|
|
|
261
288
|
let line be l
|
|
262
289
|
let opens be length(split(line, "%%LBQ%%")) - 1
|
|
263
290
|
let closes be length(split(line, "%%/LBQ%%")) - 1
|
|
264
|
-
set line to
|
|
291
|
+
set line to replace(replace(line, "%%LBQ%%", ""), "%%/LBQ%%", "")
|
|
265
292
|
when inq or opens > 0
|
|
266
293
|
when trim(line) != ""
|
|
267
294
|
set line to "> " + trim(line)
|
|
@@ -273,12 +300,12 @@ task quote_lines(s)
|
|
|
273
300
|
give join(out, "\n")
|
|
274
301
|
|
|
275
302
|
export task to_markdown(html, base, plain)
|
|
276
|
-
let h be
|
|
277
|
-
set h to
|
|
303
|
+
let h be replace(text(html), "\r\n", "\n")
|
|
304
|
+
set h to regex_replace(h, "(?s)<!--.*?-->", "")
|
|
278
305
|
-- ruido: sin contenido para el modelo (un patrón por tag: el motor Rust no tiene backreferences)
|
|
279
306
|
-- (header también: en docs/GitHub es el menú del sitio; el título de la página va aparte en `title:`)
|
|
280
307
|
each tg in ["script", "style", "noscript", "svg", "iframe", "template", "head", "nav", "header", "aside", "footer", "dialog", "select", "button", "canvas", "video", "audio", "object", "embed"]
|
|
281
|
-
set h to
|
|
308
|
+
set h to regex_replace(h, "(?is)<" + tg + "\\b.*?</" + tg + "\\s*>", "")
|
|
282
309
|
let pre be pull_pre(h, plain)
|
|
283
310
|
set h to pre["html"]
|
|
284
311
|
-- links primero (una celda o un ítem con <a> conserva el link); listas numeradas y tablas antes de
|
|
@@ -287,58 +314,58 @@ export task to_markdown(html, base, plain)
|
|
|
287
314
|
set h to number_lists(h)
|
|
288
315
|
set h to tables(h)
|
|
289
316
|
-- imágenes: alt como marca; sin alt, nada (base64 jamás llega al modelo)
|
|
290
|
-
set h to
|
|
291
|
-
set h to
|
|
317
|
+
set h to regex_replace(h, "(?is)<img\\b[^>]*\\salt\\s*=\\s*[\"']([^\"']+)[\"'][^>]*>", "[image: \\1]")
|
|
318
|
+
set h to regex_replace(h, "(?is)<img\\b[^>]*>", "")
|
|
292
319
|
each lvl in ["1", "2", "3", "4", "5", "6"]
|
|
293
320
|
let hashes be when plain then "" otherwise slice("######", 0, floor(number(lvl))) + " "
|
|
294
|
-
set h to
|
|
295
|
-
set h to
|
|
296
|
-
set h to
|
|
297
|
-
set h to
|
|
298
|
-
set h to
|
|
299
|
-
set h to
|
|
300
|
-
set h to
|
|
301
|
-
set h to
|
|
302
|
-
set h to
|
|
303
|
-
set h to
|
|
304
|
-
set h to
|
|
305
|
-
set h to
|
|
321
|
+
set h to regex_replace(h, "(?is)<h" + lvl + "\\b[^>]*>(.*?)</h" + lvl + ">", "\n\n" + hashes + "\\1\n\n")
|
|
322
|
+
set h to regex_replace(h, "(?i)<br\\s*/?>", "\n")
|
|
323
|
+
set h to regex_replace(h, "(?i)<hr\\b[^>]*>", when plain then "\n\n" otherwise "\n\n---\n\n")
|
|
324
|
+
set h to regex_replace(h, "(?is)<blockquote\\b[^>]*>", "\n\n%%LBQ%%")
|
|
325
|
+
set h to regex_replace(h, "(?i)</blockquote>", "%%/LBQ%%\n\n")
|
|
326
|
+
set h to regex_replace(h, "(?i)<li\\b[^>]*>", "\n- ")
|
|
327
|
+
set h to regex_replace(h, "(?i)</li>", "")
|
|
328
|
+
set h to regex_replace(h, "(?i)</(ul|ol|dl)>", "\n")
|
|
329
|
+
set h to regex_replace(h, "(?i)<(ul|ol|dl)\\b[^>]*>", "\n")
|
|
330
|
+
set h to regex_replace(h, "(?i)<dt\\b[^>]*>", "\n**")
|
|
331
|
+
set h to regex_replace(h, "(?i)</dt>", "**\n")
|
|
332
|
+
set h to regex_replace(h, "(?i)<dd\\b[^>]*>", "\n ")
|
|
306
333
|
-- énfasis vacíos decorativos (<i></i> como puntos de una barra) serían asteriscos sueltos
|
|
307
|
-
set h to
|
|
334
|
+
set h to regex_replace(h, "(?is)<(i|em|b|strong)\\b[^>]*>\\s*</(i|em|b|strong)>", "")
|
|
308
335
|
when not plain
|
|
309
|
-
set h to
|
|
310
|
-
set h to
|
|
311
|
-
set h to
|
|
312
|
-
set h to
|
|
313
|
-
set h to
|
|
336
|
+
set h to regex_replace(h, "(?is)<(b|strong)\\b[^>]*>(.*?)</(b|strong)>", "**\\2**")
|
|
337
|
+
set h to regex_replace(h, "(?is)<(i|em)\\b[^>]*>(.*?)</(i|em)>", "*\\2*")
|
|
338
|
+
set h to regex_replace(h, "(?is)<code\\b[^>]*>(.*?)</code>", "`\\1`")
|
|
339
|
+
set h to regex_replace(h, "(?i)</(p|div|section|article|main|header|figure|figcaption|details|summary|form|fieldset|address|tr|dd)>", "\n\n")
|
|
340
|
+
set h to regex_replace(h, "(?i)<(p|div|section|article|main|header|figure|figcaption|details|summary|form|fieldset|address)\\b[^>]*>", "\n")
|
|
314
341
|
set h to strip_tags(h)
|
|
315
342
|
set h to decode_entities(h)
|
|
316
343
|
-- énfasis vacío que quedó de tags sin texto
|
|
317
|
-
set h to
|
|
318
|
-
set h to
|
|
344
|
+
set h to regex_replace(h, "\\*\\*\\s*\\*\\*", "")
|
|
345
|
+
set h to regex_replace(h, "``", "")
|
|
319
346
|
-- espacios: colapsar (los <pre> están a salvo en sus marcadores)
|
|
320
|
-
set h to
|
|
321
|
-
set h to
|
|
322
|
-
set h to
|
|
323
|
-
set h to
|
|
324
|
-
set h to
|
|
347
|
+
set h to replace(h, cp_text(160), " ")
|
|
348
|
+
set h to regex_replace(h, "[ \\t\\f\\v]+", " ")
|
|
349
|
+
set h to regex_replace(h, "(?m)^ +", "")
|
|
350
|
+
set h to regex_replace(h, "(?m) +$", "")
|
|
351
|
+
set h to regex_replace(h, "\\n{3,}", "\n\n")
|
|
325
352
|
set h to quote_lines(h)
|
|
326
353
|
set h to push_pre(h, pre["blocks"])
|
|
327
|
-
set h to
|
|
354
|
+
set h to regex_replace(h, "\\n{3,}", "\n\n")
|
|
328
355
|
give trim(h)
|
|
329
356
|
|
|
330
357
|
-- Markdown servido por el sitio: solo sacar imágenes base64 (hermes convert_base64_images_to_links)
|
|
331
358
|
export task clean_markdown(md)
|
|
332
|
-
let out be
|
|
333
|
-
set out to
|
|
334
|
-
give trim(
|
|
359
|
+
let out be regex_replace(text(md), "!\\[([^\\]]*)\\]\\(\\s*data:image/[^)]*\\)", "[image: \\1]")
|
|
360
|
+
set out to regex_replace(out, "data:image/[a-z+]+;base64,[A-Za-z0-9+/=]{40,}", "[image]")
|
|
361
|
+
give trim(replace(out, "\r\n", "\n"))
|
|
335
362
|
|
|
336
363
|
-- href del <link rel="alternate" type="text/markdown">, resuelto; nothing si no hay
|
|
337
364
|
export task markdown_alternate(html, base)
|
|
338
|
-
each l in
|
|
365
|
+
each l in regex_find_all(text(html), "(?is)<link\\b[^>]*>")
|
|
339
366
|
let ll be lower(l)
|
|
340
367
|
when contains(ll, "alternate") and (contains(ll, "type=\"text/markdown\"") or contains(ll, "type='text/markdown'") or contains(ll, "type=\"text/x-markdown\""))
|
|
341
|
-
let href be
|
|
368
|
+
let href be regex_capture(l, "(?is)\\shref\\s*=\\s*[\"']([^\"']*)[\"']")
|
|
342
369
|
when href != nothing
|
|
343
370
|
give u.resolve(base, href[0])
|
|
344
371
|
give nothing
|
|
@@ -563,7 +590,7 @@ export task tool(url, format, max_chars, timeout)
|
|
|
563
590
|
|
|
564
591
|
export let SPEC be {
|
|
565
592
|
"name": "fetch",
|
|
566
|
-
"description": "Fetch a web page or HTTP API response and return it as Markdown (default), plain text or raw HTML — scripts, styles, navigation and footers stripped, entities decoded, links made absolute, code blocks and tables kept. Use it instead of bash+curl whenever you will READ the result (docs, READMEs, issues, changelogs, API JSON): it asks the server for Markdown first and returns about a tenth of the characters of raw HTML. Follows up to 5 redirects. Pages longer than max_chars come back as head+tail with the full text saved to a file you can page with read (offset/limit) or grep — never fetch the same URL twice in a turn; repeats within 20 minutes are served from that file anyway. Binary content (images, PDF, archives) is reported, not returned. Private/loopback hosts (localhost, 10.x, 192.168.x, *.internal) ask the user first — fine for checking your own dev server; cloud metadata endpoints are always refused. Cite the URL when you use its content.",
|
|
593
|
+
"description": "Fetch a web page or HTTP API response and return it as Markdown (default), plain text or raw HTML — scripts, styles, navigation and footers stripped, entities decoded, links made absolute, code blocks and tables kept. Use it instead of bash+curl whenever you will READ the result (docs, READMEs, issues, changelogs, API JSON): it asks the server for Markdown first and returns about a tenth of the characters of raw HTML. Follows up to 5 redirects. Pages longer than max_chars come back as head+tail with the full text saved to a file you can page with read (offset/limit) or grep — never fetch the same URL twice in a turn; repeats within 20 minutes are served from that file anyway. Binary content (images, PDF, archives) is reported, not returned. Private/loopback hosts (localhost, 10.x, 192.168.x, *.internal) ask the user first — fine for checking your own dev server; cloud metadata endpoints are always refused. Cite the URL when you use its content. To SEARCH the web (no API key, no browser): fetch https://html.duckduckgo.com/html/?q=<url-encoded query> — the results come back as Markdown with the real target links; refine the query as needed, then fetch the page you chose.",
|
|
567
594
|
"parameters": {"type": "object", "properties": {
|
|
568
595
|
"url": {"type": "string", "description": "Absolute http(s) URL"},
|
|
569
596
|
"format": {"type": "string", "enum": ["markdown", "text", "html"], "description": "markdown (default): HTML converted to Markdown, or the site's own Markdown when it offers one; text: plain text, no links or markup; html: the raw HTML (to see structure, selectors, meta tags)"},
|
package/lib/tools/grep.syn
CHANGED
|
@@ -11,7 +11,7 @@ export task tool(pattern, path, glob, regex)
|
|
|
11
11
|
let r be grep(c.ws(path), pattern, opts)
|
|
12
12
|
let lines be []
|
|
13
13
|
each m in r["matches"]
|
|
14
|
-
set lines to append(lines, c.unws(
|
|
14
|
+
set lines to append(lines, c.unws(replace(m["file"], "\\", "/")) + ":" + text(m["line"]) + ": " + trim(m["text"]))
|
|
15
15
|
when length(lines) == 0
|
|
16
16
|
give "no matches"
|
|
17
17
|
let out be join(lines, "\n")
|
package/lib/tools/memo.syn
CHANGED
|
@@ -33,10 +33,10 @@ task valid_name(name)
|
|
|
33
33
|
export task slug()
|
|
34
34
|
require env("LAMPSON_*")
|
|
35
35
|
let ws be env("LAMPSON_WORKSPACE", "workspace")
|
|
36
|
-
let norm be
|
|
36
|
+
let norm be replace(lower(ws), "\\", "/")
|
|
37
37
|
let parts be where(split(norm, "/"), (x) => x != "")
|
|
38
38
|
let base be when length(parts) > 0 then parts[length(parts) - 1] otherwise "workspace"
|
|
39
|
-
let runs be
|
|
39
|
+
let runs be regex_find_all(base, "[a-z0-9]+")
|
|
40
40
|
let clean be when length(runs) == 0 then "project" otherwise join(runs, "-")
|
|
41
41
|
let h be slice(decode(sha256(norm), "hex"), 0, 8)
|
|
42
42
|
give clean + "-" + h
|
|
@@ -61,7 +61,7 @@ export task list()
|
|
|
61
61
|
let title be ""
|
|
62
62
|
each line in split(read_file(path(name)), "\n")
|
|
63
63
|
when title == "" and trim(line) != ""
|
|
64
|
-
set title to trim(
|
|
64
|
+
set title to trim(replace(line, "#", ""))
|
|
65
65
|
set out to append(out, {"name": name, "title": when length(title) > 120 then slice(title, 0, 120) otherwise title})
|
|
66
66
|
recover err
|
|
67
67
|
-- el runtime lanza "Not a directory: …" (mayúscula) cuando memory/<slug> aún no existe
|