lampson 0.1.0 → 0.1.1

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.
@@ -1,46 +1,46 @@
1
- -- lib/tools/process.syn — tool `process`: servidores y watchers gestionados (start / logs / stop / list)
2
- use "./proc.syn" as proc
3
-
4
- export task tool(action, name, command, tail)
5
- require exec
6
- require time
7
- require env("LAMPSON_*")
8
- require env("OS")
9
- require file(".lampson")
10
- require file(".lampson/*")
11
- when action == "start"
12
- when command == nothing or command == ""
13
- raise("start needs a command")
14
- give proc.start(name, command)
15
- when action == "logs"
16
- give proc.logs(name, tail)
17
- when action == "stop"
18
- give proc.halt(name)
19
- when action == "ports"
20
- let ls be proc.listeners()
21
- when length(ls) == 0
22
- give "no TCP ports listening (>=1024)"
23
- let lines be []
24
- each l in ls
25
- set lines to append(lines, "port " + text(l["port"]) + " pid " + text(l["pid"]) + " " + l["name"])
26
- give join(lines, "\n")
27
- when action == "list"
28
- let items be proc.list()
29
- when length(items) == 0
30
- give "no managed processes"
31
- let lines be []
32
- each p in items
33
- set lines to append(lines, p["name"] + " " + (when p["running"] then "running" otherwise "exited") + " $ " + p["command"])
34
- give join(lines, "\n")
35
- raise("unknown action '" + text(action) + "' (start | logs | stop | list | ports)")
36
-
37
- export let SPEC be {
38
- "name": "process",
39
- "description": "Manage long-running processes (dev servers, watchers, APIs) so you can SEE their output. start: launches `command` detached with a `name`; its stdout/stderr go to a log you can read any time. After every bash call you automatically receive the NEW log lines of every managed process, so errors in a server you started reach you without asking. logs: last `tail` lines (default 60). A process is EXITED only when its pid is really gone; then its log ends with `[process exited with code N]` — read the reason there instead of retrying the command in bash. stop: kills the process and its whole tree (npm → node included). list: what is running. ports: every TCP port listening on the machine with its pid and program — use it when a port is busy (EADDRINUSE) or to check whether something is already up. Always use this instead of `cmd &` in bash, and stop what you started when the task is done.",
40
- "parameters": {"type": "object", "properties": {
41
- "action": {"type": "string", "enum": ["start", "logs", "stop", "list", "ports"]},
42
- "name": {"type": "string", "description": "Short id: letters, digits, - or _ (e.g. web, api, dev)"},
43
- "command": {"type": "string", "description": "start only: the shell command (runs with cwd = workspace root)"},
44
- "tail": {"type": "integer", "description": "logs only: number of lines (default 60)"}
45
- }, "required": ["action"]}
46
- }
1
+ -- lib/tools/process.syn — tool `process`: servidores y watchers gestionados (start / logs / stop / list)
2
+ use "./proc.syn" as proc
3
+
4
+ export task tool(action, name, command, tail)
5
+ require exec
6
+ require time
7
+ require env("LAMPSON_*")
8
+ require env("OS")
9
+ require file(".lampson")
10
+ require file(".lampson/*")
11
+ when action == "start"
12
+ when command == nothing or command == ""
13
+ raise("start needs a command")
14
+ give proc.start(name, command)
15
+ when action == "logs"
16
+ give proc.logs(name, tail)
17
+ when action == "stop"
18
+ give proc.halt(name)
19
+ when action == "ports"
20
+ let ls be proc.listeners()
21
+ when length(ls) == 0
22
+ give "no TCP ports listening (>=1024)"
23
+ let lines be []
24
+ each l in ls
25
+ set lines to append(lines, "port " + text(l["port"]) + " pid " + text(l["pid"]) + " " + l["name"])
26
+ give join(lines, "\n")
27
+ when action == "list"
28
+ let items be proc.list()
29
+ when length(items) == 0
30
+ give "no managed processes"
31
+ let lines be []
32
+ each p in items
33
+ set lines to append(lines, p["name"] + " " + (when p["running"] then "running" otherwise "exited") + " $ " + p["command"])
34
+ give join(lines, "\n")
35
+ raise("unknown action '" + text(action) + "' (start | logs | stop | list | ports)")
36
+
37
+ export let SPEC be {
38
+ "name": "process",
39
+ "description": "Manage long-running processes (dev servers, watchers, APIs) so you can SEE their output. start: launches `command` detached with a `name`; its stdout/stderr go to a log you can read any time. After every bash call you automatically receive the NEW log lines of every managed process, so errors in a server you started reach you without asking. logs: last `tail` lines (default 60). A process is EXITED only when its pid is really gone; then its log ends with `[process exited with code N]` — read the reason there instead of retrying the command in bash. stop: kills the process and its whole tree (npm → node included). list: what is running. ports: every TCP port listening on the machine with its pid and program — use it when a port is busy (EADDRINUSE) or to check whether something is already up. Always use this instead of `cmd &` in bash, and stop what you started when the task is done.",
40
+ "parameters": {"type": "object", "properties": {
41
+ "action": {"type": "string", "enum": ["start", "logs", "stop", "list", "ports"]},
42
+ "name": {"type": "string", "description": "Short id: letters, digits, - or _ (e.g. web, api, dev)"},
43
+ "command": {"type": "string", "description": "start only: the shell command (runs with cwd = workspace root)"},
44
+ "tail": {"type": "integer", "description": "logs only: number of lines (default 60)"}
45
+ }, "required": ["action"]}
46
+ }
package/lib/tools.syn CHANGED
@@ -1,198 +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()
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/tree.syn CHANGED
@@ -1,59 +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"))}
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"))}