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.
Files changed (54) hide show
  1. package/README.ko.md +44 -0
  2. package/README.md +172 -508
  3. package/channels/handoff.mjs +41 -0
  4. package/channels/loader.mjs +124 -0
  5. package/channels/threads.mjs +116 -0
  6. package/cli.mjs +430 -7
  7. package/daemon.mjs +13 -0
  8. package/mas/agent_turn.mjs +28 -0
  9. package/mas/confidence.mjs +108 -0
  10. package/mas/index_db.mjs +242 -0
  11. package/mas/nudge.mjs +97 -0
  12. package/mas/prompt_stack.mjs +80 -0
  13. package/mas/skill_synth.mjs +124 -25
  14. package/mas/tool_runner.mjs +10 -61
  15. package/mas/tools/browser.mjs +77 -0
  16. package/mas/tools/clarify.mjs +36 -0
  17. package/mas/tools/coding.mjs +109 -0
  18. package/mas/tools/delegation.mjs +53 -0
  19. package/mas/tools/edit.mjs +36 -0
  20. package/mas/tools/git.mjs +110 -0
  21. package/mas/tools/ha.mjs +34 -0
  22. package/mas/tools/learning.mjs +168 -0
  23. package/mas/tools/media.mjs +105 -0
  24. package/mas/tools/os.mjs +152 -0
  25. package/mas/tools/patch.mjs +91 -0
  26. package/mas/tools/recall.mjs +103 -0
  27. package/mas/tools/registry.mjs +93 -0
  28. package/mas/tools/scheduling.mjs +62 -0
  29. package/mas/tools/web.mjs +137 -0
  30. package/mas/toolsets.mjs +64 -0
  31. package/mas/trajectory_export.mjs +169 -0
  32. package/mas/trajectory_store.mjs +179 -0
  33. package/mas/user_modeler.mjs +108 -0
  34. package/package.json +20 -3
  35. package/providers/codex_cli.mjs +200 -0
  36. package/providers/gemini_cli.mjs +179 -0
  37. package/providers/registry.mjs +61 -1
  38. package/sandbox/base.mjs +82 -0
  39. package/sandbox/confiners/bubblewrap.mjs +21 -0
  40. package/sandbox/confiners/firejail.mjs +16 -0
  41. package/sandbox/confiners/landlock.mjs +14 -0
  42. package/sandbox/confiners/seatbelt.mjs +28 -0
  43. package/sandbox/daytona.mjs +37 -0
  44. package/sandbox/docker.mjs +91 -0
  45. package/sandbox/index.mjs +67 -0
  46. package/sandbox/local.mjs +59 -0
  47. package/sandbox/modal.mjs +53 -0
  48. package/sandbox/singularity.mjs +39 -0
  49. package/sandbox/ssh.mjs +56 -0
  50. package/sandbox.mjs +11 -127
  51. package/scripts/hermes-import.mjs +111 -0
  52. package/scripts/migrate-v5.mjs +342 -0
  53. package/scripts/openclaw-import.mjs +71 -0
  54. package/sessions.mjs +20 -1
@@ -0,0 +1,41 @@
1
+ // channels/handoff.mjs
2
+ //
3
+ // Migrates an active thread (sessionId) from one channel to another.
4
+ // Pure function over (threads store, live channel map) — the CLI slash
5
+ // and the daemon HTTP route both call this.
6
+
7
+ export async function runHandoff({ threads, channels, threadId, target, externalId, note = '' }) {
8
+ const cur = threads.findByThread(threadId);
9
+ if (!cur) {
10
+ const err = new Error(`THREAD_NOT_FOUND: ${threadId}`);
11
+ err.code = 'THREAD_NOT_FOUND';
12
+ throw err;
13
+ }
14
+ if (!channels[target] || typeof channels[target].send !== 'function') {
15
+ const err = new Error(`CHANNEL_NOT_AVAILABLE: ${target}`);
16
+ err.code = 'CHANNEL_NOT_AVAILABLE';
17
+ throw err;
18
+ }
19
+ const srcChannel = cur.channel;
20
+ const srcExternal = cur.externalId;
21
+
22
+ // 1. Persist the migration first so a crash mid-notify leaves us in the new home.
23
+ const next = threads.handoff(threadId, { channel: target, externalId });
24
+
25
+ // 2. Notify source (best-effort) so the human knows where the convo went.
26
+ const tail = note ? ` — ${note}` : '';
27
+ if (channels[srcChannel] && typeof channels[srcChannel].send === 'function') {
28
+ try {
29
+ await channels[srcChannel].send(srcExternal,
30
+ `handoff: this conversation moved to ${target}${tail}`);
31
+ } catch (e) {
32
+ process.stderr.write(`[handoff] source notify failed: ${e.message}\n`);
33
+ }
34
+ }
35
+
36
+ // 3. Notify target with a resume marker.
37
+ await channels[target].send(externalId,
38
+ `resumed from ${srcChannel} (session ${next.sessionId})${tail}`);
39
+
40
+ return next;
41
+ }
@@ -0,0 +1,124 @@
1
+ // channels/loader.mjs
2
+ //
3
+ // Plugin loader for @lazyclaw/channel-<name> packages. Installs into
4
+ // <configDir>/node_modules via `npm install <spec>` and dynamic-imports
5
+ // the entry, calling the package's exported register({Channel, addChannel}).
6
+
7
+ import * as fs from 'node:fs';
8
+ import * as path from 'node:path';
9
+ import { pathToFileURL } from 'node:url';
10
+ import { spawnSync } from 'node:child_process';
11
+ import { Channel } from './base.mjs';
12
+
13
+ const PLUGIN_RE = /^@lazyclaw\/channel-[a-z][a-z0-9-]*$/;
14
+
15
+ export function isPluginName(name) {
16
+ return typeof name === 'string' && PLUGIN_RE.test(name);
17
+ }
18
+
19
+ export function listInstalled(configDir) {
20
+ const root = path.join(String(configDir), 'node_modules', '@lazyclaw');
21
+ if (!fs.existsSync(root)) return [];
22
+ const out = [];
23
+ for (const entry of fs.readdirSync(root)) {
24
+ if (!entry.startsWith('channel-')) continue;
25
+ const pj = path.join(root, entry, 'package.json');
26
+ if (!fs.existsSync(pj)) continue;
27
+ try {
28
+ const meta = JSON.parse(fs.readFileSync(pj, 'utf8'));
29
+ if (!isPluginName(meta.name)) continue;
30
+ out.push({ name: meta.name, version: meta.version || '0.0.0' });
31
+ } catch { /* skip */ }
32
+ }
33
+ return out;
34
+ }
35
+
36
+ export function createLoader({ configDir, skipInstall = false, npmBin = 'npm' } = {}) {
37
+ if (!configDir) throw new Error('createLoader: configDir required');
38
+ fs.mkdirSync(configDir, { recursive: true });
39
+
40
+ /** @type {Map<string, (opts:any)=>Channel>} */
41
+ const factories = new Map();
42
+
43
+ function addChannel(kind, factory) {
44
+ if (typeof factory !== 'function') {
45
+ throw new Error(`plugin "${kind}" register() must call addChannel(name, factory)`);
46
+ }
47
+ factories.set(kind, factory);
48
+ }
49
+
50
+ function getFactory(kind) {
51
+ return factories.get(kind) || null;
52
+ }
53
+
54
+ function listKinds() {
55
+ return Array.from(factories.keys()).sort();
56
+ }
57
+
58
+ async function loadFromPath(declaredName, pkgDir) {
59
+ if (!isPluginName(declaredName)) {
60
+ const err = new Error(`INVALID_PLUGIN_NAME: ${declaredName}`);
61
+ err.code = 'INVALID_PLUGIN_NAME';
62
+ throw err;
63
+ }
64
+ const pj = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8'));
65
+ const entryRel = pj.main || 'index.mjs';
66
+ const entry = path.join(pkgDir, entryRel);
67
+ const mod = await import(pathToFileURL(entry).href);
68
+ if (typeof mod.register !== 'function') {
69
+ throw new Error(`plugin ${declaredName} missing register() export`);
70
+ }
71
+ await mod.register({ Channel, addChannel });
72
+ return { name: declaredName, version: pj.version || '0.0.0' };
73
+ }
74
+
75
+ async function install(name) {
76
+ if (!isPluginName(name)) {
77
+ const err = new Error(`INVALID_PLUGIN_NAME: ${name}`);
78
+ err.code = 'INVALID_PLUGIN_NAME';
79
+ throw err;
80
+ }
81
+ if (!skipInstall) {
82
+ const res = spawnSync(npmBin, ['install', '--no-audit', '--no-fund', name], {
83
+ cwd: configDir, stdio: 'inherit', env: process.env,
84
+ });
85
+ if (res.status !== 0) {
86
+ throw new Error(`npm install ${name} exited ${res.status}`);
87
+ }
88
+ }
89
+ const pkgDir = path.join(configDir, 'node_modules', name);
90
+ return loadFromPath(name, pkgDir);
91
+ }
92
+
93
+ async function remove(name) {
94
+ if (!isPluginName(name)) {
95
+ const err = new Error(`INVALID_PLUGIN_NAME: ${name}`);
96
+ err.code = 'INVALID_PLUGIN_NAME';
97
+ throw err;
98
+ }
99
+ const res = spawnSync(npmBin, ['uninstall', name], {
100
+ cwd: configDir, stdio: 'inherit', env: process.env,
101
+ });
102
+ if (res.status !== 0) throw new Error(`npm uninstall ${name} exited ${res.status}`);
103
+ // factories map stays — restart of daemon picks up the change.
104
+ }
105
+
106
+ async function loadAllInstalled() {
107
+ const installed = listInstalled(configDir);
108
+ const loaded = [];
109
+ for (const { name } of installed) {
110
+ try {
111
+ const pkgDir = path.join(configDir, 'node_modules', name);
112
+ loaded.push(await loadFromPath(name, pkgDir));
113
+ } catch (e) {
114
+ process.stderr.write(`[channels] failed to load ${name}: ${e.message}\n`);
115
+ }
116
+ }
117
+ return loaded;
118
+ }
119
+
120
+ return {
121
+ addChannel, getFactory, listKinds,
122
+ loadFromPath, loadAllInstalled, install, remove,
123
+ };
124
+ }
@@ -0,0 +1,116 @@
1
+ // channels/threads.mjs
2
+ //
3
+ // threadId -> { channel, externalId, sessionId, lastTurnAt } JSONL store.
4
+ // Append-only on disk; in-memory map for read. The threadId is stable
5
+ // across /handoff so cross-channel migrations preserve session context.
6
+
7
+ import * as fs from 'node:fs';
8
+ import * as path from 'node:path';
9
+ import * as crypto from 'node:crypto';
10
+
11
+ const FILE = 'threads.jsonl';
12
+
13
+ function readAll(file) {
14
+ if (!fs.existsSync(file)) return [];
15
+ const raw = fs.readFileSync(file, 'utf8');
16
+ const out = [];
17
+ for (const line of raw.split('\n')) {
18
+ if (!line) continue;
19
+ try { out.push(JSON.parse(line)); } catch { /* skip corrupt */ }
20
+ }
21
+ return out;
22
+ }
23
+
24
+ function newThreadId() {
25
+ return 'th_' + crypto.randomBytes(8).toString('hex');
26
+ }
27
+
28
+ export function openThreads(configDir) {
29
+ const dir = String(configDir || '.');
30
+ fs.mkdirSync(dir, { recursive: true });
31
+ const file = path.join(dir, FILE);
32
+
33
+ /** @type {Map<string, {threadId,channel,externalId,sessionId,lastTurnAt}>} */
34
+ const byThread = new Map();
35
+ /** @type {Map<string, string>} channel|externalId -> threadId */
36
+ const byExternal = new Map();
37
+
38
+ function externalKey(channel, externalId) {
39
+ return `${channel}|${externalId}`;
40
+ }
41
+
42
+ function apply(row) {
43
+ if (row.op === 'delete') {
44
+ const existing = byThread.get(row.threadId);
45
+ if (existing) {
46
+ byExternal.delete(externalKey(existing.channel, existing.externalId));
47
+ byThread.delete(row.threadId);
48
+ }
49
+ return;
50
+ }
51
+ const prev = byThread.get(row.threadId);
52
+ if (prev) byExternal.delete(externalKey(prev.channel, prev.externalId));
53
+ byThread.set(row.threadId, {
54
+ threadId: row.threadId,
55
+ channel: row.channel,
56
+ externalId: row.externalId,
57
+ sessionId: row.sessionId,
58
+ lastTurnAt: row.lastTurnAt,
59
+ });
60
+ byExternal.set(externalKey(row.channel, row.externalId), row.threadId);
61
+ }
62
+
63
+ for (const row of readAll(file)) apply(row);
64
+
65
+ function append(row) {
66
+ fs.appendFileSync(file, JSON.stringify(row) + '\n');
67
+ apply(row);
68
+ }
69
+
70
+ function upsert({ channel, externalId, sessionId, threadId }) {
71
+ if (!channel || !externalId || !sessionId) {
72
+ throw new Error('upsert requires channel, externalId, sessionId');
73
+ }
74
+ const existingId = byExternal.get(externalKey(channel, externalId));
75
+ const id = threadId || existingId || newThreadId();
76
+ const row = {
77
+ op: 'upsert', threadId: id, channel, externalId, sessionId,
78
+ lastTurnAt: Date.now(),
79
+ };
80
+ append(row);
81
+ return byThread.get(id);
82
+ }
83
+
84
+ function findByExternal(channel, externalId) {
85
+ const id = byExternal.get(externalKey(channel, externalId));
86
+ return id ? byThread.get(id) : null;
87
+ }
88
+
89
+ function findByThread(threadId) {
90
+ return byThread.get(threadId) || null;
91
+ }
92
+
93
+ function handoff(threadId, { channel, externalId }) {
94
+ const cur = byThread.get(threadId);
95
+ if (!cur) {
96
+ const err = new Error(`THREAD_NOT_FOUND: ${threadId}`);
97
+ err.code = 'THREAD_NOT_FOUND';
98
+ throw err;
99
+ }
100
+ if (!channel || !externalId) {
101
+ throw new Error('handoff requires channel and externalId');
102
+ }
103
+ const row = {
104
+ op: 'upsert', threadId, channel, externalId,
105
+ sessionId: cur.sessionId, lastTurnAt: Date.now(),
106
+ };
107
+ append(row);
108
+ return byThread.get(threadId);
109
+ }
110
+
111
+ function list() {
112
+ return Array.from(byThread.values());
113
+ }
114
+
115
+ return { upsert, findByExternal, findByThread, handoff, list };
116
+ }