lampson 0.2.8 → 0.2.10
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/mobile.css +95 -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 +16 -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/mobile.js +122 -0
- package/public/js/procs.js +1 -1
- package/public/js/pwa.js +5 -0
- package/public/js/terminal.js +1 -1
- 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/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');
|
package/lib/tools/url.syn
CHANGED
|
@@ -25,7 +25,7 @@ export task parse(raw)
|
|
|
25
25
|
give fail("missing URL")
|
|
26
26
|
let u be trim(text(raw))
|
|
27
27
|
-- "https:// host" — el modelo a veces mete un espacio tras el esquema (hermes normalize_url_for_request)
|
|
28
|
-
set u to
|
|
28
|
+
set u to regex_replace(u, "^([A-Za-z][A-Za-z0-9+.-]*://)\\s+", "\\1")
|
|
29
29
|
when u == ""
|
|
30
30
|
give fail("missing URL")
|
|
31
31
|
when length(u) > 2048
|
|
@@ -138,14 +138,14 @@ export task host_class(host)
|
|
|
138
138
|
-- %XX → carácter (para inspeccionar una URL con secretos codificados)
|
|
139
139
|
task percent_decode(s)
|
|
140
140
|
let out be s
|
|
141
|
-
each m in
|
|
141
|
+
each m in regex_find_all(s, "%[0-9A-Fa-f]{2}")
|
|
142
142
|
let ch be nothing
|
|
143
143
|
try
|
|
144
144
|
set ch to json_decode("\"\\u00" + slice(m, 1, 3) + "\"")
|
|
145
145
|
recover e
|
|
146
146
|
set ch to nothing
|
|
147
147
|
when ch != nothing
|
|
148
|
-
set out to
|
|
148
|
+
set out to replace(out, m, ch)
|
|
149
149
|
give out
|
|
150
150
|
|
|
151
151
|
export task sensitive(raw)
|
|
@@ -153,7 +153,7 @@ export task sensitive(raw)
|
|
|
153
153
|
give nothing
|
|
154
154
|
let u be text(raw)
|
|
155
155
|
let decoded be percent_decode(u)
|
|
156
|
-
when length(
|
|
156
|
+
when length(regex_find_all(u, SECRET_RE)) > 0 or length(regex_find_all(decoded, SECRET_RE)) > 0
|
|
157
157
|
give "an API key or token"
|
|
158
158
|
let p be parse(u)
|
|
159
159
|
when not p["ok"]
|
|
@@ -212,7 +212,7 @@ export task resolve(base, href)
|
|
|
212
212
|
|
|
213
213
|
-- nombre de archivo estable para la caché/spill de una URL: fetch-<host>-<digest>
|
|
214
214
|
export task slug(host)
|
|
215
|
-
let s be
|
|
215
|
+
let s be regex_replace(lower(text(host)), "[^a-z0-9._-]", "-")
|
|
216
216
|
when length(s) > 60
|
|
217
217
|
set s to slice(s, 0, 60)
|
|
218
218
|
when s == ""
|
package/lib/tools.syn
CHANGED
|
@@ -29,6 +29,26 @@ use "./mcp.syn" as mcp
|
|
|
29
29
|
use "./plugins.syn" as plugins
|
|
30
30
|
use "./lsp.syn" as lsp
|
|
31
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)
|
|
32
52
|
|
|
33
53
|
-- la task de la tool skill (su SPEC está en tools/skill.syn)
|
|
34
54
|
task skill_tool(name, action, source, scope)
|
|
@@ -265,10 +285,11 @@ export task registry()
|
|
|
265
285
|
"mcp": mcp_tool,
|
|
266
286
|
"plugin": plugin_tool,
|
|
267
287
|
"lsp": lsp_tool,
|
|
268
|
-
"schedule": schedule_tool
|
|
288
|
+
"schedule": schedule_tool,
|
|
289
|
+
"computer_use": computer_tool
|
|
269
290
|
}
|
|
270
291
|
|
|
271
|
-
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]
|
|
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]
|
|
272
293
|
|
|
273
294
|
-- Subconjuntos (para perfiles de agente): registry/catálogo filtrados por nombre.
|
|
274
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