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.
- package/.env.example +29 -0
- package/LICENSE +21 -0
- package/README.md +382 -0
- package/bin/lampson.js +81 -0
- package/chat.syn +799 -0
- package/lamps/example-hello/lamp.json +16 -0
- package/lamps/example-hello/lamp.syn +19 -0
- package/lampson.cmd +4 -0
- package/lampson.ps1 +88 -0
- package/lampson.sh +42 -0
- package/lib/agents.syn +471 -0
- package/lib/git.syn +58 -0
- package/lib/lamps.syn +386 -0
- package/lib/loop.syn +455 -0
- package/lib/lsp.syn +503 -0
- package/lib/mcp.syn +403 -0
- package/lib/permission.syn +154 -0
- package/lib/prompt.syn +75 -0
- package/lib/provider.syn +522 -0
- package/lib/session.syn +111 -0
- package/lib/settings.syn +70 -0
- package/lib/skills.syn +179 -0
- package/lib/tools/bash.syn +105 -0
- package/lib/tools/common.sh +49 -0
- package/lib/tools/common.syn +91 -0
- package/lib/tools/edit.syn +32 -0
- package/lib/tools/find.syn +35 -0
- package/lib/tools/grep.syn +31 -0
- package/lib/tools/img.ps1 +36 -0
- package/lib/tools/img.sh +22 -0
- package/lib/tools/ls.syn +18 -0
- package/lib/tools/memo.syn +148 -0
- package/lib/tools/proc.sh +42 -0
- package/lib/tools/proc.syn +314 -0
- package/lib/tools/process.syn +46 -0
- package/lib/tools/read.syn +25 -0
- package/lib/tools/skill.syn +14 -0
- package/lib/tools/todo.syn +97 -0
- package/lib/tools/write.syn +22 -0
- package/lib/tools.syn +198 -0
- package/lib/trace.syn +116 -0
- package/lib/tree.syn +59 -0
- package/lib/update.syn +57 -0
- package/package.json +40 -0
- package/public/fonts/plex-mono-400-latin-ext.woff2 +0 -0
- package/public/fonts/plex-mono-400-latin.woff2 +0 -0
- package/public/fonts/plex-mono-600-latin-ext.woff2 +0 -0
- package/public/fonts/plex-mono-600-latin.woff2 +0 -0
- package/public/fonts/plex-serif-400-latin-ext.woff2 +0 -0
- package/public/fonts/plex-serif-400-latin.woff2 +0 -0
- package/public/fonts/plex-serif-400i-latin-ext.woff2 +0 -0
- package/public/fonts/plex-serif-400i-latin.woff2 +0 -0
- package/public/fonts/plex-serif-600-latin-ext.woff2 +0 -0
- package/public/fonts/plex-serif-600-latin.woff2 +0 -0
- package/public/index.html +1268 -0
- package/public/vendor/xterm-addon-fit.js +2 -0
- package/public/vendor/xterm.css +218 -0
- package/public/vendor/xterm.js +2 -0
- package/skills/debugging/SKILL.md +33 -0
- package/skills/lampson/SKILL.md +117 -0
- package/skills/synsema/SKILL.md +75 -0
- 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
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|