lazyclaw 4.3.0 → 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 -508
- package/channels/handoff.mjs +41 -0
- package/channels/loader.mjs +124 -0
- package/channels/threads.mjs +116 -0
- package/cli.mjs +398 -6
- package/daemon.mjs +13 -0
- package/mas/agent_turn.mjs +28 -0
- package/mas/confidence.mjs +108 -0
- package/mas/index_db.mjs +242 -0
- package/mas/nudge.mjs +97 -0
- package/mas/prompt_stack.mjs +80 -0
- package/mas/skill_synth.mjs +124 -25
- package/mas/tool_runner.mjs +10 -61
- 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/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 +20 -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/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');
|
|
@@ -639,6 +643,66 @@ async function cmdResume(sessionId, file, opts = {}) {
|
|
|
639
643
|
}
|
|
640
644
|
}
|
|
641
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
|
+
|
|
642
706
|
async function cmdConfigEdit() {
|
|
643
707
|
// Open config.json in $EDITOR (or sensible default), then validate
|
|
644
708
|
// the result before letting the user walk away believing the edit
|
|
@@ -685,10 +749,10 @@ function cmdConfigSet(key, value) {
|
|
|
685
749
|
function applyOnboardConfig(currentCfg, flags) {
|
|
686
750
|
// Honors the OpenClaw-style unified provider/model string ("anthropic/claude-opus-4-7")
|
|
687
751
|
// by splitting it, but explicit --provider always wins.
|
|
688
|
-
const {
|
|
752
|
+
const { parseSlashProviderModel } = require_registry_sync();
|
|
689
753
|
const next = { ...currentCfg };
|
|
690
754
|
if (flags.model) {
|
|
691
|
-
const parsed =
|
|
755
|
+
const parsed = parseSlashProviderModel(flags.model);
|
|
692
756
|
if (parsed.provider && !flags.provider) next.provider = parsed.provider;
|
|
693
757
|
next.model = parsed.model || flags.model;
|
|
694
758
|
}
|
|
@@ -925,6 +989,8 @@ const SUBCOMMANDS = [
|
|
|
925
989
|
'daemon', 'version', 'completion', 'help',
|
|
926
990
|
'export', 'import',
|
|
927
991
|
'rates',
|
|
992
|
+
// v5.0 — sandbox 6-backend (Phase D)
|
|
993
|
+
'sandbox',
|
|
928
994
|
// OpenClaw-parity subsurfaces (v3.93–v3.98)
|
|
929
995
|
'auth', 'pairing', 'nodes', 'message', 'workspace', 'browse', 'cron',
|
|
930
996
|
// v3.99.6 — multi-step setup wizard + lazyclaw-only dashboard
|
|
@@ -939,8 +1005,14 @@ const SUBCOMMANDS = [
|
|
|
939
1005
|
'telegram',
|
|
940
1006
|
// v4.3.0 — Matrix /sync long-poll listener
|
|
941
1007
|
'matrix',
|
|
1008
|
+
// v5.0 — channels plugin loader (Phase F)
|
|
1009
|
+
'channels',
|
|
942
1010
|
// v4.1.0 — multi-agent slack system (Phase 9+)
|
|
943
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',
|
|
944
1016
|
];
|
|
945
1017
|
|
|
946
1018
|
const SUBCOMMAND_SUBS = {
|
|
@@ -949,6 +1021,7 @@ const SUBCOMMAND_SUBS = {
|
|
|
949
1021
|
skills: ['list', 'show', 'install', 'remove', 'search', 'curate', 'classify'],
|
|
950
1022
|
providers: ['list', 'info', 'test', 'add', 'remove', 'models'],
|
|
951
1023
|
rates: ['list', 'set', 'delete', 'shape', 'validate', 'copy'],
|
|
1024
|
+
sandbox: ['list', 'test', 'add', 'use'],
|
|
952
1025
|
completion: ['bash', 'zsh'],
|
|
953
1026
|
auth: ['list', 'add', 'remove', 'use', 'rotate'],
|
|
954
1027
|
pairing: ['list', 'add', 'remove'],
|
|
@@ -966,6 +1039,7 @@ const SUBCOMMAND_SUBS = {
|
|
|
966
1039
|
agent: ['add', 'list', 'show', 'edit', 'remove'],
|
|
967
1040
|
team: ['add', 'list', 'show', 'edit', 'remove'],
|
|
968
1041
|
task: ['start', 'list', 'show', 'abandon', 'done', 'remove'],
|
|
1042
|
+
trajectories: ['export'],
|
|
969
1043
|
};
|
|
970
1044
|
|
|
971
1045
|
function bashCompletion() {
|
|
@@ -1214,6 +1288,7 @@ const HELP_DETAILS = {
|
|
|
1214
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).',
|
|
1215
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.',
|
|
1216
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',
|
|
1217
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.',
|
|
1218
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.',
|
|
1219
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.',
|
|
@@ -2300,6 +2375,38 @@ async function cmdChat(flags = {}) {
|
|
|
2300
2375
|
|
|
2301
2376
|
// Top-of-session banner so the user can see at a glance what they're
|
|
2302
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) ─────────────────────────────────
|
|
2303
2410
|
_printChatBanner(activeProvName, activeModel, readVersionFromRepo());
|
|
2304
2411
|
|
|
2305
2412
|
const readline = await import('node:readline');
|
|
@@ -2481,8 +2588,8 @@ async function cmdChat(flags = {}) {
|
|
|
2481
2588
|
}
|
|
2482
2589
|
// Honor unified provider/model: `/model anthropic/claude-opus-4-7`
|
|
2483
2590
|
// splits and switches both.
|
|
2484
|
-
const {
|
|
2485
|
-
const parsed =
|
|
2591
|
+
const { parseSlashProviderModel } = _registryMod;
|
|
2592
|
+
const parsed = parseSlashProviderModel(arg);
|
|
2486
2593
|
if (parsed.provider) {
|
|
2487
2594
|
const next = lookupProv(parsed.provider);
|
|
2488
2595
|
if (!next) {
|
|
@@ -2942,6 +3049,53 @@ async function cmdChat(flags = {}) {
|
|
|
2942
3049
|
}
|
|
2943
3050
|
return true;
|
|
2944
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
|
+
}
|
|
2945
3099
|
case '/exit': {
|
|
2946
3100
|
return 'EXIT';
|
|
2947
3101
|
}
|
|
@@ -5961,10 +6115,118 @@ async function cmdSessions(sub, positional, flags = {}) {
|
|
|
5961
6115
|
}
|
|
5962
6116
|
}
|
|
5963
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
|
+
|
|
5964
6221
|
function cmdConfigGet(key) {
|
|
5965
6222
|
const cfg = readConfig();
|
|
5966
|
-
if (key) console.log(JSON.stringify(
|
|
5967
|
-
|
|
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 }));
|
|
5968
6230
|
}
|
|
5969
6231
|
|
|
5970
6232
|
// Structural integrity check across the whole config. Distinct from
|
|
@@ -6325,6 +6587,8 @@ async function _dispatchMenuChoice(argv) {
|
|
|
6325
6587
|
case 'inspect': return await cmdInspect(rest[0], {});
|
|
6326
6588
|
case 'export': return await cmdExport({});
|
|
6327
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]);
|
|
6328
6592
|
// help <cmd> is the safe fallback for commands that need real
|
|
6329
6593
|
// arguments (run / resume / clear / validate / graph / daemon /
|
|
6330
6594
|
// import / completion). Print the usage so the user can re-launch
|
|
@@ -6761,6 +7025,105 @@ async function main() {
|
|
|
6761
7025
|
}
|
|
6762
7026
|
break;
|
|
6763
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
|
+
}
|
|
6764
7127
|
case 'chat': {
|
|
6765
7128
|
await cmdChat(rest.flags);
|
|
6766
7129
|
break;
|
|
@@ -6790,6 +7153,10 @@ async function main() {
|
|
|
6790
7153
|
await cmdRates(sub, rest.positional.slice(1), rest.flags);
|
|
6791
7154
|
break;
|
|
6792
7155
|
}
|
|
7156
|
+
case 'sandbox': {
|
|
7157
|
+
process.exit(await cmdSandbox(rest.positional, rest.flags));
|
|
7158
|
+
break;
|
|
7159
|
+
}
|
|
6793
7160
|
case 'auth': {
|
|
6794
7161
|
const sub = rest.positional[0];
|
|
6795
7162
|
await cmdAuth(sub, rest.positional.slice(1), rest.flags);
|
|
@@ -6877,6 +7244,31 @@ async function main() {
|
|
|
6877
7244
|
await cmdDashboard(rest.flags);
|
|
6878
7245
|
break;
|
|
6879
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
|
+
}
|
|
6880
7272
|
case 'daemon': {
|
|
6881
7273
|
await cmdDaemon(rest.flags);
|
|
6882
7274
|
break;
|
package/daemon.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import { createLogger } from './logger.mjs';
|
|
|
28
28
|
import { summarizeState, listSessions as listWorkflowSessions, loadStateFile as loadWorkflowState, aggregateNodeStats } from './workflow/summary.mjs';
|
|
29
29
|
import { validateConfig } from './config-validate.mjs';
|
|
30
30
|
import { validateRates } from './rates-validate.mjs';
|
|
31
|
+
import * as nudge from './mas/nudge.mjs';
|
|
31
32
|
|
|
32
33
|
// Resolve the provider for a request. Composes opt-in wrappers in this
|
|
33
34
|
// order (innermost first):
|
|
@@ -328,6 +329,18 @@ export function makeHandler(ctx) {
|
|
|
328
329
|
// one, so it must outlive a single call.
|
|
329
330
|
const gwConfigDir = typeof ctx.sessionsDirGetter === 'function' ? ctx.sessionsDirGetter() : undefined;
|
|
330
331
|
const gateway = createGateway({ configDir: gwConfigDir, challengeRegistry: new ChallengeRegistry(), heartbeatMs: 25000 });
|
|
332
|
+
// Phase B nudge loop — scans recent.jsonl every 5 min and pushes
|
|
333
|
+
// nudge.suggest_skill onto the SSE bus so the curator can prompt.
|
|
334
|
+
const _nudgeLoop = nudge.startNudgeLoop({
|
|
335
|
+
configDir: gwConfigDir,
|
|
336
|
+
emit: (event) => {
|
|
337
|
+
try { gateway.broadcast?.('nudge.suggest_skill', event); }
|
|
338
|
+
catch (err) { logger?.warn?.('nudge_emit_failed', { err: err.message }); }
|
|
339
|
+
},
|
|
340
|
+
logger,
|
|
341
|
+
});
|
|
342
|
+
process.on('SIGTERM', () => { _nudgeLoop.stop(); });
|
|
343
|
+
process.on('SIGINT', () => { _nudgeLoop.stop(); });
|
|
331
344
|
return async function handler(req, res) {
|
|
332
345
|
// Capture method+path before any handler logic runs; req.url survives
|
|
333
346
|
// the response but capturing now keeps the log line stable even if a
|
package/mas/agent_turn.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import * as anthropic from '../providers/tool_use/anthropic.mjs';
|
|
|
25
25
|
import * as openai from '../providers/tool_use/openai.mjs';
|
|
26
26
|
import * as gemini from '../providers/tool_use/gemini.mjs';
|
|
27
27
|
import * as claudeCli from '../providers/tool_use/claude_cli.mjs';
|
|
28
|
+
import { put as _trajPut } from './trajectory_store.mjs';
|
|
28
29
|
|
|
29
30
|
export class AgentTurnError extends Error {
|
|
30
31
|
constructor(message, code) {
|
|
@@ -71,6 +72,7 @@ export async function runAgentTurn({
|
|
|
71
72
|
maxIterations = DEFAULT_MAX_ITERATIONS,
|
|
72
73
|
signal,
|
|
73
74
|
approve,
|
|
75
|
+
trajectoryRef,
|
|
74
76
|
} = {}) {
|
|
75
77
|
if (!agent) throw new AgentTurnError('agent is required', 'NO_AGENT');
|
|
76
78
|
const adapter = adapterFor(agent.provider);
|
|
@@ -92,6 +94,29 @@ export async function runAgentTurn({
|
|
|
92
94
|
const toolCalls = [];
|
|
93
95
|
let iterations = 0;
|
|
94
96
|
let lastText = '';
|
|
97
|
+
if (trajectoryRef && !trajectoryRef.startedAt) trajectoryRef.startedAt = Date.now();
|
|
98
|
+
|
|
99
|
+
const _maybePersistTrajectory = async (outcome) => {
|
|
100
|
+
if (!trajectoryRef) return;
|
|
101
|
+
try {
|
|
102
|
+
await _trajPut({
|
|
103
|
+
taskId, agentName: agent.name || 'agent',
|
|
104
|
+
workerProvider: agent.provider, workerModel: agent.model,
|
|
105
|
+
startedAt: trajectoryRef.startedAt || Date.now(),
|
|
106
|
+
endedAt: Date.now(),
|
|
107
|
+
systemPrompt: agent.role || '',
|
|
108
|
+
userMessages: userMessage ? [String(userMessage)] : [],
|
|
109
|
+
turns: toolCalls.map((c, i) => ({
|
|
110
|
+
turnIdx: i, role: 'tool', content: '',
|
|
111
|
+
toolCalls: [{ name: c.name, args: c.input, result: JSON.stringify(c.result), success: c.ok, durationMs: 0 }],
|
|
112
|
+
})).concat(lastText ? [{
|
|
113
|
+
turnIdx: toolCalls.length, role: 'assistant', content: lastText, toolCalls: [],
|
|
114
|
+
}] : []),
|
|
115
|
+
finalAnswer: lastText,
|
|
116
|
+
outcome,
|
|
117
|
+
}, { configDir });
|
|
118
|
+
} catch { /* trajectory failure must not break the agent turn */ }
|
|
119
|
+
};
|
|
95
120
|
|
|
96
121
|
while (iterations < maxIterations) {
|
|
97
122
|
if (signal?.aborted) return { text: lastText, iterations, stoppedBy: 'abort', toolCalls };
|
|
@@ -103,6 +128,7 @@ export async function runAgentTurn({
|
|
|
103
128
|
if (resp.text) lastText = resp.text;
|
|
104
129
|
|
|
105
130
|
if (resp.kind === 'final') {
|
|
131
|
+
await _maybePersistTrajectory('done');
|
|
106
132
|
return { text: resp.text || '', iterations, stoppedBy: 'final', toolCalls };
|
|
107
133
|
}
|
|
108
134
|
|
|
@@ -140,9 +166,11 @@ export async function runAgentTurn({
|
|
|
140
166
|
// model so it can recover. Only an extraordinary error (e.g. the
|
|
141
167
|
// provider returned a malformed envelope) bails out here.
|
|
142
168
|
if (toolErrored && process.env.LAZYCLAW_TOOL_STRICT === '1') {
|
|
169
|
+
await _maybePersistTrajectory('failed');
|
|
143
170
|
return { text: lastText, iterations, stoppedBy: 'tool_error', toolCalls };
|
|
144
171
|
}
|
|
145
172
|
}
|
|
146
173
|
|
|
174
|
+
await _maybePersistTrajectory('abandoned');
|
|
147
175
|
return { text: lastText, iterations, stoppedBy: 'budget', toolCalls };
|
|
148
176
|
}
|