lampson 0.1.0

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.
Files changed (62) hide show
  1. package/.env.example +29 -0
  2. package/LICENSE +21 -0
  3. package/README.md +382 -0
  4. package/bin/lampson.js +81 -0
  5. package/chat.syn +799 -0
  6. package/lamps/example-hello/lamp.json +16 -0
  7. package/lamps/example-hello/lamp.syn +19 -0
  8. package/lampson.cmd +4 -0
  9. package/lampson.ps1 +88 -0
  10. package/lampson.sh +42 -0
  11. package/lib/agents.syn +471 -0
  12. package/lib/git.syn +58 -0
  13. package/lib/lamps.syn +386 -0
  14. package/lib/loop.syn +455 -0
  15. package/lib/lsp.syn +503 -0
  16. package/lib/mcp.syn +403 -0
  17. package/lib/permission.syn +154 -0
  18. package/lib/prompt.syn +75 -0
  19. package/lib/provider.syn +522 -0
  20. package/lib/session.syn +111 -0
  21. package/lib/settings.syn +70 -0
  22. package/lib/skills.syn +179 -0
  23. package/lib/tools/bash.syn +105 -0
  24. package/lib/tools/common.sh +49 -0
  25. package/lib/tools/common.syn +91 -0
  26. package/lib/tools/edit.syn +32 -0
  27. package/lib/tools/find.syn +35 -0
  28. package/lib/tools/grep.syn +31 -0
  29. package/lib/tools/img.ps1 +36 -0
  30. package/lib/tools/img.sh +22 -0
  31. package/lib/tools/ls.syn +18 -0
  32. package/lib/tools/memo.syn +148 -0
  33. package/lib/tools/proc.sh +42 -0
  34. package/lib/tools/proc.syn +314 -0
  35. package/lib/tools/process.syn +46 -0
  36. package/lib/tools/read.syn +25 -0
  37. package/lib/tools/skill.syn +14 -0
  38. package/lib/tools/todo.syn +97 -0
  39. package/lib/tools/write.syn +22 -0
  40. package/lib/tools.syn +198 -0
  41. package/lib/trace.syn +116 -0
  42. package/lib/tree.syn +59 -0
  43. package/lib/update.syn +57 -0
  44. package/package.json +40 -0
  45. package/public/fonts/plex-mono-400-latin-ext.woff2 +0 -0
  46. package/public/fonts/plex-mono-400-latin.woff2 +0 -0
  47. package/public/fonts/plex-mono-600-latin-ext.woff2 +0 -0
  48. package/public/fonts/plex-mono-600-latin.woff2 +0 -0
  49. package/public/fonts/plex-serif-400-latin-ext.woff2 +0 -0
  50. package/public/fonts/plex-serif-400-latin.woff2 +0 -0
  51. package/public/fonts/plex-serif-400i-latin-ext.woff2 +0 -0
  52. package/public/fonts/plex-serif-400i-latin.woff2 +0 -0
  53. package/public/fonts/plex-serif-600-latin-ext.woff2 +0 -0
  54. package/public/fonts/plex-serif-600-latin.woff2 +0 -0
  55. package/public/index.html +1268 -0
  56. package/public/vendor/xterm-addon-fit.js +2 -0
  57. package/public/vendor/xterm.css +218 -0
  58. package/public/vendor/xterm.js +2 -0
  59. package/skills/debugging/SKILL.md +33 -0
  60. package/skills/lampson/SKILL.md +117 -0
  61. package/skills/synsema/SKILL.md +75 -0
  62. package/web.syn +468 -0
@@ -0,0 +1,97 @@
1
+ -- lib/tools/todo.syn — lista de tareas del agente (reemplazo total), POR SESIÓN
2
+ --
3
+ -- Diseño (deepseek-harness tool-todo + hermes todo_tool + opencode session/todo): el modelo manda la lista
4
+ -- ENTERA cada vez (nada de merges ambiguos), como máximo UNA tarea in_progress (se valida en código y la
5
+ -- llamada falla si no), y la lista NO se re-inyecta en cada paso — los tres harnesses coinciden: gasta tokens
6
+ -- y compite con el prompt. Sí se re-inyecta al COMPACTAR el contexto (lib/loop.syn), y solo los items
7
+ -- pending/in_progress: incluir los completados hace que el modelo rehaga trabajo terminado (hermes
8
+ -- todo_tool.py:136).
9
+ --
10
+ -- Alcance = la SESIÓN, como en los tres (opencode la guarda por sessionID; hermes/deepseek la atan al agente
11
+ -- de la sesión): es un andamio de planificación del trabajo en curso, no un tracker del proyecto. Sesión
12
+ -- nueva → lista vacía; /resume de una sesión → vuelve SU lista. Antes (2026-08-28) era por proyecto y la
13
+ -- lista de un pedido viejo reaparecía "apagada" en el siguiente y se pisaba sin que nadie la cerrara.
14
+ -- Persistida en .lampson/todo/<session_id>.json; la sesión actual la comparte el entry en el blackboard
15
+ -- "lampson:session" (chat.syn / web.syn) porque la tool no recibe contexto de sesión.
16
+ use "./common.syn" as c
17
+
18
+ export let DIR be ".lampson/todo"
19
+ let STATES be ["pending", "in_progress", "completed", "cancelled"]
20
+
21
+ -- id de la sesión actual (blackboard); sin entry (tests) → "default"
22
+ export task current()
23
+ observe "lampson:session" as s
24
+ when s == nothing
25
+ give "default"
26
+ give s["id"]
27
+
28
+ task path(sid)
29
+ give DIR + "/" + sid + ".json"
30
+
31
+ -- lista de una sesión (sid == nothing → la actual)
32
+ export task load(sid)
33
+ require file(".lampson")
34
+ require file(".lampson/*")
35
+ let id be when sid == nothing then current() otherwise sid
36
+ try
37
+ give json_decode(read_file(path(id)))
38
+ recover err
39
+ give []
40
+
41
+ task mark(s)
42
+ when s == "completed"
43
+ give "[x]"
44
+ when s == "in_progress"
45
+ give "[>]"
46
+ when s == "cancelled"
47
+ give "[-]"
48
+ give "[ ]"
49
+
50
+ export task format(items)
51
+ let lines be []
52
+ each e in enumerate(items)
53
+ let it be e["item"]
54
+ set lines to append(lines, text(e["index"] + 1) + ". " + mark(it["status"]) + " " + it["content"] + (when it["status"] == "in_progress" then " ← in progress" otherwise ""))
55
+ give join(lines, "\n")
56
+
57
+ -- texto para re-inyectar tras una compaction: solo lo activo; "" si no hay nada activo
58
+ export task active_text()
59
+ require file(".lampson")
60
+ require file(".lampson/*")
61
+ let active be where(load(nothing), (it) => it["status"] == "pending" or it["status"] == "in_progress")
62
+ when length(active) == 0
63
+ give ""
64
+ give "[Your active task list was preserved across context compression — completed items omitted]\n" + format(active)
65
+
66
+ export task tool(items)
67
+ require file(".lampson")
68
+ require file(".lampson/*")
69
+ when items == nothing
70
+ let cur be load(nothing)
71
+ give when length(cur) == 0 then "(empty task list)" otherwise format(cur)
72
+ let clean be []
73
+ let in_progress be 0
74
+ each it in items
75
+ when not contains(it, "content") or trim(text(it["content"])) == ""
76
+ raise("every item needs a non-empty content")
77
+ let st be when contains(it, "status") then lower(text(it["status"])) otherwise "pending"
78
+ when not contains(STATES, st)
79
+ raise("invalid status '" + st + "' (pending | in_progress | completed | cancelled)")
80
+ when st == "in_progress"
81
+ set in_progress to in_progress + 1
82
+ set clean to append(clean, {"content": trim(text(it["content"])), "status": st})
83
+ when in_progress > 1
84
+ raise("at most ONE item may be in_progress at a time (you sent " + text(in_progress) + "): finish or cancel the others first")
85
+ write_file(path(current()), json_encode(clean))
86
+ give when length(clean) == 0 then "(empty task list)" otherwise format(clean)
87
+
88
+ export let SPEC be {
89
+ "name": "todo",
90
+ "description": "Your task list for the current session. Send the WHOLE list every time (it replaces the previous one); with no items it just returns the current list. Statuses: pending, in_progress (at most ONE at a time), completed, cancelled. Use it for any task with 3+ steps: write the plan first, mark an item in_progress when you start it, and completed only after it is verified done — never on intent; if something fails, cancel it and add a revised item. The list is shown to the user and preserved across context compression; it is not re-sent to you every step, so keep it current yourself. A new session starts with an empty list.",
91
+ "parameters": {"type": "object", "properties": {
92
+ "items": {"type": "array", "items": {"type": "object", "properties": {
93
+ "content": {"type": "string"},
94
+ "status": {"type": "string", "enum": ["pending", "in_progress", "completed", "cancelled"]}
95
+ }, "required": ["content"]}}
96
+ }}
97
+ }
@@ -0,0 +1,22 @@
1
+ -- lib/tools/write.syn — crear/sobrescribir un archivo (atómico, crea directorios)
2
+ use "./common.syn" as c
3
+
4
+ export task tool(path, content)
5
+ require file("workspace")
6
+ require file("workspace/*")
7
+ let real be c.ws(path)
8
+ -- sobrescribir un archivo existente exige haberlo leído (y que no haya cambiado): igual que edit
9
+ when file_exists(real)
10
+ c.check_observed(real, "write")
11
+ write_file(real, content)
12
+ c.mark_observed(real)
13
+ give `wrote {text(length(content))} chars to {c.unws(real)}`
14
+
15
+ export let SPEC be {
16
+ "name": "write",
17
+ "description": "Create a file with the given content (atomic; creates parent dirs). Overwriting an EXISTING file requires having read it first (and not changed since) — and prefer `edit` for targeted changes. Paths are relative to the workspace root.",
18
+ "parameters": {"type": "object", "properties": {
19
+ "path": {"type": "string"},
20
+ "content": {"type": "string"}
21
+ }, "required": ["path", "content"]}
22
+ }
package/lib/tools.syn ADDED
@@ -0,0 +1,198 @@
1
+ -- lib/tools.syn — agregador de tools: una tool = un archivo en lib/tools/<nombre>.syn
2
+ --
3
+ -- Contrato de cada archivo de tool ("plugin"):
4
+ -- export task tool(...) -- la tool; sus `require` al TOPE del cuerpo (call_tool los intersecta)
5
+ -- export let SPEC be {...} -- {name, description, parameters (JSON Schema)} para el modelo
6
+ --
7
+ -- Para agregar una tool: crear lib/tools/x.syn con ese contrato y sumarla a las dos listas de abajo.
8
+ -- La lista es EXPLÍCITA a propósito: Synsema no tiene `use` dinámico (sin supply chain), así que
9
+ -- el allow-list de tools se lee en este archivo y se audita con `synsema check`.
10
+ --
11
+ -- Seguridad que ya pone el lenguaje: ninguna tool declara `net` → ninguna tool puede usar la red,
12
+ -- aunque el entry la tenga para hablar con el LLM.
13
+
14
+ use "./tools/common.syn" as common
15
+ use "./tools/read.syn" as t_read
16
+ use "./tools/write.syn" as t_write
17
+ use "./tools/edit.syn" as t_edit
18
+ use "./tools/ls.syn" as t_ls
19
+ use "./tools/find.syn" as t_find
20
+ use "./tools/grep.syn" as t_grep
21
+ use "./tools/bash.syn" as t_bash
22
+ use "./tools/skill.syn" as t_skill
23
+ use "./tools/process.syn" as t_process
24
+ use "./tools/memo.syn" as t_memo
25
+ use "./tools/todo.syn" as t_todo
26
+ use "./skills.syn" as skills
27
+ use "./mcp.syn" as mcp
28
+ use "./lamps.syn" as lamps
29
+ use "./lsp.syn" as lsp
30
+
31
+ -- la task de la tool skill (su SPEC está en tools/skill.syn)
32
+ task skill_tool(name, action, source, scope)
33
+ require exec
34
+ require time
35
+ require env("LAMPSON_*")
36
+ require env("OS")
37
+ require file.read("skills")
38
+ require file.read("skills/*")
39
+ require file.read("workspace")
40
+ require file.read("workspace/*")
41
+ require file.read(".lampson")
42
+ require file.read(".lampson/*")
43
+ when action == "install"
44
+ give skills.install(source, name, scope)
45
+ when action == "list"
46
+ let idx be skills.index()
47
+ let lines be []
48
+ each n in sort_by(keys(idx), (x) => x)
49
+ set lines to append(lines, n + " (" + idx[n]["source"] + "): " + idx[n]["description"])
50
+ give join(lines, "\n")
51
+ give skills.load(name)
52
+
53
+ -- la task de la tool mcp (mcp.syn está un nivel arriba de tools/, así que vive acá, como skill_tool).
54
+ -- add/remove SIEMPRE piden aprobación (permission.syn): conectar un server = ejecutar un comando de terceros.
55
+ task mcp_tool(action, name, command, env, scope)
56
+ require exec
57
+ require time
58
+ require env("LAMPSON_*")
59
+ require env("OS")
60
+ require file(".lampson")
61
+ require file(".lampson/*")
62
+ require file("workspace")
63
+ require file("workspace/*")
64
+ when action == "add"
65
+ give mcp.add_server(name, command, env, scope)
66
+ when action == "remove"
67
+ give mcp.remove_server(name)
68
+ let sm be mcp.summary()
69
+ when length(sm) == 0
70
+ give "no MCP servers connected. Config files: " + mcp.GLOBAL_CONFIG + " (global) · " + mcp.PROJECT_CONFIG + " (this project)"
71
+ let lines be []
72
+ each ms in sm
73
+ set lines to append(lines, ms["name"] + " (" + ms["scope"] + ", " + ms["status"] + "): " + text(length(ms["tools"])) + " tools" + (when ms["error"] != nothing then " · " + text(ms["error"]) otherwise ""))
74
+ give join(lines, "\n")
75
+
76
+ let MCP_SPEC be {
77
+ "name": "mcp",
78
+ "description": "Manage MCP (Model Context Protocol) servers. action=list (default): connected servers and their status. action=add: connect a server — name plus the full command line that starts it (e.g. command='npx -y @modelcontextprotocol/server-github'); scope=global (default) makes it available in every project (lampson/.lampson/mcp.json), scope=project only in this one (workspace/.lampson/mcp.json); env = extra environment variables the server needs (tokens, keys). action=remove: disconnect and delete it from the config. add/remove always ask the user for approval. A connected server's tools appear in your catalog as mcp_<server>_<tool> from the NEXT turn. Propose add when the user asks to connect/install an MCP or a task clearly needs one.",
79
+ "parameters": {"type": "object", "properties": {
80
+ "action": {"type": "string", "enum": ["list", "add", "remove"], "description": "Default: list"},
81
+ "name": {"type": "string", "description": "add/remove: server name (letters, digits, - or _)"},
82
+ "command": {"type": "string", "description": "add only: full command line that starts the server (quotes allowed)"},
83
+ "env": {"type": "object", "description": "add only: environment variables for the server, e.g. {\"GITHUB_TOKEN\": \"…\"}"},
84
+ "scope": {"type": "string", "enum": ["global", "project"], "description": "add only: global (default, every project) or project"}
85
+ }, "required": ["action"]}
86
+ }
87
+
88
+ -- la task de la tool lamp (lamps.syn vive un nivel arriba de tools/, como mcp). enable/disable piden humano
89
+ -- SIEMPRE (permission.syn): encender = autorizar código a correr con las capacidades de su manifiesto.
90
+ task lamp_tool(action, name, manifest, files)
91
+ require exec
92
+ require time
93
+ require env("LAMPSON_*")
94
+ require file(".lampson")
95
+ require file(".lampson/*")
96
+ require file.read("lamps")
97
+ require file.read("lamps/*")
98
+ require file("workspace")
99
+ require file("workspace/*")
100
+ when action == "create"
101
+ give lamps.create(name, manifest, files)
102
+ when action == "enable"
103
+ give lamps.set_enabled(name, true)
104
+ when action == "disable"
105
+ give lamps.set_enabled(name, false)
106
+ let sm be lamps.summary()
107
+ when length(sm) == 0
108
+ give "no lamps found. A lamp is a folder with a lamp.json manifest: " + lamps.GLOBAL_DIR + "/<name>/ (global) or " + lamps.PROJECT_DIR + "/<name>/ (this project; you can create it with write). See the lampson skill for the manifest format."
109
+ let lines be []
110
+ each l in sm
111
+ set lines to append(lines, l["name"] + " (" + l["scope"] + ", " + l["kind"] + ", " + (when l["enabled"] then "ON" otherwise "off") + "): " + (when l["error"] != nothing then "BROKEN — " + text(l["error"]) otherwise text(length(l["tools"])) + " tools [" + join(l["tools"], ", ") + "]" + (when l["description"] != "" then " — " + l["description"] otherwise "")))
112
+ give join(lines, "\n")
113
+
114
+ let LAMP_SPEC be {
115
+ "name": "lamp",
116
+ "description": "Lamps are tool plugins you can build for this project: a folder with a lamp.json manifest plus code — a Synsema program (kind=syn) run under a capability ceiling, or any executable (kind=exec). Consider one when a task needs a reusable custom tool (a project-specific query, generator, checker) that plain bash would repeat clumsily. action=list (default): every lamp found, on/off, its tools. action=create: write a PROJECT lamp (name + manifest + files) — it validates the manifest and runs `synsema check` on a syn entry, but does NOT run or enable it. action=enable / disable: turn one on or off — ALWAYS asks the user (enabling authorizes its code to run with the manifest's capabilities). An enabled lamp's tools appear in your catalog as lamp_<lamp>_<tool> from the NEXT turn. Manifest: {\"description\", \"kind\": \"syn\"|\"exec\", \"entry\": \"lamp.syn\" (syn) | \"command\": \"python lamp.py\" (exec), \"caps\": \"file.read=workspace/*\" (syn, optional extra ceiling over stdout,time,env=LAMP_*), \"timeout\": 60, \"tools\": [{\"name\", \"description\", \"parameters\": JSON Schema, \"readonly\": bool}]}. Inside the code read LAMP_TOOL and LAMP_ARGS (JSON) from env and print the result to stdout (a .syn needs `require env(\"LAMP_*\")`). Lamp names: letters, digits, - (no _).",
117
+ "parameters": {"type": "object", "properties": {
118
+ "action": {"type": "string", "enum": ["list", "create", "enable", "disable"], "description": "Default: list"},
119
+ "name": {"type": "string", "description": "create/enable/disable: lamp name"},
120
+ "manifest": {"type": "object", "description": "create only: the lamp.json content (name is filled in)"},
121
+ "files": {"type": "object", "description": "create only: {\"lamp.syn\": \"<code>\", …} — files written into the lamp folder", "additionalProperties": {"type": "string"}}
122
+ }, "required": ["action"]}
123
+ }
124
+
125
+ -- la tool lsp (lib/lsp.syn): navegación semántica. Solo lectura → permission la permite siempre.
126
+ task lsp_tool(op, path, line, character, server, scope)
127
+ require exec
128
+ require time
129
+ require env("LAMPSON_*")
130
+ require env("OS")
131
+ require file(".lampson")
132
+ require file(".lampson/*")
133
+ require file("workspace")
134
+ require file("workspace/*")
135
+ -- op=add: el agente propone un preset y el humano aprueba (permission.syn: ask siempre, como mcp add)
136
+ when op == "add"
137
+ give lsp.add_server(server, nothing, nothing, when scope == nothing then "global" otherwise scope)
138
+ when op == "list"
139
+ let sm be lsp.summary()
140
+ when length(sm) == 0
141
+ give "no language servers configured. Presets: " + join(keys(lsp.PRESETS), ", ") + " — propose op=add with server=<preset> (the user approves)."
142
+ give join(apply(sm, (x) => x["name"] + " (" + x["scope"] + ", " + x["status"] + "): " + join(x["extensions"], " ") + (when x["error"] != nothing then " · " + text(x["error"]) otherwise "")), "\n")
143
+ give lsp.query(op, path, line, character)
144
+
145
+ let LSP_SPEC be {
146
+ "name": "lsp",
147
+ "description": "Semantic code navigation through the project's language server (same engine as the editor). Use it when grep is ambiguous or before a change that needs exact definitions/usages, and to see a file's structure WITHOUT reading it: op=symbols lists every function/class/variable with line ranges — then read only the range you need. op=definition: where the symbol under the cursor is defined. op=references: every usage (declaration included). op=implementation: implementations of an interface/abstract member. op=hover: type signature and docs. Positions are 1-based line and character, like the editor; put the cursor ON the identifier (an off-symbol position returns nothing). The server for a file is chosen by extension; if none is configured for that extension, propose op=add with server=<preset> (typescript, python, rust, go, css, html) — it always asks the user for approval and needs nothing installed (npx fetches it); op=list shows what is configured. The first query of a session starts the server (a few seconds).",
148
+ "parameters": {"type": "object", "properties": {
149
+ "op": {"type": "string", "enum": ["symbols", "definition", "references", "implementation", "hover", "list", "add"]},
150
+ "server": {"type": "string", "description": "add only: preset name (typescript | python | rust | go | css | html)"},
151
+ "scope": {"type": "string", "enum": ["global", "project"], "description": "add only: global (default, every project) or project"},
152
+ "path": {"type": "string", "description": "File path relative to the workspace root"},
153
+ "line": {"type": "integer", "description": "1-based line (not for symbols)"},
154
+ "character": {"type": "integer", "description": "1-based column of the identifier (not for symbols)"}
155
+ }, "required": ["op"]}
156
+ }
157
+
158
+ -- Allow-list nombre → task. El modelo solo devuelve NOMBRES; el loop decide si se ejecuta.
159
+ export task registry()
160
+ give {
161
+ "read": t_read.tool,
162
+ "write": t_write.tool,
163
+ "edit": t_edit.tool,
164
+ "ls": t_ls.tool,
165
+ "find": t_find.tool,
166
+ "grep": t_grep.tool,
167
+ "bash": t_bash.tool,
168
+ "process": t_process.tool,
169
+ "memory": t_memo.tool,
170
+ "todo": t_todo.tool,
171
+ "skill": skill_tool,
172
+ "mcp": mcp_tool,
173
+ "lamp": lamp_tool,
174
+ "lsp": lsp_tool
175
+ }
176
+
177
+ export let CATALOG be [t_read.SPEC, t_write.SPEC, t_edit.SPEC, t_ls.SPEC, t_find.SPEC, t_grep.SPEC, LSP_SPEC, t_bash.SPEC, t_process.SPEC, t_memo.SPEC, t_todo.SPEC, t_skill.SPEC, MCP_SPEC, LAMP_SPEC]
178
+
179
+ -- Subconjuntos (para perfiles de agente): registry/catálogo filtrados por nombre.
180
+ export task registry_subset(names)
181
+ let all be registry()
182
+ let out be {}
183
+ each n in names
184
+ when contains(all, n)
185
+ set out[n] to all[n]
186
+ give out
187
+
188
+ export task catalog_subset(names)
189
+ give where(CATALOG, (s) => contains(names, s["name"]))
190
+
191
+ -- Re-exports usados por los entries
192
+ export task truncate(s, max)
193
+ give common.truncate(s, max)
194
+
195
+ export task shell_config()
196
+ require env("LAMPSON_*")
197
+ require env("OS")
198
+ give t_bash.shell_config()
package/lib/trace.syn ADDED
@@ -0,0 +1,116 @@
1
+ -- lib/trace.syn — traza legible de lo que hace el agente, por sesión: .lampson/trace/<sid>.log
2
+ --
3
+ -- Las sesiones (.lampson/sessions/*.json) guardan el historial canónico para el modelo; esto es para
4
+ -- el HUMANO que quiere ver cómo trabajó el agente y dónde se equivoca: una línea por evento con hora,
5
+ -- tiempo desde el evento anterior, paso, tokens, tool + args, resultado (primeras líneas), errores y
6
+ -- denegaciones, avisos del buzón y subagentes. Se lee con `/trace [n]` (terminal), en la web (≡ en la
7
+ -- sesión) o con cualquier editor / `tail -f`.
8
+ --
9
+ -- Formato de línea: HH:MM:SS.mmm +Δs [tag] KIND texto
10
+ -- Δs = segundos desde la línea anterior de la misma sesión (dónde se fue el tiempo: modelo vs tools).
11
+
12
+ export let DIR be ".lampson/trace"
13
+ let MAX_LINE be 600
14
+
15
+ task path(sid)
16
+ give DIR + "/" + sid + ".log"
17
+
18
+ task one_line(s, max)
19
+ let t be replace_text(replace_text(text(s), "\r", ""), "\n", " ⏎ ")
20
+ when length(t) > max
21
+ give slice(t, 0, max) + "…"
22
+ give t
23
+
24
+ task pad2(n)
25
+ let t be text(floor(n))
26
+ when length(t) < 2
27
+ give "0" + t
28
+ give t
29
+
30
+ task stamp()
31
+ let t be now()
32
+ let ms be floor((t - floor(t)) * 1000)
33
+ let mst be text(ms)
34
+ while length(mst) < 3
35
+ set mst to "0" + mst
36
+ give format_time(t, "%H:%M:%S") + "." + mst
37
+
38
+ -- delta desde la última línea: guardamos el instante en <sid>.last
39
+ task delta(sid)
40
+ let last be 0
41
+ try
42
+ set last to number(trim(read_file(DIR + "/" + sid + ".last")))
43
+ recover err
44
+ set last to 0
45
+ write_file(DIR + "/" + sid + ".last", text(now()))
46
+ when last == 0
47
+ give " "
48
+ let d be now() - last
49
+ let s be text(floor(d * 10) / 10)
50
+ while length(s) < 5
51
+ set s to " " + s
52
+ give "+" + s
53
+
54
+ export task write(sid, kind, line)
55
+ require time
56
+ require file(".lampson")
57
+ require file(".lampson/*")
58
+ when sid == nothing or sid == ""
59
+ give false
60
+ append_file(path(sid), stamp() + " " + delta(sid) + "s " + kind + " " + line + "\n")
61
+ give true
62
+
63
+ -- eventos del loop (on_event de chat/web): kind ∈ assistant | tool_call | tool_result | tool_denied |
64
+ -- error | usage | compact | inbox
65
+ export task event(sid, kind, data, tag)
66
+ require time
67
+ require file(".lampson")
68
+ require file(".lampson/*")
69
+ let t be when tag == nothing or tag == "" then "" otherwise "[" + text(tag) + "] "
70
+ when kind == "usage"
71
+ let cached be when contains(data["usage"], "cached") then data["usage"]["cached"] otherwise 0
72
+ give write(sid, "STEP " + text(data["step"]), t + "modelo respondió · in=" + text(data["usage"]["input"]) + (when cached > 0 then " (cache " + text(cached) + ")" otherwise "") + " out=" + text(data["usage"]["output"]) + " · acumulado " + text(data["total"]["input"] + data["total"]["output"]))
73
+ when kind == "assistant"
74
+ give write(sid, "SAY ", t + one_line(data, MAX_LINE))
75
+ when kind == "tool_call"
76
+ give write(sid, "CALL", t + data["name"] + " " + one_line(json_encode(data["args"]), MAX_LINE))
77
+ when kind == "tool_result"
78
+ let out be text(data["output"])
79
+ let bad be starts_with(out, "ERROR") or starts_with(out, "DENIED")
80
+ give write(sid, when bad then "FAIL" otherwise "OK ", t + data["call"]["name"] + " → " + one_line(out, MAX_LINE) + " (" + text(length(out)) + " chars)")
81
+ when kind == "tool_denied"
82
+ give write(sid, "DENY", t + data["call"]["name"] + " · " + one_line(data["reason"], MAX_LINE))
83
+ when kind == "error"
84
+ give write(sid, "ERR ", t + one_line(data, MAX_LINE))
85
+ when kind == "compact"
86
+ give write(sid, "PACK", t + "compactación de contexto (~" + text(data["before"]) + " tokens)")
87
+ when kind == "inbox"
88
+ give write(sid, "MAIL", t + one_line(data, MAX_LINE))
89
+ give false
90
+
91
+ -- marcas de turno (las llaman chat.syn / web.syn)
92
+ export task user(sid, content, agent, mode, model)
93
+ require time
94
+ require file(".lampson")
95
+ require file(".lampson/*")
96
+ give write(sid, "USER", "[" + agent + " · " + mode + " · " + model + "] " + one_line(content, MAX_LINE))
97
+
98
+ export task turn_end(sid, result)
99
+ require time
100
+ require file(".lampson")
101
+ require file(".lampson/*")
102
+ give write(sid, "END ", text(result["steps"]) + " pasos · " + text(result["usage"]["input"] + result["usage"]["output"]) + " tokens · " + result["stopped"])
103
+
104
+ export task tail(sid, n)
105
+ require file(".lampson")
106
+ require file(".lampson/*")
107
+ try
108
+ let lines be split(replace_text(read_file(path(sid)), "\r", ""), "\n")
109
+ when length(lines) > n
110
+ set lines to slice(lines, length(lines) - n, length(lines))
111
+ give join(lines, "\n")
112
+ recover err
113
+ give ""
114
+
115
+ export task file_of(sid)
116
+ give path(sid)
package/lib/tree.syn ADDED
@@ -0,0 +1,59 @@
1
+ -- lib/tree.syn — árbol de archivos del workspace (para el banner de terminal y el explorador web)
2
+ use "./tools/common.syn" as c
3
+
4
+ let MAX_ENTRIES be 2000
5
+
6
+ task walk(dir, depth, max_depth, state)
7
+ let out be []
8
+ when depth > max_depth
9
+ give out
10
+ let entries be []
11
+ try
12
+ set entries to list_dir(dir)
13
+ recover err
14
+ give out
15
+ each e in entries
16
+ when state["n"] >= MAX_ENTRIES
17
+ give out
18
+ let rel be c.unws(dir + "/" + e["name"])
19
+ when e["is_dir"]
20
+ when not contains(c.IGNORED_DIRS, e["name"])
21
+ set state["n"] to state["n"] + 1
22
+ set out to append(out, {"name": e["name"], "path": rel, "is_dir": true, "children": walk(dir + "/" + e["name"], depth + 1, max_depth, state)})
23
+ otherwise
24
+ set state["n"] to state["n"] + 1
25
+ set out to append(out, {"name": e["name"], "path": rel, "is_dir": false, "size": e["size"]})
26
+ -- carpetas primero
27
+ give where(out, (x) => x["is_dir"]) + where(out, (x) => not x["is_dir"])
28
+
29
+ -- árbol anidado: [{name, path, is_dir, size?, children?}]
30
+ export task tree(max_depth)
31
+ require file.read("workspace")
32
+ require file.read("workspace/*")
33
+ let state be {"n": 0}
34
+ give {"entries": walk(c.ROOT, 0, max_depth, state), "count": state["n"], "truncated": state["n"] >= MAX_ENTRIES}
35
+
36
+ -- render de texto para la terminal (2 niveles, compacto)
37
+ export task render(max_depth, max_lines)
38
+ require file.read("workspace")
39
+ require file.read("workspace/*")
40
+ let t be tree(max_depth)
41
+ let lines be []
42
+ each e in t["entries"]
43
+ when length(lines) < max_lines
44
+ set lines to append(lines, " " + e["name"] + (when e["is_dir"] then "/" otherwise ""))
45
+ when e["is_dir"] and contains(e, "children")
46
+ each ch in e["children"]
47
+ when length(lines) < max_lines
48
+ set lines to append(lines, " " + ch["name"] + (when ch["is_dir"] then "/" otherwise ""))
49
+ when length(lines) >= max_lines
50
+ set lines to append(lines, " … " + text(t["count"]) + " entradas en total — /files muestra el árbol completo")
51
+ give join(lines, "\n")
52
+
53
+ -- contenido de un archivo para el visor web
54
+ export task file_content(path)
55
+ require file.read("workspace")
56
+ require file.read("workspace/*")
57
+ let real be c.ws(path)
58
+ let content be read_file(real)
59
+ give {"path": c.unws(real), "content": c.truncate(content, 200000), "lines": length(split(content, "\n"))}
package/lib/update.syn ADDED
@@ -0,0 +1,57 @@
1
+ -- lib/update.syn — ¿hay una versión nueva de Lampson? (git fetch contra origin/main)
2
+ --
3
+ -- Lampson se instala como clon de git (install.ps1 / install.sh), así que "actualizar" es un
4
+ -- `git pull --ff-only`. check() compara HEAD con origin/main; el cwd es el directorio de lampson
5
+ -- (el launcher hace cd antes de arrancar). Sin red o sin .git devuelve available=false en silencio.
6
+ -- Cuando la distribución cambie (exe, app de escritorio), cambiar COMMAND y apply() acá y en el skill.
7
+
8
+ export let COMMAND be "lampson --update"
9
+
10
+ task run_git(args, secs)
11
+ try
12
+ let r be run("git", args, secs)
13
+ when r["exit_code"] != 0
14
+ give nothing
15
+ give trim(replace_text(r["stdout"], "\r", ""))
16
+ recover err
17
+ give nothing
18
+
19
+ -- {available, behind, current, latest, notes: [líneas del log], command}
20
+ export task check()
21
+ require exec("git")
22
+ require net
23
+ let current be run_git(["rev-parse", "--short", "HEAD"], 5)
24
+ let none be {"available": false, "behind": 0, "current": when current == nothing then "" otherwise current, "latest": "", "notes": [], "command": COMMAND}
25
+ when current == nothing
26
+ give none
27
+ when run_git(["fetch", "-q", "origin", "main"], 12) == nothing
28
+ give none
29
+ let behind be run_git(["rev-list", "--count", "HEAD..origin/main"], 5)
30
+ when behind == nothing or behind == "0"
31
+ give none
32
+ let latest be run_git(["rev-parse", "--short", "origin/main"], 5)
33
+ let log be run_git(["log", "--format=%s", "-5", "HEAD..origin/main"], 5)
34
+ let notes be when log == nothing then [] otherwise where(split(log, "\n"), (l) => trim(l) != "")
35
+ give {"available": true, "behind": floor(number(behind)), "current": current, "latest": when latest == nothing then "" otherwise latest, "notes": notes, "command": COMMAND}
36
+
37
+ -- una línea para banners; "" si no hay nada
38
+ export task line()
39
+ require exec("git")
40
+ require net
41
+ let u be check()
42
+ when not u["available"]
43
+ give ""
44
+ give "hay una versión nueva de Lampson (" + text(u["behind"]) + (when u["behind"] == 1 then " commit" otherwise " commits") + ", " + u["current"] + " → " + u["latest"] + ") · corré " + COMMAND
45
+
46
+ -- aplica la actualización; devuelve el texto para mostrar
47
+ export task apply()
48
+ require exec("git")
49
+ require net
50
+ try
51
+ let r be run("git", ["pull", "--ff-only", "origin", "main"], 60)
52
+ when r["exit_code"] != 0
53
+ give "no se pudo actualizar: " + trim(r["stderr"]) + "\n(si modificaste archivos de lampson, guardalos con git stash y volvé a intentar)"
54
+ let now be run_git(["rev-parse", "--short", "HEAD"], 5)
55
+ give "actualizado a " + (when now == nothing then "HEAD" otherwise now) + " · reiniciá lampson para usar la versión nueva"
56
+ recover err
57
+ give "no se pudo actualizar: " + text(err)
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "lampson",
3
+ "version": "0.1.0",
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
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/kitecosmic/lampson.git"
9
+ },
10
+ "homepage": "https://github.com/kitecosmic/lampson#readme",
11
+ "bugs": "https://github.com/kitecosmic/lampson/issues",
12
+ "keywords": ["agent", "coding-agent", "synsema", "llm", "cli", "lsp", "mcp"],
13
+ "bin": {
14
+ "lampson": "bin/lampson.js"
15
+ },
16
+ "files": [
17
+ "bin/",
18
+ "lib/",
19
+ "public/",
20
+ "skills/",
21
+ "lamps/",
22
+ "chat.syn",
23
+ "web.syn",
24
+ "lampson.ps1",
25
+ "lampson.sh",
26
+ "lampson.cmd",
27
+ ".env.example",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "dependencies": {
35
+ "synsema": "^0.6.10"
36
+ },
37
+ "scripts": {
38
+ "test": "pwsh -NoProfile -File tests/run.ps1"
39
+ }
40
+ }