lampson 0.1.3 → 0.2.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 +9 -0
- package/README.md +87 -4
- package/bin/lampson.js +1 -1
- package/chat.syn +133 -0
- package/cli.syn +68 -0
- package/hub.tpl.syn +127 -0
- package/lampson.ps1 +77 -51
- package/lampson.sh +68 -22
- package/lib/agents.syn +1 -1
- package/lib/approvals.syn +179 -0
- package/lib/lsp.syn +10 -1
- package/lib/mcp.syn +10 -1
- package/lib/permission.syn +18 -0
- package/lib/sched_run.syn +161 -0
- package/lib/schedule.syn +660 -0
- package/lib/session.syn +38 -1
- package/lib/settings.syn +66 -3
- package/lib/skills.syn +2 -0
- package/lib/tools.syn +94 -2
- package/lib/workspaces.syn +555 -0
- package/package.json +3 -1
- package/public/css/chat.css +56 -0
- package/public/css/hub.css +12 -0
- package/public/css/layout.css +97 -0
- package/public/css/panel.css +125 -0
- package/public/css/sidebar.css +80 -0
- package/public/css/tokens.css +57 -0
- package/public/hub.html +36 -0
- package/public/index.html +51 -1197
- package/public/js/agents.js +31 -0
- package/public/js/app.js +21 -0
- package/public/js/approvals.js +26 -0
- package/public/js/chat.js +92 -0
- package/public/js/config.js +100 -0
- package/public/js/core.js +91 -0
- package/public/js/events.js +31 -0
- package/public/js/hub.js +40 -0
- package/public/js/lamps.js +112 -0
- package/public/js/lsp.js +81 -0
- package/public/js/mcp.js +74 -0
- package/public/js/memory.js +17 -0
- package/public/js/panel.js +101 -0
- package/public/js/procs.js +45 -0
- package/public/js/schedules.js +118 -0
- package/public/js/sessions.js +69 -0
- package/public/js/sidebar.js +33 -0
- package/public/js/terminal.js +49 -0
- package/public/js/theme.js +6 -0
- package/public/js/todo.js +10 -0
- package/public/js/tree.js +53 -0
- package/public/js/update.js +14 -0
- package/public/js/workspaces.js +83 -0
- package/skills/lampson/SKILL.md +21 -0
- package/skills/synsema/SKILL.md +4 -1
- package/web.syn +165 -55
package/lampson.ps1
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
# lampson.ps1 — launcher Windows.
|
|
1
|
+
# lampson.ps1 — launcher Windows. Cada proyecto es un WORKSPACE con su propio proceso; un hub en :8080 los sirve a todos.
|
|
2
2
|
#
|
|
3
|
-
# cd C:\mi\proyecto ; lampson #
|
|
4
|
-
# cd C:\mi\proyecto ; lampson --web #
|
|
5
|
-
# lampson --workspace C:\otro\proyecto # elegir la
|
|
6
|
-
# lampson --
|
|
7
|
-
# lampson --
|
|
8
|
-
# lampson --
|
|
9
|
-
# lampson --help
|
|
3
|
+
# cd C:\mi\proyecto ; lampson # terminal sobre este workspace (lo registra y lo arranca si hace falta)
|
|
4
|
+
# cd C:\mi\proyecto ; lampson --web # abre http://127.0.0.1:8080/w/<slug>/ en el navegador
|
|
5
|
+
# lampson --workspace C:\otro\proyecto # elegir la carpeta explícitamente
|
|
6
|
+
# lampson --hub start|stop|status|logs # el servicio residente (workspaces + tareas programadas)
|
|
7
|
+
# lampson --install | --uninstall # arrancar el hub al iniciar sesión (Tarea Programada)
|
|
8
|
+
# lampson --agent plan · --yolo|--strict|--ask · --update · --help
|
|
10
9
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
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/lamps/memory de esta instalación. El registro y los procesos
|
|
12
|
+
# los maneja lib/workspaces.syn vía cli.syn; este script solo resuelve la carpeta, llama a cli.syn y abre lo pedido.
|
|
14
13
|
$ErrorActionPreference = "Stop"
|
|
15
14
|
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
16
15
|
$caller = (Get-Location).Path
|
|
17
|
-
$mount = Join-Path $here "workspace"
|
|
18
16
|
|
|
19
17
|
# --- args: acepta --flag y -Flag, sin distinguir mayúsculas ---
|
|
20
|
-
$Workspace = ""; $Web = $false; $Agent = ""; $Perm = ""
|
|
18
|
+
$Workspace = ""; $Web = $false; $Agent = ""; $Perm = ""; $Hub = ""; $Install = ""
|
|
21
19
|
$i = 0
|
|
22
20
|
while ($i -lt $args.Count) {
|
|
23
21
|
$a = [string]$args[$i]
|
|
24
22
|
switch -Regex ($a.ToLower()) {
|
|
25
23
|
'^--?(web|w)$' { $Web = $true }
|
|
24
|
+
'^--?(hub|daemon|d)$' { $i++; $Hub = if ($i -lt $args.Count) { ([string]$args[$i]).ToLower() } else { "status" } }
|
|
25
|
+
'^--?install$' { $Install = "install" }
|
|
26
|
+
'^--?uninstall$' { $Install = "uninstall" }
|
|
26
27
|
'^--?(workspace|ws)$' { $i++; $Workspace = [string]$args[$i] }
|
|
27
28
|
'^--?(agent|a)$' { $i++; $Agent = [string]$args[$i] }
|
|
28
29
|
'^--?(yolo|y|dangerously-skip-permissions)$' { $Perm = "yolo" }
|
|
@@ -30,59 +31,84 @@ while ($i -lt $args.Count) {
|
|
|
30
31
|
'^--?(ask)$' { $Perm = "ask" }
|
|
31
32
|
'^--?(permission|p)$' { $i++; $Perm = ([string]$args[$i]).ToLower() }
|
|
32
33
|
'^--?(update|u)$' { Write-Host "actualizando Lampson en $here"; git -C $here pull --ff-only origin main; Write-Host ("lampson " + (git -C $here rev-parse --short HEAD)); exit $LASTEXITCODE }
|
|
33
|
-
'^--?(help|h|\?)$' { Get-Content $PSCommandPath | Select-Object -Skip 1 -First
|
|
34
|
+
'^--?(help|h|\?)$' { Get-Content $PSCommandPath | Select-Object -Skip 1 -First 8 | ForEach-Object { $_.TrimStart('#',' ') }; exit 0 }
|
|
34
35
|
default { if ($Workspace -eq "" -and -not $a.StartsWith("-")) { $Workspace = $a } else { Write-Error "argumento desconocido: $a (probá lampson --help)"; exit 1 } }
|
|
35
36
|
}
|
|
36
37
|
$i++
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
if ($Workspace -ne "") {
|
|
42
|
-
if (-not (Test-Path -LiteralPath $Workspace -PathType Container)) { Write-Error "el workspace no existe o no es un directorio: $Workspace"; exit 1 }
|
|
43
|
-
$target = (Resolve-Path -LiteralPath $Workspace).Path
|
|
44
|
-
$home_ = [Environment]::GetFolderPath("UserProfile")
|
|
45
|
-
if ($target.TrimEnd('\') -eq $home_.TrimEnd('\') -or $target -match '^[A-Za-z]:\\?$') {
|
|
46
|
-
Write-Error "'$target' es tu carpeta personal / la raíz del disco, no un proyecto. Entrá al repo (cd) y volvé a correr lampson, o usá --workspace C:\ruta\al\proyecto"; exit 1
|
|
47
|
-
}
|
|
48
|
-
if ($target -eq $here) { Write-Error "no montes el propio directorio de lampson como workspace desde fuera; usá --workspace"; exit 1 }
|
|
49
|
-
if (Test-Path -LiteralPath $mount) {
|
|
50
|
-
$item = Get-Item -LiteralPath $mount -Force
|
|
51
|
-
if ($item.LinkType -ne "Junction") { Write-Error "./workspace existe y no es una junction; movelo antes de montar otro proyecto"; exit 1 }
|
|
52
|
-
$item.Delete()
|
|
53
|
-
}
|
|
54
|
-
New-Item -ItemType Junction -Path $mount -Target $target | Out-Null
|
|
55
|
-
} elseif (-not (Test-Path -LiteralPath $mount)) {
|
|
56
|
-
Write-Error "no hay workspace: corré lampson desde el directorio del proyecto, o lampson --workspace C:\ruta"; exit 1
|
|
57
|
-
}
|
|
58
|
-
$target = (Get-Item -LiteralPath $mount -Force).Target
|
|
59
|
-
if ($target -is [array]) { $target = $target[0] }
|
|
60
|
-
|
|
61
|
-
# --- skills externas globales (npx skills add -g → ~/.agents/skills; las de Claude Code → ~/.claude/skills) ---
|
|
62
|
-
# Se montan como junction bajo .lampson/ porque una capability no puede apuntar a una ruta dinámica (HOME).
|
|
40
|
+
# skills externas globales (npx skills add -g → ~/.agents/skills; Claude Code → ~/.claude/skills), montadas bajo .lampson\
|
|
41
|
+
# (una capability no puede apuntar a HOME); los workspaces las ven por su junction .lampson\global
|
|
63
42
|
$skillMounts = @{ "skills-global" = (Join-Path $HOME ".agents\skills"); "skills-claude" = (Join-Path $HOME ".claude\skills") }
|
|
64
43
|
New-Item -ItemType Directory -Force (Join-Path $here ".lampson") | Out-Null
|
|
65
44
|
foreach ($k in $skillMounts.Keys) {
|
|
66
45
|
$link = Join-Path $here ".lampson\$k"; $src = $skillMounts[$k]
|
|
67
|
-
if (Test-Path -LiteralPath $link)
|
|
68
|
-
|
|
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
|
+
}
|
|
48
|
+
|
|
49
|
+
function Invoke-Cli([string]$cmd, [string]$ws) {
|
|
50
|
+
# cli.syn imprime la respuesta como la ÚLTIMA línea (JSON)
|
|
51
|
+
$env:LAMPSON_CMD = $cmd; if ($ws -ne "") { $env:LAMPSON_WORKSPACE = $ws }
|
|
52
|
+
$out = & synsema run cli.syn 2>&1
|
|
53
|
+
if ($LASTEXITCODE -ne 0) { $out | ForEach-Object { Write-Host $_ }; throw "cli.syn falló ($cmd)" }
|
|
54
|
+
$lines = @($out | ForEach-Object { [string]$_ })
|
|
55
|
+
$lines | Select-Object -SkipLast 1 | ForEach-Object { Write-Host $_ }
|
|
56
|
+
return ($lines[-1] | ConvertFrom-Json)
|
|
69
57
|
}
|
|
70
58
|
|
|
71
|
-
# --- arrancar desde el directorio de lampson (ahí viven .env, lib/, skills/, .lampson/) ---
|
|
72
|
-
# Push/Pop: si PowerShell ejecuta este .ps1 en la shell del usuario (pasa cuando lampson.ps1 y lampson.cmd
|
|
73
|
-
# comparten nombre en el PATH), el cwd del usuario debe quedar como estaba al salir.
|
|
74
59
|
Push-Location $here
|
|
75
60
|
try {
|
|
76
|
-
$env:
|
|
77
|
-
|
|
78
|
-
if ($
|
|
61
|
+
$env:LAMPSON_HOME = $here
|
|
62
|
+
# --- hub / servicio ---
|
|
63
|
+
if ($Hub -ne "") {
|
|
64
|
+
switch ($Hub) {
|
|
65
|
+
"start" { $r = Invoke-Cli "hub-start" ""; Write-Host "hub: http://127.0.0.1:8080" }
|
|
66
|
+
"stop" { $r = Invoke-Cli "hub-stop" ""; Write-Host "hub y workspaces parados" }
|
|
67
|
+
"status" { $r = Invoke-Cli "hub-status" ""; Write-Host ("hub " + $(if ($r.alive) { "● vivo · http://127.0.0.1:8080" } else { "○ parado" })); foreach ($w in $r.workspaces) { Write-Host (" " + $(if ($w.alive) { "●" } else { "○" }) + " " + $w.name + " " + $w.path + " " + $w.policy) } }
|
|
68
|
+
"logs" { if (Test-Path ".lampson\hub.log") { Get-Content ".lampson\hub.log" -Tail 40 } else { Write-Host "sin log todavía" } }
|
|
69
|
+
"restart" { $null = Invoke-Cli "hub-stop" ""; Start-Sleep 1; $null = Invoke-Cli "hub-start" ""; Write-Host "hub reiniciado" }
|
|
70
|
+
default { Write-Error "uso: lampson --hub start|stop|status|logs|restart"; exit 1 }
|
|
71
|
+
}
|
|
72
|
+
exit 0
|
|
73
|
+
}
|
|
74
|
+
# --- arranque al iniciar sesión: Tarea Programada que corre el hub (synsema daemon, --watch) ---
|
|
75
|
+
if ($Install -eq "install") {
|
|
76
|
+
$cmd = "cmd /c cd /d `"$here`" && set LAMPSON_HOME=$here && synsema serve hub.syn > .lampson\hub.log 2>&1"
|
|
77
|
+
schtasks /Create /F /TN "Lampson hub" /SC ONLOGON /TR $cmd | Out-Null
|
|
78
|
+
Write-Host "instalado: el hub de Lampson arranca al iniciar sesión (Tarea Programada «Lampson hub»). Arrancándolo ahora…"
|
|
79
|
+
$null = Invoke-Cli "hub-start" ""
|
|
80
|
+
exit 0
|
|
81
|
+
}
|
|
82
|
+
if ($Install -eq "uninstall") { schtasks /Delete /F /TN "Lampson hub" | Out-Null; Write-Host "quitado el arranque al iniciar sesión (el hub sigue corriendo hasta lampson --hub stop)"; exit 0 }
|
|
83
|
+
|
|
84
|
+
# --- resolver el proyecto: explícito > directorio actual (si no es el propio lampson) ---
|
|
85
|
+
if ($Workspace -eq "" -and $caller -ne $here) { $Workspace = $caller }
|
|
86
|
+
if ($Workspace -eq "") { Write-Error "corré lampson desde la carpeta del proyecto, o lampson --workspace C:\ruta (lampson --hub status lista los workspaces)"; exit 1 }
|
|
87
|
+
if (-not (Test-Path -LiteralPath $Workspace -PathType Container)) { Write-Error "el workspace no existe o no es un directorio: $Workspace"; exit 1 }
|
|
88
|
+
$target = (Resolve-Path -LiteralPath $Workspace).Path
|
|
89
|
+
$home_ = [Environment]::GetFolderPath("UserProfile")
|
|
90
|
+
if ($target.TrimEnd('\') -eq $home_.TrimEnd('\') -or $target -match '^[A-Za-z]:\\?$') {
|
|
91
|
+
Write-Error "'$target' es tu carpeta personal / la raíz del disco, no un proyecto. Entrá al repo (cd) y volvé a correr lampson, o usá --workspace C:\ruta\al\proyecto"; exit 1
|
|
92
|
+
}
|
|
93
|
+
if ($target -eq $here) { Write-Error "no montes el propio directorio de lampson como workspace"; exit 1 }
|
|
94
|
+
|
|
95
|
+
# registrar/preparar el workspace y asegurar hub + proceso
|
|
96
|
+
$r = Invoke-Cli "ensure" $target
|
|
97
|
+
$wsDir = Join-Path $here ($r.dir -replace '/', '\')
|
|
79
98
|
if ($Web) {
|
|
80
|
-
Write-Host "Lampson web · workspace
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
} else {
|
|
84
|
-
synsema run chat.syn
|
|
99
|
+
Write-Host ("Lampson web · " + $r.url + " (workspace " + $r.slug + $(if ($r.alive) { " ● vivo" } else { " ○ arrancando" }) + ")")
|
|
100
|
+
Start-Process $r.url
|
|
101
|
+
exit 0
|
|
85
102
|
}
|
|
103
|
+
# terminal: chat.syn dentro del directorio del workspace (misma junction `workspace` que el proceso web)
|
|
104
|
+
Push-Location $wsDir
|
|
105
|
+
try {
|
|
106
|
+
$env:LAMPSON_WORKSPACE = $target; $env:LAMPSON_WS = $r.slug; $env:LAMPSON_PORT = [string]$r.port
|
|
107
|
+
if ($Agent -ne "") { $env:LAMPSON_AGENT = $Agent }
|
|
108
|
+
if ($Perm -ne "") { $env:LAMPSON_PERMISSION = $Perm }
|
|
109
|
+
$envFile = Join-Path $here ".env"
|
|
110
|
+
if (Test-Path -LiteralPath $envFile) { synsema run --env-file $envFile chat.syn } else { synsema run chat.syn }
|
|
111
|
+
} finally { Pop-Location }
|
|
86
112
|
} finally {
|
|
87
113
|
Pop-Location
|
|
88
114
|
}
|
package/lampson.sh
CHANGED
|
@@ -1,42 +1,88 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# lampson.sh — launcher unix.
|
|
3
|
-
# cd /mi/proyecto && lampson #
|
|
4
|
-
# cd /mi/proyecto && lampson --web #
|
|
5
|
-
# lampson --
|
|
2
|
+
# lampson.sh — launcher unix. Cada proyecto es un WORKSPACE con su propio proceso; un hub en :8080 los sirve a todos.
|
|
3
|
+
# cd /mi/proyecto && lampson # terminal sobre este workspace
|
|
4
|
+
# cd /mi/proyecto && lampson --web # abre http://127.0.0.1:8080/w/<slug>/
|
|
5
|
+
# lampson --hub start|stop|status|logs|restart · --install (systemd --user) · --uninstall
|
|
6
|
+
# lampson --workspace /ruta [--agent plan] [--yolo|--strict|--ask] · --update
|
|
6
7
|
set -euo pipefail
|
|
7
8
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
9
|
caller="$(pwd)"
|
|
9
|
-
ws=""; web=0
|
|
10
|
+
ws=""; web=0; hub=""; install=""
|
|
10
11
|
while [ $# -gt 0 ]; do
|
|
11
12
|
case "$1" in
|
|
12
13
|
--web) web=1 ;;
|
|
14
|
+
--hub|--daemon) shift; hub="${1:-status}" ;;
|
|
15
|
+
--install) install=install ;;
|
|
16
|
+
--uninstall) install=uninstall ;;
|
|
13
17
|
--workspace) shift; ws="$1" ;;
|
|
14
18
|
--agent) shift; export LAMPSON_AGENT="$1" ;;
|
|
15
19
|
--yolo|--dangerously-skip-permissions) export LAMPSON_PERMISSION=yolo ;;
|
|
16
20
|
--strict) export LAMPSON_PERMISSION=strict ;;
|
|
17
21
|
--ask) export LAMPSON_PERMISSION=ask ;;
|
|
18
22
|
--update) echo "actualizando Lampson en $here"; git -C "$here" pull --ff-only origin main; echo "lampson $(git -C "$here" rev-parse --short HEAD)"; exit $? ;;
|
|
19
|
-
|
|
23
|
+
-*) echo "uso: lampson [--web] [--hub start|stop|status|logs|restart] [--install|--uninstall] [--workspace RUTA] [--agent PERFIL] [--yolo|--strict|--ask] [--update]" >&2; exit 1 ;;
|
|
24
|
+
*) ws="$1" ;;
|
|
20
25
|
esac
|
|
21
26
|
shift
|
|
22
27
|
done
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
[ -d "$ws" ] || { echo "el workspace no existe o no es un directorio: $ws" >&2; exit 1; }
|
|
26
|
-
ws="$(cd "$ws" && pwd)"
|
|
27
|
-
if [ -e "$here/workspace" ] && [ ! -L "$here/workspace" ]; then echo "./workspace existe y no es un symlink" >&2; exit 1; fi
|
|
28
|
-
rm -f "$here/workspace"; ln -s "$ws" "$here/workspace"
|
|
29
|
-
elif [ ! -e "$here/workspace" ]; then
|
|
30
|
-
echo "no hay workspace: corré lampson desde el directorio del proyecto, o --workspace /ruta" >&2; exit 1
|
|
31
|
-
fi
|
|
32
|
-
export LAMPSON_WORKSPACE="$(readlink -f "$here/workspace")"
|
|
33
|
-
# skills externas globales (npx skills add -g → ~/.agents/skills; Claude Code → ~/.claude/skills), montadas bajo .lampson/
|
|
28
|
+
export LAMPSON_HOME="$here"
|
|
29
|
+
# skills externas globales, montadas bajo .lampson/ (los workspaces las ven por .lampson/global)
|
|
34
30
|
mkdir -p "$here/.lampson"
|
|
35
31
|
for pair in "skills-global:$HOME/.agents/skills" "skills-claude:$HOME/.claude/skills"; do
|
|
36
32
|
link="$here/.lampson/${pair%%:*}"; src="${pair#*:}"
|
|
37
|
-
[ -
|
|
38
|
-
[ -d "$src" ] && ln -s "$src" "$link"
|
|
33
|
+
[ -e "$link" ] || { [ -d "$src" ] && ln -s "$src" "$link"; } || true
|
|
39
34
|
done
|
|
40
|
-
|
|
41
|
-
cd "$here"
|
|
42
|
-
|
|
35
|
+
cli() { # última línea = JSON
|
|
36
|
+
local out; out="$(cd "$here" && LAMPSON_CMD="$1" LAMPSON_WORKSPACE="${2:-}" synsema run cli.syn)" || { echo "$out" >&2; exit 1; }
|
|
37
|
+
echo "$out" | sed '$d' >&2 || true
|
|
38
|
+
echo "$out" | tail -n 1
|
|
39
|
+
}
|
|
40
|
+
json() { node -e "const j=JSON.parse(process.argv[1]);const v=j[process.argv[2]];console.log(typeof v==='object'?JSON.stringify(v):v)" "$1" "$2" 2>/dev/null || python3 -c "import json,sys;print(json.loads(sys.argv[1])[sys.argv[2]])" "$1" "$2"; }
|
|
41
|
+
if [ -n "$hub" ]; then
|
|
42
|
+
case "$hub" in
|
|
43
|
+
start) cli hub-start >/dev/null; echo "hub: http://127.0.0.1:8080" ;;
|
|
44
|
+
stop) cli hub-stop >/dev/null; echo "hub y workspaces parados" ;;
|
|
45
|
+
status) r="$(cli hub-status)"; echo "hub $( [ "$(json "$r" alive)" = "true" ] && echo '● vivo · http://127.0.0.1:8080' || echo '○ parado')"; (cd "$here" && LAMPSON_CMD=list synsema run cli.syn | sed '$d') ;;
|
|
46
|
+
logs) [ -f "$here/.lampson/hub.log" ] && tail -n 40 "$here/.lampson/hub.log" || echo "sin log todavía" ;;
|
|
47
|
+
restart) cli hub-stop >/dev/null; sleep 1; cli hub-start >/dev/null; echo "hub reiniciado" ;;
|
|
48
|
+
*) echo "uso: lampson --hub start|stop|status|logs|restart" >&2; exit 1 ;;
|
|
49
|
+
esac
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
if [ "$install" = install ]; then
|
|
53
|
+
unit="$HOME/.config/systemd/user/lampson-hub.service"; mkdir -p "$(dirname "$unit")"
|
|
54
|
+
cat > "$unit" <<EOF
|
|
55
|
+
[Unit]
|
|
56
|
+
Description=Lampson hub (workspaces, tareas programadas, proxy)
|
|
57
|
+
After=network.target
|
|
58
|
+
[Service]
|
|
59
|
+
WorkingDirectory=$here
|
|
60
|
+
Environment=LAMPSON_HOME=$here
|
|
61
|
+
ExecStart=$(command -v synsema) serve hub.syn
|
|
62
|
+
Restart=always
|
|
63
|
+
RestartSec=3
|
|
64
|
+
TimeoutStopSec=20
|
|
65
|
+
[Install]
|
|
66
|
+
WantedBy=default.target
|
|
67
|
+
EOF
|
|
68
|
+
(cd "$here" && LAMPSON_CMD=hub-stop synsema run cli.syn >/dev/null 2>&1 || true)
|
|
69
|
+
systemctl --user daemon-reload && systemctl --user enable --now lampson-hub.service && echo "instalado: systemd --user lampson-hub.service (arranca con tu sesión; 'loginctl enable-linger $USER' para que viva sin sesión abierta)"
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
if [ "$install" = uninstall ]; then systemctl --user disable --now lampson-hub.service || true; rm -f "$HOME/.config/systemd/user/lampson-hub.service"; systemctl --user daemon-reload; echo "quitado"; exit 0; fi
|
|
73
|
+
# --- proyecto ---
|
|
74
|
+
[ -z "$ws" ] && [ "$caller" != "$here" ] && ws="$caller"
|
|
75
|
+
[ -n "$ws" ] || { echo "corré lampson desde la carpeta del proyecto, o --workspace /ruta (lampson --hub status lista los workspaces)" >&2; exit 1; }
|
|
76
|
+
[ -d "$ws" ] || { echo "el workspace no existe o no es un directorio: $ws" >&2; exit 1; }
|
|
77
|
+
ws="$(cd "$ws" && pwd -P)"
|
|
78
|
+
[ "$ws" = "$HOME" ] || [ "$ws" = "/" ] && { echo "'$ws' no es un proyecto" >&2; exit 1; }
|
|
79
|
+
r="$(cli ensure "$ws")"
|
|
80
|
+
slug="$(json "$r" slug)"; port="$(json "$r" port)"; url="$(json "$r" url)"; dir="$(json "$r" dir)"
|
|
81
|
+
if [ "$web" = 1 ]; then
|
|
82
|
+
echo "Lampson web · $url (workspace $slug)"
|
|
83
|
+
(xdg-open "$url" >/dev/null 2>&1 || open "$url" >/dev/null 2>&1 || true)
|
|
84
|
+
exit 0
|
|
85
|
+
fi
|
|
86
|
+
cd "$here/$dir"
|
|
87
|
+
export LAMPSON_WORKSPACE="$ws" LAMPSON_WS="$slug" LAMPSON_PORT="$port"
|
|
88
|
+
if [ -f "$here/.env" ]; then exec synsema run --env-file "$here/.env" chat.syn; else exec synsema run chat.syn; fi
|
package/lib/agents.syn
CHANGED
|
@@ -40,7 +40,7 @@ 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", "lamp", "delegate"],
|
|
43
|
+
"tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "bash", "process", "memory", "todo", "skill", "mcp", "lamp", "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
|
},
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
-- lib/approvals.syn — aprobaciones humanas fuera de la terminal: cola compartida + webhook + links de decisión
|
|
2
|
+
--
|
|
3
|
+
-- Un solo mecanismo para las puertas por las que un humano aprueba algo que el agente quiere hacer:
|
|
4
|
+
-- 1. la UI web (POST /api/approve desde el chat, o desde el panel «Aprobaciones» para las tareas programadas);
|
|
5
|
+
-- 2. la terminal: /approve <id> yes|no (una tarea programada corriendo en background mientras chateás);
|
|
6
|
+
-- 3. un LINK firmado que llega por el canal que sea (Telegram, mail, Slack…): GET /approve/<id>/<token>?d=yes|no
|
|
7
|
+
-- — de un solo uso; el token es un secreto por aprobación (32 bytes CSPRNG), igual que el `approve` nativo
|
|
8
|
+
-- de Synsema bajo serve (doc 62-human), solo que acá también sirve para las tareas del cron y para la UI;
|
|
9
|
+
-- 4. la consola del proceso (el link se imprime ahí también).
|
|
10
|
+
-- Además, si LAMPSON_WEBHOOK_URL está configurado (o config.json {"webhook_url"}), cada aprobación pendiente dispara
|
|
11
|
+
-- un POST JSON con {id, message, why, expires_at, respond_link_yes, respond_link_no} firmado con HMAC-SHA256
|
|
12
|
+
-- (X-Lampson-Signature: sha256=<hex>, secreto LAMPSON_WEBHOOK_SECRET) — el mismo patrón que Stripe/GitHub, así
|
|
13
|
+
-- cualquier receptor (n8n, un bot, otro .syn) reenvía los links. Los links necesitan LAMPSON_PUBLIC_URL (la URL
|
|
14
|
+
-- por la que se llega a este lampson desde afuera: un túnel, el VPS, un edge Synsema con TLS).
|
|
15
|
+
-- Fire-and-forget (10 s, un intento): un canal caído nunca bloquea; la UI y la consola quedan como respaldo.
|
|
16
|
+
--
|
|
17
|
+
-- Estado: BLACKBOARD (share/observe), que existe bajo `serve` y bajo `run` y lo ven handlers, agentes y ticks
|
|
18
|
+
-- (`state_*` solo existe bajo serve — verificado 2026-08-29). Claves: "approval:meta:<id>" (visible, sin token),
|
|
19
|
+
-- "approval:token:<id>", "approval:answer:<id>" (yes|no) y el índice "approval:index" (lista de ids).
|
|
20
|
+
--
|
|
21
|
+
-- Garantía: NUNCA se auto-aprueba. Sin respuesta al vencer el plazo → denegado (fail-closed), como Synsema.
|
|
22
|
+
|
|
23
|
+
use "./settings.syn" as settings
|
|
24
|
+
|
|
25
|
+
task read_cfg(key, envname)
|
|
26
|
+
let v be env(envname, "")
|
|
27
|
+
when v != ""
|
|
28
|
+
give v
|
|
29
|
+
let doc be settings.load()
|
|
30
|
+
when contains(doc, key)
|
|
31
|
+
give text(doc[key])
|
|
32
|
+
give ""
|
|
33
|
+
|
|
34
|
+
export task webhook_url()
|
|
35
|
+
require env("LAMPSON_*")
|
|
36
|
+
require file.read(".lampson")
|
|
37
|
+
require file.read(".lampson/*")
|
|
38
|
+
give read_cfg("webhook_url", "LAMPSON_WEBHOOK_URL")
|
|
39
|
+
|
|
40
|
+
export task public_url()
|
|
41
|
+
require env("LAMPSON_*")
|
|
42
|
+
require file.read(".lampson")
|
|
43
|
+
require file.read(".lampson/*")
|
|
44
|
+
let u be read_cfg("public_url", "LAMPSON_PUBLIC_URL")
|
|
45
|
+
while length(u) > 0 and slice(u, length(u) - 1, length(u)) == "/"
|
|
46
|
+
set u to slice(u, 0, length(u) - 1)
|
|
47
|
+
give u
|
|
48
|
+
|
|
49
|
+
task secret_text()
|
|
50
|
+
give read_cfg("webhook_secret", "LAMPSON_WEBHOOK_SECRET")
|
|
51
|
+
|
|
52
|
+
task index()
|
|
53
|
+
observe "approval:index" as ix
|
|
54
|
+
give when ix == nothing then [] otherwise ix
|
|
55
|
+
|
|
56
|
+
task meta_of(id)
|
|
57
|
+
observe "approval:meta:" + id as m
|
|
58
|
+
give m
|
|
59
|
+
|
|
60
|
+
task set_meta(id, m)
|
|
61
|
+
share m as "approval:meta:" + id
|
|
62
|
+
|
|
63
|
+
-- abre una aprobación: id único, token de un solo uso, metadatos visibles (sin token) para la UI.
|
|
64
|
+
-- source = "chat" | "schedule:<id>"; message = lo que se quiere hacer; why = por qué pide permiso.
|
|
65
|
+
export task open(source, message, why, timeout_s)
|
|
66
|
+
require time
|
|
67
|
+
require random
|
|
68
|
+
require net
|
|
69
|
+
require env("LAMPSON_*")
|
|
70
|
+
require file.read(".lampson")
|
|
71
|
+
require file.read(".lampson/*")
|
|
72
|
+
let id be "a" + slice(text(floor(now() * 1000)), 4, 13) + "-" + slice(token(16), 0, 6)
|
|
73
|
+
let tok be token(32)
|
|
74
|
+
let meta be {"id": id, "source": source, "message": message, "why": why, "created": now(), "expires_at": floor(now() + timeout_s), "status": "pending"}
|
|
75
|
+
set_meta(id, meta)
|
|
76
|
+
share tok as "approval:token:" + id
|
|
77
|
+
share nothing as "approval:answer:" + id
|
|
78
|
+
share append(index(), id) as "approval:index"
|
|
79
|
+
bus_publish("approval.request", meta)
|
|
80
|
+
let links be links_for(id, tok)
|
|
81
|
+
print("[lampson] aprobación pendiente " + id + " — " + message + " (vence en " + text(floor(timeout_s)) + " s)" + (when links["yes"] != "" then "\n sí: " + links["yes"] + "\n no: " + links["no"] otherwise "\n respondé desde la UI web (Aprobaciones), con /approve " + id + " yes|no en la terminal, o configurá la URL pública para recibir links"))
|
|
82
|
+
notify(meta, links)
|
|
83
|
+
give id
|
|
84
|
+
|
|
85
|
+
-- bajo el hub cada workspace vive en /w/<slug> (LAMPSON_WS); el link público lleva ese prefijo
|
|
86
|
+
task links_for(id, tok)
|
|
87
|
+
let slug be env("LAMPSON_WS", "")
|
|
88
|
+
let prefix be when slug == "" then "" otherwise "/w/" + slug
|
|
89
|
+
let base be public_url()
|
|
90
|
+
when base == ""
|
|
91
|
+
give {"yes": "", "no": "", "path": prefix + "/approve/" + id + "/" + tok}
|
|
92
|
+
give {"yes": base + prefix + "/approve/" + id + "/" + tok + "?d=yes", "no": base + prefix + "/approve/" + id + "/" + tok + "?d=no", "path": prefix + "/approve/" + id + "/" + tok}
|
|
93
|
+
|
|
94
|
+
-- webhook firmado (fire-and-forget)
|
|
95
|
+
task notify(meta, links)
|
|
96
|
+
let url be webhook_url()
|
|
97
|
+
when url == ""
|
|
98
|
+
give false
|
|
99
|
+
let payload be {"id": meta["id"], "type": "approve", "source": meta["source"], "message": meta["message"], "why": meta["why"], "expires_at": meta["expires_at"], "respond_path": links["path"], "respond_link_yes": links["yes"], "respond_link_no": links["no"]}
|
|
100
|
+
let body be json_encode(payload)
|
|
101
|
+
let headers be {"Content-Type": "application/json", "User-Agent": "lampson"}
|
|
102
|
+
let sec be secret_text()
|
|
103
|
+
when sec != ""
|
|
104
|
+
set headers["X-Lampson-Signature"] to "sha256=" + hmac_sha256(body, sec)
|
|
105
|
+
try
|
|
106
|
+
let r be http_post(url, body, headers)
|
|
107
|
+
when not r["ok"]
|
|
108
|
+
print("[lampson] webhook " + url + " respondió " + text(r["status"]))
|
|
109
|
+
give r["ok"]
|
|
110
|
+
recover err
|
|
111
|
+
print("[lampson] webhook falló: " + text(err))
|
|
112
|
+
give false
|
|
113
|
+
|
|
114
|
+
-- espera (polling) hasta que alguien responda o venza el plazo. Devuelve true/false; sin respuesta = false.
|
|
115
|
+
export task wait(id, timeout_s)
|
|
116
|
+
require time
|
|
117
|
+
let deadline be now() + timeout_s
|
|
118
|
+
let answer be nothing
|
|
119
|
+
while answer == nothing and now() < deadline
|
|
120
|
+
observe "approval:answer:" + id as a
|
|
121
|
+
set answer to a
|
|
122
|
+
when answer == nothing
|
|
123
|
+
sleep(0.5)
|
|
124
|
+
let meta be meta_of(id)
|
|
125
|
+
let ok be answer == "yes"
|
|
126
|
+
when meta != nothing
|
|
127
|
+
set meta["status"] to when answer == nothing then "expired" otherwise (when ok then "approved" otherwise "denied")
|
|
128
|
+
set meta["answered"] to now()
|
|
129
|
+
set_meta(id, meta)
|
|
130
|
+
bus_publish("approval.result", meta)
|
|
131
|
+
share nothing as "approval:token:" + id
|
|
132
|
+
give ok
|
|
133
|
+
|
|
134
|
+
-- responder desde la UI (loopback, ya autenticado) o desde la terminal: sin token
|
|
135
|
+
export task answer(id, decision)
|
|
136
|
+
let meta be meta_of(id)
|
|
137
|
+
when meta == nothing
|
|
138
|
+
give false
|
|
139
|
+
when meta["status"] != "pending"
|
|
140
|
+
give false
|
|
141
|
+
let v be when decision then "yes" otherwise "no"
|
|
142
|
+
share v as "approval:answer:" + id
|
|
143
|
+
give true
|
|
144
|
+
|
|
145
|
+
-- responder por link: el token debe coincidir (comparación en tiempo constante) y ser de un solo uso
|
|
146
|
+
export task answer_with_token(id, tok, decision)
|
|
147
|
+
observe "approval:token:" + id as want
|
|
148
|
+
when want == nothing
|
|
149
|
+
give {"ok": false, "why": "expired"}
|
|
150
|
+
when not verify_hmac(id, hmac_sha256(id, tok), want)
|
|
151
|
+
give {"ok": false, "why": "bad token"}
|
|
152
|
+
share nothing as "approval:token:" + id
|
|
153
|
+
let v be when decision then "yes" otherwise "no"
|
|
154
|
+
share v as "approval:answer:" + id
|
|
155
|
+
give {"ok": true, "why": ""}
|
|
156
|
+
|
|
157
|
+
-- pendientes y recientes para la UI (nunca incluye tokens); las resueltas hace más de 1 h se olvidan
|
|
158
|
+
export task list()
|
|
159
|
+
require time
|
|
160
|
+
let out be []
|
|
161
|
+
let keep be []
|
|
162
|
+
each id in index()
|
|
163
|
+
let m be meta_of(id)
|
|
164
|
+
when m != nothing
|
|
165
|
+
when m["status"] != "pending" and contains(m, "answered") and now() - m["answered"] > 3600
|
|
166
|
+
share nothing as "approval:meta:" + id
|
|
167
|
+
otherwise
|
|
168
|
+
set out to append(out, m)
|
|
169
|
+
set keep to append(keep, id)
|
|
170
|
+
when length(keep) != length(index())
|
|
171
|
+
share keep as "approval:index"
|
|
172
|
+
give sort_by(out, (m) => 0 - m["created"])
|
|
173
|
+
|
|
174
|
+
export task pending()
|
|
175
|
+
give where(list(), (m) => m["status"] == "pending")
|
|
176
|
+
|
|
177
|
+
-- página mínima que ve quien abre el link
|
|
178
|
+
export task html_page(title, body)
|
|
179
|
+
give "<!doctype html><meta charset=utf-8><meta name=viewport content='width=device-width,initial-scale=1'><title>" + title + "</title><body style='font:16px/1.5 system-ui;margin:0;display:grid;place-items:center;min-height:100vh;background:#f6f4ef;color:#1d1c1a'><div style='max-width:520px;padding:32px;border:1px solid #d9d4c7;border-radius:6px;background:#fffdf8'><p style='margin:0 0 6px;font:11px/1 ui-monospace,monospace;letter-spacing:.12em;text-transform:uppercase;color:#a33'>lampson</p><h1 style='font-size:22px;margin:0 0 12px'>" + title + "</h1><p style='margin:0'>" + body + "</p></div></body>"
|
package/lib/lsp.syn
CHANGED
|
@@ -22,7 +22,16 @@
|
|
|
22
22
|
|
|
23
23
|
use "./tools/common.syn" as c
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
-- global = compartido por todos los workspaces: .lampson/global/ (junction al .lampson de la instalación) cuando
|
|
26
|
+
-- el proceso corre bajo el hub; .lampson/ en una instalación directa o en los tests
|
|
27
|
+
task global_config()
|
|
28
|
+
try
|
|
29
|
+
when file_exists(".lampson/global")
|
|
30
|
+
give ".lampson/global/lsp.json"
|
|
31
|
+
recover err
|
|
32
|
+
give ".lampson/lsp.json"
|
|
33
|
+
give ".lampson/lsp.json"
|
|
34
|
+
export let GLOBAL_CONFIG be global_config()
|
|
26
35
|
export let PROJECT_CONFIG be "workspace/.lampson/lsp.json"
|
|
27
36
|
let CALL_TIMEOUT be 60
|
|
28
37
|
let START_WAIT be 25
|
package/lib/mcp.syn
CHANGED
|
@@ -23,7 +23,16 @@
|
|
|
23
23
|
|
|
24
24
|
use "./tools/common.syn" as c
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
-- global = compartido por todos los workspaces: .lampson/global/ (junction al .lampson de la instalación) cuando
|
|
27
|
+
-- el proceso corre bajo el hub; .lampson/ en una instalación directa o en los tests
|
|
28
|
+
task global_config()
|
|
29
|
+
try
|
|
30
|
+
when file_exists(".lampson/global")
|
|
31
|
+
give ".lampson/global/mcp.json"
|
|
32
|
+
recover err
|
|
33
|
+
give ".lampson/mcp.json"
|
|
34
|
+
give ".lampson/mcp.json"
|
|
35
|
+
export let GLOBAL_CONFIG be global_config()
|
|
27
36
|
export let PROJECT_CONFIG be "workspace/.lampson/mcp.json"
|
|
28
37
|
let CALL_TIMEOUT be 120
|
|
29
38
|
|
package/lib/permission.syn
CHANGED
|
@@ -114,6 +114,17 @@ export task evaluate(name, args, mode)
|
|
|
114
114
|
give {"decision": "deny", "reason": "strict mode (" + act + " MCP server)"}
|
|
115
115
|
give {"decision": "ask", "reason": act + "s MCP server '" + (when contains(args, "name") then text(args["name"]) otherwise "?") + "'" + (when act == "add" then " → runs: " + (when contains(args, "command") then text(args["command"]) otherwise "?") otherwise "")}
|
|
116
116
|
give {"decision": "allow", "reason": "read-only tool"}
|
|
117
|
+
when name == "schedule"
|
|
118
|
+
-- programar = autorizar corridas futuras SIN nadie mirando (un comando, una lámpara, o el agente entero
|
|
119
|
+
-- con un sobre de permisos): humano siempre, incluso en yolo. list/log son lectura.
|
|
120
|
+
let sact be when contains(args, "action") then text(args["action"]) otherwise "list"
|
|
121
|
+
when sact == "list" or sact == "log"
|
|
122
|
+
give {"decision": "allow", "reason": "read-only tool"}
|
|
123
|
+
when mode == "strict"
|
|
124
|
+
give {"decision": "deny", "reason": "strict mode (schedule " + sact + ")"}
|
|
125
|
+
when sact == "add"
|
|
126
|
+
give {"decision": "ask", "reason": "schedules an unattended " + (when contains(args, "kind") then text(args["kind"]) otherwise "?") + " task" + (when contains(args, "permission") then " with permission=" + text(args["permission"]) otherwise " with permission=ask")}
|
|
127
|
+
give {"decision": "ask", "reason": "schedule " + sact + " " + (when contains(args, "id") then text(args["id"]) otherwise "?")}
|
|
117
128
|
when name == "skill" and is_install(args)
|
|
118
129
|
-- instala instrucciones/scripts de terceros FUERA del workspace (~/.agents/skills): humano siempre, incluso en yolo
|
|
119
130
|
when mode == "strict"
|
|
@@ -145,6 +156,13 @@ export task describe_call(name, args)
|
|
|
145
156
|
when act == "remove"
|
|
146
157
|
give "mcp remove " + (when contains(args, "name") then text(args["name"]) otherwise "?")
|
|
147
158
|
give "mcp list"
|
|
159
|
+
when name == "schedule"
|
|
160
|
+
let sact be arg(args, "action", "list")
|
|
161
|
+
when sact == "add"
|
|
162
|
+
let what be arg(args, "kind", "?")
|
|
163
|
+
let body be when what == "lamp" then "lamp " + arg(args, "lamp", "?") + "." + arg(args, "tool", "?") + (when contains(args, "args") then " " + one_line(json_encode(args["args"]), 80) otherwise "") otherwise (when what == "bash" then "$ " + one_line(arg(args, "command", "?"), 120) otherwise "agent " + arg(args, "agent", "build") + ": " + one_line(arg(args, "prompt", "?"), 160))
|
|
164
|
+
give "schedule add «" + arg(args, "name", "?") + "» · " + arg(args, "at", "?") + " · " + body + " · permission " + arg(args, "permission", "ask") + (when contains(args, "notify") then " · notify " + text(args["notify"]) otherwise "")
|
|
165
|
+
give "schedule " + sact + (when contains(args, "id") then " " + text(args["id"]) otherwise "")
|
|
148
166
|
when name == "skill" and is_install(args)
|
|
149
167
|
give "skill install " + text(args["source"]) + " --skill " + text(args["name"]) + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"
|
|
150
168
|
when name == "bash"
|