lampson 0.1.1 → 0.1.3
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 +17 -1
- package/chat.syn +387 -25
- package/lib/diff.syn +168 -0
- package/lib/line.syn +370 -0
- package/lib/loop.syn +25 -11
- package/lib/md.syn +229 -0
- package/lib/permission.syn +36 -1
- package/lib/prompt.syn +1 -1
- package/lib/tools/edit.syn +4 -1
- package/lib/tools/write.syn +4 -0
- package/lib/ui.syn +161 -0
- package/package.json +2 -2
- package/public/index.html +11 -1
- package/skills/synsema/SKILL.md +15 -2
package/lib/md.syn
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
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
|
+
use "./ui.syn" as ui
|
|
9
|
+
|
|
10
|
+
let ESC be decode(bytes("1b", "hex"))
|
|
11
|
+
-- ancho útil: columnas de la terminal menos la sangría (mín. 40, máx. 100 para que la prosa se lea)
|
|
12
|
+
task width_now()
|
|
13
|
+
let c be ui.cols() - 4
|
|
14
|
+
when c < 40
|
|
15
|
+
give 40
|
|
16
|
+
when c > 100
|
|
17
|
+
give 100
|
|
18
|
+
give c
|
|
19
|
+
|
|
20
|
+
-- rutas y símbolos de código dentro de la prosa → azul (lib/x.syn, src/a/b.ts, describe_call(), foo.bar)
|
|
21
|
+
task paths(s, color)
|
|
22
|
+
when not color
|
|
23
|
+
give s
|
|
24
|
+
let out be replace_re(s, "(^|[\s(])([\w.-]*/[\w./-]{2,})", "\1" + ESC + "[34m\2" + ESC + "[0m")
|
|
25
|
+
set out to replace_re(out, "(^|[\s(])(\w+\.(syn|js|ts|tsx|jsx|json|md|py|rs|go|ps1|sh|html|css|toml|yml|yaml|env))(\b)", "\1" + ESC + "[34m\2" + ESC + "[0m")
|
|
26
|
+
set out to replace_re(out, "(^|[\s(])(\w+\(\))", "\1" + ESC + "[34m\2" + ESC + "[0m")
|
|
27
|
+
give out
|
|
28
|
+
|
|
29
|
+
task sgr(color, code, s)
|
|
30
|
+
when not color or s == ""
|
|
31
|
+
give s
|
|
32
|
+
give ESC + "[" + code + "m" + s + ESC + "[0m"
|
|
33
|
+
|
|
34
|
+
task rep(ch, n)
|
|
35
|
+
let out be ""
|
|
36
|
+
while length(out) < n
|
|
37
|
+
set out to out + ch
|
|
38
|
+
give out
|
|
39
|
+
|
|
40
|
+
-- ---------- inline ----------
|
|
41
|
+
|
|
42
|
+
task emphasis(s, color)
|
|
43
|
+
let out be s
|
|
44
|
+
-- links [texto](url) → texto url (antes que la cursiva: la url puede traer _ o *)
|
|
45
|
+
set out to replace_re(out, "\[([^\]]+)\]\(([^)]+)\)", sgr(color, "4", "\1") + sgr(color, "2", " \2"))
|
|
46
|
+
set out to replace_re(out, "\*\*([^*]+)\*\*", sgr(color, "1", "\1"))
|
|
47
|
+
set out to replace_re(out, "__([^_]+)__", sgr(color, "1", "\1"))
|
|
48
|
+
set out to replace_re(out, "~~([^~]+)~~", sgr(color, "9", "\1"))
|
|
49
|
+
-- cursiva: *x* solo si no está pegada a texto/número (2*3 no es cursiva). Dos pasadas:
|
|
50
|
+
-- el grupo \3 consume el separador, así que "*a* *b*" necesita la segunda.
|
|
51
|
+
let pass be 0
|
|
52
|
+
while pass < 2
|
|
53
|
+
set out to replace_re(out, "(^|[^\w*])\*([^*\s][^*]*?)\*($|[^\w*])", "\1" + sgr(color, "3", "\2") + "\3")
|
|
54
|
+
set out to replace_re(out, "(^|[^\w_])_([^_\s][^_]*?)_($|[^\w_])", "\1" + sgr(color, "3", "\2") + "\3")
|
|
55
|
+
set pass to pass + 1
|
|
56
|
+
give out
|
|
57
|
+
|
|
58
|
+
export task inline(s, color)
|
|
59
|
+
-- `código` primero: lo de adentro no se toca
|
|
60
|
+
let parts be split(s, "`")
|
|
61
|
+
when length(parts) < 3
|
|
62
|
+
give paths(emphasis(s, color), color)
|
|
63
|
+
let out be ""
|
|
64
|
+
let idx be 0
|
|
65
|
+
each p in parts
|
|
66
|
+
when idx == length(parts) - 1 and idx % 2 == 1
|
|
67
|
+
set out to out + "`" + paths(emphasis(p, color), color)
|
|
68
|
+
otherwise when idx % 2 == 1
|
|
69
|
+
set out to out + sgr(color, "36", p)
|
|
70
|
+
otherwise
|
|
71
|
+
set out to out + paths(emphasis(p, color), color)
|
|
72
|
+
set idx to idx + 1
|
|
73
|
+
give out
|
|
74
|
+
|
|
75
|
+
-- ---------- bloques ----------
|
|
76
|
+
|
|
77
|
+
task indent_of(line)
|
|
78
|
+
let n be 0
|
|
79
|
+
while n < length(line) and slice(line, n, n + 1) == " "
|
|
80
|
+
set n to n + 1
|
|
81
|
+
give n
|
|
82
|
+
|
|
83
|
+
task table_cells(line)
|
|
84
|
+
let t be trim(line)
|
|
85
|
+
when starts_with(t, "|")
|
|
86
|
+
set t to slice(t, 1, length(t))
|
|
87
|
+
when ends_with(t, "|")
|
|
88
|
+
set t to slice(t, 0, length(t) - 1)
|
|
89
|
+
give apply(trim, split(t, "|"))
|
|
90
|
+
|
|
91
|
+
-- rows: lista de listas de celdas (crudas); la primera es el header. Columnas alineadas al ancho
|
|
92
|
+
-- máximo de cada una (ancho visible = length de inline(c, false), sin escapes ni marcas).
|
|
93
|
+
task table_lines(rows, pre, color)
|
|
94
|
+
let widths be []
|
|
95
|
+
each r in rows
|
|
96
|
+
let i be 0
|
|
97
|
+
each c in r
|
|
98
|
+
when i >= length(widths)
|
|
99
|
+
set widths to append(widths, 0)
|
|
100
|
+
when ui.width(inline(c, false)) > widths[i]
|
|
101
|
+
set widths[i] to ui.width(inline(c, false))
|
|
102
|
+
set i to i + 1
|
|
103
|
+
-- si no entra en la terminal, achicar la columna más ancha hasta que entre (mínimo 10) y envolver
|
|
104
|
+
let avail be width_now() - 3 * (length(widths) - 1)
|
|
105
|
+
let total be 0
|
|
106
|
+
each w in widths
|
|
107
|
+
set total to total + w
|
|
108
|
+
while total > avail
|
|
109
|
+
let widest be 0
|
|
110
|
+
let wi be 0
|
|
111
|
+
each w in widths
|
|
112
|
+
when w > widths[widest]
|
|
113
|
+
set widest to wi
|
|
114
|
+
set wi to wi + 1
|
|
115
|
+
when widths[widest] <= 10
|
|
116
|
+
set total to avail
|
|
117
|
+
otherwise
|
|
118
|
+
set widths[widest] to widths[widest] - 1
|
|
119
|
+
set total to total - 1
|
|
120
|
+
let out be []
|
|
121
|
+
let ri be 0
|
|
122
|
+
each r in rows
|
|
123
|
+
-- cada celda → lista de líneas envueltas a su ancho
|
|
124
|
+
let wrapped be []
|
|
125
|
+
let height be 1
|
|
126
|
+
let i be 0
|
|
127
|
+
each c in r
|
|
128
|
+
let ls be ui.wrap(c, widths[i], "")
|
|
129
|
+
set wrapped to append(wrapped, ls)
|
|
130
|
+
when length(ls) > height
|
|
131
|
+
set height to length(ls)
|
|
132
|
+
set i to i + 1
|
|
133
|
+
let li be 0
|
|
134
|
+
while li < height
|
|
135
|
+
let cells be []
|
|
136
|
+
set i to 0
|
|
137
|
+
each ls in wrapped
|
|
138
|
+
let piece be when li < length(ls) then ls[li] otherwise ""
|
|
139
|
+
let vis be ui.width(inline(piece, false))
|
|
140
|
+
let fill be rep(" ", (when widths[i] > vis then widths[i] - vis otherwise 0))
|
|
141
|
+
set cells to append(cells, (when ri == 0 then sgr(color, "1", inline(piece, color)) otherwise inline(piece, color)) + fill)
|
|
142
|
+
set i to i + 1
|
|
143
|
+
set out to append(out, pre + join(cells, sgr(color, "2", " │ ")))
|
|
144
|
+
set li to li + 1
|
|
145
|
+
when ri == 0
|
|
146
|
+
let segs be []
|
|
147
|
+
each w in widths
|
|
148
|
+
set segs to append(segs, rep("─", w))
|
|
149
|
+
set out to append(out, pre + sgr(color, "2", join(segs, "─┼─")))
|
|
150
|
+
set ri to ri + 1
|
|
151
|
+
give out
|
|
152
|
+
|
|
153
|
+
task fence_top(lang, color)
|
|
154
|
+
let label be when lang != "" then " " + lang + " " otherwise ""
|
|
155
|
+
give sgr(color, "2", "┌──" + label + rep("─", width_now() - 3 - length(label)))
|
|
156
|
+
|
|
157
|
+
export task render(md, color)
|
|
158
|
+
let pre be " "
|
|
159
|
+
let out be []
|
|
160
|
+
let in_code be false
|
|
161
|
+
let fence be ""
|
|
162
|
+
let table be []
|
|
163
|
+
each line in split(md, "\n")
|
|
164
|
+
let t be trim(line)
|
|
165
|
+
let heading be capture(t, "^(#{1,6}) (.+)$")
|
|
166
|
+
let item be capture(t, "^(\d{1,3})[.)] (.*)$")
|
|
167
|
+
let is_row be starts_with(t, "|") and ends_with(t, "|") and not in_code
|
|
168
|
+
when length(table) > 0 and not is_row
|
|
169
|
+
each l in table_lines(table, pre, color)
|
|
170
|
+
set out to append(out, l)
|
|
171
|
+
set table to []
|
|
172
|
+
when in_code
|
|
173
|
+
when starts_with(t, fence)
|
|
174
|
+
set in_code to false
|
|
175
|
+
set out to append(out, pre + sgr(color, "2", "└" + rep("─", width_now() - 1)))
|
|
176
|
+
otherwise
|
|
177
|
+
set out to append(out, pre + sgr(color, "2", "│ ") + ui.cut(sgr(color, "36", line), width_now() - 2))
|
|
178
|
+
otherwise when starts_with(t, "```") or starts_with(t, "~~~")
|
|
179
|
+
set in_code to true
|
|
180
|
+
set fence to slice(t, 0, 3)
|
|
181
|
+
set out to append(out, pre + fence_top(trim(slice(t, 3, length(t))), color))
|
|
182
|
+
otherwise when heading != nothing
|
|
183
|
+
let lvl be length(heading[0])
|
|
184
|
+
let raw be trim(heading[1])
|
|
185
|
+
let title be inline(raw, color)
|
|
186
|
+
set out to append(out, "")
|
|
187
|
+
when lvl == 1
|
|
188
|
+
set out to append(out, pre + sgr(color, "1;36", upper(title)))
|
|
189
|
+
set out to append(out, pre + sgr(color, "36", rep("═", length(raw))))
|
|
190
|
+
otherwise when lvl == 2
|
|
191
|
+
set out to append(out, pre + sgr(color, "1;36", title))
|
|
192
|
+
set out to append(out, pre + sgr(color, "2;36", rep("─", length(raw))))
|
|
193
|
+
otherwise
|
|
194
|
+
set out to append(out, pre + sgr(color, "1", title))
|
|
195
|
+
otherwise when matches(t, "(-{3,}|\*{3,}|_{3,})")
|
|
196
|
+
set out to append(out, pre + sgr(color, "2", rep("─", width_now())))
|
|
197
|
+
otherwise when starts_with(t, ">")
|
|
198
|
+
set out to append(out, pre + sgr(color, "2", "▎ ") + sgr(color, "3", inline(trim(slice(t, 1, length(t))), color)))
|
|
199
|
+
otherwise when matches(t, "\|[\s:|-]+\|")
|
|
200
|
+
set table to table
|
|
201
|
+
otherwise when is_row
|
|
202
|
+
set table to append(table, table_cells(t))
|
|
203
|
+
otherwise when starts_with(t, "- ") or starts_with(t, "* ") or starts_with(t, "+ ") or item != nothing
|
|
204
|
+
let ind be rep(" ", indent_of(line))
|
|
205
|
+
let mark be when item != nothing then sgr(color, "36", item[0] + ".") otherwise sgr(color, "36", "•")
|
|
206
|
+
let rest be when item != nothing then item[1] otherwise slice(t, 2, length(t))
|
|
207
|
+
when starts_with(rest, "[ ] ")
|
|
208
|
+
set mark to sgr(color, "2", "☐")
|
|
209
|
+
set rest to slice(rest, 4, length(rest))
|
|
210
|
+
otherwise when starts_with(rest, "[x] ") or starts_with(rest, "[X] ")
|
|
211
|
+
set mark to sgr(color, "32", "☑")
|
|
212
|
+
set rest to slice(rest, 4, length(rest))
|
|
213
|
+
let wrapped be ui.wrap(rest, width_now() - length(ind) - 2, "")
|
|
214
|
+
let wi be 0
|
|
215
|
+
each wl in wrapped
|
|
216
|
+
set out to append(out, pre + ind + (when wi == 0 then mark + " " otherwise " ") + inline(wl, color))
|
|
217
|
+
set wi to wi + 1
|
|
218
|
+
otherwise
|
|
219
|
+
when t == ""
|
|
220
|
+
set out to append(out, "")
|
|
221
|
+
otherwise
|
|
222
|
+
each wl in ui.wrap(trim(line), width_now(), "")
|
|
223
|
+
set out to append(out, pre + inline(wl, color))
|
|
224
|
+
when length(table) > 0
|
|
225
|
+
each l in table_lines(table, pre, color)
|
|
226
|
+
set out to append(out, l)
|
|
227
|
+
when in_code
|
|
228
|
+
set out to append(out, pre + sgr(color, "2", "└" + rep("─", width_now() - 1)))
|
|
229
|
+
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/prompt.syn
CHANGED
|
@@ -35,7 +35,7 @@ Operate like a careful senior engineer: precise, honest, and economical with wor
|
|
|
35
35
|
# Method — follow these five steps on every non-trivial request
|
|
36
36
|
1. PLAN. If the task needs 3+ distinct steps (not 3 tool calls for one conceptual step), call todo FIRST and keep it current: one item in_progress at a time, marked completed the moment its work is verified — never batched, never on intent. The todo list survives context compaction; your narration does not. Skip it for a single straightforward change; when in doubt, use it.
|
|
37
37
|
2. LOCATE. Find code with grep and find, never by reading directories at random. For a broad, open-ended question about how the codebase works, delegate ONE focused question to an explore sub-agent instead of reading everything yourself — the search stays out of your context. Batch independent lookups into a single turn.
|
|
38
|
-
3. READ ONLY WHAT YOU NEED. Read the files you will change plus the ones you must understand to change them — not the whole project. Every file you read is re-sent on every later call. Use offset/limit for long files, but avoid tiny repeated slices. Trace a symbol to its definition and usages rather than guessing its shape. Never invent files, symbols, APIs or imports — if you have not seen it in this repo, go look. HARD LIMITS enforced by the harness: after
|
|
38
|
+
3. READ ONLY WHAT YOU NEED. Read the files you will change plus the ones you must understand to change them — not the whole project. Every file you read is re-sent on every later call. Use offset/limit for long files, but avoid tiny repeated slices. Trace a symbol to its definition and usages rather than guessing its shape. Never invent files, symbols, APIs or imports — if you have not seen it in this repo, go look. HARD LIMITS enforced by the harness: after 12 read-only calls in a row without acting you get a warning, after 24 read/ls/find/grep are refused until you act; a file read twice unchanged comes back as a one-line receipt.
|
|
39
39
|
4. CHANGE. Act as soon as you know enough — usually within the first 6-10 reads. Prefer edit (exact-string replacement; old_string must be unique, include surrounding context) over write; write only for new files or full rewrites. edit/write on an existing file are rejected unless you read that file in this session and it has not changed since. Minimal, targeted changes: no drive-by refactors, renames or reformatting; add every import or dependency your code needs. Do NOT print code blocks to the user as a substitute for editing — apply the change, then summarise it. If an edit fails to apply, re-read the file and retry with the exact current text; if the same region fails twice, rewrite the enclosing function with write.
|
|
40
40
|
5. VERIFY AND REPORT. Run the relevant tests, linter or build with bash and confirm they pass before claiming the work is done — AFTER your edits, not before as reconnaissance. Never assume a test command: check the README, the package manifest or the project instructions. Finish with a short summary: what changed (files, path:line for specific code) and how you verified it. Do not narrate every step.
|
|
41
41
|
|
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/lib/ui.syn
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
-- lib/ui.syn — una sola paleta y medidas de texto para la terminal (md.syn, diff.syn, line.syn, chat.syn)
|
|
2
|
+
-- Colores: dim/blue/green/red/yellow/cyan/bold/inv (+ fondos bg_add/bg_del para diffs). color=false → texto plano.
|
|
3
|
+
-- width(s) ancho visible (sin escapes; emoji/CJK cuentan 2)
|
|
4
|
+
-- cut(s, max) corta a `max` columnas respetando escapes (y cierra el color)
|
|
5
|
+
-- wrap(s, max, indent) envuelve texto PLANO por palabras; las líneas siguientes llevan `indent`
|
|
6
|
+
-- cols() columnas de la terminal (el editor las publica en el blackboard "lampson:ui:cols"; 100 si no hay)
|
|
7
|
+
-- fmt_duration(secs) → "12s" · "2m 27s" · "1h 03m"
|
|
8
|
+
|
|
9
|
+
let ESC be decode(bytes("1b", "hex"))
|
|
10
|
+
|
|
11
|
+
export task sgr(color, code, s)
|
|
12
|
+
when not color or s == ""
|
|
13
|
+
give s
|
|
14
|
+
give ESC + "[" + code + "m" + s + ESC + "[0m"
|
|
15
|
+
|
|
16
|
+
export task dim(color, s)
|
|
17
|
+
give sgr(color, "2", s)
|
|
18
|
+
export task bold(color, s)
|
|
19
|
+
give sgr(color, "1", s)
|
|
20
|
+
export task inv(color, s)
|
|
21
|
+
give sgr(color, "7", s)
|
|
22
|
+
export task blue(color, s)
|
|
23
|
+
give sgr(color, "34", s)
|
|
24
|
+
export task cyan(color, s)
|
|
25
|
+
give sgr(color, "36", s)
|
|
26
|
+
export task green(color, s)
|
|
27
|
+
give sgr(color, "32", s)
|
|
28
|
+
export task red(color, s)
|
|
29
|
+
give sgr(color, "31", s)
|
|
30
|
+
export task yellow(color, s)
|
|
31
|
+
give sgr(color, "33", s)
|
|
32
|
+
-- fondos para diffs (256 colores: verde y rojo oscuros, legibles en tema claro y oscuro)
|
|
33
|
+
export task bg_add(color, s)
|
|
34
|
+
give sgr(color, "48;5;22", s)
|
|
35
|
+
export task bg_del(color, s)
|
|
36
|
+
give sgr(color, "48;5;52", s)
|
|
37
|
+
|
|
38
|
+
-- ---------- medidas ----------
|
|
39
|
+
|
|
40
|
+
-- punto de código de UN carácter a partir de sus bytes UTF-8
|
|
41
|
+
task codepoint(ch)
|
|
42
|
+
let b be bytes(ch)
|
|
43
|
+
let n be length(b)
|
|
44
|
+
when n == 1
|
|
45
|
+
give b[0]
|
|
46
|
+
when n == 2
|
|
47
|
+
give (b[0] - 192) * 64 + (b[1] - 128)
|
|
48
|
+
when n == 3
|
|
49
|
+
give (b[0] - 224) * 4096 + (b[1] - 128) * 64 + (b[2] - 128)
|
|
50
|
+
give (b[0] - 240) * 262144 + (b[1] - 128) * 4096 + (b[2] - 128) * 64 + (b[3] - 128)
|
|
51
|
+
|
|
52
|
+
-- ancho de un carácter en celdas: emoji, CJK y símbolos "wide" ocupan 2
|
|
53
|
+
task char_width(ch)
|
|
54
|
+
let cp be codepoint(ch)
|
|
55
|
+
when cp < 4352
|
|
56
|
+
give 1
|
|
57
|
+
when cp >= 127744 and cp <= 129791
|
|
58
|
+
give 2
|
|
59
|
+
when cp >= 4352 and cp <= 4447
|
|
60
|
+
give 2
|
|
61
|
+
when cp >= 11904 and cp <= 42191
|
|
62
|
+
give 2
|
|
63
|
+
when cp >= 44032 and cp <= 55203
|
|
64
|
+
give 2
|
|
65
|
+
when cp >= 63744 and cp <= 64255
|
|
66
|
+
give 2
|
|
67
|
+
when cp >= 65040 and cp <= 65135
|
|
68
|
+
give 2
|
|
69
|
+
when cp >= 65280 and cp <= 65376
|
|
70
|
+
give 2
|
|
71
|
+
when cp >= 9800 and cp <= 9811
|
|
72
|
+
give 2
|
|
73
|
+
when cp == 9888 or cp == 9889 or cp == 9989 or cp == 10060 or cp == 11093
|
|
74
|
+
give 2
|
|
75
|
+
give 1
|
|
76
|
+
|
|
77
|
+
export task width(s)
|
|
78
|
+
let t be strip_ansi(s)
|
|
79
|
+
let w be 0
|
|
80
|
+
let i be 0
|
|
81
|
+
let n be length(t)
|
|
82
|
+
while i < n
|
|
83
|
+
set w to w + char_width(slice(t, i, i + 1))
|
|
84
|
+
set i to i + 1
|
|
85
|
+
give w
|
|
86
|
+
|
|
87
|
+
-- corta a `max` celdas visibles, saltando secuencias ANSI enteras; añade "…" si cortó y cierra el color
|
|
88
|
+
export task cut(s, max)
|
|
89
|
+
when width(s) <= max
|
|
90
|
+
give s
|
|
91
|
+
let out be ""
|
|
92
|
+
let w be 0
|
|
93
|
+
let i be 0
|
|
94
|
+
let n be length(s)
|
|
95
|
+
let limit be max - 1
|
|
96
|
+
while i < n and w < limit
|
|
97
|
+
let ch be slice(s, i, i + 1)
|
|
98
|
+
when ch == ESC
|
|
99
|
+
-- copiar la secuencia CSI completa: ESC [ … letra
|
|
100
|
+
let j be i + 1
|
|
101
|
+
while j < n and not matches(slice(s, j, j + 1), "[A-Za-z]")
|
|
102
|
+
set j to j + 1
|
|
103
|
+
set out to out + slice(s, i, j + 1)
|
|
104
|
+
set i to j + 1
|
|
105
|
+
otherwise
|
|
106
|
+
let cw be char_width(ch)
|
|
107
|
+
when w + cw <= limit
|
|
108
|
+
set out to out + ch
|
|
109
|
+
set w to w + cw
|
|
110
|
+
set i to i + 1
|
|
111
|
+
otherwise
|
|
112
|
+
set i to n
|
|
113
|
+
give out + ESC + "[0m" + "…"
|
|
114
|
+
|
|
115
|
+
-- envuelve texto plano (sin escapes) por palabras; give lista de líneas (sin indent en la primera)
|
|
116
|
+
export task wrap(s, max, indent)
|
|
117
|
+
when max <= 10 or length(s) <= max
|
|
118
|
+
give [s]
|
|
119
|
+
let words be split(s, " ")
|
|
120
|
+
let lines be []
|
|
121
|
+
let cur be ""
|
|
122
|
+
let curw be 0
|
|
123
|
+
each w in words
|
|
124
|
+
let ww be width(w)
|
|
125
|
+
let room be when length(lines) == 0 then max otherwise max - length(indent)
|
|
126
|
+
when curw == 0
|
|
127
|
+
set cur to w
|
|
128
|
+
set curw to ww
|
|
129
|
+
otherwise when curw + 1 + ww <= room
|
|
130
|
+
set cur to cur + " " + w
|
|
131
|
+
set curw to curw + 1 + ww
|
|
132
|
+
otherwise
|
|
133
|
+
set lines to append(lines, (when length(lines) == 0 then "" otherwise indent) + cur)
|
|
134
|
+
set cur to w
|
|
135
|
+
set curw to ww
|
|
136
|
+
set lines to append(lines, (when length(lines) == 0 then "" otherwise indent) + cur)
|
|
137
|
+
give lines
|
|
138
|
+
|
|
139
|
+
-- ---------- terminal ----------
|
|
140
|
+
|
|
141
|
+
export task cols()
|
|
142
|
+
observe "lampson:ui:cols" as c
|
|
143
|
+
when c == nothing
|
|
144
|
+
give 100
|
|
145
|
+
give c
|
|
146
|
+
|
|
147
|
+
export task set_cols(n)
|
|
148
|
+
share n as "lampson:ui:cols"
|
|
149
|
+
give n
|
|
150
|
+
|
|
151
|
+
export task fmt_duration(secs)
|
|
152
|
+
let s be floor(secs)
|
|
153
|
+
when s < 60
|
|
154
|
+
give text(s) + "s"
|
|
155
|
+
when s < 3600
|
|
156
|
+
let m be floor(s / 60)
|
|
157
|
+
let r be s - m * 60
|
|
158
|
+
give text(m) + "m " + (when r < 10 then "0" otherwise "") + text(r) + "s"
|
|
159
|
+
let h be floor(s / 3600)
|
|
160
|
+
let m2 be floor((s - h * 3600) / 60)
|
|
161
|
+
give text(h) + "h " + (when m2 < 10 then "0" otherwise "") + text(m2) + "m"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lampson",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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/public/index.html
CHANGED
|
@@ -145,6 +145,10 @@
|
|
|
145
145
|
.sec h2 { font:400 10.5px/1.35 var(--mono); text-transform:uppercase; letter-spacing:.1em; color:var(--ink-3); margin:0; padding:12px 16px 8px; display:flex; align-items:center; gap:8px; cursor:pointer; user-select:none; flex:none; }
|
|
146
146
|
.sec h2:hover { color:var(--ink); }
|
|
147
147
|
.sec .caret { width:9px; font-size:9px; color:var(--ink-3); }
|
|
148
|
+
.sec h2 .h2act { margin-left:auto; border:0; background:none; color:var(--ink-3); font:inherit; font-size:12px; cursor:pointer; padding:0 2px; line-height:1; opacity:.6; }
|
|
149
|
+
.sec h2 .h2act:hover { opacity:1; color:var(--accent); }
|
|
150
|
+
.sec h2 .h2act.spin { animation: h2spin .6s linear; }
|
|
151
|
+
@keyframes h2spin { to { transform: rotate(360deg); } }
|
|
148
152
|
.sec .cnt { margin-left:auto; letter-spacing:0; text-transform:none; }
|
|
149
153
|
.sec .body { display:none; overflow:auto; min-height:0; padding:0 10px 10px 16px; }
|
|
150
154
|
.sec.open .body { display:block; }
|
|
@@ -370,7 +374,7 @@
|
|
|
370
374
|
</header>
|
|
371
375
|
<aside>
|
|
372
376
|
<section class="sec grow open" data-sec="tree">
|
|
373
|
-
<h2><span class="caret">▾</span>Archivos
|
|
377
|
+
<h2><span class="caret">▾</span>Archivos<button class="h2act" id="tree-reload" title="recargar el árbol">↻</button></h2>
|
|
374
378
|
<div class="body" id="tree"></div>
|
|
375
379
|
</section>
|
|
376
380
|
<section class="sec" data-sec="procs">
|
|
@@ -700,6 +704,11 @@ function autoSec(name, hasContent) {
|
|
|
700
704
|
const pref = localStorage.getItem('lampson.sec.' + name);
|
|
701
705
|
setSec(name, pref === null ? hasContent : pref === '1');
|
|
702
706
|
}
|
|
707
|
+
// el árbol se refresca solo cuando una tool pudo crear/borrar archivos (write/edit/bash/process/delegate/lamp),
|
|
708
|
+
// con un pequeño debounce para no repintar 20 veces en un turno con 20 writes; ↻ lo fuerza a mano
|
|
709
|
+
let treeTimer = null;
|
|
710
|
+
function treeChanged() { clearTimeout(treeTimer); treeTimer = setTimeout(loadTree, 400); }
|
|
711
|
+
$('#tree-reload').onclick = e => { e.stopPropagation(); const b = e.currentTarget; b.classList.remove('spin'); void b.offsetWidth; b.classList.add('spin'); loadTree(); };
|
|
703
712
|
document.querySelectorAll('.sec h2').forEach(h => h.onclick = () => {
|
|
704
713
|
const s = h.closest('.sec'); const open = !s.classList.contains('open');
|
|
705
714
|
localStorage.setItem('lampson.sec.' + s.dataset.sec, open ? '1' : '0');
|
|
@@ -1244,6 +1253,7 @@ function handle(chunk, thinking) {
|
|
|
1244
1253
|
else if (k === 'tool_call') { thinking.querySelector('span:last-child').textContent = 'ejecutando ' + esc(d.name) + '…'; pending = add('step', `<span class="ic">⚙</span>${cmdHtml(describe(d))}`); wireMore(pending); if (d.name === 'process') setTimeout(loadProcs, 2500); if (d.name === 'delegate') { setTimeout(loadAgents, 800); setTimeout(loadAgents, 4000); } if (d.name === 'todo') setTimeout(loadTodo, 300); if (d.name === 'lamp') setTimeout(loadLamps, 300); if (d.name === 'lsp') setTimeout(loadLsp, 1500); }
|
|
1245
1254
|
else if (k === 'inbox') { add('meta', '✉ ' + esc(String(d).split('\n')[0].slice(0, 160))); loadAgents(); }
|
|
1246
1255
|
else if (k === 'tool_result') { const out = String(d.output); const bad = /^(ERROR|DENIED)/.test(out);
|
|
1256
|
+
if (!bad && d.call && /^(write|edit|bash|process|delegate|lamp|skill)$/.test(d.call.name)) treeChanged();
|
|
1247
1257
|
// lámpara recién creada: avisar en el chat con un acceso directo al switch (el modelo no sabe cómo se enciende en esta UI)
|
|
1248
1258
|
if (d.call && d.call.name === 'lamp' && /^lamp '([^']+)' created/.test(out)) { const nm = out.match(/^lamp '([^']+)' created/)[1]; const m = add('meta', `☼ lámpara <b>${esc(nm)}</b> creada — está apagada: <a href="#" class="lampgo">encenderla en «lámparas»</a>`); m.querySelector('.lampgo').onclick = (ev) => { ev.preventDefault(); $('#lamps').click(); }; $('#lamps').classList.add('new'); setTimeout(() => $('#lamps').classList.remove('new'), 4000); } const el = pending || add('step', '<span class="ic">→</span>'); pending = null; thinking.querySelector('span:last-child').textContent = 'pensando…'; const ic = el.querySelector('.ic'); if (ic) { ic.textContent = bad ? '✗' : '✓'; ic.className = 'ic ' + (bad ? 'bad' : 'ok'); } el.insertAdjacentHTML('beforeend', `<details><summary class="${bad ? 'bad' : ''}">${esc(out.split('\n')[0].slice(0, 140))} <span style="color:var(--ink-3)">(${out.length} chars)</span></summary><pre>${esc(out)}</pre></details>`); }
|
|
1249
1259
|
else if (k === 'approval_request') { const el = add('approval', `<div class="card"><div class="why">⚠ ${esc(d.why)}</div><code>${esc(describe({name: d.name, args: d.args}))}</code><div class="btns"><button class="primary" data-ok="1">Permitir</button><button data-ok="0">Denegar</button></div></div>`); el.querySelectorAll('button').forEach(b => b.onclick = async () => { el.querySelectorAll('button').forEach(x => x.disabled = true); await fetch('/api/approve', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: d.id, decision: b.dataset.ok === '1' }) }); }); el.dataset.id = d.id; }
|
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))`.
|