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/tools/proc.syn
CHANGED
|
@@ -38,7 +38,7 @@ task path(name, ext)
|
|
|
38
38
|
give DIR + "/" + name + "." + ext
|
|
39
39
|
|
|
40
40
|
task tail_lines(content, n)
|
|
41
|
-
let lines be split(
|
|
41
|
+
let lines be split(replace(content, "\r", ""), "\n")
|
|
42
42
|
when length(lines) > n
|
|
43
43
|
set lines to slice(lines, length(lines) - n, length(lines))
|
|
44
44
|
give join(lines, "\n")
|
|
@@ -211,7 +211,7 @@ export task resume_note(sid)
|
|
|
211
211
|
give "[harness] This session was resumed after a lampson restart. Managed processes do NOT survive a restart — anything this conversation started earlier is gone unless listed as running below. Current real state:\n" + join(lines, "\n") + "\nOld logs remain in " + DIR + "/<name>.log. Start a process again with the `process` tool only if it is needed now; do not assume any server is up."
|
|
212
212
|
|
|
213
213
|
task first_line_of(s)
|
|
214
|
-
let t be
|
|
214
|
+
let t be replace(text(s), "\r", "")
|
|
215
215
|
give split(t, "\n")[0]
|
|
216
216
|
|
|
217
217
|
-- {run, session} actuales (blackboard "lampson:run" / "lampson:session"; vacíos fuera de un entry)
|
|
@@ -246,43 +246,61 @@ export task list()
|
|
|
246
246
|
give []
|
|
247
247
|
give out
|
|
248
248
|
|
|
249
|
-
--
|
|
250
|
-
-- (Windows: netstat+tasklist · Linux: ss · macOS: lsof)
|
|
251
|
-
|
|
249
|
+
-- Todos los puertos TCP en escucha con su dueño, en crudo: [{port, pid, name}] — UNA llamada a proc.sh ports
|
|
250
|
+
-- (Windows: netstat+tasklist · Linux: ss · macOS: lsof), sin filtrar y sin la línea de comando. Para encontrar y
|
|
251
|
+
-- parar los procesos de Lampson por su puerto (lib/workspaces.syn): el edge de --domain escucha en :443.
|
|
252
|
+
export task port_owners()
|
|
252
253
|
require exec
|
|
253
254
|
require env("LAMPSON_*")
|
|
254
255
|
require env("OS")
|
|
255
|
-
require file(".lampson")
|
|
256
|
-
require file(".lampson/*")
|
|
257
256
|
let out be []
|
|
258
257
|
try
|
|
259
258
|
let r be run(shell(), ["lib/tools/proc.sh", "ports"], 20, {"cwd": "."})
|
|
260
|
-
|
|
261
|
-
each line in split(replace_text(r["stdout"], "\r", ""), "\n")
|
|
259
|
+
each line in split(replace(r["stdout"], "\r", ""), "\n")
|
|
262
260
|
let parts be where(split(trim(line), " "), (x) => x != "")
|
|
263
|
-
when length(parts) >= 2
|
|
264
|
-
|
|
265
|
-
let nm be when length(parts) >= 3 then parts[2] otherwise "?"
|
|
266
|
-
when port >= 1024 and port < 49152 and not contains(SYSTEM_PROCS, lower(nm)) and not contains(seen, text(port))
|
|
267
|
-
set seen[text(port)] to true
|
|
268
|
-
set out to append(out, {"port": port, "pid": floor(number(parts[1])), "name": nm, "command": ""})
|
|
261
|
+
when length(parts) >= 2 and matches(parts[0], "[0-9]+") and matches(parts[1], "[0-9]+")
|
|
262
|
+
set out to append(out, {"port": floor(number(parts[0])), "pid": floor(number(parts[1])), "name": when length(parts) >= 3 then parts[2] otherwise "?"})
|
|
269
263
|
recover err
|
|
270
264
|
give out
|
|
265
|
+
give out
|
|
266
|
+
|
|
267
|
+
-- Puertos TCP en escucha que le interesan al usuario y al agente (panel «Puertos», tool process): sin los del
|
|
268
|
+
-- sistema ni los efímeros, uno por puerto, con la línea de comando: [{port, pid, name, command}]. Un servidor
|
|
269
|
+
-- huérfano no aparece en list(); acá sí.
|
|
270
|
+
export task listeners()
|
|
271
|
+
require exec
|
|
272
|
+
require env("LAMPSON_*")
|
|
273
|
+
require env("OS")
|
|
274
|
+
require file(".lampson")
|
|
275
|
+
require file(".lampson/*")
|
|
276
|
+
let out be []
|
|
277
|
+
let seen be {}
|
|
278
|
+
each l in port_owners()
|
|
279
|
+
when l["port"] >= 1024 and l["port"] < 49152 and not contains(SYSTEM_PROCS, lower(l["name"])) and not contains(seen, text(l["port"]))
|
|
280
|
+
set seen[text(l["port"])] to true
|
|
281
|
+
set out to append(out, {"port": l["port"], "pid": l["pid"], "name": l["name"], "command": ""})
|
|
271
282
|
-- la línea de comando dice QUÉ es (next dev, un MCP server de otra herramienta, synsema serve…); un run por pid
|
|
272
283
|
let cmds be {}
|
|
273
284
|
let enriched be []
|
|
274
285
|
each l in out
|
|
275
286
|
let key be text(l["pid"])
|
|
276
287
|
when not contains(cmds, key)
|
|
277
|
-
|
|
278
|
-
let r be run(shell(), ["lib/tools/proc.sh", "info", key], 15, {"cwd": "."})
|
|
279
|
-
set cmds[key] to trim(replace_text(r["stdout"], "\n", " "))
|
|
280
|
-
recover err
|
|
281
|
-
set cmds[key] to ""
|
|
288
|
+
set cmds[key] to command_of(l["pid"])
|
|
282
289
|
set l["command"] to when length(cmds[key]) > 300 then slice(cmds[key], 0, 300) + "…" otherwise cmds[key]
|
|
283
290
|
set enriched to append(enriched, l)
|
|
284
291
|
give sort_by(enriched, (x) => x["port"])
|
|
285
292
|
|
|
293
|
+
-- la línea de comando de un pid ("" si no se puede saber) — via proc.sh info
|
|
294
|
+
export task command_of(pid)
|
|
295
|
+
require exec
|
|
296
|
+
require env("LAMPSON_*")
|
|
297
|
+
require env("OS")
|
|
298
|
+
try
|
|
299
|
+
let r be run(shell(), ["lib/tools/proc.sh", "info", text(floor(pid))], 15, {"cwd": "."})
|
|
300
|
+
give trim(replace(r["stdout"], "\n", " "))
|
|
301
|
+
recover err
|
|
302
|
+
give ""
|
|
303
|
+
|
|
286
304
|
-- matar un pid ajeno (huérfano de otra herramienta): taskkill /T o kill_tree, via proc.sh
|
|
287
305
|
export task kill_pid(pid)
|
|
288
306
|
require exec
|
package/lib/tools/qr.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// qr.js <texto> — dibuja un QR en la terminal (lampson --pair): medio bloque por fila de módulos (▀ ▄ █), con
|
|
2
|
+
// colores FORZADOS (negro sobre blanco) para que la cámara lo lea igual con tema claro u oscuro. Usa la misma
|
|
3
|
+
// librería que el QR de la web (public/vendor/qrcode.js).
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const qrcode = require(path.join(__dirname, '..', '..', 'public', 'vendor', 'qrcode.js'));
|
|
6
|
+
|
|
7
|
+
const text = process.argv[2];
|
|
8
|
+
if (!text) { console.error('uso: node qr.js <texto>'); process.exit(2); }
|
|
9
|
+
const qr = qrcode(0, 'M');
|
|
10
|
+
qr.addData(text);
|
|
11
|
+
qr.make();
|
|
12
|
+
|
|
13
|
+
const n = qr.getModuleCount();
|
|
14
|
+
const quiet = 3; // margen claro alrededor (la cámara lo necesita para encontrar el código)
|
|
15
|
+
const dark = (r, c) => r >= 0 && c >= 0 && r < n && c < n && qr.isDark(r, c);
|
|
16
|
+
const ON = '\x1b[30;107m', OFF = '\x1b[0m'; // texto negro, fondo blanco brillante
|
|
17
|
+
const lines = [];
|
|
18
|
+
for (let r = -quiet; r < n + quiet; r += 2) {
|
|
19
|
+
let line = ' ' + ON;
|
|
20
|
+
for (let c = -quiet; c < n + quiet; c++) {
|
|
21
|
+
const top = dark(r, c), bottom = dark(r + 1, c);
|
|
22
|
+
line += top && bottom ? '█' : top ? '▀' : bottom ? '▄' : ' ';
|
|
23
|
+
}
|
|
24
|
+
lines.push(line + OFF);
|
|
25
|
+
}
|
|
26
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
-- lib/tools/url.syn — URLs para la tool fetch y para permission.syn (puro, sin capacidades)
|
|
2
|
+
--
|
|
3
|
+
-- parse(url) → {ok, error?, url, scheme, host, port, path, query, origin, dir}
|
|
4
|
+
-- host_class(host) → "public" | "private" | "blocked"
|
|
5
|
+
-- sensitive(url) → motivo (texto) si la URL lleva una credencial, o nothing
|
|
6
|
+
-- resolve(base, href) → URL absoluta de un href (relativo o no), o nothing si no es navegable (#, javascript:)
|
|
7
|
+
--
|
|
8
|
+
-- Política de hosts (tomada de hermes url_safety.py, sin resolver DNS: el runtime no expone un resolver):
|
|
9
|
+
-- * "blocked": endpoints de metadata de la nube (169.254.169.254 y familia, metadata.google.internal).
|
|
10
|
+
-- Nunca son un destino legítimo: se deniegan SIEMPRE, incluso en yolo.
|
|
11
|
+
-- * "private": loopback, redes privadas (10/8, 172.16/12, 192.168/16, CGNAT 100.64/10), link-local,
|
|
12
|
+
-- localhost y sufijos internos. Legítimo para mirar el propio dev server → PIDE aprobación (yolo permite,
|
|
13
|
+
-- strict deniega). Un host numérico ambiguo (entero, hex, octal) cae aquí también.
|
|
14
|
+
-- * "public": el resto. Límite conocido: sin DNS no se detecta un nombre público que resuelve a una IP
|
|
15
|
+
-- privada (DNS rebinding); hermes lo cubre con un resolver, aquí queda documentado.
|
|
16
|
+
|
|
17
|
+
let SENSITIVE_PARAMS be ["access_token", "api_key", "apikey", "auth_token", "authorization", "awsaccesskeyid", "client_secret", "credential", "credentials", "jwt", "password", "passwd", "secret", "session_id", "signature", "token", "private_key", "x_amz_security_token", "x_amz_signature", "x-amz-security-token", "x-amz-signature"]
|
|
18
|
+
let SECRET_RE be "(?i)(\\bsk-(ant-|proj-)?[a-z0-9_-]{24,}|\\bghp_[a-z0-9]{20,}|\\bgithub_pat_[a-z0-9_]{20,}|\\bgho_[a-z0-9]{20,}|\\bglpat-[a-z0-9_-]{16,}|\\bxox[abprs]-[a-z0-9-]{10,}|\\bakia[0-9a-z]{16}\\b|\\baiza[0-9a-z_-]{30,}|-----begin [a-z ]*private key|\\beyj[a-z0-9_-]{20,}\\.eyj[a-z0-9_-]{20,})"
|
|
19
|
+
|
|
20
|
+
task fail(msg)
|
|
21
|
+
give {"ok": false, "error": msg}
|
|
22
|
+
|
|
23
|
+
export task parse(raw)
|
|
24
|
+
when raw == nothing
|
|
25
|
+
give fail("missing URL")
|
|
26
|
+
let u be trim(text(raw))
|
|
27
|
+
-- "https:// host" — el modelo a veces mete un espacio tras el esquema (hermes normalize_url_for_request)
|
|
28
|
+
set u to regex_replace(u, "^([A-Za-z][A-Za-z0-9+.-]*://)\\s+", "\\1")
|
|
29
|
+
when u == ""
|
|
30
|
+
give fail("missing URL")
|
|
31
|
+
when length(u) > 2048
|
|
32
|
+
give fail("URL longer than 2048 chars")
|
|
33
|
+
when contains(u, " ") or contains(u, "\n") or contains(u, "\t")
|
|
34
|
+
give fail("URL contains whitespace: " + u)
|
|
35
|
+
let sp be split(u, "://")
|
|
36
|
+
when length(sp) < 2
|
|
37
|
+
give fail("not an absolute http(s) URL: " + u)
|
|
38
|
+
let scheme be lower(sp[0])
|
|
39
|
+
when scheme != "http" and scheme != "https"
|
|
40
|
+
give fail("unsupported scheme '" + scheme + "' (only http and https)")
|
|
41
|
+
let rest be join(slice(sp, 1, length(sp)), "://")
|
|
42
|
+
-- fragmento y query
|
|
43
|
+
let frag_sp be split(rest, "#")
|
|
44
|
+
set rest to frag_sp[0]
|
|
45
|
+
let q_sp be split(rest, "?")
|
|
46
|
+
let query be when length(q_sp) > 1 then join(slice(q_sp, 1, length(q_sp)), "?") otherwise ""
|
|
47
|
+
set rest to q_sp[0]
|
|
48
|
+
-- autoridad y path
|
|
49
|
+
let slash_sp be split(rest, "/")
|
|
50
|
+
let authority be slash_sp[0]
|
|
51
|
+
let path be when length(slash_sp) > 1 then "/" + join(slice(slash_sp, 1, length(slash_sp)), "/") otherwise "/"
|
|
52
|
+
when contains(authority, "@")
|
|
53
|
+
give fail("credentials in the URL are not allowed")
|
|
54
|
+
when authority == ""
|
|
55
|
+
give fail("missing host: " + u)
|
|
56
|
+
let host be lower(authority)
|
|
57
|
+
let port be ""
|
|
58
|
+
when starts_with(host, "[")
|
|
59
|
+
-- IPv6 literal: [::1]:8080
|
|
60
|
+
let close be split(host, "]")
|
|
61
|
+
set host to slice(close[0], 1, length(close[0]))
|
|
62
|
+
when length(close) > 1 and starts_with(close[1], ":")
|
|
63
|
+
set port to slice(close[1], 1, length(close[1]))
|
|
64
|
+
otherwise
|
|
65
|
+
let hp be split(host, ":")
|
|
66
|
+
when length(hp) > 2
|
|
67
|
+
give fail("malformed host: " + authority)
|
|
68
|
+
set host to hp[0]
|
|
69
|
+
when length(hp) == 2
|
|
70
|
+
set port to hp[1]
|
|
71
|
+
when port != "" and not matches(port, "[0-9]{1,5}")
|
|
72
|
+
give fail("malformed port: " + port)
|
|
73
|
+
-- "example.com." (FQDN con punto final) ≡ "example.com"
|
|
74
|
+
while ends_with(host, ".")
|
|
75
|
+
set host to slice(host, 0, length(host) - 1)
|
|
76
|
+
when host == ""
|
|
77
|
+
give fail("missing host: " + u)
|
|
78
|
+
let origin be scheme + "://" + host + (when port != "" then ":" + port otherwise "")
|
|
79
|
+
let dir_parts be split(path, "/")
|
|
80
|
+
let dir be join(slice(dir_parts, 0, length(dir_parts) - 1), "/") + "/"
|
|
81
|
+
give {"ok": true, "url": origin + path + (when query != "" then "?" + query otherwise ""), "scheme": scheme, "host": host, "port": port, "path": path, "query": query, "origin": origin, "dir": dir}
|
|
82
|
+
|
|
83
|
+
-- octetos de una IPv4 en notación decimal con puntos, o nothing
|
|
84
|
+
task ipv4_octets(host)
|
|
85
|
+
when not matches(host, "[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}")
|
|
86
|
+
give nothing
|
|
87
|
+
let out be []
|
|
88
|
+
each p in split(host, ".")
|
|
89
|
+
let n be floor(number(p))
|
|
90
|
+
when n > 255
|
|
91
|
+
give nothing
|
|
92
|
+
set out to append(out, n)
|
|
93
|
+
give out
|
|
94
|
+
|
|
95
|
+
task ipv4_class(o)
|
|
96
|
+
let a be o[0]
|
|
97
|
+
let b be o[1]
|
|
98
|
+
when a == 169 and b == 254
|
|
99
|
+
give "blocked"
|
|
100
|
+
when a == 100 and b == 100 and o[2] == 100 and o[3] == 200
|
|
101
|
+
give "blocked"
|
|
102
|
+
when a == 127 or a == 10 or a == 0
|
|
103
|
+
give "private"
|
|
104
|
+
when a == 192 and b == 168
|
|
105
|
+
give "private"
|
|
106
|
+
when a == 172 and b >= 16 and b <= 31
|
|
107
|
+
give "private"
|
|
108
|
+
when a == 100 and b >= 64 and b <= 127
|
|
109
|
+
give "private"
|
|
110
|
+
when a >= 224
|
|
111
|
+
give "private"
|
|
112
|
+
give "public"
|
|
113
|
+
|
|
114
|
+
export task host_class(host)
|
|
115
|
+
let h be lower(text(host))
|
|
116
|
+
when h == "metadata.google.internal" or h == "metadata.goog" or h == "fd00:ec2::254"
|
|
117
|
+
give "blocked"
|
|
118
|
+
let o be ipv4_octets(h)
|
|
119
|
+
when o != nothing
|
|
120
|
+
give ipv4_class(o)
|
|
121
|
+
-- IPv6: loopback, unspecified, ULA fc00::/7, link-local fe80::/10, IPv4 mapeadas ::ffff:a.b.c.d
|
|
122
|
+
when contains(h, ":")
|
|
123
|
+
when h == "::1" or h == "::"
|
|
124
|
+
give "private"
|
|
125
|
+
when starts_with(h, "::ffff:")
|
|
126
|
+
let inner be ipv4_octets(slice(h, 7, length(h)))
|
|
127
|
+
give when inner != nothing then ipv4_class(inner) otherwise "private"
|
|
128
|
+
when starts_with(h, "fc") or starts_with(h, "fd") or starts_with(h, "fe8") or starts_with(h, "fe9") or starts_with(h, "fea") or starts_with(h, "feb")
|
|
129
|
+
give "private"
|
|
130
|
+
give "public"
|
|
131
|
+
when h == "localhost" or ends_with(h, ".localhost") or ends_with(h, ".internal") or ends_with(h, ".local") or ends_with(h, ".home.arpa") or ends_with(h, ".lan")
|
|
132
|
+
give "private"
|
|
133
|
+
-- formas numéricas ambiguas (2130706433, 0x7f000001, 0177.0.0.1): clásicos de bypass → tratar como privado
|
|
134
|
+
when matches(h, "[0-9]+") or matches(h, "0x[0-9a-f]+") or matches(h, "[0-9x.]+")
|
|
135
|
+
give "private"
|
|
136
|
+
give "public"
|
|
137
|
+
|
|
138
|
+
-- %XX → carácter (para inspeccionar una URL con secretos codificados)
|
|
139
|
+
task percent_decode(s)
|
|
140
|
+
let out be s
|
|
141
|
+
each m in regex_find_all(s, "%[0-9A-Fa-f]{2}")
|
|
142
|
+
let ch be nothing
|
|
143
|
+
try
|
|
144
|
+
set ch to json_decode("\"\\u00" + slice(m, 1, 3) + "\"")
|
|
145
|
+
recover e
|
|
146
|
+
set ch to nothing
|
|
147
|
+
when ch != nothing
|
|
148
|
+
set out to replace(out, m, ch)
|
|
149
|
+
give out
|
|
150
|
+
|
|
151
|
+
export task sensitive(raw)
|
|
152
|
+
when raw == nothing
|
|
153
|
+
give nothing
|
|
154
|
+
let u be text(raw)
|
|
155
|
+
let decoded be percent_decode(u)
|
|
156
|
+
when length(regex_find_all(u, SECRET_RE)) > 0 or length(regex_find_all(decoded, SECRET_RE)) > 0
|
|
157
|
+
give "an API key or token"
|
|
158
|
+
let p be parse(u)
|
|
159
|
+
when not p["ok"]
|
|
160
|
+
give nothing
|
|
161
|
+
when p["query"] == ""
|
|
162
|
+
give nothing
|
|
163
|
+
each pair in split(p["query"], "&")
|
|
164
|
+
let kv be split(pair, "=")
|
|
165
|
+
let k be lower(trim(percent_decode(kv[0])))
|
|
166
|
+
let v be when length(kv) > 1 then join(slice(kv, 1, length(kv)), "=") otherwise ""
|
|
167
|
+
when v != "" and contains(SENSITIVE_PARAMS, k)
|
|
168
|
+
give "a credential-like query parameter (" + kv[0] + ")"
|
|
169
|
+
give nothing
|
|
170
|
+
|
|
171
|
+
-- "/a/b/../c/./d" → "/a/c/d"
|
|
172
|
+
task normalize_path(path)
|
|
173
|
+
let out be []
|
|
174
|
+
each seg in split(path, "/")
|
|
175
|
+
when seg == ".."
|
|
176
|
+
when length(out) > 0
|
|
177
|
+
set out to slice(out, 0, length(out) - 1)
|
|
178
|
+
otherwise when seg != "." and seg != ""
|
|
179
|
+
set out to append(out, seg)
|
|
180
|
+
give "/" + join(out, "/") + (when ends_with(path, "/") and length(out) > 0 then "/" otherwise "")
|
|
181
|
+
|
|
182
|
+
export task resolve(base, href)
|
|
183
|
+
when href == nothing
|
|
184
|
+
give nothing
|
|
185
|
+
let h be trim(text(href))
|
|
186
|
+
let lh be lower(h)
|
|
187
|
+
when h == "" or starts_with(h, "#") or starts_with(lh, "javascript:") or starts_with(lh, "data:") or starts_with(lh, "mailto:") or starts_with(lh, "tel:")
|
|
188
|
+
give nothing
|
|
189
|
+
when matches(h, "[A-Za-z][A-Za-z0-9+.-]*:.*")
|
|
190
|
+
give h
|
|
191
|
+
let b be parse(base)
|
|
192
|
+
when not b["ok"]
|
|
193
|
+
give h
|
|
194
|
+
when starts_with(h, "//")
|
|
195
|
+
give b["scheme"] + ":" + h
|
|
196
|
+
-- separar query/fragmento del path relativo para normalizar solo el path
|
|
197
|
+
let tail be ""
|
|
198
|
+
let core be h
|
|
199
|
+
let qi be split(h, "?")
|
|
200
|
+
let fi be split(h, "#")
|
|
201
|
+
when length(qi) > 1
|
|
202
|
+
set core to qi[0]
|
|
203
|
+
set tail to "?" + join(slice(qi, 1, length(qi)), "?")
|
|
204
|
+
otherwise when length(fi) > 1
|
|
205
|
+
set core to fi[0]
|
|
206
|
+
set tail to "#" + join(slice(fi, 1, length(fi)), "#")
|
|
207
|
+
when core == ""
|
|
208
|
+
give b["origin"] + b["path"] + tail
|
|
209
|
+
when starts_with(core, "/")
|
|
210
|
+
give b["origin"] + normalize_path(core) + tail
|
|
211
|
+
give b["origin"] + normalize_path(b["dir"] + core) + tail
|
|
212
|
+
|
|
213
|
+
-- nombre de archivo estable para la caché/spill de una URL: fetch-<host>-<digest>
|
|
214
|
+
export task slug(host)
|
|
215
|
+
let s be regex_replace(lower(text(host)), "[^a-z0-9._-]", "-")
|
|
216
|
+
when length(s) > 60
|
|
217
|
+
set s to slice(s, 0, 60)
|
|
218
|
+
when s == ""
|
|
219
|
+
give "page"
|
|
220
|
+
give s
|
package/lib/tools.syn
CHANGED
|
@@ -19,6 +19,7 @@ use "./tools/ls.syn" as t_ls
|
|
|
19
19
|
use "./tools/find.syn" as t_find
|
|
20
20
|
use "./tools/grep.syn" as t_grep
|
|
21
21
|
use "./tools/bash.syn" as t_bash
|
|
22
|
+
use "./tools/fetch.syn" as t_fetch
|
|
22
23
|
use "./tools/skill.syn" as t_skill
|
|
23
24
|
use "./tools/process.syn" as t_process
|
|
24
25
|
use "./tools/memo.syn" as t_memo
|
|
@@ -28,6 +29,26 @@ use "./mcp.syn" as mcp
|
|
|
28
29
|
use "./plugins.syn" as plugins
|
|
29
30
|
use "./lsp.syn" as lsp
|
|
30
31
|
use "./schedule.syn" as schedule
|
|
32
|
+
use "./computer.syn" as computer
|
|
33
|
+
|
|
34
|
+
-- la tool computer_use (lib/computer.syn): el escritorio y el navegador del usuario en segundo plano, sobre un
|
|
35
|
+
-- driver externo. Está en el registry siempre, pero al catálogo del modelo solo entra cuando está encendida y el
|
|
36
|
+
-- driver existe (agents.tool_names). Los args llegan planos del JSON del modelo y van a computer.call como map.
|
|
37
|
+
-- OJO: cada propiedad del SPEC tiene que ser un parámetro acá — un arg que no está da "unknown parameter" antes de
|
|
38
|
+
-- llegar a computer.call (2026-09-06: delivery_mode faltaba y el modelo no pudo apretar Enter en Chrome). Lo cubre
|
|
39
|
+
-- un test de unit.test.syn que recorre las propiedades del SPEC.
|
|
40
|
+
task computer_tool(action, pid, window_id, element, x, y, text, key, keys, modifiers, direction, amount, from_x, from_y, to_x, to_y, x1, y1, x2, y2, value, name, path, url, target_id, tab_id, ref, query, screenshot, max_elements, replace, delta_y, delta_x, profile, seconds, button, delivery_mode, option, app, capture_after, fields, goal, steps, enter)
|
|
41
|
+
require exec
|
|
42
|
+
require time
|
|
43
|
+
require env("LAMPSON_*")
|
|
44
|
+
require env("OS")
|
|
45
|
+
require file.read(".lampson")
|
|
46
|
+
require file.read(".lampson/*")
|
|
47
|
+
let a be {}
|
|
48
|
+
each e in [["pid", pid], ["window_id", window_id], ["element", element], ["x", x], ["y", y], ["text", text], ["key", key], ["keys", keys], ["modifiers", modifiers], ["direction", direction], ["amount", amount], ["from_x", from_x], ["from_y", from_y], ["to_x", to_x], ["to_y", to_y], ["x1", x1], ["y1", y1], ["x2", x2], ["y2", y2], ["value", value], ["name", name], ["path", path], ["url", url], ["target_id", target_id], ["tab_id", tab_id], ["ref", ref], ["query", query], ["screenshot", screenshot], ["max_elements", max_elements], ["replace", replace], ["delta_y", delta_y], ["delta_x", delta_x], ["profile", profile], ["seconds", seconds], ["button", button], ["delivery_mode", delivery_mode], ["option", option], ["app", app], ["capture_after", capture_after], ["fields", fields], ["goal", goal], ["steps", steps], ["enter", enter]]
|
|
49
|
+
when e[1] != nothing
|
|
50
|
+
set a[e[0]] to e[1]
|
|
51
|
+
give computer.call(action, a)
|
|
31
52
|
|
|
32
53
|
-- la task de la tool skill (su SPEC está en tools/skill.syn)
|
|
33
54
|
task skill_tool(name, action, source, scope)
|
|
@@ -256,6 +277,7 @@ export task registry()
|
|
|
256
277
|
"find": t_find.tool,
|
|
257
278
|
"grep": t_grep.tool,
|
|
258
279
|
"bash": t_bash.tool,
|
|
280
|
+
"fetch": t_fetch.tool,
|
|
259
281
|
"process": t_process.tool,
|
|
260
282
|
"memory": t_memo.tool,
|
|
261
283
|
"todo": t_todo.tool,
|
|
@@ -263,10 +285,11 @@ export task registry()
|
|
|
263
285
|
"mcp": mcp_tool,
|
|
264
286
|
"plugin": plugin_tool,
|
|
265
287
|
"lsp": lsp_tool,
|
|
266
|
-
"schedule": schedule_tool
|
|
288
|
+
"schedule": schedule_tool,
|
|
289
|
+
"computer_use": computer_tool
|
|
267
290
|
}
|
|
268
291
|
|
|
269
|
-
export let CATALOG be [t_read.SPEC, t_write.SPEC, t_edit.SPEC, t_ls.SPEC, t_find.SPEC, t_grep.SPEC, LSP_SPEC, t_bash.SPEC, t_process.SPEC, t_memo.SPEC, t_todo.SPEC, t_skill.SPEC, MCP_SPEC, PLUGIN_SPEC, SCHEDULE_SPEC]
|
|
292
|
+
export let CATALOG be [t_read.SPEC, t_write.SPEC, t_edit.SPEC, t_ls.SPEC, t_find.SPEC, t_grep.SPEC, LSP_SPEC, t_fetch.SPEC, t_bash.SPEC, t_process.SPEC, t_memo.SPEC, t_todo.SPEC, t_skill.SPEC, MCP_SPEC, PLUGIN_SPEC, SCHEDULE_SPEC, computer.SPEC]
|
|
270
293
|
|
|
271
294
|
-- Subconjuntos (para perfiles de agente): registry/catálogo filtrados por nombre.
|
|
272
295
|
export task registry_subset(names)
|
package/lib/trace.syn
CHANGED
|
@@ -16,7 +16,7 @@ task path(sid)
|
|
|
16
16
|
give DIR + "/" + sid + ".log"
|
|
17
17
|
|
|
18
18
|
task one_line(s, max)
|
|
19
|
-
let t be
|
|
19
|
+
let t be replace(replace(text(s), "\r", ""), "\n", " ⏎ ")
|
|
20
20
|
when length(t) > max
|
|
21
21
|
give slice(t, 0, max) + "…"
|
|
22
22
|
give t
|
|
@@ -105,7 +105,7 @@ export task tail(sid, n)
|
|
|
105
105
|
require file(".lampson")
|
|
106
106
|
require file(".lampson/*")
|
|
107
107
|
try
|
|
108
|
-
let lines be split(
|
|
108
|
+
let lines be split(replace(read_file(path(sid)), "\r", ""), "\n")
|
|
109
109
|
when length(lines) > n
|
|
110
110
|
set lines to slice(lines, length(lines) - n, length(lines))
|
|
111
111
|
give join(lines, "\n")
|
package/lib/tree.syn
CHANGED
|
@@ -3,35 +3,38 @@ use "./tools/common.syn" as c
|
|
|
3
3
|
|
|
4
4
|
let MAX_ENTRIES be 2000
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
-- n = entradas contadas hasta ahora en todo el árbol; da {items, n} (listas y mapas son valores: el contador vuelve con give)
|
|
7
|
+
task walk(dir, depth, max_depth, n)
|
|
7
8
|
let out be []
|
|
8
9
|
when depth > max_depth
|
|
9
|
-
give out
|
|
10
|
+
give {"items": out, "n": n}
|
|
10
11
|
let entries be []
|
|
11
12
|
try
|
|
12
13
|
set entries to list_dir(dir)
|
|
13
14
|
recover err
|
|
14
|
-
give out
|
|
15
|
+
give {"items": out, "n": n}
|
|
15
16
|
each e in entries
|
|
16
|
-
when
|
|
17
|
-
give out
|
|
17
|
+
when n >= MAX_ENTRIES
|
|
18
|
+
give {"items": out, "n": n}
|
|
18
19
|
let rel be c.unws(dir + "/" + e["name"])
|
|
19
20
|
when e["is_dir"]
|
|
20
21
|
when not contains(c.IGNORED_DIRS, e["name"])
|
|
21
|
-
set
|
|
22
|
-
|
|
22
|
+
set n to n + 1
|
|
23
|
+
let sub be walk(dir + "/" + e["name"], depth + 1, max_depth, n)
|
|
24
|
+
set n to sub["n"]
|
|
25
|
+
set out to append(out, {"name": e["name"], "path": rel, "is_dir": true, "children": sub["items"]})
|
|
23
26
|
otherwise
|
|
24
|
-
set
|
|
27
|
+
set n to n + 1
|
|
25
28
|
set out to append(out, {"name": e["name"], "path": rel, "is_dir": false, "size": e["size"]})
|
|
26
29
|
-- carpetas primero
|
|
27
|
-
give where(out, (x) => x["is_dir"]) + where(out, (x) => not x["is_dir"])
|
|
30
|
+
give {"items": where(out, (x) => x["is_dir"]) + where(out, (x) => not x["is_dir"]), "n": n}
|
|
28
31
|
|
|
29
32
|
-- árbol anidado: [{name, path, is_dir, size?, children?}]
|
|
30
33
|
export task tree(max_depth)
|
|
31
34
|
require file.read("workspace")
|
|
32
35
|
require file.read("workspace/*")
|
|
33
|
-
let
|
|
34
|
-
give {"entries":
|
|
36
|
+
let w be walk(c.ROOT, 0, max_depth, 0)
|
|
37
|
+
give {"entries": w["items"], "count": w["n"], "truncated": w["n"] >= MAX_ENTRIES}
|
|
35
38
|
|
|
36
39
|
-- render de texto para la terminal (2 niveles, compacto)
|
|
37
40
|
export task render(max_depth, max_lines)
|
|
@@ -55,5 +58,18 @@ export task file_content(path)
|
|
|
55
58
|
require file.read("workspace")
|
|
56
59
|
require file.read("workspace/*")
|
|
57
60
|
let real be c.ws(path)
|
|
58
|
-
let
|
|
59
|
-
|
|
61
|
+
let rel be c.unws(real)
|
|
62
|
+
let info be file_info(real)
|
|
63
|
+
when not info["exists"]
|
|
64
|
+
raise("\"" + rel + "\" does not exist")
|
|
65
|
+
when info["is_dir"]
|
|
66
|
+
raise("\"" + rel + "\" is a folder")
|
|
67
|
+
-- binarios (sqlite, imágenes, fuentes…): read_file no puede decodificarlos como UTF-8 y el runtime lo reporta
|
|
68
|
+
-- como "File not found" (engañoso; visto 2026-09-05 con un dev.db de Prisma). En vez de un error, {binary: true}
|
|
69
|
+
-- y el visor muestra su aviso, sin ensuciar el chat.
|
|
70
|
+
try
|
|
71
|
+
let content be read_file(real)
|
|
72
|
+
-- truncated: el editor de la web lo abre en solo lectura (guardar el recorte destruiría el archivo)
|
|
73
|
+
give {"path": rel, "content": c.truncate(content, 200000), "lines": length(split(content, "\n")), "truncated": length(content) > 200000, "binary": false, "size": info["size"]}
|
|
74
|
+
recover err
|
|
75
|
+
give {"path": rel, "content": "", "lines": 0, "truncated": false, "binary": true, "size": info["size"], "error": text(err)}
|
package/lib/update.syn
CHANGED