lampson 0.1.1 → 0.1.2
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/README.md +12 -0
- package/chat.syn +252 -22
- package/lib/diff.syn +142 -0
- package/lib/line.syn +365 -0
- package/lib/md.syn +171 -0
- package/lib/permission.syn +36 -1
- package/lib/tools/edit.syn +4 -1
- package/lib/tools/write.syn +4 -0
- package/package.json +2 -2
- package/skills/synsema/SKILL.md +15 -2
package/README.md
CHANGED
|
@@ -132,6 +132,18 @@ In the REPL, `/` lists every command (`/agent`, `/ask` `/yolo` `/strict`, `/mode
|
|
|
132
132
|
`/files`, `/procs`, `/logs <name>`, `/stop <name>`, `/kill <pid>`, `/skills`, `/sessions`,
|
|
133
133
|
`/resume <id>`, `/tokens`, `/flags`). `!cmd` runs a command yourself.
|
|
134
134
|
|
|
135
|
+
The prompt is a real line editor (Synsema ≥ 0.6.11, falls back to plain `read_line` without a TTY):
|
|
136
|
+
typing `/` opens the command menu (recent ones first, filtered as you type), `Tab` completes the
|
|
137
|
+
command and then its arguments (files for `/image`, sessions, providers, processes, lamps, MCP/LSP
|
|
138
|
+
servers, flags), `↑↓` browse history or the menu, `Alt+Enter` inserts a newline, `Ctrl+O` shows the
|
|
139
|
+
last tool result in full, `Ctrl+U`/`Ctrl+W` clear the line/word, `Esc` closes the menu. Approvals are
|
|
140
|
+
an arrow-key menu (`permitir`/`denegar`, or `p`/`d`).
|
|
141
|
+
|
|
142
|
+
The terminal renders the model's markdown (headings, lists, tables, code fences) and shows every tool
|
|
143
|
+
result: `edit`/`write` print a line diff (`- red / + green`, line numbers, 2 lines of context); other
|
|
144
|
+
tools are collapsed to 15 lines. `/out [n]` prints the n-th last result of the turn in full and
|
|
145
|
+
`/verbose` toggles full output for every tool (saved in `.lampson/config.json`).
|
|
146
|
+
|
|
135
147
|
The project is mounted as `lampson/workspace` (an NTFS junction on Windows, a symlink elsewhere)
|
|
136
148
|
and every tool declares `file("workspace/*")` — that literal, named scope is what makes the
|
|
137
149
|
confinement real. Config, sessions and process logs live in the `lampson` folder, never in your project.
|
package/chat.syn
CHANGED
|
@@ -43,6 +43,9 @@ use "./lib/tools/memo.syn" as memo
|
|
|
43
43
|
use "./lib/update.syn" as update
|
|
44
44
|
use "./lib/settings.syn" as settings
|
|
45
45
|
use "./lib/trace.syn" as trace
|
|
46
|
+
use "./lib/md.syn" as md
|
|
47
|
+
use "./lib/diff.syn" as diff
|
|
48
|
+
use "./lib/line.syn" as ed
|
|
46
49
|
use "./lib/mcp.syn" as mcp
|
|
47
50
|
use "./lib/lamps.syn" as lamps
|
|
48
51
|
use "./lib/lsp.syn" as lsp
|
|
@@ -53,6 +56,12 @@ use "./lib/tools/todo.syn" as todo
|
|
|
53
56
|
|
|
54
57
|
let ESC be decode(bytes("1b", "hex"))
|
|
55
58
|
let COLOR be env("LAMPSON_NO_COLOR", "") == ""
|
|
59
|
+
-- /verbose: mostrar el output completo de cada tool (persistido en .lampson/config.json); si no, se
|
|
60
|
+
-- colapsa a OUT_LINES líneas y /out [n] muestra el resultado n-ésimo (desde el último) del turno completo
|
|
61
|
+
let saved_cfg be settings.load()
|
|
62
|
+
let VERBOSE be contains(saved_cfg, "verbose") and saved_cfg["verbose"] == true
|
|
63
|
+
let OUT_LINES be 15
|
|
64
|
+
let turn_outputs be []
|
|
56
65
|
|
|
57
66
|
task c(code, s)
|
|
58
67
|
when not COLOR
|
|
@@ -86,37 +95,83 @@ task summarize(name, out)
|
|
|
86
95
|
let lines be split(out, "\n")
|
|
87
96
|
when starts_with(out, "ERROR") or starts_with(out, "DENIED")
|
|
88
97
|
give first_line(out, 140)
|
|
89
|
-
when name == "read"
|
|
98
|
+
when name == "read" and not VERBOSE
|
|
90
99
|
give text(length(lines)) + " líneas"
|
|
91
|
-
when name == "ls" or name == "find"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
when (name == "ls" or name == "find") and not VERBOSE
|
|
101
|
+
-- las primeras entradas en una línea (hasta ~100 chars), y cuántas más hay
|
|
102
|
+
let shown be []
|
|
103
|
+
let used be 0
|
|
104
|
+
each l in lines
|
|
105
|
+
when used < 100 and trim(l) != ""
|
|
106
|
+
set shown to append(shown, trim(l))
|
|
107
|
+
set used to used + length(l) + 2
|
|
108
|
+
let rest be length(lines) - length(shown)
|
|
109
|
+
give text(length(lines)) + (when name == "ls" then " entradas: " otherwise " archivos: ") + join(shown, " ") + (when rest > 0 then dim(" +" + text(rest)) otherwise "")
|
|
110
|
+
when name == "grep" and not VERBOSE
|
|
111
|
+
let files be {}
|
|
112
|
+
each l in lines
|
|
113
|
+
let f be split(l, ":")[0]
|
|
114
|
+
when trim(l) != ""
|
|
115
|
+
set files[f] to true
|
|
116
|
+
give text(length(lines)) + " coincidencias en " + text(length(keys(files))) + " archivo" + (when length(keys(files)) == 1 then "" otherwise "s") + " · " + first_line(out, 90)
|
|
117
|
+
when name == "bash" or name == "process" or VERBOSE
|
|
118
|
+
-- hasta OUT_LINES líneas del output (todas con /verbose), el resto colapsado → /out
|
|
95
119
|
let shown be []
|
|
96
120
|
each e in enumerate(lines)
|
|
97
|
-
when e["index"] <
|
|
98
|
-
when
|
|
99
|
-
|
|
100
|
-
let extra be when length(lines) > 5 then dim(" (+" + text(length(lines) - 5) + " líneas)") otherwise ""
|
|
121
|
+
when VERBOSE or e["index"] < OUT_LINES
|
|
122
|
+
set shown to append(shown, when VERBOSE then e["item"] otherwise slice(e["item"], 0, 160))
|
|
123
|
+
let extra be when not VERBOSE and length(lines) > OUT_LINES then "\n " + dim("(+" + text(length(lines) - OUT_LINES) + " líneas · /out para ver todo)") otherwise ""
|
|
101
124
|
give join(shown, "\n ") + extra
|
|
102
125
|
give first_line(out, 120)
|
|
103
126
|
|
|
127
|
+
-- diff de un edit/write (antes/después publicado por la tool en el blackboard) — nothing si no hay
|
|
128
|
+
task diff_of(name, out)
|
|
129
|
+
when name != "edit" and name != "write"
|
|
130
|
+
give nothing
|
|
131
|
+
when starts_with(out, "ERROR") or starts_with(out, "DENIED")
|
|
132
|
+
give nothing
|
|
133
|
+
observe "lampson:ui:diff" as d
|
|
134
|
+
when d == nothing
|
|
135
|
+
give nothing
|
|
136
|
+
give diff.diff(d["old"], d["new"], 2, COLOR)
|
|
137
|
+
|
|
138
|
+
task print_diff(path, d)
|
|
139
|
+
let head be path + " · " + green("+" + text(d["added"])) + " " + red("−" + text(d["removed"]))
|
|
140
|
+
print(" " + green("✓") + " " + head)
|
|
141
|
+
each l in d["lines"]
|
|
142
|
+
print(" " + l)
|
|
143
|
+
|
|
144
|
+
-- /out: resultado completo de una tool del turno
|
|
145
|
+
task print_full(o)
|
|
146
|
+
when o["diff"] != nothing
|
|
147
|
+
print_diff(o["path"], o["diff"])
|
|
148
|
+
otherwise
|
|
149
|
+
each l in split(o["output"], "\n")
|
|
150
|
+
print(" " + l)
|
|
151
|
+
|
|
104
152
|
-- tag = "" para el agente principal; los subagentes en background NO pasan por acá (escriben su log)
|
|
105
153
|
task on_event(kind, data, tag)
|
|
106
154
|
trace.event(sid, kind, data, tag)
|
|
107
155
|
when kind == "assistant"
|
|
108
156
|
print("")
|
|
109
|
-
print(data)
|
|
157
|
+
print(md.render(data, COLOR))
|
|
110
158
|
print("")
|
|
111
159
|
otherwise when kind == "inbox"
|
|
112
160
|
print(" " + cyan("✉ " + first_line(data, 140)))
|
|
113
161
|
otherwise when kind == "tool_call"
|
|
114
|
-
print(" " + yellow("
|
|
162
|
+
print(" " + yellow("▸ " + permission.describe_call(data["name"], data["args"])))
|
|
115
163
|
otherwise when kind == "tool_result"
|
|
116
164
|
let out be data["output"]
|
|
165
|
+
let name be data["call"]["name"]
|
|
117
166
|
let bad be starts_with(out, "ERROR") or starts_with(out, "DENIED")
|
|
118
|
-
let
|
|
119
|
-
|
|
167
|
+
let d be diff_of(name, out)
|
|
168
|
+
let path be when d != nothing then text(data["call"]["args"]["path"]) otherwise ""
|
|
169
|
+
set turn_outputs to append(turn_outputs, {"name": name, "args": data["call"]["args"], "output": out, "diff": d, "path": path})
|
|
170
|
+
when d != nothing
|
|
171
|
+
print_diff(path, d)
|
|
172
|
+
otherwise
|
|
173
|
+
let mark be when bad then red("✗") otherwise green("✓")
|
|
174
|
+
print(" " + mark + " " + (when bad then red(summarize(name, out)) otherwise dim(summarize(name, out))))
|
|
120
175
|
otherwise when kind == "tool_denied"
|
|
121
176
|
set data to data
|
|
122
177
|
otherwise when kind == "error"
|
|
@@ -134,8 +189,12 @@ task ask_user(name, args, why)
|
|
|
134
189
|
print(" " + yellow("⚠ requiere tu aprobación · " + why))
|
|
135
190
|
print(" " + permission.describe_call(name, args))
|
|
136
191
|
flush()
|
|
137
|
-
|
|
138
|
-
|
|
192
|
+
-- menú ↑↓/Enter (o p/d, 1/2, Esc = no) cuando hay TTY; sin TTY, el approve nativo (fail-closed)
|
|
193
|
+
let pick be ed.choose(" ¿permitir?", ["permitir", "denegar"], COLOR)
|
|
194
|
+
when pick == -1
|
|
195
|
+
let ok be approve " ¿permitir?" within 10m
|
|
196
|
+
give ok
|
|
197
|
+
give pick == 0
|
|
139
198
|
|
|
140
199
|
let LINE be " ────────────────────────────────────────────────────────────────────────"
|
|
141
200
|
|
|
@@ -164,6 +223,8 @@ let COMMANDS be [
|
|
|
164
223
|
["/mcp", "[add <nombre> <comando…> [--project] | remove <nombre>]", "servers MCP: listar, conectar o quitar (global: lampson/.lampson/mcp.json · proyecto: .lampson/mcp.json)"],
|
|
165
224
|
["/lamps", "[on <nombre> | off <nombre> | run <lámpara> <tool> [json] | remove <nombre>]", "lámparas (plugins de tools): listar, encender o apagar (global: lampson/lamps/ · proyecto: .lampson/lamps/)"],
|
|
166
225
|
["/lsp", "[add <typescript|python|rust|go|css|html> [--project] | add <nombre> <comando…> --ext .x=lang | remove <nombre>]", "language servers (navegación semántica: symbols/definition/references/hover); arrancan en la primera consulta"],
|
|
226
|
+
["/out", "[n]", "resultado completo de la última tool del turno (n = contar hacia atrás: /out 2 es la anteúltima)"],
|
|
227
|
+
["/verbose", "", "alternar: mostrar SIEMPRE el output completo de cada tool (queda guardado en config.json)"],
|
|
167
228
|
["/trace", "[n]", "traza legible de esta sesión (pasos, tools, tiempos, tokens, errores): .lampson/trace/<sesión>.log"],
|
|
168
229
|
["/tokens", "", "tamaño del contexto y tokens gastados en esta sesión"],
|
|
169
230
|
["/new", "", "empezar una sesión nueva (historial vacío)"],
|
|
@@ -182,10 +243,138 @@ task pad(s, n)
|
|
|
182
243
|
set out to out + " "
|
|
183
244
|
give out
|
|
184
245
|
|
|
246
|
+
-- entrada multilínea sin modo raw (read_key llegará a Synsema): una línea que termina en "\" continúa
|
|
247
|
+
-- en la siguiente; una línea que EMPIEZA con """ abre un bloque que cierra con una línea """ sola
|
|
248
|
+
-- (ideal para pegar código o logs). EOF corta el bloque con lo acumulado.
|
|
249
|
+
-- candidatos para completar el argumento de un comando (Tab / menú del editor de línea).
|
|
250
|
+
-- head = argumentos ya escritos antes del último token; last = lo que se está escribiendo
|
|
251
|
+
task complete_args(cmd, head, last)
|
|
252
|
+
let first be trim(head) == ""
|
|
253
|
+
when cmd == "/image"
|
|
254
|
+
give workspace_paths(last)
|
|
255
|
+
when cmd == "/resume" or cmd == "/delete"
|
|
256
|
+
give apply((s) => s["id"], session.list())
|
|
257
|
+
when cmd == "/provider"
|
|
258
|
+
when first
|
|
259
|
+
give apply((p) => p["name"], provider.providers())
|
|
260
|
+
give []
|
|
261
|
+
when cmd == "/logs" or cmd == "/stop"
|
|
262
|
+
when first
|
|
263
|
+
give apply((p) => p["name"], proc.list())
|
|
264
|
+
give []
|
|
265
|
+
when cmd == "/memory"
|
|
266
|
+
give apply((m) => m["name"], memo.list())
|
|
267
|
+
when cmd == "/lamps"
|
|
268
|
+
when first
|
|
269
|
+
give ["on", "off", "run", "remove"]
|
|
270
|
+
give apply((l) => l["name"], lamps.summary())
|
|
271
|
+
when cmd == "/mcp"
|
|
272
|
+
when first
|
|
273
|
+
give ["add", "remove"]
|
|
274
|
+
when starts_with(trim(head), "remove")
|
|
275
|
+
give apply((m) => m["name"], mcp.summary())
|
|
276
|
+
give ["--project"]
|
|
277
|
+
when cmd == "/lsp"
|
|
278
|
+
when first
|
|
279
|
+
give ["add", "remove"]
|
|
280
|
+
when starts_with(trim(head), "remove")
|
|
281
|
+
give apply((s) => s["name"], lsp.summary())
|
|
282
|
+
when trim(head) == "add"
|
|
283
|
+
give sort_by(keys(lsp.PRESETS), (x) => x)
|
|
284
|
+
give ["--project", "--ext"]
|
|
285
|
+
when cmd == "/agent"
|
|
286
|
+
give ["build", "plan", "review", "explore", "worker"]
|
|
287
|
+
when cmd == "/model"
|
|
288
|
+
give []
|
|
289
|
+
give []
|
|
290
|
+
|
|
291
|
+
-- rutas del workspace que empiezan con el prefijo escrito (directorios con "/" final para seguir completando)
|
|
292
|
+
task workspace_paths(prefix)
|
|
293
|
+
let dir be ""
|
|
294
|
+
let base be prefix
|
|
295
|
+
let cut be -1
|
|
296
|
+
let i be length(prefix) - 1
|
|
297
|
+
while i >= 0 and cut == -1
|
|
298
|
+
when slice(prefix, i, i + 1) == "/" or slice(prefix, i, i + 1) == "\\"
|
|
299
|
+
set cut to i
|
|
300
|
+
set i to i - 1
|
|
301
|
+
when cut >= 0
|
|
302
|
+
set dir to slice(prefix, 0, cut + 1)
|
|
303
|
+
set base to slice(prefix, cut + 1, length(prefix))
|
|
304
|
+
let out be []
|
|
305
|
+
try
|
|
306
|
+
each e in list_dir(when dir == "" then "workspace" otherwise "workspace/" + dir)
|
|
307
|
+
when starts_with(e["name"], base) and e["name"] != ".git"
|
|
308
|
+
set out to append(out, dir + e["name"] + (when e["is_dir"] then "/" otherwise ""))
|
|
309
|
+
recover err
|
|
310
|
+
set out to []
|
|
311
|
+
give sort_by(out, (x) => x)
|
|
312
|
+
|
|
313
|
+
-- mientras el humano no escribe: ¿hay avisos de subagentes en background? → cortar la lectura y
|
|
314
|
+
-- dejar que el bucle principal los inyecte (turno automático)
|
|
315
|
+
task idle()
|
|
316
|
+
let pending be false
|
|
317
|
+
try
|
|
318
|
+
each c in agents.list_children()
|
|
319
|
+
when c["status"] != "running" and not c["delivered"]
|
|
320
|
+
set pending to true
|
|
321
|
+
recover err
|
|
322
|
+
set pending to false
|
|
323
|
+
give pending
|
|
324
|
+
|
|
325
|
+
let input_history be []
|
|
326
|
+
|
|
327
|
+
task read_input(prompt)
|
|
328
|
+
let first be read_line(prompt)
|
|
329
|
+
when first == nothing
|
|
330
|
+
give nothing
|
|
331
|
+
let more be []
|
|
332
|
+
when trim(first) == "\"\"\"" or starts_with(first, "\"\"\"")
|
|
333
|
+
let body be slice(first, 3, length(first))
|
|
334
|
+
when trim(body) != ""
|
|
335
|
+
set more to append(more, body)
|
|
336
|
+
let open be true
|
|
337
|
+
while open
|
|
338
|
+
let l be read_line(dim(" … "))
|
|
339
|
+
when l == nothing or trim(l) == "\"\"\""
|
|
340
|
+
set open to false
|
|
341
|
+
otherwise
|
|
342
|
+
set more to append(more, l)
|
|
343
|
+
give join(more, "\n")
|
|
344
|
+
let cur be first
|
|
345
|
+
while ends_with(cur, "\\")
|
|
346
|
+
set more to append(more, slice(cur, 0, length(cur) - 1))
|
|
347
|
+
let l be read_line(dim(" … "))
|
|
348
|
+
set cur to when l == nothing then "" otherwise l
|
|
349
|
+
set more to append(more, cur)
|
|
350
|
+
give join(more, "\n")
|
|
351
|
+
|
|
352
|
+
-- últimos comandos usados (config.json "recent"): "/" solo los muestra primero
|
|
353
|
+
task remember_command(input)
|
|
354
|
+
let name be split(trim(input), " ")[0]
|
|
355
|
+
when name == "/" or name == "/help"
|
|
356
|
+
give nothing
|
|
357
|
+
let doc be settings.load()
|
|
358
|
+
let old be when contains(doc, "recent") then doc["recent"] otherwise []
|
|
359
|
+
let recent be [name]
|
|
360
|
+
each r in old
|
|
361
|
+
when r != name and length(recent) < 3
|
|
362
|
+
set recent to append(recent, r)
|
|
363
|
+
set doc["recent"] to recent
|
|
364
|
+
settings.save(doc)
|
|
365
|
+
give nothing
|
|
366
|
+
|
|
185
367
|
task help()
|
|
368
|
+
let doc be settings.load()
|
|
369
|
+
let recent be when contains(doc, "recent") then doc["recent"] otherwise []
|
|
186
370
|
print("")
|
|
371
|
+
when length(recent) > 0
|
|
372
|
+
print(" recientes " + join(apply((r) => cyan(r), recent), " "))
|
|
373
|
+
print("")
|
|
187
374
|
print(" Escribí lo que querés hacer en el proyecto. El agente lee, busca, edita y corre comandos con tools")
|
|
188
375
|
print(" acotadas al workspace; cada paso queda a la vista. Los comandos empiezan con /:")
|
|
376
|
+
print(" Teclas: Tab completa · ↑↓ historial/menú · Alt+Enter salto de línea · Ctrl+O último resultado completo · Esc cierra el menú")
|
|
377
|
+
print(" Sin TTY (pipe): terminá la línea con \\ para continuar, o un bloque entre líneas \"\"\".")
|
|
189
378
|
print("")
|
|
190
379
|
each c in COMMANDS
|
|
191
380
|
print(" " + pad(c[0] + " " + c[1], 38) + c[2])
|
|
@@ -238,6 +427,21 @@ task banner(ws, cfg, profile, mode, sid)
|
|
|
238
427
|
print(" workspace " + ws)
|
|
239
428
|
print(" agente " + profile + " permisos " + mode + " modelo " + cfg["model"])
|
|
240
429
|
print(" sesión " + sid + " " + git.summary())
|
|
430
|
+
-- extensiones en una línea (detalle con /lamps, /mcp, /lsp): encendidas/total
|
|
431
|
+
let ls be lamps.summary()
|
|
432
|
+
let lamps_on be length(where(ls, (l) => l["enabled"]))
|
|
433
|
+
let ms be mcp.summary()
|
|
434
|
+
let mcp_on be length(where(ms, (m) => m["status"] == "ready"))
|
|
435
|
+
let ss be lsp.summary()
|
|
436
|
+
let ext be []
|
|
437
|
+
when length(ls) > 0
|
|
438
|
+
set ext to append(ext, (when lamps_on > 0 then green("lamps " + text(lamps_on) + "/" + text(length(ls))) otherwise dim("lamps 0/" + text(length(ls)))))
|
|
439
|
+
when length(ms) > 0
|
|
440
|
+
set ext to append(ext, (when mcp_on == length(ms) then green("mcp " + text(mcp_on) + "/" + text(length(ms))) otherwise red("mcp " + text(mcp_on) + "/" + text(length(ms)))))
|
|
441
|
+
when length(ss) > 0
|
|
442
|
+
set ext to append(ext, dim("lsp " + text(length(ss))))
|
|
443
|
+
when length(ext) > 0
|
|
444
|
+
print(" extras " + join(ext, dim(" · ")) + dim(" (/lamps /mcp /lsp)"))
|
|
241
445
|
let upd be update.line()
|
|
242
446
|
when upd != ""
|
|
243
447
|
print(" " + yellow("⬆ " + upd) + dim(" (o /update acá)"))
|
|
@@ -402,14 +606,13 @@ let system_msg be system_for(profile)
|
|
|
402
606
|
-- servers MCP (globales en .lampson/mcp.json, del proyecto en workspace/.lampson/mcp.json): arrancan antes
|
|
403
607
|
-- de armar el catálogo de tools; hasta 8 s de espera a que estén listos
|
|
404
608
|
let mcp_servers be mcp.start_all(8)
|
|
609
|
+
-- mcp / lsp / lámparas van resumidos en el banner ("extras"); acá solo se avisa lo que está ROTO
|
|
405
610
|
each ms in mcp.summary()
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
each s in lsp.summary()
|
|
409
|
-
print(" " + dim("○ lsp " + s["name"] + " (" + s["scope"] + ") " + join(s["extensions"], " ") + " · arranca en la primera consulta"))
|
|
410
|
-
-- lámparas (plugins de tools, lib/lamps.syn): solo se anuncian; apagadas no entran al catálogo
|
|
611
|
+
when ms["status"] != "ready"
|
|
612
|
+
print(" " + red("○ mcp " + ms["name"] + " " + ms["status"]) + dim(when ms["error"] != nothing then " · " + text(ms["error"]) otherwise ""))
|
|
411
613
|
each l in lamps.summary()
|
|
412
|
-
|
|
614
|
+
when l["error"] != nothing
|
|
615
|
+
print(" " + red("○ lamp " + l["name"] + " rota") + dim(" · " + text(l["error"])))
|
|
413
616
|
let opts be opts_for(profile, lower(env("LAMPSON_PERMISSION", "ask")))
|
|
414
617
|
|
|
415
618
|
-- marca de corrida (blackboard): session.save la estampa en meta.run; reanudar una sesión guardada por
|
|
@@ -465,14 +668,24 @@ while running
|
|
|
465
668
|
session.save(sid, messages, {"title": session.title_of(messages)})
|
|
466
669
|
flush()
|
|
467
670
|
otherwise
|
|
468
|
-
|
|
671
|
+
let saved be settings.load()
|
|
672
|
+
let ctx be {"commands": COMMANDS, "recent": (when contains(saved, "recent") then saved["recent"] otherwise []), "history": input_history, "complete": complete_args, "idle": idle, "fallback": read_input}
|
|
673
|
+
set line to ed.read((when length(pending_images) > 0 then dim("📎" + text(length(pending_images)) + " ") otherwise "") + cyan("❯ "), COLOR, ctx)
|
|
674
|
+
when line != nothing and line != ed.INTERRUPT and trim(line) != ""
|
|
675
|
+
set input_history to append(input_history, line)
|
|
469
676
|
when inbox != nothing
|
|
470
677
|
set running to running
|
|
678
|
+
otherwise when line == ed.INTERRUPT
|
|
679
|
+
set running to running
|
|
471
680
|
otherwise when line == nothing
|
|
472
681
|
set running to false
|
|
473
682
|
otherwise
|
|
474
683
|
set followups to 0
|
|
684
|
+
when not starts_with(trim(line), "/")
|
|
685
|
+
set turn_outputs to []
|
|
475
686
|
let input be trim(line)
|
|
687
|
+
when starts_with(input, "/") and length(input) > 1
|
|
688
|
+
remember_command(input)
|
|
476
689
|
when input == ""
|
|
477
690
|
set running to running
|
|
478
691
|
otherwise when input == "/exit" or input == "/quit"
|
|
@@ -491,6 +704,23 @@ while running
|
|
|
491
704
|
print(" " + update.apply())
|
|
492
705
|
otherwise when input == "/config"
|
|
493
706
|
show_config(cfg, env_info["cwd"], profile, mode, opts)
|
|
707
|
+
otherwise when input == "/verbose"
|
|
708
|
+
set VERBOSE to not VERBOSE
|
|
709
|
+
let doc be settings.load()
|
|
710
|
+
set doc["verbose"] to VERBOSE
|
|
711
|
+
settings.save(doc)
|
|
712
|
+
print(" " + (when VERBOSE then green("verbose ON") + dim(" · cada tool muestra su output completo") otherwise dim("verbose OFF · outputs colapsados a " + text(OUT_LINES) + " líneas; /out para ver uno completo")))
|
|
713
|
+
otherwise when starts_with(input, "/out")
|
|
714
|
+
let n be trim(slice(input, 4, length(input)))
|
|
715
|
+
let back be when n == "" then 1 otherwise floor(number(n))
|
|
716
|
+
when length(turn_outputs) == 0
|
|
717
|
+
print(dim(" ninguna tool corrió en este turno"))
|
|
718
|
+
otherwise when back < 1 or back > length(turn_outputs)
|
|
719
|
+
print(dim(" hay " + text(length(turn_outputs)) + " resultados en este turno: /out 1 … /out " + text(length(turn_outputs))))
|
|
720
|
+
otherwise
|
|
721
|
+
let o be turn_outputs[length(turn_outputs) - back]
|
|
722
|
+
print(" " + yellow("▸ " + permission.describe_call(o["name"], o["args"])))
|
|
723
|
+
print_full(o)
|
|
494
724
|
otherwise when starts_with(input, "/trace")
|
|
495
725
|
let n be trim(slice(input, 6, length(input)))
|
|
496
726
|
let lines be when n == "" then 40 otherwise floor(number(n))
|
package/lib/diff.syn
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
-- lib/diff.syn — diff de líneas para mostrar en la terminal qué cambió edit/write
|
|
2
|
+
-- diff(old, new, ctx, color) → {"added": n, "removed": m, "lines": [texto ya formateado…]}
|
|
3
|
+
-- Algoritmo: se recorta el prefijo y sufijo comunes (una edición es local) y se hace LCS solo sobre el
|
|
4
|
+
-- medio. Si el medio es enorme (> LIMIT² celdas) se muestra como bloque quitado + bloque agregado.
|
|
5
|
+
|
|
6
|
+
let ESC be decode(bytes("1b", "hex"))
|
|
7
|
+
let LIMIT be 600
|
|
8
|
+
|
|
9
|
+
task sgr(color, code, s)
|
|
10
|
+
when not color
|
|
11
|
+
give s
|
|
12
|
+
give ESC + "[" + code + "m" + s + ESC + "[0m"
|
|
13
|
+
|
|
14
|
+
task lpad(s, n)
|
|
15
|
+
let out be s
|
|
16
|
+
while length(out) < n
|
|
17
|
+
set out to " " + out
|
|
18
|
+
give out
|
|
19
|
+
|
|
20
|
+
-- LCS clásico: devuelve la lista de ops {"op": " "|"-"|"+", "a": idx_old, "b": idx_new}
|
|
21
|
+
task lcs_ops(a, b)
|
|
22
|
+
let n be length(a)
|
|
23
|
+
let m be length(b)
|
|
24
|
+
-- tabla (n+1)×(m+1) aplanada, fila por fila
|
|
25
|
+
let table be []
|
|
26
|
+
let size be (n + 1) * (m + 1)
|
|
27
|
+
while length(table) < size
|
|
28
|
+
set table to append(table, 0)
|
|
29
|
+
let i be n - 1
|
|
30
|
+
while i >= 0
|
|
31
|
+
let j be m - 1
|
|
32
|
+
while j >= 0
|
|
33
|
+
when a[i] == b[j]
|
|
34
|
+
set table[i * (m + 1) + j] to table[(i + 1) * (m + 1) + j + 1] + 1
|
|
35
|
+
otherwise
|
|
36
|
+
let down be table[(i + 1) * (m + 1) + j]
|
|
37
|
+
let right be table[i * (m + 1) + j + 1]
|
|
38
|
+
set table[i * (m + 1) + j] to (when down >= right then down otherwise right)
|
|
39
|
+
set j to j - 1
|
|
40
|
+
set i to i - 1
|
|
41
|
+
let ops be []
|
|
42
|
+
set i to 0
|
|
43
|
+
let j be 0
|
|
44
|
+
while i < n and j < m
|
|
45
|
+
when a[i] == b[j]
|
|
46
|
+
set ops to append(ops, {"op": " ", "a": i, "b": j})
|
|
47
|
+
set i to i + 1
|
|
48
|
+
set j to j + 1
|
|
49
|
+
otherwise when table[(i + 1) * (m + 1) + j] >= table[i * (m + 1) + j + 1]
|
|
50
|
+
set ops to append(ops, {"op": "-", "a": i, "b": j})
|
|
51
|
+
set i to i + 1
|
|
52
|
+
otherwise
|
|
53
|
+
set ops to append(ops, {"op": "+", "a": i, "b": j})
|
|
54
|
+
set j to j + 1
|
|
55
|
+
while i < n
|
|
56
|
+
set ops to append(ops, {"op": "-", "a": i, "b": j})
|
|
57
|
+
set i to i + 1
|
|
58
|
+
while j < m
|
|
59
|
+
set ops to append(ops, {"op": "+", "a": i, "b": j})
|
|
60
|
+
set j to j + 1
|
|
61
|
+
give ops
|
|
62
|
+
|
|
63
|
+
export task diff(old, new, ctx, color)
|
|
64
|
+
let a be when old == "" then [] otherwise split(old, "\n")
|
|
65
|
+
let b be when new == "" then [] otherwise split(new, "\n")
|
|
66
|
+
-- prefijo común
|
|
67
|
+
let pre be 0
|
|
68
|
+
while pre < length(a) and pre < length(b) and a[pre] == b[pre]
|
|
69
|
+
set pre to pre + 1
|
|
70
|
+
-- sufijo común (sin pisar el prefijo)
|
|
71
|
+
let suf be 0
|
|
72
|
+
while suf < length(a) - pre and suf < length(b) - pre and a[length(a) - 1 - suf] == b[length(b) - 1 - suf]
|
|
73
|
+
set suf to suf + 1
|
|
74
|
+
let mid_a be slice(a, pre, length(a) - suf)
|
|
75
|
+
let mid_b be slice(b, pre, length(b) - suf)
|
|
76
|
+
let ops be []
|
|
77
|
+
when length(mid_a) * length(mid_b) > LIMIT * LIMIT
|
|
78
|
+
each e in enumerate(mid_a)
|
|
79
|
+
set ops to append(ops, {"op": "-", "a": e["index"], "b": 0})
|
|
80
|
+
each e in enumerate(mid_b)
|
|
81
|
+
set ops to append(ops, {"op": "+", "a": length(mid_a), "b": e["index"]})
|
|
82
|
+
otherwise
|
|
83
|
+
set ops to lcs_ops(mid_a, mid_b)
|
|
84
|
+
let added be 0
|
|
85
|
+
let removed be 0
|
|
86
|
+
each o in ops
|
|
87
|
+
when o["op"] == "+"
|
|
88
|
+
set added to added + 1
|
|
89
|
+
otherwise when o["op"] == "-"
|
|
90
|
+
set removed to removed + 1
|
|
91
|
+
-- render: ctx líneas de contexto antes y después del bloque cambiado (números de línea del archivo NUEVO
|
|
92
|
+
-- para "+"/" ", del viejo para "-"), con "⋯" entre hunks
|
|
93
|
+
let width be length(text(length(b)))
|
|
94
|
+
let lines be []
|
|
95
|
+
-- contexto previo (del prefijo común)
|
|
96
|
+
let start be when pre - ctx > 0 then pre - ctx otherwise 0
|
|
97
|
+
when start > 0
|
|
98
|
+
set lines to append(lines, sgr(color, "2", lpad("", width) + " ⋯"))
|
|
99
|
+
let k be start
|
|
100
|
+
while k < pre
|
|
101
|
+
set lines to append(lines, sgr(color, "2", lpad(text(k + 1), width) + " │ " + a[k]))
|
|
102
|
+
set k to k + 1
|
|
103
|
+
-- medio: contexto interno acotado (ctx a cada lado de un cambio)
|
|
104
|
+
let last_change be -1
|
|
105
|
+
let idx be 0
|
|
106
|
+
each o in ops
|
|
107
|
+
when o["op"] != " "
|
|
108
|
+
set last_change to idx
|
|
109
|
+
set idx to idx + 1
|
|
110
|
+
set idx to 0
|
|
111
|
+
let skipping be false
|
|
112
|
+
each o in ops
|
|
113
|
+
let near be false
|
|
114
|
+
let w be idx - ctx
|
|
115
|
+
while w <= idx + ctx and not near
|
|
116
|
+
when w >= 0 and w < length(ops) and ops[w]["op"] != " "
|
|
117
|
+
set near to true
|
|
118
|
+
set w to w + 1
|
|
119
|
+
when o["op"] == " "
|
|
120
|
+
when near
|
|
121
|
+
set lines to append(lines, sgr(color, "2", lpad(text(pre + o["b"] + 1), width) + " │ " + mid_b[o["b"]]))
|
|
122
|
+
set skipping to false
|
|
123
|
+
otherwise when not skipping
|
|
124
|
+
set lines to append(lines, sgr(color, "2", lpad("", width) + " ⋯"))
|
|
125
|
+
set skipping to true
|
|
126
|
+
otherwise when o["op"] == "-"
|
|
127
|
+
set lines to append(lines, sgr(color, "2", lpad(text(pre + o["a"] + 1), width) + " │ ") + sgr(color, "31", "- " + mid_a[o["a"]]))
|
|
128
|
+
set skipping to false
|
|
129
|
+
otherwise
|
|
130
|
+
set lines to append(lines, sgr(color, "2", lpad(text(pre + o["b"] + 1), width) + " │ ") + sgr(color, "32", "+ " + mid_b[o["b"]]))
|
|
131
|
+
set skipping to false
|
|
132
|
+
set idx to idx + 1
|
|
133
|
+
-- contexto posterior (del sufijo común)
|
|
134
|
+
let after_start be length(b) - suf
|
|
135
|
+
let upto be when after_start + ctx < length(b) then after_start + ctx otherwise length(b)
|
|
136
|
+
set k to after_start
|
|
137
|
+
while k < upto
|
|
138
|
+
set lines to append(lines, sgr(color, "2", lpad(text(k + 1), width) + " │ " + b[k]))
|
|
139
|
+
set k to k + 1
|
|
140
|
+
when upto < length(b)
|
|
141
|
+
set lines to append(lines, sgr(color, "2", lpad("", width) + " ⋯"))
|
|
142
|
+
give {"added": added, "removed": removed, "lines": lines}
|
package/lib/line.syn
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
-- lib/line.syn — editor de línea para la terminal (Synsema v0.6.11+: term_open/term_recv)
|
|
2
|
+
-- read(prompt, color, ctx) → texto (puede tener "\n"), INTERRUPT si ctx["idle"]() pidió cortar,
|
|
3
|
+
-- nothing en EOF. Sin TTY (pipe/CI) delega en ctx["fallback"](prompt).
|
|
4
|
+
-- choose(question, options, color) → índice elegido, -1 sin TTY (usar approve), nothing en EOF/Esc.
|
|
5
|
+
-- Teclas: ←→ Home End · ↑↓ historial o menú · Tab completa · Alt+Enter salto de línea · Enter envía
|
|
6
|
+
-- Ctrl+A/E inicio/fin · Ctrl+U borra la línea · Ctrl+W borra palabra · Ctrl+O = /out · Esc cierra el menú
|
|
7
|
+
-- Menú: al escribir "/" aparecen los comandos (primero los recientes); con "/cmd " los argumentos que
|
|
8
|
+
-- devuelva ctx["complete"](cmd, prefijo) (archivos, servers, flags…). Máx MENU_ROWS filas.
|
|
9
|
+
-- MIGA: dibujar SIEMPRE con term_write (print queda buffereado). El runtime restaura la terminal al cerrar.
|
|
10
|
+
|
|
11
|
+
let ESC be decode(bytes("1b", "hex"))
|
|
12
|
+
let MENU_ROWS be 8
|
|
13
|
+
export let INTERRUPT be ESC + "interrupt"
|
|
14
|
+
|
|
15
|
+
task sgr(color, code, s)
|
|
16
|
+
when not color or s == ""
|
|
17
|
+
give s
|
|
18
|
+
give ESC + "[" + code + "m" + s + ESC + "[0m"
|
|
19
|
+
|
|
20
|
+
task vis(s)
|
|
21
|
+
give length(strip_ansi(s))
|
|
22
|
+
|
|
23
|
+
task rep(ch, n)
|
|
24
|
+
let out be ""
|
|
25
|
+
while length(out) < n
|
|
26
|
+
set out to out + ch
|
|
27
|
+
give out
|
|
28
|
+
|
|
29
|
+
task starts(s, p)
|
|
30
|
+
give starts_with(s, p)
|
|
31
|
+
|
|
32
|
+
-- ---------- candidatos ----------
|
|
33
|
+
|
|
34
|
+
-- buf = texto del editor. give {"kind": "cmd"|"arg"|"", "items": [{"label","fill","desc"}], "token": prefijo}
|
|
35
|
+
task candidates(buf, ctx)
|
|
36
|
+
when not starts(buf, "/") or contains(buf, "\n")
|
|
37
|
+
give {"kind": "", "items": [], "token": ""}
|
|
38
|
+
let sp be capture(buf, "^(/\S*)\s+(.*)$")
|
|
39
|
+
when sp == nothing
|
|
40
|
+
-- comando a medias: recientes primero cuando es "/" pelado
|
|
41
|
+
let items be []
|
|
42
|
+
let seen be {}
|
|
43
|
+
when buf == "/"
|
|
44
|
+
each r in ctx["recent"]
|
|
45
|
+
each c in ctx["commands"]
|
|
46
|
+
when c[0] == r and not contains(seen, r)
|
|
47
|
+
set seen[r] to true
|
|
48
|
+
set items to append(items, {"label": c[0], "fill": c[0] + (when c[1] != "" then " " otherwise ""), "args": c[1], "desc": c[2], "recent": true})
|
|
49
|
+
each c in ctx["commands"]
|
|
50
|
+
when starts(c[0], buf) and not contains(seen, c[0])
|
|
51
|
+
set seen[c[0]] to true
|
|
52
|
+
set items to append(items, {"label": c[0], "fill": c[0] + (when c[1] != "" then " " otherwise ""), "args": c[1], "desc": c[2], "recent": false})
|
|
53
|
+
give {"kind": "cmd", "items": items, "token": buf}
|
|
54
|
+
let cmd be sp[0]
|
|
55
|
+
let rest be sp[1]
|
|
56
|
+
-- último token (lo que se completa); lo anterior queda fijo
|
|
57
|
+
let toks be split(rest, " ")
|
|
58
|
+
let last be toks[length(toks) - 1]
|
|
59
|
+
let head be slice(rest, 0, length(rest) - length(last))
|
|
60
|
+
let items be []
|
|
61
|
+
let cands be ctx["complete"](cmd, head, last)
|
|
62
|
+
each c in cands
|
|
63
|
+
when starts(c, last)
|
|
64
|
+
set items to append(items, {"label": c, "fill": cmd + " " + head + c + (when ends_with(c, "/") then "" otherwise " "), "args": "", "desc": "", "recent": false})
|
|
65
|
+
give {"kind": "arg", "items": items, "token": last}
|
|
66
|
+
|
|
67
|
+
-- ---------- dibujo ----------
|
|
68
|
+
|
|
69
|
+
-- filas físicas que ocupa un texto de ancho w en una terminal de cols columnas
|
|
70
|
+
task rows_of(w, cols)
|
|
71
|
+
when cols <= 0
|
|
72
|
+
give 1
|
|
73
|
+
give floor(w / cols) + 1
|
|
74
|
+
|
|
75
|
+
-- dibuja prompt + buffer (multilínea) + menú; deja el cursor en su lugar. give filas totales dibujadas
|
|
76
|
+
-- (para poder volver al inicio en el próximo redraw)
|
|
77
|
+
task draw(h, st, color, ctx)
|
|
78
|
+
let size be term_size(h)
|
|
79
|
+
let cols be size["cols"]
|
|
80
|
+
let lines be split(st["buf"], "\n")
|
|
81
|
+
-- posición del cursor: línea y columna dentro del buffer
|
|
82
|
+
let cl be 0
|
|
83
|
+
let cc be st["cur"]
|
|
84
|
+
let i be 0
|
|
85
|
+
let found be false
|
|
86
|
+
while i < length(lines) and not found
|
|
87
|
+
when cc <= length(lines[i])
|
|
88
|
+
set cl to i
|
|
89
|
+
set found to true
|
|
90
|
+
otherwise
|
|
91
|
+
set cc to cc - length(lines[i]) - 1
|
|
92
|
+
set i to i + 1
|
|
93
|
+
let out be ""
|
|
94
|
+
-- volver al inicio de lo dibujado la vez anterior y limpiar hacia abajo
|
|
95
|
+
when st["cursor_row"] > 0
|
|
96
|
+
set out to out + ESC + "[" + text(st["cursor_row"]) + "A"
|
|
97
|
+
set out to out + "\r" + ESC + "[J"
|
|
98
|
+
let cont be sgr(color, "2", " … ")
|
|
99
|
+
let rows_before_cursor be 0
|
|
100
|
+
let total be 0
|
|
101
|
+
let cursor_col be 0
|
|
102
|
+
set i to 0
|
|
103
|
+
each l in lines
|
|
104
|
+
let pre be when i == 0 then st["prompt"] otherwise cont
|
|
105
|
+
set out to out + (when i > 0 then "\r\n" otherwise "") + pre + l
|
|
106
|
+
let r be rows_of(vis(pre) + length(l), cols)
|
|
107
|
+
when i < cl
|
|
108
|
+
set rows_before_cursor to rows_before_cursor + r
|
|
109
|
+
otherwise when i == cl
|
|
110
|
+
set rows_before_cursor to rows_before_cursor + floor((vis(pre) + cc) / cols)
|
|
111
|
+
set cursor_col to (vis(pre) + cc) % cols
|
|
112
|
+
set total to total + r
|
|
113
|
+
set i to i + 1
|
|
114
|
+
-- menú
|
|
115
|
+
let menu_rows be 0
|
|
116
|
+
when st["menu"] and length(st["items"]) > 0
|
|
117
|
+
let items be st["items"]
|
|
118
|
+
let from be 0
|
|
119
|
+
when st["sel"] >= MENU_ROWS
|
|
120
|
+
set from to st["sel"] - MENU_ROWS + 1
|
|
121
|
+
let k be from
|
|
122
|
+
while k < length(items) and k < from + MENU_ROWS
|
|
123
|
+
let it be items[k]
|
|
124
|
+
let row be ""
|
|
125
|
+
when st["kind"] == "cmd"
|
|
126
|
+
let full be it["label"] + (when it["args"] != "" then " " + it["args"] otherwise "")
|
|
127
|
+
let name be when length(full) > 34 then slice(full, 0, 33) + "…" otherwise full
|
|
128
|
+
let padn be name + rep(" ", 34 - length(name))
|
|
129
|
+
let desc be when length(it["desc"]) > cols - 40 then slice(it["desc"], 0, cols - 43) + "…" otherwise it["desc"]
|
|
130
|
+
set row to (when k == st["sel"] then sgr(color, "7", " " + padn + " ") otherwise " " + sgr(color, "1", it["label"]) + sgr(color, "2", slice(padn, length(it["label"]), length(padn)) + " ")) + (when it["recent"] then sgr(color, "36", "↺ ") otherwise " ") + sgr(color, "2", desc)
|
|
131
|
+
otherwise
|
|
132
|
+
set row to when k == st["sel"] then sgr(color, "7", " " + it["label"] + " ") otherwise " " + it["label"]
|
|
133
|
+
set out to out + "\r\n" + " " + row
|
|
134
|
+
set menu_rows to menu_rows + 1
|
|
135
|
+
set k to k + 1
|
|
136
|
+
when length(items) > MENU_ROWS
|
|
137
|
+
set out to out + "\r\n" + sgr(color, "2", " (" + text(length(items)) + " opciones · ↑↓ elegir · Tab completa · Esc cierra)")
|
|
138
|
+
set menu_rows to menu_rows + 1
|
|
139
|
+
-- recolocar el cursor: subir (filas debajo de la línea del cursor + menú), ir a la columna
|
|
140
|
+
let below be total - rows_before_cursor - 1 + menu_rows
|
|
141
|
+
when below > 0
|
|
142
|
+
set out to out + ESC + "[" + text(below) + "A"
|
|
143
|
+
set out to out + "\r"
|
|
144
|
+
when cursor_col > 0
|
|
145
|
+
set out to out + ESC + "[" + text(cursor_col) + "C"
|
|
146
|
+
term_write(h, out)
|
|
147
|
+
set st["cursor_row"] to rows_before_cursor
|
|
148
|
+
give st
|
|
149
|
+
|
|
150
|
+
-- ---------- edición ----------
|
|
151
|
+
|
|
152
|
+
task insert(st, s)
|
|
153
|
+
set st["buf"] to slice(st["buf"], 0, st["cur"]) + s + slice(st["buf"], st["cur"], length(st["buf"]))
|
|
154
|
+
set st["cur"] to st["cur"] + length(s)
|
|
155
|
+
give st
|
|
156
|
+
|
|
157
|
+
task refresh_menu(st, ctx)
|
|
158
|
+
let c be candidates(st["buf"], ctx)
|
|
159
|
+
set st["kind"] to c["kind"]
|
|
160
|
+
set st["items"] to c["items"]
|
|
161
|
+
set st["sel"] to 0
|
|
162
|
+
set st["nav"] to false
|
|
163
|
+
set st["menu"] to c["kind"] != "" and length(c["items"]) > 0 and not st["menu_off"]
|
|
164
|
+
give st
|
|
165
|
+
|
|
166
|
+
task word_start(buf, cur)
|
|
167
|
+
let i be cur
|
|
168
|
+
while i > 0 and slice(buf, i - 1, i) == " "
|
|
169
|
+
set i to i - 1
|
|
170
|
+
while i > 0 and slice(buf, i - 1, i) != " " and slice(buf, i - 1, i) != "\n"
|
|
171
|
+
set i to i - 1
|
|
172
|
+
give i
|
|
173
|
+
|
|
174
|
+
task accept(st)
|
|
175
|
+
let it be st["items"][st["sel"]]
|
|
176
|
+
set st["buf"] to it["fill"]
|
|
177
|
+
set st["cur"] to length(it["fill"])
|
|
178
|
+
set st["menu"] to false
|
|
179
|
+
give st
|
|
180
|
+
|
|
181
|
+
export task read(prompt, color, ctx)
|
|
182
|
+
let h be term_open({"ctrl_c": "exit"})
|
|
183
|
+
when h == nothing
|
|
184
|
+
give ctx["fallback"](prompt)
|
|
185
|
+
let st be {"buf": "", "cur": 0, "prompt": prompt, "cursor_row": 0, "menu": false, "menu_off": false, "kind": "", "items": [], "sel": 0, "nav": false}
|
|
186
|
+
let hist be ctx["history"]
|
|
187
|
+
let hi be length(hist)
|
|
188
|
+
let draft be ""
|
|
189
|
+
let result be nothing
|
|
190
|
+
let done be false
|
|
191
|
+
term_write(h, "\r\n")
|
|
192
|
+
set st to draw(h, st, color, ctx)
|
|
193
|
+
while not done
|
|
194
|
+
let ev be term_recv(h, 1)
|
|
195
|
+
let dirty be true
|
|
196
|
+
when ev == nothing
|
|
197
|
+
set dirty to false
|
|
198
|
+
when ctx["idle"]()
|
|
199
|
+
set result to INTERRUPT
|
|
200
|
+
set done to true
|
|
201
|
+
otherwise when ev["type"] == "focus"
|
|
202
|
+
set dirty to false
|
|
203
|
+
otherwise when ev["type"] == "eof"
|
|
204
|
+
set result to nothing
|
|
205
|
+
set done to true
|
|
206
|
+
otherwise when ev["type"] == "paste"
|
|
207
|
+
set st to insert(st, replace_text(ev["text"], "\r", ""))
|
|
208
|
+
set st to refresh_menu(st, ctx)
|
|
209
|
+
otherwise when ev["type"] == "resize"
|
|
210
|
+
set st to st
|
|
211
|
+
otherwise when ev["type"] == "key"
|
|
212
|
+
let k be ev["key"]
|
|
213
|
+
let changed be true
|
|
214
|
+
when k == "enter" and ev["alt"]
|
|
215
|
+
set st to insert(st, "\n")
|
|
216
|
+
otherwise when k == "enter"
|
|
217
|
+
when st["menu"] and st["kind"] == "cmd" and st["buf"] != st["items"][st["sel"]]["label"] and st["buf"] != st["items"][st["sel"]]["fill"]
|
|
218
|
+
set st to accept(st)
|
|
219
|
+
when st["items"][st["sel"]]["args"] == ""
|
|
220
|
+
set result to trim(st["buf"])
|
|
221
|
+
set done to true
|
|
222
|
+
otherwise when st["menu"] and st["kind"] == "arg" and st["nav"]
|
|
223
|
+
-- Enter solo completa el argumento si el humano navegó el menú; si no, envía tal cual
|
|
224
|
+
set st to accept(st)
|
|
225
|
+
otherwise
|
|
226
|
+
set result to st["buf"]
|
|
227
|
+
set done to true
|
|
228
|
+
otherwise when k == "tab" and st["menu"]
|
|
229
|
+
set st to accept(st)
|
|
230
|
+
otherwise when k == "escape"
|
|
231
|
+
when st["menu"]
|
|
232
|
+
set st["menu"] to false
|
|
233
|
+
set st["menu_off"] to true
|
|
234
|
+
set changed to false
|
|
235
|
+
otherwise when k == "up" and st["menu"]
|
|
236
|
+
set st["sel"] to (when st["sel"] > 0 then st["sel"] - 1 otherwise length(st["items"]) - 1)
|
|
237
|
+
set st["nav"] to true
|
|
238
|
+
set changed to false
|
|
239
|
+
otherwise when k == "down" and st["menu"]
|
|
240
|
+
set st["sel"] to (when st["sel"] < length(st["items"]) - 1 then st["sel"] + 1 otherwise 0)
|
|
241
|
+
set st["nav"] to true
|
|
242
|
+
set changed to false
|
|
243
|
+
otherwise when k == "up"
|
|
244
|
+
when hi > 0
|
|
245
|
+
when hi == length(hist)
|
|
246
|
+
set draft to st["buf"]
|
|
247
|
+
set hi to hi - 1
|
|
248
|
+
set st["buf"] to hist[hi]
|
|
249
|
+
set st["cur"] to length(st["buf"])
|
|
250
|
+
set changed to false
|
|
251
|
+
otherwise when k == "down"
|
|
252
|
+
when hi < length(hist)
|
|
253
|
+
set hi to hi + 1
|
|
254
|
+
set st["buf"] to (when hi == length(hist) then draft otherwise hist[hi])
|
|
255
|
+
set st["cur"] to length(st["buf"])
|
|
256
|
+
set changed to false
|
|
257
|
+
otherwise when k == "left"
|
|
258
|
+
when st["cur"] > 0
|
|
259
|
+
set st["cur"] to st["cur"] - 1
|
|
260
|
+
set changed to false
|
|
261
|
+
otherwise when k == "right"
|
|
262
|
+
when st["cur"] < length(st["buf"])
|
|
263
|
+
set st["cur"] to st["cur"] + 1
|
|
264
|
+
set changed to false
|
|
265
|
+
otherwise when k == "home" or (k == "char" and ev["ctrl"] and ev["text"] == "a")
|
|
266
|
+
set st["cur"] to 0
|
|
267
|
+
set changed to false
|
|
268
|
+
otherwise when k == "end" or (k == "char" and ev["ctrl"] and ev["text"] == "e")
|
|
269
|
+
set st["cur"] to length(st["buf"])
|
|
270
|
+
set changed to false
|
|
271
|
+
otherwise when k == "backspace"
|
|
272
|
+
when st["cur"] > 0
|
|
273
|
+
set st["buf"] to slice(st["buf"], 0, st["cur"] - 1) + slice(st["buf"], st["cur"], length(st["buf"]))
|
|
274
|
+
set st["cur"] to st["cur"] - 1
|
|
275
|
+
otherwise when k == "delete"
|
|
276
|
+
when st["cur"] < length(st["buf"])
|
|
277
|
+
set st["buf"] to slice(st["buf"], 0, st["cur"]) + slice(st["buf"], st["cur"] + 1, length(st["buf"]))
|
|
278
|
+
otherwise when k == "char" and ev["ctrl"] and ev["text"] == "u"
|
|
279
|
+
set st["buf"] to ""
|
|
280
|
+
set st["cur"] to 0
|
|
281
|
+
otherwise when k == "char" and ev["ctrl"] and ev["text"] == "w"
|
|
282
|
+
let ws be word_start(st["buf"], st["cur"])
|
|
283
|
+
set st["buf"] to slice(st["buf"], 0, ws) + slice(st["buf"], st["cur"], length(st["buf"]))
|
|
284
|
+
set st["cur"] to ws
|
|
285
|
+
otherwise when k == "char" and ev["ctrl"] and ev["text"] == "o"
|
|
286
|
+
set result to "/out"
|
|
287
|
+
set done to true
|
|
288
|
+
otherwise when k == "char" and ev["ctrl"] and ev["text"] == "l"
|
|
289
|
+
term_write(h, ESC + "[2J" + ESC + "[H")
|
|
290
|
+
set st["cursor_row"] to 0
|
|
291
|
+
set changed to false
|
|
292
|
+
otherwise when k == "char" and not ev["ctrl"]
|
|
293
|
+
set st to insert(st, ev["text"])
|
|
294
|
+
otherwise
|
|
295
|
+
set changed to false
|
|
296
|
+
when changed
|
|
297
|
+
set st["menu_off"] to false
|
|
298
|
+
set st to refresh_menu(st, ctx)
|
|
299
|
+
when not done and dirty
|
|
300
|
+
set st to draw(h, st, color, ctx)
|
|
301
|
+
-- dejar el prompt limpio (sin menú) en el scrollback y bajar a una línea nueva
|
|
302
|
+
set st["menu"] to false
|
|
303
|
+
set st to draw(h, st, color, ctx)
|
|
304
|
+
let lines be split(st["buf"], "\n")
|
|
305
|
+
let size be term_size(h)
|
|
306
|
+
let tail be 0
|
|
307
|
+
let i be 0
|
|
308
|
+
each l in lines
|
|
309
|
+
let pre be when i == 0 then vis(st["prompt"]) otherwise 4
|
|
310
|
+
when i > 0 or true
|
|
311
|
+
set tail to tail + rows_of(pre + length(l), size["cols"])
|
|
312
|
+
set i to i + 1
|
|
313
|
+
let down be tail - st["cursor_row"] - 1
|
|
314
|
+
term_write(h, (when down > 0 then ESC + "[" + text(down) + "B" otherwise "") + "\r\n")
|
|
315
|
+
term_close(h)
|
|
316
|
+
give result
|
|
317
|
+
|
|
318
|
+
-- menú vertical de opciones (aprobaciones): ↑↓ + Enter, o la letra inicial / número; Esc = nothing
|
|
319
|
+
export task choose(question, options, color)
|
|
320
|
+
let h be term_open({"ctrl_c": "exit"})
|
|
321
|
+
when h == nothing
|
|
322
|
+
give -1
|
|
323
|
+
let sel be 0
|
|
324
|
+
let done be false
|
|
325
|
+
let result be nothing
|
|
326
|
+
let drawn be 0
|
|
327
|
+
while not done
|
|
328
|
+
let out be ""
|
|
329
|
+
when drawn > 0
|
|
330
|
+
set out to out + ESC + "[" + text(drawn) + "A"
|
|
331
|
+
set out to out + "\r" + ESC + "[J" + question
|
|
332
|
+
let i be 0
|
|
333
|
+
each o in options
|
|
334
|
+
set out to out + "\r\n" + " " + (when i == sel then sgr(color, "7", " " + o + " ") otherwise " " + sgr(color, "2", o))
|
|
335
|
+
set i to i + 1
|
|
336
|
+
term_write(h, out)
|
|
337
|
+
set drawn to length(options)
|
|
338
|
+
let ev be term_recv(h, 600)
|
|
339
|
+
when ev == nothing
|
|
340
|
+
set result to nothing
|
|
341
|
+
set done to true
|
|
342
|
+
otherwise when ev["type"] == "eof"
|
|
343
|
+
set done to true
|
|
344
|
+
otherwise when ev["type"] == "key"
|
|
345
|
+
let k be ev["key"]
|
|
346
|
+
when k == "up"
|
|
347
|
+
set sel to (when sel > 0 then sel - 1 otherwise length(options) - 1)
|
|
348
|
+
otherwise when k == "down" or k == "tab"
|
|
349
|
+
set sel to (when sel < length(options) - 1 then sel + 1 otherwise 0)
|
|
350
|
+
otherwise when k == "enter"
|
|
351
|
+
set result to sel
|
|
352
|
+
set done to true
|
|
353
|
+
otherwise when k == "escape"
|
|
354
|
+
set done to true
|
|
355
|
+
otherwise when k == "char" and not ev["ctrl"]
|
|
356
|
+
let t be lower(ev["text"])
|
|
357
|
+
let j be 0
|
|
358
|
+
each o in options
|
|
359
|
+
when t == text(j + 1) or t == lower(slice(o, 0, 1))
|
|
360
|
+
set result to j
|
|
361
|
+
set done to true
|
|
362
|
+
set j to j + 1
|
|
363
|
+
term_write(h, "\r\n")
|
|
364
|
+
term_close(h)
|
|
365
|
+
give result
|
package/lib/md.syn
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
-- lib/md.syn — markdown → ANSI para la terminal (sin dependencias)
|
|
2
|
+
-- render(md, color) → texto listo para print. color=false → texto plano legible (sin escapes).
|
|
3
|
+
-- inline(s, color) → solo formato inline (negrita, cursiva, `código`, links, ~~tachado~~).
|
|
4
|
+
-- MIGA: `matches` es full-match; para buscar/reemplazar son `capture`/`find_all`/`replace_re`
|
|
5
|
+
-- (backrefs \1). En strings "..." la barra va simple ("\*", "\d"): "\\d" NO es \d.
|
|
6
|
+
-- Los bloques ``` se copian tal cual (sin parseo inline). La web ya renderiza markdown por su cuenta.
|
|
7
|
+
|
|
8
|
+
let ESC be decode(bytes("1b", "hex"))
|
|
9
|
+
let WIDTH be 72
|
|
10
|
+
|
|
11
|
+
task sgr(color, code, s)
|
|
12
|
+
when not color or s == ""
|
|
13
|
+
give s
|
|
14
|
+
give ESC + "[" + code + "m" + s + ESC + "[0m"
|
|
15
|
+
|
|
16
|
+
task rep(ch, n)
|
|
17
|
+
let out be ""
|
|
18
|
+
while length(out) < n
|
|
19
|
+
set out to out + ch
|
|
20
|
+
give out
|
|
21
|
+
|
|
22
|
+
-- ---------- inline ----------
|
|
23
|
+
|
|
24
|
+
task emphasis(s, color)
|
|
25
|
+
let out be s
|
|
26
|
+
-- links [texto](url) → texto url (antes que la cursiva: la url puede traer _ o *)
|
|
27
|
+
set out to replace_re(out, "\[([^\]]+)\]\(([^)]+)\)", sgr(color, "4", "\1") + sgr(color, "2", " \2"))
|
|
28
|
+
set out to replace_re(out, "\*\*([^*]+)\*\*", sgr(color, "1", "\1"))
|
|
29
|
+
set out to replace_re(out, "__([^_]+)__", sgr(color, "1", "\1"))
|
|
30
|
+
set out to replace_re(out, "~~([^~]+)~~", sgr(color, "9", "\1"))
|
|
31
|
+
-- cursiva: *x* solo si no está pegada a texto/número (2*3 no es cursiva). Dos pasadas:
|
|
32
|
+
-- el grupo \3 consume el separador, así que "*a* *b*" necesita la segunda.
|
|
33
|
+
let pass be 0
|
|
34
|
+
while pass < 2
|
|
35
|
+
set out to replace_re(out, "(^|[^\w*])\*([^*\s][^*]*?)\*($|[^\w*])", "\1" + sgr(color, "3", "\2") + "\3")
|
|
36
|
+
set out to replace_re(out, "(^|[^\w_])_([^_\s][^_]*?)_($|[^\w_])", "\1" + sgr(color, "3", "\2") + "\3")
|
|
37
|
+
set pass to pass + 1
|
|
38
|
+
give out
|
|
39
|
+
|
|
40
|
+
export task inline(s, color)
|
|
41
|
+
-- `código` primero: lo de adentro no se toca
|
|
42
|
+
let parts be split(s, "`")
|
|
43
|
+
when length(parts) < 3
|
|
44
|
+
give emphasis(s, color)
|
|
45
|
+
let out be ""
|
|
46
|
+
let idx be 0
|
|
47
|
+
each p in parts
|
|
48
|
+
when idx == length(parts) - 1 and idx % 2 == 1
|
|
49
|
+
set out to out + "`" + emphasis(p, color)
|
|
50
|
+
otherwise when idx % 2 == 1
|
|
51
|
+
set out to out + sgr(color, "33", p)
|
|
52
|
+
otherwise
|
|
53
|
+
set out to out + emphasis(p, color)
|
|
54
|
+
set idx to idx + 1
|
|
55
|
+
give out
|
|
56
|
+
|
|
57
|
+
-- ---------- bloques ----------
|
|
58
|
+
|
|
59
|
+
task indent_of(line)
|
|
60
|
+
let n be 0
|
|
61
|
+
while n < length(line) and slice(line, n, n + 1) == " "
|
|
62
|
+
set n to n + 1
|
|
63
|
+
give n
|
|
64
|
+
|
|
65
|
+
task table_cells(line)
|
|
66
|
+
let t be trim(line)
|
|
67
|
+
when starts_with(t, "|")
|
|
68
|
+
set t to slice(t, 1, length(t))
|
|
69
|
+
when ends_with(t, "|")
|
|
70
|
+
set t to slice(t, 0, length(t) - 1)
|
|
71
|
+
give apply(trim, split(t, "|"))
|
|
72
|
+
|
|
73
|
+
-- rows: lista de listas de celdas (crudas); la primera es el header. Columnas alineadas al ancho
|
|
74
|
+
-- máximo de cada una (ancho visible = length de inline(c, false), sin escapes ni marcas).
|
|
75
|
+
task table_lines(rows, pre, color)
|
|
76
|
+
let widths be []
|
|
77
|
+
each r in rows
|
|
78
|
+
let i be 0
|
|
79
|
+
each c in r
|
|
80
|
+
when i >= length(widths)
|
|
81
|
+
set widths to append(widths, 0)
|
|
82
|
+
when length(inline(c, false)) > widths[i]
|
|
83
|
+
set widths[i] to length(inline(c, false))
|
|
84
|
+
set i to i + 1
|
|
85
|
+
let out be []
|
|
86
|
+
let ri be 0
|
|
87
|
+
each r in rows
|
|
88
|
+
let cells be []
|
|
89
|
+
let i be 0
|
|
90
|
+
each c in r
|
|
91
|
+
let fill be rep(" ", widths[i] - length(inline(c, false)))
|
|
92
|
+
set cells to append(cells, (when ri == 0 then sgr(color, "1", inline(c, color)) otherwise inline(c, color)) + fill)
|
|
93
|
+
set i to i + 1
|
|
94
|
+
set out to append(out, pre + join(cells, sgr(color, "2", " │ ")))
|
|
95
|
+
when ri == 0
|
|
96
|
+
let segs be []
|
|
97
|
+
each w in widths
|
|
98
|
+
set segs to append(segs, rep("─", w))
|
|
99
|
+
set out to append(out, pre + sgr(color, "2", join(segs, "─┼─")))
|
|
100
|
+
set ri to ri + 1
|
|
101
|
+
give out
|
|
102
|
+
|
|
103
|
+
task fence_top(lang, color)
|
|
104
|
+
let label be when lang != "" then " " + lang + " " otherwise ""
|
|
105
|
+
give sgr(color, "2", "┌──" + label + rep("─", WIDTH - 3 - length(label)))
|
|
106
|
+
|
|
107
|
+
export task render(md, color)
|
|
108
|
+
let pre be " "
|
|
109
|
+
let out be []
|
|
110
|
+
let in_code be false
|
|
111
|
+
let fence be ""
|
|
112
|
+
let table be []
|
|
113
|
+
each line in split(md, "\n")
|
|
114
|
+
let t be trim(line)
|
|
115
|
+
let heading be capture(t, "^(#{1,6}) (.+)$")
|
|
116
|
+
let item be capture(t, "^(\d{1,3})[.)] (.*)$")
|
|
117
|
+
let is_row be starts_with(t, "|") and ends_with(t, "|") and not in_code
|
|
118
|
+
when length(table) > 0 and not is_row
|
|
119
|
+
each l in table_lines(table, pre, color)
|
|
120
|
+
set out to append(out, l)
|
|
121
|
+
set table to []
|
|
122
|
+
when in_code
|
|
123
|
+
when starts_with(t, fence)
|
|
124
|
+
set in_code to false
|
|
125
|
+
set out to append(out, pre + sgr(color, "2", "└" + rep("─", WIDTH - 1)))
|
|
126
|
+
otherwise
|
|
127
|
+
set out to append(out, pre + sgr(color, "2", "│ ") + sgr(color, "36", line))
|
|
128
|
+
otherwise when starts_with(t, "```") or starts_with(t, "~~~")
|
|
129
|
+
set in_code to true
|
|
130
|
+
set fence to slice(t, 0, 3)
|
|
131
|
+
set out to append(out, pre + fence_top(trim(slice(t, 3, length(t))), color))
|
|
132
|
+
otherwise when heading != nothing
|
|
133
|
+
let lvl be length(heading[0])
|
|
134
|
+
let raw be trim(heading[1])
|
|
135
|
+
let title be inline(raw, color)
|
|
136
|
+
set out to append(out, "")
|
|
137
|
+
when lvl == 1
|
|
138
|
+
set out to append(out, pre + sgr(color, "1;36", upper(title)))
|
|
139
|
+
set out to append(out, pre + sgr(color, "36", rep("═", length(raw))))
|
|
140
|
+
otherwise when lvl == 2
|
|
141
|
+
set out to append(out, pre + sgr(color, "1;36", title))
|
|
142
|
+
set out to append(out, pre + sgr(color, "2;36", rep("─", length(raw))))
|
|
143
|
+
otherwise
|
|
144
|
+
set out to append(out, pre + sgr(color, "1", title))
|
|
145
|
+
otherwise when matches(t, "(-{3,}|\*{3,}|_{3,})")
|
|
146
|
+
set out to append(out, pre + sgr(color, "2", rep("─", WIDTH)))
|
|
147
|
+
otherwise when starts_with(t, ">")
|
|
148
|
+
set out to append(out, pre + sgr(color, "2", "▎ ") + sgr(color, "3", inline(trim(slice(t, 1, length(t))), color)))
|
|
149
|
+
otherwise when matches(t, "\|[\s:|-]+\|")
|
|
150
|
+
set table to table
|
|
151
|
+
otherwise when is_row
|
|
152
|
+
set table to append(table, table_cells(t))
|
|
153
|
+
otherwise when starts_with(t, "- ") or starts_with(t, "* ") or starts_with(t, "+ ") or item != nothing
|
|
154
|
+
let ind be rep(" ", indent_of(line))
|
|
155
|
+
let mark be when item != nothing then sgr(color, "36", item[0] + ".") otherwise sgr(color, "36", "•")
|
|
156
|
+
let rest be when item != nothing then item[1] otherwise slice(t, 2, length(t))
|
|
157
|
+
when starts_with(rest, "[ ] ")
|
|
158
|
+
set mark to sgr(color, "2", "☐")
|
|
159
|
+
set rest to slice(rest, 4, length(rest))
|
|
160
|
+
otherwise when starts_with(rest, "[x] ") or starts_with(rest, "[X] ")
|
|
161
|
+
set mark to sgr(color, "32", "☑")
|
|
162
|
+
set rest to slice(rest, 4, length(rest))
|
|
163
|
+
set out to append(out, pre + ind + mark + " " + inline(rest, color))
|
|
164
|
+
otherwise
|
|
165
|
+
set out to append(out, when t == "" then "" otherwise pre + inline(line, color))
|
|
166
|
+
when length(table) > 0
|
|
167
|
+
each l in table_lines(table, pre, color)
|
|
168
|
+
set out to append(out, l)
|
|
169
|
+
when in_code
|
|
170
|
+
set out to append(out, pre + sgr(color, "2", "└" + rep("─", WIDTH - 1)))
|
|
171
|
+
give join(out, "\n")
|
package/lib/permission.syn
CHANGED
|
@@ -149,6 +149,41 @@ export task describe_call(name, args)
|
|
|
149
149
|
give "skill install " + text(args["source"]) + " --skill " + text(args["name"]) + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"
|
|
150
150
|
when name == "bash"
|
|
151
151
|
give "$ " + one_line(when contains(args, "command") then text(args["command"]) otherwise "", 160)
|
|
152
|
+
-- lectura/búsqueda: como lo escribiría un humano en la shell
|
|
153
|
+
when name == "read"
|
|
154
|
+
let rng be when contains(args, "offset") or contains(args, "limit") then " (" + (when contains(args, "offset") then "desde " + text(floor(number(text(args["offset"])))) otherwise "") + (when contains(args, "limit") then " " + text(floor(number(text(args["limit"])))) + " líneas" otherwise "") + ")" otherwise ""
|
|
155
|
+
give "read " + arg(args, "path", ".") + rng
|
|
156
|
+
when name == "ls"
|
|
157
|
+
give "ls " + arg(args, "path", ".")
|
|
158
|
+
when name == "find"
|
|
159
|
+
give "find " + arg(args, "pattern", "*") + (when contains(args, "path") then " en " + text(args["path"]) otherwise "")
|
|
160
|
+
when name == "grep"
|
|
161
|
+
let flags be (when contains(args, "regex") and args["regex"] == true then " -E" otherwise "") + (when contains(args, "glob") then " --glob " + text(args["glob"]) otherwise "")
|
|
162
|
+
give "grep" + flags + " \"" + one_line(arg(args, "pattern", ""), 80) + "\"" + (when contains(args, "path") then " " + text(args["path"]) otherwise "")
|
|
163
|
+
when name == "edit"
|
|
164
|
+
give "edit " + arg(args, "path", "?")
|
|
165
|
+
when name == "write"
|
|
166
|
+
give "write " + arg(args, "path", "?") + (when contains(args, "content") then dim_len(text(args["content"])) otherwise "")
|
|
167
|
+
when name == "memory"
|
|
168
|
+
let mact be arg(args, "action", "list")
|
|
169
|
+
give "memory " + mact + (when contains(args, "name") then " " + text(args["name"]) otherwise "") + (when (mact == "write" or mact == "append") and contains(args, "content") then " — " + one_line(text(args["content"]), 70) otherwise "")
|
|
170
|
+
when name == "process"
|
|
171
|
+
let pact be arg(args, "action", "list")
|
|
172
|
+
give "process " + pact + (when contains(args, "name") then " " + text(args["name"]) otherwise "") + (when contains(args, "command") then " $ " + one_line(text(args["command"]), 100) otherwise "")
|
|
173
|
+
when name == "skill"
|
|
174
|
+
give "skill " + arg(args, "action", "load") + " " + arg(args, "name", "?")
|
|
175
|
+
when name == "todo"
|
|
176
|
+
let n be when contains(args, "items") then length(args["items"]) otherwise 0
|
|
177
|
+
let done be when contains(args, "items") then length(where(args["items"], (i) => contains(i, "status") and text(i["status"]) == "done")) otherwise 0
|
|
178
|
+
give "todo " + text(done) + "/" + text(n) + " hechas"
|
|
152
179
|
when contains(args, "path")
|
|
153
180
|
give name + " " + text(args["path"])
|
|
154
|
-
give name + " " + json_encode(args)
|
|
181
|
+
give name + " " + one_line(json_encode(args), 160)
|
|
182
|
+
|
|
183
|
+
task arg(args, key, default)
|
|
184
|
+
when contains(args, key)
|
|
185
|
+
give text(args[key])
|
|
186
|
+
give default
|
|
187
|
+
|
|
188
|
+
task dim_len(s)
|
|
189
|
+
give " (" + text(length(split(s, "\n"))) + " líneas)"
|
package/lib/tools/edit.syn
CHANGED
|
@@ -16,8 +16,11 @@ export task tool(path, old_string, new_string, replace_all)
|
|
|
16
16
|
when n > 1
|
|
17
17
|
when not all
|
|
18
18
|
raise(`old_string appears {text(n)} times in {shown}; include more surrounding context to make it unique, or pass replace_all=true`)
|
|
19
|
-
|
|
19
|
+
let after be join(parts, new_string)
|
|
20
|
+
write_file(real, after)
|
|
20
21
|
c.mark_observed(real)
|
|
22
|
+
-- antes/después para que la UI muestre el diff (chat.syn / web); el modelo solo recibe la línea de abajo
|
|
23
|
+
share {"path": shown, "old": content, "new": after} as "lampson:ui:diff"
|
|
21
24
|
give `edited {shown}: {text(n)} replacement(s)`
|
|
22
25
|
|
|
23
26
|
export let SPEC be {
|
package/lib/tools/write.syn
CHANGED
|
@@ -6,10 +6,14 @@ export task tool(path, content)
|
|
|
6
6
|
require file("workspace/*")
|
|
7
7
|
let real be c.ws(path)
|
|
8
8
|
-- sobrescribir un archivo existente exige haberlo leído (y que no haya cambiado): igual que edit
|
|
9
|
+
let before be ""
|
|
9
10
|
when file_exists(real)
|
|
10
11
|
c.check_observed(real, "write")
|
|
12
|
+
set before to read_file(real)
|
|
11
13
|
write_file(real, content)
|
|
12
14
|
c.mark_observed(real)
|
|
15
|
+
-- antes/después para que la UI muestre el diff (archivo nuevo: before = "")
|
|
16
|
+
share {"path": c.unws(real), "old": before, "new": content} as "lampson:ui:diff"
|
|
13
17
|
give `wrote {text(length(content))} chars to {c.unws(real)}`
|
|
14
18
|
|
|
15
19
|
export let SPEC be {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lampson",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "A coding agent written in Synsema: terminal + web, tools confined to the mounted project, lamps (your own tool plugins), LSP, MCP, sub-agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"node": ">=18"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"synsema": "^0.6.
|
|
35
|
+
"synsema": "^0.6.11"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"test": "pwsh -NoProfile -File tests/run.ps1"
|
package/skills/synsema/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: synsema
|
|
|
3
3
|
description: Writing, checking, running and testing Synsema (.syn) code — syntax reflexes, capabilities, live processes / pseudo-terminals, and the runtime traps that cost hours. Load before touching any .syn file.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Synsema quick reference (v0.6.
|
|
6
|
+
# Synsema quick reference (v0.6.11)
|
|
7
7
|
|
|
8
8
|
> Curated 10 KB summary for the agent (the full reference is ~450 KB and lives in the user's editor
|
|
9
9
|
> skill). Kept in sync by hand with each `synsema update`; if `synsema --version` is newer than the
|
|
@@ -22,6 +22,11 @@ description: Writing, checking, running and testing Synsema (.syn) code — synt
|
|
|
22
22
|
- `try` … `recover err` (recover SWALLOWS; `raise(err)` to re-throw) · `raise("msg")`
|
|
23
23
|
- `contains(xs, x)` (on maps checks KEYS) · `append(xs, x)` returns a NEW list → `set xs to append(xs, x)`
|
|
24
24
|
- `apply(f, xs)`, `where(xs, p)`, `sort_by(xs, f)`, `slice(xs, a, b)`, `split/join/trim/lower/upper`
|
|
25
|
+
- Text/regex: `replace_text(t, old, new)` (literal), `replace_re(t, re, rep)` (`\1` backrefs), `capture(t, re)`
|
|
26
|
+
(first match; with groups → list), `find_all`, `matches` (**full match** only — not a search).
|
|
27
|
+
There is NO `replace`, `regex_replace`, `chars`, `repeat`. Rust regex: no lookahead/lookbehind.
|
|
28
|
+
In `"..."` strings backslashes are literal: write `"\d"`, `"\*"` (`"\\d"` is a literal `\d`).
|
|
29
|
+
Anonymous functions: `(x) => expr` only (no inline `task(x)`).
|
|
25
30
|
- `json_encode / json_decode` · `length` · `text(x)` · `number(s)` (ALWAYS float → `floor()` for ints)
|
|
26
31
|
- Modules: `use "./m.syn" as m` (local only, never `../`), `export task/let`. A module cannot have
|
|
27
32
|
top-level `require` or `serve` — the ENTRY file grants capabilities.
|
|
@@ -50,6 +55,12 @@ description: Writing, checking, running and testing Synsema (.syn) code — synt
|
|
|
50
55
|
`proc_spawn` in a handler is gone when the handler returns. A process that must outlive requests lives
|
|
51
56
|
inside an `agent` spawned from the handler (own lifecycle; blackboard `share/observe` + `bus_*` are shared
|
|
52
57
|
with handlers). That is how lampson's `process` tool works (`lib/tools/proc.syn`).
|
|
58
|
+
- **Own terminal / raw keys (v0.6.11+)**: `let h be term_open({"ctrl_c": "exit"})` → `nothing` without a
|
|
59
|
+
TTY / under `test`/`serve` (fall back to `read_line`); `term_recv(h, secs)` → `{type: "key", key, text,
|
|
60
|
+
ctrl, alt, shift}` (`key` = `"char"|"enter"|"tab"|"backspace"|"up"|…`), `paste`, `resize`, `eof`;
|
|
61
|
+
draw with `term_write(h, ansi)` (**`print` stays buffered**); `term_size(h)`; `term_close(h)`. Alt+Enter
|
|
62
|
+
always arrives (Shift+Enter needs kitty protocol). `ask/approve` still work while open. Lampson's line
|
|
63
|
+
editor is `lib/line.syn`; drive a terminal UI in tests via `proc_spawn(…, {pty: true})` (`tty_test.syn`).
|
|
53
64
|
- **File watch (v0.6.9+)**: `let w be watch("src", {"interval": 0.2, "ignore": ["*.tmp"]})` → events
|
|
54
65
|
`{type: "create"|"modify"|"delete", path, is_dir}` via `watch_recv(w, secs)` or `select`; polling with a
|
|
55
66
|
snapshot (latency = interval), `watch_close(w)`. Gate: `file("src")` + `file("src/*")`.
|
|
@@ -67,7 +78,9 @@ description: Writing, checking, running and testing Synsema (.syn) code — synt
|
|
|
67
78
|
- `localhost` resolves to IPv6; use `127.0.0.1`.
|
|
68
79
|
- `run("bash", ...)` hangs on Windows (WSL bash) → `C:\Program Files\Git\bin\bash.exe` or `cmd /c`.
|
|
69
80
|
- A task named `run` shadows the builtin `run` (infinite recursion).
|
|
70
|
-
- Reserved words that break variable/param names: `reason task ask stop decide analyze generate show approve confirm`.
|
|
81
|
+
- Reserved words that break variable/param names: `reason task ask stop type run decide analyze generate show approve confirm`.
|
|
82
|
+
- Reading a MISSING map key is a runtime error (`Map has no key 'x'`), not `nothing` → `contains(m, "x")` first.
|
|
83
|
+
- Maps are passed by reference: `set m["k"] to v` inside a task IS visible to the caller (lists via `append` are not — it returns a new list).
|
|
71
84
|
- `and`/`or` do NOT short-circuit → nest `when` before indexing.
|
|
72
85
|
- No `merge`: add a key with `set m["k"] to v`. No `append_file`: read + write (atomic; parents created).
|
|
73
86
|
- Runtime error messages are Capitalized (`Not a directory: …`) and `contains` is case-sensitive → compare `lower(text(err))`.
|