lampson 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -10
- package/bin/lampson.js +13 -7
- package/chat.syn +36 -31
- package/lampson.ps1 +11 -1
- package/lampson.sh +7 -0
- package/lib/agents.syn +10 -10
- package/lib/fs.syn +226 -0
- package/lib/loop.syn +9 -8
- package/lib/permission.syn +40 -14
- package/lib/plugins.syn +408 -0
- package/lib/prompt.syn +3 -2
- package/lib/sched_run.syn +10 -10
- package/lib/schedule.syn +36 -21
- package/lib/tools/fetch.syn +573 -0
- package/lib/tools/memo.syn +27 -5
- package/lib/tools/url.syn +220 -0
- package/lib/tools.syn +32 -30
- package/lib/workspaces.syn +34 -9
- package/package.json +12 -4
- package/{lamps/example-hello/lamp.json → plugins/example-hello/plugin.json} +2 -2
- package/plugins/example-hello/plugin.syn +19 -0
- package/public/css/panel.css +3 -3
- package/public/css/sidebar.css +21 -0
- package/public/hub.html +1 -1
- package/public/index.html +3 -3
- package/public/js/app.js +1 -1
- package/public/js/chat.js +4 -4
- package/public/js/core.js +1 -1
- package/public/js/memory.js +2 -1
- package/public/js/{lamps.js → plugins.js} +43 -41
- package/public/js/schedules.js +9 -9
- package/public/js/tree.js +212 -10
- package/public/js/workspaces.js +1 -1
- package/skills/lampson/SKILL.md +26 -11
- package/web.syn +32 -17
- package/lamps/example-hello/lamp.syn +0 -19
- package/lib/lamps.syn +0 -386
package/README.md
CHANGED
|
@@ -60,15 +60,15 @@ It can only touch the folder you opened it in — not your home directory, not t
|
|
|
60
60
|
a setting: it's how the language it's written in works. Reading and editing just happen; deleting, installing,
|
|
61
61
|
`git reset`, `sudo` stop and wait for your *yes*; truly destructive commands are refused in every mode.
|
|
62
62
|
|
|
63
|
-
##
|
|
63
|
+
## Plugins
|
|
64
64
|
|
|
65
|
-
A **
|
|
65
|
+
A **plugin** is a small folder that gives Lampson a new tool: query your database, call your company's API,
|
|
66
66
|
deploy, send a message. Any language. **Off by default** — you turn each one on, and only then can the agent
|
|
67
67
|
use it.
|
|
68
68
|
|
|
69
69
|
```
|
|
70
|
-
~/lampson/
|
|
71
|
-
|
|
70
|
+
~/lampson/plugins/postgres/
|
|
71
|
+
plugin.json ← what it's called, what it does, which tools it offers
|
|
72
72
|
query.py ← the code
|
|
73
73
|
```
|
|
74
74
|
|
|
@@ -79,22 +79,28 @@ use it.
|
|
|
79
79
|
"parameters": {"type": "object", "properties": {"sql": {"type": "string"}}}, "readonly": true}]}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
The script gets the call in environment variables (`
|
|
83
|
-
Turn it on with `/
|
|
84
|
-
or inside a project (`.lampson/
|
|
82
|
+
The script gets the call in environment variables (`PLUGIN_TOOL`, `PLUGIN_ARGS` as JSON) and prints the result.
|
|
83
|
+
Turn it on with `/plugins on postgres` or the **plugins** pill in the web UI. Keep plugins global (every project)
|
|
84
|
+
or inside a project (`.lampson/plugins/` — commit it and your team has it). The agent can *write* a plugin for you;
|
|
85
85
|
turning it on is always yours.
|
|
86
86
|
|
|
87
|
-
→ [How
|
|
87
|
+
→ [How plugins work](https://lampson.org/docs/plugins) · the example ships in `plugins/example-hello/`.
|
|
88
|
+
|
|
89
|
+
Plugins are yours and local: any language, no sandbox unless you write them in Synsema. For tools with an
|
|
90
|
+
**enforced capability ceiling**, versioned and shared with any agent (Claude Code, Cursor, Lampson…), use
|
|
91
|
+
[lamps.sh](https://lamps.sh): `lamp add <ref>`, then `lamp mcp` as an MCP server in Lampson. (Until 0.2.6
|
|
92
|
+
plugins were called *lamps*; old `.lampson/lamps/` folders and `LAMP_*` variables still work for now.)
|
|
88
93
|
|
|
89
94
|
## Also in the box
|
|
90
95
|
|
|
91
96
|
Sub-agents that work in parallel · skills (`SKILL.md` procedures, anything on [skills.sh](https://skills.sh)
|
|
92
97
|
works) · MCP servers with the JSON you already have · language servers for real go-to-definition · project
|
|
93
|
-
memory it reads back next session · sessions with a readable trace of every step ·
|
|
98
|
+
memory it reads back next session · sessions with a readable trace of every step · web pages fetched as
|
|
99
|
+
Markdown (a tenth of the tokens of raw HTML) · paste a screenshot and ask.
|
|
94
100
|
|
|
95
101
|
## Learn more
|
|
96
102
|
|
|
97
|
-
- **[lampson.org/docs](https://lampson.org/docs)** — quickstart, permissions, providers,
|
|
103
|
+
- **[lampson.org/docs](https://lampson.org/docs)** — quickstart, permissions, providers, plugins, schedules…
|
|
98
104
|
- **[guide.md](guide.md)** — the long version: how it runs, architecture, every knob, runtime notes.
|
|
99
105
|
|
|
100
106
|
## License
|
package/bin/lampson.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// bin/lampson.js — entry point of the npm package (`npm i -g lampson`).
|
|
3
3
|
//
|
|
4
4
|
// The code ships inside node_modules, but Lampson keeps STATE next to its code: the mounted `workspace`
|
|
5
|
-
// junction, `.lampson/` (config, sessions, traces, spill), `memory/` and global `
|
|
5
|
+
// junction, `.lampson/` (config, sessions, traces, spill), `memory/` and global `plugins/`. Living inside
|
|
6
6
|
// node_modules would lose all of that on every `npm i -g lampson@latest`. So this launcher keeps a stable
|
|
7
7
|
// home (LAMPSON_HOME, default ~/lampson), syncs the package's code files into it when the version changes,
|
|
8
8
|
// and runs the same launcher the git install uses (lampson.ps1 / lampson.sh). Everything else — synsema on
|
|
@@ -45,6 +45,9 @@ function sync() {
|
|
|
45
45
|
try { installed = fs.readFileSync(marker, 'utf8').trim(); } catch (e) { /* first run */ }
|
|
46
46
|
if (installed === pkg.version && fs.existsSync(path.join(home, 'chat.syn'))) return 'ok';
|
|
47
47
|
fs.mkdirSync(home, { recursive: true });
|
|
48
|
+
// the folders every workspace links to (lib/workspaces.syn LINKS): if `memory/` does not exist when the
|
|
49
|
+
// link is made, the link is born dangling and memory(write) fails with "No such file or directory"
|
|
50
|
+
for (const d of ['memory', 'plugins']) fs.mkdirSync(path.join(home, d), { recursive: true });
|
|
48
51
|
for (const item of CODE) {
|
|
49
52
|
const src = path.join(pkgDir, item);
|
|
50
53
|
if (!fs.existsSync(src)) continue;
|
|
@@ -52,11 +55,14 @@ function sync() {
|
|
|
52
55
|
if (fs.statSync(src).isDirectory()) { fs.rmSync(dst, { recursive: true, force: true }); copyDir(src, dst); }
|
|
53
56
|
else copyFile(src, dst);
|
|
54
57
|
}
|
|
55
|
-
// global
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
// global plugins are user content: seed the example once, never overwrite what the user put there.
|
|
59
|
+
// Until 2026-08 the folder was `lamps/` (they were "lamps"): an existing one is renamed once, same content.
|
|
60
|
+
const legacyDst = path.join(home, 'lamps'), pluginsDst = path.join(home, 'plugins');
|
|
61
|
+
if (fs.existsSync(legacyDst) && !fs.existsSync(pluginsDst)) { fs.renameSync(legacyDst, pluginsDst); console.log('lampson: renamed ' + legacyDst + ' -> ' + pluginsDst + ' (lamps are now called plugins)'); }
|
|
62
|
+
const pluginsSrc = path.join(pkgDir, 'plugins');
|
|
63
|
+
if (fs.existsSync(pluginsSrc)) {
|
|
64
|
+
for (const e of fs.readdirSync(pluginsSrc, { withFileTypes: true })) {
|
|
65
|
+
if (e.isDirectory() && !fs.existsSync(path.join(pluginsDst, e.name))) copyDir(path.join(pluginsSrc, e.name), path.join(pluginsDst, e.name));
|
|
60
66
|
}
|
|
61
67
|
}
|
|
62
68
|
fs.writeFileSync(marker, pkg.version + '\n');
|
|
@@ -68,7 +74,7 @@ if (args.some(a => /^--?(version|v)$/i.test(a))) { console.log('lampson ' + pkg.
|
|
|
68
74
|
|
|
69
75
|
let state;
|
|
70
76
|
try { state = sync(); } catch (e) { console.error('lampson: could not prepare ' + home + ': ' + e.message); process.exit(1); }
|
|
71
|
-
if (state === 'installed') console.log('lampson ' + pkg.version + ' → ' + home + ' (your config, sessions and
|
|
77
|
+
if (state === 'installed') console.log('lampson ' + pkg.version + ' → ' + home + ' (your config, sessions and plugins live there)');
|
|
72
78
|
if (state === 'updated') console.log('lampson updated to ' + pkg.version + ' in ' + home);
|
|
73
79
|
|
|
74
80
|
// --update under npm: the code comes from the registry, not from git
|
package/chat.syn
CHANGED
|
@@ -22,8 +22,8 @@ require file("workspace")
|
|
|
22
22
|
require file("workspace/*")
|
|
23
23
|
require file.read("skills")
|
|
24
24
|
require file.read("skills/*")
|
|
25
|
-
require file.read("
|
|
26
|
-
require file.read("
|
|
25
|
+
require file.read("plugins")
|
|
26
|
+
require file.read("plugins/*")
|
|
27
27
|
require file("memory")
|
|
28
28
|
require file("memory/*")
|
|
29
29
|
require file(".lampson")
|
|
@@ -49,7 +49,7 @@ use "./lib/diff.syn" as diff
|
|
|
49
49
|
use "./lib/line.syn" as ed
|
|
50
50
|
use "./lib/ui.syn" as ui
|
|
51
51
|
use "./lib/mcp.syn" as mcp
|
|
52
|
-
use "./lib/
|
|
52
|
+
use "./lib/plugins.syn" as plugins
|
|
53
53
|
use "./lib/lsp.syn" as lsp
|
|
54
54
|
use "./lib/tools/todo.syn" as todo
|
|
55
55
|
use "./lib/schedule.syn" as schedule
|
|
@@ -165,7 +165,7 @@ task print_diff_with(lead, path, d)
|
|
|
165
165
|
|
|
166
166
|
-- tools "instantáneas": la llamada se imprime junto con su resultado, en una sola línea
|
|
167
167
|
task is_instant(name)
|
|
168
|
-
give name == "read" or name == "ls" or name == "find" or name == "grep" or name == "memory" or name == "edit" or name == "write" or name == "todo" or name == "lsp"
|
|
168
|
+
give name == "read" or name == "ls" or name == "find" or name == "grep" or name == "memory" or name == "edit" or name == "write" or name == "todo" or name == "lsp" or name == "fetch"
|
|
169
169
|
|
|
170
170
|
let pending_call be ""
|
|
171
171
|
|
|
@@ -348,9 +348,9 @@ let COMMANDS be [
|
|
|
348
348
|
["/agents", "", "subagentes lanzados con delegate: estado, pasos y log de cada uno"],
|
|
349
349
|
["/todo", "", "lista de tareas del agente en esta sesión (la mantiene con la tool todo)"],
|
|
350
350
|
["/mcp", "[add <nombre> <comando…> [--project] | remove <nombre>]", "servers MCP: listar, conectar o quitar (global: lampson/.lampson/mcp.json · proyecto: .lampson/mcp.json)"],
|
|
351
|
-
["/
|
|
351
|
+
["/plugins", "[on <nombre> | off <nombre> | run <plugin> <tool> [json] | remove <nombre>]", "plugins (tools propias, en cualquier lenguaje): listar, encender o apagar (global: lampson/plugins/ · proyecto: .lampson/plugins/)"],
|
|
352
352
|
["/lsp", "[add <typescript|python|rust|go|css|html> [--project] | add <nombre> <comando…> --ext .x=lang | remove <nombre>]", "language servers (navegación semántica: symbols/definition/references/hover); arrancan en la primera consulta"],
|
|
353
|
-
["/schedule", "[add <json> | run <id> | on <id> | off <id> | remove <id> | log <id>]", "tareas programadas (cada 6h, todos los días a las 9…):
|
|
353
|
+
["/schedule", "[add <json> | run <id> | on <id> | off <id> | remove <id> | log <id>]", "tareas programadas (cada 6h, todos los días a las 9…): plugin, comando o corrida del agente; corren mientras lampson esté abierto (o con lampson --daemon start, sin nada abierto)"],
|
|
354
354
|
["/approve", "<id> [yes|no]", "responder una aprobación pendiente de una tarea programada que corre en background (sin yes|no la pregunta con el menú ↑↓)"],
|
|
355
355
|
["/out", "[n]", "resultado completo de la última tool del turno (n = contar hacia atrás: /out 2 es la anteúltima)"],
|
|
356
356
|
["/verbose", "", "alternar: mostrar SIEMPRE el output completo de cada tool (queda guardado en config.json)"],
|
|
@@ -393,10 +393,10 @@ task complete_args(cmd, head, last)
|
|
|
393
393
|
give []
|
|
394
394
|
when cmd == "/memory"
|
|
395
395
|
give apply((m) => m["name"], memo.list())
|
|
396
|
-
when cmd == "/
|
|
396
|
+
when cmd == "/plugins"
|
|
397
397
|
when first
|
|
398
398
|
give ["on", "off", "run", "remove"]
|
|
399
|
-
give apply((l) => l["name"],
|
|
399
|
+
give apply((l) => l["name"], plugins.summary())
|
|
400
400
|
when cmd == "/mcp"
|
|
401
401
|
when first
|
|
402
402
|
give ["add", "remove"]
|
|
@@ -565,21 +565,21 @@ task banner(ws, cfg, profile, mode, sid)
|
|
|
565
565
|
print(" workspace " + ws)
|
|
566
566
|
print(" agente " + profile + " permisos " + mode + " modelo " + cfg["model"])
|
|
567
567
|
print(" sesión " + sid + " " + git.summary())
|
|
568
|
-
-- extensiones en una línea (detalle con /
|
|
569
|
-
let ls be
|
|
570
|
-
let
|
|
568
|
+
-- extensiones en una línea (detalle con /plugins, /mcp, /lsp): encendidas/total
|
|
569
|
+
let ls be plugins.summary()
|
|
570
|
+
let plugins_on be length(where(ls, (l) => l["enabled"]))
|
|
571
571
|
let ms be mcp.summary()
|
|
572
572
|
let mcp_on be length(where(ms, (m) => m["status"] == "ready"))
|
|
573
573
|
let ss be lsp.summary()
|
|
574
574
|
let ext be []
|
|
575
575
|
when length(ls) > 0
|
|
576
|
-
set ext to append(ext, (when
|
|
576
|
+
set ext to append(ext, (when plugins_on > 0 then green("plugins " + text(plugins_on) + "/" + text(length(ls))) otherwise dim("plugins 0/" + text(length(ls)))))
|
|
577
577
|
when length(ms) > 0
|
|
578
578
|
set ext to append(ext, (when mcp_on == length(ms) then green("mcp " + text(mcp_on) + "/" + text(length(ms))) otherwise red("mcp " + text(mcp_on) + "/" + text(length(ms)))))
|
|
579
579
|
when length(ss) > 0
|
|
580
580
|
set ext to append(ext, dim("lsp " + text(length(ss))))
|
|
581
581
|
when length(ext) > 0
|
|
582
|
-
print(" extras " + join(ext, dim(" · ")) + dim(" (/
|
|
582
|
+
print(" extras " + join(ext, dim(" · ")) + dim(" (/plugins /mcp /lsp)"))
|
|
583
583
|
let upd be update.line()
|
|
584
584
|
when upd != ""
|
|
585
585
|
print(" " + yellow("⬆ " + upd) + dim(" (o /update acá)"))
|
|
@@ -815,8 +815,8 @@ agent Sched
|
|
|
815
815
|
require file("workspace/*")
|
|
816
816
|
require file.read("skills")
|
|
817
817
|
require file.read("skills/*")
|
|
818
|
-
require file.read("
|
|
819
|
-
require file.read("
|
|
818
|
+
require file.read("plugins")
|
|
819
|
+
require file.read("plugins/*")
|
|
820
820
|
require file("memory")
|
|
821
821
|
require file("memory/*")
|
|
822
822
|
require file(".lampson")
|
|
@@ -902,13 +902,13 @@ let system_msg be system_for(profile)
|
|
|
902
902
|
-- servers MCP (globales en .lampson/mcp.json, del proyecto en workspace/.lampson/mcp.json): arrancan antes
|
|
903
903
|
-- de armar el catálogo de tools; hasta 8 s de espera a que estén listos
|
|
904
904
|
let mcp_servers be mcp.start_all(8)
|
|
905
|
-
-- mcp / lsp /
|
|
905
|
+
-- mcp / lsp / plugins van resumidos en el banner ("extras"); acá solo se avisa lo que está ROTO
|
|
906
906
|
each ms in mcp.summary()
|
|
907
907
|
when ms["status"] != "ready"
|
|
908
908
|
print(" " + red("○ mcp " + ms["name"] + " " + ms["status"]) + dim(when ms["error"] != nothing then " · " + text(ms["error"]) otherwise ""))
|
|
909
|
-
each l in
|
|
909
|
+
each l in plugins.summary()
|
|
910
910
|
when l["error"] != nothing
|
|
911
|
-
print(" " + red("○
|
|
911
|
+
print(" " + red("○ plugin " + l["name"] + " roto") + dim(" · " + text(l["error"])))
|
|
912
912
|
let opts be opts_for(profile, lower(env("LAMPSON_PERMISSION", "ask")))
|
|
913
913
|
|
|
914
914
|
-- marca de corrida (blackboard): session.save la estampa en meta.run; reanudar una sesión guardada por
|
|
@@ -1181,7 +1181,7 @@ while running
|
|
|
1181
1181
|
let sjson be trim(slice(srest, 3, length(srest)))
|
|
1182
1182
|
when sjson == ""
|
|
1183
1183
|
print(" uso: /schedule add {\"name\": \"…\", \"when\": \"daily 09:00\", \"action\": {\"type\": \"bash\", \"command\": \"npm test\"}, \"permission\": \"ask\"}")
|
|
1184
|
-
print(" " + dim("when: every 6h · daily 09:00 · mon,wed 08:30 · weekdays 09:00 action.type:
|
|
1184
|
+
print(" " + dim("when: every 6h · daily 09:00 · mon,wed 08:30 · weekdays 09:00 action.type: plugin {plugin, tool, args} · bash {command} · prompt {prompt, agent}"))
|
|
1185
1185
|
print(" " + dim("o pedíselo al agente en lenguaje natural: «todos los días a las 9 revisá los tests y avisame»"))
|
|
1186
1186
|
otherwise
|
|
1187
1187
|
try
|
|
@@ -1249,13 +1249,17 @@ while running
|
|
|
1249
1249
|
print(" " + (when yes then green("✓ permitido ") otherwise red("✗ denegado ")) + atoks[0])
|
|
1250
1250
|
otherwise
|
|
1251
1251
|
print(" " + red("no hay una aprobación pendiente con id " + atoks[0]))
|
|
1252
|
-
otherwise when input == "/lamps" or starts_with(input, "/lamps ")
|
|
1253
|
-
|
|
1252
|
+
otherwise when input == "/plugins" or starts_with(input, "/plugins ") or input == "/lamps" or starts_with(input, "/lamps ")
|
|
1253
|
+
-- /lamps = el nombre viejo (hasta 2026-09): sigue andando, con aviso
|
|
1254
|
+
let is_old be starts_with(input, "/lamps")
|
|
1255
|
+
when is_old
|
|
1256
|
+
print(" " + dim("/lamps ahora es /plugins (las lámparas de lampson pasaron a llamarse plugins)"))
|
|
1257
|
+
let lrest be trim(slice(input, when is_old then 6 otherwise 8, length(input)))
|
|
1254
1258
|
when starts_with(lrest, "run ")
|
|
1255
|
-
-- /
|
|
1259
|
+
-- /plugins run <plugin> <tool> [json de args] → la corre el usuario, sin el modelo
|
|
1256
1260
|
let rt be where(split(trim(slice(lrest, 4, length(lrest))), " "), (x) => x != "")
|
|
1257
1261
|
when length(rt) < 2
|
|
1258
|
-
print(" uso: /
|
|
1262
|
+
print(" uso: /plugins run <plugin> <tool> [{\"arg\": \"valor\"}]")
|
|
1259
1263
|
otherwise
|
|
1260
1264
|
let rargs be {}
|
|
1261
1265
|
when length(rt) > 2
|
|
@@ -1264,30 +1268,31 @@ while running
|
|
|
1264
1268
|
recover err
|
|
1265
1269
|
print(" " + red("args: JSON inválido"))
|
|
1266
1270
|
try
|
|
1267
|
-
print(
|
|
1271
|
+
print(plugins.call(plugins.tool_name(rt[0], rt[1]), rargs))
|
|
1268
1272
|
recover err
|
|
1269
1273
|
print(" " + red(text(err)))
|
|
1270
1274
|
otherwise when starts_with(lrest, "remove ")
|
|
1271
1275
|
try
|
|
1272
|
-
print(" " +
|
|
1276
|
+
print(" " + plugins.remove(trim(slice(lrest, 7, length(lrest)))))
|
|
1273
1277
|
recover err
|
|
1274
1278
|
print(" " + red(text(err)))
|
|
1275
1279
|
set opts to opts_for(profile, mode)
|
|
1276
1280
|
otherwise when starts_with(lrest, "on ") or starts_with(lrest, "off ")
|
|
1277
1281
|
let lon be starts_with(lrest, "on ")
|
|
1278
1282
|
try
|
|
1279
|
-
print(" " +
|
|
1283
|
+
print(" " + plugins.set_enabled(trim(slice(lrest, when lon then 3 otherwise 4, length(lrest))), lon))
|
|
1280
1284
|
recover err
|
|
1281
1285
|
print(" " + red(text(err)))
|
|
1282
1286
|
set opts to opts_for(profile, mode)
|
|
1283
1287
|
otherwise
|
|
1284
|
-
let ls be
|
|
1288
|
+
let ls be plugins.summary()
|
|
1285
1289
|
when length(ls) == 0
|
|
1286
|
-
print(" sin
|
|
1287
|
-
print(" " + dim("el agente también puede
|
|
1290
|
+
print(" sin plugins. Un plugin es una carpeta con plugin.json: " + dim("lampson\\plugins\\<nombre>\\ (global) · .lampson\\plugins\\<nombre>\\ del repo (proyecto)"))
|
|
1291
|
+
print(" " + dim("el agente también puede crearlos (write en .lampson/plugins/) y pedirte encenderlos"))
|
|
1288
1292
|
each l in ls
|
|
1289
|
-
print(" " + (when l["enabled"] then green("● ") otherwise dim("○ ")) + pad(l["name"], 14) + dim(pad(l["scope"], 8) + pad(l["kind"], 5)) + (when l["error"] != nothing then red("
|
|
1290
|
-
print(" " + dim("encender/apagar: /
|
|
1293
|
+
print(" " + (when l["enabled"] then green("● ") otherwise dim("○ ")) + pad(l["name"], 14) + dim(pad(l["scope"], 8) + pad(l["kind"], 5)) + (when l["error"] != nothing then red("roto: " + text(l["error"])) otherwise dim(text(length(l["tools"])) + " tools [" + join(l["tools"], ", ") + "]" + (when l["kind"] == "syn" then " techo: " + l["caps"] otherwise " $ " + l["command"]))) + (when l["legacy"] then yellow(" carpeta vieja .lampson/lamps/ → renombrala a .lampson/plugins/") otherwise ""))
|
|
1294
|
+
print(" " + dim("encender/apagar: /plugins on <nombre> · /plugins off <nombre> · correr uno vos: /plugins run <plugin> <tool> [json]"))
|
|
1295
|
+
print(" " + dim("¿herramientas con techo real, versionadas y para cualquier agente? eso son las lámparas de lamps.sh: lamp add <ref> y entran por /mcp add lamps lamp mcp"))
|
|
1291
1296
|
otherwise when input == "/mcp" or starts_with(input, "/mcp ")
|
|
1292
1297
|
let mrest be trim(slice(input, 4, length(input)))
|
|
1293
1298
|
when starts_with(mrest, "add ")
|
package/lampson.ps1
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# lampson --agent plan · --yolo|--strict|--ask · --update · --help
|
|
9
9
|
#
|
|
10
10
|
# Cómo funciona (ver SPEC-WORKSPACES.md): .lampson\ws\<slug>\ es el cwd del proceso del workspace, con una junction
|
|
11
|
-
# `workspace` al proyecto y junctions a lib/public/skills/
|
|
11
|
+
# `workspace` al proyecto y junctions a lib/public/skills/plugins/memory de esta instalación. El registro y los procesos
|
|
12
12
|
# los maneja lib/workspaces.syn vía cli.syn; este script solo resuelve la carpeta, llama a cli.syn y abre lo pedido.
|
|
13
13
|
$ErrorActionPreference = "Stop"
|
|
14
14
|
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
@@ -45,6 +45,16 @@ foreach ($k in $skillMounts.Keys) {
|
|
|
45
45
|
$link = Join-Path $here ".lampson\$k"; $src = $skillMounts[$k]
|
|
46
46
|
if (-not (Test-Path -LiteralPath $link) -and (Test-Path -LiteralPath $src -PathType Container)) { New-Item -ItemType Junction -Path $link -Target $src | Out-Null }
|
|
47
47
|
}
|
|
48
|
+
# 2026-09: las "lámparas" globales pasaron a llamarse plugins (plugins\). git renombra las del repo; las carpetas que
|
|
49
|
+
# vos pusiste en lamps\ se mueven una vez, y la carpeta vieja se borra solo si quedó vacía.
|
|
50
|
+
$oldLamps = Join-Path $here "lamps"
|
|
51
|
+
if (Test-Path -LiteralPath $oldLamps -PathType Container) {
|
|
52
|
+
$newPlugins = Join-Path $here "plugins"; New-Item -ItemType Directory -Force $newPlugins | Out-Null
|
|
53
|
+
foreach ($d in Get-ChildItem -LiteralPath $oldLamps -Directory -Force) {
|
|
54
|
+
if (-not (Test-Path -LiteralPath (Join-Path $newPlugins $d.Name))) { Move-Item -LiteralPath $d.FullName -Destination $newPlugins; Write-Host "lampson: lamps\$($d.Name) → plugins\$($d.Name) (las lámparas ahora son plugins)" }
|
|
55
|
+
}
|
|
56
|
+
if (-not (Get-ChildItem -LiteralPath $oldLamps -Force)) { Remove-Item -LiteralPath $oldLamps -Force }
|
|
57
|
+
}
|
|
48
58
|
|
|
49
59
|
function Invoke-Cli([string]$cmd, [string]$ws) {
|
|
50
60
|
# cli.syn imprime la respuesta como la ÚLTIMA línea (JSON)
|
package/lampson.sh
CHANGED
|
@@ -32,6 +32,13 @@ for pair in "skills-global:$HOME/.agents/skills" "skills-claude:$HOME/.claude/sk
|
|
|
32
32
|
link="$here/.lampson/${pair%%:*}"; src="${pair#*:}"
|
|
33
33
|
[ -e "$link" ] || { [ -d "$src" ] && ln -s "$src" "$link"; } || true
|
|
34
34
|
done
|
|
35
|
+
# 2026-09: las "lámparas" globales pasaron a llamarse plugins (plugins/). git renombra las del repo; las carpetas
|
|
36
|
+
# que vos pusiste en lamps/ se mueven una vez, y la carpeta vieja se borra solo si quedó vacía.
|
|
37
|
+
if [ -d "$here/lamps" ]; then
|
|
38
|
+
mkdir -p "$here/plugins"
|
|
39
|
+
for d in "$here"/lamps/*/; do [ -d "$d" ] || continue; n="$(basename "$d")"; [ -e "$here/plugins/$n" ] || { mv "$d" "$here/plugins/$n" && echo "lampson: lamps/$n → plugins/$n (las lámparas ahora son plugins)"; }; done
|
|
40
|
+
rmdir "$here/lamps" 2>/dev/null || true
|
|
41
|
+
fi
|
|
35
42
|
cli() { # última línea = JSON
|
|
36
43
|
local out; out="$(cd "$here" && LAMPSON_CMD="$1" LAMPSON_WORKSPACE="${2:-}" synsema run cli.syn)" || { echo "$out" >&2; exit 1; }
|
|
37
44
|
echo "$out" | sed '$d' >&2 || true
|
package/lib/agents.syn
CHANGED
|
@@ -32,7 +32,7 @@ use "./loop.syn" as loop
|
|
|
32
32
|
use "./provider.syn" as provider
|
|
33
33
|
use "./prompt.syn" as prompt
|
|
34
34
|
use "./mcp.syn" as mcp
|
|
35
|
-
use "./
|
|
35
|
+
use "./plugins.syn" as plugins
|
|
36
36
|
|
|
37
37
|
export let DIR be ".lampson/agents"
|
|
38
38
|
let MAX_TASKS be 6
|
|
@@ -40,27 +40,27 @@ let PARALLEL be 4
|
|
|
40
40
|
|
|
41
41
|
export let PROFILES be {
|
|
42
42
|
"build": {
|
|
43
|
-
"tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "bash", "process", "memory", "todo", "skill", "mcp", "
|
|
43
|
+
"tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "fetch", "bash", "process", "memory", "todo", "skill", "mcp", "plugin", "schedule", "delegate"],
|
|
44
44
|
"steps": 40,
|
|
45
45
|
"addendum": "You are in BUILD mode: you may read, edit, create files and run commands. Delegate exploration of large codebases to the `explore` agent and independent code review to the `review` agent when it saves context; several independent questions can go in one delegate call (they run in parallel)."
|
|
46
46
|
},
|
|
47
47
|
"plan": {
|
|
48
|
-
"tools": ["read", "ls", "find", "grep", "lsp", "memory", "todo", "skill", "delegate"],
|
|
48
|
+
"tools": ["read", "ls", "find", "grep", "lsp", "fetch", "memory", "todo", "skill", "delegate"],
|
|
49
49
|
"steps": 30,
|
|
50
50
|
"addendum": "You are in PLAN mode: READ-ONLY. You cannot edit files or run commands (those tools are not available). Investigate the codebase, then answer with a concrete, numbered implementation plan: files to touch, what changes in each, risks, and how to verify. Do not write code beyond short illustrative snippets."
|
|
51
51
|
},
|
|
52
52
|
"review": {
|
|
53
|
-
"tools": ["read", "ls", "find", "grep", "lsp", "bash", "process", "memory", "skill"],
|
|
53
|
+
"tools": ["read", "ls", "find", "grep", "lsp", "fetch", "bash", "process", "memory", "skill"],
|
|
54
54
|
"steps": 25,
|
|
55
55
|
"addendum": "You are a REVIEWER: read-only on files (no write/edit), but you may run commands (tests, linters, git diff). Find real bugs, missing error handling, security issues and violations of the project's conventions. Report findings as a list with file:line, severity and a suggested fix. Do not modify files."
|
|
56
56
|
},
|
|
57
57
|
"explore": {
|
|
58
|
-
"tools": ["read", "ls", "find", "grep", "lsp", "memory", "skill"],
|
|
58
|
+
"tools": ["read", "ls", "find", "grep", "lsp", "fetch", "memory", "skill"],
|
|
59
59
|
"steps": 20,
|
|
60
60
|
"addendum": "You are an EXPLORER: read-only. Locate the code relevant to the question (files, functions, line numbers) and report it concisely with paths and short excerpts. Do not propose changes."
|
|
61
61
|
},
|
|
62
62
|
"worker": {
|
|
63
|
-
"tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "bash", "process", "memory", "todo", "skill"],
|
|
63
|
+
"tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "fetch", "bash", "process", "memory", "todo", "skill"],
|
|
64
64
|
"steps": 40,
|
|
65
65
|
"addendum": "You are a WORKER sub-agent: implement exactly the scoped task you were given (read, edit, run tests), then report what you changed (files) and how you verified it. Stay inside the scope; if something outside it is needed, report it instead of doing it."
|
|
66
66
|
}
|
|
@@ -105,8 +105,8 @@ export task registry_for(name)
|
|
|
105
105
|
set reg["delegate"] to delegate
|
|
106
106
|
each m in mcp.names(mcp_readonly_only(name))
|
|
107
107
|
set reg[m] to "mcp"
|
|
108
|
-
each m in
|
|
109
|
-
set reg[m] to "
|
|
108
|
+
each m in plugins.names(mcp_readonly_only(name))
|
|
109
|
+
set reg[m] to "plugin"
|
|
110
110
|
give reg
|
|
111
111
|
|
|
112
112
|
export task catalog_for(name)
|
|
@@ -119,7 +119,7 @@ export task catalog_for(name)
|
|
|
119
119
|
let cat be tools.catalog_subset(p["tools"])
|
|
120
120
|
when contains(p["tools"], "delegate")
|
|
121
121
|
set cat to append(cat, DELEGATE_SPEC)
|
|
122
|
-
give cat + mcp.catalog(mcp_readonly_only(name)) +
|
|
122
|
+
give cat + mcp.catalog(mcp_readonly_only(name)) + plugins.catalog(mcp_readonly_only(name))
|
|
123
123
|
|
|
124
124
|
-- Info de entorno para el system prompt (compartida por chat.syn / web.syn / delegate)
|
|
125
125
|
export task env_info(cfg)
|
|
@@ -128,7 +128,7 @@ export task env_info(cfg)
|
|
|
128
128
|
require time
|
|
129
129
|
let sc be tools.shell_config()
|
|
130
130
|
-- desde dónde habla el usuario (blackboard "lampson:ui", lo comparte el entry): el modelo NO debe decirle
|
|
131
|
-
-- "/
|
|
131
|
+
-- "/plugins" a alguien que está en la web (2026-08-28)
|
|
132
132
|
observe "lampson:ui" as ui
|
|
133
133
|
give {
|
|
134
134
|
"ui": when ui == nothing then "terminal" otherwise text(ui["kind"]),
|
package/lib/fs.syn
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
-- lib/fs.syn — operaciones de archivos del explorador web: crear, renombrar, mover, copiar, borrar
|
|
2
|
+
--
|
|
3
|
+
-- El runtime tiene write_file/list_dir/file_info pero NO rename/delete/copy/mkdir (v0.6.14), así que mover,
|
|
4
|
+
-- copiar y borrar corren por el shell POSIX (Git Bash en Windows), igual que plugins.remove. Todo path viene
|
|
5
|
+
-- del navegador y pasa por common.ws(): relativo al workspace, sin `..` ni absolutos (raise instructivo).
|
|
6
|
+
-- Reglas al estilo VS Code: nunca pisar un destino existente; duplicar = "<nombre> copy<.ext>", luego
|
|
7
|
+
-- "<nombre> copy 2<.ext>"…; una carpeta no se mueve/copia dentro de sí misma; la raíz no se toca.
|
|
8
|
+
use "./tools/common.syn" as c
|
|
9
|
+
|
|
10
|
+
task sh_quote(s)
|
|
11
|
+
give "'" + replace_text(s, "'", "'\\''") + "'"
|
|
12
|
+
|
|
13
|
+
-- shell POSIX aunque LAMPSON_SHELL apunte a cmd: mv/cp/rm/mkdir son de Git Bash
|
|
14
|
+
task shell()
|
|
15
|
+
require env("LAMPSON_*")
|
|
16
|
+
require env("OS")
|
|
17
|
+
let is_win be env("OS", "") == "Windows_NT"
|
|
18
|
+
let sh be env("LAMPSON_SHELL", when is_win then "C:\\Program Files\\Git\\bin\\bash.exe" otherwise "bash")
|
|
19
|
+
when is_win and not c.ends_with(lower(sh), "bash.exe")
|
|
20
|
+
set sh to "C:\\Program Files\\Git\\bin\\bash.exe"
|
|
21
|
+
give sh
|
|
22
|
+
|
|
23
|
+
-- corre un comando con cwd = workspace; error legible si falla
|
|
24
|
+
task sh(cmd)
|
|
25
|
+
require exec
|
|
26
|
+
require env("LAMPSON_*")
|
|
27
|
+
require env("OS")
|
|
28
|
+
let r be run(shell(), ["-c", cmd], 30, {"cwd": c.ROOT})
|
|
29
|
+
when r["exit_code"] != 0
|
|
30
|
+
let err be trim(text(r["stderr"]))
|
|
31
|
+
raise(when err == "" then "command failed: " + cmd otherwise err)
|
|
32
|
+
give true
|
|
33
|
+
|
|
34
|
+
-- path del navegador → {rel (como lo ve el modelo, "." = raíz), real ("workspace/…"), info}
|
|
35
|
+
task target(path)
|
|
36
|
+
require file.read("workspace")
|
|
37
|
+
require file.read("workspace/*")
|
|
38
|
+
let real be c.ws(path)
|
|
39
|
+
give {"rel": c.unws(real), "real": real, "info": file_info(real)}
|
|
40
|
+
|
|
41
|
+
task parent_of(rel)
|
|
42
|
+
when not contains(rel, "/")
|
|
43
|
+
give "."
|
|
44
|
+
let parts be split(rel, "/")
|
|
45
|
+
give join(slice(parts, 0, length(parts) - 1), "/")
|
|
46
|
+
|
|
47
|
+
task base_of(rel)
|
|
48
|
+
let parts be split(rel, "/")
|
|
49
|
+
give parts[length(parts) - 1]
|
|
50
|
+
|
|
51
|
+
task join_rel(dir, name)
|
|
52
|
+
give when dir == "." or dir == "" then name otherwise dir + "/" + name
|
|
53
|
+
|
|
54
|
+
task check_name(name)
|
|
55
|
+
let n be trim(text(name))
|
|
56
|
+
when n == "" or n == "." or n == ".." or contains(n, "/") or contains(n, "\\")
|
|
57
|
+
raise("invalid name \"" + text(name) + "\": one path segment, without / or \\")
|
|
58
|
+
when length(find_all(n, "[<>:\"|?*]")) > 0
|
|
59
|
+
raise("invalid name \"" + n + "\": < > : \" | ? * are not allowed in file names")
|
|
60
|
+
give n
|
|
61
|
+
|
|
62
|
+
task must_exist(t)
|
|
63
|
+
when not t["info"]["exists"]
|
|
64
|
+
raise("\"" + t["rel"] + "\" does not exist")
|
|
65
|
+
|
|
66
|
+
task must_not_exist(rel)
|
|
67
|
+
require file.read("workspace")
|
|
68
|
+
require file.read("workspace/*")
|
|
69
|
+
when file_exists(c.ROOT + "/" + rel)
|
|
70
|
+
raise("\"" + rel + "\" already exists")
|
|
71
|
+
|
|
72
|
+
-- ¿`path` es `dir` o está dentro de `dir`?
|
|
73
|
+
task inside(dir, path)
|
|
74
|
+
when dir == "."
|
|
75
|
+
give true
|
|
76
|
+
give path == dir or starts_with(path, dir + "/")
|
|
77
|
+
|
|
78
|
+
task must_be_dir(rel, info)
|
|
79
|
+
when not info["exists"]
|
|
80
|
+
raise("\"" + rel + "\" does not exist")
|
|
81
|
+
when not info["is_dir"]
|
|
82
|
+
raise("\"" + rel + "\" is not a folder")
|
|
83
|
+
|
|
84
|
+
export task mkdir(dir, name)
|
|
85
|
+
require exec
|
|
86
|
+
require env("LAMPSON_*")
|
|
87
|
+
require env("OS")
|
|
88
|
+
require file("workspace")
|
|
89
|
+
require file("workspace/*")
|
|
90
|
+
let t be target(dir)
|
|
91
|
+
let rel be join_rel(t["rel"], check_name(name))
|
|
92
|
+
must_not_exist(rel)
|
|
93
|
+
sh("mkdir -p " + sh_quote(rel))
|
|
94
|
+
give {"path": rel, "is_dir": true}
|
|
95
|
+
|
|
96
|
+
export task create(dir, name, content)
|
|
97
|
+
require file("workspace")
|
|
98
|
+
require file("workspace/*")
|
|
99
|
+
let t be target(dir)
|
|
100
|
+
let rel be join_rel(t["rel"], check_name(name))
|
|
101
|
+
must_not_exist(rel)
|
|
102
|
+
write_file(c.ROOT + "/" + rel, when content == nothing then "" otherwise text(content))
|
|
103
|
+
give {"path": rel, "is_dir": false}
|
|
104
|
+
|
|
105
|
+
export task rename(path, name)
|
|
106
|
+
require exec
|
|
107
|
+
require env("LAMPSON_*")
|
|
108
|
+
require env("OS")
|
|
109
|
+
require file("workspace")
|
|
110
|
+
require file("workspace/*")
|
|
111
|
+
let t be target(path)
|
|
112
|
+
must_exist(t)
|
|
113
|
+
when t["rel"] == "."
|
|
114
|
+
raise("the workspace root cannot be renamed")
|
|
115
|
+
let rel be join_rel(parent_of(t["rel"]), check_name(name))
|
|
116
|
+
when rel == t["rel"]
|
|
117
|
+
give {"path": rel, "is_dir": t["info"]["is_dir"]}
|
|
118
|
+
-- solo cambian mayúsculas: en un FS case-insensitive el destino "existe" → pasar por un temporal
|
|
119
|
+
when lower(rel) == lower(t["rel"])
|
|
120
|
+
let tmp be rel + ".lampson-rename-tmp"
|
|
121
|
+
sh("mv " + sh_quote(t["rel"]) + " " + sh_quote(tmp) + " && mv " + sh_quote(tmp) + " " + sh_quote(rel))
|
|
122
|
+
give {"path": rel, "is_dir": t["info"]["is_dir"]}
|
|
123
|
+
must_not_exist(rel)
|
|
124
|
+
sh("mv " + sh_quote(t["rel"]) + " " + sh_quote(rel))
|
|
125
|
+
give {"path": rel, "is_dir": t["info"]["is_dir"]}
|
|
126
|
+
|
|
127
|
+
export task move(path, to_dir)
|
|
128
|
+
require exec
|
|
129
|
+
require env("LAMPSON_*")
|
|
130
|
+
require env("OS")
|
|
131
|
+
require file("workspace")
|
|
132
|
+
require file("workspace/*")
|
|
133
|
+
let t be target(path)
|
|
134
|
+
must_exist(t)
|
|
135
|
+
when t["rel"] == "."
|
|
136
|
+
raise("the workspace root cannot be moved")
|
|
137
|
+
let d be target(to_dir)
|
|
138
|
+
must_be_dir(d["rel"], d["info"])
|
|
139
|
+
when t["info"]["is_dir"] and inside(t["rel"], d["rel"])
|
|
140
|
+
raise("cannot move a folder into itself")
|
|
141
|
+
let rel be join_rel(d["rel"], base_of(t["rel"]))
|
|
142
|
+
when rel == t["rel"]
|
|
143
|
+
give {"path": rel, "is_dir": t["info"]["is_dir"]}
|
|
144
|
+
must_not_exist(rel)
|
|
145
|
+
sh("mv " + sh_quote(t["rel"]) + " " + sh_quote(rel))
|
|
146
|
+
give {"path": rel, "is_dir": t["info"]["is_dir"]}
|
|
147
|
+
|
|
148
|
+
-- "b.ts" en `dir` → "b copy.ts", "b copy 2.ts"…; carpetas y dotfiles: "sub copy", ".env copy"
|
|
149
|
+
task copy_name(dir, base)
|
|
150
|
+
require file.read("workspace")
|
|
151
|
+
require file.read("workspace/*")
|
|
152
|
+
let stem be base
|
|
153
|
+
let ext be ""
|
|
154
|
+
let parts be split(base, ".")
|
|
155
|
+
when length(parts) > 1 and parts[0] != ""
|
|
156
|
+
set ext to "." + parts[length(parts) - 1]
|
|
157
|
+
set stem to slice(base, 0, length(base) - length(ext))
|
|
158
|
+
let n be 1
|
|
159
|
+
let cand be stem + " copy" + ext
|
|
160
|
+
while file_exists(c.ROOT + "/" + join_rel(dir, cand)) and n < 100
|
|
161
|
+
set n to n + 1
|
|
162
|
+
set cand to stem + " copy " + text(n) + ext
|
|
163
|
+
give cand
|
|
164
|
+
|
|
165
|
+
-- to_dir = nothing → duplicar al lado; otra carpeta → copiar ahí (mismo nombre, o "copy" si ya hay uno)
|
|
166
|
+
export task copy(path, to_dir)
|
|
167
|
+
require exec
|
|
168
|
+
require env("LAMPSON_*")
|
|
169
|
+
require env("OS")
|
|
170
|
+
require file("workspace")
|
|
171
|
+
require file("workspace/*")
|
|
172
|
+
let t be target(path)
|
|
173
|
+
must_exist(t)
|
|
174
|
+
when t["rel"] == "."
|
|
175
|
+
raise("the workspace root cannot be copied")
|
|
176
|
+
let dir be when to_dir == nothing then parent_of(t["rel"]) otherwise target(to_dir)["rel"]
|
|
177
|
+
must_be_dir(dir, file_info(c.ws(dir)))
|
|
178
|
+
when t["info"]["is_dir"] and inside(t["rel"], dir)
|
|
179
|
+
raise("cannot copy a folder into itself")
|
|
180
|
+
let base be base_of(t["rel"])
|
|
181
|
+
let rel be join_rel(dir, base)
|
|
182
|
+
when rel == t["rel"] or file_exists(c.ROOT + "/" + rel)
|
|
183
|
+
set rel to join_rel(dir, copy_name(dir, base))
|
|
184
|
+
sh("cp -r " + sh_quote(t["rel"]) + " " + sh_quote(rel))
|
|
185
|
+
give {"path": rel, "is_dir": t["info"]["is_dir"]}
|
|
186
|
+
|
|
187
|
+
export task remove(path)
|
|
188
|
+
require exec
|
|
189
|
+
require env("LAMPSON_*")
|
|
190
|
+
require env("OS")
|
|
191
|
+
require file("workspace")
|
|
192
|
+
require file("workspace/*")
|
|
193
|
+
let t be target(path)
|
|
194
|
+
must_exist(t)
|
|
195
|
+
when t["rel"] == "."
|
|
196
|
+
raise("the workspace root cannot be deleted")
|
|
197
|
+
when t["rel"] == ".git" or t["rel"] == ".lampson"
|
|
198
|
+
raise("\"" + t["rel"] + "\" is not deleted from the explorer")
|
|
199
|
+
sh("rm -rf " + sh_quote(t["rel"]))
|
|
200
|
+
give {"path": t["rel"], "is_dir": t["info"]["is_dir"]}
|
|
201
|
+
|
|
202
|
+
-- POST /api/fs: {op, path, name?, to?, content?} → {path, is_dir}
|
|
203
|
+
export task apply(b)
|
|
204
|
+
require exec
|
|
205
|
+
require env("LAMPSON_*")
|
|
206
|
+
require env("OS")
|
|
207
|
+
require file("workspace")
|
|
208
|
+
require file("workspace/*")
|
|
209
|
+
let op be when contains(b, "op") then lower(text(b["op"])) otherwise ""
|
|
210
|
+
let path be when contains(b, "path") then text(b["path"]) otherwise "."
|
|
211
|
+
let name be when contains(b, "name") then b["name"] otherwise nothing
|
|
212
|
+
-- (`to` es palabra reservada: `set x to`)
|
|
213
|
+
let dest be when contains(b, "to") then text(b["to"]) otherwise nothing
|
|
214
|
+
when op == "mkdir"
|
|
215
|
+
give mkdir(path, name)
|
|
216
|
+
when op == "create"
|
|
217
|
+
give create(path, name, when contains(b, "content") then b["content"] otherwise "")
|
|
218
|
+
when op == "rename"
|
|
219
|
+
give rename(path, name)
|
|
220
|
+
when op == "move"
|
|
221
|
+
give move(path, when dest == nothing then "." otherwise dest)
|
|
222
|
+
when op == "copy"
|
|
223
|
+
give copy(path, dest)
|
|
224
|
+
when op == "delete"
|
|
225
|
+
give remove(path)
|
|
226
|
+
raise("unknown op '" + op + "' (mkdir, create, rename, move, copy, delete)")
|