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/commands/mcp.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// commands/mcp.mjs — `lazyclaw mcp <list|add|remove|call>`.
|
|
2
|
+
//
|
|
3
|
+
// MCP servers are stored in cfg.mcp.servers[] (stdio transport only; an entry
|
|
4
|
+
// is { name, command, args[], env{}, allowGlob }). They are spawned at daemon
|
|
5
|
+
// boot by mcp/server_spawn.startConfigured. This command manages that list and
|
|
6
|
+
// lets you invoke a single tool from the CLI for a quick check.
|
|
7
|
+
//
|
|
8
|
+
// Verb dispatch + output convention mirror commands/agents.mjs cmdTeam:
|
|
9
|
+
// success → JSON on stdout, usage/validation errors → console.error + exit 2,
|
|
10
|
+
// operational failures → exit 1.
|
|
11
|
+
|
|
12
|
+
import { readConfig, writeConfig } from '../lib/config.mjs';
|
|
13
|
+
|
|
14
|
+
// The name becomes the tool namespace `mcp:<name>:<tool>`, so it must not carry
|
|
15
|
+
// a colon or whitespace — reuse the cron/job name grammar.
|
|
16
|
+
const NAME_RE = /^[A-Za-z0-9_.-]+$/;
|
|
17
|
+
|
|
18
|
+
const emitJson = (o) => process.stdout.write(JSON.stringify(o, null, 2) + '\n');
|
|
19
|
+
|
|
20
|
+
// `--args "-y pkg /tmp"` and `--env "K=V K2=V2"` arrive as one whitespace-
|
|
21
|
+
// joined string (parseArgs consumes a single argv token), so split on spaces.
|
|
22
|
+
function splitWords(raw) {
|
|
23
|
+
if (raw === undefined || raw === null || raw === true) return [];
|
|
24
|
+
return String(raw).split(/\s+/).filter(Boolean);
|
|
25
|
+
}
|
|
26
|
+
function parseEnv(raw) {
|
|
27
|
+
const env = {};
|
|
28
|
+
for (const pair of splitWords(raw)) {
|
|
29
|
+
const i = pair.indexOf('=');
|
|
30
|
+
if (i > 0) env[pair.slice(0, i)] = pair.slice(i + 1);
|
|
31
|
+
}
|
|
32
|
+
return env;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function cmdMcp(sub, positional = [], flags = {}) {
|
|
36
|
+
const cfg = readConfig();
|
|
37
|
+
const servers = (cfg.mcp && Array.isArray(cfg.mcp.servers)) ? cfg.mcp.servers : [];
|
|
38
|
+
|
|
39
|
+
switch (sub) {
|
|
40
|
+
case undefined:
|
|
41
|
+
case 'list': {
|
|
42
|
+
const configured = servers.map((s) => ({
|
|
43
|
+
name: s.name, command: s.command, args: s.args || [], allowGlob: s.allowGlob || '*',
|
|
44
|
+
}));
|
|
45
|
+
// listServers() reports servers connected in THIS process; the short-lived
|
|
46
|
+
// CLI never boots them, so this is normally empty — surfaced anyway so the
|
|
47
|
+
// same shape works when the command runs inside the daemon.
|
|
48
|
+
let connected = [];
|
|
49
|
+
try { connected = (await import('../mcp/client.mjs')).listServers(); }
|
|
50
|
+
catch { /* mcp client unavailable — report configured only */ }
|
|
51
|
+
emitJson({ ok: true, configured, connected });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case 'add': {
|
|
56
|
+
const name = positional[0];
|
|
57
|
+
const command = flags.command;
|
|
58
|
+
if (!name || !command || command === true) {
|
|
59
|
+
console.error('Usage: lazyclaw mcp add <name> --command <cmd> [--args "<a b c>"] [--allow-glob <glob>] [--env "K=V ..."]');
|
|
60
|
+
process.exit(2);
|
|
61
|
+
}
|
|
62
|
+
if (!NAME_RE.test(name)) {
|
|
63
|
+
console.error(`mcp add: invalid name "${name}" — letters/digits/.-_ only (it becomes the tool namespace mcp:${name}:<tool>)`);
|
|
64
|
+
process.exit(2);
|
|
65
|
+
}
|
|
66
|
+
if (servers.find((s) => s.name === name)) {
|
|
67
|
+
console.error(`mcp add: server "${name}" already exists — remove it first or pick another name`);
|
|
68
|
+
process.exit(2);
|
|
69
|
+
}
|
|
70
|
+
const record = {
|
|
71
|
+
name,
|
|
72
|
+
command: String(command),
|
|
73
|
+
args: splitWords(flags.args),
|
|
74
|
+
allowGlob: (flags['allow-glob'] && flags['allow-glob'] !== true) ? String(flags['allow-glob']) : '*',
|
|
75
|
+
};
|
|
76
|
+
const env = parseEnv(flags.env);
|
|
77
|
+
if (Object.keys(env).length) record.env = env;
|
|
78
|
+
cfg.mcp = cfg.mcp || {};
|
|
79
|
+
cfg.mcp.servers = servers.concat([record]);
|
|
80
|
+
writeConfig(cfg);
|
|
81
|
+
emitJson({ ok: true, added: record });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case 'remove':
|
|
86
|
+
case 'rm':
|
|
87
|
+
case 'delete': {
|
|
88
|
+
const name = positional[0];
|
|
89
|
+
if (!name) { console.error('Usage: lazyclaw mcp remove <name>'); process.exit(2); }
|
|
90
|
+
if (!servers.find((s) => s.name === name)) {
|
|
91
|
+
console.error(`mcp remove: no configured server "${name}"`);
|
|
92
|
+
process.exit(2);
|
|
93
|
+
}
|
|
94
|
+
cfg.mcp = cfg.mcp || {};
|
|
95
|
+
cfg.mcp.servers = servers.filter((s) => s.name !== name);
|
|
96
|
+
writeConfig(cfg);
|
|
97
|
+
emitJson({ ok: true, removed: name });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
case 'call': {
|
|
102
|
+
const server = positional[0];
|
|
103
|
+
const toolName = positional[1];
|
|
104
|
+
if (!server || !toolName) {
|
|
105
|
+
console.error('Usage: lazyclaw mcp call <server> <tool> [--args-json \'{"k":"v"}\']');
|
|
106
|
+
process.exit(2);
|
|
107
|
+
}
|
|
108
|
+
const sconf = servers.find((s) => s.name === server);
|
|
109
|
+
if (!sconf) {
|
|
110
|
+
console.error(`mcp call: no configured server "${server}" — add it with 'lazyclaw mcp add ${server} --command <cmd>'`);
|
|
111
|
+
process.exit(2);
|
|
112
|
+
}
|
|
113
|
+
let args = {};
|
|
114
|
+
if (flags['args-json'] !== undefined && flags['args-json'] !== true) {
|
|
115
|
+
try { args = JSON.parse(String(flags['args-json'])); }
|
|
116
|
+
catch (e) { console.error(`mcp call: --args-json is not valid JSON: ${e.message}`); process.exit(2); }
|
|
117
|
+
}
|
|
118
|
+
const mcpClient = await import('../mcp/client.mjs');
|
|
119
|
+
const registry = await import('../mas/tools/registry.mjs');
|
|
120
|
+
let started = false;
|
|
121
|
+
try {
|
|
122
|
+
// sensitive:true — the same approval-gate invariant the daemon boot
|
|
123
|
+
// enforces (server_spawn). A CLI call can never downgrade an MCP tool.
|
|
124
|
+
await mcpClient.startServer({
|
|
125
|
+
name: sconf.name, command: sconf.command, args: sconf.args || [],
|
|
126
|
+
env: sconf.env || {}, allowGlob: sconf.allowGlob || '*', sensitive: true,
|
|
127
|
+
});
|
|
128
|
+
started = true;
|
|
129
|
+
const full = `mcp:${server}:${toolName}`;
|
|
130
|
+
const tool = registry.lookup(full);
|
|
131
|
+
if (!tool) {
|
|
132
|
+
const available = registry.listNames().filter((n) => n.startsWith(`mcp:${server}:`));
|
|
133
|
+
console.error(`mcp call: tool "${full}" not found. Available: ${available.join(', ') || '(none)'}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
emitJson(await tool.exec(args));
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.error(`mcp call: ${e?.message || e}`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
} finally {
|
|
141
|
+
if (started) { try { await mcpClient.stopServer(server); } catch { /* best-effort */ } }
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
default:
|
|
147
|
+
console.error('Usage: lazyclaw mcp <list|add|remove|call> ...');
|
|
148
|
+
process.exit(2);
|
|
149
|
+
}
|
|
150
|
+
}
|
package/commands/misc.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import { configPath } from '../lib/config.mjs';
|
|
6
6
|
import { resolveSandbox, listBackends } from '../sandbox/index.mjs';
|
|
7
|
+
import { pickAvailableConfiner } from '../sandbox/spawn.mjs';
|
|
7
8
|
|
|
8
9
|
export async function cmdBrowse(url, flags = {}) {
|
|
9
10
|
if (!url) { console.error('Usage: lazyclaw browse <url> [--max-bytes <N>] [--timeout-ms <N>] [--meta]'); process.exit(2); }
|
|
@@ -33,6 +34,25 @@ export async function cmdSandbox(args, flags = {}) {
|
|
|
33
34
|
return 0;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
if (sub === 'status') {
|
|
38
|
+
// Show the EFFECTIVE default-on confinement posture for this host so the
|
|
39
|
+
// operator can audit what sensitive tools run under.
|
|
40
|
+
const cfg = _sandboxLoadConfigOrEmpty();
|
|
41
|
+
const sb = cfg.sandbox || {};
|
|
42
|
+
const off = sb.confine === false || sb.default === 'off' || sb.default === 'none';
|
|
43
|
+
const confiner = (sb.local && sb.local.confiner && sb.local.confiner !== 'auto')
|
|
44
|
+
? sb.local.confiner
|
|
45
|
+
: pickAvailableConfiner();
|
|
46
|
+
const allowNet = sb.allowNet !== false;
|
|
47
|
+
process.stdout.write(`confinement: ${off ? 'OFF' : 'ON'} (default-on; opt out with cfg.sandbox.confine=false)\n`);
|
|
48
|
+
process.stdout.write(`host confiner: ${confiner}\n`);
|
|
49
|
+
process.stdout.write(`policy: writes → workspace + temp only · secret dirs unreadable · network ${allowNet ? 'allowed' : 'denied'}\n`);
|
|
50
|
+
if (!off && confiner === 'none') {
|
|
51
|
+
process.stdout.write('note: no OS confiner available on this host — sensitive tools run UNCONFINED. Install bubblewrap or firejail on Linux.\n');
|
|
52
|
+
}
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
if (sub === 'test') {
|
|
37
57
|
const name = args[1];
|
|
38
58
|
if (!name) { process.stderr.write('usage: lazyclaw sandbox test <kind|profile>\n'); return 2; }
|
package/commands/sessions.mjs
CHANGED
|
@@ -7,6 +7,65 @@ import { ensureRegistry, getRegistry } from '../lib/registry_boot.mjs';
|
|
|
7
7
|
|
|
8
8
|
const BUNDLE_VERSION = 1;
|
|
9
9
|
|
|
10
|
+
// Key names that carry secrets. A bundle on a teammate's laptop must never
|
|
11
|
+
// leak per-provider keys (cfg.authProfiles[<provider>][i].key, written by
|
|
12
|
+
// providers/auth_store.mjs) or any other secret-bearing config value, not
|
|
13
|
+
// just the legacy top-level cfg['api-key']. Match on the KEY name so we can
|
|
14
|
+
// redact custom/nested entries without knowing the config schema ahead of
|
|
15
|
+
// time, while leaving non-secret keys (baseUrl, model, label, …) untouched.
|
|
16
|
+
const SECRET_KEY_RE = /(?:api[-_]?key|secret|token|password|authorization|access[-_]?key)/i;
|
|
17
|
+
|
|
18
|
+
// Deep-clone cfg with every secret-bearing string value redacted. Pure — never
|
|
19
|
+
// mutates the input. Preserves structure/labels so the bundle stays
|
|
20
|
+
// inspectable (e.g. authProfiles keeps {label} but drops {key}).
|
|
21
|
+
function redactSecrets(value, keyName = '') {
|
|
22
|
+
if (Array.isArray(value)) return value.map((v) => redactSecrets(v));
|
|
23
|
+
if (value && typeof value === 'object') {
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const [k, v] of Object.entries(value)) out[k] = redactSecrets(v, k);
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === 'string' && SECRET_KEY_RE.test(keyName)) return '***REDACTED***';
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Reciprocal of the export redaction: deep-clone cfg with every
|
|
33
|
+
// '***REDACTED***' placeholder dropped, so importing a redacted bundle never
|
|
34
|
+
// persists the literal placeholder string into config.json. Skipping the key
|
|
35
|
+
// (rather than writing it) means an existing real value survives the default
|
|
36
|
+
// "bundle wins" merge, and an authProfiles entry keeps its {label} slot minus
|
|
37
|
+
// the secret. Symmetric with redactSecrets / redactAuthProfileKeys above.
|
|
38
|
+
const REDACTED_PLACEHOLDER = '***REDACTED***';
|
|
39
|
+
function stripRedacted(value) {
|
|
40
|
+
if (Array.isArray(value)) return value.map(stripRedacted);
|
|
41
|
+
if (value && typeof value === 'object') {
|
|
42
|
+
const out = {};
|
|
43
|
+
for (const [k, v] of Object.entries(value)) {
|
|
44
|
+
if (v === REDACTED_PLACEHOLDER) continue; // drop the redacted key entirely
|
|
45
|
+
out[k] = stripRedacted(v);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Redact the per-provider api keys in authProfiles. The field is literally
|
|
53
|
+
// named `key` (not `apiKey`), so the generic key-name match above doesn't
|
|
54
|
+
// catch it; handle it structurally instead. Keep `label` so the auth profile
|
|
55
|
+
// shape stays inspectable. Operates on the already-cloned safeCfg.
|
|
56
|
+
function redactAuthProfileKeys(safeCfg) {
|
|
57
|
+
const profiles = safeCfg.authProfiles;
|
|
58
|
+
if (!profiles || typeof profiles !== 'object') return safeCfg;
|
|
59
|
+
for (const provider of Object.keys(profiles)) {
|
|
60
|
+
const arr = profiles[provider];
|
|
61
|
+
if (!Array.isArray(arr)) continue;
|
|
62
|
+
profiles[provider] = arr.map((p) =>
|
|
63
|
+
p && typeof p === 'object' && 'key' in p ? { ...p, key: '***REDACTED***' } : p,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return safeCfg;
|
|
67
|
+
}
|
|
68
|
+
|
|
10
69
|
export async function cmdExport(flags) {
|
|
11
70
|
// Portable bundle: config + every installed skill + (optionally) every
|
|
12
71
|
// persisted session. Writes JSON to stdout so the caller pipes it
|
|
@@ -19,10 +78,9 @@ export async function cmdExport(flags) {
|
|
|
19
78
|
const sessionsMod = await import('../sessions.mjs');
|
|
20
79
|
const cfgDir = path.dirname(configPath());
|
|
21
80
|
const cfg = readConfig();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
81
|
+
// redactSecrets deep-clones, so --include-secrets keeps the original cfg
|
|
82
|
+
// verbatim while the default path emits a fully redacted copy.
|
|
83
|
+
const safeCfg = flags['include-secrets'] ? cfg : redactAuthProfileKeys(redactSecrets(cfg));
|
|
26
84
|
const skills = skillsMod.listSkills(cfgDir).map(s => ({
|
|
27
85
|
name: s.name,
|
|
28
86
|
content: skillsMod.loadSkill(s.name, cfgDir),
|
|
@@ -79,11 +137,13 @@ export async function cmdImport(flags) {
|
|
|
79
137
|
// Config
|
|
80
138
|
if (bundle.config && typeof bundle.config === 'object') {
|
|
81
139
|
const existing = readConfig();
|
|
140
|
+
// Strip every redacted placeholder from the incoming config BEFORE the
|
|
141
|
+
// merge so the literal '***REDACTED***' is never persisted — at the
|
|
142
|
+
// top-level api-key, inside authProfiles[].key, or any nested secret.
|
|
143
|
+
const incoming = stripRedacted(bundle.config);
|
|
82
144
|
const next = flags['no-overwrite-config']
|
|
83
|
-
? { ...
|
|
84
|
-
: { ...existing, ...
|
|
85
|
-
// Drop redacted secrets so we never write the placeholder string.
|
|
86
|
-
if (next['api-key'] === '***REDACTED***') delete next['api-key'];
|
|
145
|
+
? { ...incoming, ...existing } // existing wins
|
|
146
|
+
: { ...existing, ...incoming }; // bundle wins (default)
|
|
87
147
|
writeConfig(next);
|
|
88
148
|
stats.configKeys = Object.keys(bundle.config).length;
|
|
89
149
|
}
|
package/commands/setup.mjs
CHANGED
|
@@ -15,7 +15,8 @@ import { SUBCOMMANDS, parseArgs, AGENT_REG_SUBS } from '../lib/args.mjs';
|
|
|
15
15
|
import {
|
|
16
16
|
_attachGhostAutocomplete, _fetchModelsForProvider, _pauseChatForSubMenu,
|
|
17
17
|
_pickModelInteractive, _pickProviderInteractive, _printChatBanner,
|
|
18
|
-
_quickPrompt, _renderBanner, _renderV5Banner,
|
|
18
|
+
_quickPrompt, _quickPromptSecret, _renderBanner, _renderV5Banner,
|
|
19
|
+
_pickYesNo,
|
|
19
20
|
} from '../tui/pickers.mjs';
|
|
20
21
|
import { firstRunMode as _firstRunMode } from '../first_run.mjs';
|
|
21
22
|
import { applyChatWindow as _applyChatWindow, CHAT_WINDOW_TURNS, CHAT_WINDOW_TOKEN_BUDGET } from '../chat_window.mjs';
|
|
@@ -78,9 +79,13 @@ export async function cmdOnboard(flags) {
|
|
|
78
79
|
const meta = (getRegistry().PROVIDER_INFO || {})[flags.provider] || {};
|
|
79
80
|
if (meta.requiresApiKey && !flags['api-key']) {
|
|
80
81
|
const prefix = meta.keyPrefix ? ` (starts with "${meta.keyPrefix}")` : '';
|
|
81
|
-
|
|
82
|
+
// Close the line-mode reader before the masked raw-mode read so they
|
|
83
|
+
// don't both consume stdin. The key is masked (•) — never echoed.
|
|
84
|
+
rl.close();
|
|
85
|
+
flags['api-key'] = await _quickPromptSecret(`api-key${prefix}: `);
|
|
86
|
+
} else {
|
|
87
|
+
rl.close();
|
|
82
88
|
}
|
|
83
|
-
rl.close();
|
|
84
89
|
}
|
|
85
90
|
const next = applyOnboardConfig(readConfig(), flags);
|
|
86
91
|
if (!next.provider) { console.error('onboard: provider is required'); process.exit(2); }
|
|
@@ -160,7 +165,7 @@ const HELP_DETAILS = {
|
|
|
160
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.',
|
|
161
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.',
|
|
162
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).',
|
|
163
|
-
setup: 'Usage: lazyclaw setup [--skip-test]\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.',
|
|
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.',
|
|
164
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.',
|
|
165
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.',
|
|
166
171
|
};
|
|
@@ -206,7 +211,16 @@ export async function cmdSetup(_sub, _positional, flags = {}) {
|
|
|
206
211
|
const cfgDir = path.dirname(configPath());
|
|
207
212
|
const colors = { accent, bold, dim, ok, warn };
|
|
208
213
|
|
|
214
|
+
// Per-step gating: `--only a,b` runs ONLY those; `--skip a,b` runs all but
|
|
215
|
+
// those. Steps: provider verify channel workspace skill webhook orchestrator.
|
|
216
|
+
// e.g. `lazyclaw setup --only channel` re-runs just the channel step.
|
|
217
|
+
const onlySet = flags.only ? new Set(String(flags.only).toLowerCase().split(',').map((s) => s.trim()).filter(Boolean)) : null;
|
|
218
|
+
const skipSet = new Set(String(flags.skip || '').toLowerCase().split(',').map((s) => s.trim()).filter(Boolean));
|
|
219
|
+
const want = (step) => (onlySet ? onlySet.has(step) : !skipSet.has(step));
|
|
220
|
+
let cfgAfterOnboard = cfg;
|
|
221
|
+
|
|
209
222
|
// ── Step 1/7: Provider + model (mandatory) ──────────────────
|
|
223
|
+
if (want('provider')) {
|
|
210
224
|
process.stdout.write(` ${accent('Step 1/7 ·')} ${bold('Pick a provider + model')}\n`);
|
|
211
225
|
process.stdout.write(` ${dim('Opens the arrow-key picker. Tip: pick a model with ≥64k context — small windows can\'t hold multi-step tool-calling state.')}\n\n`);
|
|
212
226
|
await _quickPrompt(' ▶ press Enter to open the picker ');
|
|
@@ -225,7 +239,7 @@ export async function cmdSetup(_sub, _positional, flags = {}) {
|
|
|
225
239
|
// no provider set, bail out early — the rest of the wizard depends
|
|
226
240
|
// on a provider being configured. `return` (not process.exit) so a
|
|
227
241
|
// launcher caller can re-prompt or fall back gracefully.
|
|
228
|
-
|
|
242
|
+
cfgAfterOnboard = readConfig();
|
|
229
243
|
if (!cfgAfterOnboard.provider) {
|
|
230
244
|
process.stdout.write(`\n ${warn('Setup not completed — provider was not configured.')}\n`);
|
|
231
245
|
process.stdout.write(` ${dim('Run `lazyclaw setup` again when ready, or pick "Onboard" from the menu for a single-step picker.')}\n\n`);
|
|
@@ -236,12 +250,14 @@ export async function cmdSetup(_sub, _positional, flags = {}) {
|
|
|
236
250
|
// Context window — asked right after the model pick (optional; Enter keeps
|
|
237
251
|
// defaults). Not a numbered step: it's part of the core model setup.
|
|
238
252
|
await runContextStep({ prompt: _quickPrompt, colors });
|
|
253
|
+
}
|
|
239
254
|
|
|
240
255
|
// ── Step 2/7: Verify one clean chat works ───────────────────
|
|
241
256
|
// Hermes rule: get a clean reply before layering on channels/skills.
|
|
257
|
+
if (want('verify') && cfgAfterOnboard.provider) {
|
|
242
258
|
process.stdout.write(` ${accent('Step 2/7 ·')} ${bold('Verify the provider responds')}\n`);
|
|
243
259
|
process.stdout.write(` ${dim('Sends a 1-token "ping" via `lazyclaw providers test`. Confirm a clean reply before layering on channels/skills.')}\n\n`);
|
|
244
|
-
const wantPing = !flags['skip-test'] &&
|
|
260
|
+
const wantPing = !flags['skip-test'] && await _pickYesNo('Test the provider now?', { subtitle: 'sends a 1-token ping to confirm a clean reply', yesLabel: 'Test now', noLabel: 'Skip', defaultYes: true });
|
|
245
261
|
if (wantPing) {
|
|
246
262
|
try {
|
|
247
263
|
// No-exit probe (providers/probe.mjs) — the CLI `providers test` calls
|
|
@@ -258,15 +274,20 @@ export async function cmdSetup(_sub, _positional, flags = {}) {
|
|
|
258
274
|
} else {
|
|
259
275
|
process.stdout.write(` ${dim('— skipped —')}\n\n`);
|
|
260
276
|
}
|
|
277
|
+
}
|
|
261
278
|
|
|
262
279
|
// ── Step 3/7: Channel / gateway (optional) ──────────────────
|
|
280
|
+
if (want('channel')) {
|
|
263
281
|
process.stdout.write(` ${accent('Step 3/7 ·')} ${bold('Where will you run it?')} ${dim('(optional)')}\n`);
|
|
264
282
|
await runChannelStep({ cfgDir, prompt: _quickPrompt, colors });
|
|
283
|
+
}
|
|
265
284
|
|
|
266
285
|
// ── Step 4/7: Optional workspace ────────────────────────────
|
|
286
|
+
if (want('workspace')) {
|
|
267
287
|
process.stdout.write(` ${accent('Step 4/7 ·')} ${bold('Initialise a workspace?')} ${dim('(optional)')}\n`);
|
|
268
288
|
process.stdout.write(` ${dim('A workspace is a folder of AGENTS.md / SOUL.md / TOOLS.md prompt files that auto-inject into chat / agent. Skip if you don\'t need project-specific personas yet.')}\n\n`);
|
|
269
|
-
const
|
|
289
|
+
const wantWs = await _pickYesNo('Initialise a workspace?', { yesLabel: 'Create one', noLabel: 'Skip', defaultYes: false });
|
|
290
|
+
const wsName = wantWs ? (await _quickPrompt(' workspace name: ')).trim() : '';
|
|
270
291
|
if (wsName && /^[A-Za-z0-9_.-]+$/.test(wsName)) {
|
|
271
292
|
try {
|
|
272
293
|
const ws = await import('../workspace.mjs');
|
|
@@ -281,11 +302,14 @@ export async function cmdSetup(_sub, _positional, flags = {}) {
|
|
|
281
302
|
} else {
|
|
282
303
|
process.stdout.write(` ${dim('— skipped —')}\n\n`);
|
|
283
304
|
}
|
|
305
|
+
}
|
|
284
306
|
|
|
285
307
|
// ── Step 5/7: Optional skill bundle install ─────────────────
|
|
308
|
+
if (want('skill')) {
|
|
286
309
|
process.stdout.write(` ${accent('Step 5/7 ·')} ${bold('Install a skill bundle from GitHub?')} ${dim('(optional)')}\n`);
|
|
287
310
|
process.stdout.write(` ${dim('Format: <user>/<repo>[@<ref>]. Skills are .md prompt fragments that compose into the system prompt via --skill.')}\n\n`);
|
|
288
|
-
const
|
|
311
|
+
const wantSkill = await _pickYesNo('Install a skill bundle from GitHub?', { yesLabel: 'Install one', noLabel: 'Skip', defaultYes: false });
|
|
312
|
+
const skillSpec = wantSkill ? (await _quickPrompt(' github spec (<user>/<repo>[@<ref>]): ')).trim() : '';
|
|
289
313
|
if (skillSpec) {
|
|
290
314
|
try {
|
|
291
315
|
const inst = await import('../skills_install.mjs');
|
|
@@ -302,14 +326,19 @@ export async function cmdSetup(_sub, _positional, flags = {}) {
|
|
|
302
326
|
} else {
|
|
303
327
|
process.stdout.write(` ${dim('— skipped —')}\n\n`);
|
|
304
328
|
}
|
|
329
|
+
}
|
|
305
330
|
|
|
306
331
|
// ── Step 6/7: Optional outbound webhook ─────────────────────
|
|
332
|
+
if (want('webhook')) {
|
|
307
333
|
process.stdout.write(` ${accent('Step 6/7 ·')} ${bold('Add an outbound webhook?')} ${dim('(optional)')}\n`);
|
|
308
334
|
await runWebhookStep({ prompt: _quickPrompt, colors });
|
|
335
|
+
}
|
|
309
336
|
|
|
310
337
|
// ── Step 7/7: Optional multi-agent orchestration ────────────
|
|
338
|
+
if (want('orchestrator')) {
|
|
311
339
|
process.stdout.write(` ${accent('Step 7/7 ·')} ${bold('Enable multi-agent orchestration?')} ${dim('(optional)')}\n`);
|
|
312
340
|
await runOrchestratorStep({ prompt: _quickPrompt, colors });
|
|
341
|
+
}
|
|
313
342
|
|
|
314
343
|
// ── Wrap up ─────────────────────────────────────────────────
|
|
315
344
|
process.stdout.write('\n');
|
|
@@ -319,6 +348,13 @@ export async function cmdSetup(_sub, _positional, flags = {}) {
|
|
|
319
348
|
process.stdout.write(` ${dim('•')} lazyclaw agent "..." ${dim('— one-shot prompt')}\n`);
|
|
320
349
|
process.stdout.write(` ${dim('•')} lazyclaw doctor ${dim('— diagnostic JSON')}\n`);
|
|
321
350
|
process.stdout.write(` ${dim('•')} lazyclaw setup ${dim('— re-run this wizard any time')}\n\n`);
|
|
351
|
+
|
|
352
|
+
// Release stdin so `lazyclaw setup` returns to the shell instead of hanging
|
|
353
|
+
// at "Setup complete". The interactive prompts (_arrowMenu / _quickPrompt)
|
|
354
|
+
// resume()+ref() stdin to read input; without an unref the ref'd handle keeps
|
|
355
|
+
// the event loop alive after the last step. When setup is invoked from the
|
|
356
|
+
// launcher, the next menu resume()+ref()s stdin again, so this is safe there.
|
|
357
|
+
try { if (process.stdin.unref) process.stdin.unref(); } catch { /* best-effort */ }
|
|
322
358
|
}
|
|
323
359
|
|
|
324
360
|
// First-run welcome panel + delegated onboard. Drawn once before the
|