lazyclaw 4.3.0 → 5.0.1
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 +430 -7
- 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.',
|
|
@@ -1559,6 +1634,33 @@ function _renderBanner(version) {
|
|
|
1559
1634
|
];
|
|
1560
1635
|
}
|
|
1561
1636
|
|
|
1637
|
+
// v5 sloth banner — shared with the ink splash (tui/splash.mjs).
|
|
1638
|
+
// Single-tone orange like _renderBanner so the no-arg launcher and the
|
|
1639
|
+
// chat splash share a visual identity. Opt out with LAZYCLAW_LEGACY_MENU=1
|
|
1640
|
+
// to fall back to the boxed figlet variant above.
|
|
1641
|
+
let _v5BannerRowsCache = null;
|
|
1642
|
+
async function _renderV5Banner(version) {
|
|
1643
|
+
if (!_v5BannerRowsCache) {
|
|
1644
|
+
try {
|
|
1645
|
+
const { banner } = await import('./tui/banner.generated.mjs');
|
|
1646
|
+
_v5BannerRowsCache = banner.rows;
|
|
1647
|
+
} catch {
|
|
1648
|
+
_v5BannerRowsCache = null;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
if (!_v5BannerRowsCache) return _renderBanner(version);
|
|
1652
|
+
const v = String(version || '?.?.?');
|
|
1653
|
+
return _v5BannerRowsCache.map((row, i) => {
|
|
1654
|
+
if (i === 8) {
|
|
1655
|
+
// Caption row — overlay "lazyclaw v<version>" centered on the 24-wide art
|
|
1656
|
+
const cap = `lazyclaw v${v}`;
|
|
1657
|
+
const pad = Math.max(0, Math.floor((24 - cap.length) / 2));
|
|
1658
|
+
return _orange(' '.repeat(pad) + cap + ' '.repeat(24 - pad - cap.length));
|
|
1659
|
+
}
|
|
1660
|
+
return _orange(row);
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1562
1664
|
function _printChatBanner(activeProvName, activeModel, version) {
|
|
1563
1665
|
if (!process.stdout.isTTY) return;
|
|
1564
1666
|
// Single-hue header: labels dim-orange, values/emphasis full-orange, so the
|
|
@@ -2300,6 +2402,38 @@ async function cmdChat(flags = {}) {
|
|
|
2300
2402
|
|
|
2301
2403
|
// Top-of-session banner so the user can see at a glance what they're
|
|
2302
2404
|
// talking to. Cheap (no provider call) and TTY-only.
|
|
2405
|
+
// v5 ink splash + REPL when stdin is a real TTY and the user has not
|
|
2406
|
+
// opted out via LAZYCLAW_NO_INK=1. Non-TTY pipelines and the opt-out
|
|
2407
|
+
// env var fall through to the v4 figlet + readline path unchanged.
|
|
2408
|
+
const __useInkSplash = process.stdout.isTTY && !process.env.LAZYCLAW_NO_INK;
|
|
2409
|
+
if (__useInkSplash) {
|
|
2410
|
+
try {
|
|
2411
|
+
const React = (await import('react')).default;
|
|
2412
|
+
const { render } = await import('ink');
|
|
2413
|
+
const { ReplApp } = await import('./tui/repl.mjs');
|
|
2414
|
+
const { renderSplashToString } = await import('./tui/splash.mjs');
|
|
2415
|
+
// narrow-terminal fallback: <60 cols falls back to v4
|
|
2416
|
+
if ((process.stdout.columns || 80) < 60) throw new Error('narrow-terminal');
|
|
2417
|
+
const splashProps = {
|
|
2418
|
+
provider: activeProvName, model: activeModel,
|
|
2419
|
+
trainer: {}, sessionId: flags.session || '',
|
|
2420
|
+
cwd: process.cwd(),
|
|
2421
|
+
tools: [], skills: [],
|
|
2422
|
+
};
|
|
2423
|
+
void renderSplashToString; // surfaced for tests; runtime uses <Splash/>
|
|
2424
|
+
const ink = render(/* @__PURE__ */ React.createElement(ReplApp, {
|
|
2425
|
+
splashProps,
|
|
2426
|
+
runTurn: async (_text, _signal) => { void _text; void _signal; },
|
|
2427
|
+
}));
|
|
2428
|
+
await ink.waitUntilExit();
|
|
2429
|
+
return;
|
|
2430
|
+
} catch (e) {
|
|
2431
|
+
// Fall through to legacy path on any ink failure (missing import,
|
|
2432
|
+
// narrow terminal, sandboxed stdout).
|
|
2433
|
+
if (process.env.LAZYCLAW_DEBUG) console.error('[ink] fallback:', e.message);
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
// ─── legacy v4 path (unchanged) ─────────────────────────────────
|
|
2303
2437
|
_printChatBanner(activeProvName, activeModel, readVersionFromRepo());
|
|
2304
2438
|
|
|
2305
2439
|
const readline = await import('node:readline');
|
|
@@ -2481,8 +2615,8 @@ async function cmdChat(flags = {}) {
|
|
|
2481
2615
|
}
|
|
2482
2616
|
// Honor unified provider/model: `/model anthropic/claude-opus-4-7`
|
|
2483
2617
|
// splits and switches both.
|
|
2484
|
-
const {
|
|
2485
|
-
const parsed =
|
|
2618
|
+
const { parseSlashProviderModel } = _registryMod;
|
|
2619
|
+
const parsed = parseSlashProviderModel(arg);
|
|
2486
2620
|
if (parsed.provider) {
|
|
2487
2621
|
const next = lookupProv(parsed.provider);
|
|
2488
2622
|
if (!next) {
|
|
@@ -2942,6 +3076,53 @@ async function cmdChat(flags = {}) {
|
|
|
2942
3076
|
}
|
|
2943
3077
|
return true;
|
|
2944
3078
|
}
|
|
3079
|
+
case '/handoff': {
|
|
3080
|
+
// /handoff <target-channel> <externalId> [--note=...] — migrates the
|
|
3081
|
+
// active thread (bound to replState.channel / replState.externalId)
|
|
3082
|
+
// to a new channel and posts transition stubs on both sides. In the
|
|
3083
|
+
// local-only chat REPL there is no bound channel, so we surface a
|
|
3084
|
+
// clear error and stay in the REPL (acceptance test §F).
|
|
3085
|
+
const parts = line.trim().split(/\s+/).slice(1);
|
|
3086
|
+
if (parts.length < 2) {
|
|
3087
|
+
process.stderr.write('usage: /handoff <target-channel> <externalId> [--note=...]\n');
|
|
3088
|
+
return true;
|
|
3089
|
+
}
|
|
3090
|
+
const target = parts[0];
|
|
3091
|
+
const externalId = parts[1];
|
|
3092
|
+
const note = (parts.find(p => p.startsWith('--note=')) || '').slice(7);
|
|
3093
|
+
try {
|
|
3094
|
+
const { openThreads } = await import('./channels/threads.mjs');
|
|
3095
|
+
const { runHandoff } = await import('./channels/handoff.mjs');
|
|
3096
|
+
const threads = openThreads(cfgDir);
|
|
3097
|
+
const replState = globalThis.__lazyclawReplState || {};
|
|
3098
|
+
const cur = replState.channel && replState.externalId
|
|
3099
|
+
? threads.findByExternal(replState.channel, replState.externalId)
|
|
3100
|
+
: null;
|
|
3101
|
+
if (!cur) {
|
|
3102
|
+
process.stderr.write(
|
|
3103
|
+
`handoff: no thread bound to ${replState.channel || '(none)'}:${replState.externalId || '(none)'}\n`,
|
|
3104
|
+
);
|
|
3105
|
+
return true;
|
|
3106
|
+
}
|
|
3107
|
+
const next = await runHandoff({
|
|
3108
|
+
threads, channels: replState.channels || {},
|
|
3109
|
+
threadId: cur.threadId, target, externalId, note,
|
|
3110
|
+
});
|
|
3111
|
+
process.stdout.write(`handoff -> ${next.channel}:${next.externalId} (session ${next.sessionId})\n`);
|
|
3112
|
+
replState.channel = next.channel;
|
|
3113
|
+
replState.externalId = next.externalId;
|
|
3114
|
+
} catch (e) {
|
|
3115
|
+
process.stderr.write(`handoff failed: ${e.code || 'ERR'}: ${e.message}\n`);
|
|
3116
|
+
}
|
|
3117
|
+
return true;
|
|
3118
|
+
}
|
|
3119
|
+
case '/personality': {
|
|
3120
|
+
// Phase G: thin slash wrapper over cmdPersonality.
|
|
3121
|
+
const tail = line.slice('/personality'.length).trim();
|
|
3122
|
+
const parts = tail.split(/\s+/).filter(Boolean);
|
|
3123
|
+
await cmdPersonality(parts[0] || 'list', parts[1], parts[2]);
|
|
3124
|
+
return true;
|
|
3125
|
+
}
|
|
2945
3126
|
case '/exit': {
|
|
2946
3127
|
return 'EXIT';
|
|
2947
3128
|
}
|
|
@@ -5961,10 +6142,118 @@ async function cmdSessions(sub, positional, flags = {}) {
|
|
|
5961
6142
|
}
|
|
5962
6143
|
}
|
|
5963
6144
|
|
|
6145
|
+
// sandbox subcommands — list/test/add/use (Phase D).
|
|
6146
|
+
async function cmdSandbox(args, flags = {}) {
|
|
6147
|
+
const sub = args[0];
|
|
6148
|
+
|
|
6149
|
+
if (!sub || sub === 'list') {
|
|
6150
|
+
for (const kind of listBackends()) process.stdout.write(`${kind}\n`);
|
|
6151
|
+
return 0;
|
|
6152
|
+
}
|
|
6153
|
+
|
|
6154
|
+
if (sub === 'test') {
|
|
6155
|
+
const name = args[1];
|
|
6156
|
+
if (!name) { process.stderr.write('usage: lazyclaw sandbox test <kind|profile>\n'); return 2; }
|
|
6157
|
+
const cfg = _sandboxLoadConfigOrEmpty();
|
|
6158
|
+
// If `name` looks like a known kind, route to that kind. If it
|
|
6159
|
+
// is not a known kind AND not a profile in cfg, treat as an
|
|
6160
|
+
// unknown identifier and report SANDBOX_BAD_KIND.
|
|
6161
|
+
const isKind = listBackends().includes(name);
|
|
6162
|
+
const profile = cfg.sandbox && cfg.sandbox.profiles && cfg.sandbox.profiles[name];
|
|
6163
|
+
if (!isKind && !profile) {
|
|
6164
|
+
process.stderr.write(`SANDBOX_BAD_KIND: unknown sandbox kind or profile "${name}"\n`);
|
|
6165
|
+
return 1;
|
|
6166
|
+
}
|
|
6167
|
+
let sb;
|
|
6168
|
+
try {
|
|
6169
|
+
const synthCfg = isKind
|
|
6170
|
+
? { sandbox: { default: name, ...cfg.sandbox } }
|
|
6171
|
+
: cfg;
|
|
6172
|
+
sb = resolveSandbox(synthCfg);
|
|
6173
|
+
} catch (e) {
|
|
6174
|
+
process.stderr.write(`${e.code || 'SANDBOX_ERR'}: ${e.message}\n`); return 1;
|
|
6175
|
+
}
|
|
6176
|
+
if (sb.spec.kind !== 'local' && sb.spec.kind !== 'docker') {
|
|
6177
|
+
// Remote/serverless backends just construct argv in unit tests;
|
|
6178
|
+
// we report "shape-ok" without actually executing.
|
|
6179
|
+
process.stdout.write(`ok ${sb.spec.kind} (argv-shape)\n`);
|
|
6180
|
+
return 0;
|
|
6181
|
+
}
|
|
6182
|
+
const sess = await sb.open();
|
|
6183
|
+
try {
|
|
6184
|
+
const r = await sess.exec(['echo', 'lazyclaw-sandbox-test']);
|
|
6185
|
+
if (r.code !== 0 || !/lazyclaw-sandbox-test/.test(r.stdout)) {
|
|
6186
|
+
process.stderr.write(`fail ${name}: exit=${r.code} stdout=${r.stdout}\n`); return 1;
|
|
6187
|
+
}
|
|
6188
|
+
process.stdout.write(`ok ${name}\n`);
|
|
6189
|
+
return 0;
|
|
6190
|
+
} finally { await sess.close(); }
|
|
6191
|
+
}
|
|
6192
|
+
|
|
6193
|
+
if (sub === 'add') {
|
|
6194
|
+
const name = args[1];
|
|
6195
|
+
if (!name) { process.stderr.write('usage: lazyclaw sandbox add <name> --kind <kind> [...]\n'); return 2; }
|
|
6196
|
+
const opts = {};
|
|
6197
|
+
if (flags.kind) opts.kind = flags.kind;
|
|
6198
|
+
if (flags.image) opts.image = flags.image;
|
|
6199
|
+
if (flags.host) opts.host = flags.host;
|
|
6200
|
+
if (flags.user) opts.user = flags.user;
|
|
6201
|
+
if (flags.workspace) opts.workspace = flags.workspace;
|
|
6202
|
+
if (flags.app) opts.app = flags.app;
|
|
6203
|
+
if (flags.confiner) opts.confiner = flags.confiner;
|
|
6204
|
+
if (!listBackends().includes(opts.kind)) {
|
|
6205
|
+
process.stderr.write(`unknown kind "${opts.kind}"\n`); return 1;
|
|
6206
|
+
}
|
|
6207
|
+
const cfg = _sandboxLoadConfigOrEmpty();
|
|
6208
|
+
cfg.sandbox = cfg.sandbox || {};
|
|
6209
|
+
cfg.sandbox.profiles = cfg.sandbox.profiles || {};
|
|
6210
|
+
cfg.sandbox.profiles[name] = opts;
|
|
6211
|
+
_sandboxSaveConfig(cfg);
|
|
6212
|
+
process.stdout.write(`added profile ${name} (${opts.kind})\n`);
|
|
6213
|
+
return 0;
|
|
6214
|
+
}
|
|
6215
|
+
|
|
6216
|
+
if (sub === 'use') {
|
|
6217
|
+
const name = args[1];
|
|
6218
|
+
if (!name) { process.stderr.write('usage: lazyclaw sandbox use <profile>\n'); return 2; }
|
|
6219
|
+
const cfg = _sandboxLoadConfigOrEmpty();
|
|
6220
|
+
const prof = cfg.sandbox && cfg.sandbox.profiles && cfg.sandbox.profiles[name];
|
|
6221
|
+
if (!prof) { process.stderr.write(`no profile "${name}"\n`); return 1; }
|
|
6222
|
+
cfg.sandbox = cfg.sandbox || {};
|
|
6223
|
+
cfg.sandbox.default = prof.kind;
|
|
6224
|
+
cfg.sandbox[prof.kind] = { ...(cfg.sandbox[prof.kind] || {}), ...prof, kind: undefined };
|
|
6225
|
+
delete cfg.sandbox[prof.kind].kind;
|
|
6226
|
+
_sandboxSaveConfig(cfg);
|
|
6227
|
+
process.stdout.write(`using profile ${name} (${prof.kind})\n`);
|
|
6228
|
+
return 0;
|
|
6229
|
+
}
|
|
6230
|
+
|
|
6231
|
+
process.stderr.write(`unknown subcommand "${sub}". Try: list | test | add | use\n`);
|
|
6232
|
+
return 2;
|
|
6233
|
+
}
|
|
6234
|
+
|
|
6235
|
+
function _sandboxLoadConfigOrEmpty() {
|
|
6236
|
+
const p = process.env.LAZYCLAW_CONFIG || configPath();
|
|
6237
|
+
try {
|
|
6238
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
6239
|
+
} catch { return {}; }
|
|
6240
|
+
}
|
|
6241
|
+
|
|
6242
|
+
function _sandboxSaveConfig(cfg) {
|
|
6243
|
+
const p = process.env.LAZYCLAW_CONFIG || configPath();
|
|
6244
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
6245
|
+
fs.writeFileSync(p, JSON.stringify(cfg, null, 2));
|
|
6246
|
+
}
|
|
6247
|
+
|
|
5964
6248
|
function cmdConfigGet(key) {
|
|
5965
6249
|
const cfg = readConfig();
|
|
5966
|
-
if (key) console.log(JSON.stringify(
|
|
5967
|
-
|
|
6250
|
+
if (!key) { console.log(JSON.stringify(cfg)); return; }
|
|
6251
|
+
let value = cfg;
|
|
6252
|
+
for (const seg of String(key).split('.')) {
|
|
6253
|
+
if (value && typeof value === 'object' && seg in value) value = value[seg];
|
|
6254
|
+
else { value = null; break; }
|
|
6255
|
+
}
|
|
6256
|
+
console.log(JSON.stringify({ key, value }));
|
|
5968
6257
|
}
|
|
5969
6258
|
|
|
5970
6259
|
// Structural integrity check across the whole config. Distinct from
|
|
@@ -6325,6 +6614,8 @@ async function _dispatchMenuChoice(argv) {
|
|
|
6325
6614
|
case 'inspect': return await cmdInspect(rest[0], {});
|
|
6326
6615
|
case 'export': return await cmdExport({});
|
|
6327
6616
|
case 'version': return await cmdVersion();
|
|
6617
|
+
// Phase G — persona compose subcommand (spec §9, decision C7).
|
|
6618
|
+
case 'personality': return await cmdPersonality(rest[0], rest[1], rest[2]);
|
|
6328
6619
|
// help <cmd> is the safe fallback for commands that need real
|
|
6329
6620
|
// arguments (run / resume / clear / validate / graph / daemon /
|
|
6330
6621
|
// import / completion). Print the usage so the user can re-launch
|
|
@@ -6455,9 +6746,13 @@ async function cmdLauncher() {
|
|
|
6455
6746
|
process.stdin.resume();
|
|
6456
6747
|
process.stdin.ref();
|
|
6457
6748
|
|
|
6749
|
+
const useLegacyBanner = !!process.env.LAZYCLAW_LEGACY_MENU;
|
|
6750
|
+
const bannerRowsCached = useLegacyBanner
|
|
6751
|
+
? _renderBanner(readVersionFromRepo())
|
|
6752
|
+
: await _renderV5Banner(readVersionFromRepo());
|
|
6458
6753
|
const draw = () => {
|
|
6459
6754
|
process.stdout.write('\x1b[?25l\x1b[2J\x1b[H'); // hide cursor + clear
|
|
6460
|
-
|
|
6755
|
+
bannerRowsCached.forEach((l) => process.stdout.write(l + '\n'));
|
|
6461
6756
|
process.stdout.write('\n');
|
|
6462
6757
|
process.stdout.write(` ${dim('provider ·')} ${ok(provider)}\n`);
|
|
6463
6758
|
process.stdout.write(` ${dim('model ·')} ${ok(model)}\n`);
|
|
@@ -6761,6 +7056,105 @@ async function main() {
|
|
|
6761
7056
|
}
|
|
6762
7057
|
break;
|
|
6763
7058
|
}
|
|
7059
|
+
case 'personality': {
|
|
7060
|
+
// Phase G: persona compose subcommand (spec §9, decision C7).
|
|
7061
|
+
process.exit(await cmdPersonality(rest.positional[0], rest.positional[1], rest.positional[2]));
|
|
7062
|
+
break;
|
|
7063
|
+
}
|
|
7064
|
+
case 'migrate': {
|
|
7065
|
+
// Phase A baseline accepts `lazyclaw migrate v5`; Phase G adds the
|
|
7066
|
+
// bare `lazyclaw migrate` and `lazyclaw migrate rollback` forms.
|
|
7067
|
+
const target = rest.positional[0];
|
|
7068
|
+
if (target === 'rollback') {
|
|
7069
|
+
const mod = await import('./scripts/migrate-v5.mjs');
|
|
7070
|
+
try {
|
|
7071
|
+
const { restoredFrom } = mod.rollback();
|
|
7072
|
+
console.log(`rolled back from ${restoredFrom}`);
|
|
7073
|
+
process.exit(0);
|
|
7074
|
+
} catch (e) {
|
|
7075
|
+
console.error(`migrate failed: ${e.message}`);
|
|
7076
|
+
process.exit(1);
|
|
7077
|
+
}
|
|
7078
|
+
break;
|
|
7079
|
+
}
|
|
7080
|
+
const mod = await import('./scripts/migrate-v5.mjs');
|
|
7081
|
+
// `migrate v5` keeps the Phase-A behaviour (verbose JSON); the
|
|
7082
|
+
// bare `migrate` form uses the Phase-G human summary.
|
|
7083
|
+
if (target === 'v5') {
|
|
7084
|
+
const r = await mod.migrateV5();
|
|
7085
|
+
console.log(JSON.stringify(r, null, 2));
|
|
7086
|
+
process.exit(r.ok ? 0 : 1);
|
|
7087
|
+
}
|
|
7088
|
+
try {
|
|
7089
|
+
const { backupDir } = mod.migrate();
|
|
7090
|
+
console.log(`migrated; backup at ${backupDir}`);
|
|
7091
|
+
process.exit(0);
|
|
7092
|
+
} catch (e) {
|
|
7093
|
+
console.error(`migrate failed: ${e.message}`);
|
|
7094
|
+
process.exit(1);
|
|
7095
|
+
}
|
|
7096
|
+
break;
|
|
7097
|
+
}
|
|
7098
|
+
case 'hermes': {
|
|
7099
|
+
// Phase G: import a Hermes Agent install (spec §10).
|
|
7100
|
+
if (rest.positional[0] !== 'import') {
|
|
7101
|
+
console.error('Usage: lazyclaw hermes import [--from <dir>]');
|
|
7102
|
+
process.exit(2);
|
|
7103
|
+
}
|
|
7104
|
+
const from = rest.flags.from;
|
|
7105
|
+
const mod = await import('./scripts/hermes-import.mjs');
|
|
7106
|
+
try {
|
|
7107
|
+
const { src, dst, counts } = mod.importHermes({ from });
|
|
7108
|
+
console.log(`hermes import: ${src} → ${dst}`);
|
|
7109
|
+
console.log(` skills: ${counts.skills} skins: ${counts.skins}`);
|
|
7110
|
+
process.exit(0);
|
|
7111
|
+
} catch (e) { console.error(`hermes import failed: ${e.message}`); process.exit(1); }
|
|
7112
|
+
break;
|
|
7113
|
+
}
|
|
7114
|
+
case 'openclaw': {
|
|
7115
|
+
// Phase G: import an OpenClaw install (spec §10).
|
|
7116
|
+
if (rest.positional[0] !== 'import') {
|
|
7117
|
+
console.error('Usage: lazyclaw openclaw import [--from <dir>]');
|
|
7118
|
+
process.exit(2);
|
|
7119
|
+
}
|
|
7120
|
+
const from = rest.flags.from;
|
|
7121
|
+
const mod = await import('./scripts/openclaw-import.mjs');
|
|
7122
|
+
try {
|
|
7123
|
+
const { src, dst, counts } = mod.importOpenclaw({ from });
|
|
7124
|
+
console.log(`openclaw import: ${src} → ${dst} skills:${counts.skills}`);
|
|
7125
|
+
process.exit(0);
|
|
7126
|
+
} catch (e) { console.error(`openclaw import failed: ${e.message}`); process.exit(1); }
|
|
7127
|
+
break;
|
|
7128
|
+
}
|
|
7129
|
+
case 'trajectories': {
|
|
7130
|
+
// Phase H1: read-only trajectory exporter (spec §2.7).
|
|
7131
|
+
// Usage: lazyclaw trajectories export --format <atropos|axolotl|openai-ft|jsonl>
|
|
7132
|
+
// [--since 7d] [--filter "outcome=done"] [--out ./dir]
|
|
7133
|
+
if (rest.positional[0] !== 'export') {
|
|
7134
|
+
console.error('Usage: lazyclaw trajectories export --format <atropos|axolotl|openai-ft|jsonl> [--since 7d] [--filter "outcome=done"] [--out <dir>]');
|
|
7135
|
+
process.exit(2);
|
|
7136
|
+
}
|
|
7137
|
+
const mod = await import('./mas/trajectory_export.mjs');
|
|
7138
|
+
const format = rest.flags.format || 'jsonl';
|
|
7139
|
+
if (!mod.FORMATS.includes(format)) {
|
|
7140
|
+
console.error(`trajectories export: unknown format "${format}" — choose ${mod.FORMATS.join('|')}`);
|
|
7141
|
+
process.exit(2);
|
|
7142
|
+
}
|
|
7143
|
+
try {
|
|
7144
|
+
const r = await mod.exportTrajectories({
|
|
7145
|
+
format,
|
|
7146
|
+
since: rest.flags.since,
|
|
7147
|
+
filter: mod.parseFilterArg(rest.flags.filter),
|
|
7148
|
+
outDir: rest.flags.out,
|
|
7149
|
+
});
|
|
7150
|
+
console.log(`exported ${r.count} trajectories (${r.format}) → ${r.outFile}`);
|
|
7151
|
+
process.exit(0);
|
|
7152
|
+
} catch (e) {
|
|
7153
|
+
console.error(`trajectories export failed: ${e.message}`);
|
|
7154
|
+
process.exit(1);
|
|
7155
|
+
}
|
|
7156
|
+
break;
|
|
7157
|
+
}
|
|
6764
7158
|
case 'chat': {
|
|
6765
7159
|
await cmdChat(rest.flags);
|
|
6766
7160
|
break;
|
|
@@ -6790,6 +7184,10 @@ async function main() {
|
|
|
6790
7184
|
await cmdRates(sub, rest.positional.slice(1), rest.flags);
|
|
6791
7185
|
break;
|
|
6792
7186
|
}
|
|
7187
|
+
case 'sandbox': {
|
|
7188
|
+
process.exit(await cmdSandbox(rest.positional, rest.flags));
|
|
7189
|
+
break;
|
|
7190
|
+
}
|
|
6793
7191
|
case 'auth': {
|
|
6794
7192
|
const sub = rest.positional[0];
|
|
6795
7193
|
await cmdAuth(sub, rest.positional.slice(1), rest.flags);
|
|
@@ -6877,6 +7275,31 @@ async function main() {
|
|
|
6877
7275
|
await cmdDashboard(rest.flags);
|
|
6878
7276
|
break;
|
|
6879
7277
|
}
|
|
7278
|
+
case 'channels': {
|
|
7279
|
+
const sub = (rest.positional[0] || 'list').toLowerCase();
|
|
7280
|
+
const { createLoader, listInstalled } = await import('./channels/loader.mjs');
|
|
7281
|
+
const cfgDir = path.dirname(configPath());
|
|
7282
|
+
const loader = createLoader({ configDir: cfgDir });
|
|
7283
|
+
if (sub === 'install') {
|
|
7284
|
+
const name = rest.positional[1];
|
|
7285
|
+
if (!name) { process.stderr.write('usage: lazyclaw channels install <@lazyclaw/channel-name>\n'); process.exit(2); }
|
|
7286
|
+
const info = await loader.install(name);
|
|
7287
|
+
process.stdout.write(`installed ${info.name}@${info.version}\n`);
|
|
7288
|
+
break;
|
|
7289
|
+
}
|
|
7290
|
+
if (sub === 'remove' || sub === 'uninstall') {
|
|
7291
|
+
const name = rest.positional[1];
|
|
7292
|
+
if (!name) { process.stderr.write('usage: lazyclaw channels remove <@lazyclaw/channel-name>\n'); process.exit(2); }
|
|
7293
|
+
await loader.remove(name);
|
|
7294
|
+
process.stdout.write(`removed ${name}\n`);
|
|
7295
|
+
break;
|
|
7296
|
+
}
|
|
7297
|
+
// list
|
|
7298
|
+
const rows = listInstalled(cfgDir);
|
|
7299
|
+
if (!rows.length) { process.stdout.write('no channel plugins installed\n'); break; }
|
|
7300
|
+
for (const r of rows) process.stdout.write(`${r.name}\t${r.version}\n`);
|
|
7301
|
+
break;
|
|
7302
|
+
}
|
|
6880
7303
|
case 'daemon': {
|
|
6881
7304
|
await cmdDaemon(rest.flags);
|
|
6882
7305
|
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
|
}
|