lazyclaw 6.4.0 → 6.6.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/agents.mjs +68 -1
- package/commands/agents.mjs +3 -194
- package/commands/agents_registry.mjs +227 -0
- package/commands/automation.mjs +8 -89
- package/commands/automation_loops.mjs +94 -0
- package/commands/chat.mjs +46 -680
- package/commands/chat_legacy_slash.mjs +716 -0
- package/commands/config.mjs +36 -2
- package/commands/help_text.mjs +78 -0
- package/commands/setup.mjs +1 -73
- package/daemon/lib/cost.mjs +5 -0
- package/daemon/route_table.mjs +1 -0
- package/daemon/routes/meta.mjs +29 -0
- package/daemon.mjs +1 -1
- package/lib/args.mjs +1 -1
- package/mas/agent_turn.mjs +7 -0
- package/package.json +1 -1
- package/providers/claude_cli.mjs +1 -1
- package/providers/claude_cli_session.mjs +21 -4
- package/providers/codex_cli.mjs +8 -5
- package/providers/gemini.mjs +4 -1
- package/providers/gemini_cli.mjs +17 -3
- package/providers/tool_use/gemini.mjs +12 -1
- package/providers/tool_use/openai.mjs +7 -0
- package/tui/banner.mjs +72 -0
- package/tui/editor.mjs +0 -0
- package/tui/editor_anchor.mjs +46 -0
- package/tui/orchestrator_setup.mjs +135 -0
- package/tui/pickers.mjs +34 -180
- package/tui/repl.mjs +12 -165
- package/tui/repl_altbuffer.mjs +63 -0
- package/tui/repl_reducers.mjs +114 -0
- package/tui/slash_channels.mjs +208 -0
- package/tui/slash_dashboard.mjs +220 -0
- package/tui/slash_dispatcher.mjs +12 -640
- package/tui/slash_helpers.mjs +68 -0
- package/tui/slash_trainer.mjs +173 -0
- package/web/dashboard.js +7 -1
package/commands/config.mjs
CHANGED
|
@@ -102,11 +102,45 @@ export async function cmdConfigEdit() {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// Coerce a CLI string value into the type it most likely represents so a
|
|
106
|
+
// boolean/number setting isn't stored as a string the rest of the code then
|
|
107
|
+
// mis-compares (`if (cfg.chat.recall)` is truthy for the string "false").
|
|
108
|
+
// 'true' / 'false' → boolean
|
|
109
|
+
// integer / float strings → number
|
|
110
|
+
// everything else → the original string (provider/model/api-key/…)
|
|
111
|
+
function coerceConfigValue(value) {
|
|
112
|
+
if (typeof value !== 'string') return value;
|
|
113
|
+
if (value === 'true') return true;
|
|
114
|
+
if (value === 'false') return false;
|
|
115
|
+
// Only treat as a number when the WHOLE string is a clean numeric literal —
|
|
116
|
+
// Number('') is 0 and Number('1.2.3') is NaN, both of which we reject so
|
|
117
|
+
// ids like "gpt-4.1" or empty values stay strings.
|
|
118
|
+
if (value.trim() !== '' && /^-?(?:\d+|\d*\.\d+|\d+\.\d*)$/.test(value.trim()) && Number.isFinite(Number(value))) {
|
|
119
|
+
return Number(value);
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
|
|
105
124
|
export function cmdConfigSet(key, value) {
|
|
106
125
|
const cfg = readConfig();
|
|
107
|
-
|
|
126
|
+
const coerced = coerceConfigValue(value);
|
|
127
|
+
if (typeof key === 'string' && key.includes('.')) {
|
|
128
|
+
// Dotted key → nested path. Walk/create each intermediate object so
|
|
129
|
+
// `chat.recall` lands as cfg.chat.recall (not a flat "chat.recall" key).
|
|
130
|
+
// A non-object value blocking the path is replaced rather than crashing.
|
|
131
|
+
const segs = key.split('.');
|
|
132
|
+
let node = cfg;
|
|
133
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
134
|
+
const seg = segs[i];
|
|
135
|
+
if (!node[seg] || typeof node[seg] !== 'object' || Array.isArray(node[seg])) node[seg] = {};
|
|
136
|
+
node = node[seg];
|
|
137
|
+
}
|
|
138
|
+
node[segs[segs.length - 1]] = coerced;
|
|
139
|
+
} else {
|
|
140
|
+
cfg[key] = coerced;
|
|
141
|
+
}
|
|
108
142
|
writeConfig(cfg);
|
|
109
|
-
console.log(JSON.stringify({ ok: true, key, value }));
|
|
143
|
+
console.log(JSON.stringify({ ok: true, key, value: coerced }));
|
|
110
144
|
}
|
|
111
145
|
export async function cmdDoctor() {
|
|
112
146
|
await ensureRegistry();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// commands/help_text.mjs — pure-data help tables for `lazyclaw help`,
|
|
2
|
+
// extracted verbatim from commands/setup.mjs (file-size gate, no behavior
|
|
3
|
+
// change). HELP_SUMMARIES (one-liners for `help`) and HELP_DETAILS (per-
|
|
4
|
+
// subcommand usage for `help <name>`) are read only by cmdHelp. They import
|
|
5
|
+
// nothing and reference nothing else — pure data, lowest-risk extraction.
|
|
6
|
+
|
|
7
|
+
// One-line summaries used by `lazyclaw help`. Format keeps it scan-friendly
|
|
8
|
+
// in a 80-column terminal: subcommand padded to 12 chars, then the summary.
|
|
9
|
+
export const HELP_SUMMARIES = {
|
|
10
|
+
run: 'Execute a workflow file (run <session-id> <workflow.mjs>)',
|
|
11
|
+
resume: 'Resume a workflow from its last persisted checkpoint',
|
|
12
|
+
config: 'Manage local config (get|set|list|delete <key>)',
|
|
13
|
+
chat: 'Interactive REPL with the configured provider',
|
|
14
|
+
agent: 'One-shot prompt: streams a single response, exits',
|
|
15
|
+
doctor: 'Print diagnostic JSON; exits non-zero on issues',
|
|
16
|
+
status: 'Print current provider/model/masked key as JSON',
|
|
17
|
+
onboard: 'Guided setup (use --non-interactive for scripts)',
|
|
18
|
+
sessions: 'Persistent chat sessions (list|show|clear|export)',
|
|
19
|
+
skills: 'Markdown skill bundles (list|show|install|remove)',
|
|
20
|
+
providers: 'Inspect / register providers (list|info|test|add|remove|models)',
|
|
21
|
+
daemon: 'Run the local HTTP gateway (--port, --auth-token, --allow-origin)',
|
|
22
|
+
version: 'Print VERSION + node + platform as JSON',
|
|
23
|
+
completion: 'Emit shell completion script (completion <bash|zsh>)',
|
|
24
|
+
export: 'Dump config + skills (+ optional sessions) as a JSON bundle',
|
|
25
|
+
import: 'Apply a JSON bundle from stdin or --from <path>',
|
|
26
|
+
rates: 'Manage cost rate-cards in config (rates list|set <provider/model>|delete|shape)',
|
|
27
|
+
auth: 'Multiple keys per provider (auth list|add|remove|use|rotate <provider>)',
|
|
28
|
+
pairing: 'Sender allowlist for the messaging surface (pairing list|add|remove <id>)',
|
|
29
|
+
nodes: 'Companion device registration (nodes list|register|remove <id>)',
|
|
30
|
+
message: 'Outbound webhook messaging (message list|add|remove|send <name>)',
|
|
31
|
+
workspace: 'AGENTS.md / SOUL.md / TOOLS.md system-prompt convention (workspace list|init|show|remove|path)',
|
|
32
|
+
browse: 'Fetch a URL and emit Markdown on stdout (browse <url> [--max-bytes <N>])',
|
|
33
|
+
cron: 'Schedule recurring agent runs via launchd / crontab (cron list|add|remove|show|sync|run)',
|
|
34
|
+
setup: 'Hermes-style phased first-run wizard (provider + verify chat + channel + workspace + skill + webhook)',
|
|
35
|
+
dashboard: 'Launch the lazyclaw-only web UI (lighter than the full lazyclaude dashboard)',
|
|
36
|
+
inspect: 'Print persisted workflow state without executing',
|
|
37
|
+
clear: 'Delete a persisted workflow state file (idempotent)',
|
|
38
|
+
validate: 'Static-check a workflow file: shape, deps, cycles, parallelism',
|
|
39
|
+
graph: 'Emit workflow DAG as Mermaid syntax (paste-ready for docs)',
|
|
40
|
+
orchestrator: 'Multi-agent dispatch — planner decomposes, workers run, planner synthesises',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Detailed usage per subcommand for `lazyclaw help <name>`. Kept as flat
|
|
44
|
+
// strings so the help output is identical in every terminal.
|
|
45
|
+
export const HELP_DETAILS = {
|
|
46
|
+
run: 'Usage: lazyclaw run <session-id> <workflow.mjs> [--parallel | --parallel-persistent] [--concurrency <N>]\n Default: runPersistent — sequential, persists state, resumable via `lazyclaw resume`.\n --parallel: runParallel — topological-level DAG, in-memory only, NOT resumable.\n --parallel-persistent: runPersistentDag — DAG + checkpoint + resume.\n --concurrency <N>: cap in-flight nodes within a level (DAG modes only). 0/missing → unbounded.\n Workflow file exports `nodes`; deps: string[] declares dependencies for both DAG modes.',
|
|
47
|
+
resume: 'Usage: lazyclaw resume <session-id> <workflow.mjs> [--parallel-persistent] [--concurrency <N>]\n Re-enters a previously persisted run; succeeds nodes are skipped.\n Pass --parallel-persistent to resume a DAG run (must match the original run\'s mode).\n --concurrency <N>: cap in-flight nodes per level (DAG mode only).',
|
|
48
|
+
inspect: 'Usage: lazyclaw inspect [<session-id>] [--dir <state-dir>] [--status done|resumable|failed|running] [--summary] [--filter <substr>] [--limit <N>] [--node <node-id>] [--slowest <N>] [--critical-path <workflow.mjs>] [--aggregate]\n With no session-id: list every persisted session in the state dir, sorted by recency.\n --aggregate (list mode): per-node stats across all sessions (count, success/failed/pending/running, min/max/avg/total duration).\n --status filters the listing to a single lifecycle bucket.\n --filter / --limit refine list-mode further (case-insensitive sessionId substring + post-filter cap).\n --summary trims per-node detail in single-session mode (matches list-mode shape).\n --node <id>: print just that node\'s state. Exit 0 success/pending/running, 1 failed, 2 no such node.\n --slowest <N>: top N nodes by durationMs (descending, ties broken by id).\n --critical-path <workflow.mjs>: longest-weighted-path analysis using each node\'s recorded durationMs (bottleneck finder).\n With a session-id (no per-node flag): print full state. Exit code: 0=resumable, 1=fully done, 2=no state, 3=terminal failure.',
|
|
49
|
+
clear: 'Usage: lazyclaw clear <session-id> [--dir <state-dir>]\n Delete the state file for <session-id>. Idempotent — exits 0 whether the file existed or not.\n Refuses sessionIds that resolve outside <state-dir>. Mirrors DELETE /workflows/<id> on the daemon.',
|
|
50
|
+
validate: 'Usage: lazyclaw validate <workflow.mjs>\n Static check: load + shape + dep + cycle + parallelism estimate.\n Exit 0 valid · 1 hard failure (issues populated) · 2 file/import error.',
|
|
51
|
+
graph: 'Usage: lazyclaw graph <workflow.mjs> [--lr] [--state <session-id>] [--dir <state-dir>]\n Emit the workflow DAG as Mermaid syntax (graph TD by default; --lr for left-right).\n --state overlays a persisted run\'s status (success ✓ / running ⏳ / failed ✗ / pending) with classDef styling.\n Output is paste-ready for GitHub markdown / Notion / Obsidian.',
|
|
52
|
+
config: 'Usage: lazyclaw config <get|set|list|delete|path|edit|validate> [key] [value]\n Local key-value config at $LAZYCLAW_CONFIG_DIR/config.json (default ~/.lazyclaw).\n `path` prints the file location; `edit` opens it in $EDITOR (or $LAZYCLAW_EDITOR / $VISUAL / vi) and validates JSON on save.\n `validate` checks the structural integrity of the whole config file (typed values, known providers, rate-card shape).',
|
|
53
|
+
chat: 'Usage: lazyclaw chat [--session <id>] [--skill name1,name2] [--workspace <name>] [--pick] [--sandbox docker:<image>] [--sandbox-network <net>] [--sandbox-mount <m>] [--sandbox-env <e>]\n --session persists turns to <configDir>/sessions/<id>.jsonl across invocations.\n --skill composes named skills into a system message at the head of the conversation.\n --workspace stitches AGENTS.md/SOUL.md/TOOLS.md from <configDir>/workspaces/<name>/ into the system prompt.\n --pick opens an interactive provider/model picker before the prompt (also auto-fires on first run).\n --sandbox routes the underlying claude CLI through `docker run --rm -i --network <net> -v cwd:cwd ...` (default --network=none).',
|
|
54
|
+
agent: 'Usage: lazyclaw agent <prompt|-> [--provider X] [--model Y] [--skill list] [--workspace <name>] [--thinking N] [--show-thinking] [--usage] [--cost] [--sandbox docker:<image>]\n One-shot non-interactive call. Pass "-" as the prompt to read from stdin.\n --workspace stitches AGENTS.md/SOUL.md/TOOLS.md into the system prompt (combines with --skill).\n --usage prints normalized {inputTokens, outputTokens, ...} to stderr after the response.\n --cost adds a cost line on stderr when config.rates has a card for the active provider/model.\n --sandbox docker:<image> wraps the subprocess provider (claude-cli) in a Docker container; --sandbox-network defaults to none.',
|
|
55
|
+
doctor: 'Usage: lazyclaw doctor\n Validates configuration and registered providers. Exits 0 only when no issues.',
|
|
56
|
+
status: 'Usage: lazyclaw status\n Provider, model, and masked API key. Never prints the raw key.',
|
|
57
|
+
onboard: 'Usage: lazyclaw onboard [--non-interactive] [--provider X] [--model Y] [--api-key Z]\n --model accepts the unified "provider/model" string (e.g. anthropic/claude-opus-4-7).',
|
|
58
|
+
sessions: 'Usage: lazyclaw sessions <list [--filter <substr>] [--limit <N>]|show <id>|clear <id>|export <id> [--format md|json|text]|search <query> [--regex]>\n list — recent sessions by mtime; --filter caps to ids containing substring (case-insensitive); --limit caps result count.\n export — render in chosen format (md default for human sharing, json for tooling, text for paste).\n search — case-insensitive substring (or --regex pattern) match across all session content; returns first excerpt + match count per matching session.',
|
|
59
|
+
skills: 'Usage: lazyclaw skills <list [--filter <substr>] [--limit <N>]|show <name>|install <user/repo[@ref][:path]> [--prefix <p>] [--force] | install <name> [--from <path> | --from-url <https://...>]|remove <name>|search <query> [--regex]>\n list — installed skills; --filter caps to names containing substring (case-insensitive); --limit caps result count.\n install <user>/<repo>[@<ref>][:<subpath>] — fetch a GitHub tarball, install every .md under skills/ (or the explicit subpath, or repo root). Default ref is `main`.\n --prefix prepends a name prefix so a multi-skill repo doesn\'t collide with locally-managed skills. --force overwrites existing names.\n install <name> --from <path> | --from-url <https://...> — single-file install. --from-url is HTTPS-only with a 1 MiB cap.\n search — case-insensitive substring (or --regex) match across all skill markdown bodies; returns first excerpt + match count per skill.',
|
|
60
|
+
providers: 'Usage: lazyclaw providers <list [--filter <substr>] [--limit <N>] | info <name> | test <name> [--model X] [--prompt T] | test [--all] [--prompt T] | add <name> --base-url <url> [--api-key <k>] [--default-model <id>] [--no-probe] | remove <name> | models <name> [--filter <substr>]>\n list — registered providers (--filter case-insensitive name substring; --limit caps post-filter count).\n info — static metadata: requiresApiKey, defaultModel, suggestedModels, endpoint.\n test — send a 1-token "ping" through the provider and report ok/error + duration.\n Useful after configuring an API key to verify it works before relying on it.\n No name OR --all: tests every registered provider in parallel; exits 0 only when ALL pass.\n add — register a custom OpenAI-compatible endpoint (NIM / OpenRouter / Together / Groq / vLLM / LM Studio / …).\n Probes /v1/models on success unless --no-probe is set; persists to cfg.customProviders[].\n remove — drop a custom provider entry from cfg.customProviders[].\n models — fetch + print the live model catalogue from <provider>/v1/models (works for openai / ollama / custom).',
|
|
61
|
+
daemon: 'Usage: lazyclaw daemon [--port <N>] [--once] [--auth-token <token>] [--allow-origin <origin>] [--rate-limit <N>] [--response-cache] [--log <level>] [--shutdown-timeout-ms <N>] [--cost-cap-<currency> <N> ...] [--workflow-state-dir <dir>]\n Always binds 127.0.0.1. --port 0 picks a random port and prints the URL.\n --auth-token also reads $LAZYCLAW_AUTH_TOKEN; --allow-origin also reads $LAZYCLAW_ALLOW_ORIGINS.\n --rate-limit <N> caps each remote IP at N requests / 60 s.\n --response-cache enables process-scoped memoization; per-request opt-in via body.cache.\n --log <debug|info|warn|error> emits JSON-line access logs on stderr (also reads $LAZYCLAW_LOG_LEVEL).\n --shutdown-timeout-ms <N> caps graceful drain on SIGINT/SIGTERM (default 10000). Second signal forces immediate exit.\n --cost-cap-usd 100 (or any currency code in lowercase) rejects POST /agent + /chat with 402 once cumulative cost reaches the cap.\n --workflow-state-dir <dir> backs GET /workflows + GET /workflows/<id> (default .workflow-state, also reads $LAZYCLAW_WORKFLOW_STATE_DIR).',
|
|
62
|
+
version: 'Usage: lazyclaw version\n Aliases: --version, -v.',
|
|
63
|
+
completion: 'Usage: lazyclaw completion <bash|zsh>\n bash: eval "$(lazyclaw completion bash)"\n zsh: lazyclaw completion zsh > "${fpath[1]}/_lazyclaw"',
|
|
64
|
+
export: 'Usage: lazyclaw export [--include-secrets] [--include-sessions] > bundle.json\n --include-secrets keeps the raw api-key in the bundle (default redacts it).\n --include-sessions adds full turn content (default keeps metadata only).',
|
|
65
|
+
import: 'Usage: lazyclaw import [--from <path>] [--overwrite-skills] [--no-overwrite-config] [--import-sessions]\n Reads JSON from stdin (or --from <path>). Sessions are NEVER overwritten.\n Redacted api-keys (***REDACTED***) are dropped, never written.',
|
|
66
|
+
rates: 'Usage: lazyclaw rates <list [--filter <substr>] [--limit <N>] | set <provider/model> --input <N> --output <N> [--cache-read <N>] [--cache-create <N>] [--currency USD] | delete <key> | shape | validate | copy <src> <dst> [--force]>\n Rates are per million tokens. costFromUsage uses cfg.rates to compute the cost block in /usage and body.cost.\n `list` accepts --filter (case-insensitive key substring) and --limit (post-filter cap), same shape sessions/skills/workflows lists use.\n `shape` prints the reference template (zero-filled) you can copy into config.\n `validate` checks the cfg.rates shape: required fields, non-negative numbers, known providers (warn-only).\n `copy` clones an existing card to a new key (use when a new model launches at the same price as an old one).',
|
|
67
|
+
sandbox: 'Usage: lazyclaw sandbox <list|test|add|use> [args]\n list show 6 backends (local, docker, ssh, singularity, modal, daytona)\n test <kind> run echo through the backend (or argv-shape check for remote)\n add <name> --kind <kind> [--image|--host|--user|--workspace|--app|--confiner ...]\n use <profile> set the profile as cfg.sandbox.default',
|
|
68
|
+
auth: 'Usage: lazyclaw auth <list <provider> | add <provider> <key> [--label <name>] | remove <provider> <label> | use <provider> <label> | rotate <provider>>\n Multiple keys per provider for rate-limit rotation. The active label is sent on every chat / agent call.\n `rotate` advances the cursor to the next label; pair with a 429 hook for auto-failover.',
|
|
69
|
+
pairing: 'Usage: lazyclaw pairing <list | add <id> [--label <name>] | remove <id>>\n Sender allowlist for the messaging surface. Inbound senders not on this list are rejected.\n Sender ids are opaque per-channel: Slack member id, Discord user id, phone number for SMS, etc.',
|
|
70
|
+
nodes: 'Usage: lazyclaw nodes <list | register <id> [--platform macos|ios|android|web|cli] [--label <name>] | remove <id>>\n Companion device registration table. CLI only — the actual mobile / menu-bar apps are out of scope here.\n Platform is free-form lower-case; future surfaces (iOS / Android nodes) authenticate against the daemon using these ids.',
|
|
71
|
+
message: 'Usage: lazyclaw message <list | add <name> <webhook-url> [--kind slack|discord|generic] | remove <name> | send <name> <text>>\n Outbound webhook messaging — Slack / Discord Incoming Webhooks. Auto-detects kind from the URL pattern.\n send accepts a literal string, or `-` to read the body from stdin.',
|
|
72
|
+
workspace: 'Usage: lazyclaw workspace <list | init <name> | show <name> [<file>] | remove <name> | path <name>>\n Workspace = a directory under <configDir>/workspaces/<name>/ containing AGENTS.md, SOUL.md, TOOLS.md.\n When `chat` or `agent` is invoked with --workspace <name>, the three files are stitched into a single system prompt at the head of the conversation. Missing files are skipped silently.\n init scaffolds the three files with short stubs you replace.\n show prints the composed prompt; show <name> AGENTS.md (etc) prints just one file.',
|
|
73
|
+
browse: 'Usage: lazyclaw browse <url> [--max-bytes <N>] [--timeout-ms <N>] [--user-agent <ua>] [--meta]\n Fetches the URL and emits Markdown on stdout. Pipes cleanly into `agent`:\n lazyclaw browse https://example.com/docs | lazyclaw agent -\n Strips <script>/<style>/<svg>/comments, prefers <main>/<article>, falls back to <body>.\n --max-bytes caps the body read (default 2 MB) so a misconfigured upstream can\'t OOM the process.\n --meta prints { url, title, bytes, truncated } as JSON to stderr alongside the markdown on stdout.',
|
|
74
|
+
cron: 'Usage: lazyclaw cron <list | add <name> "<cron-spec>" -- <cmd> ... | remove <name> | show <name> | sync | run <name>>\n Schedule recurring agent runs. macOS uses launchd (~/Library/LaunchAgents/com.lazyclaw.<name>.plist); Linux / WSL uses the user crontab.\n Cron spec is the standard 5-field form (minute hour dom month dow). Supports *, range a-b, list a,b,c, step */N.\n add: pass the command after `--`. Typical use:\n lazyclaw cron add daily-summary "0 9 * * 1-5" -- lazyclaw agent "Summarise today\'s TODOs"\n list / show: read from cfg.cron[name] (config is the source of truth).\n sync: re-installs every job in cfg.cron into the system scheduler — handy after a reinstall.\n run: one-shot in-process execution of the named job; the OS scheduler does the same thing on its trigger.\n Logs: ~/.lazyclaw/logs/cron-<name>.{out,err}.log (macOS launchd path).',
|
|
75
|
+
setup: 'Usage: lazyclaw setup [--skip-test] [--only <steps>] [--skip <steps>]\n --only/--skip take a comma list of: provider verify channel workspace skill webhook orchestrator (e.g. --only channel re-runs just that step).\n Hermes-style phased first-run wizard — get one clean chat working first,\n then optionally add the rest. Walks through:\n 1. Provider + model + api-key (delegates to onboard --pick; ≥64k-context tip)\n 2. Verify the provider responds (1-token ping; --skip-test bypasses)\n 3. Optional channel / gateway (Slack / Telegram / Matrix / HTTP built in;\n Discord / Email / Signal / Voice / WhatsApp via plugin packages)\n 4. Optional workspace init (AGENTS.md / SOUL.md / TOOLS.md)\n 5. Optional skill bundle install from GitHub\n 6. Optional outbound webhook (Slack / Discord)\n 7. Optional multi-agent orchestration (planner + workers)\n Each optional step takes Enter or "skip" to bypass. Re-runnable safely.\n Also fires automatically on first run when `lazyclaw` is invoked with no config.',
|
|
76
|
+
dashboard: 'Usage: lazyclaw dashboard [--port <N>] [--no-open]\n Launches the lazyclaw-only web UI on http://127.0.0.1:<port> (default 19600) and opens it in the default browser.\n Wraps `lazyclaw daemon` + a static HTML; no Python / lazyclaude dashboard required.\n See web/dashboard.html for the current tab set (v5: Chat / Sessions / Workflows / Skills / Providers / Rates / Metrics / Doctor / Config / Status / Agents / Teams / Tasks / Trainer / Recall / Sandbox / Channels).\n --no-open keeps the browser closed (handy for SSH / headless / dev). The bound URL is always printed to stdout.',
|
|
77
|
+
orchestrator: 'Usage: lazyclaw orchestrator <status | set-planner <provider[:model]> | workers add <spec> | workers remove <spec> | workers set <spec,spec,...> | workers clear | set-max-subtasks <N> | clear>\n Read/write cfg.orchestrator without editing config.json by hand.\n status — print {planner, workers, maxSubtasks} as JSON; lists registered providers for reference.\n set-planner — replace the planner spec ("provider" or "provider:model"). "orchestrator" itself is rejected (self-recursion).\n workers add — append a worker (idempotent — duplicates skipped).\n workers remove — drop a worker by exact match. Idempotent.\n workers set — replace the whole list (comma-separated specs).\n workers clear — empty the workers list.\n set-max-subtasks <N> — cap subtasks per request, clamped 1..10 (default 5).\n clear — delete the cfg.orchestrator block entirely.\n Pair with: `lazyclaw config set provider orchestrator` to route chats through it.',
|
|
78
|
+
};
|
package/commands/setup.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { dispatchSlash as _dispatchSlash, parseSlashLine as _parseSlashLine } fr
|
|
|
25
25
|
import { SLASH_COMMANDS } from '../tui/slash_commands.mjs';
|
|
26
26
|
import { runChannelStep, runWebhookStep, runOrchestratorStep, runContextStep } from './setup_channels.mjs';
|
|
27
27
|
import { splashPropsForSetup, renderSplashToString } from '../tui/splash_props.mjs';
|
|
28
|
+
import { HELP_SUMMARIES, HELP_DETAILS } from './help_text.mjs';
|
|
28
29
|
|
|
29
30
|
function applyOnboardConfig(currentCfg, flags) {
|
|
30
31
|
// Honors the OpenClaw-style unified provider/model string ("anthropic/claude-opus-4-7")
|
|
@@ -97,79 +98,6 @@ export async function cmdOnboard(flags) {
|
|
|
97
98
|
|
|
98
99
|
|
|
99
100
|
|
|
100
|
-
// One-line summaries used by `lazyclaw help`. Format keeps it scan-friendly
|
|
101
|
-
// in a 80-column terminal: subcommand padded to 12 chars, then the summary.
|
|
102
|
-
const HELP_SUMMARIES = {
|
|
103
|
-
run: 'Execute a workflow file (run <session-id> <workflow.mjs>)',
|
|
104
|
-
resume: 'Resume a workflow from its last persisted checkpoint',
|
|
105
|
-
config: 'Manage local config (get|set|list|delete <key>)',
|
|
106
|
-
chat: 'Interactive REPL with the configured provider',
|
|
107
|
-
agent: 'One-shot prompt: streams a single response, exits',
|
|
108
|
-
doctor: 'Print diagnostic JSON; exits non-zero on issues',
|
|
109
|
-
status: 'Print current provider/model/masked key as JSON',
|
|
110
|
-
onboard: 'Guided setup (use --non-interactive for scripts)',
|
|
111
|
-
sessions: 'Persistent chat sessions (list|show|clear|export)',
|
|
112
|
-
skills: 'Markdown skill bundles (list|show|install|remove)',
|
|
113
|
-
providers: 'Inspect / register providers (list|info|test|add|remove|models)',
|
|
114
|
-
daemon: 'Run the local HTTP gateway (--port, --auth-token, --allow-origin)',
|
|
115
|
-
version: 'Print VERSION + node + platform as JSON',
|
|
116
|
-
completion: 'Emit shell completion script (completion <bash|zsh>)',
|
|
117
|
-
export: 'Dump config + skills (+ optional sessions) as a JSON bundle',
|
|
118
|
-
import: 'Apply a JSON bundle from stdin or --from <path>',
|
|
119
|
-
rates: 'Manage cost rate-cards in config (rates list|set <provider/model>|delete|shape)',
|
|
120
|
-
auth: 'Multiple keys per provider (auth list|add|remove|use|rotate <provider>)',
|
|
121
|
-
pairing: 'Sender allowlist for the messaging surface (pairing list|add|remove <id>)',
|
|
122
|
-
nodes: 'Companion device registration (nodes list|register|remove <id>)',
|
|
123
|
-
message: 'Outbound webhook messaging (message list|add|remove|send <name>)',
|
|
124
|
-
workspace: 'AGENTS.md / SOUL.md / TOOLS.md system-prompt convention (workspace list|init|show|remove|path)',
|
|
125
|
-
browse: 'Fetch a URL and emit Markdown on stdout (browse <url> [--max-bytes <N>])',
|
|
126
|
-
cron: 'Schedule recurring agent runs via launchd / crontab (cron list|add|remove|show|sync|run)',
|
|
127
|
-
setup: 'Hermes-style phased first-run wizard (provider + verify chat + channel + workspace + skill + webhook)',
|
|
128
|
-
dashboard: 'Launch the lazyclaw-only web UI (lighter than the full lazyclaude dashboard)',
|
|
129
|
-
inspect: 'Print persisted workflow state without executing',
|
|
130
|
-
clear: 'Delete a persisted workflow state file (idempotent)',
|
|
131
|
-
validate: 'Static-check a workflow file: shape, deps, cycles, parallelism',
|
|
132
|
-
graph: 'Emit workflow DAG as Mermaid syntax (paste-ready for docs)',
|
|
133
|
-
orchestrator: 'Multi-agent dispatch — planner decomposes, workers run, planner synthesises',
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
// Detailed usage per subcommand for `lazyclaw help <name>`. Kept as flat
|
|
137
|
-
// strings so the help output is identical in every terminal.
|
|
138
|
-
const HELP_DETAILS = {
|
|
139
|
-
run: 'Usage: lazyclaw run <session-id> <workflow.mjs> [--parallel | --parallel-persistent] [--concurrency <N>]\n Default: runPersistent — sequential, persists state, resumable via `lazyclaw resume`.\n --parallel: runParallel — topological-level DAG, in-memory only, NOT resumable.\n --parallel-persistent: runPersistentDag — DAG + checkpoint + resume.\n --concurrency <N>: cap in-flight nodes within a level (DAG modes only). 0/missing → unbounded.\n Workflow file exports `nodes`; deps: string[] declares dependencies for both DAG modes.',
|
|
140
|
-
resume: 'Usage: lazyclaw resume <session-id> <workflow.mjs> [--parallel-persistent] [--concurrency <N>]\n Re-enters a previously persisted run; succeeds nodes are skipped.\n Pass --parallel-persistent to resume a DAG run (must match the original run\'s mode).\n --concurrency <N>: cap in-flight nodes per level (DAG mode only).',
|
|
141
|
-
inspect: 'Usage: lazyclaw inspect [<session-id>] [--dir <state-dir>] [--status done|resumable|failed|running] [--summary] [--filter <substr>] [--limit <N>] [--node <node-id>] [--slowest <N>] [--critical-path <workflow.mjs>] [--aggregate]\n With no session-id: list every persisted session in the state dir, sorted by recency.\n --aggregate (list mode): per-node stats across all sessions (count, success/failed/pending/running, min/max/avg/total duration).\n --status filters the listing to a single lifecycle bucket.\n --filter / --limit refine list-mode further (case-insensitive sessionId substring + post-filter cap).\n --summary trims per-node detail in single-session mode (matches list-mode shape).\n --node <id>: print just that node\'s state. Exit 0 success/pending/running, 1 failed, 2 no such node.\n --slowest <N>: top N nodes by durationMs (descending, ties broken by id).\n --critical-path <workflow.mjs>: longest-weighted-path analysis using each node\'s recorded durationMs (bottleneck finder).\n With a session-id (no per-node flag): print full state. Exit code: 0=resumable, 1=fully done, 2=no state, 3=terminal failure.',
|
|
142
|
-
clear: 'Usage: lazyclaw clear <session-id> [--dir <state-dir>]\n Delete the state file for <session-id>. Idempotent — exits 0 whether the file existed or not.\n Refuses sessionIds that resolve outside <state-dir>. Mirrors DELETE /workflows/<id> on the daemon.',
|
|
143
|
-
validate: 'Usage: lazyclaw validate <workflow.mjs>\n Static check: load + shape + dep + cycle + parallelism estimate.\n Exit 0 valid · 1 hard failure (issues populated) · 2 file/import error.',
|
|
144
|
-
graph: 'Usage: lazyclaw graph <workflow.mjs> [--lr] [--state <session-id>] [--dir <state-dir>]\n Emit the workflow DAG as Mermaid syntax (graph TD by default; --lr for left-right).\n --state overlays a persisted run\'s status (success ✓ / running ⏳ / failed ✗ / pending) with classDef styling.\n Output is paste-ready for GitHub markdown / Notion / Obsidian.',
|
|
145
|
-
config: 'Usage: lazyclaw config <get|set|list|delete|path|edit|validate> [key] [value]\n Local key-value config at $LAZYCLAW_CONFIG_DIR/config.json (default ~/.lazyclaw).\n `path` prints the file location; `edit` opens it in $EDITOR (or $LAZYCLAW_EDITOR / $VISUAL / vi) and validates JSON on save.\n `validate` checks the structural integrity of the whole config file (typed values, known providers, rate-card shape).',
|
|
146
|
-
chat: 'Usage: lazyclaw chat [--session <id>] [--skill name1,name2] [--workspace <name>] [--pick] [--sandbox docker:<image>] [--sandbox-network <net>] [--sandbox-mount <m>] [--sandbox-env <e>]\n --session persists turns to <configDir>/sessions/<id>.jsonl across invocations.\n --skill composes named skills into a system message at the head of the conversation.\n --workspace stitches AGENTS.md/SOUL.md/TOOLS.md from <configDir>/workspaces/<name>/ into the system prompt.\n --pick opens an interactive provider/model picker before the prompt (also auto-fires on first run).\n --sandbox routes the underlying claude CLI through `docker run --rm -i --network <net> -v cwd:cwd ...` (default --network=none).',
|
|
147
|
-
agent: 'Usage: lazyclaw agent <prompt|-> [--provider X] [--model Y] [--skill list] [--workspace <name>] [--thinking N] [--show-thinking] [--usage] [--cost] [--sandbox docker:<image>]\n One-shot non-interactive call. Pass "-" as the prompt to read from stdin.\n --workspace stitches AGENTS.md/SOUL.md/TOOLS.md into the system prompt (combines with --skill).\n --usage prints normalized {inputTokens, outputTokens, ...} to stderr after the response.\n --cost adds a cost line on stderr when config.rates has a card for the active provider/model.\n --sandbox docker:<image> wraps the subprocess provider (claude-cli) in a Docker container; --sandbox-network defaults to none.',
|
|
148
|
-
doctor: 'Usage: lazyclaw doctor\n Validates configuration and registered providers. Exits 0 only when no issues.',
|
|
149
|
-
status: 'Usage: lazyclaw status\n Provider, model, and masked API key. Never prints the raw key.',
|
|
150
|
-
onboard: 'Usage: lazyclaw onboard [--non-interactive] [--provider X] [--model Y] [--api-key Z]\n --model accepts the unified "provider/model" string (e.g. anthropic/claude-opus-4-7).',
|
|
151
|
-
sessions: 'Usage: lazyclaw sessions <list [--filter <substr>] [--limit <N>]|show <id>|clear <id>|export <id> [--format md|json|text]|search <query> [--regex]>\n list — recent sessions by mtime; --filter caps to ids containing substring (case-insensitive); --limit caps result count.\n export — render in chosen format (md default for human sharing, json for tooling, text for paste).\n search — case-insensitive substring (or --regex pattern) match across all session content; returns first excerpt + match count per matching session.',
|
|
152
|
-
skills: 'Usage: lazyclaw skills <list [--filter <substr>] [--limit <N>]|show <name>|install <user/repo[@ref][:path]> [--prefix <p>] [--force] | install <name> [--from <path> | --from-url <https://...>]|remove <name>|search <query> [--regex]>\n list — installed skills; --filter caps to names containing substring (case-insensitive); --limit caps result count.\n install <user>/<repo>[@<ref>][:<subpath>] — fetch a GitHub tarball, install every .md under skills/ (or the explicit subpath, or repo root). Default ref is `main`.\n --prefix prepends a name prefix so a multi-skill repo doesn\'t collide with locally-managed skills. --force overwrites existing names.\n install <name> --from <path> | --from-url <https://...> — single-file install. --from-url is HTTPS-only with a 1 MiB cap.\n search — case-insensitive substring (or --regex) match across all skill markdown bodies; returns first excerpt + match count per skill.',
|
|
153
|
-
providers: 'Usage: lazyclaw providers <list [--filter <substr>] [--limit <N>] | info <name> | test <name> [--model X] [--prompt T] | test [--all] [--prompt T] | add <name> --base-url <url> [--api-key <k>] [--default-model <id>] [--no-probe] | remove <name> | models <name> [--filter <substr>]>\n list — registered providers (--filter case-insensitive name substring; --limit caps post-filter count).\n info — static metadata: requiresApiKey, defaultModel, suggestedModels, endpoint.\n test — send a 1-token "ping" through the provider and report ok/error + duration.\n Useful after configuring an API key to verify it works before relying on it.\n No name OR --all: tests every registered provider in parallel; exits 0 only when ALL pass.\n add — register a custom OpenAI-compatible endpoint (NIM / OpenRouter / Together / Groq / vLLM / LM Studio / …).\n Probes /v1/models on success unless --no-probe is set; persists to cfg.customProviders[].\n remove — drop a custom provider entry from cfg.customProviders[].\n models — fetch + print the live model catalogue from <provider>/v1/models (works for openai / ollama / custom).',
|
|
154
|
-
daemon: 'Usage: lazyclaw daemon [--port <N>] [--once] [--auth-token <token>] [--allow-origin <origin>] [--rate-limit <N>] [--response-cache] [--log <level>] [--shutdown-timeout-ms <N>] [--cost-cap-<currency> <N> ...] [--workflow-state-dir <dir>]\n Always binds 127.0.0.1. --port 0 picks a random port and prints the URL.\n --auth-token also reads $LAZYCLAW_AUTH_TOKEN; --allow-origin also reads $LAZYCLAW_ALLOW_ORIGINS.\n --rate-limit <N> caps each remote IP at N requests / 60 s.\n --response-cache enables process-scoped memoization; per-request opt-in via body.cache.\n --log <debug|info|warn|error> emits JSON-line access logs on stderr (also reads $LAZYCLAW_LOG_LEVEL).\n --shutdown-timeout-ms <N> caps graceful drain on SIGINT/SIGTERM (default 10000). Second signal forces immediate exit.\n --cost-cap-usd 100 (or any currency code in lowercase) rejects POST /agent + /chat with 402 once cumulative cost reaches the cap.\n --workflow-state-dir <dir> backs GET /workflows + GET /workflows/<id> (default .workflow-state, also reads $LAZYCLAW_WORKFLOW_STATE_DIR).',
|
|
155
|
-
version: 'Usage: lazyclaw version\n Aliases: --version, -v.',
|
|
156
|
-
completion: 'Usage: lazyclaw completion <bash|zsh>\n bash: eval "$(lazyclaw completion bash)"\n zsh: lazyclaw completion zsh > "${fpath[1]}/_lazyclaw"',
|
|
157
|
-
export: 'Usage: lazyclaw export [--include-secrets] [--include-sessions] > bundle.json\n --include-secrets keeps the raw api-key in the bundle (default redacts it).\n --include-sessions adds full turn content (default keeps metadata only).',
|
|
158
|
-
import: 'Usage: lazyclaw import [--from <path>] [--overwrite-skills] [--no-overwrite-config] [--import-sessions]\n Reads JSON from stdin (or --from <path>). Sessions are NEVER overwritten.\n Redacted api-keys (***REDACTED***) are dropped, never written.',
|
|
159
|
-
rates: 'Usage: lazyclaw rates <list [--filter <substr>] [--limit <N>] | set <provider/model> --input <N> --output <N> [--cache-read <N>] [--cache-create <N>] [--currency USD] | delete <key> | shape | validate | copy <src> <dst> [--force]>\n Rates are per million tokens. costFromUsage uses cfg.rates to compute the cost block in /usage and body.cost.\n `list` accepts --filter (case-insensitive key substring) and --limit (post-filter cap), same shape sessions/skills/workflows lists use.\n `shape` prints the reference template (zero-filled) you can copy into config.\n `validate` checks the cfg.rates shape: required fields, non-negative numbers, known providers (warn-only).\n `copy` clones an existing card to a new key (use when a new model launches at the same price as an old one).',
|
|
160
|
-
sandbox: 'Usage: lazyclaw sandbox <list|test|add|use> [args]\n list show 6 backends (local, docker, ssh, singularity, modal, daytona)\n test <kind> run echo through the backend (or argv-shape check for remote)\n add <name> --kind <kind> [--image|--host|--user|--workspace|--app|--confiner ...]\n use <profile> set the profile as cfg.sandbox.default',
|
|
161
|
-
auth: 'Usage: lazyclaw auth <list <provider> | add <provider> <key> [--label <name>] | remove <provider> <label> | use <provider> <label> | rotate <provider>>\n Multiple keys per provider for rate-limit rotation. The active label is sent on every chat / agent call.\n `rotate` advances the cursor to the next label; pair with a 429 hook for auto-failover.',
|
|
162
|
-
pairing: 'Usage: lazyclaw pairing <list | add <id> [--label <name>] | remove <id>>\n Sender allowlist for the messaging surface. Inbound senders not on this list are rejected.\n Sender ids are opaque per-channel: Slack member id, Discord user id, phone number for SMS, etc.',
|
|
163
|
-
nodes: 'Usage: lazyclaw nodes <list | register <id> [--platform macos|ios|android|web|cli] [--label <name>] | remove <id>>\n Companion device registration table. CLI only — the actual mobile / menu-bar apps are out of scope here.\n Platform is free-form lower-case; future surfaces (iOS / Android nodes) authenticate against the daemon using these ids.',
|
|
164
|
-
message: 'Usage: lazyclaw message <list | add <name> <webhook-url> [--kind slack|discord|generic] | remove <name> | send <name> <text>>\n Outbound webhook messaging — Slack / Discord Incoming Webhooks. Auto-detects kind from the URL pattern.\n send accepts a literal string, or `-` to read the body from stdin.',
|
|
165
|
-
workspace: 'Usage: lazyclaw workspace <list | init <name> | show <name> [<file>] | remove <name> | path <name>>\n Workspace = a directory under <configDir>/workspaces/<name>/ containing AGENTS.md, SOUL.md, TOOLS.md.\n When `chat` or `agent` is invoked with --workspace <name>, the three files are stitched into a single system prompt at the head of the conversation. Missing files are skipped silently.\n init scaffolds the three files with short stubs you replace.\n show prints the composed prompt; show <name> AGENTS.md (etc) prints just one file.',
|
|
166
|
-
browse: 'Usage: lazyclaw browse <url> [--max-bytes <N>] [--timeout-ms <N>] [--user-agent <ua>] [--meta]\n Fetches the URL and emits Markdown on stdout. Pipes cleanly into `agent`:\n lazyclaw browse https://example.com/docs | lazyclaw agent -\n Strips <script>/<style>/<svg>/comments, prefers <main>/<article>, falls back to <body>.\n --max-bytes caps the body read (default 2 MB) so a misconfigured upstream can\'t OOM the process.\n --meta prints { url, title, bytes, truncated } as JSON to stderr alongside the markdown on stdout.',
|
|
167
|
-
cron: 'Usage: lazyclaw cron <list | add <name> "<cron-spec>" -- <cmd> ... | remove <name> | show <name> | sync | run <name>>\n Schedule recurring agent runs. macOS uses launchd (~/Library/LaunchAgents/com.lazyclaw.<name>.plist); Linux / WSL uses the user crontab.\n Cron spec is the standard 5-field form (minute hour dom month dow). Supports *, range a-b, list a,b,c, step */N.\n add: pass the command after `--`. Typical use:\n lazyclaw cron add daily-summary "0 9 * * 1-5" -- lazyclaw agent "Summarise today\'s TODOs"\n list / show: read from cfg.cron[name] (config is the source of truth).\n sync: re-installs every job in cfg.cron into the system scheduler — handy after a reinstall.\n run: one-shot in-process execution of the named job; the OS scheduler does the same thing on its trigger.\n Logs: ~/.lazyclaw/logs/cron-<name>.{out,err}.log (macOS launchd path).',
|
|
168
|
-
setup: 'Usage: lazyclaw setup [--skip-test] [--only <steps>] [--skip <steps>]\n --only/--skip take a comma list of: provider verify channel workspace skill webhook orchestrator (e.g. --only channel re-runs just that step).\n Hermes-style phased first-run wizard — get one clean chat working first,\n then optionally add the rest. Walks through:\n 1. Provider + model + api-key (delegates to onboard --pick; ≥64k-context tip)\n 2. Verify the provider responds (1-token ping; --skip-test bypasses)\n 3. Optional channel / gateway (Slack / Telegram / Matrix / HTTP built in;\n Discord / Email / Signal / Voice / WhatsApp via plugin packages)\n 4. Optional workspace init (AGENTS.md / SOUL.md / TOOLS.md)\n 5. Optional skill bundle install from GitHub\n 6. Optional outbound webhook (Slack / Discord)\n 7. Optional multi-agent orchestration (planner + workers)\n Each optional step takes Enter or "skip" to bypass. Re-runnable safely.\n Also fires automatically on first run when `lazyclaw` is invoked with no config.',
|
|
169
|
-
dashboard: 'Usage: lazyclaw dashboard [--port <N>] [--no-open]\n Launches the lazyclaw-only web UI on http://127.0.0.1:<port> (default 19600) and opens it in the default browser.\n Wraps `lazyclaw daemon` + a static HTML; no Python / lazyclaude dashboard required.\n See web/dashboard.html for the current tab set (v5: Chat / Sessions / Workflows / Skills / Providers / Rates / Metrics / Doctor / Config / Status / Agents / Teams / Tasks / Trainer / Recall / Sandbox / Channels).\n --no-open keeps the browser closed (handy for SSH / headless / dev). The bound URL is always printed to stdout.',
|
|
170
|
-
orchestrator: 'Usage: lazyclaw orchestrator <status | set-planner <provider[:model]> | workers add <spec> | workers remove <spec> | workers set <spec,spec,...> | workers clear | set-max-subtasks <N> | clear>\n Read/write cfg.orchestrator without editing config.json by hand.\n status — print {planner, workers, maxSubtasks} as JSON; lists registered providers for reference.\n set-planner — replace the planner spec ("provider" or "provider:model"). "orchestrator" itself is rejected (self-recursion).\n workers add — append a worker (idempotent — duplicates skipped).\n workers remove — drop a worker by exact match. Idempotent.\n workers set — replace the whole list (comma-separated specs).\n workers clear — empty the workers list.\n set-max-subtasks <N> — cap subtasks per request, clamped 1..10 (default 5).\n clear — delete the cfg.orchestrator block entirely.\n Pair with: `lazyclaw config set provider orchestrator` to route chats through it.',
|
|
171
|
-
};
|
|
172
|
-
|
|
173
101
|
export function cmdHelp(name) {
|
|
174
102
|
if (!name) {
|
|
175
103
|
process.stdout.write('lazyclaw — terminal AI assistant + workflow engine\n\n');
|
package/daemon/lib/cost.mjs
CHANGED
|
@@ -26,6 +26,11 @@ export function accumulateMetricsFromCost(metrics, usage, cost) {
|
|
|
26
26
|
if (usage) {
|
|
27
27
|
if (Number.isFinite(usage.inputTokens)) metrics.tokensTotal.inputTokens += usage.inputTokens;
|
|
28
28
|
if (Number.isFinite(usage.outputTokens)) metrics.tokensTotal.outputTokens += usage.outputTokens;
|
|
29
|
+
// inputTokens is reported NET of cache by the providers, so the cache
|
|
30
|
+
// buckets would otherwise go uncounted. Track them separately to keep the
|
|
31
|
+
// token totals complete (this is metrics-only — cost math is unchanged).
|
|
32
|
+
if (Number.isFinite(usage.cacheReadInputTokens)) metrics.tokensTotal.cacheReadInputTokens += usage.cacheReadInputTokens;
|
|
33
|
+
if (Number.isFinite(usage.cacheCreationInputTokens)) metrics.tokensTotal.cacheCreationInputTokens += usage.cacheCreationInputTokens;
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
36
|
|
package/daemon/route_table.mjs
CHANGED
|
@@ -28,6 +28,7 @@ export const ROUTES = [
|
|
|
28
28
|
{ m: (c) => c.route === 'GET /dashboard.css', h: meta.dashboardCss },
|
|
29
29
|
{ m: (c) => c.route === 'GET /dashboard.js', h: meta.dashboardJs },
|
|
30
30
|
{ m: (c) => c.req.method === 'GET' && /^\/avatars\/\d{2}\.png$/.test(c.path || ''), h: meta.avatar },
|
|
31
|
+
{ m: (c) => c.req.method === 'GET' && /^\/agent-avatars\/[A-Za-z0-9_-]+\.[a-z]+$/.test(c.path || ''), h: meta.agentAvatar },
|
|
31
32
|
{ m: (c) => c.route === 'GET /version', h: meta.version },
|
|
32
33
|
{ m: (c) => c.route === 'POST /exec/request', h: conversation.execRequest },
|
|
33
34
|
{ m: (c) => c.route === 'GET /health' || c.route === 'GET /healthz', h: meta.health },
|
package/daemon/routes/meta.mjs
CHANGED
|
@@ -65,6 +65,35 @@ export async function dashboardJs(c) {
|
|
|
65
65
|
return serveWebFile(c, 'dashboard.js', 'text/javascript; charset=utf-8');
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// Serve a custom agent character image (web app: <img src="/agent-avatars/NN">).
|
|
69
|
+
// These are user-supplied photos copied under <configDir>/agent-avatars/ by
|
|
70
|
+
// `lazyclaw agent set-avatar`. Served from the config dir (NOT the package web/
|
|
71
|
+
// dir) since they're per-install user data. The filename is constrained to
|
|
72
|
+
// `<word>.<ext>` so no path traversal escapes the agent-avatars directory.
|
|
73
|
+
const _AGENT_AVATAR_CT = {
|
|
74
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
75
|
+
'.gif': 'image/gif', '.webp': 'image/webp',
|
|
76
|
+
};
|
|
77
|
+
export async function agentAvatar(c) {
|
|
78
|
+
const { res } = c;
|
|
79
|
+
const m = /^\/agent-avatars\/([A-Za-z0-9_-]+\.[a-z]+)$/.exec(c.path || '');
|
|
80
|
+
const file = m && m[1];
|
|
81
|
+
const ct = file && _AGENT_AVATAR_CT[nodePath.extname(file).toLowerCase()];
|
|
82
|
+
const base = c.gwConfigDir;
|
|
83
|
+
if (!file || file.includes('..') || !ct || !base) {
|
|
84
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
85
|
+
return res.end('not found\n');
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const body = _readAssetCached(nodePath.join(base, 'agent-avatars', file));
|
|
89
|
+
res.writeHead(200, { 'content-type': ct, 'cache-control': 'no-cache' });
|
|
90
|
+
return res.end(body);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
93
|
+
return res.end(`not found: ${file} (${e?.message || e})\n`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
68
97
|
export async function dashboard(c) {
|
|
69
98
|
const { res } = c;
|
|
70
99
|
// Serve the lazyclaw-only web dashboard (a single static
|
package/daemon.mjs
CHANGED
|
@@ -102,7 +102,7 @@ export function makeHandler(ctx) {
|
|
|
102
102
|
// EUR-priced regional contract) doesn't silently sum mismatched
|
|
103
103
|
// numbers. Tokens are unit-free so we keep them in a single counter.
|
|
104
104
|
costsByCurrency: /** @type {Record<string, number>} */({}),
|
|
105
|
-
tokensTotal: { inputTokens: 0, outputTokens: 0 },
|
|
105
|
+
tokensTotal: { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, cacheCreationInputTokens: 0 },
|
|
106
106
|
};
|
|
107
107
|
// Device gateway (Phase 27). The ChallengeRegistry is a per-process
|
|
108
108
|
// singleton: a challenge minted by one request is consumed by a later
|
package/lib/args.mjs
CHANGED
|
@@ -277,6 +277,6 @@ _lazyclaw "$@"
|
|
|
277
277
|
// AGENT_REG_SUBS to decide whether a bare `agent <sub>` routes to the agent
|
|
278
278
|
// registry vs a one-shot agent run. TEAM_SUBS/TASK_SUBS mirror their command's
|
|
279
279
|
// subcommands for the same disambiguation pattern.
|
|
280
|
-
export const AGENT_REG_SUBS = new Set(['add', 'list', 'show', 'edit', 'remove', 'rm', 'delete', 'memory', 'reflect', 'skill-synth']);
|
|
280
|
+
export const AGENT_REG_SUBS = new Set(['add', 'list', 'show', 'edit', 'remove', 'rm', 'delete', 'memory', 'reflect', 'skill-synth', 'set-avatar']);
|
|
281
281
|
export const TEAM_SUBS = new Set(['add', 'list', 'show', 'edit', 'remove', 'rm', 'delete']);
|
|
282
282
|
export const TASK_SUBS = new Set(['start', 'tick', 'list', 'show', 'abandon', 'done', 'transcript', 'remove', 'rm', 'delete']);
|
package/mas/agent_turn.mjs
CHANGED
|
@@ -204,6 +204,13 @@ export async function runAgentTurn({
|
|
|
204
204
|
iterations++;
|
|
205
205
|
const resp = await adapter.callOnce({
|
|
206
206
|
messages, tools, model: agent.model, apiKey, system: systemPrompt,
|
|
207
|
+
// Forward-compat: thread an explicit per-agent output cap when the agent
|
|
208
|
+
// record carries one. No current config/setup path populates
|
|
209
|
+
// agent.maxTokens / agent.maxOutputTokens, so this is `undefined` today —
|
|
210
|
+
// each adapter then falls back to its own DEFAULT (anthropic/openai) or
|
|
211
|
+
// leaves the cap unset (gemini), keeping existing turns byte-stable. When
|
|
212
|
+
// a future config does set it, the cap reaches gemini (FIX D) too.
|
|
213
|
+
maxTokens: agent.maxTokens ?? agent.maxOutputTokens,
|
|
207
214
|
fetchImpl, baseUrl, signal, cache,
|
|
208
215
|
});
|
|
209
216
|
if (resp.text) lastText = resp.text;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lazyclaw",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.6.0",
|
|
4
4
|
"description": "Lazy, elegant terminal CLI for chatting with Claude / OpenAI / Gemini / Ollama, orchestrating multi-step LLM workflows, and running multi-agent Slack teams with cross-task memory. Banner-on-launch, slash-command ghost autocomplete, persistent sessions, local HTTP gateway.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/providers/claude_cli.mjs
CHANGED
|
@@ -179,7 +179,7 @@ export const claudeCliProvider = {
|
|
|
179
179
|
});
|
|
180
180
|
let yielded = false;
|
|
181
181
|
try {
|
|
182
|
-
for await (const chunk of session.send(String(last.content), { signal: opts.signal, onUsage: opts.onUsage })) {
|
|
182
|
+
for await (const chunk of session.send(String(last.content), { signal: opts.signal, onUsage: opts.onUsage, onTruncated: opts.onTruncated })) {
|
|
183
183
|
yielded = true;
|
|
184
184
|
yield chunk;
|
|
185
185
|
}
|
|
@@ -78,12 +78,15 @@ class ClaudeSession {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// Stream one turn. Yields text deltas until the matching `result` event.
|
|
81
|
-
async *send(userText, { signal, onUsage } = {}) {
|
|
81
|
+
async *send(userText, { signal, onUsage, onTruncated } = {}) {
|
|
82
82
|
if (this.busy) throw new Error('claude session busy — sends must be serialised per session');
|
|
83
83
|
if (!this.alive) throw new Error('claude session is not alive');
|
|
84
84
|
this.busy = true;
|
|
85
85
|
this._touch();
|
|
86
86
|
|
|
87
|
+
// Per-turn usage accumulated from `assistant` events (incl. cache fields);
|
|
88
|
+
// falls back to the result event's usage if no assistant event was seen.
|
|
89
|
+
const u = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0, saw: false };
|
|
87
90
|
const queue = [];
|
|
88
91
|
let done = false;
|
|
89
92
|
let error = null;
|
|
@@ -100,16 +103,30 @@ class ClaudeSession {
|
|
|
100
103
|
try { obj = JSON.parse(line); } catch { continue; }
|
|
101
104
|
const t = extractTextDelta(obj);
|
|
102
105
|
if (t) { queue.push(t); if (wake) wake(); }
|
|
106
|
+
if (obj.type === 'assistant' && obj.message && obj.message.usage) {
|
|
107
|
+
const mu = obj.message.usage;
|
|
108
|
+
u.input += mu.input_tokens || 0;
|
|
109
|
+
u.output += mu.output_tokens || 0;
|
|
110
|
+
u.cacheCreate += mu.cache_creation_input_tokens || 0;
|
|
111
|
+
u.cacheRead += mu.cache_read_input_tokens || 0;
|
|
112
|
+
u.saw = true;
|
|
113
|
+
}
|
|
103
114
|
if (obj.type === 'result') {
|
|
104
|
-
if (typeof onUsage === 'function'
|
|
115
|
+
if (typeof onUsage === 'function') {
|
|
116
|
+
const ru = obj.usage || {};
|
|
105
117
|
try {
|
|
106
118
|
onUsage({
|
|
107
|
-
inputTokens:
|
|
108
|
-
outputTokens:
|
|
119
|
+
inputTokens: u.saw ? u.input : (ru.input_tokens || 0),
|
|
120
|
+
outputTokens: u.saw ? u.output : (ru.output_tokens || 0),
|
|
121
|
+
cacheCreationInputTokens: u.saw ? u.cacheCreate : (ru.cache_creation_input_tokens || 0),
|
|
122
|
+
cacheReadInputTokens: u.saw ? u.cacheRead : (ru.cache_read_input_tokens || 0),
|
|
109
123
|
totalCostUsd: obj.total_cost_usd || 0,
|
|
110
124
|
});
|
|
111
125
|
} catch { /* never fail a turn on a usage callback */ }
|
|
112
126
|
}
|
|
127
|
+
if (typeof onTruncated === 'function' && obj.is_error && /max_turns/.test(String(obj.subtype || ''))) {
|
|
128
|
+
try { onTruncated('max_turns'); } catch { /* ditto */ }
|
|
129
|
+
}
|
|
113
130
|
done = true; if (wake) wake();
|
|
114
131
|
}
|
|
115
132
|
}
|
package/providers/codex_cli.mjs
CHANGED
|
@@ -140,15 +140,18 @@ function extractUsage(obj) {
|
|
|
140
140
|
// OpenAI Responses-API convention (codex follows it): input_tokens already
|
|
141
141
|
// INCLUDES cached_input_tokens, and output_tokens already INCLUDES
|
|
142
142
|
// reasoning_output_tokens — they are subset breakdowns, not additive. Summing
|
|
143
|
-
// them double-counts and trips the cost cap early. Report
|
|
144
|
-
//
|
|
145
|
-
|
|
143
|
+
// them double-counts and trips the cost cap early. Report NET (non-cached)
|
|
144
|
+
// input — total minus the cached subset — to match Anthropic's convention so
|
|
145
|
+
// rates.mjs's single cost formula doesn't bill the cached tokens at BOTH the
|
|
146
|
+
// input rate and the cache-read rate. Surface the cached subset separately.
|
|
147
|
+
const cacheRead = u.cached_input_tokens ?? 0;
|
|
148
|
+
const input = (u.input_tokens ?? 0) - cacheRead;
|
|
146
149
|
const output = u.output_tokens ?? 0;
|
|
147
|
-
if (!input && !output) return null;
|
|
150
|
+
if (!input && !output && !cacheRead) return null;
|
|
148
151
|
return {
|
|
149
152
|
inputTokens: input,
|
|
150
153
|
outputTokens: output,
|
|
151
|
-
cacheReadInputTokens:
|
|
154
|
+
cacheReadInputTokens: cacheRead,
|
|
152
155
|
totalCostUsd: 0,
|
|
153
156
|
};
|
|
154
157
|
}
|
package/providers/gemini.mjs
CHANGED
|
@@ -268,7 +268,10 @@ export const geminiProvider = {
|
|
|
268
268
|
if (lastUsage && typeof opts.onUsage === 'function') {
|
|
269
269
|
try {
|
|
270
270
|
opts.onUsage({
|
|
271
|
-
|
|
271
|
+
// promptTokenCount is cache-INCLUSIVE; report NET (minus cached) to
|
|
272
|
+
// match Anthropic's convention so rates.mjs doesn't bill the cached
|
|
273
|
+
// tokens at BOTH the input rate and the cache-read rate.
|
|
274
|
+
inputTokens: (lastUsage.promptTokenCount ?? 0) - (lastUsage.cachedContentTokenCount ?? 0),
|
|
272
275
|
outputTokens: lastUsage.candidatesTokenCount ?? null,
|
|
273
276
|
cacheReadInputTokens: lastUsage.cachedContentTokenCount ?? 0,
|
|
274
277
|
totalCostUsd: 0,
|
package/providers/gemini_cli.mjs
CHANGED
|
@@ -103,13 +103,17 @@ function extractUsage(stats) {
|
|
|
103
103
|
let input = 0, output = 0, cached = 0;
|
|
104
104
|
for (const m of Object.values(models)) {
|
|
105
105
|
const t = m?.tokens || {};
|
|
106
|
-
|
|
106
|
+
// cached is a subset of prompt → report NET (prompt minus cached) so it
|
|
107
|
+
// matches Anthropic's convention and rates.mjs doesn't bill the cached
|
|
108
|
+
// tokens at BOTH the input rate and the cache-read rate. Surface cached
|
|
109
|
+
// separately so it can still bill at the cache-read rate.
|
|
110
|
+
input += (t.prompt ?? t.input ?? 0) - (t.cached ?? 0);
|
|
107
111
|
// thoughts = reasoning tokens, billed as output and reported separately from
|
|
108
|
-
// candidates (additive).
|
|
112
|
+
// candidates (additive).
|
|
109
113
|
output += (t.candidates ?? t.output ?? 0) + (t.thoughts ?? 0);
|
|
110
114
|
cached += (t.cached ?? 0);
|
|
111
115
|
}
|
|
112
|
-
if (!input && !output) return null;
|
|
116
|
+
if (!input && !output && !cached) return null;
|
|
113
117
|
return { inputTokens: input, outputTokens: output, cacheReadInputTokens: cached, totalCostUsd: 0 };
|
|
114
118
|
}
|
|
115
119
|
|
|
@@ -217,6 +221,16 @@ export const geminiCliProvider = {
|
|
|
217
221
|
// so surface it instead of silently yielding nothing.
|
|
218
222
|
const text = typeof parsed.response === 'string' ? parsed.response : '';
|
|
219
223
|
if (!text && parsed.error) {
|
|
224
|
+
// A failed turn is still METERED: gemini-cli puts the partial token
|
|
225
|
+
// usage in the SAME JSON's `stats` even when the turn errored out. Bill
|
|
226
|
+
// it BEFORE throwing so a failed-but-metered turn still reaches the cost
|
|
227
|
+
// cap (the error path used to throw before extractUsage ever ran, so a
|
|
228
|
+
// failed turn leaked its tokens past accounting). Guarded so a usage
|
|
229
|
+
// callback can never mask the real error.
|
|
230
|
+
const failedUsage = extractUsage(parsed.stats);
|
|
231
|
+
if (failedUsage && typeof opts.onUsage === 'function') {
|
|
232
|
+
try { opts.onUsage(failedUsage); } catch (_) { /* never let usage hide the error */ }
|
|
233
|
+
}
|
|
220
234
|
const em = parsed.error?.message
|
|
221
235
|
|| (typeof parsed.error === 'string' ? parsed.error : JSON.stringify(parsed.error));
|
|
222
236
|
throw new CliExitError(exitInfo.code, exitInfo.signal, em);
|
|
@@ -78,6 +78,7 @@ export async function callOnce({
|
|
|
78
78
|
model,
|
|
79
79
|
apiKey,
|
|
80
80
|
system,
|
|
81
|
+
maxTokens,
|
|
81
82
|
baseUrl,
|
|
82
83
|
fetchImpl,
|
|
83
84
|
signal,
|
|
@@ -97,6 +98,13 @@ export async function callOnce({
|
|
|
97
98
|
if (system && String(system).trim()) {
|
|
98
99
|
body.system_instruction = { parts: [{ text: String(system) }] };
|
|
99
100
|
}
|
|
101
|
+
// Cap the output like the anthropic/openai adapters do. Gemini carries the
|
|
102
|
+
// ceiling on generationConfig.maxOutputTokens; merge so a caller that already
|
|
103
|
+
// populated generationConfig keeps their other fields. Only a finite,
|
|
104
|
+
// positive maxTokens applies — omitting it leaves generationConfig untouched.
|
|
105
|
+
if (Number.isFinite(maxTokens) && maxTokens > 0) {
|
|
106
|
+
body.generationConfig = { ...(body.generationConfig || {}), maxOutputTokens: maxTokens };
|
|
107
|
+
}
|
|
100
108
|
|
|
101
109
|
const res = await fetchFn(url, {
|
|
102
110
|
method: 'POST',
|
|
@@ -146,7 +154,10 @@ export function parseResponse(json) {
|
|
|
146
154
|
const um = json?.usageMetadata;
|
|
147
155
|
const usage = um
|
|
148
156
|
? {
|
|
149
|
-
|
|
157
|
+
// promptTokenCount is cache-INCLUSIVE; report NET (minus cached) to
|
|
158
|
+
// match Anthropic's convention so the cost cap doesn't bill the cached
|
|
159
|
+
// tokens at BOTH the input rate and the cache-read rate.
|
|
160
|
+
inputTokens: (um.promptTokenCount || 0) - (um.cachedContentTokenCount || 0),
|
|
150
161
|
outputTokens: um.candidatesTokenCount || 0,
|
|
151
162
|
cacheReadInputTokens: um.cachedContentTokenCount || 0,
|
|
152
163
|
}
|
|
@@ -131,6 +131,13 @@ export function parseResponse(json) {
|
|
|
131
131
|
else { try { input = JSON.parse(a); } catch (e) { input = {}; parseError = `malformed tool arguments: ${e.message}`; } }
|
|
132
132
|
} else if (a && typeof a === 'object') {
|
|
133
133
|
input = a;
|
|
134
|
+
} else if (a !== undefined && a !== null) {
|
|
135
|
+
// arguments is present but neither a string nor an object (e.g. a number
|
|
136
|
+
// or boolean) — an unexpected wire shape. Surface it as a tool failure
|
|
137
|
+
// instead of silently running the tool with {} (mirrors the malformed
|
|
138
|
+
// JSON-string branch above).
|
|
139
|
+
input = {};
|
|
140
|
+
parseError = `unexpected tool arguments type: ${typeof a}`;
|
|
134
141
|
}
|
|
135
142
|
return { id: tc.id, name: tc?.function?.name, input, ...(parseError ? { parseError } : {}) };
|
|
136
143
|
});
|