lazyclaw 6.0.1 → 6.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.
Files changed (44) hide show
  1. package/README.ko.md +88 -25
  2. package/README.md +121 -190
  3. package/channels/handoff.mjs +18 -5
  4. package/channels/matrix.mjs +23 -5
  5. package/channels/slack.mjs +83 -50
  6. package/channels/slack_env.mjs +45 -0
  7. package/channels/telegram.mjs +49 -6
  8. package/cli.mjs +10 -21
  9. package/commands/channels.mjs +106 -107
  10. package/commands/chat.mjs +81 -45
  11. package/commands/daemon.mjs +15 -0
  12. package/commands/gateway.mjs +194 -0
  13. package/commands/providers.mjs +17 -38
  14. package/commands/service.mjs +113 -0
  15. package/commands/setup.mjs +55 -54
  16. package/commands/setup_channels.mjs +207 -0
  17. package/config_features.mjs +106 -0
  18. package/daemon/lib/inbound_dedup.mjs +108 -0
  19. package/daemon/lib/learn_queue.mjs +46 -0
  20. package/daemon/routes/_deps.mjs +6 -0
  21. package/daemon/routes/conversation.mjs +68 -4
  22. package/daemon/routes/ops.mjs +9 -29
  23. package/dotenv_min.mjs +28 -0
  24. package/first_run.mjs +9 -0
  25. package/lib/gateway_guard.mjs +100 -0
  26. package/lib/inbound_client.mjs +108 -0
  27. package/lib/service_install.mjs +207 -0
  28. package/package.json +2 -3
  29. package/providers/codex_cli.mjs +17 -1
  30. package/providers/compat_vendors.mjs +161 -0
  31. package/providers/gemini_cli.mjs +19 -3
  32. package/providers/model_catalogue.mjs +142 -9
  33. package/providers/probe.mjs +28 -0
  34. package/providers/registry.mjs +31 -162
  35. package/tui/editor.mjs +18 -2
  36. package/tui/pickers.mjs +3 -3
  37. package/tui/provider_families.mjs +8 -6
  38. package/tui/repl.mjs +25 -4
  39. package/tui/slash_commands.mjs +4 -0
  40. package/tui/slash_dispatcher.mjs +118 -0
  41. package/tui/splash_props.mjs +52 -0
  42. package/web/dashboard.css +6 -12
  43. package/web/dashboard.html +2 -34
  44. package/web/dashboard.js +3 -3
package/README.ko.md CHANGED
@@ -1,44 +1,107 @@
1
1
  # lazyclaw (한국어 안내)
2
2
 
3
- > 영문 정본은 `README.md`. 본 문서는 핵심만 한국어로 안내한다.
3
+ > 영문 정본은 [`README.md`](./README.md). 본 문서는 핵심만 한국어로.
4
4
 
5
- ## 설치
5
+ **터미널 에이전트 — 학습은 네 Claude 구독으로 $0, 응답은 모든 채널로.**
6
+
7
+ 터미널에서 대화하면 백그라운드 학습 루프가 대화를 재사용 가능한 스킬로 증류(claude-cli 구독 = API 청구 없음). Slack·Telegram·Discord·Matrix·Email·Signal·WhatsApp·Voice 연결. 어려운 작업은 planner + workers로 분산. 작고 읽을 수 있는 Node 코어.
8
+
9
+ ## 빠른 시작
10
+
11
+ ```bash
12
+ npm install -g lazyclaw # 또는: npx lazyclaw
13
+ lazyclaw # 새 설치 → 가이드 셋업 후 chat
14
+ ```
15
+
16
+ **첫 실행 = phased 위저드** (Hermes식 — 깔끔한 대화 1개 먼저, 그 다음 레이어링):
17
+ 1. **Provider + model** — 화살표 picker (`claude-cli`는 키 불요)
18
+ 2. **Verify** — 1-토큰 ping으로 응답 확인
19
+ 3. **Context window** — 턴당 유지할 히스토리 (선택)
20
+ 4. **Channel** — Slack/Telegram/Matrix/HTTP 빌트인, 나머지는 플러그인 (선택)
21
+ 5. **Workspace / skills / webhook** (선택)
22
+ 6. **Orchestration** — planner + workers 켜기 (선택)
23
+
24
+ 언제든 `lazyclaw setup` 또는 chat에서 `/config`로 재실행.
25
+
26
+ ## $0 자가 학습
27
+
28
+ `provider`(chat)와 `trainer`(학습 루프)를 분리. Claude Pro/Max 구독이 학습을 돌리는 동안 chat은 아무 backend나. 매 턴 후 fire-and-forget 루프가 trajectory 기록 + 스킬 증류(`trained_by` 태그). `trainer: { provider: "auto" }`면 claude-cli 세션 자동 감지 → **$0**.
29
+
30
+ ```jsonc
31
+ { "provider": "openai", "model": "gpt-4.1",
32
+ "trainer": { "provider": "auto" } } // 학습은 네 Claude 구독으로 $0
33
+ ```
34
+
35
+ ## 어디서나 대화
36
+
37
+ 모든 리스너는 상시 데몬의 **공유 세션 스토어**로 forward → chat·대시보드·전 채널이 메모리 하나를 쓰는 단일 에이전트(컨텍스트가 채널 간 따라옴).
38
+
39
+ ```bash
40
+ lazyclaw service install # 1) 상시 데몬(공유 brain)
41
+
42
+ # 2) 리스너를 데몬에 연결 (Slack Socket Mode: .env에 SLACK_BOT_TOKEN + SLACK_APP_TOKEN)
43
+ lazyclaw slack listen # inbound를 데몬으로 forward, 스레드 응답
44
+ lazyclaw slack listen --provider orchestrator # …오케스트레이션 응답
45
+ lazyclaw slack listen --daemon-url http://127.0.0.1:19600 # 비기본 데몬
46
+ lazyclaw channels # 설정된 채널 보기
47
+ lazyclaw channels enable|disable slack
48
+ ```
49
+
50
+ 리스너는 thin forwarder: 채널 소켓을 소유(Slack Socket Mode는 공개 URL 불요, app-level `xapp-` 토큰)하고 각 메시지를 데몬 `/inbound`로 POST → 데몬이 영속 세션에 바인딩 후 provider 실행. **데몬이 떠 있어야 함**(`lazyclaw service install` 또는 `lazyclaw daemon`); 타깃은 `--daemon-url`/`LAZYCLAW_DAEMON_URL`로 override. 위저드 채널 단계나 chat `/channels`로 설정.
51
+
52
+ ## 상시 가동 (always-on)
53
+
54
+ **한 프로세스, 모든 채널:** `lazyclaw gateway` = 데몬 코어 + 설정된 채널 transport(Slack Socket Mode/Telegram long-poll/Matrix sync)를 단일 프로세스로. 채널이 in-process라 `/handoff`가 타깃 채널에 resume 마커 통지 가능(통지 실패 시 handoff 롤백).
6
55
 
7
56
  ```bash
8
- npm install -g lazyclaw
57
+ lazyclaw gateway # 데몬 코어 + 활성 채널 전부, 한 프로세스
58
+ lazyclaw gateway --channels slack # 채널 명시
59
+ lazyclaw service install gateway # 재부팅에도 살아있게
9
60
  ```
10
61
 
11
- Node.js 18+ 필요. `better-sqlite3` native dep 빌드된다
12
- (darwin/linux/win64 × x64/arm64 prebuilt 제공).
62
+ gateway는 **기본 인증** — 첫 실행 시 bearer 토큰을 만들어 `~/.lazyclaw/gateway.token`(0600, 로그 남김)에 영속. 자기 채널은 자동 사용, 외부 호출자는 파일에서 읽음(`--auth-token`/`--no-auth`로 override).
63
+
64
+ 분리 실행도 가능: 데몬만(`lazyclaw service install`) + 채널별 `* listen` forwarder.
13
65
 
14
- ## 실행
66
+ 인바운드 메시지는 **idempotent** — 채널 native id(Slack `channel:ts`, Telegram `chat:message_id`, Matrix `event_id`)로 dedup, 재전송/리스너 재시작 replay는 기록된 응답 반환(provider 재실행 없음). 세션 바인딩된 채널 턴은 chat REPL과 동일한 post-task **학습 루프**에 공급(trainer `auto` → Claude 구독 $0).
67
+
68
+ > ⚠️ `security.allowUnattendedSensitive=true`이면 채널 리스너/데몬이 **부팅 거부** — 이 플래그는 모든 inbound에 대해 fail-closed tool 승인 게이트를 우회하므로 상시 표면 + 이 플래그 = RCE 경로. 민감 tool 승인은 인터랙티브로 유지.
69
+
70
+ ## 멀티 에이전트 오케스트레이션
71
+
72
+ provider를 `orchestrator`로 → **Plan → Delegate → Synthesise**. planner가 분해, workers가 병렬 실행, planner가 병합.
15
73
 
16
74
  ```bash
17
- lazyclaw # 인터랙티브 splash + REPL
18
- lazyclaw migrate v5 # v4 install 이 있는 경우
75
+ lazyclaw orchestrator set-planner claude-cli:claude-sonnet-4-6
76
+ lazyclaw orchestrator workers add claude-cli:claude-sonnet-4-6
77
+ lazyclaw orchestrator on
19
78
  ```
20
79
 
21
- ## v5.0 핵심 변경
80
+ chat에서 `/orchestrator` (빈 입력 = on/off picker) 또는 `/orchestrator on|off|planner <spec>|worker add <spec>`.
22
81
 
23
- - **Trainer provider 분리** — `provider` (chat) 와 `trainer`
24
- (skill synthesis) 를 독립 설정. Claude Pro/Max 사용자는 학습 비용
25
- $0 (`docs/trainer-recipes.md`).
26
- - **FTS5 recall** — `~/.lazyclaw/index.db` 단일 SQLite + FTS5 corpus.
27
- cross-CLI trajectory recall 제공 (`lazyclaw recall <query>`).
28
- - **Persona 7-layer compose** — workspace 별 SOUL.md, Hermes skin
29
- import (`docs/persona-cookbook.md`).
30
- - **Sandbox 6-backend** — local / docker / ssh / singularity / modal
31
- / daytona.
82
+ ## chat에서 직접 조작 (슬래시)
32
83
 
33
- ## 마이그레이션
84
+ | 슬래시 | 기능 |
85
+ |---|---|
86
+ | `/config` | chat 나가고 setup 위저드 재실행 |
87
+ | `/provider` · `/model` | 검색 picker로 provider/model 선택 |
88
+ | `/channels [<name> on\|off]` | 채널 보기 / 토글 |
89
+ | `/orchestrator [on\|off\|…]` | 멀티에이전트 보기 / 토글 (빈 입력=picker) |
90
+ | `/context [turns N\|tokens N]` | 히스토리 윈도우 조절 |
91
+ | `/skill` · `/personality` · `/memory` · `/loop` · `/goal` | 스킬·페르소나·메모리·루프·목표 |
34
92
 
35
- 자세한 절차는 `docs/migration-v4-to-v5.md` 참고.
93
+ `/help`로 전체 목록. 고스트 자동완성, 한글 IME는 박스 안에서 조합.
36
94
 
37
- ## 자주 쓰는 명령
95
+ ## 대시보드
38
96
 
39
97
  ```bash
40
- lazyclaw recall "<query>" # FTS5 검색
41
- lazyclaw rates --trainer-only # trainer 비용 추적
42
- lazyclaw persona use <name> # persona 활성화
43
- lazyclaw-export --format openai-ft # trajectory export
98
+ lazyclaw dashboard # http://127.0.0.1:19600 로컬 웹 UI (17 탭, 다크 앰버)
44
99
  ```
100
+
101
+ ## 설정 / 보안
102
+
103
+ 설정은 `~/.lazyclaw/config.json`(plain JSON), 시크릿은 `~/.lazyclaw/.env`(0600, 로그 안 남김). 디렉터리 이동 `LAZYCLAW_CONFIG_DIR=/path`.
104
+
105
+ > ⚠️ `config.json`은 shell rc처럼 신뢰 코드 — `$(...)` 값은 로드 시 실행됨. 신뢰 못 할 스니펫 붙여넣기 금지. 민감 tool(shell/write/network)은 기본 fail-closed(승인 hook 필요).
106
+
107
+ Node 18+ (Slack Socket Mode는 22+). macOS / Linux / WSL 1급.
package/README.md CHANGED
@@ -1,268 +1,199 @@
1
1
  # lazyclaw
2
2
 
3
- <img src="docs/assets/sleepy-sloth-source.png" alt="lazyclaw sloth" width="120" align="right">
3
+ **A terminal agent that learns on your Claude subscription — for $0 — and reaches you on every channel.**
4
4
 
5
- **A terminal agent whose learning loop runs free on your Claude Pro subscription.**
6
-
7
- Chat with any provider. Train the skill bank, user model, and reflection pass on `claude-cli` — $0. One SQLite + FTS5 store remembers every session. Hand the same conversation off between TUI, Slack, Discord, Telegram, Matrix, Email, and Voice.
5
+ Chat in the terminal. Let the background learning loop distil your conversations into reusable skills on `claude-cli` (your Pro/Max subscription — no API bill). Wire it to Slack, Telegram, Discord, Matrix, Email, Signal, WhatsApp, or Voice. Fan a hard task out to a planner + workers. One small, auditable Node core — no daemon you can't read.
8
6
 
9
7
  [![npm](https://img.shields.io/npm/v/lazyclaw.svg)](https://www.npmjs.com/package/lazyclaw)
10
8
  [![Node](https://img.shields.io/badge/node-%E2%89%A518-blue.svg)](https://nodejs.org/)
11
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
12
10
 
13
11
  ```bash
14
- npx lazyclaw onboard # 15 seconds to a working CLI
12
+ npx lazyclaw # first run walks you through setup, then drops into chat
15
13
  ```
16
14
 
17
- <img src="docs/screenshots/help.png" alt="lazyclaw subcommand reference" width="820">
15
+ <img width="1871" height="1146" alt="image" src="https://github.com/user-attachments/assets/365d05ac-cd24-4451-96b1-01fe82582a2b" />
18
16
 
19
17
  *한국어: [README.ko.md](./README.ko.md)*
20
18
 
21
19
  ---
22
20
 
23
- ## Why lazyclaw
24
-
25
- Four things no other terminal agent CLI does together:
21
+ ## What it is
26
22
 
27
- 1. **Split chat and trainer providers.** Pay-per-token for chat, $0 subscription for the learning loop. Or vice versa.
28
- 2. **Recall across every CLI.** One FTS5 index over sessions, skills, trajectories, and memory.
29
- 3. **Cross-channel handoff.** `/handoff slack <channel-id>` moves the live conversation; context follows.
30
- 4. **Six sandbox backends, one API.** `local` / `docker` / `ssh` / `singularity` / `modal` / `daytona`.
23
+ lazyclaw is a single-binary-feel Node CLI in the "claw" family (Hermes → OpenClaw → nanoclaw). It is **TUI-first**: `lazyclaw` with no arguments opens a chat REPL with a sloth splash, slash commands, and ghost-text autocomplete. Underneath, every turn feeds a learning loop, and the same agent answers from any messaging channel you connect.
31
24
 
32
- ## What's new in 5.0.x
25
+ You can read the whole thing. No hosted service, no telemetry, config in plain JSON at `~/.lazyclaw/`, secrets in `~/.lazyclaw/.env` (0600).
33
26
 
34
- The 5.0.3 → 5.0.9 maintenance line is mostly a launcher and splash overhaul. `lazyclaw` with no arguments now drops you straight into chat — the arrow-key picker has moved to `lazyclaw menu`. The splash itself is responsive: a Larry-3D gradient wordmark, braille-rendered sloth hero, a tool catalog driven by `mas/tools/registry.mjs` (no more hand-edited lists), grouped skills, and a Hermes-style status bar that auto-collapses to a single column on narrow terminals.
27
+ ## Quick start
35
28
 
36
- Per-release notes live in [CHANGELOG.md](./CHANGELOG.md).
29
+ ```bash
30
+ npm install -g lazyclaw # or: npx lazyclaw
31
+ lazyclaw # fresh install → guided setup, then chat
32
+ ```
37
33
 
38
- ## Known limitations (v5.1 roadmap)
34
+ The first run is a **phased wizard** (Hermes-style — get one clean chat working first, then layer the rest):
39
35
 
40
- Calibrate expectations before reading the rest:
36
+ 1. **Provider + model** arrow-key picker (`claude-cli` is keyless; gemini/openai/anthropic take an API key).
37
+ 2. **Verify** — a one-token ping confirms the provider answers.
38
+ 3. **Context window** — how much history to keep per turn (optional).
39
+ 4. **Channel** — Slack / Telegram / Matrix / HTTP built in; Discord / Email / Signal / Voice / WhatsApp via plugins (optional).
40
+ 5. **Workspace / skills / webhook** (optional).
41
+ 6. **Orchestration** — turn on the planner + workers pipeline (optional).
41
42
 
42
- - Recall today is reachable via `lazyclaw loop --recall "<query>" ...` and `lazyclaw goal ... --recall "<query>"`; the `/recall` slash command and top-level `lazyclaw recall ...` CLI shape both ship in v5.1.
43
- - `lazyclaw sandbox` exposes `list | test | add | use`; the `sandbox run --backend ...` shape lands in v5.1.
44
- - `codex-cli` and `gemini-cli` provider modules are tracked but not yet registered in the main runtime.
45
- - The E2E matrix in `tests/e2e/phaseH-e2e-matrix.spec.ts` still has a number of flows marked `test.skip` pending v5.1 wiring; the min-green-set is documented at the top of that file.
43
+ Re-run it any time with `lazyclaw setup`, or `/config` from inside chat.
46
44
 
47
- ## Install
45
+ <img src="docs/screenshots/onboard.png" alt="lazyclaw provider picker" width="680">
48
46
 
49
- ```bash
50
- # Try it (no install)
51
- npx lazyclaw onboard && lazyclaw chat
47
+ ## $0 self-learning
52
48
 
53
- # Keep it
54
- npm install -g lazyclaw
49
+ lazyclaw splits two provider slots: **`provider`** (chat — the hot path) and **`trainer`** (the learning loop — skill synthesis, reflection, the user model). They are independent, so a Claude Pro/Max subscription can power the learning while chat runs anywhere — or both run on one backend.
55
50
 
56
- # Hack on it
57
- git clone https://github.com/cmblir/lazyclaw && cd lazyclaw
58
- npm install && npm link
59
- ```
51
+ After every turn, a fire-and-forget loop records the trajectory and distils reusable skills, tagged `trained_by`. With `trainer: { provider: "auto" }` it auto-detects your `claude-cli` session and runs the loop for free; otherwise it mirrors the chat provider.
60
52
 
61
- ```bash
62
- lazyclaw version
53
+ ```jsonc
54
+ // ~/.lazyclaw/config.json
55
+ {
56
+ "provider": "openai", // chat: pay-per-token (or claude-cli, ollama, …)
57
+ "model": "gpt-4.1",
58
+ "trainer": { "provider": "auto" } // learning: $0 on your Claude subscription
59
+ }
63
60
  ```
64
61
 
65
- Output reports the current installed version, Node version, and platform. See [CHANGELOG.md](./CHANGELOG.md) for release notes.
62
+ | Setup | `provider` | `trainer` | Cost |
63
+ |---|---|---|---|
64
+ | Subscription only | `claude-cli` | `auto` | **$0** |
65
+ | Hybrid (recommended) | `openai` / any | `auto` | chat only |
66
+ | Pure API | `openai` / any | `openai` | both metered |
66
67
 
67
- Requires **Node 18+**. macOS / Linux / WSL are first-class. Native PowerShell runs but ghost-text and the ANSI banner fall back to plain prompts.
68
+ ## Talk to it anywhere
68
69
 
69
- ## First run
70
+ Connect a channel and **the same agent** answers there — every listener forwards into the always-on daemon's shared session store, so chat, the dashboard, and all channels are one agent with one memory (and context follows across channels). Slack, Telegram, Matrix, and HTTP are built in; Discord, Email, Signal, Voice, and WhatsApp install as `@lazyclaw/channel-*` plugins.
70
71
 
71
72
  ```bash
72
- lazyclaw onboard # arrow-key picker; defaults to claude-cli (no key)
73
- # → writes ~/.lazyclaw/config.json
74
- ```
73
+ lazyclaw service install # 1) the always-on daemon (the shared brain)
75
74
 
76
- <img src="docs/screenshots/onboard.png" alt="lazyclaw onboard --non-interactive" width="720">
75
+ # 2) point a listener at it (Slack Socket Mode: SLACK_BOT_TOKEN + SLACK_APP_TOKEN in ~/.lazyclaw/.env)
76
+ lazyclaw slack listen # forwards inbound to the daemon, replies in-thread
77
+ lazyclaw slack listen --provider orchestrator # …and orchestrate the reply
78
+ lazyclaw slack listen --daemon-url http://127.0.0.1:19600 # non-default daemon
77
79
 
78
- ```bash
79
- lazyclaw # no-arg chat (since v5.0.6)
80
- lazyclaw menu # arrow-key launcher (formerly the no-arg default)
81
- lazyclaw status # → { provider, model, hasApiKey }
82
- lazyclaw doctor # validates config + provider registry + index.db
80
+ lazyclaw channels # view configured channels
81
+ lazyclaw channels enable|disable slack
82
+ lazyclaw channels install @lazyclaw/channel-discord
83
83
  ```
84
84
 
85
- Move the config dir with `LAZYCLAW_CONFIG_DIR=/elsewhere`. For automation, add `--non-interactive --provider X --model Y [--api-key Z]`.
85
+ A listener is a thin forwarder: it owns the channel socket (Slack Socket Mode needs no public URL, just an app-level `xapp-` token) and POSTs each message to the daemon's `/inbound`, which binds the conversation to a persistent session and runs the provider. It needs a reachable daemon (`lazyclaw service install`, or `lazyclaw daemon`); override the target with `--daemon-url` / `LAZYCLAW_DAEMON_URL`. Set it all up from the wizard's channel step, or `/channels` in chat.
86
86
 
87
- ## The trainer split — $0 learning on your Claude Pro subscription
87
+ ## Run it always-on
88
88
 
89
- v5 separates two provider slots in your config: `provider` (chat the hot path) and `trainer.provider` (skill synthesis, user-model updates, reflection bursty, cheap). They are wired independently, so a Pro/Max subscription can power the learning loop while chat runs through any provider, paid or local. lazyclaw is the **only** terminal agent CLI that splits these two roles.
89
+ **One process, every channel:** `lazyclaw gateway` runs the daemon core *and* your configured channel transports (Slack Socket Mode / Telegram long-poll / Matrix sync) in a single process and because the channels live in-process, `/handoff` can notify the target channel with a resume marker (a failed notify rolls the handoff back).
90
90
 
91
- ```jsonc
92
- // ~/.lazyclaw/config.json
93
- {
94
- "provider": "openai", // chat: pay-per-token
95
- "model": "gpt-4.1",
96
- "trainer": {
97
- "provider": "claude-cli", // trainer: $0 on Pro/Max
98
- "model": "claude-haiku-4-5",
99
- "schedule": "nightly",
100
- "budget": { "maxCallsPerDay": 200, "usdPerDay": 0.50 }
101
- }
102
- }
91
+ ```bash
92
+ lazyclaw gateway # daemon core + every enabled channel, one process
93
+ lazyclaw gateway --channels slack # explicit channel set
94
+ lazyclaw service install gateway # …and keep it alive across reboots
103
95
  ```
104
96
 
105
- The canonical default is `trainer.provider = "auto"` resolves to `claude-cli` when a Pro/Max session is detected, else mirrors the chat provider.
97
+ The gateway is **authenticated by default**: it mints a bearer token on first run and persists it to `~/.lazyclaw/gateway.token` (0600, never logged). Its own channels use it automatically; external callers read it from the file (`--auth-token`/`--no-auth` to override).
106
98
 
107
- **Three configs covering the common cases:**
99
+ Or run pieces separately: the daemon is the agent core — one provider path and one session/memory store on `127.0.0.1` — and `* listen` commands are standalone single-channel forwarders.
108
100
 
109
- | Setup | `provider` | `trainer.provider` | Cost |
110
- |---|---|---|---|
111
- | Subscription only | `claude-cli` | `claude-cli` | $0 |
112
- | Hybrid (recommended) | `openai` / any | `claude-cli` | chat-only |
113
- | Pure API | `openai` / any | `openai` / any | both metered |
101
+ ```bash
102
+ lazyclaw service install # daemon only: launchd (macOS) · systemd user unit (Linux) · pidfile fallback
103
+ lazyclaw service status
104
+ lazyclaw service uninstall
105
+ ```
114
106
 
115
- Full JSONC examples and the `auto` resolution rules: [docs/trainer-recipes.md](./docs/trainer-recipes.md).
107
+ The backend is auto-detected (override with `--backend launchd|systemd|fallback`); flags pass through, e.g. `lazyclaw service install gateway --port 19600 --auth-token "$TOK" --channels slack`.
116
108
 
117
- ## Provider matrix
109
+ Inbound channel messages are **idempotent**: each message's native id (Slack `channel:ts`, Telegram `chat:message_id`, Matrix `event_id`) is deduplicated by the daemon — a redelivery or listener-restart replay returns the recorded reply instead of running the provider twice. And every session-bound channel turn feeds the same post-task **learning loop** as the chat REPL (trainer `auto` → your Claude subscription, $0).
118
110
 
119
- | Chat provider | Trainer provider | Auth |
120
- |---|---|---|
121
- | `claude-cli` | `claude-cli` | subscription (Pro/Max) |
122
- | `anthropic` | `anthropic` | API key |
123
- | `openai` | `openai` | API key |
124
- | `gemini` | `gemini` | API key |
125
- | `ollama` | `ollama` | local (no key) |
126
- | `nim` / `openrouter` / `groq` / `together` / `xai` / `deepseek` / `mistral` / `fireworks` | — | API key (OpenAI-compatible) |
111
+ > [!NOTE]
112
+ > A channel listener or the daemon refuses to start while `security.allowUnattendedSensitive=true` — that flag bypasses the fail-closed tool-approval gate for *every* inbound message, so an always-on surface plus that flag is a remote-code-execution path. Keep sensitive-tool approval interactive.
127
113
 
128
- Plus two meta-providers, usable in either slot:
129
- - `orchestrator` — composes any of the above into a multi-agent pipeline ([docs/multi-agent.md](./docs/multi-agent.md))
130
- - `custom` — any OpenAI-compatible v1 endpoint with your own base URL + key
114
+ ## Multi-agent orchestration
131
115
 
132
- <img src="docs/screenshots/providers.png" alt="lazyclaw providers info model list + capabilities" width="720">
116
+ Set the provider to `orchestrator` and a hard request becomes **Plan → Delegate → Synthesise**: a planner decomposes the task, workers run the subtasks in parallel, then the planner merges the results. Workers are real agents with the tool registry.
133
117
 
134
- ## What it ships
118
+ ```bash
119
+ lazyclaw orchestrator set-planner claude-cli:claude-sonnet-4-6
120
+ lazyclaw orchestrator workers add claude-cli:claude-sonnet-4-6
121
+ lazyclaw orchestrator on # route chats through the pipeline
122
+ ```
135
123
 
136
- ### Tool registry 12 categories plus MCP
124
+ From chat: `/orchestrator` opens an on/off picker, or `/orchestrator on|off|planner <spec>|worker add <spec>`. Details: [docs/multi-agent.md](./docs/multi-agent.md).
137
125
 
138
- A single registry (`mas/tools/registry.mjs`) aggregates every first-party tool across twelve categories:
126
+ ## Drive it from chat
139
127
 
140
- `agents` · `browser` · `coding` · `exec` · `fs` · `git` · `iot` · `learning` · `media` · `net` · `os` · `scheduling`
128
+ The REPL has slash commands for everything you'd otherwise edit config for point-and-pick, no JSON:
141
129
 
142
- Tools flagged `sensitive: true` (writes, network egress, shell exec, sensitive git ops) route through an approval hook before execution. The splash renderer, the agent toolset resolver, and the runtime all read from the same registry, so a new tool group lights up everywhere at once. External servers extend the registry over stdio MCP.
130
+ | Slash | Does |
131
+ |---|---|
132
+ | `/config` | leave chat and re-run the setup wizard |
133
+ | `/provider` · `/model` | pick provider / model from a searchable list |
134
+ | `/channels [<name> on\|off]` | view / toggle channels |
135
+ | `/orchestrator [on\|off\|…]` | view / toggle multi-agent (picker on bare call) |
136
+ | `/context [turns N\|tokens N]` | resize the chat history window |
137
+ | `/skill` · `/personality` · `/memory` · `/loop` · `/goal` | skills, personas, memory, loops, goals |
143
138
 
144
- ### Channels that hand off
139
+ `/help` lists them all. Ghost-text autocomplete completes commands as you type; CJK/Hangul input composes inside the box.
145
140
 
146
- First-class channels for Slack, Discord, Telegram, Matrix, Email, and Voice. Signal and WhatsApp ship as full implementations with external runtime dependencies (`signal-cli` for Signal; `whatsapp-web.js` browser automation with QR-on-first-run for WhatsApp). Move a live conversation between any two with `/handoff <target> <externalId>` — no other agent CLI does cross-channel handoff.
141
+ ## The dashboard
147
142
 
148
143
  ```bash
149
- lazyclaw channels install @lazyclaw/channel-discord
150
- # inside Slack: /handoff discord <channel-id>
151
- # inside Discord: /handoff tui <thread-id>
144
+ lazyclaw dashboard # local web UI on http://127.0.0.1:19600
152
145
  ```
153
146
 
154
- Slack is built-in and does not need `channels install`. The plugin loader expects `@lazyclaw/channel-<name>` npm package names.
155
-
156
- ### Personas that compose
157
-
158
- Swap personality per channel without losing session memory. Layers compose top-down: global SOUL → workspace SOUL → active personality → agent role → user model (USER.md) → skill bank → memory core → recent trajectory tail. See [docs/persona-cookbook.md](./docs/persona-cookbook.md).
159
-
160
- ### Loops and scheduled goals
161
-
162
- Durable foreground or `--detach` loops; cron-scheduled goals with channel fan-out. State lives in `~/.lazyclaw/loops/<id>/` and survives restart. See [docs/loop-goal-preflight.md](./docs/loop-goal-preflight.md).
147
+ A framework-free SPA over the daemon's JSON API: Chat, Sessions, Workflows, Skills, Providers, Rates, Metrics, Doctor, Config, Status, Agents, Teams, Tasks, Trainer, Recall, Sandbox, Channels — 17 tabs, dark amber theme.
163
148
 
164
- ### A TUI that ghosts the right answer
149
+ ## Providers
165
150
 
166
- Ink-based UI with a responsive splash (Larry-3D gradient wordmark, sloth braille hero, grouped subcommand catalog, registry-backed tool list, filename-grouped skills, Hermes-style status bar — auto-collapses to single-column on narrow terminals), Cursor-style ghost autocomplete (`→` accepts, `Tab` cycles), interrupt-and-redirect REPL, and multiline editor.
151
+ | Chat / trainer | Auth |
152
+ |---|---|
153
+ | `claude-cli` | subscription (Pro/Max) — keyless |
154
+ | `anthropic` · `openai` · `gemini` | API key |
155
+ | `ollama` | local, no key |
156
+ | `nim` · `openrouter` · `groq` · `together` · `xai` · `deepseek` · `mistral` · `fireworks` | OpenAI-compatible API key |
157
+ | `custom` | any OpenAI-compatible v1 endpoint (your base URL + key) |
158
+ | `orchestrator` | meta-provider — planner + workers over any of the above |
167
159
 
168
- ## Command reference
160
+ <img src="docs/screenshots/providers.png" alt="lazyclaw providers info" width="680">
169
161
 
170
- | Command | Purpose |
171
- |---|---|
172
- | `lazyclaw` | No-arg → drops into chat (since v5.0.6) |
173
- | `lazyclaw menu` | Arrow-key launcher (formerly the no-arg default) |
174
- | `lazyclaw onboard` | Arrow-key setup; writes `~/.lazyclaw/config.json` |
175
- | `lazyclaw status` | Print active provider / model / masked key |
176
- | `lazyclaw doctor` | Validate config, provider registry, FTS5 index |
177
- | `lazyclaw chat` | Interactive REPL with ghost autocomplete + slash commands |
178
- | `lazyclaw agent "<prompt>"` | One-shot generate; supports stdin |
179
- | `lazyclaw run \| resume \| inspect` | DAG / sequential / persistent workflow jobs (`--dir <state-dir>`) |
180
- | `lazyclaw config get\|set\|list\|edit\|validate` | Dotted-key config access |
181
- | `lazyclaw sandbox list\|test\|add\|use` | Manage sandbox backends |
182
- | `lazyclaw channels install\|list\|remove` | Channel plugin lifecycle (`@lazyclaw/channel-<name>`) |
183
- | `lazyclaw trajectories export --format ...` | Export to atropos / axolotl / openai-ft / jsonl |
184
- | `lazyclaw personality use\|list\|show` | Activate / inspect personas |
185
- | `lazyclaw dashboard` | Local web UI on `127.0.0.1:19600` |
186
- | `lazyclaw migrate v5` | v4 → v5 with backup |
187
- | `lazyclaw recall` | (v5.1) Top-level recall over the FTS5 index — today, use `loop`/`goal` with `--recall "<query>"` |
188
- | `lazyclaw version` / `lazyclaw help` | Version + subcommand help |
189
-
190
- In-REPL slash commands include `/help`, `/status`, `/provider`, `/model`, `/skill`, `/loop`, `/goal`, `/memory`, `/agent`, `/team`, `/handoff`, `/personality`, `/exit`. Depth lives in `lazyclaw <cmd> --help` and the docs below.
191
-
192
- ## Migrating from v4
193
-
194
- > [!IMPORTANT]
195
- > `lazyclaw migrate v5` backs up your existing `~/.lazyclaw/` to `backup-v4-<ts>/` before rewriting anything. Don't skip the backup — the SQLite schema and skill frontmatter both change shape.
196
-
197
- - **What changed**: split `trainer` provider block, new `~/.lazyclaw/index.db` (SQLite + FTS5), per-day JSONL trajectory sink, persona file directory, kebab-case provider IDs (`claude-cli`, `gemini-cli`, ...), additive SKILL.md frontmatter (`group`, `trained_by`, `confidence`, `cross_cli_tested`).
198
- - **Migrate**:
199
- ```bash
200
- npm install -g lazyclaw@5
201
- lazyclaw migrate v5
202
- ```
203
- - **Rollback**: restore `~/.lazyclaw/backup-v4-<ts>/` and `npm install -g lazyclaw@4`.
204
-
205
- Full guide: [docs/migration-v4-to-v5.md](./docs/migration-v4-to-v5.md).
206
-
207
- ## CLI vs Channels — quick reference
208
-
209
- | Capability | TUI | Slack / Discord / Telegram |
210
- |---|---|---|
211
- | Start a chat | `lazyclaw chat` | `@lazyclaw <message>` |
212
- | Hand off to another surface | `/handoff slack <channel-id>` | `/handoff tui <thread-id>` |
213
- | Recall across history | `lazyclaw loop --recall "<query>" ...` (slash + top-level v5.1) | `lazyclaw loop --recall "<query>" ...` (channel-side `/recall` v5.1) |
214
- | Switch persona | `/personality use terse` | `/personality use terse` |
215
- | Switch model | `/model <name>` | `/model <name>` |
216
- | Show status | `lazyclaw status` | `@lazyclaw status` |
217
-
218
- Channel plugins implement a shared `channels/base.mjs` contract, so the surface you address them through is interchangeable.
219
-
220
- ## Configuration
162
+ ## What else it ships
221
163
 
222
- ```jsonc
223
- // ~/.lazyclaw/config.json
224
- {
225
- "provider": "claude-cli",
226
- "model": "claude-opus-4-7",
227
- "trainer": {
228
- "provider": "auto", // "auto" | claude-cli | anthropic | openai | gemini | ollama
229
- "model": "claude-haiku-4-5",
230
- "schedule": "nightly",
231
- "budget": { "maxCallsPerDay": 200, "usdPerDay": 0.50 }
232
- },
233
- "sandbox": { "backend": "local" }, // local | docker | ssh | singularity | modal | daytona
234
- "channels": { "slack": { "enabled": true } },
235
- "persona": { "active": "default" },
236
- "orchestra": { "learning": { "crossCliDampenFactor": 0.85 } }
237
- }
238
- ```
164
+ - **Tool registry** — 12 categories (`agents`, `browser`, `coding`, `exec`, `fs`, `git`, `iot`, `learning`, `media`, `net`, `os`, `scheduling`) plus stdio MCP. Sensitive tools (shell, write, network) are **fail-closed** behind an approval hook by default.
165
+ - **Durable recall** — one SQLite + FTS5 index over sessions, skills, trajectories, and memory; rebuildable from the corpus.
166
+ - **Loops & goals** — durable foreground/`--detach` loops and cron-scheduled goals that survive restart.
167
+ - **Personas** — layered SOUL / workspace / personality / role / user-model / skills compose into the system prompt.
168
+ - **Sandboxes** — `local` / `docker` / `ssh` / `singularity` / `modal` / `daytona` behind one API.
239
169
 
240
- Override the config directory with `LAZYCLAW_CONFIG_DIR=/path`. Workflow state moves with `LAZYCLAW_WORKFLOW_STATE_DIR=/path`. Channel tokens live exclusively in `~/.lazyclaw/.env`. Full schema in `lazyclaw config --help` and the docs.
170
+ ## Configuration & security
241
171
 
242
- ## Security and privacy
172
+ Config is plain JSON at `~/.lazyclaw/config.json`; channel + provider secrets live in `~/.lazyclaw/.env` (written 0600, never logged). Move the dir with `LAZYCLAW_CONFIG_DIR=/path`.
243
173
 
244
174
  > [!WARNING]
245
- > **`~/.lazyclaw/config.json` is trusted code.** Values resolved with `$(...)` execute at config load. Treat the file like a shell rc — never paste an untrusted snippet without reading it first.
175
+ > Treat `~/.lazyclaw/config.json` like a shell rc — values resolved with `$(...)` execute at load. Don't paste an untrusted snippet without reading it.
176
+
177
+ Sensitive tools deny by default unless an approval hook grants them; `config.json` and workflow state are written owner-only; secrets are scrubbed from the `bash` tool's child env and redacted from trajectories and synthesised skills.
246
178
 
247
- - **Trainer transcripts stay local.** Skill synthesis and user-model writes go through your configured trainer provider only; there is no hosted lazyclaw service.
248
- - **Sandbox backends enforce different blast radii.** `local` runs as you; `docker` / `singularity` / `daytona` isolate; `ssh` / `modal` move execution off-host. Pick per task.
249
- - **Secrets are redacted** from trajectories and synthesised skills (`sk-...`, `ghp_...`, `AKIA...`, bearer tokens, `*_KEY=...`, PEM blocks). Channel tokens live in `~/.lazyclaw/.env` and are never logged.
179
+ ## Install / hack
250
180
 
251
- ## Documentation
181
+ ```bash
182
+ npm install -g lazyclaw # install
183
+ git clone https://github.com/cmblir/lazyclaw && cd lazyclaw && npm install && npm link # hack
184
+ node --test tests/*.test.mjs # run the suite
185
+ ```
252
186
 
253
- - [docs/migration-v4-to-v5.md](./docs/migration-v4-to-v5.md) full v4 v5 walkthrough + rollback
254
- - [docs/trainer-recipes.md](./docs/trainer-recipes.md) — $0, hybrid, offline, and `auto` trainer configs
255
- - [docs/persona-cookbook.md](./docs/persona-cookbook.md) — layered persona compose stack + skin import
256
- - [docs/multi-agent.md](./docs/multi-agent.md) — orchestrator pipeline + Slack team data model
257
- - [docs/loop-goal-preflight.md](./docs/loop-goal-preflight.md) — durable loops and cron-scheduled goals
258
- - [docs/agent-memory.md](./docs/agent-memory.md) — per-agent memory, reflection, and skill synthesis
259
- - [CHANGELOG.md](./CHANGELOG.md) — release notes (Keep a Changelog)
260
- - [README.ko.md](./README.ko.md) — Korean companion
187
+ Requires **Node 18+** (Node 22+ for Slack Socket Mode). macOS / Linux / WSL are first-class.
261
188
 
262
- ## Community
189
+ ## Docs
263
190
 
264
- Source and issues: [cmblir/lazyclaw](https://github.com/cmblir/lazyclaw). PRs welcome.
191
+ - [docs/multi-agent.md](./docs/multi-agent.md) orchestrator pipeline + Slack teams
192
+ - [docs/trainer-recipes.md](./docs/trainer-recipes.md) — $0, hybrid, and `auto` trainer configs
193
+ - [docs/persona-cookbook.md](./docs/persona-cookbook.md) — persona compose stack
194
+ - [docs/loop-goal-preflight.md](./docs/loop-goal-preflight.md) — loops + scheduled goals
195
+ - [CHANGELOG.md](./CHANGELOG.md) — release notes · [README.ko.md](./README.ko.md) — Korean
265
196
 
266
197
  ## License
267
198
 
268
- [MIT](./LICENSE)
199
+ [MIT](./LICENSE) · Source & issues: [cmblir/lazyclaw](https://github.com/cmblir/lazyclaw)
@@ -4,6 +4,16 @@
4
4
  // Pure function over (threads store, live channel map) — the CLI slash
5
5
  // and the daemon HTTP route both call this.
6
6
 
7
+ // The note rides inside a message posted INTO a channel, and /handoff callers
8
+ // control it — strip control characters (no ANSI/newline injection into
9
+ // channel messages) and bound the length. Session ids are internal state and
10
+ // never belong in user-visible text.
11
+ function sanitizeNote(note) {
12
+ // eslint-disable-next-line no-control-regex
13
+ const cleaned = String(note || '').replace(/[\x00-\x1f\x7f]/g, ' ').trim();
14
+ return cleaned.length > 200 ? cleaned.slice(0, 200) + '…' : cleaned;
15
+ }
16
+
7
17
  export async function runHandoff({ threads, channels, threadId, target, externalId, note = '' }) {
8
18
  const cur = threads.findByThread(threadId);
9
19
  if (!cur) {
@@ -23,7 +33,8 @@ export async function runHandoff({ threads, channels, threadId, target, external
23
33
  const next = threads.handoff(threadId, { channel: target, externalId });
24
34
 
25
35
  // 2. Notify source (best-effort) so the human knows where the convo went.
26
- const tail = note ? ` — ${note}` : '';
36
+ const cleanNote = sanitizeNote(note);
37
+ const tail = cleanNote ? ` — ${cleanNote}` : '';
27
38
  if (channels[srcChannel] && typeof channels[srcChannel].send === 'function') {
28
39
  try {
29
40
  await channels[srcChannel].send(srcExternal,
@@ -33,9 +44,10 @@ export async function runHandoff({ threads, channels, threadId, target, external
33
44
  }
34
45
  }
35
46
 
36
- // 3. Notify target with a resume marker.
47
+ // 3. Notify target with a resume marker. (No session id — internal state
48
+ // never belongs in user-visible channel text.)
37
49
  await channels[target].send(externalId,
38
- `resumed from ${srcChannel} (session ${next.sessionId})${tail}`);
50
+ `resumed from ${srcChannel}${tail}`);
39
51
 
40
52
  return next;
41
53
  }
@@ -62,8 +74,9 @@ export async function handoffWithRollback({ threads, threadId, target, externalI
62
74
  const next = threads.handoff(threadId, { channel: target, externalId: String(externalId) });
63
75
  if (typeof send === 'function') {
64
76
  try {
65
- const tail = note ? ` — ${note}` : '';
66
- await send(String(externalId), `resumed from ${prior.channel} (session ${next.sessionId})${tail}`);
77
+ const cleanNote = sanitizeNote(note);
78
+ const tail = cleanNote ? ` ${cleanNote}` : '';
79
+ await send(String(externalId), `resumed from ${prior.channel}${tail}`);
67
80
  } catch (e) {
68
81
  // Target never got the resume marker — roll the binding back so the
69
82
  // session isn't stranded on a channel that can't be reached.