lazyclaw 4.2.2 → 5.0.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 +44 -0
- package/README.md +172 -353
- package/agents.mjs +19 -3
- package/channels/handoff.mjs +41 -0
- package/channels/loader.mjs +124 -0
- package/channels/matrix.mjs +417 -0
- package/channels/telegram.mjs +362 -0
- package/channels/threads.mjs +116 -0
- package/cli.mjs +730 -27
- package/daemon.mjs +111 -0
- package/gateway/device_auth.mjs +664 -0
- package/gateway/http_gateway.mjs +304 -0
- package/mas/agent_memory.mjs +35 -34
- package/mas/agent_turn.mjs +30 -1
- package/mas/confidence.mjs +108 -0
- package/mas/index_db.mjs +242 -0
- package/mas/mention_router.mjs +75 -4
- package/mas/nudge.mjs +97 -0
- package/mas/prompt_stack.mjs +80 -0
- package/mas/provider_adapters.mjs +83 -0
- package/mas/redact.mjs +46 -0
- package/mas/skill_synth.mjs +331 -0
- package/mas/tool_runner.mjs +19 -48
- package/mas/tools/browser.mjs +77 -0
- package/mas/tools/clarify.mjs +36 -0
- package/mas/tools/coding.mjs +109 -0
- package/mas/tools/delegation.mjs +53 -0
- package/mas/tools/edit.mjs +36 -0
- package/mas/tools/git.mjs +110 -0
- package/mas/tools/ha.mjs +34 -0
- package/mas/tools/learning.mjs +168 -0
- package/mas/tools/media.mjs +105 -0
- package/mas/tools/os.mjs +152 -0
- package/mas/tools/patch.mjs +91 -0
- package/mas/tools/recall.mjs +103 -0
- package/mas/tools/registry.mjs +93 -0
- package/mas/tools/scheduling.mjs +62 -0
- package/mas/tools/skill_view.mjs +43 -0
- package/mas/tools/web.mjs +137 -0
- package/mas/toolsets.mjs +64 -0
- package/mas/trajectory_export.mjs +169 -0
- package/mas/trajectory_store.mjs +179 -0
- package/mas/user_modeler.mjs +108 -0
- package/package.json +22 -3
- package/providers/codex_cli.mjs +200 -0
- package/providers/gemini_cli.mjs +179 -0
- package/providers/registry.mjs +61 -1
- package/sandbox/base.mjs +82 -0
- package/sandbox/confiners/bubblewrap.mjs +21 -0
- package/sandbox/confiners/firejail.mjs +16 -0
- package/sandbox/confiners/landlock.mjs +14 -0
- package/sandbox/confiners/seatbelt.mjs +28 -0
- package/sandbox/daytona.mjs +37 -0
- package/sandbox/docker.mjs +91 -0
- package/sandbox/index.mjs +67 -0
- package/sandbox/local.mjs +59 -0
- package/sandbox/modal.mjs +53 -0
- package/sandbox/singularity.mjs +39 -0
- package/sandbox/ssh.mjs +56 -0
- package/sandbox.mjs +11 -127
- package/scripts/hermes-import.mjs +111 -0
- package/scripts/migrate-v5.mjs +342 -0
- package/scripts/openclaw-import.mjs +71 -0
- package/sessions.mjs +20 -1
- package/skills.mjs +101 -8
- package/skills_curator.mjs +323 -0
- package/workspace.mjs +18 -3
package/cli.mjs
CHANGED
|
@@ -4,6 +4,10 @@ import path from 'node:path';
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import { pathToFileURL } from 'node:url';
|
|
7
|
+
// sandbox subcommands — list/test/add/use (Phase D).
|
|
8
|
+
import { resolveSandbox, listBackends } from './sandbox/index.mjs';
|
|
9
|
+
// Phase G: defaultConfigDir for personality subcommand (spec §9, decision C7).
|
|
10
|
+
import { defaultConfigDir as _persDefaultCfg } from './memory.mjs';
|
|
7
11
|
|
|
8
12
|
async function loadEngine() {
|
|
9
13
|
return import('./workflow/persistent.mjs');
|
|
@@ -56,6 +60,17 @@ function _resolveAuthKey(cfg, provider) {
|
|
|
56
60
|
return cfg['api-key'] || '';
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
// Per-provider base-URL override (used by tests + private gateways).
|
|
64
|
+
// Single source of truth — the reflect / skill-synth / task-tick paths
|
|
65
|
+
// all resolve through here so a new provider's env var lands in one spot.
|
|
66
|
+
function _resolveBaseUrl(provider) {
|
|
67
|
+
return {
|
|
68
|
+
anthropic: process.env.LAZYCLAW_ANTHROPIC_BASE_URL,
|
|
69
|
+
openai: process.env.LAZYCLAW_OPENAI_BASE_URL,
|
|
70
|
+
gemini: process.env.LAZYCLAW_GEMINI_BASE_URL,
|
|
71
|
+
}[provider] || undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
59
74
|
async function importWorkflow(file) {
|
|
60
75
|
const abs = path.resolve(file);
|
|
61
76
|
const url = pathToFileURL(abs).href;
|
|
@@ -628,6 +643,66 @@ async function cmdResume(sessionId, file, opts = {}) {
|
|
|
628
643
|
}
|
|
629
644
|
}
|
|
630
645
|
|
|
646
|
+
// --- Phase G: personality subcommand (spec §9, decision C7) -------------
|
|
647
|
+
async function cmdPersonality(sub, a, b) {
|
|
648
|
+
const cfgDir = process.env.LAZYCLAW_CONFIG_DIR || _persDefaultCfg();
|
|
649
|
+
const dir = path.join(cfgDir, 'personalities');
|
|
650
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
651
|
+
|
|
652
|
+
if (!sub || sub === 'list') {
|
|
653
|
+
const names = fs.existsSync(dir)
|
|
654
|
+
? fs.readdirSync(dir).filter(f => f.endsWith('.md')).map(f => f.slice(0, -3))
|
|
655
|
+
: [];
|
|
656
|
+
if (!names.length) { console.log('No personalities installed'); return 0; }
|
|
657
|
+
for (const n of names.sort()) console.log(n);
|
|
658
|
+
return 0;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (sub === 'show') {
|
|
662
|
+
if (!a) { console.error('Usage: lazyclaw personality show <name>'); return 2; }
|
|
663
|
+
const p = path.join(dir, `${a}.md`);
|
|
664
|
+
if (!fs.existsSync(p)) { console.error(`personality not found: ${a}`); return 1; }
|
|
665
|
+
process.stdout.write(fs.readFileSync(p, 'utf8'));
|
|
666
|
+
return 0;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (sub === 'install') {
|
|
670
|
+
if (!a || !b) { console.error('Usage: lazyclaw personality install <name> <file>'); return 2; }
|
|
671
|
+
const dst = path.join(dir, `${a}.md`);
|
|
672
|
+
if (fs.existsSync(dst)) { console.error(`personality already installed: ${a}`); return 1; }
|
|
673
|
+
if (!fs.existsSync(b)) { console.error(`source file not found: ${b}`); return 1; }
|
|
674
|
+
fs.writeFileSync(dst, fs.readFileSync(b, 'utf8'));
|
|
675
|
+
console.log(`installed ${a}`);
|
|
676
|
+
return 0;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (sub === 'remove') {
|
|
680
|
+
if (!a) { console.error('Usage: lazyclaw personality remove <name>'); return 2; }
|
|
681
|
+
const p = path.join(dir, `${a}.md`);
|
|
682
|
+
if (!fs.existsSync(p)) { console.error(`personality not installed: ${a}`); return 1; }
|
|
683
|
+
fs.unlinkSync(p);
|
|
684
|
+
console.log(`removed ${a}`);
|
|
685
|
+
return 0;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (sub === 'use') {
|
|
689
|
+
if (!a) { console.error('Usage: lazyclaw personality use <name>'); return 2; }
|
|
690
|
+
const p = path.join(dir, `${a}.md`);
|
|
691
|
+
if (!fs.existsSync(p)) { console.error(`personality not installed: ${a}`); return 1; }
|
|
692
|
+
const cfgPath = path.join(cfgDir, 'config.json');
|
|
693
|
+
let cfg = {};
|
|
694
|
+
try { cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); } catch {}
|
|
695
|
+
cfg.persona = { ...(cfg.persona || {}), personality: a };
|
|
696
|
+
fs.mkdirSync(cfgDir, { recursive: true });
|
|
697
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
|
|
698
|
+
console.log(`active personality: ${a}`);
|
|
699
|
+
return 0;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
console.error(`Unknown personality subcommand: ${sub}`);
|
|
703
|
+
return 2;
|
|
704
|
+
}
|
|
705
|
+
|
|
631
706
|
async function cmdConfigEdit() {
|
|
632
707
|
// Open config.json in $EDITOR (or sensible default), then validate
|
|
633
708
|
// the result before letting the user walk away believing the edit
|
|
@@ -674,10 +749,10 @@ function cmdConfigSet(key, value) {
|
|
|
674
749
|
function applyOnboardConfig(currentCfg, flags) {
|
|
675
750
|
// Honors the OpenClaw-style unified provider/model string ("anthropic/claude-opus-4-7")
|
|
676
751
|
// by splitting it, but explicit --provider always wins.
|
|
677
|
-
const {
|
|
752
|
+
const { parseSlashProviderModel } = require_registry_sync();
|
|
678
753
|
const next = { ...currentCfg };
|
|
679
754
|
if (flags.model) {
|
|
680
|
-
const parsed =
|
|
755
|
+
const parsed = parseSlashProviderModel(flags.model);
|
|
681
756
|
if (parsed.provider && !flags.provider) next.provider = parsed.provider;
|
|
682
757
|
next.model = parsed.model || flags.model;
|
|
683
758
|
}
|
|
@@ -914,6 +989,8 @@ const SUBCOMMANDS = [
|
|
|
914
989
|
'daemon', 'version', 'completion', 'help',
|
|
915
990
|
'export', 'import',
|
|
916
991
|
'rates',
|
|
992
|
+
// v5.0 — sandbox 6-backend (Phase D)
|
|
993
|
+
'sandbox',
|
|
917
994
|
// OpenClaw-parity subsurfaces (v3.93–v3.98)
|
|
918
995
|
'auth', 'pairing', 'nodes', 'message', 'workspace', 'browse', 'cron',
|
|
919
996
|
// v3.99.6 — multi-step setup wizard + lazyclaw-only dashboard
|
|
@@ -924,20 +1001,31 @@ const SUBCOMMANDS = [
|
|
|
924
1001
|
'loop', 'loops', 'goal', 'memory',
|
|
925
1002
|
// v4.0.0 — Slack Socket Mode listener (inbound DM / @-mention)
|
|
926
1003
|
'slack',
|
|
1004
|
+
// v4.3.0 — Telegram long-poll listener (zero-install mobile control)
|
|
1005
|
+
'telegram',
|
|
1006
|
+
// v4.3.0 — Matrix /sync long-poll listener
|
|
1007
|
+
'matrix',
|
|
1008
|
+
// v5.0 — channels plugin loader (Phase F)
|
|
1009
|
+
'channels',
|
|
927
1010
|
// v4.1.0 — multi-agent slack system (Phase 9+)
|
|
928
1011
|
'agent', 'team', 'task',
|
|
1012
|
+
// v5.0 Phase G — persona compose + cross-tool import (spec §9, §10)
|
|
1013
|
+
'personality', 'migrate', 'hermes', 'openclaw',
|
|
1014
|
+
// v5.0 Phase H1 — trajectory exporter (spec §2.7)
|
|
1015
|
+
'trajectories',
|
|
929
1016
|
];
|
|
930
1017
|
|
|
931
1018
|
const SUBCOMMAND_SUBS = {
|
|
932
1019
|
config: ['get', 'set', 'list', 'delete', 'unset', 'path', 'edit', 'validate'],
|
|
933
1020
|
sessions: ['list', 'show', 'clear', 'export', 'search'],
|
|
934
|
-
skills: ['list', 'show', 'install', 'remove', 'search'],
|
|
1021
|
+
skills: ['list', 'show', 'install', 'remove', 'search', 'curate', 'classify'],
|
|
935
1022
|
providers: ['list', 'info', 'test', 'add', 'remove', 'models'],
|
|
936
1023
|
rates: ['list', 'set', 'delete', 'shape', 'validate', 'copy'],
|
|
1024
|
+
sandbox: ['list', 'test', 'add', 'use'],
|
|
937
1025
|
completion: ['bash', 'zsh'],
|
|
938
1026
|
auth: ['list', 'add', 'remove', 'use', 'rotate'],
|
|
939
1027
|
pairing: ['list', 'add', 'remove'],
|
|
940
|
-
nodes: ['list', 'register', 'remove'],
|
|
1028
|
+
nodes: ['list', 'register', 'remove', 'pending', 'approve', 'revoke', 'devices'],
|
|
941
1029
|
message: ['list', 'add', 'remove', 'send'],
|
|
942
1030
|
workspace: ['list', 'init', 'show', 'remove', 'path'],
|
|
943
1031
|
cron: ['list', 'add', 'remove', 'show', 'sync', 'run'],
|
|
@@ -946,9 +1034,12 @@ const SUBCOMMAND_SUBS = {
|
|
|
946
1034
|
goal: ['add', 'list', 'show', 'close', 'switch', 'tick', 'channel'],
|
|
947
1035
|
memory: ['show', 'dream', 'edit'],
|
|
948
1036
|
slack: ['listen'],
|
|
1037
|
+
telegram: ['listen'],
|
|
1038
|
+
matrix: ['listen'],
|
|
949
1039
|
agent: ['add', 'list', 'show', 'edit', 'remove'],
|
|
950
1040
|
team: ['add', 'list', 'show', 'edit', 'remove'],
|
|
951
1041
|
task: ['start', 'list', 'show', 'abandon', 'done', 'remove'],
|
|
1042
|
+
trajectories: ['export'],
|
|
952
1043
|
};
|
|
953
1044
|
|
|
954
1045
|
function bashCompletion() {
|
|
@@ -1197,6 +1288,7 @@ const HELP_DETAILS = {
|
|
|
1197
1288
|
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).',
|
|
1198
1289
|
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.',
|
|
1199
1290
|
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).',
|
|
1291
|
+
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',
|
|
1200
1292
|
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.',
|
|
1201
1293
|
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.',
|
|
1202
1294
|
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.',
|
|
@@ -2283,6 +2375,38 @@ async function cmdChat(flags = {}) {
|
|
|
2283
2375
|
|
|
2284
2376
|
// Top-of-session banner so the user can see at a glance what they're
|
|
2285
2377
|
// talking to. Cheap (no provider call) and TTY-only.
|
|
2378
|
+
// v5 ink splash + REPL when stdin is a real TTY and the user has not
|
|
2379
|
+
// opted out via LAZYCLAW_NO_INK=1. Non-TTY pipelines and the opt-out
|
|
2380
|
+
// env var fall through to the v4 figlet + readline path unchanged.
|
|
2381
|
+
const __useInkSplash = process.stdout.isTTY && !process.env.LAZYCLAW_NO_INK;
|
|
2382
|
+
if (__useInkSplash) {
|
|
2383
|
+
try {
|
|
2384
|
+
const React = (await import('react')).default;
|
|
2385
|
+
const { render } = await import('ink');
|
|
2386
|
+
const { ReplApp } = await import('./tui/repl.mjs');
|
|
2387
|
+
const { renderSplashToString } = await import('./tui/splash.mjs');
|
|
2388
|
+
// narrow-terminal fallback: <60 cols falls back to v4
|
|
2389
|
+
if ((process.stdout.columns || 80) < 60) throw new Error('narrow-terminal');
|
|
2390
|
+
const splashProps = {
|
|
2391
|
+
provider: activeProvName, model: activeModel,
|
|
2392
|
+
trainer: {}, sessionId: flags.session || '',
|
|
2393
|
+
cwd: process.cwd(),
|
|
2394
|
+
tools: [], skills: [],
|
|
2395
|
+
};
|
|
2396
|
+
void renderSplashToString; // surfaced for tests; runtime uses <Splash/>
|
|
2397
|
+
const ink = render(/* @__PURE__ */ React.createElement(ReplApp, {
|
|
2398
|
+
splashProps,
|
|
2399
|
+
runTurn: async (_text, _signal) => { void _text; void _signal; },
|
|
2400
|
+
}));
|
|
2401
|
+
await ink.waitUntilExit();
|
|
2402
|
+
return;
|
|
2403
|
+
} catch (e) {
|
|
2404
|
+
// Fall through to legacy path on any ink failure (missing import,
|
|
2405
|
+
// narrow terminal, sandboxed stdout).
|
|
2406
|
+
if (process.env.LAZYCLAW_DEBUG) console.error('[ink] fallback:', e.message);
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
// ─── legacy v4 path (unchanged) ─────────────────────────────────
|
|
2286
2410
|
_printChatBanner(activeProvName, activeModel, readVersionFromRepo());
|
|
2287
2411
|
|
|
2288
2412
|
const readline = await import('node:readline');
|
|
@@ -2464,8 +2588,8 @@ async function cmdChat(flags = {}) {
|
|
|
2464
2588
|
}
|
|
2465
2589
|
// Honor unified provider/model: `/model anthropic/claude-opus-4-7`
|
|
2466
2590
|
// splits and switches both.
|
|
2467
|
-
const {
|
|
2468
|
-
const parsed =
|
|
2591
|
+
const { parseSlashProviderModel } = _registryMod;
|
|
2592
|
+
const parsed = parseSlashProviderModel(arg);
|
|
2469
2593
|
if (parsed.provider) {
|
|
2470
2594
|
const next = lookupProv(parsed.provider);
|
|
2471
2595
|
if (!next) {
|
|
@@ -2925,6 +3049,53 @@ async function cmdChat(flags = {}) {
|
|
|
2925
3049
|
}
|
|
2926
3050
|
return true;
|
|
2927
3051
|
}
|
|
3052
|
+
case '/handoff': {
|
|
3053
|
+
// /handoff <target-channel> <externalId> [--note=...] — migrates the
|
|
3054
|
+
// active thread (bound to replState.channel / replState.externalId)
|
|
3055
|
+
// to a new channel and posts transition stubs on both sides. In the
|
|
3056
|
+
// local-only chat REPL there is no bound channel, so we surface a
|
|
3057
|
+
// clear error and stay in the REPL (acceptance test §F).
|
|
3058
|
+
const parts = line.trim().split(/\s+/).slice(1);
|
|
3059
|
+
if (parts.length < 2) {
|
|
3060
|
+
process.stderr.write('usage: /handoff <target-channel> <externalId> [--note=...]\n');
|
|
3061
|
+
return true;
|
|
3062
|
+
}
|
|
3063
|
+
const target = parts[0];
|
|
3064
|
+
const externalId = parts[1];
|
|
3065
|
+
const note = (parts.find(p => p.startsWith('--note=')) || '').slice(7);
|
|
3066
|
+
try {
|
|
3067
|
+
const { openThreads } = await import('./channels/threads.mjs');
|
|
3068
|
+
const { runHandoff } = await import('./channels/handoff.mjs');
|
|
3069
|
+
const threads = openThreads(cfgDir);
|
|
3070
|
+
const replState = globalThis.__lazyclawReplState || {};
|
|
3071
|
+
const cur = replState.channel && replState.externalId
|
|
3072
|
+
? threads.findByExternal(replState.channel, replState.externalId)
|
|
3073
|
+
: null;
|
|
3074
|
+
if (!cur) {
|
|
3075
|
+
process.stderr.write(
|
|
3076
|
+
`handoff: no thread bound to ${replState.channel || '(none)'}:${replState.externalId || '(none)'}\n`,
|
|
3077
|
+
);
|
|
3078
|
+
return true;
|
|
3079
|
+
}
|
|
3080
|
+
const next = await runHandoff({
|
|
3081
|
+
threads, channels: replState.channels || {},
|
|
3082
|
+
threadId: cur.threadId, target, externalId, note,
|
|
3083
|
+
});
|
|
3084
|
+
process.stdout.write(`handoff -> ${next.channel}:${next.externalId} (session ${next.sessionId})\n`);
|
|
3085
|
+
replState.channel = next.channel;
|
|
3086
|
+
replState.externalId = next.externalId;
|
|
3087
|
+
} catch (e) {
|
|
3088
|
+
process.stderr.write(`handoff failed: ${e.code || 'ERR'}: ${e.message}\n`);
|
|
3089
|
+
}
|
|
3090
|
+
return true;
|
|
3091
|
+
}
|
|
3092
|
+
case '/personality': {
|
|
3093
|
+
// Phase G: thin slash wrapper over cmdPersonality.
|
|
3094
|
+
const tail = line.slice('/personality'.length).trim();
|
|
3095
|
+
const parts = tail.split(/\s+/).filter(Boolean);
|
|
3096
|
+
await cmdPersonality(parts[0] || 'list', parts[1], parts[2]);
|
|
3097
|
+
return true;
|
|
3098
|
+
}
|
|
2928
3099
|
case '/exit': {
|
|
2929
3100
|
return 'EXIT';
|
|
2930
3101
|
}
|
|
@@ -3577,8 +3748,51 @@ async function cmdNodes(sub, positional, flags = {}) {
|
|
|
3577
3748
|
console.log(JSON.stringify({ ok: true, removed: id }));
|
|
3578
3749
|
return;
|
|
3579
3750
|
}
|
|
3751
|
+
// Device-gateway pairing (Phase 27) — distinct from the config-based
|
|
3752
|
+
// `nodes register` table above. These drive the Ed25519 PairingStore
|
|
3753
|
+
// a companion node authenticates against via `lazyclaw daemon`.
|
|
3754
|
+
case 'pending': {
|
|
3755
|
+
const { PairingStore } = await import('./gateway/device_auth.mjs');
|
|
3756
|
+
const store = new PairingStore(path.dirname(configPath()));
|
|
3757
|
+
console.log(JSON.stringify(store.pending(), null, 2));
|
|
3758
|
+
return;
|
|
3759
|
+
}
|
|
3760
|
+
case 'devices': {
|
|
3761
|
+
const { PairingStore } = await import('./gateway/device_auth.mjs');
|
|
3762
|
+
const store = new PairingStore(path.dirname(configPath()));
|
|
3763
|
+
console.log(JSON.stringify(store.devicesList(), null, 2));
|
|
3764
|
+
return;
|
|
3765
|
+
}
|
|
3766
|
+
case 'approve': {
|
|
3767
|
+
const requestId = positional[0];
|
|
3768
|
+
if (!requestId) {
|
|
3769
|
+
console.error('Usage: lazyclaw nodes approve <requestId> (see `lazyclaw nodes pending`)');
|
|
3770
|
+
process.exit(2);
|
|
3771
|
+
}
|
|
3772
|
+
const { PairingStore } = await import('./gateway/device_auth.mjs');
|
|
3773
|
+
const store = new PairingStore(path.dirname(configPath()));
|
|
3774
|
+
try {
|
|
3775
|
+
const { deviceId } = store.approve(requestId);
|
|
3776
|
+
// The token is intentionally NOT printed — the device receives its
|
|
3777
|
+
// rotated token on its next /gateway/connect, so it never has to
|
|
3778
|
+
// pass through a terminal / shell history.
|
|
3779
|
+
console.log(JSON.stringify({ ok: true, approved: requestId, deviceId, note: 'device receives its token on next /gateway/connect' }));
|
|
3780
|
+
} catch (e) { console.error(`error: ${e.message}`); process.exit(1); }
|
|
3781
|
+
return;
|
|
3782
|
+
}
|
|
3783
|
+
case 'revoke': {
|
|
3784
|
+
const deviceId = positional[0];
|
|
3785
|
+
if (!deviceId) {
|
|
3786
|
+
console.error('Usage: lazyclaw nodes revoke <deviceId>');
|
|
3787
|
+
process.exit(2);
|
|
3788
|
+
}
|
|
3789
|
+
const { PairingStore } = await import('./gateway/device_auth.mjs');
|
|
3790
|
+
const store = new PairingStore(path.dirname(configPath()));
|
|
3791
|
+
console.log(JSON.stringify(store.revoke(deviceId)));
|
|
3792
|
+
return;
|
|
3793
|
+
}
|
|
3580
3794
|
default:
|
|
3581
|
-
console.error('Usage: lazyclaw nodes <list|register|remove> ...');
|
|
3795
|
+
console.error('Usage: lazyclaw nodes <list|register|remove|pending|approve <requestId>|revoke <deviceId>|devices> ...');
|
|
3582
3796
|
process.exit(2);
|
|
3583
3797
|
}
|
|
3584
3798
|
}
|
|
@@ -4382,7 +4596,7 @@ async function cmdMemory(sub, positional, flags = {}) {
|
|
|
4382
4596
|
}
|
|
4383
4597
|
}
|
|
4384
4598
|
|
|
4385
|
-
const AGENT_REG_SUBS = new Set(['add', 'list', 'show', 'edit', 'remove', 'rm', 'delete', 'memory', 'reflect']);
|
|
4599
|
+
const AGENT_REG_SUBS = new Set(['add', 'list', 'show', 'edit', 'remove', 'rm', 'delete', 'memory', 'reflect', 'skill-synth']);
|
|
4386
4600
|
const TEAM_SUBS = new Set(['add', 'list', 'show', 'edit', 'remove', 'rm', 'delete']);
|
|
4387
4601
|
const TASK_SUBS = new Set(['start', 'tick', 'list', 'show', 'abandon', 'done', 'transcript', 'remove', 'rm', 'delete']);
|
|
4388
4602
|
|
|
@@ -4510,14 +4724,36 @@ async function cmdTask(sub, positional, flags = {}) {
|
|
|
4510
4724
|
const cfg = readConfig();
|
|
4511
4725
|
const leadAgent = agentsById[team.lead];
|
|
4512
4726
|
const apiKey = _resolveAuthKey(cfg, leadAgent.provider);
|
|
4513
|
-
// Per-provider base-url override
|
|
4514
|
-
//
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4727
|
+
// Per-provider base-url override (tests point this at a local mock;
|
|
4728
|
+
// production leaves it unset for the built-in default).
|
|
4729
|
+
const baseUrl = _resolveBaseUrl(leadAgent.provider);
|
|
4730
|
+
// --approve-url turns on remote human-in-the-loop approval for the
|
|
4731
|
+
// sensitive tools (bash/write): each such call long-polls a running
|
|
4732
|
+
// daemon's `POST /exec/request`, which broadcasts to paired devices
|
|
4733
|
+
// over the gateway SSE and resolves when one approves. Fail-closed —
|
|
4734
|
+
// any endpoint error denies the call. Omit the flag → ungated (the
|
|
4735
|
+
// historical behavior).
|
|
4736
|
+
let approve;
|
|
4737
|
+
if (flags['approve-url']) {
|
|
4738
|
+
const approveUrl = String(flags['approve-url']).replace(/\/$/, '');
|
|
4739
|
+
const approveToken = flags['approve-token'] ? String(flags['approve-token']) : '';
|
|
4740
|
+
const approveTimeoutMs = flags['approve-timeout'] ? parseInt(flags['approve-timeout'], 10) : 120000;
|
|
4741
|
+
approve = async ({ tool, args, agent }) => {
|
|
4742
|
+
const summary = `${tool}: ${typeof args === 'object' ? JSON.stringify(args) : String(args)}`.slice(0, 400);
|
|
4743
|
+
try {
|
|
4744
|
+
const r = await fetch(`${approveUrl}/exec/request`, {
|
|
4745
|
+
method: 'POST',
|
|
4746
|
+
headers: { 'content-type': 'application/json', ...(approveToken ? { authorization: `Bearer ${approveToken}` } : {}) },
|
|
4747
|
+
body: JSON.stringify({ tool, agentId: agent, summary, timeoutMs: approveTimeoutMs }),
|
|
4748
|
+
});
|
|
4749
|
+
if (!r.ok) return { approved: false, reason: `approval endpoint HTTP ${r.status}` };
|
|
4750
|
+
const j = await r.json();
|
|
4751
|
+
return { approved: !!j.approved, reason: j.reason || (j.approved ? 'approved' : 'denied') };
|
|
4752
|
+
} catch (err) {
|
|
4753
|
+
return { approved: false, reason: `approval request failed: ${err?.message || err}` };
|
|
4754
|
+
}
|
|
4755
|
+
};
|
|
4756
|
+
}
|
|
4521
4757
|
try {
|
|
4522
4758
|
const result = await router.runTaskTurn({
|
|
4523
4759
|
task, team, agentsById,
|
|
@@ -4527,6 +4763,7 @@ async function cmdTask(sub, positional, flags = {}) {
|
|
|
4527
4763
|
baseUrl,
|
|
4528
4764
|
logger: (line) => process.stderr.write(line),
|
|
4529
4765
|
maxAgentTurns: flags['max-turns'] ? parseInt(flags['max-turns'], 10) : undefined,
|
|
4766
|
+
approve,
|
|
4530
4767
|
});
|
|
4531
4768
|
emitJson({ id: result.task.id, status: result.task.status, iterations: result.iterations, stoppedBy: result.stoppedBy });
|
|
4532
4769
|
} catch (err) {
|
|
@@ -4685,7 +4922,7 @@ async function cmdAgentRegistry(sub, positional, flags = {}) {
|
|
|
4685
4922
|
return;
|
|
4686
4923
|
}
|
|
4687
4924
|
case 'add': {
|
|
4688
|
-
if (!name) { console.error('Usage: lazyclaw agent add <name> [--role "..."] [--provider X] [--model Y] [--display "..."] [--tools bash,read,write,grep] [--tags a,b]'); process.exit(2); }
|
|
4925
|
+
if (!name) { console.error('Usage: lazyclaw agent add <name> [--role "..."] [--provider X] [--model Y] [--display "..."] [--tools bash,read,write,grep,skill_view] [--tags a,b] [--skill-write auto|manual|off]'); process.exit(2); }
|
|
4689
4926
|
const tools = agentsMod.parseToolsFlag(flags.tools);
|
|
4690
4927
|
try {
|
|
4691
4928
|
const a = agentsMod.registerAgent({
|
|
@@ -4696,6 +4933,7 @@ async function cmdAgentRegistry(sub, positional, flags = {}) {
|
|
|
4696
4933
|
model: flags.model || '',
|
|
4697
4934
|
tools: tools === null ? undefined : tools,
|
|
4698
4935
|
tags: agentsMod.parseToolsFlag(flags.tags) || [],
|
|
4936
|
+
skillWrite: flags['skill-write'],
|
|
4699
4937
|
}, cfgDir);
|
|
4700
4938
|
emitJson(a);
|
|
4701
4939
|
} catch (err) {
|
|
@@ -4712,7 +4950,7 @@ async function cmdAgentRegistry(sub, positional, flags = {}) {
|
|
|
4712
4950
|
return;
|
|
4713
4951
|
}
|
|
4714
4952
|
case 'edit': {
|
|
4715
|
-
if (!name) { console.error('Usage: lazyclaw agent edit <name> [--role "..."] [--provider X] [--model Y] [--display "..."] [--tools ...]'); process.exit(2); }
|
|
4953
|
+
if (!name) { console.error('Usage: lazyclaw agent edit <name> [--role "..."] [--provider X] [--model Y] [--display "..."] [--tools ...] [--skill-write auto|manual|off] [--memory-write auto|manual|off]'); process.exit(2); }
|
|
4716
4954
|
const patch = {};
|
|
4717
4955
|
if (flags.role !== undefined) patch.role = String(flags.role);
|
|
4718
4956
|
if (flags.provider !== undefined) patch.provider = String(flags.provider);
|
|
@@ -4721,6 +4959,8 @@ async function cmdAgentRegistry(sub, positional, flags = {}) {
|
|
|
4721
4959
|
if (flags['display-name'] !== undefined) patch.displayName = String(flags['display-name']);
|
|
4722
4960
|
if (flags.tools !== undefined) patch.tools = agentsMod.parseToolsFlag(flags.tools);
|
|
4723
4961
|
if (flags.tags !== undefined) patch.tags = agentsMod.parseToolsFlag(flags.tags);
|
|
4962
|
+
if (flags['skill-write'] !== undefined) patch.skillWrite = String(flags['skill-write']);
|
|
4963
|
+
if (flags['memory-write'] !== undefined) patch.memoryWrite = String(flags['memory-write']);
|
|
4724
4964
|
if (Object.keys(patch).length === 0) {
|
|
4725
4965
|
console.error('agent edit: no fields to update');
|
|
4726
4966
|
process.exit(2);
|
|
@@ -4796,11 +5036,7 @@ async function cmdAgentRegistry(sub, positional, flags = {}) {
|
|
|
4796
5036
|
try { _loadDotenvIfAny(cfgDir); } catch { /* best-effort */ }
|
|
4797
5037
|
const cfg = readConfig();
|
|
4798
5038
|
const apiKey = _resolveAuthKey(cfg, a.provider);
|
|
4799
|
-
const baseUrl =
|
|
4800
|
-
anthropic: process.env.LAZYCLAW_ANTHROPIC_BASE_URL,
|
|
4801
|
-
openai: process.env.LAZYCLAW_OPENAI_BASE_URL,
|
|
4802
|
-
gemini: process.env.LAZYCLAW_GEMINI_BASE_URL,
|
|
4803
|
-
}[a.provider] || undefined;
|
|
5039
|
+
const baseUrl = _resolveBaseUrl(a.provider);
|
|
4804
5040
|
try {
|
|
4805
5041
|
const body = await memMod.reflectOnce({ agent: a, task, apiKey, baseUrl });
|
|
4806
5042
|
if (!body || !body.trim()) {
|
|
@@ -4817,8 +5053,50 @@ async function cmdAgentRegistry(sub, positional, flags = {}) {
|
|
|
4817
5053
|
}
|
|
4818
5054
|
return;
|
|
4819
5055
|
}
|
|
5056
|
+
case 'skill-synth': {
|
|
5057
|
+
const aname = positional[0];
|
|
5058
|
+
const taskId = flags.task || positional[1];
|
|
5059
|
+
if (!aname || !taskId) {
|
|
5060
|
+
console.error('Usage: lazyclaw agent skill-synth <name> --task <id> [--dry-run]');
|
|
5061
|
+
process.exit(2);
|
|
5062
|
+
}
|
|
5063
|
+
const tasksMod = await import('./tasks.mjs');
|
|
5064
|
+
const synthMod = await import('./mas/skill_synth.mjs');
|
|
5065
|
+
const skillsMod = await import('./skills.mjs');
|
|
5066
|
+
const a = agentsMod.getAgent(aname, cfgDir);
|
|
5067
|
+
if (!a) { console.error(`agent skill-synth: no agent "${aname}"`); process.exit(2); }
|
|
5068
|
+
const task = tasksMod.getTask(taskId, cfgDir);
|
|
5069
|
+
if (!task) { console.error(`agent skill-synth: no task "${taskId}"`); process.exit(2); }
|
|
5070
|
+
try { _loadDotenvIfAny(cfgDir); } catch { /* best-effort */ }
|
|
5071
|
+
const cfg = readConfig();
|
|
5072
|
+
const apiKey = _resolveAuthKey(cfg, a.provider);
|
|
5073
|
+
const baseUrl = _resolveBaseUrl(a.provider);
|
|
5074
|
+
try {
|
|
5075
|
+
const result = await synthMod.synthesizeSkill({ agent: a, task, apiKey, baseUrl });
|
|
5076
|
+
if (!result) {
|
|
5077
|
+
process.stderr.write('skill synthesis produced nothing worth saving\n');
|
|
5078
|
+
return;
|
|
5079
|
+
}
|
|
5080
|
+
if (!flags['dry-run']) {
|
|
5081
|
+
// installSynthesized reserves a collision-free name (never
|
|
5082
|
+
// clobbers a human-authored skill) and version-bumps when it
|
|
5083
|
+
// improves its own prior skill.
|
|
5084
|
+
const installed = synthMod.installSynthesized(
|
|
5085
|
+
{ name: result.name, description: result.description, body: result.body, sourceTask: task.id },
|
|
5086
|
+
cfgDir,
|
|
5087
|
+
);
|
|
5088
|
+
emitJson({ skill: installed.skill, description: result.description, version: installed.version, path: installed.path });
|
|
5089
|
+
} else {
|
|
5090
|
+
process.stdout.write(result.doc + (result.doc.endsWith('\n') ? '' : '\n'));
|
|
5091
|
+
}
|
|
5092
|
+
} catch (err) {
|
|
5093
|
+
console.error(`agent skill-synth: ${err?.message || err}`);
|
|
5094
|
+
process.exit(2);
|
|
5095
|
+
}
|
|
5096
|
+
return;
|
|
5097
|
+
}
|
|
4820
5098
|
default:
|
|
4821
|
-
console.error('Usage: lazyclaw agent <add|list|show|edit|remove|memory|reflect> ...');
|
|
5099
|
+
console.error('Usage: lazyclaw agent <add|list|show|edit|remove|memory|reflect|skill-synth> ...');
|
|
4822
5100
|
process.exit(2);
|
|
4823
5101
|
}
|
|
4824
5102
|
}
|
|
@@ -4929,6 +5207,165 @@ async function cmdSlack(sub, positional, flags = {}) {
|
|
|
4929
5207
|
});
|
|
4930
5208
|
}
|
|
4931
5209
|
|
|
5210
|
+
// `lazyclaw telegram listen` — zero-install mobile control surface.
|
|
5211
|
+
// Long-polls the Telegram Bot API (no public URL / webhook needed) and
|
|
5212
|
+
// pipes each inbound message through the active provider, replying in
|
|
5213
|
+
// the same chat. Mirrors `slack listen`. Access is gated by the existing
|
|
5214
|
+
// `pairing` allowlist (Telegram numeric user ids); an empty allowlist
|
|
5215
|
+
// means "reply to anyone who can reach the bot".
|
|
5216
|
+
async function cmdTelegram(sub, positional, flags = {}) {
|
|
5217
|
+
if (sub !== 'listen') {
|
|
5218
|
+
console.error('Usage: lazyclaw telegram listen [--provider X] [--model Y]\n Long-polls the Telegram Bot API. Set TELEGRAM_BOT_TOKEN in ~/.lazyclaw/.env.\n Restrict who can talk to it with `lazyclaw pairing add <telegram-user-id>`.');
|
|
5219
|
+
process.exit(2);
|
|
5220
|
+
}
|
|
5221
|
+
await ensureRegistry();
|
|
5222
|
+
const cfg = readConfig();
|
|
5223
|
+
const cfgDir = path.dirname(configPath());
|
|
5224
|
+
|
|
5225
|
+
const envInfo = _loadDotenvIfAny(cfgDir);
|
|
5226
|
+
process.stderr.write(`[telegram] .env: ${envInfo.loaded} keys loaded from ${envInfo.path}\n`);
|
|
5227
|
+
|
|
5228
|
+
const provName = flags.provider || cfg.provider || 'mock';
|
|
5229
|
+
const prov = _registryMod.PROVIDERS[provName];
|
|
5230
|
+
if (!prov) { console.error(`unknown provider: ${provName}`); process.exit(2); }
|
|
5231
|
+
const model = flags.model || cfg.model;
|
|
5232
|
+
|
|
5233
|
+
const threadMsgs = new Map();
|
|
5234
|
+
const MAX_TURNS = 20;
|
|
5235
|
+
|
|
5236
|
+
const handler = async ({ threadId, text }) => {
|
|
5237
|
+
const cleaned = String(text || '').trim();
|
|
5238
|
+
if (!cleaned) { process.stderr.write('[telegram] dropping empty inbound\n'); return null; }
|
|
5239
|
+
const msgs = threadMsgs.get(threadId) || [];
|
|
5240
|
+
msgs.push({ role: 'user', content: cleaned });
|
|
5241
|
+
let acc = '';
|
|
5242
|
+
try {
|
|
5243
|
+
for await (const chunk of prov.sendMessage(msgs, { apiKey: _resolveAuthKey(cfg, provName), model })) acc += chunk;
|
|
5244
|
+
} catch (err) {
|
|
5245
|
+
msgs.pop();
|
|
5246
|
+
const why = err?.message || String(err);
|
|
5247
|
+
process.stderr.write(`[telegram] provider error: ${why}\n`);
|
|
5248
|
+
return `(provider error: ${why})`;
|
|
5249
|
+
}
|
|
5250
|
+
msgs.push({ role: 'assistant', content: acc });
|
|
5251
|
+
if (msgs.length > MAX_TURNS) msgs.splice(0, msgs.length - MAX_TURNS);
|
|
5252
|
+
threadMsgs.set(threadId, msgs);
|
|
5253
|
+
if (!acc.trim()) { process.stderr.write('[telegram] provider returned empty text — not posting\n'); return null; }
|
|
5254
|
+
return acc;
|
|
5255
|
+
};
|
|
5256
|
+
|
|
5257
|
+
// The pairing allowlist doubles as the Telegram sender allowlist.
|
|
5258
|
+
const allowlist = (cfg.pairing || []).map((p) => String(p.id));
|
|
5259
|
+
const { TelegramChannel } = await import('./channels/telegram.mjs');
|
|
5260
|
+
let ch;
|
|
5261
|
+
try {
|
|
5262
|
+
ch = new TelegramChannel({ allowlist: allowlist.length ? allowlist : null });
|
|
5263
|
+
} catch (err) {
|
|
5264
|
+
console.error(`telegram: ${err?.message || err}`);
|
|
5265
|
+
process.exit(2);
|
|
5266
|
+
}
|
|
5267
|
+
process.stderr.write(`[telegram] provider=${provName} model=${model || '(default)'} allowlist=${allowlist.length || 'open'}\n`);
|
|
5268
|
+
try {
|
|
5269
|
+
await ch.start(handler, { poll: true, logger: (line) => process.stderr.write(line) });
|
|
5270
|
+
} catch (err) {
|
|
5271
|
+
if (err?.code === 'TELEGRAM_MISSING_TOKEN') {
|
|
5272
|
+
console.error('telegram: TELEGRAM_BOT_TOKEN not set');
|
|
5273
|
+
console.error(`hint: add TELEGRAM_BOT_TOKEN=... to ${path.join(cfgDir, '.env')}`);
|
|
5274
|
+
} else {
|
|
5275
|
+
console.error(`telegram: ${err?.message || err}`);
|
|
5276
|
+
}
|
|
5277
|
+
process.exit(2);
|
|
5278
|
+
}
|
|
5279
|
+
process.stderr.write(`[telegram] listening. Ctrl-C to stop.\n`);
|
|
5280
|
+
|
|
5281
|
+
await new Promise((resolve) => {
|
|
5282
|
+
const onSig = async () => {
|
|
5283
|
+
process.stderr.write(`\n[telegram] shutting down…\n`);
|
|
5284
|
+
try { await ch.stop(); } catch { /* best-effort */ }
|
|
5285
|
+
resolve();
|
|
5286
|
+
};
|
|
5287
|
+
process.once('SIGINT', onSig);
|
|
5288
|
+
process.once('SIGTERM', onSig);
|
|
5289
|
+
});
|
|
5290
|
+
}
|
|
5291
|
+
|
|
5292
|
+
// `lazyclaw matrix listen` — Matrix inbound over the client-server API's
|
|
5293
|
+
// long-poll /sync (no SDK). Mirrors `telegram listen`. Set MATRIX_HOMESERVER
|
|
5294
|
+
// + MATRIX_ACCESS_TOKEN (+ MATRIX_USER_ID for self-filtering) in ~/.lazyclaw/.env.
|
|
5295
|
+
async function cmdMatrix(sub, positional, flags = {}) {
|
|
5296
|
+
if (sub !== 'listen') {
|
|
5297
|
+
console.error('Usage: lazyclaw matrix listen [--provider X] [--model Y]\n Long-polls the Matrix /sync API. Set MATRIX_HOMESERVER + MATRIX_ACCESS_TOKEN (+ MATRIX_USER_ID) in ~/.lazyclaw/.env.\n Restrict who can talk to it with `lazyclaw pairing add <@user:server>`.');
|
|
5298
|
+
process.exit(2);
|
|
5299
|
+
}
|
|
5300
|
+
await ensureRegistry();
|
|
5301
|
+
const cfg = readConfig();
|
|
5302
|
+
const cfgDir = path.dirname(configPath());
|
|
5303
|
+
|
|
5304
|
+
const envInfo = _loadDotenvIfAny(cfgDir);
|
|
5305
|
+
process.stderr.write(`[matrix] .env: ${envInfo.loaded} keys loaded from ${envInfo.path}\n`);
|
|
5306
|
+
|
|
5307
|
+
const provName = flags.provider || cfg.provider || 'mock';
|
|
5308
|
+
const prov = _registryMod.PROVIDERS[provName];
|
|
5309
|
+
if (!prov) { console.error(`unknown provider: ${provName}`); process.exit(2); }
|
|
5310
|
+
const model = flags.model || cfg.model;
|
|
5311
|
+
|
|
5312
|
+
const threadMsgs = new Map();
|
|
5313
|
+
const MAX_TURNS = 20;
|
|
5314
|
+
const handler = async ({ threadId, text }) => {
|
|
5315
|
+
const cleaned = String(text || '').trim();
|
|
5316
|
+
if (!cleaned) { process.stderr.write('[matrix] dropping empty inbound\n'); return null; }
|
|
5317
|
+
const msgs = threadMsgs.get(threadId) || [];
|
|
5318
|
+
msgs.push({ role: 'user', content: cleaned });
|
|
5319
|
+
let acc = '';
|
|
5320
|
+
try {
|
|
5321
|
+
for await (const chunk of prov.sendMessage(msgs, { apiKey: _resolveAuthKey(cfg, provName), model })) acc += chunk;
|
|
5322
|
+
} catch (err) {
|
|
5323
|
+
msgs.pop();
|
|
5324
|
+
const why = err?.message || String(err);
|
|
5325
|
+
process.stderr.write(`[matrix] provider error: ${why}\n`);
|
|
5326
|
+
return `(provider error: ${why})`;
|
|
5327
|
+
}
|
|
5328
|
+
msgs.push({ role: 'assistant', content: acc });
|
|
5329
|
+
if (msgs.length > MAX_TURNS) msgs.splice(0, msgs.length - MAX_TURNS);
|
|
5330
|
+
threadMsgs.set(threadId, msgs);
|
|
5331
|
+
if (!acc.trim()) { process.stderr.write('[matrix] provider returned empty text — not posting\n'); return null; }
|
|
5332
|
+
return acc;
|
|
5333
|
+
};
|
|
5334
|
+
|
|
5335
|
+
const allowlist = (cfg.pairing || []).map((p) => String(p.id));
|
|
5336
|
+
const { MatrixChannel } = await import('./channels/matrix.mjs');
|
|
5337
|
+
let ch;
|
|
5338
|
+
try {
|
|
5339
|
+
ch = new MatrixChannel({ allowlist: allowlist.length ? allowlist : null });
|
|
5340
|
+
} catch (err) {
|
|
5341
|
+
console.error(`matrix: ${err?.message || err}`);
|
|
5342
|
+
process.exit(2);
|
|
5343
|
+
}
|
|
5344
|
+
process.stderr.write(`[matrix] provider=${provName} model=${model || '(default)'} allowlist=${allowlist.length || 'open'}\n`);
|
|
5345
|
+
try {
|
|
5346
|
+
await ch.start(handler, { poll: true, logger: (line) => process.stderr.write(line) });
|
|
5347
|
+
} catch (err) {
|
|
5348
|
+
if (err?.code === 'MATRIX_MISSING_TOKEN' || err?.code === 'MATRIX_MISSING_HOMESERVER') {
|
|
5349
|
+
console.error(`matrix: ${err.message}`);
|
|
5350
|
+
console.error(`hint: set MATRIX_HOMESERVER and MATRIX_ACCESS_TOKEN in ${path.join(cfgDir, '.env')}`);
|
|
5351
|
+
} else {
|
|
5352
|
+
console.error(`matrix: ${err?.message || err}`);
|
|
5353
|
+
}
|
|
5354
|
+
process.exit(2);
|
|
5355
|
+
}
|
|
5356
|
+
process.stderr.write(`[matrix] listening. Ctrl-C to stop.\n`);
|
|
5357
|
+
|
|
5358
|
+
await new Promise((resolve) => {
|
|
5359
|
+
const onSig = async () => {
|
|
5360
|
+
process.stderr.write(`\n[matrix] shutting down…\n`);
|
|
5361
|
+
try { await ch.stop(); } catch { /* best-effort */ }
|
|
5362
|
+
resolve();
|
|
5363
|
+
};
|
|
5364
|
+
process.once('SIGINT', onSig);
|
|
5365
|
+
process.once('SIGTERM', onSig);
|
|
5366
|
+
});
|
|
5367
|
+
}
|
|
5368
|
+
|
|
4932
5369
|
async function cmdSkills(sub, positional, flags = {}) {
|
|
4933
5370
|
const skillsMod = await import('./skills.mjs');
|
|
4934
5371
|
const cfgDir = path.dirname(configPath());
|
|
@@ -5120,8 +5557,24 @@ async function cmdSkills(sub, positional, flags = {}) {
|
|
|
5120
5557
|
console.log(JSON.stringify({ query, regex: useRegex, matches }, null, 2));
|
|
5121
5558
|
return;
|
|
5122
5559
|
}
|
|
5560
|
+
case 'curate': {
|
|
5561
|
+
// Lifecycle sweep: agent-authored skills unused >90d move into
|
|
5562
|
+
// skills/.archive/ (recoverable). Human-authored skills are never
|
|
5563
|
+
// moved. The real clock is injected here; the module stays pure.
|
|
5564
|
+
const curator = await import('./skills_curator.mjs');
|
|
5565
|
+
const r = curator.curate(cfgDir, Date.now());
|
|
5566
|
+
console.log(JSON.stringify(r, null, 2));
|
|
5567
|
+
return;
|
|
5568
|
+
}
|
|
5569
|
+
case 'classify': {
|
|
5570
|
+
const name = positional[0];
|
|
5571
|
+
if (!name) { console.error('Usage: lazyclaw skills classify <name>'); process.exit(2); }
|
|
5572
|
+
const curator = await import('./skills_curator.mjs');
|
|
5573
|
+
console.log(JSON.stringify({ name, state: curator.classify(name, cfgDir, Date.now()), usage: curator.usageOf(name, cfgDir) }, null, 2));
|
|
5574
|
+
return;
|
|
5575
|
+
}
|
|
5123
5576
|
default:
|
|
5124
|
-
console.error('Usage: lazyclaw skills <list|show <name>|install <name> [--from path]|remove <name>|search <query> [--regex]
|
|
5577
|
+
console.error('Usage: lazyclaw skills <list|show <name>|install <name> [--from path]|remove <name>|search <query> [--regex]|curate|classify <name>>');
|
|
5125
5578
|
process.exit(2);
|
|
5126
5579
|
}
|
|
5127
5580
|
}
|
|
@@ -5662,10 +6115,118 @@ async function cmdSessions(sub, positional, flags = {}) {
|
|
|
5662
6115
|
}
|
|
5663
6116
|
}
|
|
5664
6117
|
|
|
6118
|
+
// sandbox subcommands — list/test/add/use (Phase D).
|
|
6119
|
+
async function cmdSandbox(args, flags = {}) {
|
|
6120
|
+
const sub = args[0];
|
|
6121
|
+
|
|
6122
|
+
if (!sub || sub === 'list') {
|
|
6123
|
+
for (const kind of listBackends()) process.stdout.write(`${kind}\n`);
|
|
6124
|
+
return 0;
|
|
6125
|
+
}
|
|
6126
|
+
|
|
6127
|
+
if (sub === 'test') {
|
|
6128
|
+
const name = args[1];
|
|
6129
|
+
if (!name) { process.stderr.write('usage: lazyclaw sandbox test <kind|profile>\n'); return 2; }
|
|
6130
|
+
const cfg = _sandboxLoadConfigOrEmpty();
|
|
6131
|
+
// If `name` looks like a known kind, route to that kind. If it
|
|
6132
|
+
// is not a known kind AND not a profile in cfg, treat as an
|
|
6133
|
+
// unknown identifier and report SANDBOX_BAD_KIND.
|
|
6134
|
+
const isKind = listBackends().includes(name);
|
|
6135
|
+
const profile = cfg.sandbox && cfg.sandbox.profiles && cfg.sandbox.profiles[name];
|
|
6136
|
+
if (!isKind && !profile) {
|
|
6137
|
+
process.stderr.write(`SANDBOX_BAD_KIND: unknown sandbox kind or profile "${name}"\n`);
|
|
6138
|
+
return 1;
|
|
6139
|
+
}
|
|
6140
|
+
let sb;
|
|
6141
|
+
try {
|
|
6142
|
+
const synthCfg = isKind
|
|
6143
|
+
? { sandbox: { default: name, ...cfg.sandbox } }
|
|
6144
|
+
: cfg;
|
|
6145
|
+
sb = resolveSandbox(synthCfg);
|
|
6146
|
+
} catch (e) {
|
|
6147
|
+
process.stderr.write(`${e.code || 'SANDBOX_ERR'}: ${e.message}\n`); return 1;
|
|
6148
|
+
}
|
|
6149
|
+
if (sb.spec.kind !== 'local' && sb.spec.kind !== 'docker') {
|
|
6150
|
+
// Remote/serverless backends just construct argv in unit tests;
|
|
6151
|
+
// we report "shape-ok" without actually executing.
|
|
6152
|
+
process.stdout.write(`ok ${sb.spec.kind} (argv-shape)\n`);
|
|
6153
|
+
return 0;
|
|
6154
|
+
}
|
|
6155
|
+
const sess = await sb.open();
|
|
6156
|
+
try {
|
|
6157
|
+
const r = await sess.exec(['echo', 'lazyclaw-sandbox-test']);
|
|
6158
|
+
if (r.code !== 0 || !/lazyclaw-sandbox-test/.test(r.stdout)) {
|
|
6159
|
+
process.stderr.write(`fail ${name}: exit=${r.code} stdout=${r.stdout}\n`); return 1;
|
|
6160
|
+
}
|
|
6161
|
+
process.stdout.write(`ok ${name}\n`);
|
|
6162
|
+
return 0;
|
|
6163
|
+
} finally { await sess.close(); }
|
|
6164
|
+
}
|
|
6165
|
+
|
|
6166
|
+
if (sub === 'add') {
|
|
6167
|
+
const name = args[1];
|
|
6168
|
+
if (!name) { process.stderr.write('usage: lazyclaw sandbox add <name> --kind <kind> [...]\n'); return 2; }
|
|
6169
|
+
const opts = {};
|
|
6170
|
+
if (flags.kind) opts.kind = flags.kind;
|
|
6171
|
+
if (flags.image) opts.image = flags.image;
|
|
6172
|
+
if (flags.host) opts.host = flags.host;
|
|
6173
|
+
if (flags.user) opts.user = flags.user;
|
|
6174
|
+
if (flags.workspace) opts.workspace = flags.workspace;
|
|
6175
|
+
if (flags.app) opts.app = flags.app;
|
|
6176
|
+
if (flags.confiner) opts.confiner = flags.confiner;
|
|
6177
|
+
if (!listBackends().includes(opts.kind)) {
|
|
6178
|
+
process.stderr.write(`unknown kind "${opts.kind}"\n`); return 1;
|
|
6179
|
+
}
|
|
6180
|
+
const cfg = _sandboxLoadConfigOrEmpty();
|
|
6181
|
+
cfg.sandbox = cfg.sandbox || {};
|
|
6182
|
+
cfg.sandbox.profiles = cfg.sandbox.profiles || {};
|
|
6183
|
+
cfg.sandbox.profiles[name] = opts;
|
|
6184
|
+
_sandboxSaveConfig(cfg);
|
|
6185
|
+
process.stdout.write(`added profile ${name} (${opts.kind})\n`);
|
|
6186
|
+
return 0;
|
|
6187
|
+
}
|
|
6188
|
+
|
|
6189
|
+
if (sub === 'use') {
|
|
6190
|
+
const name = args[1];
|
|
6191
|
+
if (!name) { process.stderr.write('usage: lazyclaw sandbox use <profile>\n'); return 2; }
|
|
6192
|
+
const cfg = _sandboxLoadConfigOrEmpty();
|
|
6193
|
+
const prof = cfg.sandbox && cfg.sandbox.profiles && cfg.sandbox.profiles[name];
|
|
6194
|
+
if (!prof) { process.stderr.write(`no profile "${name}"\n`); return 1; }
|
|
6195
|
+
cfg.sandbox = cfg.sandbox || {};
|
|
6196
|
+
cfg.sandbox.default = prof.kind;
|
|
6197
|
+
cfg.sandbox[prof.kind] = { ...(cfg.sandbox[prof.kind] || {}), ...prof, kind: undefined };
|
|
6198
|
+
delete cfg.sandbox[prof.kind].kind;
|
|
6199
|
+
_sandboxSaveConfig(cfg);
|
|
6200
|
+
process.stdout.write(`using profile ${name} (${prof.kind})\n`);
|
|
6201
|
+
return 0;
|
|
6202
|
+
}
|
|
6203
|
+
|
|
6204
|
+
process.stderr.write(`unknown subcommand "${sub}". Try: list | test | add | use\n`);
|
|
6205
|
+
return 2;
|
|
6206
|
+
}
|
|
6207
|
+
|
|
6208
|
+
function _sandboxLoadConfigOrEmpty() {
|
|
6209
|
+
const p = process.env.LAZYCLAW_CONFIG || configPath();
|
|
6210
|
+
try {
|
|
6211
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
6212
|
+
} catch { return {}; }
|
|
6213
|
+
}
|
|
6214
|
+
|
|
6215
|
+
function _sandboxSaveConfig(cfg) {
|
|
6216
|
+
const p = process.env.LAZYCLAW_CONFIG || configPath();
|
|
6217
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
6218
|
+
fs.writeFileSync(p, JSON.stringify(cfg, null, 2));
|
|
6219
|
+
}
|
|
6220
|
+
|
|
5665
6221
|
function cmdConfigGet(key) {
|
|
5666
6222
|
const cfg = readConfig();
|
|
5667
|
-
if (key) console.log(JSON.stringify(
|
|
5668
|
-
|
|
6223
|
+
if (!key) { console.log(JSON.stringify(cfg)); return; }
|
|
6224
|
+
let value = cfg;
|
|
6225
|
+
for (const seg of String(key).split('.')) {
|
|
6226
|
+
if (value && typeof value === 'object' && seg in value) value = value[seg];
|
|
6227
|
+
else { value = null; break; }
|
|
6228
|
+
}
|
|
6229
|
+
console.log(JSON.stringify({ key, value }));
|
|
5669
6230
|
}
|
|
5670
6231
|
|
|
5671
6232
|
// Structural integrity check across the whole config. Distinct from
|
|
@@ -5997,6 +6558,8 @@ async function _dispatchMenuChoice(argv) {
|
|
|
5997
6558
|
case 'goal': return await cmdGoal(rest[0], rest.slice(1), {});
|
|
5998
6559
|
case 'memory': return await cmdMemory(rest[0], rest.slice(1), {});
|
|
5999
6560
|
case 'slack': return await cmdSlack(rest[0], rest.slice(1), {});
|
|
6561
|
+
case 'telegram': return await cmdTelegram(rest[0], rest.slice(1), {});
|
|
6562
|
+
case 'matrix': return await cmdMatrix(rest[0], rest.slice(1), {});
|
|
6000
6563
|
case 'team': return await cmdTeam(rest[0], rest.slice(1), {});
|
|
6001
6564
|
case 'task': return await cmdTask(rest[0], rest.slice(1), {});
|
|
6002
6565
|
case 'auth': return await cmdAuth(rest[0], rest.slice(1), {});
|
|
@@ -6024,6 +6587,8 @@ async function _dispatchMenuChoice(argv) {
|
|
|
6024
6587
|
case 'inspect': return await cmdInspect(rest[0], {});
|
|
6025
6588
|
case 'export': return await cmdExport({});
|
|
6026
6589
|
case 'version': return await cmdVersion();
|
|
6590
|
+
// Phase G — persona compose subcommand (spec §9, decision C7).
|
|
6591
|
+
case 'personality': return await cmdPersonality(rest[0], rest[1], rest[2]);
|
|
6027
6592
|
// help <cmd> is the safe fallback for commands that need real
|
|
6028
6593
|
// arguments (run / resume / clear / validate / graph / daemon /
|
|
6029
6594
|
// import / completion). Print the usage so the user can re-launch
|
|
@@ -6460,6 +7025,105 @@ async function main() {
|
|
|
6460
7025
|
}
|
|
6461
7026
|
break;
|
|
6462
7027
|
}
|
|
7028
|
+
case 'personality': {
|
|
7029
|
+
// Phase G: persona compose subcommand (spec §9, decision C7).
|
|
7030
|
+
process.exit(await cmdPersonality(rest.positional[0], rest.positional[1], rest.positional[2]));
|
|
7031
|
+
break;
|
|
7032
|
+
}
|
|
7033
|
+
case 'migrate': {
|
|
7034
|
+
// Phase A baseline accepts `lazyclaw migrate v5`; Phase G adds the
|
|
7035
|
+
// bare `lazyclaw migrate` and `lazyclaw migrate rollback` forms.
|
|
7036
|
+
const target = rest.positional[0];
|
|
7037
|
+
if (target === 'rollback') {
|
|
7038
|
+
const mod = await import('./scripts/migrate-v5.mjs');
|
|
7039
|
+
try {
|
|
7040
|
+
const { restoredFrom } = mod.rollback();
|
|
7041
|
+
console.log(`rolled back from ${restoredFrom}`);
|
|
7042
|
+
process.exit(0);
|
|
7043
|
+
} catch (e) {
|
|
7044
|
+
console.error(`migrate failed: ${e.message}`);
|
|
7045
|
+
process.exit(1);
|
|
7046
|
+
}
|
|
7047
|
+
break;
|
|
7048
|
+
}
|
|
7049
|
+
const mod = await import('./scripts/migrate-v5.mjs');
|
|
7050
|
+
// `migrate v5` keeps the Phase-A behaviour (verbose JSON); the
|
|
7051
|
+
// bare `migrate` form uses the Phase-G human summary.
|
|
7052
|
+
if (target === 'v5') {
|
|
7053
|
+
const r = await mod.migrateV5();
|
|
7054
|
+
console.log(JSON.stringify(r, null, 2));
|
|
7055
|
+
process.exit(r.ok ? 0 : 1);
|
|
7056
|
+
}
|
|
7057
|
+
try {
|
|
7058
|
+
const { backupDir } = mod.migrate();
|
|
7059
|
+
console.log(`migrated; backup at ${backupDir}`);
|
|
7060
|
+
process.exit(0);
|
|
7061
|
+
} catch (e) {
|
|
7062
|
+
console.error(`migrate failed: ${e.message}`);
|
|
7063
|
+
process.exit(1);
|
|
7064
|
+
}
|
|
7065
|
+
break;
|
|
7066
|
+
}
|
|
7067
|
+
case 'hermes': {
|
|
7068
|
+
// Phase G: import a Hermes Agent install (spec §10).
|
|
7069
|
+
if (rest.positional[0] !== 'import') {
|
|
7070
|
+
console.error('Usage: lazyclaw hermes import [--from <dir>]');
|
|
7071
|
+
process.exit(2);
|
|
7072
|
+
}
|
|
7073
|
+
const from = rest.flags.from;
|
|
7074
|
+
const mod = await import('./scripts/hermes-import.mjs');
|
|
7075
|
+
try {
|
|
7076
|
+
const { src, dst, counts } = mod.importHermes({ from });
|
|
7077
|
+
console.log(`hermes import: ${src} → ${dst}`);
|
|
7078
|
+
console.log(` skills: ${counts.skills} skins: ${counts.skins}`);
|
|
7079
|
+
process.exit(0);
|
|
7080
|
+
} catch (e) { console.error(`hermes import failed: ${e.message}`); process.exit(1); }
|
|
7081
|
+
break;
|
|
7082
|
+
}
|
|
7083
|
+
case 'openclaw': {
|
|
7084
|
+
// Phase G: import an OpenClaw install (spec §10).
|
|
7085
|
+
if (rest.positional[0] !== 'import') {
|
|
7086
|
+
console.error('Usage: lazyclaw openclaw import [--from <dir>]');
|
|
7087
|
+
process.exit(2);
|
|
7088
|
+
}
|
|
7089
|
+
const from = rest.flags.from;
|
|
7090
|
+
const mod = await import('./scripts/openclaw-import.mjs');
|
|
7091
|
+
try {
|
|
7092
|
+
const { src, dst, counts } = mod.importOpenclaw({ from });
|
|
7093
|
+
console.log(`openclaw import: ${src} → ${dst} skills:${counts.skills}`);
|
|
7094
|
+
process.exit(0);
|
|
7095
|
+
} catch (e) { console.error(`openclaw import failed: ${e.message}`); process.exit(1); }
|
|
7096
|
+
break;
|
|
7097
|
+
}
|
|
7098
|
+
case 'trajectories': {
|
|
7099
|
+
// Phase H1: read-only trajectory exporter (spec §2.7).
|
|
7100
|
+
// Usage: lazyclaw trajectories export --format <atropos|axolotl|openai-ft|jsonl>
|
|
7101
|
+
// [--since 7d] [--filter "outcome=done"] [--out ./dir]
|
|
7102
|
+
if (rest.positional[0] !== 'export') {
|
|
7103
|
+
console.error('Usage: lazyclaw trajectories export --format <atropos|axolotl|openai-ft|jsonl> [--since 7d] [--filter "outcome=done"] [--out <dir>]');
|
|
7104
|
+
process.exit(2);
|
|
7105
|
+
}
|
|
7106
|
+
const mod = await import('./mas/trajectory_export.mjs');
|
|
7107
|
+
const format = rest.flags.format || 'jsonl';
|
|
7108
|
+
if (!mod.FORMATS.includes(format)) {
|
|
7109
|
+
console.error(`trajectories export: unknown format "${format}" — choose ${mod.FORMATS.join('|')}`);
|
|
7110
|
+
process.exit(2);
|
|
7111
|
+
}
|
|
7112
|
+
try {
|
|
7113
|
+
const r = await mod.exportTrajectories({
|
|
7114
|
+
format,
|
|
7115
|
+
since: rest.flags.since,
|
|
7116
|
+
filter: mod.parseFilterArg(rest.flags.filter),
|
|
7117
|
+
outDir: rest.flags.out,
|
|
7118
|
+
});
|
|
7119
|
+
console.log(`exported ${r.count} trajectories (${r.format}) → ${r.outFile}`);
|
|
7120
|
+
process.exit(0);
|
|
7121
|
+
} catch (e) {
|
|
7122
|
+
console.error(`trajectories export failed: ${e.message}`);
|
|
7123
|
+
process.exit(1);
|
|
7124
|
+
}
|
|
7125
|
+
break;
|
|
7126
|
+
}
|
|
6463
7127
|
case 'chat': {
|
|
6464
7128
|
await cmdChat(rest.flags);
|
|
6465
7129
|
break;
|
|
@@ -6489,6 +7153,10 @@ async function main() {
|
|
|
6489
7153
|
await cmdRates(sub, rest.positional.slice(1), rest.flags);
|
|
6490
7154
|
break;
|
|
6491
7155
|
}
|
|
7156
|
+
case 'sandbox': {
|
|
7157
|
+
process.exit(await cmdSandbox(rest.positional, rest.flags));
|
|
7158
|
+
break;
|
|
7159
|
+
}
|
|
6492
7160
|
case 'auth': {
|
|
6493
7161
|
const sub = rest.positional[0];
|
|
6494
7162
|
await cmdAuth(sub, rest.positional.slice(1), rest.flags);
|
|
@@ -6548,6 +7216,16 @@ async function main() {
|
|
|
6548
7216
|
await cmdSlack(sub, rest.positional.slice(1), rest.flags);
|
|
6549
7217
|
break;
|
|
6550
7218
|
}
|
|
7219
|
+
case 'telegram': {
|
|
7220
|
+
const sub = rest.positional[0];
|
|
7221
|
+
await cmdTelegram(sub, rest.positional.slice(1), rest.flags);
|
|
7222
|
+
break;
|
|
7223
|
+
}
|
|
7224
|
+
case 'matrix': {
|
|
7225
|
+
const sub = rest.positional[0];
|
|
7226
|
+
await cmdMatrix(sub, rest.positional.slice(1), rest.flags);
|
|
7227
|
+
break;
|
|
7228
|
+
}
|
|
6551
7229
|
case 'team': {
|
|
6552
7230
|
const sub = rest.positional[0];
|
|
6553
7231
|
await cmdTeam(sub, rest.positional.slice(1), rest.flags);
|
|
@@ -6566,6 +7244,31 @@ async function main() {
|
|
|
6566
7244
|
await cmdDashboard(rest.flags);
|
|
6567
7245
|
break;
|
|
6568
7246
|
}
|
|
7247
|
+
case 'channels': {
|
|
7248
|
+
const sub = (rest.positional[0] || 'list').toLowerCase();
|
|
7249
|
+
const { createLoader, listInstalled } = await import('./channels/loader.mjs');
|
|
7250
|
+
const cfgDir = path.dirname(configPath());
|
|
7251
|
+
const loader = createLoader({ configDir: cfgDir });
|
|
7252
|
+
if (sub === 'install') {
|
|
7253
|
+
const name = rest.positional[1];
|
|
7254
|
+
if (!name) { process.stderr.write('usage: lazyclaw channels install <@lazyclaw/channel-name>\n'); process.exit(2); }
|
|
7255
|
+
const info = await loader.install(name);
|
|
7256
|
+
process.stdout.write(`installed ${info.name}@${info.version}\n`);
|
|
7257
|
+
break;
|
|
7258
|
+
}
|
|
7259
|
+
if (sub === 'remove' || sub === 'uninstall') {
|
|
7260
|
+
const name = rest.positional[1];
|
|
7261
|
+
if (!name) { process.stderr.write('usage: lazyclaw channels remove <@lazyclaw/channel-name>\n'); process.exit(2); }
|
|
7262
|
+
await loader.remove(name);
|
|
7263
|
+
process.stdout.write(`removed ${name}\n`);
|
|
7264
|
+
break;
|
|
7265
|
+
}
|
|
7266
|
+
// list
|
|
7267
|
+
const rows = listInstalled(cfgDir);
|
|
7268
|
+
if (!rows.length) { process.stdout.write('no channel plugins installed\n'); break; }
|
|
7269
|
+
for (const r of rows) process.stdout.write(`${r.name}\t${r.version}\n`);
|
|
7270
|
+
break;
|
|
7271
|
+
}
|
|
6569
7272
|
case 'daemon': {
|
|
6570
7273
|
await cmdDaemon(rest.flags);
|
|
6571
7274
|
break;
|