lazyclaw 6.3.1 → 6.4.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/README.ko.md +5 -1
- package/README.md +17 -13
- package/agents.mjs +54 -4
- package/channels-discord/index.mjs +5 -1
- package/channels-email/index.mjs +4 -3
- package/channels-whatsapp/index.mjs +3 -2
- package/chat_window.mjs +11 -2
- package/cli.mjs +103 -3
- package/commands/agents.mjs +6 -1
- package/commands/auth_nodes.mjs +19 -1
- package/commands/automation.mjs +20 -5
- package/commands/channels.mjs +46 -3
- package/commands/chat.mjs +90 -33
- package/commands/chat_hardening.mjs +23 -0
- package/commands/daemon.mjs +99 -1
- package/commands/gateway.mjs +123 -4
- package/commands/mcp.mjs +150 -0
- package/commands/misc.mjs +20 -0
- package/commands/sessions.mjs +68 -8
- package/commands/setup.mjs +44 -8
- package/commands/setup_channels.mjs +219 -47
- package/commands/skills.mjs +19 -1
- package/commands/workflow_named.mjs +137 -0
- package/config-validate.mjs +10 -1
- package/config_features.mjs +45 -0
- package/cron.mjs +19 -8
- package/daemon/lib/auth.mjs +25 -0
- package/daemon/lib/cost.mjs +36 -0
- package/daemon/lib/respond.mjs +20 -1
- package/daemon/lib/team_inbound.mjs +69 -0
- package/daemon/route_table.mjs +5 -1
- package/daemon/routes/_deps.mjs +2 -2
- package/daemon/routes/config.mjs +11 -6
- package/daemon/routes/conversation.mjs +103 -40
- package/daemon/routes/events.mjs +45 -0
- package/daemon/routes/meta.mjs +38 -7
- package/daemon/routes/providers.mjs +26 -6
- package/daemon/routes/workflows.mjs +30 -0
- package/daemon.mjs +26 -2
- package/dotenv_min.mjs +15 -3
- package/gateway/challenge_registry.mjs +90 -0
- package/gateway/device_auth.mjs +71 -97
- package/gateway/http_gateway.mjs +29 -3
- package/lib/args.mjs +69 -3
- package/lib/config.mjs +85 -3
- package/lib/render.mjs +43 -0
- package/lib/service_install.mjs +15 -1
- package/mas/agent_turn.mjs +61 -12
- package/mas/confidence.mjs +20 -1
- package/mas/embedder.mjs +100 -0
- package/mas/events.mjs +57 -0
- package/mas/index_db.mjs +103 -10
- package/mas/learning.mjs +96 -72
- package/mas/mention_router.mjs +48 -90
- package/mas/prompt_stack.mjs +60 -3
- package/mas/recall_blend.mjs +56 -0
- package/mas/redact.mjs +55 -0
- package/mas/router_posting.mjs +96 -0
- package/mas/router_termination.mjs +63 -0
- package/mas/scrub_env.mjs +21 -6
- package/mas/skill_synth.mjs +7 -33
- package/mas/tool_runner.mjs +3 -2
- package/mas/tools/bash.mjs +9 -2
- package/mas/tools/coding.mjs +28 -5
- package/mas/tools/delegation.mjs +34 -4
- package/mas/tools/git.mjs +19 -9
- package/mas/tools/ha.mjs +2 -0
- package/mas/tools/learning.mjs +55 -0
- package/mas/tools/media.mjs +21 -3
- package/mas/tools/os.mjs +70 -16
- package/mas/tools/recall.mjs +28 -2
- package/mcp/client.mjs +8 -1
- package/mcp/server_spawn.mjs +5 -1
- package/memory.mjs +24 -0
- package/package.json +3 -1
- package/providers/anthropic.mjs +101 -6
- package/providers/cache.mjs +9 -1
- package/providers/claude_cli.mjs +104 -22
- package/providers/claude_cli_session.mjs +166 -0
- package/providers/cli_error.mjs +38 -0
- package/providers/cli_login.mjs +179 -0
- package/providers/codex_cli.mjs +66 -11
- package/providers/gemini.mjs +101 -3
- package/providers/gemini_cli.mjs +62 -15
- package/providers/model_catalogue.mjs +33 -2
- package/providers/ollama.mjs +104 -3
- package/providers/openai.mjs +110 -8
- package/providers/openai_compat.mjs +97 -6
- package/providers/orchestrator.mjs +112 -12
- package/providers/registry.mjs +15 -9
- package/providers/retry.mjs +14 -2
- package/providers/tool_use/anthropic.mjs +17 -3
- package/providers/tool_use/claude_cli.mjs +72 -20
- package/providers/tool_use/gemini.mjs +18 -2
- package/providers/tool_use/openai.mjs +28 -5
- package/sandbox/confiners/seatbelt.mjs +37 -12
- package/sandbox/index.mjs +49 -0
- package/sandbox/local.mjs +7 -1
- package/sandbox/spawn.mjs +144 -0
- package/sandbox.mjs +5 -1
- package/scripts/loop-worker.mjs +25 -1
- package/sessions.mjs +0 -0
- package/skills/channel-style.md +20 -0
- package/skills/code-review.md +33 -0
- package/skills/commit-message.md +30 -0
- package/skills/concise.md +24 -0
- package/skills/debug-coach.md +25 -0
- package/skills/explain.md +24 -0
- package/skills/korean.md +25 -0
- package/skills/summarize.md +33 -0
- package/skills.mjs +24 -2
- package/skills_curator.mjs +6 -0
- package/skills_install.mjs +10 -2
- package/tasks.mjs +6 -1
- package/teams.mjs +78 -0
- package/tui/chat_mode_slash.mjs +59 -0
- package/tui/config_picker.mjs +1 -0
- package/tui/editor.mjs +178 -151
- package/tui/editor_keys.mjs +275 -0
- package/tui/hud.mjs +111 -0
- package/tui/login_flow.mjs +113 -0
- package/tui/modal_picker.mjs +10 -1
- package/tui/model_pick.mjs +287 -0
- package/tui/orchestrator_flow.mjs +164 -0
- package/tui/pickers.mjs +196 -80
- package/tui/repl.mjs +106 -44
- package/tui/repl_reset.mjs +37 -0
- package/tui/run_turn.mjs +228 -26
- package/tui/slash_args.mjs +159 -0
- package/tui/slash_commands.mjs +7 -5
- package/tui/slash_dispatcher.mjs +537 -344
- package/tui/slash_popup.mjs +5 -1
- package/tui/splash.mjs +15 -2
- package/tui/status_bar.mjs +26 -0
- package/tui/theme.mjs +28 -0
- package/web/avatars/01.png +0 -0
- package/web/avatars/02.png +0 -0
- package/web/avatars/03.png +0 -0
- package/web/avatars/04.png +0 -0
- package/web/avatars/05.png +0 -0
- package/web/avatars/06.png +0 -0
- package/web/avatars/07.png +0 -0
- package/web/avatars/08.png +0 -0
- package/web/avatars/09.png +0 -0
- package/web/avatars/10.png +0 -0
- package/web/avatars/11.png +0 -0
- package/web/avatars/12.png +0 -0
- package/web/avatars/13.png +0 -0
- package/web/avatars/14.png +0 -0
- package/web/avatars/15.png +0 -0
- package/web/avatars/16.png +0 -0
- package/web/avatars/17.png +0 -0
- package/web/avatars/18.png +0 -0
- package/web/avatars/19.png +0 -0
- package/web/avatars/20.png +0 -0
- package/web/dashboard.css +77 -0
- package/web/dashboard.html +29 -2
- package/web/dashboard.js +296 -33
- package/workflow/builtin_caps.mjs +94 -0
- package/workflow/declarative.mjs +101 -0
- package/workflow/named.mjs +71 -0
- package/workflow/named_cron.mjs +50 -0
- package/workflow/nodes.mjs +67 -0
- package/workflow/run_request.mjs +74 -0
- package/workflow/yaml_min.mjs +97 -0
package/README.ko.md
CHANGED
|
@@ -85,12 +85,16 @@ chat에서 `/orchestrator` (빈 입력 = on/off picker) 또는 `/orchestrator on
|
|
|
85
85
|
|---|---|
|
|
86
86
|
| `/config` | chat 나가고 setup 위저드 재실행 |
|
|
87
87
|
| `/provider` · `/model` | 검색 picker로 provider/model 선택 |
|
|
88
|
+
| `/trainer [set\|fallback]` · `/agent edit <name>` | trainer / agent의 provider+model을 같은 picker로 선택 (`auto`·custom-id 행 포함) |
|
|
88
89
|
| `/channels [<name> on\|off]` | 채널 보기 / 토글 |
|
|
89
90
|
| `/orchestrator [on\|off\|…]` | 멀티에이전트 보기 / 토글 (빈 입력=picker) |
|
|
90
91
|
| `/context [turns N\|tokens N]` | 히스토리 윈도우 조절 |
|
|
92
|
+
| `/agentic [on\|off]` · `/plan [on\|off]` | chat에서 tool 실행(approval 게이트); plan은 읽기전용 "먼저 제안" |
|
|
91
93
|
| `/skill` · `/personality` · `/memory` · `/loop` · `/goal` | 스킬·페르소나·메모리·루프·목표 |
|
|
92
94
|
|
|
93
|
-
`/help`로 전체 목록.
|
|
95
|
+
`/help`로 전체 목록. 타이핑하면 자동완성: 명령어(`/...`)와 그 **인자**까지 — `/login`→`codex-cli`/`gemini-cli`, `/hud`→`on`/`off`, `/channels`→채널명, `/task`·`/team`·`/agent`·`/personality`·`/trainer`·`/orchestrator` 서브커맨드 등 — 팝업에 떠서 ↑/↓ 선택, Enter로 채움. provider→model 2단계 선택(`/model`·`/trainer set`·`/orchestrator planner`)은 `↹ pick` 힌트가 뜨고 **Tab**으로 드릴인 모달 열림. 한글 IME는 박스 안에서 조합.
|
|
96
|
+
|
|
97
|
+
스킬은 markdown 지침 번들로 시스템 프롬프트에 합성된다. `lazyclaw skills starter`로 번들 스타터 팩 8종(`concise` · `korean` · `commit-message` · `code-review` · `channel-style` · `summarize` · `explain` · `debug-coach`) 설치, `lazyclaw skills install <user>/<repo>`로 GitHub에서 추가 설치, chat에서는 `/skills`로 선택.
|
|
94
98
|
|
|
95
99
|
## 대시보드
|
|
96
100
|
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ The first run is a **phased wizard** (Hermes-style — get one clean chat workin
|
|
|
36
36
|
1. **Provider + model** — arrow-key picker (`claude-cli` is keyless; gemini/openai/anthropic take an API key).
|
|
37
37
|
2. **Verify** — a one-token ping confirms the provider answers.
|
|
38
38
|
3. **Context window** — how much history to keep per turn (optional).
|
|
39
|
-
4. **Channel** — Slack / Telegram / Matrix / HTTP built in; Discord / Email /
|
|
39
|
+
4. **Channel** — Slack / Telegram / Matrix / HTTP built in; Discord / Email / Voice / WhatsApp ship in-tree (need a runtime dep via `lazyclaw channels install <name>`), Signal needs `signal-cli` (optional).
|
|
40
40
|
5. **Workspace / skills / webhook** (optional).
|
|
41
41
|
6. **Orchestration** — turn on the planner + workers pipeline (optional).
|
|
42
42
|
|
|
@@ -67,7 +67,7 @@ After every turn, a fire-and-forget loop records the trajectory and distils reus
|
|
|
67
67
|
|
|
68
68
|
## Talk to it anywhere
|
|
69
69
|
|
|
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,
|
|
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, Voice, and WhatsApp ship in-tree and run once their runtime dependency is installed into the config dir (`lazyclaw channels install <name>`), and Signal needs the external `signal-cli` binary on your PATH.
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
73
|
lazyclaw service install # 1) the always-on daemon (the shared brain)
|
|
@@ -79,7 +79,7 @@ lazyclaw slack listen --daemon-url http://127.0.0.1:19600 # non-default daemon
|
|
|
79
79
|
|
|
80
80
|
lazyclaw channels # view configured channels
|
|
81
81
|
lazyclaw channels enable|disable slack
|
|
82
|
-
lazyclaw channels install
|
|
82
|
+
lazyclaw channels install discord # installs discord.js into ~/.lazyclaw and enables the channel
|
|
83
83
|
```
|
|
84
84
|
|
|
85
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.
|
|
@@ -129,14 +129,16 @@ The REPL has slash commands for everything you'd otherwise edit config for — p
|
|
|
129
129
|
|
|
130
130
|
| Slash | Does |
|
|
131
131
|
|---|---|
|
|
132
|
-
| `/config` |
|
|
132
|
+
| `/config` | change one setting in-chat (provider/model/context/channel creds/webhook/…); `/setup` re-runs the whole wizard |
|
|
133
133
|
| `/provider` · `/model` | pick provider / model from a searchable list |
|
|
134
|
-
| `/
|
|
134
|
+
| `/trainer [set\|fallback]` · `/agent edit <name>` | pick the trainer / an agent's provider+model from the same list (with an `auto` and a custom-id row) |
|
|
135
|
+
| `/channels [<name> on\|off\|setup]` | view / toggle channels; `setup` sets the bot token & credentials in-chat |
|
|
135
136
|
| `/orchestrator [on\|off\|…]` | view / toggle multi-agent (picker on bare call) |
|
|
136
137
|
| `/context [turns N\|tokens N]` | resize the chat history window |
|
|
138
|
+
| `/agentic [on\|off]` · `/plan [on\|off]` | let chat run tools (approval-gated); plan mode is read-only "propose first" |
|
|
137
139
|
| `/skill` · `/personality` · `/memory` · `/loop` · `/goal` | skills, personas, memory, loops, goals |
|
|
138
140
|
|
|
139
|
-
`/help` lists them all.
|
|
141
|
+
`/help` lists them all. Autocomplete works as you type: command names (`/...`) and, after a command, its **arguments** — `/login` → `codex-cli`/`gemini-cli`, `/hud` → `on`/`off`, `/channels` → channel names, subcommands for `/task` `/team` `/agent` `/personality` `/trainer` `/orchestrator`, and more — appear in a popup (↑/↓ select, Enter fill). The 2-step provider→model picks (`/model`, `/trainer set`, `/orchestrator planner`) show a `↹ pick` hint; press **Tab** to open the drill-in modal. CJK/Hangul input composes inside the box.
|
|
140
142
|
|
|
141
143
|
## The dashboard
|
|
142
144
|
|
|
@@ -144,7 +146,7 @@ The REPL has slash commands for everything you'd otherwise edit config for — p
|
|
|
144
146
|
lazyclaw dashboard # local web UI on http://127.0.0.1:19600
|
|
145
147
|
```
|
|
146
148
|
|
|
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 —
|
|
149
|
+
A framework-free SPA over the daemon's JSON API: Chat, Sessions, Workflows, Skills, Providers, Rates, Metrics, Doctor, Config, Status, Agents, Teams, Tasks, **Team Live**, Trainer, Recall, Sandbox, Channels — 18 tabs, dark amber theme. The **Team Live** tab is a real-time org view: avatar tiles with status rings + harness badges (`provider · model`), live agent-A→B delegation, and click-to-drill-down (harness, current task, recent activity), streamed over Server-Sent Events (`GET /events`).
|
|
148
150
|
|
|
149
151
|
## Providers
|
|
150
152
|
|
|
@@ -162,17 +164,19 @@ A framework-free SPA over the daemon's JSON API: Chat, Sessions, Workflows, Skil
|
|
|
162
164
|
## What else it ships
|
|
163
165
|
|
|
164
166
|
- **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
|
-
- **
|
|
167
|
+
- **Skills** — markdown instruction bundles composed into the system prompt. `lazyclaw skills starter` installs the bundled pack (`concise` · `korean` · `commit-message` · `code-review` · `channel-style` · `summarize` · `explain` · `debug-coach`); `lazyclaw skills install <user>/<repo>` pulls more from GitHub; `/skills` picks one in chat.
|
|
168
|
+
- **Durable recall** — one SQLite + FTS5 index over sessions, skills, trajectories, and memory; rebuildable from the corpus (`lazyclaw index rebuild`). Optionally blend in embedding similarity (`cfg.recall.embeddings`, off by default; OpenAI/Gemini key or a local Ollama model — the $0 path stays pure FTS5); `lazyclaw index embed` backfills the vectors.
|
|
166
169
|
- **Loops & goals** — durable foreground/`--detach` loops and cron-scheduled goals that survive restart.
|
|
167
170
|
- **Personas** — layered SOUL / workspace / personality / role / user-model / skills compose into the system prompt.
|
|
168
|
-
- **
|
|
171
|
+
- **Agent teams, live** — build a hierarchy (a planner agent with sub-agents on different harnesses, e.g. a data-engineer on `gemini-cli` and a backend on `claude-cli`) via each agent's `manager`; a Slack message to a team's channel auto-routes into the multi-agent loop, and the **Team Live** dashboard tab shows who is doing what, on which harness, and the real-time A→B delegation as it happens.
|
|
172
|
+
- **Default-on confinement** — sensitive tools (`bash`, `python_exec`/`node_exec`, `git_*`, the `os` tools) run confined by default: writes are limited to the workspace + temp, secret dirs (`~/.ssh`, `~/.aws`, the config dir, …) are unreadable, and the secret-scrubbed env still applies. macOS uses `seatbelt`, Linux uses `bubblewrap`/`firejail` (auto-detected). Network is allowed; opt out with `cfg.sandbox.confine=false`. Run `lazyclaw sandbox status` to see the effective posture.
|
|
173
|
+
- **Sandboxes** — `local` / `docker` / `ssh` / `singularity` / `modal` / `daytona` behind one API; pluggable backends for heavier isolation (containers, remote hosts) layered on top of the default local confinement.
|
|
174
|
+
- **MCP** — stdio MCP servers in `cfg.mcp.servers` boot with the daemon; their tools register as `mcp:<server>:<tool>` (always approval-gated). Manage them from the CLI: `lazyclaw mcp list` / `mcp add <name> --command <cmd> [--args "…"] [--allow-glob <glob>]` / `mcp remove <name>` / `mcp call <server> <tool> [--args-json '{…}']` (spawns the server, runs one tool, tears it down).
|
|
175
|
+
- **Daemon lifecycle** — `lazyclaw daemon status | stop | logs` over a pidfile; `maxTokens` in config raises the output cap; `LAZYCLAW_REQUEST_TIMEOUT_MS` sets the per-provider idle timeout (default 120s).
|
|
169
176
|
|
|
170
177
|
## Configuration & security
|
|
171
178
|
|
|
172
|
-
Config is plain JSON at `~/.lazyclaw/config.json
|
|
173
|
-
|
|
174
|
-
> [!WARNING]
|
|
175
|
-
> Treat `~/.lazyclaw/config.json` like a shell rc — values resolved with `$(...)` execute at load. Don't paste an untrusted snippet without reading it.
|
|
179
|
+
Config is plain JSON at `~/.lazyclaw/config.json` — parsed with `JSON.parse`, no shell or code execution; channel + provider secrets live in `~/.lazyclaw/.env` (written 0600, never logged). Move the dir with `LAZYCLAW_CONFIG_DIR=/path`.
|
|
176
180
|
|
|
177
181
|
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.
|
|
178
182
|
|
package/agents.mjs
CHANGED
|
@@ -16,11 +16,23 @@ import fs from 'node:fs';
|
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import os from 'node:os';
|
|
18
18
|
import { ensureValidName as cronEnsureValidName } from './cron.mjs';
|
|
19
|
+
import * as toolRegistry from './mas/tools/registry.mjs';
|
|
19
20
|
|
|
20
21
|
const AGENTS_DIRNAME = 'agents';
|
|
21
22
|
|
|
22
23
|
export const DEFAULT_TOOLS = ['bash', 'read', 'write', 'grep', 'skill_view'];
|
|
23
|
-
|
|
24
|
+
|
|
25
|
+
// The valid tool set is derived from the LIVE tool registry (51+ tools), not a
|
|
26
|
+
// hardcoded 8-name list with a stale, unregistered 'slack_post' — that list
|
|
27
|
+
// rejected recall/delegate/git_*/edit/etc. and silently capped team agents.
|
|
28
|
+
export function knownTools() {
|
|
29
|
+
return new Set([...DEFAULT_TOOLS, ...toolRegistry.listNames()]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Back-compat export: a snapshot of the registered tool names (the registry
|
|
33
|
+
// self-populates at import). mcp:* tools register dynamically; validateTools
|
|
34
|
+
// accepts them even when absent from this snapshot.
|
|
35
|
+
export const ALL_TOOLS = [...knownTools()];
|
|
24
36
|
|
|
25
37
|
export class AgentError extends Error {
|
|
26
38
|
constructor(message, code) {
|
|
@@ -52,9 +64,12 @@ function validateTools(tools) {
|
|
|
52
64
|
if (!Array.isArray(tools)) {
|
|
53
65
|
throw new AgentError('tools must be an array', 'AGENT_BAD_TOOLS');
|
|
54
66
|
}
|
|
55
|
-
|
|
67
|
+
// Validate against the LIVE registry; accept mcp:* names (they register when
|
|
68
|
+
// their server starts) so a config referencing one isn't rejected at edit time.
|
|
69
|
+
const known = knownTools();
|
|
70
|
+
const bad = tools.filter(t => !known.has(t) && !/^mcp:/.test(String(t)));
|
|
56
71
|
if (bad.length) {
|
|
57
|
-
throw new AgentError(`unknown tool(s): ${bad.join(', ')}
|
|
72
|
+
throw new AgentError(`unknown tool(s): ${bad.join(', ')}`, 'AGENT_BAD_TOOLS');
|
|
58
73
|
}
|
|
59
74
|
// Dedupe while preserving order.
|
|
60
75
|
return [...new Set(tools)];
|
|
@@ -75,6 +90,10 @@ function defaultShape(name) {
|
|
|
75
90
|
tools: [...DEFAULT_TOOLS],
|
|
76
91
|
tags: [],
|
|
77
92
|
iconEmoji: '',
|
|
93
|
+
// Optional parent agent (hierarchy). '' = top-level. A team's org tree is
|
|
94
|
+
// derived from members' manager links (see teams.teamTree). Validated to
|
|
95
|
+
// reference a registered agent and to never form a cycle.
|
|
96
|
+
manager: '',
|
|
78
97
|
// Phase 18 — agent memory write trigger. 'auto' means the router
|
|
79
98
|
// fires a reflection LLM call on terminal `done`; 'manual' waits
|
|
80
99
|
// for `lazyclaw agent reflect`; 'off' disables writes entirely.
|
|
@@ -105,7 +124,34 @@ function writeAtomic(filePath, obj) {
|
|
|
105
124
|
const VALID_MEMORY_WRITE = ['auto', 'manual', 'off'];
|
|
106
125
|
const VALID_SKILL_WRITE = ['auto', 'manual', 'off'];
|
|
107
126
|
|
|
108
|
-
|
|
127
|
+
// Validate a proposed `manager` (parent agent) for `name`: it must reference a
|
|
128
|
+
// registered agent, may not be the agent itself, and may not close a cycle
|
|
129
|
+
// (i.e. `name` must not already be an ancestor of the proposed manager).
|
|
130
|
+
// Returns the normalised manager string ('' when none).
|
|
131
|
+
function validateManager(name, manager, configDir) {
|
|
132
|
+
if (!manager) return '';
|
|
133
|
+
const mgr = String(manager);
|
|
134
|
+
if (mgr === name) {
|
|
135
|
+
throw new AgentError(`agent "${name}" cannot manage itself`, 'AGENT_BAD_MANAGER');
|
|
136
|
+
}
|
|
137
|
+
if (!getAgent(mgr, configDir)) {
|
|
138
|
+
throw new AgentError(`unknown manager "${mgr}" (not a registered agent)`, 'AGENT_BAD_MANAGER');
|
|
139
|
+
}
|
|
140
|
+
let cur = mgr;
|
|
141
|
+
const visited = new Set();
|
|
142
|
+
while (cur) {
|
|
143
|
+
if (cur === name) {
|
|
144
|
+
throw new AgentError(`manager "${manager}" would create a cycle`, 'AGENT_MANAGER_CYCLE');
|
|
145
|
+
}
|
|
146
|
+
if (visited.has(cur)) break; // pre-existing cycle elsewhere — stop walking
|
|
147
|
+
visited.add(cur);
|
|
148
|
+
const rec = getAgent(cur, configDir);
|
|
149
|
+
cur = rec && rec.manager ? String(rec.manager) : null;
|
|
150
|
+
}
|
|
151
|
+
return mgr;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function registerAgent({ name, displayName, role = '', provider = 'claude-cli', model = '', tools, tags = [], iconEmoji = '', memoryWrite, memoryMaxChars, skillWrite, manager } = {}, configDir = defaultConfigDir()) {
|
|
109
155
|
ensureValidName(name);
|
|
110
156
|
const p = agentPath(name, configDir);
|
|
111
157
|
if (fs.existsSync(p)) {
|
|
@@ -132,6 +178,7 @@ export function registerAgent({ name, displayName, role = '', provider = 'claude
|
|
|
132
178
|
memoryWrite: mw,
|
|
133
179
|
memoryMaxChars: Number.isFinite(+memoryMaxChars) && +memoryMaxChars > 0 ? +memoryMaxChars : 12 * 1024,
|
|
134
180
|
skillWrite: sw,
|
|
181
|
+
manager: validateManager(name, manager, configDir),
|
|
135
182
|
};
|
|
136
183
|
writeAtomic(p, data);
|
|
137
184
|
return data;
|
|
@@ -173,6 +220,9 @@ export function patchAgent(name, patch, configDir = defaultConfigDir()) {
|
|
|
173
220
|
if (patch.skillWrite !== undefined && !VALID_SKILL_WRITE.includes(patch.skillWrite)) {
|
|
174
221
|
throw new AgentError(`skillWrite must be one of ${VALID_SKILL_WRITE.join(', ')}`, 'AGENT_BAD_SKILL_WRITE');
|
|
175
222
|
}
|
|
223
|
+
if (patch.manager !== undefined) {
|
|
224
|
+
next.manager = validateManager(name, patch.manager, configDir);
|
|
225
|
+
}
|
|
176
226
|
writeAtomic(agentPath(name, configDir), next);
|
|
177
227
|
return next;
|
|
178
228
|
}
|
|
@@ -15,6 +15,10 @@ export class DiscordChannel extends Channel {
|
|
|
15
15
|
constructor(opts = {}) {
|
|
16
16
|
super('discord');
|
|
17
17
|
this._token = opts.token || process.env.DISCORD_BOT_TOKEN || null;
|
|
18
|
+
// loadDep resolves the runtime dep from the config dir when the gateway
|
|
19
|
+
// injects it (lazyclaw channels install discord); falls back to a bare
|
|
20
|
+
// import so a dep installed alongside lazyclaw still works.
|
|
21
|
+
this._loadDep = typeof opts.loadDep === 'function' ? opts.loadDep : ((s) => import(s));
|
|
18
22
|
this._client = null;
|
|
19
23
|
this._lib = null;
|
|
20
24
|
}
|
|
@@ -22,7 +26,7 @@ export class DiscordChannel extends Channel {
|
|
|
22
26
|
async start(handler, opts = {}) {
|
|
23
27
|
await super.start(handler, opts);
|
|
24
28
|
if (!this._token) throw new Error('DiscordChannel: DISCORD_BOT_TOKEN missing');
|
|
25
|
-
this._lib = await
|
|
29
|
+
this._lib = await this._loadDep('discord.js');
|
|
26
30
|
const { Client, GatewayIntentBits, Partials } = this._lib;
|
|
27
31
|
this._client = new Client({
|
|
28
32
|
intents: [
|
package/channels-email/index.mjs
CHANGED
|
@@ -15,15 +15,16 @@ export class EmailChannel extends Channel {
|
|
|
15
15
|
this._imapOpts = opts.imap;
|
|
16
16
|
this._smtpOpts = opts.smtp || {};
|
|
17
17
|
this._from = opts.from || opts.imap.user;
|
|
18
|
+
this._loadDep = typeof opts.loadDep === 'function' ? opts.loadDep : ((s) => import(s));
|
|
18
19
|
this._imap = null;
|
|
19
20
|
this._transporter = null;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
async start(handler, opts = {}) {
|
|
23
24
|
await super.start(handler, opts);
|
|
24
|
-
const Imap = (await
|
|
25
|
-
const { simpleParser } = await
|
|
26
|
-
const nodemailer = (await
|
|
25
|
+
const Imap = (await this._loadDep('node-imap')).default;
|
|
26
|
+
const { simpleParser } = await this._loadDep('mailparser');
|
|
27
|
+
const nodemailer = (await this._loadDep('nodemailer')).default;
|
|
27
28
|
|
|
28
29
|
this._transporter = nodemailer.createTransport({
|
|
29
30
|
host: this._smtpOpts.host,
|
|
@@ -10,6 +10,7 @@ export class WhatsappChannel extends Channel {
|
|
|
10
10
|
constructor(opts = {}) {
|
|
11
11
|
super('whatsapp');
|
|
12
12
|
this._opts = opts || {};
|
|
13
|
+
this._loadDep = typeof opts.loadDep === 'function' ? opts.loadDep : ((s) => import(s));
|
|
13
14
|
this._client = null;
|
|
14
15
|
this._qrState = 'pending'; // pending | shown | authenticated | failed
|
|
15
16
|
this._lastQr = null;
|
|
@@ -20,8 +21,8 @@ export class WhatsappChannel extends Channel {
|
|
|
20
21
|
|
|
21
22
|
async start(handler, opts = {}) {
|
|
22
23
|
await super.start(handler, opts);
|
|
23
|
-
const wweb = await
|
|
24
|
-
const qrt = await
|
|
24
|
+
const wweb = await this._loadDep('whatsapp-web.js');
|
|
25
|
+
const qrt = await this._loadDep('qrcode-terminal');
|
|
25
26
|
const { Client, LocalAuth } = wweb;
|
|
26
27
|
this._client = new Client({
|
|
27
28
|
authStrategy: new LocalAuth({ dataPath: this._opts.dataPath || './whatsapp' }),
|
package/chat_window.mjs
CHANGED
|
@@ -16,6 +16,16 @@
|
|
|
16
16
|
export const CHAT_WINDOW_TURNS = Number(process.env.LAZYCLAW_CHAT_WINDOW_TURNS) || 20;
|
|
17
17
|
export const CHAT_WINDOW_TOKEN_BUDGET = Number(process.env.LAZYCLAW_CHAT_WINDOW_TOKENS) || 8000;
|
|
18
18
|
|
|
19
|
+
// Approximate token count of a messages[] array (4 chars/token, same heuristic
|
|
20
|
+
// the window cap uses). Drives the status-bar context gauge so it reflects the
|
|
21
|
+
// conversation history lazyclaw actually holds — NOT a provider's self-reported
|
|
22
|
+
// usage, which for CLI providers (codex/claude/gemini) includes their own
|
|
23
|
+
// system prompt + tool defs per call and has nothing to do with this budget.
|
|
24
|
+
export function estimateMessagesTokens(messages) {
|
|
25
|
+
const arr = Array.isArray(messages) ? messages : [];
|
|
26
|
+
return Math.ceil(arr.reduce((n, m) => n + String(m?.content || '').length, 0) / 4);
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
// Trim a hydrated messages[] array to fit the sliding window. Returns
|
|
20
30
|
// { messages, dropped } so the caller can log a one-shot "dropped N
|
|
21
31
|
// older turns" line at session start. The first message is preserved
|
|
@@ -31,8 +41,7 @@ export function applyChatWindow(messages, { turns = CHAT_WINDOW_TURNS, tokens =
|
|
|
31
41
|
while (out.length > turns) out.shift();
|
|
32
42
|
// Token budget cap: estimate 4 chars / token, then trim from the
|
|
33
43
|
// front (oldest) until estimated tokens ≤ budget.
|
|
34
|
-
|
|
35
|
-
while (out.length > 1 && estTokens(out) > tokens) out.shift();
|
|
44
|
+
while (out.length > 1 && estimateMessagesTokens(out) > tokens) out.shift();
|
|
36
45
|
const dropped = before - out.length;
|
|
37
46
|
if (sys) out.unshift(sys);
|
|
38
47
|
return { messages: out, dropped };
|
package/cli.mjs
CHANGED
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
// Phase D2 — provider-registry bootstrap (lazy load + per-call re-register).
|
|
11
11
|
import { ensureRegistry, requireRegistry, getRegistry } from './lib/registry_boot.mjs';
|
|
12
12
|
// Phase D2 — argument parsing + subcommand inventory + agent-registry classifier.
|
|
13
|
-
|
|
13
|
+
// nearest/usageHint power the unknown-subcommand "did you mean" + `help <name>`.
|
|
14
|
+
import { SUBCOMMANDS, parseArgs, AGENT_REG_SUBS, nearest, usageHint } from './lib/args.mjs';
|
|
14
15
|
// Phase D4 — interactive TUI pickers/banner helpers. Still imported here for the
|
|
15
16
|
// onboard / setup / launcher / chat paths that remain inline in this entrypoint.
|
|
16
17
|
import {
|
|
@@ -32,6 +33,11 @@ import { dispatchSlash as _dispatchSlash, parseSlashLine as _parseSlashLine } fr
|
|
|
32
33
|
// loop reads this same list the Ink path (_help) and the popup use.
|
|
33
34
|
import { SLASH_COMMANDS } from './tui/slash_commands.mjs';
|
|
34
35
|
|
|
36
|
+
// Sentinel thrown when cmdHelp would process.exit(2) for a known subcommand
|
|
37
|
+
// that lacks a rich HELP_DETAILS entry — caught in the `help` branch so we can
|
|
38
|
+
// fall back to a one-line usage hint instead of exiting non-zero.
|
|
39
|
+
class _HelpFallback extends Error {}
|
|
40
|
+
|
|
35
41
|
async function main() {
|
|
36
42
|
const argv = process.argv.slice(2);
|
|
37
43
|
const cmd = argv[0];
|
|
@@ -189,10 +195,67 @@ async function main() {
|
|
|
189
195
|
}
|
|
190
196
|
break;
|
|
191
197
|
}
|
|
198
|
+
case 'index': {
|
|
199
|
+
// FTS index recovery surface documented by mas/index_db.mjs and the
|
|
200
|
+
// doctor failure-log. reindexAll (NOT the destructive rebuild) drops the
|
|
201
|
+
// db and repopulates from the on-disk corpus, so recall is never left empty.
|
|
202
|
+
const indexSub = rest.positional[0];
|
|
203
|
+
const cfgDir = path.dirname(configPath());
|
|
204
|
+
if (indexSub === 'embed') {
|
|
205
|
+
// Opt-in hybrid recall: embed FTS rows that lack a vector. No-op when
|
|
206
|
+
// cfg.recall.embeddings is off (then it just reports 0 embedded).
|
|
207
|
+
try {
|
|
208
|
+
const indexDb = await import('./mas/index_db.mjs');
|
|
209
|
+
const embedded = await indexDb.backfillEmbeddings(cfgDir, readConfig());
|
|
210
|
+
console.log(JSON.stringify({ ok: true, embedded }));
|
|
211
|
+
process.exit(0);
|
|
212
|
+
} catch (e) {
|
|
213
|
+
console.error(`index embed failed: ${e.message}`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (indexSub !== 'rebuild') {
|
|
218
|
+
console.error('Usage: lazyclaw index <rebuild|embed>');
|
|
219
|
+
process.exit(2);
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const indexDb = await import('./mas/index_db.mjs');
|
|
223
|
+
indexDb.reindexAll(cfgDir);
|
|
224
|
+
// reindexAll returns void; derive what landed by counting the FTS rows.
|
|
225
|
+
const db = indexDb.openIndex(cfgDir);
|
|
226
|
+
const counts = {};
|
|
227
|
+
for (const [scope, table] of [
|
|
228
|
+
['sessions', 'fts_sessions'], ['skills', 'fts_skills'],
|
|
229
|
+
['trajectories', 'fts_trajectories'], ['memories', 'fts_memories'],
|
|
230
|
+
]) {
|
|
231
|
+
counts[scope] = db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get().n;
|
|
232
|
+
}
|
|
233
|
+
console.log(JSON.stringify({ ok: true, counts }));
|
|
234
|
+
process.exit(0);
|
|
235
|
+
} catch (e) {
|
|
236
|
+
console.error(`index rebuild failed: ${e.message}`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case 'mcp': {
|
|
242
|
+
// list (configured + connected) / add / remove / call. Config mutation
|
|
243
|
+
// (add/remove) edits cfg.mcp.servers; servers (re)spawn at daemon boot.
|
|
244
|
+
const sub = rest.positional[0];
|
|
245
|
+
await (await import('./commands/mcp.mjs')).cmdMcp(sub, rest.positional.slice(1), rest.flags);
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
192
248
|
case 'chat': {
|
|
193
249
|
await (await import('./commands/chat.mjs')).cmdChat(rest.flags);
|
|
194
250
|
break;
|
|
195
251
|
}
|
|
252
|
+
case 'workflow': {
|
|
253
|
+
// Named declarative workflows (cfg.workflows) — add/list/show/remove/run.
|
|
254
|
+
// Distinct from the top-level `run <session-id> <file.mjs>` engine.
|
|
255
|
+
const sub = rest.positional[0];
|
|
256
|
+
await (await import('./commands/workflow_named.mjs')).cmdWorkflow(sub, rest.positional.slice(1), rest.flags);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
196
259
|
case 'sessions': {
|
|
197
260
|
const sub = rest.positional[0];
|
|
198
261
|
await (await import('./commands/sessions.mjs')).cmdSessions(sub, rest.positional.slice(1), rest.flags);
|
|
@@ -369,7 +432,38 @@ async function main() {
|
|
|
369
432
|
case 'help':
|
|
370
433
|
case '--help':
|
|
371
434
|
case '-h': {
|
|
372
|
-
|
|
435
|
+
const name = rest.positional[0];
|
|
436
|
+
const cmdHelp = (await import('./commands/setup.mjs')).cmdHelp;
|
|
437
|
+
// Bare `help` / unknown-name handling stays here so we can offer a
|
|
438
|
+
// did-you-mean and a bounded usage-hint fallback without editing the
|
|
439
|
+
// rich HELP_DETAILS map in commands/setup.mjs.
|
|
440
|
+
if (!name) { cmdHelp(undefined); break; }
|
|
441
|
+
const hint = usageHint(name);
|
|
442
|
+
if (!hint) {
|
|
443
|
+
// Unknown subcommand name passed to help — suggest the nearest match.
|
|
444
|
+
process.stderr.write(`unknown command "${name}"`);
|
|
445
|
+
const guess = nearest(name, SUBCOMMANDS);
|
|
446
|
+
process.stderr.write(guess ? ` — did you mean "${guess}"?\n` : `\n`);
|
|
447
|
+
process.stderr.write('run `lazyclaw help` to see the list\n');
|
|
448
|
+
process.exit(2);
|
|
449
|
+
}
|
|
450
|
+
// Known subcommand: prefer the rich HELP_DETAILS entry. cmdHelp exits 2
|
|
451
|
+
// for subcommands that lack a detailed entry (writing its own "unknown
|
|
452
|
+
// subcommand" lines to stderr first), so guard that fatal exit AND buffer
|
|
453
|
+
// its stderr — on fallback we drop that noise and print the one-line
|
|
454
|
+
// usage hint instead. `help <name>` must never error on a real command.
|
|
455
|
+
const realExit = process.exit;
|
|
456
|
+
const realErr = process.stderr.write.bind(process.stderr);
|
|
457
|
+
const buffered = [];
|
|
458
|
+
let detailMissing = false;
|
|
459
|
+
process.exit = (code) => { if (code === 2) { detailMissing = true; throw new _HelpFallback(); } realExit(code); };
|
|
460
|
+
process.stderr.write = (chunk, ...a) => { buffered.push([chunk, a]); return true; };
|
|
461
|
+
try { cmdHelp(name); }
|
|
462
|
+
catch (e) { if (!(e instanceof _HelpFallback)) { process.exit = realExit; process.stderr.write = realErr; throw e; } }
|
|
463
|
+
finally { process.exit = realExit; process.stderr.write = realErr; }
|
|
464
|
+
// Non-fallback path: replay whatever cmdHelp wrote to stderr verbatim.
|
|
465
|
+
if (detailMissing) process.stdout.write(hint + '\n');
|
|
466
|
+
else for (const [chunk, a] of buffered) process.stderr.write(chunk, ...a);
|
|
373
467
|
break;
|
|
374
468
|
}
|
|
375
469
|
case 'menu': {
|
|
@@ -377,10 +471,16 @@ async function main() {
|
|
|
377
471
|
await (await import('./commands/setup.mjs')).cmdLauncher();
|
|
378
472
|
break;
|
|
379
473
|
}
|
|
380
|
-
default:
|
|
474
|
+
default: {
|
|
475
|
+
// Unknown top-level subcommand: lead with a closest-match suggestion so
|
|
476
|
+
// a typo (`sesions` -> `sessions`) is fixable without scanning the dump.
|
|
477
|
+
const guess = nearest(cmd, SUBCOMMANDS);
|
|
478
|
+
if (guess) console.error(`unknown command "${cmd}" — did you mean "${guess}"?`);
|
|
479
|
+
else console.error(`unknown command "${cmd}"`);
|
|
381
480
|
console.error('Usage: lazyclaw <' + SUBCOMMANDS.join('|') + '> ...');
|
|
382
481
|
console.error('Run `lazyclaw help` for a one-line summary of each subcommand.');
|
|
383
482
|
process.exit(2);
|
|
483
|
+
}
|
|
384
484
|
}
|
|
385
485
|
}
|
|
386
486
|
|
package/commands/agents.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import fs from 'node:fs';
|
|
|
5
5
|
import { configPath, readConfig, _resolveAuthKey, _resolveBaseUrl } from '../lib/config.mjs';
|
|
6
6
|
import { ensureRegistry, getRegistry } from '../lib/registry_boot.mjs';
|
|
7
7
|
import { loadDotenvIfAny as _loadDotenvShared } from '../dotenv_min.mjs';
|
|
8
|
+
import { defaultSandboxSpec } from '../sandbox/index.mjs';
|
|
8
9
|
|
|
9
10
|
// Thin .env loader wrapper kept local so the module stays self-contained.
|
|
10
11
|
export function _loadDotenvIfAny(cfgDir) { return _loadDotenvShared(cfgDir); }
|
|
@@ -326,6 +327,9 @@ export async function cmdTask(sub, positional, flags = {}) {
|
|
|
326
327
|
maxAgentTurns: flags['max-turns'] ? parseInt(flags['max-turns'], 10) : undefined,
|
|
327
328
|
approve,
|
|
328
329
|
security: cfg.security,
|
|
330
|
+
// Default-on isolation: confine every tool the team runs (filesystem
|
|
331
|
+
// to cwd, secrets unreadable, net allowed). Opt out via cfg.sandbox.
|
|
332
|
+
sandbox: defaultSandboxSpec(cfg, { cwd: process.cwd(), configDir: cfgDir }),
|
|
329
333
|
});
|
|
330
334
|
emitJson({ id: result.task.id, status: result.task.status, iterations: result.iterations, stoppedBy: result.stoppedBy });
|
|
331
335
|
} catch (err) {
|
|
@@ -484,7 +488,7 @@ export async function cmdAgentRegistry(sub, positional, flags = {}) {
|
|
|
484
488
|
return;
|
|
485
489
|
}
|
|
486
490
|
case 'add': {
|
|
487
|
-
if (!name) { console.error('Usage: lazyclaw agent add <name> [--role "..."] [--provider X] [--model Y] [--display "..."] [--tools bash,read,write,grep,skill_view] [--tags a,b] [--skill-write auto|manual|off]'); process.exit(2); }
|
|
491
|
+
if (!name) { console.error('Usage: lazyclaw agent add <name> [--role "..."] [--provider X] [--model Y] [--display "..."] [--manager <agent>] [--tools bash,read,write,grep,skill_view] [--tags a,b] [--skill-write auto|manual|off]'); process.exit(2); }
|
|
488
492
|
const tools = agentsMod.parseToolsFlag(flags.tools);
|
|
489
493
|
try {
|
|
490
494
|
const a = agentsMod.registerAgent({
|
|
@@ -496,6 +500,7 @@ export async function cmdAgentRegistry(sub, positional, flags = {}) {
|
|
|
496
500
|
tools: tools === null ? undefined : tools,
|
|
497
501
|
tags: agentsMod.parseToolsFlag(flags.tags) || [],
|
|
498
502
|
skillWrite: flags['skill-write'],
|
|
503
|
+
manager: flags.manager,
|
|
499
504
|
}, cfgDir);
|
|
500
505
|
emitJson(a);
|
|
501
506
|
} catch (err) {
|
package/commands/auth_nodes.mjs
CHANGED
|
@@ -204,8 +204,26 @@ export async function cmdNodes(sub, positional, flags = {}) {
|
|
|
204
204
|
console.log(JSON.stringify(store.revoke(deviceId)));
|
|
205
205
|
return;
|
|
206
206
|
}
|
|
207
|
+
case 'rotate': {
|
|
208
|
+
const deviceId = positional[0];
|
|
209
|
+
if (!deviceId) {
|
|
210
|
+
console.error('Usage: lazyclaw nodes rotate <deviceId> [--ttl <ms>]');
|
|
211
|
+
process.exit(2);
|
|
212
|
+
}
|
|
213
|
+
const ttlRaw = flags.ttl;
|
|
214
|
+
const ttlMs = (ttlRaw !== undefined && ttlRaw !== true) ? Number(ttlRaw) : undefined;
|
|
215
|
+
const { PairingStore } = await import('../gateway/device_auth.mjs');
|
|
216
|
+
const store = new PairingStore(path.dirname(configPath()));
|
|
217
|
+
try {
|
|
218
|
+
const { expiresAt } = store.rotate(deviceId, { ttlMs });
|
|
219
|
+
// Like approve, the token is intentionally NOT printed — the device
|
|
220
|
+
// receives its rotated token on its next /gateway/connect.
|
|
221
|
+
console.log(JSON.stringify({ ok: true, rotated: deviceId, ...(expiresAt ? { expiresAt } : {}), note: 'device receives its new token on next /gateway/connect' }));
|
|
222
|
+
} catch (e) { console.error(`error: ${e.message}`); process.exit(1); }
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
207
225
|
default:
|
|
208
|
-
console.error('Usage: lazyclaw nodes <list|register|remove|pending|approve <requestId>|revoke <deviceId>|devices> ...');
|
|
226
|
+
console.error('Usage: lazyclaw nodes <list|register|remove|pending|approve <requestId>|revoke <deviceId>|rotate <deviceId>|devices> ...');
|
|
209
227
|
process.exit(2);
|
|
210
228
|
}
|
|
211
229
|
}
|
package/commands/automation.mjs
CHANGED
|
@@ -10,6 +10,21 @@ import { loadDotenvIfAny as _loadDotenvShared } from '../dotenv_min.mjs';
|
|
|
10
10
|
// Thin .env loader wrapper kept local so the module stays self-contained.
|
|
11
11
|
export function _loadDotenvIfAny(cfgDir) { return _loadDotenvShared(cfgDir); }
|
|
12
12
|
|
|
13
|
+
// Build the detached loop-worker argv. Pure + exported so the flag-forwarding
|
|
14
|
+
// contract is unit-testable: --use-memory / --recall MUST reach the worker, or
|
|
15
|
+
// `loop --detach --use-memory` silently runs with no memory (the worker only
|
|
16
|
+
// builds a system message when it sees the flags).
|
|
17
|
+
export function buildDetachArgv(worker, { loopId, prompt, max, provName, cfgDir, until, requestedSession, model, useMemory, recall } = {}) {
|
|
18
|
+
const argv = [worker, '--loop-id', loopId, '--prompt', prompt,
|
|
19
|
+
'--max', String(max), '--provider', provName, '--cfg-dir', cfgDir];
|
|
20
|
+
if (until) { argv.push('--until', String(until)); }
|
|
21
|
+
if (requestedSession) { argv.push('--session-existing', requestedSession); }
|
|
22
|
+
if (model) { argv.push('--model', String(model)); }
|
|
23
|
+
if (useMemory) { argv.push('--use-memory'); }
|
|
24
|
+
if (recall) { argv.push('--recall', String(recall)); }
|
|
25
|
+
return argv;
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
export async function cmdCron(sub, positional, flags = {}) {
|
|
14
29
|
const cron = await import('../cron.mjs');
|
|
15
30
|
const cfg = readConfig();
|
|
@@ -169,11 +184,11 @@ export async function cmdLoop(prompt, flags = {}) {
|
|
|
169
184
|
// <repo>/scripts/loop-worker.mjs (was a sibling when cmdLoop was in cli.mjs).
|
|
170
185
|
const here = path.dirname(new URL(import.meta.url).pathname);
|
|
171
186
|
const worker = path.join(here, '..', 'scripts', 'loop-worker.mjs');
|
|
172
|
-
const argv =
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
187
|
+
const argv = buildDetachArgv(worker, {
|
|
188
|
+
loopId, prompt, max, provName, cfgDir,
|
|
189
|
+
until: flags.until, requestedSession, model,
|
|
190
|
+
useMemory: flags['use-memory'], recall: flags.recall,
|
|
191
|
+
});
|
|
177
192
|
const child = spawn(process.execPath, argv, {
|
|
178
193
|
detached: true,
|
|
179
194
|
stdio: 'ignore',
|
package/commands/channels.mjs
CHANGED
|
@@ -200,9 +200,40 @@ export async function cmdChannels(sub, positional = [], flags = {}) {
|
|
|
200
200
|
|
|
201
201
|
if (sub === 'install') {
|
|
202
202
|
const name = positional[0];
|
|
203
|
-
if (!name) { process.stderr.write('usage: lazyclaw channels install
|
|
204
|
-
const
|
|
205
|
-
|
|
203
|
+
if (!name) { process.stderr.write('usage: lazyclaw channels install <name> (e.g. discord, email, whatsapp)\n'); process.exit(2); }
|
|
204
|
+
const { isPluginName } = await import('../channels/loader.mjs');
|
|
205
|
+
// Back-compat: an explicit @lazyclaw/channel-* spec still routes through the
|
|
206
|
+
// legacy plugin loader (for anyone shipping a real published package).
|
|
207
|
+
if (isPluginName(name)) {
|
|
208
|
+
const info = await loader.install(name);
|
|
209
|
+
process.stdout.write(`installed ${info.name}@${info.version}\n`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const { channelByName, channelReadiness } = await import('./setup_channels.mjs');
|
|
213
|
+
const spec = channelByName(name.toLowerCase());
|
|
214
|
+
if (!spec) { process.stderr.write(`unknown channel: ${name} (known: ${KNOWN_CHANNELS.join(', ')})\n`); process.exit(2); }
|
|
215
|
+
if (spec.builtin) { process.stdout.write(`channel ${spec.name} is built in — no install needed.\n`); return; }
|
|
216
|
+
if (spec.binary && (!spec.deps || !spec.deps.length)) {
|
|
217
|
+
process.stderr.write(`channel ${spec.name} needs the external "${spec.binary}" binary on your PATH (not an npm package). Install it from its project, then: lazyclaw channels enable ${spec.name}\n`);
|
|
218
|
+
process.exit(2);
|
|
219
|
+
}
|
|
220
|
+
const deps = spec.deps || [];
|
|
221
|
+
if (!deps.length) { process.stdout.write(`channel ${spec.name} needs no runtime package — set creds with \`lazyclaw setup\`, then \`lazyclaw channels enable ${spec.name}\`.\n`); return; }
|
|
222
|
+
// Install the in-tree adapter's runtime deps INTO the config dir; the
|
|
223
|
+
// gateway resolves them from there (commands/gateway.mjs _loadPluginChannel).
|
|
224
|
+
const { spawnSync } = await import('node:child_process');
|
|
225
|
+
process.stdout.write(`installing ${deps.join(', ')} into ${cfgDir} …\n`);
|
|
226
|
+
const res = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--prefix', cfgDir, ...deps], { stdio: 'inherit', env: process.env });
|
|
227
|
+
if (res.status !== 0) { process.stderr.write(`npm install failed (exit ${res.status})\n`); process.exit(1); }
|
|
228
|
+
const r = channelReadiness(spec.name, cfgDir);
|
|
229
|
+
if (r.ready) {
|
|
230
|
+
const cfg = readConfig();
|
|
231
|
+
channelSetEnabled(cfg, spec.name, true);
|
|
232
|
+
writeConfig(cfg);
|
|
233
|
+
process.stdout.write(`installed ${deps.join(', ')} — channel ${spec.name} enabled.\n`);
|
|
234
|
+
} else {
|
|
235
|
+
process.stdout.write(`installed ${deps.join(', ')}. Still missing: ${[...r.missingDeps, r.missingBinary].filter(Boolean).join(', ')}.\n`);
|
|
236
|
+
}
|
|
206
237
|
return;
|
|
207
238
|
}
|
|
208
239
|
if (sub === 'remove' || sub === 'uninstall') {
|
|
@@ -212,6 +243,18 @@ export async function cmdChannels(sub, positional = [], flags = {}) {
|
|
|
212
243
|
process.stdout.write(`removed ${name}\n`);
|
|
213
244
|
return;
|
|
214
245
|
}
|
|
246
|
+
if (sub === 'test') {
|
|
247
|
+
const name = (positional[0] || '').toLowerCase();
|
|
248
|
+
if (!name) { process.stderr.write('usage: lazyclaw channels test <name>\n'); process.exit(2); }
|
|
249
|
+
const { verifyChannel } = await import('./setup_channels.mjs');
|
|
250
|
+
try { (await import('../dotenv_min.mjs')).loadDotenvIfAny(cfgDir); } catch { /* best-effort */ }
|
|
251
|
+
const r = await verifyChannel(name);
|
|
252
|
+
if (flags.json) { process.stdout.write(JSON.stringify({ channel: name, ...r }) + '\n'); return; }
|
|
253
|
+
if (r.ok === true) process.stdout.write(`✓ ${name} verified — ${r.detail}\n`);
|
|
254
|
+
else if (r.ok === null) process.stdout.write(`· ${name}: ${r.detail}\n`);
|
|
255
|
+
else { process.stdout.write(`✗ ${name}: ${r.detail}${r.hint ? `\n fix: ${r.hint}` : ''}\n`); process.exitCode = 1; }
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
215
258
|
if (sub === 'enable' || sub === 'disable') {
|
|
216
259
|
const name = (positional[0] || '').toLowerCase();
|
|
217
260
|
if (!name) { process.stderr.write(`usage: lazyclaw channels ${sub} <name>\n`); process.exit(2); }
|